Переглянути джерело

feat(tui): 每秒刷新 Research67 步骤耗时

后端事件间隔较长时,Research67 进度会长时间静止,用户无法判断当前步骤是否仍在持续执行。本提交使用桥接层提供的 activeSince 在 TUI 本地刷新经过时间。

- 解析并校验 Research67 progress 的 activeSince 时间戳
- 仅在任务运行且存在起始时间时启动一秒定时器,并在状态变化时清理
- 将实时耗时与 Guard、失败等语义 footer 组合显示
- 覆盖秒级、分钟级格式化和异常进度数据过滤
- 更新紧凑进度树快照以展示本地计时文案
- 验证:TUI 测试 19 passed、9 snapshots;packages/tui typecheck 通过
Jinchen Zhang 1 тиждень тому
батько
коміт
b017b6ea7d

+ 31 - 3
packages/tui/src/routes/session/index.tsx

@@ -2260,6 +2260,22 @@ function Task(props: ToolProps) {
       (props.metadata.background === true && value !== undefined && value.type !== "idle")
     )
   })
+  const [progressNow, setProgressNow] = createSignal(Date.now())
+  const progressStartedAt = createMemo(() => (isRunning() ? progress()?.activeSince : undefined))
+  createEffect(() => {
+    if (!progressStartedAt()) return
+    setProgressNow(Date.now())
+    const timer = setInterval(() => setProgressNow(Date.now()), 1000)
+    onCleanup(() => clearInterval(timer))
+  })
+  const progressFooter = createMemo(() => {
+    const value = progress()
+    if (!value) return undefined
+    const elapsed = value.activeSince
+      ? `当前步骤已运行 ${formatResearch67Elapsed(value.activeSince, progressNow())}`
+      : undefined
+    return [value.footer, elapsed].filter(Boolean).join(" · ") || undefined
+  })
   const retry = createMemo(() => {
     const value = status()
     if (value?.type !== "retry") return
@@ -2352,8 +2368,8 @@ function Task(props: ToolProps) {
               </text>
             )}
           </For>
-          <Show when={progress()!.footer}>
-            <text fg={theme.textMuted}> {progress()!.footer}</text>
+          <Show when={progressFooter()}>
+            <text fg={theme.textMuted}> {progressFooter()}</text>
           </Show>
         </box>
       </InlineTool>
@@ -2364,6 +2380,7 @@ function Task(props: ToolProps) {
 type Research67ProgressStatus = "completed" | "running" | "pending" | "failed"
 type Research67Progress = {
   rows: Array<{ id: string; label: string; status: Research67ProgressStatus; detail?: string; depth: 0 | 1 }>
+  activeSince?: number
   footer?: string
 }
 
@@ -2387,7 +2404,18 @@ export function parseResearch67Progress(value: unknown): Research67Progress | un
     return [result]
   })
   if (rows.length === 0) return undefined
-  return { rows, footer: stringValue(progress.footer) }
+  const activeSince = numberValue(progress.activeSince)
+  return {
+    rows,
+    activeSince: activeSince && activeSince > 0 ? activeSince : undefined,
+    footer: stringValue(progress.footer),
+  }
+}
+
+export function formatResearch67Elapsed(activeSince: number, now: number) {
+  const seconds = Math.max(0, Math.floor((now - activeSince) / 1000))
+  if (seconds < 60) return `${seconds} 秒`
+  return `${Math.floor(seconds / 60)} 分 ${seconds % 60} 秒`
 }
 
 function isResearch67ProgressStatus(value: string | undefined): value is Research67ProgressStatus {

+ 1 - 1
packages/tui/test/cli/tui/__snapshots__/inline-tool-wrap-snapshot.test.tsx.snap

@@ -100,5 +100,5 @@ exports[`TUI inline tool wrapping renders a compact Research67 semantic progress
       ● 筛选入选论文
       ○ 形成文献综述
      ↳ ○ paper-writer · 论文撰写
-      连接正常"
+      当前步骤已运行 12 秒"
 `;

+ 8 - 3
packages/tui/test/cli/tui/inline-tool-wrap-snapshot.test.tsx

@@ -4,6 +4,7 @@ import type { BoxRenderable, ScrollBoxRenderable } from "@opentui/core"
 import { testRender, type JSX } from "@opentui/solid"
 import {
   formatCompletedSubagentDetail,
+  formatResearch67Elapsed,
   formatSubagentRetry,
   formatSubagentTitle,
   formatSubagentToolcalls,
@@ -139,7 +140,7 @@ function Research67ProgressFixture() {
           <text> ● 筛选入选论文</text>
           <text> ○ 形成文献综述</text>
           <text>↳ ○ paper-writer · 论文撰写</text>
-          <text> 连接正常</text>
+          <text> 当前步骤已运行 12 秒</text>
         </box>
       </InlineToolRow>
     </box>
@@ -289,12 +290,16 @@ describe("TUI inline tool wrapping", () => {
           { id: "bad", label: "Bad", status: "unknown", depth: 0 },
           { id: "deep", label: "Deep", status: "pending", depth: 2 },
         ],
-        footer: "连接正常",
+        activeSince: 1_000,
       }),
     ).toEqual({
       rows: [{ id: "done", label: "Done", status: "completed", detail: undefined, depth: 0 }],
-      footer: "连接正常",
+      activeSince: 1_000,
+      footer: undefined,
     })
+    expect(formatResearch67Elapsed(1_000, 1_000)).toBe("0 秒")
+    expect(formatResearch67Elapsed(1_000, 2_000)).toBe("1 秒")
+    expect(formatResearch67Elapsed(1_000, 62_000)).toBe("1 分 1 秒")
     expect(parseResearch67Progress({ kind: "other", rows: [] })).toBeUndefined()
     const statuses = ["completed", "running", "pending", "failed"] as const
     expect(statuses.map(research67ProgressIcon)).toEqual(["✓", "●", "○", "✗"])