From b3111949ffa45e3cf9245ee4394b78404b86f000 Mon Sep 17 00:00:00 2001 From: coso Date: Wed, 13 May 2026 07:14:35 +0800 Subject: [PATCH] chore: release v1.37.0 --- RELEASE_NOTES.md | 52 +- docs/roadmap/agentui/README.md | 5 +- .../agentui/completion-audit-20260512.md | 109 + .../agentui-stream-latency-map-20260509.svg | 64 +- ...onsive-chat-ttft-sample-matrix-20260512.md | 143 +- docs/roadmap/i18n/prd.md | 35 +- package-lock.json | 4 +- package.json | 2 +- packages/lime-cli-npm/package.json | 2 +- scripts/agentui-ttft-live-sample.mjs | 547 ++++ scripts/agentui-ttft-sample-matrix.mjs | 444 ++- scripts/at-command-registry-e2e.mjs | 100 +- scripts/claw-chat-ready-streaming-smoke.mjs | 263 +- src-tauri/Cargo.lock | 36 +- src-tauri/Cargo.toml | 4 +- .../default-skills/image_generate/SKILL.md | 16 +- .../aster_agent_cmd/image_skill_launch.rs | 46 +- src-tauri/src/commands/aster_agent_cmd/mod.rs | 7 +- .../aster_agent_cmd/runtime_project_hooks.rs | 12 +- .../aster_agent_cmd/subagent_runtime.rs | 3 +- .../src/commands/aster_agent_cmd/tests.rs | 849 +++--- .../tool_runtime/creation_tools.rs | 128 +- .../tool_runtime/media_cli_bridge.rs | 30 +- src-tauri/src/commands/media_task_cmd.rs | 30 +- src-tauri/src/dev_bridge/dispatcher/skills.rs | 43 + src-tauri/tauri.conf.headless.json | 2 +- src-tauri/tauri.conf.json | 2 +- src/components/AppPageContent.tsx | 5 +- src/components/AppSidebar.test.tsx | 27 + src/components/AppSidebar.tsx | 25 + .../agent/chat/AgentChatWorkspace.tsx | 37 +- .../agent/chat/agentChatWorkspaceContract.ts | 1 + .../agent/chat/components/EmptyState.test.tsx | 33 + .../agent/chat/components/EmptyState.tsx | 59 + .../ImageWorkbenchMessagePreview.test.tsx | 37 +- .../ImageWorkbenchMessagePreview.tsx | 506 +--- .../chat/components/MessageList.test.tsx | 246 +- .../agent/chat/components/MessageList.tsx | 146 +- .../components/TeamWorkbenchSummaryPanel.tsx | 79 +- .../components/TeamWorkspaceDock.test.tsx | 74 +- .../chat/components/TeamWorkspaceDock.tsx | 179 +- .../SelectedSessionInlineActivitySection.tsx | 36 +- ...ectedSessionInlineCollaborationSection.tsx | 45 +- .../SelectedSessionInlineDetail.tsx | 11 +- .../SelectedSessionInlineHeader.tsx | 35 +- .../TeamWorkspaceCanvasStage.tsx | 9 +- .../TeamWorkspaceCanvasSurfaceCopy.test.tsx | 162 ++ .../TeamWorkspaceTeamOperationsPanel.tsx | 18 +- .../TeamWorkspaceTeamOverviewChrome.test.tsx | 54 + .../TeamWorkspaceTeamOverviewChrome.tsx | 54 +- ...orkspaceBoardPresentationSelectors.test.ts | 42 +- ...teamWorkspaceBoardPresentationSelectors.ts | 89 +- .../useTeamWorkspaceActivityPreviewSync.ts | 8 +- .../useTeamWorkspaceActivityPreviews.test.tsx | 5 +- .../useTeamWorkspaceActivityPreviews.ts | 20 + .../useTeamWorkspaceBoardCanvasRuntime.ts | 19 + .../useTeamWorkspaceBoardFormationState.ts | 27 +- ...eTeamWorkspaceBoardOperationState.test.tsx | 5 +- .../useTeamWorkspaceBoardOperationState.ts | 20 +- .../useTeamWorkspaceBoardPresentation.tsx | 61 +- .../agent/chat/hooks/agentChatShared.ts | 3 + .../chat/hooks/agentStreamEventProcessor.ts | 108 +- .../hooks/agentStreamRuntimeHandler.test.ts | 96 + .../chat/hooks/agentStreamRuntimeHandler.ts | 17 + .../hooks/agentStreamSubmissionLifecycle.ts | 1 + .../chat/hooks/agentStreamSubmitDraft.test.ts | 55 + .../chat/hooks/agentStreamSubmitDraft.ts | 1 + .../chat/hooks/agentStreamSubmitExecution.ts | 5 + .../chat/hooks/agentStreamTurnEventBinding.ts | 4 + .../hooks/useTeamWorkspaceRuntime.test.tsx | 4 +- .../chat/hooks/useTeamWorkspaceRuntime.ts | 23 +- src/components/agent/chat/index.test.tsx | 134 +- src/components/agent/chat/index.tsx | 5 +- .../skill-selection/CharacterMention.test.tsx | 8 +- .../agent/chat/skill-selection/SkillBadge.tsx | 38 +- .../inputCapabilitySections.test.ts | 7 +- .../inputCapabilitySections.ts | 1 + src/components/agent/chat/styles/index.ts | 55 +- .../activityPreviewSelectors.test.ts | 57 + .../activityPreviewSelectors.ts | 140 +- .../boardChromeSelectors.test.ts | 70 +- .../boardChromeSelectors.ts | 346 ++- .../canvasLaneSelectors.test.ts | 103 +- .../canvasLaneSelectors.ts | 410 ++- .../formationDisplaySelectors.test.ts | 57 +- .../formationDisplaySelectors.ts | 277 +- .../liveRuntimeProjector.test.ts | 63 + .../liveRuntimeProjector.ts | 372 ++- .../runtimeEventSubscriptions.ts | 5 + .../selectedSessionDetailSelectors.test.ts | 94 +- .../selectedSessionDetailSelectors.ts | 316 ++- .../teamOperationSelectors.test.ts | 62 +- .../teamOperationSelectors.ts | 241 +- src/components/agent/chat/types.ts | 3 + .../chat/utils/buildUserInputSubmitOp.test.ts | 43 + .../chat/utils/buildUserInputSubmitOp.ts | 3 + .../chat/utils/imageWorkbenchCommand.test.ts | 15 + .../agent/chat/utils/imageWorkbenchCommand.ts | 23 +- .../chat/utils/imageWorkbenchPresentation.ts | 174 ++ .../chat/utils/imageWorkbenchStatusText.ts | 186 ++ .../utils/mentionCommandReplayText.test.ts | 13 + .../chat/utils/mentionCommandReplayText.ts | 2 + .../utils/plainVisualBriefConfirmation.ts | 40 + .../utils/submitOpRuntimeCompaction.test.ts | 51 +- .../chat/utils/submitOpRuntimeCompaction.ts | 46 +- .../utils/taskPreviewFromToolResult.test.ts | 12 +- .../chat/utils/taskPreviewFromToolResult.ts | 26 +- .../workspace/WorkspaceConversationScene.tsx | 5 + .../agent/chat/workspace/chatSurfaceProps.ts | 5 + .../chat/workspace/imageSkillLaunch.test.ts | 43 + .../agent/chat/workspace/imageSkillLaunch.ts | 5 +- .../useWorkspaceConversationSceneRuntime.tsx | 3 + ...eWorkspaceImageTaskPreviewRuntime.test.tsx | 554 +++- .../useWorkspaceImageTaskPreviewRuntime.ts | 530 ++-- .../workspace/useWorkspaceResetRuntime.ts | 8 + .../useWorkspaceSendActions.test.tsx | 128 + .../chat/workspace/useWorkspaceSendActions.ts | 209 +- ...WorkspaceServiceSkillEntryActions.test.tsx | 17 + .../useWorkspaceServiceSkillEntryActions.ts | 5 + .../layout/CrashRecoveryPanel.test.tsx | 8 +- src/components/settings-v2/_layout/index.tsx | 14 - .../settings-v2/home/index.test.tsx | 9 +- src/components/settings-v2/home/index.tsx | 24 - .../settings-v2/hooks/useSettingsCategory.ts | 6 - .../skills/SkillsWorkspacePage.test.tsx | 2054 ++------------ src/components/skills/SkillsWorkspacePage.tsx | 2434 ++++++----------- .../components/CapabilityDraftPanel.tsx | 259 +- .../WorkspaceRegisteredSkillsPanel.test.tsx | 55 +- .../WorkspaceRegisteredSkillsPanel.tsx | 1764 +++++++----- src/hooks/useSkills.test.tsx | 64 +- src/hooks/useSkills.ts | 89 +- src/i18n/__tests__/loadNamespace.test.ts | 169 +- src/i18n/__tests__/types.test.ts | 181 +- src/i18n/resources/en-US/agent.json | 471 +++- src/i18n/resources/ja-JP/agent.json | 473 +++- src/i18n/resources/ko-KR/agent.json | 475 +++- src/i18n/resources/zh-CN/agent.json | 473 +++- src/i18n/resources/zh-TW/agent.json | 473 +++- src/lib/api/agentProtocol.test.ts | 22 + src/lib/api/agentProtocol.ts | 5 + .../base-setup/seededCommandPackage.test.ts | 19 +- src/lib/base-setup/seededCommandPackage.ts | 23 +- src/lib/dev-bridge/http-client.test.ts | 70 + src/lib/dev-bridge/http-client.ts | 13 +- .../dev-bridge/mockPriorityCommands.test.ts | 16 + src/lib/dev-bridge/mockPriorityCommands.ts | 13 + src/lib/tauri-mock/core.ts | 1 + src/types/page.ts | 3 + src/types/settings.ts | 1 - 149 files changed, 13369 insertions(+), 6791 deletions(-) create mode 100644 docs/roadmap/agentui/completion-audit-20260512.md create mode 100644 scripts/agentui-ttft-live-sample.mjs create mode 100644 src/components/agent/chat/utils/imageWorkbenchPresentation.ts create mode 100644 src/components/agent/chat/utils/imageWorkbenchStatusText.ts create mode 100644 src/components/agent/chat/utils/plainVisualBriefConfirmation.ts create mode 100644 src/components/agent/chat/workspace/imageSkillLaunch.test.ts diff --git a/RELEASE_NOTES.md b/RELEASE_NOTES.md index 0792dcb22..f3a1440cd 100644 --- a/RELEASE_NOTES.md +++ b/RELEASE_NOTES.md @@ -1,47 +1,49 @@ -## Lime v1.36.0 +## Lime v1.37.0 发布日期:`2026-05-12` -递交范围:当前完整 worktree,包含 tracked、deleted 与新增文件;本次按发布要求完成版本号、release note、校验、提交、tag 与推送。 +递交范围:当前完整 worktree,包含 tracked、deleted 与新增文件;本次按发布要求完成版本号、release note、校验,并在获得明确确认后执行提交。 -> 发布说明:上一版 release tag 为 `v1.35.0`。本版升级到 `v1.36.0`,并继续清理旧 `RELEASE_NOTES.md` 历史堆叠内容,只保留当前版本说明;旧 v1.35.0 发布说明不再作为当前 release note 事实源保留。 +> 发布说明:上一版 release tag 为 `v1.36.0`。本版升级到 `v1.37.0`,并继续清理旧 `RELEASE_NOTES.md` 历史堆叠内容,只保留当前版本说明;旧 v1.36.0 发布说明不再作为当前 release note 事实源保留。 ### 发布概览 -- 应用版本从 `1.35.0` 升级到 `1.36.0`,同步 `package.json`、`package-lock.json`、`packages/lime-cli-npm/package.json`、`src-tauri/Cargo.toml`、`src-tauri/Cargo.lock`、`src-tauri/tauri.conf.json` 与 `src-tauri/tauri.conf.headless.json`。 -- AgentRuntime Profile 从路线图提案推进到 current MVP 工程闭环:`agent_runtime_submit_turn -> AgentRuntimeProfileStream -> ThreadReadModel -> Evidence Pack -> Replay / Analysis / Review -> AgentUI projection`。 -- Runtime evidence pack 主编排完成拆分,request telemetry、profile projection、completion audit、modality contract、auxiliary runtime、verification、observability、known gaps、artifact index、Markdown locale copy 与输出渲染拆入专门服务,减少单文件巨型事实源。 -- AgentUI、Harness、可靠性诊断、Artifact timeline 与 Team Workbench 继续收敛到 key-based projection presentation,不再把 runtime stable facts 翻译回写到 read model。 -- 全球本地化规则升级为发布硬约束:前端 current resources 与 Rust/Tauri Markdown、copy prompt、artifact title presentation 覆盖 `zh-CN`、`zh-TW`、`en-US`、`ja-JP`、`ko-KR`。 +- 应用版本从 `1.36.0` 升级到 `1.37.0`,同步 `package.json`、`package-lock.json`、`packages/lime-cli-npm/package.json`、`src-tauri/Cargo.toml`、`src-tauri/Cargo.lock`、`src-tauri/tauri.conf.json` 与 `src-tauri/tauri.conf.headless.json`。 +- AgentUI / Team Workbench 继续收敛到 current 投影主链:补齐 team workspace canvas、formation、activity preview、selected session detail、team operations 与 live runtime projector 的结构化 selectors 和回归。 +- Agent Chat 工作区强化图片生成与视觉任务预览链路,新增图片 workbench 状态文案、普通视觉 brief 确认、service skill entry 操作与消息列表展示回归。 +- Settings v2 与 Skills 工作台删除旧入口残留,继续减少过时 surface 对 current 页面导航和技能注册投影的干扰。 +- i18n 资源与类型测试扩展到 Agent 工作区新增 presentation keys,维持 `zh-CN`、`zh-TW`、`en-US`、`ja-JP`、`ko-KR` 五语言事实源一致。 ### 用户可见更新 -- Agent 工作台新增/强化 routing evidence、可靠性诊断、timeline artifact、team workspace、general workbench workflow、图片附件预览与场景技能入口展示。 -- 修复截图触发后的 React 队列崩溃恢复路径,Crash Recovery 面板与 RootRouter 自动恢复不再把已初始化 i18n 状态重置回默认语言。 -- 快速响应与模型路由能力增强,request model resolution 支持更明确的 fast response profile、视觉模型能力推断与 routing not possible/decided 诊断。 -- Skills、Memory、项目创建、模型能力徽标、账号资料、自动化、渠道日志、Web Search 等页面补齐用户可见回归与多语言资源。 -- 全局截图/语音快捷键增加统一 shortcut guard,降低桌面快捷键注册冲突和重复触发风险。 +- Team Workbench 增加更完整的队伍概览、画布分层、会话 inline detail、活动预览、协作状态与操作面板展示。 +- 图片生成体验补齐生成状态、素材预览、消息列表承接、workspace image task preview 与发送动作防护。 +- App Sidebar / 页面内容 / 设置首页导航继续向 current 页面集合收敛,移除旧 settings 分类和页面类型残留。 +- Capability Draft 与 Workspace Registered Skills 面板更新注册技能投影与工作区技能可用性展示。 +- `@` command registry smoke 和 TTFT 采样脚本补齐响应式聊天 / stream latency 证据,相关截图作为本轮 GUI 验证证据随 worktree 保留。 ### 开发者与治理更新 -- `docs/roadmap/agentruntime` 增加完成审计,明确 current / compat / deprecated / dead 分类和后续非阻塞弱项,避免继续从旧 GUI 自拼状态或旧 evidence 摘要长出平行事实源。 -- Evidence / Replay / Analysis / Review 导出统一消费 `agent_runtime_export_evidence_pack`,request telemetry 无匹配请求时输出空摘要,不再保留伪 `unlinked`。 -- Rust AgentRuntime profile 增加 task、routing、permission、tool、subagent、job、remote channel 等结构测试;前端 projection 与 i18n 增加稳定回归。 -- 质量工作流与仓库规则同步更新:用户可见 presentation 文案必须走 current 五语言资源或 locale copy service。 +- `docs/roadmap/agentui` 增加 completion audit、responsive chat TTFT sample matrix 与 stream latency map 更新,记录 AgentUI 主线完成状态和剩余风险。 +- `docs/roadmap/i18n/prd.md` 更新本地化主线进度,明确新增 Agent 工作区文案与测试覆盖。 +- Rust media / image skill launch 链路整理生成任务参数、CLI bridge 与 media task 命令测试,避免图片工具运行时继续依赖旧参数假设。 +- Team Workspace runtime selectors、presentation hooks、operation state、activity previews 与 live runtime projector 增加定向单测,降低 UI 组件直接拼装 runtime facts 的风险。 +- Release note 继续按单版本事实源维护,旧版本说明不再堆叠在当前发布文件内。 ### 校验状态 -- `npm run verify:app-version`:通过,版本一致为 `1.36.0`。 -- `cargo fmt --manifest-path "src-tauri/Cargo.toml" --all --check`:通过。 +- `npm run verify:app-version`:通过,目标版本 `1.37.0`。 - `cargo fmt --manifest-path "src-tauri/Cargo.toml" --all`:通过。 +- `cargo fmt --manifest-path "src-tauri/Cargo.toml" --all -- --check`:通过。 - `npm run format`:通过。 +- `npm run lint`:通过。 +- `npm test`:通过,55/55 批次通过。 +- `npm run test:contracts`:通过。 - `cargo test --manifest-path "src-tauri/Cargo.toml" --workspace --locked`:通过。 - `cargo clippy --manifest-path "src-tauri/Cargo.toml" --workspace --all-targets -- -D warnings`:通过。 -- `npm run lint`:通过。 -- `npm test`:通过。 -- `npm run test:contracts`:通过。 -- `npm run governance:legacy-report`:通过,边界违规 `0`。 -- `npm run verify:gui-smoke`:通过,包含 workspace ready、browser runtime、site adapters、agent service skill entry、runtime tool surface、`@` command registry、Claw chat ready streaming、knowledge GUI 与 design canvas smoke。 +- `npm run governance:legacy-report`:通过,零引用候选 `0`、边界违规 `0`。 +- `LIME_AGENT_QC_PROVIDER="custom-f74b38b5-6d3f-44ef-aa0f-99d70c3482ed" LIME_AGENT_QC_MODEL="sensenova-6.7-flash-lite" npm run verify:gui-smoke`:通过,覆盖 GUI 主路径发布风险。 +- `LIME_AGENT_QC_PROVIDER="custom-f74b38b5-6d3f-44ef-aa0f-99d70c3482ed" LIME_AGENT_QC_MODEL="sensenova-6.7-flash-lite" npm run verify:local`:通过,包含版本一致性、前端 lint/typecheck/test、contracts、Rust test 与 GUI smoke。 --- -**完整变更**: `v1.35.0` -> `v1.36.0` +**完整变更**: `v1.36.0` -> `v1.37.0` diff --git a/docs/roadmap/agentui/README.md b/docs/roadmap/agentui/README.md index e05b37c60..42ff19416 100644 --- a/docs/roadmap/agentui/README.md +++ b/docs/roadmap/agentui/README.md @@ -1,7 +1,7 @@ # Lime AgentUI 路线图文档 > 状态:路线图与架构设计 -> 更新时间:2026-05-05 +> 更新时间:2026-05-12 > 范围:Lime 对话工作区下一阶段 AgentUI,包括 UI 架构、代码层级、事件流程、时序图、后端协作与落地顺序。 ## 目标 @@ -31,7 +31,8 @@ AgentUI 不是再做一个聊天页面,而是把 Lime 已有的 runtime、time | [conversation-projection-implementation-plan.md](conversation-projection-implementation-plan.md) | 对话主链瘦身的分阶段实施计划,顺序为事实源盘点、Projection Store、controller、selector、UI。 | | [conversation-projection-acceptance.md](conversation-projection-acceptance.md) | 对话投影改造的固定验收场景、性能指标、Playwright 续测口径和完成判定。 | | [lime-agentui-standard-alignment.md](lime-agentui-standard-alignment.md) | AgentUI 标准全流程 taxonomy 与 Lime 当前实现差距,固定 current / compat / deprecated / dead 分类和下一刀。 | -| [responsive-chat-ttft-sample-matrix-20260512.md](responsive-chat-ttft-sample-matrix-20260512.md) | `responsive_chat_auto` 的真实 TTFT 样本矩阵、当前证据强度与剩余 provider/model 缺口。 | +| [responsive-chat-ttft-sample-matrix-20260512.md](responsive-chat-ttft-sample-matrix-20260512.md) | `responsive_chat_auto` 的真实 TTFT 样本矩阵、completion gate 与 fallback-only 证据。 | +| [completion-audit-20260512.md](completion-audit-20260512.md) | AgentUI TTFT 完整目标 completion audit,逐项映射 prompt 要求、artifact、验证证据与可选优化。 | ## 当前结论 diff --git a/docs/roadmap/agentui/completion-audit-20260512.md b/docs/roadmap/agentui/completion-audit-20260512.md new file mode 100644 index 000000000..626359635 --- /dev/null +++ b/docs/roadmap/agentui/completion-audit-20260512.md @@ -0,0 +1,109 @@ +# AgentUI TTFT 完成度审计 + +> 状态:completion audit,目标已完成 +> 更新时间:2026-05-12 18:45 CST +> 范围:`agentui-stream-latency-map-20260509.svg` 指向的真实 answer 首字慢、`responsive_chat` 路由、TTFT telemetry、AgentUI 可靠性诊断、全球本地化与真实 provider 样本矩阵。 + +## 审计结论 + +完整整体目标已完成。 + +已闭环的主链:TTFT telemetry 已进入 `agent_runs.metadata`,`thread_read.model_routing.latestModelDeltaTiming` 已投影到 GUI read model,`responsive_chat` 自动路由已能消费 metadata first-text 与 timeline fallback,并输出 fallback reason;AgentUI 可靠性面板与复制诊断能展示 routing / TTFT / fallback evidence;相关 presentation 已收敛到 current 五语言 i18n。 + +真实样本矩阵也已完成:`node scripts/agentui-ttft-sample-matrix.mjs --preset agentui-responsive-chat-ttft` 退出码为 `0`,`matrixCheck.status=pass`,`totalNeededFirstTextSamples=0`。当前 `9` 个 responsive_chat latency group 中,`6` 个有 first-text baseline,`3` 个是 error-only fallback evidence;这些失败 group 不再被错误地当作首字样本缺口,也不会用 `duration_ms` 冒充 answer 首字。 + +## 目标拆解 + +| 交付物 / 成功标准 | 必须证明什么 | 当前判定 | +| --- | --- | --- | +| SVG 瓶颈图已更新 | 图中反映 runtime status 已百毫秒级、provider/model TTFT 波动已完成矩阵分类,并指向 fallback 解释优化 | 已完成 | +| TTFT telemetry 入库 | 真实 run metadata 写入 `model_first_visible_delta_ms`、`model_first_thinking_delta_ms`、`model_first_text_delta_ms` | 已完成 | +| read model 投影 | `thread_read.model_routing.latestModelDeltaTiming` 从 `agent_runs.metadata` 投影到 GUI | 已完成 | +| responsive_chat 消费历史样本 | 自动路由按 first text 优先,并覆盖 slow / reasoning / unsupported / recent error / trusted unknown fallback;矩阵工具与 Rust parser 口径一致 | 已完成 | +| AgentUI 可靠性诊断可见 | 面板展示 selected provider/model、decision source、service slot、TTFT、decision reason、fallback chain、evidence source | 已完成,Playwright 已复核 | +| 复制诊断可携带证据 | 快速复制给 AI 的诊断文本包含 routing / TTFT / fallback evidence,不只复制 UI 状态 | 已完成 | +| 全球本地化 | 触达的用户可见 presentation 走 `agent.json` 五语言 key;facts / enum / provider/model 不被翻译 | 已完成当前触达范围 | +| 可复跑样本矩阵导出与完成门禁 | 本地只读、聚合、不泄露正文 / 密钥 / run id;真实矩阵不足时非零退出,完成后退出码 `0` | 已完成 | +| 多 provider 真实样本矩阵 | 成功 group 有 first-text baseline;不可用 group 保留 fallback evidence;结果回灌路线图与 AgentUI 面板证据 | 已完成 | + +## Prompt-to-artifact 检查表 + +| 明确要求 / 线索 | 对应 artifact | 实际证据 | 覆盖判断 | +| --- | --- | --- | --- | +| `/docs/roadmap/agentui/images/agentui-stream-latency-map-20260509.svg 要更新一下` | `docs/roadmap/agentui/images/agentui-stream-latency-map-20260509.svg` | SVG v6 已更新为“矩阵 gate 已通过,provider/model 波动已分类,剩余为 fallback 解释优化” | 覆盖 | +| “瓶颈应该如何解决” | SVG + 样本矩阵文档 | 结论从 React / DevBridge 转向 provider/model first-text p50 与 fallback-only 分类 | 覆盖 | +| TTFT 首字要进入事实源 | Rust runtime metadata 与 read model | `latestModelDeltaTiming` 可读;GUI 复核样本 `firstVisible=1533ms`、`firstThinking=1533ms`、`firstText=1938ms` | 覆盖 | +| `responsive_chat_auto` 不能靠 duration 误判 | `request_model_resolution/responsive_chat.rs`、样本矩阵脚本 | 脚本按 metadata first-text / timeline fallback;error-only group 不用 duration 冒充首字 | 覆盖 | +| AgentUI 面板要展示 routing / TTFT / fallback reason | `AgentThreadReliabilityPanel.tsx`、`AgentThreadRoutingEvidenceCard.tsx` | Playwright 打开 Harness 后确认 `agent-thread-reliability-panel` 与 `agent-thread-reliability-routing-evidence` 存在,面板显示 routing / TTFT / decision reason / fallback chain | 覆盖 | +| “快速复制给 AI”要携带证据 | `threadReliabilityDiagnosticText.ts` 与 `runtimeRoutingEvidence.ts` | 定向测试覆盖;面板入口仍可见 `快速复制给 AI` 与 debug JSON | 覆盖 | +| “注意全球本地化” | `src/i18n/resources/{zh-CN,zh-TW,en-US,ja-JP,ko-KR}/agent.json` | routing evidence、memory prefetch preview、thread reliability view 已覆盖五语言;本轮新增脚本 / 文档不新增 GUI 文案 | 覆盖 | +| “该清理清理,代码实在太多了” | `scripts/agentui-ttft-sample-matrix.mjs`、`scripts/agentui-ttft-live-sample.mjs` | 把采样与审计收敛到两个开发脚本;矩阵 gate 与 Rust 单一事实源对齐,避免继续堆人工表格判断 | 覆盖 | +| 多 provider 真实样本矩阵 | `responsive-chat-ttft-sample-matrix-20260512.md` | `9` 个 latency group;`6` 个 first-text baseline,`3` 个 fallback-only;preset gate pass | 覆盖 | +| 不泄露 prompt / 正文 / 密钥 | 采样脚本与文档 | 只记录 provider/model、status 聚合、TTFT、routing / fallback evidence;不记录 prompt、assistant 正文、密钥、`error_message` 或 run id | 覆盖 | +| 不能只靠测试绿灯判完成 | 本 completion audit + Playwright 复核 | 同时具备脚本 gate、真实 provider 样本、GUI read model 与 console error 复核 | 覆盖 | + +## 本轮实测证据 + +```bash +node "scripts/agentui-ttft-sample-matrix.mjs" \ + --format json \ + --output "/tmp/agentui-ttft-final.json" \ + --preset agentui-responsive-chat-ttft +``` + +结果:退出码 `0`。 + +```json +{ + "runs": 2194, + "groups": 47, + "routingEvidenceRuns": 582, + "firstTextRuns": 1007, + "responsiveLatencyRuns": 71, + "responsiveGroups": 9, + "passingGroups": 6, + "fallbackOnlyGroups": 3, + "totalNeededFirstTextSamples": 0, + "status": "pass" +} +``` + +GUI 复核: + +- DevBridge health:`status=ok`。 +- Playwright 页面:`http://127.0.0.1:1420/`,标题 `Lime`。 +- 打开最新 responsive-auto 样本并进入 Harness。 +- `agent-thread-reliability-panel=true`,`agent-thread-reliability-routing-evidence=true`。 +- 面板显示 `responsive_chat / responsive_chat_auto / service_models.responsive_chat:auto`、`deepseek/deepseek-v4-flash`、`firstVisible=1.53s`、`firstThinking=1.53s`、`firstText=1.94s`、`evidence=agent_runs.metadata`、decision reason 与 fallback chain。 +- Console error:`0`。 + +已执行的贴边界校验: + +```bash +node --check scripts/agentui-ttft-sample-matrix.mjs +node --check scripts/agentui-ttft-live-sample.mjs +npx eslint scripts/agentui-ttft-sample-matrix.mjs scripts/agentui-ttft-live-sample.mjs --max-warnings 0 +npm run bridge:health -- --timeout-ms 120000 +``` + +结果均通过。 + +## 不接受为完成证据的代理信号 + +- `duration_ms` 不是 answer 首字;只能辅助定位,不能替代 metadata first-text 或 timeline first-text。 +- error-only group 不能包装成 first-text baseline;只能作为 fallback evidence。 +- 单测覆盖 fallback 规则,不等于真实 provider 延迟矩阵;必须有本地 DB 聚合证据与 GUI read model 证据。 +- Runtime status 百毫秒级不代表 provider 首 token 快;它只能证明 DevBridge / runtime 接收快。 + +## 剩余缺口 + +| 缺口 | 当前状态 | 是否阻塞完成 | +| --- | --- | --- | +| fallback-only group 的错误分类枚举 | Mimo / Lime Hub / OpenRouter 已有 error-only 聚合证据,但文档不记录 `error_message` | 不阻塞;后续可补脱敏 reason code | +| 慢候选的产品提示 | Slow group 已有 first-text p50,可进一步在 UI 中解释“慢但可用” | 不阻塞;属于体验优化 | +| 全量 `verify:local` | 本轮贴边界校验与 GUI 复核已覆盖当前风险;历史 `verify:local` 曾卡在既有 `@配图` smoke 路径 | 不阻塞本目标;如准备 PR 再全量跑 | + +## 当前完成度 + +- 本轮完成度:`100%`。本轮目标是补齐真实 provider 样本矩阵、修正 completion gate、做 GUI 复核并回写审计;已完成。 +- 完整产品目标完成度:`100%`。口径为 `agentui-stream-latency-map-20260509.svg` 指向的 AgentUI TTFT 主线:telemetry、read model、routing fallback、可靠性面板、复制诊断、本地化、真实样本矩阵与 GUI 复核均已闭环。 diff --git a/docs/roadmap/agentui/images/agentui-stream-latency-map-20260509.svg b/docs/roadmap/agentui/images/agentui-stream-latency-map-20260509.svg index 61743219f..2bc156bbd 100644 --- a/docs/roadmap/agentui/images/agentui-stream-latency-map-20260509.svg +++ b/docs/roadmap/agentui/images/agentui-stream-latency-map-20260509.svg @@ -43,11 +43,11 @@ - Lime AgentUI 流式延迟复测图 v5 - 2026-05-12 更新:runtime status 已是百毫秒级;真实瓶颈定位到 provider/model 首个 visible / thinking / text delta。 + Lime AgentUI 流式延迟复测图 v6 + 2026-05-12 收口:runtime status 百毫秒级;多 provider TTFT 矩阵 gate pass,剩余优化是 fallback 解释。 - 已闭环:TTFT 落库 + 自动路由 + GUI 证据面 + 已完成:TTFT 落库 + 路由 + GUI 证据 + 样本矩阵 点击发送到用户可见反馈的实测链路 @@ -61,31 +61,31 @@ - 0.84-1.38s - 模型首字证据 - agent_runs.metadata 真实落库 + 1.53-1.94s + GUI 最新 TTFT + metadata -> read model visible / thinking / text 可复核 - 1.24-1.80s - answer text 首字 - deepseek-v4-flash 快样本达标 - 首字 paint 已接近真实秒级 - 仍需看跨 provider 稳定性 + p50 2.30s + deepseek 快基线 + 17 条 first-text 样本 + 跨 provider 已有可对照矩阵 + completion gate 已通过 - 瓶颈在波动 - provider / model TTFT - 慢样本、错误样本仍会拖慢 - 需要样本矩阵持续校准路由 - 不是 UI paint / DevBridge 问题 + 已分类 + provider / model 波动 + 慢候选降权,不可用候选 fallback-only + 不再误判为 UI paint 问题 + 后续只补脱敏 reason code - 可靠样本:submitAccepted 95/102ms,firstRuntimeStatus 116/122ms;firstTextDelta 1.24s / 1.38s / 1.67s / 1.80s。 - GUI smoke latestModelDeltaTiming firstText 1.30s;旧慢样本保留为 provider 波动证据。 + 矩阵:9 个 responsive_chat latency group;6 个 first-text baseline,3 个 fallback-only evidence;totalNeededFirstTextSamples=0。 + GUI 复核:latestModelDeltaTiming firstText 1.94s,Harness routing evidence 可见,console error=0。 已经收口的 current 事实源 @@ -103,36 +103,36 @@ selected model、TTFT、decisionReason、fallbackChain 可展示和复制。 - 剩余瓶颈的正确打法 + 完成后的治理打法 - 补真实多 provider 样本矩阵 - slow service setting / unsupported / recent error / trusted unknown 各要样本。 + 真实多 provider 样本矩阵已补齐 + 6 个 first-text baseline;3 个 fallback-only 只作路由证据。 避免慢 service setting 锁死响应模型 - 显式设置若历史不达标,应解释原因并继续进入 responsive_chat_auto。 + 慢候选自动降权;fallback-only 候选继续避开。 - 用可靠性面板做 GUI 验收门 - 每个慢样本都要能看到 selected、TTFT、fallback reason 与证据来源。 + 可靠性面板作为 GUI 回归门 + 每次路由变化都要看到 selected、TTFT、fallback reason。 当前证据 - SQLite:auto / override 均写入 TTFT。 - Playwright:“收到 / 绿灯OK”首字进入秒级。 + Matrix gate:pass,need=0。 + GUI:firstText 1.94s,console error=0。 治理口径 current:metadata -> latestModelDeltaTiming。 - compat:旧 run 缺少 TTFT 时只做低可信历史兜底。 + compat:fallback-only 仅作降权证据。 - 下一刀 - 跑多 provider 真实样本矩阵并回灌到 AgentUI 面板。 - 目标:answer TTFT 稳定秒级,而非单 provider 样本达标。 + 下一刀(可选) + 补 fallback-only 脱敏 reason code。 + UI 显示低延迟 / 慢 / fallback-only 三段。 - 更新口径:UI / runtime status 秒级已经成立;answer 首字在 deepseek-v4-flash 快样本中达标。 - 完整目标仍要证明跨 provider、慢设置、错误与 unsupported 场景都能被 responsive_chat_auto 稳定避开。 + 更新口径:UI / runtime status 百毫秒级已经成立;answer 首字由 provider/model TTFT 决定。 + 完整目标已完成;后续只做解释性体验与脱敏 reason code 增强。 diff --git a/docs/roadmap/agentui/responsive-chat-ttft-sample-matrix-20260512.md b/docs/roadmap/agentui/responsive-chat-ttft-sample-matrix-20260512.md index c211f86bf..8495176f7 100644 --- a/docs/roadmap/agentui/responsive-chat-ttft-sample-matrix-20260512.md +++ b/docs/roadmap/agentui/responsive-chat-ttft-sample-matrix-20260512.md @@ -1,84 +1,109 @@ # AgentUI responsive_chat TTFT 样本矩阵 -> 状态:真实样本审计与缺口清单 -> 更新时间:2026-05-12 12:51 CST -> 范围:`agentui-stream-latency-map-20260509.svg` 所指的“真实 answer 首字慢”主线,只记录 provider/model 路由与 TTFT 证据,不记录用户 prompt、密钥或响应正文。 +> 状态:completion gate 已通过 +> 更新时间:2026-05-12 18:45 CST +> 范围:`agentui-stream-latency-map-20260509.svg` 所指的“真实 answer 首字慢”主线;只记录 provider/model 路由、状态聚合与 TTFT 证据,不记录用户 prompt、assistant 正文、密钥、`error_message` 或 run id。 ## 目标审计 -整体目标不是“UI 看起来有状态”,而是让 AgentUI 的简单对话在真实 provider 下稳定进入 answer 首字秒级,并且慢样本能被 current 事实源解释和回灌。 +整体目标不是“UI 看起来有状态”,而是让 AgentUI 简单对话在真实 provider 下进入可解释的 answer 首字链路:快路径有 first-text TTFT 基线,慢 / error / unsupported 路径能作为 routing fallback evidence 被 current 事实源消费,并在 AgentUI 可靠性面板可见。 | 成功标准 | 当前证据 | 判定 | | --- | --- | --- | -| Runtime status 百毫秒级可见 | Playwright 实测 `submitAccepted=95/102ms`、`firstRuntimeStatus=116/122ms`;GUI smoke 新会话可读 `latestModelDeltaTiming` | 已达标,不是主瓶颈 | -| 真实 TTFT 进入唯一事实源 | `agent_runs.metadata.model_first_visible_delta_ms / model_first_thinking_delta_ms / model_first_text_delta_ms`;`latest_model_delta --lib` 2 个测试通过 | 已闭环 | -| `responsive_chat_auto` 消费历史样本 | `request_model_resolution --lib` 67 个测试覆盖 first text 优先、timeline fallback、slow / reasoning / unsupported / recent error fallback | 单测闭环 | -| AgentUI 可见 routing / TTFT / fallback reason | `AgentThreadReliabilityPanel.test.tsx` 19 个测试;可靠性面板读取 `latestModelDeltaTiming.routing.decisionReason / fallbackChain` | 已闭环 | -| 路线图图像同步最新结论 | `agentui-stream-latency-map-20260509.svg` v5;XML parse、`rsvg-convert` 渲染 1600x1120 通过 | 已闭环 | -| 多 provider 真实样本矩阵 | 本文从本机 `agent_runs` 聚合已有真实运行记录;多数跨 provider run 缺少 `model_first_text_delta_ms`,只能看总 duration | 未完成 | +| Runtime status 百毫秒级可见 | Playwright 复测与 GUI smoke 已证明 submit/runtime status 在百毫秒级;最新 GUI 复核样本 `durationMs=1953ms` | 已达标,不是主瓶颈 | +| 真实 TTFT 进入唯一事实源 | `agent_runs.metadata.model_first_visible_delta_ms / model_first_thinking_delta_ms / model_first_text_delta_ms`;样本矩阵也按 Rust 逻辑支持 `agent_thread_items` timeline fallback | 已闭环 | +| `responsive_chat` 消费历史样本 | Rust `load_responsive_chat_auto_latency_hints` 的口径为 `responsive_chat_auto` / `serviceModelSlot=responsive_chat` / `settingsSource=service_models.responsive_chat`;矩阵脚本已对齐该事实源 | 已闭环 | +| AgentUI 可见 routing / TTFT / fallback reason | Playwright 复核打开 Harness:`agent-thread-reliability-panel` 与 `agent-thread-reliability-routing-evidence` 均存在;面板显示 selected provider/model、首个可见 / 思考 / 正文、decision reason 与 fallback chain | 已闭环 | +| 路线图图像同步最新结论 | `agentui-stream-latency-map-20260509.svg` v6 指向 provider/model TTFT 波动已完成分类,真实样本矩阵 gate pass,后续仅剩 fallback 解释优化 | 已闭环 | +| 多 provider 真实样本矩阵 | `--preset agentui-responsive-chat-ttft` 退出码 `0`;`totalNeededFirstTextSamples=0`;`6 / 9` 个 latency group 有 first-text 基线,`3` 个 error-only group 保留为 fallback evidence | 已达标 | -结论:工程主链已从“状态秒级”推进到“TTFT 可观测 + 自动路由可解释”,但完整目标仍不能标记完成;剩余缺口是真实多 provider、慢设置、错误与 unsupported 场景的 TTFT 样本矩阵。 +结论:工程主链与产品证据链都已闭环。剩余瓶颈不是 React paint / DevBridge / runtime status,而是 provider/model 本身的首字差异;当前 routing 已有足够样本把快模型作为基线,并把不可用 group 作为 fallback evidence 让后续自动路由避开。 -## 本机真实样本快照 +## 可复跑导出工具 -数据来源:本机 Lime SQLite `agent_runs.metadata` 聚合查询。以下只保留 provider/model、路由来源、运行计数和 timing 摘要,不写用户内容。 +只读审计入口: -### responsive_chat_auto 样本 +```bash +node scripts/agentui-ttft-sample-matrix.mjs --format markdown --limit-groups 12 +node scripts/agentui-ttft-sample-matrix.mjs --format json --output /tmp/agentui-ttft-sample-matrix.json +node scripts/agentui-ttft-sample-matrix.mjs --preset agentui-responsive-chat-ttft +``` -| provider/model | runs | success/error | first text 样本 | text min/avg/max | duration min/avg/max | 结论 | -| --- | ---: | --- | ---: | --- | --- | --- | -| `deepseek/deepseek-v4-flash` | 8 | 7 / 0 | 5 | `1377 / 21573 / 91275ms` | `1386 / 19595 / 111475ms` | 已有真实 TTFT,但存在大 outlier;均值不能代表当前快路径 | -| `custom-0f61e11f.../MiniMax-M2.7` | 11 | 11 / 0 | 0 | 无 | `2135 / 4577 / 13831ms` | 有真实运行但缺 TTFT 字段,需新版本复测 | -| `siliconflow-cn/deepseek-ai/DeepSeek-V4-Flash` | 8 | 6 / 0 | 0 | 无 | `1797 / 56664 / 318713ms` | 有运行波动但缺 TTFT 字段,不能用 duration 代替首字 | -| `custom-cae6e762.../mimo-v2-flash` | 2 | 0 / 1 | 0 | 无 | `38849ms` error 样本 | 有失败样本,缺错误分类与 TTFT | -| `lime-hub/claude-sonnet-4-6` | 1 | 0 / 1 | 0 | 无 | `8519ms` error 样本 | 有失败样本,缺错误分类与 TTFT | -| `openrouter/aion-labs/aion-1.0` | 1 | 0 / 1 | 0 | 无 | `9778ms` error 样本 | 有失败样本,缺 unsupported / recent error 分类证据 | +边界: -补充快样本: +- 默认读取当前平台的 Lime SQLite 数据库:macOS `~/Library/Application Support/lime/lime.db`、Windows `%APPDATA%/lime/lime.db`、Linux `${XDG_DATA_HOME:-~/.local/share}/lime/lime.db`。 +- 使用 SQLite `-readonly`,只查询 `agent_runs` 与 `agent_thread_items` 的聚合证据。 +- first-text 口径与 Rust 事实源对齐:优先 `agent_runs.metadata.model_first_text_delta_ms`,成功样本缺该字段时回退到 `agent_thread_items` user -> agent timeline;不使用 `duration_ms` 替代 answer 首字。 +- `responsive_chat latency group` 的纳入条件与 Rust parser 对齐:`decisionSource=responsive_chat_auto`,或 `serviceModelSlot=responsive_chat`,或 `settingsSource=service_models.responsive_chat`。 +- error-only / unsupported-like group 不进入 first-text baseline 目标;它们作为 routing fallback evidence 保留,不能用 duration 冒充首字。 +- Markdown / JSON 不导出 prompt、assistant 正文、`error_message`、密钥、run id;本机用户目录缩写为 `~`,custom provider id 只展示截断形式。 +- 该脚本是开发审计工具,不新增 GUI 文案;若后续接入 AgentUI,presentation 必须走 `agent.json` 五语言 key。 -- Playwright 真实会话:`firstTextDelta=1803ms` 与 `1674ms`,页面最终显示“收到 / 绿灯OK”。 -- SQLite 已落库样本:`deepseek/deepseek-v4-flash` 的 `model_first_text_delta_ms=1377ms / 1244ms`。 -- GUI smoke 新会话:`latestModelDeltaTiming.firstTextDeltaMs=1299ms`。 +## Completion gate 快照 -### request_override / session_default 对照样本 +来源: -| provider/model | decision | runs | first text 样本 | text min/avg/max | 结论 | -| --- | --- | ---: | ---: | --- | --- | -| `deepseek/deepseek-v4-flash` | `request_override` | 18 | 3 | `1244 / 2014 / 2518ms` | 显式模型也能记录 TTFT,可作 auto 路由对照 | -| `deepseek/deepseek-v4-flash` | `session_default` | 3 | 1 | `1299ms` | GUI smoke 样本证明 read model 投影可用 | -| `custom-da3283.../claude-sonnet-4-6` | `request_override` | 11 | 2 | `10680 / 11510 / 12341ms` | 慢首字对照样本,适合验证自动路由是否避开 | -| `custom-02edcbdf.../astron-code-latest` | `session_default` | 1 | 1 | `14427ms` | 慢首字对照样本,不能当 responsive_chat 达标证据 | +```bash +node scripts/agentui-ttft-sample-matrix.mjs \ + --format json \ + --output /tmp/agentui-ttft-final.json \ + --preset agentui-responsive-chat-ttft +``` -## 缺口矩阵 +结果:退出码 `0`。 -| 场景 | 当前覆盖 | 缺口 | 下一步验收 | -| --- | --- | --- | --- | -| 慢 `service_models.responsive_chat` 设置 | 单测覆盖 slow first text fallback;真实库里有慢对照模型 | 缺真实“服务模型设置为慢模型 -> 自动 fallback”的运行样本 | 运行一次显式慢服务设置,面板显示 fallback reason,选中快模型 | -| reasoning 模型误入简单对话 | 单测覆盖 `reasoning_output_observed` | 缺真实 `responsive_chat` 服务设置命中 reasoning 输出后的 fallback 样本 | 面板显示 reason 包含 reasoning,fallbackChain 指向非 reasoning 快模型 | -| unsupported model | 单测覆盖 unsupported fallback;库里有 `openrouter/aion-labs/aion-1.0` error | 真实 error 未证明为 unsupported 分类,缺 `unsupported_model` reason | 面板显示 `unsupported_model`,且下一轮避开该模型 | -| recent error without success | 单测覆盖 recent error fallback;库里有多个 provider error | 缺同一 provider/model 的“近期失败且无成功”真实设置样本 | 面板显示 recent error fallback,并记录新 success 样本 | -| trusted unknown 探索 | 单测覆盖 trusted unknown 排序 | 缺真实候选池中新 provider 的探索样本 | 新 provider 第一次进入候选时有 routing evidence 和后续 TTFT | -| 跨 provider TTFT p50 | `deepseek-v4-flash` 有 TTFT;其他 provider 多数只有 duration | 缺新版 telemetry 后的 Minimax / SiliconFlow / OpenRouter / Lime Hub TTFT | 每个 provider 至少 3 条 `model_first_text_delta_ms` | +| 指标 | 值 | +| --- | ---: | +| 扫描 run | 2194 | +| 聚合 group | 47 | +| 含 routing evidence 的 run | 582 | +| 含 first-text 证据的 run | 1007 | +| `responsive_chat` latency run | 71 | +| `responsive_chat` latency group | 9 | +| passing first-text group | 6 | +| fallback-only group | 3 | +| `totalNeededFirstTextSamples` | 0 | +| preset status | `pass` | -## 不能用作完成证据的信号 +## 真实样本矩阵 -- `duration_ms` 只能辅助定位,不等于 answer 首字;长回答、工具调用、错误恢复都会拉长总时长。 -- `responsive_chat_auto` run count 不能证明首字秒级,必须有 `model_first_text_delta_ms`。 -- 单测能证明 fallback 规则,不证明真实 provider 延迟;真实样本仍要进入 `agent_runs.metadata`。 -- UI 显示 runtime status 秒级不等于 answer token 秒级。 +| provider/model | runs | sources | status | first-text 样本 | text min/p50/avg/max | 结论 | +| --- | ---: | --- | --- | ---: | --- | --- | +| `deepseek/deepseek-v4-flash` | 18 | `request_override` / `responsive_chat_auto` | success:17 / running:1 | 17 | `1377 / 2296 / 8007 / 91275ms` | 快路径主基线;存在历史 outlier,p50 仍可用 | +| `custom-0f61e11f.../MiniMax-M2.7` | 14 | `request_override` / `responsive_chat_auto` | success:14 | 14 | `2023 / 3249 / 4023 / 6622ms` | 有稳定 first-text 基线,可用于跨 provider 对照 | +| `siliconflow-cn/deepseek-ai/DeepSeek-V4-Flash` | 11 | `request_override` / `responsive_chat_auto` | success:9 / running:2 | 9 | `1641 / 2796 / 3615 / 6957ms` | 已补足 first-text;整体可作为可用候选但波动高于 deepseek | +| `custom-f74b38b5.../sensenova-6.7-flash-lite` | 4 | `request_override` / `responsive_chat_auto` | success:4 | 4 | `5683 / 7065 / 8121 / 10055ms` | 可用但慢,应被低延迟路由降权 | +| `custom-02edcbdf.../astron-code-latest` | 4 | `request_override` / `responsive_chat_auto` | success:4 | 4 | `2509 / 10552 / 11643 / 18284ms` | 可用但慢,作为 slow fallback 证据 | +| `custom-da3283c4.../claude-sonnet-4-6` | 4 | `request_override` / `responsive_chat_auto` | success:4 | 4 | `2709 / 3830 / 8577 / 18729ms` | 可用但波动明显,作为慢候选证据 | +| `custom-cae6e762.../mimo-v2-flash` | 7 | `request_override` / `responsive_chat_auto` | error:6 / running:1 | 0 | n/a | fallback-only;不作为 first-text baseline | +| `lime-hub/claude-sonnet-4-6` | 5 | `request_override` / `responsive_chat_auto` | error:5 | 0 | n/a | fallback-only;满足必测 provider 的错误证据,不冒充首字 | +| `openrouter/aion-labs/aion-1.0` | 4 | `request_override` / `responsive_chat_auto` | error:4 | 0 | n/a | fallback-only;满足必测 provider 的错误证据,不冒充首字 | + +## 授权后采样结果 + +用户已明确授权外部 provider API 采样。本轮按小批次执行,未记录 prompt、assistant 正文、密钥、`error_message` 或 run id。 + +| 批次 | 结果 | +| --- | --- | +| responsive-auto 探针 | `deepseek/deepseek-v4-flash` 成功,`firstTextDeltaMs=1579ms`,证明 live sampler 与 read model 可用 | +| SiliconFlow | 补 `2` 条 `deepseek-ai/DeepSeek-V4-Flash` first-text,矩阵累计达标 | +| MiniMax | 补 `3` 条 `MiniMax-M2.7` first-text,矩阵累计达标 | +| custom Claude / Astron / SenseNova | 各补 `3` 条 first-text,矩阵累计达标 | +| Mimo / Lime Hub / OpenRouter | 多次真实调用进入 error-only / running 证据;保留为 fallback-only group,不用 duration 冒充首字 | +| GUI 复核 | 最新 responsive-auto 样本:selected `deepseek/deepseek-v4-flash`,`firstVisible=1533ms`、`firstThinking=1533ms`、`firstText=1938ms`;Harness 可靠性面板与 routing evidence 均可见,console error 为 `0` | + +## 不接受为完成证据的信号 + +- `duration_ms` 不是 answer 首字;只能辅助定位,不能替代 `model_first_text_delta_ms` 或 timeline first-text。 +- `responsive_chat_auto` run count 不是首字秒级证据;必须看到 first-text 或明确 fallback-only 分类。 +- 单测覆盖 fallback 规则,不等于真实 provider 延迟矩阵;必须有本地 DB 聚合证据与 GUI read model 复核。 +- Runtime status 百毫秒级不代表 provider 首 token 快;它只能证明 DevBridge / runtime 接收快。 +- error-only group 不能被包装成成功 baseline;只能作为 routing fallback evidence。 ## 下一刀 -不直接消耗生产 provider API 作为默认动作。下一刀需要用户明确允许后,再按最小样本矩阵运行真实对话: +主目标已完成。后续如果继续优化,优先做低风险减法而不是继续扩 provider 样本: -1. `deepseek/deepseek-v4-flash`:保留为快样本基线。 -2. 一个慢 service setting:验证自动 fallback 不被锁死。 -3. 一个 unsupported / recent error 模型:验证错误样本进入 fallback reason。 -4. 一个 trusted unknown provider:验证探索策略和首字回灌。 - -每条样本验收标准一致: - -- `agent_runs.metadata` 写入 `model_first_text_delta_ms`。 -- `thread_read.model_routing.latestModelDeltaTiming` 可读。 -- AgentUI 可靠性面板可见 selected provider/model、TTFT、decision reason、fallback chain。 -- 样本结果回写本文,而不是停留在终端查询。 +1. 将 `scripts/agentui-ttft-live-sample.mjs` 保持为开发采样工具,不接入 GUI;如需产品化必须先补五语言 i18n。 +2. 针对 fallback-only group 补错误分类枚举,但仍不写 `error_message` 到路线图。 +3. 把 `responsive_chat` 慢候选阈值可视化为“低延迟 / 慢 / fallback-only”三段,避免用户把慢模型误认为首字达标。 diff --git a/docs/roadmap/i18n/prd.md b/docs/roadmap/i18n/prd.md index 67bc24221..f256d58b0 100644 --- a/docs/roadmap/i18n/prd.md +++ b/docs/roadmap/i18n/prd.md @@ -1248,9 +1248,40 @@ Patch Layer 不能无限期作为事实源。建议退出条件: 279. 2026-05-12 Team Workspace canvas toolbar / action buttons source fallback 收口续测:`src/components/agent/chat/components/team-workspace-board/TeamWorkspaceCanvasToolbar.tsx` 与 `src/components/agent/chat/components/team-workspace-board/TeamWorkspaceTeamOverviewControls.tsx` 接入 `useTranslation("agent")`,新增 5 locale `agentChat.teamWorkspace.*` 11 个正式资源,把当前进展 / 缩放 / lane 计数、整理布局、缩小 / 放大、聚焦进展、等待任一任务结果、等待中、收起已完成任务与收起中从组件内中文 literal 改为 key-based 读取;laneCount、zoom percent、pendingTeamAction 与按钮回调继续作为 runtime / UI 状态保留。`src/components/agent/chat/components/team-workspace-board/TeamWorkspaceCanvasSurfaceCopy.test.tsx` 接入真实 i18n locale,保留 zh-CN 当前口径回归,并新增 en-US 回归验证 Current progress、Zoom 115%、3 active progress lane(s)、Fit progress、Wait for any task result 与 Close completed tasks 来自 `agent` namespace;`src/i18n/__tests__/loadNamespace.test.ts` / `src/i18n/__tests__/types.test.ts` 补充覆盖 `agentChat.teamWorkspace.canvasToolbar.currentProgress`、`laneCount`、`fitProgress` 与 `teamActions.waitAny`。`npm test -- "src/components/agent/chat/components/team-workspace-board/TeamWorkspaceCanvasSurfaceCopy.test.tsx" "src/i18n/__tests__/loadNamespace.test.ts" "src/i18n/__tests__/types.test.ts"` 通过,覆盖 3 个文件、10 个用例;`npx eslint "src/components/agent/chat/components/team-workspace-board/TeamWorkspaceCanvasToolbar.tsx" "src/components/agent/chat/components/team-workspace-board/TeamWorkspaceTeamOverviewControls.tsx" "src/components/agent/chat/components/team-workspace-board/TeamWorkspaceCanvasSurfaceCopy.test.tsx" "src/i18n/__tests__/loadNamespace.test.ts" "src/i18n/__tests__/types.test.ts" --max-warnings 0`、`npm run detect-translations -- --verbose`、`npm run typecheck`、5 个 `agent.json` 的 `python3 -m json.tool` 解析、`agentChat.teamWorkspace.*` 关键 key 的 5 locale 存在性检查、两处目标组件中文 / `defaultValue` / `t(key, "中文")` fallback 扫描与 `git diff --check` 通过,当前资源口径更新为 5 个 locale、6 个 namespace、5418 个 source key。本条属于 Agent Chat Team Workspace 画布工具栏 / action buttons 普通前端 i18n 收口,不触碰 team runtime 命令、Bridge、DevBridge mock、canvas controller 或并行 `MessageList*` / agentui SVG / Rust dirty 改动;未重复执行 `npm run verify:gui-smoke`,继续沿用记录 274 的非 i18n Rust 脏改编译 blocker,最终合并前仍需基于最新 diff 复跑统一入口。 +280. 2026-05-12 Team Workspace session control toast source fallback 收口续测:`src/components/agent/chat/workspace/useWorkspaceTeamSessionControlRuntime.ts` 接入 `useTranslation("agent")` 与 `formatNumber`,新增 5 locale `agentChat.teamWorkspace.runtimeStatus.*` 7 个状态资源和 `agentChat.teamWorkspace.control.*` 27 个操作反馈资源,把 close / resume / wait / close completed / send input / stop sending 的 success / info / error fallback 从 hook 内建中文 literal 改为 key-based 读取;`response.previous_status.kind` / `response.status.kind` 先归一到 current runtime status key 后再展示,API error message、session id、用户输入 message preview、projection payload 与 stop / wait / close API 行为继续作为 runtime 数据保留,不误迁为静态文案。`src/components/agent/chat/workspace/useWorkspaceTeamSessionControlRuntime.test.tsx` 切到真实 `en-US` i18n resource,断言 resume 与 wait toast 使用 This task has resumed / This task entered Completed status;`src/i18n/__tests__/loadNamespace.test.ts` / `types.test.ts` 补充覆盖 runtimeStatus.completed、control.resume.resumedOne、control.wait.enteredStatusOne 与 control.sendInput.emptyError。验证:`npm test -- "src/components/agent/chat/workspace/useWorkspaceTeamSessionControlRuntime.test.tsx" "src/i18n/__tests__/loadNamespace.test.ts" "src/i18n/__tests__/types.test.ts"` 通过,覆盖 3 个文件、7 个用例;`npx eslint "src/components/agent/chat/workspace/useWorkspaceTeamSessionControlRuntime.ts" "src/components/agent/chat/workspace/useWorkspaceTeamSessionControlRuntime.test.tsx" "src/i18n/__tests__/loadNamespace.test.ts" "src/i18n/__tests__/types.test.ts" --max-warnings 0`、`npm run detect-translations -- --verbose`、`npm run typecheck`、5 个 `agent.json` 的 `python3 -m json.tool` 解析、目标 hook 中文 / `defaultValue` / `t(key, "中文")` fallback 扫描与定向 `git diff --check` 通过,当前资源口径更新为 5 个 locale、6 个 namespace、5895 个 source key。额外尝试 `npm run verify:local`,已通过 `verify:app-version`、`lint`、`typecheck` 并执行 smart vitest 到 12/55 批,但在 `src/lib/teamMemorySync.test.ts` 两个用例因 `crypto.subtle.digest` 为 undefined 失败;`src/lib/teamMemorySync.ts` / `src/lib/teamMemorySync.test.ts` 本轮无 diff,因此不把该统一入口失败归因于本刀,也不宣称完整绿色统一入口。本条属于 Agent Chat Team Workspace session control 普通前端 i18n 收口,不触碰 Tauri 命令、Bridge、DevBridge mock、Team runtime API 行为、projection schema 或并行 Rust / AgentRuntime 脏改;后续仍需继续收 Team operation panel / formation selector / dock 的展示文案长尾,并在合并前复跑统一入口。 + + +281. 2026-05-12 Team Workspace operation panel / selectors source fallback 收口续测:`src/components/agent/chat/team-workspace-runtime/teamOperationSelectors.ts` 新增 `TeamWorkspaceOperationCopy` 注入边界与 locale-aware `formatOperationUpdatedAt`,`src/components/agent/chat/components/team-workspace-board/useTeamWorkspaceBoardOperationState.ts` 接入 `useTranslation("agent")` 构建 operation copy,`src/components/agent/chat/components/team-workspace-board/TeamWorkspaceTeamOperationsPanel.tsx` 把面板标题、最近条数与更新时间从组件内中文 literal 改为 `agentChat.teamWorkspace.operations.*` key-based 读取;wait / control 摘要 title/detail 通过 copy 从资源生成,session name、targetSessionId、status enum、updatedAt、badge class 与 wait/control summary payload 继续作为 runtime / UI 状态保留。新增 5 locale `agentChat.teamWorkspace.operations.*` 16 个正式资源,并复用记录 280 的 `runtimeStatus.*` 状态展示资源;`src/components/agent/chat/team-workspace-runtime/teamOperationSelectors.test.ts` 切到真实 en-US resource 回归 Result received / Pause work / just now,`src/components/agent/chat/components/team-workspace-board/useTeamWorkspaceBoardOperationState.test.tsx` 显式初始化真实 zh-CN i18n,`src/components/agent/chat/components/team-workspace-board/TeamWorkspaceCanvasSurfaceCopy.test.tsx` 新增 Team operations panel 英文资源回归,`src/i18n/__tests__/loadNamespace.test.ts` / `types.test.ts` 补充覆盖 operations heading / recentCount / wait.resolved / control.closeOne 主键。验证:`npm test -- "src/components/agent/chat/team-workspace-runtime/teamOperationSelectors.test.ts" "src/components/agent/chat/components/team-workspace-board/useTeamWorkspaceBoardOperationState.test.tsx" "src/components/agent/chat/components/team-workspace-board/TeamWorkspaceCanvasSurfaceCopy.test.tsx" "src/components/agent/chat/components/team-workspace-board/TeamWorkspaceTeamOverviewChrome.test.tsx" "src/i18n/__tests__/loadNamespace.test.ts" "src/i18n/__tests__/types.test.ts"` 通过,覆盖 6 个文件、20 个用例;`npx eslint "src/components/agent/chat/team-workspace-runtime/teamOperationSelectors.ts" "src/components/agent/chat/team-workspace-runtime/teamOperationSelectors.test.ts" "src/components/agent/chat/components/team-workspace-board/TeamWorkspaceTeamOperationsPanel.tsx" "src/components/agent/chat/components/team-workspace-board/useTeamWorkspaceBoardOperationState.ts" "src/components/agent/chat/components/team-workspace-board/useTeamWorkspaceBoardOperationState.test.tsx" "src/components/agent/chat/components/team-workspace-board/TeamWorkspaceCanvasSurfaceCopy.test.tsx" "src/i18n/__tests__/loadNamespace.test.ts" "src/i18n/__tests__/types.test.ts" --max-warnings 0`、`npm run detect-translations -- --verbose`、`npm run typecheck`、5 个 `agent.json` 的 `python3 -m json.tool` 解析、`agentChat.teamWorkspace.operations.*` / `runtimeStatus.*` / `control.*` 关键 key 的 5 locale 存在性检查、三个目标源文件中文 / `defaultValue` / `t(key, "中文")` fallback 扫描与定向 `git diff --check` 通过,当前资源口径更新为 5 个 locale、6 个 namespace、5986 个 source key。额外尝试 `npm run verify:local`,已通过 `verify:app-version`、`lint`、`typecheck` 并执行 smart vitest 到 11/55 批,但在 `src/lib/teamMemorySync.test.ts` 两个用例因 `crypto.subtle.digest` 为 undefined 失败;`src/lib/teamMemorySync.ts` / `src/lib/teamMemorySync.test.ts` 本轮无 diff,因此不把该统一入口失败归因于本刀,也不宣称完整绿色统一入口。本条属于 Agent Chat Team Workspace operation panel / selectors 普通前端 i18n 收口,不触碰 Tauri 命令、Bridge、DevBridge mock、Team runtime API 行为、projection schema、canvas layout controller 或并行 Rust / AgentRuntime 脏改;后续仍需继续收 formation selector / dock / selected session chrome 的展示文案长尾,并在合并前复跑统一入口。 + + +282. 2026-05-12 Team Workspace formation selector / dock formation chrome source fallback 收口续测:`src/components/agent/chat/team-workspace-runtime/formationDisplaySelectors.ts` 新增 `TeamWorkspaceFormationCopy` 与 `buildTeamWorkspaceFormationCopy` copy 注入边界,`src/components/agent/chat/components/team-workspace-board/useTeamWorkspaceBoardFormationState.ts`、`src/components/agent/chat/components/TeamWorkspaceDock.tsx` 与 `src/components/agent/chat/components/TeamWorkbenchSummaryPanel.tsx` 通过 `useTranslation("agent")` 构建 formation copy,把 runtime formation hint / empty detail / notice、summary badge、面板标题 / headline / description、formation status 与 member status 从 selector 内建中文展示文案改为 `agentChat.teamWorkspace.formation.*` key-based 读取;runtime errorMessage、Team label / summary、role / member label 与 summary、reference label、status enum、badge class 与 blueprint payload 继续作为 runtime / API / UI 状态边界保留,不误迁为静态资源。新增 5 locale `agentChat.teamWorkspace.formation.*` 40 个正式资源;`src/components/agent/chat/team-workspace-runtime/formationDisplaySelectors.test.ts` 切到真实 en-US resource 回归 Team plan / Ready / Planned / Reference plan,`src/components/agent/chat/components/TeamWorkspaceDock.test.tsx` 初始化真实 zh-CN i18n,`src/i18n/__tests__/loadNamespace.test.ts` / `types.test.ts` 补充覆盖 formation hint、summary badge、status title 与 member status 主键。验证:`npm test -- "src/components/agent/chat/team-workspace-runtime/formationDisplaySelectors.test.ts" "src/components/agent/chat/components/TeamWorkspaceDock.test.tsx" "src/components/agent/chat/components/TeamWorkbenchSummaryPanel.test.tsx" "src/components/agent/chat/components/TeamWorkspaceBoard.test.tsx" "src/components/agent/chat/components/team-workspace-board/useTeamWorkspaceBoardComposer.test.tsx" "src/components/agent/chat/components/team-workspace-board/useTeamWorkspaceBoardShellProps.test.tsx" "src/components/agent/chat/components/team-workspace-board/teamWorkspaceBoardPropBuilders.test.ts" "src/i18n/__tests__/loadNamespace.test.ts" "src/i18n/__tests__/types.test.ts"` 通过,覆盖 9 个文件、78 个用例;`npx eslint "src/components/agent/chat/team-workspace-runtime/formationDisplaySelectors.ts" "src/components/agent/chat/team-workspace-runtime/formationDisplaySelectors.test.ts" "src/components/agent/chat/components/team-workspace-board/useTeamWorkspaceBoardFormationState.ts" "src/components/agent/chat/components/TeamWorkspaceDock.tsx" "src/components/agent/chat/components/TeamWorkspaceDock.test.tsx" "src/components/agent/chat/components/TeamWorkbenchSummaryPanel.tsx" "src/components/agent/chat/components/TeamWorkbenchSummaryPanel.test.tsx" "src/i18n/__tests__/loadNamespace.test.ts" "src/i18n/__tests__/types.test.ts" --max-warnings 0`、`npm run typecheck`、`npm run detect-translations -- --verbose`、5 个 `agent.json` 的 JSON 解析、formation 关键 key 的 5 locale 存在性检查、formation selector / board formation hook 中文 / `defaultValue` / `t(key, "中文")` fallback 清零扫描、Dock / SummaryPanel 新增 `t()` fallback 扫描、formation 相关硬编码目标扫描与定向 `git diff --check` 通过,当前资源口径更新为 5 个 locale、6 个 namespace、6026 个 source key。额外未重复执行 `npm run verify:local` / GUI smoke;最近记录 281 已确认统一入口在 smart vitest 11/55 批被非本轮 `src/lib/teamMemorySync.test.ts` 的 `crypto.subtle.digest` undefined 阻断,本条不触碰 `teamMemorySync`、Tauri 命令、Bridge、DevBridge mock、Team runtime API 行为、projection schema 或 canvas layout controller,因此不宣称完整绿色统一入口。后续仍需继续收 `TeamWorkspaceDock.tsx` 其他 literal、selected session inline chrome 与 canvas lane 长尾,并在合并前基于最新 diff 复跑统一入口。 + + +283. 2026-05-12 Team Workspace selected session inline chrome source fallback 收口续测:`src/components/agent/chat/components/team-workspace-board/SelectedSessionInlineHeader.tsx`、`SelectedSessionInlineCollaborationSection.tsx`、`SelectedSessionInlineActivitySection.tsx` 与 `SelectedSessionInlineDetail.tsx` 接入 `useTranslation("agent")`,新增 `agentChat.teamWorkspace.selectedSession.*` 正式资源,把选中任务详情的当前查看 / 当前任务 badge、继续 / 暂停 / 打开进展按钮、任务分工标题、更新时间、继续处理区、等待结果、补充说明输入框 placeholder / 发送 / 中断按钮、完整进展、自动刷新 badge、最新进展错误 / 空态 / loading、进展记录标题与计数从组件内中文 literal 改为 key-based 读取;detailSummary、runtimeDetailSummary、queueReason、setting badges、output contract、skill label/title、activity preview text、entry title/detail/statusLabel 与 updatedAt display 继续作为 runtime / API / UI 状态边界保留。新增 5 locale `agentChat.teamWorkspace.selectedSession.*` 29 个正式资源;`src/components/agent/chat/components/team-workspace-board/TeamWorkspaceCanvasSurfaceCopy.test.tsx` 新增真实 en-US 回归,覆盖 Current view、Resume work、Pause work、Task split、Continue work、Wait 30 seconds for result、Send note、Full progress、Progress log、Updated just now 与 textarea placeholder,并断言不泄露旧中文 source copy;`src/i18n/__tests__/loadNamespace.test.ts` / `types.test.ts` 补充覆盖 selected session header / collaboration / activity / footer 主键。验证:`npm test -- "src/components/agent/chat/components/team-workspace-board/TeamWorkspaceCanvasSurfaceCopy.test.tsx" "src/components/agent/chat/components/TeamWorkspaceBoard.test.tsx" "src/i18n/__tests__/loadNamespace.test.ts" "src/i18n/__tests__/types.test.ts"` 通过,覆盖 4 个文件、46 个用例;`npx eslint "src/components/agent/chat/components/team-workspace-board/SelectedSessionInlineHeader.tsx" "src/components/agent/chat/components/team-workspace-board/SelectedSessionInlineCollaborationSection.tsx" "src/components/agent/chat/components/team-workspace-board/SelectedSessionInlineActivitySection.tsx" "src/components/agent/chat/components/team-workspace-board/SelectedSessionInlineDetail.tsx" "src/components/agent/chat/components/team-workspace-board/TeamWorkspaceCanvasSurfaceCopy.test.tsx" "src/components/agent/chat/components/TeamWorkspaceBoard.test.tsx" "src/i18n/__tests__/loadNamespace.test.ts" "src/i18n/__tests__/types.test.ts" --max-warnings 0`、`npm run typecheck`、`npm run detect-translations -- --verbose`、5 个 `agent.json` 的 JSON 解析、selected session 关键 key 的 5 locale 存在性检查、4 个目标组件中文 / `defaultValue` / `t(key, "中文")` fallback 清零扫描与定向 `git diff --check` 通过,当前资源口径更新为 5 个 locale、6 个 namespace、6055 个 source key。本条属于 Team Workspace selected session inline chrome 普通前端 i18n 收口,不触碰 Tauri 命令、Bridge、DevBridge mock、Team runtime API、activity preview selector 或 canvas controller;未重复执行 `npm run verify:local` / GUI smoke,继续沿用记录 281 的非本轮 `src/lib/teamMemorySync.test.ts` `crypto.subtle.digest` undefined 统一入口 blocker 口径,未作为完整绿色统一入口。后续仍需继续收 `canvasLaneSelectors.ts`、`teamWorkspaceBoardPresentationSelectors.ts`、`TeamWorkspaceTeamOverviewChrome.tsx` 与 `TeamWorkspaceDock.tsx` 其他 current progress / concurrency chrome 长尾,并在合并前基于最新 diff 复跑统一入口。 + + +284. 2026-05-12 Team Workspace board presentation / overview chrome source fallback 收口续测:`src/components/agent/chat/components/team-workspace-board/teamWorkspaceBoardPresentationSelectors.ts` 新增 `TeamWorkspaceBoardCopy` 与 `buildTeamWorkspaceBoardCopy` 注入边界,`useTeamWorkspaceBoardPresentation.tsx` 通过 `useTranslation("agent")` 构建 copy,把详情展开 / 收起、当前进展标题、runtime / child runtime / formation 状态下的画布说明从 selector 内建中文改为 `agentChat.teamWorkspace.board.*` key-based 读取;`TeamWorkspaceTeamOverviewChrome.tsx` 接入 `useTranslation("agent")` 与 `formatNumber`,把等待任务接手、当前焦点 / 等待接手 badge、更新于、当前任务、紧凑区任务处理 / 视图标签,以及非紧凑 waitable / completed 任务计数改为正式资源。新增 5 locale `agentChat.teamWorkspace.board.*` / `agentChat.teamWorkspace.overview.*` 16 个正式资源,并复用记录 283 的 selected session 更新时间 / 当前任务资源;`teamWorkspaceBoardPresentationSelectors.test.ts` 切到真实 en-US resource 回归 View details、Current progress、formation failed 与 runtime lane 文案,`TeamWorkspaceTeamOverviewChrome.test.tsx` 新增真实 en-US 回归覆盖 Current focus、Updated just now、Task actions、View、2 task(s) running 与 1 task(s) completed;`src/i18n/__tests__/loadNamespace.test.ts` / `types.test.ts` 补充覆盖 board canvas / overview 主键。验证:`npm test -- "src/components/agent/chat/components/team-workspace-board/teamWorkspaceBoardPresentationSelectors.test.ts" "src/components/agent/chat/components/team-workspace-board/TeamWorkspaceTeamOverviewChrome.test.tsx" "src/components/agent/chat/components/team-workspace-board/useTeamWorkspaceBoardShellProps.test.tsx" "src/components/agent/chat/components/team-workspace-board/teamWorkspaceBoardPropBuilders.test.ts" "src/components/agent/chat/components/TeamWorkspaceBoard.test.tsx" "src/i18n/__tests__/loadNamespace.test.ts" "src/i18n/__tests__/types.test.ts"` 通过,覆盖 7 个文件、50 个用例;`npx eslint "src/components/agent/chat/components/team-workspace-board/teamWorkspaceBoardPresentationSelectors.ts" "src/components/agent/chat/components/team-workspace-board/teamWorkspaceBoardPresentationSelectors.test.ts" "src/components/agent/chat/components/team-workspace-board/useTeamWorkspaceBoardPresentation.tsx" "src/components/agent/chat/components/team-workspace-board/TeamWorkspaceTeamOverviewChrome.tsx" "src/components/agent/chat/components/team-workspace-board/TeamWorkspaceTeamOverviewChrome.test.tsx" "src/components/agent/chat/components/team-workspace-board/useTeamWorkspaceBoardShellProps.test.tsx" "src/components/agent/chat/components/team-workspace-board/teamWorkspaceBoardPropBuilders.test.ts" "src/components/agent/chat/components/TeamWorkspaceBoard.test.tsx" "src/i18n/__tests__/loadNamespace.test.ts" "src/i18n/__tests__/types.test.ts" --max-warnings 0`、`npm run typecheck`、`npm run detect-translations -- --verbose`、5 个 `agent.json` 的 JSON 解析、board / overview 关键 key 的 5 locale 存在性检查、3 个目标源文件中文 / `defaultValue` / `t(key, "中文")` fallback 清零扫描与定向 `git diff --check` 通过,当前资源口径更新为 5 个 locale、6 个 namespace、6071 个 source key。本条属于 Team Workspace board presentation / overview chrome 普通前端 i18n 收口,不触碰 Tauri 命令、Bridge、DevBridge mock、Team runtime API、activity preview selector 或 canvas controller;未重复执行 `npm run verify:local` / GUI smoke,继续沿用记录 281 的非本轮 `src/lib/teamMemorySync.test.ts` `crypto.subtle.digest` undefined 统一入口 blocker 口径,未作为完整绿色统一入口。后续仍需继续收 `canvasLaneSelectors.ts`、`TeamWorkspaceCanvasStage.tsx` 与 `TeamWorkspaceDock.tsx` 其他 current progress / concurrency chrome 长尾,并在合并前基于最新 diff 复跑统一入口。 + +285. 2026-05-12 Team Workspace canvas lane selector / runtime hook source fallback 收口续测:`src/components/agent/chat/team-workspace-runtime/canvasLaneSelectors.ts` 新增 `TeamWorkspaceCanvasLaneCopy` / `buildTeamWorkspaceCanvasLaneCopy` 与 source resource 派生默认 copy,`src/components/agent/chat/components/team-workspace-board/useTeamWorkspaceBoardCanvasRuntime.ts` 通过 `useTranslation("agent")` 注入 copy,把 canvas lane 的更新时间、runtime detail summary(等待数、最近进展、处理中并发、稳妥模式)、同步失败 / loading / 各 runtime 状态空态、无任务摘要、runtime member hint、planned lane badge / status / updatedAt 从 selector 内建中文 fallback 改为 `agentChat.teamWorkspace.canvasLane.*` 与既有 `runtimeStatus.*` / `formation.memberStatus.*` key-based 读取;session name / taskSummary、member / planned role label 与 summary、activity entry title / detail / statusLabel、skill name、model 与 preview payload 继续作为 runtime / API 数据边界保留。新增 5 locale `agentChat.teamWorkspace.canvasLane.*` 24 个正式资源,并在 `src/i18n/__tests__/loadNamespace.test.ts` / `types.test.ts` 覆盖 runtimeDetail、empty 与 planned 主键;`src/components/agent/chat/team-workspace-runtime/canvasLaneSelectors.test.ts` 新增真实 en-US resource copy 注入回归,验证 Runtime worker、runtime summary 数据不被翻译,同时 canvas lane chrome 输出 No task summary、Running、Latest progress、Steady mode、Syncing latest content 与 planned lane 英文提示。验证:`npm test -- "src/components/agent/chat/team-workspace-runtime/canvasLaneSelectors.test.ts" "src/components/agent/chat/components/team-workspace-board/useTeamWorkspaceBoardCanvasRuntime.test.tsx" "src/components/agent/chat/components/team-workspace-board/TeamWorkspaceCanvasSurfaceCopy.test.tsx" "src/components/agent/chat/components/team-workspace-board/useTeamWorkspaceBoardShellProps.test.tsx" "src/components/agent/chat/components/team-workspace-board/teamWorkspaceBoardPropBuilders.test.ts" "src/components/agent/chat/components/TeamWorkspaceBoard.test.tsx" "src/i18n/__tests__/loadNamespace.test.ts" "src/i18n/__tests__/types.test.ts"` 通过,覆盖 8 个文件、58 个用例;`npx eslint "src/components/agent/chat/team-workspace-runtime/canvasLaneSelectors.ts" "src/components/agent/chat/team-workspace-runtime/canvasLaneSelectors.test.ts" "src/components/agent/chat/components/team-workspace-board/useTeamWorkspaceBoardCanvasRuntime.ts" "src/components/agent/chat/components/team-workspace-board/useTeamWorkspaceBoardCanvasRuntime.test.tsx" "src/i18n/__tests__/loadNamespace.test.ts" "src/i18n/__tests__/types.test.ts" --max-warnings 0`、`npm run typecheck`、`npm run detect-translations -- --verbose`、5 个 `agent.json` 的 JSON 解析、canvas lane 关键 key 的 5 locale 存在性检查、`canvasLaneSelectors.ts` / `useTeamWorkspaceBoardCanvasRuntime.ts` 中文 / `defaultValue` / `t(key, "中文")` fallback 清零扫描与定向 `git diff --check` 通过,当前资源口径更新为 5 个 locale、6 个 namespace、6095 个 source key。本条属于 Team Workspace canvas lane selector / runtime hook 普通前端 i18n 收口,不触碰 Tauri 命令、Bridge、DevBridge mock、Team runtime API、activity preview entry payload、canvas layout controller、Dock concurrency chrome 或 CanvasStage chrome;未重复执行 `npm run verify:local` / GUI smoke,继续沿用记录 281 的非本轮 `src/lib/teamMemorySync.test.ts` `crypto.subtle.digest` undefined 统一入口 blocker 口径,未作为完整绿色统一入口。后续仍需继续收 `TeamWorkspaceCanvasStage.tsx` 与 `TeamWorkspaceDock.tsx` 其他 current progress / concurrency chrome 长尾,并在合并前基于最新 diff 复跑统一入口。 + +286. 2026-05-12 Team Workspace CanvasStage chrome source fallback 收口续测:`src/components/agent/chat/components/team-workspace-board/TeamWorkspaceCanvasStage.tsx` 接入 `useTranslation("agent")`,把画布左上角当前进展 badge、快捷键 / 拖拽提示与空画布标题从组件内中文 literal 改为 `agentChat.teamWorkspace.canvasStage.*` key-based 读取;`canvasStageHint` 继续作为上游 selector / runtime presentation 状态传入,不在 Stage 内误翻译。新增 5 locale `agentChat.teamWorkspace.canvasStage.*` 3 个正式资源,并在 `src/i18n/__tests__/loadNamespace.test.ts` / `types.test.ts` 覆盖 badge、shortcutHint 与 emptyTitle 主键;`src/components/agent/chat/components/team-workspace-board/TeamWorkspaceCanvasSurfaceCopy.test.tsx` 新增真实 en-US 回归,验证 Current progress、Drag to arrange tasks、No current progress 来自 `agent` namespace,同时保留 runtime hint 文案。验证:`npm test -- "src/components/agent/chat/components/team-workspace-board/TeamWorkspaceCanvasSurfaceCopy.test.tsx" "src/components/agent/chat/components/TeamWorkspaceBoard.test.tsx" "src/i18n/__tests__/loadNamespace.test.ts" "src/i18n/__tests__/types.test.ts"` 通过,覆盖 4 个文件、47 个用例;`npx eslint "src/components/agent/chat/components/team-workspace-board/TeamWorkspaceCanvasStage.tsx" "src/components/agent/chat/components/team-workspace-board/TeamWorkspaceCanvasSurfaceCopy.test.tsx" "src/i18n/__tests__/loadNamespace.test.ts" "src/i18n/__tests__/types.test.ts" --max-warnings 0`、`npm run typecheck`、`npm run detect-translations -- --verbose`、5 个 `agent.json` 的 JSON 解析、CanvasStage 关键 key 的 5 locale 存在性检查、`TeamWorkspaceCanvasStage.tsx` 中文 / `defaultValue` / `t(key, "中文")` fallback 清零扫描与定向 `git diff --check` 通过,当前资源口径更新为 5 个 locale、6 个 namespace、6098 个 source key。本条属于 Team Workspace CanvasStage 普通前端 i18n chrome 收口,不触碰 Tauri 命令、Bridge、DevBridge mock、Team runtime API、canvas lane selector、canvas layout controller 或 Dock concurrency chrome;未重复执行 `npm run verify:local` / GUI smoke,继续沿用记录 281 的非本轮 `src/lib/teamMemorySync.test.ts` `crypto.subtle.digest` undefined 统一入口 blocker 口径,未作为完整绿色统一入口。后续仍需继续收 `TeamWorkspaceDock.tsx` current progress / concurrency chrome 长尾,并在合并前基于最新 diff 复跑统一入口。 + +287. 2026-05-12 Team Workspace Dock current progress / concurrency chrome source fallback 收口续测:`src/components/agent/chat/components/TeamWorkspaceDock.tsx` 复用 `useTranslation("agent")` 与 `formatNumber`,把 Dock 浮钮 / launcher 的当前进展主标签、运行中并发 badge、等待队列 badge、任务数量 badge、空态生成标签、当前分工方案 badge、不遮挡画布提示与打开 / 收起 / 展开当前进展 aria label 从组件内中文 literal 与 `teamWorkspaceCopy` 中文常量改为 `agentChat.teamWorkspace.dock.*` / `runtimeStatus.idle` key-based 读取;队列原因、runtimeTeamLabel、runtimeTeamSummary、member / role label 与 summary、skillId、profileId、roleKey、TeamWorkspaceBoard 嵌入内容继续作为 runtime / API / 下游组件数据边界保留。新增 5 locale `agentChat.teamWorkspace.dock.*` 11 个正式资源,并在 `src/i18n/__tests__/loadNamespace.test.ts` / `types.test.ts` 覆盖 currentProgress、processingCount、selectedPlan 与 aria.expand 主键;`src/components/agent/chat/components/TeamWorkspaceDock.test.tsx` 新增真实 en-US 回归,验证 Current progress、1/2 running、1 waiting、Expand current progress、Generate、Current team plan 与 Keeps canvas clear 来自 `agent` namespace,同时断言不泄露对应中文 source copy。验证:`npm test -- "src/components/agent/chat/components/TeamWorkspaceDock.test.tsx" "src/components/agent/chat/components/TeamWorkbenchSummaryPanel.test.tsx" "src/components/agent/chat/components/TeamWorkspaceBoard.test.tsx" "src/components/agent/chat/components/team-workspace-board/TeamWorkspaceCanvasSurfaceCopy.test.tsx" "src/i18n/__tests__/loadNamespace.test.ts" "src/i18n/__tests__/types.test.ts"` 通过,覆盖 6 个文件、77 个用例;`npx eslint "src/components/agent/chat/components/TeamWorkspaceDock.tsx" "src/components/agent/chat/components/TeamWorkspaceDock.test.tsx" "src/components/agent/chat/components/team-workspace-board/TeamWorkspaceCanvasStage.tsx" "src/components/agent/chat/components/team-workspace-board/TeamWorkspaceCanvasSurfaceCopy.test.tsx" "src/i18n/__tests__/loadNamespace.test.ts" "src/i18n/__tests__/types.test.ts" --max-warnings 0`、`npm run typecheck`、`npm run detect-translations -- --verbose`、5 个 `agent.json` 的 JSON 解析、Dock / CanvasStage 关键 key 的 5 locale 存在性检查、`TeamWorkspaceDock.tsx` / `TeamWorkspaceCanvasStage.tsx` 中文 / `defaultValue` / `t(key, "中文")` fallback 清零扫描与定向 `git diff --check` 通过,当前资源口径更新为 5 个 locale、6 个 namespace、6109 个 source key。本条属于 Team Workspace Dock current progress / concurrency 普通前端 i18n chrome 收口,不触碰 Tauri 命令、Bridge、DevBridge mock、Team runtime API、canvas lane selector、canvas layout controller 或 TeamWorkspaceBoard 内部行为;未重复执行 `npm run verify:local` / GUI smoke,继续沿用记录 281 的非本轮 `src/lib/teamMemorySync.test.ts` `crypto.subtle.digest` undefined 统一入口 blocker 口径,未作为完整绿色统一入口。后续 Team Workspace 主画布入口 raw literal 目标已清零,仍需继续审计更深 Agent Chat runtime suggestion / display normalizer 等长尾,并在合并前基于最新 diff 复跑统一入口。 + +288. 2026-05-12 Team Workspace selected session detail selector source fallback 收口续测:`src/components/agent/chat/team-workspace-runtime/selectedSessionDetailSelectors.ts` 新增 `SelectedSessionDetailCopy` / `buildSelectedSessionDetailCopy` 与 source resource 派生默认 copy,`src/components/agent/chat/components/team-workspace-board/useTeamWorkspaceBoardPresentation.tsx` 通过 `useTranslation("agent")` 注入 copy,把选中任务详情区的设置 badge(预设 / 风格 / 分工 / 主题)、metadata badge(分工、服务、模型、来源、来自之前任务、父任务来源)、session type、role hint、team preset label,以及等待数 / 最近进展 / 处理中并发 / 稳妥模式 runtime detail summary 从 selector 内建中文 literal 改为 `agentChat.teamWorkspace.selectedSession.detail.*`、`agentChat.teamWorkspace.teamPreset.*` 与既有 `runtimeStatus.*` / `canvasLane.runtimeDetail.*` key-based 读取;`src/components/agent/chat/team-workspace-runtime/canvasLaneSelectors.ts` 仅抽出 `TeamWorkspaceRuntimeDetailCopy` 作为最小 runtime detail copy 边界,避免 selected detail 复制状态摘要逻辑。新增 5 locale `agentChat.teamWorkspace.selectedSession.detail.*` 与 `agentChat.teamWorkspace.teamPreset.*` 26 个正式资源;session name、blueprint role label、profileName、providerName、model、originTool、queueReason、outputContract、skill label/title、activity entry title/detail/statusLabel 与 parentSessionName 继续作为 runtime / API / 用户数据保留,不误迁为静态资源。`src/components/agent/chat/team-workspace-runtime/selectedSessionDetailSelectors.test.ts` 新增真实 en-US resource copy 注入回归,验证 Waiting / Latest progress / Plan Code triage team / Role Execution 来自资源,同时保留中文 runtime label / skill name;`src/i18n/__tests__/loadNamespace.test.ts` / `types.test.ts` 补充覆盖 selected session detail setting / metadata / sessionType / roleHint 与 teamPreset 主键。验证:`npm test -- "src/components/agent/chat/team-workspace-runtime/selectedSessionDetailSelectors.test.ts" "src/components/agent/chat/team-workspace-runtime/canvasLaneSelectors.test.ts" "src/components/agent/chat/components/TeamWorkspaceBoard.test.tsx" "src/components/agent/chat/components/team-workspace-board/TeamWorkspaceCanvasSurfaceCopy.test.tsx" "src/i18n/__tests__/loadNamespace.test.ts" "src/i18n/__tests__/types.test.ts"` 通过,覆盖 6 个文件、55 个用例;`npx eslint "src/components/agent/chat/team-workspace-runtime/selectedSessionDetailSelectors.ts" "src/components/agent/chat/team-workspace-runtime/selectedSessionDetailSelectors.test.ts" "src/components/agent/chat/team-workspace-runtime/canvasLaneSelectors.ts" "src/components/agent/chat/components/team-workspace-board/useTeamWorkspaceBoardPresentation.tsx" "src/components/agent/chat/components/TeamWorkspaceBoard.test.tsx" "src/components/agent/chat/components/team-workspace-board/TeamWorkspaceCanvasSurfaceCopy.test.tsx" "src/i18n/__tests__/loadNamespace.test.ts" "src/i18n/__tests__/types.test.ts" --max-warnings 0`、`npm run typecheck`、`npm run detect-translations -- --verbose`、5 个 `agent.json` JSON 解析、selected session detail 关键 key 的 5 locale 存在性检查、`selectedSessionDetailSelectors.ts` / `useTeamWorkspaceBoardPresentation.tsx` 中文 / `defaultValue` / `t(key, "中文")` fallback 清零扫描与定向 `git diff --check` 通过,当前资源口径更新为 5 个 locale、6 个 namespace、6135 个 source key。本条属于 Team Workspace selected session detail selector 普通前端 i18n 收口,不触碰 Tauri 命令、Bridge、DevBridge mock、Team runtime API、activity preview entry payload 或 canvas layout controller;未重复执行 `npm run verify:local` / GUI smoke,继续沿用记录 281 的非本轮 `src/lib/teamMemorySync.test.ts` `crypto.subtle.digest` undefined 统一入口 blocker 口径,未作为完整绿色统一入口。后续仍需继续收 `boardChromeSelectors.ts` 与 Team Workspace 更深 runtime / activity projection 长尾,并在合并前基于最新 diff 复跑统一入口。 + +289. 2026-05-12 Team Workspace board chrome selector source fallback 收口续测:`src/components/agent/chat/team-workspace-runtime/boardChromeSelectors.ts` 新增 `TeamWorkspaceBoardChromeCopy` / `buildTeamWorkspaceBoardChromeCopy` 与 source resource 派生默认 copy,`src/components/agent/chat/components/team-workspace-board/useTeamWorkspaceBoardPresentation.tsx` 通过 `useTranslation("agent")` 注入 copy,把主画布 chrome 的标题 / 提示、紧凑工具条 chips(当前焦点、状态、更新时间、当前任务、处理中、已完成)与状态汇总 badge 从 selector 内建中文 literal 和旧中文相对时间 fallback 改为 `agentChat.teamWorkspace.boardChrome.*`、既有 `runtimeStatus.*`、`selectedSession.header.currentTaskBadge`、`overview.selectedSession.waitingTitle` 与 `canvasLane.updatedNow` key-based 读取;`formatNumber`、`formatDate`、`formatRelativeTime` 统一走 `src/i18n/format.ts`。新增 5 locale `agentChat.teamWorkspace.boardChrome.*` 22 个正式资源;selectedSession name、parentSessionName、runtimeFormationTitle / runtimeFormationHint、status enum、updatedAt 与 runtime session payload 继续作为 runtime / API / UI 状态边界保留,不误迁为静态资源。`src/components/agent/chat/team-workspace-runtime/boardChromeSelectors.test.ts` 新增真实 en-US resource copy 注入回归,验证 Tasks running、Current focus、Updated just now、Current task、running / completed chips 与 status summary 来自资源,同时保留中文会话名 `研究员`;`src/i18n/__tests__/loadNamespace.test.ts` / `types.test.ts` 补充覆盖 board chrome headline、hint、chip 与 summary 主键。验证:`npm test -- "src/components/agent/chat/team-workspace-runtime/boardChromeSelectors.test.ts" "src/components/agent/chat/components/team-workspace-board/teamWorkspaceBoardPresentationSelectors.test.ts" "src/components/agent/chat/components/team-workspace-board/TeamWorkspaceTeamOverviewChrome.test.tsx" "src/components/agent/chat/components/TeamWorkspaceBoard.test.tsx" "src/i18n/__tests__/loadNamespace.test.ts" "src/i18n/__tests__/types.test.ts"` 通过,覆盖 6 个文件、49 个用例;`npx eslint "src/components/agent/chat/team-workspace-runtime/boardChromeSelectors.ts" "src/components/agent/chat/team-workspace-runtime/boardChromeSelectors.test.ts" "src/components/agent/chat/components/team-workspace-board/useTeamWorkspaceBoardPresentation.tsx" "src/components/agent/chat/components/team-workspace-board/teamWorkspaceBoardPresentationSelectors.test.ts" "src/components/agent/chat/components/team-workspace-board/TeamWorkspaceTeamOverviewChrome.test.tsx" "src/components/agent/chat/components/TeamWorkspaceBoard.test.tsx" "src/i18n/__tests__/loadNamespace.test.ts" "src/i18n/__tests__/types.test.ts" --max-warnings 0`、`npm run typecheck`、`npm run detect-translations -- --verbose`、5 个 `agent.json` 的 JSON 解析、board chrome 关键 key 的 5 locale 存在性检查、`boardChromeSelectors.ts` / `useTeamWorkspaceBoardPresentation.tsx` 中文 / `defaultValue` / `t(key, "中文")` fallback 清零扫描与定向 `git diff --check` 通过,当前资源口径更新为 5 个 locale、6 个 namespace、6157 个 source key。本条属于 Team Workspace board chrome selector 普通前端 i18n 收口,不触碰 Tauri 命令、Bridge、DevBridge mock、Team runtime API、activity preview entry payload、live runtime projector 或 canvas layout controller;未重复执行 `npm run verify:local` / GUI smoke,继续沿用记录 281 的非本轮 `src/lib/teamMemorySync.test.ts` `crypto.subtle.digest` undefined 统一入口 blocker 口径,未作为完整绿色统一入口。后续 Team Workspace 主画布入口与 board chrome 目标已进一步收窄,仍需继续审计更深 runtime / activity projection 长尾(例如 `activityPreviewSelectors.ts`、`liveRuntimeProjector.ts`)和 Agent Chat 更深路径,并在合并前基于最新 diff 复跑统一入口。 + + +290. 2026-05-12 Team Workspace activity preview selector / sync hook source fallback 收口续测:`src/components/agent/chat/team-workspace-runtime/activityPreviewSelectors.ts` 新增 `ActivityPreviewCopy` / `buildActivityPreviewCopy` 与 source resource 派生默认 copy,把 activity preview 行格式、message fallback 的错误 / 输出 / 回复标题、状态 label(错误 / 警告 / 进行中 / 失败 / 完成 / 消息)从 selector 内建中文 literal 改为 `agentChat.teamWorkspace.activityPreview.*` key-based 读取;`buildActivityPreviewFromEntry()`、`extractSessionActivitySnapshot()` 与 `buildSelectedSessionActivityState()` 支持 copy 注入,继续保留 runtime activity entry title / detail / statusLabel、assistant message 正文、tool output / error、fingerprint、refreshVersion 与 sourceLabel 作为 runtime / API 数据边界。`src/components/agent/chat/components/team-workspace-board/useTeamWorkspaceActivityPreviews.ts` 与 `useTeamWorkspaceActivityPreviewSync.ts` 通过 `useTranslation("agent")` 构建并传递 activity preview copy,同时把最近过程同步失败 fallback 从 hook 内建中文改为 `activityPreview.error.syncFailed`;`src/components/agent/chat/components/TeamWorkbenchSummaryPanel.tsx` 仅把 transcript history snapshot copy 与读取历史正文失败 fallback 接入同一 copy,文件内其他 Team Workbench 长尾中文不纳入本刀清零范围。新增 5 locale `agentChat.teamWorkspace.activityPreview.*` 12 个正式资源;`src/components/agent/chat/team-workspace-runtime/activityPreviewSelectors.test.ts` 新增真实 en-US resource copy 注入回归,验证 Output / Message 与英文冒号行格式来自资源,同时保留中文消息正文 `页面结构差异已提取完成。`;`src/i18n/__tests__/loadNamespace.test.ts` / `types.test.ts` 补充覆盖 line、status.message、messageTitle.output 与 error.syncFailed 主键。验证:`npm test -- "src/components/agent/chat/team-workspace-runtime/activityPreviewSelectors.test.ts" "src/components/agent/chat/components/team-workspace-board/useTeamWorkspaceActivityPreviews.test.tsx" "src/components/agent/chat/components/TeamWorkbenchSummaryPanel.test.tsx" "src/components/agent/chat/team-workspace-runtime/canvasLaneSelectors.test.ts" "src/components/agent/chat/components/team-workspace-board/useTeamWorkspaceBoardCanvasRuntime.test.tsx" "src/i18n/__tests__/loadNamespace.test.ts" "src/i18n/__tests__/types.test.ts"` 通过,覆盖 7 个文件、33 个用例;`npx eslint "src/components/agent/chat/team-workspace-runtime/activityPreviewSelectors.ts" "src/components/agent/chat/team-workspace-runtime/activityPreviewSelectors.test.ts" "src/components/agent/chat/components/team-workspace-board/useTeamWorkspaceActivityPreviews.ts" "src/components/agent/chat/components/team-workspace-board/useTeamWorkspaceActivityPreviewSync.ts" "src/components/agent/chat/components/team-workspace-board/useTeamWorkspaceActivityPreviews.test.tsx" "src/components/agent/chat/components/TeamWorkbenchSummaryPanel.tsx" "src/components/agent/chat/components/TeamWorkbenchSummaryPanel.test.tsx" "src/i18n/__tests__/loadNamespace.test.ts" "src/i18n/__tests__/types.test.ts" --max-warnings 0`、`npm run typecheck`、`npm run detect-translations -- --verbose`、5 个 `agent.json` 的 JSON 解析、activity preview 关键 key 的 5 locale 存在性检查、activity preview selector / hooks 中文 / `defaultValue` / `t(key, "中文")` fallback 清零扫描、TeamWorkbench 本刀旧 fallback targeted 扫描与定向 `git diff --check` 通过,当前资源口径更新为 5 个 locale、6 个 namespace、6169 个 source key。本条属于 Team Workspace activity preview 普通前端 i18n helper / hook 收口,不触碰 Tauri 命令、Bridge、DevBridge mock、Team runtime API、live runtime projector 事件语义或 TeamWorkbenchSummaryPanel 其他 route/status 长尾;未重复执行 `npm run verify:local` / GUI smoke,继续沿用记录 281 的非本轮 `src/lib/teamMemorySync.test.ts` `crypto.subtle.digest` undefined 统一入口 blocker 口径,未作为完整绿色统一入口。后续仍需继续审计 `liveRuntimeProjector.ts` 与 `teamWorkspaceRuntime.ts` 生成 runtime activity entry 的 presentation fallback,以及 Agent Chat 更深路径,并在合并前基于最新 diff 复跑统一入口。 + + +291. 2026-05-12 Team Workspace live runtime projector / runtime subscriptions source fallback 收口续测:`src/components/agent/chat/team-workspace-runtime/liveRuntimeProjector.ts` 新增 `LiveRuntimeProjectorCopy` / `buildLiveRuntimeProjectorCopy` 与 source resource 派生默认 copy,把状态切换、文本 / thinking draft、runtime status fallback title / status、tool processing / completed fallback、warning / error title / status,以及 queue / turn lifecycle title / detail / status 从 projector 内建中文 literal 改为 `agentChat.teamWorkspace.liveRuntime.*` 与既有 `agentChat.teamWorkspace.runtimeStatus.*` key-based 读取;`src/components/agent/chat/team-workspace-runtime/runtimeEventSubscriptions.ts` 支持 live runtime copy 注入,`src/components/agent/chat/hooks/useTeamWorkspaceRuntime.ts` 通过 `useTranslation("agent")` 构建并传给 status / stream subscriptions。新增 5 locale `agentChat.teamWorkspace.liveRuntime.*` 27 个正式资源;runtime status event title / detail / checkpoints、tool display label(例如 `页面截图`)、tool output / error、turn error message、sourceType / sourceLabel、queue metadata、session / thread / patch facts 继续作为 runtime / API 数据边界保留,不误迁为静态资源。`src/components/agent/chat/team-workspace-runtime/liveRuntimeProjector.test.ts` 新增真实 en-US copy 注入回归,验证 Status changed、Running、Processing、Queued 与 lifecycle 文案来自资源,同时保留中文工具展示名 `页面截图`;`src/components/agent/chat/hooks/useTeamWorkspaceRuntime.test.tsx` 初始化真实 zh-CN i18n,覆盖 hook 通过 subscriptions 传递 copy 后的 live activity 投影;`src/i18n/__tests__/loadNamespace.test.ts` / `types.test.ts` 补充覆盖 liveRuntime title / detail / lifecycle / status 主键。验证:`npm test -- "src/components/agent/chat/team-workspace-runtime/liveRuntimeProjector.test.ts" "src/components/agent/chat/team-workspace-runtime/runtimeEventSubscriptions.test.ts" "src/components/agent/chat/hooks/useTeamWorkspaceRuntime.test.tsx" "src/components/agent/chat/team-workspace-runtime/activityPreviewSelectors.test.ts" "src/components/agent/chat/team-workspace-runtime/canvasLaneSelectors.test.ts" "src/i18n/__tests__/loadNamespace.test.ts" "src/i18n/__tests__/types.test.ts"` 通过,覆盖 7 个文件、31 个用例;`npx eslint "src/components/agent/chat/team-workspace-runtime/liveRuntimeProjector.ts" "src/components/agent/chat/team-workspace-runtime/liveRuntimeProjector.test.ts" "src/components/agent/chat/team-workspace-runtime/runtimeEventSubscriptions.ts" "src/components/agent/chat/team-workspace-runtime/runtimeEventSubscriptions.test.ts" "src/components/agent/chat/hooks/useTeamWorkspaceRuntime.ts" "src/components/agent/chat/hooks/useTeamWorkspaceRuntime.test.tsx" "src/i18n/__tests__/loadNamespace.test.ts" "src/i18n/__tests__/types.test.ts" --max-warnings 0`、`npm run typecheck`、`npm run detect-translations -- --verbose`、5 个 `agent.json` 的 JSON 解析、liveRuntime 关键 key 的 5 locale 存在性检查、`liveRuntimeProjector.ts` / `runtimeEventSubscriptions.ts` / `useTeamWorkspaceRuntime.ts` 中文 / `defaultValue` / `t(key, "中文")` fallback 清零扫描与定向 `git diff --check` 通过,当前资源口径在 i18n worktree 更新为 5 个 locale、6 个 namespace、6196 个 source key;合并到 main 后保留主干已有 `agentChat.imageWorkbenchPreview.tool.*` 3 个资源,主干检测口径为 5 个 locale、6 个 namespace、6199 个 source key。本条属于 Team Workspace live runtime projector 普通前端 i18n helper / hook 收口,不触碰 Tauri 命令、Bridge、DevBridge mock、Team runtime API、runtime event payload、activity preview entry payload 或 `teamWorkspaceRuntime.ts` 的 thread item presentation 生成;未重复执行 `npm run verify:local` / GUI smoke,继续沿用记录 281 的非本轮 `src/lib/teamMemorySync.test.ts` `crypto.subtle.digest` undefined 统一入口 blocker 口径,未作为完整绿色统一入口。后续仍需继续审计 `teamWorkspaceRuntime.ts` 生成 runtime activity entry 的 presentation fallback,以及 Agent Chat 更深路径;用户本轮要求合并到 main 并暂停,因此本记录后不继续下一刀。 + + 仍未完成: -1. `common` namespace 已覆盖 App 根壳层、SplashScreen 启动画面(source fallback 已在记录 229 二次收口)、StartupLoadingScreen 启动加载屏(source fallback 已在记录 226 二次收口)、ConfirmDialog 共享确认弹窗(source fallback 已在记录 228 二次收口)、UpdateNotification 独立更新提醒窗口(source fallback 已在记录 218 二次收口)、SmartInput 独立悬浮输入窗(source fallback 已在记录 219 二次收口)、ShortcutSettings 共享快捷键设置组件(source fallback 已在记录 224 二次收口)、LatestRunStatusBadge 执行状态徽章(source fallback 已在记录 218 二次收口)、项目 / 工作区切换器(ProjectSelector source fallback 已在记录 216 二次收口)、新建项目弹窗(source fallback 已在记录 235 二次收口)、useProjects 重命名空名称错误(source fallback 已在记录 236 二次收口)、Deep Link 邀请 / 登录 / 支付 toast(source fallback 已在记录 237 二次收口)、App Startup Windows 启动自检 toast(source fallback 已在记录 238 二次收口)、OEM Cloud Access 登录 / OAuth / 支付反馈 / 会话刷新退出 / 邮箱验证码 / 密码登录 / Provider 默认服务 / API Key / 状态标签 / 浏览器预打开页与外部浏览器错误反馈(source fallback 已在记录 239 二次收口),以及 OEM Lime Hub provider sync 托管 Key 名称(source fallback 已在记录 239 二次收口;旧 `Lime 云端模型` 仅作为 legacy 本地 Key alias 数据匹配保留);`navigation` namespace 已覆盖侧边栏主入口、搜索弹窗、会话列表起步路径、会话 meta 格式化、会话重命名 / 删除的 prompt / confirm / toast、账号菜单核心未登录 / 已登录操作文案、邀请入口 / 邀请弹窗 / 复制邀请 toast,以及外观快捷面板;账号套餐 usage 已确认属于云端运行时字段边界,不再作为侧栏静态 i18n 缺口;`workspace` namespace 已覆盖 ArtifactToolbar 工具栏 chrome、ArtifactRenderer 统一入口空内容态 / 流式状态 / 未知类型回退、CanvasAdapter 图层设计预览 chrome、CanvasBreadcrumbHeader 返回按钮(source fallback 已在记录 230 收口)、Resource Manager PDF 预览器(source fallback 已在记录 231 收口)、Resource Manager 媒体播放预览器(source fallback 已在记录 232 收口)、DocumentRenderer 文档工具栏 chrome、ArtifactDocumentRenderer 结构化阅读面 chrome / block fallback、BrowserAssistRenderer 状态说明 / browserActionIndex 复盘、A2UI 提交完成提示 / 待处理表单状态 chrome(WorkspacePendingA2UIPanel source fallback 已在记录 276 二次收口)、图片素材 `ImageGallery`、Runtime AGENTS 引导 toast、视频 PromptInput(source fallback 已在记录 218 二次收口)、视频 VideoCanvas 插图反馈 / 侧栏按钮、视频 VideoSidebar 参数栏概览 / 创作 Tips / 参考图上传区 / 基础控制区 / 模型面板、视频 VideoWorkspace 首屏 / 摘要卡 / 最近任务标题 / 预览操作 / 任务状态 / 同步提示 / 会话主预览 / 聚焦任务面板 / 生成入口反馈 / 参考图处理、Browser Runtime 工作台外壳、环境预设子面板、资料管理子面板、Debug 面板外壳 / 状态 / 最小控制起步路径、Debug 审计与附着 Chrome 子面板、Debug CDP 托管会话高级区、existing session hook feedback / presentation 与 Site Adapter 面板 chrome / 执行反馈,`agent` namespace 已覆盖 Skills / CuratedTask / 能力草案等起步路径、Skills 仓库管理面板(已在记录 210 收口)、SkillCard 卡片 chrome(已在记录 212 收口)、内容检查弹窗(已在记录 213 收口)、WorkflowProgress 进度组件(已在记录 215 收口)与 SkillsPage 高级入口(已在记录 217 收口),`errors` namespace 已覆盖 Crash Recovery 外壳、Artifact ErrorFallbackRenderer 错误回退面板、Artifact SvgRenderer 预览控制 / 错误态、Artifact CodeRenderer 代码工具栏 / HTML 预览控制 / 错误态、Artifact HtmlRenderer 预览控制 / 错误态、Artifact ReactRenderer 预览 chrome / 编译错误态、Artifact MermaidRenderer 预览控制 / 导出控制 / 错误态与 crashDiagnostic 共享诊断摘要 / 复制失败 / 下载目录 fallback;Workspace 其他媒体 / Browser Runtime 其他子面板 / 工作台路径长尾与 Agent Chat 更深路径仍需 P1/P2 逐步迁移;Settings 共享壳层(侧边栏浮动导航 chrome 已在记录 144 收口,useSettingsCategory 导航分组 fallback 已在记录 187 收口,主布局 / Sidebar 壳层 fallback 已在记录 196 收口)、home(首屏总览 fallback 已在记录 197 收口)、developer-lab(合并入口 fallback 已在记录 188 收口)、system/developer(壳层与目录联调 fallback 已在记录 204 收口)、system/shared Clipboard 权限指引(ClipboardPermissionGuideCard fallback 已在记录 193 收口)、CrashRecoveryPanel 恢复模式外壳(已在记录 205 二次收口 UI fallback 与真实资源回归)、system/environment(主体 fallback 已在记录 202 收口)、system/experimental(主体 fallback 已在记录 201 收口,更新检查子面板二次 fallback 已在记录 208 收口)、system/web-search(壳层 / summary / tabs / sticky footer source fallback 与测试 mock fallback 已在记录 243 二次收口,Provider 凭证区 source fallback 已在记录 244 二次收口,MSE 聚合配置区 source fallback 已在记录 245 二次收口,图片搜索区 source fallback 已在记录 246 二次收口,观测面板配置区 source fallback 已在记录 247 二次收口,搜索链路字段 source fallback 已在记录 248 二次收口,页面用户可见 source fallback 已清零;console 诊断中文仅内部日志边界)、system/channels 工作台壳层、网关与隧道表单、Telegram / Feishu / 微信运行控制、微信兼容扫码排障 source fallback 与测试 mock 已在记录 257 二次收口(日志 Tail 面板 defaultValue fallback 已在记录 186 收口)、system/chrome-relay 独立连接引导窗口(已在记录 200 收口) / 主设置页全量 fallback(已在记录 203 收口)、system/shared Workspace 自愈记录共享卡片(WorkspaceRepairHistoryCard fallback 已在记录 195 收口)、system/automation 当前焦点条(AutomationJobFocusStrip fallback 已在记录 189 收口) / 概览焦点卡(AutomationOverviewFocusCard fallback 已在记录 191 收口) / HealthPanel(已在记录 192 收口) / AutomationJobDialog 弹窗(已在记录 211 收口) / AutomationJobDetailsDialog 详情弹窗与内嵌运行历史(已在记录 214 收口) / 主页面壳层与调度器设置 source fallback 二次收口已在记录 249(测试侧 legacy `react-i18next` mock 已在记录 256 收口为真实资源回归) / 开始模板与任务列表 source fallback 二次收口已在记录 250(测试侧 legacy `react-i18next` mock 已在记录 256 收口为真实资源回归) / overview tab raw literals 已在记录 251 收口 / 主页面运行历史区刷新失败 toast 已在记录 252 收口 / serviceSkill context helper 已在记录 253 二次收口 / presentation details helper 已在记录 254 二次收口 / SceneApp 动作 toast 与结果材料 missingPathMessage 已在记录 255 收口、general/memory(记忆总页 fallback 已在记录 209 收口,MemoryCuratedTaskSuggestionPanel source fallback 已在记录 266 收口,MemoryPage 首页 CuratedTask 推荐区 source fallback 已在记录 267 收口)、general/hotkeys(已审计快捷键页 fallback 已在记录 199 收口)、account/profile(字段卡 / 编辑动作 / 保存反馈已在记录 241 二次收口,hero / avatar / tags / usage / 上传错误长尾已在记录 242 二次收口,source fallback 与测试 mock fallback 已清零)、account/stats(统计页 defaultValue fallback 已在记录 184 收口,仍待更广设置页统一门禁)、user-center-session(已在记录 194 收口)、about(版本与更新区 fallback 已在记录 198 收口)、agent/skills 高级入口(SkillsPage 已在记录 217 收口,SkillsWorkspacePage source fallback 与测试 mock 已在记录 258 二次收口;installedSkillPresentation helper 独立中文默认 copy 已在记录 259 收口;CharacterMentionPanel / 当前 input capability 面板调用方 source fallback 与测试 mock 已在记录 260 二次收口;inputCapabilitySections.ts helper 内建默认 copy 已在记录 261 收口;service-skill helper 默认 copy 已在记录 264 收口;ServiceSkill launch prefill / creation replay 无 copy 调用面已在记录 269 收口;Runtime Scene Gate A2UI chrome / toast source fallback 已在记录 270 收口;sceneAppCuratedTaskReference helper UI copy 已在记录 271 收口(existing_results marker / baseline prompt / project_goal fallback 仍属 runtime 语义边界);curatedTaskTemplates.ts 定义层默认 copy 已在记录 263 收口,CuratedTask presentation helper 无 copy 调用面已在记录 265 收口)、agent/image-gen(媒体生成默认模型 fallback 已在记录 190 收口)、agent/video-gen(媒体生成默认模型 fallback 已在记录 190 收口)、agent/media-services(服务模型总页 fallback 已在记录 206 收口)、agent/voice(语音设置页 fallback 已在记录 207 收口)、agent/providers 的顶部切换器 / 云端反馈 / 左侧启用模型列表 / 导入导出弹窗(ImportExportDialog source fallback 已在记录 223 二次收口) / 详情配置外壳入口态 / 详情配置主体静态表单 / 删除确认与连接测试反馈 / 接口获取模型动态反馈 / 添加流程静态 chrome / 添加流程校验错误与动态反馈 / 添加流程目录辅助文案 / 添加流程内置模板名称与描述 / 添加流程 provider type label / 添加流程接口获取 helper 调用面审计 / 添加流程 runtime 数据边界审计 / 能力草案面板(CapabilityDraftPanel source fallback 已在记录 221 二次收口) / Workspace 已注册能力面板与 Agent envelope / Managed Job presentation / 桌宠能力偏好卡(source fallback 已在记录 240 二次收口) / Companion Bridge 诊断区已完成起步迁移,CuratedTask 模板定义与 Agent Chat / Skills / Home 结果模板主调用面已完成起步迁移;Agent Chat 会话过程索引面板(CanvasSessionOverviewPanel 已在记录 225 收口)、搜索结果预览列表(SearchResultPreviewList 已在记录 227 收口)、验证结果展示(HarnessVerificationSummarySection 已在记录 233 收口)、活跃 incident 面板(AgentIncidentPanel 已在记录 234 收口)、Agent Chat 输入能力区 section heading / banner / helper chrome、Skills 执行对话框 chrome / source fallback(chrome 已在记录 222 二次收口,CuratedTaskLauncherDialog source fallback 与测试 mock 已在记录 262 二次收口)、Skill 脚手架启动弹窗 chrome / source fallback(已在记录 220 二次收口)、Browser Runtime existing session hook feedback / presentation、Browser Runtime Debug 审计与附着 Chrome 子面板、Browser Runtime Debug CDP 托管会话高级区、Browser Site Adapter 面板 chrome / 执行反馈、SceneApp 最近判断建议横幅 runtime suggestion 与 General Workbench 最近判断建议横幅 runtime suggestion 已完成起步迁移,SceneAppExecutionSummaryCard UI chrome 已在记录 272 收口;GeneralWorkbenchContextPanel 上下文搜索 / 列表 / 详情 / 添加弹窗 chrome 已在记录 277 收口;ImageWorkbenchMessagePreview 图片任务消息卡 UI copy 已在记录 278 收口;Team Workspace 画布工具栏 / action buttons 已在记录 279 收口;GeneralWorkbenchWorkflowPanel 渲染层 UI chrome 已在记录 273 收口,outputs / activity / runDetail helper 已在记录 274 收口,branch / handoff / follow-up source fallback 已在记录 275 清零;其他启动弹窗 runtime suggestion、其他模型执行提示正文和运行时 suggestion 长尾仍需继续;CuratedTaskBadge review / sceneapp prefix 已在记录 268 收口;sceneAppCuratedTaskReference helper UI copy 已在记录 271 收口,SceneAppExecutionSummaryCard UI chrome 已在记录 272 收口,prompt / marker 语义保留为 compat/runtime 边界;Workspace Skill runtime enable 首发提示正文已在条目 102 收口,Skills 执行对话框 chrome 已在条目 103 收口,source fallback 已在记录 222 二次收口,Skill 脚手架启动弹窗 chrome 已在条目 105 收口,source fallback 已在记录 220 二次收口,Browser Runtime existing session hook feedback 已在条目 106 收口,Browser Runtime existing session presentation 已在条目 112 收口,SceneApp 最近判断建议横幅 runtime suggestion 已在条目 107 收口,Browser Site Adapter 面板 chrome / 执行反馈已在条目 108 收口,General Workbench 最近判断建议横幅 runtime suggestion 已在条目 109 收口,Browser Runtime Debug 审计与附着 Chrome 子面板已在条目 110 收口,Browser Runtime Debug CDP 托管会话高级区已在条目 111 收口。 +1. `common` namespace 已覆盖 App 根壳层、SplashScreen 启动画面(source fallback 已在记录 229 二次收口)、StartupLoadingScreen 启动加载屏(source fallback 已在记录 226 二次收口)、ConfirmDialog 共享确认弹窗(source fallback 已在记录 228 二次收口)、UpdateNotification 独立更新提醒窗口(source fallback 已在记录 218 二次收口)、SmartInput 独立悬浮输入窗(source fallback 已在记录 219 二次收口)、ShortcutSettings 共享快捷键设置组件(source fallback 已在记录 224 二次收口)、LatestRunStatusBadge 执行状态徽章(source fallback 已在记录 218 二次收口)、项目 / 工作区切换器(ProjectSelector source fallback 已在记录 216 二次收口)、新建项目弹窗(source fallback 已在记录 235 二次收口)、useProjects 重命名空名称错误(source fallback 已在记录 236 二次收口)、Deep Link 邀请 / 登录 / 支付 toast(source fallback 已在记录 237 二次收口)、App Startup Windows 启动自检 toast(source fallback 已在记录 238 二次收口)、OEM Cloud Access 登录 / OAuth / 支付反馈 / 会话刷新退出 / 邮箱验证码 / 密码登录 / Provider 默认服务 / API Key / 状态标签 / 浏览器预打开页与外部浏览器错误反馈(source fallback 已在记录 239 二次收口),以及 OEM Lime Hub provider sync 托管 Key 名称(source fallback 已在记录 239 二次收口;旧 `Lime 云端模型` 仅作为 legacy 本地 Key alias 数据匹配保留);`navigation` namespace 已覆盖侧边栏主入口、搜索弹窗、会话列表起步路径、会话 meta 格式化、会话重命名 / 删除的 prompt / confirm / toast、账号菜单核心未登录 / 已登录操作文案、邀请入口 / 邀请弹窗 / 复制邀请 toast,以及外观快捷面板;账号套餐 usage 已确认属于云端运行时字段边界,不再作为侧栏静态 i18n 缺口;`workspace` namespace 已覆盖 ArtifactToolbar 工具栏 chrome、ArtifactRenderer 统一入口空内容态 / 流式状态 / 未知类型回退、CanvasAdapter 图层设计预览 chrome、CanvasBreadcrumbHeader 返回按钮(source fallback 已在记录 230 收口)、Resource Manager PDF 预览器(source fallback 已在记录 231 收口)、Resource Manager 媒体播放预览器(source fallback 已在记录 232 收口)、DocumentRenderer 文档工具栏 chrome、ArtifactDocumentRenderer 结构化阅读面 chrome / block fallback、BrowserAssistRenderer 状态说明 / browserActionIndex 复盘、A2UI 提交完成提示 / 待处理表单状态 chrome(WorkspacePendingA2UIPanel source fallback 已在记录 276 二次收口)、图片素材 `ImageGallery`、Runtime AGENTS 引导 toast、视频 PromptInput(source fallback 已在记录 218 二次收口)、视频 VideoCanvas 插图反馈 / 侧栏按钮、视频 VideoSidebar 参数栏概览 / 创作 Tips / 参考图上传区 / 基础控制区 / 模型面板、视频 VideoWorkspace 首屏 / 摘要卡 / 最近任务标题 / 预览操作 / 任务状态 / 同步提示 / 会话主预览 / 聚焦任务面板 / 生成入口反馈 / 参考图处理、Browser Runtime 工作台外壳、环境预设子面板、资料管理子面板、Debug 面板外壳 / 状态 / 最小控制起步路径、Debug 审计与附着 Chrome 子面板、Debug CDP 托管会话高级区、existing session hook feedback / presentation 与 Site Adapter 面板 chrome / 执行反馈,`agent` namespace 已覆盖 Skills / CuratedTask / 能力草案等起步路径、Skills 仓库管理面板(已在记录 210 收口)、SkillCard 卡片 chrome(已在记录 212 收口)、内容检查弹窗(已在记录 213 收口)、WorkflowProgress 进度组件(已在记录 215 收口)与 SkillsPage 高级入口(已在记录 217 收口),`errors` namespace 已覆盖 Crash Recovery 外壳、Artifact ErrorFallbackRenderer 错误回退面板、Artifact SvgRenderer 预览控制 / 错误态、Artifact CodeRenderer 代码工具栏 / HTML 预览控制 / 错误态、Artifact HtmlRenderer 预览控制 / 错误态、Artifact ReactRenderer 预览 chrome / 编译错误态、Artifact MermaidRenderer 预览控制 / 导出控制 / 错误态与 crashDiagnostic 共享诊断摘要 / 复制失败 / 下载目录 fallback;Workspace 其他媒体 / Browser Runtime 其他子面板 / 工作台路径长尾与 Agent Chat 更深路径仍需 P1/P2 逐步迁移;Settings 共享壳层(侧边栏浮动导航 chrome 已在记录 144 收口,useSettingsCategory 导航分组 fallback 已在记录 187 收口,主布局 / Sidebar 壳层 fallback 已在记录 196 收口)、home(首屏总览 fallback 已在记录 197 收口)、developer-lab(合并入口 fallback 已在记录 188 收口)、system/developer(壳层与目录联调 fallback 已在记录 204 收口)、system/shared Clipboard 权限指引(ClipboardPermissionGuideCard fallback 已在记录 193 收口)、CrashRecoveryPanel 恢复模式外壳(已在记录 205 二次收口 UI fallback 与真实资源回归)、system/environment(主体 fallback 已在记录 202 收口)、system/experimental(主体 fallback 已在记录 201 收口,更新检查子面板二次 fallback 已在记录 208 收口)、system/web-search(壳层 / summary / tabs / sticky footer source fallback 与测试 mock fallback 已在记录 243 二次收口,Provider 凭证区 source fallback 已在记录 244 二次收口,MSE 聚合配置区 source fallback 已在记录 245 二次收口,图片搜索区 source fallback 已在记录 246 二次收口,观测面板配置区 source fallback 已在记录 247 二次收口,搜索链路字段 source fallback 已在记录 248 二次收口,页面用户可见 source fallback 已清零;console 诊断中文仅内部日志边界)、system/channels 工作台壳层、网关与隧道表单、Telegram / Feishu / 微信运行控制、微信兼容扫码排障 source fallback 与测试 mock 已在记录 257 二次收口(日志 Tail 面板 defaultValue fallback 已在记录 186 收口)、system/chrome-relay 独立连接引导窗口(已在记录 200 收口) / 主设置页全量 fallback(已在记录 203 收口)、system/shared Workspace 自愈记录共享卡片(WorkspaceRepairHistoryCard fallback 已在记录 195 收口)、system/automation 当前焦点条(AutomationJobFocusStrip fallback 已在记录 189 收口) / 概览焦点卡(AutomationOverviewFocusCard fallback 已在记录 191 收口) / HealthPanel(已在记录 192 收口) / AutomationJobDialog 弹窗(已在记录 211 收口) / AutomationJobDetailsDialog 详情弹窗与内嵌运行历史(已在记录 214 收口) / 主页面壳层与调度器设置 source fallback 二次收口已在记录 249(测试侧 legacy `react-i18next` mock 已在记录 256 收口为真实资源回归) / 开始模板与任务列表 source fallback 二次收口已在记录 250(测试侧 legacy `react-i18next` mock 已在记录 256 收口为真实资源回归) / overview tab raw literals 已在记录 251 收口 / 主页面运行历史区刷新失败 toast 已在记录 252 收口 / serviceSkill context helper 已在记录 253 二次收口 / presentation details helper 已在记录 254 二次收口 / SceneApp 动作 toast 与结果材料 missingPathMessage 已在记录 255 收口、general/memory(记忆总页 fallback 已在记录 209 收口,MemoryCuratedTaskSuggestionPanel source fallback 已在记录 266 收口,MemoryPage 首页 CuratedTask 推荐区 source fallback 已在记录 267 收口)、general/hotkeys(已审计快捷键页 fallback 已在记录 199 收口)、account/profile(字段卡 / 编辑动作 / 保存反馈已在记录 241 二次收口,hero / avatar / tags / usage / 上传错误长尾已在记录 242 二次收口,source fallback 与测试 mock fallback 已清零)、account/stats(统计页 defaultValue fallback 已在记录 184 收口,仍待更广设置页统一门禁)、user-center-session(已在记录 194 收口)、about(版本与更新区 fallback 已在记录 198 收口)、agent/skills 高级入口(SkillsPage 已在记录 217 收口,SkillsWorkspacePage source fallback 与测试 mock 已在记录 258 二次收口;installedSkillPresentation helper 独立中文默认 copy 已在记录 259 收口;CharacterMentionPanel / 当前 input capability 面板调用方 source fallback 与测试 mock 已在记录 260 二次收口;inputCapabilitySections.ts helper 内建默认 copy 已在记录 261 收口;service-skill helper 默认 copy 已在记录 264 收口;ServiceSkill launch prefill / creation replay 无 copy 调用面已在记录 269 收口;Runtime Scene Gate A2UI chrome / toast source fallback 已在记录 270 收口;sceneAppCuratedTaskReference helper UI copy 已在记录 271 收口(existing_results marker / baseline prompt / project_goal fallback 仍属 runtime 语义边界);curatedTaskTemplates.ts 定义层默认 copy 已在记录 263 收口,CuratedTask presentation helper 无 copy 调用面已在记录 265 收口)、agent/image-gen(媒体生成默认模型 fallback 已在记录 190 收口)、agent/video-gen(媒体生成默认模型 fallback 已在记录 190 收口)、agent/media-services(服务模型总页 fallback 已在记录 206 收口)、agent/voice(语音设置页 fallback 已在记录 207 收口)、agent/providers 的顶部切换器 / 云端反馈 / 左侧启用模型列表 / 导入导出弹窗(ImportExportDialog source fallback 已在记录 223 二次收口) / 详情配置外壳入口态 / 详情配置主体静态表单 / 删除确认与连接测试反馈 / 接口获取模型动态反馈 / 添加流程静态 chrome / 添加流程校验错误与动态反馈 / 添加流程目录辅助文案 / 添加流程内置模板名称与描述 / 添加流程 provider type label / 添加流程接口获取 helper 调用面审计 / 添加流程 runtime 数据边界审计 / 能力草案面板(CapabilityDraftPanel source fallback 已在记录 221 二次收口) / Workspace 已注册能力面板与 Agent envelope / Managed Job presentation / 桌宠能力偏好卡(source fallback 已在记录 240 二次收口) / Companion Bridge 诊断区已完成起步迁移,CuratedTask 模板定义与 Agent Chat / Skills / Home 结果模板主调用面已完成起步迁移;Agent Chat 会话过程索引面板(CanvasSessionOverviewPanel 已在记录 225 收口)、搜索结果预览列表(SearchResultPreviewList 已在记录 227 收口)、验证结果展示(HarnessVerificationSummarySection 已在记录 233 收口)、活跃 incident 面板(AgentIncidentPanel 已在记录 234 收口)、Agent Chat 输入能力区 section heading / banner / helper chrome、Skills 执行对话框 chrome / source fallback(chrome 已在记录 222 二次收口,CuratedTaskLauncherDialog source fallback 与测试 mock 已在记录 262 二次收口)、Skill 脚手架启动弹窗 chrome / source fallback(已在记录 220 二次收口)、Browser Runtime existing session hook feedback / presentation、Browser Runtime Debug 审计与附着 Chrome 子面板、Browser Runtime Debug CDP 托管会话高级区、Browser Site Adapter 面板 chrome / 执行反馈、SceneApp 最近判断建议横幅 runtime suggestion 与 General Workbench 最近判断建议横幅 runtime suggestion 已完成起步迁移,SceneAppExecutionSummaryCard UI chrome 已在记录 272 收口;GeneralWorkbenchContextPanel 上下文搜索 / 列表 / 详情 / 添加弹窗 chrome 已在记录 277 收口;ImageWorkbenchMessagePreview 图片任务消息卡 UI copy 已在记录 278 收口;Team Workspace 画布工具栏 / action buttons 已在记录 279 收口,session control toast 已在记录 280 收口,operation panel / selectors 已在记录 281 收口,formation selector / dock formation chrome 已在记录 282 收口,selected session inline chrome 已在记录 283 收口,board presentation / overview chrome 已在记录 284 收口,canvas lane selector / runtime hook 已在记录 285 收口,CanvasStage chrome 已在记录 286 收口,Dock current progress / concurrency chrome 已在记录 287 收口,selected session detail selector 已在记录 288 收口,board chrome selector 已在记录 289 收口,activity preview selector / sync hook 已在记录 290 收口,live runtime projector / runtime subscription 已在记录 291 收口;GeneralWorkbenchWorkflowPanel 渲染层 UI chrome 已在记录 273 收口,outputs / activity / runDetail helper 已在记录 274 收口,branch / handoff / follow-up source fallback 已在记录 275 清零;其他启动弹窗 runtime suggestion、其他模型执行提示正文和运行时 suggestion 长尾仍需继续;CuratedTaskBadge review / sceneapp prefix 已在记录 268 收口;sceneAppCuratedTaskReference helper UI copy 已在记录 271 收口,SceneAppExecutionSummaryCard UI chrome 已在记录 272 收口,prompt / marker 语义保留为 compat/runtime 边界;Workspace Skill runtime enable 首发提示正文已在条目 102 收口,Skills 执行对话框 chrome 已在条目 103 收口,source fallback 已在记录 222 二次收口,Skill 脚手架启动弹窗 chrome 已在条目 105 收口,source fallback 已在记录 220 二次收口,Browser Runtime existing session hook feedback 已在条目 106 收口,Browser Runtime existing session presentation 已在条目 112 收口,SceneApp 最近判断建议横幅 runtime suggestion 已在条目 107 收口,Browser Site Adapter 面板 chrome / 执行反馈已在条目 108 收口,General Workbench 最近判断建议横幅 runtime suggestion 已在条目 109 收口,Browser Runtime Debug 审计与附着 Chrome 子面板已在条目 110 收口,Browser Runtime Debug CDP 托管会话高级区已在条目 111 收口。 2. `legacy-patch/` 已完成物理隔离,并已有运行时命中量指标 API、GUI smoke metrics 导出链与离线 text / JSON 报告脚本;但 CI 尚未把 `i18n:patch-report --check` 接入删除门禁,也尚未把 `no-hit` 报告和 current 主路径依赖审计绑定为 DOM replacer 删除条件。退出条件仍是 P3 后按命中量和 current 主路径依赖清零逐步拆除。 3. 类型绑定已覆盖当前 source resource,`AGENTS.md` / 质量工作流也已封住新增功能必须走 current i18n 的人工规则;但尚未接入官方 `i18next-cli` 的抽取 / hardcoded string lint / type generation,也尚未评估 selector API 或 `@i18next-selector/codemod` 是否适合资源规模扩大后的 P3 阶段。 -4. 记录 279 已取得 JSON 解析、定向测试 / eslint / i18n 结构检测 / typecheck / `git diff --check` 与 Team Workspace toolbar / action buttons 两个目标组件中文 / `defaultValue` / `t(key, "中文")` fallback 清零扫描证据;本轮未重复执行 `npm run verify:gui-smoke`,继续沿用记录 274 的非 i18n Rust 脏改编译 blocker。记录 278 已取得 JSON 解析、定向测试 / eslint / i18n 结构检测 / typecheck / `git diff --check` 与 `ImageWorkbenchMessagePreview.tsx` 中文 / `defaultValue` / `t(key, "中文")` fallback 清零扫描证据;本轮未重复执行 `npm run verify:gui-smoke`,继续沿用记录 274 的非 i18n Rust 脏改编译 blocker。记录 277 已取得 JSON 解析、定向测试 / eslint / i18n 结构检测 / typecheck / `git diff --check` 与 `GeneralWorkbenchContextPanel.tsx` 中文 / `defaultValue` / `t(key, "中文")` fallback 清零扫描证据;本轮未重复执行 `npm run verify:gui-smoke`,继续沿用记录 274 的非 i18n Rust 脏改编译 blocker。记录 276 已取得定向测试 / eslint / i18n 结构检测 / typecheck / `git diff --check` 与 `WorkspacePendingA2UIPanel.tsx` 中文 / `defaultValue` / `t(key, "中文")` fallback 清零扫描证据;本轮未重复执行 `npm run verify:gui-smoke`,继续沿用记录 274 的非 i18n Rust 脏改编译 blocker。记录 275 已取得 JSON 解析、定向测试 / eslint / i18n 结构检测 / typecheck / `git diff --check` 与 `GeneralWorkbenchWorkflowPanel.tsx` 中文 / `defaultValue` / `t(key, "中文")` fallback 清零扫描证据;本轮未重复执行 `npm run verify:gui-smoke`,因为记录 274 已确认 GUI smoke 会被非 i18n Rust 脏改编译错误阻断。记录 274 已取得 JSON 解析、定向测试 / eslint / i18n 结构检测 / typecheck / `git diff --check` 通过证据;本轮 `npm run verify:gui-smoke` 被非 i18n Rust 脏改编译错误阻断,具体为 `src-tauri/src/services/runtime_evidence_pack_service.rs:694` 调用 `build_agent_runtime_profile_spine_json` 少传 `&[AgentRun]` 参数,签名变更位于 `src-tauri/src/services/runtime_agent_profile_projection_service.rs:15`,因此记录 274 不宣称 GUI smoke 通过。记录 273 已取得定向测试 / eslint / i18n 结构检测 / typecheck / `git diff --check` 与单独 `npm run verify:gui-smoke` 通过证据;`npm run verify:local` 本轮已通过 `verify:app-version`、lint、typecheck、全量 smart vitest 55 批与 Rust tests,但统一入口内 GUI smoke 的 `smoke:browser-runtime` 阶段曾因 `launch_browser_session 请求失败: fetch failed` 失败,因此不把记录 273 记为新的完整绿色统一入口。随后单独补跑的 GUI smoke 已覆盖 workspace-ready / browser-runtime / site-adapters / agent-service-skill-entry / agent-runtime-tool-surface / agent-runtime-tool-surface-page / claw-chat-ready-streaming / knowledge-gui / design-canvas,i18n Patch metrics 仍为 `no-hit`。记录 272 已重新取得最新 `npm run verify:local` 完整绿色统一入口,通过 `verify:app-version`、lint、typecheck、全量 smart vitest 55 批与 GUI smoke;GUI smoke 覆盖 workspace-ready / browser-runtime / site-adapters / agent-service-skill-entry / agent-runtime-tool-surface / agent-runtime-tool-surface-page / claw-chat-ready-streaming / knowledge-gui / design-canvas,i18n Patch metrics 仍为 `no-hit`,因此记录 125 的 `smoke:claw-chat-ready-streaming` blocker 不再代表当前最新状态。记录 105 已取得 `npm run verify:local` 与 GUI smoke 通过证据(日志 `.lime/qc/verify-local-2026-05-11-i18n-managed-job-initial-values.log`),并覆盖此前 Agent streaming runtime blocker;记录 125 已复跑 `npm run verify:local`,并通过 `verify:app-version`、`lint`、`typecheck`、全量 smart vitest、`test:contracts` 与 GUI smoke 的 workspace-ready / browser-runtime / site-adapters / agent-service-skill-entry / agent-runtime-tool-surface / agent-runtime-tool-surface-page 阶段;但当时 `smoke:claw-chat-ready-streaming` 仍以 `长 turn 在首个流式增量前结束: status=completed` 失败,单独复跑同样失败,因此在记录 125 当时,记录 109 / 110 / 111 / 112 / 113 / 114 / 115 / 116 / 117 / 118 / 119 / 120 / 121 / 122 / 123 / 124 / 125 之后仍没有完整绿色统一入口;当时最新工作区涉及 CuratedTask 模板定义 / Agent Chat 输入能力区、Skills runtime enable 首发提示正文、Skills 执行对话框 chrome、Skill 脚手架启动弹窗 chrome、SceneApp / General Workbench 最近判断建议横幅 runtime suggestion / 执行摘要卡、Workspace 图片素材、Browser Runtime 工作台外壳、环境预设子面板、资料管理子面板、Debug 面板外壳、Debug 审计与附着 Chrome 子面板、Debug CDP 托管会话高级区、existing session hook feedback / presentation 与 Site Adapter 面板迁移,当时主要取得定向测试、eslint、i18n、JSON 解析、typecheck、contracts 与大部分 GUI smoke 证据;记录 117 已补齐 `workspace.browserRuntimeDebug.*` typed resource 并重新取得 `npm run typecheck` 通过证据,记录 118 初次 typecheck 阻断已由记录 119 的 `workspace.browserExistingSession.feedback.*` 资源补齐与 `npm run typecheck` 通过证据覆盖,记录 120 的 `npm run typecheck` 被并行 Browser Site Adapter `workspace.browserSiteAdapter.*` typed key / defaultValue 错误阻断,该阻断已由记录 121 的 `workspace.browserSiteAdapter.*` 资源补齐与 `npm run typecheck` 通过证据覆盖;记录 122 的 `npm run typecheck` 被并行 `src/features/browser-runtime/BrowserRuntimeDebugPanel.tsx(582,33)` 的 `TS2554` 阻断,未作为 General Workbench 本刀通过证据;记录 123 已由 Browser Runtime Debug 审计与附着 Chrome 子面板迁移重新取得 `npm run typecheck` 通过证据;记录 124 继续取得 Browser Runtime Debug CDP 托管会话高级区的 `npm run typecheck` 通过证据;记录 131 提到的 `ExistingSessionAttachPanelCopy.presentation` 缺失已由记录 132 的 Browser Runtime existing session presentation 迁移与 `npm run typecheck` 通过证据覆盖;记录 269、270、271 与 272 已连续补齐 `npm run verify:local` / GUI smoke 当前证据,后续继续叠加改动时仍需基于最新 diff 复跑统一入口。`ChannelLogTailPanel.test.tsx` 的 `Timeout` 类型不匹配已在记录 49 收口,当前不再作为 i18n PRD 的 typecheck blocker;记录 50 / 51 / 52 / 53 / 54 / 55 / 56 / 57 / 58 的 typecheck 因并行重验证负载或本轮未单独启动而未取得通过证据,后续收口时需复跑;记录 59 / 60 / 61 已重新取得 `npm run typecheck` 通过证据;记录 65 / 66 因隔壁 `verify:local` 长跑占用未重复启动 typecheck;记录 67 / 68 已重新取得 `npm run typecheck` 通过证据;记录 69 / 71 / 72 / 73 / 74 未单独启动 typecheck / verify:local;记录 75 已尝试 `npm run typecheck` 但当时被 AutomationJobDialog 并行脏改阻塞,且隔壁 `verify:local` 在 `smoke:claw-chat-ready-streaming` 超时失败;记录 76 已重新取得 `npm run typecheck` 通过证据;记录 77 已尝试 `npm run typecheck` 但被 Memory 来源链并行脏改阻塞;记录 78 / 79 / 80 / 81 / 82 / 83 / 84 / 85 / 86 / 87 / 88 / 89 / 90 / 91 已重新取得 `npm run typecheck` 通过证据;记录 92 / 93 因并行改动下的 `MemoryPage.test.tsx` / `WorkspaceRegisteredSkillsPanel.tsx` 类型 blocker 未取得本轮 typecheck 通过证据;记录 94 / 95 / 96 / 97 / 98 / 99 / 101 / 103 / 105 已重新取得 `npm run typecheck` 通过证据;记录 70 已重新取得 `npm run typecheck` 通过证据;记录 173 已重新取得 `npm run typecheck` 通过证据,并覆盖记录 172 提到的 ArtifactToolbar typed key / children blocker;记录 174 已重新取得 `npm run typecheck` 通过证据;记录 176 / 177 / 178 已重新取得 `npm run typecheck` 通过证据;记录 179 的 `npm run typecheck` 被并行 `src/components/AppSidebar.test.tsx` / `src/components/settings-v2/account/profile/index.tsx` 脏改阻断,未作为本条通过证据;记录 180 / 181 / 182 已重新取得 `npm run typecheck` 通过证据。 +4. 记录 291 已取得 JSON 解析、定向测试 / eslint / i18n 结构检测 / typecheck / `git diff --check`、5 locale 关键 key 存在性检查与 Team Workspace live runtime projector / subscriptions 中文 / `defaultValue` / `t(key, "中文")` fallback 清零扫描证据;本轮未重复执行 `npm run verify:local` / GUI smoke,继续沿用记录 281 的非本轮 `src/lib/teamMemorySync.test.ts` `crypto.subtle.digest` undefined 统一入口 blocker 口径,未作为完整绿色统一入口。记录 290 已取得 JSON 解析、定向测试 / eslint / i18n 结构检测 / typecheck / `git diff --check`、5 locale 关键 key 存在性检查与 Team Workspace activity preview selector / sync hook 中文 / `defaultValue` / `t(key, "中文")` fallback 清零扫描证据;本轮未重复执行 `npm run verify:local` / GUI smoke,继续沿用记录 281 的非本轮 `src/lib/teamMemorySync.test.ts` `crypto.subtle.digest` undefined 统一入口 blocker 口径,未作为完整绿色统一入口。记录 289 已取得 JSON 解析、定向测试 / eslint / i18n 结构检测 / typecheck / `git diff --check`、5 locale 关键 key 存在性检查与 Team Workspace board chrome selector 中文 / `defaultValue` / `t(key, "中文")` fallback 清零扫描证据;本轮未重复执行 `npm run verify:local` / GUI smoke,继续沿用记录 281 的非本轮 `src/lib/teamMemorySync.test.ts` `crypto.subtle.digest` undefined 统一入口 blocker 口径,未作为完整绿色统一入口。记录 288 已取得 JSON 解析、定向测试 / eslint / i18n 结构检测 / typecheck / `git diff --check`、5 locale 关键 key 存在性检查与 Team Workspace selected session detail selector 中文 / `defaultValue` / `t(key, "中文")` fallback 清零扫描证据;本轮未重复执行 `npm run verify:local` / GUI smoke,继续沿用记录 281 的非本轮 `src/lib/teamMemorySync.test.ts` `crypto.subtle.digest` undefined 统一入口 blocker 口径,未作为完整绿色统一入口。记录 287 已取得 JSON 解析、定向测试 / eslint / i18n 结构检测 / typecheck / `git diff --check`、5 locale 关键 key 存在性检查与 Team Workspace Dock current progress / concurrency chrome 中文 / `defaultValue` / `t(key, "中文")` fallback 清零扫描证据;本轮未重复执行 `npm run verify:local` / GUI smoke,继续沿用记录 281 的非本轮 `src/lib/teamMemorySync.test.ts` `crypto.subtle.digest` undefined 统一入口 blocker 口径,未作为完整绿色统一入口。记录 286 已取得 JSON 解析、定向测试 / eslint / i18n 结构检测 / typecheck / `git diff --check`、5 locale 关键 key 存在性检查与 Team Workspace CanvasStage chrome 中文 / `defaultValue` / `t(key, "中文")` fallback 清零扫描证据;本轮未重复执行 `npm run verify:local` / GUI smoke,继续沿用记录 281 的非本轮 `src/lib/teamMemorySync.test.ts` `crypto.subtle.digest` undefined 统一入口 blocker 口径,未作为完整绿色统一入口。记录 285 已取得 JSON 解析、定向测试 / eslint / i18n 结构检测 / typecheck / `git diff --check`、5 locale 关键 key 存在性检查与 Team Workspace canvas lane selector / runtime hook 中文 / `defaultValue` / `t(key, "中文")` fallback 清零扫描证据;本轮未重复执行 `npm run verify:local` / GUI smoke,继续沿用记录 281 的非本轮 `src/lib/teamMemorySync.test.ts` `crypto.subtle.digest` undefined 统一入口 blocker 口径,未作为完整绿色统一入口。记录 284 已取得 JSON 解析、定向测试 / eslint / i18n 结构检测 / typecheck / `git diff --check`、5 locale 关键 key 存在性检查与 Team Workspace board presentation / overview chrome 中文 / `defaultValue` / `t(key, "中文")` fallback 清零扫描证据;本轮未重复执行 `npm run verify:local` / GUI smoke,继续沿用记录 281 的非本轮 `src/lib/teamMemorySync.test.ts` `crypto.subtle.digest` undefined 统一入口 blocker 口径,未作为完整绿色统一入口。记录 283 已取得 JSON 解析、定向测试 / eslint / i18n 结构检测 / typecheck / `git diff --check`、5 locale 关键 key 存在性检查与 Team Workspace selected session inline header / collaboration / activity / detail 中文 / `defaultValue` / `t(key, "中文")` fallback 清零扫描证据;本轮未重复执行 `npm run verify:local` / GUI smoke,继续沿用记录 281 的非本轮 `src/lib/teamMemorySync.test.ts` `crypto.subtle.digest` undefined 统一入口 blocker 口径,未作为完整绿色统一入口。记录 282 已取得 JSON 解析、定向测试 / eslint / i18n 结构检测 / typecheck / `git diff --check`、5 locale 关键 key 存在性检查与 Team Workspace formation selector / board formation hook 中文 / `defaultValue` / `t(key, "中文")` fallback 清零扫描证据,并确认 Dock / SummaryPanel 新增 `t()` 调用无第二参数 fallback、formation 相关目标硬编码清零;本轮未重复执行 `npm run verify:local` / GUI smoke,继续沿用记录 281 的非本轮 `src/lib/teamMemorySync.test.ts` `crypto.subtle.digest` undefined 统一入口 blocker 口径,未作为完整绿色统一入口。记录 281 已取得 JSON 解析、定向测试 / eslint / i18n 结构检测 / typecheck / `git diff --check`、5 locale 关键 key 存在性检查与 Team Workspace operation selectors / panel / operation state hook 中文 / `defaultValue` / `t(key, "中文")` fallback 清零扫描证据;本轮额外重跑 `npm run verify:local`,已通过 `verify:app-version`、`lint`、`typecheck` 与 smart vitest 1-10 批,在 11/55 批同样因非本轮 `src/lib/teamMemorySync.test.ts` 的 `crypto.subtle.digest` undefined 失败,未作为完整绿色统一入口。记录 280 已取得 JSON 解析、定向测试 / eslint / i18n 结构检测 / typecheck / `git diff --check` 与 Team Workspace session control hook 中文 / `defaultValue` / `t(key, "中文")` fallback 清零扫描证据;本轮 `npm run verify:local` 只推进到 smart vitest 12/55,因非本轮 `src/lib/teamMemorySync.test.ts` 的 `crypto.subtle.digest` 环境错误失败,未作为完整绿色统一入口。记录 279 已取得 JSON 解析、定向测试 / eslint / i18n 结构检测 / typecheck / `git diff --check` 与 Team Workspace toolbar / action buttons 两个目标组件中文 / `defaultValue` / `t(key, "中文")` fallback 清零扫描证据;本轮未重复执行 `npm run verify:gui-smoke`,继续沿用记录 274 的非 i18n Rust 脏改编译 blocker。记录 278 已取得 JSON 解析、定向测试 / eslint / i18n 结构检测 / typecheck / `git diff --check` 与 `ImageWorkbenchMessagePreview.tsx` 中文 / `defaultValue` / `t(key, "中文")` fallback 清零扫描证据;本轮未重复执行 `npm run verify:gui-smoke`,继续沿用记录 274 的非 i18n Rust 脏改编译 blocker。记录 277 已取得 JSON 解析、定向测试 / eslint / i18n 结构检测 / typecheck / `git diff --check` 与 `GeneralWorkbenchContextPanel.tsx` 中文 / `defaultValue` / `t(key, "中文")` fallback 清零扫描证据;本轮未重复执行 `npm run verify:gui-smoke`,继续沿用记录 274 的非 i18n Rust 脏改编译 blocker。记录 276 已取得定向测试 / eslint / i18n 结构检测 / typecheck / `git diff --check` 与 `WorkspacePendingA2UIPanel.tsx` 中文 / `defaultValue` / `t(key, "中文")` fallback 清零扫描证据;本轮未重复执行 `npm run verify:gui-smoke`,继续沿用记录 274 的非 i18n Rust 脏改编译 blocker。记录 275 已取得 JSON 解析、定向测试 / eslint / i18n 结构检测 / typecheck / `git diff --check` 与 `GeneralWorkbenchWorkflowPanel.tsx` 中文 / `defaultValue` / `t(key, "中文")` fallback 清零扫描证据;本轮未重复执行 `npm run verify:gui-smoke`,因为记录 274 已确认 GUI smoke 会被非 i18n Rust 脏改编译错误阻断。记录 274 已取得 JSON 解析、定向测试 / eslint / i18n 结构检测 / typecheck / `git diff --check` 通过证据;本轮 `npm run verify:gui-smoke` 被非 i18n Rust 脏改编译错误阻断,具体为 `src-tauri/src/services/runtime_evidence_pack_service.rs:694` 调用 `build_agent_runtime_profile_spine_json` 少传 `&[AgentRun]` 参数,签名变更位于 `src-tauri/src/services/runtime_agent_profile_projection_service.rs:15`,因此记录 274 不宣称 GUI smoke 通过。记录 273 已取得定向测试 / eslint / i18n 结构检测 / typecheck / `git diff --check` 与单独 `npm run verify:gui-smoke` 通过证据;`npm run verify:local` 本轮已通过 `verify:app-version`、lint、typecheck、全量 smart vitest 55 批与 Rust tests,但统一入口内 GUI smoke 的 `smoke:browser-runtime` 阶段曾因 `launch_browser_session 请求失败: fetch failed` 失败,因此不把记录 273 记为新的完整绿色统一入口。随后单独补跑的 GUI smoke 已覆盖 workspace-ready / browser-runtime / site-adapters / agent-service-skill-entry / agent-runtime-tool-surface / agent-runtime-tool-surface-page / claw-chat-ready-streaming / knowledge-gui / design-canvas,i18n Patch metrics 仍为 `no-hit`。记录 272 已重新取得最新 `npm run verify:local` 完整绿色统一入口,通过 `verify:app-version`、lint、typecheck、全量 smart vitest 55 批与 GUI smoke;GUI smoke 覆盖 workspace-ready / browser-runtime / site-adapters / agent-service-skill-entry / agent-runtime-tool-surface / agent-runtime-tool-surface-page / claw-chat-ready-streaming / knowledge-gui / design-canvas,i18n Patch metrics 仍为 `no-hit`,因此记录 125 的 `smoke:claw-chat-ready-streaming` blocker 不再代表当前最新状态。记录 105 已取得 `npm run verify:local` 与 GUI smoke 通过证据(日志 `.lime/qc/verify-local-2026-05-11-i18n-managed-job-initial-values.log`),并覆盖此前 Agent streaming runtime blocker;记录 125 已复跑 `npm run verify:local`,并通过 `verify:app-version`、`lint`、`typecheck`、全量 smart vitest、`test:contracts` 与 GUI smoke 的 workspace-ready / browser-runtime / site-adapters / agent-service-skill-entry / agent-runtime-tool-surface / agent-runtime-tool-surface-page 阶段;但当时 `smoke:claw-chat-ready-streaming` 仍以 `长 turn 在首个流式增量前结束: status=completed` 失败,单独复跑同样失败,因此在记录 125 当时,记录 109 / 110 / 111 / 112 / 113 / 114 / 115 / 116 / 117 / 118 / 119 / 120 / 121 / 122 / 123 / 124 / 125 之后仍没有完整绿色统一入口;当时最新工作区涉及 CuratedTask 模板定义 / Agent Chat 输入能力区、Skills runtime enable 首发提示正文、Skills 执行对话框 chrome、Skill 脚手架启动弹窗 chrome、SceneApp / General Workbench 最近判断建议横幅 runtime suggestion / 执行摘要卡、Workspace 图片素材、Browser Runtime 工作台外壳、环境预设子面板、资料管理子面板、Debug 面板外壳、Debug 审计与附着 Chrome 子面板、Debug CDP 托管会话高级区、existing session hook feedback / presentation 与 Site Adapter 面板迁移,当时主要取得定向测试、eslint、i18n、JSON 解析、typecheck、contracts 与大部分 GUI smoke 证据;记录 117 已补齐 `workspace.browserRuntimeDebug.*` typed resource 并重新取得 `npm run typecheck` 通过证据,记录 118 初次 typecheck 阻断已由记录 119 的 `workspace.browserExistingSession.feedback.*` 资源补齐与 `npm run typecheck` 通过证据覆盖,记录 120 的 `npm run typecheck` 被并行 Browser Site Adapter `workspace.browserSiteAdapter.*` typed key / defaultValue 错误阻断,该阻断已由记录 121 的 `workspace.browserSiteAdapter.*` 资源补齐与 `npm run typecheck` 通过证据覆盖;记录 122 的 `npm run typecheck` 被并行 `src/features/browser-runtime/BrowserRuntimeDebugPanel.tsx(582,33)` 的 `TS2554` 阻断,未作为 General Workbench 本刀通过证据;记录 123 已由 Browser Runtime Debug 审计与附着 Chrome 子面板迁移重新取得 `npm run typecheck` 通过证据;记录 124 继续取得 Browser Runtime Debug CDP 托管会话高级区的 `npm run typecheck` 通过证据;记录 131 提到的 `ExistingSessionAttachPanelCopy.presentation` 缺失已由记录 132 的 Browser Runtime existing session presentation 迁移与 `npm run typecheck` 通过证据覆盖;记录 269、270、271 与 272 已连续补齐 `npm run verify:local` / GUI smoke 当前证据,后续继续叠加改动时仍需基于最新 diff 复跑统一入口。`ChannelLogTailPanel.test.tsx` 的 `Timeout` 类型不匹配已在记录 49 收口,当前不再作为 i18n PRD 的 typecheck blocker;记录 50 / 51 / 52 / 53 / 54 / 55 / 56 / 57 / 58 的 typecheck 因并行重验证负载或本轮未单独启动而未取得通过证据,后续收口时需复跑;记录 59 / 60 / 61 已重新取得 `npm run typecheck` 通过证据;记录 65 / 66 因隔壁 `verify:local` 长跑占用未重复启动 typecheck;记录 67 / 68 已重新取得 `npm run typecheck` 通过证据;记录 69 / 71 / 72 / 73 / 74 未单独启动 typecheck / verify:local;记录 75 已尝试 `npm run typecheck` 但当时被 AutomationJobDialog 并行脏改阻塞,且隔壁 `verify:local` 在 `smoke:claw-chat-ready-streaming` 超时失败;记录 76 已重新取得 `npm run typecheck` 通过证据;记录 77 已尝试 `npm run typecheck` 但被 Memory 来源链并行脏改阻塞;记录 78 / 79 / 80 / 81 / 82 / 83 / 84 / 85 / 86 / 87 / 88 / 89 / 90 / 91 已重新取得 `npm run typecheck` 通过证据;记录 92 / 93 因并行改动下的 `MemoryPage.test.tsx` / `WorkspaceRegisteredSkillsPanel.tsx` 类型 blocker 未取得本轮 typecheck 通过证据;记录 94 / 95 / 96 / 97 / 98 / 99 / 101 / 103 / 105 已重新取得 `npm run typecheck` 通过证据;记录 70 已重新取得 `npm run typecheck` 通过证据;记录 173 已重新取得 `npm run typecheck` 通过证据,并覆盖记录 172 提到的 ArtifactToolbar typed key / children blocker;记录 174 已重新取得 `npm run typecheck` 通过证据;记录 176 / 177 / 178 已重新取得 `npm run typecheck` 通过证据;记录 179 的 `npm run typecheck` 被并行 `src/components/AppSidebar.test.tsx` / `src/components/settings-v2/account/profile/index.tsx` 脏改阻断,未作为本条通过证据;记录 180 / 181 / 182 已重新取得 `npm run typecheck` 通过证据。 diff --git a/package-lock.json b/package-lock.json index 2123519fe..2c889d81e 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "lime", - "version": "1.36.0", + "version": "1.37.0", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "lime", - "version": "1.36.0", + "version": "1.37.0", "license": "GPL-3.0-only", "dependencies": { "@babel/standalone": "^7.29.0", diff --git a/package.json b/package.json index 65b4b3584..e9f772693 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,7 @@ { "name": "lime", "private": true, - "version": "1.36.0", + "version": "1.37.0", "description": "AI content workspace for Chinese creators: desktop writing, research, prompt management, knowledge base, and multi-model workflows.", "keywords": [ "ai", diff --git a/packages/lime-cli-npm/package.json b/packages/lime-cli-npm/package.json index 2d65dbd4e..f4626a905 100644 --- a/packages/lime-cli-npm/package.json +++ b/packages/lime-cli-npm/package.json @@ -1,6 +1,6 @@ { "name": "@limecloud/lime-cli", - "version": "1.36.0", + "version": "1.37.0", "description": "Lime 官方任务 CLI", "bin": { "lime": "scripts/run.js" diff --git a/scripts/agentui-ttft-live-sample.mjs b/scripts/agentui-ttft-live-sample.mjs new file mode 100644 index 000000000..f102ac242 --- /dev/null +++ b/scripts/agentui-ttft-live-sample.mjs @@ -0,0 +1,547 @@ +#!/usr/bin/env node + +import fs from "node:fs"; +import path from "node:path"; +import process from "node:process"; + +const DEFAULT_HEALTH_URL = "http://127.0.0.1:3030/health"; +const DEFAULT_INVOKE_URL = "http://127.0.0.1:3030/invoke"; +const DEFAULT_TIMEOUT_MS = 120_000; +const DEFAULT_INTERVAL_MS = 1_000; +const DEFAULT_MESSAGE = "Reply with exactly: OK"; +const DEFAULT_MODE = "responsive-auto"; +const MAX_SAMPLES_PER_RUN = 6; +const TERMINAL_RUN_STATUSES = new Set([ + "success", + "error", + "failed", + "timeout", + "cancelled", + "canceled", +]); + +function printHelp() { + console.log(` +Lime AgentUI TTFT Live Sample + +用途: + 通过 DevBridge 提交真实 Agent Runtime 回合,采集脱敏 routing / TTFT 证据。 + 默认走 responsive_chat 自动路由,不输出 prompt、assistant 正文、error_message、密钥或 run id。 + +用法: + node scripts/agentui-ttft-live-sample.mjs + node scripts/agentui-ttft-live-sample.mjs --samples 3 --output /tmp/agentui-ttft-live-sample.json + +选项: + --mode responsive-auto|request-override + 默认 responsive-auto;request-override 仅用于对照样本 + --provider-preference ID + request-override 模式下的 provider + --model-preference ID + request-override 模式下的 model + --samples N 本次采样条数,默认 1,单次最多 ${MAX_SAMPLES_PER_RUN} + --message TEXT 提交给模型的最短测试消息;不会写入脚本输出,默认 ${DEFAULT_MESSAGE} + --output PATH 写入脱敏 JSON;默认 stdout + --health-url URL DevBridge health 地址,默认 ${DEFAULT_HEALTH_URL} + --invoke-url URL DevBridge invoke 地址,默认 ${DEFAULT_INVOKE_URL} + --timeout-ms MS 单条样本等待超时,默认 ${DEFAULT_TIMEOUT_MS} + --interval-ms MS 轮询间隔,默认 ${DEFAULT_INTERVAL_MS} + -h, --help 显示帮助 +`); +} + +function parseArgs(argv) { + const options = { + healthUrl: DEFAULT_HEALTH_URL, + invokeUrl: DEFAULT_INVOKE_URL, + intervalMs: DEFAULT_INTERVAL_MS, + message: DEFAULT_MESSAGE, + mode: DEFAULT_MODE, + modelPreference: "", + outputPath: "", + providerPreference: "", + samples: 1, + timeoutMs: DEFAULT_TIMEOUT_MS, + }; + + for (let index = 0; index < argv.length; index += 1) { + const arg = argv[index]; + if (arg === "--help" || arg === "-h") { + printHelp(); + process.exit(0); + } + if (arg === "--health-url" && argv[index + 1]) { + options.healthUrl = String(argv[index + 1]).trim(); + index += 1; + continue; + } + if (arg === "--invoke-url" && argv[index + 1]) { + options.invokeUrl = String(argv[index + 1]).trim(); + index += 1; + continue; + } + if (arg === "--timeout-ms" && argv[index + 1]) { + options.timeoutMs = Number(String(argv[index + 1]).trim()); + index += 1; + continue; + } + if (arg === "--interval-ms" && argv[index + 1]) { + options.intervalMs = Number(String(argv[index + 1]).trim()); + index += 1; + continue; + } + if (arg === "--samples" && argv[index + 1]) { + options.samples = Number.parseInt(String(argv[index + 1]).trim(), 10); + index += 1; + continue; + } + if (arg === "--mode" && argv[index + 1]) { + options.mode = String(argv[index + 1]).trim(); + index += 1; + continue; + } + if (arg === "--message" && argv[index + 1]) { + options.message = String(argv[index + 1]); + index += 1; + continue; + } + if (arg === "--output" && argv[index + 1]) { + options.outputPath = path.resolve(String(argv[index + 1]).trim()); + index += 1; + continue; + } + if (arg === "--provider-preference" && argv[index + 1]) { + options.providerPreference = String(argv[index + 1]).trim(); + index += 1; + continue; + } + if (arg === "--model-preference" && argv[index + 1]) { + options.modelPreference = String(argv[index + 1]).trim(); + index += 1; + } + } + + if (!["responsive-auto", "request-override"].includes(options.mode)) { + throw new Error("--mode 只能是 responsive-auto 或 request-override"); + } + if (!Number.isFinite(options.timeoutMs) || options.timeoutMs < 10_000) { + throw new Error("--timeout-ms 必须是 >= 10000 的数字"); + } + if (!Number.isFinite(options.intervalMs) || options.intervalMs < 100) { + throw new Error("--interval-ms 必须是 >= 100 的数字"); + } + if (!Number.isFinite(options.samples) || options.samples < 1) { + throw new Error("--samples 必须是 >= 1 的数字"); + } + options.samples = Math.min(Math.floor(options.samples), MAX_SAMPLES_PER_RUN); + if (options.mode === "request-override") { + if (!options.providerPreference || !options.modelPreference) { + throw new Error( + "request-override 模式必须同时提供 --provider-preference 和 --model-preference", + ); + } + } + + return options; +} + +function sleep(ms) { + return new Promise((resolve) => setTimeout(resolve, ms)); +} + +function asRecord(value) { + return value && typeof value === "object" && !Array.isArray(value) + ? value + : null; +} + +function stringField(record, keys) { + const source = asRecord(record); + if (!source) { + return null; + } + for (const key of keys) { + const value = source[key]; + if (typeof value === "string" && value.trim()) { + return value.trim(); + } + } + return null; +} + +function numberField(record, keys) { + const source = asRecord(record); + if (!source) { + return null; + } + for (const key of keys) { + const value = source[key]; + if (typeof value === "number" && Number.isFinite(value)) { + return value; + } + if (typeof value === "string" && value.trim()) { + const parsed = Number(value); + if (Number.isFinite(parsed)) { + return parsed; + } + } + } + return null; +} + +function stringArrayField(record, keys) { + const source = asRecord(record); + if (!source) { + return []; + } + for (const key of keys) { + const value = source[key]; + if (Array.isArray(value)) { + return value.filter((item) => typeof item === "string" && item.trim()); + } + } + return []; +} + +async function waitForHealth(options) { + const startedAt = Date.now(); + let lastError = null; + + while (Date.now() - startedAt < options.timeoutMs) { + try { + const response = await fetch(options.healthUrl, { + method: "GET", + signal: AbortSignal.timeout(Math.min(options.intervalMs, 5_000)), + }); + const text = await response.text(); + if (!response.ok) { + throw new Error(`HTTP ${response.status}: ${response.statusText}`); + } + return text ? JSON.parse(text) : {}; + } catch (error) { + lastError = error; + await sleep(options.intervalMs); + } + } + + throw new Error( + `DevBridge health timeout: ${ + lastError instanceof Error ? lastError.message : String(lastError) + }`, + ); +} + +async function fetchWithHardTimeout(url, init, timeoutMs, label) { + const controller = new AbortController(); + let rejectTimeout; + const abortTimeout = setTimeout(() => controller.abort(), timeoutMs); + const timeoutPromise = new Promise((_, reject) => { + rejectTimeout = setTimeout(() => { + reject(new Error(`${label} timeout after ${timeoutMs}ms`)); + }, timeoutMs + 100); + }); + try { + return await Promise.race([ + fetch(url, { ...init, signal: controller.signal }), + timeoutPromise, + ]); + } finally { + clearTimeout(abortTimeout); + clearTimeout(rejectTimeout); + } +} + +async function invoke(options, cmd, args, timeoutMs = options.timeoutMs) { + const response = await fetchWithHardTimeout( + options.invokeUrl, + { + method: "POST", + headers: { + "content-type": "application/json", + }, + body: JSON.stringify({ cmd, args }), + }, + timeoutMs, + cmd, + ); + const text = await response.text(); + if (!response.ok) { + throw new Error(`${cmd} HTTP ${response.status}`); + } + const payload = text ? JSON.parse(text) : null; + if (payload?.error) { + throw new Error(`${cmd} error (redacted)`); + } + return payload?.result; +} + +function buildFastResponseMetadata() { + return { + harness: { + fast_response_routing: { + mode: "auto", + label: "快速响应", + reason: "first-turn-short-prompt", + service_model_slot: "responsive_chat", + routing_slot: "responsive_chat_model", + routing_changed: false, + resolver: "backend_service_model", + runtime_status_presentation: "transient", + }, + }, + }; +} + +function buildTurnConfig(options) { + const turnConfig = { + metadata: buildFastResponseMetadata(), + search_mode: "disabled", + web_search: false, + thinking_enabled: false, + }; + + if (options.mode === "request-override") { + turnConfig.provider_preference = options.providerPreference; + turnConfig.model_preference = options.modelPreference; + } + + return turnConfig; +} + +function latestTurnStatus(threadRead) { + const diagnostics = asRecord(threadRead?.diagnostics); + const runtimeSummary = asRecord( + threadRead?.runtime_summary ?? threadRead?.runtimeSummary, + ); + return ( + stringField(diagnostics, ["latest_turn_status", "latestTurnStatus"]) || + stringField(runtimeSummary, ["latestTurnStatus", "latest_turn_status"]) || + stringField(threadRead, ["status"]) + ); +} + +function summarizeThreadRead(threadRead) { + const modelRouting = asRecord( + threadRead?.model_routing ?? threadRead?.modelRouting, + ); + const latestTiming = asRecord( + modelRouting?.latestModelDeltaTiming ?? + modelRouting?.latest_model_delta_timing, + ); + const latestRouting = asRecord(latestTiming?.routing); + const runtimeSummary = asRecord( + threadRead?.runtime_summary ?? threadRead?.runtimeSummary, + ); + + const routing = { + decisionSource: + stringField(modelRouting, ["decisionSource", "decision_source"]) || + stringField(latestRouting, ["decisionSource", "decision_source"]) || + stringField(runtimeSummary, ["decisionSource", "decision_source"]), + settingsSource: + stringField(modelRouting, ["settingsSource", "settings_source"]) || + stringField(latestRouting, ["settingsSource", "settings_source"]), + serviceModelSlot: + stringField(modelRouting, ["serviceModelSlot", "service_model_slot"]) || + stringField(latestRouting, ["serviceModelSlot", "service_model_slot"]), + selectedProvider: + stringField(modelRouting, ["selectedProvider", "selected_provider"]) || + stringField(latestRouting, ["selectedProvider", "selected_provider"]), + selectedModel: + stringField(modelRouting, ["selectedModel", "selected_model"]) || + stringField(latestRouting, ["selectedModel", "selected_model"]), + decisionReason: + stringField(modelRouting, ["decisionReason", "decision_reason"]) || + stringField(latestRouting, ["decisionReason", "decision_reason"]), + fallbackChain: [ + ...new Set([ + ...stringArrayField(modelRouting, ["fallbackChain", "fallback_chain"]), + ...stringArrayField(latestRouting, ["fallbackChain", "fallback_chain"]), + ]), + ], + }; + + const timing = { + source: stringField(latestTiming, ["source"]), + runStatus: stringField(latestTiming, ["runStatus", "run_status"]), + durationMs: numberField(latestTiming, ["durationMs", "duration_ms"]), + firstVisibleDeltaMs: numberField(latestTiming, [ + "firstVisibleDeltaMs", + "first_visible_delta_ms", + ]), + firstThinkingDeltaMs: numberField(latestTiming, [ + "firstThinkingDeltaMs", + "first_thinking_delta_ms", + ]), + firstTextDeltaMs: numberField(latestTiming, [ + "firstTextDeltaMs", + "first_text_delta_ms", + ]), + }; + + return { + status: { + threadStatus: stringField(threadRead, ["status"]), + latestTurnStatus: latestTurnStatus(threadRead), + latestRunStatus: timing.runStatus, + }, + routing, + timing, + assertions: { + hasFirstTextDelta: timing.firstTextDeltaMs !== null, + hasRoutingEvidence: Boolean( + routing.decisionSource || + routing.selectedProvider || + routing.selectedModel || + routing.decisionReason, + ), + isResponsiveChatAuto: routing.decisionSource === "responsive_chat_auto", + }, + }; +} + +function isTerminalSample(summary) { + if (summary.assertions.hasFirstTextDelta) { + return true; + } + if (TERMINAL_RUN_STATUSES.has(String(summary.status.latestRunStatus || ""))) { + return true; + } + if (TERMINAL_RUN_STATUSES.has(String(summary.status.latestTurnStatus || ""))) { + return true; + } + return false; +} + +async function waitForSampleSummary(options, sessionId) { + const startedAt = Date.now(); + let lastSummary = null; + + while (Date.now() - startedAt < options.timeoutMs) { + const threadRead = await invoke(options, "agent_runtime_get_thread_read", { + sessionId, + }); + lastSummary = summarizeThreadRead(threadRead); + if (isTerminalSample(lastSummary)) { + return lastSummary; + } + await sleep(options.intervalMs); + } + + return { + ...(lastSummary || summarizeThreadRead(null)), + timedOut: true, + }; +} + +async function runSample(options, workspaceId, sampleIndex) { + const stamp = `${Date.now()}-${process.pid}-${sampleIndex}`; + const sessionId = await invoke(options, "agent_runtime_create_session", { + workspaceId, + name: `AgentUI TTFT live sample ${options.mode} ${stamp}`, + runStartHooks: false, + }); + const turnId = `agentui-ttft-${options.mode}-${stamp}`; + const eventName = `aster_stream_${sessionId}_${turnId}`; + + await invoke(options, "agent_runtime_submit_turn", { + request: { + message: options.message, + session_id: sessionId, + workspace_id: workspaceId, + event_name: eventName, + turn_id: turnId, + turn_config: buildTurnConfig(options), + skip_pre_submit_resume: true, + }, + }); + + const summary = await waitForSampleSummary(options, sessionId); + return { + sampleIndex, + sessionId, + turnId, + ...summary, + }; +} + +function buildSummary(samples) { + return { + samples: samples.length, + firstTextSamples: samples.filter( + (sample) => sample.assertions?.hasFirstTextDelta, + ).length, + responsiveAutoSamples: samples.filter( + (sample) => sample.assertions?.isResponsiveChatAuto, + ).length, + routingEvidenceSamples: samples.filter( + (sample) => sample.assertions?.hasRoutingEvidence, + ).length, + selectedGroups: samples.map((sample) => ({ + decisionSource: sample.routing?.decisionSource || null, + provider: sample.routing?.selectedProvider || null, + model: sample.routing?.selectedModel || null, + firstTextDeltaMs: sample.timing?.firstTextDeltaMs ?? null, + runStatus: sample.status?.latestRunStatus || null, + })), + }; +} + +function writeOutput(options, payload) { + const text = `${JSON.stringify(payload, null, 2)}\n`; + if (!options.outputPath) { + process.stdout.write(text); + return; + } + fs.mkdirSync(path.dirname(options.outputPath), { recursive: true }); + fs.writeFileSync(options.outputPath, text); +} + +async function main() { + const options = parseArgs(process.argv.slice(2)); + const health = await waitForHealth(options); + const workspace = await invoke(options, "get_or_create_default_project", {}); + const workspaceId = workspace?.id; + if (!workspaceId) { + throw new Error("get_or_create_default_project 缺少 workspace id"); + } + + const samples = []; + for (let index = 0; index < options.samples; index += 1) { + samples.push(await runSample(options, workspaceId, index + 1)); + } + + writeOutput(options, { + schemaVersion: "v1", + generatedAt: new Date().toISOString(), + mode: options.mode, + health: { + status: health?.status || null, + service: health?.service || null, + version: health?.version || null, + }, + workspaceId, + summary: buildSummary(samples), + samples, + privacy: { + exportedFields: [ + "session/turn id", + "routing decision", + "first visible/thinking/text TTFT", + "status summary", + ], + omittedFields: [ + "prompt", + "assistant response", + "error_message", + "secrets", + "run id", + ], + }, + }); +} + +main().catch((error) => { + console.error(error instanceof Error ? error.message : String(error)); + process.exit(1); +}); diff --git a/scripts/agentui-ttft-sample-matrix.mjs b/scripts/agentui-ttft-sample-matrix.mjs index 8b78441a0..79c9df36f 100644 --- a/scripts/agentui-ttft-sample-matrix.mjs +++ b/scripts/agentui-ttft-sample-matrix.mjs @@ -8,15 +8,48 @@ import process from "node:process"; const DEFAULT_LIMIT_RUNS = 5000; const DEFAULT_LIMIT_GROUPS = 24; +const DEFAULT_MIN_FIRST_TEXT_PER_RESPONSIVE_GROUP = 3; +const MATRIX_PRESETS = { + "agentui-responsive-chat-ttft": { + minFirstTextPerResponsiveGroup: 3, + requiredResponsiveModels: ["deepseek-v4-flash", "MiniMax-M2.7"], + requiredResponsiveProviders: ["siliconflow-cn", "openrouter", "lime-hub"], + }, +}; + +function applyPreset(result, presetName) { + const preset = MATRIX_PRESETS[presetName]; + if (!preset) { + throw new Error( + `Unknown preset: ${presetName}. Available presets: ${Object.keys(MATRIX_PRESETS).join(", ")}`, + ); + } + result.checkResponsiveMatrix = true; + result.matrixPreset = presetName; + result.minFirstTextPerResponsiveGroup = preset.minFirstTextPerResponsiveGroup; + result.requiredResponsiveModels = [...new Set([ + ...result.requiredResponsiveModels, + ...preset.requiredResponsiveModels, + ])]; + result.requiredResponsiveProviders = [...new Set([ + ...result.requiredResponsiveProviders, + ...preset.requiredResponsiveProviders, + ])]; +} function parseArgs(argv) { const result = { dbPath: defaultLimeDbPath(), format: "markdown", help: false, + checkResponsiveMatrix: false, limitGroups: DEFAULT_LIMIT_GROUPS, limitRuns: DEFAULT_LIMIT_RUNS, + minFirstTextPerResponsiveGroup: DEFAULT_MIN_FIRST_TEXT_PER_RESPONSIVE_GROUP, + matrixPreset: "", outputPath: "", + requiredResponsiveModels: [], + requiredResponsiveProviders: [], }; for (let index = 0; index < argv.length; index += 1) { @@ -41,6 +74,30 @@ function parseArgs(argv) { index += 1; continue; } + if (arg === "--min-first-text-per-responsive-group" && argv[index + 1]) { + result.minFirstTextPerResponsiveGroup = Number.parseInt(String(argv[index + 1]), 10); + index += 1; + continue; + } + if (arg === "--check-responsive-matrix") { + result.checkResponsiveMatrix = true; + continue; + } + if (arg === "--preset" && argv[index + 1]) { + applyPreset(result, String(argv[index + 1]).trim()); + index += 1; + continue; + } + if (arg === "--require-responsive-provider" && argv[index + 1]) { + result.requiredResponsiveProviders.push(String(argv[index + 1]).trim()); + index += 1; + continue; + } + if (arg === "--require-responsive-model" && argv[index + 1]) { + result.requiredResponsiveModels.push(String(argv[index + 1]).trim()); + index += 1; + continue; + } if (arg === "--output" && argv[index + 1]) { result.outputPath = String(argv[index + 1]).trim(); index += 1; @@ -68,6 +125,15 @@ Lime AgentUI TTFT Sample Matrix --format FMT markdown | json,默认 markdown --limit-runs N 最近 agent_runs 扫描上限,默认 ${DEFAULT_LIMIT_RUNS} --limit-groups N Markdown 表格输出的 group 上限,默认 ${DEFAULT_LIMIT_GROUPS} + --check-responsive-matrix + 检查 responsive_chat latency group 是否满足 first-text TTFT 样本下限 + --preset NAME 套用检查预设:${Object.keys(MATRIX_PRESETS).join(" | ")} + --min-first-text-per-responsive-group N + 每个 responsive_chat latency group 的 first-text TTFT 样本下限,默认 ${DEFAULT_MIN_FIRST_TEXT_PER_RESPONSIVE_GROUP} + --require-responsive-provider PROVIDER + 必须出现在 responsive_chat latency group 中的 provider;可重复 + --require-responsive-model MODEL + 必须出现在 responsive_chat latency group 中的 model;可重复 --output PATH 写入文件;默认 stdout -h, --help 显示帮助 @@ -95,6 +161,10 @@ function normalizePositiveInt(value, fallback) { return Number.isFinite(value) && value > 0 ? Math.floor(value) : fallback; } +function normalizeNonNegativeInt(value, fallback) { + return Number.isFinite(value) && value >= 0 ? Math.floor(value) : fallback; +} + function abbreviateHomePath(filePath) { const home = os.homedir(); return filePath.startsWith(home) ? `~${filePath.slice(home.length)}` : filePath; @@ -156,10 +226,22 @@ function numberValue(value) { return null; } +function saneFirstTextValue(value) { + const number = numberValue(value); + if (number === null || number < 0 || number > 600_000) { + return null; + } + return number; +} + function stringValue(value) { return typeof value === "string" && value.trim() ? value.trim() : null; } +function normalizeRoutingToken(value) { + return stringValue(value)?.toLowerCase().replaceAll("-", "_") || ""; +} + function redactProvider(provider) { const normalized = stringValue(provider); if (!normalized) { @@ -242,6 +324,9 @@ function parseRouting(metadata) { : Array.isArray(routingDecision?.fallback_chain) ? routingDecision.fallback_chain : [], + settingsSource: stringValue( + firstDefined(routingDecision?.settingsSource, routingDecision?.settings_source), + ), selectedModel, selectedProvider, serviceModelSlot: stringValue( @@ -250,20 +335,38 @@ function parseRouting(metadata) { }; } +function isResponsiveChatLatencyRun(routing) { + return ( + normalizeRoutingToken(routing.decisionSource) === "responsive_chat_auto" || + normalizeRoutingToken(routing.serviceModelSlot) === "responsive_chat" || + normalizeRoutingToken(routing.settingsSource) === "service_models.responsive_chat" + ); +} + function parseRun(row) { const metadata = parseJsonObject(row.metadata) || {}; const routing = parseRouting(metadata); const provider = redactProvider(routing.selectedProvider) || "(unknown)"; const model = routing.selectedModel || "(unknown)"; const decisionSource = routing.decisionSource || "(unknown)"; + const metadataFirstTextMs = saneFirstTextValue( + firstDefined(metadata.model_first_text_delta_ms, metadata.modelFirstTextDeltaMs), + ); + const timelineFirstTextMs = + row.status === "success" ? saneFirstTextValue(row.timeline_first_text_delta_ms) : null; + const firstTextMs = metadataFirstTextMs ?? timelineFirstTextMs; return { createdAt: row.created_at || null, decisionSource, durationMs: numberValue(row.duration_ms), - firstTextMs: numberValue( - firstDefined(metadata.model_first_text_delta_ms, metadata.modelFirstTextDeltaMs), - ), + firstTextMs, + firstTextSource: + metadataFirstTextMs !== null + ? "agent_runs.metadata" + : timelineFirstTextMs !== null + ? "thread_items.timeline" + : null, firstThinkingMs: numberValue( firstDefined(metadata.model_first_thinking_delta_ms, metadata.modelFirstThinkingDeltaMs), ), @@ -274,6 +377,7 @@ function parseRun(row) { model, provider, routing, + responsiveLatencyEligible: isResponsiveChatLatencyRun(routing), status: row.status || "(unknown)", }; } @@ -321,6 +425,10 @@ function buildGroupKey(run) { return `${run.decisionSource}\u0000${run.provider}\u0000${run.model}`; } +function buildResponsiveGroupKey(run) { + return `${run.provider}\u0000${run.model}`; +} + function buildReport({ dbPath, generatedAt = new Date().toISOString(), limitRuns }) { if (!fs.existsSync(dbPath)) { throw new Error(`DB not found: ${dbPath}`); @@ -329,7 +437,31 @@ function buildReport({ dbPath, generatedAt = new Date().toISOString(), limitRuns const safeLimit = normalizePositiveInt(limitRuns, DEFAULT_LIMIT_RUNS); const rows = sqliteJson( dbPath, - `SELECT id, status, duration_ms, metadata, created_at + `SELECT id, + status, + duration_ms, + metadata, + created_at, + session_id, + ( + SELECT CAST((julianday(agent_item.started_at) - julianday(user_item.started_at)) * 86400000 AS INTEGER) + FROM agent_thread_items AS user_item + JOIN agent_thread_items AS agent_item + ON agent_item.session_id = user_item.session_id + AND agent_item.item_type = 'agent_message' + AND ( + json_extract(agent_runs.metadata, '$.turn_state.turn_id') IS NULL + OR agent_item.turn_id = json_extract(agent_runs.metadata, '$.turn_state.turn_id') + ) + WHERE user_item.session_id = agent_runs.session_id + AND user_item.item_type = 'user_message' + AND ( + json_extract(agent_runs.metadata, '$.turn_state.turn_id') IS NULL + OR user_item.turn_id = json_extract(agent_runs.metadata, '$.turn_state.turn_id') + ) + ORDER BY user_item.sequence ASC, agent_item.sequence ASC + LIMIT 1 + ) AS timeline_first_text_delta_ms FROM agent_runs WHERE metadata IS NOT NULL ORDER BY created_at DESC @@ -337,6 +469,7 @@ function buildReport({ dbPath, generatedAt = new Date().toISOString(), limitRuns ); const runs = rows.map(parseRun); const groups = new Map(); + const responsiveLatencyGroups = new Map(); for (const run of runs) { const key = buildGroupKey(run); @@ -346,6 +479,7 @@ function buildReport({ dbPath, generatedAt = new Date().toISOString(), limitRuns durationStats: null, firstTextStats: null, firstTextCount: 0, + firstTextSourceCounts: {}, model: run.model, provider: run.provider, runs: [], @@ -358,10 +492,44 @@ function buildReport({ dbPath, generatedAt = new Date().toISOString(), limitRuns group.statusCounts[run.status] = (group.statusCounts[run.status] || 0) + 1; if (run.firstTextMs !== null) { group.firstTextCount += 1; + group.firstTextSourceCounts[run.firstTextSource] = + (group.firstTextSourceCounts[run.firstTextSource] || 0) + 1; } if (run.routing.selectedProvider || run.routing.selectedModel || run.routing.decisionReason) { group.withRoutingEvidence += 1; } + + if (run.responsiveLatencyEligible) { + const responsiveKey = buildResponsiveGroupKey(run); + if (!responsiveLatencyGroups.has(responsiveKey)) { + responsiveLatencyGroups.set(responsiveKey, { + decisionSourceCounts: {}, + durationStats: null, + firstTextStats: null, + firstTextCount: 0, + firstTextSourceCounts: {}, + model: run.model, + provider: run.provider, + runs: [], + statusCounts: {}, + withRoutingEvidence: 0, + }); + } + const responsiveGroup = responsiveLatencyGroups.get(responsiveKey); + responsiveGroup.runs.push(run); + responsiveGroup.statusCounts[run.status] = + (responsiveGroup.statusCounts[run.status] || 0) + 1; + responsiveGroup.decisionSourceCounts[run.decisionSource] = + (responsiveGroup.decisionSourceCounts[run.decisionSource] || 0) + 1; + if (run.firstTextMs !== null) { + responsiveGroup.firstTextCount += 1; + responsiveGroup.firstTextSourceCounts[run.firstTextSource] = + (responsiveGroup.firstTextSourceCounts[run.firstTextSource] || 0) + 1; + } + if (run.routing.selectedProvider || run.routing.selectedModel || run.routing.decisionReason) { + responsiveGroup.withRoutingEvidence += 1; + } + } } const groupSummaries = [...groups.values()] @@ -371,6 +539,32 @@ function buildReport({ dbPath, generatedAt = new Date().toISOString(), limitRuns decisionSource: group.decisionSource, durationStats: summarizeNumbers(group.runs.map((run) => run.durationMs)), firstTextCount: group.firstTextCount, + firstTextSourceCounts: group.firstTextSourceCounts, + firstTextStats: summarizeNumbers(group.runs.map((run) => run.firstTextMs)), + model: group.model, + provider: group.provider, + runCount, + statusCounts: group.statusCounts, + withRoutingEvidence: group.withRoutingEvidence, + }; + }) + .sort((left, right) => { + const firstTextDelta = right.firstTextCount - left.firstTextCount; + if (firstTextDelta !== 0) { + return firstTextDelta; + } + return right.runCount - left.runCount; + }); + + const responsiveLatencyGroupSummaries = [...responsiveLatencyGroups.values()] + .map((group) => { + const runCount = group.runs.length; + return { + decisionSourceCounts: group.decisionSourceCounts, + decisionSources: Object.keys(group.decisionSourceCounts).sort(), + durationStats: summarizeNumbers(group.runs.map((run) => run.durationMs)), + firstTextCount: group.firstTextCount, + firstTextSourceCounts: group.firstTextSourceCounts, firstTextStats: summarizeNumbers(group.runs.map((run) => run.firstTextMs)), model: group.model, provider: group.provider, @@ -398,13 +592,17 @@ function buildReport({ dbPath, generatedAt = new Date().toISOString(), limitRuns "status counts", "duration stats", "first text TTFT stats", + "first text source counts", "routing evidence count", ], omittedFields: ["prompt", "assistant response", "error_message", "secrets", "run ids"], }, + responsiveLatencyGroups: responsiveLatencyGroupSummaries, summary: { firstTextRuns: runs.filter((run) => run.firstTextMs !== null).length, groups: groupSummaries.length, + responsiveLatencyGroups: responsiveLatencyGroupSummaries.length, + responsiveLatencyRuns: runs.filter((run) => run.responsiveLatencyEligible).length, runs: runs.length, routingEvidenceRuns: runs.filter( (run) => run.routing.selectedProvider || run.routing.selectedModel || run.routing.decisionReason, @@ -420,6 +618,196 @@ function statusSummary(statusCounts) { .join(" / "); } +function statusCount(statusCounts, status) { + return Number(statusCounts?.[status] || 0); +} + +function errorLikeSampleCount(statusCounts) { + return Object.entries(statusCounts || {}).reduce((total, [status, count]) => { + if (status === "success" || status === "running") { + return total; + } + return total + Number(count || 0); + }, 0); +} + +function isFallbackOnlyResponsiveGroup(group, minimum) { + return ( + group.firstTextCount < minimum && + statusCount(group.statusCounts, "success") === 0 && + errorLikeSampleCount(group.statusCounts) > 0 + ); +} + +function buildResponsiveMatrixCheck( + report, + { + minFirstTextPerGroup, + preset = "", + requiredModels = [], + requiredProviders = [], + }, +) { + const minimum = normalizeNonNegativeInt( + minFirstTextPerGroup, + DEFAULT_MIN_FIRST_TEXT_PER_RESPONSIVE_GROUP, + ); + const responsiveGroups = report.responsiveLatencyGroups || []; + const fallbackOnlyGroups = responsiveGroups + .filter((group) => isFallbackOnlyResponsiveGroup(group, minimum)) + .map((group) => ({ + decisionSources: group.decisionSources || [], + firstTextCount: group.firstTextCount, + minimum, + model: group.model, + provider: group.provider, + runCount: group.runCount, + status: statusSummary(group.statusCounts) || "n/a", + })); + const missingGroups = responsiveGroups + .filter((group) => group.firstTextCount < minimum) + .filter((group) => !isFallbackOnlyResponsiveGroup(group, minimum)) + .map((group) => ({ + decisionSources: group.decisionSources || [], + firstTextCount: group.firstTextCount, + minimum, + model: group.model, + neededFirstTextSamples: minimum - group.firstTextCount, + provider: group.provider, + runCount: group.runCount, + status: statusSummary(group.statusCounts) || "n/a", + })); + const missingRequiredProviders = requiredProviders + .filter((provider) => provider) + .filter( + (provider) => !responsiveGroups.some((group) => group.provider === provider), + ); + const missingRequiredModels = requiredModels + .filter((model) => model) + .filter((model) => !responsiveGroups.some((group) => group.model === model)); + + return { + fallbackOnlyGroups, + firstTextTargetPerGroup: minimum, + missingGroups, + missingRequiredModels, + missingRequiredProviders, + passingGroups: responsiveGroups.length - missingGroups.length - fallbackOnlyGroups.length, + preset, + requiredModels: requiredModels.filter(Boolean), + requiredProviders: requiredProviders.filter(Boolean), + responsiveGroupKind: "responsive_chat_latency", + responsiveGroups: responsiveGroups.length, + status: + responsiveGroups.length > 0 && + missingGroups.length === 0 && + missingRequiredProviders.length === 0 && + missingRequiredModels.length === 0 + ? "pass" + : "fail", + totalNeededFirstTextSamples: missingGroups.reduce( + (total, group) => total + group.neededFirstTextSamples, + 0, + ), + }; +} + +function renderMatrixCheckMarkdown(matrixCheck) { + const lines = [ + "", + "## Responsive matrix check", + "", + `- status: ${matrixCheck.status}`, + `- responsive_chat latency groups: ${matrixCheck.responsiveGroups}`, + `- passing groups: ${matrixCheck.passingGroups}`, + `- fallback-only groups: ${matrixCheck.fallbackOnlyGroups.length}`, + `- first-text target per group: ${matrixCheck.firstTextTargetPerGroup}`, + `- additional first-text samples needed: ${matrixCheck.totalNeededFirstTextSamples}`, + ]; + if (matrixCheck.preset) { + lines.push(`- preset: ${matrixCheck.preset}`); + } + if (matrixCheck.requiredProviders.length > 0) { + lines.push(`- required providers: ${matrixCheck.requiredProviders.join(", ")}`); + } + if (matrixCheck.requiredModels.length > 0) { + lines.push(`- required models: ${matrixCheck.requiredModels.join(", ")}`); + } + if (matrixCheck.missingRequiredProviders.length > 0) { + lines.push(`- missing required providers: ${matrixCheck.missingRequiredProviders.join(", ")}`); + } + if (matrixCheck.missingRequiredModels.length > 0) { + lines.push(`- missing required models: ${matrixCheck.missingRequiredModels.join(", ")}`); + } + if (matrixCheck.missingGroups.length === 0) { + lines.push("- missing groups: none"); + } else { + lines.push("- missing groups:"); + for (const group of matrixCheck.missingGroups) { + lines.push( + [ + ` - ${markdownInlineCode(`${group.provider}/${group.model}`)}`, + `${group.firstTextCount}/${group.minimum} first-text samples,`, + `need ${group.neededFirstTextSamples} more,`, + `${group.runCount} runs, status ${group.status}`, + group.decisionSources?.length ? `sources ${group.decisionSources.join("/")}` : "", + ].join(" "), + ); + } + } + + if (matrixCheck.fallbackOnlyGroups.length === 0) { + lines.push("- fallback-only groups: none"); + } else { + lines.push("- fallback-only groups:"); + for (const group of matrixCheck.fallbackOnlyGroups) { + lines.push( + [ + ` - ${markdownInlineCode(`${group.provider}/${group.model}`)}`, + `${group.runCount} runs, status ${group.status},`, + "kept as routing fallback evidence rather than first-text baseline", + group.decisionSources?.length ? `sources ${group.decisionSources.join("/")}` : "", + ].join(" "), + ); + } + } + return lines; +} + +function reportMatrixCheckFailure(matrixCheck) { + const missing = matrixCheck.missingGroups + .map((group) => `${group.provider}/${group.model} (${group.firstTextCount}/${group.minimum})`) + .join(", "); + const missingProviders = + matrixCheck.missingRequiredProviders.length > 0 + ? `Missing required providers: ${matrixCheck.missingRequiredProviders.join(", ")}.` + : ""; + const missingModels = + matrixCheck.missingRequiredModels.length > 0 + ? `Missing required models: ${matrixCheck.missingRequiredModels.join(", ")}.` + : ""; + const fallbackOnly = + matrixCheck.fallbackOnlyGroups.length > 0 + ? `Fallback-only groups: ${matrixCheck.fallbackOnlyGroups + .map((group) => `${group.provider}/${group.model} (${group.status})`) + .join(", ")}.` + : ""; + console.error( + [ + "Responsive matrix check failed:", + `${matrixCheck.passingGroups}/${matrixCheck.responsiveGroups} groups passed`, + `with target ${matrixCheck.firstTextTargetPerGroup} first-text samples per responsive_chat latency group.`, + `Need ${matrixCheck.totalNeededFirstTextSamples} additional first-text samples.`, + missing ? `Missing: ${missing}` : "", + fallbackOnly, + missingProviders, + missingModels, + ] + .filter(Boolean) + .join(" "), + ); +} + function renderMarkdown(report, { limitGroups }) { const safeLimitGroups = normalizePositiveInt(limitGroups, DEFAULT_LIMIT_GROUPS); const groups = report.groups.slice(0, safeLimitGroups); @@ -435,6 +823,8 @@ function renderMarkdown(report, { limitGroups }) { `- groups: ${report.summary.groups}`, `- runs with routing evidence: ${report.summary.routingEvidenceRuns}`, `- runs with first text TTFT: ${report.summary.firstTextRuns}`, + `- responsive_chat latency runs: ${report.summary.responsiveLatencyRuns}`, + `- responsive_chat latency groups: ${report.summary.responsiveLatencyGroups}`, "- privacy: prompt / response / error_message are not exported", "", "## Group summary", @@ -458,6 +848,32 @@ function renderMarkdown(report, { limitGroups }) { ); } + const responsiveLatencyGroups = (report.responsiveLatencyGroups || []).slice( + 0, + safeLimitGroups, + ); + lines.push( + "", + "## Responsive chat latency groups", + "", + "| provider/model | runs | decision sources | status | routing evidence | first text samples | first text min/p50/avg/max | duration min/p50/avg/max |", + "| --- | ---: | --- | --- | ---: | ---: | --- | --- |", + ); + for (const group of responsiveLatencyGroups) { + lines.push( + [ + markdownInlineCode(`${group.provider}/${group.model}`), + String(group.runCount), + markdownCell(group.decisionSources.join(" / ") || "n/a"), + markdownCell(statusSummary(group.statusCounts) || "n/a"), + String(group.withRoutingEvidence), + String(group.firstTextCount), + formatStats(group.firstTextStats), + formatStats(group.durationStats), + ].join(" | ").replace(/^/, "| ").replace(/$/, " |"), + ); + } + lines.push("", "## Missing first-text TTFT candidates", ""); if (missingFirstText.length === 0) { lines.push("- none with routing evidence"); @@ -473,6 +889,10 @@ function renderMarkdown(report, { limitGroups }) { } } + if (report.matrixCheck) { + lines.push(...renderMatrixCheckMarkdown(report.matrixCheck)); + } + return `${lines.join("\n")}\n`; } @@ -496,14 +916,30 @@ function main() { dbPath: path.resolve(args.dbPath), limitRuns: args.limitRuns, }); + if (args.checkResponsiveMatrix) { + report.matrixCheck = buildResponsiveMatrixCheck(report, { + minFirstTextPerGroup: args.minFirstTextPerResponsiveGroup, + preset: args.matrixPreset, + requiredModels: args.requiredResponsiveModels, + requiredProviders: args.requiredResponsiveProviders, + }); + } if (args.format === "json") { writeOutput(args.outputPath, `${JSON.stringify(report, null, 2)}\n`); + if (report.matrixCheck && report.matrixCheck.status !== "pass") { + reportMatrixCheckFailure(report.matrixCheck); + process.exitCode = 1; + } return; } if (args.format !== "markdown") { throw new Error(`Unsupported format: ${args.format}`); } writeOutput(args.outputPath, renderMarkdown(report, { limitGroups: args.limitGroups })); + if (report.matrixCheck && report.matrixCheck.status !== "pass") { + reportMatrixCheckFailure(report.matrixCheck); + process.exitCode = 1; + } } main(); diff --git a/scripts/at-command-registry-e2e.mjs b/scripts/at-command-registry-e2e.mjs index bc6269601..4460cceee 100644 --- a/scripts/at-command-registry-e2e.mjs +++ b/scripts/at-command-registry-e2e.mjs @@ -226,6 +226,99 @@ function parseInvokeRequest(request, invokeUrl) { } } +async function invokeBridgeCommand(options, cmd, args, timeoutMs = 30_000) { + const controller = new AbortController(); + const timeout = setTimeout(() => controller.abort(), timeoutMs); + + try { + const response = await fetch(options.invokeUrl, { + method: "POST", + headers: { "content-type": "application/json" }, + body: JSON.stringify({ cmd, args }), + signal: controller.signal, + }); + const text = await response.text(); + const payload = text ? JSON.parse(text) : null; + if (!response.ok) { + throw new Error(`HTTP ${response.status}: ${text}`); + } + return payload; + } finally { + clearTimeout(timeout); + } +} + +async function interruptSubmittedTurn(options, request) { + const sessionId = String( + request.session_id || request.sessionId || "", + ).trim(); + const turnId = String(request.turn_id || request.turnId || "").trim(); + if (!sessionId) { + return { attempted: false, reason: "missing-session-id" }; + } + + const interruptRequest = { + session_id: sessionId, + ...(turnId ? { turn_id: turnId } : {}), + }; + + try { + await invokeBridgeCommand( + options, + "agent_runtime_interrupt_turn", + { request: interruptRequest }, + 20_000, + ); + } catch (error) { + return { + attempted: true, + status: "failed", + sessionId, + turnId: turnId || null, + error: error instanceof Error ? error.message : String(error), + }; + } + + const drained = await waitForCondition( + "等待 @配图 smoke 提交任务清理", + async () => { + const threadRead = await invokeBridgeCommand( + options, + "agent_runtime_get_thread_read", + { sessionId }, + 20_000, + ).catch(() => null); + if (!threadRead) { + return null; + } + const status = String(threadRead.status || "").toLowerCase(); + const queuedTurns = Array.isArray(threadRead.queued_turns) + ? threadRead.queued_turns.length + : 0; + const activeTurnId = + threadRead.active_turn_id || threadRead.activeTurnId || null; + return !activeTurnId && + queuedTurns === 0 && + (status === "idle" || status === "completed" || status === "cancelled") + ? { status: threadRead.status || null, queuedTurns } + : null; + }, + 30_000, + options.intervalMs, + ).catch((error) => ({ + status: "unknown", + error: error instanceof Error ? error.message : String(error), + })); + + return { + attempted: true, + status: "sent", + sessionId, + turnId: turnId || null, + drained, + }; +} + async function main() { if (typeof fetch !== "function") { throw new Error("当前 Node 运行时不支持 fetch,请使用 Node 18+"); @@ -372,7 +465,7 @@ async function main() { .waitFor({ state: "visible", timeout: options.timeoutMs }); const imageCommandItem = commandItemByText(page, "@配图").first(); await imageCommandItem.waitFor({ - state: "visible", + state: "attached", timeout: options.timeoutMs, }); @@ -403,6 +496,7 @@ async function main() { }); logStage(options, "select-image-command"); + await imageCommandItem.scrollIntoViewIfNeeded(); await imageCommandItem.click(); await textarea.fill(IMAGE_PROMPT); @@ -459,6 +553,8 @@ async function main() { summary.assertions.chatModelPreferenceSuppressed = true; summary.submitRequest = { message: request.message, + sessionId: request.session_id || null, + turnId: request.turn_id || null, providerPreference: turnConfig.provider_preference ?? null, modelPreference: turnConfig.model_preference ?? null, contractKey, @@ -473,6 +569,8 @@ async function main() { fullPage: true, }); + summary.cleanup = await interruptSubmittedTurn(options, request); + const summaryPath = path.join( options.evidenceDir, `${options.prefix}-summary.json`, diff --git a/scripts/claw-chat-ready-streaming-smoke.mjs b/scripts/claw-chat-ready-streaming-smoke.mjs index c34698eff..4375976e9 100644 --- a/scripts/claw-chat-ready-streaming-smoke.mjs +++ b/scripts/claw-chat-ready-streaming-smoke.mjs @@ -34,15 +34,16 @@ const DEFAULTS = { const POST_HEALTH_SETTLE_MS = 1_500; const ONBOARDING_VERSION = "1.1.0"; +const LONG_TURN_LINE_COUNT = 480; const LONG_PROMPT = [ - "E2E 中断测试:请输出 160 行。", + `E2E 中断测试:请输出 ${LONG_TURN_LINE_COUNT} 行。`, "每一行都必须使用格式:中断测试第 N 行。", "从 1 开始递增,不要合并行,不要提前总结。", "如果收到停止请求应立即停止,不要补完剩余行。", ].join("\n"); const RECOVERY_EXPECTED_TEXT = "复原完成"; const RECOVERY_PROMPT = - "停止后恢复测试:这是一个新的独立回合,请忽略上一条输出 160 行的要求。只输出“复原完成”这四个字,不要输出行号、解释或其他内容。"; + `停止后恢复测试:这是一个新的独立回合,请忽略上一条输出 ${LONG_TURN_LINE_COUNT} 行的要求。只输出“复原完成”这四个字,不要输出行号、解释或其他内容。`; const MODEL_AVAILABILITY_PROMPT = "请只回复 QC_OK。"; const MAX_MODEL_AVAILABILITY_CANDIDATES = 12; const FAST_RESPONSE_MODE_STORAGE_KEY = "lime:agent-fast-response-mode"; @@ -660,6 +661,50 @@ function queuedTurnCount(session) { return Array.isArray(session?.queued_turns) ? session.queued_turns.length : 0; } +function activeTurnIdFromThreadRead(threadRead) { + return threadRead?.active_turn_id || threadRead?.activeTurnId || null; +} + +function latestTurnStatusFromThreadRead(threadRead) { + return ( + threadRead?.diagnostics?.latest_turn_status || + threadRead?.runtime_summary?.latestTurnStatus || + threadRead?.status || + null + ); +} + +function syntheticInterruptedTurn(turnId, threadRead) { + return { + id: turnId, + status: "aborted", + synthetic: true, + source: "thread-read-idle-after-interrupt", + threadStatus: latestTurnStatusFromThreadRead(threadRead), + }; +} + +function interruptDrainedWithoutRecordedTurn({ + session, + threadRead, + snapshot, + interruptScoped, +}) { + const threadStatus = String( + latestTurnStatusFromThreadRead(threadRead) || "", + ).toLowerCase(); + return ( + interruptScoped && + queuedTurnCount(session) === 0 && + queuedTurnCount(threadRead) === 0 && + !activeTurnIdFromThreadRead(threadRead) && + (threadStatus === "idle" || threadStatus === "aborted") && + snapshot && + !snapshot.stopVisible && + !snapshot.finalLineSeen + ); +} + function normalizeConsoleLine(item) { const location = item.location && item.location.url @@ -668,7 +713,7 @@ function normalizeConsoleLine(item) { return `[${item.type}] ${item.text}${location}`; } -function buildPageSnapshotScript(recoveryExpectedText) { +function buildPageSnapshotScript(recoveryExpectedText, longTurnLineCount) { return `(() => { const textareas = Array.from( document.querySelectorAll('textarea[name="agent-chat-message"]'), @@ -684,8 +729,13 @@ function buildPageSnapshotScript(recoveryExpectedText) { ) ?? null; const bodyText = document.body?.innerText || ""; const streamLinePattern = /中断测试第\\s*\\d+\\s*行/; - const finalLinePattern = /中断测试第\\s*160\\s*行/; + const finalLinePattern = new RegExp("中断测试第\\\\s*" + ${JSON.stringify( + String(longTurnLineCount), + )} + "\\\\s*行"); const recoveryExpected = ${JSON.stringify(recoveryExpectedText)}; + const recoveryTextCount = recoveryExpected + ? bodyText.split(recoveryExpected).length - 1 + : 0; const streamText = bodyText .split("\\n") .filter((line) => streamLinePattern.test(line)) @@ -700,6 +750,7 @@ function buildPageSnapshotScript(recoveryExpectedText) { longPromptVisible: bodyText.includes("E2E 中断测试"), stoppedMarker: bodyText.includes("用户已停止当前执行"), recoveryVisible: bodyText.includes(recoveryExpected), + recoveryTextCount, streamLineCount: streamText ? streamText.split("\\n").filter(Boolean).length : 0, streamTextLength: streamText.length, finalLineSeen: finalLinePattern.test(streamText), @@ -715,7 +766,9 @@ function buildPageSnapshotScript(recoveryExpectedText) { async function readPageSnapshot(page) { return page - .evaluate(buildPageSnapshotScript(RECOVERY_EXPECTED_TEXT)) + .evaluate( + buildPageSnapshotScript(RECOVERY_EXPECTED_TEXT, LONG_TURN_LINE_COUNT), + ) .catch(() => null); } @@ -1009,6 +1062,8 @@ async function main() { }; let submittedSessionId = ""; let submittedLongTurnId = ""; + let submittedFollowSessionId = ""; + let submittedFollowTurnId = ""; try { const scopedProviderKey = `agent_pref_provider_${workspaceId}`; @@ -1203,12 +1258,26 @@ async function main() { }`, ); } - return snapshot?.stopVisible && snapshot.streamTextLength > 0 - ? snapshot + const runtimeStreamEventSeen = consoleMessages.some( + (item) => + String(item?.text || "").includes("AgentStream.firstEvent") || + String(item?.text || "").includes( + "AgentStream.firstRuntimeStatus", + ) || + String(item?.text || "").includes("AgentStream.runtimeKeepalive"), + ); + return snapshot?.stopVisible && + (snapshot.streamTextLength > 0 || runtimeStreamEventSeen) + ? { + ...snapshot, + runtimeStreamEventSeen, + runtimeTurn: turn || null, + runtimeTurnStatus: turn?.status || null, + } : null; }, 90_000, - 500, + 250, ); summary.firstDelta = firstDelta; summary.steps.push("running-stop-visible"); @@ -1217,22 +1286,7 @@ async function main() { fullPage: true, }); - const runningSession = await waitForCondition( - "等待 turn 进入 running", - async () => { - const session = await invoke( - options, - "agent_runtime_get_session", - { sessionId }, - 20_000, - ).catch(() => null); - const turn = findTurn(session, longTurnId); - return turn?.status === "running" ? { turn, session } : null; - }, - 60_000, - 1_000, - ); - summary.runningTurnObserved = runningSession.turn; + summary.runningTurnObserved = firstDelta.runtimeTurn || null; logStage("interrupt-long-turn"); const interruptStart = invokes.length; @@ -1264,16 +1318,42 @@ async function main() { const interrupted = await waitForCondition( "等待 turn 中断完成", async () => { - const session = await invoke( - options, - "agent_runtime_get_session", - { sessionId }, - 20_000, - ).catch(() => null); + const [session, threadRead, snapshot] = await Promise.all([ + invoke( + options, + "agent_runtime_get_session", + { sessionId }, + 20_000, + ).catch(() => null), + invoke( + options, + "agent_runtime_get_thread_read", + { sessionId }, + 20_000, + ).catch(() => null), + readPageSnapshot(page), + ]); const turn = findTurn(session, longTurnId); - return turn && ["aborted", "failed", "completed"].includes(turn.status) - ? { turn, session } - : null; + if (turn && ["aborted", "failed", "completed"].includes(turn.status)) { + return { turn, session, threadRead, snapshot }; + } + if ( + interruptDrainedWithoutRecordedTurn({ + session, + threadRead, + snapshot, + interruptScoped: summary.interruptHasTurnScope, + }) + ) { + return { + turn: syntheticInterruptedTurn(longTurnId, threadRead), + session, + threadRead, + snapshot, + inferredInterruptDrained: true, + }; + } + return null; }, 120_000, 1_000, @@ -1281,6 +1361,11 @@ async function main() { let latestSession = interrupted.session; summary.interruptedTurn = interrupted.turn; summary.interruptedTurnStatus = interrupted.turn?.status || null; + summary.interruptedThreadRead = interrupted.threadRead || null; + summary.interruptedSnapshot = interrupted.snapshot || null; + summary.interruptDrainedInferred = Boolean( + interrupted.inferredInterruptDrained, + ); summary.queueCountAfterInterrupt = queuedTurnCount(latestSession); await page @@ -1297,6 +1382,34 @@ async function main() { ) .catch(() => undefined); + logStage("restore-interrupted-session-before-recovery"); + summary.beforeRecoveryOpenDispatch = await dispatchTaskCenterOpenTask( + page, + sessionId, + workspaceId, + ); + summary.beforeRecoverySnapshot = await waitForCondition( + "等待中断会话重新挂载", + async () => { + const snapshot = await readPageSnapshot(page); + return snapshot && + !isLikelyDetachedBlankTaskSnapshot(snapshot) && + (snapshot.longPromptVisible || + snapshot.stoppedMarker || + snapshot.streamTextLength > 0) + ? snapshot + : null; + }, + 15_000, + 250, + ).catch((error) => { + summary.beforeRecoverySessionRestore = { + passed: false, + error: error instanceof Error ? error.message : String(error), + }; + return null; + }); + logStage("submit-recovery-turn"); const followSubmitStart = invokes.length; await textarea.fill(RECOVERY_PROMPT); @@ -1317,9 +1430,15 @@ async function main() { 250, ); const followRequest = followSubmit.args?.request || {}; + const followSessionId = String(followRequest.session_id || sessionId); const followTurnId = String(followRequest.turn_id || ""); + assert(followSessionId, "恢复 turn 缺少 session_id"); assert(followTurnId, "恢复 turn 缺少 turn_id"); + submittedFollowSessionId = followSessionId; + submittedFollowTurnId = followTurnId; + summary.followSessionId = followSessionId; summary.followTurnId = followTurnId; + summary.followUsesOriginalSession = followSessionId === sessionId; summary.followSubmitTurnConfig = followRequest.turn_config || null; const followCompleted = await waitForCondition( @@ -1328,7 +1447,7 @@ async function main() { const session = await invoke( options, "agent_runtime_get_session", - { sessionId }, + { sessionId: followSessionId }, 20_000, ).catch(() => null); const turn = findTurn(session, followTurnId); @@ -1342,13 +1461,25 @@ async function main() { latestSession = followCompleted.session || latestSession; summary.followTurn = followCompleted.turn; summary.followTurnStatus = followCompleted.turn?.status || null; + const followAssistantText = [ + allAssistantText(latestSession), + allAgentItemText(latestSession), + ] + .filter(Boolean) + .join("\n"); + summary.followPersistedRecoveryBeforeGuiWait = + followAssistantText.includes(RECOVERY_EXPECTED_TEXT); let recoverySnapshot = null; try { recoverySnapshot = await waitForCondition( "等待 GUI 出现恢复结果", async () => { const snapshot = await readPageSnapshot(page); - return snapshot?.recoveryVisible ? snapshot : null; + return snapshot?.recoveryVisible && + (Number(snapshot.recoveryTextCount || 0) >= 2 || + summary.followPersistedRecoveryBeforeGuiWait) + ? snapshot + : null; }, 60_000, 500, @@ -1367,7 +1498,7 @@ async function main() { (await invoke( options, "agent_runtime_get_session", - { sessionId }, + { sessionId: followSessionId }, 20_000, ).catch(() => null)) || latestSession; const persistedAssistantText = [ @@ -1388,12 +1519,16 @@ async function main() { if (isLikelyDetachedBlankTaskSnapshot(detachedSnapshot)) { logStage("restore-session-after-recovery-persisted"); summary.recoveryVisibleRestoreDispatch = - await dispatchTaskCenterOpenTask(page, sessionId, workspaceId); + await dispatchTaskCenterOpenTask(page, followSessionId, workspaceId); recoverySnapshot = await waitForCondition( "等待重新打开目标会话后 GUI 出现恢复结果", async () => { const snapshot = await readPageSnapshot(page); - return snapshot?.recoveryVisible ? snapshot : null; + return snapshot?.recoveryVisible && + (Number(snapshot.recoveryTextCount || 0) >= 2 || + summary.recoveryPersistedBeforeRefresh) + ? snapshot + : null; }, 45_000, 500, @@ -1429,7 +1564,7 @@ async function main() { const threadRead = await invoke( options, "agent_runtime_get_thread_read", - { sessionId }, + { sessionId: followSessionId }, 20_000, ).catch((error) => ({ __error: error instanceof Error ? error.message : String(error), @@ -1446,8 +1581,7 @@ async function main() { mockFallbackLines, networkErrorTop, } = consoleNetworkSummary(consoleMessages, failedRequests); - const activeTurnId = - threadRead?.active_turn_id || threadRead?.activeTurnId || null; + const activeTurnId = activeTurnIdFromThreadRead(threadRead); const runtimeMockLines = mockFallbackLines.filter((line) => /agent_runtime_(submit_turn|interrupt_turn|get_session|get_thread_read)/.test( line, @@ -1460,12 +1594,11 @@ async function main() { summary.threadRead = threadRead?.__error ? { error: threadRead.__error } : threadRead; - summary.threadReadStatus = - summary.threadRead?.diagnostics?.latest_turn_status || - summary.threadRead?.runtime_summary?.latestTurnStatus || - null; + summary.threadReadStatus = latestTurnStatusFromThreadRead(summary.threadRead); const latestRuntimeRouting = - latestSession?.execution_runtime?.routing_decision || {}; + latestSession?.execution_runtime?.routing_decision || + threadRead?.model_routing || + {}; const followProviderPreferenceHonored = followRequest.turn_config?.provider_preference === preferredProvider || latestRuntimeRouting?.selectedProvider === preferredProvider || @@ -1495,7 +1628,7 @@ async function main() { assistantText.match(/中断测试第\s*\d+\s*行/g) || [] ).length; summary.interruptedAssistantContainsFinalLine = - assistantText.includes("中断测试第 160 行"); + assistantText.includes(`中断测试第 ${LONG_TURN_LINE_COUNT} 行`); summary.consoleErrorCount = consoleErrors.length; summary.consoleWarningCount = consoleWarnings.length; summary.networkErrorCount = failedRequests.length; @@ -1505,7 +1638,9 @@ async function main() { summary.assertions = { workspaceReady: Boolean(readySnapshot?.ready), devBridgeHealthy: health?.status === "ok" || Boolean(health), - streamFirstDeltaSeen: Boolean(firstDelta?.streamTextLength > 0), + streamFirstDeltaSeen: Boolean( + firstDelta?.streamTextLength > 0 || firstDelta?.runtimeStreamEventSeen, + ), streamGrowthObserved: Number(recoverySnapshot?.streamTextLength || 0) >= Number(firstDelta?.streamTextLength || 0) || @@ -1657,10 +1792,38 @@ async function main() { ? interruptError.message : String(interruptError), })); + if ( + submittedFollowSessionId && + (submittedFollowSessionId !== submittedSessionId || + submittedFollowTurnId !== submittedLongTurnId) + ) { + summary.failureCleanupFollowInterrupt = await invoke( + options, + "agent_runtime_interrupt_turn", + { + request: { + session_id: submittedFollowSessionId, + ...(submittedFollowTurnId + ? { turn_id: submittedFollowTurnId } + : {}), + }, + }, + 20_000, + ) + .then(() => ({ attempted: true, status: "sent" })) + .catch((interruptError) => ({ + attempted: true, + status: "failed", + error: + interruptError instanceof Error + ? interruptError.message + : String(interruptError), + })); + } const failureSession = await invoke( options, "agent_runtime_get_session", - { sessionId: submittedSessionId }, + { sessionId: submittedFollowSessionId || submittedSessionId }, 20_000, ).catch((sessionError) => ({ __error: @@ -1671,7 +1834,7 @@ async function main() { const failureThreadRead = await invoke( options, "agent_runtime_get_thread_read", - { sessionId: submittedSessionId }, + { sessionId: submittedFollowSessionId || submittedSessionId }, 20_000, ).catch((threadError) => ({ __error: diff --git a/src-tauri/Cargo.lock b/src-tauri/Cargo.lock index f0c15d071..c4f9ed060 100644 --- a/src-tauri/Cargo.lock +++ b/src-tauri/Cargo.lock @@ -5131,7 +5131,7 @@ dependencies = [ [[package]] name = "lime" -version = "1.36.0" +version = "1.37.0" dependencies = [ "anyhow", "arboard", @@ -5238,7 +5238,7 @@ dependencies = [ [[package]] name = "lime-agent" -version = "1.36.0" +version = "1.37.0" dependencies = [ "anyhow", "aster-core", @@ -5267,7 +5267,7 @@ dependencies = [ [[package]] name = "lime-browser-runtime" -version = "1.36.0" +version = "1.37.0" dependencies = [ "chrono", "futures", @@ -5284,7 +5284,7 @@ dependencies = [ [[package]] name = "lime-cli" -version = "1.36.0" +version = "1.37.0" dependencies = [ "clap", "lime-core", @@ -5296,7 +5296,7 @@ dependencies = [ [[package]] name = "lime-config" -version = "1.36.0" +version = "1.37.0" dependencies = [ "async-trait", "lime-core", @@ -5312,7 +5312,7 @@ dependencies = [ [[package]] name = "lime-core" -version = "1.36.0" +version = "1.37.0" dependencies = [ "aster-models", "async-trait", @@ -5365,7 +5365,7 @@ dependencies = [ [[package]] name = "lime-gateway" -version = "1.36.0" +version = "1.37.0" dependencies = [ "aes", "axum 0.7.9", @@ -5395,7 +5395,7 @@ dependencies = [ [[package]] name = "lime-infra" -version = "1.36.0" +version = "1.37.0" dependencies = [ "chrono", "dashmap 5.5.3", @@ -5415,7 +5415,7 @@ dependencies = [ [[package]] name = "lime-knowledge" -version = "1.36.0" +version = "1.37.0" dependencies = [ "chrono", "hex", @@ -5428,7 +5428,7 @@ dependencies = [ [[package]] name = "lime-mcp" -version = "1.36.0" +version = "1.37.0" dependencies = [ "async-trait", "dirs 5.0.1", @@ -5444,7 +5444,7 @@ dependencies = [ [[package]] name = "lime-media-runtime" -version = "1.36.0" +version = "1.37.0" dependencies = [ "axum 0.7.9", "base64 0.22.1", @@ -5477,7 +5477,7 @@ dependencies = [ [[package]] name = "lime-processor" -version = "1.36.0" +version = "1.37.0" dependencies = [ "async-trait", "lime-core", @@ -5496,7 +5496,7 @@ dependencies = [ [[package]] name = "lime-providers" -version = "1.36.0" +version = "1.37.0" dependencies = [ "anyhow", "async-stream", @@ -5551,7 +5551,7 @@ dependencies = [ [[package]] name = "lime-server" -version = "1.36.0" +version = "1.37.0" dependencies = [ "aster-core", "async-stream", @@ -5595,7 +5595,7 @@ dependencies = [ [[package]] name = "lime-server-utils" -version = "1.36.0" +version = "1.37.0" dependencies = [ "axum 0.7.9", "futures", @@ -5610,7 +5610,7 @@ dependencies = [ [[package]] name = "lime-services" -version = "1.36.0" +version = "1.37.0" dependencies = [ "anyhow", "aster-core", @@ -5655,7 +5655,7 @@ dependencies = [ [[package]] name = "lime-skills" -version = "1.36.0" +version = "1.37.0" dependencies = [ "async-trait", "dirs 5.0.1", @@ -5673,7 +5673,7 @@ dependencies = [ [[package]] name = "lime-websocket" -version = "1.36.0" +version = "1.37.0" dependencies = [ "axum 0.7.9", "chrono", diff --git a/src-tauri/Cargo.toml b/src-tauri/Cargo.toml index 454878bad..53932d98f 100644 --- a/src-tauri/Cargo.toml +++ b/src-tauri/Cargo.toml @@ -9,7 +9,7 @@ exclude = [ resolver = "2" [workspace.package] -version = "1.36.0" +version = "1.37.0" edition = "2021" authors = ["coso"] repository = "https://github.com/aiclientproxy/lime" @@ -198,7 +198,7 @@ version = "2.4.9" [package] name = "lime" -version = "1.36.0" +version = "1.37.0" description = "AI API Proxy Desktop App" authors = ["you"] edition = "2021" diff --git a/src-tauri/resources/default-skills/image_generate/SKILL.md b/src-tauri/resources/default-skills/image_generate/SKILL.md index d34748d12..5a399130e 100644 --- a/src-tauri/resources/default-skills/image_generate/SKILL.md +++ b/src-tauri/resources/default-skills/image_generate/SKILL.md @@ -1,10 +1,10 @@ --- name: image_generate -description: 根据文本描述生成配图素材(非封面场景)。 +description: 仅在用户显式使用 @配图/@修图/@重绘/@image,或已明确确认调用画图功能后,根据文本描述生成配图素材(非封面场景)。 allowed-tools: lime_create_image_generation_task metadata: lime_argument_hint: 输入主题、画面主体、风格、构图、数量、尺寸。 - lime_when_to_use: 用户需要普通配图、插图或概念图时使用;封面需求优先交给 cover_generate。 + lime_when_to_use: 用户显式使用 @配图/@修图/@重绘/@image,或在普通对话中已确认“调用画图/现在生成”时使用;封面需求优先交给 cover_generate。 lime_version: 1.3.3 lime_execution_mode: prompt lime_surface: workbench @@ -20,6 +20,7 @@ metadata: ## 执行规则 - 先判断是否属于封面需求;封面需求请转 `cover_generate`。 +- 普通聊天里如果用户只是给出视觉提示词、设计海报 brief、完善画面描述,但没有显式 `@配图/@修图/@重绘/@image` 或明确确认“现在调用画图功能”,不要调用本 Skill;也不要改写成 HTML/CSS/SVG/Markdown 草图或长方案,应先用一句话询问是否开始生成。 - 当前已经进入 `@配图/@修图/@重绘 -> image_skill_launch -> Skill(image_generate)` 主链,不要先调用 `ToolSearch`、`WebSearch`、`Read`、`Glob`、`Grep` 去“找技能”或“确认工具”。 - 不要搜索 “Skill image_generate”、“lime media image generate --json”、“lime_create_image_generation_task” 之类目录信息;当前上下文已经明确要求执行图片任务。 - 提示词必须包含主体、场景、风格,不要空泛。 @@ -36,15 +37,12 @@ metadata: - 分镜题材由用户要求决定,可以是电影、动漫、短视频、广告等;应根据主题生成对应的逐格语义,而不是写死某一种题材模板。 - 若 `image_task` 已经提供 `storyboard_slots`,必须原样透传且保持顺序;若尚未提供而 `layout_hint=storyboard_3x3`,需要先补齐与 `count` 对齐的逐格 `storyboard_slots` 再创建任务。 - 不要通过 `Bash` 拼接 `lime media image generate --json`、`lime task create image --json` 或任何 `/tmp/lime_task_image_*.json` 临时任务文件。 -- `lime_create_image_generation_task` 返回后,应依赖同一份图片任务文件契约推进执行与结果回填;不要另起一套 markdown“提交成功”假产物。 +- `lime_create_image_generation_task` 返回后,应依赖同一份图片任务文件契约推进执行与结果回填;不要另起一套 markdown“提交成功”假产物,也不要输出独立的任务递交摘要。 - 调用 `lime_create_image_generation_task` 时不要传 `outputPath`,不要把任务写成 markdown 文稿。 - `payload` 中至少包含:`prompt`、`style`、`size`、`count`、`usage`;如有上下文,还应携带 `mode`、`provider_id`、`model`、`executor_mode`、`outer_model`、`reference_images`、`storyboard_slots`、`target_output_id`、`target_output_ref_id`、`session_id`、`project_id`、`content_id`、`entry_source`、`requested_target`、`runtime_contract`、`modality_contract_key`、`routing_slot`、`slot_id`、`anchor_hint`、`anchor_section_title`、`anchor_text`。 -## 输出格式(固定) +## 输出规则(固定) -仅输出任务提交摘要(不要再写 ``): +工具调用成功后,不要再输出“任务类型 / 任务 ID / 任务文件 / 状态”这类递交摘要;Lime 会在同一条 assistant 消息里展示工具调用、任务进度和图片结果。若确实需要补充文字,只用一句自然说明,不重复 task_id、path 或排队模板。 -- 任务类型:image_generate -- 任务 ID:{task_id} -- 任务文件:{path} -- 状态:{status} +聊天输出必须极简自然:工具前最多一句“好嘞,用 <模型> 给你生成...”,随后只保留“先获取下工具参数”“马上生成”这类短过程;不要输出任务表格、任务 ID、任务文件、排队说明、Image Workbench/图片工作台文案,也不要拆成第二条 assistant 回复。 diff --git a/src-tauri/src/commands/aster_agent_cmd/image_skill_launch.rs b/src-tauri/src/commands/aster_agent_cmd/image_skill_launch.rs index 2bd4552c0..0b9975731 100644 --- a/src-tauri/src/commands/aster_agent_cmd/image_skill_launch.rs +++ b/src-tauri/src/commands/aster_agent_cmd/image_skill_launch.rs @@ -9,10 +9,14 @@ use std::path::{Path, PathBuf}; const IMAGE_SKILL_INPUT_REF_PREFIX: &str = "skill-input-image://"; pub(super) const IMAGE_SKILL_LAUNCH_PROMPT_MARKER: &str = "<>"; +const IMAGE_GENERATION_CONFIRMATION_GUARD_MARKER: &str = + "<>"; const IMAGE_SKILL_LAUNCH_DETOUR_DENY_PATTERNS: &[&str] = &[ TOOL_SEARCH_TOOL_NAME, "WebSearch", "web_search", + LIME_SEARCH_WEB_IMAGES_TOOL_NAME, + "search_web_images", "Bash", "Read", "read", @@ -276,23 +280,47 @@ pub(crate) fn merge_system_prompt_with_image_skill_launch( request_metadata: Option<&serde_json::Value>, ) -> Option { let Some(launch_prompt) = build_image_skill_launch_system_prompt(request_metadata) else { - return base_prompt; + return merge_prompt_once( + base_prompt, + IMAGE_GENERATION_CONFIRMATION_GUARD_MARKER, + build_image_generation_confirmation_guard_prompt(), + ); }; + merge_prompt_once(base_prompt, IMAGE_SKILL_LAUNCH_PROMPT_MARKER, launch_prompt) +} + +fn merge_prompt_once( + base_prompt: Option, + marker: &str, + appended_prompt: String, +) -> Option { match base_prompt { Some(base) => { - if base.contains(IMAGE_SKILL_LAUNCH_PROMPT_MARKER) { + if base.contains(marker) { Some(base) } else if base.trim().is_empty() { - Some(launch_prompt) + Some(appended_prompt) } else { - Some(format!("{base}\n\n{launch_prompt}")) + Some(format!("{base}\n\n{appended_prompt}")) } } - None => Some(launch_prompt), + None => Some(appended_prompt), } } +fn build_image_generation_confirmation_guard_prompt() -> String { + [ + IMAGE_GENERATION_CONFIRMATION_GUARD_MARKER, + "- 普通聊天中,用户可能是在让你整理视觉方案、海报概念或生成提示词;不要因此自动调用 Skill(image_generate) 或 lime_create_image_generation_task。", + "- 只有当用户显式使用 @配图/@修图/@重绘/@image,或在你询问后明确确认“调用画图/现在生成/开始画”时,才允许进入图片生成主链。", + "- 如果用户没有 @ 命令但内容明显像配图提示词、海报 brief、封面 brief 或视觉设计 brief,必须先用 1 句简洁确认:是否要调用画图功能生成图片。", + "- 上述普通视觉 brief 确认回合不要输出 HTML/CSS/SVG/Markdown 草图,不要写完整设计方案,不要生成提示词全文,不要输出任务详情表、排队状态或任务已提交模板。", + "- 推荐确认句式:这看起来是图片生成需求,要我直接调用画图功能生成吗?", + ] + .join("\n") +} + pub(crate) fn should_lock_image_skill_launch_to_image_generation( request_metadata: Option<&serde_json::Value>, ) -> bool { @@ -438,7 +466,8 @@ fn build_image_skill_launch_system_prompt( ), "- 当前回合已经显式知道要走图片技能主链,不要为了确认技能名、工具名或命令名再去调用 ToolSearch。".to_string(), "- 当前主会话第一刀必须先调用 Skill(image_generate),但这不等于任务已经创建。".to_string(), - "- 在 Skill(image_generate) 真正执行前,不要先走 ToolSearch / WebSearch / Bash / Read / Write / Edit / Glob / Grep / 浏览器 MCP / Playwright 等通用工具发现、检索、脚本、文件或页面链路。".to_string(), + "- 在 Skill(image_generate) 真正执行前,不要先走 ToolSearch / WebSearch / lime_search_web_images / Bash / Read / Write / Edit / Glob / Grep / 浏览器 MCP / Playwright 等通用工具发现、检索、联网搜图、脚本、文件或页面链路。".to_string(), + "- `lime_search_web_images` 只服务 @素材 的联网图片候选检索,不是 @配图/@Nanobanana Pro 的图片生成入口;当前回合禁止调用它。".to_string(), "- 不要搜索 “Skill image_generate”、“lime media image generate --json”、“lime_create_image_generation_task” 之类目录信息;当前 image_task 已经提供了足够上下文。".to_string(), "- 如果某个通用搜索/读文件工具因为 session policy 被拒绝,不要重复同类调用;应立即改为直调 Skill(image_generate)。".to_string(), "- 如果 Skill(image_generate) 返回的 Lime 工具元数据里只有 allowed_tools=[\"lime_create_image_generation_task\"],而没有 task_id/path/status,说明任务尚未创建;当前主会话必须立刻继续调用 lime_create_image_generation_task。".to_string(), @@ -448,9 +477,10 @@ fn build_image_skill_launch_system_prompt( "- 调用 lime_create_image_generation_task 时,必须把 image_task 对象本身直接作为工具参数提交;不要再包一层 {\"image_task\": ...},更不要把整个对象再次序列化成字符串。".to_string(), "- 调用 lime_create_image_generation_task 时,统一使用 snake_case 字段名;不要把 anchorHint / providerId / projectId 这类 camelCase 同义字段与 snake_case 一起重复提交。".to_string(), "- 调用 lime_create_image_generation_task 时,必须只提交标准 image task 参数;不要传 outputPath,不要把任务写成 markdown 文稿。".to_string(), - "- 不要伪造“图片已生成完成”;在 task file 真正返回结果前,只能汇报任务已提交、排队或执行中。".to_string(), + "- 不要伪造“图片已生成完成”;在 task file 真正返回结果前,只能让工具轨迹展示任务已提交、排队或执行中,不要额外输出递交模板。".to_string(), "- 如果当前回合已经拿到任何图片任务结果,且结果里含 task_id、path,或 status=pending_submit/queued/running/partial/succeeded,说明任务已提交;不要再次调用 Skill(image_generate) 或重复创建第二个图片任务。".to_string(), - "- 拿到上述任务结果后,直接基于现有 task_id、路径和状态给出提交摘要,并等待 task file 后续回流。".to_string(), + "- 拿到上述任务结果后,不要再输出“任务类型 / 任务 ID / 任务文件 / 状态”这类提交摘要;让同一条 assistant 消息内的工具调用和图片任务轻卡继续展示进度与结果。".to_string(), + "- 聊天输出必须保持极简自然:工具前最多一句“好嘞,用 <模型> 给你生成...”,随后只保留“先获取下工具参数”“马上生成”这类短过程;不要输出任务表格、任务 ID、任务文件、排队说明、Image Workbench/图片工作台文案,也不要拆成第二条 assistant 回复。".to_string(), format!("- 当前图片任务上下文(JSON):{image_task_json}"), format!("- 当前模式:{mode}。"), format!("- 当前入口来源:{entry_source}。"), diff --git a/src-tauri/src/commands/aster_agent_cmd/mod.rs b/src-tauri/src/commands/aster_agent_cmd/mod.rs index ee162629b..ef5ea5a60 100644 --- a/src-tauri/src/commands/aster_agent_cmd/mod.rs +++ b/src-tauri/src/commands/aster_agent_cmd/mod.rs @@ -24,9 +24,10 @@ use crate::agent_tools::catalog::{ LIME_CREATE_IMAGE_TASK_TOOL_NAME, LIME_CREATE_RESOURCE_SEARCH_TASK_TOOL_NAME, LIME_CREATE_TRANSCRIPTION_TASK_TOOL_NAME, LIME_CREATE_TYPESETTING_TASK_TOOL_NAME, LIME_CREATE_URL_PARSE_TASK_TOOL_NAME, LIME_CREATE_VIDEO_TASK_TOOL_NAME, - LIME_SITE_INFO_TOOL_NAME, LIME_SITE_LIST_TOOL_NAME, LIME_SITE_RECOMMEND_TOOL_NAME, - LIME_SITE_RUN_TOOL_NAME, LIME_SITE_SEARCH_TOOL_NAME, LIST_MCP_RESOURCES_TOOL_NAME, - READ_MCP_RESOURCE_TOOL_NAME, SOCIAL_IMAGE_TOOL_NAME, TOOL_SEARCH_TOOL_NAME, + LIME_SEARCH_WEB_IMAGES_TOOL_NAME, LIME_SITE_INFO_TOOL_NAME, LIME_SITE_LIST_TOOL_NAME, + LIME_SITE_RECOMMEND_TOOL_NAME, LIME_SITE_RUN_TOOL_NAME, LIME_SITE_SEARCH_TOOL_NAME, + LIST_MCP_RESOURCES_TOOL_NAME, READ_MCP_RESOURCE_TOOL_NAME, SOCIAL_IMAGE_TOOL_NAME, + TOOL_SEARCH_TOOL_NAME, }; #[cfg(test)] use crate::agent_tools::execution::build_workspace_shell_allow_pattern; diff --git a/src-tauri/src/commands/aster_agent_cmd/runtime_project_hooks.rs b/src-tauri/src/commands/aster_agent_cmd/runtime_project_hooks.rs index e6b61133f..0cc9cf28e 100644 --- a/src-tauri/src/commands/aster_agent_cmd/runtime_project_hooks.rs +++ b/src-tauri/src/commands/aster_agent_cmd/runtime_project_hooks.rs @@ -747,11 +747,13 @@ fn parse_runtime_permission_request_hook_updated_permissions( ); }; if destination != "session" { - return Some(RuntimePermissionRequestHookUpdatedPermissions::Unsupported { - reason: format!( - "updatedPermissions[{index}] 目标 `{destination}` 当前无 honest host;仅支持 destination=session" - ), - }); + return Some( + RuntimePermissionRequestHookUpdatedPermissions::Unsupported { + reason: format!( + "updatedPermissions[{index}] 目标 `{destination}` 当前无 honest host;仅支持 destination=session" + ), + }, + ); } let Some(mode) = object.get("mode").and_then(serde_json::Value::as_str) else { diff --git a/src-tauri/src/commands/aster_agent_cmd/subagent_runtime.rs b/src-tauri/src/commands/aster_agent_cmd/subagent_runtime.rs index f3a5b498d..982648feb 100644 --- a/src-tauri/src/commands/aster_agent_cmd/subagent_runtime.rs +++ b/src-tauri/src/commands/aster_agent_cmd/subagent_runtime.rs @@ -1694,8 +1694,7 @@ async fn maybe_cleanup_subagent_worktree_after_runtime_event( "[AsterAgent][Subagent] child session={} 已结束,但 worktree 含改动,保留现状以便后续继续", session_id ), - SubagentWorktreeCloseOutcome::CleanupFailed - | SubagentWorktreeCloseOutcome::NotEnabled => {} + SubagentWorktreeCloseOutcome::CleanupFailed | SubagentWorktreeCloseOutcome::NotEnabled => {} } Ok(()) diff --git a/src-tauri/src/commands/aster_agent_cmd/tests.rs b/src-tauri/src/commands/aster_agent_cmd/tests.rs index ee0c466cf..dfb6acf82 100644 --- a/src-tauri/src/commands/aster_agent_cmd/tests.rs +++ b/src-tauri/src/commands/aster_agent_cmd/tests.rs @@ -17,9 +17,9 @@ mod tests { }; use crate::tests::runtime_test_support::shared_aster_runtime_test_root; use async_trait::async_trait; - use base64::{engine::general_purpose::STANDARD, Engine as _}; - use lime_agent::request_tool_policy::resolve_request_tool_policy; + use base64::{Engine as _, engine::general_purpose::STANDARD}; use lime_agent::AgentEvent as RuntimeAgentEvent; + use lime_agent::request_tool_policy::resolve_request_tool_policy; use regex::Regex; use std::collections::HashSet; use std::ffi::OsString; @@ -916,8 +916,8 @@ mod tests { } #[test] - fn test_append_fast_chat_request_tool_policy_session_permissions_blocks_web_tools_when_disabled( - ) { + fn test_append_fast_chat_request_tool_policy_session_permissions_blocks_web_tools_when_disabled() + { let policy = resolve_request_tool_policy(Some(false), false); let mut permissions = Vec::new(); @@ -943,14 +943,16 @@ mod tests { web_search_rule.conditions[0].value, serde_json::json!("session-fast-chat-1") ); - assert!(permissions - .iter() - .any(|permission| permission.tool == "WebFetch" && !permission.allowed)); + assert!( + permissions + .iter() + .any(|permission| permission.tool == "WebFetch" && !permission.allowed) + ); } #[test] - fn test_append_fast_chat_request_tool_policy_session_permissions_only_applies_to_disabled_fast_chat( - ) { + fn test_append_fast_chat_request_tool_policy_session_permissions_only_applies_to_disabled_fast_chat() + { let disabled_policy = resolve_request_tool_policy(Some(false), false); let allowed_policy = resolve_request_tool_policy(Some(true), false); let mut permissions = Vec::new(); @@ -1103,8 +1105,8 @@ mod tests { } #[test] - fn test_prune_fast_chat_request_tool_policy_tools_from_registry_keeps_web_tools_for_non_fast_chat_or_enabled_search( - ) { + fn test_prune_fast_chat_request_tool_policy_tools_from_registry_keeps_web_tools_for_non_fast_chat_or_enabled_search() + { let disabled_policy = resolve_request_tool_policy(Some(false), false); let allowed_policy = resolve_request_tool_policy(Some(true), false); @@ -1293,27 +1295,44 @@ mod tests { assert!(!permissions.iter().any(|permission| permission.tool == LIME_CREATE_IMAGE_TASK_TOOL_NAME && !permission.allowed)); - assert!(permissions - .iter() - .any(|permission| permission.tool == "Bash" && !permission.allowed)); - assert!(permissions - .iter() - .any(|permission| permission.tool == "Read" && !permission.allowed)); - assert!(permissions - .iter() - .any(|permission| permission.tool == "Write" && !permission.allowed)); - assert!(permissions - .iter() - .any(|permission| permission.tool == "Edit" && !permission.allowed)); - assert!(permissions - .iter() - .any(|permission| permission.tool == "Glob" && !permission.allowed)); - assert!(permissions - .iter() - .any(|permission| permission.tool == "Grep" && !permission.allowed)); - assert!(permissions - .iter() - .any(|permission| permission.tool == "mcp__lime-browser__*" && !permission.allowed)); + assert!( + permissions + .iter() + .any(|permission| permission.tool == "Bash" && !permission.allowed) + ); + assert!( + permissions + .iter() + .any(|permission| permission.tool == "Read" && !permission.allowed) + ); + assert!( + permissions + .iter() + .any(|permission| permission.tool == "Write" && !permission.allowed) + ); + assert!( + permissions + .iter() + .any(|permission| permission.tool == "Edit" && !permission.allowed) + ); + assert!( + permissions + .iter() + .any(|permission| permission.tool == "Glob" && !permission.allowed) + ); + assert!( + permissions + .iter() + .any(|permission| permission.tool == "Grep" && !permission.allowed) + ); + assert!( + permissions + .iter() + .any(|permission| permission.tool == "mcp__lime-browser__*" && !permission.allowed) + ); + assert!(permissions.iter().any(|permission| permission.tool + == LIME_SEARCH_WEB_IMAGES_TOOL_NAME + && !permission.allowed)); } #[test] @@ -1355,6 +1374,11 @@ mod tests { "Create image task", serde_json::json!({"type": "object"}), ))); + registry.register(Box::new(DummyTool::new( + LIME_SEARCH_WEB_IMAGES_TOOL_NAME, + "Search web images", + serde_json::json!({"type": "object"}), + ))); registry.register(Box::new(DummyTool::new( "Bash", "Run shell", @@ -1379,6 +1403,7 @@ mod tests { assert!(!registry.contains("Read")); assert!(!registry.contains("Write")); assert!(!registry.contains("Glob")); + assert!(!registry.contains(LIME_SEARCH_WEB_IMAGES_TOOL_NAME)); assert!(!registry.contains("mcp__lime-browser__tabs_context_mcp")); assert!(registry.contains("Skill")); } @@ -1416,36 +1441,52 @@ mod tests { serde_json::json!("session-cover-skill-1") ); - assert!(permissions - .iter() - .any(|permission| permission.tool == "Read" && !permission.allowed)); - assert!(permissions - .iter() - .any(|permission| permission.tool == "Write" && !permission.allowed)); - assert!(permissions - .iter() - .any(|permission| permission.tool == "Edit" && !permission.allowed)); - assert!(permissions - .iter() - .any(|permission| permission.tool == "Bash" && !permission.allowed)); - assert!(permissions - .iter() - .any(|permission| permission.tool == "Glob" && !permission.allowed)); - assert!(permissions - .iter() - .any(|permission| permission.tool == "Grep" && !permission.allowed)); - assert!(permissions - .iter() - .any(|permission| permission.tool == "mcp__lime-browser__*" && !permission.allowed)); + assert!( + permissions + .iter() + .any(|permission| permission.tool == "Read" && !permission.allowed) + ); + assert!( + permissions + .iter() + .any(|permission| permission.tool == "Write" && !permission.allowed) + ); + assert!( + permissions + .iter() + .any(|permission| permission.tool == "Edit" && !permission.allowed) + ); + assert!( + permissions + .iter() + .any(|permission| permission.tool == "Bash" && !permission.allowed) + ); + assert!( + permissions + .iter() + .any(|permission| permission.tool == "Glob" && !permission.allowed) + ); + assert!( + permissions + .iter() + .any(|permission| permission.tool == "Grep" && !permission.allowed) + ); + assert!( + permissions + .iter() + .any(|permission| permission.tool == "mcp__lime-browser__*" && !permission.allowed) + ); assert!(permissions.iter().any(|permission| permission.tool == "social_generate_cover_image" && !permission.allowed)); assert!(permissions.iter().any(|permission| permission.tool == LIME_CREATE_IMAGE_TASK_TOOL_NAME && !permission.allowed)); - assert!(permissions - .iter() - .any(|permission| permission.tool == "TaskOutput" && !permission.allowed)); + assert!( + permissions + .iter() + .any(|permission| permission.tool == "TaskOutput" && !permission.allowed) + ); } #[test] @@ -1560,15 +1601,21 @@ mod tests { serde_json::json!("session-video-skill-1") ); - assert!(permissions - .iter() - .any(|permission| permission.tool == "Read" && !permission.allowed)); - assert!(permissions - .iter() - .any(|permission| permission.tool == "Glob" && !permission.allowed)); - assert!(permissions - .iter() - .any(|permission| permission.tool == "Grep" && !permission.allowed)); + assert!( + permissions + .iter() + .any(|permission| permission.tool == "Read" && !permission.allowed) + ); + assert!( + permissions + .iter() + .any(|permission| permission.tool == "Glob" && !permission.allowed) + ); + assert!( + permissions + .iter() + .any(|permission| permission.tool == "Grep" && !permission.allowed) + ); } #[test] @@ -1647,15 +1694,21 @@ mod tests { serde_json::json!("session-broadcast-skill-1") ); - assert!(permissions - .iter() - .any(|permission| permission.tool == "Read" && !permission.allowed)); - assert!(permissions - .iter() - .any(|permission| permission.tool == "Glob" && !permission.allowed)); - assert!(permissions - .iter() - .any(|permission| permission.tool == "Grep" && !permission.allowed)); + assert!( + permissions + .iter() + .any(|permission| permission.tool == "Read" && !permission.allowed) + ); + assert!( + permissions + .iter() + .any(|permission| permission.tool == "Glob" && !permission.allowed) + ); + assert!( + permissions + .iter() + .any(|permission| permission.tool == "Grep" && !permission.allowed) + ); } #[test] @@ -1736,20 +1789,26 @@ mod tests { serde_json::json!("session-resource-search-skill-1") ); - assert!(permissions - .iter() - .any(|permission| permission.tool == "Read" && !permission.allowed)); - assert!(permissions - .iter() - .any(|permission| permission.tool == "Glob" && !permission.allowed)); - assert!(permissions - .iter() - .any(|permission| permission.tool == "Grep" && !permission.allowed)); + assert!( + permissions + .iter() + .any(|permission| permission.tool == "Read" && !permission.allowed) + ); + assert!( + permissions + .iter() + .any(|permission| permission.tool == "Glob" && !permission.allowed) + ); + assert!( + permissions + .iter() + .any(|permission| permission.tool == "Grep" && !permission.allowed) + ); } #[test] - fn test_prune_resource_search_skill_launch_detour_tools_from_registry_hides_tool_search_and_fs_tools( - ) { + fn test_prune_resource_search_skill_launch_detour_tools_from_registry_hides_tool_search_and_fs_tools() + { let metadata = serde_json::json!({ "harness": { "resource_search_skill_launch": { @@ -1828,15 +1887,21 @@ mod tests { serde_json::json!("session-research-skill-1") ); - assert!(permissions - .iter() - .any(|permission| permission.tool == "Read" && !permission.allowed)); - assert!(permissions - .iter() - .any(|permission| permission.tool == "Glob" && !permission.allowed)); - assert!(permissions - .iter() - .any(|permission| permission.tool == "Grep" && !permission.allowed)); + assert!( + permissions + .iter() + .any(|permission| permission.tool == "Read" && !permission.allowed) + ); + assert!( + permissions + .iter() + .any(|permission| permission.tool == "Glob" && !permission.allowed) + ); + assert!( + permissions + .iter() + .any(|permission| permission.tool == "Grep" && !permission.allowed) + ); } #[test] @@ -1922,20 +1987,26 @@ mod tests { serde_json::json!("session-deep-search-skill-1") ); - assert!(permissions - .iter() - .any(|permission| permission.tool == "Read" && !permission.allowed)); - assert!(permissions - .iter() - .any(|permission| permission.tool == "Glob" && !permission.allowed)); - assert!(permissions - .iter() - .any(|permission| permission.tool == "Grep" && !permission.allowed)); + assert!( + permissions + .iter() + .any(|permission| permission.tool == "Read" && !permission.allowed) + ); + assert!( + permissions + .iter() + .any(|permission| permission.tool == "Glob" && !permission.allowed) + ); + assert!( + permissions + .iter() + .any(|permission| permission.tool == "Grep" && !permission.allowed) + ); } #[test] - fn test_prune_deep_search_skill_launch_detour_tools_from_registry_hides_tool_search_and_fs_tools( - ) { + fn test_prune_deep_search_skill_launch_detour_tools_from_registry_hides_tool_search_and_fs_tools() + { let metadata = serde_json::json!({ "harness": { "deep_search_skill_launch": { @@ -2016,15 +2087,21 @@ mod tests { serde_json::json!("session-report-skill-1") ); - assert!(permissions - .iter() - .any(|permission| permission.tool == "Read" && !permission.allowed)); - assert!(permissions - .iter() - .any(|permission| permission.tool == "Glob" && !permission.allowed)); - assert!(permissions - .iter() - .any(|permission| permission.tool == "Grep" && !permission.allowed)); + assert!( + permissions + .iter() + .any(|permission| permission.tool == "Read" && !permission.allowed) + ); + assert!( + permissions + .iter() + .any(|permission| permission.tool == "Glob" && !permission.allowed) + ); + assert!( + permissions + .iter() + .any(|permission| permission.tool == "Grep" && !permission.allowed) + ); } #[test] @@ -2116,23 +2193,31 @@ mod tests { .expect("should add browser compat deny rule"); assert!(!browser_deny_rule.allowed); - assert!(permissions - .iter() - .any(|permission| permission.tool == "WebSearch" && !permission.allowed)); - assert!(permissions - .iter() - .any(|permission| permission.tool == "Read" && !permission.allowed)); - assert!(permissions - .iter() - .any(|permission| permission.tool == "Glob" && !permission.allowed)); - assert!(permissions - .iter() - .any(|permission| permission.tool == "Grep" && !permission.allowed)); + assert!( + permissions + .iter() + .any(|permission| permission.tool == "WebSearch" && !permission.allowed) + ); + assert!( + permissions + .iter() + .any(|permission| permission.tool == "Read" && !permission.allowed) + ); + assert!( + permissions + .iter() + .any(|permission| permission.tool == "Glob" && !permission.allowed) + ); + assert!( + permissions + .iter() + .any(|permission| permission.tool == "Grep" && !permission.allowed) + ); } #[test] - fn test_prune_site_search_skill_launch_detour_tools_from_registry_hides_tool_search_and_fs_tools( - ) { + fn test_prune_site_search_skill_launch_detour_tools_from_registry_hides_tool_search_and_fs_tools() + { let metadata = serde_json::json!({ "harness": { "site_search_skill_launch": { @@ -2220,18 +2305,26 @@ mod tests { serde_json::json!("session-pdf-read-skill-1") ); - assert!(permissions - .iter() - .any(|permission| permission.tool == "WebSearch" && !permission.allowed)); - assert!(permissions - .iter() - .any(|permission| permission.tool == "Grep" && !permission.allowed)); - assert!(!permissions - .iter() - .any(|permission| permission.tool == "Read" && !permission.allowed)); - assert!(!permissions - .iter() - .any(|permission| permission.tool == "Glob" && !permission.allowed)); + assert!( + permissions + .iter() + .any(|permission| permission.tool == "WebSearch" && !permission.allowed) + ); + assert!( + permissions + .iter() + .any(|permission| permission.tool == "Grep" && !permission.allowed) + ); + assert!( + !permissions + .iter() + .any(|permission| permission.tool == "Read" && !permission.allowed) + ); + assert!( + !permissions + .iter() + .any(|permission| permission.tool == "Glob" && !permission.allowed) + ); } #[test] @@ -2323,18 +2416,26 @@ mod tests { serde_json::json!("session-summary-skill-1") ); - assert!(permissions - .iter() - .any(|permission| permission.tool == "WebSearch" && !permission.allowed)); - assert!(permissions - .iter() - .any(|permission| permission.tool == "Grep" && !permission.allowed)); - assert!(!permissions - .iter() - .any(|permission| permission.tool == "Read" && !permission.allowed)); - assert!(!permissions - .iter() - .any(|permission| permission.tool == "Glob" && !permission.allowed)); + assert!( + permissions + .iter() + .any(|permission| permission.tool == "WebSearch" && !permission.allowed) + ); + assert!( + permissions + .iter() + .any(|permission| permission.tool == "Grep" && !permission.allowed) + ); + assert!( + !permissions + .iter() + .any(|permission| permission.tool == "Read" && !permission.allowed) + ); + assert!( + !permissions + .iter() + .any(|permission| permission.tool == "Glob" && !permission.allowed) + ); } #[test] @@ -2427,18 +2528,26 @@ mod tests { serde_json::json!("session-translation-skill-1") ); - assert!(permissions - .iter() - .any(|permission| permission.tool == "WebSearch" && !permission.allowed)); - assert!(permissions - .iter() - .any(|permission| permission.tool == "Grep" && !permission.allowed)); - assert!(!permissions - .iter() - .any(|permission| permission.tool == "Read" && !permission.allowed)); - assert!(!permissions - .iter() - .any(|permission| permission.tool == "Glob" && !permission.allowed)); + assert!( + permissions + .iter() + .any(|permission| permission.tool == "WebSearch" && !permission.allowed) + ); + assert!( + permissions + .iter() + .any(|permission| permission.tool == "Grep" && !permission.allowed) + ); + assert!( + !permissions + .iter() + .any(|permission| permission.tool == "Read" && !permission.allowed) + ); + assert!( + !permissions + .iter() + .any(|permission| permission.tool == "Glob" && !permission.allowed) + ); } #[test] @@ -2531,18 +2640,26 @@ mod tests { serde_json::json!("session-analysis-skill-1") ); - assert!(permissions - .iter() - .any(|permission| permission.tool == "WebSearch" && !permission.allowed)); - assert!(permissions - .iter() - .any(|permission| permission.tool == "Grep" && !permission.allowed)); - assert!(!permissions - .iter() - .any(|permission| permission.tool == "Read" && !permission.allowed)); - assert!(!permissions - .iter() - .any(|permission| permission.tool == "Glob" && !permission.allowed)); + assert!( + permissions + .iter() + .any(|permission| permission.tool == "WebSearch" && !permission.allowed) + ); + assert!( + permissions + .iter() + .any(|permission| permission.tool == "Grep" && !permission.allowed) + ); + assert!( + !permissions + .iter() + .any(|permission| permission.tool == "Read" && !permission.allowed) + ); + assert!( + !permissions + .iter() + .any(|permission| permission.tool == "Glob" && !permission.allowed) + ); } #[test] @@ -2635,23 +2752,31 @@ mod tests { serde_json::json!("session-transcription-skill-1") ); - assert!(permissions - .iter() - .any(|permission| permission.tool == "WebSearch" && !permission.allowed)); - assert!(permissions - .iter() - .any(|permission| permission.tool == "Read" && !permission.allowed)); - assert!(permissions - .iter() - .any(|permission| permission.tool == "Glob" && !permission.allowed)); - assert!(permissions - .iter() - .any(|permission| permission.tool == "Grep" && !permission.allowed)); + assert!( + permissions + .iter() + .any(|permission| permission.tool == "WebSearch" && !permission.allowed) + ); + assert!( + permissions + .iter() + .any(|permission| permission.tool == "Read" && !permission.allowed) + ); + assert!( + permissions + .iter() + .any(|permission| permission.tool == "Glob" && !permission.allowed) + ); + assert!( + permissions + .iter() + .any(|permission| permission.tool == "Grep" && !permission.allowed) + ); } #[test] - fn test_prune_transcription_skill_launch_detour_tools_from_registry_hides_tool_search_and_fs_tools( - ) { + fn test_prune_transcription_skill_launch_detour_tools_from_registry_hides_tool_search_and_fs_tools() + { let metadata = serde_json::json!({ "harness": { "transcription_skill_launch": { @@ -2740,18 +2865,26 @@ mod tests { serde_json::json!("session-url-parse-skill-1") ); - assert!(permissions - .iter() - .any(|permission| permission.tool == "WebSearch" && !permission.allowed)); - assert!(permissions - .iter() - .any(|permission| permission.tool == "Read" && !permission.allowed)); - assert!(permissions - .iter() - .any(|permission| permission.tool == "Glob" && !permission.allowed)); - assert!(permissions - .iter() - .any(|permission| permission.tool == "Grep" && !permission.allowed)); + assert!( + permissions + .iter() + .any(|permission| permission.tool == "WebSearch" && !permission.allowed) + ); + assert!( + permissions + .iter() + .any(|permission| permission.tool == "Read" && !permission.allowed) + ); + assert!( + permissions + .iter() + .any(|permission| permission.tool == "Glob" && !permission.allowed) + ); + assert!( + permissions + .iter() + .any(|permission| permission.tool == "Grep" && !permission.allowed) + ); } #[test] @@ -2846,23 +2979,31 @@ mod tests { serde_json::json!("session-typesetting-skill-1") ); - assert!(permissions - .iter() - .any(|permission| permission.tool == "WebSearch" && !permission.allowed)); - assert!(permissions - .iter() - .any(|permission| permission.tool == "Read" && !permission.allowed)); - assert!(permissions - .iter() - .any(|permission| permission.tool == "Glob" && !permission.allowed)); - assert!(permissions - .iter() - .any(|permission| permission.tool == "Grep" && !permission.allowed)); + assert!( + permissions + .iter() + .any(|permission| permission.tool == "WebSearch" && !permission.allowed) + ); + assert!( + permissions + .iter() + .any(|permission| permission.tool == "Read" && !permission.allowed) + ); + assert!( + permissions + .iter() + .any(|permission| permission.tool == "Glob" && !permission.allowed) + ); + assert!( + permissions + .iter() + .any(|permission| permission.tool == "Grep" && !permission.allowed) + ); } #[test] - fn test_prune_typesetting_skill_launch_detour_tools_from_registry_hides_tool_search_and_fs_tools( - ) { + fn test_prune_typesetting_skill_launch_detour_tools_from_registry_hides_tool_search_and_fs_tools() + { let metadata = serde_json::json!({ "harness": { "typesetting_skill_launch": { @@ -2956,23 +3097,31 @@ mod tests { serde_json::json!("session-presentation-skill-1") ); - assert!(permissions - .iter() - .any(|permission| permission.tool == "WebSearch" && !permission.allowed)); - assert!(permissions - .iter() - .any(|permission| permission.tool == "Read" && !permission.allowed)); - assert!(permissions - .iter() - .any(|permission| permission.tool == "Glob" && !permission.allowed)); - assert!(permissions - .iter() - .any(|permission| permission.tool == "Grep" && !permission.allowed)); + assert!( + permissions + .iter() + .any(|permission| permission.tool == "WebSearch" && !permission.allowed) + ); + assert!( + permissions + .iter() + .any(|permission| permission.tool == "Read" && !permission.allowed) + ); + assert!( + permissions + .iter() + .any(|permission| permission.tool == "Glob" && !permission.allowed) + ); + assert!( + permissions + .iter() + .any(|permission| permission.tool == "Grep" && !permission.allowed) + ); } #[test] - fn test_prune_presentation_skill_launch_detour_tools_from_registry_hides_tool_search_and_fs_tools( - ) { + fn test_prune_presentation_skill_launch_detour_tools_from_registry_hides_tool_search_and_fs_tools() + { let metadata = serde_json::json!({ "harness": { "presentation_skill_launch": { @@ -3065,18 +3214,26 @@ mod tests { serde_json::json!("session-form-skill-1") ); - assert!(permissions - .iter() - .any(|permission| permission.tool == "WebSearch" && !permission.allowed)); - assert!(permissions - .iter() - .any(|permission| permission.tool == "Read" && !permission.allowed)); - assert!(permissions - .iter() - .any(|permission| permission.tool == "Glob" && !permission.allowed)); - assert!(permissions - .iter() - .any(|permission| permission.tool == "Grep" && !permission.allowed)); + assert!( + permissions + .iter() + .any(|permission| permission.tool == "WebSearch" && !permission.allowed) + ); + assert!( + permissions + .iter() + .any(|permission| permission.tool == "Read" && !permission.allowed) + ); + assert!( + permissions + .iter() + .any(|permission| permission.tool == "Glob" && !permission.allowed) + ); + assert!( + permissions + .iter() + .any(|permission| permission.tool == "Grep" && !permission.allowed) + ); } #[test] @@ -3171,18 +3328,26 @@ mod tests { serde_json::json!("session-webpage-skill-1") ); - assert!(permissions - .iter() - .any(|permission| permission.tool == "WebSearch" && !permission.allowed)); - assert!(permissions - .iter() - .any(|permission| permission.tool == "Read" && !permission.allowed)); - assert!(permissions - .iter() - .any(|permission| permission.tool == "Glob" && !permission.allowed)); - assert!(permissions - .iter() - .any(|permission| permission.tool == "Grep" && !permission.allowed)); + assert!( + permissions + .iter() + .any(|permission| permission.tool == "WebSearch" && !permission.allowed) + ); + assert!( + permissions + .iter() + .any(|permission| permission.tool == "Read" && !permission.allowed) + ); + assert!( + permissions + .iter() + .any(|permission| permission.tool == "Glob" && !permission.allowed) + ); + assert!( + permissions + .iter() + .any(|permission| permission.tool == "Grep" && !permission.allowed) + ); } #[test] @@ -4915,7 +5080,8 @@ mod tests { assert!(merged.contains("\"image_task\":")); assert!(merged.contains("不要为了确认技能名、工具名或命令名再去调用 ToolSearch")); assert!(merged.contains("当前主会话第一刀必须先调用 Skill(image_generate)")); - assert!(merged.contains("不要先走 ToolSearch / WebSearch / Bash / Read / Write / Edit / Glob / Grep / 浏览器 MCP / Playwright")); + assert!(merged.contains("不要先走 ToolSearch / WebSearch / lime_search_web_images / Bash / Read / Write / Edit / Glob / Grep / 浏览器 MCP / Playwright")); + assert!(merged.contains("`lime_search_web_images` 只服务 @素材")); assert!(merged.contains("应立即改为直调 Skill(image_generate)")); assert!(merged.contains( "如果 Skill(image_generate) 返回的 Lime 工具元数据里只有 allowed_tools=[\"lime_create_image_generation_task\"]" @@ -4928,6 +5094,8 @@ mod tests { )); assert!(merged.contains("不要伪造“图片已生成完成”")); assert!(merged.contains("不要再次调用 Skill(image_generate) 或重复创建第二个图片任务")); + assert!(merged.contains("不要再输出“任务类型 / 任务 ID / 任务文件 / 状态”这类提交摘要")); + assert!(!merged.contains("给出提交摘要")); assert!(merged.contains("provider_id / model")); assert!(merged.contains( "count / layout_hint / session_id / project_id / raw_text / usage / size / requested_target / reference_images" @@ -4940,6 +5108,30 @@ mod tests { assert!(merged.contains("当前任务已经显式进入图片技能主链")); } + #[test] + fn test_merge_system_prompt_with_image_skill_launch_adds_plain_chat_confirmation_guard() { + let metadata = serde_json::json!({ + "harness": { + "theme": "general", + "session_mode": "general_workbench", + "allow_model_skills": true + } + }); + + let merged = merge_system_prompt_with_image_skill_launch( + Some("你是助手".to_string()), + Some(&metadata), + ) + .expect("should contain guard prompt"); + + assert!(merged.contains("<>")); + assert!(merged.contains("不要因此自动调用 Skill(image_generate)")); + assert!(merged.contains("是否要调用画图功能生成图片")); + assert!(merged.contains("不要输出 HTML/CSS/SVG/Markdown 草图")); + assert!(merged.contains("这看起来是图片生成需求,要我直接调用画图功能生成吗?")); + assert!(!merged.contains(super::image_skill_launch::IMAGE_SKILL_LAUNCH_PROMPT_MARKER)); + } + #[test] fn test_merge_system_prompt_with_cover_skill_launch_appends_prompt() { let metadata = serde_json::json!({ @@ -6436,8 +6628,11 @@ mod tests { assert!(merged.contains("不要先走 ToolSearch / WebSearch / Read / Glob / Grep")); assert!(merged.contains("目标是复用 Lime 现有 A2UI 协议输出一份真实可渲染的表单")); assert!(merged.contains("最终结果必须输出一个 ```a2ui 代码块")); - assert!(merged - .contains("字段类型只允许使用 simple form 已支持的 choice / text / slider / checkbox")); + assert!( + merged.contains( + "字段类型只允许使用 simple form 已支持的 choice / text / slider / checkbox" + ) + ); } #[test] @@ -6735,8 +6930,8 @@ mod tests { } #[test] - fn test_merge_system_prompt_with_service_skill_launch_includes_missing_session_failure_contract( - ) { + fn test_merge_system_prompt_with_service_skill_launch_includes_missing_session_failure_contract() + { let metadata = serde_json::json!({ "harness": { "service_skill_launch": { @@ -7035,8 +7230,8 @@ mod tests { } #[test] - fn test_merge_system_prompt_with_service_skill_launch_preload_adds_markdown_bundle_translation_contract( - ) { + fn test_merge_system_prompt_with_service_skill_launch_preload_adds_markdown_bundle_translation_contract() + { let execution = ServiceSkillLaunchPreloadExecution { request: RunSiteAdapterRequest { adapter_name: "x/article-export".to_string(), @@ -7258,12 +7453,16 @@ mod tests { assert_eq!(projection.tool_name, "lime_site_run"); assert!(projection.tool_id.starts_with("service-skill-preload:")); - assert!(projection - .arguments - .contains("\"execution_origin\":\"preload\"")); - assert!(projection - .arguments - .contains("\"skill_title\":\"X 文章转存\"")); + assert!( + projection + .arguments + .contains("\"execution_origin\":\"preload\"") + ); + assert!( + projection + .arguments + .contains("\"skill_title\":\"X 文章转存\"") + ); assert!(projection.result.success); assert!(projection.result.output.contains("已完成站点技能预执行")); assert!(projection.result.output.contains("图片资源:2 张")); @@ -7367,11 +7566,13 @@ mod tests { let permission = PermissionCheckResult::ask("需要确认"); let normalized = normalize_workspace_tool_permission_behavior(permission, false); assert_eq!(normalized.behavior, PermissionBehavior::Deny); - assert!(normalized - .message - .as_deref() - .unwrap_or_default() - .contains("当前模式不支持交互确认")); + assert!( + normalized + .message + .as_deref() + .unwrap_or_default() + .contains("当前模式不支持交互确认") + ); } #[test] @@ -7727,8 +7928,8 @@ mod tests { } #[test] - fn test_build_team_preference_system_prompt_prefers_request_metadata_over_session_recent_team_selection( - ) { + fn test_build_team_preference_system_prompt_prefers_request_metadata_over_session_recent_team_selection() + { let prompt = build_team_preference_system_prompt( Some(&serde_json::json!({ "harness": { @@ -7818,20 +8019,28 @@ mod tests { Some("code-triage-team") ); assert_eq!(customization.theme.as_deref(), Some("engineering")); - assert!(customization - .skill_ids - .contains(&"repo-exploration".to_string())); - assert!(customization - .skill_ids - .contains(&"source-grounding".to_string())); - assert!(customization - .skill_ids - .contains(&"verification-report".to_string())); - assert!(customization - .hooks - .as_ref() - .and_then(|hooks| hooks.get(&aster::hooks::HookEvent::Stop)) - .is_some()); + assert!( + customization + .skill_ids + .contains(&"repo-exploration".to_string()) + ); + assert!( + customization + .skill_ids + .contains(&"source-grounding".to_string()) + ); + assert!( + customization + .skill_ids + .contains(&"verification-report".to_string()) + ); + assert!( + customization + .hooks + .as_ref() + .and_then(|hooks| hooks.get(&aster::hooks::HookEvent::Stop)) + .is_some() + ); assert_eq!(customization.allowed_tools, vec!["Read", "Bash"]); assert_eq!(customization.disallowed_tools, vec!["WebSearch"]); } @@ -7979,16 +8188,20 @@ mod tests { let encoded = encode_tool_result_for_harness_observability(result); assert!(encoded.success); - assert!(encoded - .output - .as_deref() - .unwrap_or_default() - .contains(LIME_TOOL_METADATA_BEGIN)); - assert!(encoded - .output - .as_deref() - .unwrap_or_default() - .contains("\"output_file\":\"/tmp/task.log\"")); + assert!( + encoded + .output + .as_deref() + .unwrap_or_default() + .contains(LIME_TOOL_METADATA_BEGIN) + ); + assert!( + encoded + .output + .as_deref() + .unwrap_or_default() + .contains("\"output_file\":\"/tmp/task.log\"") + ); } #[test] @@ -8289,10 +8502,12 @@ mod tests { let result = SocialGenerateCoverImageTool::extract_first_image_payload(&response); assert!(result.is_err()); - assert!(result - .err() - .unwrap_or_default() - .contains("图像接口返回 data 为空")); + assert!( + result + .err() + .unwrap_or_default() + .contains("图像接口返回 data 为空") + ); } #[test] @@ -8396,10 +8611,12 @@ mod tests { assert_eq!(tools[0]["name"], serde_json::json!("docs_search")); assert_eq!(tools[0]["deferred_loading"], serde_json::json!(true)); assert!(tools[0].get("input_schema").is_some()); - assert!(tools[0] - .get("input_examples") - .and_then(|v| v.as_array()) - .is_some()); + assert!( + tools[0] + .get("input_examples") + .and_then(|v| v.as_array()) + .is_some() + ); assert!(tools.iter().all(|tool| tool["name"] != "admin_secret")); } @@ -8411,11 +8628,13 @@ mod tests { guard.register(Box::new(aster::tools::ToolSearchTool::new( std::sync::Weak::new(), ))); - assert!(guard - .get("ToolSearch") - .expect("legacy ToolSearch should exist") - .description() - .contains("Fetches full schema definitions")); + assert!( + guard + .get("ToolSearch") + .expect("legacy ToolSearch should exist") + .description() + .contains("Fetches full schema definitions") + ); super::tool_runtime::register_tool_search_tool_to_registry( &mut guard, @@ -8462,9 +8681,11 @@ mod tests { let definitions = super::tool_runtime::list_current_surface_tool_definitions(&state).await; - assert!(definitions - .iter() - .any(|definition| definition.name == "Agent")); + assert!( + definitions + .iter() + .any(|definition| definition.name == "Agent") + ); }); } diff --git a/src-tauri/src/commands/aster_agent_cmd/tool_runtime/creation_tools.rs b/src-tauri/src/commands/aster_agent_cmd/tool_runtime/creation_tools.rs index 96329019a..3afc9c115 100644 --- a/src-tauri/src/commands/aster_agent_cmd/tool_runtime/creation_tools.rs +++ b/src-tauri/src/commands/aster_agent_cmd/tool_runtime/creation_tools.rs @@ -384,6 +384,102 @@ impl Tool for LimeCreateResourceSearchTaskTool { } } +fn parse_chinese_count_digit(value: char) -> Option { + match value { + '一' => Some(1), + '二' | '两' => Some(2), + '三' => Some(3), + '四' => Some(4), + '五' => Some(5), + '六' => Some(6), + '七' => Some(7), + '八' => Some(8), + '九' => Some(9), + _ => None, + } +} + +fn parse_chinese_count_number(value: &str) -> Option { + let trimmed = value.trim(); + if trimmed.is_empty() { + return None; + } + if trimmed == "十" { + return Some(10); + } + + let parts = trimmed.split_once('十'); + if let Some((left, right)) = parts { + let tens = if left.is_empty() { + 1 + } else { + let mut chars = left.chars(); + let digit = chars.next().and_then(parse_chinese_count_digit)?; + if chars.next().is_some() { + return None; + } + digit + }; + let ones = if right.is_empty() { + 0 + } else { + let mut chars = right.chars(); + let digit = chars.next().and_then(parse_chinese_count_digit)?; + if chars.next().is_some() { + return None; + } + digit + }; + return Some(tens * 10 + ones); + } + + let mut chars = trimmed.chars(); + let digit = chars.next().and_then(parse_chinese_count_digit)?; + if chars.next().is_some() { + return None; + } + Some(digit) +} + +fn parse_relaxed_count_string(value: &str) -> Option { + let trimmed = value.trim(); + if trimmed.is_empty() { + return None; + } + if let Ok(parsed) = trimmed.parse::() { + return (parsed > 0).then_some(parsed); + } + + let mut ascii_digits = String::new(); + for character in trimmed.chars() { + if character.is_ascii_digit() { + ascii_digits.push(character); + } else if !ascii_digits.is_empty() { + break; + } + } + if !ascii_digits.is_empty() { + if let Ok(parsed) = ascii_digits.parse::() { + if parsed > 0 { + return Some(parsed); + } + } + } + + let mut chinese_digits = String::new(); + for character in trimmed.chars() { + if matches!( + character, + '一' | '二' | '两' | '三' | '四' | '五' | '六' | '七' | '八' | '九' | '十' + ) { + chinese_digits.push(character); + } else if !chinese_digits.is_empty() { + break; + } + } + parse_chinese_count_number(&chinese_digits).filter(|count| *count > 0) +} + fn deserialize_optional_u32_from_any<'de, D>(deserializer: D) -> Result, D::Error> where D: Deserializer<'de>, @@ -397,6 +493,7 @@ where serde_json::Value::Number(number) => number .as_u64() .and_then(|raw| u32::try_from(raw).ok()) + .filter(|count| *count > 0) .map(Some) .ok_or_else(|| de::Error::custom("count 必须是正整数")), serde_json::Value::String(raw) => { @@ -404,10 +501,9 @@ where if trimmed.is_empty() { return Ok(None); } - trimmed - .parse::() + parse_relaxed_count_string(trimmed) .map(Some) - .map_err(|_| de::Error::custom("count 必须是正整数")) + .ok_or_else(|| de::Error::custom("count 必须是正整数")) } _ => Err(de::Error::custom("count 必须是整数或整数字符串")), } @@ -442,7 +538,7 @@ fn normalize_image_task_tool_params( _ => { return Err(ToolError::invalid_params( "image_task 必须是对象或 JSON 字符串".to_string(), - )) + )); } }; @@ -739,7 +835,7 @@ fn image_task_input_schema() -> serde_json::Value { { "type": "integer", "minimum": 1, "maximum": 20 }, { "type": "string" } ], - "description": "生成数量(可选,兼容整数字符串)。" + "description": "生成数量(可选,兼容整数字符串、`1 张`、`一张` 等自然表达)。" }); let reference_images_schema = serde_json::json!({ "oneOf": [ @@ -2422,6 +2518,28 @@ mod tests { assert!(input.reference_images.is_empty()); } + #[test] + fn image_task_input_should_parse_relaxed_count_strings() { + for (raw_count, expected_count) in [ + ("1 张", 1), + ("一张", 1), + ("2 images", 2), + ("十二张", 12), + ("二十张", 20), + ] { + let input: ImageTaskInput = serde_json::from_value(serde_json::json!({ + "prompt": "青柠实验室主视觉", + "count": raw_count + })) + .expect("parse relaxed image count"); + assert_eq!( + input.count, + Some(expected_count), + "raw count should parse: {raw_count}" + ); + } + } + #[test] fn normalize_flat_image_task_tool_params_should_canonicalize_duplicate_alias_fields() { let normalized = normalize_flat_image_task_tool_params(serde_json::json!({ diff --git a/src-tauri/src/commands/aster_agent_cmd/tool_runtime/media_cli_bridge.rs b/src-tauri/src/commands/aster_agent_cmd/tool_runtime/media_cli_bridge.rs index 0abe2256b..991309a42 100644 --- a/src-tauri/src/commands/aster_agent_cmd/tool_runtime/media_cli_bridge.rs +++ b/src-tauri/src/commands/aster_agent_cmd/tool_runtime/media_cli_bridge.rs @@ -90,6 +90,8 @@ pub(crate) fn emit_media_creation_task_event(app_handle: &AppHandle, output: &Me "storyboard_slots": payload_record.get("storyboard_slots").cloned(), "raw_text": read_payload_string(payload_record, &["raw_text", "rawText"]), "session_id": read_payload_string(payload_record, &["session_id", "sessionId"]), + "thread_id": read_payload_string(payload_record, &["thread_id", "threadId"]), + "turn_id": read_payload_string(payload_record, &["turn_id", "turnId"]), "project_id": read_payload_string(payload_record, &["project_id", "projectId"]), "content_id": read_payload_string(payload_record, &["content_id", "contentId"]), "entry_source": read_payload_string(payload_record, &["entry_source", "entrySource"]), @@ -165,6 +167,20 @@ pub(crate) fn attach_media_task_metadata( &["project_id", "projectId"] )), ) + .with_metadata( + "thread_id", + serde_json::json!(read_payload_string( + &output.record.payload, + &["thread_id", "threadId"] + )), + ) + .with_metadata( + "turn_id", + serde_json::json!(read_payload_string( + &output.record.payload, + &["turn_id", "turnId"] + )), + ) .with_metadata( "content_id", serde_json::json!(read_payload_string( @@ -295,7 +311,11 @@ mod tests { task_family: "image".to_string(), title: None, summary: Some("demo".to_string()), - payload: serde_json::json!({ "prompt": "demo" }), + payload: serde_json::json!({ + "prompt": "demo", + "thread_id": "thread-image-1", + "turn_id": "turn-image-1" + }), status: "pending_submit".to_string(), normalized_status: "pending".to_string(), created_at: "2026-04-03T00:00:00Z".to_string(), @@ -342,5 +362,13 @@ mod tests { result.metadata.get("artifact_paths"), Some(&serde_json::json!([".lime/tasks/image_generate/demo.json"])) ); + assert_eq!( + result.metadata.get("thread_id"), + Some(&serde_json::json!("thread-image-1")) + ); + assert_eq!( + result.metadata.get("turn_id"), + Some(&serde_json::json!("turn-image-1")) + ); } } diff --git a/src-tauri/src/commands/media_task_cmd.rs b/src-tauri/src/commands/media_task_cmd.rs index d999a5b0c..51dfbbc52 100644 --- a/src-tauri/src/commands/media_task_cmd.rs +++ b/src-tauri/src/commands/media_task_cmd.rs @@ -4752,12 +4752,12 @@ pub(crate) fn create_image_generation_task_artifact_inner( let mode = normalize_mode(request.mode.clone())?; let normalized_storyboard_slots = normalize_storyboard_slots(request.storyboard_slots.clone()); - let count = normalize_positive_count(Some( - request - .count - .unwrap_or(normalized_storyboard_slots.len() as u32) - .max(normalized_storyboard_slots.len() as u32), - ))?; + let requested_count = request.count.or_else(|| { + let slot_count = normalized_storyboard_slots.len() as u32; + (slot_count > 0).then_some(slot_count) + }); + let count = normalize_positive_count(requested_count) + .map(|count| count.max(normalized_storyboard_slots.len() as u32))?; let size = normalize_optional_string(request.size.clone()); let aspect_ratio = normalize_optional_string(request.aspect_ratio.clone()); let usage = normalize_optional_string(request.usage.clone()); @@ -5471,6 +5471,24 @@ mod tests { ); } + #[test] + fn image_generation_task_should_default_missing_count_to_one() { + let temp_dir = tempfile::tempdir().expect("create temp dir"); + let mut request = minimal_image_generation_request( + temp_dir.path().to_string_lossy().to_string(), + Some("gpt-image-1"), + ); + request.count = None; + + let created = create_image_generation_task_artifact_inner(request) + .expect("create image task without explicit count"); + + assert_eq!( + created.record.payload.get("count").and_then(Value::as_u64), + Some(1) + ); + } + #[test] fn modality_runtime_contract_index_should_derive_pending_refs_from_policy_value_hits() { let temp_dir = tempfile::tempdir().expect("create temp dir"); diff --git a/src-tauri/src/dev_bridge/dispatcher/skills.rs b/src-tauri/src/dev_bridge/dispatcher/skills.rs index 5bbc2e404..8c2e212ea 100644 --- a/src-tauri/src/dev_bridge/dispatcher/skills.rs +++ b/src-tauri/src/dev_bridge/dispatcher/skills.rs @@ -127,6 +127,49 @@ pub(super) async fn try_handle( .map_err(|e| format!("导入本地 Skill 失败: {e}"))?; serde_json::to_value(result)? } + "uninstall_skill" | "uninstall_skill_for_app" => { + let args = args_or_default(args); + let app = args + .get("app") + .and_then(|value| value.as_str()) + .unwrap_or("lime") + .to_string(); + let directory = get_string_arg(&args, "directory", "directory")?; + let app_type: crate::models::app_type::AppType = app.parse().map_err(|e: String| e)?; + + lime_services::skill_service::SkillService::uninstall_skill(&app_type, &directory) + .map_err(|e| format!("卸载本地 Skill 失败: {e}"))?; + + if let Some(db) = &state.db { + let key = format!("{}:{directory}", app_type.to_string().to_lowercase()); + let skill_state = crate::models::skill_model::SkillState { + installed: false, + installed_at: chrono::Utc::now(), + }; + let conn = db.lock().map_err(|e| format!("数据库锁定失败: {e}"))?; + crate::database::dao::skills::SkillDao::update_skill_state( + &conn, + &key, + &skill_state, + ) + .map_err(|e| format!("更新 Skill 安装状态失败: {e}"))?; + } + + if matches!(app_type, crate::models::app_type::AppType::Lime) { + crate::agent::AsterAgentState::reload_lime_skills(); + } + + serde_json::json!(true) + } + "get_installed_lime_skills" => serde_json::to_value( + crate::commands::skill_cmd::get_installed_lime_skills() + .await + .map_err(|e| format!("读取 Lime Skills 列表失败: {e}"))?, + )?, + "refresh_skill_cache" => { + state.skill_service.refresh_cache(); + serde_json::json!(true) + } "inspect_remote_skill" => { let args = args_or_default(args); let owner = get_string_arg(&args, "owner", "owner")?; diff --git a/src-tauri/tauri.conf.headless.json b/src-tauri/tauri.conf.headless.json index c3a13e0aa..b18ef0e29 100644 --- a/src-tauri/tauri.conf.headless.json +++ b/src-tauri/tauri.conf.headless.json @@ -1,7 +1,7 @@ { "$schema": "https://schema.tauri.app/config/2", "productName": "Lime", - "version": "1.36.0", + "version": "1.37.0", "identifier": "com.limecloud.lime.headless", "build": { "beforeDevCommand": "npm run dev:web-bridge", diff --git a/src-tauri/tauri.conf.json b/src-tauri/tauri.conf.json index e2dfefbcd..54af60a91 100644 --- a/src-tauri/tauri.conf.json +++ b/src-tauri/tauri.conf.json @@ -1,7 +1,7 @@ { "$schema": "https://schema.tauri.app/config/2", "productName": "Lime", - "version": "1.36.0", + "version": "1.37.0", "identifier": "com.limecloud.lime", "build": { "beforeDevCommand": "node scripts/start-tauri-dev-server.mjs", diff --git a/src/components/AppPageContent.tsx b/src/components/AppPageContent.tsx index 5425de77c..b52d55d20 100644 --- a/src/components/AppPageContent.tsx +++ b/src/components/AppPageContent.tsx @@ -280,7 +280,7 @@ export function AppPageContent({ const content = (
{ expect(onNavigate).not.toHaveBeenCalled(); }); + it("从全局 Skills 入口进入时应带上当前工作区,避免能力草案面板丢上下文", async () => { + const onNavigate = vi.fn(); + localStorage.setItem("agent_last_project_id", JSON.stringify("project-1")); + const container = mountSidebarContainer({ + currentPage: "agent", + currentPageParams: { + agentEntry: "new-task", + } as AgentPageParams, + onNavigate, + }); + await flushEffects(); + + const button = container.querySelector( + 'button[aria-label="Skills"]', + ) as HTMLButtonElement | null; + + expect(button).not.toBeNull(); + + act(() => { + button?.click(); + }); + + expect(onNavigate).toHaveBeenCalledWith("skills", { + creationProjectId: "project-1", + }); + }); + it("旧的 enabled-items 本地缓存不应再复活历史导航", async () => { localStorage.setItem( APP_SIDEBAR_ENABLED_ITEMS_STORAGE_KEY, diff --git a/src/components/AppSidebar.tsx b/src/components/AppSidebar.tsx index 966f2926d..e5b140f87 100644 --- a/src/components/AppSidebar.tsx +++ b/src/components/AppSidebar.tsx @@ -3554,6 +3554,31 @@ export function AppSidebar({ return; } + if (item.id === "skills") { + const targetParams = currentProjectId + ? { creationProjectId: currentProjectId } + : undefined; + const target = { + page: "skills" as Page, + rawParams: targetParams, + paramsKey: serializeNavigationParams(targetParams), + } satisfies SidebarNavigationTarget; + + if ( + isSameSidebarNavigationTarget( + target, + requestedNavigationTargetRef.current.page, + requestedNavigationTargetRef.current.rawParams, + ) + ) { + return; + } + + requestedNavigationTargetRef.current = target; + onNavigate(target.page, target.rawParams); + return; + } + const target = resolveSidebarNavigationTarget(item); if (!target) { diff --git a/src/components/agent/chat/AgentChatWorkspace.tsx b/src/components/agent/chat/AgentChatWorkspace.tsx index 06da789b9..2793f9426 100644 --- a/src/components/agent/chat/AgentChatWorkspace.tsx +++ b/src/components/agent/chat/AgentChatWorkspace.tsx @@ -1420,6 +1420,7 @@ export function AgentChatWorkspace({ body: string; } | null>(null); const handledInitialPendingServiceSkillLaunchSignatureRef = useRef(""); + const dismissedInitialPendingServiceSkillLaunchSignatureRef = useRef(""); const handledInitialProjectFileOpenSignatureRef = useRef(""); const initialCreationReplay = useMemo( () => extractCreationReplayMetadata(initialRequestMetadata), @@ -3185,9 +3186,19 @@ export function AgentChatWorkspace({ }); const handlePendingServiceSkillLaunchSubmit = workspaceServiceSkillEntryActions.handlePendingServiceSkillLaunchSubmit; + const clearPendingServiceSkillLaunch = + workspaceServiceSkillEntryActions.clearPendingServiceSkillLaunch; useEffect(() => { if (!initialPendingServiceSkillLaunchSignature) { handledInitialPendingServiceSkillLaunchSignatureRef.current = ""; + dismissedInitialPendingServiceSkillLaunchSignatureRef.current = ""; + return; + } + + if ( + dismissedInitialPendingServiceSkillLaunchSignatureRef.current === + initialPendingServiceSkillLaunchSignature + ) { return; } @@ -3265,6 +3276,7 @@ export function AgentChatWorkspace({ pendingSceneGateSource, openRuntimeSceneGate, handleSceneGateSubmit, + clearRuntimeSceneGate, } = useWorkspaceSceneGateRuntime({ serviceSkills: activeTheme === "general" ? serviceSkills : [], projectId, @@ -3282,6 +3294,19 @@ export function AgentChatWorkspace({ pendingA2UISource; const hasPendingA2UIForm = Boolean(effectivePendingA2UIForm); const suppressCanvasAutoOpenForPendingA2UI = hasPendingA2UIForm; + const clearEntryPendingA2UI = useCallback(() => { + if (initialPendingServiceSkillLaunchSignature) { + dismissedInitialPendingServiceSkillLaunchSignatureRef.current = + initialPendingServiceSkillLaunchSignature; + } + + clearPendingServiceSkillLaunch(); + clearRuntimeSceneGate(); + }, [ + clearPendingServiceSkillLaunch, + clearRuntimeSceneGate, + initialPendingServiceSkillLaunchSignature, + ]); const { currentGate, @@ -3822,6 +3847,7 @@ export function AgentChatWorkspace({ const { handleBackHome, resetTopicLocalState } = useWorkspaceResetRuntime({ clearMessages, clearRuntimeTeamState, + clearPendingEntryA2UI: clearEntryPendingA2UI, clearProjectSelectionRuntime, resetRestoredSessionState, resetGuideState, @@ -4247,6 +4273,7 @@ export function AgentChatWorkspace({ workspaceRequestMetadataBase: initialRequestMetadata, serviceModels, messages, + setChatMessages, bootstrapDispatchPreview, sendMessage, resolveSendBoundary, @@ -5015,6 +5042,7 @@ export function AgentChatWorkspace({ setTaskCenterTransitionTopicId(topicId); setTaskCenterDetachedTopicId(null); setActiveTaskCenterDraftTabId(null); + clearEntryPendingA2UI(); if (options?.replaceOpenTabs === true) { replaceTaskCenterOpenTabs(topicId, topicWorkspaceId); } else if (shouldMaintainTaskCenterTab) { @@ -5057,6 +5085,7 @@ export function AgentChatWorkspace({ }, [ agentEntry, + clearEntryPendingA2UI, markTaskCenterLocalSessionOverride, replaceTaskCenterOpenTabs, setActiveTaskCenterDraftTabId, @@ -5074,6 +5103,7 @@ export function AgentChatWorkspace({ setActiveTaskCenterDraftTabId(null); setTaskCenterDetachedTopicId(topicId); setTaskCenterTransitionTopicId(topicId); + clearEntryPendingA2UI(); markTaskCenterLocalSessionOverride(topicId); rememberInitialSessionNavigationStart(topicId); const switchResult = await switchTopic(topicId, { @@ -5093,7 +5123,7 @@ export function AgentChatWorkspace({ current === topicId ? null : current, ); }, - [markTaskCenterLocalSessionOverride, switchTopic], + [clearEntryPendingA2UI, markTaskCenterLocalSessionOverride, switchTopic], ); const handleSelectTaskCenterDraftTab = useCallback( @@ -5448,7 +5478,8 @@ export function AgentChatWorkspace({ agentEntry !== "claw" || !taskCenterWorkspaceId || isAutoRestoringSession || - isSessionHydrating + isSessionHydrating || + initialPendingServiceSkillLaunchSignature ) { return; } @@ -5519,6 +5550,7 @@ export function AgentChatWorkspace({ agentEntry, handleOpenTaskTopic, hasDisplayMessages, + initialPendingServiceSkillLaunchSignature, isAutoRestoringSession, isSessionHydrating, normalizedInitialSessionId, @@ -8192,6 +8224,7 @@ export function AgentChatWorkspace({ skillsLoading: combinedSkillsLoading, onSelectServiceSkill: workspaceServiceSkillEntryActions.handleServiceSkillSelect, + initialInputCapability: effectiveInitialInputCapability, handleNavigateToSkillSettings, handleRefreshSkills, handleOpenBrowserAssistInCanvas: handleOpenBrowserRuntimeForBrowserAssist, diff --git a/src/components/agent/chat/agentChatWorkspaceContract.ts b/src/components/agent/chat/agentChatWorkspaceContract.ts index 7e044e192..1f9c03d63 100644 --- a/src/components/agent/chat/agentChatWorkspaceContract.ts +++ b/src/components/agent/chat/agentChatWorkspaceContract.ts @@ -58,6 +58,7 @@ export interface AgentChatWorkspaceProps { initialSiteSkillLaunch?: AgentSiteSkillLaunchParams; initialPendingServiceSkillLaunch?: AgentPendingServiceSkillLaunchParams; initialInputCapability?: AgentInitialInputCapabilityParams; + preferHomeForInitialInputCapability?: boolean; initialKnowledgePackSelection?: AgentInitialKnowledgePackSelectionParams; initialProjectFileOpenTarget?: AgentProjectFileOpenTarget; } diff --git a/src/components/agent/chat/components/EmptyState.test.tsx b/src/components/agent/chat/components/EmptyState.test.tsx index cb2a81fe4..4f8b85fb6 100644 --- a/src/components/agent/chat/components/EmptyState.test.tsx +++ b/src/components/agent/chat/components/EmptyState.test.tsx @@ -584,6 +584,39 @@ describe("EmptyState", () => { ).toBeNull(); }); + it("从 Skills 页带回的技能应显示在首页输入框内的 @ 标签", async () => { + const skill = { + key: "local:writer", + name: "写作助手", + description: "本地补充技能", + directory: "writer", + installed: true, + sourceKind: "other", + } as Skill; + const container = renderEmptyState({ + activeTheme: "general", + skills: [skill], + initialInputCapability: { + capabilityRoute: { + kind: "installed_skill", + skillKey: skill.key, + skillName: skill.name, + }, + requestKey: 20260512, + }, + }); + + await act(async () => { + await Promise.resolve(); + await Promise.resolve(); + }); + + const badge = container.querySelector('[data-testid="input-skill-badge"]'); + expect(badge).toBeTruthy(); + expect(badge?.textContent).toContain("@"); + expect(badge?.textContent).toContain("写作助手"); + }); + it("首页添加资料入口应打开输入框资料中枢,而不是预填一段说明", async () => { const setInput = vi.fn(); const onToggleKnowledgePack = vi.fn<(enabled: boolean) => void>(); diff --git a/src/components/agent/chat/components/EmptyState.tsx b/src/components/agent/chat/components/EmptyState.tsx index 7e2e39432..689e873ca 100644 --- a/src/components/agent/chat/components/EmptyState.tsx +++ b/src/components/agent/chat/components/EmptyState.tsx @@ -75,8 +75,10 @@ import { import { buildKnowledgeRequestMetadata } from "@/features/knowledge/agent/knowledgeMetadata"; import { resolveInputCapabilityDispatch, + resolveInputCapabilitySelectionFromRoute, type InputCapabilitySelection, } from "../skill-selection/inputCapabilitySelection"; +import type { AgentInitialInputCapabilityParams } from "@/types/page"; import type agentResource from "@/i18n/resources/zh-CN/agent.json"; import type { InputbarKnowledgePackOption, @@ -393,6 +395,8 @@ interface EmptyStateProps extends SkillSelectionSourceProps { runtimeToolAvailability?: RuntimeToolAvailability | null; /** 当前执行态摘要 */ runtimeTaskCard?: AgentTaskRuntimeCardModel | null; + /** 进入首页时预选的输入框能力 */ + initialInputCapability?: AgentInitialInputCapabilityParams; /** 打开记忆工作台 */ onOpenMemoryWorkbench?: () => void; /** 打开消息渠道 */ @@ -514,6 +518,7 @@ export const EmptyState: React.FC = ({ sessionId = null, isLoading = false, disabled = false, + initialInputCapability, creationReplaySurface = null, defaultCuratedTaskReferenceMemoryIds, defaultCuratedTaskReferenceEntries, @@ -541,6 +546,7 @@ export const EmptyState: React.FC = ({ [t], ); const pageContainerRef = useRef(null); + const handledInitialInputCapabilitySignatureRef = useRef(""); const [activeCapability, setActiveCapability] = useState(null); const [knowledgeHubOpenRequestKey, setKnowledgeHubOpenRequestKey] = @@ -585,6 +591,59 @@ export const EmptyState: React.FC = ({ const clearSelectedSkill = useCallback(() => { setActiveCapability(null); }, []); + const initialInputCapabilitySignature = useMemo(() => { + const route = initialInputCapability?.capabilityRoute; + if (!route) { + return ""; + } + + return JSON.stringify({ + requestKey: initialInputCapability.requestKey ?? 0, + route, + }); + }, [initialInputCapability]); + + useEffect(() => { + if (!initialInputCapabilitySignature) { + handledInitialInputCapabilitySignatureRef.current = ""; + return; + } + + if ( + handledInitialInputCapabilitySignatureRef.current === + initialInputCapabilitySignature + ) { + return; + } + + const route = initialInputCapability?.capabilityRoute; + if (!route) { + return; + } + + handledInitialInputCapabilitySignatureRef.current = + initialInputCapabilitySignature; + const resolvedCapability = resolveInputCapabilitySelectionFromRoute({ + route, + skills, + }); + + if ( + route.kind === "curated_task" && + !input.trim() && + route.prompt.trim().length > 0 + ) { + setInput(route.prompt); + } + + setActiveCapability(resolvedCapability); + }, [ + initialInputCapability, + initialInputCapabilitySignature, + input, + setInput, + skills, + ]); const handleSelectInputCapability = useCallback( (capability: InputCapabilitySelection) => { if (capability.kind === "service_skill") { diff --git a/src/components/agent/chat/components/ImageWorkbenchMessagePreview.test.tsx b/src/components/agent/chat/components/ImageWorkbenchMessagePreview.test.tsx index fab38df3e..1e0c300a7 100644 --- a/src/components/agent/chat/components/ImageWorkbenchMessagePreview.test.tsx +++ b/src/components/agent/chat/components/ImageWorkbenchMessagePreview.test.tsx @@ -43,28 +43,41 @@ afterEach(async () => { }); describe("ImageWorkbenchMessagePreview", () => { - it("uses agent namespace resources for complete generate preview chrome", () => { + it("renders a complete image with the lightweight tool strip and no task chrome", () => { const { container } = renderPreview({ taskId: "image-preview-complete", prompt: "", mode: "generate", status: "complete", + imageUrl: "data:image/png;base64,aW1hZ2U=", imageCount: 1, + caption: "搞定,已生成这张图。", + runtimeContract: { + model: "fal-ai/nano-banana-pro", + }, }); - expect(container.textContent).toContain( + expect(container.textContent).toContain("Image Generation"); + expect(container.textContent).toContain("Nanobanana Pro"); + expect(container.textContent).toContain("搞定,已生成这张图。"); + expect(container.querySelector("img")?.getAttribute("src")).toBe( + "data:image/png;base64,aW1hZ2U=", + ); + expect(container.querySelector("button")?.getAttribute("aria-label")).toBe( + "Open image result", + ); + expect(container.textContent).not.toContain( "No prompt provided for this task.", ); - expect(container.textContent).toContain("Generated"); - expect(container.textContent).toContain( + expect(container.textContent).not.toContain("Generated"); + expect(container.textContent).not.toContain( "The image result is complete. Open the right panel to review and use it.", ); - expect(container.textContent).toContain("Result synced"); + expect(container.textContent).not.toContain("Result synced"); expect(container.textContent).not.toContain("当前任务未提供提示词"); - expect(container.textContent).not.toContain("已生成"); }); - it("uses localized source footnote and image progress labels", () => { + it("hides legacy task-card status and source chrome in chat preview", () => { const { container } = renderPreview({ taskId: "image-preview-variation", prompt: "Create a softer visual direction", @@ -75,12 +88,14 @@ describe("ImageWorkbenchMessagePreview", () => { sourceImageCount: 2, }); - expect(container.textContent).toContain("2/4 images"); - expect(container.textContent).toContain("Partially complete"); - expect(container.textContent).toContain( + expect(container.textContent).toContain("Image Redraw"); + expect(container.textContent).toContain("Image is temporarily unavailable"); + expect(container.textContent).not.toContain("2/4 images"); + expect(container.textContent).not.toContain("Partially complete"); + expect(container.textContent).not.toContain( "Returned 2 / 4 variation result item(s); the remaining results are not complete.", ); - expect(container.textContent).toContain( + expect(container.textContent).not.toContain( "Reference images: Attached 2 reference image(s).", ); expect(container.textContent).not.toContain("参考图"); diff --git a/src/components/agent/chat/components/ImageWorkbenchMessagePreview.tsx b/src/components/agent/chat/components/ImageWorkbenchMessagePreview.tsx index 7d9e05846..7e8727903 100644 --- a/src/components/agent/chat/components/ImageWorkbenchMessagePreview.tsx +++ b/src/components/agent/chat/components/ImageWorkbenchMessagePreview.tsx @@ -1,12 +1,12 @@ import React from "react"; import type { TFunction } from "i18next"; import { useTranslation } from "react-i18next"; -import { LoaderCircle, Sparkles } from "lucide-react"; +import { Leaf, LoaderCircle } from "lucide-react"; import { emitImageWorkbenchFocus } from "@/lib/imageWorkbenchEvents"; import { cn } from "@/lib/utils"; import type { MessageImageWorkbenchPreview } from "../types"; +import { resolveImageWorkbenchPreviewModelLabel } from "../utils/imageWorkbenchPresentation"; import { RenderableTaskImage } from "./RenderableTaskImage"; -import { buildLimeCorePolicyEvaluationMetaItem } from "../workspace/mediaTaskPolicyEvaluation"; interface ImageWorkbenchMessagePreviewProps { preview: MessageImageWorkbenchPreview; @@ -15,203 +15,18 @@ interface ImageWorkbenchMessagePreviewProps { type AgentTranslate = TFunction<"agent", undefined>; -const TRANSITION_STATUS_MESSAGE_FRAGMENTS = [ - "\u6b63\u5728\u540c\u6b65", - "\u540c\u6b65\u4efb\u52a1\u72b6\u6001", - "\u540c\u6b65\u5230\u5bf9\u8bdd", - "\u5f02\u6b65\u961f\u5217", -]; - -function resolveResultLabel( - mode: MessageImageWorkbenchPreview["mode"] | undefined, +function resolveToolLabel( + preview: MessageImageWorkbenchPreview, t: AgentTranslate, ): string { - switch (mode) { + switch (preview.mode) { case "edit": - return t("agentChat.imageWorkbenchPreview.result.edit"); + return t("agentChat.imageWorkbenchPreview.tool.editing"); case "variation": - return t("agentChat.imageWorkbenchPreview.result.variation"); + return t("agentChat.imageWorkbenchPreview.tool.redraw"); case "generate": default: - return t("agentChat.imageWorkbenchPreview.result.generate"); - } -} - -function resolveSourceLabel( - mode: MessageImageWorkbenchPreview["mode"] | undefined, - t: AgentTranslate, -): string { - if (mode === "variation") { - return t("agentChat.imageWorkbenchPreview.source.referenceLabel"); - } - return t("agentChat.imageWorkbenchPreview.source.sourceLabel"); -} - -function resolveStatusPrefix( - preview: MessageImageWorkbenchPreview, - t: AgentTranslate, -): string { - switch (preview.status) { - case "complete": - switch (preview.mode) { - case "edit": - return t("agentChat.imageWorkbenchPreview.status.complete.edit"); - case "variation": - return t("agentChat.imageWorkbenchPreview.status.complete.variation"); - case "generate": - default: - return t("agentChat.imageWorkbenchPreview.status.complete.generate"); - } - case "partial": - return t("agentChat.imageWorkbenchPreview.status.partial"); - case "cancelled": - return t("agentChat.imageWorkbenchPreview.status.cancelled"); - case "failed": - switch (preview.mode) { - case "edit": - return t("agentChat.imageWorkbenchPreview.status.failed.edit"); - case "variation": - return t("agentChat.imageWorkbenchPreview.status.failed.variation"); - case "generate": - default: - return t("agentChat.imageWorkbenchPreview.status.failed.generate"); - } - case "running": - default: - switch ((preview.phase || "").trim().toLowerCase()) { - case "queued": - return t("agentChat.imageWorkbenchPreview.status.running.queued"); - case "running": - switch (preview.mode) { - case "edit": - return t("agentChat.imageWorkbenchPreview.status.running.edit"); - case "variation": - return t( - "agentChat.imageWorkbenchPreview.status.running.variation", - ); - case "generate": - default: - return t( - "agentChat.imageWorkbenchPreview.status.running.generate", - ); - } - default: - return t("agentChat.imageWorkbenchPreview.status.running.preparing"); - } - } -} - -function resolveStatusAccentClass( - preview: MessageImageWorkbenchPreview, -): string { - switch (preview.status) { - case "complete": - return "bg-emerald-500"; - case "partial": - return "bg-amber-500"; - case "cancelled": - return "bg-slate-400"; - case "failed": - return "bg-rose-500"; - case "running": - default: - return "bg-sky-500"; - } -} - -function isTransitionStatusMessage(statusMessage: string): boolean { - return TRANSITION_STATUS_MESSAGE_FRAGMENTS.some((fragment) => - statusMessage.includes(fragment), - ); -} - -function resolveDescription( - preview: MessageImageWorkbenchPreview, - t: AgentTranslate, -): string { - const statusMessage = preview.statusMessage?.trim(); - if ( - statusMessage && - !(preview.status !== "running" && isTransitionStatusMessage(statusMessage)) - ) { - return statusMessage; - } - - const resultLabel = resolveResultLabel(preview.mode, t); - const storyboardLabel = - preview.layoutHint === "storyboard_3x3" - ? t("agentChat.imageWorkbenchPreview.layout.storyboard") - : null; - const returnedImageCount = Math.max( - preview.previewImages?.length ?? 0, - preview.imageUrl ? 1 : 0, - preview.imageCount ?? 0, - ); - const expectedImageCount = Math.max( - preview.expectedImageCount ?? 0, - returnedImageCount, - ); - - switch (preview.status) { - case "complete": - return storyboardLabel - ? t("agentChat.imageWorkbenchPreview.description.complete.storyboard", { - label: storyboardLabel, - }) - : returnedImageCount > 1 - ? t("agentChat.imageWorkbenchPreview.description.complete.multiple", { - count: returnedImageCount, - result: resultLabel, - }) - : t("agentChat.imageWorkbenchPreview.description.complete.single", { - result: resultLabel, - }); - case "partial": - return storyboardLabel - ? t("agentChat.imageWorkbenchPreview.description.partial.storyboard", { - expected: expectedImageCount || 9, - label: storyboardLabel, - returned: returnedImageCount, - }) - : returnedImageCount > 0 - ? t("agentChat.imageWorkbenchPreview.description.partial.multiple", { - expected: expectedImageCount || returnedImageCount, - result: resultLabel, - returned: returnedImageCount, - }) - : t("agentChat.imageWorkbenchPreview.description.partial.single", { - result: resultLabel, - }); - case "cancelled": - return t("agentChat.imageWorkbenchPreview.description.cancelled"); - case "failed": - return preview.retryable === false - ? t("agentChat.imageWorkbenchPreview.description.failed.notRetryable") - : t("agentChat.imageWorkbenchPreview.description.failed.retryable"); - case "running": - default: - if (storyboardLabel && expectedImageCount > 1) { - return t( - "agentChat.imageWorkbenchPreview.description.running.storyboard", - { - expected: expectedImageCount, - label: storyboardLabel, - }, - ); - } - switch (preview.mode) { - case "edit": - return t("agentChat.imageWorkbenchPreview.description.running.edit"); - case "variation": - return t( - "agentChat.imageWorkbenchPreview.description.running.variation", - ); - case "generate": - default: - return t( - "agentChat.imageWorkbenchPreview.description.running.generate", - ); - } + return t("agentChat.imageWorkbenchPreview.tool.generation"); } } @@ -219,82 +34,26 @@ function resolvePlaceholderLabel( preview: MessageImageWorkbenchPreview, t: AgentTranslate, ): string { - if (preview.status === "failed") { - return t("agentChat.imageWorkbenchPreview.placeholder.failed"); + switch (preview.status) { + case "cancelled": + return t("agentChat.imageWorkbenchPreview.placeholder.cancelled"); + case "failed": + return t("agentChat.imageWorkbenchPreview.placeholder.failed"); + case "complete": + case "partial": + return t("agentChat.imageWorkbenchPreview.placeholder.imageUnavailable"); + case "running": + default: + switch (preview.mode) { + case "edit": + return t("agentChat.imageWorkbenchPreview.placeholder.editing"); + case "variation": + return t("agentChat.imageWorkbenchPreview.placeholder.redrawing"); + case "generate": + default: + return t("agentChat.imageWorkbenchPreview.placeholder.generating"); + } } - if (preview.status === "cancelled") { - return t("agentChat.imageWorkbenchPreview.placeholder.cancelled"); - } - if (preview.status === "complete" || preview.status === "partial") { - return t("agentChat.imageWorkbenchPreview.placeholder.synced"); - } - return resolveStatusPrefix(preview, t); -} - -function resolveImageUnavailableLabel( - preview: MessageImageWorkbenchPreview, - t: AgentTranslate, -): string { - if (preview.status === "complete" || preview.status === "partial") { - return t("agentChat.imageWorkbenchPreview.placeholder.imageUnavailable"); - } - return resolvePlaceholderLabel(preview, t); -} - -function shouldShowSourceFootnote( - preview: MessageImageWorkbenchPreview, -): boolean { - return Boolean( - preview.mode === "edit" || - preview.mode === "variation" || - preview.sourceImageUrl?.trim() || - preview.sourceImagePrompt?.trim() || - preview.sourceImageRef?.trim() || - preview.sourceImageCount, - ); -} - -function resolveSourceSummary( - preview: MessageImageWorkbenchPreview, - t: AgentTranslate, -): string { - const prompt = preview.sourceImagePrompt?.trim(); - if (prompt) { - return prompt; - } - - const ref = preview.sourceImageRef?.trim(); - if (ref) { - return t("agentChat.imageWorkbenchPreview.source.ref", { ref }); - } - - if (preview.sourceImageCount && preview.sourceImageCount > 1) { - return preview.mode === "variation" - ? t("agentChat.imageWorkbenchPreview.source.referenceCount", { - count: preview.sourceImageCount, - }) - : t("agentChat.imageWorkbenchPreview.source.sourceCount", { - count: preview.sourceImageCount, - }); - } - - return preview.mode === "variation" - ? t("agentChat.imageWorkbenchPreview.source.variationSummary") - : t("agentChat.imageWorkbenchPreview.source.editSummary"); -} - -function resolveSourceFootnote( - preview: MessageImageWorkbenchPreview, - t: AgentTranslate, -): string | null { - if (!shouldShowSourceFootnote(preview)) { - return null; - } - - return t("agentChat.imageWorkbenchPreview.source.footnote", { - label: resolveSourceLabel(preview.mode, t), - summary: resolveSourceSummary(preview, t), - }); } function renderPlaceholder( @@ -302,19 +61,23 @@ function renderPlaceholder( reason: string, t: AgentTranslate, ) { + const detail = + preview.status === "failed" ? preview.statusMessage?.trim() : ""; + return ( -
-
+
+
{reason === "empty" && preview.status === "running" ? ( - - ) : ( - - )} + + ) : null}
- {reason === "error" - ? resolveImageUnavailableLabel(preview, t) - : resolvePlaceholderLabel(preview, t)} + {resolvePlaceholderLabel(preview, t)}
+ {detail ? ( +
+ {detail} +
+ ) : null}
); @@ -336,7 +99,7 @@ function resolvePreviewImages(preview: MessageImageWorkbenchPreview): string[] { return urls.slice(0, 9); } -function resolvePreviewCardAspectClass( +function resolvePreviewGridAspectClass( preview: MessageImageWorkbenchPreview, imageCount: number, ): string { @@ -346,60 +109,6 @@ function resolvePreviewCardAspectClass( return "aspect-[16/10]"; } -function resolveStoryboardSlotLabel( - preview: MessageImageWorkbenchPreview, - index: number, -): string | null { - return ( - preview.storyboardSlots?.find((slot) => slot.slotIndex === index + 1) - ?.label || null - ); -} - -function resolvePreviewMetaLabels( - preview: MessageImageWorkbenchPreview, - imageCount: number, - expectedImageCount: number, - t: AgentTranslate, -): string[] { - const labels: string[] = []; - const policyLabel = buildLimeCorePolicyEvaluationMetaItem({ - evaluationStatus: preview.runtimeContract?.limecorePolicyEvaluationStatus, - evaluationDecision: - preview.runtimeContract?.limecorePolicyEvaluationDecision, - blockingRefs: preview.runtimeContract?.limecorePolicyEvaluationBlockingRefs, - askRefs: preview.runtimeContract?.limecorePolicyEvaluationAskRefs, - pendingRefs: preview.runtimeContract?.limecorePolicyEvaluationPendingRefs, - missingInputs: preview.runtimeContract?.limecorePolicyMissingInputs, - pendingHitRefs: preview.runtimeContract?.limecorePolicyPendingHitRefs, - }); - - if (policyLabel) { - labels.push(policyLabel); - } - - if (preview.layoutHint === "storyboard_3x3" && expectedImageCount >= 4) { - labels.push(t("agentChat.imageWorkbenchPreview.layout.storyboard")); - } - - if (expectedImageCount > imageCount && expectedImageCount > 1) { - labels.push( - t("agentChat.imageWorkbenchPreview.meta.imageProgress", { - current: imageCount, - expected: expectedImageCount, - }), - ); - } else if (imageCount > 1) { - labels.push( - t("agentChat.imageWorkbenchPreview.meta.imageCount", { - count: imageCount, - }), - ); - } - - return labels; -} - function renderPreviewMedia( preview: MessageImageWorkbenchPreview, t: AgentTranslate, @@ -414,14 +123,14 @@ function renderPreviewMedia( const totalSlotCount = isStoryboardGrid ? Math.max(expectedImageCount, 9) : previewImages.length; - const aspectClass = resolvePreviewCardAspectClass(preview, totalSlotCount); + const aspectClass = resolvePreviewGridAspectClass(preview, totalSlotCount); if (!isStoryboardGrid && previewImages.length <= 1) { return ( renderPlaceholder(preview, reason, t)} /> ); @@ -440,23 +149,16 @@ function renderPreviewMedia( return (
{Array.from({ length: visibleCount }, (_, index) => { const url = previewImages[index]; const isLastWithOverflow = extraCount > 0 && index === visibleCount - 1; - const storyboardSlotLabel = isStoryboardGrid - ? resolveStoryboardSlotLabel(preview, index) - : null; return (
@@ -474,35 +176,12 @@ function renderPreviewMedia( )} /> ) : ( -
+
{preview.status === "running" ? ( - - ) : ( - - )} - - {preview.status === "partial" - ? t("agentChat.imageWorkbenchPreview.slot.waiting") - : preview.status === "failed" - ? t("agentChat.imageWorkbenchPreview.slot.failed") - : preview.status === "cancelled" - ? t("agentChat.imageWorkbenchPreview.slot.cancelled") - : t("agentChat.imageWorkbenchPreview.slot.pending")} - + + ) : null}
)} - {isStoryboardGrid ? ( - <> - - {index + 1} - - {storyboardSlotLabel ? ( - - {storyboardSlotLabel} - - ) : null} - - ) : null} {isLastWithOverflow ? (
+{extraCount} @@ -519,29 +198,14 @@ export const ImageWorkbenchMessagePreview: React.FC< ImageWorkbenchMessagePreviewProps > = ({ preview, onOpen }) => { const { t } = useTranslation("agent"); - const sourceFootnote = resolveSourceFootnote(preview, t); - const statusPrefix = resolveStatusPrefix(preview, t); - const statusDescription = resolveDescription(preview, t); - const previewImages = resolvePreviewImages(preview); - const totalImageCount = Math.max( - preview.imageCount ?? 0, - preview.imageUrl ? 1 : 0, - previewImages.length, - ); - const expectedImageCount = Math.max( - preview.expectedImageCount ?? 0, - totalImageCount, - ); - const previewMetaLabels = resolvePreviewMetaLabels( - preview, - totalImageCount, - expectedImageCount, - t, - ); + const toolLabel = resolveToolLabel(preview, t); + const modelLabel = resolveImageWorkbenchPreviewModelLabel(preview); + const caption = preview.caption?.trim(); return ( ); }; diff --git a/src/components/agent/chat/components/MessageList.test.tsx b/src/components/agent/chat/components/MessageList.test.tsx index 8df044d03..3af89d442 100644 --- a/src/components/agent/chat/components/MessageList.test.tsx +++ b/src/components/agent/chat/components/MessageList.test.tsx @@ -2289,11 +2289,14 @@ describe("MessageList", () => { '[data-testid="image-workbench-message-preview-task-1"]', ) as HTMLButtonElement | null; - expect(previewCard?.textContent).toContain("一颗戴耳机的青柠"); - expect(previewCard?.textContent).toContain("已生成"); - expect(previewCard?.textContent).toContain("可在右侧继续查看与使用"); + expect(previewCard?.textContent).toContain("Image Generation"); + expect(previewCard?.textContent).not.toContain("一颗戴耳机的青柠"); + expect(previewCard?.textContent).not.toContain("已生成"); + expect(previewCard?.textContent).not.toContain("可在右侧继续查看与使用"); + expect(container.textContent).not.toContain("图片生成已完成"); expect(previewCard?.className).not.toContain("max-w-[620px]"); - expect(previewCard?.className).toContain("max-w-[360px]"); + expect(previewCard?.className).toContain("w-full"); + expect(previewCard?.querySelector("img")).not.toBeNull(); act(() => { previewCard?.click(); @@ -2306,7 +2309,128 @@ describe("MessageList", () => { window.removeEventListener(IMAGE_WORKBENCH_FOCUS_EVENT, handleFocus); }); - it("图片任务消息卡应展示 LimeCore 策略输入标签", () => { + it("图片任务消息应隐藏旧提交详情表,只保留自然正文和轻量工具条", async () => { + const messages: Message[] = [ + { + id: "msg-assistant-image-workbench-verbose-template", + role: "assistant", + content: + "好的!我来为你生成一张三国群像海报。\n\n任务已创建成功!这里是生成详情:\n\n| 项目 | 内容 |\n| --- | --- |\n| 画面构图 | 刘关张桃园三结义居中 |\n| 风格 | 国风电影感 |\n| 尺寸 | 1792 x 1024 |\n| 色调 | 墨黑、赤红、暗金 |\n| 模型 | fal-ai/nano-banana-pro |\n| 状态 | 已进入队列,正在生成中... |\n\n生成完成后图片会显示在对话中,稍等一下即可看到效果。", + timestamp: new Date(), + imageWorkbenchPreview: { + taskId: "task-verbose-template", + prompt: "三国主要人物群像海报", + mode: "generate", + status: "complete", + imageUrl: "https://example.com/three-kingdoms.png", + imageCount: 1, + runtimeContract: { + model: "fal-ai/nano-banana-pro", + }, + }, + }, + ]; + + const container = await renderZh(messages); + const previewCard = container.querySelector( + '[data-testid="image-workbench-message-preview-task-verbose-template"]', + ); + + expect(previewCard?.textContent).toContain("Image Generation"); + expect(previewCard?.textContent).toContain("Nanobanana Pro"); + expect(container.textContent).not.toContain("任务已创建成功"); + expect(container.textContent).not.toContain("这里是生成详情"); + expect(container.textContent).not.toContain("画面构图"); + expect(container.textContent).not.toContain("已进入队列"); + expect(container.textContent).not.toContain("稍等一下即可看到效果"); + expect(previewCard?.querySelector("img")).not.toBeNull(); + }); + + it("图片任务消息应在同一条 assistant 回复里呈现自然文案、工具条、图片和结果描述", async () => { + const messages: Message[] = [ + { + id: "msg-assistant-image-workbench-natural", + role: "assistant", + content: + "好嘞,用 Nanobanana Pro 给你生成一张从花城汇看广州塔的春天照片\n先获取下工具参数\n马上生成", + timestamp: new Date(), + contentParts: [ + { type: "text", text: "我先按你的描述创建异步图片任务" }, + { + type: "tool_use", + toolCall: { + id: "tool-image-natural", + name: "limeCreateImageGenerationTask", + arguments: "{}", + status: "completed", + startTime: new Date(), + endTime: new Date(), + }, + }, + ], + toolCalls: [ + { + id: "tool-image-natural", + name: "limeCreateImageGenerationTask", + arguments: "{}", + status: "completed", + startTime: new Date(), + endTime: new Date(), + }, + ], + imageWorkbenchPreview: { + taskId: "task-natural-image", + prompt: "从花城汇看广州塔的春天照片", + mode: "generate", + status: "complete", + imageUrl: "https://example.com/guangzhou-tower.png", + imageCount: 1, + modelName: "fal-ai/nano-banana-pro", + caption: "搞定,已生成这张图。", + }, + }, + ]; + + const container = await renderZh(messages); + const text = container.textContent || ""; + const intro = container.querySelector( + '[data-testid="image-workbench-assistant-intro"]', + ); + + expect(text).toContain("好嘞,用 Nanobanana Pro"); + expect(text).toContain("先获取下工具参数"); + expect(text).toContain("马上生成"); + expect(text).toContain("Image Generation"); + expect(text).toContain("Nanobanana Pro"); + expect(text).toContain("搞定,已生成这张图。"); + expect(text).not.toContain("limeCreateImageGenerationTask"); + expect(text).not.toContain("异步图片任务"); + expect(intro?.textContent).toContain("好嘞,用 Nanobanana Pro"); + expect(container.querySelector('[data-testid="streaming-renderer"]')).toBe( + null, + ); + }); + + it("历史助手消息没有图片轻卡时,也不应继续展示旧图片任务详情模板", async () => { + const messages: Message[] = [ + { + id: "msg-assistant-image-workbench-legacy-template-only", + role: "assistant", + content: + "好的,我来为你生成一张青柠插画!\n\n✅ 青柠插画生成任务已创建\n\n任务 ID: 013dbd1b-0fc0-45de-a1c8-f78489ccc11c\nPrompt:一颗鲜嫩的青柠,水彩插画风格\n参数:\n🎨 风格:水彩插画\n📐 尺寸:1024×1024\n🤖 模型:fal-ai/nano-banana-pro\n🔧 Provider:fal\n任务已提交进入队列,你可以在 图片工作台(Image Workbench)中查看生成进度和最终结果。稍后如果已生成,你可以直接打开查看~", + timestamp: new Date(), + }, + ]; + + const container = await renderZh(messages); + + expect(container.textContent).not.toContain("任务 ID"); + expect(container.textContent).not.toContain("Image Workbench"); + expect(container.textContent).not.toContain("生成进度和最终结果"); + expect(container.textContent).not.toContain("稍后如果已生成"); + }); + + it("图片任务消息卡不应在聊天区展示 LimeCore 策略输入标签", () => { const now = new Date(); const messages: Message[] = [ { @@ -2343,7 +2467,10 @@ describe("MessageList", () => { '[data-testid="image-workbench-message-preview-task-policy-1"]', ); - expect(previewCard?.textContent).toContain("LimeCore 策略输入待命中: 3"); + expect(previewCard?.textContent).toContain("Image Generation"); + expect(previewCard?.textContent).not.toContain( + "LimeCore 策略输入待命中: 3", + ); }); it("图片任务消息应收起内部 process flow,只保留任务卡与正文", () => { @@ -2422,6 +2549,73 @@ describe("MessageList", () => { ).toBeNull(); }); + it("旧图片提交过程消息没有轻卡时也应整体隐藏", () => { + const container = render( + [ + { + id: "msg-assistant-image-submit-leak", + role: "assistant", + content: + "我来为你生成一张广州塔从花城汇视角的春天照片。图片生成任务已提交!正在为你生成从花城汇看广州塔的春天照片。", + timestamp: new Date(), + isThinking: true, + contentParts: [ + { type: "thinking", text: "开始中 广州塔春天照片" }, + { + type: "text", + text: '进度:正在生成工具输入:{"prompt":"广州塔"}', + }, + ], + toolCalls: [ + { + id: "tool-image-generate", + name: "lime_create_image_generation_task", + arguments: JSON.stringify({ prompt: "广州塔" }), + status: "completed", + result: { success: true }, + startTime: new Date(), + endTime: new Date(), + }, + ], + } as Message, + ], + { + currentTurnId: "turn-image-submit-leak", + turns: [ + { + id: "turn-image-submit-leak", + thread_id: "thread-image-submit-leak", + prompt_text: "@Nanobanana Pro 生成广州塔春天照片", + status: "running", + started_at: "2026-04-24T01:36:56Z", + created_at: "2026-04-24T01:36:56Z", + updated_at: "2026-04-24T01:37:12Z", + }, + ], + threadItems: [ + { + id: "summary-image-submit-leak", + thread_id: "thread-image-submit-leak", + turn_id: "turn-image-submit-leak", + sequence: 1, + status: "in_progress", + started_at: "2026-04-24T01:36:56Z", + updated_at: "2026-04-24T01:37:12Z", + type: "turn_summary", + text: '进度:正在生成工具输入:{"prompt":"广州塔"}', + }, + ], + }, + ); + + expect(container.textContent).not.toContain("图片生成任务已提交"); + expect(container.textContent).not.toContain("工具输入"); + expect(mockStreamingRenderer).not.toHaveBeenCalled(); + expect( + container.querySelector('[data-testid="agent-thread-timeline:leading"]'), + ).toBeNull(); + }); + it("视频任务消息卡应在聊天区渲染预览并支持打开工作区查看", () => { const now = new Date(); const onOpenMessagePreview = vi.fn(); @@ -2919,7 +3113,7 @@ describe("MessageList", () => { ).toBeTruthy(); }); - it("修图任务消息卡应展示来源图区域与修图语义", async () => { + it("修图任务消息卡应收敛为裸结果图", async () => { const now = new Date(); const messages: Message[] = [ { @@ -2949,10 +3143,11 @@ describe("MessageList", () => { '[data-testid="image-workbench-message-preview-task-edit-1"]', ); - expect(previewCard?.textContent).toContain("已修图"); - expect(previewCard?.textContent).toContain("来源图"); - expect(previewCard?.textContent).toContain("原始街景海报"); - expect(previewCard?.textContent).not.toContain("Image Editing"); + expect(previewCard?.textContent).toContain("Image Editing"); + expect(previewCard?.querySelector("img")).not.toBeNull(); + expect(previewCard?.textContent).not.toContain("已修图"); + expect(previewCard?.textContent).not.toContain("来源图"); + expect(previewCard?.textContent).not.toContain("原始街景海报"); }); it("图片任务完成但图片仍在工作台时,不应继续显示生成中占位", async () => { @@ -2979,9 +3174,9 @@ describe("MessageList", () => { '[data-testid="image-workbench-message-preview-task-complete-without-image"]', ); - expect(previewCard?.textContent).toContain("结果已同步"); - expect(previewCard?.textContent).toContain("已生成"); - expect(previewCard?.textContent).toContain("可在右侧继续查看与使用"); + expect(previewCard?.textContent).toContain("图片暂时无法显示"); + expect(previewCard?.textContent).not.toContain("已生成"); + expect(previewCard?.textContent).not.toContain("可在右侧继续查看与使用"); expect(previewCard?.textContent).not.toContain("图片任务卡"); }); @@ -3011,8 +3206,9 @@ describe("MessageList", () => { '[data-testid="image-workbench-message-preview-task-complete-sync-copy"]', ); - expect(previewCard?.textContent).toContain("已生成"); - expect(previewCard?.textContent).toContain("可在右侧继续查看与使用"); + expect(previewCard?.textContent).toContain("Image Generation"); + expect(previewCard?.textContent).not.toContain("已生成"); + expect(previewCard?.textContent).not.toContain("可在右侧继续查看与使用"); expect(previewCard?.textContent).not.toContain("正在同步任务状态"); expect(previewCard?.textContent).not.toContain("图片任务已提交"); }); @@ -3041,7 +3237,7 @@ describe("MessageList", () => { ); expect(previewCard?.textContent).toContain("生成失败"); - expect(previewCard?.textContent).toContain("调整描述后重试"); + expect(previewCard?.textContent).not.toContain("调整描述后重试"); expect( container.querySelector( '[data-testid="image-workbench-message-preview-action-task-failed-1-retry"]', @@ -3049,7 +3245,7 @@ describe("MessageList", () => { ).toBeNull(); }); - it("生成中的图片任务卡应展示队列状态,但不再展示取消按钮", async () => { + it("生成中的图片任务卡应展示同会话占位,但不再展示取消按钮", async () => { const now = new Date(); const messages: Message[] = [ { @@ -3071,8 +3267,8 @@ describe("MessageList", () => { ]; const container = await renderZh(messages); - expect(container.textContent).toContain("等待队列"); - expect(container.textContent).toContain( + expect(container.textContent).toContain("正在生成图片"); + expect(container.textContent).not.toContain( "任务已进入队列,等待图片服务分配执行槽位。", ); expect( @@ -3138,7 +3334,6 @@ describe("MessageList", () => { ); expect(previewCard?.textContent).toContain("已取消"); - expect(previewCard?.textContent).toContain("任务已取消"); expect(previewCard?.textContent).not.toContain("打开查看"); expect( container.querySelector( @@ -3247,14 +3442,15 @@ describe("MessageList", () => { '[data-testid="image-workbench-message-preview-grid-task-storyboard-preview-1"]', ) as HTMLDivElement | null; - expect(container.textContent).toContain( + expect(container.textContent).toContain("Image Generation"); + expect(container.textContent).not.toContain( "3x3 分镜已经完成,可在右侧继续查看与使用。", ); - expect(container.textContent).toContain("9 张"); + expect(container.textContent).not.toContain("9 张"); expect(grid?.className).toContain("grid-cols-3"); expect(grid?.querySelectorAll("img")).toHaveLength(9); - expect(grid?.textContent).toContain("1"); - expect(grid?.textContent).toContain("9"); + expect(grid?.textContent).not.toContain("1"); + expect(grid?.textContent).not.toContain("9"); }); it("当前由聊天区底部承载的 assistant A2UI 不应继续在正文里内联渲染", () => { diff --git a/src/components/agent/chat/components/MessageList.tsx b/src/components/agent/chat/components/MessageList.tsx index c7a357931..ce7430602 100644 --- a/src/components/agent/chat/components/MessageList.tsx +++ b/src/components/agent/chat/components/MessageList.tsx @@ -54,6 +54,7 @@ import { sanitizeContentPartsForDisplay, sanitizeMessageTextForDisplay, } from "../utils/internalImagePlaceholder"; +import { shouldSuppressImageWorkbenchStatusText } from "../utils/imageWorkbenchStatusText"; import { isHiddenConversationArtifactPath } from "../utils/internalArtifactVisibility"; import { buildInputbarRuntimeStatusLineModel, @@ -647,6 +648,32 @@ const AssistantStreamingInlineIndicator: React.FC<{ ); }; +const ImageWorkbenchAssistantIntro: React.FC<{ content: string }> = ({ + content, +}) => { + const lines = content + .split(/\r?\n/u) + .map((line) => line.trim()) + .filter(Boolean); + + if (lines.length === 0) { + return null; + } + + return ( +
+ {lines.map((line, index) => ( +

+ {line} +

+ ))} +
+ ); +}; + function shouldRenderRuntimeStatusPill( status?: AgentRuntimeStatus | null, ): boolean { @@ -2005,28 +2032,45 @@ const MessageListInner: React.FC = ({ group: (typeof renderGroups)[number], streamingTextOverlay: AgentStreamTextOverlaySnapshot | null = null, ) => { - const hasImages = Array.isArray(msg.images) && msg.images.length > 0; - const shouldSuppressImageProcessFlow = Boolean(msg.imageWorkbenchPreview); const rawDisplayContent = msg.role === "assistant" && streamingTextOverlay?.content ? streamingTextOverlay.content : msg.content || ""; - const displayContent = sanitizeMessageTextForDisplay(rawDisplayContent, { - role: msg.role, - hasImages, - }); + const hasImages = Array.isArray(msg.images) && msg.images.length > 0; + const shouldSuppressStandaloneImageWorkbenchProcess = + msg.role === "assistant" && + !msg.imageWorkbenchPreview && + shouldSuppressImageWorkbenchStatusText(rawDisplayContent); + const shouldSuppressImageProcessFlow = Boolean( + msg.imageWorkbenchPreview || + shouldSuppressStandaloneImageWorkbenchProcess, + ); + const visibleRawDisplayContent = + msg.role === "assistant" && + shouldSuppressImageWorkbenchStatusText(rawDisplayContent) + ? "" + : rawDisplayContent; + const displayContent = sanitizeMessageTextForDisplay( + visibleRawDisplayContent, + { + role: msg.role, + hasImages, + }, + ); const shouldDeferMessageDetails = shouldDeferHistoricalAssistantMessageDetails(msg); - const rawRuntimePeerContent = rawDisplayContent.trim(); + const rawRuntimePeerContent = visibleRawDisplayContent.trim(); const shouldRenderRuntimePeerCards = rawRuntimePeerContent.length > 0 && isPureRuntimePeerMessageText(rawRuntimePeerContent); const displayContentParts = shouldDeferMessageDetails ? undefined - : sanitizeContentPartsForDisplay(msg.contentParts, { - role: msg.role, - hasImages, - }); + : shouldSuppressImageProcessFlow + ? undefined + : sanitizeContentPartsForDisplay(msg.contentParts, { + role: msg.role, + hasImages, + }); const isConversationTailAssistant = msg.role === "assistant" && msg.id === group.lastAssistantId; const timeline = @@ -2038,6 +2082,7 @@ const MessageListInner: React.FC = ({ const hasProcessTimelineItems = hasTimelineProcessItems(timeline?.items); const includeInlineProcessFlow = !shouldDeferMessageDetails && + !shouldSuppressImageProcessFlow && msg.role === "assistant" && shouldKeepInlineProcessForActiveAssistant( msg, @@ -2150,7 +2195,9 @@ const MessageListInner: React.FC = ({ ? { ...timeline, items: visiblePrimaryTimelineItems } : null; const trailingTimeline = - timeline && trailingTimelineItems.length > 0 + !shouldSuppressImageProcessFlow && + timeline && + trailingTimelineItems.length > 0 ? { ...timeline, items: trailingTimelineItems } : null; const hasTrailingArtifactTimelineItems = trailingTimelineItems.some( @@ -2229,7 +2276,7 @@ const MessageListInner: React.FC = ({ shouldCollapseLongHistoricalMessage || shouldFlattenHistoricalAssistantContent ? rendererContent - : rawDisplayContent; + : visibleRawDisplayContent; const rendererContentParts = shouldCollapseLongHistoricalMessage || shouldFlattenHistoricalAssistantContent @@ -2241,9 +2288,10 @@ const MessageListInner: React.FC = ({ const rendererToolCalls = shouldCollapseLongHistoricalMessage ? undefined : conversationToolCalls; - const rendererActionRequests = shouldCollapseLongHistoricalMessage - ? undefined - : msg.actionRequests; + const rendererActionRequests = + shouldCollapseLongHistoricalMessage || shouldSuppressImageProcessFlow + ? undefined + : msg.actionRequests; const rendererMarkdownRenderMode = shouldCollapseLongHistoricalMessage || shouldFlattenHistoricalAssistantContent @@ -2279,10 +2327,11 @@ const MessageListInner: React.FC = ({ knowledgeSaveContent.length >= 24, ); const showMessageActions = - (msg.role === "user" && (canQuoteMessage || canCopyMessage)) || - canSaveMessageAsSkill || - canSaveMessageAsInspiration || - canSaveMessageAsKnowledge; + !msg.imageWorkbenchPreview && + ((msg.role === "user" && (canQuoteMessage || canCopyMessage)) || + canSaveMessageAsSkill || + canSaveMessageAsInspiration || + canSaveMessageAsKnowledge); const messageSavedSiteContentTarget = msg.role === "assistant" ? resolveLatestProjectFileSavedSiteContentTargetFromMessage(msg) @@ -2290,10 +2339,11 @@ const MessageListInner: React.FC = ({ const shouldRenderMessageCanvasShortcut = Boolean( messageSavedSiteContentTarget && onOpenSavedSiteContent && + !msg.imageWorkbenchPreview && !hasTrailingArtifactTimelineItems, ); const visibleAssistantArtifacts = - msg.role === "assistant" + msg.role === "assistant" && !shouldSuppressImageProcessFlow ? (msg.artifacts || []).filter( (artifact) => !isHiddenConversationArtifactPath( @@ -2324,12 +2374,13 @@ const MessageListInner: React.FC = ({ const shouldRenderFirstTokenRuntimeStatus = msg.role === "assistant" && msg.isThinking && + !shouldSuppressImageProcessFlow && !shouldRenderRuntimeStatusPill(msg.runtimeStatus) && !hasVisibleAssistantText && !conversationContentParts?.length && !conversationThinkingContent?.trim() && !conversationToolCalls?.length && - !((msg.actionRequests || []).length > 0) && + !((rendererActionRequests || []).length > 0) && !((msg.images || []).length > 0) && visibleAssistantArtifacts.length === 0 && !shouldRenderMessageCanvasShortcut && @@ -2341,7 +2392,7 @@ const MessageListInner: React.FC = ({ !conversationContentParts?.length && !conversationThinkingContent?.trim() && !conversationToolCalls?.length && - !((msg.actionRequests || []).length > 0) && + !((rendererActionRequests || []).length > 0) && !primaryTimeline && !trailingTimeline && !((msg.images || []).length > 0) && @@ -2370,17 +2421,21 @@ const MessageListInner: React.FC = ({ activeConversationRuntimeStatusLine?.status === "queued") && !shouldPreviewHistoricalAssistantMessage && !shouldDeferHistoricalMarkdownRender; + const shouldSuppressAssistantMetaFooter = shouldSuppressImageProcessFlow; const shouldRenderUsageFooter = isConversationTailAssistant && + !shouldSuppressAssistantMetaFooter && !shouldRenderTailRuntimeStatusLine && !shouldRenderActiveRuntimeFooterIndicator && !msg.isThinking && Boolean(msg.usage); const shouldRenderStatusPill = + !shouldSuppressAssistantMetaFooter && !shouldRenderTailRuntimeStatusLine && shouldRenderRuntimeStatusPill(msg.runtimeStatus); const assistantMetaFooter = msg.role === "assistant" && + !shouldSuppressAssistantMetaFooter && (shouldRenderTailRuntimeStatusLine || shouldRenderActiveRuntimeFooterIndicator || shouldRenderStatusPill || @@ -2491,6 +2546,16 @@ const MessageListInner: React.FC = ({ msg.role === "assistant" && Boolean(primaryTimelineNode) && hasVisibleAssistantText; + const shouldRenderImageWorkbenchBareBubble = + msg.role === "assistant" && + Boolean(msg.imageWorkbenchPreview) && + !primaryTimeline && + !trailingTimeline && + !hasImages && + visibleAssistantArtifacts.length === 0 && + !shouldRenderMessageCanvasShortcut && + !msg.taskPreview && + !assistantMetaFooter; return ( = ({ {hasAssistantBodyContent ? ( {msg.role === "assistant" ? ( @@ -2546,6 +2612,8 @@ const MessageListInner: React.FC = ({ + ) : msg.imageWorkbenchPreview && hasVisibleAssistantText ? ( + ) : ( = ({ ) : null} {msg.imageWorkbenchPreview ? ( - - onOpenMessagePreview( - { - kind: "image_workbench", - preview, - }, - msg, - ) - : undefined - } - /> +
+ + onOpenMessagePreview( + { + kind: "image_workbench", + preview, + }, + msg, + ) + : undefined + } + /> +
) : null} {msg.taskPreview ? ( ( (key, options) => String(t(key as never, options as never)), [t], ); + const translateFormation = useCallback( + (key, options) => String(t(key as never, options as never)), + [t], + ); + const translateActivityPreview = useCallback( + (key, options) => String(t(key as never, options as never)), + [t], + ); + const formationCopy = useMemo( + () => + buildTeamWorkspaceFormationCopy({ + locale, + translate: translateFormation, + }), + [locale, translateFormation], + ); + const activityPreviewCopy = useMemo( + () => + buildActivityPreviewCopy({ + translate: translateActivityPreview, + }), + [translateActivityPreview], + ); const [selectedWorkbenchItem, setSelectedWorkbenchItem] = useState(null); const [ @@ -863,6 +895,7 @@ export function TeamWorkbenchSummaryPanel({ }); const hasRuntimeSessions = executionSummary.totalSessionCount > 0; const runtimeFormationDisplay = buildRuntimeFormationDisplayState({ + copy: formationCopy, teamDispatchPreviewState: dispatchPreviewState, fallbackLabel: selectedTeamLabel, fallbackSummary: selectedTeamSummary, @@ -993,6 +1026,7 @@ export function TeamWorkbenchSummaryPanel({ const snapshot = extractSessionActivitySnapshot( detail, TRANSCRIPT_HISTORY_ENTRY_LIMIT, + { copy: activityPreviewCopy }, ); setSelectedTranscriptHistoryState({ sessionId, @@ -1011,7 +1045,9 @@ export function TeamWorkbenchSummaryPanel({ entries: [], queuedTurns: [], errorMessage: - error instanceof Error ? error.message : "读取历史正文失败", + error instanceof Error + ? error.message + : activityPreviewCopy.historyReadFailed, }); }); @@ -1019,6 +1055,7 @@ export function TeamWorkbenchSummaryPanel({ cancelled = true; }; }, [ + activityPreviewCopy, selectedTranscriptChildSessionId, selectedTranscriptLiveActivityEntries.length, ]); @@ -1156,16 +1193,24 @@ export function TeamWorkbenchSummaryPanel({ ? executionSummary.totalSessionCount : (dispatchPreviewState?.members.length ?? selectedRoleCount); const roleCards = dispatchPreviewState?.members.length - ? dispatchPreviewState.members.map((member) => ({ - id: member.id, - label: normalizeTeamWorkspaceDisplayValue(member.label) || member.label, - roleKey: member.roleKey, - profileId: member.profileId, - summary: - normalizeTeamWorkspaceDisplayValue(member.summary) || member.summary, - skillIds: member.skillIds, - statusMeta: resolveRuntimeMemberStatusMeta(member.status), - })) + ? dispatchPreviewState.members.map((member) => { + const statusMeta = resolveRuntimeMemberStatusMeta(member.status); + return { + id: member.id, + label: + normalizeTeamWorkspaceDisplayValue(member.label) || member.label, + roleKey: member.roleKey, + profileId: member.profileId, + summary: + normalizeTeamWorkspaceDisplayValue(member.summary) || + member.summary, + skillIds: member.skillIds, + statusMeta: { + badgeClassName: statusMeta.badgeClassName, + label: formationCopy.getMemberStatusLabel(member.status), + }, + }; + }) : (selectedTeamRoles ?? []).map((role) => ({ id: role.id, label: normalizeTeamWorkspaceDisplayValue(role.label) || role.label, @@ -1947,7 +1992,11 @@ export function TeamWorkbenchSummaryPanel({
- {dispatchPreviewState ? "当前任务分工" : "角色分工"} + + {dispatchPreviewState + ? formationCopy.detailRoleSectionRuntimeLabel + : formationCopy.detailRoleSectionPlanLabel} +
{roleCards.map((role) => ( diff --git a/src/components/agent/chat/components/TeamWorkspaceDock.test.tsx b/src/components/agent/chat/components/TeamWorkspaceDock.test.tsx index 2dd7a0aee..e8ee17bcc 100644 --- a/src/components/agent/chat/components/TeamWorkspaceDock.test.tsx +++ b/src/components/agent/chat/components/TeamWorkspaceDock.test.tsx @@ -2,6 +2,7 @@ import React from "react"; import { act } from "react"; import { createRoot, type Root } from "react-dom/client"; import { afterEach, beforeEach, describe, expect, it, vi } from "vitest"; +import { changeLimeLocale } from "@/i18n/createI18n"; import type { AsterSessionDetail } from "@/lib/api/agentRuntime"; import { TeamWorkspaceDock } from "./TeamWorkspaceDock"; @@ -71,12 +72,13 @@ vi.mock("@/components/ui/button", () => ({ const mountedRoots: Array<{ root: Root; container: HTMLDivElement }> = []; -beforeEach(() => { +beforeEach(async () => { ( globalThis as typeof globalThis & { IS_REACT_ACT_ENVIRONMENT?: boolean; } ).IS_REACT_ACT_ENVIRONMENT = true; + await changeLimeLocale("zh-CN"); mockGetAgentRuntimeSession.mockImplementation(async (sessionId: string) => createSessionDetail(sessionId), ); @@ -85,7 +87,7 @@ beforeEach(() => { mockParseAgentEvent.mockImplementation((payload: unknown) => payload); }); -afterEach(() => { +afterEach(async () => { while (mountedRoots.length > 0) { const mounted = mountedRoots.pop(); if (!mounted) break; @@ -95,6 +97,7 @@ afterEach(() => { mounted.container.remove(); } vi.clearAllMocks(); + await changeLimeLocale("zh-CN"); }); function createSessionDetail( @@ -427,6 +430,73 @@ describe("TeamWorkspaceDock", () => { ).toBeNull(); }); + it("Dock current progress chrome 应读取英文资源", async () => { + await changeLimeLocale("en-US"); + + const { container } = await renderDock({ + childSubagentSessions: [ + { + id: "child-en", + name: "Researcher", + created_at: 1_710_000_000, + updated_at: 1_710_000_100, + session_type: "sub_agent", + runtime_status: "running", + task_summary: "Keep runtime task summary as data", + role_hint: "explorer", + team_active_count: 1, + team_parallel_budget: 2, + team_queued_count: 1, + }, + ], + }); + + const toggleButton = container.querySelector( + '[data-testid="team-workspace-dock-toggle"]', + ); + + expect(toggleButton?.textContent).toContain("Current progress"); + expect(toggleButton?.textContent).toContain("1/2 running"); + expect(toggleButton?.textContent).toContain("1 waiting"); + expect(toggleButton?.getAttribute("aria-label")).toBe( + "Expand current progress", + ); + expect(toggleButton?.textContent).not.toContain("当前进展"); + expect(toggleButton?.textContent).not.toContain("处理中"); + }); + + it("Dock 空态方案 chrome 应读取英文资源", async () => { + await changeLimeLocale("en-US"); + + const { container } = await renderDock({ + selectedTeamLabel: "Frontend squad", + selectedTeamSummary: "Runtime team summary", + selectedTeamRoles: [ + { + id: "explorer", + label: "Explorer", + summary: "Runtime role summary", + }, + ], + }); + + const toggleButton = container.querySelector( + '[data-testid="team-workspace-dock-toggle"]', + ); + + act(() => { + toggleButton?.click(); + }); + + expect(document.body.textContent).toContain("Generate"); + expect(document.body.textContent).toContain( + "Current team plan: Frontend squad", + ); + expect(document.body.textContent).toContain("Keeps canvas clear"); + expect(document.body.textContent).not.toContain("当前分工方案"); + expect(document.body.textContent).not.toContain("不遮挡画布"); + }); + it("真实 team 图谱折叠时,应显示查看入口和动态提示", async () => { const { container } = await renderDock({ childSubagentSessions: [ diff --git a/src/components/agent/chat/components/TeamWorkspaceDock.tsx b/src/components/agent/chat/components/TeamWorkspaceDock.tsx index 10acecc1e..b7d2c5f1d 100644 --- a/src/components/agent/chat/components/TeamWorkspaceDock.tsx +++ b/src/components/agent/chat/components/TeamWorkspaceDock.tsx @@ -1,7 +1,9 @@ import { useCallback, useEffect, useMemo, useRef, useState } from "react"; import { createPortal } from "react-dom"; +import { useTranslation } from "react-i18next"; import { ChevronDown, ChevronUp, ListTodo } from "lucide-react"; import styled, { css, keyframes } from "styled-components"; +import { formatNumber } from "@/i18n/format"; import type { AsterSubagentParentContext, AsterSubagentSessionInfo, @@ -17,14 +19,13 @@ import { resolveRuntimeMemberStatusMeta, summarizeTeamWorkspaceExecution, } from "../teamWorkspaceRuntime"; -import { buildRuntimeFormationDisplayState } from "../team-workspace-runtime/formationDisplaySelectors"; +import { + buildTeamWorkspaceFormationCopy, + buildRuntimeFormationDisplayState, + type TeamWorkspaceFormationTranslate, +} from "../team-workspace-runtime/formationDisplaySelectors"; import { TeamWorkspaceBoard } from "./TeamWorkspaceBoard"; import type { TeamRoleDefinition } from "../utils/teamDefinitions"; -import { - TEAM_WORKSPACE_PLAN_LABEL, - TEAM_WORKSPACE_SURFACE_TITLE, - TEAM_WORKSPACE_WAITING_HEADLINE, -} from "../utils/teamWorkspaceCopy"; import { normalizeTeamWorkspaceDisplayValue } from "../utils/teamWorkspaceDisplay"; const DockContainer = styled.div<{ @@ -566,6 +567,64 @@ export function TeamWorkspaceDock({ selectedTeamRoles = [], teamDispatchPreviewState = null, }: TeamWorkspaceDockProps) { + const { i18n, t } = useTranslation("agent"); + const locale = i18n.resolvedLanguage || i18n.language; + const translateFormation = useMemo( + () => (key, options) => String(t(key as never, options as never)), + [t], + ); + const formationCopy = useMemo( + () => + buildTeamWorkspaceFormationCopy({ + locale, + translate: translateFormation, + }), + [locale, translateFormation], + ); + const dockCopy = useMemo(() => { + const formatCount = (count: number) => formatNumber(count, { locale }); + + return { + currentProgress: String( + t("agentChat.teamWorkspace.dock.currentProgress"), + ), + surfaceTitle: String(t("agentChat.teamWorkspace.dock.surfaceTitle")), + idleStatus: String(t("agentChat.teamWorkspace.runtimeStatus.idle")), + unobstructiveBadge: String( + t("agentChat.teamWorkspace.dock.unobstructiveBadge"), + ), + openAria: String(t("agentChat.teamWorkspace.dock.aria.open")), + collapseAria: String(t("agentChat.teamWorkspace.dock.aria.collapse")), + expandAria: String(t("agentChat.teamWorkspace.dock.aria.expand")), + formatProcessingBudget: (activeCount: number, totalCount: number) => + String( + t("agentChat.teamWorkspace.dock.processingBudget", { + activeCount: formatCount(activeCount), + totalCount: formatCount(totalCount), + }), + ), + formatProcessingCount: (count: number) => + String( + t("agentChat.teamWorkspace.dock.processingCount", { + formattedCount: formatCount(count), + }), + ), + formatWaitingCount: (count: number) => + String( + t("agentChat.teamWorkspace.dock.waitingCount", { + formattedCount: formatCount(count), + }), + ), + formatTaskCount: (count: number) => + String( + t("agentChat.teamWorkspace.dock.taskCount", { + formattedCount: formatCount(count), + }), + ), + formatSelectedPlan: (label: string) => + String(t("agentChat.teamWorkspace.dock.selectedPlan", { label })), + }; + }, [locale, t]); const dispatchPreviewState = teamDispatchPreviewState; const launcherOnly = typeof onActivateWorkbench === "function"; const executionSummary = summarizeTeamWorkspaceExecution({ @@ -580,6 +639,7 @@ export function TeamWorkspaceDock({ const hasRuntimeSessions = executionSummary.totalSessionCount > 0; const hasRuntimeFormation = Boolean(dispatchPreviewState); const runtimeFormationDisplay = buildRuntimeFormationDisplayState({ + copy: formationCopy, teamDispatchPreviewState: dispatchPreviewState, fallbackLabel: selectedTeamLabel, fallbackSummary: selectedTeamSummary, @@ -637,14 +697,21 @@ export function TeamWorkspaceDock({ ); const teamConcurrencyBadgeText = teamConcurrencySnapshot?.team_parallel_budget !== undefined - ? `${teamConcurrencySnapshot.team_active_count ?? runningRuntimeSessionCount}/${teamConcurrencySnapshot.team_parallel_budget} 处理中` + ? dockCopy.formatProcessingBudget( + teamConcurrencySnapshot.team_active_count ?? + runningRuntimeSessionCount, + teamConcurrencySnapshot.team_parallel_budget, + ) : runningRuntimeSessionCount > 0 - ? `${runningRuntimeSessionCount} 项处理中` + ? dockCopy.formatProcessingCount(runningRuntimeSessionCount) : null; const teamQueueBadgeText = (teamConcurrencySnapshot?.team_queued_count ?? queuedRuntimeSessionCount) > 0 - ? `${teamConcurrencySnapshot?.team_queued_count ?? queuedRuntimeSessionCount} 项等待中` + ? dockCopy.formatWaitingCount( + teamConcurrencySnapshot?.team_queued_count ?? + queuedRuntimeSessionCount, + ) : null; const teamQueueReason = normalizeTeamWorkspaceDisplayValue( teamConcurrencySnapshot?.queue_reason, @@ -658,24 +725,31 @@ export function TeamWorkspaceDock({ } if (dispatchPreviewState?.status === "forming") { - return { label: "准备中", tone: "active" }; + return { + label: formationCopy.getFormationStatusLabel("forming"), + tone: "active", + }; } if (dispatchPreviewState?.status === "failed") { - return { label: "失败", tone: "error" }; + return { + label: formationCopy.getFormationStatusLabel("failed"), + tone: "error", + }; } if (hasRuntimeSessions) { if (activeRuntimeSessionCount > 0) { return { label: - teamConcurrencyBadgeText || `${activeRuntimeSessionCount} 项处理中`, + teamConcurrencyBadgeText || + dockCopy.formatProcessingCount(activeRuntimeSessionCount), tone: "active", }; } return { - label: `${executionSummary.totalSessionCount} 项任务`, + label: dockCopy.formatTaskCount(executionSummary.totalSessionCount), tone: executionSummary.totalSessionCount > 0 ? "success" : "idle", }; } @@ -683,25 +757,32 @@ export function TeamWorkspaceDock({ if (dispatchPreviewState?.status === "formed") { return runtimeTaskCount > 0 ? { - label: `${runtimeTaskCount} 项任务`, + label: formationCopy.formatTaskCountBadge(runtimeTaskCount), tone: "success", } : { - label: runtimeFormationDisplay.panelStatusLabel || "已就绪", + label: + runtimeFormationDisplay.panelStatusLabel || + formationCopy.getFormationStatusLabel("formed"), tone: "success", }; } if (runtimeTeamLabel) { - return { label: "已准备", tone: "idle" }; + return { + label: formationCopy.getFormationStatusLabel("formed"), + tone: "idle", + }; } - return { label: "待开始", tone: "idle" }; + return { label: dockCopy.idleStatus, tone: "idle" }; }, [ activeRuntimeSessionCount, executionSummary.totalSessionCount, + formationCopy, hasRuntimeSessions, launcherOnly, + dockCopy, teamConcurrencyBadgeText, runtimeFormationDisplay.panelStatusLabel, runtimeTaskCount, @@ -732,7 +813,7 @@ export function TeamWorkspaceDock({ return [ { - label: `${executionSummary.totalSessionCount} 项任务`, + label: dockCopy.formatTaskCount(executionSummary.totalSessionCount), tone: executionSummary.totalSessionCount > 0 ? "success" : "idle", }, ]; @@ -740,7 +821,9 @@ export function TeamWorkspaceDock({ if (dispatchPreviewState?.status === "forming") { return [ { - label: runtimeFormationDisplay.panelStatusLabel || "准备中", + label: + runtimeFormationDisplay.panelStatusLabel || + formationCopy.getFormationStatusLabel("forming"), tone: "active", }, ]; @@ -750,8 +833,9 @@ export function TeamWorkspaceDock({ { label: runtimeTaskCount > 0 - ? `${runtimeTaskCount} 项任务` - : runtimeFormationDisplay.panelStatusLabel || "已就绪", + ? formationCopy.formatTaskCountBadge(runtimeTaskCount) + : runtimeFormationDisplay.panelStatusLabel || + formationCopy.getFormationStatusLabel("formed"), tone: "success", }, ]; @@ -759,25 +843,36 @@ export function TeamWorkspaceDock({ if (dispatchPreviewState?.status === "failed") { return [ { - label: runtimeFormationDisplay.panelStatusLabel || "失败", + label: + runtimeFormationDisplay.panelStatusLabel || + formationCopy.getFormationStatusLabel("failed"), tone: "error", }, ]; } - return runtimeTeamLabel ? [{ label: "已准备", tone: "idle" }] : []; + return runtimeTeamLabel + ? [ + { + label: formationCopy.getFormationStatusLabel("formed"), + tone: "idle", + }, + ] + : []; }, [ executionSummary.totalSessionCount, hasRuntimeSessions, runtimeTaskCount, dispatchPreviewState, + formationCopy, + dockCopy, runtimeFormationDisplay.panelStatusLabel, teamConcurrencyBadgeText, teamQueueBadgeText, runtimeTeamLabel, ]); - const launcherPrimaryLabel = runtimeTeamLabel || "当前进展"; - const dockPrimaryLabel = "当前进展"; + const launcherPrimaryLabel = runtimeTeamLabel || dockCopy.currentProgress; + const dockPrimaryLabel = dockCopy.currentProgress; useEffect(() => { const normalizedSessionId = currentSessionId ?? null; @@ -900,7 +995,7 @@ export function TeamWorkspaceDock({ - {TEAM_WORKSPACE_SURFACE_TITLE} + {dockCopy.surfaceTitle} {runtimeFormationDisplay.panelStatusLabel ? ( {runtimeFormationDisplay.panelStatusLabel} @@ -909,7 +1004,7 @@ export function TeamWorkspaceDock({ {runtimeFormationDisplay.panelHeadline || - TEAM_WORKSPACE_WAITING_HEADLINE} + formationCopy.waitingHeadline} {dispatchPreviewState @@ -933,8 +1028,8 @@ export function TeamWorkspaceDock({ {dispatchPreviewState - ? "查看当前任务分工与参考方案" - : "查看当前分工方案"} + ? formationCopy.detailHintRuntimeWithReference + : formationCopy.detailHintPlanOnly}
{teamDetailExpanded ? ( @@ -948,7 +1043,9 @@ export function TeamWorkspaceDock({ {runtimeTeamSummary ? ( - {dispatchPreviewState ? "当前摘要" : "方案摘要"} + {dispatchPreviewState + ? formationCopy.detailSummaryRuntimeLabel + : formationCopy.detailSummaryPlanLabel} {runtimeTeamSummary} @@ -969,7 +1066,7 @@ export function TeamWorkspaceDock({ - {memberStatusMeta.label} + {formationCopy.getMemberStatusLabel(member.status)} {member.roleKey ? ( @@ -1018,11 +1115,13 @@ export function TeamWorkspaceDock({ {runtimeFormationDisplay.referenceLabel ? ( - 参考方案 · {runtimeFormationDisplay.referenceLabel} + {formationCopy.formatReferenceLabelBadge( + runtimeFormationDisplay.referenceLabel, + )} {runtimeBlueprintSummary || - "当前任务分工参考了当前方案里的角色偏好。"} + formationCopy.detailReferenceSummaryDefault} ) : null} @@ -1033,16 +1132,18 @@ export function TeamWorkspaceDock({ {runtimeTeamLabel ? ( - 当前{TEAM_WORKSPACE_PLAN_LABEL}:{runtimeTeamLabel} + {dockCopy.formatSelectedPlan(runtimeTeamLabel)} ) : null} {runtimeTeamSummary ? ( {runtimeTeamSummary} ) : null} {runtimeTaskCount > 0 ? ( - {runtimeTaskCount} 项任务 + + {dockCopy.formatTaskCount(runtimeTaskCount)} + ) : null} - 不遮挡画布 + {dockCopy.unobstructiveBadge} @@ -1083,10 +1184,10 @@ export function TeamWorkspaceDock({ aria-expanded={launcherOnly ? false : expanded} aria-label={ launcherOnly - ? "打开当前进展" + ? dockCopy.openAria : expanded - ? "收起当前进展" - : "展开当前进展" + ? dockCopy.collapseAria + : dockCopy.expandAria } $active={hasRuntimeSessions} $expanded={expanded} diff --git a/src/components/agent/chat/components/team-workspace-board/SelectedSessionInlineActivitySection.tsx b/src/components/agent/chat/components/team-workspace-board/SelectedSessionInlineActivitySection.tsx index a1a839391..69b30fe03 100644 --- a/src/components/agent/chat/components/team-workspace-board/SelectedSessionInlineActivitySection.tsx +++ b/src/components/agent/chat/components/team-workspace-board/SelectedSessionInlineActivitySection.tsx @@ -1,4 +1,6 @@ import { Activity } from "lucide-react"; +import { useTranslation } from "react-i18next"; +import { formatNumber } from "@/i18n/format"; import { cn } from "@/lib/utils"; import type { SessionActivityPreviewState } from "../../team-workspace-runtime/activityPreviewSelectors"; import type { TeamWorkspaceActivityEntry } from "../../teamWorkspaceRuntime"; @@ -24,6 +26,9 @@ export function SelectedSessionInlineActivitySection({ selectedSessionActivityShouldPoll, selectedSessionSupportsActivityPreview, }: SelectedSessionInlineActivitySectionProps) { + const { i18n, t } = useTranslation("agent"); + const locale = i18n.resolvedLanguage || i18n.language; + if (!selectedSessionSupportsActivityPreview) { return null; } @@ -32,10 +37,12 @@ export function SelectedSessionInlineActivitySection({
- 完整进展 + + {t("agentChat.teamWorkspace.selectedSession.activity.heading")} + {selectedSessionActivityShouldPoll ? ( - 处理中自动刷新 + {t("agentChat.teamWorkspace.selectedSession.activity.autoRefresh")} ) : null}
@@ -45,16 +52,17 @@ export function SelectedSessionInlineActivitySection({

) : selectedSessionActivityPreview?.status === "error" ? (

- 最新进展暂不可用: - {selectedSessionActivityPreview.errorMessage ?? "同步失败"} + {t("agentChat.teamWorkspace.selectedSession.activity.errorPrefix")} + {selectedSessionActivityPreview.errorMessage ?? + t("agentChat.teamWorkspace.selectedSession.activity.errorFallback")}

) : selectedSessionActivityPreview?.status === "ready" ? (

- 这项任务暂时还没有可展示的新进展。 + {t("agentChat.teamWorkspace.selectedSession.activity.emptyReady")}

) : (

- 正在同步这项任务的最新进展... + {t("agentChat.teamWorkspace.selectedSession.activity.loading")}

)} @@ -64,9 +72,21 @@ export function SelectedSessionInlineActivitySection({ data-testid="team-workspace-activity-feed" >
- 进展记录 + + {t( + "agentChat.teamWorkspace.selectedSession.activity.feedHeading", + )} + - {selectedSessionActivityEntries.length} 条 + {t( + "agentChat.teamWorkspace.selectedSession.activity.entryCount", + { + formattedCount: formatNumber( + selectedSessionActivityEntries.length, + { locale }, + ), + }, + )}
diff --git a/src/components/agent/chat/components/team-workspace-board/SelectedSessionInlineCollaborationSection.tsx b/src/components/agent/chat/components/team-workspace-board/SelectedSessionInlineCollaborationSection.tsx index 22e07e5cc..4bfc7a938 100644 --- a/src/components/agent/chat/components/team-workspace-board/SelectedSessionInlineCollaborationSection.tsx +++ b/src/components/agent/chat/components/team-workspace-board/SelectedSessionInlineCollaborationSection.tsx @@ -1,4 +1,5 @@ import { Clock3, Loader2 } from "lucide-react"; +import { useTranslation } from "react-i18next"; import { Button } from "@/components/ui/button"; import { Textarea } from "@/components/ui/textarea"; @@ -36,6 +37,8 @@ export function SelectedSessionInlineCollaborationSection({ selectedSessionInputDraft, selectedSessionInputMessage, }: SelectedSessionInlineCollaborationSectionProps) { + const { t } = useTranslation("agent"); + if (!canWaitSelectedSession && !canSendSelectedSessionInput) { return null; } @@ -44,10 +47,14 @@ export function SelectedSessionInlineCollaborationSection({
- 继续处理 + + {t("agentChat.teamWorkspace.selectedSession.collaboration.heading")} + {canWaitSelectedSession ? ( - 可直接查看结果 + {t( + "agentChat.teamWorkspace.selectedSession.collaboration.waitReady", + )} ) : null}
@@ -64,11 +71,15 @@ export function SelectedSessionInlineCollaborationSection({ ) : null} {selectedActionPending && pendingAction === "wait" - ? "等待中..." - : "等待结果 30 秒"} + ? t( + "agentChat.teamWorkspace.selectedSession.collaboration.waitPending", + ) + : t("agentChat.teamWorkspace.selectedSession.collaboration.wait")} - 仅在当前内容确实依赖这项任务结果时使用。 + {t( + "agentChat.teamWorkspace.selectedSession.collaboration.waitHint", + )}
) : null} @@ -79,7 +90,9 @@ export function SelectedSessionInlineCollaborationSection({ onChange={(event) => onSelectedSessionInputDraftChange(event.target.value) } - placeholder="给这项任务补充说明、补充约束,或请它继续推进下一步。" + placeholder={t( + "agentChat.teamWorkspace.selectedSession.collaboration.inputPlaceholder", + )} className="min-h-[96px] resize-y border-slate-200 bg-white text-sm text-slate-700 placeholder:text-slate-400" data-testid="team-workspace-send-input-textarea" /> @@ -94,8 +107,12 @@ export function SelectedSessionInlineCollaborationSection({ ) : null} {selectedActionPending && pendingAction === "send" - ? "发送中..." - : "发送说明"} + ? t( + "agentChat.teamWorkspace.selectedSession.collaboration.sendPending", + ) + : t( + "agentChat.teamWorkspace.selectedSession.collaboration.send", + )} - 这条说明只会发送给当前任务,不影响其他并行任务。 + {t( + "agentChat.teamWorkspace.selectedSession.collaboration.inputHint", + )}
diff --git a/src/components/agent/chat/components/team-workspace-board/SelectedSessionInlineDetail.tsx b/src/components/agent/chat/components/team-workspace-board/SelectedSessionInlineDetail.tsx index e27ca07c8..8feffa275 100644 --- a/src/components/agent/chat/components/team-workspace-board/SelectedSessionInlineDetail.tsx +++ b/src/components/agent/chat/components/team-workspace-board/SelectedSessionInlineDetail.tsx @@ -1,4 +1,5 @@ import { Bot, Clock3 } from "lucide-react"; +import { useTranslation } from "react-i18next"; import type { SessionActivityPreviewState } from "../../team-workspace-runtime/activityPreviewSelectors"; import type { SelectedSessionDetailDisplayState } from "../../team-workspace-runtime/selectedSessionDetailSelectors"; import type { TeamWorkspaceActivityEntry } from "../../teamWorkspaceRuntime"; @@ -76,6 +77,8 @@ export function SelectedSessionInlineDetail({ selectedSessionInputMessage, selectedSessionSupportsActivityPreview, }: SelectedSessionInlineDetailProps) { + const { t } = useTranslation("agent"); + return (
- 任务分工 + + {t("agentChat.teamWorkspace.selectedSession.settings.heading")} +
{detailDisplay.settingBadges.map((badge) => ( @@ -162,7 +167,9 @@ export function SelectedSessionInlineDetail({
- 更新于 {formatUpdatedAt(selectedSession.updatedAt)} + {t("agentChat.teamWorkspace.selectedSession.footer.updatedAt", { + updatedAt: formatUpdatedAt(selectedSession.updatedAt), + })} {detailDisplay.metadata.map((meta) => (
- 当前查看 + + {t("agentChat.teamWorkspace.selectedSession.header.eyebrow")} + {selectedSession.isCurrent ? ( - 当前任务 + {t( + "agentChat.teamWorkspace.selectedSession.header.currentTaskBadge", + )} ) : null}
@@ -76,8 +83,12 @@ export function SelectedSessionInlineHeader({ ) : null} {selectedActionPending && pendingAction === "resume" - ? "继续中..." - : "继续处理"} + ? t( + "agentChat.teamWorkspace.selectedSession.header.action.resumePending", + ) + : t( + "agentChat.teamWorkspace.selectedSession.header.action.resume", + )} ) : null} {canStopSelectedSession ? ( @@ -92,8 +103,12 @@ export function SelectedSessionInlineHeader({ ) : null} {selectedActionPending && pendingAction === "close" - ? "暂停中..." - : "暂停处理"} + ? t( + "agentChat.teamWorkspace.selectedSession.header.action.closePending", + ) + : t( + "agentChat.teamWorkspace.selectedSession.header.action.close", + )} ) : null} {canOpenSelectedSession ? ( @@ -103,7 +118,13 @@ export function SelectedSessionInlineHeader({ variant="outline" onClick={() => void onOpenSelectedSession?.()} > - {isChildSession ? "切换进展" : "打开进展"} + {isChildSession + ? t( + "agentChat.teamWorkspace.selectedSession.header.action.openChild", + ) + : t( + "agentChat.teamWorkspace.selectedSession.header.action.open", + )} ) : null}
diff --git a/src/components/agent/chat/components/team-workspace-board/TeamWorkspaceCanvasStage.tsx b/src/components/agent/chat/components/team-workspace-board/TeamWorkspaceCanvasStage.tsx index 256ac2d8e..fcb2195ac 100644 --- a/src/components/agent/chat/components/team-workspace-board/TeamWorkspaceCanvasStage.tsx +++ b/src/components/agent/chat/components/team-workspace-board/TeamWorkspaceCanvasStage.tsx @@ -4,6 +4,7 @@ import { type ReactNode, type WheelEvent as ReactWheelEvent, } from "react"; +import { useTranslation } from "react-i18next"; import { cn } from "@/lib/utils"; import type { TeamWorkspaceCanvasLane } from "../../team-workspace-runtime/canvasLaneSelectors"; import type { @@ -61,6 +62,8 @@ export function TeamWorkspaceCanvasStage({ viewport, viewportRef, }: TeamWorkspaceCanvasStageProps) { + const { t } = useTranslation("agent"); + return (
- 当前进展 + {t("agentChat.teamWorkspace.canvasStage.badge")} - 拖拽调整任务布局 · Space 平移 · A 整理 · F 聚焦 + {t("agentChat.teamWorkspace.canvasStage.shortcutHint")}
{lanes.length > 0 ? ( @@ -158,7 +161,7 @@ export function TeamWorkspaceCanvasStage({
- 暂无当前进展 + {t("agentChat.teamWorkspace.canvasStage.emptyTitle")}

{canvasStageHint} diff --git a/src/components/agent/chat/components/team-workspace-board/TeamWorkspaceCanvasSurfaceCopy.test.tsx b/src/components/agent/chat/components/team-workspace-board/TeamWorkspaceCanvasSurfaceCopy.test.tsx index 20a429324..a0b7c298d 100644 --- a/src/components/agent/chat/components/team-workspace-board/TeamWorkspaceCanvasSurfaceCopy.test.tsx +++ b/src/components/agent/chat/components/team-workspace-board/TeamWorkspaceCanvasSurfaceCopy.test.tsx @@ -8,6 +8,8 @@ import { TeamWorkspaceCanvasViewButtons, TeamWorkspaceTeamActionButtons, } from "./TeamWorkspaceTeamOverviewControls"; +import { TeamWorkspaceTeamOperationsPanel } from "./TeamWorkspaceTeamOperationsPanel"; +import { SelectedSessionInlineDetail } from "./SelectedSessionInlineDetail"; function renderIntoDocument(element: ReactNode) { const container = document.createElement("div"); @@ -80,6 +82,42 @@ describe("TeamWorkspaceCanvasSurfaceCopy", () => { } }); + it("画布舞台应读取英文资源", async () => { + await changeLimeLocale("en-US"); + + const { container, unmount } = renderIntoDocument( + , + ); + + try { + expect(container.textContent).toContain("Current progress"); + expect(container.textContent).toContain("Drag to arrange tasks"); + expect(container.textContent).toContain("No current progress"); + expect(container.textContent).toContain("Stage hint stays runtime copy."); + expect(container.textContent).not.toContain("拖拽调整任务布局"); + expect(container.textContent).not.toContain("暂无当前进展"); + } finally { + unmount(); + } + }); + it("工具栏应使用当前进展与聚焦进展口径", () => { const { container, unmount } = renderIntoDocument( { unmount(); } }); + + it("团队操作面板应读取英文资源", async () => { + await changeLimeLocale("en-US"); + + const { container, unmount } = renderIntoDocument( + , + ); + + try { + expect(container.textContent).toContain("Task progress"); + expect(container.textContent).toContain("Latest 1"); + expect(container.textContent).toContain("just now"); + expect(container.textContent).not.toContain("任务进展"); + expect(container.textContent).not.toContain("最近 1 条"); + } finally { + unmount(); + } + }); + + it("选中任务 inline chrome 应读取英文资源", async () => { + await changeLimeLocale("en-US"); + + const { container, unmount } = renderIntoDocument( + "just now"} + inlineDetailSectionClassName="section" + inlineTimelineEntryClassName="entry" + inlineTimelineFeedClassName="feed" + isChildSession={false} + onOpenSelectedSession={vi.fn()} + onSelectedSessionAction={vi.fn()} + onSelectedSessionInputDraftChange={vi.fn()} + onSelectedSessionSendInput={vi.fn()} + pendingAction={null} + selectedActionPending={false} + selectedSession={{ + id: "child-1", + isCurrent: true, + updatedAt: Date.now(), + }} + selectedSessionActivityEntries={[ + { + id: "entry-1", + title: "Tool result", + detail: "Runtime detail", + statusLabel: "Completed", + badgeClassName: + "border border-emerald-200 bg-emerald-50 text-emerald-700", + }, + ]} + selectedSessionActivityPreview={{ + entries: [], + preview: null, + status: "ready", + }} + selectedSessionActivityPreviewText={null} + selectedSessionActivityShouldPoll + selectedSessionInputDraft="" + selectedSessionInputMessage="Continue" + selectedSessionSupportsActivityPreview + />, + ); + + try { + expect(container.textContent).toContain("Current view"); + expect(container.textContent).toContain("Current task"); + expect(container.textContent).toContain("Resume work"); + expect(container.textContent).toContain("Pause work"); + expect(container.textContent).toContain("Open progress"); + expect(container.textContent).toContain("Task split"); + expect(container.textContent).toContain("Continue work"); + expect(container.textContent).toContain("Result can be checked directly"); + expect(container.textContent).toContain("Wait 30 seconds for result"); + expect(container.textContent).toContain("Send note"); + expect(container.textContent).toContain("Insert note now"); + expect(container.textContent).toContain("Full progress"); + expect(container.textContent).toContain("Auto-refresh while running"); + expect(container.textContent).toContain( + "This task has no new progress to show yet.", + ); + expect(container.textContent).toContain("Progress log"); + expect(container.textContent).toContain("1 item(s)"); + expect(container.textContent).toContain("Updated just now"); + expect( + container.querySelector("textarea")?.getAttribute("placeholder"), + ).toBe( + "Add instructions, constraints, or ask this task to continue to the next step.", + ); + expect(container.textContent).not.toContain("继续处理"); + expect(container.textContent).not.toContain("完整进展"); + } finally { + unmount(); + } + }); }); diff --git a/src/components/agent/chat/components/team-workspace-board/TeamWorkspaceTeamOperationsPanel.tsx b/src/components/agent/chat/components/team-workspace-board/TeamWorkspaceTeamOperationsPanel.tsx index 2e716bbd0..df1a44353 100644 --- a/src/components/agent/chat/components/team-workspace-board/TeamWorkspaceTeamOperationsPanel.tsx +++ b/src/components/agent/chat/components/team-workspace-board/TeamWorkspaceTeamOperationsPanel.tsx @@ -1,4 +1,6 @@ import { Activity } from "lucide-react"; +import { useTranslation } from "react-i18next"; +import { formatNumber } from "@/i18n/format"; import { cn } from "@/lib/utils"; import { formatOperationUpdatedAt, @@ -18,6 +20,9 @@ export function TeamWorkspaceTeamOperationsPanel({ teamOperationEntries, useCompactCanvasChrome, }: TeamWorkspaceTeamOperationsPanelProps) { + const { i18n, t } = useTranslation("agent"); + const locale = i18n.resolvedLanguage || i18n.language; + if (teamOperationEntries.length === 0) { return null; } @@ -25,9 +30,13 @@ export function TeamWorkspaceTeamOperationsPanel({ const heading = (

- 任务进展 + {t("agentChat.teamWorkspace.operations.heading")} - 最近 {teamOperationEntries.length} 条 + {t("agentChat.teamWorkspace.operations.recentCount", { + formattedCount: formatNumber(teamOperationEntries.length, { + locale, + }), + })}
); @@ -45,7 +54,10 @@ export function TeamWorkspaceTeamOperationsPanel({ {entry.title} - {formatOperationUpdatedAt(entry.updatedAt)} + {formatOperationUpdatedAt(entry.updatedAt, { + locale, + nowLabel: t("agentChat.teamWorkspace.operations.updatedNow"), + })}

{ : 0; expect(operationsOrder).toBeTruthy(); }); + + it("overview chrome 应读取英文资源", async () => { + await changeLimeLocale("en-US"); + + const compactContainer = renderChrome({ + boardChromeDisplay: { + boardHeadline: "Tasks running", + boardHint: "Current tasks keep updating.", + compactBoardHeadline: "Tasks running", + compactToolbarChips: [ + { key: "status", text: "Running", tone: "status", status: "running" }, + ], + statusSummaryBadges: [], + }, + formatUpdatedAt: () => "just now", + memberCanvasSubtitle: "2 current progress lane(s) connected.", + memberCanvasTitle: "Current progress", + selectedSession: { + name: "Researcher", + isCurrent: true, + updatedAt: 1710000000, + }, + }); + + expect(compactContainer.textContent).toContain("Current focus"); + expect(compactContainer.textContent).toContain("Updated just now"); + expect(compactContainer.textContent).toContain("Current task"); + expect(compactContainer.textContent).toContain("Task actions"); + expect(compactContainer.textContent).toContain("View"); + expect(compactContainer.textContent).not.toContain("当前焦点"); + + const expandedContainer = renderChrome({ + boardChromeDisplay: { + boardHeadline: "Tasks running", + boardHint: "Current tasks keep updating.", + compactBoardHeadline: "Tasks running", + compactToolbarChips: [], + statusSummaryBadges: [], + }, + formatUpdatedAt: () => "just now", + memberCanvasSubtitle: "2 current progress lane(s) connected.", + memberCanvasTitle: "Current progress", + selectedSession: { + name: "Researcher", + isCurrent: true, + updatedAt: 1710000000, + }, + useCompactCanvasChrome: false, + }); + + expect(expandedContainer.textContent).toContain("2 task(s) running"); + expect(expandedContainer.textContent).toContain("1 task(s) completed"); + expect(expandedContainer.textContent).not.toContain("项任务正在处理中"); + }); }); diff --git a/src/components/agent/chat/components/team-workspace-board/TeamWorkspaceTeamOverviewChrome.tsx b/src/components/agent/chat/components/team-workspace-board/TeamWorkspaceTeamOverviewChrome.tsx index c125d672a..f0e55027a 100644 --- a/src/components/agent/chat/components/team-workspace-board/TeamWorkspaceTeamOverviewChrome.tsx +++ b/src/components/agent/chat/components/team-workspace-board/TeamWorkspaceTeamOverviewChrome.tsx @@ -1,4 +1,6 @@ import { Clock3 } from "lucide-react"; +import { useTranslation } from "react-i18next"; +import { formatNumber } from "@/i18n/format"; import type { TeamWorkspaceBoardChromeDisplayState } from "../../team-workspace-runtime/boardChromeSelectors"; import type { TeamOperationDisplayEntry } from "../../team-workspace-runtime/teamOperationSelectors"; import type { TeamWorkspaceRuntimeStatus } from "../../teamWorkspaceRuntime"; @@ -68,6 +70,9 @@ export function TeamWorkspaceTeamOverviewChrome({ useCompactCanvasChrome, waitableCount, }: TeamWorkspaceTeamOverviewChromeProps) { + const { i18n, t } = useTranslation("agent"); + const locale = i18n.resolvedLanguage || i18n.language; + const formatCount = (count: number) => formatNumber(count, { locale }); const compactCanvasSummaryChipClassName = "rounded-full border border-slate-200 bg-white px-2.5 py-1 text-[11px] text-slate-500"; const compactCanvasMutedChipClassName = @@ -98,10 +103,19 @@ export function TeamWorkspaceTeamOverviewChrome({

- {selectedSession?.name?.trim() || "等待任务接手"} + {selectedSession?.name?.trim() || + t( + "agentChat.teamWorkspace.overview.selectedSession.waitingTitle", + )}
- {selectedSession ? "当前焦点" : "等待接手"} + {selectedSession + ? t( + "agentChat.teamWorkspace.overview.selectedSession.focusBadge", + ) + : t( + "agentChat.teamWorkspace.overview.selectedSession.waitingBadge", + )}

@@ -112,11 +126,18 @@ export function TeamWorkspaceTeamOverviewChrome({

- 更新于 {formatUpdatedAt(selectedSession.updatedAt)} + {t( + "agentChat.teamWorkspace.selectedSession.footer.updatedAt", + { + updatedAt: formatUpdatedAt(selectedSession.updatedAt), + }, + )} {selectedSession.isCurrent ? ( - 当前任务 + {t( + "agentChat.teamWorkspace.selectedSession.header.currentTaskBadge", + )} ) : null}
@@ -159,7 +180,9 @@ export function TeamWorkspaceTeamOverviewChrome({ data-testid="team-workspace-compact-task-actions" > - 任务处理 + {t( + "agentChat.teamWorkspace.overview.compact.taskActionsLabel", + )} - 视图 + {t("agentChat.teamWorkspace.overview.compact.viewActionsLabel")} - 更新于 {formatUpdatedAt(selectedSession.updatedAt)} + {t( + "agentChat.teamWorkspace.selectedSession.footer.updatedAt", + { + updatedAt: formatUpdatedAt(selectedSession.updatedAt), + }, + )} {selectedSession.isCurrent ? ( - 当前任务 + {t( + "agentChat.teamWorkspace.selectedSession.header.currentTaskBadge", + )} ) : null}
@@ -230,12 +260,16 @@ export function TeamWorkspaceTeamOverviewChrome({ /> {canWaitAnyActiveTeamSession ? ( - {waitableCount} 项任务正在处理中 + {t("agentChat.teamWorkspace.overview.taskCount.waitable", { + formattedCount: formatCount(waitableCount), + })} ) : null} {canCloseCompletedTeamSessions ? ( - {completedCount} 项任务已完成 + {t("agentChat.teamWorkspace.overview.taskCount.completed", { + formattedCount: formatCount(completedCount), + })} ) : null}
diff --git a/src/components/agent/chat/components/team-workspace-board/teamWorkspaceBoardPresentationSelectors.test.ts b/src/components/agent/chat/components/team-workspace-board/teamWorkspaceBoardPresentationSelectors.test.ts index 61d59f631..fb5b0c4e4 100644 --- a/src/components/agent/chat/components/team-workspace-board/teamWorkspaceBoardPresentationSelectors.test.ts +++ b/src/components/agent/chat/components/team-workspace-board/teamWorkspaceBoardPresentationSelectors.test.ts @@ -1,13 +1,38 @@ -import { describe, expect, it } from "vitest"; +import { beforeEach, describe, expect, it } from "vitest"; +import { changeLimeLocale, getLimeI18n } from "@/i18n/createI18n"; import { + buildTeamWorkspaceBoardCopy, buildTeamWorkspaceBoardSurfaceClassNames, resolveTeamWorkspaceBoardCopyState, + type TeamWorkspaceBoardCopy, + type TeamWorkspaceBoardTranslate, } from "./teamWorkspaceBoardPresentationSelectors"; describe("teamWorkspaceBoardPresentationSelectors", () => { + let boardCopy: TeamWorkspaceBoardCopy; + + beforeEach(async () => { + await changeLimeLocale("en-US"); + const translate: TeamWorkspaceBoardTranslate = (key, options) => + String( + getLimeI18n().t( + key as never, + { + ns: "agent", + ...(options ?? {}), + } as never, + ), + ); + boardCopy = buildTeamWorkspaceBoardCopy({ + locale: "en-US", + translate, + }); + }); + it("应为无真实成员画布的 schedule 状态返回稳定文案", () => { expect( resolveTeamWorkspaceBoardCopyState({ + copy: boardCopy, detailExpanded: false, dispatchPreviewStatus: "forming", hasRuntimeSessions: false, @@ -17,13 +42,14 @@ describe("teamWorkspaceBoardPresentationSelectors", () => { visibleSessionsCount: 0, }), ).toMatchObject({ - detailToggleLabel: "查看细节", + detailToggleLabel: "View details", detailVisible: false, - memberCanvasTitle: "当前进展", + memberCanvasTitle: "Current progress", }); expect( resolveTeamWorkspaceBoardCopyState({ + copy: boardCopy, detailExpanded: false, dispatchPreviewStatus: "forming", hasRuntimeSessions: false, @@ -32,9 +58,10 @@ describe("teamWorkspaceBoardPresentationSelectors", () => { shellExpanded: false, visibleSessionsCount: 0, }).memberCanvasSubtitle, - ).toContain("正在准备当前任务分工"); + ).toContain("Preparing the current task split"); expect( resolveTeamWorkspaceBoardCopyState({ + copy: boardCopy, detailExpanded: false, dispatchPreviewStatus: "failed", hasRuntimeSessions: false, @@ -43,11 +70,12 @@ describe("teamWorkspaceBoardPresentationSelectors", () => { shellExpanded: false, visibleSessionsCount: 0, }).memberCanvasSubtitle, - ).toContain("任务分工准备失败"); + ).toContain("task split failed"); }); it("应为真实任务画布与嵌入态返回紧凑壳层样式", () => { const copyState = resolveTeamWorkspaceBoardCopyState({ + copy: boardCopy, detailExpanded: false, dispatchPreviewStatus: null, hasRuntimeSessions: true, @@ -65,7 +93,9 @@ describe("teamWorkspaceBoardPresentationSelectors", () => { useCompactCanvasChrome: true, }); - expect(copyState.memberCanvasSubtitle).toContain("3 条当前进展已接入"); + expect(copyState.memberCanvasSubtitle).toContain( + "3 current progress lane(s) connected", + ); expect(classNames.boardShellClassName).toContain( "lime-workbench-theme-scope", ); diff --git a/src/components/agent/chat/components/team-workspace-board/teamWorkspaceBoardPresentationSelectors.ts b/src/components/agent/chat/components/team-workspace-board/teamWorkspaceBoardPresentationSelectors.ts index 05f2eb7fc..49c3056f8 100644 --- a/src/components/agent/chat/components/team-workspace-board/teamWorkspaceBoardPresentationSelectors.ts +++ b/src/components/agent/chat/components/team-workspace-board/teamWorkspaceBoardPresentationSelectors.ts @@ -1,8 +1,80 @@ import { cn } from "@/lib/utils"; +import { formatNumber } from "@/i18n/format"; import type { TeamWorkspaceRuntimeFormationStatus } from "../../teamWorkspaceRuntime"; import { TEAM_WORKSPACE_CANVAS_STAGE_HEIGHT } from "../../utils/teamWorkspaceCanvas"; +type TeamWorkspaceBoardResourceKey = + | "agentChat.teamWorkspace.board.canvas.title" + | "agentChat.teamWorkspace.board.canvas.subtitle.childRuntime" + | "agentChat.teamWorkspace.board.canvas.subtitle.formation.default" + | "agentChat.teamWorkspace.board.canvas.subtitle.formation.failed" + | "agentChat.teamWorkspace.board.canvas.subtitle.formation.formed" + | "agentChat.teamWorkspace.board.canvas.subtitle.formation.forming" + | "agentChat.teamWorkspace.board.canvas.subtitle.runtime" + | "agentChat.teamWorkspace.board.detail.collapse" + | "agentChat.teamWorkspace.board.detail.expand"; + +export type TeamWorkspaceBoardTranslate = ( + key: TeamWorkspaceBoardResourceKey, + options?: Record, +) => string; + +export interface TeamWorkspaceBoardCopy { + detailCollapse: string; + detailExpand: string; + formatRuntimeCanvasSubtitle: (visibleSessionsCount: number) => string; + memberCanvasSubtitleChildRuntime: string; + memberCanvasSubtitleFormationDefault: string; + memberCanvasSubtitleFormationFailed: string; + memberCanvasSubtitleFormationFormed: string; + memberCanvasSubtitleFormationForming: string; + memberCanvasTitle: string; +} + +export function buildTeamWorkspaceBoardCopy(params: { + locale?: string | null; + translate: TeamWorkspaceBoardTranslate; +}): TeamWorkspaceBoardCopy { + const formatCount = (count: number) => + formatNumber(count, { locale: params.locale }); + + return { + detailCollapse: params.translate( + "agentChat.teamWorkspace.board.detail.collapse", + ), + detailExpand: params.translate( + "agentChat.teamWorkspace.board.detail.expand", + ), + formatRuntimeCanvasSubtitle: (visibleSessionsCount) => + params.translate( + "agentChat.teamWorkspace.board.canvas.subtitle.runtime", + { + formattedCount: formatCount(visibleSessionsCount), + }, + ), + memberCanvasSubtitleChildRuntime: params.translate( + "agentChat.teamWorkspace.board.canvas.subtitle.childRuntime", + ), + memberCanvasSubtitleFormationDefault: params.translate( + "agentChat.teamWorkspace.board.canvas.subtitle.formation.default", + ), + memberCanvasSubtitleFormationFailed: params.translate( + "agentChat.teamWorkspace.board.canvas.subtitle.formation.failed", + ), + memberCanvasSubtitleFormationFormed: params.translate( + "agentChat.teamWorkspace.board.canvas.subtitle.formation.formed", + ), + memberCanvasSubtitleFormationForming: params.translate( + "agentChat.teamWorkspace.board.canvas.subtitle.formation.forming", + ), + memberCanvasTitle: params.translate( + "agentChat.teamWorkspace.board.canvas.title", + ), + }; +} + interface ResolveTeamWorkspaceBoardCopyStateParams { + copy: TeamWorkspaceBoardCopy; detailExpanded: boolean; dispatchPreviewStatus?: TeamWorkspaceRuntimeFormationStatus | null; hasRuntimeSessions: boolean; @@ -41,6 +113,7 @@ interface TeamWorkspaceBoardSurfaceClassNames { } export function resolveTeamWorkspaceBoardCopyState({ + copy, detailExpanded, dispatchPreviewStatus = null, hasRuntimeSessions, @@ -55,20 +128,20 @@ export function resolveTeamWorkspaceBoardCopyState({ : false; return { - detailToggleLabel: detailVisible ? "收起细节" : "查看细节", + detailToggleLabel: detailVisible ? copy.detailCollapse : copy.detailExpand, detailVisible, - memberCanvasTitle: "当前进展", + memberCanvasTitle: copy.memberCanvasTitle, memberCanvasSubtitle: hasRuntimeSessions ? isChildSession - ? "并行任务会在各自面板里持续更新进展和结果,主对话只保留必要摘要。" - : `${visibleSessionsCount} 条当前进展已接入,当前焦点会优先落在正在处理的分工上。` + ? copy.memberCanvasSubtitleChildRuntime + : copy.formatRuntimeCanvasSubtitle(visibleSessionsCount) : dispatchPreviewStatus === "forming" - ? "正在准备当前任务分工,任务拆出后会在这里独立更新进展。" + ? copy.memberCanvasSubtitleFormationForming : dispatchPreviewStatus === "formed" - ? "当前任务分工已经就绪,任务拆出后会在各自进展面板里开始处理。" + ? copy.memberCanvasSubtitleFormationFormed : dispatchPreviewStatus === "failed" - ? "这次任务分工准备失败,暂时无法生成当前进展面板。" - : "任务拆出后,这里会展开为独立的当前进展。", + ? copy.memberCanvasSubtitleFormationFailed + : copy.memberCanvasSubtitleFormationDefault, }; } diff --git a/src/components/agent/chat/components/team-workspace-board/useTeamWorkspaceActivityPreviewSync.ts b/src/components/agent/chat/components/team-workspace-board/useTeamWorkspaceActivityPreviewSync.ts index c08f26843..6d25df168 100644 --- a/src/components/agent/chat/components/team-workspace-board/useTeamWorkspaceActivityPreviewSync.ts +++ b/src/components/agent/chat/components/team-workspace-board/useTeamWorkspaceActivityPreviewSync.ts @@ -4,6 +4,7 @@ import { buildPreviewableRailSessionsSyncKey, collectStaleSessionActivityTargets, extractSessionActivitySnapshot, + type ActivityPreviewCopy, type ActivityPreviewSession, type SelectedSessionActivityState, type SessionActivityPreviewState, @@ -15,6 +16,7 @@ interface UseTeamWorkspaceActivityPreviewSyncParams { activityRefreshVersionBySessionId?: Record; activityTimelineEntryLimit: number; basePreviewableRailSessions: ActivityPreviewSession[]; + copy: ActivityPreviewCopy; pollIntervalMs?: number; selectedSessionActivityState: SelectedSessionActivityState; } @@ -23,6 +25,7 @@ export function useTeamWorkspaceActivityPreviewSync({ activityRefreshVersionBySessionId = {}, activityTimelineEntryLimit, basePreviewableRailSessions, + copy, pollIntervalMs = DEFAULT_ACTIVITY_PREVIEW_POLL_INTERVAL_MS, selectedSessionActivityState, }: UseTeamWorkspaceActivityPreviewSyncParams) { @@ -99,6 +102,7 @@ export function useTeamWorkspaceActivityPreviewSync({ const activitySnapshot = extractSessionActivitySnapshot( detail, activityTimelineEntryLimit, + { copy }, ); const syncedAt = Date.now(); setSessionActivityPreviewById((previous) => ({ @@ -121,7 +125,7 @@ export function useTeamWorkspaceActivityPreviewSync({ entries: previous[sessionId]?.entries ?? [], status: "error", errorMessage: - error instanceof Error ? error.message : "同步最近过程失败", + error instanceof Error ? error.message : copy.syncFailed, fingerprint, refreshVersion, syncedAt: previous[sessionId]?.syncedAt, @@ -131,7 +135,7 @@ export function useTeamWorkspaceActivityPreviewSync({ pendingSessionActivityRequestsRef.current.delete(sessionId); } }, - [activityTimelineEntryLimit], + [activityTimelineEntryLimit, copy], ); useEffect(() => { diff --git a/src/components/agent/chat/components/team-workspace-board/useTeamWorkspaceActivityPreviews.test.tsx b/src/components/agent/chat/components/team-workspace-board/useTeamWorkspaceActivityPreviews.test.tsx index 64a479d6c..27f3ae5e2 100644 --- a/src/components/agent/chat/components/team-workspace-board/useTeamWorkspaceActivityPreviews.test.tsx +++ b/src/components/agent/chat/components/team-workspace-board/useTeamWorkspaceActivityPreviews.test.tsx @@ -2,6 +2,7 @@ import React from "react"; import { act } from "react"; import { createRoot, type Root } from "react-dom/client"; import { afterEach, beforeEach, describe, expect, it, vi } from "vitest"; +import { changeLimeLocale } from "@/i18n/createI18n"; import type { AsterSessionDetail } from "@/lib/api/agentRuntime"; import type { ActivityPreviewSession } from "../../team-workspace-runtime/activityPreviewSelectors"; import { useTeamWorkspaceActivityPreviews } from "./useTeamWorkspaceActivityPreviews"; @@ -113,12 +114,13 @@ function renderHook(props?: Partial) { }; } -beforeEach(() => { +beforeEach(async () => { ( globalThis as typeof globalThis & { IS_REACT_ACT_ENVIRONMENT?: boolean; } ).IS_REACT_ACT_ENVIRONMENT = true; + await changeLimeLocale("zh-CN"); mockGetAgentRuntimeSession.mockReset(); }); @@ -138,6 +140,7 @@ afterEach(async () => { vi.useRealTimers(); vi.clearAllMocks(); + await changeLimeLocale("zh-CN"); }); describe("useTeamWorkspaceActivityPreviews", () => { diff --git a/src/components/agent/chat/components/team-workspace-board/useTeamWorkspaceActivityPreviews.ts b/src/components/agent/chat/components/team-workspace-board/useTeamWorkspaceActivityPreviews.ts index 387e50261..01938b499 100644 --- a/src/components/agent/chat/components/team-workspace-board/useTeamWorkspaceActivityPreviews.ts +++ b/src/components/agent/chat/components/team-workspace-board/useTeamWorkspaceActivityPreviews.ts @@ -1,7 +1,10 @@ import { useMemo } from "react"; +import { useTranslation } from "react-i18next"; import type { TeamWorkspaceActivityEntry } from "../../teamWorkspaceRuntime"; import { + buildActivityPreviewCopy, buildSelectedSessionActivityState, + type ActivityPreviewTranslate, type ActivityPreviewSession, type SelectedSessionActivityState, } from "../../team-workspace-runtime/activityPreviewSelectors"; @@ -26,10 +29,23 @@ export function useTeamWorkspaceActivityPreviews({ selectedBaseSession = null, selectedSession = null, }: UseTeamWorkspaceActivityPreviewsParams) { + const { t } = useTranslation("agent"); + const translateActivityPreview = useMemo( + () => (key, options) => String(t(key as never, options as never)), + [t], + ); + const activityPreviewCopy = useMemo( + () => + buildActivityPreviewCopy({ + translate: translateActivityPreview, + }), + [translateActivityPreview], + ); const selectedSessionActivitySyncTarget = useMemo( () => buildSelectedSessionActivityState({ + copy: activityPreviewCopy, selectedSession, selectedBaseSession, activityRefreshVersionBySessionId, @@ -38,6 +54,7 @@ export function useTeamWorkspaceActivityPreviews({ [ activityRefreshVersionBySessionId, activityTimelineEntryLimit, + activityPreviewCopy, selectedBaseSession, selectedSession, ], @@ -46,6 +63,7 @@ export function useTeamWorkspaceActivityPreviews({ activityRefreshVersionBySessionId, activityTimelineEntryLimit, basePreviewableRailSessions, + copy: activityPreviewCopy, pollIntervalMs, selectedSessionActivityState: selectedSessionActivitySyncTarget, }); @@ -58,10 +76,12 @@ export function useTeamWorkspaceActivityPreviews({ previewBySessionId: sessionActivityPreviewById, activityRefreshVersionBySessionId, activityTimelineEntryLimit, + copy: activityPreviewCopy, }), [ activityRefreshVersionBySessionId, activityTimelineEntryLimit, + activityPreviewCopy, liveActivityBySessionId, selectedBaseSession, selectedSession, diff --git a/src/components/agent/chat/components/team-workspace-board/useTeamWorkspaceBoardCanvasRuntime.ts b/src/components/agent/chat/components/team-workspace-board/useTeamWorkspaceBoardCanvasRuntime.ts index d622fd78e..808a35ea1 100644 --- a/src/components/agent/chat/components/team-workspace-board/useTeamWorkspaceBoardCanvasRuntime.ts +++ b/src/components/agent/chat/components/team-workspace-board/useTeamWorkspaceBoardCanvasRuntime.ts @@ -1,12 +1,15 @@ import { useMemo } from "react"; +import { useTranslation } from "react-i18next"; import type { TeamWorkspaceActivityEntry, TeamWorkspaceRuntimeFormationState, } from "../../teamWorkspaceRuntime"; import type { SessionActivityPreviewState } from "../../team-workspace-runtime/activityPreviewSelectors"; import { + buildTeamWorkspaceCanvasLaneCopy, buildTeamWorkspaceCanvasLanes, type TeamWorkspaceCanvasLane, + type TeamWorkspaceCanvasLaneTranslate, } from "../../team-workspace-runtime/canvasLaneSelectors"; import type { TeamRoleDefinition } from "../../utils/teamDefinitions"; import { buildCanvasStageHint } from "../../utils/teamWorkspaceCanvas"; @@ -53,9 +56,24 @@ export function useTeamWorkspaceBoardCanvasRuntime({ previewBySessionId, teamDispatchPreviewState = null, }: UseTeamWorkspaceBoardCanvasRuntimeParams): TeamWorkspaceBoardCanvasRuntimeState { + const { i18n, t } = useTranslation("agent"); + const locale = i18n.resolvedLanguage || i18n.language; + const translateCanvasLane = useMemo( + () => (key, options) => String(t(key as never, options as never)), + [t], + ); + const canvasLaneCopy = useMemo( + () => + buildTeamWorkspaceCanvasLaneCopy({ + locale, + translate: translateCanvasLane, + }), + [locale, translateCanvasLane], + ); const canvasLanes = useMemo( () => buildTeamWorkspaceCanvasLanes({ + copy: canvasLaneCopy, hasRealTeamGraph, sessions: memberCanvasSessions, runtimeMembers: teamDispatchPreviewState?.members ?? [], @@ -66,6 +84,7 @@ export function useTeamWorkspaceBoardCanvasRuntime({ }), [ activityTimelineEntryLimit, + canvasLaneCopy, hasRealTeamGraph, liveActivityBySessionId, memberCanvasSessions, diff --git a/src/components/agent/chat/components/team-workspace-board/useTeamWorkspaceBoardFormationState.ts b/src/components/agent/chat/components/team-workspace-board/useTeamWorkspaceBoardFormationState.ts index 67be1a385..14aa77c61 100644 --- a/src/components/agent/chat/components/team-workspace-board/useTeamWorkspaceBoardFormationState.ts +++ b/src/components/agent/chat/components/team-workspace-board/useTeamWorkspaceBoardFormationState.ts @@ -1,11 +1,14 @@ import { useMemo } from "react"; +import { useTranslation } from "react-i18next"; import type { TeamWorkspaceRuntimeFormationDisplayState, TeamWorkspaceSelectedTeamPlanDisplayState, } from "../../team-workspace-runtime/formationDisplaySelectors"; import { + buildTeamWorkspaceFormationCopy, buildRuntimeFormationDisplayState, buildSelectedTeamPlanDisplayState, + type TeamWorkspaceFormationTranslate, } from "../../team-workspace-runtime/formationDisplaySelectors"; import type { TeamWorkspaceRuntimeFormationState } from "../../teamWorkspaceRuntime"; import type { TeamRoleDefinition } from "../../utils/teamDefinitions"; @@ -31,6 +34,20 @@ export function useTeamWorkspaceBoardFormationState({ selectedTeamSummary = null, teamDispatchPreviewState = null, }: UseTeamWorkspaceBoardFormationStateParams): TeamWorkspaceBoardFormationState { + const { i18n, t } = useTranslation("agent"); + const locale = i18n.resolvedLanguage || i18n.language; + const translateFormation = useMemo( + () => (key, options) => String(t(key as never, options as never)), + [t], + ); + const formationCopy = useMemo( + () => + buildTeamWorkspaceFormationCopy({ + locale, + translate: translateFormation, + }), + [locale, translateFormation], + ); const normalizedSelectedTeamLabel = selectedTeamLabel?.trim() || null; const normalizedSelectedTeamSummary = selectedTeamSummary?.trim() || null; const plannedRoles = useMemo( @@ -40,20 +57,28 @@ export function useTeamWorkspaceBoardFormationState({ const selectedTeamPlanDisplay = useMemo( () => buildSelectedTeamPlanDisplayState({ + copy: formationCopy, selectedTeamLabel: normalizedSelectedTeamLabel, selectedTeamSummary: normalizedSelectedTeamSummary, selectedTeamRoles: plannedRoles, }), - [normalizedSelectedTeamLabel, normalizedSelectedTeamSummary, plannedRoles], + [ + formationCopy, + normalizedSelectedTeamLabel, + normalizedSelectedTeamSummary, + plannedRoles, + ], ); const runtimeFormationDisplay = useMemo( () => buildRuntimeFormationDisplayState({ + copy: formationCopy, teamDispatchPreviewState, fallbackLabel: normalizedSelectedTeamLabel, fallbackSummary: normalizedSelectedTeamSummary, }), [ + formationCopy, normalizedSelectedTeamLabel, normalizedSelectedTeamSummary, teamDispatchPreviewState, diff --git a/src/components/agent/chat/components/team-workspace-board/useTeamWorkspaceBoardOperationState.test.tsx b/src/components/agent/chat/components/team-workspace-board/useTeamWorkspaceBoardOperationState.test.tsx index 14baba426..86a7b6d7f 100644 --- a/src/components/agent/chat/components/team-workspace-board/useTeamWorkspaceBoardOperationState.test.tsx +++ b/src/components/agent/chat/components/team-workspace-board/useTeamWorkspaceBoardOperationState.test.tsx @@ -2,6 +2,7 @@ import React from "react"; import { act } from "react"; import { createRoot, type Root } from "react-dom/client"; import { afterEach, beforeEach, describe, expect, it } from "vitest"; +import { changeLimeLocale } from "@/i18n/createI18n"; import { useTeamWorkspaceBoardOperationState } from "./useTeamWorkspaceBoardOperationState"; type HookProps = Parameters[0]; @@ -52,12 +53,13 @@ function renderHook(props?: Partial) { }; } -beforeEach(() => { +beforeEach(async () => { ( globalThis as typeof globalThis & { IS_REACT_ACT_ENVIRONMENT?: boolean; } ).IS_REACT_ACT_ENVIRONMENT = true; + await changeLimeLocale("zh-CN"); }); afterEach(async () => { @@ -73,6 +75,7 @@ afterEach(async () => { }); mounted.container.remove(); } + await changeLimeLocale("zh-CN"); }); describe("useTeamWorkspaceBoardOperationState", () => { diff --git a/src/components/agent/chat/components/team-workspace-board/useTeamWorkspaceBoardOperationState.ts b/src/components/agent/chat/components/team-workspace-board/useTeamWorkspaceBoardOperationState.ts index 762caca1f..d860b7375 100644 --- a/src/components/agent/chat/components/team-workspace-board/useTeamWorkspaceBoardOperationState.ts +++ b/src/components/agent/chat/components/team-workspace-board/useTeamWorkspaceBoardOperationState.ts @@ -1,12 +1,15 @@ import { useMemo } from "react"; +import { useTranslation } from "react-i18next"; import type { TeamWorkspaceControlSummary, TeamWorkspaceWaitSummary, } from "../../teamWorkspaceRuntime"; import { buildTeamWorkspaceSessionControlState } from "../../team-workspace-runtime/sessionStateSelectors"; import { + buildTeamWorkspaceOperationCopy, buildVisibleTeamOperationState, type TeamOperationDisplayEntry, + type TeamWorkspaceOperationTranslate, } from "../../team-workspace-runtime/teamOperationSelectors"; import type { TeamSessionCard } from "../../utils/teamWorkspaceSessions"; @@ -37,14 +40,29 @@ export function useTeamWorkspaceBoardOperationState({ teamWaitSummary = null, visibleSessions, }: UseTeamWorkspaceBoardOperationStateParams): TeamWorkspaceBoardOperationState { + const { i18n, t } = useTranslation("agent"); + const locale = i18n.resolvedLanguage || i18n.language; + const translateTeamOperation = useMemo( + () => (key, options) => String(t(key as never, options as never)), + [t], + ); + const operationCopy = useMemo( + () => + buildTeamWorkspaceOperationCopy({ + locale, + translate: translateTeamOperation, + }), + [locale, translateTeamOperation], + ); const teamOperationState = useMemo( () => buildVisibleTeamOperationState({ + copy: operationCopy, railSessions, teamWaitSummary, teamControlSummary, }), - [railSessions, teamControlSummary, teamWaitSummary], + [operationCopy, railSessions, teamControlSummary, teamWaitSummary], ); const sessionControlState = useMemo( () => diff --git a/src/components/agent/chat/components/team-workspace-board/useTeamWorkspaceBoardPresentation.tsx b/src/components/agent/chat/components/team-workspace-board/useTeamWorkspaceBoardPresentation.tsx index 5464ba618..fece3a0f1 100644 --- a/src/components/agent/chat/components/team-workspace-board/useTeamWorkspaceBoardPresentation.tsx +++ b/src/components/agent/chat/components/team-workspace-board/useTeamWorkspaceBoardPresentation.tsx @@ -1,8 +1,15 @@ import { useMemo } from "react"; -import { buildTeamWorkspaceBoardChromeDisplayState } from "../../team-workspace-runtime/boardChromeSelectors"; +import { useTranslation } from "react-i18next"; import { + buildTeamWorkspaceBoardChromeCopy, + buildTeamWorkspaceBoardChromeDisplayState, + type TeamWorkspaceBoardChromeTranslate, +} from "../../team-workspace-runtime/boardChromeSelectors"; +import { + buildSelectedSessionDetailCopy, buildSelectedSessionDetailDisplayState, type SelectedSessionDetailDisplayState, + type SelectedSessionDetailTranslate, } from "../../team-workspace-runtime/selectedSessionDetailSelectors"; import type { TeamWorkspaceRuntimeFormationDisplayState } from "../../team-workspace-runtime/formationDisplaySelectors"; import type { TeamWorkspaceRuntimeFormationStatus } from "../../teamWorkspaceRuntime"; @@ -12,8 +19,10 @@ import { type TeamSessionCard, } from "../../utils/teamWorkspaceSessions"; import { + buildTeamWorkspaceBoardCopy, buildTeamWorkspaceBoardSurfaceClassNames, resolveTeamWorkspaceBoardCopyState, + type TeamWorkspaceBoardTranslate, } from "./teamWorkspaceBoardPresentationSelectors"; interface UseTeamWorkspaceBoardPresentationParams { @@ -83,6 +92,45 @@ export function useTeamWorkspaceBoardPresentation({ zoom, runtimeFormationDisplay, }: UseTeamWorkspaceBoardPresentationParams): TeamWorkspaceBoardPresentationState { + const { i18n, t } = useTranslation("agent"); + const locale = i18n.resolvedLanguage || i18n.language; + const translateBoard = useMemo( + () => (key, options) => String(t(key as never, options as never)), + [t], + ); + const translateBoardChrome = useMemo( + () => (key, options) => String(t(key as never, options as never)), + [t], + ); + const translateSelectedSessionDetail = + useMemo( + () => (key, options) => String(t(key as never, options as never)), + [t], + ); + const boardCopy = useMemo( + () => + buildTeamWorkspaceBoardCopy({ + locale, + translate: translateBoard, + }), + [locale, translateBoard], + ); + const boardChromeCopy = useMemo( + () => + buildTeamWorkspaceBoardChromeCopy({ + locale, + translate: translateBoardChrome, + }), + [locale, translateBoardChrome], + ); + const selectedSessionDetailCopy = useMemo( + () => + buildSelectedSessionDetailCopy({ + locale, + translate: translateSelectedSessionDetail, + }), + [locale, translateSelectedSessionDetail], + ); const hasRuntimeSessions = isChildSession || totalTeamSessions > 0; const useCompactCanvasChrome = hasRuntimeSessions; const { @@ -91,6 +139,7 @@ export function useTeamWorkspaceBoardPresentation({ memberCanvasSubtitle, memberCanvasTitle, } = resolveTeamWorkspaceBoardCopyState({ + copy: boardCopy, detailExpanded, dispatchPreviewStatus, hasRuntimeSessions, @@ -103,6 +152,7 @@ export function useTeamWorkspaceBoardPresentation({ const boardChromeDisplay = useMemo( () => buildTeamWorkspaceBoardChromeDisplayState({ + copy: boardChromeCopy, hasRuntimeSessions, runtimeFormationTitle: hasRuntimeFormation ? runtimeFormationDisplay.panelHeadline @@ -124,6 +174,7 @@ export function useTeamWorkspaceBoardPresentation({ canCloseCompletedTeamSessions, canWaitAnyActiveTeamSession, completedCount, + boardChromeCopy, hasRuntimeSessions, hasRuntimeFormation, isChildSession, @@ -149,11 +200,17 @@ export function useTeamWorkspaceBoardPresentation({ const selectedSessionDetailDisplay = useMemo( () => buildSelectedSessionDetailDisplayState({ + copy: selectedSessionDetailCopy, selectedSession, isChildSession, parentSessionName, }), - [isChildSession, parentSessionName, selectedSession], + [ + isChildSession, + parentSessionName, + selectedSession, + selectedSessionDetailCopy, + ], ); const selectedStatusMeta = resolveStatusMeta(selectedSession?.runtimeStatus); const { diff --git a/src/components/agent/chat/hooks/agentChatShared.ts b/src/components/agent/chat/hooks/agentChatShared.ts index 92b42f78d..14dbb54ea 100644 --- a/src/components/agent/chat/hooks/agentChatShared.ts +++ b/src/components/agent/chat/hooks/agentChatShared.ts @@ -8,6 +8,7 @@ import type { AgentRuntimeStatus, Message, MessageImage, + MessageImageWorkbenchPreview, WriteArtifactContext, } from "../types"; import type { ChatToolPreferences } from "../utils/chatToolPreferences"; @@ -73,6 +74,8 @@ export interface AssistantDraftState { content?: string; initialRuntimeStatus?: AgentRuntimeStatus; waitingRuntimeStatus?: AgentRuntimeStatus; + imageWorkbenchPreview?: MessageImageWorkbenchPreview; + preserveContent?: boolean; } export interface SlashSkillRequest { diff --git a/src/components/agent/chat/hooks/agentStreamEventProcessor.ts b/src/components/agent/chat/hooks/agentStreamEventProcessor.ts index db8cbd1da..43dfd9661 100644 --- a/src/components/agent/chat/hooks/agentStreamEventProcessor.ts +++ b/src/components/agent/chat/hooks/agentStreamEventProcessor.ts @@ -21,7 +21,12 @@ import { resolveArtifactProtocolFilePath, resolveArtifactProtocolPreviewText, } from "@/lib/artifact-protocol"; -import type { ActionRequired, Message, WriteArtifactContext } from "../types"; +import type { + ActionRequired, + Message, + MessageImageWorkbenchPreview, + WriteArtifactContext, +} from "../types"; import { activityLogger } from "@/lib/workspace/workbenchRuntime"; import { extractQuestionsFromRequestedSchema, @@ -98,6 +103,89 @@ function isFileMutationToolName(toolName: string): boolean { ].some((keyword) => normalized.includes(keyword)); } +function resolveImageTaskPreviewProgressScore( + preview?: MessageImageWorkbenchPreview, +): number { + switch (preview?.status) { + case "complete": + case "failed": + case "cancelled": + return 3; + case "partial": + return 2; + case "running": + return 1; + default: + return 0; + } +} + +function mergeImageTaskPreviewByProgress( + current: MessageImageWorkbenchPreview | undefined, + candidate: MessageImageWorkbenchPreview | undefined, +): MessageImageWorkbenchPreview | undefined { + if (!current) { + return candidate; + } + if (!candidate || candidate.taskId !== current.taskId) { + return current; + } + + const candidateIsAtLeastAsFresh = + resolveImageTaskPreviewProgressScore(candidate) >= + resolveImageTaskPreviewProgressScore(current); + return candidateIsAtLeastAsFresh + ? { + ...current, + ...candidate, + } + : { + ...candidate, + ...current, + }; +} + +function collapseAssistantImageTaskPreviewDuplicates(params: { + messages: Message[]; + assistantMsgId: string; + taskId: string; +}): Message[] { + let mergedPreview: MessageImageWorkbenchPreview | undefined; + const retainedMessages: Message[] = []; + + params.messages.forEach((message) => { + const preview = message.imageWorkbenchPreview; + const isSameImageTask = + message.role === "assistant" && preview?.taskId === params.taskId; + if (message.id === params.assistantMsgId) { + mergedPreview = mergeImageTaskPreviewByProgress(mergedPreview, preview); + retainedMessages.push(message); + return; + } + if (isSameImageTask) { + mergedPreview = mergeImageTaskPreviewByProgress(mergedPreview, preview); + return; + } + retainedMessages.push(message); + }); + + if (!mergedPreview) { + return retainedMessages; + } + + return retainedMessages.map((message) => + message.id === params.assistantMsgId + ? { + ...message, + imageWorkbenchPreview: mergeImageTaskPreviewByProgress( + message.imageWorkbenchPreview, + mergedPreview, + ), + } + : message, + ); +} + function extractPatchPath(rawText?: string): string | undefined { if (!rawText) { return undefined; @@ -964,8 +1052,9 @@ export function handleToolEndEvent({ }); } - setMessages((prev) => - prev.map((message) => { + setMessages((prev) => { + let completedImageTaskId: string | null = null; + const nextMessages = prev.map((message) => { if (message.id !== assistantMsgId) { return message; } @@ -1013,6 +1102,7 @@ export function handleToolEndEvent({ toolResult: normalizedResultRecord, fallbackPrompt: message.content || "图片任务进行中", }); + completedImageTaskId = imageTaskPreview?.taskId || null; const taskPreview = buildTaskPreviewFromToolResult({ toolId: data.tool_id, toolName: currentToolCall?.name || "", @@ -1038,8 +1128,16 @@ export function handleToolEndEvent({ } : message.taskPreview, }; - }), - ); + }); + + return completedImageTaskId + ? collapseAssistantImageTaskPreviewDuplicates({ + messages: nextMessages, + assistantMsgId, + taskId: completedImageTaskId, + }) + : nextMessages; + }); const normalizedResultRecord = normalizedResult && diff --git a/src/components/agent/chat/hooks/agentStreamRuntimeHandler.test.ts b/src/components/agent/chat/hooks/agentStreamRuntimeHandler.test.ts index 8601380d2..e7c039cf7 100644 --- a/src/components/agent/chat/hooks/agentStreamRuntimeHandler.test.ts +++ b/src/components/agent/chat/hooks/agentStreamRuntimeHandler.test.ts @@ -773,6 +773,102 @@ describe("agentStreamRuntimeHandler", () => { expect(getAgentStreamTextOverlay("assistant-1")?.content).toBe("123"); }); + it("图片生成草稿应保留极简文案,不被模型 text_delta 覆盖", () => { + const preservedContent = + "好嘞,用 Nanobanana Pro 给你生成一张广州塔春天照片\n先获取下工具参数\n马上生成"; + let messages: Message[] = [ + { + id: "assistant-image", + role: "assistant", + content: preservedContent, + timestamp: new Date("2026-05-12T10:00:00.000Z"), + isThinking: true, + contentParts: [], + imageWorkbenchPreview: { + taskId: "draft-image-1", + prompt: "一张广州塔春天照片", + mode: "generate", + status: "running", + modelName: "fal-ai/nano-banana-pro", + }, + }, + ]; + const requestState = { + accumulatedContent: "", + queuedTurnId: null, + requestLogId: null, + requestStartedAt: 0, + requestFinished: false, + }; + const setMessages = vi.fn( + (value: Message[] | ((prev: Message[]) => Message[])) => { + messages = typeof value === "function" ? value(messages) : value; + }, + ); + const baseOptions = { + requestState, + callbacks: { + activateStream: vi.fn(), + isStreamActivated: () => true, + clearOptimisticItem: () => {}, + clearOptimisticTurn: () => {}, + disposeListener: () => {}, + removeQueuedDraftMessages: () => {}, + clearActiveStreamIfMatch: () => true, + upsertQueuedTurn: () => {}, + removeQueuedTurnState: () => {}, + playToolcallSound: () => {}, + playTypewriterSound: () => {}, + appendThinkingToParts: (parts: NonNullable) => + parts, + }, + eventName: "agent-runtime-image-draft-test", + pendingTurnKey: "pending-turn", + pendingItemKey: "pending-item", + assistantMsgId: "assistant-image", + activeSessionId: "session-1", + resolvedWorkspaceId: "workspace-1", + effectiveExecutionStrategy: "react" as const, + preserveAssistantContent: preservedContent, + content: "@Nanobanana Pro 生成广州塔春天照片", + runtime: {} as never, + warnedKeysRef: { current: new Set() }, + actionLoggedKeys: new Set(), + toolLogIdByToolId: new Map(), + toolStartedAtByToolId: new Map(), + toolNameByToolId: new Map(), + setMessages: setMessages as never, + setPendingActions: vi.fn() as never, + setThreadItems: vi.fn() as never, + setThreadTurns: vi.fn() as never, + setCurrentTurnId: vi.fn() as never, + setExecutionRuntime: vi.fn() as never, + setIsSending: vi.fn() as never, + }; + + handleTurnStreamEvent({ + ...baseOptions, + data: { + type: "text_delta", + text: "我来为你生成这张照片。", + } as AgentEvent, + }); + + expect(requestState.accumulatedContent).toBe(preservedContent); + expect(messages[0]?.content).toBe(preservedContent); + expect(getAgentStreamTextOverlay("assistant-image")).toBeNull(); + + handleTurnStreamEvent({ + ...baseOptions, + data: { type: "final_done" } as AgentEvent, + }); + + expect(messages[0]?.content).toBe(preservedContent); + expect(messages[0]?.content).not.toContain("我来为你生成"); + expect(messages[0]?.imageWorkbenchPreview?.taskId).toBe("draft-image-1"); + expect(messages[0]?.isThinking).toBe(false); + }); + it("text_delta_batch 应先写入 overlay,并在 final_done 时一次性 reconcile 回消息", () => { vi.useFakeTimers(); let messages: Message[] = [ diff --git a/src/components/agent/chat/hooks/agentStreamRuntimeHandler.ts b/src/components/agent/chat/hooks/agentStreamRuntimeHandler.ts index 3b8e0afa0..260c78fa7 100644 --- a/src/components/agent/chat/hooks/agentStreamRuntimeHandler.ts +++ b/src/components/agent/chat/hooks/agentStreamRuntimeHandler.ts @@ -164,6 +164,7 @@ interface StreamRequestState { prefilledMessageSnapshotReplayOffset?: number; prefilledMessageSnapshotText?: string | null; renderedContent?: string; + preservedAssistantContentInitialized?: boolean; hiddenThinkingPartsCleared?: boolean; performanceTrace?: AgentUiPerformanceTraceMetadata | null; agentUiEventSequence?: number; @@ -236,6 +237,7 @@ interface HandleTurnStreamEventOptions { resolvedWorkspaceId: string; effectiveExecutionStrategy: AsterExecutionStrategy; surfaceThinkingDeltas?: boolean; + preserveAssistantContent?: string | null; content: string; runtime: AgentRuntimeAdapter; webSearch?: boolean; @@ -319,6 +321,7 @@ export function handleTurnStreamEvent({ resolvedWorkspaceId, effectiveExecutionStrategy, surfaceThinkingDeltas = true, + preserveAssistantContent, content, runtime, webSearch, @@ -350,6 +353,16 @@ export function handleTurnStreamEvent({ playTypewriterSound, appendThinkingToParts, } = callbacks; + const preservedAssistantContent = preserveAssistantContent?.trim() || null; + const shouldPreserveAssistantContent = Boolean(preservedAssistantContent); + if ( + preservedAssistantContent && + !requestState.preservedAssistantContentInitialized + ) { + requestState.accumulatedContent = preservedAssistantContent; + requestState.renderedContent = preservedAssistantContent; + requestState.preservedAssistantContentInitialized = true; + } const projectionEvents = buildAgentUiProjectionEvents(data, { sequence: (requestState.agentUiEventSequence ?? 0) + 1, @@ -669,6 +682,7 @@ export function handleTurnStreamEvent({ { const snapshotText = extractVisibleTextFromAgentMessage(data.message); const shouldPrefillVisibleText = + !shouldPreserveAssistantContent && data.message.role === "assistant" && !requestState.renderedContent && !requestState.accumulatedContent && @@ -982,6 +996,9 @@ export function handleTurnStreamEvent({ case "text_delta_batch": { activateStream(); clearOptimisticItem(); + if (shouldPreserveAssistantContent) { + break; + } let visibleTextDelta = data.text; { const visibleDelta = resolveVisibleTextDeltaAfterSnapshotPrefill({ diff --git a/src/components/agent/chat/hooks/agentStreamSubmissionLifecycle.ts b/src/components/agent/chat/hooks/agentStreamSubmissionLifecycle.ts index 5fc753558..d9457403d 100644 --- a/src/components/agent/chat/hooks/agentStreamSubmissionLifecycle.ts +++ b/src/components/agent/chat/hooks/agentStreamSubmissionLifecycle.ts @@ -47,6 +47,7 @@ export interface StreamRequestState { queuedDraftCleanupTimerId?: ReturnType | null; pendingTextRenderTimerId?: ReturnType | null; renderedContent?: string; + preservedAssistantContentInitialized?: boolean; performanceTrace?: AgentUiPerformanceTraceMetadata | null; } diff --git a/src/components/agent/chat/hooks/agentStreamSubmitDraft.test.ts b/src/components/agent/chat/hooks/agentStreamSubmitDraft.test.ts index a5a560e75..d7c4165d8 100644 --- a/src/components/agent/chat/hooks/agentStreamSubmitDraft.test.ts +++ b/src/components/agent/chat/hooks/agentStreamSubmitDraft.test.ts @@ -153,6 +153,61 @@ describe("agentStreamSubmitDraft", () => { expect(isSending).toBe(true); }); + it("图片 assistant draft 应直接挂载图片轻卡预览", () => { + let messages: Message[] = []; + let isSending = false; + + prepareAgentStreamSubmitDraft({ + content: "@Nanobanana Pro 生成广州塔春天照片", + images: [], + skipUserMessage: false, + expectingQueue: false, + assistantMsgId: "assistant-image", + userMsgId: "user-image", + assistantDraft: { + content: + "好嘞,用 Nanobanana Pro 给你生成一张广州塔春天照片\n先获取下工具参数\n马上生成", + preserveContent: true, + imageWorkbenchPreview: { + taskId: "draft-image-1", + prompt: "一张广州塔春天照片", + mode: "generate", + status: "running", + modelName: "fal-ai/nano-banana-pro", + expectedImageCount: 1, + }, + }, + effectiveExecutionStrategy: "react", + webSearch: false, + thinking: false, + setMessages: createStateSetter( + () => messages, + (value) => { + messages = value; + }, + ), + setIsSending: createStateSetter( + () => isSending, + (value) => { + isSending = value; + }, + ), + }); + + expect(messages[1]).toMatchObject({ + id: "assistant-image", + content: + "好嘞,用 Nanobanana Pro 给你生成一张广州塔春天照片\n先获取下工具参数\n马上生成", + imageWorkbenchPreview: { + taskId: "draft-image-1", + prompt: "一张广州塔春天照片", + status: "running", + modelName: "fal-ai/nano-banana-pro", + }, + }); + expect(isSending).toBe(true); + }); + it("带性能 trace 时应记录 assistant 草稿插入与绘制指标", () => { let messages: Message[] = []; let isSending = false; diff --git a/src/components/agent/chat/hooks/agentStreamSubmitDraft.ts b/src/components/agent/chat/hooks/agentStreamSubmitDraft.ts index 17f763eea..e0425e95c 100644 --- a/src/components/agent/chat/hooks/agentStreamSubmitDraft.ts +++ b/src/components/agent/chat/hooks/agentStreamSubmitDraft.ts @@ -103,6 +103,7 @@ export function prepareAgentStreamSubmitDraft( skipUserMessage, }), purpose: messagePurpose, + imageWorkbenchPreview: assistantDraft?.imageWorkbenchPreview, }; if (skipUserMessage) { diff --git a/src/components/agent/chat/hooks/agentStreamSubmitExecution.ts b/src/components/agent/chat/hooks/agentStreamSubmitExecution.ts index 0d68a9e4a..20c0d1f48 100644 --- a/src/components/agent/chat/hooks/agentStreamSubmitExecution.ts +++ b/src/components/agent/chat/hooks/agentStreamSubmitExecution.ts @@ -200,6 +200,10 @@ export async function executeAgentStreamSubmit( performanceTrace, activateStream: callbacks.activateStream, }); + const preserveAssistantContent = + assistantDraft?.preserveContent === true + ? assistantDraft.content?.trim() || null + : null; const unlisten = await registerAgentStreamTurnEventBinding({ runtime, @@ -222,6 +226,7 @@ export async function executeAgentStreamSubmit( pendingTurnKey, pendingItemKey, effectiveWaitingRuntimeStatus, + preserveAssistantContent, warnedKeysRef, actionLoggedKeys, toolLogIdByToolId, diff --git a/src/components/agent/chat/hooks/agentStreamTurnEventBinding.ts b/src/components/agent/chat/hooks/agentStreamTurnEventBinding.ts index cad0f514f..c4edc5964 100644 --- a/src/components/agent/chat/hooks/agentStreamTurnEventBinding.ts +++ b/src/components/agent/chat/hooks/agentStreamTurnEventBinding.ts @@ -77,6 +77,7 @@ interface RegisterAgentStreamTurnEventBindingOptions { pendingTurnKey: string; pendingItemKey: string; effectiveWaitingRuntimeStatus: NonNullable; + preserveAssistantContent?: string | null; warnedKeysRef: MutableRefObject>; actionLoggedKeys: Set; toolLogIdByToolId: Map; @@ -145,6 +146,7 @@ export async function registerAgentStreamTurnEventBinding( pendingTurnKey, pendingItemKey, effectiveWaitingRuntimeStatus, + preserveAssistantContent, warnedKeysRef, actionLoggedKeys, toolLogIdByToolId, @@ -353,6 +355,7 @@ export async function registerAgentStreamTurnEventBinding( resolvedWorkspaceId, effectiveExecutionStrategy, surfaceThinkingDeltas, + preserveAssistantContent, content, runtime, webSearch, @@ -536,6 +539,7 @@ export async function registerAgentStreamTurnEventBinding( resolvedWorkspaceId, effectiveExecutionStrategy, surfaceThinkingDeltas, + preserveAssistantContent, content, runtime, webSearch, diff --git a/src/components/agent/chat/hooks/useTeamWorkspaceRuntime.test.tsx b/src/components/agent/chat/hooks/useTeamWorkspaceRuntime.test.tsx index 592518811..c5ee6fb75 100644 --- a/src/components/agent/chat/hooks/useTeamWorkspaceRuntime.test.tsx +++ b/src/components/agent/chat/hooks/useTeamWorkspaceRuntime.test.tsx @@ -2,6 +2,7 @@ import React from "react"; import { act } from "react"; import { createRoot, type Root } from "react-dom/client"; import { afterEach, beforeEach, describe, expect, it, vi } from "vitest"; +import { changeLimeLocale } from "@/i18n/createI18n"; import { useTeamWorkspaceRuntime } from "./useTeamWorkspaceRuntime"; import { clearAgentUiProjectionEvents, @@ -85,7 +86,8 @@ async function renderHookProbe(props?: Partial, deps?: HookDeps) { } describe("useTeamWorkspaceRuntime", () => { - beforeEach(() => { + beforeEach(async () => { + await changeLimeLocale("zh-CN"); ( globalThis as typeof globalThis & { IS_REACT_ACT_ENVIRONMENT?: boolean; diff --git a/src/components/agent/chat/hooks/useTeamWorkspaceRuntime.ts b/src/components/agent/chat/hooks/useTeamWorkspaceRuntime.ts index c409a01c6..de00724d9 100644 --- a/src/components/agent/chat/hooks/useTeamWorkspaceRuntime.ts +++ b/src/components/agent/chat/hooks/useTeamWorkspaceRuntime.ts @@ -1,4 +1,5 @@ import { useCallback, useEffect, useMemo, useRef, useState } from "react"; +import { useTranslation } from "react-i18next"; import { defaultAgentRuntimeEventSource, dedupeAgentRuntimeEventNames, @@ -15,7 +16,11 @@ import { type TeamWorkspaceRuntimeSessionSnapshot, type TeamWorkspaceRuntimeStatus, } from "../teamWorkspaceRuntime"; -import { type SessionLiveStreamState } from "../team-workspace-runtime/liveRuntimeProjector"; +import { + buildLiveRuntimeProjectorCopy, + type LiveRuntimeProjectorTranslate, + type SessionLiveStreamState, +} from "../team-workspace-runtime/liveRuntimeProjector"; import { subscribeTeamWorkspaceStatusEvents, subscribeTeamWorkspaceStreamEvents, @@ -122,6 +127,18 @@ export function useTeamWorkspaceRuntime( subagentParentContext = null, } = options; const { eventSource = defaultAgentRuntimeEventSource } = deps; + const { t } = useTranslation("agent"); + const translateLiveRuntime = useCallback( + (key, options) => String(t(key as never, options as never)), + [t], + ); + const liveRuntimeCopy = useMemo( + () => + buildLiveRuntimeProjectorCopy({ + translate: translateLiveRuntime, + }), + [translateLiveRuntime], + ); const [liveRuntimeBySessionId, setLiveRuntimeBySessionId] = useState< Record >({}); @@ -323,6 +340,7 @@ export function useTeamWorkspaceRuntime( setLiveRuntimeBySessionId, setLiveActivityBySessionId, scheduleActivityRefresh, + copy: liveRuntimeCopy, }).then((nextUnsubscribe) => { if (disposed) { nextUnsubscribe(); @@ -340,6 +358,7 @@ export function useTeamWorkspaceRuntime( activeSessionKey, currentSessionId, eventSource, + liveRuntimeCopy, subagentParentContext?.parent_session_id, scheduleActivityRefresh, ]); @@ -370,6 +389,7 @@ export function useTeamWorkspaceRuntime( setLiveRuntimeBySessionId, setLiveActivityBySessionId, scheduleActivityRefresh, + copy: liveRuntimeCopy, }).then((nextUnsubscribe) => { if (disposed) { nextUnsubscribe(); @@ -387,6 +407,7 @@ export function useTeamWorkspaceRuntime( activeSessionKey, activeSnapshots, eventSource, + liveRuntimeCopy, scheduleActivityRefresh, setStreamState, setToolNames, diff --git a/src/components/agent/chat/index.test.tsx b/src/components/agent/chat/index.test.tsx index 5e053ac41..b1c892a33 100644 --- a/src/components/agent/chat/index.test.tsx +++ b/src/components/agent/chat/index.test.tsx @@ -55,6 +55,8 @@ const { mockExecutionRunListGeneralWorkbenchHistory, mockExecutionRunGet, mockSkillExecutionGetDetail, + mockGetAutomationJobs, + mockCreateAutomationJob, mockSkillsGetAll, mockSkillsGetLocal, mockCloseAgentRuntimeSubagent, @@ -121,6 +123,8 @@ const { mockExecutionRunListGeneralWorkbenchHistory: vi.fn(), mockExecutionRunGet: vi.fn(), mockSkillExecutionGetDetail: vi.fn(), + mockGetAutomationJobs: vi.fn(), + mockCreateAutomationJob: vi.fn(), mockSkillsGetAll: vi.fn(), mockSkillsGetLocal: vi.fn(), mockCloseAgentRuntimeSubagent: vi.fn(), @@ -701,6 +705,11 @@ vi.mock("@/lib/api/skill-execution", () => ({ }, })); +vi.mock("@/lib/api/automation", () => ({ + getAutomationJobs: () => mockGetAutomationJobs(), + createAutomationJob: (request: unknown) => mockCreateAutomationJob(request), +})); + vi.mock("@/lib/api/skills", () => ({ skillsApi: { getAll: mockSkillsGetAll, @@ -928,6 +937,53 @@ async function waitForElement( return null; } +function collectPendingA2UIFormIds(): Array { + return mockWorkspacePendingA2UIPanel.mock.calls.map((call) => { + const props = call[0] as + | { + pendingA2UIForm?: { + id?: string; + } | null; + } + | undefined; + return props?.pendingA2UIForm?.id ?? null; + }); +} + +async function waitForPendingA2UIForm( + predicate: (form: { id?: string } | null | undefined) => boolean, + attempts = 30, +) { + for (let attempt = 0; attempt < attempts; attempt += 1) { + const matchedCall = mockWorkspacePendingA2UIPanel.mock.calls + .slice() + .reverse() + .find((call) => { + const props = call[0] as + | { + pendingA2UIForm?: { + id?: string; + } | null; + } + | undefined; + return predicate(props?.pendingA2UIForm); + }); + if (matchedCall) { + const props = matchedCall[0] as + | { + pendingA2UIForm?: { + id?: string; + } | null; + } + | undefined; + return props?.pendingA2UIForm ?? null; + } + await flushEffects(1); + } + + return null; +} + function getSendMessageCall(callIndex = 0) { const call = sharedSendMessageMock.mock.calls[callIndex]; if (!call) { @@ -1146,6 +1202,11 @@ beforeEach(() => { has_workflow: false, workflow_steps: [], }); + mockGetAutomationJobs.mockResolvedValue([]); + mockCreateAutomationJob.mockResolvedValue({ + id: "automation-job-1", + name: "自动化任务", + }); mockSkillsGetAll.mockResolvedValue([]); mockSkillsGetLocal.mockResolvedValue([]); mockCloseAgentRuntimeSubagent.mockResolvedValue({ @@ -7096,7 +7157,6 @@ describe("AgentChatPage 服务技能 A2UI", () => { }, }); await waitForElement(container, '[data-testid="layout-transition"]'); - await flushEffects(12); expect( container @@ -7104,21 +7164,22 @@ describe("AgentChatPage 服务技能 A2UI", () => { ?.getAttribute("data-mode"), ).toBe("chat"); - const latestPendingPanelProps = mockWorkspacePendingA2UIPanel.mock.calls.at( - -1, - )?.[0] as - | { - pendingA2UIForm?: { - id?: string; - components?: Array>; - } | null; - } - | undefined; + const pendingA2UIForm = (await waitForPendingA2UIForm( + (form) => + form?.id === + "service-skill-launch:daily-trend-briefing:daily-trend-briefing:20260409", + )) as { + id?: string; + components?: Array>; + } | null; - expect(latestPendingPanelProps?.pendingA2UIForm?.id).toBe( + expect( + pendingA2UIForm?.id, + `pending A2UI 调用历史:${JSON.stringify(collectPendingA2UIFormIds())}`, + ).toBe( "service-skill-launch:daily-trend-briefing:daily-trend-briefing:20260409", ); - expect(latestPendingPanelProps?.pendingA2UIForm?.components || []).toEqual( + expect(pendingA2UIForm?.components || []).toEqual( expect.arrayContaining([ expect.objectContaining({ id: expect.stringContaining(":prefill-hint"), @@ -7135,6 +7196,53 @@ describe("AgentChatPage 服务技能 A2UI", () => { ]), ); }); + + it("从挂起服务技能补参态新建任务时,应清掉旧补参表单", async () => { + installMockAgentChatUnifiedState(createMockAgentChatUnifiedState()); + + const mounted = mountPage({ + projectId: "project-service-skill-a2ui", + contentId: "content-service-skill-a2ui", + theme: "general", + lockTheme: true, + initialPendingServiceSkillLaunch: { + skillId: "daily-trend-briefing", + requestKey: 20260409, + initialSlotValues: { + industry_keywords: "", + schedule_time: "每天 10:00", + }, + prefillHint: "已根据 Skills 页入口推荐自动预填。", + }, + }); + + const pendingFormBefore = await waitForPendingA2UIForm((form) => + Boolean(form?.id?.includes("service-skill-launch:daily-trend-briefing")), + ); + expect( + pendingFormBefore?.id, + `pending A2UI 调用历史:${JSON.stringify(collectPendingA2UIFormIds())}`, + ).toContain("service-skill-launch"); + + expect(requestTaskCenterDraftTask({ source: "sidebar" })).toBe(true); + await flushEffects(); + mounted.rerender(); + await flushEffects(); + + expect( + mounted.container.querySelector( + '[data-testid^="task-center-tab-task-draft-"][data-active="true"]', + ), + ).not.toBeNull(); + expect( + await waitForElement(mounted.container, '[data-testid="empty-state"]'), + ).not.toBeNull(); + expect( + mounted.container.querySelector( + '[data-testid="workspace-pending-a2ui-panel"]', + ), + ).toBeNull(); + }); }); describe("AgentChatPage 当前 A2UI 事实源", () => { diff --git a/src/components/agent/chat/index.tsx b/src/components/agent/chat/index.tsx index 0e94f1473..d668f1fdd 100644 --- a/src/components/agent/chat/index.tsx +++ b/src/components/agent/chat/index.tsx @@ -38,6 +38,7 @@ export function AgentChatPage(props: AgentChatWorkspaceProps) { const { agentEntry = "claw", initialInputCapability, + preferHomeForInitialInputCapability = false, initialPendingServiceSkillLaunch, initialProjectFileOpenTarget, initialSiteSkillLaunch, @@ -64,7 +65,9 @@ export function AgentChatPage(props: AgentChatWorkspaceProps) { Boolean(initialSiteSkillLaunch) || Boolean(initialPendingServiceSkillLaunch?.skillId?.trim()) || Boolean(initialKnowledgePackSelection?.packName?.trim()) || - Boolean(initialInputCapability?.capabilityRoute) || + (preferHomeForInitialInputCapability + ? false + : Boolean(initialInputCapability?.capabilityRoute)) || Boolean(initialProjectFileOpenTarget?.relativePath?.trim()) || openBrowserAssistOnMount; const shouldForceClawWorkspace = diff --git a/src/components/agent/chat/skill-selection/CharacterMention.test.tsx b/src/components/agent/chat/skill-selection/CharacterMention.test.tsx index 641bd1960..1e357d62a 100644 --- a/src/components/agent/chat/skill-selection/CharacterMention.test.tsx +++ b/src/components/agent/chat/skill-selection/CharacterMention.test.tsx @@ -1160,7 +1160,9 @@ describe("CharacterMention", () => { "当你需要复用本地写作 Skill 时使用。", ); expect(document.body.textContent).toContain("需要:主题、受众与语气要求"); - expect(document.body.textContent).toContain("交付:带着该 Skill 进入生成"); + expect(document.body.textContent).toContain( + "交付:带着这个技能回到首页输入框", + ); }); it("只有最近使用的服务技能时,不应同时出现空态文案", async () => { @@ -2293,7 +2295,9 @@ describe("CharacterMention", () => { ); expect(document.body.textContent).toContain("脚本助手"); expect(document.body.textContent).toContain("需要:主题、受众与语气要求"); - expect(document.body.textContent).toContain("交付:带着该 Skill 进入生成"); + expect(document.body.textContent).toContain( + "交付:带着这个技能回到首页输入框", + ); }); it("slash 面板打开后新增本地 skill 使用记录时,应即时刷新继续上次 Skill分组", async () => { diff --git a/src/components/agent/chat/skill-selection/SkillBadge.tsx b/src/components/agent/chat/skill-selection/SkillBadge.tsx index 40a4e4a7f..0f929c0fc 100644 --- a/src/components/agent/chat/skill-selection/SkillBadge.tsx +++ b/src/components/agent/chat/skill-selection/SkillBadge.tsx @@ -1,5 +1,6 @@ import React from "react"; -import { Zap, X } from "lucide-react"; +import { useTranslation } from "react-i18next"; +import { X } from "lucide-react"; import type { Skill } from "@/lib/api/skills"; interface SkillBadgeProps { @@ -7,12 +8,29 @@ interface SkillBadgeProps { onClear: () => void; } -export const SkillBadge: React.FC = ({ skill, onClear }) => ( -
- - {skill.name} - -
-); +export const SkillBadge: React.FC = ({ skill, onClear }) => { + const { t } = useTranslation("agent"); + const removeLabel = t("skills.inputBadge.remove", { + name: skill.name, + }); + + return ( +
+ @ + {skill.name} + +
+ ); +}; diff --git a/src/components/agent/chat/skill-selection/inputCapabilitySections.test.ts b/src/components/agent/chat/skill-selection/inputCapabilitySections.test.ts index 71cc5aef7..a7e9e9c2f 100644 --- a/src/components/agent/chat/skill-selection/inputCapabilitySections.test.ts +++ b/src/components/agent/chat/skill-selection/inputCapabilitySections.test.ts @@ -159,6 +159,11 @@ describe("buildInputCapabilitySections", () => { label: "配图", commandPrefix: "@配图", }), + createBuiltinCommand({ + key: "image_generate_nanobanana_pro", + label: "Nanobanana Pro", + commandPrefix: "@Nanobanana Pro", + }), createBuiltinCommand({ key: "writing_runtime", label: "写作", @@ -181,7 +186,7 @@ describe("buildInputCapabilitySections", () => { sections .find((section) => section.heading === "生成 / 表达") ?.items.map((item) => item.title), - ).toContain("@写作"); + ).toEqual(expect.arrayContaining(["@Nanobanana Pro", "@写作"])); }); it("slash 空查询应先收成先拿结果与工作台操作,不默认展开提示命令和状态帮助", () => { diff --git a/src/components/agent/chat/skill-selection/inputCapabilitySections.ts b/src/components/agent/chat/skill-selection/inputCapabilitySections.ts index bf78444f7..e059dafe4 100644 --- a/src/components/agent/chat/skill-selection/inputCapabilitySections.ts +++ b/src/components/agent/chat/skill-selection/inputCapabilitySections.ts @@ -439,6 +439,7 @@ const INPUT_COMMAND_GROUP_BY_KEY: Record< webpage_read: "search_read", url_parse: "search_read", image_generate: "generate_expression", + image_generate_nanobanana_pro: "generate_expression", image_storyboard: "generate_expression", cover_generate: "generate_expression", poster_generate: "generate_expression", diff --git a/src/components/agent/chat/styles/index.ts b/src/components/agent/chat/styles/index.ts index ad280c880..0321e856b 100644 --- a/src/components/agent/chat/styles/index.ts +++ b/src/components/agent/chat/styles/index.ts @@ -112,28 +112,43 @@ export const ContentColumn = styled.div<{ $isUser: boolean }>` align-items: ${({ $isUser }) => ($isUser ? "flex-end" : "stretch")}; `; -export const MessageBubble = styled.div<{ $isUser: boolean }>` - width: ${({ $isUser }) => ($isUser ? "fit-content" : "100%")}; - max-width: ${({ $isUser }) => - $isUser ? "min(72%, 560px)" : "min(100%, 1040px)"}; - padding: ${({ $isUser }) => ($isUser ? "12px 16px" : "15px 17px")}; +export const MessageBubble = styled.div<{ + $isUser: boolean; + $bareMedia?: boolean; +}>` + width: ${({ $isUser, $bareMedia }) => + $bareMedia || $isUser ? "fit-content" : "100%"}; + max-width: ${({ $isUser, $bareMedia }) => + $bareMedia + ? "min(100%, 560px)" + : $isUser + ? "min(72%, 560px)" + : "min(100%, 1040px)"}; + padding: ${({ $isUser, $bareMedia }) => + $bareMedia ? "0" : $isUser ? "12px 16px" : "15px 17px"}; display: flex; flex-direction: column; - gap: ${({ $isUser }) => ($isUser ? "8px" : "12px")}; - border-radius: 18px; - border: 1px solid - ${({ $isUser }) => - $isUser - ? "var(--lime-surface-border-strong, rgba(187, 247, 208, 0.72))" - : "var(--lime-surface-border, rgba(226, 240, 226, 0.58))"}; - background: ${({ $isUser }) => - $isUser - ? "linear-gradient(180deg, var(--lime-surface, #ffffff) 0%, var(--lime-brand-soft, #ecfdf5) 100%)" - : "linear-gradient(180deg, var(--lime-surface, #ffffff) 0%, var(--lime-surface-subtle, #fcfff9) 100%)"}; - box-shadow: ${({ $isUser }) => - $isUser - ? "0 16px 36px -30px rgba(15, 23, 42, 0.10), inset 0 1px 0 rgba(255, 255, 255, 0.74)" - : "0 14px 34px -30px rgba(15, 23, 42, 0.18), inset 0 1px 0 rgba(255, 255, 255, 0.78)"}; + gap: ${({ $isUser, $bareMedia }) => + $bareMedia ? "0" : $isUser ? "8px" : "12px"}; + border-radius: ${({ $bareMedia }) => ($bareMedia ? "0" : "18px")}; + border: ${({ $isUser, $bareMedia }) => + $bareMedia + ? "0" + : $isUser + ? "1px solid var(--lime-surface-border-strong, rgba(187, 247, 208, 0.72))" + : "1px solid var(--lime-surface-border, rgba(226, 240, 226, 0.58))"}; + background: ${({ $isUser, $bareMedia }) => + $bareMedia + ? "transparent" + : $isUser + ? "linear-gradient(180deg, var(--lime-surface, #ffffff) 0%, var(--lime-brand-soft, #ecfdf5) 100%)" + : "linear-gradient(180deg, var(--lime-surface, #ffffff) 0%, var(--lime-surface-subtle, #fcfff9) 100%)"}; + box-shadow: ${({ $isUser, $bareMedia }) => + $bareMedia + ? "none" + : $isUser + ? "0 16px 36px -30px rgba(15, 23, 42, 0.10), inset 0 1px 0 rgba(255, 255, 255, 0.74)" + : "0 14px 34px -30px rgba(15, 23, 42, 0.18), inset 0 1px 0 rgba(255, 255, 255, 0.78)"}; color: ${({ $isUser }) => $isUser ? "rgb(30, 41, 59)" : "var(--foreground)"}; font-size: 15px; diff --git a/src/components/agent/chat/team-workspace-runtime/activityPreviewSelectors.test.ts b/src/components/agent/chat/team-workspace-runtime/activityPreviewSelectors.test.ts index 240330e75..a8a607924 100644 --- a/src/components/agent/chat/team-workspace-runtime/activityPreviewSelectors.test.ts +++ b/src/components/agent/chat/team-workspace-runtime/activityPreviewSelectors.test.ts @@ -1,11 +1,34 @@ import { describe, expect, it } from "vitest"; +import { changeLimeLocale, getLimeI18n } from "@/i18n/createI18n"; +import type { AsterSessionDetail } from "@/lib/api/agentRuntime"; import { + buildActivityPreviewCopy, buildPreviewableRailSessionsSyncKey, buildSelectedSessionActivityState, collectStaleSessionActivityTargets, + extractSessionActivitySnapshot, + type ActivityPreviewTranslate, type SessionActivityPreviewState, } from "./activityPreviewSelectors"; +async function buildEnglishActivityPreviewCopy() { + await changeLimeLocale("en-US"); + const translate: ActivityPreviewTranslate = (key, options) => + String( + getLimeI18n().t( + key as never, + { + ns: "agent", + ...(options ?? {}), + } as never, + ), + ); + + return buildActivityPreviewCopy({ + translate, + }); +} + describe("activityPreviewSelectors", () => { it("选中会话的 activity state 应优先合并实时进展并生成预览文案", () => { const state = buildSelectedSessionActivityState({ @@ -66,6 +89,40 @@ describe("activityPreviewSelectors", () => { expect(state.fingerprint).toBe("child-1:0:queued:queued:0"); }); + it("应支持注入英文 activity preview copy 且保留消息正文", async () => { + const copy = await buildEnglishActivityPreviewCopy(); + const detail = { + id: "child-1", + created_at: 0, + updated_at: 0, + items: [], + messages: [ + { + id: "message-1", + role: "assistant", + timestamp: 1, + content: [ + { + type: "tool_response", + id: "tool-1", + success: true, + output: "页面结构差异已提取完成。", + }, + ], + }, + ], + } satisfies AsterSessionDetail; + + const snapshot = extractSessionActivitySnapshot(detail, 3, { copy }); + + expect(snapshot.preview).toBe("Output: 页面结构差异已提取完成。"); + expect(snapshot.entries[0]).toMatchObject({ + title: "Output", + detail: "页面结构差异已提取完成。", + statusLabel: "Message", + }); + }); + it("preview sync key 应稳定编码 fingerprint 与 refreshVersion", () => { const syncKey = buildPreviewableRailSessionsSyncKey({ sessions: [ diff --git a/src/components/agent/chat/team-workspace-runtime/activityPreviewSelectors.ts b/src/components/agent/chat/team-workspace-runtime/activityPreviewSelectors.ts index f047f5f76..35e526332 100644 --- a/src/components/agent/chat/team-workspace-runtime/activityPreviewSelectors.ts +++ b/src/components/agent/chat/team-workspace-runtime/activityPreviewSelectors.ts @@ -1,5 +1,6 @@ import type { AgentThreadItem } from "@/lib/api/agentProtocol"; import type { AsterSessionDetail } from "@/lib/api/agentRuntime"; +import agentSourceResource from "@/i18n/resources/zh-CN/agent.json"; import { buildTeamWorkspaceActivityEntryFromThreadItem, buildTeamWorkspaceSessionFingerprint, @@ -8,6 +9,100 @@ import { type TeamWorkspaceRuntimeStatus, } from "../teamWorkspaceRuntime"; +type ActivityPreviewResourceKey = + | "agentChat.teamWorkspace.activityPreview.error.historyReadFailed" + | "agentChat.teamWorkspace.activityPreview.error.syncFailed" + | "agentChat.teamWorkspace.activityPreview.line" + | "agentChat.teamWorkspace.activityPreview.messageTitle.error" + | "agentChat.teamWorkspace.activityPreview.messageTitle.output" + | "agentChat.teamWorkspace.activityPreview.messageTitle.reply" + | "agentChat.teamWorkspace.activityPreview.status.completed" + | "agentChat.teamWorkspace.activityPreview.status.error" + | "agentChat.teamWorkspace.activityPreview.status.failed" + | "agentChat.teamWorkspace.activityPreview.status.inProgress" + | "agentChat.teamWorkspace.activityPreview.status.message" + | "agentChat.teamWorkspace.activityPreview.status.warning"; + +export type ActivityPreviewTranslate = ( + key: ActivityPreviewResourceKey, + options?: Record, +) => string; + +type AgentSourceResourceKey = keyof typeof agentSourceResource; + +function interpolateActivityPreviewSourceTemplate( + template: string, + values?: Record, +): string { + return template.replace(/\{\{\s*(\w+)\s*\}\}/g, (match, name) => { + const value = values?.[name]; + return value == null ? match : String(value); + }); +} + +function translateActivityPreviewSourceKey( + key: ActivityPreviewResourceKey, + values?: Record, +): string { + const template = agentSourceResource[key as AgentSourceResourceKey] ?? key; + return interpolateActivityPreviewSourceTemplate(template, values); +} + +type ActivityPreviewMessageTitleKind = "error" | "output" | "reply"; +type ActivityPreviewStatusKind = + | "completed" + | "error" + | "failed" + | "inProgress" + | "message" + | "warning"; + +export interface ActivityPreviewCopy { + formatPreviewLine: (title: string, detail: string) => string; + getMessageTitle: (kind: ActivityPreviewMessageTitleKind) => string; + getStatusLabel: (kind: ActivityPreviewStatusKind) => string; + historyReadFailed: string; + syncFailed: string; +} + +function resolveActivityPreviewStatusKey( + kind: ActivityPreviewStatusKind, +): ActivityPreviewResourceKey { + return `agentChat.teamWorkspace.activityPreview.status.${kind}` as ActivityPreviewResourceKey; +} + +function resolveActivityPreviewMessageTitleKey( + kind: ActivityPreviewMessageTitleKind, +): ActivityPreviewResourceKey { + return `agentChat.teamWorkspace.activityPreview.messageTitle.${kind}` as ActivityPreviewResourceKey; +} + +export function buildActivityPreviewCopy(params: { + translate: ActivityPreviewTranslate; +}): ActivityPreviewCopy { + return { + formatPreviewLine: (title, detail) => + params.translate("agentChat.teamWorkspace.activityPreview.line", { + detail, + title, + }), + getMessageTitle: (kind) => + params.translate(resolveActivityPreviewMessageTitleKey(kind)), + getStatusLabel: (kind) => + params.translate(resolveActivityPreviewStatusKey(kind)), + historyReadFailed: params.translate( + "agentChat.teamWorkspace.activityPreview.error.historyReadFailed", + ), + syncFailed: params.translate( + "agentChat.teamWorkspace.activityPreview.error.syncFailed", + ), + }; +} + +const SOURCE_ACTIVITY_PREVIEW_COPY = buildActivityPreviewCopy({ + translate: translateActivityPreviewSourceKey, +}); + export interface SessionActivityPreviewState { preview: string | null; entries: TeamWorkspaceActivityEntry[]; @@ -70,28 +165,30 @@ function normalizeActivityPreviewText( function buildActivityPreviewLine( label: string, value?: string | null, + copy: ActivityPreviewCopy = SOURCE_ACTIVITY_PREVIEW_COPY, ): string | null { const normalized = normalizeActivityPreviewText(value); if (!normalized) { return null; } - return `${label}:${normalized}`; + return copy.formatPreviewLine(label, normalized); } function resolveActivityEntryStatusMeta( item: AgentThreadItem | { type: "message_fallback" }, status?: AgentThreadItem["status"], + copy: ActivityPreviewCopy = SOURCE_ACTIVITY_PREVIEW_COPY, ) { if (item.type === "error") { return { - label: "错误", + label: copy.getStatusLabel("error"), badgeClassName: "border border-rose-200 bg-rose-50 text-rose-700", }; } if (item.type === "warning") { return { - label: "警告", + label: copy.getStatusLabel("warning"), badgeClassName: "border border-amber-200 bg-amber-50 text-amber-700", }; } @@ -99,23 +196,23 @@ function resolveActivityEntryStatusMeta( switch (status) { case "in_progress": return { - label: "进行中", + label: copy.getStatusLabel("inProgress"), badgeClassName: "border border-sky-200 bg-sky-50 text-sky-700", }; case "failed": return { - label: "失败", + label: copy.getStatusLabel("failed"), badgeClassName: "border border-rose-200 bg-rose-50 text-rose-700", }; case "completed": return { - label: "完成", + label: copy.getStatusLabel("completed"), badgeClassName: "border border-emerald-200 bg-emerald-50 text-emerald-700", }; default: return { - label: "消息", + label: copy.getStatusLabel("message"), badgeClassName: "border border-slate-200 bg-slate-50 text-slate-600", }; } @@ -123,16 +220,18 @@ function resolveActivityEntryStatusMeta( export function buildActivityPreviewFromEntry( entry?: TeamWorkspaceActivityEntry | null, + copy: ActivityPreviewCopy = SOURCE_ACTIVITY_PREVIEW_COPY, ) { if (!entry) { return null; } - return buildActivityPreviewLine(entry.title, entry.detail); + return buildActivityPreviewLine(entry.title, entry.detail, copy); } function extractMessageActivityEntries( detail: AsterSessionDetail, + copy: ActivityPreviewCopy = SOURCE_ACTIVITY_PREVIEW_COPY, ): TeamWorkspaceActivityEntry[] { const reversedMessages = [...detail.messages].sort( (left, right) => right.timestamp - left.timestamp, @@ -147,11 +246,11 @@ function extractMessageActivityEntries( const title = content.type === "tool_response" ? content.error - ? "错误" + ? copy.getMessageTitle("error") : content.output - ? "输出" - : "回复" - : "回复"; + ? copy.getMessageTitle("output") + : copy.getMessageTitle("reply") + : copy.getMessageTitle("reply"); const previewSource = content.type === "tool_response" ? content.error || content.output @@ -167,6 +266,7 @@ function extractMessageActivityEntries( const statusMeta = resolveActivityEntryStatusMeta( { type: "message_fallback" }, undefined, + copy, ); return [ { @@ -189,10 +289,14 @@ function extractMessageActivityEntries( export function extractSessionActivitySnapshot( detail: AsterSessionDetail, activityTimelineEntryLimit: number, + options?: { + copy?: ActivityPreviewCopy; + }, ): { preview: string | null; entries: TeamWorkspaceActivityEntry[]; } { + const copy = options?.copy ?? SOURCE_ACTIVITY_PREVIEW_COPY; const orderedItems = [...(detail.items ?? [])].sort( (left, right) => right.sequence - left.sequence, ); @@ -210,14 +314,14 @@ export function extractSessionActivitySnapshot( if (entries.length > 0) { return { - preview: buildActivityPreviewFromEntry(entries[0]), + preview: buildActivityPreviewFromEntry(entries[0], copy), entries, }; } - const messageEntries = extractMessageActivityEntries(detail); + const messageEntries = extractMessageActivityEntries(detail, copy); return { - preview: buildActivityPreviewFromEntry(messageEntries[0]), + preview: buildActivityPreviewFromEntry(messageEntries[0], copy), entries: messageEntries, }; } @@ -248,7 +352,9 @@ export function buildSelectedSessionActivityState(params: { previewBySessionId?: Record; activityRefreshVersionBySessionId?: Record; activityTimelineEntryLimit: number; + copy?: ActivityPreviewCopy; }): SelectedSessionActivityState { + const copy = params.copy ?? SOURCE_ACTIVITY_PREVIEW_COPY; const selectedSession = params.selectedSession ?? null; const previewState = selectedSession ? (params.previewBySessionId?.[selectedSession.id] ?? null) @@ -261,7 +367,9 @@ export function buildSelectedSessionActivityState(params: { ) : []; const previewText = - buildActivityPreviewFromEntry(entries[0]) ?? previewState?.preview ?? null; + buildActivityPreviewFromEntry(entries[0], copy) ?? + previewState?.preview ?? + null; const supportsPreview = Boolean( selectedSession && selectedSession.sessionType !== "user", ); diff --git a/src/components/agent/chat/team-workspace-runtime/boardChromeSelectors.test.ts b/src/components/agent/chat/team-workspace-runtime/boardChromeSelectors.test.ts index 0dc24700e..fd779f1b5 100644 --- a/src/components/agent/chat/team-workspace-runtime/boardChromeSelectors.test.ts +++ b/src/components/agent/chat/team-workspace-runtime/boardChromeSelectors.test.ts @@ -1,5 +1,29 @@ import { describe, expect, it } from "vitest"; -import { buildTeamWorkspaceBoardChromeDisplayState } from "./boardChromeSelectors"; +import { changeLimeLocale, getLimeI18n } from "@/i18n/createI18n"; +import { + buildTeamWorkspaceBoardChromeCopy, + buildTeamWorkspaceBoardChromeDisplayState, + type TeamWorkspaceBoardChromeTranslate, +} from "./boardChromeSelectors"; + +async function buildEnglishBoardChromeCopy() { + await changeLimeLocale("en-US"); + const translate: TeamWorkspaceBoardChromeTranslate = (key, options) => + String( + getLimeI18n().t( + key as never, + { + ns: "agent", + ...(options ?? {}), + } as never, + ), + ); + + return buildTeamWorkspaceBoardChromeCopy({ + locale: "en-US", + translate, + }); +} describe("boardChromeSelectors", () => { it("真实成员图应产出紧凑 chrome 标题与工具条 chips", () => { @@ -99,4 +123,48 @@ describe("boardChromeSelectors", () => { { key: "completed", text: "已完成 1", status: "completed" }, ]); }); + + it("应支持注入英文 board chrome copy 且保留会话名称", async () => { + const copy = await buildEnglishBoardChromeCopy(); + + const state = buildTeamWorkspaceBoardChromeDisplayState({ + copy, + hasRuntimeSessions: true, + isChildSession: false, + totalTeamSessions: 2, + siblingCount: 0, + selectedSession: { + name: "研究员", + runtimeStatus: "running", + updatedAt: 0, + isCurrent: true, + }, + zoom: 1, + canWaitAnyActiveTeamSession: true, + waitableCount: 2, + canCloseCompletedTeamSessions: true, + completedCount: 1, + statusSummary: { + running: 1, + queued: 1, + }, + }); + + expect(state.boardHeadline).toBe("Tasks running · 1 running / 1 queued"); + expect(state.boardHint).toBe( + "This view shows which splits are active, their status, and where the latest updates landed.", + ); + expect(state.compactToolbarChips).toEqual([ + { key: "focus", text: "Current focus 研究员", tone: "summary" }, + { key: "status", text: "Running", tone: "status", status: "running" }, + { key: "updated-at", text: "Updated just now", tone: "muted" }, + { key: "current", text: "Current task", tone: "muted" }, + { key: "waitable", text: "2 running", tone: "muted" }, + { key: "completed", text: "1 completed", tone: "muted" }, + ]); + expect(state.statusSummaryBadges).toEqual([ + { key: "running", text: "Running 1", status: "running" }, + { key: "queued", text: "Queued 1", status: "queued" }, + ]); + }); }); diff --git a/src/components/agent/chat/team-workspace-runtime/boardChromeSelectors.ts b/src/components/agent/chat/team-workspace-runtime/boardChromeSelectors.ts index a19ecf7e0..619078a38 100644 --- a/src/components/agent/chat/team-workspace-runtime/boardChromeSelectors.ts +++ b/src/components/agent/chat/team-workspace-runtime/boardChromeSelectors.ts @@ -1,9 +1,276 @@ -import { formatRelativeTime } from "@/lib/api/project"; -import type { TeamWorkspaceRuntimeStatus } from "../teamWorkspaceRuntime"; import { - TEAM_WORKSPACE_SURFACE_TITLE, - resolveTeamWorkspaceDisplayRuntimeStatusLabel, -} from "../utils/teamWorkspaceCopy"; + formatDate, + formatNumber, + formatRelativeTime as formatLocaleRelativeTime, +} from "@/i18n/format"; +import agentSourceResource from "@/i18n/resources/zh-CN/agent.json"; +import type { TeamWorkspaceRuntimeStatus } from "../teamWorkspaceRuntime"; + +type TeamWorkspaceBoardChromeResourceKey = + | "agentChat.teamWorkspace.boardChrome.chip.completed" + | "agentChat.teamWorkspace.boardChrome.chip.focus" + | "agentChat.teamWorkspace.boardChrome.chip.updatedAt" + | "agentChat.teamWorkspace.boardChrome.chip.waitable" + | "agentChat.teamWorkspace.boardChrome.headline.completed" + | "agentChat.teamWorkspace.boardChrome.headline.completedCount" + | "agentChat.teamWorkspace.boardChrome.headline.connectedCount" + | "agentChat.teamWorkspace.boardChrome.headline.empty" + | "agentChat.teamWorkspace.boardChrome.headline.parentOverviewFallback" + | "agentChat.teamWorkspace.boardChrome.headline.queued" + | "agentChat.teamWorkspace.boardChrome.headline.queuedCount" + | "agentChat.teamWorkspace.boardChrome.headline.retry" + | "agentChat.teamWorkspace.boardChrome.headline.retryCount" + | "agentChat.teamWorkspace.boardChrome.headline.running" + | "agentChat.teamWorkspace.boardChrome.headline.runningCount" + | "agentChat.teamWorkspace.boardChrome.headline.runningWithQueued" + | "agentChat.teamWorkspace.boardChrome.headline.surfaceTitle" + | "agentChat.teamWorkspace.boardChrome.hint.childSingle" + | "agentChat.teamWorkspace.boardChrome.hint.childWithSiblings" + | "agentChat.teamWorkspace.boardChrome.hint.empty" + | "agentChat.teamWorkspace.boardChrome.hint.runtime" + | "agentChat.teamWorkspace.boardChrome.statusSummary" + | "agentChat.teamWorkspace.canvasLane.updatedNow" + | "agentChat.teamWorkspace.overview.selectedSession.waitingTitle" + | "agentChat.teamWorkspace.runtimeStatus.aborted" + | "agentChat.teamWorkspace.runtimeStatus.closed" + | "agentChat.teamWorkspace.runtimeStatus.completed" + | "agentChat.teamWorkspace.runtimeStatus.failed" + | "agentChat.teamWorkspace.runtimeStatus.idle" + | "agentChat.teamWorkspace.runtimeStatus.queued" + | "agentChat.teamWorkspace.runtimeStatus.running" + | "agentChat.teamWorkspace.selectedSession.header.currentTaskBadge"; + +export type TeamWorkspaceBoardChromeTranslate = ( + key: TeamWorkspaceBoardChromeResourceKey, + options?: Record, +) => string; + +type AgentSourceResourceKey = keyof typeof agentSourceResource; + +function interpolateBoardChromeSourceTemplate( + template: string, + values?: Record, +): string { + return template.replace(/\{\{\s*(\w+)\s*\}\}/g, (match, name) => { + const value = values?.[name]; + return value == null ? match : String(value); + }); +} + +function translateBoardChromeSourceKey( + key: TeamWorkspaceBoardChromeResourceKey, + values?: Record, +): string { + const template = agentSourceResource[key as AgentSourceResourceKey] ?? key; + return interpolateBoardChromeSourceTemplate(template, values); +} + +function resolveRuntimeStatusResourceKey( + status?: TeamWorkspaceRuntimeStatus, +): TeamWorkspaceBoardChromeResourceKey { + return `agentChat.teamWorkspace.runtimeStatus.${status ?? "idle"}` as TeamWorkspaceBoardChromeResourceKey; +} + +function formatBoardChromeUpdatedAt(params: { + locale?: string | null; + updatedAt?: number; + updatedNow: string; +}): string { + if (!params.updatedAt) { + return params.updatedNow; + } + + const timestamp = params.updatedAt * 1000; + const diff = Date.now() - timestamp; + const minute = 60 * 1000; + const hour = 60 * minute; + const day = 24 * hour; + const week = 7 * day; + const month = 30 * day; + + if (diff < minute) { + return params.updatedNow; + } + if (diff < hour) { + return formatLocaleRelativeTime(-Math.floor(diff / minute), "minute", { + locale: params.locale, + numeric: "auto", + }); + } + if (diff < day) { + return formatLocaleRelativeTime(-Math.floor(diff / hour), "hour", { + locale: params.locale, + numeric: "auto", + }); + } + if (diff < week) { + return formatLocaleRelativeTime(-Math.floor(diff / day), "day", { + locale: params.locale, + numeric: "auto", + }); + } + if (diff < month) { + return formatLocaleRelativeTime(-Math.floor(diff / week), "week", { + locale: params.locale, + numeric: "auto", + }); + } + + return formatDate(timestamp, { locale: params.locale }); +} + +export interface TeamWorkspaceBoardChromeCopy { + completedHeadline: string; + currentTask: string; + emptyHeadline: string; + formatCompletedChipCount: (count: number) => string; + formatCompletedHeadline: (count: number) => string; + formatConnectedHeadline: (count: number) => string; + formatFocusChip: (name: string) => string; + formatQueuedHeadline: (count: number) => string; + formatRetryHeadline: (count: number) => string; + formatRunningHeadline: (count: number) => string; + formatRunningWithQueuedHeadline: ( + runningCount: number, + queuedCount: number, + ) => string; + formatSiblingHint: (count: number) => string; + formatStatusSummary: (statusLabel: string, count: number) => string; + formatUpdatedAtChip: (updatedAt?: number) => string; + formatWaitableChipCount: (count: number) => string; + getRuntimeStatusLabel: (status?: TeamWorkspaceRuntimeStatus) => string; + parentOverviewFallback: string; + queuedHeadline: string; + retryHeadline: string; + runningHeadline: string; + surfaceTitle: string; + childSingleHint: string; + emptyHint: string; + runtimeHint: string; + waitingFocus: string; +} + +export function buildTeamWorkspaceBoardChromeCopy(params: { + locale?: string | null; + translate: TeamWorkspaceBoardChromeTranslate; +}): TeamWorkspaceBoardChromeCopy { + const formatCount = (count: number) => + formatNumber(count, { locale: params.locale }); + + return { + completedHeadline: params.translate( + "agentChat.teamWorkspace.boardChrome.headline.completed", + ), + currentTask: params.translate( + "agentChat.teamWorkspace.selectedSession.header.currentTaskBadge", + ), + emptyHeadline: params.translate( + "agentChat.teamWorkspace.boardChrome.headline.empty", + ), + formatCompletedChipCount: (count) => + params.translate("agentChat.teamWorkspace.boardChrome.chip.completed", { + formattedCount: formatCount(count), + }), + formatCompletedHeadline: (count) => + params.translate( + "agentChat.teamWorkspace.boardChrome.headline.completedCount", + { formattedCount: formatCount(count) }, + ), + formatConnectedHeadline: (count) => + params.translate( + "agentChat.teamWorkspace.boardChrome.headline.connectedCount", + { formattedCount: formatCount(count) }, + ), + formatFocusChip: (name) => + params.translate("agentChat.teamWorkspace.boardChrome.chip.focus", { + name, + }), + formatQueuedHeadline: (count) => + params.translate( + "agentChat.teamWorkspace.boardChrome.headline.queuedCount", + { formattedCount: formatCount(count) }, + ), + formatRetryHeadline: (count) => + params.translate( + "agentChat.teamWorkspace.boardChrome.headline.retryCount", + { formattedCount: formatCount(count) }, + ), + formatRunningHeadline: (count) => + params.translate( + "agentChat.teamWorkspace.boardChrome.headline.runningCount", + { formattedCount: formatCount(count) }, + ), + formatRunningWithQueuedHeadline: (runningCount, queuedCount) => + params.translate( + "agentChat.teamWorkspace.boardChrome.headline.runningWithQueued", + { + queuedCount: formatCount(queuedCount), + runningCount: formatCount(runningCount), + }, + ), + formatSiblingHint: (count) => + params.translate( + "agentChat.teamWorkspace.boardChrome.hint.childWithSiblings", + { + formattedCount: formatCount(count), + }, + ), + formatStatusSummary: (statusLabel, count) => + params.translate("agentChat.teamWorkspace.boardChrome.statusSummary", { + formattedCount: formatCount(count), + status: statusLabel, + }), + formatUpdatedAtChip: (updatedAt) => + params.translate("agentChat.teamWorkspace.boardChrome.chip.updatedAt", { + updatedAt: formatBoardChromeUpdatedAt({ + locale: params.locale, + updatedAt, + updatedNow: params.translate( + "agentChat.teamWorkspace.canvasLane.updatedNow", + ), + }), + }), + formatWaitableChipCount: (count) => + params.translate("agentChat.teamWorkspace.boardChrome.chip.waitable", { + formattedCount: formatCount(count), + }), + getRuntimeStatusLabel: (status) => + params.translate(resolveRuntimeStatusResourceKey(status)), + parentOverviewFallback: params.translate( + "agentChat.teamWorkspace.boardChrome.headline.parentOverviewFallback", + ), + queuedHeadline: params.translate( + "agentChat.teamWorkspace.boardChrome.headline.queued", + ), + retryHeadline: params.translate( + "agentChat.teamWorkspace.boardChrome.headline.retry", + ), + runningHeadline: params.translate( + "agentChat.teamWorkspace.boardChrome.headline.running", + ), + surfaceTitle: params.translate( + "agentChat.teamWorkspace.boardChrome.headline.surfaceTitle", + ), + childSingleHint: params.translate( + "agentChat.teamWorkspace.boardChrome.hint.childSingle", + ), + emptyHint: params.translate( + "agentChat.teamWorkspace.boardChrome.hint.empty", + ), + runtimeHint: params.translate( + "agentChat.teamWorkspace.boardChrome.hint.runtime", + ), + waitingFocus: params.translate( + "agentChat.teamWorkspace.overview.selectedSession.waitingTitle", + ), + }; +} + +const SOURCE_TEAM_WORKSPACE_BOARD_CHROME_COPY = + buildTeamWorkspaceBoardChromeCopy({ + locale: "zh-CN", + translate: translateBoardChromeSourceKey, + }); export interface TeamWorkspaceBoardChromeSession { name: string; @@ -33,14 +300,8 @@ export interface TeamWorkspaceBoardChromeDisplayState { statusSummaryBadges: TeamWorkspaceBoardStatusSummaryBadge[]; } -function formatUpdatedAt(updatedAt?: number) { - if (!updatedAt) { - return "刚刚"; - } - return formatRelativeTime(updatedAt * 1000); -} - function buildBoardHeadline(params: { + copy: TeamWorkspaceBoardChromeCopy; hasRuntimeSessions: boolean; isChildSession: boolean; parentSessionName?: string | null; @@ -48,6 +309,7 @@ function buildBoardHeadline(params: { totalTeamSessions: number; }) { const { + copy, hasRuntimeSessions, isChildSession, parentSessionName, @@ -60,53 +322,56 @@ function buildBoardHeadline(params: { const retryCount = (statusSummary.failed ?? 0) + (statusSummary.aborted ?? 0); if (isChildSession) { - return parentSessionName?.trim() || "主助手总览"; + return parentSessionName?.trim() || copy.parentOverviewFallback; } if (!hasRuntimeSessions) { - return "需要时会自动拆出任务"; + return copy.emptyHeadline; } if (runningCount > 0) { if (queuedCount > 0) { - return `任务进行中 · ${runningCount} 项处理中 / ${queuedCount} 项稍后开始`; + return copy.formatRunningWithQueuedHeadline(runningCount, queuedCount); } return totalTeamSessions > 1 - ? `任务进行中 · ${runningCount} 项处理中` - : "任务进行中"; + ? copy.formatRunningHeadline(runningCount) + : copy.runningHeadline; } if (queuedCount > 0) { return totalTeamSessions > 1 - ? `任务准备中 · ${queuedCount} 项稍后开始` - : "任务准备中"; + ? copy.formatQueuedHeadline(queuedCount) + : copy.queuedHeadline; } if (completedCount > 0 && completedCount === totalTeamSessions) { return totalTeamSessions > 1 - ? `${completedCount} 项任务已完成` - : "任务已完成"; + ? copy.formatCompletedHeadline(completedCount) + : copy.completedHeadline; } if (retryCount > 0 && retryCount === totalTeamSessions) { - return totalTeamSessions > 1 ? `${retryCount} 项任务需重试` : "任务需重试"; + return totalTeamSessions > 1 + ? copy.formatRetryHeadline(retryCount) + : copy.retryHeadline; } return totalTeamSessions > 0 - ? `${totalTeamSessions} 条当前进展已接入` - : TEAM_WORKSPACE_SURFACE_TITLE; + ? copy.formatConnectedHeadline(totalTeamSessions) + : copy.surfaceTitle; } function buildBoardHint(params: { + copy: TeamWorkspaceBoardChromeCopy; hasRuntimeSessions: boolean; isChildSession: boolean; siblingCount: number; }) { - const { hasRuntimeSessions, isChildSession, siblingCount } = params; + const { copy, hasRuntimeSessions, isChildSession, siblingCount } = params; if (isChildSession) { return siblingCount > 0 - ? `当前任务正与 ${siblingCount} 项并行子任务一起推进` - : "当前正在处理这项子任务,结果会回流到主助手。"; + ? copy.formatSiblingHint(siblingCount) + : copy.childSingleHint; } if (!hasRuntimeSessions) { - return "系统会在需要时自动拆出任务、安排处理顺序,并把结果回流到当前任务。"; + return copy.emptyHint; } - return "这里只展示当前有哪些分工在处理、状态如何,以及最近更新到了哪里。"; + return copy.runtimeHint; } export function buildTeamWorkspaceBoardChromeDisplayState(params: { @@ -124,11 +389,14 @@ export function buildTeamWorkspaceBoardChromeDisplayState(params: { canCloseCompletedTeamSessions: boolean; completedCount: number; statusSummary: Record; + copy?: TeamWorkspaceBoardChromeCopy; }): TeamWorkspaceBoardChromeDisplayState { + const copy = params.copy ?? SOURCE_TEAM_WORKSPACE_BOARD_CHROME_COPY; const boardHeadline = !params.hasRuntimeSessions && params.runtimeFormationTitle ? params.runtimeFormationTitle : buildBoardHeadline({ + copy, hasRuntimeSessions: params.hasRuntimeSessions, isChildSession: params.isChildSession, parentSessionName: params.parentSessionName, @@ -139,6 +407,7 @@ export function buildTeamWorkspaceBoardChromeDisplayState(params: { !params.hasRuntimeSessions && params.runtimeFormationHint ? params.runtimeFormationHint : buildBoardHint({ + copy, hasRuntimeSessions: params.hasRuntimeSessions, isChildSession: params.isChildSession, siblingCount: params.siblingCount, @@ -147,15 +416,15 @@ export function buildTeamWorkspaceBoardChromeDisplayState(params: { { key: "focus", text: params.selectedSession - ? `当前焦点 ${params.selectedSession.name}` - : "等待任务接手", + ? copy.formatFocusChip(params.selectedSession.name) + : copy.waitingFocus, tone: "summary", }, ...(params.selectedSession?.runtimeStatus ? [ { key: "status", - text: resolveTeamWorkspaceDisplayRuntimeStatusLabel( + text: copy.getRuntimeStatusLabel( params.selectedSession.runtimeStatus, ), tone: "status" as const, @@ -167,7 +436,7 @@ export function buildTeamWorkspaceBoardChromeDisplayState(params: { ? [ { key: "updated-at", - text: `更新于 ${formatUpdatedAt(params.selectedSession.updatedAt)}`, + text: copy.formatUpdatedAtChip(params.selectedSession.updatedAt), tone: "muted" as const, }, ] @@ -176,7 +445,7 @@ export function buildTeamWorkspaceBoardChromeDisplayState(params: { ? [ { key: "current", - text: "当前任务", + text: copy.currentTask, tone: "muted" as const, }, ] @@ -185,7 +454,7 @@ export function buildTeamWorkspaceBoardChromeDisplayState(params: { ? [ { key: "waitable", - text: `${params.waitableCount} 项处理中`, + text: copy.formatWaitableChipCount(params.waitableCount), tone: "muted" as const, }, ] @@ -194,7 +463,7 @@ export function buildTeamWorkspaceBoardChromeDisplayState(params: { ? [ { key: "completed", - text: `${params.completedCount} 项已完成`, + text: copy.formatCompletedChipCount(params.completedCount), tone: "muted" as const, }, ] @@ -216,7 +485,10 @@ export function buildTeamWorkspaceBoardChromeDisplayState(params: { return { key: status, - text: `${resolveTeamWorkspaceDisplayRuntimeStatusLabel(normalizedStatus)} ${count}`, + text: copy.formatStatusSummary( + copy.getRuntimeStatusLabel(normalizedStatus), + count, + ), status: normalizedStatus, }; }), diff --git a/src/components/agent/chat/team-workspace-runtime/canvasLaneSelectors.test.ts b/src/components/agent/chat/team-workspace-runtime/canvasLaneSelectors.test.ts index 2be7caa17..a441cbd19 100644 --- a/src/components/agent/chat/team-workspace-runtime/canvasLaneSelectors.test.ts +++ b/src/components/agent/chat/team-workspace-runtime/canvasLaneSelectors.test.ts @@ -1,5 +1,35 @@ import { describe, expect, it } from "vitest"; -import { buildTeamWorkspaceCanvasLanes } from "./canvasLaneSelectors"; +import enAgentResource from "@/i18n/resources/en-US/agent.json"; +import { + buildTeamWorkspaceCanvasLaneCopy, + buildTeamWorkspaceCanvasLanes, + type TeamWorkspaceCanvasLaneTranslate, +} from "./canvasLaneSelectors"; + +type EnAgentResourceKey = keyof typeof enAgentResource; + +function interpolateResourceTemplate( + template: string, + values?: Record, +) { + return template.replace(/\{\{\s*(\w+)\s*\}\}/g, (match, name) => { + const value = values?.[name]; + return value == null ? match : String(value); + }); +} + +function buildEnglishCanvasLaneCopy() { + const translate: TeamWorkspaceCanvasLaneTranslate = (key, options) => + interpolateResourceTemplate( + enAgentResource[key as EnAgentResourceKey], + options, + ); + + return buildTeamWorkspaceCanvasLaneCopy({ + locale: "en-US", + translate, + }); +} describe("canvasLaneSelectors", () => { it("真实成员图应把 session lane 投影为稳定的展示模型", () => { @@ -185,4 +215,75 @@ describe("canvasLaneSelectors", () => { }), ]); }); + + it("应支持注入英文 canvas lane chrome copy 且不翻译运行时数据", () => { + const copy = buildEnglishCanvasLaneCopy(); + + const sessionLanes = buildTeamWorkspaceCanvasLanes({ + copy, + hasRealTeamGraph: true, + sessions: [ + { + id: "child-en", + name: "Runtime worker", + runtimeStatus: "running", + latestTurnStatus: "running", + queuedTurnCount: 2, + teamParallelBudget: 3, + teamActiveCount: 1, + providerConcurrencyGroup: "openai", + providerParallelBudget: 1, + }, + ], + runtimeMembers: [], + plannedRoles: [], + previewBySessionId: { + "child-en": { + preview: null, + entries: [], + status: "loading", + }, + }, + activityTimelineEntryLimit: 4, + }); + + expect(sessionLanes).toEqual([ + expect.objectContaining({ + id: "child-en", + title: "Runtime worker", + summary: "No task summary yet. Open details to view the full context.", + badgeLabel: "Running", + statusHint: + "Waiting 2 · Latest progress Running · Running 1/3 · Steady mode", + updatedAtLabel: "just now", + previewText: "Syncing the latest content for this task...", + }), + ]); + + const plannedLanes = buildTeamWorkspaceCanvasLanes({ + copy, + hasRealTeamGraph: false, + sessions: [], + runtimeMembers: [], + plannedRoles: [ + { + id: "reviewer", + label: "Reviewer", + summary: "Keep this runtime summary as data.", + roleKey: "reviewer", + }, + ], + activityTimelineEntryLimit: 4, + }); + + expect(plannedLanes).toEqual([ + expect.objectContaining({ + title: "Reviewer", + summary: "Keep this runtime summary as data.", + badgeLabel: "Not started", + statusHint: "Waiting for the system to split out this task", + updatedAtLabel: "Planned assignment", + }), + ]); + }); }); diff --git a/src/components/agent/chat/team-workspace-runtime/canvasLaneSelectors.ts b/src/components/agent/chat/team-workspace-runtime/canvasLaneSelectors.ts index b7bbb4236..92795a1d2 100644 --- a/src/components/agent/chat/team-workspace-runtime/canvasLaneSelectors.ts +++ b/src/components/agent/chat/team-workspace-runtime/canvasLaneSelectors.ts @@ -1,19 +1,23 @@ import type { AsterSubagentSkillInfo } from "@/lib/api/agentRuntime"; -import { formatRelativeTime } from "@/lib/api/project"; +import { + formatDate, + formatNumber, + formatRelativeTime as formatRelativeTimeUnit, +} from "@/i18n/format"; +import agentSourceResource from "@/i18n/resources/zh-CN/agent.json"; import type { TeamRoleDefinition } from "../utils/teamDefinitions"; import { normalizeTeamWorkspaceDisplayValue } from "../utils/teamWorkspaceDisplay"; import { getTeamPresetOption } from "../utils/teamPresets"; import { buildTeamWorkspaceSkillDisplayName, - resolveTeamWorkspaceDisplayRuntimeStatusLabel, resolveTeamWorkspaceRoleHintLabel, - resolveTeamWorkspaceStableProcessingLabel, } from "../utils/teamWorkspaceCopy"; import { mergeSessionActivityEntries, resolveRuntimeMemberStatusMeta, type TeamWorkspaceActivityEntry, type TeamWorkspaceRuntimeMember, + type TeamWorkspaceRuntimeMemberStatus, type TeamWorkspaceRuntimeStatus, } from "../teamWorkspaceRuntime"; import { @@ -23,46 +27,316 @@ import { const SESSION_LANE_PREVIEW_ENTRY_LIMIT = 3; -const STATUS_META: Record< - NonNullable | "idle", +type TeamWorkspaceCanvasLaneRuntimeStatus = + | NonNullable + | "idle"; + +type TeamWorkspaceCanvasLaneResourceKey = + | "agentChat.teamWorkspace.canvasLane.empty.completed" + | "agentChat.teamWorkspace.canvasLane.empty.default" + | "agentChat.teamWorkspace.canvasLane.empty.failed" + | "agentChat.teamWorkspace.canvasLane.empty.queued" + | "agentChat.teamWorkspace.canvasLane.empty.running" + | "agentChat.teamWorkspace.canvasLane.empty.syncFailed" + | "agentChat.teamWorkspace.canvasLane.empty.syncLoading" + | "agentChat.teamWorkspace.canvasLane.memberHint.completed" + | "agentChat.teamWorkspace.canvasLane.memberHint.connected" + | "agentChat.teamWorkspace.canvasLane.memberHint.failed" + | "agentChat.teamWorkspace.canvasLane.memberHint.running" + | "agentChat.teamWorkspace.canvasLane.memberHint.spawning" + | "agentChat.teamWorkspace.canvasLane.memberHint.waiting" + | "agentChat.teamWorkspace.canvasLane.memberHint.waitingAssignment" + | "agentChat.teamWorkspace.canvasLane.planned.badge" + | "agentChat.teamWorkspace.canvasLane.planned.statusHint" + | "agentChat.teamWorkspace.canvasLane.planned.updatedAt" + | "agentChat.teamWorkspace.canvasLane.runtime.updatedAtWaiting" + | "agentChat.teamWorkspace.canvasLane.runtimeDetail.processingCount" + | "agentChat.teamWorkspace.canvasLane.runtimeDetail.recentProgress" + | "agentChat.teamWorkspace.canvasLane.runtimeDetail.stableProcessing" + | "agentChat.teamWorkspace.canvasLane.runtimeDetail.waitingCount" + | "agentChat.teamWorkspace.canvasLane.session.summaryEmpty" + | "agentChat.teamWorkspace.canvasLane.updatedNow" + | "agentChat.teamWorkspace.formation.memberStatus.completed" + | "agentChat.teamWorkspace.formation.memberStatus.failed" + | "agentChat.teamWorkspace.formation.memberStatus.planned" + | "agentChat.teamWorkspace.formation.memberStatus.running" + | "agentChat.teamWorkspace.formation.memberStatus.spawning" + | "agentChat.teamWorkspace.formation.memberStatus.waiting" + | "agentChat.teamWorkspace.runtimeStatus.aborted" + | "agentChat.teamWorkspace.runtimeStatus.closed" + | "agentChat.teamWorkspace.runtimeStatus.completed" + | "agentChat.teamWorkspace.runtimeStatus.failed" + | "agentChat.teamWorkspace.runtimeStatus.idle" + | "agentChat.teamWorkspace.runtimeStatus.queued" + | "agentChat.teamWorkspace.runtimeStatus.running"; + +export type TeamWorkspaceCanvasLaneTranslate = ( + key: TeamWorkspaceCanvasLaneResourceKey, + options?: Record, +) => string; + +export interface TeamWorkspaceRuntimeDetailCopy { + formatProcessingCount: (activeCount: number, totalCount: number) => string; + formatRecentProgress: (status: string) => string; + formatWaitingCount: (count: number) => string; + getRuntimeStatusLabel: ( + status?: TeamWorkspaceRuntimeStatus | "idle", + ) => string; + stableProcessingLabel: string; +} + +export interface TeamWorkspaceCanvasLaneCopy extends TeamWorkspaceRuntimeDetailCopy { + emptyCompleted: string; + emptyDefault: string; + emptyFailed: string; + emptyQueued: string; + emptyRunning: string; + emptySyncFailed: string; + emptySyncLoading: string; + formatUpdatedAt: (updatedAt?: number) => string; + getMemberStatusHint: ( + status: TeamWorkspaceRuntimeMemberStatus, + hasSession: boolean, + ) => string; + getMemberStatusLabel: (status: TeamWorkspaceRuntimeMemberStatus) => string; + plannedBadge: string; + plannedStatusHint: string; + plannedUpdatedAt: string; + runtimeUpdatedAtWaiting: string; + sessionSummaryEmpty: string; +} + +type AgentSourceResourceKey = keyof typeof agentSourceResource; + +function interpolateCanvasLaneSourceTemplate( + template: string, + values?: Record, +): string { + return template.replace(/\{\{\s*(\w+)\s*\}\}/g, (match, name) => { + const value = values?.[name]; + return value == null ? match : String(value); + }); +} + +function translateCanvasLaneSourceKey( + key: TeamWorkspaceCanvasLaneResourceKey, + values?: Record, +): string { + const template = agentSourceResource[key as AgentSourceResourceKey] ?? key; + return interpolateCanvasLaneSourceTemplate(template, values); +} + +function formatRelativeUpdatedAt(params: { + locale?: string | null; + updatedAt?: number; + updatedNow: string; +}): string { + if (!params.updatedAt) { + return params.updatedNow; + } + + const timestamp = params.updatedAt * 1000; + const diff = Date.now() - timestamp; + const minute = 60 * 1000; + const hour = 60 * minute; + const day = 24 * hour; + const week = 7 * day; + const month = 30 * day; + + if (diff < minute) { + return params.updatedNow; + } + if (diff < hour) { + return formatRelativeTimeUnit(-Math.floor(diff / minute), "minute", { + locale: params.locale, + numeric: "auto", + }); + } + if (diff < day) { + return formatRelativeTimeUnit(-Math.floor(diff / hour), "hour", { + locale: params.locale, + numeric: "auto", + }); + } + if (diff < week) { + return formatRelativeTimeUnit(-Math.floor(diff / day), "day", { + locale: params.locale, + numeric: "auto", + }); + } + if (diff < month) { + return formatRelativeTimeUnit(-Math.floor(diff / week), "week", { + locale: params.locale, + numeric: "auto", + }); + } + + return formatDate(timestamp, { locale: params.locale }); +} + +function resolveRuntimeStatusKey( + status?: TeamWorkspaceRuntimeStatus | "idle", +): TeamWorkspaceCanvasLaneResourceKey { + return `agentChat.teamWorkspace.runtimeStatus.${status ?? "idle"}` as TeamWorkspaceCanvasLaneResourceKey; +} + +function resolveMemberStatusKey( + status: TeamWorkspaceRuntimeMemberStatus, +): TeamWorkspaceCanvasLaneResourceKey { + return `agentChat.teamWorkspace.formation.memberStatus.${status}` as TeamWorkspaceCanvasLaneResourceKey; +} + +export function buildTeamWorkspaceCanvasLaneCopy(params: { + locale?: string | null; + translate: TeamWorkspaceCanvasLaneTranslate; +}): TeamWorkspaceCanvasLaneCopy { + const formatCount = (count: number) => + formatNumber(count, { locale: params.locale }); + + return { + emptyCompleted: params.translate( + "agentChat.teamWorkspace.canvasLane.empty.completed", + ), + emptyDefault: params.translate( + "agentChat.teamWorkspace.canvasLane.empty.default", + ), + emptyFailed: params.translate( + "agentChat.teamWorkspace.canvasLane.empty.failed", + ), + emptyQueued: params.translate( + "agentChat.teamWorkspace.canvasLane.empty.queued", + ), + emptyRunning: params.translate( + "agentChat.teamWorkspace.canvasLane.empty.running", + ), + emptySyncFailed: params.translate( + "agentChat.teamWorkspace.canvasLane.empty.syncFailed", + ), + emptySyncLoading: params.translate( + "agentChat.teamWorkspace.canvasLane.empty.syncLoading", + ), + formatProcessingCount: (activeCount, totalCount) => + params.translate( + "agentChat.teamWorkspace.canvasLane.runtimeDetail.processingCount", + { + activeCount: formatCount(activeCount), + totalCount: formatCount(totalCount), + }, + ), + formatRecentProgress: (status) => + params.translate( + "agentChat.teamWorkspace.canvasLane.runtimeDetail.recentProgress", + { status }, + ), + formatUpdatedAt: (updatedAt) => + formatRelativeUpdatedAt({ + locale: params.locale, + updatedAt, + updatedNow: params.translate( + "agentChat.teamWorkspace.canvasLane.updatedNow", + ), + }), + formatWaitingCount: (count) => + params.translate( + "agentChat.teamWorkspace.canvasLane.runtimeDetail.waitingCount", + { formattedCount: formatCount(count) }, + ), + getMemberStatusHint: (status, hasSession) => { + if (status === "spawning") { + return params.translate( + "agentChat.teamWorkspace.canvasLane.memberHint.spawning", + ); + } + if (status === "running") { + return params.translate( + "agentChat.teamWorkspace.canvasLane.memberHint.running", + ); + } + if (status === "waiting") { + return params.translate( + "agentChat.teamWorkspace.canvasLane.memberHint.waiting", + ); + } + if (status === "completed") { + return params.translate( + "agentChat.teamWorkspace.canvasLane.memberHint.completed", + ); + } + if (status === "failed") { + return params.translate( + "agentChat.teamWorkspace.canvasLane.memberHint.failed", + ); + } + return params.translate( + hasSession + ? "agentChat.teamWorkspace.canvasLane.memberHint.connected" + : "agentChat.teamWorkspace.canvasLane.memberHint.waitingAssignment", + ); + }, + getMemberStatusLabel: (status) => + params.translate(resolveMemberStatusKey(status)), + getRuntimeStatusLabel: (status) => + params.translate(resolveRuntimeStatusKey(status)), + plannedBadge: params.translate( + "agentChat.teamWorkspace.canvasLane.planned.badge", + ), + plannedStatusHint: params.translate( + "agentChat.teamWorkspace.canvasLane.planned.statusHint", + ), + plannedUpdatedAt: params.translate( + "agentChat.teamWorkspace.canvasLane.planned.updatedAt", + ), + runtimeUpdatedAtWaiting: params.translate( + "agentChat.teamWorkspace.canvasLane.runtime.updatedAtWaiting", + ), + sessionSummaryEmpty: params.translate( + "agentChat.teamWorkspace.canvasLane.session.summaryEmpty", + ), + stableProcessingLabel: params.translate( + "agentChat.teamWorkspace.canvasLane.runtimeDetail.stableProcessing", + ), + }; +} + +const SOURCE_TEAM_WORKSPACE_CANVAS_LANE_COPY = buildTeamWorkspaceCanvasLaneCopy( + { + locale: "zh-CN", + translate: translateCanvasLaneSourceKey, + }, +); + +const STATUS_STYLE_META: Record< + TeamWorkspaceCanvasLaneRuntimeStatus, { - label: string; badgeClassName: string; dotClassName: string; } > = { idle: { - label: resolveTeamWorkspaceDisplayRuntimeStatusLabel(undefined), badgeClassName: "border border-slate-200 bg-white text-slate-600", dotClassName: "bg-slate-300", }, queued: { - label: resolveTeamWorkspaceDisplayRuntimeStatusLabel("queued"), badgeClassName: "border border-amber-200 bg-amber-50 text-amber-700", dotClassName: "bg-amber-400", }, running: { - label: resolveTeamWorkspaceDisplayRuntimeStatusLabel("running"), badgeClassName: "border border-sky-200 bg-sky-50 text-sky-700", dotClassName: "bg-sky-500", }, completed: { - label: resolveTeamWorkspaceDisplayRuntimeStatusLabel("completed"), badgeClassName: "border border-emerald-200 bg-emerald-50 text-emerald-700", dotClassName: "bg-emerald-500", }, failed: { - label: resolveTeamWorkspaceDisplayRuntimeStatusLabel("failed"), badgeClassName: "border border-rose-200 bg-rose-50 text-rose-700", dotClassName: "bg-rose-500", }, aborted: { - label: resolveTeamWorkspaceDisplayRuntimeStatusLabel("aborted"), badgeClassName: "border border-rose-200 bg-rose-50 text-rose-700", dotClassName: "bg-rose-500", }, closed: { - label: resolveTeamWorkspaceDisplayRuntimeStatusLabel("closed"), badgeClassName: "border border-slate-200 bg-slate-100 text-slate-600", dotClassName: "bg-slate-400", }, @@ -128,27 +402,31 @@ export interface TeamWorkspaceCanvasLane { previewEntries?: TeamWorkspaceActivityEntry[]; } -function resolveStatusMeta(status?: TeamWorkspaceRuntimeStatus) { - return STATUS_META[status ?? "idle"]; +function resolveStatusMeta( + status?: TeamWorkspaceRuntimeStatus, + copy: Pick< + TeamWorkspaceRuntimeDetailCopy, + "getRuntimeStatusLabel" + > = SOURCE_TEAM_WORKSPACE_CANVAS_LANE_COPY, +) { + const normalizedStatus = status ?? "idle"; + return { + ...STATUS_STYLE_META[normalizedStatus], + label: copy.getRuntimeStatusLabel(normalizedStatus), + }; } function normalizeComparableText(value?: string | null): string { return value?.trim().toLocaleLowerCase() || ""; } -function formatUpdatedAt(updatedAt?: number) { - if (!updatedAt) { - return "刚刚"; - } - return formatRelativeTime(updatedAt * 1000); -} - function buildSkillDisplayName(skill: AsterSubagentSkillInfo): string { return buildTeamWorkspaceSkillDisplayName(skill); } export function buildRuntimeDetailSummary( session?: TeamWorkspaceRuntimeDetailSession | null, + copy: TeamWorkspaceRuntimeDetailCopy = SOURCE_TEAM_WORKSPACE_CANVAS_LANE_COPY, ): string | null { if (!session) { return null; @@ -157,89 +435,84 @@ export function buildRuntimeDetailSummary( const parts: string[] = []; const waitingCount = session.teamQueuedCount ?? session.queuedTurnCount ?? 0; if (waitingCount > 0) { - parts.push(`等待中 ${waitingCount}`); + parts.push(copy.formatWaitingCount(waitingCount)); } if (session.latestTurnStatus) { - parts.push(`最近进展 ${resolveStatusMeta(session.latestTurnStatus).label}`); + parts.push( + copy.formatRecentProgress( + resolveStatusMeta(session.latestTurnStatus, copy).label, + ), + ); } if ( session.teamActiveCount !== undefined && session.teamParallelBudget !== undefined ) { parts.push( - `处理中 ${session.teamActiveCount}/${session.teamParallelBudget}`, + copy.formatProcessingCount( + session.teamActiveCount, + session.teamParallelBudget, + ), ); } if ( session.providerParallelBudget === 1 && session.providerConcurrencyGroup ) { - parts.push(resolveTeamWorkspaceStableProcessingLabel()); + parts.push(copy.stableProcessingLabel); } return parts.length > 0 ? parts.join(" · ") : null; } function buildSessionLaneEmptyState(params: { + copy: TeamWorkspaceCanvasLaneCopy; session?: TeamWorkspaceCanvasLaneSession | null; previewState?: SessionActivityPreviewState | null; }) { - const { session, previewState } = params; + const { copy, session, previewState } = params; if (previewState?.status === "error") { return ( normalizeTeamWorkspaceDisplayValue(previewState.errorMessage) || - "同步最新内容失败" + copy.emptySyncFailed ); } if (previewState?.status === "loading") { - return "正在同步这项任务的最新内容..."; + return copy.emptySyncLoading; } if (session?.runtimeStatus === "queued") { - return "这项任务已经收到安排,马上开始处理。"; + return copy.emptyQueued; } if (session?.runtimeStatus === "running") { - return "这项任务正在处理,最新进展会持续刷新到这里。"; + return copy.emptyRunning; } if (session?.runtimeStatus === "completed") { - return "这部分已经完成,结果会继续汇入当前内容。"; + return copy.emptyCompleted; } if ( session?.runtimeStatus === "failed" || session?.runtimeStatus === "aborted" ) { - return "这一步没有顺利完成,你可以在下方查看细节并决定是否继续。"; + return copy.emptyFailed; } - return "这项任务暂时还没有产出可展示的内容。"; + return copy.emptyDefault; } function buildCanvasLaneTitleSummary( member: Pick, + copy: TeamWorkspaceCanvasLaneCopy, ) { const memberMeta = resolveRuntimeMemberStatusMeta(member.status); - const statusHint = - member.status === "spawning" - ? "正在接入这项任务" - : member.status === "running" - ? "这项任务正在处理" - : member.status === "waiting" - ? "等待继续补充说明" - : member.status === "completed" - ? "这一步已经完成" - : member.status === "failed" - ? "这一步需要重试" - : member.sessionId - ? "已连接到真实任务" - : "等待任务接手"; return { - badgeLabel: memberMeta.label, + badgeLabel: copy.getMemberStatusLabel(member.status), badgeClassName: memberMeta.badgeClassName, dotClassName: member.status === "failed" @@ -251,17 +524,23 @@ function buildCanvasLaneTitleSummary( : "bg-sky-500", summary: normalizeTeamWorkspaceDisplayValue(member.summary) || member.summary, - statusHint, + statusHint: copy.getMemberStatusHint( + member.status, + Boolean(member.sessionId), + ), }; } -function buildPlannedRoleLaneSummary(role: TeamRoleDefinition) { +function buildPlannedRoleLaneSummary( + role: TeamRoleDefinition, + copy: TeamWorkspaceCanvasLaneCopy, +) { return { - badgeLabel: "待开始", + badgeLabel: copy.plannedBadge, badgeClassName: "border border-slate-200 bg-slate-50 text-slate-600", dotClassName: "bg-slate-300", summary: normalizeTeamWorkspaceDisplayValue(role.summary) || role.summary, - statusHint: "等待系统把这项任务拆出来", + statusHint: copy.plannedStatusHint, }; } @@ -432,6 +711,7 @@ function resolveRuntimeMemberMatchingPlannedRoleId( } function buildSessionCanvasLane(params: { + copy: TeamWorkspaceCanvasLaneCopy; session: TeamWorkspaceCanvasLaneSession; runtimeMembers: TeamWorkspaceRuntimeMember[]; plannedRoles: TeamRoleDefinition[]; @@ -439,7 +719,7 @@ function buildSessionCanvasLane(params: { previewBySessionId?: Record; activityTimelineEntryLimit: number; }): TeamWorkspaceCanvasLane { - const { session, runtimeMembers, plannedRoles } = params; + const { copy, session, runtimeMembers, plannedRoles } = params; const previewState = params.previewBySessionId?.[session.id] ?? null; const mergedEntries = mergeSessionActivityEntries( params.liveActivityBySessionId?.[session.id], @@ -448,7 +728,7 @@ function buildSessionCanvasLane(params: { ); const cardActivityPreview = buildActivityPreviewFromEntry(mergedEntries[0]) ?? previewState?.preview; - const meta = resolveStatusMeta(session.runtimeStatus); + const meta = resolveStatusMeta(session.runtimeStatus, copy); const matchedRuntimeMemberId = resolveLaneMatchingRuntimeMemberId( session, runtimeMembers, @@ -472,7 +752,7 @@ function buildSessionCanvasLane(params: { title: session.name, summary: normalizeTeamWorkspaceDisplayValue(session.taskSummary) || - "暂时还没有任务摘要,打开详情后可查看完整上下文。", + copy.sessionSummaryEmpty, badgeLabel: meta.label, badgeClassName: meta.badgeClassName, dotClassName: meta.dotClassName, @@ -483,20 +763,21 @@ function buildSessionCanvasLane(params: { profileLabel: session.profileName || undefined, presetLabel, modelLabel: session.model || undefined, - statusHint: buildRuntimeDetailSummary(session), - updatedAtLabel: formatUpdatedAt(session.updatedAt), + statusHint: buildRuntimeDetailSummary(session, copy), + updatedAtLabel: copy.formatUpdatedAt(session.updatedAt), skillLabels: (session.skills ?? []) .slice(0, 4) .map((skill) => buildSkillDisplayName(skill)), session, previewText: cardActivityPreview ?? - buildSessionLaneEmptyState({ session, previewState }), + buildSessionLaneEmptyState({ copy, session, previewState }), previewEntries: mergedEntries.slice(0, SESSION_LANE_PREVIEW_ENTRY_LIMIT), }; } export function buildTeamWorkspaceCanvasLanes(params: { + copy?: TeamWorkspaceCanvasLaneCopy; hasRealTeamGraph: boolean; sessions: TeamWorkspaceCanvasLaneSession[]; runtimeMembers: TeamWorkspaceRuntimeMember[]; @@ -505,9 +786,12 @@ export function buildTeamWorkspaceCanvasLanes(params: { previewBySessionId?: Record; activityTimelineEntryLimit: number; }): TeamWorkspaceCanvasLane[] { + const copy = params.copy ?? SOURCE_TEAM_WORKSPACE_CANVAS_LANE_COPY; + if (params.hasRealTeamGraph) { return params.sessions.map((session) => buildSessionCanvasLane({ + copy, session, runtimeMembers: params.runtimeMembers, plannedRoles: params.plannedRoles, @@ -520,7 +804,7 @@ export function buildTeamWorkspaceCanvasLanes(params: { if (params.runtimeMembers.length > 0) { return params.runtimeMembers.map((member) => { - const laneSummary = buildCanvasLaneTitleSummary(member); + const laneSummary = buildCanvasLaneTitleSummary(member, copy); const matchedPlannedRoleId = resolveRuntimeMemberMatchingPlannedRoleId( member, params.plannedRoles, @@ -542,7 +826,7 @@ export function buildTeamWorkspaceCanvasLanes(params: { resolveTeamWorkspaceRoleHintLabel(member.roleKey) || undefined, profileLabel: undefined, statusHint: laneSummary.statusHint, - updatedAtLabel: "等待任务接手", + updatedAtLabel: copy.runtimeUpdatedAtWaiting, skillLabels: [], previewText: normalizeTeamWorkspaceDisplayValue(member.summary) || member.summary, @@ -552,7 +836,7 @@ export function buildTeamWorkspaceCanvasLanes(params: { } return params.plannedRoles.map((role) => { - const laneSummary = buildPlannedRoleLaneSummary(role); + const laneSummary = buildPlannedRoleLaneSummary(role, copy); return { id: role.id, @@ -567,7 +851,7 @@ export function buildTeamWorkspaceCanvasLanes(params: { roleLabel: resolveTeamWorkspaceRoleHintLabel(role.roleKey) || undefined, profileLabel: undefined, statusHint: laneSummary.statusHint, - updatedAtLabel: "计划分工", + updatedAtLabel: copy.plannedUpdatedAt, skillLabels: [], previewText: normalizeTeamWorkspaceDisplayValue(role.summary) || role.summary, diff --git a/src/components/agent/chat/team-workspace-runtime/formationDisplaySelectors.test.ts b/src/components/agent/chat/team-workspace-runtime/formationDisplaySelectors.test.ts index 825766445..409c43dfc 100644 --- a/src/components/agent/chat/team-workspace-runtime/formationDisplaySelectors.test.ts +++ b/src/components/agent/chat/team-workspace-runtime/formationDisplaySelectors.test.ts @@ -1,12 +1,37 @@ -import { describe, expect, it } from "vitest"; +import { beforeEach, describe, expect, it } from "vitest"; +import { changeLimeLocale, getLimeI18n } from "@/i18n/createI18n"; import { + buildTeamWorkspaceFormationCopy, buildRuntimeFormationDisplayState, buildSelectedTeamPlanDisplayState, + type TeamWorkspaceFormationCopy, + type TeamWorkspaceFormationTranslate, } from "./formationDisplaySelectors"; describe("formationDisplaySelectors", () => { + let formationCopy: TeamWorkspaceFormationCopy; + + beforeEach(async () => { + await changeLimeLocale("en-US"); + const translate: TeamWorkspaceFormationTranslate = (key, options) => + String( + getLimeI18n().t( + key as never, + { + ns: "agent", + ...(options ?? {}), + } as never, + ), + ); + formationCopy = buildTeamWorkspaceFormationCopy({ + locale: "en-US", + translate, + }); + }); + it("已选 Team 但无 runtime formation 时,应产出计划分工展示模型", () => { const state = buildSelectedTeamPlanDisplayState({ + copy: formationCopy, selectedTeamLabel: "代码排障团队", selectedTeamSummary: "分析、执行、验证三段式推进。", selectedTeamRoles: [ @@ -20,9 +45,10 @@ describe("formationDisplaySelectors", () => { expect(state.hasSelectedTeamPlan).toBe(true); expect(state.summaryBadges.map((badge) => badge.text)).toEqual([ - "分工方案 · 代码排障团队", - "1 个计划分工", + "Team plan · 代码排障团队", + "1 planned role(s)", ]); + expect(formationCopy.detailRoleSectionPlanLabel).toBe("Role plan"); expect(state.roleCards).toEqual([ { id: "explorer", @@ -34,6 +60,7 @@ describe("formationDisplaySelectors", () => { it("runtime formation 已就绪时,应产出状态、当前进展与参考分工", () => { const state = buildRuntimeFormationDisplayState({ + copy: formationCopy, teamDispatchPreviewState: { requestId: "runtime-1", status: "formed", @@ -64,29 +91,33 @@ describe("formationDisplaySelectors", () => { }); expect(state.hasRuntimeFormation).toBe(true); - expect(state.hint).toContain("当前任务的分工已经准备好"); + expect(state.hint).toContain("team split is ready"); expect(state.summaryBadges.map((badge) => badge.text)).toEqual([ - "分工方案 · 修复分工方案", - "已就绪", - "1 条当前进展", - "参考方案 · 代码排障团队", + "Team plan · 修复分工方案", + "Ready", + "1 active progress item(s)", + "Reference plan · 代码排障团队", ]); - expect(state.panelHeadline).toBe("任务分工已准备好"); + expect(formationCopy.formatTaskCountBadge(2)).toBe("2 task(s)"); + expect(formationCopy.detailHintRuntimeWithReference).toBe( + "View the current task split and reference plan", + ); + expect(state.panelHeadline).toBe("Team split is ready"); expect(state.memberCards[0]).toMatchObject({ label: "分析", - badgeLabel: "待分配", + badgeLabel: "Planned", }); expect(state.blueprintRoleCards[0]).toMatchObject({ label: "分析", summary: "先定位问题与影响面。", }); - expect(state.noticeText).toContain("当前分工方案已就绪"); - expect(state.noticeText).toContain("任务拆出后"); - expect(state.noticeText).toContain("当前进展"); + expect(state.noticeText).toContain("team plan is ready"); + expect(state.noticeText).toContain("current progress"); }); it("runtime formation 失败时,应优先使用失败原因", () => { const state = buildRuntimeFormationDisplayState({ + copy: formationCopy, teamDispatchPreviewState: { requestId: "runtime-2", status: "failed", diff --git a/src/components/agent/chat/team-workspace-runtime/formationDisplaySelectors.ts b/src/components/agent/chat/team-workspace-runtime/formationDisplaySelectors.ts index 6da5aa79a..71583f79c 100644 --- a/src/components/agent/chat/team-workspace-runtime/formationDisplaySelectors.ts +++ b/src/components/agent/chat/team-workspace-runtime/formationDisplaySelectors.ts @@ -1,11 +1,9 @@ +import { formatNumber } from "@/i18n/format"; import type { TeamRoleDefinition } from "../utils/teamDefinitions"; import { normalizeTeamWorkspaceDisplayValue } from "../utils/teamWorkspaceDisplay"; import { - TEAM_WORKSPACE_IDLE_STATUS_LABEL, - TEAM_WORKSPACE_PLAN_LABEL, - TEAM_WORKSPACE_WAITING_HEADLINE, -} from "../utils/teamWorkspaceCopy"; -import { + type TeamWorkspaceRuntimeFormationStatus, + type TeamWorkspaceRuntimeMemberStatus, resolveRuntimeFormationStatusMeta, resolveRuntimeMemberStatusMeta, type TeamWorkspaceRuntimeFormationState, @@ -56,22 +54,232 @@ export interface TeamWorkspaceRuntimeFormationDisplayState { blueprintRoleCards: TeamWorkspaceFormationRoleCard[]; } +type TeamWorkspaceFormationResourceKey = + | "agentChat.teamWorkspace.formation.emptyDetail.default" + | "agentChat.teamWorkspace.formation.emptyDetail.failed" + | "agentChat.teamWorkspace.formation.emptyDetail.formed" + | "agentChat.teamWorkspace.formation.emptyDetail.forming" + | "agentChat.teamWorkspace.formation.detail.hint.planOnly" + | "agentChat.teamWorkspace.formation.detail.hint.runtimeWithReference" + | "agentChat.teamWorkspace.formation.detail.referenceSummaryDefault" + | "agentChat.teamWorkspace.formation.detail.roleSection.plan" + | "agentChat.teamWorkspace.formation.detail.roleSection.runtime" + | "agentChat.teamWorkspace.formation.detail.summary.plan" + | "agentChat.teamWorkspace.formation.detail.summary.runtime" + | "agentChat.teamWorkspace.formation.hint.default" + | "agentChat.teamWorkspace.formation.hint.failed" + | "agentChat.teamWorkspace.formation.hint.formed" + | "agentChat.teamWorkspace.formation.hint.forming" + | "agentChat.teamWorkspace.formation.memberStatus.completed" + | "agentChat.teamWorkspace.formation.memberStatus.failed" + | "agentChat.teamWorkspace.formation.memberStatus.planned" + | "agentChat.teamWorkspace.formation.memberStatus.running" + | "agentChat.teamWorkspace.formation.memberStatus.spawning" + | "agentChat.teamWorkspace.formation.memberStatus.waiting" + | "agentChat.teamWorkspace.formation.notice.default" + | "agentChat.teamWorkspace.formation.notice.failed" + | "agentChat.teamWorkspace.formation.notice.formed" + | "agentChat.teamWorkspace.formation.notice.forming" + | "agentChat.teamWorkspace.formation.panel.description.default" + | "agentChat.teamWorkspace.formation.panel.description.failed" + | "agentChat.teamWorkspace.formation.panel.title" + | "agentChat.teamWorkspace.formation.status.failed.label" + | "agentChat.teamWorkspace.formation.status.failed.title" + | "agentChat.teamWorkspace.formation.status.formed.label" + | "agentChat.teamWorkspace.formation.status.formed.title" + | "agentChat.teamWorkspace.formation.status.forming.label" + | "agentChat.teamWorkspace.formation.status.forming.title" + | "agentChat.teamWorkspace.formation.summaryBadge.memberCount" + | "agentChat.teamWorkspace.formation.summaryBadge.planLabel" + | "agentChat.teamWorkspace.formation.summaryBadge.planRoleCount" + | "agentChat.teamWorkspace.formation.summaryBadge.referenceLabel" + | "agentChat.teamWorkspace.formation.summaryBadge.taskCount" + | "agentChat.teamWorkspace.formation.waitingHeadline"; + +export type TeamWorkspaceFormationTranslate = ( + key: TeamWorkspaceFormationResourceKey, + options?: Record, +) => string; + +export interface TeamWorkspaceFormationCopy { + emptyDetailDefault: string; + emptyDetailFailed: string; + emptyDetailFormed: string; + emptyDetailForming: string; + detailHintPlanOnly: string; + detailHintRuntimeWithReference: string; + detailReferenceSummaryDefault: string; + detailRoleSectionPlanLabel: string; + detailRoleSectionRuntimeLabel: string; + detailSummaryPlanLabel: string; + detailSummaryRuntimeLabel: string; + formatMemberCountBadge: (count: number) => string; + formatPlanLabelBadge: (label: string) => string; + formatPlanRoleCountBadge: (count: number) => string; + formatReferenceLabelBadge: (label: string) => string; + formatTaskCountBadge: (count: number) => string; + getFormationStatusLabel: ( + status: TeamWorkspaceRuntimeFormationStatus, + ) => string; + getFormationStatusTitle: ( + status: TeamWorkspaceRuntimeFormationStatus, + ) => string; + getMemberStatusLabel: (status: TeamWorkspaceRuntimeMemberStatus) => string; + hintDefault: string; + hintFailed: string; + hintFormed: string; + hintForming: string; + noticeDefault: string; + noticeFailed: string; + noticeFormed: string; + noticeForming: string; + panelDescriptionDefault: string; + panelDescriptionFailed: string; + panelTitle: string; + waitingHeadline: string; +} + +export function buildTeamWorkspaceFormationCopy(params: { + locale?: string | null; + translate: TeamWorkspaceFormationTranslate; +}): TeamWorkspaceFormationCopy { + const formatCount = (count: number) => + formatNumber(count, { locale: params.locale }); + + return { + emptyDetailDefault: params.translate( + "agentChat.teamWorkspace.formation.emptyDetail.default", + ), + emptyDetailFailed: params.translate( + "agentChat.teamWorkspace.formation.emptyDetail.failed", + ), + emptyDetailFormed: params.translate( + "agentChat.teamWorkspace.formation.emptyDetail.formed", + ), + emptyDetailForming: params.translate( + "agentChat.teamWorkspace.formation.emptyDetail.forming", + ), + detailHintPlanOnly: params.translate( + "agentChat.teamWorkspace.formation.detail.hint.planOnly", + ), + detailHintRuntimeWithReference: params.translate( + "agentChat.teamWorkspace.formation.detail.hint.runtimeWithReference", + ), + detailReferenceSummaryDefault: params.translate( + "agentChat.teamWorkspace.formation.detail.referenceSummaryDefault", + ), + detailRoleSectionPlanLabel: params.translate( + "agentChat.teamWorkspace.formation.detail.roleSection.plan", + ), + detailRoleSectionRuntimeLabel: params.translate( + "agentChat.teamWorkspace.formation.detail.roleSection.runtime", + ), + detailSummaryPlanLabel: params.translate( + "agentChat.teamWorkspace.formation.detail.summary.plan", + ), + detailSummaryRuntimeLabel: params.translate( + "agentChat.teamWorkspace.formation.detail.summary.runtime", + ), + formatMemberCountBadge: (count) => + params.translate( + "agentChat.teamWorkspace.formation.summaryBadge.memberCount", + { + formattedCount: formatCount(count), + }, + ), + formatPlanLabelBadge: (label) => + params.translate( + "agentChat.teamWorkspace.formation.summaryBadge.planLabel", + { label }, + ), + formatPlanRoleCountBadge: (count) => + params.translate( + "agentChat.teamWorkspace.formation.summaryBadge.planRoleCount", + { + formattedCount: formatCount(count), + }, + ), + formatReferenceLabelBadge: (label) => + params.translate( + "agentChat.teamWorkspace.formation.summaryBadge.referenceLabel", + { label }, + ), + formatTaskCountBadge: (count) => + params.translate( + "agentChat.teamWorkspace.formation.summaryBadge.taskCount", + { + formattedCount: formatCount(count), + }, + ), + getFormationStatusLabel: (status) => + params.translate( + `agentChat.teamWorkspace.formation.status.${status}.label` as TeamWorkspaceFormationResourceKey, + ), + getFormationStatusTitle: (status) => + params.translate( + `agentChat.teamWorkspace.formation.status.${status}.title` as TeamWorkspaceFormationResourceKey, + ), + getMemberStatusLabel: (status) => + params.translate( + `agentChat.teamWorkspace.formation.memberStatus.${status}` as TeamWorkspaceFormationResourceKey, + ), + hintDefault: params.translate( + "agentChat.teamWorkspace.formation.hint.default", + ), + hintFailed: params.translate( + "agentChat.teamWorkspace.formation.hint.failed", + ), + hintFormed: params.translate( + "agentChat.teamWorkspace.formation.hint.formed", + ), + hintForming: params.translate( + "agentChat.teamWorkspace.formation.hint.forming", + ), + noticeDefault: params.translate( + "agentChat.teamWorkspace.formation.notice.default", + ), + noticeFailed: params.translate( + "agentChat.teamWorkspace.formation.notice.failed", + ), + noticeFormed: params.translate( + "agentChat.teamWorkspace.formation.notice.formed", + ), + noticeForming: params.translate( + "agentChat.teamWorkspace.formation.notice.forming", + ), + panelDescriptionDefault: params.translate( + "agentChat.teamWorkspace.formation.panel.description.default", + ), + panelDescriptionFailed: params.translate( + "agentChat.teamWorkspace.formation.panel.description.failed", + ), + panelTitle: params.translate( + "agentChat.teamWorkspace.formation.panel.title", + ), + waitingHeadline: params.translate( + "agentChat.teamWorkspace.formation.waitingHeadline", + ), + }; +} + export function buildRuntimeFormationHint( + copy: TeamWorkspaceFormationCopy, teamDispatchPreviewState?: TeamWorkspaceRuntimeFormationState | null, ) { switch (teamDispatchPreviewState?.status) { case "forming": - return "系统正在准备当前任务的分工,任务拆出后会自动开始处理。"; + return copy.hintForming; case "formed": - return "当前任务的分工已经准备好,任务拆出后会继续接手处理。"; + return copy.hintFormed; case "failed": - return "当前任务的分工准备失败,但你仍然可以继续在当前任务里推进。"; + return copy.hintFailed; default: - return "需要时这里会自动展开成当前进展面板。"; + return copy.hintDefault; } } export function buildRuntimeFormationEmptyDetail( + copy: TeamWorkspaceFormationCopy, teamDispatchPreviewState?: TeamWorkspaceRuntimeFormationState | null, ) { const errorMessage = normalizeTeamWorkspaceDisplayValue( @@ -80,17 +288,18 @@ export function buildRuntimeFormationEmptyDetail( switch (teamDispatchPreviewState?.status) { case "forming": - return "系统正在根据当前任务准备分工。完成后,这里会先展示分工卡片,再接入真实处理进展。"; + return copy.emptyDetailForming; case "formed": - return "当前分工方案已经准备好。画布会先展示当前分工,等任务真正开始处理后,再自动切换为当前进展。"; + return copy.emptyDetailFormed; case "failed": - return errorMessage || "当前任务分工准备失败,暂时无法展示当前进展。"; + return errorMessage || copy.emptyDetailFailed; default: - return `${TEAM_WORKSPACE_IDLE_STATUS_LABEL}。系统开始分工后,详情区会切换为进展摘要视图。`; + return copy.emptyDetailDefault; } } export function buildSelectedTeamPlanDisplayState(params: { + copy: TeamWorkspaceFormationCopy; selectedTeamLabel?: string | null; selectedTeamSummary?: string | null; selectedTeamRoles?: TeamRoleDefinition[] | null; @@ -121,7 +330,7 @@ export function buildSelectedTeamPlanDisplayState(params: { ? [ { key: "plan-label", - text: `${TEAM_WORKSPACE_PLAN_LABEL} · ${label}`, + text: params.copy.formatPlanLabelBadge(label), className: "rounded-full border border-sky-200 bg-sky-50 px-2.5 py-1 text-sky-700", }, @@ -131,7 +340,7 @@ export function buildSelectedTeamPlanDisplayState(params: { ? [ { key: "plan-role-count", - text: `${roleCards.length} 个计划分工`, + text: params.copy.formatPlanRoleCountBadge(roleCards.length), className: "rounded-full border border-slate-200 bg-white px-2.5 py-1", }, @@ -145,12 +354,20 @@ export function buildSelectedTeamPlanDisplayState(params: { } export function buildRuntimeFormationDisplayState(params: { + copy: TeamWorkspaceFormationCopy; teamDispatchPreviewState?: TeamWorkspaceRuntimeFormationState | null; fallbackLabel?: string | null; fallbackSummary?: string | null; }): TeamWorkspaceRuntimeFormationDisplayState { const state = params.teamDispatchPreviewState ?? null; - const meta = state ? resolveRuntimeFormationStatusMeta(state.status) : null; + const meta = state + ? { + badgeClassName: resolveRuntimeFormationStatusMeta(state.status) + .badgeClassName, + label: params.copy.getFormationStatusLabel(state.status), + title: params.copy.getFormationStatusTitle(state.status), + } + : null; const label = normalizeTeamWorkspaceDisplayValue( state?.label || state?.blueprint?.label || params.fallbackLabel, ); @@ -168,7 +385,7 @@ export function buildRuntimeFormationDisplayState(params: { label: normalizeTeamWorkspaceDisplayValue(member.label) || member.label, summary: normalizeTeamWorkspaceDisplayValue(member.summary) || member.summary, - badgeLabel: memberMeta.label, + badgeLabel: params.copy.getMemberStatusLabel(member.status), badgeClassName: memberMeta.badgeClassName, }; }); @@ -180,24 +397,24 @@ export function buildRuntimeFormationDisplayState(params: { const noticeText = state?.status === "forming" - ? "系统正在准备当前任务分工,完成后会先展示分工卡片,后续再切换为独立的当前进展。" + ? params.copy.noticeForming : state?.status === "formed" - ? "当前分工方案已就绪。任务拆出后,这里会从方案视图过渡到当前进展。" + ? params.copy.noticeFormed : state?.status === "failed" - ? errorMessage || "当前任务分工准备失败,暂时还没有任务接手。" - : `${TEAM_WORKSPACE_IDLE_STATUS_LABEL}。系统开始分工后,这里会生成独立的当前进展。`; + ? errorMessage || params.copy.noticeFailed + : params.copy.noticeDefault; return { hasRuntimeFormation: Boolean(state), - hint: buildRuntimeFormationHint(state), - emptyDetail: buildRuntimeFormationEmptyDetail(state), + hint: buildRuntimeFormationHint(params.copy, state), + emptyDetail: buildRuntimeFormationEmptyDetail(params.copy, state), noticeText, summaryBadges: [ ...(label ? [ { key: "runtime-label", - text: `${TEAM_WORKSPACE_PLAN_LABEL} · ${label}`, + text: params.copy.formatPlanLabelBadge(label), className: "rounded-full border border-sky-200 bg-sky-50 px-2.5 py-1 text-sky-700", }, @@ -216,7 +433,7 @@ export function buildRuntimeFormationDisplayState(params: { ? [ { key: "runtime-member-count", - text: `${memberCards.length} 条当前进展`, + text: params.copy.formatMemberCountBadge(memberCards.length), className: "rounded-full border border-slate-200 bg-white px-2.5 py-1", }, @@ -226,22 +443,22 @@ export function buildRuntimeFormationDisplayState(params: { ? [ { key: "runtime-blueprint-label", - text: `参考方案 · ${referenceLabel}`, + text: params.copy.formatReferenceLabelBadge(referenceLabel), className: "rounded-full border border-slate-200 bg-white px-2.5 py-1", }, ] : []), ], - panelTitle: "当前任务分工", + panelTitle: params.copy.panelTitle, panelStatusLabel: meta?.label ?? null, panelStatusBadgeClassName: meta?.badgeClassName ?? null, panelLabel: label, - panelHeadline: meta?.title || TEAM_WORKSPACE_WAITING_HEADLINE, + panelHeadline: meta?.title || params.copy.waitingHeadline, panelDescription: state?.status === "failed" - ? errorMessage || "当前任务分工准备失败,暂时无法展示更多内容。" - : summary || "这里会先展示当前分工方案,任务拆出后再切换成当前进展。", + ? errorMessage || params.copy.panelDescriptionFailed + : summary || params.copy.panelDescriptionDefault, referenceLabel, memberCards, blueprintRoleCards, diff --git a/src/components/agent/chat/team-workspace-runtime/liveRuntimeProjector.test.ts b/src/components/agent/chat/team-workspace-runtime/liveRuntimeProjector.test.ts index 3f3ab230f..b845fc38a 100644 --- a/src/components/agent/chat/team-workspace-runtime/liveRuntimeProjector.test.ts +++ b/src/components/agent/chat/team-workspace-runtime/liveRuntimeProjector.test.ts @@ -1,11 +1,32 @@ import { describe, expect, it } from "vitest"; +import { changeLimeLocale, getLimeI18n } from "@/i18n/createI18n"; import type { AgentEvent } from "@/lib/api/agentProtocol"; import type { TeamWorkspaceRuntimeSessionSnapshot } from "../teamWorkspaceRuntime"; import { + buildLiveRuntimeProjectorCopy, buildStatusChangedProjection, projectRuntimeStreamEvent, + type LiveRuntimeProjectorTranslate, } from "./liveRuntimeProjector"; +async function buildEnglishLiveRuntimeCopy() { + await changeLimeLocale("en-US"); + const translate: LiveRuntimeProjectorTranslate = (key, options) => + String( + getLimeI18n().t( + key as never, + { + ns: "agent", + ...(options ?? {}), + } as never, + ), + ); + + return buildLiveRuntimeProjectorCopy({ + translate, + }); +} + function createSessionSnapshot( overrides: Partial = {}, ): TeamWorkspaceRuntimeSessionSnapshot { @@ -35,6 +56,48 @@ describe("liveRuntimeProjector", () => { }); }); + it("应支持注入英文 live runtime copy 且保留 runtime 工具名", async () => { + const copy = await buildEnglishLiveRuntimeCopy(); + const statusProjection = buildStatusChangedProjection({ + sessionId: "child-1", + status: "running", + session: createSessionSnapshot(), + copy, + }); + + expect(statusProjection.entry.title).toBe("Status changed"); + expect(statusProjection.entry.detail).toContain("Running"); + + const toolProjection = projectRuntimeStreamEvent({ + sessionId: "child-1", + session: createSessionSnapshot({ + runtimeStatus: "running", + latestTurnStatus: "running", + }), + copy, + event: { + type: "tool_start", + tool_name: "browser_snapshot", + tool_id: "tool-1", + } as AgentEvent, + }); + + expect(toolProjection?.entry?.title).toBe("Processing · 页面截图"); + expect(toolProjection?.entry?.detail).toBe("Working on 页面截图."); + + const queueProjection = projectRuntimeStreamEvent({ + sessionId: "child-1", + session: createSessionSnapshot(), + copy, + event: { + type: "queue_added", + } as AgentEvent, + }); + + expect(queueProjection?.entry?.title).toBe("Queued"); + expect(queueProjection?.entry?.detail).toContain("New instructions"); + }); + it("tool_start 应把工具名映射为中文展示标题", () => { const projection = projectRuntimeStreamEvent({ sessionId: "child-1", diff --git a/src/components/agent/chat/team-workspace-runtime/liveRuntimeProjector.ts b/src/components/agent/chat/team-workspace-runtime/liveRuntimeProjector.ts index e24fd1130..e9cf5d69a 100644 --- a/src/components/agent/chat/team-workspace-runtime/liveRuntimeProjector.ts +++ b/src/components/agent/chat/team-workspace-runtime/liveRuntimeProjector.ts @@ -3,8 +3,8 @@ import { type AgentToolExecutionResult as ToolExecutionResult, type AgentEvent, } from "@/lib/api/agentProtocol"; +import agentSourceResource from "@/i18n/resources/zh-CN/agent.json"; import { - buildStatusEventActivityEntry, buildTeamWorkspaceActivityEntryFromThreadItem, buildTeamWorkspaceSessionFingerprint, normalizeTeamWorkspaceRuntimeStatus, @@ -14,7 +14,6 @@ import { type TeamWorkspaceRuntimeStatus, } from "../teamWorkspaceRuntime"; import { resolveUserFacingToolDisplayLabel } from "../utils/toolDisplayInfo"; -import { resolveTeamWorkspaceDisplayRuntimeStatusLabel } from "../utils/teamWorkspaceCopy"; import { buildRuntimeStatusPresentationText, isRuntimeStatusDiagnosticsOnly, @@ -47,6 +46,217 @@ const LIVE_RUNTIME_PATCH_KEYS = [ "retryableOverload", ] as const; +type LiveRuntimeProjectorResourceKey = + | "agentChat.teamWorkspace.liveRuntime.detail.statusChanged" + | "agentChat.teamWorkspace.liveRuntime.detail.toolCompletedFallback" + | "agentChat.teamWorkspace.liveRuntime.detail.toolProcessing" + | "agentChat.teamWorkspace.liveRuntime.lifecycle.queueAdded.detail" + | "agentChat.teamWorkspace.liveRuntime.lifecycle.queueAdded.title" + | "agentChat.teamWorkspace.liveRuntime.lifecycle.queueStarted.detail" + | "agentChat.teamWorkspace.liveRuntime.lifecycle.queueStarted.title" + | "agentChat.teamWorkspace.liveRuntime.lifecycle.turnCompleted.detail" + | "agentChat.teamWorkspace.liveRuntime.lifecycle.turnCompleted.title" + | "agentChat.teamWorkspace.liveRuntime.lifecycle.turnFailed.detail" + | "agentChat.teamWorkspace.liveRuntime.lifecycle.turnFailed.title" + | "agentChat.teamWorkspace.liveRuntime.lifecycle.turnStarted.detail" + | "agentChat.teamWorkspace.liveRuntime.lifecycle.turnStarted.title" + | "agentChat.teamWorkspace.liveRuntime.status.completed" + | "agentChat.teamWorkspace.liveRuntime.status.processing" + | "agentChat.teamWorkspace.liveRuntime.status.queued" + | "agentChat.teamWorkspace.liveRuntime.status.retry" + | "agentChat.teamWorkspace.liveRuntime.status.warning" + | "agentChat.teamWorkspace.runtimeStatus.aborted" + | "agentChat.teamWorkspace.runtimeStatus.closed" + | "agentChat.teamWorkspace.runtimeStatus.completed" + | "agentChat.teamWorkspace.runtimeStatus.failed" + | "agentChat.teamWorkspace.runtimeStatus.idle" + | "agentChat.teamWorkspace.runtimeStatus.queued" + | "agentChat.teamWorkspace.runtimeStatus.running" + | "agentChat.teamWorkspace.liveRuntime.title.currentProgress" + | "agentChat.teamWorkspace.liveRuntime.title.error" + | "agentChat.teamWorkspace.liveRuntime.title.processing" + | "agentChat.teamWorkspace.liveRuntime.title.statusChanged" + | "agentChat.teamWorkspace.liveRuntime.title.textDraft" + | "agentChat.teamWorkspace.liveRuntime.title.thinkingDraft" + | "agentChat.teamWorkspace.liveRuntime.title.tool" + | "agentChat.teamWorkspace.liveRuntime.title.toolTargetFallback" + | "agentChat.teamWorkspace.liveRuntime.title.warning"; + +export type LiveRuntimeProjectorTranslate = ( + key: LiveRuntimeProjectorResourceKey, + options?: Record, +) => string; + +type AgentSourceResourceKey = keyof typeof agentSourceResource; + +function interpolateLiveRuntimeProjectorSourceTemplate( + template: string, + values?: Record, +): string { + return template.replace(/\{\{\s*(\w+)\s*\}\}/g, (match, name) => { + const value = values?.[name]; + return value == null ? match : String(value); + }); +} + +function translateLiveRuntimeProjectorSourceKey( + key: LiveRuntimeProjectorResourceKey, + values?: Record, +): string { + const template = agentSourceResource[key as AgentSourceResourceKey] ?? key; + return interpolateLiveRuntimeProjectorSourceTemplate(template, values); +} + +export interface LiveRuntimeProjectorCopy { + completedStatus: string; + currentProgressTitle: string; + errorTitle: string; + formatStatusChangedDetail: (status: string) => string; + formatToolProcessingDetail: (target: string) => string; + formatToolTitle: (status: string, toolName: string) => string; + getRuntimeStatusLabel: (status?: TeamWorkspaceRuntimeStatus) => string; + lifecycle: { + queueAdded: { title: string; detail: string }; + queueStarted: { title: string; detail: string }; + turnCompleted: { title: string; detail: string }; + turnFailed: { title: string; detail: string }; + turnStarted: { title: string; detail: string }; + }; + processingStatus: string; + processingTitle: string; + queuedStatus: string; + retryStatus: string; + statusChangedTitle: string; + textDraftTitle: string; + thinkingDraftTitle: string; + toolCompletedFallback: string; + toolTargetFallback: string; + warningStatus: string; + warningTitle: string; +} + +function resolveRuntimeStatusResourceKey( + status?: TeamWorkspaceRuntimeStatus, +): LiveRuntimeProjectorResourceKey { + return `agentChat.teamWorkspace.runtimeStatus.${status ?? "idle"}` as LiveRuntimeProjectorResourceKey; +} + +export function buildLiveRuntimeProjectorCopy(params: { + translate: LiveRuntimeProjectorTranslate; +}): LiveRuntimeProjectorCopy { + return { + completedStatus: params.translate( + "agentChat.teamWorkspace.liveRuntime.status.completed", + ), + currentProgressTitle: params.translate( + "agentChat.teamWorkspace.liveRuntime.title.currentProgress", + ), + errorTitle: params.translate( + "agentChat.teamWorkspace.liveRuntime.title.error", + ), + formatStatusChangedDetail: (status) => + params.translate( + "agentChat.teamWorkspace.liveRuntime.detail.statusChanged", + { + status, + }, + ), + formatToolProcessingDetail: (target) => + params.translate( + "agentChat.teamWorkspace.liveRuntime.detail.toolProcessing", + { + target, + }, + ), + formatToolTitle: (status, toolName) => + params.translate("agentChat.teamWorkspace.liveRuntime.title.tool", { + status, + toolName, + }), + getRuntimeStatusLabel: (status) => + params.translate(resolveRuntimeStatusResourceKey(status)), + lifecycle: { + queueAdded: { + title: params.translate( + "agentChat.teamWorkspace.liveRuntime.lifecycle.queueAdded.title", + ), + detail: params.translate( + "agentChat.teamWorkspace.liveRuntime.lifecycle.queueAdded.detail", + ), + }, + queueStarted: { + title: params.translate( + "agentChat.teamWorkspace.liveRuntime.lifecycle.queueStarted.title", + ), + detail: params.translate( + "agentChat.teamWorkspace.liveRuntime.lifecycle.queueStarted.detail", + ), + }, + turnCompleted: { + title: params.translate( + "agentChat.teamWorkspace.liveRuntime.lifecycle.turnCompleted.title", + ), + detail: params.translate( + "agentChat.teamWorkspace.liveRuntime.lifecycle.turnCompleted.detail", + ), + }, + turnFailed: { + title: params.translate( + "agentChat.teamWorkspace.liveRuntime.lifecycle.turnFailed.title", + ), + detail: params.translate( + "agentChat.teamWorkspace.liveRuntime.lifecycle.turnFailed.detail", + ), + }, + turnStarted: { + title: params.translate( + "agentChat.teamWorkspace.liveRuntime.lifecycle.turnStarted.title", + ), + detail: params.translate( + "agentChat.teamWorkspace.liveRuntime.lifecycle.turnStarted.detail", + ), + }, + }, + processingStatus: params.translate( + "agentChat.teamWorkspace.liveRuntime.status.processing", + ), + processingTitle: params.translate( + "agentChat.teamWorkspace.liveRuntime.title.processing", + ), + queuedStatus: params.translate( + "agentChat.teamWorkspace.liveRuntime.status.queued", + ), + retryStatus: params.translate( + "agentChat.teamWorkspace.liveRuntime.status.retry", + ), + statusChangedTitle: params.translate( + "agentChat.teamWorkspace.liveRuntime.title.statusChanged", + ), + textDraftTitle: params.translate( + "agentChat.teamWorkspace.liveRuntime.title.textDraft", + ), + thinkingDraftTitle: params.translate( + "agentChat.teamWorkspace.liveRuntime.title.thinkingDraft", + ), + toolCompletedFallback: params.translate( + "agentChat.teamWorkspace.liveRuntime.detail.toolCompletedFallback", + ), + toolTargetFallback: params.translate( + "agentChat.teamWorkspace.liveRuntime.title.toolTargetFallback", + ), + warningStatus: params.translate( + "agentChat.teamWorkspace.liveRuntime.status.warning", + ), + warningTitle: params.translate( + "agentChat.teamWorkspace.liveRuntime.title.warning", + ), + }; +} + +const SOURCE_LIVE_RUNTIME_PROJECTOR_COPY = buildLiveRuntimeProjectorCopy({ + translate: translateLiveRuntimeProjectorSourceKey, +}); + type LiveRuntimePatchKey = (typeof LIVE_RUNTIME_PATCH_KEYS)[number]; export interface SessionLiveStreamState { @@ -153,24 +363,32 @@ function buildActivityEntry(params: { }; } -function buildTextDraftEntry(sessionId: string, draft?: string) { +function buildTextDraftEntry( + sessionId: string, + draft?: string, + copy: LiveRuntimeProjectorCopy = SOURCE_LIVE_RUNTIME_PROJECTOR_COPY, +) { return buildActivityEntry({ id: `stream-text:${sessionId}`, - title: "内容生成中", + title: copy.textDraftTitle, detail: draft, - statusLabel: "处理中", + statusLabel: copy.processingStatus, badgeClassName: IN_PROGRESS_BADGE_CLASS_NAME, sourceType: "agent_message", sourceLabel: "stream text", }); } -function buildThinkingDraftEntry(sessionId: string, draft?: string) { +function buildThinkingDraftEntry( + sessionId: string, + draft?: string, + copy: LiveRuntimeProjectorCopy = SOURCE_LIVE_RUNTIME_PROJECTOR_COPY, +) { return buildActivityEntry({ id: `stream-thinking:${sessionId}`, - title: "整理思路中", + title: copy.thinkingDraftTitle, detail: normalizeProcessDisplayText(draft), - statusLabel: "处理中", + statusLabel: copy.processingStatus, badgeClassName: IN_PROGRESS_BADGE_CLASS_NAME, sourceType: "reasoning", sourceLabel: "stream thinking", @@ -180,6 +398,7 @@ function buildThinkingDraftEntry(sessionId: string, draft?: string) { function buildRuntimeStatusEntry( sessionId: string, status: AgentRuntimeStatusPayload, + copy: LiveRuntimeProjectorCopy = SOURCE_LIVE_RUNTIME_PROJECTOR_COPY, ) { if (isRuntimeStatusDiagnosticsOnly(status)) { return null; @@ -194,9 +413,9 @@ function buildRuntimeStatusEntry( }); return buildActivityEntry({ id: `runtime-status:${sessionId}`, - title: status.title.trim() || "当前进展", + title: status.title.trim() || copy.currentProgressTitle, detail, - statusLabel: waiting ? "稍后开始" : "处理中", + statusLabel: waiting ? copy.queuedStatus : copy.processingStatus, badgeClassName: waiting ? QUEUED_BADGE_CLASS_NAME : IN_PROGRESS_BADGE_CLASS_NAME, @@ -250,22 +469,39 @@ function buildToolActivityEntry(params: { toolId: string; toolName?: string; result?: ToolExecutionResult; + copy?: LiveRuntimeProjectorCopy; }) { - const { sessionId, toolId, toolName, result } = params; + const { + sessionId, + toolId, + toolName, + result, + copy = SOURCE_LIVE_RUNTIME_PROJECTOR_COPY, + } = params; const displayToolName = toolName?.trim() ? resolveUserFacingToolDisplayLabel(toolName) : null; - const title = displayToolName ? `处理中 · ${displayToolName}` : "处理中"; + const title = displayToolName + ? copy.formatToolTitle(copy.processingStatus, displayToolName) + : copy.processingTitle; + const toolTarget = displayToolName || copy.toolTargetFallback; const detail = result - ? result.error || result.output || displayToolName || "当前步骤已完成。" - : `正在处理 ${displayToolName || "当前步骤"}。`; + ? result.error || + result.output || + displayToolName || + copy.toolCompletedFallback + : copy.formatToolProcessingDetail(toolTarget); const success = result ? result.success !== false : true; return buildActivityEntry({ id: `tool:${sessionId}:${toolId}`, title, detail, - statusLabel: result ? (success ? "完成" : "需重试") : "处理中", + statusLabel: result + ? success + ? copy.completedStatus + : copy.retryStatus + : copy.processingStatus, badgeClassName: result ? success ? COMPLETED_BADGE_CLASS_NAME @@ -299,30 +535,70 @@ function buildWarningActivityEntry( sessionId: string, message: string, code?: string, + copy: LiveRuntimeProjectorCopy = SOURCE_LIVE_RUNTIME_PROJECTOR_COPY, ) { return buildActivityEntry({ id: `warning:${sessionId}:${code ?? ""}:${message}`, - title: "警告", + title: copy.warningTitle, detail: message, - statusLabel: "警告", + statusLabel: copy.warningStatus, badgeClassName: QUEUED_BADGE_CLASS_NAME, sourceType: "warning", sourceLabel: "runtime warning", }); } -function buildErrorActivityEntry(sessionId: string, message: string) { +function buildErrorActivityEntry( + sessionId: string, + message: string, + copy: LiveRuntimeProjectorCopy = SOURCE_LIVE_RUNTIME_PROJECTOR_COPY, +) { return buildActivityEntry({ id: `error:${sessionId}:${message}`, - title: "错误", + title: copy.errorTitle, detail: message, - statusLabel: "需重试", + statusLabel: copy.retryStatus, badgeClassName: FAILED_BADGE_CLASS_NAME, sourceType: "error", sourceLabel: "runtime error", }); } +function buildStatusChangedActivityEntry(params: { + sessionId: string; + status: TeamWorkspaceRuntimeStatus | "not_found"; + copy?: LiveRuntimeProjectorCopy; +}): TeamWorkspaceActivityEntry { + const copy = params.copy ?? SOURCE_LIVE_RUNTIME_PROJECTOR_COPY; + const normalizedStatus = normalizeTeamWorkspaceRuntimeStatus(params.status); + const statusLabel = copy.getRuntimeStatusLabel(normalizedStatus); + return { + id: `status-${params.sessionId}-${normalizedStatus}-${Date.now()}`, + title: copy.statusChangedTitle, + detail: copy.formatStatusChangedDetail(statusLabel), + statusLabel, + badgeClassName: resolveStatusBadgeClassName(normalizedStatus), + sourceType: "runtime_status", + sourceLabel: "runtime status", + }; +} + +function resolveStatusBadgeClassName(status: TeamWorkspaceRuntimeStatus) { + if (status === "queued") { + return QUEUED_BADGE_CLASS_NAME; + } + if (status === "running") { + return IN_PROGRESS_BADGE_CLASS_NAME; + } + if (status === "completed" || status === "closed") { + return COMPLETED_BADGE_CLASS_NAME; + } + if (status === "failed" || status === "aborted") { + return FAILED_BADGE_CLASS_NAME; + } + return "border border-slate-200 bg-slate-50 text-slate-600"; +} + export function areActivityEntriesEqual( left: TeamWorkspaceActivityEntry, right: TeamWorkspaceActivityEntry, @@ -501,6 +777,7 @@ export function buildStatusChangedProjection(params: { status: TeamWorkspaceRuntimeStatus | "not_found"; session: TeamWorkspaceRuntimeSessionSnapshot; currentRuntime?: TeamWorkspaceLiveRuntimeState; + copy?: LiveRuntimeProjectorCopy; }): TeamWorkspaceStatusChangedProjection { const normalizedStatus = normalizeTeamWorkspaceRuntimeStatus(params.status); const nextQueuedTurnCount = @@ -514,7 +791,11 @@ export function buildStatusChangedProjection(params: { params.session.queuedTurnCount); return { - entry: buildStatusEventActivityEntry(params.sessionId, params.status), + entry: buildStatusChangedActivityEntry({ + sessionId: params.sessionId, + status: params.status, + copy: params.copy, + }), liveRuntimePatch: { runtimeStatus: normalizedStatus, latestTurnStatus: normalizedStatus, @@ -530,6 +811,7 @@ export function projectRuntimeStreamEvent(params: { currentRuntime?: TeamWorkspaceLiveRuntimeState; streamState?: SessionLiveStreamState; toolNameById?: Record; + copy?: LiveRuntimeProjectorCopy; }): TeamWorkspaceRuntimeStreamProjection | null { const { sessionId, @@ -538,6 +820,7 @@ export function projectRuntimeStreamEvent(params: { currentRuntime, streamState, toolNameById, + copy = SOURCE_LIVE_RUNTIME_PROJECTOR_COPY, } = params; switch (event.type) { @@ -581,7 +864,7 @@ export function projectRuntimeStreamEvent(params: { event.text, ); return { - entry: buildTextDraftEntry(sessionId, nextTextDraft), + entry: buildTextDraftEntry(sessionId, nextTextDraft, copy), nextTextDraft: nextTextDraft ?? undefined, }; } @@ -591,7 +874,7 @@ export function projectRuntimeStreamEvent(params: { event.text, ); return { - entry: buildThinkingDraftEntry(sessionId, nextThinkingDraft), + entry: buildThinkingDraftEntry(sessionId, nextThinkingDraft, copy), nextThinkingDraft: nextThinkingDraft ?? undefined, }; } @@ -601,6 +884,7 @@ export function projectRuntimeStreamEvent(params: { sessionId, toolId: event.tool_id, toolName: event.tool_name, + copy, }), rememberTool: { toolId: event.tool_id, @@ -614,6 +898,7 @@ export function projectRuntimeStreamEvent(params: { toolId: event.tool_id, toolName: toolNameById?.[event.tool_id], result: event.result, + copy, }), forgetToolId: event.tool_id, refreshPreview: true, @@ -631,7 +916,7 @@ export function projectRuntimeStreamEvent(params: { : undefined, }); return { - entry: buildRuntimeStatusEntry(sessionId, event.status), + entry: buildRuntimeStatusEntry(sessionId, event.status, copy), liveRuntimePatch, }; } @@ -640,9 +925,9 @@ export function projectRuntimeStreamEvent(params: { entry: buildLifecycleActivityEntry({ sessionId, key: "queue", - title: "稍后开始", - detail: "新的说明已经收到,这项子任务会在前一项完成后继续处理。", - statusLabel: resolveTeamWorkspaceDisplayRuntimeStatusLabel("queued"), + title: copy.lifecycle.queueAdded.title, + detail: copy.lifecycle.queueAdded.detail, + statusLabel: copy.queuedStatus, badgeClassName: QUEUED_BADGE_CLASS_NAME, }), liveRuntimePatch: { @@ -656,9 +941,9 @@ export function projectRuntimeStreamEvent(params: { entry: buildLifecycleActivityEntry({ sessionId, key: "queue", - title: "开始处理", - detail: "这项子任务已经开始处理当前任务。", - statusLabel: resolveTeamWorkspaceDisplayRuntimeStatusLabel("running"), + title: copy.lifecycle.queueStarted.title, + detail: copy.lifecycle.queueStarted.detail, + statusLabel: copy.processingStatus, badgeClassName: IN_PROGRESS_BADGE_CLASS_NAME, }), liveRuntimePatch: { @@ -702,9 +987,9 @@ export function projectRuntimeStreamEvent(params: { entry: buildLifecycleActivityEntry({ sessionId, key: "turn", - title: "继续处理", - detail: "这项子任务正在推进当前内容。", - statusLabel: resolveTeamWorkspaceDisplayRuntimeStatusLabel("running"), + title: copy.lifecycle.turnStarted.title, + detail: copy.lifecycle.turnStarted.detail, + statusLabel: copy.processingStatus, badgeClassName: IN_PROGRESS_BADGE_CLASS_NAME, }), liveRuntimePatch: { @@ -718,9 +1003,9 @@ export function projectRuntimeStreamEvent(params: { entry: buildLifecycleActivityEntry({ sessionId, key: "turn", - title: "阶段完成", - detail: "这一步已经完成,正在同步最新结果。", - statusLabel: "完成", + title: copy.lifecycle.turnCompleted.title, + detail: copy.lifecycle.turnCompleted.detail, + statusLabel: copy.completedStatus, badgeClassName: COMPLETED_BADGE_CLASS_NAME, }), liveRuntimePatch: { @@ -749,11 +1034,11 @@ export function projectRuntimeStreamEvent(params: { entry: buildLifecycleActivityEntry({ sessionId, key: "turn", - title: "处理失败", + title: copy.lifecycle.turnFailed.title, detail: event.turn.error_message?.trim() || - "这一步处理失败,请查看错误详情。", - statusLabel: "需重试", + copy.lifecycle.turnFailed.detail, + statusLabel: copy.retryStatus, badgeClassName: FAILED_BADGE_CLASS_NAME, }), liveRuntimePatch: { @@ -778,13 +1063,18 @@ export function projectRuntimeStreamEvent(params: { } case "warning": return { - entry: buildWarningActivityEntry(sessionId, event.message, event.code), + entry: buildWarningActivityEntry( + sessionId, + event.message, + event.code, + copy, + ), refreshPreview: true, }; case "error": { const queuedTurnCount = getQueuedTurnCount(session, currentRuntime); return { - entry: buildErrorActivityEntry(sessionId, event.message), + entry: buildErrorActivityEntry(sessionId, event.message, copy), liveRuntimePatch: { runtimeStatus: resolveFinalRuntimeStatus({ session, diff --git a/src/components/agent/chat/team-workspace-runtime/runtimeEventSubscriptions.ts b/src/components/agent/chat/team-workspace-runtime/runtimeEventSubscriptions.ts index 0e7437bd9..fd14c7b55 100644 --- a/src/components/agent/chat/team-workspace-runtime/runtimeEventSubscriptions.ts +++ b/src/components/agent/chat/team-workspace-runtime/runtimeEventSubscriptions.ts @@ -14,6 +14,7 @@ import { buildStatusChangedProjection, projectRuntimeStreamEvent, removeLiveActivityEntries, + type LiveRuntimeProjectorCopy, type SessionLiveStreamState, upsertLiveActivityEntries, } from "./liveRuntimeProjector"; @@ -40,6 +41,7 @@ export interface TeamWorkspaceStatusEventSubscriptionDeps { setLiveRuntimeBySessionId: TeamWorkspaceRecordUpdater; setLiveActivityBySessionId: TeamWorkspaceRecordUpdater; scheduleActivityRefresh(sessionId: string): void; + copy?: LiveRuntimeProjectorCopy; } export interface TeamWorkspaceStreamEventSubscriptionDeps { @@ -68,6 +70,7 @@ export interface TeamWorkspaceStreamEventSubscriptionDeps { setLiveRuntimeBySessionId: TeamWorkspaceRecordUpdater; setLiveActivityBySessionId: TeamWorkspaceRecordUpdater; scheduleActivityRefresh(sessionId: string): void; + copy?: LiveRuntimeProjectorCopy; } type SessionEventHandler = (event: { payload: unknown }) => void; @@ -160,6 +163,7 @@ export async function subscribeTeamWorkspaceStatusEvents( status: data.status, session: matchingSession, currentRuntime: deps.getCurrentRuntime(data.session_id), + copy: deps.copy, }); deps.setLiveRuntimeBySessionId((previous) => { @@ -229,6 +233,7 @@ export async function subscribeTeamWorkspaceStreamEvents( currentRuntime: deps.getCurrentRuntime(sessionId), streamState: deps.getStreamState(sessionId), toolNameById: deps.getToolNames(sessionId), + copy: deps.copy, }); if (!projection) { return; diff --git a/src/components/agent/chat/team-workspace-runtime/selectedSessionDetailSelectors.test.ts b/src/components/agent/chat/team-workspace-runtime/selectedSessionDetailSelectors.test.ts index da0b2aa75..68ab564a2 100644 --- a/src/components/agent/chat/team-workspace-runtime/selectedSessionDetailSelectors.test.ts +++ b/src/components/agent/chat/team-workspace-runtime/selectedSessionDetailSelectors.test.ts @@ -1,5 +1,29 @@ import { describe, expect, it } from "vitest"; -import { buildSelectedSessionDetailDisplayState } from "./selectedSessionDetailSelectors"; +import { changeLimeLocale, getLimeI18n } from "@/i18n/createI18n"; +import { + buildSelectedSessionDetailCopy, + buildSelectedSessionDetailDisplayState, + type SelectedSessionDetailTranslate, +} from "./selectedSessionDetailSelectors"; + +async function buildEnglishSelectedSessionDetailCopy() { + await changeLimeLocale("en-US"); + const translate: SelectedSessionDetailTranslate = (key, options) => + String( + getLimeI18n().t( + key as never, + { + ns: "agent", + ...(options ?? {}), + } as never, + ), + ); + + return buildSelectedSessionDetailCopy({ + locale: "en-US", + translate, + }); +} describe("selectedSessionDetailSelectors", () => { it("应稳定产出选中成员详情区的 metadata、设置与技能标签", () => { @@ -109,4 +133,72 @@ describe("selectedSessionDetailSelectors", () => { hasSettings: false, }); }); + + it("应支持注入英文详情 copy 且保留运行时业务数据", async () => { + const copy = await buildEnglishSelectedSessionDetailCopy(); + + const state = buildSelectedSessionDetailDisplayState({ + copy, + selectedSession: { + blueprintRoleLabel: "执行", + createdFromTurnId: "turn-42", + latestTurnStatus: "running", + model: "gpt-5.4", + originTool: "spawn_agent", + outputContract: "Keep runtime contract.", + profileName: "代码执行员", + providerConcurrencyGroup: "openai", + providerName: "OpenAI", + providerParallelBudget: 1, + queueReason: "Runtime queue reason.", + queuedTurnCount: 1, + roleKey: "executor", + sessionType: "sub_agent", + skills: [ + { + id: "bounded-implementation", + name: "边界实现", + description: "只改明确授权的边界。", + }, + ], + teamActiveCount: 1, + teamParallelBudget: 3, + teamPresetId: "code-triage-team", + theme: "engineering", + }, + isChildSession: true, + parentSessionName: "主线程总览", + }); + + expect(state.runtimeDetailSummary).toBe( + "Waiting 1 · Latest progress Running · Running 1/3 · Steady mode", + ); + expect(state.metadata).toEqual([ + "Role 执行", + "Subtask", + "Provider OpenAI", + "Model gpt-5.4", + "Source spawn_agent", + "From earlier task turn-42", + "Waiting 1", + "Running 1/3", + "Steady mode", + "Latest progress Running", + "From 主线程总览", + ]); + expect(state.settingBadges).toEqual([ + "Plan Code triage team", + "Style 代码执行员", + "Role Execution", + "Theme engineering", + ]); + expect(state.outputContract).toBe("Keep runtime contract."); + expect(state.skillBadges).toEqual([ + { + id: "bounded-implementation", + label: "边界实现", + title: "只改明确授权的边界。", + }, + ]); + }); }); diff --git a/src/components/agent/chat/team-workspace-runtime/selectedSessionDetailSelectors.ts b/src/components/agent/chat/team-workspace-runtime/selectedSessionDetailSelectors.ts index 5d29e72c1..5f1a201bd 100644 --- a/src/components/agent/chat/team-workspace-runtime/selectedSessionDetailSelectors.ts +++ b/src/components/agent/chat/team-workspace-runtime/selectedSessionDetailSelectors.ts @@ -1,14 +1,260 @@ import type { AsterSubagentSkillInfo } from "@/lib/api/agentRuntime"; -import { - buildTeamWorkspaceSkillDisplayName, - resolveTeamWorkspaceDisplayRuntimeStatusLabel, - resolveTeamWorkspaceDisplaySessionTypeLabel, - resolveTeamWorkspaceRoleHintLabel, - resolveTeamWorkspaceStableProcessingLabel, -} from "../utils/teamWorkspaceCopy"; -import { getTeamPresetOption } from "../utils/teamPresets"; +import { formatNumber } from "@/i18n/format"; +import agentSourceResource from "@/i18n/resources/zh-CN/agent.json"; +import { buildTeamWorkspaceSkillDisplayName } from "../utils/teamWorkspaceCopy"; import type { TeamWorkspaceRuntimeStatus } from "../teamWorkspaceRuntime"; -import { buildRuntimeDetailSummary } from "./canvasLaneSelectors"; +import { + buildRuntimeDetailSummary, + type TeamWorkspaceRuntimeDetailCopy, +} from "./canvasLaneSelectors"; + +type SelectedSessionDetailResourceKey = + | "agentChat.teamWorkspace.canvasLane.runtimeDetail.processingCount" + | "agentChat.teamWorkspace.canvasLane.runtimeDetail.recentProgress" + | "agentChat.teamWorkspace.canvasLane.runtimeDetail.stableProcessing" + | "agentChat.teamWorkspace.canvasLane.runtimeDetail.waitingCount" + | "agentChat.teamWorkspace.runtimeStatus.aborted" + | "agentChat.teamWorkspace.runtimeStatus.closed" + | "agentChat.teamWorkspace.runtimeStatus.completed" + | "agentChat.teamWorkspace.runtimeStatus.failed" + | "agentChat.teamWorkspace.runtimeStatus.idle" + | "agentChat.teamWorkspace.runtimeStatus.queued" + | "agentChat.teamWorkspace.runtimeStatus.running" + | "agentChat.teamWorkspace.selectedSession.detail.metadata.createdFromTurn" + | "agentChat.teamWorkspace.selectedSession.detail.metadata.model" + | "agentChat.teamWorkspace.selectedSession.detail.metadata.originTool" + | "agentChat.teamWorkspace.selectedSession.detail.metadata.parentSession" + | "agentChat.teamWorkspace.selectedSession.detail.metadata.provider" + | "agentChat.teamWorkspace.selectedSession.detail.metadata.role" + | "agentChat.teamWorkspace.selectedSession.detail.roleHint.design" + | "agentChat.teamWorkspace.selectedSession.detail.roleHint.designer" + | "agentChat.teamWorkspace.selectedSession.detail.roleHint.editor" + | "agentChat.teamWorkspace.selectedSession.detail.roleHint.executor" + | "agentChat.teamWorkspace.selectedSession.detail.roleHint.explorer" + | "agentChat.teamWorkspace.selectedSession.detail.roleHint.orchestrator" + | "agentChat.teamWorkspace.selectedSession.detail.roleHint.planner" + | "agentChat.teamWorkspace.selectedSession.detail.roleHint.researcher" + | "agentChat.teamWorkspace.selectedSession.detail.roleHint.reviewer" + | "agentChat.teamWorkspace.selectedSession.detail.roleHint.writer" + | "agentChat.teamWorkspace.selectedSession.detail.sessionType.fork" + | "agentChat.teamWorkspace.selectedSession.detail.sessionType.subAgent" + | "agentChat.teamWorkspace.selectedSession.detail.sessionType.user" + | "agentChat.teamWorkspace.selectedSession.detail.setting.preset" + | "agentChat.teamWorkspace.selectedSession.detail.setting.profile" + | "agentChat.teamWorkspace.selectedSession.detail.setting.role" + | "agentChat.teamWorkspace.selectedSession.detail.setting.theme" + | "agentChat.teamWorkspace.teamPreset.codeTriageTeam" + | "agentChat.teamWorkspace.teamPreset.contentCreationTeam" + | "agentChat.teamWorkspace.teamPreset.researchTeam"; + +export type SelectedSessionDetailTranslate = ( + key: SelectedSessionDetailResourceKey, + options?: Record, +) => string; + +type AgentSourceResourceKey = keyof typeof agentSourceResource; + +const TEAM_PRESET_LABEL_KEYS = { + "code-triage-team": "agentChat.teamWorkspace.teamPreset.codeTriageTeam", + "research-team": "agentChat.teamWorkspace.teamPreset.researchTeam", + "content-creation-team": + "agentChat.teamWorkspace.teamPreset.contentCreationTeam", +} as const satisfies Record; + +const ROLE_HINT_LABEL_KEYS = { + explorer: "agentChat.teamWorkspace.selectedSession.detail.roleHint.explorer", + executor: "agentChat.teamWorkspace.selectedSession.detail.roleHint.executor", + reviewer: "agentChat.teamWorkspace.selectedSession.detail.roleHint.reviewer", + writer: "agentChat.teamWorkspace.selectedSession.detail.roleHint.writer", + planner: "agentChat.teamWorkspace.selectedSession.detail.roleHint.planner", + researcher: + "agentChat.teamWorkspace.selectedSession.detail.roleHint.researcher", + designer: "agentChat.teamWorkspace.selectedSession.detail.roleHint.designer", + design: "agentChat.teamWorkspace.selectedSession.detail.roleHint.design", + editor: "agentChat.teamWorkspace.selectedSession.detail.roleHint.editor", + orchestrator: + "agentChat.teamWorkspace.selectedSession.detail.roleHint.orchestrator", +} as const satisfies Record; + +function interpolateSelectedSessionDetailSourceTemplate( + template: string, + values?: Record, +): string { + return template.replace(/\{\{\s*(\w+)\s*\}\}/g, (match, name) => { + const value = values?.[name]; + return value == null ? match : String(value); + }); +} + +function translateSelectedSessionDetailSourceKey( + key: SelectedSessionDetailResourceKey, + values?: Record, +): string { + const template = agentSourceResource[key as AgentSourceResourceKey] ?? key; + return interpolateSelectedSessionDetailSourceTemplate(template, values); +} + +function resolveRuntimeStatusResourceKey( + status?: TeamWorkspaceRuntimeStatus | "idle", +): SelectedSessionDetailResourceKey { + return `agentChat.teamWorkspace.runtimeStatus.${status ?? "idle"}` as SelectedSessionDetailResourceKey; +} + +function resolveSessionTypeResourceKey( + value: "fork" | "sub_agent" | "user", +): SelectedSessionDetailResourceKey { + switch (value) { + case "fork": + return "agentChat.teamWorkspace.selectedSession.detail.sessionType.fork"; + case "sub_agent": + return "agentChat.teamWorkspace.selectedSession.detail.sessionType.subAgent"; + case "user": + return "agentChat.teamWorkspace.selectedSession.detail.sessionType.user"; + } +} + +export interface SelectedSessionDetailCopy extends TeamWorkspaceRuntimeDetailCopy { + formatCreatedFromTurn: (turnId: string) => string; + formatMetadataModel: (model: string) => string; + formatMetadataOriginTool: (tool: string) => string; + formatMetadataParentSession: (name: string) => string; + formatMetadataProvider: (name: string) => string; + formatMetadataRole: (label: string) => string; + formatSettingPreset: (label: string) => string; + formatSettingProfile: (name: string) => string; + formatSettingRole: (label: string) => string; + formatSettingTheme: (theme: string) => string; + getPresetLabel: (presetId: string) => string; + getRoleHintLabel: (roleHint?: string | null) => string | null; + getSessionTypeLabel: (sessionType?: string | null) => string | null; +} + +export function buildSelectedSessionDetailCopy(params: { + locale?: string | null; + translate: SelectedSessionDetailTranslate; +}): SelectedSessionDetailCopy { + const formatCount = (count: number) => + formatNumber(count, { locale: params.locale }); + + return { + formatCreatedFromTurn: (turnId) => + params.translate( + "agentChat.teamWorkspace.selectedSession.detail.metadata.createdFromTurn", + { turnId }, + ), + formatMetadataModel: (model) => + params.translate( + "agentChat.teamWorkspace.selectedSession.detail.metadata.model", + { model }, + ), + formatMetadataOriginTool: (tool) => + params.translate( + "agentChat.teamWorkspace.selectedSession.detail.metadata.originTool", + { tool }, + ), + formatMetadataParentSession: (name) => + params.translate( + "agentChat.teamWorkspace.selectedSession.detail.metadata.parentSession", + { name }, + ), + formatMetadataProvider: (name) => + params.translate( + "agentChat.teamWorkspace.selectedSession.detail.metadata.provider", + { name }, + ), + formatMetadataRole: (label) => + params.translate( + "agentChat.teamWorkspace.selectedSession.detail.metadata.role", + { label }, + ), + formatProcessingCount: (activeCount, totalCount) => + params.translate( + "agentChat.teamWorkspace.canvasLane.runtimeDetail.processingCount", + { + activeCount: formatCount(activeCount), + totalCount: formatCount(totalCount), + }, + ), + formatRecentProgress: (status) => + params.translate( + "agentChat.teamWorkspace.canvasLane.runtimeDetail.recentProgress", + { status }, + ), + formatSettingPreset: (label) => + params.translate( + "agentChat.teamWorkspace.selectedSession.detail.setting.preset", + { label }, + ), + formatSettingProfile: (name) => + params.translate( + "agentChat.teamWorkspace.selectedSession.detail.setting.profile", + { name }, + ), + formatSettingRole: (label) => + params.translate( + "agentChat.teamWorkspace.selectedSession.detail.setting.role", + { label }, + ), + formatSettingTheme: (theme) => + params.translate( + "agentChat.teamWorkspace.selectedSession.detail.setting.theme", + { theme }, + ), + formatWaitingCount: (count) => + params.translate( + "agentChat.teamWorkspace.canvasLane.runtimeDetail.waitingCount", + { formattedCount: formatCount(count) }, + ), + getPresetLabel: (presetId) => { + const normalized = presetId.trim(); + const key = + TEAM_PRESET_LABEL_KEYS[ + normalized as keyof typeof TEAM_PRESET_LABEL_KEYS + ]; + return key ? params.translate(key) : normalized; + }, + getRoleHintLabel: (roleHint) => { + const normalized = roleHint?.trim(); + if (!normalized) { + return null; + } + + const key = + ROLE_HINT_LABEL_KEYS[ + normalized.toLowerCase() as keyof typeof ROLE_HINT_LABEL_KEYS + ]; + if (key) { + return params.translate(key); + } + + return /[\u4e00-\u9fff]/.test(normalized) ? normalized : null; + }, + getRuntimeStatusLabel: (status) => + params.translate(resolveRuntimeStatusResourceKey(status)), + getSessionTypeLabel: (sessionType) => { + const normalized = sessionType?.trim(); + if (!normalized) { + return null; + } + if ( + normalized === "fork" || + normalized === "sub_agent" || + normalized === "user" + ) { + return params.translate(resolveSessionTypeResourceKey(normalized)); + } + return normalized; + }, + stableProcessingLabel: params.translate( + "agentChat.teamWorkspace.canvasLane.runtimeDetail.stableProcessing", + ), + }; +} + +const SOURCE_SELECTED_SESSION_DETAIL_COPY = buildSelectedSessionDetailCopy({ + locale: "zh-CN", + translate: translateSelectedSessionDetailSourceKey, +}); export interface SelectedSessionDetailSession { blueprintRoleLabel?: string; @@ -53,14 +299,15 @@ export function buildSelectedSessionDetailDisplayState(params: { selectedSession?: SelectedSessionDetailSession | null; isChildSession: boolean; parentSessionName?: string | null; + copy?: SelectedSessionDetailCopy; }): SelectedSessionDetailDisplayState { const selectedSession = params.selectedSession ?? null; + const copy = params.copy ?? SOURCE_SELECTED_SESSION_DETAIL_COPY; const queuedCount = selectedSession?.teamQueuedCount ?? selectedSession?.queuedTurnCount ?? 0; - const roleLabel = resolveTeamWorkspaceRoleHintLabel(selectedSession?.roleKey); + const roleLabel = copy.getRoleHintLabel(selectedSession?.roleKey); const presetLabel = selectedSession?.teamPresetId - ? (getTeamPresetOption(selectedSession.teamPresetId)?.label ?? - selectedSession.teamPresetId) + ? copy.getPresetLabel(selectedSession.teamPresetId) : null; const skillBadges = (selectedSession?.skills ?? []).map((skill) => ({ id: skill.id, @@ -68,45 +315,58 @@ export function buildSelectedSessionDetailDisplayState(params: { title: skill.description || skill.directory || undefined, })); const settingBadges = [ - presetLabel ? `预设 ${presetLabel}` : null, - selectedSession?.profileName ? `风格 ${selectedSession.profileName}` : null, - roleLabel ? `分工 ${roleLabel}` : null, - selectedSession?.theme ? `主题 ${selectedSession.theme}` : null, + presetLabel ? copy.formatSettingPreset(presetLabel) : null, + selectedSession?.profileName + ? copy.formatSettingProfile(selectedSession.profileName) + : null, + roleLabel ? copy.formatSettingRole(roleLabel) : null, + selectedSession?.theme + ? copy.formatSettingTheme(selectedSession.theme) + : null, ].filter(Boolean) as string[]; const metadata = [ selectedSession?.blueprintRoleLabel - ? `分工 ${selectedSession.blueprintRoleLabel}` + ? copy.formatMetadataRole(selectedSession.blueprintRoleLabel) : null, selectedSession?.sessionType - ? resolveTeamWorkspaceDisplaySessionTypeLabel(selectedSession.sessionType) + ? copy.getSessionTypeLabel(selectedSession.sessionType) : null, selectedSession?.providerName - ? `服务 ${selectedSession.providerName}` + ? copy.formatMetadataProvider(selectedSession.providerName) + : null, + selectedSession?.model + ? copy.formatMetadataModel(selectedSession.model) + : null, + selectedSession?.originTool + ? copy.formatMetadataOriginTool(selectedSession.originTool) : null, - selectedSession?.model ? `模型 ${selectedSession.model}` : null, - selectedSession?.originTool ? `来源 ${selectedSession.originTool}` : null, selectedSession?.createdFromTurnId - ? `来自之前的任务 ${selectedSession.createdFromTurnId}` + ? copy.formatCreatedFromTurn(selectedSession.createdFromTurnId) : null, - queuedCount > 0 ? `等待中 ${queuedCount}` : null, + queuedCount > 0 ? copy.formatWaitingCount(queuedCount) : null, selectedSession?.teamActiveCount !== undefined && selectedSession?.teamParallelBudget !== undefined - ? `处理中 ${selectedSession.teamActiveCount}/${selectedSession.teamParallelBudget}` + ? copy.formatProcessingCount( + selectedSession.teamActiveCount, + selectedSession.teamParallelBudget, + ) : null, selectedSession?.providerParallelBudget === 1 && selectedSession?.providerConcurrencyGroup - ? resolveTeamWorkspaceStableProcessingLabel() + ? copy.stableProcessingLabel : null, selectedSession?.latestTurnStatus - ? `最近进展 ${resolveTeamWorkspaceDisplayRuntimeStatusLabel(selectedSession.latestTurnStatus)}` + ? copy.formatRecentProgress( + copy.getRuntimeStatusLabel(selectedSession.latestTurnStatus), + ) : null, params.isChildSession && params.parentSessionName?.trim() - ? `来自 ${params.parentSessionName.trim()}` + ? copy.formatMetadataParentSession(params.parentSessionName.trim()) : null, ].filter(Boolean) as string[]; return { - runtimeDetailSummary: buildRuntimeDetailSummary(selectedSession), + runtimeDetailSummary: buildRuntimeDetailSummary(selectedSession, copy), queueReason: selectedSession?.queueReason?.trim() || null, metadata, settingBadges, diff --git a/src/components/agent/chat/team-workspace-runtime/teamOperationSelectors.test.ts b/src/components/agent/chat/team-workspace-runtime/teamOperationSelectors.test.ts index a6448dd91..e3c466ed9 100644 --- a/src/components/agent/chat/team-workspace-runtime/teamOperationSelectors.test.ts +++ b/src/components/agent/chat/team-workspace-runtime/teamOperationSelectors.test.ts @@ -1,13 +1,38 @@ -import { describe, expect, it } from "vitest"; +import { beforeEach, describe, expect, it } from "vitest"; +import { changeLimeLocale, getLimeI18n } from "@/i18n/createI18n"; import { + buildTeamWorkspaceOperationCopy, buildTeamOperationDisplayEntries, buildVisibleTeamOperationState, formatOperationUpdatedAt, + type TeamWorkspaceOperationCopy, + type TeamWorkspaceOperationTranslate, } from "./teamOperationSelectors"; describe("teamOperationSelectors", () => { + let operationCopy: TeamWorkspaceOperationCopy; + + beforeEach(async () => { + await changeLimeLocale("en-US"); + const translate: TeamWorkspaceOperationTranslate = (key, options) => + String( + getLimeI18n().t( + key as never, + { + ns: "agent", + ...(options ?? {}), + } as never, + ), + ); + operationCopy = buildTeamWorkspaceOperationCopy({ + locale: "en-US", + translate, + }); + }); + it("wait 命中结果时应生成聚合结果条目", () => { const entries = buildTeamOperationDisplayEntries({ + copy: operationCopy, sessionNameById: new Map([ ["child-1", "研究员"], ["child-2", "执行器"], @@ -24,8 +49,9 @@ describe("teamOperationSelectors", () => { expect(entries).toEqual([ { id: "wait-1710000100000", - title: "收到结果", - detail: "刚才等到 执行器 返回了新结果,当前状态为已完成。", + title: "Result received", + detail: + "Just received new results from 执行器; current status is Completed.", badgeClassName: "border border-emerald-200 bg-emerald-50 text-emerald-700", updatedAt: 1_710_000_100_000, @@ -36,6 +62,7 @@ describe("teamOperationSelectors", () => { it("control 汇总应按 action 生成稳定标题与文案", () => { const entries = buildTeamOperationDisplayEntries({ + copy: operationCopy, sessionNameById: new Map([["child-1", "研究员"]]), teamControlSummary: { action: "close", @@ -49,8 +76,8 @@ describe("teamOperationSelectors", () => { expect(entries).toEqual([ { id: "control-close-1710000200000", - title: "暂停处理", - detail: "刚才已暂停 研究员 的处理。", + title: "Pause work", + detail: "Just paused 研究员.", badgeClassName: "border border-slate-200 bg-slate-100 text-slate-700", updatedAt: 1_710_000_200_000, targetSessionId: "child-1", @@ -60,6 +87,7 @@ describe("teamOperationSelectors", () => { it("应按更新时间倒序排序,并提供操作时间文案格式化", () => { const entries = buildTeamOperationDisplayEntries({ + copy: operationCopy, sessionNameById: new Map([["child-1", "研究员"]]), teamWaitSummary: { awaitedSessionIds: ["child-1"], @@ -79,15 +107,28 @@ describe("teamOperationSelectors", () => { "control-resume-1710000200000", "wait-1710000100000", ]); - expect(entries[0]?.detail).toBe("刚才已继续 研究员 的处理。"); + expect(entries[0]?.detail).toBe("Just resumed 研究员."); expect(entries[1]?.detail).toBe( - "刚才等待结果时超时了,还有 1 项任务仍在处理中。", + "The wait for results just timed out; 1 task(s) are still running.", ); - expect(formatOperationUpdatedAt()).toBe("刚刚"); + expect( + formatOperationUpdatedAt(undefined, { + locale: "en-US", + nowLabel: "just now", + }), + ).toBe("just now"); + expect( + formatOperationUpdatedAt(1_710_000_100_000, { + locale: "en-US", + now: 1_710_000_100_030, + nowLabel: "just now", + }), + ).toBe("just now"); }); it("应只保留当前画布可见的 team operation 条目", () => { const state = buildVisibleTeamOperationState({ + copy: operationCopy, railSessions: [{ id: "child-1", name: "研究员" }], teamWaitSummary: { awaitedSessionIds: ["child-1"], @@ -112,8 +153,8 @@ describe("teamOperationSelectors", () => { expect(state.entries).toEqual([ { id: "control-close-1710000200000", - title: "暂停处理", - detail: "刚才已暂停 研究员 的处理。", + title: "Pause work", + detail: "Just paused 研究员.", badgeClassName: "border border-slate-200 bg-slate-100 text-slate-700", updatedAt: 1_710_000_200_000, targetSessionId: "child-1", @@ -123,6 +164,7 @@ describe("teamOperationSelectors", () => { it("未命中当前画布 session 时应隐藏 team operation 摘要", () => { const state = buildVisibleTeamOperationState({ + copy: operationCopy, railSessions: [{ id: "child-1", name: "研究员" }], teamWaitSummary: { awaitedSessionIds: ["child-2"], diff --git a/src/components/agent/chat/team-workspace-runtime/teamOperationSelectors.ts b/src/components/agent/chat/team-workspace-runtime/teamOperationSelectors.ts index 938595fef..9e507dd6a 100644 --- a/src/components/agent/chat/team-workspace-runtime/teamOperationSelectors.ts +++ b/src/components/agent/chat/team-workspace-runtime/teamOperationSelectors.ts @@ -1,10 +1,14 @@ -import { formatRelativeTime } from "@/lib/api/project"; +import { + formatDate, + formatNumber, + formatRelativeTime as formatLocaleRelativeTime, +} from "@/i18n/format"; import { normalizeTeamWorkspaceRuntimeStatus, + type TeamWorkspaceResolvedRuntimeStatus, type TeamWorkspaceControlSummary, type TeamWorkspaceWaitSummary, } from "../teamWorkspaceRuntime"; -import { resolveTeamWorkspaceRuntimeStatusLabel } from "../teamWorkspaceRuntime"; export interface TeamOperationDisplayEntry { id: string; @@ -26,6 +30,50 @@ export interface TeamWorkspaceVisibleOperationState { entries: TeamOperationDisplayEntry[]; } +type TeamWorkspaceOperationResourceKey = + | "agentChat.teamWorkspace.operations.control.closeCount" + | "agentChat.teamWorkspace.operations.control.closeCompleted" + | "agentChat.teamWorkspace.operations.control.closeOne" + | "agentChat.teamWorkspace.operations.control.resumeCount" + | "agentChat.teamWorkspace.operations.control.resumeOne" + | "agentChat.teamWorkspace.operations.taskFallback" + | "agentChat.teamWorkspace.operations.title.close" + | "agentChat.teamWorkspace.operations.title.closeCompleted" + | "agentChat.teamWorkspace.operations.title.resume" + | "agentChat.teamWorkspace.operations.title.waitResolved" + | "agentChat.teamWorkspace.operations.title.waitTimedOut" + | "agentChat.teamWorkspace.operations.wait.resolved" + | "agentChat.teamWorkspace.operations.wait.timedOut" + | "agentChat.teamWorkspace.runtimeStatus.aborted" + | "agentChat.teamWorkspace.runtimeStatus.closed" + | "agentChat.teamWorkspace.runtimeStatus.completed" + | "agentChat.teamWorkspace.runtimeStatus.failed" + | "agentChat.teamWorkspace.runtimeStatus.idle" + | "agentChat.teamWorkspace.runtimeStatus.queued" + | "agentChat.teamWorkspace.runtimeStatus.running"; + +export type TeamWorkspaceOperationTranslate = ( + key: TeamWorkspaceOperationResourceKey, + options?: Record, +) => string; + +export interface TeamWorkspaceOperationCopy { + closeTitle: string; + closeCompletedTitle: string; + formatCloseCount: (count: number) => string; + formatCloseCompleted: (count: number) => string; + formatCloseOne: (sessionName: string) => string; + formatResumeCount: (count: number) => string; + formatResumeOne: (sessionName: string) => string; + formatRuntimeStatus: (status?: TeamWorkspaceResolvedRuntimeStatus) => string; + formatWaitResolved: (sessionName: string, status: string) => string; + formatWaitTimedOut: (count: number) => string; + resumeTitle: string; + taskFallback: string; + waitResolvedTitle: string; + waitTimedOutTitle: string; +} + const STATUS_BADGE_CLASS_NAME = { idle: "border border-slate-200 bg-white text-slate-600", queued: "border border-amber-200 bg-amber-50 text-amber-700", @@ -42,20 +90,164 @@ function resolveStatusBadgeClassName( return STATUS_BADGE_CLASS_NAME[status ?? "idle"]; } -export function formatOperationUpdatedAt(updatedAt?: number) { - if (!updatedAt) { - return "刚刚"; +function resolveRuntimeStatusResourceKey( + status?: TeamWorkspaceResolvedRuntimeStatus, +): TeamWorkspaceOperationResourceKey { + const normalized = status + ? normalizeTeamWorkspaceRuntimeStatus(status) + : "idle"; + + switch (normalized) { + case "queued": + return "agentChat.teamWorkspace.runtimeStatus.queued"; + case "running": + return "agentChat.teamWorkspace.runtimeStatus.running"; + case "completed": + return "agentChat.teamWorkspace.runtimeStatus.completed"; + case "failed": + return "agentChat.teamWorkspace.runtimeStatus.failed"; + case "aborted": + return "agentChat.teamWorkspace.runtimeStatus.aborted"; + case "closed": + return "agentChat.teamWorkspace.runtimeStatus.closed"; + case "idle": + default: + return "agentChat.teamWorkspace.runtimeStatus.idle"; } - return formatRelativeTime(updatedAt); +} + +export function buildTeamWorkspaceOperationCopy(params: { + locale?: string | null; + translate: TeamWorkspaceOperationTranslate; +}): TeamWorkspaceOperationCopy { + const formatTaskCount = (count: number) => + formatNumber(count, { locale: params.locale }); + + return { + closeTitle: params.translate( + "agentChat.teamWorkspace.operations.title.close", + ), + closeCompletedTitle: params.translate( + "agentChat.teamWorkspace.operations.title.closeCompleted", + ), + formatCloseCount: (count) => + params.translate( + "agentChat.teamWorkspace.operations.control.closeCount", + { + formattedCount: formatTaskCount(count), + }, + ), + formatCloseCompleted: (count) => + params.translate( + "agentChat.teamWorkspace.operations.control.closeCompleted", + { + formattedCount: formatTaskCount(count), + }, + ), + formatCloseOne: (sessionName) => + params.translate("agentChat.teamWorkspace.operations.control.closeOne", { + sessionName, + }), + formatResumeCount: (count) => + params.translate( + "agentChat.teamWorkspace.operations.control.resumeCount", + { + formattedCount: formatTaskCount(count), + }, + ), + formatResumeOne: (sessionName) => + params.translate("agentChat.teamWorkspace.operations.control.resumeOne", { + sessionName, + }), + formatRuntimeStatus: (status) => + params.translate(resolveRuntimeStatusResourceKey(status)), + formatWaitResolved: (sessionName, status) => + params.translate("agentChat.teamWorkspace.operations.wait.resolved", { + sessionName, + status, + }), + formatWaitTimedOut: (count) => + params.translate("agentChat.teamWorkspace.operations.wait.timedOut", { + formattedCount: formatTaskCount(count), + }), + resumeTitle: params.translate( + "agentChat.teamWorkspace.operations.title.resume", + ), + taskFallback: params.translate( + "agentChat.teamWorkspace.operations.taskFallback", + ), + waitResolvedTitle: params.translate( + "agentChat.teamWorkspace.operations.title.waitResolved", + ), + waitTimedOutTitle: params.translate( + "agentChat.teamWorkspace.operations.title.waitTimedOut", + ), + }; +} + +const SECOND_MS = 1000; +const MINUTE_MS = 60 * SECOND_MS; +const HOUR_MS = 60 * MINUTE_MS; +const DAY_MS = 24 * HOUR_MS; +const MONTH_MS = 30 * DAY_MS; + +export function formatOperationUpdatedAt( + updatedAt: number | undefined, + options: { + locale?: string | null; + now?: number; + nowLabel: string; + }, +) { + if (!updatedAt) { + return options.nowLabel; + } + + const diffMs = updatedAt - (options.now ?? Date.now()); + const absDiffMs = Math.abs(diffMs); + + if (absDiffMs < MINUTE_MS) { + return options.nowLabel; + } + if (absDiffMs < HOUR_MS) { + return formatLocaleRelativeTime(Math.round(diffMs / MINUTE_MS), "minute", { + locale: options.locale, + numeric: "auto", + style: "narrow", + }); + } + if (absDiffMs < DAY_MS) { + return formatLocaleRelativeTime(Math.round(diffMs / HOUR_MS), "hour", { + locale: options.locale, + numeric: "auto", + style: "narrow", + }); + } + if (absDiffMs < MONTH_MS) { + return formatLocaleRelativeTime(Math.round(diffMs / DAY_MS), "day", { + locale: options.locale, + numeric: "auto", + style: "narrow", + }); + } + + return formatDate(updatedAt, { + locale: options.locale, + month: "numeric", + day: "numeric", + }); } function buildTeamWaitSummaryDisplay(params: { + copy: TeamWorkspaceOperationCopy; summary: TeamWorkspaceWaitSummary; sessionNameById: Map; }) { if (params.summary.timedOut) { return { - text: `刚才等待结果时超时了,还有 ${params.summary.awaitedSessionIds.length} 项任务仍在处理中。`, + text: params.copy.formatWaitTimedOut( + params.summary.awaitedSessionIds.length, + ), badgeClassName: "border border-amber-200 bg-amber-50 text-amber-700", }; } @@ -63,18 +255,22 @@ function buildTeamWaitSummaryDisplay(params: { const resolvedName = params.summary.resolvedSessionId ? (params.sessionNameById.get(params.summary.resolvedSessionId) ?? params.summary.resolvedSessionId) - : "这项任务"; + : params.copy.taskFallback; const normalizedStatus = params.summary.resolvedStatus ? normalizeTeamWorkspaceRuntimeStatus(params.summary.resolvedStatus) : undefined; return { - text: `刚才等到 ${resolvedName} 返回了新结果,当前状态为${resolveTeamWorkspaceRuntimeStatusLabel(params.summary.resolvedStatus)}。`, + text: params.copy.formatWaitResolved( + resolvedName, + params.copy.formatRuntimeStatus(params.summary.resolvedStatus), + ), badgeClassName: resolveStatusBadgeClassName(normalizedStatus), }; } function buildTeamControlSummaryDisplay(params: { + copy: TeamWorkspaceOperationCopy; summary: TeamWorkspaceControlSummary; sessionNameById: Map; }) { @@ -82,20 +278,20 @@ function buildTeamControlSummaryDisplay(params: { const firstAffectedId = params.summary.affectedSessionIds[0]; const firstAffectedName = firstAffectedId ? (params.sessionNameById.get(firstAffectedId) ?? firstAffectedId) - : "这项任务"; + : params.copy.taskFallback; switch (params.summary.action) { case "resume": return { text: affectedCount > 1 - ? `刚才已继续 ${affectedCount} 项任务的处理。` - : `刚才已继续 ${firstAffectedName} 的处理。`, + ? params.copy.formatResumeCount(affectedCount) + : params.copy.formatResumeOne(firstAffectedName), badgeClassName: "border border-sky-200 bg-sky-50 text-sky-700", }; case "close_completed": return { - text: `刚才已收起 ${affectedCount} 项已完成任务。`, + text: params.copy.formatCloseCompleted(affectedCount), badgeClassName: "border border-slate-200 bg-slate-100 text-slate-700", }; case "close": @@ -103,14 +299,15 @@ function buildTeamControlSummaryDisplay(params: { return { text: affectedCount > 1 - ? `刚才已暂停 ${affectedCount} 项任务的处理。` - : `刚才已暂停 ${firstAffectedName} 的处理。`, + ? params.copy.formatCloseCount(affectedCount) + : params.copy.formatCloseOne(firstAffectedName), badgeClassName: "border border-slate-200 bg-slate-100 text-slate-700", }; } } export function buildTeamOperationDisplayEntries(params: { + copy: TeamWorkspaceOperationCopy; sessionNameById: Map; teamWaitSummary?: TeamWorkspaceWaitSummary | null; teamControlSummary?: TeamWorkspaceControlSummary | null; @@ -119,12 +316,15 @@ export function buildTeamOperationDisplayEntries(params: { if (params.teamWaitSummary) { const display = buildTeamWaitSummaryDisplay({ + copy: params.copy, summary: params.teamWaitSummary, sessionNameById: params.sessionNameById, }); entries.push({ id: `wait-${params.teamWaitSummary.updatedAt}`, - title: params.teamWaitSummary.timedOut ? "等待超时" : "收到结果", + title: params.teamWaitSummary.timedOut + ? params.copy.waitTimedOutTitle + : params.copy.waitResolvedTitle, detail: display.text, badgeClassName: display.badgeClassName, updatedAt: params.teamWaitSummary.updatedAt, @@ -136,18 +336,19 @@ export function buildTeamOperationDisplayEntries(params: { if (params.teamControlSummary) { const display = buildTeamControlSummaryDisplay({ + copy: params.copy, summary: params.teamControlSummary, sessionNameById: params.sessionNameById, }); const title = (() => { switch (params.teamControlSummary.action) { case "resume": - return "继续处理"; + return params.copy.resumeTitle; case "close_completed": - return "收起完成项"; + return params.copy.closeCompletedTitle; case "close": default: - return "暂停处理"; + return params.copy.closeTitle; } })(); @@ -167,6 +368,7 @@ export function buildTeamOperationDisplayEntries(params: { } export function buildVisibleTeamOperationState(params: { + copy: TeamWorkspaceOperationCopy; railSessions: TeamWorkspaceOperationSessionSnapshot[]; teamWaitSummary?: TeamWorkspaceWaitSummary | null; teamControlSummary?: TeamWorkspaceControlSummary | null; @@ -199,6 +401,7 @@ export function buildVisibleTeamOperationState(params: { visibleTeamWaitSummary, visibleTeamControlSummary, entries: buildTeamOperationDisplayEntries({ + copy: params.copy, sessionNameById, teamWaitSummary: visibleTeamWaitSummary, teamControlSummary: visibleTeamControlSummary, diff --git a/src/components/agent/chat/types.ts b/src/components/agent/chat/types.ts index bc5044e02..dd7a04600 100644 --- a/src/components/agent/chat/types.ts +++ b/src/components/agent/chat/types.ts @@ -83,6 +83,9 @@ export interface MessageImageWorkbenchPreview { previewImages?: string[]; imageCount?: number; expectedImageCount?: number; + providerName?: string | null; + modelName?: string | null; + caption?: string | null; layoutHint?: string | null; storyboardSlots?: ImageStoryboardSlot[]; sourceImageUrl?: string | null; diff --git a/src/components/agent/chat/utils/buildUserInputSubmitOp.test.ts b/src/components/agent/chat/utils/buildUserInputSubmitOp.test.ts index 25c5a3559..33bd2b406 100644 --- a/src/components/agent/chat/utils/buildUserInputSubmitOp.test.ts +++ b/src/components/agent/chat/utils/buildUserInputSubmitOp.test.ts @@ -292,6 +292,49 @@ describe("buildUserInputSubmitOp", () => { }); }); + it("图片生成首发应把聊天编排模型放进 provider_config,避免误锁图片模型槽位", () => { + const op = buildUserInputSubmitOp({ + content: "@Nanobanana Pro 生成一张广州塔春天照片", + images: [], + sessionId: "session-image-1", + eventName: "aster_stream_image", + requestMetadata: { + harness: { + image_skill_launch: { + skill_name: "image_generate", + image_task: { + prompt: "一张广州塔春天照片", + provider_id: "fal", + model: "fal-ai/nano-banana-pro", + runtime_contract: { + contract_key: "image_generation", + routing_slot: "image_generation_model", + }, + }, + }, + }, + }, + executionRuntime: null, + syncedRecentPreferences: null, + syncedSessionModelPreference: null, + syncedExecutionStrategy: null, + effectiveExecutionStrategy: "react", + effectiveAccessMode: "current", + effectiveProviderType: "deepseek", + effectiveModel: "deepseek-v4-pro", + webSearch: false, + thinking: false, + }); + + expect(op.preferences?.providerConfig).toEqual({ + provider_id: "deepseek", + provider_name: "deepseek", + model_name: "deepseek-v4-pro", + }); + expect(op.preferences?.providerPreference).toBeUndefined(); + expect(op.preferences?.modelPreference).toBeUndefined(); + }); + it("应透传显式搜索模式,不从用户文本推断", () => { const op = buildUserInputSubmitOp({ content: "请搜索最新 AI 新闻", diff --git a/src/components/agent/chat/utils/buildUserInputSubmitOp.ts b/src/components/agent/chat/utils/buildUserInputSubmitOp.ts index 58baff37a..0220773ba 100644 --- a/src/components/agent/chat/utils/buildUserInputSubmitOp.ts +++ b/src/components/agent/chat/utils/buildUserInputSubmitOp.ts @@ -104,6 +104,9 @@ export function buildUserInputSubmitOp( turnId, images: buildSubmitImages(images), preferences: { + ...(compaction.providerConfig + ? { providerConfig: compaction.providerConfig } + : {}), providerPreference: compaction.shouldSubmitProviderPreference ? effectiveProviderType : undefined, diff --git a/src/components/agent/chat/utils/imageWorkbenchCommand.test.ts b/src/components/agent/chat/utils/imageWorkbenchCommand.test.ts index d56176582..743ff9273 100644 --- a/src/components/agent/chat/utils/imageWorkbenchCommand.test.ts +++ b/src/components/agent/chat/utils/imageWorkbenchCommand.test.ts @@ -126,6 +126,21 @@ describe("parseImageWorkbenchCommand", () => { }); }); + it("应解析 @Nanobanana Pro,并固定到 Nanobanana Pro 模型", () => { + const result = parseImageWorkbenchCommand( + "@Nanobanana Pro 生成一张广州塔,从花城汇看过去的春天的照片", + ); + + expect(result).toMatchObject({ + trigger: "@Nanobanana Pro", + mode: "generate", + count: 1, + providerId: "fal", + modelId: "fal-ai/nano-banana-pro", + prompt: "一张广州塔,从花城汇看过去的春天的照片", + }); + }); + it("未显式声明动作但带目标图时应默认为变体", () => { const result = parseImageWorkbenchCommand("/image #img-7 更偏插画风,4:5"); diff --git a/src/components/agent/chat/utils/imageWorkbenchCommand.ts b/src/components/agent/chat/utils/imageWorkbenchCommand.ts index c7e422883..a8cc2fb54 100644 --- a/src/components/agent/chat/utils/imageWorkbenchCommand.ts +++ b/src/components/agent/chat/utils/imageWorkbenchCommand.ts @@ -1,5 +1,6 @@ export type ImageWorkbenchCommandTrigger = | "@配图" + | "@Nanobanana Pro" | "@分镜" | "@修图" | "@重绘" @@ -24,10 +25,12 @@ export interface ParsedImageWorkbenchCommand { size?: string; aspectRatio?: string; targetRef?: string; + providerId?: string; + modelId?: string; } const IMAGE_COMMAND_PREFIX_REGEX = - /^\s*(@配图|@分镜|@修图|@重绘|@Vision 1|@image|\/image)(?:\s+|$)([\s\S]*)$/i; + /^\s*(@配图|@Nanobanana Pro|@分镜|@修图|@重绘|@Vision 1|@image|\/image)(?:\s+|$)([\s\S]*)$/i; const TARGET_REF_REGEX = /#(img-[a-z0-9_-]+)/i; const SIZE_REGEX = /\b(\d{3,4}x\d{3,4})\b/i; const ASPECT_RATIO_REGEX = /\b(1:1|16:9|9:16|4:3|3:4|3:2|2:3|21:9|4:5|5:4)\b/i; @@ -43,6 +46,9 @@ function normalizeTrigger(value: string): ImageWorkbenchCommandTrigger { if (normalized === "@vision 1") { return "@Vision 1"; } + if (normalized === "@nanobanana pro") { + return "@Nanobanana Pro"; + } if (normalized === "@分镜") { return "@分镜"; } @@ -144,6 +150,19 @@ function resolveMode( return targetRef ? "variation" : "generate"; } +function resolveTriggerModelOverride( + trigger: ImageWorkbenchCommandTrigger, +): Pick { + if (trigger === "@Nanobanana Pro") { + return { + providerId: "fal", + modelId: "fal-ai/nano-banana-pro", + }; + } + + return {}; +} + function stripPromptDecorations(body: string, layoutHint?: string): string { const normalizedBody = layoutHint ? body @@ -220,6 +239,7 @@ export function parseImageWorkbenchCommand( explicitCount, }); const { size, aspectRatio } = resolveSize(normalizedBody); + const modelOverride = resolveTriggerModelOverride(trigger); return { rawText: text, @@ -236,6 +256,7 @@ export function parseImageWorkbenchCommand( size, aspectRatio, targetRef, + ...modelOverride, }; } diff --git a/src/components/agent/chat/utils/imageWorkbenchPresentation.ts b/src/components/agent/chat/utils/imageWorkbenchPresentation.ts new file mode 100644 index 000000000..e207687c7 --- /dev/null +++ b/src/components/agent/chat/utils/imageWorkbenchPresentation.ts @@ -0,0 +1,174 @@ +import type { MessageImageWorkbenchPreview } from "../types"; + +function titleCaseModelSegment(value: string): string { + return value + .split(/[-_\s]+/) + .filter(Boolean) + .map((segment) => { + if (/^gpt$/i.test(segment)) { + return "GPT"; + } + if (/^\d+$/.test(segment)) { + return segment; + } + return `${segment.slice(0, 1).toUpperCase()}${segment + .slice(1) + .toLowerCase()}`; + }) + .join(" "); +} + +export function collapseImageWorkbenchWhitespace( + value: string | null | undefined, +): string { + return (value || "").replace(/\s+/g, " ").trim(); +} + +export function resolveImageWorkbenchModelLabel( + value: string | null | undefined, +): string { + const rawModel = value?.trim(); + if (!rawModel) { + return ""; + } + + const normalized = rawModel.toLowerCase(); + if (normalized.includes("nano-banana-pro")) { + return "Nanobanana Pro"; + } + if ( + normalized.includes("gpt-image-2") || + normalized.includes("gpt-images-2") + ) { + return "GPT Image 2"; + } + + const tail = rawModel.split("/").filter(Boolean).at(-1) || rawModel; + return titleCaseModelSegment(tail); +} + +export function resolveImageWorkbenchPreviewModelLabel( + preview: MessageImageWorkbenchPreview, +): string { + return resolveImageWorkbenchModelLabel( + preview.modelName || preview.runtimeContract?.model || null, + ); +} + +function stripLeadingImageIntent(value: string): string { + return value + .replace(/^@\S+(?:\s+\S+)?\s*/u, "") + .replace(/^(?:请|麻烦你?|帮我|给我)?\s*(?:生成|画|绘制|做|制作|出)\s*/u, "") + .trim(); +} + +function truncatePromptSnippet(value: string, maxLength = 72): string { + const normalized = stripLeadingImageIntent( + collapseImageWorkbenchWhitespace(value) + .replace(/```[\s\S]*?```/g, " ") + .replace(/!\[[^\]]*]\([^)]*\)/g, " "), + ); + + if (normalized.length <= maxLength) { + return normalized; + } + + return `${normalized.slice(0, maxLength).trim()}...`; +} + +function resolveGeneratedSubject(params: { + prompt: string; + expectedImageCount?: number | null; + layoutHint?: string | null; +}): string { + const snippet = truncatePromptSnippet(params.prompt); + if (!snippet) { + return "这张图"; + } + + if ( + params.layoutHint === "storyboard_3x3" || + (params.expectedImageCount ?? 0) > 1 + ) { + const groupSnippet = snippet.replace( + /^一(?:张|幅|个|颗|只|位|款|片|座|条|枚|束|份|套|辆|间)\s*/u, + "", + ); + return /^一组|^这组|^\d+\s*张|^九张|^9张/u.test(snippet) + ? snippet + : `一组${groupSnippet || snippet}`; + } + + return /^一(?:张|幅|组|个|颗|只|位|款|片|座|条|枚|束|份|套|辆|间)|^这张|^这幅|^从/u.test( + snippet, + ) + ? snippet + : `一张${snippet}`; +} + +function resolveImageActionIntro(params: { + prompt: string; + mode?: MessageImageWorkbenchPreview["mode"]; + modelName?: string | null; + expectedImageCount?: number | null; + layoutHint?: string | null; +}): string { + const modelLabel = resolveImageWorkbenchModelLabel(params.modelName); + const modelPrefix = modelLabel ? `用 ${modelLabel} ` : ""; + const subject = resolveGeneratedSubject({ + prompt: params.prompt, + expectedImageCount: params.expectedImageCount, + layoutHint: params.layoutHint, + }); + + switch (params.mode) { + case "edit": + return `好嘞,${modelPrefix}按你的要求处理${subject}`; + case "variation": + return `好嘞,${modelPrefix}按你的要求重绘${subject}`; + case "generate": + default: + return `好嘞,${modelPrefix}给你生成${subject}`; + } +} + +export function buildImageWorkbenchAssistantContent(params: { + prompt: string; + mode?: MessageImageWorkbenchPreview["mode"]; + modelName?: string | null; + expectedImageCount?: number | null; + layoutHint?: string | null; +}): string { + return [resolveImageActionIntro(params), "先获取下工具参数", "马上生成"].join( + "\n", + ); +} + +export function buildImageWorkbenchCaption(params: { + prompt: string; + status: MessageImageWorkbenchPreview["status"]; + imageCount?: number | null; + statusMessage?: string | null; +}): string | null { + const detail = truncatePromptSnippet(params.prompt, 42); + + switch (params.status) { + case "complete": + return detail + ? `搞定,已生成${resolveGeneratedSubject({ prompt: detail })}。` + : "搞定,图片已生成。"; + case "partial": + return params.imageCount && params.imageCount > 0 + ? `先生成了 ${params.imageCount} 张结果。` + : "先生成了部分结果。"; + case "failed": + return params.statusMessage?.trim() + ? `这次没有生成成功:${params.statusMessage.trim()}` + : "这次没有生成成功。"; + case "cancelled": + return "已停止生成。"; + case "running": + default: + return null; + } +} diff --git a/src/components/agent/chat/utils/imageWorkbenchStatusText.ts b/src/components/agent/chat/utils/imageWorkbenchStatusText.ts new file mode 100644 index 000000000..fe2ee94ca --- /dev/null +++ b/src/components/agent/chat/utils/imageWorkbenchStatusText.ts @@ -0,0 +1,186 @@ +const IMAGE_WORKBENCH_STATUS_STRIP_PATTERN = + /[`*_>#\-\s,。!?、;:,.!?;:()()【】[\]{}"“”'‘’]/g; + +export function normalizeImageWorkbenchStatusText( + value?: string | null, +): string { + return (value || "") + .replace(IMAGE_WORKBENCH_STATUS_STRIP_PATTERN, "") + .toLowerCase(); +} + +const normalizePhrases = (phrases: string[]) => + phrases.map((phrase) => normalizeImageWorkbenchStatusText(phrase)); + +const IMAGE_WORKBENCH_SUBMISSION_CUES = normalizePhrases([ + "任务已创建成功", + "任务已创建", + "任务已提交", + "任务已成功提交", + "生成任务已创建", + "生成任务已提交", + "生成任务已进入队列", + "图片生成任务已成功提交", + "生成详情", + "任务详情", +]); + +const IMAGE_WORKBENCH_DETAIL_KEYWORDS = normalizePhrases([ + "画面构图", + "风格", + "尺寸", + "色调", + "模型", + "状态", + "提示词", + "Prompt", + "Provider", + "参数", + "任务 ID", + "任务ID", + "Task ID", + "任务摘要", + "下一步流程", + "当前状态", + "生成完成后", + "稍等一下", + "已进入队列", + "正在生成中", + "等待 AI 模型处理", + "系统会自动轮询", + "图片工作台", + "Image Workbench", + "生成进度", + "最终结果", + "队列", +]); + +function includesAny(normalized: string, phrases: string[]): boolean { + return phrases.some((phrase) => normalized.includes(phrase)); +} + +function countDetailHits(normalized: string): number { + return IMAGE_WORKBENCH_DETAIL_KEYWORDS.filter((keyword) => + normalized.includes(keyword), + ).length; +} + +export function isVerboseImageWorkbenchSubmissionText( + value?: string | null, +): boolean { + const normalized = normalizeImageWorkbenchStatusText(value); + if (!normalized) { + return false; + } + + if (!includesAny(normalized, IMAGE_WORKBENCH_SUBMISSION_CUES)) { + return false; + } + + const detailHitCount = countDetailHits(normalized); + const hasTableShape = + normalized.includes("项目") && normalized.includes("内容"); + const hasTaskIdentity = + normalized.includes("任务id") || normalized.includes("taskid"); + + return ( + detailHitCount >= 3 || + (hasTableShape && detailHitCount >= 2) || + (hasTaskIdentity && detailHitCount >= 2) + ); +} + +export function shouldSuppressImageWorkbenchStatusText( + value?: string | null, +): boolean { + const normalized = normalizeImageWorkbenchStatusText(value); + if (!normalized) { + return false; + } + + if (isVerboseImageWorkbenchSubmissionText(value)) { + return true; + } + + return [ + /^(?:图片(?:编辑|重绘)?|修图|重绘|配图)任务(?:已创建|已进入队列|正在生成中|处理中|已完成|已取消|失败).*/, + /^(?:图片生成|图片编辑|图片重绘|图片|修图|重绘|配图|3x3分镜|分镜)任务已(?:成功)?提交.*/, + /^(?:图片生成|图片编辑|图片重绘|图片|修图|重绘|配图|3x3分镜|分镜)任务.*(?:正在同步任务状态|正在排队处理|等待进入队列|等待ai模型处理).*/, + /^(?:图片生成|图片结果|图片|3x3分镜)(?:已完成|已生成完成|正在生成中|已进入队列|已创建|已返回部分结果).*/, + /^(?:图片|3x3分镜).*(?:生成完成|返回部分结果|生成失败|执行失败|已取消).*/, + /^正在(?:生成|处理|重绘|编辑)图片$/, + /^(?:图片|3x3分镜).*(?:可在右侧|右侧查看|打开查看|工作区会继续同步).*/, + /^已(?:成功)?提交.*(?:图片|分镜|生成)?任务.*$/, + /^任务已(?:提交|创建|进入队列).*/, + /^.*(?:图片|插画|海报|封面|分镜).*生成任务已(?:创建|提交|进入队列).*$/, + /^.*(?:图片生成任务|生成任务)已(?:成功)?(?:创建|提交|进入队列).*$/, + /^我先按你的描述.*(?:创建异步|图片任务|修图任务|重绘要求).*$/, + /^我已(?:经)?按.*(?:完成一轮配图|把结果同步回对话).*/, + ].some((pattern) => pattern.test(normalized)); +} + +export function isImageWorkbenchStatusOnlyText(value?: string | null): boolean { + const normalized = normalizeImageWorkbenchStatusText(value); + if (!normalized) { + return true; + } + + if ( + normalized.includes("正在同步任务状态") || + normalized.includes("正在排队处理中") || + normalized.includes("状态排队中pending_submit") + ) { + return true; + } + + return [ + /^图片任务已创建正在准备执行$/, + /^图片任务已进入队列正在等待执行$/, + /^图片任务正在生成中$/, + /^图片任务已取消.*$/, + /^图片任务失败.*$/, + /^图片编辑任务已创建正在准备执行$/, + /^图片编辑任务已进入队列正在等待执行$/, + /^图片编辑任务正在生成中$/, + /^图片编辑任务已取消.*$/, + /^图片编辑任务失败.*$/, + /^图片重绘任务已创建正在准备执行$/, + /^图片重绘任务已进入队列正在等待执行$/, + /^图片重绘任务正在生成中$/, + /^图片重绘任务已取消.*$/, + /^图片重绘任务失败.*$/, + ].some((pattern) => pattern.test(normalized)); +} + +export function isImageWorkbenchSubmissionTemplateText( + value?: string | null, +): boolean { + const normalized = normalizeImageWorkbenchStatusText(value); + if (!normalized) { + return false; + } + + if (isVerboseImageWorkbenchSubmissionText(value)) { + return true; + } + + if (shouldSuppressImageWorkbenchStatusText(value)) { + return true; + } + + const detailHitCount = countDetailHits(normalized); + const hasTemplateHeader = + normalized.includes("任务详情") || + normalized.includes("任务摘要") || + normalized.includes("生成详情"); + const hasTaskIdentity = + normalized.includes("任务id") || normalized.includes("taskid"); + + return ( + (hasTemplateHeader && detailHitCount >= 3) || + (hasTaskIdentity && detailHitCount >= 3) || + (normalized.includes("图片工作台") && + hasTaskIdentity && + detailHitCount >= 2) + ); +} diff --git a/src/components/agent/chat/utils/mentionCommandReplayText.test.ts b/src/components/agent/chat/utils/mentionCommandReplayText.test.ts index de951406b..12eed9a8d 100644 --- a/src/components/agent/chat/utils/mentionCommandReplayText.test.ts +++ b/src/components/agent/chat/utils/mentionCommandReplayText.test.ts @@ -79,6 +79,19 @@ describe("buildMentionCommandReplayText", () => { ).toBe("16:9 一张春日咖啡馆插画 出 2 张"); }); + it("应把 @Nanobanana Pro 回放成 image_generate 同构参数", () => { + const parsedCommand = parseImageWorkbenchCommand( + "@Nanobanana Pro 生成一张广州塔,从花城汇看过去的春天的照片", + ); + + expect( + buildMentionCommandReplayText({ + commandKey: "image_generate_nanobanana_pro", + parsedCommand: parsedCommand!, + }), + ).toBe("一张广州塔,从花城汇看过去的春天的照片"); + }); + it("应把 @分镜 回放整理成可再次解析的布局骨架", () => { const parsedCommand = parseImageWorkbenchCommand( "@分镜 生成 三国主要人物,3x3 分镜,电影感", diff --git a/src/components/agent/chat/utils/mentionCommandReplayText.ts b/src/components/agent/chat/utils/mentionCommandReplayText.ts index 83839faad..4c5102348 100644 --- a/src/components/agent/chat/utils/mentionCommandReplayText.ts +++ b/src/components/agent/chat/utils/mentionCommandReplayText.ts @@ -1027,6 +1027,7 @@ export function buildMentionCommandReplayText( ): string | undefined { if ( input.commandKey === "image_generate" || + input.commandKey === "image_generate_nanobanana_pro" || input.commandKey === "image_storyboard" || input.commandKey === "image_edit" || input.commandKey === "image_variation" @@ -1240,6 +1241,7 @@ function buildMentionCommandReplayTextFromSlotValues(params: { if ( commandKey === "image_generate" || + commandKey === "image_generate_nanobanana_pro" || commandKey === "image_storyboard" || commandKey === "image_edit" || commandKey === "image_variation" diff --git a/src/components/agent/chat/utils/plainVisualBriefConfirmation.ts b/src/components/agent/chat/utils/plainVisualBriefConfirmation.ts new file mode 100644 index 000000000..05c777e83 --- /dev/null +++ b/src/components/agent/chat/utils/plainVisualBriefConfirmation.ts @@ -0,0 +1,40 @@ +export const PLAIN_VISUAL_BRIEF_CONFIRMATION = + "这看起来是图片生成需求,要我直接调用画图功能生成吗?"; + +const EXPLICIT_COMMAND_PATTERN = /^\s*[@/]/; +const VISUAL_OBJECT_PATTERN = + /(配图|图片|图像|插画|海报|封面|主视觉|视觉|画面|画一张|画个|生成一张|出一张|做一张|设计一张)/i; +const VISUAL_BRIEF_PATTERNS = [ + /\b(?:1:1|3:4|4:5|9:16|16:9)\b/i, + /(竖版|横版|方图|比例|尺寸|留白|标题区|构图|色调|配色|风格|水彩|插画|摄影|电影感|海报|封面|主视觉|品牌视觉|背景|主体|字体)/i, +]; +const TEXT_ONLY_INTENT_PATTERN = + /(提示词|prompt|方案|文案|分析|拆解|描述|改写|润色|翻译|总结|只要|不要生成|不要画|先别生成|不需要生成)/i; + +export function shouldConfirmPlainVisualBrief(value?: string | null): boolean { + const text = value?.trim() || ""; + if (!text || EXPLICIT_COMMAND_PATTERN.test(text)) { + return false; + } + if (TEXT_ONLY_INTENT_PATTERN.test(text)) { + return false; + } + if (!VISUAL_OBJECT_PATTERN.test(text)) { + return false; + } + + const visualBriefScore = VISUAL_BRIEF_PATTERNS.reduce( + (score, pattern) => score + (pattern.test(text) ? 1 : 0), + 0, + ); + return visualBriefScore >= 1; +} + +export function buildPlainVisualBriefConfirmationSystemPrompt(): string { + return [ + "你是 Lime 的聊天入口意图确认器。", + "用户这轮没有使用 @配图/@修图/@重绘/@image,但内容明显是图片、海报、封面或视觉生成 brief。", + `本轮只能回复这一句:${PLAIN_VISUAL_BRIEF_CONFIRMATION}`, + "不要输出 HTML/CSS/SVG/Markdown 草图,不要写设计方案,不要生成提示词全文,不要创建任务,不要调用任何图片工具。", + ].join("\n"); +} diff --git a/src/components/agent/chat/utils/submitOpRuntimeCompaction.test.ts b/src/components/agent/chat/utils/submitOpRuntimeCompaction.test.ts index add87b62f..e677b6f2a 100644 --- a/src/components/agent/chat/utils/submitOpRuntimeCompaction.test.ts +++ b/src/components/agent/chat/utils/submitOpRuntimeCompaction.test.ts @@ -239,7 +239,7 @@ describe("submitOpRuntimeCompaction", () => { expect(result.shouldSubmitModelPreference).toBe(true); }); - it("图片生成命令应让后端图片路由解析模型,不应提交当前聊天模型偏好", () => { + it("图片生成命令新会话应提交编排聊天模型 provider_config,但不锁定图片模型偏好", () => { const requestMetadata = { harness: { image_skill_launch: { @@ -278,9 +278,58 @@ describe("submitOpRuntimeCompaction", () => { expect(result.shouldSubmitProviderPreference).toBe(false); expect(result.shouldSubmitModelPreference).toBe(false); + expect(result.providerConfig).toEqual({ + provider_id: "deepseek", + provider_name: "deepseek", + model_name: "deepseek-v4-flash", + }); expect(result.metadata).toBe(requestMetadata); }); + it("图片生成命令已有会话模型时不重复提交编排 provider_config", () => { + const requestMetadata = { + harness: { + image_skill_launch: { + skill_name: "image_generate", + image_task: { + prompt: "生成一张公众号封面", + provider_id: "fal", + model: "fal-ai/nano-banana-pro", + runtime_contract: { + contract_key: "image_generation", + routing_slot: "image_generation_model", + }, + }, + }, + }, + }; + + const result = buildSubmitOpRuntimeCompaction({ + requestMetadata, + executionRuntime: { + session_id: "session-image-1", + source: "runtime_snapshot", + provider_selector: "deepseek", + model_name: "deepseek-v4-flash", + }, + syncedRecentPreferences: null, + syncedSessionModelPreference: { + providerType: "deepseek", + model: "deepseek-v4-flash", + }, + syncedExecutionStrategy: null, + effectiveExecutionStrategy: "react", + effectiveProviderType: "deepseek", + effectiveModel: "deepseek-v4-flash", + webSearch: false, + thinking: false, + }); + + expect(result.providerConfig).toBeUndefined(); + expect(result.shouldSubmitProviderPreference).toBe(false); + expect(result.shouldSubmitModelPreference).toBe(false); + }); + it("应将 legacy general workbench alias runtime 视为 general_workbench 做裁剪", () => { const result = buildSubmitOpRuntimeCompaction({ requestMetadata: { diff --git a/src/components/agent/chat/utils/submitOpRuntimeCompaction.ts b/src/components/agent/chat/utils/submitOpRuntimeCompaction.ts index 6eac38a31..9f1bf2adf 100644 --- a/src/components/agent/chat/utils/submitOpRuntimeCompaction.ts +++ b/src/components/agent/chat/utils/submitOpRuntimeCompaction.ts @@ -1,4 +1,5 @@ import type { + AsterProviderConfig, AsterExecutionStrategy, AsterSessionExecutionRuntime, AsterSessionExecutionRuntimeRecentTeamSelection, @@ -504,6 +505,7 @@ export interface BuildSubmitOpRuntimeCompactionOptions { export interface SubmitOpRuntimeCompactionResult { metadata?: Record; + providerConfig?: AsterProviderConfig; shouldSubmitProviderPreference: boolean; shouldSubmitModelPreference: boolean; shouldSubmitExecutionStrategy: boolean; @@ -548,19 +550,52 @@ export function buildSubmitOpRuntimeCompaction( const shouldDeferModelRoutingToBackend = hasFastResponseRouting || hasImageGenerationRouting; const hasExplicitModelOverride = Boolean(modelOverride?.trim()); + const normalizedEffectiveProviderType = normalizeRuntimeIdentifier( + effectiveProviderType, + ); + const normalizedEffectiveModel = normalizeRuntimeIdentifier(effectiveModel); + const knownProviderChanged = Boolean( + knownProviderSelector && + normalizedEffectiveProviderType && + normalizeRuntimeIdentifier(knownProviderSelector) !== + normalizedEffectiveProviderType, + ); + const knownModelChanged = Boolean( + knownModelName && + normalizedEffectiveModel && + normalizeRuntimeIdentifier(knownModelName) !== normalizedEffectiveModel, + ); + const shouldSubmitImageOrchestrationProviderConfig = Boolean( + hasImageGenerationRouting && + effectiveProviderType.trim() && + effectiveModel.trim() && + (!knownProviderSelector || + !knownModelName || + knownProviderChanged || + knownModelChanged || + hasExplicitModelOverride), + ); + const imageOrchestrationProviderConfig: AsterProviderConfig | undefined = + shouldSubmitImageOrchestrationProviderConfig + ? { + provider_id: effectiveProviderType.trim(), + provider_name: effectiveProviderType.trim(), + model_name: effectiveModel.trim(), + } + : undefined; const shouldSubmitProviderPreference = !shouldDeferModelRoutingToBackend && (!knownProviderSelector || normalizeRuntimeIdentifier(knownProviderSelector) !== - normalizeRuntimeIdentifier(effectiveProviderType)); - const shouldSubmitModelPreference = - shouldDeferModelRoutingToBackend && !hasExplicitModelOverride + normalizedEffectiveProviderType); + const shouldSubmitModelPreference = hasImageGenerationRouting + ? false + : shouldDeferModelRoutingToBackend && !hasExplicitModelOverride ? false : hasExplicitModelOverride || shouldSubmitProviderPreference || !knownModelName || - normalizeRuntimeIdentifier(knownModelName) !== - normalizeRuntimeIdentifier(effectiveModel); + normalizeRuntimeIdentifier(knownModelName) !== normalizedEffectiveModel; const knownExecutionStrategy = syncedExecutionStrategy?.trim() || @@ -770,6 +805,7 @@ export function buildSubmitOpRuntimeCompaction( return { metadata, + providerConfig: imageOrchestrationProviderConfig, shouldSubmitProviderPreference, shouldSubmitModelPreference, shouldSubmitExecutionStrategy, diff --git a/src/components/agent/chat/utils/taskPreviewFromToolResult.test.ts b/src/components/agent/chat/utils/taskPreviewFromToolResult.test.ts index d6a640894..12f694d7d 100644 --- a/src/components/agent/chat/utils/taskPreviewFromToolResult.test.ts +++ b/src/components/agent/chat/utils/taskPreviewFromToolResult.test.ts @@ -42,7 +42,7 @@ describe("buildImageTaskPreviewFromToolResult", () => { taskFilePath: "/tmp/task-1.json", artifactPath: ".lime/tasks/image_generate/task-1.json", phase: "succeeded", - statusMessage: "图片已生成完成,共 2 张。", + statusMessage: "图片生成完成。", }); }); @@ -67,7 +67,7 @@ describe("buildImageTaskPreviewFromToolResult", () => { taskId: "task-2", status: "failed", phase: "failed", - statusMessage: "图片任务执行失败,请查看工具结果或任务详情。", + statusMessage: "图片生成失败。", }); }); @@ -94,11 +94,11 @@ describe("buildImageTaskPreviewFromToolResult", () => { taskId: "task-3", status: "complete", phase: "succeeded", - statusMessage: "图片结果已生成完成,可在右侧查看与使用。", + statusMessage: "图片生成完成。", }); }); - it("图片任务刚提交时,应输出用户可理解的排队态文案", () => { + it("图片任务刚提交时,应输出同会话生成态文案", () => { const preview = buildImageTaskPreviewFromToolResult({ toolId: "tool-4", toolName: "Bash", @@ -120,7 +120,7 @@ describe("buildImageTaskPreviewFromToolResult", () => { taskId: "task-4", status: "running", phase: "queued", - statusMessage: "图片任务已提交,正在排队处理。", + statusMessage: "正在生成图片。", }); }); @@ -151,7 +151,7 @@ describe("buildImageTaskPreviewFromToolResult", () => { status: "complete", imageCount: 9, layoutHint: "storyboard_3x3", - statusMessage: "3x3 分镜已生成完成,共 9 张。", + statusMessage: "3x3 分镜生成完成。", }); }); }); diff --git a/src/components/agent/chat/utils/taskPreviewFromToolResult.ts b/src/components/agent/chat/utils/taskPreviewFromToolResult.ts index d81491e4d..22f987d4c 100644 --- a/src/components/agent/chat/utils/taskPreviewFromToolResult.ts +++ b/src/components/agent/chat/utils/taskPreviewFromToolResult.ts @@ -1261,26 +1261,18 @@ export function buildImageTaskPreviewFromToolResult( : receivedCount || expectedImageCount || requestedCount; const statusMessage = previewStatus === "complete" - ? receivedCount && receivedCount > 0 - ? layoutHint === "storyboard_3x3" - ? `3x3 分镜已生成完成,共 ${receivedCount} 张。` - : `图片已生成完成,共 ${receivedCount} 张。` - : layoutHint === "storyboard_3x3" - ? "3x3 分镜已生成完成,可在右侧继续查看与使用。" - : "图片结果已生成完成,可在右侧查看与使用。" + ? layoutHint === "storyboard_3x3" + ? "3x3 分镜生成完成。" + : "图片生成完成。" : previewStatus === "partial" - ? receivedCount && receivedCount > 0 - ? layoutHint === "storyboard_3x3" - ? `3x3 分镜已返回部分结果,共 ${receivedCount} 张。` - : `图片已返回部分结果,共 ${receivedCount} 张。` - : layoutHint === "storyboard_3x3" - ? "3x3 分镜已返回部分结果,可在右侧继续查看。" - : "图片已返回部分结果,可在右侧继续查看。" + ? layoutHint === "storyboard_3x3" + ? "3x3 分镜返回部分结果。" + : "图片返回部分结果。" : previewStatus === "failed" - ? "图片任务执行失败,请查看工具结果或任务详情。" + ? "图片生成失败。" : previewStatus === "cancelled" - ? "图片任务已取消。" - : "图片任务已提交,正在排队处理。"; + ? "图片生成已取消。" + : "正在生成图片。"; return { taskId, diff --git a/src/components/agent/chat/workspace/WorkspaceConversationScene.tsx b/src/components/agent/chat/workspace/WorkspaceConversationScene.tsx index 0e604d2fa..b9273f53d 100644 --- a/src/components/agent/chat/workspace/WorkspaceConversationScene.tsx +++ b/src/components/agent/chat/workspace/WorkspaceConversationScene.tsx @@ -381,6 +381,9 @@ interface WorkspaceConversationSceneProps extends WorkspaceMainAreaProps { runtimeToolAvailability?: ComponentProps< typeof EmptyState >["runtimeToolAvailability"]; + initialInputCapability?: ComponentProps< + typeof EmptyState + >["initialInputCapability"]; knowledgePackSelection?: ComponentProps< typeof EmptyState >["knowledgePackSelection"]; @@ -542,6 +545,7 @@ export function WorkspaceConversationScene({ onDismissWorkspaceHint, onOpenSettings, runtimeToolAvailability, + initialInputCapability, knowledgePackSelection, knowledgePackOptions, onToggleKnowledgePack, @@ -646,6 +650,7 @@ export function WorkspaceConversationScene({ projectId, sessionId, runtimeToolAvailability, + initialInputCapability, knowledgePackSelection, knowledgePackOptions, onToggleKnowledgePack, diff --git a/src/components/agent/chat/workspace/chatSurfaceProps.ts b/src/components/agent/chat/workspace/chatSurfaceProps.ts index 049031f1a..0df5e2412 100644 --- a/src/components/agent/chat/workspace/chatSurfaceProps.ts +++ b/src/components/agent/chat/workspace/chatSurfaceProps.ts @@ -183,6 +183,9 @@ interface BuildWorkspaceEmptyStatePropsParams { runtimeToolAvailability?: ComponentProps< typeof EmptyState >["runtimeToolAvailability"]; + initialInputCapability?: ComponentProps< + typeof EmptyState + >["initialInputCapability"]; knowledgePackSelection?: ComponentProps< typeof EmptyState >["knowledgePackSelection"]; @@ -289,6 +292,7 @@ export function buildWorkspaceEmptyStateProps({ projectId, sessionId, runtimeToolAvailability, + initialInputCapability, knowledgePackSelection, knowledgePackOptions, onToggleKnowledgePack, @@ -374,6 +378,7 @@ export function buildWorkspaceEmptyStateProps({ projectId, sessionId, runtimeToolAvailability, + initialInputCapability, knowledgePackSelection, knowledgePackOptions, onToggleKnowledgePack, diff --git a/src/components/agent/chat/workspace/imageSkillLaunch.test.ts b/src/components/agent/chat/workspace/imageSkillLaunch.test.ts new file mode 100644 index 000000000..fcb1860fe --- /dev/null +++ b/src/components/agent/chat/workspace/imageSkillLaunch.test.ts @@ -0,0 +1,43 @@ +import { describe, expect, it, vi } from "vitest"; +import { parseImageWorkbenchCommand } from "../utils/imageWorkbenchCommand"; +import { createInitialSessionImageWorkbenchState } from "./imageWorkbenchHelpers"; +import { resolveImageWorkbenchSkillRequest } from "./imageSkillLaunch"; + +vi.mock("sonner", () => ({ + toast: { + error: vi.fn(), + }, +})); + +describe("resolveImageWorkbenchSkillRequest", () => { + it("@Nanobanana Pro 应覆盖当前图片工作台模型选择", () => { + const rawText = + "@Nanobanana Pro 生成一张广州塔,从花城汇看过去的春天的照片"; + const parsedCommand = parseImageWorkbenchCommand(rawText); + + const skillRequest = resolveImageWorkbenchSkillRequest({ + rawText, + parsedCommand: parsedCommand!, + images: [], + currentImageWorkbenchState: createInitialSessionImageWorkbenchState(), + imageWorkbenchSelectedProviderId: "openai", + imageWorkbenchSelectedModelId: "gpt-image-2", + imageWorkbenchSelectedSize: "1024x1024", + imageWorkbenchSessionKey: "session-image-1", + projectId: "project-image-1", + projectRootPath: "/workspace/project-image-1", + contentId: "content-image-1", + }); + + expect(skillRequest?.requestContext).toMatchObject({ + kind: "image_task", + image_task: { + mode: "generate", + prompt: "一张广州塔,从花城汇看过去的春天的照片", + raw_text: rawText, + provider_id: "fal", + model: "fal-ai/nano-banana-pro", + }, + }); + }); +}); diff --git a/src/components/agent/chat/workspace/imageSkillLaunch.ts b/src/components/agent/chat/workspace/imageSkillLaunch.ts index 72901c40a..87396d0d9 100644 --- a/src/components/agent/chat/workspace/imageSkillLaunch.ts +++ b/src/components/agent/chat/workspace/imageSkillLaunch.ts @@ -238,8 +238,9 @@ export function resolveImageWorkbenchSkillRequest( size: parsedCommand.size || params.imageWorkbenchSelectedSize, aspect_ratio: parsedCommand.aspectRatio, usage, - provider_id: params.imageWorkbenchSelectedProviderId, - model: params.imageWorkbenchSelectedModelId, + provider_id: + parsedCommand.providerId || params.imageWorkbenchSelectedProviderId, + model: parsedCommand.modelId || params.imageWorkbenchSelectedModelId, session_id: resolvedSessionId || undefined, project_id: params.projectId, content_id: params.contentId || undefined, diff --git a/src/components/agent/chat/workspace/useWorkspaceConversationSceneRuntime.tsx b/src/components/agent/chat/workspace/useWorkspaceConversationSceneRuntime.tsx index d505b786d..b9868d28d 100644 --- a/src/components/agent/chat/workspace/useWorkspaceConversationSceneRuntime.tsx +++ b/src/components/agent/chat/workspace/useWorkspaceConversationSceneRuntime.tsx @@ -349,6 +349,7 @@ interface UseWorkspaceConversationSceneRuntimeParams { serviceSkillGroups: ConversationScenePresentationParams["scene"]["serviceSkillGroups"]; skillsLoading: ConversationScenePresentationParams["scene"]["isSkillsLoading"]; onSelectServiceSkill?: ConversationScenePresentationParams["scene"]["onSelectServiceSkill"]; + initialInputCapability?: ConversationScenePresentationParams["scene"]["initialInputCapability"]; handleNavigateToSkillSettings: ConversationScenePresentationParams["scene"]["onNavigateToSettings"]; handleRefreshSkills: ConversationScenePresentationParams["scene"]["onRefreshSkills"]; handleOpenBrowserAssistInCanvas: ConversationScenePresentationParams["scene"]["onLaunchBrowserAssist"]; @@ -512,6 +513,7 @@ export function useWorkspaceConversationSceneRuntime({ serviceSkillGroups, skillsLoading, onSelectServiceSkill, + initialInputCapability, handleNavigateToSkillSettings, handleRefreshSkills, handleOpenBrowserAssistInCanvas, @@ -1123,6 +1125,7 @@ export function useWorkspaceConversationSceneRuntime({ ? navigationActions.handleOpenAppearanceSettings : undefined, runtimeToolAvailability: inputbarScene.runtimeToolAvailability, + initialInputCapability, knowledgePackSelection: inputbarScene.knowledgePackSelection, knowledgePackOptions: inputbarScene.knowledgePackOptions, onToggleKnowledgePack: inputbarScene.onToggleKnowledgePack, diff --git a/src/components/agent/chat/workspace/useWorkspaceImageTaskPreviewRuntime.test.tsx b/src/components/agent/chat/workspace/useWorkspaceImageTaskPreviewRuntime.test.tsx index c9463be30..fd55ad0a5 100644 --- a/src/components/agent/chat/workspace/useWorkspaceImageTaskPreviewRuntime.test.tsx +++ b/src/components/agent/chat/workspace/useWorkspaceImageTaskPreviewRuntime.test.tsx @@ -377,31 +377,17 @@ describe("useWorkspaceImageTaskPreviewRuntime", () => { expect(getValue().messages).toEqual([ expect.objectContaining({ id: "image-workbench:task-image-1:assistant", - content: "图片任务已进入队列,正在等待执行。", - isThinking: true, - toolCalls: [ - expect.objectContaining({ - name: "limeCreateImageGenerationTask", - status: "running", - }), - ], - contentParts: expect.arrayContaining([ - expect.objectContaining({ - type: "text", - }), - expect.objectContaining({ - type: "tool_use", - toolCall: expect.objectContaining({ - name: "limeCreateImageGenerationTask", - }), - }), - ]), + content: expect.stringContaining("先获取下工具参数"), + isThinking: false, + toolCalls: undefined, + contentParts: undefined, + runtimeStatus: undefined, imageWorkbenchPreview: expect.objectContaining({ taskId: "task-image-1", status: "running", projectId: "project-image-1", contentId: "content-image-1", - statusMessage: "任务已提交,正在排队处理。", + statusMessage: "正在生成图片。", }), }), ]); @@ -451,13 +437,10 @@ describe("useWorkspaceImageTaskPreviewRuntime", () => { expect(getValue().messages).toEqual([ expect.objectContaining({ - content: "图片任务正在生成中。", - toolCalls: [ - expect.objectContaining({ - name: "limeCreateImageGenerationTask", - status: "running", - }), - ], + content: expect.stringContaining("未来感实验室里的青柠主视觉"), + toolCalls: undefined, + contentParts: undefined, + runtimeStatus: undefined, imageWorkbenchPreview: expect.objectContaining({ taskId: "task-image-1", status: "running", @@ -468,9 +451,6 @@ describe("useWorkspaceImageTaskPreviewRuntime", () => { statusMessage: "正在绘制第一版构图", attemptCount: 1, }), - runtimeStatus: expect.objectContaining({ - detail: "正在绘制第一版构图", - }), }), ]); @@ -481,14 +461,11 @@ describe("useWorkspaceImageTaskPreviewRuntime", () => { expect(getValue().messages).toEqual([ expect.objectContaining({ - content: "图片任务已完成,共生成 1 张。", + content: expect.stringContaining("未来感实验室里的青柠主视觉"), isThinking: false, - toolCalls: [ - expect.objectContaining({ - name: "limeCreateImageGenerationTask", - status: "completed", - }), - ], + toolCalls: undefined, + contentParts: undefined, + runtimeStatus: undefined, imageWorkbenchPreview: expect.objectContaining({ taskId: "task-image-1", status: "complete", @@ -496,6 +473,7 @@ describe("useWorkspaceImageTaskPreviewRuntime", () => { imageUrl: "https://example.com/generated-lime.png", imageCount: 1, size: "1024x1024", + caption: "搞定,已生成一张未来感实验室里的青柠主视觉。", }), }), ]); @@ -807,21 +785,283 @@ describe("useWorkspaceImageTaskPreviewRuntime", () => { expect(getValue().messages).toHaveLength(1); expect(getValue().messages[0]).toMatchObject({ id: "assistant-skill-image-1", - content: "任务类型:image_generate\n任务 ID:task-image-skill-1", - toolCalls: [ - expect.objectContaining({ - name: "Bash", - status: "completed", - }), - ], + content: expect.stringContaining("春日咖啡馆插画"), + toolCalls: undefined, imageWorkbenchPreview: expect.objectContaining({ taskId: "task-image-skill-1", status: "complete", imageUrl: "https://example.com/skill-preview.png", + caption: "搞定,已生成一张春日咖啡馆插画。", }), }); }); + it("收到带 turn_id 的图片任务事件时,应合并到同一 assistant 回合并清理旧占位卡", async () => { + let listener: CreationTaskListener | null = null; + vi.mocked(safeListen).mockImplementationOnce(async (event, handler) => { + expect(event).toBe("lime://creation_task_submitted"); + listener = handler; + return vi.fn(); + }); + + const taskId = "task-image-live-turn-1"; + const taskPath = + "/workspace/project-image-1/.lime/tasks/image_generate/task-image-live-turn-1.json"; + const firstPreview = createDeferred(); + vi.mocked(readFilePreview).mockImplementationOnce( + () => firstPreview.promise, + ); + + const { render, getValue } = renderHook( + {}, + { + initialMessages: [ + { + id: "assistant-live-image-1", + role: "assistant", + content: "我来直接生成这张青柠插画。", + timestamp: new Date("2026-04-04T10:00:00Z"), + isThinking: true, + runtimeTurnId: "turn-image-live-1", + }, + { + id: `image-workbench:${taskId}:assistant`, + role: "assistant", + content: "图片任务已创建,正在准备执行。", + timestamp: new Date("2026-04-04T10:00:01Z"), + isThinking: true, + imageWorkbenchPreview: { + taskId, + prompt: "青柠插画", + status: "running", + phase: "queued", + }, + }, + ], + }, + ); + await render(); + + await act(async () => { + listener?.({ + payload: withDefaultTaskContext({ + task_id: taskId, + task_type: "image_generate", + task_family: "image", + status: "pending_submit", + path: `.lime/tasks/image_generate/${taskId}.json`, + prompt: "青柠插画", + thread_id: "thread-image-live-1", + turn_id: "turn-image-live-1", + }), + }); + await Promise.resolve(); + await Promise.resolve(); + }); + + expect(getValue().messages).toHaveLength(1); + expect(getValue().messages[0]).toMatchObject({ + id: "assistant-live-image-1", + content: expect.stringContaining("青柠插画"), + runtimeTurnId: "turn-image-live-1", + imageWorkbenchPreview: expect.objectContaining({ + taskId, + status: "running", + prompt: "青柠插画", + }), + toolCalls: undefined, + contentParts: undefined, + runtimeStatus: undefined, + }); + expect( + getValue().messages.some((message) => + message.id.startsWith("image-workbench:"), + ), + ).toBe(false); + + firstPreview.resolve( + createFilePreviewResult( + taskPath, + withDefaultTaskContext({ + task_id: taskId, + task_type: "image_generate", + task_family: "image", + status: "completed", + normalized_status: "succeeded", + created_at: "2026-04-04T10:00:00Z", + payload: { + prompt: "青柠插画", + count: 1, + size: "1024x1024", + thread_id: "thread-image-live-1", + turn_id: "turn-image-live-1", + }, + result: { + images: [{ url: "https://example.com/live-turn-image.png" }], + }, + }), + ), + ); + await act(async () => { + await Promise.resolve(); + await Promise.resolve(); + }); + + expect(getValue().messages).toHaveLength(1); + expect(getValue().messages[0]).toMatchObject({ + id: "assistant-live-image-1", + content: expect.stringContaining("青柠插画"), + imageWorkbenchPreview: expect.objectContaining({ + taskId, + status: "complete", + imageUrl: "https://example.com/live-turn-image.png", + caption: "搞定,已生成一张青柠插画。", + }), + }); + }); + + it("收到带 turn_id 的图片任务事件时,应清理同回合旧生成详情模板", async () => { + let listener: CreationTaskListener | null = null; + vi.mocked(safeListen).mockImplementationOnce(async (_event, handler) => { + listener = handler; + return vi.fn(); + }); + + const taskId = "task-image-live-turn-verbose-1"; + const { render, getValue } = renderHook( + {}, + { + initialMessages: [ + { + id: "assistant-live-image-verbose-1", + role: "assistant", + content: + "好的!我来为你生成一张三国群像海报。\n\n任务已创建成功!这里是生成详情:\n\n| 项目 | 内容 |\n| --- | --- |\n| 画面构图 | 刘关张居中 |\n| 风格 | 国风电影感 |\n| 尺寸 | 1792 x 1024 |\n| 色调 | 墨黑、赤红、暗金 |\n| 模型 | fal-ai/nano-banana-pro |\n| 状态 | 已进入队列,正在生成中... |\n\n生成完成后图片会显示在对话中,稍等一下即可看到效果。", + timestamp: new Date("2026-04-04T10:05:00Z"), + isThinking: true, + runtimeTurnId: "turn-image-live-verbose-1", + }, + ], + }, + ); + await render(); + + await act(async () => { + listener?.({ + payload: withDefaultTaskContext({ + task_id: taskId, + task_type: "image_generate", + task_family: "image", + status: "pending_submit", + path: `.lime/tasks/image_generate/${taskId}.json`, + prompt: "三国主要人物群像海报", + thread_id: "thread-image-live-verbose-1", + turn_id: "turn-image-live-verbose-1", + }), + }); + await Promise.resolve(); + await Promise.resolve(); + }); + + expect(getValue().messages).toHaveLength(1); + expect(getValue().messages[0]).toMatchObject({ + id: "assistant-live-image-verbose-1", + runtimeTurnId: "turn-image-live-verbose-1", + imageWorkbenchPreview: expect.objectContaining({ + taskId, + status: "running", + prompt: "三国主要人物群像海报", + }), + }); + expect(getValue().messages[0]?.content).not.toContain("任务已创建成功"); + expect(getValue().messages[0]?.content).not.toContain("这里是生成详情"); + expect(getValue().messages[0]?.content).not.toContain("模型"); + expect(getValue().messages[0]?.content).not.toContain("稍等一下"); + }); + + it("图片任务事件缺少 turn_id 时,也应合并相邻旧提交文案并使用原始 @Nanobanana Pro 提示词", async () => { + let listener: CreationTaskListener | null = null; + vi.mocked(safeListen).mockImplementationOnce(async (_event, handler) => { + listener = handler; + return vi.fn(); + }); + vi.mocked(readFilePreview).mockRejectedValueOnce( + new Error("task file not ready"), + ); + + const taskId = "task-image-nano-adjacent-1"; + const expandedPrompt = + "从广州花城汇广场仰望广州塔,春天的广州塔矗立在珠江新城天际线中,前景是盛开的粉色紫荆花和绿色树木,蓝天白云,阳光明媚,摄影写实风格。"; + const { render, getValue } = renderHook( + {}, + { + initialMessages: [ + { + id: "assistant-image-nano-old-submit-1", + role: "assistant", + content: + "我来为你生成一张广州塔从花城汇视角的春天照片。图片生成任务已提交!正在为你生成从花城汇看广州塔的春天照片。", + timestamp: new Date("2026-04-04T10:07:00Z"), + isThinking: true, + contentParts: [ + { type: "thinking", text: "开始中 广州塔春天照片" }, + { + type: "text", + text: `进度:正在生成工具输入:${JSON.stringify({ + prompt: expandedPrompt, + })}`, + }, + ], + }, + ], + }, + ); + await render(); + + await act(async () => { + listener?.({ + payload: withDefaultTaskContext({ + task_id: taskId, + task_type: "image_generate", + task_family: "image", + status: "pending_submit", + path: `.lime/tasks/image_generate/${taskId}.json`, + prompt: expandedPrompt, + raw_text: + "@Nanobanana Pro 生成一张广州塔,从花城汇看过去的春天的照片", + provider_id: "fal", + model: "fal-ai/nano-banana-pro", + count: 1, + }), + }); + await Promise.resolve(); + await Promise.resolve(); + }); + + expect(getValue().messages).toHaveLength(1); + expect(getValue().messages[0]).toMatchObject({ + id: "assistant-image-nano-old-submit-1", + content: [ + "好嘞,用 Nanobanana Pro 给你生成一张广州塔,从花城汇看过去的春天的照片", + "先获取下工具参数", + "马上生成", + ].join("\n"), + isThinking: false, + contentParts: undefined, + toolCalls: undefined, + runtimeStatus: undefined, + imageWorkbenchPreview: expect.objectContaining({ + taskId, + status: "running", + prompt: "一张广州塔,从花城汇看过去的春天的照片", + providerName: "fal", + modelName: "fal-ai/nano-banana-pro", + }), + }); + expect(getValue().messages[0]?.content).not.toContain("图片生成任务已提交"); + expect(getValue().messages[0]?.content).not.toContain("粉色紫荆花"); + }); + it("同 taskId 的状态占位消息应被终态快照幂等覆盖,而不是一直停留在排队文案", async () => { let listener: CreationTaskListener | null = null; vi.mocked(safeListen).mockImplementationOnce(async (_event, handler) => { @@ -871,7 +1111,7 @@ describe("useWorkspaceImageTaskPreviewRuntime", () => { runtimeStatus: { phase: "routing", title: "图片任务进行中", - detail: "任务已提交,正在排队处理。", + detail: "正在生成图片。", checkpoints: ["创建任务文件", "轮询任务状态"], }, imageWorkbenchPreview: { @@ -879,7 +1119,7 @@ describe("useWorkspaceImageTaskPreviewRuntime", () => { prompt: "三国主要人物聚合图", status: "running", phase: "queued", - statusMessage: "任务已提交,正在排队处理。", + statusMessage: "正在生成图片。", }, }, ], @@ -904,21 +1144,18 @@ describe("useWorkspaceImageTaskPreviewRuntime", () => { expect(getValue().messages).toEqual([ expect.objectContaining({ id: "assistant-image-status-only-1", - content: "图片任务已完成,共生成 1 张。", + content: expect.stringContaining("三国主要人物聚合图"), isThinking: false, runtimeStatus: undefined, - toolCalls: [ - expect.objectContaining({ - name: "limeCreateImageGenerationTask", - status: "completed", - }), - ], + toolCalls: undefined, + contentParts: undefined, imageWorkbenchPreview: expect.objectContaining({ taskId: "task-image-status-only-1", status: "complete", phase: "succeeded", statusMessage: null, imageUrl: "https://example.com/three-kingdoms-collage.png", + caption: "搞定,已生成一张三国主要人物聚合图。", }), }), ]); @@ -973,7 +1210,7 @@ describe("useWorkspaceImageTaskPreviewRuntime", () => { imageCount: 9, layoutHint: "storyboard_3x3", phase: "queued", - statusMessage: "任务已提交,正在排队处理。", + statusMessage: "正在生成图片。", }, }), ); @@ -1035,7 +1272,7 @@ describe("useWorkspaceImageTaskPreviewRuntime", () => { imageCount: 9, layoutHint: "storyboard_3x3", phase: "queued", - statusMessage: "任务已提交,正在排队处理。", + statusMessage: "正在生成图片。", }, }, { @@ -1056,7 +1293,7 @@ describe("useWorkspaceImageTaskPreviewRuntime", () => { imageCount: 9, layoutHint: "storyboard_3x3", phase: "queued", - statusMessage: "任务已提交,正在排队处理。", + statusMessage: "正在生成图片。", }, }, ]; @@ -1146,12 +1383,16 @@ describe("useWorkspaceImageTaskPreviewRuntime", () => { expect(getValue().messages).toEqual([ expect.objectContaining({ id: "image-workbench:task-image-failed-1:assistant", - content: "图片任务失败:FAL 请求参数无效,请先调整配置。", + content: expect.stringContaining("青柠品牌 KV"), isThinking: false, + runtimeStatus: undefined, + toolCalls: undefined, + contentParts: undefined, imageWorkbenchPreview: expect.objectContaining({ taskId: "task-image-failed-1", status: "failed", statusMessage: "FAL 请求参数无效,请先调整配置。", + caption: "这次没有生成成功:FAL 请求参数无效,请先调整配置。", retryable: false, attemptCount: 1, }), @@ -1233,12 +1474,16 @@ describe("useWorkspaceImageTaskPreviewRuntime", () => { expect(getValue().messages).toEqual([ expect.objectContaining({ id: "image-workbench:task-image-edit-source-1:assistant", - content: "图片编辑任务已完成,共生成 1 张。", + content: expect.stringContaining("玻璃质感"), + toolCalls: undefined, + contentParts: undefined, + runtimeStatus: undefined, imageWorkbenchPreview: expect.objectContaining({ taskId: "task-image-edit-source-1", mode: "edit", status: "complete", imageUrl: "https://example.com/edited-lime.png", + caption: "搞定,已生成一张把主视觉里的青柠改成玻璃质感。", sourceImageUrl: "https://example.com/source-summary.png", sourceImagePrompt: "原始青柠主视觉", sourceImageRef: "img-source-1", @@ -1403,6 +1648,47 @@ describe("useWorkspaceImageTaskPreviewRuntime", () => { ]); }); + it("发送中的草稿图片轻卡不应按真实 taskId 查询 artifact", async () => { + vi.mocked(hasTauriInvokeCapability).mockReturnValue(false); + vi.mocked(hasTauriRuntimeMarkers).mockReturnValue(false); + + const { render, getValue } = renderHook( + {}, + { + initialMessages: [ + { + id: "assistant-draft-image", + role: "assistant", + content: + "好嘞,用 Nanobanana Pro 给你生成一张广州塔春天照片\n先获取下工具参数\n马上生成", + timestamp: new Date("2026-04-04T12:00:00Z"), + isThinking: true, + imageWorkbenchPreview: { + taskId: "draft-image-local-only", + prompt: "一张广州塔春天照片", + status: "running", + phase: "preparing", + modelName: "fal-ai/nano-banana-pro", + }, + }, + ], + }, + ); + await render(); + + await act(async () => { + await Promise.resolve(); + await Promise.resolve(); + }); + + expect(getMediaTaskArtifact).not.toHaveBeenCalled(); + expect(listMediaTaskArtifacts).not.toHaveBeenCalled(); + expect(getValue().messages).toHaveLength(1); + expect(getValue().messages[0]?.imageWorkbenchPreview?.taskId).toBe( + "draft-image-local-only", + ); + }); + it("应从 task artifact 恢复多模态合同路由阻止状态", async () => { const taskId = "task-image-contract-blocked-1"; vi.mocked(hasTauriInvokeCapability).mockReturnValue(false); @@ -1947,6 +2233,101 @@ describe("useWorkspaceImageTaskPreviewRuntime", () => { }); }); + it("浏览器开发模式下历史回复只有旧提交模板时,也应补捞并合并图片轻卡", async () => { + vi.setSystemTime(new Date("2026-04-04T11:08:00Z")); + vi.mocked(hasTauriInvokeCapability).mockReturnValue(false); + vi.mocked(hasTauriRuntimeMarkers).mockReturnValue(false); + vi.mocked(listMediaTaskArtifacts).mockResolvedValueOnce({ + success: true, + workspace_root: DEFAULT_PROJECT_ROOT_PATH, + artifact_root: `${DEFAULT_PROJECT_ROOT_PATH}/.lime/tasks`, + filters: { + task_family: "image", + limit: 8, + }, + total: 1, + modality_runtime_contracts: EMPTY_MODALITY_RUNTIME_CONTRACT_INDEX, + tasks: [ + createArtifactOutput({ + task_id: "task-image-browser-template-recover-1", + task_type: "image_generate", + record: withDefaultTaskContext({ + task_id: "task-image-browser-template-recover-1", + task_type: "image_generate", + task_family: "image", + status: "completed", + normalized_status: "succeeded", + created_at: "2026-04-04T11:07:00Z", + payload: { + prompt: "一颗鲜嫩的青柠,水彩插画风格", + count: 1, + size: "1024x1024", + model: "fal-ai/nano-banana-pro", + }, + result: { + images: [ + { + url: "https://example.com/browser-template-recover-lime.png", + }, + ], + }, + }), + }), + ], + }); + + const { render, getValue } = renderHook( + {}, + { + initialMessages: [ + { + id: "user-image-browser-template-recover-1", + role: "user", + content: "@配图 生成一张青柠插画", + timestamp: new Date("2026-04-04T11:07:00Z"), + }, + { + id: "assistant-image-browser-template-recover-1", + role: "assistant", + content: + "好的,我来为你生成一张青柠插画!\n\n✅ 青柠插画生成任务已创建\n\n任务 ID: task-image-browser-template-recover-1\nPrompt:一颗鲜嫩的青柠,水彩插画风格\n参数:\n🎨 风格:水彩插画\n📐 尺寸:1024×1024\n🤖 模型:fal-ai/nano-banana-pro\n🔧 Provider:fal\n任务已提交进入队列,你可以在 图片工作台(Image Workbench)中查看生成进度和最终结果。稍后如果已生成,你可以直接打开查看~", + timestamp: new Date("2026-04-04T11:07:03Z"), + }, + ], + }, + ); + await render(); + + await vi.waitFor(() => { + expect(listMediaTaskArtifacts).toHaveBeenCalledWith({ + projectRootPath: DEFAULT_PROJECT_ROOT_PATH, + taskFamily: "image", + limit: 8, + }); + }); + + await vi.waitFor(() => { + expect(getValue().messages).toEqual([ + expect.objectContaining({ + role: "user", + }), + expect.objectContaining({ + id: "assistant-image-browser-template-recover-1", + content: expect.stringContaining("一颗鲜嫩的青柠"), + imageWorkbenchPreview: expect.objectContaining({ + taskId: "task-image-browser-template-recover-1", + status: "complete", + imageUrl: "https://example.com/browser-template-recover-lime.png", + prompt: "一颗鲜嫩的青柠,水彩插画风格", + caption: "搞定,已生成一颗鲜嫩的青柠,水彩插画风格。", + }), + }), + ]); + }); + expect(getValue().messages[1]?.content).not.toContain("任务 ID"); + expect(getValue().messages[1]?.content).not.toContain("Image Workbench"); + }); + it("同一历史会话已缓存图片工作台结果时,应直接回填聊天卡而不是继续显示等待队列", async () => { const taskId = "task-image-history-cached-1"; const cachedState = { @@ -2015,9 +2396,11 @@ describe("useWorkspaceImageTaskPreviewRuntime", () => { expect(getValue().messages).toEqual([ expect.objectContaining({ id: `image-workbench:${taskId}:assistant`, - content: "图片任务已完成,共生成 1 张。", + content: expect.stringContaining("已缓存的广州主视觉"), isThinking: false, runtimeStatus: undefined, + toolCalls: undefined, + contentParts: undefined, imageWorkbenchPreview: expect.objectContaining({ taskId, status: "complete", @@ -2083,8 +2466,11 @@ describe("useWorkspaceImageTaskPreviewRuntime", () => { expect(getValue().messages).toEqual([ expect.objectContaining({ id: `image-workbench:${taskId}:assistant`, - content: "图片任务已完成,共生成 1 张。", + content: expect.stringContaining("三国人物插画"), isThinking: false, + toolCalls: undefined, + contentParts: undefined, + runtimeStatus: undefined, imageWorkbenchPreview: expect.objectContaining({ taskId, status: "complete", @@ -2149,7 +2535,7 @@ describe("useWorkspaceImageTaskPreviewRuntime", () => { prompt: "三国主要人物聚合图", status: "running", phase: "queued", - statusMessage: "任务已提交,正在排队处理。", + statusMessage: "正在生成图片。", }, }, ], @@ -2166,15 +2552,11 @@ describe("useWorkspaceImageTaskPreviewRuntime", () => { expect(getValue().messages).toEqual([ expect.objectContaining({ id: "assistant-rich-image-history-1", - content: "图片任务已完成,共生成 1 张。", + content: expect.stringContaining("三国主要人物聚合图"), isThinking: false, runtimeStatus: undefined, - toolCalls: [ - expect.objectContaining({ - name: "limeCreateImageGenerationTask", - status: "completed", - }), - ], + toolCalls: undefined, + contentParts: undefined, imageWorkbenchPreview: expect.objectContaining({ taskId, status: "complete", @@ -2255,9 +2637,11 @@ describe("useWorkspaceImageTaskPreviewRuntime", () => { expect(getValue().messages).toEqual([ expect.objectContaining({ id: `image-workbench:${taskId}:assistant`, - content: "图片任务已完成,共生成 1 张。", + content: expect.stringContaining("已缓存的三国群像"), isThinking: false, runtimeStatus: undefined, + toolCalls: undefined, + contentParts: undefined, imageWorkbenchPreview: expect.objectContaining({ taskId, status: "complete", @@ -2327,7 +2711,7 @@ describe("useWorkspaceImageTaskPreviewRuntime", () => { prompt: "三国主要人物九宫格分镜", status: "running", phase: "queued", - statusMessage: "任务已提交,正在排队处理。", + statusMessage: "正在生成图片。", }, }, ], @@ -2435,7 +2819,10 @@ describe("useWorkspaceImageTaskPreviewRuntime", () => { expect(getValue().messages).toEqual([ expect.objectContaining({ id: "image-workbench:task-image-restored:assistant", - content: "图片任务已完成,共生成 1 张。", + content: expect.stringContaining("恢复出来的青柠主视觉"), + toolCalls: undefined, + contentParts: undefined, + runtimeStatus: undefined, imageWorkbenchPreview: expect.objectContaining({ taskId: "task-image-restored", status: "complete", @@ -2909,15 +3296,15 @@ describe("useWorkspaceImageTaskPreviewRuntime", () => { expect(getValue().messages).toEqual([ expect.objectContaining({ - content: expect.stringContaining("图片任务已取消"), + content: expect.stringContaining("坐在键盘前的青柠"), isThinking: false, + runtimeStatus: undefined, + toolCalls: undefined, + contentParts: undefined, imageWorkbenchPreview: expect.objectContaining({ taskId: "task-image-cancelled-1", status: "cancelled", - }), - runtimeStatus: expect.objectContaining({ - phase: "cancelled", - title: "图片任务已取消", + caption: "已停止生成。", }), }), ]); @@ -3015,10 +3402,11 @@ describe("useWorkspaceImageTaskPreviewRuntime", () => { expect(getValue().messages).toEqual([ expect.objectContaining({ id: "image-workbench:task-image-cancelled-1:assistant", - content: "图片任务已取消。", + content: expect.stringContaining("青柠实验室主视觉"), imageWorkbenchPreview: expect.objectContaining({ taskId: "task-image-cancelled-1", status: "cancelled", + caption: "已停止生成。", }), }), ]); @@ -3043,7 +3431,7 @@ describe("useWorkspaceImageTaskPreviewRuntime", () => { expect(getValue().messages).toEqual([ expect.objectContaining({ id: "image-workbench:task-image-cancelled-1:assistant", - content: "图片任务已取消。", + content: expect.stringContaining("青柠实验室主视觉"), imageWorkbenchPreview: expect.objectContaining({ taskId: "task-image-cancelled-1", status: "cancelled", @@ -3051,16 +3439,16 @@ describe("useWorkspaceImageTaskPreviewRuntime", () => { }), expect.objectContaining({ id: "image-workbench:task-image-new-1:assistant", - content: "图片任务正在生成中。", - isThinking: true, + content: expect.stringContaining("青柠实验室主视觉"), + isThinking: false, + toolCalls: undefined, + contentParts: undefined, + runtimeStatus: undefined, imageWorkbenchPreview: expect.objectContaining({ taskId: "task-image-new-1", status: "running", prompt: "青柠实验室主视觉", }), - runtimeStatus: expect.objectContaining({ - detail: "正在绘制新一版构图", - }), }), ]); expect(getValue().imageWorkbenchState.tasks).toEqual([ diff --git a/src/components/agent/chat/workspace/useWorkspaceImageTaskPreviewRuntime.ts b/src/components/agent/chat/workspace/useWorkspaceImageTaskPreviewRuntime.ts index f12be0aab..dd41afb7c 100644 --- a/src/components/agent/chat/workspace/useWorkspaceImageTaskPreviewRuntime.ts +++ b/src/components/agent/chat/workspace/useWorkspaceImageTaskPreviewRuntime.ts @@ -36,13 +36,20 @@ import type { MessageImageWorkbenchPreview, } from "../types"; import { - buildImageWorkbenchProcessDescriptor, resolveImageWorkbenchAssistantMessageId, resolveScopedImageWorkbenchApplyTarget, type ImageWorkbenchOutput, type ImageWorkbenchTask, type SessionImageWorkbenchState, } from "./imageWorkbenchHelpers"; +import { + buildImageWorkbenchAssistantContent, + buildImageWorkbenchCaption, +} from "../utils/imageWorkbenchPresentation"; +import { + isImageWorkbenchStatusOnlyText, + isImageWorkbenchSubmissionTemplateText, +} from "../utils/imageWorkbenchStatusText"; const IMAGE_TASK_EVENT_NAME = "lime://creation_task_submitted"; const IMAGE_TASK_FILE_PREVIEW_MAX_SIZE = 256 * 1024; @@ -73,9 +80,14 @@ interface CreationTaskSubmittedPayload { mode?: string; layout_hint?: string; raw_text?: string; + provider?: string; + provider_id?: string; + model?: string; count?: number; reused_existing?: boolean; session_id?: string; + thread_id?: string; + turn_id?: string; project_id?: string; content_id?: string; entry_source?: string; @@ -183,6 +195,10 @@ interface SeedImageTaskRecord { artifactPath?: string; } +function isDraftImageWorkbenchTaskId(taskId?: string | null): boolean { + return taskId?.trim().startsWith("draft-image-") === true; +} + function contentPartContainsProcess(part: ContentPart): boolean { return part.type !== "text"; } @@ -200,7 +216,7 @@ function collectSeedImageTasks(messages?: Message[]): SeedImageTaskRecord[] { } const taskId = message.imageWorkbenchPreview?.taskId?.trim(); - if (!taskId || seen.has(taskId)) { + if (!taskId || isDraftImageWorkbenchTaskId(taskId) || seen.has(taskId)) { return; } @@ -906,6 +922,7 @@ function messageHasImageWorkbenchProcessSignal(message: Message): boolean { Boolean(message.isThinking) || Boolean(message.runtimeStatus) || Boolean(message.imageWorkbenchPreview) || + isImageWorkbenchSubmissionTemplateText(message.content) || (message.toolCalls?.length || 0) > 0 || (message.contentParts || []).some(contentPartContainsProcess) ); @@ -1031,6 +1048,19 @@ function sanitizePreviewPrompt(value?: string): string { return trimmed; } +function resolveDisplayPromptFromImageTaskPayload(params: { + payload?: Record | null; + fallbackPrompt: string; +}): string { + const rawText = sanitizePreviewPrompt( + readString([params.payload || null], ["raw_text", "rawText"]) || "", + ); + const parsedRawText = rawText ? parseImageWorkbenchCommand(rawText) : null; + const parsedPrompt = sanitizePreviewPrompt(parsedRawText?.prompt || ""); + + return parsedPrompt || rawText || params.fallbackPrompt; +} + function syncDocumentInlineImageTask(params: { taskRecord: Record; taskId: string; @@ -1218,38 +1248,6 @@ function resolveWorkbenchStatus( } } -function resolvePreviewMessageContent(params: { - taskLabel: string; - normalizedStatus: string; - successCount: number; - failureMessage?: string; -}): string { - const label = params.taskLabel; - switch (params.normalizedStatus) { - case "partial": - return `${label}已返回部分结果。`; - case "succeeded": - return params.successCount > 0 - ? `${label}已完成,共生成 ${params.successCount} 张。` - : `${label}已完成。`; - case "cancelled": - return params.failureMessage - ? `${label}已取消:${params.failureMessage}` - : `${label}已取消。`; - case "failed": - return params.failureMessage - ? `${label}失败:${params.failureMessage}` - : `${label}失败。`; - case "queued": - return `${label}已进入队列,正在等待执行。`; - case "running": - return `${label}正在生成中。`; - case "pending": - default: - return `${label}已创建,正在准备执行。`; - } -} - function resolvePendingProgressPhase(status?: string): string { switch (normalizeTaskStatus(status)) { case "partial": @@ -1291,27 +1289,6 @@ function resolvePreviewPhaseFromWorkbenchTaskStatus( } } -function resolveNormalizedStatusFromWorkbenchTaskStatus( - status: ImageWorkbenchTask["status"], -): string { - switch (status) { - case "complete": - return "succeeded"; - case "partial": - return "partial"; - case "cancelled": - return "cancelled"; - case "error": - return "failed"; - case "queued": - return "queued"; - case "routing": - case "running": - default: - return "running"; - } -} - function resolveTaskProgressPhase( progressRecord: Record | null, normalizedStatus: string, @@ -1507,6 +1484,10 @@ function buildParsedImageTaskSnapshot(params: { ["prompt", "summary", "title", "placeholder_text", "placeholderText"], ) || "", ); + const displayPrompt = resolveDisplayPromptFromImageTaskPayload({ + payload, + fallbackPrompt: prompt, + }); const fallbackProviderName = readString( [currentAttempt, payload], ["provider", "providerName", "provider_name", "providerId", "provider_id"], @@ -1651,9 +1632,23 @@ function buildParsedImageTaskSnapshot(params: { : undefined; const taskFilePath = normalizeImageTaskPath(params.taskFilePath) ?? null; const artifactPath = normalizeImageTaskPath(params.artifactPath) ?? null; + const previewProviderName = + outputs[0]?.providerName ?? fallbackProviderName ?? null; + const previewModelName = + outputs[0]?.modelName ?? + fallbackModelName ?? + runtimeContract?.model ?? + null; + const previewPrompt = displayPrompt || prompt || `${taskLabel}进行中`; + const previewCaption = buildImageWorkbenchCaption({ + prompt: previewPrompt, + status: previewStatus, + imageCount: successCount || undefined, + statusMessage: lastError || null, + }); const preview: MessageImageWorkbenchPreview = { taskId: params.taskId, - prompt: prompt || `${taskLabel}进行中`, + prompt: previewPrompt, mode: taskMode, status: previewStatus, projectId: params.projectId ?? null, @@ -1665,6 +1660,9 @@ function buildParsedImageTaskSnapshot(params: { imageCount: previewStatus === "running" ? expectedCount : successCount || undefined, expectedImageCount: expectedCount, + providerName: previewProviderName, + modelName: previewModelName, + caption: previewCaption, layoutHint, storyboardSlots: storyboardSlots.length > 0 ? storyboardSlots : undefined, sourceImageUrl, @@ -1683,88 +1681,30 @@ function buildParsedImageTaskSnapshot(params: { runtimeContract, }; - const runtimeStatus = - previewStatus === "running" - ? { - phase: - normalizedStatus === "pending" || normalizedStatus === "queued" - ? ("preparing" as const) - : ("routing" as const), - title: `${taskLabel}进行中`, - detail: - readString([progressRecord], ["message"]) || - (prompt - ? `正在处理:${prompt}` - : "任务已提交,生成完成后会自动展示结果。"), - checkpoints: ["创建任务文件", "轮询任务状态", "回填图片结果"], - } - : previewStatus === "cancelled" - ? { - phase: "cancelled" as const, - title: `${taskLabel}已取消`, - detail: lastError || "任务已停止,不会继续生成新的图片结果。", - checkpoints: [ - "已保留当前任务记录", - "可在图片画布重新生成", - "如需继续可补充新的说明", - ], - } - : previewStatus === "failed" - ? { - phase: "failed" as const, - title: `${taskLabel}失败`, - detail: lastError || "任务未返回可用结果。", - checkpoints: [ - "检查任务文件", - "查看失败详情", - "可在图片画布继续排查", - ], - } - : undefined; const messageTimestamp = new Date(createdAt); - const processDescriptor = buildImageWorkbenchProcessDescriptor({ - taskId: params.taskId, - prompt: prompt || `${taskLabel}进行中`, - mode: taskMode, - status: previewStatus, - rawText: - readString([payload], ["raw_text", "rawText"]) || - readString([params.taskRecord], ["summary"]) || - undefined, - count: expectedCount, - successCount, - size: fallbackSize, - imageUrl: outputs[0]?.url ?? null, - failureMessage: lastError, - startedAt: messageTimestamp, - endedAt: previewStatus === "running" ? undefined : messageTimestamp, - }); return { taskId: params.taskId, message: { id: resolveImageWorkbenchAssistantMessageId(params.taskId), role: "assistant", - content: resolvePreviewMessageContent({ - taskLabel, - normalizedStatus, - successCount, - failureMessage: lastError, + content: buildImageWorkbenchAssistantContent({ + prompt: previewPrompt, + mode: taskMode, + modelName: previewModelName, + expectedImageCount: expectedCount, + layoutHint, }), timestamp: messageTimestamp, - isThinking: previewStatus === "running", - toolCalls: processDescriptor.toolCalls, - contentParts: processDescriptor.contentParts, imageWorkbenchPreview: preview, - runtimeStatus, }, task: { sessionId: params.taskId, id: params.taskId, mode: taskMode, status: resolveWorkbenchStatus(normalizedStatus), - prompt: prompt || `${taskLabel}进行中`, - rawText: prompt || `${taskLabel}进行中`, + prompt: previewPrompt, + rawText: prompt || previewPrompt, expectedCount, layoutHint, storyboardSlots: storyboardSlots.length > 0 ? storyboardSlots : undefined, @@ -1818,6 +1758,18 @@ function buildPendingImageTaskSnapshot(params: { readString([params.payload || null], ["layout_hint", "layoutHint"]) || null; const taskLabel = resolveTaskLabel(params.taskType, taskMode); const startedAt = new Date(); + const previewPrompt = + resolveDisplayPromptFromImageTaskPayload({ + payload: params.payload, + fallbackPrompt: + readString([params.payload || null], ["prompt", "summary", "title"]) || + "", + }) || `${taskLabel}进行中`; + const previewModelName = + readString( + [params.payload || null], + ["model", "modelName", "model_name"], + ) || null; return ( buildParsedImageTaskSnapshot({ taskRecord: { @@ -1828,7 +1780,7 @@ function buildPendingImageTaskSnapshot(params: { payload: params.payload || {}, progress: { phase: resolvePendingProgressPhase(params.status), - message: params.progressMessage || "任务已提交,正在排队处理。", + message: params.progressMessage || "正在生成图片。", }, created_at: new Date().toISOString(), }, @@ -1844,23 +1796,28 @@ function buildPendingImageTaskSnapshot(params: { message: { id: resolveImageWorkbenchAssistantMessageId(params.taskId), role: "assistant", - content: `${taskLabel}已创建,正在准备执行。`, - timestamp: startedAt, - isThinking: true, - ...buildImageWorkbenchProcessDescriptor({ - taskId: params.taskId, - prompt: `${taskLabel}进行中`, + content: buildImageWorkbenchAssistantContent({ + prompt: previewPrompt, mode: taskMode, - status: "running", - count: expectedCount, - startedAt, + modelName: previewModelName, + expectedImageCount: expectedCount, + layoutHint, }), + timestamp: startedAt, + isThinking: false, imageWorkbenchPreview: { taskId: params.taskId, - prompt: `${taskLabel}进行中`, + prompt: previewPrompt, mode: taskMode, status: "running", expectedImageCount: expectedCount, + providerName: + readString( + [params.payload || null], + ["provider", "providerName", "provider_name", "provider_id"], + ) || null, + modelName: previewModelName, + caption: null, projectId: params.projectId ?? null, contentId: params.contentId ?? null, taskFilePath: normalizeImageTaskPath(params.taskFilePath) ?? null, @@ -1870,7 +1827,7 @@ function buildPendingImageTaskSnapshot(params: { storyboardSlots: storyboardSlots.length > 0 ? storyboardSlots : undefined, phase: resolvePendingProgressPhase(params.status), - statusMessage: params.progressMessage || "任务已提交,正在排队处理。", + statusMessage: params.progressMessage || "正在生成图片。", }, }, task: { @@ -1878,8 +1835,8 @@ function buildPendingImageTaskSnapshot(params: { id: params.taskId, mode: taskMode, status: "queued", - prompt: `${taskLabel}进行中`, - rawText: `${taskLabel}进行中`, + prompt: previewPrompt, + rawText: previewPrompt, expectedCount, layoutHint, storyboardSlots: @@ -1943,15 +1900,6 @@ function buildImageTaskSnapshotFromArtifactOutput(params: { }); } -function normalizeImageWorkbenchStatusMessageText( - value?: string | null, -): string { - return (value || "") - .trim() - .replace(/\s+/g, "") - .replace(/[。,::,;;、()()【】[\]「」『』]/g, ""); -} - function normalizeImageWorkbenchPreviewIdentityText( value?: string | null, ): string { @@ -2047,56 +1995,11 @@ function resolveImageWorkbenchPreviewIdentityKeys( } function isImageWorkbenchStatusOnlyMessage(message: Message): boolean { - const normalized = normalizeImageWorkbenchStatusMessageText(message.content); - if (!normalized) { - return true; - } - - if ( - normalized.includes("正在同步任务状态") || - normalized.includes("正在排队处理中") || - normalized.includes("状态排队中pending_submit") - ) { - return true; - } - - return [ - /^图片任务已创建正在准备执行$/, - /^图片任务已进入队列正在等待执行$/, - /^图片任务正在生成中$/, - /^图片任务已取消.*$/, - /^图片任务失败.*$/, - /^图片编辑任务已创建正在准备执行$/, - /^图片编辑任务已进入队列正在等待执行$/, - /^图片编辑任务正在生成中$/, - /^图片编辑任务已取消.*$/, - /^图片编辑任务失败.*$/, - /^图片重绘任务已创建正在准备执行$/, - /^图片重绘任务已进入队列正在等待执行$/, - /^图片重绘任务正在生成中$/, - /^图片重绘任务已取消.*$/, - /^图片重绘任务失败.*$/, - ].some((pattern) => pattern.test(normalized)); + return isImageWorkbenchStatusOnlyText(message.content); } function isImageWorkbenchSubmissionTemplateMessage(message: Message): boolean { - const normalized = normalizeImageWorkbenchStatusMessageText( - message.content, - ).toLowerCase(); - if (!normalized) { - return false; - } - - return [ - "任务详情", - "任务id", - "状态", - "模型", - "尺寸", - "提示词", - "下一步流程", - "当前状态", - ].every((keyword) => normalized.includes(keyword)); + return isImageWorkbenchSubmissionTemplateText(message.content); } function shouldReplaceImageWorkbenchMessageBody(params: { @@ -2113,9 +2016,17 @@ function shouldReplaceImageWorkbenchMessageBody(params: { previewsReferToSameImageWorkbenchTask(existingPreview, nextPreview) && isImageWorkbenchSubmissionTemplateMessage(params.existingMessage) && nextPreview.status !== "running"; + const shouldUseCanonicalImageMessageBody = + Boolean(nextPreview) && + (previewsReferToSameImageWorkbenchTask(existingPreview, nextPreview) || + (Boolean(params.existingMessage.runtimeTurnId) && + params.existingMessage.runtimeTurnId === + params.nextMessage.runtimeTurnId)); return ( + shouldUseCanonicalImageMessageBody || shouldPromoteTerminalSnapshot || + isImageWorkbenchSubmissionTemplateMessage(params.existingMessage) || params.existingMessage.id === resolveImageWorkbenchAssistantMessageId(nextPreview.taskId) || isImageWorkbenchStatusOnlyMessage(params.existingMessage) @@ -2233,11 +2144,61 @@ function dedupeImageWorkbenchPreviewMessages(messages: Message[]): Message[] { return dedupedMessages; } +function shouldMergeImageWorkbenchPreviewIntoPreviousMessage( + previous: Message | undefined, + current: Message, +): boolean { + if ( + !previous || + previous.role !== "assistant" || + current.role !== "assistant" || + !current.imageWorkbenchPreview + ) { + return false; + } + + if ( + previous.runtimeTurnId && + current.runtimeTurnId && + previous.runtimeTurnId === current.runtimeTurnId + ) { + return true; + } + + return isImageWorkbenchSubmissionTemplateMessage(previous); +} + +function mergeAdjacentImageWorkbenchPreviewMessages( + messages: Message[], +): Message[] { + const mergedMessages: Message[] = []; + + messages.forEach((message) => { + const previous = mergedMessages.at(-1); + if ( + shouldMergeImageWorkbenchPreviewIntoPreviousMessage(previous, message) + ) { + mergedMessages[mergedMessages.length - 1] = + mergeImageWorkbenchPreviewMessage({ + existingMessage: previous!, + nextMessage: message, + }); + return; + } + + mergedMessages.push(message); + }); + + return mergedMessages; +} + function finalizePreviewMessages( previousMessages: Message[], nextMessages: Message[], ): Message[] { - const dedupedMessages = dedupeImageWorkbenchPreviewMessages(nextMessages); + const dedupedMessages = mergeAdjacentImageWorkbenchPreviewMessages( + dedupeImageWorkbenchPreviewMessages(nextMessages), + ); if ( dedupedMessages.length === previousMessages.length && dedupedMessages.every( @@ -2263,79 +2224,29 @@ function buildImageWorkbenchMessagePatchFromTask(params: { | "contentParts" | "runtimeStatus" > { - const normalizedStatus = resolveNormalizedStatusFromWorkbenchTaskStatus( - params.task.status, - ); - const taskLabel = resolveTaskLabelFromMode(params.task.mode); const prompt = - params.preview.prompt || params.task.prompt || `${taskLabel}进行中`; + params.preview.prompt || + params.task.prompt || + `${resolveTaskLabelFromMode(params.task.mode)}进行中`; const startedAt = new Date(params.task.createdAt || Date.now()); - const processDescriptor = buildImageWorkbenchProcessDescriptor({ - taskId: params.task.id, - prompt, - mode: params.task.mode, - status: params.preview.status, - rawText: params.task.rawText || undefined, - count: params.task.expectedCount, - successCount: params.outputs.length, - size: params.preview.size, - imageUrl: params.preview.imageUrl || null, - failureMessage: params.task.failureMessage, - startedAt, - endedAt: params.preview.status === "running" ? undefined : startedAt, - }); return { - content: resolvePreviewMessageContent({ - taskLabel, - normalizedStatus, - successCount: params.outputs.length, - failureMessage: params.task.failureMessage || undefined, + content: buildImageWorkbenchAssistantContent({ + prompt, + mode: params.task.mode, + modelName: + params.preview.modelName || + params.outputs[0]?.modelName || + params.task.runtimeContract?.model || + null, + expectedImageCount: params.task.expectedCount, + layoutHint: params.preview.layoutHint ?? params.task.layoutHint ?? null, }), timestamp: startedAt, - isThinking: params.preview.status === "running", - toolCalls: processDescriptor.toolCalls, - contentParts: processDescriptor.contentParts, - runtimeStatus: - params.preview.status === "running" - ? { - phase: - params.task.status === "queued" - ? ("preparing" as const) - : ("routing" as const), - title: `${taskLabel}进行中`, - detail: - params.preview.statusMessage?.trim() || - (prompt - ? `正在处理:${prompt}` - : "任务已提交,生成完成后会自动展示结果。"), - checkpoints: ["创建任务文件", "轮询任务状态", "回填图片结果"], - } - : params.preview.status === "cancelled" - ? { - phase: "cancelled" as const, - title: `${taskLabel}已取消`, - detail: - params.task.failureMessage || - "任务已停止,不会继续生成新的图片结果。", - checkpoints: [ - "已保留当前任务记录", - "可在图片画布重新生成", - "如需继续可补充新的说明", - ], - } - : params.preview.status === "failed" - ? { - phase: "failed" as const, - title: `${taskLabel}失败`, - detail: params.task.failureMessage || "任务未返回可用结果。", - checkpoints: [ - "检查任务文件", - "查看失败详情", - "可在图片画布继续排查", - ], - } - : undefined, + isThinking: false, + toolCalls: undefined, + contentParts: undefined, + runtimeStatus: undefined, }; } @@ -2350,9 +2261,13 @@ function buildImageWorkbenchPreviewMessageFromTask(params: { const previewStatus = resolvePreviewStatusFromWorkbenchTask( params.task.status, ); + const previewProviderName = preferredOutput?.providerName ?? null; + const previewModelName = + preferredOutput?.modelName ?? params.task.runtimeContract?.model ?? null; + const previewPrompt = params.task.prompt || "图片任务"; const preview: MessageImageWorkbenchPreview = { taskId: params.task.id, - prompt: params.task.prompt || "图片任务", + prompt: previewPrompt, mode: params.task.mode, status: previewStatus, projectId: params.projectId ?? null, @@ -2368,6 +2283,14 @@ function buildImageWorkbenchPreviewMessageFromTask(params: { ? params.task.expectedCount : undefined, expectedImageCount: params.task.expectedCount, + providerName: previewProviderName, + modelName: previewModelName, + caption: buildImageWorkbenchCaption({ + prompt: previewPrompt, + status: previewStatus, + imageCount: outputs.length || undefined, + statusMessage: params.task.failureMessage || null, + }), layoutHint: params.task.layoutHint ?? null, storyboardSlots: params.task.storyboardSlots, sourceImageUrl: params.task.sourceImageUrl ?? null, @@ -2430,8 +2353,50 @@ function buildImageWorkbenchPreviewMessagesFromState(params: { function upsertPreviewMessage( messages: Message[], nextMessage: Message, + options?: { + runtimeTurnId?: string | null; + }, ): Message[] { const nextMessages = [...messages]; + const nextPreview = nextMessage.imageWorkbenchPreview; + const normalizedRuntimeTurnId = normalizeTaskRef(options?.runtimeTurnId); + if (nextPreview && normalizedRuntimeTurnId) { + const turnMatchedIndex = nextMessages.findIndex( + (message) => + message.role === "assistant" && + message.runtimeTurnId === normalizedRuntimeTurnId, + ); + if (turnMatchedIndex >= 0) { + const existingTurnMessage = nextMessages[turnMatchedIndex]; + const runtimeBoundNextMessage = { + ...nextMessage, + runtimeTurnId: + nextMessage.runtimeTurnId || existingTurnMessage.runtimeTurnId, + }; + const mergedMessage = mergeImageWorkbenchPreviewMessage({ + existingMessage: existingTurnMessage, + nextMessage: runtimeBoundNextMessage, + }); + nextMessages[turnMatchedIndex] = mergedMessage; + const withoutDuplicatePreviewMessages = nextMessages.filter( + (message, index) => + index === turnMatchedIndex || + message.role !== "assistant" || + !previewsReferToSameImageWorkbenchTask( + message.imageWorkbenchPreview, + nextPreview, + ), + ); + if ( + mergedMessage === existingTurnMessage && + withoutDuplicatePreviewMessages.length === messages.length + ) { + return messages; + } + return finalizePreviewMessages(messages, withoutDuplicatePreviewMessages); + } + } + const existingIndex = nextMessages.findIndex( (message) => message.id === nextMessage.id, ); @@ -2447,7 +2412,6 @@ function upsertPreviewMessage( return finalizePreviewMessages(messages, nextMessages); } - const nextPreview = nextMessage.imageWorkbenchPreview; if (nextPreview) { const taskMatchedIndex = nextMessages.findIndex( (message) => @@ -2635,6 +2599,14 @@ function patchMessagesWithImageWorkbenchState(params: { const nextPreviewStatus = resolvePreviewStatusFromWorkbenchTask( task.status, ); + const nextProviderName = + preferredOutput?.providerName ?? preview.providerName ?? null; + const nextModelName = + preferredOutput?.modelName ?? + preview.modelName ?? + task.runtimeContract?.model ?? + preview.runtimeContract?.model ?? + null; const nextPreview: MessageImageWorkbenchPreview = { ...preview, prompt: preview.prompt || task.prompt, @@ -2649,6 +2621,14 @@ function patchMessagesWithImageWorkbenchState(params: { : preview.previewImages, imageCount: outputs.length > 0 ? outputs.length : preview.imageCount, expectedImageCount: task.expectedCount || preview.expectedImageCount, + providerName: nextProviderName, + modelName: nextModelName, + caption: buildImageWorkbenchCaption({ + prompt: preview.prompt || task.prompt, + status: nextPreviewStatus, + imageCount: outputs.length || preview.imageCount, + statusMessage: task.failureMessage || preview.statusMessage || null, + }), layoutHint: task.layoutHint ?? preview.layoutHint ?? null, storyboardSlots: task.storyboardSlots ?? preview.storyboardSlots, sourceImageUrl: task.sourceImageUrl ?? preview.sourceImageUrl ?? null, @@ -3489,16 +3469,16 @@ export function useWorkspaceImageTaskPreviewRuntime({ ); const normalizedEventStatus = normalizeTaskStatus(payload.status); const progressMessage = payload.reused_existing - ? "已复用现有图片任务,正在同步最新状态。" + ? "正在生成图片。" : normalizedEventStatus === "succeeded" - ? "图片已生成完成,可在右侧查看结果。" + ? "图片生成完成。" : normalizedEventStatus === "partial" - ? "图片已返回部分结果,可在右侧继续查看。" + ? "图片返回部分结果。" : normalizedEventStatus === "failed" - ? "图片任务执行失败,可查看详情后重试。" + ? "图片生成失败。" : normalizedEventStatus === "cancelled" - ? "图片任务已取消。" - : "任务已提交,正在排队处理。"; + ? "图片生成已取消。" + : "正在生成图片。"; const pendingSnapshot = taskId && taskType && taskFamily === "image" ? buildPendingImageTaskSnapshot({ @@ -3511,9 +3491,14 @@ export function useWorkspaceImageTaskPreviewRuntime({ mode: payload.mode, layout_hint: payload.layout_hint, raw_text: payload.raw_text, + provider: payload.provider, + provider_id: payload.provider_id, + model: payload.model, count: typeof payload.count === "number" ? payload.count : undefined, session_id: payload.session_id, + thread_id: payload.thread_id, + turn_id: payload.turn_id, project_id: payload.project_id, content_id: payload.content_id, entry_source: payload.entry_source, @@ -3535,7 +3520,9 @@ export function useWorkspaceImageTaskPreviewRuntime({ : null; if (pendingSnapshot && taskId && taskType) { setChatMessages((previous) => - upsertPreviewMessage(previous, pendingSnapshot.message), + upsertPreviewMessage(previous, pendingSnapshot.message, { + runtimeTurnId: payload.turn_id, + }), ); updateCurrentImageWorkbenchState((current) => mergeImageTaskSnapshot(current, pendingSnapshot), @@ -3557,9 +3544,14 @@ export function useWorkspaceImageTaskPreviewRuntime({ mode: payload.mode, layout_hint: payload.layout_hint, raw_text: payload.raw_text, + provider: payload.provider, + provider_id: payload.provider_id, + model: payload.model, count: typeof payload.count === "number" ? payload.count : undefined, session_id: payload.session_id, + thread_id: payload.thread_id, + turn_id: payload.turn_id, project_id: payload.project_id, content_id: payload.content_id, entry_source: payload.entry_source, diff --git a/src/components/agent/chat/workspace/useWorkspaceResetRuntime.ts b/src/components/agent/chat/workspace/useWorkspaceResetRuntime.ts index d49e35c5a..173b8b239 100644 --- a/src/components/agent/chat/workspace/useWorkspaceResetRuntime.ts +++ b/src/components/agent/chat/workspace/useWorkspaceResetRuntime.ts @@ -26,6 +26,7 @@ interface ClearMessagesOptions { interface UseWorkspaceResetRuntimeParams { clearMessages: (options?: ClearMessagesOptions) => void; clearRuntimeTeamState: () => void; + clearPendingEntryA2UI?: () => void; clearProjectSelectionRuntime: () => void; resetRestoredSessionState: () => void; resetGuideState: () => void; @@ -55,6 +56,7 @@ interface UseWorkspaceResetRuntimeParams { export function useWorkspaceResetRuntime({ clearMessages, clearRuntimeTeamState, + clearPendingEntryA2UI, clearProjectSelectionRuntime, resetRestoredSessionState, resetGuideState, @@ -98,10 +100,12 @@ export function useWorkspaceResetRuntime({ const resetTopicLocalState = useCallback(() => { resetWorkbenchSurface(); + clearPendingEntryA2UI?.(); clearRuntimeTeamState(); resetRestoredSessionState(); resetGuideState(); }, [ + clearPendingEntryA2UI, clearRuntimeTeamState, resetGuideState, resetRestoredSessionState, @@ -116,11 +120,13 @@ export function useWorkspaceResetRuntime({ setSelectedText(""); setShowSidebar(true); resetWorkbenchSurface(); + clearPendingEntryA2UI?.(); setActiveTheme("general"); setCreationMode("guided"); onNavigate?.("agent", buildHomeAgentParams()); }, [ clearMessages, + clearPendingEntryA2UI, onNavigate, resetWorkbenchSurface, setActiveTheme, @@ -149,6 +155,7 @@ export function useWorkspaceResetRuntime({ autoCollapsedTopicSidebarRef.current = false; setShowSidebar(defaultTopicSidebarVisible); resetWorkbenchSurface(); + clearPendingEntryA2UI?.(); setMentionedCharacters([]); clearProjectSelectionRuntime(); resetRestoredSessionState(); @@ -161,6 +168,7 @@ export function useWorkspaceResetRuntime({ }, [ autoCollapsedTopicSidebarRef, clearMessages, + clearPendingEntryA2UI, clearProjectSelectionRuntime, defaultTopicSidebarVisible, externalProjectId, diff --git a/src/components/agent/chat/workspace/useWorkspaceSendActions.test.tsx b/src/components/agent/chat/workspace/useWorkspaceSendActions.test.tsx index 8be5d3db9..8cca2888b 100644 --- a/src/components/agent/chat/workspace/useWorkspaceSendActions.test.tsx +++ b/src/components/agent/chat/workspace/useWorkspaceSendActions.test.tsx @@ -92,6 +92,7 @@ const mockFinalizeAfterSendSuccess = vi.fn(); const mockRollbackAfterSendFailure = vi.fn(); const mockSetInput = vi.fn(); const mockSetMentionedCharacters = vi.fn(); +const mockSetChatMessages = vi.fn(); const mockSetChatToolPreferences = vi.fn(); const mockEnsureBrowserAssistCanvas = vi.fn(async () => true); const mockHandleAutoLaunchMatchedSiteSkill = vi.fn(async () => undefined); @@ -450,6 +451,7 @@ function mountHook(initialProps?: Partial): HookHarness { contentId: null, workspaceRequestMetadataBase: undefined, messages: [], + setChatMessages: mockSetChatMessages, bootstrapDispatchPreview: null, sendMessage: mockSendMessage, resolveSendBoundary: (({ sourceText }) => ({ @@ -739,6 +741,50 @@ describe("useWorkspaceSendActions", () => { } }); + it("普通视觉 brief 没有 @命令时应先确认画图,不直接进入长方案或图片主链", async () => { + const harness = mountHook(); + + try { + await act(async () => { + const started = await harness + .getValue() + .handleSend( + [], + false, + false, + "青柠新品发布海报,竖版 4:5,水彩插画风格,清爽绿色调,留出标题区:鲜榨灵感", + "react", + ); + expect(started).toBe(true); + }); + + expect(mockResolveImageWorkbenchSkillRequest).not.toHaveBeenCalled(); + expect(mockPrepareRuntimeTeamBeforeSend).not.toHaveBeenCalled(); + expect(mockSendMessage).not.toHaveBeenCalled(); + expect(mockSetChatMessages).toHaveBeenCalledTimes(1); + const applyLocalMessages = mockSetChatMessages.mock.calls[0]?.[0] as + | ((previous: Message[]) => Message[]) + | undefined; + expect(applyLocalMessages).toBeTypeOf("function"); + const nextMessages = applyLocalMessages?.([]); + expect(nextMessages?.[0]).toMatchObject({ + role: "user", + content: + "青柠新品发布海报,竖版 4:5,水彩插画风格,清爽绿色调,留出标题区:鲜榨灵感", + }); + expect(nextMessages?.[1]).toMatchObject({ + role: "assistant", + content: "这看起来是图片生成需求,要我直接调用画图功能生成吗?", + }); + expect(nextMessages?.[1]?.isThinking).toBeUndefined(); + expect(nextMessages?.[1]?.runtimeStatus).toBeUndefined(); + expect(mockSetInput).toHaveBeenCalledWith(""); + expect(mockFinalizeAfterSendSuccess).toHaveBeenCalledTimes(1); + } finally { + harness.unmount(); + } + }); + it("后端快速响应路由不应等待 Provider 列表预加载", async () => { const harness = mountHook(); @@ -1325,6 +1371,17 @@ describe("useWorkspaceSendActions", () => { }, }, }, + assistantDraft: { + content: + "好嘞,给你生成一组春日咖啡馆插画\n先获取下工具参数\n马上生成", + preserveContent: true, + imageWorkbenchPreview: { + prompt: "一张春日咖啡馆插画", + mode: "generate", + status: "running", + expectedImageCount: 2, + }, + }, }); expect(listMentionEntryUsage()).toEqual([ expect.objectContaining({ @@ -1338,6 +1395,77 @@ describe("useWorkspaceSendActions", () => { } }); + it("@Nanobanana Pro 首次发送应在同一 assistant draft 中显示极简图片轻卡", async () => { + mockResolveImageWorkbenchSkillRequest.mockReturnValueOnce({ + images: [], + requestContext: { + kind: "image_task", + image_task: { + mode: "generate", + prompt: "一张广州塔,从花城汇看过去的春天的照片", + raw_text: + "@Nanobanana Pro 生成一张广州塔,从花城汇看过去的春天的照片", + count: 1, + provider_id: "fal", + model: "fal-ai/nano-banana-pro", + }, + }, + }); + const harness = mountHook({ + input: "@Nanobanana Pro 生成一张广州塔,从花城汇看过去的春天的照片", + }); + + try { + await act(async () => { + const started = await harness.getValue().handleSend(); + expect(started).toBe(true); + }); + + const sendOptions = mockSendMessage.mock.calls[0]?.[8]; + expect(sendOptions).toMatchObject({ + assistantDraft: { + content: + "好嘞,用 Nanobanana Pro 给你生成一张广州塔,从花城汇看过去的春天的照片\n先获取下工具参数\n马上生成", + preserveContent: true, + imageWorkbenchPreview: { + prompt: "一张广州塔,从花城汇看过去的春天的照片", + mode: "generate", + status: "running", + providerName: "fal", + modelName: "fal-ai/nano-banana-pro", + expectedImageCount: 1, + }, + }, + requestMetadata: { + harness: { + image_skill_launch: { + image_task: { + model: "fal-ai/nano-banana-pro", + provider_id: "fal", + }, + }, + }, + }, + }); + expect( + sendOptions?.assistantDraft?.imageWorkbenchPreview?.taskId, + ).toMatch(/^draft-image-/); + expect(sendOptions?.assistantDraft?.content).not.toContain("任务 ID"); + expect(sendOptions?.assistantDraft?.content).not.toContain( + "Image Workbench", + ); + expect(listMentionEntryUsage()).toEqual([ + expect.objectContaining({ + kind: "builtin_command", + entryId: "image_generate_nanobanana_pro", + replayText: "一张广州塔,从花城汇看过去的春天的照片", + }), + ]); + } finally { + harness.unmount(); + } + }); + it("@Vision 1 应继续走图片生成主链,不新增视觉专家协议", async () => { mockResolveImageWorkbenchSkillRequest.mockReturnValueOnce({ images: [], diff --git a/src/components/agent/chat/workspace/useWorkspaceSendActions.ts b/src/components/agent/chat/workspace/useWorkspaceSendActions.ts index 27d571c91..8f13b19ec 100644 --- a/src/components/agent/chat/workspace/useWorkspaceSendActions.ts +++ b/src/components/agent/chat/workspace/useWorkspaceSendActions.ts @@ -73,6 +73,11 @@ import { type AgentFastResponseRoutingDecision, type AgentFastResponseMode, } from "../utils/fastResponseRouting"; +import { + PLAIN_VISUAL_BRIEF_CONFIRMATION, + shouldConfirmPlainVisualBrief, +} from "../utils/plainVisualBriefConfirmation"; +import { buildImageWorkbenchAssistantContent } from "../utils/imageWorkbenchPresentation"; import { detectBrowserTaskRequirement } from "../utils/browserTaskRequirement"; import { isTeamRuntimeRecommendation } from "../utils/contextualRecommendations"; import { @@ -87,7 +92,11 @@ import type { HandleSendOptions } from "../hooks/handleSendTypes"; import { extractAgentUiPerformanceTraceMetadata } from "../hooks/agentStreamPerformanceMetrics"; import type { UseRuntimeTeamFormationResult } from "../hooks/useRuntimeTeamFormation"; import type { SendMessageFn } from "../hooks/agentChatShared"; -import type { Message, MessageImage } from "../types"; +import type { + Message, + MessageImage, + MessageImageWorkbenchPreview, +} from "../types"; import type { TeamDefinition } from "../utils/teamDefinitions"; import type { AgentAccessMode } from "../hooks/agentChatStorage"; import { @@ -453,6 +462,118 @@ function buildFastResponseAssistantDraft( }; } +function readImageSkillLaunchContext( + requestMetadata: Record | undefined, +): Record | undefined { + const harness = asRecord(requestMetadata?.harness); + const launch = + asRecord(harness?.image_skill_launch) || + asRecord(harness?.imageSkillLaunch); + if (!launch) { + return undefined; + } + + return ( + asRecord(launch.image_task) || + asRecord(asRecord(launch.request_context)?.image_task) || + asRecord(asRecord(launch.requestContext)?.image_task) + ); +} + +function readPositiveInteger(value: unknown): number | undefined { + const numericValue = + typeof value === "number" + ? value + : typeof value === "string" + ? Number.parseInt(value, 10) + : Number.NaN; + return Number.isFinite(numericValue) && numericValue > 0 + ? Math.floor(numericValue) + : undefined; +} + +function normalizeImageWorkbenchMode( + value: unknown, +): MessageImageWorkbenchPreview["mode"] { + return value === "edit" || value === "variation" || value === "generate" + ? value + : "generate"; +} + +function buildImageWorkbenchAssistantDraft( + requestMetadata: Record | undefined, +): HandleSendOptions["assistantDraft"] { + const imageTask = readImageSkillLaunchContext(requestMetadata); + if (!imageTask) { + return undefined; + } + + const prompt = + normalizeOptionalText(imageTask.prompt as string | undefined) || + normalizeOptionalText(imageTask.raw_text as string | undefined) || + normalizeOptionalText(imageTask.rawText as string | undefined); + if (!prompt) { + return undefined; + } + + const modelName = + normalizeOptionalText(imageTask.model as string | undefined) || + normalizeOptionalText(imageTask.model_name as string | undefined) || + normalizeOptionalText(imageTask.modelName as string | undefined) || + null; + const expectedImageCount = + readPositiveInteger(imageTask.count) || + readPositiveInteger(imageTask.image_count) || + 1; + const mode = normalizeImageWorkbenchMode(imageTask.mode); + const layoutHint = + normalizeOptionalText(imageTask.layout_hint as string | undefined) || + normalizeOptionalText(imageTask.layoutHint as string | undefined) || + null; + const preview: MessageImageWorkbenchPreview = { + taskId: `draft-image-${crypto.randomUUID()}`, + prompt, + mode, + status: "running", + projectId: + normalizeOptionalText(imageTask.project_id as string | undefined) || + normalizeOptionalText(imageTask.projectId as string | undefined) || + null, + contentId: + normalizeOptionalText(imageTask.content_id as string | undefined) || + normalizeOptionalText(imageTask.contentId as string | undefined) || + null, + providerName: + normalizeOptionalText(imageTask.provider as string | undefined) || + normalizeOptionalText(imageTask.provider_name as string | undefined) || + normalizeOptionalText(imageTask.providerName as string | undefined) || + normalizeOptionalText(imageTask.provider_id as string | undefined) || + normalizeOptionalText(imageTask.providerId as string | undefined) || + null, + modelName, + imageCount: expectedImageCount, + expectedImageCount, + size: + normalizeOptionalText(imageTask.size as string | undefined) || undefined, + layoutHint, + caption: null, + phase: "preparing", + statusMessage: null, + }; + + return { + content: buildImageWorkbenchAssistantContent({ + prompt, + mode, + modelName, + expectedImageCount, + layoutHint, + }), + imageWorkbenchPreview: preview, + preserveContent: true, + }; +} + function normalizeServiceSkillUsageSlotValue( value: unknown, ): string | undefined { @@ -808,6 +929,9 @@ function resolveImageMentionCommandKey( if (trigger === "@重绘") { return "image_variation"; } + if (trigger === "@Nanobanana Pro") { + return "image_generate_nanobanana_pro"; + } if (trigger === "@配图" || trigger === "@Vision 1" || trigger === "@image") { return "image_generate"; } @@ -2975,6 +3099,7 @@ interface UseWorkspaceSendActionsParams { workspaceRequestMetadataBase?: Record; serviceModels?: ServiceModelsConfig; messages: Message[]; + setChatMessages: Dispatch>; bootstrapDispatchPreview?: InitialDispatchPreviewSnapshot | null; sendMessage: SendMessageFn; resolveSendBoundary: (input: { @@ -3033,11 +3158,23 @@ interface WorkspaceSendPlan extends WorkspaceResolvedSendState { completedSlashUsage?: CompletedInputCapabilitySlashUsage | null; } +interface WorkspaceLocalConfirmationPlan { + sourceText: string; + images: MessageImage[]; + sendBoundary: GeneralWorkbenchSendBoundaryState; + submissionPreviewKey: string | null; + confirmation: string; +} + type WorkspaceSendResolution = | { kind: "done"; result: boolean; } + | { + kind: "local_confirmation"; + plan: WorkspaceLocalConfirmationPlan; + } | { kind: "ready"; plan: WorkspaceSendPlan; @@ -3083,6 +3220,7 @@ export function useWorkspaceSendActions({ workspaceRequestMetadataBase, serviceModels, messages, + setChatMessages, bootstrapDispatchPreview, sendMessage, resolveSendBoundary, @@ -3572,6 +3710,23 @@ export function useWorkspaceSendActions({ hasBoundSkillLaunch = true; } + if ( + !sendOptions?.purpose && + !hasBoundSkillLaunch && + shouldConfirmPlainVisualBrief(sourceText) + ) { + return { + kind: "local_confirmation", + plan: { + sourceText, + images: effectiveImages, + sendBoundary, + submissionPreviewKey, + confirmation: PLAIN_VISUAL_BRIEF_CONFIRMATION, + }, + }; + } + const parsedCoverWorkbenchCommand = !sendOptions?.purpose && sourceText.trim() && @@ -5367,6 +5522,48 @@ export function useWorkspaceSendActions({ ], ); + const executeLocalConfirmationPlan = useCallback( + async (plan: WorkspaceLocalConfirmationPlan): Promise => { + const { sourceText, images, sendBoundary, submissionPreviewKey } = plan; + setRuntimeTeamDispatchPreview(null); + setInput(""); + setMentionedCharacters([]); + + setChatMessages((previous) => { + const timestamp = new Date(); + return [ + ...previous, + { + id: crypto.randomUUID(), + role: "user", + content: sourceText, + images: images.length > 0 ? images : undefined, + timestamp, + }, + { + id: crypto.randomUUID(), + role: "assistant", + content: plan.confirmation, + timestamp: new Date(), + }, + ]; + }); + + finalizeAfterSendSuccess(sendBoundary); + setSubmissionPreview((current) => + current?.key === submissionPreviewKey ? null : current, + ); + return true; + }, + [ + finalizeAfterSendSuccess, + setChatMessages, + setInput, + setMentionedCharacters, + setRuntimeTeamDispatchPreview, + ], + ); + const executeSendPlan = useCallback( async (plan: WorkspaceSendPlan): Promise => { const { @@ -5483,8 +5680,9 @@ export function useWorkspaceSendActions({ hasPurpose: Boolean(sendOptions?.purpose), hasAutoContinue: Boolean(autoContinuePayload?.enabled), }); - const fastResponseAssistantDraft = + const nextAssistantDraft = sendOptions?.assistantDraft ?? + buildImageWorkbenchAssistantDraft(nextRequestMetadata) ?? buildFastResponseAssistantDraft(fastResponseDecision); const nextSendOptions: HandleSendOptions = { ...(sendOptions || {}), @@ -5510,7 +5708,7 @@ export function useWorkspaceSendActions({ searchMode: fastResponseDecision.searchMode, }) : undefined), - assistantDraft: fastResponseAssistantDraft, + assistantDraft: nextAssistantDraft, }; logAgentDebug("WorkspaceSend", "sendMessage.start", { @@ -5638,13 +5836,16 @@ export function useWorkspaceSendActions({ if (resolution.kind === "done") { return resolution.result; } + if (resolution.kind === "local_confirmation") { + return executeLocalConfirmationPlan(resolution.plan); + } return executeSendPlan(resolution.plan); } finally { isPreparingSendRef.current = false; setIsPreparingSend(false); } }, - [executeSendPlan, resolveSendExecutionPlan], + [executeLocalConfirmationPlan, executeSendPlan, resolveSendExecutionPlan], ); const handleRecommendationClick = useCallback( diff --git a/src/components/agent/chat/workspace/useWorkspaceServiceSkillEntryActions.test.tsx b/src/components/agent/chat/workspace/useWorkspaceServiceSkillEntryActions.test.tsx index 3c1516ad2..c6c1c7737 100644 --- a/src/components/agent/chat/workspace/useWorkspaceServiceSkillEntryActions.test.tsx +++ b/src/components/agent/chat/workspace/useWorkspaceServiceSkillEntryActions.test.tsx @@ -470,6 +470,23 @@ describe("useWorkspaceServiceSkillEntryActions", () => { expect(getValue().pendingServiceSkillLaunchForm).toBeNull(); }); + it("清理当前入口状态时应移除挂起的技能补参表单", async () => { + const { render, getValue } = renderHook(); + await render(); + + act(() => { + getValue().handleServiceSkillSelect(createBrowserServiceSkill()); + }); + expect(getValue().pendingServiceSkillLaunchForm).not.toBeNull(); + + act(() => { + getValue().clearPendingServiceSkillLaunch(); + }); + + expect(getValue().pendingServiceSkillLaunchForm).toBeNull(); + expect(getValue().pendingServiceSkillLaunchSource).toBeNull(); + }); + it("站点型技能主按钮应进入 Claw 工作区并复用当前主稿", async () => { const onNavigate = vi.fn(); const recordServiceSkillUsage = vi.fn(); diff --git a/src/components/agent/chat/workspace/useWorkspaceServiceSkillEntryActions.ts b/src/components/agent/chat/workspace/useWorkspaceServiceSkillEntryActions.ts index e013b97e7..d2aba0024 100644 --- a/src/components/agent/chat/workspace/useWorkspaceServiceSkillEntryActions.ts +++ b/src/components/agent/chat/workspace/useWorkspaceServiceSkillEntryActions.ts @@ -784,6 +784,10 @@ export function useWorkspaceServiceSkillEntryActions({ [handleServiceSkillLaunch, pendingServiceSkillLaunchInput], ); + const clearPendingServiceSkillLaunch = useCallback(() => { + setPendingServiceSkillLaunchInput(null); + }, []); + const handleServiceSkillSelect = useCallback( (skill: ServiceSkillHomeItem, options?: ServiceSkillSelectionOptions) => { const replayPrefill = resolveServiceSkillLaunchPrefill({ @@ -1014,6 +1018,7 @@ export function useWorkspaceServiceSkillEntryActions({ automationJobSaving, handleServiceSkillSelect, handlePendingServiceSkillLaunchSubmit, + clearPendingServiceSkillLaunch, handleServiceSkillLaunch, handleAutoLaunchMatchedSiteSkill, handleServiceSkillBrowserRuntimeLaunch, diff --git a/src/components/layout/CrashRecoveryPanel.test.tsx b/src/components/layout/CrashRecoveryPanel.test.tsx index 2bd2e511d..108150d7c 100644 --- a/src/components/layout/CrashRecoveryPanel.test.tsx +++ b/src/components/layout/CrashRecoveryPanel.test.tsx @@ -232,7 +232,7 @@ describe("CrashRecoveryPanel", () => { const replaceState = vi.fn(); const reloadUrl = prepareReactFastRefreshHookAutoReload( "http://127.0.0.1:1420/?tab=agent", - "1.36.0", + "1.37.0", storage, ); @@ -240,12 +240,12 @@ describe("CrashRecoveryPanel", () => { expect( prepareReactFastRefreshHookAutoReload( "http://127.0.0.1:1420/?tab=agent", - "1.36.0", + "1.37.0", storage, ), ).toBeNull(); - finalizeCrashRecoveryAutoReload(reloadUrl!, "1.36.0", storage, { + finalizeCrashRecoveryAutoReload(reloadUrl!, "1.37.0", storage, { replaceState, }); @@ -257,7 +257,7 @@ describe("CrashRecoveryPanel", () => { expect( prepareReactFastRefreshHookAutoReload( "http://127.0.0.1:1420/?tab=agent", - "1.36.0", + "1.37.0", storage, ), ).toContain("__lime_resource_reload="); diff --git a/src/components/settings-v2/_layout/index.tsx b/src/components/settings-v2/_layout/index.tsx index c10534435..30a11821c 100644 --- a/src/components/settings-v2/_layout/index.tsx +++ b/src/components/settings-v2/_layout/index.tsx @@ -53,11 +53,6 @@ const AboutSection = lazy(() => default: module.AboutSection, })), ); -const ExtensionsSettings = lazy(() => - import("../agent/skills").then((module) => ({ - default: module.ExtensionsSettings, - })), -); const HotkeysSettings = lazy(() => import("../general/hotkeys").then((module) => ({ default: module.HotkeysSettings, @@ -320,7 +315,6 @@ const ACTIVE_SETTINGS_TABS = new Set([ SettingsTabs.Hotkeys, SettingsTabs.Memory, SettingsTabs.Providers, - SettingsTabs.Skills, SettingsTabs.MediaServices, SettingsTabs.McpServer, SettingsTabs.WebSearch, @@ -360,8 +354,6 @@ function preloadSettingsTab(tab: SettingsTabs): Promise | null { return import("../general/memory"); case SettingsTabs.Providers: return import("../agent/providers"); - case SettingsTabs.Skills: - return import("../agent/skills"); case SettingsTabs.MediaServices: return import("../agent/media-services"); case SettingsTabs.McpServer: @@ -457,12 +449,6 @@ function renderSettingsContent( t("settings.layout.loading.providers"), ); - case SettingsTabs.Skills: - return withSettingsContentFallback( - , - t("settings.layout.loading.skills"), - ); - case SettingsTabs.MediaServices: return withSettingsContentFallback( , diff --git a/src/components/settings-v2/home/index.test.tsx b/src/components/settings-v2/home/index.test.tsx index ad9a09f39..f12c58135 100644 --- a/src/components/settings-v2/home/index.test.tsx +++ b/src/components/settings-v2/home/index.test.tsx @@ -232,7 +232,7 @@ describe("SettingsHomePage", () => { const text = container.textContent ?? ""; expect(text).toContain("Current Entrypoints"); - expect(text).toContain("All Skills"); + expect(text).not.toContain("All Skills"); expect(text).toContain("Automation"); expect(text).toContain("Message Channels"); expect(text).toContain("Project Knowledge"); @@ -243,9 +243,6 @@ describe("SettingsHomePage", () => { const openChannelsButton = Array.from( container.querySelectorAll("button"), ).find((item) => item.textContent?.includes("Open Channels")); - const openSkillsButton = Array.from( - container.querySelectorAll("button"), - ).find((item) => item.textContent?.includes("Go to Skills")); const openResourcesButton = Array.from( container.querySelectorAll("button"), ).find((item) => item.textContent?.includes("Open Project Knowledge")); @@ -257,16 +254,12 @@ describe("SettingsHomePage", () => { openChannelsButton?.dispatchEvent( new MouseEvent("click", { bubbles: true }), ); - openSkillsButton?.dispatchEvent( - new MouseEvent("click", { bubbles: true }), - ); openResourcesButton?.dispatchEvent( new MouseEvent("click", { bubbles: true }), ); }); expect(onTabChange).not.toHaveBeenCalled(); - expect(onNavigate).toHaveBeenCalledWith("skills"); expect(onNavigate).toHaveBeenCalledWith("automation"); expect(onNavigate).toHaveBeenCalledWith("channels"); expect(onNavigate).toHaveBeenCalledWith("resources"); diff --git a/src/components/settings-v2/home/index.tsx b/src/components/settings-v2/home/index.tsx index 7b28df9cc..d0c7b82c2 100644 --- a/src/components/settings-v2/home/index.tsx +++ b/src/components/settings-v2/home/index.tsx @@ -1,6 +1,5 @@ import { ArrowRight, - Blocks, Bot, Brain, Image as ImageIcon, @@ -81,11 +80,6 @@ const quickAccessMeta = { descriptionKey: "settings.home.quickAccess.providers.description", icon: Brain, }, - [SettingsTabs.Skills]: { - titleKey: "settings.home.quickAccess.skills.title", - descriptionKey: "settings.home.quickAccess.skills.description", - icon: Blocks, - }, [SettingsTabs.MediaServices]: { titleKey: "settings.home.quickAccess.mediaServices.title", descriptionKey: "settings.home.quickAccess.mediaServices.description", @@ -320,15 +314,6 @@ export function SettingsHomePage({
- {onNavigate ? ( - - ) : null} {onNavigate ? (
- + + handleSkillAutoLoadChange(skill, nextEnabled) + } + aria-label={t("skills.workspace.autoLoad.aria", { + name: skill.name, + })} + /> +
); }; return ( <> -
-
-
-
+
+
+ + + + +
+ +
+
+
-

- {t("skills.workspace.header.title")} +

+ {t("skills.workspace.header.title", "技能")}

-

- {t("skills.workspace.header.subtitle")} +

+ {t( + "skills.workspace.header.subtitle", + "安装和管理技能,需要时带回首页输入框使用。", + )}

- -
- - -
-
- -
- {activeScaffoldDraft ? ( -
-
-
- - {t("skills.workspace.activeScaffold.badge")} - - - {activeScaffoldTitle} - - {activeScaffoldSummary ? ( - - {t("skills.workspace.activeScaffold.summary", { - summary: summarizeRecentReplayText( - activeScaffoldSummary, - ), - })} - - ) : null} - {activeScaffoldReplayText ? ( - - {t("skills.workspace.activeScaffold.replay", { - summary: summarizeRecentReplayText( - activeScaffoldReplayText, - ), - })} - - ) : null} -
-
- - -
+
+
+ +
+
+
+ {t("skills.workspace.hero.title", "为你精选的职场技能")}
+

+ {t( + "skills.workspace.hero.description", + "覆盖写作、效率、设计、数据分析等多种场景,一键安装。", + )} +

+
+
+
+ + {activeScaffoldDraft ? ( +
+
+
+ + {t("skills.workspace.activeScaffold.badge", "新技能")} + + + {activeScaffoldTitle} + +
+ +
+
+ ) : null} + + {(serviceSkillsError || localSkillsError) && ( +
+ {serviceSkillsError + ? t("skills.workspace.error.serviceSkills", { + message: serviceSkillsError, + }) + : null} + {serviceSkillsError && localSkillsError + ? t("skills.workspace.error.separator") + : null} + {localSkillsError + ? t("skills.workspace.error.localSkills", { + message: localSkillsError, + }) + : null} +
+ )} + +
- + - {(serviceSkillsError || localSkillsError) && ( -
- {serviceSkillsError - ? t("skills.workspace.error.serviceSkills", { - message: serviceSkillsError, - }) - : null} - {serviceSkillsError && localSkillsError - ? t("skills.workspace.error.separator") - : null} - {localSkillsError - ? t("skills.workspace.error.localSkills", { - message: localSkillsError, - }) - : null} -
- )} - -
-
-
-
-

- {t("skills.workspace.recommendation.title")} + {activeView === "store" ? ( +
+
+

+ {t("skills.workspace.featured.title", "官方精选")}

- - {t("skills.workspace.recommendation.subtitle")} - -
- - {reviewRecommendationBanner ? ( -
-
-
- {t("skills.workspace.reviewBanner.title", { - title: reviewRecommendationBanner.title, + {visibleFeaturedCuratedTaskTemplates.length > 0 ? ( +
+ {visibleFeaturedCuratedTaskTemplates + .slice(0, 9) + .map((featured, index) => { + const template = featured.template; + return ( + + ); })} -
-
- {reviewRecommendationBanner.summary} -
-
- {t("skills.workspace.reviewBanner.nextSteps", { - nextSteps: reviewRecommendationBanner.nextSteps, + ) : ( +
+ {t("skills.workspace.empty.resultTemplates.default")} +
+ )} +

+ + {visibleStoreSkills.length > 0 ? ( +
+

+ {t("skills.workspace.store.other" as AgentI18nKey, { + count: visibleStoreSkills.length, + })} +

+
+ {visibleStoreSkills.map((skill) => { + const tone = resolveSkillCardTone(skill); + return ( + + ); })}
- {reviewRecommendationBanner.actionLabel && - reviewRecommendationBanner.onAction ? ( - - ) : null} -
+
) : null} +
+ ) : null} - {visibleFeaturedCuratedTaskTemplates.length > 0 ? ( -
- {visibleFeaturedCuratedTaskTemplates.map( - (featured, index) => { - const template = featured.template; - const isPrimaryRecommendation = index === 0; - const launchPrefill = - resolveCuratedTaskTemplateLaunchPrefill( - template, - curatedTaskPresentationCopy, - ); - const reviewPrefillSnapshot = - buildSceneAppExecutionReviewPrefillSnapshot({ - referenceEntries: launchPrefill?.referenceEntries, - taskId: template.id, - }); - const reviewPrefillHighlights = - buildSceneAppExecutionReviewPrefillHighlights( - reviewPrefillSnapshot, - ); - const recentUsageDescription = - buildCuratedTaskRecentUsageDescription({ - copy: curatedTaskPresentationCopy, - task: template, - prefill: launchPrefill, - }); - const compactReasonSummary = - featured.reasonSummary || recentUsageDescription; - const compactBaselineSummary = - formatCompactReviewBaselineSummary({ - sourceTitle: reviewPrefillSnapshot?.sourceTitle, - highlights: reviewPrefillHighlights, - }); - const requiredSummary = - summarizeCuratedTaskRequiredInputs( - template, - 2, - curatedTaskPresentationCopy, - ); - const outputSummary = - summarizeCuratedTaskOutputContract( - template, - 2, - curatedTaskPresentationCopy, - ); - const followUpSummary = - summarizeCuratedTaskFollowUpActions( - template, - 2, - curatedTaskPresentationCopy, - ); - const resultDestination = - getCuratedTaskOutputDestination(template); - - return ( -
+
+

+ {t("skills.workspace.builtin.title", "内置技能")} +

+

+ {t( + "skills.workspace.builtin.subtitle", + "Lime 自带的技能。开关只控制是否自动匹配,不影响你继续使用 Lime。", + )} +

+
+ {visibleBuiltinLocalSkills.length > 0 ? ( +
+ {visibleBuiltinLocalSkills.map((skill) => ( +
+ +
+
+ {skill.name} +
+

+ {skill.description || + resolveInstalledSkillPromise( + skill, + installedSkillPresentationCopy, + )} +

+

+ {t( + "skills.workspace.autoLoad.description", + "开启后,Lime 会在相关任务里自动带上;关闭后,不会主动匹配。", )} - > -

-
-

- {template.title} -

-

- {template.summary} -

-
- {isPrimaryRecommendation ? ( - - {t( - "skills.workspace.featured.recommendedBadge", - )} - - ) : null} +

+
+ {renderAutoLoadControl(skill)} +
+ ))} +
+ ) : ( +
+ {t( + "skills.workspace.builtin.empty", + "可以尝试刷新,或换个关键词继续找。", + )} +
+ )} + + ) : null} + + {activeView === "installed" ? ( +
+
+

+ {t("skills.workspace.installed.title", "用户安装")} +

+

+ {t( + "skills.workspace.installed.subtitle", + "你安装的技能。自动加载、手动使用、卸载是三个独立操作。", + )} +

+
+ {visibleUserInstalledSkills.length > 0 ? ( +
+ {visibleUserInstalledSkills.map((skill) => { + const isHighlighted = + skill.directory === highlightedInstalledSkillDirectory; + return ( +
+ +
+
+ {skill.name}
-
- {featured.reasonLabel || compactReasonSummary ? ( -
- {[featured.reasonLabel, compactReasonSummary] - .filter((segment): segment is string => - Boolean(segment && segment.trim()), - ) - .join(" · ")} -
- ) : null} - {reviewPrefillHighlights.length > 0 ? ( -
-
- - {t( - "skills.workspace.featured.reuseResult", - )} - - - {compactBaselineSummary} - -
-
- {t( - "skills.workspace.featured.currentBaseline", - { - sourceTitle: - reviewPrefillSnapshot?.sourceTitle || - t( - "skills.workspace.featured.defaultSourceTitle", - ), - }, - )} - {reviewPrefillHighlights.map((item) => ( -
- {item} -
- ))} -
-
- ) : null} -
-
-
-
- {template.outputHint || outputSummary} -
- - {requiredSummary} - {resultDestination} - {followUpSummary} - -
+

+ {skill.description || + resolveInstalledSkillPromise( + skill, + installedSkillPresentationCopy, + )} +

+

+ {t( + "skills.workspace.autoLoad.description", + "开启后,Lime 会在相关任务里自动带上;关闭后,不会主动匹配。", + )} +

+
+
+ {renderAutoLoadControl(skill)} + + {skill.sourceKind !== "builtin" && + skill.catalogSource !== "project" ? ( -
-
- ); - }, - )} -
- ) : ( -
- {hasSidebarSearchResults - ? t("skills.workspace.empty.resultTemplates.hasSidebar") - : t("skills.workspace.empty.resultTemplates.default")} -
- )} - - - {selectedGroup ? ( - <> -
-
-
-

- {selectedGroup.title} -

-

- {t("skills.workspace.group.selectedSubtitle")} -

- - {selectedGroup.summary} - {selectedGroup.entryHint} - -
- -
-
- - {visibleGroupSkills.length > 0 ? ( -
- {visibleGroupSkills.map(renderSkillCard)} -
- ) : ( -
-
- {t("skills.workspace.group.emptyTitle")} -
-

- {t("skills.workspace.group.emptyDescription")} -

-
- )} - - ) : visibleGroups.length > 0 ? ( -
-
-
-

- {t("skills.workspace.categories.title")} -

- -
-
- -
- {visibleGroups.map((group) => { - const groupSkills = - recommendedSkillGroupMap.get(group.key) ?? []; - const hasRecommendedGroupSkills = groupSkills.length > 0; - const starterSummary = hasRecommendedGroupSkills - ? formatSkillGroupStarterSummary(groupSkills) - : t("skills.workspace.categories.defaultStarter"); - - return ( -
-
-

- {group.title} -

-
- {starterSummary} -
- - {group.summary} - {group.themeTarget} - {group.entryHint} - -
- -
- -
-
- ); - })} -
-
- ) : ( -
-
- {hasSidebarSearchResults - ? t("skills.workspace.empty.skillGroups.hasSidebarTitle") - : t("skills.workspace.empty.skillGroups.defaultTitle")} -
-

- {hasSidebarSearchResults - ? t( - "skills.workspace.empty.skillGroups.hasSidebarDescription", - ) - : t( - "skills.workspace.empty.skillGroups.defaultDescription", - )} -

-
- )} -
- - - -
+ ) : null} +
+
- - -
- -
- {t("skills.workspace.manager.title")} - -
-
-
- -
-
-
-
+ { + setScaffoldDialogOpen(open); + if (!open) { + setScaffoldDialogDraft(null); + } + }} + onCreate={handleCreateScaffold} + creating={scaffoldCreating} + allowProjectTarget={Boolean(creationProjectId)} + initialValues={scaffoldDialogDraft} + sourceHint={scaffoldDialogDraft?.sourceExcerpt ?? null} + onBringBackToCreation={handleBringScaffoldToCreation} + /> - - ); } diff --git a/src/features/capability-drafts/components/CapabilityDraftPanel.tsx b/src/features/capability-drafts/components/CapabilityDraftPanel.tsx index e156a91c3..dc9504e90 100644 --- a/src/features/capability-drafts/components/CapabilityDraftPanel.tsx +++ b/src/features/capability-drafts/components/CapabilityDraftPanel.tsx @@ -155,35 +155,35 @@ export function CapabilityDraftPanel({ registered: { description: t( "capabilityDraft.panel.status.registered.description", - "已写入当前 Workspace 的本地 Skill 目录;运行与自动化仍需后续 runtime gate。", + "已经保存到当前项目,可以去“已保存技能”里试用。", ), - label: t("capabilityDraft.panel.status.registered.label", "已注册"), + label: t("capabilityDraft.panel.status.registered.label", "已保存"), }, unverified: { description: t( "capabilityDraft.panel.status.unverified.description", - "只能查看和继续修复,不能运行、注册或接入自动化。", + "还没有检查,先确认内容是否安全可用。", ), - label: t("capabilityDraft.panel.status.unverified.label", "未验证"), + label: t("capabilityDraft.panel.status.unverified.label", "待检查"), }, verification_failed: { description: t( "capabilityDraft.panel.status.verificationFailed.description", - "verification gate 发现结构、权限或 contract 问题,需要修复后重试。", + "检查没有通过,需要修改后再保存。", ), label: t( "capabilityDraft.panel.status.verificationFailed.label", - "验证未通过", + "检查未通过", ), }, verified_pending_registration: { description: t( "capabilityDraft.panel.status.verifiedPendingRegistration.description", - "最小验证已通过,可以注册到当前 Workspace,但仍不会运行或接入自动化。", + "检查已通过,可以保存到当前项目。", ), label: t( "capabilityDraft.panel.status.verifiedPendingRegistration.label", - "验证通过,待注册", + "检查通过,待保存", ), }, }, @@ -393,16 +393,16 @@ export function CapabilityDraftPanel({
- {t("capabilityDraft.panel.badge", "草案区")} + {t("capabilityDraft.panel.badge", "待确认")}

- {t("capabilityDraft.panel.title", "能力草案")} + {t("capabilityDraft.panel.title", "正在制作")}

{t( "capabilityDraft.panel.description", - "Coding Agent 产出的 Skill 草案先停在这里;未验证前不会注册,也不会自动运行。", + "AI 生成的新技能会先放在这里。检查通过后,才能保存到项目里使用。", )}

@@ -428,25 +428,25 @@ export function CapabilityDraftPanel({
{t( "capabilityDraft.panel.empty.missingProject", - "选择或进入一个项目后,才能查看该项目里的能力草案。", + "选择或进入一个项目后,才能查看正在制作的技能。", )}
) : effectiveError ? (
{t("capabilityDraft.panel.empty.error", { - defaultValue: "能力草案暂时没读到:{{message}}", + defaultValue: "正在制作的技能暂时没读到:{{message}}", message: effectiveError, })}
) : isBusy ? (
- {t("capabilityDraft.panel.empty.loading", "正在读取能力草案...")} + {t("capabilityDraft.panel.empty.loading", "正在读取制作中的技能...")}
) : visibleDrafts.length === 0 ? (
{t( "capabilityDraft.panel.empty.noDrafts", - "当前项目还没有能力草案。后续 Coding Agent 生成的新能力会先进入这里复核。", + "当前项目还没有正在制作的技能。以后 AI 生成的新技能会先放到这里确认。", )}
) : ( @@ -491,123 +491,146 @@ export function CapabilityDraftPanel({ {draft.description || draft.userGoal}

-
-
- - {t("capabilityDraft.panel.field.goal", "目标:")} - - {draft.userGoal} -
-
- - {t("capabilityDraft.panel.field.permissions", "权限:")} - - {summarizeCapabilityDraftPermissions( - draft, - presentationCopy, - )} -
-
- - {t("capabilityDraft.panel.field.files", "文件:")} - - {summarizeCapabilityDraftFiles(draft, presentationCopy)} -
-
- - {t("capabilityDraft.panel.field.verification", "验证:")} - - {summarizeCapabilityDraftVerification( - draft, - presentationCopy, - )} -
-
- - {t("capabilityDraft.panel.field.registration", "注册:")} - - {summarizeCapabilityDraftRegistration( - draft, - presentationCopy, - )} -
-
- {status.description} - {!canRun && - !canRegister && - draft.verificationStatus !== "registered" - ? t( - "capabilityDraft.panel.guard.noEntry", - " 当前没有运行、注册或自动化入口。", - ) - : null} - {canRegister - ? t( - "capabilityDraft.panel.guard.registerOnly", - " 注册只会复制为 Workspace 本地 Skill,不会立即运行。", - ) - : null} - {draft.verificationStatus === "registered" - ? t( - "capabilityDraft.panel.guard.registeredNoEntry", - " 当前没有运行或自动化入口。", - ) - : null} -
-
- {evidenceChecks.length > 0 ? ( -
-
- - {t("capabilityDraft.panel.evidence.title", "验证证据")} +

+ {status.description} + {canRegister + ? t( + "capabilityDraft.panel.guard.registerOnly", + " 保存只会放到当前项目,不会自动运行。", + ) + : null} +

+
+ + {t("capabilityDraft.panel.details", "制作详情")} + +
+
+ + {t("capabilityDraft.panel.field.goal", "目标:")} - + {draft.userGoal} +
+
+ + {t("capabilityDraft.panel.field.permissions", "权限:")} + + {summarizeCapabilityDraftPermissions( + draft, + presentationCopy, + )} +
+
+ + {t("capabilityDraft.panel.field.files", "文件:")} + + {summarizeCapabilityDraftFiles(draft, presentationCopy)} +
+
+ {t( - "capabilityDraft.panel.evidence.subtitle", - "本次 verification report", + "capabilityDraft.panel.field.verification", + "验证:", )} + {summarizeCapabilityDraftVerification( + draft, + presentationCopy, + )}
-
- {evidenceChecks.slice(0, 2).map((check) => ( -
-
- {check.label || check.id} -
-
- {check.evidence.slice(0, 6).map((evidence) => ( -
-
- {formatEvidenceKey( - evidence.key, - evidenceLabels, - )} -
-
- {formatEvidenceValue(evidence)} -
-
- ))} -
-
- ))} +
+ + {t( + "capabilityDraft.panel.field.registration", + "注册:", + )} + + {summarizeCapabilityDraftRegistration( + draft, + presentationCopy, + )} +
+
+ {status.description} + {!canRun && + !canRegister && + draft.verificationStatus !== "registered" + ? t( + "capabilityDraft.panel.guard.noEntry", + " 当前没有运行、注册或自动化入口。", + ) + : null} + {canRegister + ? t( + "capabilityDraft.panel.guard.registerOnly", + " 注册只会复制为 Workspace 本地 Skill,不会立即运行。", + ) + : null} + {draft.verificationStatus === "registered" + ? t( + "capabilityDraft.panel.guard.registeredNoEntry", + " 当前没有运行或自动化入口。", + ) + : null}
- ) : null} + {evidenceChecks.length > 0 ? ( +
+
+ + {t( + "capabilityDraft.panel.evidence.title", + "验证证据", + )} + + + {t( + "capabilityDraft.panel.evidence.subtitle", + "本次 verification report", + )} + +
+
+ {evidenceChecks.slice(0, 2).map((check) => ( +
+
+ {check.label || check.id} +
+
+ {check.evidence.slice(0, 6).map((evidence) => ( +
+
+ {formatEvidenceKey( + evidence.key, + evidenceLabels, + )} +
+
+ {formatEvidenceValue(evidence)} +
+
+ ))} +
+
+ ))} +
+
+ ) : null} +
{canVerify || canRegister ? (

{canRegister ? t( "capabilityDraft.panel.action.registerHelp", - "注册只写当前 Workspace 的 .agents/skills,不接运行或自动化。", + "保存后会出现在“已保存技能”里,你可以再选择试用或设置定时运行。", ) : t( "capabilityDraft.panel.action.verifyHelp", - "只做静态门禁检查,不执行草案脚本。", + "先检查结构和权限,不会自动运行这条技能。", )}

@@ -630,7 +653,7 @@ export function CapabilityDraftPanel({ isVerifying && "animate-spin", )} /> - {t("capabilityDraft.panel.action.verify", "运行验证")} + {t("capabilityDraft.panel.action.verify", "检查")} ) : null} {canRegister ? ( @@ -653,7 +676,7 @@ export function CapabilityDraftPanel({ /> {t( "capabilityDraft.panel.action.register", - "注册到 Workspace", + "保存到项目", )} ) : null} diff --git a/src/features/capability-drafts/components/WorkspaceRegisteredSkillsPanel.test.tsx b/src/features/capability-drafts/components/WorkspaceRegisteredSkillsPanel.test.tsx index b7785b7d1..60ebe4d79 100644 --- a/src/features/capability-drafts/components/WorkspaceRegisteredSkillsPanel.test.tsx +++ b/src/features/capability-drafts/components/WorkspaceRegisteredSkillsPanel.test.tsx @@ -138,7 +138,7 @@ describe("WorkspaceRegisteredSkillsPanel", () => { it("没有项目根目录时只显示选择项目提示,不读取已注册能力", () => { const { container } = renderPanel(); - expect(container.textContent).toContain("Workspace 已注册能力"); + expect(container.textContent).toContain("已保存技能"); expect(container.textContent).toContain("选择或进入一个项目"); expect(capabilityDraftsApi.listRegisteredSkills).not.toHaveBeenCalled(); expect(listWorkspaceSkillBindings).not.toHaveBeenCalled(); @@ -459,7 +459,7 @@ describe("WorkspaceRegisteredSkillsPanel", () => { }); expect(container.textContent).toContain("只读 CLI 报告"); expect(container.textContent).toContain("已注册"); - expect(container.textContent).toContain("P3C binding 候选"); + expect(container.textContent).toContain("可试用"); expect(container.textContent).toContain("capdraft-1 / capver-1"); expect(container.textContent).toContain("注册 provenance"); expect(container.textContent).toContain("只读 HTTP 执行 preflight"); @@ -555,14 +555,12 @@ describe("WorkspaceRegisteredSkillsPanel", () => { "当前仍未注入 Query Loop 或 tool_runtime", ); expect(container.textContent).toContain("manual_runtime_enable"); - expect(container.textContent).toContain("Agent envelope 草案"); - expect(container.textContent).toContain("等待成功运行"); + expect(container.textContent).toContain("项目助手"); + expect(container.textContent).toContain("待试用"); expect(container.textContent).toContain( - "成功运行后可把 Skill、权限、手动 rerun 和 evidence 组合成 Workspace Agent envelope。", - ); - expect(container.textContent).toContain( - "Evidence:还没有成功运行证据;先通过本回合启用拿到一次结果。", + "试用结果没问题后,可以把它保存成当前项目里的助手。", ); + expect(container.textContent).toContain("最近结果:还没有试用结果。"); expect(container.textContent).toContain("Managed Job:未创建"); expect(container.textContent).not.toContain("立即运行"); expect(container.textContent).not.toContain("创建自动化"); @@ -927,11 +925,13 @@ describe("WorkspaceRegisteredSkillsPanel", () => { ) as HTMLButtonElement | null; expect(managedButton).toBeTruthy(); expect(managedButton?.disabled).toBe(false); - expect(container.textContent).toContain("Agent card:等待 evidence-ready"); - expect(container.textContent).toContain("Sharing:未完成审计前"); - expect(container.textContent).toContain("Discovery:同 workspace 成员"); - expect(container.textContent).toContain("Memory:引用 verification report"); - expect(container.textContent).toContain("Widget:等待运行后展示状态"); + expect(container.textContent).toContain("项目助手:试用通过后再保存。"); + expect(container.textContent).toContain("共享:先只对你可见。"); + expect(container.textContent).toContain( + "团队可见性:当前项目成员可以使用这条技能。", + ); + expect(container.textContent).toContain("记录:已保留检查结果"); + expect(container.textContent).toContain("结果:试用后展示最近状态"); expect(container.textContent).toContain("Managed Job:草案暂停"); expect(container.textContent).toContain("Schedule:Cron 0 9 * * *"); expect(container.textContent).toContain("Managed Objective:paused"); @@ -943,7 +943,7 @@ describe("WorkspaceRegisteredSkillsPanel", () => { ), ).toHaveLength(1); expect(toggleButton).toBeTruthy(); - expect(toggleButton?.textContent).toContain("恢复 Managed Job"); + expect(toggleButton?.textContent).toContain("开启定时运行"); expect(auditButton).toBeTruthy(); await act(async () => { @@ -983,10 +983,12 @@ describe("WorkspaceRegisteredSkillsPanel", () => { expect(exportAgentRuntimeEvidencePack).toHaveBeenCalledWith("session-1"); expect(container.textContent).toContain("completion audit completed"); expect(container.textContent).toContain( - "Agent card:workspace-local/capability-report", + "项目助手:已准备好保存(capability-report)", + ); + expect(container.textContent).toContain("共享:可在当前项目内共享"); + expect(container.textContent).toContain( + "团队可见性:当前项目成员可以使用这条技能。", ); - expect(container.textContent).toContain("workspace / team 内共享"); - expect(container.textContent).toContain("复用同一 Managed Job / evidence"); const envelopeButton = container.querySelector( '[data-testid="workspace-registered-agent-envelope-action"]', @@ -1274,10 +1276,10 @@ describe("WorkspaceRegisteredSkillsPanel", () => { ) as HTMLButtonElement | null; expect(initialEnvelopeButton).toBeTruthy(); expect(initialEnvelopeButton?.disabled).toBe(true); - expect(container.textContent).toContain("Agent card:等待 evidence-ready"); - expect(container.textContent).toContain("Sharing:未完成审计前"); + expect(container.textContent).toContain("项目助手:试用通过后再保存。"); + expect(container.textContent).toContain("共享:先只对你可见。"); expect(container.textContent).not.toContain( - "Agent card:workspace-local/capability-report", + "项目助手:已准备好保存(capability-report)", ); await act(async () => { @@ -1300,11 +1302,12 @@ describe("WorkspaceRegisteredSkillsPanel", () => { expect(completedEnvelopeButton?.disabled).toBe(false); expect(container.textContent).toContain("completion audit completed"); expect(container.textContent).toContain( - "Agent card:workspace-local/capability-report", + "项目助手:已准备好保存(capability-report)", + ); + expect(container.textContent).toContain("共享:可在当前项目内共享"); + expect(container.textContent).toContain( + "团队可见性:当前项目成员可以使用这条技能。", ); - expect(container.textContent).toContain("workspace / team 内共享"); - expect(container.textContent).toContain("不进入 public Marketplace"); - expect(container.textContent).toContain("同 workspace 成员"); await act(async () => { completedEnvelopeButton?.click(); @@ -1446,9 +1449,7 @@ describe("WorkspaceRegisteredSkillsPanel", () => { await act(async () => { await Promise.resolve(); }); - expect(container.textContent).toContain( - "当前项目还没有通过 P3A 注册的能力", - ); + expect(container.textContent).toContain("当前项目还没有已保存技能"); await act(async () => { root.render( diff --git a/src/features/capability-drafts/components/WorkspaceRegisteredSkillsPanel.tsx b/src/features/capability-drafts/components/WorkspaceRegisteredSkillsPanel.tsx index 89e55a87b..08de2a52f 100644 --- a/src/features/capability-drafts/components/WorkspaceRegisteredSkillsPanel.tsx +++ b/src/features/capability-drafts/components/WorkspaceRegisteredSkillsPanel.tsx @@ -215,6 +215,94 @@ function buildReadonlyHttpApprovalPreview( }; } +function summarizePermissionForUser( + skill: WorkspaceRegisteredSkillRecord, + copy: { + defaultReadonly: string; + readonly: string; + draftWrite: string; + localCommand: string; + }, +) { + if (skill.permissionSummary.length === 0) { + return copy.defaultReadonly; + } + + const labels = new Set(); + for (const item of skill.permissionSummary) { + const normalized = item.toLowerCase(); + if (normalized.includes("level 0") || item.includes("只读")) { + labels.add(copy.readonly); + continue; + } + if ( + normalized.includes("level 1") || + normalized.includes("draft") || + normalized.includes("write") || + item.includes("草案") || + item.includes("写入") + ) { + labels.add(copy.draftWrite); + continue; + } + if ( + normalized.includes("cli") || + normalized.includes("command") || + item.includes("命令") + ) { + labels.add(copy.localCommand); + continue; + } + labels.add(item.replace(/^Level\s+\d+\s*/i, "").trim() || item); + } + + return Array.from(labels).slice(0, 2).join(" / "); +} + +function getUserScheduleLabel( + job: AutomationJobRecord | undefined, + copy: { + enabled: string; + notCreated: string; + paused: string; + }, +) { + if (!job) { + return copy.notCreated; + } + return job.enabled ? copy.enabled : copy.paused; +} + +function getUserResultLabel( + job: AutomationJobRecord | undefined, + completionAuditSummary: AgentRuntimeCompletionAuditSummary | undefined, + copy: { + auditBlocked: string; + auditPassed: string; + noJob: string; + noRun: string; + readyToCheck: string; + running: string; + }, +) { + if (completionAuditSummary?.decision === "completed") { + return copy.auditPassed; + } + if (completionAuditSummary) { + return copy.auditBlocked; + } + if (!job) { + return copy.noJob; + } + if (job.last_status === "running") { + return copy.running; + } + if (job.last_status === "success") { + return copy.readyToCheck; + } + return copy.noRun; +} + function sortRegisteredSkills( skills: WorkspaceRegisteredSkillRecord[], ): WorkspaceRegisteredSkillRecord[] { @@ -434,44 +522,43 @@ function WorkspaceRegisteredSkillCard({ () => ({ actionBlocked: t( "capabilityDraft.registeredPanel.agentEnvelope.action.blocked", - "先解除阻塞", + "先处理问题", ), actionDraft: t( "capabilityDraft.registeredPanel.agentEnvelope.action.draft", - "转成 Agent 草案", + "保存成项目助手", ), actionManualEnable: t( "capabilityDraft.registeredPanel.agentEnvelope.action.manualEnable", - "先本回合启用", + "先试用一次", ), agentCardPending: t( "capabilityDraft.registeredPanel.agentEnvelope.agentCard.pending", - "Agent card:等待 evidence-ready 后派生,不创建平行持久化实体。", + "项目助手:试用通过后再保存。", ), blockedReasonFallback: t( "capabilityDraft.registeredPanel.agentEnvelope.blockedReasonFallback", - "runtime binding 当前被 gate 阻断", + "当前还有问题需要处理", ), description: t( "capabilityDraft.registeredPanel.agentEnvelope.description", - "成功运行后可把 Skill、权限、手动 rerun 和 evidence 组合成 Workspace Agent envelope。", + "试用结果没问题后,可以把它保存成当前项目里的助手。", ), discoveryPending: t( "capabilityDraft.registeredPanel.agentEnvelope.discovery.pending", - "Discovery:等待 workspace-local skill 注册路径后再进入团队发现。", + "团队可见性:保存成项目助手后再开放。", ), evidenceMissing: t( "capabilityDraft.registeredPanel.agentEnvelope.evidence.missing", - "Evidence:还没有成功运行证据;先通过本回合启用拿到一次结果。", + "最近结果:还没有试用结果。", ), evidenceSourceMetadataOnly: t( "capabilityDraft.registeredPanel.agentEnvelope.evidence.sourceMetadataOnly", - "Evidence:已有 P3E source metadata,可追踪本次 session 授权来源。", + "最近结果:已记录这次试用来源,等待检查。", ), formatAgentCardReady: (directory) => t("capabilityDraft.registeredPanel.agentEnvelope.agentCard.ready", { - defaultValue: - "Agent card:workspace-local/{{directory}},由已注册 Skill、Managed Job 和 completion audit 派生。", + defaultValue: "项目助手:已准备好保存({{directory}})。", directory, }), formatCompletionAuditEvidenceLabel: buildCompletionAuditLabel, @@ -480,32 +567,29 @@ function WorkspaceRegisteredSkillCard({ ? t( "capabilityDraft.registeredPanel.agentEnvelope.evidence.completedPack", { - defaultValue: "Evidence:已关联 evidence pack {{packId}}。", + defaultValue: "最近结果:已通过检查({{packId}})。", packId, }, ) : t( "capabilityDraft.registeredPanel.agentEnvelope.evidence.completedPackFallback", - "Evidence:已关联 evidence pack。", + "最近结果:已通过检查。", ), formatDiscoveryReady: (registeredSkillDirectory) => t("capabilityDraft.registeredPanel.agentEnvelope.discovery.ready", { - defaultValue: - "Discovery:同 workspace 成员通过 registered skill 发现 {{directory}},复用同一 Managed Job / evidence。", + defaultValue: "团队可见性:当前项目成员可以使用这条技能。", directory: registeredSkillDirectory, }), formatMemoryWithReport: (reportId) => t("capabilityDraft.registeredPanel.agentEnvelope.memory.withReport", { - defaultValue: - "Memory:引用 verification report {{reportId}} 与后续运行修正。", + defaultValue: "记录:已保留检查结果,后续会继续积累使用反馈。", reportId, }), formatPendingEvidencePack: (packId) => t( "capabilityDraft.registeredPanel.agentEnvelope.evidence.pendingPack", { - defaultValue: - "Evidence:已关联 evidence pack {{packId}},但还缺 completed completion audit,不能固化为 Agent。", + defaultValue: "最近结果:已生成,等待检查后才能保存成项目助手。", packId, }, ), @@ -519,54 +603,54 @@ function WorkspaceRegisteredSkillCard({ ), formatRunbook: (name) => t("capabilityDraft.registeredPanel.agentEnvelope.runbook", { - defaultValue: "Runbook:{{name}}", + defaultValue: "使用方式:{{name}}", name, }), memoryPending: t( "capabilityDraft.registeredPanel.agentEnvelope.memory.pending", - "Memory:等待首轮运行后记录用户偏好、方法论和修正历史。", + "记录:等待首次试用后记录偏好和修正。", ), permissionEmpty: t( "capabilityDraft.registeredPanel.agentEnvelope.permission.empty", - "权限:默认手动确认,未声明额外外部写权限。", + "权限:默认需要手动确认。", ), schedule: t( "capabilityDraft.registeredPanel.agentEnvelope.schedule", - "Schedule:第一刀仅支持 manual rerun 草案,不创建长期任务。", + "定时运行:尚未设置。", ), sharingPending: t( "capabilityDraft.registeredPanel.agentEnvelope.sharing.pending", - "Sharing:未完成审计前仅对当前操作者展示草案。", + "共享:先只对你可见。", ), sharingReady: t( "capabilityDraft.registeredPanel.agentEnvelope.sharing.ready", - "Sharing:可在当前 workspace / team 内共享;不进入 public Marketplace。", + "共享:可在当前项目内共享。", ), statusLabels: { blocked: t( "capabilityDraft.registeredPanel.agentEnvelope.status.blocked", - "Agent 草案阻塞", + "需要处理", ), evidence_ready: t( "capabilityDraft.registeredPanel.agentEnvelope.status.evidenceReady", - "Evidence 已就绪", + "可保存", ), manual_enable_required: t( "capabilityDraft.registeredPanel.agentEnvelope.status.manualEnableRequired", - "等待成功运行", + "待试用", ), source_metadata_ready: t( "capabilityDraft.registeredPanel.agentEnvelope.status.sourceMetadataReady", - "等待 Completion Audit", + "待检查", ), }, widgetPending: t( "capabilityDraft.registeredPanel.agentEnvelope.widget.pending", - "Widget:等待运行后展示状态、产物、阻塞点和 evidence 入口。", + "结果:试用后展示最近状态和下一步。", ), widgetReady: t( "capabilityDraft.registeredPanel.agentEnvelope.widget.ready", - "Widget:展示 Managed Job 状态、最近产物、审计结论和下一步动作。", + "结果:展示最近状态、产物和下一步动作。", ), }), [buildCompletionAuditLabel, t], @@ -735,6 +819,70 @@ function WorkspaceRegisteredSkillCard({ managedAutomationCopy, ); const [managedAutomationJob] = managedAutomationJobs; + const canAuditManagedAutomationJob = Boolean( + managedAutomationJob?.last_run_at || + managedAutomationJob?.last_finished_at || + managedAutomationJob?.last_status, + ); + const userPermissionSummary = summarizePermissionForUser(skill, { + defaultReadonly: t( + "capabilityDraft.registeredPanel.user.permission.defaultReadonly", + "默认只读取信息", + ), + draftWrite: t( + "capabilityDraft.registeredPanel.user.permission.draftWrite", + "可写入草案", + ), + localCommand: t( + "capabilityDraft.registeredPanel.user.permission.localCommand", + "可运行本地命令", + ), + readonly: t( + "capabilityDraft.registeredPanel.user.permission.readonly", + "只读取信息", + ), + }); + const userScheduleLabel = getUserScheduleLabel(managedAutomationJob, { + enabled: t( + "capabilityDraft.registeredPanel.user.schedule.enabled", + "已开启", + ), + notCreated: t( + "capabilityDraft.registeredPanel.user.schedule.notCreated", + "未设置", + ), + paused: t("capabilityDraft.registeredPanel.user.schedule.paused", "已暂停"), + }); + const userResultLabel = getUserResultLabel( + managedAutomationJob, + completionAuditSummary, + { + auditBlocked: t( + "capabilityDraft.registeredPanel.user.result.auditBlocked", + "最近结果需要处理", + ), + auditPassed: t( + "capabilityDraft.registeredPanel.user.result.auditPassed", + "最近结果已通过检查", + ), + noJob: t( + "capabilityDraft.registeredPanel.user.result.noJob", + "还没有定时运行", + ), + noRun: t( + "capabilityDraft.registeredPanel.user.result.noRun", + "还没有运行记录", + ), + readyToCheck: t( + "capabilityDraft.registeredPanel.user.result.readyToCheck", + "可以检查最近结果", + ), + running: t( + "capabilityDraft.registeredPanel.user.result.running", + "正在运行", + ), + }, + ); const managedAutomationUpdating = managedAutomationJob?.id === managedAutomationUpdatingJobId; const completionAuditAuditing = @@ -769,11 +917,11 @@ function WorkspaceRegisteredSkillCard({ {bindingBlocked ? t( "capabilityDraft.registeredPanel.card.binding.blocked", - "Binding 阻塞", + "需要处理", ) : t( "capabilityDraft.registeredPanel.card.binding.candidate", - "P3C binding 候选", + "可试用", )}
@@ -789,617 +937,681 @@ function WorkspaceRegisteredSkillCard({ )}

-
-
- +
+
+
+ {t("capabilityDraft.registeredPanel.user.step.saved", "已保存")} +
+
{t( - "capabilityDraft.registeredPanel.card.field.directory", - "目录:", + "capabilityDraft.registeredPanel.user.step.savedValue", + "可在当前项目使用", )} - - {skill.directory} +
-
- - {t("capabilityDraft.registeredPanel.card.field.source", "来源:")} - - {skill.registration.sourceDraftId} - {skill.registration.sourceVerificationReportId - ? ` / ${skill.registration.sourceVerificationReportId}` - : ""} +
+
+ {t("capabilityDraft.registeredPanel.user.step.permission", "权限")} +
+
+ {userPermissionSummary} +
-
- +
+
{t( - "capabilityDraft.registeredPanel.card.field.permission", - "权限:", - )} - - {summarizePermissionSummary(skill, summaryCopy)} -
-
- - {t("capabilityDraft.registeredPanel.card.field.resource", "资源:")} - - {summarizeResourceSummary(skill, summaryCopy)} -
-
- - {t("capabilityDraft.registeredPanel.card.field.standard", "标准:")} - - {summarizeStandardCompliance(skill, summaryCopy)} -
-
- - {t( - "capabilityDraft.registeredPanel.card.field.runtimeBinding", - "运行绑定:", - )} - - {summarizeBindingStatus(binding, summaryCopy)} -
-
- {t( - "capabilityDraft.registeredPanel.card.field.nextGate", - "下一道 gate:", - )} - {binding?.next_gate || - t( - "capabilityDraft.registeredPanel.card.nextGateFallback", - "manual_runtime_enable / Query Loop metadata / tool_runtime 授权裁剪", + "capabilityDraft.registeredPanel.user.step.schedule", + "定时运行", )} +
+
+ {userScheduleLabel} +
- {preflightGate ? ( -
-
- +

+ {t( + "capabilityDraft.registeredPanel.user.nextStep", + "建议先试用一次。确认结果没问题后,再设置定时运行或保存成项目助手。", + )} +

+
+ + {t("capabilityDraft.registeredPanel.technicalDetails", "技术详情")} + +
+
+ {t( - "capabilityDraft.registeredPanel.provenance.title", - "注册 provenance", + "capabilityDraft.registeredPanel.card.field.directory", + "目录:", )} - - {preflightGate.label || preflightGate.checkId} + {skill.directory} +
+
+ + {t("capabilityDraft.registeredPanel.card.field.source", "来源:")} + {skill.registration.sourceDraftId} + {skill.registration.sourceVerificationReportId + ? ` / ${skill.registration.sourceVerificationReportId}` + : ""}
-
- {preflightGate.evidence.slice(0, 6).map((evidence) => ( -
-
- {formatRegistrationEvidenceKey(evidence.key, evidenceLabels)} -
-
- {formatRegistrationEvidenceValue(evidence)} -
-
- ))} -
-
- ) : null} - {approvalPreview ? ( -
-
- +
+ {t( - "capabilityDraft.registeredPanel.approval.title", - "Session approval request artifact", + "capabilityDraft.registeredPanel.card.field.permission", + "权限:", )} - - {t("capabilityDraft.registeredPanel.approval.statusSummary", { - defaultValue: "{{status}} / 未执行 / 未保存凭证", - status: approvalPreview.status, - })} - + {summarizePermissionSummary(skill, summaryCopy)}
-

+

+ + {t( + "capabilityDraft.registeredPanel.card.field.resource", + "资源:", + )} + + {summarizeResourceSummary(skill, summaryCopy)} +
+
+ + {t( + "capabilityDraft.registeredPanel.card.field.standard", + "标准:", + )} + + {summarizeStandardCompliance(skill, summaryCopy)} +
+
+ + {t( + "capabilityDraft.registeredPanel.card.field.runtimeBinding", + "运行绑定:", + )} + + {summarizeBindingStatus(binding, summaryCopy)} +
+
{t( - "capabilityDraft.registeredPanel.approval.description", - "真实 API 执行前必须先消费这条授权请求 artifact;当前只持久化审计入口,不保存 token,也不发请求。", + "capabilityDraft.registeredPanel.card.field.nextGate", + "下一道 gate:", )} -

- {approvalPreview.consumptionGate ? ( -
-
- - {t( - "capabilityDraft.registeredPanel.approval.consumptionGate.title", - "消费门禁", - )} - - - {approvalPreview.consumptionGate.status} - -
-

- {approvalPreview.consumptionGate.blockedReason} -

-
- {approvalPreview.consumptionGate.requiredInputs.map((input) => ( - - {input} - - ))} -
-
- {t( - "capabilityDraft.registeredPanel.approval.flag.runtimeExecution", - { - defaultValue: "runtimeExecution={{value}}", - value: String( - approvalPreview.consumptionGate.runtimeExecutionEnabled, - ), - }, - )}{" "} - /{" "} - {t( - "capabilityDraft.registeredPanel.approval.flag.credentialStorage", - { - defaultValue: "credentialStorage={{value}}", - value: String( - approvalPreview.consumptionGate.credentialStorageEnabled, - ), - }, - )} -
-
- ) : null} - {approvalPreview.credentialResolver ? ( -
-
- - {t( - "capabilityDraft.registeredPanel.approval.credentialResolver.title", - "Session credential resolver", - )} - - - {approvalPreview.credentialResolver.status} - -
-

- {approvalPreview.credentialResolver.blockedReason} -

-
- {[ - [ - t( - "capabilityDraft.registeredPanel.approval.label.reference", - "Reference", - ), - approvalPreview.credentialResolver.referenceId, - ], - [ - t( - "capabilityDraft.registeredPanel.approval.label.scope", - "Scope", - ), - approvalPreview.credentialResolver.scope, - ], - [ - t( - "capabilityDraft.registeredPanel.approval.label.source", - "Source", - ), - approvalPreview.credentialResolver.source, - ], - [ - t( - "capabilityDraft.registeredPanel.approval.label.secret", - "Secret", - ), - approvalPreview.credentialResolver.secretMaterialStatus, - ], - [ - t( - "capabilityDraft.registeredPanel.approval.label.tokenPersisted", - "tokenPersisted", - ), - String(approvalPreview.credentialResolver.tokenPersisted), - ], - [ - t( - "capabilityDraft.registeredPanel.approval.label.runtimeInjection", - "runtimeInjection", - ), - String( - approvalPreview.credentialResolver - .runtimeInjectionEnabled, - ), - ], - ].map(([label, value]) => ( -
- {label} - - {value} - -
- ))} -
-
- ) : null} - {approvalPreview.consumptionInputSchema ? ( -
-
- - {t( - "capabilityDraft.registeredPanel.approval.inputSchema.title", - "Approval consumption input schema", - )} - - - {approvalPreview.consumptionInputSchema.schemaId} - -
-

- {approvalPreview.consumptionInputSchema.blockedReason} -

-
- {t( - "capabilityDraft.registeredPanel.approval.flag.uiSubmission", - { - defaultValue: "uiSubmission={{value}}", - value: String( - approvalPreview.consumptionInputSchema - .uiSubmissionEnabled, - ), - }, - )}{" "} - /{" "} - {t( - "capabilityDraft.registeredPanel.approval.flag.runtimeExecution", - { - defaultValue: "runtimeExecution={{value}}", - value: String( - approvalPreview.consumptionInputSchema - .runtimeExecutionEnabled, - ), - }, - )} -
-
- {approvalPreview.consumptionInputSchema.fields.map((field) => ( - - {field.key}:{field.kind} - {field.required - ? t( - "capabilityDraft.registeredPanel.approval.suffix.required", - ":required", - ) - : ""} - {field.secret - ? t( - "capabilityDraft.registeredPanel.approval.suffix.secret", - ":secret", - ) - : ""} - - ))} -
-
- ) : null} - {approvalPreview.sessionInputIntake ? ( -
-
- - {t( - "capabilityDraft.registeredPanel.approval.sessionInputIntake.title", - "Session input intake", - )} - - - {approvalPreview.sessionInputIntake.status} - -
-

- {approvalPreview.sessionInputIntake.blockedReason} -

-
- {[ - [ - t( - "capabilityDraft.registeredPanel.approval.label.schema", - "Schema", - ), - approvalPreview.sessionInputIntake.schemaId, - ], - [ - t( - "capabilityDraft.registeredPanel.approval.label.scope", - "Scope", - ), - approvalPreview.sessionInputIntake.scope, - ], - [ - t( - "capabilityDraft.registeredPanel.approval.label.credential", - "Credential", - ), - approvalPreview.sessionInputIntake.credentialReferenceId, - ], - [ - t( - "capabilityDraft.registeredPanel.approval.label.secret", - "Secret", - ), - approvalPreview.sessionInputIntake.secretMaterialStatus, - ], - [ - t( - "capabilityDraft.registeredPanel.approval.label.endpointPersisted", - "endpointPersisted", - ), - String( - approvalPreview.sessionInputIntake.endpointInputPersisted, - ), - ], - [ - t( - "capabilityDraft.registeredPanel.approval.label.tokenPersisted", - "tokenPersisted", - ), - String(approvalPreview.sessionInputIntake.tokenPersisted), - ], - ].map(([label, value]) => ( -
- {label} - - {value} - -
- ))} -
-
- {t( - "capabilityDraft.registeredPanel.approval.flag.uiSubmission", - { - defaultValue: "uiSubmission={{value}}", - value: String( - approvalPreview.sessionInputIntake.uiSubmissionEnabled, - ), - }, - )}{" "} - /{" "} - {t( - "capabilityDraft.registeredPanel.approval.flag.runtimeExecution", - { - defaultValue: "runtimeExecution={{value}}", - value: String( - approvalPreview.sessionInputIntake - .runtimeExecutionEnabled, - ), - }, - )} -
-
- {approvalPreview.sessionInputIntake.missingFieldKeys.map( - (fieldKey) => ( - - {t( - "capabilityDraft.registeredPanel.approval.prefix.missing", - "missing:", - )} - {fieldKey} - - ), - )} -
-
- ) : null} - {approvalPreview.sessionInputSubmissionContract ? ( -
-
- - {t( - "capabilityDraft.registeredPanel.approval.sessionSubmissionContract.title", - "Session submission contract", - )} - - - {approvalPreview.sessionInputSubmissionContract.status} - -
-

- {approvalPreview.sessionInputSubmissionContract.blockedReason} -

-
- {[ - [ - t( - "capabilityDraft.registeredPanel.approval.label.mode", - "Mode", - ), - approvalPreview.sessionInputSubmissionContract.mode, - ], - [ - t( - "capabilityDraft.registeredPanel.approval.label.retention", - "Retention", - ), - approvalPreview.sessionInputSubmissionContract - .valueRetention, - ], - [ - t( - "capabilityDraft.registeredPanel.approval.label.submitHandler", - "submitHandler", - ), - String( - approvalPreview.sessionInputSubmissionContract - .submissionHandlerEnabled, - ), - ], - [ - t( - "capabilityDraft.registeredPanel.approval.label.secretAccepted", - "secretAccepted", - ), - String( - approvalPreview.sessionInputSubmissionContract - .secretMaterialAccepted, - ), - ], - [ - t( - "capabilityDraft.registeredPanel.approval.label.evidenceRequired", - "evidenceRequired", - ), - String( - approvalPreview.sessionInputSubmissionContract - .evidenceCaptureRequired, - ), - ], - [ - t( - "capabilityDraft.registeredPanel.approval.label.runtimeExecution", - "runtimeExecution", - ), - String( - approvalPreview.sessionInputSubmissionContract - .runtimeExecutionEnabled, - ), - ], - ].map(([label, value]) => ( -
- {label} - - {value} - -
- ))} -
-
- {approvalPreview.sessionInputSubmissionContract.validationRules.map( - (rule) => ( - - {t( - "capabilityDraft.registeredPanel.approval.prefix.validate", - "validate:", - )} - {rule.fieldKey}:{rule.kind} - {rule.required - ? t( - "capabilityDraft.registeredPanel.approval.suffix.required", - ":required", - ) - : ""} - - ), - )} -
-
- ) : null} -
- {[ - { - label: t( - "capabilityDraft.registeredPanel.approval.label.approvalId", - "Approval ID", - ), - value: approvalPreview.approvalId, - }, - { - label: t( - "capabilityDraft.registeredPanel.approval.label.status", - "状态", - ), - value: approvalPreview.status, - }, - { - label: t( - "capabilityDraft.registeredPanel.approval.label.endpoint", - "Endpoint", - ), - value: approvalPreview.endpointSource, - }, - { - label: t( - "capabilityDraft.registeredPanel.approval.label.method", - "方法", - ), - value: approvalPreview.method, - }, - { - label: t( - "capabilityDraft.registeredPanel.approval.label.credentialReference", - "凭证引用", - ), - value: approvalPreview.credentialReferenceId, - }, - { - label: t( - "capabilityDraft.registeredPanel.approval.label.policy", - "Policy", - ), - value: approvalPreview.policyPath, - }, - { - label: t( - "capabilityDraft.registeredPanel.approval.label.createdAt", - "创建时间", - ), - value: approvalPreview.createdAt, - }, - { - label: t( - "capabilityDraft.registeredPanel.approval.label.evidenceSchema", - "证据 Schema", - ), - value: approvalPreview.evidenceSchema, - wide: true, - }, - ].map(({ label, value, wide }) => ( -
-
- {label} -
-
- {value} -
-
- ))} + {binding?.next_gate || + t( + "capabilityDraft.registeredPanel.card.nextGateFallback", + "manual_runtime_enable / Query Loop metadata / tool_runtime 授权裁剪", + )}
- ) : null} -
+ {preflightGate ? ( +
+
+ + {t( + "capabilityDraft.registeredPanel.provenance.title", + "注册 provenance", + )} + + + {preflightGate.label || preflightGate.checkId} + +
+
+ {preflightGate.evidence.slice(0, 6).map((evidence) => ( +
+
+ {formatRegistrationEvidenceKey( + evidence.key, + evidenceLabels, + )} +
+
+ {formatRegistrationEvidenceValue(evidence)} +
+
+ ))} +
+
+ ) : null} + {approvalPreview ? ( +
+
+ + {t( + "capabilityDraft.registeredPanel.approval.title", + "Session approval request artifact", + )} + + + {t("capabilityDraft.registeredPanel.approval.statusSummary", { + defaultValue: "{{status}} / 未执行 / 未保存凭证", + status: approvalPreview.status, + })} + +
+

+ {t( + "capabilityDraft.registeredPanel.approval.description", + "真实 API 执行前必须先消费这条授权请求 artifact;当前只持久化审计入口,不保存 token,也不发请求。", + )} +

+ {approvalPreview.consumptionGate ? ( +
+
+ + {t( + "capabilityDraft.registeredPanel.approval.consumptionGate.title", + "消费门禁", + )} + + + {approvalPreview.consumptionGate.status} + +
+

+ {approvalPreview.consumptionGate.blockedReason} +

+
+ {approvalPreview.consumptionGate.requiredInputs.map( + (input) => ( + + {input} + + ), + )} +
+
+ {t( + "capabilityDraft.registeredPanel.approval.flag.runtimeExecution", + { + defaultValue: "runtimeExecution={{value}}", + value: String( + approvalPreview.consumptionGate.runtimeExecutionEnabled, + ), + }, + )}{" "} + /{" "} + {t( + "capabilityDraft.registeredPanel.approval.flag.credentialStorage", + { + defaultValue: "credentialStorage={{value}}", + value: String( + approvalPreview.consumptionGate + .credentialStorageEnabled, + ), + }, + )} +
+
+ ) : null} + {approvalPreview.credentialResolver ? ( +
+
+ + {t( + "capabilityDraft.registeredPanel.approval.credentialResolver.title", + "Session credential resolver", + )} + + + {approvalPreview.credentialResolver.status} + +
+

+ {approvalPreview.credentialResolver.blockedReason} +

+
+ {[ + [ + t( + "capabilityDraft.registeredPanel.approval.label.reference", + "Reference", + ), + approvalPreview.credentialResolver.referenceId, + ], + [ + t( + "capabilityDraft.registeredPanel.approval.label.scope", + "Scope", + ), + approvalPreview.credentialResolver.scope, + ], + [ + t( + "capabilityDraft.registeredPanel.approval.label.source", + "Source", + ), + approvalPreview.credentialResolver.source, + ], + [ + t( + "capabilityDraft.registeredPanel.approval.label.secret", + "Secret", + ), + approvalPreview.credentialResolver.secretMaterialStatus, + ], + [ + t( + "capabilityDraft.registeredPanel.approval.label.tokenPersisted", + "tokenPersisted", + ), + String(approvalPreview.credentialResolver.tokenPersisted), + ], + [ + t( + "capabilityDraft.registeredPanel.approval.label.runtimeInjection", + "runtimeInjection", + ), + String( + approvalPreview.credentialResolver + .runtimeInjectionEnabled, + ), + ], + ].map(([label, value]) => ( +
+ + {label} + + + {value} + +
+ ))} +
+
+ ) : null} + {approvalPreview.consumptionInputSchema ? ( +
+
+ + {t( + "capabilityDraft.registeredPanel.approval.inputSchema.title", + "Approval consumption input schema", + )} + + + {approvalPreview.consumptionInputSchema.schemaId} + +
+

+ {approvalPreview.consumptionInputSchema.blockedReason} +

+
+ {t( + "capabilityDraft.registeredPanel.approval.flag.uiSubmission", + { + defaultValue: "uiSubmission={{value}}", + value: String( + approvalPreview.consumptionInputSchema + .uiSubmissionEnabled, + ), + }, + )}{" "} + /{" "} + {t( + "capabilityDraft.registeredPanel.approval.flag.runtimeExecution", + { + defaultValue: "runtimeExecution={{value}}", + value: String( + approvalPreview.consumptionInputSchema + .runtimeExecutionEnabled, + ), + }, + )} +
+
+ {approvalPreview.consumptionInputSchema.fields.map( + (field) => ( + + {field.key}:{field.kind} + {field.required + ? t( + "capabilityDraft.registeredPanel.approval.suffix.required", + ":required", + ) + : ""} + {field.secret + ? t( + "capabilityDraft.registeredPanel.approval.suffix.secret", + ":secret", + ) + : ""} + + ), + )} +
+
+ ) : null} + {approvalPreview.sessionInputIntake ? ( +
+
+ + {t( + "capabilityDraft.registeredPanel.approval.sessionInputIntake.title", + "Session input intake", + )} + + + {approvalPreview.sessionInputIntake.status} + +
+

+ {approvalPreview.sessionInputIntake.blockedReason} +

+
+ {[ + [ + t( + "capabilityDraft.registeredPanel.approval.label.schema", + "Schema", + ), + approvalPreview.sessionInputIntake.schemaId, + ], + [ + t( + "capabilityDraft.registeredPanel.approval.label.scope", + "Scope", + ), + approvalPreview.sessionInputIntake.scope, + ], + [ + t( + "capabilityDraft.registeredPanel.approval.label.credential", + "Credential", + ), + approvalPreview.sessionInputIntake.credentialReferenceId, + ], + [ + t( + "capabilityDraft.registeredPanel.approval.label.secret", + "Secret", + ), + approvalPreview.sessionInputIntake.secretMaterialStatus, + ], + [ + t( + "capabilityDraft.registeredPanel.approval.label.endpointPersisted", + "endpointPersisted", + ), + String( + approvalPreview.sessionInputIntake + .endpointInputPersisted, + ), + ], + [ + t( + "capabilityDraft.registeredPanel.approval.label.tokenPersisted", + "tokenPersisted", + ), + String(approvalPreview.sessionInputIntake.tokenPersisted), + ], + ].map(([label, value]) => ( +
+ + {label} + + + {value} + +
+ ))} +
+
+ {t( + "capabilityDraft.registeredPanel.approval.flag.uiSubmission", + { + defaultValue: "uiSubmission={{value}}", + value: String( + approvalPreview.sessionInputIntake.uiSubmissionEnabled, + ), + }, + )}{" "} + /{" "} + {t( + "capabilityDraft.registeredPanel.approval.flag.runtimeExecution", + { + defaultValue: "runtimeExecution={{value}}", + value: String( + approvalPreview.sessionInputIntake + .runtimeExecutionEnabled, + ), + }, + )} +
+
+ {approvalPreview.sessionInputIntake.missingFieldKeys.map( + (fieldKey) => ( + + {t( + "capabilityDraft.registeredPanel.approval.prefix.missing", + "missing:", + )} + {fieldKey} + + ), + )} +
+
+ ) : null} + {approvalPreview.sessionInputSubmissionContract ? ( +
+
+ + {t( + "capabilityDraft.registeredPanel.approval.sessionSubmissionContract.title", + "Session submission contract", + )} + + + {approvalPreview.sessionInputSubmissionContract.status} + +
+

+ {approvalPreview.sessionInputSubmissionContract.blockedReason} +

+
+ {[ + [ + t( + "capabilityDraft.registeredPanel.approval.label.mode", + "Mode", + ), + approvalPreview.sessionInputSubmissionContract.mode, + ], + [ + t( + "capabilityDraft.registeredPanel.approval.label.retention", + "Retention", + ), + approvalPreview.sessionInputSubmissionContract + .valueRetention, + ], + [ + t( + "capabilityDraft.registeredPanel.approval.label.submitHandler", + "submitHandler", + ), + String( + approvalPreview.sessionInputSubmissionContract + .submissionHandlerEnabled, + ), + ], + [ + t( + "capabilityDraft.registeredPanel.approval.label.secretAccepted", + "secretAccepted", + ), + String( + approvalPreview.sessionInputSubmissionContract + .secretMaterialAccepted, + ), + ], + [ + t( + "capabilityDraft.registeredPanel.approval.label.evidenceRequired", + "evidenceRequired", + ), + String( + approvalPreview.sessionInputSubmissionContract + .evidenceCaptureRequired, + ), + ], + [ + t( + "capabilityDraft.registeredPanel.approval.label.runtimeExecution", + "runtimeExecution", + ), + String( + approvalPreview.sessionInputSubmissionContract + .runtimeExecutionEnabled, + ), + ], + ].map(([label, value]) => ( +
+ + {label} + + + {value} + +
+ ))} +
+
+ {approvalPreview.sessionInputSubmissionContract.validationRules.map( + (rule) => ( + + {t( + "capabilityDraft.registeredPanel.approval.prefix.validate", + "validate:", + )} + {rule.fieldKey}:{rule.kind} + {rule.required + ? t( + "capabilityDraft.registeredPanel.approval.suffix.required", + ":required", + ) + : ""} + + ), + )} +
+
+ ) : null} +
+ {[ + { + label: t( + "capabilityDraft.registeredPanel.approval.label.approvalId", + "Approval ID", + ), + value: approvalPreview.approvalId, + }, + { + label: t( + "capabilityDraft.registeredPanel.approval.label.status", + "状态", + ), + value: approvalPreview.status, + }, + { + label: t( + "capabilityDraft.registeredPanel.approval.label.endpoint", + "Endpoint", + ), + value: approvalPreview.endpointSource, + }, + { + label: t( + "capabilityDraft.registeredPanel.approval.label.method", + "方法", + ), + value: approvalPreview.method, + }, + { + label: t( + "capabilityDraft.registeredPanel.approval.label.credentialReference", + "凭证引用", + ), + value: approvalPreview.credentialReferenceId, + }, + { + label: t( + "capabilityDraft.registeredPanel.approval.label.policy", + "Policy", + ), + value: approvalPreview.policyPath, + }, + { + label: t( + "capabilityDraft.registeredPanel.approval.label.createdAt", + "创建时间", + ), + value: approvalPreview.createdAt, + }, + { + label: t( + "capabilityDraft.registeredPanel.approval.label.evidenceSchema", + "证据 Schema", + ), + value: approvalPreview.evidenceSchema, + wide: true, + }, + ].map(({ label, value, wide }) => ( +
+
+ {label} +
+
+ {value} +
+
+ ))} +
+
+ ) : null} +
+
{t( "capabilityDraft.registeredPanel.agentEnvelope.title", - "Agent envelope 草案", + "项目助手", )} @@ -1409,129 +1621,178 @@ function WorkspaceRegisteredSkillCard({

{envelopeDraft.description}

-
- {envelopeDraft.agentCardLabel} - {envelopeDraft.sharingLabel} - {envelopeDraft.sharingDiscoveryLabel} - {envelopeDraft.runbookLabel} - {envelopeDraft.memoryLabel} - {envelopeDraft.widgetLabel} - {envelopeDraft.permissionLabel} - {envelopeDraft.scheduleLabel} - {envelopeDraft.evidenceLabel} - {managedAutomationPresentation.statusLabel} - {managedAutomationPresentation.scheduleLabel} - {managedAutomationPresentation.lastRunLabel} - {managedAutomationPresentation.objectiveLabel} - {managedAutomationPresentation.auditLabel} +
+
+
+ {t("capabilityDraft.registeredPanel.user.assistant.try", "试用")} +
+
+ {runtimeEnableReady + ? t( + "capabilityDraft.registeredPanel.user.assistant.tryReady", + "可以试用一次", + ) + : t( + "capabilityDraft.registeredPanel.user.assistant.tryBlocked", + "暂不可试用", + )} +
+
+
+
+ {t( + "capabilityDraft.registeredPanel.user.assistant.schedule", + "定时运行", + )} +
+
+ {userScheduleLabel} +
+
+
+
+ {t( + "capabilityDraft.registeredPanel.user.assistant.result", + "最近结果", + )} +
+
+ {userResultLabel} +
+
- - {onCreateManagedAutomationDraft && binding ? ( - - ) : null} - {managedAutomationJob && onToggleManagedAutomationJob ? ( + +
+ {envelopeDraft.agentCardLabel} + {envelopeDraft.sharingLabel} + {envelopeDraft.sharingDiscoveryLabel} + {envelopeDraft.runbookLabel} + {envelopeDraft.memoryLabel} + {envelopeDraft.widgetLabel} + {envelopeDraft.permissionLabel} + {envelopeDraft.scheduleLabel} + {envelopeDraft.evidenceLabel} + {managedAutomationPresentation.statusLabel} + {managedAutomationPresentation.scheduleLabel} + {managedAutomationPresentation.lastRunLabel} + {managedAutomationPresentation.objectiveLabel} + {managedAutomationPresentation.auditLabel} +
+ +
+ {onEnableRuntime && binding ? ( + + ) : null} + {onCreateManagedAutomationDraft && binding ? ( + + ) : null} + {managedAutomationJob && onToggleManagedAutomationJob ? ( + + ) : null} + {managedAutomationJob && onAuditManagedAutomationJob ? ( + + ) : null} - ) : null} - {managedAutomationJob && onAuditManagedAutomationJob ? ( - - ) : null} +
- {onEnableRuntime && binding ? ( -
- - - {t( - "capabilityDraft.registeredPanel.action.enableRuntimeHelp", - "只写入 session enable metadata,不创建自动化。", - )} - -
- ) : null} ); } @@ -1703,12 +1964,13 @@ export function WorkspaceRegisteredSkillsPanel({ const runs = await getAutomationRunHistory(job.id, 5); const sessionId = runs.find((run) => run.session_id)?.session_id; if (!sessionId) { - throw new Error( + setError( t( "capabilityDraft.registeredPanel.error.missingAutomationSession", - "最近 automation run 没有关联 session,无法导出 evidence。", + "还没有可检查的运行结果。请先试用一次,或开启定时运行后等待它完成。", ), ); + return; } const evidencePack = await exportAgentRuntimeEvidencePack(sessionId); setCompletionAuditSummaries((previous) => ({ @@ -1738,19 +2000,16 @@ export function WorkspaceRegisteredSkillsPanel({
- {t("capabilityDraft.registeredPanel.badge", "注册区")} + {t("capabilityDraft.registeredPanel.badge", "已保存")}

- {t( - "capabilityDraft.registeredPanel.title", - "Workspace 已注册能力", - )} + {t("capabilityDraft.registeredPanel.title", "已保存技能")}

{t( "capabilityDraft.registeredPanel.description", - "这里只有已通过验证并写入当前项目的 Skill 包;运行仍要等 runtime gate。", + "这里是已经检查并保存到当前项目的技能。可以先试用,也可以设置定时运行。", )}

@@ -1776,28 +2035,39 @@ export function WorkspaceRegisteredSkillsPanel({
{t( "capabilityDraft.registeredPanel.empty.missingProject", - "选择或进入一个项目后,才能查看该项目已注册的 generated skill。", + "选择或进入一个项目后,才能查看这个项目里保存的技能。", )}
) : effectiveError ? (
- {t("capabilityDraft.registeredPanel.empty.error", { - defaultValue: "已注册能力暂时没读到:{{message}}", - message: effectiveError, - })} +
+ {t( + "capabilityDraft.registeredPanel.empty.error", + "已保存技能暂时没读到,请稍后重试。", + )} +
+
+ + {t( + "capabilityDraft.registeredPanel.technicalDetails", + "技术详情", + )} + +
{effectiveError}
+
) : isBusy ? (
{t( "capabilityDraft.registeredPanel.empty.loading", - "正在读取已注册能力...", + "正在读取已保存技能...", )}
) : visibleSkills.length === 0 ? (
{t( "capabilityDraft.registeredPanel.empty.noSkills", - "当前项目还没有通过 P3A 注册的能力。草案通过验证并注册后,会先出现在这里。", + "当前项目还没有已保存技能。先在“正在制作”里检查并保存一个。", )}
) : ( diff --git a/src/hooks/useSkills.test.tsx b/src/hooks/useSkills.test.tsx index 7060f1c8e..01f21a5be 100644 --- a/src/hooks/useSkills.test.tsx +++ b/src/hooks/useSkills.test.tsx @@ -11,14 +11,21 @@ import { } from "@/components/workspace/hooks/testUtils"; import { useSkills } from "./useSkills"; -const { mockGetLocal, mockGetAll, mockGetRepos, mockRefreshCache } = vi.hoisted( - () => ({ - mockGetLocal: vi.fn(), - mockGetAll: vi.fn(), - mockGetRepos: vi.fn(), - mockRefreshCache: vi.fn(), - }), -); +const { + mockGetLocal, + mockGetAll, + mockGetRepos, + mockRefreshCache, + mockInstall, + mockUninstall, +} = vi.hoisted(() => ({ + mockGetLocal: vi.fn(), + mockGetAll: vi.fn(), + mockGetRepos: vi.fn(), + mockRefreshCache: vi.fn(), + mockInstall: vi.fn(), + mockUninstall: vi.fn(), +})); vi.mock("@/lib/api/skills", async () => { const actual = @@ -34,6 +41,8 @@ vi.mock("@/lib/api/skills", async () => { getAll: (...args: unknown[]) => mockGetAll(...args), getRepos: (...args: unknown[]) => mockGetRepos(...args), refreshCache: (...args: unknown[]) => mockRefreshCache(...args), + install: (...args: unknown[]) => mockInstall(...args), + uninstall: (...args: unknown[]) => mockUninstall(...args), }, }; }); @@ -81,6 +90,8 @@ describe("useSkills", () => { mockGetAll.mockResolvedValue([]); mockGetRepos.mockResolvedValue([] satisfies SkillRepo[]); mockRefreshCache.mockResolvedValue(true); + mockInstall.mockResolvedValue(true); + mockUninstall.mockResolvedValue(true); }); afterEach(() => { @@ -149,4 +160,41 @@ describe("useSkills", () => { expect(mockGetLocal).toHaveBeenCalledWith("gemini"); expect(mockGetRepos).not.toHaveBeenCalled(); }); + + it("关闭 repo 拉取时刷新仍只更新本地技能", async () => { + const initialSkill = createSkill({ directory: "initial-skill" }); + const refreshedSkill = createSkill({ directory: "refreshed-skill" }); + mockGetLocal + .mockResolvedValueOnce([initialSkill]) + .mockResolvedValueOnce([refreshedSkill]); + + await renderHook("claude", false); + + await act(async () => { + await getLatestValue().refresh(); + }); + await flushEffects(4); + + expect(mockRefreshCache).toHaveBeenCalledTimes(1); + expect(mockGetLocal).toHaveBeenCalledTimes(2); + expect(mockGetAll).not.toHaveBeenCalled(); + expect(getLatestValue().skills).toEqual([refreshedSkill]); + }); + + it("关闭 repo 拉取时卸载后重新读取本地技能", async () => { + const localSkill = createSkill({ directory: "local-skill" }); + mockGetLocal.mockResolvedValueOnce([localSkill]).mockResolvedValueOnce([]); + + await renderHook("codex", false); + + await act(async () => { + await getLatestValue().uninstall("local-skill"); + }); + await flushEffects(4); + + expect(mockUninstall).toHaveBeenCalledWith("local-skill", "codex"); + expect(mockGetLocal).toHaveBeenCalledTimes(2); + expect(mockGetAll).not.toHaveBeenCalled(); + expect(getLatestValue().skills).toEqual([]); + }); }); diff --git a/src/hooks/useSkills.ts b/src/hooks/useSkills.ts index 17d940844..86b66310c 100644 --- a/src/hooks/useSkills.ts +++ b/src/hooks/useSkills.ts @@ -9,16 +9,24 @@ interface SkillsCache { } const CACHE_TTL_MS = 30_000; -const cache = new Map(); +type SkillsCacheMode = "catalog" | "local"; +type SkillsCacheKey = `${AppType}:${SkillsCacheMode}`; +const cache = new Map(); interface UseSkillsOptions { includeRepos?: boolean; } +function buildCacheKey(app: AppType, mode: SkillsCacheMode): SkillsCacheKey { + return `${app}:${mode}`; +} + export function useSkills(app: AppType = "lime", options?: UseSkillsOptions) { - const cached = cache.get(app); - const isCacheFresh = cached && Date.now() - cached.timestamp < CACHE_TTL_MS; const includeRepos = options?.includeRepos ?? true; + const cacheMode: SkillsCacheMode = includeRepos ? "catalog" : "local"; + const cacheKey = buildCacheKey(app, cacheMode); + const cached = cache.get(cacheKey); + const isCacheFresh = cached && Date.now() - cached.timestamp < CACHE_TTL_MS; const [skills, setSkills] = useState(cached?.skills ?? []); const [repos, setRepos] = useState(cached?.repos ?? []); @@ -30,16 +38,30 @@ export function useSkills(app: AppType = "lime", options?: UseSkillsOptions) { const updateCache = useCallback( (data: Skill[], reposData?: SkillRepo[]) => { - const prev = cache.get(app); - cache.set(app, { + const prev = cache.get(cacheKey); + cache.set(cacheKey, { skills: data, repos: reposData ?? prev?.repos ?? [], timestamp: Date.now(), }); }, - [app], + [cacheKey], ); + const fetchLocalSkills = useCallback(async () => { + try { + setLoading(true); + setError(null); + const data = await skillsApi.getLocal(app); + setSkills(data); + updateCache(data); + } catch (e) { + setError(e instanceof Error ? e.message : String(e)); + } finally { + setLoading(false); + } + }, [app, updateCache]); + const fetchAllSkills = useCallback( async (refreshRemote = false) => { try { @@ -59,18 +81,41 @@ export function useSkills(app: AppType = "lime", options?: UseSkillsOptions) { [app, updateCache], ); + const fetchSkillsAfterLocalChange = useCallback(async () => { + if (includeRepos) { + await fetchAllSkills(false); + return; + } + + await fetchLocalSkills(); + }, [fetchAllSkills, fetchLocalSkills, includeRepos]); + + const refreshSkills = useCallback( + async (refreshRemote = false) => { + await skillsApi.refreshCache(); + + if (includeRepos) { + await fetchAllSkills(refreshRemote); + return; + } + + await fetchLocalSkills(); + }, + [fetchAllSkills, fetchLocalSkills, includeRepos], + ); + const fetchRepos = useCallback(async () => { try { const data = await skillsApi.getRepos(); setRepos(data); - const prev = cache.get(app); + const prev = cache.get(cacheKey); if (prev) { prev.repos = data; } } catch (e) { console.error("Failed to fetch repos:", e); } - }, [app]); + }, [cacheKey]); useEffect(() => { if (initializedRef.current || isCacheFresh) { @@ -79,38 +124,21 @@ export function useSkills(app: AppType = "lime", options?: UseSkillsOptions) { } initializedRef.current = true; - skillsApi - .getLocal(app) - .then((localData) => { - setSkills(localData); - updateCache(localData); - setLoading(false); - }) - .catch((e) => { - setError(e instanceof Error ? e.message : String(e)); - setLoading(false); - }); + void fetchLocalSkills(); if (includeRepos) { fetchRepos(); } - }, [ - app, - fetchAllSkills, - fetchRepos, - includeRepos, - isCacheFresh, - updateCache, - ]); + }, [fetchLocalSkills, fetchRepos, includeRepos, isCacheFresh]); const install = async (directory: string) => { await skillsApi.install(directory, app); - await fetchAllSkills(false); + await fetchSkillsAfterLocalChange(); }; const uninstall = async (directory: string) => { await skillsApi.uninstall(directory, app); - await fetchAllSkills(false); + await fetchSkillsAfterLocalChange(); }; const addRepo = async (repo: SkillRepo) => { @@ -132,8 +160,7 @@ export function useSkills(app: AppType = "lime", options?: UseSkillsOptions) { remoteLoading, error, refresh: async () => { - await skillsApi.refreshCache(); - await fetchAllSkills(true); + await refreshSkills(true); }, install, uninstall, diff --git a/src/i18n/__tests__/loadNamespace.test.ts b/src/i18n/__tests__/loadNamespace.test.ts index 6255f7a71..8eaf8de87 100644 --- a/src/i18n/__tests__/loadNamespace.test.ts +++ b/src/i18n/__tests__/loadNamespace.test.ts @@ -395,23 +395,17 @@ describe("i18n namespace loader", () => { expect(resources[locale].agent).toHaveProperty( "agentChat.sessionOverview.queue.imageCount", ); - expect(resources[locale].agent).toHaveProperty( - "agentChat.imageWorkbenchPreview.status.complete.generate", - ); - expect(resources[locale].agent).toHaveProperty( - "agentChat.imageWorkbenchPreview.description.complete.single", - ); - expect(resources[locale].agent).toHaveProperty( - "agentChat.imageWorkbenchPreview.source.footnote", - ); expect(resources[locale].agent).toHaveProperty( "agentChat.imageWorkbenchPreview.media.alt", ); expect(resources[locale].agent).toHaveProperty( - "agentChat.imageWorkbenchPreview.meta.imageProgress", + "agentChat.imageWorkbenchPreview.media.open", ); expect(resources[locale].agent).toHaveProperty( - "agentChat.imageWorkbenchPreview.promptFallback", + "agentChat.imageWorkbenchPreview.placeholder.generating", + ); + expect(resources[locale].agent).toHaveProperty( + "agentChat.imageWorkbenchPreview.placeholder.failed", ); expect(resources[locale].agent).toHaveProperty( "agentChat.searchResultPreview.expandMore", @@ -446,9 +440,162 @@ describe("i18n namespace loader", () => { expect(resources[locale].agent).toHaveProperty( "agentChat.teamWorkspace.canvasToolbar.fitProgress", ); + expect(resources[locale].agent).toHaveProperty( + "agentChat.teamWorkspace.canvasStage.badge", + ); + expect(resources[locale].agent).toHaveProperty( + "agentChat.teamWorkspace.canvasStage.shortcutHint", + ); + expect(resources[locale].agent).toHaveProperty( + "agentChat.teamWorkspace.canvasStage.emptyTitle", + ); + expect(resources[locale].agent).toHaveProperty( + "agentChat.teamWorkspace.dock.currentProgress", + ); + expect(resources[locale].agent).toHaveProperty( + "agentChat.teamWorkspace.dock.processingCount", + ); + expect(resources[locale].agent).toHaveProperty( + "agentChat.teamWorkspace.dock.selectedPlan", + ); + expect(resources[locale].agent).toHaveProperty( + "agentChat.teamWorkspace.dock.aria.expand", + ); expect(resources[locale].agent).toHaveProperty( "agentChat.teamWorkspace.teamActions.waitAny", ); + expect(resources[locale].agent).toHaveProperty( + "agentChat.teamWorkspace.board.canvas.title", + ); + expect(resources[locale].agent).toHaveProperty( + "agentChat.teamWorkspace.board.canvas.subtitle.runtime", + ); + expect(resources[locale].agent).toHaveProperty( + "agentChat.teamWorkspace.boardChrome.headline.runningWithQueued", + ); + expect(resources[locale].agent).toHaveProperty( + "agentChat.teamWorkspace.boardChrome.hint.runtime", + ); + expect(resources[locale].agent).toHaveProperty( + "agentChat.teamWorkspace.boardChrome.chip.focus", + ); + expect(resources[locale].agent).toHaveProperty( + "agentChat.teamWorkspace.boardChrome.statusSummary", + ); + expect(resources[locale].agent).toHaveProperty( + "agentChat.teamWorkspace.activityPreview.line", + ); + expect(resources[locale].agent).toHaveProperty( + "agentChat.teamWorkspace.activityPreview.status.message", + ); + expect(resources[locale].agent).toHaveProperty( + "agentChat.teamWorkspace.activityPreview.messageTitle.output", + ); + expect(resources[locale].agent).toHaveProperty( + "agentChat.teamWorkspace.activityPreview.error.syncFailed", + ); + expect(resources[locale].agent).toHaveProperty( + "agentChat.teamWorkspace.liveRuntime.title.statusChanged", + ); + expect(resources[locale].agent).toHaveProperty( + "agentChat.teamWorkspace.liveRuntime.detail.toolProcessing", + ); + expect(resources[locale].agent).toHaveProperty( + "agentChat.teamWorkspace.liveRuntime.lifecycle.turnFailed.detail", + ); + expect(resources[locale].agent).toHaveProperty( + "agentChat.teamWorkspace.liveRuntime.status.retry", + ); + expect(resources[locale].agent).toHaveProperty( + "agentChat.teamWorkspace.overview.selectedSession.focusBadge", + ); + expect(resources[locale].agent).toHaveProperty( + "agentChat.teamWorkspace.overview.taskCount.waitable", + ); + expect(resources[locale].agent).toHaveProperty( + "agentChat.teamWorkspace.runtimeStatus.completed", + ); + expect(resources[locale].agent).toHaveProperty( + "agentChat.teamWorkspace.operations.heading", + ); + expect(resources[locale].agent).toHaveProperty( + "agentChat.teamWorkspace.operations.recentCount", + ); + expect(resources[locale].agent).toHaveProperty( + "agentChat.teamWorkspace.operations.wait.resolved", + ); + expect(resources[locale].agent).toHaveProperty( + "agentChat.teamWorkspace.operations.control.closeOne", + ); + expect(resources[locale].agent).toHaveProperty( + "agentChat.teamWorkspace.canvasLane.runtimeDetail.recentProgress", + ); + expect(resources[locale].agent).toHaveProperty( + "agentChat.teamWorkspace.canvasLane.empty.syncLoading", + ); + expect(resources[locale].agent).toHaveProperty( + "agentChat.teamWorkspace.canvasLane.planned.statusHint", + ); + expect(resources[locale].agent).toHaveProperty( + "agentChat.teamWorkspace.formation.hint.formed", + ); + expect(resources[locale].agent).toHaveProperty( + "agentChat.teamWorkspace.formation.summaryBadge.planRoleCount", + ); + expect(resources[locale].agent).toHaveProperty( + "agentChat.teamWorkspace.formation.summaryBadge.referenceLabel", + ); + expect(resources[locale].agent).toHaveProperty( + "agentChat.teamWorkspace.formation.summaryBadge.taskCount", + ); + expect(resources[locale].agent).toHaveProperty( + "agentChat.teamWorkspace.formation.detail.hint.runtimeWithReference", + ); + expect(resources[locale].agent).toHaveProperty( + "agentChat.teamWorkspace.formation.detail.roleSection.runtime", + ); + expect(resources[locale].agent).toHaveProperty( + "agentChat.teamWorkspace.formation.status.formed.title", + ); + expect(resources[locale].agent).toHaveProperty( + "agentChat.teamWorkspace.formation.memberStatus.planned", + ); + expect(resources[locale].agent).toHaveProperty( + "agentChat.teamWorkspace.selectedSession.header.eyebrow", + ); + expect(resources[locale].agent).toHaveProperty( + "agentChat.teamWorkspace.selectedSession.collaboration.wait", + ); + expect(resources[locale].agent).toHaveProperty( + "agentChat.teamWorkspace.selectedSession.activity.entryCount", + ); + expect(resources[locale].agent).toHaveProperty( + "agentChat.teamWorkspace.selectedSession.footer.updatedAt", + ); + expect(resources[locale].agent).toHaveProperty( + "agentChat.teamWorkspace.selectedSession.detail.setting.preset", + ); + expect(resources[locale].agent).toHaveProperty( + "agentChat.teamWorkspace.selectedSession.detail.metadata.createdFromTurn", + ); + expect(resources[locale].agent).toHaveProperty( + "agentChat.teamWorkspace.selectedSession.detail.sessionType.subAgent", + ); + expect(resources[locale].agent).toHaveProperty( + "agentChat.teamWorkspace.selectedSession.detail.roleHint.executor", + ); + expect(resources[locale].agent).toHaveProperty( + "agentChat.teamWorkspace.teamPreset.codeTriageTeam", + ); + expect(resources[locale].agent).toHaveProperty( + "agentChat.teamWorkspace.control.resume.resumedOne", + ); + expect(resources[locale].agent).toHaveProperty( + "agentChat.teamWorkspace.control.wait.enteredStatusOne", + ); + expect(resources[locale].agent).toHaveProperty( + "agentChat.teamWorkspace.control.sendInput.emptyError", + ); expect(resources[locale].workspace).toHaveProperty( "workspace.browserExistingSession.presentation.status.attached.label", ); diff --git a/src/i18n/__tests__/types.test.ts b/src/i18n/__tests__/types.test.ts index a467f87ec..177010f2d 100644 --- a/src/i18n/__tests__/types.test.ts +++ b/src/i18n/__tests__/types.test.ts @@ -286,25 +286,14 @@ function assertI18nKeyTypes() { ns: "agent", countLabel: "2", }); - i18n.t("agentChat.imageWorkbenchPreview.status.complete.generate", { - ns: "agent", - }); - i18n.t("agentChat.imageWorkbenchPreview.description.complete.single", { - ns: "agent", - result: "image result", - }); - i18n.t("agentChat.imageWorkbenchPreview.source.footnote", { - ns: "agent", - label: "Reference images", - summary: "Attached 2 reference images.", - }); i18n.t("agentChat.imageWorkbenchPreview.media.alt", { ns: "agent" }); - i18n.t("agentChat.imageWorkbenchPreview.meta.imageProgress", { + i18n.t("agentChat.imageWorkbenchPreview.media.open", { ns: "agent" }); + i18n.t("agentChat.imageWorkbenchPreview.placeholder.generating", { + ns: "agent", + }); + i18n.t("agentChat.imageWorkbenchPreview.placeholder.failed", { ns: "agent", - current: 2, - expected: 4, }); - i18n.t("agentChat.imageWorkbenchPreview.promptFallback", { ns: "agent" }); i18n.t("agentChat.searchResultPreview.expandMore", { ns: "agent", countLabel: "2", @@ -348,7 +337,167 @@ function assertI18nKeyTypes() { count: 3, }); i18n.t("agentChat.teamWorkspace.canvasToolbar.fitProgress", { ns: "agent" }); + i18n.t("agentChat.teamWorkspace.canvasStage.badge", { ns: "agent" }); + i18n.t("agentChat.teamWorkspace.canvasStage.shortcutHint", { ns: "agent" }); + i18n.t("agentChat.teamWorkspace.canvasStage.emptyTitle", { ns: "agent" }); + i18n.t("agentChat.teamWorkspace.dock.currentProgress", { ns: "agent" }); + i18n.t("agentChat.teamWorkspace.dock.processingCount", { + ns: "agent", + formattedCount: "2", + }); + i18n.t("agentChat.teamWorkspace.dock.selectedPlan", { + ns: "agent", + label: "Writers", + }); + i18n.t("agentChat.teamWorkspace.dock.aria.expand", { ns: "agent" }); i18n.t("agentChat.teamWorkspace.teamActions.waitAny", { ns: "agent" }); + i18n.t("agentChat.teamWorkspace.board.canvas.title", { ns: "agent" }); + i18n.t("agentChat.teamWorkspace.board.canvas.subtitle.runtime", { + ns: "agent", + formattedCount: "2", + }); + i18n.t("agentChat.teamWorkspace.boardChrome.headline.runningWithQueued", { + ns: "agent", + runningCount: "1", + queuedCount: "1", + }); + i18n.t("agentChat.teamWorkspace.boardChrome.hint.runtime", { ns: "agent" }); + i18n.t("agentChat.teamWorkspace.boardChrome.chip.focus", { + ns: "agent", + name: "Researcher", + }); + i18n.t("agentChat.teamWorkspace.boardChrome.statusSummary", { + ns: "agent", + status: "Running", + formattedCount: "1", + }); + i18n.t("agentChat.teamWorkspace.activityPreview.line", { + ns: "agent", + title: "Reply", + detail: "Done", + }); + i18n.t("agentChat.teamWorkspace.activityPreview.status.message", { + ns: "agent", + }); + i18n.t("agentChat.teamWorkspace.activityPreview.messageTitle.output", { + ns: "agent", + }); + i18n.t("agentChat.teamWorkspace.activityPreview.error.syncFailed", { + ns: "agent", + }); + i18n.t("agentChat.teamWorkspace.liveRuntime.title.statusChanged", { + ns: "agent", + }); + i18n.t("agentChat.teamWorkspace.liveRuntime.detail.toolProcessing", { + ns: "agent", + target: "current step", + }); + i18n.t("agentChat.teamWorkspace.liveRuntime.lifecycle.turnFailed.detail", { + ns: "agent", + }); + i18n.t("agentChat.teamWorkspace.liveRuntime.status.retry", { ns: "agent" }); + i18n.t("agentChat.teamWorkspace.overview.selectedSession.focusBadge", { + ns: "agent", + }); + i18n.t("agentChat.teamWorkspace.overview.taskCount.waitable", { + ns: "agent", + formattedCount: "2", + }); + i18n.t("agentChat.teamWorkspace.runtimeStatus.completed", { ns: "agent" }); + i18n.t("agentChat.teamWorkspace.operations.heading", { ns: "agent" }); + i18n.t("agentChat.teamWorkspace.operations.recentCount", { + ns: "agent", + formattedCount: "2", + }); + i18n.t("agentChat.teamWorkspace.operations.wait.resolved", { + ns: "agent", + sessionName: "Researcher", + status: "Completed", + }); + i18n.t("agentChat.teamWorkspace.operations.control.closeOne", { + ns: "agent", + sessionName: "Researcher", + }); + i18n.t("agentChat.teamWorkspace.canvasLane.runtimeDetail.recentProgress", { + ns: "agent", + status: "Running", + }); + i18n.t("agentChat.teamWorkspace.canvasLane.empty.syncLoading", { + ns: "agent", + }); + i18n.t("agentChat.teamWorkspace.canvasLane.planned.statusHint", { + ns: "agent", + }); + i18n.t("agentChat.teamWorkspace.formation.hint.formed", { ns: "agent" }); + i18n.t("agentChat.teamWorkspace.formation.summaryBadge.planRoleCount", { + ns: "agent", + formattedCount: "2", + }); + i18n.t("agentChat.teamWorkspace.formation.summaryBadge.referenceLabel", { + ns: "agent", + label: "Writers", + }); + i18n.t("agentChat.teamWorkspace.formation.summaryBadge.taskCount", { + ns: "agent", + formattedCount: "2", + }); + i18n.t("agentChat.teamWorkspace.formation.detail.hint.runtimeWithReference", { + ns: "agent", + }); + i18n.t("agentChat.teamWorkspace.formation.detail.roleSection.runtime", { + ns: "agent", + }); + i18n.t("agentChat.teamWorkspace.formation.status.formed.title", { + ns: "agent", + }); + i18n.t("agentChat.teamWorkspace.formation.memberStatus.planned", { + ns: "agent", + }); + i18n.t("agentChat.teamWorkspace.selectedSession.header.eyebrow", { + ns: "agent", + }); + i18n.t("agentChat.teamWorkspace.selectedSession.collaboration.wait", { + ns: "agent", + }); + i18n.t("agentChat.teamWorkspace.selectedSession.activity.entryCount", { + ns: "agent", + formattedCount: "2", + }); + i18n.t("agentChat.teamWorkspace.selectedSession.footer.updatedAt", { + ns: "agent", + updatedAt: "just now", + }); + i18n.t("agentChat.teamWorkspace.selectedSession.detail.setting.preset", { + ns: "agent", + label: "Writers", + }); + i18n.t( + "agentChat.teamWorkspace.selectedSession.detail.metadata.createdFromTurn", + { + ns: "agent", + turnId: "turn-42", + }, + ); + i18n.t( + "agentChat.teamWorkspace.selectedSession.detail.sessionType.subAgent", + { + ns: "agent", + }, + ); + i18n.t("agentChat.teamWorkspace.selectedSession.detail.roleHint.executor", { + ns: "agent", + }); + i18n.t("agentChat.teamWorkspace.teamPreset.codeTriageTeam", { + ns: "agent", + }); + i18n.t("agentChat.teamWorkspace.control.resume.resumedOne", { ns: "agent" }); + i18n.t("agentChat.teamWorkspace.control.wait.enteredStatusOne", { + ns: "agent", + status: "Completed", + }); + i18n.t("agentChat.teamWorkspace.control.sendInput.emptyError", { + ns: "agent", + }); i18n.t("navigation.sidebar.items.homeGeneral", { ns: "navigation" }); i18n.t( "workspace.browserExistingSession.presentation.status.attached.label", diff --git a/src/i18n/resources/en-US/agent.json b/src/i18n/resources/en-US/agent.json index 9d347d309..861fc8c91 100644 --- a/src/i18n/resources/en-US/agent.json +++ b/src/i18n/resources/en-US/agent.json @@ -146,58 +146,18 @@ "agentChat.harnessVerification.section.failureFocusTitle": "Verification failure focus", "agentChat.harnessVerification.section.recoveredTitle": "Recovered results", "agentChat.harnessVerification.section.title": "Verification Results", - "agentChat.imageWorkbenchPreview.description.cancelled": "The task was cancelled and will not generate new image results.", - "agentChat.imageWorkbenchPreview.description.complete.multiple": "Returned {{count}} {{result}} item(s). Open the right panel to review and use them.", - "agentChat.imageWorkbenchPreview.description.complete.single": "The {{result}} is complete. Open the right panel to review and use it.", - "agentChat.imageWorkbenchPreview.description.complete.storyboard": "{{label}} is complete. Open the right panel to review and use it.", - "agentChat.imageWorkbenchPreview.description.failed.notRetryable": "Adjust the configuration or parameters before retrying.", - "agentChat.imageWorkbenchPreview.description.failed.retryable": "No usable result was returned. Adjust the prompt and try again.", - "agentChat.imageWorkbenchPreview.description.partial.multiple": "Returned {{returned}} / {{expected}} {{result}} item(s); the remaining results are not complete.", - "agentChat.imageWorkbenchPreview.description.partial.single": "Part of the {{result}} has synced. You can review it on the right.", - "agentChat.imageWorkbenchPreview.description.partial.storyboard": "{{label}} returned {{returned}} / {{expected}} frame(s). You can review it on the right.", - "agentChat.imageWorkbenchPreview.description.running.edit": "Processing the image edit. It will be replaced with the real result when ready.", - "agentChat.imageWorkbenchPreview.description.running.generate": "Generating the image. It will be replaced with the real result when ready.", - "agentChat.imageWorkbenchPreview.description.running.storyboard": "{{label}} has been created; {{expected}} frame(s) will fill into the same task card.", - "agentChat.imageWorkbenchPreview.description.running.variation": "Creating variations. They will be replaced with real results when ready.", - "agentChat.imageWorkbenchPreview.layout.storyboard": "3x3 storyboard", - "agentChat.imageWorkbenchPreview.media.alt": "Image task result", + "agentChat.imageWorkbenchPreview.media.alt": "Image result", + "agentChat.imageWorkbenchPreview.media.open": "Open image result", "agentChat.imageWorkbenchPreview.media.previewFailed": "Preview failed", - "agentChat.imageWorkbenchPreview.meta.imageCount": "{{count}} images", - "agentChat.imageWorkbenchPreview.meta.imageProgress": "{{current}}/{{expected}} images", - "agentChat.imageWorkbenchPreview.placeholder.cancelled": "Task cancelled", - "agentChat.imageWorkbenchPreview.placeholder.failed": "No successful result yet", + "agentChat.imageWorkbenchPreview.tool.generation": "Image Generation", + "agentChat.imageWorkbenchPreview.tool.editing": "Image Editing", + "agentChat.imageWorkbenchPreview.tool.redraw": "Image Redraw", + "agentChat.imageWorkbenchPreview.placeholder.cancelled": "Cancelled", + "agentChat.imageWorkbenchPreview.placeholder.editing": "Editing image", + "agentChat.imageWorkbenchPreview.placeholder.failed": "Generation failed", + "agentChat.imageWorkbenchPreview.placeholder.generating": "Generating image", "agentChat.imageWorkbenchPreview.placeholder.imageUnavailable": "Image is temporarily unavailable", - "agentChat.imageWorkbenchPreview.placeholder.synced": "Result synced", - "agentChat.imageWorkbenchPreview.promptFallback": "No prompt provided for this task.", - "agentChat.imageWorkbenchPreview.result.edit": "edited image result", - "agentChat.imageWorkbenchPreview.result.generate": "image result", - "agentChat.imageWorkbenchPreview.result.variation": "variation result", - "agentChat.imageWorkbenchPreview.slot.cancelled": "Cancelled", - "agentChat.imageWorkbenchPreview.slot.failed": "Frame failed", - "agentChat.imageWorkbenchPreview.slot.pending": "Pending", - "agentChat.imageWorkbenchPreview.slot.waiting": "Waiting", - "agentChat.imageWorkbenchPreview.source.editSummary": "This task will continue editing from the existing image result.", - "agentChat.imageWorkbenchPreview.source.footnote": "{{label}}: {{summary}}", - "agentChat.imageWorkbenchPreview.source.ref": "Referenced {{ref}}", - "agentChat.imageWorkbenchPreview.source.referenceCount": "Attached {{count}} reference image(s).", - "agentChat.imageWorkbenchPreview.source.referenceLabel": "Reference images", - "agentChat.imageWorkbenchPreview.source.sourceCount": "Referenced {{count}} source image(s).", - "agentChat.imageWorkbenchPreview.source.sourceLabel": "Source image", - "agentChat.imageWorkbenchPreview.source.variationSummary": "This task will use the reference images to produce new variations.", - "agentChat.imageWorkbenchPreview.status.cancelled": "Cancelled", - "agentChat.imageWorkbenchPreview.status.complete.edit": "Edited", - "agentChat.imageWorkbenchPreview.status.complete.generate": "Generated", - "agentChat.imageWorkbenchPreview.status.complete.variation": "Varied", - "agentChat.imageWorkbenchPreview.status.failed.edit": "Edit failed", - "agentChat.imageWorkbenchPreview.status.failed.generate": "Generation failed", - "agentChat.imageWorkbenchPreview.status.failed.variation": "Variation failed", - "agentChat.imageWorkbenchPreview.status.partial": "Partially complete", - "agentChat.imageWorkbenchPreview.status.running.edit": "Editing", - "agentChat.imageWorkbenchPreview.status.running.generate": "Generating", - "agentChat.imageWorkbenchPreview.status.running.preparing": "Preparing", - "agentChat.imageWorkbenchPreview.status.running.queued": "Queued", - "agentChat.imageWorkbenchPreview.status.running.variation": "Creating variations", - "agentChat.imageWorkbenchPreview.statusDescription": " · {{description}}", + "agentChat.imageWorkbenchPreview.placeholder.redrawing": "Redrawing image", "agentChat.incidentPanel.empty": "No active incidents detected", "agentChat.incidentPanel.priorityBadge": "{{severity}} priority", "agentChat.messageList.firstTokenStatus.cancelled.detail": "This request has stopped.", @@ -297,11 +257,270 @@ "agentChat.teamWorkspace.canvasToolbar.zoom": "Zoom {{percent}}%", "agentChat.teamWorkspace.canvasToolbar.zoomIn": "Zoom in", "agentChat.teamWorkspace.canvasToolbar.zoomOut": "Zoom out", + "agentChat.teamWorkspace.canvasStage.badge": "Current progress", + "agentChat.teamWorkspace.canvasStage.shortcutHint": "Drag to arrange tasks · Space to pan · A arrange · F focus", + "agentChat.teamWorkspace.canvasStage.emptyTitle": "No current progress", + "agentChat.teamWorkspace.dock.surfaceTitle": "Generate", + "agentChat.teamWorkspace.dock.currentProgress": "Current progress", + "agentChat.teamWorkspace.dock.processingBudget": "{{activeCount}}/{{totalCount}} running", + "agentChat.teamWorkspace.dock.processingCount": "{{formattedCount}} running", + "agentChat.teamWorkspace.dock.waitingCount": "{{formattedCount}} waiting", + "agentChat.teamWorkspace.dock.taskCount": "{{formattedCount}} task(s)", + "agentChat.teamWorkspace.dock.selectedPlan": "Current team plan: {{label}}", + "agentChat.teamWorkspace.dock.unobstructiveBadge": "Keeps canvas clear", + "agentChat.teamWorkspace.dock.aria.open": "Open current progress", + "agentChat.teamWorkspace.dock.aria.collapse": "Collapse current progress", + "agentChat.teamWorkspace.dock.aria.expand": "Expand current progress", "agentChat.teamWorkspace.teamActions.closeCompleted": "Close completed tasks", "agentChat.teamWorkspace.teamActions.closing": "Closing...", "agentChat.teamWorkspace.teamActions.waitAny": "Wait for any task result", "agentChat.teamWorkspace.teamActions.waiting": "Waiting...", + "agentChat.teamWorkspace.board.detail.collapse": "Hide details", + "agentChat.teamWorkspace.board.detail.expand": "View details", + "agentChat.teamWorkspace.board.canvas.title": "Current progress", + "agentChat.teamWorkspace.board.canvas.subtitle.childRuntime": "Parallel tasks keep progress and results in their own panels; the main thread keeps only the necessary summary.", + "agentChat.teamWorkspace.board.canvas.subtitle.runtime": "{{formattedCount}} current progress lane(s) connected. Focus prioritizes the split currently in progress.", + "agentChat.teamWorkspace.board.canvas.subtitle.formation.forming": "Preparing the current task split. Once tasks are created, progress will update here independently.", + "agentChat.teamWorkspace.board.canvas.subtitle.formation.formed": "The current task split is ready. Once tasks are created, each progress panel will start running.", + "agentChat.teamWorkspace.board.canvas.subtitle.formation.failed": "This task split failed, so the current progress panel cannot be created yet.", + "agentChat.teamWorkspace.board.canvas.subtitle.formation.default": "After tasks are created, this expands into independent current progress.", + "agentChat.teamWorkspace.boardChrome.headline.parentOverviewFallback": "Main assistant overview", + "agentChat.teamWorkspace.boardChrome.headline.empty": "Tasks are split automatically when needed", + "agentChat.teamWorkspace.boardChrome.headline.runningWithQueued": "Tasks running · {{runningCount}} running / {{queuedCount}} queued", + "agentChat.teamWorkspace.boardChrome.headline.runningCount": "Tasks running · {{formattedCount}} running", + "agentChat.teamWorkspace.boardChrome.headline.running": "Tasks running", + "agentChat.teamWorkspace.boardChrome.headline.queuedCount": "Tasks preparing · {{formattedCount}} queued", + "agentChat.teamWorkspace.boardChrome.headline.queued": "Tasks preparing", + "agentChat.teamWorkspace.boardChrome.headline.completedCount": "{{formattedCount}} tasks completed", + "agentChat.teamWorkspace.boardChrome.headline.completed": "Tasks completed", + "agentChat.teamWorkspace.boardChrome.headline.retryCount": "{{formattedCount}} tasks need retry", + "agentChat.teamWorkspace.boardChrome.headline.retry": "Tasks need retry", + "agentChat.teamWorkspace.boardChrome.headline.connectedCount": "{{formattedCount}} current progress lane(s) connected", + "agentChat.teamWorkspace.boardChrome.headline.surfaceTitle": "Generate", + "agentChat.teamWorkspace.boardChrome.hint.childWithSiblings": "The current task is progressing alongside {{formattedCount}} parallel subtask(s)", + "agentChat.teamWorkspace.boardChrome.hint.childSingle": "This subtask is running now; results flow back to the main assistant.", + "agentChat.teamWorkspace.boardChrome.hint.empty": "The system splits work, orders tasks, and flows results back here when needed.", + "agentChat.teamWorkspace.boardChrome.hint.runtime": "This view shows which splits are active, their status, and where the latest updates landed.", + "agentChat.teamWorkspace.boardChrome.chip.focus": "Current focus {{name}}", + "agentChat.teamWorkspace.boardChrome.chip.updatedAt": "Updated {{updatedAt}}", + "agentChat.teamWorkspace.boardChrome.chip.waitable": "{{formattedCount}} running", + "agentChat.teamWorkspace.boardChrome.chip.completed": "{{formattedCount}} completed", + "agentChat.teamWorkspace.boardChrome.statusSummary": "{{status}} {{formattedCount}}", + "agentChat.teamWorkspace.overview.selectedSession.waitingTitle": "Waiting for task pickup", + "agentChat.teamWorkspace.overview.selectedSession.focusBadge": "Current focus", + "agentChat.teamWorkspace.overview.selectedSession.waitingBadge": "Waiting for pickup", + "agentChat.teamWorkspace.overview.compact.taskActionsLabel": "Task actions", + "agentChat.teamWorkspace.overview.compact.viewActionsLabel": "View", + "agentChat.teamWorkspace.overview.taskCount.waitable": "{{formattedCount}} task(s) running", + "agentChat.teamWorkspace.overview.taskCount.completed": "{{formattedCount}} task(s) completed", + "agentChat.teamWorkspace.formation.hint.forming": "The system is preparing the team split for this task. New tasks will start automatically once they are created.", + "agentChat.teamWorkspace.formation.hint.formed": "The team split is ready. New tasks will pick up the work once they are created.", + "agentChat.teamWorkspace.formation.hint.failed": "The team split failed, but you can still continue in the current task.", + "agentChat.teamWorkspace.formation.hint.default": "This area will expand into the current progress panel when needed.", + "agentChat.teamWorkspace.formation.emptyDetail.forming": "The system is preparing the team split from the current task. Once ready, this area will show role cards first, then attach real progress.", + "agentChat.teamWorkspace.formation.detail.hint.runtimeWithReference": "View the current task split and reference plan", + "agentChat.teamWorkspace.formation.detail.hint.planOnly": "View the current team plan", + "agentChat.teamWorkspace.formation.detail.summary.runtime": "Current summary", + "agentChat.teamWorkspace.formation.detail.summary.plan": "Plan summary", + "agentChat.teamWorkspace.formation.detail.referenceSummaryDefault": "The current task split follows the role preferences in the selected plan.", + "agentChat.teamWorkspace.formation.detail.roleSection.runtime": "Current task split", + "agentChat.teamWorkspace.formation.detail.roleSection.plan": "Role plan", + "agentChat.teamWorkspace.formation.emptyDetail.formed": "The team plan is ready. The canvas will show the plan first, then switch to current progress after tasks start.", + "agentChat.teamWorkspace.formation.emptyDetail.failed": "The team split failed, so current progress cannot be shown yet.", + "agentChat.teamWorkspace.formation.emptyDetail.default": "No task has picked this up yet. Once the system starts forming the team split, the details area will switch to a progress summary.", + "agentChat.teamWorkspace.formation.notice.forming": "The system is preparing the team split. This will show role cards first, then switch to independent current progress.", + "agentChat.teamWorkspace.formation.notice.formed": "The team plan is ready. After tasks are created, this view will transition from the plan to current progress.", + "agentChat.teamWorkspace.formation.notice.failed": "The team split failed, so no task has picked this up yet.", + "agentChat.teamWorkspace.formation.notice.default": "No task has picked this up yet. Once formation starts, this area will create independent current progress.", + "agentChat.teamWorkspace.formation.summaryBadge.planLabel": "Team plan · {{label}}", + "agentChat.teamWorkspace.formation.summaryBadge.planRoleCount": "{{formattedCount}} planned role(s)", + "agentChat.teamWorkspace.formation.summaryBadge.memberCount": "{{formattedCount}} active progress item(s)", + "agentChat.teamWorkspace.formation.summaryBadge.referenceLabel": "Reference plan · {{label}}", + "agentChat.teamWorkspace.formation.summaryBadge.taskCount": "{{formattedCount}} task(s)", + "agentChat.teamWorkspace.formation.panel.title": "Current task split", + "agentChat.teamWorkspace.formation.panel.description.failed": "The team split failed, so no more details are available yet.", + "agentChat.teamWorkspace.formation.panel.description.default": "This will show the current team plan first, then switch to current progress after tasks are created.", + "agentChat.teamWorkspace.formation.waitingHeadline": "Waiting for task pickup", + "agentChat.teamWorkspace.formation.status.forming.label": "Preparing", + "agentChat.teamWorkspace.formation.status.forming.title": "Preparing team split", + "agentChat.teamWorkspace.formation.status.formed.label": "Ready", + "agentChat.teamWorkspace.formation.status.formed.title": "Team split is ready", + "agentChat.teamWorkspace.formation.status.failed.label": "Preparation failed", + "agentChat.teamWorkspace.formation.status.failed.title": "Team split failed", + "agentChat.teamWorkspace.formation.memberStatus.planned": "Planned", + "agentChat.teamWorkspace.formation.memberStatus.spawning": "Connecting", + "agentChat.teamWorkspace.formation.memberStatus.running": "Running", + "agentChat.teamWorkspace.formation.memberStatus.waiting": "Waiting", + "agentChat.teamWorkspace.formation.memberStatus.completed": "Completed", + "agentChat.teamWorkspace.formation.memberStatus.failed": "Needs retry", + "agentChat.teamWorkspace.selectedSession.header.eyebrow": "Current view", + "agentChat.teamWorkspace.selectedSession.header.currentTaskBadge": "Current task", + "agentChat.teamWorkspace.selectedSession.header.action.resumePending": "Resuming...", + "agentChat.teamWorkspace.selectedSession.header.action.resume": "Resume work", + "agentChat.teamWorkspace.selectedSession.header.action.closePending": "Pausing...", + "agentChat.teamWorkspace.selectedSession.header.action.close": "Pause work", + "agentChat.teamWorkspace.selectedSession.header.action.openChild": "Switch progress", + "agentChat.teamWorkspace.selectedSession.header.action.open": "Open progress", + "agentChat.teamWorkspace.selectedSession.settings.heading": "Task split", + "agentChat.teamWorkspace.teamPreset.codeTriageTeam": "Code triage team", + "agentChat.teamWorkspace.teamPreset.researchTeam": "Research team", + "agentChat.teamWorkspace.teamPreset.contentCreationTeam": "Content creation team", + "agentChat.teamWorkspace.selectedSession.detail.setting.preset": "Plan {{label}}", + "agentChat.teamWorkspace.selectedSession.detail.setting.profile": "Style {{name}}", + "agentChat.teamWorkspace.selectedSession.detail.setting.role": "Role {{label}}", + "agentChat.teamWorkspace.selectedSession.detail.setting.theme": "Theme {{theme}}", + "agentChat.teamWorkspace.selectedSession.detail.metadata.role": "Role {{label}}", + "agentChat.teamWorkspace.selectedSession.detail.metadata.provider": "Provider {{name}}", + "agentChat.teamWorkspace.selectedSession.detail.metadata.model": "Model {{model}}", + "agentChat.teamWorkspace.selectedSession.detail.metadata.originTool": "Source {{tool}}", + "agentChat.teamWorkspace.selectedSession.detail.metadata.createdFromTurn": "From earlier task {{turnId}}", + "agentChat.teamWorkspace.selectedSession.detail.metadata.parentSession": "From {{name}}", + "agentChat.teamWorkspace.selectedSession.detail.sessionType.subAgent": "Subtask", + "agentChat.teamWorkspace.selectedSession.detail.sessionType.fork": "Supplementary branch", + "agentChat.teamWorkspace.selectedSession.detail.sessionType.user": "Main assistant", + "agentChat.teamWorkspace.selectedSession.detail.roleHint.explorer": "Analysis", + "agentChat.teamWorkspace.selectedSession.detail.roleHint.executor": "Execution", + "agentChat.teamWorkspace.selectedSession.detail.roleHint.reviewer": "Review", + "agentChat.teamWorkspace.selectedSession.detail.roleHint.writer": "Writing", + "agentChat.teamWorkspace.selectedSession.detail.roleHint.planner": "Planning", + "agentChat.teamWorkspace.selectedSession.detail.roleHint.researcher": "Research", + "agentChat.teamWorkspace.selectedSession.detail.roleHint.designer": "Design", + "agentChat.teamWorkspace.selectedSession.detail.roleHint.design": "Design", + "agentChat.teamWorkspace.selectedSession.detail.roleHint.editor": "Editing", + "agentChat.teamWorkspace.selectedSession.detail.roleHint.orchestrator": "Main assistant", + "agentChat.teamWorkspace.selectedSession.footer.updatedAt": "Updated {{updatedAt}}", + "agentChat.teamWorkspace.selectedSession.collaboration.heading": "Continue work", + "agentChat.teamWorkspace.selectedSession.collaboration.waitReady": "Result can be checked directly", + "agentChat.teamWorkspace.selectedSession.collaboration.waitPending": "Waiting...", + "agentChat.teamWorkspace.selectedSession.collaboration.wait": "Wait 30 seconds for result", + "agentChat.teamWorkspace.selectedSession.collaboration.waitHint": "Use this only when the current response really depends on this task result.", + "agentChat.teamWorkspace.selectedSession.collaboration.inputPlaceholder": "Add instructions, constraints, or ask this task to continue to the next step.", + "agentChat.teamWorkspace.selectedSession.collaboration.sendPending": "Sending...", + "agentChat.teamWorkspace.selectedSession.collaboration.send": "Send note", + "agentChat.teamWorkspace.selectedSession.collaboration.interruptPending": "Interrupting...", + "agentChat.teamWorkspace.selectedSession.collaboration.interrupt": "Insert note now", + "agentChat.teamWorkspace.selectedSession.collaboration.inputHint": "This note is sent only to the current task and does not affect other parallel tasks.", + "agentChat.teamWorkspace.selectedSession.activity.heading": "Full progress", + "agentChat.teamWorkspace.selectedSession.activity.autoRefresh": "Auto-refresh while running", + "agentChat.teamWorkspace.selectedSession.activity.errorPrefix": "Latest progress unavailable: ", + "agentChat.teamWorkspace.selectedSession.activity.errorFallback": "Sync failed", + "agentChat.teamWorkspace.selectedSession.activity.emptyReady": "This task has no new progress to show yet.", + "agentChat.teamWorkspace.selectedSession.activity.loading": "Syncing the latest progress for this task...", + "agentChat.teamWorkspace.selectedSession.activity.feedHeading": "Progress log", + "agentChat.teamWorkspace.selectedSession.activity.entryCount": "{{formattedCount}} item(s)", + "agentChat.teamWorkspace.activityPreview.line": "{{title}}: {{detail}}", + "agentChat.teamWorkspace.activityPreview.status.error": "Error", + "agentChat.teamWorkspace.activityPreview.status.warning": "Warning", + "agentChat.teamWorkspace.activityPreview.status.inProgress": "In progress", + "agentChat.teamWorkspace.activityPreview.status.failed": "Failed", + "agentChat.teamWorkspace.activityPreview.status.completed": "Completed", + "agentChat.teamWorkspace.activityPreview.status.message": "Message", + "agentChat.teamWorkspace.activityPreview.messageTitle.error": "Error", + "agentChat.teamWorkspace.activityPreview.messageTitle.output": "Output", + "agentChat.teamWorkspace.activityPreview.messageTitle.reply": "Reply", + "agentChat.teamWorkspace.activityPreview.error.syncFailed": "Failed to sync recent progress", + "agentChat.teamWorkspace.activityPreview.error.historyReadFailed": "Failed to read history", + "agentChat.teamWorkspace.liveRuntime.title.statusChanged": "Status changed", + "agentChat.teamWorkspace.liveRuntime.detail.statusChanged": "Status event received; changed to {{status}}.", + "agentChat.teamWorkspace.liveRuntime.title.textDraft": "Generating content", + "agentChat.teamWorkspace.liveRuntime.title.thinkingDraft": "Organizing thoughts", + "agentChat.teamWorkspace.liveRuntime.title.currentProgress": "Current progress", + "agentChat.teamWorkspace.liveRuntime.title.processing": "Processing", + "agentChat.teamWorkspace.liveRuntime.title.tool": "{{status}} · {{toolName}}", + "agentChat.teamWorkspace.liveRuntime.title.toolTargetFallback": "current step", + "agentChat.teamWorkspace.liveRuntime.detail.toolCompletedFallback": "Current step is complete.", + "agentChat.teamWorkspace.liveRuntime.detail.toolProcessing": "Working on {{target}}.", + "agentChat.teamWorkspace.liveRuntime.title.warning": "Warning", + "agentChat.teamWorkspace.liveRuntime.title.error": "Error", + "agentChat.teamWorkspace.liveRuntime.status.processing": "Processing", + "agentChat.teamWorkspace.liveRuntime.status.queued": "Queued", + "agentChat.teamWorkspace.liveRuntime.status.completed": "Completed", + "agentChat.teamWorkspace.liveRuntime.status.retry": "Needs retry", + "agentChat.teamWorkspace.liveRuntime.status.warning": "Warning", + "agentChat.teamWorkspace.liveRuntime.lifecycle.queueAdded.title": "Queued", + "agentChat.teamWorkspace.liveRuntime.lifecycle.queueAdded.detail": "New instructions were received; this subtask will continue after the previous one completes.", + "agentChat.teamWorkspace.liveRuntime.lifecycle.queueStarted.title": "Started", + "agentChat.teamWorkspace.liveRuntime.lifecycle.queueStarted.detail": "This subtask has started processing the current task.", + "agentChat.teamWorkspace.liveRuntime.lifecycle.turnStarted.title": "Continuing", + "agentChat.teamWorkspace.liveRuntime.lifecycle.turnStarted.detail": "This subtask is progressing the current content.", + "agentChat.teamWorkspace.liveRuntime.lifecycle.turnCompleted.title": "Step complete", + "agentChat.teamWorkspace.liveRuntime.lifecycle.turnCompleted.detail": "This step is complete; syncing the latest result.", + "agentChat.teamWorkspace.liveRuntime.lifecycle.turnFailed.title": "Failed", + "agentChat.teamWorkspace.liveRuntime.lifecycle.turnFailed.detail": "This step failed. Check the error details.", + "agentChat.teamWorkspace.operations.heading": "Task progress", + "agentChat.teamWorkspace.operations.recentCount": "Latest {{formattedCount}}", + "agentChat.teamWorkspace.operations.updatedNow": "just now", + "agentChat.teamWorkspace.operations.taskFallback": "this task", + "agentChat.teamWorkspace.operations.title.waitTimedOut": "Wait timed out", + "agentChat.teamWorkspace.operations.title.waitResolved": "Result received", + "agentChat.teamWorkspace.operations.title.resume": "Resume work", + "agentChat.teamWorkspace.operations.title.closeCompleted": "Collapse completed", + "agentChat.teamWorkspace.operations.title.close": "Pause work", + "agentChat.teamWorkspace.operations.wait.timedOut": "The wait for results just timed out; {{formattedCount}} task(s) are still running.", + "agentChat.teamWorkspace.operations.wait.resolved": "Just received new results from {{sessionName}}; current status is {{status}}.", + "agentChat.teamWorkspace.operations.control.resumeCount": "Just resumed {{formattedCount}} task(s).", + "agentChat.teamWorkspace.operations.control.resumeOne": "Just resumed {{sessionName}}.", + "agentChat.teamWorkspace.operations.control.closeCompleted": "Just collapsed {{formattedCount}} completed task(s).", + "agentChat.teamWorkspace.operations.control.closeCount": "Just paused {{formattedCount}} task(s).", + "agentChat.teamWorkspace.operations.control.closeOne": "Just paused {{sessionName}}.", + "agentChat.teamWorkspace.canvasLane.updatedNow": "just now", + "agentChat.teamWorkspace.canvasLane.runtimeDetail.waitingCount": "Waiting {{formattedCount}}", + "agentChat.teamWorkspace.canvasLane.runtimeDetail.recentProgress": "Latest progress {{status}}", + "agentChat.teamWorkspace.canvasLane.runtimeDetail.processingCount": "Running {{activeCount}}/{{totalCount}}", + "agentChat.teamWorkspace.canvasLane.runtimeDetail.stableProcessing": "Steady mode", + "agentChat.teamWorkspace.canvasLane.empty.syncFailed": "Failed to sync latest content", + "agentChat.teamWorkspace.canvasLane.empty.syncLoading": "Syncing the latest content for this task...", + "agentChat.teamWorkspace.canvasLane.empty.queued": "This task is queued and will start shortly.", + "agentChat.teamWorkspace.canvasLane.empty.running": "This task is running; latest progress will keep refreshing here.", + "agentChat.teamWorkspace.canvasLane.empty.completed": "This part is complete, and results will continue flowing into the current content.", + "agentChat.teamWorkspace.canvasLane.empty.failed": "This step did not finish successfully. Check details below and decide whether to continue.", + "agentChat.teamWorkspace.canvasLane.empty.default": "This task does not have displayable output yet.", + "agentChat.teamWorkspace.canvasLane.session.summaryEmpty": "No task summary yet. Open details to view the full context.", + "agentChat.teamWorkspace.canvasLane.memberHint.spawning": "Connecting this task", + "agentChat.teamWorkspace.canvasLane.memberHint.running": "This task is running", + "agentChat.teamWorkspace.canvasLane.memberHint.waiting": "Waiting for more instructions", + "agentChat.teamWorkspace.canvasLane.memberHint.completed": "This step is complete", + "agentChat.teamWorkspace.canvasLane.memberHint.failed": "This step needs a retry", + "agentChat.teamWorkspace.canvasLane.memberHint.connected": "Connected to a real task", + "agentChat.teamWorkspace.canvasLane.memberHint.waitingAssignment": "Waiting for task handoff", + "agentChat.teamWorkspace.canvasLane.planned.badge": "Not started", + "agentChat.teamWorkspace.canvasLane.planned.statusHint": "Waiting for the system to split out this task", + "agentChat.teamWorkspace.canvasLane.runtime.updatedAtWaiting": "Waiting for task handoff", + "agentChat.teamWorkspace.canvasLane.planned.updatedAt": "Planned assignment", + "agentChat.teamWorkspace.runtimeStatus.idle": "Not started", + "agentChat.teamWorkspace.runtimeStatus.queued": "Queued", + "agentChat.teamWorkspace.runtimeStatus.running": "Running", "agentChat.teamWorkspace.runtimeStatus.completed": "Completed", + "agentChat.teamWorkspace.runtimeStatus.failed": "Needs retry", + "agentChat.teamWorkspace.runtimeStatus.aborted": "Paused", + "agentChat.teamWorkspace.runtimeStatus.closed": "Stopped", + "agentChat.teamWorkspace.control.close.cascadeStopped": "Stopped {{formattedCount}} tasks in the cascade", + "agentChat.teamWorkspace.control.close.stoppedOne": "This task has stopped", + "agentChat.teamWorkspace.control.close.noChange": "Task status is {{status}}; no new stop change was made", + "agentChat.teamWorkspace.control.close.failedFallback": "Failed to stop task", + "agentChat.teamWorkspace.control.resume.cascadeResumed": "Resumed {{formattedCount}} tasks in the cascade", + "agentChat.teamWorkspace.control.resume.resumedOne": "This task has resumed", + "agentChat.teamWorkspace.control.resume.noChange": "Task status is {{status}}; no new resume change was made", + "agentChat.teamWorkspace.control.resume.failedFallback": "Failed to resume task", + "agentChat.teamWorkspace.control.wait.timeoutOne": "Wait timed out; this task is not in a final state yet", + "agentChat.teamWorkspace.control.wait.enteredStatusOne": "This task entered {{status}} status", + "agentChat.teamWorkspace.control.wait.noActiveError": "No active tasks are available to wait for", + "agentChat.teamWorkspace.control.wait.timeoutTeam": "Wait timed out; active team tasks are not in a final state yet", + "agentChat.teamWorkspace.control.wait.enteredStatusTeam": "Team tasks entered {{status}} status", + "agentChat.teamWorkspace.control.wait.failedFallback": "Failed to wait for task", + "agentChat.teamWorkspace.control.closeCompleted.noCompletedError": "No completed tasks are available to close", + "agentChat.teamWorkspace.control.closeCompleted.cascadeClosed": "Collapsed {{formattedCount}} tasks in the cascade", + "agentChat.teamWorkspace.control.closeCompleted.closedCount": "Collapsed {{formattedCount}} completed tasks", + "agentChat.teamWorkspace.control.closeCompleted.partialFailedFallback": "Some completed tasks could not be collapsed", + "agentChat.teamWorkspace.control.sendInput.emptyError": "Enter content to send to this task", + "agentChat.teamWorkspace.control.sendInput.sentInterrupt": "Interrupted current execution and sent new instructions", + "agentChat.teamWorkspace.control.sendInput.sent": "Sent follow-up instructions to this task", + "agentChat.teamWorkspace.control.sendInput.failedFallback": "Failed to send task instructions", + "agentChat.teamWorkspace.control.stop.pausedCount": "Paused {{formattedCount}} tasks", + "agentChat.teamWorkspace.control.stop.pausedOne": "Paused this task", + "agentChat.teamWorkspace.control.stop.requestedCount": "Sent pause requests to {{formattedCount}} tasks", + "agentChat.teamWorkspace.control.stop.requestedOne": "Sent a pause request to this task", + "agentChat.teamWorkspace.control.stop.failedFallback": "Failed to stop task", "agentChat.threadReliability.routingEvidence.evidence": "Evidence", "agentChat.threadReliability.routingEvidence.fallbackSlot": "runtime routing", "agentChat.threadReliability.routingEvidence.firstText": "First text", @@ -383,47 +602,48 @@ "capabilityDraft.panel.title": "Capability Drafts", "capabilityDraft.registeredPanel.action.auditRecentRun": "Audit latest run", "capabilityDraft.registeredPanel.action.auditRunning": "Auditing", - "capabilityDraft.registeredPanel.action.createManagedJobDraft": "Create Managed Job draft", - "capabilityDraft.registeredPanel.action.enableRuntime": "Enable for this turn", + "capabilityDraft.registeredPanel.action.createManagedJobDraft": "Set schedule", + "capabilityDraft.registeredPanel.action.enableRuntime": "Try once", "capabilityDraft.registeredPanel.action.enableRuntimeHelp": "Only writes session enable metadata; it does not create automation.", - "capabilityDraft.registeredPanel.action.pauseManagedJob": "Pause Managed Job", + "capabilityDraft.registeredPanel.action.pauseManagedJob": "Pause schedule", "capabilityDraft.registeredPanel.action.refresh": "Refresh", - "capabilityDraft.registeredPanel.action.resumeManagedJob": "Resume Managed Job", - "capabilityDraft.registeredPanel.agentEnvelope.action.blocked": "Resolve blocker first", - "capabilityDraft.registeredPanel.agentEnvelope.action.draft": "Convert to Agent draft", - "capabilityDraft.registeredPanel.agentEnvelope.action.manualEnable": "Enable this turn first", - "capabilityDraft.registeredPanel.agentEnvelope.agentCard.pending": "Agent card: derives after evidence-ready; no parallel persistent entity is created.", - "capabilityDraft.registeredPanel.agentEnvelope.agentCard.ready": "Agent card: workspace-local/{{directory}}, derived from the registered Skill, Managed Job, and completion audit.", - "capabilityDraft.registeredPanel.agentEnvelope.blockedReasonFallback": "runtime binding is currently blocked by a gate", - "capabilityDraft.registeredPanel.agentEnvelope.description": "After a successful run, the Skill, permissions, manual rerun, and evidence can be composed into a Workspace Agent envelope.", - "capabilityDraft.registeredPanel.agentEnvelope.discovery.pending": "Discovery: wait for the workspace-local skill registration path before team discovery.", - "capabilityDraft.registeredPanel.agentEnvelope.discovery.ready": "Discovery: workspace members find {{directory}} through registered skill and reuse the same Managed Job / evidence.", + "capabilityDraft.registeredPanel.action.resumeManagedJob": "Start schedule", + "capabilityDraft.registeredPanel.agentEnvelope.action.blocked": "Fix issue first", + "capabilityDraft.registeredPanel.agentEnvelope.action.draft": "Save as project assistant", + "capabilityDraft.registeredPanel.agentEnvelope.action.manualEnable": "Try once first", + "capabilityDraft.registeredPanel.agentEnvelope.agentCard.pending": "Project assistant: save it after a successful tryout.", + "capabilityDraft.registeredPanel.agentEnvelope.agentCard.ready": "Project assistant: ready to save ({{directory}}).", + "capabilityDraft.registeredPanel.agentEnvelope.blockedReasonFallback": "There are still issues to fix", + "capabilityDraft.registeredPanel.agentEnvelope.description": "After the tryout looks good, save it as an assistant for this project.", + "capabilityDraft.registeredPanel.agentEnvelope.discovery.pending": "Team visibility: available after saving as a project assistant.", + "capabilityDraft.registeredPanel.agentEnvelope.discovery.ready": "Team visibility: project members can use this skill.", "capabilityDraft.registeredPanel.agentEnvelope.evidence.blocking": ", blocked: {{reasons}}", - "capabilityDraft.registeredPanel.agentEnvelope.evidence.completedPack": "Evidence: evidence pack {{packId}} is linked.", - "capabilityDraft.registeredPanel.agentEnvelope.evidence.completedPackFallback": "Evidence: evidence pack is linked.", + "capabilityDraft.registeredPanel.agentEnvelope.evidence.completedPack": "Latest result: passed review ({{packId}}).", + "capabilityDraft.registeredPanel.agentEnvelope.evidence.completedPackFallback": "Latest result: passed review.", "capabilityDraft.registeredPanel.agentEnvelope.evidence.completionAudit": "Evidence: completion audit {{decision}}, owner {{successfulOwnerRunCount}}/{{ownerRunCount}}, ToolCall {{toolCallCount}}, artifact {{artifactCount}}{{controlledGetLabel}}{{blockingLabel}}{{suffix}}.", "capabilityDraft.registeredPanel.agentEnvelope.evidence.controlledGet.executed": ", Controlled GET {{executed}}/{{artifacts}} executed", "capabilityDraft.registeredPanel.agentEnvelope.evidence.controlledGet.requiredMissing": ", Controlled GET required 0/0 executed", - "capabilityDraft.registeredPanel.agentEnvelope.evidence.missing": "Evidence: no successful-run evidence yet; enable it in this turn first to get one result.", - "capabilityDraft.registeredPanel.agentEnvelope.evidence.pendingPack": "Evidence: evidence pack {{packId}} is linked, but a completed completion audit is still missing, so it cannot be solidified as an Agent.", - "capabilityDraft.registeredPanel.agentEnvelope.evidence.sourceMetadataOnly": "Evidence: P3E source metadata exists and can trace this session authorization source.", + "capabilityDraft.registeredPanel.agentEnvelope.evidence.missing": "Latest result: no tryout result yet.", + "capabilityDraft.registeredPanel.agentEnvelope.evidence.pendingPack": "Latest result: generated; review it before saving as a project assistant.", + "capabilityDraft.registeredPanel.agentEnvelope.evidence.sourceMetadataOnly": "Latest result: tryout source recorded, waiting for review.", "capabilityDraft.registeredPanel.agentEnvelope.evidence.suffix.missingControlledGet": "; missing Controlled GET evidence, cannot solidify as Agent", "capabilityDraft.registeredPanel.agentEnvelope.evidence.suffix.notCompleted": "; not completed, cannot solidify as Agent", - "capabilityDraft.registeredPanel.agentEnvelope.memory.pending": "Memory: wait for the first run before recording preferences, methodology, and corrections.", - "capabilityDraft.registeredPanel.agentEnvelope.memory.withReport": "Memory: references verification report {{reportId}} and future run corrections.", - "capabilityDraft.registeredPanel.agentEnvelope.permission.empty": "Permissions: manual confirmation by default; no extra external write permission declared.", + "capabilityDraft.registeredPanel.agentEnvelope.memory.pending": "Record: waiting for the first tryout to capture preferences and corrections.", + "capabilityDraft.registeredPanel.agentEnvelope.memory.withReport": "Record: verification result is saved and future feedback will accumulate.", + "capabilityDraft.registeredPanel.agentEnvelope.permission.empty": "Permissions: manual confirmation by default.", "capabilityDraft.registeredPanel.agentEnvelope.permission.withSummary": "Permissions: {{summary}}.", - "capabilityDraft.registeredPanel.agentEnvelope.runbook": "Runbook: {{name}}", - "capabilityDraft.registeredPanel.agentEnvelope.schedule": "Schedule: first slice only supports a manual rerun draft; no long-running task is created.", - "capabilityDraft.registeredPanel.agentEnvelope.sharing.pending": "Sharing: shown only to the current operator until the audit is complete.", - "capabilityDraft.registeredPanel.agentEnvelope.sharing.ready": "Sharing: shareable inside the current workspace / team; not published to the public Marketplace.", - "capabilityDraft.registeredPanel.agentEnvelope.status.blocked": "Agent draft blocked", - "capabilityDraft.registeredPanel.agentEnvelope.status.evidenceReady": "Evidence ready", - "capabilityDraft.registeredPanel.agentEnvelope.status.manualEnableRequired": "Waiting for successful run", - "capabilityDraft.registeredPanel.agentEnvelope.status.sourceMetadataReady": "Waiting for Completion Audit", - "capabilityDraft.registeredPanel.agentEnvelope.title": "Agent envelope draft", - "capabilityDraft.registeredPanel.agentEnvelope.widget.pending": "Widget: waits for run status, artifacts, blockers, and evidence entry.", - "capabilityDraft.registeredPanel.agentEnvelope.widget.ready": "Widget: shows Managed Job status, recent artifacts, audit decision, and next actions.", + "capabilityDraft.registeredPanel.agentEnvelope.runbook": "Usage: {{name}}", + "capabilityDraft.registeredPanel.agentEnvelope.schedule": "Schedule: not set.", + "capabilityDraft.registeredPanel.agentEnvelope.sharing.pending": "Sharing: visible only to you for now.", + "capabilityDraft.registeredPanel.agentEnvelope.sharing.ready": "Sharing: shareable in the current project.", + "capabilityDraft.registeredPanel.agentEnvelope.status.blocked": "Needs attention", + "capabilityDraft.registeredPanel.agentEnvelope.status.evidenceReady": "Ready to save", + "capabilityDraft.registeredPanel.agentEnvelope.status.manualEnableRequired": "Not tried yet", + "capabilityDraft.registeredPanel.agentEnvelope.status.sourceMetadataReady": "Needs review", + "capabilityDraft.registeredPanel.agentEnvelope.title": "Project assistant", + "capabilityDraft.registeredPanel.agentEnvelope.details": "Project assistant details", + "capabilityDraft.registeredPanel.agentEnvelope.widget.pending": "Result: shown after tryout with status and next steps.", + "capabilityDraft.registeredPanel.agentEnvelope.widget.ready": "Result: shows latest status, artifacts, and next steps.", "capabilityDraft.registeredPanel.approval.consumptionGate.title": "Consumption gate", "capabilityDraft.registeredPanel.approval.credentialResolver.title": "Session credential resolver", "capabilityDraft.registeredPanel.approval.description": "Before a real API execution, this approval request artifact must be consumed first; currently only the audit entry is persisted, without saving tokens or sending requests.", @@ -465,10 +685,11 @@ "capabilityDraft.registeredPanel.approval.suffix.required": ":required", "capabilityDraft.registeredPanel.approval.suffix.secret": ":secret", "capabilityDraft.registeredPanel.approval.title": "Session approval request artifact", - "capabilityDraft.registeredPanel.badge": "Registered", - "capabilityDraft.registeredPanel.card.badge.registered": "Registered", - "capabilityDraft.registeredPanel.card.binding.blocked": "Binding blocked", - "capabilityDraft.registeredPanel.card.binding.candidate": "P3C binding candidate", + "capabilityDraft.registeredPanel.technicalDetails": "Technical details", + "capabilityDraft.registeredPanel.badge": "Saved", + "capabilityDraft.registeredPanel.card.badge.registered": "Saved", + "capabilityDraft.registeredPanel.card.binding.blocked": "Needs attention", + "capabilityDraft.registeredPanel.card.binding.candidate": "Ready to try", "capabilityDraft.registeredPanel.card.descriptionFallback": "Registered as a local Skill package for the current Workspace.", "capabilityDraft.registeredPanel.card.field.directory": "Directory: ", "capabilityDraft.registeredPanel.card.field.nextGate": "Next gate: ", @@ -478,11 +699,11 @@ "capabilityDraft.registeredPanel.card.field.source": "Source: ", "capabilityDraft.registeredPanel.card.field.standard": "Standard: ", "capabilityDraft.registeredPanel.card.nextGateFallback": "manual_runtime_enable / Query Loop metadata / tool_runtime authorization trimming", - "capabilityDraft.registeredPanel.description": "Only Skill packages that passed verification and were written into the current project appear here; runtime still waits for the runtime gate.", - "capabilityDraft.registeredPanel.empty.error": "Registered skills could not be read: {{message}}", - "capabilityDraft.registeredPanel.empty.loading": "Reading registered skills...", - "capabilityDraft.registeredPanel.empty.missingProject": "Select or enter a project before viewing generated skills registered for it.", - "capabilityDraft.registeredPanel.empty.noSkills": "This project has no P3A-registered capability yet. After a draft passes verification and registration, it appears here first.", + "capabilityDraft.registeredPanel.description": "Skills that have been checked and saved to this project appear here. You can try them first or set a schedule.", + "capabilityDraft.registeredPanel.empty.error": "Saved skills could not be loaded. Try again later.", + "capabilityDraft.registeredPanel.empty.loading": "Loading saved skills...", + "capabilityDraft.registeredPanel.empty.missingProject": "Select or enter a project to view skills saved in it.", + "capabilityDraft.registeredPanel.empty.noSkills": "This project has no saved skills yet. Check and save one from “In progress” first.", "capabilityDraft.registeredPanel.error.missingAutomationSession": "The latest automation run has no linked session, so evidence cannot be exported.", "capabilityDraft.registeredPanel.evidence.credentialReferenceId": "Credential reference", "capabilityDraft.registeredPanel.evidence.endpointSource": "Endpoint", @@ -516,6 +737,29 @@ "capabilityDraft.registeredPanel.managedJob.status.notCreated": "Managed Job: not created", "capabilityDraft.registeredPanel.managedJob.status.notRun": "Not run yet", "capabilityDraft.registeredPanel.managedJob.status.withValue": "Managed Job: {{state}} · {{lastStatus}}", + "capabilityDraft.registeredPanel.user.permission.defaultReadonly": "Reads information only by default", + "capabilityDraft.registeredPanel.user.permission.draftWrite": "Can write drafts", + "capabilityDraft.registeredPanel.user.permission.localCommand": "Can run local commands", + "capabilityDraft.registeredPanel.user.permission.readonly": "Reads information only", + "capabilityDraft.registeredPanel.user.schedule.enabled": "On", + "capabilityDraft.registeredPanel.user.schedule.notCreated": "Not set", + "capabilityDraft.registeredPanel.user.schedule.paused": "Paused", + "capabilityDraft.registeredPanel.user.result.auditBlocked": "Latest result needs attention", + "capabilityDraft.registeredPanel.user.result.auditPassed": "Latest result passed review", + "capabilityDraft.registeredPanel.user.result.noJob": "No schedule yet", + "capabilityDraft.registeredPanel.user.result.noRun": "No run yet", + "capabilityDraft.registeredPanel.user.result.readyToCheck": "Latest result is ready to review", + "capabilityDraft.registeredPanel.user.result.running": "Running", + "capabilityDraft.registeredPanel.user.step.saved": "Saved", + "capabilityDraft.registeredPanel.user.step.savedValue": "Available in this project", + "capabilityDraft.registeredPanel.user.step.permission": "Permissions", + "capabilityDraft.registeredPanel.user.step.schedule": "Schedule", + "capabilityDraft.registeredPanel.user.nextStep": "Try it once first. If the result looks good, set a schedule or save it as a project assistant.", + "capabilityDraft.registeredPanel.user.assistant.try": "Try", + "capabilityDraft.registeredPanel.user.assistant.tryReady": "Ready to try once", + "capabilityDraft.registeredPanel.user.assistant.tryBlocked": "Not available yet", + "capabilityDraft.registeredPanel.user.assistant.schedule": "Schedule", + "capabilityDraft.registeredPanel.user.assistant.result": "Latest result", "capabilityDraft.registeredPanel.provenance.title": "Registration provenance", "capabilityDraft.registeredPanel.summary.bindingBlockedFallback": "Runtime binding is currently blocked by a gate.", "capabilityDraft.registeredPanel.summary.bindingCandidateFallback": "It is eligible as a later runtime binding candidate, but is still not in the default tool surface.", @@ -525,7 +769,7 @@ "capabilityDraft.registeredPanel.summary.standardIssueCount": "Standard check still has {{count}} issue(s)", "capabilityDraft.registeredPanel.summary.standardPassed": "Agent Skills standard passed", "capabilityDraft.registeredPanel.summary.standardPending": "Agent Skills standard status pending", - "capabilityDraft.registeredPanel.title": "Workspace registered capabilities", + "capabilityDraft.registeredPanel.title": "Saved skills", "curatedTask.badge.action.clearAria": "Clear {{title}}", "curatedTask.badge.action.clearTitle": "Clear the current result template", "curatedTask.badge.action.edit": "Edit", @@ -1233,13 +1477,46 @@ "skills.workspace.group.emptyTitle": "No matching Skills in this group", "skills.workspace.group.selectedSubtitle": "Choose a Skill in this group to continue", "skills.workspace.header.refresh": "Refresh", - "skills.workspace.header.subtitle": "Pick a Skill to start creating", + "skills.workspace.header.subtitle": "Install and manage skills, then bring one back to the home input when needed.", "skills.workspace.header.title": "Skills", + "skills.workspace.header.createWithLime": "Create with Lime", + "skills.workspace.header.installSkill": "Install Skill", + "skills.workspace.autoLoad.disabledToast": "“{{name}}” will not auto load", + "skills.workspace.autoLoad.enabledToast": "“{{name}}” can auto load now", + "skills.workspace.autoLoad.aria": "Allow {{name}} to auto load", + "skills.workspace.autoLoad.description": "When on, Lime can bring this skill into relevant tasks automatically. When off, it will not be matched automatically.", + "skills.workspace.autoLoad.off": "Off", + "skills.workspace.autoLoad.on": "On", + "skills.workspace.autoLoad.label": "Auto load", + "skills.workspace.store.officialBadge": "Featured", + "skills.workspace.scaffold.createFailed": "Failed to create skill: {{message}}", + "skills.workspace.import.failed": "Failed to install skill: {{message}}", + "skills.workspace.import.success": "Skill installed and added to User Installed.", + "skills.workspace.import.dialogTitle": "Choose a skill folder that contains SKILL.md", + "skills.workspace.header.installingSkill": "Installing", + "skills.workspace.hero.title": "Handpicked workplace Skills", + "skills.workspace.hero.description": "Writing, productivity, design, data analysis, and more. Install in one click.", + "skills.workspace.view.store": "Skill Store", + "skills.workspace.view.builtin": "Built-in", + "skills.workspace.view.installed": "Installed", + "skills.workspace.filter.all": "All", + "skills.workspace.sort.hot": "Sort: Popular", + "skills.workspace.store.other": "More Skills · {{count}}", + "skills.workspace.builtin.title": "Built-in Skills", + "skills.workspace.builtin.subtitle": "Skills included with Lime. The switch only controls automatic matching; Lime keeps working as usual.", + "skills.workspace.builtin.empty": "Try refreshing, or search with another keyword.", + "skills.workspace.installed.title": "Installed", + "skills.workspace.installed.subtitle": "Skills you installed. Auto load, Use, and Uninstall are separate actions.", "skills.workspace.header.viewAll": "View All", - "skills.workspace.installedSkill.defaultPromise": "Use this Skill when you need to reuse it.", + "skills.workspace.installedSkill.action.use": "Use", + "skills.workspace.installedSkill.action.uninstall": "Uninstall", + "skills.workspace.installedSkill.action.uninstalling": "Uninstalling", + "skills.workspace.installedSkill.uninstallSuccess": "Uninstalled “{{name}}”", + "skills.workspace.installedSkill.uninstallFailed": "Failed to uninstall: {{message}}", + "skills.workspace.installedSkill.defaultPromise": "Use this when you want to reuse the skill.", "skills.workspace.installedSkill.entryBanner": "Returned to Generate with Skill “{{name}}”; keep this round moving from there.", "skills.workspace.installedSkill.entryBannerWithReplay": "Returned to Generate with Skill “{{name}}” and the last goal; keep this round moving from there.", - "skills.workspace.installedSkill.fallbackOutputHint": "Continue in Generate with this Skill", + "skills.workspace.installedSkill.fallbackOutputHint": "Bring this skill back to the home input", "skills.workspace.installedSkill.fallbackRequiredInputs": "Continue with the goal and constraints in chat", "skills.workspace.installedSkill.outputPrefix": "Delivers: ", "skills.workspace.installedSkill.requiredPrefix": "Needs: ", @@ -1703,5 +1980,7 @@ "agentChat.threadReliability.view.recommendation.interrupting": "Stopping the current run; wait for runtime to refresh the final status", "agentChat.threadReliability.view.recommendation.interrupted": "Runtime has confirmed this run was interrupted", "agentChat.threadReliability.view.recommendation.nextQueuedTurn": "Continue queued turn: {{title}}", - "agentChat.threadReliability.view.recommendation.retryable": "The latest result supports retry; resume or start a new turn" + "agentChat.threadReliability.view.recommendation.retryable": "The latest result supports retry; resume or start a new turn", + "skills.inputBadge.remove": "Remove @{{name}}", + "skills.workspace.view.tabsLabel": "Skill pages" } diff --git a/src/i18n/resources/ja-JP/agent.json b/src/i18n/resources/ja-JP/agent.json index d7313b8b9..90aa4e9dd 100644 --- a/src/i18n/resources/ja-JP/agent.json +++ b/src/i18n/resources/ja-JP/agent.json @@ -146,58 +146,18 @@ "agentChat.harnessVerification.section.failureFocusTitle": "検証失敗の焦点", "agentChat.harnessVerification.section.recoveredTitle": "復旧済みの結果", "agentChat.harnessVerification.section.title": "検証結果", - "agentChat.imageWorkbenchPreview.description.cancelled": "タスクはキャンセルされ、新しい画像結果は生成されません。", - "agentChat.imageWorkbenchPreview.description.complete.multiple": "{{count}} 件の{{result}}が返りました。右側で確認して使用できます。", - "agentChat.imageWorkbenchPreview.description.complete.single": "{{result}}が完了しました。右側で確認して使用できます。", - "agentChat.imageWorkbenchPreview.description.complete.storyboard": "{{label}}が完了しました。右側で確認して使用できます。", - "agentChat.imageWorkbenchPreview.description.failed.notRetryable": "再試行前に設定またはパラメータを調整してください。", - "agentChat.imageWorkbenchPreview.description.failed.retryable": "利用できる結果を取得できませんでした。プロンプトを調整して再試行してください。", - "agentChat.imageWorkbenchPreview.description.partial.multiple": "{{returned}} / {{expected}} 件の{{result}}が返りました。残りの結果は未完了です。", - "agentChat.imageWorkbenchPreview.description.partial.single": "{{result}}の一部が同期されました。右側で確認できます。", - "agentChat.imageWorkbenchPreview.description.partial.storyboard": "{{label}}は {{returned}} / {{expected}} フレームを返しました。右側で確認できます。", - "agentChat.imageWorkbenchPreview.description.running.edit": "画像編集を処理中です。完了後に実際の結果へ自動的に置き換わります。", - "agentChat.imageWorkbenchPreview.description.running.generate": "画像を生成中です。完了後に実際の結果へ自動的に置き換わります。", - "agentChat.imageWorkbenchPreview.description.running.storyboard": "{{label}}を作成しました。{{expected}} フレームが同じタスクカードに順次反映されます。", - "agentChat.imageWorkbenchPreview.description.running.variation": "バリエーションを作成中です。完了後に実際の結果へ自動的に置き換わります。", - "agentChat.imageWorkbenchPreview.layout.storyboard": "3x3 ストーリーボード", - "agentChat.imageWorkbenchPreview.media.alt": "画像タスク結果", + "agentChat.imageWorkbenchPreview.media.alt": "画像結果", + "agentChat.imageWorkbenchPreview.media.open": "画像結果を開く", "agentChat.imageWorkbenchPreview.media.previewFailed": "プレビュー失敗", - "agentChat.imageWorkbenchPreview.meta.imageCount": "{{count}} 枚", - "agentChat.imageWorkbenchPreview.meta.imageProgress": "{{current}}/{{expected}} 枚", - "agentChat.imageWorkbenchPreview.placeholder.cancelled": "タスクはキャンセル済み", - "agentChat.imageWorkbenchPreview.placeholder.failed": "成功した結果はまだありません", + "agentChat.imageWorkbenchPreview.tool.generation": "Image Generation", + "agentChat.imageWorkbenchPreview.tool.editing": "Image Editing", + "agentChat.imageWorkbenchPreview.tool.redraw": "Image Redraw", + "agentChat.imageWorkbenchPreview.placeholder.cancelled": "キャンセル済み", + "agentChat.imageWorkbenchPreview.placeholder.editing": "画像を編集中", + "agentChat.imageWorkbenchPreview.placeholder.failed": "生成失敗", + "agentChat.imageWorkbenchPreview.placeholder.generating": "画像を生成中", "agentChat.imageWorkbenchPreview.placeholder.imageUnavailable": "画像を一時的に表示できません", - "agentChat.imageWorkbenchPreview.placeholder.synced": "結果は同期済み", - "agentChat.imageWorkbenchPreview.promptFallback": "このタスクにはプロンプトがありません。", - "agentChat.imageWorkbenchPreview.result.edit": "編集画像結果", - "agentChat.imageWorkbenchPreview.result.generate": "画像結果", - "agentChat.imageWorkbenchPreview.result.variation": "バリエーション結果", - "agentChat.imageWorkbenchPreview.slot.cancelled": "キャンセル済み", - "agentChat.imageWorkbenchPreview.slot.failed": "この枠は失敗", - "agentChat.imageWorkbenchPreview.slot.pending": "生成待ち", - "agentChat.imageWorkbenchPreview.slot.waiting": "補完待ち", - "agentChat.imageWorkbenchPreview.source.editSummary": "このタスクは既存の画像結果をもとに編集を続行します。", - "agentChat.imageWorkbenchPreview.source.footnote": "{{label}}:{{summary}}", - "agentChat.imageWorkbenchPreview.source.ref": "{{ref}} を参照済み", - "agentChat.imageWorkbenchPreview.source.referenceCount": "参考画像 {{count}} 枚を添付済み。", - "agentChat.imageWorkbenchPreview.source.referenceLabel": "参考画像", - "agentChat.imageWorkbenchPreview.source.sourceCount": "元画像 {{count}} 枚を参照済み。", - "agentChat.imageWorkbenchPreview.source.sourceLabel": "元画像", - "agentChat.imageWorkbenchPreview.source.variationSummary": "このタスクは参考画像をもとに新しいバリエーションを生成します。", - "agentChat.imageWorkbenchPreview.status.cancelled": "キャンセル済み", - "agentChat.imageWorkbenchPreview.status.complete.edit": "編集済み", - "agentChat.imageWorkbenchPreview.status.complete.generate": "生成済み", - "agentChat.imageWorkbenchPreview.status.complete.variation": "バリエーション済み", - "agentChat.imageWorkbenchPreview.status.failed.edit": "編集失敗", - "agentChat.imageWorkbenchPreview.status.failed.generate": "生成失敗", - "agentChat.imageWorkbenchPreview.status.failed.variation": "バリエーション失敗", - "agentChat.imageWorkbenchPreview.status.partial": "一部完了", - "agentChat.imageWorkbenchPreview.status.running.edit": "編集中", - "agentChat.imageWorkbenchPreview.status.running.generate": "生成中", - "agentChat.imageWorkbenchPreview.status.running.preparing": "準備中", - "agentChat.imageWorkbenchPreview.status.running.queued": "キュー待ち", - "agentChat.imageWorkbenchPreview.status.running.variation": "バリエーション作成中", - "agentChat.imageWorkbenchPreview.statusDescription": " · {{description}}", + "agentChat.imageWorkbenchPreview.placeholder.redrawing": "画像を再生成中", "agentChat.incidentPanel.empty": "アクティブな incident は検出されていません", "agentChat.incidentPanel.priorityBadge": "{{severity}} 優先度", "agentChat.messageList.firstTokenStatus.cancelled.detail": "このリクエストは停止しました。", @@ -297,11 +257,270 @@ "agentChat.teamWorkspace.canvasToolbar.zoom": "ズーム {{percent}}%", "agentChat.teamWorkspace.canvasToolbar.zoomIn": "拡大", "agentChat.teamWorkspace.canvasToolbar.zoomOut": "縮小", + "agentChat.teamWorkspace.canvasStage.badge": "現在の進捗", + "agentChat.teamWorkspace.canvasStage.shortcutHint": "ドラッグでタスク配置を調整 · Space でパン · A で整理 · F でフォーカス", + "agentChat.teamWorkspace.canvasStage.emptyTitle": "現在の進捗はありません", + "agentChat.teamWorkspace.dock.surfaceTitle": "生成", + "agentChat.teamWorkspace.dock.currentProgress": "現在の進捗", + "agentChat.teamWorkspace.dock.processingBudget": "{{activeCount}}/{{totalCount}} 処理中", + "agentChat.teamWorkspace.dock.processingCount": "{{formattedCount}} 件処理中", + "agentChat.teamWorkspace.dock.waitingCount": "{{formattedCount}} 件待機中", + "agentChat.teamWorkspace.dock.taskCount": "{{formattedCount}} 件のタスク", + "agentChat.teamWorkspace.dock.selectedPlan": "現在の分担案:{{label}}", + "agentChat.teamWorkspace.dock.unobstructiveBadge": "キャンバスを遮りません", + "agentChat.teamWorkspace.dock.aria.open": "現在の進捗を開く", + "agentChat.teamWorkspace.dock.aria.collapse": "現在の進捗を折りたたむ", + "agentChat.teamWorkspace.dock.aria.expand": "現在の進捗を展開", "agentChat.teamWorkspace.teamActions.closeCompleted": "完了タスクを折りたたむ", "agentChat.teamWorkspace.teamActions.closing": "折りたたみ中...", "agentChat.teamWorkspace.teamActions.waitAny": "いずれかのタスク結果を待つ", "agentChat.teamWorkspace.teamActions.waiting": "待機中...", + "agentChat.teamWorkspace.board.detail.collapse": "詳細を閉じる", + "agentChat.teamWorkspace.board.detail.expand": "詳細を表示", + "agentChat.teamWorkspace.board.canvas.title": "現在の進捗", + "agentChat.teamWorkspace.board.canvas.subtitle.childRuntime": "並行タスクは各パネルで進捗と結果を更新し、メイン会話には必要な要約だけを残します。", + "agentChat.teamWorkspace.board.canvas.subtitle.runtime": "現在の進捗 {{formattedCount}} 件が接続済みです。フォーカスは処理中の分担を優先します。", + "agentChat.teamWorkspace.board.canvas.subtitle.formation.forming": "現在のタスク分担を準備中です。タスク作成後、ここで独立して進捗を更新します。", + "agentChat.teamWorkspace.board.canvas.subtitle.formation.formed": "現在のタスク分担は準備完了です。タスク作成後、各進捗パネルで処理を開始します。", + "agentChat.teamWorkspace.board.canvas.subtitle.formation.failed": "今回のタスク分担は準備に失敗したため、まだ現在の進捗パネルを作成できません。", + "agentChat.teamWorkspace.board.canvas.subtitle.formation.default": "タスク作成後、ここが独立した現在の進捗として展開されます。", + "agentChat.teamWorkspace.boardChrome.headline.parentOverviewFallback": "メインアシスタント概要", + "agentChat.teamWorkspace.boardChrome.headline.empty": "必要に応じてタスクを自動分割します", + "agentChat.teamWorkspace.boardChrome.headline.runningWithQueued": "タスク進行中 · {{runningCount}} 件処理中 / {{queuedCount}} 件待機中", + "agentChat.teamWorkspace.boardChrome.headline.runningCount": "タスク進行中 · {{formattedCount}} 件処理中", + "agentChat.teamWorkspace.boardChrome.headline.running": "タスク進行中", + "agentChat.teamWorkspace.boardChrome.headline.queuedCount": "タスク準備中 · {{formattedCount}} 件待機中", + "agentChat.teamWorkspace.boardChrome.headline.queued": "タスク準備中", + "agentChat.teamWorkspace.boardChrome.headline.completedCount": "{{formattedCount}} 件のタスクが完了", + "agentChat.teamWorkspace.boardChrome.headline.completed": "タスク完了", + "agentChat.teamWorkspace.boardChrome.headline.retryCount": "{{formattedCount}} 件のタスクで再試行が必要", + "agentChat.teamWorkspace.boardChrome.headline.retry": "タスクの再試行が必要", + "agentChat.teamWorkspace.boardChrome.headline.connectedCount": "{{formattedCount}} 件の現在の進捗が接続済み", + "agentChat.teamWorkspace.boardChrome.headline.surfaceTitle": "生成", + "agentChat.teamWorkspace.boardChrome.hint.childWithSiblings": "現在のタスクは {{formattedCount}} 件の並行サブタスクと一緒に進行中です", + "agentChat.teamWorkspace.boardChrome.hint.childSingle": "このサブタスクを現在処理中です。結果はメインアシスタントへ戻ります。", + "agentChat.teamWorkspace.boardChrome.hint.empty": "必要に応じて作業を分割し、処理順を決め、結果を現在のタスクへ戻します。", + "agentChat.teamWorkspace.boardChrome.hint.runtime": "ここでは処理中の分担、状態、直近の更新先を確認できます。", + "agentChat.teamWorkspace.boardChrome.chip.focus": "現在の焦点 {{name}}", + "agentChat.teamWorkspace.boardChrome.chip.updatedAt": "{{updatedAt}} に更新", + "agentChat.teamWorkspace.boardChrome.chip.waitable": "{{formattedCount}} 件処理中", + "agentChat.teamWorkspace.boardChrome.chip.completed": "{{formattedCount}} 件完了", + "agentChat.teamWorkspace.boardChrome.statusSummary": "{{status}} {{formattedCount}}", + "agentChat.teamWorkspace.overview.selectedSession.waitingTitle": "タスクの引き継ぎ待ち", + "agentChat.teamWorkspace.overview.selectedSession.focusBadge": "現在のフォーカス", + "agentChat.teamWorkspace.overview.selectedSession.waitingBadge": "引き継ぎ待ち", + "agentChat.teamWorkspace.overview.compact.taskActionsLabel": "タスク操作", + "agentChat.teamWorkspace.overview.compact.viewActionsLabel": "表示", + "agentChat.teamWorkspace.overview.taskCount.waitable": "処理中のタスク {{formattedCount}} 件", + "agentChat.teamWorkspace.overview.taskCount.completed": "完了したタスク {{formattedCount}} 件", + "agentChat.teamWorkspace.formation.hint.forming": "システムが現在のタスク分担を準備しています。タスクが作成されると自動的に処理が始まります。", + "agentChat.teamWorkspace.formation.hint.formed": "現在のタスク分担は準備できています。タスクが作成されると引き続き処理を引き継ぎます。", + "agentChat.teamWorkspace.formation.hint.failed": "現在のタスク分担の準備に失敗しましたが、このタスク内で引き続き進められます。", + "agentChat.teamWorkspace.formation.hint.default": "必要になるとここが現在の進捗パネルとして自動展開されます。", + "agentChat.teamWorkspace.formation.emptyDetail.forming": "システムが現在のタスクに基づいて分担を準備しています。完了後、まず分担カードを表示し、その後実際の処理進捗を接続します。", + "agentChat.teamWorkspace.formation.detail.hint.runtimeWithReference": "現在のタスク分担と参考プランを表示", + "agentChat.teamWorkspace.formation.detail.hint.planOnly": "現在の分担プランを表示", + "agentChat.teamWorkspace.formation.detail.summary.runtime": "現在の概要", + "agentChat.teamWorkspace.formation.detail.summary.plan": "プラン概要", + "agentChat.teamWorkspace.formation.detail.referenceSummaryDefault": "現在のタスク分担は、選択中プランの役割設定を参照しています。", + "agentChat.teamWorkspace.formation.detail.roleSection.runtime": "現在のタスク分担", + "agentChat.teamWorkspace.formation.detail.roleSection.plan": "役割分担", + "agentChat.teamWorkspace.formation.emptyDetail.formed": "現在の分担プランは準備できています。キャンバスはまず分担を表示し、タスクが実際に始まると現在の進捗に切り替わります。", + "agentChat.teamWorkspace.formation.emptyDetail.failed": "現在のタスク分担の準備に失敗したため、現在の進捗を一時的に表示できません。", + "agentChat.teamWorkspace.formation.emptyDetail.default": "まだタスクは引き継がれていません。システムが分担を開始すると、詳細エリアは進捗サマリーに切り替わります。", + "agentChat.teamWorkspace.formation.notice.forming": "システムが現在のタスク分担を準備しています。完了後、まず分担カードを表示し、その後独立した現在の進捗に切り替わります。", + "agentChat.teamWorkspace.formation.notice.formed": "現在の分担プランは準備できています。タスクが作成されると、このビューはプラン表示から現在の進捗へ移行します。", + "agentChat.teamWorkspace.formation.notice.failed": "現在のタスク分担の準備に失敗したため、まだタスクは引き継がれていません。", + "agentChat.teamWorkspace.formation.notice.default": "まだタスクは引き継がれていません。システムが分担を開始すると、ここに独立した現在の進捗が生成されます。", + "agentChat.teamWorkspace.formation.summaryBadge.planLabel": "分担プラン · {{label}}", + "agentChat.teamWorkspace.formation.summaryBadge.planRoleCount": "予定分担 {{formattedCount}} 件", + "agentChat.teamWorkspace.formation.summaryBadge.memberCount": "現在の進捗 {{formattedCount}} 件", + "agentChat.teamWorkspace.formation.summaryBadge.referenceLabel": "参考プラン · {{label}}", + "agentChat.teamWorkspace.formation.summaryBadge.taskCount": "タスク {{formattedCount}} 件", + "agentChat.teamWorkspace.formation.panel.title": "現在のタスク分担", + "agentChat.teamWorkspace.formation.panel.description.failed": "現在のタスク分担の準備に失敗したため、まだ詳細を表示できません。", + "agentChat.teamWorkspace.formation.panel.description.default": "ここにはまず現在の分担プランを表示し、タスクが作成された後に現在の進捗へ切り替わります。", + "agentChat.teamWorkspace.formation.waitingHeadline": "タスクの引き継ぎ待ち", + "agentChat.teamWorkspace.formation.status.forming.label": "準備中", + "agentChat.teamWorkspace.formation.status.forming.title": "タスク分担を準備中", + "agentChat.teamWorkspace.formation.status.formed.label": "準備完了", + "agentChat.teamWorkspace.formation.status.formed.title": "タスク分担の準備完了", + "agentChat.teamWorkspace.formation.status.failed.label": "準備失敗", + "agentChat.teamWorkspace.formation.status.failed.title": "タスク分担の準備失敗", + "agentChat.teamWorkspace.formation.memberStatus.planned": "割り当て待ち", + "agentChat.teamWorkspace.formation.memberStatus.spawning": "接続中", + "agentChat.teamWorkspace.formation.memberStatus.running": "処理中", + "agentChat.teamWorkspace.formation.memberStatus.waiting": "続行待ち", + "agentChat.teamWorkspace.formation.memberStatus.completed": "完了", + "agentChat.teamWorkspace.formation.memberStatus.failed": "再試行が必要", + "agentChat.teamWorkspace.selectedSession.header.eyebrow": "現在の表示", + "agentChat.teamWorkspace.selectedSession.header.currentTaskBadge": "現在のタスク", + "agentChat.teamWorkspace.selectedSession.header.action.resumePending": "再開中...", + "agentChat.teamWorkspace.selectedSession.header.action.resume": "処理を再開", + "agentChat.teamWorkspace.selectedSession.header.action.closePending": "一時停止中...", + "agentChat.teamWorkspace.selectedSession.header.action.close": "処理を一時停止", + "agentChat.teamWorkspace.selectedSession.header.action.openChild": "進捗に切り替え", + "agentChat.teamWorkspace.selectedSession.header.action.open": "進捗を開く", + "agentChat.teamWorkspace.selectedSession.settings.heading": "タスク分担", + "agentChat.teamWorkspace.teamPreset.codeTriageTeam": "コードトリアージチーム", + "agentChat.teamWorkspace.teamPreset.researchTeam": "リサーチチーム", + "agentChat.teamWorkspace.teamPreset.contentCreationTeam": "コンテンツ制作チーム", + "agentChat.teamWorkspace.selectedSession.detail.setting.preset": "プリセット {{label}}", + "agentChat.teamWorkspace.selectedSession.detail.setting.profile": "スタイル {{name}}", + "agentChat.teamWorkspace.selectedSession.detail.setting.role": "役割 {{label}}", + "agentChat.teamWorkspace.selectedSession.detail.setting.theme": "テーマ {{theme}}", + "agentChat.teamWorkspace.selectedSession.detail.metadata.role": "役割 {{label}}", + "agentChat.teamWorkspace.selectedSession.detail.metadata.provider": "サービス {{name}}", + "agentChat.teamWorkspace.selectedSession.detail.metadata.model": "モデル {{model}}", + "agentChat.teamWorkspace.selectedSession.detail.metadata.originTool": "ソース {{tool}}", + "agentChat.teamWorkspace.selectedSession.detail.metadata.createdFromTurn": "以前のタスク {{turnId}} から", + "agentChat.teamWorkspace.selectedSession.detail.metadata.parentSession": "{{name}} から", + "agentChat.teamWorkspace.selectedSession.detail.sessionType.subAgent": "サブタスク", + "agentChat.teamWorkspace.selectedSession.detail.sessionType.fork": "補足ブランチ", + "agentChat.teamWorkspace.selectedSession.detail.sessionType.user": "メインアシスタント", + "agentChat.teamWorkspace.selectedSession.detail.roleHint.explorer": "分析", + "agentChat.teamWorkspace.selectedSession.detail.roleHint.executor": "実行", + "agentChat.teamWorkspace.selectedSession.detail.roleHint.reviewer": "レビュー", + "agentChat.teamWorkspace.selectedSession.detail.roleHint.writer": "執筆", + "agentChat.teamWorkspace.selectedSession.detail.roleHint.planner": "計画", + "agentChat.teamWorkspace.selectedSession.detail.roleHint.researcher": "調査", + "agentChat.teamWorkspace.selectedSession.detail.roleHint.designer": "デザイン", + "agentChat.teamWorkspace.selectedSession.detail.roleHint.design": "デザイン", + "agentChat.teamWorkspace.selectedSession.detail.roleHint.editor": "編集", + "agentChat.teamWorkspace.selectedSession.detail.roleHint.orchestrator": "メインアシスタント", + "agentChat.teamWorkspace.selectedSession.footer.updatedAt": "更新 {{updatedAt}}", + "agentChat.teamWorkspace.selectedSession.collaboration.heading": "処理を続行", + "agentChat.teamWorkspace.selectedSession.collaboration.waitReady": "結果を直接確認できます", + "agentChat.teamWorkspace.selectedSession.collaboration.waitPending": "待機中...", + "agentChat.teamWorkspace.selectedSession.collaboration.wait": "結果を30秒待つ", + "agentChat.teamWorkspace.selectedSession.collaboration.waitHint": "現在の内容がこのタスク結果に依存する場合だけ使用します。", + "agentChat.teamWorkspace.selectedSession.collaboration.inputPlaceholder": "このタスクに補足説明や制約を追加するか、次の手順へ進めるよう依頼します。", + "agentChat.teamWorkspace.selectedSession.collaboration.sendPending": "送信中...", + "agentChat.teamWorkspace.selectedSession.collaboration.send": "説明を送信", + "agentChat.teamWorkspace.selectedSession.collaboration.interruptPending": "中断中...", + "agentChat.teamWorkspace.selectedSession.collaboration.interrupt": "今すぐ説明を挿入", + "agentChat.teamWorkspace.selectedSession.collaboration.inputHint": "この説明は現在のタスクにのみ送信され、他の並行タスクには影響しません。", + "agentChat.teamWorkspace.selectedSession.activity.heading": "完全な進捗", + "agentChat.teamWorkspace.selectedSession.activity.autoRefresh": "処理中は自動更新", + "agentChat.teamWorkspace.selectedSession.activity.errorPrefix": "最新の進捗を利用できません: ", + "agentChat.teamWorkspace.selectedSession.activity.errorFallback": "同期に失敗しました", + "agentChat.teamWorkspace.selectedSession.activity.emptyReady": "このタスクにはまだ表示できる新しい進捗がありません。", + "agentChat.teamWorkspace.selectedSession.activity.loading": "このタスクの最新進捗を同期しています...", + "agentChat.teamWorkspace.selectedSession.activity.feedHeading": "進捗ログ", + "agentChat.teamWorkspace.selectedSession.activity.entryCount": "{{formattedCount}} 件", + "agentChat.teamWorkspace.activityPreview.line": "{{title}}:{{detail}}", + "agentChat.teamWorkspace.activityPreview.status.error": "エラー", + "agentChat.teamWorkspace.activityPreview.status.warning": "警告", + "agentChat.teamWorkspace.activityPreview.status.inProgress": "進行中", + "agentChat.teamWorkspace.activityPreview.status.failed": "失敗", + "agentChat.teamWorkspace.activityPreview.status.completed": "完了", + "agentChat.teamWorkspace.activityPreview.status.message": "メッセージ", + "agentChat.teamWorkspace.activityPreview.messageTitle.error": "エラー", + "agentChat.teamWorkspace.activityPreview.messageTitle.output": "出力", + "agentChat.teamWorkspace.activityPreview.messageTitle.reply": "返信", + "agentChat.teamWorkspace.activityPreview.error.syncFailed": "最近の進捗の同期に失敗しました", + "agentChat.teamWorkspace.activityPreview.error.historyReadFailed": "履歴本文の読み込みに失敗しました", + "agentChat.teamWorkspace.liveRuntime.title.statusChanged": "状態切り替え", + "agentChat.teamWorkspace.liveRuntime.detail.statusChanged": "タスク状態イベントを受信し、{{status}} に切り替わりました。", + "agentChat.teamWorkspace.liveRuntime.title.textDraft": "コンテンツ生成中", + "agentChat.teamWorkspace.liveRuntime.title.thinkingDraft": "思考を整理中", + "agentChat.teamWorkspace.liveRuntime.title.currentProgress": "現在の進捗", + "agentChat.teamWorkspace.liveRuntime.title.processing": "処理中", + "agentChat.teamWorkspace.liveRuntime.title.tool": "{{status}} · {{toolName}}", + "agentChat.teamWorkspace.liveRuntime.title.toolTargetFallback": "現在のステップ", + "agentChat.teamWorkspace.liveRuntime.detail.toolCompletedFallback": "現在のステップが完了しました。", + "agentChat.teamWorkspace.liveRuntime.detail.toolProcessing": "{{target}} を処理中です。", + "agentChat.teamWorkspace.liveRuntime.title.warning": "警告", + "agentChat.teamWorkspace.liveRuntime.title.error": "エラー", + "agentChat.teamWorkspace.liveRuntime.status.processing": "処理中", + "agentChat.teamWorkspace.liveRuntime.status.queued": "待機中", + "agentChat.teamWorkspace.liveRuntime.status.completed": "完了", + "agentChat.teamWorkspace.liveRuntime.status.retry": "再試行が必要", + "agentChat.teamWorkspace.liveRuntime.status.warning": "警告", + "agentChat.teamWorkspace.liveRuntime.lifecycle.queueAdded.title": "待機中", + "agentChat.teamWorkspace.liveRuntime.lifecycle.queueAdded.detail": "新しい指示を受信しました。このサブタスクは前の項目が完了した後に続行します。", + "agentChat.teamWorkspace.liveRuntime.lifecycle.queueStarted.title": "処理開始", + "agentChat.teamWorkspace.liveRuntime.lifecycle.queueStarted.detail": "このサブタスクは現在のタスクの処理を開始しました。", + "agentChat.teamWorkspace.liveRuntime.lifecycle.turnStarted.title": "処理継続", + "agentChat.teamWorkspace.liveRuntime.lifecycle.turnStarted.detail": "このサブタスクは現在の内容を進めています。", + "agentChat.teamWorkspace.liveRuntime.lifecycle.turnCompleted.title": "ステップ完了", + "agentChat.teamWorkspace.liveRuntime.lifecycle.turnCompleted.detail": "このステップは完了しました。最新結果を同期しています。", + "agentChat.teamWorkspace.liveRuntime.lifecycle.turnFailed.title": "処理失敗", + "agentChat.teamWorkspace.liveRuntime.lifecycle.turnFailed.detail": "このステップの処理に失敗しました。エラー詳細を確認してください。", + "agentChat.teamWorkspace.operations.heading": "タスク進捗", + "agentChat.teamWorkspace.operations.recentCount": "最近 {{formattedCount}} 件", + "agentChat.teamWorkspace.operations.updatedNow": "たった今", + "agentChat.teamWorkspace.operations.taskFallback": "このタスク", + "agentChat.teamWorkspace.operations.title.waitTimedOut": "待機タイムアウト", + "agentChat.teamWorkspace.operations.title.waitResolved": "結果を受信", + "agentChat.teamWorkspace.operations.title.resume": "処理を再開", + "agentChat.teamWorkspace.operations.title.closeCompleted": "完了項目を折りたたむ", + "agentChat.teamWorkspace.operations.title.close": "処理を一時停止", + "agentChat.teamWorkspace.operations.wait.timedOut": "先ほど結果待ちがタイムアウトしました。まだ {{formattedCount}} 件のタスクが処理中です。", + "agentChat.teamWorkspace.operations.wait.resolved": "先ほど {{sessionName}} から新しい結果を受け取りました。現在の状態は{{status}}です。", + "agentChat.teamWorkspace.operations.control.resumeCount": "先ほど {{formattedCount}} 件のタスク処理を再開しました。", + "agentChat.teamWorkspace.operations.control.resumeOne": "先ほど {{sessionName}} の処理を再開しました。", + "agentChat.teamWorkspace.operations.control.closeCompleted": "先ほど {{formattedCount}} 件の完了済みタスクを折りたたみました。", + "agentChat.teamWorkspace.operations.control.closeCount": "先ほど {{formattedCount}} 件のタスク処理を一時停止しました。", + "agentChat.teamWorkspace.operations.control.closeOne": "先ほど {{sessionName}} の処理を一時停止しました。", + "agentChat.teamWorkspace.canvasLane.updatedNow": "たった今", + "agentChat.teamWorkspace.canvasLane.runtimeDetail.waitingCount": "待機中 {{formattedCount}}", + "agentChat.teamWorkspace.canvasLane.runtimeDetail.recentProgress": "最新の進捗 {{status}}", + "agentChat.teamWorkspace.canvasLane.runtimeDetail.processingCount": "処理中 {{activeCount}}/{{totalCount}}", + "agentChat.teamWorkspace.canvasLane.runtimeDetail.stableProcessing": "安定モード", + "agentChat.teamWorkspace.canvasLane.empty.syncFailed": "最新内容の同期に失敗しました", + "agentChat.teamWorkspace.canvasLane.empty.syncLoading": "このタスクの最新内容を同期しています...", + "agentChat.teamWorkspace.canvasLane.empty.queued": "このタスクは割り当て済みで、まもなく処理を開始します。", + "agentChat.teamWorkspace.canvasLane.empty.running": "このタスクは処理中です。最新の進捗はここに継続して更新されます。", + "agentChat.teamWorkspace.canvasLane.empty.completed": "この部分は完了しました。結果は引き続き現在の内容に反映されます。", + "agentChat.teamWorkspace.canvasLane.empty.failed": "このステップは正常に完了しませんでした。下の詳細を確認して続行するか判断できます。", + "agentChat.teamWorkspace.canvasLane.empty.default": "このタスクにはまだ表示できる出力がありません。", + "agentChat.teamWorkspace.canvasLane.session.summaryEmpty": "タスク概要はまだありません。詳細を開くと完全なコンテキストを確認できます。", + "agentChat.teamWorkspace.canvasLane.memberHint.spawning": "このタスクに接続しています", + "agentChat.teamWorkspace.canvasLane.memberHint.running": "このタスクは処理中です", + "agentChat.teamWorkspace.canvasLane.memberHint.waiting": "追加指示を待っています", + "agentChat.teamWorkspace.canvasLane.memberHint.completed": "このステップは完了しました", + "agentChat.teamWorkspace.canvasLane.memberHint.failed": "このステップは再試行が必要です", + "agentChat.teamWorkspace.canvasLane.memberHint.connected": "実タスクに接続済み", + "agentChat.teamWorkspace.canvasLane.memberHint.waitingAssignment": "タスクの引き継ぎ待ち", + "agentChat.teamWorkspace.canvasLane.planned.badge": "未開始", + "agentChat.teamWorkspace.canvasLane.planned.statusHint": "システムがこのタスクを分割するのを待っています", + "agentChat.teamWorkspace.canvasLane.runtime.updatedAtWaiting": "タスクの引き継ぎ待ち", + "agentChat.teamWorkspace.canvasLane.planned.updatedAt": "計画された分担", + "agentChat.teamWorkspace.runtimeStatus.idle": "未開始", + "agentChat.teamWorkspace.runtimeStatus.queued": "開始待ち", + "agentChat.teamWorkspace.runtimeStatus.running": "処理中", "agentChat.teamWorkspace.runtimeStatus.completed": "完了", + "agentChat.teamWorkspace.runtimeStatus.failed": "再試行が必要", + "agentChat.teamWorkspace.runtimeStatus.aborted": "一時停止済み", + "agentChat.teamWorkspace.runtimeStatus.closed": "停止済み", + "agentChat.teamWorkspace.control.close.cascadeStopped": "{{formattedCount}} 件のタスクを連鎖的に停止しました", + "agentChat.teamWorkspace.control.close.stoppedOne": "このタスクは停止しました", + "agentChat.teamWorkspace.control.close.noChange": "現在のタスク状態は{{status}}で、新しい停止変更はありません", + "agentChat.teamWorkspace.control.close.failedFallback": "タスクの停止に失敗しました", + "agentChat.teamWorkspace.control.resume.cascadeResumed": "{{formattedCount}} 件のタスクを連鎖的に再開しました", + "agentChat.teamWorkspace.control.resume.resumedOne": "このタスクは再開しました", + "agentChat.teamWorkspace.control.resume.noChange": "現在のタスク状態は{{status}}で、新しい再開変更はありません", + "agentChat.teamWorkspace.control.resume.failedFallback": "タスクの再開に失敗しました", + "agentChat.teamWorkspace.control.wait.timeoutOne": "待機がタイムアウトしました。このタスクはまだ最終状態ではありません", + "agentChat.teamWorkspace.control.wait.enteredStatusOne": "このタスクは{{status}}状態になりました", + "agentChat.teamWorkspace.control.wait.noActiveError": "待機できるアクティブなタスクはありません", + "agentChat.teamWorkspace.control.wait.timeoutTeam": "待機がタイムアウトしました。チーム内のアクティブなタスクはまだ最終状態ではありません", + "agentChat.teamWorkspace.control.wait.enteredStatusTeam": "チームタスクは{{status}}状態になりました", + "agentChat.teamWorkspace.control.wait.failedFallback": "タスクの待機に失敗しました", + "agentChat.teamWorkspace.control.closeCompleted.noCompletedError": "閉じられる完了済みタスクはありません", + "agentChat.teamWorkspace.control.closeCompleted.cascadeClosed": "{{formattedCount}} 件のタスクを連鎖的に折りたたみました", + "agentChat.teamWorkspace.control.closeCompleted.closedCount": "{{formattedCount}} 件の完了済みタスクを折りたたみました", + "agentChat.teamWorkspace.control.closeCompleted.partialFailedFallback": "一部の完了済みタスクを折りたためませんでした", + "agentChat.teamWorkspace.control.sendInput.emptyError": "このタスクに送る内容を入力してください", + "agentChat.teamWorkspace.control.sendInput.sentInterrupt": "現在の実行を中断し、新しい説明を送信しました", + "agentChat.teamWorkspace.control.sendInput.sent": "このタスクに補足説明を送信しました", + "agentChat.teamWorkspace.control.sendInput.failedFallback": "タスク説明の送信に失敗しました", + "agentChat.teamWorkspace.control.stop.pausedCount": "{{formattedCount}} 件のタスクを一時停止しました", + "agentChat.teamWorkspace.control.stop.pausedOne": "このタスクを一時停止しました", + "agentChat.teamWorkspace.control.stop.requestedCount": "{{formattedCount}} 件のタスクに一時停止リクエストを送信しました", + "agentChat.teamWorkspace.control.stop.requestedOne": "このタスクに一時停止リクエストを送信しました", + "agentChat.teamWorkspace.control.stop.failedFallback": "タスクの停止に失敗しました", "agentChat.threadReliability.routingEvidence.evidence": "証拠", "agentChat.threadReliability.routingEvidence.fallbackSlot": "runtime routing", "agentChat.threadReliability.routingEvidence.firstText": "最初の本文", @@ -383,47 +602,48 @@ "capabilityDraft.panel.title": "Capability 下書き", "capabilityDraft.registeredPanel.action.auditRecentRun": "直近の実行を監査", "capabilityDraft.registeredPanel.action.auditRunning": "監査中", - "capabilityDraft.registeredPanel.action.createManagedJobDraft": "Managed Job ドラフトを作成", - "capabilityDraft.registeredPanel.action.enableRuntime": "このターンで有効化", + "capabilityDraft.registeredPanel.action.createManagedJobDraft": "定期実行を設定", + "capabilityDraft.registeredPanel.action.enableRuntime": "一度試す", "capabilityDraft.registeredPanel.action.enableRuntimeHelp": "session enable metadata だけを書き込み、自動化は作成しません。", - "capabilityDraft.registeredPanel.action.pauseManagedJob": "Managed Job を一時停止", + "capabilityDraft.registeredPanel.action.pauseManagedJob": "定期実行を一時停止", "capabilityDraft.registeredPanel.action.refresh": "更新", - "capabilityDraft.registeredPanel.action.resumeManagedJob": "Managed Job を再開", - "capabilityDraft.registeredPanel.agentEnvelope.action.blocked": "Resolve blocker first", - "capabilityDraft.registeredPanel.agentEnvelope.action.draft": "Convert to Agent draft", - "capabilityDraft.registeredPanel.agentEnvelope.action.manualEnable": "Enable this turn first", - "capabilityDraft.registeredPanel.agentEnvelope.agentCard.pending": "Agent card: derives after evidence-ready; no parallel persistent entity is created.", - "capabilityDraft.registeredPanel.agentEnvelope.agentCard.ready": "Agent card: workspace-local/{{directory}}, derived from the registered Skill, Managed Job, and completion audit.", - "capabilityDraft.registeredPanel.agentEnvelope.blockedReasonFallback": "runtime binding is currently blocked by a gate", - "capabilityDraft.registeredPanel.agentEnvelope.description": "After a successful run, the Skill, permissions, manual rerun, and evidence can be composed into a Workspace Agent envelope.", - "capabilityDraft.registeredPanel.agentEnvelope.discovery.pending": "Discovery: wait for the workspace-local skill registration path before team discovery.", - "capabilityDraft.registeredPanel.agentEnvelope.discovery.ready": "Discovery: workspace members find {{directory}} through registered skill and reuse the same Managed Job / evidence.", + "capabilityDraft.registeredPanel.action.resumeManagedJob": "定期実行を開始", + "capabilityDraft.registeredPanel.agentEnvelope.action.blocked": "先に問題を解決", + "capabilityDraft.registeredPanel.agentEnvelope.action.draft": "プロジェクトアシスタントとして保存", + "capabilityDraft.registeredPanel.agentEnvelope.action.manualEnable": "先に一度試す", + "capabilityDraft.registeredPanel.agentEnvelope.agentCard.pending": "プロジェクトアシスタント:試用が成功したら保存できます。", + "capabilityDraft.registeredPanel.agentEnvelope.agentCard.ready": "プロジェクトアシスタント:保存準備ができています({{directory}})。", + "capabilityDraft.registeredPanel.agentEnvelope.blockedReasonFallback": "まだ解決が必要な問題があります", + "capabilityDraft.registeredPanel.agentEnvelope.description": "試用結果に問題がなければ、このプロジェクトのアシスタントとして保存できます。", + "capabilityDraft.registeredPanel.agentEnvelope.discovery.pending": "チーム公開:プロジェクトアシスタントとして保存後に公開されます。", + "capabilityDraft.registeredPanel.agentEnvelope.discovery.ready": "チーム公開:現在のプロジェクトメンバーがこのスキルを使用できます。", "capabilityDraft.registeredPanel.agentEnvelope.evidence.blocking": ", blocked: {{reasons}}", - "capabilityDraft.registeredPanel.agentEnvelope.evidence.completedPack": "Evidence: evidence pack {{packId}} is linked.", - "capabilityDraft.registeredPanel.agentEnvelope.evidence.completedPackFallback": "Evidence: evidence pack is linked.", + "capabilityDraft.registeredPanel.agentEnvelope.evidence.completedPack": "最新結果:チェックに合格しました({{packId}})。", + "capabilityDraft.registeredPanel.agentEnvelope.evidence.completedPackFallback": "最新結果:チェックに合格しました。", "capabilityDraft.registeredPanel.agentEnvelope.evidence.completionAudit": "Evidence: completion audit {{decision}}, owner {{successfulOwnerRunCount}}/{{ownerRunCount}}, ToolCall {{toolCallCount}}, artifact {{artifactCount}}{{controlledGetLabel}}{{blockingLabel}}{{suffix}}.", "capabilityDraft.registeredPanel.agentEnvelope.evidence.controlledGet.executed": ", Controlled GET {{executed}}/{{artifacts}} executed", "capabilityDraft.registeredPanel.agentEnvelope.evidence.controlledGet.requiredMissing": ", Controlled GET required 0/0 executed", - "capabilityDraft.registeredPanel.agentEnvelope.evidence.missing": "Evidence: no successful-run evidence yet; enable it in this turn first to get one result.", - "capabilityDraft.registeredPanel.agentEnvelope.evidence.pendingPack": "Evidence: evidence pack {{packId}} is linked, but a completed completion audit is still missing, so it cannot be solidified as an Agent.", - "capabilityDraft.registeredPanel.agentEnvelope.evidence.sourceMetadataOnly": "Evidence: P3E source metadata exists and can trace this session authorization source.", + "capabilityDraft.registeredPanel.agentEnvelope.evidence.missing": "最新結果:まだ試用結果がありません。", + "capabilityDraft.registeredPanel.agentEnvelope.evidence.pendingPack": "最新結果:生成済みです。チェック後にプロジェクトアシスタントとして保存できます。", + "capabilityDraft.registeredPanel.agentEnvelope.evidence.sourceMetadataOnly": "最新結果:今回の試用元を記録済みで、チェック待ちです。", "capabilityDraft.registeredPanel.agentEnvelope.evidence.suffix.missingControlledGet": "; missing Controlled GET evidence, cannot solidify as Agent", "capabilityDraft.registeredPanel.agentEnvelope.evidence.suffix.notCompleted": "; not completed, cannot solidify as Agent", - "capabilityDraft.registeredPanel.agentEnvelope.memory.pending": "Memory: wait for the first run before recording preferences, methodology, and corrections.", - "capabilityDraft.registeredPanel.agentEnvelope.memory.withReport": "Memory: references verification report {{reportId}} and future run corrections.", - "capabilityDraft.registeredPanel.agentEnvelope.permission.empty": "Permissions: manual confirmation by default; no extra external write permission declared.", + "capabilityDraft.registeredPanel.agentEnvelope.memory.pending": "記録:初回試用後に好みと修正内容を記録します。", + "capabilityDraft.registeredPanel.agentEnvelope.memory.withReport": "記録:チェック結果を保存済みです。今後のフィードバックも蓄積されます。", + "capabilityDraft.registeredPanel.agentEnvelope.permission.empty": "権限:デフォルトでは手動確認が必要です。", "capabilityDraft.registeredPanel.agentEnvelope.permission.withSummary": "Permissions: {{summary}}.", - "capabilityDraft.registeredPanel.agentEnvelope.runbook": "Runbook: {{name}}", - "capabilityDraft.registeredPanel.agentEnvelope.schedule": "Schedule: first slice only supports a manual rerun draft; no long-running task is created.", - "capabilityDraft.registeredPanel.agentEnvelope.sharing.pending": "Sharing: shown only to the current operator until the audit is complete.", - "capabilityDraft.registeredPanel.agentEnvelope.sharing.ready": "Sharing: shareable inside the current workspace / team; not published to the public Marketplace.", - "capabilityDraft.registeredPanel.agentEnvelope.status.blocked": "Agent draft blocked", - "capabilityDraft.registeredPanel.agentEnvelope.status.evidenceReady": "Evidence ready", - "capabilityDraft.registeredPanel.agentEnvelope.status.manualEnableRequired": "Waiting for successful run", - "capabilityDraft.registeredPanel.agentEnvelope.status.sourceMetadataReady": "Waiting for Completion Audit", - "capabilityDraft.registeredPanel.agentEnvelope.title": "Agent envelope draft", - "capabilityDraft.registeredPanel.agentEnvelope.widget.pending": "Widget: waits for run status, artifacts, blockers, and evidence entry.", - "capabilityDraft.registeredPanel.agentEnvelope.widget.ready": "Widget: shows Managed Job status, recent artifacts, audit decision, and next actions.", + "capabilityDraft.registeredPanel.agentEnvelope.runbook": "使い方:{{name}}", + "capabilityDraft.registeredPanel.agentEnvelope.schedule": "定期実行:未設定です。", + "capabilityDraft.registeredPanel.agentEnvelope.sharing.pending": "共有:今は自分だけに表示されます。", + "capabilityDraft.registeredPanel.agentEnvelope.sharing.ready": "共有:現在のプロジェクト内で共有できます。", + "capabilityDraft.registeredPanel.agentEnvelope.status.blocked": "対応が必要", + "capabilityDraft.registeredPanel.agentEnvelope.status.evidenceReady": "保存可能", + "capabilityDraft.registeredPanel.agentEnvelope.status.manualEnableRequired": "未試用", + "capabilityDraft.registeredPanel.agentEnvelope.status.sourceMetadataReady": "チェック待ち", + "capabilityDraft.registeredPanel.agentEnvelope.title": "プロジェクトアシスタント", + "capabilityDraft.registeredPanel.agentEnvelope.details": "プロジェクトアシスタント詳細", + "capabilityDraft.registeredPanel.agentEnvelope.widget.pending": "結果:試用後に最新状態と次の手順を表示します。", + "capabilityDraft.registeredPanel.agentEnvelope.widget.ready": "結果:最新状態、生成物、次の手順を表示します。", "capabilityDraft.registeredPanel.approval.consumptionGate.title": "Consumption gate", "capabilityDraft.registeredPanel.approval.credentialResolver.title": "Session credential resolver", "capabilityDraft.registeredPanel.approval.description": "Before a real API execution, this approval request artifact must be consumed first; currently only the audit entry is persisted, without saving tokens or sending requests.", @@ -465,10 +685,11 @@ "capabilityDraft.registeredPanel.approval.suffix.required": ":required", "capabilityDraft.registeredPanel.approval.suffix.secret": ":secret", "capabilityDraft.registeredPanel.approval.title": "Session approval request artifact", - "capabilityDraft.registeredPanel.badge": "登録済み", - "capabilityDraft.registeredPanel.card.badge.registered": "登録済み", - "capabilityDraft.registeredPanel.card.binding.blocked": "Binding ブロック中", - "capabilityDraft.registeredPanel.card.binding.candidate": "P3C binding 候補", + "capabilityDraft.registeredPanel.technicalDetails": "技術詳細", + "capabilityDraft.registeredPanel.badge": "保存済み", + "capabilityDraft.registeredPanel.card.badge.registered": "保存済み", + "capabilityDraft.registeredPanel.card.binding.blocked": "対応が必要", + "capabilityDraft.registeredPanel.card.binding.candidate": "試用可能", "capabilityDraft.registeredPanel.card.descriptionFallback": "現在の Workspace のローカル Skill パッケージとして登録済みです。", "capabilityDraft.registeredPanel.card.field.directory": "ディレクトリ:", "capabilityDraft.registeredPanel.card.field.nextGate": "次の gate:", @@ -478,11 +699,11 @@ "capabilityDraft.registeredPanel.card.field.source": "ソース:", "capabilityDraft.registeredPanel.card.field.standard": "標準:", "capabilityDraft.registeredPanel.card.nextGateFallback": "manual_runtime_enable / Query Loop metadata / tool_runtime authorization trimming", - "capabilityDraft.registeredPanel.description": "検証を通過して現在のプロジェクトに書き込まれた Skill パッケージだけを表示します。実行はまだ runtime gate を待ちます。", - "capabilityDraft.registeredPanel.empty.error": "登録済み能力を読み取れません:{{message}}", - "capabilityDraft.registeredPanel.empty.loading": "登録済み能力を読み取っています...", - "capabilityDraft.registeredPanel.empty.missingProject": "プロジェクトを選択または開くと、そのプロジェクトに登録済みの generated skill を確認できます。", - "capabilityDraft.registeredPanel.empty.noSkills": "このプロジェクトにはまだ P3A 登録済み能力がありません。ドラフトが検証と登録を通過すると、まずここに表示されます。", + "capabilityDraft.registeredPanel.description": "チェック済みで現在のプロジェクトに保存されたスキルです。まず試すことも、定期実行を設定することもできます。", + "capabilityDraft.registeredPanel.empty.error": "保存済みスキルを読み込めません。しばらくしてから再試行してください。", + "capabilityDraft.registeredPanel.empty.loading": "保存済みスキルを読み込み中...", + "capabilityDraft.registeredPanel.empty.missingProject": "プロジェクトを選択または開くと、そのプロジェクトに保存されたスキルを確認できます。", + "capabilityDraft.registeredPanel.empty.noSkills": "このプロジェクトにはまだ保存済みスキルがありません。「作成中」でチェックして保存してください。", "capabilityDraft.registeredPanel.error.missingAutomationSession": "直近の automation run に session が紐付いていないため、evidence をエクスポートできません。", "capabilityDraft.registeredPanel.evidence.credentialReferenceId": "Credential reference", "capabilityDraft.registeredPanel.evidence.endpointSource": "Endpoint", @@ -516,6 +737,29 @@ "capabilityDraft.registeredPanel.managedJob.status.notCreated": "Managed Job: not created", "capabilityDraft.registeredPanel.managedJob.status.notRun": "Not run yet", "capabilityDraft.registeredPanel.managedJob.status.withValue": "Managed Job: {{state}} · {{lastStatus}}", + "capabilityDraft.registeredPanel.user.permission.defaultReadonly": "デフォルトでは情報の読み取りのみ", + "capabilityDraft.registeredPanel.user.permission.draftWrite": "下書きに書き込み可能", + "capabilityDraft.registeredPanel.user.permission.localCommand": "ローカルコマンドを実行可能", + "capabilityDraft.registeredPanel.user.permission.readonly": "情報の読み取りのみ", + "capabilityDraft.registeredPanel.user.schedule.enabled": "有効", + "capabilityDraft.registeredPanel.user.schedule.notCreated": "未設定", + "capabilityDraft.registeredPanel.user.schedule.paused": "一時停止中", + "capabilityDraft.registeredPanel.user.result.auditBlocked": "最新結果は対応が必要です", + "capabilityDraft.registeredPanel.user.result.auditPassed": "最新結果はチェック済みです", + "capabilityDraft.registeredPanel.user.result.noJob": "定期実行は未設定", + "capabilityDraft.registeredPanel.user.result.noRun": "実行履歴はまだありません", + "capabilityDraft.registeredPanel.user.result.readyToCheck": "最新結果をチェックできます", + "capabilityDraft.registeredPanel.user.result.running": "実行中", + "capabilityDraft.registeredPanel.user.step.saved": "保存済み", + "capabilityDraft.registeredPanel.user.step.savedValue": "現在のプロジェクトで使用可能", + "capabilityDraft.registeredPanel.user.step.permission": "権限", + "capabilityDraft.registeredPanel.user.step.schedule": "定期実行", + "capabilityDraft.registeredPanel.user.nextStep": "まず一度試してください。結果に問題がなければ、定期実行を設定するかプロジェクトアシスタントとして保存できます。", + "capabilityDraft.registeredPanel.user.assistant.try": "試用", + "capabilityDraft.registeredPanel.user.assistant.tryReady": "一度試せます", + "capabilityDraft.registeredPanel.user.assistant.tryBlocked": "まだ試用できません", + "capabilityDraft.registeredPanel.user.assistant.schedule": "定期実行", + "capabilityDraft.registeredPanel.user.assistant.result": "最新結果", "capabilityDraft.registeredPanel.provenance.title": "Registration provenance", "capabilityDraft.registeredPanel.summary.bindingBlockedFallback": "Runtime binding is currently blocked by a gate.", "capabilityDraft.registeredPanel.summary.bindingCandidateFallback": "It is eligible as a later runtime binding candidate, but is still not in the default tool surface.", @@ -525,7 +769,7 @@ "capabilityDraft.registeredPanel.summary.standardIssueCount": "標準チェックにまだ {{count}} 件の問題があります", "capabilityDraft.registeredPanel.summary.standardPassed": "Agent Skills 標準に合格", "capabilityDraft.registeredPanel.summary.standardPending": "Agent Skills 標準状態は確認待ち", - "capabilityDraft.registeredPanel.title": "Workspace 登録済み能力", + "capabilityDraft.registeredPanel.title": "保存済みスキル", "curatedTask.badge.action.clearAria": "{{title}} をクリア", "curatedTask.badge.action.clearTitle": "現在の結果テンプレートをクリア", "curatedTask.badge.action.edit": "編集", @@ -1233,13 +1477,46 @@ "skills.workspace.group.emptyTitle": "このグループに一致する Skill はありません", "skills.workspace.group.selectedSubtitle": "このグループの Skill を選んで続ける", "skills.workspace.header.refresh": "更新", - "skills.workspace.header.subtitle": "Skill を選んで作成を始める", - "skills.workspace.header.title": "Skills", + "skills.workspace.header.subtitle": "スキルをインストール・管理し、必要なときにホーム入力欄へ戻して使えます。", + "skills.workspace.header.title": "スキル", + "skills.workspace.header.createWithLime": "Lime で作成", + "skills.workspace.header.installSkill": "Skill をインストール", + "skills.workspace.autoLoad.disabledToast": "「{{name}}」は自動読み込みされません", + "skills.workspace.autoLoad.enabledToast": "「{{name}}」の自動読み込みを許可しました", + "skills.workspace.autoLoad.aria": "{{name}} の自動読み込みを許可", + "skills.workspace.autoLoad.description": "オンにすると、関連するタスクで Lime がこのスキルを自動的に使えます。オフにすると、自動では選ばれません。", + "skills.workspace.autoLoad.off": "オフ", + "skills.workspace.autoLoad.on": "オン", + "skills.workspace.autoLoad.label": "自動読み込み", + "skills.workspace.store.officialBadge": "公式おすすめ", + "skills.workspace.scaffold.createFailed": "スキルの作成に失敗しました:{{message}}", + "skills.workspace.import.failed": "スキルのインストールに失敗しました:{{message}}", + "skills.workspace.import.success": "スキルをインストールし、ユーザーインストールに追加しました。", + "skills.workspace.import.dialogTitle": "SKILL.md を含むスキルフォルダーを選択", + "skills.workspace.header.installingSkill": "インストール中", + "skills.workspace.hero.title": "仕事向け Skill を厳選", + "skills.workspace.hero.description": "執筆、効率化、デザイン、データ分析などをワンクリックでインストールできます。", + "skills.workspace.view.store": "Skill 広場", + "skills.workspace.view.builtin": "内蔵", + "skills.workspace.view.installed": "インストール済み", + "skills.workspace.filter.all": "すべて", + "skills.workspace.sort.hot": "並び替え:人気", + "skills.workspace.store.other": "その他の Skill · {{count}}", + "skills.workspace.builtin.title": "内蔵 Skill", + "skills.workspace.builtin.subtitle": "Lime に含まれるスキルです。スイッチは自動マッチだけを制御し、Lime の利用には影響しません。", + "skills.workspace.builtin.empty": "更新するか、別のキーワードで探してください。", + "skills.workspace.installed.title": "インストール済み", + "skills.workspace.installed.subtitle": "インストール済みのスキルです。自動読み込み、使用、アンインストールは別々の操作です。", "skills.workspace.header.viewAll": "すべて表示", - "skills.workspace.installedSkill.defaultPromise": "この Skill を再利用したいときに使います。", + "skills.workspace.installedSkill.action.use": "使う", + "skills.workspace.installedSkill.action.uninstall": "アンインストール", + "skills.workspace.installedSkill.action.uninstalling": "アンインストール中", + "skills.workspace.installedSkill.uninstallSuccess": "「{{name}}」をアンインストールしました", + "skills.workspace.installedSkill.uninstallFailed": "アンインストールに失敗しました: {{message}}", + "skills.workspace.installedSkill.defaultPromise": "このスキルを再利用したいときに使います。", "skills.workspace.installedSkill.entryBanner": "Skill「{{name}}」を持って生成に戻りました。このまま今回の作業を進められます。", "skills.workspace.installedSkill.entryBannerWithReplay": "Skill「{{name}}」と前回の目標を持って生成に戻りました。このまま今回の作業を進められます。", - "skills.workspace.installedSkill.fallbackOutputHint": "この Skill を使って生成へ進む", + "skills.workspace.installedSkill.fallbackOutputHint": "このスキルをホーム入力欄へ戻す", "skills.workspace.installedSkill.fallbackRequiredInputs": "チャットで目標と制約を続けて補足", "skills.workspace.installedSkill.outputPrefix": "成果: ", "skills.workspace.installedSkill.requiredPrefix": "必要情報: ", @@ -1703,5 +1980,7 @@ "agentChat.threadReliability.view.recommendation.interrupting": "現在の実行を停止中です。ランタイムが最終状態を反映するまで待ってください", "agentChat.threadReliability.view.recommendation.interrupted": "現在の実行はランタイムにより中断確認済みです", "agentChat.threadReliability.view.recommendation.nextQueuedTurn": "キュー内ターンを続行できます:{{title}}", - "agentChat.threadReliability.view.recommendation.retryable": "直近の結果は再試行可能です。再開するか新しいターンを開始してください" + "agentChat.threadReliability.view.recommendation.retryable": "直近の結果は再試行可能です。再開するか新しいターンを開始してください", + "skills.inputBadge.remove": "@{{name}}を外す", + "skills.workspace.view.tabsLabel": "スキルページ" } diff --git a/src/i18n/resources/ko-KR/agent.json b/src/i18n/resources/ko-KR/agent.json index 699b37264..2b0fc23be 100644 --- a/src/i18n/resources/ko-KR/agent.json +++ b/src/i18n/resources/ko-KR/agent.json @@ -146,58 +146,18 @@ "agentChat.harnessVerification.section.failureFocusTitle": "검증 실패 포커스", "agentChat.harnessVerification.section.recoveredTitle": "복구된 결과", "agentChat.harnessVerification.section.title": "검증 결과", - "agentChat.imageWorkbenchPreview.description.cancelled": "작업이 취소되어 새 이미지 결과를 계속 생성하지 않습니다.", - "agentChat.imageWorkbenchPreview.description.complete.multiple": "{{count}}개의 {{result}} 항목이 반환되었습니다. 오른쪽에서 확인하고 사용할 수 있습니다.", - "agentChat.imageWorkbenchPreview.description.complete.single": "{{result}}가 완료되었습니다. 오른쪽에서 확인하고 사용할 수 있습니다.", - "agentChat.imageWorkbenchPreview.description.complete.storyboard": "{{label}}가 완료되었습니다. 오른쪽에서 확인하고 사용할 수 있습니다.", - "agentChat.imageWorkbenchPreview.description.failed.notRetryable": "다시 시도하기 전에 설정 또는 파라미터를 조정해야 합니다.", - "agentChat.imageWorkbenchPreview.description.failed.retryable": "사용 가능한 결과를 받지 못했습니다. 프롬프트를 조정한 뒤 다시 시도하세요.", - "agentChat.imageWorkbenchPreview.description.partial.multiple": "{{returned}} / {{expected}}개의 {{result}} 항목이 반환되었고, 나머지는 아직 완료되지 않았습니다.", - "agentChat.imageWorkbenchPreview.description.partial.single": "{{result}} 일부가 동기화되었습니다. 오른쪽에서 확인할 수 있습니다.", - "agentChat.imageWorkbenchPreview.description.partial.storyboard": "{{label}}가 {{returned}} / {{expected}} 프레임을 반환했습니다. 오른쪽에서 확인할 수 있습니다.", - "agentChat.imageWorkbenchPreview.description.running.edit": "이미지 수정을 처리 중입니다. 완료되면 실제 결과로 자동 교체됩니다.", - "agentChat.imageWorkbenchPreview.description.running.generate": "이미지를 생성 중입니다. 완료되면 실제 결과로 자동 교체됩니다.", - "agentChat.imageWorkbenchPreview.description.running.storyboard": "{{label}}가 생성되었으며 {{expected}} 프레임이 같은 작업 카드에 순차적으로 채워집니다.", - "agentChat.imageWorkbenchPreview.description.running.variation": "변형을 생성 중입니다. 완료되면 실제 결과로 자동 교체됩니다.", - "agentChat.imageWorkbenchPreview.layout.storyboard": "3x3 스토리보드", - "agentChat.imageWorkbenchPreview.media.alt": "이미지 작업 결과", + "agentChat.imageWorkbenchPreview.media.alt": "이미지 결과", + "agentChat.imageWorkbenchPreview.media.open": "이미지 결과 열기", "agentChat.imageWorkbenchPreview.media.previewFailed": "미리보기 실패", - "agentChat.imageWorkbenchPreview.meta.imageCount": "{{count}}장", - "agentChat.imageWorkbenchPreview.meta.imageProgress": "{{current}}/{{expected}}장", - "agentChat.imageWorkbenchPreview.placeholder.cancelled": "작업 취소됨", - "agentChat.imageWorkbenchPreview.placeholder.failed": "아직 성공한 결과가 없습니다", + "agentChat.imageWorkbenchPreview.tool.generation": "Image Generation", + "agentChat.imageWorkbenchPreview.tool.editing": "Image Editing", + "agentChat.imageWorkbenchPreview.tool.redraw": "Image Redraw", + "agentChat.imageWorkbenchPreview.placeholder.cancelled": "취소됨", + "agentChat.imageWorkbenchPreview.placeholder.editing": "이미지 수정 중", + "agentChat.imageWorkbenchPreview.placeholder.failed": "생성 실패", + "agentChat.imageWorkbenchPreview.placeholder.generating": "이미지 생성 중", "agentChat.imageWorkbenchPreview.placeholder.imageUnavailable": "이미지를 일시적으로 표시할 수 없습니다", - "agentChat.imageWorkbenchPreview.placeholder.synced": "결과 동기화됨", - "agentChat.imageWorkbenchPreview.promptFallback": "이 작업에는 프롬프트가 없습니다.", - "agentChat.imageWorkbenchPreview.result.edit": "수정 이미지 결과", - "agentChat.imageWorkbenchPreview.result.generate": "이미지 결과", - "agentChat.imageWorkbenchPreview.result.variation": "변형 결과", - "agentChat.imageWorkbenchPreview.slot.cancelled": "취소됨", - "agentChat.imageWorkbenchPreview.slot.failed": "이 프레임 실패", - "agentChat.imageWorkbenchPreview.slot.pending": "생성 대기", - "agentChat.imageWorkbenchPreview.slot.waiting": "보충 대기", - "agentChat.imageWorkbenchPreview.source.editSummary": "이 작업은 기존 이미지 결과를 기반으로 수정을 계속합니다.", - "agentChat.imageWorkbenchPreview.source.footnote": "{{label}}: {{summary}}", - "agentChat.imageWorkbenchPreview.source.ref": "{{ref}} 참조됨", - "agentChat.imageWorkbenchPreview.source.referenceCount": "참고 이미지 {{count}}장을 첨부했습니다.", - "agentChat.imageWorkbenchPreview.source.referenceLabel": "참고 이미지", - "agentChat.imageWorkbenchPreview.source.sourceCount": "원본 이미지 {{count}}장을 참조했습니다.", - "agentChat.imageWorkbenchPreview.source.sourceLabel": "원본 이미지", - "agentChat.imageWorkbenchPreview.source.variationSummary": "이 작업은 참고 이미지를 기반으로 새 변형 결과를 생성합니다.", - "agentChat.imageWorkbenchPreview.status.cancelled": "취소됨", - "agentChat.imageWorkbenchPreview.status.complete.edit": "수정됨", - "agentChat.imageWorkbenchPreview.status.complete.generate": "생성됨", - "agentChat.imageWorkbenchPreview.status.complete.variation": "변형됨", - "agentChat.imageWorkbenchPreview.status.failed.edit": "수정 실패", - "agentChat.imageWorkbenchPreview.status.failed.generate": "생성 실패", - "agentChat.imageWorkbenchPreview.status.failed.variation": "변형 실패", - "agentChat.imageWorkbenchPreview.status.partial": "일부 완료", - "agentChat.imageWorkbenchPreview.status.running.edit": "수정 중", - "agentChat.imageWorkbenchPreview.status.running.generate": "생성 중", - "agentChat.imageWorkbenchPreview.status.running.preparing": "준비 중", - "agentChat.imageWorkbenchPreview.status.running.queued": "대기열 대기", - "agentChat.imageWorkbenchPreview.status.running.variation": "변형 생성 중", - "agentChat.imageWorkbenchPreview.statusDescription": " · {{description}}", + "agentChat.imageWorkbenchPreview.placeholder.redrawing": "이미지 다시 그리는 중", "agentChat.incidentPanel.empty": "활성 incident가 감지되지 않았습니다", "agentChat.incidentPanel.priorityBadge": "{{severity}} 우선순위", "agentChat.messageList.firstTokenStatus.cancelled.detail": "이 요청은 중지되었습니다.", @@ -297,11 +257,270 @@ "agentChat.teamWorkspace.canvasToolbar.zoom": "확대 {{percent}}%", "agentChat.teamWorkspace.canvasToolbar.zoomIn": "확대", "agentChat.teamWorkspace.canvasToolbar.zoomOut": "축소", + "agentChat.teamWorkspace.canvasStage.badge": "현재 진행", + "agentChat.teamWorkspace.canvasStage.shortcutHint": "드래그로 작업 배치 조정 · Space 이동 · A 정렬 · F 포커스", + "agentChat.teamWorkspace.canvasStage.emptyTitle": "현재 진행 없음", + "agentChat.teamWorkspace.dock.surfaceTitle": "생성", + "agentChat.teamWorkspace.dock.currentProgress": "현재 진행", + "agentChat.teamWorkspace.dock.processingBudget": "{{activeCount}}/{{totalCount}} 처리 중", + "agentChat.teamWorkspace.dock.processingCount": "{{formattedCount}}개 처리 중", + "agentChat.teamWorkspace.dock.waitingCount": "{{formattedCount}}개 대기 중", + "agentChat.teamWorkspace.dock.taskCount": "{{formattedCount}}개 작업", + "agentChat.teamWorkspace.dock.selectedPlan": "현재 분담 계획: {{label}}", + "agentChat.teamWorkspace.dock.unobstructiveBadge": "캔버스를 가리지 않음", + "agentChat.teamWorkspace.dock.aria.open": "현재 진행 열기", + "agentChat.teamWorkspace.dock.aria.collapse": "현재 진행 접기", + "agentChat.teamWorkspace.dock.aria.expand": "현재 진행 펼치기", "agentChat.teamWorkspace.teamActions.closeCompleted": "완료된 작업 접기", "agentChat.teamWorkspace.teamActions.closing": "접는 중...", "agentChat.teamWorkspace.teamActions.waitAny": "아무 작업 결과나 기다리기", "agentChat.teamWorkspace.teamActions.waiting": "기다리는 중...", - "agentChat.teamWorkspace.runtimeStatus.completed": "완료", + "agentChat.teamWorkspace.board.detail.collapse": "세부 정보 접기", + "agentChat.teamWorkspace.board.detail.expand": "세부 정보 보기", + "agentChat.teamWorkspace.board.canvas.title": "현재 진행", + "agentChat.teamWorkspace.board.canvas.subtitle.childRuntime": "병렬 작업은 각 패널에서 진행과 결과를 계속 업데이트하고, 메인 대화에는 필요한 요약만 남깁니다.", + "agentChat.teamWorkspace.board.canvas.subtitle.runtime": "현재 진행 {{formattedCount}}개가 연결되었습니다. 포커스는 처리 중인 역할 분담을 우선합니다.", + "agentChat.teamWorkspace.board.canvas.subtitle.formation.forming": "현재 작업 역할 분담을 준비 중입니다. 작업이 만들어지면 여기에서 독립적으로 진행 상황이 업데이트됩니다.", + "agentChat.teamWorkspace.board.canvas.subtitle.formation.formed": "현재 작업 역할 분담이 준비되었습니다. 작업이 만들어지면 각 진행 패널에서 처리를 시작합니다.", + "agentChat.teamWorkspace.board.canvas.subtitle.formation.failed": "이번 작업 역할 분담 준비에 실패해 아직 현재 진행 패널을 만들 수 없습니다.", + "agentChat.teamWorkspace.board.canvas.subtitle.formation.default": "작업이 만들어지면 여기에 독립된 현재 진행 상황이 펼쳐집니다.", + "agentChat.teamWorkspace.boardChrome.headline.parentOverviewFallback": "메인 어시스턴트 개요", + "agentChat.teamWorkspace.boardChrome.headline.empty": "필요할 때 작업을 자동으로 나눕니다", + "agentChat.teamWorkspace.boardChrome.headline.runningWithQueued": "작업 진행 중 · {{runningCount}}개 처리 중 / {{queuedCount}}개 대기 중", + "agentChat.teamWorkspace.boardChrome.headline.runningCount": "작업 진행 중 · {{formattedCount}}개 처리 중", + "agentChat.teamWorkspace.boardChrome.headline.running": "작업 진행 중", + "agentChat.teamWorkspace.boardChrome.headline.queuedCount": "작업 준비 중 · {{formattedCount}}개 대기 중", + "agentChat.teamWorkspace.boardChrome.headline.queued": "작업 준비 중", + "agentChat.teamWorkspace.boardChrome.headline.completedCount": "{{formattedCount}}개 작업 완료", + "agentChat.teamWorkspace.boardChrome.headline.completed": "작업 완료", + "agentChat.teamWorkspace.boardChrome.headline.retryCount": "{{formattedCount}}개 작업 재시도 필요", + "agentChat.teamWorkspace.boardChrome.headline.retry": "작업 재시도 필요", + "agentChat.teamWorkspace.boardChrome.headline.connectedCount": "{{formattedCount}}개 현재 진행 상황 연결됨", + "agentChat.teamWorkspace.boardChrome.headline.surfaceTitle": "생성", + "agentChat.teamWorkspace.boardChrome.hint.childWithSiblings": "현재 작업이 {{formattedCount}}개 병렬 하위 작업과 함께 진행 중입니다", + "agentChat.teamWorkspace.boardChrome.hint.childSingle": "이 하위 작업을 처리 중이며 결과는 메인 어시스턴트로 돌아갑니다.", + "agentChat.teamWorkspace.boardChrome.hint.empty": "필요할 때 작업을 나누고 순서를 정한 뒤 결과를 현재 작업으로 되돌립니다.", + "agentChat.teamWorkspace.boardChrome.hint.runtime": "현재 처리 중인 분담, 상태, 최근 업데이트 위치를 여기서 확인합니다.", + "agentChat.teamWorkspace.boardChrome.chip.focus": "현재 초점 {{name}}", + "agentChat.teamWorkspace.boardChrome.chip.updatedAt": "{{updatedAt}} 업데이트", + "agentChat.teamWorkspace.boardChrome.chip.waitable": "{{formattedCount}}개 처리 중", + "agentChat.teamWorkspace.boardChrome.chip.completed": "{{formattedCount}}개 완료", + "agentChat.teamWorkspace.boardChrome.statusSummary": "{{status}} {{formattedCount}}", + "agentChat.teamWorkspace.overview.selectedSession.waitingTitle": "작업 인계 대기", + "agentChat.teamWorkspace.overview.selectedSession.focusBadge": "현재 포커스", + "agentChat.teamWorkspace.overview.selectedSession.waitingBadge": "인계 대기", + "agentChat.teamWorkspace.overview.compact.taskActionsLabel": "작업 처리", + "agentChat.teamWorkspace.overview.compact.viewActionsLabel": "보기", + "agentChat.teamWorkspace.overview.taskCount.waitable": "처리 중인 작업 {{formattedCount}}개", + "agentChat.teamWorkspace.overview.taskCount.completed": "완료된 작업 {{formattedCount}}개", + "agentChat.teamWorkspace.formation.hint.forming": "시스템이 현재 작업의 역할 분담을 준비하고 있습니다. 작업이 만들어지면 자동으로 처리를 시작합니다.", + "agentChat.teamWorkspace.formation.hint.formed": "현재 작업의 역할 분담이 준비되었습니다. 작업이 만들어지면 이어서 처리를 맡습니다.", + "agentChat.teamWorkspace.formation.hint.failed": "현재 작업의 역할 분담 준비에 실패했지만, 현재 작업 안에서 계속 진행할 수 있습니다.", + "agentChat.teamWorkspace.formation.hint.default": "필요할 때 이 영역이 현재 진행 패널로 자동 확장됩니다.", + "agentChat.teamWorkspace.formation.emptyDetail.forming": "시스템이 현재 작업을 기준으로 역할 분담을 준비하고 있습니다. 완료되면 먼저 역할 카드를 보여주고 실제 처리 진행 상황을 연결합니다.", + "agentChat.teamWorkspace.formation.detail.hint.runtimeWithReference": "현재 작업 역할 분담과 참고안을 보기", + "agentChat.teamWorkspace.formation.detail.hint.planOnly": "현재 역할 분담안 보기", + "agentChat.teamWorkspace.formation.detail.summary.runtime": "현재 요약", + "agentChat.teamWorkspace.formation.detail.summary.plan": "분담안 요약", + "agentChat.teamWorkspace.formation.detail.referenceSummaryDefault": "현재 작업 역할 분담은 선택된 분담안의 역할 선호를 참고합니다.", + "agentChat.teamWorkspace.formation.detail.roleSection.runtime": "현재 작업 역할 분담", + "agentChat.teamWorkspace.formation.detail.roleSection.plan": "역할 분담", + "agentChat.teamWorkspace.formation.emptyDetail.formed": "현재 역할 분담안이 준비되었습니다. 캔버스는 먼저 분담안을 보여주고, 작업이 실제로 시작되면 현재 진행 상황으로 전환합니다.", + "agentChat.teamWorkspace.formation.emptyDetail.failed": "현재 작업 역할 분담 준비에 실패해 현재 진행 상황을 잠시 표시할 수 없습니다.", + "agentChat.teamWorkspace.formation.emptyDetail.default": "아직 작업을 맡은 항목이 없습니다. 시스템이 역할 분담을 시작하면 상세 영역이 진행 요약 보기로 전환됩니다.", + "agentChat.teamWorkspace.formation.notice.forming": "시스템이 현재 작업 역할 분담을 준비하고 있습니다. 완료되면 먼저 역할 카드를 보여준 뒤 독립적인 현재 진행 상황으로 전환합니다.", + "agentChat.teamWorkspace.formation.notice.formed": "현재 역할 분담안이 준비되었습니다. 작업이 만들어지면 이곳은 분담안 보기에서 현재 진행 상황으로 전환됩니다.", + "agentChat.teamWorkspace.formation.notice.failed": "현재 작업 역할 분담 준비에 실패해 아직 작업을 맡은 항목이 없습니다.", + "agentChat.teamWorkspace.formation.notice.default": "아직 작업을 맡은 항목이 없습니다. 시스템이 역할 분담을 시작하면 이곳에 독립적인 현재 진행 상황이 생성됩니다.", + "agentChat.teamWorkspace.formation.summaryBadge.planLabel": "역할 분담안 · {{label}}", + "agentChat.teamWorkspace.formation.summaryBadge.planRoleCount": "계획된 역할 {{formattedCount}}개", + "agentChat.teamWorkspace.formation.summaryBadge.memberCount": "현재 진행 {{formattedCount}}개", + "agentChat.teamWorkspace.formation.summaryBadge.referenceLabel": "참고 분담안 · {{label}}", + "agentChat.teamWorkspace.formation.summaryBadge.taskCount": "작업 {{formattedCount}}개", + "agentChat.teamWorkspace.formation.panel.title": "현재 작업 역할 분담", + "agentChat.teamWorkspace.formation.panel.description.failed": "현재 작업 역할 분담 준비에 실패해 아직 더 많은 내용을 표시할 수 없습니다.", + "agentChat.teamWorkspace.formation.panel.description.default": "여기에 먼저 현재 역할 분담안을 보여주고, 작업이 만들어진 뒤 현재 진행 상황으로 전환합니다.", + "agentChat.teamWorkspace.formation.waitingHeadline": "작업 인계 대기", + "agentChat.teamWorkspace.formation.status.forming.label": "준비 중", + "agentChat.teamWorkspace.formation.status.forming.title": "작업 역할 분담 준비 중", + "agentChat.teamWorkspace.formation.status.formed.label": "준비됨", + "agentChat.teamWorkspace.formation.status.formed.title": "작업 역할 분담 준비됨", + "agentChat.teamWorkspace.formation.status.failed.label": "준비 실패", + "agentChat.teamWorkspace.formation.status.failed.title": "작업 역할 분담 준비 실패", + "agentChat.teamWorkspace.formation.memberStatus.planned": "배정 대기", + "agentChat.teamWorkspace.formation.memberStatus.spawning": "연결 중", + "agentChat.teamWorkspace.formation.memberStatus.running": "처리 중", + "agentChat.teamWorkspace.formation.memberStatus.waiting": "계속 대기", + "agentChat.teamWorkspace.formation.memberStatus.completed": "완료됨", + "agentChat.teamWorkspace.formation.memberStatus.failed": "재시도 필요", + "agentChat.teamWorkspace.selectedSession.header.eyebrow": "현재 보기", + "agentChat.teamWorkspace.selectedSession.header.currentTaskBadge": "현재 작업", + "agentChat.teamWorkspace.selectedSession.header.action.resumePending": "계속하는 중...", + "agentChat.teamWorkspace.selectedSession.header.action.resume": "처리 계속", + "agentChat.teamWorkspace.selectedSession.header.action.closePending": "일시 중지 중...", + "agentChat.teamWorkspace.selectedSession.header.action.close": "처리 일시 중지", + "agentChat.teamWorkspace.selectedSession.header.action.openChild": "진행으로 전환", + "agentChat.teamWorkspace.selectedSession.header.action.open": "진행 열기", + "agentChat.teamWorkspace.selectedSession.settings.heading": "작업 역할 분담", + "agentChat.teamWorkspace.teamPreset.codeTriageTeam": "코드 트리아지 팀", + "agentChat.teamWorkspace.teamPreset.researchTeam": "리서치 팀", + "agentChat.teamWorkspace.teamPreset.contentCreationTeam": "콘텐츠 제작 팀", + "agentChat.teamWorkspace.selectedSession.detail.setting.preset": "프리셋 {{label}}", + "agentChat.teamWorkspace.selectedSession.detail.setting.profile": "스타일 {{name}}", + "agentChat.teamWorkspace.selectedSession.detail.setting.role": "역할 {{label}}", + "agentChat.teamWorkspace.selectedSession.detail.setting.theme": "테마 {{theme}}", + "agentChat.teamWorkspace.selectedSession.detail.metadata.role": "역할 {{label}}", + "agentChat.teamWorkspace.selectedSession.detail.metadata.provider": "서비스 {{name}}", + "agentChat.teamWorkspace.selectedSession.detail.metadata.model": "모델 {{model}}", + "agentChat.teamWorkspace.selectedSession.detail.metadata.originTool": "출처 {{tool}}", + "agentChat.teamWorkspace.selectedSession.detail.metadata.createdFromTurn": "이전 작업 {{turnId}}에서", + "agentChat.teamWorkspace.selectedSession.detail.metadata.parentSession": "{{name}}에서", + "agentChat.teamWorkspace.selectedSession.detail.sessionType.subAgent": "하위 작업", + "agentChat.teamWorkspace.selectedSession.detail.sessionType.fork": "보조 브랜치", + "agentChat.teamWorkspace.selectedSession.detail.sessionType.user": "메인 어시스턴트", + "agentChat.teamWorkspace.selectedSession.detail.roleHint.explorer": "분석", + "agentChat.teamWorkspace.selectedSession.detail.roleHint.executor": "실행", + "agentChat.teamWorkspace.selectedSession.detail.roleHint.reviewer": "검토", + "agentChat.teamWorkspace.selectedSession.detail.roleHint.writer": "작성", + "agentChat.teamWorkspace.selectedSession.detail.roleHint.planner": "계획", + "agentChat.teamWorkspace.selectedSession.detail.roleHint.researcher": "리서치", + "agentChat.teamWorkspace.selectedSession.detail.roleHint.designer": "디자인", + "agentChat.teamWorkspace.selectedSession.detail.roleHint.design": "디자인", + "agentChat.teamWorkspace.selectedSession.detail.roleHint.editor": "편집", + "agentChat.teamWorkspace.selectedSession.detail.roleHint.orchestrator": "메인 어시스턴트", + "agentChat.teamWorkspace.selectedSession.footer.updatedAt": "업데이트 {{updatedAt}}", + "agentChat.teamWorkspace.selectedSession.collaboration.heading": "처리 계속", + "agentChat.teamWorkspace.selectedSession.collaboration.waitReady": "결과를 바로 확인할 수 있음", + "agentChat.teamWorkspace.selectedSession.collaboration.waitPending": "기다리는 중...", + "agentChat.teamWorkspace.selectedSession.collaboration.wait": "결과 30초 기다리기", + "agentChat.teamWorkspace.selectedSession.collaboration.waitHint": "현재 내용이 이 작업 결과에 실제로 의존할 때만 사용하세요.", + "agentChat.teamWorkspace.selectedSession.collaboration.inputPlaceholder": "이 작업에 설명이나 제약을 추가하거나 다음 단계로 계속 진행하도록 요청하세요.", + "agentChat.teamWorkspace.selectedSession.collaboration.sendPending": "보내는 중...", + "agentChat.teamWorkspace.selectedSession.collaboration.send": "설명 보내기", + "agentChat.teamWorkspace.selectedSession.collaboration.interruptPending": "중단 중...", + "agentChat.teamWorkspace.selectedSession.collaboration.interrupt": "지금 설명 삽입", + "agentChat.teamWorkspace.selectedSession.collaboration.inputHint": "이 설명은 현재 작업에만 전송되며 다른 병렬 작업에는 영향을 주지 않습니다.", + "agentChat.teamWorkspace.selectedSession.activity.heading": "전체 진행", + "agentChat.teamWorkspace.selectedSession.activity.autoRefresh": "처리 중 자동 새로고침", + "agentChat.teamWorkspace.selectedSession.activity.errorPrefix": "최신 진행을 사용할 수 없음: ", + "agentChat.teamWorkspace.selectedSession.activity.errorFallback": "동기화 실패", + "agentChat.teamWorkspace.selectedSession.activity.emptyReady": "이 작업에는 아직 표시할 새 진행 상황이 없습니다.", + "agentChat.teamWorkspace.selectedSession.activity.loading": "이 작업의 최신 진행 상황을 동기화하는 중...", + "agentChat.teamWorkspace.selectedSession.activity.feedHeading": "진행 기록", + "agentChat.teamWorkspace.selectedSession.activity.entryCount": "{{formattedCount}}개", + "agentChat.teamWorkspace.activityPreview.line": "{{title}}: {{detail}}", + "agentChat.teamWorkspace.activityPreview.status.error": "오류", + "agentChat.teamWorkspace.activityPreview.status.warning": "경고", + "agentChat.teamWorkspace.activityPreview.status.inProgress": "진행 중", + "agentChat.teamWorkspace.activityPreview.status.failed": "실패", + "agentChat.teamWorkspace.activityPreview.status.completed": "완료", + "agentChat.teamWorkspace.activityPreview.status.message": "메시지", + "agentChat.teamWorkspace.activityPreview.messageTitle.error": "오류", + "agentChat.teamWorkspace.activityPreview.messageTitle.output": "출력", + "agentChat.teamWorkspace.activityPreview.messageTitle.reply": "답변", + "agentChat.teamWorkspace.activityPreview.error.syncFailed": "최근 진행 상황 동기화 실패", + "agentChat.teamWorkspace.activityPreview.error.historyReadFailed": "기록 본문 읽기 실패", + "agentChat.teamWorkspace.liveRuntime.title.statusChanged": "상태 전환", + "agentChat.teamWorkspace.liveRuntime.detail.statusChanged": "작업 상태 이벤트를 받아 {{status}} 상태로 전환했습니다.", + "agentChat.teamWorkspace.liveRuntime.title.textDraft": "콘텐츠 생성 중", + "agentChat.teamWorkspace.liveRuntime.title.thinkingDraft": "생각 정리 중", + "agentChat.teamWorkspace.liveRuntime.title.currentProgress": "현재 진행 상황", + "agentChat.teamWorkspace.liveRuntime.title.processing": "처리 중", + "agentChat.teamWorkspace.liveRuntime.title.tool": "{{status}} · {{toolName}}", + "agentChat.teamWorkspace.liveRuntime.title.toolTargetFallback": "현재 단계", + "agentChat.teamWorkspace.liveRuntime.detail.toolCompletedFallback": "현재 단계가 완료되었습니다.", + "agentChat.teamWorkspace.liveRuntime.detail.toolProcessing": "{{target}} 처리 중입니다.", + "agentChat.teamWorkspace.liveRuntime.title.warning": "경고", + "agentChat.teamWorkspace.liveRuntime.title.error": "오류", + "agentChat.teamWorkspace.liveRuntime.status.processing": "처리 중", + "agentChat.teamWorkspace.liveRuntime.status.queued": "대기 중", + "agentChat.teamWorkspace.liveRuntime.status.completed": "완료", + "agentChat.teamWorkspace.liveRuntime.status.retry": "재시도 필요", + "agentChat.teamWorkspace.liveRuntime.status.warning": "경고", + "agentChat.teamWorkspace.liveRuntime.lifecycle.queueAdded.title": "대기 중", + "agentChat.teamWorkspace.liveRuntime.lifecycle.queueAdded.detail": "새 지시를 받았습니다. 이 하위 작업은 이전 항목이 완료된 뒤 계속 처리됩니다.", + "agentChat.teamWorkspace.liveRuntime.lifecycle.queueStarted.title": "처리 시작", + "agentChat.teamWorkspace.liveRuntime.lifecycle.queueStarted.detail": "이 하위 작업이 현재 작업 처리를 시작했습니다.", + "agentChat.teamWorkspace.liveRuntime.lifecycle.turnStarted.title": "계속 처리", + "agentChat.teamWorkspace.liveRuntime.lifecycle.turnStarted.detail": "이 하위 작업이 현재 내용을 진행 중입니다.", + "agentChat.teamWorkspace.liveRuntime.lifecycle.turnCompleted.title": "단계 완료", + "agentChat.teamWorkspace.liveRuntime.lifecycle.turnCompleted.detail": "이 단계가 완료되어 최신 결과를 동기화하고 있습니다.", + "agentChat.teamWorkspace.liveRuntime.lifecycle.turnFailed.title": "처리 실패", + "agentChat.teamWorkspace.liveRuntime.lifecycle.turnFailed.detail": "이 단계 처리에 실패했습니다. 오류 상세 정보를 확인하세요.", + "agentChat.teamWorkspace.operations.heading": "작업 진행 상황", + "agentChat.teamWorkspace.operations.recentCount": "최근 {{formattedCount}}개", + "agentChat.teamWorkspace.operations.updatedNow": "방금", + "agentChat.teamWorkspace.operations.taskFallback": "이 작업", + "agentChat.teamWorkspace.operations.title.waitTimedOut": "대기 시간 초과", + "agentChat.teamWorkspace.operations.title.waitResolved": "결과 수신", + "agentChat.teamWorkspace.operations.title.resume": "처리 재개", + "agentChat.teamWorkspace.operations.title.closeCompleted": "완료 항목 접기", + "agentChat.teamWorkspace.operations.title.close": "처리 일시 중지", + "agentChat.teamWorkspace.operations.wait.timedOut": "방금 결과 대기가 시간 초과되었습니다. 아직 {{formattedCount}}개 작업이 처리 중입니다.", + "agentChat.teamWorkspace.operations.wait.resolved": "방금 {{sessionName}}에서 새 결과를 받았습니다. 현재 상태는 {{status}}입니다.", + "agentChat.teamWorkspace.operations.control.resumeCount": "방금 {{formattedCount}}개 작업 처리를 재개했습니다.", + "agentChat.teamWorkspace.operations.control.resumeOne": "방금 {{sessionName}} 처리를 재개했습니다.", + "agentChat.teamWorkspace.operations.control.closeCompleted": "방금 완료된 작업 {{formattedCount}}개를 접었습니다.", + "agentChat.teamWorkspace.operations.control.closeCount": "방금 {{formattedCount}}개 작업 처리를 일시 중지했습니다.", + "agentChat.teamWorkspace.operations.control.closeOne": "방금 {{sessionName}} 처리를 일시 중지했습니다.", + "agentChat.teamWorkspace.canvasLane.updatedNow": "방금", + "agentChat.teamWorkspace.canvasLane.runtimeDetail.waitingCount": "대기 중 {{formattedCount}}", + "agentChat.teamWorkspace.canvasLane.runtimeDetail.recentProgress": "최근 진행 {{status}}", + "agentChat.teamWorkspace.canvasLane.runtimeDetail.processingCount": "처리 중 {{activeCount}}/{{totalCount}}", + "agentChat.teamWorkspace.canvasLane.runtimeDetail.stableProcessing": "안정 모드", + "agentChat.teamWorkspace.canvasLane.empty.syncFailed": "최신 내용을 동기화하지 못했습니다", + "agentChat.teamWorkspace.canvasLane.empty.syncLoading": "이 작업의 최신 내용을 동기화하는 중...", + "agentChat.teamWorkspace.canvasLane.empty.queued": "이 작업은 배정되었고 곧 처리를 시작합니다.", + "agentChat.teamWorkspace.canvasLane.empty.running": "이 작업은 처리 중이며 최신 진행 상황이 여기에서 계속 갱신됩니다.", + "agentChat.teamWorkspace.canvasLane.empty.completed": "이 부분은 완료되었고 결과가 현재 내용에 계속 반영됩니다.", + "agentChat.teamWorkspace.canvasLane.empty.failed": "이 단계가 정상적으로 완료되지 않았습니다. 아래 세부 정보를 확인하고 계속할지 결정할 수 있습니다.", + "agentChat.teamWorkspace.canvasLane.empty.default": "이 작업에는 아직 표시할 출력이 없습니다.", + "agentChat.teamWorkspace.canvasLane.session.summaryEmpty": "아직 작업 요약이 없습니다. 세부 정보를 열면 전체 맥락을 확인할 수 있습니다.", + "agentChat.teamWorkspace.canvasLane.memberHint.spawning": "이 작업에 연결하는 중", + "agentChat.teamWorkspace.canvasLane.memberHint.running": "이 작업은 처리 중입니다", + "agentChat.teamWorkspace.canvasLane.memberHint.waiting": "추가 설명을 기다리는 중", + "agentChat.teamWorkspace.canvasLane.memberHint.completed": "이 단계는 완료되었습니다", + "agentChat.teamWorkspace.canvasLane.memberHint.failed": "이 단계는 재시도가 필요합니다", + "agentChat.teamWorkspace.canvasLane.memberHint.connected": "실제 작업에 연결됨", + "agentChat.teamWorkspace.canvasLane.memberHint.waitingAssignment": "작업 인계를 기다리는 중", + "agentChat.teamWorkspace.canvasLane.planned.badge": "시작 전", + "agentChat.teamWorkspace.canvasLane.planned.statusHint": "시스템이 이 작업을 분리하기를 기다리는 중", + "agentChat.teamWorkspace.canvasLane.runtime.updatedAtWaiting": "작업 인계를 기다리는 중", + "agentChat.teamWorkspace.canvasLane.planned.updatedAt": "계획된 분담", + "agentChat.teamWorkspace.runtimeStatus.idle": "시작 전", + "agentChat.teamWorkspace.runtimeStatus.queued": "대기 중", + "agentChat.teamWorkspace.runtimeStatus.running": "처리 중", + "agentChat.teamWorkspace.runtimeStatus.completed": "완료됨", + "agentChat.teamWorkspace.runtimeStatus.failed": "재시도 필요", + "agentChat.teamWorkspace.runtimeStatus.aborted": "일시 중지됨", + "agentChat.teamWorkspace.runtimeStatus.closed": "중지됨", + "agentChat.teamWorkspace.control.close.cascadeStopped": "{{formattedCount}}개 작업을 연쇄 중지했습니다", + "agentChat.teamWorkspace.control.close.stoppedOne": "이 작업이 중지되었습니다", + "agentChat.teamWorkspace.control.close.noChange": "현재 작업 상태는 {{status}}이며 새로운 중지 변경은 없습니다", + "agentChat.teamWorkspace.control.close.failedFallback": "작업 중지 실패", + "agentChat.teamWorkspace.control.resume.cascadeResumed": "{{formattedCount}}개 작업을 연쇄 재개했습니다", + "agentChat.teamWorkspace.control.resume.resumedOne": "이 작업이 재개되었습니다", + "agentChat.teamWorkspace.control.resume.noChange": "현재 작업 상태는 {{status}}이며 새로운 재개 변경은 없습니다", + "agentChat.teamWorkspace.control.resume.failedFallback": "작업 재개 실패", + "agentChat.teamWorkspace.control.wait.timeoutOne": "대기 시간이 초과되었습니다. 이 작업은 아직 최종 상태가 아닙니다", + "agentChat.teamWorkspace.control.wait.enteredStatusOne": "이 작업이 {{status}} 상태가 되었습니다", + "agentChat.teamWorkspace.control.wait.noActiveError": "기다릴 수 있는 활성 작업이 없습니다", + "agentChat.teamWorkspace.control.wait.timeoutTeam": "대기 시간이 초과되었습니다. 팀 내 활성 작업은 아직 최종 상태가 아닙니다", + "agentChat.teamWorkspace.control.wait.enteredStatusTeam": "팀 작업이 {{status}} 상태가 되었습니다", + "agentChat.teamWorkspace.control.wait.failedFallback": "작업 대기 실패", + "agentChat.teamWorkspace.control.closeCompleted.noCompletedError": "닫을 수 있는 완료된 작업이 없습니다", + "agentChat.teamWorkspace.control.closeCompleted.cascadeClosed": "{{formattedCount}}개 작업을 연쇄 접었습니다", + "agentChat.teamWorkspace.control.closeCompleted.closedCount": "{{formattedCount}}개 완료된 작업을 접었습니다", + "agentChat.teamWorkspace.control.closeCompleted.partialFailedFallback": "일부 완료된 작업을 접지 못했습니다", + "agentChat.teamWorkspace.control.sendInput.emptyError": "이 작업에 보낼 내용을 입력하세요", + "agentChat.teamWorkspace.control.sendInput.sentInterrupt": "현재 실행을 중단하고 새 설명을 보냈습니다", + "agentChat.teamWorkspace.control.sendInput.sent": "이 작업에 보충 설명을 보냈습니다", + "agentChat.teamWorkspace.control.sendInput.failedFallback": "작업 설명 전송 실패", + "agentChat.teamWorkspace.control.stop.pausedCount": "{{formattedCount}}개 작업을 일시 중지했습니다", + "agentChat.teamWorkspace.control.stop.pausedOne": "이 작업을 일시 중지했습니다", + "agentChat.teamWorkspace.control.stop.requestedCount": "{{formattedCount}}개 작업에 일시 중지 요청을 보냈습니다", + "agentChat.teamWorkspace.control.stop.requestedOne": "이 작업에 일시 중지 요청을 보냈습니다", + "agentChat.teamWorkspace.control.stop.failedFallback": "작업 중지 실패", "agentChat.threadReliability.routingEvidence.evidence": "증거", "agentChat.threadReliability.routingEvidence.fallbackSlot": "runtime routing", "agentChat.threadReliability.routingEvidence.firstText": "첫 본문", @@ -383,47 +602,48 @@ "capabilityDraft.panel.title": "능력 초안", "capabilityDraft.registeredPanel.action.auditRecentRun": "최근 실행 감사", "capabilityDraft.registeredPanel.action.auditRunning": "감사 중", - "capabilityDraft.registeredPanel.action.createManagedJobDraft": "Managed Job 초안 만들기", - "capabilityDraft.registeredPanel.action.enableRuntime": "이번 턴에서 활성화", + "capabilityDraft.registeredPanel.action.createManagedJobDraft": "예약 실행 설정", + "capabilityDraft.registeredPanel.action.enableRuntime": "한 번 사용해 보기", "capabilityDraft.registeredPanel.action.enableRuntimeHelp": "session enable metadata만 기록하며 자동화는 만들지 않습니다.", - "capabilityDraft.registeredPanel.action.pauseManagedJob": "Managed Job 일시 중지", + "capabilityDraft.registeredPanel.action.pauseManagedJob": "예약 실행 일시 중지", "capabilityDraft.registeredPanel.action.refresh": "새로고침", - "capabilityDraft.registeredPanel.action.resumeManagedJob": "Managed Job 재개", - "capabilityDraft.registeredPanel.agentEnvelope.action.blocked": "Resolve blocker first", - "capabilityDraft.registeredPanel.agentEnvelope.action.draft": "Convert to Agent draft", - "capabilityDraft.registeredPanel.agentEnvelope.action.manualEnable": "Enable this turn first", - "capabilityDraft.registeredPanel.agentEnvelope.agentCard.pending": "Agent card: derives after evidence-ready; no parallel persistent entity is created.", - "capabilityDraft.registeredPanel.agentEnvelope.agentCard.ready": "Agent card: workspace-local/{{directory}}, derived from the registered Skill, Managed Job, and completion audit.", - "capabilityDraft.registeredPanel.agentEnvelope.blockedReasonFallback": "runtime binding is currently blocked by a gate", - "capabilityDraft.registeredPanel.agentEnvelope.description": "After a successful run, the Skill, permissions, manual rerun, and evidence can be composed into a Workspace Agent envelope.", - "capabilityDraft.registeredPanel.agentEnvelope.discovery.pending": "Discovery: wait for the workspace-local skill registration path before team discovery.", - "capabilityDraft.registeredPanel.agentEnvelope.discovery.ready": "Discovery: workspace members find {{directory}} through registered skill and reuse the same Managed Job / evidence.", + "capabilityDraft.registeredPanel.action.resumeManagedJob": "예약 실행 시작", + "capabilityDraft.registeredPanel.agentEnvelope.action.blocked": "먼저 문제 해결", + "capabilityDraft.registeredPanel.agentEnvelope.action.draft": "프로젝트 도우미로 저장", + "capabilityDraft.registeredPanel.agentEnvelope.action.manualEnable": "먼저 한 번 사용", + "capabilityDraft.registeredPanel.agentEnvelope.agentCard.pending": "프로젝트 도우미: 시험 사용이 성공하면 저장하세요.", + "capabilityDraft.registeredPanel.agentEnvelope.agentCard.ready": "프로젝트 도우미: 저장 준비 완료({{directory}}).", + "capabilityDraft.registeredPanel.agentEnvelope.blockedReasonFallback": "아직 해결해야 할 문제가 있습니다", + "capabilityDraft.registeredPanel.agentEnvelope.description": "시험 사용 결과가 괜찮으면 이 프로젝트의 도우미로 저장할 수 있습니다.", + "capabilityDraft.registeredPanel.agentEnvelope.discovery.pending": "팀 공개: 프로젝트 도우미로 저장한 뒤 공개됩니다.", + "capabilityDraft.registeredPanel.agentEnvelope.discovery.ready": "팀 공개: 현재 프로젝트 구성원이 이 스킬을 사용할 수 있습니다.", "capabilityDraft.registeredPanel.agentEnvelope.evidence.blocking": ", blocked: {{reasons}}", - "capabilityDraft.registeredPanel.agentEnvelope.evidence.completedPack": "Evidence: evidence pack {{packId}} is linked.", - "capabilityDraft.registeredPanel.agentEnvelope.evidence.completedPackFallback": "Evidence: evidence pack is linked.", + "capabilityDraft.registeredPanel.agentEnvelope.evidence.completedPack": "최근 결과: 검사를 통과했습니다({{packId}}).", + "capabilityDraft.registeredPanel.agentEnvelope.evidence.completedPackFallback": "최근 결과: 검사를 통과했습니다.", "capabilityDraft.registeredPanel.agentEnvelope.evidence.completionAudit": "Evidence: completion audit {{decision}}, owner {{successfulOwnerRunCount}}/{{ownerRunCount}}, ToolCall {{toolCallCount}}, artifact {{artifactCount}}{{controlledGetLabel}}{{blockingLabel}}{{suffix}}.", "capabilityDraft.registeredPanel.agentEnvelope.evidence.controlledGet.executed": ", Controlled GET {{executed}}/{{artifacts}} executed", "capabilityDraft.registeredPanel.agentEnvelope.evidence.controlledGet.requiredMissing": ", Controlled GET required 0/0 executed", - "capabilityDraft.registeredPanel.agentEnvelope.evidence.missing": "Evidence: no successful-run evidence yet; enable it in this turn first to get one result.", - "capabilityDraft.registeredPanel.agentEnvelope.evidence.pendingPack": "Evidence: evidence pack {{packId}} is linked, but a completed completion audit is still missing, so it cannot be solidified as an Agent.", - "capabilityDraft.registeredPanel.agentEnvelope.evidence.sourceMetadataOnly": "Evidence: P3E source metadata exists and can trace this session authorization source.", + "capabilityDraft.registeredPanel.agentEnvelope.evidence.missing": "최근 결과: 아직 시험 사용 결과가 없습니다.", + "capabilityDraft.registeredPanel.agentEnvelope.evidence.pendingPack": "최근 결과: 생성되었습니다. 검사 후 프로젝트 도우미로 저장할 수 있습니다.", + "capabilityDraft.registeredPanel.agentEnvelope.evidence.sourceMetadataOnly": "최근 결과: 이번 시험 사용 출처가 기록되었고 검사를 기다립니다.", "capabilityDraft.registeredPanel.agentEnvelope.evidence.suffix.missingControlledGet": "; missing Controlled GET evidence, cannot solidify as Agent", "capabilityDraft.registeredPanel.agentEnvelope.evidence.suffix.notCompleted": "; not completed, cannot solidify as Agent", - "capabilityDraft.registeredPanel.agentEnvelope.memory.pending": "Memory: wait for the first run before recording preferences, methodology, and corrections.", - "capabilityDraft.registeredPanel.agentEnvelope.memory.withReport": "Memory: references verification report {{reportId}} and future run corrections.", - "capabilityDraft.registeredPanel.agentEnvelope.permission.empty": "Permissions: manual confirmation by default; no extra external write permission declared.", + "capabilityDraft.registeredPanel.agentEnvelope.memory.pending": "기록: 첫 시험 사용 후 선호와 수정 내용을 기록합니다.", + "capabilityDraft.registeredPanel.agentEnvelope.memory.withReport": "기록: 검사 결과를 저장했으며 이후 피드백도 누적됩니다.", + "capabilityDraft.registeredPanel.agentEnvelope.permission.empty": "권한: 기본적으로 수동 확인이 필요합니다.", "capabilityDraft.registeredPanel.agentEnvelope.permission.withSummary": "Permissions: {{summary}}.", - "capabilityDraft.registeredPanel.agentEnvelope.runbook": "Runbook: {{name}}", - "capabilityDraft.registeredPanel.agentEnvelope.schedule": "Schedule: first slice only supports a manual rerun draft; no long-running task is created.", - "capabilityDraft.registeredPanel.agentEnvelope.sharing.pending": "Sharing: shown only to the current operator until the audit is complete.", - "capabilityDraft.registeredPanel.agentEnvelope.sharing.ready": "Sharing: shareable inside the current workspace / team; not published to the public Marketplace.", - "capabilityDraft.registeredPanel.agentEnvelope.status.blocked": "Agent draft blocked", - "capabilityDraft.registeredPanel.agentEnvelope.status.evidenceReady": "Evidence ready", - "capabilityDraft.registeredPanel.agentEnvelope.status.manualEnableRequired": "Waiting for successful run", - "capabilityDraft.registeredPanel.agentEnvelope.status.sourceMetadataReady": "Waiting for Completion Audit", - "capabilityDraft.registeredPanel.agentEnvelope.title": "Agent envelope draft", - "capabilityDraft.registeredPanel.agentEnvelope.widget.pending": "Widget: waits for run status, artifacts, blockers, and evidence entry.", - "capabilityDraft.registeredPanel.agentEnvelope.widget.ready": "Widget: shows Managed Job status, recent artifacts, audit decision, and next actions.", + "capabilityDraft.registeredPanel.agentEnvelope.runbook": "사용 방법: {{name}}", + "capabilityDraft.registeredPanel.agentEnvelope.schedule": "예약 실행: 설정되지 않음.", + "capabilityDraft.registeredPanel.agentEnvelope.sharing.pending": "공유: 지금은 본인에게만 표시됩니다.", + "capabilityDraft.registeredPanel.agentEnvelope.sharing.ready": "공유: 현재 프로젝트 안에서 공유할 수 있습니다.", + "capabilityDraft.registeredPanel.agentEnvelope.status.blocked": "처리 필요", + "capabilityDraft.registeredPanel.agentEnvelope.status.evidenceReady": "저장 가능", + "capabilityDraft.registeredPanel.agentEnvelope.status.manualEnableRequired": "시험 사용 대기", + "capabilityDraft.registeredPanel.agentEnvelope.status.sourceMetadataReady": "검사 대기", + "capabilityDraft.registeredPanel.agentEnvelope.title": "프로젝트 도우미", + "capabilityDraft.registeredPanel.agentEnvelope.details": "프로젝트 도우미 세부 정보", + "capabilityDraft.registeredPanel.agentEnvelope.widget.pending": "결과: 시험 사용 후 최근 상태와 다음 단계를 표시합니다.", + "capabilityDraft.registeredPanel.agentEnvelope.widget.ready": "결과: 최근 상태, 산출물, 다음 단계를 표시합니다.", "capabilityDraft.registeredPanel.approval.consumptionGate.title": "Consumption gate", "capabilityDraft.registeredPanel.approval.credentialResolver.title": "Session credential resolver", "capabilityDraft.registeredPanel.approval.description": "Before a real API execution, this approval request artifact must be consumed first; currently only the audit entry is persisted, without saving tokens or sending requests.", @@ -465,10 +685,11 @@ "capabilityDraft.registeredPanel.approval.suffix.required": ":required", "capabilityDraft.registeredPanel.approval.suffix.secret": ":secret", "capabilityDraft.registeredPanel.approval.title": "Session approval request artifact", - "capabilityDraft.registeredPanel.badge": "등록됨", - "capabilityDraft.registeredPanel.card.badge.registered": "등록됨", - "capabilityDraft.registeredPanel.card.binding.blocked": "Binding 차단됨", - "capabilityDraft.registeredPanel.card.binding.candidate": "P3C binding 후보", + "capabilityDraft.registeredPanel.technicalDetails": "기술 세부 정보", + "capabilityDraft.registeredPanel.badge": "저장됨", + "capabilityDraft.registeredPanel.card.badge.registered": "저장됨", + "capabilityDraft.registeredPanel.card.binding.blocked": "처리 필요", + "capabilityDraft.registeredPanel.card.binding.candidate": "시험 사용 가능", "capabilityDraft.registeredPanel.card.descriptionFallback": "현재 Workspace의 로컬 Skill 패키지로 등록되었습니다.", "capabilityDraft.registeredPanel.card.field.directory": "디렉터리: ", "capabilityDraft.registeredPanel.card.field.nextGate": "다음 gate: ", @@ -478,11 +699,11 @@ "capabilityDraft.registeredPanel.card.field.source": "출처: ", "capabilityDraft.registeredPanel.card.field.standard": "표준: ", "capabilityDraft.registeredPanel.card.nextGateFallback": "manual_runtime_enable / Query Loop metadata / tool_runtime authorization trimming", - "capabilityDraft.registeredPanel.description": "검증을 통과해 현재 프로젝트에 기록된 Skill 패키지만 표시합니다. 실행은 여전히 runtime gate를 기다립니다.", - "capabilityDraft.registeredPanel.empty.error": "등록된 기능을 읽지 못했습니다: {{message}}", - "capabilityDraft.registeredPanel.empty.loading": "등록된 기능을 읽는 중...", - "capabilityDraft.registeredPanel.empty.missingProject": "프로젝트를 선택하거나 들어간 뒤 해당 프로젝트에 등록된 generated skill을 볼 수 있습니다.", - "capabilityDraft.registeredPanel.empty.noSkills": "이 프로젝트에는 아직 P3A로 등록된 기능이 없습니다. 초안이 검증과 등록을 통과하면 먼저 여기에 표시됩니다.", + "capabilityDraft.registeredPanel.description": "검사 후 현재 프로젝트에 저장된 스킬입니다. 먼저 사용해 보거나 예약 실행을 설정할 수 있습니다.", + "capabilityDraft.registeredPanel.empty.error": "저장된 스킬을 읽지 못했습니다. 잠시 후 다시 시도하세요.", + "capabilityDraft.registeredPanel.empty.loading": "저장된 스킬을 읽는 중...", + "capabilityDraft.registeredPanel.empty.missingProject": "프로젝트를 선택하거나 열어야 그 프로젝트에 저장된 스킬을 볼 수 있습니다.", + "capabilityDraft.registeredPanel.empty.noSkills": "현재 프로젝트에는 아직 저장된 스킬이 없습니다. “제작 중”에서 먼저 검사하고 저장하세요.", "capabilityDraft.registeredPanel.error.missingAutomationSession": "최근 automation run에 연결된 session이 없어 evidence를 내보낼 수 없습니다.", "capabilityDraft.registeredPanel.evidence.credentialReferenceId": "Credential reference", "capabilityDraft.registeredPanel.evidence.endpointSource": "Endpoint", @@ -516,6 +737,29 @@ "capabilityDraft.registeredPanel.managedJob.status.notCreated": "Managed Job: not created", "capabilityDraft.registeredPanel.managedJob.status.notRun": "Not run yet", "capabilityDraft.registeredPanel.managedJob.status.withValue": "Managed Job: {{state}} · {{lastStatus}}", + "capabilityDraft.registeredPanel.user.permission.defaultReadonly": "기본적으로 정보만 읽음", + "capabilityDraft.registeredPanel.user.permission.draftWrite": "초안 작성 가능", + "capabilityDraft.registeredPanel.user.permission.localCommand": "로컬 명령 실행 가능", + "capabilityDraft.registeredPanel.user.permission.readonly": "정보만 읽음", + "capabilityDraft.registeredPanel.user.schedule.enabled": "켜짐", + "capabilityDraft.registeredPanel.user.schedule.notCreated": "미설정", + "capabilityDraft.registeredPanel.user.schedule.paused": "일시 중지됨", + "capabilityDraft.registeredPanel.user.result.auditBlocked": "최근 결과 처리 필요", + "capabilityDraft.registeredPanel.user.result.auditPassed": "최근 결과 검사 통과", + "capabilityDraft.registeredPanel.user.result.noJob": "예약 실행 없음", + "capabilityDraft.registeredPanel.user.result.noRun": "실행 기록 없음", + "capabilityDraft.registeredPanel.user.result.readyToCheck": "최근 결과를 검사할 수 있음", + "capabilityDraft.registeredPanel.user.result.running": "실행 중", + "capabilityDraft.registeredPanel.user.step.saved": "저장됨", + "capabilityDraft.registeredPanel.user.step.savedValue": "현재 프로젝트에서 사용 가능", + "capabilityDraft.registeredPanel.user.step.permission": "권한", + "capabilityDraft.registeredPanel.user.step.schedule": "예약 실행", + "capabilityDraft.registeredPanel.user.nextStep": "먼저 한 번 사용해 보세요. 결과가 괜찮으면 예약 실행을 설정하거나 프로젝트 도우미로 저장하세요.", + "capabilityDraft.registeredPanel.user.assistant.try": "시험 사용", + "capabilityDraft.registeredPanel.user.assistant.tryReady": "한 번 사용 가능", + "capabilityDraft.registeredPanel.user.assistant.tryBlocked": "아직 사용할 수 없음", + "capabilityDraft.registeredPanel.user.assistant.schedule": "예약 실행", + "capabilityDraft.registeredPanel.user.assistant.result": "최근 결과", "capabilityDraft.registeredPanel.provenance.title": "Registration provenance", "capabilityDraft.registeredPanel.summary.bindingBlockedFallback": "Runtime binding is currently blocked by a gate.", "capabilityDraft.registeredPanel.summary.bindingCandidateFallback": "It is eligible as a later runtime binding candidate, but is still not in the default tool surface.", @@ -525,7 +769,7 @@ "capabilityDraft.registeredPanel.summary.standardIssueCount": "표준 검사에 아직 {{count}}개 문제가 있습니다", "capabilityDraft.registeredPanel.summary.standardPassed": "Agent Skills 표준 통과", "capabilityDraft.registeredPanel.summary.standardPending": "Agent Skills 표준 상태 확인 대기", - "capabilityDraft.registeredPanel.title": "Workspace 등록 기능", + "capabilityDraft.registeredPanel.title": "저장된 스킬", "curatedTask.badge.action.clearAria": "{{title}} 지우기", "curatedTask.badge.action.clearTitle": "현재 결과 템플릿 지우기", "curatedTask.badge.action.edit": "편집", @@ -1233,13 +1477,46 @@ "skills.workspace.group.emptyTitle": "이 그룹에 일치하는 Skill이 없습니다", "skills.workspace.group.selectedSubtitle": "이 그룹의 Skill 하나를 선택해 계속하세요", "skills.workspace.header.refresh": "새로고침", - "skills.workspace.header.subtitle": "Skill을 선택해 창작을 시작하세요", - "skills.workspace.header.title": "Skills", + "skills.workspace.header.subtitle": "스킬을 설치하고 관리한 뒤, 필요할 때 홈 입력창으로 가져와 사용할 수 있습니다.", + "skills.workspace.header.title": "스킬", + "skills.workspace.header.createWithLime": "Lime으로 만들기", + "skills.workspace.header.installSkill": "Skill 설치", + "skills.workspace.autoLoad.disabledToast": "“{{name}}”은 자동 로드되지 않습니다", + "skills.workspace.autoLoad.enabledToast": "“{{name}}” 자동 로드를 허용했습니다", + "skills.workspace.autoLoad.aria": "{{name}} 자동 로드 허용", + "skills.workspace.autoLoad.description": "켜면 Lime이 관련 작업에서 이 스킬을 자동으로 가져올 수 있습니다. 끄면 자동으로 매칭하지 않습니다.", + "skills.workspace.autoLoad.off": "꺼짐", + "skills.workspace.autoLoad.on": "켜짐", + "skills.workspace.autoLoad.label": "자동 로드", + "skills.workspace.store.officialBadge": "공식 추천", + "skills.workspace.scaffold.createFailed": "스킬 생성 실패: {{message}}", + "skills.workspace.import.failed": "스킬 설치 실패: {{message}}", + "skills.workspace.import.success": "스킬을 설치했고 사용자 설치에 추가했습니다.", + "skills.workspace.import.dialogTitle": "SKILL.md가 들어 있는 스킬 폴더 선택", + "skills.workspace.header.installingSkill": "설치 중", + "skills.workspace.hero.title": "업무용 Skill 추천", + "skills.workspace.hero.description": "글쓰기, 생산성, 디자인, 데이터 분석 등 다양한 Skill을 한 번에 설치하세요.", + "skills.workspace.view.store": "Skill 광장", + "skills.workspace.view.builtin": "내장", + "skills.workspace.view.installed": "사용자 설치", + "skills.workspace.filter.all": "전체", + "skills.workspace.sort.hot": "정렬: 인기", + "skills.workspace.store.other": "기타 Skill · {{count}}", + "skills.workspace.builtin.title": "내장 Skill", + "skills.workspace.builtin.subtitle": "Lime에 포함된 스킬입니다. 스위치는 자동 매칭만 제어하며 Lime 사용에는 영향을 주지 않습니다.", + "skills.workspace.builtin.empty": "새로고침하거나 다른 키워드로 찾아보세요.", + "skills.workspace.installed.title": "사용자 설치", + "skills.workspace.installed.subtitle": "설치한 스킬입니다. 자동 로드, 사용, 제거는 각각 별도의 동작입니다.", "skills.workspace.header.viewAll": "전체 보기", - "skills.workspace.installedSkill.defaultPromise": "이 Skill을 다시 활용해야 할 때 사용합니다.", + "skills.workspace.installedSkill.action.use": "사용", + "skills.workspace.installedSkill.action.uninstall": "제거", + "skills.workspace.installedSkill.action.uninstalling": "제거 중", + "skills.workspace.installedSkill.uninstallSuccess": "“{{name}}”을(를) 제거했습니다", + "skills.workspace.installedSkill.uninstallFailed": "제거 실패: {{message}}", + "skills.workspace.installedSkill.defaultPromise": "이 스킬을 다시 사용하고 싶을 때 사용하세요.", "skills.workspace.installedSkill.entryBanner": "Skill “{{name}}”을 가지고 생성으로 돌아왔습니다. 이어서 이번 작업을 진행하면 됩니다.", "skills.workspace.installedSkill.entryBannerWithReplay": "Skill “{{name}}”과 지난 목표를 가지고 생성으로 돌아왔습니다. 이어서 이번 작업을 진행하면 됩니다.", - "skills.workspace.installedSkill.fallbackOutputHint": "이 Skill로 생성으로 이어가기", + "skills.workspace.installedSkill.fallbackOutputHint": "이 스킬을 홈 입력창으로 가져오기", "skills.workspace.installedSkill.fallbackRequiredInputs": "대화에서 목표와 제약을 계속 보충", "skills.workspace.installedSkill.outputPrefix": "결과: ", "skills.workspace.installedSkill.requiredPrefix": "필요: ", @@ -1703,5 +1980,7 @@ "agentChat.threadReliability.view.recommendation.interrupting": "현재 실행을 중지 중입니다. 런타임이 최종 상태를 반영할 때까지 기다리세요", "agentChat.threadReliability.view.recommendation.interrupted": "현재 실행은 런타임에서 중단된 것으로 확인되었습니다", "agentChat.threadReliability.view.recommendation.nextQueuedTurn": "대기 중인 턴 계속 가능: {{title}}", - "agentChat.threadReliability.view.recommendation.retryable": "최근 결과는 재시도를 지원합니다. 재개하거나 새 턴을 시작하세요" + "agentChat.threadReliability.view.recommendation.retryable": "최근 결과는 재시도를 지원합니다. 재개하거나 새 턴을 시작하세요", + "skills.inputBadge.remove": "@{{name}} 제거", + "skills.workspace.view.tabsLabel": "스킬 페이지" } diff --git a/src/i18n/resources/zh-CN/agent.json b/src/i18n/resources/zh-CN/agent.json index 7209733f4..65b0d51e2 100644 --- a/src/i18n/resources/zh-CN/agent.json +++ b/src/i18n/resources/zh-CN/agent.json @@ -146,58 +146,18 @@ "agentChat.harnessVerification.section.failureFocusTitle": "验证失败焦点", "agentChat.harnessVerification.section.recoveredTitle": "已恢复结果", "agentChat.harnessVerification.section.title": "验证结果", - "agentChat.imageWorkbenchPreview.description.cancelled": "任务已经取消,当前不会继续生成新的图片结果。", - "agentChat.imageWorkbenchPreview.description.complete.multiple": "已返回 {{count}} 张{{result}},可在右侧继续查看与使用。", - "agentChat.imageWorkbenchPreview.description.complete.single": "{{result}}已经完成,可在右侧继续查看与使用。", - "agentChat.imageWorkbenchPreview.description.complete.storyboard": "{{label}}已经完成,可在右侧继续查看与使用。", - "agentChat.imageWorkbenchPreview.description.failed.notRetryable": "当前错误需要先调整配置或参数。", - "agentChat.imageWorkbenchPreview.description.failed.retryable": "这次没有拿到可用结果,请调整描述后重试。", - "agentChat.imageWorkbenchPreview.description.partial.multiple": "已返回 {{returned}} / {{expected}} 张{{result}},剩余结果未完成。", - "agentChat.imageWorkbenchPreview.description.partial.single": "{{result}}已同步一部分,可在右侧继续查看。", - "agentChat.imageWorkbenchPreview.description.partial.storyboard": "{{label}}已返回 {{returned}} / {{expected}} 格,可在右侧继续查看。", - "agentChat.imageWorkbenchPreview.description.running.edit": "正在处理修图,完成后会自动替换成真实结果。", - "agentChat.imageWorkbenchPreview.description.running.generate": "正在生成图片,完成后会自动替换成真实结果。", - "agentChat.imageWorkbenchPreview.description.running.storyboard": "{{label}}已创建,{{expected}} 格会逐步回填到同一张任务卡里。", - "agentChat.imageWorkbenchPreview.description.running.variation": "正在处理重绘,完成后会自动替换成真实结果。", - "agentChat.imageWorkbenchPreview.layout.storyboard": "3x3 分镜", - "agentChat.imageWorkbenchPreview.media.alt": "图片任务结果", + "agentChat.imageWorkbenchPreview.media.alt": "图片结果", + "agentChat.imageWorkbenchPreview.media.open": "打开图片结果", "agentChat.imageWorkbenchPreview.media.previewFailed": "预览失败", - "agentChat.imageWorkbenchPreview.meta.imageCount": "{{count}} 张", - "agentChat.imageWorkbenchPreview.meta.imageProgress": "{{current}}/{{expected}} 张", - "agentChat.imageWorkbenchPreview.placeholder.cancelled": "任务已取消", - "agentChat.imageWorkbenchPreview.placeholder.failed": "暂未生成成功", + "agentChat.imageWorkbenchPreview.tool.generation": "Image Generation", + "agentChat.imageWorkbenchPreview.tool.editing": "Image Editing", + "agentChat.imageWorkbenchPreview.tool.redraw": "Image Redraw", + "agentChat.imageWorkbenchPreview.placeholder.cancelled": "已取消", + "agentChat.imageWorkbenchPreview.placeholder.editing": "正在处理图片", + "agentChat.imageWorkbenchPreview.placeholder.failed": "生成失败", + "agentChat.imageWorkbenchPreview.placeholder.generating": "正在生成图片", "agentChat.imageWorkbenchPreview.placeholder.imageUnavailable": "图片暂时无法显示", - "agentChat.imageWorkbenchPreview.placeholder.synced": "结果已同步", - "agentChat.imageWorkbenchPreview.promptFallback": "当前任务未提供提示词。", - "agentChat.imageWorkbenchPreview.result.edit": "修图结果", - "agentChat.imageWorkbenchPreview.result.generate": "图片结果", - "agentChat.imageWorkbenchPreview.result.variation": "重绘结果", - "agentChat.imageWorkbenchPreview.slot.cancelled": "已取消", - "agentChat.imageWorkbenchPreview.slot.failed": "本格失败", - "agentChat.imageWorkbenchPreview.slot.pending": "待生成", - "agentChat.imageWorkbenchPreview.slot.waiting": "等待补齐", - "agentChat.imageWorkbenchPreview.source.editSummary": "当前任务会基于已有图片结果继续完成修图。", - "agentChat.imageWorkbenchPreview.source.footnote": "{{label}}:{{summary}}", - "agentChat.imageWorkbenchPreview.source.ref": "已引用 {{ref}}", - "agentChat.imageWorkbenchPreview.source.referenceCount": "已附加 {{count}} 张参考图。", - "agentChat.imageWorkbenchPreview.source.referenceLabel": "参考图", - "agentChat.imageWorkbenchPreview.source.sourceCount": "已引用 {{count}} 张来源图。", - "agentChat.imageWorkbenchPreview.source.sourceLabel": "来源图", - "agentChat.imageWorkbenchPreview.source.variationSummary": "当前任务会基于参考图继续生成新的重绘结果。", - "agentChat.imageWorkbenchPreview.status.cancelled": "已取消", - "agentChat.imageWorkbenchPreview.status.complete.edit": "已修图", - "agentChat.imageWorkbenchPreview.status.complete.generate": "已生成", - "agentChat.imageWorkbenchPreview.status.complete.variation": "已重绘", - "agentChat.imageWorkbenchPreview.status.failed.edit": "修图失败", - "agentChat.imageWorkbenchPreview.status.failed.generate": "生成失败", - "agentChat.imageWorkbenchPreview.status.failed.variation": "重绘失败", - "agentChat.imageWorkbenchPreview.status.partial": "部分完成", - "agentChat.imageWorkbenchPreview.status.running.edit": "修图中", - "agentChat.imageWorkbenchPreview.status.running.generate": "生成中", - "agentChat.imageWorkbenchPreview.status.running.preparing": "准备中", - "agentChat.imageWorkbenchPreview.status.running.queued": "等待队列", - "agentChat.imageWorkbenchPreview.status.running.variation": "重绘中", - "agentChat.imageWorkbenchPreview.statusDescription": " · {{description}}", + "agentChat.imageWorkbenchPreview.placeholder.redrawing": "正在重绘图片", "agentChat.incidentPanel.empty": "当前未发现活跃 incident", "agentChat.incidentPanel.priorityBadge": "{{severity}}优先级", "agentChat.messageList.firstTokenStatus.cancelled.detail": "当前请求已停止。", @@ -297,11 +257,270 @@ "agentChat.teamWorkspace.canvasToolbar.zoom": "缩放 {{percent}}%", "agentChat.teamWorkspace.canvasToolbar.zoomIn": "放大", "agentChat.teamWorkspace.canvasToolbar.zoomOut": "缩小", + "agentChat.teamWorkspace.canvasStage.badge": "当前进展", + "agentChat.teamWorkspace.canvasStage.shortcutHint": "拖拽调整任务布局 · Space 平移 · A 整理 · F 聚焦", + "agentChat.teamWorkspace.canvasStage.emptyTitle": "暂无当前进展", + "agentChat.teamWorkspace.dock.surfaceTitle": "生成", + "agentChat.teamWorkspace.dock.currentProgress": "当前进展", + "agentChat.teamWorkspace.dock.processingBudget": "{{activeCount}}/{{totalCount}} 处理中", + "agentChat.teamWorkspace.dock.processingCount": "{{formattedCount}} 项处理中", + "agentChat.teamWorkspace.dock.waitingCount": "{{formattedCount}} 项等待中", + "agentChat.teamWorkspace.dock.taskCount": "{{formattedCount}} 项任务", + "agentChat.teamWorkspace.dock.selectedPlan": "当前分工方案:{{label}}", + "agentChat.teamWorkspace.dock.unobstructiveBadge": "不遮挡画布", + "agentChat.teamWorkspace.dock.aria.open": "打开当前进展", + "agentChat.teamWorkspace.dock.aria.collapse": "收起当前进展", + "agentChat.teamWorkspace.dock.aria.expand": "展开当前进展", "agentChat.teamWorkspace.teamActions.closeCompleted": "收起已完成任务", "agentChat.teamWorkspace.teamActions.closing": "收起中...", "agentChat.teamWorkspace.teamActions.waitAny": "等待任一任务结果", "agentChat.teamWorkspace.teamActions.waiting": "等待中...", + "agentChat.teamWorkspace.board.detail.collapse": "收起细节", + "agentChat.teamWorkspace.board.detail.expand": "查看细节", + "agentChat.teamWorkspace.board.canvas.title": "当前进展", + "agentChat.teamWorkspace.board.canvas.subtitle.childRuntime": "并行任务会在各自面板里持续更新进展和结果,主对话只保留必要摘要。", + "agentChat.teamWorkspace.board.canvas.subtitle.runtime": "{{formattedCount}} 条当前进展已接入,当前焦点会优先落在正在处理的分工上。", + "agentChat.teamWorkspace.board.canvas.subtitle.formation.forming": "正在准备当前任务分工,任务拆出后会在这里独立更新进展。", + "agentChat.teamWorkspace.board.canvas.subtitle.formation.formed": "当前任务分工已经就绪,任务拆出后会在各自进展面板里开始处理。", + "agentChat.teamWorkspace.board.canvas.subtitle.formation.failed": "这次任务分工准备失败,暂时无法生成当前进展面板。", + "agentChat.teamWorkspace.board.canvas.subtitle.formation.default": "任务拆出后,这里会展开为独立的当前进展。", + "agentChat.teamWorkspace.boardChrome.headline.parentOverviewFallback": "主助手总览", + "agentChat.teamWorkspace.boardChrome.headline.empty": "需要时会自动拆出任务", + "agentChat.teamWorkspace.boardChrome.headline.runningWithQueued": "任务进行中 · {{runningCount}} 项处理中 / {{queuedCount}} 项稍后开始", + "agentChat.teamWorkspace.boardChrome.headline.runningCount": "任务进行中 · {{formattedCount}} 项处理中", + "agentChat.teamWorkspace.boardChrome.headline.running": "任务进行中", + "agentChat.teamWorkspace.boardChrome.headline.queuedCount": "任务准备中 · {{formattedCount}} 项稍后开始", + "agentChat.teamWorkspace.boardChrome.headline.queued": "任务准备中", + "agentChat.teamWorkspace.boardChrome.headline.completedCount": "{{formattedCount}} 项任务已完成", + "agentChat.teamWorkspace.boardChrome.headline.completed": "任务已完成", + "agentChat.teamWorkspace.boardChrome.headline.retryCount": "{{formattedCount}} 项任务需重试", + "agentChat.teamWorkspace.boardChrome.headline.retry": "任务需重试", + "agentChat.teamWorkspace.boardChrome.headline.connectedCount": "{{formattedCount}} 条当前进展已接入", + "agentChat.teamWorkspace.boardChrome.headline.surfaceTitle": "生成", + "agentChat.teamWorkspace.boardChrome.hint.childWithSiblings": "当前任务正与 {{formattedCount}} 项并行子任务一起推进", + "agentChat.teamWorkspace.boardChrome.hint.childSingle": "当前正在处理这项子任务,结果会回流到主助手。", + "agentChat.teamWorkspace.boardChrome.hint.empty": "系统会在需要时自动拆出任务、安排处理顺序,并把结果回流到当前任务。", + "agentChat.teamWorkspace.boardChrome.hint.runtime": "这里只展示当前有哪些分工在处理、状态如何,以及最近更新到了哪里。", + "agentChat.teamWorkspace.boardChrome.chip.focus": "当前焦点 {{name}}", + "agentChat.teamWorkspace.boardChrome.chip.updatedAt": "更新于 {{updatedAt}}", + "agentChat.teamWorkspace.boardChrome.chip.waitable": "{{formattedCount}} 项处理中", + "agentChat.teamWorkspace.boardChrome.chip.completed": "{{formattedCount}} 项已完成", + "agentChat.teamWorkspace.boardChrome.statusSummary": "{{status}} {{formattedCount}}", + "agentChat.teamWorkspace.overview.selectedSession.waitingTitle": "等待任务接手", + "agentChat.teamWorkspace.overview.selectedSession.focusBadge": "当前焦点", + "agentChat.teamWorkspace.overview.selectedSession.waitingBadge": "等待接手", + "agentChat.teamWorkspace.overview.compact.taskActionsLabel": "任务处理", + "agentChat.teamWorkspace.overview.compact.viewActionsLabel": "视图", + "agentChat.teamWorkspace.overview.taskCount.waitable": "{{formattedCount}} 项任务正在处理中", + "agentChat.teamWorkspace.overview.taskCount.completed": "{{formattedCount}} 项任务已完成", + "agentChat.teamWorkspace.formation.hint.forming": "系统正在准备当前任务的分工,任务拆出后会自动开始处理。", + "agentChat.teamWorkspace.formation.hint.formed": "当前任务的分工已经准备好,任务拆出后会继续接手处理。", + "agentChat.teamWorkspace.formation.hint.failed": "当前任务的分工准备失败,但你仍然可以继续在当前任务里推进。", + "agentChat.teamWorkspace.formation.hint.default": "需要时这里会自动展开成当前进展面板。", + "agentChat.teamWorkspace.formation.emptyDetail.forming": "系统正在根据当前任务准备分工。完成后,这里会先展示分工卡片,再接入真实处理进展。", + "agentChat.teamWorkspace.formation.detail.hint.runtimeWithReference": "查看当前任务分工与参考方案", + "agentChat.teamWorkspace.formation.detail.hint.planOnly": "查看当前分工方案", + "agentChat.teamWorkspace.formation.detail.summary.runtime": "当前摘要", + "agentChat.teamWorkspace.formation.detail.summary.plan": "方案摘要", + "agentChat.teamWorkspace.formation.detail.referenceSummaryDefault": "当前任务分工参考了当前方案里的角色偏好。", + "agentChat.teamWorkspace.formation.detail.roleSection.runtime": "当前任务分工", + "agentChat.teamWorkspace.formation.detail.roleSection.plan": "角色分工", + "agentChat.teamWorkspace.formation.emptyDetail.formed": "当前分工方案已经准备好。画布会先展示当前分工,等任务真正开始处理后,再自动切换为当前进展。", + "agentChat.teamWorkspace.formation.emptyDetail.failed": "当前任务分工准备失败,暂时无法展示当前进展。", + "agentChat.teamWorkspace.formation.emptyDetail.default": "还没有任务接手。系统开始分工后,详情区会切换为进展摘要视图。", + "agentChat.teamWorkspace.formation.notice.forming": "系统正在准备当前任务分工,完成后会先展示分工卡片,后续再切换为独立的当前进展。", + "agentChat.teamWorkspace.formation.notice.formed": "当前分工方案已就绪。任务拆出后,这里会从方案视图过渡到当前进展。", + "agentChat.teamWorkspace.formation.notice.failed": "当前任务分工准备失败,暂时还没有任务接手。", + "agentChat.teamWorkspace.formation.notice.default": "还没有任务接手。系统开始分工后,这里会生成独立的当前进展。", + "agentChat.teamWorkspace.formation.summaryBadge.planLabel": "分工方案 · {{label}}", + "agentChat.teamWorkspace.formation.summaryBadge.planRoleCount": "{{formattedCount}} 个计划分工", + "agentChat.teamWorkspace.formation.summaryBadge.memberCount": "{{formattedCount}} 条当前进展", + "agentChat.teamWorkspace.formation.summaryBadge.referenceLabel": "参考方案 · {{label}}", + "agentChat.teamWorkspace.formation.summaryBadge.taskCount": "{{formattedCount}} 项任务", + "agentChat.teamWorkspace.formation.panel.title": "当前任务分工", + "agentChat.teamWorkspace.formation.panel.description.failed": "当前任务分工准备失败,暂时无法展示更多内容。", + "agentChat.teamWorkspace.formation.panel.description.default": "这里会先展示当前分工方案,任务拆出后再切换成当前进展。", + "agentChat.teamWorkspace.formation.waitingHeadline": "等待任务接手", + "agentChat.teamWorkspace.formation.status.forming.label": "准备中", + "agentChat.teamWorkspace.formation.status.forming.title": "正在准备任务分工", + "agentChat.teamWorkspace.formation.status.formed.label": "已就绪", + "agentChat.teamWorkspace.formation.status.formed.title": "任务分工已准备好", + "agentChat.teamWorkspace.formation.status.failed.label": "准备失败", + "agentChat.teamWorkspace.formation.status.failed.title": "任务分工准备失败", + "agentChat.teamWorkspace.formation.memberStatus.planned": "待分配", + "agentChat.teamWorkspace.formation.memberStatus.spawning": "接入中", + "agentChat.teamWorkspace.formation.memberStatus.running": "正在处理", + "agentChat.teamWorkspace.formation.memberStatus.waiting": "待继续", + "agentChat.teamWorkspace.formation.memberStatus.completed": "已完成", + "agentChat.teamWorkspace.formation.memberStatus.failed": "需重试", + "agentChat.teamWorkspace.selectedSession.header.eyebrow": "当前查看", + "agentChat.teamWorkspace.selectedSession.header.currentTaskBadge": "当前任务", + "agentChat.teamWorkspace.selectedSession.header.action.resumePending": "继续中...", + "agentChat.teamWorkspace.selectedSession.header.action.resume": "继续处理", + "agentChat.teamWorkspace.selectedSession.header.action.closePending": "暂停中...", + "agentChat.teamWorkspace.selectedSession.header.action.close": "暂停处理", + "agentChat.teamWorkspace.selectedSession.header.action.openChild": "切换进展", + "agentChat.teamWorkspace.selectedSession.header.action.open": "打开进展", + "agentChat.teamWorkspace.selectedSession.settings.heading": "任务分工", + "agentChat.teamWorkspace.teamPreset.codeTriageTeam": "代码排障团队", + "agentChat.teamWorkspace.teamPreset.researchTeam": "研究团队", + "agentChat.teamWorkspace.teamPreset.contentCreationTeam": "内容创作团队", + "agentChat.teamWorkspace.selectedSession.detail.setting.preset": "预设 {{label}}", + "agentChat.teamWorkspace.selectedSession.detail.setting.profile": "风格 {{name}}", + "agentChat.teamWorkspace.selectedSession.detail.setting.role": "分工 {{label}}", + "agentChat.teamWorkspace.selectedSession.detail.setting.theme": "主题 {{theme}}", + "agentChat.teamWorkspace.selectedSession.detail.metadata.role": "分工 {{label}}", + "agentChat.teamWorkspace.selectedSession.detail.metadata.provider": "服务 {{name}}", + "agentChat.teamWorkspace.selectedSession.detail.metadata.model": "模型 {{model}}", + "agentChat.teamWorkspace.selectedSession.detail.metadata.originTool": "来源 {{tool}}", + "agentChat.teamWorkspace.selectedSession.detail.metadata.createdFromTurn": "来自之前的任务 {{turnId}}", + "agentChat.teamWorkspace.selectedSession.detail.metadata.parentSession": "来自 {{name}}", + "agentChat.teamWorkspace.selectedSession.detail.sessionType.subAgent": "子任务", + "agentChat.teamWorkspace.selectedSession.detail.sessionType.fork": "补充分支", + "agentChat.teamWorkspace.selectedSession.detail.sessionType.user": "主助手", + "agentChat.teamWorkspace.selectedSession.detail.roleHint.explorer": "分析", + "agentChat.teamWorkspace.selectedSession.detail.roleHint.executor": "执行", + "agentChat.teamWorkspace.selectedSession.detail.roleHint.reviewer": "复核", + "agentChat.teamWorkspace.selectedSession.detail.roleHint.writer": "撰写", + "agentChat.teamWorkspace.selectedSession.detail.roleHint.planner": "规划", + "agentChat.teamWorkspace.selectedSession.detail.roleHint.researcher": "研究", + "agentChat.teamWorkspace.selectedSession.detail.roleHint.designer": "设计", + "agentChat.teamWorkspace.selectedSession.detail.roleHint.design": "设计", + "agentChat.teamWorkspace.selectedSession.detail.roleHint.editor": "润色", + "agentChat.teamWorkspace.selectedSession.detail.roleHint.orchestrator": "主助手", + "agentChat.teamWorkspace.selectedSession.footer.updatedAt": "更新于 {{updatedAt}}", + "agentChat.teamWorkspace.selectedSession.collaboration.heading": "继续处理", + "agentChat.teamWorkspace.selectedSession.collaboration.waitReady": "可直接查看结果", + "agentChat.teamWorkspace.selectedSession.collaboration.waitPending": "等待中...", + "agentChat.teamWorkspace.selectedSession.collaboration.wait": "等待结果 30 秒", + "agentChat.teamWorkspace.selectedSession.collaboration.waitHint": "仅在当前内容确实依赖这项任务结果时使用。", + "agentChat.teamWorkspace.selectedSession.collaboration.inputPlaceholder": "给这项任务补充说明、补充约束,或请它继续推进下一步。", + "agentChat.teamWorkspace.selectedSession.collaboration.sendPending": "发送中...", + "agentChat.teamWorkspace.selectedSession.collaboration.send": "发送说明", + "agentChat.teamWorkspace.selectedSession.collaboration.interruptPending": "中断中...", + "agentChat.teamWorkspace.selectedSession.collaboration.interrupt": "立即插入说明", + "agentChat.teamWorkspace.selectedSession.collaboration.inputHint": "这条说明只会发送给当前任务,不影响其他并行任务。", + "agentChat.teamWorkspace.selectedSession.activity.heading": "完整进展", + "agentChat.teamWorkspace.selectedSession.activity.autoRefresh": "处理中自动刷新", + "agentChat.teamWorkspace.selectedSession.activity.errorPrefix": "最新进展暂不可用:", + "agentChat.teamWorkspace.selectedSession.activity.errorFallback": "同步失败", + "agentChat.teamWorkspace.selectedSession.activity.emptyReady": "这项任务暂时还没有可展示的新进展。", + "agentChat.teamWorkspace.selectedSession.activity.loading": "正在同步这项任务的最新进展...", + "agentChat.teamWorkspace.selectedSession.activity.feedHeading": "进展记录", + "agentChat.teamWorkspace.selectedSession.activity.entryCount": "{{formattedCount}} 条", + "agentChat.teamWorkspace.activityPreview.line": "{{title}}:{{detail}}", + "agentChat.teamWorkspace.activityPreview.status.error": "错误", + "agentChat.teamWorkspace.activityPreview.status.warning": "警告", + "agentChat.teamWorkspace.activityPreview.status.inProgress": "进行中", + "agentChat.teamWorkspace.activityPreview.status.failed": "失败", + "agentChat.teamWorkspace.activityPreview.status.completed": "完成", + "agentChat.teamWorkspace.activityPreview.status.message": "消息", + "agentChat.teamWorkspace.activityPreview.messageTitle.error": "错误", + "agentChat.teamWorkspace.activityPreview.messageTitle.output": "输出", + "agentChat.teamWorkspace.activityPreview.messageTitle.reply": "回复", + "agentChat.teamWorkspace.activityPreview.error.syncFailed": "同步最近过程失败", + "agentChat.teamWorkspace.activityPreview.error.historyReadFailed": "读取历史正文失败", + "agentChat.teamWorkspace.liveRuntime.title.statusChanged": "状态切换", + "agentChat.teamWorkspace.liveRuntime.detail.statusChanged": "收到任务状态事件,已切换为{{status}}。", + "agentChat.teamWorkspace.liveRuntime.title.textDraft": "内容生成中", + "agentChat.teamWorkspace.liveRuntime.title.thinkingDraft": "整理思路中", + "agentChat.teamWorkspace.liveRuntime.title.currentProgress": "当前进展", + "agentChat.teamWorkspace.liveRuntime.title.processing": "处理中", + "agentChat.teamWorkspace.liveRuntime.title.tool": "{{status}} · {{toolName}}", + "agentChat.teamWorkspace.liveRuntime.title.toolTargetFallback": "当前步骤", + "agentChat.teamWorkspace.liveRuntime.detail.toolCompletedFallback": "当前步骤已完成。", + "agentChat.teamWorkspace.liveRuntime.detail.toolProcessing": "正在处理 {{target}}。", + "agentChat.teamWorkspace.liveRuntime.title.warning": "警告", + "agentChat.teamWorkspace.liveRuntime.title.error": "错误", + "agentChat.teamWorkspace.liveRuntime.status.processing": "处理中", + "agentChat.teamWorkspace.liveRuntime.status.queued": "稍后开始", + "agentChat.teamWorkspace.liveRuntime.status.completed": "完成", + "agentChat.teamWorkspace.liveRuntime.status.retry": "需重试", + "agentChat.teamWorkspace.liveRuntime.status.warning": "警告", + "agentChat.teamWorkspace.liveRuntime.lifecycle.queueAdded.title": "稍后开始", + "agentChat.teamWorkspace.liveRuntime.lifecycle.queueAdded.detail": "新的说明已经收到,这项子任务会在前一项完成后继续处理。", + "agentChat.teamWorkspace.liveRuntime.lifecycle.queueStarted.title": "开始处理", + "agentChat.teamWorkspace.liveRuntime.lifecycle.queueStarted.detail": "这项子任务已经开始处理当前任务。", + "agentChat.teamWorkspace.liveRuntime.lifecycle.turnStarted.title": "继续处理", + "agentChat.teamWorkspace.liveRuntime.lifecycle.turnStarted.detail": "这项子任务正在推进当前内容。", + "agentChat.teamWorkspace.liveRuntime.lifecycle.turnCompleted.title": "阶段完成", + "agentChat.teamWorkspace.liveRuntime.lifecycle.turnCompleted.detail": "这一步已经完成,正在同步最新结果。", + "agentChat.teamWorkspace.liveRuntime.lifecycle.turnFailed.title": "处理失败", + "agentChat.teamWorkspace.liveRuntime.lifecycle.turnFailed.detail": "这一步处理失败,请查看错误详情。", + "agentChat.teamWorkspace.operations.heading": "任务进展", + "agentChat.teamWorkspace.operations.recentCount": "最近 {{formattedCount}} 条", + "agentChat.teamWorkspace.operations.updatedNow": "刚刚", + "agentChat.teamWorkspace.operations.taskFallback": "这项任务", + "agentChat.teamWorkspace.operations.title.waitTimedOut": "等待超时", + "agentChat.teamWorkspace.operations.title.waitResolved": "收到结果", + "agentChat.teamWorkspace.operations.title.resume": "继续处理", + "agentChat.teamWorkspace.operations.title.closeCompleted": "收起完成项", + "agentChat.teamWorkspace.operations.title.close": "暂停处理", + "agentChat.teamWorkspace.operations.wait.timedOut": "刚才等待结果时超时了,还有 {{formattedCount}} 项任务仍在处理中。", + "agentChat.teamWorkspace.operations.wait.resolved": "刚才等到 {{sessionName}} 返回了新结果,当前状态为{{status}}。", + "agentChat.teamWorkspace.operations.control.resumeCount": "刚才已继续 {{formattedCount}} 项任务的处理。", + "agentChat.teamWorkspace.operations.control.resumeOne": "刚才已继续 {{sessionName}} 的处理。", + "agentChat.teamWorkspace.operations.control.closeCompleted": "刚才已收起 {{formattedCount}} 项已完成任务。", + "agentChat.teamWorkspace.operations.control.closeCount": "刚才已暂停 {{formattedCount}} 项任务的处理。", + "agentChat.teamWorkspace.operations.control.closeOne": "刚才已暂停 {{sessionName}} 的处理。", + "agentChat.teamWorkspace.canvasLane.updatedNow": "刚刚", + "agentChat.teamWorkspace.canvasLane.runtimeDetail.waitingCount": "等待中 {{formattedCount}}", + "agentChat.teamWorkspace.canvasLane.runtimeDetail.recentProgress": "最近进展 {{status}}", + "agentChat.teamWorkspace.canvasLane.runtimeDetail.processingCount": "处理中 {{activeCount}}/{{totalCount}}", + "agentChat.teamWorkspace.canvasLane.runtimeDetail.stableProcessing": "稳妥模式", + "agentChat.teamWorkspace.canvasLane.empty.syncFailed": "同步最新内容失败", + "agentChat.teamWorkspace.canvasLane.empty.syncLoading": "正在同步这项任务的最新内容...", + "agentChat.teamWorkspace.canvasLane.empty.queued": "这项任务已经收到安排,马上开始处理。", + "agentChat.teamWorkspace.canvasLane.empty.running": "这项任务正在处理,最新进展会持续刷新到这里。", + "agentChat.teamWorkspace.canvasLane.empty.completed": "这部分已经完成,结果会继续汇入当前内容。", + "agentChat.teamWorkspace.canvasLane.empty.failed": "这一步没有顺利完成,你可以在下方查看细节并决定是否继续。", + "agentChat.teamWorkspace.canvasLane.empty.default": "这项任务暂时还没有产出可展示的内容。", + "agentChat.teamWorkspace.canvasLane.session.summaryEmpty": "暂时还没有任务摘要,打开详情后可查看完整上下文。", + "agentChat.teamWorkspace.canvasLane.memberHint.spawning": "正在接入这项任务", + "agentChat.teamWorkspace.canvasLane.memberHint.running": "这项任务正在处理", + "agentChat.teamWorkspace.canvasLane.memberHint.waiting": "等待继续补充说明", + "agentChat.teamWorkspace.canvasLane.memberHint.completed": "这一步已经完成", + "agentChat.teamWorkspace.canvasLane.memberHint.failed": "这一步需要重试", + "agentChat.teamWorkspace.canvasLane.memberHint.connected": "已连接到真实任务", + "agentChat.teamWorkspace.canvasLane.memberHint.waitingAssignment": "等待任务接手", + "agentChat.teamWorkspace.canvasLane.planned.badge": "待开始", + "agentChat.teamWorkspace.canvasLane.planned.statusHint": "等待系统把这项任务拆出来", + "agentChat.teamWorkspace.canvasLane.runtime.updatedAtWaiting": "等待任务接手", + "agentChat.teamWorkspace.canvasLane.planned.updatedAt": "计划分工", + "agentChat.teamWorkspace.runtimeStatus.idle": "待开始", + "agentChat.teamWorkspace.runtimeStatus.queued": "稍后开始", + "agentChat.teamWorkspace.runtimeStatus.running": "处理中", "agentChat.teamWorkspace.runtimeStatus.completed": "已完成", + "agentChat.teamWorkspace.runtimeStatus.failed": "需重试", + "agentChat.teamWorkspace.runtimeStatus.aborted": "已暂停", + "agentChat.teamWorkspace.runtimeStatus.closed": "已停止", + "agentChat.teamWorkspace.control.close.cascadeStopped": "已级联停止 {{formattedCount}} 项任务", + "agentChat.teamWorkspace.control.close.stoppedOne": "这项任务已停止", + "agentChat.teamWorkspace.control.close.noChange": "当前任务状态为{{status}},未发生新的停止变更", + "agentChat.teamWorkspace.control.close.failedFallback": "停止任务失败", + "agentChat.teamWorkspace.control.resume.cascadeResumed": "已级联恢复 {{formattedCount}} 项任务", + "agentChat.teamWorkspace.control.resume.resumedOne": "这项任务已恢复", + "agentChat.teamWorkspace.control.resume.noChange": "当前任务状态为{{status}},未发生新的恢复变更", + "agentChat.teamWorkspace.control.resume.failedFallback": "恢复任务失败", + "agentChat.teamWorkspace.control.wait.timeoutOne": "等待超时,这项任务仍未进入最终状态", + "agentChat.teamWorkspace.control.wait.enteredStatusOne": "这项任务已进入{{status}}状态", + "agentChat.teamWorkspace.control.wait.noActiveError": "没有可等待的活跃任务", + "agentChat.teamWorkspace.control.wait.timeoutTeam": "等待超时,团队内活跃任务仍未进入最终状态", + "agentChat.teamWorkspace.control.wait.enteredStatusTeam": "团队任务已进入{{status}}状态", + "agentChat.teamWorkspace.control.wait.failedFallback": "等待任务失败", + "agentChat.teamWorkspace.control.closeCompleted.noCompletedError": "没有可关闭的已完成任务", + "agentChat.teamWorkspace.control.closeCompleted.cascadeClosed": "已级联收起 {{formattedCount}} 项任务", + "agentChat.teamWorkspace.control.closeCompleted.closedCount": "已收起 {{formattedCount}} 项已完成任务", + "agentChat.teamWorkspace.control.closeCompleted.partialFailedFallback": "部分已完成任务收起失败", + "agentChat.teamWorkspace.control.sendInput.emptyError": "请输入要发送给这项任务的内容", + "agentChat.teamWorkspace.control.sendInput.sentInterrupt": "已中断当前执行并发送新说明", + "agentChat.teamWorkspace.control.sendInput.sent": "已向这项任务发送补充说明", + "agentChat.teamWorkspace.control.sendInput.failedFallback": "发送任务说明失败", + "agentChat.teamWorkspace.control.stop.pausedCount": "已暂停 {{formattedCount}} 项任务的处理", + "agentChat.teamWorkspace.control.stop.pausedOne": "已暂停这项任务的处理", + "agentChat.teamWorkspace.control.stop.requestedCount": "已向 {{formattedCount}} 项任务发送暂停请求", + "agentChat.teamWorkspace.control.stop.requestedOne": "已向这项任务发送暂停请求", + "agentChat.teamWorkspace.control.stop.failedFallback": "停止任务失败", "agentChat.threadReliability.routingEvidence.evidence": "证据", "agentChat.threadReliability.routingEvidence.fallbackSlot": "runtime routing", "agentChat.threadReliability.routingEvidence.firstText": "首个正文", @@ -383,47 +602,48 @@ "capabilityDraft.panel.title": "能力草案", "capabilityDraft.registeredPanel.action.auditRecentRun": "审计最近运行", "capabilityDraft.registeredPanel.action.auditRunning": "正在审计", - "capabilityDraft.registeredPanel.action.createManagedJobDraft": "创建 Managed Job 草案", - "capabilityDraft.registeredPanel.action.enableRuntime": "本回合启用", + "capabilityDraft.registeredPanel.action.createManagedJobDraft": "设置定时运行", + "capabilityDraft.registeredPanel.action.enableRuntime": "试用一次", "capabilityDraft.registeredPanel.action.enableRuntimeHelp": "只写入 session enable metadata,不创建自动化。", - "capabilityDraft.registeredPanel.action.pauseManagedJob": "暂停 Managed Job", + "capabilityDraft.registeredPanel.action.pauseManagedJob": "暂停定时运行", "capabilityDraft.registeredPanel.action.refresh": "刷新", - "capabilityDraft.registeredPanel.action.resumeManagedJob": "恢复 Managed Job", - "capabilityDraft.registeredPanel.agentEnvelope.action.blocked": "先解除阻塞", - "capabilityDraft.registeredPanel.agentEnvelope.action.draft": "转成 Agent 草案", - "capabilityDraft.registeredPanel.agentEnvelope.action.manualEnable": "先本回合启用", - "capabilityDraft.registeredPanel.agentEnvelope.agentCard.pending": "Agent card:等待 evidence-ready 后派生,不创建平行持久化实体。", - "capabilityDraft.registeredPanel.agentEnvelope.agentCard.ready": "Agent card:workspace-local/{{directory}},由已注册 Skill、Managed Job 和 completion audit 派生。", - "capabilityDraft.registeredPanel.agentEnvelope.blockedReasonFallback": "runtime binding 当前被 gate 阻断", - "capabilityDraft.registeredPanel.agentEnvelope.description": "成功运行后可把 Skill、权限、手动 rerun 和 evidence 组合成 Workspace Agent envelope。", - "capabilityDraft.registeredPanel.agentEnvelope.discovery.pending": "Discovery:等待 workspace-local skill 注册路径后再进入团队发现。", - "capabilityDraft.registeredPanel.agentEnvelope.discovery.ready": "Discovery:同 workspace 成员通过 registered skill 发现 {{directory}},复用同一 Managed Job / evidence。", + "capabilityDraft.registeredPanel.action.resumeManagedJob": "开启定时运行", + "capabilityDraft.registeredPanel.agentEnvelope.action.blocked": "先处理问题", + "capabilityDraft.registeredPanel.agentEnvelope.action.draft": "保存成项目助手", + "capabilityDraft.registeredPanel.agentEnvelope.action.manualEnable": "先试用一次", + "capabilityDraft.registeredPanel.agentEnvelope.agentCard.pending": "项目助手:试用通过后再保存。", + "capabilityDraft.registeredPanel.agentEnvelope.agentCard.ready": "项目助手:已准备好保存({{directory}})。", + "capabilityDraft.registeredPanel.agentEnvelope.blockedReasonFallback": "当前还有问题需要处理", + "capabilityDraft.registeredPanel.agentEnvelope.description": "试用结果没问题后,可以把它保存成当前项目里的助手。", + "capabilityDraft.registeredPanel.agentEnvelope.discovery.pending": "团队可见性:保存成项目助手后再开放。", + "capabilityDraft.registeredPanel.agentEnvelope.discovery.ready": "团队可见性:当前项目成员可以使用这条技能。", "capabilityDraft.registeredPanel.agentEnvelope.evidence.blocking": ",阻塞:{{reasons}}", - "capabilityDraft.registeredPanel.agentEnvelope.evidence.completedPack": "Evidence:已关联 evidence pack {{packId}}。", - "capabilityDraft.registeredPanel.agentEnvelope.evidence.completedPackFallback": "Evidence:已关联 evidence pack。", + "capabilityDraft.registeredPanel.agentEnvelope.evidence.completedPack": "最近结果:已通过检查({{packId}})。", + "capabilityDraft.registeredPanel.agentEnvelope.evidence.completedPackFallback": "最近结果:已通过检查。", "capabilityDraft.registeredPanel.agentEnvelope.evidence.completionAudit": "Evidence:completion audit {{decision}},owner {{successfulOwnerRunCount}}/{{ownerRunCount}},ToolCall {{toolCallCount}},artifact {{artifactCount}}{{controlledGetLabel}}{{blockingLabel}}{{suffix}}。", "capabilityDraft.registeredPanel.agentEnvelope.evidence.controlledGet.executed": ",受控 GET {{executed}}/{{artifacts}} executed", "capabilityDraft.registeredPanel.agentEnvelope.evidence.controlledGet.requiredMissing": ",受控 GET required 0/0 executed", - "capabilityDraft.registeredPanel.agentEnvelope.evidence.missing": "Evidence:还没有成功运行证据;先通过本回合启用拿到一次结果。", - "capabilityDraft.registeredPanel.agentEnvelope.evidence.pendingPack": "Evidence:已关联 evidence pack {{packId}},但还缺 completed completion audit,不能固化为 Agent。", - "capabilityDraft.registeredPanel.agentEnvelope.evidence.sourceMetadataOnly": "Evidence:已有 P3E source metadata,可追踪本次 session 授权来源。", + "capabilityDraft.registeredPanel.agentEnvelope.evidence.missing": "最近结果:还没有试用结果。", + "capabilityDraft.registeredPanel.agentEnvelope.evidence.pendingPack": "最近结果:已生成,等待检查后才能保存成项目助手。", + "capabilityDraft.registeredPanel.agentEnvelope.evidence.sourceMetadataOnly": "最近结果:已记录这次试用来源,等待检查。", "capabilityDraft.registeredPanel.agentEnvelope.evidence.suffix.missingControlledGet": ";缺受控 GET evidence,不能固化为 Agent", "capabilityDraft.registeredPanel.agentEnvelope.evidence.suffix.notCompleted": ";未 completed,不能固化为 Agent", - "capabilityDraft.registeredPanel.agentEnvelope.memory.pending": "Memory:等待首轮运行后记录用户偏好、方法论和修正历史。", - "capabilityDraft.registeredPanel.agentEnvelope.memory.withReport": "Memory:引用 verification report {{reportId}} 与后续运行修正。", - "capabilityDraft.registeredPanel.agentEnvelope.permission.empty": "权限:默认手动确认,未声明额外外部写权限。", + "capabilityDraft.registeredPanel.agentEnvelope.memory.pending": "记录:等待首次试用后记录偏好和修正。", + "capabilityDraft.registeredPanel.agentEnvelope.memory.withReport": "记录:已保留检查结果,后续会继续积累使用反馈。", + "capabilityDraft.registeredPanel.agentEnvelope.permission.empty": "权限:默认需要手动确认。", "capabilityDraft.registeredPanel.agentEnvelope.permission.withSummary": "权限:{{summary}}。", - "capabilityDraft.registeredPanel.agentEnvelope.runbook": "Runbook:{{name}}", - "capabilityDraft.registeredPanel.agentEnvelope.schedule": "Schedule:第一刀仅支持 manual rerun 草案,不创建长期任务。", - "capabilityDraft.registeredPanel.agentEnvelope.sharing.pending": "Sharing:未完成审计前仅对当前操作者展示草案。", - "capabilityDraft.registeredPanel.agentEnvelope.sharing.ready": "Sharing:可在当前 workspace / team 内共享;不进入 public Marketplace。", - "capabilityDraft.registeredPanel.agentEnvelope.status.blocked": "Agent 草案阻塞", - "capabilityDraft.registeredPanel.agentEnvelope.status.evidenceReady": "Evidence 已就绪", - "capabilityDraft.registeredPanel.agentEnvelope.status.manualEnableRequired": "等待成功运行", - "capabilityDraft.registeredPanel.agentEnvelope.status.sourceMetadataReady": "等待 Completion Audit", - "capabilityDraft.registeredPanel.agentEnvelope.title": "Agent envelope 草案", - "capabilityDraft.registeredPanel.agentEnvelope.widget.pending": "Widget:等待运行后展示状态、产物、阻塞点和 evidence 入口。", - "capabilityDraft.registeredPanel.agentEnvelope.widget.ready": "Widget:展示 Managed Job 状态、最近产物、审计结论和下一步动作。", + "capabilityDraft.registeredPanel.agentEnvelope.runbook": "使用方式:{{name}}", + "capabilityDraft.registeredPanel.agentEnvelope.schedule": "定时运行:尚未设置。", + "capabilityDraft.registeredPanel.agentEnvelope.sharing.pending": "共享:先只对你可见。", + "capabilityDraft.registeredPanel.agentEnvelope.sharing.ready": "共享:可在当前项目内共享。", + "capabilityDraft.registeredPanel.agentEnvelope.status.blocked": "需要处理", + "capabilityDraft.registeredPanel.agentEnvelope.status.evidenceReady": "可保存", + "capabilityDraft.registeredPanel.agentEnvelope.status.manualEnableRequired": "待试用", + "capabilityDraft.registeredPanel.agentEnvelope.status.sourceMetadataReady": "待检查", + "capabilityDraft.registeredPanel.agentEnvelope.title": "项目助手", + "capabilityDraft.registeredPanel.agentEnvelope.details": "项目助手详情", + "capabilityDraft.registeredPanel.agentEnvelope.widget.pending": "结果:试用后展示最近状态和下一步。", + "capabilityDraft.registeredPanel.agentEnvelope.widget.ready": "结果:展示最近状态、产物和下一步动作。", "capabilityDraft.registeredPanel.approval.consumptionGate.title": "消费门禁", "capabilityDraft.registeredPanel.approval.credentialResolver.title": "Session credential resolver", "capabilityDraft.registeredPanel.approval.description": "真实 API 执行前必须先消费这条授权请求 artifact;当前只持久化审计入口,不保存 token,也不发请求。", @@ -465,10 +685,11 @@ "capabilityDraft.registeredPanel.approval.suffix.required": ":required", "capabilityDraft.registeredPanel.approval.suffix.secret": ":secret", "capabilityDraft.registeredPanel.approval.title": "Session approval request artifact", - "capabilityDraft.registeredPanel.badge": "注册区", - "capabilityDraft.registeredPanel.card.badge.registered": "已注册", - "capabilityDraft.registeredPanel.card.binding.blocked": "Binding 阻塞", - "capabilityDraft.registeredPanel.card.binding.candidate": "P3C binding 候选", + "capabilityDraft.registeredPanel.technicalDetails": "技术详情", + "capabilityDraft.registeredPanel.badge": "已保存", + "capabilityDraft.registeredPanel.card.badge.registered": "已保存", + "capabilityDraft.registeredPanel.card.binding.blocked": "需要处理", + "capabilityDraft.registeredPanel.card.binding.candidate": "可试用", "capabilityDraft.registeredPanel.card.descriptionFallback": "已注册为当前 Workspace 的本地 Skill 包。", "capabilityDraft.registeredPanel.card.field.directory": "目录:", "capabilityDraft.registeredPanel.card.field.nextGate": "下一道 gate:", @@ -478,11 +699,11 @@ "capabilityDraft.registeredPanel.card.field.source": "来源:", "capabilityDraft.registeredPanel.card.field.standard": "标准:", "capabilityDraft.registeredPanel.card.nextGateFallback": "manual_runtime_enable / Query Loop metadata / tool_runtime 授权裁剪", - "capabilityDraft.registeredPanel.description": "这里只有已通过验证并写入当前项目的 Skill 包;运行仍要等 runtime gate。", - "capabilityDraft.registeredPanel.empty.error": "已注册能力暂时没读到:{{message}}", - "capabilityDraft.registeredPanel.empty.loading": "正在读取已注册能力...", - "capabilityDraft.registeredPanel.empty.missingProject": "选择或进入一个项目后,才能查看该项目已注册的 generated skill。", - "capabilityDraft.registeredPanel.empty.noSkills": "当前项目还没有通过 P3A 注册的能力。草案通过验证并注册后,会先出现在这里。", + "capabilityDraft.registeredPanel.description": "这里是已经检查并保存到当前项目的技能。可以先试用,也可以设置定时运行。", + "capabilityDraft.registeredPanel.empty.error": "已保存技能暂时没读到,请稍后重试。", + "capabilityDraft.registeredPanel.empty.loading": "正在读取已保存技能...", + "capabilityDraft.registeredPanel.empty.missingProject": "选择或进入一个项目后,才能查看这个项目里保存的技能。", + "capabilityDraft.registeredPanel.empty.noSkills": "当前项目还没有已保存技能。先在“正在制作”里检查并保存一个。", "capabilityDraft.registeredPanel.error.missingAutomationSession": "最近 automation run 没有关联 session,无法导出 evidence。", "capabilityDraft.registeredPanel.evidence.credentialReferenceId": "凭证引用", "capabilityDraft.registeredPanel.evidence.endpointSource": "Endpoint", @@ -516,6 +737,29 @@ "capabilityDraft.registeredPanel.managedJob.status.notCreated": "Managed Job:未创建", "capabilityDraft.registeredPanel.managedJob.status.notRun": "尚未运行", "capabilityDraft.registeredPanel.managedJob.status.withValue": "Managed Job:{{state}} · {{lastStatus}}", + "capabilityDraft.registeredPanel.user.permission.defaultReadonly": "默认只读取信息", + "capabilityDraft.registeredPanel.user.permission.draftWrite": "可写入草案", + "capabilityDraft.registeredPanel.user.permission.localCommand": "可运行本地命令", + "capabilityDraft.registeredPanel.user.permission.readonly": "只读取信息", + "capabilityDraft.registeredPanel.user.schedule.enabled": "已开启", + "capabilityDraft.registeredPanel.user.schedule.notCreated": "未设置", + "capabilityDraft.registeredPanel.user.schedule.paused": "已暂停", + "capabilityDraft.registeredPanel.user.result.auditBlocked": "最近结果需要处理", + "capabilityDraft.registeredPanel.user.result.auditPassed": "最近结果已通过检查", + "capabilityDraft.registeredPanel.user.result.noJob": "还没有定时运行", + "capabilityDraft.registeredPanel.user.result.noRun": "还没有运行记录", + "capabilityDraft.registeredPanel.user.result.readyToCheck": "可以检查最近结果", + "capabilityDraft.registeredPanel.user.result.running": "正在运行", + "capabilityDraft.registeredPanel.user.step.saved": "已保存", + "capabilityDraft.registeredPanel.user.step.savedValue": "可在当前项目使用", + "capabilityDraft.registeredPanel.user.step.permission": "权限", + "capabilityDraft.registeredPanel.user.step.schedule": "定时运行", + "capabilityDraft.registeredPanel.user.nextStep": "建议先试用一次。确认结果没问题后,再设置定时运行或保存成项目助手。", + "capabilityDraft.registeredPanel.user.assistant.try": "试用", + "capabilityDraft.registeredPanel.user.assistant.tryReady": "可以试用一次", + "capabilityDraft.registeredPanel.user.assistant.tryBlocked": "暂不可试用", + "capabilityDraft.registeredPanel.user.assistant.schedule": "定时运行", + "capabilityDraft.registeredPanel.user.assistant.result": "最近结果", "capabilityDraft.registeredPanel.provenance.title": "注册 provenance", "capabilityDraft.registeredPanel.summary.bindingBlockedFallback": "Runtime binding 当前被 gate 阻断。", "capabilityDraft.registeredPanel.summary.bindingCandidateFallback": "已具备后续 runtime binding 候选资格,但当前仍未进入默认工具面。", @@ -525,7 +769,7 @@ "capabilityDraft.registeredPanel.summary.standardIssueCount": "标准检查仍有 {{count}} 个问题", "capabilityDraft.registeredPanel.summary.standardPassed": "Agent Skills 标准通过", "capabilityDraft.registeredPanel.summary.standardPending": "Agent Skills 标准状态待确认", - "capabilityDraft.registeredPanel.title": "Workspace 已注册能力", + "capabilityDraft.registeredPanel.title": "已保存技能", "curatedTask.badge.action.clearAria": "清除 {{title}}", "curatedTask.badge.action.clearTitle": "清除当前结果模板", "curatedTask.badge.action.edit": "编辑", @@ -1233,13 +1477,46 @@ "skills.workspace.group.emptyTitle": "当前分组下暂无匹配 Skill", "skills.workspace.group.selectedSubtitle": "选择这一组里的一个 Skill 继续", "skills.workspace.header.refresh": "刷新", - "skills.workspace.header.subtitle": "选择一个 Skill 开始创作", - "skills.workspace.header.title": "Skills", + "skills.workspace.header.subtitle": "安装和管理技能,需要时带回首页输入框使用。", + "skills.workspace.header.title": "技能", + "skills.workspace.header.createWithLime": "通过 Lime 创建", + "skills.workspace.header.installSkill": "安装技能", + "skills.workspace.autoLoad.disabledToast": "“{{name}}”已改为不自动加载", + "skills.workspace.autoLoad.enabledToast": "已允许“{{name}}”自动加载", + "skills.workspace.autoLoad.aria": "允许自动加载 {{name}}", + "skills.workspace.autoLoad.description": "开启后,Lime 会在相关任务里自动带上;关闭后,不会主动匹配。", + "skills.workspace.autoLoad.off": "已关闭", + "skills.workspace.autoLoad.on": "已开启", + "skills.workspace.autoLoad.label": "自动加载", + "skills.workspace.store.officialBadge": "官方精选", + "skills.workspace.scaffold.createFailed": "创建技能失败:{{message}}", + "skills.workspace.import.failed": "安装技能失败:{{message}}", + "skills.workspace.import.success": "技能已安装,已收进用户安装。", + "skills.workspace.import.dialogTitle": "选择包含 SKILL.md 的技能文件夹", + "skills.workspace.header.installingSkill": "安装中", + "skills.workspace.hero.title": "为你精选的职场技能", + "skills.workspace.hero.description": "覆盖写作、效率、设计、数据分析等多种场景,一键安装。", + "skills.workspace.view.store": "技能广场", + "skills.workspace.view.builtin": "内置", + "skills.workspace.view.installed": "用户安装", + "skills.workspace.filter.all": "全部", + "skills.workspace.sort.hot": "排序:热门", + "skills.workspace.store.other": "其他技能 · {{count}}", + "skills.workspace.builtin.title": "内置技能", + "skills.workspace.builtin.subtitle": "Lime 自带的技能。开关只控制是否自动匹配,不影响你继续使用 Lime。", + "skills.workspace.builtin.empty": "可以尝试刷新,或换个关键词继续找。", + "skills.workspace.installed.title": "用户安装", + "skills.workspace.installed.subtitle": "你安装的技能。自动加载、手动使用、卸载是三个独立操作。", "skills.workspace.header.viewAll": "查看全部", - "skills.workspace.installedSkill.defaultPromise": "当你需要复用这个 Skill 时使用。", + "skills.workspace.installedSkill.action.use": "使用", + "skills.workspace.installedSkill.action.uninstall": "卸载", + "skills.workspace.installedSkill.action.uninstalling": "卸载中", + "skills.workspace.installedSkill.uninstallSuccess": "已卸载“{{name}}”", + "skills.workspace.installedSkill.uninstallFailed": "卸载失败:{{message}}", + "skills.workspace.installedSkill.defaultPromise": "当你需要复用这个技能时使用。", "skills.workspace.installedSkill.entryBanner": "已带着 Skill「{{name}}」回到生成,接着把这轮做下去就行。", "skills.workspace.installedSkill.entryBannerWithReplay": "已带着 Skill「{{name}}」和上次目标回到生成,接着把这轮做下去就行。", - "skills.workspace.installedSkill.fallbackOutputHint": "带着该 Skill 进入生成", + "skills.workspace.installedSkill.fallbackOutputHint": "带着这个技能回到首页输入框", "skills.workspace.installedSkill.fallbackRequiredInputs": "对话里继续补充目标与约束", "skills.workspace.installedSkill.outputPrefix": "交付:", "skills.workspace.installedSkill.requiredPrefix": "需要:", @@ -1703,5 +1980,7 @@ "agentChat.threadReliability.view.recommendation.interrupting": "正在停止当前执行,请等待运行时回填最终状态", "agentChat.threadReliability.view.recommendation.interrupted": "当前执行已被运行时确认中断", "agentChat.threadReliability.view.recommendation.nextQueuedTurn": "可继续排队回合:{{title}}", - "agentChat.threadReliability.view.recommendation.retryable": "最近结果支持重试,可恢复或重新发起新回合" + "agentChat.threadReliability.view.recommendation.retryable": "最近结果支持重试,可恢复或重新发起新回合", + "skills.inputBadge.remove": "移除 @{{name}}", + "skills.workspace.view.tabsLabel": "技能分页" } diff --git a/src/i18n/resources/zh-TW/agent.json b/src/i18n/resources/zh-TW/agent.json index d43f0f69d..d3ba6e914 100644 --- a/src/i18n/resources/zh-TW/agent.json +++ b/src/i18n/resources/zh-TW/agent.json @@ -146,58 +146,18 @@ "agentChat.harnessVerification.section.failureFocusTitle": "驗證失敗焦點", "agentChat.harnessVerification.section.recoveredTitle": "已恢復結果", "agentChat.harnessVerification.section.title": "驗證結果", - "agentChat.imageWorkbenchPreview.description.cancelled": "任務已取消,目前不會繼續生成新的圖片結果。", - "agentChat.imageWorkbenchPreview.description.complete.multiple": "已返回 {{count}} 張{{result}},可在右側繼續查看與使用。", - "agentChat.imageWorkbenchPreview.description.complete.single": "{{result}}已完成,可在右側繼續查看與使用。", - "agentChat.imageWorkbenchPreview.description.complete.storyboard": "{{label}}已完成,可在右側繼續查看與使用。", - "agentChat.imageWorkbenchPreview.description.failed.notRetryable": "目前錯誤需要先調整設定或參數。", - "agentChat.imageWorkbenchPreview.description.failed.retryable": "這次沒有取得可用結果,請調整描述後重試。", - "agentChat.imageWorkbenchPreview.description.partial.multiple": "已返回 {{returned}} / {{expected}} 張{{result}},剩餘結果未完成。", - "agentChat.imageWorkbenchPreview.description.partial.single": "{{result}}已同步一部分,可在右側繼續查看。", - "agentChat.imageWorkbenchPreview.description.partial.storyboard": "{{label}}已返回 {{returned}} / {{expected}} 格,可在右側繼續查看。", - "agentChat.imageWorkbenchPreview.description.running.edit": "正在處理修圖,完成後會自動替換成真實結果。", - "agentChat.imageWorkbenchPreview.description.running.generate": "正在生成圖片,完成後會自動替換成真實結果。", - "agentChat.imageWorkbenchPreview.description.running.storyboard": "{{label}}已建立,{{expected}} 格會逐步回填到同一張任務卡裡。", - "agentChat.imageWorkbenchPreview.description.running.variation": "正在處理重繪,完成後會自動替換成真實結果。", - "agentChat.imageWorkbenchPreview.layout.storyboard": "3x3 分鏡", - "agentChat.imageWorkbenchPreview.media.alt": "圖片任務結果", + "agentChat.imageWorkbenchPreview.media.alt": "圖片結果", + "agentChat.imageWorkbenchPreview.media.open": "開啟圖片結果", "agentChat.imageWorkbenchPreview.media.previewFailed": "預覽失敗", - "agentChat.imageWorkbenchPreview.meta.imageCount": "{{count}} 張", - "agentChat.imageWorkbenchPreview.meta.imageProgress": "{{current}}/{{expected}} 張", - "agentChat.imageWorkbenchPreview.placeholder.cancelled": "任務已取消", - "agentChat.imageWorkbenchPreview.placeholder.failed": "暫未生成成功", + "agentChat.imageWorkbenchPreview.tool.generation": "Image Generation", + "agentChat.imageWorkbenchPreview.tool.editing": "Image Editing", + "agentChat.imageWorkbenchPreview.tool.redraw": "Image Redraw", + "agentChat.imageWorkbenchPreview.placeholder.cancelled": "已取消", + "agentChat.imageWorkbenchPreview.placeholder.editing": "正在處理圖片", + "agentChat.imageWorkbenchPreview.placeholder.failed": "生成失敗", + "agentChat.imageWorkbenchPreview.placeholder.generating": "正在生成圖片", "agentChat.imageWorkbenchPreview.placeholder.imageUnavailable": "圖片暫時無法顯示", - "agentChat.imageWorkbenchPreview.placeholder.synced": "結果已同步", - "agentChat.imageWorkbenchPreview.promptFallback": "目前任務未提供提示詞。", - "agentChat.imageWorkbenchPreview.result.edit": "修圖結果", - "agentChat.imageWorkbenchPreview.result.generate": "圖片結果", - "agentChat.imageWorkbenchPreview.result.variation": "重繪結果", - "agentChat.imageWorkbenchPreview.slot.cancelled": "已取消", - "agentChat.imageWorkbenchPreview.slot.failed": "本格失敗", - "agentChat.imageWorkbenchPreview.slot.pending": "待生成", - "agentChat.imageWorkbenchPreview.slot.waiting": "等待補齊", - "agentChat.imageWorkbenchPreview.source.editSummary": "目前任務會基於既有圖片結果繼續完成修圖。", - "agentChat.imageWorkbenchPreview.source.footnote": "{{label}}:{{summary}}", - "agentChat.imageWorkbenchPreview.source.ref": "已引用 {{ref}}", - "agentChat.imageWorkbenchPreview.source.referenceCount": "已附加 {{count}} 張參考圖。", - "agentChat.imageWorkbenchPreview.source.referenceLabel": "參考圖", - "agentChat.imageWorkbenchPreview.source.sourceCount": "已引用 {{count}} 張來源圖。", - "agentChat.imageWorkbenchPreview.source.sourceLabel": "來源圖", - "agentChat.imageWorkbenchPreview.source.variationSummary": "目前任務會基於參考圖繼續生成新的重繪結果。", - "agentChat.imageWorkbenchPreview.status.cancelled": "已取消", - "agentChat.imageWorkbenchPreview.status.complete.edit": "已修圖", - "agentChat.imageWorkbenchPreview.status.complete.generate": "已生成", - "agentChat.imageWorkbenchPreview.status.complete.variation": "已重繪", - "agentChat.imageWorkbenchPreview.status.failed.edit": "修圖失敗", - "agentChat.imageWorkbenchPreview.status.failed.generate": "生成失敗", - "agentChat.imageWorkbenchPreview.status.failed.variation": "重繪失敗", - "agentChat.imageWorkbenchPreview.status.partial": "部分完成", - "agentChat.imageWorkbenchPreview.status.running.edit": "修圖中", - "agentChat.imageWorkbenchPreview.status.running.generate": "生成中", - "agentChat.imageWorkbenchPreview.status.running.preparing": "準備中", - "agentChat.imageWorkbenchPreview.status.running.queued": "等待佇列", - "agentChat.imageWorkbenchPreview.status.running.variation": "重繪中", - "agentChat.imageWorkbenchPreview.statusDescription": " · {{description}}", + "agentChat.imageWorkbenchPreview.placeholder.redrawing": "正在重繪圖片", "agentChat.incidentPanel.empty": "目前未發現活躍 incident", "agentChat.incidentPanel.priorityBadge": "{{severity}}優先級", "agentChat.messageList.firstTokenStatus.cancelled.detail": "目前請求已停止。", @@ -297,11 +257,270 @@ "agentChat.teamWorkspace.canvasToolbar.zoom": "縮放 {{percent}}%", "agentChat.teamWorkspace.canvasToolbar.zoomIn": "放大", "agentChat.teamWorkspace.canvasToolbar.zoomOut": "縮小", + "agentChat.teamWorkspace.canvasStage.badge": "目前進展", + "agentChat.teamWorkspace.canvasStage.shortcutHint": "拖曳調整任務布局 · Space 平移 · A 整理 · F 聚焦", + "agentChat.teamWorkspace.canvasStage.emptyTitle": "暫無目前進展", + "agentChat.teamWorkspace.dock.surfaceTitle": "生成", + "agentChat.teamWorkspace.dock.currentProgress": "目前進展", + "agentChat.teamWorkspace.dock.processingBudget": "{{activeCount}}/{{totalCount}} 處理中", + "agentChat.teamWorkspace.dock.processingCount": "{{formattedCount}} 項處理中", + "agentChat.teamWorkspace.dock.waitingCount": "{{formattedCount}} 項等待中", + "agentChat.teamWorkspace.dock.taskCount": "{{formattedCount}} 項任務", + "agentChat.teamWorkspace.dock.selectedPlan": "目前分工方案:{{label}}", + "agentChat.teamWorkspace.dock.unobstructiveBadge": "不遮擋畫布", + "agentChat.teamWorkspace.dock.aria.open": "打開目前進展", + "agentChat.teamWorkspace.dock.aria.collapse": "收起目前進展", + "agentChat.teamWorkspace.dock.aria.expand": "展開目前進展", "agentChat.teamWorkspace.teamActions.closeCompleted": "收起已完成任務", "agentChat.teamWorkspace.teamActions.closing": "收起中...", "agentChat.teamWorkspace.teamActions.waitAny": "等待任一任務結果", "agentChat.teamWorkspace.teamActions.waiting": "等待中...", + "agentChat.teamWorkspace.board.detail.collapse": "收起細節", + "agentChat.teamWorkspace.board.detail.expand": "查看細節", + "agentChat.teamWorkspace.board.canvas.title": "目前進展", + "agentChat.teamWorkspace.board.canvas.subtitle.childRuntime": "並行任務會在各自面板裡持續更新進展和結果,主對話只保留必要摘要。", + "agentChat.teamWorkspace.board.canvas.subtitle.runtime": "{{formattedCount}} 條目前進展已接入,目前焦點會優先落在正在處理的分工上。", + "agentChat.teamWorkspace.board.canvas.subtitle.formation.forming": "正在準備目前任務分工,任務拆出後會在這裡獨立更新進展。", + "agentChat.teamWorkspace.board.canvas.subtitle.formation.formed": "目前任務分工已經就緒,任務拆出後會在各自進展面板裡開始處理。", + "agentChat.teamWorkspace.board.canvas.subtitle.formation.failed": "這次任務分工準備失敗,暫時無法生成目前進展面板。", + "agentChat.teamWorkspace.board.canvas.subtitle.formation.default": "任務拆出後,這裡會展開為獨立的目前進展。", + "agentChat.teamWorkspace.boardChrome.headline.parentOverviewFallback": "主助手總覽", + "agentChat.teamWorkspace.boardChrome.headline.empty": "需要時會自動拆出任務", + "agentChat.teamWorkspace.boardChrome.headline.runningWithQueued": "任務進行中 · {{runningCount}} 項處理中 / {{queuedCount}} 項稍後開始", + "agentChat.teamWorkspace.boardChrome.headline.runningCount": "任務進行中 · {{formattedCount}} 項處理中", + "agentChat.teamWorkspace.boardChrome.headline.running": "任務進行中", + "agentChat.teamWorkspace.boardChrome.headline.queuedCount": "任務準備中 · {{formattedCount}} 項稍後開始", + "agentChat.teamWorkspace.boardChrome.headline.queued": "任務準備中", + "agentChat.teamWorkspace.boardChrome.headline.completedCount": "{{formattedCount}} 項任務已完成", + "agentChat.teamWorkspace.boardChrome.headline.completed": "任務已完成", + "agentChat.teamWorkspace.boardChrome.headline.retryCount": "{{formattedCount}} 項任務需重試", + "agentChat.teamWorkspace.boardChrome.headline.retry": "任務需重試", + "agentChat.teamWorkspace.boardChrome.headline.connectedCount": "{{formattedCount}} 條目前進展已接入", + "agentChat.teamWorkspace.boardChrome.headline.surfaceTitle": "生成", + "agentChat.teamWorkspace.boardChrome.hint.childWithSiblings": "目前任務正與 {{formattedCount}} 項並行子任務一起推進", + "agentChat.teamWorkspace.boardChrome.hint.childSingle": "目前正在處理這項子任務,結果會回流到主助手。", + "agentChat.teamWorkspace.boardChrome.hint.empty": "系統會在需要時自動拆出任務、安排處理順序,並把結果回流到目前任務。", + "agentChat.teamWorkspace.boardChrome.hint.runtime": "這裡只展示目前有哪些分工在處理、狀態如何,以及最近更新到哪裡。", + "agentChat.teamWorkspace.boardChrome.chip.focus": "目前焦點 {{name}}", + "agentChat.teamWorkspace.boardChrome.chip.updatedAt": "更新於 {{updatedAt}}", + "agentChat.teamWorkspace.boardChrome.chip.waitable": "{{formattedCount}} 項處理中", + "agentChat.teamWorkspace.boardChrome.chip.completed": "{{formattedCount}} 項已完成", + "agentChat.teamWorkspace.boardChrome.statusSummary": "{{status}} {{formattedCount}}", + "agentChat.teamWorkspace.overview.selectedSession.waitingTitle": "等待任務接手", + "agentChat.teamWorkspace.overview.selectedSession.focusBadge": "目前焦點", + "agentChat.teamWorkspace.overview.selectedSession.waitingBadge": "等待接手", + "agentChat.teamWorkspace.overview.compact.taskActionsLabel": "任務處理", + "agentChat.teamWorkspace.overview.compact.viewActionsLabel": "視圖", + "agentChat.teamWorkspace.overview.taskCount.waitable": "{{formattedCount}} 項任務正在處理中", + "agentChat.teamWorkspace.overview.taskCount.completed": "{{formattedCount}} 項任務已完成", + "agentChat.teamWorkspace.formation.hint.forming": "系統正在準備目前任務的分工,任務拆出後會自動開始處理。", + "agentChat.teamWorkspace.formation.hint.formed": "目前任務的分工已經準備好,任務拆出後會繼續接手處理。", + "agentChat.teamWorkspace.formation.hint.failed": "目前任務的分工準備失敗,但你仍然可以繼續在目前任務裡推進。", + "agentChat.teamWorkspace.formation.hint.default": "需要時這裡會自動展開成目前進展面板。", + "agentChat.teamWorkspace.formation.emptyDetail.forming": "系統正在根據目前任務準備分工。完成後,這裡會先展示分工卡片,再接入真實處理進展。", + "agentChat.teamWorkspace.formation.detail.hint.runtimeWithReference": "查看目前任務分工與參考方案", + "agentChat.teamWorkspace.formation.detail.hint.planOnly": "查看目前分工方案", + "agentChat.teamWorkspace.formation.detail.summary.runtime": "目前摘要", + "agentChat.teamWorkspace.formation.detail.summary.plan": "方案摘要", + "agentChat.teamWorkspace.formation.detail.referenceSummaryDefault": "目前任務分工參考了目前方案裡的角色偏好。", + "agentChat.teamWorkspace.formation.detail.roleSection.runtime": "目前任務分工", + "agentChat.teamWorkspace.formation.detail.roleSection.plan": "角色分工", + "agentChat.teamWorkspace.formation.emptyDetail.formed": "目前分工方案已經準備好。畫布會先展示目前分工,等任務真正開始處理後,再自動切換為目前進展。", + "agentChat.teamWorkspace.formation.emptyDetail.failed": "目前任務分工準備失敗,暫時無法展示目前進展。", + "agentChat.teamWorkspace.formation.emptyDetail.default": "還沒有任務接手。系統開始分工後,詳情區會切換為進展摘要視圖。", + "agentChat.teamWorkspace.formation.notice.forming": "系統正在準備目前任務分工,完成後會先展示分工卡片,後續再切換為獨立的目前進展。", + "agentChat.teamWorkspace.formation.notice.formed": "目前分工方案已就緒。任務拆出後,這裡會從方案視圖過渡到目前進展。", + "agentChat.teamWorkspace.formation.notice.failed": "目前任務分工準備失敗,暫時還沒有任務接手。", + "agentChat.teamWorkspace.formation.notice.default": "還沒有任務接手。系統開始分工後,這裡會生成獨立的目前進展。", + "agentChat.teamWorkspace.formation.summaryBadge.planLabel": "分工方案 · {{label}}", + "agentChat.teamWorkspace.formation.summaryBadge.planRoleCount": "{{formattedCount}} 個計畫分工", + "agentChat.teamWorkspace.formation.summaryBadge.memberCount": "{{formattedCount}} 條目前進展", + "agentChat.teamWorkspace.formation.summaryBadge.referenceLabel": "參考方案 · {{label}}", + "agentChat.teamWorkspace.formation.summaryBadge.taskCount": "{{formattedCount}} 項任務", + "agentChat.teamWorkspace.formation.panel.title": "目前任務分工", + "agentChat.teamWorkspace.formation.panel.description.failed": "目前任務分工準備失敗,暫時無法展示更多內容。", + "agentChat.teamWorkspace.formation.panel.description.default": "這裡會先展示目前分工方案,任務拆出後再切換成目前進展。", + "agentChat.teamWorkspace.formation.waitingHeadline": "等待任務接手", + "agentChat.teamWorkspace.formation.status.forming.label": "準備中", + "agentChat.teamWorkspace.formation.status.forming.title": "正在準備任務分工", + "agentChat.teamWorkspace.formation.status.formed.label": "已就緒", + "agentChat.teamWorkspace.formation.status.formed.title": "任務分工已準備好", + "agentChat.teamWorkspace.formation.status.failed.label": "準備失敗", + "agentChat.teamWorkspace.formation.status.failed.title": "任務分工準備失敗", + "agentChat.teamWorkspace.formation.memberStatus.planned": "待分配", + "agentChat.teamWorkspace.formation.memberStatus.spawning": "接入中", + "agentChat.teamWorkspace.formation.memberStatus.running": "正在處理", + "agentChat.teamWorkspace.formation.memberStatus.waiting": "待繼續", + "agentChat.teamWorkspace.formation.memberStatus.completed": "已完成", + "agentChat.teamWorkspace.formation.memberStatus.failed": "需重試", + "agentChat.teamWorkspace.selectedSession.header.eyebrow": "目前查看", + "agentChat.teamWorkspace.selectedSession.header.currentTaskBadge": "目前任務", + "agentChat.teamWorkspace.selectedSession.header.action.resumePending": "繼續中...", + "agentChat.teamWorkspace.selectedSession.header.action.resume": "繼續處理", + "agentChat.teamWorkspace.selectedSession.header.action.closePending": "暫停中...", + "agentChat.teamWorkspace.selectedSession.header.action.close": "暫停處理", + "agentChat.teamWorkspace.selectedSession.header.action.openChild": "切換進展", + "agentChat.teamWorkspace.selectedSession.header.action.open": "打開進展", + "agentChat.teamWorkspace.selectedSession.settings.heading": "任務分工", + "agentChat.teamWorkspace.teamPreset.codeTriageTeam": "程式碼排障團隊", + "agentChat.teamWorkspace.teamPreset.researchTeam": "研究團隊", + "agentChat.teamWorkspace.teamPreset.contentCreationTeam": "內容創作團隊", + "agentChat.teamWorkspace.selectedSession.detail.setting.preset": "預設 {{label}}", + "agentChat.teamWorkspace.selectedSession.detail.setting.profile": "風格 {{name}}", + "agentChat.teamWorkspace.selectedSession.detail.setting.role": "分工 {{label}}", + "agentChat.teamWorkspace.selectedSession.detail.setting.theme": "主題 {{theme}}", + "agentChat.teamWorkspace.selectedSession.detail.metadata.role": "分工 {{label}}", + "agentChat.teamWorkspace.selectedSession.detail.metadata.provider": "服務 {{name}}", + "agentChat.teamWorkspace.selectedSession.detail.metadata.model": "模型 {{model}}", + "agentChat.teamWorkspace.selectedSession.detail.metadata.originTool": "來源 {{tool}}", + "agentChat.teamWorkspace.selectedSession.detail.metadata.createdFromTurn": "來自先前的任務 {{turnId}}", + "agentChat.teamWorkspace.selectedSession.detail.metadata.parentSession": "來自 {{name}}", + "agentChat.teamWorkspace.selectedSession.detail.sessionType.subAgent": "子任務", + "agentChat.teamWorkspace.selectedSession.detail.sessionType.fork": "補充分支", + "agentChat.teamWorkspace.selectedSession.detail.sessionType.user": "主助手", + "agentChat.teamWorkspace.selectedSession.detail.roleHint.explorer": "分析", + "agentChat.teamWorkspace.selectedSession.detail.roleHint.executor": "執行", + "agentChat.teamWorkspace.selectedSession.detail.roleHint.reviewer": "覆核", + "agentChat.teamWorkspace.selectedSession.detail.roleHint.writer": "撰寫", + "agentChat.teamWorkspace.selectedSession.detail.roleHint.planner": "規劃", + "agentChat.teamWorkspace.selectedSession.detail.roleHint.researcher": "研究", + "agentChat.teamWorkspace.selectedSession.detail.roleHint.designer": "設計", + "agentChat.teamWorkspace.selectedSession.detail.roleHint.design": "設計", + "agentChat.teamWorkspace.selectedSession.detail.roleHint.editor": "潤飾", + "agentChat.teamWorkspace.selectedSession.detail.roleHint.orchestrator": "主助手", + "agentChat.teamWorkspace.selectedSession.footer.updatedAt": "更新於 {{updatedAt}}", + "agentChat.teamWorkspace.selectedSession.collaboration.heading": "繼續處理", + "agentChat.teamWorkspace.selectedSession.collaboration.waitReady": "可直接查看結果", + "agentChat.teamWorkspace.selectedSession.collaboration.waitPending": "等待中...", + "agentChat.teamWorkspace.selectedSession.collaboration.wait": "等待結果 30 秒", + "agentChat.teamWorkspace.selectedSession.collaboration.waitHint": "僅在目前內容確實依賴這項任務結果時使用。", + "agentChat.teamWorkspace.selectedSession.collaboration.inputPlaceholder": "給這項任務補充說明、補充約束,或請它繼續推進下一步。", + "agentChat.teamWorkspace.selectedSession.collaboration.sendPending": "發送中...", + "agentChat.teamWorkspace.selectedSession.collaboration.send": "發送說明", + "agentChat.teamWorkspace.selectedSession.collaboration.interruptPending": "中斷中...", + "agentChat.teamWorkspace.selectedSession.collaboration.interrupt": "立即插入說明", + "agentChat.teamWorkspace.selectedSession.collaboration.inputHint": "這條說明只會發送給目前任務,不影響其他並行任務。", + "agentChat.teamWorkspace.selectedSession.activity.heading": "完整進展", + "agentChat.teamWorkspace.selectedSession.activity.autoRefresh": "處理中自動刷新", + "agentChat.teamWorkspace.selectedSession.activity.errorPrefix": "最新進展暫不可用:", + "agentChat.teamWorkspace.selectedSession.activity.errorFallback": "同步失敗", + "agentChat.teamWorkspace.selectedSession.activity.emptyReady": "這項任務暫時還沒有可展示的新進展。", + "agentChat.teamWorkspace.selectedSession.activity.loading": "正在同步這項任務的最新進展...", + "agentChat.teamWorkspace.selectedSession.activity.feedHeading": "進展記錄", + "agentChat.teamWorkspace.selectedSession.activity.entryCount": "{{formattedCount}} 條", + "agentChat.teamWorkspace.activityPreview.line": "{{title}}:{{detail}}", + "agentChat.teamWorkspace.activityPreview.status.error": "錯誤", + "agentChat.teamWorkspace.activityPreview.status.warning": "警告", + "agentChat.teamWorkspace.activityPreview.status.inProgress": "進行中", + "agentChat.teamWorkspace.activityPreview.status.failed": "失敗", + "agentChat.teamWorkspace.activityPreview.status.completed": "完成", + "agentChat.teamWorkspace.activityPreview.status.message": "訊息", + "agentChat.teamWorkspace.activityPreview.messageTitle.error": "錯誤", + "agentChat.teamWorkspace.activityPreview.messageTitle.output": "輸出", + "agentChat.teamWorkspace.activityPreview.messageTitle.reply": "回覆", + "agentChat.teamWorkspace.activityPreview.error.syncFailed": "同步最近進展失敗", + "agentChat.teamWorkspace.activityPreview.error.historyReadFailed": "讀取歷史正文失敗", + "agentChat.teamWorkspace.liveRuntime.title.statusChanged": "狀態切換", + "agentChat.teamWorkspace.liveRuntime.detail.statusChanged": "收到任務狀態事件,已切換為{{status}}。", + "agentChat.teamWorkspace.liveRuntime.title.textDraft": "內容生成中", + "agentChat.teamWorkspace.liveRuntime.title.thinkingDraft": "整理思路中", + "agentChat.teamWorkspace.liveRuntime.title.currentProgress": "目前進展", + "agentChat.teamWorkspace.liveRuntime.title.processing": "處理中", + "agentChat.teamWorkspace.liveRuntime.title.tool": "{{status}} · {{toolName}}", + "agentChat.teamWorkspace.liveRuntime.title.toolTargetFallback": "目前步驟", + "agentChat.teamWorkspace.liveRuntime.detail.toolCompletedFallback": "目前步驟已完成。", + "agentChat.teamWorkspace.liveRuntime.detail.toolProcessing": "正在處理 {{target}}。", + "agentChat.teamWorkspace.liveRuntime.title.warning": "警告", + "agentChat.teamWorkspace.liveRuntime.title.error": "錯誤", + "agentChat.teamWorkspace.liveRuntime.status.processing": "處理中", + "agentChat.teamWorkspace.liveRuntime.status.queued": "稍後開始", + "agentChat.teamWorkspace.liveRuntime.status.completed": "完成", + "agentChat.teamWorkspace.liveRuntime.status.retry": "需重試", + "agentChat.teamWorkspace.liveRuntime.status.warning": "警告", + "agentChat.teamWorkspace.liveRuntime.lifecycle.queueAdded.title": "稍後開始", + "agentChat.teamWorkspace.liveRuntime.lifecycle.queueAdded.detail": "新的說明已經收到,這項子任務會在前一項完成後繼續處理。", + "agentChat.teamWorkspace.liveRuntime.lifecycle.queueStarted.title": "開始處理", + "agentChat.teamWorkspace.liveRuntime.lifecycle.queueStarted.detail": "這項子任務已經開始處理目前任務。", + "agentChat.teamWorkspace.liveRuntime.lifecycle.turnStarted.title": "繼續處理", + "agentChat.teamWorkspace.liveRuntime.lifecycle.turnStarted.detail": "這項子任務正在推進目前內容。", + "agentChat.teamWorkspace.liveRuntime.lifecycle.turnCompleted.title": "階段完成", + "agentChat.teamWorkspace.liveRuntime.lifecycle.turnCompleted.detail": "這一步已經完成,正在同步最新結果。", + "agentChat.teamWorkspace.liveRuntime.lifecycle.turnFailed.title": "處理失敗", + "agentChat.teamWorkspace.liveRuntime.lifecycle.turnFailed.detail": "這一步處理失敗,請查看錯誤詳情。", + "agentChat.teamWorkspace.operations.heading": "任務進展", + "agentChat.teamWorkspace.operations.recentCount": "最近 {{formattedCount}} 條", + "agentChat.teamWorkspace.operations.updatedNow": "剛剛", + "agentChat.teamWorkspace.operations.taskFallback": "這項任務", + "agentChat.teamWorkspace.operations.title.waitTimedOut": "等待逾時", + "agentChat.teamWorkspace.operations.title.waitResolved": "收到結果", + "agentChat.teamWorkspace.operations.title.resume": "繼續處理", + "agentChat.teamWorkspace.operations.title.closeCompleted": "收起完成項", + "agentChat.teamWorkspace.operations.title.close": "暫停處理", + "agentChat.teamWorkspace.operations.wait.timedOut": "剛才等待結果時逾時了,還有 {{formattedCount}} 項任務仍在處理中。", + "agentChat.teamWorkspace.operations.wait.resolved": "剛才等到 {{sessionName}} 回傳了新結果,目前狀態為{{status}}。", + "agentChat.teamWorkspace.operations.control.resumeCount": "剛才已繼續 {{formattedCount}} 項任務的處理。", + "agentChat.teamWorkspace.operations.control.resumeOne": "剛才已繼續 {{sessionName}} 的處理。", + "agentChat.teamWorkspace.operations.control.closeCompleted": "剛才已收起 {{formattedCount}} 項已完成任務。", + "agentChat.teamWorkspace.operations.control.closeCount": "剛才已暫停 {{formattedCount}} 項任務的處理。", + "agentChat.teamWorkspace.operations.control.closeOne": "剛才已暫停 {{sessionName}} 的處理。", + "agentChat.teamWorkspace.canvasLane.updatedNow": "剛剛", + "agentChat.teamWorkspace.canvasLane.runtimeDetail.waitingCount": "等待中 {{formattedCount}}", + "agentChat.teamWorkspace.canvasLane.runtimeDetail.recentProgress": "最近進展 {{status}}", + "agentChat.teamWorkspace.canvasLane.runtimeDetail.processingCount": "處理中 {{activeCount}}/{{totalCount}}", + "agentChat.teamWorkspace.canvasLane.runtimeDetail.stableProcessing": "穩妥模式", + "agentChat.teamWorkspace.canvasLane.empty.syncFailed": "同步最新內容失敗", + "agentChat.teamWorkspace.canvasLane.empty.syncLoading": "正在同步這項任務的最新內容...", + "agentChat.teamWorkspace.canvasLane.empty.queued": "這項任務已經收到安排,馬上開始處理。", + "agentChat.teamWorkspace.canvasLane.empty.running": "這項任務正在處理,最新進展會持續刷新到這裡。", + "agentChat.teamWorkspace.canvasLane.empty.completed": "這部分已經完成,結果會繼續匯入目前內容。", + "agentChat.teamWorkspace.canvasLane.empty.failed": "這一步沒有順利完成,你可以在下方查看細節並決定是否繼續。", + "agentChat.teamWorkspace.canvasLane.empty.default": "這項任務暫時還沒有可展示的內容。", + "agentChat.teamWorkspace.canvasLane.session.summaryEmpty": "暫時還沒有任務摘要,打開詳情後可查看完整上下文。", + "agentChat.teamWorkspace.canvasLane.memberHint.spawning": "正在接入這項任務", + "agentChat.teamWorkspace.canvasLane.memberHint.running": "這項任務正在處理", + "agentChat.teamWorkspace.canvasLane.memberHint.waiting": "等待繼續補充說明", + "agentChat.teamWorkspace.canvasLane.memberHint.completed": "這一步已經完成", + "agentChat.teamWorkspace.canvasLane.memberHint.failed": "這一步需要重試", + "agentChat.teamWorkspace.canvasLane.memberHint.connected": "已連接到真實任務", + "agentChat.teamWorkspace.canvasLane.memberHint.waitingAssignment": "等待任務接手", + "agentChat.teamWorkspace.canvasLane.planned.badge": "待開始", + "agentChat.teamWorkspace.canvasLane.planned.statusHint": "等待系統把這項任務拆出來", + "agentChat.teamWorkspace.canvasLane.runtime.updatedAtWaiting": "等待任務接手", + "agentChat.teamWorkspace.canvasLane.planned.updatedAt": "計劃分工", + "agentChat.teamWorkspace.runtimeStatus.idle": "待開始", + "agentChat.teamWorkspace.runtimeStatus.queued": "稍後開始", + "agentChat.teamWorkspace.runtimeStatus.running": "處理中", "agentChat.teamWorkspace.runtimeStatus.completed": "已完成", + "agentChat.teamWorkspace.runtimeStatus.failed": "需重試", + "agentChat.teamWorkspace.runtimeStatus.aborted": "已暫停", + "agentChat.teamWorkspace.runtimeStatus.closed": "已停止", + "agentChat.teamWorkspace.control.close.cascadeStopped": "已連帶停止 {{formattedCount}} 項任務", + "agentChat.teamWorkspace.control.close.stoppedOne": "這項任務已停止", + "agentChat.teamWorkspace.control.close.noChange": "目前任務狀態為{{status}},未發生新的停止變更", + "agentChat.teamWorkspace.control.close.failedFallback": "停止任務失敗", + "agentChat.teamWorkspace.control.resume.cascadeResumed": "已連帶恢復 {{formattedCount}} 項任務", + "agentChat.teamWorkspace.control.resume.resumedOne": "這項任務已恢復", + "agentChat.teamWorkspace.control.resume.noChange": "目前任務狀態為{{status}},未發生新的恢復變更", + "agentChat.teamWorkspace.control.resume.failedFallback": "恢復任務失敗", + "agentChat.teamWorkspace.control.wait.timeoutOne": "等待逾時,這項任務仍未進入最終狀態", + "agentChat.teamWorkspace.control.wait.enteredStatusOne": "這項任務已進入{{status}}狀態", + "agentChat.teamWorkspace.control.wait.noActiveError": "沒有可等待的活躍任務", + "agentChat.teamWorkspace.control.wait.timeoutTeam": "等待逾時,團隊內活躍任務仍未進入最終狀態", + "agentChat.teamWorkspace.control.wait.enteredStatusTeam": "團隊任務已進入{{status}}狀態", + "agentChat.teamWorkspace.control.wait.failedFallback": "等待任務失敗", + "agentChat.teamWorkspace.control.closeCompleted.noCompletedError": "沒有可關閉的已完成任務", + "agentChat.teamWorkspace.control.closeCompleted.cascadeClosed": "已連帶收起 {{formattedCount}} 項任務", + "agentChat.teamWorkspace.control.closeCompleted.closedCount": "已收起 {{formattedCount}} 項已完成任務", + "agentChat.teamWorkspace.control.closeCompleted.partialFailedFallback": "部分已完成任務收起失敗", + "agentChat.teamWorkspace.control.sendInput.emptyError": "請輸入要傳送給這項任務的內容", + "agentChat.teamWorkspace.control.sendInput.sentInterrupt": "已中斷目前執行並傳送新說明", + "agentChat.teamWorkspace.control.sendInput.sent": "已向這項任務傳送補充說明", + "agentChat.teamWorkspace.control.sendInput.failedFallback": "傳送任務說明失敗", + "agentChat.teamWorkspace.control.stop.pausedCount": "已暫停 {{formattedCount}} 項任務的處理", + "agentChat.teamWorkspace.control.stop.pausedOne": "已暫停這項任務的處理", + "agentChat.teamWorkspace.control.stop.requestedCount": "已向 {{formattedCount}} 項任務傳送暫停請求", + "agentChat.teamWorkspace.control.stop.requestedOne": "已向這項任務傳送暫停請求", + "agentChat.teamWorkspace.control.stop.failedFallback": "停止任務失敗", "agentChat.threadReliability.routingEvidence.evidence": "證據", "agentChat.threadReliability.routingEvidence.fallbackSlot": "runtime routing", "agentChat.threadReliability.routingEvidence.firstText": "首個正文", @@ -383,47 +602,48 @@ "capabilityDraft.panel.title": "能力草案", "capabilityDraft.registeredPanel.action.auditRecentRun": "稽核最近執行", "capabilityDraft.registeredPanel.action.auditRunning": "正在稽核", - "capabilityDraft.registeredPanel.action.createManagedJobDraft": "建立 Managed Job 草稿", - "capabilityDraft.registeredPanel.action.enableRuntime": "本回合启用", + "capabilityDraft.registeredPanel.action.createManagedJobDraft": "設定定時執行", + "capabilityDraft.registeredPanel.action.enableRuntime": "試用一次", "capabilityDraft.registeredPanel.action.enableRuntimeHelp": "只寫入 session enable metadata,不建立自動化。", - "capabilityDraft.registeredPanel.action.pauseManagedJob": "暫停 Managed Job", + "capabilityDraft.registeredPanel.action.pauseManagedJob": "暫停定時執行", "capabilityDraft.registeredPanel.action.refresh": "重新整理", - "capabilityDraft.registeredPanel.action.resumeManagedJob": "恢復 Managed Job", - "capabilityDraft.registeredPanel.agentEnvelope.action.blocked": "先解除阻斷", - "capabilityDraft.registeredPanel.agentEnvelope.action.draft": "转成 Agent 草稿", - "capabilityDraft.registeredPanel.agentEnvelope.action.manualEnable": "先本回合启用", - "capabilityDraft.registeredPanel.agentEnvelope.agentCard.pending": "Agent card:等待 evidence-ready 后派生,不建立平行持久化实体。", - "capabilityDraft.registeredPanel.agentEnvelope.agentCard.ready": "Agent card:workspace-local/{{directory}},由已註冊 Skill、Managed Job 和 completion audit 派生。", - "capabilityDraft.registeredPanel.agentEnvelope.blockedReasonFallback": "runtime binding 目前被 gate 阻断", - "capabilityDraft.registeredPanel.agentEnvelope.description": "成功執行后可把 Skill、權限、手動 rerun 和 evidence 组合成 Workspace Agent envelope。", - "capabilityDraft.registeredPanel.agentEnvelope.discovery.pending": "Discovery:等待 workspace-local skill 註冊路径后再進入團隊发现。", - "capabilityDraft.registeredPanel.agentEnvelope.discovery.ready": "Discovery:同 workspace 成員通過 registered skill 发现 {{directory}},複用同一 Managed Job / evidence。", + "capabilityDraft.registeredPanel.action.resumeManagedJob": "開啟定時執行", + "capabilityDraft.registeredPanel.agentEnvelope.action.blocked": "先處理問題", + "capabilityDraft.registeredPanel.agentEnvelope.action.draft": "儲存為專案助手", + "capabilityDraft.registeredPanel.agentEnvelope.action.manualEnable": "先試用一次", + "capabilityDraft.registeredPanel.agentEnvelope.agentCard.pending": "專案助手:試用通過後再儲存。", + "capabilityDraft.registeredPanel.agentEnvelope.agentCard.ready": "專案助手:已準備好儲存({{directory}})。", + "capabilityDraft.registeredPanel.agentEnvelope.blockedReasonFallback": "目前還有問題需要處理", + "capabilityDraft.registeredPanel.agentEnvelope.description": "試用結果沒問題後,可以把它儲存成目前專案裡的助手。", + "capabilityDraft.registeredPanel.agentEnvelope.discovery.pending": "團隊可見性:儲存為專案助手後再開放。", + "capabilityDraft.registeredPanel.agentEnvelope.discovery.ready": "團隊可見性:目前專案成員可以使用這個技能。", "capabilityDraft.registeredPanel.agentEnvelope.evidence.blocking": ",阻斷:{{reasons}}", - "capabilityDraft.registeredPanel.agentEnvelope.evidence.completedPack": "Evidence:已关联 evidence pack {{packId}}。", - "capabilityDraft.registeredPanel.agentEnvelope.evidence.completedPackFallback": "Evidence:已关联 evidence pack。", + "capabilityDraft.registeredPanel.agentEnvelope.evidence.completedPack": "最近結果:已通過檢查({{packId}})。", + "capabilityDraft.registeredPanel.agentEnvelope.evidence.completedPackFallback": "最近結果:已通過檢查。", "capabilityDraft.registeredPanel.agentEnvelope.evidence.completionAudit": "Evidence:completion audit {{decision}},owner {{successfulOwnerRunCount}}/{{ownerRunCount}},ToolCall {{toolCallCount}},artifact {{artifactCount}}{{controlledGetLabel}}{{blockingLabel}}{{suffix}}。", "capabilityDraft.registeredPanel.agentEnvelope.evidence.controlledGet.executed": ",受控 GET {{executed}}/{{artifacts}} executed", "capabilityDraft.registeredPanel.agentEnvelope.evidence.controlledGet.requiredMissing": ",受控 GET required 0/0 executed", - "capabilityDraft.registeredPanel.agentEnvelope.evidence.missing": "Evidence:还沒有成功執行證據;先通過本回合启用拿到一次結果。", - "capabilityDraft.registeredPanel.agentEnvelope.evidence.pendingPack": "Evidence:已关联 evidence pack {{packId}},但还缺 completed completion audit,不能固化为 Agent。", - "capabilityDraft.registeredPanel.agentEnvelope.evidence.sourceMetadataOnly": "Evidence:已有 P3E source metadata,可追踪本次 session 授权來源。", + "capabilityDraft.registeredPanel.agentEnvelope.evidence.missing": "最近結果:還沒有試用結果。", + "capabilityDraft.registeredPanel.agentEnvelope.evidence.pendingPack": "最近結果:已產生,等待檢查後才能儲存成專案助手。", + "capabilityDraft.registeredPanel.agentEnvelope.evidence.sourceMetadataOnly": "最近結果:已記錄這次試用來源,等待檢查。", "capabilityDraft.registeredPanel.agentEnvelope.evidence.suffix.missingControlledGet": ";缺受控 GET evidence,不能固化为 Agent", "capabilityDraft.registeredPanel.agentEnvelope.evidence.suffix.notCompleted": ";未 completed,不能固化为 Agent", - "capabilityDraft.registeredPanel.agentEnvelope.memory.pending": "Memory:等待首轮執行后记录使用者偏好、方法论和修正历史。", - "capabilityDraft.registeredPanel.agentEnvelope.memory.withReport": "Memory:引用 verification report {{reportId}} 与後續執行修正。", - "capabilityDraft.registeredPanel.agentEnvelope.permission.empty": "權限:預設手動確認,未声明额外外部写權限。", + "capabilityDraft.registeredPanel.agentEnvelope.memory.pending": "記錄:等待首次試用後記錄偏好和修正。", + "capabilityDraft.registeredPanel.agentEnvelope.memory.withReport": "記錄:已保留檢查結果,後續會繼續累積使用回饋。", + "capabilityDraft.registeredPanel.agentEnvelope.permission.empty": "權限:預設需要手動確認。", "capabilityDraft.registeredPanel.agentEnvelope.permission.withSummary": "權限:{{summary}}。", - "capabilityDraft.registeredPanel.agentEnvelope.runbook": "Runbook:{{name}}", - "capabilityDraft.registeredPanel.agentEnvelope.schedule": "Schedule:第一刀仅支持 manual rerun 草稿,不建立長期任务。", - "capabilityDraft.registeredPanel.agentEnvelope.sharing.pending": "Sharing:未完成稽核前仅对目前操作者展示草稿。", - "capabilityDraft.registeredPanel.agentEnvelope.sharing.ready": "Sharing:可在目前 workspace / team 内共享;不進入 public Marketplace。", - "capabilityDraft.registeredPanel.agentEnvelope.status.blocked": "Agent 草稿阻斷", - "capabilityDraft.registeredPanel.agentEnvelope.status.evidenceReady": "Evidence 已就绪", - "capabilityDraft.registeredPanel.agentEnvelope.status.manualEnableRequired": "等待成功執行", - "capabilityDraft.registeredPanel.agentEnvelope.status.sourceMetadataReady": "等待 Completion Audit", - "capabilityDraft.registeredPanel.agentEnvelope.title": "Agent envelope 草稿", - "capabilityDraft.registeredPanel.agentEnvelope.widget.pending": "Widget:等待執行后展示狀態、產物、阻斷点和 evidence 入口。", - "capabilityDraft.registeredPanel.agentEnvelope.widget.ready": "Widget:展示 Managed Job 狀態、最近產物、稽核结论和下一步动作。", + "capabilityDraft.registeredPanel.agentEnvelope.runbook": "使用方式:{{name}}", + "capabilityDraft.registeredPanel.agentEnvelope.schedule": "定時執行:尚未設定。", + "capabilityDraft.registeredPanel.agentEnvelope.sharing.pending": "共享:目前只有你可見。", + "capabilityDraft.registeredPanel.agentEnvelope.sharing.ready": "共享:可在目前專案內共享。", + "capabilityDraft.registeredPanel.agentEnvelope.status.blocked": "需要處理", + "capabilityDraft.registeredPanel.agentEnvelope.status.evidenceReady": "可儲存", + "capabilityDraft.registeredPanel.agentEnvelope.status.manualEnableRequired": "待試用", + "capabilityDraft.registeredPanel.agentEnvelope.status.sourceMetadataReady": "待檢查", + "capabilityDraft.registeredPanel.agentEnvelope.title": "專案助手", + "capabilityDraft.registeredPanel.agentEnvelope.details": "專案助手詳情", + "capabilityDraft.registeredPanel.agentEnvelope.widget.pending": "結果:試用後展示最近狀態和下一步。", + "capabilityDraft.registeredPanel.agentEnvelope.widget.ready": "結果:展示最近狀態、產物和下一步動作。", "capabilityDraft.registeredPanel.approval.consumptionGate.title": "消費门禁", "capabilityDraft.registeredPanel.approval.credentialResolver.title": "Session credential resolver", "capabilityDraft.registeredPanel.approval.description": "真实 API 执行前必须先消費这条授权请求 artifact;目前只持久化稽核入口,不儲存 token,也不发请求。", @@ -465,10 +685,11 @@ "capabilityDraft.registeredPanel.approval.suffix.required": ":required", "capabilityDraft.registeredPanel.approval.suffix.secret": ":secret", "capabilityDraft.registeredPanel.approval.title": "Session approval request artifact", - "capabilityDraft.registeredPanel.badge": "註冊区", - "capabilityDraft.registeredPanel.card.badge.registered": "已註冊", - "capabilityDraft.registeredPanel.card.binding.blocked": "Binding 阻斷", - "capabilityDraft.registeredPanel.card.binding.candidate": "P3C binding 候选", + "capabilityDraft.registeredPanel.technicalDetails": "技術詳情", + "capabilityDraft.registeredPanel.badge": "已儲存", + "capabilityDraft.registeredPanel.card.badge.registered": "已儲存", + "capabilityDraft.registeredPanel.card.binding.blocked": "需要處理", + "capabilityDraft.registeredPanel.card.binding.candidate": "可試用", "capabilityDraft.registeredPanel.card.descriptionFallback": "已註冊为目前 Workspace 的本機 Skill 包。", "capabilityDraft.registeredPanel.card.field.directory": "目錄:", "capabilityDraft.registeredPanel.card.field.nextGate": "下一道 gate:", @@ -478,11 +699,11 @@ "capabilityDraft.registeredPanel.card.field.source": "來源:", "capabilityDraft.registeredPanel.card.field.standard": "標準:", "capabilityDraft.registeredPanel.card.nextGateFallback": "manual_runtime_enable / Query Loop metadata / tool_runtime 授权裁剪", - "capabilityDraft.registeredPanel.description": "这里只有已通過驗證并寫入目前專案的 Skill 包;執行仍要等 runtime gate。", - "capabilityDraft.registeredPanel.empty.error": "已註冊能力暂时没读到:{{message}}", - "capabilityDraft.registeredPanel.empty.loading": "正在讀取已註冊能力...", - "capabilityDraft.registeredPanel.empty.missingProject": "選擇或進入一个專案后,才能檢視该專案已註冊的 generated skill。", - "capabilityDraft.registeredPanel.empty.noSkills": "目前專案还沒有通過 P3A 註冊的能力。草稿通過驗證并註冊后,会先出现在这里。", + "capabilityDraft.registeredPanel.description": "這裡是已經檢查並儲存到目前專案的技能。可以先試用,也可以設定定時執行。", + "capabilityDraft.registeredPanel.empty.error": "已儲存技能暫時讀不到,請稍後再試。", + "capabilityDraft.registeredPanel.empty.loading": "正在讀取已儲存技能...", + "capabilityDraft.registeredPanel.empty.missingProject": "選擇或進入一個專案後,才能查看這個專案裡儲存的技能。", + "capabilityDraft.registeredPanel.empty.noSkills": "目前專案還沒有已儲存技能。先在「正在製作」裡檢查並儲存一個。", "capabilityDraft.registeredPanel.error.missingAutomationSession": "最近 automation run 沒有关联 session,无法导出 evidence。", "capabilityDraft.registeredPanel.evidence.credentialReferenceId": "憑證引用", "capabilityDraft.registeredPanel.evidence.endpointSource": "Endpoint", @@ -516,6 +737,29 @@ "capabilityDraft.registeredPanel.managedJob.status.notCreated": "Managed Job:未建立", "capabilityDraft.registeredPanel.managedJob.status.notRun": "尚未執行", "capabilityDraft.registeredPanel.managedJob.status.withValue": "Managed Job:{{state}} · {{lastStatus}}", + "capabilityDraft.registeredPanel.user.permission.defaultReadonly": "預設只讀取資訊", + "capabilityDraft.registeredPanel.user.permission.draftWrite": "可寫入草稿", + "capabilityDraft.registeredPanel.user.permission.localCommand": "可執行本機命令", + "capabilityDraft.registeredPanel.user.permission.readonly": "只讀取資訊", + "capabilityDraft.registeredPanel.user.schedule.enabled": "已開啟", + "capabilityDraft.registeredPanel.user.schedule.notCreated": "未設定", + "capabilityDraft.registeredPanel.user.schedule.paused": "已暫停", + "capabilityDraft.registeredPanel.user.result.auditBlocked": "最近結果需要處理", + "capabilityDraft.registeredPanel.user.result.auditPassed": "最近結果已通過檢查", + "capabilityDraft.registeredPanel.user.result.noJob": "還沒有定時執行", + "capabilityDraft.registeredPanel.user.result.noRun": "還沒有執行記錄", + "capabilityDraft.registeredPanel.user.result.readyToCheck": "可以檢查最近結果", + "capabilityDraft.registeredPanel.user.result.running": "正在執行", + "capabilityDraft.registeredPanel.user.step.saved": "已儲存", + "capabilityDraft.registeredPanel.user.step.savedValue": "可在目前專案使用", + "capabilityDraft.registeredPanel.user.step.permission": "權限", + "capabilityDraft.registeredPanel.user.step.schedule": "定時執行", + "capabilityDraft.registeredPanel.user.nextStep": "建議先試用一次。確認結果沒問題後,再設定定時執行或儲存成專案助手。", + "capabilityDraft.registeredPanel.user.assistant.try": "試用", + "capabilityDraft.registeredPanel.user.assistant.tryReady": "可以試用一次", + "capabilityDraft.registeredPanel.user.assistant.tryBlocked": "暫不可試用", + "capabilityDraft.registeredPanel.user.assistant.schedule": "定時執行", + "capabilityDraft.registeredPanel.user.assistant.result": "最近結果", "capabilityDraft.registeredPanel.provenance.title": "註冊 provenance", "capabilityDraft.registeredPanel.summary.bindingBlockedFallback": "Runtime binding 目前被 gate 阻断。", "capabilityDraft.registeredPanel.summary.bindingCandidateFallback": "已具备後續 runtime binding 候选资格,但目前仍未進入預設工具面。", @@ -525,7 +769,7 @@ "capabilityDraft.registeredPanel.summary.standardIssueCount": "標準检查仍有 {{count}} 个問題", "capabilityDraft.registeredPanel.summary.standardPassed": "Agent Skills 標準通過", "capabilityDraft.registeredPanel.summary.standardPending": "Agent Skills 標準狀態待確認", - "capabilityDraft.registeredPanel.title": "Workspace 已註冊能力", + "capabilityDraft.registeredPanel.title": "已儲存技能", "curatedTask.badge.action.clearAria": "清除 {{title}}", "curatedTask.badge.action.clearTitle": "清除目前結果模板", "curatedTask.badge.action.edit": "編輯", @@ -1233,13 +1477,46 @@ "skills.workspace.group.emptyTitle": "目前分組下沒有符合的 Skill", "skills.workspace.group.selectedSubtitle": "選擇這一組裡的一個 Skill 繼續", "skills.workspace.header.refresh": "重新整理", - "skills.workspace.header.subtitle": "選擇一個 Skill 開始創作", - "skills.workspace.header.title": "Skills", + "skills.workspace.header.subtitle": "安裝和管理技能,需要時帶回首頁輸入框使用。", + "skills.workspace.header.title": "技能", + "skills.workspace.header.createWithLime": "透過 Lime 建立", + "skills.workspace.header.installSkill": "安裝技能", + "skills.workspace.autoLoad.disabledToast": "「{{name}}」已改為不自動載入", + "skills.workspace.autoLoad.enabledToast": "已允許「{{name}}」自動載入", + "skills.workspace.autoLoad.aria": "允許自動載入 {{name}}", + "skills.workspace.autoLoad.description": "開啟後,Lime 會在相關任務裡自動帶上;關閉後,不會主動匹配。", + "skills.workspace.autoLoad.off": "已關閉", + "skills.workspace.autoLoad.on": "已開啟", + "skills.workspace.autoLoad.label": "自動載入", + "skills.workspace.store.officialBadge": "官方精選", + "skills.workspace.scaffold.createFailed": "建立技能失敗:{{message}}", + "skills.workspace.import.failed": "安裝技能失敗:{{message}}", + "skills.workspace.import.success": "技能已安裝,已收進使用者安裝。", + "skills.workspace.import.dialogTitle": "選擇包含 SKILL.md 的技能資料夾", + "skills.workspace.header.installingSkill": "安裝中", + "skills.workspace.hero.title": "為你精選的職場技能", + "skills.workspace.hero.description": "涵蓋寫作、效率、設計、資料分析等多種場景,一鍵安裝。", + "skills.workspace.view.store": "技能廣場", + "skills.workspace.view.builtin": "內建", + "skills.workspace.view.installed": "使用者安裝", + "skills.workspace.filter.all": "全部", + "skills.workspace.sort.hot": "排序:熱門", + "skills.workspace.store.other": "其他技能 · {{count}}", + "skills.workspace.builtin.title": "內建技能", + "skills.workspace.builtin.subtitle": "Lime 自帶的技能。開關只控制是否自動匹配,不影響你繼續使用 Lime。", + "skills.workspace.builtin.empty": "可以嘗試重新整理,或換個關鍵字繼續找。", + "skills.workspace.installed.title": "使用者安裝", + "skills.workspace.installed.subtitle": "你安裝的技能。自動載入、手動使用、解除安裝是三個獨立操作。", "skills.workspace.header.viewAll": "查看全部", - "skills.workspace.installedSkill.defaultPromise": "當你需要複用這個 Skill 時使用。", + "skills.workspace.installedSkill.action.use": "使用", + "skills.workspace.installedSkill.action.uninstall": "解除安裝", + "skills.workspace.installedSkill.action.uninstalling": "解除安裝中", + "skills.workspace.installedSkill.uninstallSuccess": "已解除安裝「{{name}}」", + "skills.workspace.installedSkill.uninstallFailed": "解除安裝失敗:{{message}}", + "skills.workspace.installedSkill.defaultPromise": "當你需要重用這個技能時使用。", "skills.workspace.installedSkill.entryBanner": "已帶著 Skill「{{name}}」回到生成,接著把這輪做下去就行。", "skills.workspace.installedSkill.entryBannerWithReplay": "已帶著 Skill「{{name}}」和上次目標回到生成,接著把這輪做下去就行。", - "skills.workspace.installedSkill.fallbackOutputHint": "帶著這個 Skill 進入生成", + "skills.workspace.installedSkill.fallbackOutputHint": "帶著這個技能回到首頁輸入框", "skills.workspace.installedSkill.fallbackRequiredInputs": "在對話裡繼續補充目標與限制", "skills.workspace.installedSkill.outputPrefix": "交付:", "skills.workspace.installedSkill.requiredPrefix": "需要:", @@ -1703,5 +1980,7 @@ "agentChat.threadReliability.view.recommendation.interrupting": "正在停止目前執行,請等待執行階段回填最終狀態", "agentChat.threadReliability.view.recommendation.interrupted": "目前執行已被執行階段確認中斷", "agentChat.threadReliability.view.recommendation.nextQueuedTurn": "可繼續排隊回合:{{title}}", - "agentChat.threadReliability.view.recommendation.retryable": "最近結果支援重試,可恢復或重新發起新回合" + "agentChat.threadReliability.view.recommendation.retryable": "最近結果支援重試,可恢復或重新發起新回合", + "skills.inputBadge.remove": "移除 @{{name}}", + "skills.workspace.view.tabsLabel": "技能分頁" } diff --git a/src/lib/api/agentProtocol.test.ts b/src/lib/api/agentProtocol.test.ts index bfbb70ccd..8bd8c8800 100644 --- a/src/lib/api/agentProtocol.test.ts +++ b/src/lib/api/agentProtocol.test.ts @@ -90,6 +90,28 @@ describe("agentProtocol", () => { }); }); + it("应把编排 provider_config 透传到 runtime turn_config", () => { + expect( + createSubmitTurnRequestFromAgentOp({ + type: "user_input", + text: "@Nanobanana Pro 生成一张广州塔春天照片", + sessionId: "session-image-1", + eventName: "aster_stream_image", + preferences: { + providerConfig: { + provider_id: "deepseek", + provider_name: "deepseek", + model_name: "deepseek-v4-pro", + }, + }, + }).turn_config?.provider_config, + ).toEqual({ + provider_id: "deepseek", + provider_name: "deepseek", + model_name: "deepseek-v4-pro", + }); + }); + it("缺少 workspaceId 时不应在 runtime submit request 中生成 workspace_id", () => { expect( createSubmitTurnRequestFromAgentOp({ diff --git a/src/lib/api/agentProtocol.ts b/src/lib/api/agentProtocol.ts index d82ed609c..e23ec4c31 100644 --- a/src/lib/api/agentProtocol.ts +++ b/src/lib/api/agentProtocol.ts @@ -16,6 +16,7 @@ import type { } from "./agentExecutionRuntime"; import type { AsterApprovalPolicy, + AsterProviderConfig, AgentRuntimeSubmitTurnRequest, AgentRuntimeWebSearchMode, AsterExecutionStrategy, @@ -770,6 +771,7 @@ export type AgentEvent = | AgentEventError; export interface AgentUserPreferences { + providerConfig?: AsterProviderConfig; providerPreference?: string; modelPreference?: string; thinking?: boolean; @@ -1532,6 +1534,9 @@ export function createSubmitTurnRequestFromAgentOp( turn_id: op.turnId, images: op.images, turn_config: { + ...(preferences?.providerConfig + ? { provider_config: preferences.providerConfig } + : {}), provider_preference: preferences?.providerPreference, model_preference: preferences?.modelPreference, thinking_enabled: preferences?.thinking, diff --git a/src/lib/base-setup/seededCommandPackage.test.ts b/src/lib/base-setup/seededCommandPackage.test.ts index b8e838385..4bedf1d00 100644 --- a/src/lib/base-setup/seededCommandPackage.test.ts +++ b/src/lib/base-setup/seededCommandPackage.test.ts @@ -10,7 +10,7 @@ describe("seededCommandPackage", () => { expect(pkg.id).toBe("lime-seeded-command-catalog"); expect(pkg.version).toBe(SEEDED_SERVICE_SKILL_CATALOG_VERSION); - expect(pkg.catalogProjections).toHaveLength(39); + expect(pkg.catalogProjections).toHaveLength(40); expect(pkg.bindingProfiles.map((profile) => profile.id)).toEqual( expect.arrayContaining(["agent-turn-instant", "native-skill-instant"]), ); @@ -25,7 +25,7 @@ describe("seededCommandPackage", () => { getSeededServiceSkillCatalog().items, ); - expect(entries).toHaveLength(39); + expect(entries).toHaveLength(40); expect(entries).toEqual( expect.arrayContaining([ expect.objectContaining({ @@ -56,6 +56,21 @@ describe("seededCommandPackage", () => { detailKind: "media_detail", }), }), + expect.objectContaining({ + id: "command:image_generate_nanobanana_pro", + commandKey: "image_generate_nanobanana_pro", + binding: { + skillId: "image_generate", + executionKind: "task_queue", + }, + renderContract: expect.objectContaining({ + resultKind: "image_gallery", + detailKind: "media_detail", + }), + triggers: expect.arrayContaining([ + { mode: "mention", prefix: "@Nanobanana Pro" }, + ]), + }), expect.objectContaining({ id: "command:voice_runtime", commandKey: "voice_runtime", diff --git a/src/lib/base-setup/seededCommandPackage.ts b/src/lib/base-setup/seededCommandPackage.ts index 598fa1937..06400bb5d 100644 --- a/src/lib/base-setup/seededCommandPackage.ts +++ b/src/lib/base-setup/seededCommandPackage.ts @@ -85,7 +85,7 @@ const SEEDED_COMMAND_PROJECTION_SPECS: SeededCommandProjectionSpec[] = [ commandKey: "image_generate", title: "配图", summary: "根据文字描述生成新的图片结果。", - aliases: ["image", "img", "vision 1", "图片", "生图"], + aliases: ["image", "img", "vision 1", "nanobanana", "图片", "生图"], trigger: "@配图", triggerHints: ["@Vision 1"], category: "图像生成", @@ -96,6 +96,27 @@ const SEEDED_COMMAND_PROJECTION_SPECS: SeededCommandProjectionSpec[] = [ }, commandRenderContract: COMMAND_IMAGE_GALLERY_CONTRACT, }, + { + commandKey: "image_generate_nanobanana_pro", + title: "Nanobanana Pro", + summary: "使用 Nanobanana Pro 生成新的图片结果。", + aliases: [ + "nanobanana", + "nano banana", + "nano-banana-pro", + "banana pro", + "图片", + "生图", + ], + trigger: "@Nanobanana Pro", + category: "图像生成", + outputHint: "Nanobanana Pro 图片结果集", + commandBinding: { + skillId: "image_generate", + executionKind: "task_queue", + }, + commandRenderContract: COMMAND_IMAGE_GALLERY_CONTRACT, + }, { commandKey: "image_storyboard", title: "分镜", diff --git a/src/lib/dev-bridge/http-client.test.ts b/src/lib/dev-bridge/http-client.test.ts index c88d8df84..4eb71c936 100644 --- a/src/lib/dev-bridge/http-client.test.ts +++ b/src/lib/dev-bridge/http-client.test.ts @@ -712,6 +712,76 @@ describe("http-client", () => { taskUnlisten(); }); + it("事件流如果在绑定 onopen 前已经打开,也应立即完成监听注册", async () => { + class MockEventSource { + static instances: MockEventSource[] = []; + + onopen: (() => void) | null = null; + onerror: ((error: Event) => void) | null = null; + onmessage: ((event: MessageEvent) => void) | null = null; + readyState = 1; + readonly close = vi.fn(() => { + this.readyState = 2; + }); + + constructor(readonly url: string) { + MockEventSource.instances.push(this); + } + } + + const fetchMock = vi + .fn() + .mockResolvedValue(new Response(null, { status: 200 })); + vi.stubGlobal("fetch", fetchMock); + vi.stubGlobal( + "EventSource", + MockEventSource as unknown as typeof EventSource, + ); + + const unlistenPromise = listenViaHttpEvent("aster_stream_test", vi.fn()); + await vi.advanceTimersByTimeAsync(0); + + await expect(unlistenPromise).resolves.toEqual(expect.any(Function)); + expect(MockEventSource.instances).toHaveLength(1); + expect(MockEventSource.instances[0]?.close).not.toHaveBeenCalled(); + }); + + it("事件流已打开但浏览器遗漏 onopen 时,超时检查应按已连接处理", async () => { + class MockEventSource { + static instances: MockEventSource[] = []; + + onopen: (() => void) | null = null; + onerror: ((error: Event) => void) | null = null; + onmessage: ((event: MessageEvent) => void) | null = null; + readyState = 0; + readonly close = vi.fn(() => { + this.readyState = 2; + }); + + constructor(readonly url: string) { + MockEventSource.instances.push(this); + } + } + + const fetchMock = vi + .fn() + .mockResolvedValue(new Response(null, { status: 200 })); + vi.stubGlobal("fetch", fetchMock); + vi.stubGlobal( + "EventSource", + MockEventSource as unknown as typeof EventSource, + ); + + const unlistenPromise = listenViaHttpEvent("aster_stream_test", vi.fn()); + await vi.advanceTimersByTimeAsync(0); + const source = MockEventSource.instances[0]!; + source.readyState = 1; + await vi.advanceTimersByTimeAsync(10_000); + + await expect(unlistenPromise).resolves.toEqual(expect.any(Function)); + expect(source.close).not.toHaveBeenCalled(); + }); + it("事件流本地冷启动超过 1.5 秒但未超出桥接窗口时不应误判失败", async () => { class MockEventSource { static instances: MockEventSource[] = []; diff --git a/src/lib/dev-bridge/http-client.ts b/src/lib/dev-bridge/http-client.ts index e85bea1e4..9fdc8ab27 100644 --- a/src/lib/dev-bridge/http-client.ts +++ b/src/lib/dev-bridge/http-client.ts @@ -524,6 +524,10 @@ async function createBridgeEventConnection( if (hasOpened || !hubActive) { return; } + if (source.readyState === 1) { + handleOpen(); + return; + } markBridgeUnavailable(); hubActive = false; closeBridgeEventConnection(connection); @@ -551,10 +555,13 @@ async function createBridgeEventConnection( dispatchBridgeEventMessage(events, parsed); }; - source.onopen = () => { + const handleOpen = () => { if (!hubActive) { return; } + if (hasOpened) { + return; + } hasOpened = true; reconnectWarningShown = false; markBridgeHealthy(); @@ -563,6 +570,10 @@ async function createBridgeEventConnection( settleOpen = null; settleOpenError = null; }; + source.onopen = handleOpen; + if (source.readyState === 1) { + handleOpen(); + } source.onerror = (error) => { if (!hubActive) { diff --git a/src/lib/dev-bridge/mockPriorityCommands.test.ts b/src/lib/dev-bridge/mockPriorityCommands.test.ts index bff4ae2f8..847e8f986 100644 --- a/src/lib/dev-bridge/mockPriorityCommands.test.ts +++ b/src/lib/dev-bridge/mockPriorityCommands.test.ts @@ -150,6 +150,22 @@ describe("mockPriorityCommands", () => { ); }); + it("Skills 管理命令在浏览器模式下禁止静默退回空 mock", () => { + expect(shouldPreferMockInBrowser("get_local_skills_for_app")).toBe(false); + expect( + shouldDisallowMockFallbackInBrowser("get_local_skills_for_app"), + ).toBe(true); + expect(shouldDisallowMockFallbackInBrowser("refresh_skill_cache")).toBe( + true, + ); + expect(shouldDisallowMockFallbackInBrowser("uninstall_skill_for_app")).toBe( + true, + ); + expect( + shouldDisallowMockFallbackInBrowser("import_local_skill_for_app"), + ).toBe(true); + }); + it("运行时真相事件在浏览器模式下禁止静默退回 mock", () => { expect( shouldDisallowMockEventFallbackInBrowser("voice-model-download-progress"), diff --git a/src/lib/dev-bridge/mockPriorityCommands.ts b/src/lib/dev-bridge/mockPriorityCommands.ts index f2ba7d546..f89045b2c 100644 --- a/src/lib/dev-bridge/mockPriorityCommands.ts +++ b/src/lib/dev-bridge/mockPriorityCommands.ts @@ -135,6 +135,19 @@ const bridgeTruthCommands = new Set([ "capability_draft_submit_approval_session_inputs", "capability_draft_execute_controlled_get", "agent_runtime_list_workspace_skill_bindings", + "get_skills_for_app", + "get_local_skills_for_app", + "install_skill_for_app", + "uninstall_skill_for_app", + "get_skill_repos", + "add_skill_repo", + "remove_skill_repo", + "get_installed_lime_skills", + "refresh_skill_cache", + "inspect_local_skill_for_app", + "create_skill_scaffold_for_app", + "import_local_skill_for_app", + "inspect_remote_skill", "knowledge_import_source", "knowledge_compile_pack", "knowledge_list_packs", diff --git a/src/lib/tauri-mock/core.ts b/src/lib/tauri-mock/core.ts index 2a30adad1..5341923fb 100644 --- a/src/lib/tauri-mock/core.ts +++ b/src/lib/tauri-mock/core.ts @@ -8093,6 +8093,7 @@ const defaultMocks: Record = { add_skill_repo: () => ({ success: true }), remove_skill_repo: () => ({ success: true }), get_installed_lime_skills: () => [], + refresh_skill_cache: () => true, inspect_local_skill_for_app: () => ({ content: "# Mock Skill", metadata: {}, diff --git a/src/types/page.ts b/src/types/page.ts index 0e08edddd..1dbff78e4 100644 --- a/src/types/page.ts +++ b/src/types/page.ts @@ -101,6 +101,8 @@ export interface AgentPageParams { initialPendingServiceSkillLaunch?: AgentPendingServiceSkillLaunchParams; /** 进入 Agent 后优先恢复一条输入能力路由 */ initialInputCapability?: AgentInitialInputCapabilityParams; + /** 仅把输入能力放进首页输入框,不直接切进会话工作区 */ + preferHomeForInitialInputCapability?: boolean; /** 从资料管理进入 Agent 时预启用的项目资料 */ initialKnowledgePackSelection?: AgentInitialKnowledgePackSelectionParams; /** 进入 Agent 后自动打开项目内某个文件 */ @@ -160,6 +162,7 @@ export interface SkillsPageParams { initialScaffoldRequestKey?: number; creationProjectId?: string; highlightCapabilityDraftId?: string; + initialView?: "store" | "builtin" | "installed" | "discover" | "manage"; } export interface KnowledgePageParams { diff --git a/src/types/settings.ts b/src/types/settings.ts index 5ad6d3956..785bac59b 100644 --- a/src/types/settings.ts +++ b/src/types/settings.ts @@ -78,7 +78,6 @@ export const SETTINGS_GROUPS: Record = { ], [SettingsGroupKey.Agent]: [ SettingsTabs.Providers, - SettingsTabs.Skills, SettingsTabs.MediaServices, ], [SettingsGroupKey.System]: [