diff options
| author | Adam <[email protected]> | 2025-10-29 07:31:57 -0500 |
|---|---|---|
| committer | Adam <[email protected]> | 2025-10-29 07:32:01 -0500 |
| commit | 5b86fa91099d66cdc876cd4209a97ae2c903d510 (patch) | |
| tree | a00ea234769f9d68104791d69ec6bbf154c7cf2a /packages/desktop | |
| parent | aa7e008fe1ea1c1ee0e28915b61633865c304152 (diff) | |
| download | opencode-5b86fa91099d66cdc876cd4209a97ae2c903d510.tar.gz opencode-5b86fa91099d66cdc876cd4209a97ae2c903d510.zip | |
wip: desktop work
Diffstat (limited to 'packages/desktop')
| -rw-r--r-- | packages/desktop/src/components/assistant-message.tsx | 70 | ||||
| -rw-r--r-- | packages/desktop/src/pages/index.tsx | 87 |
2 files changed, 105 insertions, 52 deletions
diff --git a/packages/desktop/src/components/assistant-message.tsx b/packages/desktop/src/components/assistant-message.tsx index 90f6e70fe..8c654660b 100644 --- a/packages/desktop/src/components/assistant-message.tsx +++ b/packages/desktop/src/components/assistant-message.tsx @@ -1,4 +1,4 @@ -import type { Part, AssistantMessage, ReasoningPart, TextPart, ToolPart } from "@opencode-ai/sdk" +import type { Part, AssistantMessage, ReasoningPart, TextPart, ToolPart, Message } from "@opencode-ai/sdk" import { children, Component, createMemo, For, Match, Show, Switch, type JSX } from "solid-js" import { Dynamic } from "solid-js/web" import { Markdown } from "./markdown" @@ -17,47 +17,44 @@ import type { WriteTool } from "opencode/tool/write" import type { TodoWriteTool } from "opencode/tool/todo" import { DiffChanges } from "./diff-changes" -export function AssistantMessage(props: { message: AssistantMessage; parts: Part[]; lastToolOnly?: boolean }) { +export function AssistantMessage(props: { message: AssistantMessage; parts: Part[] }) { const filteredParts = createMemo(() => { - let tool = false return props.parts?.filter((x) => { - if (x.type === "tool" && props.lastToolOnly && tool) return false - if (x.type === "tool") tool = true + if (x.type === "reasoning") return false return x.type !== "tool" || x.tool !== "todoread" }) }) return ( <div class="w-full flex flex-col items-start gap-4"> - <For each={filteredParts()}> - {(part) => { - const component = createMemo(() => PART_MAPPING[part.type as keyof typeof PART_MAPPING]) - return ( - <Show when={component()}> - <Dynamic component={component()} part={part as any} message={props.message} /> - </Show> - ) - }} - </For> + <For each={filteredParts()}>{(part) => <Part part={part} message={props.message} />}</For> </div> ) } +export function Part(props: { part: Part; message: Message; readonly?: boolean }) { + const component = createMemo(() => PART_MAPPING[props.part.type as keyof typeof PART_MAPPING]) + return ( + <Show when={component()}> + <Dynamic component={component()} part={props.part as any} message={props.message} readonly={props.readonly} /> + </Show> + ) +} + const PART_MAPPING = { text: TextPart, tool: ToolPart, reasoning: ReasoningPart, } -function ReasoningPart(props: { part: ReasoningPart; message: AssistantMessage }) { - return null - // return ( - // <Show when={props.part.text.trim()}> - // <div>{props.part.text}</div> - // </Show> - // ) +function ReasoningPart(props: { part: ReasoningPart; message: Message }) { + return ( + <Show when={props.part.text.trim()}> + <Markdown text={props.part.text.trim()} /> + </Show> + ) } -function TextPart(props: { part: TextPart; message: AssistantMessage }) { +function TextPart(props: { part: TextPart; message: Message }) { return ( <Show when={props.part.text.trim()}> <Markdown text={props.part.text.trim()} /> @@ -65,17 +62,11 @@ function TextPart(props: { part: TextPart; message: AssistantMessage }) { ) } -function ToolPart(props: { part: ToolPart; message: AssistantMessage }) { - // const sync = useSync() - +function ToolPart(props: { part: ToolPart; message: Message; readonly?: boolean }) { const component = createMemo(() => { const render = ToolRegistry.render(props.part.tool) ?? GenericTool - const metadata = props.part.state.status === "pending" ? {} : (props.part.state.metadata ?? {}) const input = props.part.state.status === "completed" ? props.part.state.input : {} - // const permissions = sync.data.permission[props.message.sessionID] ?? [] - // const permissionIndex = permissions.findIndex((x) => x.callID === props.part.callID) - // const permission = permissions[permissionIndex] return ( <Dynamic @@ -83,8 +74,8 @@ function ToolPart(props: { part: ToolPart; message: AssistantMessage }) { input={input} tool={props.part.tool} metadata={metadata} - // permission={permission?.metadata ?? {}} output={props.part.state.status === "completed" ? props.part.state.output : undefined} + readonly={props.readonly} /> ) }) @@ -106,7 +97,12 @@ const isTriggerTitle = (val: any): val is TriggerTitle => { return typeof val === "object" && val !== null && "title" in val && !(val instanceof Node) } -function BasicTool(props: { icon: IconProps["name"]; trigger: TriggerTitle | JSX.Element; children?: JSX.Element }) { +function BasicTool(props: { + icon: IconProps["name"] + trigger: TriggerTitle | JSX.Element + children?: JSX.Element + readonly?: boolean +}) { const resolved = children(() => props.children) return ( <Collapsible> @@ -161,13 +157,13 @@ function BasicTool(props: { icon: IconProps["name"]; trigger: TriggerTitle | JSX </Switch> </div> </div> - <Show when={resolved()}> + <Show when={resolved() && !props.readonly}> <Collapsible.Arrow /> </Show> </div> </Collapsible.Trigger> - <Show when={props.children}> - <Collapsible.Content>{props.children}</Collapsible.Content> + <Show when={resolved() && !props.readonly}> + <Collapsible.Content>{resolved()}</Collapsible.Content> </Show> </Collapsible> // <> @@ -177,15 +173,15 @@ function BasicTool(props: { icon: IconProps["name"]; trigger: TriggerTitle | JSX } function GenericTool(props: ToolProps<any>) { - return <BasicTool icon="mcp" trigger={{ title: props.tool }} /> + return <BasicTool icon="mcp" trigger={{ title: props.tool }} readonly={props.readonly} /> } type ToolProps<T extends Tool.Info> = { input: Partial<Tool.InferParameters<T>> metadata: Partial<Tool.InferMetadata<T>> - // permission: Record<string, any> tool: string output?: string + readonly?: boolean } const ToolRegistry = (() => { diff --git a/packages/desktop/src/pages/index.tsx b/packages/desktop/src/pages/index.tsx index c1884b2c0..800f3651e 100644 --- a/packages/desktop/src/pages/index.tsx +++ b/packages/desktop/src/pages/index.tsx @@ -33,7 +33,7 @@ import { Code } from "@/components/code" import { useSync } from "@/context/sync" import { useSDK } from "@/context/sdk" import { ProgressCircle } from "@/components/progress-circle" -import { AssistantMessage } from "@/components/assistant-message" +import { AssistantMessage, Part } from "@/components/assistant-message" import { type AssistantMessage as AssistantMessageType } from "@opencode-ai/sdk" import { DiffChanges } from "@/components/diff-changes" @@ -178,6 +178,8 @@ export default function Page() { } const handleDiffTriggerClick = (event: MouseEvent) => { + // disabling scroll to diff for now + return const target = event.currentTarget as HTMLElement queueMicrotask(() => { if (target.getAttribute("aria-expanded") !== "true") return @@ -636,6 +638,7 @@ export default function Page() { <div class="flex flex-col items-start gap-50 pb-50"> <For each={local.session.userMessages()}> {(message) => { + const [expanded, setExpanded] = createSignal(false) const title = createMemo(() => message.summary?.title) const prompt = createMemo(() => local.session.getMessageText(message)) const summary = createMemo(() => message.summary?.body) @@ -649,15 +652,12 @@ export default function Page() { if (!last) return false return !last.time.completed }) - const lastWithContent = createMemo(() => - assistantMessages().findLast((m) => { - const parts = sync.data.part[m.id] - return parts?.find((p) => p.type === "text" || p.type === "tool") - }), - ) return ( - <div data-message={message.id} class="flex flex-col items-start self-stretch gap-8"> + <div + data-message={message.id} + class="flex flex-col items-start self-stretch gap-8 min-h-[calc(100vh-15rem)]" + > {/* Title */} <div class="py-2 flex flex-col items-start gap-2 self-stretch sticky top-0 bg-background-stronger"> <h1 class="text-14-medium text-text-strong overflow-hidden text-ellipsis min-w-0"> @@ -665,14 +665,19 @@ export default function Page() { </h1> </div> <Show when={title}> - <div class="-mt-5 text-12-regular text-text-base line-clamp-3">{prompt()}</div> + <div class="-mt-8 text-12-regular text-text-base line-clamp-3">{prompt()}</div> </Show> {/* Response */} <div class="w-full flex flex-col gap-2"> - <Collapsible variant="ghost"> + <Collapsible variant="ghost" open={expanded()} onOpenChange={setExpanded}> <Collapsible.Trigger class="text-text-weak hover:text-text-strong"> <div class="flex items-center gap-1 self-stretch"> - <h2 class="text-12-medium">Show steps</h2> + <h2 class="text-12-medium"> + <Switch> + <Match when={expanded()}>Hide steps</Match> + <Match when={!expanded()}>Show steps</Match> + </Switch> + </h2> <Collapsible.Arrow /> </div> </Collapsible.Trigger> @@ -687,11 +692,63 @@ export default function Page() { </div> </Collapsible.Content> </Collapsible> - <Show when={working() && lastWithContent()}> - {(last) => { - const lastParts = createMemo(() => sync.data.part[last().id]) + <Show when={working() && !expanded()}> + {(_) => { + const lastMessageWithText = createMemo(() => + assistantMessages().findLast((m) => { + const parts = sync.data.part[m.id] + return parts?.find((p) => p.type === "text") + }), + ) + const lastMessageWithReasoning = createMemo(() => + assistantMessages().findLast((m) => { + const parts = sync.data.part[m.id] + return parts?.find((p) => p.type === "reasoning") + }), + ) + const lastMessageWithTool = createMemo(() => + assistantMessages().findLast((m) => { + const parts = sync.data.part[m.id] + return parts?.find( + (p) => p.type === "tool" && p.state.status === "completed", + ) + }), + ) return ( - <AssistantMessage lastToolOnly message={last()} parts={lastParts()} /> + <div class="w-full flex flex-col gap-2"> + <Switch> + <Match when={lastMessageWithText()}> + {(last) => { + const lastTextPart = createMemo(() => + sync.data.part[last().id].findLast((p) => p.type === "text"), + ) + return <Part message={last()} part={lastTextPart()!} readonly /> + }} + </Match> + <Match when={lastMessageWithReasoning()}> + {(last) => { + const lastReasoningPart = createMemo(() => + sync.data.part[last().id].findLast( + (p) => p.type === "reasoning", + ), + ) + return ( + <Part message={last()} part={lastReasoningPart()!} readonly /> + ) + }} + </Match> + </Switch> + <Show when={lastMessageWithTool()}> + {(last) => { + const lastToolPart = createMemo(() => + sync.data.part[last().id].findLast( + (p) => p.type === "tool" && p.state.status === "completed", + ), + ) + return <Part message={last()} part={lastToolPart()!} readonly /> + }} + </Show> + </div> ) }} </Show> |
