diff --git a/.agents/skills/dev-loop/SKILL.md b/.agents/skills/dev-loop/SKILL.md index c11dad3f..ad72e311 100644 --- a/.agents/skills/dev-loop/SKILL.md +++ b/.agents/skills/dev-loop/SKILL.md @@ -14,15 +14,17 @@ description: "自主开发推进引擎——ROADMAP 驱动、模型分配、并 | 入口 | 别名/模型 | 上下文 | 强项 | 派发策略 | |---|---|---:|---|---| -| Codex 自带 agent 工具 | GPT-5.5 | 256k | 全方面强,代码、agentic 执行、审查都稳 | 中等上下文内的核心实现、跨前后端小集成、关键 review | -| Claude CLI | **opus** = DeepSeek-V4-Pro | 1M | 速度快、强推理、长上下文 | 大范围阅读、路线图/架构判断、复杂设计评审、安全/方案审查 | +| Codex 自带 agent 工具 | GPT-5.5 low/mid | 256k | 前端、看图、UI/视觉判断、常规实现和审查 | 前端 UI、截图对比、局部体验判断、常规 code review | +| Codex 自带 agent 工具 | GPT-5.5 xhigh | 256k | 最强架构推理和复杂工程设计 | 复杂架构、关键方案、跨模块取舍、高强度 sidecar | +| Claude CLI | **opus** = DeepSeek-V4-Pro | 1M | 速度快、强推理、长上下文 | 大范围阅读、文档整理、roadmap/architecture 归纳、竞品/仓库查找、复杂方案审查 | | Claude CLI | **sonnet** = GLM-5.1 | 200k | 强代码和 agentic 能力 | 窄范围代码实现、测试修复、Go/TS 小切片 | | Claude CLI | **haiku** = DeepSeek-V4-Flash | 200k | 速度快、轻量反馈 | 快速检查、轻量 review、日志/文档/小范围 UI 可读性审查 | - **主 Agent**:设计决策、审查输出、编辑核心文件(AGENTS.md/STATE.md/ROADMAP.md)。 -- **Codex GPT-5.5 subagent**:工具可用时优先派给高价值代码实现和强 review;不要给超 256k 的大仓库研究。 -- **Claude opus**:DeepSeek-V4-Pro,1M 上下文,长上下文推理、竞品研究、安全/架构审查。 -- **Claude sonnet**:GLM-5.1,200k 上下文,明确路径内的实现和 focused tests;prompt 精简,只传必要文件。 +- **Codex GPT-5.5 low/mid**:前端、看图、截图对比、常规 UI/UX 判断。 +- **Codex GPT-5.5 xhigh**:复杂架构推理、关键方案和高风险设计复核。 +- **Claude opus**:DeepSeek-V4-Pro,1M 上下文,速度快、强推理,适合长文本、找东西、简单文档、架构整理、大范围归纳和复杂方案审查。 +- **Claude sonnet**:GLM-5.1,200k 上下文,强代码模型,适合明确路径内的实现和 focused tests;prompt 精简,只传必要文件。 - **Claude haiku**:DeepSeek-V4-Flash,200k 上下文,快速检查、轻量 review、日志/文档/小范围 UI 可读性审查,不作为代码主力。 ## CC 原生工具配合 @@ -46,7 +48,7 @@ dev-loop 配合两个 CC 内置命令使用效果最好: ## 标准工作循环 ### 1. 理解 -- 读 `AGENTS.md` / `docs/handoffs/STATE.md` / `docs/roadmap.md` +- 读 `AGENTS.md`、`docs/roadmap.md` 和当前任务关联的设计/架构文档 - 理解现有架构、约定、当前进度 - STATE.md 是跨 session 状态文件,每次接手先读 @@ -59,21 +61,22 @@ dev-loop 配合两个 CC 内置命令使用效果最好: ### 3. 执行 - **自己(主 session)**:设计决策、审查输出、编辑核心文件(AGENTS.md/STATE.md/ROADMAP.md) -- **派 Codex GPT-5.5 subagent**:中等上下文内的核心实现、跨模块小集成、关键代码 review -- **派 Claude opus**:复杂架构推理、长上下文研究、安全审查、多维度审计 +- **派 GPT-5.5 low/mid**:前端 UI、看图、截图对比、局部体验判断 +- **派 GPT-5.5 xhigh**:复杂架构、关键方案、跨模块取舍 +- **派 Claude opus**:长文本、找东西、简单文档、架构整理、大范围归纳、复杂方案审查 - **派 Claude sonnet**:窄范围编码实现、bug 修复、focused tests - **派 Claude haiku**:快速检查、轻量 review、日志/文档/小范围 UI 可读性审查 - 每次 subagent 完成后审查其输出 ### 4. 审查 -- 完成一批变更后启动交叉审查:按维度混用 Codex GPT-5.5、Claude opus、Claude sonnet、Claude haiku +- 完成一批变更后启动交叉审查:按维度混用 GPT-5.5 low/mid/xhigh、Claude opus、Claude sonnet、Claude haiku - 维度:结构、文档、安全、架构、易用性、视觉 QA - 让其他 agent 提问题:"审查这个变更,列出你担心的问题" - 修复高优先级项 ### 5. 同步 - AGENTS.md / CLAUDE.md(规则变更) -- `docs/handoffs/STATE.md`(事实变更:进度/阻塞/部署状态) +- `docs/roadmap.md` 或当前任务计划文档(事实变更:进度、阻塞、下一步) - ROADMAP.md(标记完成、记录阻塞、写下一步) - 运行 `neat-freak` 清理过时文档 - 运行 `memory-management` 同步 memory(如有跨系统需求) @@ -100,12 +103,12 @@ dev-loop 配合两个 CC 内置命令使用效果最好: ### 交叉审查维度与模型 | 维度 | 模型 | 为什么 | |---|---|---| -| 结构 | sonnet | 机械检查,批量扫文件 | -| 文档 | sonnet | 一致性检查,不重推理 | -| 安全 | **opus** | 必须深度推理 | -| 架构 | **opus** | 需要设计判断 | -| 易用性 | sonnet | 清单式检查 | -| 业务逻辑 | **haiku** | 简短复杂逻辑审查 | +| 结构 | opus | 长上下文整理和跨文件一致性检查 | +| 文档 | opus | 一致性检查、整理和归纳 | +| 安全 | **GPT-5.5 xhigh** | 必须深度推理 | +| 架构 | **GPT-5.5 xhigh** | 需要设计判断 | +| 易用性 | GPT-5.5 low/mid | 前端体验和截图判断 | +| 业务逻辑 | **sonnet** | 强代码模型,适合 focused 逻辑检查 | 审查 agent 的 prompt 要具体:告诉它查什么、怎么报告、文件在哪。 diff --git a/.agents/skills/dev-loop/references/model-strategy.md b/.agents/skills/dev-loop/references/model-strategy.md index 56600bb3..a1c27ea6 100644 --- a/.agents/skills/dev-loop/references/model-strategy.md +++ b/.agents/skills/dev-loop/references/model-strategy.md @@ -6,15 +6,16 @@ | 入口 | 别名/模型 | 上下文 | 优势 | 限制 | |---|---|---:|---|---| -| Codex 自带 agent 工具 | GPT-5.5 | 256k | 全方面强,代码、agentic 执行、审查都稳 | 上下文不如 Claude opus,不能吃超大仓库研究 | -| Claude CLI | **opus** = DeepSeek-V4-Pro | 1M | 速度快、强推理、长上下文,适合架构设计、安全审查、竞品仓库研究 | 代码实现不作为首选 | +| Codex 自带 agent 工具 | GPT-5.5 low/mid | 256k | 前端、看图、UI/视觉判断、常规实现和审查 | 不吃超大仓库研究 | +| Codex 自带 agent 工具 | GPT-5.5 xhigh | 256k | 最强架构推理和复杂工程设计 | 上下文仍不适合超大仓库全文阅读 | +| Claude CLI | **opus** = DeepSeek-V4-Pro | 1M | 速度快、强推理、长上下文,适合找东西、长文本整理、架构整理和复杂方案审查 | 代码实现不作为首选 | | Claude CLI | **sonnet** = GLM-5.1 | 200k | 强代码和 agentic 能力,适合聚焦实现 | 不要给大批量阅读 | | Claude CLI | **haiku** = DeepSeek-V4-Flash | 200k | 速度快、轻量反馈,适合快速检查、轻量 review、日志/文档/小范围 UI 可读性审查 | 不作为代码主力 | ## 选择原则 - **先看入口**:Codex 自带 agent 工具和 Claude CLI 是两套执行面,不能把别名混用。 -- **先限上下文**:超过 256k 的研究、竞品仓库阅读、跨大量文件审查优先 Claude opus;不超过 256k 的核心代码实现优先 Codex GPT-5.5。 +- **先限上下文**:超过 256k 的研究、竞品仓库阅读、跨大量文件审查优先 Claude opus;复杂架构判断优先 GPT-5.5 xhigh;明确文件集代码实现优先 Claude sonnet。 - **先限写入范围**:任何编码 subagent 都必须有允许路径、禁止范围、验收命令和证据输出。 - **轻量检查单独派发**:快速 sanity check、日志/文档/小范围 UI 可读性审查优先 Claude haiku,不让代码主力消耗在低风险扫读上。 @@ -23,21 +24,21 @@ ``` 任务类型? ├── 核心实现 / 跨前后端小集成 -│ ├── 上下文 <= 256k → Codex GPT-5.5 subagent -│ └── 上下文 > 256k → 拆小;设计交给 Claude opus,代码交给 GPT-5.5/sonnet +│ ├── 上下文 <= 256k → GPT-5.5 low/mid 或 xhigh,按复杂度选择 +│ └── 上下文 > 256k → Claude opus 先整理,再拆给实现 agent ├── 窄范围代码修复(明确 1-3 个文件) │ ├── Go/TS/测试小切片 → Claude sonnet(GLM-5.1) -│ └── 高风险实现 review → Codex GPT-5.5 或 Claude opus 复核 +│ └── 高风险实现 review → GPT-5.5 xhigh 或主 Agent 复核 ├── 长上下文推理 / 架构 / 安全 / 竞品仓库研究 -│ └── Claude opus(DeepSeek-V4-Pro, 1M) +│ └── GPT-5.5 xhigh(复杂架构)或 Claude opus(长文本/找东西/整理) ├── 截图 / 竞品图 / 视觉 QA / UI 可读性 │ └── Claude haiku(DeepSeek-V4-Flash,200k,快速轻量反馈) ├── 机械批量文档或格式统一 -│ ├── 中等上下文 → Codex GPT-5.5 -│ └── 超大上下文或需要归纳 → Claude opus 先规划,再分片执行 +│ ├── 中等上下文 → GPT-5.5 low/mid +│ └── 超大上下文或需要归纳 → Claude opus 先整理,再分片执行 └── 交叉审查 - ├── 安全/架构/长期方向 → Claude opus - ├── 代码正确性/集成风险 → Codex GPT-5.5 + ├── 安全/架构/长期方向 → GPT-5.5 xhigh + ├── 代码正确性/集成风险 → GPT-5.5 xhigh 或主 Agent ├── 小范围实现细节 → Claude sonnet └── 小范围 UI 可读性/布局文字检查 → Claude haiku ``` @@ -46,9 +47,10 @@ | Agent | 上限 | 策略 | |---|---:|---| -| Codex GPT-5.5 | 256k | 给完整任务卡 + 必要文件;适合强实现和强 review | -| Claude opus | 1M | DeepSeek-V4-Pro;可给大仓库、大量文档、竞品源码;产出方案/审查,不直接机械改大批文件 | -| Claude sonnet | 200k | GLM-5.1;prompt 精简,只传相关文件;适合窄范围代码和测试 | +| GPT-5.5 low/mid | 256k | 前端、看图、截图对比、常规 UI/UX 判断 | +| GPT-5.5 xhigh | 256k | 复杂架构、关键方案、高风险设计复核 | +| Claude opus | 1M | DeepSeek-V4-Pro;速度快、强推理;长文本、找东西、简单文档、架构整理、大范围归纳 | +| Claude sonnet | 200k | GLM-5.1;强代码模型;prompt 精简,只传相关文件;适合窄范围代码和测试 | | Claude haiku | 200k | DeepSeek-V4-Flash;快速检查、轻量 review、日志/文档/小范围 UI 可读性审查 | ## 并行度 diff --git a/.agents/skills/dev-team/SKILL.md b/.agents/skills/dev-team/SKILL.md index 4bb3d706..36c512f1 100644 --- a/.agents/skills/dev-team/SKILL.md +++ b/.agents/skills/dev-team/SKILL.md @@ -11,12 +11,12 @@ description: 多 Team 并行开发引擎 — 大规模 Issue 修复、跨模块 ``` 你(主 Agent) - ├── Team Leader 1 (Codex GPT-5.5 或 Claude opus) → Worktree A - │ ├── Worker 1 (GPT-5.5 / Claude sonnet) → 修 1-3 issues - │ ├── Worker 2 (GPT-5.5 / Claude sonnet) → 修 1-3 issues + ├── Team Leader 1 (主 Agent 或 GPT-5.5 xhigh) → Worktree A + │ ├── Worker 1 (Claude sonnet / GPT-5.5 low-mid) → 修 1-3 issues + │ ├── Worker 2 (Claude sonnet / GPT-5.5 low-mid) → 修 1-3 issues │ ├── Worker 3 (Claude haiku) → 快速检查 / 轻量 review(如需要) - │ └── Worker 4 (GPT-5.5 / opus) → 测试 + 审查 - ├── Team Leader 2 (Codex GPT-5.5 或 Claude opus) → Worktree B + │ └── Worker 4 (GPT-5.5 xhigh / 主 Agent) → 测试 + 审查 + ├── Team Leader 2 (主 Agent 或 GPT-5.5 xhigh) → Worktree B │ └── ... (同上) └── ... (最多 5 个 Team 并行) ``` @@ -25,8 +25,9 @@ description: 多 Team 并行开发引擎 — 大规模 Issue 修复、跨模块 | Agent | 上下文 | 定位 | |---|---:|---| -| Codex GPT-5.5 subagent | 256k | 全方面强,适合核心实现、跨模块小集成、强代码 review | -| Claude opus = DeepSeek-V4-Pro | 1M | 速度快、强推理、长上下文;适合架构、安全、竞品仓库研究 | +| GPT-5.5 low/mid | 256k | 前端、看图、截图对比、常规 UI/UX 判断 | +| GPT-5.5 xhigh | 256k | 复杂架构、关键方案、高风险设计复核 | +| Claude opus = DeepSeek-V4-Pro | 1M | 速度快、强推理、长上下文;适合长文本、找东西、简单文档、架构整理、大范围归纳 | | Claude sonnet = GLM-5.1 | 200k | 强代码和 agentic 能力;适合明确文件范围内的实现和测试 | | Claude haiku = DeepSeek-V4-Flash | 200k | 速度快、轻量反馈;适合快速检查、轻量 review、日志/文档/小范围 UI 可读性审查 | @@ -91,10 +92,11 @@ You are Team Leader for {team_name}. Fix {N} issues ({batch_name}). 1. Create worktree: git worktree add .worktrees/{worktree_name} -b feat/{branch_name} 2. Read key source files: {file_list} 3. Spawn workers by task type: - - Codex GPT-5.5: core implementation / integration review (<=256k context) + - GPT-5.5 low/mid: frontend, screenshots, visual/UI review + - GPT-5.5 xhigh: complex architecture and high-risk review + - Claude opus: long-text search, docs, architecture整理 - Claude sonnet: narrow code fixes with explicit file whitelist - Claude haiku: fast lightweight checks and narrow UI readability review - - Claude opus: long-context architecture/security review 4. Each worker: read → write failing test → implement fix → go test passes 5. Review all work, resolve conflicts, go test -race, commit 6. Push branch @@ -152,7 +154,7 @@ git branch -d feat/team-* ``` 输入:129 个 Issue,按 label 分组为 5 个批次 Team 数:5 -每个 Team:1 Leader + 3-4 Workers,按任务类型混用 GPT-5.5 / opus / sonnet / haiku +每个 Team:1 Leader + 3-4 Workers,按任务类型混用 GPT-5.5 low/mid/xhigh、opus、sonnet、haiku 总 agent 数:约 20-25 Worktree 数:5 diff --git a/AGENTS.md b/AGENTS.md index 68793cd6..005183eb 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -24,9 +24,9 @@ Agent 不要一次性扫全仓库。按下面顺序加载,够用就停: 1. 先读本文件。 2. 读 `docs/roadmap.md` 和 `docs/architecture.md` — 当前目标、架构边界和实现阶段。 -3. 做 Desktop/Web v4 重构时,继续读 `docs/desktop-web-v4-clean-rebuild-plan.md` 和 `docs/v4-clean-rebuild-decision-questions.md`。 +3. 做 Desktop/Web v4 重构时,继续读 `docs/desktop-web-v4-clean-rebuild-plan.md`、`docs/v4-frontend-progress-2026-06-07.md` 和 `docs/v4-clean-rebuild-decision-questions.md`。 4. 明确任务卡:目标、所属方向、写入范围、接口影响、验收命令。 -5. 如果用户要求持续推进、自我迭代、长程开发、worktree/subagent 分发或交叉 review,必须先加载 `.agents/skills/dev-loop/SKILL.md`,再按其中 `references/` 执行。短任务(单文件修复、小改动)不需要。 +5. 如果用户要求持续推进、自我迭代、长程开发、worktree/subagent 分发或交叉 review,默认先加载 `.agents/skills/dev-loop/SKILL.md`,再按其中 `references/` 执行;但本轮 Desktop/Web v4 clean rebuild 用户已明确要求不用 dev-loop,按 `docs/roadmap.md` 和 `docs/desktop-web-v4-clean-rebuild-plan.md` 直接推进。短任务(单文件修复、小改动)不需要。 6. 只读相关主文档章节:产品或架构不清读 `docs/architecture.md`。 7. 改接口时读 `api/README.md`、`api/openapi.yaml`、`api/events.md`。 8. 改 TokenDance ID 登录、OIDC、跨产品鉴权、Feishu/Lark 集成、Gateway 调用、安全风险、公开包装、i18n 或共享设计 token 时,同步读 `../docs/identity/identity-auth.md`、`../docs/identity/authorization-model.md`、`../docs/security/security-risk.md`、`../docs/identity/feishu-integration.md`、`../docs/ecosystem/product-matrix.md`、`../docs/ecosystem/ecosystem-execution-queue.md`、`../docs/identity/i18n-packaging.md`、`../docs/design/design-system.md`、`../docs/design/design-playbook.md` 或 `../docs/design/visual-qa-matrix.md` 中相关文档。 @@ -58,19 +58,20 @@ Desktop 和 Mobile 是**独立 Tauri 项目**,各自拥有独立的 `src-tauri - 跨两个方向的改动先在 PR 描述里写清楚影响面。 - 开发者必须审查自己 Agent 生成的代码、文档和命令输出;不要把未看懂的 Agent 改动直接合入。 -### Desktop / Mobile 端口与资源分配 +### Desktop / Web / Mobile 端口与资源分配 -两个 Tauri 项目独立运行,不得互相占用端口或修改对方配置。 +Desktop/Web 是本轮 v4 shared workbench 主线,端口必须固定且不能互相抢占。Mobile 不进入本轮 v4 重构,只保留独立预览端口。 -| 资源 | Desktop | Mobile | -|---|---|---| -| Tauri 项目 | `app/desktop/src-tauri/` | `app/mobile/src-tauri/` | -| Vite 开发端口 | **5173** (strict) | **5174** (strict) | -| Rust crate | `agenthub-desktop` | `agenthub-mobile` | -| Tauri identifier | `com.agenthub.desktop` | `com.agenthub.mobile` | -| 前端源码 | `app/desktop/src/` | `app/mobile/src/` | -| 共享前端 | `app/shared/` (`@agenthub/shared`) | `app/shared/` (`@agenthub/shared`) | -| Storybook | 6006 | 无(共用 desktop 的 Storybook) | +| 资源 | Desktop/Tauri | Web | Mobile | +|---|---|---|---| +| Vite 开发端口 | **5173** (strict) | **5174** (strict) | **5175** (strict,非本轮主线) | +| 前端源码 | `app/desktop/src/` | `app/web/src/` | `app/mobile/src/` | +| 平台入口 | `app/desktop/src/App.tsx` + Tauri host adapter | `app/web/src/App.tsx` + Hub/Web adapter | Mobile native adaptation | +| Tauri 项目 | `app/desktop/src-tauri/` | 无 | `app/mobile/src-tauri/` | +| Rust crate | `agenthub-desktop` | 无 | `agenthub-mobile` | +| Tauri identifier | `com.agenthub.desktop` | 无 | `com.agenthub.mobile` | +| 共享前端 | `app/shared/` (`@agenthub/shared`) | `app/shared/` (`@agenthub/shared`) | 稳定子集 | +| Storybook | 6006 | 共用 shared/desktop story | 无 | 其他固定端口: @@ -80,14 +81,14 @@ Desktop 和 Mobile 是**独立 Tauri 项目**,各自拥有独立的 `src-tauri | Edge Server (本地) | 3210 | Desktop 本地 Edge | | OIDC callback (Desktop) | 随机 (127.0.0.1:0) | Rust TcpListener 动态分配 | | OIDC callback (Mobile) | 深链 `agenthub://` | 不走本地 HTTP server | -| Web 工作台 | 5175 (预留) | 尚未开发 | +| Web 工作台 | 5174 | `app/web/vite.config.ts` | Rust/Tauri 隔离规则: - **Desktop Agent 只能修改** `app/desktop/src-tauri/`,**Mobile Agent 只能修改** `app/mobile/src-tauri/`。 - 任何 Agent 不得修改对方的 `tauri.conf.json`、`Cargo.toml`、`lib.rs`。 - 如需共享 Rust 代码,先提议创建 `app/shared-rust/` crate,两边各自在 `Cargo.toml` 中 `[dependencies]` 引用。 -- Desktop 特有功能(tray、Edge 进程管理、keyring)不往 mobile 移植;Mobile 特有功能(deep link、platform secure store)不往 desktop 移植。 +- Desktop 特有功能(tray、Edge 进程管理、keyring)不往 Web/Mobile 移植;Web 只能通过 Hub/Web adapter 访问远端能力;Mobile 特有功能(deep link、platform secure store)不往 desktop 移植。 - 共享的前端代码(类型、hooks、i18n、UI 组件)放 `app/shared/`,两个项目通过 `workspace:*` 引用。 ### AgentHub 产品术语边界 @@ -181,18 +182,21 @@ git status --short --branch # 确认只改了允许的路径 ### 模型分配策略 > AgentHub 项目专用。这里的 `opus` / `sonnet` / `haiku` 是本地 Claude CLI 路由别名,不等于公开 Claude 模型名;Codex 自带 agent 工具单独建模。dev-loop skill 同步更新。 +> 本轮 Desktop/Web v4 clean rebuild 的代码实现主力是 **GLM-5.1 对应的本地 Claude CLI 路由**。如果用户或网关把某个 alias 重新指向 GLM-5.1,派任务前用 `claude -p ... --output-format json` 或 `claude-subagent` skill 的探针确认实际路由,再写入任务卡;不要按公开模型名猜测。 | 入口 | 别名/模型 | 上下文 | 强项 | 优先使用场景 | |---|---|---:|---|---| -| Codex 自带 agent 工具 | GPT-5.5 | 256k | 全方面强,代码、agentic 执行、审查都稳 | 中等上下文内的核心实现、跨前后端小集成、主 Agent 复核前的强力 sidecar | -| Claude CLI | **opus** = DeepSeek-V4-Pro | 1M | 速度快、强推理、长上下文 | 大范围阅读、路线图/架构判断、复杂设计评审、安全/方案审查 | +| Codex 自带 agent 工具 | GPT-5.5 low/mid | 256k | 前端、看图、UI/视觉判断、常规实现和审查 | 前端 UI、截图对比、局部体验判断、常规 code review | +| Codex 自带 agent 工具 | GPT-5.5 xhigh | 256k | 最强架构推理和复杂工程设计 | 复杂架构、关键方案、跨模块取舍、主 Agent 复核前的高强度 sidecar | +| Claude CLI | **opus** = DeepSeek-V4-Pro | 1M | 速度快、强推理、长上下文 | 大范围阅读、文档整理、roadmap/architecture 归纳、竞品/仓库查找、复杂方案审查 | | Claude CLI | **sonnet** = GLM-5.1 | 200k | 强代码和 agentic 能力 | 窄范围代码实现、测试修复、Go/TS 小切片、明确文件集的重构 | | Claude CLI | **haiku** = DeepSeek-V4-Flash | 200k | 速度快、轻量反馈 | 快速检查、轻量 review、日志/文档/小范围 UI 可读性审查 | -- **主 Agent(本 session)**:负责决策、分支治理、提交、roadmap、比赛材料和最终验收。 -- **Codex GPT-5.5 subagent**:工具可用时优先用于高价值代码实现或关键 review;上下文 256k,不承担超大仓库研究。 -- **Claude opus**:DeepSeek-V4-Pro,1M 上下文,用于长上下文推理、竞品仓库研究、架构/安全审查。 -- **Claude sonnet**:GLM-5.1,200k 上下文,用于明确路径内的代码实现和 focused tests;每次只给必要文件。 +- **主 Agent(本 session)**:负责架构设计、规划、分支治理、文档、开发进度管理、整体工程化设计和任务拆解。 +- **Codex GPT-5.5 low/mid**:用于看图、前端 UI、截图对比和常规前端判断。 +- **Codex GPT-5.5 xhigh**:用于复杂架构推理、关键方案和高风险设计复核。 +- **Claude opus**:DeepSeek-V4-Pro,1M 上下文,速度快、强推理,用于长文本、找东西、简单文档、架构整理、大范围归纳和复杂方案审查。 +- **Claude sonnet**:GLM-5.1,200k 上下文,强代码模型,用于明确路径内的代码实现和 focused tests;每次只给必要文件。 - **Claude haiku**:DeepSeek-V4-Flash,200k 上下文,用于快速检查、轻量 review、日志/文档/小范围 UI 可读性审查,不派它做代码主力。 ### Agent 间进度同步 @@ -202,7 +206,7 @@ git status --short --branch # 确认只改了允许的路径 ### 仓库级 Skill - 仓库只提交白名单 skill:`.agents/skills/dev-loop/`、`.agents/skills/test-coverage/`、`.agents/skills/pre-push/`、`.agents/skills/integration-test/`、`.agents/skills/adapter-dev/`、`.agents/skills/env-sandbox/`、`.agents/skills/ui-screenshot/`、`.agents/skills/dev-team/`。 -- 长程多步骤任务(跨文件重构、多步骤功能、需要审查的变更)必须先读 `.agents/skills/dev-loop/SKILL.md`。 +- 长程多步骤任务(跨文件重构、多步骤功能、需要审查的变更)默认先读 `.agents/skills/dev-loop/SKILL.md`;本轮 Desktop/Web v4 clean rebuild 是用户明确排除 dev-loop 的例外,按 roadmap/plan 直接推进。 - 短任务(单文件修复、typo、小改动)不需要 dev-loop——直接做。 - `.agents/skills/dev-loop/references/` 已内嵌模型分配策略、审查清单、worktree 指南;不要假设外部同名 skill 一定可用。 - `docs/architecture.md` 路线图摘要和 `docs/roadmap.md` 是持续开发台账,用来记录当前目标、方向任务、分支进展、验证和下一步;不要把详细方案写成第二套主文档。 @@ -293,16 +297,20 @@ feat/* → dev/delicious233 → master | 分支 | 说明 | 状态 | |------|------|:--:| +| **feat/desktop-web-v4-clean-rebuild** | 当前 Desktop/Web v4 shared UI 主工作树,最终合回 `dev/delicious233` | 当前工作 | | **dev/delicious233** | 主开发分支,唯一事实源 | ✅ 活跃 | | master | PR-only 稳定快照,v0.1.0 已同步 | ✅ 当前 | -| origin/dev/trump | Trump 独立分支,与主线大幅分叉(截至 2026-06 快照),代码已过期不建议合入 | 保留,不自动合并 | -| origin/dev/johnny | Johnny 开发线,仍有少量独有提交 | 单独审,不直合 | +| origin/feat/backend-edge-hub | 后端/Edge-Hub 并行线,本轮 Desktop/Web UI 暂不合并 | 隔离 | +| origin/dev/trump | Trump 分支当前落后主线,无独有提交;不作为本轮 UI 来源 | 保留,不自动合并 | +| origin/dev/johnny | Johnny 开发线仍有少量独有提交但大幅落后 | 单独审,不直合 | | ~~feat/web-desktop-parity / origin/worktree-feat+web-desktop-parity~~ | 早期 Web parity 残留已导出 patch 并删除远端 | ✅ 已归档 | | ~~OIDC 旧保存分支~~ | 旧保存点已被主文档覆盖 | ✅ 已删除 | | ~~codex/johnny-fork~~ | Codex 实验分支 | ✅ 已清理 | | ~~codex/trump-ui-fork~~ | Codex UI fork | ✅ 已清理 | | ~~feat/agent-runtime-expansion~~ | Runtime 扩展 | ✅ 已清理 | | ~~feat/web-agent-closeout-20260526~~ | WebAgent 收尾 | ✅ 已合入并删除 | + +接手提醒:当前主树可能存在 ahead/dirty 并行状态。继续前先运行 `git status --short --branch` 和 `git worktree list`,以 live 输出为准;不要按本表直接推断可提交范围。 | ~~feat/team-hub-authz / team-hub-reliability / team-adapter-compat~~ | 授权、可靠性、adapter 修复 | ✅ 已合入并删除 | 规则: @@ -315,7 +323,7 @@ feat/* → dev/delicious233 → master 开发引擎:`.agents/skills/dev-loop/` — 模型分配(opus/sonnet/haiku)+ 标准循环 + 交叉审查。 -P0 本地执行主链路、M3b/M4/M5/M6/M7 的已验收子项已合入主线;P1/P2 的 TokenDance ID、多端、Hub replay 和远程审批仍按 `docs/architecture.md` 路线图摘要的部分闭环继续推进。 +历史本地执行主链路和旧里程碑的已验收子项已合入主线;当前 Desktop/Web v4 clean rebuild 以 `docs/roadmap.md`、`docs/architecture.md` 和 `docs/desktop-web-v4-clean-rebuild-plan.md` 为准。 进度同步: @@ -342,8 +350,7 @@ P0 本地执行主链路、M3b/M4/M5/M6/M7 的已验收子项已合入主线;P ## 5. 文档规则 - 主文档只保留一份:`docs/architecture.md`。 -- `docs/roadmaps/` 只记录持续开发目标、当前进展、验证和下一步,不承载完整产品或架构说明。 -- `docs/roadmap.md` 是 Sprint 目标和待办清单;完成后可归档进 `docs/archive/`,不要长期扩写成第二套实现文档。 +- `docs/roadmap.md` 记录持续开发目标、当前进展、验证和下一步,不承载完整产品或架构说明。 - AgentHub 自有文档中文优先;`README_EN.md` 是唯一常规英文入口。 - 新增长期说明先考虑合并进三份主文档,不要随手新增根级文档。 - 详细调研放 `docs/reference/`。 diff --git a/README.md b/README.md index 637dcd41..d01a559b 100644 --- a/README.md +++ b/README.md @@ -144,6 +144,7 @@ AgentHub/ │ ├── architecture.md # 产品定位 + 系统架构 + 实现状态 │ ├── roadmap.md # 当前主线与任务优先级 │ ├── desktop-web-v4-clean-rebuild-plan.md +│ ├── v4-frontend-progress-2026-06-07.md │ ├── adr/ # 架构决策记录 │ ├── designs/ # 组件设计文档 │ ├── governance/ # 安全台账、分支治理、文档标准 @@ -162,6 +163,7 @@ AgentHub/ | [架构文档](docs/architecture.md) | 产品定位、系统架构、实现状态(首选入口) | | [路线图](docs/roadmap.md) | 当前主线、阶段任务和验收口径 | | [v4 重构计划](docs/desktop-web-v4-clean-rebuild-plan.md) | Desktop/Web shared workbench clean rebuild | +| [v4 前端进度](docs/v4-frontend-progress-2026-06-07.md) | 5173/5174 shared UI、动效、主题、侧栏和验证证据 | | [API 契约](api/) | REST + WebSocket 接口定义 | | [安全风险台账](docs/governance/security-risk-register.md) | 安全风险登记与追踪 | diff --git a/api/openapi.yaml b/api/openapi.yaml index b77f8985..37756117 100644 --- a/api/openapi.yaml +++ b/api/openapi.yaml @@ -2415,7 +2415,7 @@ paths: post: tags: [HubSyncRelay] operationId: hubAddAgentToSession - summary: Add an agent to a session. + summary: Add an agent instance to a Hub session. x-agenthub-status: implemented x-agenthub-owner: Hub security: [{ bearerAuth: [] }] @@ -2430,12 +2430,30 @@ paths: application/json: schema: type: object - required: [agent_id] + required: [agent_type, display_name] properties: - agent_id: { type: string } + agent_type: + type: string + description: Agent Runtime adapter id such as codex, claude-code, or opencode. + custom_agent_id: + type: string + description: Optional legacy CustomAgent id when dispatching a CustomAgent-backed instance. + display_name: + type: string + description: User-visible name for this session agent instance. responses: - "201": - $ref: "#/components/responses/Created" + "200": + description: Created session agent instance. + content: + application/json: + schema: + type: object + properties: + code: + type: string + example: ok + data: + $ref: "#/components/schemas/AgentInstance" /client/sessions/search: get: @@ -4646,6 +4664,35 @@ components: type: object additionalProperties: true description: Capability flags (streaming, toolCalls, fileChanges, etc.). + AgentInstance: + type: object + required: [id, agent_type, session_id, inviter_user_id, display_name] + properties: + id: + type: string + format: uuid + description: Exact Hub AgentInstance id used by /web/agent-tasks agent_instance_id dispatch. + agent_type: + type: string + description: Agent Runtime adapter id such as codex, claude-code, or opencode. + custom_agent_id: + type: string + format: uuid + description: Optional legacy CustomAgent id. + session_id: + type: string + format: uuid + inviter_user_id: + type: string + format: uuid + workspace_id: + type: string + format: uuid + display_name: + type: string + created_at: + type: string + format: date-time ExecutionTarget: type: object required: [id, owner_id, name, target_type, is_online] diff --git a/app/desktop/package.json b/app/desktop/package.json index 72f30c94..6d3171ef 100644 --- a/app/desktop/package.json +++ b/app/desktop/package.json @@ -30,6 +30,7 @@ "@tauri-apps/api": "^2.11.0", "@tauri-apps/plugin-dialog": "2.7.1", "@tauri-apps/plugin-shell": "^2.2.0", + "class-variance-authority": "^0.7.1", "clsx": "^2.1.1", "i18next": "^26.2.0", "lucide-react": "^1.16.0", diff --git a/app/desktop/scripts/guarded-dev.mjs b/app/desktop/scripts/guarded-dev.mjs index 02bb1830..6cb5fe54 100644 --- a/app/desktop/scripts/guarded-dev.mjs +++ b/app/desktop/scripts/guarded-dev.mjs @@ -33,7 +33,7 @@ async function assertExistingServerIsDesktop() { [ `[agenthub-desktop] ${DEV_URL} is already serving a non-Desktop app.`, 'Stop the process on port 5173 before launching AgentHub Desktop.', - 'Desktop must load app/desktop/src/main.tsx; Mobile uses port 5174.', + 'Desktop must load app/desktop/src/main.tsx; Web uses port 5174.', ].join('\n'), ); process.exit(1); diff --git a/app/desktop/src/App.tsx b/app/desktop/src/App.tsx index 7f73904d..ca34524b 100644 --- a/app/desktop/src/App.tsx +++ b/app/desktop/src/App.tsx @@ -1,1045 +1,30 @@ -import { - useState, - useEffect, - useCallback, - useMemo, - useRef, - Suspense, - lazy, - type CSSProperties, -} from 'react'; -import { useTranslation } from 'react-i18next'; -import { useHiddenMessages } from '@/hooks/useHiddenMessages'; -import { useSidebarResize } from '@/hooks/useSidebarResize'; -import { useThreadCache } from '@/hooks/useThreadCache'; -import { useHealth } from '@/hooks/useHealth'; -import { useChatMessages } from '@/hooks/useChatMessages'; -import { useIsMobile } from '@/hooks/useMediaQuery'; -import { useThreadNavigation } from '@/hooks/useThreadNavigation'; -import { useEdgeStatus } from '@/hooks/useEdgeStatus'; -import { useAgentList } from '@/api/agentQueries'; -import { useHubAgentTeams, type AgentTeamOverview } from '@/api/agentTeamQueries'; -import { useModelCatalog } from '@/api/modelCatalogQueries'; -import { useModelsDevDisplayNames } from '@/api/modelsDevCatalog'; -import { useSendRun } from '@/hooks/useSendRun'; -import { - createThread, - decidePermission as decidePermissionRest, -} from '@/api/edgeClient'; -import { useThreads, useThreadMessages } from '@/api/threadQueries'; -import { useRuns } from '@/api/runQueries'; -import { useHubExecutionTargets } from '@/api/executionTargetQueries'; -import { useAuth } from '@/hooks/useAuth'; -import useFocusSourceTracking from '@/hooks/useFocusSourceTracking'; -import useShellShortcuts from '@/hooks/useShellShortcuts'; -import { useDesktopCommands } from '@/hooks/useDesktopCommands'; -import type { ChatMessage } from '@/components/ChatView.types'; -import { useConnectionStore } from '@/stores/connectionStore'; -import { useThreadStore } from '@/stores/threadStore'; -import { useUIStore } from '@/stores/uiStore'; -import { - clamp, - isRunActiveStatus, - focusComposer, - setComposerDraft, - hideMessages, - isTeamRunActiveStatus, - isPendingTeamApprovalStatus, - isTauriRuntime, -} from '@/utils/appUtils'; -import { useShallow } from 'zustand/shallow'; -import { SkeletonLine } from '@shared/ui'; -import { useToastStore } from '@/stores/toastStore'; -import { useHubStore } from '@/stores/hubStore'; -import { Slot } from '@/views/viewRegistry'; -import ErrorBoundary from '@/components/ErrorBoundary'; -import ConnectionStatus from '@/components/ConnectionStatus'; -import DesktopHubTaskBridge from '@/components/DesktopHubTaskBridge'; -import TopMenuBar from '@/components/TopMenuBar'; -import { useTopMenuConfig } from '@/hooks/useTopMenuConfig'; -import { ToastContainer } from '@/components/Toast'; -import type { SectionId as SettingsSectionId } from '@/components/SettingsPage'; -import { useStoredValueState } from '@/components/settings/utils'; -import { - DEFAULT_AGENT_AUTO, - resolveAvailableDefaultAgentId, -} from '@/utils/defaultAgent'; - -// Lazy-loaded non-critical components -const AuthPage = lazy(() => import('@/components/AuthPage')); -const HomeDashboard = lazy(() => import('@/components/HomeDashboard')); -const SettingsPage = lazy(() => import('@/components/SettingsPage')); -import { - AlertTriangle, - Bot, - ChevronLeft, - ChevronRight, - ClipboardList, - Circle, - Copy, - Home, - MessageSquareText, - Maximize2, - Menu, - Minimize2, - Minus, - Moon, - PanelLeftClose, - PanelLeftOpen, - PanelRightClose, - PanelRightOpen, - Route, - Search, - Settings, - Square, - Sun, - UserCircle, - Wifi, - WifiOff, - X, -} from 'lucide-react'; -import { useQueryClient } from '@tanstack/react-query'; -import { useTheme } from '@/contexts/ThemeContext'; -import { - applyRuntimeAgentLabel, - buildChatMessagesFromThreadItems, - buildDisplayedRunOutputMessage, - filterOptimisticMessagesForThread, - mergeChatMessages, -} from '@/utils/chatMessages'; -import { resolveThreadSelectionId, type ThreadSelectionInput } from '@/utils/threadSelection'; -import { buildAutomaticThreadTitle } from '@/utils/threadTitle'; -import ShellIconButton from '@/components/ShellIconButton'; -import { getCurrentWindow } from '@tauri-apps/api/window'; -import styles from '@/App.module.css'; - -interface OptimisticRun { - runId: string; - status: string; - outputText: string; - toolCalls: []; - changedFiles: []; -} - -const LEFT_SIDEBAR_MIN = 248; -const LEFT_SIDEBAR_MAX = 420; -const RUN_CARD_MIN_WORKSPACE_WIDTH = 760; - -function summarizeAgentTeamOverview(overview?: AgentTeamOverview) { - const runs = overview?.bundles.flatMap((bundle) => bundle.runs) ?? []; - const activeRuns = runs.filter((run) => isTeamRunActiveStatus(run.status)).length; - const pendingApprovals = (overview?.state?.approvals ?? []).filter((approval) => ( - isPendingTeamApprovalStatus(approval.status) - )).length; - const pendingConflicts = (overview?.state?.conflicts ?? []).filter((conflict) => ( - conflict.status !== 'resolved' - )).length; - return { - activeRuns, - pendingApprovals, - pendingConflicts, - blockingCount: pendingApprovals + pendingConflicts, - }; -} +import { useMemo, useState } from 'react'; +import { AgentHubWorkbench } from '@shared/workbench'; +import { useCreateRun } from '@/api/runQueries'; +import { DesktopChrome } from '@/components/DesktopChrome'; +import { createDesktopPlatform, desktopAgents } from '@/platform/desktopPlatform'; +import { useDesktopWorkbenchModel } from '@/platform/useDesktopWorkbenchModel'; export default function App() { - useFocusSourceTracking(); - - const { online, health } = useHealth(); - const { t } = useTranslation(); - const isMobile = useIsMobile(); - const edgeStatus = useEdgeStatus(online); - const addToast = useToastStore((s) => s.addToast); - const { theme, toggleTheme } = useTheme(); - const queryClient = useQueryClient(); - const hubAuth = useAuth(); - - const { data: threadData } = useThreads(); - const threads = threadData?.items ?? []; - const { - addThreadToCache, - updateThreadInCache, - setThreadTitleInCache, - pendingCreatedThreadIdsRef, - emptyCreatedThreadIdsRef, - manuallyNamedThreadIdsRef, - silentCreatedThreadToastIdsRef, - } = useThreadCache(); - - const hubAuthenticated = useHubStore((s) => s.authenticated); - const showAuthModal = useHubStore((s) => s.showAuthModal); - const executionTargetsQuery = useHubExecutionTargets(true); - const hubInventoryEnabled = hubAuth.isAuthenticated && Boolean(hubAuth.token); - const agentTeamsQuery = useHubAgentTeams({ - enabled: hubInventoryEnabled, - getToken: () => hubAuth.token, - }); - const { setOnline, setConnected, wsLatency } = useConnectionStore( - useShallow((s) => ({ setOnline: s.setOnline, setConnected: s.setConnected, wsLatency: s.wsLatency })), - ); - const { selectedThreadId, selectedAgentId, selectThread, selectAgentThread } = useThreadStore( - useShallow((s) => ({ selectedThreadId: s.selectedThreadId, selectedAgentId: s.selectedAgentId, selectThread: s.selectThread, selectAgentThread: s.selectAgentThread })), - ); - const activeThreadId = resolveThreadSelectionId(selectedThreadId as ThreadSelectionInput); - const { messages, isConnected, currentRun, permissionRequests, decidePermission } = useChatMessages(online, activeThreadId); - const { data: agentData } = useAgentList(online); - const agents = useMemo(() => agentData?.items ?? [], [agentData?.items]); - const modelCatalogQuery = useModelCatalog(online); - const modelsDevDisplayNamesQuery = useModelsDevDisplayNames(true); - const agentTeamSummary = useMemo( - () => summarizeAgentTeamOverview(agentTeamsQuery.data), - [agentTeamsQuery.data], - ); - const teamRunBadgeCount = agentTeamSummary.blockingCount || agentTeamSummary.activeRuns; - const teamRunButtonLabel = agentTeamSummary.blockingCount > 0 - ? t('workspace.teamRunsWithBlocks', { count: agentTeamSummary.blockingCount }) - : agentTeamSummary.activeRuns > 0 - ? t('workspace.teamRunsActive', { count: agentTeamSummary.activeRuns }) - : t('settings.agentScheduling'); - const [userMessages, setUserMessages] = useState([]); - const { hiddenMessageIds, hideMessage } = useHiddenMessages(activeThreadId); - const [shortcutHelpOpen, setShortcutHelpOpen] = useState(false); - const [workspaceExpanded, setWorkspaceExpanded] = useState(false); - const [settingsOpen, setSettingsOpen] = useState(false); - const [settingsInitialSection, setSettingsInitialSection] = useState('general'); - const [defaultAgent, setDefaultAgent] = useStoredValueState('defaultAgent', DEFAULT_AGENT_AUTO); - const [pendingComposerDraft, setPendingComposerDraft] = useState(''); - const { - leftSidebarCollapsed, - rightPanelOpen, - leftSidebarWidth, - activeRailView, - setLeftSidebarCollapsed, - setRightPanelOpen, - setLeftSidebarView, - setActiveRailView, - } = useUIStore( - useShallow((s) => ({ - leftSidebarCollapsed: s.leftSidebarCollapsed, - rightPanelOpen: s.rightPanelOpen, - leftSidebarWidth: s.sidebarWidth, - activeRailView: s.activeRailView, - setLeftSidebarCollapsed: s.setLeftSidebarCollapsed, - setRightPanelOpen: s.setRightPanelOpen, - setLeftSidebarView: s.setLeftSidebarView, - setActiveRailView: s.setActiveRailView, - })), - ); - const { handleStartResize, handleResizeKeyDown } = useSidebarResize(); - const [optimisticRun, setOptimisticRun] = useState(null); - const [runStartPending, setRunStartPending] = useState(false); - const [rightPanelMounted, setRightPanelMounted] = useState(rightPanelOpen); - const [workspaceWidth, setWorkspaceWidth] = useState(0); - const workspaceRef = useRef(null); - - // Mobile/tablet overlays - const [navPanelOpen, setNavPanelOpen] = useState(false); - - // Sync health → connection store - const prevOnlineRef = useRef(null); - useEffect(() => { - if (prevOnlineRef.current === online) return; - prevOnlineRef.current = online; - setOnline(online, health); - }, [health, online, setOnline]); - - // Sync isConnected → connection store - useEffect(() => { - setConnected(isConnected); - }, [isConnected, setConnected]); - - // Toast when new thread appears - const prevThreadIdsRef = useRef>(new Set()); - useEffect(() => { - if (!online || threads.length === 0) { prevThreadIdsRef.current = new Set(); return; } - const currentIds = new Set(threads.map((th) => th.threadId)); - const wasInitial = prevThreadIdsRef.current.size === 0; - if (!wasInitial) { - for (const th of threads) { - if (!prevThreadIdsRef.current.has(th.threadId)) { - if (silentCreatedThreadToastIdsRef.current.delete(th.threadId)) continue; - addToast({ type: 'success', message: t('toast.threadCreated') }); - } - } - } - prevThreadIdsRef.current = currentIds; - }, [threads, online, addToast, t]); - - useEffect(() => { - if (!threadData?.items) return; - const liveThreadIds = new Set(threads.map((thread) => thread.threadId)); - const knownThreadIds = new Set(liveThreadIds); - for (const threadId of pendingCreatedThreadIdsRef.current) { - knownThreadIds.add(threadId); - if (liveThreadIds.has(threadId)) pendingCreatedThreadIdsRef.current.delete(threadId); - } - useThreadStore.getState().pruneMissingThreads([...knownThreadIds]); - }, [threadData?.items, threads]); - - const selectedThread = threads.find((th) => th.threadId === activeThreadId); - const { data: threadItemData } = useThreadMessages(activeThreadId); - const { data: allRunsData } = useRuns(undefined, undefined, { enabled: online }); - const validDefaultAgentId = useMemo( - () => resolveAvailableDefaultAgentId(defaultAgent, agents), - [agents, defaultAgent], - ); - const effectiveSelectedAgentId = selectedAgentId ?? validDefaultAgentId; - const selectedAgent = agents.find((a) => a.id === effectiveSelectedAgentId); - const manuallySelectedAgent = selectedAgentId ? agents.find((a) => a.id === selectedAgentId) : undefined; - const displayedRun = currentRun ?? optimisticRun; - const runIsActive = isRunActiveStatus(displayedRun?.status); - const runCardConstrained = workspaceWidth > 0 && workspaceWidth < RUN_CARD_MIN_WORKSPACE_WIDTH; - const effectiveRightPanelOpen = rightPanelOpen && !runCardConstrained; - const showRunCardSpace = !!displayedRun && effectiveRightPanelOpen && !isMobile && !workspaceExpanded; - const effectiveSidebarCollapsed = leftSidebarCollapsed - || activeRailView === 'messages' - || activeRailView === 'team'; - const composerLocked = runStartPending || runIsActive; - const persistedMessages = useMemo( - () => buildChatMessagesFromThreadItems(threadItemData?.items ?? []), - [threadItemData?.items], - ); - const allMessages = useMemo(() => { - const merged = mergeChatMessages({ - persisted: persistedMessages, - optimistic: filterOptimisticMessagesForThread(userMessages, activeThreadId), - live: messages, - }); - const labeled = applyRuntimeAgentLabel(merged, selectedAgent?.name); - if (!displayedRun) return hideMessages(labeled, hiddenMessageIds); - - const hasVisibleAgentFeedback = messages.some( - (msg) => msg.role === 'agent' && msg.blocks.some((block) => block.kind !== 'session_init'), - ); - if (hasVisibleAgentFeedback) return hideMessages(labeled, hiddenMessageIds); - - const runOutputMessage = buildDisplayedRunOutputMessage(displayedRun, selectedAgent?.name); - if (!runOutputMessage) return hideMessages(labeled, hiddenMessageIds); - - return hideMessages([ - ...labeled, - runOutputMessage, - ], hiddenMessageIds); - }, [activeThreadId, displayedRun, hiddenMessageIds, messages, persistedMessages, selectedAgent?.name, userMessages]); - const effectiveLeftSidebarWidth = clamp(leftSidebarWidth, LEFT_SIDEBAR_MIN, LEFT_SIDEBAR_MAX); - const shellStyle = { - '--left-sidebar-width': `${effectiveLeftSidebarWidth}px`, - } as CSSProperties; - - useEffect(() => { - if (currentRun) setOptimisticRun(null); - }, [currentRun]); - - useEffect(() => { - if (effectiveRightPanelOpen) { - setRightPanelMounted(true); - return; - } - const timer = window.setTimeout(() => setRightPanelMounted(false), 220); - return () => window.clearTimeout(timer); - }, [effectiveRightPanelOpen]); - - useEffect(() => { - const node = workspaceRef.current; - if (!node || typeof ResizeObserver === 'undefined') return; - - const updateWidth = () => setWorkspaceWidth(node.getBoundingClientRect().width); - updateWidth(); - - const observer = new ResizeObserver((entries) => { - const entry = entries[0]; - if (entry) setWorkspaceWidth(entry.contentRect.width); - }); - observer.observe(node); - return () => observer.disconnect(); - }, []); - - const { - handleSend, - handleCancel, - handleRetry, - } = useSendRun({ - runStartPending, - runIsActive, - activeThreadId, - threads, - agents, - selectedAgentId: effectiveSelectedAgentId, - optimisticRun, - currentRun, - allMessages, - threadItemCount: threadItemData?.items?.length, - setRunStartPending, - setOptimisticRun, - setUserMessages, - selectThread, - addThreadToCache, - updateThreadInCache, - setThreadTitleInCache, - emptyCreatedThreadIdsRef, - manuallyNamedThreadIdsRef, - queryClient, - addToast, - t, - }); - - const setViewModeCompat = useCallback((mode: 'agent' | 'im') => { - setActiveRailView(mode === 'im' ? 'messages' : 'agents'); - }, [setActiveRailView]); - - const setLeftSidebarViewCompat = useCallback((v: 'home' | 'thread') => { - setLeftSidebarView(v); - if (v === 'thread' && activeRailView === 'home') { - setActiveRailView('agents'); - } - }, [activeRailView, setActiveRailView, setLeftSidebarView]); - - const { - handleSelectThread, - handleThreadTitleEdited, - handleSelectAgent, - handleCreateThread, - handleQuickChat, - handleForkThread, - handleStartLocalOrchestration, - handleSearchThreadSelect, - handleSearchMessageSelect, - openGlobalSearch, - } = useThreadNavigation({ - allMessages, - selectedAgentName: manuallySelectedAgent?.name, - selectedAgentId: selectedAgentId ?? null, - selectedThreadId: selectedThread?.threadId, - selectedThreadTitle: selectedThread?.title, - selectThread, - selectAgentThread, - setLeftSidebarView: setLeftSidebarViewCompat, - setViewMode: setViewModeCompat, - setPendingComposerDraft, - addThreadToCache, - emptyCreatedThreadIdsRef, - manuallyNamedThreadIdsRef, - queryClient, - addToast, - t, - agents, - }); - - const openSettings = useCallback((section: SettingsSectionId = 'general') => { - setSettingsInitialSection(section); - setSettingsOpen(true); - }, []); - - const handleOpenAuth = useCallback(() => { - useHubStore.getState().setShowAuthModal(true); - }, []); - - const handleOpenHubAccount = useCallback(() => { - if (hubAuthenticated) { - openSettings('account'); - return; - } - handleOpenAuth(); - }, [handleOpenAuth, hubAuthenticated, openSettings]); - - const desktopWindowAvailable = isTauriRuntime(); - - const { handleWindowCommand, handleEditCommand, handleCopyDiagnostics } = useDesktopCommands({ - online, - isConnected, - wsLatency, - healthVersion: health?.version, - selectedAgent, - selectedThread, - displayedRun, - }); - - const handleOpenFolder = useCallback(async () => { - if (!desktopWindowAvailable) { - addToast({ type: 'info', message: t('prompt.browseWorkDirUnavailable') }); - return; - } - try { - const selected = await (await import('@tauri-apps/plugin-dialog')).open({ directory: true, multiple: false }); - const workDir = Array.isArray(selected) ? selected[0] : selected; - if (typeof workDir !== 'string' || !workDir.trim()) return; - window.localStorage.setItem('agenthub.prompt.workDir', workDir); - window.dispatchEvent(new CustomEvent('agenthub:workdir-selected', { detail: { workDir } })); - addToast({ type: 'success', message: t('toast.workDirSelected') }); - focusComposer(); - } catch { - addToast({ type: 'error', message: t('toast.error') }); - } - }, [addToast, desktopWindowAvailable, t]); - - const handleReviewApprovalsFromHome = useCallback(() => { - if (permissionRequests.length === 0) { - openSettings('permissions'); - return; - } - setLeftSidebarView('thread'); - setActiveRailView('agents'); - if (displayedRun) setRightPanelOpen(true); - }, [displayedRun, openSettings, permissionRequests.length, setActiveRailView, setLeftSidebarView, setRightPanelOpen]); - - const handleDecidePermission = useCallback(async (requestId: string, decision: 'allow' | 'deny', reason?: string) => { - const request = permissionRequests.find((item) => item.requestId === requestId); - if (!request?.runId) { - addToast({ type: 'error', message: t('toast.error') }); - return; - } - try { - await decidePermissionRest({ runId: request.runId, requestId, decision, reason }); - decidePermission(requestId, decision, reason); - } catch { - addToast({ type: 'error', message: t('toast.error') }); - } - }, [addToast, decidePermission, permissionRequests, t]); - - const handleDelete = useCallback((messageId: string) => { - setUserMessages((prev) => prev.filter((m) => m.id !== messageId)); - hideMessage(messageId); - }, [hideMessage]); - - useEffect(() => { - if (!pendingComposerDraft || activeRailView !== 'agents') return undefined; - let attempts = 0; - let timer: number | undefined; - const tryApplyDraft = () => { - const textarea = document.querySelector('textarea[aria-label], textarea[placeholder]'); - if (!textarea) { - attempts += 1; - if (attempts < 12) timer = window.setTimeout(tryApplyDraft, 50); - return; - } - setComposerDraft(pendingComposerDraft); - focusComposer(); - setPendingComposerDraft(''); - }; - timer = window.setTimeout(tryApplyDraft, 0); - return () => { - if (timer !== undefined) window.clearTimeout(timer); - }; - }, [activeRailView, pendingComposerDraft]); - - const handleShareWorkspace = useCallback(async () => { - const title = selectedThread?.title ?? selectedAgent?.name ?? 'AgentHub'; - const summary = [ - `AgentHub: ${title}`, - selectedThread ? `Thread: ${selectedThread.threadId}` : null, - selectedAgent ? `Agent: ${selectedAgent.name}` : null, - ].filter(Boolean).join('\n'); - try { - await navigator.clipboard.writeText(summary); - addToast({ type: 'success', message: t('toast.copied') }); - } catch { - addToast({ type: 'error', message: t('toast.error') }); - } - }, [addToast, selectedAgent, selectedThread, t]); - - const topMenus = useTopMenuConfig({ - desktopWindowAvailable, - displayedRun, - handleCopyDiagnostics, - handleCreateThread, - handleEditCommand, - handleOpenHubAccount, - handleOpenFolder, - handleQuickChat, - handleWindowCommand, - leftSidebarCollapsed, - online, - openSettings, - rightPanelOpen, - setLeftSidebarCollapsed, - setRightPanelOpen, - setShortcutHelpOpen, - setWorkspaceExpanded, - t, - theme, - toggleTheme, - workspaceExpanded, - }); - - useShellShortcuts({ - online, - isMobile, - workspaceExpanded, - leftSidebarCollapsed, - rightPanelOpen, - shortcutHelpOpen, - displayedRun, - handleCreateThread, - handleQuickChat, - handleOpenFolder, - handleWindowCommand: handleWindowCommand as (command: string) => Promise, - openSettings: openSettings as (section?: string) => void, - setNavPanelOpen, - setShortcutHelpOpen, - setLeftSidebarCollapsed, - setRightPanelOpen, - }); - - // ── Double-click top bar → toggle maximize/restore - const handleTopBarDoubleClick = useCallback(async (e: React.MouseEvent) => { - const target = e.target as HTMLElement; - if (target.closest('button, input, select, a')) return; - try { - const w = getCurrentWindow(); - if (await w.isMaximized()) { - await w.unmaximize(); - } else { - await w.maximize(); - } - } catch { - // Tauri window APIs are unavailable in browser-only test environments. - } - }, []); - - // ── Render ───────────────────────────────── + const [selectedConversationId, setSelectedConversationId] = useState(); + const workbench = useDesktopWorkbenchModel(selectedConversationId); + const createRun = useCreateRun(); + const desktopPlatform = useMemo(() => createDesktopPlatform({ + ...(workbench.activeProjectId ? { activeProjectId: workbench.activeProjectId } : {}), + ...(workbench.activeThreadId ? { activeThreadId: workbench.activeThreadId } : {}), + submitRun: createRun.mutateAsync, + }), [createRun.mutateAsync, workbench.activeProjectId, workbench.activeThreadId]); return ( - -
- - {/* Top status bar — drag region + window controls */} -
-
- {!isMobile && !workspaceExpanded && ( - setLeftSidebarCollapsed(!leftSidebarCollapsed)} - label={leftSidebarCollapsed ? t('nav.expandSidebar') : t('nav.collapseSidebar')} - tooltipSide="bottom" - aria-expanded={!leftSidebarCollapsed} - > - {leftSidebarCollapsed ? : } - - )} - - - - - {online ? `Edge ${health?.version ?? 'v1'}` : t('status.offline')} - - {wsLatency != null && {wsLatency}ms} - {isConnected ? : } - {edgeStatus.lastError && } -
-
- {/* Window controls — no drag region so clicks register */} -
- getCurrentWindow().minimize()} label={t('window.minimize')} tooltipSide="bottom"> - - - { - const w = getCurrentWindow(); - if (await w.isMaximized()) { - await w.unmaximize(); - } else { - await w.maximize(); - } - }} label={t('window.maximize')} tooltipSide="bottom"> - - - getCurrentWindow().close()} label={t('window.close')} tooltipSide="bottom"> - - -
-
-
- - {edgeStatus.showBanner && ( -
-
- )} - - + - - {settingsOpen ? ( - - setSettingsOpen(false)} - onOpenAuth={handleOpenAuth} - defaultAgent={defaultAgent} - setDefaultAgent={setDefaultAgent} - /> - - ) : ( - <> - - {/* Mobile toolbar */} - {isMobile && ( -
- setNavPanelOpen(true)} label={t('nav.openMenu')} aria-expanded={navPanelOpen}> - - - openGlobalSearch()} label={t('search.openGlobal')}> - - - {selectedAgent?.name ?? 'AgentHub'} - openSettings('general')} label={t('nav.settings')}> - - - - {hubAuthenticated ? : } - - - {theme === 'dark' ? : } - -
- )} - - {/* Mobile nav overlay */} - {isMobile && ( - <> -
setNavPanelOpen(false)} /> -
-
-
-
- -
-
-
-
- -
-
-
-
- - )} - -
- {/* Global Rail — icon-only navigation */} - {!isMobile && !workspaceExpanded && ( -
- setActiveRailView('home')} - label={t('nav.home')} - tooltipSide="right" - aria-pressed={activeRailView === 'home'} - > - - - setActiveRailView('messages')} - label={t('im.groupChat')} - tooltipSide="right" - aria-pressed={activeRailView === 'messages'} - > - - - setActiveRailView('agents')} - label={t('nav.agent')} - tooltipSide="right" - aria-pressed={activeRailView === 'agents'} - > - - - setActiveRailView('team')} - label={teamRunButtonLabel} - tooltipSide="right" - aria-pressed={activeRailView === 'team'} - > - - - {teamRunBadgeCount > 0 ? ( - - {teamRunBadgeCount > 9 ? '9+' : teamRunBadgeCount} - - ) : null} - - -
- openSettings('general')} - label={t('nav.settings')} - tooltipSide="right" - > - - - - {hubAuthenticated ? : } - - - {theme === 'dark' ? : } - -
- )} - - {/* Conversation Sidebar — agents + threads */} - {!isMobile && !workspaceExpanded && !effectiveSidebarCollapsed && ( - <> -
- {/* Global search */} - - - {/* Agents section */} -
-
- -
-
- - {/* Threads section */} -
-
- -
-
-
-
- - )} - - {/* Main zone */} -
-
- {/* Workspace header */} -
-
-

{selectedAgent ? selectedAgent.name : 'AgentHub'}

- {selectedThread && {selectedThread.title}} -
- - - - openSettings('tasks')} - label={t('settings.tasks')} - tooltipSide="bottom" - > - - - {displayedRun && !effectiveRightPanelOpen && ( - setRightPanelOpen(true)} - label={t('run.open')} - tooltipSide="bottom" - aria-expanded={effectiveRightPanelOpen} - > - - - )} - {displayedRun && effectiveRightPanelOpen && ( - setRightPanelOpen(false)} - label={t('run.close')} - tooltipSide="bottom" - aria-expanded={effectiveRightPanelOpen} - > - - - )} - setWorkspaceExpanded((v) => !v)} - label={workspaceExpanded ? t('workspace.collapse') : t('workspace.expand')} - tooltipSide="bottom" - aria-pressed={workspaceExpanded} - > - {workspaceExpanded ? : } - -
-
- - {/* Content: transcript + inline inspector */} -
-
- {/* Transcript */} -
- {activeRailView === 'home' ? ( - - { - try { - const thread = await createThread(); - addThreadToCache(thread, { empty: true }); - handleSelectThread(thread.threadId); - } catch { - // continue - } - }} - onSelectThread={handleSelectThread} - onQuickStart={async (prompt) => { - const title = buildAutomaticThreadTitle(prompt); - try { - const thread = await createThread(title ?? undefined); - addThreadToCache(thread); - handleSelectThread(thread.threadId); - await handleSend(prompt, effectiveSelectedAgentId ?? undefined, { - threadId: thread.threadId, - threadInfo: thread, - createdEmptyThread: true, - }); - } catch { - addToast({ type: 'error', message: t('toast.error') }); - } - }} - onViewRuns={() => openSettings('tasks')} - onReviewApprovals={handleReviewApprovalsFromHome} - onViewAllThreads={() => setActiveRailView('agents')} - onOpenTeamRuns={() => setActiveRailView('team')} - onOpenHubAccount={handleOpenHubAccount} - permissionCount={permissionRequests.length} - agentTeamOverview={agentTeamsQuery.data} - agentTeamsLoading={agentTeamsQuery.isLoading || agentTeamsQuery.isFetching} - agentTeamsSignedIn={hubInventoryEnabled} - agents={agents} - selectedAgentId={effectiveSelectedAgentId ?? undefined} - onSelectAgent={handleSelectAgent} - onStartLocalOrchestration={handleStartLocalOrchestration} - /> - - ) : activeRailView === 'messages' ? ( - - ) : activeRailView === 'team' ? ( - - ) : ( - - )} -
- - {/* Input area — only for agent chat */} - {activeRailView === 'agents' && ( -
- -
- )} -
- - {/* Inline Inspector Panel */} - {!isMobile && !workspaceExpanded && displayedRun && rightPanelMounted && ( -
-
- -
}> - - - -
-
- )} -
-
-
-
- - )} - - {/* Modals */} - - - - - setShortcutHelpOpen(false)} /> - - {showAuthModal && ( -
useHubStore.getState().setShowAuthModal(false)}> -
e.stopPropagation()}> - - useHubStore.getState().setShowAuthModal(false)} - onClose={() => useHubStore.getState().setShowAuthModal(false)} - /> - -
-
- )} - -
- + ); } diff --git a/app/desktop/src/__e2e__/smoke.spec.ts b/app/desktop/src/__e2e__/smoke.spec.ts index ce3356f5..87fe5377 100644 --- a/app/desktop/src/__e2e__/smoke.spec.ts +++ b/app/desktop/src/__e2e__/smoke.spec.ts @@ -18,7 +18,7 @@ test.describe('AgentHub Desktop smoke', () => { await expect(statusBar).toHaveAttribute('aria-atomic', 'true'); }); - test('PromptInput is visible and has textarea', async ({ page }) => { + test('v4 composer is visible and has textarea', async ({ page }) => { await page.goto('/'); // The textarea exists but may be disabled when the backend is offline. // Verify it is present in the DOM and visible. @@ -28,9 +28,9 @@ test.describe('AgentHub Desktop smoke', () => { await expect(textarea).toHaveValue(''); }); - test('agent selector button exists', async ({ page }) => { + test('agent mention button exists', async ({ page }) => { await page.goto('/'); - // The PromptInput renders a button that shows @Agent (or @) to open the agent selector + // The v4 composer renders a button that shows @Agent (or @) to open the agent selector. const agentBtn = page.locator('button').filter({ hasText: '@' }); await expect(agentBtn).toBeVisible(); // The button displays the current agent or the placeholder "@Agent" @@ -38,13 +38,12 @@ test.describe('AgentHub Desktop smoke', () => { expect(btnText).toMatch(/@(Agent|\w+)/); }); - test('ThreadPanel is rendered', async ({ page }) => { + test('v4 sidebar navigation is rendered', async ({ page }) => { await page.goto('/'); - // Both ThreadPanel and AgentList render