From 0ff73ed8a6cfa2beb6ce436a921cb98f836677c3 Mon Sep 17 00:00:00 2001 From: Adam <2363879+adamdotdevin@users.noreply.github.com> Date: Wed, 29 Oct 2025 11:55:25 -0500 Subject: wip: desktop work --- .../desktop/src/components/assistant-message.tsx | 431 ------------------- packages/desktop/src/components/message.tsx | 459 +++++++++++++++++++++ packages/desktop/src/pages/index.tsx | 25 +- 3 files changed, 476 insertions(+), 439 deletions(-) delete mode 100644 packages/desktop/src/components/assistant-message.tsx create mode 100644 packages/desktop/src/components/message.tsx diff --git a/packages/desktop/src/components/assistant-message.tsx b/packages/desktop/src/components/assistant-message.tsx deleted file mode 100644 index 8c654660b..000000000 --- a/packages/desktop/src/components/assistant-message.tsx +++ /dev/null @@ -1,431 +0,0 @@ -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" -import { Checkbox, Collapsible, Diff, Icon, IconProps } from "@opencode-ai/ui" -import { getDirectory, getFilename } from "@/utils" -import type { Tool } from "opencode/tool/tool" -import type { ReadTool } from "opencode/tool/read" -import type { ListTool } from "opencode/tool/ls" -import type { GlobTool } from "opencode/tool/glob" -import type { GrepTool } from "opencode/tool/grep" -import type { WebFetchTool } from "opencode/tool/webfetch" -import type { TaskTool } from "opencode/tool/task" -import type { BashTool } from "opencode/tool/bash" -import type { EditTool } from "opencode/tool/edit" -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[] }) { - const filteredParts = createMemo(() => { - return props.parts?.filter((x) => { - if (x.type === "reasoning") return false - return x.type !== "tool" || x.tool !== "todoread" - }) - }) - return ( -
- {(part) => } -
- ) -} - -export function Part(props: { part: Part; message: Message; readonly?: boolean }) { - const component = createMemo(() => PART_MAPPING[props.part.type as keyof typeof PART_MAPPING]) - return ( - - - - ) -} - -const PART_MAPPING = { - text: TextPart, - tool: ToolPart, - reasoning: ReasoningPart, -} - -function ReasoningPart(props: { part: ReasoningPart; message: Message }) { - return ( - - - - ) -} - -function TextPart(props: { part: TextPart; message: Message }) { - return ( - - - - ) -} - -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 : {} - - return ( - - ) - }) - - return {component()} -} - -type TriggerTitle = { - title: string - titleClass?: string - subtitle?: string - subtitleClass?: string - args?: string[] - argsClass?: string - action?: JSX.Element -} - -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 - readonly?: boolean -}) { - const resolved = children(() => props.children) - return ( - - -
-
- -
- - - {(trigger) => ( -
-
- - {trigger().title} - - - - {trigger().subtitle} - - - - - {(arg) => ( - - {arg} - - )} - - -
- {trigger().action} -
- )} -
- {props.trigger as JSX.Element} -
-
-
- - - -
-
- - {resolved()} - -
- // <> - // {props.part.state.error.replace("Error: ", "")} - // - ) -} - -function GenericTool(props: ToolProps) { - return -} - -type ToolProps = { - input: Partial> - metadata: Partial> - tool: string - output?: string - readonly?: boolean -} - -const ToolRegistry = (() => { - const state: Record< - string, - { - name: string - render?: Component> - } - > = {} - function register(input: { name: string; render?: Component> }) { - state[input.name] = input - return input - } - return { - register, - render(name: string) { - return state[name]?.render - }, - } -})() - -ToolRegistry.register({ - name: "read", - render(props) { - return ( - - ) - }, -}) - -ToolRegistry.register({ - name: "list", - render(props) { - return ( - - -
{props.output}
-
-
- ) - }, -}) - -ToolRegistry.register({ - name: "glob", - render(props) { - return ( - {props.output} - - - ) - }, -}) - -ToolRegistry.register({ - name: "grep", - render(props) { - const args = [] - if (props.input.pattern) args.push("pattern=" + props.input.pattern) - if (props.input.include) args.push("include=" + props.input.include) - return ( - - -
{props.output}
-
-
- ) - }, -}) - -ToolRegistry.register({ - name: "webfetch", - render(props) { - return ( - - - - ), - }} - > - -
{props.output}
-
-
- ) - }, -}) - -ToolRegistry.register({ - name: "task", - render(props) { - return ( - - -
{props.output}
-
-
- ) - }, -}) - -ToolRegistry.register({ - name: "bash", - render(props) { - return ( - - -
{props.output}
-
-
- ) - }, -}) - -ToolRegistry.register({ - name: "edit", - render(props) { - return ( - -
-
Edit
-
- - {getDirectory(props.input.filePath!)} - - {getFilename(props.input.filePath ?? "")} -
-
-
- - - -
- - } - > - -
- -
-
-
- ) - }, -}) - -ToolRegistry.register({ - name: "write", - render(props) { - return ( - -
-
Write
-
- - {getDirectory(props.input.filePath!)} - - {getFilename(props.input.filePath ?? "")} -
-
-
{/* */}
- - } - > - -
{props.output}
-
-
- ) - }, -}) - -ToolRegistry.register({ - name: "todowrite", - render(props) { - return ( - t.status === "completed").length}/${props.input.todos?.length}`, - }} - > - -
- - {(todo) => ( - -
{todo.content}
-
- )} -
-
-
-
- ) - }, -}) diff --git a/packages/desktop/src/components/message.tsx b/packages/desktop/src/components/message.tsx new file mode 100644 index 000000000..589ca3118 --- /dev/null +++ b/packages/desktop/src/components/message.tsx @@ -0,0 +1,459 @@ +import type { Part, ReasoningPart, TextPart, ToolPart, Message, AssistantMessage, UserMessage } 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" +import { Checkbox, Collapsible, Diff, Icon, IconProps } from "@opencode-ai/ui" +import { getDirectory, getFilename } from "@/utils" +import type { Tool } from "opencode/tool/tool" +import type { ReadTool } from "opencode/tool/read" +import type { ListTool } from "opencode/tool/ls" +import type { GlobTool } from "opencode/tool/glob" +import type { GrepTool } from "opencode/tool/grep" +import type { WebFetchTool } from "opencode/tool/webfetch" +import type { TaskTool } from "opencode/tool/task" +import type { BashTool } from "opencode/tool/bash" +import type { EditTool } from "opencode/tool/edit" +import type { WriteTool } from "opencode/tool/write" +import type { TodoWriteTool } from "opencode/tool/todo" +import { DiffChanges } from "./diff-changes" + +export function Message(props: { message: Message; parts: Part[] }) { + return ( + + + {(userMessage) => } + + + {(assistantMessage) => } + + + ) +} + +function AssistantMessage(props: { message: AssistantMessage; parts: Part[] }) { + const filteredParts = createMemo(() => { + return props.parts?.filter((x) => { + if (x.type === "reasoning") return false + return x.type !== "tool" || x.tool !== "todoread" + }) + }) + return ( +
+ {(part) => } +
+ ) +} + +function UserMessage(props: { message: UserMessage; parts: Part[] }) { + const text = createMemo(() => + props.parts + ?.filter((p) => p.type === "text" && !p.synthetic) + ?.map((p) => (p as TextPart).text) + ?.join(""), + ) + return
{text()}
+} + +export function Part(props: { part: Part; message: Message; hideDetails?: boolean }) { + const component = createMemo(() => PART_MAPPING[props.part.type as keyof typeof PART_MAPPING]) + return ( + + + + ) +} + +const PART_MAPPING = { + text: TextPart, + tool: ToolPart, + reasoning: ReasoningPart, +} + +function ReasoningPart(props: { part: ReasoningPart; message: Message }) { + return ( + + + + ) +} + +function TextPart(props: { part: TextPart; message: Message }) { + return ( + + + + ) +} + +function ToolPart(props: { part: ToolPart; message: Message; hideDetails?: 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 : {} + + return ( + + ) + }) + + return {component()} +} + +type TriggerTitle = { + title: string + titleClass?: string + subtitle?: string + subtitleClass?: string + args?: string[] + argsClass?: string + action?: JSX.Element +} + +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 + hideDetails?: boolean +}) { + const resolved = children(() => props.children) + return ( + + +
+
+ +
+ + + {(trigger) => ( +
+
+ + {trigger().title} + + + + {trigger().subtitle} + + + + + {(arg) => ( + + {arg} + + )} + + +
+ {trigger().action} +
+ )} +
+ {props.trigger as JSX.Element} +
+
+
+ + + +
+
+ + {resolved()} + +
+ // <> + // {props.part.state.error.replace("Error: ", "")} + // + ) +} + +function GenericTool(props: ToolProps) { + return +} + +type ToolProps = { + input: Partial> + metadata: Partial> + tool: string + output?: string + hideDetails?: boolean +} + +const ToolRegistry = (() => { + const state: Record< + string, + { + name: string + render?: Component> + } + > = {} + function register(input: { name: string; render?: Component> }) { + state[input.name] = input + return input + } + return { + register, + render(name: string) { + return state[name]?.render + }, + } +})() + +ToolRegistry.register({ + name: "read", + render(props) { + return ( + + ) + }, +}) + +ToolRegistry.register({ + name: "list", + render(props) { + return ( + + +
{props.output}
+
+
+ ) + }, +}) + +ToolRegistry.register({ + name: "glob", + render(props) { + return ( + {props.output} + + + ) + }, +}) + +ToolRegistry.register({ + name: "grep", + render(props) { + const args = [] + if (props.input.pattern) args.push("pattern=" + props.input.pattern) + if (props.input.include) args.push("include=" + props.input.include) + return ( + + +
{props.output}
+
+
+ ) + }, +}) + +ToolRegistry.register({ + name: "webfetch", + render(props) { + return ( + + + + ), + }} + > + +
{props.output}
+
+
+ ) + }, +}) + +ToolRegistry.register({ + name: "task", + render(props) { + return ( + + +
{props.output}
+
+
+ ) + }, +}) + +ToolRegistry.register({ + name: "bash", + render(props) { + return ( + + +
{props.output}
+
+
+ ) + }, +}) + +ToolRegistry.register({ + name: "edit", + render(props) { + return ( + +
+
Edit
+
+ + {getDirectory(props.input.filePath!)} + + {getFilename(props.input.filePath ?? "")} +
+
+
+ + + +
+ + } + > + +
+ +
+
+
+ ) + }, +}) + +ToolRegistry.register({ + name: "write", + render(props) { + return ( + +
+
Write
+
+ + {getDirectory(props.input.filePath!)} + + {getFilename(props.input.filePath ?? "")} +
+
+
{/* */}
+ + } + > + +
{props.output}
+
+
+ ) + }, +}) + +ToolRegistry.register({ + name: "todowrite", + render(props) { + return ( + t.status === "completed").length}/${props.input.todos?.length}`, + }} + > + +
+ + {(todo) => ( + +
{todo.content}
+
+ )} +
+
+
+
+ ) + }, +}) diff --git a/packages/desktop/src/pages/index.tsx b/packages/desktop/src/pages/index.tsx index 800f3651e..ac6b6f9c8 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, Part } from "@/components/assistant-message" +import { Message, Part } from "@/components/message" import { type AssistantMessage as AssistantMessageType } from "@opencode-ai/sdk" import { DiffChanges } from "@/components/diff-changes" @@ -198,6 +198,7 @@ export default function Page() { } if (!session) return + local.session.setActive(session.id) const toAbsolutePath = (path: string) => (path.startsWith("/") ? path : sync.absolute(path)) const text = parts.map((part) => part.content).join("") @@ -259,7 +260,6 @@ export default function Page() { ], }, }) - local.session.setActive(session.id) } const handleNewSession = () => { @@ -639,8 +639,9 @@ export default function Page() { {(message) => { const [expanded, setExpanded] = createSignal(false) - const title = createMemo(() => message.summary?.title) + const parts = createMemo(() => sync.data.part[message.id]) const prompt = createMemo(() => local.session.getMessageText(message)) + const title = createMemo(() => message.summary?.title) const summary = createMemo(() => message.summary?.body) const assistantMessages = createMemo(() => { return sync.data.message[activeSession().id]?.filter( @@ -665,7 +666,9 @@ export default function Page() { -
{prompt()}
+
+ +
{/* Response */}
@@ -686,7 +689,7 @@ export default function Page() { {(assistantMessage) => { const parts = createMemo(() => sync.data.part[assistantMessage.id]) - return + return }}
@@ -722,7 +725,9 @@ export default function Page() { const lastTextPart = createMemo(() => sync.data.part[last().id].findLast((p) => p.type === "text"), ) - return + return ( + + ) }} @@ -733,7 +738,11 @@ export default function Page() { ), ) return ( - + ) }} @@ -745,7 +754,7 @@ export default function Page() { (p) => p.type === "tool" && p.state.status === "completed", ), ) - return + return }} -- cgit v1.2.3