Przeglądaj źródła

wip: desktop work

Adam 3 miesięcy temu
rodzic
commit
5b86fa9109

+ 33 - 37
packages/desktop/src/components/assistant-message.tsx

@@ -1,4 +1,4 @@
-import type { Part, AssistantMessage, ReasoningPart, TextPart, ToolPart } from "@opencode-ai/sdk"
+import type { Part, AssistantMessage, ReasoningPart, TextPart, ToolPart, Message } from "@opencode-ai/sdk"
 import { children, Component, createMemo, For, Match, Show, Switch, type JSX } from "solid-js"
 import { Dynamic } from "solid-js/web"
 import { Markdown } from "./markdown"
@@ -17,47 +17,44 @@ import type { WriteTool } from "opencode/tool/write"
 import type { TodoWriteTool } from "opencode/tool/todo"
 import { DiffChanges } from "./diff-changes"
 
-export function AssistantMessage(props: { message: AssistantMessage; parts: Part[]; lastToolOnly?: boolean }) {
+export function AssistantMessage(props: { message: AssistantMessage; parts: Part[] }) {
   const filteredParts = createMemo(() => {
-    let tool = false
     return props.parts?.filter((x) => {
-      if (x.type === "tool" && props.lastToolOnly && tool) return false
-      if (x.type === "tool") tool = true
+      if (x.type === "reasoning") return false
       return x.type !== "tool" || x.tool !== "todoread"
     })
   })
   return (
     <div class="w-full flex flex-col items-start gap-4">
-      <For each={filteredParts()}>
-        {(part) => {
-          const component = createMemo(() => PART_MAPPING[part.type as keyof typeof PART_MAPPING])
-          return (
-            <Show when={component()}>
-              <Dynamic component={component()} part={part as any} message={props.message} />
-            </Show>
-          )
-        }}
-      </For>
+      <For each={filteredParts()}>{(part) => <Part part={part} message={props.message} />}</For>
     </div>
   )
 }
 
+export function Part(props: { part: Part; message: Message; readonly?: boolean }) {
+  const component = createMemo(() => PART_MAPPING[props.part.type as keyof typeof PART_MAPPING])
+  return (
+    <Show when={component()}>
+      <Dynamic component={component()} part={props.part as any} message={props.message} readonly={props.readonly} />
+    </Show>
+  )
+}
+
 const PART_MAPPING = {
   text: TextPart,
   tool: ToolPart,
   reasoning: ReasoningPart,
 }
 
-function ReasoningPart(props: { part: ReasoningPart; message: AssistantMessage }) {
-  return null
-  // return (
-  //   <Show when={props.part.text.trim()}>
-  //     <div>{props.part.text}</div>
-  //   </Show>
-  // )
+function ReasoningPart(props: { part: ReasoningPart; message: Message }) {
+  return (
+    <Show when={props.part.text.trim()}>
+      <Markdown text={props.part.text.trim()} />
+    </Show>
+  )
 }
 
-function TextPart(props: { part: TextPart; message: AssistantMessage }) {
+function TextPart(props: { part: TextPart; message: Message }) {
   return (
     <Show when={props.part.text.trim()}>
       <Markdown text={props.part.text.trim()} />
@@ -65,17 +62,11 @@ function TextPart(props: { part: TextPart; message: AssistantMessage }) {
   )
 }
 
-function ToolPart(props: { part: ToolPart; message: AssistantMessage }) {
-  // const sync = useSync()
-
+function ToolPart(props: { part: ToolPart; message: Message; readonly?: boolean }) {
   const component = createMemo(() => {
     const render = ToolRegistry.render(props.part.tool) ?? GenericTool
-
     const metadata = props.part.state.status === "pending" ? {} : (props.part.state.metadata ?? {})
     const input = props.part.state.status === "completed" ? props.part.state.input : {}
-    // const permissions = sync.data.permission[props.message.sessionID] ?? []
-    // const permissionIndex = permissions.findIndex((x) => x.callID === props.part.callID)
-    // const permission = permissions[permissionIndex]
 
     return (
       <Dynamic
@@ -83,8 +74,8 @@ function ToolPart(props: { part: ToolPart; message: AssistantMessage }) {
         input={input}
         tool={props.part.tool}
         metadata={metadata}
-        // permission={permission?.metadata ?? {}}
         output={props.part.state.status === "completed" ? props.part.state.output : undefined}
+        readonly={props.readonly}
       />
     )
   })
@@ -106,7 +97,12 @@ const isTriggerTitle = (val: any): val is TriggerTitle => {
   return typeof val === "object" && val !== null && "title" in val && !(val instanceof Node)
 }
 
-function BasicTool(props: { icon: IconProps["name"]; trigger: TriggerTitle | JSX.Element; children?: JSX.Element }) {
+function BasicTool(props: {
+  icon: IconProps["name"]
+  trigger: TriggerTitle | JSX.Element
+  children?: JSX.Element
+  readonly?: boolean
+}) {
   const resolved = children(() => props.children)
   return (
     <Collapsible>
@@ -161,13 +157,13 @@ function BasicTool(props: { icon: IconProps["name"]; trigger: TriggerTitle | JSX
               </Switch>
             </div>
           </div>
-          <Show when={resolved()}>
+          <Show when={resolved() && !props.readonly}>
             <Collapsible.Arrow />
           </Show>
         </div>
       </Collapsible.Trigger>
-      <Show when={props.children}>
-        <Collapsible.Content>{props.children}</Collapsible.Content>
+      <Show when={resolved() && !props.readonly}>
+        <Collapsible.Content>{resolved()}</Collapsible.Content>
       </Show>
     </Collapsible>
     // <>
@@ -177,15 +173,15 @@ function BasicTool(props: { icon: IconProps["name"]; trigger: TriggerTitle | JSX
 }
 
 function GenericTool(props: ToolProps<any>) {
-  return <BasicTool icon="mcp" trigger={{ title: props.tool }} />
+  return <BasicTool icon="mcp" trigger={{ title: props.tool }} readonly={props.readonly} />
 }
 
 type ToolProps<T extends Tool.Info> = {
   input: Partial<Tool.InferParameters<T>>
   metadata: Partial<Tool.InferMetadata<T>>
-  // permission: Record<string, any>
   tool: string
   output?: string
+  readonly?: boolean
 }
 
 const ToolRegistry = (() => {

+ 72 - 15
packages/desktop/src/pages/index.tsx

@@ -33,7 +33,7 @@ import { Code } from "@/components/code"
 import { useSync } from "@/context/sync"
 import { useSDK } from "@/context/sdk"
 import { ProgressCircle } from "@/components/progress-circle"
-import { AssistantMessage } from "@/components/assistant-message"
+import { AssistantMessage, Part } from "@/components/assistant-message"
 import { type AssistantMessage as AssistantMessageType } from "@opencode-ai/sdk"
 import { DiffChanges } from "@/components/diff-changes"
 
@@ -178,6 +178,8 @@ export default function Page() {
   }
 
   const handleDiffTriggerClick = (event: MouseEvent) => {
+    // disabling scroll to diff for now
+    return
     const target = event.currentTarget as HTMLElement
     queueMicrotask(() => {
       if (target.getAttribute("aria-expanded") !== "true") return
@@ -636,6 +638,7 @@ export default function Page() {
                             <div class="flex flex-col items-start gap-50 pb-50">
                               <For each={local.session.userMessages()}>
                                 {(message) => {
+                                  const [expanded, setExpanded] = createSignal(false)
                                   const title = createMemo(() => message.summary?.title)
                                   const prompt = createMemo(() => local.session.getMessageText(message))
                                   const summary = createMemo(() => message.summary?.body)
@@ -649,15 +652,12 @@ export default function Page() {
                                     if (!last) return false
                                     return !last.time.completed
                                   })
-                                  const lastWithContent = createMemo(() =>
-                                    assistantMessages().findLast((m) => {
-                                      const parts = sync.data.part[m.id]
-                                      return parts?.find((p) => p.type === "text" || p.type === "tool")
-                                    }),
-                                  )
 
                                   return (
-                                    <div data-message={message.id} class="flex flex-col items-start self-stretch gap-8">
+                                    <div
+                                      data-message={message.id}
+                                      class="flex flex-col items-start self-stretch gap-8 min-h-[calc(100vh-15rem)]"
+                                    >
                                       {/* Title */}
                                       <div class="py-2 flex flex-col items-start gap-2 self-stretch sticky top-0 bg-background-stronger">
                                         <h1 class="text-14-medium text-text-strong overflow-hidden text-ellipsis min-w-0">
@@ -665,14 +665,19 @@ export default function Page() {
                                         </h1>
                                       </div>
                                       <Show when={title}>
-                                        <div class="-mt-5 text-12-regular text-text-base line-clamp-3">{prompt()}</div>
+                                        <div class="-mt-8 text-12-regular text-text-base line-clamp-3">{prompt()}</div>
                                       </Show>
                                       {/* Response */}
                                       <div class="w-full flex flex-col gap-2">
-                                        <Collapsible variant="ghost">
+                                        <Collapsible variant="ghost" open={expanded()} onOpenChange={setExpanded}>
                                           <Collapsible.Trigger class="text-text-weak hover:text-text-strong">
                                             <div class="flex items-center gap-1 self-stretch">
-                                              <h2 class="text-12-medium">Show steps</h2>
+                                              <h2 class="text-12-medium">
+                                                <Switch>
+                                                  <Match when={expanded()}>Hide steps</Match>
+                                                  <Match when={!expanded()}>Show steps</Match>
+                                                </Switch>
+                                              </h2>
                                               <Collapsible.Arrow />
                                             </div>
                                           </Collapsible.Trigger>
@@ -687,11 +692,63 @@ export default function Page() {
                                             </div>
                                           </Collapsible.Content>
                                         </Collapsible>
-                                        <Show when={working() && lastWithContent()}>
-                                          {(last) => {
-                                            const lastParts = createMemo(() => sync.data.part[last().id])
+                                        <Show when={working() && !expanded()}>
+                                          {(_) => {
+                                            const lastMessageWithText = createMemo(() =>
+                                              assistantMessages().findLast((m) => {
+                                                const parts = sync.data.part[m.id]
+                                                return parts?.find((p) => p.type === "text")
+                                              }),
+                                            )
+                                            const lastMessageWithReasoning = createMemo(() =>
+                                              assistantMessages().findLast((m) => {
+                                                const parts = sync.data.part[m.id]
+                                                return parts?.find((p) => p.type === "reasoning")
+                                              }),
+                                            )
+                                            const lastMessageWithTool = createMemo(() =>
+                                              assistantMessages().findLast((m) => {
+                                                const parts = sync.data.part[m.id]
+                                                return parts?.find(
+                                                  (p) => p.type === "tool" && p.state.status === "completed",
+                                                )
+                                              }),
+                                            )
                                             return (
-                                              <AssistantMessage lastToolOnly message={last()} parts={lastParts()} />
+                                              <div class="w-full flex flex-col gap-2">
+                                                <Switch>
+                                                  <Match when={lastMessageWithText()}>
+                                                    {(last) => {
+                                                      const lastTextPart = createMemo(() =>
+                                                        sync.data.part[last().id].findLast((p) => p.type === "text"),
+                                                      )
+                                                      return <Part message={last()} part={lastTextPart()!} readonly />
+                                                    }}
+                                                  </Match>
+                                                  <Match when={lastMessageWithReasoning()}>
+                                                    {(last) => {
+                                                      const lastReasoningPart = createMemo(() =>
+                                                        sync.data.part[last().id].findLast(
+                                                          (p) => p.type === "reasoning",
+                                                        ),
+                                                      )
+                                                      return (
+                                                        <Part message={last()} part={lastReasoningPart()!} readonly />
+                                                      )
+                                                    }}
+                                                  </Match>
+                                                </Switch>
+                                                <Show when={lastMessageWithTool()}>
+                                                  {(last) => {
+                                                    const lastToolPart = createMemo(() =>
+                                                      sync.data.part[last().id].findLast(
+                                                        (p) => p.type === "tool" && p.state.status === "completed",
+                                                      ),
+                                                    )
+                                                    return <Part message={last()} part={lastToolPart()!} readonly />
+                                                  }}
+                                                </Show>
+                                              </div>
                                             )
                                           }}
                                         </Show>

+ 1 - 0
packages/ui/src/components/diff.css

@@ -18,6 +18,7 @@
   [data-slot="diff-hunk-separator-content"] {
     position: sticky;
     background-color: var(--surface-diff-hidden-base);
+    color: var(--text-base);
     width: var(--pjs-column-content-width);
     left: var(--pjs-column-number-width);
     padding-left: 8px;

+ 0 - 29
packages/ui/src/styles/utilities.css

@@ -48,35 +48,6 @@
   border-width: 0;
 }
 
-.scroller {
-  --fade-height: 1.5rem;
-
-  --mask-top: linear-gradient(to bottom, transparent, black var(--fade-height));
-  --mask-bottom: linear-gradient(to top, transparent, black var(--fade-height));
-
-  mask-image: var(--mask-top), var(--mask-bottom);
-  mask-repeat: no-repeat;
-
-  mask-size: 100% var(--fade-height);
-
-  animation: adjust-masks linear;
-  animation-timeline: scroll(self);
-}
-
-@keyframes adjust-masks {
-  from {
-    mask-position:
-      0 calc(0% - var(--fade-height)),
-      0 100%;
-  }
-
-  to {
-    mask-position:
-      0 0,
-      0 calc(100% + var(--fade-height));
-  }
-}
-
 .truncate-start {
   text-overflow: ellipsis;
   overflow: hidden;