Skip to content

Commit 0bbf26a

Browse files
authored
deslopity deslopity (#18343)
1 parent 83cdb4d commit 0bbf26a

File tree

1 file changed

+144
-133
lines changed

1 file changed

+144
-133
lines changed

packages/ui/src/components/message-part.tsx

Lines changed: 144 additions & 133 deletions
Original file line numberDiff line numberDiff line change
@@ -4,15 +4,15 @@ import {
44
createMemo,
55
createSignal,
66
For,
7-
Index,
87
Match,
98
onMount,
109
Show,
1110
Switch,
1211
onCleanup,
12+
Index,
1313
type JSX,
1414
} from "solid-js"
15-
import { createStore, unwrap } from "solid-js/store"
15+
import { createStore } from "solid-js/store"
1616
import stripAnsi from "strip-ansi"
1717
import { Dynamic } from "solid-js/web"
1818
import {
@@ -481,15 +481,6 @@ function partDefaultOpen(part: PartType, shell = false, edit = false) {
481481
return toolDefaultOpen(part.tool, shell, edit)
482482
}
483483

484-
function bindMessage<T extends MessageType>(input: T) {
485-
const data = useData()
486-
const base = structuredClone(unwrap(input)) as T
487-
return createMemo(() => {
488-
const next = data.store.message?.[base.sessionID]?.find((item) => item.id === base.id)
489-
return (next as T | undefined) ?? base
490-
})
491-
}
492-
493484
export function AssistantParts(props: {
494485
messages: AssistantMessage[]
495486
showAssistantCopyPartID?: string | null
@@ -530,55 +521,62 @@ export function AssistantParts(props: {
530521

531522
return (
532523
<Index each={grouped()}>
533-
{(entry) => {
534-
const kind = createMemo(() => entry().type)
535-
const parts = createMemo(
536-
() => {
537-
const value = entry()
538-
if (value.type !== "context") return emptyTools
539-
return value.refs
540-
.map((ref) => part().get(ref.messageID)?.get(ref.partID))
541-
.filter((part): part is ToolPart => !!part && isContextGroupTool(part))
542-
},
543-
emptyTools,
544-
{ equals: same },
545-
)
546-
const busy = createMemo(() => props.working && last() === entry().key)
547-
const message = createMemo(() => {
548-
const value = entry()
549-
if (value.type !== "part") return
550-
return msgs().get(value.ref.messageID)
551-
})
552-
const item = createMemo(() => {
553-
const value = entry()
554-
if (value.type !== "part") return
555-
return part().get(value.ref.messageID)?.get(value.ref.partID)
556-
})
557-
const ready = createMemo(() => {
558-
if (kind() !== "part") return
559-
const msg = message()
560-
const value = item()
561-
if (!msg || !value) return
562-
return { msg, value }
563-
})
524+
{(entryAccessor) => {
525+
const entryType = createMemo(() => entryAccessor().type)
564526

565527
return (
566-
<>
567-
<Show when={kind() === "context" && parts().length > 0}>
568-
<ContextToolGroup parts={parts()} busy={busy()} />
569-
</Show>
570-
<Show when={ready()}>
571-
{(ready) => (
572-
<Part
573-
part={ready().value}
574-
message={ready().msg}
575-
showAssistantCopyPartID={props.showAssistantCopyPartID}
576-
turnDurationMs={props.turnDurationMs}
577-
defaultOpen={partDefaultOpen(ready().value, props.shellToolDefaultOpen, props.editToolDefaultOpen)}
578-
/>
579-
)}
580-
</Show>
581-
</>
528+
<Switch>
529+
<Match when={entryType() === "context"}>
530+
{(() => {
531+
const parts = createMemo(
532+
() => {
533+
const entry = entryAccessor()
534+
if (entry.type !== "context") return emptyTools
535+
return entry.refs
536+
.map((ref) => part().get(ref.messageID)?.get(ref.partID))
537+
.filter((part): part is ToolPart => !!part && isContextGroupTool(part))
538+
},
539+
emptyTools,
540+
{ equals: same },
541+
)
542+
const busy = createMemo(() => props.working && last() === entryAccessor().key)
543+
544+
return (
545+
<Show when={parts().length > 0}>
546+
<ContextToolGroup parts={parts()} busy={busy()} />
547+
</Show>
548+
)
549+
})()}
550+
</Match>
551+
<Match when={entryType() === "part"}>
552+
{(() => {
553+
const message = createMemo(() => {
554+
const entry = entryAccessor()
555+
if (entry.type !== "part") return
556+
return msgs().get(entry.ref.messageID)
557+
})
558+
const item = createMemo(() => {
559+
const entry = entryAccessor()
560+
if (entry.type !== "part") return
561+
return part().get(entry.ref.messageID)?.get(entry.ref.partID)
562+
})
563+
564+
return (
565+
<Show when={message()}>
566+
<Show when={item()}>
567+
<Part
568+
part={item()!}
569+
message={message()!}
570+
showAssistantCopyPartID={props.showAssistantCopyPartID}
571+
turnDurationMs={props.turnDurationMs}
572+
defaultOpen={partDefaultOpen(item()!, props.shellToolDefaultOpen, props.editToolDefaultOpen)}
573+
/>
574+
</Show>
575+
</Show>
576+
)
577+
})()}
578+
</Match>
579+
</Switch>
582580
)
583581
}}
584582
</Index>
@@ -690,22 +688,25 @@ export function registerPartComponent(type: string, component: PartComponent) {
690688
}
691689

692690
export function Message(props: MessageProps) {
693-
if (props.message.role === "user") {
694-
return <UserMessageDisplay message={props.message as UserMessage} parts={props.parts} actions={props.actions} />
695-
}
696-
697-
if (props.message.role === "assistant") {
698-
return (
699-
<AssistantMessageDisplay
700-
message={props.message as AssistantMessage}
701-
parts={props.parts}
702-
showAssistantCopyPartID={props.showAssistantCopyPartID}
703-
showReasoningSummaries={props.showReasoningSummaries}
704-
/>
705-
)
706-
}
707-
708-
return undefined
691+
return (
692+
<Switch>
693+
<Match when={props.message.role === "user" && props.message}>
694+
{(userMessage) => (
695+
<UserMessageDisplay message={userMessage() as UserMessage} parts={props.parts} actions={props.actions} />
696+
)}
697+
</Match>
698+
<Match when={props.message.role === "assistant" && props.message}>
699+
{(assistantMessage) => (
700+
<AssistantMessageDisplay
701+
message={assistantMessage() as AssistantMessage}
702+
parts={props.parts}
703+
showAssistantCopyPartID={props.showAssistantCopyPartID}
704+
showReasoningSummaries={props.showReasoningSummaries}
705+
/>
706+
)}
707+
</Match>
708+
</Switch>
709+
)
709710
}
710711

711712
export function AssistantMessageDisplay(props: {
@@ -732,42 +733,52 @@ export function AssistantMessageDisplay(props: {
732733

733734
return (
734735
<Index each={grouped()}>
735-
{(entry) => {
736-
const kind = createMemo(() => entry().type)
737-
const parts = createMemo(
738-
() => {
739-
const value = entry()
740-
if (value.type !== "context") return emptyTools
741-
return value.refs
742-
.map((ref) => part().get(ref.partID))
743-
.filter((part): part is ToolPart => !!part && isContextGroupTool(part))
744-
},
745-
emptyTools,
746-
{ equals: same },
747-
)
748-
const item = createMemo(() => {
749-
const value = entry()
750-
if (value.type !== "part") return
751-
return part().get(value.ref.partID)
752-
})
753-
const ready = createMemo(() => {
754-
if (kind() !== "part") return
755-
const value = item()
756-
if (!value) return
757-
return value
758-
})
736+
{(entryAccessor) => {
737+
const entryType = createMemo(() => entryAccessor().type)
759738

760739
return (
761-
<>
762-
<Show when={kind() === "context" && parts().length > 0}>
763-
<ContextToolGroup parts={parts()} />
764-
</Show>
765-
<Show when={ready()}>
766-
{(ready) => (
767-
<Part part={ready()} message={props.message} showAssistantCopyPartID={props.showAssistantCopyPartID} />
768-
)}
769-
</Show>
770-
</>
740+
<Switch>
741+
<Match when={entryType() === "context"}>
742+
{(() => {
743+
const parts = createMemo(
744+
() => {
745+
const entry = entryAccessor()
746+
if (entry.type !== "context") return emptyTools
747+
return entry.refs
748+
.map((ref) => part().get(ref.partID))
749+
.filter((part): part is ToolPart => !!part && isContextGroupTool(part))
750+
},
751+
emptyTools,
752+
{ equals: same },
753+
)
754+
755+
return (
756+
<Show when={parts().length > 0}>
757+
<ContextToolGroup parts={parts()} />
758+
</Show>
759+
)
760+
})()}
761+
</Match>
762+
<Match when={entryType() === "part"}>
763+
{(() => {
764+
const item = createMemo(() => {
765+
const entry = entryAccessor()
766+
if (entry.type !== "part") return
767+
return part().get(entry.ref.partID)
768+
})
769+
770+
return (
771+
<Show when={item()}>
772+
<Part
773+
part={item()!}
774+
message={props.message}
775+
showAssistantCopyPartID={props.showAssistantCopyPartID}
776+
/>
777+
</Show>
778+
)
779+
})()}
780+
</Match>
781+
</Switch>
771782
)
772783
}}
773784
</Index>
@@ -834,9 +845,11 @@ function ContextToolGroup(props: { parts: ToolPart[]; busy?: boolean }) {
834845
<Collapsible.Content>
835846
<div data-component="context-tool-group-list">
836847
<Index each={props.parts}>
837-
{(part) => {
838-
const trigger = createMemo(() => contextToolTrigger(part(), i18n))
839-
const running = createMemo(() => part().state.status === "pending" || part().state.status === "running")
848+
{(partAccessor) => {
849+
const trigger = createMemo(() => contextToolTrigger(partAccessor(), i18n))
850+
const running = createMemo(
851+
() => partAccessor().state.status === "pending" || partAccessor().state.status === "running",
852+
)
840853
return (
841854
<div data-slot="context-tool-group-item">
842855
<div data-component="tool-trigger">
@@ -874,7 +887,6 @@ export function UserMessageDisplay(props: { message: UserMessage; parts: PartTyp
874887
const data = useData()
875888
const dialog = useDialog()
876889
const i18n = useI18n()
877-
const message = bindMessage(props.message)
878890
const [state, setState] = createStore({
879891
copied: false,
880892
busy: undefined as "fork" | "revert" | undefined,
@@ -897,22 +909,22 @@ export function UserMessageDisplay(props: { message: UserMessage; parts: PartTyp
897909
const agents = createMemo(() => (props.parts?.filter((p) => p.type === "agent") as AgentPart[]) ?? [])
898910

899911
const model = createMemo(() => {
900-
const providerID = message().model?.providerID
901-
const modelID = message().model?.modelID
912+
const providerID = props.message.model?.providerID
913+
const modelID = props.message.model?.modelID
902914
if (!providerID || !modelID) return ""
903915
const match = data.store.provider?.all?.find((p) => p.id === providerID)
904916
return match?.models?.[modelID]?.name ?? modelID
905917
})
906918
const timefmt = createMemo(() => new Intl.DateTimeFormat(i18n.locale(), { timeStyle: "short" }))
907919

908920
const stamp = createMemo(() => {
909-
const created = message().time?.created
921+
const created = props.message.time?.created
910922
if (typeof created !== "number") return ""
911923
return timefmt().format(created)
912924
})
913925

914926
const metaHead = createMemo(() => {
915-
const agent = message().agent
927+
const agent = props.message.agent
916928
const items = [agent ? agent[0]?.toUpperCase() + agent.slice(1) : "", model()]
917929
return items.filter((x) => !!x).join("\u00A0\u00B7\u00A0")
918930
})
@@ -938,8 +950,8 @@ export function UserMessageDisplay(props: { message: UserMessage; parts: PartTyp
938950
void Promise.resolve()
939951
.then(() =>
940952
act({
941-
sessionID: message().sessionID,
942-
messageID: message().id,
953+
sessionID: props.message.sessionID,
954+
messageID: props.message.id,
943955
}),
944956
)
945957
.finally(() => {
@@ -1298,27 +1310,27 @@ PART_MAPPING["text"] = function TextPartDisplay(props) {
12981310
const i18n = useI18n()
12991311
const numfmt = createMemo(() => new Intl.NumberFormat(i18n.locale()))
13001312
const part = () => props.part as TextPart
1301-
const message = bindMessage(props.message)
13021313
const interrupted = createMemo(
1303-
() => message().role === "assistant" && (message() as AssistantMessage).error?.name === "MessageAbortedError",
1314+
() =>
1315+
props.message.role === "assistant" && (props.message as AssistantMessage).error?.name === "MessageAbortedError",
13041316
)
13051317

13061318
const model = createMemo(() => {
1307-
const current = message()
1308-
if (current.role !== "assistant") return ""
1309-
const match = data.store.provider?.all?.find((p) => p.id === current.providerID)
1310-
return match?.models?.[current.modelID]?.name ?? current.modelID
1319+
if (props.message.role !== "assistant") return ""
1320+
const message = props.message as AssistantMessage
1321+
const match = data.store.provider?.all?.find((p) => p.id === message.providerID)
1322+
return match?.models?.[message.modelID]?.name ?? message.modelID
13111323
})
13121324

13131325
const duration = createMemo(() => {
1314-
const current = message()
1315-
if (current.role !== "assistant") return ""
1316-
const completed = current.time.completed
1326+
if (props.message.role !== "assistant") return ""
1327+
const message = props.message as AssistantMessage
1328+
const completed = message.time.completed
13171329
const ms =
13181330
typeof props.turnDurationMs === "number"
13191331
? props.turnDurationMs
13201332
: typeof completed === "number"
1321-
? completed - current.time.created
1333+
? completed - message.time.created
13221334
: -1
13231335
if (!(ms >= 0)) return ""
13241336
const total = Math.round(ms / 1000)
@@ -1332,9 +1344,8 @@ PART_MAPPING["text"] = function TextPartDisplay(props) {
13321344
})
13331345

13341346
const meta = createMemo(() => {
1335-
const current = message()
1336-
if (current.role !== "assistant") return ""
1337-
const agent = current.agent
1347+
if (props.message.role !== "assistant") return ""
1348+
const agent = (props.message as AssistantMessage).agent
13381349
const items = [
13391350
agent ? agent[0]?.toUpperCase() + agent.slice(1) : "",
13401351
model(),
@@ -1347,13 +1358,13 @@ PART_MAPPING["text"] = function TextPartDisplay(props) {
13471358
const displayText = () => (part().text ?? "").trim()
13481359
const throttledText = createThrottledValue(displayText)
13491360
const isLastTextPart = createMemo(() => {
1350-
const last = (data.store.part?.[message().id] ?? [])
1361+
const last = (data.store.part?.[props.message.id] ?? [])
13511362
.filter((item): item is TextPart => item?.type === "text" && !!item.text?.trim())
13521363
.at(-1)
13531364
return last?.id === part().id
13541365
})
13551366
const showCopy = createMemo(() => {
1356-
if (message().role !== "assistant") return isLastTextPart()
1367+
if (props.message.role !== "assistant") return isLastTextPart()
13571368
if (props.showAssistantCopyPartID === null) return false
13581369
if (typeof props.showAssistantCopyPartID === "string") return props.showAssistantCopyPartID === part().id
13591370
return isLastTextPart()

0 commit comments

Comments
 (0)