Adam 2 месяцев назад
Родитель
Сommit
28aba35ff9

+ 5 - 5
packages/desktop/src/pages/layout.tsx

@@ -413,11 +413,11 @@ export default function Layout(props: ParentProps) {
     const updated = createMemo(() => DateTime.fromMillis(props.session.time.updated))
     const updated = createMemo(() => DateTime.fromMillis(props.session.time.updated))
     const notifications = createMemo(() => notification.session.unseen(props.session.id))
     const notifications = createMemo(() => notification.session.unseen(props.session.id))
     const hasError = createMemo(() => notifications().some((n) => n.type === "error"))
     const hasError = createMemo(() => notifications().some((n) => n.type === "error"))
-    const isWorking = createMemo(
-      () =>
-        props.session.id !== params.id &&
-        globalSync.child(props.project.worktree)[0].session_status[props.session.id]?.type === "busy",
-    )
+    const isWorking = createMemo(() => {
+      if (props.session.id === params.id) return false
+      const status = globalSync.child(props.project.worktree)[0].session_status[props.session.id]
+      return status?.type === "busy" || status?.type === "retry"
+    })
     return (
     return (
       <>
       <>
         <div
         <div

+ 14 - 0
packages/ui/src/components/session-turn.css

@@ -238,6 +238,10 @@
     justify-content: space-between;
     justify-content: space-between;
     width: 100%;
     width: 100%;
     gap: 20px;
     gap: 20px;
+
+    [data-expandable="false"] {
+      pointer-events: none;
+    }
   }
   }
 
 
   [data-slot="session-turn-file-info"] {
   [data-slot="session-turn-file-info"] {
@@ -323,6 +327,16 @@
       height: 14px;
       height: 14px;
     }
     }
   }
   }
+  [data-slot="session-turn-retry-message"] {
+    font-weight: 500;
+    color: var(--syntax-critical);
+  }
+  [data-slot="session-turn-retry-seconds"] {
+    color: var(--text-weak);
+  }
+  [data-slot="session-turn-retry-attempt"] {
+    color: var(--text-weak);
+  }
 
 
   [data-slot="session-turn-details-text"] {
   [data-slot="session-turn-details-text"] {
     font-size: 13px; /* text-12-medium */
     font-size: 13px; /* text-12-medium */

+ 43 - 3
packages/ui/src/components/session-turn.tsx

@@ -3,7 +3,7 @@ import { useData } from "../context"
 import { useDiffComponent } from "../context/diff"
 import { useDiffComponent } from "../context/diff"
 import { getDirectory, getFilename } from "@opencode-ai/util/path"
 import { getDirectory, getFilename } from "@opencode-ai/util/path"
 import { checksum } from "@opencode-ai/util/encode"
 import { checksum } from "@opencode-ai/util/encode"
-import { createEffect, createMemo, For, Match, onCleanup, ParentProps, Show, Switch } from "solid-js"
+import { createEffect, createMemo, createSignal, For, Match, onCleanup, ParentProps, Show, Switch } from "solid-js"
 import { createResizeObserver } from "@solid-primitives/resize-observer"
 import { createResizeObserver } from "@solid-primitives/resize-observer"
 import { DiffChanges } from "./diff-changes"
 import { DiffChanges } from "./diff-changes"
 import { Typewriter } from "./typewriter"
 import { Typewriter } from "./typewriter"
@@ -49,6 +49,29 @@ export function SessionTurn(
       },
       },
   )
   )
   const working = createMemo(() => status()?.type !== "idle")
   const working = createMemo(() => status()?.type !== "idle")
+  const retry = createMemo(() => {
+    const s = status()
+    if (s.type !== "retry") return
+    return s
+  })
+  const [retrySeconds, setRetrySeconds] = createSignal(0)
+
+  createEffect(() => {
+    const r = retry()
+    if (!r) {
+      setRetrySeconds(0)
+      return
+    }
+
+    const updateSeconds = () => {
+      const next = r.next
+      if (next) setRetrySeconds(Math.max(0, Math.round((next - Date.now()) / 1000)))
+    }
+    updateSeconds()
+
+    const timer = setInterval(updateSeconds, 1000)
+    onCleanup(() => clearInterval(timer))
+  })
 
 
   let scrollRef: HTMLDivElement | undefined
   let scrollRef: HTMLDivElement | undefined
   const [state, setState] = createStore({
   const [state, setState] = createStore({
@@ -300,10 +323,12 @@ export function SessionTurn(
                   {/* Trigger (sticky) */}
                   {/* Trigger (sticky) */}
                   <div ref={(el) => setState("stickyTriggerRef", el)} data-slot="session-turn-response-trigger">
                   <div ref={(el) => setState("stickyTriggerRef", el)} data-slot="session-turn-response-trigger">
                     <Button
                     <Button
+                      data-expandable={assistantMessages().length > 0}
                       data-slot="session-turn-collapsible-trigger-content"
                       data-slot="session-turn-collapsible-trigger-content"
                       variant="ghost"
                       variant="ghost"
                       size="small"
                       size="small"
                       onClick={() => {
                       onClick={() => {
+                        if (assistantMessages().length === 0) return
                         const next = !store.stepsExpanded
                         const next = !store.stepsExpanded
                         setStore("stepsExpanded", next)
                         setStore("stepsExpanded", next)
                         props.onStepsExpandedChange?.(next)
                         props.onStepsExpandedChange?.(next)
@@ -313,17 +338,32 @@ export function SessionTurn(
                         <Spinner />
                         <Spinner />
                       </Show>
                       </Show>
                       <Switch>
                       <Switch>
+                        <Match when={retry()}>
+                          <span data-slot="session-turn-retry-message">
+                            {(() => {
+                              const r = retry()
+                              if (!r) return ""
+                              return r.message.length > 60 ? r.message.slice(0, 60) + "..." : r.message
+                            })()}
+                          </span>
+                          <span data-slot="session-turn-retry-seconds">
+                            · retrying {retrySeconds() > 0 ? `in ${retrySeconds()}s ` : ""}
+                          </span>
+                          <span data-slot="session-turn-retry-attempt">(#{retry()?.attempt})</span>
+                        </Match>
                         <Match when={working()}>{store.status ?? "Considering next steps"}</Match>
                         <Match when={working()}>{store.status ?? "Considering next steps"}</Match>
                         <Match when={store.stepsExpanded}>Hide steps</Match>
                         <Match when={store.stepsExpanded}>Hide steps</Match>
                         <Match when={!store.stepsExpanded}>Show steps</Match>
                         <Match when={!store.stepsExpanded}>Show steps</Match>
                       </Switch>
                       </Switch>
                       <span>·</span>
                       <span>·</span>
                       <span>{store.duration}</span>
                       <span>{store.duration}</span>
-                      <Icon name="chevron-grabber-vertical" size="small" />
+                      <Show when={assistantMessages().length > 0}>
+                        <Icon name="chevron-grabber-vertical" size="small" />
+                      </Show>
                     </Button>
                     </Button>
                   </div>
                   </div>
                   {/* Response */}
                   {/* Response */}
-                  <Show when={store.stepsExpanded}>
+                  <Show when={store.stepsExpanded && assistantMessages().length > 0}>
                     <div data-slot="session-turn-collapsible-content-inner">
                     <div data-slot="session-turn-collapsible-content-inner">
                       <For each={assistantMessages()}>
                       <For each={assistantMessages()}>
                         {(assistantMessage) => {
                         {(assistantMessage) => {