diff options
Diffstat (limited to 'packages/ui/src/components')
| -rw-r--r-- | packages/ui/src/components/message-part.tsx | 50 | ||||
| -rw-r--r-- | packages/ui/src/components/session-turn.tsx | 96 |
2 files changed, 86 insertions, 60 deletions
diff --git a/packages/ui/src/components/message-part.tsx b/packages/ui/src/components/message-part.tsx index 8590b31cb..be40fbd33 100644 --- a/packages/ui/src/components/message-part.tsx +++ b/packages/ui/src/components/message-part.tsx @@ -95,6 +95,12 @@ export const PART_MAPPING: Record<string, PartComponent | undefined> = {} const TEXT_RENDER_THROTTLE_MS = 250 +function same<T>(a: readonly T[], b: readonly T[]) { + if (a === b) return true + if (a.length !== b.length) return false + return a.every((x, i) => x === b[i]) +} + function createThrottledValue(getValue: () => string) { const [value, setValue] = createSignal(getValue()) let timeout: ReturnType<typeof setTimeout> | undefined @@ -257,11 +263,15 @@ export function Message(props: MessageProps) { } export function AssistantMessageDisplay(props: { message: AssistantMessage; parts: PartType[] }) { - const filteredParts = createMemo(() => { - return props.parts?.filter((x) => { - return x.type !== "tool" || (x as ToolPart).tool !== "todoread" - }) - }) + const emptyParts: PartType[] = [] + const filteredParts = createMemo( + () => + props.parts.filter((x) => { + return x.type !== "tool" || (x as ToolPart).tool !== "todoread" + }), + emptyParts, + { equals: same }, + ) return <For each={filteredParts()}>{(part) => <Part part={part} message={props.message} />}</For> } @@ -425,9 +435,7 @@ PART_MAPPING["tool"] = function ToolPartDisplay(props) { const part = props.part as ToolPart const permission = createMemo(() => { - const sessionID = props.message.sessionID - const permissions = data.store.permission?.[sessionID] ?? [] - const next = permissions[0] + const next = data.store.permission?.[props.message.sessionID]?.[0] if (!next) return undefined if (next.callID !== part.callID) return undefined return next @@ -448,13 +456,17 @@ PART_MAPPING["tool"] = function ToolPartDisplay(props) { }) } - const component = createMemo(() => { - const render = ToolRegistry.render(part.tool) ?? GenericTool - // @ts-expect-error - const metadata = part.state?.metadata ?? {} - const input = part.state?.input ?? {} + const emptyInput: Record<string, any> = {} + const emptyMetadata: Record<string, any> = {} - return ( + const input = () => part.state?.input ?? emptyInput + // @ts-expect-error + const metadata = () => part.state?.metadata ?? emptyMetadata + + const render = ToolRegistry.render(part.tool) ?? GenericTool + + return ( + <div data-component="tool-part-wrapper" data-permission={!!permission()}> <Switch> <Match when={part.state.status === "error" && part.state.error}> {(error) => { @@ -483,9 +495,9 @@ PART_MAPPING["tool"] = function ToolPartDisplay(props) { <Match when={true}> <Dynamic component={render} - input={input} + input={input()} tool={part.tool} - metadata={metadata} + metadata={metadata()} // @ts-expect-error output={part.state.output} status={part.state.status} @@ -495,12 +507,6 @@ PART_MAPPING["tool"] = function ToolPartDisplay(props) { /> </Match> </Switch> - ) - }) - - return ( - <div data-component="tool-part-wrapper" data-permission={!!permission()}> - <Show when={component()}>{component()}</Show> <Show when={permission()}> {(perm) => ( <div data-component="permission-prompt"> diff --git a/packages/ui/src/components/session-turn.tsx b/packages/ui/src/components/session-turn.tsx index bd75ab5e8..e2169102f 100644 --- a/packages/ui/src/components/session-turn.tsx +++ b/packages/ui/src/components/session-turn.tsx @@ -1,4 +1,11 @@ -import { AssistantMessage, Part as PartType, TextPart, ToolPart } from "@opencode-ai/sdk/v2/client" +import { + AssistantMessage, + Message as MessageType, + Part as PartType, + type Permission, + TextPart, + ToolPart, +} from "@opencode-ai/sdk/v2/client" import { useData } from "../context" import { useDiffComponent } from "../context/diff" import { getDirectory, getFilename } from "@opencode-ai/util/path" @@ -61,13 +68,20 @@ function computeStatusFromPart(part: PartType | undefined): string | undefined { return undefined } +function same<T>(a: readonly T[], b: readonly T[]) { + if (a === b) return true + if (a.length !== b.length) return false + return a.every((x, i) => x === b[i]) +} + function AssistantMessageItem(props: { message: AssistantMessage responsePartId: string | undefined hideResponsePart: boolean }) { const data = useData() - const msgParts = createMemo(() => data.store.part[props.message.id] ?? []) + const emptyParts: PartType[] = [] + const msgParts = createMemo(() => data.store.part[props.message.id] ?? emptyParts) const lastTextPart = createMemo(() => { const parts = msgParts() for (let i = parts.length - 1; i >= 0; i--) { @@ -109,7 +123,14 @@ export function SessionTurn( const data = useData() const diffComponent = useDiffComponent() - const allMessages = createMemo(() => data.store.message[props.sessionID] ?? []) + const emptyMessages: MessageType[] = [] + const emptyParts: PartType[] = [] + const emptyAssistant: AssistantMessage[] = [] + const emptyPermissions: Permission[] = [] + const emptyPermissionParts: { part: ToolPart; message: AssistantMessage }[] = [] + const idle = { type: "idle" as const } + + const allMessages = createMemo(() => data.store.message[props.sessionID] ?? emptyMessages) const messageIndex = createMemo(() => { const messages = allMessages() @@ -147,27 +168,31 @@ export function SessionTurn( const parts = createMemo(() => { const msg = message() - if (!msg) return [] - return data.store.part[msg.id] ?? [] + if (!msg) return emptyParts + return data.store.part[msg.id] ?? emptyParts }) - const assistantMessages = createMemo(() => { - const msg = message() - if (!msg) return [] as AssistantMessage[] - - const messages = allMessages() - const index = messageIndex() - if (index < 0) return [] as AssistantMessage[] - - const result: AssistantMessage[] = [] - for (let i = index + 1; i < messages.length; i++) { - const item = messages[i] - if (!item) continue - if (item.role === "user") break - if (item.role === "assistant" && item.parentID === msg.id) result.push(item as AssistantMessage) - } - return result - }) + const assistantMessages = createMemo( + () => { + const msg = message() + if (!msg) return emptyAssistant + + const messages = allMessages() + const index = messageIndex() + if (index < 0) return emptyAssistant + + const result: AssistantMessage[] = [] + for (let i = index + 1; i < messages.length; i++) { + const item = messages[i] + if (!item) continue + if (item.role === "user") break + if (item.role === "assistant" && item.parentID === msg.id) result.push(item as AssistantMessage) + } + return result + }, + emptyAssistant, + { equals: same }, + ) const lastAssistantMessage = createMemo(() => assistantMessages().at(-1)) @@ -176,7 +201,7 @@ export function SessionTurn( const lastTextPart = createMemo(() => { const msgs = assistantMessages() for (let mi = msgs.length - 1; mi >= 0; mi--) { - const msgParts = data.store.part[msgs[mi].id] ?? [] + const msgParts = data.store.part[msgs[mi].id] ?? emptyParts for (let pi = msgParts.length - 1; pi >= 0; pi--) { const part = msgParts[pi] if (part?.type === "text") return part as TextPart @@ -196,25 +221,25 @@ export function SessionTurn( return false }) - const permissions = createMemo(() => data.store.permission?.[props.sessionID] ?? []) + const permissions = createMemo(() => data.store.permission?.[props.sessionID] ?? emptyPermissions) const permissionCount = createMemo(() => permissions().length) const nextPermission = createMemo(() => permissions()[0]) const permissionParts = createMemo(() => { - if (props.stepsExpanded) return [] as { part: ToolPart; message: AssistantMessage }[] + if (props.stepsExpanded) return emptyPermissionParts const next = nextPermission() - if (!next) return [] as { part: ToolPart; message: AssistantMessage }[] + if (!next) return emptyPermissionParts for (const message of assistantMessages()) { - const parts = data.store.part[message.id] ?? [] + const parts = data.store.part[message.id] ?? emptyParts for (const part of parts) { if (part?.type !== "tool") continue const tool = part as ToolPart if (tool.callID === next.callID) return [{ part: tool, message }] } } - return [] as { part: ToolPart; message: AssistantMessage }[] + return emptyPermissionParts }) const shellModePart = createMemo(() => { @@ -224,7 +249,7 @@ export function SessionTurn( const msgs = assistantMessages() if (msgs.length !== 1) return - const msgParts = data.store.part[msgs[0].id] ?? [] + const msgParts = data.store.part[msgs[0].id] ?? emptyParts if (msgParts.length !== 1) return const assistantPart = msgParts[0] @@ -239,7 +264,7 @@ export function SessionTurn( let currentTask: ToolPart | undefined for (let mi = msgs.length - 1; mi >= 0; mi--) { - const msgParts = data.store.part[msgs[mi].id] ?? [] + const msgParts = data.store.part[msgs[mi].id] ?? emptyParts for (let pi = msgParts.length - 1; pi >= 0; pi--) { const part = msgParts[pi] if (!part) continue @@ -266,12 +291,12 @@ export function SessionTurn( : undefined if (taskSessionId) { - const taskMessages = data.store.message[taskSessionId] ?? [] + const taskMessages = data.store.message[taskSessionId] ?? emptyMessages for (let mi = taskMessages.length - 1; mi >= 0; mi--) { const msg = taskMessages[mi] if (!msg || msg.role !== "assistant") continue - const msgParts = data.store.part[msg.id] ?? [] + const msgParts = data.store.part[msg.id] ?? emptyParts for (let pi = msgParts.length - 1; pi >= 0; pi--) { const part = msgParts[pi] if (part) return computeStatusFromPart(part) @@ -282,12 +307,7 @@ export function SessionTurn( return computeStatusFromPart(last) }) - const status = createMemo( - () => - data.store.session_status[props.sessionID] ?? { - type: "idle", - }, - ) + const status = createMemo(() => data.store.session_status[props.sessionID] ?? idle) const working = createMemo(() => status().type !== "idle" && isLastUserMessage()) const retry = createMemo(() => { const s = status() |
