diff --git a/RELEASE_NOTES.md b/RELEASE_NOTES.md index 1906e4c28..4cfd3dfcf 100644 --- a/RELEASE_NOTES.md +++ b/RELEASE_NOTES.md @@ -2,32 +2,34 @@ ### ✨ 主要更新 -- `aster-rust` 依赖已从 `v0.27.1` 升级到 `v0.27.2` -- `src-tauri/Cargo.toml` 的远程 git tag 引用已同步到 `v0.27.2` -- 应用与 CLI 发布版本同步提升到 `1.5.1`,用于标记这次依赖补丁发布 - -### ⚠️ 发布与兼容性说明 - -- 本次发布 tag 为 `v1.5.1`,应用内版本号保持为 `1.5.1` -- `@limecloud/lime-cli@1.5.1` 要求 `Node >= 18`,支持 `darwin / linux / win32` 与 `x64 / arm64` -- 本次为补丁发布,主要目的是对齐 `aster-rust v0.27.2`,不额外引入新的 Lime 功能面 -- 本地 `.cargo/config.toml` patch override 仍仅用于开发联调,不属于发布事实源 +- 本次 `v1.5.1` 已收口当前工作区全部改动,核心集中在 Agent 聊天工作台、General Workbench、Service Skill、Team Workspace、Artifact / Timeline 展示与输入发送主链 +- `src/components/agent/**`、`src/components/workspace/**`、`src/lib/api/**`、`src/features/browser-runtime/**`、`src/components/settings-v2/**` 一批界面、运行时与回归测试已一并进入本次发布 +- 浏览器运行时、现有会话桥接、工具展示、团队协作、项目选择、技能目录、工作台工具命令与内容同步相关边界已同步更新 +- 工程文档 `docs/aiprompts/commands.md`、`playwright-e2e.md`、`quality-workflow.md` 已随当前实现一起更新 ### 🔗 依赖与版本同步 -- 应用版本已同步提升到 `1.5.1`,覆盖 `package.json`、`src-tauri/Cargo.toml`、`src-tauri/tauri.conf.json`、`src-tauri/tauri.conf.headless.json` -- `packages/lime-cli-npm/package.json` 与 README 发布示例已同步更新到 `1.5.1` -- 本地 `package-lock.json` 也已同步到 `1.5.1`,用于版本一致性校验 -- `src-tauri/Cargo.lock` 已刷新,`aster-core` / `aster-models` 已对齐到 `0.27.2` +- `aster-rust` 依赖已从 `v0.27.1` 升级到 `v0.27.2` +- `src-tauri/Cargo.toml` 的远程 git tag 引用已同步到 `v0.27.2` +- 应用与 CLI 发布版本保持为 `1.5.1` +- 应用版本入口已对齐到 `1.5.1`,覆盖 `package.json`、`src-tauri/Cargo.toml`、`src-tauri/tauri.conf.json`、`src-tauri/tauri.conf.headless.json` +- `packages/lime-cli-npm/package.json`、README 发布示例与本地 `package-lock.json` 已同步到 `1.5.1` +- `src-tauri/Cargo.lock` 中 `aster-core` / `aster-models` 已对齐到 `0.27.2` -### 🧪 发布前校验 +### ⚠️ 发布说明 + +- 本次发布 tag 仍为 `v1.5.1` +- 因为要把当前工作区全部改动统一纳入同一补丁版本,本次会更新已经存在的 `v1.5.1` Git tag 与 GitHub Release 指向 +- 当前公开 release note 已按这次完整发布内容刷新 + +### 🧪 当前校验 -- `cargo update --manifest-path src-tauri/Cargo.toml` - `npm run verify:app-version` +- Git 提交前的仓库 AI 验证会随提交钩子执行 ### 📝 文档同步 -- 发布说明已切换到当前这次 `v1.5.1` 补丁版内容,可直接作为 GitHub Release note 使用 +- 发布说明已更新为当前这次完整的 `v1.5.1` 内容,可直接作为 GitHub Release note 使用 --- diff --git a/docs/aiprompts/commands.md b/docs/aiprompts/commands.md index 7de3daa32..c4556cd82 100644 --- a/docs/aiprompts/commands.md +++ b/docs/aiprompts/commands.md @@ -88,7 +88,7 @@ - `CharacterMention`、`builtinCommands`、场景 slash 补全不得再各自维护一套业务命令静态常量 - 服务端尚未返回 `entries` 时,允许网关层从 legacy `items` 兼容投影出 `entries` -- 客户端必须保留 seeded fallback,不能因为服务端暂时不可用就让 `@配图`、`@PPT`、`@表单`、`@网页`、`@代码`、`@发布`、`@搜索`、`@深搜`、`@研报`、`@站点搜索`、`@读PDF`、`@总结`、`@翻译`、`@分析`、`@转写` 这类主链入口失能 +- 客户端必须保留 seeded fallback,不能因为服务端暂时不可用就让 `@配图`、`@配音`、`@PPT`、`@表单`、`@网页`、`@代码`、`@发布`、`@搜索`、`@深搜`、`@研报`、`@站点搜索`、`@读PDF`、`@总结`、`@翻译`、`@分析`、`@转写` 这类主链入口失能 - `src/components/agent/chat/commands/catalog.ts` 只继续承接 Lime 本地 / Codex 原生命令;产品型 `/` 场景不应再长期硬编码在这里 - 若服务端下发的 `renderContract` 超出 Lime 当前支持范围,优先由服务端回退到已支持类型,客户端也必须退化到通用 timeline / artifact 展示 @@ -225,6 +225,10 @@ Skill 执行链路同样遵循单一命令边界。当前前端入口为 `src/li - 工作流入口型命令:`@发布` / `@publish` / `@发文` / `@投稿` 在 `src/components/agent/chat/workspace/useWorkspaceSendActions.ts` 中保留原始用户文本展示,但把实际 dispatch 改写到现有 `/content_post_with_cover ...` 主链,并把结构化 `publish_command` 写入 `request_metadata.harness.publish_command`。当前实现优先复用已有 `content_post_with_cover` 发布工作流、`content-posts/*.md` / `*.publish-pack.json` 产物链,以及 `detectBrowserTaskRequirement(...)` 推导出的浏览器门禁,而不是再发明新的 `publish_task` 协议。若输入里已明确平台后台,如微信公众号后台,必须继续写入 `browser_requirement=required_with_user_step` 与平台 launch URL;若只是整理发布稿而未指定平台,则允许先在同一工作流里生成发布稿与发布前检查,不强行要求浏览器。后续若统一 agent/workflow runtime 成熟,可以把 `@发布` 从当前 slash workflow 迁走,但在那之前不得同时维护第二套发布入口真相。 +`Claw` 的纯文本配音命令也应沿同一条服务型技能主链收敛: + +- Agent 驱动的配音命令:`@配音` / `@voice` / `@dubbing` / `@dub` 在 `src/components/agent/chat/workspace/useWorkspaceSendActions.ts` 中保留原始用户文本发送。聊天发送边界会优先从当前 `serviceSkills` / seeded fallback 中解析配音能力(当前兜底为 `cloud-video-dubbing`),并把结构化 `service_scene_launch` 写入 `request_metadata.harness.service_scene_launch`,其中固定 `scene_key=voice_runtime`、`entry_source=at_voice_command`,同时注入 OEM `scene_base_url / tenant_id / session_token` 运行时上下文。Rust 侧 `runtime_turn.rs`、`prompt_context.rs` 与 `tool_runtime/service_skill_tools.rs` 会把当前 turn 切到 `workbench`,并强约束首刀优先调用 `lime_run_service_skill`,由 OEM scene runtime 负责 run / poll。当前上下文缺少明确配音要求时,允许 Agent 最多追问 1 个关键问题;但不能退回普通聊天解释、不能伪造“配音已完成”,也不能重新回流到旧的本地 TTS 测试命令。 + 这些命令除了 Tauri `generate_handler!` 之外,也必须继续保持 DevBridge dispatcher 已桥接,避免浏览器模式、headless smoke 或 Playwright 续测时回退成 unknown command。 自动化设置链路同样遵循这条路径。当前主入口为 `src/lib/api/automation.ts`,统一承接: diff --git a/docs/aiprompts/playwright-e2e.md b/docs/aiprompts/playwright-e2e.md index 369d3132c..96a92e6fb 100644 --- a/docs/aiprompts/playwright-e2e.md +++ b/docs/aiprompts/playwright-e2e.md @@ -259,6 +259,15 @@ npm run test:contracts 5. 等待工作流继续推进后,确认产物仍落在现有 `content-posts/*.md` / `*.publish-pack.json` 主链,而不是另一套发布任务协议 6. 刷新页面或切换会话后再返回原话题,确认发布稿与发布包仍可恢复 +### Claw `@配音` 云端技能主链验证 + +1. 在 `Claw` 对话框输入 `@配音 目标语言: 英文 风格: 科技感 给这个新品视频做一版发布配音稿` +2. 确认聊天区保留原始 `@配音 ...` 文本,而不是被改写成 slash scene 或其它内部协议 +3. 如页面可查看发送详情或运行时摘要,确认 `request_metadata.harness.service_scene_launch.service_scene_run.scene_key=voice_runtime`,并且 `skill_id` 已绑定到当前可用的配音 service skill +4. 确认首刀进入 `lime_run_service_skill` 主链,而不是普通聊天解释、站点型 `lime_site_run`,或旧的本地 TTS 测试命令 +5. 如果当前 OEM 云端会话缺失,确认界面明确提示需要登录 / 注入会话,而不是伪造“配音已完成” +6. 刷新页面或切换会话后再返回原话题,确认该配音任务的时间线与最近使用状态仍可恢复 + ### Claw `@读PDF` Prompt Skill 验证 1. 在 `Claw` 对话框输入 `@读PDF /tmp/agent-report.pdf 提炼三点结论并标注关键证据` diff --git a/docs/aiprompts/quality-workflow.md b/docs/aiprompts/quality-workflow.md index 5a4068a22..6f51fe068 100644 --- a/docs/aiprompts/quality-workflow.md +++ b/docs/aiprompts/quality-workflow.md @@ -204,6 +204,7 @@ npm run bridge:health -- --timeout-ms 120000 - 检查纯文本 `Claw @表单` 是否已经走 `原始用户消息 -> harness.form_skill_launch -> Agent 首刀 Skill(form_generate) -> ```a2ui simple form JSON` 主链,而不是退回普通聊天字段建议、卡在 `ToolSearch / WebSearch / Read / Glob / Grep` 这类通用工具偏航,或回流成单文件 HTML 表单原型;同时确认 render contract 已收敛为 `form + json` - 检查纯文本 `Claw @代码` 是否已经走 `原始用户消息 -> harness.code_command + preferred_team_preset_id -> code_orchestrated -> code_execution / tools / team runtime` 主链,而不是继续停留在普通聊天、没有打开 `task/subagent` 偏好,或把代码任务改写成另一套 prompt / workflow 旁路 - 检查纯文本 `Claw @发布` 是否已经走 `原始用户消息 -> displayContent 保留 -> dispatch /content_post_with_cover -> content_post workflow` 主链,而不是直接把 `@发布` 文本原样当普通聊天发送,或重新造一套 `publish_task` 协议;同时确认平台后台类输入会继续触发 `browser_requirement` +- 检查纯文本 `Claw @配音` 是否已经走 `原始用户消息 -> harness.service_scene_launch(scene_key=voice_runtime) -> Agent 首刀 lime_run_service_skill -> OEM scene run/timeline` 主链,而不是退回普通聊天解释、误走站点型 `service_skill_launch`,或重新回流旧的本地 TTS 测试命令;同时确认 `skill_id`、OEM runtime 上下文与最近使用记录都能写回 - 检查产品型 `/scene-key` 是否已经走 `原始用户消息 -> harness.service_scene_launch -> Agent 首刀 lime_run_service_skill -> OEM run/timeline` 主链,而不是前端直接调用云端 run API - 如果某个 `/scene-key` 绑定的是 `site_adapter` 型技能,还要额外检查 `scene -> linkedSkillId -> 完整 ServiceSkill 目录 -> harness.service_skill_launch` 这条绑定链是否仍然成立,避免首页隐藏 site skill 后 slash scene 变成“目录可见但执行找不到 skill” - 如果某个 `site_adapter` 结果开始返回 `markdown_bundle`,还要确认保存链会把 Markdown、图片和 `meta.json` 一起落到项目导出目录,并把重写后的相对图片路径写回内容 metadata;同时确认聊天轻卡或 tool timeline 能显示项目目录、Markdown 路径和图片数量,不能只把远程图片 URL 或临时 DOM 文本留在聊天结果里 @@ -245,6 +246,7 @@ npm run bridge:health -- --timeout-ms 120000 - 修改 `@PPT` parser、`useWorkspaceSendActions`、`runtime_turn`、`presentation_skill_launch`、`presentation_generate` skill 或演示稿 artifact 预览链路,尤其是调整 `Claw @PPT -> harness.presentation_skill_launch -> Agent 首刀 Skill(presentation_generate) -> write_file Markdown artifact` 主链 - 修改 `@代码` parser、`useWorkspaceSendActions`、mention builtin command 或 `code_orchestrated` 发送边界,尤其是调整 `Claw @代码 -> harness.code_command -> code_orchestrated -> tools / team runtime` 主链 - 修改 `@发布` parser、`useWorkspaceSendActions`、content post workflow 入口或浏览器门禁推导,尤其是调整 `Claw @发布 -> displayContent/raw -> /content_post_with_cover -> publish workflow` 主链 +- 修改 `@配音` parser、`useWorkspaceSendActions`、`service_scene_launch` 组装、OEM runtime 上下文注入或 `lime_run_service_skill` 配音场景接线,尤其是调整 `Claw @配音 -> harness.service_scene_launch(scene_key=voice_runtime) -> lime_run_service_skill -> OEM run/timeline` 主链 - 修改 `/scene-key` 解析、`serviceSkillSceneLaunch`、`useWorkspaceSendActions`、`runtime_turn`、`prompt_context`、`lime_run_service_skill` 或 `client/skills` scene 目录协议,尤其是调整 `Claw /scene-key -> harness.service_scene_launch -> Agent 首刀 lime_run_service_skill -> OEM run/timeline` 主链 - 修改 `src/lib/dev-bridge/` - 修改 `src/lib/tauri-mock/` @@ -403,6 +405,13 @@ npm run bridge:health -- --timeout-ms 120000 - 若浏览器门禁或 harness 协议继续扩散,再补 `npm run test:contracts` - 若 GUI 主路径受影响,再补 `npm run verify:gui-smoke` +如果本轮修改了 `Claw @配音` 或配音服务型技能接线,最低校验至少包含: + +- `npx vitest run "src/components/agent/chat/utils/voiceWorkbenchCommand.test.ts" "src/components/agent/chat/workspace/useWorkspaceSendActions.test.tsx" "src/components/agent/chat/skill-selection/CharacterMention.test.tsx" "src/lib/api/skillCatalog.test.ts" "src/lib/api/serviceSkills.test.ts"` +- 如有改动扩散到 `service_scene_launch` runtime、`lime_run_service_skill`、OEM scene run/poll 或云端结果回流,再补对应 `runtime_turn` / `prompt_context` / `tool_runtime/service_skill_tools` 定向测试 +- 若 `service_scene_launch` / harness 协议继续扩散,再补 `npm run test:contracts` +- 若 GUI 主路径受影响,再补 `npm run verify:gui-smoke` + 如果本轮修改了 Provider 模型真相源或设置页中的“支持的模型”展示逻辑,还应额外确认: - 资源索引损坏时,GUI 会明确提示“模型真相源异常” diff --git a/src/App.tsx b/src/App.tsx index 66cdb081b..cb843c2d7 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -8,12 +8,7 @@ * _需求: 2.2, 3.2, 5.2_ */ -import React, { - Suspense, - lazy, - useState, - useCallback, -} from "react"; +import React, { Suspense, lazy, useState, useCallback } from "react"; import styled from "styled-components"; import { withI18nPatch } from "./i18n/withI18nPatch"; import { AppPageContent } from "./components/AppPageContent"; @@ -211,10 +206,9 @@ function AppContent() { onOpenBrowserConnectorSettings: handleOpenBrowserConnectorSettings, }); - const { error: registryError, refresh: _refreshRegistry } = - useRelayRegistry({ - autoLoad: hasTauriDesktopRuntime, - }); + const { error: registryError, refresh: _refreshRegistry } = useRelayRegistry({ + autoLoad: hasTauriDesktopRuntime, + }); useAppStartupEffects({ currentPage, registryError, diff --git a/src/components/AppPageContent.test.tsx b/src/components/AppPageContent.test.tsx index 85f44b5e3..7c009c487 100644 --- a/src/components/AppPageContent.test.tsx +++ b/src/components/AppPageContent.test.tsx @@ -140,20 +140,26 @@ describe("AppPageContent", () => { const container = renderContent("channels"); await flushEffects(); - expect(container.querySelector('[data-testid="im-config-page"]')).not.toBeNull(); + expect( + container.querySelector('[data-testid="im-config-page"]'), + ).not.toBeNull(); }); it("video 页面应渲染现役视频工作台入口", async () => { const container = renderContent("video"); await flushEffects(); - expect(container.querySelector('[data-testid="video-page"]')).not.toBeNull(); + expect( + container.querySelector('[data-testid="video-page"]'), + ).not.toBeNull(); }); it("settings 页面应渲染设置页入口", async () => { const container = renderContent("settings"); await flushEffects(); - expect(container.querySelector('[data-testid="settings-page"]')).not.toBeNull(); + expect( + container.querySelector('[data-testid="settings-page"]'), + ).not.toBeNull(); }); }); diff --git a/src/components/AppSidebar.test.tsx b/src/components/AppSidebar.test.tsx index fc5fb9c20..bd6311974 100644 --- a/src/components/AppSidebar.test.tsx +++ b/src/components/AppSidebar.test.tsx @@ -25,12 +25,10 @@ interface MountedSidebar { const mountedSidebars: MountedSidebar[] = []; const APP_SIDEBAR_COLLAPSED_STORAGE_KEY = "lime.app-sidebar.collapsed"; -function mountSidebar( - options?: { - currentPage?: Page; - currentPageParams?: PageParams; - }, -): MountedSidebar["container"] { +function mountSidebar(options?: { + currentPage?: Page; + currentPageParams?: PageParams; +}): MountedSidebar["container"] { const container = document.createElement("div"); document.body.appendChild(container); const root = createRoot(container); @@ -91,7 +89,9 @@ describe("AppSidebar", () => { expect( container.querySelector('button[aria-label="展开导航栏"]'), ).not.toBeNull(); - expect(localStorage.getItem(APP_SIDEBAR_COLLAPSED_STORAGE_KEY)).toBe("true"); + expect(localStorage.getItem(APP_SIDEBAR_COLLAPSED_STORAGE_KEY)).toBe( + "true", + ); }); it("新建任务页应自动展开导航栏,不沿用上一个页面的折叠状态", async () => { @@ -107,7 +107,9 @@ describe("AppSidebar", () => { expect( container.querySelector('button[aria-label="折叠导航栏"]'), ).not.toBeNull(); - expect(localStorage.getItem(APP_SIDEBAR_COLLAPSED_STORAGE_KEY)).toBe("false"); + expect(localStorage.getItem(APP_SIDEBAR_COLLAPSED_STORAGE_KEY)).toBe( + "false", + ); }); it("旧导航配置未包含能力入口时也应显示固定能力分组和核心能力项", async () => { @@ -142,7 +144,9 @@ describe("AppSidebar", () => { expect(container.textContent).toContain("能力"); expect( - container.querySelector('button[aria-label="消息渠道"][aria-current="page"]'), + container.querySelector( + 'button[aria-label="消息渠道"][aria-current="page"]', + ), ).not.toBeNull(); }); diff --git a/src/components/AppSidebar.tsx b/src/components/AppSidebar.tsx index 8aab93916..d760d8ee4 100644 --- a/src/components/AppSidebar.tsx +++ b/src/components/AppSidebar.tsx @@ -19,11 +19,7 @@ import { } from "lucide-react"; import * as LucideIcons from "lucide-react"; import { getPluginsForSurface, PluginUIInfo } from "@/lib/api/pluginUI"; -import { - AgentPageParams, - Page, - PageParams, -} from "@/types/page"; +import { AgentPageParams, Page, PageParams } from "@/types/page"; import { getConfig } from "@/lib/api/appConfig"; import { buildClawAgentParams, @@ -88,7 +84,9 @@ const Container = styled.aside<{ --sidebar-border: ${({ $themeMode }) => $themeMode === "dark" ? "#2d3a46" : "#e2f0e2"}; --sidebar-divider: ${({ $themeMode }) => - $themeMode === "dark" ? "rgba(148, 163, 184, 0.14)" : "rgba(132, 204, 22, 0.15)"}; + $themeMode === "dark" + ? "rgba(148, 163, 184, 0.14)" + : "rgba(132, 204, 22, 0.15)"}; --sidebar-hover: ${({ $themeMode }) => $themeMode === "dark" ? "#22303c" : "#eef7ee"}; --sidebar-active: ${({ $themeMode }) => @@ -109,13 +107,12 @@ const Container = styled.aside<{ padding: ${({ $collapsed }) => ($collapsed ? "12px 6px" : "12px 10px")}; position: relative; isolation: isolate; - background: - linear-gradient( - 180deg, - var(--sidebar-surface-top) 0%, - var(--sidebar-surface-middle) 34%, - var(--sidebar-surface-bottom) 100% - ); + background: linear-gradient( + 180deg, + var(--sidebar-surface-top) 0%, + var(--sidebar-surface-middle) 34%, + var(--sidebar-surface-bottom) 100% + ); border-right: 1px solid var(--sidebar-border); box-shadow: 16px 0 36px -34px rgba(15, 23, 42, 0.42); transition: @@ -178,7 +175,8 @@ const UserButton = styled.button<{ $collapsed?: boolean }>` padding: ${({ $collapsed }) => ($collapsed ? "8px" : "8px 10px")}; cursor: pointer; color: var(--sidebar-foreground); - justify-content: ${({ $collapsed }) => ($collapsed ? "center" : "flex-start")}; + justify-content: ${({ $collapsed }) => + $collapsed ? "center" : "flex-start"}; transition: background-color 0.18s ease, color 0.18s ease; @@ -228,7 +226,8 @@ const SearchButton = styled.button<{ $collapsed?: boolean }>` color: var(--sidebar-muted); padding: ${({ $collapsed }) => ($collapsed ? "0" : "0 10px")}; cursor: pointer; - justify-content: ${({ $collapsed }) => ($collapsed ? "center" : "flex-start")}; + justify-content: ${({ $collapsed }) => + $collapsed ? "center" : "flex-start"}; transition: border-color 0.18s ease, background-color 0.18s ease, @@ -304,13 +303,16 @@ const NavButton = styled.button<{ $active?: boolean; $collapsed?: boolean }>` background-color 0.18s ease, color 0.18s ease, transform 0.18s ease; - justify-content: ${({ $collapsed }) => ($collapsed ? "center" : "flex-start")}; + justify-content: ${({ $collapsed }) => + $collapsed ? "center" : "flex-start"}; &:hover { background: ${({ $active }) => $active ? "var(--sidebar-active)" : "var(--sidebar-hover)"}; color: ${({ $active }) => - $active ? "var(--sidebar-active-foreground)" : "var(--sidebar-foreground)"}; + $active + ? "var(--sidebar-active-foreground)" + : "var(--sidebar-foreground)"}; } svg { @@ -332,7 +334,10 @@ const NavLabel = styled.span<{ $collapsed?: boolean }>` display: ${({ $collapsed }) => ($collapsed ? "none" : "inline")}; `; -const NavTrailingIcon = styled.span<{ $collapsed?: boolean; $active?: boolean }>` +const NavTrailingIcon = styled.span<{ + $collapsed?: boolean; + $active?: boolean; +}>` display: ${({ $collapsed }) => ($collapsed ? "none" : "inline-flex")}; align-items: center; justify-content: center; @@ -387,7 +392,8 @@ const FooterArea = styled.div<{ $collapsed?: boolean }>` const ActionRow = styled.div<{ $collapsed?: boolean }>` display: flex; align-items: center; - justify-content: ${({ $collapsed }) => ($collapsed ? "center" : "space-between")}; + justify-content: ${({ $collapsed }) => + $collapsed ? "center" : "space-between"}; padding: 0 2px; `; @@ -412,7 +418,9 @@ const IconActionButton = styled.button<{ $active?: boolean }>` background: ${({ $active }) => $active ? "var(--sidebar-active)" : "var(--sidebar-hover)"}; color: ${({ $active }) => - $active ? "var(--sidebar-active-foreground)" : "var(--sidebar-foreground)"}; + $active + ? "var(--sidebar-active-foreground)" + : "var(--sidebar-foreground)"}; } svg { @@ -433,7 +441,8 @@ export function AppSidebar({ currentPageParams, onNavigate, }: AppSidebarProps) { - const agentEntry = (currentPageParams as AgentPageParams | undefined)?.agentEntry; + const agentEntry = (currentPageParams as AgentPageParams | undefined) + ?.agentEntry; const isClawTaskCenter = currentPage === "agent" && agentEntry === "claw"; const isNewTaskHome = currentPage === "agent" && agentEntry === "new-task"; const [collapsed, setCollapsed] = useState(() => { @@ -461,7 +470,8 @@ export function AppSidebar({ const [refreshTrigger, setRefreshTrigger] = useState(0); const [expandedGroupIds, setExpandedGroupIds] = useState(() => MAIN_SIDEBAR_NAV_ITEMS.filter( - (item) => item.defaultExpanded && item.children && item.children.length > 0, + (item) => + item.defaultExpanded && item.children && item.children.length > 0, ).map((item) => item.id), ); @@ -641,7 +651,8 @@ export function AppSidebar({ }; const activeGroupIds = MAIN_SIDEBAR_NAV_ITEMS.filter( - (item) => item.children && item.children.length > 0 && isGroupActive(item), + (item) => + item.children && item.children.length > 0 && isGroupActive(item), ).map((item) => item.id); if (activeGroupIds.length === 0) { @@ -807,7 +818,9 @@ export function AppSidebar({ {filteredMainSections.map((section) => (
- {section.title} + + {section.title} + {section.items.map((item) => renderNavItem(item))}
))} @@ -823,7 +836,9 @@ export function AppSidebar({ {filteredFooterSections.map((section) => (
- {section.title} + + {section.title} + {section.items.map((item) => renderNavItem(item))}
))} diff --git a/src/components/Modal.tsx b/src/components/Modal.tsx index 02ce9424a..fd76179f6 100644 --- a/src/components/Modal.tsx +++ b/src/components/Modal.tsx @@ -223,7 +223,9 @@ export function ModalFooter({ className?: string; }) { return ( -
+
{children}
); diff --git a/src/components/agent/chat/AgentChatWorkspace.tsx b/src/components/agent/chat/AgentChatWorkspace.tsx index b09a33d60..c0b9d416a 100644 --- a/src/components/agent/chat/AgentChatWorkspace.tsx +++ b/src/components/agent/chat/AgentChatWorkspace.tsx @@ -271,7 +271,9 @@ function resolveTaskPreviewArtifact( if (normalizedArtifactPath) { const matchedArtifact = messageArtifacts.find( (artifact) => - !isHiddenInternalArtifactPath(resolveArtifactProtocolFilePath(artifact)) && + !isHiddenInternalArtifactPath( + resolveArtifactProtocolFilePath(artifact), + ) && (areArtifactProtocolPathsEquivalent( resolveArtifactProtocolFilePath(artifact), normalizedArtifactPath, @@ -310,9 +312,7 @@ function normalizeVideoAspectRatio( } } -function normalizeVideoResolution( - value?: string, -): "480p" | "720p" | "1080p" { +function normalizeVideoResolution(value?: string): "480p" | "720p" | "1080p" { switch (value) { case "480p": case "1080p": @@ -527,7 +527,9 @@ export function AgentChatWorkspace({ let resolvedRootPath = defaultProject.rootPath; try { - const ensuredWorkspace = await ensureWorkspaceReady(defaultProject.id); + const ensuredWorkspace = await ensureWorkspaceReady( + defaultProject.id, + ); resolvedRootPath = ensuredWorkspace.rootPath || resolvedRootPath; } catch (error) { logAgentDebug( @@ -1024,7 +1026,9 @@ export function AgentChatWorkspace({ : theme ) as ThemeType; const rawBody = content.body || ""; - const sanitizedBody = isCorruptedGeneralWorkbenchDocumentContent(rawBody) + const sanitizedBody = isCorruptedGeneralWorkbenchDocumentContent( + rawBody, + ) ? "" : rawBody; @@ -2173,8 +2177,8 @@ export function AgentChatWorkspace({ pendingActionRequest, }); - const generalWorkbenchSidebarRuntime = useWorkspaceGeneralWorkbenchSidebarRuntime( - { + const generalWorkbenchSidebarRuntime = + useWorkspaceGeneralWorkbenchSidebarRuntime({ isThemeWorkbench, sessionId, messages, @@ -2182,8 +2186,7 @@ export function AgentChatWorkspace({ themeWorkbenchBackendRunState, contextActivityLogs: contextWorkspace.activityLogs, historyPageSize: GENERAL_WORKBENCH_HISTORY_PAGE_SIZE, - }, - ); + }); const { handleViewContextDetail } = useWorkspaceContextDetailActions({ contextWorkspace, @@ -2460,12 +2463,10 @@ export function AgentChatWorkspace({ setCanvasState, }); - const submitImageWorkbenchAgentCommandRef = - useRef< - (( - params: SubmitImageWorkbenchAgentCommandParams, - ) => Promise) | null - >(null); + const submitImageWorkbenchAgentCommandRef = useRef< + | ((params: SubmitImageWorkbenchAgentCommandParams) => Promise) + | null + >(null); const imageWorkbenchActionRuntime = useWorkspaceImageWorkbenchActionRuntime({ cancelImageTask: cancelMediaTaskArtifact, contentId, @@ -2572,7 +2573,11 @@ export function AgentChatWorkspace({ ), }, ), - [effectiveChatToolPreferences.thinking, handleSendRef, webSearchPreferenceRef], + [ + effectiveChatToolPreferences.thinking, + handleSendRef, + webSearchPreferenceRef, + ], ); submitImageWorkbenchAgentCommandRef.current = submitImageWorkbenchAgentCommand; @@ -2961,11 +2966,17 @@ export function AgentChatWorkspace({ if (normalizedArtifactPath) { const matchedTaskFile = taskFiles.find( (file) => - areArtifactProtocolPathsEquivalent(file.name, normalizedArtifactPath) || + areArtifactProtocolPathsEquivalent( + file.name, + normalizedArtifactPath, + ) || normalizeArtifactProtocolPath(file.name) === normalizedArtifactPath, ); if (matchedTaskFile?.content?.trim()) { - handleWorkspaceFileClick(matchedTaskFile.name, matchedTaskFile.content); + handleWorkspaceFileClick( + matchedTaskFile.name, + matchedTaskFile.content, + ); return; } } @@ -3093,32 +3104,34 @@ export function AgentChatWorkspace({ hasRealTeamGraph: teamSessionRuntime.hasRealTeamGraph, teamDispatchPreviewState, }); - const generalWorkbenchShellRuntime = useWorkspaceGeneralWorkbenchShellRuntime({ - showChatPanel: effectiveShowChatPanel, - showSidebar, - hasPendingA2UIForm, - contextHarnessRuntime, - generalWorkbenchScaffoldRuntime, - generalWorkbenchSidebarRuntime, - harnessInventoryRuntime, - handleCreateVersionSnapshot, - handleSwitchBranchVersion, - handleSetBranchStatus, - handleAddImage, - handleImportDocument, - handleViewContextDetail, - messages, - harnessState, - compatSubagentRuntime, - childSubagentSessions, - selectedTeamLabel, - selectedTeamSummary, - selectedTeamRoles: selectedTeam?.roles, - teamMemorySnapshot: resolvedTeamMemoryShadowSnapshot, - handleOpenSubagentSession, - handleHarnessLoadFilePreview, - handleFileClick: handleWorkspaceFileClick, - }); + const generalWorkbenchShellRuntime = useWorkspaceGeneralWorkbenchShellRuntime( + { + showChatPanel: effectiveShowChatPanel, + showSidebar, + hasPendingA2UIForm, + contextHarnessRuntime, + generalWorkbenchScaffoldRuntime, + generalWorkbenchSidebarRuntime, + harnessInventoryRuntime, + handleCreateVersionSnapshot, + handleSwitchBranchVersion, + handleSetBranchStatus, + handleAddImage, + handleImportDocument, + handleViewContextDetail, + messages, + harnessState, + compatSubagentRuntime, + childSubagentSessions, + selectedTeamLabel, + selectedTeamSummary, + selectedTeamRoles: selectedTeam?.roles, + teamMemorySnapshot: resolvedTeamMemoryShadowSnapshot, + handleOpenSubagentSession, + handleHarnessLoadFilePreview, + handleFileClick: handleWorkspaceFileClick, + }, + ); useWorkspaceWorkflowProgressSync({ enabled: isSpecializedThemeMode && hasMessages && steps.length > 0, @@ -3283,10 +3296,8 @@ export function AgentChatWorkspace({ const workspaceShellSceneRuntime = useWorkspaceConversationShellSceneRuntime({ messageListEmptyStateVariant: agentEntry === "claw" ? "task-center" : "default", - navbarContextVariant: - agentEntry === "claw" ? "task-center" : "default", - sidebarContextVariant: - agentEntry === "claw" ? "task-center" : "default", + navbarContextVariant: agentEntry === "claw" ? "task-center" : "default", + sidebarContextVariant: agentEntry === "claw" ? "task-center" : "default", navigationActions, inputbarScene, canvasScene, diff --git a/src/components/agent/chat/commands/executor.ts b/src/components/agent/chat/commands/executor.ts index c53552fd9..9f1fa8460 100644 --- a/src/components/agent/chat/commands/executor.ts +++ b/src/components/agent/chat/commands/executor.ts @@ -16,7 +16,9 @@ function ensureSessionMutationAllowed( return true; } - notifyInfo(`当前仍有任务执行中,请先等待结束或停止生成后再执行 ${commandPrefix}`); + notifyInfo( + `当前仍有任务执行中,请先等待结束或停止生成后再执行 ${commandPrefix}`, + ); return false; } @@ -78,7 +80,9 @@ export async function executeCodexSlashCommand( return true; case "model": if (command.userInput.trim()) { - notifyInfo("当前暂不支持通过 /model 切换模型,请使用输入框右侧的模型选择器"); + notifyInfo( + "当前暂不支持通过 /model 切换模型,请使用输入框右侧的模型选择器", + ); return true; } appendAssistantMessage(buildCodexSlashModelMessage(statusSnapshot)); diff --git a/src/components/agent/chat/commands/formatter.ts b/src/components/agent/chat/commands/formatter.ts index 560a5d8db..a95f035ee 100644 --- a/src/components/agent/chat/commands/formatter.ts +++ b/src/components/agent/chat/commands/formatter.ts @@ -2,7 +2,10 @@ import { getSupportedCodexSlashCommands, getUnsupportedCodexSlashCommands, } from "./parser"; -import type { CodexSlashStatusSnapshot, ParsedCodexSlashCommand } from "./types"; +import type { + CodexSlashStatusSnapshot, + ParsedCodexSlashCommand, +} from "./types"; function formatExecutionStrategyLabel( strategy: CodexSlashStatusSnapshot["executionStrategy"], diff --git a/src/components/agent/chat/components/A2UITaskCard.tsx b/src/components/agent/chat/components/A2UITaskCard.tsx index f30523af6..eebed4fcd 100644 --- a/src/components/agent/chat/components/A2UITaskCard.tsx +++ b/src/components/agent/chat/components/A2UITaskCard.tsx @@ -1,9 +1,6 @@ import { A2UIRenderer } from "@/lib/workspace/a2ui"; import { cn } from "@/lib/utils"; -import type { - A2UIFormData, - A2UIResponse, -} from "@/lib/workspace/a2ui"; +import type { A2UIFormData, A2UIResponse } from "@/lib/workspace/a2ui"; import { DEFAULT_A2UI_TASK_CARD_PRESET, type A2UITaskCardPreset, diff --git a/src/components/agent/chat/components/AgentPlanBlock.tsx b/src/components/agent/chat/components/AgentPlanBlock.tsx index d50f78bd6..6f1fbef93 100644 --- a/src/components/agent/chat/components/AgentPlanBlock.tsx +++ b/src/components/agent/chat/components/AgentPlanBlock.tsx @@ -24,7 +24,10 @@ export const AgentPlanBlock: React.FC = ({
执行计划
- + {isComplete ? ( "已生成" ) : ( diff --git a/src/components/agent/chat/components/AgentRuntimeStrip.tsx b/src/components/agent/chat/components/AgentRuntimeStrip.tsx index fc30b80e7..5ef45fc4e 100644 --- a/src/components/agent/chat/components/AgentRuntimeStrip.tsx +++ b/src/components/agent/chat/components/AgentRuntimeStrip.tsx @@ -9,9 +9,7 @@ import type { import type { ChatToolPreferences } from "../utils/chatToolPreferences"; import type { CompatSubagentRuntimeStatus } from "../utils/compatSubagentRuntime"; import type { HarnessSessionState } from "../utils/harnessState"; -import { - getOutputSchemaRuntimeLabel, -} from "../utils/sessionExecutionRuntime"; +import { getOutputSchemaRuntimeLabel } from "../utils/sessionExecutionRuntime"; interface AgentRuntimeStripProps { activeTheme?: string; diff --git a/src/components/agent/chat/components/AgentThreadOutcomeSummary.tsx b/src/components/agent/chat/components/AgentThreadOutcomeSummary.tsx index da5d73e78..b1033b445 100644 --- a/src/components/agent/chat/components/AgentThreadOutcomeSummary.tsx +++ b/src/components/agent/chat/components/AgentThreadOutcomeSummary.tsx @@ -84,7 +84,9 @@ export const AgentThreadOutcomeSummary: React.FC<
- {outcome.retryable ? "建议可重试或恢复" : "当前无需人工恢复"} + + {outcome.retryable ? "建议可重试或恢复" : "当前无需人工恢复"} +
); diff --git a/src/components/agent/chat/components/AgentThreadReliabilityPanel.test.tsx b/src/components/agent/chat/components/AgentThreadReliabilityPanel.test.tsx index 8600ac84c..8d65b92f4 100644 --- a/src/components/agent/chat/components/AgentThreadReliabilityPanel.test.tsx +++ b/src/components/agent/chat/components/AgentThreadReliabilityPanel.test.tsx @@ -180,7 +180,9 @@ describe("AgentThreadReliabilityPanel", () => { expect(container.textContent).toContain("待处理请求"); expect(container.textContent).toContain("Provider 失败"); expect(container.textContent).toContain("审批等待超过阈值"); - expect(container.textContent).toContain("审批等待过久,建议尽快处理或停止当前执行"); + expect(container.textContent).toContain( + "审批等待过久,建议尽快处理或停止当前执行", + ); }); it("缺少 thread_read 时,应从当前 turn 与 pendingActions 推导并支持中断", async () => { @@ -226,8 +228,8 @@ describe("AgentThreadReliabilityPanel", () => { expect(container.textContent).toContain("等待人工处理"); expect(container.textContent).toContain("请确认是否继续发布"); - const button = Array.from(container.querySelectorAll("button")).find((node) => - node.textContent?.includes("停止当前执行"), + const button = Array.from(container.querySelectorAll("button")).find( + (node) => node.textContent?.includes("停止当前执行"), ); expect(button).toBeDefined(); @@ -271,8 +273,8 @@ describe("AgentThreadReliabilityPanel", () => { onInterruptCurrentTurn, }); - const button = Array.from(container.querySelectorAll("button")).find((node) => - node.textContent?.includes("停止当前执行"), + const button = Array.from(container.querySelectorAll("button")).find( + (node) => node.textContent?.includes("停止当前执行"), ); expect(button).toBeDefined(); @@ -460,7 +462,9 @@ describe("AgentThreadReliabilityPanel", () => { }); expect(container.textContent).toContain("当前回合长时间无进展"); - expect(container.textContent).toContain("当前回合长时间无进展,建议停止后恢复执行"); + expect(container.textContent).toContain( + "当前回合长时间无进展,建议停止后恢复执行", + ); }); it("存在待处理请求时应支持重新拉起请求", async () => { @@ -712,7 +716,9 @@ describe("AgentThreadReliabilityPanel", () => { expect(container.textContent).toContain("快速复制给 AI"); expect(container.textContent).toContain("复制原始 JSON(debug)"); expect(container.textContent).toContain("外部分析交接"); - expect(container.textContent).toContain("analysis-brief.md / analysis-context.json"); + expect(container.textContent).toContain( + "analysis-brief.md / analysis-context.json", + ); }); it("应支持复制原始 JSON 诊断数据", async () => { diff --git a/src/components/agent/chat/components/AgentThreadReliabilityPanel.tsx b/src/components/agent/chat/components/AgentThreadReliabilityPanel.tsx index 5b4a32e54..cc6085c81 100644 --- a/src/components/agent/chat/components/AgentThreadReliabilityPanel.tsx +++ b/src/components/agent/chat/components/AgentThreadReliabilityPanel.tsx @@ -55,9 +55,7 @@ interface AgentThreadReliabilityPanelProps { onResumeThread?: () => boolean | Promise; onReplayPendingRequest?: (requestId: string) => boolean | Promise; onLocatePendingRequest?: (requestId: string) => void; - onPromoteQueuedTurn?: ( - queuedTurnId: string, - ) => boolean | Promise; + onPromoteQueuedTurn?: (queuedTurnId: string) => boolean | Promise; className?: string; harnessState?: HarnessSessionState | null; messages?: Message[]; @@ -76,7 +74,10 @@ function normalizeDiagnosticText(value?: string | null): string { return typeof value === "string" ? value.trim().replace(/\s+/g, " ") : ""; } -function truncateDiagnosticText(value?: string | null, maxLength = 240): string { +function truncateDiagnosticText( + value?: string | null, + maxLength = 240, +): string { const normalized = normalizeDiagnosticText(value); if (!normalized) { return ""; @@ -145,9 +146,14 @@ function summarizeRecentMessages(messages: Message[]) { : null, action_request_count: message.actionRequests?.length || 0, action_request_titles: - message.actionRequests?.slice(0, 3).map((request) => - truncateDiagnosticText(request.prompt || request.toolName || request.requestId, 120), - ) || [], + message.actionRequests + ?.slice(0, 3) + .map((request) => + truncateDiagnosticText( + request.prompt || request.toolName || request.requestId, + 120, + ), + ) || [], tool_calls: message.toolCalls?.slice(0, 4).map((toolCall) => ({ id: toolCall.id, @@ -175,8 +181,12 @@ function summarizeHarnessState(harnessState?: HarnessSessionState | null) { ? { phase: harnessState.runtimeStatus.phase, title: harnessState.runtimeStatus.title, - detail: truncateDiagnosticText(harnessState.runtimeStatus.detail, 220), - checkpoints: harnessState.runtimeStatus.checkpoints?.slice(0, 6) || [], + detail: truncateDiagnosticText( + harnessState.runtimeStatus.detail, + 220, + ), + checkpoints: + harnessState.runtimeStatus.checkpoints?.slice(0, 6) || [], metadata: harnessState.runtimeStatus.metadata || null, } : null, @@ -214,23 +224,30 @@ function summarizeHarnessState(harnessState?: HarnessSessionState | null) { truncated: signal.truncated || false, offloaded: signal.offloaded || false, })), - active_file_writes: harnessState.activeFileWrites.slice(0, 6).map((write) => ({ - id: write.id, - path: write.path, - display_name: write.displayName, - phase: write.phase, - status: write.status, - preview: truncateDiagnosticText(write.preview || write.latestChunk, 160), - })), - recent_file_events: harnessState.recentFileEvents.slice(0, 8).map((event) => ({ - id: event.id, - path: event.path, - display_name: event.displayName, - kind: event.kind, - action: event.action, - source_tool_name: event.sourceToolName, - preview: truncateDiagnosticText(event.preview, 140), - })), + active_file_writes: harnessState.activeFileWrites + .slice(0, 6) + .map((write) => ({ + id: write.id, + path: write.path, + display_name: write.displayName, + phase: write.phase, + status: write.status, + preview: truncateDiagnosticText( + write.preview || write.latestChunk, + 160, + ), + })), + recent_file_events: harnessState.recentFileEvents + .slice(0, 8) + .map((event) => ({ + id: event.id, + path: event.path, + display_name: event.displayName, + kind: event.kind, + action: event.action, + source_tool_name: event.sourceToolName, + preview: truncateDiagnosticText(event.preview, 140), + })), }; } @@ -441,7 +458,9 @@ function buildReliabilityDiagnosticText(params: { sections.push("", "### 后端诊断聚合"); if (threadRead?.diagnostics) { const diagnostics = threadRead.diagnostics; - sections.push(`- 最新回合状态:${diagnostics.latest_turn_status || "未知"}`); + sections.push( + `- 最新回合状态:${diagnostics.latest_turn_status || "未知"}`, + ); sections.push( `- 最新回合开始时间:${diagnostics.latest_turn_started_at || "未知"}`, ); @@ -461,7 +480,9 @@ function buildReliabilityDiagnosticText(params: { `- 最新回合错误:${diagnostics.latest_turn_error_message || "无"}`, ); sections.push(`- 中断原因:${diagnostics.interrupt_reason || "未知"}`); - sections.push(`- 中断来源:${diagnostics.runtime_interrupt_source || "未知"}`); + sections.push( + `- 中断来源:${diagnostics.runtime_interrupt_source || "未知"}`, + ); sections.push( `- 中断请求时间:${diagnostics.runtime_interrupt_requested_at || "未知"}`, ); @@ -480,8 +501,12 @@ function buildReliabilityDiagnosticText(params: { sections.push( `- 最老待处理请求等待时长(秒):${diagnostics.oldest_pending_request_wait_seconds ?? "无"}`, ); - sections.push(`- 主阻塞类型:${diagnostics.primary_blocking_kind || "未知"}`); - sections.push(`- 主阻塞摘要:${diagnostics.primary_blocking_summary || "未知"}`); + sections.push( + `- 主阻塞类型:${diagnostics.primary_blocking_kind || "未知"}`, + ); + sections.push( + `- 主阻塞摘要:${diagnostics.primary_blocking_summary || "未知"}`, + ); sections.push( `- 最近 warning:${ diagnostics.latest_warning @@ -737,7 +762,9 @@ export const AgentThreadReliabilityPanel: React.FC< ); toast.success("原始 JSON 已复制"); } catch (error) { - toast.error(error instanceof Error ? error.message : "复制原始 JSON 失败"); + toast.error( + error instanceof Error ? error.message : "复制原始 JSON 失败", + ); } }; @@ -773,11 +800,15 @@ export const AgentThreadReliabilityPanel: React.FC< ) : null} -
{summary}
+
+ {summary} +
- {view.updatedAtLabel ? 最近刷新 {view.updatedAtLabel} : null} + {view.updatedAtLabel ? ( + 最近刷新 {view.updatedAtLabel} + ) : null} {view.interruptStateLabel ? (
当前入口属于 `compat` 线程级快速诊断,只覆盖当前 thread 的运行信号。 - 正式交给外部 Claude / Codex 分析时,请优先使用工作台“交接制品 - → 外部分析交接”的 `analysis-brief.md / analysis-context.json` - 主链;这里的“快速复制给 AI”只适合临时排障,“复制原始 JSON(debug)”适合程序化分析、存档或二次处理。 + 正式交给外部 Claude / Codex 分析时,请优先使用工作台“交接制品 → + 外部分析交接”的 `analysis-brief.md / analysis-context.json` + 主链;这里的“快速复制给 AI”只适合临时排障,“复制原始 + JSON(debug)”适合程序化分析、存档或二次处理。
diff --git a/src/components/agent/chat/components/AgentThreadTimeline.test.tsx b/src/components/agent/chat/components/AgentThreadTimeline.test.tsx index 1e8f3d10f..49fd006e9 100644 --- a/src/components/agent/chat/components/AgentThreadTimeline.test.tsx +++ b/src/components/agent/chat/components/AgentThreadTimeline.test.tsx @@ -22,7 +22,10 @@ function resolveMockToolText(toolCall: { let parsedArguments: Record | null = null; if (toolCall.arguments) { try { - parsedArguments = JSON.parse(toolCall.arguments) as Record; + parsedArguments = JSON.parse(toolCall.arguments) as Record< + string, + unknown + >; } catch { parsedArguments = null; } @@ -167,9 +170,7 @@ function at(second: number): string { return `2026-03-15T09:10:${String(second).padStart(2, "0")}Z`; } -function createTurn( - overrides?: Partial, -): AgentThreadTurn { +function createTurn(overrides?: Partial): AgentThreadTurn { return { id: "turn-1", thread_id: "thread-1", @@ -384,7 +385,7 @@ describe("AgentThreadTimeline", () => { createFileArtifactItem({ id: "artifact-hidden-task-json", path: ".lime/tasks/image_generate/task-image-1.json", - content: "{\"status\":\"running\"}", + content: '{"status":"running"}', metadata: {}, }), ]); @@ -515,9 +516,7 @@ describe("AgentThreadTimeline", () => { .map((node) => node.dataset.testid) .filter((value): value is string => Boolean(value)) .filter( - (value) => - !value.endsWith(":shell") && - !value.endsWith(":details"), + (value) => !value.endsWith(":shell") && !value.endsWith(":details"), ); expect(blockIds).toEqual(["agent-thread-block:1:process"]); @@ -552,7 +551,9 @@ describe("AgentThreadTimeline", () => { expect(summary?.textContent).toContain("2 步"); expect(summary?.textContent).toContain("2 个工具步骤"); - expect(container.querySelectorAll('[data-testid="tool-call-item"]')).toHaveLength(0); + expect( + container.querySelectorAll('[data-testid="tool-call-item"]'), + ).toHaveLength(0); act(() => { summary?.dispatchEvent(new MouseEvent("click", { bubbles: true })); @@ -791,14 +792,19 @@ describe("AgentThreadTimeline", () => { }); expect( - container.querySelector('[data-testid="agent-thread-block:1:process:details"]'), + container.querySelector( + '[data-testid="agent-thread-block:1:process:details"]', + ), ).toBeNull(); expect(container.textContent).not.toContain("思考摘要"); - expect((container.textContent?.split(reasoningText).length ?? 1) - 1).toBe(1); + expect((container.textContent?.split(reasoningText).length ?? 1) - 1).toBe( + 1, + ); }); it("已完成的思考应默认折叠,只保留摘要行", () => { - const reasoningText = "先核对执行链路,再立即恢复当前运行。\n随后补齐自动续提。"; + const reasoningText = + "先核对执行链路,再立即恢复当前运行。\n随后补齐自动续提。"; const items: AgentThreadItem[] = [ { ...createBaseItem("reasoning-1", 1), @@ -820,7 +826,9 @@ describe("AgentThreadTimeline", () => { expect(block?.open).toBe(false); expect(summary?.textContent).toContain("已完成思考"); - expect(summary?.textContent).toContain("先核对执行链路,再立即恢复当前运行。"); + expect(summary?.textContent).toContain( + "先核对执行链路,再立即恢复当前运行。", + ); expect(container.textContent).not.toContain("随后补齐自动续提。"); act(() => { @@ -914,8 +922,12 @@ describe("AgentThreadTimeline", () => { summary?.dispatchEvent(new MouseEvent("click", { bubbles: true })); }); - expect((container.textContent?.split("先判断任务类型").length ?? 1) - 1).toBe(1); - expect((container.textContent?.split("再决定是否联网").length ?? 1) - 1).toBe(1); + expect( + (container.textContent?.split("先判断任务类型").length ?? 1) - 1, + ).toBe(1); + expect( + (container.textContent?.split("再决定是否联网").length ?? 1) - 1, + ).toBe(1); }); it("已完成的 request_user_input 应以只读 A2UI 卡片回显", () => { @@ -945,7 +957,9 @@ describe("AgentThreadTimeline", () => { expect( container.querySelector('[data-testid="timeline-a2ui-card"]'), ).not.toBeNull(); - expect(container.querySelector('[data-testid="decision-panel"]')).toBeNull(); + expect( + container.querySelector('[data-testid="decision-panel"]'), + ).toBeNull(); }); it("真实协作成员 item 应支持查看协作详情", () => { diff --git a/src/components/agent/chat/components/AgentThreadTimeline.tsx b/src/components/agent/chat/components/AgentThreadTimeline.tsx index 1bd38f285..4a27f963e 100644 --- a/src/components/agent/chat/components/AgentThreadTimeline.tsx +++ b/src/components/agent/chat/components/AgentThreadTimeline.tsx @@ -273,7 +273,8 @@ function toToolCallState(item: AgentThreadItem): ToolCallState | null { item.error !== undefined || item.exit_code !== undefined ? { - success: item.status === "completed" && item.error === undefined, + success: + item.status === "completed" && item.error === undefined, output: item.aggregated_output || "", error: item.error, metadata: @@ -405,61 +406,61 @@ function ThinkingItemCard({ () => parseAIResponse(displayText, false), [displayText], ); - const hasStructuredPreview = parsedContent.hasA2UI || parsedContent.hasPending; + const hasStructuredPreview = + parsedContent.hasA2UI || parsedContent.hasPending; const shouldHideTurnSummaryContent = item.type === "turn_summary" && !hasStructuredPreview && isInternalRoutingTurnSummaryText(displayText); - const content = - hasStructuredPreview ? ( -
- {parsedContent.parts.map((part, index) => { - if (part.type === "a2ui" && typeof part.content !== "string") { - const readonlyResponse: A2UIResponse = { - ...part.content, - submitAction: undefined, - }; - - return ( - - ); - } - - if (part.type === "pending_a2ui") { - return ( - - ); - } - - const textContent = - typeof part.content === "string" ? part.content.trim() : ""; - if (!textContent) { - return null; - } + const content = hasStructuredPreview ? ( +
+ {parsedContent.parts.map((part, index) => { + if (part.type === "a2ui" && typeof part.content !== "string") { + const readonlyResponse: A2UIResponse = { + ...part.content, + submitAction: undefined, + }; return ( - ); - })} -
- ) : shouldHideTurnSummaryContent ? null : ( - - ); + } + + if (part.type === "pending_a2ui") { + return ( + + ); + } + + const textContent = + typeof part.content === "string" ? part.content.trim() : ""; + if (!textContent) { + return null; + } + + return ( + + ); + })} +
+ ) : shouldHideTurnSummaryContent ? null : ( + + ); const statusLabel = item.type === "turn_summary" ? item.status === "in_progress" @@ -577,7 +578,12 @@ function InlinePlanBlock({ function renderThinkingItemDetails(item: AgentThreadItem) { if (item.type === "plan") { - return ; + return ( + + ); } if (item.type === "reasoning" || item.type === "turn_summary") { @@ -613,10 +619,12 @@ function isToolExecutionTimelineItem(item: AgentThreadItem): boolean { ); } -function extractCompactThinkingParts(item: Extract< - AgentThreadItem, - { type: "plan" | "reasoning" | "turn_summary" | "context_compaction" } ->) { +function extractCompactThinkingParts( + item: Extract< + AgentThreadItem, + { type: "plan" | "reasoning" | "turn_summary" | "context_compaction" } + >, +) { if (item.type === "context_compaction") { const title = item.stage === "completed" || item.status === "completed" @@ -638,15 +646,20 @@ function extractCompactThinkingParts(item: Extract< } if (item.type === "reasoning") { - const { summaryText, bodyText, combinedText } = resolveReasoningDisplayText(item); + const { summaryText, bodyText, combinedText } = + resolveReasoningDisplayText(item); const previewSource = summaryText || combinedText; const lines = previewSource .split(/\r?\n/) .map((line) => line.trim()) .filter(Boolean); - const [title = item.status === "in_progress" ? "思考中" : "已完成思考", ...rest] = - lines; - const detail = [rest.join("\n").trim(), bodyText].filter(Boolean).join("\n\n"); + const [ + title = item.status === "in_progress" ? "思考中" : "已完成思考", + ...rest + ] = lines; + const detail = [rest.join("\n").trim(), bodyText] + .filter(Boolean) + .join("\n\n"); const parsed = parseAIResponse(combinedText, false); if (parsed.hasA2UI || parsed.hasPending) { @@ -886,7 +899,10 @@ function renderGroupItemDetails(
{item.type} - + {resolveItemStatusLabel(item.status)}
@@ -911,7 +927,9 @@ function renderTimelineItemDetails( ) { if (isThinkingTimelineItem(item)) { if (options?.groupedToolCall) { - return ; + return ( + + ); } return renderThinkingItemDetails(item); } @@ -927,7 +945,9 @@ function renderTimelineItemDetails( ); } -function resolveCompactTechnicalSummary(block: AgentThreadOrderedBlock): string { +function resolveCompactTechnicalSummary( + block: AgentThreadOrderedBlock, +): string { return `处理了 ${block.items.length} 个步骤`; } @@ -1060,23 +1080,41 @@ function normalizeBlockPreviewLine( if ( kind === "artifact" && - !hasAnyPrefix(trimmed, ["看了 ", "读了 ", "写了 ", "改了 ", "动了 ", "产出了 "]) + !hasAnyPrefix(trimmed, [ + "看了 ", + "读了 ", + "写了 ", + "改了 ", + "动了 ", + "产出了 ", + ]) ) { return `产出了 ${trimmed}`; } if ( kind === "approval" && - !hasAnyPrefix(trimmed, ["等你补充:", "等你确认:", "等你补充信息", "等你确认这一步"]) + !hasAnyPrefix(trimmed, [ + "等你补充:", + "等你确认:", + "等你补充信息", + "等你确认这一步", + ]) ) { return `等你确认:${trimmed}`; } - if (kind === "alert" && !hasAnyPrefix(trimmed, ["收到提醒:", "碰到错误:"])) { + if ( + kind === "alert" && + !hasAnyPrefix(trimmed, ["收到提醒:", "碰到错误:"]) + ) { return `收到提醒:${trimmed}`; } - if (kind === "subagent" && !hasAnyPrefix(trimmed, ["分给协作成员", "协作成员"])) { + if ( + kind === "subagent" && + !hasAnyPrefix(trimmed, ["分给协作成员", "协作成员"]) + ) { return `分给协作成员处理 ${trimmed}`; } @@ -1136,7 +1174,9 @@ function resolveBlockSummaryLines(block: AgentThreadOrderedBlock): string[] { } if (block.kind === "subagent") { - return [block.status === "completed" ? "协作成员处理完了" : "协作成员在处理"]; + return [ + block.status === "completed" ? "协作成员处理完了" : "协作成员在处理", + ]; } if (block.kind === "other") { @@ -1385,8 +1425,7 @@ function TimelineBlockCard({ const visibleHeadline = headline; const visibleSupportingLines = isThinkingOnlyBlock && open ? [] : supportingLines; - const summaryCountLabel = - block.items.length > 1 ? block.countLabel : null; + const summaryCountLabel = block.items.length > 1 ? block.countLabel : null; const processMixLabel = resolveProcessMixLabel(block); const summaryDetailHint = hasDetailEntries && block.items.length > 1 && !open @@ -1395,13 +1434,23 @@ function TimelineBlockCard({ const summaryToneClassName = cn( "text-slate-900", block.status === "in_progress" && "text-sky-700", - block.kind === "approval" && block.status !== "completed" && "text-amber-800", + block.kind === "approval" && + block.status !== "completed" && + "text-amber-800", (block.status === "failed" || block.kind === "alert") && "text-rose-700", ); return ( -
-
+
+
= ({ (item) => item.type !== "user_message" && item.type !== "agent_message" && - !(item.type === "file_artifact" && isHiddenInternalArtifactPath(item.path)), + !( + item.type === "file_artifact" && + isHiddenInternalArtifactPath(item.path) + ), ), [items], ); @@ -1553,11 +1605,13 @@ export const AgentThreadTimeline: React.FC = ({ data-testid="agent-thread-flow" data-placement={placement} > - {inlineStatusHint ? : null} + {inlineStatusHint ? ( + + ) : null} {displayModel.orderedBlocks.map((block, index) => { const blockHasFocusedItem = Boolean( focusedItemId && - block.items.some((item) => item.id === focusedItemId), + block.items.some((item) => item.id === focusedItemId), ); return ( diff --git a/src/components/agent/chat/components/AgentThreadTimelineArtifactCard.test.tsx b/src/components/agent/chat/components/AgentThreadTimelineArtifactCard.test.tsx index 5706d10ef..c19a1ee0d 100644 --- a/src/components/agent/chat/components/AgentThreadTimelineArtifactCard.test.tsx +++ b/src/components/agent/chat/components/AgentThreadTimelineArtifactCard.test.tsx @@ -135,7 +135,9 @@ describe("AgentThreadTimelineArtifactCard", () => { expect(container.textContent).toContain("2 个区块"); expect(container.textContent).toContain("1 条来源"); expect(container.textContent).toContain("V2"); - expect(container.textContent).toContain("定位到 本轮重点是补齐来源线索与交付节奏。"); + expect(container.textContent).toContain( + "定位到 本轮重点是补齐来源线索与交付节奏。", + ); expect(container.textContent).not.toContain("artifact_document_service"); expect(container.textContent).not.toContain("schemaVersion"); expect(container.textContent).not.toContain('"artifactId"'); @@ -155,7 +157,9 @@ describe("AgentThreadTimelineArtifactCard", () => { ); expect(container.textContent).toContain("已同步"); - expect(container.textContent).toContain("包含结构化结果,点击在画布中查看完整内容。"); + expect(container.textContent).toContain( + "包含结构化结果,点击在画布中查看完整内容。", + ); expect(container.textContent).not.toContain('"queue"'); expect(container.textContent).not.toContain('"retryable"'); }); diff --git a/src/components/agent/chat/components/AgentThreadTimelineArtifactCard.tsx b/src/components/agent/chat/components/AgentThreadTimelineArtifactCard.tsx index 06b413994..36dca8ae2 100644 --- a/src/components/agent/chat/components/AgentThreadTimelineArtifactCard.tsx +++ b/src/components/agent/chat/components/AgentThreadTimelineArtifactCard.tsx @@ -145,7 +145,9 @@ function resolveBlockLabel( normalizeText(record.text) || normalizeText(record.markdown); - return label ? truncateInlineText(label, 20) : fallbackByType[block.type] || blockId; + return label + ? truncateInlineText(label, 20) + : fallbackByType[block.type] || blockId; } function resolveFallbackPreview(content: string | undefined): string | null { diff --git a/src/components/agent/chat/components/CanvasWorkbenchLayout.test.tsx b/src/components/agent/chat/components/CanvasWorkbenchLayout.test.tsx index c9f171496..f9ac3ae95 100644 --- a/src/components/agent/chat/components/CanvasWorkbenchLayout.test.tsx +++ b/src/components/agent/chat/components/CanvasWorkbenchLayout.test.tsx @@ -24,21 +24,23 @@ type MockResizeObserverCallback = ( observer: unknown, ) => void; -const { mockListDirectory, mockToast, resizeObserverState } = vi.hoisted(() => ({ - mockListDirectory: vi.fn(), - mockToast: { - success: vi.fn(), - error: vi.fn(), - info: vi.fn(), - }, - resizeObserverState: { - width: 1280, - observers: [] as Array<{ - callback: MockResizeObserverCallback; - target: Element | null; - }>, - }, -})); +const { mockListDirectory, mockToast, resizeObserverState } = vi.hoisted( + () => ({ + mockListDirectory: vi.fn(), + mockToast: { + success: vi.fn(), + error: vi.fn(), + info: vi.fn(), + }, + resizeObserverState: { + width: 1280, + observers: [] as Array<{ + callback: MockResizeObserverCallback; + target: Element | null; + }>, + }, + }), +); vi.mock("sonner", () => ({ toast: mockToast, @@ -387,7 +389,8 @@ beforeEach(() => { disconnect = () => { resizeObserverState.observers = resizeObserverState.observers.filter( (observer) => - observer.callback !== this.callback || observer.target !== this.target, + observer.callback !== this.callback || + observer.target !== this.target, ); }; }, @@ -537,9 +540,9 @@ describe("CanvasWorkbenchLayout", () => { await flushEffects(); expect(mockListDirectory).toHaveBeenCalledWith("/workspace"); - expect(container.querySelector('[data-testid="preview-panel"]')?.textContent).toContain( - "default-canvas:draft.md", - ); + expect( + container.querySelector('[data-testid="preview-panel"]')?.textContent, + ).toContain("default-canvas:draft.md"); clickButtonByLabel(container, "切换画布标签-变更"); await flushEffects(); @@ -558,9 +561,9 @@ describe("CanvasWorkbenchLayout", () => { clickButtonByLabel(container, "切换画布标签-产物"); clickButtonByLabel(container, "选择画布产物-draft.md"); await flushEffects(); - expect(container.querySelector('[data-testid="preview-panel"]')?.textContent).toContain( - "artifact:draft.md", - ); + expect( + container.querySelector('[data-testid="preview-panel"]')?.textContent, + ).toContain("artifact:draft.md"); clickButtonByLabel(container, "切换画布标签-全部文件"); await flushEffects(); @@ -639,15 +642,15 @@ describe("CanvasWorkbenchLayout", () => { container.querySelector('button[aria-label="展开当前文稿检查器"]'), ).not.toBeNull(); expect( - container.querySelector('[data-testid="canvas-workbench-document-inspector"]'), + container.querySelector( + '[data-testid="canvas-workbench-document-inspector"]', + ), ).toBeNull(); expect(container.textContent).toContain("当前文稿"); expect(container.textContent).toContain("统一在右侧切换产物与版本"); expect(container.textContent).toContain("董事会季度复盘"); expect(container.textContent).toContain("需要优先补齐来源与版本线索。"); - expect(container.textContent).toContain( - "默认先收起概览、来源、版本与编辑", - ); + expect(container.textContent).toContain("默认先收起概览、来源、版本与编辑"); clickButtonByLabel(container, "展开当前文稿检查器"); await flushEffects(); @@ -656,7 +659,9 @@ describe("CanvasWorkbenchLayout", () => { container.querySelector('button[aria-label="折叠当前文稿检查器"]'), ).not.toBeNull(); expect( - container.querySelector('[data-testid="canvas-workbench-document-inspector"]'), + container.querySelector( + '[data-testid="canvas-workbench-document-inspector"]', + ), ).not.toBeNull(); }); @@ -746,7 +751,9 @@ describe("CanvasWorkbenchLayout", () => { await flushEffects(); - expect(container.querySelector('[data-testid="team-preview"]')).not.toBeNull(); + expect( + container.querySelector('[data-testid="team-preview"]'), + ).not.toBeNull(); expect(container.querySelector('[data-testid="team-panel"]')).toBeNull(); expect( container.querySelector('button[aria-label="展开画布工作台"]'), @@ -759,9 +766,7 @@ describe("CanvasWorkbenchLayout", () => { it("teamView 的 autoFocusToken 变化时应切到 Team Workbench", async () => { const renderPreview = vi.fn((target: CanvasWorkbenchPreviewTarget) => ( -
- fallback:{target.kind} -
+
fallback:{target.kind}
)); const renderTeamPreview = vi.fn( (_options?: { stackedWorkbenchTrigger?: React.ReactNode }) => ( @@ -811,9 +816,15 @@ describe("CanvasWorkbenchLayout", () => { const harness = mountHarness(baseProps); await flushEffects(); - expect(harness.container.querySelector('[data-testid="team-preview"]')).toBeNull(); - expect(harness.container.querySelector('[data-testid="team-panel"]')).toBeNull(); - expect(harness.container.querySelector('[data-testid="fallback-preview"]')).not.toBeNull(); + expect( + harness.container.querySelector('[data-testid="team-preview"]'), + ).toBeNull(); + expect( + harness.container.querySelector('[data-testid="team-panel"]'), + ).toBeNull(); + expect( + harness.container.querySelector('[data-testid="fallback-preview"]'), + ).not.toBeNull(); harness.rerender({ ...baseProps, @@ -824,8 +835,12 @@ describe("CanvasWorkbenchLayout", () => { }); await flushEffects(); - expect(harness.container.querySelector('[data-testid="team-preview"]')).not.toBeNull(); - expect(harness.container.querySelector('[data-testid="team-panel"]')).not.toBeNull(); + expect( + harness.container.querySelector('[data-testid="team-preview"]'), + ).not.toBeNull(); + expect( + harness.container.querySelector('[data-testid="team-panel"]'), + ).not.toBeNull(); expect(harness.container.textContent).toContain("多成员实时协作"); }); @@ -1006,7 +1021,9 @@ describe("CanvasWorkbenchLayout", () => { await flushEffects(); - expect(container.querySelector('[data-testid="team-preview"]')).not.toBeNull(); + expect( + container.querySelector('[data-testid="team-preview"]'), + ).not.toBeNull(); expect( container.querySelector('[data-testid="canvas-workbench-layout"]'), ).toBeNull(); @@ -1055,7 +1072,9 @@ describe("CanvasWorkbenchLayout", () => { .querySelector('[data-testid="canvas-workbench-layout"]') ?.getAttribute("data-panel-placement"), ).toBe("side"); - expect(container.querySelector('[data-testid="canvas-workbench-trigger"]')).toBeNull(); + expect( + container.querySelector('[data-testid="canvas-workbench-trigger"]'), + ).toBeNull(); expect(container.querySelector('[data-testid="team-panel"]')).toBeNull(); expect( container.querySelector('button[aria-label="展开画布工作台"]'), diff --git a/src/components/agent/chat/components/CanvasWorkbenchLayout.tsx b/src/components/agent/chat/components/CanvasWorkbenchLayout.tsx index 4c1afb468..1713d0a5b 100644 --- a/src/components/agent/chat/components/CanvasWorkbenchLayout.tsx +++ b/src/components/agent/chat/components/CanvasWorkbenchLayout.tsx @@ -167,7 +167,9 @@ export interface CanvasWorkbenchTeamView { tone: "idle" | "active" | "error"; label?: string | null; } | null; - renderPreview: (options?: { stackedWorkbenchTrigger?: ReactNode }) => ReactNode; + renderPreview: (options?: { + stackedWorkbenchTrigger?: ReactNode; + }) => ReactNode; renderPanel?: () => ReactNode; renderFooter?: () => ReactNode; } @@ -231,12 +233,13 @@ function resolveStackedWorkbenchMetrics(shellWidth: number): { maxWidth: number; defaultWidth: number; } { - const safeShellWidth = shellWidth > 0 ? shellWidth : STACKED_LAYOUT_BREAKPOINT; - const minWidth = Math.max(280, Math.min(340, Math.round(safeShellWidth * 0.36))); - const maxWidth = Math.max( - minWidth + 40, - Math.min(480, safeShellWidth - 28), + const safeShellWidth = + shellWidth > 0 ? shellWidth : STACKED_LAYOUT_BREAKPOINT; + const minWidth = Math.max( + 280, + Math.min(340, Math.round(safeShellWidth * 0.36)), ); + const maxWidth = Math.max(minWidth + 40, Math.min(480, safeShellWidth - 28)); const defaultWidth = clamp( Math.round(safeShellWidth * 0.42), minWidth, @@ -314,7 +317,9 @@ function resolvePreviousArtifactContent( if (candidate.id === artifact.id) { continue; } - const candidatePath = normalizePath(resolveArtifactProtocolFilePath(candidate)); + const candidatePath = normalizePath( + resolveArtifactProtocolFilePath(candidate), + ); if (candidatePath === currentPath && candidate.content.trim()) { return candidate.content; } @@ -342,10 +347,15 @@ function resolveMappedPreviousContentForPath( workspaceRoot, version.metadata?.sourceFileName, ); - return versionPath ? normalizePath(versionPath) === normalizedTarget : false; + return versionPath + ? normalizePath(versionPath) === normalizedTarget + : false; }); if (matchedVersion) { - return resolvePreviousVersionContent(matchedVersion, canvasState.versions); + return resolvePreviousVersionContent( + matchedVersion, + canvasState.versions, + ); } } @@ -354,7 +364,9 @@ function resolveMappedPreviousContentForPath( workspaceRoot, resolveArtifactProtocolFilePath(artifact), ); - return artifactPath ? normalizePath(artifactPath) === normalizedTarget : false; + return artifactPath + ? normalizePath(artifactPath) === normalizedTarget + : false; }); return matchedArtifact @@ -384,7 +396,9 @@ function buildEntries( absolutePath: resolveAbsoluteWorkspacePath(workspaceRoot, filePath), previewText: resolveArtifactPreviewText(artifact), createdAt: artifact.updatedAt || artifact.createdAt, - badgeLabel: writePhase ? formatArtifactWritePhaseLabel(writePhase) : undefined, + badgeLabel: writePhase + ? formatArtifactWritePhaseLabel(writePhase) + : undefined, kindLabel: "产物", }; }); @@ -410,7 +424,8 @@ function buildEntries( previewText: version.content.trim().slice(0, 180), createdAt: version.createdAt, isCurrent: version.id === canvasState.currentVersionId, - badgeLabel: version.id === canvasState.currentVersionId ? "当前" : undefined, + badgeLabel: + version.id === canvasState.currentVersionId ? "当前" : undefined, kindLabel: "版本", })), ); @@ -427,7 +442,10 @@ function buildEntries( title: extractFileNameFromPath(taskFile.name), subtitle: taskFile.name, filePath: taskFile.name, - absolutePath: resolveAbsoluteWorkspacePath(workspaceRoot, taskFile.name), + absolutePath: resolveAbsoluteWorkspacePath( + workspaceRoot, + taskFile.name, + ), previewText: taskFile.content?.trim().slice(0, 180), createdAt: taskFile.updatedAt, badgeLabel: taskFile.type === "document" ? "文档" : undefined, @@ -445,9 +463,7 @@ function buildEntries( }); } -function renderDiffState( - diffLines: CanvasWorkbenchDiffLine[], -): ReactNode { +function renderDiffState(diffLines: CanvasWorkbenchDiffLine[]): ReactNode { return (
@@ -464,7 +480,9 @@ function renderDiffState( {line.type === "add" ? "+" : line.type === "remove" ? "-" : " "} - {line.value || " "} + + {line.value || " "} +
))}
@@ -487,36 +505,42 @@ export const CanvasWorkbenchLayout = memo(function CanvasWorkbenchLayout({ onLayoutModeChange, teamView = null, }: CanvasWorkbenchLayoutProps) { - const shouldPreferTeamTabByDefault = teamView?.enabled === true && !defaultPreview; + const shouldPreferTeamTabByDefault = + teamView?.enabled === true && !defaultPreview; const shellRef = useRef(null); const stackedResizeCleanupRef = useRef<(() => void) | null>(null); const [activeTab, setActiveTab] = useState(() => shouldPreferTeamTabByDefault ? "team" : "artifacts", ); - const [collapsed, setCollapsed] = useState(() => shouldPreferTeamTabByDefault); + const [collapsed, setCollapsed] = useState( + () => shouldPreferTeamTabByDefault, + ); const [isStackedLayout, setIsStackedLayout] = useState(false); const [stackedWorkbenchOpen, setStackedWorkbenchOpen] = useState(false); const [shellWidth, setShellWidth] = useState(STACKED_LAYOUT_BREAKPOINT); - const [stackedWorkbenchWidth, setStackedWorkbenchWidth] = useState( - null, - ); - const [documentInspectorCollapsed, setDocumentInspectorCollapsed] = useState(true); + const [stackedWorkbenchWidth, setStackedWorkbenchWidth] = useState< + number | null + >(null); + const [documentInspectorCollapsed, setDocumentInspectorCollapsed] = + useState(true); const [selectedKey, setSelectedKey] = useState(null); const [artifactDocumentController, setArtifactDocumentController] = useState(null); - const [directoryCache, setDirectoryCache] = useState>( - {}, - ); - const [loadingDirectories, setLoadingDirectories] = useState>( - {}, - ); - const [expandedDirectories, setExpandedDirectories] = useState>( - {}, - ); + const [directoryCache, setDirectoryCache] = useState< + Record + >({}); + const [loadingDirectories, setLoadingDirectories] = useState< + Record + >({}); + const [expandedDirectories, setExpandedDirectories] = useState< + Record + >({}); const [workspaceFileSelections, setWorkspaceFileSelections] = useState< Record >({}); - const availableTabs = useMemo>( + const availableTabs = useMemo< + Array<{ key: CanvasWorkbenchTab; label: string }> + >( () => [ ...(teamView?.enabled ? [{ key: "team" as const, label: "团队" }] : []), { key: "artifacts" as const, label: "产物" }, @@ -559,7 +583,10 @@ export const CanvasWorkbenchLayout = memo(function CanvasWorkbenchLayout({ }, [defaultPreview, entries, entryMap, selectedFileId]); useEffect(() => { - if (selectedKey && (entryMap.has(selectedKey) || selectedKey.startsWith("workspace-file:"))) { + if ( + selectedKey && + (entryMap.has(selectedKey) || selectedKey.startsWith("workspace-file:")) + ) { return; } setSelectedKey(fallbackEntryKey); @@ -593,12 +620,7 @@ export const CanvasWorkbenchLayout = memo(function CanvasWorkbenchLayout({ return; } void loadDirectory(workspaceRoot); - }, [ - directoryCache, - loadDirectory, - workspaceRoot, - workspaceUnavailable, - ]); + }, [directoryCache, loadDirectory, workspaceRoot, workspaceUnavailable]); useEffect(() => { const node = shellRef.current; @@ -615,7 +637,9 @@ export const CanvasWorkbenchLayout = memo(function CanvasWorkbenchLayout({ }; const fallbackWidth = - node.getBoundingClientRect().width || node.clientWidth || window.innerWidth; + node.getBoundingClientRect().width || + node.clientWidth || + window.innerWidth; updateLayout(fallbackWidth); if (typeof ResizeObserver === "undefined") { @@ -717,11 +741,7 @@ export const CanvasWorkbenchLayout = memo(function CanvasWorkbenchLayout({ setStackedWorkbenchOpen(true); emitCompactRightPanelOpen({ source: "workbench" }); } - }, [ - teamView?.autoFocusToken, - teamView?.enabled, - usesStackedWorkbenchLayout, - ]); + }, [teamView?.autoFocusToken, teamView?.enabled, usesStackedWorkbenchLayout]); const handleToggleDirectory = useCallback( (path: string) => { @@ -797,7 +817,9 @@ export const CanvasWorkbenchLayout = memo(function CanvasWorkbenchLayout({ ); const effectiveKey = selectedKey || fallbackEntryKey; - const selectedEntry = effectiveKey ? entryMap.get(effectiveKey) || null : null; + const selectedEntry = effectiveKey + ? entryMap.get(effectiveKey) || null + : null; const selectedWorkspaceFile = effectiveKey?.startsWith("workspace-file:") ? workspaceFileSelections[effectiveKey] || null : null; @@ -938,7 +960,13 @@ export const CanvasWorkbenchLayout = memo(function CanvasWorkbenchLayout({ kind: "empty", title: "暂无可预览内容", }; - }, [activeTab, defaultPreview, selectedEntry, selectedWorkspaceFile, teamView]); + }, [ + activeTab, + defaultPreview, + selectedEntry, + selectedWorkspaceFile, + teamView, + ]); const currentContent = useMemo(() => { if (currentTarget.kind === "default-canvas") { @@ -979,8 +1007,14 @@ export const CanvasWorkbenchLayout = memo(function CanvasWorkbenchLayout({ return resolvePreviousArtifactContent(selectedEntry.artifact, artifacts); } - if (selectedEntry.source === "document-version" && isDocumentCanvasState(canvasState)) { - return resolvePreviousVersionContent(selectedEntry.version, canvasState.versions); + if ( + selectedEntry.source === "document-version" && + isDocumentCanvasState(canvasState) + ) { + return resolvePreviousVersionContent( + selectedEntry.version, + canvasState.versions, + ); } if (selectedEntry.absolutePath) { @@ -1030,9 +1064,7 @@ export const CanvasWorkbenchLayout = memo(function CanvasWorkbenchLayout({ await navigator.clipboard.writeText(selectionPath); toast.success("已复制路径"); } catch (error) { - toast.error( - error instanceof Error ? error.message : "复制路径失败", - ); + toast.error(error instanceof Error ? error.message : "复制路径失败"); } }, [selectionPath]); @@ -1056,7 +1088,8 @@ export const CanvasWorkbenchLayout = memo(function CanvasWorkbenchLayout({ } const showDocumentInspector = Boolean( - selectedEntry?.source === "artifact" && artifactDocumentController?.document, + selectedEntry?.source === "artifact" && + artifactDocumentController?.document, ); const documentTitle = artifactDocumentController?.document?.title?.trim() || @@ -1142,7 +1175,12 @@ export const CanvasWorkbenchLayout = memo(function CanvasWorkbenchLayout({ {showDocumentInspector && artifactDocumentController ? ( -
+
- {isDirectory && isExpanded ? renderDirectoryNode(entry.path, depth + 1) : null} + {isDirectory && isExpanded + ? renderDirectoryNode(entry.path, depth + 1) + : null}
); }); @@ -1299,7 +1341,9 @@ export const CanvasWorkbenchLayout = memo(function CanvasWorkbenchLayout({
Workspace Root
-
{workspaceRoot}
+
+ {workspaceRoot} +
+
团队工作台已启用。
); }; - const renderTabButtons = (stacked: boolean) => ( + const renderTabButtons = (stacked: boolean) => stacked ? (
{availableTabs.map((tab) => ( @@ -1453,8 +1495,7 @@ export const CanvasWorkbenchLayout = memo(function CanvasWorkbenchLayout({ ))}
- ) - ); + ); const renderWorkbenchHeader = ( stacked: boolean, @@ -1463,7 +1504,9 @@ export const CanvasWorkbenchLayout = memo(function CanvasWorkbenchLayout({ }, ) => { const headerTitle = - activeTab === "team" ? teamView?.title || currentTarget.title : currentTarget.title; + activeTab === "team" + ? teamView?.title || currentTarget.title + : currentTarget.title; if (stacked) { return ( @@ -1558,7 +1601,7 @@ export const CanvasWorkbenchLayout = memo(function CanvasWorkbenchLayout({ ); }; - const renderWorkbenchFooter = (stacked: boolean) => ( + const renderWorkbenchFooter = (stacked: boolean) => activeTab === "team" && teamView?.renderFooter ? (
) : ( -
-
- - - -
- {previousContent !== null ? ( -
- - 已关联到上一版本,可在“变更”中查看差异。 +
+
+ + +
- ) : null} -
- ) - ); + {previousContent !== null ? ( +
+ + 已关联到上一版本,可在“变更”中查看差异。 +
+ ) : null} +
+ ); const renderActiveTab = () => activeTab === "team" ? renderTeamTab() : activeTab === "artifacts" - ? renderEntriesTab() - : activeTab === "files" - ? renderFilesTab() - : activeTab === "changes" - ? renderChangesTab() - : renderPreviewTab(); + ? renderEntriesTab() + : activeTab === "files" + ? renderFilesTab() + : activeTab === "changes" + ? renderChangesTab() + : renderPreviewTab(); const handleStartStackedResize = useCallback( (event: ReactMouseEvent) => { @@ -1723,38 +1765,38 @@ export const CanvasWorkbenchLayout = memo(function CanvasWorkbenchLayout({ !usesStackedWorkbenchLayout; return ( +
-
- {activeTab === "team" && teamView?.enabled - ? teamView.renderPreview( - preferFullscreenTeamPreview - ? undefined - : { - stackedWorkbenchTrigger, - }, - ) - : renderPreview(currentTarget, { - stackedWorkbenchTrigger, - onArtifactDocumentControllerChange: - handleArtifactDocumentControllerChange, - })} + {activeTab === "team" && teamView?.enabled + ? teamView.renderPreview( + preferFullscreenTeamPreview + ? undefined + : { + stackedWorkbenchTrigger, + }, + ) + : renderPreview(currentTarget, { + stackedWorkbenchTrigger, + onArtifactDocumentControllerChange: + handleArtifactDocumentControllerChange, + })}
{preferFullscreenTeamPreview ? null : usesStackedWorkbenchLayout ? ( @@ -1787,11 +1829,11 @@ export const CanvasWorkbenchLayout = memo(function CanvasWorkbenchLayout({
- {renderWorkbenchHeader(true)} -
- {renderActiveTab()} -
- {renderWorkbenchFooter(true)} + {renderWorkbenchHeader(true)} +
+ {renderActiveTab()} +
+ {renderWorkbenchFooter(true)}
@@ -1840,7 +1882,9 @@ export const CanvasWorkbenchLayout = memo(function CanvasWorkbenchLayout({ {renderWorkbenchHeader(false, { showCollapseButton: true, })} -
{renderActiveTab()}
+
+ {renderActiveTab()} +
{renderWorkbenchFooter(false)} )} diff --git a/src/components/agent/chat/components/ChatModelSelector.integration.test.tsx b/src/components/agent/chat/components/ChatModelSelector.integration.test.tsx index 2487aa60c..c9a5aa005 100644 --- a/src/components/agent/chat/components/ChatModelSelector.integration.test.tsx +++ b/src/components/agent/chat/components/ChatModelSelector.integration.test.tsx @@ -56,10 +56,9 @@ vi.mock("@/lib/api/agentRuntime", async () => { }); vi.mock("@/lib/api/agentProtocol", async () => { - const actual = - await vi.importActual( - "@/lib/api/agentProtocol", - ); + const actual = await vi.importActual< + typeof import("@/lib/api/agentProtocol") + >("@/lib/api/agentProtocol"); return { ...actual, parseAgentEvent: mockParseAgentEvent, @@ -67,9 +66,10 @@ vi.mock("@/lib/api/agentProtocol", async () => { }); vi.mock("@/lib/dev-bridge", async () => { - const actual = await vi.importActual( - "@/lib/dev-bridge", - ); + const actual = + await vi.importActual( + "@/lib/dev-bridge", + ); return { ...actual, @@ -121,7 +121,9 @@ const mountedRoots: MountedHarness[] = []; interface MountOptions { onManageProviders?: () => void; - chatModelSelectorProps?: Partial>; + chatModelSelectorProps?: Partial< + React.ComponentProps + >; } function createModel(id: string, providerId: string) { diff --git a/src/components/agent/chat/components/ChatModelSelector.tsx b/src/components/agent/chat/components/ChatModelSelector.tsx index e063e1801..41fed9b88 100644 --- a/src/components/agent/chat/components/ChatModelSelector.tsx +++ b/src/components/agent/chat/components/ChatModelSelector.tsx @@ -1,8 +1,5 @@ import React from "react"; -import { - ModelSelector, - type ModelSelectorProps, -} from "@/components/input-kit"; +import { ModelSelector, type ModelSelectorProps } from "@/components/input-kit"; export type ChatModelSelectorProps = ModelSelectorProps; diff --git a/src/components/agent/chat/components/ChatNavbar.test.tsx b/src/components/agent/chat/components/ChatNavbar.test.tsx index 0b390744b..e64a1be73 100644 --- a/src/components/agent/chat/components/ChatNavbar.test.tsx +++ b/src/components/agent/chat/components/ChatNavbar.test.tsx @@ -137,8 +137,12 @@ describe("ChatNavbar", () => { }); expect(container.querySelector('[aria-label="切换历史"]')).not.toBeNull(); - expect(container.querySelector('[aria-label="展开Harness"]')).not.toBeNull(); - expect(container.querySelector('[data-testid="project-selector"]')).toBeNull(); + expect( + container.querySelector('[aria-label="展开Harness"]'), + ).not.toBeNull(); + expect( + container.querySelector('[data-testid="project-selector"]'), + ).toBeNull(); }); it("任务中心顶栏应展示轻量上下文提示", () => { diff --git a/src/components/agent/chat/components/ChatNavbar.tsx b/src/components/agent/chat/components/ChatNavbar.tsx index c2ba4ee7a..d264aa06f 100644 --- a/src/components/agent/chat/components/ChatNavbar.tsx +++ b/src/components/agent/chat/components/ChatNavbar.tsx @@ -46,8 +46,7 @@ interface ChatNavbarProps { const toolbarGroupClassName = "flex items-center rounded-[20px] border border-slate-200/80 bg-white/90 p-1.5 shadow-sm shadow-slate-950/5 backdrop-blur-sm"; -const toolbarDividerClassName = - "mx-1.5 h-6 w-px shrink-0 bg-slate-200/80"; +const toolbarDividerClassName = "mx-1.5 h-6 w-px shrink-0 bg-slate-200/80"; const toolbarEmbeddedButtonClassName = "h-9 rounded-2xl border border-transparent px-3.5 text-xs shadow-none"; @@ -111,7 +110,8 @@ export const ChatNavbar: React.FC = ({ Boolean(onBackToProjectManagement)); const showWorkspaceTools = showHistoryToggle || showCanvasToggle; const showProjectSelector = !isWorkspaceCompact; - const showCompactSettingsButton = isWorkspaceCompact && Boolean(onToggleSettings); + const showCompactSettingsButton = + isWorkspaceCompact && Boolean(onToggleSettings); const compactProjectSelectorClassName = isWorkspaceCompact ? "min-w-[184px] max-w-[248px]" : "min-w-[196px] max-w-[280px]"; @@ -141,7 +141,10 @@ export const ChatNavbar: React.FC = ({ ) : null} @@ -332,7 +343,6 @@ export const ChatNavbar: React.FC = ({ ) : null}
) : null} -
); diff --git a/src/components/agent/chat/components/ChatSidebar.test.tsx b/src/components/agent/chat/components/ChatSidebar.test.tsx index 87bc29d1e..f0c0cfb35 100644 --- a/src/components/agent/chat/components/ChatSidebar.test.tsx +++ b/src/components/agent/chat/components/ChatSidebar.test.tsx @@ -17,9 +17,15 @@ vi.mock("@/components/ui/badge", () => ({ })); vi.mock("@/components/ui/dropdown-menu", () => ({ - DropdownMenu: ({ children }: { children: React.ReactNode }) =>
{children}
, - DropdownMenuTrigger: ({ children }: { children: React.ReactNode }) => <>{children}, - DropdownMenuContent: ({ children }: { children: React.ReactNode }) =>
{children}
, + DropdownMenu: ({ children }: { children: React.ReactNode }) => ( +
{children}
+ ), + DropdownMenuTrigger: ({ children }: { children: React.ReactNode }) => ( + <>{children} + ), + DropdownMenuContent: ({ children }: { children: React.ReactNode }) => ( +
{children}
+ ), DropdownMenuItem: ({ children, onClick, @@ -238,7 +244,9 @@ describe("ChatSidebar", () => { const taskHeading = container.querySelector( '[data-testid="task-section-heading"]', - ) as (HTMLDivElement & { scrollIntoView?: ReturnType }) | null; + ) as + | (HTMLDivElement & { scrollIntoView?: ReturnType }) + | null; expect(taskHeading).toBeTruthy(); const scrollIntoView = vi.fn(); @@ -268,9 +276,7 @@ describe("ChatSidebar", () => { const container = renderSidebar({ onSwitchTopic }); const taskItem = Array.from( container.querySelectorAll('[role="button"]'), - ).find( - (element) => element.textContent?.includes("任务一"), - ); + ).find((element) => element.textContent?.includes("任务一")); expect(taskItem).toBeTruthy(); if (taskItem) { act(() => { @@ -475,9 +481,7 @@ describe("ChatSidebar", () => { expect(resumeButton).toBeTruthy(); act(() => { - resumeButton?.dispatchEvent( - new MouseEvent("click", { bubbles: true }), - ); + resumeButton?.dispatchEvent(new MouseEvent("click", { bubbles: true })); }); expect(onResumeTask).toHaveBeenCalledWith("topic-1", "user_action"); @@ -596,7 +600,9 @@ describe("ChatSidebar", () => { collapseButton?.dispatchEvent(new MouseEvent("click", { bubbles: true })); }); - expect(container.textContent).toContain("已收起 · 2 个子代理 · 1 个处理中 · 1 个已完成"); + expect(container.textContent).toContain( + "已收起 · 2 个子代理 · 1 个处理中 · 1 个已完成", + ); expect(container.textContent).not.toContain("代码审查代理"); expect(container.textContent).not.toContain("文档校对代理"); @@ -684,9 +690,9 @@ describe("ChatSidebar", () => { expect(container.textContent).not.toContain("回归验证代理"); expect(container.textContent).toContain("展开剩余 1 个子代理"); - const expandMoreButton = Array.from(container.querySelectorAll("button")).find( - (element) => element.textContent?.includes("展开剩余 1 个子代理"), - ); + const expandMoreButton = Array.from( + container.querySelectorAll("button"), + ).find((element) => element.textContent?.includes("展开剩余 1 个子代理")); expect(expandMoreButton).toBeTruthy(); act(() => { @@ -698,9 +704,9 @@ describe("ChatSidebar", () => { expect(container.textContent).toContain("回归验证代理"); expect(container.textContent).toContain("收起子代理列表"); - const collapseListButton = Array.from(container.querySelectorAll("button")).find( - (element) => element.textContent?.includes("收起子代理列表"), - ); + const collapseListButton = Array.from( + container.querySelectorAll("button"), + ).find((element) => element.textContent?.includes("收起子代理列表")); expect(collapseListButton).toBeTruthy(); act(() => { diff --git a/src/components/agent/chat/components/ChatSidebar.tsx b/src/components/agent/chat/components/ChatSidebar.tsx index a758a74db..2017e9e70 100644 --- a/src/components/agent/chat/components/ChatSidebar.tsx +++ b/src/components/agent/chat/components/ChatSidebar.tsx @@ -55,7 +55,7 @@ const STATUS_META: Record< draft: { label: "待补充", badgeClassName: - "border border-slate-200 bg-white text-slate-600 dark:border-white/10 dark:bg-white/5 dark:text-slate-300", + "border border-slate-200/80 bg-white/80 text-slate-600 dark:border-white/10 dark:bg-white/5 dark:text-slate-300", dotClassName: "bg-slate-400", }, running: { @@ -84,11 +84,7 @@ const STATUS_META: Record< }, }; -type TaskSectionKey = - | "running" - | "waiting" - | "recent" - | "older"; +type TaskSectionKey = "running" | "waiting" | "recent" | "older"; type ChatSidebarContextVariant = "default" | "task-center"; @@ -209,7 +205,10 @@ function resolveCurrentStatusPreview( pendingActionCount: number, workspaceError: boolean, ) { - if ((workspaceError || statusReason === "workspace_error") && status === "failed") { + if ( + (workspaceError || statusReason === "workspace_error") && + status === "failed" + ) { return "工作区异常,等待你重新选择本地目录后继续。"; } if (status === "running") { @@ -462,9 +461,8 @@ export const ChatSidebar: React.FC = ({ recent: false, older: false, }); - const [teamSectionCollapsedOverride, setTeamSectionCollapsedOverride] = useState< - boolean | null - >(null); + const [teamSectionCollapsedOverride, setTeamSectionCollapsedOverride] = + useState(null); const [showAllChildSubagents, setShowAllChildSubagents] = useState(false); const [showAllSiblingSubagents, setShowAllSiblingSubagents] = useState(false); const editInputRef = useRef(null); @@ -551,7 +549,10 @@ export const ChatSidebar: React.FC = ({ () => showAllChildSubagents ? sortedChildSubagentSessions - : sortedChildSubagentSessions.slice(0, TEAM_SECTION_INITIAL_CHILD_COUNT), + : sortedChildSubagentSessions.slice( + 0, + TEAM_SECTION_INITIAL_CHILD_COUNT, + ), [showAllChildSubagents, sortedChildSubagentSessions], ); const visibleSiblingSubagentSessions = useMemo( @@ -662,7 +663,9 @@ export const ChatSidebar: React.FC = ({ }, [teamSectionIdentity]); useEffect(() => { - if (sortedChildSubagentSessions.length <= TEAM_SECTION_INITIAL_CHILD_COUNT) { + if ( + sortedChildSubagentSessions.length <= TEAM_SECTION_INITIAL_CHILD_COUNT + ) { setShowAllChildSubagents(false); } }, [sortedChildSubagentSessions.length]); @@ -785,9 +788,16 @@ export const ChatSidebar: React.FC = ({
- {options?.subtitle ?? resolveSubagentSessionTypeLabel(session.session_type)} - {session.role_hint ? 角色 · {session.role_hint} : null} - {updatedAt ? 更新于 {formatRelativeTime(updatedAt)} : null} + + {options?.subtitle ?? + resolveSubagentSessionTypeLabel(session.session_type)} + + {session.role_hint ? ( + 角色 · {session.role_hint} + ) : null} + {updatedAt ? ( + 更新于 {formatRelativeTime(updatedAt)} + ) : null}
{session.task_summary ? (

@@ -802,7 +812,7 @@ export const ChatSidebar: React.FC = ({ return (

@@ -1069,7 +1080,7 @@ export const ChatSidebar: React.FC = ({ {taskHeadingLabel}
{taskHeadingHint ? ( -

+

{taskHeadingHint}

) : null} @@ -1106,261 +1117,268 @@ export const ChatSidebar: React.FC = ({ ) : (
{sections.map((section) => { - const isOlderSection = section.key === "older"; - const isSectionCollapsed = collapsedSections[section.key]; - const visibleItems = - isOlderSection && !showAllOlder - ? section.items.slice(0, OLDER_TASKS_INITIAL_COUNT) - : section.items; + const isOlderSection = section.key === "older"; + const isSectionCollapsed = collapsedSections[section.key]; + const visibleItems = + isOlderSection && !showAllOlder + ? section.items.slice(0, OLDER_TASKS_INITIAL_COUNT) + : section.items; - if (section.items.length === 0) { - return null; - } + if (section.items.length === 0) { + return null; + } - return ( -
-
- - {section.items.length} - - + - {isSectionCollapsed ? null : ( -
- {visibleItems.map((item) => { - const statusMeta = STATUS_META[item.status]; - const isResumableItem = - item.status === "waiting" || - (item.status === "failed" && - item.statusReason === "workspace_error"); + {isSectionCollapsed ? null : ( +
+ {visibleItems.map((item) => { + const statusMeta = STATUS_META[item.status]; + const isResumableItem = + item.status === "waiting" || + (item.status === "failed" && + item.statusReason === "workspace_error"); - return ( -
{ - if (editingTopicId !== item.id) { - onSwitchTopic(item.id); - } - }} - onDoubleClick={() => - handleStartEdit(item.id, item.title) - } - onKeyDown={(event) => { - if ( - event.key === "Enter" || - event.key === " " - ) { - event.preventDefault(); + return ( +
{ if (editingTopicId !== item.id) { onSwitchTopic(item.id); } + }} + onDoubleClick={() => + handleStartEdit(item.id, item.title) } - }} - className={cn( - "group rounded-[22px] border p-3.5 text-left shadow-sm shadow-slate-950/5 transition", - isResumableItem - ? "border-amber-200/80 bg-[linear-gradient(180deg,rgba(255,251,235,0.9)_0%,rgba(255,255,255,0.96)_100%)] shadow-sm shadow-amber-950/5 dark:border-amber-500/20 dark:bg-white/10" - : "", - item.isCurrent - ? "border-slate-300 bg-white/98 ring-1 ring-slate-100 dark:border-white/15 dark:bg-white/10" - : "border-slate-200/70 bg-white/72 hover:border-slate-300 hover:bg-white/92 dark:border-white/10 dark:bg-white/5 dark:hover:border-white/10 dark:hover:bg-white/5", - )} - > -
- - -
-
- {editingTopicId === item.id ? ( - - setEditTitle(event.target.value) - } - onKeyDown={handleEditKeyDown} - onBlur={handleSaveEdit} - onClick={(event) => - event.stopPropagation() - } - className="h-8 flex-1 rounded-xl border border-slate-300 bg-white px-2.5 text-sm font-medium text-slate-900 outline-none focus:border-slate-400 dark:border-white/10 dark:bg-[#17191f] dark:text-slate-100" - /> - ) : ( - <> -
-
-
- {item.title || "未命名任务"} -
- {item.isPinned ? ( - - ) : null} - {item.hasUnread ? ( - - ) : null} -
-
-
-
- {formatRelativeTime(item.updatedAt)} -
- - - - - - - - handleStartEdit(item.id, item.title) - } - > - - 重命名任务 - - - handleTogglePinned(item.id) - } - > - {item.isPinned ? ( - - ) : ( - - )} - {item.isPinned ? "取消固定" : "固定任务"} - - - - handleDeleteClick(item.id) - } - > - - 删除任务 - - - -
- + onKeyDown={(event) => { + if ( + event.key === "Enter" || + event.key === " " + ) { + event.preventDefault(); + if (editingTopicId !== item.id) { + onSwitchTopic(item.id); + } + } + }} + className={cn( + "group rounded-[22px] border p-3.5 text-left shadow-sm shadow-slate-950/5 transition", + isResumableItem + ? "border-amber-200/80 bg-[linear-gradient(180deg,rgba(255,251,235,0.9)_0%,rgba(255,255,255,0.96)_100%)] shadow-sm shadow-amber-950/5 dark:border-amber-500/20 dark:bg-white/10" + : "", + item.isCurrent + ? "border-emerald-200/60 bg-white/98 ring-1 ring-emerald-50 dark:border-white/15 dark:bg-white/10" + : "border-slate-200/60 bg-white/72 hover:border-emerald-200/60 hover:bg-white/92 dark:border-white/10 dark:bg-white/5 dark:hover:border-white/10 dark:hover:bg-white/5", + )} + > +
+ + /> -
- {item.lastPreview} -
- -
- +
+ {editingTopicId === item.id ? ( + + setEditTitle(event.target.value) + } + onKeyDown={handleEditKeyDown} + onBlur={handleSaveEdit} + onClick={(event) => + event.stopPropagation() + } + className="h-8 flex-1 rounded-xl border border-slate-300 bg-white px-2.5 text-sm font-medium text-slate-900 outline-none focus:border-slate-400 dark:border-white/10 dark:bg-[#17191f] dark:text-slate-100" + /> + ) : ( + <> +
+
+
+ {item.title || "未命名任务"} +
+ {item.isPinned ? ( + + ) : null} + {item.hasUnread ? ( + + ) : null} +
+
+
+
+ {formatRelativeTime( + item.updatedAt, + )} +
+ + + + + + + + handleStartEdit( + item.id, + item.title, + ) + } + > + + 重命名任务 + + + handleTogglePinned(item.id) + } + > + {item.isPinned ? ( + + ) : ( + + )} + {item.isPinned + ? "取消固定" + : "固定任务"} + + + + handleDeleteClick(item.id) + } + > + + 删除任务 + + + +
+ )} - > - {item.status === "running" ? ( - - ) : null} - {item.statusLabel} - - - {item.messagesCount > 0 - ? `${item.messagesCount} 条消息` - : "尚未开始执行"} - - {isResumableItem && onResumeTask ? ( -
+ +
+ {item.lastPreview} +
+ +
+ - 继续任务 - - ) : null} + {item.status === "running" ? ( + + ) : null} + {item.statusLabel} + + + {item.messagesCount > 0 + ? `${item.messagesCount} 条消息` + : "尚未开始执行"} + + {isResumableItem && onResumeTask ? ( + + ) : null} +
-
- ); - })} + ); + })} - {isOlderSection && - section.items.length > OLDER_TASKS_INITIAL_COUNT && - !showAllOlder ? ( - - ) : null} -
- )} - - ); + {isOlderSection && + section.items.length > OLDER_TASKS_INITIAL_COUNT && + !showAllOlder ? ( + + ) : null} +
+ )} + + ); })}
)} diff --git a/src/components/agent/chat/components/EmptyState.test.tsx b/src/components/agent/chat/components/EmptyState.test.tsx index 576821549..f48056d46 100644 --- a/src/components/agent/chat/components/EmptyState.test.tsx +++ b/src/components/agent/chat/components/EmptyState.test.tsx @@ -284,12 +284,7 @@ describe("EmptyState", () => { await Promise.resolve(); }); - const expectedLabels = [ - "技能", - "自动化", - "多代理", - "浏览器接入", - ]; + const expectedLabels = ["技能", "自动化", "多代理", "浏览器接入"]; const expectedAlts = [ "技能能力卡占位图", "自动化能力卡占位图", @@ -745,7 +740,7 @@ describe("EmptyState", () => { executionStrategy?: "react" | "code_orchestrated" | "auto", images?: unknown[], ) => void - >(); + >(); const container = renderEmptyState({ input: "请输出一篇用户访谈纪要", activeTheme: "general", @@ -826,9 +821,8 @@ describe("EmptyState", () => { it("通用主题工具栏应包含附件、思考、Plan 与多代理开关", async () => { const onThinkingEnabledChange = vi.fn<(enabled: boolean) => void>(); const onSubagentEnabledChange = vi.fn<(enabled: boolean) => void>(); - const setExecutionStrategy = vi.fn< - (strategy: "react" | "code_orchestrated" | "auto") => void - >(); + const setExecutionStrategy = + vi.fn<(strategy: "react" | "code_orchestrated" | "auto") => void>(); const container = renderEmptyState({ activeTheme: "general", thinkingEnabled: false, @@ -896,5 +890,4 @@ describe("EmptyState", () => { expect(onLaunchBrowserAssist).toHaveBeenCalledTimes(1); }); - }); diff --git a/src/components/agent/chat/components/EmptyState.tsx b/src/components/agent/chat/components/EmptyState.tsx index 1ca48db4d..f94b7df7d 100644 --- a/src/components/agent/chat/components/EmptyState.tsx +++ b/src/components/agent/chat/components/EmptyState.tsx @@ -338,8 +338,10 @@ export const EmptyState: React.FC = ({ appendSelectedTextToRecommendation, setAppendSelectedTextToRecommendation, ] = useState(true); - const [entryRecommendedSolutionsVersion, setEntryRecommendedSolutionsVersion] = - useState(0); + const [ + entryRecommendedSolutionsVersion, + setEntryRecommendedSolutionsVersion, + ] = useState(0); useEffect(() => { const loadConfigPreferences = async () => { @@ -417,13 +419,10 @@ export const EmptyState: React.FC = ({ subagentEnabled, ]); - const entryRecommendedSolutions = useMemo( - () => { - void entryRecommendedSolutionsVersion; - return listEntryRecommendedSolutions(); - }, - [entryRecommendedSolutionsVersion], - ); + const entryRecommendedSolutions = useMemo(() => { + void entryRecommendedSolutionsVersion; + return listEntryRecommendedSolutions(); + }, [entryRecommendedSolutionsVersion]); const selectedTextPreview = useMemo(() => { const normalized = (recommendationSelectedText || "") @@ -488,11 +487,7 @@ export const EmptyState: React.FC = ({ } const imagesToSend = pendingImages.length > 0 ? pendingImages : undefined; - onSend( - wrapTextWithDefaultSkill(input), - executionStrategy, - imagesToSend, - ); + onSend(wrapTextWithDefaultSkill(input), executionStrategy, imagesToSend); setPendingImages([]); clearSelectedSkill?.(); }; @@ -658,7 +653,9 @@ export const EmptyState: React.FC = ({ key: "agent-teams", eyebrow: "支撑能力", title: "多代理", - value: subagentEnabled ? "当前任务支持并行协作" : "复杂任务可拆成并行分工", + value: subagentEnabled + ? "当前任务支持并行协作" + : "复杂任务可拆成并行分工", description: "当研究、方案和执行需要同时推进时,可把任务拆给多个代理并行处理,再统一回收结论。", icon: , @@ -672,9 +669,7 @@ export const EmptyState: React.FC = ({ key: "browser", eyebrow: "支撑能力", title: "浏览器接入", - value: browserAssistLoading - ? "正在检查连接状态" - : "CDP / 浏览器插件复用", + value: browserAssistLoading ? "正在检查连接状态" : "CDP / 浏览器插件复用", description: "登录、验证和网页动作可直接复用 CDP 或浏览器插件连接,不必再切到单独工作台。", icon: , @@ -686,7 +681,9 @@ export const EmptyState: React.FC = ({ void onLaunchBrowserAssist(); } : undefined, - mediaActionLabel: browserAssistLoading ? "浏览器连接准备中" : "连接浏览器", + mediaActionLabel: browserAssistLoading + ? "浏览器连接准备中" + : "连接浏览器", mediaActionDisabled: browserAssistLoading, }); @@ -847,9 +844,13 @@ export const EmptyState: React.FC = ({ handleApplyEntryRecommendedSolution(solution); }} > - {solution.title} + + {solution.title} + {solution.isRecent ? ( - {solution.badge} + + {solution.badge} + ) : null} diff --git a/src/components/agent/chat/components/EmptyStateComposerPanel.test.tsx b/src/components/agent/chat/components/EmptyStateComposerPanel.test.tsx index f5cba2aa8..f57aaf0cf 100644 --- a/src/components/agent/chat/components/EmptyStateComposerPanel.test.tsx +++ b/src/components/agent/chat/components/EmptyStateComposerPanel.test.tsx @@ -312,9 +312,9 @@ describe("EmptyStateComposerPanel", () => { onSend, }); - const textarea = container.querySelector("textarea") as - | HTMLTextAreaElement - | null; + const textarea = container.querySelector( + "textarea", + ) as HTMLTextAreaElement | null; const pendingButton = Array.from(container.querySelectorAll("button")).find( (button) => button.textContent?.includes("稍后处理"), ) as HTMLButtonElement | undefined; @@ -377,7 +377,9 @@ describe("EmptyStateComposerPanel", () => { expect(enableTeamButton).toBeTruthy(); act(() => { - enableTeamButton?.dispatchEvent(new MouseEvent("click", { bubbles: true })); + enableTeamButton?.dispatchEvent( + new MouseEvent("click", { bubbles: true }), + ); }); expect(onSubagentEnabledChange).toHaveBeenCalledWith(true); @@ -391,16 +393,14 @@ describe("EmptyStateComposerPanel", () => { onSubagentEnabledChange: vi.fn(), }); - const continueButton = Array.from(container.querySelectorAll("button")).find( - (button) => button.textContent?.includes("继续单代理"), - ); + const continueButton = Array.from( + container.querySelectorAll("button"), + ).find((button) => button.textContent?.includes("继续单代理")); expect(continueButton).toBeTruthy(); act(() => { - continueButton?.dispatchEvent( - new MouseEvent("click", { bubbles: true }), - ); + continueButton?.dispatchEvent(new MouseEvent("click", { bubbles: true })); }); expect(container.textContent).not.toContain("当前任务更适合 Team 协作"); @@ -427,7 +427,9 @@ describe("EmptyStateComposerPanel", () => { container.querySelector('[data-testid="empty-state-team-selector"]'), ).toBeNull(); expect( - container.querySelector('[data-testid="empty-state-team-mode-enable-button"]'), + container.querySelector( + '[data-testid="empty-state-team-mode-enable-button"]', + ), ).toBeNull(); const toggleButton = container.querySelector( @@ -561,14 +563,14 @@ describe("EmptyStateComposerPanel", () => { input: "请拆成多个子任务分别分析、实现、验证,并最终统一回归验收", }); - const enableButton = Array.from( - container.querySelectorAll("button"), - ).find((button) => button.textContent?.includes("启用 Team")) as - | HTMLButtonElement - | undefined; + const enableButton = Array.from(container.querySelectorAll("button")).find( + (button) => button.textContent?.includes("启用 Team"), + ) as HTMLButtonElement | undefined; expect( - container.querySelector('[data-testid="empty-state-team-mode-enable-button"]'), + container.querySelector( + '[data-testid="empty-state-team-mode-enable-button"]', + ), ).toBeNull(); expect( container.querySelector('button[title="多代理偏好已关闭"]'), diff --git a/src/components/agent/chat/components/EmptyStateHero.tsx b/src/components/agent/chat/components/EmptyStateHero.tsx index 272190cc5..0897b0b60 100644 --- a/src/components/agent/chat/components/EmptyStateHero.tsx +++ b/src/components/agent/chat/components/EmptyStateHero.tsx @@ -134,7 +134,7 @@ const SloganLine = styled.p` width: fit-content; align-items: center; gap: 1.3rem; - + /* lime gradient with scan effect */ background: linear-gradient( 90deg, @@ -162,7 +162,11 @@ const SloganLine = styled.p` height: 0.92rem; flex-shrink: 0; border-radius: 9999px; - background: linear-gradient(135deg, rgba(132, 204, 22, 0.9), rgba(16, 185, 129, 0.75)); + background: linear-gradient( + 135deg, + rgba(132, 204, 22, 0.9), + rgba(16, 185, 129, 0.75) + ); box-shadow: 0 0 0 12px rgba(132, 204, 22, 0.12); } `; @@ -272,7 +276,6 @@ const HeroCard = styled.article.attrs({ font-size: 0.92rem; line-height: 1.3; } - } .card-media { @@ -376,7 +379,9 @@ export function EmptyStateHero({ ) : null} {description} {supportingDescription ? ( - {supportingDescription} + + {supportingDescription} + ) : null} @@ -386,7 +391,9 @@ export function EmptyStateHero({ {badge.label} @@ -394,11 +401,9 @@ export function EmptyStateHero({ ))}
) : null} - - {prioritySlot ? ( @@ -483,7 +488,6 @@ export function EmptyStateHero({ ))} - ); diff --git a/src/components/agent/chat/components/EmptyStateQuickActions.tsx b/src/components/agent/chat/components/EmptyStateQuickActions.tsx index 33589e6f5..3b0184a31 100644 --- a/src/components/agent/chat/components/EmptyStateQuickActions.tsx +++ b/src/components/agent/chat/components/EmptyStateQuickActions.tsx @@ -263,10 +263,14 @@ export function EmptyStateQuickActions({ - onContextSearchModeChange("web")}> + onContextSearchModeChange("web")} + >
网络搜索 @@ -437,7 +460,9 @@ function GeneralWorkbenchContextPanelComponent({ ) : null}
- onContextSearchModeChange("social")}> + onContextSearchModeChange("social")} + >
社交媒体 @@ -453,7 +478,9 @@ function GeneralWorkbenchContextPanelComponent({
{contextSearchError ? ( -
{contextSearchError}
+
+ {contextSearchError} +
) : contextSearchLoading ? ( -
正在联网检索并整理上下文...
+
+ 正在联网检索并整理上下文... +
) : contextSearchBlockedReason ? ( -
{contextSearchBlockedReason}
+
+ {contextSearchBlockedReason} +
) : (
输入关键词后按 Enter,可直接把检索结果加入当前上下文。 @@ -498,7 +531,9 @@ function GeneralWorkbenchContextPanelComponent({
-
{selectedSearchResult.name}
+
+ {selectedSearchResult.name} +
{resolveContextSourceSubLabel( selectedSearchResult.source, @@ -510,13 +545,16 @@ function GeneralWorkbenchContextPanelComponent({ {selectedSearchResult.active ? " · 已启用" : " · 未启用"}
-
Source guide
+
+ Source guide +
{selectedSearchResult.query ? (
检索词:{selectedSearchResult.query}
) : null} - {selectedSearchResult.citations && selectedSearchResult.citations.length > 0 ? ( + {selectedSearchResult.citations && + selectedSearchResult.citations.length > 0 ? (
{selectedSearchResult.citations.map((citation) => (
上下文列表 - {contextItems.length} 条 + + {contextItems.length} 条 +
- 已生效 {contextBudget.activeCount}/{contextBudget.activeCountLimit} 条 · 检索结果{" "} - {searchContextItems.length} 条 · 估算 {contextBudget.estimatedTokens}/ - {contextBudget.tokenLimit} tokens + 已生效 {contextBudget.activeCount}/{contextBudget.activeCountLimit}{" "} + 条 · 检索结果 {searchContextItems.length} 条 · 估算{" "} + {contextBudget.estimatedTokens}/{contextBudget.tokenLimit} tokens
搜索结果可点击查看详情,其他上下文可直接勾选启用。 @@ -615,7 +655,9 @@ function GeneralWorkbenchContextPanelComponent({ void onDropContextFile(event); }} > -
or drop your files here
+
+ or drop your files here +
{contextCreateError ? ( -
{contextCreateError}
+
+ {contextCreateError} +
) : null}
@@ -697,7 +741,9 @@ function GeneralWorkbenchContextPanelComponent({
-

添加文本内容

+

+ 添加文本内容 +

-

添加网站链接

+

+ 添加网站链接 +

{isExpanded ? (
{entry.detail.argumentsText ? (
-
请求参数
+
+ 请求参数 +
{entry.detail.argumentsText}
@@ -255,7 +263,9 @@ export function GeneralWorkbenchExecLog({ ) : null} {entry.detail.resultText ? (
-
执行结果
+
+ 执行结果 +
{entry.detail.resultText}
@@ -263,7 +273,9 @@ export function GeneralWorkbenchExecLog({ ) : null} {entry.detail.errorText ? (
-
错误信息
+
+ 错误信息 +
{entry.detail.errorText}
@@ -271,7 +283,9 @@ export function GeneralWorkbenchExecLog({ ) : null} {entry.detail.sourceRef ? (
-
技能标识
+
+ 技能标识 +
{entry.detail.sourceRef}
@@ -279,7 +293,9 @@ export function GeneralWorkbenchExecLog({ ) : null} {entry.detail.description ? (
-
技能说明
+
+ 技能说明 +
{entry.detail.description}
@@ -287,15 +303,20 @@ export function GeneralWorkbenchExecLog({ ) : null} {entry.detail.whenToUse ? (
-
适用场景
+
+ 适用场景 +
{entry.detail.whenToUse}
) : null} - {entry.detail.workflowSteps && entry.detail.workflowSteps.length > 0 ? ( + {entry.detail.workflowSteps && + entry.detail.workflowSteps.length > 0 ? (
-
工作流步骤
+
+ 工作流步骤 +
{entry.detail.workflowSteps.map((step, index) => (
) : null} - {entry.detail.allowedTools && entry.detail.allowedTools.length > 0 ? ( + {entry.detail.allowedTools && + entry.detail.allowedTools.length > 0 ? (
-
允许工具
+
+ 允许工具 +
{entry.detail.allowedTools.map((toolName) => (
) : null} - {entry.detail.artifactPaths && entry.detail.artifactPaths.length > 0 ? ( + {entry.detail.artifactPaths && + entry.detail.artifactPaths.length > 0 ? (
-
关联产物
+
+ 关联产物 +
{entry.detail.artifactPaths.map((artifactPath) => (
{filteredEntries.map((entry) => (
- +
- + {entry.typeLabel} @@ -424,15 +458,20 @@ export function GeneralWorkbenchExecLog({ minute: "2-digit", second: "2-digit", }) - : new Date(entry.timestamp).toLocaleTimeString("zh-CN", { - hour: "2-digit", - minute: "2-digit", - second: "2-digit", - }) + : new Date(entry.timestamp).toLocaleTimeString( + "zh-CN", + { + hour: "2-digit", + minute: "2-digit", + second: "2-digit", + }, + ) : ""}
-
{entry.content}
+
+ {entry.content} +
{entry.meta ? (
{entry.meta}
) : null} diff --git a/src/components/agent/chat/components/GeneralWorkbenchHarnessCard.tsx b/src/components/agent/chat/components/GeneralWorkbenchHarnessCard.tsx index 734f54570..d9cbe6e87 100644 --- a/src/components/agent/chat/components/GeneralWorkbenchHarnessCard.tsx +++ b/src/components/agent/chat/components/GeneralWorkbenchHarnessCard.tsx @@ -77,7 +77,8 @@ export function GeneralWorkbenchHarnessCard({ }: GeneralWorkbenchHarnessCardProps) { const runStateMeta = resolveRunStateMeta(runState); const resolvedRunTitle = runTitle?.trim() || "暂无运行记录"; - const artifactSummary = artifactCount > 0 ? `${artifactCount} 个产物` : "暂无产物"; + const artifactSummary = + artifactCount > 0 ? `${artifactCount} 个产物` : "暂无产物"; const updatedAtLabel = formatUpdatedAt(updatedAt); const iconLabel = [ "工作台 Harness", @@ -312,7 +313,9 @@ export function GeneralWorkbenchHarnessCard({ 工作台 Harness - {runStateMeta.label} + + {runStateMeta.label} + {pendingCount > 0 ? ( {`待处理 ${pendingCount}`} ) : null} diff --git a/src/components/agent/chat/components/GeneralWorkbenchSidebar.test.tsx b/src/components/agent/chat/components/GeneralWorkbenchSidebar.test.tsx index 5cdc98818..7262e6592 100644 --- a/src/components/agent/chat/components/GeneralWorkbenchSidebar.test.tsx +++ b/src/components/agent/chat/components/GeneralWorkbenchSidebar.test.tsx @@ -114,9 +114,7 @@ function renderSidebar( searchMode: "web", query: "品牌 2026", previewText: "品牌讨论聚焦产品定位、渠道节奏与转化质量。", - citations: [ - { title: "官方博客", url: "https://example.com/blog" }, - ], + citations: [{ title: "官方博客", url: "https://example.com/blog" }], active: true, }, ], @@ -198,9 +196,9 @@ describe("GeneralWorkbenchSidebar", () => { it("点击添加上下文应打开添加弹窗", () => { const { container } = renderSidebar(); - const addContextButton = Array.from(container.querySelectorAll("button")).find( - (button) => button.textContent?.includes("添加上下文"), - ); + const addContextButton = Array.from( + container.querySelectorAll("button"), + ).find((button) => button.textContent?.includes("添加上下文")); expect(addContextButton).toBeTruthy(); if (addContextButton) { act(() => { @@ -218,9 +216,9 @@ describe("GeneralWorkbenchSidebar", () => { const onAddTextContext = vi.fn().mockResolvedValue(undefined); const { container } = renderSidebar({ onAddTextContext }); - const addContextButton = Array.from(container.querySelectorAll("button")).find( - (button) => button.textContent?.includes("添加上下文"), - ); + const addContextButton = Array.from( + container.querySelectorAll("button"), + ).find((button) => button.textContent?.includes("添加上下文")); expect(addContextButton).toBeTruthy(); if (addContextButton) { act(() => { @@ -346,7 +344,9 @@ describe("GeneralWorkbenchSidebar", () => { expect(container.textContent).toContain("技能:社媒主稿与封面"); expect(container.textContent).toContain("生成内容主稿,并补齐封面素材。"); - expect(container.textContent).toContain("技能标识:content_post_with_cover"); + expect(container.textContent).toContain( + "技能标识:content_post_with_cover", + ); }); it("执行日志应支持展开技能详情", () => { @@ -379,7 +379,9 @@ describe("GeneralWorkbenchSidebar", () => { expect(container.textContent).toContain("文件读取"); expect(container.textContent).toContain("图片生成"); expect(container.textContent).toContain("适用场景"); - expect(container.textContent).toContain("适合需要主稿与封面同时产出的社媒场景。"); + expect(container.textContent).toContain( + "适合需要主稿与封面同时产出的社媒场景。", + ); }); it("执行日志应支持展开工具详情", () => { @@ -625,7 +627,9 @@ describe("GeneralWorkbenchSidebar", () => { expect(container.textContent).toContain("品牌话题观察"); expect(container.textContent).not.toContain("检索词:品牌 2026"); expect(container.textContent).not.toContain("品牌讨论聚焦产品定位"); - expect((container.textContent?.match(/品牌话题观察/g) || []).length).toBe(1); + expect((container.textContent?.match(/品牌话题观察/g) || []).length).toBe( + 1, + ); const openButton = container.querySelector( 'button[aria-label="查看搜索结果 品牌话题观察"]', @@ -771,7 +775,7 @@ describe("GeneralWorkbenchSidebar", () => { gateKey: "topic_select", source: "skill", artifactPaths: ["content-posts/research.md"], - inputSummary: "{\"topic\":\"AI\"}", + inputSummary: '{"topic":"AI"}', outputSummary: "已完成选题调研", }, { @@ -810,7 +814,7 @@ describe("GeneralWorkbenchSidebar", () => { expect(container.textContent).toContain("write_file"); expect(container.textContent).toContain("技能:research_topic"); expect(container.textContent).toContain("修改:content-posts/research.md"); - expect(container.textContent).toContain("输入:{\"topic\":\"AI\"}"); + expect(container.textContent).toContain('输入:{"topic":"AI"}'); expect(container.textContent).toContain("输出:已完成选题调研"); const runButtons = Array.from(container.querySelectorAll("button")).filter( (button) => button.textContent === "运行:rungrp01", @@ -1006,18 +1010,25 @@ describe("GeneralWorkbenchSidebar", () => { }); } - expect(container.textContent).toContain("工作流:social_content_pipeline_v1"); + expect(container.textContent).toContain( + "工作流:social_content_pipeline_v1", + ); expect(container.textContent).toContain("执行ID:exec-artifact-1"); expect(container.textContent).toContain("版本ID:ver-artifact-1"); - expect(container.textContent).toContain("阶段:选题闸门 → 写作闸门 → 发布闸门"); + expect(container.textContent).toContain( + "阶段:选题闸门 → 写作闸门 → 发布闸门", + ); expect(container.textContent).toContain("content-posts/demo.md"); - expect(container.textContent).toContain("content-posts/demo.publish-pack.json"); + expect(container.textContent).toContain( + "content-posts/demo.publish-pack.json", + ); - const copyArtifactButton = Array.from(container.querySelectorAll("button")).find( - (button) => - button - .getAttribute("aria-label") - ?.startsWith("复制产物路径-content-posts/demo.md"), + const copyArtifactButton = Array.from( + container.querySelectorAll("button"), + ).find((button) => + button + .getAttribute("aria-label") + ?.startsWith("复制产物路径-content-posts/demo.md"), ); expect(copyArtifactButton).toBeTruthy(); if (copyArtifactButton) { @@ -1026,13 +1037,16 @@ describe("GeneralWorkbenchSidebar", () => { }); } - expect(mockWriteClipboardText).toHaveBeenCalledWith("content-posts/demo.md"); + expect(mockWriteClipboardText).toHaveBeenCalledWith( + "content-posts/demo.md", + ); - const revealArtifactButton = Array.from(container.querySelectorAll("button")).find( - (button) => - button - .getAttribute("aria-label") - ?.startsWith("定位产物路径-content-posts/demo.md"), + const revealArtifactButton = Array.from( + container.querySelectorAll("button"), + ).find((button) => + button + .getAttribute("aria-label") + ?.startsWith("定位产物路径-content-posts/demo.md"), ); expect(revealArtifactButton).toBeTruthy(); if (revealArtifactButton) { @@ -1045,11 +1059,12 @@ describe("GeneralWorkbenchSidebar", () => { "content-posts/demo.md", ); - const openArtifactButton = Array.from(container.querySelectorAll("button")).find( - (button) => - button - .getAttribute("aria-label") - ?.startsWith("打开产物路径-content-posts/demo.md"), + const openArtifactButton = Array.from( + container.querySelectorAll("button"), + ).find((button) => + button + .getAttribute("aria-label") + ?.startsWith("打开产物路径-content-posts/demo.md"), ); expect(openArtifactButton).toBeTruthy(); if (openArtifactButton) { @@ -1101,11 +1116,12 @@ describe("GeneralWorkbenchSidebar", () => { }); } - const revealArtifactButton = Array.from(container.querySelectorAll("button")).find( - (button) => - button - .getAttribute("aria-label") - ?.startsWith("定位活动产物路径-content-posts/group.md"), + const revealArtifactButton = Array.from( + container.querySelectorAll("button"), + ).find((button) => + button + .getAttribute("aria-label") + ?.startsWith("定位活动产物路径-content-posts/group.md"), ); expect(revealArtifactButton).toBeTruthy(); if (revealArtifactButton) { @@ -1118,11 +1134,12 @@ describe("GeneralWorkbenchSidebar", () => { "content-posts/group.md", ); - const openArtifactButton = Array.from(container.querySelectorAll("button")).find( - (button) => - button - .getAttribute("aria-label") - ?.startsWith("打开活动产物路径-content-posts/group.md"), + const openArtifactButton = Array.from( + container.querySelectorAll("button"), + ).find((button) => + button + .getAttribute("aria-label") + ?.startsWith("打开活动产物路径-content-posts/group.md"), ); expect(openArtifactButton).toBeTruthy(); if (openArtifactButton) { @@ -1197,7 +1214,9 @@ describe("GeneralWorkbenchSidebar", () => { const consoleWarnSpy = vi .spyOn(console, "warn") .mockImplementation(() => {}); - mockRevealSessionFileInFinder.mockRejectedValueOnce(new Error("文件不存在")); + mockRevealSessionFileInFinder.mockRejectedValueOnce( + new Error("文件不存在"), + ); try { const { container } = renderSidebar({ activeRunDetail: { diff --git a/src/components/agent/chat/components/GeneralWorkbenchSidebarPanels.tsx b/src/components/agent/chat/components/GeneralWorkbenchSidebarPanels.tsx index efae17916..c06559d61 100644 --- a/src/components/agent/chat/components/GeneralWorkbenchSidebarPanels.tsx +++ b/src/components/agent/chat/components/GeneralWorkbenchSidebarPanels.tsx @@ -4,8 +4,7 @@ import { GeneralWorkbenchWorkflowPanel } from "./GeneralWorkbenchWorkflowPanel"; import type { GeneralWorkbenchSidebarTab } from "./GeneralWorkbenchSidebarShell"; import type { GeneralWorkbenchSidebarContentProps } from "./generalWorkbenchSidebarContentContract"; -export interface GeneralWorkbenchSidebarPanelsProps - extends GeneralWorkbenchSidebarContentProps { +export interface GeneralWorkbenchSidebarPanelsProps extends GeneralWorkbenchSidebarContentProps { activeTab: GeneralWorkbenchSidebarTab; } diff --git a/src/components/agent/chat/components/GeneralWorkbenchSidebarShell.tsx b/src/components/agent/chat/components/GeneralWorkbenchSidebarShell.tsx index 9fff7ece4..30f81fc4f 100644 --- a/src/components/agent/chat/components/GeneralWorkbenchSidebarShell.tsx +++ b/src/components/agent/chat/components/GeneralWorkbenchSidebarShell.tsx @@ -130,7 +130,9 @@ export function GeneralWorkbenchSidebarShell({ onClick={() => onTabChange("context")} > 上下文 - + {activeContextCount} @@ -142,7 +144,9 @@ export function GeneralWorkbenchSidebarShell({ onClick={() => onTabChange("workflow")} > 编排 - + {branchCount} diff --git a/src/components/agent/chat/components/GeneralWorkbenchWorkflowPanel.tsx b/src/components/agent/chat/components/GeneralWorkbenchWorkflowPanel.tsx index 0c5996b10..f66c55bae 100644 --- a/src/components/agent/chat/components/GeneralWorkbenchWorkflowPanel.tsx +++ b/src/components/agent/chat/components/GeneralWorkbenchWorkflowPanel.tsx @@ -20,7 +20,10 @@ import { DropdownMenuTrigger, } from "@/components/ui/dropdown-menu"; import { cn } from "@/lib/utils"; -import type { TopicBranchItem, TopicBranchStatus } from "../hooks/useTopicBranchBoard"; +import type { + TopicBranchItem, + TopicBranchStatus, +} from "../hooks/useTopicBranchBoard"; import type { SidebarActivityLog } from "../hooks/useThemeContextWorkspace"; import type { GeneralWorkbenchActivityLogGroup, @@ -249,8 +252,7 @@ function getWorkflowStepRowClassName(status: StepStatus) { "flex items-center gap-2 rounded-[12px] border px-2.5 py-2 text-sm leading-5", status === "completed" && "border-emerald-200 bg-emerald-50/80 text-slate-900", - status === "active" && - "border-sky-200 bg-sky-50/80 text-slate-900", + status === "active" && "border-sky-200 bg-sky-50/80 text-slate-900", status !== "completed" && status !== "active" && "border-slate-200/80 bg-white/82 text-slate-500", @@ -274,7 +276,9 @@ function getBranchStatusText(status: TopicBranchStatus): string { return "备选"; } -function formatGateLabel(gateKey?: SidebarActivityLog["gateKey"]): string | null { +function formatGateLabel( + gateKey?: SidebarActivityLog["gateKey"], +): string | null { if (!gateKey || gateKey === "idle") { return null; } @@ -320,7 +324,8 @@ function renderActivityLogItem( const gateLabel = formatGateLabel(group.gateKey); const runLabel = formatRunIdShort(group.runId); const sourceLabel = group.source?.trim() || "-"; - const primaryLog = group.logs.find((log) => log.source === "skill") || group.logs[0]; + const primaryLog = + group.logs.find((log) => log.source === "skill") || group.logs[0]; return ( @@ -351,14 +356,21 @@ function renderActivityLogItem( {log.name} {log.timeLabel} - {log.inputSummary ? 输入:{log.inputSummary} : null} - {log.outputSummary ? 输出:{log.outputSummary} : null} + {log.inputSummary ? ( + 输入:{log.inputSummary} + ) : null} + {log.outputSummary ? ( + 输出:{log.outputSummary} + ) : null} ))} {group.runId && onViewRunDetail ? ( - onViewRunDetail(group.runId!)}> + onViewRunDetail(group.runId!)} + > 运行:{runLabel || group.runId} ) : null} @@ -445,7 +457,10 @@ function GeneralWorkbenchWorkflowPanelComponent({
-
{workflowSteps.map((step) => ( -
+
{getStepIcon(step.status)} {step.title}
@@ -503,12 +521,16 @@ function GeneralWorkbenchWorkflowPanelComponent({
{isVersionMode ? "产物版本" : "篇内分支"} - {branchItems.length} + + {branchItems.length} +
{branchItems.length === 0 ? ( - {isVersionMode ? "暂无文稿版本,先生成或创建快照" : "暂无分支任务"} + {isVersionMode + ? "暂无文稿版本,先生成或创建快照" + : "暂无分支任务"} ) : ( branchItems.map((item) => ( @@ -522,16 +544,23 @@ function GeneralWorkbenchWorkflowPanelComponent({ {getBranchStatusText(item.status)} {!isVersionMode ? ( - onDeleteTopic(item.id)} aria-label="删除分支"> + onDeleteTopic(item.id)} + aria-label="删除分支" + > ) : null} - onSetBranchStatus(item.id, "merged")}> + onSetBranchStatus(item.id, "merged")} + > {isVersionMode ? "设为主稿" : "采纳到主稿"} - onSetBranchStatus(item.id, "pending")}> + onSetBranchStatus(item.id, "pending")} + > {isVersionMode ? "标记待评审" : "标记待决策"} @@ -554,7 +583,11 @@ function GeneralWorkbenchWorkflowPanelComponent({ className={TOGGLE_BUTTON_CLASSNAME} onClick={onToggleCreationTasks} > - {showCreationTasks ? : } + {showCreationTasks ? ( + + ) : ( + + )}
@@ -585,7 +618,9 @@ function GeneralWorkbenchWorkflowPanelComponent({ {task.absolutePath ? ( - {task.absolutePath} + + {task.absolutePath} + - {showActivityLogs ? : } + {showActivityLogs ? ( + + ) : ( + + )}
@@ -659,15 +698,23 @@ function GeneralWorkbenchWorkflowPanelComponent({ 运行详情 ID:{activeRunDetail.id} - 状态:{formatRunStatusLabel(activeRunDetail.status)} + + 状态:{formatRunStatusLabel(activeRunDetail.status)} + {runMetadataSummary.workflow ? ( - 工作流:{runMetadataSummary.workflow} + + 工作流:{runMetadataSummary.workflow} + ) : null} {runMetadataSummary.executionId ? ( - 执行ID:{runMetadataSummary.executionId} + + 执行ID:{runMetadataSummary.executionId} + ) : null} {runMetadataSummary.versionId ? ( - 版本ID:{runMetadataSummary.versionId} + + 版本ID:{runMetadataSummary.versionId} + ) : null} {activeRunStagesLabel ? ( 阶段:{activeRunStagesLabel} @@ -696,7 +743,9 @@ function GeneralWorkbenchWorkflowPanelComponent({ {runMetadataSummary.artifactPaths.map((artifactPath) => ( - {artifactPath} + + {artifactPath} + { setInputValue(strategyTextarea, "先补 runtime save,再补 UI 回归。"); } if (regressionsTextarea) { - setInputValue(regressionsTextarea, "npm run test:contracts\nRust 定向测试"); + setInputValue( + regressionsTextarea, + "npm run test:contracts\nRust 定向测试", + ); } if (followupsTextarea) { setInputValue(followupsTextarea, "补充 HarnessStatusPanel 测试"); diff --git a/src/components/agent/chat/components/ImageTaskViewer.tsx b/src/components/agent/chat/components/ImageTaskViewer.tsx index b326e7477..f730c2324 100644 --- a/src/components/agent/chat/components/ImageTaskViewer.tsx +++ b/src/components/agent/chat/components/ImageTaskViewer.tsx @@ -346,7 +346,9 @@ export function ImageTaskViewer({
{reason === "error" - ? resolveImageUnavailableDescription(selectedTask?.mode) + ? resolveImageUnavailableDescription( + selectedTask?.mode, + ) : resolveEmptyStateDescription( selectedTask?.status, selectedTask?.failureMessage, diff --git a/src/components/agent/chat/components/ImageWorkbenchMessagePreview.tsx b/src/components/agent/chat/components/ImageWorkbenchMessagePreview.tsx index f6fd64054..8097017d7 100644 --- a/src/components/agent/chat/components/ImageWorkbenchMessagePreview.tsx +++ b/src/components/agent/chat/components/ImageWorkbenchMessagePreview.tsx @@ -108,10 +108,7 @@ function resolveDescription(preview: MessageImageWorkbenchPreview): string { const statusMessage = preview.statusMessage?.trim(); if ( statusMessage && - !( - preview.status !== "running" && - isTransitionStatusMessage(statusMessage) - ) + !(preview.status !== "running" && isTransitionStatusMessage(statusMessage)) ) { return statusMessage; } @@ -147,7 +144,9 @@ function resolveDescription(preview: MessageImageWorkbenchPreview): string { } } -function resolvePlaceholderLabel(preview: MessageImageWorkbenchPreview): string { +function resolvePlaceholderLabel( + preview: MessageImageWorkbenchPreview, +): string { if (preview.status === "failed") { return "暂未生成成功"; } @@ -174,11 +173,11 @@ function shouldShowSourceFootnote( ): boolean { return Boolean( preview.mode === "edit" || - preview.mode === "variation" || - preview.sourceImageUrl?.trim() || - preview.sourceImagePrompt?.trim() || - preview.sourceImageRef?.trim() || - preview.sourceImageCount, + preview.mode === "variation" || + preview.sourceImageUrl?.trim() || + preview.sourceImagePrompt?.trim() || + preview.sourceImageRef?.trim() || + preview.sourceImageCount, ); } @@ -214,7 +213,10 @@ function resolveSourceFootnote( return `${resolveSourceLabel(preview.mode)}:${resolveSourceSummary(preview)}`; } -function renderPlaceholder(preview: MessageImageWorkbenchPreview, reason: string) { +function renderPlaceholder( + preview: MessageImageWorkbenchPreview, + reason: string, +) { return (
diff --git a/src/components/agent/chat/components/Inputbar/components/InputbarAccessModeSelect.tsx b/src/components/agent/chat/components/Inputbar/components/InputbarAccessModeSelect.tsx index 35a044853..90f4b72d0 100644 --- a/src/components/agent/chat/components/Inputbar/components/InputbarAccessModeSelect.tsx +++ b/src/components/agent/chat/components/Inputbar/components/InputbarAccessModeSelect.tsx @@ -1,10 +1,6 @@ import React from "react"; import { ShieldCheck } from "lucide-react"; -import { - MetaSelect, - MetaSelectIcon, - MetaSelectWrap, -} from "../styles"; +import { MetaSelect, MetaSelectIcon, MetaSelectWrap } from "../styles"; import type { AgentAccessMode } from "../../../hooks/agentChatStorage"; interface InputbarAccessModeSelectProps { diff --git a/src/components/agent/chat/components/Inputbar/components/InputbarCore.test.tsx b/src/components/agent/chat/components/Inputbar/components/InputbarCore.test.tsx index 0de6b42a7..33385987b 100644 --- a/src/components/agent/chat/components/Inputbar/components/InputbarCore.test.tsx +++ b/src/components/agent/chat/components/Inputbar/components/InputbarCore.test.tsx @@ -326,7 +326,9 @@ describe("InputbarCore", () => { expect(removeButton).toBeTruthy(); act(() => { - removeButton?.dispatchEvent(new MouseEvent("mousedown", { bubbles: true })); + removeButton?.dispatchEvent( + new MouseEvent("mousedown", { bubbles: true }), + ); removeButton?.dispatchEvent(new MouseEvent("click", { bubbles: true })); }); diff --git a/src/components/agent/chat/components/Inputbar/components/InputbarCore.tsx b/src/components/agent/chat/components/Inputbar/components/InputbarCore.tsx index 442bb76da..ee07655b2 100644 --- a/src/components/agent/chat/components/Inputbar/components/InputbarCore.tsx +++ b/src/components/agent/chat/components/Inputbar/components/InputbarCore.tsx @@ -170,7 +170,8 @@ export const InputbarCore: React.FC = ({ : ""; const shouldRenderMetaBar = !shouldUseCompactFloatingComposer && - (Boolean(leftExtra) || (toolMode === "default" && !shouldCollapseFloatingTools)); + (Boolean(leftExtra) || + (toolMode === "default" && !shouldCollapseFloatingTools)); const dictationButtonTitle = isDictationProcessing ? dictationState === "polishing" ? "语音润色中" @@ -189,11 +190,18 @@ export const InputbarCore: React.FC = ({ }, [isFloatingVariant, toolMode]); const handleCollapseComposer = useCallback(() => { - if (!isFloatingVariant || toolMode !== "attach-only" || pendingImages.length > 0) { + if ( + !isFloatingVariant || + toolMode !== "attach-only" || + pendingImages.length > 0 + ) { return; } const activeElement = document.activeElement; - if (activeElement && inputBarContainerRef.current?.contains(activeElement)) { + if ( + activeElement && + inputBarContainerRef.current?.contains(activeElement) + ) { return; } setIsComposerExpanded(false); @@ -301,7 +309,9 @@ export const InputbarCore: React.FC = ({ type="button" aria-label={`移除图片 ${index + 1}`} onMouseDown={handleRemoveImageMouseDown} - onClick={(event) => handleRemoveImageClick(event, index)} + onClick={(event) => + handleRemoveImageClick(event, index) + } > @@ -339,7 +349,9 @@ export const InputbarCore: React.FC = ({ onClick={handleToggleTextareaExpanded} disabled={disabled} className={isTextareaExpanded ? "is-active" : ""} - aria-label={isTextareaExpanded ? "收起输入框" : "展开输入框"} + aria-label={ + isTextareaExpanded ? "收起输入框" : "展开输入框" + } title={isTextareaExpanded ? "收起输入框" : "展开输入框"} > {isTextareaExpanded ? ( diff --git a/src/components/agent/chat/components/Inputbar/components/InputbarExecutionStrategySelect.tsx b/src/components/agent/chat/components/Inputbar/components/InputbarExecutionStrategySelect.tsx index 00fcfdbbf..244cc2749 100644 --- a/src/components/agent/chat/components/Inputbar/components/InputbarExecutionStrategySelect.tsx +++ b/src/components/agent/chat/components/Inputbar/components/InputbarExecutionStrategySelect.tsx @@ -18,8 +18,11 @@ interface InputbarExecutionStrategySelectProps { export const InputbarExecutionStrategySelect: React.FC< InputbarExecutionStrategySelectProps > = (props) => { - const { isFullscreen = false, executionStrategy, setExecutionStrategy } = - props; + const { + isFullscreen = false, + executionStrategy, + setExecutionStrategy, + } = props; if (isFullscreen || !setExecutionStrategy) { return null; diff --git a/src/components/agent/chat/components/Inputbar/components/InputbarTools.tsx b/src/components/agent/chat/components/Inputbar/components/InputbarTools.tsx index eb8892695..c9fc6da9a 100644 --- a/src/components/agent/chat/components/Inputbar/components/InputbarTools.tsx +++ b/src/components/agent/chat/components/Inputbar/components/InputbarTools.tsx @@ -1,9 +1,5 @@ import React from "react"; -import { - Lightbulb, - Globe, - Workflow, -} from "lucide-react"; +import { Lightbulb, Globe, Workflow } from "lucide-react"; import { ToolButton } from "../styles"; import { isGeneralResearchTheme } from "../../../utils/generalAgentPrompt"; diff --git a/src/components/agent/chat/components/Inputbar/components/InputbarVisionCapabilityNotice.test.tsx b/src/components/agent/chat/components/Inputbar/components/InputbarVisionCapabilityNotice.test.tsx index cc3a15e9a..f8abe63f5 100644 --- a/src/components/agent/chat/components/Inputbar/components/InputbarVisionCapabilityNotice.test.tsx +++ b/src/components/agent/chat/components/Inputbar/components/InputbarVisionCapabilityNotice.test.tsx @@ -9,7 +9,8 @@ const mockUseProviderModels = vi.fn(); const mockResolveVisionModel = vi.fn(); vi.mock("@/hooks/useConfiguredProviders", () => ({ - useConfiguredProviders: (options: unknown) => mockUseConfiguredProviders(options), + useConfiguredProviders: (options: unknown) => + mockUseConfiguredProviders(options), })); vi.mock("@/hooks/useProviderModels", () => ({ @@ -131,6 +132,8 @@ describe("InputbarVisionCapabilityNotice", () => { const container = renderNotice(); - expect(container.textContent).toContain("当前 Provider 暂无可用的多模态模型"); + expect(container.textContent).toContain( + "当前 Provider 暂无可用的多模态模型", + ); }); }); diff --git a/src/components/agent/chat/components/Inputbar/components/InputbarVisionCapabilityNotice.tsx b/src/components/agent/chat/components/Inputbar/components/InputbarVisionCapabilityNotice.tsx index 37ce8ad15..d167caffc 100644 --- a/src/components/agent/chat/components/Inputbar/components/InputbarVisionCapabilityNotice.tsx +++ b/src/components/agent/chat/components/Inputbar/components/InputbarVisionCapabilityNotice.tsx @@ -31,7 +31,11 @@ export const InputbarVisionCapabilityNotice: React.FC< providerType: selectedProvider?.type, apiHost: selectedProvider?.apiHost, }), - [selectedProvider?.apiHost, selectedProvider?.providerId, selectedProvider?.type], + [ + selectedProvider?.apiHost, + selectedProvider?.providerId, + selectedProvider?.type, + ], ); const { models, loading: modelsLoading } = useProviderModels( diff --git a/src/components/agent/chat/components/Inputbar/components/TeamSelectorPanel.test.tsx b/src/components/agent/chat/components/Inputbar/components/TeamSelectorPanel.test.tsx index ab9998e5f..6a9441248 100644 --- a/src/components/agent/chat/components/Inputbar/components/TeamSelectorPanel.test.tsx +++ b/src/components/agent/chat/components/Inputbar/components/TeamSelectorPanel.test.tsx @@ -158,7 +158,9 @@ describe("TeamSelectorPanel", () => { await flushEffects(); - const savedTeam = onSelectTeam.mock.calls[0]?.[0] as TeamDefinition | undefined; + const savedTeam = onSelectTeam.mock.calls[0]?.[0] as + | TeamDefinition + | undefined; expect(savedTeam).toBeTruthy(); expect(savedTeam?.source).toBe("custom"); diff --git a/src/components/agent/chat/components/Inputbar/components/TeamSelectorPanel.tsx b/src/components/agent/chat/components/Inputbar/components/TeamSelectorPanel.tsx index 5c1e8b8e3..2b7aabae6 100644 --- a/src/components/agent/chat/components/Inputbar/components/TeamSelectorPanel.tsx +++ b/src/components/agent/chat/components/Inputbar/components/TeamSelectorPanel.tsx @@ -210,8 +210,8 @@ function TeamCard({ {role.profileId ? ( 画像 ·{" "} - {getBuiltinTeamProfileOption(role.profileId)?.label || - role.profileId} + {getBuiltinTeamProfileOption(role.profileId) + ?.label || role.profileId} ) : null} {role.roleKey ? ( @@ -225,7 +225,8 @@ function TeamCard({ className="rounded-full border border-slate-200 bg-slate-50 px-2 py-0.5 text-[11px] text-slate-500" > 技能 ·{" "} - {getBuiltinTeamSkillOption(skillId)?.label || skillId} + {getBuiltinTeamSkillOption(skillId)?.label || + skillId} ))}
@@ -332,7 +333,9 @@ export const TeamSelectorPanel: React.FC = ({ const builtinTeams = useMemo( () => - listBuiltinTeamDefinitions().filter((team) => matchTeamQuery(team, query)), + listBuiltinTeamDefinitions().filter((team) => + matchTeamQuery(team, query), + ), [query], ); @@ -374,7 +377,11 @@ export const TeamSelectorPanel: React.FC = ({ }; const handleStartCreate = (base?: TeamDefinition | null) => { - setDraft(base ? buildDraftFromTeam(cloneTeamDefinitionAsCustom(base)) : createBlankDraft(activeTheme)); + setDraft( + base + ? buildDraftFromTeam(cloneTeamDefinitionAsCustom(base)) + : createBlankDraft(activeTheme), + ); }; const handleStartEdit = (team: TeamDefinition) => { @@ -399,7 +406,8 @@ export const TeamSelectorPanel: React.FC = ({ theme: draft?.theme, presetId: draft?.presetId || - (draft?.id && customTeams.find((team) => team.id === draft.id)?.presetId), + (draft?.id && + customTeams.find((team) => team.id === draft.id)?.presetId), roles: draft?.roles, }); @@ -417,9 +425,10 @@ export const TeamSelectorPanel: React.FC = ({ Date.now(), }; - const nextCustomTeams = [...customTeams.filter((team) => team.id !== nextTeam.id), nextTeam].sort( - (left, right) => (right.updatedAt || 0) - (left.updatedAt || 0), - ); + const nextCustomTeams = [ + ...customTeams.filter((team) => team.id !== nextTeam.id), + nextTeam, + ].sort((left, right) => (right.updatedAt || 0) - (left.updatedAt || 0)); try { await persistCustomTeams(nextCustomTeams); setCustomTeams(nextCustomTeams); @@ -514,7 +523,8 @@ export const TeamSelectorPanel: React.FC = ({ if (!inspectorTeam) { return (
- 还没有可预览的 Team。你可以从左侧模板中查看详情,或新建一个自定义 Team。 + 还没有可预览的 Team。你可以从左侧模板中查看详情,或新建一个自定义 + Team。
); } @@ -550,7 +560,8 @@ export const TeamSelectorPanel: React.FC = ({ ) : null}
- {inspectorTeam.description || "该 Team 将作为主代理拆分子任务时的协作参考。"} + {inspectorTeam.description || + "该 Team 将作为主代理拆分子任务时的协作参考。"}
@@ -939,7 +950,9 @@ export const TeamSelectorPanel: React.FC = ({ onChange={(event) => updateDraftRole(index, (item) => ({ ...item, - skillIds: parseSkillIdsInput(event.target.value), + skillIds: parseSkillIdsInput( + event.target.value, + ), })) } data-testid={`team-role-skill-ids-input-${index}`} @@ -1054,10 +1067,7 @@ export const TeamSelectorPanel: React.FC = ({ className="bg-slate-900 text-white hover:bg-slate-800" onClick={() => handleStartCreate( - selectedTeam || - recommendedTeam || - builtinTeams[0] || - null, + selectedTeam || recommendedTeam || builtinTeams[0] || null, ) } > diff --git a/src/components/agent/chat/components/Inputbar/hooks/useHintRoutes.ts b/src/components/agent/chat/components/Inputbar/hooks/useHintRoutes.ts index de475607c..2a5843d9f 100644 --- a/src/components/agent/chat/components/Inputbar/hooks/useHintRoutes.ts +++ b/src/components/agent/chat/components/Inputbar/hooks/useHintRoutes.ts @@ -18,10 +18,7 @@ interface UseHintRoutesParams { textareaRef: RefObject; } -export function useHintRoutes({ - setInput, - textareaRef, -}: UseHintRoutesParams) { +export function useHintRoutes({ setInput, textareaRef }: UseHintRoutesParams) { const [showHintPopup, setShowHintPopup] = useState(false); const [hintRoutes, setHintRoutes] = useState([]); const [hintIndex, setHintIndex] = useState(0); diff --git a/src/components/agent/chat/components/Inputbar/hooks/useImageAttachments.test.tsx b/src/components/agent/chat/components/Inputbar/hooks/useImageAttachments.test.tsx index d9b437c0b..0ea3912b2 100644 --- a/src/components/agent/chat/components/Inputbar/hooks/useImageAttachments.test.tsx +++ b/src/components/agent/chat/components/Inputbar/hooks/useImageAttachments.test.tsx @@ -40,7 +40,8 @@ class MockFileReader { } function Harness() { - const { pendingImages, handlePaste, handleRemoveImage } = useImageAttachments(); + const { pendingImages, handlePaste, handleRemoveImage } = + useImageAttachments(); return (
diff --git a/src/components/agent/chat/components/Inputbar/hooks/useImageAttachments.ts b/src/components/agent/chat/components/Inputbar/hooks/useImageAttachments.ts index 9ca45abae..c64b0a3c2 100644 --- a/src/components/agent/chat/components/Inputbar/hooks/useImageAttachments.ts +++ b/src/components/agent/chat/components/Inputbar/hooks/useImageAttachments.ts @@ -18,11 +18,17 @@ export function useImageAttachments() { const fileInputRef = useRef(null); const appendImageFile = useCallback( - async (file: File, successMessage?: string, preferredMediaType?: string) => { + async ( + file: File, + successMessage?: string, + preferredMediaType?: string, + ) => { try { const image = await readImageAttachment(file, preferredMediaType); setPendingImages((prev) => [...prev, image]); - toast.success(successMessage ?? `已添加图片: ${file.name || "未命名图片"}`); + toast.success( + successMessage ?? `已添加图片: ${file.name || "未命名图片"}`, + ); } catch { toast.error(`图片读取失败: ${file.name || "未命名图片"}`); } diff --git a/src/components/agent/chat/components/Inputbar/hooks/useInputbarDictation.ts b/src/components/agent/chat/components/Inputbar/hooks/useInputbarDictation.ts index d56364460..830d7bfe5 100644 --- a/src/components/agent/chat/components/Inputbar/hooks/useInputbarDictation.ts +++ b/src/components/agent/chat/components/Inputbar/hooks/useInputbarDictation.ts @@ -1,4 +1,10 @@ -import { useCallback, useEffect, useRef, useState, type RefObject } from "react"; +import { + useCallback, + useEffect, + useRef, + useState, + type RefObject, +} from "react"; import { toast } from "sonner"; import { cancelRecording, @@ -32,7 +38,9 @@ function insertTranscriptAtCursor( if (!textarea) { return { nextText: currentText ? `${currentText}\n${transcript}` : transcript, - cursor: currentText ? currentText.length + transcript.length + 1 : transcript.length, + cursor: currentText + ? currentText.length + transcript.length + 1 + : transcript.length, }; } @@ -40,10 +48,8 @@ function insertTranscriptAtCursor( const selectionEnd = textarea.selectionEnd ?? currentText.length; const before = currentText.slice(0, selectionStart); const after = currentText.slice(selectionEnd); - const prefix = - before.length > 0 && !/[\s\n]$/.test(before) ? "\n" : ""; - const suffix = - after.length > 0 && !/^[\s\n]/.test(after) ? "\n" : ""; + const prefix = before.length > 0 && !/[\s\n]$/.test(before) ? "\n" : ""; + const suffix = after.length > 0 && !/^[\s\n]/.test(after) ? "\n" : ""; const inserted = `${prefix}${transcript}${suffix}`; const nextText = `${before}${inserted}${after}`; const cursor = before.length + inserted.length; @@ -85,13 +91,16 @@ export function useInputbarDictation({ }; }, []); - const focusTextarea = useCallback((cursor: number) => { - window.requestAnimationFrame(() => { - const textarea = textareaRef.current; - textarea?.focus(); - textarea?.setSelectionRange(cursor, cursor); - }); - }, [textareaRef]); + const focusTextarea = useCallback( + (cursor: number) => { + window.requestAnimationFrame(() => { + const textarea = textareaRef.current; + textarea?.focus(); + textarea?.setSelectionRange(cursor, cursor); + }); + }, + [textareaRef], + ); const insertTranscript = useCallback( (transcript: string) => { diff --git a/src/components/agent/chat/components/Inputbar/hooks/useInputbarToolState.ts b/src/components/agent/chat/components/Inputbar/hooks/useInputbarToolState.ts index bc0c36a81..331d1af4b 100644 --- a/src/components/agent/chat/components/Inputbar/hooks/useInputbarToolState.ts +++ b/src/components/agent/chat/components/Inputbar/hooks/useInputbarToolState.ts @@ -29,8 +29,7 @@ export function useInputbarToolState({ ); const [isFullscreen, setIsFullscreen] = useState(false); - const webSearchEnabled = - toolStates?.webSearch ?? localToolStates.webSearch; + const webSearchEnabled = toolStates?.webSearch ?? localToolStates.webSearch; const thinkingEnabled = toolStates?.thinking ?? localToolStates.thinking; const subagentEnabled = toolStates?.subagent ?? localToolStates.subagent; @@ -127,12 +126,7 @@ export function useInputbarToolState({ }); toast.info(`多代理${enabled ? "偏好已开启" : "偏好已关闭"}`); }, - [ - subagentEnabled, - thinkingEnabled, - updateToolStates, - webSearchEnabled, - ], + [subagentEnabled, thinkingEnabled, updateToolStates, webSearchEnabled], ); return { diff --git a/src/components/agent/chat/components/Inputbar/styles.ts b/src/components/agent/chat/components/Inputbar/styles.ts index e407e62e0..52d593b9c 100644 --- a/src/components/agent/chat/components/Inputbar/styles.ts +++ b/src/components/agent/chat/components/Inputbar/styles.ts @@ -39,10 +39,10 @@ export const Container = styled.div` export const InputBarContainer = styled.div` position: relative; - border: 1px solid #dcfce7; + border: 1px solid rgba(110, 231, 183, 0.55); border-radius: 22px; padding: 10px 12px 10px 10px; - background: linear-gradient(180deg, #fdfffb 0%, #f4fbf5 100%); + background: linear-gradient(180deg, #fdfffb 0%, #f6fbf7 100%); box-shadow: 0 10px 28px rgba(22, 101, 52, 0.04), inset 0 1px 0 rgba(255, 255, 255, 0.9); @@ -52,10 +52,10 @@ export const InputBarContainer = styled.div` background 0.2s ease; &:focus-within { - border-color: #bbf7d0; - background: linear-gradient(180deg, #ffffff 0%, #f0fdf4 100%); + border-color: rgba(74, 222, 128, 0.65); + background: linear-gradient(180deg, #ffffff 0%, #f4fbf5 100%); box-shadow: - 0 0 0 3px rgba(134, 239, 172, 0.35), + 0 0 0 3px rgba(74, 222, 128, 0.22), 0 14px 32px rgba(22, 101, 52, 0.06), inset 0 1px 0 rgba(255, 255, 255, 0.92); } @@ -68,8 +68,8 @@ export const InputBarContainer = styled.div` &.floating-composer { border-radius: 20px; padding: 16px 18px 14px 16px; - background: linear-gradient(180deg, #fcfffc 0%, #f4fcf5 100%); - border-color: #d1fae5; + background: linear-gradient(180deg, #fcfffc 0%, #f6fbf7 100%); + border-color: rgba(110, 231, 183, 0.6); box-shadow: 0 16px 32px rgba(22, 101, 52, 0.05), inset 0 1px 0 rgba(255, 255, 255, 0.85); @@ -81,13 +81,12 @@ export const InputBarContainer = styled.div` } &.floating-composer:focus-within { - background: linear-gradient(180deg, #ffffff 0%, #ecfdf5 100%); - border-color: #a7f3d0; + background: linear-gradient(180deg, #ffffff 0%, #f0fdf4 100%); + border-color: rgba(74, 222, 128, 0.7); box-shadow: - 0 0 0 3px rgba(110, 231, 183, 0.38), + 0 0 0 3px rgba(74, 222, 128, 0.25), 0 18px 34px rgba(22, 101, 52, 0.08), inset 0 1px 0 rgba(255, 255, 255, 0.88); - } `; @@ -240,10 +239,10 @@ export const MetaSelectWrap = styled.label` } &:focus-within { - border-color: rgba(125, 167, 255, 0.42); + border-color: rgba(167, 243, 208, 0.42); background: rgba(255, 255, 255, 0.98); box-shadow: - 0 0 0 3px rgba(191, 219, 254, 0.28), + 0 0 0 3px rgba(167, 243, 208, 0.28), inset 0 1px 0 rgba(255, 255, 255, 0.92); color: hsl(var(--foreground)); } @@ -299,11 +298,9 @@ export const MetaToggleButton = styled.button<{ border-radius: 999px; border: 1px solid ${({ $checked }) => - $checked - ? "rgba(125, 167, 255, 0.4)" - : "rgba(148, 163, 184, 0.24)"}; + $checked ? "rgba(167, 243, 208, 0.4)" : "rgba(148, 163, 184, 0.24)"}; background: ${({ $checked }) => - $checked ? "rgba(240, 246, 255, 0.98)" : "rgba(255, 255, 255, 0.9)"}; + $checked ? "rgba(240, 253, 244, 0.98)" : "rgba(255, 255, 255, 0.9)"}; box-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.82); color: ${({ $checked }) => $checked ? "hsl(var(--foreground))" : "hsl(var(--muted-foreground))"}; @@ -317,20 +314,18 @@ export const MetaToggleButton = styled.button<{ &:hover { border-color: ${({ $checked }) => - $checked - ? "rgba(125, 167, 255, 0.5)" - : "rgba(148, 163, 184, 0.34)"}; + $checked ? "rgba(167, 243, 208, 0.5)" : "rgba(148, 163, 184, 0.34)"}; background: ${({ $checked }) => - $checked ? "rgba(244, 248, 255, 1)" : "rgba(255, 255, 255, 0.98)"}; + $checked ? "rgba(220, 252, 231, 1)" : "rgba(255, 255, 255, 0.98)"}; color: hsl(var(--foreground)); transform: translateY(-1px); } &:focus-visible { outline: none; - border-color: rgba(125, 167, 255, 0.44); + border-color: rgba(167, 243, 208, 0.44); box-shadow: - 0 0 0 3px rgba(191, 219, 254, 0.28), + 0 0 0 3px rgba(167, 243, 208, 0.28), inset 0 1px 0 rgba(255, 255, 255, 0.92); } @@ -352,11 +347,9 @@ export const MetaToggleCheck = styled.span<{ border-radius: 4px; border: 1px solid ${({ $checked }) => - $checked - ? "rgba(59, 130, 246, 0.45)" - : "rgba(148, 163, 184, 0.42)"}; + $checked ? "rgba(34, 197, 94, 0.45)" : "rgba(148, 163, 184, 0.42)"}; background: ${({ $checked }) => - $checked ? "rgba(219, 234, 254, 0.9)" : "rgba(255, 255, 255, 0.96)"}; + $checked ? "rgba(220, 252, 231, 0.9)" : "rgba(255, 255, 255, 0.96)"}; box-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.88); flex-shrink: 0; @@ -429,9 +422,9 @@ export const ToolButton = styled.button` } &.active { - color: #0f172a; - border-color: rgba(125, 167, 255, 0.44); - background: rgba(224, 236, 255, 0.82); + color: #14532d; + border-color: rgba(167, 243, 208, 0.44); + background: rgba(220, 252, 231, 0.82); } span { @@ -464,13 +457,13 @@ export const InputIconButton = styled.button<{ : "rgba(148, 163, 184, 0.28)"}; background: ${({ $primary, $destructive }) => $primary - ? "#0f172a" + ? "#1aa34e" : $destructive ? "rgba(255, 226, 234, 0.92)" : "rgba(255, 255, 255, 0.9)"}; color: ${({ $primary, $destructive }) => $primary - ? "#f8fafc" + ? "#f0fdf4" : $destructive ? "#be123c" : "hsl(var(--muted-foreground))"}; @@ -485,7 +478,7 @@ export const InputIconButton = styled.button<{ transform: translateY(-1px); background: ${({ $primary, $destructive }) => $primary - ? "#111c31" + ? "#168840" : $destructive ? "rgba(255, 221, 229, 1)" : "rgba(255, 255, 255, 1)"}; @@ -498,21 +491,21 @@ export const InputIconButton = styled.button<{ } &.is-active { - border-color: rgba(125, 167, 255, 0.44); - background: rgba(224, 236, 255, 0.82); - color: #0f172a; + border-color: rgba(167, 243, 208, 0.44); + background: rgba(220, 252, 231, 0.82); + color: #14532d; } &.is-recording { border-color: rgba(120, 235, 190, 0.6); background: rgba(120, 235, 190, 0.12); - color: #0f172a; + color: #14532d; } &.is-processing { - border-color: rgba(160, 200, 255, 0.6); - background: rgba(160, 200, 255, 0.12); - color: #0f172a; + border-color: rgba(167, 243, 208, 0.6); + background: rgba(167, 243, 208, 0.12); + color: #14532d; } &:disabled { diff --git a/src/components/agent/chat/components/MarkdownRenderer.test.tsx b/src/components/agent/chat/components/MarkdownRenderer.test.tsx index cced800ad..da9f5ca70 100644 --- a/src/components/agent/chat/components/MarkdownRenderer.test.tsx +++ b/src/components/agent/chat/components/MarkdownRenderer.test.tsx @@ -145,8 +145,8 @@ function renderHarness( { isStreaming: nextIsStreaming = isStreaming, collapseCodeBlocks: nextCollapseCodeBlocks = collapseCodeBlocks, - shouldCollapseCodeBlock: nextShouldCollapseCodeBlock = - shouldCollapseCodeBlock, + shouldCollapseCodeBlock: + nextShouldCollapseCodeBlock = shouldCollapseCodeBlock, showBlockActions: nextShowBlockActions = showBlockActions, onQuoteContent: nextOnQuoteContent = onQuoteContent, }: RenderOptions = {}, @@ -409,11 +409,7 @@ describe("MarkdownRenderer", () => { }); it("代码块高亮应关闭 textShadow 与字体连字,避免中英混排发虚", () => { - const content = [ - "```typescript", - "const answer = 42;", - "```", - ].join("\n"); + const content = ["```typescript", "const answer = 42;", "```"].join("\n"); const container = render(content); const syntaxHighlighter = container.querySelector( diff --git a/src/components/agent/chat/components/MarkdownRenderer.tsx b/src/components/agent/chat/components/MarkdownRenderer.tsx index f8db72f79..0deeef0fe 100644 --- a/src/components/agent/chat/components/MarkdownRenderer.tsx +++ b/src/components/agent/chat/components/MarkdownRenderer.tsx @@ -179,8 +179,8 @@ const MarkdownContainer = styled.div` th, td { - border-right: 1px solid hsl(var(--border)); - border-bottom: 1px solid hsl(var(--border)); + border-right: 1px solid rgba(167, 243, 208, 0.42); + border-bottom: 1px solid rgba(167, 243, 208, 0.42); padding: 0.45rem 0.65rem; vertical-align: top; text-align: left; @@ -190,10 +190,10 @@ const MarkdownContainer = styled.div` font-weight: 600; background: linear-gradient( 180deg, - hsl(var(--muted) / 0.78) 0%, - hsl(var(--muted) / 0.96) 100% + rgba(220, 252, 231, 0.45) 0%, + rgba(220, 252, 231, 0.8) 100% ); - color: hsl(var(--foreground)); + color: #14532d; white-space: nowrap; } @@ -206,7 +206,7 @@ const MarkdownContainer = styled.div` } tbody tr:nth-child(even) td { - background: hsl(var(--muted) / 0.22); + background: rgba(220, 252, 231, 0.15); } a { @@ -245,14 +245,14 @@ const MarkdownDivider = styled.hr` const MarkdownQuoteCard = styled.blockquote` margin: 0 0 0.95em; padding: 0; - border: 1px solid hsl(var(--border)); + border: 1px solid rgba(167, 243, 208, 0.5); border-radius: 20px; background: linear-gradient( 180deg, hsl(var(--background)) 0%, - hsl(var(--muted) / 0.58) 100% + rgba(220, 252, 231, 0.3) 100% ); - box-shadow: 0 14px 34px -30px rgba(15, 23, 42, 0.25); + box-shadow: 0 14px 34px -30px rgba(15, 23, 42, 0.18); overflow: hidden; `; @@ -432,7 +432,7 @@ const MarkdownBlockActionButton = styled.button` const MarkdownTableScroll = styled.div` margin: 0 0 0.82em; overflow-x: auto; - border: 1px solid hsl(var(--border)); + border: 1px solid rgba(167, 243, 208, 0.6); border-radius: 14px; background: hsl(var(--background)); box-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.7); @@ -515,11 +515,16 @@ function resolveCodePresentationMode( .split("\n") .map((line) => line.trim()) .filter(Boolean); - const arrowRows = lines.filter((line) => FLOW_ARROW_ONLY_PATTERN.test(line)).length; + const arrowRows = lines.filter((line) => + FLOW_ARROW_ONLY_PATTERN.test(line), + ).length; const bulletRows = lines.filter((line) => /^[-*]\s+/.test(line)).length; const hasCodeSignals = CODE_SIGNAL_PATTERN.test(trimmed); - if ((PLAIN_TEXT_LANGUAGES.has(normalizedLanguage) || !hasCodeSignals) && arrowRows >= 2) { + if ( + (PLAIN_TEXT_LANGUAGES.has(normalizedLanguage) || !hasCodeSignals) && + arrowRows >= 2 + ) { return "flow"; } @@ -583,23 +588,34 @@ export const MarkdownRenderer: React.FC = memo( }; }, []); - const handleCopy = React.useCallback(async (copyKey: string, value: string) => { - try { - await navigator.clipboard.writeText(value); - setCopied(copyKey); - if (copyTimeoutRef.current !== null) { - window.clearTimeout(copyTimeoutRef.current); + const handleCopy = React.useCallback( + async (copyKey: string, value: string) => { + try { + await navigator.clipboard.writeText(value); + setCopied(copyKey); + if (copyTimeoutRef.current !== null) { + window.clearTimeout(copyTimeoutRef.current); + } + copyTimeoutRef.current = window.setTimeout( + () => setCopied(null), + 1200, + ); + } catch { + // 剪贴板在受限上下文里可能不可用,这里保持静默降级。 } - copyTimeoutRef.current = window.setTimeout(() => setCopied(null), 1200); - } catch { - // 剪贴板在受限上下文里可能不可用,这里保持静默降级。 - } - }, []); + }, + [], + ); const getSelectedMarkdownText = React.useCallback(() => { const block = blockRef.current; const selection = window.getSelection(); - if (!block || !selection || selection.rangeCount === 0 || selection.isCollapsed) { + if ( + !block || + !selection || + selection.rangeCount === 0 || + selection.isCollapsed + ) { return null; } @@ -647,14 +663,18 @@ export const MarkdownRenderer: React.FC = memo( const selectedText = getSelectedMarkdownText() ?? selectionSnapshotRef.current ?? undefined; selectionSnapshotRef.current = null; - onQuoteContent(selectedText?.trim().length ? selectedText : normalizedContent); + onQuoteContent( + selectedText?.trim().length ? selectedText : normalizedContent, + ); }, [getSelectedMarkdownText, normalizedContent, onQuoteContent]); const handleCopyContent = React.useCallback(async () => { const selectedText = getSelectedMarkdownText() ?? selectionSnapshotRef.current ?? undefined; selectionSnapshotRef.current = null; - const copyValue = selectedText?.trim().length ? selectedText : normalizedContent; + const copyValue = selectedText?.trim().length + ? selectedText + : normalizedContent; if (!copyValue) { return; } @@ -880,11 +900,7 @@ export const MarkdownRenderer: React.FC = memo( aria-label="复制内容区块" title={isContentCopied ? "已复制" : "复制内容区块"} > - {isContentCopied ? ( - - ) : ( - - )} + {isContentCopied ? : } ) : null} @@ -1042,7 +1058,9 @@ export const MarkdownRenderer: React.FC = memo( }, code({ inline, className, children, ...props }: any) { const content = String( - Array.isArray(children) ? children.join("") : children || "", + Array.isArray(children) + ? children.join("") + : children || "", ); const isInlineCode = typeof inline === "boolean" diff --git a/src/components/agent/chat/components/MessageList.test.tsx b/src/components/agent/chat/components/MessageList.test.tsx index 4fb9644d9..71ba18bca 100644 --- a/src/components/agent/chat/components/MessageList.test.tsx +++ b/src/components/agent/chat/components/MessageList.test.tsx @@ -1630,7 +1630,7 @@ describe("MessageList", () => { id: "artifact-hidden-task-json", type: "code", title: "task-image-1.json", - content: "{\"status\":\"running\"}", + content: '{"status":"running"}', status: "complete", meta: { filePath: ".lime/tasks/image_generate/task-image-1.json", diff --git a/src/components/agent/chat/components/MessageList.tsx b/src/components/agent/chat/components/MessageList.tsx index 94d8a3366..7593ea23e 100644 --- a/src/components/agent/chat/components/MessageList.tsx +++ b/src/components/agent/chat/components/MessageList.tsx @@ -100,7 +100,10 @@ interface MessageListProps { /** Artifact 点击回调 */ onArtifactClick?: (artifact: Artifact) => void; /** 打开消息结果预览 */ - onOpenMessagePreview?: (target: MessagePreviewTarget, message: Message) => void; + onOpenMessagePreview?: ( + target: MessagePreviewTarget, + message: Message, + ) => void; /** 打开子代理会话 */ onOpenSubagentSession?: (sessionId: string) => void; /** 权限确认响应回调 */ @@ -697,7 +700,9 @@ const MessageListInner: React.FC = ({ const visibleArtifacts = artifacts?.filter( (artifact) => - !isHiddenInternalArtifactPath(resolveArtifactProtocolFilePath(artifact)), + !isHiddenInternalArtifactPath( + resolveArtifactProtocolFilePath(artifact), + ), ) || []; if (visibleArtifacts.length === 0) { return null; @@ -787,55 +792,53 @@ const MessageListInner: React.FC = ({

+ ) : isTaskCenterEmptyState ? ( +
+
+
+
+ Lime +
+ + 任务中心 + +
+ +
+

+ 回到进行中的任务和最近工作现场 +

+

+ 这里会承接最近会话、进行中的任务和刚恢复的工作内容。要开始一个全新目标,随时回到“新建任务”。 +

+
+ +
+ + 左侧会继续显示最近任务 + + + 恢复中的会话会自动回到这里 + +
+
+
) : ( - isTaskCenterEmptyState ? ( -
-
-
-
- Lime -
- - 任务中心 - -
- -
-

- 回到进行中的任务和最近工作现场 -

-

- 这里会承接最近会话、进行中的任务和刚恢复的工作内容。要开始一个全新目标,随时回到“新建任务”。 -

-
- -
- - 左侧会继续显示最近任务 - - - 恢复中的会话会自动回到这里 - -
-
-
- ) : ( -
- Lime -

开始一段新的对话吧

-
- ) +
+ Lime +

开始一段新的对话吧

+
))} {messageGroups.map((group, groupIndex) => { diff --git a/src/components/agent/chat/components/RuntimeReviewDecisionDialog.tsx b/src/components/agent/chat/components/RuntimeReviewDecisionDialog.tsx index f1237d5d7..0d5f08f9e 100644 --- a/src/components/agent/chat/components/RuntimeReviewDecisionDialog.tsx +++ b/src/components/agent/chat/components/RuntimeReviewDecisionDialog.tsx @@ -144,14 +144,12 @@ export function RuntimeReviewDecisionDialog({ } }, [open, template]); - const statusOptions = - template?.decision_status_options.length - ? template.decision_status_options - : DEFAULT_STATUS_OPTIONS; - const riskLevelOptions = - template?.risk_level_options.length - ? template.risk_level_options - : DEFAULT_RISK_LEVEL_OPTIONS; + const statusOptions = template?.decision_status_options.length + ? template.decision_status_options + : DEFAULT_STATUS_OPTIONS; + const riskLevelOptions = template?.risk_level_options.length + ? template.risk_level_options + : DEFAULT_RISK_LEVEL_OPTIONS; const handleSave = async () => { if (!template || !formState) { diff --git a/src/components/agent/chat/components/SearchResultPreviewList.tsx b/src/components/agent/chat/components/SearchResultPreviewList.tsx index fd4be8ae4..32949fbd8 100644 --- a/src/components/agent/chat/components/SearchResultPreviewList.tsx +++ b/src/components/agent/chat/components/SearchResultPreviewList.tsx @@ -6,7 +6,11 @@ import { Globe, Search, } from "lucide-react"; -import { Popover, PopoverContent, PopoverTrigger } from "@/components/ui/popover"; +import { + Popover, + PopoverContent, + PopoverTrigger, +} from "@/components/ui/popover"; import { cn } from "@/lib/utils"; import type { SearchResultPreviewItem } from "../utils/searchResultPreview"; @@ -226,7 +230,10 @@ export function SearchResultPreviewList({ aria-label={expanded ? "收起搜索结果" : "展开搜索结果"} > {expanded ? "收起结果" : `展开其余 ${hiddenCount} 条结果`} diff --git a/src/components/agent/chat/components/TeamMemoryShadowCard.test.tsx b/src/components/agent/chat/components/TeamMemoryShadowCard.test.tsx index a60254409..258714960 100644 --- a/src/components/agent/chat/components/TeamMemoryShadowCard.test.tsx +++ b/src/components/agent/chat/components/TeamMemoryShadowCard.test.tsx @@ -31,7 +31,9 @@ afterEach(() => { } }); -function renderCard(snapshot?: Parameters[0]["snapshot"]) { +function renderCard( + snapshot?: Parameters[0]["snapshot"], +) { const container = document.createElement("div"); document.body.appendChild(container); const root = createRoot(container); @@ -79,12 +81,16 @@ describe("TeamMemoryShadowCard", () => { container.querySelector('[data-testid="team-memory-shadow-card"]'), ).not.toBeNull(); expect(container.textContent).toContain("协作记忆影子"); - expect(container.textContent).toContain("当前仓库已缓存 3 条 Team 续接上下文"); + expect(container.textContent).toContain( + "当前仓库已缓存 3 条 Team 续接上下文", + ); expect(container.textContent).toContain("/workspace/lime"); expect(container.textContent).toContain("当前 Team"); expect(container.textContent).toContain("子代理概览"); expect(container.textContent).toContain("父会话上下文"); - expect(container.textContent).toContain("研究代理 [running] explorer · 梳理主线风险"); + expect(container.textContent).toContain( + "研究代理 [running] explorer · 梳理主线风险", + ); expect(container.textContent).toContain("当前任务:汇总结论"); }); }); diff --git a/src/components/agent/chat/components/TeamMemoryShadowCard.tsx b/src/components/agent/chat/components/TeamMemoryShadowCard.tsx index a42688e2b..5c0a53e22 100644 --- a/src/components/agent/chat/components/TeamMemoryShadowCard.tsx +++ b/src/components/agent/chat/components/TeamMemoryShadowCard.tsx @@ -89,7 +89,8 @@ export function TeamMemoryShadowCard({ 当前仓库已缓存 {entries.length} 条 Team 续接上下文

- 仅保存在当前仓库本地作用域,用来续接 Team 选择、子代理概览和父会话上下文。 + 仅保存在当前仓库本地作用域,用来续接 Team + 选择、子代理概览和父会话上下文。

diff --git a/src/components/agent/chat/components/TeamWorkbenchSummaryPanel.tsx b/src/components/agent/chat/components/TeamWorkbenchSummaryPanel.tsx index 38bf6fc5f..96adaf92d 100644 --- a/src/components/agent/chat/components/TeamWorkbenchSummaryPanel.tsx +++ b/src/components/agent/chat/components/TeamWorkbenchSummaryPanel.tsx @@ -115,27 +115,26 @@ export function TeamWorkbenchSummaryPanel({ .slice(0, 4); const displayRoleCount = hasRealTeamGraph ? executionSummary.totalSessionCount - : dispatchPreviewState?.members.length ?? 0; - const roleCards = - dispatchPreviewState?.members.length - ? dispatchPreviewState.members.map((member) => ({ - id: member.id, - label: member.label, - roleKey: member.roleKey, - profileId: member.profileId, - summary: member.summary, - skillIds: member.skillIds, - statusMeta: resolveRuntimeMemberStatusMeta(member.status), - })) - : (selectedTeamRoles ?? []).map((role) => ({ - id: role.id, - label: role.label, - roleKey: role.roleKey, - profileId: role.profileId, - summary: role.summary, - skillIds: role.skillIds ?? [], - statusMeta: null, - })); + : (dispatchPreviewState?.members.length ?? 0); + const roleCards = dispatchPreviewState?.members.length + ? dispatchPreviewState.members.map((member) => ({ + id: member.id, + label: member.label, + roleKey: member.roleKey, + profileId: member.profileId, + summary: member.summary, + skillIds: member.skillIds, + statusMeta: resolveRuntimeMemberStatusMeta(member.status), + })) + : (selectedTeamRoles ?? []).map((role) => ({ + id: role.id, + label: role.label, + roleKey: role.roleKey, + profileId: role.profileId, + summary: role.summary, + skillIds: role.skillIds ?? [], + statusMeta: null, + })); const summaryCards = [ { label: "活跃 Agent", @@ -159,7 +158,9 @@ export function TeamWorkbenchSummaryPanel({ ? "当前成员" : "总会话", value: String( - hasRealTeamGraph ? executionSummary.totalSessionCount : displayRoleCount, + hasRealTeamGraph + ? executionSummary.totalSessionCount + : displayRoleCount, ), hint: hasRealTeamGraph ? "已进入团队图谱" @@ -201,7 +202,7 @@ export function TeamWorkbenchSummaryPanel({ ? dispatchPreviewState.errorMessage?.trim() || "这次 Team 准备失败,可继续在当前对话中推进。" : runtimeSummaryText || - "这里展示团队总览与运行密度;主对话只保留调度记录,角色执行正文在左侧 Team 画布查看。"} + "这里展示团队总览与运行密度;主对话只保留调度记录,角色执行正文在左侧 Team 画布查看。"}

{!hasRealTeamGraph && dispatchPreviewState ? (
- 尚未出现真实团队成员。开始分派成员后,左侧 Team 画布会自动切换为实时协作视图。 + 尚未出现真实团队成员。开始分派成员后,左侧 Team + 画布会自动切换为实时协作视图。
) : null} diff --git a/src/components/agent/chat/components/TeamWorkspaceBoard.test.tsx b/src/components/agent/chat/components/TeamWorkspaceBoard.test.tsx index a26be9ece..14a69faa7 100644 --- a/src/components/agent/chat/components/TeamWorkspaceBoard.test.tsx +++ b/src/components/agent/chat/components/TeamWorkspaceBoard.test.tsx @@ -10,10 +10,9 @@ const { mockGetAgentRuntimeSession } = vi.hoisted(() => ({ })); vi.mock("@/lib/api/agentRuntime", async () => { - const actual = - await vi.importActual( - "@/lib/api/agentRuntime", - ); + const actual = await vi.importActual( + "@/lib/api/agentRuntime", + ); return { ...actual, getAgentRuntimeSession: mockGetAgentRuntimeSession, @@ -34,12 +33,7 @@ vi.mock("@/components/ui/button", () => ({ type?: "button" | "submit" | "reset"; [key: string]: unknown; }) => ( - ), @@ -413,7 +407,9 @@ describe("TeamWorkspaceBoard", () => { expect(container.textContent).toContain("仓库探索"); expect(container.textContent).toContain("最近进展 处理中"); expect( - container.querySelector('[data-testid="team-workspace-member-detail-child-1"]'), + container.querySelector( + '[data-testid="team-workspace-member-detail-child-1"]', + ), ).toBeFalsy(); expect( container @@ -453,7 +449,9 @@ describe("TeamWorkspaceBoard", () => { await expandLane(container, "child-1"); expect( - container.querySelector('[data-testid="team-workspace-member-detail-child-1"]'), + container.querySelector( + '[data-testid="team-workspace-member-detail-child-1"]', + ), ).toBeTruthy(); expect( container @@ -651,7 +649,9 @@ describe("TeamWorkspaceBoard", () => { expect(container.textContent).toContain("检索代理"); expect(container.textContent).toContain("等待中 1"); expect( - container.querySelector('[data-testid="team-workspace-member-detail-child-current"]'), + container.querySelector( + '[data-testid="team-workspace-member-detail-child-current"]', + ), ).toBeFalsy(); expect( container.querySelector('[data-testid="team-workspace-canvas-toolbar"]') @@ -765,10 +765,14 @@ describe("TeamWorkspaceBoard", () => { expect(boardBody?.className).toContain("overflow-y-auto"); expect(railList?.getAttribute("data-layout-kind")).toBe("free-canvas"); expect( - container.querySelector('[data-testid="team-workspace-inspector-overlay"]'), + container.querySelector( + '[data-testid="team-workspace-inspector-overlay"]', + ), ).toBeNull(); expect( - container.querySelector('[data-testid="team-workspace-member-detail-child-1"]'), + container.querySelector( + '[data-testid="team-workspace-member-detail-child-1"]', + ), ).toBeNull(); expect( container @@ -812,45 +816,63 @@ describe("TeamWorkspaceBoard", () => { }); expect( - container.querySelector('[data-testid="team-workspace-member-detail-child-collapse-1"]'), + container.querySelector( + '[data-testid="team-workspace-member-detail-child-collapse-1"]', + ), ).toBeFalsy(); expect( - container.querySelector('[data-testid="team-workspace-member-detail-child-collapse-2"]'), + container.querySelector( + '[data-testid="team-workspace-member-detail-child-collapse-2"]', + ), ).toBeFalsy(); expect( container - .querySelector('[data-testid="team-workspace-member-lane-child-collapse-1"]') + .querySelector( + '[data-testid="team-workspace-member-lane-child-collapse-1"]', + ) ?.getAttribute("data-expanded"), ).toBe("false"); expect( container - .querySelector('[data-testid="team-workspace-member-lane-child-collapse-2"]') + .querySelector( + '[data-testid="team-workspace-member-lane-child-collapse-2"]', + ) ?.getAttribute("data-expanded"), ).toBe("false"); await expandLane(container, "child-collapse-1"); expect( - container.querySelector('[data-testid="team-workspace-member-detail-child-collapse-1"]'), + container.querySelector( + '[data-testid="team-workspace-member-detail-child-collapse-1"]', + ), ).toBeTruthy(); expect( container - .querySelector('[data-testid="team-workspace-member-lane-child-collapse-1"]') + .querySelector( + '[data-testid="team-workspace-member-lane-child-collapse-1"]', + ) ?.getAttribute("data-expanded"), ).toBe("true"); expect( container - .querySelector('[data-testid="team-workspace-member-lane-child-collapse-2"]') + .querySelector( + '[data-testid="team-workspace-member-lane-child-collapse-2"]', + ) ?.getAttribute("data-expanded"), ).toBe("false"); await expandLane(container, "child-collapse-2"); expect( - container.querySelector('[data-testid="team-workspace-member-detail-child-collapse-1"]'), + container.querySelector( + '[data-testid="team-workspace-member-detail-child-collapse-1"]', + ), ).toBeFalsy(); expect( - container.querySelector('[data-testid="team-workspace-member-detail-child-collapse-2"]'), + container.querySelector( + '[data-testid="team-workspace-member-detail-child-collapse-2"]', + ), ).toBeTruthy(); }); @@ -889,8 +911,7 @@ describe("TeamWorkspaceBoard", () => { title: "状态切换", detail: "收到 team 状态事件,已切换为运行中。", statusLabel: "运行中", - badgeClassName: - "border border-sky-200 bg-sky-50 text-sky-700", + badgeClassName: "border border-sky-200 bg-sky-50 text-sky-700", }, ], }, @@ -910,7 +931,9 @@ describe("TeamWorkspaceBoard", () => { ).toContain("处理中"); expect(container.textContent).toContain("最近进展 处理中"); expect( - container.querySelector('[data-testid="team-workspace-member-detail-child-live-1"]'), + container.querySelector( + '[data-testid="team-workspace-member-detail-child-live-1"]', + ), ).toBeFalsy(); await expandLane(container, "child-live-1"); @@ -1428,22 +1451,23 @@ describe("TeamWorkspaceBoard", () => { }); it("同一会话重新挂载后,应恢复上次保存的 lane 布局", async () => { - const boardProps: Partial> = { - currentSessionId: "parent-persist-1", - childSubagentSessions: [ - { - id: "child-persist-1", - name: "持久化代理", - created_at: 1_710_000_000, - updated_at: 1_710_000_100, - session_type: "sub_agent", - runtime_status: "running", - latest_turn_status: "running", - task_summary: "验证会话级画布持久化", - role_hint: "reviewer", - }, - ], - }; + const boardProps: Partial> = + { + currentSessionId: "parent-persist-1", + childSubagentSessions: [ + { + id: "child-persist-1", + name: "持久化代理", + created_at: 1_710_000_000, + updated_at: 1_710_000_100, + session_type: "sub_agent", + runtime_status: "running", + latest_turn_status: "running", + task_summary: "验证会话级画布持久化", + role_hint: "reviewer", + }, + ], + }; const firstContainer = await renderBoard(boardProps); const firstHeader = firstContainer.querySelector( @@ -1490,7 +1514,9 @@ describe("TeamWorkspaceBoard", () => { await expandLane(container, "child-close-1"); expect( - container.querySelector('[data-testid="team-workspace-member-detail-child-close-1"]'), + container.querySelector( + '[data-testid="team-workspace-member-detail-child-close-1"]', + ), ).toBeTruthy(); const closeButton = Array.from(container.querySelectorAll("button")).find( @@ -1532,7 +1558,9 @@ describe("TeamWorkspaceBoard", () => { ); expect(resumeButton).toBeTruthy(); expect( - container.querySelector('[data-testid="team-workspace-member-detail-child-resume-1"]'), + container.querySelector( + '[data-testid="team-workspace-member-detail-child-resume-1"]', + ), ).toBeTruthy(); await act(async () => { @@ -1768,9 +1796,7 @@ describe("TeamWorkspaceBoard", () => { expect(operations).toBeTruthy(); expect(operations?.textContent).toContain("暂停处理"); - expect(operations?.textContent).toContain( - "刚才已暂停 2 位协作成员的处理", - ); + expect(operations?.textContent).toContain("刚才已暂停 2 位协作成员的处理"); }); it("点击 Team 轨迹项时应切换焦点到对应 agent", async () => { @@ -1973,7 +1999,9 @@ describe("TeamWorkspaceBoard", () => { }); await act(async () => { - interruptButton?.dispatchEvent(new MouseEvent("click", { bubbles: true })); + interruptButton?.dispatchEvent( + new MouseEvent("click", { bubbles: true }), + ); await Promise.resolve(); }); @@ -2081,7 +2109,9 @@ describe("TeamWorkspaceBoard", () => { }); expect( - container.querySelector('[data-testid="team-workspace-runtime-formation"]'), + container.querySelector( + '[data-testid="team-workspace-runtime-formation"]', + ), ).toBeTruthy(); expect( container.querySelector('[data-testid="team-workspace-runtime-members"]'), diff --git a/src/components/agent/chat/components/TeamWorkspaceBoard.tsx b/src/components/agent/chat/components/TeamWorkspaceBoard.tsx index 5ac667e70..7025331e0 100644 --- a/src/components/agent/chat/components/TeamWorkspaceBoard.tsx +++ b/src/components/agent/chat/components/TeamWorkspaceBoard.tsx @@ -106,9 +106,7 @@ interface TeamWorkspaceBoardProps { sessionIds: string[], timeoutMs?: number, ) => void | Promise; - onCloseCompletedTeamSessions?: ( - sessionIds: string[], - ) => void | Promise; + onCloseCompletedTeamSessions?: (sessionIds: string[]) => void | Promise; onCloseSubagentSession?: (sessionId: string) => void | Promise; onResumeSubagentSession?: (sessionId: string) => void | Promise; onOpenSubagentSession?: (sessionId: string) => void | Promise; @@ -279,8 +277,8 @@ function canStartCanvasPanGesture( } return ( - target.closest('[data-team-workspace-canvas-pan-surface="true"]') !== null || - target === currentTarget + target.closest('[data-team-workspace-canvas-pan-surface="true"]') !== + null || target === currentTarget ); } @@ -349,7 +347,8 @@ function resolveActivityEntryStatusMeta( case "completed": return { label: "完成", - badgeClassName: "border border-emerald-200 bg-emerald-50 text-emerald-700", + badgeClassName: + "border border-emerald-200 bg-emerald-50 text-emerald-700", }; default: return { @@ -483,7 +482,9 @@ function isEditableKeyboardTarget(target: EventTarget | null): boolean { ); } -function buildRuntimeDetailSummary(session?: TeamSessionCard | null): string | null { +function buildRuntimeDetailSummary( + session?: TeamSessionCard | null, +): string | null { if (!session) { return null; } @@ -500,9 +501,14 @@ function buildRuntimeDetailSummary(session?: TeamSessionCard | null): string | n session.teamActiveCount !== undefined && session.teamParallelBudget !== undefined ) { - parts.push(`处理中 ${session.teamActiveCount}/${session.teamParallelBudget}`); + parts.push( + `处理中 ${session.teamActiveCount}/${session.teamParallelBudget}`, + ); } - if (session.providerParallelBudget === 1 && session.providerConcurrencyGroup) { + if ( + session.providerParallelBudget === 1 && + session.providerConcurrencyGroup + ) { parts.push(resolveTeamWorkspaceStableProcessingLabel()); } @@ -512,8 +518,8 @@ function buildRuntimeDetailSummary(session?: TeamSessionCard | null): string | n function isWaitableTeamSession(session?: TeamSessionCard | null) { return Boolean( session && - session.sessionType !== "user" && - !isTeamWorkspaceTerminalStatus(session.runtimeStatus), + session.sessionType !== "user" && + !isTeamWorkspaceTerminalStatus(session.runtimeStatus), ); } @@ -540,7 +546,8 @@ function buildTeamWaitSummaryDisplay( } const resolvedName = summary.resolvedSessionId - ? sessionNameById.get(summary.resolvedSessionId) ?? summary.resolvedSessionId + ? (sessionNameById.get(summary.resolvedSessionId) ?? + summary.resolvedSessionId) : "成员"; const normalizedStatus = summary.resolvedStatus ? normalizeTeamWorkspaceRuntimeStatus(summary.resolvedStatus) @@ -563,7 +570,7 @@ function buildTeamControlSummaryDisplay( const affectedCount = summary.affectedSessionIds.length; const firstAffectedId = summary.affectedSessionIds[0]; const firstAffectedName = firstAffectedId - ? sessionNameById.get(firstAffectedId) ?? firstAffectedId + ? (sessionNameById.get(firstAffectedId) ?? firstAffectedId) : "成员"; switch (summary.action) { @@ -765,7 +772,10 @@ function resolveSessionBlueprintRoleId( ) { score += 8; } - if (sessionRoleKey && normalizeComparableText(role.roleKey) === sessionRoleKey) { + if ( + sessionRoleKey && + normalizeComparableText(role.roleKey) === sessionRoleKey + ) { score += 4; } if ( @@ -788,10 +798,7 @@ function resolveSessionBlueprintRoleId( if (candidates.length === 0) { return null; } - if ( - candidates.length > 1 && - candidates[0]?.score === candidates[1]?.score - ) { + if (candidates.length > 1 && candidates[0]?.score === candidates[1]?.score) { return null; } @@ -809,7 +816,7 @@ function orderSessionsByRuntimeRoles( const runtimeRoles = ( teamDispatchPreviewState.members.length > 0 ? teamDispatchPreviewState.members - : teamDispatchPreviewState.blueprint?.roles ?? [] + : (teamDispatchPreviewState.blueprint?.roles ?? []) ).map((role) => ({ id: role.id, label: role.label, @@ -821,7 +828,9 @@ function orderSessionsByRuntimeRoles( return sessions; } - const roleOrder = new Map(runtimeRoles.map((role, index) => [role.id, index])); + const roleOrder = new Map( + runtimeRoles.map((role, index) => [role.id, index]), + ); const usedRoleIds = new Set(); return [...sessions] @@ -973,7 +982,10 @@ function buildSessionLaneEmptyState(params: { return "这部分已经完成,结果会继续汇入当前内容。"; } - if (session?.runtimeStatus === "failed" || session?.runtimeStatus === "aborted") { + if ( + session?.runtimeStatus === "failed" || + session?.runtimeStatus === "aborted" + ) { return "这一步没有顺利完成,你可以在下方查看细节并决定是否继续。"; } @@ -1052,7 +1064,10 @@ function resolveCanvasLanePreferredSize(params: { params.viewportWidth, ); const gapX = TEAM_WORKSPACE_CANVAS_AUTO_LAYOUT_GAP_X; - const safeViewportWidth = Math.max(params.viewportWidth, columns >= 3 ? 1180 : 980); + const safeViewportWidth = Math.max( + params.viewportWidth, + columns >= 3 ? 1180 : 980, + ); const usableWidth = safeViewportWidth - TEAM_WORKSPACE_CANVAS_VIEWPORT_PADDING * 2 - @@ -1060,7 +1075,11 @@ function resolveCanvasLanePreferredSize(params: { const rawWidth = Math.floor(usableWidth / columns); const width = params.laneKind === "session" - ? clampCanvasNumber(rawWidth, 340, columns === 1 ? 560 : columns === 2 ? 460 : 390) + ? clampCanvasNumber( + rawWidth, + 340, + columns === 1 ? 560 : columns === 2 ? 460 : 390, + ) : clampCanvasNumber(rawWidth - 20, 320, columns === 1 ? 520 : 380); const height = params.laneKind === "session" @@ -1083,8 +1102,7 @@ function buildCanvasStageHint(params: { hasRuntimeFormation, hasSelectedTeamPlan, teamDispatchPreviewState, - } = - params; + } = params; if (hasRealTeamGraph) { return "拖动画布空白处可平移,滚轮配合 Ctrl/Cmd 可缩放,拖动成员卡片可调整布局。"; @@ -1166,7 +1184,10 @@ function resolveLaneMatchingRuntimeMemberId( } const explicitRoleId = session.blueprintRoleId?.trim(); - if (explicitRoleId && runtimeMembers.some((member) => member.id === explicitRoleId)) { + if ( + explicitRoleId && + runtimeMembers.some((member) => member.id === explicitRoleId) + ) { return explicitRoleId; } @@ -1181,10 +1202,16 @@ function resolveLaneMatchingRuntimeMemberId( const candidates = runtimeMembers .map((member) => { let score = 0; - if (normalizedRoleLabel && normalizeComparableText(member.label) === normalizedRoleLabel) { + if ( + normalizedRoleLabel && + normalizeComparableText(member.label) === normalizedRoleLabel + ) { score += 8; } - if (normalizedRoleKey && normalizeComparableText(member.roleKey) === normalizedRoleKey) { + if ( + normalizedRoleKey && + normalizeComparableText(member.roleKey) === normalizedRoleKey + ) { score += 5; } if ( @@ -1204,10 +1231,7 @@ function resolveLaneMatchingRuntimeMemberId( if (candidates.length === 0) { return null; } - if ( - candidates.length > 1 && - candidates[0]?.score === candidates[1]?.score - ) { + if (candidates.length > 1 && candidates[0]?.score === candidates[1]?.score) { return null; } return candidates[0]?.memberId ?? null; @@ -1232,10 +1256,16 @@ function resolveLaneMatchingPlannedRoleId( const candidates = plannedRoles .map((role) => { let score = 0; - if (normalizedRoleLabel && normalizeComparableText(role.label) === normalizedRoleLabel) { + if ( + normalizedRoleLabel && + normalizeComparableText(role.label) === normalizedRoleLabel + ) { score += 8; } - if (normalizedRoleKey && normalizeComparableText(role.roleKey) === normalizedRoleKey) { + if ( + normalizedRoleKey && + normalizeComparableText(role.roleKey) === normalizedRoleKey + ) { score += 5; } if ( @@ -1255,10 +1285,7 @@ function resolveLaneMatchingPlannedRoleId( if (candidates.length === 0) { return null; } - if ( - candidates.length > 1 && - candidates[0]?.score === candidates[1]?.score - ) { + if (candidates.length > 1 && candidates[0]?.score === candidates[1]?.score) { return null; } return candidates[0]?.roleId ?? null; @@ -1279,10 +1306,16 @@ function resolveRuntimeMemberMatchingPlannedRoleId( const candidates = plannedRoles .map((role) => { let score = 0; - if (normalizedRoleLabel && normalizeComparableText(role.label) === normalizedRoleLabel) { + if ( + normalizedRoleLabel && + normalizeComparableText(role.label) === normalizedRoleLabel + ) { score += 8; } - if (normalizedRoleKey && normalizeComparableText(role.roleKey) === normalizedRoleKey) { + if ( + normalizedRoleKey && + normalizeComparableText(role.roleKey) === normalizedRoleKey + ) { score += 5; } if ( @@ -1302,10 +1335,7 @@ function resolveRuntimeMemberMatchingPlannedRoleId( if (candidates.length === 0) { return null; } - if ( - candidates.length > 1 && - candidates[0]?.score === candidates[1]?.score - ) { + if (candidates.length > 1 && candidates[0]?.score === candidates[1]?.score) { return null; } return candidates[0]?.roleId ?? null; @@ -1403,12 +1433,12 @@ export function TeamWorkspaceBoard({ "team-workspace"; const [shellExpanded, setShellExpanded] = useState(defaultShellExpanded); const detailExpanded = !embedded; - const canvasViewportFallbackHeight = - embedded && !detailExpanded ? 720 : 560; + const canvasViewportFallbackHeight = embedded && !detailExpanded ? 720 : 560; const [canvasLayoutState, setCanvasLayoutState] = - useState(() => - loadTeamWorkspaceCanvasLayout(canvasStorageScopeId) ?? - createDefaultTeamWorkspaceCanvasLayoutState(), + useState( + () => + loadTeamWorkspaceCanvasLayout(canvasStorageScopeId) ?? + createDefaultTeamWorkspaceCanvasLayoutState(), ); const [pendingSessionAction, setPendingSessionAction] = useState<{ sessionId: string; @@ -1438,12 +1468,11 @@ export function TeamWorkspaceBoard({ width: 960, height: embedded ? 720 : 560, }); - const canvasLayoutStateRef = useRef( - canvasLayoutState, - ); - const canvasLaneLayoutsRef = useRef>( - {}, - ); + const canvasLayoutStateRef = + useRef(canvasLayoutState); + const canvasLaneLayoutsRef = useRef< + Record + >({}); const canvasInteractionCleanupRef = useRef<(() => void) | null>(null); useEffect(() => { @@ -1602,10 +1631,8 @@ export function TeamWorkspaceBoard({ () => baseVisibleSessions.map( (session) => - applyLiveRuntimeState( - session, - liveRuntimeBySessionId[session.id], - ) ?? session, + applyLiveRuntimeState(session, liveRuntimeBySessionId[session.id]) ?? + session, ), [baseVisibleSessions, liveRuntimeBySessionId], ); @@ -1871,12 +1898,14 @@ export function TeamWorkspaceBoard({ const [selectedSessionId, setSelectedSessionId] = useState( () => railSessions[0]?.id ?? null, ); - const [expandedSessionId, setExpandedSessionId] = useState(null); + const [expandedSessionId, setExpandedSessionId] = useState( + null, + ); useEffect(() => { const defaultSelectedId = isChildSession - ? currentSessionId ?? railSessions[0]?.id ?? null - : memberCanvasSessions[0]?.id ?? railSessions[0]?.id ?? null; + ? (currentSessionId ?? railSessions[0]?.id ?? null) + : (memberCanvasSessions[0]?.id ?? railSessions[0]?.id ?? null); if (!selectedSessionId) { setSelectedSessionId(defaultSelectedId); @@ -1909,7 +1938,9 @@ export function TeamWorkspaceBoard({ return; } - if (!memberCanvasSessions.some((session) => session.id === expandedSessionId)) { + if ( + !memberCanvasSessions.some((session) => session.id === expandedSessionId) + ) { setExpandedSessionId(null); } }, [expandedSessionId, memberCanvasSessions]); @@ -1929,7 +1960,7 @@ export function TeamWorkspaceBoard({ [baseRailSessions, selectedSessionId], ); const selectedSessionActivityPreview = selectedSession - ? sessionActivityPreviewById[selectedSession.id] ?? null + ? (sessionActivityPreviewById[selectedSession.id] ?? null) : null; const selectedSessionActivityEntries = selectedSession ? mergeSessionActivityEntries( @@ -1946,15 +1977,15 @@ export function TeamWorkspaceBoard({ selectedSession && selectedSession.sessionType !== "user", ); const selectedSessionActivityId = selectedSessionSupportsActivityPreview - ? selectedSession?.id ?? null + ? (selectedSession?.id ?? null) : null; - const selectedSessionActivityFingerprint = selectedSessionSupportsActivityPreview - ? buildSessionActivityFingerprint(selectedBaseSession) - : null; - const selectedSessionActivityRefreshVersion = - selectedSessionActivityId - ? (activityRefreshVersionBySessionId[selectedSessionActivityId] ?? 0) - : 0; + const selectedSessionActivityFingerprint = + selectedSessionSupportsActivityPreview + ? buildSessionActivityFingerprint(selectedBaseSession) + : null; + const selectedSessionActivityRefreshVersion = selectedSessionActivityId + ? (activityRefreshVersionBySessionId[selectedSessionActivityId] ?? 0) + : 0; const selectedSessionActivityShouldPoll = selectedSessionSupportsActivityPreview && shouldPollSessionActivity(selectedSession); @@ -2073,7 +2104,8 @@ export function TeamWorkspaceBoard({ !selectedSessionActivityShouldPoll && cachedPreview?.status === "ready" && cachedPreview.fingerprint === fingerprint && - (cachedPreview.refreshVersion ?? 0) === selectedSessionActivityRefreshVersion + (cachedPreview.refreshVersion ?? 0) === + selectedSessionActivityRefreshVersion ) { return; } @@ -2086,7 +2118,8 @@ export function TeamWorkspaceBoard({ selectedSessionActivityRefreshVersion, { force: - (current?.refreshVersion ?? 0) < selectedSessionActivityRefreshVersion, + (current?.refreshVersion ?? 0) < + selectedSessionActivityRefreshVersion, }, ); }; @@ -2139,7 +2172,8 @@ export function TeamWorkspaceBoard({ const refreshVersion = activityRefreshVersionBySessionId[session.id] ?? 0; - const cachedPreview = sessionActivityPreviewByIdRef.current[session.id]; + const cachedPreview = + sessionActivityPreviewByIdRef.current[session.id]; return syncSessionActivityPreview( session.id, buildSessionActivityFingerprint(session), @@ -2187,7 +2221,8 @@ export function TeamWorkspaceBoard({ normalizedSelectedTeamRoles, ); const presetLabel = session.teamPresetId - ? getTeamPresetOption(session.teamPresetId)?.label ?? session.teamPresetId + ? (getTeamPresetOption(session.teamPresetId)?.label ?? + session.teamPresetId) : undefined; return { @@ -2219,7 +2254,8 @@ export function TeamWorkspaceBoard({ .map((skill) => buildSkillDisplayName(skill)), session, previewText: - cardActivityPreview || buildSessionLaneEmptyState({ session, previewState }), + cardActivityPreview || + buildSessionLaneEmptyState({ session, previewState }), previewEntries: mergedEntries.slice(0, 3), }; }), @@ -2232,70 +2268,64 @@ export function TeamWorkspaceBoard({ ], ); - const canvasBlueprintLanes = useMemo( - () => { - if (hasRealTeamGraph) { - return []; - } + const canvasBlueprintLanes = useMemo(() => { + if (hasRealTeamGraph) { + return []; + } - if (runtimeMembers.length > 0) { - return runtimeMembers.map((member) => { - const laneSummary = buildCanvasLaneTitleSummary(member); - const matchedPlannedRoleId = resolveRuntimeMemberMatchingPlannedRoleId( - member, - normalizedSelectedTeamRoles, - ); - return { - id: member.id, - persistKey: `runtime:${member.id}`, - fallbackPersistKeys: matchedPlannedRoleId - ? [`planned:${matchedPlannedRoleId}`] - : [], - kind: "runtime" as const, - title: member.label, - summary: laneSummary.summary, - badgeLabel: laneSummary.badgeLabel, - badgeClassName: laneSummary.badgeClassName, - dotClassName: laneSummary.dotClassName, - roleLabel: resolveTeamWorkspaceRoleHintLabel(member.roleKey) || undefined, - profileLabel: undefined, - statusHint: laneSummary.statusHint, - updatedAtLabel: "等待成员加入", - skillLabels: [], - previewText: member.summary, - previewEntries: [], - }; - }); - } - - return normalizedSelectedTeamRoles.map((role) => { - const laneSummary = buildPlannedRoleLaneSummary(role); + if (runtimeMembers.length > 0) { + return runtimeMembers.map((member) => { + const laneSummary = buildCanvasLaneTitleSummary(member); + const matchedPlannedRoleId = resolveRuntimeMemberMatchingPlannedRoleId( + member, + normalizedSelectedTeamRoles, + ); return { - id: role.id, - persistKey: `planned:${role.id}`, - fallbackPersistKeys: [], - kind: "planned" as const, - title: role.label, + id: member.id, + persistKey: `runtime:${member.id}`, + fallbackPersistKeys: matchedPlannedRoleId + ? [`planned:${matchedPlannedRoleId}`] + : [], + kind: "runtime" as const, + title: member.label, summary: laneSummary.summary, badgeLabel: laneSummary.badgeLabel, badgeClassName: laneSummary.badgeClassName, dotClassName: laneSummary.dotClassName, - roleLabel: resolveTeamWorkspaceRoleHintLabel(role.roleKey) || undefined, + roleLabel: + resolveTeamWorkspaceRoleHintLabel(member.roleKey) || undefined, profileLabel: undefined, statusHint: laneSummary.statusHint, - updatedAtLabel: "计划分工", + updatedAtLabel: "等待成员加入", skillLabels: [], - previewText: role.summary, + previewText: member.summary, previewEntries: [], }; }); - }, - [ - hasRealTeamGraph, - normalizedSelectedTeamRoles, - runtimeMembers, - ], - ); + } + + return normalizedSelectedTeamRoles.map((role) => { + const laneSummary = buildPlannedRoleLaneSummary(role); + return { + id: role.id, + persistKey: `planned:${role.id}`, + fallbackPersistKeys: [], + kind: "planned" as const, + title: role.label, + summary: laneSummary.summary, + badgeLabel: laneSummary.badgeLabel, + badgeClassName: laneSummary.badgeClassName, + dotClassName: laneSummary.dotClassName, + roleLabel: resolveTeamWorkspaceRoleHintLabel(role.roleKey) || undefined, + profileLabel: undefined, + statusHint: laneSummary.statusHint, + updatedAtLabel: "计划分工", + skillLabels: [], + previewText: role.summary, + previewEntries: [], + }; + }); + }, [hasRealTeamGraph, normalizedSelectedTeamRoles, runtimeMembers]); const canvasLanes = hasRealTeamGraph ? canvasSessionLanes @@ -2406,10 +2436,7 @@ export function TeamWorkspaceBoard({ }, []); const bindCanvasMouseInteraction = useCallback( - ( - onMove: (event: MouseEvent) => void, - onEnd?: () => void, - ) => { + (onMove: (event: MouseEvent) => void, onEnd?: () => void) => { canvasInteractionCleanupRef.current?.(); const handleMouseMove = (event: MouseEvent) => { @@ -2581,7 +2608,11 @@ export function TeamWorkspaceBoard({ })); }); }, - [bindCanvasMouseInteraction, isCanvasPanModifierActive, updateCanvasViewport], + [ + bindCanvasMouseInteraction, + isCanvasPanModifierActive, + updateCanvasViewport, + ], ); const handleStartCanvasLaneDrag = useCallback( @@ -2610,7 +2641,11 @@ export function TeamWorkspaceBoard({ })); }); }, - [bindCanvasMouseInteraction, bringCanvasLaneToFront, updateCanvasLaneLayout], + [ + bindCanvasMouseInteraction, + bringCanvasLaneToFront, + updateCanvasLaneLayout, + ], ); const handleStartCanvasLaneResize = useCallback( @@ -2680,7 +2715,11 @@ export function TeamWorkspaceBoard({ }); }); }, - [bindCanvasMouseInteraction, bringCanvasLaneToFront, updateCanvasLaneLayout], + [ + bindCanvasMouseInteraction, + bringCanvasLaneToFront, + updateCanvasLaneLayout, + ], ); const handleCanvasWheel = useCallback( @@ -2720,7 +2759,9 @@ export function TeamWorkspaceBoard({ }, [updateCanvasViewport]); const handleResetCanvasView = useCallback(() => { - updateCanvasViewport(() => createDefaultTeamWorkspaceCanvasLayoutState().viewport); + updateCanvasViewport( + () => createDefaultTeamWorkspaceCanvasLayoutState().viewport, + ); }, [updateCanvasViewport]); const handleAutoArrangeCanvas = useCallback(() => { if (canvasLanes.length === 0) { @@ -2985,8 +3026,10 @@ export function TeamWorkspaceBoard({ return null; } - return [...teamControlSummary.requestedSessionIds, ...teamControlSummary.affectedSessionIds] - .some((sessionId) => railSessionNameById.has(sessionId)) + return [ + ...teamControlSummary.requestedSessionIds, + ...teamControlSummary.affectedSessionIds, + ].some((sessionId) => railSessionNameById.has(sessionId)) ? teamControlSummary : null; }, [railSessionNameById, teamControlSummary]); @@ -2998,7 +3041,8 @@ export function TeamWorkspaceBoard({ teamControlSummary: visibleTeamControlSummary, }).filter( (entry) => - !entry.targetSessionId || railSessions.some((session) => session.id === entry.targetSessionId), + !entry.targetSessionId || + railSessions.some((session) => session.id === entry.targetSessionId), ), [ railSessionNameById, @@ -3024,38 +3068,40 @@ export function TeamWorkspaceBoard({ ); const canOpenSelectedSession = Boolean( selectedSession && - onOpenSubagentSession && - selectedSession.id !== currentSessionId, + onOpenSubagentSession && + selectedSession.id !== currentSessionId, ); const canWaitSelectedSession = Boolean( - selectedSession && isWaitableTeamSession(selectedSession) && onWaitSubagentSession, + selectedSession && + isWaitableTeamSession(selectedSession) && + onWaitSubagentSession, ); const canSendSelectedSessionInput = Boolean( selectedSession && - selectedSession.sessionType !== "user" && - selectedSession.runtimeStatus !== "closed" && - onSendSubagentInput && - selectedSession.id !== currentSessionId, + selectedSession.sessionType !== "user" && + selectedSession.runtimeStatus !== "closed" && + onSendSubagentInput && + selectedSession.id !== currentSessionId, ); const canStopSelectedSession = Boolean( selectedSession && - selectedSession.sessionType !== "user" && - isTeamWorkspaceActiveStatus( - selectedSession.runtimeStatus ?? selectedSession.latestTurnStatus, - ) && - onCloseSubagentSession, + selectedSession.sessionType !== "user" && + isTeamWorkspaceActiveStatus( + selectedSession.runtimeStatus ?? selectedSession.latestTurnStatus, + ) && + onCloseSubagentSession, ); const canResumeSelectedSession = Boolean( selectedSession && - selectedSession.sessionType !== "user" && - selectedSession.runtimeStatus === "closed" && - onResumeSubagentSession, + selectedSession.sessionType !== "user" && + selectedSession.runtimeStatus === "closed" && + onResumeSubagentSession, ); const selectedActionPending = Boolean( selectedSession && pendingSessionAction?.sessionId === selectedSession.id, ); const selectedSessionInputDraft = selectedSession - ? sessionInputDraftById[selectedSession.id] ?? "" + ? (sessionInputDraftById[selectedSession.id] ?? "") : ""; const selectedSessionInputMessage = selectedSessionInputDraft.trim(); const handleWaitAnyActiveTeamSessions = useCallback(async () => { @@ -3090,7 +3136,9 @@ export function TeamWorkspaceBoard({ if (!entry.targetSessionId) { return; } - if (!railSessions.some((session) => session.id === entry.targetSessionId)) { + if ( + !railSessions.some((session) => session.id === entry.targetSessionId) + ) { return; } setSelectedSessionId(entry.targetSessionId); @@ -3282,7 +3330,9 @@ export function TeamWorkspaceBoard({ const selectedStatusMeta = resolveStatusMeta(selectedSession?.runtimeStatus); const detailVisible = - isEmptyShellState || !hasRealTeamGraph ? detailExpanded || shellExpanded : false; + isEmptyShellState || !hasRealTeamGraph + ? detailExpanded || shellExpanded + : false; const detailToggleLabel = detailVisible ? "收起细节" : "查看细节"; const boardHeadline = !hasRealTeamGraph && runtimeFormationMeta @@ -3310,7 +3360,9 @@ export function TeamWorkspaceBoard({ }); const useCompactCanvasChrome = hasRealTeamGraph; const runtimeDetailSummary = buildRuntimeDetailSummary(selectedSession); - const selectedPresetOption = getTeamPresetOption(selectedSession?.teamPresetId); + const selectedPresetOption = getTeamPresetOption( + selectedSession?.teamPresetId, + ); const selectedSkills = selectedSession?.skills ?? []; const selectedMetadata = [ selectedSession?.blueprintRoleLabel @@ -3327,7 +3379,9 @@ export function TeamWorkspaceBoard({ selectedSession?.createdFromTurnId ? `来自之前的任务 ${selectedSession.createdFromTurnId}` : null, - selectedSession && (selectedSession.teamQueuedCount ?? selectedSession.queuedTurnCount ?? 0) > 0 + selectedSession && + (selectedSession.teamQueuedCount ?? selectedSession.queuedTurnCount ?? 0) > + 0 ? `等待中 ${selectedSession.teamQueuedCount ?? selectedSession.queuedTurnCount}` : null, selectedSession?.teamActiveCount !== undefined && @@ -3368,7 +3422,9 @@ export function TeamWorkspaceBoard({ const boardBodyClassName = embedded ? cn( "min-h-0 flex-1 overflow-y-auto overscroll-contain", - useCompactCanvasChrome ? "p-3 sm:p-3.5 space-y-2.5" : "p-3 sm:p-4 space-y-3", + useCompactCanvasChrome + ? "p-3 sm:p-3.5 space-y-2.5" + : "p-3 sm:p-4 space-y-3", ) : cn(useCompactCanvasChrome ? "p-3 sm:p-3.5" : "p-3 sm:p-4"); const canvasStageHeight = @@ -3376,7 +3432,10 @@ export function TeamWorkspaceBoard({ ? "clamp(560px, 76vh, 980px)" : TEAM_WORKSPACE_CANVAS_STAGE_HEIGHT; const railCardClassName = embedded - ? cn("pointer-events-auto", useCompactCanvasChrome ? "space-y-3" : "space-y-4") + ? cn( + "pointer-events-auto", + useCompactCanvasChrome ? "space-y-3" : "space-y-4", + ) : "rounded-[22px] border border-slate-200 bg-slate-50 p-3.5 shadow-sm shadow-slate-950/5"; const detailCardClassName = cn( embedded @@ -3523,7 +3582,8 @@ export function TeamWorkspaceBoard({ ) : null} {resolveTeamWorkspaceRoleHintLabel(selectedSession?.roleKey) ? ( - 分工 {resolveTeamWorkspaceRoleHintLabel(selectedSession?.roleKey)} + 分工{" "} + {resolveTeamWorkspaceRoleHintLabel(selectedSession?.roleKey)} ) : null} {selectedSession?.theme ? ( @@ -3602,7 +3662,9 @@ export function TeamWorkspaceBoard({
{selectedSessionActivityEntries.map((entry) => ( -
+
{entry.title} @@ -3758,16 +3825,16 @@ export function TeamWorkspaceBoard({ 来自之前的任务 {subagentParentContext.created_from_turn_id} ) : null} - {!useCompactCanvasChrome && !isChildSession && totalTeamSessions > 0 ? ( + {!useCompactCanvasChrome && + !isChildSession && + totalTeamSessions > 0 ? ( {totalTeamSessions} 位协作成员 ) : null}
{!useCompactCanvasChrome ? ( -

- {boardHint} -

+

{boardHint}

) : null}
@@ -3789,30 +3856,28 @@ export function TeamWorkspaceBoard({ {detailToggleLabel} ) : null} - {isEmptyShellState - ? ( - - {runtimeFormationMeta?.label || "还没有协作成员加入"} - - ) - : !useCompactCanvasChrome - ? Object.entries(statusSummary) - .filter(([, count]) => count > 0) - .map(([status, count]) => { - const meta = resolveStatusMeta(status as RuntimeStatus); - return ( - - {meta.label} {count} - - ); - }) - : null} + {isEmptyShellState ? ( + + {runtimeFormationMeta?.label || "还没有协作成员加入"} + + ) : !useCompactCanvasChrome ? ( + Object.entries(statusSummary) + .filter(([, count]) => count > 0) + .map(([status, count]) => { + const meta = resolveStatusMeta(status as RuntimeStatus); + return ( + + {meta.label} {count} + + ); + }) + ) : null} {isChildSession && onReturnToParentSession ? ( ) : null} {canWaitAnyActiveTeamSession ? ( @@ -4011,9 +4076,9 @@ export function TeamWorkspaceBoard({ {pendingTeamAction === "close_completed" ? ( ) : null} - {pendingTeamAction === "close_completed" - ? "收起中..." - : "收起已完成成员"} + {pendingTeamAction === "close_completed" + ? "收起中..." + : "收起已完成成员"} ) : null} {canCloseCompletedTeamSessions ? ( @@ -4098,10 +4163,12 @@ export function TeamWorkspaceBoard({ const content = ( <>
- + {entry.title} @@ -4146,7 +4213,12 @@ export function TeamWorkspaceBoard({ ) ) : null} -
+
{!useCompactCanvasChrome ? (
@@ -4241,7 +4313,9 @@ export function TeamWorkspaceBoard({ 画布 - 空白处拖拽 · Space 手型 · A 整理 · F 适应 + + 空白处拖拽 · Space 手型 · A 整理 · F 适应 +
{canvasLanes.length > 0 ? (
- handleStartCanvasLaneDrag(lane.persistKey, event) + handleStartCanvasLaneDrag( + lane.persistKey, + event, + ) } >
@@ -4426,21 +4503,24 @@ export function TeamWorkspaceBoard({

{lane.summary}

- {(lane.skillLabels.length > 0 || lane.presetLabel) ? ( + {lane.skillLabels.length > 0 || + lane.presetLabel ? (
{lane.presetLabel ? ( {lane.presetLabel} ) : null} - {lane.skillLabels.slice(0, 4).map((skillLabel) => ( - - {skillLabel} - - ))} + {lane.skillLabels + .slice(0, 4) + .map((skillLabel) => ( + + {skillLabel} + + ))}
) : null}
@@ -4448,14 +4528,17 @@ export function TeamWorkspaceBoard({ 成员进展 - {lane.kind === "session" ? "最近进展" : "等待接入"} + {lane.kind === "session" + ? "最近进展" + : "等待接入"}

{lane.previewText}

- {lane.previewEntries && lane.previewEntries.length > 0 ? ( + {lane.previewEntries && + lane.previewEntries.length > 0 ? (
{lane.previewEntries.map((entry) => (
- {expanded ? renderSelectedSessionInlineDetail() : null} + {expanded + ? renderSelectedSessionInlineDetail() + : null}
{resizeHandles.map((handle) => ( diff --git a/src/components/agent/chat/components/TeamWorkspaceDock.test.tsx b/src/components/agent/chat/components/TeamWorkspaceDock.test.tsx index 80da9abea..f0d7e91a8 100644 --- a/src/components/agent/chat/components/TeamWorkspaceDock.test.tsx +++ b/src/components/agent/chat/components/TeamWorkspaceDock.test.tsx @@ -8,19 +8,15 @@ import { TeamWorkspaceDock } from "./TeamWorkspaceDock"; const { mockGetAgentRuntimeSession } = vi.hoisted(() => ({ mockGetAgentRuntimeSession: vi.fn(), })); -const { - mockSafeListen, - mockParseAgentEvent, -} = vi.hoisted(() => ({ +const { mockSafeListen, mockParseAgentEvent } = vi.hoisted(() => ({ mockSafeListen: vi.fn(), mockParseAgentEvent: vi.fn((payload: unknown) => payload), })); vi.mock("@/lib/api/agentRuntime", async () => { - const actual = - await vi.importActual( - "@/lib/api/agentRuntime", - ); + const actual = await vi.importActual( + "@/lib/api/agentRuntime", + ); return { ...actual, getAgentRuntimeSession: mockGetAgentRuntimeSession, @@ -28,10 +24,9 @@ vi.mock("@/lib/api/agentRuntime", async () => { }); vi.mock("@/lib/api/agentProtocol", async () => { - const actual = - await vi.importActual( - "@/lib/api/agentProtocol", - ); + const actual = await vi.importActual< + typeof import("@/lib/api/agentProtocol") + >("@/lib/api/agentProtocol"); return { ...actual, parseAgentEvent: mockParseAgentEvent, @@ -138,7 +133,9 @@ async function renderDock( nextProps?: Partial>, ) => { await act(async () => { - root.render(); + root.render( + , + ); await Promise.resolve(); }); await flushDockEffects(); @@ -180,9 +177,13 @@ describe("TeamWorkspaceDock", () => { expect(document.body.textContent).toContain("当前协作方案:前端联调团队"); expect(document.body.textContent).toContain("分析、实现、验证三段式推进。"); expect(document.body.textContent).not.toContain("查看详情"); - expect(document.body.querySelector('[data-testid="team-workspace-empty-card"]')).toBeTruthy(); expect( - document.body.querySelector('[data-testid="team-workspace-selected-team"]'), + document.body.querySelector('[data-testid="team-workspace-empty-card"]'), + ).toBeTruthy(); + expect( + document.body.querySelector( + '[data-testid="team-workspace-selected-team"]', + ), ).toBeTruthy(); }); diff --git a/src/components/agent/chat/components/TeamWorkspaceDock.tsx b/src/components/agent/chat/components/TeamWorkspaceDock.tsx index f11295690..2727c2117 100644 --- a/src/components/agent/chat/components/TeamWorkspaceDock.tsx +++ b/src/components/agent/chat/components/TeamWorkspaceDock.tsx @@ -33,11 +33,7 @@ const DockContainer = styled.div<{ $placement === "inline" ? "relative" : "absolute"}; right: ${({ $placement }) => ($placement === "inline" ? "auto" : "14px")}; bottom: ${({ $placement, $withBottomOverlay }) => - $placement === "inline" - ? "auto" - : $withBottomOverlay - ? "108px" - : "16px"}; + $placement === "inline" ? "auto" : $withBottomOverlay ? "108px" : "16px"}; z-index: ${({ $placement }) => ($placement === "inline" ? 120 : 18)}; display: flex; flex-direction: column; @@ -98,8 +94,8 @@ const DockPanel = styled.div<{ : "min(560px, calc(100vw - 40px))"}; max-height: ${({ $compact }) => $compact - ? "min(220px, calc(100vh - 200px))" - : "min(520px, calc(100vh - 148px))"}; + ? "min(220px, calc(100vh - 200px))" + : "min(520px, calc(100vh - 148px))"}; } @media (max-width: 720px) { @@ -333,7 +329,9 @@ const EmptyStateBadge = styled.span<{ $tone?: "neutral" | "success" }>` border-radius: 999px; border: 1px solid ${({ $tone }) => - $tone === "success" ? "rgba(167, 243, 208, 0.96)" : "rgba(226, 232, 240, 0.96)"}; + $tone === "success" + ? "rgba(167, 243, 208, 0.96)" + : "rgba(226, 232, 240, 0.96)"}; background: ${({ $tone }) => ($tone === "success" ? "#ecfdf5" : "#f8fafc")}; color: ${({ $tone }) => ($tone === "success" ? "#047857" : "#64748b")}; padding: 4px 10px; @@ -458,9 +456,7 @@ interface TeamWorkspaceDockProps { sessionIds: string[], timeoutMs?: number, ) => void | Promise; - onCloseCompletedTeamSessions?: ( - sessionIds: string[], - ) => void | Promise; + onCloseCompletedTeamSessions?: (sessionIds: string[]) => void | Promise; onCloseSubagentSession?: (sessionId: string) => void | Promise; onResumeSubagentSession?: (sessionId: string) => void | Promise; onOpenSubagentSession?: (sessionId: string) => void | Promise; @@ -557,7 +553,8 @@ export function TeamWorkspaceDock({ (selectedTeamRoles?.length ?? 0) > 0; const activeRuntimeSessionCount = childSubagentSessions.filter( (session) => - session.runtime_status === "queued" || session.runtime_status === "running", + session.runtime_status === "queued" || + session.runtime_status === "running", ).length; const runningRuntimeSessionCount = childSubagentSessions.filter( (session) => session.runtime_status === "running", @@ -580,14 +577,15 @@ export function TeamWorkspaceDock({ ? `${runningRuntimeSessionCount} 位处理中` : null; const teamQueueBadgeText = - (teamConcurrencySnapshot?.team_queued_count ?? queuedRuntimeSessionCount) > 0 + (teamConcurrencySnapshot?.team_queued_count ?? queuedRuntimeSessionCount) > + 0 ? `${teamConcurrencySnapshot?.team_queued_count ?? queuedRuntimeSessionCount} 位等待中` : null; - const teamQueueReason = - teamConcurrencySnapshot?.queue_reason?.trim() || null; - const launcherStatusMeta = useMemo< - { label: string; tone: "idle" | "active" | "success" | "error" } | null - >(() => { + const teamQueueReason = teamConcurrencySnapshot?.queue_reason?.trim() || null; + const launcherStatusMeta = useMemo<{ + label: string; + tone: "idle" | "active" | "success" | "error"; + } | null>(() => { if (!launcherOnly) { return null; } @@ -604,8 +602,7 @@ export function TeamWorkspaceDock({ if (activeRuntimeSessionCount > 0) { return { label: - teamConcurrencyBadgeText || - `${activeRuntimeSessionCount} 位处理中`, + teamConcurrencyBadgeText || `${activeRuntimeSessionCount} 位处理中`, tone: "active", }; } @@ -645,11 +642,7 @@ export function TeamWorkspaceDock({ } if (hasRealTeamGraph) { if (teamConcurrencyBadgeText || teamQueueBadgeText) { - return [ - "查看任务进展", - teamConcurrencyBadgeText, - teamQueueBadgeText, - ] + return ["查看任务进展", teamConcurrencyBadgeText, teamQueueBadgeText] .filter(Boolean) .join(" · "); } @@ -693,10 +686,8 @@ export function TeamWorkspaceDock({ return; } - const graphAppeared = - !previousHasRealGraphRef.current && hasRealTeamGraph; - const graphCleared = - previousHasRealGraphRef.current && !hasRealTeamGraph; + const graphAppeared = !previousHasRealGraphRef.current && hasRealTeamGraph; + const graphCleared = previousHasRealGraphRef.current && !hasRealTeamGraph; if (graphCleared) { setUserDismissedAutoExpand(false); @@ -777,12 +768,12 @@ export function TeamWorkspaceDock({ liveRuntimeBySessionId={liveRuntimeBySessionId} liveActivityBySessionId={liveActivityBySessionId} activityRefreshVersionBySessionId={activityRefreshVersionBySessionId} - onSendSubagentInput={onSendSubagentInput} - onWaitSubagentSession={onWaitSubagentSession} - onWaitActiveTeamSessions={onWaitActiveTeamSessions} - onCloseCompletedTeamSessions={onCloseCompletedTeamSessions} - onCloseSubagentSession={onCloseSubagentSession} - onResumeSubagentSession={onResumeSubagentSession} + onSendSubagentInput={onSendSubagentInput} + onWaitSubagentSession={onWaitSubagentSession} + onWaitActiveTeamSessions={onWaitActiveTeamSessions} + onCloseCompletedTeamSessions={onCloseCompletedTeamSessions} + onCloseSubagentSession={onCloseSubagentSession} + onResumeSubagentSession={onResumeSubagentSession} onOpenSubagentSession={onOpenSubagentSession} onReturnToParentSession={onReturnToParentSession} teamWaitSummary={teamWaitSummary} @@ -811,18 +802,16 @@ export function TeamWorkspaceDock({ {runtimeFormationMeta?.title || "等待协作成员加入"} - {dispatchPreviewState?.status === "forming" - ? "系统正在按当前任务准备协作分工,成员接入后会自动展开完整协作面板。" - : dispatchPreviewState?.status === "formed" - ? `已准备 ${dispatchPreviewState.members.length} 位协作成员,后续会自动接入并继续处理。` - : dispatchPreviewState?.status === "failed" - ? dispatchPreviewState.errorMessage?.trim() || - "这次协作准备失败,你仍然可以继续在当前对话中处理。" - : ( - <> - 需要时系统会自动邀请协作成员加入,这里会切换成完整协作面板。 - - )} + {dispatchPreviewState?.status === "forming" ? ( + "系统正在按当前任务准备协作分工,成员接入后会自动展开完整协作面板。" + ) : dispatchPreviewState?.status === "formed" ? ( + `已准备 ${dispatchPreviewState.members.length} 位协作成员,后续会自动接入并继续处理。` + ) : dispatchPreviewState?.status === "failed" ? ( + dispatchPreviewState.errorMessage?.trim() || + "这次协作准备失败,你仍然可以继续在当前对话中处理。" + ) : ( + <>需要时系统会自动邀请协作成员加入,这里会切换成完整协作面板。 + )} {teamQueueReason ? ( {teamQueueReason} @@ -836,7 +825,9 @@ export function TeamWorkspaceDock({ data-testid="team-workspace-selected-team-toggle" >
- {runtimeTeamLabel} + + {runtimeTeamLabel} + {dispatchPreviewState ? "查看当前协作分工与参考方案" @@ -856,7 +847,9 @@ export function TeamWorkspaceDock({ {dispatchPreviewState ? "当前摘要" : "方案摘要"} - {runtimeTeamSummary} + + {runtimeTeamSummary} + ) : null} {dispatchPreviewState?.members.map((member) => { @@ -883,7 +876,9 @@ export function TeamWorkspaceDock({ ) : null}
- {member.summary} + + {member.summary} + {member.skillIds.length > 0 ? (
{member.skillIds.map((skillId) => ( @@ -903,7 +898,9 @@ export function TeamWorkspaceDock({ ? selectedTeamRoles?.map((role) => ( {role.label} - {role.summary} + + {role.summary} + )) : null} @@ -932,7 +929,9 @@ export function TeamWorkspaceDock({ {runtimeTeamSummary} ) : null} {dispatchPreviewState?.members.length ? ( - {dispatchPreviewState.members.length} 个成员 + + {dispatchPreviewState.members.length} 个成员 + ) : null} 不遮挡画布 diff --git a/src/components/agent/chat/components/ToolCallDisplay.test.tsx b/src/components/agent/chat/components/ToolCallDisplay.test.tsx index dfea66d72..50b298f48 100644 --- a/src/components/agent/chat/components/ToolCallDisplay.test.tsx +++ b/src/components/agent/chat/components/ToolCallDisplay.test.tsx @@ -164,7 +164,9 @@ describe("ToolCallDisplay", () => { expect( container.querySelector('[data-testid="tool-call-rendered-result"]'), ).not.toBeNull(); - expect(container.textContent).toContain("本次检索未返回可解析链接,请稍后重试。"); + expect(container.textContent).toContain( + "本次检索未返回可解析链接,请稍后重试。", + ); expect(container.textContent).not.toContain("查看原始输出"); }); @@ -189,7 +191,9 @@ describe("ToolCallDisplay", () => { { id: "tool-search-2", name: "WebSearch", - arguments: JSON.stringify({ query: "March 13 2026 world headlines" }), + arguments: JSON.stringify({ + query: "March 13 2026 world headlines", + }), status: "completed", result: { success: true, output: "https://example.com/2" }, startTime: new Date("2026-03-13T12:00:02.000Z"), @@ -207,7 +211,9 @@ describe("ToolCallDisplay", () => { expect(container.textContent).toContain("3月13日国际新闻"); expect(container.textContent).toContain("March 13 2026 world headlines"); expect(container.textContent).toContain("搜索 3月13日国际新闻"); - expect(container.textContent).toContain("搜索 March 13 2026 world headlines"); + expect(container.textContent).toContain( + "搜索 March 13 2026 world headlines", + ); expect(container.textContent).toContain("中文日期检索"); expect(container.textContent).toContain("头条检索"); }); @@ -246,7 +252,9 @@ describe("ToolCallDisplay", () => { mountedRoots.push({ container, root }); - const groups = container.querySelectorAll('[data-testid="tool-call-work-group"]'); + const groups = container.querySelectorAll( + '[data-testid="tool-call-work-group"]', + ); expect(groups).toHaveLength(1); expect(container.textContent).toContain("已执行 2 条命令"); expect(container.textContent).toContain("2"); @@ -254,7 +262,9 @@ describe("ToolCallDisplay", () => { expect(container.textContent).toContain("ls -la"); act(() => { - const groupToggle = groups[0]?.querySelector("button") as HTMLButtonElement | null; + const groupToggle = groups[0]?.querySelector( + "button", + ) as HTMLButtonElement | null; groupToggle?.click(); }); @@ -324,8 +334,7 @@ describe("ToolCallDisplay", () => { project_root_path: "/Users/coso/.proxycast/projects/project-1", markdown_relative_path: "exports/x-article-export/github-mcp/index.md", - images_relative_dir: - "exports/x-article-export/github-mcp/images", + images_relative_dir: "exports/x-article-export/github-mcp/images", image_count: 7, }, saved_project_id: "project-1", @@ -359,7 +368,9 @@ describe("ToolCallDisplay", () => { expect(container.textContent).toContain( "图片资源:7 张 · exports/x-article-export/github-mcp/images", ); - expect(container.textContent).toContain("脚本来源:服务端脚本 · 2026-03-25"); + expect(container.textContent).toContain( + "脚本来源:服务端脚本 · 2026-03-25", + ); }); it("站点能力工具结果应支持直接打开已保存内容", () => { @@ -559,7 +570,10 @@ describe("ToolCallDisplay", () => { { id: "tool-remote-trigger-1", name: "RemoteTrigger", - arguments: JSON.stringify({ action: "run", trigger_id: "remote-1" }), + arguments: JSON.stringify({ + action: "run", + trigger_id: "remote-1", + }), status: "completed", result: { success: true, output: "ok" }, startTime: new Date("2026-03-20T12:20:14.000Z"), diff --git a/src/components/agent/chat/components/ToolCallDisplay.tsx b/src/components/agent/chat/components/ToolCallDisplay.tsx index 9ef917282..7f11cd939 100644 --- a/src/components/agent/chat/components/ToolCallDisplay.tsx +++ b/src/components/agent/chat/components/ToolCallDisplay.tsx @@ -13,12 +13,7 @@ import React, { useCallback, } from "react"; import { open as openExternal } from "@tauri-apps/plugin-shell"; -import { - ChevronDown, - ChevronRight, - Loader2, - ExternalLink, -} from "lucide-react"; +import { ChevronDown, ChevronRight, Loader2, ExternalLink } from "lucide-react"; import { cn } from "@/lib/utils"; import type { AgentToolCallState as ToolCallState, @@ -389,43 +384,43 @@ function normalizeSiteToolResultSummary( } return { - savedContent: hasSavedContent - ? { - contentId: readFirstNonEmptyString([savedContentRecord], [ - "content_id", - "contentId", - ]), - projectId: readFirstNonEmptyString([savedContentRecord], [ - "project_id", - "projectId", - ]), - title: readFirstNonEmptyString([savedContentRecord], ["title"]), - projectRootPath: readFirstNonEmptyString([savedContentRecord], [ - "project_root_path", - "projectRootPath", - ]), - bundleRelativeDir: readFirstNonEmptyString([savedContentRecord], [ - "bundle_relative_dir", - "bundleRelativeDir", - ]), - markdownRelativePath: readFirstNonEmptyString([savedContentRecord], [ - "markdown_relative_path", - "markdownRelativePath", - ]), - imagesRelativeDir: readFirstNonEmptyString([savedContentRecord], [ - "images_relative_dir", - "imagesRelativeDir", - ]), - metaRelativePath: readFirstNonEmptyString([savedContentRecord], [ - "meta_relative_path", - "metaRelativePath", - ]), - imageCount: readFirstFiniteNumber([savedContentRecord], [ - "image_count", - "imageCount", - ]), - } - : undefined, + savedContent: hasSavedContent + ? { + contentId: readFirstNonEmptyString( + [savedContentRecord], + ["content_id", "contentId"], + ), + projectId: readFirstNonEmptyString( + [savedContentRecord], + ["project_id", "projectId"], + ), + title: readFirstNonEmptyString([savedContentRecord], ["title"]), + projectRootPath: readFirstNonEmptyString( + [savedContentRecord], + ["project_root_path", "projectRootPath"], + ), + bundleRelativeDir: readFirstNonEmptyString( + [savedContentRecord], + ["bundle_relative_dir", "bundleRelativeDir"], + ), + markdownRelativePath: readFirstNonEmptyString( + [savedContentRecord], + ["markdown_relative_path", "markdownRelativePath"], + ), + imagesRelativeDir: readFirstNonEmptyString( + [savedContentRecord], + ["images_relative_dir", "imagesRelativeDir"], + ), + metaRelativePath: readFirstNonEmptyString( + [savedContentRecord], + ["meta_relative_path", "metaRelativePath"], + ), + imageCount: readFirstFiniteNumber( + [savedContentRecord], + ["image_count", "imageCount"], + ), + } + : undefined, savedProjectId, savedBy: readFirstNonEmptyString(candidates, ["saved_by", "savedBy"]), saveSkippedProjectId, @@ -449,7 +444,9 @@ function resolveSiteProjectSourceLabel(source?: string): string | null { return null; } -function resolveSiteAdapterSourceLabel(summary: SiteToolResultSummary): string | null { +function resolveSiteAdapterSourceLabel( + summary: SiteToolResultSummary, +): string | null { if (summary.adapterSourceKind === "server_synced") { return summary.adapterSourceVersion ? `服务端脚本 · ${summary.adapterSourceVersion}` @@ -741,7 +738,8 @@ export const ToolCallDisplay: React.FC = ({ // 获取文件名 const fileName = useMemo( - () => resolveToolPrimarySubjectFromInfo(toolCall.name, parsedArgs, filePath), + () => + resolveToolPrimarySubjectFromInfo(toolCall.name, parsedArgs, filePath), [filePath, parsedArgs, toolCall.name], ); @@ -825,8 +823,11 @@ export const ToolCallDisplay: React.FC = ({ const notices: ToolResultNotice[] = []; const savedProjectId = - siteResultSummary.savedProjectId || siteResultSummary.savedContent?.projectId; - const savedSourceLabel = resolveSiteProjectSourceLabel(siteResultSummary.savedBy); + siteResultSummary.savedProjectId || + siteResultSummary.savedContent?.projectId; + const savedSourceLabel = resolveSiteProjectSourceLabel( + siteResultSummary.savedBy, + ); if (siteResultSummary.savedContent?.title) { let text = `结果已自动保存${ @@ -984,9 +985,7 @@ export const ToolCallDisplay: React.FC = ({ const shouldShowRawSearchResultToggle = hasSearchResults && resultText !== "(无输出)"; const shouldRenderResultPanel = - isExpanded && - hasResult && - (!hasSearchResults || showRawSearchResultOutput); + isExpanded && hasResult && (!hasSearchResults || showRawSearchResultOutput); const handleOpenExternalUrl = useCallback(async (url: string) => { try { @@ -1007,7 +1006,13 @@ export const ToolCallDisplay: React.FC = ({ ) { setIsExpanded(true); } - }, [isMessageStreaming, isRunning, hasResult, hasResultImages, hasSearchResults]); + }, [ + isMessageStreaming, + isRunning, + hasResult, + hasResultImages, + hasSearchResults, + ]); useEffect(() => { if (hasSearchResults && !hasUserToggledExpandedRef.current) { @@ -1042,7 +1047,10 @@ export const ToolCallDisplay: React.FC = ({ return (
{grouped ? ( -
+
{groupMarker} @@ -1165,9 +1173,7 @@ export const ToolCallDisplay: React.FC = ({ {hasSearchResults && isExpanded && (
- - {searchSemantic.label} - + {searchSemantic.label}
= ({ setShowRawSearchResultOutput((current) => !current) } > - {showRawSearchResultOutput - ? "收起原始输出" - : "查看原始输出"} + {showRawSearchResultOutput ? "收起原始输出" : "查看原始输出"}
) : null} @@ -1312,11 +1316,7 @@ export const ToolCallList: React.FC = ({ for (const toolCall of toolCalls) { const isSearch = isUnifiedWebSearchToolName(toolCall.name); const lastGroup = groups[groups.length - 1]; - if ( - isSearch && - lastGroup && - lastGroup.type === "search" - ) { + if (isSearch && lastGroup && lastGroup.type === "search") { lastGroup.items.push(toolCall); continue; } diff --git a/src/components/agent/chat/components/emptyStateSurfaceTokens.ts b/src/components/agent/chat/components/emptyStateSurfaceTokens.ts index 9fc800f68..970e38aeb 100644 --- a/src/components/agent/chat/components/emptyStateSurfaceTokens.ts +++ b/src/components/agent/chat/components/emptyStateSurfaceTokens.ts @@ -1,13 +1,13 @@ export type EmptyStateTone = "slate" | "sky" | "emerald" | "amber" | "lime"; export const EMPTY_STATE_PANEL_CLASSNAME = - "rounded-[26px] border border-slate-200/80 bg-white p-4 shadow-sm shadow-slate-950/5 md:p-5"; + "rounded-[26px] border border-lime-200/80 bg-white p-4 shadow-sm shadow-slate-950/5 md:p-5"; export const EMPTY_STATE_PANEL_EMBEDDED_CLASSNAME = - "rounded-[22px] border border-slate-200/80 bg-white p-3.5 shadow-sm shadow-slate-950/5"; + "rounded-[22px] border border-lime-200/80 bg-white p-3.5 shadow-sm shadow-slate-950/5"; export const EMPTY_STATE_CARD_SURFACE_CLASSNAME = - "overflow-hidden rounded-[22px] border border-slate-200/80 bg-white p-3.5 shadow-sm shadow-slate-950/5"; + "overflow-hidden rounded-[22px] border border-lime-200/80 bg-white p-3.5 shadow-sm shadow-slate-950/5"; export const EMPTY_STATE_BADGE_BASE_CLASSNAME = "inline-flex items-center rounded-full border px-2.5 py-1 text-[11px] font-medium shadow-sm shadow-slate-950/5"; @@ -31,19 +31,19 @@ export const EMPTY_STATE_ICON_TONE_CLASSNAMES: Record = }; export const EMPTY_STATE_META_PILL_CLASSNAME = - "rounded-full border border-slate-200/80 bg-white/90 px-2 py-0.5 text-[10px] font-medium text-slate-500"; + "rounded-full border border-lime-200/80 bg-lime-50/50 px-2 py-0.5 text-[10px] font-medium text-slate-600"; export const EMPTY_STATE_PRESET_BUTTON_CLASSNAME = - "inline-flex items-center gap-2 rounded-full border border-slate-200/80 bg-white/92 px-3 py-1.5 text-[13px] text-slate-600 shadow-sm shadow-slate-950/5 transition-colors hover:border-slate-300 hover:bg-white hover:text-slate-900"; + "inline-flex items-center gap-2 rounded-full border border-lime-200/80 bg-lime-50/50 px-3 py-1.5 text-[13px] text-slate-600 shadow-sm shadow-slate-950/5 transition-colors hover:border-lime-300 hover:bg-white hover:text-slate-900"; export const EMPTY_STATE_SECONDARY_ACTION_BUTTON_CLASSNAME = - "h-8 w-full rounded-full border-slate-200/80 bg-white/88 text-xs text-slate-700 shadow-none transition-colors hover:border-slate-300 hover:bg-white"; + "h-8 w-full rounded-full border border-lime-200/80 bg-lime-50/50 text-xs text-slate-700 shadow-none transition-colors hover:border-lime-300 hover:bg-white"; export const EMPTY_STATE_PRIMARY_ACTION_BUTTON_CLASSNAME = "h-9 w-full rounded-full bg-slate-900 px-5 text-white shadow-sm shadow-slate-900/10 transition-colors hover:bg-slate-800 sm:w-auto"; export const EMPTY_STATE_RECOMMENDATION_CARD_CLASSNAME = - "group flex min-w-0 flex-col items-start gap-2 rounded-[20px] border border-slate-200/80 bg-[linear-gradient(180deg,rgba(255,255,255,0.97)_0%,rgba(248,250,252,0.9)_100%)] px-3.5 py-3.5 text-left shadow-sm shadow-slate-950/5 transition-colors hover:border-slate-300 hover:bg-white"; + "group flex min-w-0 flex-col items-start gap-2 rounded-[20px] border border-lime-200/80 bg-[linear-gradient(180deg,rgba(255,255,255,0.97)_0%,rgba(247,254,231,0.9)_100%)] px-3.5 py-3.5 text-left shadow-sm shadow-slate-950/5 transition-colors hover:border-lime-300 hover:bg-white"; export const EMPTY_STATE_PAGE_CONTAINER_CLASSNAME = "relative flex min-h-full flex-1 flex-col items-stretch justify-start overflow-y-auto bg-transparent px-2 pb-5 pt-[clamp(2px,0.6vw,6px)] md:px-3"; @@ -52,29 +52,28 @@ export const EMPTY_STATE_CONTENT_WRAPPER_CLASSNAME = "relative z-[1] mx-auto flex min-h-full w-full max-w-[1360px] flex-1 flex-col items-stretch gap-3"; export const EMPTY_STATE_THEME_TABS_CONTAINER_CLASSNAME = - "flex w-full max-w-full flex-nowrap justify-start gap-1.5 overflow-x-auto overflow-y-hidden rounded-[20px] border border-slate-200/80 bg-slate-50 p-[5px] shadow-[0_10px_24px_-22px_rgba(15,23,42,0.18)] [scrollbar-width:none] [&::-webkit-scrollbar]:hidden"; + "flex w-full max-w-full flex-nowrap justify-start gap-1.5 overflow-x-auto overflow-y-hidden rounded-[20px] border border-lime-200/80 bg-lime-50/50 p-[5px] shadow-[0_10px_24px_-22px_rgba(15,23,42,0.18)] [scrollbar-width:none] [&::-webkit-scrollbar]:hidden"; export const EMPTY_STATE_SELECT_TRIGGER_CLASSNAME = - "h-8 rounded-full border-slate-200/80 bg-white/92 px-3 text-xs text-slate-700 shadow-none transition-colors hover:border-slate-300 hover:bg-white focus:ring-1 focus:ring-slate-200"; + "h-8 rounded-full border-lime-200/80 bg-white/92 px-3 text-xs text-slate-700 shadow-none transition-colors hover:border-lime-300 hover:bg-white focus:ring-1 focus:ring-lime-200"; export const EMPTY_STATE_PASSIVE_BADGE_CLASSNAME = - "h-8 rounded-full border border-slate-200/80 bg-white/92 px-3 text-xs font-normal text-slate-600 shadow-none hover:border-slate-300 hover:bg-white hover:text-slate-900"; + "h-8 rounded-full border border-lime-200/80 bg-lime-50/50 px-3 text-xs font-normal text-slate-600 shadow-none hover:border-lime-300 hover:bg-white hover:text-slate-900"; export const EMPTY_STATE_ICON_TOOL_BUTTON_CLASSNAME = - "ml-1 h-8 w-8 rounded-full border-slate-200/80 bg-white text-slate-500 shadow-none transition-colors hover:border-slate-300 hover:bg-slate-50 hover:text-slate-700"; + "ml-1 h-8 w-8 rounded-full border-lime-200/80 bg-white text-slate-500 shadow-none transition-colors hover:border-lime-300 hover:bg-lime-50 hover:text-slate-700"; -const EMPTY_STATE_TOOL_TOGGLE_TONE_CLASSNAMES: Record = { - slate: - "border-slate-400 bg-slate-200 text-slate-900 shadow-[inset_0_0_0_1px_rgba(148,163,184,0.28)] hover:border-slate-400 hover:bg-slate-200 hover:text-slate-900", - sky: - "border-sky-300 bg-sky-50 text-sky-700 hover:border-sky-300 hover:bg-sky-50 hover:text-sky-700", - emerald: - "border-emerald-300 bg-emerald-50 text-emerald-700 hover:border-emerald-300 hover:bg-emerald-50 hover:text-emerald-700", - amber: - "border-amber-300 bg-amber-50 text-amber-700 hover:border-amber-300 hover:bg-amber-50 hover:text-amber-700", - lime: - "border-lime-300 bg-lime-50 text-lime-700 hover:border-lime-300 hover:bg-lime-50 hover:text-lime-700", -}; +const EMPTY_STATE_TOOL_TOGGLE_TONE_CLASSNAMES: Record = + { + slate: + "border-slate-400 bg-slate-200 text-slate-900 shadow-[inset_0_0_0_1px_rgba(148,163,184,0.28)] hover:border-slate-400 hover:bg-slate-200 hover:text-slate-900", + sky: "border-sky-300 bg-sky-50 text-sky-700 hover:border-sky-300 hover:bg-sky-50 hover:text-sky-700", + emerald: + "border-emerald-300 bg-emerald-50 text-emerald-700 hover:border-emerald-300 hover:bg-emerald-50 hover:text-emerald-700", + amber: + "border-amber-300 bg-amber-50 text-amber-700 hover:border-amber-300 hover:bg-amber-50 hover:text-amber-700", + lime: "border-lime-300 bg-lime-50 text-lime-700 hover:border-lime-300 hover:bg-lime-50 hover:text-lime-700", + }; export function getEmptyStateIconToolButtonClassName( enabled: boolean, @@ -92,8 +91,8 @@ export function getEmptyStateThemeTabClassName(active: boolean) { return [ "flex flex-none items-center gap-1.5 rounded-xl border px-3 py-[7px] text-xs font-medium leading-none transition-[background-color,border-color,color,box-shadow]", active - ? "border-slate-300 bg-white/95 text-slate-900 shadow-[0_10px_22px_-20px_rgba(15,23,42,0.24)]" - : "border-transparent text-slate-600 hover:border-slate-200/90 hover:bg-white/80 hover:text-slate-900", + ? "border-lime-300 bg-white/95 text-slate-900 shadow-[0_10px_22px_-20px_rgba(15,23,42,0.24)]" + : "border-transparent text-slate-600 hover:border-lime-200/90 hover:bg-white/80 hover:text-slate-900", ].join(" "); } @@ -101,7 +100,7 @@ export function getEmptyStateThemeTabIconClassName(active: boolean) { return [ "flex h-5 w-5 items-center justify-center rounded-full border text-[11px]", active - ? "border-slate-200 bg-white text-slate-700" + ? "border-lime-200 bg-white text-slate-700" : "border-transparent bg-transparent text-slate-400", ].join(" "); } diff --git a/src/components/agent/chat/components/generalWorkbenchContextData.ts b/src/components/agent/chat/components/generalWorkbenchContextData.ts index 238c5c722..7be36cb85 100644 --- a/src/components/agent/chat/components/generalWorkbenchContextData.ts +++ b/src/components/agent/chat/components/generalWorkbenchContextData.ts @@ -80,6 +80,7 @@ export function resolveGeneralWorkbenchSelectedSearchResult( return null; } return ( - searchContextItems.find((item) => item.id === selectedSearchResultId) || null + searchContextItems.find((item) => item.id === selectedSearchResultId) || + null ); } diff --git a/src/components/agent/chat/components/generalWorkbenchExecLogData.ts b/src/components/agent/chat/components/generalWorkbenchExecLogData.ts index 87e2d134f..d0a366f30 100644 --- a/src/components/agent/chat/components/generalWorkbenchExecLogData.ts +++ b/src/components/agent/chat/components/generalWorkbenchExecLogData.ts @@ -22,9 +22,7 @@ function truncate(text: string, max = 300): string { return ""; } const normalized = text.trim(); - return normalized.length > max - ? `${normalized.slice(0, max)}…` - : normalized; + return normalized.length > max ? `${normalized.slice(0, max)}…` : normalized; } function formatExecLogJson(value: unknown): string | undefined { @@ -161,7 +159,9 @@ function buildRunEntry( allowedTools: allowedTools.length > 0 ? allowedTools : undefined, whenToUse: skillDetail?.when_to_use?.trim() || undefined, artifactPaths: - group.artifactPaths.length > 0 ? [...group.artifactPaths] : undefined, + group.artifactPaths.length > 0 + ? [...group.artifactPaths] + : undefined, } : undefined; @@ -169,8 +169,14 @@ function buildRunEntry( group.artifactPaths.length > 0 ? `产物:${group.artifactPaths.map((path) => path.split("/").pop()).join("、")}` : undefined; - const durationLabel = group.logs.find((log) => log.durationLabel)?.durationLabel; - const meta = [sourceRef ? `技能标识:${sourceRef}` : null, detailSummary, artifactSummary] + const durationLabel = group.logs.find( + (log) => log.durationLabel, + )?.durationLabel; + const meta = [ + sourceRef ? `技能标识:${sourceRef}` : null, + detailSummary, + artifactSummary, + ] .filter((item): item is string => Boolean(item)) .join(" · "); @@ -221,7 +227,12 @@ export function buildGeneralWorkbenchExecLogEntries({ for (const toolCall of message.toolCalls || []) { entries.push( - buildToolCallEntry(message.id, toolCall, message.timestamp, toolCallIndex), + buildToolCallEntry( + message.id, + toolCall, + message.timestamp, + toolCallIndex, + ), ); toolCallIndex += 1; } @@ -248,14 +259,18 @@ export function buildGeneralWorkbenchExecLogEntries({ type: "task", typeLabel: "任务提交", content: group.label || group.taskType, - timestamp: latestTask?.createdAt ? new Date(latestTask.createdAt) : new Date(0), + timestamp: latestTask?.createdAt + ? new Date(latestTask.createdAt) + : new Date(0), status: "completed", }); } entries.sort((left, right) => { - const leftTime = left.timestamp instanceof Date ? left.timestamp.getTime() : 0; - const rightTime = right.timestamp instanceof Date ? right.timestamp.getTime() : 0; + const leftTime = + left.timestamp instanceof Date ? left.timestamp.getTime() : 0; + const rightTime = + right.timestamp instanceof Date ? right.timestamp.getTime() : 0; return leftTime - rightTime; }); @@ -271,9 +286,10 @@ export function filterGeneralWorkbenchExecLogEntries( } return entries.filter((entry) => { - const timestamp = entry.timestamp instanceof Date - ? entry.timestamp.getTime() - : new Date(entry.timestamp).getTime(); + const timestamp = + entry.timestamp instanceof Date + ? entry.timestamp.getTime() + : new Date(entry.timestamp).getTime(); return Number.isFinite(timestamp) && timestamp > clearedAt; }); } diff --git a/src/components/agent/chat/components/generalWorkbenchSidebarContract.ts b/src/components/agent/chat/components/generalWorkbenchSidebarContract.ts index 62bd32139..22171e23f 100644 --- a/src/components/agent/chat/components/generalWorkbenchSidebarContract.ts +++ b/src/components/agent/chat/components/generalWorkbenchSidebarContract.ts @@ -71,7 +71,8 @@ export interface GeneralWorkbenchSidebarExecLogContract { } export interface GeneralWorkbenchSidebarProps - extends GeneralWorkbenchSidebarShellContract, + extends + GeneralWorkbenchSidebarShellContract, GeneralWorkbenchSidebarWorkflowContract, GeneralWorkbenchSidebarContextContract, GeneralWorkbenchSidebarExecLogContract {} diff --git a/src/components/agent/chat/components/generalWorkbenchSidebarOrchestrationContract.ts b/src/components/agent/chat/components/generalWorkbenchSidebarOrchestrationContract.ts index d48d4367d..861ba345f 100644 --- a/src/components/agent/chat/components/generalWorkbenchSidebarOrchestrationContract.ts +++ b/src/components/agent/chat/components/generalWorkbenchSidebarOrchestrationContract.ts @@ -11,28 +11,31 @@ import type { GeneralWorkbenchAddTextContextAction, } from "./useGeneralWorkbenchContextPanelState"; -export interface GeneralWorkbenchSidebarContextOrchestrationInput - extends Omit { +export interface GeneralWorkbenchSidebarContextOrchestrationInput extends Omit< + BuildGeneralWorkbenchContextPanelPropsParams, + "contextPanelState" +> { onAddTextContext?: GeneralWorkbenchAddTextContextAction; onAddLinkContext?: GeneralWorkbenchAddLinkContextAction; onAddFileContext?: GeneralWorkbenchAddFileContextAction; } -export interface GeneralWorkbenchSidebarWorkflowOrchestrationInput - extends Omit< - BuildGeneralWorkbenchWorkflowPanelPropsParams, - | "creationTaskEventsCount" - | "isVersionMode" - | "onOpenArtifactWithDefaultApp" - | "onRevealArtifactInFinder" - | "workflowPanelState" - > { +export interface GeneralWorkbenchSidebarWorkflowOrchestrationInput extends Omit< + BuildGeneralWorkbenchWorkflowPanelPropsParams, + | "creationTaskEventsCount" + | "isVersionMode" + | "onOpenArtifactWithDefaultApp" + | "onRevealArtifactInFinder" + | "workflowPanelState" +> { activityLogs: SidebarActivityLog[]; creationTaskEvents: GeneralWorkbenchCreationTaskEvent[]; } -export interface GeneralWorkbenchSidebarExecLogOrchestrationInput - extends Omit { +export interface GeneralWorkbenchSidebarExecLogOrchestrationInput extends Omit< + BuildGeneralWorkbenchExecLogPropsParams, + "execLogState" +> { messages: Message[]; skillDetailMap: Record; } @@ -47,19 +50,17 @@ export interface GeneralWorkbenchSidebarOrchestrationInput { export type GeneralWorkbenchSidebarContextOrchestrationSource = GeneralWorkbenchSidebarContextOrchestrationInput; -export interface GeneralWorkbenchSidebarWorkflowOrchestrationSource - extends Omit< - GeneralWorkbenchSidebarWorkflowOrchestrationInput, - "creationTaskEvents" - > { +export interface GeneralWorkbenchSidebarWorkflowOrchestrationSource extends Omit< + GeneralWorkbenchSidebarWorkflowOrchestrationInput, + "creationTaskEvents" +> { creationTaskEvents?: GeneralWorkbenchCreationTaskEvent[]; } -export interface GeneralWorkbenchSidebarExecLogOrchestrationSource - extends Omit< - GeneralWorkbenchSidebarExecLogOrchestrationInput, - "messages" | "skillDetailMap" - > { +export interface GeneralWorkbenchSidebarExecLogOrchestrationSource extends Omit< + GeneralWorkbenchSidebarExecLogOrchestrationInput, + "messages" | "skillDetailMap" +> { messages?: Message[]; skillDetailMap?: Record; } diff --git a/src/components/agent/chat/components/generalWorkbenchSidebarShared.ts b/src/components/agent/chat/components/generalWorkbenchSidebarShared.ts index 6fe6e927d..28d6b4d20 100644 --- a/src/components/agent/chat/components/generalWorkbenchSidebarShared.ts +++ b/src/components/agent/chat/components/generalWorkbenchSidebarShared.ts @@ -28,7 +28,9 @@ export function formatGeneralWorkbenchActionErrorMessage( return `${prefix}:${detail}`; } -export async function writeGeneralWorkbenchClipboardText(text: string): Promise { +export async function writeGeneralWorkbenchClipboardText( + text: string, +): Promise { const value = text.trim(); if (!value) { return; diff --git a/src/components/agent/chat/components/generalWorkbenchWorkflowData.test.ts b/src/components/agent/chat/components/generalWorkbenchWorkflowData.test.ts index 550ec4886..241f522e9 100644 --- a/src/components/agent/chat/components/generalWorkbenchWorkflowData.test.ts +++ b/src/components/agent/chat/components/generalWorkbenchWorkflowData.test.ts @@ -48,7 +48,10 @@ describe("generalWorkbenchWorkflowData", () => { status: "completed", timeLabel: "10:31", runId: "run-1", - artifactPaths: ["content-posts/demo.md", "content-posts/demo-cover.png"], + artifactPaths: [ + "content-posts/demo.md", + "content-posts/demo-cover.png", + ], }, ])[0]?.artifactPaths, ).toEqual(["content-posts/demo.md", "content-posts/demo-cover.png"]); diff --git a/src/components/agent/chat/components/useGeneralWorkbenchContextPanelState.ts b/src/components/agent/chat/components/useGeneralWorkbenchContextPanelState.ts index 1480875a0..28f4d628b 100644 --- a/src/components/agent/chat/components/useGeneralWorkbenchContextPanelState.ts +++ b/src/components/agent/chat/components/useGeneralWorkbenchContextPanelState.ts @@ -85,14 +85,18 @@ export function useGeneralWorkbenchContextPanelState({ onAddLinkContext, onAddFileContext, }: UseGeneralWorkbenchContextPanelStateParams): GeneralWorkbenchContextPanelState { - const [selectedSearchResultId, setSelectedSearchResultId] = useState(null); + const [selectedSearchResultId, setSelectedSearchResultId] = useState< + string | null + >(null); const [addContextDialogOpen, setAddContextDialogOpen] = useState(false); const [addTextDialogOpen, setAddTextDialogOpen] = useState(false); const [addLinkDialogOpen, setAddLinkDialogOpen] = useState(false); const [contextDraftText, setContextDraftText] = useState(""); const [contextDraftLink, setContextDraftLink] = useState(""); const [contextCreateLoading, setContextCreateLoading] = useState(false); - const [contextCreateError, setContextCreateError] = useState(null); + const [contextCreateError, setContextCreateError] = useState( + null, + ); const [contextDropActive, setContextDropActive] = useState(false); const searchInputRef = useRef(null); @@ -212,15 +216,12 @@ export function useGeneralWorkbenchContextPanelState({ return; } - await runContextAction( - async () => { - await onAddFileContext({ - path: selected, - name: resolveGeneralWorkbenchFileNameFromPath(selected), - }); - }, - "已添加文件上下文", - ); + await runContextAction(async () => { + await onAddFileContext({ + path: selected, + name: resolveGeneralWorkbenchFileNameFromPath(selected), + }); + }, "已添加文件上下文"); } catch (error) { const nextError = formatGeneralWorkbenchActionErrorMessage( "读取文件失败", @@ -242,36 +243,32 @@ export function useGeneralWorkbenchContextPanelState({ const fileWithPath = file as File & { path?: string }; if (fileWithPath.path && onAddFileContext) { - await runContextAction( - async () => { - await onAddFileContext({ - path: fileWithPath.path || "", - name: file.name, - }); - }, - "已添加文件上下文", - ); + await runContextAction(async () => { + await onAddFileContext({ + path: fileWithPath.path || "", + name: file.name, + }); + }, "已添加文件上下文"); return; } if (!onAddTextContext) { - setContextCreateError("当前环境无法读取拖拽文件路径,请使用“上传文件”按钮"); + setContextCreateError( + "当前环境无法读取拖拽文件路径,请使用“上传文件”按钮", + ); return; } - await runContextAction( - async () => { - const content = await file.text(); - if (!content.trim()) { - throw new Error("文件内容为空"); - } - await onAddTextContext({ - content, - name: file.name, - }); - }, - "已添加文本上下文", - ); + await runContextAction(async () => { + const content = await file.text(); + if (!content.trim()) { + throw new Error("文件内容为空"); + } + await onAddTextContext({ + content, + name: file.name, + }); + }, "已添加文本上下文"); }, [onAddFileContext, onAddTextContext, runContextAction], ); @@ -286,14 +283,11 @@ export function useGeneralWorkbenchContextPanelState({ setContextCreateError("请输入文本内容"); return; } - await runContextAction( - async () => { - await onAddTextContext({ - content: normalizedText, - }); - }, - "已添加文本上下文", - ); + await runContextAction(async () => { + await onAddTextContext({ + content: normalizedText, + }); + }, "已添加文本上下文"); }, [contextDraftText, onAddTextContext, runContextAction]); const handleSubmitLinkContext = useCallback(async () => { @@ -306,14 +300,11 @@ export function useGeneralWorkbenchContextPanelState({ setContextCreateError("请输入网站链接"); return; } - await runContextAction( - async () => { - await onAddLinkContext({ - url: normalizedLink, - }); - }, - "已添加网站链接上下文", - ); + await runContextAction(async () => { + await onAddLinkContext({ + url: normalizedLink, + }); + }, "已添加网站链接上下文"); }, [contextDraftLink, onAddLinkContext, runContextAction]); return { diff --git a/src/components/agent/chat/components/useGeneralWorkbenchExecLogState.ts b/src/components/agent/chat/components/useGeneralWorkbenchExecLogState.ts index 0cca24630..cc1dff592 100644 --- a/src/components/agent/chat/components/useGeneralWorkbenchExecLogState.ts +++ b/src/components/agent/chat/components/useGeneralWorkbenchExecLogState.ts @@ -44,7 +44,8 @@ export function useGeneralWorkbenchExecLogState({ ); const visibleExecLogEntries = useMemo( - () => filterGeneralWorkbenchExecLogEntries(execLogEntries, execLogClearedAt), + () => + filterGeneralWorkbenchExecLogEntries(execLogEntries, execLogClearedAt), [execLogClearedAt, execLogEntries], ); diff --git a/src/components/agent/chat/components/useGeneralWorkbenchSidebarOrchestration.ts b/src/components/agent/chat/components/useGeneralWorkbenchSidebarOrchestration.ts index f8c4ce958..001392c26 100644 --- a/src/components/agent/chat/components/useGeneralWorkbenchSidebarOrchestration.ts +++ b/src/components/agent/chat/components/useGeneralWorkbenchSidebarOrchestration.ts @@ -31,7 +31,8 @@ export function useGeneralWorkbenchSidebarOrchestration({ }: UseGeneralWorkbenchSidebarOrchestrationParams): GeneralWorkbenchSidebarOrchestration { const { isVersionMode, context, workflow, execLog } = input; const branchCount = workflow.branchItems.length; - const runDetailSessionId = workflow.activeRunDetail?.session_id?.trim() || null; + const runDetailSessionId = + workflow.activeRunDetail?.session_id?.trim() || null; const { handleRevealArtifactInFinder, handleOpenArtifactWithDefaultApp } = useGeneralWorkbenchArtifactActions({ runDetailSessionId, diff --git a/src/components/agent/chat/components/useGeneralWorkbenchWorkflowPanelState.ts b/src/components/agent/chat/components/useGeneralWorkbenchWorkflowPanelState.ts index 2b804bf7b..b4be4fc7a 100644 --- a/src/components/agent/chat/components/useGeneralWorkbenchWorkflowPanelState.ts +++ b/src/components/agent/chat/components/useGeneralWorkbenchWorkflowPanelState.ts @@ -21,7 +21,9 @@ interface UseGeneralWorkbenchWorkflowPanelStateParams { export interface GeneralWorkbenchWorkflowPanelState { completedSteps: number; progressPercent: number; - groupedActivityLogs: ReturnType; + groupedActivityLogs: ReturnType< + typeof buildGeneralWorkbenchActivityLogGroups + >; groupedCreationTaskEvents: ReturnType< typeof buildGeneralWorkbenchCreationTaskGroups >; @@ -49,7 +51,9 @@ export function useGeneralWorkbenchWorkflowPanelState({ ); const progressPercent = - workflowSteps.length > 0 ? (completedSteps / workflowSteps.length) * 100 : 0; + workflowSteps.length > 0 + ? (completedSteps / workflowSteps.length) * 100 + : 0; const groupedActivityLogs = useMemo( () => buildGeneralWorkbenchActivityLogGroups(activityLogs), diff --git a/src/components/agent/chat/hooks/agentChatActionState.ts b/src/components/agent/chat/hooks/agentChatActionState.ts index 7946e936f..1a0cd87b2 100644 --- a/src/components/agent/chat/hooks/agentChatActionState.ts +++ b/src/components/agent/chat/hooks/agentChatActionState.ts @@ -1,6 +1,9 @@ import type { Dispatch, SetStateAction } from "react"; import type { ActionRequired, Message } from "../types"; -import { appendActionRequiredToParts, resolveActionPromptKey } from "./agentChatCoreUtils"; +import { + appendActionRequiredToParts, + resolveActionPromptKey, +} from "./agentChatCoreUtils"; interface UpsertAssistantActionRequestOptions { assistantMsgId: string; diff --git a/src/components/agent/chat/hooks/agentChatSendMessage.test.ts b/src/components/agent/chat/hooks/agentChatSendMessage.test.ts index 8c8d40126..3da8facb1 100644 --- a/src/components/agent/chat/hooks/agentChatSendMessage.test.ts +++ b/src/components/agent/chat/hooks/agentChatSendMessage.test.ts @@ -111,7 +111,9 @@ describe("createAgentChatSendMessage", () => { await sendMessage("/review src-tauri", [], false, false, false); expect(rawSendMessage).toHaveBeenCalledTimes(1); - expect(rawSendMessage.mock.calls[0]?.[0]).toContain("请对以下对象进行代码审查"); + expect(rawSendMessage.mock.calls[0]?.[0]).toContain( + "请对以下对象进行代码审查", + ); expect(rawSendMessage.mock.calls[0]?.[0]).toContain("src-tauri"); expect(listSlashEntryUsage()).toEqual([ expect.objectContaining({ diff --git a/src/components/agent/chat/hooks/agentChatSendMessage.ts b/src/components/agent/chat/hooks/agentChatSendMessage.ts index e20d939d5..fdb59f6ec 100644 --- a/src/components/agent/chat/hooks/agentChatSendMessage.ts +++ b/src/components/agent/chat/hooks/agentChatSendMessage.ts @@ -1,13 +1,7 @@ import type { CodexSlashStatusSnapshot } from "../commands"; -import { - executeCodexSlashCommand, - parseCodexSlashCommand, -} from "../commands"; +import { executeCodexSlashCommand, parseCodexSlashCommand } from "../commands"; import { recordSlashEntryUsage } from "../skill-selection/slashEntryUsage"; -import type { - ClearMessagesOptions, - SendMessageFn, -} from "./agentChatShared"; +import type { ClearMessagesOptions, SendMessageFn } from "./agentChatShared"; interface CreateAgentChatSendMessageOptions { baseStatusSnapshot: CodexSlashStatusSnapshot; diff --git a/src/components/agent/chat/hooks/agentChatShared.test.ts b/src/components/agent/chat/hooks/agentChatShared.test.ts index 63f99a225..74ffb307b 100644 --- a/src/components/agent/chat/hooks/agentChatShared.test.ts +++ b/src/components/agent/chat/hooks/agentChatShared.test.ts @@ -87,9 +87,8 @@ describe("agentChatShared", () => { }); it("应优先展示待处理请求的摘要", () => { - const messages = createPendingActionMessages( - "请先确认发布标题后继续执行。", - ); + const messages = + createPendingActionMessages("请先确认发布标题后继续执行。"); expect(extractTaskPreviewFromMessages(messages)).toBe( "请先确认发布标题后继续执行。", diff --git a/src/components/agent/chat/hooks/agentChatShared.ts b/src/components/agent/chat/hooks/agentChatShared.ts index da9f17f39..56d4ebf4c 100644 --- a/src/components/agent/chat/hooks/agentChatShared.ts +++ b/src/components/agent/chat/hooks/agentChatShared.ts @@ -145,24 +145,32 @@ function normalizeTaskPreviewText(value: string) { } function extractMessageTextContent(message: Message): string { - const sanitizedContent = sanitizeMessageTextForPreview(message.content || "", { - role: message.role, - hasImages: Array.isArray(message.images) && message.images.length > 0, - }); + const sanitizedContent = sanitizeMessageTextForPreview( + message.content || "", + { + role: message.role, + hasImages: Array.isArray(message.images) && message.images.length > 0, + }, + ); if (sanitizedContent) { return normalizeTaskPreviewText(sanitizedContent); } const partText = message.contentParts ?.filter( - (part): part is Extract<(typeof message.contentParts)[number], { type: "text" | "thinking" }> => - part.type === "text" || part.type === "thinking", + ( + part, + ): part is Extract< + (typeof message.contentParts)[number], + { type: "text" | "thinking" } + > => part.type === "text" || part.type === "thinking", ) .map((part) => part.type === "text" ? sanitizeMessageTextForPreview(part.text, { role: message.role, - hasImages: Array.isArray(message.images) && message.images.length > 0, + hasImages: + Array.isArray(message.images) && message.images.length > 0, }) : part.text, ) @@ -189,9 +197,9 @@ function extractToolCallPreview(message: Message): string { } function extractActionRequestPreview(message: Message): string { - const pendingAction = [...(message.actionRequests || [])].reverse().find( - (item) => item.status !== "submitted", - ); + const pendingAction = [...(message.actionRequests || [])] + .reverse() + .find((item) => item.status !== "submitted"); if (!pendingAction) { return ""; } @@ -201,9 +209,7 @@ function extractActionRequestPreview(message: Message): string { .join(" "); return normalizeTaskPreviewText( - pendingAction.prompt || - questionText || - "等待你确认或补充信息后继续执行。", + pendingAction.prompt || questionText || "等待你确认或补充信息后继续执行。", ); } @@ -351,8 +357,13 @@ export function buildLiveTaskSnapshot(params: { queuedTurnCount?: number; workspaceError: boolean; }): LiveTaskSnapshot { - const { messages, isSending, pendingActionCount, queuedTurnCount, workspaceError } = - params; + const { + messages, + isSending, + pendingActionCount, + queuedTurnCount, + workspaceError, + } = params; const lastMessage = messages[messages.length - 1]; const preview = extractTaskPreviewFromMessages(messages); const taskState = deriveTaskLiveState({ @@ -368,8 +379,7 @@ export function buildLiveTaskSnapshot(params: { messagesCount: messages.length, status: taskState.status, statusReason: taskState.statusReason, - lastPreview: - preview || "等待你补充任务需求后开始执行。", + lastPreview: preview || "等待你补充任务需求后开始执行。", hasUnread: false, }; } diff --git a/src/components/agent/chat/hooks/agentChatStorage.ts b/src/components/agent/chat/hooks/agentChatStorage.ts index fce6ecf97..cf8b9569a 100644 --- a/src/components/agent/chat/hooks/agentChatStorage.ts +++ b/src/components/agent/chat/hooks/agentChatStorage.ts @@ -197,11 +197,7 @@ export const getAccessModeStorageKey = ( }; export const normalizeAccessMode = (value: unknown): AgentAccessMode => { - if ( - value === "read-only" || - value === "current" || - value === "full-access" - ) { + if (value === "read-only" || value === "current" || value === "full-access") { return value; } return DEFAULT_AGENT_ACCESS_MODE; diff --git a/src/components/agent/chat/hooks/agentProjectStorage.test.tsx b/src/components/agent/chat/hooks/agentProjectStorage.test.tsx index 70ec718e7..f73875e9a 100644 --- a/src/components/agent/chat/hooks/agentProjectStorage.test.tsx +++ b/src/components/agent/chat/hooks/agentProjectStorage.test.tsx @@ -104,7 +104,9 @@ describe("agentProjectStorage", () => { }); expect(harness.getValue().projectId).toBe("project-updated"); - expect(loadPersistedProjectId(LAST_PROJECT_ID_KEY)).toBe("project-updated"); + expect(loadPersistedProjectId(LAST_PROJECT_ID_KEY)).toBe( + "project-updated", + ); } finally { harness.unmount(); } diff --git a/src/components/agent/chat/hooks/agentProjectStorage.ts b/src/components/agent/chat/hooks/agentProjectStorage.ts index 189b2344c..f6272e595 100644 --- a/src/components/agent/chat/hooks/agentProjectStorage.ts +++ b/src/components/agent/chat/hooks/agentProjectStorage.ts @@ -4,7 +4,9 @@ import { normalizeProjectId } from "../utils/topicProjectResolution"; export const LAST_PROJECT_ID_KEY = "agent_last_project_id"; export const SESSION_WORKSPACE_STORAGE_KEY_PREFIX = "agent_session_workspace_"; -export function getSessionWorkspaceStorageKey(sessionId: string): string | null { +export function getSessionWorkspaceStorageKey( + sessionId: string, +): string | null { const normalizedSessionId = sessionId.trim(); if (!normalizedSessionId) { return null; diff --git a/src/components/agent/chat/hooks/agentRuntimeAdapter.ts b/src/components/agent/chat/hooks/agentRuntimeAdapter.ts index 9eadf3394..9ad4062fb 100644 --- a/src/components/agent/chat/hooks/agentRuntimeAdapter.ts +++ b/src/components/agent/chat/hooks/agentRuntimeAdapter.ts @@ -50,7 +50,9 @@ export interface AgentRuntimeAdapter { ): Promise; listSessions(): Promise; getSession(sessionId: string): Promise; - getSessionReadModel(sessionId: string): Promise; + getSessionReadModel( + sessionId: string, + ): Promise; replayRequest( sessionId: string, requestId: string, diff --git a/src/components/agent/chat/hooks/agentSessionRefresh.test.ts b/src/components/agent/chat/hooks/agentSessionRefresh.test.ts index b1d6ca02a..db039db79 100644 --- a/src/components/agent/chat/hooks/agentSessionRefresh.test.ts +++ b/src/components/agent/chat/hooks/agentSessionRefresh.test.ts @@ -76,10 +76,7 @@ describe("agentSessionRefresh", () => { preserveExecutionStrategyOnMissingDetail: true, }, ); - expect(markSynced).toHaveBeenCalledWith( - "session-1", - "code_orchestrated", - ); + expect(markSynced).toHaveBeenCalledWith("session-1", "code_orchestrated"); }); it("刷新 read model 时应在会话仍匹配时应用 snapshot", async () => { diff --git a/src/components/agent/chat/hooks/agentSessionState.ts b/src/components/agent/chat/hooks/agentSessionState.ts index 87f763e04..0b552197e 100644 --- a/src/components/agent/chat/hooks/agentSessionState.ts +++ b/src/components/agent/chat/hooks/agentSessionState.ts @@ -154,10 +154,7 @@ export function buildHydratedAgentSessionSnapshot( snapshot: { sessionId: syncSessionId ? topicId : currentSessionId, messages: shouldPreserveExistingTimeline - ? mergeHydratedMessagesWithLocalState( - currentMessages, - hydratedMessages, - ) + ? mergeHydratedMessagesWithLocalState(currentMessages, hydratedMessages) : hydratedMessages, threadTurns: nextThreadTurns, threadItems: nextThreadItems, diff --git a/src/components/agent/chat/hooks/agentStreamCompaction.ts b/src/components/agent/chat/hooks/agentStreamCompaction.ts index abf1d504b..770a0c12f 100644 --- a/src/components/agent/chat/hooks/agentStreamCompaction.ts +++ b/src/components/agent/chat/hooks/agentStreamCompaction.ts @@ -1,8 +1,15 @@ import type { Dispatch, MutableRefObject, SetStateAction } from "react"; -import { parseAgentEvent, type AgentThreadItem, type AgentThreadTurn } from "@/lib/api/agentProtocol"; +import { + parseAgentEvent, + type AgentThreadItem, + type AgentThreadTurn, +} from "@/lib/api/agentProtocol"; import type { AgentRuntimeAdapter } from "./agentRuntimeAdapter"; import type { ActiveStreamState } from "./agentStreamSubmissionLifecycle"; -import { upsertThreadItemState, upsertThreadTurnState } from "./agentThreadState"; +import { + upsertThreadItemState, + upsertThreadTurnState, +} from "./agentThreadState"; import { resolveRuntimeWarningToastPresentation } from "./runtimeWarningPresentation"; interface AgentStreamCompactionNotifications { @@ -148,7 +155,8 @@ export async function runAgentStreamCompaction( break; } case "error": - notifiedErrorMessage = String(data.message ?? "").trim() || "未知错误"; + notifiedErrorMessage = + String(data.message ?? "").trim() || "未知错误"; notify.error(`压缩上下文失败: ${notifiedErrorMessage}`); clearActiveStreamIfMatch(eventName); disposeListener(); diff --git a/src/components/agent/chat/hooks/agentStreamFlowControl.test.ts b/src/components/agent/chat/hooks/agentStreamFlowControl.test.ts index eaf8227c3..492d50aa1 100644 --- a/src/components/agent/chat/hooks/agentStreamFlowControl.test.ts +++ b/src/components/agent/chat/hooks/agentStreamFlowControl.test.ts @@ -122,21 +122,36 @@ describe("agentStreamFlowControl", () => { } as never, removeStreamListener, refreshSessionReadModel, - setQueuedTurns: createStateSetter(() => queuedTurns, (value) => { - queuedTurns = value; - }), - setThreadItems: createStateSetter(() => threadItems, (value) => { - threadItems = value; - }), - setThreadTurns: createStateSetter(() => threadTurns, (value) => { - threadTurns = value; - }), - setCurrentTurnId: createStateSetter(() => currentTurnId, (value) => { - currentTurnId = value; - }), - setMessages: createStateSetter(() => messages, (value) => { - messages = value; - }), + setQueuedTurns: createStateSetter( + () => queuedTurns, + (value) => { + queuedTurns = value; + }, + ), + setThreadItems: createStateSetter( + () => threadItems, + (value) => { + threadItems = value; + }, + ), + setThreadTurns: createStateSetter( + () => threadTurns, + (value) => { + threadTurns = value; + }, + ), + setCurrentTurnId: createStateSetter( + () => currentTurnId, + (value) => { + currentTurnId = value; + }, + ), + setMessages: createStateSetter( + () => messages, + (value) => { + messages = value; + }, + ), setActiveStream: (next) => { activeStream = next as never; }, @@ -182,9 +197,12 @@ describe("agentStreamFlowControl", () => { queuedTurnId: "queued-1", sessionIdRef, refreshSessionReadModel, - setQueuedTurns: createStateSetter(() => queuedTurns, (value) => { - queuedTurns = value; - }), + setQueuedTurns: createStateSetter( + () => queuedTurns, + (value) => { + queuedTurns = value; + }, + ), notify, }), ).resolves.toBe(true); @@ -208,9 +226,12 @@ describe("agentStreamFlowControl", () => { queuedTurnId: "queued-1", sessionIdRef, refreshSessionReadModel, - setQueuedTurns: createStateSetter(() => queuedTurns, (value) => { - queuedTurns = value; - }), + setQueuedTurns: createStateSetter( + () => queuedTurns, + (value) => { + queuedTurns = value; + }, + ), notify, }), ).resolves.toBe(true); diff --git a/src/components/agent/chat/hooks/agentStreamFlowControl.ts b/src/components/agent/chat/hooks/agentStreamFlowControl.ts index 06df7fea5..f67cd0459 100644 --- a/src/components/agent/chat/hooks/agentStreamFlowControl.ts +++ b/src/components/agent/chat/hooks/agentStreamFlowControl.ts @@ -5,7 +5,10 @@ import type { Message } from "../types"; import type { ActiveStreamState } from "./agentStreamSubmissionLifecycle"; import type { AgentRuntimeAdapter } from "./agentRuntimeAdapter"; import { updateMessageArtifactsStatus } from "../utils/messageArtifacts"; -import { removeThreadItemState, removeThreadTurnState } from "./agentThreadState"; +import { + removeThreadItemState, + removeThreadTurnState, +} from "./agentThreadState"; interface AgentStreamFlowNotify { info: (message: string) => void; @@ -47,7 +50,10 @@ interface PromoteQueuedTurnOptions extends QueueActionOptions { onError?: (error: unknown) => void; } -interface ResumeThreadOptions extends Omit { +interface ResumeThreadOptions extends Omit< + QueueActionOptions, + "setQueuedTurns" +> { runtime: Pick; onError?: (error: unknown) => void; } @@ -147,7 +153,10 @@ export async function removeQueuedAgentTurn(options: RemoveQueuedTurnOptions) { } try { - const removed = await runtime.removeQueuedTurn(activeSessionId, queuedTurnId); + const removed = await runtime.removeQueuedTurn( + activeSessionId, + queuedTurnId, + ); if (removed) { setQueuedTurns((prev) => removeQueuedTurnFromState(prev, queuedTurnId)); } @@ -161,7 +170,9 @@ export async function removeQueuedAgentTurn(options: RemoveQueuedTurnOptions) { } } -export async function promoteQueuedAgentTurn(options: PromoteQueuedTurnOptions) { +export async function promoteQueuedAgentTurn( + options: PromoteQueuedTurnOptions, +) { const { runtime, queuedTurnId, @@ -179,7 +190,10 @@ export async function promoteQueuedAgentTurn(options: PromoteQueuedTurnOptions) setQueuedTurns((prev) => removeQueuedTurnFromState(prev, queuedTurnId)); try { - const promoted = await runtime.promoteQueuedTurn(activeSessionId, queuedTurnId); + const promoted = await runtime.promoteQueuedTurn( + activeSessionId, + queuedTurnId, + ); await refreshSessionReadModel(activeSessionId); if (!promoted) { return false; diff --git a/src/components/agent/chat/hooks/agentStreamPreparedSendDispatch.test.ts b/src/components/agent/chat/hooks/agentStreamPreparedSendDispatch.test.ts index d89617dc5..4028b823c 100644 --- a/src/components/agent/chat/hooks/agentStreamPreparedSendDispatch.test.ts +++ b/src/components/agent/chat/hooks/agentStreamPreparedSendDispatch.test.ts @@ -55,8 +55,8 @@ const preparedSend: PreparedAgentStreamUserInputSend = { describe("agentStreamPreparedSendDispatch", () => { function createEnv(): AgentStreamPreparedSendEnv { - const runPreparedSubmit = vi.fn( - async (task: () => Promise) => task(), + const runPreparedSubmit = vi.fn(async (task: () => Promise) => + task(), ) as unknown as AgentStreamPreparedSendEnv["runPreparedSubmit"]; return { runtime: {} as never, diff --git a/src/components/agent/chat/hooks/agentStreamPreparedSendEnv.test.ts b/src/components/agent/chat/hooks/agentStreamPreparedSendEnv.test.ts index 7d6ceee19..ab22c368c 100644 --- a/src/components/agent/chat/hooks/agentStreamPreparedSendEnv.test.ts +++ b/src/components/agent/chat/hooks/agentStreamPreparedSendEnv.test.ts @@ -45,8 +45,7 @@ describe("createAgentStreamPreparedSendEnv", () => { setExecutionRuntime: noopDispatch(), setQueuedTurns: noopDispatch(), setPendingActions: noopDispatch(), - setWorkspacePathMissing: - noopDispatch(), + setWorkspacePathMissing: noopDispatch(), setIsSending: noopDispatch(), playToolcallSound: () => undefined, playTypewriterSound: () => undefined, diff --git a/src/components/agent/chat/hooks/agentStreamPreparedSendEnv.ts b/src/components/agent/chat/hooks/agentStreamPreparedSendEnv.ts index b9bc7037c..ec08748ea 100644 --- a/src/components/agent/chat/hooks/agentStreamPreparedSendEnv.ts +++ b/src/components/agent/chat/hooks/agentStreamPreparedSendEnv.ts @@ -71,11 +71,10 @@ export interface AgentStreamPreparedSendEnv { appendThinkingToParts: AppendThinkingToPartsFn; } -export interface CreateAgentStreamPreparedSendEnvOptions - extends Omit< - AgentStreamPreparedSendEnv, - "getQueuedTurnsCount" | "isThreadBusy" - > { +export interface CreateAgentStreamPreparedSendEnvOptions extends Omit< + AgentStreamPreparedSendEnv, + "getQueuedTurnsCount" | "isThreadBusy" +> { queuedTurnsCount: number; threadBusy: boolean; } diff --git a/src/components/agent/chat/hooks/agentStreamSend.ts b/src/components/agent/chat/hooks/agentStreamSend.ts index 1e1d99aef..f9cc47035 100644 --- a/src/components/agent/chat/hooks/agentStreamSend.ts +++ b/src/components/agent/chat/hooks/agentStreamSend.ts @@ -1,4 +1,7 @@ -import type { AutoContinueRequestPayload, AsterExecutionStrategy } from "@/lib/api/agentRuntime"; +import type { + AutoContinueRequestPayload, + AsterExecutionStrategy, +} from "@/lib/api/agentRuntime"; import type { MessageImage } from "../types"; import type { SendMessageOptions } from "./agentChatShared"; import { dispatchPreparedAgentStreamSend } from "./agentStreamPreparedSendDispatch"; diff --git a/src/components/agent/chat/hooks/agentStreamSlashSkillPreflight.test.ts b/src/components/agent/chat/hooks/agentStreamSlashSkillPreflight.test.ts index bcb5e146d..0bfe72666 100644 --- a/src/components/agent/chat/hooks/agentStreamSlashSkillPreflight.test.ts +++ b/src/components/agent/chat/hooks/agentStreamSlashSkillPreflight.test.ts @@ -2,13 +2,11 @@ import { beforeEach, describe, expect, it, vi } from "vitest"; import type { PreparedAgentStreamUserInputSend } from "./agentStreamUserInputSendPreparation"; import { maybeHandleSlashSkillBeforeSend } from "./agentStreamSlashSkillPreflight"; -const { - mockParseSkillSlashCommand, - mockTryExecuteSlashSkillCommand, -} = vi.hoisted(() => ({ - mockParseSkillSlashCommand: vi.fn(), - mockTryExecuteSlashSkillCommand: vi.fn(), -})); +const { mockParseSkillSlashCommand, mockTryExecuteSlashSkillCommand } = + vi.hoisted(() => ({ + mockParseSkillSlashCommand: vi.fn(), + mockTryExecuteSlashSkillCommand: vi.fn(), + })); vi.mock("./skillCommand", () => ({ parseSkillSlashCommand: (...args: unknown[]) => diff --git a/src/components/agent/chat/hooks/agentStreamSlashSkillPreflight.ts b/src/components/agent/chat/hooks/agentStreamSlashSkillPreflight.ts index 9d2c02395..8150ca992 100644 --- a/src/components/agent/chat/hooks/agentStreamSlashSkillPreflight.ts +++ b/src/components/agent/chat/hooks/agentStreamSlashSkillPreflight.ts @@ -104,7 +104,9 @@ export async function maybeHandleSlashSkillBeforeSend( assistantMsgId: id, eventName: skillEventName, sessionId: - env.activeStreamRef.current?.sessionId || env.sessionIdRef.current || "", + env.activeStreamRef.current?.sessionId || + env.sessionIdRef.current || + "", }); }, setStreamUnlisten: (unlistenFn) => { diff --git a/src/components/agent/chat/hooks/agentStreamSubmissionLifecycle.test.ts b/src/components/agent/chat/hooks/agentStreamSubmissionLifecycle.test.ts index 2386c2f56..7384b060c 100644 --- a/src/components/agent/chat/hooks/agentStreamSubmissionLifecycle.test.ts +++ b/src/components/agent/chat/hooks/agentStreamSubmissionLifecycle.test.ts @@ -10,7 +10,9 @@ import { function createStateSetter(getValue: () => T, setValue: (value: T) => void) { return (next: T | ((prev: T) => T)) => { - setValue(typeof next === "function" ? (next as (prev: T) => T)(getValue()) : next); + setValue( + typeof next === "function" ? (next as (prev: T) => T)(getValue()) : next, + ); }; } @@ -48,21 +50,36 @@ describe("agentStreamSubmissionLifecycle", () => { setActiveStream: (next) => { activeStream = next; }, - setMessages: createStateSetter(() => messages, (value) => { - messages = value; - }), - setQueuedTurns: createStateSetter(() => queuedTurns, (value) => { - queuedTurns = value; - }), - setThreadItems: createStateSetter(() => threadItems, (value) => { - threadItems = value; - }), - setThreadTurns: createStateSetter(() => threadTurns, (value) => { - threadTurns = value; - }), - setCurrentTurnId: createStateSetter(() => currentTurnId, (value) => { - currentTurnId = value; - }), + setMessages: createStateSetter( + () => messages, + (value) => { + messages = value; + }, + ), + setQueuedTurns: createStateSetter( + () => queuedTurns, + (value) => { + queuedTurns = value; + }, + ), + setThreadItems: createStateSetter( + () => threadItems, + (value) => { + threadItems = value; + }, + ), + setThreadTurns: createStateSetter( + () => threadTurns, + (value) => { + threadTurns = value; + }, + ), + setCurrentTurnId: createStateSetter( + () => currentTurnId, + (value) => { + currentTurnId = value; + }, + ), }); expect(threadTurns).toHaveLength(1); @@ -120,21 +137,36 @@ describe("agentStreamSubmissionLifecycle", () => { initialThreadId: "local-thread:assistant-2", listenerMapRef: { current: new Map() }, setActiveStream: () => {}, - setMessages: createStateSetter(() => messages, (value) => { - messages = value; - }), - setQueuedTurns: createStateSetter(() => queuedTurns, (value) => { - queuedTurns = value; - }), - setThreadItems: createStateSetter(() => threadItems, (value) => { - threadItems = value; - }), - setThreadTurns: createStateSetter(() => threadTurns, (value) => { - threadTurns = value; - }), - setCurrentTurnId: createStateSetter(() => currentTurnId, (value) => { - currentTurnId = value; - }), + setMessages: createStateSetter( + () => messages, + (value) => { + messages = value; + }, + ), + setQueuedTurns: createStateSetter( + () => queuedTurns, + (value) => { + queuedTurns = value; + }, + ), + setThreadItems: createStateSetter( + () => threadItems, + (value) => { + threadItems = value; + }, + ), + setThreadTurns: createStateSetter( + () => threadTurns, + (value) => { + threadTurns = value; + }, + ), + setCurrentTurnId: createStateSetter( + () => currentTurnId, + (value) => { + currentTurnId = value; + }, + ), }); lifecycle.markOptimisticFailure("发送失败"); diff --git a/src/components/agent/chat/hooks/agentStreamSubmissionLifecycle.ts b/src/components/agent/chat/hooks/agentStreamSubmissionLifecycle.ts index 730f8c0f5..66e1215a3 100644 --- a/src/components/agent/chat/hooks/agentStreamSubmissionLifecycle.ts +++ b/src/components/agent/chat/hooks/agentStreamSubmissionLifecycle.ts @@ -126,7 +126,8 @@ export function createAgentStreamSubmissionLifecycle( setMessages((prev) => prev.filter( (msg) => - msg.id !== assistantMsgId && (userMsgId ? msg.id !== userMsgId : true), + msg.id !== assistantMsgId && + (userMsgId ? msg.id !== userMsgId : true), ), ); }; diff --git a/src/components/agent/chat/hooks/agentStreamSubmitDraft.test.ts b/src/components/agent/chat/hooks/agentStreamSubmitDraft.test.ts index 8597101cc..e21780ddf 100644 --- a/src/components/agent/chat/hooks/agentStreamSubmitDraft.test.ts +++ b/src/components/agent/chat/hooks/agentStreamSubmitDraft.test.ts @@ -5,7 +5,9 @@ import { prepareAgentStreamSubmitDraft } from "./agentStreamSubmitDraft"; function createStateSetter(getValue: () => T, setValue: (value: T) => void) { return (next: T | ((prev: T) => T)) => { - setValue(typeof next === "function" ? (next as (prev: T) => T)(getValue()) : next); + setValue( + typeof next === "function" ? (next as (prev: T) => T)(getValue()) : next, + ); }; } @@ -24,12 +26,18 @@ describe("agentStreamSubmitDraft", () => { effectiveExecutionStrategy: "react", webSearch: true, thinking: true, - setMessages: createStateSetter(() => messages, (value) => { - messages = value; - }), - setIsSending: createStateSetter(() => isSending, (value) => { - isSending = value; - }), + setMessages: createStateSetter( + () => messages, + (value) => { + messages = value; + }, + ), + setIsSending: createStateSetter( + () => isSending, + (value) => { + isSending = value; + }, + ), }); expect(messages).toHaveLength(2); @@ -72,12 +80,18 @@ describe("agentStreamSubmitDraft", () => { effectiveExecutionStrategy: "code_orchestrated", webSearch: false, thinking: true, - setMessages: createStateSetter(() => messages, (value) => { - messages = value; - }), - setIsSending: createStateSetter(() => isSending, (value) => { - isSending = value; - }), + setMessages: createStateSetter( + () => messages, + (value) => { + messages = value; + }, + ), + setIsSending: createStateSetter( + () => isSending, + (value) => { + isSending = value; + }, + ), }); expect(messages).toHaveLength(1); @@ -106,12 +120,18 @@ describe("agentStreamSubmitDraft", () => { effectiveExecutionStrategy: "react", webSearch: false, thinking: false, - setMessages: createStateSetter(() => messages, (value) => { - messages = value; - }), - setIsSending: createStateSetter(() => isSending, (value) => { - isSending = value; - }), + setMessages: createStateSetter( + () => messages, + (value) => { + messages = value; + }, + ), + setIsSending: createStateSetter( + () => isSending, + (value) => { + isSending = value; + }, + ), }); expect(messages[0]).toMatchObject({ diff --git a/src/components/agent/chat/hooks/agentStreamSubmitExecution.test.ts b/src/components/agent/chat/hooks/agentStreamSubmitExecution.test.ts index 86c2aa082..1ec28a690 100644 --- a/src/components/agent/chat/hooks/agentStreamSubmitExecution.test.ts +++ b/src/components/agent/chat/hooks/agentStreamSubmitExecution.test.ts @@ -86,7 +86,8 @@ describe("agentStreamSubmitExecution", () => { playToolcallSound: () => {}, playTypewriterSound: () => {}, }, - appendThinkingToParts: (parts: NonNullable) => parts, + appendThinkingToParts: (parts: NonNullable) => + parts, setMessages: noopDispatch(), setIsSending: noopDispatch(), setPendingActions: noopDispatch(), diff --git a/src/components/agent/chat/hooks/agentStreamSubmitFailure.ts b/src/components/agent/chat/hooks/agentStreamSubmitFailure.ts index 98ae34c97..6df124522 100644 --- a/src/components/agent/chat/hooks/agentStreamSubmitFailure.ts +++ b/src/components/agent/chat/hooks/agentStreamSubmitFailure.ts @@ -9,7 +9,10 @@ import { } from "../utils/agentRuntimeStatus"; import { resolveAgentRuntimeErrorPresentation } from "../utils/agentRuntimeErrorPresentation"; import { isWorkspacePathErrorMessage } from "./agentChatCoreUtils"; -import type { ActiveStreamState, StreamRequestState } from "./agentStreamSubmissionLifecycle"; +import type { + ActiveStreamState, + StreamRequestState, +} from "./agentStreamSubmissionLifecycle"; import type { WorkspacePathMissingState } from "./agentChatShared"; interface StreamObserver { diff --git a/src/components/agent/chat/hooks/agentStreamTurnEventBinding.ts b/src/components/agent/chat/hooks/agentStreamTurnEventBinding.ts index 44d86d904..0bf47ebb7 100644 --- a/src/components/agent/chat/hooks/agentStreamTurnEventBinding.ts +++ b/src/components/agent/chat/hooks/agentStreamTurnEventBinding.ts @@ -1,5 +1,9 @@ import type { Dispatch, MutableRefObject, SetStateAction } from "react"; -import { parseAgentEvent, type AgentThreadItem, type AgentThreadTurn } from "@/lib/api/agentProtocol"; +import { + parseAgentEvent, + type AgentThreadItem, + type AgentThreadTurn, +} from "@/lib/api/agentProtocol"; import type { AsterExecutionStrategy, AsterSessionExecutionRuntime, @@ -144,57 +148,60 @@ export async function registerAgentStreamTurnEventBinding( }, }); - return runtime.listenToTurnEvents(eventName, (event: { payload: unknown }) => { - const data = parseAgentEvent(event.payload); - if (!data) { - return; - } + return runtime.listenToTurnEvents( + eventName, + (event: { payload: unknown }) => { + const data = parseAgentEvent(event.payload); + if (!data) { + return; + } - handleTurnStreamEvent({ - data, - requestState, - callbacks: { - activateStream: () => - callbacks.activateStream( - activeSessionId, - effectiveWaitingRuntimeStatus, - ), - isStreamActivated: callbacks.isStreamActivated, - clearOptimisticItem: callbacks.clearOptimisticItem, - clearOptimisticTurn: callbacks.clearOptimisticTurn, - disposeListener: callbacks.disposeListener, - removeQueuedDraftMessages: callbacks.removeQueuedDraftMessages, - clearActiveStreamIfMatch: callbacks.clearActiveStreamIfMatch, - upsertQueuedTurn: callbacks.upsertQueuedTurn, - removeQueuedTurnState: callbacks.removeQueuedTurnState, - playToolcallSound: sounds.playToolcallSound, - playTypewriterSound: sounds.playTypewriterSound, - appendThinkingToParts, - }, - observer, - eventName, - pendingTurnKey, - pendingItemKey, - assistantMsgId, - activeSessionId, - resolvedWorkspaceId, - effectiveExecutionStrategy, - content, - runtime, - webSearch, - warnedKeysRef, - actionLoggedKeys, - toolLogIdByToolId, - toolStartedAtByToolId, - toolNameByToolId, - onWriteFile, - setMessages, - setPendingActions, - setThreadItems, - setThreadTurns, - setCurrentTurnId, - setExecutionRuntime, - setIsSending, - }); - }); + handleTurnStreamEvent({ + data, + requestState, + callbacks: { + activateStream: () => + callbacks.activateStream( + activeSessionId, + effectiveWaitingRuntimeStatus, + ), + isStreamActivated: callbacks.isStreamActivated, + clearOptimisticItem: callbacks.clearOptimisticItem, + clearOptimisticTurn: callbacks.clearOptimisticTurn, + disposeListener: callbacks.disposeListener, + removeQueuedDraftMessages: callbacks.removeQueuedDraftMessages, + clearActiveStreamIfMatch: callbacks.clearActiveStreamIfMatch, + upsertQueuedTurn: callbacks.upsertQueuedTurn, + removeQueuedTurnState: callbacks.removeQueuedTurnState, + playToolcallSound: sounds.playToolcallSound, + playTypewriterSound: sounds.playTypewriterSound, + appendThinkingToParts, + }, + observer, + eventName, + pendingTurnKey, + pendingItemKey, + assistantMsgId, + activeSessionId, + resolvedWorkspaceId, + effectiveExecutionStrategy, + content, + runtime, + webSearch, + warnedKeysRef, + actionLoggedKeys, + toolLogIdByToolId, + toolStartedAtByToolId, + toolNameByToolId, + onWriteFile, + setMessages, + setPendingActions, + setThreadItems, + setThreadTurns, + setCurrentTurnId, + setExecutionRuntime, + setIsSending, + }); + }, + ); } diff --git a/src/components/agent/chat/hooks/agentStreamUserInputSendPreparation.test.ts b/src/components/agent/chat/hooks/agentStreamUserInputSendPreparation.test.ts index 34f6e7537..725894bb7 100644 --- a/src/components/agent/chat/hooks/agentStreamUserInputSendPreparation.test.ts +++ b/src/components/agent/chat/hooks/agentStreamUserInputSendPreparation.test.ts @@ -9,7 +9,9 @@ import type { ActiveStreamState } from "./agentStreamSubmissionLifecycle"; function createStateSetter(getValue: () => T, setValue: (value: T) => void) { return (next: T | ((prev: T) => T)) => { - setValue(typeof next === "function" ? (next as (prev: T) => T)(getValue()) : next); + setValue( + typeof next === "function" ? (next as (prev: T) => T)(getValue()) : next, + ); }; } @@ -50,12 +52,18 @@ describe("agentStreamUserInputSendPreparation", () => { providerType: "openai", model: "gpt-5.4", }), - setMessages: createStateSetter(() => messages, (value) => { - messages = value; - }), - setIsSending: createStateSetter(() => isSending, (value) => { - isSending = value; - }), + setMessages: createStateSetter( + () => messages, + (value) => { + messages = value; + }, + ), + setIsSending: createStateSetter( + () => isSending, + (value) => { + isSending = value; + }, + ), }; } @@ -68,12 +76,18 @@ describe("agentStreamUserInputSendPreparation", () => { let isSending = false; const env = { ...createEnv(), - setMessages: createStateSetter(() => messages, (value) => { - messages = value; - }), - setIsSending: createStateSetter(() => isSending, (value) => { - isSending = value; - }), + setMessages: createStateSetter( + () => messages, + (value) => { + messages = value; + }, + ), + setIsSending: createStateSetter( + () => isSending, + (value) => { + isSending = value; + }, + ), }; const result = prepareAgentStreamUserInputSend({ @@ -111,12 +125,18 @@ describe("agentStreamUserInputSendPreparation", () => { let isSending = false; const env = { ...createEnv(), - setMessages: createStateSetter(() => messages, (value) => { - messages = value; - }), - setIsSending: createStateSetter(() => isSending, (value) => { - isSending = value; - }), + setMessages: createStateSetter( + () => messages, + (value) => { + messages = value; + }, + ), + setIsSending: createStateSetter( + () => isSending, + (value) => { + isSending = value; + }, + ), }; prepareAgentStreamUserInputSend({ @@ -138,8 +158,9 @@ describe("agentStreamUserInputSendPreparation", () => { }); it("有 active stream 时应进入 queue 模式,并允许 model override", () => { - vi.spyOn(crypto, "randomUUID") - .mockReturnValueOnce("00000000-0000-0000-0000-000000000003"); + vi.spyOn(crypto, "randomUUID").mockReturnValueOnce( + "00000000-0000-0000-0000-000000000003", + ); let messages: Message[] = []; let isSending = false; @@ -154,12 +175,18 @@ describe("agentStreamUserInputSendPreparation", () => { providerType: "claude", model: "sonnet", }), - setMessages: createStateSetter(() => messages, (value) => { - messages = value; - }), - setIsSending: createStateSetter(() => isSending, (value) => { - isSending = value; - }), + setMessages: createStateSetter( + () => messages, + (value) => { + messages = value; + }, + ), + setIsSending: createStateSetter( + () => isSending, + (value) => { + isSending = value; + }, + ), getSyncedSessionModelPreference: () => null, }; @@ -199,12 +226,18 @@ describe("agentStreamUserInputSendPreparation", () => { ...createEnv({ threadBusy: true, }), - setMessages: createStateSetter(() => messages, (value) => { - messages = value; - }), - setIsSending: createStateSetter(() => isSending, (value) => { - isSending = value; - }), + setMessages: createStateSetter( + () => messages, + (value) => { + messages = value; + }, + ), + setIsSending: createStateSetter( + () => isSending, + (value) => { + isSending = value; + }, + ), }; const result = prepareAgentStreamUserInputSend({ @@ -232,12 +265,18 @@ describe("agentStreamUserInputSendPreparation", () => { sessionId: null, }), hasPendingPreparedSubmit: () => true, - setMessages: createStateSetter(() => messages, (value) => { - messages = value; - }), - setIsSending: createStateSetter(() => isSending, (value) => { - isSending = value; - }), + setMessages: createStateSetter( + () => messages, + (value) => { + messages = value; + }, + ), + setIsSending: createStateSetter( + () => isSending, + (value) => { + isSending = value; + }, + ), }; const result = prepareAgentStreamUserInputSend({ diff --git a/src/components/agent/chat/hooks/agentStreamUserInputSubmission.test.ts b/src/components/agent/chat/hooks/agentStreamUserInputSubmission.test.ts index 917d65f0b..558264a70 100644 --- a/src/components/agent/chat/hooks/agentStreamUserInputSubmission.test.ts +++ b/src/components/agent/chat/hooks/agentStreamUserInputSubmission.test.ts @@ -16,7 +16,9 @@ import { buildWaitingAgentRuntimeStatus } from "../utils/agentRuntimeStatus"; function createStateSetter(getValue: () => T, setValue: (value: T) => void) { return (next: T | ((prev: T) => T)) => { - setValue(typeof next === "function" ? (next as (prev: T) => T)(getValue()) : next); + setValue( + typeof next === "function" ? (next as (prev: T) => T)(getValue()) : next, + ); }; } @@ -78,22 +80,37 @@ describe("agentStreamUserInputSubmission", () => { activeStreamRefState.current = next; }, clearActiveStreamIfMatch: () => false, - setMessages: createStateSetter(() => messages, (value) => { - messages = value; - }), - setThreadItems: createStateSetter(() => threadItems, (value) => { - threadItems = value; - }), - setThreadTurns: createStateSetter(() => threadTurns, (value) => { - threadTurns = value; - }), - setCurrentTurnId: createStateSetter(() => currentTurnId, (value) => { - currentTurnId = value; - }), + setMessages: createStateSetter( + () => messages, + (value) => { + messages = value; + }, + ), + setThreadItems: createStateSetter( + () => threadItems, + (value) => { + threadItems = value; + }, + ), + setThreadTurns: createStateSetter( + () => threadTurns, + (value) => { + threadTurns = value; + }, + ), + setCurrentTurnId: createStateSetter( + () => currentTurnId, + (value) => { + currentTurnId = value; + }, + ), setExecutionRuntime: noopDispatch(), - setQueuedTurns: createStateSetter(() => queuedTurns, (value) => { - queuedTurns = value; - }), + setQueuedTurns: createStateSetter( + () => queuedTurns, + (value) => { + queuedTurns = value; + }, + ), setPendingActions: noopDispatch(), setWorkspacePathMissing: noopDispatch(), setIsSending: noopDispatch(), diff --git a/src/components/agent/chat/hooks/agentStreamUserInputSubmission.ts b/src/components/agent/chat/hooks/agentStreamUserInputSubmission.ts index 9a557026f..111037c13 100644 --- a/src/components/agent/chat/hooks/agentStreamUserInputSubmission.ts +++ b/src/components/agent/chat/hooks/agentStreamUserInputSubmission.ts @@ -41,7 +41,8 @@ export async function submitAgentStreamUserInput( userMsgId, content, expectingQueue, - initialThreadId: env.sessionIdRef.current || `local-thread:${assistantMsgId}`, + initialThreadId: + env.sessionIdRef.current || `local-thread:${assistantMsgId}`, listenerMapRef: env.listenerMapRef, setActiveStream: env.setActiveStream, setMessages: env.setMessages, diff --git a/src/components/agent/chat/hooks/agentThreadState.ts b/src/components/agent/chat/hooks/agentThreadState.ts index 608147aa4..2ac596790 100644 --- a/src/components/agent/chat/hooks/agentThreadState.ts +++ b/src/components/agent/chat/hooks/agentThreadState.ts @@ -1,6 +1,9 @@ import type { AgentThreadItem, AgentThreadTurn } from "../types"; -function compareItemOrder(left: AgentThreadItem, right: AgentThreadItem): number { +function compareItemOrder( + left: AgentThreadItem, + right: AgentThreadItem, +): number { if (left.started_at !== right.started_at) { return left.started_at.localeCompare(right.started_at); } @@ -64,7 +67,8 @@ export function markThreadActionItemSubmitted( return items.map((item) => { if ( - (item.type !== "approval_request" && item.type !== "request_user_input") || + (item.type !== "approval_request" && + item.type !== "request_user_input") || !requestIds.has(item.request_id) ) { return item; diff --git a/src/components/agent/chat/hooks/runtimeWarningPresentation.ts b/src/components/agent/chat/hooks/runtimeWarningPresentation.ts index facf89e04..4318ba19a 100644 --- a/src/components/agent/chat/hooks/runtimeWarningPresentation.ts +++ b/src/components/agent/chat/hooks/runtimeWarningPresentation.ts @@ -1,7 +1,6 @@ export const ARTIFACT_DOCUMENT_REPAIRED_WARNING_CODE = "artifact_document_repaired"; -export const ARTIFACT_DOCUMENT_FAILED_WARNING_CODE = - "artifact_document_failed"; +export const ARTIFACT_DOCUMENT_FAILED_WARNING_CODE = "artifact_document_failed"; export const ARTIFACT_DOCUMENT_PERSIST_FAILED_WARNING_CODE = "artifact_document_persist_failed"; diff --git a/src/components/agent/chat/hooks/skillCommand.ts b/src/components/agent/chat/hooks/skillCommand.ts index 95c7cc728..ce7465cbd 100644 --- a/src/components/agent/chat/hooks/skillCommand.ts +++ b/src/components/agent/chat/hooks/skillCommand.ts @@ -713,8 +713,9 @@ export async function tryExecuteSlashSkillCommand( })(), ); - const finalizedArtifact = - emittedArtifact as ReturnType | null; + const finalizedArtifact = emittedArtifact as ReturnType< + typeof buildArtifactFromWrite + > | null; const finalizedArtifactPath = emittedArtifactPath; if (finalizedArtifact && finalizedArtifactPath) { onWriteFile?.(finalizedArtifact.content, finalizedArtifactPath, { diff --git a/src/components/agent/chat/hooks/useAgentChatStateSnapshotDebug.test.tsx b/src/components/agent/chat/hooks/useAgentChatStateSnapshotDebug.test.tsx index 3000d9d3a..29bbea4f1 100644 --- a/src/components/agent/chat/hooks/useAgentChatStateSnapshotDebug.test.tsx +++ b/src/components/agent/chat/hooks/useAgentChatStateSnapshotDebug.test.tsx @@ -142,7 +142,7 @@ describe("useAgentChatStateSnapshotDebug", () => { queuedTurnsCount: 2, }), expect.objectContaining({ - dedupeKey: expect.stringContaining("\"queuedTurnsCount\":2"), + dedupeKey: expect.stringContaining('"queuedTurnsCount":2'), }), ); } finally { diff --git a/src/components/agent/chat/hooks/useAgentContext.ts b/src/components/agent/chat/hooks/useAgentContext.ts index 2f2d33807..7c07f13e6 100644 --- a/src/components/agent/chat/hooks/useAgentContext.ts +++ b/src/components/agent/chat/hooks/useAgentContext.ts @@ -40,7 +40,8 @@ interface UseAgentContextOptions { workspaceId: string; sessionIdRef: MutableRefObject; topicsUpdaterRef: MutableRefObject< - ((sessionId: string, executionStrategy: AsterExecutionStrategy) => void) | null + | ((sessionId: string, executionStrategy: AsterExecutionStrategy) => void) + | null >; sendMessageRef: MutableRefObject; runtime: { @@ -116,7 +117,9 @@ export function useAgentContext(options: UseAgentContextOptions) { const syncedSessionExecutionStrategyRef = useRef( new Map(), ); - const pendingSessionAccessModeSyncRef = useRef(new Map()); + const pendingSessionAccessModeSyncRef = useRef( + new Map(), + ); providerTypeRef.current = providerType; modelRef.current = model; @@ -133,10 +136,13 @@ export function useAgentContext(options: UseAgentContextOptions) { targetProviderType: string, targetModel: string, ) => { - savePersisted(getSessionModelPreferenceKey(workspaceId, targetSessionId), { - providerType: targetProviderType, - model: targetModel, - }); + savePersisted( + getSessionModelPreferenceKey(workspaceId, targetSessionId), + { + providerType: targetProviderType, + model: targetModel, + }, + ); }, [workspaceId], ); @@ -468,9 +474,11 @@ export function useAgentContext(options: UseAgentContextOptions) { return; } - void syncAccessMode(trimmedSessionId, latestAccessMode).catch((error) => { - console.warn("[AsterChat] 更新会话 accessMode 失败:", error); - }); + void syncAccessMode(trimmedSessionId, latestAccessMode).catch( + (error) => { + console.warn("[AsterChat] 更新会话 accessMode 失败:", error); + }, + ); }); }, [runtime], @@ -545,13 +553,16 @@ export function useAgentContext(options: UseAgentContextOptions) { savePersisted(storageKey, accessMode); }, [accessMode, workspaceId]); - const triggerAIGuide = useCallback(async (initialPrompt?: string) => { - const sendMessage = sendMessageRef.current; - if (!sendMessage) { - throw new Error("发送器尚未就绪"); - } - await sendMessage(initialPrompt?.trim() || "", [], false, false, true); - }, [sendMessageRef]); + const triggerAIGuide = useCallback( + async (initialPrompt?: string) => { + const sendMessage = sendMessageRef.current; + if (!sendMessage) { + throw new Error("发送器尚未就绪"); + } + await sendMessage(initialPrompt?.trim() || "", [], false, false, true); + }, + [sendMessageRef], + ); const fixWorkspacePathAndRetry = useCallback( async (newPath: string) => { diff --git a/src/components/agent/chat/hooks/useAgentRuntimeSyncEffects.test.tsx b/src/components/agent/chat/hooks/useAgentRuntimeSyncEffects.test.tsx index c3dba6d87..1297947a3 100644 --- a/src/components/agent/chat/hooks/useAgentRuntimeSyncEffects.test.tsx +++ b/src/components/agent/chat/hooks/useAgentRuntimeSyncEffects.test.tsx @@ -5,7 +5,9 @@ import type { AgentThreadTurn } from "../types"; import { useAgentRuntimeSyncEffects } from "./useAgentRuntimeSyncEffects"; const mockIsDevBridgeAvailable = vi.hoisted(() => vi.fn(() => false)); -const mockHasDevBridgeEventListenerCapability = vi.hoisted(() => vi.fn(() => false)); +const mockHasDevBridgeEventListenerCapability = vi.hoisted(() => + vi.fn(() => false), +); const mockHasTauriEventListenerCapability = vi.hoisted(() => vi.fn(() => true)); vi.mock("@/lib/dev-bridge", () => ({ @@ -203,13 +205,13 @@ describe("useAgentRuntimeSyncEffects", () => { it("收到 subagent 状态事件后应刷新当前会话详情", async () => { const refreshSessionDetail = vi.fn(async () => true); - const listeners = new Map< - string, - (event: { payload: unknown }) => void - >(); + const listeners = new Map void>(); const runtime = { listenToTeamEvents: vi.fn(async (eventName, handler) => { - listeners.set(eventName, handler as (event: { payload: unknown }) => void); + listeners.set( + eventName, + handler as (event: { payload: unknown }) => void, + ); return () => { listeners.delete(eventName); }; diff --git a/src/components/agent/chat/hooks/useAgentRuntimeSyncEffects.ts b/src/components/agent/chat/hooks/useAgentRuntimeSyncEffects.ts index 7852cb2ad..9db9a788b 100644 --- a/src/components/agent/chat/hooks/useAgentRuntimeSyncEffects.ts +++ b/src/components/agent/chat/hooks/useAgentRuntimeSyncEffects.ts @@ -1,8 +1,4 @@ -import { - useEffect, - useRef, - type MutableRefObject, -} from "react"; +import { useEffect, useRef, type MutableRefObject } from "react"; import { hasDevBridgeEventListenerCapability, isDevBridgeAvailable, @@ -110,11 +106,7 @@ export function useAgentRuntimeSyncEffects( return () => { window.clearInterval(timer); }; - }, [ - refreshSessionDetail, - sessionId, - shouldUseDevBridgeRuntimePolling, - ]); + }, [refreshSessionDetail, sessionId, shouldUseDevBridgeRuntimePolling]); useEffect(() => { if (!sessionId) { @@ -133,16 +125,19 @@ export function useAgentRuntimeSyncEffects( const subscribe = async () => { for (const eventName of eventNames) { - const unlisten = await runtime.listenToTeamEvents(eventName, (event) => { - const data = parseAgentEvent(event.payload); - if (disposed || data?.type !== "subagent_status_changed") { - return; - } - if (sessionIdRef.current !== sessionId) { - return; - } - void refreshSessionDetail(sessionId); - }); + const unlisten = await runtime.listenToTeamEvents( + eventName, + (event) => { + const data = parseAgentEvent(event.payload); + if (disposed || data?.type !== "subagent_status_changed") { + return; + } + if (sessionIdRef.current !== sessionId) { + return; + } + void refreshSessionDetail(sessionId); + }, + ); if (disposed) { unlisten(); diff --git a/src/components/agent/chat/hooks/useAgentSession.ts b/src/components/agent/chat/hooks/useAgentSession.ts index 6887cd2a9..ab8419629 100644 --- a/src/components/agent/chat/hooks/useAgentSession.ts +++ b/src/components/agent/chat/hooks/useAgentSession.ts @@ -101,7 +101,10 @@ interface UseAgentSessionOptions { sessionId: string, executionStrategy: AsterExecutionStrategy, ) => void; - persistSessionAccessMode: (sessionId: string, accessMode: AgentAccessMode) => void; + persistSessionAccessMode: ( + sessionId: string, + accessMode: AgentAccessMode, + ) => void; loadSessionAccessMode: (sessionId: string) => AgentAccessMode | null; filterSessionsByWorkspace: (sessions: T[]) => T[]; setExecutionStrategyState: ( @@ -208,8 +211,9 @@ export function useAgentSession(options: UseAgentSessionOptions) { const messagesRef = useRef(messages); const threadTurnsRef = useRef(threadTurns); const threadItemsRef = useRef(threadItems); - const executionRuntimeRef = - useRef(executionRuntime); + const executionRuntimeRef = useRef( + executionRuntime, + ); const restoreCandidateSessionIdRef = useRef( loadScopedSessionRestoreCandidate(), ); @@ -1104,24 +1108,26 @@ export function useAgentSession(options: UseAgentSessionOptions) { topicsCount: topics.length, workspaceId: resolvedWorkspaceId, }); - switchTopic(targetSessionId).catch((error) => { - console.warn("[AsterChat] 自动恢复会话失败:", error); - logAgentDebug( - "useAgentSession", - "autoRestore.error", - { - error, - targetSessionId, - workspaceId: resolvedWorkspaceId, - }, - { level: "warn" }, - ); - persistSessionRestoreCandidate(null); - }).finally(() => { - if (!cancelled) { - setIsAutoRestoringSession(false); - } - }); + switchTopic(targetSessionId) + .catch((error) => { + console.warn("[AsterChat] 自动恢复会话失败:", error); + logAgentDebug( + "useAgentSession", + "autoRestore.error", + { + error, + targetSessionId, + workspaceId: resolvedWorkspaceId, + }, + { level: "warn" }, + ); + persistSessionRestoreCandidate(null); + }) + .finally(() => { + if (!cancelled) { + setIsAutoRestoringSession(false); + } + }); return () => { cancelled = true; @@ -1243,8 +1249,7 @@ export function useAgentSession(options: UseAgentSessionOptions) { hydratedSessionRef.current = sessionId; const hasLocalTimelineCache = - messages.length > 0 && - (threadTurns.length > 0 || threadItems.length > 0); + messages.length > 0 && (threadTurns.length > 0 || threadItems.length > 0); const hasPreservedMessageCache = preserveRestoredMessages && messages.length > 0; logAgentDebug("useAgentSession", "hydrateSession.start", { diff --git a/src/components/agent/chat/hooks/useAgentStreamController.ts b/src/components/agent/chat/hooks/useAgentStreamController.ts index 7667f25c2..a9a4454d2 100644 --- a/src/components/agent/chat/hooks/useAgentStreamController.ts +++ b/src/components/agent/chat/hooks/useAgentStreamController.ts @@ -1,4 +1,10 @@ -import { useCallback, useEffect, useRef, useState, type MutableRefObject } from "react"; +import { + useCallback, + useEffect, + useRef, + useState, + type MutableRefObject, +} from "react"; import type { ActiveStreamState } from "./agentStreamSubmissionLifecycle"; interface UseAgentStreamControllerOptions { diff --git a/src/components/agent/chat/hooks/useAgentTools.ts b/src/components/agent/chat/hooks/useAgentTools.ts index 7eb2c970e..0a26a4b1b 100644 --- a/src/components/agent/chat/hooks/useAgentTools.ts +++ b/src/components/agent/chat/hooks/useAgentTools.ts @@ -40,7 +40,9 @@ function upsertSubmittedAction( actions: ActionRequired[], nextAction: ActionRequired, ): ActionRequired[] { - const next = actions.filter((item) => item.requestId !== nextAction.requestId); + const next = actions.filter( + (item) => item.requestId !== nextAction.requestId, + ); next.push(nextAction); return next; } @@ -202,10 +204,11 @@ export function useAgentTools(options: UseAgentToolsOptions) { setSubmittedActionsInFlight((prev) => upsertSubmittedAction(prev, { - ...(metadataAction || persistedAction || { - requestId: effectiveRequestId, - actionType, - }), + ...(metadataAction || + persistedAction || { + requestId: effectiveRequestId, + actionType, + }), requestId: effectiveRequestId, actionType, status: "submitted", @@ -237,10 +240,11 @@ export function useAgentTools(options: UseAgentToolsOptions) { refreshSessionId = sessionIdRef.current; setSubmittedActionsInFlight((prev) => upsertSubmittedAction(prev, { - ...(metadataAction || persistedAction || { - requestId: effectiveRequestId, - actionType, - }), + ...(metadataAction || + persistedAction || { + requestId: effectiveRequestId, + actionType, + }), requestId: effectiveRequestId, actionType, status: "submitted", diff --git a/src/components/agent/chat/hooks/useArtifactAutoPreviewSync.ts b/src/components/agent/chat/hooks/useArtifactAutoPreviewSync.ts index aabe5e75e..aaa9ceae4 100644 --- a/src/components/agent/chat/hooks/useArtifactAutoPreviewSync.ts +++ b/src/components/agent/chat/hooks/useArtifactAutoPreviewSync.ts @@ -1,10 +1,7 @@ import { useEffect } from "react"; import type { Artifact } from "@/lib/artifact/types"; import { resolveArtifactProtocolFilePath } from "@/lib/artifact-protocol"; -import type { - ArtifactWriteMetadata, - WriteArtifactContext, -} from "../types"; +import type { ArtifactWriteMetadata, WriteArtifactContext } from "../types"; import { buildArtifactFromWrite, resolveArtifactWritePhase, @@ -37,7 +34,9 @@ function normalizePreviewText(value: string, maxChars: number): string { return `${trimmed.slice(0, maxChars).trimEnd()}…`; } -export function shouldAutoSyncArtifactPreview(artifact: Artifact | null): boolean { +export function shouldAutoSyncArtifactPreview( + artifact: Artifact | null, +): boolean { if (!artifact) { return false; } @@ -77,7 +76,8 @@ export function mergePreviewContentIntoArtifact( const nextContent = typeof preview.content === "string" ? preview.content : artifact.content; - const nextPath = preview.path?.trim() || resolveArtifactProtocolFilePath(artifact); + const nextPath = + preview.path?.trim() || resolveArtifactProtocolFilePath(artifact); const currentContent = artifact.content; if (!nextContent.trim() && currentContent.trim()) { diff --git a/src/components/agent/chat/hooks/useArtifactDisplayState.ts b/src/components/agent/chat/hooks/useArtifactDisplayState.ts index c9d962179..940049e2d 100644 --- a/src/components/agent/chat/hooks/useArtifactDisplayState.ts +++ b/src/components/agent/chat/hooks/useArtifactDisplayState.ts @@ -83,7 +83,9 @@ export function settleLiveArtifactAfterStreamStops( }; } -function hasRenderableArtifactContent(artifact: Artifact | null | undefined): boolean { +function hasRenderableArtifactContent( + artifact: Artifact | null | undefined, +): boolean { return Boolean(artifact?.content.trim()); } @@ -178,7 +180,8 @@ function buildOverlayState( phase, phaseLabel: "已完成", title: "写入已结束", - detail: "文件已经完成,但当前还没有可直接渲染的内容,暂时保留上一版本。", + detail: + "文件已经完成,但当前还没有可直接渲染的内容,暂时保留上一版本。", displayName, filePath, showProgress: false, @@ -339,7 +342,9 @@ export function useArtifactDisplayState( return; } - const writePhase = liveArtifact ? resolveArtifactWritePhase(liveArtifact) : null; + const writePhase = liveArtifact + ? resolveArtifactWritePhase(liveArtifact) + : null; const isPendingTransition = liveArtifact && !hasRenderableArtifactContent(liveArtifact) && diff --git a/src/components/agent/chat/hooks/useAsterAgentChat.ts b/src/components/agent/chat/hooks/useAsterAgentChat.ts index 98c44619a..24320c939 100644 --- a/src/components/agent/chat/hooks/useAsterAgentChat.ts +++ b/src/components/agent/chat/hooks/useAsterAgentChat.ts @@ -216,14 +216,19 @@ export function useAsterAgentChat(options: UseAsterAgentChatRuntimeOptions) { session.sessionId && session.topics.some((topic) => topic.id === session.sessionId), ); - const activeExecutionRuntime = useMemo< - AsterSessionExecutionRuntime | null - >(() => { - const threadStatus = session.threadRead?.status; - const shouldPreferRuntime = - stream.isSending || threadStatus === "running" || threadStatus === "queued"; - return shouldPreferRuntime ? session.executionRuntime : null; - }, [session.executionRuntime, session.threadRead?.status, stream.isSending]); + const activeExecutionRuntime = + useMemo(() => { + const threadStatus = session.threadRead?.status; + const shouldPreferRuntime = + stream.isSending || + threadStatus === "running" || + threadStatus === "queued"; + return shouldPreferRuntime ? session.executionRuntime : null; + }, [ + session.executionRuntime, + session.threadRead?.status, + stream.isSending, + ]); useAgentRuntimeSyncEffects({ runtime, diff --git a/src/components/agent/chat/hooks/useContentSync.test.tsx b/src/components/agent/chat/hooks/useContentSync.test.tsx index 866d65872..dc3449422 100644 --- a/src/components/agent/chat/hooks/useContentSync.test.tsx +++ b/src/components/agent/chat/hooks/useContentSync.test.tsx @@ -21,7 +21,9 @@ interface HookHarness { unmount: () => void; } -function mountHook(options?: Parameters[0]): HookHarness { +function mountHook( + options?: Parameters[0], +): HookHarness { const container = document.createElement("div"); document.body.appendChild(container); const root = createRoot(container); diff --git a/src/components/agent/chat/hooks/useContentSync.ts b/src/components/agent/chat/hooks/useContentSync.ts index 6437aebb0..d522b1538 100644 --- a/src/components/agent/chat/hooks/useContentSync.ts +++ b/src/components/agent/chat/hooks/useContentSync.ts @@ -141,10 +141,13 @@ export function useContentSync( isSyncingRef.current = false; }, [clearTimers]); - useEffect(() => () => { - clearTimers(); - isSyncingRef.current = false; - }, [clearTimers]); + useEffect( + () => () => { + clearTimers(); + isSyncingRef.current = false; + }, + [clearTimers], + ); return { syncContent, syncStatus, resetStatus }; } diff --git a/src/components/agent/chat/hooks/useGeneralWorkbenchEntryPrompt.test.tsx b/src/components/agent/chat/hooks/useGeneralWorkbenchEntryPrompt.test.tsx index 557ed6604..c03d769b5 100644 --- a/src/components/agent/chat/hooks/useGeneralWorkbenchEntryPrompt.test.tsx +++ b/src/components/agent/chat/hooks/useGeneralWorkbenchEntryPrompt.test.tsx @@ -40,11 +40,13 @@ function mountHook(initialProps?: Partial): HookHarness { ): Promise => null, ); const loadRunState = vi.fn( - async (_sessionId: string): Promise => - null, + async ( + _sessionId: string, + ): Promise => null, ); - let hookValue: ReturnType | null = null; + let hookValue: ReturnType | null = + null; let currentProps: HookProps = { activeTheme: "general", contentId: "content-1", @@ -165,7 +167,9 @@ describe("useGeneralWorkbenchEntryPrompt", () => { const root = createRoot(container); const onHydrateInitialPrompt = vi.fn(); const loadRunState = vi.fn( - async (_sessionId: string): Promise => ({ + async ( + _sessionId: string, + ): Promise => ({ run_state: "auto_running", current_gate_key: "write_mode", queue_items: [ @@ -219,7 +223,9 @@ describe("useGeneralWorkbenchEntryPrompt", () => { title: "发现上次未完成任务", description: expect.stringContaining("撰写主稿"), }); - expect(hookValueRef.current?.generalWorkbenchEntryCheckPending).toBe(false); + expect(hookValueRef.current?.generalWorkbenchEntryCheckPending).toBe( + false, + ); } finally { act(() => { root.unmount(); diff --git a/src/components/agent/chat/hooks/useGeneralWorkbenchEntryPrompt.ts b/src/components/agent/chat/hooks/useGeneralWorkbenchEntryPrompt.ts index d6d62648f..8d3df9302 100644 --- a/src/components/agent/chat/hooks/useGeneralWorkbenchEntryPrompt.ts +++ b/src/components/agent/chat/hooks/useGeneralWorkbenchEntryPrompt.ts @@ -190,8 +190,10 @@ export function useGeneralWorkbenchEntryPrompt({ }: UseGeneralWorkbenchEntryPromptOptions) { const [generalWorkbenchEntryPrompt, setGeneralWorkbenchEntryPrompt] = useState(null); - const [generalWorkbenchEntryCheckPending, setGeneralWorkbenchEntryCheckPending] = - useState(false); + const [ + generalWorkbenchEntryCheckPending, + setGeneralWorkbenchEntryCheckPending, + ] = useState(false); const hydratedPromptSignatureRef = useRef(null); const dismissedPromptSignatureRef = useRef(null); diff --git a/src/components/agent/chat/hooks/useGeneralWorkbenchEntryPromptActions.test.tsx b/src/components/agent/chat/hooks/useGeneralWorkbenchEntryPromptActions.test.tsx index c219fb12e..649a93418 100644 --- a/src/components/agent/chat/hooks/useGeneralWorkbenchEntryPromptActions.test.tsx +++ b/src/components/agent/chat/hooks/useGeneralWorkbenchEntryPromptActions.test.tsx @@ -32,8 +32,9 @@ function mountHook(initialProps?: Partial): HookHarness { const onInputChange = vi.fn(); const onRequirePrompt = vi.fn(); - let hookValue: ReturnType | null = - null; + let hookValue: ReturnType< + typeof useGeneralWorkbenchEntryPromptActions + > | null = null; let currentProps: HookProps = { generalWorkbenchEntryPrompt: null, input: "", @@ -117,9 +118,7 @@ describe("useGeneralWorkbenchEntryPromptActions", () => { try { await act(async () => { - await harness - .getValue() - .handleContinueGeneralWorkbenchEntryPrompt(); + await harness.getValue().handleContinueGeneralWorkbenchEntryPrompt(); }); expect(harness.onContinuePrompt).toHaveBeenCalledWith("请先生成主稿"); @@ -128,9 +127,7 @@ describe("useGeneralWorkbenchEntryPromptActions", () => { }); await act(async () => { - await harness - .getValue() - .handleContinueGeneralWorkbenchEntryPrompt(); + await harness.getValue().handleContinueGeneralWorkbenchEntryPrompt(); }); expect(harness.onContinuePrompt).toHaveBeenLastCalledWith( "我已经补充了额外要求", @@ -155,9 +152,7 @@ describe("useGeneralWorkbenchEntryPromptActions", () => { try { await act(async () => { - await harness - .getValue() - .handleContinueGeneralWorkbenchEntryPrompt(); + await harness.getValue().handleContinueGeneralWorkbenchEntryPrompt(); }); expect(harness.onRequirePrompt).toHaveBeenCalledTimes(1); expect(harness.onContinuePrompt).not.toHaveBeenCalled(); @@ -185,7 +180,9 @@ describe("useGeneralWorkbenchEntryPromptActions", () => { harness.getValue().handleRestartGeneralWorkbenchEntryPrompt(); }); - expect(harness.dismissGeneralWorkbenchEntryPrompt).toHaveBeenCalledTimes(1); + expect(harness.dismissGeneralWorkbenchEntryPrompt).toHaveBeenCalledTimes( + 1, + ); const options = harness.dismissGeneralWorkbenchEntryPrompt.mock.calls[0]?.[0]; expect(options?.consumeInitialPrompt).toBe(true); diff --git a/src/components/agent/chat/hooks/useGeneralWorkbenchSendBoundary.test.tsx b/src/components/agent/chat/hooks/useGeneralWorkbenchSendBoundary.test.tsx index d116eb073..8922151ee 100644 --- a/src/components/agent/chat/hooks/useGeneralWorkbenchSendBoundary.test.tsx +++ b/src/components/agent/chat/hooks/useGeneralWorkbenchSendBoundary.test.tsx @@ -1,9 +1,7 @@ import { act } from "react"; import { createRoot } from "react-dom/client"; import { afterEach, beforeEach, describe, expect, it, vi } from "vitest"; -import { - useGeneralWorkbenchSendBoundary, -} from "./useGeneralWorkbenchSendBoundary"; +import { useGeneralWorkbenchSendBoundary } from "./useGeneralWorkbenchSendBoundary"; interface HookHarnessProps { isThemeWorkbench: boolean; @@ -31,7 +29,8 @@ function mountHook(initialProps?: Partial): HookHarness { const onResetConsumedInitialPrompt = vi.fn(); const onClearEntryPrompt = vi.fn(); - let hookValue: ReturnType | null = null; + let hookValue: ReturnType | null = + null; let currentProps: HookHarnessProps = { isThemeWorkbench: true, contentId: "content-1", diff --git a/src/components/agent/chat/hooks/useGeneralWorkbenchSendBoundary.ts b/src/components/agent/chat/hooks/useGeneralWorkbenchSendBoundary.ts index 72f85b015..54c553ff5 100644 --- a/src/components/agent/chat/hooks/useGeneralWorkbenchSendBoundary.ts +++ b/src/components/agent/chat/hooks/useGeneralWorkbenchSendBoundary.ts @@ -98,7 +98,13 @@ export function useGeneralWorkbenchSendBoundary({ onClearEntryPrompt, }: UseGeneralWorkbenchSendBoundaryOptions) { const resolveSendBoundary = useCallback( - ({ sourceText, sendOptions }: { sourceText: string; sendOptions?: HandleSendOptions }) => + ({ + sourceText, + sendOptions, + }: { + sourceText: string; + sendOptions?: HandleSendOptions; + }) => buildGeneralWorkbenchSendBoundaryState({ isThemeWorkbench, contentId, diff --git a/src/components/agent/chat/hooks/useRuntimeTeamFormation.test.tsx b/src/components/agent/chat/hooks/useRuntimeTeamFormation.test.tsx index fa2570f96..363cdaf99 100644 --- a/src/components/agent/chat/hooks/useRuntimeTeamFormation.test.tsx +++ b/src/components/agent/chat/hooks/useRuntimeTeamFormation.test.tsx @@ -109,7 +109,8 @@ describe("useRuntimeTeamFormation", () => { }; await act(async () => { resultRef.current = await getValue().prepareRuntimeTeamBeforeSend({ - input: "请把这个需求从多个角度拆解成子任务,分别调研竞品方案和技术实现路径,然后给出综合分析报告", + input: + "请把这个需求从多个角度拆解成子任务,分别调研竞品方案和技术实现路径,然后给出综合分析报告", }); }); @@ -130,7 +131,8 @@ describe("useRuntimeTeamFormation", () => { }; await act(async () => { resultRef.current = await getValue().prepareRuntimeTeamBeforeSend({ - input: "请从多个维度拆解这个复杂需求,先做竞品调研,再做技术方案评估,最后输出可行性分析报告", + input: + "请从多个维度拆解这个复杂需求,先做竞品调研,再做技术方案评估,最后输出可行性分析报告", }); }); @@ -147,7 +149,8 @@ describe("useRuntimeTeamFormation", () => { }; await act(async () => { firstResultRef.current = await getValue().prepareRuntimeTeamBeforeSend({ - input: "请先规划一个多角色协作的 team 来处理这个跨部门需求,需要调研、开发、测试三个角色分别推进各自负责的子任务", + input: + "请先规划一个多角色协作的 team 来处理这个跨部门需求,需要调研、开发、测试三个角色分别推进各自负责的子任务", subagentEnabled: true, }); }); @@ -162,7 +165,8 @@ describe("useRuntimeTeamFormation", () => { await act(async () => { await getValue().prepareRuntimeTeamBeforeSend({ - input: "请拆分成多个角色来协作执行这个跨领域的综合分析任务,需要数据分析师、行业研究员和报告撰写员分别负责各自模块", + input: + "请拆分成多个角色来协作执行这个跨领域的综合分析任务,需要数据分析师、行业研究员和报告撰写员分别负责各自模块", }); }); @@ -200,7 +204,8 @@ describe("shouldPrepareRuntimeTeamBeforeSend", () => { shouldPrepareRuntimeTeamBeforeSend({ subagentEnabled: true, projectId: "project-1", - input: "请从多个角度拆解这个需求,调研竞品方案,然后分别给出技术实现方案和产品设计方案,最后做交叉验证", + input: + "请从多个角度拆解这个需求,调研竞品方案,然后分别给出技术实现方案和产品设计方案,最后做交叉验证", }), ).toBe(true); }); @@ -210,7 +215,8 @@ describe("shouldPrepareRuntimeTeamBeforeSend", () => { shouldPrepareRuntimeTeamBeforeSend({ subagentEnabled: false, projectId: "project-1", - input: "请从多个角度拆解这个需求,调研竞品方案,然后分别给出技术实现方案和产品设计方案", + input: + "请从多个角度拆解这个需求,调研竞品方案,然后分别给出技术实现方案和产品设计方案", }), ).toBe(false); }); @@ -220,7 +226,8 @@ describe("shouldPrepareRuntimeTeamBeforeSend", () => { shouldPrepareRuntimeTeamBeforeSend({ subagentEnabled: true, projectId: "", - input: "请从多个角度拆解这个需求,调研竞品方案,然后分别给出技术实现方案和产品设计方案", + input: + "请从多个角度拆解这个需求,调研竞品方案,然后分别给出技术实现方案和产品设计方案", }), ).toBe(false); }); @@ -240,7 +247,8 @@ describe("shouldPrepareRuntimeTeamBeforeSend", () => { shouldPrepareRuntimeTeamBeforeSend({ subagentEnabled: true, projectId: "project-1", - input: "请从多个角度拆解这个需求,调研竞品方案,然后分别给出技术实现方案和产品设计方案", + input: + "请从多个角度拆解这个需求,调研竞品方案,然后分别给出技术实现方案和产品设计方案", purpose: "content_review", }), ).toBe(false); @@ -261,7 +269,8 @@ describe("shouldPrepareRuntimeTeamBeforeSend", () => { shouldPrepareRuntimeTeamBeforeSend({ subagentEnabled: true, projectId: "project-1", - input: "生成一份关于人工智能在医疗领域应用的详细报告,需要包含市场分析、技术趋势、政策法规、典型案例和未来展望五个部分", + input: + "生成一份关于人工智能在医疗领域应用的详细报告,需要包含市场分析、技术趋势、政策法规、典型案例和未来展望五个部分", }), ).toBe(false); @@ -269,7 +278,8 @@ describe("shouldPrepareRuntimeTeamBeforeSend", () => { shouldPrepareRuntimeTeamBeforeSend({ subagentEnabled: true, projectId: "project-1", - input: "帮我写一篇关于可持续发展目标的深度分析报告,要求涵盖经济、环境和社会三个维度的详细分析以及各国实践案例", + input: + "帮我写一篇关于可持续发展目标的深度分析报告,要求涵盖经济、环境和社会三个维度的详细分析以及各国实践案例", }), ).toBe(false); }); diff --git a/src/components/agent/chat/hooks/useSelectedTeamPreference.test.tsx b/src/components/agent/chat/hooks/useSelectedTeamPreference.test.tsx index 68b6daaca..d98d1b75a 100644 --- a/src/components/agent/chat/hooks/useSelectedTeamPreference.test.tsx +++ b/src/components/agent/chat/hooks/useSelectedTeamPreference.test.tsx @@ -216,7 +216,9 @@ describe("useSelectedTeamPreference", () => { }); it("项目级持久化失败时应回滚到当前项目 Team", async () => { - const consoleWarnSpy = vi.spyOn(console, "warn").mockImplementation(() => {}); + const consoleWarnSpy = vi + .spyOn(console, "warn") + .mockImplementation(() => {}); const engineeringTeam = createTeamDefinitionFromPreset( "code-triage-team", ) as TeamDefinition; @@ -499,7 +501,11 @@ describe("useSelectedTeamPreference", () => { }); await flushEffects(); - expect(syncSpy).toHaveBeenCalledWith("session-1", researchTeam, "general"); + expect(syncSpy).toHaveBeenCalledWith( + "session-1", + researchTeam, + "general", + ); expect(loadSelectedTeamReference("general")).toEqual({ id: "research-team", source: "builtin", @@ -550,7 +556,11 @@ describe("useSelectedTeamPreference", () => { await flushEffects(); expect(harness.getValue().selectedTeam?.id).toBe("research-team"); - expect(syncSpy).toHaveBeenCalledWith("session-1", researchTeam, "general"); + expect(syncSpy).toHaveBeenCalledWith( + "session-1", + researchTeam, + "general", + ); } finally { harness.unmount(); } diff --git a/src/components/agent/chat/hooks/useSelectedTeamPreference.ts b/src/components/agent/chat/hooks/useSelectedTeamPreference.ts index 896e0ff80..db77ae706 100644 --- a/src/components/agent/chat/hooks/useSelectedTeamPreference.ts +++ b/src/components/agent/chat/hooks/useSelectedTeamPreference.ts @@ -8,9 +8,7 @@ import { buildTeamDefinitionSummary, createTeamDefinitionFromPreset, } from "../utils/teamDefinitions"; -import { - createTeamDefinitionFromExecutionRuntimeRecentTeamSelection, -} from "../utils/sessionExecutionRuntime"; +import { createTeamDefinitionFromExecutionRuntimeRecentTeamSelection } from "../utils/sessionExecutionRuntime"; import { resolveSelectedTeamFromShadowSnapshot } from "./useTeamMemoryShadowSync"; import { loadCustomTeams, @@ -84,7 +82,9 @@ function persistSelectedTeamShadowCache( if (team?.source === "custom") { const nextCustomTeams = [ team, - ...loadCustomTeams().filter((existingTeam) => existingTeam.id !== team.id), + ...loadCustomTeams().filter( + (existingTeam) => existingTeam.id !== team.id, + ), ]; saveCustomTeams(nextCustomTeams); } @@ -161,8 +161,8 @@ export function useSelectedTeamPreference( ); const hasRuntimeSelection = Boolean( runtimeSelectionMatchesTheme && - runtimeSelection && - (runtimeSelection.disabled || runtimeSelectedTeam), + runtimeSelection && + (runtimeSelection.disabled || runtimeSelectedTeam), ); const [selectedTeam, setSelectedTeamState] = useState( () => resolveCurrentSelection(), @@ -171,7 +171,9 @@ export function useSelectedTeamPreference( const lastScopeKeyRef = useRef(scopeKey); const lastHydratedSourceRef = useRef(null); const lastBackfilledSourceRef = useRef(null); - const pendingSessionTeamSyncRef = useRef(new Map()); + const pendingSessionTeamSyncRef = useRef( + new Map(), + ); if (lastScopeKeyRef.current !== scopeKey) { lastScopeKeyRef.current = scopeKey; @@ -216,13 +218,13 @@ export function useSelectedTeamPreference( fallbackTeam, )}`; - if (workspacePreferenceState.kind !== "unset") { - if (lastHydratedSourceRef.current !== fallbackSourceKey) { - setSelectedTeamState((current) => - areSameSelectedTeam(current, fallbackTeam) ? current : fallbackTeam, - ); - lastHydratedSourceRef.current = fallbackSourceKey; - } + if (workspacePreferenceState.kind !== "unset") { + if (lastHydratedSourceRef.current !== fallbackSourceKey) { + setSelectedTeamState((current) => + areSameSelectedTeam(current, fallbackTeam) ? current : fallbackTeam, + ); + lastHydratedSourceRef.current = fallbackSourceKey; + } if (lastBackfilledSourceRef.current !== fallbackSourceKey) { scheduleSessionRecentTeamSelectionSync(fallbackTeam); diff --git a/src/components/agent/chat/hooks/useTeamMemoryShadowSync.test.tsx b/src/components/agent/chat/hooks/useTeamMemoryShadowSync.test.tsx index c7853f75f..103e3b121 100644 --- a/src/components/agent/chat/hooks/useTeamMemoryShadowSync.test.tsx +++ b/src/components/agent/chat/hooks/useTeamMemoryShadowSync.test.tsx @@ -151,9 +151,7 @@ describe("useTeamMemoryShadowSync", () => { expect(snapshot?.entries["team.selection"]?.content).toContain( "Team:研究双人组", ); - expect(snapshot?.entries["team.selection"]?.content).toContain( - "角色:", - ); + expect(snapshot?.entries["team.selection"]?.content).toContain("角色:"); expect(snapshot?.entries["team.subagents"]?.content).toContain( "研究代理 [running] explorer · 梳理主线风险", ); diff --git a/src/components/agent/chat/hooks/useTeamMemoryShadowSync.ts b/src/components/agent/chat/hooks/useTeamMemoryShadowSync.ts index 805eebc50..1379030c4 100644 --- a/src/components/agent/chat/hooks/useTeamMemoryShadowSync.ts +++ b/src/components/agent/chat/hooks/useTeamMemoryShadowSync.ts @@ -51,10 +51,13 @@ function buildTeamSelectionMemoryContent(params: { `Team:${team.label}`, `来源:${team.source}`, team.presetId ? `预设:${team.presetId}` : null, - normalizeLine(team.description) ? `说明:${normalizeLine(team.description)}` : null, + normalizeLine(team.description) + ? `说明:${normalizeLine(team.description)}` + : null, team.roles.length > 0 ? "角色:" : null, - ...team.roles.map((role) => - `- ${role.label}:${normalizeLine(role.summary) || "负责当前子任务"}` + ...team.roles.map( + (role) => + `- ${role.label}:${normalizeLine(role.summary) || "负责当前子任务"}`, ), ].filter((item): item is string => Boolean(item)); @@ -79,8 +82,8 @@ function buildSubagentMemoryContent(params: { sessionId?: string | null; childSubagentSessions?: AsterSubagentSessionInfo[]; }): string | null { - const sessions = (params.childSubagentSessions || []).filter( - (session) => normalizeLine(session.name), + const sessions = (params.childSubagentSessions || []).filter((session) => + normalizeLine(session.name), ); if (sessions.length === 0) { return null; @@ -108,8 +111,12 @@ function buildParentContextMemoryContent( return [ `父会话:${context.parent_session_name || context.parent_session_id}`, - normalizeLine(context.role_hint) ? `当前角色:${normalizeLine(context.role_hint)}` : null, - normalizeLine(context.task_summary) ? `当前任务:${normalizeLine(context.task_summary)}` : null, + normalizeLine(context.role_hint) + ? `当前角色:${normalizeLine(context.role_hint)}` + : null, + normalizeLine(context.task_summary) + ? `当前任务:${normalizeLine(context.task_summary)}` + : null, siblingSessions.length > 0 ? "兄弟会话:" : null, ...siblingSessions.map(summarizeSubagentSession), ] @@ -200,10 +207,7 @@ export function syncTeamMemoryShadowSnapshot( return nextSnapshot; } -function readMemoryField( - lines: string[], - prefix: string, -): string | null { +function readMemoryField(lines: string[], prefix: string): string | null { const target = `${prefix}:`; const matchedLine = lines.find((line) => line.startsWith(target)); if (!matchedLine) { @@ -220,7 +224,9 @@ function parseRoleLines(lines: string[]) { const roleText = line.slice(2).trim(); const separatorIndex = roleText.indexOf(":"); const label = - separatorIndex >= 0 ? roleText.slice(0, separatorIndex).trim() : roleText; + separatorIndex >= 0 + ? roleText.slice(0, separatorIndex).trim() + : roleText; const summary = separatorIndex >= 0 ? roleText.slice(separatorIndex + 1).trim() @@ -276,9 +282,7 @@ export function resolveSelectedTeamFromShadowSnapshot( } return normalizeTeamDefinition({ - id: - presetId || - `shadow-team-${label.replace(/\s+/g, "-").toLowerCase()}`, + id: presetId || `shadow-team-${label.replace(/\s+/g, "-").toLowerCase()}`, source: source === "custom" ? "custom" : "custom", label, description: description || "", @@ -364,7 +368,9 @@ function serializeSnapshot(snapshot?: TeamMemorySnapshot | null): string { export function useTeamMemoryShadowSync( options: TeamMemoryShadowSyncOptions, ): TeamMemorySnapshot | null { - const storage = options.storage ?? (typeof localStorage === "undefined" ? null : localStorage); + const storage = + options.storage ?? + (typeof localStorage === "undefined" ? null : localStorage); const [snapshot, setSnapshot] = useState(null); const teamKey = useMemo( () => serializeTeamDefinition(options.selectedTeam), diff --git a/src/components/agent/chat/hooks/useTeamWorkspaceRuntime.test.tsx b/src/components/agent/chat/hooks/useTeamWorkspaceRuntime.test.tsx index da19bb902..c0f56885c 100644 --- a/src/components/agent/chat/hooks/useTeamWorkspaceRuntime.test.tsx +++ b/src/components/agent/chat/hooks/useTeamWorkspaceRuntime.test.tsx @@ -4,19 +4,15 @@ import { createRoot, type Root } from "react-dom/client"; import { afterEach, beforeEach, describe, expect, it, vi } from "vitest"; import { useTeamWorkspaceRuntime } from "./useTeamWorkspaceRuntime"; -const { - mockSafeListen, - mockParseAgentEvent, -} = vi.hoisted(() => ({ +const { mockSafeListen, mockParseAgentEvent } = vi.hoisted(() => ({ mockSafeListen: vi.fn(), mockParseAgentEvent: vi.fn((payload: unknown) => payload), })); vi.mock("@/lib/api/agentProtocol", async () => { - const actual = - await vi.importActual( - "@/lib/api/agentProtocol", - ); + const actual = await vi.importActual< + typeof import("@/lib/api/agentProtocol") + >("@/lib/api/agentProtocol"); return { ...actual, parseAgentEvent: mockParseAgentEvent, @@ -90,7 +86,10 @@ describe("useTeamWorkspaceRuntime", () => { latestValue = null; mockParseAgentEvent.mockImplementation((payload: unknown) => payload); mockSafeListen.mockImplementation( - async (_eventName: string, _handler: (event: { payload: unknown }) => void) => + async ( + _eventName: string, + _handler: (event: { payload: unknown }) => void, + ) => () => {}, ); }); @@ -111,12 +110,12 @@ describe("useTeamWorkspaceRuntime", () => { }); it("收到 team 状态事件后,应立即投影 live runtime 与 live activity,并在去抖后递增刷新版本", async () => { - const listeners = new Map< - string, - (event: { payload: unknown }) => void - >(); + const listeners = new Map void>(); mockSafeListen.mockImplementation( - async (eventName: string, handler: (event: { payload: unknown }) => void) => { + async ( + eventName: string, + handler: (event: { payload: unknown }) => void, + ) => { listeners.set(eventName, handler); return () => { listeners.delete(eventName); @@ -150,34 +149,34 @@ describe("useTeamWorkspaceRuntime", () => { expect( latestValue?.liveActivityBySessionId["child-1"]?.[0]?.detail, ).toContain("已切换为处理中"); - expect( - latestValue?.activityRefreshVersionBySessionId["child-1"] ?? 0, - ).toBe(0); + expect(latestValue?.activityRefreshVersionBySessionId["child-1"] ?? 0).toBe( + 0, + ); await act(async () => { vi.advanceTimersByTime(239); await Promise.resolve(); }); - expect( - latestValue?.activityRefreshVersionBySessionId["child-1"] ?? 0, - ).toBe(0); + expect(latestValue?.activityRefreshVersionBySessionId["child-1"] ?? 0).toBe( + 0, + ); await act(async () => { vi.advanceTimersByTime(1); await Promise.resolve(); }); - expect( - latestValue?.activityRefreshVersionBySessionId["child-1"] ?? 0, - ).toBe(1); + expect(latestValue?.activityRefreshVersionBySessionId["child-1"] ?? 0).toBe( + 1, + ); }); it("收到子代理 runtime stream 事件后,应投影最近过程,并在关键完成事件后递增刷新版本", async () => { - const listeners = new Map< - string, - (event: { payload: unknown }) => void - >(); + const listeners = new Map void>(); mockSafeListen.mockImplementation( - async (eventName: string, handler: (event: { payload: unknown }) => void) => { + async ( + eventName: string, + handler: (event: { payload: unknown }) => void, + ) => { listeners.set(eventName, handler); return () => { listeners.delete(eventName); @@ -209,9 +208,9 @@ describe("useTeamWorkspaceRuntime", () => { expect( latestValue?.liveActivityBySessionId["child-1"]?.[0]?.detail, ).toContain("正在处理 browser_snapshot"); - expect( - latestValue?.activityRefreshVersionBySessionId["child-1"] ?? 0, - ).toBe(0); + expect(latestValue?.activityRefreshVersionBySessionId["child-1"] ?? 0).toBe( + 0, + ); await act(async () => { listeners.get("agent_subagent_stream:child-1")?.({ @@ -239,18 +238,18 @@ describe("useTeamWorkspaceRuntime", () => { await Promise.resolve(); }); - expect( - latestValue?.activityRefreshVersionBySessionId["child-1"] ?? 0, - ).toBe(1); + expect(latestValue?.activityRefreshVersionBySessionId["child-1"] ?? 0).toBe( + 1, + ); }); it("收到 turn_completed 后,应立即把 live runtime 从 running 回落到 completed", async () => { - const listeners = new Map< - string, - (event: { payload: unknown }) => void - >(); + const listeners = new Map void>(); mockSafeListen.mockImplementation( - async (eventName: string, handler: (event: { payload: unknown }) => void) => { + async ( + eventName: string, + handler: (event: { payload: unknown }) => void, + ) => { listeners.set(eventName, handler); return () => { listeners.delete(eventName); @@ -295,12 +294,12 @@ describe("useTeamWorkspaceRuntime", () => { }); it("收到 final_done 且没有后续状态事件时,也应结束 running 状态", async () => { - const listeners = new Map< - string, - (event: { payload: unknown }) => void - >(); + const listeners = new Map void>(); mockSafeListen.mockImplementation( - async (eventName: string, handler: (event: { payload: unknown }) => void) => { + async ( + eventName: string, + handler: (event: { payload: unknown }) => void, + ) => { listeners.set(eventName, handler); return () => { listeners.delete(eventName); @@ -339,12 +338,12 @@ describe("useTeamWorkspaceRuntime", () => { }); it("收到 error 后,应立即把 live runtime 从 running 回落到 failed", async () => { - const listeners = new Map< - string, - (event: { payload: unknown }) => void - >(); + const listeners = new Map void>(); mockSafeListen.mockImplementation( - async (eventName: string, handler: (event: { payload: unknown }) => void) => { + async ( + eventName: string, + handler: (event: { payload: unknown }) => void, + ) => { listeners.set(eventName, handler); return () => { listeners.delete(eventName); @@ -387,12 +386,12 @@ describe("useTeamWorkspaceRuntime", () => { }); it("base snapshot 追平或 session 移除后,应自动清理过期 live 状态", async () => { - const listeners = new Map< - string, - (event: { payload: unknown }) => void - >(); + const listeners = new Map void>(); mockSafeListen.mockImplementation( - async (eventName: string, handler: (event: { payload: unknown }) => void) => { + async ( + eventName: string, + handler: (event: { payload: unknown }) => void, + ) => { listeners.set(eventName, handler); return () => { listeners.delete(eventName); @@ -435,7 +434,9 @@ describe("useTeamWorkspaceRuntime", () => { }); expect(latestValue?.liveRuntimeBySessionId["child-1"]).toBeUndefined(); - expect(latestValue?.liveActivityBySessionId["child-1"]?.length ?? 0).toBe(1); + expect(latestValue?.liveActivityBySessionId["child-1"]?.length ?? 0).toBe( + 1, + ); await render({ childSubagentSessions: [], diff --git a/src/components/agent/chat/hooks/useTeamWorkspaceRuntime.ts b/src/components/agent/chat/hooks/useTeamWorkspaceRuntime.ts index c45d2b2e2..3d7e8270b 100644 --- a/src/components/agent/chat/hooks/useTeamWorkspaceRuntime.ts +++ b/src/components/agent/chat/hooks/useTeamWorkspaceRuntime.ts @@ -159,12 +159,17 @@ function buildRuntimeStatusEntry( return buildActivityEntry({ id: `runtime-status:${sessionId}`, title: status.title.trim() || "当前进展", - detail: [status.detail, ...(status.checkpoints ?? []).map((item) => `• ${item}`)] + detail: [ + status.detail, + ...(status.checkpoints ?? []).map((item) => `• ${item}`), + ] .map((item) => item.trim()) .filter(Boolean) .join("\n"), statusLabel: waiting ? "稍后开始" : "处理中", - badgeClassName: waiting ? QUEUED_BADGE_CLASS_NAME : IN_PROGRESS_BADGE_CLASS_NAME, + badgeClassName: waiting + ? QUEUED_BADGE_CLASS_NAME + : IN_PROGRESS_BADGE_CLASS_NAME, }); } @@ -334,16 +339,19 @@ function buildLiveRuntimeState( patch.queuedTurnCount ?? current?.queuedTurnCount ?? session.queuedTurnCount, - teamPhase: - patch.teamPhase ?? current?.teamPhase ?? session.teamPhase, + teamPhase: patch.teamPhase ?? current?.teamPhase ?? session.teamPhase, teamParallelBudget: patch.teamParallelBudget ?? current?.teamParallelBudget ?? session.teamParallelBudget, teamActiveCount: - patch.teamActiveCount ?? current?.teamActiveCount ?? session.teamActiveCount, + patch.teamActiveCount ?? + current?.teamActiveCount ?? + session.teamActiveCount, teamQueuedCount: - patch.teamQueuedCount ?? current?.teamQueuedCount ?? session.teamQueuedCount, + patch.teamQueuedCount ?? + current?.teamQueuedCount ?? + session.teamQueuedCount, providerConcurrencyGroup: patch.providerConcurrencyGroup ?? current?.providerConcurrencyGroup ?? @@ -390,7 +398,8 @@ function resolveFinalRuntimeStatus(params: { queuedTurnCount?: number; }): TeamWorkspaceRuntimeStatus { const queuedTurnCount = - params.queuedTurnCount ?? getQueuedTurnCount(params.session, params.current); + params.queuedTurnCount ?? + getQueuedTurnCount(params.session, params.current); if (queuedTurnCount > 0) { return "queued"; } @@ -417,8 +426,14 @@ function projectRuntimeStreamEvent(params: { streamState?: SessionLiveStreamState; toolNameById?: Record; }): TeamWorkspaceRuntimeStreamProjection | null { - const { sessionId, session, event, currentRuntime, streamState, toolNameById } = - params; + const { + sessionId, + session, + event, + currentRuntime, + streamState, + toolNameById, + } = params; switch (event.type) { case "item_started": @@ -429,9 +444,13 @@ function projectRuntimeStreamEvent(params: { if ( event.type === "item_completed" && - (event.item.type === "agent_message" || event.item.type === "turn_summary") + (event.item.type === "agent_message" || + event.item.type === "turn_summary") ) { - clearEntryIds.push(`stream-text:${sessionId}`, `runtime-status:${sessionId}`); + clearEntryIds.push( + `stream-text:${sessionId}`, + `runtime-status:${sessionId}`, + ); } if (event.type === "item_completed" && event.item.type === "reasoning") { @@ -443,7 +462,8 @@ function projectRuntimeStreamEvent(params: { clearEntryIds, clearTextDraft: event.type === "item_completed" && - (event.item.type === "agent_message" || event.item.type === "turn_summary"), + (event.item.type === "agent_message" || + event.item.type === "turn_summary"), clearThinkingDraft: event.type === "item_completed" && event.item.type === "reasoning", refreshPreview: event.type === "item_completed", @@ -531,7 +551,10 @@ function projectRuntimeStreamEvent(params: { }), runtimeStatus: "running", latestTurnStatus: "running", - queuedTurnCount: Math.max(getQueuedTurnCount(session, currentRuntime) - 1, 0), + queuedTurnCount: Math.max( + getQueuedTurnCount(session, currentRuntime) - 1, + 0, + ), }; case "queue_removed": { const queuedTurnCount = Math.max( @@ -569,36 +592,35 @@ function projectRuntimeStreamEvent(params: { runtimeStatus: "running", latestTurnStatus: "running", }; - case "turn_completed": - { - const queuedTurnCount = getQueuedTurnCount(session, currentRuntime); - return { - entry: buildLifecycleActivityEntry({ - sessionId, - key: "turn", - title: "阶段完成", - detail: "这一步已经完成,正在同步最新结果。", - statusLabel: "完成", - badgeClassName: COMPLETED_BADGE_CLASS_NAME, - }), - runtimeStatus: resolveFinalRuntimeStatus({ - session, - current: currentRuntime, - terminalStatus: "completed", - queuedTurnCount, - }), - latestTurnStatus: "completed", + case "turn_completed": { + const queuedTurnCount = getQueuedTurnCount(session, currentRuntime); + return { + entry: buildLifecycleActivityEntry({ + sessionId, + key: "turn", + title: "阶段完成", + detail: "这一步已经完成,正在同步最新结果。", + statusLabel: "完成", + badgeClassName: COMPLETED_BADGE_CLASS_NAME, + }), + runtimeStatus: resolveFinalRuntimeStatus({ + session, + current: currentRuntime, + terminalStatus: "completed", queuedTurnCount, - clearEntryIds: [ - `stream-text:${sessionId}`, - `stream-thinking:${sessionId}`, - `runtime-status:${sessionId}`, - ], - clearTextDraft: true, - clearThinkingDraft: true, - refreshPreview: true, - }; - } + }), + latestTurnStatus: "completed", + queuedTurnCount, + clearEntryIds: [ + `stream-text:${sessionId}`, + `stream-thinking:${sessionId}`, + `runtime-status:${sessionId}`, + ], + clearTextDraft: true, + clearThinkingDraft: true, + refreshPreview: true, + }; + } case "turn_failed": { const queuedTurnCount = getQueuedTurnCount(session, currentRuntime); return { @@ -607,7 +629,8 @@ function projectRuntimeStreamEvent(params: { key: "turn", title: "处理失败", detail: - event.turn.error_message?.trim() || "这一步处理失败,请查看错误详情。", + event.turn.error_message?.trim() || + "这一步处理失败,请查看错误详情。", statusLabel: "需重试", badgeClassName: FAILED_BADGE_CLASS_NAME, }), @@ -713,7 +736,7 @@ function buildActiveSubagentSnapshots(params: UseTeamWorkspaceRuntimeOptions) { } const siblingSessions = subagentParentContext - ? subagentParentContext.sibling_subagent_sessions ?? [] + ? (subagentParentContext.sibling_subagent_sessions ?? []) : childSubagentSessions; siblingSessions.forEach((session) => { @@ -761,8 +784,10 @@ export function useTeamWorkspaceRuntime( const [liveActivityBySessionId, setLiveActivityBySessionId] = useState< Record >({}); - const [activityRefreshVersionBySessionId, setActivityRefreshVersionBySessionId] = - useState>({}); + const [ + activityRefreshVersionBySessionId, + setActivityRefreshVersionBySessionId, + ] = useState>({}); const refreshTimersRef = useRef>({}); const activeSnapshotByIdRef = useRef< Map @@ -853,7 +878,9 @@ export function useTeamWorkspaceRuntime( }, []); useEffect(() => { - const activeSessionIds = new Set(activeSnapshots.map((session) => session.id)); + const activeSessionIds = new Set( + activeSnapshots.map((session) => session.id), + ); Object.entries(refreshTimersRef.current).forEach(([sessionId, timerId]) => { if (activeSessionIds.has(sessionId)) { @@ -884,12 +911,14 @@ export function useTeamWorkspaceRuntime( }); setLiveRuntimeBySessionId((previous) => { - const nextEntries = Object.entries(previous).filter(([sessionId, live]) => { - if (!activeSessionIds.has(sessionId)) { - return false; - } - return baseFingerprintById.get(sessionId) === live.baseFingerprint; - }); + const nextEntries = Object.entries(previous).filter( + ([sessionId, live]) => { + if (!activeSessionIds.has(sessionId)) { + return false; + } + return baseFingerprintById.get(sessionId) === live.baseFingerprint; + }, + ); if (nextEntries.length === Object.keys(previous).length) { return previous; } @@ -910,9 +939,7 @@ export function useTeamWorkspaceRuntime( useEffect(() => { const eventNames = [ - currentSessionId - ? currentSessionId - : null, + currentSessionId ? currentSessionId : null, subagentParentContext?.parent_session_id ? subagentParentContext.parent_session_id : null, @@ -934,7 +961,9 @@ export function useTeamWorkspaceRuntime( return; } - const matchingSession = activeSnapshotByIdRef.current.get(data.session_id); + const matchingSession = activeSnapshotByIdRef.current.get( + data.session_id, + ); if (!matchingSession) { return; } @@ -959,7 +988,7 @@ export function useTeamWorkspaceRuntime( normalizedStatus === "closed" || normalizedStatus === "idle" ? 0 - : current?.queuedTurnCount ?? matchingSession.queuedTurnCount; + : (current?.queuedTurnCount ?? matchingSession.queuedTurnCount); if ( current?.runtimeStatus === normalizedStatus && current.latestTurnStatus === normalizedStatus && @@ -1066,7 +1095,8 @@ export function useTeamWorkspaceRuntime( if (projection.rememberTool) { toolNameBySessionIdRef.current[sessionId] = { ...(toolNameBySessionIdRef.current[sessionId] ?? {}), - [projection.rememberTool.toolId]: projection.rememberTool.toolName, + [projection.rememberTool.toolId]: + projection.rememberTool.toolName, }; } @@ -1106,7 +1136,8 @@ export function useTeamWorkspaceRuntime( if (Object.keys(nextStreamState).length === 0) { delete liveStreamStateBySessionIdRef.current[sessionId]; } else { - liveStreamStateBySessionIdRef.current[sessionId] = nextStreamState; + liveStreamStateBySessionIdRef.current[sessionId] = + nextStreamState; } } @@ -1159,7 +1190,8 @@ export function useTeamWorkspaceRuntime( current.teamQueuedCount === nextState.teamQueuedCount && current.providerConcurrencyGroup === nextState.providerConcurrencyGroup && - current.providerParallelBudget === nextState.providerParallelBudget && + current.providerParallelBudget === + nextState.providerParallelBudget && current.queueReason === nextState.queueReason && current.retryableOverload === nextState.retryableOverload && current.baseFingerprint === nextState.baseFingerprint @@ -1182,7 +1214,10 @@ export function useTeamWorkspaceRuntime( projection.clearEntryIds ?? [], ); const nextEntries = projection.entry - ? upsertLiveActivityEntries(nextWithoutTransient, projection.entry) + ? upsertLiveActivityEntries( + nextWithoutTransient, + projection.entry, + ) : nextWithoutTransient; if (nextEntries === existingEntries) { diff --git a/src/components/agent/chat/hooks/useThemeContextWorkspace.test.tsx b/src/components/agent/chat/hooks/useThemeContextWorkspace.test.tsx index a43c0ca02..a9daa24a8 100644 --- a/src/components/agent/chat/hooks/useThemeContextWorkspace.test.tsx +++ b/src/components/agent/chat/hooks/useThemeContextWorkspace.test.tsx @@ -52,9 +52,13 @@ beforeEach(() => { title: "默认搜索上下文", summary: "默认搜索摘要", citations: [], - rawResponse: '{"title":"默认搜索上下文","summary":"默认搜索摘要","citations":[]}', + rawResponse: + '{"title":"默认搜索上下文","summary":"默认搜索摘要","citations":[]}', + }); + mockUseMaterials.mockReturnValue({ + materials: [], + getContent: vi.fn().mockResolvedValue(""), }); - mockUseMaterials.mockReturnValue({ materials: [], getContent: vi.fn().mockResolvedValue("") }); }); afterEach(() => { @@ -244,7 +248,9 @@ describe("useThemeContextWorkspace", () => { const contentItems = snapshot!.sidebarContextItems.filter( (item) => item.source === "content", ); - const sameTitleItems = contentItems.filter((item) => item.name === "新帖子"); + const sameTitleItems = contentItems.filter( + (item) => item.name === "新帖子", + ); expect(sameTitleItems).toHaveLength(1); expect(sameTitleItems[0]?.id).toBe("content:content-new"); @@ -257,12 +263,11 @@ describe("useThemeContextWorkspace", () => { mockIsSpecializedWorkbenchTheme.mockReturnValue(true); mockSearchThemeContextWithWebSearch.mockResolvedValue({ title: "智能体泡沫观察", - summary: "2026 年市场对智能体基建和应用进入分化阶段,讨论聚焦落地成本、场景ROI与平台生态。", - citations: [ - { title: "官方博客", url: "https://example.com/blog" }, - ], + summary: + "2026 年市场对智能体基建和应用进入分化阶段,讨论聚焦落地成本、场景ROI与平台生态。", + citations: [{ title: "官方博客", url: "https://example.com/blog" }], rawResponse: - '{"title":"智能体泡沫观察","summary":"2026 年市场对智能体基建和应用进入分化阶段,讨论聚焦落地成本、场景ROI与平台生态。","citations":[{"title":"官方博客","url":"https://example.com/blog"}]}' , + '{"title":"智能体泡沫观察","summary":"2026 年市场对智能体基建和应用进入分化阶段,讨论聚焦落地成本、场景ROI与平台生态。","citations":[{"title":"官方博客","url":"https://example.com/blog"}]}', }); let snapshot: ThemeContextWorkspaceState | null = null; @@ -297,17 +302,25 @@ describe("useThemeContextWorkspace", () => { expect(snapshot!.contextSearchQuery).toBe(""); expect(snapshot!.sidebarContextItems[0]?.source).toBe("search"); expect(snapshot!.sidebarContextItems[0]?.active).toBe(true); - expect(snapshot!.sidebarContextItems[0]?.previewText).toContain("2026 年市场"); - expect(snapshot!.sidebarContextItems[0]?.citations?.[0]?.url).toBe("https://example.com/blog"); + expect(snapshot!.sidebarContextItems[0]?.previewText).toContain( + "2026 年市场", + ); + expect(snapshot!.sidebarContextItems[0]?.citations?.[0]?.url).toBe( + "https://example.com/blog", + ); expect(snapshot!.activeContextPrompt).toContain("智能体泡沫观察"); expect(snapshot!.activeContextPrompt).toContain("https://example.com/blog"); }); it("prepareActiveContextPrompt 应加载本地正文并拼入提示词", async () => { mockIsSpecializedWorkbenchTheme.mockReturnValue(true); - const mockMaterialGetContent = vi.fn().mockResolvedValue("品牌手册正文,包含品牌定位、目标人群和传播语气。"); + const mockMaterialGetContent = vi + .fn() + .mockResolvedValue("品牌手册正文,包含品牌定位、目标人群和传播语气。"); mockUseMaterials.mockReturnValue({ - materials: [{ id: "m1", name: "品牌手册", description: "品牌资产", tags: [] }], + materials: [ + { id: "m1", name: "品牌手册", description: "品牌资产", tags: [] }, + ], getContent: mockMaterialGetContent, }); diff --git a/src/components/agent/chat/hooks/useThemeContextWorkspace.ts b/src/components/agent/chat/hooks/useThemeContextWorkspace.ts index 4b59ff8c3..47db40f50 100644 --- a/src/components/agent/chat/hooks/useThemeContextWorkspace.ts +++ b/src/components/agent/chat/hooks/useThemeContextWorkspace.ts @@ -1,5 +1,9 @@ import { useCallback, useEffect, useMemo, useRef, useState } from "react"; -import { getContent, listContents, type ContentListItem } from "@/lib/api/project"; +import { + getContent, + listContents, + type ContentListItem, +} from "@/lib/api/project"; import { extractArtifactProtocolPathsFromValue } from "@/lib/artifact-protocol"; import { normalizeProjectId } from "../utils/topicProjectResolution"; import { useMaterials } from "@/hooks/useMaterials"; @@ -95,14 +99,8 @@ export interface ThemeContextWorkspaceState { content: string; name?: string; }) => Promise; - addLinkContext: (payload: { - url: string; - name?: string; - }) => Promise; - addFileContext: (payload: { - path: string; - name?: string; - }) => Promise; + addLinkContext: (payload: { url: string; name?: string }) => Promise; + addFileContext: (payload: { path: string; name?: string }) => Promise; sidebarContextItems: SidebarContextItem[]; toggleContextActive: (contextId: string) => void; getContextDetail: (contextId: string) => ContextCatalogItem | null; @@ -185,7 +183,9 @@ function normalizeText(value: string): string { function truncateContextBody(value: string, source: ContextSource): string { const normalized = normalizeText(value); const maxLength = - source === "search" ? SEARCH_CONTEXT_PREVIEW_LENGTH : LOCAL_CONTEXT_PREVIEW_LENGTH; + source === "search" + ? SEARCH_CONTEXT_PREVIEW_LENGTH + : LOCAL_CONTEXT_PREVIEW_LENGTH; if (normalized.length <= maxLength) { return normalized; } @@ -216,10 +216,16 @@ function resolvePathFileName(path: string): string { } function resolveContentTimestamp(content: ContentListItem): number { - if (typeof content.updated_at === "number" && Number.isFinite(content.updated_at)) { + if ( + typeof content.updated_at === "number" && + Number.isFinite(content.updated_at) + ) { return content.updated_at; } - if (typeof content.created_at === "number" && Number.isFinite(content.created_at)) { + if ( + typeof content.created_at === "number" && + Number.isFinite(content.created_at) + ) { return content.created_at; } return 0; @@ -373,7 +379,11 @@ function buildActiveContextPromptText( lines.push(`摘要:${body}`); } - if (item.source === "search" && item.citations && item.citations.length > 0) { + if ( + item.source === "search" && + item.citations && + item.citations.length > 0 + ) { lines.push("来源:"); item.citations.slice(0, 5).forEach((citation) => { lines.push(`- ${citation.title} ${citation.url}`); @@ -397,7 +407,8 @@ function resolveContextTokenEstimate( contextBodyById: Record, ): number { const cachedBody = contextBodyById[item.id]; - const sourceText = cachedBody || item.bodyText || item.previewText || item.name; + const sourceText = + cachedBody || item.bodyText || item.previewText || item.name; return estimateTokens(sourceText, item.estimatedTokens); } @@ -433,10 +444,14 @@ export function useThemeContextWorkspace({ const [contextSearchMode, setContextSearchModeState] = useState("web"); const [contextSearchLoading, setContextSearchLoading] = useState(false); - const [contextSearchError, setContextSearchError] = useState(null); + const [contextSearchError, setContextSearchError] = useState( + null, + ); const [activeContextIds, setActiveContextIds] = useState([]); const [manualContextIds, setManualContextIds] = useState([]); - const [contextBodyById, setContextBodyById] = useState>({}); + const [contextBodyById, setContextBodyById] = useState< + Record + >({}); const contextSelectionInitializedRef = useRef(null); const contextSnapshotByToolCallRef = useRef>({}); const contextBodyByIdRef = useRef>({}); @@ -543,18 +558,20 @@ export function useThemeContextWorkspace({ }); const dedupedProjectContents = dedupeContentsByTitle(projectContents); - const contentItems: ContextCatalogItem[] = dedupedProjectContents.map((content) => { - const snippet = `${content.title} ${content.content_type || ""} ${content.status || ""}`; - return { - id: `content:${content.id}`, - name: content.title, - source: "content", - normalizedText: normalizeText(snippet).toLowerCase(), - previewText: truncateContextBody(snippet, "content"), - estimatedTokens: 320, - createdAt: resolveContentTimestamp(content) || undefined, - }; - }); + const contentItems: ContextCatalogItem[] = dedupedProjectContents.map( + (content) => { + const snippet = `${content.title} ${content.content_type || ""} ${content.status || ""}`; + return { + id: `content:${content.id}`, + name: content.title, + source: "content", + normalizedText: normalizeText(snippet).toLowerCase(), + previewText: truncateContextBody(snippet, "content"), + estimatedTokens: 320, + createdAt: resolveContentTimestamp(content) || undefined, + }; + }, + ); return [...generatedItems, ...materialItems, ...contentItems]; }, [enabled, generatedSearchContexts, materials, projectContents]); @@ -592,7 +609,9 @@ export function useThemeContextWorkspace({ const manualStorageKey = buildManualContextStorageKey(contextProjectId); const persisted = loadTransient(storageKey, []); const manualPersisted = loadTransient(manualStorageKey, []); - const validPersisted = persisted.filter((id) => contextCatalog.some((item) => item.id === id)); + const validPersisted = persisted.filter((id) => + contextCatalog.some((item) => item.id === id), + ); const validManualPersisted = manualPersisted.filter((id) => contextCatalog.some((item) => item.id === id), ); @@ -604,7 +623,9 @@ export function useThemeContextWorkspace({ ); } else { setActiveContextIds( - contextCatalog.slice(0, DEFAULT_ACTIVE_CONTEXT_COUNT).map((item) => item.id), + contextCatalog + .slice(0, DEFAULT_ACTIVE_CONTEXT_COUNT) + .map((item) => item.id), ); setManualContextIds([]); } @@ -623,7 +644,10 @@ export function useThemeContextWorkspace({ setActiveContextIds((previous) => { const validSet = new Set(contextCatalog.map((item) => item.id)); const normalized = previous.filter((id) => validSet.has(id)); - const targetCount = Math.min(DEFAULT_ACTIVE_CONTEXT_COUNT, contextCatalog.length); + const targetCount = Math.min( + DEFAULT_ACTIVE_CONTEXT_COUNT, + contextCatalog.length, + ); if (normalized.length >= targetCount) { return normalized; } @@ -663,7 +687,9 @@ export function useThemeContextWorkspace({ return validActive; } - const removable = validActive.filter((id) => !manualContextIds.includes(id)); + const removable = validActive.filter( + (id) => !manualContextIds.includes(id), + ); if (removable.length === 0) { return validActive.slice(0, DEFAULT_CONTEXT_ITEM_LIMIT); } @@ -680,7 +706,10 @@ export function useThemeContextWorkspace({ }); }, [contextCatalog, enabled, manualContextIds]); - const activeContextSet = useMemo(() => new Set(activeContextIds), [activeContextIds]); + const activeContextSet = useMemo( + () => new Set(activeContextIds), + [activeContextIds], + ); const activeContextTokenUsage = useMemo( () => @@ -731,7 +760,9 @@ export function useThemeContextWorkspace({ } if (removeSet.size > 0) { - setActiveContextIds((previous) => previous.filter((id) => !removeSet.has(id))); + setActiveContextIds((previous) => + previous.filter((id) => !removeSet.has(id)), + ); } }, [ activeContextTokenUsage, @@ -749,9 +780,13 @@ export function useThemeContextWorkspace({ } if (item.source === "search") { - const summary = normalizeText(item.bodyText || item.previewText || item.name); + const summary = normalizeText( + item.bodyText || item.previewText || item.name, + ); setContextBodyById((previous) => - previous[item.id] === summary ? previous : { ...previous, [item.id]: summary }, + previous[item.id] === summary + ? previous + : { ...previous, [item.id]: summary }, ); return summary; } @@ -785,7 +820,10 @@ export function useThemeContextWorkspace({ return normalizedBody; } catch (error) { console.warn("[useThemeContextWorkspace] 加载上下文正文失败:", error); - const fallbackBody = truncateContextBody(item.previewText || item.name, item.source); + const fallbackBody = truncateContextBody( + item.previewText || item.name, + item.source, + ); setContextBodyById((previous) => previous[item.id] === fallbackBody ? previous @@ -856,7 +894,10 @@ export function useThemeContextWorkspace({ : "failed"; const logId = `${message.id}-${toolCall.id}`; const existingSnapshot = contextSnapshotByToolCallRef.current[logId]; - if (!existingSnapshot || (existingSnapshot.length === 0 && activeContextIds.length > 0)) { + if ( + !existingSnapshot || + (existingSnapshot.length === 0 && activeContextIds.length > 0) + ) { contextSnapshotByToolCallRef.current[logId] = [...activeContextIds]; } @@ -878,7 +919,10 @@ export function useThemeContextWorkspace({ name: toolCall.name, status, timeLabel: formatLogTimeLabel(message.timestamp), - durationLabel: formatDurationLabel(toolCall.startTime, toolCall.endTime), + durationLabel: formatDurationLabel( + toolCall.startTime, + toolCall.endTime, + ), applyTarget: resolveApplyTarget(toolCall.name), contextIds: contextSnapshotByToolCallRef.current[logId], inputSummary, @@ -895,7 +939,8 @@ export function useThemeContextWorkspace({ }, [activeContextIds, enabled, messages]); const activeContextPrompt = useMemo( - () => buildActiveContextPromptText(orderedActiveContextItems, contextBodyById), + () => + buildActiveContextPromptText(orderedActiveContextItems, contextBodyById), [contextBodyById, orderedActiveContextItems], ); @@ -922,7 +967,10 @@ export function useThemeContextWorkspace({ mergedContextBodies[id] = body; }); - return buildActiveContextPromptText(orderedActiveContextItems, mergedContextBodies); + return buildActiveContextPromptText( + orderedActiveContextItems, + mergedContextBodies, + ); }, [enabled, loadContextBody, orderedActiveContextItems]); const setContextSearchQuery = useCallback((value: string) => { @@ -981,7 +1029,10 @@ export function useThemeContextWorkspace({ mode: contextSearchMode, }); - const contextId = buildGeneratedContextId(contextSearchMode, trimmedQuery); + const contextId = buildGeneratedContextId( + contextSearchMode, + trimmedQuery, + ); const nextContext: GeneratedSearchContextItem = { id: contextId, name: result.title, @@ -1011,7 +1062,9 @@ export function useThemeContextWorkspace({ ); } catch (error) { setContextSearchError( - error instanceof Error ? error.message : String(error || "上下文搜索失败"), + error instanceof Error + ? error.message + : String(error || "上下文搜索失败"), ); } finally { setContextSearchLoading(false); @@ -1040,7 +1093,10 @@ export function useThemeContextWorkspace({ throw new Error("请输入文本内容"); } - const firstLine = normalizedContent.split(/\r?\n/).find((line) => line.trim().length > 0) || ""; + const firstLine = + normalizedContent + .split(/\r?\n/) + .find((line) => line.trim().length > 0) || ""; const material = await uploadMaterial({ projectId: contextProjectId, name: resolveContextTitle(payload.name || firstLine, "文本上下文"), @@ -1119,7 +1175,10 @@ export function useThemeContextWorkspace({ const material = await uploadMaterial({ projectId: contextProjectId, - name: resolveContextTitle(payload.name || resolvePathFileName(normalizedPath), "上下文文件"), + name: resolveContextTitle( + payload.name || resolvePathFileName(normalizedPath), + "上下文文件", + ), type: "document", filePath: normalizedPath, tags: ["上下文", "文件"], diff --git a/src/components/agent/chat/hooks/useThemeScopedChatToolPreferences.test.tsx b/src/components/agent/chat/hooks/useThemeScopedChatToolPreferences.test.tsx index da0af603c..c742a13c1 100644 --- a/src/components/agent/chat/hooks/useThemeScopedChatToolPreferences.test.tsx +++ b/src/components/agent/chat/hooks/useThemeScopedChatToolPreferences.test.tsx @@ -1,13 +1,6 @@ import { act } from "react"; import { createRoot } from "react-dom/client"; -import { - afterEach, - beforeEach, - describe, - expect, - it, - vi, -} from "vitest"; +import { afterEach, beforeEach, describe, expect, it, vi } from "vitest"; import { saveChatToolPreferences, type ChatToolPreferences, @@ -350,9 +343,7 @@ describe("useThemeScopedChatToolPreferences", () => { }); expect( - harness - .getValue() - .getSyncedSessionRecentPreferences("session-sync-1"), + harness.getValue().getSyncedSessionRecentPreferences("session-sync-1"), ).toEqual({ webSearch: true, thinking: true, diff --git a/src/components/agent/chat/hooks/useThemeScopedChatToolPreferences.ts b/src/components/agent/chat/hooks/useThemeScopedChatToolPreferences.ts index 098145be0..584b05078 100644 --- a/src/components/agent/chat/hooks/useThemeScopedChatToolPreferences.ts +++ b/src/components/agent/chat/hooks/useThemeScopedChatToolPreferences.ts @@ -109,7 +109,10 @@ export function useThemeScopedChatToolPreferences( ); }) .catch((error) => { - console.warn("[AgentChat] 回写会话 recent_preferences 失败:", error); + console.warn( + "[AgentChat] 回写会话 recent_preferences 失败:", + error, + ); }); }); }, @@ -142,12 +145,21 @@ export function useThemeScopedChatToolPreferences( return; } - const nextPreferences = runtimePreferences ?? loadChatToolPreferences(theme); + const nextPreferences = + runtimePreferences ?? loadChatToolPreferences(theme); const currentSessionId = sessionSync?.getSessionId()?.trim(); if (runtimePreferences && currentSessionId) { - syncedSessionPreferenceRef.current.set(currentSessionId, nextPreferences); + syncedSessionPreferenceRef.current.set( + currentSessionId, + nextPreferences, + ); } - if (areSameChatToolPreferences(chatToolPreferencesRef.current, nextPreferences)) { + if ( + areSameChatToolPreferences( + chatToolPreferencesRef.current, + nextPreferences, + ) + ) { lastHydratedSourceRef.current = nextSourceKey; return; } diff --git a/src/components/agent/chat/hooks/useTopicBranchBoard.test.tsx b/src/components/agent/chat/hooks/useTopicBranchBoard.test.tsx index dee735753..e1b0fc31c 100644 --- a/src/components/agent/chat/hooks/useTopicBranchBoard.test.tsx +++ b/src/components/agent/chat/hooks/useTopicBranchBoard.test.tsx @@ -32,7 +32,10 @@ interface ProbeProps { projectId?: string; currentTopicId: string | null; onSnapshot: (value: ReturnType) => void; - externalStatusMap?: Record; + externalStatusMap?: Record< + string, + "in_progress" | "pending" | "merged" | "candidate" + >; onStatusMapChange?: ( next: Record, ) => void; @@ -82,7 +85,9 @@ describe("useTopicBranchBoard", () => { ); }); - const current = snapshot?.branchItems.find((item: { id: string }) => item.id === "topic-a"); + const current = snapshot?.branchItems.find( + (item: { id: string }) => item.id === "topic-a", + ); expect(current?.status).toBe("in_progress"); }); @@ -110,7 +115,9 @@ describe("useTopicBranchBoard", () => { snapshot?.setTopicStatus("topic-b", "merged"); }); - const merged = snapshot?.branchItems.find((item: { id: string }) => item.id === "topic-b"); + const merged = snapshot?.branchItems.find( + (item: { id: string }) => item.id === "topic-b", + ); expect(merged?.status).toBe("merged"); }); @@ -142,15 +149,22 @@ describe("useTopicBranchBoard", () => { ); }); - const current = snapshot?.branchItems.find((item: { id: string }) => item.id === "topic-a"); - const merged = snapshot?.branchItems.find((item: { id: string }) => item.id === "topic-b"); + const current = snapshot?.branchItems.find( + (item: { id: string }) => item.id === "topic-a", + ); + const merged = snapshot?.branchItems.find( + (item: { id: string }) => item.id === "topic-b", + ); expect(current?.status).toBe("in_progress"); expect(merged?.status).toBe("merged"); }); it("外部托管模式应回调状态变更", async () => { let snapshot = null as ReturnType | null; - let controlledMap: Record = { + let controlledMap: Record< + string, + "in_progress" | "pending" | "merged" | "candidate" + > = { "topic-a": "in_progress", "topic-b": "candidate", }; diff --git a/src/components/agent/chat/hooks/useTopicBranchBoard.ts b/src/components/agent/chat/hooks/useTopicBranchBoard.ts index f8ce61f2b..d2d35fce3 100644 --- a/src/components/agent/chat/hooks/useTopicBranchBoard.ts +++ b/src/components/agent/chat/hooks/useTopicBranchBoard.ts @@ -35,7 +35,9 @@ function buildStorageKey(projectId: string): string { return `${TOPIC_BRANCH_STATUS_KEY_PREFIX}${projectId}`; } -function loadBranchStatusMap(storageKey: string): Record { +function loadBranchStatusMap( + storageKey: string, +): Record { try { const raw = sessionStorage.getItem(storageKey); if (!raw) { @@ -95,8 +97,12 @@ export function useTopicBranchBoard({ onStatusMapChange, }: UseTopicBranchBoardOptions) { const normalizedProjectId = normalizeProjectId(projectId); - const storageKey = normalizedProjectId ? buildStorageKey(normalizedProjectId) : null; - const [innerStatusMap, setInnerStatusMap] = useState>({}); + const storageKey = normalizedProjectId + ? buildStorageKey(normalizedProjectId) + : null; + const [innerStatusMap, setInnerStatusMap] = useState< + Record + >({}); const useExternalState = !!(externalStatusMap && onStatusMapChange); const statusMap = useExternalState ? externalStatusMap : innerStatusMap; @@ -104,11 +110,14 @@ export function useTopicBranchBoard({ ( updater: | Record - | ((previous: Record) => Record), + | (( + previous: Record, + ) => Record), ) => { if (useExternalState) { const previous = externalStatusMap || {}; - const next = typeof updater === "function" ? updater(previous) : updater; + const next = + typeof updater === "function" ? updater(previous) : updater; onStatusMapChange?.(next); return; } @@ -191,8 +200,7 @@ export function useTopicBranchBoard({ return { id: topic.id, title: topic.title, - status: - statusMap[topic.id] || resolveDefaultStatus(topic, isCurrent), + status: statusMap[topic.id] || resolveDefaultStatus(topic, isCurrent), isCurrent, }; }), diff --git a/src/components/agent/chat/hooks/useTrayModelShortcuts.test.ts b/src/components/agent/chat/hooks/useTrayModelShortcuts.test.ts index fed6f26e0..a0c395bf3 100644 --- a/src/components/agent/chat/hooks/useTrayModelShortcuts.test.ts +++ b/src/components/agent/chat/hooks/useTrayModelShortcuts.test.ts @@ -81,7 +81,11 @@ describe("buildTrayPayload", () => { }); it("相同签名的 payload 会复用缓存,避免重复拉取数据", async () => { - const first = await buildTrayPayload("deepseek", "deepseek-chat", "general"); + const first = await buildTrayPayload( + "deepseek", + "deepseek-chat", + "general", + ); const second = await buildTrayPayload( "deepseek", "deepseek-chat", diff --git a/src/components/agent/chat/hooks/useWorkspaceProjectSelection.test.tsx b/src/components/agent/chat/hooks/useWorkspaceProjectSelection.test.tsx index e3f65ea35..609e53abf 100644 --- a/src/components/agent/chat/hooks/useWorkspaceProjectSelection.test.tsx +++ b/src/components/agent/chat/hooks/useWorkspaceProjectSelection.test.tsx @@ -133,7 +133,9 @@ describe("useWorkspaceProjectSelection", () => { expect( harness.getValue().consumePendingTopicSwitch("project-other"), ).toBeNull(); - expect(harness.getValue().consumePendingTopicSwitch("project-target")).toEqual({ + expect( + harness.getValue().consumePendingTopicSwitch("project-target"), + ).toEqual({ topicId: "topic-1", targetProjectId: "project-target", }); diff --git a/src/components/agent/chat/hooks/useWorkspaceProjectSelection.ts b/src/components/agent/chat/hooks/useWorkspaceProjectSelection.ts index b838f2857..5158e8836 100644 --- a/src/components/agent/chat/hooks/useWorkspaceProjectSelection.ts +++ b/src/components/agent/chat/hooks/useWorkspaceProjectSelection.ts @@ -88,7 +88,11 @@ export function useWorkspaceProjectSelection( rememberProjectId(normalizedProjectId); setInternalProjectId(normalizedProjectId); }, - [clearProjectSelectionRuntime, normalizedExternalProjectId, rememberProjectId], + [ + clearProjectSelectionRuntime, + normalizedExternalProjectId, + rememberProjectId, + ], ); const startTopicProjectResolution = useCallback(() => { diff --git a/src/components/agent/chat/index.test.tsx b/src/components/agent/chat/index.test.tsx index f32bf35ad..782323508 100644 --- a/src/components/agent/chat/index.test.tsx +++ b/src/components/agent/chat/index.test.tsx @@ -2390,7 +2390,9 @@ describe("AgentChatPage 通用工作台", () => { ?.getAttribute("data-mode"), ).toBe("chat-canvas"); expect( - mounted.container.querySelector('[data-testid="canvas-workbench-layout-mock"]'), + mounted.container.querySelector( + '[data-testid="canvas-workbench-layout-mock"]', + ), ).not.toBeNull(); clickButton(mounted.container, "toggle-canvas"); @@ -2402,7 +2404,9 @@ describe("AgentChatPage 通用工作台", () => { ?.getAttribute("data-mode"), ).toBe("chat"); expect( - mounted.container.querySelector('[data-testid="team-workspace-dock-activate"]'), + mounted.container.querySelector( + '[data-testid="team-workspace-dock-activate"]', + ), ).toBeNull(); }); @@ -3076,14 +3080,7 @@ describe("AgentChatPage 通用工作台", () => { expect(sharedSendMessageMock).toHaveBeenCalledTimes(1); expect(sharedSendMessageMock.mock.calls[0]).toEqual( - expect.arrayContaining([ - prompt, - [], - false, - false, - false, - "auto", - ]), + expect.arrayContaining([prompt, [], false, false, false, "auto"]), ); expect(sharedSendMessageMock.mock.calls[0]?.[8]).toMatchObject({ requestMetadata: { @@ -4873,9 +4870,7 @@ describe("AgentChatPage 自动引导", () => { workflowSteps?: Array<{ title: string; status: string }>; } | undefined; - expect(latestInputbarProps?.workflowGate?.key).toBe( - "publish_confirm", - ); + expect(latestInputbarProps?.workflowGate?.key).toBe("publish_confirm"); const workflowSteps = latestInputbarProps?.workflowSteps || []; expect(workflowSteps.length).toBeGreaterThan(0); expect(workflowSteps.at(-1)?.status).toBe("active"); diff --git a/src/components/agent/chat/protocol-fact-source-guard.test.ts b/src/components/agent/chat/protocol-fact-source-guard.test.ts index 7b56bee94..b8670a7ea 100644 --- a/src/components/agent/chat/protocol-fact-source-guard.test.ts +++ b/src/components/agent/chat/protocol-fact-source-guard.test.ts @@ -54,7 +54,8 @@ const FORBIDDEN_PATTERNS: Array<{ label: string; pattern: RegExp }> = [ }, { label: "filesystem event 目录位置 hint 旧 readString fallback", - pattern: /readString\(\s*(?:args|metadata)\s*,\s*\["directory",\s*"cwd"\]\s*\)/, + pattern: + /readString\(\s*(?:args|metadata)\s*,\s*\["directory",\s*"cwd"\]\s*\)/, }, { label: "filesystem event 输出位置 hint 旧 readString fallback", @@ -87,7 +88,7 @@ describe("agent chat protocol fact source guard", () => { for (const filePath of files) { const content = readFileSync(filePath, "utf8"); - if (content.includes('@/lib/api/agentStream')) { + if (content.includes("@/lib/api/agentStream")) { offenders.push(relative(process.cwd(), filePath)); } } @@ -108,7 +109,7 @@ describe("agent chat protocol fact source guard", () => { for (const filePath of files) { const content = readFileSync(filePath, "utf8"); if ( - content.includes('./agentStream') || + content.includes("./agentStream") || content.includes("./agentStream") ) { offenders.push(relative(process.cwd(), filePath)); diff --git a/src/components/agent/chat/service-skills/ServiceSkillLaunchDialog.test.tsx b/src/components/agent/chat/service-skills/ServiceSkillLaunchDialog.test.tsx index 210cfb75e..d9f79bce0 100644 --- a/src/components/agent/chat/service-skills/ServiceSkillLaunchDialog.test.tsx +++ b/src/components/agent/chat/service-skills/ServiceSkillLaunchDialog.test.tsx @@ -88,8 +88,7 @@ const MOCK_SKILL: ServiceSkillHomeItem = { ], triggerHints: ["已经有参考视频,希望快速得到一版同结构脚本时使用。"], examples: ["参考这个视频结构,帮我先写一版节奏接近的脚本。"], - outputDestination: - "结果会写回当前工作区中的脚本草稿,方便继续补镜头与口播。", + outputDestination: "结果会写回当前工作区中的脚本草稿,方便继续补镜头与口播。", source: "cloud_catalog", runnerType: "instant", defaultExecutorBinding: "agent_turn", @@ -135,7 +134,8 @@ const METADATA_ONLY_SKILL: ServiceSkillHomeItem = { description: "只通过标准摘要回退展示技能类型与结果去向。", metadata: { Lime_skill_type: "site", - Lime_output_destination: "结果会回流到当前工作区顶部结果区,便于继续整理。", + Lime_output_destination: + "结果会回流到当前工作区顶部结果区,便于继续整理。", }, resourceSummary: { hasScripts: false, @@ -161,7 +161,8 @@ beforeEach(() => { domain: "github.com", profile_key: "attached-github", target_id: "tab-github", - message: "已检测到 github.com 的真实浏览器页面,Claw 可以直接复用当前会话执行。", + message: + "已检测到 github.com 的真实浏览器页面,Claw 可以直接复用当前会话执行。", }); }); @@ -516,8 +517,12 @@ describe("ServiceSkillLaunchDialog", () => { await flushEffects(); expect(document.body.textContent).toContain("需要先准备浏览器"); - expect(document.body.textContent).toContain("Claw 不会在后台偷偷启动浏览器"); - expect(document.body.textContent).toContain("请先进入浏览器工作台连接真实浏览器并打开目标站点页面"); + expect(document.body.textContent).toContain( + "Claw 不会在后台偷偷启动浏览器", + ); + expect(document.body.textContent).toContain( + "请先进入浏览器工作台连接真实浏览器并打开目标站点页面", + ); expect(launchButton?.disabled).toBe(true); expect(launchButton?.textContent).toBe("先准备浏览器再执行"); }); @@ -535,7 +540,8 @@ describe("ServiceSkillLaunchDialog", () => { defaultExecutorBinding: "cloud_scene", runnerLabel: "云端托管执行", runnerTone: "slate", - runnerDescription: "会提交到 OEM 云端执行,完成后再把结果回流到当前工作区。", + runnerDescription: + "会提交到 OEM 云端执行,完成后再把结果回流到当前工作区。", actionLabel: "提交云端", outputDestination: "运行结果会在云端完成后回流到当前工作区。", }, diff --git a/src/components/agent/chat/service-skills/ServiceSkillLaunchDialog.tsx b/src/components/agent/chat/service-skills/ServiceSkillLaunchDialog.tsx index a8045d87e..c60e47bba 100644 --- a/src/components/agent/chat/service-skills/ServiceSkillLaunchDialog.tsx +++ b/src/components/agent/chat/service-skills/ServiceSkillLaunchDialog.tsx @@ -375,7 +375,9 @@ export function ServiceSkillLaunchDialog({ {siteLaunchReadiness.phase === "blocked" && canOpenBrowserRuntime ? (

- {buildSiteLaunchBlockedMessage(siteLaunchReadiness.result)} + {buildSiteLaunchBlockedMessage( + siteLaunchReadiness.result, + )}

) : null} ), ); @@ -72,7 +72,7 @@ export const ArtifactWorkbenchToolbarActions: React.FC ( -
+
{showSaveToProject ? ( { project_root_path: "/Users/coso/.proxycast/projects/project-1", markdown_relative_path: "exports/x-article-export/github-mcp/index.md", - images_relative_dir: - "exports/x-article-export/github-mcp/images", + images_relative_dir: "exports/x-article-export/github-mcp/images", image_count: 7, }, }, diff --git a/src/components/agent/chat/workspace/ServiceSkillExecutionCard.tsx b/src/components/agent/chat/workspace/ServiceSkillExecutionCard.tsx index 4b96b101a..093983f94 100644 --- a/src/components/agent/chat/workspace/ServiceSkillExecutionCard.tsx +++ b/src/components/agent/chat/workspace/ServiceSkillExecutionCard.tsx @@ -15,7 +15,7 @@ const PHASE_LABELS: Record = { }; const PHASE_TONE_CLASSES: Record = { - running: "border-sky-200 bg-sky-50 text-sky-900", + running: "border-emerald-200 bg-emerald-50 text-emerald-900", success: "border-emerald-200 bg-emerald-50 text-emerald-900", error: "border-rose-200 bg-rose-50 text-rose-900", blocked: "border-amber-200 bg-amber-50 text-amber-900", diff --git a/src/components/agent/chat/workspace/WorkspaceContentSyncNotice.tsx b/src/components/agent/chat/workspace/WorkspaceContentSyncNotice.tsx index edea9de12..2cb7bba09 100644 --- a/src/components/agent/chat/workspace/WorkspaceContentSyncNotice.tsx +++ b/src/components/agent/chat/workspace/WorkspaceContentSyncNotice.tsx @@ -6,9 +6,7 @@ interface WorkspaceContentSyncNoticeProps { status: Exclude; } -function resolveContentSyncNoticeMeta( - status: Exclude, -): { +function resolveContentSyncNoticeMeta(status: Exclude): { label: string; Icon: typeof Loader2; animated?: boolean; diff --git a/src/components/agent/chat/workspace/WorkspaceConversationScene.tsx b/src/components/agent/chat/workspace/WorkspaceConversationScene.tsx index 9c1473e00..ce0524776 100644 --- a/src/components/agent/chat/workspace/WorkspaceConversationScene.tsx +++ b/src/components/agent/chat/workspace/WorkspaceConversationScene.tsx @@ -29,11 +29,15 @@ interface WorkspaceConversationSceneProps extends WorkspaceMainSceneProps { serviceSkillExecutionCard?: ComponentProps< typeof WorkspaceChatContent >["serviceSkillExecutionCard"]; - stepProgressProps?: ComponentProps["stepProgressProps"]; + stepProgressProps?: ComponentProps< + typeof WorkspaceChatContent + >["stepProgressProps"]; showChatLayout: boolean; contextWorkspaceEnabled: boolean; generalWorkbenchMessageViewportBottomPadding?: string; - messageListProps: ComponentProps["messageListProps"]; + messageListProps: ComponentProps< + typeof WorkspaceChatContent + >["messageListProps"]; teamWorkspaceDockProps?: ComponentProps< typeof WorkspaceChatContent >["teamWorkspaceDockProps"]; @@ -81,7 +85,9 @@ interface WorkspaceConversationSceneProps extends WorkspaceMainSceneProps { typeof EmptyState >["onCreationModeChange"]; activeTheme: ComponentProps["activeTheme"]; - onThemeChange?: NonNullable["onThemeChange"]>; + onThemeChange?: NonNullable< + ComponentProps["onThemeChange"] + >; themeLocked: boolean; artifactsCount: number; generalCanvasContent?: string | null; @@ -108,7 +114,9 @@ interface WorkspaceConversationSceneProps extends WorkspaceMainSceneProps { isRunning: boolean; navbarChrome: ComponentProps["chrome"]; navbarContextVariant?: "default" | "task-center"; - onToggleHistory: NonNullable["onToggleHistory"]>; + onToggleHistory: NonNullable< + ComponentProps["onToggleHistory"] + >; showHistoryToggle: boolean; onBackToProjectManagement?: ComponentProps< typeof ChatNavbar @@ -119,7 +127,9 @@ interface WorkspaceConversationSceneProps extends WorkspaceMainSceneProps { chatMode: string; showHarnessToggle: boolean; harnessPanelVisible: boolean; - onToggleHarnessPanel?: ComponentProps["onToggleHarnessPanel"]; + onToggleHarnessPanel?: ComponentProps< + typeof ChatNavbar + >["onToggleHarnessPanel"]; harnessPendingCount: number; harnessAttentionLevel: ComponentProps< typeof ChatNavbar @@ -324,7 +334,7 @@ export function WorkspaceConversationScene({ harnessPendingCount, harnessAttentionLevel, harnessToggleLabel: - chatMode === "general" && !isThemeWorkbench ? "工作台" : undefined, + chatMode === "general" && !isThemeWorkbench ? undefined : undefined, showContextCompactionAction, contextCompactionRunning, onCompactContext, diff --git a/src/components/agent/chat/workspace/WorkspaceHarnessDialogs.tsx b/src/components/agent/chat/workspace/WorkspaceHarnessDialogs.tsx index 2274d7834..16798aed8 100644 --- a/src/components/agent/chat/workspace/WorkspaceHarnessDialogs.tsx +++ b/src/components/agent/chat/workspace/WorkspaceHarnessDialogs.tsx @@ -78,11 +78,15 @@ interface GeneralWorkbenchDialogSectionProps extends HarnessPanelBaseProps { activeTheme: ComponentProps["activeTheme"]; toolPreferences: ComponentProps["toolPreferences"]; isSending: ComponentProps["isSending"]; - executionRuntime: ComponentProps["executionRuntime"]; + executionRuntime: ComponentProps< + typeof AgentRuntimeStrip + >["executionRuntime"]; isExecutionRuntimeActive: ComponentProps< typeof AgentRuntimeStrip >["isExecutionRuntimeActive"]; - runtimeStatusTitle: ComponentProps["runtimeStatusTitle"]; + runtimeStatusTitle: ComponentProps< + typeof AgentRuntimeStrip + >["runtimeStatusTitle"]; selectedTeamRoleCount: ComponentProps< typeof AgentRuntimeStrip >["selectedTeamRoleCount"]; diff --git a/src/components/agent/chat/workspace/WorkspaceMainScene.tsx b/src/components/agent/chat/workspace/WorkspaceMainScene.tsx index f89f99ea5..fb0094990 100644 --- a/src/components/agent/chat/workspace/WorkspaceMainScene.tsx +++ b/src/components/agent/chat/workspace/WorkspaceMainScene.tsx @@ -5,7 +5,9 @@ import { WorkspaceCanvasContent } from "./WorkspaceCanvasContent"; import { WorkspaceContentSyncNotice } from "./WorkspaceContentSyncNotice"; import { WorkspaceMainArea } from "./WorkspaceMainArea"; -type WorkspaceCanvasContentProps = ComponentProps; +type WorkspaceCanvasContentProps = ComponentProps< + typeof WorkspaceCanvasContent +>; type WorkspaceMainAreaProps = ComponentProps; interface WorkspaceMainSceneProps { @@ -55,7 +57,9 @@ export function WorkspaceMainScene({ hasPendingA2UIForm, inputbarNode, }: WorkspaceMainSceneProps) { - const navbarNode = chatNavbarProps ? : null; + const navbarNode = chatNavbarProps ? ( + + ) : null; const contentSyncNoticeNode = !isThemeWorkbench && contentId && syncStatus !== "idle" ? ( @@ -79,7 +83,7 @@ export function WorkspaceMainScene({ layoutMode={layoutMode} forceCanvasMode={Boolean( isThemeWorkbench && - (hasLiveCanvasPreviewContent || Boolean(teamWorkbenchView)), + (hasLiveCanvasPreviewContent || Boolean(teamWorkbenchView)), )} chatContent={chatContent} canvasContent={canvasContent} diff --git a/src/components/agent/chat/workspace/WorkspacePageShell.tsx b/src/components/agent/chat/workspace/WorkspacePageShell.tsx index b31fb4116..2829fecfd 100644 --- a/src/components/agent/chat/workspace/WorkspacePageShell.tsx +++ b/src/components/agent/chat/workspace/WorkspacePageShell.tsx @@ -1,7 +1,10 @@ import type { ComponentProps, ReactNode } from "react"; import { PanelLeftOpen } from "lucide-react"; import { ChatSidebar } from "../components/ChatSidebar"; -import { PageContainer, GeneralWorkbenchLeftExpandButton } from "./WorkspaceStyles"; +import { + PageContainer, + GeneralWorkbenchLeftExpandButton, +} from "./WorkspaceStyles"; interface WorkspacePageShellProps { compactChrome: boolean; diff --git a/src/components/agent/chat/workspace/WorkspaceShellScene.tsx b/src/components/agent/chat/workspace/WorkspaceShellScene.tsx index 20f1d8200..fe324a6fb 100644 --- a/src/components/agent/chat/workspace/WorkspaceShellScene.tsx +++ b/src/components/agent/chat/workspace/WorkspaceShellScene.tsx @@ -94,7 +94,9 @@ export function WorkspaceShellScene({ showChatPanel={showChatPanel} showSidebar={showSidebar} chatSidebarProps={chatSidebarProps} - showGeneralWorkbenchLeftExpandButton={showGeneralWorkbenchLeftExpandButton} + showGeneralWorkbenchLeftExpandButton={ + showGeneralWorkbenchLeftExpandButton + } onExpandGeneralWorkbenchSidebar={onExpandGeneralWorkbenchSidebar} mainAreaNode={mainAreaNode} /> diff --git a/src/components/agent/chat/workspace/WorkspaceStyles.tsx b/src/components/agent/chat/workspace/WorkspaceStyles.tsx index be87d4369..0c94f3253 100644 --- a/src/components/agent/chat/workspace/WorkspaceStyles.tsx +++ b/src/components/agent/chat/workspace/WorkspaceStyles.tsx @@ -10,8 +10,8 @@ export const PageContainer = styled.div<{ $compact?: boolean }>` width: 100%; position: relative; min-height: 0; - gap: ${({ $compact }) => ($compact ? "8px" : "14px")}; - padding: ${({ $compact }) => ($compact ? "4px 8px 8px" : "8px 14px 14px")}; + gap: ${({ $compact }) => ($compact ? "8px" : "20px")}; + padding: ${({ $compact }) => ($compact ? "4px 10px 10px" : "12px 20px 20px")}; box-sizing: border-box; overflow: hidden; isolation: isolate; @@ -23,15 +23,10 @@ export const PageContainer = styled.div<{ $compact?: boolean }>` ), radial-gradient( circle at 86% 14%, - rgba(16, 185, 129, 0.06), + rgba(16, 185, 129, 0.04), transparent 28% ), - radial-gradient( - circle at 72% 84%, - rgba(245, 158, 11, 0.04), - transparent 24% - ), - linear-gradient(180deg, #f8fafc 0%, #f8fafc 44%, #f3f8f5 100%); + linear-gradient(180deg, #f8fcf9 0%, #f8fcf9 44%, #f3f8f5 100%); > * { position: relative; @@ -47,13 +42,9 @@ export const MainArea = styled.div<{ $compact?: boolean }>` min-height: 0; overflow: hidden; position: relative; - border: 1px solid rgba(226, 232, 240, 0.88); - border-radius: ${({ $compact }) => ($compact ? "24px" : "32px")}; - background: linear-gradient(180deg, #ffffff 0%, #f8fafc 100%); - box-shadow: - 0 24px 72px -36px rgba(15, 23, 42, 0.18), - 0 16px 28px -24px rgba(15, 23, 42, 0.1), - inset 0 1px 0 rgba(255, 255, 255, 0.76); + background: transparent; + border: none; + box-shadow: none; `; function resolveContentSyncTone(status: SyncStatus): { diff --git a/src/components/agent/chat/workspace/artifactWorkbenchActions.ts b/src/components/agent/chat/workspace/artifactWorkbenchActions.ts index 275beda9b..e56d55481 100644 --- a/src/components/agent/chat/workspace/artifactWorkbenchActions.ts +++ b/src/components/agent/chat/workspace/artifactWorkbenchActions.ts @@ -231,17 +231,16 @@ function resolveChecklistPreview(block: ArtifactDocumentBlock): string { function resolveMetricPreview(block: ArtifactDocumentBlock): string { return resolveMetricItems(block) - .map((item) => [item.label, item.value, item.detail].filter(Boolean).join(": ")) + .map((item) => + [item.label, item.value, item.detail].filter(Boolean).join(": "), + ) .join("\n"); } function resolveTablePreview(block: ArtifactDocumentBlock): string { const columns = resolveTableColumns(block); const rows = resolveTableRows(block, columns); - return [ - columns.join(" | "), - ...rows.map((row) => row.join(" | ")), - ] + return [columns.join(" | "), ...rows.map((row) => row.join(" | "))] .filter(Boolean) .join("\n"); } @@ -307,8 +306,12 @@ function buildArtifactDocumentVersionDiff( ): ArtifactDocumentVersionDiff { const previousBlocks = previousDocument.blocks || []; const nextBlocks = nextDocument.blocks || []; - const previousMap = new Map(previousBlocks.map((block, index) => [block.id, { block, index }])); - const nextMap = new Map(nextBlocks.map((block, index) => [block.id, { block, index }])); + const previousMap = new Map( + previousBlocks.map((block, index) => [block.id, { block, index }]), + ); + const nextMap = new Map( + nextBlocks.map((block, index) => [block.id, { block, index }]), + ); const changedBlocks: ArtifactDocumentBlockDiffEntry[] = []; let addedCount = 0; let removedCount = 0; @@ -334,7 +337,8 @@ function buildArtifactDocumentVersionDiff( const previousIndex = previousEntry.index; const isMoved = previousIndex !== nextIndex; const isUpdated = - serializeBlockForComparison(previousBlock) !== serializeBlockForComparison(block); + serializeBlockForComparison(previousBlock) !== + serializeBlockForComparison(block); if (!isMoved && !isUpdated) { return; @@ -431,7 +435,8 @@ export function createArtifactDocumentNextVersion( return nextDocument; } - const currentVersion = resolveArtifactDocumentCurrentVersion(previousDocument); + const currentVersion = + resolveArtifactDocumentCurrentVersion(previousDocument); const nextVersionNo = resolveNextVersionNo(previousDocument); const nextVersionId = `${nextDocument.artifactId}:v${nextVersionNo}`; const createdAt = new Date().toISOString(); diff --git a/src/components/agent/chat/workspace/artifactWorkbenchDocument.tsx b/src/components/agent/chat/workspace/artifactWorkbenchDocument.tsx index efd6a2c85..b3830bb6d 100644 --- a/src/components/agent/chat/workspace/artifactWorkbenchDocument.tsx +++ b/src/components/agent/chat/workspace/artifactWorkbenchDocument.tsx @@ -274,7 +274,7 @@ function getDiffBadgeClassName(changeType: string): string { case "removed": return "border-rose-200 bg-rose-50 text-rose-700"; case "updated": - return "border-sky-200 bg-sky-50 text-sky-700"; + return "border-emerald-200 bg-emerald-50 text-emerald-700"; case "moved": return "border-amber-200 bg-amber-50 text-amber-700"; default: @@ -331,7 +331,9 @@ function resolveArtifactRecoveryPresentation( return null; } -function serializeEditableArtifactDraft(draft: EditableArtifactBlockDraft): string { +function serializeEditableArtifactDraft( + draft: EditableArtifactBlockDraft, +): string { switch (draft.editorKind) { case "rich_text": return JSON.stringify({ @@ -586,9 +588,7 @@ function resolveEditableArtifactBlocks( blockId: block.id, label: normalizeText(block.title) || `代码块 ${codeBlockIndex}`, detail: - currentSectionLabel || - normalizeText(block.language) || - "代码片段", + currentSectionLabel || normalizeText(block.language) || "代码片段", editorKind: "code_block", draft, }); @@ -619,7 +619,10 @@ function replaceEditableArtifactBlockContent( }; } - if (block.type === "section_header" && draft.editorKind === "section_header") { + if ( + block.type === "section_header" && + draft.editorKind === "section_header" + ) { const title = draft.title.trim(); const description = draft.description.trim(); return { @@ -629,7 +632,10 @@ function replaceEditableArtifactBlockContent( }; } - if (block.type === "hero_summary" && draft.editorKind === "hero_summary") { + if ( + block.type === "hero_summary" && + draft.editorKind === "hero_summary" + ) { const eyebrow = draft.eyebrow.trim(); const title = draft.title.trim(); const summary = draft.summary.trim(); @@ -663,7 +669,8 @@ function replaceEditableArtifactBlockContent( content: nextBody, text: nextBody, tone: resolveEditableCalloutTone(tone), - variant: tone || normalizeText(block.variant) || normalizeText(block.tone), + variant: + tone || normalizeText(block.variant) || normalizeText(block.tone), }; } @@ -676,16 +683,15 @@ function replaceEditableArtifactBlockContent( return { ...block, title: title || undefined, - items: - items.length > 0 - ? items - : normalizeStringArray(block.items), + items: items.length > 0 ? items : normalizeStringArray(block.items), }; } if (block.type === "table" && draft.editorKind === "table") { const title = draft.title.trim(); - const currentColumns = Array.isArray(block.columns) ? block.columns : []; + const currentColumns = Array.isArray(block.columns) + ? block.columns + : []; const currentRows = Array.isArray(block.rows) ? block.rows : []; const columns = parseTableDraftColumns(draft.columns); const effectiveColumns = columns.length > 0 ? columns : currentColumns; @@ -701,7 +707,11 @@ function replaceEditableArtifactBlockContent( if (block.type === "checklist" && draft.editorKind === "checklist") { const title = draft.title.trim(); const currentItems = Array.isArray(block.items) ? block.items : []; - const items = parseChecklistDraftItems(draft.items, block.id, currentItems); + const items = parseChecklistDraftItems( + draft.items, + block.id, + currentItems, + ); return { ...block, title: title || undefined, @@ -711,7 +721,9 @@ function replaceEditableArtifactBlockContent( if (block.type === "metric_grid" && draft.editorKind === "metric_grid") { const title = draft.title.trim(); - const currentMetrics = Array.isArray(block.metrics) ? block.metrics : []; + const currentMetrics = Array.isArray(block.metrics) + ? block.metrics + : []; const metrics = parseMetricDraftItems( draft.metrics, block.id, @@ -736,10 +748,8 @@ function replaceEditableArtifactBlockContent( text, quote: text, attribution: attribution || undefined, - author: - attribution || normalizeText(block.author) || undefined, - source: - attribution || normalizeText(block.source) || undefined, + author: attribution || normalizeText(block.author) || undefined, + source: attribution || normalizeText(block.source) || undefined, }; } @@ -799,17 +809,25 @@ function resolveEditableDraftPreviewText( .filter(Boolean) .join("\n"); case "key_points": - return [draft.title.trim(), draft.items.trim()].filter(Boolean).join("\n"); + return [draft.title.trim(), draft.items.trim()] + .filter(Boolean) + .join("\n"); case "table": return [draft.title.trim(), draft.columns.trim(), draft.rows.trim()] .filter(Boolean) .join("\n"); case "checklist": - return [draft.title.trim(), draft.items.trim()].filter(Boolean).join("\n"); + return [draft.title.trim(), draft.items.trim()] + .filter(Boolean) + .join("\n"); case "metric_grid": - return [draft.title.trim(), draft.metrics.trim()].filter(Boolean).join("\n"); + return [draft.title.trim(), draft.metrics.trim()] + .filter(Boolean) + .join("\n"); case "quote": - return [draft.text.trim(), draft.attribution.trim()].filter(Boolean).join("\n"); + return [draft.text.trim(), draft.attribution.trim()] + .filter(Boolean) + .join("\n"); case "code_block": return [draft.title.trim(), draft.language.trim(), draft.code.trim()] .filter(Boolean) @@ -860,7 +878,7 @@ const OverviewPanel: React.FC<{ className={cn( recoveryPresentation.tone === "warning" ? "border-amber-200 bg-amber-50 text-amber-700" - : "border-sky-200 bg-sky-50 text-sky-700", + : "border-emerald-200 bg-emerald-50 text-emerald-700", )} > {recoveryPresentation.badgeLabel} @@ -871,7 +889,9 @@ const OverviewPanel: React.FC<{ ) : null}
{document.summary ? ( -

{document.summary}

+

+ {document.summary} +

) : null} @@ -882,7 +902,7 @@ const OverviewPanel: React.FC<{ "rounded-2xl border px-4 py-3", recoveryPresentation.tone === "warning" ? "border-amber-200 bg-amber-50 text-amber-950" - : "border-sky-200 bg-sky-50 text-sky-950", + : "border-emerald-200 bg-emerald-50 text-emerald-950", )} >
@@ -891,7 +911,7 @@ const OverviewPanel: React.FC<{ "mt-0.5 inline-flex h-8 w-8 shrink-0 items-center justify-center rounded-full border", recoveryPresentation.tone === "warning" ? "border-amber-200 bg-white text-amber-700" - : "border-sky-200 bg-white text-sky-700", + : "border-emerald-200 bg-white text-emerald-700", )} > @@ -911,7 +931,7 @@ const OverviewPanel: React.FC<{
) : null} {recoveryActionError ? ( -

{recoveryActionError}

+

+ {recoveryActionError} +

) : null}
@@ -1025,7 +1047,9 @@ const SourcesPanel: React.FC<{ return (
{items.map((link, index) => { - const source = link.sourceId ? sourceLookup.get(link.sourceId) : undefined; + const source = link.sourceId + ? sourceLookup.get(link.sourceId) + : undefined; const timelineLink = resolveTimelineLink?.(link.blockId) || null; return (
- {currentVersion?.id === version.id ? "当前" : version.createdBy || "快照"} + {currentVersion?.id === version.id + ? "当前" + : version.createdBy || "快照"}
{version.title || "未命名交付物"} @@ -1162,135 +1194,153 @@ const DiffPanel: React.FC<{ onSelectBlock?: (blockId: string) => void; resolveTimelineLink?: (blockId: string) => ArtifactTimelineLink | null; onSelectTimelineItem?: (itemId: string) => void; -}> = memo(({ diff, onSelectBlock, resolveTimelineLink, onSelectTimelineItem }) => { - if (!diff || diff.changedBlocks.length === 0) { +}> = memo( + ({ diff, onSelectBlock, resolveTimelineLink, onSelectTimelineItem }) => { + if (!diff || diff.changedBlocks.length === 0) { + return ( + } + title="还没有版本差异" + detail="当前文档还没有可展示的 block diff。后续版本生成后,会在这里展示新增、更新、删除与位置变化。" + /> + ); + } + return ( - } - title="还没有版本差异" - detail="当前文档还没有可展示的 block diff。后续版本生成后,会在这里展示新增、更新、删除与位置变化。" - /> - ); - } - - return ( -
-
-
-
Updated
-
- {diff.updatedCount || 0} +
+
+
+
+ Updated +
+
+ {diff.updatedCount || 0} +
-
-
-
Added
-
- {diff.addedCount || 0} +
+
+ Added +
+
+ {diff.addedCount || 0} +
-
-
-
Removed
-
- {diff.removedCount || 0} +
+
+ Removed +
+
+ {diff.removedCount || 0} +
-
-
-
Moved
-
- {diff.movedCount || 0} +
+
+ Moved +
+
+ {diff.movedCount || 0} +
-
-
+ -
- {diff.changedBlocks.map((changedBlock) => { - const diffLines = buildCanvasWorkbenchDiff( - changedBlock.beforeText || "", - changedBlock.afterText || "", - ).slice(0, 12); - const canJump = changedBlock.changeType !== "removed"; - const timelineLink = resolveTimelineLink?.(changedBlock.blockId) || null; +
+ {diff.changedBlocks.map((changedBlock) => { + const diffLines = buildCanvasWorkbenchDiff( + changedBlock.beforeText || "", + changedBlock.afterText || "", + ).slice(0, 12); + const canJump = changedBlock.changeType !== "removed"; + const timelineLink = + resolveTimelineLink?.(changedBlock.blockId) || null; - return ( -
-
-
-
- - {getDiffLabel(changedBlock.changeType)} - - - {changedBlock.blockId} - -
-

- {changedBlock.summary || "检测到 block 发生变化。"} -

-
-
- {timelineLink && onSelectTimelineItem ? ( - - ) : null} - {canJump ? ( - - ) : null} -
-
- - {diffLines.length > 0 ? ( -
-
- {diffLines.map((line, index) => ( -
+
+
+
+ - - {line.type === "add" - ? "+" - : line.type === "remove" - ? "-" - : " "} - - {line.value || " "} -
- ))} + {getDiffLabel(changedBlock.changeType)} + + + {changedBlock.blockId} + +
+

+ {changedBlock.summary || "检测到 block 发生变化。"} +

+
+
+ {timelineLink && onSelectTimelineItem ? ( + + ) : null} + {canJump ? ( + + ) : null}
- ) : changedBlock.afterText || changedBlock.beforeText ? ( -
- {changedBlock.afterText || changedBlock.beforeText} -
- ) : null} -
- ); - })} + + {diffLines.length > 0 ? ( +
+
+ {diffLines.map((line, index) => ( +
+ + {line.type === "add" + ? "+" + : line.type === "remove" + ? "-" + : " "} + + {line.value || " "} +
+ ))} +
+
+ ) : changedBlock.afterText || changedBlock.beforeText ? ( +
+ {changedBlock.afterText || changedBlock.beforeText} +
+ ) : null} + + ); + })} +
-
- ); -}); + ); + }, +); DiffPanel.displayName = "DiffPanel"; const EditInspectorPanel: React.FC<{ @@ -1324,7 +1374,9 @@ const EditInspectorPanel: React.FC<{ return (
-
Workbench 编辑
+
+ Workbench 编辑 +

当前支持章节头、摘要卡、正文块与提示块原位编辑,保存后会回写到同一份 ArtifactDocument JSON,不再把正文打回聊天区。 @@ -1347,7 +1399,7 @@ const EditInspectorPanel: React.FC<{ "w-full rounded-2xl border px-4 py-3 text-left transition", isActive ? "border-slate-900 bg-slate-900 text-white" - : "border-slate-200 bg-white text-slate-900 hover:border-sky-200 hover:bg-sky-50", + : "border-slate-200 bg-white text-slate-900 hover:border-emerald-200 hover:bg-emerald-50", )} onClick={() => onSelectBlock(block.blockId)} > @@ -1462,7 +1514,10 @@ export const ArtifactWorkbenchEditSurface: React.FC<{ return; } - if (entry.editorKind === "rich_text" && draft.editorKind === "rich_text") { + if ( + entry.editorKind === "rich_text" && + draft.editorKind === "rich_text" + ) { const latestDraft: EditableArtifactBlockDraft = { ...draft, markdown: editorRef.current?.flushContent() ?? draft.markdown, @@ -1486,7 +1541,10 @@ export const ArtifactWorkbenchEditSurface: React.FC<{ setRewriteSuggestion(null); setIsRewriting(true); try { - if (entry.editorKind === "rich_text" && draft.editorKind === "rich_text") { + if ( + entry.editorKind === "rich_text" && + draft.editorKind === "rich_text" + ) { const latestDraft: EditableArtifactBlockDraft = { ...draft, markdown: editorRef.current?.flushContent() ?? draft.markdown, @@ -1624,7 +1682,7 @@ export const ArtifactWorkbenchEditSurface: React.FC<{ {timelineLink && onJumpToTimelineItem ? (

- 只作用于当前 block;发送时会带上当前未保存草稿、相邻结构和来源绑定。 + 只作用于当前 + block;发送时会带上当前未保存草稿、相邻结构和来源绑定。