deslopity deslopity (#18343)

pull/18253/head^2
Luke Parker 2026-03-20 15:24:27 +10:00 committed by GitHub
parent 83cdb4de64
commit 0bbf26a1ce
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
1 changed files with 144 additions and 133 deletions

View File

@ -4,15 +4,15 @@ import {
createMemo, createMemo,
createSignal, createSignal,
For, For,
Index,
Match, Match,
onMount, onMount,
Show, Show,
Switch, Switch,
onCleanup, onCleanup,
Index,
type JSX, type JSX,
} from "solid-js" } from "solid-js"
import { createStore, unwrap } from "solid-js/store" import { createStore } from "solid-js/store"
import stripAnsi from "strip-ansi" import stripAnsi from "strip-ansi"
import { Dynamic } from "solid-js/web" import { Dynamic } from "solid-js/web"
import { import {
@ -481,15 +481,6 @@ function partDefaultOpen(part: PartType, shell = false, edit = false) {
return toolDefaultOpen(part.tool, shell, edit) return toolDefaultOpen(part.tool, shell, edit)
} }
function bindMessage<T extends MessageType>(input: T) {
const data = useData()
const base = structuredClone(unwrap(input)) as T
return createMemo(() => {
const next = data.store.message?.[base.sessionID]?.find((item) => item.id === base.id)
return (next as T | undefined) ?? base
})
}
export function AssistantParts(props: { export function AssistantParts(props: {
messages: AssistantMessage[] messages: AssistantMessage[]
showAssistantCopyPartID?: string | null showAssistantCopyPartID?: string | null
@ -530,55 +521,62 @@ export function AssistantParts(props: {
return ( return (
<Index each={grouped()}> <Index each={grouped()}>
{(entry) => { {(entryAccessor) => {
const kind = createMemo(() => entry().type) const entryType = createMemo(() => entryAccessor().type)
const parts = createMemo(
() => {
const value = entry()
if (value.type !== "context") return emptyTools
return value.refs
.map((ref) => part().get(ref.messageID)?.get(ref.partID))
.filter((part): part is ToolPart => !!part && isContextGroupTool(part))
},
emptyTools,
{ equals: same },
)
const busy = createMemo(() => props.working && last() === entry().key)
const message = createMemo(() => {
const value = entry()
if (value.type !== "part") return
return msgs().get(value.ref.messageID)
})
const item = createMemo(() => {
const value = entry()
if (value.type !== "part") return
return part().get(value.ref.messageID)?.get(value.ref.partID)
})
const ready = createMemo(() => {
if (kind() !== "part") return
const msg = message()
const value = item()
if (!msg || !value) return
return { msg, value }
})
return ( return (
<> <Switch>
<Show when={kind() === "context" && parts().length > 0}> <Match when={entryType() === "context"}>
<ContextToolGroup parts={parts()} busy={busy()} /> {(() => {
</Show> const parts = createMemo(
<Show when={ready()}> () => {
{(ready) => ( const entry = entryAccessor()
<Part if (entry.type !== "context") return emptyTools
part={ready().value} return entry.refs
message={ready().msg} .map((ref) => part().get(ref.messageID)?.get(ref.partID))
showAssistantCopyPartID={props.showAssistantCopyPartID} .filter((part): part is ToolPart => !!part && isContextGroupTool(part))
turnDurationMs={props.turnDurationMs} },
defaultOpen={partDefaultOpen(ready().value, props.shellToolDefaultOpen, props.editToolDefaultOpen)} emptyTools,
/> { equals: same },
)} )
</Show> const busy = createMemo(() => props.working && last() === entryAccessor().key)
</>
return (
<Show when={parts().length > 0}>
<ContextToolGroup parts={parts()} busy={busy()} />
</Show>
)
})()}
</Match>
<Match when={entryType() === "part"}>
{(() => {
const message = createMemo(() => {
const entry = entryAccessor()
if (entry.type !== "part") return
return msgs().get(entry.ref.messageID)
})
const item = createMemo(() => {
const entry = entryAccessor()
if (entry.type !== "part") return
return part().get(entry.ref.messageID)?.get(entry.ref.partID)
})
return (
<Show when={message()}>
<Show when={item()}>
<Part
part={item()!}
message={message()!}
showAssistantCopyPartID={props.showAssistantCopyPartID}
turnDurationMs={props.turnDurationMs}
defaultOpen={partDefaultOpen(item()!, props.shellToolDefaultOpen, props.editToolDefaultOpen)}
/>
</Show>
</Show>
)
})()}
</Match>
</Switch>
) )
}} }}
</Index> </Index>
@ -690,22 +688,25 @@ export function registerPartComponent(type: string, component: PartComponent) {
} }
export function Message(props: MessageProps) { export function Message(props: MessageProps) {
if (props.message.role === "user") { return (
return <UserMessageDisplay message={props.message as UserMessage} parts={props.parts} actions={props.actions} /> <Switch>
} <Match when={props.message.role === "user" && props.message}>
{(userMessage) => (
if (props.message.role === "assistant") { <UserMessageDisplay message={userMessage() as UserMessage} parts={props.parts} actions={props.actions} />
return ( )}
<AssistantMessageDisplay </Match>
message={props.message as AssistantMessage} <Match when={props.message.role === "assistant" && props.message}>
parts={props.parts} {(assistantMessage) => (
showAssistantCopyPartID={props.showAssistantCopyPartID} <AssistantMessageDisplay
showReasoningSummaries={props.showReasoningSummaries} message={assistantMessage() as AssistantMessage}
/> parts={props.parts}
) showAssistantCopyPartID={props.showAssistantCopyPartID}
} showReasoningSummaries={props.showReasoningSummaries}
/>
return undefined )}
</Match>
</Switch>
)
} }
export function AssistantMessageDisplay(props: { export function AssistantMessageDisplay(props: {
@ -732,42 +733,52 @@ export function AssistantMessageDisplay(props: {
return ( return (
<Index each={grouped()}> <Index each={grouped()}>
{(entry) => { {(entryAccessor) => {
const kind = createMemo(() => entry().type) const entryType = createMemo(() => entryAccessor().type)
const parts = createMemo(
() => {
const value = entry()
if (value.type !== "context") return emptyTools
return value.refs
.map((ref) => part().get(ref.partID))
.filter((part): part is ToolPart => !!part && isContextGroupTool(part))
},
emptyTools,
{ equals: same },
)
const item = createMemo(() => {
const value = entry()
if (value.type !== "part") return
return part().get(value.ref.partID)
})
const ready = createMemo(() => {
if (kind() !== "part") return
const value = item()
if (!value) return
return value
})
return ( return (
<> <Switch>
<Show when={kind() === "context" && parts().length > 0}> <Match when={entryType() === "context"}>
<ContextToolGroup parts={parts()} /> {(() => {
</Show> const parts = createMemo(
<Show when={ready()}> () => {
{(ready) => ( const entry = entryAccessor()
<Part part={ready()} message={props.message} showAssistantCopyPartID={props.showAssistantCopyPartID} /> if (entry.type !== "context") return emptyTools
)} return entry.refs
</Show> .map((ref) => part().get(ref.partID))
</> .filter((part): part is ToolPart => !!part && isContextGroupTool(part))
},
emptyTools,
{ equals: same },
)
return (
<Show when={parts().length > 0}>
<ContextToolGroup parts={parts()} />
</Show>
)
})()}
</Match>
<Match when={entryType() === "part"}>
{(() => {
const item = createMemo(() => {
const entry = entryAccessor()
if (entry.type !== "part") return
return part().get(entry.ref.partID)
})
return (
<Show when={item()}>
<Part
part={item()!}
message={props.message}
showAssistantCopyPartID={props.showAssistantCopyPartID}
/>
</Show>
)
})()}
</Match>
</Switch>
) )
}} }}
</Index> </Index>
@ -834,9 +845,11 @@ function ContextToolGroup(props: { parts: ToolPart[]; busy?: boolean }) {
<Collapsible.Content> <Collapsible.Content>
<div data-component="context-tool-group-list"> <div data-component="context-tool-group-list">
<Index each={props.parts}> <Index each={props.parts}>
{(part) => { {(partAccessor) => {
const trigger = createMemo(() => contextToolTrigger(part(), i18n)) const trigger = createMemo(() => contextToolTrigger(partAccessor(), i18n))
const running = createMemo(() => part().state.status === "pending" || part().state.status === "running") const running = createMemo(
() => partAccessor().state.status === "pending" || partAccessor().state.status === "running",
)
return ( return (
<div data-slot="context-tool-group-item"> <div data-slot="context-tool-group-item">
<div data-component="tool-trigger"> <div data-component="tool-trigger">
@ -874,7 +887,6 @@ export function UserMessageDisplay(props: { message: UserMessage; parts: PartTyp
const data = useData() const data = useData()
const dialog = useDialog() const dialog = useDialog()
const i18n = useI18n() const i18n = useI18n()
const message = bindMessage(props.message)
const [state, setState] = createStore({ const [state, setState] = createStore({
copied: false, copied: false,
busy: undefined as "fork" | "revert" | undefined, busy: undefined as "fork" | "revert" | undefined,
@ -897,8 +909,8 @@ export function UserMessageDisplay(props: { message: UserMessage; parts: PartTyp
const agents = createMemo(() => (props.parts?.filter((p) => p.type === "agent") as AgentPart[]) ?? []) const agents = createMemo(() => (props.parts?.filter((p) => p.type === "agent") as AgentPart[]) ?? [])
const model = createMemo(() => { const model = createMemo(() => {
const providerID = message().model?.providerID const providerID = props.message.model?.providerID
const modelID = message().model?.modelID const modelID = props.message.model?.modelID
if (!providerID || !modelID) return "" if (!providerID || !modelID) return ""
const match = data.store.provider?.all?.find((p) => p.id === providerID) const match = data.store.provider?.all?.find((p) => p.id === providerID)
return match?.models?.[modelID]?.name ?? modelID return match?.models?.[modelID]?.name ?? modelID
@ -906,13 +918,13 @@ export function UserMessageDisplay(props: { message: UserMessage; parts: PartTyp
const timefmt = createMemo(() => new Intl.DateTimeFormat(i18n.locale(), { timeStyle: "short" })) const timefmt = createMemo(() => new Intl.DateTimeFormat(i18n.locale(), { timeStyle: "short" }))
const stamp = createMemo(() => { const stamp = createMemo(() => {
const created = message().time?.created const created = props.message.time?.created
if (typeof created !== "number") return "" if (typeof created !== "number") return ""
return timefmt().format(created) return timefmt().format(created)
}) })
const metaHead = createMemo(() => { const metaHead = createMemo(() => {
const agent = message().agent const agent = props.message.agent
const items = [agent ? agent[0]?.toUpperCase() + agent.slice(1) : "", model()] const items = [agent ? agent[0]?.toUpperCase() + agent.slice(1) : "", model()]
return items.filter((x) => !!x).join("\u00A0\u00B7\u00A0") return items.filter((x) => !!x).join("\u00A0\u00B7\u00A0")
}) })
@ -938,8 +950,8 @@ export function UserMessageDisplay(props: { message: UserMessage; parts: PartTyp
void Promise.resolve() void Promise.resolve()
.then(() => .then(() =>
act({ act({
sessionID: message().sessionID, sessionID: props.message.sessionID,
messageID: message().id, messageID: props.message.id,
}), }),
) )
.finally(() => { .finally(() => {
@ -1298,27 +1310,27 @@ PART_MAPPING["text"] = function TextPartDisplay(props) {
const i18n = useI18n() const i18n = useI18n()
const numfmt = createMemo(() => new Intl.NumberFormat(i18n.locale())) const numfmt = createMemo(() => new Intl.NumberFormat(i18n.locale()))
const part = () => props.part as TextPart const part = () => props.part as TextPart
const message = bindMessage(props.message)
const interrupted = createMemo( const interrupted = createMemo(
() => message().role === "assistant" && (message() as AssistantMessage).error?.name === "MessageAbortedError", () =>
props.message.role === "assistant" && (props.message as AssistantMessage).error?.name === "MessageAbortedError",
) )
const model = createMemo(() => { const model = createMemo(() => {
const current = message() if (props.message.role !== "assistant") return ""
if (current.role !== "assistant") return "" const message = props.message as AssistantMessage
const match = data.store.provider?.all?.find((p) => p.id === current.providerID) const match = data.store.provider?.all?.find((p) => p.id === message.providerID)
return match?.models?.[current.modelID]?.name ?? current.modelID return match?.models?.[message.modelID]?.name ?? message.modelID
}) })
const duration = createMemo(() => { const duration = createMemo(() => {
const current = message() if (props.message.role !== "assistant") return ""
if (current.role !== "assistant") return "" const message = props.message as AssistantMessage
const completed = current.time.completed const completed = message.time.completed
const ms = const ms =
typeof props.turnDurationMs === "number" typeof props.turnDurationMs === "number"
? props.turnDurationMs ? props.turnDurationMs
: typeof completed === "number" : typeof completed === "number"
? completed - current.time.created ? completed - message.time.created
: -1 : -1
if (!(ms >= 0)) return "" if (!(ms >= 0)) return ""
const total = Math.round(ms / 1000) const total = Math.round(ms / 1000)
@ -1332,9 +1344,8 @@ PART_MAPPING["text"] = function TextPartDisplay(props) {
}) })
const meta = createMemo(() => { const meta = createMemo(() => {
const current = message() if (props.message.role !== "assistant") return ""
if (current.role !== "assistant") return "" const agent = (props.message as AssistantMessage).agent
const agent = current.agent
const items = [ const items = [
agent ? agent[0]?.toUpperCase() + agent.slice(1) : "", agent ? agent[0]?.toUpperCase() + agent.slice(1) : "",
model(), model(),
@ -1347,13 +1358,13 @@ PART_MAPPING["text"] = function TextPartDisplay(props) {
const displayText = () => (part().text ?? "").trim() const displayText = () => (part().text ?? "").trim()
const throttledText = createThrottledValue(displayText) const throttledText = createThrottledValue(displayText)
const isLastTextPart = createMemo(() => { const isLastTextPart = createMemo(() => {
const last = (data.store.part?.[message().id] ?? []) const last = (data.store.part?.[props.message.id] ?? [])
.filter((item): item is TextPart => item?.type === "text" && !!item.text?.trim()) .filter((item): item is TextPart => item?.type === "text" && !!item.text?.trim())
.at(-1) .at(-1)
return last?.id === part().id return last?.id === part().id
}) })
const showCopy = createMemo(() => { const showCopy = createMemo(() => {
if (message().role !== "assistant") return isLastTextPart() if (props.message.role !== "assistant") return isLastTextPart()
if (props.showAssistantCopyPartID === null) return false if (props.showAssistantCopyPartID === null) return false
if (typeof props.showAssistantCopyPartID === "string") return props.showAssistantCopyPartID === part().id if (typeof props.showAssistantCopyPartID === "string") return props.showAssistantCopyPartID === part().id
return isLastTextPart() return isLastTextPart()