Browse Source

wip: desktop work

Adam 3 months ago
parent
commit
fc115ea367

+ 1 - 0
bun.lock

@@ -141,6 +141,7 @@
         "@types/luxon": "3.7.1",
         "@types/node": "catalog:",
         "@typescript/native-preview": "catalog:",
+        "opencode": "workspace:*",
         "typescript": "catalog:",
         "vite": "catalog:",
         "vite-plugin-icons-spritesheet": "3.0.1",

+ 2 - 1
packages/desktop/package.json

@@ -12,12 +12,13 @@
   },
   "license": "MIT",
   "devDependencies": {
+    "opencode": "workspace:*",
     "@tailwindcss/vite": "catalog:",
     "@tsconfig/bun": "1.0.9",
     "@types/luxon": "3.7.1",
     "@types/node": "catalog:",
-    "typescript": "catalog:",
     "@typescript/native-preview": "catalog:",
+    "typescript": "catalog:",
     "vite": "catalog:",
     "vite-plugin-icons-spritesheet": "3.0.1",
     "vite-plugin-solid": "catalog:"

+ 362 - 0
packages/desktop/src/components/assistant-message.tsx

@@ -0,0 +1,362 @@
+import type { Part, AssistantMessage, ReasoningPart, TextPart, ToolPart } from "@opencode-ai/sdk"
+import type { Tool } from "opencode/tool/tool"
+import type { ReadTool } from "opencode/tool/read"
+import { children, Component, createMemo, For, Match, Show, Switch, type JSX } from "solid-js"
+import { Dynamic } from "solid-js/web"
+import { Markdown } from "./markdown"
+import { Collapsible, Icon, IconProps } from "@opencode-ai/ui"
+import { getDirectory, getFilename } from "@/utils"
+import { ListTool } from "opencode/tool/ls"
+import { GlobTool } from "opencode/tool/glob"
+import { GrepTool } from "opencode/tool/grep"
+import { WebFetchTool } from "opencode/tool/webfetch"
+import { TaskTool } from "opencode/tool/task"
+import { BashTool } from "opencode/tool/bash"
+import { EditTool } from "opencode/tool/edit"
+import { DiffChanges } from "./diff-changes"
+import { WriteTool } from "opencode/tool/write"
+
+export function AssistantMessage(props: { message: AssistantMessage; parts: Part[] }) {
+  return (
+    <div class="w-full flex flex-col items-start gap-4">
+      <For each={props.parts}>
+        {(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>
+    </div>
+  )
+}
+
+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 TextPart(props: { part: TextPart; message: AssistantMessage }) {
+  return (
+    <Show when={props.part.text.trim()}>
+      <Markdown text={props.part.text.trim()} />
+    </Show>
+  )
+}
+
+function ToolPart(props: { part: ToolPart; message: AssistantMessage }) {
+  // const sync = useSync()
+
+  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
+          component={render}
+          input={input}
+          tool={props.part.tool}
+          metadata={metadata}
+          // permission={permission?.metadata ?? {}}
+          output={props.part.state.status === "completed" ? props.part.state.output : undefined}
+        />
+        {/* <Show when={props.part.state.status === "error"}>{props.part.state.error.replace("Error: ", "")}</Show> */}
+      </>
+    )
+  })
+
+  return <Show when={component()}>{component()}</Show>
+}
+
+type TriggerTitle = {
+  title: string
+  subtitle?: string
+  args?: 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 }) {
+  const resolved = children(() => props.children)
+
+  return (
+    <Collapsible>
+      <Collapsible.Trigger>
+        <div class="w-full flex items-center self-stretch gap-5 justify-between">
+          <div class="w-full flex items-center self-stretch gap-5">
+            <Icon name={props.icon} size="small" />
+            <Switch>
+              <Match when={isTriggerTitle(props.trigger)}>
+                <div class="w-full flex items-center gap-2 justify-between">
+                  <div class="flex items-center gap-2">
+                    <span class="text-12-medium text-text-base capitalize">
+                      {(props.trigger as TriggerTitle).title}
+                    </span>
+                    <Show when={(props.trigger as TriggerTitle).subtitle}>
+                      <span class="text-12-medium text-text-weak">{(props.trigger as TriggerTitle).subtitle}</span>
+                    </Show>
+                    <Show when={(props.trigger as TriggerTitle).args?.length}>
+                      <For each={(props.trigger as TriggerTitle).args}>
+                        {(arg) => <span class="text-12-regular text-text-weaker">{arg}</span>}
+                      </For>
+                    </Show>
+                  </div>
+                  <Show when={(props.trigger as TriggerTitle).action}>{(props.trigger as TriggerTitle).action}</Show>
+                </div>
+              </Match>
+              <Match when={true}>{props.trigger as JSX.Element}</Match>
+            </Switch>
+          </div>
+          <Show when={resolved()}>
+            <Collapsible.Arrow />
+          </Show>
+        </div>
+      </Collapsible.Trigger>
+      <Show when={props.children}>
+        <Collapsible.Content>{props.children}</Collapsible.Content>
+      </Show>
+    </Collapsible>
+  )
+}
+
+function GenericTool(props: ToolProps<any>) {
+  return <BasicTool icon="mcp" trigger={{ title: props.tool }} />
+}
+
+type ToolProps<T extends Tool.Info> = {
+  input: Partial<Tool.InferParameters<T>>
+  metadata: Partial<Tool.InferMetadata<T>>
+  // permission: Record<string, any>
+  tool: string
+  output?: string
+}
+
+const ToolRegistry = (() => {
+  const state: Record<
+    string,
+    {
+      name: string
+      render?: Component<ToolProps<any>>
+    }
+  > = {}
+  function register<T extends Tool.Info>(input: { name: string; render?: Component<ToolProps<T>> }) {
+    state[input.name] = input
+    return input
+  }
+  return {
+    register,
+    render(name: string) {
+      return state[name]?.render
+    },
+  }
+})()
+
+ToolRegistry.register<typeof ReadTool>({
+  name: "read",
+  render(props) {
+    return (
+      <BasicTool
+        icon="glasses"
+        trigger={{ title: props.tool, subtitle: props.input.filePath ? getFilename(props.input.filePath) : "" }}
+      />
+    )
+  },
+})
+
+ToolRegistry.register<typeof ListTool>({
+  name: "list",
+  render(props) {
+    return (
+      <BasicTool icon="bullet-list" trigger={{ title: props.tool, subtitle: props.input.path || "/" }}>
+        <Show when={false && props.output}>
+          <div class="whitespace-pre">{props.output}</div>
+        </Show>
+      </BasicTool>
+    )
+  },
+})
+
+ToolRegistry.register<typeof GlobTool>({
+  name: "glob",
+  render(props) {
+    return (
+      <BasicTool
+        icon="magnifying-glass-menu"
+        trigger={{
+          title: props.tool,
+          subtitle: props.input.path || "/",
+          args: props.input.pattern ? ["pattern=" + props.input.pattern] : [],
+        }}
+      >
+        <Show when={false && props.output}>
+          <div class="whitespace-pre">{props.output}</div>
+        </Show>
+      </BasicTool>
+    )
+  },
+})
+
+ToolRegistry.register<typeof GrepTool>({
+  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 (
+      <BasicTool
+        icon="magnifying-glass-menu"
+        trigger={{
+          title: props.tool,
+          subtitle: props.input.path || "/",
+          args,
+        }}
+      >
+        <Show when={false && props.output}>
+          <div class="whitespace-pre">{props.output}</div>
+        </Show>
+      </BasicTool>
+    )
+  },
+})
+
+ToolRegistry.register<typeof WebFetchTool>({
+  name: "webfetch",
+  render(props) {
+    return (
+      <BasicTool
+        icon="window-cursor"
+        trigger={{
+          title: props.tool,
+          subtitle: props.input.url || "",
+          args: props.input.format ? ["format=" + props.input.format] : [],
+          action: (
+            <div class="size-6 flex items-center justify-center">
+              <Icon name="square-arrow-top-right" size="small" />
+            </div>
+          ),
+        }}
+      >
+        <Show when={false && props.output}>
+          <div class="whitespace-pre">{props.output}</div>
+        </Show>
+      </BasicTool>
+    )
+  },
+})
+
+ToolRegistry.register<typeof TaskTool>({
+  name: "task",
+  render(props) {
+    return (
+      <BasicTool
+        icon="task"
+        trigger={{
+          title: `${props.input.subagent_type || props.tool} Agent`,
+          subtitle: props.input.description,
+        }}
+      >
+        <Show when={false && props.output}>
+          <div class="whitespace-pre">{props.output}</div>
+        </Show>
+      </BasicTool>
+    )
+  },
+})
+
+ToolRegistry.register<typeof BashTool>({
+  name: "bash",
+  render(props) {
+    return (
+      <BasicTool
+        icon="console"
+        trigger={{
+          title: "Shell",
+          subtitle: "Ran " + props.input.command,
+        }}
+      >
+        <Show when={false && props.output}>
+          <div class="whitespace-pre">{props.output}</div>
+        </Show>
+      </BasicTool>
+    )
+  },
+})
+
+ToolRegistry.register<typeof EditTool>({
+  name: "edit",
+  render(props) {
+    return (
+      <BasicTool
+        icon="code-lines"
+        trigger={
+          <div class="flex items-center justify-between w-full">
+            <div class="flex items-center gap-5">
+              <div class="text-12-medium text-text-base capitalize">Edit</div>
+              <div class="flex">
+                <Show when={props.input.filePath?.includes("/")}>
+                  <span class="text-text-weak">{getDirectory(props.input.filePath!)}/</span>
+                </Show>
+                <span class="text-text-strong">{getFilename(props.input.filePath ?? "")}</span>
+              </div>
+            </div>
+            <div class="flex gap-4 items-center justify-end">{/* <DiffChanges diff={diff} /> */}</div>
+          </div>
+        }
+      >
+        <Show when={false && props.output}>
+          <div class="whitespace-pre">{props.output}</div>
+        </Show>
+      </BasicTool>
+    )
+  },
+})
+
+ToolRegistry.register<typeof WriteTool>({
+  name: "write",
+  render(props) {
+    return (
+      <BasicTool
+        icon="code-lines"
+        trigger={
+          <div class="flex items-center justify-between w-full">
+            <div class="flex items-center gap-5">
+              <div class="text-12-medium text-text-base capitalize">Write</div>
+              <div class="flex">
+                <Show when={props.input.filePath?.includes("/")}>
+                  <span class="text-text-weak">{getDirectory(props.input.filePath!)}/</span>
+                </Show>
+                <span class="text-text-strong">{getFilename(props.input.filePath ?? "")}</span>
+              </div>
+            </div>
+            <div class="flex gap-4 items-center justify-end">{/* <DiffChanges diff={diff} /> */}</div>
+          </div>
+        }
+      >
+        <Show when={false && props.output}>
+          <div class="whitespace-pre">{props.output}</div>
+        </Show>
+      </BasicTool>
+    )
+  },
+})

+ 20 - 0
packages/desktop/src/components/diff-changes.tsx

@@ -0,0 +1,20 @@
+import { FileDiff } from "@opencode-ai/sdk"
+import { createMemo, Show } from "solid-js"
+
+export function DiffChanges(props: { diff: FileDiff | FileDiff[] }) {
+  const additions = createMemo(() =>
+    Array.isArray(props.diff) ? props.diff.reduce((acc, diff) => acc + (diff.additions ?? 0), 0) : props.diff.additions,
+  )
+  const deletions = createMemo(() =>
+    Array.isArray(props.diff) ? props.diff.reduce((acc, diff) => acc + (diff.deletions ?? 0), 0) : props.diff.deletions,
+  )
+  const total = createMemo(() => additions() + deletions())
+  return (
+    <Show when={total() > 0}>
+      <div class="flex gap-2 justify-end items-center">
+        <span class="text-12-mono text-right text-text-diff-add-base">{`+${additions()}`}</span>
+        <span class="text-12-mono text-right text-text-diff-delete-base">{`-${deletions()}`}</span>
+      </div>
+    </Show>
+  )
+}

+ 0 - 8
packages/desktop/src/context/local.tsx

@@ -460,13 +460,6 @@ export const { use: useLocal, provider: LocalProvider } = createSimpleContext({
         return sync.data.message[store.active]?.find((m) => m.id === store.activeMessage)
       })
 
-      const activeAssistantMessages = createMemo(() => {
-        if (!store.active || !activeMessage()) return []
-        return sync.data.message[store.active]?.filter(
-          (m) => m.role === "assistant" && m.parentID == activeMessage()?.id,
-        )
-      })
-
       const model = createMemo(() => {
         if (!last()) return
         const model = sync.data.provider.find((x) => x.id === last().providerID)?.models[last().modelID]
@@ -504,7 +497,6 @@ export const { use: useLocal, provider: LocalProvider } = createSimpleContext({
       return {
         active,
         activeMessage,
-        activeAssistantMessages,
         lastUserMessage,
         cost,
         last,

+ 133 - 47
packages/desktop/src/pages/index.tsx

@@ -22,6 +22,10 @@ import { Code } from "@/components/code"
 import { useSync } from "@/context/sync"
 import { useSDK } from "@/context/sdk"
 import { Diff } from "@/components/diff"
+import { ProgressCircle } from "@/components/progress-circle"
+import { AssistantMessage } from "@/components/assistant-message"
+import { type AssistantMessage as AssistantMessageType } from "@opencode-ai/sdk"
+import { DiffChanges } from "@/components/diff-changes"
 
 export default function Page() {
   const local = useLocal()
@@ -92,7 +96,7 @@ export default function Page() {
       }
     }
 
-    if (event.key.length === 1 && event.key !== "Unidentified") {
+    if (event.key.length === 1 && event.key !== "Unidentified" && !(event.ctrlKey || event.metaKey)) {
       inputRef?.focus()
     }
   }
@@ -392,9 +396,6 @@ export default function Page() {
               {(session) => {
                 const diffs = createMemo(() => session.summary?.diffs ?? [])
                 const filesChanged = createMemo(() => diffs().length)
-                const additions = createMemo(() => diffs().reduce((acc, diff) => (acc ?? 0) + (diff.additions ?? 0), 0))
-                const deletions = createMemo(() => diffs().reduce((acc, diff) => (acc ?? 0) + (diff.deletions ?? 0), 0))
-
                 return (
                   <Tooltip placement="right" value={session.title}>
                     <div>
@@ -408,12 +409,7 @@ export default function Page() {
                       </div>
                       <div class="flex justify-between items-center self-stretch">
                         <span class="text-12-regular text-text-weak">{`${filesChanged() || "No"} file${filesChanged() !== 1 ? "s" : ""} changed`}</span>
-                        <Show when={additions() || deletions()}>
-                          <div class="flex gap-2 justify-end items-center">
-                            <span class="text-12-mono text-right text-text-diff-add-base">{`+${additions()}`}</span>
-                            <span class="text-12-mono text-right text-text-diff-delete-base">{`-${deletions()}`}</span>
-                          </div>
-                        </Show>
+                        <DiffChanges diff={diffs()} />
                       </div>
                     </div>
                   </Tooltip>
@@ -434,13 +430,12 @@ export default function Page() {
             <Tabs onChange={handleTabChange}>
               <div class="sticky top-0 shrink-0 flex">
                 <Tabs.List>
-                  <Tabs.Trigger value="chat" class="flex gap-x-1.5 items-center">
+                  <Tabs.Trigger value="chat" class="flex gap-x-4 items-center">
                     <div>Chat</div>
-                    <Show when={local.session.active()}>
-                      <div class="flex flex-col h-4 px-2 -mr-2 justify-center items-center rounded-full bg-surface-base text-12-medium text-text-strong">
-                        {local.session.context()}%
-                      </div>
-                    </Show>
+                    <Tooltip value={`${local.session.tokens() ?? 0} Tokens`} class="flex items-center gap-1.5">
+                      <ProgressCircle percentage={local.session.context() ?? 0} />
+                      <div class="text-14-regular text-text-weak text-right">{local.session.context() ?? 0}%</div>
+                    </Tooltip>
                   </Tabs.Trigger>
                   {/* <Tabs.Trigger value="review">Review</Tabs.Trigger> */}
                   <SortableProvider ids={local.file.opened().map((file) => file.path)}>
@@ -548,33 +543,114 @@ export default function Page() {
                           <Show when={local.session.userMessages().length > 1}>
                             <ul role="list" class="w-60 shrink-0 flex flex-col items-start gap-1">
                               <For each={local.session.userMessages()}>
-                                {(message) => (
-                                  <li
-                                    class="group/li flex items-center gap-x-2 py-1 self-stretch cursor-default"
-                                    onClick={() => local.session.setActiveMessage(message.id)}
-                                  >
-                                    <div class="w-[18px] shrink-0">
-                                      <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 18 12" fill="none">
-                                        <g>
-                                          <rect x="0" width="2" height="12" rx="1" fill="#CFCECD" />
-                                          <rect x="4" width="2" height="12" rx="1" fill="#CFCECD" />
-                                          <rect x="8" width="2" height="12" rx="1" fill="#CFCECD" />
-                                          <rect x="12" width="2" height="12" rx="1" fill="#CFCECD" />
-                                          <rect x="16" width="2" height="12" rx="1" fill="#CFCECD" />
-                                        </g>
-                                      </svg>
-                                    </div>
-                                    <div
-                                      data-active={local.session.activeMessage()?.id === message.id}
-                                      classList={{
-                                        "text-14-regular text-text-weak whitespace-nowrap truncate min-w-0": true,
-                                        "text-text-weak data-[active=true]:text-text-strong group-hover/li:text-text-base": true,
-                                      }}
+                                {(message) => {
+                                  const countLines = (text: string) => {
+                                    if (!text) return 0
+                                    return text.split("\n").length
+                                  }
+
+                                  const additions = createMemo(
+                                    () =>
+                                      message.summary?.diffs.reduce((acc, diff) => acc + (diff.additions ?? 0), 0) ?? 0,
+                                  )
+
+                                  const deletions = createMemo(
+                                    () =>
+                                      message.summary?.diffs.reduce((acc, diff) => acc + (diff.deletions ?? 0), 0) ?? 0,
+                                  )
+
+                                  const totalBeforeLines = createMemo(
+                                    () =>
+                                      message.summary?.diffs.reduce((acc, diff) => acc + countLines(diff.before), 0) ??
+                                      0,
+                                  )
+
+                                  const blockCounts = createMemo(() => {
+                                    const TOTAL_BLOCKS = 5
+
+                                    const adds = additions()
+                                    const dels = deletions()
+                                    const unchanged = Math.max(0, totalBeforeLines() - dels)
+
+                                    const totalActivity = unchanged + adds + dels
+
+                                    if (totalActivity === 0) {
+                                      return { added: 0, deleted: 0, neutral: TOTAL_BLOCKS }
+                                    }
+
+                                    const percentAdded = adds / totalActivity
+                                    const percentDeleted = dels / totalActivity
+                                    const added_raw = percentAdded * TOTAL_BLOCKS
+                                    const deleted_raw = percentDeleted * TOTAL_BLOCKS
+
+                                    let added = adds > 0 ? Math.ceil(added_raw) : 0
+                                    let deleted = dels > 0 ? Math.ceil(deleted_raw) : 0
+
+                                    let total_allocated = added + deleted
+                                    if (total_allocated > TOTAL_BLOCKS) {
+                                      if (added_raw < deleted_raw) {
+                                        added = Math.floor(added_raw)
+                                      } else {
+                                        deleted = Math.floor(deleted_raw)
+                                      }
+
+                                      total_allocated = added + deleted
+                                      if (total_allocated > TOTAL_BLOCKS) {
+                                        if (added_raw < deleted_raw) {
+                                          deleted = Math.floor(deleted_raw)
+                                        } else {
+                                          added = Math.floor(added_raw)
+                                        }
+                                      }
+                                    }
+
+                                    const neutral = Math.max(0, TOTAL_BLOCKS - added - deleted)
+
+                                    return { added, deleted, neutral }
+                                  })
+
+                                  const ADD_COLOR = "var(--icon-diff-add-base)"
+                                  const DELETE_COLOR = "var(--icon-diff-delete-base)"
+                                  const NEUTRAL_COLOR = "var(--icon-weak-base)"
+
+                                  const visibleBlocks = createMemo(() => {
+                                    const counts = blockCounts()
+                                    const blocks = [
+                                      ...Array(counts.added).fill(ADD_COLOR),
+                                      ...Array(counts.deleted).fill(DELETE_COLOR),
+                                      ...Array(counts.neutral).fill(NEUTRAL_COLOR),
+                                    ]
+                                    return blocks.slice(0, 5)
+                                  })
+
+                                  return (
+                                    <li
+                                      class="group/li flex items-center gap-x-2 py-1 self-stretch cursor-default"
+                                      onClick={() => local.session.setActiveMessage(message.id)}
                                     >
-                                      {message.summary?.title ?? local.session.getMessageText(message)}
-                                    </div>
-                                  </li>
-                                )}
+                                      <div class="w-[18px] shrink-0">
+                                        <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 18 12" fill="none">
+                                          <g>
+                                            <For each={visibleBlocks()}>
+                                              {(color, i) => (
+                                                <rect x={i() * 4} width="2" height="12" rx="1" fill={color} />
+                                              )}
+                                            </For>
+                                          </g>
+                                        </svg>
+                                      </div>
+                                      <div
+                                        data-active={local.session.activeMessage()?.id === message.id}
+                                        classList={{
+                                          "text-14-regular text-text-weak whitespace-nowrap truncate min-w-0": true,
+                                          "text-text-weak data-[active=true]:text-text-strong group-hover/li:text-text-base": true,
+                                        }}
+                                      >
+                                        {message.summary?.title ?? local.session.getMessageText(message)}
+                                      </div>
+                                    </li>
+                                  )
+                                }}
                               </For>
                             </ul>
                           </Show>
@@ -585,6 +661,11 @@ export default function Page() {
                                   const title = createMemo(() => message.summary?.title)
                                   const prompt = createMemo(() => local.session.getMessageText(message))
                                   const summary = createMemo(() => message.summary?.body)
+                                  const assistantMessages = createMemo(() => {
+                                    return sync.data.message[activeSession().id]?.filter(
+                                      (m) => m.role === "assistant" && m.parentID == message.id,
+                                    ) as AssistantMessageType[]
+                                  })
 
                                   return (
                                     <div
@@ -633,10 +714,7 @@ export default function Page() {
                                                           </div>
                                                         </div>
                                                         <div class="flex gap-4 items-center justify-end">
-                                                          <div class="flex gap-2 justify-end items-center">
-                                                            <span class="text-12-mono text-right text-text-diff-add-base">{`+${diff.additions}`}</span>
-                                                            <span class="text-12-mono text-right text-text-diff-delete-base">{`-${diff.deletions}`}</span>
-                                                          </div>
+                                                          <DiffChanges diff={diff} />
                                                           <Icon name="chevron-grabber-vertical" size="small" />
                                                         </div>
                                                       </div>
@@ -661,10 +739,18 @@ export default function Page() {
                                         </Show>
                                       </div>
                                       {/* Response */}
-                                      <div data-todo="Response (Timeline)">
+                                      <div data-todo="Response" class="w-full">
                                         <div class="flex flex-col items-start gap-1 self-stretch">
                                           <h2 class="text-12-medium text-text-weak">Response</h2>
                                         </div>
+                                        <div class="w-full flex flex-col items-start self-stretch gap-8">
+                                          <For each={assistantMessages()}>
+                                            {(assistantMessage) => {
+                                              const parts = createMemo(() => sync.data.part[assistantMessage.id])
+                                              return <AssistantMessage message={assistantMessage} parts={parts()} />
+                                            }}
+                                          </For>
+                                        </div>
                                       </div>
                                     </div>
                                   )

+ 3 - 0
packages/opencode/src/tool/tool.ts

@@ -32,6 +32,9 @@ export namespace Tool {
     }>
   }
 
+  export type InferParameters<T extends Info> = T extends Info<infer P> ? z.infer<P> : never
+  export type InferMetadata<T extends Info> = T extends Info<any, infer M> ? M : never
+
   export function define<Parameters extends z.ZodType, Result extends Metadata>(
     id: string,
     init: Info<Parameters, Result>["init"] | Awaited<ReturnType<Info<Parameters, Result>["init"]>>,

+ 224 - 214
packages/ui/script/colors.txt

@@ -1,214 +1,224 @@
-  --background-base: #f8f7f7;
-  --background-weak: var(--smoke-light-3);
-  --background-strong: var(--smoke-light-1);
-  --background-stronger: #fcfcfc;
-  --base: var(--smoke-light-alpha-2);
-  --surface-base: var(--smoke-light-alpha-2);
-  --base2: var(--smoke-light-alpha-2);
-  --base3: var(--smoke-light-alpha-2);
-  --surface-inset-base: var(--smoke-light-alpha-3);
-  --surface-inset-base-hover: var(--smoke-light-alpha-3);
-  --surface-inset-strong: #1f000017;
-  --surface-inset-strong-hover: #1f000017;
-  --surface-raised-base: var(--smoke-light-alpha-1);
-  --surface-float-base: var(--smoke-dark-1);
-  --surface-float-base-hover: var(--smoke-dark-2);
-  --surface-raised-base-hover: var(--smoke-light-alpha-2);
-  --surface-raised-strong: var(--smoke-light-1);
-  --surface-raised-strong-hover: var(--white);
-  --surface-raised-stronger: var(--white);
-  --surface-raised-stronger-hover: var(--white);
-  --surface-weak: var(--smoke-light-alpha-3);
-  --surface-weaker: var(--smoke-light-alpha-4);
-  --surface-strong: #ffffff;
-  --surface-raised-stronger-non-alpha: var(--white);
-  --surface-brand-base: var(--yuzu-light-9);
-  --surface-brand-hover: var(--yuzu-light-10);
-  --surface-interactive-base: var(--cobalt-light-3);
-  --surface-interactive-hover: var(--cobalt-light-4);
-  --surface-interactive-weak: var(--cobalt-light-2);
-  --surface-interactive-weak-hover: var(--cobalt-light-3);
-  --surface-success-base: var(--apple-light-3);
-  --surface-success-weak: var(--apple-light-2);
-  --surface-success-strong: var(--apple-light-9);
-  --surface-warning-base: var(--solaris-light-3);
-  --surface-warning-weak: var(--solaris-light-2);
-  --surface-warning-strong: var(--solaris-light-9);
-  --surface-critical-base: var(--ember-light-3);
-  --surface-critical-weak: var(--ember-light-2);
-  --surface-critical-strong: var(--ember-light-9);
-  --surface-info-base: var(--lilac-light-3);
-  --surface-info-weak: var(--lilac-light-2);
-  --surface-info-strong: var(--lilac-light-9);
-  --surface-diff-skip-base: var(--smoke-light-3);
-  --surface-diff-unchanged-base: #ffffff00;
-  --surface-diff-hidden-base: var(--blue-light-3);
-  --surface-diff-hidden-weak: var(--blue-light-2);
-  --surface-diff-hidden-weaker: var(--blue-light-1);
-  --surface-diff-hidden-strong: var(--blue-light-5);
-  --surface-diff-hidden-stronger: var(--blue-light-9);
-  --surface-diff-add-base: var(--mint-light-3);
-  --surface-diff-add-weak: var(--mint-light-2);
-  --surface-diff-add-weaker: var(--mint-light-1);
-  --surface-diff-add-strong: var(--mint-light-5);
-  --surface-diff-add-stronger: var(--mint-light-9);
-  --surface-diff-delete-base: var(--ember-light-3);
-  --surface-diff-delete-weak: var(--ember-light-2);
-  --surface-diff-delete-weaker: var(--ember-light-1);
-  --surface-diff-delete-strong: var(--ember-light-6);
-  --surface-diff-delete-stronger: var(--ember-light-9);
-  --text-base: var(--smoke-light-11);
-  --input-base: var(--smoke-light-1);
-  --input-hover: var(--smoke-light-2);
-  --input-active: var(--cobalt-light-1);
-  --input-selected: var(--cobalt-light-4);
-  --input-focus: var(--cobalt-light-1);
-  --input-disabled: var(--smoke-light-4);
-  --text-weak: var(--smoke-light-9);
-  --text-weaker: var(--smoke-light-8);
-  --text-strong: var(--smoke-light-12);
-  --text-on-brand-base: var(--smoke-light-alpha-11);
-  --text-on-interactive-base: var(--smoke-light-1);
-  --text-on-success-base: var(--smoke-dark-alpha-11);
-  --text-on-warning-base: var(--smoke-dark-alpha-11);
-  --text-on-info-base: var(--smoke-dark-alpha-11);
-  --text-diff-add-base: var(--mint-light-11);
-  --text-diff-delete-base: var(--ember-light-11);
-  --text-diff-delete-strong: var(--ember-light-12);
-  --text-diff-add-strong: var(--mint-light-12);
-  --text-on-info-weak: var(--smoke-dark-alpha-9);
-  --text-on-info-strong: var(--smoke-dark-alpha-12);
-  --text-on-warning-weak: var(--smoke-dark-alpha-9);
-  --text-on-warning-strong: var(--smoke-dark-alpha-12);
-  --text-on-success-weak: var(--smoke-dark-alpha-9);
-  --text-on-success-strong: var(--smoke-dark-alpha-12);
-  --text-on-brand-weak: var(--smoke-light-alpha-9);
-  --text-on-brand-weaker: var(--smoke-light-alpha-8);
-  --text-on-brand-strong: var(--smoke-light-alpha-12);
-  --button-secondary-base: #fdfcfc;
-  --border-base: var(--smoke-light-alpha-7);
-  --border-hover: var(--smoke-light-alpha-8);
-  --border-active: var(--smoke-light-alpha-9);
-  --border-selected: var(--cobalt-light-alpha-9);
-  --border-disabled: var(--smoke-light-alpha-8);
-  --border-focus: var(--smoke-light-alpha-9);
-  --border-weak-base: var(--smoke-light-alpha-5);
-  --border-strong-base: var(--smoke-light-alpha-7);
-  --border-strong-hover: var(--smoke-light-alpha-8);
-  --border-strong-active: var(--smoke-light-alpha-7);
-  --border-strong-selected: var(--cobalt-light-alpha-6);
-  --border-strong-disabled: var(--smoke-light-alpha-6);
-  --border-strong-focus: var(--smoke-light-alpha-7);
-  --border-weak-hover: var(--smoke-light-alpha-6);
-  --border-weak-active: var(--smoke-light-alpha-7);
-  --border-weak-selected: var(--cobalt-light-alpha-6);
-  --border-weak-disabled: var(--smoke-light-alpha-6);
-  --border-weak-focus: var(--smoke-light-alpha-7);
-  --border-interactive-base: var(--cobalt-light-7);
-  --border-interactive-hover: var(--cobalt-light-8);
-  --border-interactive-active: var(--cobalt-light-9);
-  --border-interactive-selected: var(--cobalt-light-9);
-  --border-interactive-disabled: var(--smoke-light-8);
-  --border-interactive-focus: var(--cobalt-light-9);
-  --border-success-base: var(--apple-light-6);
-  --border-success-hover: var(--apple-light-7);
-  --border-success-selected: var(--apple-light-9);
-  --border-warning-base: var(--solaris-light-6);
-  --border-warning-hover: var(--solaris-light-7);
-  --border-warning-selected: var(--solaris-light-9);
-  --border-critical-base: var(--ember-light-6);
-  --border-critical-hover: var(--ember-light-7);
-  --border-critical-selected: var(--ember-light-9);
-  --border-info-base: var(--lilac-light-6);
-  --border-info-hover: var(--lilac-light-7);
-  --border-info-selected: var(--lilac-light-9);
-  --icon-base: var(--smoke-light-9);
-  --icon-hover: var(--smoke-light-11);
-  --icon-active: var(--smoke-light-12);
-  --icon-selected: var(--smoke-light-12);
-  --icon-disabled: var(--smoke-light-8);
-  --icon-focus: var(--smoke-light-12);
-  --icon-weak-base: var(--smoke-light-7);
-  --icon-invert-base: #ffffff;
-  --icon-weak-hover: var(--smoke-light-8);
-  --icon-weak-active: var(--smoke-light-9);
-  --icon-weak-selected: var(--smoke-light-10);
-  --icon-weak-disabled: var(--smoke-light-6);
-  --icon-weak-focus: var(--smoke-light-9);
-  --icon-strong-base: var(--smoke-light-12);
-  --icon-strong-hover: var(--smoke-light-12);
-  --icon-strong-active: var(--smoke-light-12);
-  --icon-strong-selected: var(--smoke-light-12);
-  --icon-strong-disabled: var(--smoke-light-8);
-  --icon-strong-focus: var(--smoke-light-12);
-  --icon-brand-base: var(--smoke-light-12);
-  --icon-interactive-base: var(--cobalt-light-9);
-  --icon-success-base: var(--apple-light-7);
-  --icon-success-hover: var(--apple-light-8);
-  --icon-success-active: var(--apple-light-11);
-  --icon-warning-base: var(--amber-light-7);
-  --icon-warning-hover: var(--amber-light-8);
-  --icon-warning-active: var(--amber-light-11);
-  --icon-critical-base: var(--ember-light-7);
-  --icon-critical-hover: var(--ember-light-8);
-  --icon-critical-active: var(--ember-light-11);
-  --icon-info-base: var(--lilac-light-7);
-  --icon-info-hover: var(--lilac-light-8);
-  --icon-info-active: var(--lilac-light-11);
-  --icon-on-brand-base: var(--smoke-light-alpha-11);
-  --icon-on-brand-hover: var(--smoke-light-alpha-12);
-  --icon-on-brand-selected: var(--smoke-light-alpha-12);
-  --icon-on-interactive-base: var(--smoke-light-alpha-9);
-  --icon-on-interactive-hover: var(--smoke-light-alpha-10);
-  --icon-on-interactive-selected: var(--smoke-light-alpha-11);
-  --icon-agent-plan-base: var(--purple-light-9);
-  --icon-agent-docs-base: var(--amber-light-9);
-  --icon-agent-ask-base: var(--cyan-light-9);
-  --icon-agent-build-base: var(--blue-light-9);
-  --icon-on-success-base: var(--apple-light-alpha-9);
-  --icon-on-success-hover: var(--apple-light-alpha-10);
-  --icon-on-success-selected: var(--apple-light-alpha-11);
-  --icon-on-warning-base: var(--amber-lightalpha-9);
-  --icon-on-warning-hover: var(--amber-lightalpha-10);
-  --icon-on-warning-selected: var(--amber-lightalpha-11);
-  --icon-on-critical-base: var(--ember-light-alpha-9);
-  --icon-on-critical-hover: var(--ember-light-alpha-10);
-  --icon-on-critical-selected: var(--ember-light-alpha-11);
-  --icon-on-info-base: var(--lilac-light-9);
-  --icon-on-info-hover: var(--lilac-light-alpha-10);
-  --icon-on-info-selected: var(--lilac-light-alpha-11);
-  --icon-diff-add-base: var(--mint-light-11);
-  --icon-diff-add-hover: var(--mint-light-12);
-  --icon-diff-add-active: var(--mint-light-12);
-  --icon-diff-delete-base: var(--ember-light-9);
-  --icon-diff-delete-hover: var(--ember-light-10);
-  --icon-diff-delete-active: var(--ember-light-11);
-  --syntax-comment: #ffffff;
-  --syntax-string: #ffffff;
-  --syntax-keyword: #ffffff;
-  --syntax-function: #ffffff;
-  --syntax-number: #ffffff;
-  --syntax-operator: #ffffff;
-  --syntax-variable: #ffffff;
-  --syntax-type: #ffffff;
-  --syntax-constant: #ffffff;
-  --syntax-punctuation: #ffffff;
-  --syntax-success: #ffffff;
-  --syntax-warning: #ffffff;
-  --syntax-critical: #ffffff;
-  --syntax-info: #ffffff;
-  --markdown-heading: #ffffff;
-  --markdown-text: #ffffff;
-  --markdown-link: #ffffff;
-  --markdown-link-text: #ffffff;
-  --markdown-code: #ffffff;
-  --markdown-block-quote: #ffffff;
-  --markdown-emph: #ffffff;
-  --markdown-strong: #ffffff;
-  --markdown-horizontal-rule: #ffffff;
-  --markdown-list-item: #ffffff;
-  --markdown-list-enumeration: #ffffff;
-  --markdown-image: #ffffff;
-  --markdown-image-text: #ffffff;
-  --markdown-code-block: #ffffff;
-  --border-color: #ffffff;
+--background-base: #F8F7F7;
+--background-weak: var(--smoke-light-3);
+--background-strong: var(--smoke-light-1);
+--background-stronger: #FCFCFC;
+--base: var(--smoke-light-alpha-2);
+--surface-base: var(--smoke-light-alpha-2);
+--surface-base-hover: #0500000F;
+--surface-base-active: var(--smoke-light-alpha-3);
+--surface-base-interactive-active: var(--cobalt-light-alpha-3);
+--base2: var(--smoke-light-alpha-2);
+--base3: var(--smoke-light-alpha-2);
+--surface-inset-base: var(--smoke-light-alpha-2);
+--surface-inset-base-hover: var(--smoke-light-alpha-3);
+--surface-inset-strong: #1F000017;
+--surface-inset-strong-hover: #1F000017;
+--surface-raised-base: var(--smoke-light-alpha-1);
+--surface-float-base: var(--smoke-dark-1);
+--surface-float-base-hover: var(--smoke-dark-2);
+--surface-raised-base-hover: var(--smoke-light-alpha-2);
+--surface-raised-strong: var(--smoke-light-1);
+--surface-raised-strong-hover: var(--white);
+--surface-raised-stronger: var(--white);
+--surface-raised-stronger-hover: var(--white);
+--surface-weak: var(--smoke-light-alpha-3);
+--surface-weaker: var(--smoke-light-alpha-4);
+--surface-strong: #FFFFFF;
+--surface-raised-stronger-non-alpha: var(--white);
+--surface-brand-base: var(--yuzu-light-9);
+--surface-brand-hover: var(--yuzu-light-10);
+--surface-interactive-base: var(--cobalt-light-3);
+--surface-interactive-hover: var(--cobalt-light-4);
+--surface-interactive-weak: var(--cobalt-light-2);
+--surface-interactive-weak-hover: var(--cobalt-light-3);
+--surface-success-base: var(--apple-light-3);
+--surface-success-weak: var(--apple-light-2);
+--surface-success-strong: var(--apple-light-9);
+--surface-warning-base: var(--solaris-light-3);
+--surface-warning-weak: var(--solaris-light-2);
+--surface-warning-strong: var(--solaris-light-9);
+--surface-critical-base: var(--ember-light-3);
+--surface-critical-weak: var(--ember-light-2);
+--surface-critical-strong: var(--ember-light-9);
+--surface-info-base: var(--lilac-light-3);
+--surface-info-weak: var(--lilac-light-2);
+--surface-info-strong: var(--lilac-light-9);
+--surface-diff-hidden-base: var(--blue-light-3);
+--surface-diff-skip-base: var(--smoke-light-2);
+--surface-diff-unchanged-base: #FFFFFF00;
+--surface-diff-hidden-weak: var(--blue-light-2);
+--surface-diff-hidden-weaker: var(--blue-light-1);
+--surface-diff-hidden-strong: var(--blue-light-5);
+--surface-diff-hidden-stronger: var(--blue-light-9);
+--surface-diff-add-base: var(--mint-light-3);
+--surface-diff-add-weak: var(--mint-light-2);
+--surface-diff-add-weaker: var(--mint-light-1);
+--surface-diff-add-strong: var(--mint-light-5);
+--surface-diff-add-stronger: var(--mint-light-9);
+--surface-diff-delete-base: var(--ember-light-3);
+--surface-diff-delete-weak: var(--ember-light-2);
+--surface-diff-delete-weaker: var(--ember-light-1);
+--surface-diff-delete-strong: var(--ember-light-6);
+--surface-diff-delete-stronger: var(--ember-light-9);
+--text-base: var(--smoke-light-11);
+--input-base: var(--smoke-light-1);
+--input-hover: var(--smoke-light-2);
+--input-active: var(--cobalt-light-1);
+--input-selected: var(--cobalt-light-4);
+--input-focus: var(--cobalt-light-1);
+--input-disabled: var(--smoke-light-4);
+--text-weak: var(--smoke-light-9);
+--text-weaker: var(--smoke-light-8);
+--text-strong: var(--smoke-light-12);
+--text-interactive-base: var(--cobalt-light-9);
+--text-on-brand-base: var(--smoke-light-alpha-11);
+--text-on-interactive-base: var(--smoke-light-1);
+--text-on-interactive-weak: var(--smoke-dark-alpha-11);
+--text-on-success-base: var(--smoke-dark-alpha-11);
+--text-on-warning-base: var(--smoke-dark-alpha-11);
+--text-on-info-base: var(--smoke-dark-alpha-11);
+--text-diff-add-base: var(--mint-light-11);
+--text-diff-delete-base: var(--ember-light-11);
+--text-diff-delete-strong: var(--ember-light-12);
+--text-diff-add-strong: var(--mint-light-12);
+--text-on-info-weak: var(--smoke-dark-alpha-9);
+--text-on-info-strong: var(--smoke-dark-alpha-12);
+--text-on-warning-weak: var(--smoke-dark-alpha-9);
+--text-on-warning-strong: var(--smoke-dark-alpha-12);
+--text-on-success-weak: var(--smoke-dark-alpha-9);
+--text-on-success-strong: var(--smoke-dark-alpha-12);
+--text-on-brand-weak: var(--smoke-light-alpha-9);
+--text-on-brand-weaker: var(--smoke-light-alpha-8);
+--text-on-brand-strong: var(--smoke-light-alpha-12);
+--button-secondary-base: #FDFCFC;
+--button-secondary-base-hover: #FAF9F9;
+--border-base: var(--smoke-light-alpha-7);
+--border-hover: var(--smoke-light-alpha-8);
+--border-active: var(--smoke-light-alpha-9);
+--border-selected: var(--cobalt-light-alpha-9);
+--border-disabled: var(--smoke-light-alpha-8);
+--border-focus: var(--smoke-light-alpha-9);
+--border-weak-base: var(--smoke-light-alpha-5);
+--border-strong-base: var(--smoke-light-alpha-7);
+--border-strong-hover: var(--smoke-light-alpha-8);
+--border-strong-active: var(--smoke-light-alpha-7);
+--border-strong-selected: var(--cobalt-light-alpha-6);
+--border-strong-disabled: var(--smoke-light-alpha-6);
+--border-strong-focus: var(--smoke-light-alpha-7);
+--border-weak-hover: var(--smoke-light-alpha-6);
+--border-weak-active: var(--smoke-light-alpha-7);
+--border-weak-selected: var(--cobalt-light-alpha-5);
+--border-weak-disabled: var(--smoke-light-alpha-6);
+--border-weak-focus: var(--smoke-light-alpha-7);
+--border-interactive-base: var(--cobalt-light-7);
+--border-interactive-hover: var(--cobalt-light-8);
+--border-interactive-active: var(--cobalt-light-9);
+--border-interactive-selected: var(--cobalt-light-9);
+--border-interactive-disabled: var(--smoke-light-8);
+--border-interactive-focus: var(--cobalt-light-9);
+--border-success-base: var(--apple-light-6);
+--border-success-hover: var(--apple-light-7);
+--border-success-selected: var(--apple-light-9);
+--border-warning-base: var(--solaris-light-6);
+--border-warning-hover: var(--solaris-light-7);
+--border-warning-selected: var(--solaris-light-9);
+--border-critical-base: var(--ember-light-6);
+--border-critical-hover: var(--ember-light-7);
+--border-critical-selected: var(--ember-light-9);
+--border-info-base: var(--lilac-light-6);
+--border-info-hover: var(--lilac-light-7);
+--border-info-selected: var(--lilac-light-9);
+--icon-base: var(--smoke-light-9);
+--icon-hover: var(--smoke-light-11);
+--icon-active: var(--smoke-light-12);
+--icon-selected: var(--smoke-light-12);
+--icon-disabled: var(--smoke-light-8);
+--icon-focus: var(--smoke-light-12);
+--icon-weak-base: var(--smoke-light-7);
+--icon-invert-base: #FFFFFF;
+--icon-weak-hover: var(--smoke-light-8);
+--icon-weak-active: var(--smoke-light-9);
+--icon-weak-selected: var(--smoke-light-10);
+--icon-weak-disabled: var(--smoke-light-6);
+--icon-weak-focus: var(--smoke-light-9);
+--icon-strong-base: var(--smoke-light-12);
+--icon-strong-hover: var(--smoke-light-12);
+--icon-strong-active: var(--smoke-light-12);
+--icon-strong-selected: var(--smoke-light-12);
+--icon-strong-disabled: var(--smoke-light-8);
+--icon-strong-focus: var(--smoke-light-12);
+--icon-brand-base: var(--smoke-light-12);
+--icon-interactive-base: var(--cobalt-light-9);
+--icon-success-base: var(--apple-light-7);
+--icon-success-hover: var(--apple-light-8);
+--icon-success-active: var(--apple-light-11);
+--icon-warning-base: var(--amber-light-7);
+--icon-warning-hover: var(--amber-light-8);
+--icon-warning-active: var(--amber-light-11);
+--icon-critical-base: var(--ember-light-7);
+--icon-critical-hover: var(--ember-light-8);
+--icon-critical-active: var(--ember-light-11);
+--icon-info-base: var(--lilac-light-7);
+--icon-info-hover: var(--lilac-light-8);
+--icon-info-active: var(--lilac-light-11);
+--icon-on-brand-base: var(--smoke-light-alpha-11);
+--icon-on-brand-hover: var(--smoke-light-alpha-12);
+--icon-on-brand-selected: var(--smoke-light-alpha-12);
+--icon-on-interactive-base: var(--smoke-light-1);
+--icon-agent-plan-base: var(--purple-light-9);
+--icon-agent-docs-base: var(--amber-light-9);
+--icon-agent-ask-base: var(--cyan-light-9);
+--icon-agent-build-base: var(--cobalt-light-9);
+--icon-on-success-base: var(--apple-light-alpha-9);
+--icon-on-success-hover: var(--apple-light-alpha-10);
+--icon-on-success-selected: var(--apple-light-alpha-11);
+--icon-on-warning-base: var(--amber-lightalpha-9);
+--icon-on-warning-hover: var(--amber-lightalpha-10);
+--icon-on-warning-selected: var(--amber-lightalpha-11);
+--icon-on-critical-base: var(--ember-light-alpha-9);
+--icon-on-critical-hover: var(--ember-light-alpha-10);
+--icon-on-critical-selected: var(--ember-light-alpha-11);
+--icon-on-info-base: var(--lilac-light-9);
+--icon-on-info-hover: var(--lilac-light-alpha-10);
+--icon-on-info-selected: var(--lilac-light-alpha-11);
+--icon-diff-add-base: var(--mint-light-11);
+--icon-diff-add-hover: var(--mint-light-12);
+--icon-diff-add-active: var(--mint-light-12);
+--icon-diff-delete-base: var(--ember-light-9);
+--icon-diff-delete-hover: var(--ember-light-10);
+--icon-diff-delete-active: var(--ember-light-11);
+--syntax-comment: #8A8A8A;
+--syntax-string: #D68C27;
+--syntax-keyword: #3B7DD8;
+--syntax-function: #D1383D;
+--syntax-number: #3D9A57;
+--syntax-operator: #D68C27;
+--syntax-variable: #B0851F;
+--syntax-type: #318795;
+--syntax-constant: #953170;
+--syntax-punctuation: #1A1A1A;
+--syntax-success: var(--apple-dark-10);
+--syntax-warning: var(--amber-light-10);
+--syntax-critical: var(--ember-dark-9);
+--syntax-info: var(--lilac-dark-11);
+--markdown-heading: #D68C27;
+--markdown-text: #1A1A1A;
+--markdown-link: #3B7DD8;
+--markdown-link-text: #318795;
+--markdown-code: #3D9A57;
+--markdown-block-quote: #B0851F;
+--markdown-emph: #B0851F;
+--markdown-strong: #D68C27;
+--markdown-horizontal-rule: #8A8A8A;
+--markdown-list-item: #3B7DD8;
+--markdown-list-enumeration: #318795;
+--markdown-image: #3B7DD8;
+--markdown-image-text: #318795;
+--markdown-code-block: #1A1A1A;
+--border-color: #FFFFFF;
+--border-weaker-base: var(--smoke-light-alpha-3);
+--border-weaker-hover: var(--smoke-light-alpha-4);
+--border-weaker-active: var(--smoke-light-alpha-6);
+--border-weaker-selected: var(--cobalt-light-alpha-4);
+--border-weaker-disabled: var(--smoke-light-alpha-2);
+--border-weaker-focus: var(--smoke-light-alpha-6);

+ 39 - 7
packages/ui/src/components/collapsible.css

@@ -1,23 +1,55 @@
 [data-component="collapsible"] {
+  width: 100%;
   display: flex;
   flex-direction: column;
+  background-color: var(--surface-inset-base);
+  border: 1px solid var(--border-weaker-base);
+  transition: background-color 0.15s ease;
+  border-radius: 8px;
+  overflow: clip;
 
-  [data-slot="trigger"] {
-    cursor: pointer;
+  [data-slot="collapsible-trigger"] {
+    width: 100%;
+    display: flex;
+    height: 40px;
+    padding: 6px 8px 6px 12px;
+    align-items: center;
+    align-self: stretch;
+    cursor: default;
     user-select: none;
+    color: var(--text-base);
 
+    /* text-12-medium */
+    font-family: var(--font-family-sans);
+    font-size: var(--font-size-small);
+    font-style: normal;
+    font-weight: var(--font-weight-medium);
+    line-height: var(--line-height-large); /* 166.667% */
+    letter-spacing: var(--letter-spacing-normal);
+
+    /* &:hover { */
+    /*   background-color: var(--surface-base); */
+    /* } */
     &:focus-visible {
-      outline: 2px solid var(--border-focus);
-      outline-offset: 2px;
+      outline: none;
     }
-
     &[data-disabled] {
       cursor: not-allowed;
-      opacity: 0.5;
+    }
+
+    [data-slot="collapsible-arrow"] {
+      width: 24px;
+      height: 24px;
+      display: flex;
+      align-items: center;
+      justify-content: center;
+
+      /* [data-slot="collapsible-arrow-icon"] { */
+      /* } */
     }
   }
 
-  [data-slot="content"] {
+  [data-slot="collapsible-content"] {
     overflow: hidden;
     /* animation: slideUp 250ms ease-out; */
 

+ 12 - 2
packages/ui/src/components/collapsible.tsx

@@ -1,5 +1,6 @@
 import { Collapsible as Kobalte, CollapsibleRootProps } from "@kobalte/core/collapsible"
 import { ComponentProps, ParentProps, splitProps } from "solid-js"
+import { Icon } from "./icon"
 
 export interface CollapsibleProps extends ParentProps<CollapsibleRootProps> {
   class?: string
@@ -21,14 +22,23 @@ function CollapsibleRoot(props: CollapsibleProps) {
 }
 
 function CollapsibleTrigger(props: ComponentProps<typeof Kobalte.Trigger>) {
-  return <Kobalte.Trigger data-slot="trigger" {...props} />
+  return <Kobalte.Trigger data-slot="collapsible-trigger" {...props} />
 }
 
 function CollapsibleContent(props: ComponentProps<typeof Kobalte.Content>) {
-  return <Kobalte.Content data-slot="content" {...props} />
+  return <Kobalte.Content data-slot="collapsible-content" {...props} />
+}
+
+function CollapsibleArrow(props?: ComponentProps<"div">) {
+  return (
+    <div data-slot="collapsible-arrow" {...(props || {})}>
+      <Icon data-slot="collapsible-arrow-icon" name="chevron-grabber-vertical" size="small" />
+    </div>
+  )
 }
 
 export const Collapsible = Object.assign(CollapsibleRoot, {
+  Arrow: CollapsibleArrow,
   Trigger: CollapsibleTrigger,
   Content: CollapsibleContent,
 })

+ 10 - 0
packages/ui/src/components/icon.tsx

@@ -139,6 +139,16 @@ const newIcons = {
   folder: `<path d="M2.08301 2.91675V16.2501H17.9163V5.41675H9.99967L8.33301 2.91675H2.08301Z" stroke="currentColor" stroke-linecap="round"/>`,
   "pencil-line": `<path d="M9.58301 17.9166H17.9163M17.9163 5.83325L14.1663 2.08325L2.08301 14.1666V17.9166H5.83301L17.9163 5.83325Z" stroke="currentColor" stroke-linecap="square"/>`,
   "chevron-grabber-vertical": `<path d="M6.66675 12.4998L10.0001 15.8332L13.3334 12.4998M6.66675 7.49984L10.0001 4.1665L13.3334 7.49984" stroke="currentColor" stroke-linecap="square"/>`,
+  mcp: `<g><path d="M0.972656 9.37176L9.5214 1.60019C10.7018 0.527151 12.6155 0.527151 13.7957 1.60019C14.9761 2.67321 14.9761 4.41295 13.7957 5.48599L7.3397 11.3552" stroke="currentColor" stroke-linecap="round"/><path d="M7.42871 11.2747L13.7957 5.48643C14.9761 4.41338 16.8898 4.41338 18.0702 5.48643L18.1147 5.52688C19.2951 6.59993 19.2951 8.33966 18.1147 9.4127L10.3831 16.4414C9.98966 16.7991 9.98966 17.379 10.3831 17.7366L11.9707 19.1799" stroke="currentColor" stroke-linecap="round"/><path d="M11.6587 3.54346L5.33619 9.29119C4.15584 10.3642 4.15584 12.1039 5.33619 13.177C6.51649 14.25 8.43019 14.25 9.61054 13.177L15.9331 7.42923" stroke="currentColor" stroke-linecap="round"/></g>`,
+  glasses: `<path d="M0.416626 7.91667H1.66663M19.5833 7.91667H18.3333M11.866 7.57987C11.3165 7.26398 10.6793 7.08333 9.99996 7.08333C9.32061 7.08333 8.68344 7.26398 8.13389 7.57987M8.74996 10C8.74996 12.0711 7.07103 13.75 4.99996 13.75C2.92889 13.75 1.24996 12.0711 1.24996 10C1.24996 7.92893 2.92889 6.25 4.99996 6.25C7.07103 6.25 8.74996 7.92893 8.74996 10ZM18.75 10C18.75 12.0711 17.071 13.75 15 13.75C12.9289 13.75 11.25 12.0711 11.25 10C11.25 7.92893 12.9289 6.25 15 6.25C17.071 6.25 18.75 7.92893 18.75 10Z" stroke="currentColor" stroke-linecap="square"/>`,
+  "bullet-list": `<path d="M9.58329 13.7497H17.0833M9.58329 6.24967H17.0833M6.24996 6.24967C6.24996 7.17015 5.50377 7.91634 4.58329 7.91634C3.66282 7.91634 2.91663 7.17015 2.91663 6.24967C2.91663 5.3292 3.66282 4.58301 4.58329 4.58301C5.50377 4.58301 6.24996 5.3292 6.24996 6.24967ZM6.24996 13.7497C6.24996 14.6701 5.50377 15.4163 4.58329 15.4163C3.66282 15.4163 2.91663 14.6701 2.91663 13.7497C2.91663 12.8292 3.66282 12.083 4.58329 12.083C5.50377 12.083 6.24996 12.8292 6.24996 13.7497Z" stroke="currentColor" stroke-linecap="square"/>`,
+  "magnifying-glass-menu": `<path d="M2.08325 10.0002H4.58325M2.08325 5.41683H5.41659M2.08325 14.5835H5.41659M16.4583 13.9585L18.7499 16.2502M17.9166 10.0002C17.9166 12.9917 15.4915 15.4168 12.4999 15.4168C9.50838 15.4168 7.08325 12.9917 7.08325 10.0002C7.08325 7.00862 9.50838 4.5835 12.4999 4.5835C15.4915 4.5835 17.9166 7.00862 17.9166 10.0002Z" stroke="currentColor" stroke-linecap="square"/>`,
+  "window-cursor": `<path d="M17.9166 10.4167V3.75H2.08325V17.0833H10.4166M17.9166 13.5897L11.6666 11.6667L13.5897 17.9167L15.032 15.0321L17.9166 13.5897Z" stroke="currentColor" stroke-width="1.07143" stroke-linecap="square"/><path d="M5.00024 6.125C5.29925 6.12518 5.54126 6.36795 5.54126 6.66699C5.54108 6.96589 5.29914 7.20783 5.00024 7.20801C4.7012 7.20801 4.45843 6.966 4.45825 6.66699C4.45825 6.36784 4.70109 6.125 5.00024 6.125ZM7.91626 6.125C8.21541 6.125 8.45825 6.36784 8.45825 6.66699C8.45808 6.966 8.21531 7.20801 7.91626 7.20801C7.61736 7.20783 7.37542 6.96589 7.37524 6.66699C7.37524 6.36795 7.61726 6.12518 7.91626 6.125ZM10.8333 6.125C11.1324 6.125 11.3752 6.36784 11.3752 6.66699C11.3751 6.966 11.1323 7.20801 10.8333 7.20801C10.5342 7.20801 10.2914 6.966 10.2913 6.66699C10.2913 6.36784 10.5341 6.125 10.8333 6.125Z" fill="currentColor" stroke="currentColor" stroke-width="0.25" stroke-linecap="square"/>`,
+  task: `<path d="M9.99992 2.0835V17.9168M7.08325 3.75016H2.08325V16.2502H7.08325M12.9166 16.2502H17.9166V3.75016H12.9166" stroke="currentColor" stroke-linecap="square"/>`,
+  checklist: `<path d="M9.58342 13.7498H17.0834M9.58342 6.24984H17.0834M2.91675 6.6665L4.58341 7.9165L7.08341 4.1665M2.91675 14.1665L4.58341 15.4165L7.08341 11.6665" stroke="currentColor" stroke-linecap="square"/>`,
+  console: `<path d="M3.75 5.4165L8.33333 9.99984L3.75 14.5832M10.4167 14.5832H16.25" stroke="currentColor" stroke-linecap="square"/>`,
+  "code-lines": `<path d="M2.08325 3.75H11.2499M14.5833 3.75H17.9166M2.08325 10L7.08325 10M10.4166 10L17.9166 10M2.08325 16.25L8.74992 16.25M12.0833 16.25L17.9166 16.25" stroke="currentColor" stroke-linecap="square" stroke-linejoin="round"/>`,
+  "square-arrow-top-right": `<path d="M7.91675 2.9165H2.91675V17.0832H17.0834V12.0832M12.0834 2.9165H17.0834V7.9165M9.58342 10.4165L16.6667 3.33317" stroke="currentColor" stroke-linecap="square"/>`,
 }
 
 export interface IconProps extends ComponentProps<"svg"> {

+ 13 - 3
packages/ui/src/styles/tailwind/colors.css

@@ -9,6 +9,9 @@
   --color-background-stronger: var(--background-stronger);
   --color-base: var(--base);
   --color-surface-base: var(--surface-base);
+  --color-surface-base-hover: var(--surface-base-hover);
+  --color-surface-base-active: var(--surface-base-active);
+  --color-surface-base-interactive-active: var(--surface-base-interactive-active);
   --color-base2: var(--base2);
   --color-base3: var(--base3);
   --color-surface-inset-base: var(--surface-inset-base);
@@ -45,9 +48,9 @@
   --color-surface-info-base: var(--surface-info-base);
   --color-surface-info-weak: var(--surface-info-weak);
   --color-surface-info-strong: var(--surface-info-strong);
+  --color-surface-diff-hidden-base: var(--surface-diff-hidden-base);
   --color-surface-diff-skip-base: var(--surface-diff-skip-base);
   --color-surface-diff-unchanged-base: var(--surface-diff-unchanged-base);
-  --color-surface-diff-hidden-base: var(--surface-diff-hidden-base);
   --color-surface-diff-hidden-weak: var(--surface-diff-hidden-weak);
   --color-surface-diff-hidden-weaker: var(--surface-diff-hidden-weaker);
   --color-surface-diff-hidden-strong: var(--surface-diff-hidden-strong);
@@ -72,8 +75,10 @@
   --color-text-weak: var(--text-weak);
   --color-text-weaker: var(--text-weaker);
   --color-text-strong: var(--text-strong);
+  --color-text-interactive-base: var(--text-interactive-base);
   --color-text-on-brand-base: var(--text-on-brand-base);
   --color-text-on-interactive-base: var(--text-on-interactive-base);
+  --color-text-on-interactive-weak: var(--text-on-interactive-weak);
   --color-text-on-success-base: var(--text-on-success-base);
   --color-text-on-warning-base: var(--text-on-warning-base);
   --color-text-on-info-base: var(--text-on-info-base);
@@ -91,6 +96,7 @@
   --color-text-on-brand-weaker: var(--text-on-brand-weaker);
   --color-text-on-brand-strong: var(--text-on-brand-strong);
   --color-button-secondary-base: var(--button-secondary-base);
+  --color-button-secondary-base-hover: var(--button-secondary-base-hover);
   --color-border-base: var(--border-base);
   --color-border-hover: var(--border-hover);
   --color-border-active: var(--border-active);
@@ -164,8 +170,6 @@
   --color-icon-on-brand-hover: var(--icon-on-brand-hover);
   --color-icon-on-brand-selected: var(--icon-on-brand-selected);
   --color-icon-on-interactive-base: var(--icon-on-interactive-base);
-  --color-icon-on-interactive-hover: var(--icon-on-interactive-hover);
-  --color-icon-on-interactive-selected: var(--icon-on-interactive-selected);
   --color-icon-agent-plan-base: var(--icon-agent-plan-base);
   --color-icon-agent-docs-base: var(--icon-agent-docs-base);
   --color-icon-agent-ask-base: var(--icon-agent-ask-base);
@@ -217,4 +221,10 @@
   --color-markdown-image-text: var(--markdown-image-text);
   --color-markdown-code-block: var(--markdown-code-block);
   --color-border-color: var(--border-color);
+  --color-border-weaker-base: var(--border-weaker-base);
+  --color-border-weaker-hover: var(--border-weaker-hover);
+  --color-border-weaker-active: var(--border-weaker-active);
+  --color-border-weaker-selected: var(--border-weaker-selected);
+  --color-border-weaker-disabled: var(--border-weaker-disabled);
+  --color-border-weaker-focus: var(--border-weaker-focus);
 }

+ 35 - 15
packages/ui/src/styles/theme.css

@@ -66,11 +66,14 @@
   --background-weak: var(--smoke-light-3);
   --background-strong: var(--smoke-light-1);
   --background-stronger: #fcfcfc;
-  --surface-base: var(--smoke-light-alpha-2);
   --base: var(--smoke-light-alpha-2);
+  --surface-base: var(--smoke-light-alpha-2);
+  --surface-base-hover: #0500000f;
+  --surface-base-active: var(--smoke-light-alpha-3);
+  --surface-base-interactive-active: var(--cobalt-light-alpha-3);
   --base2: var(--smoke-light-alpha-2);
   --base3: var(--smoke-light-alpha-2);
-  --surface-inset-base: var(--smoke-light-alpha-3);
+  --surface-inset-base: var(--smoke-light-alpha-2);
   --surface-inset-base-hover: var(--smoke-light-alpha-3);
   --surface-inset-strong: #1f000017;
   --surface-inset-strong-hover: #1f000017;
@@ -105,7 +108,7 @@
   --surface-info-weak: var(--lilac-light-2);
   --surface-info-strong: var(--lilac-light-9);
   --surface-diff-hidden-base: var(--blue-light-3);
-  --surface-diff-skip-base: var(--smoke-light-3);
+  --surface-diff-skip-base: var(--smoke-light-2);
   --surface-diff-unchanged-base: #ffffff00;
   --surface-diff-hidden-weak: var(--blue-light-2);
   --surface-diff-hidden-weaker: var(--blue-light-1);
@@ -131,6 +134,7 @@
   --text-weak: var(--smoke-light-9);
   --text-weaker: var(--smoke-light-8);
   --text-strong: var(--smoke-light-12);
+  --text-interactive-base: var(--cobalt-light-9);
   --text-on-brand-base: var(--smoke-light-alpha-11);
   --text-on-interactive-base: var(--smoke-light-1);
   --text-on-interactive-weak: var(--smoke-dark-alpha-11);
@@ -151,7 +155,7 @@
   --text-on-brand-weaker: var(--smoke-light-alpha-8);
   --text-on-brand-strong: var(--smoke-light-alpha-12);
   --button-secondary-base: #fdfcfc;
-  --button-secondary-base-hover: var(--smoke-light-2);
+  --button-secondary-base-hover: #faf9f9;
   --border-base: var(--smoke-light-alpha-7);
   --border-hover: var(--smoke-light-alpha-8);
   --border-active: var(--smoke-light-alpha-9);
@@ -167,7 +171,7 @@
   --border-strong-focus: var(--smoke-light-alpha-7);
   --border-weak-hover: var(--smoke-light-alpha-6);
   --border-weak-active: var(--smoke-light-alpha-7);
-  --border-weak-selected: var(--cobalt-light-alpha-4);
+  --border-weak-selected: var(--cobalt-light-alpha-5);
   --border-weak-disabled: var(--smoke-light-alpha-6);
   --border-weak-focus: var(--smoke-light-alpha-7);
   --border-interactive-base: var(--cobalt-light-7);
@@ -228,7 +232,7 @@
   --icon-agent-plan-base: var(--purple-light-9);
   --icon-agent-docs-base: var(--amber-light-9);
   --icon-agent-ask-base: var(--cyan-light-9);
-  --icon-agent-build-base: var(--blue-light-9);
+  --icon-agent-build-base: var(--cobalt-light-9);
   --icon-on-success-base: var(--apple-light-alpha-9);
   --icon-on-success-hover: var(--apple-light-alpha-10);
   --icon-on-success-selected: var(--apple-light-alpha-11);
@@ -276,6 +280,12 @@
   --markdown-image-text: #318795;
   --markdown-code-block: #1a1a1a;
   --border-color: #ffffff;
+  --border-weaker-base: var(--smoke-light-alpha-3);
+  --border-weaker-hover: var(--smoke-light-alpha-4);
+  --border-weaker-active: var(--smoke-light-alpha-6);
+  --border-weaker-selected: var(--cobalt-light-alpha-4);
+  --border-weaker-disabled: var(--smoke-light-alpha-2);
+  --border-weaker-focus: var(--smoke-light-alpha-6);
 
   @media (prefers-color-scheme: dark) {
     /* OC-1-Dark */
@@ -284,8 +294,11 @@
     --background-weak: #201d1d;
     --background-strong: #151313;
     --background-stronger: #201c1c;
-    --surface-base: var(--smoke-dark-alpha-3);
     --base: var(--smoke-dark-alpha-2);
+    --surface-base: var(--smoke-dark-alpha-2);
+    --surface-base-hover: #e0b7b716;
+    --surface-base-active: var(--smoke-dark-alpha-3);
+    --surface-base-interactive-active: var(--cobalt-dark-alpha-2);
     --base2: var(--smoke-dark-alpha-2);
     --base3: var(--smoke-dark-alpha-2);
     --surface-inset-base: #0e0b0b7f;
@@ -300,8 +313,8 @@
     --surface-raised-strong-hover: var(--smoke-dark-alpha-6);
     --surface-raised-stronger: var(--smoke-dark-alpha-6);
     --surface-raised-stronger-hover: var(--smoke-dark-alpha-7);
-    --surface-weak: var(--smoke-dark-alpha-5);
-    --surface-weaker: var(--smoke-dark-alpha-6);
+    --surface-weak: var(--smoke-dark-alpha-4);
+    --surface-weaker: var(--smoke-dark-alpha-5);
     --surface-strong: var(--smoke-dark-alpha-7);
     --surface-raised-stronger-non-alpha: var(--smoke-dark-4);
     --surface-brand-base: var(--yuzu-light-9);
@@ -323,7 +336,7 @@
     --surface-info-weak: var(--lilac-light-2);
     --surface-info-strong: var(--lilac-light-9);
     --surface-diff-hidden-base: var(--blue-dark-2);
-    --surface-diff-skip-base: var(--smoke-dark-alpha-2);
+    --surface-diff-skip-base: var(--smoke-dark-alpha-1);
     --surface-diff-unchanged-base: var(--smoke-dark-1);
     --surface-diff-hidden-weak: var(--blue-dark-1);
     --surface-diff-hidden-weaker: var(--blue-dark-3);
@@ -349,6 +362,7 @@
     --text-weak: var(--smoke-dark-alpha-9);
     --text-weaker: var(--smoke-dark-alpha-8);
     --text-strong: var(--smoke-dark-alpha-12);
+    --text-interactive-base: var(--cobalt-dark-11);
     --text-on-brand-base: var(--smoke-dark-alpha-11);
     --text-on-interactive-base: var(--smoke-dark-12);
     --text-on-interactive-weak: var(--smoke-dark-alpha-11);
@@ -368,12 +382,12 @@
     --text-on-brand-weak: var(--smoke-dark-alpha-9);
     --text-on-brand-weaker: var(--smoke-dark-alpha-8);
     --text-on-brand-strong: var(--smoke-dark-alpha-12);
-    --button-secondary-base: var(--smoke-dark-6);
-    --button-secondary-base-hover: var(--smoke-dark-5);
+    --button-secondary-base: var(--smoke-dark-4);
+    --button-secondary-base-hover: #2a2727;
     --border-base: var(--smoke-dark-alpha-7);
     --border-hover: var(--smoke-dark-alpha-8);
     --border-active: var(--smoke-dark-alpha-9);
-    --border-selected: var(--cobalt-dark-alpha-9);
+    --border-selected: var(--cobalt-dark-alpha-11);
     --border-disabled: var(--smoke-dark-alpha-8);
     --border-focus: var(--smoke-dark-alpha-9);
     --border-weak-base: var(--smoke-dark-alpha-6);
@@ -385,7 +399,7 @@
     --border-strong-focus: var(--smoke-dark-alpha-8);
     --border-weak-hover: var(--smoke-dark-alpha-7);
     --border-weak-active: var(--smoke-dark-alpha-8);
-    --border-weak-selected: var(--cobalt-dark-alpha-3);
+    --border-weak-selected: var(--cobalt-dark-alpha-6);
     --border-weak-disabled: var(--smoke-dark-alpha-6);
     --border-weak-focus: var(--smoke-dark-alpha-8);
     --border-interactive-base: var(--cobalt-light-7);
@@ -446,7 +460,7 @@
     --icon-agent-plan-base: var(--purple-dark-9);
     --icon-agent-docs-base: var(--amber-dark-9);
     --icon-agent-ask-base: var(--cyan-dark-9);
-    --icon-agent-build-base: var(--blue-dark-9);
+    --icon-agent-build-base: var(--cobalt-dark-11);
     --icon-on-success-base: var(--apple-dark-alpha-9);
     --icon-on-success-hover: var(--apple-dark-alpha-10);
     --icon-on-success-selected: var(--apple-dark-alpha-11);
@@ -494,5 +508,11 @@
     --markdown-image-text: #56b6c2;
     --markdown-code-block: #eeeeee;
     --border-color: #ffffff;
+    --border-weaker-base: var(--smoke-dark-alpha-3);
+    --border-weaker-hover: var(--smoke-dark-alpha-4);
+    --border-weaker-active: var(--smoke-dark-alpha-6);
+    --border-weaker-selected: var(--cobalt-dark-alpha-3);
+    --border-weaker-disabled: var(--smoke-dark-alpha-2);
+    --border-weaker-focus: var(--smoke-dark-alpha-6);
   }
 }