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 (
-
- )
-}
-
-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 (
+
+ )
+}
+
+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