mirror of
https://github.com/aiclientproxy/proxycast.git
synced 2026-09-24 23:10:56 +08:00
chore: include full workspace in v1.5.1
This commit is contained in:
+19
-17
@@ -2,32 +2,34 @@
|
||||
|
||||
### ✨ 主要更新
|
||||
|
||||
- `aster-rust` 依赖已从 `v0.27.1` 升级到 `v0.27.2`
|
||||
- `src-tauri/Cargo.toml` 的远程 git tag 引用已同步到 `v0.27.2`
|
||||
- 应用与 CLI 发布版本同步提升到 `1.5.1`,用于标记这次依赖补丁发布
|
||||
|
||||
### ⚠️ 发布与兼容性说明
|
||||
|
||||
- 本次发布 tag 为 `v1.5.1`,应用内版本号保持为 `1.5.1`
|
||||
- `@limecloud/lime-cli@1.5.1` 要求 `Node >= 18`,支持 `darwin / linux / win32` 与 `x64 / arm64`
|
||||
- 本次为补丁发布,主要目的是对齐 `aster-rust v0.27.2`,不额外引入新的 Lime 功能面
|
||||
- 本地 `.cargo/config.toml` patch override 仍仅用于开发联调,不属于发布事实源
|
||||
- 本次 `v1.5.1` 已收口当前工作区全部改动,核心集中在 Agent 聊天工作台、General Workbench、Service Skill、Team Workspace、Artifact / Timeline 展示与输入发送主链
|
||||
- `src/components/agent/**`、`src/components/workspace/**`、`src/lib/api/**`、`src/features/browser-runtime/**`、`src/components/settings-v2/**` 一批界面、运行时与回归测试已一并进入本次发布
|
||||
- 浏览器运行时、现有会话桥接、工具展示、团队协作、项目选择、技能目录、工作台工具命令与内容同步相关边界已同步更新
|
||||
- 工程文档 `docs/aiprompts/commands.md`、`playwright-e2e.md`、`quality-workflow.md` 已随当前实现一起更新
|
||||
|
||||
### 🔗 依赖与版本同步
|
||||
|
||||
- 应用版本已同步提升到 `1.5.1`,覆盖 `package.json`、`src-tauri/Cargo.toml`、`src-tauri/tauri.conf.json`、`src-tauri/tauri.conf.headless.json`
|
||||
- `packages/lime-cli-npm/package.json` 与 README 发布示例已同步更新到 `1.5.1`
|
||||
- 本地 `package-lock.json` 也已同步到 `1.5.1`,用于版本一致性校验
|
||||
- `src-tauri/Cargo.lock` 已刷新,`aster-core` / `aster-models` 已对齐到 `0.27.2`
|
||||
- `aster-rust` 依赖已从 `v0.27.1` 升级到 `v0.27.2`
|
||||
- `src-tauri/Cargo.toml` 的远程 git tag 引用已同步到 `v0.27.2`
|
||||
- 应用与 CLI 发布版本保持为 `1.5.1`
|
||||
- 应用版本入口已对齐到 `1.5.1`,覆盖 `package.json`、`src-tauri/Cargo.toml`、`src-tauri/tauri.conf.json`、`src-tauri/tauri.conf.headless.json`
|
||||
- `packages/lime-cli-npm/package.json`、README 发布示例与本地 `package-lock.json` 已同步到 `1.5.1`
|
||||
- `src-tauri/Cargo.lock` 中 `aster-core` / `aster-models` 已对齐到 `0.27.2`
|
||||
|
||||
### 🧪 发布前校验
|
||||
### ⚠️ 发布说明
|
||||
|
||||
- 本次发布 tag 仍为 `v1.5.1`
|
||||
- 因为要把当前工作区全部改动统一纳入同一补丁版本,本次会更新已经存在的 `v1.5.1` Git tag 与 GitHub Release 指向
|
||||
- 当前公开 release note 已按这次完整发布内容刷新
|
||||
|
||||
### 🧪 当前校验
|
||||
|
||||
- `cargo update --manifest-path src-tauri/Cargo.toml`
|
||||
- `npm run verify:app-version`
|
||||
- Git 提交前的仓库 AI 验证会随提交钩子执行
|
||||
|
||||
### 📝 文档同步
|
||||
|
||||
- 发布说明已切换到当前这次 `v1.5.1` 补丁版内容,可直接作为 GitHub Release note 使用
|
||||
- 发布说明已更新为当前这次完整的 `v1.5.1` 内容,可直接作为 GitHub Release note 使用
|
||||
|
||||
---
|
||||
|
||||
|
||||
@@ -88,7 +88,7 @@
|
||||
|
||||
- `CharacterMention`、`builtinCommands`、场景 slash 补全不得再各自维护一套业务命令静态常量
|
||||
- 服务端尚未返回 `entries` 时,允许网关层从 legacy `items` 兼容投影出 `entries`
|
||||
- 客户端必须保留 seeded fallback,不能因为服务端暂时不可用就让 `@配图`、`@PPT`、`@表单`、`@网页`、`@代码`、`@发布`、`@搜索`、`@深搜`、`@研报`、`@站点搜索`、`@读PDF`、`@总结`、`@翻译`、`@分析`、`@转写` 这类主链入口失能
|
||||
- 客户端必须保留 seeded fallback,不能因为服务端暂时不可用就让 `@配图`、`@配音`、`@PPT`、`@表单`、`@网页`、`@代码`、`@发布`、`@搜索`、`@深搜`、`@研报`、`@站点搜索`、`@读PDF`、`@总结`、`@翻译`、`@分析`、`@转写` 这类主链入口失能
|
||||
- `src/components/agent/chat/commands/catalog.ts` 只继续承接 Lime 本地 / Codex 原生命令;产品型 `/` 场景不应再长期硬编码在这里
|
||||
- 若服务端下发的 `renderContract` 超出 Lime 当前支持范围,优先由服务端回退到已支持类型,客户端也必须退化到通用 timeline / artifact 展示
|
||||
|
||||
@@ -225,6 +225,10 @@ Skill 执行链路同样遵循单一命令边界。当前前端入口为 `src/li
|
||||
|
||||
- 工作流入口型命令:`@发布` / `@publish` / `@发文` / `@投稿` 在 `src/components/agent/chat/workspace/useWorkspaceSendActions.ts` 中保留原始用户文本展示,但把实际 dispatch 改写到现有 `/content_post_with_cover ...` 主链,并把结构化 `publish_command` 写入 `request_metadata.harness.publish_command`。当前实现优先复用已有 `content_post_with_cover` 发布工作流、`content-posts/*.md` / `*.publish-pack.json` 产物链,以及 `detectBrowserTaskRequirement(...)` 推导出的浏览器门禁,而不是再发明新的 `publish_task` 协议。若输入里已明确平台后台,如微信公众号后台,必须继续写入 `browser_requirement=required_with_user_step` 与平台 launch URL;若只是整理发布稿而未指定平台,则允许先在同一工作流里生成发布稿与发布前检查,不强行要求浏览器。后续若统一 agent/workflow runtime 成熟,可以把 `@发布` 从当前 slash workflow 迁走,但在那之前不得同时维护第二套发布入口真相。
|
||||
|
||||
`Claw` 的纯文本配音命令也应沿同一条服务型技能主链收敛:
|
||||
|
||||
- Agent 驱动的配音命令:`@配音` / `@voice` / `@dubbing` / `@dub` 在 `src/components/agent/chat/workspace/useWorkspaceSendActions.ts` 中保留原始用户文本发送。聊天发送边界会优先从当前 `serviceSkills` / seeded fallback 中解析配音能力(当前兜底为 `cloud-video-dubbing`),并把结构化 `service_scene_launch` 写入 `request_metadata.harness.service_scene_launch`,其中固定 `scene_key=voice_runtime`、`entry_source=at_voice_command`,同时注入 OEM `scene_base_url / tenant_id / session_token` 运行时上下文。Rust 侧 `runtime_turn.rs`、`prompt_context.rs` 与 `tool_runtime/service_skill_tools.rs` 会把当前 turn 切到 `workbench`,并强约束首刀优先调用 `lime_run_service_skill`,由 OEM scene runtime 负责 run / poll。当前上下文缺少明确配音要求时,允许 Agent 最多追问 1 个关键问题;但不能退回普通聊天解释、不能伪造“配音已完成”,也不能重新回流到旧的本地 TTS 测试命令。
|
||||
|
||||
这些命令除了 Tauri `generate_handler!` 之外,也必须继续保持 DevBridge dispatcher 已桥接,避免浏览器模式、headless smoke 或 Playwright 续测时回退成 unknown command。
|
||||
|
||||
自动化设置链路同样遵循这条路径。当前主入口为 `src/lib/api/automation.ts`,统一承接:
|
||||
|
||||
@@ -259,6 +259,15 @@ npm run test:contracts
|
||||
5. 等待工作流继续推进后,确认产物仍落在现有 `content-posts/*.md` / `*.publish-pack.json` 主链,而不是另一套发布任务协议
|
||||
6. 刷新页面或切换会话后再返回原话题,确认发布稿与发布包仍可恢复
|
||||
|
||||
### Claw `@配音` 云端技能主链验证
|
||||
|
||||
1. 在 `Claw` 对话框输入 `@配音 目标语言: 英文 风格: 科技感 给这个新品视频做一版发布配音稿`
|
||||
2. 确认聊天区保留原始 `@配音 ...` 文本,而不是被改写成 slash scene 或其它内部协议
|
||||
3. 如页面可查看发送详情或运行时摘要,确认 `request_metadata.harness.service_scene_launch.service_scene_run.scene_key=voice_runtime`,并且 `skill_id` 已绑定到当前可用的配音 service skill
|
||||
4. 确认首刀进入 `lime_run_service_skill` 主链,而不是普通聊天解释、站点型 `lime_site_run`,或旧的本地 TTS 测试命令
|
||||
5. 如果当前 OEM 云端会话缺失,确认界面明确提示需要登录 / 注入会话,而不是伪造“配音已完成”
|
||||
6. 刷新页面或切换会话后再返回原话题,确认该配音任务的时间线与最近使用状态仍可恢复
|
||||
|
||||
### Claw `@读PDF` Prompt Skill 验证
|
||||
|
||||
1. 在 `Claw` 对话框输入 `@读PDF /tmp/agent-report.pdf 提炼三点结论并标注关键证据`
|
||||
|
||||
@@ -204,6 +204,7 @@ npm run bridge:health -- --timeout-ms 120000
|
||||
- 检查纯文本 `Claw @表单` 是否已经走 `原始用户消息 -> harness.form_skill_launch -> Agent 首刀 Skill(form_generate) -> ```a2ui simple form JSON` 主链,而不是退回普通聊天字段建议、卡在 `ToolSearch / WebSearch / Read / Glob / Grep` 这类通用工具偏航,或回流成单文件 HTML 表单原型;同时确认 render contract 已收敛为 `form + json`
|
||||
- 检查纯文本 `Claw @代码` 是否已经走 `原始用户消息 -> harness.code_command + preferred_team_preset_id -> code_orchestrated -> code_execution / tools / team runtime` 主链,而不是继续停留在普通聊天、没有打开 `task/subagent` 偏好,或把代码任务改写成另一套 prompt / workflow 旁路
|
||||
- 检查纯文本 `Claw @发布` 是否已经走 `原始用户消息 -> displayContent 保留 -> dispatch /content_post_with_cover -> content_post workflow` 主链,而不是直接把 `@发布` 文本原样当普通聊天发送,或重新造一套 `publish_task` 协议;同时确认平台后台类输入会继续触发 `browser_requirement`
|
||||
- 检查纯文本 `Claw @配音` 是否已经走 `原始用户消息 -> harness.service_scene_launch(scene_key=voice_runtime) -> Agent 首刀 lime_run_service_skill -> OEM scene run/timeline` 主链,而不是退回普通聊天解释、误走站点型 `service_skill_launch`,或重新回流旧的本地 TTS 测试命令;同时确认 `skill_id`、OEM runtime 上下文与最近使用记录都能写回
|
||||
- 检查产品型 `/scene-key` 是否已经走 `原始用户消息 -> harness.service_scene_launch -> Agent 首刀 lime_run_service_skill -> OEM run/timeline` 主链,而不是前端直接调用云端 run API
|
||||
- 如果某个 `/scene-key` 绑定的是 `site_adapter` 型技能,还要额外检查 `scene -> linkedSkillId -> 完整 ServiceSkill 目录 -> harness.service_skill_launch` 这条绑定链是否仍然成立,避免首页隐藏 site skill 后 slash scene 变成“目录可见但执行找不到 skill”
|
||||
- 如果某个 `site_adapter` 结果开始返回 `markdown_bundle`,还要确认保存链会把 Markdown、图片和 `meta.json` 一起落到项目导出目录,并把重写后的相对图片路径写回内容 metadata;同时确认聊天轻卡或 tool timeline 能显示项目目录、Markdown 路径和图片数量,不能只把远程图片 URL 或临时 DOM 文本留在聊天结果里
|
||||
@@ -245,6 +246,7 @@ npm run bridge:health -- --timeout-ms 120000
|
||||
- 修改 `@PPT` parser、`useWorkspaceSendActions`、`runtime_turn`、`presentation_skill_launch`、`presentation_generate` skill 或演示稿 artifact 预览链路,尤其是调整 `Claw @PPT -> harness.presentation_skill_launch -> Agent 首刀 Skill(presentation_generate) -> write_file Markdown artifact` 主链
|
||||
- 修改 `@代码` parser、`useWorkspaceSendActions`、mention builtin command 或 `code_orchestrated` 发送边界,尤其是调整 `Claw @代码 -> harness.code_command -> code_orchestrated -> tools / team runtime` 主链
|
||||
- 修改 `@发布` parser、`useWorkspaceSendActions`、content post workflow 入口或浏览器门禁推导,尤其是调整 `Claw @发布 -> displayContent/raw -> /content_post_with_cover -> publish workflow` 主链
|
||||
- 修改 `@配音` parser、`useWorkspaceSendActions`、`service_scene_launch` 组装、OEM runtime 上下文注入或 `lime_run_service_skill` 配音场景接线,尤其是调整 `Claw @配音 -> harness.service_scene_launch(scene_key=voice_runtime) -> lime_run_service_skill -> OEM run/timeline` 主链
|
||||
- 修改 `/scene-key` 解析、`serviceSkillSceneLaunch`、`useWorkspaceSendActions`、`runtime_turn`、`prompt_context`、`lime_run_service_skill` 或 `client/skills` scene 目录协议,尤其是调整 `Claw /scene-key -> harness.service_scene_launch -> Agent 首刀 lime_run_service_skill -> OEM run/timeline` 主链
|
||||
- 修改 `src/lib/dev-bridge/`
|
||||
- 修改 `src/lib/tauri-mock/`
|
||||
@@ -403,6 +405,13 @@ npm run bridge:health -- --timeout-ms 120000
|
||||
- 若浏览器门禁或 harness 协议继续扩散,再补 `npm run test:contracts`
|
||||
- 若 GUI 主路径受影响,再补 `npm run verify:gui-smoke`
|
||||
|
||||
如果本轮修改了 `Claw @配音` 或配音服务型技能接线,最低校验至少包含:
|
||||
|
||||
- `npx vitest run "src/components/agent/chat/utils/voiceWorkbenchCommand.test.ts" "src/components/agent/chat/workspace/useWorkspaceSendActions.test.tsx" "src/components/agent/chat/skill-selection/CharacterMention.test.tsx" "src/lib/api/skillCatalog.test.ts" "src/lib/api/serviceSkills.test.ts"`
|
||||
- 如有改动扩散到 `service_scene_launch` runtime、`lime_run_service_skill`、OEM scene run/poll 或云端结果回流,再补对应 `runtime_turn` / `prompt_context` / `tool_runtime/service_skill_tools` 定向测试
|
||||
- 若 `service_scene_launch` / harness 协议继续扩散,再补 `npm run test:contracts`
|
||||
- 若 GUI 主路径受影响,再补 `npm run verify:gui-smoke`
|
||||
|
||||
如果本轮修改了 Provider 模型真相源或设置页中的“支持的模型”展示逻辑,还应额外确认:
|
||||
|
||||
- 资源索引损坏时,GUI 会明确提示“模型真相源异常”
|
||||
|
||||
+4
-10
@@ -8,12 +8,7 @@
|
||||
* _需求: 2.2, 3.2, 5.2_
|
||||
*/
|
||||
|
||||
import React, {
|
||||
Suspense,
|
||||
lazy,
|
||||
useState,
|
||||
useCallback,
|
||||
} from "react";
|
||||
import React, { Suspense, lazy, useState, useCallback } from "react";
|
||||
import styled from "styled-components";
|
||||
import { withI18nPatch } from "./i18n/withI18nPatch";
|
||||
import { AppPageContent } from "./components/AppPageContent";
|
||||
@@ -211,10 +206,9 @@ function AppContent() {
|
||||
onOpenBrowserConnectorSettings: handleOpenBrowserConnectorSettings,
|
||||
});
|
||||
|
||||
const { error: registryError, refresh: _refreshRegistry } =
|
||||
useRelayRegistry({
|
||||
autoLoad: hasTauriDesktopRuntime,
|
||||
});
|
||||
const { error: registryError, refresh: _refreshRegistry } = useRelayRegistry({
|
||||
autoLoad: hasTauriDesktopRuntime,
|
||||
});
|
||||
useAppStartupEffects({
|
||||
currentPage,
|
||||
registryError,
|
||||
|
||||
@@ -140,20 +140,26 @@ describe("AppPageContent", () => {
|
||||
const container = renderContent("channels");
|
||||
await flushEffects();
|
||||
|
||||
expect(container.querySelector('[data-testid="im-config-page"]')).not.toBeNull();
|
||||
expect(
|
||||
container.querySelector('[data-testid="im-config-page"]'),
|
||||
).not.toBeNull();
|
||||
});
|
||||
|
||||
it("video 页面应渲染现役视频工作台入口", async () => {
|
||||
const container = renderContent("video");
|
||||
await flushEffects();
|
||||
|
||||
expect(container.querySelector('[data-testid="video-page"]')).not.toBeNull();
|
||||
expect(
|
||||
container.querySelector('[data-testid="video-page"]'),
|
||||
).not.toBeNull();
|
||||
});
|
||||
|
||||
it("settings 页面应渲染设置页入口", async () => {
|
||||
const container = renderContent("settings");
|
||||
await flushEffects();
|
||||
|
||||
expect(container.querySelector('[data-testid="settings-page"]')).not.toBeNull();
|
||||
expect(
|
||||
container.querySelector('[data-testid="settings-page"]'),
|
||||
).not.toBeNull();
|
||||
});
|
||||
});
|
||||
|
||||
@@ -25,12 +25,10 @@ interface MountedSidebar {
|
||||
const mountedSidebars: MountedSidebar[] = [];
|
||||
const APP_SIDEBAR_COLLAPSED_STORAGE_KEY = "lime.app-sidebar.collapsed";
|
||||
|
||||
function mountSidebar(
|
||||
options?: {
|
||||
currentPage?: Page;
|
||||
currentPageParams?: PageParams;
|
||||
},
|
||||
): MountedSidebar["container"] {
|
||||
function mountSidebar(options?: {
|
||||
currentPage?: Page;
|
||||
currentPageParams?: PageParams;
|
||||
}): MountedSidebar["container"] {
|
||||
const container = document.createElement("div");
|
||||
document.body.appendChild(container);
|
||||
const root = createRoot(container);
|
||||
@@ -91,7 +89,9 @@ describe("AppSidebar", () => {
|
||||
expect(
|
||||
container.querySelector('button[aria-label="展开导航栏"]'),
|
||||
).not.toBeNull();
|
||||
expect(localStorage.getItem(APP_SIDEBAR_COLLAPSED_STORAGE_KEY)).toBe("true");
|
||||
expect(localStorage.getItem(APP_SIDEBAR_COLLAPSED_STORAGE_KEY)).toBe(
|
||||
"true",
|
||||
);
|
||||
});
|
||||
|
||||
it("新建任务页应自动展开导航栏,不沿用上一个页面的折叠状态", async () => {
|
||||
@@ -107,7 +107,9 @@ describe("AppSidebar", () => {
|
||||
expect(
|
||||
container.querySelector('button[aria-label="折叠导航栏"]'),
|
||||
).not.toBeNull();
|
||||
expect(localStorage.getItem(APP_SIDEBAR_COLLAPSED_STORAGE_KEY)).toBe("false");
|
||||
expect(localStorage.getItem(APP_SIDEBAR_COLLAPSED_STORAGE_KEY)).toBe(
|
||||
"false",
|
||||
);
|
||||
});
|
||||
|
||||
it("旧导航配置未包含能力入口时也应显示固定能力分组和核心能力项", async () => {
|
||||
@@ -142,7 +144,9 @@ describe("AppSidebar", () => {
|
||||
|
||||
expect(container.textContent).toContain("能力");
|
||||
expect(
|
||||
container.querySelector('button[aria-label="消息渠道"][aria-current="page"]'),
|
||||
container.querySelector(
|
||||
'button[aria-label="消息渠道"][aria-current="page"]',
|
||||
),
|
||||
).not.toBeNull();
|
||||
});
|
||||
|
||||
|
||||
@@ -19,11 +19,7 @@ import {
|
||||
} from "lucide-react";
|
||||
import * as LucideIcons from "lucide-react";
|
||||
import { getPluginsForSurface, PluginUIInfo } from "@/lib/api/pluginUI";
|
||||
import {
|
||||
AgentPageParams,
|
||||
Page,
|
||||
PageParams,
|
||||
} from "@/types/page";
|
||||
import { AgentPageParams, Page, PageParams } from "@/types/page";
|
||||
import { getConfig } from "@/lib/api/appConfig";
|
||||
import {
|
||||
buildClawAgentParams,
|
||||
@@ -88,7 +84,9 @@ const Container = styled.aside<{
|
||||
--sidebar-border: ${({ $themeMode }) =>
|
||||
$themeMode === "dark" ? "#2d3a46" : "#e2f0e2"};
|
||||
--sidebar-divider: ${({ $themeMode }) =>
|
||||
$themeMode === "dark" ? "rgba(148, 163, 184, 0.14)" : "rgba(132, 204, 22, 0.15)"};
|
||||
$themeMode === "dark"
|
||||
? "rgba(148, 163, 184, 0.14)"
|
||||
: "rgba(132, 204, 22, 0.15)"};
|
||||
--sidebar-hover: ${({ $themeMode }) =>
|
||||
$themeMode === "dark" ? "#22303c" : "#eef7ee"};
|
||||
--sidebar-active: ${({ $themeMode }) =>
|
||||
@@ -109,13 +107,12 @@ const Container = styled.aside<{
|
||||
padding: ${({ $collapsed }) => ($collapsed ? "12px 6px" : "12px 10px")};
|
||||
position: relative;
|
||||
isolation: isolate;
|
||||
background:
|
||||
linear-gradient(
|
||||
180deg,
|
||||
var(--sidebar-surface-top) 0%,
|
||||
var(--sidebar-surface-middle) 34%,
|
||||
var(--sidebar-surface-bottom) 100%
|
||||
);
|
||||
background: linear-gradient(
|
||||
180deg,
|
||||
var(--sidebar-surface-top) 0%,
|
||||
var(--sidebar-surface-middle) 34%,
|
||||
var(--sidebar-surface-bottom) 100%
|
||||
);
|
||||
border-right: 1px solid var(--sidebar-border);
|
||||
box-shadow: 16px 0 36px -34px rgba(15, 23, 42, 0.42);
|
||||
transition:
|
||||
@@ -178,7 +175,8 @@ const UserButton = styled.button<{ $collapsed?: boolean }>`
|
||||
padding: ${({ $collapsed }) => ($collapsed ? "8px" : "8px 10px")};
|
||||
cursor: pointer;
|
||||
color: var(--sidebar-foreground);
|
||||
justify-content: ${({ $collapsed }) => ($collapsed ? "center" : "flex-start")};
|
||||
justify-content: ${({ $collapsed }) =>
|
||||
$collapsed ? "center" : "flex-start"};
|
||||
transition:
|
||||
background-color 0.18s ease,
|
||||
color 0.18s ease;
|
||||
@@ -228,7 +226,8 @@ const SearchButton = styled.button<{ $collapsed?: boolean }>`
|
||||
color: var(--sidebar-muted);
|
||||
padding: ${({ $collapsed }) => ($collapsed ? "0" : "0 10px")};
|
||||
cursor: pointer;
|
||||
justify-content: ${({ $collapsed }) => ($collapsed ? "center" : "flex-start")};
|
||||
justify-content: ${({ $collapsed }) =>
|
||||
$collapsed ? "center" : "flex-start"};
|
||||
transition:
|
||||
border-color 0.18s ease,
|
||||
background-color 0.18s ease,
|
||||
@@ -304,13 +303,16 @@ const NavButton = styled.button<{ $active?: boolean; $collapsed?: boolean }>`
|
||||
background-color 0.18s ease,
|
||||
color 0.18s ease,
|
||||
transform 0.18s ease;
|
||||
justify-content: ${({ $collapsed }) => ($collapsed ? "center" : "flex-start")};
|
||||
justify-content: ${({ $collapsed }) =>
|
||||
$collapsed ? "center" : "flex-start"};
|
||||
|
||||
&:hover {
|
||||
background: ${({ $active }) =>
|
||||
$active ? "var(--sidebar-active)" : "var(--sidebar-hover)"};
|
||||
color: ${({ $active }) =>
|
||||
$active ? "var(--sidebar-active-foreground)" : "var(--sidebar-foreground)"};
|
||||
$active
|
||||
? "var(--sidebar-active-foreground)"
|
||||
: "var(--sidebar-foreground)"};
|
||||
}
|
||||
|
||||
svg {
|
||||
@@ -332,7 +334,10 @@ const NavLabel = styled.span<{ $collapsed?: boolean }>`
|
||||
display: ${({ $collapsed }) => ($collapsed ? "none" : "inline")};
|
||||
`;
|
||||
|
||||
const NavTrailingIcon = styled.span<{ $collapsed?: boolean; $active?: boolean }>`
|
||||
const NavTrailingIcon = styled.span<{
|
||||
$collapsed?: boolean;
|
||||
$active?: boolean;
|
||||
}>`
|
||||
display: ${({ $collapsed }) => ($collapsed ? "none" : "inline-flex")};
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
@@ -387,7 +392,8 @@ const FooterArea = styled.div<{ $collapsed?: boolean }>`
|
||||
const ActionRow = styled.div<{ $collapsed?: boolean }>`
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: ${({ $collapsed }) => ($collapsed ? "center" : "space-between")};
|
||||
justify-content: ${({ $collapsed }) =>
|
||||
$collapsed ? "center" : "space-between"};
|
||||
padding: 0 2px;
|
||||
`;
|
||||
|
||||
@@ -412,7 +418,9 @@ const IconActionButton = styled.button<{ $active?: boolean }>`
|
||||
background: ${({ $active }) =>
|
||||
$active ? "var(--sidebar-active)" : "var(--sidebar-hover)"};
|
||||
color: ${({ $active }) =>
|
||||
$active ? "var(--sidebar-active-foreground)" : "var(--sidebar-foreground)"};
|
||||
$active
|
||||
? "var(--sidebar-active-foreground)"
|
||||
: "var(--sidebar-foreground)"};
|
||||
}
|
||||
|
||||
svg {
|
||||
@@ -433,7 +441,8 @@ export function AppSidebar({
|
||||
currentPageParams,
|
||||
onNavigate,
|
||||
}: AppSidebarProps) {
|
||||
const agentEntry = (currentPageParams as AgentPageParams | undefined)?.agentEntry;
|
||||
const agentEntry = (currentPageParams as AgentPageParams | undefined)
|
||||
?.agentEntry;
|
||||
const isClawTaskCenter = currentPage === "agent" && agentEntry === "claw";
|
||||
const isNewTaskHome = currentPage === "agent" && agentEntry === "new-task";
|
||||
const [collapsed, setCollapsed] = useState<boolean>(() => {
|
||||
@@ -461,7 +470,8 @@ export function AppSidebar({
|
||||
const [refreshTrigger, setRefreshTrigger] = useState(0);
|
||||
const [expandedGroupIds, setExpandedGroupIds] = useState<string[]>(() =>
|
||||
MAIN_SIDEBAR_NAV_ITEMS.filter(
|
||||
(item) => item.defaultExpanded && item.children && item.children.length > 0,
|
||||
(item) =>
|
||||
item.defaultExpanded && item.children && item.children.length > 0,
|
||||
).map((item) => item.id),
|
||||
);
|
||||
|
||||
@@ -641,7 +651,8 @@ export function AppSidebar({
|
||||
};
|
||||
|
||||
const activeGroupIds = MAIN_SIDEBAR_NAV_ITEMS.filter(
|
||||
(item) => item.children && item.children.length > 0 && isGroupActive(item),
|
||||
(item) =>
|
||||
item.children && item.children.length > 0 && isGroupActive(item),
|
||||
).map((item) => item.id);
|
||||
|
||||
if (activeGroupIds.length === 0) {
|
||||
@@ -807,7 +818,9 @@ export function AppSidebar({
|
||||
<MenuScroll>
|
||||
{filteredMainSections.map((section) => (
|
||||
<Section key={section.id} $collapsed={collapsed}>
|
||||
<SectionTitle $collapsed={collapsed}>{section.title}</SectionTitle>
|
||||
<SectionTitle $collapsed={collapsed}>
|
||||
{section.title}
|
||||
</SectionTitle>
|
||||
{section.items.map((item) => renderNavItem(item))}
|
||||
</Section>
|
||||
))}
|
||||
@@ -823,7 +836,9 @@ export function AppSidebar({
|
||||
<FooterArea $collapsed={collapsed}>
|
||||
{filteredFooterSections.map((section) => (
|
||||
<Section key={section.id} $collapsed={collapsed}>
|
||||
<SectionTitle $collapsed={collapsed}>{section.title}</SectionTitle>
|
||||
<SectionTitle $collapsed={collapsed}>
|
||||
{section.title}
|
||||
</SectionTitle>
|
||||
{section.items.map((item) => renderNavItem(item))}
|
||||
</Section>
|
||||
))}
|
||||
|
||||
@@ -223,7 +223,9 @@ export function ModalFooter({
|
||||
className?: string;
|
||||
}) {
|
||||
return (
|
||||
<div className={`shrink-0 flex flex-wrap justify-end gap-2 border-t px-6 py-4 ${className}`}>
|
||||
<div
|
||||
className={`shrink-0 flex flex-wrap justify-end gap-2 border-t px-6 py-4 ${className}`}
|
||||
>
|
||||
{children}
|
||||
</div>
|
||||
);
|
||||
|
||||
@@ -271,7 +271,9 @@ function resolveTaskPreviewArtifact(
|
||||
if (normalizedArtifactPath) {
|
||||
const matchedArtifact = messageArtifacts.find(
|
||||
(artifact) =>
|
||||
!isHiddenInternalArtifactPath(resolveArtifactProtocolFilePath(artifact)) &&
|
||||
!isHiddenInternalArtifactPath(
|
||||
resolveArtifactProtocolFilePath(artifact),
|
||||
) &&
|
||||
(areArtifactProtocolPathsEquivalent(
|
||||
resolveArtifactProtocolFilePath(artifact),
|
||||
normalizedArtifactPath,
|
||||
@@ -310,9 +312,7 @@ function normalizeVideoAspectRatio(
|
||||
}
|
||||
}
|
||||
|
||||
function normalizeVideoResolution(
|
||||
value?: string,
|
||||
): "480p" | "720p" | "1080p" {
|
||||
function normalizeVideoResolution(value?: string): "480p" | "720p" | "1080p" {
|
||||
switch (value) {
|
||||
case "480p":
|
||||
case "1080p":
|
||||
@@ -527,7 +527,9 @@ export function AgentChatWorkspace({
|
||||
let resolvedRootPath = defaultProject.rootPath;
|
||||
|
||||
try {
|
||||
const ensuredWorkspace = await ensureWorkspaceReady(defaultProject.id);
|
||||
const ensuredWorkspace = await ensureWorkspaceReady(
|
||||
defaultProject.id,
|
||||
);
|
||||
resolvedRootPath = ensuredWorkspace.rootPath || resolvedRootPath;
|
||||
} catch (error) {
|
||||
logAgentDebug(
|
||||
@@ -1024,7 +1026,9 @@ export function AgentChatWorkspace({
|
||||
: theme
|
||||
) as ThemeType;
|
||||
const rawBody = content.body || "";
|
||||
const sanitizedBody = isCorruptedGeneralWorkbenchDocumentContent(rawBody)
|
||||
const sanitizedBody = isCorruptedGeneralWorkbenchDocumentContent(
|
||||
rawBody,
|
||||
)
|
||||
? ""
|
||||
: rawBody;
|
||||
|
||||
@@ -2173,8 +2177,8 @@ export function AgentChatWorkspace({
|
||||
pendingActionRequest,
|
||||
});
|
||||
|
||||
const generalWorkbenchSidebarRuntime = useWorkspaceGeneralWorkbenchSidebarRuntime(
|
||||
{
|
||||
const generalWorkbenchSidebarRuntime =
|
||||
useWorkspaceGeneralWorkbenchSidebarRuntime({
|
||||
isThemeWorkbench,
|
||||
sessionId,
|
||||
messages,
|
||||
@@ -2182,8 +2186,7 @@ export function AgentChatWorkspace({
|
||||
themeWorkbenchBackendRunState,
|
||||
contextActivityLogs: contextWorkspace.activityLogs,
|
||||
historyPageSize: GENERAL_WORKBENCH_HISTORY_PAGE_SIZE,
|
||||
},
|
||||
);
|
||||
});
|
||||
|
||||
const { handleViewContextDetail } = useWorkspaceContextDetailActions({
|
||||
contextWorkspace,
|
||||
@@ -2460,12 +2463,10 @@ export function AgentChatWorkspace({
|
||||
setCanvasState,
|
||||
});
|
||||
|
||||
const submitImageWorkbenchAgentCommandRef =
|
||||
useRef<
|
||||
((
|
||||
params: SubmitImageWorkbenchAgentCommandParams,
|
||||
) => Promise<boolean>) | null
|
||||
>(null);
|
||||
const submitImageWorkbenchAgentCommandRef = useRef<
|
||||
| ((params: SubmitImageWorkbenchAgentCommandParams) => Promise<boolean>)
|
||||
| null
|
||||
>(null);
|
||||
const imageWorkbenchActionRuntime = useWorkspaceImageWorkbenchActionRuntime({
|
||||
cancelImageTask: cancelMediaTaskArtifact,
|
||||
contentId,
|
||||
@@ -2572,7 +2573,11 @@ export function AgentChatWorkspace({
|
||||
),
|
||||
},
|
||||
),
|
||||
[effectiveChatToolPreferences.thinking, handleSendRef, webSearchPreferenceRef],
|
||||
[
|
||||
effectiveChatToolPreferences.thinking,
|
||||
handleSendRef,
|
||||
webSearchPreferenceRef,
|
||||
],
|
||||
);
|
||||
submitImageWorkbenchAgentCommandRef.current =
|
||||
submitImageWorkbenchAgentCommand;
|
||||
@@ -2961,11 +2966,17 @@ export function AgentChatWorkspace({
|
||||
if (normalizedArtifactPath) {
|
||||
const matchedTaskFile = taskFiles.find(
|
||||
(file) =>
|
||||
areArtifactProtocolPathsEquivalent(file.name, normalizedArtifactPath) ||
|
||||
areArtifactProtocolPathsEquivalent(
|
||||
file.name,
|
||||
normalizedArtifactPath,
|
||||
) ||
|
||||
normalizeArtifactProtocolPath(file.name) === normalizedArtifactPath,
|
||||
);
|
||||
if (matchedTaskFile?.content?.trim()) {
|
||||
handleWorkspaceFileClick(matchedTaskFile.name, matchedTaskFile.content);
|
||||
handleWorkspaceFileClick(
|
||||
matchedTaskFile.name,
|
||||
matchedTaskFile.content,
|
||||
);
|
||||
return;
|
||||
}
|
||||
}
|
||||
@@ -3093,32 +3104,34 @@ export function AgentChatWorkspace({
|
||||
hasRealTeamGraph: teamSessionRuntime.hasRealTeamGraph,
|
||||
teamDispatchPreviewState,
|
||||
});
|
||||
const generalWorkbenchShellRuntime = useWorkspaceGeneralWorkbenchShellRuntime({
|
||||
showChatPanel: effectiveShowChatPanel,
|
||||
showSidebar,
|
||||
hasPendingA2UIForm,
|
||||
contextHarnessRuntime,
|
||||
generalWorkbenchScaffoldRuntime,
|
||||
generalWorkbenchSidebarRuntime,
|
||||
harnessInventoryRuntime,
|
||||
handleCreateVersionSnapshot,
|
||||
handleSwitchBranchVersion,
|
||||
handleSetBranchStatus,
|
||||
handleAddImage,
|
||||
handleImportDocument,
|
||||
handleViewContextDetail,
|
||||
messages,
|
||||
harnessState,
|
||||
compatSubagentRuntime,
|
||||
childSubagentSessions,
|
||||
selectedTeamLabel,
|
||||
selectedTeamSummary,
|
||||
selectedTeamRoles: selectedTeam?.roles,
|
||||
teamMemorySnapshot: resolvedTeamMemoryShadowSnapshot,
|
||||
handleOpenSubagentSession,
|
||||
handleHarnessLoadFilePreview,
|
||||
handleFileClick: handleWorkspaceFileClick,
|
||||
});
|
||||
const generalWorkbenchShellRuntime = useWorkspaceGeneralWorkbenchShellRuntime(
|
||||
{
|
||||
showChatPanel: effectiveShowChatPanel,
|
||||
showSidebar,
|
||||
hasPendingA2UIForm,
|
||||
contextHarnessRuntime,
|
||||
generalWorkbenchScaffoldRuntime,
|
||||
generalWorkbenchSidebarRuntime,
|
||||
harnessInventoryRuntime,
|
||||
handleCreateVersionSnapshot,
|
||||
handleSwitchBranchVersion,
|
||||
handleSetBranchStatus,
|
||||
handleAddImage,
|
||||
handleImportDocument,
|
||||
handleViewContextDetail,
|
||||
messages,
|
||||
harnessState,
|
||||
compatSubagentRuntime,
|
||||
childSubagentSessions,
|
||||
selectedTeamLabel,
|
||||
selectedTeamSummary,
|
||||
selectedTeamRoles: selectedTeam?.roles,
|
||||
teamMemorySnapshot: resolvedTeamMemoryShadowSnapshot,
|
||||
handleOpenSubagentSession,
|
||||
handleHarnessLoadFilePreview,
|
||||
handleFileClick: handleWorkspaceFileClick,
|
||||
},
|
||||
);
|
||||
|
||||
useWorkspaceWorkflowProgressSync({
|
||||
enabled: isSpecializedThemeMode && hasMessages && steps.length > 0,
|
||||
@@ -3283,10 +3296,8 @@ export function AgentChatWorkspace({
|
||||
const workspaceShellSceneRuntime = useWorkspaceConversationShellSceneRuntime({
|
||||
messageListEmptyStateVariant:
|
||||
agentEntry === "claw" ? "task-center" : "default",
|
||||
navbarContextVariant:
|
||||
agentEntry === "claw" ? "task-center" : "default",
|
||||
sidebarContextVariant:
|
||||
agentEntry === "claw" ? "task-center" : "default",
|
||||
navbarContextVariant: agentEntry === "claw" ? "task-center" : "default",
|
||||
sidebarContextVariant: agentEntry === "claw" ? "task-center" : "default",
|
||||
navigationActions,
|
||||
inputbarScene,
|
||||
canvasScene,
|
||||
|
||||
@@ -16,7 +16,9 @@ function ensureSessionMutationAllowed(
|
||||
return true;
|
||||
}
|
||||
|
||||
notifyInfo(`当前仍有任务执行中,请先等待结束或停止生成后再执行 ${commandPrefix}`);
|
||||
notifyInfo(
|
||||
`当前仍有任务执行中,请先等待结束或停止生成后再执行 ${commandPrefix}`,
|
||||
);
|
||||
return false;
|
||||
}
|
||||
|
||||
@@ -78,7 +80,9 @@ export async function executeCodexSlashCommand(
|
||||
return true;
|
||||
case "model":
|
||||
if (command.userInput.trim()) {
|
||||
notifyInfo("当前暂不支持通过 /model 切换模型,请使用输入框右侧的模型选择器");
|
||||
notifyInfo(
|
||||
"当前暂不支持通过 /model 切换模型,请使用输入框右侧的模型选择器",
|
||||
);
|
||||
return true;
|
||||
}
|
||||
appendAssistantMessage(buildCodexSlashModelMessage(statusSnapshot));
|
||||
|
||||
@@ -2,7 +2,10 @@ import {
|
||||
getSupportedCodexSlashCommands,
|
||||
getUnsupportedCodexSlashCommands,
|
||||
} from "./parser";
|
||||
import type { CodexSlashStatusSnapshot, ParsedCodexSlashCommand } from "./types";
|
||||
import type {
|
||||
CodexSlashStatusSnapshot,
|
||||
ParsedCodexSlashCommand,
|
||||
} from "./types";
|
||||
|
||||
function formatExecutionStrategyLabel(
|
||||
strategy: CodexSlashStatusSnapshot["executionStrategy"],
|
||||
|
||||
@@ -1,9 +1,6 @@
|
||||
import { A2UIRenderer } from "@/lib/workspace/a2ui";
|
||||
import { cn } from "@/lib/utils";
|
||||
import type {
|
||||
A2UIFormData,
|
||||
A2UIResponse,
|
||||
} from "@/lib/workspace/a2ui";
|
||||
import type { A2UIFormData, A2UIResponse } from "@/lib/workspace/a2ui";
|
||||
import {
|
||||
DEFAULT_A2UI_TASK_CARD_PRESET,
|
||||
type A2UITaskCardPreset,
|
||||
|
||||
@@ -24,7 +24,10 @@ export const AgentPlanBlock: React.FC<AgentPlanBlockProps> = ({
|
||||
<ListChecks className="h-4 w-4" />
|
||||
</div>
|
||||
<div className="text-sm font-medium text-foreground">执行计划</div>
|
||||
<Badge variant={isComplete ? "outline" : "secondary"} className="ml-auto">
|
||||
<Badge
|
||||
variant={isComplete ? "outline" : "secondary"}
|
||||
className="ml-auto"
|
||||
>
|
||||
{isComplete ? (
|
||||
"已生成"
|
||||
) : (
|
||||
|
||||
@@ -9,9 +9,7 @@ import type {
|
||||
import type { ChatToolPreferences } from "../utils/chatToolPreferences";
|
||||
import type { CompatSubagentRuntimeStatus } from "../utils/compatSubagentRuntime";
|
||||
import type { HarnessSessionState } from "../utils/harnessState";
|
||||
import {
|
||||
getOutputSchemaRuntimeLabel,
|
||||
} from "../utils/sessionExecutionRuntime";
|
||||
import { getOutputSchemaRuntimeLabel } from "../utils/sessionExecutionRuntime";
|
||||
|
||||
interface AgentRuntimeStripProps {
|
||||
activeTheme?: string;
|
||||
|
||||
@@ -84,7 +84,9 @@ export const AgentThreadOutcomeSummary: React.FC<
|
||||
|
||||
<div className="mt-2 inline-flex items-center gap-1 text-xs text-muted-foreground">
|
||||
<RotateCcw className="h-3.5 w-3.5" />
|
||||
<span>{outcome.retryable ? "建议可重试或恢复" : "当前无需人工恢复"}</span>
|
||||
<span>
|
||||
{outcome.retryable ? "建议可重试或恢复" : "当前无需人工恢复"}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
||||
@@ -180,7 +180,9 @@ describe("AgentThreadReliabilityPanel", () => {
|
||||
expect(container.textContent).toContain("待处理请求");
|
||||
expect(container.textContent).toContain("Provider 失败");
|
||||
expect(container.textContent).toContain("审批等待超过阈值");
|
||||
expect(container.textContent).toContain("审批等待过久,建议尽快处理或停止当前执行");
|
||||
expect(container.textContent).toContain(
|
||||
"审批等待过久,建议尽快处理或停止当前执行",
|
||||
);
|
||||
});
|
||||
|
||||
it("缺少 thread_read 时,应从当前 turn 与 pendingActions 推导并支持中断", async () => {
|
||||
@@ -226,8 +228,8 @@ describe("AgentThreadReliabilityPanel", () => {
|
||||
expect(container.textContent).toContain("等待人工处理");
|
||||
expect(container.textContent).toContain("请确认是否继续发布");
|
||||
|
||||
const button = Array.from(container.querySelectorAll("button")).find((node) =>
|
||||
node.textContent?.includes("停止当前执行"),
|
||||
const button = Array.from(container.querySelectorAll("button")).find(
|
||||
(node) => node.textContent?.includes("停止当前执行"),
|
||||
);
|
||||
expect(button).toBeDefined();
|
||||
|
||||
@@ -271,8 +273,8 @@ describe("AgentThreadReliabilityPanel", () => {
|
||||
onInterruptCurrentTurn,
|
||||
});
|
||||
|
||||
const button = Array.from(container.querySelectorAll("button")).find((node) =>
|
||||
node.textContent?.includes("停止当前执行"),
|
||||
const button = Array.from(container.querySelectorAll("button")).find(
|
||||
(node) => node.textContent?.includes("停止当前执行"),
|
||||
);
|
||||
expect(button).toBeDefined();
|
||||
|
||||
@@ -460,7 +462,9 @@ describe("AgentThreadReliabilityPanel", () => {
|
||||
});
|
||||
|
||||
expect(container.textContent).toContain("当前回合长时间无进展");
|
||||
expect(container.textContent).toContain("当前回合长时间无进展,建议停止后恢复执行");
|
||||
expect(container.textContent).toContain(
|
||||
"当前回合长时间无进展,建议停止后恢复执行",
|
||||
);
|
||||
});
|
||||
|
||||
it("存在待处理请求时应支持重新拉起请求", async () => {
|
||||
@@ -712,7 +716,9 @@ describe("AgentThreadReliabilityPanel", () => {
|
||||
expect(container.textContent).toContain("快速复制给 AI");
|
||||
expect(container.textContent).toContain("复制原始 JSON(debug)");
|
||||
expect(container.textContent).toContain("外部分析交接");
|
||||
expect(container.textContent).toContain("analysis-brief.md / analysis-context.json");
|
||||
expect(container.textContent).toContain(
|
||||
"analysis-brief.md / analysis-context.json",
|
||||
);
|
||||
});
|
||||
|
||||
it("应支持复制原始 JSON 诊断数据", async () => {
|
||||
|
||||
@@ -55,9 +55,7 @@ interface AgentThreadReliabilityPanelProps {
|
||||
onResumeThread?: () => boolean | Promise<boolean>;
|
||||
onReplayPendingRequest?: (requestId: string) => boolean | Promise<boolean>;
|
||||
onLocatePendingRequest?: (requestId: string) => void;
|
||||
onPromoteQueuedTurn?: (
|
||||
queuedTurnId: string,
|
||||
) => boolean | Promise<boolean>;
|
||||
onPromoteQueuedTurn?: (queuedTurnId: string) => boolean | Promise<boolean>;
|
||||
className?: string;
|
||||
harnessState?: HarnessSessionState | null;
|
||||
messages?: Message[];
|
||||
@@ -76,7 +74,10 @@ function normalizeDiagnosticText(value?: string | null): string {
|
||||
return typeof value === "string" ? value.trim().replace(/\s+/g, " ") : "";
|
||||
}
|
||||
|
||||
function truncateDiagnosticText(value?: string | null, maxLength = 240): string {
|
||||
function truncateDiagnosticText(
|
||||
value?: string | null,
|
||||
maxLength = 240,
|
||||
): string {
|
||||
const normalized = normalizeDiagnosticText(value);
|
||||
if (!normalized) {
|
||||
return "";
|
||||
@@ -145,9 +146,14 @@ function summarizeRecentMessages(messages: Message[]) {
|
||||
: null,
|
||||
action_request_count: message.actionRequests?.length || 0,
|
||||
action_request_titles:
|
||||
message.actionRequests?.slice(0, 3).map((request) =>
|
||||
truncateDiagnosticText(request.prompt || request.toolName || request.requestId, 120),
|
||||
) || [],
|
||||
message.actionRequests
|
||||
?.slice(0, 3)
|
||||
.map((request) =>
|
||||
truncateDiagnosticText(
|
||||
request.prompt || request.toolName || request.requestId,
|
||||
120,
|
||||
),
|
||||
) || [],
|
||||
tool_calls:
|
||||
message.toolCalls?.slice(0, 4).map((toolCall) => ({
|
||||
id: toolCall.id,
|
||||
@@ -175,8 +181,12 @@ function summarizeHarnessState(harnessState?: HarnessSessionState | null) {
|
||||
? {
|
||||
phase: harnessState.runtimeStatus.phase,
|
||||
title: harnessState.runtimeStatus.title,
|
||||
detail: truncateDiagnosticText(harnessState.runtimeStatus.detail, 220),
|
||||
checkpoints: harnessState.runtimeStatus.checkpoints?.slice(0, 6) || [],
|
||||
detail: truncateDiagnosticText(
|
||||
harnessState.runtimeStatus.detail,
|
||||
220,
|
||||
),
|
||||
checkpoints:
|
||||
harnessState.runtimeStatus.checkpoints?.slice(0, 6) || [],
|
||||
metadata: harnessState.runtimeStatus.metadata || null,
|
||||
}
|
||||
: null,
|
||||
@@ -214,23 +224,30 @@ function summarizeHarnessState(harnessState?: HarnessSessionState | null) {
|
||||
truncated: signal.truncated || false,
|
||||
offloaded: signal.offloaded || false,
|
||||
})),
|
||||
active_file_writes: harnessState.activeFileWrites.slice(0, 6).map((write) => ({
|
||||
id: write.id,
|
||||
path: write.path,
|
||||
display_name: write.displayName,
|
||||
phase: write.phase,
|
||||
status: write.status,
|
||||
preview: truncateDiagnosticText(write.preview || write.latestChunk, 160),
|
||||
})),
|
||||
recent_file_events: harnessState.recentFileEvents.slice(0, 8).map((event) => ({
|
||||
id: event.id,
|
||||
path: event.path,
|
||||
display_name: event.displayName,
|
||||
kind: event.kind,
|
||||
action: event.action,
|
||||
source_tool_name: event.sourceToolName,
|
||||
preview: truncateDiagnosticText(event.preview, 140),
|
||||
})),
|
||||
active_file_writes: harnessState.activeFileWrites
|
||||
.slice(0, 6)
|
||||
.map((write) => ({
|
||||
id: write.id,
|
||||
path: write.path,
|
||||
display_name: write.displayName,
|
||||
phase: write.phase,
|
||||
status: write.status,
|
||||
preview: truncateDiagnosticText(
|
||||
write.preview || write.latestChunk,
|
||||
160,
|
||||
),
|
||||
})),
|
||||
recent_file_events: harnessState.recentFileEvents
|
||||
.slice(0, 8)
|
||||
.map((event) => ({
|
||||
id: event.id,
|
||||
path: event.path,
|
||||
display_name: event.displayName,
|
||||
kind: event.kind,
|
||||
action: event.action,
|
||||
source_tool_name: event.sourceToolName,
|
||||
preview: truncateDiagnosticText(event.preview, 140),
|
||||
})),
|
||||
};
|
||||
}
|
||||
|
||||
@@ -441,7 +458,9 @@ function buildReliabilityDiagnosticText(params: {
|
||||
sections.push("", "### 后端诊断聚合");
|
||||
if (threadRead?.diagnostics) {
|
||||
const diagnostics = threadRead.diagnostics;
|
||||
sections.push(`- 最新回合状态:${diagnostics.latest_turn_status || "未知"}`);
|
||||
sections.push(
|
||||
`- 最新回合状态:${diagnostics.latest_turn_status || "未知"}`,
|
||||
);
|
||||
sections.push(
|
||||
`- 最新回合开始时间:${diagnostics.latest_turn_started_at || "未知"}`,
|
||||
);
|
||||
@@ -461,7 +480,9 @@ function buildReliabilityDiagnosticText(params: {
|
||||
`- 最新回合错误:${diagnostics.latest_turn_error_message || "无"}`,
|
||||
);
|
||||
sections.push(`- 中断原因:${diagnostics.interrupt_reason || "未知"}`);
|
||||
sections.push(`- 中断来源:${diagnostics.runtime_interrupt_source || "未知"}`);
|
||||
sections.push(
|
||||
`- 中断来源:${diagnostics.runtime_interrupt_source || "未知"}`,
|
||||
);
|
||||
sections.push(
|
||||
`- 中断请求时间:${diagnostics.runtime_interrupt_requested_at || "未知"}`,
|
||||
);
|
||||
@@ -480,8 +501,12 @@ function buildReliabilityDiagnosticText(params: {
|
||||
sections.push(
|
||||
`- 最老待处理请求等待时长(秒):${diagnostics.oldest_pending_request_wait_seconds ?? "无"}`,
|
||||
);
|
||||
sections.push(`- 主阻塞类型:${diagnostics.primary_blocking_kind || "未知"}`);
|
||||
sections.push(`- 主阻塞摘要:${diagnostics.primary_blocking_summary || "未知"}`);
|
||||
sections.push(
|
||||
`- 主阻塞类型:${diagnostics.primary_blocking_kind || "未知"}`,
|
||||
);
|
||||
sections.push(
|
||||
`- 主阻塞摘要:${diagnostics.primary_blocking_summary || "未知"}`,
|
||||
);
|
||||
sections.push(
|
||||
`- 最近 warning:${
|
||||
diagnostics.latest_warning
|
||||
@@ -737,7 +762,9 @@ export const AgentThreadReliabilityPanel: React.FC<
|
||||
);
|
||||
toast.success("原始 JSON 已复制");
|
||||
} catch (error) {
|
||||
toast.error(error instanceof Error ? error.message : "复制原始 JSON 失败");
|
||||
toast.error(
|
||||
error instanceof Error ? error.message : "复制原始 JSON 失败",
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
@@ -773,11 +800,15 @@ export const AgentThreadReliabilityPanel: React.FC<
|
||||
</span>
|
||||
) : null}
|
||||
</div>
|
||||
<div className="mt-2 text-sm leading-6 text-muted-foreground">{summary}</div>
|
||||
<div className="mt-2 text-sm leading-6 text-muted-foreground">
|
||||
{summary}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="flex flex-wrap items-center justify-end gap-2 text-xs text-muted-foreground">
|
||||
{view.updatedAtLabel ? <span>最近刷新 {view.updatedAtLabel}</span> : null}
|
||||
{view.updatedAtLabel ? (
|
||||
<span>最近刷新 {view.updatedAtLabel}</span>
|
||||
) : null}
|
||||
{view.interruptStateLabel ? (
|
||||
<Badge
|
||||
variant="outline"
|
||||
@@ -812,9 +843,10 @@ export const AgentThreadReliabilityPanel: React.FC<
|
||||
</div>
|
||||
<div className="mt-2 rounded-xl border border-amber-200 bg-amber-50/80 px-3 py-2 text-[11px] leading-5 text-amber-900">
|
||||
当前入口属于 `compat` 线程级快速诊断,只覆盖当前 thread 的运行信号。
|
||||
正式交给外部 Claude / Codex 分析时,请优先使用工作台“交接制品
|
||||
→ 外部分析交接”的 `analysis-brief.md / analysis-context.json`
|
||||
主链;这里的“快速复制给 AI”只适合临时排障,“复制原始 JSON(debug)”适合程序化分析、存档或二次处理。
|
||||
正式交给外部 Claude / Codex 分析时,请优先使用工作台“交接制品 →
|
||||
外部分析交接”的 `analysis-brief.md / analysis-context.json`
|
||||
主链;这里的“快速复制给 AI”只适合临时排障,“复制原始
|
||||
JSON(debug)”适合程序化分析、存档或二次处理。
|
||||
</div>
|
||||
|
||||
<div className="mt-4 grid gap-2 md:grid-cols-3">
|
||||
|
||||
@@ -22,7 +22,10 @@ function resolveMockToolText(toolCall: {
|
||||
let parsedArguments: Record<string, unknown> | null = null;
|
||||
if (toolCall.arguments) {
|
||||
try {
|
||||
parsedArguments = JSON.parse(toolCall.arguments) as Record<string, unknown>;
|
||||
parsedArguments = JSON.parse(toolCall.arguments) as Record<
|
||||
string,
|
||||
unknown
|
||||
>;
|
||||
} catch {
|
||||
parsedArguments = null;
|
||||
}
|
||||
@@ -167,9 +170,7 @@ function at(second: number): string {
|
||||
return `2026-03-15T09:10:${String(second).padStart(2, "0")}Z`;
|
||||
}
|
||||
|
||||
function createTurn(
|
||||
overrides?: Partial<AgentThreadTurn>,
|
||||
): AgentThreadTurn {
|
||||
function createTurn(overrides?: Partial<AgentThreadTurn>): AgentThreadTurn {
|
||||
return {
|
||||
id: "turn-1",
|
||||
thread_id: "thread-1",
|
||||
@@ -384,7 +385,7 @@ describe("AgentThreadTimeline", () => {
|
||||
createFileArtifactItem({
|
||||
id: "artifact-hidden-task-json",
|
||||
path: ".lime/tasks/image_generate/task-image-1.json",
|
||||
content: "{\"status\":\"running\"}",
|
||||
content: '{"status":"running"}',
|
||||
metadata: {},
|
||||
}),
|
||||
]);
|
||||
@@ -515,9 +516,7 @@ describe("AgentThreadTimeline", () => {
|
||||
.map((node) => node.dataset.testid)
|
||||
.filter((value): value is string => Boolean(value))
|
||||
.filter(
|
||||
(value) =>
|
||||
!value.endsWith(":shell") &&
|
||||
!value.endsWith(":details"),
|
||||
(value) => !value.endsWith(":shell") && !value.endsWith(":details"),
|
||||
);
|
||||
|
||||
expect(blockIds).toEqual(["agent-thread-block:1:process"]);
|
||||
@@ -552,7 +551,9 @@ describe("AgentThreadTimeline", () => {
|
||||
|
||||
expect(summary?.textContent).toContain("2 步");
|
||||
expect(summary?.textContent).toContain("2 个工具步骤");
|
||||
expect(container.querySelectorAll('[data-testid="tool-call-item"]')).toHaveLength(0);
|
||||
expect(
|
||||
container.querySelectorAll('[data-testid="tool-call-item"]'),
|
||||
).toHaveLength(0);
|
||||
|
||||
act(() => {
|
||||
summary?.dispatchEvent(new MouseEvent("click", { bubbles: true }));
|
||||
@@ -791,14 +792,19 @@ describe("AgentThreadTimeline", () => {
|
||||
});
|
||||
|
||||
expect(
|
||||
container.querySelector('[data-testid="agent-thread-block:1:process:details"]'),
|
||||
container.querySelector(
|
||||
'[data-testid="agent-thread-block:1:process:details"]',
|
||||
),
|
||||
).toBeNull();
|
||||
expect(container.textContent).not.toContain("思考摘要");
|
||||
expect((container.textContent?.split(reasoningText).length ?? 1) - 1).toBe(1);
|
||||
expect((container.textContent?.split(reasoningText).length ?? 1) - 1).toBe(
|
||||
1,
|
||||
);
|
||||
});
|
||||
|
||||
it("已完成的思考应默认折叠,只保留摘要行", () => {
|
||||
const reasoningText = "先核对执行链路,再立即恢复当前运行。\n随后补齐自动续提。";
|
||||
const reasoningText =
|
||||
"先核对执行链路,再立即恢复当前运行。\n随后补齐自动续提。";
|
||||
const items: AgentThreadItem[] = [
|
||||
{
|
||||
...createBaseItem("reasoning-1", 1),
|
||||
@@ -820,7 +826,9 @@ describe("AgentThreadTimeline", () => {
|
||||
|
||||
expect(block?.open).toBe(false);
|
||||
expect(summary?.textContent).toContain("已完成思考");
|
||||
expect(summary?.textContent).toContain("先核对执行链路,再立即恢复当前运行。");
|
||||
expect(summary?.textContent).toContain(
|
||||
"先核对执行链路,再立即恢复当前运行。",
|
||||
);
|
||||
expect(container.textContent).not.toContain("随后补齐自动续提。");
|
||||
|
||||
act(() => {
|
||||
@@ -914,8 +922,12 @@ describe("AgentThreadTimeline", () => {
|
||||
summary?.dispatchEvent(new MouseEvent("click", { bubbles: true }));
|
||||
});
|
||||
|
||||
expect((container.textContent?.split("先判断任务类型").length ?? 1) - 1).toBe(1);
|
||||
expect((container.textContent?.split("再决定是否联网").length ?? 1) - 1).toBe(1);
|
||||
expect(
|
||||
(container.textContent?.split("先判断任务类型").length ?? 1) - 1,
|
||||
).toBe(1);
|
||||
expect(
|
||||
(container.textContent?.split("再决定是否联网").length ?? 1) - 1,
|
||||
).toBe(1);
|
||||
});
|
||||
|
||||
it("已完成的 request_user_input 应以只读 A2UI 卡片回显", () => {
|
||||
@@ -945,7 +957,9 @@ describe("AgentThreadTimeline", () => {
|
||||
expect(
|
||||
container.querySelector('[data-testid="timeline-a2ui-card"]'),
|
||||
).not.toBeNull();
|
||||
expect(container.querySelector('[data-testid="decision-panel"]')).toBeNull();
|
||||
expect(
|
||||
container.querySelector('[data-testid="decision-panel"]'),
|
||||
).toBeNull();
|
||||
});
|
||||
|
||||
it("真实协作成员 item 应支持查看协作详情", () => {
|
||||
|
||||
@@ -273,7 +273,8 @@ function toToolCallState(item: AgentThreadItem): ToolCallState | null {
|
||||
item.error !== undefined ||
|
||||
item.exit_code !== undefined
|
||||
? {
|
||||
success: item.status === "completed" && item.error === undefined,
|
||||
success:
|
||||
item.status === "completed" && item.error === undefined,
|
||||
output: item.aggregated_output || "",
|
||||
error: item.error,
|
||||
metadata:
|
||||
@@ -405,61 +406,61 @@ function ThinkingItemCard({
|
||||
() => parseAIResponse(displayText, false),
|
||||
[displayText],
|
||||
);
|
||||
const hasStructuredPreview = parsedContent.hasA2UI || parsedContent.hasPending;
|
||||
const hasStructuredPreview =
|
||||
parsedContent.hasA2UI || parsedContent.hasPending;
|
||||
const shouldHideTurnSummaryContent =
|
||||
item.type === "turn_summary" &&
|
||||
!hasStructuredPreview &&
|
||||
isInternalRoutingTurnSummaryText(displayText);
|
||||
|
||||
const content =
|
||||
hasStructuredPreview ? (
|
||||
<div className="space-y-3">
|
||||
{parsedContent.parts.map((part, index) => {
|
||||
if (part.type === "a2ui" && typeof part.content !== "string") {
|
||||
const readonlyResponse: A2UIResponse = {
|
||||
...part.content,
|
||||
submitAction: undefined,
|
||||
};
|
||||
|
||||
return (
|
||||
<A2UITaskCard
|
||||
key={`timeline-a2ui-${index}`}
|
||||
response={readonlyResponse}
|
||||
compact={true}
|
||||
preview={true}
|
||||
preset={TIMELINE_A2UI_TASK_CARD_PRESET}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
if (part.type === "pending_a2ui") {
|
||||
return (
|
||||
<A2UITaskLoadingCard
|
||||
key={`timeline-pending-a2ui-${index}`}
|
||||
compact={true}
|
||||
preset={TIMELINE_A2UI_TASK_CARD_PRESET}
|
||||
subtitle="这一步还在整理,稍等一下。"
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
const textContent =
|
||||
typeof part.content === "string" ? part.content.trim() : "";
|
||||
if (!textContent) {
|
||||
return null;
|
||||
}
|
||||
const content = hasStructuredPreview ? (
|
||||
<div className="space-y-3">
|
||||
{parsedContent.parts.map((part, index) => {
|
||||
if (part.type === "a2ui" && typeof part.content !== "string") {
|
||||
const readonlyResponse: A2UIResponse = {
|
||||
...part.content,
|
||||
submitAction: undefined,
|
||||
};
|
||||
|
||||
return (
|
||||
<MarkdownRenderer
|
||||
key={`timeline-text-${index}`}
|
||||
content={textContent}
|
||||
<A2UITaskCard
|
||||
key={`timeline-a2ui-${index}`}
|
||||
response={readonlyResponse}
|
||||
compact={true}
|
||||
preview={true}
|
||||
preset={TIMELINE_A2UI_TASK_CARD_PRESET}
|
||||
/>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
) : shouldHideTurnSummaryContent ? null : (
|
||||
<MarkdownRenderer content={displayText} />
|
||||
);
|
||||
}
|
||||
|
||||
if (part.type === "pending_a2ui") {
|
||||
return (
|
||||
<A2UITaskLoadingCard
|
||||
key={`timeline-pending-a2ui-${index}`}
|
||||
compact={true}
|
||||
preset={TIMELINE_A2UI_TASK_CARD_PRESET}
|
||||
subtitle="这一步还在整理,稍等一下。"
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
const textContent =
|
||||
typeof part.content === "string" ? part.content.trim() : "";
|
||||
if (!textContent) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<MarkdownRenderer
|
||||
key={`timeline-text-${index}`}
|
||||
content={textContent}
|
||||
/>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
) : shouldHideTurnSummaryContent ? null : (
|
||||
<MarkdownRenderer content={displayText} />
|
||||
);
|
||||
const statusLabel =
|
||||
item.type === "turn_summary"
|
||||
? item.status === "in_progress"
|
||||
@@ -577,7 +578,12 @@ function InlinePlanBlock({
|
||||
|
||||
function renderThinkingItemDetails(item: AgentThreadItem) {
|
||||
if (item.type === "plan") {
|
||||
return <InlinePlanBlock content={item.text} isComplete={item.status !== "in_progress"} />;
|
||||
return (
|
||||
<InlinePlanBlock
|
||||
content={item.text}
|
||||
isComplete={item.status !== "in_progress"}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
if (item.type === "reasoning" || item.type === "turn_summary") {
|
||||
@@ -613,10 +619,12 @@ function isToolExecutionTimelineItem(item: AgentThreadItem): boolean {
|
||||
);
|
||||
}
|
||||
|
||||
function extractCompactThinkingParts(item: Extract<
|
||||
AgentThreadItem,
|
||||
{ type: "plan" | "reasoning" | "turn_summary" | "context_compaction" }
|
||||
>) {
|
||||
function extractCompactThinkingParts(
|
||||
item: Extract<
|
||||
AgentThreadItem,
|
||||
{ type: "plan" | "reasoning" | "turn_summary" | "context_compaction" }
|
||||
>,
|
||||
) {
|
||||
if (item.type === "context_compaction") {
|
||||
const title =
|
||||
item.stage === "completed" || item.status === "completed"
|
||||
@@ -638,15 +646,20 @@ function extractCompactThinkingParts(item: Extract<
|
||||
}
|
||||
|
||||
if (item.type === "reasoning") {
|
||||
const { summaryText, bodyText, combinedText } = resolveReasoningDisplayText(item);
|
||||
const { summaryText, bodyText, combinedText } =
|
||||
resolveReasoningDisplayText(item);
|
||||
const previewSource = summaryText || combinedText;
|
||||
const lines = previewSource
|
||||
.split(/\r?\n/)
|
||||
.map((line) => line.trim())
|
||||
.filter(Boolean);
|
||||
const [title = item.status === "in_progress" ? "思考中" : "已完成思考", ...rest] =
|
||||
lines;
|
||||
const detail = [rest.join("\n").trim(), bodyText].filter(Boolean).join("\n\n");
|
||||
const [
|
||||
title = item.status === "in_progress" ? "思考中" : "已完成思考",
|
||||
...rest
|
||||
] = lines;
|
||||
const detail = [rest.join("\n").trim(), bodyText]
|
||||
.filter(Boolean)
|
||||
.join("\n\n");
|
||||
|
||||
const parsed = parseAIResponse(combinedText, false);
|
||||
if (parsed.hasA2UI || parsed.hasPending) {
|
||||
@@ -886,7 +899,10 @@ function renderGroupItemDetails(
|
||||
<div className="py-1.5">
|
||||
<div className="mb-2 flex items-center gap-2">
|
||||
<span className="text-sm font-medium text-foreground">{item.type}</span>
|
||||
<Badge variant={resolveStatusBadgeVariant(item.status)} className="ml-auto">
|
||||
<Badge
|
||||
variant={resolveStatusBadgeVariant(item.status)}
|
||||
className="ml-auto"
|
||||
>
|
||||
{resolveItemStatusLabel(item.status)}
|
||||
</Badge>
|
||||
</div>
|
||||
@@ -911,7 +927,9 @@ function renderTimelineItemDetails(
|
||||
) {
|
||||
if (isThinkingTimelineItem(item)) {
|
||||
if (options?.groupedToolCall) {
|
||||
return <GroupedThinkingRow item={item} groupMarker={options.groupMarker} />;
|
||||
return (
|
||||
<GroupedThinkingRow item={item} groupMarker={options.groupMarker} />
|
||||
);
|
||||
}
|
||||
return renderThinkingItemDetails(item);
|
||||
}
|
||||
@@ -927,7 +945,9 @@ function renderTimelineItemDetails(
|
||||
);
|
||||
}
|
||||
|
||||
function resolveCompactTechnicalSummary(block: AgentThreadOrderedBlock): string {
|
||||
function resolveCompactTechnicalSummary(
|
||||
block: AgentThreadOrderedBlock,
|
||||
): string {
|
||||
return `处理了 ${block.items.length} 个步骤`;
|
||||
}
|
||||
|
||||
@@ -1060,23 +1080,41 @@ function normalizeBlockPreviewLine(
|
||||
|
||||
if (
|
||||
kind === "artifact" &&
|
||||
!hasAnyPrefix(trimmed, ["看了 ", "读了 ", "写了 ", "改了 ", "动了 ", "产出了 "])
|
||||
!hasAnyPrefix(trimmed, [
|
||||
"看了 ",
|
||||
"读了 ",
|
||||
"写了 ",
|
||||
"改了 ",
|
||||
"动了 ",
|
||||
"产出了 ",
|
||||
])
|
||||
) {
|
||||
return `产出了 ${trimmed}`;
|
||||
}
|
||||
|
||||
if (
|
||||
kind === "approval" &&
|
||||
!hasAnyPrefix(trimmed, ["等你补充:", "等你确认:", "等你补充信息", "等你确认这一步"])
|
||||
!hasAnyPrefix(trimmed, [
|
||||
"等你补充:",
|
||||
"等你确认:",
|
||||
"等你补充信息",
|
||||
"等你确认这一步",
|
||||
])
|
||||
) {
|
||||
return `等你确认:${trimmed}`;
|
||||
}
|
||||
|
||||
if (kind === "alert" && !hasAnyPrefix(trimmed, ["收到提醒:", "碰到错误:"])) {
|
||||
if (
|
||||
kind === "alert" &&
|
||||
!hasAnyPrefix(trimmed, ["收到提醒:", "碰到错误:"])
|
||||
) {
|
||||
return `收到提醒:${trimmed}`;
|
||||
}
|
||||
|
||||
if (kind === "subagent" && !hasAnyPrefix(trimmed, ["分给协作成员", "协作成员"])) {
|
||||
if (
|
||||
kind === "subagent" &&
|
||||
!hasAnyPrefix(trimmed, ["分给协作成员", "协作成员"])
|
||||
) {
|
||||
return `分给协作成员处理 ${trimmed}`;
|
||||
}
|
||||
|
||||
@@ -1136,7 +1174,9 @@ function resolveBlockSummaryLines(block: AgentThreadOrderedBlock): string[] {
|
||||
}
|
||||
|
||||
if (block.kind === "subagent") {
|
||||
return [block.status === "completed" ? "协作成员处理完了" : "协作成员在处理"];
|
||||
return [
|
||||
block.status === "completed" ? "协作成员处理完了" : "协作成员在处理",
|
||||
];
|
||||
}
|
||||
|
||||
if (block.kind === "other") {
|
||||
@@ -1385,8 +1425,7 @@ function TimelineBlockCard({
|
||||
const visibleHeadline = headline;
|
||||
const visibleSupportingLines =
|
||||
isThinkingOnlyBlock && open ? [] : supportingLines;
|
||||
const summaryCountLabel =
|
||||
block.items.length > 1 ? block.countLabel : null;
|
||||
const summaryCountLabel = block.items.length > 1 ? block.countLabel : null;
|
||||
const processMixLabel = resolveProcessMixLabel(block);
|
||||
const summaryDetailHint =
|
||||
hasDetailEntries && block.items.length > 1 && !open
|
||||
@@ -1395,13 +1434,23 @@ function TimelineBlockCard({
|
||||
const summaryToneClassName = cn(
|
||||
"text-slate-900",
|
||||
block.status === "in_progress" && "text-sky-700",
|
||||
block.kind === "approval" && block.status !== "completed" && "text-amber-800",
|
||||
block.kind === "approval" &&
|
||||
block.status !== "completed" &&
|
||||
"text-amber-800",
|
||||
(block.status === "failed" || block.kind === "alert") && "text-rose-700",
|
||||
);
|
||||
|
||||
return (
|
||||
<div className="py-0.5" data-testid={`${dataTestId}:shell`} data-emphasis={emphasis}>
|
||||
<details data-testid={dataTestId} data-emphasis={emphasis} open={hasDetailEntries ? open : true}>
|
||||
<div
|
||||
className="py-0.5"
|
||||
data-testid={`${dataTestId}:shell`}
|
||||
data-emphasis={emphasis}
|
||||
>
|
||||
<details
|
||||
data-testid={dataTestId}
|
||||
data-emphasis={emphasis}
|
||||
open={hasDetailEntries ? open : true}
|
||||
>
|
||||
<summary
|
||||
className={cn(
|
||||
"list-none rounded-md px-2 py-1.5",
|
||||
@@ -1516,7 +1565,10 @@ export const AgentThreadTimeline: React.FC<AgentThreadTimelineProps> = ({
|
||||
(item) =>
|
||||
item.type !== "user_message" &&
|
||||
item.type !== "agent_message" &&
|
||||
!(item.type === "file_artifact" && isHiddenInternalArtifactPath(item.path)),
|
||||
!(
|
||||
item.type === "file_artifact" &&
|
||||
isHiddenInternalArtifactPath(item.path)
|
||||
),
|
||||
),
|
||||
[items],
|
||||
);
|
||||
@@ -1553,11 +1605,13 @@ export const AgentThreadTimeline: React.FC<AgentThreadTimelineProps> = ({
|
||||
data-testid="agent-thread-flow"
|
||||
data-placement={placement}
|
||||
>
|
||||
{inlineStatusHint ? <ThreadInlineStatusHint hint={inlineStatusHint} /> : null}
|
||||
{inlineStatusHint ? (
|
||||
<ThreadInlineStatusHint hint={inlineStatusHint} />
|
||||
) : null}
|
||||
{displayModel.orderedBlocks.map((block, index) => {
|
||||
const blockHasFocusedItem = Boolean(
|
||||
focusedItemId &&
|
||||
block.items.some((item) => item.id === focusedItemId),
|
||||
block.items.some((item) => item.id === focusedItemId),
|
||||
);
|
||||
|
||||
return (
|
||||
|
||||
@@ -135,7 +135,9 @@ describe("AgentThreadTimelineArtifactCard", () => {
|
||||
expect(container.textContent).toContain("2 个区块");
|
||||
expect(container.textContent).toContain("1 条来源");
|
||||
expect(container.textContent).toContain("V2");
|
||||
expect(container.textContent).toContain("定位到 本轮重点是补齐来源线索与交付节奏。");
|
||||
expect(container.textContent).toContain(
|
||||
"定位到 本轮重点是补齐来源线索与交付节奏。",
|
||||
);
|
||||
expect(container.textContent).not.toContain("artifact_document_service");
|
||||
expect(container.textContent).not.toContain("schemaVersion");
|
||||
expect(container.textContent).not.toContain('"artifactId"');
|
||||
@@ -155,7 +157,9 @@ describe("AgentThreadTimelineArtifactCard", () => {
|
||||
);
|
||||
|
||||
expect(container.textContent).toContain("已同步");
|
||||
expect(container.textContent).toContain("包含结构化结果,点击在画布中查看完整内容。");
|
||||
expect(container.textContent).toContain(
|
||||
"包含结构化结果,点击在画布中查看完整内容。",
|
||||
);
|
||||
expect(container.textContent).not.toContain('"queue"');
|
||||
expect(container.textContent).not.toContain('"retryable"');
|
||||
});
|
||||
|
||||
@@ -145,7 +145,9 @@ function resolveBlockLabel(
|
||||
normalizeText(record.text) ||
|
||||
normalizeText(record.markdown);
|
||||
|
||||
return label ? truncateInlineText(label, 20) : fallbackByType[block.type] || blockId;
|
||||
return label
|
||||
? truncateInlineText(label, 20)
|
||||
: fallbackByType[block.type] || blockId;
|
||||
}
|
||||
|
||||
function resolveFallbackPreview(content: string | undefined): string | null {
|
||||
|
||||
@@ -24,21 +24,23 @@ type MockResizeObserverCallback = (
|
||||
observer: unknown,
|
||||
) => void;
|
||||
|
||||
const { mockListDirectory, mockToast, resizeObserverState } = vi.hoisted(() => ({
|
||||
mockListDirectory: vi.fn(),
|
||||
mockToast: {
|
||||
success: vi.fn(),
|
||||
error: vi.fn(),
|
||||
info: vi.fn(),
|
||||
},
|
||||
resizeObserverState: {
|
||||
width: 1280,
|
||||
observers: [] as Array<{
|
||||
callback: MockResizeObserverCallback;
|
||||
target: Element | null;
|
||||
}>,
|
||||
},
|
||||
}));
|
||||
const { mockListDirectory, mockToast, resizeObserverState } = vi.hoisted(
|
||||
() => ({
|
||||
mockListDirectory: vi.fn(),
|
||||
mockToast: {
|
||||
success: vi.fn(),
|
||||
error: vi.fn(),
|
||||
info: vi.fn(),
|
||||
},
|
||||
resizeObserverState: {
|
||||
width: 1280,
|
||||
observers: [] as Array<{
|
||||
callback: MockResizeObserverCallback;
|
||||
target: Element | null;
|
||||
}>,
|
||||
},
|
||||
}),
|
||||
);
|
||||
|
||||
vi.mock("sonner", () => ({
|
||||
toast: mockToast,
|
||||
@@ -387,7 +389,8 @@ beforeEach(() => {
|
||||
disconnect = () => {
|
||||
resizeObserverState.observers = resizeObserverState.observers.filter(
|
||||
(observer) =>
|
||||
observer.callback !== this.callback || observer.target !== this.target,
|
||||
observer.callback !== this.callback ||
|
||||
observer.target !== this.target,
|
||||
);
|
||||
};
|
||||
},
|
||||
@@ -537,9 +540,9 @@ describe("CanvasWorkbenchLayout", () => {
|
||||
await flushEffects();
|
||||
|
||||
expect(mockListDirectory).toHaveBeenCalledWith("/workspace");
|
||||
expect(container.querySelector('[data-testid="preview-panel"]')?.textContent).toContain(
|
||||
"default-canvas:draft.md",
|
||||
);
|
||||
expect(
|
||||
container.querySelector('[data-testid="preview-panel"]')?.textContent,
|
||||
).toContain("default-canvas:draft.md");
|
||||
|
||||
clickButtonByLabel(container, "切换画布标签-变更");
|
||||
await flushEffects();
|
||||
@@ -558,9 +561,9 @@ describe("CanvasWorkbenchLayout", () => {
|
||||
clickButtonByLabel(container, "切换画布标签-产物");
|
||||
clickButtonByLabel(container, "选择画布产物-draft.md");
|
||||
await flushEffects();
|
||||
expect(container.querySelector('[data-testid="preview-panel"]')?.textContent).toContain(
|
||||
"artifact:draft.md",
|
||||
);
|
||||
expect(
|
||||
container.querySelector('[data-testid="preview-panel"]')?.textContent,
|
||||
).toContain("artifact:draft.md");
|
||||
|
||||
clickButtonByLabel(container, "切换画布标签-全部文件");
|
||||
await flushEffects();
|
||||
@@ -639,15 +642,15 @@ describe("CanvasWorkbenchLayout", () => {
|
||||
container.querySelector('button[aria-label="展开当前文稿检查器"]'),
|
||||
).not.toBeNull();
|
||||
expect(
|
||||
container.querySelector('[data-testid="canvas-workbench-document-inspector"]'),
|
||||
container.querySelector(
|
||||
'[data-testid="canvas-workbench-document-inspector"]',
|
||||
),
|
||||
).toBeNull();
|
||||
expect(container.textContent).toContain("当前文稿");
|
||||
expect(container.textContent).toContain("统一在右侧切换产物与版本");
|
||||
expect(container.textContent).toContain("董事会季度复盘");
|
||||
expect(container.textContent).toContain("需要优先补齐来源与版本线索。");
|
||||
expect(container.textContent).toContain(
|
||||
"默认先收起概览、来源、版本与编辑",
|
||||
);
|
||||
expect(container.textContent).toContain("默认先收起概览、来源、版本与编辑");
|
||||
|
||||
clickButtonByLabel(container, "展开当前文稿检查器");
|
||||
await flushEffects();
|
||||
@@ -656,7 +659,9 @@ describe("CanvasWorkbenchLayout", () => {
|
||||
container.querySelector('button[aria-label="折叠当前文稿检查器"]'),
|
||||
).not.toBeNull();
|
||||
expect(
|
||||
container.querySelector('[data-testid="canvas-workbench-document-inspector"]'),
|
||||
container.querySelector(
|
||||
'[data-testid="canvas-workbench-document-inspector"]',
|
||||
),
|
||||
).not.toBeNull();
|
||||
});
|
||||
|
||||
@@ -746,7 +751,9 @@ describe("CanvasWorkbenchLayout", () => {
|
||||
|
||||
await flushEffects();
|
||||
|
||||
expect(container.querySelector('[data-testid="team-preview"]')).not.toBeNull();
|
||||
expect(
|
||||
container.querySelector('[data-testid="team-preview"]'),
|
||||
).not.toBeNull();
|
||||
expect(container.querySelector('[data-testid="team-panel"]')).toBeNull();
|
||||
expect(
|
||||
container.querySelector('button[aria-label="展开画布工作台"]'),
|
||||
@@ -759,9 +766,7 @@ describe("CanvasWorkbenchLayout", () => {
|
||||
|
||||
it("teamView 的 autoFocusToken 变化时应切到 Team Workbench", async () => {
|
||||
const renderPreview = vi.fn((target: CanvasWorkbenchPreviewTarget) => (
|
||||
<div data-testid="fallback-preview">
|
||||
fallback:{target.kind}
|
||||
</div>
|
||||
<div data-testid="fallback-preview">fallback:{target.kind}</div>
|
||||
));
|
||||
const renderTeamPreview = vi.fn(
|
||||
(_options?: { stackedWorkbenchTrigger?: React.ReactNode }) => (
|
||||
@@ -811,9 +816,15 @@ describe("CanvasWorkbenchLayout", () => {
|
||||
const harness = mountHarness(baseProps);
|
||||
await flushEffects();
|
||||
|
||||
expect(harness.container.querySelector('[data-testid="team-preview"]')).toBeNull();
|
||||
expect(harness.container.querySelector('[data-testid="team-panel"]')).toBeNull();
|
||||
expect(harness.container.querySelector('[data-testid="fallback-preview"]')).not.toBeNull();
|
||||
expect(
|
||||
harness.container.querySelector('[data-testid="team-preview"]'),
|
||||
).toBeNull();
|
||||
expect(
|
||||
harness.container.querySelector('[data-testid="team-panel"]'),
|
||||
).toBeNull();
|
||||
expect(
|
||||
harness.container.querySelector('[data-testid="fallback-preview"]'),
|
||||
).not.toBeNull();
|
||||
|
||||
harness.rerender({
|
||||
...baseProps,
|
||||
@@ -824,8 +835,12 @@ describe("CanvasWorkbenchLayout", () => {
|
||||
});
|
||||
await flushEffects();
|
||||
|
||||
expect(harness.container.querySelector('[data-testid="team-preview"]')).not.toBeNull();
|
||||
expect(harness.container.querySelector('[data-testid="team-panel"]')).not.toBeNull();
|
||||
expect(
|
||||
harness.container.querySelector('[data-testid="team-preview"]'),
|
||||
).not.toBeNull();
|
||||
expect(
|
||||
harness.container.querySelector('[data-testid="team-panel"]'),
|
||||
).not.toBeNull();
|
||||
expect(harness.container.textContent).toContain("多成员实时协作");
|
||||
});
|
||||
|
||||
@@ -1006,7 +1021,9 @@ describe("CanvasWorkbenchLayout", () => {
|
||||
|
||||
await flushEffects();
|
||||
|
||||
expect(container.querySelector('[data-testid="team-preview"]')).not.toBeNull();
|
||||
expect(
|
||||
container.querySelector('[data-testid="team-preview"]'),
|
||||
).not.toBeNull();
|
||||
expect(
|
||||
container.querySelector('[data-testid="canvas-workbench-layout"]'),
|
||||
).toBeNull();
|
||||
@@ -1055,7 +1072,9 @@ describe("CanvasWorkbenchLayout", () => {
|
||||
.querySelector('[data-testid="canvas-workbench-layout"]')
|
||||
?.getAttribute("data-panel-placement"),
|
||||
).toBe("side");
|
||||
expect(container.querySelector('[data-testid="canvas-workbench-trigger"]')).toBeNull();
|
||||
expect(
|
||||
container.querySelector('[data-testid="canvas-workbench-trigger"]'),
|
||||
).toBeNull();
|
||||
expect(container.querySelector('[data-testid="team-panel"]')).toBeNull();
|
||||
expect(
|
||||
container.querySelector('button[aria-label="展开画布工作台"]'),
|
||||
|
||||
@@ -167,7 +167,9 @@ export interface CanvasWorkbenchTeamView {
|
||||
tone: "idle" | "active" | "error";
|
||||
label?: string | null;
|
||||
} | null;
|
||||
renderPreview: (options?: { stackedWorkbenchTrigger?: ReactNode }) => ReactNode;
|
||||
renderPreview: (options?: {
|
||||
stackedWorkbenchTrigger?: ReactNode;
|
||||
}) => ReactNode;
|
||||
renderPanel?: () => ReactNode;
|
||||
renderFooter?: () => ReactNode;
|
||||
}
|
||||
@@ -231,12 +233,13 @@ function resolveStackedWorkbenchMetrics(shellWidth: number): {
|
||||
maxWidth: number;
|
||||
defaultWidth: number;
|
||||
} {
|
||||
const safeShellWidth = shellWidth > 0 ? shellWidth : STACKED_LAYOUT_BREAKPOINT;
|
||||
const minWidth = Math.max(280, Math.min(340, Math.round(safeShellWidth * 0.36)));
|
||||
const maxWidth = Math.max(
|
||||
minWidth + 40,
|
||||
Math.min(480, safeShellWidth - 28),
|
||||
const safeShellWidth =
|
||||
shellWidth > 0 ? shellWidth : STACKED_LAYOUT_BREAKPOINT;
|
||||
const minWidth = Math.max(
|
||||
280,
|
||||
Math.min(340, Math.round(safeShellWidth * 0.36)),
|
||||
);
|
||||
const maxWidth = Math.max(minWidth + 40, Math.min(480, safeShellWidth - 28));
|
||||
const defaultWidth = clamp(
|
||||
Math.round(safeShellWidth * 0.42),
|
||||
minWidth,
|
||||
@@ -314,7 +317,9 @@ function resolvePreviousArtifactContent(
|
||||
if (candidate.id === artifact.id) {
|
||||
continue;
|
||||
}
|
||||
const candidatePath = normalizePath(resolveArtifactProtocolFilePath(candidate));
|
||||
const candidatePath = normalizePath(
|
||||
resolveArtifactProtocolFilePath(candidate),
|
||||
);
|
||||
if (candidatePath === currentPath && candidate.content.trim()) {
|
||||
return candidate.content;
|
||||
}
|
||||
@@ -342,10 +347,15 @@ function resolveMappedPreviousContentForPath(
|
||||
workspaceRoot,
|
||||
version.metadata?.sourceFileName,
|
||||
);
|
||||
return versionPath ? normalizePath(versionPath) === normalizedTarget : false;
|
||||
return versionPath
|
||||
? normalizePath(versionPath) === normalizedTarget
|
||||
: false;
|
||||
});
|
||||
if (matchedVersion) {
|
||||
return resolvePreviousVersionContent(matchedVersion, canvasState.versions);
|
||||
return resolvePreviousVersionContent(
|
||||
matchedVersion,
|
||||
canvasState.versions,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -354,7 +364,9 @@ function resolveMappedPreviousContentForPath(
|
||||
workspaceRoot,
|
||||
resolveArtifactProtocolFilePath(artifact),
|
||||
);
|
||||
return artifactPath ? normalizePath(artifactPath) === normalizedTarget : false;
|
||||
return artifactPath
|
||||
? normalizePath(artifactPath) === normalizedTarget
|
||||
: false;
|
||||
});
|
||||
|
||||
return matchedArtifact
|
||||
@@ -384,7 +396,9 @@ function buildEntries(
|
||||
absolutePath: resolveAbsoluteWorkspacePath(workspaceRoot, filePath),
|
||||
previewText: resolveArtifactPreviewText(artifact),
|
||||
createdAt: artifact.updatedAt || artifact.createdAt,
|
||||
badgeLabel: writePhase ? formatArtifactWritePhaseLabel(writePhase) : undefined,
|
||||
badgeLabel: writePhase
|
||||
? formatArtifactWritePhaseLabel(writePhase)
|
||||
: undefined,
|
||||
kindLabel: "产物",
|
||||
};
|
||||
});
|
||||
@@ -410,7 +424,8 @@ function buildEntries(
|
||||
previewText: version.content.trim().slice(0, 180),
|
||||
createdAt: version.createdAt,
|
||||
isCurrent: version.id === canvasState.currentVersionId,
|
||||
badgeLabel: version.id === canvasState.currentVersionId ? "当前" : undefined,
|
||||
badgeLabel:
|
||||
version.id === canvasState.currentVersionId ? "当前" : undefined,
|
||||
kindLabel: "版本",
|
||||
})),
|
||||
);
|
||||
@@ -427,7 +442,10 @@ function buildEntries(
|
||||
title: extractFileNameFromPath(taskFile.name),
|
||||
subtitle: taskFile.name,
|
||||
filePath: taskFile.name,
|
||||
absolutePath: resolveAbsoluteWorkspacePath(workspaceRoot, taskFile.name),
|
||||
absolutePath: resolveAbsoluteWorkspacePath(
|
||||
workspaceRoot,
|
||||
taskFile.name,
|
||||
),
|
||||
previewText: taskFile.content?.trim().slice(0, 180),
|
||||
createdAt: taskFile.updatedAt,
|
||||
badgeLabel: taskFile.type === "document" ? "文档" : undefined,
|
||||
@@ -445,9 +463,7 @@ function buildEntries(
|
||||
});
|
||||
}
|
||||
|
||||
function renderDiffState(
|
||||
diffLines: CanvasWorkbenchDiffLine[],
|
||||
): ReactNode {
|
||||
function renderDiffState(diffLines: CanvasWorkbenchDiffLine[]): ReactNode {
|
||||
return (
|
||||
<div className={cn("overflow-hidden", WORKBENCH_PANEL_CLASSNAME)}>
|
||||
<div className="max-h-[28rem] overflow-auto">
|
||||
@@ -464,7 +480,9 @@ function renderDiffState(
|
||||
<span className="select-none text-center">
|
||||
{line.type === "add" ? "+" : line.type === "remove" ? "-" : " "}
|
||||
</span>
|
||||
<span className="whitespace-pre-wrap break-all">{line.value || " "}</span>
|
||||
<span className="whitespace-pre-wrap break-all">
|
||||
{line.value || " "}
|
||||
</span>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
@@ -487,36 +505,42 @@ export const CanvasWorkbenchLayout = memo(function CanvasWorkbenchLayout({
|
||||
onLayoutModeChange,
|
||||
teamView = null,
|
||||
}: CanvasWorkbenchLayoutProps) {
|
||||
const shouldPreferTeamTabByDefault = teamView?.enabled === true && !defaultPreview;
|
||||
const shouldPreferTeamTabByDefault =
|
||||
teamView?.enabled === true && !defaultPreview;
|
||||
const shellRef = useRef<HTMLDivElement | null>(null);
|
||||
const stackedResizeCleanupRef = useRef<(() => void) | null>(null);
|
||||
const [activeTab, setActiveTab] = useState<CanvasWorkbenchTab>(() =>
|
||||
shouldPreferTeamTabByDefault ? "team" : "artifacts",
|
||||
);
|
||||
const [collapsed, setCollapsed] = useState(() => shouldPreferTeamTabByDefault);
|
||||
const [collapsed, setCollapsed] = useState(
|
||||
() => shouldPreferTeamTabByDefault,
|
||||
);
|
||||
const [isStackedLayout, setIsStackedLayout] = useState(false);
|
||||
const [stackedWorkbenchOpen, setStackedWorkbenchOpen] = useState(false);
|
||||
const [shellWidth, setShellWidth] = useState(STACKED_LAYOUT_BREAKPOINT);
|
||||
const [stackedWorkbenchWidth, setStackedWorkbenchWidth] = useState<number | null>(
|
||||
null,
|
||||
);
|
||||
const [documentInspectorCollapsed, setDocumentInspectorCollapsed] = useState(true);
|
||||
const [stackedWorkbenchWidth, setStackedWorkbenchWidth] = useState<
|
||||
number | null
|
||||
>(null);
|
||||
const [documentInspectorCollapsed, setDocumentInspectorCollapsed] =
|
||||
useState(true);
|
||||
const [selectedKey, setSelectedKey] = useState<string | null>(null);
|
||||
const [artifactDocumentController, setArtifactDocumentController] =
|
||||
useState<ArtifactWorkbenchDocumentController | null>(null);
|
||||
const [directoryCache, setDirectoryCache] = useState<Record<string, DirectoryListing>>(
|
||||
{},
|
||||
);
|
||||
const [loadingDirectories, setLoadingDirectories] = useState<Record<string, boolean>>(
|
||||
{},
|
||||
);
|
||||
const [expandedDirectories, setExpandedDirectories] = useState<Record<string, boolean>>(
|
||||
{},
|
||||
);
|
||||
const [directoryCache, setDirectoryCache] = useState<
|
||||
Record<string, DirectoryListing>
|
||||
>({});
|
||||
const [loadingDirectories, setLoadingDirectories] = useState<
|
||||
Record<string, boolean>
|
||||
>({});
|
||||
const [expandedDirectories, setExpandedDirectories] = useState<
|
||||
Record<string, boolean>
|
||||
>({});
|
||||
const [workspaceFileSelections, setWorkspaceFileSelections] = useState<
|
||||
Record<string, WorkspaceFileSelection>
|
||||
>({});
|
||||
const availableTabs = useMemo<Array<{ key: CanvasWorkbenchTab; label: string }>>(
|
||||
const availableTabs = useMemo<
|
||||
Array<{ key: CanvasWorkbenchTab; label: string }>
|
||||
>(
|
||||
() => [
|
||||
...(teamView?.enabled ? [{ key: "team" as const, label: "团队" }] : []),
|
||||
{ key: "artifacts" as const, label: "产物" },
|
||||
@@ -559,7 +583,10 @@ export const CanvasWorkbenchLayout = memo(function CanvasWorkbenchLayout({
|
||||
}, [defaultPreview, entries, entryMap, selectedFileId]);
|
||||
|
||||
useEffect(() => {
|
||||
if (selectedKey && (entryMap.has(selectedKey) || selectedKey.startsWith("workspace-file:"))) {
|
||||
if (
|
||||
selectedKey &&
|
||||
(entryMap.has(selectedKey) || selectedKey.startsWith("workspace-file:"))
|
||||
) {
|
||||
return;
|
||||
}
|
||||
setSelectedKey(fallbackEntryKey);
|
||||
@@ -593,12 +620,7 @@ export const CanvasWorkbenchLayout = memo(function CanvasWorkbenchLayout({
|
||||
return;
|
||||
}
|
||||
void loadDirectory(workspaceRoot);
|
||||
}, [
|
||||
directoryCache,
|
||||
loadDirectory,
|
||||
workspaceRoot,
|
||||
workspaceUnavailable,
|
||||
]);
|
||||
}, [directoryCache, loadDirectory, workspaceRoot, workspaceUnavailable]);
|
||||
|
||||
useEffect(() => {
|
||||
const node = shellRef.current;
|
||||
@@ -615,7 +637,9 @@ export const CanvasWorkbenchLayout = memo(function CanvasWorkbenchLayout({
|
||||
};
|
||||
|
||||
const fallbackWidth =
|
||||
node.getBoundingClientRect().width || node.clientWidth || window.innerWidth;
|
||||
node.getBoundingClientRect().width ||
|
||||
node.clientWidth ||
|
||||
window.innerWidth;
|
||||
updateLayout(fallbackWidth);
|
||||
|
||||
if (typeof ResizeObserver === "undefined") {
|
||||
@@ -717,11 +741,7 @@ export const CanvasWorkbenchLayout = memo(function CanvasWorkbenchLayout({
|
||||
setStackedWorkbenchOpen(true);
|
||||
emitCompactRightPanelOpen({ source: "workbench" });
|
||||
}
|
||||
}, [
|
||||
teamView?.autoFocusToken,
|
||||
teamView?.enabled,
|
||||
usesStackedWorkbenchLayout,
|
||||
]);
|
||||
}, [teamView?.autoFocusToken, teamView?.enabled, usesStackedWorkbenchLayout]);
|
||||
|
||||
const handleToggleDirectory = useCallback(
|
||||
(path: string) => {
|
||||
@@ -797,7 +817,9 @@ export const CanvasWorkbenchLayout = memo(function CanvasWorkbenchLayout({
|
||||
);
|
||||
|
||||
const effectiveKey = selectedKey || fallbackEntryKey;
|
||||
const selectedEntry = effectiveKey ? entryMap.get(effectiveKey) || null : null;
|
||||
const selectedEntry = effectiveKey
|
||||
? entryMap.get(effectiveKey) || null
|
||||
: null;
|
||||
const selectedWorkspaceFile = effectiveKey?.startsWith("workspace-file:")
|
||||
? workspaceFileSelections[effectiveKey] || null
|
||||
: null;
|
||||
@@ -938,7 +960,13 @@ export const CanvasWorkbenchLayout = memo(function CanvasWorkbenchLayout({
|
||||
kind: "empty",
|
||||
title: "暂无可预览内容",
|
||||
};
|
||||
}, [activeTab, defaultPreview, selectedEntry, selectedWorkspaceFile, teamView]);
|
||||
}, [
|
||||
activeTab,
|
||||
defaultPreview,
|
||||
selectedEntry,
|
||||
selectedWorkspaceFile,
|
||||
teamView,
|
||||
]);
|
||||
|
||||
const currentContent = useMemo(() => {
|
||||
if (currentTarget.kind === "default-canvas") {
|
||||
@@ -979,8 +1007,14 @@ export const CanvasWorkbenchLayout = memo(function CanvasWorkbenchLayout({
|
||||
return resolvePreviousArtifactContent(selectedEntry.artifact, artifacts);
|
||||
}
|
||||
|
||||
if (selectedEntry.source === "document-version" && isDocumentCanvasState(canvasState)) {
|
||||
return resolvePreviousVersionContent(selectedEntry.version, canvasState.versions);
|
||||
if (
|
||||
selectedEntry.source === "document-version" &&
|
||||
isDocumentCanvasState(canvasState)
|
||||
) {
|
||||
return resolvePreviousVersionContent(
|
||||
selectedEntry.version,
|
||||
canvasState.versions,
|
||||
);
|
||||
}
|
||||
|
||||
if (selectedEntry.absolutePath) {
|
||||
@@ -1030,9 +1064,7 @@ export const CanvasWorkbenchLayout = memo(function CanvasWorkbenchLayout({
|
||||
await navigator.clipboard.writeText(selectionPath);
|
||||
toast.success("已复制路径");
|
||||
} catch (error) {
|
||||
toast.error(
|
||||
error instanceof Error ? error.message : "复制路径失败",
|
||||
);
|
||||
toast.error(error instanceof Error ? error.message : "复制路径失败");
|
||||
}
|
||||
}, [selectionPath]);
|
||||
|
||||
@@ -1056,7 +1088,8 @@ export const CanvasWorkbenchLayout = memo(function CanvasWorkbenchLayout({
|
||||
}
|
||||
|
||||
const showDocumentInspector = Boolean(
|
||||
selectedEntry?.source === "artifact" && artifactDocumentController?.document,
|
||||
selectedEntry?.source === "artifact" &&
|
||||
artifactDocumentController?.document,
|
||||
);
|
||||
const documentTitle =
|
||||
artifactDocumentController?.document?.title?.trim() ||
|
||||
@@ -1142,7 +1175,12 @@ export const CanvasWorkbenchLayout = memo(function CanvasWorkbenchLayout({
|
||||
</section>
|
||||
|
||||
{showDocumentInspector && artifactDocumentController ? (
|
||||
<section className={cn(WORKBENCH_PANEL_CLASSNAME, "overflow-hidden bg-slate-50/80")}>
|
||||
<section
|
||||
className={cn(
|
||||
WORKBENCH_PANEL_CLASSNAME,
|
||||
"overflow-hidden bg-slate-50/80",
|
||||
)}
|
||||
>
|
||||
<button
|
||||
type="button"
|
||||
aria-label={documentInspectorButtonLabel}
|
||||
@@ -1258,7 +1296,9 @@ export const CanvasWorkbenchLayout = memo(function CanvasWorkbenchLayout({
|
||||
) : (
|
||||
<Folder className="h-4 w-4 shrink-0 text-amber-600" />
|
||||
)
|
||||
) : entry.name.match(/\.(ts|tsx|js|jsx|rs|json|yml|yaml|toml)$/i) ? (
|
||||
) : entry.name.match(
|
||||
/\.(ts|tsx|js|jsx|rs|json|yml|yaml|toml)$/i,
|
||||
) ? (
|
||||
<FileCode2 className="h-4 w-4 shrink-0 text-sky-600" />
|
||||
) : (
|
||||
<FileText className="h-4 w-4 shrink-0 text-slate-500" />
|
||||
@@ -1268,7 +1308,9 @@ export const CanvasWorkbenchLayout = memo(function CanvasWorkbenchLayout({
|
||||
<Loader2 className="h-4 w-4 shrink-0 animate-spin" />
|
||||
) : null}
|
||||
</button>
|
||||
{isDirectory && isExpanded ? renderDirectoryNode(entry.path, depth + 1) : null}
|
||||
{isDirectory && isExpanded
|
||||
? renderDirectoryNode(entry.path, depth + 1)
|
||||
: null}
|
||||
</div>
|
||||
);
|
||||
});
|
||||
@@ -1299,7 +1341,9 @@ export const CanvasWorkbenchLayout = memo(function CanvasWorkbenchLayout({
|
||||
<div className="text-xs font-medium uppercase tracking-[0.08em] text-slate-500">
|
||||
Workspace Root
|
||||
</div>
|
||||
<div className="mt-1 truncate text-sm text-foreground">{workspaceRoot}</div>
|
||||
<div className="mt-1 truncate text-sm text-foreground">
|
||||
{workspaceRoot}
|
||||
</div>
|
||||
</div>
|
||||
<button
|
||||
type="button"
|
||||
@@ -1400,13 +1444,11 @@ export const CanvasWorkbenchLayout = memo(function CanvasWorkbenchLayout({
|
||||
return teamView.renderPanel ? (
|
||||
teamView.renderPanel()
|
||||
) : (
|
||||
<div className={WORKBENCH_MUTED_PANEL_CLASSNAME}>
|
||||
团队工作台已启用。
|
||||
</div>
|
||||
<div className={WORKBENCH_MUTED_PANEL_CLASSNAME}>团队工作台已启用。</div>
|
||||
);
|
||||
};
|
||||
|
||||
const renderTabButtons = (stacked: boolean) => (
|
||||
const renderTabButtons = (stacked: boolean) =>
|
||||
stacked ? (
|
||||
<div className="mt-3 flex gap-2 overflow-x-auto pb-1 [scrollbar-width:none] [&::-webkit-scrollbar]:hidden">
|
||||
{availableTabs.map((tab) => (
|
||||
@@ -1453,8 +1495,7 @@ export const CanvasWorkbenchLayout = memo(function CanvasWorkbenchLayout({
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
)
|
||||
);
|
||||
);
|
||||
|
||||
const renderWorkbenchHeader = (
|
||||
stacked: boolean,
|
||||
@@ -1463,7 +1504,9 @@ export const CanvasWorkbenchLayout = memo(function CanvasWorkbenchLayout({
|
||||
},
|
||||
) => {
|
||||
const headerTitle =
|
||||
activeTab === "team" ? teamView?.title || currentTarget.title : currentTarget.title;
|
||||
activeTab === "team"
|
||||
? teamView?.title || currentTarget.title
|
||||
: currentTarget.title;
|
||||
|
||||
if (stacked) {
|
||||
return (
|
||||
@@ -1558,7 +1601,7 @@ export const CanvasWorkbenchLayout = memo(function CanvasWorkbenchLayout({
|
||||
);
|
||||
};
|
||||
|
||||
const renderWorkbenchFooter = (stacked: boolean) => (
|
||||
const renderWorkbenchFooter = (stacked: boolean) =>
|
||||
activeTab === "team" && teamView?.renderFooter ? (
|
||||
<div
|
||||
className={cn(
|
||||
@@ -1569,83 +1612,82 @@ export const CanvasWorkbenchLayout = memo(function CanvasWorkbenchLayout({
|
||||
{teamView.renderFooter()}
|
||||
</div>
|
||||
) : (
|
||||
<div
|
||||
className={cn(
|
||||
"border-t border-slate-200/80",
|
||||
stacked ? "px-3 py-3" : "px-4 py-3",
|
||||
)}
|
||||
>
|
||||
<div className="flex flex-wrap gap-2">
|
||||
<button
|
||||
type="button"
|
||||
aria-label="复制当前路径"
|
||||
disabled={!selectionPath}
|
||||
onClick={() => {
|
||||
void handleCopyPath();
|
||||
}}
|
||||
className={cn(
|
||||
"inline-flex items-center gap-1 rounded-xl border px-3 py-2 text-xs transition-colors disabled:cursor-not-allowed disabled:opacity-50",
|
||||
WORKBENCH_GHOST_BUTTON_CLASSNAME,
|
||||
)}
|
||||
>
|
||||
<Copy className="h-3.5 w-3.5" />
|
||||
复制路径
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
aria-label="定位当前文件"
|
||||
disabled={!selectionPath}
|
||||
onClick={() => {
|
||||
if (selectionPath) {
|
||||
void onRevealPath(selectionPath);
|
||||
}
|
||||
}}
|
||||
className={cn(
|
||||
"inline-flex items-center gap-1 rounded-xl border px-3 py-2 text-xs transition-colors disabled:cursor-not-allowed disabled:opacity-50",
|
||||
WORKBENCH_GHOST_BUTTON_CLASSNAME,
|
||||
)}
|
||||
>
|
||||
<FolderOpen className="h-3.5 w-3.5" />
|
||||
定位
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
aria-label="系统打开当前文件"
|
||||
disabled={!selectionPath}
|
||||
onClick={() => {
|
||||
if (selectionPath) {
|
||||
void onOpenPath(selectionPath);
|
||||
}
|
||||
}}
|
||||
className={cn(
|
||||
"inline-flex items-center gap-1 rounded-xl border px-3 py-2 text-xs transition-colors disabled:cursor-not-allowed disabled:opacity-50",
|
||||
WORKBENCH_GHOST_BUTTON_CLASSNAME,
|
||||
)}
|
||||
>
|
||||
<ExternalLink className="h-3.5 w-3.5" />
|
||||
打开
|
||||
</button>
|
||||
</div>
|
||||
{previousContent !== null ? (
|
||||
<div className="mt-3 flex items-center gap-2 text-[11px] text-slate-500">
|
||||
<GitCompare className="h-3.5 w-3.5" />
|
||||
已关联到上一版本,可在“变更”中查看差异。
|
||||
<div
|
||||
className={cn(
|
||||
"border-t border-slate-200/80",
|
||||
stacked ? "px-3 py-3" : "px-4 py-3",
|
||||
)}
|
||||
>
|
||||
<div className="flex flex-wrap gap-2">
|
||||
<button
|
||||
type="button"
|
||||
aria-label="复制当前路径"
|
||||
disabled={!selectionPath}
|
||||
onClick={() => {
|
||||
void handleCopyPath();
|
||||
}}
|
||||
className={cn(
|
||||
"inline-flex items-center gap-1 rounded-xl border px-3 py-2 text-xs transition-colors disabled:cursor-not-allowed disabled:opacity-50",
|
||||
WORKBENCH_GHOST_BUTTON_CLASSNAME,
|
||||
)}
|
||||
>
|
||||
<Copy className="h-3.5 w-3.5" />
|
||||
复制路径
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
aria-label="定位当前文件"
|
||||
disabled={!selectionPath}
|
||||
onClick={() => {
|
||||
if (selectionPath) {
|
||||
void onRevealPath(selectionPath);
|
||||
}
|
||||
}}
|
||||
className={cn(
|
||||
"inline-flex items-center gap-1 rounded-xl border px-3 py-2 text-xs transition-colors disabled:cursor-not-allowed disabled:opacity-50",
|
||||
WORKBENCH_GHOST_BUTTON_CLASSNAME,
|
||||
)}
|
||||
>
|
||||
<FolderOpen className="h-3.5 w-3.5" />
|
||||
定位
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
aria-label="系统打开当前文件"
|
||||
disabled={!selectionPath}
|
||||
onClick={() => {
|
||||
if (selectionPath) {
|
||||
void onOpenPath(selectionPath);
|
||||
}
|
||||
}}
|
||||
className={cn(
|
||||
"inline-flex items-center gap-1 rounded-xl border px-3 py-2 text-xs transition-colors disabled:cursor-not-allowed disabled:opacity-50",
|
||||
WORKBENCH_GHOST_BUTTON_CLASSNAME,
|
||||
)}
|
||||
>
|
||||
<ExternalLink className="h-3.5 w-3.5" />
|
||||
打开
|
||||
</button>
|
||||
</div>
|
||||
) : null}
|
||||
</div>
|
||||
)
|
||||
);
|
||||
{previousContent !== null ? (
|
||||
<div className="mt-3 flex items-center gap-2 text-[11px] text-slate-500">
|
||||
<GitCompare className="h-3.5 w-3.5" />
|
||||
已关联到上一版本,可在“变更”中查看差异。
|
||||
</div>
|
||||
) : null}
|
||||
</div>
|
||||
);
|
||||
|
||||
const renderActiveTab = () =>
|
||||
activeTab === "team"
|
||||
? renderTeamTab()
|
||||
: activeTab === "artifacts"
|
||||
? renderEntriesTab()
|
||||
: activeTab === "files"
|
||||
? renderFilesTab()
|
||||
: activeTab === "changes"
|
||||
? renderChangesTab()
|
||||
: renderPreviewTab();
|
||||
? renderEntriesTab()
|
||||
: activeTab === "files"
|
||||
? renderFilesTab()
|
||||
: activeTab === "changes"
|
||||
? renderChangesTab()
|
||||
: renderPreviewTab();
|
||||
|
||||
const handleStartStackedResize = useCallback(
|
||||
(event: ReactMouseEvent<HTMLDivElement>) => {
|
||||
@@ -1723,38 +1765,38 @@ export const CanvasWorkbenchLayout = memo(function CanvasWorkbenchLayout({
|
||||
!usesStackedWorkbenchLayout;
|
||||
|
||||
return (
|
||||
<div
|
||||
ref={shellRef}
|
||||
data-testid="canvas-workbench-shell"
|
||||
data-layout-mode={usesStackedWorkbenchLayout ? "stacked" : "split"}
|
||||
className={cn(
|
||||
"relative h-full min-h-0 overflow-hidden rounded-[28px]",
|
||||
useFramelessTeamShell
|
||||
? "border-0 bg-transparent shadow-none"
|
||||
: "border border-slate-200 bg-white shadow-sm shadow-slate-950/5",
|
||||
usesStackedWorkbenchLayout ? "block" : "flex flex-row",
|
||||
)}
|
||||
>
|
||||
<div
|
||||
ref={shellRef}
|
||||
data-testid="canvas-workbench-shell"
|
||||
data-layout-mode={usesStackedWorkbenchLayout ? "stacked" : "split"}
|
||||
data-testid="canvas-workbench-preview-region"
|
||||
className={cn(
|
||||
"relative h-full min-h-0 overflow-hidden rounded-[28px]",
|
||||
useFramelessTeamShell
|
||||
? "border-0 bg-transparent shadow-none"
|
||||
: "border border-slate-200 bg-white shadow-sm shadow-slate-950/5",
|
||||
usesStackedWorkbenchLayout ? "block" : "flex flex-row",
|
||||
"min-w-0 overflow-hidden",
|
||||
usesStackedWorkbenchLayout ? "h-full" : "flex-1 h-full",
|
||||
)}
|
||||
>
|
||||
<div
|
||||
data-testid="canvas-workbench-preview-region"
|
||||
className={cn(
|
||||
"min-w-0 overflow-hidden",
|
||||
usesStackedWorkbenchLayout ? "h-full" : "flex-1 h-full",
|
||||
)}
|
||||
>
|
||||
{activeTab === "team" && teamView?.enabled
|
||||
? teamView.renderPreview(
|
||||
preferFullscreenTeamPreview
|
||||
? undefined
|
||||
: {
|
||||
stackedWorkbenchTrigger,
|
||||
},
|
||||
)
|
||||
: renderPreview(currentTarget, {
|
||||
stackedWorkbenchTrigger,
|
||||
onArtifactDocumentControllerChange:
|
||||
handleArtifactDocumentControllerChange,
|
||||
})}
|
||||
{activeTab === "team" && teamView?.enabled
|
||||
? teamView.renderPreview(
|
||||
preferFullscreenTeamPreview
|
||||
? undefined
|
||||
: {
|
||||
stackedWorkbenchTrigger,
|
||||
},
|
||||
)
|
||||
: renderPreview(currentTarget, {
|
||||
stackedWorkbenchTrigger,
|
||||
onArtifactDocumentControllerChange:
|
||||
handleArtifactDocumentControllerChange,
|
||||
})}
|
||||
</div>
|
||||
|
||||
{preferFullscreenTeamPreview ? null : usesStackedWorkbenchLayout ? (
|
||||
@@ -1787,11 +1829,11 @@ export const CanvasWorkbenchLayout = memo(function CanvasWorkbenchLayout({
|
||||
<span className="h-10 w-1.5 rounded-full bg-slate-200" />
|
||||
</div>
|
||||
<div className="flex min-h-0 flex-1 flex-col pl-5">
|
||||
{renderWorkbenchHeader(true)}
|
||||
<div className="flex-1 overflow-auto px-3 py-3">
|
||||
{renderActiveTab()}
|
||||
</div>
|
||||
{renderWorkbenchFooter(true)}
|
||||
{renderWorkbenchHeader(true)}
|
||||
<div className="flex-1 overflow-auto px-3 py-3">
|
||||
{renderActiveTab()}
|
||||
</div>
|
||||
{renderWorkbenchFooter(true)}
|
||||
</div>
|
||||
</section>
|
||||
</>
|
||||
@@ -1840,7 +1882,9 @@ export const CanvasWorkbenchLayout = memo(function CanvasWorkbenchLayout({
|
||||
{renderWorkbenchHeader(false, {
|
||||
showCollapseButton: true,
|
||||
})}
|
||||
<div className="flex-1 overflow-auto px-4 py-4">{renderActiveTab()}</div>
|
||||
<div className="flex-1 overflow-auto px-4 py-4">
|
||||
{renderActiveTab()}
|
||||
</div>
|
||||
{renderWorkbenchFooter(false)}
|
||||
</>
|
||||
)}
|
||||
|
||||
@@ -56,10 +56,9 @@ vi.mock("@/lib/api/agentRuntime", async () => {
|
||||
});
|
||||
|
||||
vi.mock("@/lib/api/agentProtocol", async () => {
|
||||
const actual =
|
||||
await vi.importActual<typeof import("@/lib/api/agentProtocol")>(
|
||||
"@/lib/api/agentProtocol",
|
||||
);
|
||||
const actual = await vi.importActual<
|
||||
typeof import("@/lib/api/agentProtocol")
|
||||
>("@/lib/api/agentProtocol");
|
||||
return {
|
||||
...actual,
|
||||
parseAgentEvent: mockParseAgentEvent,
|
||||
@@ -67,9 +66,10 @@ vi.mock("@/lib/api/agentProtocol", async () => {
|
||||
});
|
||||
|
||||
vi.mock("@/lib/dev-bridge", async () => {
|
||||
const actual = await vi.importActual<typeof import("@/lib/dev-bridge")>(
|
||||
"@/lib/dev-bridge",
|
||||
);
|
||||
const actual =
|
||||
await vi.importActual<typeof import("@/lib/dev-bridge")>(
|
||||
"@/lib/dev-bridge",
|
||||
);
|
||||
|
||||
return {
|
||||
...actual,
|
||||
@@ -121,7 +121,9 @@ const mountedRoots: MountedHarness[] = [];
|
||||
|
||||
interface MountOptions {
|
||||
onManageProviders?: () => void;
|
||||
chatModelSelectorProps?: Partial<React.ComponentProps<typeof ChatModelSelector>>;
|
||||
chatModelSelectorProps?: Partial<
|
||||
React.ComponentProps<typeof ChatModelSelector>
|
||||
>;
|
||||
}
|
||||
|
||||
function createModel(id: string, providerId: string) {
|
||||
|
||||
@@ -1,8 +1,5 @@
|
||||
import React from "react";
|
||||
import {
|
||||
ModelSelector,
|
||||
type ModelSelectorProps,
|
||||
} from "@/components/input-kit";
|
||||
import { ModelSelector, type ModelSelectorProps } from "@/components/input-kit";
|
||||
|
||||
export type ChatModelSelectorProps = ModelSelectorProps;
|
||||
|
||||
|
||||
@@ -137,8 +137,12 @@ describe("ChatNavbar", () => {
|
||||
});
|
||||
|
||||
expect(container.querySelector('[aria-label="切换历史"]')).not.toBeNull();
|
||||
expect(container.querySelector('[aria-label="展开Harness"]')).not.toBeNull();
|
||||
expect(container.querySelector('[data-testid="project-selector"]')).toBeNull();
|
||||
expect(
|
||||
container.querySelector('[aria-label="展开Harness"]'),
|
||||
).not.toBeNull();
|
||||
expect(
|
||||
container.querySelector('[data-testid="project-selector"]'),
|
||||
).toBeNull();
|
||||
});
|
||||
|
||||
it("任务中心顶栏应展示轻量上下文提示", () => {
|
||||
|
||||
@@ -46,8 +46,7 @@ interface ChatNavbarProps {
|
||||
const toolbarGroupClassName =
|
||||
"flex items-center rounded-[20px] border border-slate-200/80 bg-white/90 p-1.5 shadow-sm shadow-slate-950/5 backdrop-blur-sm";
|
||||
|
||||
const toolbarDividerClassName =
|
||||
"mx-1.5 h-6 w-px shrink-0 bg-slate-200/80";
|
||||
const toolbarDividerClassName = "mx-1.5 h-6 w-px shrink-0 bg-slate-200/80";
|
||||
|
||||
const toolbarEmbeddedButtonClassName =
|
||||
"h-9 rounded-2xl border border-transparent px-3.5 text-xs shadow-none";
|
||||
@@ -111,7 +110,8 @@ export const ChatNavbar: React.FC<ChatNavbarProps> = ({
|
||||
Boolean(onBackToProjectManagement));
|
||||
const showWorkspaceTools = showHistoryToggle || showCanvasToggle;
|
||||
const showProjectSelector = !isWorkspaceCompact;
|
||||
const showCompactSettingsButton = isWorkspaceCompact && Boolean(onToggleSettings);
|
||||
const showCompactSettingsButton =
|
||||
isWorkspaceCompact && Boolean(onToggleSettings);
|
||||
const compactProjectSelectorClassName = isWorkspaceCompact
|
||||
? "min-w-[184px] max-w-[248px]"
|
||||
: "min-w-[196px] max-w-[280px]";
|
||||
@@ -141,7 +141,10 @@ export const ChatNavbar: React.FC<ChatNavbarProps> = ({
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="sm"
|
||||
className={cn(embeddedButtonClassName, toolbarTextButtonClassName)}
|
||||
className={cn(
|
||||
embeddedButtonClassName,
|
||||
toolbarTextButtonClassName,
|
||||
)}
|
||||
onClick={onBackToResources}
|
||||
>
|
||||
<FolderOpen size={16} className="mr-0.5" />
|
||||
@@ -155,7 +158,10 @@ export const ChatNavbar: React.FC<ChatNavbarProps> = ({
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="sm"
|
||||
className={cn(embeddedButtonClassName, toolbarTextButtonClassName)}
|
||||
className={cn(
|
||||
embeddedButtonClassName,
|
||||
toolbarTextButtonClassName,
|
||||
)}
|
||||
onClick={onBackToProjectManagement}
|
||||
>
|
||||
项目管理
|
||||
@@ -274,14 +280,19 @@ export const ChatNavbar: React.FC<ChatNavbarProps> = ({
|
||||
type="button"
|
||||
variant="ghost"
|
||||
size="sm"
|
||||
className={cn(embeddedButtonClassName, toolbarTextButtonClassName)}
|
||||
className={cn(
|
||||
embeddedButtonClassName,
|
||||
toolbarTextButtonClassName,
|
||||
)}
|
||||
onClick={onCompactContext}
|
||||
disabled={contextCompactionRunning}
|
||||
aria-label="压缩上下文"
|
||||
title="压缩上下文"
|
||||
>
|
||||
<Box size={14} />
|
||||
<span>{contextCompactionRunning ? "压缩中..." : "压缩上下文"}</span>
|
||||
<span>
|
||||
{contextCompactionRunning ? "压缩中..." : "压缩上下文"}
|
||||
</span>
|
||||
</Button>
|
||||
) : null}
|
||||
|
||||
@@ -332,7 +343,6 @@ export const ChatNavbar: React.FC<ChatNavbarProps> = ({
|
||||
) : null}
|
||||
</div>
|
||||
) : null}
|
||||
|
||||
</div>
|
||||
</Navbar>
|
||||
);
|
||||
|
||||
@@ -17,9 +17,15 @@ vi.mock("@/components/ui/badge", () => ({
|
||||
}));
|
||||
|
||||
vi.mock("@/components/ui/dropdown-menu", () => ({
|
||||
DropdownMenu: ({ children }: { children: React.ReactNode }) => <div>{children}</div>,
|
||||
DropdownMenuTrigger: ({ children }: { children: React.ReactNode }) => <>{children}</>,
|
||||
DropdownMenuContent: ({ children }: { children: React.ReactNode }) => <div>{children}</div>,
|
||||
DropdownMenu: ({ children }: { children: React.ReactNode }) => (
|
||||
<div>{children}</div>
|
||||
),
|
||||
DropdownMenuTrigger: ({ children }: { children: React.ReactNode }) => (
|
||||
<>{children}</>
|
||||
),
|
||||
DropdownMenuContent: ({ children }: { children: React.ReactNode }) => (
|
||||
<div>{children}</div>
|
||||
),
|
||||
DropdownMenuItem: ({
|
||||
children,
|
||||
onClick,
|
||||
@@ -238,7 +244,9 @@ describe("ChatSidebar", () => {
|
||||
|
||||
const taskHeading = container.querySelector(
|
||||
'[data-testid="task-section-heading"]',
|
||||
) as (HTMLDivElement & { scrollIntoView?: ReturnType<typeof vi.fn> }) | null;
|
||||
) as
|
||||
| (HTMLDivElement & { scrollIntoView?: ReturnType<typeof vi.fn> })
|
||||
| null;
|
||||
expect(taskHeading).toBeTruthy();
|
||||
|
||||
const scrollIntoView = vi.fn();
|
||||
@@ -268,9 +276,7 @@ describe("ChatSidebar", () => {
|
||||
const container = renderSidebar({ onSwitchTopic });
|
||||
const taskItem = Array.from(
|
||||
container.querySelectorAll('[role="button"]'),
|
||||
).find(
|
||||
(element) => element.textContent?.includes("任务一"),
|
||||
);
|
||||
).find((element) => element.textContent?.includes("任务一"));
|
||||
expect(taskItem).toBeTruthy();
|
||||
if (taskItem) {
|
||||
act(() => {
|
||||
@@ -475,9 +481,7 @@ describe("ChatSidebar", () => {
|
||||
expect(resumeButton).toBeTruthy();
|
||||
|
||||
act(() => {
|
||||
resumeButton?.dispatchEvent(
|
||||
new MouseEvent("click", { bubbles: true }),
|
||||
);
|
||||
resumeButton?.dispatchEvent(new MouseEvent("click", { bubbles: true }));
|
||||
});
|
||||
|
||||
expect(onResumeTask).toHaveBeenCalledWith("topic-1", "user_action");
|
||||
@@ -596,7 +600,9 @@ describe("ChatSidebar", () => {
|
||||
collapseButton?.dispatchEvent(new MouseEvent("click", { bubbles: true }));
|
||||
});
|
||||
|
||||
expect(container.textContent).toContain("已收起 · 2 个子代理 · 1 个处理中 · 1 个已完成");
|
||||
expect(container.textContent).toContain(
|
||||
"已收起 · 2 个子代理 · 1 个处理中 · 1 个已完成",
|
||||
);
|
||||
expect(container.textContent).not.toContain("代码审查代理");
|
||||
expect(container.textContent).not.toContain("文档校对代理");
|
||||
|
||||
@@ -684,9 +690,9 @@ describe("ChatSidebar", () => {
|
||||
expect(container.textContent).not.toContain("回归验证代理");
|
||||
expect(container.textContent).toContain("展开剩余 1 个子代理");
|
||||
|
||||
const expandMoreButton = Array.from(container.querySelectorAll("button")).find(
|
||||
(element) => element.textContent?.includes("展开剩余 1 个子代理"),
|
||||
);
|
||||
const expandMoreButton = Array.from(
|
||||
container.querySelectorAll("button"),
|
||||
).find((element) => element.textContent?.includes("展开剩余 1 个子代理"));
|
||||
expect(expandMoreButton).toBeTruthy();
|
||||
|
||||
act(() => {
|
||||
@@ -698,9 +704,9 @@ describe("ChatSidebar", () => {
|
||||
expect(container.textContent).toContain("回归验证代理");
|
||||
expect(container.textContent).toContain("收起子代理列表");
|
||||
|
||||
const collapseListButton = Array.from(container.querySelectorAll("button")).find(
|
||||
(element) => element.textContent?.includes("收起子代理列表"),
|
||||
);
|
||||
const collapseListButton = Array.from(
|
||||
container.querySelectorAll("button"),
|
||||
).find((element) => element.textContent?.includes("收起子代理列表"));
|
||||
expect(collapseListButton).toBeTruthy();
|
||||
|
||||
act(() => {
|
||||
|
||||
@@ -55,7 +55,7 @@ const STATUS_META: Record<
|
||||
draft: {
|
||||
label: "待补充",
|
||||
badgeClassName:
|
||||
"border border-slate-200 bg-white text-slate-600 dark:border-white/10 dark:bg-white/5 dark:text-slate-300",
|
||||
"border border-slate-200/80 bg-white/80 text-slate-600 dark:border-white/10 dark:bg-white/5 dark:text-slate-300",
|
||||
dotClassName: "bg-slate-400",
|
||||
},
|
||||
running: {
|
||||
@@ -84,11 +84,7 @@ const STATUS_META: Record<
|
||||
},
|
||||
};
|
||||
|
||||
type TaskSectionKey =
|
||||
| "running"
|
||||
| "waiting"
|
||||
| "recent"
|
||||
| "older";
|
||||
type TaskSectionKey = "running" | "waiting" | "recent" | "older";
|
||||
|
||||
type ChatSidebarContextVariant = "default" | "task-center";
|
||||
|
||||
@@ -209,7 +205,10 @@ function resolveCurrentStatusPreview(
|
||||
pendingActionCount: number,
|
||||
workspaceError: boolean,
|
||||
) {
|
||||
if ((workspaceError || statusReason === "workspace_error") && status === "failed") {
|
||||
if (
|
||||
(workspaceError || statusReason === "workspace_error") &&
|
||||
status === "failed"
|
||||
) {
|
||||
return "工作区异常,等待你重新选择本地目录后继续。";
|
||||
}
|
||||
if (status === "running") {
|
||||
@@ -462,9 +461,8 @@ export const ChatSidebar: React.FC<ChatSidebarProps> = ({
|
||||
recent: false,
|
||||
older: false,
|
||||
});
|
||||
const [teamSectionCollapsedOverride, setTeamSectionCollapsedOverride] = useState<
|
||||
boolean | null
|
||||
>(null);
|
||||
const [teamSectionCollapsedOverride, setTeamSectionCollapsedOverride] =
|
||||
useState<boolean | null>(null);
|
||||
const [showAllChildSubagents, setShowAllChildSubagents] = useState(false);
|
||||
const [showAllSiblingSubagents, setShowAllSiblingSubagents] = useState(false);
|
||||
const editInputRef = useRef<HTMLInputElement>(null);
|
||||
@@ -551,7 +549,10 @@ export const ChatSidebar: React.FC<ChatSidebarProps> = ({
|
||||
() =>
|
||||
showAllChildSubagents
|
||||
? sortedChildSubagentSessions
|
||||
: sortedChildSubagentSessions.slice(0, TEAM_SECTION_INITIAL_CHILD_COUNT),
|
||||
: sortedChildSubagentSessions.slice(
|
||||
0,
|
||||
TEAM_SECTION_INITIAL_CHILD_COUNT,
|
||||
),
|
||||
[showAllChildSubagents, sortedChildSubagentSessions],
|
||||
);
|
||||
const visibleSiblingSubagentSessions = useMemo(
|
||||
@@ -662,7 +663,9 @@ export const ChatSidebar: React.FC<ChatSidebarProps> = ({
|
||||
}, [teamSectionIdentity]);
|
||||
|
||||
useEffect(() => {
|
||||
if (sortedChildSubagentSessions.length <= TEAM_SECTION_INITIAL_CHILD_COUNT) {
|
||||
if (
|
||||
sortedChildSubagentSessions.length <= TEAM_SECTION_INITIAL_CHILD_COUNT
|
||||
) {
|
||||
setShowAllChildSubagents(false);
|
||||
}
|
||||
}, [sortedChildSubagentSessions.length]);
|
||||
@@ -785,9 +788,16 @@ export const ChatSidebar: React.FC<ChatSidebarProps> = ({
|
||||
</Badge>
|
||||
</div>
|
||||
<div className="mt-1 flex flex-wrap items-center gap-2 text-[11px] text-slate-500 dark:text-slate-400">
|
||||
<span>{options?.subtitle ?? resolveSubagentSessionTypeLabel(session.session_type)}</span>
|
||||
{session.role_hint ? <span>角色 · {session.role_hint}</span> : null}
|
||||
{updatedAt ? <span>更新于 {formatRelativeTime(updatedAt)}</span> : null}
|
||||
<span>
|
||||
{options?.subtitle ??
|
||||
resolveSubagentSessionTypeLabel(session.session_type)}
|
||||
</span>
|
||||
{session.role_hint ? (
|
||||
<span>角色 · {session.role_hint}</span>
|
||||
) : null}
|
||||
{updatedAt ? (
|
||||
<span>更新于 {formatRelativeTime(updatedAt)}</span>
|
||||
) : null}
|
||||
</div>
|
||||
{session.task_summary ? (
|
||||
<p className="mt-2 line-clamp-2 text-xs leading-5 text-slate-600 dark:text-slate-300">
|
||||
@@ -802,7 +812,7 @@ export const ChatSidebar: React.FC<ChatSidebarProps> = ({
|
||||
|
||||
return (
|
||||
<aside
|
||||
className="w-[308px] shrink-0 overflow-hidden rounded-[30px] border border-slate-200/80 bg-[linear-gradient(180deg,rgba(248,250,252,0.98)_0%,rgba(241,245,249,0.92)_100%)] shadow-sm shadow-slate-950/5 backdrop-blur dark:border-white/10 dark:bg-[#111318]"
|
||||
className="w-[308px] shrink-0 overflow-hidden rounded-[30px] border border-emerald-200/40 bg-[linear-gradient(180deg,rgba(252,254,252,0.98)_0%,rgba(247,251,248,0.92)_100%)] shadow-sm shadow-slate-950/5 backdrop-blur dark:border-white/10 dark:bg-[#111318]"
|
||||
data-testid="chat-sidebar"
|
||||
>
|
||||
<div className="flex h-full min-h-0 flex-col gap-4 p-4">
|
||||
@@ -814,14 +824,14 @@ export const ChatSidebar: React.FC<ChatSidebarProps> = ({
|
||||
value={searchKeyword}
|
||||
onChange={(event) => setSearchKeyword(event.target.value)}
|
||||
placeholder="搜索任务标题或摘要"
|
||||
className="h-11 w-full rounded-[18px] border border-slate-200/80 bg-white/92 pl-9 pr-3 text-sm text-slate-700 shadow-sm shadow-slate-950/5 outline-none transition focus:border-slate-300 focus:ring-2 focus:ring-slate-200 dark:border-white/10 dark:bg-white/5 dark:text-slate-100 dark:focus:border-white/20 dark:focus:ring-white/10"
|
||||
className="h-11 w-full rounded-[18px] border border-emerald-200/40 bg-white/90 pl-9 pr-3 text-sm text-slate-700 shadow-sm shadow-slate-950/5 outline-none transition focus:border-emerald-300/60 focus:ring-2 focus:ring-emerald-100/50 dark:border-white/10 dark:bg-white/5 dark:text-slate-100 dark:focus:border-white/20 dark:focus:ring-white/10"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<button
|
||||
type="button"
|
||||
onClick={onNewChat}
|
||||
className="inline-flex h-11 w-full items-center justify-center gap-2 rounded-[18px] bg-slate-900 px-4 text-sm font-semibold text-white shadow-sm shadow-slate-950/10 transition hover:bg-slate-800 dark:bg-white dark:text-slate-900 dark:hover:bg-slate-100"
|
||||
className="inline-flex h-11 w-full items-center justify-center gap-2 rounded-[18px] bg-[#1a3b2b] px-4 text-sm font-semibold text-white shadow-sm shadow-[#1a3b2b]/10 transition hover:bg-[#132c20] dark:bg-white dark:text-slate-900 dark:hover:bg-slate-100"
|
||||
>
|
||||
<Plus className="h-4 w-4" />
|
||||
新建任务
|
||||
@@ -834,8 +844,8 @@ export const ChatSidebar: React.FC<ChatSidebarProps> = ({
|
||||
className={cn(
|
||||
"inline-flex h-9 flex-1 items-center justify-center rounded-2xl border px-2 text-xs font-medium transition",
|
||||
statusFilter === "all"
|
||||
? "border-slate-900 bg-slate-900 text-white dark:border-white dark:bg-white dark:text-slate-900"
|
||||
: "border-slate-200/80 bg-white/90 text-slate-500 hover:border-slate-300 hover:bg-white hover:text-slate-900 dark:border-white/10 dark:bg-white/5 dark:text-slate-300",
|
||||
? "border-[#1a3b2b] bg-[#1a3b2b] text-white dark:border-white dark:bg-white dark:text-slate-900"
|
||||
: "border-slate-200/80 bg-white/90 text-slate-500 hover:border-slate-300 hover:bg-white hover:text-slate-800 dark:border-white/10 dark:bg-white/5 dark:text-slate-300",
|
||||
)}
|
||||
>
|
||||
全部任务
|
||||
@@ -846,8 +856,8 @@ export const ChatSidebar: React.FC<ChatSidebarProps> = ({
|
||||
className={cn(
|
||||
"inline-flex h-9 flex-1 items-center justify-center rounded-2xl border px-2 text-xs font-medium transition",
|
||||
statusFilter === "active"
|
||||
? "border-slate-900 bg-slate-900 text-white dark:border-white dark:bg-white dark:text-slate-900"
|
||||
: "border-slate-200/80 bg-white/90 text-slate-500 hover:border-slate-300 hover:bg-white hover:text-slate-900 dark:border-white/10 dark:bg-white/5 dark:text-slate-300",
|
||||
? "border-[#1a3b2b] bg-[#1a3b2b] text-white dark:border-white dark:bg-white dark:text-slate-900"
|
||||
: "border-slate-200/80 bg-white/90 text-slate-500 hover:border-slate-300 hover:bg-white hover:text-slate-800 dark:border-white/10 dark:bg-white/5 dark:text-slate-300",
|
||||
)}
|
||||
>
|
||||
仅看进行中
|
||||
@@ -979,7 +989,8 @@ export const ChatSidebar: React.FC<ChatSidebarProps> = ({
|
||||
) : null}
|
||||
{currentTaskItem?.updatedAt ? (
|
||||
<span>
|
||||
更新于 {formatRelativeTime(currentTaskItem.updatedAt)}
|
||||
更新于{" "}
|
||||
{formatRelativeTime(currentTaskItem.updatedAt)}
|
||||
</span>
|
||||
) : null}
|
||||
</div>
|
||||
@@ -1069,7 +1080,7 @@ export const ChatSidebar: React.FC<ChatSidebarProps> = ({
|
||||
{taskHeadingLabel}
|
||||
</div>
|
||||
{taskHeadingHint ? (
|
||||
<p className="mt-1 text-[11px] leading-5 text-slate-500 dark:text-slate-400">
|
||||
<p className="mt-1 text-[11px] leading-5 text-slate-400 dark:text-slate-400">
|
||||
{taskHeadingHint}
|
||||
</p>
|
||||
) : null}
|
||||
@@ -1106,261 +1117,268 @@ export const ChatSidebar: React.FC<ChatSidebarProps> = ({
|
||||
) : (
|
||||
<div className="space-y-3">
|
||||
{sections.map((section) => {
|
||||
const isOlderSection = section.key === "older";
|
||||
const isSectionCollapsed = collapsedSections[section.key];
|
||||
const visibleItems =
|
||||
isOlderSection && !showAllOlder
|
||||
? section.items.slice(0, OLDER_TASKS_INITIAL_COUNT)
|
||||
: section.items;
|
||||
const isOlderSection = section.key === "older";
|
||||
const isSectionCollapsed = collapsedSections[section.key];
|
||||
const visibleItems =
|
||||
isOlderSection && !showAllOlder
|
||||
? section.items.slice(0, OLDER_TASKS_INITIAL_COUNT)
|
||||
: section.items;
|
||||
|
||||
if (section.items.length === 0) {
|
||||
return null;
|
||||
}
|
||||
if (section.items.length === 0) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<section key={section.key} className="space-y-2">
|
||||
<button
|
||||
type="button"
|
||||
onClick={() =>
|
||||
setCollapsedSections((prev) => ({
|
||||
...prev,
|
||||
[section.key]: !prev[section.key],
|
||||
}))
|
||||
}
|
||||
className="flex w-full items-center justify-between rounded-2xl px-2.5 py-2 text-left transition hover:bg-white/78 dark:hover:bg-white/5"
|
||||
>
|
||||
<div className="flex items-center gap-2">
|
||||
<ChevronDown
|
||||
className={cn(
|
||||
"h-4 w-4 text-slate-400 transition-transform",
|
||||
isSectionCollapsed ? "-rotate-90" : "",
|
||||
)}
|
||||
/>
|
||||
<span className="text-xs font-semibold text-slate-700 dark:text-slate-200">
|
||||
{section.title}
|
||||
return (
|
||||
<section key={section.key} className="space-y-2">
|
||||
<button
|
||||
type="button"
|
||||
onClick={() =>
|
||||
setCollapsedSections((prev) => ({
|
||||
...prev,
|
||||
[section.key]: !prev[section.key],
|
||||
}))
|
||||
}
|
||||
className="flex w-full items-center justify-between rounded-2xl px-2.5 py-2 text-left transition hover:bg-white/78 dark:hover:bg-white/5"
|
||||
>
|
||||
<div className="flex items-center gap-2">
|
||||
<ChevronDown
|
||||
className={cn(
|
||||
"h-4 w-4 text-emerald-400 transition-transform",
|
||||
isSectionCollapsed ? "-rotate-90" : "",
|
||||
)}
|
||||
/>
|
||||
<span className="text-xs font-semibold text-slate-700 dark:text-slate-200">
|
||||
{section.title}
|
||||
</span>
|
||||
</div>
|
||||
<span className="text-[11px] text-slate-400">
|
||||
{section.items.length}
|
||||
</span>
|
||||
</div>
|
||||
<span className="text-[11px] text-slate-400">
|
||||
{section.items.length}
|
||||
</span>
|
||||
</button>
|
||||
</button>
|
||||
|
||||
{isSectionCollapsed ? null : (
|
||||
<div className="space-y-2">
|
||||
{visibleItems.map((item) => {
|
||||
const statusMeta = STATUS_META[item.status];
|
||||
const isResumableItem =
|
||||
item.status === "waiting" ||
|
||||
(item.status === "failed" &&
|
||||
item.statusReason === "workspace_error");
|
||||
{isSectionCollapsed ? null : (
|
||||
<div className="space-y-2">
|
||||
{visibleItems.map((item) => {
|
||||
const statusMeta = STATUS_META[item.status];
|
||||
const isResumableItem =
|
||||
item.status === "waiting" ||
|
||||
(item.status === "failed" &&
|
||||
item.statusReason === "workspace_error");
|
||||
|
||||
return (
|
||||
<div
|
||||
key={item.id}
|
||||
role="button"
|
||||
tabIndex={0}
|
||||
onClick={() => {
|
||||
if (editingTopicId !== item.id) {
|
||||
onSwitchTopic(item.id);
|
||||
}
|
||||
}}
|
||||
onDoubleClick={() =>
|
||||
handleStartEdit(item.id, item.title)
|
||||
}
|
||||
onKeyDown={(event) => {
|
||||
if (
|
||||
event.key === "Enter" ||
|
||||
event.key === " "
|
||||
) {
|
||||
event.preventDefault();
|
||||
return (
|
||||
<div
|
||||
key={item.id}
|
||||
role="button"
|
||||
tabIndex={0}
|
||||
onClick={() => {
|
||||
if (editingTopicId !== item.id) {
|
||||
onSwitchTopic(item.id);
|
||||
}
|
||||
}}
|
||||
onDoubleClick={() =>
|
||||
handleStartEdit(item.id, item.title)
|
||||
}
|
||||
}}
|
||||
className={cn(
|
||||
"group rounded-[22px] border p-3.5 text-left shadow-sm shadow-slate-950/5 transition",
|
||||
isResumableItem
|
||||
? "border-amber-200/80 bg-[linear-gradient(180deg,rgba(255,251,235,0.9)_0%,rgba(255,255,255,0.96)_100%)] shadow-sm shadow-amber-950/5 dark:border-amber-500/20 dark:bg-white/10"
|
||||
: "",
|
||||
item.isCurrent
|
||||
? "border-slate-300 bg-white/98 ring-1 ring-slate-100 dark:border-white/15 dark:bg-white/10"
|
||||
: "border-slate-200/70 bg-white/72 hover:border-slate-300 hover:bg-white/92 dark:border-white/10 dark:bg-white/5 dark:hover:border-white/10 dark:hover:bg-white/5",
|
||||
)}
|
||||
>
|
||||
<div className="flex items-start gap-3">
|
||||
<span
|
||||
className={cn(
|
||||
"mt-1.5 h-2.5 w-2.5 shrink-0 rounded-full",
|
||||
statusMeta.dotClassName,
|
||||
)}
|
||||
/>
|
||||
|
||||
<div className="min-w-0 flex-1">
|
||||
<div className="flex items-start gap-2">
|
||||
{editingTopicId === item.id ? (
|
||||
<input
|
||||
ref={editInputRef}
|
||||
type="text"
|
||||
value={editTitle}
|
||||
onChange={(event) =>
|
||||
setEditTitle(event.target.value)
|
||||
}
|
||||
onKeyDown={handleEditKeyDown}
|
||||
onBlur={handleSaveEdit}
|
||||
onClick={(event) =>
|
||||
event.stopPropagation()
|
||||
}
|
||||
className="h-8 flex-1 rounded-xl border border-slate-300 bg-white px-2.5 text-sm font-medium text-slate-900 outline-none focus:border-slate-400 dark:border-white/10 dark:bg-[#17191f] dark:text-slate-100"
|
||||
/>
|
||||
) : (
|
||||
<>
|
||||
<div className="min-w-0 flex-1">
|
||||
<div className="flex items-center gap-1.5">
|
||||
<div className="truncate text-sm font-semibold text-slate-900 dark:text-slate-100">
|
||||
{item.title || "未命名任务"}
|
||||
</div>
|
||||
{item.isPinned ? (
|
||||
<Pin className="h-3.5 w-3.5 text-slate-400" />
|
||||
) : null}
|
||||
{item.hasUnread ? (
|
||||
<span className="h-2 w-2 rounded-full bg-sky-500" />
|
||||
) : null}
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex shrink-0 items-center gap-1 pt-0.5">
|
||||
<div className="text-[11px] text-slate-400">
|
||||
{formatRelativeTime(item.updatedAt)}
|
||||
</div>
|
||||
<button
|
||||
type="button"
|
||||
aria-label="删除任务"
|
||||
title="删除任务"
|
||||
className={cn(
|
||||
"inline-flex h-7 w-7 items-center justify-center rounded-lg text-slate-400 transition hover:bg-rose-50 hover:text-rose-600 dark:hover:bg-rose-500/10 dark:hover:text-rose-300",
|
||||
item.isCurrent
|
||||
? "opacity-100"
|
||||
: "opacity-0 group-hover:opacity-100",
|
||||
)}
|
||||
onClick={(event) => {
|
||||
event.stopPropagation();
|
||||
handleDeleteClick(item.id);
|
||||
}}
|
||||
>
|
||||
<Trash2 className="h-4 w-4" />
|
||||
</button>
|
||||
<DropdownMenu>
|
||||
<DropdownMenuTrigger asChild>
|
||||
<button
|
||||
type="button"
|
||||
aria-label="任务操作"
|
||||
className={cn(
|
||||
"inline-flex h-7 w-7 items-center justify-center rounded-lg text-slate-400 transition hover:bg-slate-100 hover:text-slate-700 dark:hover:bg-white/10 dark:hover:text-slate-100",
|
||||
item.isCurrent
|
||||
? "opacity-100"
|
||||
: "opacity-0 group-hover:opacity-100",
|
||||
)}
|
||||
onClick={(event) =>
|
||||
event.stopPropagation()
|
||||
}
|
||||
>
|
||||
<MoreHorizontal className="h-4 w-4" />
|
||||
</button>
|
||||
</DropdownMenuTrigger>
|
||||
<DropdownMenuContent align="end">
|
||||
<DropdownMenuItem
|
||||
onClick={() =>
|
||||
handleStartEdit(item.id, item.title)
|
||||
}
|
||||
>
|
||||
<PencilLine className="h-4 w-4" />
|
||||
重命名任务
|
||||
</DropdownMenuItem>
|
||||
<DropdownMenuItem
|
||||
onClick={() =>
|
||||
handleTogglePinned(item.id)
|
||||
}
|
||||
>
|
||||
{item.isPinned ? (
|
||||
<PinOff className="h-4 w-4" />
|
||||
) : (
|
||||
<Pin className="h-4 w-4" />
|
||||
)}
|
||||
{item.isPinned ? "取消固定" : "固定任务"}
|
||||
</DropdownMenuItem>
|
||||
<DropdownMenuSeparator />
|
||||
<DropdownMenuItem
|
||||
className="text-rose-600"
|
||||
onClick={() =>
|
||||
handleDeleteClick(item.id)
|
||||
}
|
||||
>
|
||||
<Trash2 className="h-4 w-4" />
|
||||
删除任务
|
||||
</DropdownMenuItem>
|
||||
</DropdownMenuContent>
|
||||
</DropdownMenu>
|
||||
</div>
|
||||
</>
|
||||
onKeyDown={(event) => {
|
||||
if (
|
||||
event.key === "Enter" ||
|
||||
event.key === " "
|
||||
) {
|
||||
event.preventDefault();
|
||||
if (editingTopicId !== item.id) {
|
||||
onSwitchTopic(item.id);
|
||||
}
|
||||
}
|
||||
}}
|
||||
className={cn(
|
||||
"group rounded-[22px] border p-3.5 text-left shadow-sm shadow-slate-950/5 transition",
|
||||
isResumableItem
|
||||
? "border-amber-200/80 bg-[linear-gradient(180deg,rgba(255,251,235,0.9)_0%,rgba(255,255,255,0.96)_100%)] shadow-sm shadow-amber-950/5 dark:border-amber-500/20 dark:bg-white/10"
|
||||
: "",
|
||||
item.isCurrent
|
||||
? "border-emerald-200/60 bg-white/98 ring-1 ring-emerald-50 dark:border-white/15 dark:bg-white/10"
|
||||
: "border-slate-200/60 bg-white/72 hover:border-emerald-200/60 hover:bg-white/92 dark:border-white/10 dark:bg-white/5 dark:hover:border-white/10 dark:hover:bg-white/5",
|
||||
)}
|
||||
>
|
||||
<div className="flex items-start gap-3">
|
||||
<span
|
||||
className={cn(
|
||||
"mt-1.5 h-2.5 w-2.5 shrink-0 rounded-full",
|
||||
statusMeta.dotClassName,
|
||||
)}
|
||||
</div>
|
||||
/>
|
||||
|
||||
<div className="mt-2 line-clamp-2 text-xs leading-5 text-slate-500 dark:text-slate-400">
|
||||
{item.lastPreview}
|
||||
</div>
|
||||
|
||||
<div className="mt-3 flex items-center gap-2">
|
||||
<Badge
|
||||
variant="outline"
|
||||
className={cn(
|
||||
"px-2.5 py-1 text-[11px] font-medium",
|
||||
statusMeta.badgeClassName,
|
||||
<div className="min-w-0 flex-1">
|
||||
<div className="flex items-start gap-2">
|
||||
{editingTopicId === item.id ? (
|
||||
<input
|
||||
ref={editInputRef}
|
||||
type="text"
|
||||
value={editTitle}
|
||||
onChange={(event) =>
|
||||
setEditTitle(event.target.value)
|
||||
}
|
||||
onKeyDown={handleEditKeyDown}
|
||||
onBlur={handleSaveEdit}
|
||||
onClick={(event) =>
|
||||
event.stopPropagation()
|
||||
}
|
||||
className="h-8 flex-1 rounded-xl border border-slate-300 bg-white px-2.5 text-sm font-medium text-slate-900 outline-none focus:border-slate-400 dark:border-white/10 dark:bg-[#17191f] dark:text-slate-100"
|
||||
/>
|
||||
) : (
|
||||
<>
|
||||
<div className="min-w-0 flex-1">
|
||||
<div className="flex items-center gap-1.5">
|
||||
<div className="truncate text-sm font-semibold text-slate-800 dark:text-slate-100">
|
||||
{item.title || "未命名任务"}
|
||||
</div>
|
||||
{item.isPinned ? (
|
||||
<Pin className="h-3.5 w-3.5 text-emerald-400" />
|
||||
) : null}
|
||||
{item.hasUnread ? (
|
||||
<span className="h-2 w-2 rounded-full bg-emerald-500" />
|
||||
) : null}
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex shrink-0 items-center gap-1 pt-0.5">
|
||||
<div className="text-[11px] text-slate-400">
|
||||
{formatRelativeTime(
|
||||
item.updatedAt,
|
||||
)}
|
||||
</div>
|
||||
<button
|
||||
type="button"
|
||||
aria-label="删除任务"
|
||||
title="删除任务"
|
||||
className={cn(
|
||||
"inline-flex h-7 w-7 items-center justify-center rounded-lg text-slate-400 transition hover:bg-rose-50 hover:text-rose-600 dark:hover:bg-rose-500/10 dark:hover:text-rose-300",
|
||||
item.isCurrent
|
||||
? "opacity-100"
|
||||
: "opacity-0 group-hover:opacity-100",
|
||||
)}
|
||||
onClick={(event) => {
|
||||
event.stopPropagation();
|
||||
handleDeleteClick(item.id);
|
||||
}}
|
||||
>
|
||||
<Trash2 className="h-4 w-4" />
|
||||
</button>
|
||||
<DropdownMenu>
|
||||
<DropdownMenuTrigger asChild>
|
||||
<button
|
||||
type="button"
|
||||
aria-label="任务操作"
|
||||
className={cn(
|
||||
"inline-flex h-7 w-7 items-center justify-center rounded-lg text-slate-400 transition hover:bg-slate-100 hover:text-slate-700 dark:hover:bg-white/10 dark:hover:text-slate-100",
|
||||
item.isCurrent
|
||||
? "opacity-100"
|
||||
: "opacity-0 group-hover:opacity-100",
|
||||
)}
|
||||
onClick={(event) =>
|
||||
event.stopPropagation()
|
||||
}
|
||||
>
|
||||
<MoreHorizontal className="h-4 w-4" />
|
||||
</button>
|
||||
</DropdownMenuTrigger>
|
||||
<DropdownMenuContent align="end">
|
||||
<DropdownMenuItem
|
||||
onClick={() =>
|
||||
handleStartEdit(
|
||||
item.id,
|
||||
item.title,
|
||||
)
|
||||
}
|
||||
>
|
||||
<PencilLine className="h-4 w-4" />
|
||||
重命名任务
|
||||
</DropdownMenuItem>
|
||||
<DropdownMenuItem
|
||||
onClick={() =>
|
||||
handleTogglePinned(item.id)
|
||||
}
|
||||
>
|
||||
{item.isPinned ? (
|
||||
<PinOff className="h-4 w-4" />
|
||||
) : (
|
||||
<Pin className="h-4 w-4" />
|
||||
)}
|
||||
{item.isPinned
|
||||
? "取消固定"
|
||||
: "固定任务"}
|
||||
</DropdownMenuItem>
|
||||
<DropdownMenuSeparator />
|
||||
<DropdownMenuItem
|
||||
className="text-rose-600"
|
||||
onClick={() =>
|
||||
handleDeleteClick(item.id)
|
||||
}
|
||||
>
|
||||
<Trash2 className="h-4 w-4" />
|
||||
删除任务
|
||||
</DropdownMenuItem>
|
||||
</DropdownMenuContent>
|
||||
</DropdownMenu>
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
>
|
||||
{item.status === "running" ? (
|
||||
<Loader2 className="mr-1 h-3 w-3 animate-spin" />
|
||||
) : null}
|
||||
{item.statusLabel}
|
||||
</Badge>
|
||||
<span className="text-[11px] text-slate-400">
|
||||
{item.messagesCount > 0
|
||||
? `${item.messagesCount} 条消息`
|
||||
: "尚未开始执行"}
|
||||
</span>
|
||||
{isResumableItem && onResumeTask ? (
|
||||
<button
|
||||
type="button"
|
||||
onClick={(event) => {
|
||||
event.stopPropagation();
|
||||
handleResumeTask(item);
|
||||
}}
|
||||
className="inline-flex items-center rounded-full border border-amber-200/80 bg-amber-50 px-2.5 py-1 text-[11px] font-medium text-amber-700 transition hover:border-amber-300 hover:bg-amber-100 dark:border-amber-500/20 dark:bg-amber-500/10 dark:text-amber-200 dark:hover:bg-amber-500/15"
|
||||
</div>
|
||||
|
||||
<div className="mt-2 line-clamp-2 text-xs leading-5 text-slate-500 dark:text-slate-400">
|
||||
{item.lastPreview}
|
||||
</div>
|
||||
|
||||
<div className="mt-3 flex items-center gap-2">
|
||||
<Badge
|
||||
variant="outline"
|
||||
className={cn(
|
||||
"px-2.5 py-1 text-[11px] font-medium",
|
||||
statusMeta.badgeClassName,
|
||||
)}
|
||||
>
|
||||
继续任务
|
||||
</button>
|
||||
) : null}
|
||||
{item.status === "running" ? (
|
||||
<Loader2 className="mr-1 h-3 w-3 animate-spin" />
|
||||
) : null}
|
||||
{item.statusLabel}
|
||||
</Badge>
|
||||
<span className="text-[11px] text-slate-400">
|
||||
{item.messagesCount > 0
|
||||
? `${item.messagesCount} 条消息`
|
||||
: "尚未开始执行"}
|
||||
</span>
|
||||
{isResumableItem && onResumeTask ? (
|
||||
<button
|
||||
type="button"
|
||||
onClick={(event) => {
|
||||
event.stopPropagation();
|
||||
handleResumeTask(item);
|
||||
}}
|
||||
className="inline-flex items-center rounded-full border border-amber-200/80 bg-amber-50 px-2.5 py-1 text-[11px] font-medium text-amber-700 transition hover:border-amber-300 hover:bg-amber-100 dark:border-amber-500/20 dark:bg-amber-500/10 dark:text-amber-200 dark:hover:bg-amber-500/15"
|
||||
>
|
||||
继续任务
|
||||
</button>
|
||||
) : null}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
);
|
||||
})}
|
||||
|
||||
{isOlderSection &&
|
||||
section.items.length > OLDER_TASKS_INITIAL_COUNT &&
|
||||
!showAllOlder ? (
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => setShowAllOlder(true)}
|
||||
className="w-full rounded-2xl border border-dashed border-slate-200 bg-white/75 px-3 py-2 text-sm font-medium text-slate-500 transition hover:border-slate-300 hover:text-slate-800 dark:border-white/10 dark:bg-white/5 dark:text-slate-300 dark:hover:border-white/20 dark:hover:text-white"
|
||||
>
|
||||
{olderSectionMoreLabel}
|
||||
</button>
|
||||
) : null}
|
||||
</div>
|
||||
)}
|
||||
</section>
|
||||
);
|
||||
{isOlderSection &&
|
||||
section.items.length > OLDER_TASKS_INITIAL_COUNT &&
|
||||
!showAllOlder ? (
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => setShowAllOlder(true)}
|
||||
className="w-full rounded-2xl border border-dashed border-slate-200 bg-white/75 px-3 py-2 text-sm font-medium text-slate-500 transition hover:border-slate-300 hover:text-slate-800 dark:border-white/10 dark:bg-white/5 dark:text-slate-300 dark:hover:border-white/20 dark:hover:text-white"
|
||||
>
|
||||
{olderSectionMoreLabel}
|
||||
</button>
|
||||
) : null}
|
||||
</div>
|
||||
)}
|
||||
</section>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
)}
|
||||
|
||||
@@ -284,12 +284,7 @@ describe("EmptyState", () => {
|
||||
await Promise.resolve();
|
||||
});
|
||||
|
||||
const expectedLabels = [
|
||||
"技能",
|
||||
"自动化",
|
||||
"多代理",
|
||||
"浏览器接入",
|
||||
];
|
||||
const expectedLabels = ["技能", "自动化", "多代理", "浏览器接入"];
|
||||
const expectedAlts = [
|
||||
"技能能力卡占位图",
|
||||
"自动化能力卡占位图",
|
||||
@@ -745,7 +740,7 @@ describe("EmptyState", () => {
|
||||
executionStrategy?: "react" | "code_orchestrated" | "auto",
|
||||
images?: unknown[],
|
||||
) => void
|
||||
>();
|
||||
>();
|
||||
const container = renderEmptyState({
|
||||
input: "请输出一篇用户访谈纪要",
|
||||
activeTheme: "general",
|
||||
@@ -826,9 +821,8 @@ describe("EmptyState", () => {
|
||||
it("通用主题工具栏应包含附件、思考、Plan 与多代理开关", async () => {
|
||||
const onThinkingEnabledChange = vi.fn<(enabled: boolean) => void>();
|
||||
const onSubagentEnabledChange = vi.fn<(enabled: boolean) => void>();
|
||||
const setExecutionStrategy = vi.fn<
|
||||
(strategy: "react" | "code_orchestrated" | "auto") => void
|
||||
>();
|
||||
const setExecutionStrategy =
|
||||
vi.fn<(strategy: "react" | "code_orchestrated" | "auto") => void>();
|
||||
const container = renderEmptyState({
|
||||
activeTheme: "general",
|
||||
thinkingEnabled: false,
|
||||
@@ -896,5 +890,4 @@ describe("EmptyState", () => {
|
||||
|
||||
expect(onLaunchBrowserAssist).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
|
||||
});
|
||||
|
||||
@@ -338,8 +338,10 @@ export const EmptyState: React.FC<EmptyStateProps> = ({
|
||||
appendSelectedTextToRecommendation,
|
||||
setAppendSelectedTextToRecommendation,
|
||||
] = useState(true);
|
||||
const [entryRecommendedSolutionsVersion, setEntryRecommendedSolutionsVersion] =
|
||||
useState(0);
|
||||
const [
|
||||
entryRecommendedSolutionsVersion,
|
||||
setEntryRecommendedSolutionsVersion,
|
||||
] = useState(0);
|
||||
|
||||
useEffect(() => {
|
||||
const loadConfigPreferences = async () => {
|
||||
@@ -417,13 +419,10 @@ export const EmptyState: React.FC<EmptyStateProps> = ({
|
||||
subagentEnabled,
|
||||
]);
|
||||
|
||||
const entryRecommendedSolutions = useMemo(
|
||||
() => {
|
||||
void entryRecommendedSolutionsVersion;
|
||||
return listEntryRecommendedSolutions();
|
||||
},
|
||||
[entryRecommendedSolutionsVersion],
|
||||
);
|
||||
const entryRecommendedSolutions = useMemo(() => {
|
||||
void entryRecommendedSolutionsVersion;
|
||||
return listEntryRecommendedSolutions();
|
||||
}, [entryRecommendedSolutionsVersion]);
|
||||
|
||||
const selectedTextPreview = useMemo(() => {
|
||||
const normalized = (recommendationSelectedText || "")
|
||||
@@ -488,11 +487,7 @@ export const EmptyState: React.FC<EmptyStateProps> = ({
|
||||
}
|
||||
const imagesToSend = pendingImages.length > 0 ? pendingImages : undefined;
|
||||
|
||||
onSend(
|
||||
wrapTextWithDefaultSkill(input),
|
||||
executionStrategy,
|
||||
imagesToSend,
|
||||
);
|
||||
onSend(wrapTextWithDefaultSkill(input), executionStrategy, imagesToSend);
|
||||
setPendingImages([]);
|
||||
clearSelectedSkill?.();
|
||||
};
|
||||
@@ -658,7 +653,9 @@ export const EmptyState: React.FC<EmptyStateProps> = ({
|
||||
key: "agent-teams",
|
||||
eyebrow: "支撑能力",
|
||||
title: "多代理",
|
||||
value: subagentEnabled ? "当前任务支持并行协作" : "复杂任务可拆成并行分工",
|
||||
value: subagentEnabled
|
||||
? "当前任务支持并行协作"
|
||||
: "复杂任务可拆成并行分工",
|
||||
description:
|
||||
"当研究、方案和执行需要同时推进时,可把任务拆给多个代理并行处理,再统一回收结论。",
|
||||
icon: <Workflow className="h-5 w-5" />,
|
||||
@@ -672,9 +669,7 @@ export const EmptyState: React.FC<EmptyStateProps> = ({
|
||||
key: "browser",
|
||||
eyebrow: "支撑能力",
|
||||
title: "浏览器接入",
|
||||
value: browserAssistLoading
|
||||
? "正在检查连接状态"
|
||||
: "CDP / 浏览器插件复用",
|
||||
value: browserAssistLoading ? "正在检查连接状态" : "CDP / 浏览器插件复用",
|
||||
description:
|
||||
"登录、验证和网页动作可直接复用 CDP 或浏览器插件连接,不必再切到单独工作台。",
|
||||
icon: <Globe className="h-5 w-5" />,
|
||||
@@ -686,7 +681,9 @@ export const EmptyState: React.FC<EmptyStateProps> = ({
|
||||
void onLaunchBrowserAssist();
|
||||
}
|
||||
: undefined,
|
||||
mediaActionLabel: browserAssistLoading ? "浏览器连接准备中" : "连接浏览器",
|
||||
mediaActionLabel: browserAssistLoading
|
||||
? "浏览器连接准备中"
|
||||
: "连接浏览器",
|
||||
mediaActionDisabled: browserAssistLoading,
|
||||
});
|
||||
|
||||
@@ -847,9 +844,13 @@ export const EmptyState: React.FC<EmptyStateProps> = ({
|
||||
handleApplyEntryRecommendedSolution(solution);
|
||||
}}
|
||||
>
|
||||
<RecommendationShelfTitle>{solution.title}</RecommendationShelfTitle>
|
||||
<RecommendationShelfTitle>
|
||||
{solution.title}
|
||||
</RecommendationShelfTitle>
|
||||
{solution.isRecent ? (
|
||||
<RecommendationShelfMeta>{solution.badge}</RecommendationShelfMeta>
|
||||
<RecommendationShelfMeta>
|
||||
{solution.badge}
|
||||
</RecommendationShelfMeta>
|
||||
) : null}
|
||||
</RecommendationShelfButton>
|
||||
</RecommendationShelfRow>
|
||||
|
||||
@@ -312,9 +312,9 @@ describe("EmptyStateComposerPanel", () => {
|
||||
onSend,
|
||||
});
|
||||
|
||||
const textarea = container.querySelector("textarea") as
|
||||
| HTMLTextAreaElement
|
||||
| null;
|
||||
const textarea = container.querySelector(
|
||||
"textarea",
|
||||
) as HTMLTextAreaElement | null;
|
||||
const pendingButton = Array.from(container.querySelectorAll("button")).find(
|
||||
(button) => button.textContent?.includes("稍后处理"),
|
||||
) as HTMLButtonElement | undefined;
|
||||
@@ -377,7 +377,9 @@ describe("EmptyStateComposerPanel", () => {
|
||||
expect(enableTeamButton).toBeTruthy();
|
||||
|
||||
act(() => {
|
||||
enableTeamButton?.dispatchEvent(new MouseEvent("click", { bubbles: true }));
|
||||
enableTeamButton?.dispatchEvent(
|
||||
new MouseEvent("click", { bubbles: true }),
|
||||
);
|
||||
});
|
||||
|
||||
expect(onSubagentEnabledChange).toHaveBeenCalledWith(true);
|
||||
@@ -391,16 +393,14 @@ describe("EmptyStateComposerPanel", () => {
|
||||
onSubagentEnabledChange: vi.fn(),
|
||||
});
|
||||
|
||||
const continueButton = Array.from(container.querySelectorAll("button")).find(
|
||||
(button) => button.textContent?.includes("继续单代理"),
|
||||
);
|
||||
const continueButton = Array.from(
|
||||
container.querySelectorAll("button"),
|
||||
).find((button) => button.textContent?.includes("继续单代理"));
|
||||
|
||||
expect(continueButton).toBeTruthy();
|
||||
|
||||
act(() => {
|
||||
continueButton?.dispatchEvent(
|
||||
new MouseEvent("click", { bubbles: true }),
|
||||
);
|
||||
continueButton?.dispatchEvent(new MouseEvent("click", { bubbles: true }));
|
||||
});
|
||||
|
||||
expect(container.textContent).not.toContain("当前任务更适合 Team 协作");
|
||||
@@ -427,7 +427,9 @@ describe("EmptyStateComposerPanel", () => {
|
||||
container.querySelector('[data-testid="empty-state-team-selector"]'),
|
||||
).toBeNull();
|
||||
expect(
|
||||
container.querySelector('[data-testid="empty-state-team-mode-enable-button"]'),
|
||||
container.querySelector(
|
||||
'[data-testid="empty-state-team-mode-enable-button"]',
|
||||
),
|
||||
).toBeNull();
|
||||
|
||||
const toggleButton = container.querySelector(
|
||||
@@ -561,14 +563,14 @@ describe("EmptyStateComposerPanel", () => {
|
||||
input: "请拆成多个子任务分别分析、实现、验证,并最终统一回归验收",
|
||||
});
|
||||
|
||||
const enableButton = Array.from(
|
||||
container.querySelectorAll("button"),
|
||||
).find((button) => button.textContent?.includes("启用 Team")) as
|
||||
| HTMLButtonElement
|
||||
| undefined;
|
||||
const enableButton = Array.from(container.querySelectorAll("button")).find(
|
||||
(button) => button.textContent?.includes("启用 Team"),
|
||||
) as HTMLButtonElement | undefined;
|
||||
|
||||
expect(
|
||||
container.querySelector('[data-testid="empty-state-team-mode-enable-button"]'),
|
||||
container.querySelector(
|
||||
'[data-testid="empty-state-team-mode-enable-button"]',
|
||||
),
|
||||
).toBeNull();
|
||||
expect(
|
||||
container.querySelector('button[title="多代理偏好已关闭"]'),
|
||||
|
||||
@@ -134,7 +134,7 @@ const SloganLine = styled.p`
|
||||
width: fit-content;
|
||||
align-items: center;
|
||||
gap: 1.3rem;
|
||||
|
||||
|
||||
/* lime gradient with scan effect */
|
||||
background: linear-gradient(
|
||||
90deg,
|
||||
@@ -162,7 +162,11 @@ const SloganLine = styled.p`
|
||||
height: 0.92rem;
|
||||
flex-shrink: 0;
|
||||
border-radius: 9999px;
|
||||
background: linear-gradient(135deg, rgba(132, 204, 22, 0.9), rgba(16, 185, 129, 0.75));
|
||||
background: linear-gradient(
|
||||
135deg,
|
||||
rgba(132, 204, 22, 0.9),
|
||||
rgba(16, 185, 129, 0.75)
|
||||
);
|
||||
box-shadow: 0 0 0 12px rgba(132, 204, 22, 0.12);
|
||||
}
|
||||
`;
|
||||
@@ -272,7 +276,6 @@ const HeroCard = styled.article.attrs({
|
||||
font-size: 0.92rem;
|
||||
line-height: 1.3;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
.card-media {
|
||||
@@ -376,7 +379,9 @@ export function EmptyStateHero({
|
||||
) : null}
|
||||
<LeadDescriptionText>{description}</LeadDescriptionText>
|
||||
{supportingDescription ? (
|
||||
<LeadSupportingText>{supportingDescription}</LeadSupportingText>
|
||||
<LeadSupportingText>
|
||||
{supportingDescription}
|
||||
</LeadSupportingText>
|
||||
) : null}
|
||||
</LeadTextGroup>
|
||||
|
||||
@@ -386,7 +391,9 @@ export function EmptyStateHero({
|
||||
<span
|
||||
key={badge.key}
|
||||
className={`${EMPTY_STATE_BADGE_BASE_CLASSNAME} ${
|
||||
EMPTY_STATE_BADGE_TONE_CLASSNAMES[badge.tone || "slate"]
|
||||
EMPTY_STATE_BADGE_TONE_CLASSNAMES[
|
||||
badge.tone || "slate"
|
||||
]
|
||||
}`}
|
||||
>
|
||||
{badge.label}
|
||||
@@ -394,11 +401,9 @@ export function EmptyStateHero({
|
||||
))}
|
||||
</div>
|
||||
) : null}
|
||||
|
||||
</LeadBody>
|
||||
</LeadBlock>
|
||||
</LeadColumn>
|
||||
|
||||
</StageGrid>
|
||||
|
||||
{prioritySlot ? (
|
||||
@@ -483,7 +488,6 @@ export function EmptyStateHero({
|
||||
</HeroCard>
|
||||
))}
|
||||
</CardsShell>
|
||||
|
||||
</HeroContent>
|
||||
</HeroSection>
|
||||
);
|
||||
|
||||
@@ -263,10 +263,14 @@ export function EmptyStateQuickActions({
|
||||
<button
|
||||
type="button"
|
||||
data-testid={
|
||||
item.testId ? `${item.testId}-secondary-status` : undefined
|
||||
item.testId
|
||||
? `${item.testId}-secondary-status`
|
||||
: undefined
|
||||
}
|
||||
className={`inline-flex items-center rounded-full border px-2 py-1 text-[11px] font-medium transition-colors hover:brightness-95 ${STATUS_TONE_CLASSNAMES[item.secondaryStatusTone ?? "slate"]}`}
|
||||
title={item.secondaryStatusDescription ?? "打开对应任务"}
|
||||
title={
|
||||
item.secondaryStatusDescription ?? "打开对应任务"
|
||||
}
|
||||
onClick={(event) => {
|
||||
event.stopPropagation();
|
||||
onSecondaryStatusAction(item);
|
||||
|
||||
@@ -78,7 +78,8 @@ const CONTEXT_SECTION_TITLE_CLASSNAME =
|
||||
const CONTEXT_SECTION_BADGE_CLASSNAME =
|
||||
"inline-flex min-h-4 min-w-4 items-center justify-center rounded-full border border-slate-200 bg-white px-1.5 text-[10px] font-semibold text-slate-500";
|
||||
|
||||
const CONTEXT_META_TEXT_CLASSNAME = "mt-1.5 text-[11px] leading-5 text-slate-500";
|
||||
const CONTEXT_META_TEXT_CLASSNAME =
|
||||
"mt-1.5 text-[11px] leading-5 text-slate-500";
|
||||
|
||||
const CONTEXT_ADD_BUTTON_CLASSNAME =
|
||||
"flex h-9 w-full items-center justify-center gap-2 rounded-[12px] border border-dashed border-slate-200/90 bg-white/90 px-3 text-sm font-medium text-slate-700 shadow-sm shadow-slate-950/5 transition-colors hover:border-slate-300 hover:bg-white hover:text-slate-900";
|
||||
@@ -91,7 +92,8 @@ const CONTEXT_SEARCH_INPUT_WRAP_CLASSNAME = "relative";
|
||||
const CONTEXT_SEARCH_INPUT_CLASSNAME =
|
||||
"h-7 w-full border-0 bg-transparent pl-7 text-[13px] leading-5 text-slate-900 placeholder:text-slate-400 focus:outline-none";
|
||||
|
||||
const CONTEXT_SEARCH_ICON_CLASSNAME = "absolute left-0 top-[5px] text-slate-400";
|
||||
const CONTEXT_SEARCH_ICON_CLASSNAME =
|
||||
"absolute left-0 top-[5px] text-slate-400";
|
||||
|
||||
const CONTEXT_SEARCH_ACTION_ROW_CLASSNAME =
|
||||
"mt-2.5 flex items-center justify-between gap-2.5";
|
||||
@@ -139,7 +141,8 @@ const CONTEXT_DETAIL_CARD_CLASSNAME =
|
||||
const CONTEXT_DETAIL_TITLE_CLASSNAME =
|
||||
"text-lg font-bold leading-[1.35] text-slate-900";
|
||||
|
||||
const CONTEXT_DETAIL_META_CLASSNAME = "mt-2 text-[11px] leading-5 text-slate-500";
|
||||
const CONTEXT_DETAIL_META_CLASSNAME =
|
||||
"mt-2 text-[11px] leading-5 text-slate-500";
|
||||
|
||||
const CONTEXT_DETAIL_SECTION_CLASSNAME =
|
||||
"mt-3 rounded-[12px] border border-slate-200/80 bg-slate-50/80 p-2.5";
|
||||
@@ -300,7 +303,11 @@ function renderContextList(
|
||||
>
|
||||
<button
|
||||
type="button"
|
||||
aria-label={interactive ? `查看搜索结果 ${item.name}` : `查看上下文 ${item.name}`}
|
||||
aria-label={
|
||||
interactive
|
||||
? `查看搜索结果 ${item.name}`
|
||||
: `查看上下文 ${item.name}`
|
||||
}
|
||||
className={CONTEXT_OPEN_BUTTON_CLASSNAME}
|
||||
onClick={() => {
|
||||
if (interactive) {
|
||||
@@ -312,7 +319,11 @@ function renderContextList(
|
||||
>
|
||||
<div className={CONTEXT_ICON_WRAP_CLASSNAME}>
|
||||
{item.source === "search" ? (
|
||||
item.searchMode === "social" ? <Share2 size={12} /> : <Globe size={12} />
|
||||
item.searchMode === "social" ? (
|
||||
<Share2 size={12} />
|
||||
) : (
|
||||
<Globe size={12} />
|
||||
)
|
||||
) : (
|
||||
<CheckCircle2 size={12} />
|
||||
)}
|
||||
@@ -385,7 +396,9 @@ function GeneralWorkbenchContextPanelComponent({
|
||||
<section className={CONTEXT_SECTION_CLASSNAME}>
|
||||
<div className={CONTEXT_SECTION_TITLE_CLASSNAME}>
|
||||
<span>搜索上下文</span>
|
||||
<span className={CONTEXT_SECTION_BADGE_CLASSNAME}>{latestSearchLabel}</span>
|
||||
<span className={CONTEXT_SECTION_BADGE_CLASSNAME}>
|
||||
{latestSearchLabel}
|
||||
</span>
|
||||
</div>
|
||||
<button
|
||||
type="button"
|
||||
@@ -403,7 +416,9 @@ function GeneralWorkbenchContextPanelComponent({
|
||||
className={CONTEXT_SEARCH_INPUT_CLASSNAME}
|
||||
value={contextSearchQuery}
|
||||
placeholder="搜索网络添加新上下文"
|
||||
onChange={(event) => onContextSearchQueryChange(event.target.value)}
|
||||
onChange={(event) =>
|
||||
onContextSearchQueryChange(event.target.value)
|
||||
}
|
||||
onKeyDown={(event) => {
|
||||
if (event.key === "Enter" && !isSearchActionDisabled) {
|
||||
event.preventDefault();
|
||||
@@ -420,13 +435,21 @@ function GeneralWorkbenchContextPanelComponent({
|
||||
aria-label="选择上下文搜索来源"
|
||||
className={CONTEXT_SEARCH_MODE_TRIGGER_CLASSNAME}
|
||||
>
|
||||
{contextSearchMode === "social" ? <Share2 size={13} /> : <Globe size={13} />}
|
||||
<span>{contextSearchMode === "social" ? "社交媒体" : "网络搜索"}</span>
|
||||
{contextSearchMode === "social" ? (
|
||||
<Share2 size={13} />
|
||||
) : (
|
||||
<Globe size={13} />
|
||||
)}
|
||||
<span>
|
||||
{contextSearchMode === "social" ? "社交媒体" : "网络搜索"}
|
||||
</span>
|
||||
<ChevronDown size={13} />
|
||||
</button>
|
||||
</DropdownMenuTrigger>
|
||||
<DropdownMenuContent align="start" className="w-36 p-1">
|
||||
<DropdownMenuItem onClick={() => onContextSearchModeChange("web")}>
|
||||
<DropdownMenuItem
|
||||
onClick={() => onContextSearchModeChange("web")}
|
||||
>
|
||||
<div className={CONTEXT_SEARCH_MODE_ROW_CLASSNAME}>
|
||||
<Globe size={14} />
|
||||
<span>网络搜索</span>
|
||||
@@ -437,7 +460,9 @@ function GeneralWorkbenchContextPanelComponent({
|
||||
) : null}
|
||||
</div>
|
||||
</DropdownMenuItem>
|
||||
<DropdownMenuItem onClick={() => onContextSearchModeChange("social")}>
|
||||
<DropdownMenuItem
|
||||
onClick={() => onContextSearchModeChange("social")}
|
||||
>
|
||||
<div className={CONTEXT_SEARCH_MODE_ROW_CLASSNAME}>
|
||||
<Share2 size={14} />
|
||||
<span>社交媒体</span>
|
||||
@@ -453,7 +478,9 @@ function GeneralWorkbenchContextPanelComponent({
|
||||
<button
|
||||
type="button"
|
||||
aria-label="提交上下文搜索"
|
||||
className={getContextSearchSubmitButtonClassName(isSearchActionDisabled)}
|
||||
className={getContextSearchSubmitButtonClassName(
|
||||
isSearchActionDisabled,
|
||||
)}
|
||||
disabled={isSearchActionDisabled}
|
||||
onClick={() => {
|
||||
if (!isSearchActionDisabled) {
|
||||
@@ -470,11 +497,17 @@ function GeneralWorkbenchContextPanelComponent({
|
||||
</div>
|
||||
</div>
|
||||
{contextSearchError ? (
|
||||
<div className={getContextHintTextClassName(true)}>{contextSearchError}</div>
|
||||
<div className={getContextHintTextClassName(true)}>
|
||||
{contextSearchError}
|
||||
</div>
|
||||
) : contextSearchLoading ? (
|
||||
<div className={getContextHintTextClassName()}>正在联网检索并整理上下文...</div>
|
||||
<div className={getContextHintTextClassName()}>
|
||||
正在联网检索并整理上下文...
|
||||
</div>
|
||||
) : contextSearchBlockedReason ? (
|
||||
<div className={getContextHintTextClassName()}>{contextSearchBlockedReason}</div>
|
||||
<div className={getContextHintTextClassName()}>
|
||||
{contextSearchBlockedReason}
|
||||
</div>
|
||||
) : (
|
||||
<div className={getContextHintTextClassName()}>
|
||||
输入关键词后按 Enter,可直接把检索结果加入当前上下文。
|
||||
@@ -498,7 +531,9 @@ function GeneralWorkbenchContextPanelComponent({
|
||||
</button>
|
||||
</div>
|
||||
<div className={CONTEXT_DETAIL_CARD_CLASSNAME}>
|
||||
<div className={CONTEXT_DETAIL_TITLE_CLASSNAME}>{selectedSearchResult.name}</div>
|
||||
<div className={CONTEXT_DETAIL_TITLE_CLASSNAME}>
|
||||
{selectedSearchResult.name}
|
||||
</div>
|
||||
<div className={CONTEXT_DETAIL_META_CLASSNAME}>
|
||||
{resolveContextSourceSubLabel(
|
||||
selectedSearchResult.source,
|
||||
@@ -510,13 +545,16 @@ function GeneralWorkbenchContextPanelComponent({
|
||||
{selectedSearchResult.active ? " · 已启用" : " · 未启用"}
|
||||
</div>
|
||||
<div className={CONTEXT_DETAIL_SECTION_CLASSNAME}>
|
||||
<div className={CONTEXT_DETAIL_SECTION_LABEL_CLASSNAME}>Source guide</div>
|
||||
<div className={CONTEXT_DETAIL_SECTION_LABEL_CLASSNAME}>
|
||||
Source guide
|
||||
</div>
|
||||
{selectedSearchResult.query ? (
|
||||
<div className={CONTEXT_QUERY_CLASSNAME}>
|
||||
检索词:{selectedSearchResult.query}
|
||||
</div>
|
||||
) : null}
|
||||
{selectedSearchResult.citations && selectedSearchResult.citations.length > 0 ? (
|
||||
{selectedSearchResult.citations &&
|
||||
selectedSearchResult.citations.length > 0 ? (
|
||||
<div className={CONTEXT_DETAIL_SOURCE_LIST_CLASSNAME}>
|
||||
{selectedSearchResult.citations.map((citation) => (
|
||||
<a
|
||||
@@ -553,12 +591,14 @@ function GeneralWorkbenchContextPanelComponent({
|
||||
<section className={cn(CONTEXT_SECTION_CLASSNAME, "border-b-0")}>
|
||||
<div className={CONTEXT_SECTION_TITLE_CLASSNAME}>
|
||||
<span>上下文列表</span>
|
||||
<span className={CONTEXT_SECTION_BADGE_CLASSNAME}>{contextItems.length} 条</span>
|
||||
<span className={CONTEXT_SECTION_BADGE_CLASSNAME}>
|
||||
{contextItems.length} 条
|
||||
</span>
|
||||
</div>
|
||||
<div className={CONTEXT_META_TEXT_CLASSNAME}>
|
||||
已生效 {contextBudget.activeCount}/{contextBudget.activeCountLimit} 条 · 检索结果{" "}
|
||||
{searchContextItems.length} 条 · 估算 {contextBudget.estimatedTokens}/
|
||||
{contextBudget.tokenLimit} tokens
|
||||
已生效 {contextBudget.activeCount}/{contextBudget.activeCountLimit}{" "}
|
||||
条 · 检索结果 {searchContextItems.length} 条 · 估算{" "}
|
||||
{contextBudget.estimatedTokens}/{contextBudget.tokenLimit} tokens
|
||||
</div>
|
||||
<div className={CONTEXT_META_TEXT_CLASSNAME}>
|
||||
搜索结果可点击查看详情,其他上下文可直接勾选启用。
|
||||
@@ -615,7 +655,9 @@ function GeneralWorkbenchContextPanelComponent({
|
||||
void onDropContextFile(event);
|
||||
}}
|
||||
>
|
||||
<div className={CONTEXT_DROP_HINT_CLASSNAME}>or drop your files here</div>
|
||||
<div className={CONTEXT_DROP_HINT_CLASSNAME}>
|
||||
or drop your files here
|
||||
</div>
|
||||
<div className={CONTEXT_MODAL_ACTION_GRID_CLASSNAME}>
|
||||
<button
|
||||
type="button"
|
||||
@@ -662,7 +704,9 @@ function GeneralWorkbenchContextPanelComponent({
|
||||
</div>
|
||||
</div>
|
||||
{contextCreateError ? (
|
||||
<div className={CONTEXT_MODAL_ERROR_TEXT_CLASSNAME}>{contextCreateError}</div>
|
||||
<div className={CONTEXT_MODAL_ERROR_TEXT_CLASSNAME}>
|
||||
{contextCreateError}
|
||||
</div>
|
||||
) : null}
|
||||
</div>
|
||||
</div>
|
||||
@@ -697,7 +741,9 @@ function GeneralWorkbenchContextPanelComponent({
|
||||
<ArrowLeft size={20} />
|
||||
</button>
|
||||
</div>
|
||||
<h3 className={CONTEXT_MODAL_TITLE_CENTERED_CLASSNAME}>添加文本内容</h3>
|
||||
<h3 className={CONTEXT_MODAL_TITLE_CENTERED_CLASSNAME}>
|
||||
添加文本内容
|
||||
</h3>
|
||||
<div className={CONTEXT_MODAL_HEADER_ACTIONS_CLASSNAME}>
|
||||
<button
|
||||
type="button"
|
||||
@@ -718,19 +764,26 @@ function GeneralWorkbenchContextPanelComponent({
|
||||
className={CONTEXT_TEXTAREA_CLASSNAME}
|
||||
value={contextDraftText}
|
||||
placeholder="在此粘贴或输入文本..."
|
||||
onChange={(event) => onContextDraftTextChange(event.target.value)}
|
||||
onChange={(event) =>
|
||||
onContextDraftTextChange(event.target.value)
|
||||
}
|
||||
/>
|
||||
{contextCreateError ? (
|
||||
<div className={CONTEXT_MODAL_ERROR_TEXT_CLASSNAME}>{contextCreateError}</div>
|
||||
<div className={CONTEXT_MODAL_ERROR_TEXT_CLASSNAME}>
|
||||
{contextCreateError}
|
||||
</div>
|
||||
) : null}
|
||||
<div className={CONTEXT_MODAL_FOOTER_CLASSNAME}>
|
||||
<button
|
||||
type="button"
|
||||
aria-label="确认添加文本上下文"
|
||||
className={getContextConfirmButtonClassName(
|
||||
contextCreateLoading || contextDraftText.trim().length === 0,
|
||||
contextCreateLoading ||
|
||||
contextDraftText.trim().length === 0,
|
||||
)}
|
||||
disabled={contextCreateLoading || contextDraftText.trim().length === 0}
|
||||
disabled={
|
||||
contextCreateLoading || contextDraftText.trim().length === 0
|
||||
}
|
||||
onClick={() => {
|
||||
void onSubmitTextContext();
|
||||
}}
|
||||
@@ -775,7 +828,9 @@ function GeneralWorkbenchContextPanelComponent({
|
||||
<ArrowLeft size={20} />
|
||||
</button>
|
||||
</div>
|
||||
<h3 className={CONTEXT_MODAL_TITLE_CENTERED_CLASSNAME}>添加网站链接</h3>
|
||||
<h3 className={CONTEXT_MODAL_TITLE_CENTERED_CLASSNAME}>
|
||||
添加网站链接
|
||||
</h3>
|
||||
<div className={CONTEXT_MODAL_HEADER_ACTIONS_CLASSNAME}>
|
||||
<button
|
||||
type="button"
|
||||
@@ -796,19 +851,26 @@ function GeneralWorkbenchContextPanelComponent({
|
||||
className={CONTEXT_LINK_INPUT_CLASSNAME}
|
||||
value={contextDraftLink}
|
||||
placeholder="请输入网站链接"
|
||||
onChange={(event) => onContextDraftLinkChange(event.target.value)}
|
||||
onChange={(event) =>
|
||||
onContextDraftLinkChange(event.target.value)
|
||||
}
|
||||
/>
|
||||
{contextCreateError ? (
|
||||
<div className={CONTEXT_MODAL_ERROR_TEXT_CLASSNAME}>{contextCreateError}</div>
|
||||
<div className={CONTEXT_MODAL_ERROR_TEXT_CLASSNAME}>
|
||||
{contextCreateError}
|
||||
</div>
|
||||
) : null}
|
||||
<div className={CONTEXT_MODAL_FOOTER_CLASSNAME}>
|
||||
<button
|
||||
type="button"
|
||||
aria-label="确认添加链接上下文"
|
||||
className={getContextConfirmButtonClassName(
|
||||
contextCreateLoading || contextDraftLink.trim().length === 0,
|
||||
contextCreateLoading ||
|
||||
contextDraftLink.trim().length === 0,
|
||||
)}
|
||||
disabled={contextCreateLoading || contextDraftLink.trim().length === 0}
|
||||
disabled={
|
||||
contextCreateLoading || contextDraftLink.trim().length === 0
|
||||
}
|
||||
onClick={() => {
|
||||
void onSubmitLinkContext();
|
||||
}}
|
||||
@@ -828,4 +890,6 @@ function GeneralWorkbenchContextPanelComponent({
|
||||
);
|
||||
}
|
||||
|
||||
export const GeneralWorkbenchContextPanel = memo(GeneralWorkbenchContextPanelComponent);
|
||||
export const GeneralWorkbenchContextPanel = memo(
|
||||
GeneralWorkbenchContextPanelComponent,
|
||||
);
|
||||
|
||||
@@ -94,7 +94,10 @@ const EXEC_LOG_DETAIL_TAG_LIST_CLASSNAME = "flex flex-wrap gap-1.5";
|
||||
const EXEC_LOG_DETAIL_TAG_CLASSNAME =
|
||||
"inline-flex min-h-6 items-center rounded-full border border-slate-200 bg-white px-2.5 text-[10.5px] font-medium text-slate-600";
|
||||
|
||||
function matchesExecLogFilter(entry: ExecLogEntry, filter: ExecLogFilter): boolean {
|
||||
function matchesExecLogFilter(
|
||||
entry: ExecLogEntry,
|
||||
filter: ExecLogFilter,
|
||||
): boolean {
|
||||
if (filter === "skill") {
|
||||
return entry.type === "run";
|
||||
}
|
||||
@@ -130,18 +133,14 @@ function getExecLogBadgeClassName(type: string, status?: string) {
|
||||
status === "completed" &&
|
||||
"border-emerald-200 bg-emerald-50 text-emerald-700",
|
||||
!status && type === "user" && "border-sky-200 bg-sky-50 text-sky-700",
|
||||
!status &&
|
||||
type === "thinking" &&
|
||||
"border-sky-100 bg-sky-50 text-sky-600",
|
||||
!status && type === "thinking" && "border-sky-100 bg-sky-50 text-sky-600",
|
||||
!status &&
|
||||
type === "response" &&
|
||||
"border-slate-200 bg-slate-100 text-slate-600",
|
||||
!status &&
|
||||
type === "run" &&
|
||||
"border-emerald-200 bg-emerald-50 text-emerald-700",
|
||||
!status &&
|
||||
type === "task" &&
|
||||
"border-amber-200 bg-amber-50 text-amber-700",
|
||||
!status && type === "task" && "border-amber-200 bg-amber-50 text-amber-700",
|
||||
!status && type === "tool" && "border-sky-200 bg-sky-50 text-sky-700",
|
||||
);
|
||||
}
|
||||
@@ -206,7 +205,9 @@ export function GeneralWorkbenchExecLog({
|
||||
|
||||
useEffect(() => {
|
||||
setExpandedEntryIds((previous) =>
|
||||
previous.filter((entryId) => entries.some((entry) => entry.id === entryId)),
|
||||
previous.filter((entryId) =>
|
||||
entries.some((entry) => entry.id === entryId),
|
||||
),
|
||||
);
|
||||
}, [entries]);
|
||||
|
||||
@@ -216,7 +217,8 @@ export function GeneralWorkbenchExecLog({
|
||||
return null;
|
||||
}
|
||||
const isExpanded = expandedEntryIds.includes(entry.id);
|
||||
const detailLabel = entry.detail.kind === "tool" ? "工具详情" : "技能详情";
|
||||
const detailLabel =
|
||||
entry.detail.kind === "tool" ? "工具详情" : "技能详情";
|
||||
const hasDetailContent =
|
||||
Boolean(entry.detail.sourceRef) ||
|
||||
Boolean(entry.detail.description) ||
|
||||
@@ -241,13 +243,19 @@ export function GeneralWorkbenchExecLog({
|
||||
onClick={() => toggleDetail(entry.id)}
|
||||
>
|
||||
{isExpanded ? `收起${detailLabel}` : `查看${detailLabel}`}
|
||||
{isExpanded ? <ChevronDown size={12} /> : <ChevronRight size={12} />}
|
||||
{isExpanded ? (
|
||||
<ChevronDown size={12} />
|
||||
) : (
|
||||
<ChevronRight size={12} />
|
||||
)}
|
||||
</button>
|
||||
{isExpanded ? (
|
||||
<div className={EXEC_LOG_DETAIL_PANEL_CLASSNAME}>
|
||||
{entry.detail.argumentsText ? (
|
||||
<div className={EXEC_LOG_DETAIL_SECTION_CLASSNAME}>
|
||||
<div className={EXEC_LOG_DETAIL_LABEL_CLASSNAME}>请求参数</div>
|
||||
<div className={EXEC_LOG_DETAIL_LABEL_CLASSNAME}>
|
||||
请求参数
|
||||
</div>
|
||||
<div className={EXEC_LOG_DETAIL_TEXT_CLASSNAME}>
|
||||
{entry.detail.argumentsText}
|
||||
</div>
|
||||
@@ -255,7 +263,9 @@ export function GeneralWorkbenchExecLog({
|
||||
) : null}
|
||||
{entry.detail.resultText ? (
|
||||
<div className={EXEC_LOG_DETAIL_SECTION_CLASSNAME}>
|
||||
<div className={EXEC_LOG_DETAIL_LABEL_CLASSNAME}>执行结果</div>
|
||||
<div className={EXEC_LOG_DETAIL_LABEL_CLASSNAME}>
|
||||
执行结果
|
||||
</div>
|
||||
<div className={EXEC_LOG_DETAIL_TEXT_CLASSNAME}>
|
||||
{entry.detail.resultText}
|
||||
</div>
|
||||
@@ -263,7 +273,9 @@ export function GeneralWorkbenchExecLog({
|
||||
) : null}
|
||||
{entry.detail.errorText ? (
|
||||
<div className={EXEC_LOG_DETAIL_SECTION_CLASSNAME}>
|
||||
<div className={EXEC_LOG_DETAIL_LABEL_CLASSNAME}>错误信息</div>
|
||||
<div className={EXEC_LOG_DETAIL_LABEL_CLASSNAME}>
|
||||
错误信息
|
||||
</div>
|
||||
<div className={EXEC_LOG_DETAIL_TEXT_CLASSNAME}>
|
||||
{entry.detail.errorText}
|
||||
</div>
|
||||
@@ -271,7 +283,9 @@ export function GeneralWorkbenchExecLog({
|
||||
) : null}
|
||||
{entry.detail.sourceRef ? (
|
||||
<div className={EXEC_LOG_DETAIL_SECTION_CLASSNAME}>
|
||||
<div className={EXEC_LOG_DETAIL_LABEL_CLASSNAME}>技能标识</div>
|
||||
<div className={EXEC_LOG_DETAIL_LABEL_CLASSNAME}>
|
||||
技能标识
|
||||
</div>
|
||||
<div className={EXEC_LOG_DETAIL_TEXT_CLASSNAME}>
|
||||
{entry.detail.sourceRef}
|
||||
</div>
|
||||
@@ -279,7 +293,9 @@ export function GeneralWorkbenchExecLog({
|
||||
) : null}
|
||||
{entry.detail.description ? (
|
||||
<div className={EXEC_LOG_DETAIL_SECTION_CLASSNAME}>
|
||||
<div className={EXEC_LOG_DETAIL_LABEL_CLASSNAME}>技能说明</div>
|
||||
<div className={EXEC_LOG_DETAIL_LABEL_CLASSNAME}>
|
||||
技能说明
|
||||
</div>
|
||||
<div className={EXEC_LOG_DETAIL_TEXT_CLASSNAME}>
|
||||
{entry.detail.description}
|
||||
</div>
|
||||
@@ -287,15 +303,20 @@ export function GeneralWorkbenchExecLog({
|
||||
) : null}
|
||||
{entry.detail.whenToUse ? (
|
||||
<div className={EXEC_LOG_DETAIL_SECTION_CLASSNAME}>
|
||||
<div className={EXEC_LOG_DETAIL_LABEL_CLASSNAME}>适用场景</div>
|
||||
<div className={EXEC_LOG_DETAIL_LABEL_CLASSNAME}>
|
||||
适用场景
|
||||
</div>
|
||||
<div className={EXEC_LOG_DETAIL_TEXT_CLASSNAME}>
|
||||
{entry.detail.whenToUse}
|
||||
</div>
|
||||
</div>
|
||||
) : null}
|
||||
{entry.detail.workflowSteps && entry.detail.workflowSteps.length > 0 ? (
|
||||
{entry.detail.workflowSteps &&
|
||||
entry.detail.workflowSteps.length > 0 ? (
|
||||
<div className={EXEC_LOG_DETAIL_SECTION_CLASSNAME}>
|
||||
<div className={EXEC_LOG_DETAIL_LABEL_CLASSNAME}>工作流步骤</div>
|
||||
<div className={EXEC_LOG_DETAIL_LABEL_CLASSNAME}>
|
||||
工作流步骤
|
||||
</div>
|
||||
<div className={EXEC_LOG_DETAIL_LIST_CLASSNAME}>
|
||||
{entry.detail.workflowSteps.map((step, index) => (
|
||||
<div
|
||||
@@ -308,9 +329,12 @@ export function GeneralWorkbenchExecLog({
|
||||
</div>
|
||||
</div>
|
||||
) : null}
|
||||
{entry.detail.allowedTools && entry.detail.allowedTools.length > 0 ? (
|
||||
{entry.detail.allowedTools &&
|
||||
entry.detail.allowedTools.length > 0 ? (
|
||||
<div className={EXEC_LOG_DETAIL_SECTION_CLASSNAME}>
|
||||
<div className={EXEC_LOG_DETAIL_LABEL_CLASSNAME}>允许工具</div>
|
||||
<div className={EXEC_LOG_DETAIL_LABEL_CLASSNAME}>
|
||||
允许工具
|
||||
</div>
|
||||
<div className={EXEC_LOG_DETAIL_TAG_LIST_CLASSNAME}>
|
||||
{entry.detail.allowedTools.map((toolName) => (
|
||||
<span
|
||||
@@ -323,9 +347,12 @@ export function GeneralWorkbenchExecLog({
|
||||
</div>
|
||||
</div>
|
||||
) : null}
|
||||
{entry.detail.artifactPaths && entry.detail.artifactPaths.length > 0 ? (
|
||||
{entry.detail.artifactPaths &&
|
||||
entry.detail.artifactPaths.length > 0 ? (
|
||||
<div className={EXEC_LOG_DETAIL_SECTION_CLASSNAME}>
|
||||
<div className={EXEC_LOG_DETAIL_LABEL_CLASSNAME}>关联产物</div>
|
||||
<div className={EXEC_LOG_DETAIL_LABEL_CLASSNAME}>
|
||||
关联产物
|
||||
</div>
|
||||
<div className={EXEC_LOG_DETAIL_LIST_CLASSNAME}>
|
||||
{entry.detail.artifactPaths.map((artifactPath) => (
|
||||
<div
|
||||
@@ -410,10 +437,17 @@ export function GeneralWorkbenchExecLog({
|
||||
<div className={EXEC_LOG_TIMELINE_CLASSNAME}>
|
||||
{filteredEntries.map((entry) => (
|
||||
<div key={entry.id} className={EXEC_LOG_ITEM_CLASSNAME}>
|
||||
<span className={getExecLogDotClassName(entry.type, entry.status)} />
|
||||
<span
|
||||
className={getExecLogDotClassName(entry.type, entry.status)}
|
||||
/>
|
||||
<div className={EXEC_LOG_ITEM_CARD_CLASSNAME}>
|
||||
<div className={EXEC_LOG_HEADER_CLASSNAME}>
|
||||
<span className={getExecLogBadgeClassName(entry.type, entry.status)}>
|
||||
<span
|
||||
className={getExecLogBadgeClassName(
|
||||
entry.type,
|
||||
entry.status,
|
||||
)}
|
||||
>
|
||||
{entry.typeLabel}
|
||||
</span>
|
||||
<span className={EXEC_LOG_TIME_CLASSNAME}>
|
||||
@@ -424,15 +458,20 @@ export function GeneralWorkbenchExecLog({
|
||||
minute: "2-digit",
|
||||
second: "2-digit",
|
||||
})
|
||||
: new Date(entry.timestamp).toLocaleTimeString("zh-CN", {
|
||||
hour: "2-digit",
|
||||
minute: "2-digit",
|
||||
second: "2-digit",
|
||||
})
|
||||
: new Date(entry.timestamp).toLocaleTimeString(
|
||||
"zh-CN",
|
||||
{
|
||||
hour: "2-digit",
|
||||
minute: "2-digit",
|
||||
second: "2-digit",
|
||||
},
|
||||
)
|
||||
: ""}
|
||||
</span>
|
||||
</div>
|
||||
<div className={EXEC_LOG_CONTENT_CLASSNAME}>{entry.content}</div>
|
||||
<div className={EXEC_LOG_CONTENT_CLASSNAME}>
|
||||
{entry.content}
|
||||
</div>
|
||||
{entry.meta ? (
|
||||
<div className={EXEC_LOG_META_CLASSNAME}>{entry.meta}</div>
|
||||
) : null}
|
||||
|
||||
@@ -77,7 +77,8 @@ export function GeneralWorkbenchHarnessCard({
|
||||
}: GeneralWorkbenchHarnessCardProps) {
|
||||
const runStateMeta = resolveRunStateMeta(runState);
|
||||
const resolvedRunTitle = runTitle?.trim() || "暂无运行记录";
|
||||
const artifactSummary = artifactCount > 0 ? `${artifactCount} 个产物` : "暂无产物";
|
||||
const artifactSummary =
|
||||
artifactCount > 0 ? `${artifactCount} 个产物` : "暂无产物";
|
||||
const updatedAtLabel = formatUpdatedAt(updatedAt);
|
||||
const iconLabel = [
|
||||
"工作台 Harness",
|
||||
@@ -312,7 +313,9 @@ export function GeneralWorkbenchHarnessCard({
|
||||
<span className="text-sm font-semibold text-foreground">
|
||||
工作台 Harness
|
||||
</span>
|
||||
<Badge variant={runStateMeta.variant}>{runStateMeta.label}</Badge>
|
||||
<Badge variant={runStateMeta.variant}>
|
||||
{runStateMeta.label}
|
||||
</Badge>
|
||||
{pendingCount > 0 ? (
|
||||
<Badge variant="destructive">{`待处理 ${pendingCount}`}</Badge>
|
||||
) : null}
|
||||
|
||||
@@ -114,9 +114,7 @@ function renderSidebar(
|
||||
searchMode: "web",
|
||||
query: "品牌 2026",
|
||||
previewText: "品牌讨论聚焦产品定位、渠道节奏与转化质量。",
|
||||
citations: [
|
||||
{ title: "官方博客", url: "https://example.com/blog" },
|
||||
],
|
||||
citations: [{ title: "官方博客", url: "https://example.com/blog" }],
|
||||
active: true,
|
||||
},
|
||||
],
|
||||
@@ -198,9 +196,9 @@ describe("GeneralWorkbenchSidebar", () => {
|
||||
it("点击添加上下文应打开添加弹窗", () => {
|
||||
const { container } = renderSidebar();
|
||||
|
||||
const addContextButton = Array.from(container.querySelectorAll("button")).find(
|
||||
(button) => button.textContent?.includes("添加上下文"),
|
||||
);
|
||||
const addContextButton = Array.from(
|
||||
container.querySelectorAll("button"),
|
||||
).find((button) => button.textContent?.includes("添加上下文"));
|
||||
expect(addContextButton).toBeTruthy();
|
||||
if (addContextButton) {
|
||||
act(() => {
|
||||
@@ -218,9 +216,9 @@ describe("GeneralWorkbenchSidebar", () => {
|
||||
const onAddTextContext = vi.fn().mockResolvedValue(undefined);
|
||||
const { container } = renderSidebar({ onAddTextContext });
|
||||
|
||||
const addContextButton = Array.from(container.querySelectorAll("button")).find(
|
||||
(button) => button.textContent?.includes("添加上下文"),
|
||||
);
|
||||
const addContextButton = Array.from(
|
||||
container.querySelectorAll("button"),
|
||||
).find((button) => button.textContent?.includes("添加上下文"));
|
||||
expect(addContextButton).toBeTruthy();
|
||||
if (addContextButton) {
|
||||
act(() => {
|
||||
@@ -346,7 +344,9 @@ describe("GeneralWorkbenchSidebar", () => {
|
||||
|
||||
expect(container.textContent).toContain("技能:社媒主稿与封面");
|
||||
expect(container.textContent).toContain("生成内容主稿,并补齐封面素材。");
|
||||
expect(container.textContent).toContain("技能标识:content_post_with_cover");
|
||||
expect(container.textContent).toContain(
|
||||
"技能标识:content_post_with_cover",
|
||||
);
|
||||
});
|
||||
|
||||
it("执行日志应支持展开技能详情", () => {
|
||||
@@ -379,7 +379,9 @@ describe("GeneralWorkbenchSidebar", () => {
|
||||
expect(container.textContent).toContain("文件读取");
|
||||
expect(container.textContent).toContain("图片生成");
|
||||
expect(container.textContent).toContain("适用场景");
|
||||
expect(container.textContent).toContain("适合需要主稿与封面同时产出的社媒场景。");
|
||||
expect(container.textContent).toContain(
|
||||
"适合需要主稿与封面同时产出的社媒场景。",
|
||||
);
|
||||
});
|
||||
|
||||
it("执行日志应支持展开工具详情", () => {
|
||||
@@ -625,7 +627,9 @@ describe("GeneralWorkbenchSidebar", () => {
|
||||
expect(container.textContent).toContain("品牌话题观察");
|
||||
expect(container.textContent).not.toContain("检索词:品牌 2026");
|
||||
expect(container.textContent).not.toContain("品牌讨论聚焦产品定位");
|
||||
expect((container.textContent?.match(/品牌话题观察/g) || []).length).toBe(1);
|
||||
expect((container.textContent?.match(/品牌话题观察/g) || []).length).toBe(
|
||||
1,
|
||||
);
|
||||
|
||||
const openButton = container.querySelector(
|
||||
'button[aria-label="查看搜索结果 品牌话题观察"]',
|
||||
@@ -771,7 +775,7 @@ describe("GeneralWorkbenchSidebar", () => {
|
||||
gateKey: "topic_select",
|
||||
source: "skill",
|
||||
artifactPaths: ["content-posts/research.md"],
|
||||
inputSummary: "{\"topic\":\"AI\"}",
|
||||
inputSummary: '{"topic":"AI"}',
|
||||
outputSummary: "已完成选题调研",
|
||||
},
|
||||
{
|
||||
@@ -810,7 +814,7 @@ describe("GeneralWorkbenchSidebar", () => {
|
||||
expect(container.textContent).toContain("write_file");
|
||||
expect(container.textContent).toContain("技能:research_topic");
|
||||
expect(container.textContent).toContain("修改:content-posts/research.md");
|
||||
expect(container.textContent).toContain("输入:{\"topic\":\"AI\"}");
|
||||
expect(container.textContent).toContain('输入:{"topic":"AI"}');
|
||||
expect(container.textContent).toContain("输出:已完成选题调研");
|
||||
const runButtons = Array.from(container.querySelectorAll("button")).filter(
|
||||
(button) => button.textContent === "运行:rungrp01",
|
||||
@@ -1006,18 +1010,25 @@ describe("GeneralWorkbenchSidebar", () => {
|
||||
});
|
||||
}
|
||||
|
||||
expect(container.textContent).toContain("工作流:social_content_pipeline_v1");
|
||||
expect(container.textContent).toContain(
|
||||
"工作流:social_content_pipeline_v1",
|
||||
);
|
||||
expect(container.textContent).toContain("执行ID:exec-artifact-1");
|
||||
expect(container.textContent).toContain("版本ID:ver-artifact-1");
|
||||
expect(container.textContent).toContain("阶段:选题闸门 → 写作闸门 → 发布闸门");
|
||||
expect(container.textContent).toContain(
|
||||
"阶段:选题闸门 → 写作闸门 → 发布闸门",
|
||||
);
|
||||
expect(container.textContent).toContain("content-posts/demo.md");
|
||||
expect(container.textContent).toContain("content-posts/demo.publish-pack.json");
|
||||
expect(container.textContent).toContain(
|
||||
"content-posts/demo.publish-pack.json",
|
||||
);
|
||||
|
||||
const copyArtifactButton = Array.from(container.querySelectorAll("button")).find(
|
||||
(button) =>
|
||||
button
|
||||
.getAttribute("aria-label")
|
||||
?.startsWith("复制产物路径-content-posts/demo.md"),
|
||||
const copyArtifactButton = Array.from(
|
||||
container.querySelectorAll("button"),
|
||||
).find((button) =>
|
||||
button
|
||||
.getAttribute("aria-label")
|
||||
?.startsWith("复制产物路径-content-posts/demo.md"),
|
||||
);
|
||||
expect(copyArtifactButton).toBeTruthy();
|
||||
if (copyArtifactButton) {
|
||||
@@ -1026,13 +1037,16 @@ describe("GeneralWorkbenchSidebar", () => {
|
||||
});
|
||||
}
|
||||
|
||||
expect(mockWriteClipboardText).toHaveBeenCalledWith("content-posts/demo.md");
|
||||
expect(mockWriteClipboardText).toHaveBeenCalledWith(
|
||||
"content-posts/demo.md",
|
||||
);
|
||||
|
||||
const revealArtifactButton = Array.from(container.querySelectorAll("button")).find(
|
||||
(button) =>
|
||||
button
|
||||
.getAttribute("aria-label")
|
||||
?.startsWith("定位产物路径-content-posts/demo.md"),
|
||||
const revealArtifactButton = Array.from(
|
||||
container.querySelectorAll("button"),
|
||||
).find((button) =>
|
||||
button
|
||||
.getAttribute("aria-label")
|
||||
?.startsWith("定位产物路径-content-posts/demo.md"),
|
||||
);
|
||||
expect(revealArtifactButton).toBeTruthy();
|
||||
if (revealArtifactButton) {
|
||||
@@ -1045,11 +1059,12 @@ describe("GeneralWorkbenchSidebar", () => {
|
||||
"content-posts/demo.md",
|
||||
);
|
||||
|
||||
const openArtifactButton = Array.from(container.querySelectorAll("button")).find(
|
||||
(button) =>
|
||||
button
|
||||
.getAttribute("aria-label")
|
||||
?.startsWith("打开产物路径-content-posts/demo.md"),
|
||||
const openArtifactButton = Array.from(
|
||||
container.querySelectorAll("button"),
|
||||
).find((button) =>
|
||||
button
|
||||
.getAttribute("aria-label")
|
||||
?.startsWith("打开产物路径-content-posts/demo.md"),
|
||||
);
|
||||
expect(openArtifactButton).toBeTruthy();
|
||||
if (openArtifactButton) {
|
||||
@@ -1101,11 +1116,12 @@ describe("GeneralWorkbenchSidebar", () => {
|
||||
});
|
||||
}
|
||||
|
||||
const revealArtifactButton = Array.from(container.querySelectorAll("button")).find(
|
||||
(button) =>
|
||||
button
|
||||
.getAttribute("aria-label")
|
||||
?.startsWith("定位活动产物路径-content-posts/group.md"),
|
||||
const revealArtifactButton = Array.from(
|
||||
container.querySelectorAll("button"),
|
||||
).find((button) =>
|
||||
button
|
||||
.getAttribute("aria-label")
|
||||
?.startsWith("定位活动产物路径-content-posts/group.md"),
|
||||
);
|
||||
expect(revealArtifactButton).toBeTruthy();
|
||||
if (revealArtifactButton) {
|
||||
@@ -1118,11 +1134,12 @@ describe("GeneralWorkbenchSidebar", () => {
|
||||
"content-posts/group.md",
|
||||
);
|
||||
|
||||
const openArtifactButton = Array.from(container.querySelectorAll("button")).find(
|
||||
(button) =>
|
||||
button
|
||||
.getAttribute("aria-label")
|
||||
?.startsWith("打开活动产物路径-content-posts/group.md"),
|
||||
const openArtifactButton = Array.from(
|
||||
container.querySelectorAll("button"),
|
||||
).find((button) =>
|
||||
button
|
||||
.getAttribute("aria-label")
|
||||
?.startsWith("打开活动产物路径-content-posts/group.md"),
|
||||
);
|
||||
expect(openArtifactButton).toBeTruthy();
|
||||
if (openArtifactButton) {
|
||||
@@ -1197,7 +1214,9 @@ describe("GeneralWorkbenchSidebar", () => {
|
||||
const consoleWarnSpy = vi
|
||||
.spyOn(console, "warn")
|
||||
.mockImplementation(() => {});
|
||||
mockRevealSessionFileInFinder.mockRejectedValueOnce(new Error("文件不存在"));
|
||||
mockRevealSessionFileInFinder.mockRejectedValueOnce(
|
||||
new Error("文件不存在"),
|
||||
);
|
||||
try {
|
||||
const { container } = renderSidebar({
|
||||
activeRunDetail: {
|
||||
|
||||
@@ -4,8 +4,7 @@ import { GeneralWorkbenchWorkflowPanel } from "./GeneralWorkbenchWorkflowPanel";
|
||||
import type { GeneralWorkbenchSidebarTab } from "./GeneralWorkbenchSidebarShell";
|
||||
import type { GeneralWorkbenchSidebarContentProps } from "./generalWorkbenchSidebarContentContract";
|
||||
|
||||
export interface GeneralWorkbenchSidebarPanelsProps
|
||||
extends GeneralWorkbenchSidebarContentProps {
|
||||
export interface GeneralWorkbenchSidebarPanelsProps extends GeneralWorkbenchSidebarContentProps {
|
||||
activeTab: GeneralWorkbenchSidebarTab;
|
||||
}
|
||||
|
||||
|
||||
@@ -130,7 +130,9 @@ export function GeneralWorkbenchSidebarShell({
|
||||
onClick={() => onTabChange("context")}
|
||||
>
|
||||
<span className={SIDEBAR_TAB_LABEL_CLASSNAME}>上下文</span>
|
||||
<span className={getSidebarTabCountClassName(activeTab === "context")}>
|
||||
<span
|
||||
className={getSidebarTabCountClassName(activeTab === "context")}
|
||||
>
|
||||
{activeContextCount}
|
||||
</span>
|
||||
</button>
|
||||
@@ -142,7 +144,9 @@ export function GeneralWorkbenchSidebarShell({
|
||||
onClick={() => onTabChange("workflow")}
|
||||
>
|
||||
<span className={SIDEBAR_TAB_LABEL_CLASSNAME}>编排</span>
|
||||
<span className={getSidebarTabCountClassName(activeTab === "workflow")}>
|
||||
<span
|
||||
className={getSidebarTabCountClassName(activeTab === "workflow")}
|
||||
>
|
||||
{branchCount}
|
||||
</span>
|
||||
</button>
|
||||
|
||||
@@ -20,7 +20,10 @@ import {
|
||||
DropdownMenuTrigger,
|
||||
} from "@/components/ui/dropdown-menu";
|
||||
import { cn } from "@/lib/utils";
|
||||
import type { TopicBranchItem, TopicBranchStatus } from "../hooks/useTopicBranchBoard";
|
||||
import type {
|
||||
TopicBranchItem,
|
||||
TopicBranchStatus,
|
||||
} from "../hooks/useTopicBranchBoard";
|
||||
import type { SidebarActivityLog } from "../hooks/useThemeContextWorkspace";
|
||||
import type {
|
||||
GeneralWorkbenchActivityLogGroup,
|
||||
@@ -249,8 +252,7 @@ function getWorkflowStepRowClassName(status: StepStatus) {
|
||||
"flex items-center gap-2 rounded-[12px] border px-2.5 py-2 text-sm leading-5",
|
||||
status === "completed" &&
|
||||
"border-emerald-200 bg-emerald-50/80 text-slate-900",
|
||||
status === "active" &&
|
||||
"border-sky-200 bg-sky-50/80 text-slate-900",
|
||||
status === "active" && "border-sky-200 bg-sky-50/80 text-slate-900",
|
||||
status !== "completed" &&
|
||||
status !== "active" &&
|
||||
"border-slate-200/80 bg-white/82 text-slate-500",
|
||||
@@ -274,7 +276,9 @@ function getBranchStatusText(status: TopicBranchStatus): string {
|
||||
return "备选";
|
||||
}
|
||||
|
||||
function formatGateLabel(gateKey?: SidebarActivityLog["gateKey"]): string | null {
|
||||
function formatGateLabel(
|
||||
gateKey?: SidebarActivityLog["gateKey"],
|
||||
): string | null {
|
||||
if (!gateKey || gateKey === "idle") {
|
||||
return null;
|
||||
}
|
||||
@@ -320,7 +324,8 @@ function renderActivityLogItem(
|
||||
const gateLabel = formatGateLabel(group.gateKey);
|
||||
const runLabel = formatRunIdShort(group.runId);
|
||||
const sourceLabel = group.source?.trim() || "-";
|
||||
const primaryLog = group.logs.find((log) => log.source === "skill") || group.logs[0];
|
||||
const primaryLog =
|
||||
group.logs.find((log) => log.source === "skill") || group.logs[0];
|
||||
|
||||
return (
|
||||
<ActivityItem key={`activity-${group.key}`}>
|
||||
@@ -351,14 +356,21 @@ function renderActivityLogItem(
|
||||
<span>{log.name}</span>
|
||||
<span className="ml-auto">{log.timeLabel}</span>
|
||||
</ActivityTitle>
|
||||
{log.inputSummary ? <ActivityMeta>输入:{log.inputSummary}</ActivityMeta> : null}
|
||||
{log.outputSummary ? <ActivityMeta>输出:{log.outputSummary}</ActivityMeta> : null}
|
||||
{log.inputSummary ? (
|
||||
<ActivityMeta>输入:{log.inputSummary}</ActivityMeta>
|
||||
) : null}
|
||||
{log.outputSummary ? (
|
||||
<ActivityMeta>输出:{log.outputSummary}</ActivityMeta>
|
||||
) : null}
|
||||
</ActivityStepItem>
|
||||
))}
|
||||
</ActivityStepList>
|
||||
<ActionRow>
|
||||
{group.runId && onViewRunDetail ? (
|
||||
<RunLinkButton type="button" onClick={() => onViewRunDetail(group.runId!)}>
|
||||
<RunLinkButton
|
||||
type="button"
|
||||
onClick={() => onViewRunDetail(group.runId!)}
|
||||
>
|
||||
运行:{runLabel || group.runId}
|
||||
</RunLinkButton>
|
||||
) : null}
|
||||
@@ -445,7 +457,10 @@ function GeneralWorkbenchWorkflowPanelComponent({
|
||||
<section className={cn(WORKFLOW_SECTION_CLASSNAME, "relative z-10")}>
|
||||
<DropdownMenu>
|
||||
<DropdownMenuTrigger asChild>
|
||||
<button type="button" className={WORKFLOW_NEW_TOPIC_BUTTON_CLASSNAME}>
|
||||
<button
|
||||
type="button"
|
||||
className={WORKFLOW_NEW_TOPIC_BUTTON_CLASSNAME}
|
||||
>
|
||||
<Plus size={14} />
|
||||
{isVersionMode ? "创建版本快照" : "新建分支任务"}
|
||||
<ChevronDown size={12} className="ml-auto" />
|
||||
@@ -492,7 +507,10 @@ function GeneralWorkbenchWorkflowPanelComponent({
|
||||
</div>
|
||||
<div className={WORKFLOW_STEP_LIST_CLASSNAME}>
|
||||
{workflowSteps.map((step) => (
|
||||
<div key={step.id} className={getWorkflowStepRowClassName(step.status)}>
|
||||
<div
|
||||
key={step.id}
|
||||
className={getWorkflowStepRowClassName(step.status)}
|
||||
>
|
||||
{getStepIcon(step.status)}
|
||||
<span>{step.title}</span>
|
||||
</div>
|
||||
@@ -503,12 +521,16 @@ function GeneralWorkbenchWorkflowPanelComponent({
|
||||
<section className={WORKFLOW_SECTION_CLASSNAME}>
|
||||
<div className={WORKFLOW_SECTION_TITLE_CLASSNAME}>
|
||||
<span>{isVersionMode ? "产物版本" : "篇内分支"}</span>
|
||||
<span className={WORKFLOW_SECTION_BADGE_CLASSNAME}>{branchItems.length}</span>
|
||||
<span className={WORKFLOW_SECTION_BADGE_CLASSNAME}>
|
||||
{branchItems.length}
|
||||
</span>
|
||||
</div>
|
||||
<BranchList className="custom-scrollbar">
|
||||
{branchItems.length === 0 ? (
|
||||
<ActivityMeta>
|
||||
{isVersionMode ? "暂无文稿版本,先生成或创建快照" : "暂无分支任务"}
|
||||
{isVersionMode
|
||||
? "暂无文稿版本,先生成或创建快照"
|
||||
: "暂无分支任务"}
|
||||
</ActivityMeta>
|
||||
) : (
|
||||
branchItems.map((item) => (
|
||||
@@ -522,16 +544,23 @@ function GeneralWorkbenchWorkflowPanelComponent({
|
||||
{getBranchStatusText(item.status)}
|
||||
</StatusBadge>
|
||||
{!isVersionMode ? (
|
||||
<DeleteButton onClick={() => onDeleteTopic(item.id)} aria-label="删除分支">
|
||||
<DeleteButton
|
||||
onClick={() => onDeleteTopic(item.id)}
|
||||
aria-label="删除分支"
|
||||
>
|
||||
<Trash2 size={12} />
|
||||
</DeleteButton>
|
||||
) : null}
|
||||
</BranchHead>
|
||||
<ActionRow>
|
||||
<TinyButton onClick={() => onSetBranchStatus(item.id, "merged")}>
|
||||
<TinyButton
|
||||
onClick={() => onSetBranchStatus(item.id, "merged")}
|
||||
>
|
||||
{isVersionMode ? "设为主稿" : "采纳到主稿"}
|
||||
</TinyButton>
|
||||
<TinyButton onClick={() => onSetBranchStatus(item.id, "pending")}>
|
||||
<TinyButton
|
||||
onClick={() => onSetBranchStatus(item.id, "pending")}
|
||||
>
|
||||
{isVersionMode ? "标记待评审" : "标记待决策"}
|
||||
</TinyButton>
|
||||
</ActionRow>
|
||||
@@ -554,7 +583,11 @@ function GeneralWorkbenchWorkflowPanelComponent({
|
||||
className={TOGGLE_BUTTON_CLASSNAME}
|
||||
onClick={onToggleCreationTasks}
|
||||
>
|
||||
{showCreationTasks ? <ChevronDown size={13} /> : <ChevronRight size={13} />}
|
||||
{showCreationTasks ? (
|
||||
<ChevronDown size={13} />
|
||||
) : (
|
||||
<ChevronRight size={13} />
|
||||
)}
|
||||
</button>
|
||||
</span>
|
||||
</div>
|
||||
@@ -585,7 +618,9 @@ function GeneralWorkbenchWorkflowPanelComponent({
|
||||
{task.absolutePath ? (
|
||||
<RunDetailArtifacts>
|
||||
<RunDetailArtifactRow>
|
||||
<RunDetailArtifactPath>{task.absolutePath}</RunDetailArtifactPath>
|
||||
<RunDetailArtifactPath>
|
||||
{task.absolutePath}
|
||||
</RunDetailArtifactPath>
|
||||
<RunDetailActionButton
|
||||
type="button"
|
||||
aria-label={`复制任务文件绝对路径-${task.taskId}`}
|
||||
@@ -633,7 +668,11 @@ function GeneralWorkbenchWorkflowPanelComponent({
|
||||
className={TOGGLE_BUTTON_CLASSNAME}
|
||||
onClick={onToggleActivityLogs}
|
||||
>
|
||||
{showActivityLogs ? <ChevronDown size={13} /> : <ChevronRight size={13} />}
|
||||
{showActivityLogs ? (
|
||||
<ChevronDown size={13} />
|
||||
) : (
|
||||
<ChevronRight size={13} />
|
||||
)}
|
||||
</button>
|
||||
</span>
|
||||
</div>
|
||||
@@ -659,15 +698,23 @@ function GeneralWorkbenchWorkflowPanelComponent({
|
||||
<RunDetailPanel>
|
||||
<RunDetailTitle>运行详情</RunDetailTitle>
|
||||
<RunDetailRow>ID:{activeRunDetail.id}</RunDetailRow>
|
||||
<RunDetailRow>状态:{formatRunStatusLabel(activeRunDetail.status)}</RunDetailRow>
|
||||
<RunDetailRow>
|
||||
状态:{formatRunStatusLabel(activeRunDetail.status)}
|
||||
</RunDetailRow>
|
||||
{runMetadataSummary.workflow ? (
|
||||
<RunDetailRow>工作流:{runMetadataSummary.workflow}</RunDetailRow>
|
||||
<RunDetailRow>
|
||||
工作流:{runMetadataSummary.workflow}
|
||||
</RunDetailRow>
|
||||
) : null}
|
||||
{runMetadataSummary.executionId ? (
|
||||
<RunDetailRow>执行ID:{runMetadataSummary.executionId}</RunDetailRow>
|
||||
<RunDetailRow>
|
||||
执行ID:{runMetadataSummary.executionId}
|
||||
</RunDetailRow>
|
||||
) : null}
|
||||
{runMetadataSummary.versionId ? (
|
||||
<RunDetailRow>版本ID:{runMetadataSummary.versionId}</RunDetailRow>
|
||||
<RunDetailRow>
|
||||
版本ID:{runMetadataSummary.versionId}
|
||||
</RunDetailRow>
|
||||
) : null}
|
||||
{activeRunStagesLabel ? (
|
||||
<RunDetailRow>阶段:{activeRunStagesLabel}</RunDetailRow>
|
||||
@@ -696,7 +743,9 @@ function GeneralWorkbenchWorkflowPanelComponent({
|
||||
<RunDetailArtifacts>
|
||||
{runMetadataSummary.artifactPaths.map((artifactPath) => (
|
||||
<RunDetailArtifactRow key={`run-detail-${artifactPath}`}>
|
||||
<RunDetailArtifactPath>{artifactPath}</RunDetailArtifactPath>
|
||||
<RunDetailArtifactPath>
|
||||
{artifactPath}
|
||||
</RunDetailArtifactPath>
|
||||
<RunDetailActionButton
|
||||
type="button"
|
||||
aria-label={`复制产物路径-${artifactPath}`}
|
||||
@@ -738,4 +787,6 @@ function GeneralWorkbenchWorkflowPanelComponent({
|
||||
);
|
||||
}
|
||||
|
||||
export const GeneralWorkbenchWorkflowPanel = memo(GeneralWorkbenchWorkflowPanelComponent);
|
||||
export const GeneralWorkbenchWorkflowPanel = memo(
|
||||
GeneralWorkbenchWorkflowPanelComponent,
|
||||
);
|
||||
|
||||
@@ -1169,7 +1169,10 @@ describe("HarnessStatusPanel", () => {
|
||||
setInputValue(strategyTextarea, "先补 runtime save,再补 UI 回归。");
|
||||
}
|
||||
if (regressionsTextarea) {
|
||||
setInputValue(regressionsTextarea, "npm run test:contracts\nRust 定向测试");
|
||||
setInputValue(
|
||||
regressionsTextarea,
|
||||
"npm run test:contracts\nRust 定向测试",
|
||||
);
|
||||
}
|
||||
if (followupsTextarea) {
|
||||
setInputValue(followupsTextarea, "补充 HarnessStatusPanel 测试");
|
||||
|
||||
@@ -346,7 +346,9 @@ export function ImageTaskViewer({
|
||||
</div>
|
||||
<div className="text-sm leading-6 text-slate-500">
|
||||
{reason === "error"
|
||||
? resolveImageUnavailableDescription(selectedTask?.mode)
|
||||
? resolveImageUnavailableDescription(
|
||||
selectedTask?.mode,
|
||||
)
|
||||
: resolveEmptyStateDescription(
|
||||
selectedTask?.status,
|
||||
selectedTask?.failureMessage,
|
||||
|
||||
@@ -108,10 +108,7 @@ function resolveDescription(preview: MessageImageWorkbenchPreview): string {
|
||||
const statusMessage = preview.statusMessage?.trim();
|
||||
if (
|
||||
statusMessage &&
|
||||
!(
|
||||
preview.status !== "running" &&
|
||||
isTransitionStatusMessage(statusMessage)
|
||||
)
|
||||
!(preview.status !== "running" && isTransitionStatusMessage(statusMessage))
|
||||
) {
|
||||
return statusMessage;
|
||||
}
|
||||
@@ -147,7 +144,9 @@ function resolveDescription(preview: MessageImageWorkbenchPreview): string {
|
||||
}
|
||||
}
|
||||
|
||||
function resolvePlaceholderLabel(preview: MessageImageWorkbenchPreview): string {
|
||||
function resolvePlaceholderLabel(
|
||||
preview: MessageImageWorkbenchPreview,
|
||||
): string {
|
||||
if (preview.status === "failed") {
|
||||
return "暂未生成成功";
|
||||
}
|
||||
@@ -174,11 +173,11 @@ function shouldShowSourceFootnote(
|
||||
): boolean {
|
||||
return Boolean(
|
||||
preview.mode === "edit" ||
|
||||
preview.mode === "variation" ||
|
||||
preview.sourceImageUrl?.trim() ||
|
||||
preview.sourceImagePrompt?.trim() ||
|
||||
preview.sourceImageRef?.trim() ||
|
||||
preview.sourceImageCount,
|
||||
preview.mode === "variation" ||
|
||||
preview.sourceImageUrl?.trim() ||
|
||||
preview.sourceImagePrompt?.trim() ||
|
||||
preview.sourceImageRef?.trim() ||
|
||||
preview.sourceImageCount,
|
||||
);
|
||||
}
|
||||
|
||||
@@ -214,7 +213,10 @@ function resolveSourceFootnote(
|
||||
return `${resolveSourceLabel(preview.mode)}:${resolveSourceSummary(preview)}`;
|
||||
}
|
||||
|
||||
function renderPlaceholder(preview: MessageImageWorkbenchPreview, reason: string) {
|
||||
function renderPlaceholder(
|
||||
preview: MessageImageWorkbenchPreview,
|
||||
reason: string,
|
||||
) {
|
||||
return (
|
||||
<div className="flex aspect-[16/10] items-center justify-center bg-[linear-gradient(180deg,rgba(248,250,252,0.98),rgba(241,245,249,0.98))] px-6 text-center">
|
||||
<div className="space-y-2">
|
||||
|
||||
+1
-5
@@ -1,10 +1,6 @@
|
||||
import React from "react";
|
||||
import { ShieldCheck } from "lucide-react";
|
||||
import {
|
||||
MetaSelect,
|
||||
MetaSelectIcon,
|
||||
MetaSelectWrap,
|
||||
} from "../styles";
|
||||
import { MetaSelect, MetaSelectIcon, MetaSelectWrap } from "../styles";
|
||||
import type { AgentAccessMode } from "../../../hooks/agentChatStorage";
|
||||
|
||||
interface InputbarAccessModeSelectProps {
|
||||
|
||||
@@ -326,7 +326,9 @@ describe("InputbarCore", () => {
|
||||
expect(removeButton).toBeTruthy();
|
||||
|
||||
act(() => {
|
||||
removeButton?.dispatchEvent(new MouseEvent("mousedown", { bubbles: true }));
|
||||
removeButton?.dispatchEvent(
|
||||
new MouseEvent("mousedown", { bubbles: true }),
|
||||
);
|
||||
removeButton?.dispatchEvent(new MouseEvent("click", { bubbles: true }));
|
||||
});
|
||||
|
||||
|
||||
@@ -170,7 +170,8 @@ export const InputbarCore: React.FC<InputbarCoreProps> = ({
|
||||
: "";
|
||||
const shouldRenderMetaBar =
|
||||
!shouldUseCompactFloatingComposer &&
|
||||
(Boolean(leftExtra) || (toolMode === "default" && !shouldCollapseFloatingTools));
|
||||
(Boolean(leftExtra) ||
|
||||
(toolMode === "default" && !shouldCollapseFloatingTools));
|
||||
const dictationButtonTitle = isDictationProcessing
|
||||
? dictationState === "polishing"
|
||||
? "语音润色中"
|
||||
@@ -189,11 +190,18 @@ export const InputbarCore: React.FC<InputbarCoreProps> = ({
|
||||
}, [isFloatingVariant, toolMode]);
|
||||
|
||||
const handleCollapseComposer = useCallback(() => {
|
||||
if (!isFloatingVariant || toolMode !== "attach-only" || pendingImages.length > 0) {
|
||||
if (
|
||||
!isFloatingVariant ||
|
||||
toolMode !== "attach-only" ||
|
||||
pendingImages.length > 0
|
||||
) {
|
||||
return;
|
||||
}
|
||||
const activeElement = document.activeElement;
|
||||
if (activeElement && inputBarContainerRef.current?.contains(activeElement)) {
|
||||
if (
|
||||
activeElement &&
|
||||
inputBarContainerRef.current?.contains(activeElement)
|
||||
) {
|
||||
return;
|
||||
}
|
||||
setIsComposerExpanded(false);
|
||||
@@ -301,7 +309,9 @@ export const InputbarCore: React.FC<InputbarCoreProps> = ({
|
||||
type="button"
|
||||
aria-label={`移除图片 ${index + 1}`}
|
||||
onMouseDown={handleRemoveImageMouseDown}
|
||||
onClick={(event) => handleRemoveImageClick(event, index)}
|
||||
onClick={(event) =>
|
||||
handleRemoveImageClick(event, index)
|
||||
}
|
||||
>
|
||||
<X size={12} />
|
||||
</ImageRemoveButton>
|
||||
@@ -339,7 +349,9 @@ export const InputbarCore: React.FC<InputbarCoreProps> = ({
|
||||
onClick={handleToggleTextareaExpanded}
|
||||
disabled={disabled}
|
||||
className={isTextareaExpanded ? "is-active" : ""}
|
||||
aria-label={isTextareaExpanded ? "收起输入框" : "展开输入框"}
|
||||
aria-label={
|
||||
isTextareaExpanded ? "收起输入框" : "展开输入框"
|
||||
}
|
||||
title={isTextareaExpanded ? "收起输入框" : "展开输入框"}
|
||||
>
|
||||
{isTextareaExpanded ? (
|
||||
|
||||
+5
-2
@@ -18,8 +18,11 @@ interface InputbarExecutionStrategySelectProps {
|
||||
export const InputbarExecutionStrategySelect: React.FC<
|
||||
InputbarExecutionStrategySelectProps
|
||||
> = (props) => {
|
||||
const { isFullscreen = false, executionStrategy, setExecutionStrategy } =
|
||||
props;
|
||||
const {
|
||||
isFullscreen = false,
|
||||
executionStrategy,
|
||||
setExecutionStrategy,
|
||||
} = props;
|
||||
|
||||
if (isFullscreen || !setExecutionStrategy) {
|
||||
return null;
|
||||
|
||||
@@ -1,9 +1,5 @@
|
||||
import React from "react";
|
||||
import {
|
||||
Lightbulb,
|
||||
Globe,
|
||||
Workflow,
|
||||
} from "lucide-react";
|
||||
import { Lightbulb, Globe, Workflow } from "lucide-react";
|
||||
import { ToolButton } from "../styles";
|
||||
import { isGeneralResearchTheme } from "../../../utils/generalAgentPrompt";
|
||||
|
||||
|
||||
+5
-2
@@ -9,7 +9,8 @@ const mockUseProviderModels = vi.fn();
|
||||
const mockResolveVisionModel = vi.fn();
|
||||
|
||||
vi.mock("@/hooks/useConfiguredProviders", () => ({
|
||||
useConfiguredProviders: (options: unknown) => mockUseConfiguredProviders(options),
|
||||
useConfiguredProviders: (options: unknown) =>
|
||||
mockUseConfiguredProviders(options),
|
||||
}));
|
||||
|
||||
vi.mock("@/hooks/useProviderModels", () => ({
|
||||
@@ -131,6 +132,8 @@ describe("InputbarVisionCapabilityNotice", () => {
|
||||
|
||||
const container = renderNotice();
|
||||
|
||||
expect(container.textContent).toContain("当前 Provider 暂无可用的多模态模型");
|
||||
expect(container.textContent).toContain(
|
||||
"当前 Provider 暂无可用的多模态模型",
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
+5
-1
@@ -31,7 +31,11 @@ export const InputbarVisionCapabilityNotice: React.FC<
|
||||
providerType: selectedProvider?.type,
|
||||
apiHost: selectedProvider?.apiHost,
|
||||
}),
|
||||
[selectedProvider?.apiHost, selectedProvider?.providerId, selectedProvider?.type],
|
||||
[
|
||||
selectedProvider?.apiHost,
|
||||
selectedProvider?.providerId,
|
||||
selectedProvider?.type,
|
||||
],
|
||||
);
|
||||
|
||||
const { models, loading: modelsLoading } = useProviderModels(
|
||||
|
||||
@@ -158,7 +158,9 @@ describe("TeamSelectorPanel", () => {
|
||||
|
||||
await flushEffects();
|
||||
|
||||
const savedTeam = onSelectTeam.mock.calls[0]?.[0] as TeamDefinition | undefined;
|
||||
const savedTeam = onSelectTeam.mock.calls[0]?.[0] as
|
||||
| TeamDefinition
|
||||
| undefined;
|
||||
|
||||
expect(savedTeam).toBeTruthy();
|
||||
expect(savedTeam?.source).toBe("custom");
|
||||
|
||||
@@ -210,8 +210,8 @@ function TeamCard({
|
||||
{role.profileId ? (
|
||||
<span className="rounded-full border border-slate-200 bg-slate-50 px-2 py-0.5 text-[11px] text-slate-500">
|
||||
画像 ·{" "}
|
||||
{getBuiltinTeamProfileOption(role.profileId)?.label ||
|
||||
role.profileId}
|
||||
{getBuiltinTeamProfileOption(role.profileId)
|
||||
?.label || role.profileId}
|
||||
</span>
|
||||
) : null}
|
||||
{role.roleKey ? (
|
||||
@@ -225,7 +225,8 @@ function TeamCard({
|
||||
className="rounded-full border border-slate-200 bg-slate-50 px-2 py-0.5 text-[11px] text-slate-500"
|
||||
>
|
||||
技能 ·{" "}
|
||||
{getBuiltinTeamSkillOption(skillId)?.label || skillId}
|
||||
{getBuiltinTeamSkillOption(skillId)?.label ||
|
||||
skillId}
|
||||
</span>
|
||||
))}
|
||||
</div>
|
||||
@@ -332,7 +333,9 @@ export const TeamSelectorPanel: React.FC<TeamSelectorPanelProps> = ({
|
||||
|
||||
const builtinTeams = useMemo(
|
||||
() =>
|
||||
listBuiltinTeamDefinitions().filter((team) => matchTeamQuery(team, query)),
|
||||
listBuiltinTeamDefinitions().filter((team) =>
|
||||
matchTeamQuery(team, query),
|
||||
),
|
||||
[query],
|
||||
);
|
||||
|
||||
@@ -374,7 +377,11 @@ export const TeamSelectorPanel: React.FC<TeamSelectorPanelProps> = ({
|
||||
};
|
||||
|
||||
const handleStartCreate = (base?: TeamDefinition | null) => {
|
||||
setDraft(base ? buildDraftFromTeam(cloneTeamDefinitionAsCustom(base)) : createBlankDraft(activeTheme));
|
||||
setDraft(
|
||||
base
|
||||
? buildDraftFromTeam(cloneTeamDefinitionAsCustom(base))
|
||||
: createBlankDraft(activeTheme),
|
||||
);
|
||||
};
|
||||
|
||||
const handleStartEdit = (team: TeamDefinition) => {
|
||||
@@ -399,7 +406,8 @@ export const TeamSelectorPanel: React.FC<TeamSelectorPanelProps> = ({
|
||||
theme: draft?.theme,
|
||||
presetId:
|
||||
draft?.presetId ||
|
||||
(draft?.id && customTeams.find((team) => team.id === draft.id)?.presetId),
|
||||
(draft?.id &&
|
||||
customTeams.find((team) => team.id === draft.id)?.presetId),
|
||||
roles: draft?.roles,
|
||||
});
|
||||
|
||||
@@ -417,9 +425,10 @@ export const TeamSelectorPanel: React.FC<TeamSelectorPanelProps> = ({
|
||||
Date.now(),
|
||||
};
|
||||
|
||||
const nextCustomTeams = [...customTeams.filter((team) => team.id !== nextTeam.id), nextTeam].sort(
|
||||
(left, right) => (right.updatedAt || 0) - (left.updatedAt || 0),
|
||||
);
|
||||
const nextCustomTeams = [
|
||||
...customTeams.filter((team) => team.id !== nextTeam.id),
|
||||
nextTeam,
|
||||
].sort((left, right) => (right.updatedAt || 0) - (left.updatedAt || 0));
|
||||
try {
|
||||
await persistCustomTeams(nextCustomTeams);
|
||||
setCustomTeams(nextCustomTeams);
|
||||
@@ -514,7 +523,8 @@ export const TeamSelectorPanel: React.FC<TeamSelectorPanelProps> = ({
|
||||
if (!inspectorTeam) {
|
||||
return (
|
||||
<div className="rounded-3xl border border-dashed border-slate-200 bg-white px-5 py-6 text-sm text-slate-500">
|
||||
还没有可预览的 Team。你可以从左侧模板中查看详情,或新建一个自定义 Team。
|
||||
还没有可预览的 Team。你可以从左侧模板中查看详情,或新建一个自定义
|
||||
Team。
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -550,7 +560,8 @@ export const TeamSelectorPanel: React.FC<TeamSelectorPanelProps> = ({
|
||||
) : null}
|
||||
</div>
|
||||
<div className="mt-2 text-sm leading-6 text-slate-600">
|
||||
{inspectorTeam.description || "该 Team 将作为主代理拆分子任务时的协作参考。"}
|
||||
{inspectorTeam.description ||
|
||||
"该 Team 将作为主代理拆分子任务时的协作参考。"}
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex flex-wrap gap-2">
|
||||
@@ -939,7 +950,9 @@ export const TeamSelectorPanel: React.FC<TeamSelectorPanelProps> = ({
|
||||
onChange={(event) =>
|
||||
updateDraftRole(index, (item) => ({
|
||||
...item,
|
||||
skillIds: parseSkillIdsInput(event.target.value),
|
||||
skillIds: parseSkillIdsInput(
|
||||
event.target.value,
|
||||
),
|
||||
}))
|
||||
}
|
||||
data-testid={`team-role-skill-ids-input-${index}`}
|
||||
@@ -1054,10 +1067,7 @@ export const TeamSelectorPanel: React.FC<TeamSelectorPanelProps> = ({
|
||||
className="bg-slate-900 text-white hover:bg-slate-800"
|
||||
onClick={() =>
|
||||
handleStartCreate(
|
||||
selectedTeam ||
|
||||
recommendedTeam ||
|
||||
builtinTeams[0] ||
|
||||
null,
|
||||
selectedTeam || recommendedTeam || builtinTeams[0] || null,
|
||||
)
|
||||
}
|
||||
>
|
||||
|
||||
@@ -18,10 +18,7 @@ interface UseHintRoutesParams {
|
||||
textareaRef: RefObject<HTMLTextAreaElement>;
|
||||
}
|
||||
|
||||
export function useHintRoutes({
|
||||
setInput,
|
||||
textareaRef,
|
||||
}: UseHintRoutesParams) {
|
||||
export function useHintRoutes({ setInput, textareaRef }: UseHintRoutesParams) {
|
||||
const [showHintPopup, setShowHintPopup] = useState(false);
|
||||
const [hintRoutes, setHintRoutes] = useState<HintRouteItem[]>([]);
|
||||
const [hintIndex, setHintIndex] = useState(0);
|
||||
|
||||
@@ -40,7 +40,8 @@ class MockFileReader {
|
||||
}
|
||||
|
||||
function Harness() {
|
||||
const { pendingImages, handlePaste, handleRemoveImage } = useImageAttachments();
|
||||
const { pendingImages, handlePaste, handleRemoveImage } =
|
||||
useImageAttachments();
|
||||
|
||||
return (
|
||||
<div>
|
||||
|
||||
@@ -18,11 +18,17 @@ export function useImageAttachments() {
|
||||
const fileInputRef = useRef<HTMLInputElement>(null);
|
||||
|
||||
const appendImageFile = useCallback(
|
||||
async (file: File, successMessage?: string, preferredMediaType?: string) => {
|
||||
async (
|
||||
file: File,
|
||||
successMessage?: string,
|
||||
preferredMediaType?: string,
|
||||
) => {
|
||||
try {
|
||||
const image = await readImageAttachment(file, preferredMediaType);
|
||||
setPendingImages((prev) => [...prev, image]);
|
||||
toast.success(successMessage ?? `已添加图片: ${file.name || "未命名图片"}`);
|
||||
toast.success(
|
||||
successMessage ?? `已添加图片: ${file.name || "未命名图片"}`,
|
||||
);
|
||||
} catch {
|
||||
toast.error(`图片读取失败: ${file.name || "未命名图片"}`);
|
||||
}
|
||||
|
||||
@@ -1,4 +1,10 @@
|
||||
import { useCallback, useEffect, useRef, useState, type RefObject } from "react";
|
||||
import {
|
||||
useCallback,
|
||||
useEffect,
|
||||
useRef,
|
||||
useState,
|
||||
type RefObject,
|
||||
} from "react";
|
||||
import { toast } from "sonner";
|
||||
import {
|
||||
cancelRecording,
|
||||
@@ -32,7 +38,9 @@ function insertTranscriptAtCursor(
|
||||
if (!textarea) {
|
||||
return {
|
||||
nextText: currentText ? `${currentText}\n${transcript}` : transcript,
|
||||
cursor: currentText ? currentText.length + transcript.length + 1 : transcript.length,
|
||||
cursor: currentText
|
||||
? currentText.length + transcript.length + 1
|
||||
: transcript.length,
|
||||
};
|
||||
}
|
||||
|
||||
@@ -40,10 +48,8 @@ function insertTranscriptAtCursor(
|
||||
const selectionEnd = textarea.selectionEnd ?? currentText.length;
|
||||
const before = currentText.slice(0, selectionStart);
|
||||
const after = currentText.slice(selectionEnd);
|
||||
const prefix =
|
||||
before.length > 0 && !/[\s\n]$/.test(before) ? "\n" : "";
|
||||
const suffix =
|
||||
after.length > 0 && !/^[\s\n]/.test(after) ? "\n" : "";
|
||||
const prefix = before.length > 0 && !/[\s\n]$/.test(before) ? "\n" : "";
|
||||
const suffix = after.length > 0 && !/^[\s\n]/.test(after) ? "\n" : "";
|
||||
const inserted = `${prefix}${transcript}${suffix}`;
|
||||
const nextText = `${before}${inserted}${after}`;
|
||||
const cursor = before.length + inserted.length;
|
||||
@@ -85,13 +91,16 @@ export function useInputbarDictation({
|
||||
};
|
||||
}, []);
|
||||
|
||||
const focusTextarea = useCallback((cursor: number) => {
|
||||
window.requestAnimationFrame(() => {
|
||||
const textarea = textareaRef.current;
|
||||
textarea?.focus();
|
||||
textarea?.setSelectionRange(cursor, cursor);
|
||||
});
|
||||
}, [textareaRef]);
|
||||
const focusTextarea = useCallback(
|
||||
(cursor: number) => {
|
||||
window.requestAnimationFrame(() => {
|
||||
const textarea = textareaRef.current;
|
||||
textarea?.focus();
|
||||
textarea?.setSelectionRange(cursor, cursor);
|
||||
});
|
||||
},
|
||||
[textareaRef],
|
||||
);
|
||||
|
||||
const insertTranscript = useCallback(
|
||||
(transcript: string) => {
|
||||
|
||||
@@ -29,8 +29,7 @@ export function useInputbarToolState({
|
||||
);
|
||||
const [isFullscreen, setIsFullscreen] = useState(false);
|
||||
|
||||
const webSearchEnabled =
|
||||
toolStates?.webSearch ?? localToolStates.webSearch;
|
||||
const webSearchEnabled = toolStates?.webSearch ?? localToolStates.webSearch;
|
||||
const thinkingEnabled = toolStates?.thinking ?? localToolStates.thinking;
|
||||
const subagentEnabled = toolStates?.subagent ?? localToolStates.subagent;
|
||||
|
||||
@@ -127,12 +126,7 @@ export function useInputbarToolState({
|
||||
});
|
||||
toast.info(`多代理${enabled ? "偏好已开启" : "偏好已关闭"}`);
|
||||
},
|
||||
[
|
||||
subagentEnabled,
|
||||
thinkingEnabled,
|
||||
updateToolStates,
|
||||
webSearchEnabled,
|
||||
],
|
||||
[subagentEnabled, thinkingEnabled, updateToolStates, webSearchEnabled],
|
||||
);
|
||||
|
||||
return {
|
||||
|
||||
@@ -39,10 +39,10 @@ export const Container = styled.div`
|
||||
|
||||
export const InputBarContainer = styled.div`
|
||||
position: relative;
|
||||
border: 1px solid #dcfce7;
|
||||
border: 1px solid rgba(110, 231, 183, 0.55);
|
||||
border-radius: 22px;
|
||||
padding: 10px 12px 10px 10px;
|
||||
background: linear-gradient(180deg, #fdfffb 0%, #f4fbf5 100%);
|
||||
background: linear-gradient(180deg, #fdfffb 0%, #f6fbf7 100%);
|
||||
box-shadow:
|
||||
0 10px 28px rgba(22, 101, 52, 0.04),
|
||||
inset 0 1px 0 rgba(255, 255, 255, 0.9);
|
||||
@@ -52,10 +52,10 @@ export const InputBarContainer = styled.div`
|
||||
background 0.2s ease;
|
||||
|
||||
&:focus-within {
|
||||
border-color: #bbf7d0;
|
||||
background: linear-gradient(180deg, #ffffff 0%, #f0fdf4 100%);
|
||||
border-color: rgba(74, 222, 128, 0.65);
|
||||
background: linear-gradient(180deg, #ffffff 0%, #f4fbf5 100%);
|
||||
box-shadow:
|
||||
0 0 0 3px rgba(134, 239, 172, 0.35),
|
||||
0 0 0 3px rgba(74, 222, 128, 0.22),
|
||||
0 14px 32px rgba(22, 101, 52, 0.06),
|
||||
inset 0 1px 0 rgba(255, 255, 255, 0.92);
|
||||
}
|
||||
@@ -68,8 +68,8 @@ export const InputBarContainer = styled.div`
|
||||
&.floating-composer {
|
||||
border-radius: 20px;
|
||||
padding: 16px 18px 14px 16px;
|
||||
background: linear-gradient(180deg, #fcfffc 0%, #f4fcf5 100%);
|
||||
border-color: #d1fae5;
|
||||
background: linear-gradient(180deg, #fcfffc 0%, #f6fbf7 100%);
|
||||
border-color: rgba(110, 231, 183, 0.6);
|
||||
box-shadow:
|
||||
0 16px 32px rgba(22, 101, 52, 0.05),
|
||||
inset 0 1px 0 rgba(255, 255, 255, 0.85);
|
||||
@@ -81,13 +81,12 @@ export const InputBarContainer = styled.div`
|
||||
}
|
||||
|
||||
&.floating-composer:focus-within {
|
||||
background: linear-gradient(180deg, #ffffff 0%, #ecfdf5 100%);
|
||||
border-color: #a7f3d0;
|
||||
background: linear-gradient(180deg, #ffffff 0%, #f0fdf4 100%);
|
||||
border-color: rgba(74, 222, 128, 0.7);
|
||||
box-shadow:
|
||||
0 0 0 3px rgba(110, 231, 183, 0.38),
|
||||
0 0 0 3px rgba(74, 222, 128, 0.25),
|
||||
0 18px 34px rgba(22, 101, 52, 0.08),
|
||||
inset 0 1px 0 rgba(255, 255, 255, 0.88);
|
||||
|
||||
}
|
||||
`;
|
||||
|
||||
@@ -240,10 +239,10 @@ export const MetaSelectWrap = styled.label`
|
||||
}
|
||||
|
||||
&:focus-within {
|
||||
border-color: rgba(125, 167, 255, 0.42);
|
||||
border-color: rgba(167, 243, 208, 0.42);
|
||||
background: rgba(255, 255, 255, 0.98);
|
||||
box-shadow:
|
||||
0 0 0 3px rgba(191, 219, 254, 0.28),
|
||||
0 0 0 3px rgba(167, 243, 208, 0.28),
|
||||
inset 0 1px 0 rgba(255, 255, 255, 0.92);
|
||||
color: hsl(var(--foreground));
|
||||
}
|
||||
@@ -299,11 +298,9 @@ export const MetaToggleButton = styled.button<{
|
||||
border-radius: 999px;
|
||||
border: 1px solid
|
||||
${({ $checked }) =>
|
||||
$checked
|
||||
? "rgba(125, 167, 255, 0.4)"
|
||||
: "rgba(148, 163, 184, 0.24)"};
|
||||
$checked ? "rgba(167, 243, 208, 0.4)" : "rgba(148, 163, 184, 0.24)"};
|
||||
background: ${({ $checked }) =>
|
||||
$checked ? "rgba(240, 246, 255, 0.98)" : "rgba(255, 255, 255, 0.9)"};
|
||||
$checked ? "rgba(240, 253, 244, 0.98)" : "rgba(255, 255, 255, 0.9)"};
|
||||
box-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.82);
|
||||
color: ${({ $checked }) =>
|
||||
$checked ? "hsl(var(--foreground))" : "hsl(var(--muted-foreground))"};
|
||||
@@ -317,20 +314,18 @@ export const MetaToggleButton = styled.button<{
|
||||
|
||||
&:hover {
|
||||
border-color: ${({ $checked }) =>
|
||||
$checked
|
||||
? "rgba(125, 167, 255, 0.5)"
|
||||
: "rgba(148, 163, 184, 0.34)"};
|
||||
$checked ? "rgba(167, 243, 208, 0.5)" : "rgba(148, 163, 184, 0.34)"};
|
||||
background: ${({ $checked }) =>
|
||||
$checked ? "rgba(244, 248, 255, 1)" : "rgba(255, 255, 255, 0.98)"};
|
||||
$checked ? "rgba(220, 252, 231, 1)" : "rgba(255, 255, 255, 0.98)"};
|
||||
color: hsl(var(--foreground));
|
||||
transform: translateY(-1px);
|
||||
}
|
||||
|
||||
&:focus-visible {
|
||||
outline: none;
|
||||
border-color: rgba(125, 167, 255, 0.44);
|
||||
border-color: rgba(167, 243, 208, 0.44);
|
||||
box-shadow:
|
||||
0 0 0 3px rgba(191, 219, 254, 0.28),
|
||||
0 0 0 3px rgba(167, 243, 208, 0.28),
|
||||
inset 0 1px 0 rgba(255, 255, 255, 0.92);
|
||||
}
|
||||
|
||||
@@ -352,11 +347,9 @@ export const MetaToggleCheck = styled.span<{
|
||||
border-radius: 4px;
|
||||
border: 1px solid
|
||||
${({ $checked }) =>
|
||||
$checked
|
||||
? "rgba(59, 130, 246, 0.45)"
|
||||
: "rgba(148, 163, 184, 0.42)"};
|
||||
$checked ? "rgba(34, 197, 94, 0.45)" : "rgba(148, 163, 184, 0.42)"};
|
||||
background: ${({ $checked }) =>
|
||||
$checked ? "rgba(219, 234, 254, 0.9)" : "rgba(255, 255, 255, 0.96)"};
|
||||
$checked ? "rgba(220, 252, 231, 0.9)" : "rgba(255, 255, 255, 0.96)"};
|
||||
box-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.88);
|
||||
flex-shrink: 0;
|
||||
|
||||
@@ -429,9 +422,9 @@ export const ToolButton = styled.button`
|
||||
}
|
||||
|
||||
&.active {
|
||||
color: #0f172a;
|
||||
border-color: rgba(125, 167, 255, 0.44);
|
||||
background: rgba(224, 236, 255, 0.82);
|
||||
color: #14532d;
|
||||
border-color: rgba(167, 243, 208, 0.44);
|
||||
background: rgba(220, 252, 231, 0.82);
|
||||
}
|
||||
|
||||
span {
|
||||
@@ -464,13 +457,13 @@ export const InputIconButton = styled.button<{
|
||||
: "rgba(148, 163, 184, 0.28)"};
|
||||
background: ${({ $primary, $destructive }) =>
|
||||
$primary
|
||||
? "#0f172a"
|
||||
? "#1aa34e"
|
||||
: $destructive
|
||||
? "rgba(255, 226, 234, 0.92)"
|
||||
: "rgba(255, 255, 255, 0.9)"};
|
||||
color: ${({ $primary, $destructive }) =>
|
||||
$primary
|
||||
? "#f8fafc"
|
||||
? "#f0fdf4"
|
||||
: $destructive
|
||||
? "#be123c"
|
||||
: "hsl(var(--muted-foreground))"};
|
||||
@@ -485,7 +478,7 @@ export const InputIconButton = styled.button<{
|
||||
transform: translateY(-1px);
|
||||
background: ${({ $primary, $destructive }) =>
|
||||
$primary
|
||||
? "#111c31"
|
||||
? "#168840"
|
||||
: $destructive
|
||||
? "rgba(255, 221, 229, 1)"
|
||||
: "rgba(255, 255, 255, 1)"};
|
||||
@@ -498,21 +491,21 @@ export const InputIconButton = styled.button<{
|
||||
}
|
||||
|
||||
&.is-active {
|
||||
border-color: rgba(125, 167, 255, 0.44);
|
||||
background: rgba(224, 236, 255, 0.82);
|
||||
color: #0f172a;
|
||||
border-color: rgba(167, 243, 208, 0.44);
|
||||
background: rgba(220, 252, 231, 0.82);
|
||||
color: #14532d;
|
||||
}
|
||||
|
||||
&.is-recording {
|
||||
border-color: rgba(120, 235, 190, 0.6);
|
||||
background: rgba(120, 235, 190, 0.12);
|
||||
color: #0f172a;
|
||||
color: #14532d;
|
||||
}
|
||||
|
||||
&.is-processing {
|
||||
border-color: rgba(160, 200, 255, 0.6);
|
||||
background: rgba(160, 200, 255, 0.12);
|
||||
color: #0f172a;
|
||||
border-color: rgba(167, 243, 208, 0.6);
|
||||
background: rgba(167, 243, 208, 0.12);
|
||||
color: #14532d;
|
||||
}
|
||||
|
||||
&:disabled {
|
||||
|
||||
@@ -145,8 +145,8 @@ function renderHarness(
|
||||
{
|
||||
isStreaming: nextIsStreaming = isStreaming,
|
||||
collapseCodeBlocks: nextCollapseCodeBlocks = collapseCodeBlocks,
|
||||
shouldCollapseCodeBlock: nextShouldCollapseCodeBlock =
|
||||
shouldCollapseCodeBlock,
|
||||
shouldCollapseCodeBlock:
|
||||
nextShouldCollapseCodeBlock = shouldCollapseCodeBlock,
|
||||
showBlockActions: nextShowBlockActions = showBlockActions,
|
||||
onQuoteContent: nextOnQuoteContent = onQuoteContent,
|
||||
}: RenderOptions = {},
|
||||
@@ -409,11 +409,7 @@ describe("MarkdownRenderer", () => {
|
||||
});
|
||||
|
||||
it("代码块高亮应关闭 textShadow 与字体连字,避免中英混排发虚", () => {
|
||||
const content = [
|
||||
"```typescript",
|
||||
"const answer = 42;",
|
||||
"```",
|
||||
].join("\n");
|
||||
const content = ["```typescript", "const answer = 42;", "```"].join("\n");
|
||||
|
||||
const container = render(content);
|
||||
const syntaxHighlighter = container.querySelector(
|
||||
|
||||
@@ -179,8 +179,8 @@ const MarkdownContainer = styled.div`
|
||||
|
||||
th,
|
||||
td {
|
||||
border-right: 1px solid hsl(var(--border));
|
||||
border-bottom: 1px solid hsl(var(--border));
|
||||
border-right: 1px solid rgba(167, 243, 208, 0.42);
|
||||
border-bottom: 1px solid rgba(167, 243, 208, 0.42);
|
||||
padding: 0.45rem 0.65rem;
|
||||
vertical-align: top;
|
||||
text-align: left;
|
||||
@@ -190,10 +190,10 @@ const MarkdownContainer = styled.div`
|
||||
font-weight: 600;
|
||||
background: linear-gradient(
|
||||
180deg,
|
||||
hsl(var(--muted) / 0.78) 0%,
|
||||
hsl(var(--muted) / 0.96) 100%
|
||||
rgba(220, 252, 231, 0.45) 0%,
|
||||
rgba(220, 252, 231, 0.8) 100%
|
||||
);
|
||||
color: hsl(var(--foreground));
|
||||
color: #14532d;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
@@ -206,7 +206,7 @@ const MarkdownContainer = styled.div`
|
||||
}
|
||||
|
||||
tbody tr:nth-child(even) td {
|
||||
background: hsl(var(--muted) / 0.22);
|
||||
background: rgba(220, 252, 231, 0.15);
|
||||
}
|
||||
|
||||
a {
|
||||
@@ -245,14 +245,14 @@ const MarkdownDivider = styled.hr`
|
||||
const MarkdownQuoteCard = styled.blockquote`
|
||||
margin: 0 0 0.95em;
|
||||
padding: 0;
|
||||
border: 1px solid hsl(var(--border));
|
||||
border: 1px solid rgba(167, 243, 208, 0.5);
|
||||
border-radius: 20px;
|
||||
background: linear-gradient(
|
||||
180deg,
|
||||
hsl(var(--background)) 0%,
|
||||
hsl(var(--muted) / 0.58) 100%
|
||||
rgba(220, 252, 231, 0.3) 100%
|
||||
);
|
||||
box-shadow: 0 14px 34px -30px rgba(15, 23, 42, 0.25);
|
||||
box-shadow: 0 14px 34px -30px rgba(15, 23, 42, 0.18);
|
||||
overflow: hidden;
|
||||
`;
|
||||
|
||||
@@ -432,7 +432,7 @@ const MarkdownBlockActionButton = styled.button`
|
||||
const MarkdownTableScroll = styled.div`
|
||||
margin: 0 0 0.82em;
|
||||
overflow-x: auto;
|
||||
border: 1px solid hsl(var(--border));
|
||||
border: 1px solid rgba(167, 243, 208, 0.6);
|
||||
border-radius: 14px;
|
||||
background: hsl(var(--background));
|
||||
box-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.7);
|
||||
@@ -515,11 +515,16 @@ function resolveCodePresentationMode(
|
||||
.split("\n")
|
||||
.map((line) => line.trim())
|
||||
.filter(Boolean);
|
||||
const arrowRows = lines.filter((line) => FLOW_ARROW_ONLY_PATTERN.test(line)).length;
|
||||
const arrowRows = lines.filter((line) =>
|
||||
FLOW_ARROW_ONLY_PATTERN.test(line),
|
||||
).length;
|
||||
const bulletRows = lines.filter((line) => /^[-*]\s+/.test(line)).length;
|
||||
const hasCodeSignals = CODE_SIGNAL_PATTERN.test(trimmed);
|
||||
|
||||
if ((PLAIN_TEXT_LANGUAGES.has(normalizedLanguage) || !hasCodeSignals) && arrowRows >= 2) {
|
||||
if (
|
||||
(PLAIN_TEXT_LANGUAGES.has(normalizedLanguage) || !hasCodeSignals) &&
|
||||
arrowRows >= 2
|
||||
) {
|
||||
return "flow";
|
||||
}
|
||||
|
||||
@@ -583,23 +588,34 @@ export const MarkdownRenderer: React.FC<MarkdownRendererProps> = memo(
|
||||
};
|
||||
}, []);
|
||||
|
||||
const handleCopy = React.useCallback(async (copyKey: string, value: string) => {
|
||||
try {
|
||||
await navigator.clipboard.writeText(value);
|
||||
setCopied(copyKey);
|
||||
if (copyTimeoutRef.current !== null) {
|
||||
window.clearTimeout(copyTimeoutRef.current);
|
||||
const handleCopy = React.useCallback(
|
||||
async (copyKey: string, value: string) => {
|
||||
try {
|
||||
await navigator.clipboard.writeText(value);
|
||||
setCopied(copyKey);
|
||||
if (copyTimeoutRef.current !== null) {
|
||||
window.clearTimeout(copyTimeoutRef.current);
|
||||
}
|
||||
copyTimeoutRef.current = window.setTimeout(
|
||||
() => setCopied(null),
|
||||
1200,
|
||||
);
|
||||
} catch {
|
||||
// 剪贴板在受限上下文里可能不可用,这里保持静默降级。
|
||||
}
|
||||
copyTimeoutRef.current = window.setTimeout(() => setCopied(null), 1200);
|
||||
} catch {
|
||||
// 剪贴板在受限上下文里可能不可用,这里保持静默降级。
|
||||
}
|
||||
}, []);
|
||||
},
|
||||
[],
|
||||
);
|
||||
|
||||
const getSelectedMarkdownText = React.useCallback(() => {
|
||||
const block = blockRef.current;
|
||||
const selection = window.getSelection();
|
||||
if (!block || !selection || selection.rangeCount === 0 || selection.isCollapsed) {
|
||||
if (
|
||||
!block ||
|
||||
!selection ||
|
||||
selection.rangeCount === 0 ||
|
||||
selection.isCollapsed
|
||||
) {
|
||||
return null;
|
||||
}
|
||||
|
||||
@@ -647,14 +663,18 @@ export const MarkdownRenderer: React.FC<MarkdownRendererProps> = memo(
|
||||
const selectedText =
|
||||
getSelectedMarkdownText() ?? selectionSnapshotRef.current ?? undefined;
|
||||
selectionSnapshotRef.current = null;
|
||||
onQuoteContent(selectedText?.trim().length ? selectedText : normalizedContent);
|
||||
onQuoteContent(
|
||||
selectedText?.trim().length ? selectedText : normalizedContent,
|
||||
);
|
||||
}, [getSelectedMarkdownText, normalizedContent, onQuoteContent]);
|
||||
|
||||
const handleCopyContent = React.useCallback(async () => {
|
||||
const selectedText =
|
||||
getSelectedMarkdownText() ?? selectionSnapshotRef.current ?? undefined;
|
||||
selectionSnapshotRef.current = null;
|
||||
const copyValue = selectedText?.trim().length ? selectedText : normalizedContent;
|
||||
const copyValue = selectedText?.trim().length
|
||||
? selectedText
|
||||
: normalizedContent;
|
||||
if (!copyValue) {
|
||||
return;
|
||||
}
|
||||
@@ -880,11 +900,7 @@ export const MarkdownRenderer: React.FC<MarkdownRendererProps> = memo(
|
||||
aria-label="复制内容区块"
|
||||
title={isContentCopied ? "已复制" : "复制内容区块"}
|
||||
>
|
||||
{isContentCopied ? (
|
||||
<Check size={14} />
|
||||
) : (
|
||||
<Copy size={14} />
|
||||
)}
|
||||
{isContentCopied ? <Check size={14} /> : <Copy size={14} />}
|
||||
</MarkdownBlockActionButton>
|
||||
</MarkdownBlockActions>
|
||||
) : null}
|
||||
@@ -1042,7 +1058,9 @@ export const MarkdownRenderer: React.FC<MarkdownRendererProps> = memo(
|
||||
},
|
||||
code({ inline, className, children, ...props }: any) {
|
||||
const content = String(
|
||||
Array.isArray(children) ? children.join("") : children || "",
|
||||
Array.isArray(children)
|
||||
? children.join("")
|
||||
: children || "",
|
||||
);
|
||||
const isInlineCode =
|
||||
typeof inline === "boolean"
|
||||
|
||||
@@ -1630,7 +1630,7 @@ describe("MessageList", () => {
|
||||
id: "artifact-hidden-task-json",
|
||||
type: "code",
|
||||
title: "task-image-1.json",
|
||||
content: "{\"status\":\"running\"}",
|
||||
content: '{"status":"running"}',
|
||||
status: "complete",
|
||||
meta: {
|
||||
filePath: ".lime/tasks/image_generate/task-image-1.json",
|
||||
|
||||
@@ -100,7 +100,10 @@ interface MessageListProps {
|
||||
/** Artifact 点击回调 */
|
||||
onArtifactClick?: (artifact: Artifact) => void;
|
||||
/** 打开消息结果预览 */
|
||||
onOpenMessagePreview?: (target: MessagePreviewTarget, message: Message) => void;
|
||||
onOpenMessagePreview?: (
|
||||
target: MessagePreviewTarget,
|
||||
message: Message,
|
||||
) => void;
|
||||
/** 打开子代理会话 */
|
||||
onOpenSubagentSession?: (sessionId: string) => void;
|
||||
/** 权限确认响应回调 */
|
||||
@@ -697,7 +700,9 @@ const MessageListInner: React.FC<MessageListProps> = ({
|
||||
const visibleArtifacts =
|
||||
artifacts?.filter(
|
||||
(artifact) =>
|
||||
!isHiddenInternalArtifactPath(resolveArtifactProtocolFilePath(artifact)),
|
||||
!isHiddenInternalArtifactPath(
|
||||
resolveArtifactProtocolFilePath(artifact),
|
||||
),
|
||||
) || [];
|
||||
if (visibleArtifacts.length === 0) {
|
||||
return null;
|
||||
@@ -787,55 +792,53 @@ const MessageListInner: React.FC<MessageListProps> = ({
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
) : isTaskCenterEmptyState ? (
|
||||
<div className="flex min-h-[22rem] items-center justify-center py-6">
|
||||
<section
|
||||
data-testid="message-list-empty-task-center"
|
||||
className="w-full max-w-[640px] rounded-[28px] border border-slate-200/80 bg-white px-6 py-6 text-left shadow-sm shadow-slate-950/5 md:px-7 md:py-7"
|
||||
>
|
||||
<div className="mb-4 flex items-center gap-3">
|
||||
<div className="flex h-12 w-12 items-center justify-center rounded-[18px] border border-slate-200/80 bg-slate-50/80">
|
||||
<img
|
||||
src={logoImg}
|
||||
alt="Lime"
|
||||
className="h-7 w-7 opacity-80"
|
||||
/>
|
||||
</div>
|
||||
<span className="inline-flex items-center rounded-full border border-slate-200/80 bg-slate-50 px-3 py-1 text-xs font-medium text-slate-600">
|
||||
任务中心
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<div className="space-y-2">
|
||||
<p className="text-[26px] font-semibold tracking-tight text-slate-900">
|
||||
回到进行中的任务和最近工作现场
|
||||
</p>
|
||||
<p className="max-w-[46rem] text-sm leading-7 text-slate-600">
|
||||
这里会承接最近会话、进行中的任务和刚恢复的工作内容。要开始一个全新目标,随时回到“新建任务”。
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div className="mt-5 flex flex-wrap gap-2">
|
||||
<span className="inline-flex items-center rounded-full border border-slate-200/80 bg-white px-3 py-1.5 text-xs text-slate-500">
|
||||
左侧会继续显示最近任务
|
||||
</span>
|
||||
<span className="inline-flex items-center rounded-full border border-slate-200/80 bg-white px-3 py-1.5 text-xs text-slate-500">
|
||||
恢复中的会话会自动回到这里
|
||||
</span>
|
||||
</div>
|
||||
</section>
|
||||
</div>
|
||||
) : (
|
||||
isTaskCenterEmptyState ? (
|
||||
<div className="flex min-h-[22rem] items-center justify-center py-6">
|
||||
<section
|
||||
data-testid="message-list-empty-task-center"
|
||||
className="w-full max-w-[640px] rounded-[28px] border border-slate-200/80 bg-white px-6 py-6 text-left shadow-sm shadow-slate-950/5 md:px-7 md:py-7"
|
||||
>
|
||||
<div className="mb-4 flex items-center gap-3">
|
||||
<div className="flex h-12 w-12 items-center justify-center rounded-[18px] border border-slate-200/80 bg-slate-50/80">
|
||||
<img
|
||||
src={logoImg}
|
||||
alt="Lime"
|
||||
className="h-7 w-7 opacity-80"
|
||||
/>
|
||||
</div>
|
||||
<span className="inline-flex items-center rounded-full border border-slate-200/80 bg-slate-50 px-3 py-1 text-xs font-medium text-slate-600">
|
||||
任务中心
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<div className="space-y-2">
|
||||
<p className="text-[26px] font-semibold tracking-tight text-slate-900">
|
||||
回到进行中的任务和最近工作现场
|
||||
</p>
|
||||
<p className="max-w-[46rem] text-sm leading-7 text-slate-600">
|
||||
这里会承接最近会话、进行中的任务和刚恢复的工作内容。要开始一个全新目标,随时回到“新建任务”。
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div className="mt-5 flex flex-wrap gap-2">
|
||||
<span className="inline-flex items-center rounded-full border border-slate-200/80 bg-white px-3 py-1.5 text-xs text-slate-500">
|
||||
左侧会继续显示最近任务
|
||||
</span>
|
||||
<span className="inline-flex items-center rounded-full border border-slate-200/80 bg-white px-3 py-1.5 text-xs text-slate-500">
|
||||
恢复中的会话会自动回到这里
|
||||
</span>
|
||||
</div>
|
||||
</section>
|
||||
</div>
|
||||
) : (
|
||||
<div className="flex flex-col items-center justify-center h-64 text-muted-foreground opacity-50">
|
||||
<img
|
||||
src={logoImg}
|
||||
alt="Lime"
|
||||
className="w-12 h-12 mb-4 opacity-20"
|
||||
/>
|
||||
<p className="text-lg font-medium">开始一段新的对话吧</p>
|
||||
</div>
|
||||
)
|
||||
<div className="flex flex-col items-center justify-center h-64 text-muted-foreground opacity-50">
|
||||
<img
|
||||
src={logoImg}
|
||||
alt="Lime"
|
||||
className="w-12 h-12 mb-4 opacity-20"
|
||||
/>
|
||||
<p className="text-lg font-medium">开始一段新的对话吧</p>
|
||||
</div>
|
||||
))}
|
||||
|
||||
{messageGroups.map((group, groupIndex) => {
|
||||
|
||||
@@ -144,14 +144,12 @@ export function RuntimeReviewDecisionDialog({
|
||||
}
|
||||
}, [open, template]);
|
||||
|
||||
const statusOptions =
|
||||
template?.decision_status_options.length
|
||||
? template.decision_status_options
|
||||
: DEFAULT_STATUS_OPTIONS;
|
||||
const riskLevelOptions =
|
||||
template?.risk_level_options.length
|
||||
? template.risk_level_options
|
||||
: DEFAULT_RISK_LEVEL_OPTIONS;
|
||||
const statusOptions = template?.decision_status_options.length
|
||||
? template.decision_status_options
|
||||
: DEFAULT_STATUS_OPTIONS;
|
||||
const riskLevelOptions = template?.risk_level_options.length
|
||||
? template.risk_level_options
|
||||
: DEFAULT_RISK_LEVEL_OPTIONS;
|
||||
|
||||
const handleSave = async () => {
|
||||
if (!template || !formState) {
|
||||
|
||||
@@ -6,7 +6,11 @@ import {
|
||||
Globe,
|
||||
Search,
|
||||
} from "lucide-react";
|
||||
import { Popover, PopoverContent, PopoverTrigger } from "@/components/ui/popover";
|
||||
import {
|
||||
Popover,
|
||||
PopoverContent,
|
||||
PopoverTrigger,
|
||||
} from "@/components/ui/popover";
|
||||
import { cn } from "@/lib/utils";
|
||||
import type { SearchResultPreviewItem } from "../utils/searchResultPreview";
|
||||
|
||||
@@ -226,7 +230,10 @@ export function SearchResultPreviewList({
|
||||
aria-label={expanded ? "收起搜索结果" : "展开搜索结果"}
|
||||
>
|
||||
<ChevronDown
|
||||
className={cn("h-3.5 w-3.5 transition-transform", expanded && "rotate-180")}
|
||||
className={cn(
|
||||
"h-3.5 w-3.5 transition-transform",
|
||||
expanded && "rotate-180",
|
||||
)}
|
||||
/>
|
||||
<span>
|
||||
{expanded ? "收起结果" : `展开其余 ${hiddenCount} 条结果`}
|
||||
|
||||
@@ -31,7 +31,9 @@ afterEach(() => {
|
||||
}
|
||||
});
|
||||
|
||||
function renderCard(snapshot?: Parameters<typeof TeamMemoryShadowCard>[0]["snapshot"]) {
|
||||
function renderCard(
|
||||
snapshot?: Parameters<typeof TeamMemoryShadowCard>[0]["snapshot"],
|
||||
) {
|
||||
const container = document.createElement("div");
|
||||
document.body.appendChild(container);
|
||||
const root = createRoot(container);
|
||||
@@ -79,12 +81,16 @@ describe("TeamMemoryShadowCard", () => {
|
||||
container.querySelector('[data-testid="team-memory-shadow-card"]'),
|
||||
).not.toBeNull();
|
||||
expect(container.textContent).toContain("协作记忆影子");
|
||||
expect(container.textContent).toContain("当前仓库已缓存 3 条 Team 续接上下文");
|
||||
expect(container.textContent).toContain(
|
||||
"当前仓库已缓存 3 条 Team 续接上下文",
|
||||
);
|
||||
expect(container.textContent).toContain("/workspace/lime");
|
||||
expect(container.textContent).toContain("当前 Team");
|
||||
expect(container.textContent).toContain("子代理概览");
|
||||
expect(container.textContent).toContain("父会话上下文");
|
||||
expect(container.textContent).toContain("研究代理 [running] explorer · 梳理主线风险");
|
||||
expect(container.textContent).toContain(
|
||||
"研究代理 [running] explorer · 梳理主线风险",
|
||||
);
|
||||
expect(container.textContent).toContain("当前任务:汇总结论");
|
||||
});
|
||||
});
|
||||
|
||||
@@ -89,7 +89,8 @@ export function TeamMemoryShadowCard({
|
||||
当前仓库已缓存 {entries.length} 条 Team 续接上下文
|
||||
</div>
|
||||
<p className="mt-1 text-xs leading-5 text-slate-500">
|
||||
仅保存在当前仓库本地作用域,用来续接 Team 选择、子代理概览和父会话上下文。
|
||||
仅保存在当前仓库本地作用域,用来续接 Team
|
||||
选择、子代理概览和父会话上下文。
|
||||
</p>
|
||||
</div>
|
||||
<div className="flex flex-wrap items-center gap-2">
|
||||
|
||||
@@ -115,27 +115,26 @@ export function TeamWorkbenchSummaryPanel({
|
||||
.slice(0, 4);
|
||||
const displayRoleCount = hasRealTeamGraph
|
||||
? executionSummary.totalSessionCount
|
||||
: dispatchPreviewState?.members.length ?? 0;
|
||||
const roleCards =
|
||||
dispatchPreviewState?.members.length
|
||||
? dispatchPreviewState.members.map((member) => ({
|
||||
id: member.id,
|
||||
label: member.label,
|
||||
roleKey: member.roleKey,
|
||||
profileId: member.profileId,
|
||||
summary: member.summary,
|
||||
skillIds: member.skillIds,
|
||||
statusMeta: resolveRuntimeMemberStatusMeta(member.status),
|
||||
}))
|
||||
: (selectedTeamRoles ?? []).map((role) => ({
|
||||
id: role.id,
|
||||
label: role.label,
|
||||
roleKey: role.roleKey,
|
||||
profileId: role.profileId,
|
||||
summary: role.summary,
|
||||
skillIds: role.skillIds ?? [],
|
||||
statusMeta: null,
|
||||
}));
|
||||
: (dispatchPreviewState?.members.length ?? 0);
|
||||
const roleCards = dispatchPreviewState?.members.length
|
||||
? dispatchPreviewState.members.map((member) => ({
|
||||
id: member.id,
|
||||
label: member.label,
|
||||
roleKey: member.roleKey,
|
||||
profileId: member.profileId,
|
||||
summary: member.summary,
|
||||
skillIds: member.skillIds,
|
||||
statusMeta: resolveRuntimeMemberStatusMeta(member.status),
|
||||
}))
|
||||
: (selectedTeamRoles ?? []).map((role) => ({
|
||||
id: role.id,
|
||||
label: role.label,
|
||||
roleKey: role.roleKey,
|
||||
profileId: role.profileId,
|
||||
summary: role.summary,
|
||||
skillIds: role.skillIds ?? [],
|
||||
statusMeta: null,
|
||||
}));
|
||||
const summaryCards = [
|
||||
{
|
||||
label: "活跃 Agent",
|
||||
@@ -159,7 +158,9 @@ export function TeamWorkbenchSummaryPanel({
|
||||
? "当前成员"
|
||||
: "总会话",
|
||||
value: String(
|
||||
hasRealTeamGraph ? executionSummary.totalSessionCount : displayRoleCount,
|
||||
hasRealTeamGraph
|
||||
? executionSummary.totalSessionCount
|
||||
: displayRoleCount,
|
||||
),
|
||||
hint: hasRealTeamGraph
|
||||
? "已进入团队图谱"
|
||||
@@ -201,7 +202,7 @@ export function TeamWorkbenchSummaryPanel({
|
||||
? dispatchPreviewState.errorMessage?.trim() ||
|
||||
"这次 Team 准备失败,可继续在当前对话中推进。"
|
||||
: runtimeSummaryText ||
|
||||
"这里展示团队总览与运行密度;主对话只保留调度记录,角色执行正文在左侧 Team 画布查看。"}
|
||||
"这里展示团队总览与运行密度;主对话只保留调度记录,角色执行正文在左侧 Team 画布查看。"}
|
||||
</p>
|
||||
{!hasRealTeamGraph && dispatchPreviewState ? (
|
||||
<div
|
||||
@@ -221,7 +222,8 @@ export function TeamWorkbenchSummaryPanel({
|
||||
) : null}
|
||||
{!hasRealTeamGraph && !dispatchPreviewState ? (
|
||||
<div className="mt-3 rounded-2xl border border-sky-100 bg-sky-50 px-3 py-2 text-xs leading-5 text-sky-700">
|
||||
尚未出现真实团队成员。开始分派成员后,左侧 Team 画布会自动切换为实时协作视图。
|
||||
尚未出现真实团队成员。开始分派成员后,左侧 Team
|
||||
画布会自动切换为实时协作视图。
|
||||
</div>
|
||||
) : null}
|
||||
</section>
|
||||
|
||||
@@ -10,10 +10,9 @@ const { mockGetAgentRuntimeSession } = vi.hoisted(() => ({
|
||||
}));
|
||||
|
||||
vi.mock("@/lib/api/agentRuntime", async () => {
|
||||
const actual =
|
||||
await vi.importActual<typeof import("@/lib/api/agentRuntime")>(
|
||||
"@/lib/api/agentRuntime",
|
||||
);
|
||||
const actual = await vi.importActual<typeof import("@/lib/api/agentRuntime")>(
|
||||
"@/lib/api/agentRuntime",
|
||||
);
|
||||
return {
|
||||
...actual,
|
||||
getAgentRuntimeSession: mockGetAgentRuntimeSession,
|
||||
@@ -34,12 +33,7 @@ vi.mock("@/components/ui/button", () => ({
|
||||
type?: "button" | "submit" | "reset";
|
||||
[key: string]: unknown;
|
||||
}) => (
|
||||
<button
|
||||
type={type}
|
||||
className={className}
|
||||
onClick={onClick}
|
||||
{...props}
|
||||
>
|
||||
<button type={type} className={className} onClick={onClick} {...props}>
|
||||
{children}
|
||||
</button>
|
||||
),
|
||||
@@ -413,7 +407,9 @@ describe("TeamWorkspaceBoard", () => {
|
||||
expect(container.textContent).toContain("仓库探索");
|
||||
expect(container.textContent).toContain("最近进展 处理中");
|
||||
expect(
|
||||
container.querySelector('[data-testid="team-workspace-member-detail-child-1"]'),
|
||||
container.querySelector(
|
||||
'[data-testid="team-workspace-member-detail-child-1"]',
|
||||
),
|
||||
).toBeFalsy();
|
||||
expect(
|
||||
container
|
||||
@@ -453,7 +449,9 @@ describe("TeamWorkspaceBoard", () => {
|
||||
await expandLane(container, "child-1");
|
||||
|
||||
expect(
|
||||
container.querySelector('[data-testid="team-workspace-member-detail-child-1"]'),
|
||||
container.querySelector(
|
||||
'[data-testid="team-workspace-member-detail-child-1"]',
|
||||
),
|
||||
).toBeTruthy();
|
||||
expect(
|
||||
container
|
||||
@@ -651,7 +649,9 @@ describe("TeamWorkspaceBoard", () => {
|
||||
expect(container.textContent).toContain("检索代理");
|
||||
expect(container.textContent).toContain("等待中 1");
|
||||
expect(
|
||||
container.querySelector('[data-testid="team-workspace-member-detail-child-current"]'),
|
||||
container.querySelector(
|
||||
'[data-testid="team-workspace-member-detail-child-current"]',
|
||||
),
|
||||
).toBeFalsy();
|
||||
expect(
|
||||
container.querySelector('[data-testid="team-workspace-canvas-toolbar"]')
|
||||
@@ -765,10 +765,14 @@ describe("TeamWorkspaceBoard", () => {
|
||||
expect(boardBody?.className).toContain("overflow-y-auto");
|
||||
expect(railList?.getAttribute("data-layout-kind")).toBe("free-canvas");
|
||||
expect(
|
||||
container.querySelector('[data-testid="team-workspace-inspector-overlay"]'),
|
||||
container.querySelector(
|
||||
'[data-testid="team-workspace-inspector-overlay"]',
|
||||
),
|
||||
).toBeNull();
|
||||
expect(
|
||||
container.querySelector('[data-testid="team-workspace-member-detail-child-1"]'),
|
||||
container.querySelector(
|
||||
'[data-testid="team-workspace-member-detail-child-1"]',
|
||||
),
|
||||
).toBeNull();
|
||||
expect(
|
||||
container
|
||||
@@ -812,45 +816,63 @@ describe("TeamWorkspaceBoard", () => {
|
||||
});
|
||||
|
||||
expect(
|
||||
container.querySelector('[data-testid="team-workspace-member-detail-child-collapse-1"]'),
|
||||
container.querySelector(
|
||||
'[data-testid="team-workspace-member-detail-child-collapse-1"]',
|
||||
),
|
||||
).toBeFalsy();
|
||||
expect(
|
||||
container.querySelector('[data-testid="team-workspace-member-detail-child-collapse-2"]'),
|
||||
container.querySelector(
|
||||
'[data-testid="team-workspace-member-detail-child-collapse-2"]',
|
||||
),
|
||||
).toBeFalsy();
|
||||
expect(
|
||||
container
|
||||
.querySelector('[data-testid="team-workspace-member-lane-child-collapse-1"]')
|
||||
.querySelector(
|
||||
'[data-testid="team-workspace-member-lane-child-collapse-1"]',
|
||||
)
|
||||
?.getAttribute("data-expanded"),
|
||||
).toBe("false");
|
||||
expect(
|
||||
container
|
||||
.querySelector('[data-testid="team-workspace-member-lane-child-collapse-2"]')
|
||||
.querySelector(
|
||||
'[data-testid="team-workspace-member-lane-child-collapse-2"]',
|
||||
)
|
||||
?.getAttribute("data-expanded"),
|
||||
).toBe("false");
|
||||
|
||||
await expandLane(container, "child-collapse-1");
|
||||
|
||||
expect(
|
||||
container.querySelector('[data-testid="team-workspace-member-detail-child-collapse-1"]'),
|
||||
container.querySelector(
|
||||
'[data-testid="team-workspace-member-detail-child-collapse-1"]',
|
||||
),
|
||||
).toBeTruthy();
|
||||
expect(
|
||||
container
|
||||
.querySelector('[data-testid="team-workspace-member-lane-child-collapse-1"]')
|
||||
.querySelector(
|
||||
'[data-testid="team-workspace-member-lane-child-collapse-1"]',
|
||||
)
|
||||
?.getAttribute("data-expanded"),
|
||||
).toBe("true");
|
||||
expect(
|
||||
container
|
||||
.querySelector('[data-testid="team-workspace-member-lane-child-collapse-2"]')
|
||||
.querySelector(
|
||||
'[data-testid="team-workspace-member-lane-child-collapse-2"]',
|
||||
)
|
||||
?.getAttribute("data-expanded"),
|
||||
).toBe("false");
|
||||
|
||||
await expandLane(container, "child-collapse-2");
|
||||
|
||||
expect(
|
||||
container.querySelector('[data-testid="team-workspace-member-detail-child-collapse-1"]'),
|
||||
container.querySelector(
|
||||
'[data-testid="team-workspace-member-detail-child-collapse-1"]',
|
||||
),
|
||||
).toBeFalsy();
|
||||
expect(
|
||||
container.querySelector('[data-testid="team-workspace-member-detail-child-collapse-2"]'),
|
||||
container.querySelector(
|
||||
'[data-testid="team-workspace-member-detail-child-collapse-2"]',
|
||||
),
|
||||
).toBeTruthy();
|
||||
});
|
||||
|
||||
@@ -889,8 +911,7 @@ describe("TeamWorkspaceBoard", () => {
|
||||
title: "状态切换",
|
||||
detail: "收到 team 状态事件,已切换为运行中。",
|
||||
statusLabel: "运行中",
|
||||
badgeClassName:
|
||||
"border border-sky-200 bg-sky-50 text-sky-700",
|
||||
badgeClassName: "border border-sky-200 bg-sky-50 text-sky-700",
|
||||
},
|
||||
],
|
||||
},
|
||||
@@ -910,7 +931,9 @@ describe("TeamWorkspaceBoard", () => {
|
||||
).toContain("处理中");
|
||||
expect(container.textContent).toContain("最近进展 处理中");
|
||||
expect(
|
||||
container.querySelector('[data-testid="team-workspace-member-detail-child-live-1"]'),
|
||||
container.querySelector(
|
||||
'[data-testid="team-workspace-member-detail-child-live-1"]',
|
||||
),
|
||||
).toBeFalsy();
|
||||
|
||||
await expandLane(container, "child-live-1");
|
||||
@@ -1428,22 +1451,23 @@ describe("TeamWorkspaceBoard", () => {
|
||||
});
|
||||
|
||||
it("同一会话重新挂载后,应恢复上次保存的 lane 布局", async () => {
|
||||
const boardProps: Partial<React.ComponentProps<typeof TeamWorkspaceBoard>> = {
|
||||
currentSessionId: "parent-persist-1",
|
||||
childSubagentSessions: [
|
||||
{
|
||||
id: "child-persist-1",
|
||||
name: "持久化代理",
|
||||
created_at: 1_710_000_000,
|
||||
updated_at: 1_710_000_100,
|
||||
session_type: "sub_agent",
|
||||
runtime_status: "running",
|
||||
latest_turn_status: "running",
|
||||
task_summary: "验证会话级画布持久化",
|
||||
role_hint: "reviewer",
|
||||
},
|
||||
],
|
||||
};
|
||||
const boardProps: Partial<React.ComponentProps<typeof TeamWorkspaceBoard>> =
|
||||
{
|
||||
currentSessionId: "parent-persist-1",
|
||||
childSubagentSessions: [
|
||||
{
|
||||
id: "child-persist-1",
|
||||
name: "持久化代理",
|
||||
created_at: 1_710_000_000,
|
||||
updated_at: 1_710_000_100,
|
||||
session_type: "sub_agent",
|
||||
runtime_status: "running",
|
||||
latest_turn_status: "running",
|
||||
task_summary: "验证会话级画布持久化",
|
||||
role_hint: "reviewer",
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
const firstContainer = await renderBoard(boardProps);
|
||||
const firstHeader = firstContainer.querySelector(
|
||||
@@ -1490,7 +1514,9 @@ describe("TeamWorkspaceBoard", () => {
|
||||
await expandLane(container, "child-close-1");
|
||||
|
||||
expect(
|
||||
container.querySelector('[data-testid="team-workspace-member-detail-child-close-1"]'),
|
||||
container.querySelector(
|
||||
'[data-testid="team-workspace-member-detail-child-close-1"]',
|
||||
),
|
||||
).toBeTruthy();
|
||||
|
||||
const closeButton = Array.from(container.querySelectorAll("button")).find(
|
||||
@@ -1532,7 +1558,9 @@ describe("TeamWorkspaceBoard", () => {
|
||||
);
|
||||
expect(resumeButton).toBeTruthy();
|
||||
expect(
|
||||
container.querySelector('[data-testid="team-workspace-member-detail-child-resume-1"]'),
|
||||
container.querySelector(
|
||||
'[data-testid="team-workspace-member-detail-child-resume-1"]',
|
||||
),
|
||||
).toBeTruthy();
|
||||
|
||||
await act(async () => {
|
||||
@@ -1768,9 +1796,7 @@ describe("TeamWorkspaceBoard", () => {
|
||||
|
||||
expect(operations).toBeTruthy();
|
||||
expect(operations?.textContent).toContain("暂停处理");
|
||||
expect(operations?.textContent).toContain(
|
||||
"刚才已暂停 2 位协作成员的处理",
|
||||
);
|
||||
expect(operations?.textContent).toContain("刚才已暂停 2 位协作成员的处理");
|
||||
});
|
||||
|
||||
it("点击 Team 轨迹项时应切换焦点到对应 agent", async () => {
|
||||
@@ -1973,7 +1999,9 @@ describe("TeamWorkspaceBoard", () => {
|
||||
});
|
||||
|
||||
await act(async () => {
|
||||
interruptButton?.dispatchEvent(new MouseEvent("click", { bubbles: true }));
|
||||
interruptButton?.dispatchEvent(
|
||||
new MouseEvent("click", { bubbles: true }),
|
||||
);
|
||||
await Promise.resolve();
|
||||
});
|
||||
|
||||
@@ -2081,7 +2109,9 @@ describe("TeamWorkspaceBoard", () => {
|
||||
});
|
||||
|
||||
expect(
|
||||
container.querySelector('[data-testid="team-workspace-runtime-formation"]'),
|
||||
container.querySelector(
|
||||
'[data-testid="team-workspace-runtime-formation"]',
|
||||
),
|
||||
).toBeTruthy();
|
||||
expect(
|
||||
container.querySelector('[data-testid="team-workspace-runtime-members"]'),
|
||||
|
||||
@@ -106,9 +106,7 @@ interface TeamWorkspaceBoardProps {
|
||||
sessionIds: string[],
|
||||
timeoutMs?: number,
|
||||
) => void | Promise<void>;
|
||||
onCloseCompletedTeamSessions?: (
|
||||
sessionIds: string[],
|
||||
) => void | Promise<void>;
|
||||
onCloseCompletedTeamSessions?: (sessionIds: string[]) => void | Promise<void>;
|
||||
onCloseSubagentSession?: (sessionId: string) => void | Promise<void>;
|
||||
onResumeSubagentSession?: (sessionId: string) => void | Promise<void>;
|
||||
onOpenSubagentSession?: (sessionId: string) => void | Promise<void>;
|
||||
@@ -279,8 +277,8 @@ function canStartCanvasPanGesture(
|
||||
}
|
||||
|
||||
return (
|
||||
target.closest('[data-team-workspace-canvas-pan-surface="true"]') !== null ||
|
||||
target === currentTarget
|
||||
target.closest('[data-team-workspace-canvas-pan-surface="true"]') !==
|
||||
null || target === currentTarget
|
||||
);
|
||||
}
|
||||
|
||||
@@ -349,7 +347,8 @@ function resolveActivityEntryStatusMeta(
|
||||
case "completed":
|
||||
return {
|
||||
label: "完成",
|
||||
badgeClassName: "border border-emerald-200 bg-emerald-50 text-emerald-700",
|
||||
badgeClassName:
|
||||
"border border-emerald-200 bg-emerald-50 text-emerald-700",
|
||||
};
|
||||
default:
|
||||
return {
|
||||
@@ -483,7 +482,9 @@ function isEditableKeyboardTarget(target: EventTarget | null): boolean {
|
||||
);
|
||||
}
|
||||
|
||||
function buildRuntimeDetailSummary(session?: TeamSessionCard | null): string | null {
|
||||
function buildRuntimeDetailSummary(
|
||||
session?: TeamSessionCard | null,
|
||||
): string | null {
|
||||
if (!session) {
|
||||
return null;
|
||||
}
|
||||
@@ -500,9 +501,14 @@ function buildRuntimeDetailSummary(session?: TeamSessionCard | null): string | n
|
||||
session.teamActiveCount !== undefined &&
|
||||
session.teamParallelBudget !== undefined
|
||||
) {
|
||||
parts.push(`处理中 ${session.teamActiveCount}/${session.teamParallelBudget}`);
|
||||
parts.push(
|
||||
`处理中 ${session.teamActiveCount}/${session.teamParallelBudget}`,
|
||||
);
|
||||
}
|
||||
if (session.providerParallelBudget === 1 && session.providerConcurrencyGroup) {
|
||||
if (
|
||||
session.providerParallelBudget === 1 &&
|
||||
session.providerConcurrencyGroup
|
||||
) {
|
||||
parts.push(resolveTeamWorkspaceStableProcessingLabel());
|
||||
}
|
||||
|
||||
@@ -512,8 +518,8 @@ function buildRuntimeDetailSummary(session?: TeamSessionCard | null): string | n
|
||||
function isWaitableTeamSession(session?: TeamSessionCard | null) {
|
||||
return Boolean(
|
||||
session &&
|
||||
session.sessionType !== "user" &&
|
||||
!isTeamWorkspaceTerminalStatus(session.runtimeStatus),
|
||||
session.sessionType !== "user" &&
|
||||
!isTeamWorkspaceTerminalStatus(session.runtimeStatus),
|
||||
);
|
||||
}
|
||||
|
||||
@@ -540,7 +546,8 @@ function buildTeamWaitSummaryDisplay(
|
||||
}
|
||||
|
||||
const resolvedName = summary.resolvedSessionId
|
||||
? sessionNameById.get(summary.resolvedSessionId) ?? summary.resolvedSessionId
|
||||
? (sessionNameById.get(summary.resolvedSessionId) ??
|
||||
summary.resolvedSessionId)
|
||||
: "成员";
|
||||
const normalizedStatus = summary.resolvedStatus
|
||||
? normalizeTeamWorkspaceRuntimeStatus(summary.resolvedStatus)
|
||||
@@ -563,7 +570,7 @@ function buildTeamControlSummaryDisplay(
|
||||
const affectedCount = summary.affectedSessionIds.length;
|
||||
const firstAffectedId = summary.affectedSessionIds[0];
|
||||
const firstAffectedName = firstAffectedId
|
||||
? sessionNameById.get(firstAffectedId) ?? firstAffectedId
|
||||
? (sessionNameById.get(firstAffectedId) ?? firstAffectedId)
|
||||
: "成员";
|
||||
|
||||
switch (summary.action) {
|
||||
@@ -765,7 +772,10 @@ function resolveSessionBlueprintRoleId(
|
||||
) {
|
||||
score += 8;
|
||||
}
|
||||
if (sessionRoleKey && normalizeComparableText(role.roleKey) === sessionRoleKey) {
|
||||
if (
|
||||
sessionRoleKey &&
|
||||
normalizeComparableText(role.roleKey) === sessionRoleKey
|
||||
) {
|
||||
score += 4;
|
||||
}
|
||||
if (
|
||||
@@ -788,10 +798,7 @@ function resolveSessionBlueprintRoleId(
|
||||
if (candidates.length === 0) {
|
||||
return null;
|
||||
}
|
||||
if (
|
||||
candidates.length > 1 &&
|
||||
candidates[0]?.score === candidates[1]?.score
|
||||
) {
|
||||
if (candidates.length > 1 && candidates[0]?.score === candidates[1]?.score) {
|
||||
return null;
|
||||
}
|
||||
|
||||
@@ -809,7 +816,7 @@ function orderSessionsByRuntimeRoles(
|
||||
const runtimeRoles = (
|
||||
teamDispatchPreviewState.members.length > 0
|
||||
? teamDispatchPreviewState.members
|
||||
: teamDispatchPreviewState.blueprint?.roles ?? []
|
||||
: (teamDispatchPreviewState.blueprint?.roles ?? [])
|
||||
).map((role) => ({
|
||||
id: role.id,
|
||||
label: role.label,
|
||||
@@ -821,7 +828,9 @@ function orderSessionsByRuntimeRoles(
|
||||
return sessions;
|
||||
}
|
||||
|
||||
const roleOrder = new Map(runtimeRoles.map((role, index) => [role.id, index]));
|
||||
const roleOrder = new Map(
|
||||
runtimeRoles.map((role, index) => [role.id, index]),
|
||||
);
|
||||
const usedRoleIds = new Set<string>();
|
||||
|
||||
return [...sessions]
|
||||
@@ -973,7 +982,10 @@ function buildSessionLaneEmptyState(params: {
|
||||
return "这部分已经完成,结果会继续汇入当前内容。";
|
||||
}
|
||||
|
||||
if (session?.runtimeStatus === "failed" || session?.runtimeStatus === "aborted") {
|
||||
if (
|
||||
session?.runtimeStatus === "failed" ||
|
||||
session?.runtimeStatus === "aborted"
|
||||
) {
|
||||
return "这一步没有顺利完成,你可以在下方查看细节并决定是否继续。";
|
||||
}
|
||||
|
||||
@@ -1052,7 +1064,10 @@ function resolveCanvasLanePreferredSize(params: {
|
||||
params.viewportWidth,
|
||||
);
|
||||
const gapX = TEAM_WORKSPACE_CANVAS_AUTO_LAYOUT_GAP_X;
|
||||
const safeViewportWidth = Math.max(params.viewportWidth, columns >= 3 ? 1180 : 980);
|
||||
const safeViewportWidth = Math.max(
|
||||
params.viewportWidth,
|
||||
columns >= 3 ? 1180 : 980,
|
||||
);
|
||||
const usableWidth =
|
||||
safeViewportWidth -
|
||||
TEAM_WORKSPACE_CANVAS_VIEWPORT_PADDING * 2 -
|
||||
@@ -1060,7 +1075,11 @@ function resolveCanvasLanePreferredSize(params: {
|
||||
const rawWidth = Math.floor(usableWidth / columns);
|
||||
const width =
|
||||
params.laneKind === "session"
|
||||
? clampCanvasNumber(rawWidth, 340, columns === 1 ? 560 : columns === 2 ? 460 : 390)
|
||||
? clampCanvasNumber(
|
||||
rawWidth,
|
||||
340,
|
||||
columns === 1 ? 560 : columns === 2 ? 460 : 390,
|
||||
)
|
||||
: clampCanvasNumber(rawWidth - 20, 320, columns === 1 ? 520 : 380);
|
||||
const height =
|
||||
params.laneKind === "session"
|
||||
@@ -1083,8 +1102,7 @@ function buildCanvasStageHint(params: {
|
||||
hasRuntimeFormation,
|
||||
hasSelectedTeamPlan,
|
||||
teamDispatchPreviewState,
|
||||
} =
|
||||
params;
|
||||
} = params;
|
||||
|
||||
if (hasRealTeamGraph) {
|
||||
return "拖动画布空白处可平移,滚轮配合 Ctrl/Cmd 可缩放,拖动成员卡片可调整布局。";
|
||||
@@ -1166,7 +1184,10 @@ function resolveLaneMatchingRuntimeMemberId(
|
||||
}
|
||||
|
||||
const explicitRoleId = session.blueprintRoleId?.trim();
|
||||
if (explicitRoleId && runtimeMembers.some((member) => member.id === explicitRoleId)) {
|
||||
if (
|
||||
explicitRoleId &&
|
||||
runtimeMembers.some((member) => member.id === explicitRoleId)
|
||||
) {
|
||||
return explicitRoleId;
|
||||
}
|
||||
|
||||
@@ -1181,10 +1202,16 @@ function resolveLaneMatchingRuntimeMemberId(
|
||||
const candidates = runtimeMembers
|
||||
.map((member) => {
|
||||
let score = 0;
|
||||
if (normalizedRoleLabel && normalizeComparableText(member.label) === normalizedRoleLabel) {
|
||||
if (
|
||||
normalizedRoleLabel &&
|
||||
normalizeComparableText(member.label) === normalizedRoleLabel
|
||||
) {
|
||||
score += 8;
|
||||
}
|
||||
if (normalizedRoleKey && normalizeComparableText(member.roleKey) === normalizedRoleKey) {
|
||||
if (
|
||||
normalizedRoleKey &&
|
||||
normalizeComparableText(member.roleKey) === normalizedRoleKey
|
||||
) {
|
||||
score += 5;
|
||||
}
|
||||
if (
|
||||
@@ -1204,10 +1231,7 @@ function resolveLaneMatchingRuntimeMemberId(
|
||||
if (candidates.length === 0) {
|
||||
return null;
|
||||
}
|
||||
if (
|
||||
candidates.length > 1 &&
|
||||
candidates[0]?.score === candidates[1]?.score
|
||||
) {
|
||||
if (candidates.length > 1 && candidates[0]?.score === candidates[1]?.score) {
|
||||
return null;
|
||||
}
|
||||
return candidates[0]?.memberId ?? null;
|
||||
@@ -1232,10 +1256,16 @@ function resolveLaneMatchingPlannedRoleId(
|
||||
const candidates = plannedRoles
|
||||
.map((role) => {
|
||||
let score = 0;
|
||||
if (normalizedRoleLabel && normalizeComparableText(role.label) === normalizedRoleLabel) {
|
||||
if (
|
||||
normalizedRoleLabel &&
|
||||
normalizeComparableText(role.label) === normalizedRoleLabel
|
||||
) {
|
||||
score += 8;
|
||||
}
|
||||
if (normalizedRoleKey && normalizeComparableText(role.roleKey) === normalizedRoleKey) {
|
||||
if (
|
||||
normalizedRoleKey &&
|
||||
normalizeComparableText(role.roleKey) === normalizedRoleKey
|
||||
) {
|
||||
score += 5;
|
||||
}
|
||||
if (
|
||||
@@ -1255,10 +1285,7 @@ function resolveLaneMatchingPlannedRoleId(
|
||||
if (candidates.length === 0) {
|
||||
return null;
|
||||
}
|
||||
if (
|
||||
candidates.length > 1 &&
|
||||
candidates[0]?.score === candidates[1]?.score
|
||||
) {
|
||||
if (candidates.length > 1 && candidates[0]?.score === candidates[1]?.score) {
|
||||
return null;
|
||||
}
|
||||
return candidates[0]?.roleId ?? null;
|
||||
@@ -1279,10 +1306,16 @@ function resolveRuntimeMemberMatchingPlannedRoleId(
|
||||
const candidates = plannedRoles
|
||||
.map((role) => {
|
||||
let score = 0;
|
||||
if (normalizedRoleLabel && normalizeComparableText(role.label) === normalizedRoleLabel) {
|
||||
if (
|
||||
normalizedRoleLabel &&
|
||||
normalizeComparableText(role.label) === normalizedRoleLabel
|
||||
) {
|
||||
score += 8;
|
||||
}
|
||||
if (normalizedRoleKey && normalizeComparableText(role.roleKey) === normalizedRoleKey) {
|
||||
if (
|
||||
normalizedRoleKey &&
|
||||
normalizeComparableText(role.roleKey) === normalizedRoleKey
|
||||
) {
|
||||
score += 5;
|
||||
}
|
||||
if (
|
||||
@@ -1302,10 +1335,7 @@ function resolveRuntimeMemberMatchingPlannedRoleId(
|
||||
if (candidates.length === 0) {
|
||||
return null;
|
||||
}
|
||||
if (
|
||||
candidates.length > 1 &&
|
||||
candidates[0]?.score === candidates[1]?.score
|
||||
) {
|
||||
if (candidates.length > 1 && candidates[0]?.score === candidates[1]?.score) {
|
||||
return null;
|
||||
}
|
||||
return candidates[0]?.roleId ?? null;
|
||||
@@ -1403,12 +1433,12 @@ export function TeamWorkspaceBoard({
|
||||
"team-workspace";
|
||||
const [shellExpanded, setShellExpanded] = useState(defaultShellExpanded);
|
||||
const detailExpanded = !embedded;
|
||||
const canvasViewportFallbackHeight =
|
||||
embedded && !detailExpanded ? 720 : 560;
|
||||
const canvasViewportFallbackHeight = embedded && !detailExpanded ? 720 : 560;
|
||||
const [canvasLayoutState, setCanvasLayoutState] =
|
||||
useState<TeamWorkspaceCanvasLayoutState>(() =>
|
||||
loadTeamWorkspaceCanvasLayout(canvasStorageScopeId) ??
|
||||
createDefaultTeamWorkspaceCanvasLayoutState(),
|
||||
useState<TeamWorkspaceCanvasLayoutState>(
|
||||
() =>
|
||||
loadTeamWorkspaceCanvasLayout(canvasStorageScopeId) ??
|
||||
createDefaultTeamWorkspaceCanvasLayoutState(),
|
||||
);
|
||||
const [pendingSessionAction, setPendingSessionAction] = useState<{
|
||||
sessionId: string;
|
||||
@@ -1438,12 +1468,11 @@ export function TeamWorkspaceBoard({
|
||||
width: 960,
|
||||
height: embedded ? 720 : 560,
|
||||
});
|
||||
const canvasLayoutStateRef = useRef<TeamWorkspaceCanvasLayoutState>(
|
||||
canvasLayoutState,
|
||||
);
|
||||
const canvasLaneLayoutsRef = useRef<Record<string, TeamWorkspaceCanvasItemLayout>>(
|
||||
{},
|
||||
);
|
||||
const canvasLayoutStateRef =
|
||||
useRef<TeamWorkspaceCanvasLayoutState>(canvasLayoutState);
|
||||
const canvasLaneLayoutsRef = useRef<
|
||||
Record<string, TeamWorkspaceCanvasItemLayout>
|
||||
>({});
|
||||
const canvasInteractionCleanupRef = useRef<(() => void) | null>(null);
|
||||
|
||||
useEffect(() => {
|
||||
@@ -1602,10 +1631,8 @@ export function TeamWorkspaceBoard({
|
||||
() =>
|
||||
baseVisibleSessions.map(
|
||||
(session) =>
|
||||
applyLiveRuntimeState(
|
||||
session,
|
||||
liveRuntimeBySessionId[session.id],
|
||||
) ?? session,
|
||||
applyLiveRuntimeState(session, liveRuntimeBySessionId[session.id]) ??
|
||||
session,
|
||||
),
|
||||
[baseVisibleSessions, liveRuntimeBySessionId],
|
||||
);
|
||||
@@ -1871,12 +1898,14 @@ export function TeamWorkspaceBoard({
|
||||
const [selectedSessionId, setSelectedSessionId] = useState<string | null>(
|
||||
() => railSessions[0]?.id ?? null,
|
||||
);
|
||||
const [expandedSessionId, setExpandedSessionId] = useState<string | null>(null);
|
||||
const [expandedSessionId, setExpandedSessionId] = useState<string | null>(
|
||||
null,
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
const defaultSelectedId = isChildSession
|
||||
? currentSessionId ?? railSessions[0]?.id ?? null
|
||||
: memberCanvasSessions[0]?.id ?? railSessions[0]?.id ?? null;
|
||||
? (currentSessionId ?? railSessions[0]?.id ?? null)
|
||||
: (memberCanvasSessions[0]?.id ?? railSessions[0]?.id ?? null);
|
||||
|
||||
if (!selectedSessionId) {
|
||||
setSelectedSessionId(defaultSelectedId);
|
||||
@@ -1909,7 +1938,9 @@ export function TeamWorkspaceBoard({
|
||||
return;
|
||||
}
|
||||
|
||||
if (!memberCanvasSessions.some((session) => session.id === expandedSessionId)) {
|
||||
if (
|
||||
!memberCanvasSessions.some((session) => session.id === expandedSessionId)
|
||||
) {
|
||||
setExpandedSessionId(null);
|
||||
}
|
||||
}, [expandedSessionId, memberCanvasSessions]);
|
||||
@@ -1929,7 +1960,7 @@ export function TeamWorkspaceBoard({
|
||||
[baseRailSessions, selectedSessionId],
|
||||
);
|
||||
const selectedSessionActivityPreview = selectedSession
|
||||
? sessionActivityPreviewById[selectedSession.id] ?? null
|
||||
? (sessionActivityPreviewById[selectedSession.id] ?? null)
|
||||
: null;
|
||||
const selectedSessionActivityEntries = selectedSession
|
||||
? mergeSessionActivityEntries(
|
||||
@@ -1946,15 +1977,15 @@ export function TeamWorkspaceBoard({
|
||||
selectedSession && selectedSession.sessionType !== "user",
|
||||
);
|
||||
const selectedSessionActivityId = selectedSessionSupportsActivityPreview
|
||||
? selectedSession?.id ?? null
|
||||
? (selectedSession?.id ?? null)
|
||||
: null;
|
||||
const selectedSessionActivityFingerprint = selectedSessionSupportsActivityPreview
|
||||
? buildSessionActivityFingerprint(selectedBaseSession)
|
||||
: null;
|
||||
const selectedSessionActivityRefreshVersion =
|
||||
selectedSessionActivityId
|
||||
? (activityRefreshVersionBySessionId[selectedSessionActivityId] ?? 0)
|
||||
: 0;
|
||||
const selectedSessionActivityFingerprint =
|
||||
selectedSessionSupportsActivityPreview
|
||||
? buildSessionActivityFingerprint(selectedBaseSession)
|
||||
: null;
|
||||
const selectedSessionActivityRefreshVersion = selectedSessionActivityId
|
||||
? (activityRefreshVersionBySessionId[selectedSessionActivityId] ?? 0)
|
||||
: 0;
|
||||
const selectedSessionActivityShouldPoll =
|
||||
selectedSessionSupportsActivityPreview &&
|
||||
shouldPollSessionActivity(selectedSession);
|
||||
@@ -2073,7 +2104,8 @@ export function TeamWorkspaceBoard({
|
||||
!selectedSessionActivityShouldPoll &&
|
||||
cachedPreview?.status === "ready" &&
|
||||
cachedPreview.fingerprint === fingerprint &&
|
||||
(cachedPreview.refreshVersion ?? 0) === selectedSessionActivityRefreshVersion
|
||||
(cachedPreview.refreshVersion ?? 0) ===
|
||||
selectedSessionActivityRefreshVersion
|
||||
) {
|
||||
return;
|
||||
}
|
||||
@@ -2086,7 +2118,8 @@ export function TeamWorkspaceBoard({
|
||||
selectedSessionActivityRefreshVersion,
|
||||
{
|
||||
force:
|
||||
(current?.refreshVersion ?? 0) < selectedSessionActivityRefreshVersion,
|
||||
(current?.refreshVersion ?? 0) <
|
||||
selectedSessionActivityRefreshVersion,
|
||||
},
|
||||
);
|
||||
};
|
||||
@@ -2139,7 +2172,8 @@ export function TeamWorkspaceBoard({
|
||||
|
||||
const refreshVersion =
|
||||
activityRefreshVersionBySessionId[session.id] ?? 0;
|
||||
const cachedPreview = sessionActivityPreviewByIdRef.current[session.id];
|
||||
const cachedPreview =
|
||||
sessionActivityPreviewByIdRef.current[session.id];
|
||||
return syncSessionActivityPreview(
|
||||
session.id,
|
||||
buildSessionActivityFingerprint(session),
|
||||
@@ -2187,7 +2221,8 @@ export function TeamWorkspaceBoard({
|
||||
normalizedSelectedTeamRoles,
|
||||
);
|
||||
const presetLabel = session.teamPresetId
|
||||
? getTeamPresetOption(session.teamPresetId)?.label ?? session.teamPresetId
|
||||
? (getTeamPresetOption(session.teamPresetId)?.label ??
|
||||
session.teamPresetId)
|
||||
: undefined;
|
||||
|
||||
return {
|
||||
@@ -2219,7 +2254,8 @@ export function TeamWorkspaceBoard({
|
||||
.map((skill) => buildSkillDisplayName(skill)),
|
||||
session,
|
||||
previewText:
|
||||
cardActivityPreview || buildSessionLaneEmptyState({ session, previewState }),
|
||||
cardActivityPreview ||
|
||||
buildSessionLaneEmptyState({ session, previewState }),
|
||||
previewEntries: mergedEntries.slice(0, 3),
|
||||
};
|
||||
}),
|
||||
@@ -2232,70 +2268,64 @@ export function TeamWorkspaceBoard({
|
||||
],
|
||||
);
|
||||
|
||||
const canvasBlueprintLanes = useMemo<TeamWorkspaceCanvasLane[]>(
|
||||
() => {
|
||||
if (hasRealTeamGraph) {
|
||||
return [];
|
||||
}
|
||||
const canvasBlueprintLanes = useMemo<TeamWorkspaceCanvasLane[]>(() => {
|
||||
if (hasRealTeamGraph) {
|
||||
return [];
|
||||
}
|
||||
|
||||
if (runtimeMembers.length > 0) {
|
||||
return runtimeMembers.map((member) => {
|
||||
const laneSummary = buildCanvasLaneTitleSummary(member);
|
||||
const matchedPlannedRoleId = resolveRuntimeMemberMatchingPlannedRoleId(
|
||||
member,
|
||||
normalizedSelectedTeamRoles,
|
||||
);
|
||||
return {
|
||||
id: member.id,
|
||||
persistKey: `runtime:${member.id}`,
|
||||
fallbackPersistKeys: matchedPlannedRoleId
|
||||
? [`planned:${matchedPlannedRoleId}`]
|
||||
: [],
|
||||
kind: "runtime" as const,
|
||||
title: member.label,
|
||||
summary: laneSummary.summary,
|
||||
badgeLabel: laneSummary.badgeLabel,
|
||||
badgeClassName: laneSummary.badgeClassName,
|
||||
dotClassName: laneSummary.dotClassName,
|
||||
roleLabel: resolveTeamWorkspaceRoleHintLabel(member.roleKey) || undefined,
|
||||
profileLabel: undefined,
|
||||
statusHint: laneSummary.statusHint,
|
||||
updatedAtLabel: "等待成员加入",
|
||||
skillLabels: [],
|
||||
previewText: member.summary,
|
||||
previewEntries: [],
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
return normalizedSelectedTeamRoles.map((role) => {
|
||||
const laneSummary = buildPlannedRoleLaneSummary(role);
|
||||
if (runtimeMembers.length > 0) {
|
||||
return runtimeMembers.map((member) => {
|
||||
const laneSummary = buildCanvasLaneTitleSummary(member);
|
||||
const matchedPlannedRoleId = resolveRuntimeMemberMatchingPlannedRoleId(
|
||||
member,
|
||||
normalizedSelectedTeamRoles,
|
||||
);
|
||||
return {
|
||||
id: role.id,
|
||||
persistKey: `planned:${role.id}`,
|
||||
fallbackPersistKeys: [],
|
||||
kind: "planned" as const,
|
||||
title: role.label,
|
||||
id: member.id,
|
||||
persistKey: `runtime:${member.id}`,
|
||||
fallbackPersistKeys: matchedPlannedRoleId
|
||||
? [`planned:${matchedPlannedRoleId}`]
|
||||
: [],
|
||||
kind: "runtime" as const,
|
||||
title: member.label,
|
||||
summary: laneSummary.summary,
|
||||
badgeLabel: laneSummary.badgeLabel,
|
||||
badgeClassName: laneSummary.badgeClassName,
|
||||
dotClassName: laneSummary.dotClassName,
|
||||
roleLabel: resolveTeamWorkspaceRoleHintLabel(role.roleKey) || undefined,
|
||||
roleLabel:
|
||||
resolveTeamWorkspaceRoleHintLabel(member.roleKey) || undefined,
|
||||
profileLabel: undefined,
|
||||
statusHint: laneSummary.statusHint,
|
||||
updatedAtLabel: "计划分工",
|
||||
updatedAtLabel: "等待成员加入",
|
||||
skillLabels: [],
|
||||
previewText: role.summary,
|
||||
previewText: member.summary,
|
||||
previewEntries: [],
|
||||
};
|
||||
});
|
||||
},
|
||||
[
|
||||
hasRealTeamGraph,
|
||||
normalizedSelectedTeamRoles,
|
||||
runtimeMembers,
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
return normalizedSelectedTeamRoles.map((role) => {
|
||||
const laneSummary = buildPlannedRoleLaneSummary(role);
|
||||
return {
|
||||
id: role.id,
|
||||
persistKey: `planned:${role.id}`,
|
||||
fallbackPersistKeys: [],
|
||||
kind: "planned" as const,
|
||||
title: role.label,
|
||||
summary: laneSummary.summary,
|
||||
badgeLabel: laneSummary.badgeLabel,
|
||||
badgeClassName: laneSummary.badgeClassName,
|
||||
dotClassName: laneSummary.dotClassName,
|
||||
roleLabel: resolveTeamWorkspaceRoleHintLabel(role.roleKey) || undefined,
|
||||
profileLabel: undefined,
|
||||
statusHint: laneSummary.statusHint,
|
||||
updatedAtLabel: "计划分工",
|
||||
skillLabels: [],
|
||||
previewText: role.summary,
|
||||
previewEntries: [],
|
||||
};
|
||||
});
|
||||
}, [hasRealTeamGraph, normalizedSelectedTeamRoles, runtimeMembers]);
|
||||
|
||||
const canvasLanes = hasRealTeamGraph
|
||||
? canvasSessionLanes
|
||||
@@ -2406,10 +2436,7 @@ export function TeamWorkspaceBoard({
|
||||
}, []);
|
||||
|
||||
const bindCanvasMouseInteraction = useCallback(
|
||||
(
|
||||
onMove: (event: MouseEvent) => void,
|
||||
onEnd?: () => void,
|
||||
) => {
|
||||
(onMove: (event: MouseEvent) => void, onEnd?: () => void) => {
|
||||
canvasInteractionCleanupRef.current?.();
|
||||
|
||||
const handleMouseMove = (event: MouseEvent) => {
|
||||
@@ -2581,7 +2608,11 @@ export function TeamWorkspaceBoard({
|
||||
}));
|
||||
});
|
||||
},
|
||||
[bindCanvasMouseInteraction, isCanvasPanModifierActive, updateCanvasViewport],
|
||||
[
|
||||
bindCanvasMouseInteraction,
|
||||
isCanvasPanModifierActive,
|
||||
updateCanvasViewport,
|
||||
],
|
||||
);
|
||||
|
||||
const handleStartCanvasLaneDrag = useCallback(
|
||||
@@ -2610,7 +2641,11 @@ export function TeamWorkspaceBoard({
|
||||
}));
|
||||
});
|
||||
},
|
||||
[bindCanvasMouseInteraction, bringCanvasLaneToFront, updateCanvasLaneLayout],
|
||||
[
|
||||
bindCanvasMouseInteraction,
|
||||
bringCanvasLaneToFront,
|
||||
updateCanvasLaneLayout,
|
||||
],
|
||||
);
|
||||
|
||||
const handleStartCanvasLaneResize = useCallback(
|
||||
@@ -2680,7 +2715,11 @@ export function TeamWorkspaceBoard({
|
||||
});
|
||||
});
|
||||
},
|
||||
[bindCanvasMouseInteraction, bringCanvasLaneToFront, updateCanvasLaneLayout],
|
||||
[
|
||||
bindCanvasMouseInteraction,
|
||||
bringCanvasLaneToFront,
|
||||
updateCanvasLaneLayout,
|
||||
],
|
||||
);
|
||||
|
||||
const handleCanvasWheel = useCallback(
|
||||
@@ -2720,7 +2759,9 @@ export function TeamWorkspaceBoard({
|
||||
}, [updateCanvasViewport]);
|
||||
|
||||
const handleResetCanvasView = useCallback(() => {
|
||||
updateCanvasViewport(() => createDefaultTeamWorkspaceCanvasLayoutState().viewport);
|
||||
updateCanvasViewport(
|
||||
() => createDefaultTeamWorkspaceCanvasLayoutState().viewport,
|
||||
);
|
||||
}, [updateCanvasViewport]);
|
||||
const handleAutoArrangeCanvas = useCallback(() => {
|
||||
if (canvasLanes.length === 0) {
|
||||
@@ -2985,8 +3026,10 @@ export function TeamWorkspaceBoard({
|
||||
return null;
|
||||
}
|
||||
|
||||
return [...teamControlSummary.requestedSessionIds, ...teamControlSummary.affectedSessionIds]
|
||||
.some((sessionId) => railSessionNameById.has(sessionId))
|
||||
return [
|
||||
...teamControlSummary.requestedSessionIds,
|
||||
...teamControlSummary.affectedSessionIds,
|
||||
].some((sessionId) => railSessionNameById.has(sessionId))
|
||||
? teamControlSummary
|
||||
: null;
|
||||
}, [railSessionNameById, teamControlSummary]);
|
||||
@@ -2998,7 +3041,8 @@ export function TeamWorkspaceBoard({
|
||||
teamControlSummary: visibleTeamControlSummary,
|
||||
}).filter(
|
||||
(entry) =>
|
||||
!entry.targetSessionId || railSessions.some((session) => session.id === entry.targetSessionId),
|
||||
!entry.targetSessionId ||
|
||||
railSessions.some((session) => session.id === entry.targetSessionId),
|
||||
),
|
||||
[
|
||||
railSessionNameById,
|
||||
@@ -3024,38 +3068,40 @@ export function TeamWorkspaceBoard({
|
||||
);
|
||||
const canOpenSelectedSession = Boolean(
|
||||
selectedSession &&
|
||||
onOpenSubagentSession &&
|
||||
selectedSession.id !== currentSessionId,
|
||||
onOpenSubagentSession &&
|
||||
selectedSession.id !== currentSessionId,
|
||||
);
|
||||
const canWaitSelectedSession = Boolean(
|
||||
selectedSession && isWaitableTeamSession(selectedSession) && onWaitSubagentSession,
|
||||
selectedSession &&
|
||||
isWaitableTeamSession(selectedSession) &&
|
||||
onWaitSubagentSession,
|
||||
);
|
||||
const canSendSelectedSessionInput = Boolean(
|
||||
selectedSession &&
|
||||
selectedSession.sessionType !== "user" &&
|
||||
selectedSession.runtimeStatus !== "closed" &&
|
||||
onSendSubagentInput &&
|
||||
selectedSession.id !== currentSessionId,
|
||||
selectedSession.sessionType !== "user" &&
|
||||
selectedSession.runtimeStatus !== "closed" &&
|
||||
onSendSubagentInput &&
|
||||
selectedSession.id !== currentSessionId,
|
||||
);
|
||||
const canStopSelectedSession = Boolean(
|
||||
selectedSession &&
|
||||
selectedSession.sessionType !== "user" &&
|
||||
isTeamWorkspaceActiveStatus(
|
||||
selectedSession.runtimeStatus ?? selectedSession.latestTurnStatus,
|
||||
) &&
|
||||
onCloseSubagentSession,
|
||||
selectedSession.sessionType !== "user" &&
|
||||
isTeamWorkspaceActiveStatus(
|
||||
selectedSession.runtimeStatus ?? selectedSession.latestTurnStatus,
|
||||
) &&
|
||||
onCloseSubagentSession,
|
||||
);
|
||||
const canResumeSelectedSession = Boolean(
|
||||
selectedSession &&
|
||||
selectedSession.sessionType !== "user" &&
|
||||
selectedSession.runtimeStatus === "closed" &&
|
||||
onResumeSubagentSession,
|
||||
selectedSession.sessionType !== "user" &&
|
||||
selectedSession.runtimeStatus === "closed" &&
|
||||
onResumeSubagentSession,
|
||||
);
|
||||
const selectedActionPending = Boolean(
|
||||
selectedSession && pendingSessionAction?.sessionId === selectedSession.id,
|
||||
);
|
||||
const selectedSessionInputDraft = selectedSession
|
||||
? sessionInputDraftById[selectedSession.id] ?? ""
|
||||
? (sessionInputDraftById[selectedSession.id] ?? "")
|
||||
: "";
|
||||
const selectedSessionInputMessage = selectedSessionInputDraft.trim();
|
||||
const handleWaitAnyActiveTeamSessions = useCallback(async () => {
|
||||
@@ -3090,7 +3136,9 @@ export function TeamWorkspaceBoard({
|
||||
if (!entry.targetSessionId) {
|
||||
return;
|
||||
}
|
||||
if (!railSessions.some((session) => session.id === entry.targetSessionId)) {
|
||||
if (
|
||||
!railSessions.some((session) => session.id === entry.targetSessionId)
|
||||
) {
|
||||
return;
|
||||
}
|
||||
setSelectedSessionId(entry.targetSessionId);
|
||||
@@ -3282,7 +3330,9 @@ export function TeamWorkspaceBoard({
|
||||
|
||||
const selectedStatusMeta = resolveStatusMeta(selectedSession?.runtimeStatus);
|
||||
const detailVisible =
|
||||
isEmptyShellState || !hasRealTeamGraph ? detailExpanded || shellExpanded : false;
|
||||
isEmptyShellState || !hasRealTeamGraph
|
||||
? detailExpanded || shellExpanded
|
||||
: false;
|
||||
const detailToggleLabel = detailVisible ? "收起细节" : "查看细节";
|
||||
const boardHeadline =
|
||||
!hasRealTeamGraph && runtimeFormationMeta
|
||||
@@ -3310,7 +3360,9 @@ export function TeamWorkspaceBoard({
|
||||
});
|
||||
const useCompactCanvasChrome = hasRealTeamGraph;
|
||||
const runtimeDetailSummary = buildRuntimeDetailSummary(selectedSession);
|
||||
const selectedPresetOption = getTeamPresetOption(selectedSession?.teamPresetId);
|
||||
const selectedPresetOption = getTeamPresetOption(
|
||||
selectedSession?.teamPresetId,
|
||||
);
|
||||
const selectedSkills = selectedSession?.skills ?? [];
|
||||
const selectedMetadata = [
|
||||
selectedSession?.blueprintRoleLabel
|
||||
@@ -3327,7 +3379,9 @@ export function TeamWorkspaceBoard({
|
||||
selectedSession?.createdFromTurnId
|
||||
? `来自之前的任务 ${selectedSession.createdFromTurnId}`
|
||||
: null,
|
||||
selectedSession && (selectedSession.teamQueuedCount ?? selectedSession.queuedTurnCount ?? 0) > 0
|
||||
selectedSession &&
|
||||
(selectedSession.teamQueuedCount ?? selectedSession.queuedTurnCount ?? 0) >
|
||||
0
|
||||
? `等待中 ${selectedSession.teamQueuedCount ?? selectedSession.queuedTurnCount}`
|
||||
: null,
|
||||
selectedSession?.teamActiveCount !== undefined &&
|
||||
@@ -3368,7 +3422,9 @@ export function TeamWorkspaceBoard({
|
||||
const boardBodyClassName = embedded
|
||||
? cn(
|
||||
"min-h-0 flex-1 overflow-y-auto overscroll-contain",
|
||||
useCompactCanvasChrome ? "p-3 sm:p-3.5 space-y-2.5" : "p-3 sm:p-4 space-y-3",
|
||||
useCompactCanvasChrome
|
||||
? "p-3 sm:p-3.5 space-y-2.5"
|
||||
: "p-3 sm:p-4 space-y-3",
|
||||
)
|
||||
: cn(useCompactCanvasChrome ? "p-3 sm:p-3.5" : "p-3 sm:p-4");
|
||||
const canvasStageHeight =
|
||||
@@ -3376,7 +3432,10 @@ export function TeamWorkspaceBoard({
|
||||
? "clamp(560px, 76vh, 980px)"
|
||||
: TEAM_WORKSPACE_CANVAS_STAGE_HEIGHT;
|
||||
const railCardClassName = embedded
|
||||
? cn("pointer-events-auto", useCompactCanvasChrome ? "space-y-3" : "space-y-4")
|
||||
? cn(
|
||||
"pointer-events-auto",
|
||||
useCompactCanvasChrome ? "space-y-3" : "space-y-4",
|
||||
)
|
||||
: "rounded-[22px] border border-slate-200 bg-slate-50 p-3.5 shadow-sm shadow-slate-950/5";
|
||||
const detailCardClassName = cn(
|
||||
embedded
|
||||
@@ -3523,7 +3582,8 @@ export function TeamWorkspaceBoard({
|
||||
) : null}
|
||||
{resolveTeamWorkspaceRoleHintLabel(selectedSession?.roleKey) ? (
|
||||
<span className="rounded-full border border-slate-200 bg-white px-2.5 py-1">
|
||||
分工 {resolveTeamWorkspaceRoleHintLabel(selectedSession?.roleKey)}
|
||||
分工{" "}
|
||||
{resolveTeamWorkspaceRoleHintLabel(selectedSession?.roleKey)}
|
||||
</span>
|
||||
) : null}
|
||||
{selectedSession?.theme ? (
|
||||
@@ -3602,7 +3662,9 @@ export function TeamWorkspaceBoard({
|
||||
<Button
|
||||
type="button"
|
||||
size="sm"
|
||||
disabled={selectedActionPending || !selectedSessionInputMessage}
|
||||
disabled={
|
||||
selectedActionPending || !selectedSessionInputMessage
|
||||
}
|
||||
onClick={() => void handleSelectedSessionSendInput(false)}
|
||||
>
|
||||
{selectedActionPending &&
|
||||
@@ -3618,7 +3680,9 @@ export function TeamWorkspaceBoard({
|
||||
type="button"
|
||||
size="sm"
|
||||
variant="outline"
|
||||
disabled={selectedActionPending || !selectedSessionInputMessage}
|
||||
disabled={
|
||||
selectedActionPending || !selectedSessionInputMessage
|
||||
}
|
||||
onClick={() => void handleSelectedSessionSendInput(true)}
|
||||
>
|
||||
{selectedActionPending &&
|
||||
@@ -3682,7 +3746,10 @@ export function TeamWorkspaceBoard({
|
||||
</div>
|
||||
<div className="mt-3 space-y-2.5">
|
||||
{selectedSessionActivityEntries.map((entry) => (
|
||||
<div key={entry.id} className={inlineTimelineEntryClassName}>
|
||||
<div
|
||||
key={entry.id}
|
||||
className={inlineTimelineEntryClassName}
|
||||
>
|
||||
<div className="flex flex-wrap items-center gap-2 text-[11px]">
|
||||
<span className="font-semibold text-slate-800">
|
||||
{entry.title}
|
||||
@@ -3758,16 +3825,16 @@ export function TeamWorkspaceBoard({
|
||||
来自之前的任务 {subagentParentContext.created_from_turn_id}
|
||||
</span>
|
||||
) : null}
|
||||
{!useCompactCanvasChrome && !isChildSession && totalTeamSessions > 0 ? (
|
||||
{!useCompactCanvasChrome &&
|
||||
!isChildSession &&
|
||||
totalTeamSessions > 0 ? (
|
||||
<span className="rounded-full border border-sky-200 bg-sky-50 px-2.5 py-1 text-[11px] text-sky-700">
|
||||
{totalTeamSessions} 位协作成员
|
||||
</span>
|
||||
) : null}
|
||||
</div>
|
||||
{!useCompactCanvasChrome ? (
|
||||
<p className="mt-1 text-xs leading-5 text-slate-500">
|
||||
{boardHint}
|
||||
</p>
|
||||
<p className="mt-1 text-xs leading-5 text-slate-500">{boardHint}</p>
|
||||
) : null}
|
||||
</div>
|
||||
<div className="flex flex-wrap items-center gap-2">
|
||||
@@ -3789,30 +3856,28 @@ export function TeamWorkspaceBoard({
|
||||
{detailToggleLabel}
|
||||
</Button>
|
||||
) : null}
|
||||
{isEmptyShellState
|
||||
? (
|
||||
<span className="rounded-full border border-slate-200 bg-white px-2.5 py-1 text-[11px] text-slate-500">
|
||||
{runtimeFormationMeta?.label || "还没有协作成员加入"}
|
||||
</span>
|
||||
)
|
||||
: !useCompactCanvasChrome
|
||||
? Object.entries(statusSummary)
|
||||
.filter(([, count]) => count > 0)
|
||||
.map(([status, count]) => {
|
||||
const meta = resolveStatusMeta(status as RuntimeStatus);
|
||||
return (
|
||||
<span
|
||||
key={status}
|
||||
className={cn(
|
||||
"inline-flex items-center rounded-full px-2.5 py-1 text-[11px] font-medium",
|
||||
meta.badgeClassName,
|
||||
)}
|
||||
>
|
||||
{meta.label} {count}
|
||||
</span>
|
||||
);
|
||||
})
|
||||
: null}
|
||||
{isEmptyShellState ? (
|
||||
<span className="rounded-full border border-slate-200 bg-white px-2.5 py-1 text-[11px] text-slate-500">
|
||||
{runtimeFormationMeta?.label || "还没有协作成员加入"}
|
||||
</span>
|
||||
) : !useCompactCanvasChrome ? (
|
||||
Object.entries(statusSummary)
|
||||
.filter(([, count]) => count > 0)
|
||||
.map(([status, count]) => {
|
||||
const meta = resolveStatusMeta(status as RuntimeStatus);
|
||||
return (
|
||||
<span
|
||||
key={status}
|
||||
className={cn(
|
||||
"inline-flex items-center rounded-full px-2.5 py-1 text-[11px] font-medium",
|
||||
meta.badgeClassName,
|
||||
)}
|
||||
>
|
||||
{meta.label} {count}
|
||||
</span>
|
||||
);
|
||||
})
|
||||
) : null}
|
||||
{isChildSession && onReturnToParentSession ? (
|
||||
<Button
|
||||
type="button"
|
||||
@@ -3974,8 +4039,8 @@ export function TeamWorkspaceBoard({
|
||||
</div>
|
||||
{!useCompactCanvasChrome &&
|
||||
(canWaitAnyActiveTeamSession ||
|
||||
canCloseCompletedTeamSessions ||
|
||||
teamOperationEntries.length > 0) ? (
|
||||
canCloseCompletedTeamSessions ||
|
||||
teamOperationEntries.length > 0) ? (
|
||||
<div className="mt-3 flex flex-wrap items-center gap-2">
|
||||
{canWaitAnyActiveTeamSession ? (
|
||||
<Button
|
||||
@@ -3989,9 +4054,9 @@ export function TeamWorkspaceBoard({
|
||||
{pendingTeamAction === "wait_any" ? (
|
||||
<Loader2 className="mr-1.5 h-3.5 w-3.5 animate-spin" />
|
||||
) : null}
|
||||
{pendingTeamAction === "wait_any"
|
||||
? "等待中..."
|
||||
: "等待任一成员结果"}
|
||||
{pendingTeamAction === "wait_any"
|
||||
? "等待中..."
|
||||
: "等待任一成员结果"}
|
||||
</Button>
|
||||
) : null}
|
||||
{canWaitAnyActiveTeamSession ? (
|
||||
@@ -4011,9 +4076,9 @@ export function TeamWorkspaceBoard({
|
||||
{pendingTeamAction === "close_completed" ? (
|
||||
<Loader2 className="mr-1.5 h-3.5 w-3.5 animate-spin" />
|
||||
) : null}
|
||||
{pendingTeamAction === "close_completed"
|
||||
? "收起中..."
|
||||
: "收起已完成成员"}
|
||||
{pendingTeamAction === "close_completed"
|
||||
? "收起中..."
|
||||
: "收起已完成成员"}
|
||||
</Button>
|
||||
) : null}
|
||||
{canCloseCompletedTeamSessions ? (
|
||||
@@ -4098,10 +4163,12 @@ export function TeamWorkspaceBoard({
|
||||
const content = (
|
||||
<>
|
||||
<div className="flex flex-wrap items-center gap-2">
|
||||
<span className={cn(
|
||||
"rounded-full px-2 py-0.5 text-[10px] font-medium",
|
||||
entry.badgeClassName,
|
||||
)}>
|
||||
<span
|
||||
className={cn(
|
||||
"rounded-full px-2 py-0.5 text-[10px] font-medium",
|
||||
entry.badgeClassName,
|
||||
)}
|
||||
>
|
||||
{entry.title}
|
||||
</span>
|
||||
<span className="text-[11px] text-slate-500">
|
||||
@@ -4146,7 +4213,12 @@ export function TeamWorkspaceBoard({
|
||||
)
|
||||
) : null}
|
||||
|
||||
<div className={cn("mt-3", useCompactCanvasChrome ? "space-y-2.5" : "space-y-3")}>
|
||||
<div
|
||||
className={cn(
|
||||
"mt-3",
|
||||
useCompactCanvasChrome ? "space-y-2.5" : "space-y-3",
|
||||
)}
|
||||
>
|
||||
{!useCompactCanvasChrome ? (
|
||||
<div className="flex flex-wrap items-center justify-between gap-2">
|
||||
<div className="flex flex-wrap items-center gap-2 text-[11px] text-slate-500">
|
||||
@@ -4241,7 +4313,9 @@ export function TeamWorkspaceBoard({
|
||||
<span className="rounded-full border border-slate-200 bg-slate-50 px-2 py-0.5 text-[10px] font-semibold tracking-[0.12em] text-slate-500">
|
||||
画布
|
||||
</span>
|
||||
<span className="truncate">空白处拖拽 · Space 手型 · A 整理 · F 适应</span>
|
||||
<span className="truncate">
|
||||
空白处拖拽 · Space 手型 · A 整理 · F 适应
|
||||
</span>
|
||||
</div>
|
||||
{canvasLanes.length > 0 ? (
|
||||
<div
|
||||
@@ -4370,7 +4444,10 @@ export function TeamWorkspaceBoard({
|
||||
data-testid={`team-workspace-member-lane-header-${lane.id}`}
|
||||
className="flex cursor-grab items-start justify-between gap-3 border-b border-slate-200 bg-slate-50/88 px-4 py-3 active:cursor-grabbing"
|
||||
onMouseDown={(event) =>
|
||||
handleStartCanvasLaneDrag(lane.persistKey, event)
|
||||
handleStartCanvasLaneDrag(
|
||||
lane.persistKey,
|
||||
event,
|
||||
)
|
||||
}
|
||||
>
|
||||
<div className="min-w-0 flex-1">
|
||||
@@ -4426,21 +4503,24 @@ export function TeamWorkspaceBoard({
|
||||
<p className="text-sm leading-6 text-slate-600">
|
||||
{lane.summary}
|
||||
</p>
|
||||
{(lane.skillLabels.length > 0 || lane.presetLabel) ? (
|
||||
{lane.skillLabels.length > 0 ||
|
||||
lane.presetLabel ? (
|
||||
<div className="mt-2 flex flex-wrap items-center gap-1.5 text-[11px] text-slate-500">
|
||||
{lane.presetLabel ? (
|
||||
<span className="rounded-full border border-slate-200 bg-white px-2 py-0.5">
|
||||
{lane.presetLabel}
|
||||
</span>
|
||||
) : null}
|
||||
{lane.skillLabels.slice(0, 4).map((skillLabel) => (
|
||||
<span
|
||||
key={`${lane.persistKey}-${skillLabel}`}
|
||||
className="rounded-full border border-slate-200 bg-white px-2 py-0.5"
|
||||
>
|
||||
{skillLabel}
|
||||
</span>
|
||||
))}
|
||||
{lane.skillLabels
|
||||
.slice(0, 4)
|
||||
.map((skillLabel) => (
|
||||
<span
|
||||
key={`${lane.persistKey}-${skillLabel}`}
|
||||
className="rounded-full border border-slate-200 bg-white px-2 py-0.5"
|
||||
>
|
||||
{skillLabel}
|
||||
</span>
|
||||
))}
|
||||
</div>
|
||||
) : null}
|
||||
<div className="mt-3 flex min-h-0 flex-1 flex-col overflow-hidden rounded-2xl border border-slate-200 bg-slate-50">
|
||||
@@ -4448,14 +4528,17 @@ export function TeamWorkspaceBoard({
|
||||
<PanelTop className="h-3 w-3" />
|
||||
<span>成员进展</span>
|
||||
<span className="rounded-full border border-slate-200 bg-white px-2 py-0.5 text-[10px] font-medium tracking-normal text-slate-600 normal-case">
|
||||
{lane.kind === "session" ? "最近进展" : "等待接入"}
|
||||
{lane.kind === "session"
|
||||
? "最近进展"
|
||||
: "等待接入"}
|
||||
</span>
|
||||
</div>
|
||||
<div className="min-h-0 flex-1 overflow-auto px-3 py-3">
|
||||
<p className="whitespace-pre-wrap break-words text-[12px] leading-5 text-slate-700">
|
||||
{lane.previewText}
|
||||
</p>
|
||||
{lane.previewEntries && lane.previewEntries.length > 0 ? (
|
||||
{lane.previewEntries &&
|
||||
lane.previewEntries.length > 0 ? (
|
||||
<div className="mt-3 space-y-2">
|
||||
{lane.previewEntries.map((entry) => (
|
||||
<div
|
||||
@@ -4505,7 +4588,9 @@ export function TeamWorkspaceBoard({
|
||||
) : null}
|
||||
</div>
|
||||
</div>
|
||||
{expanded ? renderSelectedSessionInlineDetail() : null}
|
||||
{expanded
|
||||
? renderSelectedSessionInlineDetail()
|
||||
: null}
|
||||
</div>
|
||||
</div>
|
||||
{resizeHandles.map((handle) => (
|
||||
|
||||
@@ -8,19 +8,15 @@ import { TeamWorkspaceDock } from "./TeamWorkspaceDock";
|
||||
const { mockGetAgentRuntimeSession } = vi.hoisted(() => ({
|
||||
mockGetAgentRuntimeSession: vi.fn(),
|
||||
}));
|
||||
const {
|
||||
mockSafeListen,
|
||||
mockParseAgentEvent,
|
||||
} = vi.hoisted(() => ({
|
||||
const { mockSafeListen, mockParseAgentEvent } = vi.hoisted(() => ({
|
||||
mockSafeListen: vi.fn(),
|
||||
mockParseAgentEvent: vi.fn((payload: unknown) => payload),
|
||||
}));
|
||||
|
||||
vi.mock("@/lib/api/agentRuntime", async () => {
|
||||
const actual =
|
||||
await vi.importActual<typeof import("@/lib/api/agentRuntime")>(
|
||||
"@/lib/api/agentRuntime",
|
||||
);
|
||||
const actual = await vi.importActual<typeof import("@/lib/api/agentRuntime")>(
|
||||
"@/lib/api/agentRuntime",
|
||||
);
|
||||
return {
|
||||
...actual,
|
||||
getAgentRuntimeSession: mockGetAgentRuntimeSession,
|
||||
@@ -28,10 +24,9 @@ vi.mock("@/lib/api/agentRuntime", async () => {
|
||||
});
|
||||
|
||||
vi.mock("@/lib/api/agentProtocol", async () => {
|
||||
const actual =
|
||||
await vi.importActual<typeof import("@/lib/api/agentProtocol")>(
|
||||
"@/lib/api/agentProtocol",
|
||||
);
|
||||
const actual = await vi.importActual<
|
||||
typeof import("@/lib/api/agentProtocol")
|
||||
>("@/lib/api/agentProtocol");
|
||||
return {
|
||||
...actual,
|
||||
parseAgentEvent: mockParseAgentEvent,
|
||||
@@ -138,7 +133,9 @@ async function renderDock(
|
||||
nextProps?: Partial<React.ComponentProps<typeof TeamWorkspaceDock>>,
|
||||
) => {
|
||||
await act(async () => {
|
||||
root.render(<TeamWorkspaceDock {...defaultProps} {...props} {...nextProps} />);
|
||||
root.render(
|
||||
<TeamWorkspaceDock {...defaultProps} {...props} {...nextProps} />,
|
||||
);
|
||||
await Promise.resolve();
|
||||
});
|
||||
await flushDockEffects();
|
||||
@@ -180,9 +177,13 @@ describe("TeamWorkspaceDock", () => {
|
||||
expect(document.body.textContent).toContain("当前协作方案:前端联调团队");
|
||||
expect(document.body.textContent).toContain("分析、实现、验证三段式推进。");
|
||||
expect(document.body.textContent).not.toContain("查看详情");
|
||||
expect(document.body.querySelector('[data-testid="team-workspace-empty-card"]')).toBeTruthy();
|
||||
expect(
|
||||
document.body.querySelector('[data-testid="team-workspace-selected-team"]'),
|
||||
document.body.querySelector('[data-testid="team-workspace-empty-card"]'),
|
||||
).toBeTruthy();
|
||||
expect(
|
||||
document.body.querySelector(
|
||||
'[data-testid="team-workspace-selected-team"]',
|
||||
),
|
||||
).toBeTruthy();
|
||||
});
|
||||
|
||||
|
||||
@@ -33,11 +33,7 @@ const DockContainer = styled.div<{
|
||||
$placement === "inline" ? "relative" : "absolute"};
|
||||
right: ${({ $placement }) => ($placement === "inline" ? "auto" : "14px")};
|
||||
bottom: ${({ $placement, $withBottomOverlay }) =>
|
||||
$placement === "inline"
|
||||
? "auto"
|
||||
: $withBottomOverlay
|
||||
? "108px"
|
||||
: "16px"};
|
||||
$placement === "inline" ? "auto" : $withBottomOverlay ? "108px" : "16px"};
|
||||
z-index: ${({ $placement }) => ($placement === "inline" ? 120 : 18)};
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
@@ -98,8 +94,8 @@ const DockPanel = styled.div<{
|
||||
: "min(560px, calc(100vw - 40px))"};
|
||||
max-height: ${({ $compact }) =>
|
||||
$compact
|
||||
? "min(220px, calc(100vh - 200px))"
|
||||
: "min(520px, calc(100vh - 148px))"};
|
||||
? "min(220px, calc(100vh - 200px))"
|
||||
: "min(520px, calc(100vh - 148px))"};
|
||||
}
|
||||
|
||||
@media (max-width: 720px) {
|
||||
@@ -333,7 +329,9 @@ const EmptyStateBadge = styled.span<{ $tone?: "neutral" | "success" }>`
|
||||
border-radius: 999px;
|
||||
border: 1px solid
|
||||
${({ $tone }) =>
|
||||
$tone === "success" ? "rgba(167, 243, 208, 0.96)" : "rgba(226, 232, 240, 0.96)"};
|
||||
$tone === "success"
|
||||
? "rgba(167, 243, 208, 0.96)"
|
||||
: "rgba(226, 232, 240, 0.96)"};
|
||||
background: ${({ $tone }) => ($tone === "success" ? "#ecfdf5" : "#f8fafc")};
|
||||
color: ${({ $tone }) => ($tone === "success" ? "#047857" : "#64748b")};
|
||||
padding: 4px 10px;
|
||||
@@ -458,9 +456,7 @@ interface TeamWorkspaceDockProps {
|
||||
sessionIds: string[],
|
||||
timeoutMs?: number,
|
||||
) => void | Promise<void>;
|
||||
onCloseCompletedTeamSessions?: (
|
||||
sessionIds: string[],
|
||||
) => void | Promise<void>;
|
||||
onCloseCompletedTeamSessions?: (sessionIds: string[]) => void | Promise<void>;
|
||||
onCloseSubagentSession?: (sessionId: string) => void | Promise<void>;
|
||||
onResumeSubagentSession?: (sessionId: string) => void | Promise<void>;
|
||||
onOpenSubagentSession?: (sessionId: string) => void | Promise<void>;
|
||||
@@ -557,7 +553,8 @@ export function TeamWorkspaceDock({
|
||||
(selectedTeamRoles?.length ?? 0) > 0;
|
||||
const activeRuntimeSessionCount = childSubagentSessions.filter(
|
||||
(session) =>
|
||||
session.runtime_status === "queued" || session.runtime_status === "running",
|
||||
session.runtime_status === "queued" ||
|
||||
session.runtime_status === "running",
|
||||
).length;
|
||||
const runningRuntimeSessionCount = childSubagentSessions.filter(
|
||||
(session) => session.runtime_status === "running",
|
||||
@@ -580,14 +577,15 @@ export function TeamWorkspaceDock({
|
||||
? `${runningRuntimeSessionCount} 位处理中`
|
||||
: null;
|
||||
const teamQueueBadgeText =
|
||||
(teamConcurrencySnapshot?.team_queued_count ?? queuedRuntimeSessionCount) > 0
|
||||
(teamConcurrencySnapshot?.team_queued_count ?? queuedRuntimeSessionCount) >
|
||||
0
|
||||
? `${teamConcurrencySnapshot?.team_queued_count ?? queuedRuntimeSessionCount} 位等待中`
|
||||
: null;
|
||||
const teamQueueReason =
|
||||
teamConcurrencySnapshot?.queue_reason?.trim() || null;
|
||||
const launcherStatusMeta = useMemo<
|
||||
{ label: string; tone: "idle" | "active" | "success" | "error" } | null
|
||||
>(() => {
|
||||
const teamQueueReason = teamConcurrencySnapshot?.queue_reason?.trim() || null;
|
||||
const launcherStatusMeta = useMemo<{
|
||||
label: string;
|
||||
tone: "idle" | "active" | "success" | "error";
|
||||
} | null>(() => {
|
||||
if (!launcherOnly) {
|
||||
return null;
|
||||
}
|
||||
@@ -604,8 +602,7 @@ export function TeamWorkspaceDock({
|
||||
if (activeRuntimeSessionCount > 0) {
|
||||
return {
|
||||
label:
|
||||
teamConcurrencyBadgeText ||
|
||||
`${activeRuntimeSessionCount} 位处理中`,
|
||||
teamConcurrencyBadgeText || `${activeRuntimeSessionCount} 位处理中`,
|
||||
tone: "active",
|
||||
};
|
||||
}
|
||||
@@ -645,11 +642,7 @@ export function TeamWorkspaceDock({
|
||||
}
|
||||
if (hasRealTeamGraph) {
|
||||
if (teamConcurrencyBadgeText || teamQueueBadgeText) {
|
||||
return [
|
||||
"查看任务进展",
|
||||
teamConcurrencyBadgeText,
|
||||
teamQueueBadgeText,
|
||||
]
|
||||
return ["查看任务进展", teamConcurrencyBadgeText, teamQueueBadgeText]
|
||||
.filter(Boolean)
|
||||
.join(" · ");
|
||||
}
|
||||
@@ -693,10 +686,8 @@ export function TeamWorkspaceDock({
|
||||
return;
|
||||
}
|
||||
|
||||
const graphAppeared =
|
||||
!previousHasRealGraphRef.current && hasRealTeamGraph;
|
||||
const graphCleared =
|
||||
previousHasRealGraphRef.current && !hasRealTeamGraph;
|
||||
const graphAppeared = !previousHasRealGraphRef.current && hasRealTeamGraph;
|
||||
const graphCleared = previousHasRealGraphRef.current && !hasRealTeamGraph;
|
||||
|
||||
if (graphCleared) {
|
||||
setUserDismissedAutoExpand(false);
|
||||
@@ -777,12 +768,12 @@ export function TeamWorkspaceDock({
|
||||
liveRuntimeBySessionId={liveRuntimeBySessionId}
|
||||
liveActivityBySessionId={liveActivityBySessionId}
|
||||
activityRefreshVersionBySessionId={activityRefreshVersionBySessionId}
|
||||
onSendSubagentInput={onSendSubagentInput}
|
||||
onWaitSubagentSession={onWaitSubagentSession}
|
||||
onWaitActiveTeamSessions={onWaitActiveTeamSessions}
|
||||
onCloseCompletedTeamSessions={onCloseCompletedTeamSessions}
|
||||
onCloseSubagentSession={onCloseSubagentSession}
|
||||
onResumeSubagentSession={onResumeSubagentSession}
|
||||
onSendSubagentInput={onSendSubagentInput}
|
||||
onWaitSubagentSession={onWaitSubagentSession}
|
||||
onWaitActiveTeamSessions={onWaitActiveTeamSessions}
|
||||
onCloseCompletedTeamSessions={onCloseCompletedTeamSessions}
|
||||
onCloseSubagentSession={onCloseSubagentSession}
|
||||
onResumeSubagentSession={onResumeSubagentSession}
|
||||
onOpenSubagentSession={onOpenSubagentSession}
|
||||
onReturnToParentSession={onReturnToParentSession}
|
||||
teamWaitSummary={teamWaitSummary}
|
||||
@@ -811,18 +802,16 @@ export function TeamWorkspaceDock({
|
||||
{runtimeFormationMeta?.title || "等待协作成员加入"}
|
||||
</EmptyStateTitle>
|
||||
<EmptyStateDescription>
|
||||
{dispatchPreviewState?.status === "forming"
|
||||
? "系统正在按当前任务准备协作分工,成员接入后会自动展开完整协作面板。"
|
||||
: dispatchPreviewState?.status === "formed"
|
||||
? `已准备 ${dispatchPreviewState.members.length} 位协作成员,后续会自动接入并继续处理。`
|
||||
: dispatchPreviewState?.status === "failed"
|
||||
? dispatchPreviewState.errorMessage?.trim() ||
|
||||
"这次协作准备失败,你仍然可以继续在当前对话中处理。"
|
||||
: (
|
||||
<>
|
||||
需要时系统会自动邀请协作成员加入,这里会切换成完整协作面板。
|
||||
</>
|
||||
)}
|
||||
{dispatchPreviewState?.status === "forming" ? (
|
||||
"系统正在按当前任务准备协作分工,成员接入后会自动展开完整协作面板。"
|
||||
) : dispatchPreviewState?.status === "formed" ? (
|
||||
`已准备 ${dispatchPreviewState.members.length} 位协作成员,后续会自动接入并继续处理。`
|
||||
) : dispatchPreviewState?.status === "failed" ? (
|
||||
dispatchPreviewState.errorMessage?.trim() ||
|
||||
"这次协作准备失败,你仍然可以继续在当前对话中处理。"
|
||||
) : (
|
||||
<>需要时系统会自动邀请协作成员加入,这里会切换成完整协作面板。</>
|
||||
)}
|
||||
</EmptyStateDescription>
|
||||
{teamQueueReason ? (
|
||||
<EmptyStateBadge>{teamQueueReason}</EmptyStateBadge>
|
||||
@@ -836,7 +825,9 @@ export function TeamWorkspaceDock({
|
||||
data-testid="team-workspace-selected-team-toggle"
|
||||
>
|
||||
<div>
|
||||
<EmptyStateDetailTitle>{runtimeTeamLabel}</EmptyStateDetailTitle>
|
||||
<EmptyStateDetailTitle>
|
||||
{runtimeTeamLabel}
|
||||
</EmptyStateDetailTitle>
|
||||
<EmptyStateDetailHint>
|
||||
{dispatchPreviewState
|
||||
? "查看当前协作分工与参考方案"
|
||||
@@ -856,7 +847,9 @@ export function TeamWorkspaceDock({
|
||||
<EmptyStateRoleName>
|
||||
{dispatchPreviewState ? "当前摘要" : "方案摘要"}
|
||||
</EmptyStateRoleName>
|
||||
<EmptyStateRoleSummary>{runtimeTeamSummary}</EmptyStateRoleSummary>
|
||||
<EmptyStateRoleSummary>
|
||||
{runtimeTeamSummary}
|
||||
</EmptyStateRoleSummary>
|
||||
</EmptyStateRoleItem>
|
||||
) : null}
|
||||
{dispatchPreviewState?.members.map((member) => {
|
||||
@@ -883,7 +876,9 @@ export function TeamWorkspaceDock({
|
||||
</span>
|
||||
) : null}
|
||||
</div>
|
||||
<EmptyStateRoleSummary>{member.summary}</EmptyStateRoleSummary>
|
||||
<EmptyStateRoleSummary>
|
||||
{member.summary}
|
||||
</EmptyStateRoleSummary>
|
||||
{member.skillIds.length > 0 ? (
|
||||
<div className="mt-2 flex flex-wrap gap-1.5">
|
||||
{member.skillIds.map((skillId) => (
|
||||
@@ -903,7 +898,9 @@ export function TeamWorkspaceDock({
|
||||
? selectedTeamRoles?.map((role) => (
|
||||
<EmptyStateRoleItem key={`dock-role-${role.id}`}>
|
||||
<EmptyStateRoleName>{role.label}</EmptyStateRoleName>
|
||||
<EmptyStateRoleSummary>{role.summary}</EmptyStateRoleSummary>
|
||||
<EmptyStateRoleSummary>
|
||||
{role.summary}
|
||||
</EmptyStateRoleSummary>
|
||||
</EmptyStateRoleItem>
|
||||
))
|
||||
: null}
|
||||
@@ -932,7 +929,9 @@ export function TeamWorkspaceDock({
|
||||
<EmptyStateBadge>{runtimeTeamSummary}</EmptyStateBadge>
|
||||
) : null}
|
||||
{dispatchPreviewState?.members.length ? (
|
||||
<EmptyStateBadge>{dispatchPreviewState.members.length} 个成员</EmptyStateBadge>
|
||||
<EmptyStateBadge>
|
||||
{dispatchPreviewState.members.length} 个成员
|
||||
</EmptyStateBadge>
|
||||
) : null}
|
||||
<EmptyStateBadge>不遮挡画布</EmptyStateBadge>
|
||||
</EmptyStateFooter>
|
||||
|
||||
@@ -164,7 +164,9 @@ describe("ToolCallDisplay", () => {
|
||||
expect(
|
||||
container.querySelector('[data-testid="tool-call-rendered-result"]'),
|
||||
).not.toBeNull();
|
||||
expect(container.textContent).toContain("本次检索未返回可解析链接,请稍后重试。");
|
||||
expect(container.textContent).toContain(
|
||||
"本次检索未返回可解析链接,请稍后重试。",
|
||||
);
|
||||
expect(container.textContent).not.toContain("查看原始输出");
|
||||
});
|
||||
|
||||
@@ -189,7 +191,9 @@ describe("ToolCallDisplay", () => {
|
||||
{
|
||||
id: "tool-search-2",
|
||||
name: "WebSearch",
|
||||
arguments: JSON.stringify({ query: "March 13 2026 world headlines" }),
|
||||
arguments: JSON.stringify({
|
||||
query: "March 13 2026 world headlines",
|
||||
}),
|
||||
status: "completed",
|
||||
result: { success: true, output: "https://example.com/2" },
|
||||
startTime: new Date("2026-03-13T12:00:02.000Z"),
|
||||
@@ -207,7 +211,9 @@ describe("ToolCallDisplay", () => {
|
||||
expect(container.textContent).toContain("3月13日国际新闻");
|
||||
expect(container.textContent).toContain("March 13 2026 world headlines");
|
||||
expect(container.textContent).toContain("搜索 3月13日国际新闻");
|
||||
expect(container.textContent).toContain("搜索 March 13 2026 world headlines");
|
||||
expect(container.textContent).toContain(
|
||||
"搜索 March 13 2026 world headlines",
|
||||
);
|
||||
expect(container.textContent).toContain("中文日期检索");
|
||||
expect(container.textContent).toContain("头条检索");
|
||||
});
|
||||
@@ -246,7 +252,9 @@ describe("ToolCallDisplay", () => {
|
||||
|
||||
mountedRoots.push({ container, root });
|
||||
|
||||
const groups = container.querySelectorAll('[data-testid="tool-call-work-group"]');
|
||||
const groups = container.querySelectorAll(
|
||||
'[data-testid="tool-call-work-group"]',
|
||||
);
|
||||
expect(groups).toHaveLength(1);
|
||||
expect(container.textContent).toContain("已执行 2 条命令");
|
||||
expect(container.textContent).toContain("2");
|
||||
@@ -254,7 +262,9 @@ describe("ToolCallDisplay", () => {
|
||||
expect(container.textContent).toContain("ls -la");
|
||||
|
||||
act(() => {
|
||||
const groupToggle = groups[0]?.querySelector("button") as HTMLButtonElement | null;
|
||||
const groupToggle = groups[0]?.querySelector(
|
||||
"button",
|
||||
) as HTMLButtonElement | null;
|
||||
groupToggle?.click();
|
||||
});
|
||||
|
||||
@@ -324,8 +334,7 @@ describe("ToolCallDisplay", () => {
|
||||
project_root_path: "/Users/coso/.proxycast/projects/project-1",
|
||||
markdown_relative_path:
|
||||
"exports/x-article-export/github-mcp/index.md",
|
||||
images_relative_dir:
|
||||
"exports/x-article-export/github-mcp/images",
|
||||
images_relative_dir: "exports/x-article-export/github-mcp/images",
|
||||
image_count: 7,
|
||||
},
|
||||
saved_project_id: "project-1",
|
||||
@@ -359,7 +368,9 @@ describe("ToolCallDisplay", () => {
|
||||
expect(container.textContent).toContain(
|
||||
"图片资源:7 张 · exports/x-article-export/github-mcp/images",
|
||||
);
|
||||
expect(container.textContent).toContain("脚本来源:服务端脚本 · 2026-03-25");
|
||||
expect(container.textContent).toContain(
|
||||
"脚本来源:服务端脚本 · 2026-03-25",
|
||||
);
|
||||
});
|
||||
|
||||
it("站点能力工具结果应支持直接打开已保存内容", () => {
|
||||
@@ -559,7 +570,10 @@ describe("ToolCallDisplay", () => {
|
||||
{
|
||||
id: "tool-remote-trigger-1",
|
||||
name: "RemoteTrigger",
|
||||
arguments: JSON.stringify({ action: "run", trigger_id: "remote-1" }),
|
||||
arguments: JSON.stringify({
|
||||
action: "run",
|
||||
trigger_id: "remote-1",
|
||||
}),
|
||||
status: "completed",
|
||||
result: { success: true, output: "ok" },
|
||||
startTime: new Date("2026-03-20T12:20:14.000Z"),
|
||||
|
||||
@@ -13,12 +13,7 @@ import React, {
|
||||
useCallback,
|
||||
} from "react";
|
||||
import { open as openExternal } from "@tauri-apps/plugin-shell";
|
||||
import {
|
||||
ChevronDown,
|
||||
ChevronRight,
|
||||
Loader2,
|
||||
ExternalLink,
|
||||
} from "lucide-react";
|
||||
import { ChevronDown, ChevronRight, Loader2, ExternalLink } from "lucide-react";
|
||||
import { cn } from "@/lib/utils";
|
||||
import type {
|
||||
AgentToolCallState as ToolCallState,
|
||||
@@ -389,43 +384,43 @@ function normalizeSiteToolResultSummary(
|
||||
}
|
||||
|
||||
return {
|
||||
savedContent: hasSavedContent
|
||||
? {
|
||||
contentId: readFirstNonEmptyString([savedContentRecord], [
|
||||
"content_id",
|
||||
"contentId",
|
||||
]),
|
||||
projectId: readFirstNonEmptyString([savedContentRecord], [
|
||||
"project_id",
|
||||
"projectId",
|
||||
]),
|
||||
title: readFirstNonEmptyString([savedContentRecord], ["title"]),
|
||||
projectRootPath: readFirstNonEmptyString([savedContentRecord], [
|
||||
"project_root_path",
|
||||
"projectRootPath",
|
||||
]),
|
||||
bundleRelativeDir: readFirstNonEmptyString([savedContentRecord], [
|
||||
"bundle_relative_dir",
|
||||
"bundleRelativeDir",
|
||||
]),
|
||||
markdownRelativePath: readFirstNonEmptyString([savedContentRecord], [
|
||||
"markdown_relative_path",
|
||||
"markdownRelativePath",
|
||||
]),
|
||||
imagesRelativeDir: readFirstNonEmptyString([savedContentRecord], [
|
||||
"images_relative_dir",
|
||||
"imagesRelativeDir",
|
||||
]),
|
||||
metaRelativePath: readFirstNonEmptyString([savedContentRecord], [
|
||||
"meta_relative_path",
|
||||
"metaRelativePath",
|
||||
]),
|
||||
imageCount: readFirstFiniteNumber([savedContentRecord], [
|
||||
"image_count",
|
||||
"imageCount",
|
||||
]),
|
||||
}
|
||||
: undefined,
|
||||
savedContent: hasSavedContent
|
||||
? {
|
||||
contentId: readFirstNonEmptyString(
|
||||
[savedContentRecord],
|
||||
["content_id", "contentId"],
|
||||
),
|
||||
projectId: readFirstNonEmptyString(
|
||||
[savedContentRecord],
|
||||
["project_id", "projectId"],
|
||||
),
|
||||
title: readFirstNonEmptyString([savedContentRecord], ["title"]),
|
||||
projectRootPath: readFirstNonEmptyString(
|
||||
[savedContentRecord],
|
||||
["project_root_path", "projectRootPath"],
|
||||
),
|
||||
bundleRelativeDir: readFirstNonEmptyString(
|
||||
[savedContentRecord],
|
||||
["bundle_relative_dir", "bundleRelativeDir"],
|
||||
),
|
||||
markdownRelativePath: readFirstNonEmptyString(
|
||||
[savedContentRecord],
|
||||
["markdown_relative_path", "markdownRelativePath"],
|
||||
),
|
||||
imagesRelativeDir: readFirstNonEmptyString(
|
||||
[savedContentRecord],
|
||||
["images_relative_dir", "imagesRelativeDir"],
|
||||
),
|
||||
metaRelativePath: readFirstNonEmptyString(
|
||||
[savedContentRecord],
|
||||
["meta_relative_path", "metaRelativePath"],
|
||||
),
|
||||
imageCount: readFirstFiniteNumber(
|
||||
[savedContentRecord],
|
||||
["image_count", "imageCount"],
|
||||
),
|
||||
}
|
||||
: undefined,
|
||||
savedProjectId,
|
||||
savedBy: readFirstNonEmptyString(candidates, ["saved_by", "savedBy"]),
|
||||
saveSkippedProjectId,
|
||||
@@ -449,7 +444,9 @@ function resolveSiteProjectSourceLabel(source?: string): string | null {
|
||||
return null;
|
||||
}
|
||||
|
||||
function resolveSiteAdapterSourceLabel(summary: SiteToolResultSummary): string | null {
|
||||
function resolveSiteAdapterSourceLabel(
|
||||
summary: SiteToolResultSummary,
|
||||
): string | null {
|
||||
if (summary.adapterSourceKind === "server_synced") {
|
||||
return summary.adapterSourceVersion
|
||||
? `服务端脚本 · ${summary.adapterSourceVersion}`
|
||||
@@ -741,7 +738,8 @@ export const ToolCallDisplay: React.FC<ToolCallDisplayProps> = ({
|
||||
|
||||
// 获取文件名
|
||||
const fileName = useMemo(
|
||||
() => resolveToolPrimarySubjectFromInfo(toolCall.name, parsedArgs, filePath),
|
||||
() =>
|
||||
resolveToolPrimarySubjectFromInfo(toolCall.name, parsedArgs, filePath),
|
||||
[filePath, parsedArgs, toolCall.name],
|
||||
);
|
||||
|
||||
@@ -825,8 +823,11 @@ export const ToolCallDisplay: React.FC<ToolCallDisplayProps> = ({
|
||||
|
||||
const notices: ToolResultNotice[] = [];
|
||||
const savedProjectId =
|
||||
siteResultSummary.savedProjectId || siteResultSummary.savedContent?.projectId;
|
||||
const savedSourceLabel = resolveSiteProjectSourceLabel(siteResultSummary.savedBy);
|
||||
siteResultSummary.savedProjectId ||
|
||||
siteResultSummary.savedContent?.projectId;
|
||||
const savedSourceLabel = resolveSiteProjectSourceLabel(
|
||||
siteResultSummary.savedBy,
|
||||
);
|
||||
|
||||
if (siteResultSummary.savedContent?.title) {
|
||||
let text = `结果已自动保存${
|
||||
@@ -984,9 +985,7 @@ export const ToolCallDisplay: React.FC<ToolCallDisplayProps> = ({
|
||||
const shouldShowRawSearchResultToggle =
|
||||
hasSearchResults && resultText !== "(无输出)";
|
||||
const shouldRenderResultPanel =
|
||||
isExpanded &&
|
||||
hasResult &&
|
||||
(!hasSearchResults || showRawSearchResultOutput);
|
||||
isExpanded && hasResult && (!hasSearchResults || showRawSearchResultOutput);
|
||||
|
||||
const handleOpenExternalUrl = useCallback(async (url: string) => {
|
||||
try {
|
||||
@@ -1007,7 +1006,13 @@ export const ToolCallDisplay: React.FC<ToolCallDisplayProps> = ({
|
||||
) {
|
||||
setIsExpanded(true);
|
||||
}
|
||||
}, [isMessageStreaming, isRunning, hasResult, hasResultImages, hasSearchResults]);
|
||||
}, [
|
||||
isMessageStreaming,
|
||||
isRunning,
|
||||
hasResult,
|
||||
hasResultImages,
|
||||
hasSearchResults,
|
||||
]);
|
||||
|
||||
useEffect(() => {
|
||||
if (hasSearchResults && !hasUserToggledExpandedRef.current) {
|
||||
@@ -1042,7 +1047,10 @@ export const ToolCallDisplay: React.FC<ToolCallDisplayProps> = ({
|
||||
return (
|
||||
<div className={cn("group", grouped && "pl-1")}>
|
||||
{grouped ? (
|
||||
<div className="flex items-start gap-2 py-1.5" data-testid="tool-call-row">
|
||||
<div
|
||||
className="flex items-start gap-2 py-1.5"
|
||||
data-testid="tool-call-row"
|
||||
>
|
||||
<span className="pt-0.5 font-mono text-xs text-slate-400">
|
||||
{groupMarker}
|
||||
</span>
|
||||
@@ -1165,9 +1173,7 @@ export const ToolCallDisplay: React.FC<ToolCallDisplayProps> = ({
|
||||
{hasSearchResults && isExpanded && (
|
||||
<div className="mb-2 ml-6 mt-1.5">
|
||||
<div className="mb-2 flex flex-wrap gap-2 text-[11px] text-slate-500">
|
||||
<span>
|
||||
{searchSemantic.label}
|
||||
</span>
|
||||
<span>{searchSemantic.label}</span>
|
||||
</div>
|
||||
<SearchResultPreviewList
|
||||
items={searchResultItems}
|
||||
@@ -1189,9 +1195,7 @@ export const ToolCallDisplay: React.FC<ToolCallDisplayProps> = ({
|
||||
setShowRawSearchResultOutput((current) => !current)
|
||||
}
|
||||
>
|
||||
{showRawSearchResultOutput
|
||||
? "收起原始输出"
|
||||
: "查看原始输出"}
|
||||
{showRawSearchResultOutput ? "收起原始输出" : "查看原始输出"}
|
||||
</button>
|
||||
</div>
|
||||
) : null}
|
||||
@@ -1312,11 +1316,7 @@ export const ToolCallList: React.FC<ToolCallListProps> = ({
|
||||
for (const toolCall of toolCalls) {
|
||||
const isSearch = isUnifiedWebSearchToolName(toolCall.name);
|
||||
const lastGroup = groups[groups.length - 1];
|
||||
if (
|
||||
isSearch &&
|
||||
lastGroup &&
|
||||
lastGroup.type === "search"
|
||||
) {
|
||||
if (isSearch && lastGroup && lastGroup.type === "search") {
|
||||
lastGroup.items.push(toolCall);
|
||||
continue;
|
||||
}
|
||||
|
||||
@@ -1,13 +1,13 @@
|
||||
export type EmptyStateTone = "slate" | "sky" | "emerald" | "amber" | "lime";
|
||||
|
||||
export const EMPTY_STATE_PANEL_CLASSNAME =
|
||||
"rounded-[26px] border border-slate-200/80 bg-white p-4 shadow-sm shadow-slate-950/5 md:p-5";
|
||||
"rounded-[26px] border border-lime-200/80 bg-white p-4 shadow-sm shadow-slate-950/5 md:p-5";
|
||||
|
||||
export const EMPTY_STATE_PANEL_EMBEDDED_CLASSNAME =
|
||||
"rounded-[22px] border border-slate-200/80 bg-white p-3.5 shadow-sm shadow-slate-950/5";
|
||||
"rounded-[22px] border border-lime-200/80 bg-white p-3.5 shadow-sm shadow-slate-950/5";
|
||||
|
||||
export const EMPTY_STATE_CARD_SURFACE_CLASSNAME =
|
||||
"overflow-hidden rounded-[22px] border border-slate-200/80 bg-white p-3.5 shadow-sm shadow-slate-950/5";
|
||||
"overflow-hidden rounded-[22px] border border-lime-200/80 bg-white p-3.5 shadow-sm shadow-slate-950/5";
|
||||
|
||||
export const EMPTY_STATE_BADGE_BASE_CLASSNAME =
|
||||
"inline-flex items-center rounded-full border px-2.5 py-1 text-[11px] font-medium shadow-sm shadow-slate-950/5";
|
||||
@@ -31,19 +31,19 @@ export const EMPTY_STATE_ICON_TONE_CLASSNAMES: Record<EmptyStateTone, string> =
|
||||
};
|
||||
|
||||
export const EMPTY_STATE_META_PILL_CLASSNAME =
|
||||
"rounded-full border border-slate-200/80 bg-white/90 px-2 py-0.5 text-[10px] font-medium text-slate-500";
|
||||
"rounded-full border border-lime-200/80 bg-lime-50/50 px-2 py-0.5 text-[10px] font-medium text-slate-600";
|
||||
|
||||
export const EMPTY_STATE_PRESET_BUTTON_CLASSNAME =
|
||||
"inline-flex items-center gap-2 rounded-full border border-slate-200/80 bg-white/92 px-3 py-1.5 text-[13px] text-slate-600 shadow-sm shadow-slate-950/5 transition-colors hover:border-slate-300 hover:bg-white hover:text-slate-900";
|
||||
"inline-flex items-center gap-2 rounded-full border border-lime-200/80 bg-lime-50/50 px-3 py-1.5 text-[13px] text-slate-600 shadow-sm shadow-slate-950/5 transition-colors hover:border-lime-300 hover:bg-white hover:text-slate-900";
|
||||
|
||||
export const EMPTY_STATE_SECONDARY_ACTION_BUTTON_CLASSNAME =
|
||||
"h-8 w-full rounded-full border-slate-200/80 bg-white/88 text-xs text-slate-700 shadow-none transition-colors hover:border-slate-300 hover:bg-white";
|
||||
"h-8 w-full rounded-full border border-lime-200/80 bg-lime-50/50 text-xs text-slate-700 shadow-none transition-colors hover:border-lime-300 hover:bg-white";
|
||||
|
||||
export const EMPTY_STATE_PRIMARY_ACTION_BUTTON_CLASSNAME =
|
||||
"h-9 w-full rounded-full bg-slate-900 px-5 text-white shadow-sm shadow-slate-900/10 transition-colors hover:bg-slate-800 sm:w-auto";
|
||||
|
||||
export const EMPTY_STATE_RECOMMENDATION_CARD_CLASSNAME =
|
||||
"group flex min-w-0 flex-col items-start gap-2 rounded-[20px] border border-slate-200/80 bg-[linear-gradient(180deg,rgba(255,255,255,0.97)_0%,rgba(248,250,252,0.9)_100%)] px-3.5 py-3.5 text-left shadow-sm shadow-slate-950/5 transition-colors hover:border-slate-300 hover:bg-white";
|
||||
"group flex min-w-0 flex-col items-start gap-2 rounded-[20px] border border-lime-200/80 bg-[linear-gradient(180deg,rgba(255,255,255,0.97)_0%,rgba(247,254,231,0.9)_100%)] px-3.5 py-3.5 text-left shadow-sm shadow-slate-950/5 transition-colors hover:border-lime-300 hover:bg-white";
|
||||
|
||||
export const EMPTY_STATE_PAGE_CONTAINER_CLASSNAME =
|
||||
"relative flex min-h-full flex-1 flex-col items-stretch justify-start overflow-y-auto bg-transparent px-2 pb-5 pt-[clamp(2px,0.6vw,6px)] md:px-3";
|
||||
@@ -52,29 +52,28 @@ export const EMPTY_STATE_CONTENT_WRAPPER_CLASSNAME =
|
||||
"relative z-[1] mx-auto flex min-h-full w-full max-w-[1360px] flex-1 flex-col items-stretch gap-3";
|
||||
|
||||
export const EMPTY_STATE_THEME_TABS_CONTAINER_CLASSNAME =
|
||||
"flex w-full max-w-full flex-nowrap justify-start gap-1.5 overflow-x-auto overflow-y-hidden rounded-[20px] border border-slate-200/80 bg-slate-50 p-[5px] shadow-[0_10px_24px_-22px_rgba(15,23,42,0.18)] [scrollbar-width:none] [&::-webkit-scrollbar]:hidden";
|
||||
"flex w-full max-w-full flex-nowrap justify-start gap-1.5 overflow-x-auto overflow-y-hidden rounded-[20px] border border-lime-200/80 bg-lime-50/50 p-[5px] shadow-[0_10px_24px_-22px_rgba(15,23,42,0.18)] [scrollbar-width:none] [&::-webkit-scrollbar]:hidden";
|
||||
|
||||
export const EMPTY_STATE_SELECT_TRIGGER_CLASSNAME =
|
||||
"h-8 rounded-full border-slate-200/80 bg-white/92 px-3 text-xs text-slate-700 shadow-none transition-colors hover:border-slate-300 hover:bg-white focus:ring-1 focus:ring-slate-200";
|
||||
"h-8 rounded-full border-lime-200/80 bg-white/92 px-3 text-xs text-slate-700 shadow-none transition-colors hover:border-lime-300 hover:bg-white focus:ring-1 focus:ring-lime-200";
|
||||
|
||||
export const EMPTY_STATE_PASSIVE_BADGE_CLASSNAME =
|
||||
"h-8 rounded-full border border-slate-200/80 bg-white/92 px-3 text-xs font-normal text-slate-600 shadow-none hover:border-slate-300 hover:bg-white hover:text-slate-900";
|
||||
"h-8 rounded-full border border-lime-200/80 bg-lime-50/50 px-3 text-xs font-normal text-slate-600 shadow-none hover:border-lime-300 hover:bg-white hover:text-slate-900";
|
||||
|
||||
export const EMPTY_STATE_ICON_TOOL_BUTTON_CLASSNAME =
|
||||
"ml-1 h-8 w-8 rounded-full border-slate-200/80 bg-white text-slate-500 shadow-none transition-colors hover:border-slate-300 hover:bg-slate-50 hover:text-slate-700";
|
||||
"ml-1 h-8 w-8 rounded-full border-lime-200/80 bg-white text-slate-500 shadow-none transition-colors hover:border-lime-300 hover:bg-lime-50 hover:text-slate-700";
|
||||
|
||||
const EMPTY_STATE_TOOL_TOGGLE_TONE_CLASSNAMES: Record<EmptyStateTone, string> = {
|
||||
slate:
|
||||
"border-slate-400 bg-slate-200 text-slate-900 shadow-[inset_0_0_0_1px_rgba(148,163,184,0.28)] hover:border-slate-400 hover:bg-slate-200 hover:text-slate-900",
|
||||
sky:
|
||||
"border-sky-300 bg-sky-50 text-sky-700 hover:border-sky-300 hover:bg-sky-50 hover:text-sky-700",
|
||||
emerald:
|
||||
"border-emerald-300 bg-emerald-50 text-emerald-700 hover:border-emerald-300 hover:bg-emerald-50 hover:text-emerald-700",
|
||||
amber:
|
||||
"border-amber-300 bg-amber-50 text-amber-700 hover:border-amber-300 hover:bg-amber-50 hover:text-amber-700",
|
||||
lime:
|
||||
"border-lime-300 bg-lime-50 text-lime-700 hover:border-lime-300 hover:bg-lime-50 hover:text-lime-700",
|
||||
};
|
||||
const EMPTY_STATE_TOOL_TOGGLE_TONE_CLASSNAMES: Record<EmptyStateTone, string> =
|
||||
{
|
||||
slate:
|
||||
"border-slate-400 bg-slate-200 text-slate-900 shadow-[inset_0_0_0_1px_rgba(148,163,184,0.28)] hover:border-slate-400 hover:bg-slate-200 hover:text-slate-900",
|
||||
sky: "border-sky-300 bg-sky-50 text-sky-700 hover:border-sky-300 hover:bg-sky-50 hover:text-sky-700",
|
||||
emerald:
|
||||
"border-emerald-300 bg-emerald-50 text-emerald-700 hover:border-emerald-300 hover:bg-emerald-50 hover:text-emerald-700",
|
||||
amber:
|
||||
"border-amber-300 bg-amber-50 text-amber-700 hover:border-amber-300 hover:bg-amber-50 hover:text-amber-700",
|
||||
lime: "border-lime-300 bg-lime-50 text-lime-700 hover:border-lime-300 hover:bg-lime-50 hover:text-lime-700",
|
||||
};
|
||||
|
||||
export function getEmptyStateIconToolButtonClassName(
|
||||
enabled: boolean,
|
||||
@@ -92,8 +91,8 @@ export function getEmptyStateThemeTabClassName(active: boolean) {
|
||||
return [
|
||||
"flex flex-none items-center gap-1.5 rounded-xl border px-3 py-[7px] text-xs font-medium leading-none transition-[background-color,border-color,color,box-shadow]",
|
||||
active
|
||||
? "border-slate-300 bg-white/95 text-slate-900 shadow-[0_10px_22px_-20px_rgba(15,23,42,0.24)]"
|
||||
: "border-transparent text-slate-600 hover:border-slate-200/90 hover:bg-white/80 hover:text-slate-900",
|
||||
? "border-lime-300 bg-white/95 text-slate-900 shadow-[0_10px_22px_-20px_rgba(15,23,42,0.24)]"
|
||||
: "border-transparent text-slate-600 hover:border-lime-200/90 hover:bg-white/80 hover:text-slate-900",
|
||||
].join(" ");
|
||||
}
|
||||
|
||||
@@ -101,7 +100,7 @@ export function getEmptyStateThemeTabIconClassName(active: boolean) {
|
||||
return [
|
||||
"flex h-5 w-5 items-center justify-center rounded-full border text-[11px]",
|
||||
active
|
||||
? "border-slate-200 bg-white text-slate-700"
|
||||
? "border-lime-200 bg-white text-slate-700"
|
||||
: "border-transparent bg-transparent text-slate-400",
|
||||
].join(" ");
|
||||
}
|
||||
|
||||
@@ -80,6 +80,7 @@ export function resolveGeneralWorkbenchSelectedSearchResult(
|
||||
return null;
|
||||
}
|
||||
return (
|
||||
searchContextItems.find((item) => item.id === selectedSearchResultId) || null
|
||||
searchContextItems.find((item) => item.id === selectedSearchResultId) ||
|
||||
null
|
||||
);
|
||||
}
|
||||
|
||||
@@ -22,9 +22,7 @@ function truncate(text: string, max = 300): string {
|
||||
return "";
|
||||
}
|
||||
const normalized = text.trim();
|
||||
return normalized.length > max
|
||||
? `${normalized.slice(0, max)}…`
|
||||
: normalized;
|
||||
return normalized.length > max ? `${normalized.slice(0, max)}…` : normalized;
|
||||
}
|
||||
|
||||
function formatExecLogJson(value: unknown): string | undefined {
|
||||
@@ -161,7 +159,9 @@ function buildRunEntry(
|
||||
allowedTools: allowedTools.length > 0 ? allowedTools : undefined,
|
||||
whenToUse: skillDetail?.when_to_use?.trim() || undefined,
|
||||
artifactPaths:
|
||||
group.artifactPaths.length > 0 ? [...group.artifactPaths] : undefined,
|
||||
group.artifactPaths.length > 0
|
||||
? [...group.artifactPaths]
|
||||
: undefined,
|
||||
}
|
||||
: undefined;
|
||||
|
||||
@@ -169,8 +169,14 @@ function buildRunEntry(
|
||||
group.artifactPaths.length > 0
|
||||
? `产物:${group.artifactPaths.map((path) => path.split("/").pop()).join("、")}`
|
||||
: undefined;
|
||||
const durationLabel = group.logs.find((log) => log.durationLabel)?.durationLabel;
|
||||
const meta = [sourceRef ? `技能标识:${sourceRef}` : null, detailSummary, artifactSummary]
|
||||
const durationLabel = group.logs.find(
|
||||
(log) => log.durationLabel,
|
||||
)?.durationLabel;
|
||||
const meta = [
|
||||
sourceRef ? `技能标识:${sourceRef}` : null,
|
||||
detailSummary,
|
||||
artifactSummary,
|
||||
]
|
||||
.filter((item): item is string => Boolean(item))
|
||||
.join(" · ");
|
||||
|
||||
@@ -221,7 +227,12 @@ export function buildGeneralWorkbenchExecLogEntries({
|
||||
|
||||
for (const toolCall of message.toolCalls || []) {
|
||||
entries.push(
|
||||
buildToolCallEntry(message.id, toolCall, message.timestamp, toolCallIndex),
|
||||
buildToolCallEntry(
|
||||
message.id,
|
||||
toolCall,
|
||||
message.timestamp,
|
||||
toolCallIndex,
|
||||
),
|
||||
);
|
||||
toolCallIndex += 1;
|
||||
}
|
||||
@@ -248,14 +259,18 @@ export function buildGeneralWorkbenchExecLogEntries({
|
||||
type: "task",
|
||||
typeLabel: "任务提交",
|
||||
content: group.label || group.taskType,
|
||||
timestamp: latestTask?.createdAt ? new Date(latestTask.createdAt) : new Date(0),
|
||||
timestamp: latestTask?.createdAt
|
||||
? new Date(latestTask.createdAt)
|
||||
: new Date(0),
|
||||
status: "completed",
|
||||
});
|
||||
}
|
||||
|
||||
entries.sort((left, right) => {
|
||||
const leftTime = left.timestamp instanceof Date ? left.timestamp.getTime() : 0;
|
||||
const rightTime = right.timestamp instanceof Date ? right.timestamp.getTime() : 0;
|
||||
const leftTime =
|
||||
left.timestamp instanceof Date ? left.timestamp.getTime() : 0;
|
||||
const rightTime =
|
||||
right.timestamp instanceof Date ? right.timestamp.getTime() : 0;
|
||||
return leftTime - rightTime;
|
||||
});
|
||||
|
||||
@@ -271,9 +286,10 @@ export function filterGeneralWorkbenchExecLogEntries(
|
||||
}
|
||||
|
||||
return entries.filter((entry) => {
|
||||
const timestamp = entry.timestamp instanceof Date
|
||||
? entry.timestamp.getTime()
|
||||
: new Date(entry.timestamp).getTime();
|
||||
const timestamp =
|
||||
entry.timestamp instanceof Date
|
||||
? entry.timestamp.getTime()
|
||||
: new Date(entry.timestamp).getTime();
|
||||
return Number.isFinite(timestamp) && timestamp > clearedAt;
|
||||
});
|
||||
}
|
||||
|
||||
@@ -71,7 +71,8 @@ export interface GeneralWorkbenchSidebarExecLogContract {
|
||||
}
|
||||
|
||||
export interface GeneralWorkbenchSidebarProps
|
||||
extends GeneralWorkbenchSidebarShellContract,
|
||||
extends
|
||||
GeneralWorkbenchSidebarShellContract,
|
||||
GeneralWorkbenchSidebarWorkflowContract,
|
||||
GeneralWorkbenchSidebarContextContract,
|
||||
GeneralWorkbenchSidebarExecLogContract {}
|
||||
|
||||
+24
-23
@@ -11,28 +11,31 @@ import type {
|
||||
GeneralWorkbenchAddTextContextAction,
|
||||
} from "./useGeneralWorkbenchContextPanelState";
|
||||
|
||||
export interface GeneralWorkbenchSidebarContextOrchestrationInput
|
||||
extends Omit<BuildGeneralWorkbenchContextPanelPropsParams, "contextPanelState"> {
|
||||
export interface GeneralWorkbenchSidebarContextOrchestrationInput extends Omit<
|
||||
BuildGeneralWorkbenchContextPanelPropsParams,
|
||||
"contextPanelState"
|
||||
> {
|
||||
onAddTextContext?: GeneralWorkbenchAddTextContextAction;
|
||||
onAddLinkContext?: GeneralWorkbenchAddLinkContextAction;
|
||||
onAddFileContext?: GeneralWorkbenchAddFileContextAction;
|
||||
}
|
||||
|
||||
export interface GeneralWorkbenchSidebarWorkflowOrchestrationInput
|
||||
extends Omit<
|
||||
BuildGeneralWorkbenchWorkflowPanelPropsParams,
|
||||
| "creationTaskEventsCount"
|
||||
| "isVersionMode"
|
||||
| "onOpenArtifactWithDefaultApp"
|
||||
| "onRevealArtifactInFinder"
|
||||
| "workflowPanelState"
|
||||
> {
|
||||
export interface GeneralWorkbenchSidebarWorkflowOrchestrationInput extends Omit<
|
||||
BuildGeneralWorkbenchWorkflowPanelPropsParams,
|
||||
| "creationTaskEventsCount"
|
||||
| "isVersionMode"
|
||||
| "onOpenArtifactWithDefaultApp"
|
||||
| "onRevealArtifactInFinder"
|
||||
| "workflowPanelState"
|
||||
> {
|
||||
activityLogs: SidebarActivityLog[];
|
||||
creationTaskEvents: GeneralWorkbenchCreationTaskEvent[];
|
||||
}
|
||||
|
||||
export interface GeneralWorkbenchSidebarExecLogOrchestrationInput
|
||||
extends Omit<BuildGeneralWorkbenchExecLogPropsParams, "execLogState"> {
|
||||
export interface GeneralWorkbenchSidebarExecLogOrchestrationInput extends Omit<
|
||||
BuildGeneralWorkbenchExecLogPropsParams,
|
||||
"execLogState"
|
||||
> {
|
||||
messages: Message[];
|
||||
skillDetailMap: Record<string, SkillDetailInfo | null>;
|
||||
}
|
||||
@@ -47,19 +50,17 @@ export interface GeneralWorkbenchSidebarOrchestrationInput {
|
||||
export type GeneralWorkbenchSidebarContextOrchestrationSource =
|
||||
GeneralWorkbenchSidebarContextOrchestrationInput;
|
||||
|
||||
export interface GeneralWorkbenchSidebarWorkflowOrchestrationSource
|
||||
extends Omit<
|
||||
GeneralWorkbenchSidebarWorkflowOrchestrationInput,
|
||||
"creationTaskEvents"
|
||||
> {
|
||||
export interface GeneralWorkbenchSidebarWorkflowOrchestrationSource extends Omit<
|
||||
GeneralWorkbenchSidebarWorkflowOrchestrationInput,
|
||||
"creationTaskEvents"
|
||||
> {
|
||||
creationTaskEvents?: GeneralWorkbenchCreationTaskEvent[];
|
||||
}
|
||||
|
||||
export interface GeneralWorkbenchSidebarExecLogOrchestrationSource
|
||||
extends Omit<
|
||||
GeneralWorkbenchSidebarExecLogOrchestrationInput,
|
||||
"messages" | "skillDetailMap"
|
||||
> {
|
||||
export interface GeneralWorkbenchSidebarExecLogOrchestrationSource extends Omit<
|
||||
GeneralWorkbenchSidebarExecLogOrchestrationInput,
|
||||
"messages" | "skillDetailMap"
|
||||
> {
|
||||
messages?: Message[];
|
||||
skillDetailMap?: Record<string, SkillDetailInfo | null>;
|
||||
}
|
||||
|
||||
@@ -28,7 +28,9 @@ export function formatGeneralWorkbenchActionErrorMessage(
|
||||
return `${prefix}:${detail}`;
|
||||
}
|
||||
|
||||
export async function writeGeneralWorkbenchClipboardText(text: string): Promise<void> {
|
||||
export async function writeGeneralWorkbenchClipboardText(
|
||||
text: string,
|
||||
): Promise<void> {
|
||||
const value = text.trim();
|
||||
if (!value) {
|
||||
return;
|
||||
|
||||
@@ -48,7 +48,10 @@ describe("generalWorkbenchWorkflowData", () => {
|
||||
status: "completed",
|
||||
timeLabel: "10:31",
|
||||
runId: "run-1",
|
||||
artifactPaths: ["content-posts/demo.md", "content-posts/demo-cover.png"],
|
||||
artifactPaths: [
|
||||
"content-posts/demo.md",
|
||||
"content-posts/demo-cover.png",
|
||||
],
|
||||
},
|
||||
])[0]?.artifactPaths,
|
||||
).toEqual(["content-posts/demo.md", "content-posts/demo-cover.png"]);
|
||||
|
||||
@@ -85,14 +85,18 @@ export function useGeneralWorkbenchContextPanelState({
|
||||
onAddLinkContext,
|
||||
onAddFileContext,
|
||||
}: UseGeneralWorkbenchContextPanelStateParams): GeneralWorkbenchContextPanelState {
|
||||
const [selectedSearchResultId, setSelectedSearchResultId] = useState<string | null>(null);
|
||||
const [selectedSearchResultId, setSelectedSearchResultId] = useState<
|
||||
string | null
|
||||
>(null);
|
||||
const [addContextDialogOpen, setAddContextDialogOpen] = useState(false);
|
||||
const [addTextDialogOpen, setAddTextDialogOpen] = useState(false);
|
||||
const [addLinkDialogOpen, setAddLinkDialogOpen] = useState(false);
|
||||
const [contextDraftText, setContextDraftText] = useState("");
|
||||
const [contextDraftLink, setContextDraftLink] = useState("");
|
||||
const [contextCreateLoading, setContextCreateLoading] = useState(false);
|
||||
const [contextCreateError, setContextCreateError] = useState<string | null>(null);
|
||||
const [contextCreateError, setContextCreateError] = useState<string | null>(
|
||||
null,
|
||||
);
|
||||
const [contextDropActive, setContextDropActive] = useState(false);
|
||||
|
||||
const searchInputRef = useRef<HTMLInputElement>(null);
|
||||
@@ -212,15 +216,12 @@ export function useGeneralWorkbenchContextPanelState({
|
||||
return;
|
||||
}
|
||||
|
||||
await runContextAction(
|
||||
async () => {
|
||||
await onAddFileContext({
|
||||
path: selected,
|
||||
name: resolveGeneralWorkbenchFileNameFromPath(selected),
|
||||
});
|
||||
},
|
||||
"已添加文件上下文",
|
||||
);
|
||||
await runContextAction(async () => {
|
||||
await onAddFileContext({
|
||||
path: selected,
|
||||
name: resolveGeneralWorkbenchFileNameFromPath(selected),
|
||||
});
|
||||
}, "已添加文件上下文");
|
||||
} catch (error) {
|
||||
const nextError = formatGeneralWorkbenchActionErrorMessage(
|
||||
"读取文件失败",
|
||||
@@ -242,36 +243,32 @@ export function useGeneralWorkbenchContextPanelState({
|
||||
|
||||
const fileWithPath = file as File & { path?: string };
|
||||
if (fileWithPath.path && onAddFileContext) {
|
||||
await runContextAction(
|
||||
async () => {
|
||||
await onAddFileContext({
|
||||
path: fileWithPath.path || "",
|
||||
name: file.name,
|
||||
});
|
||||
},
|
||||
"已添加文件上下文",
|
||||
);
|
||||
await runContextAction(async () => {
|
||||
await onAddFileContext({
|
||||
path: fileWithPath.path || "",
|
||||
name: file.name,
|
||||
});
|
||||
}, "已添加文件上下文");
|
||||
return;
|
||||
}
|
||||
|
||||
if (!onAddTextContext) {
|
||||
setContextCreateError("当前环境无法读取拖拽文件路径,请使用“上传文件”按钮");
|
||||
setContextCreateError(
|
||||
"当前环境无法读取拖拽文件路径,请使用“上传文件”按钮",
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
await runContextAction(
|
||||
async () => {
|
||||
const content = await file.text();
|
||||
if (!content.trim()) {
|
||||
throw new Error("文件内容为空");
|
||||
}
|
||||
await onAddTextContext({
|
||||
content,
|
||||
name: file.name,
|
||||
});
|
||||
},
|
||||
"已添加文本上下文",
|
||||
);
|
||||
await runContextAction(async () => {
|
||||
const content = await file.text();
|
||||
if (!content.trim()) {
|
||||
throw new Error("文件内容为空");
|
||||
}
|
||||
await onAddTextContext({
|
||||
content,
|
||||
name: file.name,
|
||||
});
|
||||
}, "已添加文本上下文");
|
||||
},
|
||||
[onAddFileContext, onAddTextContext, runContextAction],
|
||||
);
|
||||
@@ -286,14 +283,11 @@ export function useGeneralWorkbenchContextPanelState({
|
||||
setContextCreateError("请输入文本内容");
|
||||
return;
|
||||
}
|
||||
await runContextAction(
|
||||
async () => {
|
||||
await onAddTextContext({
|
||||
content: normalizedText,
|
||||
});
|
||||
},
|
||||
"已添加文本上下文",
|
||||
);
|
||||
await runContextAction(async () => {
|
||||
await onAddTextContext({
|
||||
content: normalizedText,
|
||||
});
|
||||
}, "已添加文本上下文");
|
||||
}, [contextDraftText, onAddTextContext, runContextAction]);
|
||||
|
||||
const handleSubmitLinkContext = useCallback(async () => {
|
||||
@@ -306,14 +300,11 @@ export function useGeneralWorkbenchContextPanelState({
|
||||
setContextCreateError("请输入网站链接");
|
||||
return;
|
||||
}
|
||||
await runContextAction(
|
||||
async () => {
|
||||
await onAddLinkContext({
|
||||
url: normalizedLink,
|
||||
});
|
||||
},
|
||||
"已添加网站链接上下文",
|
||||
);
|
||||
await runContextAction(async () => {
|
||||
await onAddLinkContext({
|
||||
url: normalizedLink,
|
||||
});
|
||||
}, "已添加网站链接上下文");
|
||||
}, [contextDraftLink, onAddLinkContext, runContextAction]);
|
||||
|
||||
return {
|
||||
|
||||
@@ -44,7 +44,8 @@ export function useGeneralWorkbenchExecLogState({
|
||||
);
|
||||
|
||||
const visibleExecLogEntries = useMemo(
|
||||
() => filterGeneralWorkbenchExecLogEntries(execLogEntries, execLogClearedAt),
|
||||
() =>
|
||||
filterGeneralWorkbenchExecLogEntries(execLogEntries, execLogClearedAt),
|
||||
[execLogClearedAt, execLogEntries],
|
||||
);
|
||||
|
||||
|
||||
@@ -31,7 +31,8 @@ export function useGeneralWorkbenchSidebarOrchestration({
|
||||
}: UseGeneralWorkbenchSidebarOrchestrationParams): GeneralWorkbenchSidebarOrchestration {
|
||||
const { isVersionMode, context, workflow, execLog } = input;
|
||||
const branchCount = workflow.branchItems.length;
|
||||
const runDetailSessionId = workflow.activeRunDetail?.session_id?.trim() || null;
|
||||
const runDetailSessionId =
|
||||
workflow.activeRunDetail?.session_id?.trim() || null;
|
||||
const { handleRevealArtifactInFinder, handleOpenArtifactWithDefaultApp } =
|
||||
useGeneralWorkbenchArtifactActions({
|
||||
runDetailSessionId,
|
||||
|
||||
@@ -21,7 +21,9 @@ interface UseGeneralWorkbenchWorkflowPanelStateParams {
|
||||
export interface GeneralWorkbenchWorkflowPanelState {
|
||||
completedSteps: number;
|
||||
progressPercent: number;
|
||||
groupedActivityLogs: ReturnType<typeof buildGeneralWorkbenchActivityLogGroups>;
|
||||
groupedActivityLogs: ReturnType<
|
||||
typeof buildGeneralWorkbenchActivityLogGroups
|
||||
>;
|
||||
groupedCreationTaskEvents: ReturnType<
|
||||
typeof buildGeneralWorkbenchCreationTaskGroups
|
||||
>;
|
||||
@@ -49,7 +51,9 @@ export function useGeneralWorkbenchWorkflowPanelState({
|
||||
);
|
||||
|
||||
const progressPercent =
|
||||
workflowSteps.length > 0 ? (completedSteps / workflowSteps.length) * 100 : 0;
|
||||
workflowSteps.length > 0
|
||||
? (completedSteps / workflowSteps.length) * 100
|
||||
: 0;
|
||||
|
||||
const groupedActivityLogs = useMemo(
|
||||
() => buildGeneralWorkbenchActivityLogGroups(activityLogs),
|
||||
|
||||
@@ -1,6 +1,9 @@
|
||||
import type { Dispatch, SetStateAction } from "react";
|
||||
import type { ActionRequired, Message } from "../types";
|
||||
import { appendActionRequiredToParts, resolveActionPromptKey } from "./agentChatCoreUtils";
|
||||
import {
|
||||
appendActionRequiredToParts,
|
||||
resolveActionPromptKey,
|
||||
} from "./agentChatCoreUtils";
|
||||
|
||||
interface UpsertAssistantActionRequestOptions {
|
||||
assistantMsgId: string;
|
||||
|
||||
@@ -111,7 +111,9 @@ describe("createAgentChatSendMessage", () => {
|
||||
await sendMessage("/review src-tauri", [], false, false, false);
|
||||
|
||||
expect(rawSendMessage).toHaveBeenCalledTimes(1);
|
||||
expect(rawSendMessage.mock.calls[0]?.[0]).toContain("请对以下对象进行代码审查");
|
||||
expect(rawSendMessage.mock.calls[0]?.[0]).toContain(
|
||||
"请对以下对象进行代码审查",
|
||||
);
|
||||
expect(rawSendMessage.mock.calls[0]?.[0]).toContain("src-tauri");
|
||||
expect(listSlashEntryUsage()).toEqual([
|
||||
expect.objectContaining({
|
||||
|
||||
@@ -1,13 +1,7 @@
|
||||
import type { CodexSlashStatusSnapshot } from "../commands";
|
||||
import {
|
||||
executeCodexSlashCommand,
|
||||
parseCodexSlashCommand,
|
||||
} from "../commands";
|
||||
import { executeCodexSlashCommand, parseCodexSlashCommand } from "../commands";
|
||||
import { recordSlashEntryUsage } from "../skill-selection/slashEntryUsage";
|
||||
import type {
|
||||
ClearMessagesOptions,
|
||||
SendMessageFn,
|
||||
} from "./agentChatShared";
|
||||
import type { ClearMessagesOptions, SendMessageFn } from "./agentChatShared";
|
||||
|
||||
interface CreateAgentChatSendMessageOptions {
|
||||
baseStatusSnapshot: CodexSlashStatusSnapshot;
|
||||
|
||||
@@ -87,9 +87,8 @@ describe("agentChatShared", () => {
|
||||
});
|
||||
|
||||
it("应优先展示待处理请求的摘要", () => {
|
||||
const messages = createPendingActionMessages(
|
||||
"请先确认发布标题后继续执行。",
|
||||
);
|
||||
const messages =
|
||||
createPendingActionMessages("请先确认发布标题后继续执行。");
|
||||
|
||||
expect(extractTaskPreviewFromMessages(messages)).toBe(
|
||||
"请先确认发布标题后继续执行。",
|
||||
|
||||
@@ -145,24 +145,32 @@ function normalizeTaskPreviewText(value: string) {
|
||||
}
|
||||
|
||||
function extractMessageTextContent(message: Message): string {
|
||||
const sanitizedContent = sanitizeMessageTextForPreview(message.content || "", {
|
||||
role: message.role,
|
||||
hasImages: Array.isArray(message.images) && message.images.length > 0,
|
||||
});
|
||||
const sanitizedContent = sanitizeMessageTextForPreview(
|
||||
message.content || "",
|
||||
{
|
||||
role: message.role,
|
||||
hasImages: Array.isArray(message.images) && message.images.length > 0,
|
||||
},
|
||||
);
|
||||
if (sanitizedContent) {
|
||||
return normalizeTaskPreviewText(sanitizedContent);
|
||||
}
|
||||
|
||||
const partText = message.contentParts
|
||||
?.filter(
|
||||
(part): part is Extract<(typeof message.contentParts)[number], { type: "text" | "thinking" }> =>
|
||||
part.type === "text" || part.type === "thinking",
|
||||
(
|
||||
part,
|
||||
): part is Extract<
|
||||
(typeof message.contentParts)[number],
|
||||
{ type: "text" | "thinking" }
|
||||
> => part.type === "text" || part.type === "thinking",
|
||||
)
|
||||
.map((part) =>
|
||||
part.type === "text"
|
||||
? sanitizeMessageTextForPreview(part.text, {
|
||||
role: message.role,
|
||||
hasImages: Array.isArray(message.images) && message.images.length > 0,
|
||||
hasImages:
|
||||
Array.isArray(message.images) && message.images.length > 0,
|
||||
})
|
||||
: part.text,
|
||||
)
|
||||
@@ -189,9 +197,9 @@ function extractToolCallPreview(message: Message): string {
|
||||
}
|
||||
|
||||
function extractActionRequestPreview(message: Message): string {
|
||||
const pendingAction = [...(message.actionRequests || [])].reverse().find(
|
||||
(item) => item.status !== "submitted",
|
||||
);
|
||||
const pendingAction = [...(message.actionRequests || [])]
|
||||
.reverse()
|
||||
.find((item) => item.status !== "submitted");
|
||||
if (!pendingAction) {
|
||||
return "";
|
||||
}
|
||||
@@ -201,9 +209,7 @@ function extractActionRequestPreview(message: Message): string {
|
||||
.join(" ");
|
||||
|
||||
return normalizeTaskPreviewText(
|
||||
pendingAction.prompt ||
|
||||
questionText ||
|
||||
"等待你确认或补充信息后继续执行。",
|
||||
pendingAction.prompt || questionText || "等待你确认或补充信息后继续执行。",
|
||||
);
|
||||
}
|
||||
|
||||
@@ -351,8 +357,13 @@ export function buildLiveTaskSnapshot(params: {
|
||||
queuedTurnCount?: number;
|
||||
workspaceError: boolean;
|
||||
}): LiveTaskSnapshot {
|
||||
const { messages, isSending, pendingActionCount, queuedTurnCount, workspaceError } =
|
||||
params;
|
||||
const {
|
||||
messages,
|
||||
isSending,
|
||||
pendingActionCount,
|
||||
queuedTurnCount,
|
||||
workspaceError,
|
||||
} = params;
|
||||
const lastMessage = messages[messages.length - 1];
|
||||
const preview = extractTaskPreviewFromMessages(messages);
|
||||
const taskState = deriveTaskLiveState({
|
||||
@@ -368,8 +379,7 @@ export function buildLiveTaskSnapshot(params: {
|
||||
messagesCount: messages.length,
|
||||
status: taskState.status,
|
||||
statusReason: taskState.statusReason,
|
||||
lastPreview:
|
||||
preview || "等待你补充任务需求后开始执行。",
|
||||
lastPreview: preview || "等待你补充任务需求后开始执行。",
|
||||
hasUnread: false,
|
||||
};
|
||||
}
|
||||
|
||||
@@ -197,11 +197,7 @@ export const getAccessModeStorageKey = (
|
||||
};
|
||||
|
||||
export const normalizeAccessMode = (value: unknown): AgentAccessMode => {
|
||||
if (
|
||||
value === "read-only" ||
|
||||
value === "current" ||
|
||||
value === "full-access"
|
||||
) {
|
||||
if (value === "read-only" || value === "current" || value === "full-access") {
|
||||
return value;
|
||||
}
|
||||
return DEFAULT_AGENT_ACCESS_MODE;
|
||||
|
||||
@@ -104,7 +104,9 @@ describe("agentProjectStorage", () => {
|
||||
});
|
||||
|
||||
expect(harness.getValue().projectId).toBe("project-updated");
|
||||
expect(loadPersistedProjectId(LAST_PROJECT_ID_KEY)).toBe("project-updated");
|
||||
expect(loadPersistedProjectId(LAST_PROJECT_ID_KEY)).toBe(
|
||||
"project-updated",
|
||||
);
|
||||
} finally {
|
||||
harness.unmount();
|
||||
}
|
||||
|
||||
@@ -4,7 +4,9 @@ import { normalizeProjectId } from "../utils/topicProjectResolution";
|
||||
export const LAST_PROJECT_ID_KEY = "agent_last_project_id";
|
||||
export const SESSION_WORKSPACE_STORAGE_KEY_PREFIX = "agent_session_workspace_";
|
||||
|
||||
export function getSessionWorkspaceStorageKey(sessionId: string): string | null {
|
||||
export function getSessionWorkspaceStorageKey(
|
||||
sessionId: string,
|
||||
): string | null {
|
||||
const normalizedSessionId = sessionId.trim();
|
||||
if (!normalizedSessionId) {
|
||||
return null;
|
||||
|
||||
@@ -50,7 +50,9 @@ export interface AgentRuntimeAdapter {
|
||||
): Promise<string>;
|
||||
listSessions(): Promise<AsterSessionInfo[]>;
|
||||
getSession(sessionId: string): Promise<AsterSessionDetail>;
|
||||
getSessionReadModel(sessionId: string): Promise<AsterSessionDetail["thread_read"]>;
|
||||
getSessionReadModel(
|
||||
sessionId: string,
|
||||
): Promise<AsterSessionDetail["thread_read"]>;
|
||||
replayRequest(
|
||||
sessionId: string,
|
||||
requestId: string,
|
||||
|
||||
@@ -76,10 +76,7 @@ describe("agentSessionRefresh", () => {
|
||||
preserveExecutionStrategyOnMissingDetail: true,
|
||||
},
|
||||
);
|
||||
expect(markSynced).toHaveBeenCalledWith(
|
||||
"session-1",
|
||||
"code_orchestrated",
|
||||
);
|
||||
expect(markSynced).toHaveBeenCalledWith("session-1", "code_orchestrated");
|
||||
});
|
||||
|
||||
it("刷新 read model 时应在会话仍匹配时应用 snapshot", async () => {
|
||||
|
||||
@@ -154,10 +154,7 @@ export function buildHydratedAgentSessionSnapshot(
|
||||
snapshot: {
|
||||
sessionId: syncSessionId ? topicId : currentSessionId,
|
||||
messages: shouldPreserveExistingTimeline
|
||||
? mergeHydratedMessagesWithLocalState(
|
||||
currentMessages,
|
||||
hydratedMessages,
|
||||
)
|
||||
? mergeHydratedMessagesWithLocalState(currentMessages, hydratedMessages)
|
||||
: hydratedMessages,
|
||||
threadTurns: nextThreadTurns,
|
||||
threadItems: nextThreadItems,
|
||||
|
||||
@@ -1,8 +1,15 @@
|
||||
import type { Dispatch, MutableRefObject, SetStateAction } from "react";
|
||||
import { parseAgentEvent, type AgentThreadItem, type AgentThreadTurn } from "@/lib/api/agentProtocol";
|
||||
import {
|
||||
parseAgentEvent,
|
||||
type AgentThreadItem,
|
||||
type AgentThreadTurn,
|
||||
} from "@/lib/api/agentProtocol";
|
||||
import type { AgentRuntimeAdapter } from "./agentRuntimeAdapter";
|
||||
import type { ActiveStreamState } from "./agentStreamSubmissionLifecycle";
|
||||
import { upsertThreadItemState, upsertThreadTurnState } from "./agentThreadState";
|
||||
import {
|
||||
upsertThreadItemState,
|
||||
upsertThreadTurnState,
|
||||
} from "./agentThreadState";
|
||||
import { resolveRuntimeWarningToastPresentation } from "./runtimeWarningPresentation";
|
||||
|
||||
interface AgentStreamCompactionNotifications {
|
||||
@@ -148,7 +155,8 @@ export async function runAgentStreamCompaction(
|
||||
break;
|
||||
}
|
||||
case "error":
|
||||
notifiedErrorMessage = String(data.message ?? "").trim() || "未知错误";
|
||||
notifiedErrorMessage =
|
||||
String(data.message ?? "").trim() || "未知错误";
|
||||
notify.error(`压缩上下文失败: ${notifiedErrorMessage}`);
|
||||
clearActiveStreamIfMatch(eventName);
|
||||
disposeListener();
|
||||
|
||||
@@ -122,21 +122,36 @@ describe("agentStreamFlowControl", () => {
|
||||
} as never,
|
||||
removeStreamListener,
|
||||
refreshSessionReadModel,
|
||||
setQueuedTurns: createStateSetter(() => queuedTurns, (value) => {
|
||||
queuedTurns = value;
|
||||
}),
|
||||
setThreadItems: createStateSetter(() => threadItems, (value) => {
|
||||
threadItems = value;
|
||||
}),
|
||||
setThreadTurns: createStateSetter(() => threadTurns, (value) => {
|
||||
threadTurns = value;
|
||||
}),
|
||||
setCurrentTurnId: createStateSetter(() => currentTurnId, (value) => {
|
||||
currentTurnId = value;
|
||||
}),
|
||||
setMessages: createStateSetter(() => messages, (value) => {
|
||||
messages = value;
|
||||
}),
|
||||
setQueuedTurns: createStateSetter(
|
||||
() => queuedTurns,
|
||||
(value) => {
|
||||
queuedTurns = value;
|
||||
},
|
||||
),
|
||||
setThreadItems: createStateSetter(
|
||||
() => threadItems,
|
||||
(value) => {
|
||||
threadItems = value;
|
||||
},
|
||||
),
|
||||
setThreadTurns: createStateSetter(
|
||||
() => threadTurns,
|
||||
(value) => {
|
||||
threadTurns = value;
|
||||
},
|
||||
),
|
||||
setCurrentTurnId: createStateSetter(
|
||||
() => currentTurnId,
|
||||
(value) => {
|
||||
currentTurnId = value;
|
||||
},
|
||||
),
|
||||
setMessages: createStateSetter(
|
||||
() => messages,
|
||||
(value) => {
|
||||
messages = value;
|
||||
},
|
||||
),
|
||||
setActiveStream: (next) => {
|
||||
activeStream = next as never;
|
||||
},
|
||||
@@ -182,9 +197,12 @@ describe("agentStreamFlowControl", () => {
|
||||
queuedTurnId: "queued-1",
|
||||
sessionIdRef,
|
||||
refreshSessionReadModel,
|
||||
setQueuedTurns: createStateSetter(() => queuedTurns, (value) => {
|
||||
queuedTurns = value;
|
||||
}),
|
||||
setQueuedTurns: createStateSetter(
|
||||
() => queuedTurns,
|
||||
(value) => {
|
||||
queuedTurns = value;
|
||||
},
|
||||
),
|
||||
notify,
|
||||
}),
|
||||
).resolves.toBe(true);
|
||||
@@ -208,9 +226,12 @@ describe("agentStreamFlowControl", () => {
|
||||
queuedTurnId: "queued-1",
|
||||
sessionIdRef,
|
||||
refreshSessionReadModel,
|
||||
setQueuedTurns: createStateSetter(() => queuedTurns, (value) => {
|
||||
queuedTurns = value;
|
||||
}),
|
||||
setQueuedTurns: createStateSetter(
|
||||
() => queuedTurns,
|
||||
(value) => {
|
||||
queuedTurns = value;
|
||||
},
|
||||
),
|
||||
notify,
|
||||
}),
|
||||
).resolves.toBe(true);
|
||||
|
||||
@@ -5,7 +5,10 @@ import type { Message } from "../types";
|
||||
import type { ActiveStreamState } from "./agentStreamSubmissionLifecycle";
|
||||
import type { AgentRuntimeAdapter } from "./agentRuntimeAdapter";
|
||||
import { updateMessageArtifactsStatus } from "../utils/messageArtifacts";
|
||||
import { removeThreadItemState, removeThreadTurnState } from "./agentThreadState";
|
||||
import {
|
||||
removeThreadItemState,
|
||||
removeThreadTurnState,
|
||||
} from "./agentThreadState";
|
||||
|
||||
interface AgentStreamFlowNotify {
|
||||
info: (message: string) => void;
|
||||
@@ -47,7 +50,10 @@ interface PromoteQueuedTurnOptions extends QueueActionOptions {
|
||||
onError?: (error: unknown) => void;
|
||||
}
|
||||
|
||||
interface ResumeThreadOptions extends Omit<QueueActionOptions, "setQueuedTurns"> {
|
||||
interface ResumeThreadOptions extends Omit<
|
||||
QueueActionOptions,
|
||||
"setQueuedTurns"
|
||||
> {
|
||||
runtime: Pick<AgentRuntimeAdapter, "resumeThread">;
|
||||
onError?: (error: unknown) => void;
|
||||
}
|
||||
@@ -147,7 +153,10 @@ export async function removeQueuedAgentTurn(options: RemoveQueuedTurnOptions) {
|
||||
}
|
||||
|
||||
try {
|
||||
const removed = await runtime.removeQueuedTurn(activeSessionId, queuedTurnId);
|
||||
const removed = await runtime.removeQueuedTurn(
|
||||
activeSessionId,
|
||||
queuedTurnId,
|
||||
);
|
||||
if (removed) {
|
||||
setQueuedTurns((prev) => removeQueuedTurnFromState(prev, queuedTurnId));
|
||||
}
|
||||
@@ -161,7 +170,9 @@ export async function removeQueuedAgentTurn(options: RemoveQueuedTurnOptions) {
|
||||
}
|
||||
}
|
||||
|
||||
export async function promoteQueuedAgentTurn(options: PromoteQueuedTurnOptions) {
|
||||
export async function promoteQueuedAgentTurn(
|
||||
options: PromoteQueuedTurnOptions,
|
||||
) {
|
||||
const {
|
||||
runtime,
|
||||
queuedTurnId,
|
||||
@@ -179,7 +190,10 @@ export async function promoteQueuedAgentTurn(options: PromoteQueuedTurnOptions)
|
||||
setQueuedTurns((prev) => removeQueuedTurnFromState(prev, queuedTurnId));
|
||||
|
||||
try {
|
||||
const promoted = await runtime.promoteQueuedTurn(activeSessionId, queuedTurnId);
|
||||
const promoted = await runtime.promoteQueuedTurn(
|
||||
activeSessionId,
|
||||
queuedTurnId,
|
||||
);
|
||||
await refreshSessionReadModel(activeSessionId);
|
||||
if (!promoted) {
|
||||
return false;
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user