diff --git a/.agents/skills/dev-loop/SKILL.md b/.agents/skills/dev-loop/SKILL.md new file mode 100644 index 000000000..a61dd453e --- /dev/null +++ b/.agents/skills/dev-loop/SKILL.md @@ -0,0 +1,129 @@ +--- +name: dev-loop +description: "自主开发推进引擎——ROADMAP 驱动、模型分配、并行 subagent 协作、交叉审查、文档同步、自动沉淀。长程多步骤任务使用 /dev-loop 启动。短任务(单文件修复、小改动)不需要——直接做。" +--- + +# Dev Loop — 自主开发推进引擎 + +> 短任务(单文件修复、typo、小改动)**不需要这个 skill**——直接做,别绕弯。 +> 长程任务(跨文件重构、多步骤功能、需要审查的变更)用它。 + +## 模型分配策略 + +| 别名 | 后端 | 上下文 | 角色 | 策略 | +|---|---|---|---|---| +| **opus** | deepseek-v4-pro | 1M | 推理/架构/审查 | 稳定可靠 | +| **sonnet** | deepseek-v4-flash | 1M | 快速并行执行 | 批量机械工作 | +| **haiku** | glm-5.1 | **200k** | 简短复杂逻辑/业务编码 | 优先编码,上下文小是硬限制 | + +## CC 原生工具配合 + +dev-loop 配合两个 CC 内置命令使用效果最好: + +### `/goal` — 会话目标锁 +- 用法:`/goal "完成 server 文档中文化"` +- 设置后,会话未达目标前 `/stop` 被阻止 +- 适用于:一个明确的、可在本次会话完成的目标 +- 完成后自动解除。中途变更目标用 `/goal` 重新设置 + +### `/loop` — 定时自触发 +- 用法:`/loop 10m 继续完善项目,运行测试,提交代码` +- 适用于:需要持续关注的项目、长时间运行的迭代任务 +- 选择间隔时注意:< 5 分钟保持 prompt cache 热,> 5 分钟 cache 失效但有更长自主窗口 +- 如果没有具体的外部信号需要轮询,默认 20-30 分钟 + +**组合使用:** `/goal "完成翻译并清理"` + `/loop 10m /dev-loop 继续推进剩下的工作` + +## 标准工作循环 + +### 1. 理解 +- 读 AGENTS.md / CLAUDE.md / README.md / ROADMAP.md / STATE.md +- 理解现有架构、约定、当前进度 + +### 2. 规划 +- 长期任务:创建 ROADMAP.md 或在现有 ROADMAP 中登记任务 +- 本次会话目标明确 → `/goal "描述"` 锁定,防止中途退出 +- 用平台 goal/todo 工具跟踪 session 状态(ROADMAP.md 是跨 session 权威) +- 从 ROADMAP 中选取 1-3 个最高价值任务 +- 不确定的设计先做轻量探索(只读 agent) + +### 3. 执行 +- **自己(opus 主 session)**:设计决策、审查输出、编辑核心文件(AGENTS.md/STATE.md/ROADMAP.md) +- **派 opus subagent**:复杂功能、架构重构、安全审查、多维度审计 +- **派 sonnet subagent**:批量机械工作(翻译、格式化、测试生成、重命名) +- **派 haiku subagent**:编码实现、bug 修复、算法。优先用 haiku,实际失败才换 opus +- 每次 subagent 完成后审查其输出 + +### 4. 审查 +- 完成一批变更后启动交叉审查:4-5 个 opus agent 并行 +- 维度:结构、文档、安全、架构、易用性 +- 让其他 agent 提问题:"审查这个变更,列出你担心的问题" +- 修复高优先级项 + +### 5. 同步 +- AGENTS.md / CLAUDE.md(规则变更) +- STATE.md(事实变更) +- ROADMAP.md(标记完成、记录阻塞、写下一步) +- 运行 `neat-freak` 清理过时文档 +- 运行 `memory-management` 同步 memory(如有跨系统需求) +- Git:小范围 commit,及时 push + +### 6. 沉淀 +- 发现可复用的 SOP → 写成项目级 skill +- Skill 不含本机路径、凭据、IP——别人也能用 +- 自己调用测试,迭代描述和触发条件 + +## Worktree 指南 + +**什么时候用:** +- 跨 session 重构(跨天、跨对话) +- 可能破坏主分支的实验性变更 +- 并行开发多个独立功能 + +**什么时候不用:** +- 单文件修复、文档变更 → 直接在主分支 +- 90% 的工作不需要 worktree + +## 审查模式 + +### 交叉审查维度与模型 +| 维度 | 模型 | 为什么 | +|---|---|---| +| 结构 | sonnet | 机械检查,批量扫文件 | +| 文档 | sonnet | 一致性检查,不重推理 | +| 安全 | **opus** | 必须深度推理 | +| 架构 | **opus** | 需要设计判断 | +| 易用性 | sonnet | 清单式检查 | +| 业务逻辑 | **haiku** | 简短复杂逻辑审查 | + +审查 agent 的 prompt 要具体:告诉它查什么、怎么报告、文件在哪。 + +### 自我质疑 +- "新 agent 能理解吗?" "STATE.md 需要更新吗?" +- "有文件引用过时路径吗?" "memory 同步了吗?" +- "这个改动影响了其他项目吗?" + +## 产品思维 + +- 用户第一次用怎么想?错误信息有用吗? +- 测试覆盖了真实场景还是只测 happy path? +- E2E 能跑通完整用户流程吗?界面一致吗? + +## Git 约定 + +- `main` 稳定,`dev` 开发;小范围 commit,及时 push +- 不用 `--force`、`--no-verify` +- Commit message 写"为什么"不是"做了什么" + +## 自主边界 + +**无需确认可直接做:** 读文件、写代码、加测试、更新文档、跑本地命令、派 subagent、小范围 commit。 + +**必须暂停等确认:** 生产部署、破坏性数据变更、secret 处理、不可逆迁移、超出 ROADMAP 范围的大重写。 + +**受阻时:** 先尝试直接解决。经过具体尝试仍受阻后,把阻塞点和下一步写入 ROADMAP.md,再问用户。 + +## 参考 + +- `references/model-strategy.md` — 模型选择决策树 + fallback 策略 +- `references/review-checklist.md` — 代码审查清单 diff --git a/.agents/skills/dev-loop/references/model-strategy.md b/.agents/skills/dev-loop/references/model-strategy.md new file mode 100644 index 000000000..da0788ad9 --- /dev/null +++ b/.agents/skills/dev-loop/references/model-strategy.md @@ -0,0 +1,73 @@ +# 模型选择决策树 + +最后更新:2026-05-23 + +## 可用模型 + +| 别名 | 实际后端 | 上下文 | 优势 | 限制 | +|---|---|---|---|---| +| **opus** | deepseek-v4-pro | 1M | 深度推理、架构设计、安全审查、复杂重构 | 慢、贵 | +| **sonnet** | deepseek-v4-flash | 1M | 快速并行执行、批量操作、格式统一 | 编码能力不如 haiku | +| **haiku** | glm-5.1 | **200k** | 简短复杂逻辑、业务编码、算法 | **上下文小**、不稳定 | + +## Haiku 定位 + +haiku 专精于**短上下文内的复杂逻辑**——单个函数的精巧算法、棘手的业务规则、深度 bug 分析。不适合大批量文件操作或需要加载大量参考文档的任务。 + +**能用 haiku 的:** +- 单个函数/模块的复杂算法实现 +- 聚焦的 bug 修复(错误 + 相关代码 < 200k) +- 业务逻辑审查(单个文件或小范围) +- 代码片段优化 + +**不能用 haiku 的:** +- 跨文件重构(上下文不够) +- 批量文件操作(3+ 文件可能溢出) +- 需要加载多个参考文档的任务 +- 翻译超过 5 个文件的批次 + +**原则:haiku = 小而精。短上下文、高复杂度、单点突破。大任务→sonnet/opus。** + +## Haiku Fallback + +haiku 编码强但不稳定。首次编码用 haiku(前提:不超 200k)。返回乱码/截断→不重试,直接换 opus。批量大上下文→用 sonnet。 + +## 决策流程 + +``` +任务类型? +├── 设计/架构/审查类 +│ └── → opus(主 session 或 opus subagent) +├── 批量机械操作(翻译、格式化、重命名) +│ ├── 5 个文件以内 → sonnet subagent +│ └── 多文件大规模 → 拆批,每批 sonnet subagent +├── 编码实现/功能开发 +│ ├── 单文件复杂逻辑 → haiku subagent(优先) +│ ├── 小范围业务编码(1-2 文件)→ haiku subagent +│ ├── 多文件重构 → opus subagent(haiku 上下文不够) +│ └── haiku 不稳定 → 换 opus +├── 交叉审查 +│ ├── 安全/架构/业务逻辑 → opus(需深度推理) +│ └── 结构/文档/易用性 → sonnet(机械检查) +├── 探索/搜索 +│ ├── 单个目标明确 → 自己做(Grep/Glob) +│ ├── 多维度搜索 → 2-3 sonnet 并行 Explore agent +│ └── 深度代码理解 → 1 opus Explore agent +└── 安全审查 + └── → opus subagent(必须,不能跳过) +``` + +## 并行度 + +- 独立任务:最大并行数 = 任务数 +- 审查任务:4-5 维度同时跑,opus 管安全/架构,sonnet 管结构/文档/易用性 +- 翻译任务:按文件数分片,~20-40 文件/sonnet subagent +- haiku 子任务:每次只给 1-2 个文件,prompt 精简 + +## 上下文管理 + +| 模型 | 上限 | 策略 | +|---|---|---| +| haiku | 200k | 精简 prompt,只传必要文件,不传大型参考文档 | +| sonnet | 1M | 可传大量文件,适合批量操作 | +| opus | 1M | 适合深度分析大型代码库 | diff --git a/.agents/skills/dev-loop/references/review-checklist.md b/.agents/skills/dev-loop/references/review-checklist.md new file mode 100644 index 000000000..9ca78258d --- /dev/null +++ b/.agents/skills/dev-loop/references/review-checklist.md @@ -0,0 +1,53 @@ +# 代码审查清单 + +最后更新:2026-05-23 + +## 结构审查 + +- [ ] 新文件放在了正确的位置? +- [ ] 有没有死代码(未使用的函数、导入、变量)? +- [ ] 命名一致吗?(kebab-case、PascalCase、camelCase) +- [ ] 有没有 TODO/FIXME 没有对应 issue? +- [ ] 测试文件在正确的位置? + +## 文档审查 + +- [ ] README 的安装步骤能跑通吗? +- [ ] AGENTS.md / CLAUDE.md 需要更新吗? +- [ ] STATE.md 事实变更了吗? +- [ ] ROADMAP.md 需要标记完成吗? +- [ ] 新增 API/命令有文档吗? +- [ ] CHANGELOG 更新了吗? + +## 安全审查 + +- [ ] 有没有硬编码凭据(密码、API key、token)? +- [ ] 有没有 `.env` 或 `config.json` 被提交? +- [ ] 用户输入有校验吗? +- [ ] SQL 有注入风险吗? +- [ ] 错误信息泄露了内部实现细节吗? +- [ ] 第三方依赖是最新的吗? + +## 架构审查 + +- [ ] 新代码遵循现有模式吗? +- [ ] 有没有循环依赖? +- [ ] 错误处理一致吗? +- [ ] 超时设置合理吗? +- [ ] 并发安全吗?(锁、竞态条件) +- [ ] 接口向后兼容吗? + +## 易用性审查 + +- [ ] 错误信息对用户有用吗? +- [ ] 首次使用体验顺畅吗? +- [ ] 帮助文本/usage 准确吗? +- [ ] 命名直观吗? + +## 测试审查 + +- [ ] 有单元测试吗? +- [ ] 有端到端测试覆盖关键路径吗? +- [ ] 测试了错误情况还是只测 happy path? +- [ ] Mock 合理吗?有没有 mock 了不该 mock 的? +- [ ] 测试能独立运行吗?不依赖特定环境? diff --git a/.codex/skills/set-goal/SKILL.md b/.codex/skills/set-goal/SKILL.md deleted file mode 100644 index df66906ee..000000000 --- a/.codex/skills/set-goal/SKILL.md +++ /dev/null @@ -1,110 +0,0 @@ ---- -name: set-goal -description: Use when 用户要求持续推进 AgentHub、按路线图开发、协调 subagent、使用 worktree、自己迭代、自己 review、交叉 review、验证、更新文档或收口 git。 ---- - -# Set Goal - -这个 skill 用来启动仓库级持续开发循环。它不是普通的一次性修 bug 流程,而是让 Agent 按路线图持续推进、自己拆任务、自己验证、自己 review、必要时调度 subagent,并把进展写回仓库。 - -## 核心规则 - -AgentHub 的持续开发台账放在 `docs/roadmap.md` 和 `docs/roadmaps/`,不是根目录 `ROADMAP.md`。 - -每次触发时,先尝试设置平台运行时 goal。平台 goal 如果可用,必须包含这句话: - -```text -使用当前仓库的 docs/roadmap.md 和 docs/roadmaps/ 记录进展、决策、验证结果和下一步。 -``` - -如果平台 goal 工具不可用或报错,不要停止;继续使用 `docs/roadmap.md` 和对应方向路线图。 - -## 触发场景 - -以下情况使用本 skill: - -- 用户调用 `set-goal` -- 用户说“持续开发”“综合推进项目”“继续按路线图推进” -- 用户要求 Agent 自己决定下一步、自己 review、自己提交 -- 用户要求多 Agent / subagent / worktree 并行推进 -- 当前任务跨多步,需要实现、验证、文档、review 和 git 收口 - -普通小修小补不需要强行触发,除非用户明确要求持续推进。 - -## 路线图位置 - -- 总路线图:`docs/roadmap.md` -- 前端路线图:`docs/roadmaps/frontend.md` -- 后端路线图:`docs/roadmaps/backend.md` -- 客户端路线图:`docs/roadmaps/client.md` -- 分支路线图:`docs/roadmaps/branches/.md` - -分支名里的 `/` 在文件名中改成 `-`,例如: - -```text -codex/m2-edge-data-layer -> docs/roadmaps/branches/codex-m2-edge-data-layer.md -``` - -## 执行入口 - -按顺序读取: - -1. `AGENTS.md` -2. `README.md` -3. `docs/roadmap.md` -4. 当前任务所属方向的路线图 -5. 当前分支路线图 -6. 相关主文档、API 文档、代码和测试 - -然后加载本 skill 的 reference: - -1. `references/platform-goals.md` -2. `references/roadmap-driven-loop.md` -3. `references/agent-development-loop.md` -4. `references/review-and-git-gates.md` - -## 默认循环 - -每轮只选 1 到 3 个高价值任务,按这个顺序推进: - -1. 确认当前分支、工作区和路线图。 -2. 选择本轮任务,写清写入范围和验收命令。 -3. 需要时使用 `.worktrees/` 和 subagent,但写入范围必须互不重叠。 -4. 先做基线验证,再实现。 -5. 实现后运行最小相关测试。 -6. 自我 review:检查 diff、边界、错误处理、文档和隐私。 -7. 交叉 review:非平凡改动必须让独立 reviewer 或 subagent 检查。 -8. 修复 review 发现的高优先级问题。 -9. 重新验证。 -10. 更新路线图、相关文档和验收记录。 -11. 检查 git status,提交并按项目规则 push。 - -## 自主边界 - -可以直接执行: - -- 读文件、改代码、写测试、更新文档 -- 创建或更新 `docs/roadmaps/**` -- 运行本地检查和测试 -- 调度 subagent 做明确、窄范围的实现或 review -- 在当前分支提交小而完整的增量 - -必须暂停确认: - -- 部署到生产环境 -- 删除数据或执行不可逆迁移 -- 处理真实密钥、token、私有服务器信息 -- force push 共享分支 -- 超出当前路线图的大范围重写 - -## 失败处理 - -如果遇到阻塞: - -1. 先用具体命令复现。 -2. 判断是环境、依赖、权限、测试、设计还是实现问题。 -3. 尝试一个最小可逆修复。 -4. 仍无法解决时,把错误原文、已尝试动作、下一步写入分支路线图。 -5. 再向用户说明阻塞。 - -不要把“我觉得可能”写成事实;路线图只记录可复用、可验证的信息。 diff --git a/.codex/skills/set-goal/references/agent-development-loop.md b/.codex/skills/set-goal/references/agent-development-loop.md deleted file mode 100644 index 719ef28f9..000000000 --- a/.codex/skills/set-goal/references/agent-development-loop.md +++ /dev/null @@ -1,118 +0,0 @@ -# Agent 开发循环 - -本文件把 AgentHub 的持续开发方式写成可执行循环。目标是让 Agent 不停在“分析完了”,而是推进一个可验证增量。 - -## 1. 总循环 - -```text -读取规则 - -> 读取路线图 - -> 选择本轮任务 - -> 确认写入范围 - -> 基线验证 - -> 实现 - -> 自我 review - -> 交叉 review - -> 修复 - -> 重新验证 - -> 更新路线图和文档 - -> git 收口 -``` - -每轮宁愿小,也要闭环。 - -## 2. 任务拆解 - -拆任务时必须写清: - -- 目标 -- 所属方向:前端、后端、客户端 -- 分支名和 worktree -- 允许修改的路径 -- 不允许修改的路径 -- 依赖的文档或接口 -- 验收命令 -- 隐私红线 - -任务过大时继续拆,直到一个 Agent 能在一个分支里完成并验证。 - -## 3. Subagent 分工 - -subagent 只做窄任务。提示必须包含完整上下文,不能要求它自己猜。 - -实现 subagent: - -```text -目标:<明确功能> -允许修改:<路径> -禁止修改:<路径> -必读:<文档> -验收:<命令> -隐私红线:不得提交密钥、本机路径、真实服务器信息 -完成后汇报:改了哪些文件、跑了哪些检查、还有什么风险 -``` - -review subagent: - -```text -审查范围: -审查维度:结构 / 文档 / 包装 / 易用性 / 架构 -输出:按 Critical / High / Medium / Low 分级,给文件和行号 -不要修改文件,除非主 Agent 明确授权 -``` - -## 4. 两段式 Review - -实现后先查“有没有做对”,再查“做得好不好”。 - -第一段:规格 review - -- 是否满足任务目标。 -- 是否超出写入范围。 -- 是否漏掉验收命令。 -- 是否改变未声明的 API 或行为。 - -第二段:质量 review - -- 结构是否清晰。 -- 错误处理是否完整。 -- 测试是否覆盖关键失败路径。 -- 文档是否同步。 -- 是否存在安全和隐私问题。 - -任一 review 发现 Critical / High,必须修复后复审。 - -## 5. 自我迭代 - -Agent 不能只改一次就结束。每轮至少做一次自我迭代: - -1. 看自己的 diff。 -2. 找一个最可能被 reviewer 打回的问题。 -3. 修掉它。 -4. 再运行相关检查。 - -如果没有发现问题,也要在分支路线图或最终汇报中写清“已检查的维度”。 - -## 6. 文档同步 - -改行为、接口、目录、流程时必须同步文档。 - -优先级: - -1. `api/`:接口和事件变化。 -2. 三份主文档:产品需求、系统架构、功能实现。 -3. `AGENTS.md`:开发规则变化。 -4. `docs/roadmap.md` 和方向/分支路线图:进度变化。 - -不要把长方案塞进路线图;路线图只记录状态和下一步。 - -## 7. 结束条件 - -只有满足以下条件,才算本轮结束: - -- 任务完成或阻塞写清。 -- 自我 review 完成。 -- 非平凡改动完成交叉 review。 -- 验证命令运行并记录结果。 -- 路线图更新。 -- git 状态干净,或剩余改动有明确说明。 diff --git a/.codex/skills/set-goal/references/platform-goals.md b/.codex/skills/set-goal/references/platform-goals.md deleted file mode 100644 index 0220ee879..000000000 --- a/.codex/skills/set-goal/references/platform-goals.md +++ /dev/null @@ -1,72 +0,0 @@ -# 平台 Goal 规则 - -`set-goal` 可以在不同 Agent 平台运行。不同平台的 goal / todo 工具不一样,但行为必须一致:平台运行时 goal 只是会话辅助,仓库里的路线图才是跨会话依据。 - -## 必须写入的 goal 文本 - -如果当前平台支持 goal 或 todo 状态,创建或更新 goal 时必须包含: - -```text -使用当前仓库的 docs/roadmap.md 和 docs/roadmaps/ 记录进展、决策、验证结果和下一步。 -``` - -推荐目标: - -```text -按照当前仓库的 docs/roadmap.md 和 docs/roadmaps/ 持续推进 AgentHub,完成一个可验证的小增量,并同步测试、文档、review 和 git 状态。 -``` - -## Codex - -Codex 可用时: - -- 优先创建一个具体、当前仓库范围内的 active goal。 -- 使用 plan / todo 工具维护本轮执行清单。 -- 同一时间只保留一个 `in_progress` 任务。 -- 如果 goal 工具失败,继续用 plan 和 `docs/roadmaps/**`;不要因为运行时 goal 写不进去而停止。 - -### Codex goal 开关和迁移排障 - -Codex 官方 Goal mode 通过 `/goal` 入口使用。正常情况下新版 Codex CLI / Desktop 会默认启用;如果 slash command 里没有 `/goal`,先检查本机 `~/.codex/config.toml`: - -```toml -[features] -goals = true -``` - -改完配置后必须完全重启 Codex App 或当前 CLI 会话。只改配置但不重启,旧 app-server 进程仍可能继续使用旧 feature set。 - -如果创建 goal 报错: - -```text -no such table: thread_goals -``` - -按这个顺序处理: - -1. 检查版本:`codex --version`。优先升级到 `0.133.0` 或更新版本。 -2. 检查 `~/.codex/goals_1.sqlite` 是否已有 `thread_goals` 表。 -3. 检查 `~/.codex/state_5.sqlite` 的迁移记录里是否已有 `drop thread goals`。 -4. 如果 `goals_1.sqlite.thread_goals` 存在,但当前线程仍报缺表,通常是旧 Desktop/app-server 进程仍在查旧 `state_5.sqlite`;先完全退出并重启 Codex。 -5. 如果必须让当前旧线程继续工作,可以先备份 `state_5.sqlite`,再给旧库补一个与 `goals_1.sqlite.thread_goals` 同结构的兼容表。这个操作只是本机兼容 workaround,不要写进仓库脚本,也不要把本机 sqlite 文件提交。 - -官方参考: - -- `https://developers.openai.com/codex/use-cases/follow-goals` -- `https://developers.openai.com/codex/cli/slash-commands` -- `https://developers.openai.com/codex/changelog` - -## Claude Code / DeepSeek / 其它 Agent - -如果平台没有 goal 工具: - -- 在第一条进度说明里声明当前目标。 -- 维护本轮短 checklist。 -- 使用 `docs/roadmap.md`、方向路线图和分支路线图作为持久台账。 - -## 卫生规则 - -- goal 不能大于当前仓库范围。 -- goal 不能替代路线图。 -- 不读 `AGENTS.md`、`README.md` 和路线图,不得新开方向。 -- 只把可复用、可验证的信息写进路线图;命令噪音和临时猜测不写。 diff --git a/.codex/skills/set-goal/references/review-and-git-gates.md b/.codex/skills/set-goal/references/review-and-git-gates.md deleted file mode 100644 index bbeb9a822..000000000 --- a/.codex/skills/set-goal/references/review-and-git-gates.md +++ /dev/null @@ -1,127 +0,0 @@ -# Review 和 Git 门槛 - -本文件把交叉 review、验证和 git 收口规则内嵌到仓库级 skill 中。不要依赖外部 skill 是否安装。 - -## 1. Worktree 门槛 - -以下情况使用 `.worktrees/`: - -- 多会话开发 -- 改动跨多个模块 -- 需要多个 Agent 并行写不同路径 -- 当前工作区已有无关改动 -- 回滚和对比成本明显 - -小文档改动或单文件修复可以在当前工作区完成。 - -创建前: - -1. 同步 `master`。 -2. 确认 `.worktrees/` 已被忽略。 -3. 记录任务卡和写入范围。 -4. 在 worktree 中运行基线检查。 - -## 2. Subagent 门槛 - -可以使用 subagent,但必须满足: - -- 每个 subagent 任务自包含。 -- 写入范围明确且互不重叠。 -- 允许修改的路径写在提示里。 -- 必读文档、验收命令、隐私红线写在提示里。 -- 主 Agent 负责最终集成、验证和提交。 - -不要为了仪式感派 subagent。任务紧耦合时主 Agent 自己做。 - -## 3. 自我 Review - -提交前主 Agent 必须自己看 diff: - -- 是否改了未授权路径。 -- 是否留下 TODO、调试输出、死代码。 -- 是否破坏 API、事件名、路径或配置。 -- 是否缺少测试或确定性检查。 -- 是否泄露本机路径、密钥、真实服务器信息。 -- 文档是否和实现一致。 - -自我 review 不能替代交叉 review。 - -## 4. 交叉 Review - -非平凡改动必须做交叉 review。可以由 subagent、另一个 Agent 或主 Agent 的独立 review pass 完成。 - -至少覆盖这些维度: - -| 维度 | 检查重点 | -|---|---| -| structure | 目录、包边界、死代码、测试缺口 | -| docs | README、AGENTS、路线图、API 文档是否同步 | -| packaging | setup、CI、脚本、依赖、构建入口 | -| usability | 新人能否按文档运行,错误信息是否可理解 | -| architecture | Hub-Edge-Runner 边界、权限、超时、隐私 | - -问题分级: - -| 级别 | 处理 | -|---|---| -| Critical | 本轮必须修 | -| High | 本轮优先修 | -| Medium | 可修则修,否则写入路线图 | -| Low | 只记录,不阻塞 | - -## 5. 验证门槛 - -没有新鲜验证证据,不得声称完成。 - -流程: - -1. 找到能证明结果的命令。 -2. 运行完整命令。 -3. 阅读输出和退出码。 -4. 结果不通过就说真实状态。 -5. 结果通过才写“通过”。 - -常用最小检查: - -```powershell -git diff --check -python -c "import yaml, pathlib; yaml.safe_load(pathlib.Path('api/openapi.yaml').read_text(encoding='utf-8')); print('yaml ok')" -git status --short --branch -``` - -Go 代码变更追加: - -```powershell -go test ./... -``` - -前端或 Desktop 变更追加对应目录的: - -```powershell -pnpm test -pnpm build -``` - -## 6. Git 门槛 - -提交前: - -1. `git status --short --branch` -2. `git diff --check` -3. 检查 staged diff。 -4. 不提交无关用户改动。 -5. commit message 使用 `type(scope): 中文摘要`。 -6. 有需要时 push 当前分支。 - -禁止使用会丢用户工作的命令,除非用户明确要求。 - -## 7. 完成门槛 - -一次 `set-goal` 运行结束时,至少应留下: - -- 一个完成的小增量,或一个写清楚的阻塞。 -- 对应路线图已更新。 -- 测试或确定性检查已运行,或写清无法运行原因。 -- 相关文档已同步。 -- git 状态已检查。 -- 适合提交时已提交。 diff --git a/.codex/skills/set-goal/references/roadmap-driven-loop.md b/.codex/skills/set-goal/references/roadmap-driven-loop.md deleted file mode 100644 index b4e6801ec..000000000 --- a/.codex/skills/set-goal/references/roadmap-driven-loop.md +++ /dev/null @@ -1,122 +0,0 @@ -# 路线图驱动循环 - -本文件定义 `set-goal` 的路线图读写规则。目标是让每个 Agent 都能知道当前项目在做什么、谁负责、写哪里、怎么验收。 - -## 1. 发现当前事实 - -优先读取: - -- `AGENTS.md` -- `README.md` -- `docs/roadmap.md` -- 当前任务方向路线图:`docs/roadmaps/frontend.md`、`backend.md` 或 `client.md` -- 当前分支路线图:`docs/roadmaps/branches/.md` -- 与任务相关的 API、主文档、入口代码、测试配置 - -能从仓库查到的信息就查仓库,不要先问用户。 - -## 2. 路线图分层 - -### 总路线图 - -`docs/roadmap.md` 只记录: - -- 当前总目标 -- 三条方向的活跃状态 -- 里程碑 -- 全局验收门槛 -- 跨方向待办 - -它不写详细实现方案。 - -### 方向路线图 - -`docs/roadmaps/frontend.md`、`backend.md`、`client.md` 记录各自方向: - -- 负责范围 -- 当前目标 -- 近期任务 -- 依赖文档和接口 -- 方向内验收命令 - -### 分支路线图 - -每个持续开发分支使用: - -```text -docs/roadmaps/branches/.md -``` - -分支名里的 `/` 改成 `-`。分支路线图记录: - -- 分支目标 -- 写入范围 -- 已完成 -- 下一步 -- 验收记录 -- 阻塞信息 - -## 3. 创建缺失路线图 - -如果缺少路线图,按最小模板创建,不要写成长文。 - -```markdown -# <名称>路线图 - -最后更新:YYYY-MM-DD - -## 当前目标 - -- [ ] <目标> - -## 写入范围 - -- `/` - -## 下一步 - -- [ ] <任务> - -## 验收 - -- [ ] <命令或检查> -``` - -不确定的内容写成“待确认”或“发现任务”,不要编造事实。 - -## 4. 选择本轮任务 - -每轮选 1 到 3 个任务。优先级: - -1. 阻塞其他人的接口、数据模型、脚手架。 -2. 能用测试或脚本验证。 -3. 能减少后续合并冲突。 -4. 能让前端、后端、客户端之一继续推进。 -5. 能修正文档和实现不一致。 - -不要把 `set-goal` 变成无边界清理。无关重构放待办池。 - -## 5. 更新路线图 - -每轮结束前更新: - -- 分支路线图:本轮完成、验证结果、下一步。 -- 方向路线图:如果影响方向目标或任务状态。 -- 总路线图:如果影响里程碑、跨方向依赖或全局验收门槛。 - -路线图只写稳定事实: - -- 可以写:`go test ./...` 通过、`api/openapi.yaml` 已同步、PR 已创建。 -- 不写:中间命令噪音、主观评价、临时猜测。 - -## 6. 阻塞处理 - -阻塞必须写清: - -- 错误原文 -- 复现命令 -- 已尝试动作 -- 判断的阻塞类型 -- 下一步建议 - -如果是网络、权限、依赖下载失败,不要把它写成代码失败。 diff --git a/.gitignore b/.gitignore index 39cd3e118..e449a3b6d 100644 --- a/.gitignore +++ b/.gitignore @@ -76,11 +76,12 @@ docs/review/ .agenthub/secrets/ .claude/ .codex/* -!.codex/ -!.codex/skills/ -.codex/skills/* -!.codex/skills/set-goal/ -!.codex/skills/set-goal/** +.agents/* +!.agents/ +!.agents/skills/ +.agents/skills/* +!.agents/skills/dev-loop/ +!.agents/skills/dev-loop/** .cursor/ .continue/ @@ -88,3 +89,4 @@ docs/review/ /reference/* !/reference/INDEX.md /chat-verify/ +test_store.json diff --git a/AGENTS.md b/AGENTS.md index bdf9a42ab..4023b1f2d 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -26,7 +26,7 @@ Agent 不要一次性扫全仓库。按下面顺序加载,够用就停: 1. 先读本文件。 2. 明确任务卡:目标、所属方向、写入范围、接口影响、验收命令。 -3. 如果用户要求 `set-goal`、持续推进、自我迭代、worktree/subagent 分发或交叉 review,必须先加载 `.codex/skills/set-goal/SKILL.md`,再按其中 `references/` 执行。 +3. 如果用户要求持续推进、自我迭代、长程开发、worktree/subagent 分发或交叉 review,必须先加载 `.agents/skills/dev-loop/SKILL.md`,再按其中 `references/` 执行。短任务(单文件修复、小改动)不需要。 4. 只读相关主文档章节:产品不清读 `docs/product-requirements.md`;边界不清读 `docs/system-architecture.md`;实现顺序不清读 `docs/implementation-guide.md`。 5. 改接口时读 `api/README.md`、`api/openapi.yaml`、`api/events.md`。 6. 持续开发和任务拆解读 `docs/roadmap.md`、`docs/roadmaps/<方向>.md` 和当前分支路线图。 @@ -63,11 +63,12 @@ AgentHub 的开发工作流是“三个开发者,每个开发者可以带一 ### 仓库级 Skill -- 仓库只提交白名单 skill:`.codex/skills/set-goal/`。 -- 用户调用 `set-goal`,或要求持续推进、自我迭代、自己 review、交叉 review、worktree/subagent 分发时,必须先读 `.codex/skills/set-goal/SKILL.md`。 -- `.codex/skills/set-goal/references/` 已内嵌 Agent 开发 Loop、路线图驱动、review、验证和 git 收口规则;不要假设外部同名 skill 一定可用。 +- 仓库只提交白名单 skill:`.agents/skills/dev-loop/`。 +- 长程多步骤任务(跨文件重构、多步骤功能、需要审查的变更)必须先读 `.agents/skills/dev-loop/SKILL.md`。 +- 短任务(单文件修复、typo、小改动)不需要 dev-loop——直接做。 +- `.agents/skills/dev-loop/references/` 已内嵌模型分配策略、审查清单、worktree 指南;不要假设外部同名 skill 一定可用。 - `docs/roadmap.md` 和 `docs/roadmaps/` 是持续开发台账,用来记录当前目标、方向任务、分支进展、验证和下一步;不要把详细方案写成第二套主文档。 -- 除白名单 skill 外,`.codex/` 的本机状态、缓存、会话记录和个人配置不得提交。 +- 除白名单 skill 外,`.agents/`、`.codex/`、`.claude/` 的本机状态、缓存、会话记录和个人配置不得提交。 ## 3. 技术主线 @@ -126,6 +127,26 @@ fix/short-topic `codex/` 是 Codex App 自动工作分支前缀,可以用于临时 PR。手工创建分支优先用 `feat/`、`fix/`、`docs/`。`master` 是稳定分支。实现、协议和结构调整走 PR;小文档修正可以直接提交,但不确定就走 PR。PR 标题也用 `type(scope): 中文摘要`。 +### 当前活跃分支 (2026-05-23) + +``` +Remote (6): + origin/master ← 稳定(skill 基础设施已上线) + origin/dev/delicious233 ← 主 dev: P1 交互体验推进中 + origin/dev/trump ← Trump dev + origin/feat/trump-webui ← Web 工作区 + origin/docs/dev-loop-skill-master ← 已合并删除 + origin/feat/backend-foundation ← 后台预留 (dormant) + +本地 worktree: dev/delicious233 +``` + +合并方向:`feat/* → dev/delicious233 → master` + +开发引擎:`.agents/skills/dev-loop/` — 模型分配(opus/sonnet/haiku) + 标准循环 + 交叉审查 + +当前 P1 进度:P0 级 3/3 ✅ | P1 级 2/4(ThreadPanel/DiffViewer ✅, ChatView/AgentList ❌) + 进度同步: - 每个开发者至少在一天结束前 push 当前分支。 @@ -171,7 +192,7 @@ fix/short-topic - 真实服务器 IP、内网地址、数据库连接串、生产账号、个人路径。 - 生产数据库 dump、用户数据、聊天记录、日志中的敏感字段。 - GitHub issue、PR、commit message 中也不要写上述内容。 -- 本机 Agent 记忆和运行状态,例如 `.agenthub/memory/`、`.claude/`、`.codex/`;仓库级 `.codex/skills/set-goal/` 是唯一例外。 +- 本机 Agent 记忆和运行状态,例如 `.agenthub/memory/`、`.claude/`、`.codex/`、`.agents/`;仓库级 `.agents/skills/dev-loop/` 是唯一例外。 执行规则: @@ -216,3 +237,55 @@ pnpm build - 前端状态转换和 API client 要有单元测试。 - 关键 UI 流程后续用 Playwright 覆盖:新建 Thread、启动 Run、查看 Diff、Approval、Preview。 - Desktop/Runner 改动至少提供本地 smoke test 步骤;无法自动化时写在 PR 验收里。 + +## 8. 质量治理 + +### 测试覆盖率 + +| 模块 | 最低覆盖率 | 当前 | +|------|-----------|------| +| edge-server | 70% | 72.0%(CI 强制阻断) | +| app/desktop | 不做硬性要求 | 123 tests | +| app/web | 不做硬性要求 | build 通过即可 | + +- CI 使用 `go test -short` 跳过需要真实 CLI 的集成测试。 +- 新增 adapter 功能必须补同包 `*_test.go`。 +- 修改 shared types 必须同步更新所有消费者的测试。 + +### CI 触发规则 + +| 触发条件 | CI 行为 | +|----------|---------| +| push 到 `master` | 全量:Go test + pnpm test + pnpm build + YAML 校验 | +| push 到 `dev/*` | 全量 | +| PR 到 `master` / `dev/*` | 全量 | +| push 到 `feat/*` | **不触发**(仅在开 PR 后触发) | + +### 分支治理 + +``` +master ← 稳定发布。CI 必须全绿才能合入。 +dev/delicious233 ← 主开发。所有 feat/* 的合入目标。 +dev/trump ← Web 前端合并中继。 +feat/* ← 功能分支。开 PR 到 dev 时触发 CI。 +``` + +规则: +- `master` 禁止直接 push,必须通过 PR。 +- `dev/*` 合并前本地验证:`go test ./...` + `pnpm test` + `pnpm build`。 +- `feat/*` 合并前需要 rebase 到最新 `dev`,解决冲突后再开 PR。 +- 删除已合并的 `feat/*` 分支和对应的 worktree。 +- worktree 放在 `.worktrees/`,已在 `.gitignore`,严禁提交。 + +### 提交规范 + +``` +type(scope): 中文摘要 + +type: init|feat|fix|docs|refactor|chore|test|perf|ci|revert +scope: client|edge|api|docs|desktop|web +``` + +- 摘要不超过 50 字。 +- 不要写 "added"、"fixed" 等英文动词——用中文。 +- hook 脚本在 `scripts/git-hooks/commit-msg`,clone 后运行 `scripts/setup.ps1` 启用。 diff --git a/app/shared/package.json b/app/shared/package.json index a9a001417..2f6570c82 100644 --- a/app/shared/package.json +++ b/app/shared/package.json @@ -9,6 +9,9 @@ ".": "./src/index.ts", "./types": "./src/types.ts", "./events": "./src/events.ts", - "./errors": "./src/errors.ts" + "./errors": "./src/errors.ts", + "./apiClient": "./src/apiClient.ts", + "./eventClient": "./src/eventClient.ts", + "./mock": "./src/mock.ts" } } diff --git a/app/shared/src/apiClient.ts b/app/shared/src/apiClient.ts new file mode 100644 index 000000000..5f453083f --- /dev/null +++ b/app/shared/src/apiClient.ts @@ -0,0 +1,332 @@ +import type { + HealthResponse, + ListResponse, + Project, + ProjectMemory, + Thread, + ThreadItem, + Message, + Runner, + Run, + StartRunRequest, + RunLogs, + RunDiff, + Approval, + Artifact, + Preview, + Workspace, + WorkspaceFile, +} from './types'; +import { parseError } from './errors'; + +// ── Config ──────────────────────────────────── + +let baseUrl = 'http://127.0.0.1:3210'; + +export function setBaseUrl(url: string) { + baseUrl = url.replace(/\/+$/, ''); +} + +export function getBaseUrl() { + return baseUrl; +} + +// ── Internal fetch wrapper ──────────────────── + +async function request( + path: string, + init?: RequestInit, +): Promise { + const res = await fetch(`${baseUrl}${path}`, { + ...init, + headers: { + 'Content-Type': 'application/json', + ...init?.headers, + }, + }); + + if (!res.ok) { + throw await parseError(res); + } + + if (res.status === 204) { + return undefined as unknown as T; + } + + return res.json() as Promise; +} + +function qs(params: Record): string { + const sp = new URLSearchParams(); + for (const [k, v] of Object.entries(params)) { + if (v !== undefined) sp.set(k, String(v)); + } + const s = sp.toString(); + return s ? `?${s}` : ''; +} + +// ── Foundation ───────────────────────────────── + +export function getHealth(): Promise { + return request('/v1/health'); +} + +// ── Projects ────────────────────────────────── + +export function listProjects(opts?: { + pageSize?: number; + pageCursor?: string; +}): Promise> { + return request(`/v1/projects${qs(opts ?? {})}`);} + +export function getProject(projectId: string): Promise { + return request(`/v1/projects/${encodeURIComponent(projectId)}`); +} + +export function createProject(body: { + name: string; + description?: string; +}): Promise { + return request('/v1/projects', { + method: 'POST', + body: JSON.stringify(body), + }); +} + +export function getProjectMemory( + projectId: string, +): Promise { + return request( + `/v1/projects/${encodeURIComponent(projectId)}/memory`, + ); +} + +// ── Threads ─────────────────────────────────── + +export function listThreads(opts?: { + projectId?: string; + pageSize?: number; + pageCursor?: string; +}): Promise> { + return request(`/v1/threads${qs(opts ?? {})}`); +} + +export function getThread(threadId: string): Promise { + return request(`/v1/threads/${encodeURIComponent(threadId)}`); +} + +export function createThread(body: { + projectId: string; + title?: string; +}): Promise { + return request('/v1/threads', { + method: 'POST', + body: JSON.stringify(body), + }); +} + +export function updateThread( + threadId: string, + body: { title?: string; status?: string }, +): Promise { + return request(`/v1/threads/${encodeURIComponent(threadId)}`, { + method: 'PATCH', + body: JSON.stringify(body), + }); +} + +export function archiveThread(threadId: string): Promise { + return request( + `/v1/threads/${encodeURIComponent(threadId)}:archive`, + { method: 'POST' }, + ); +} + +// ── Thread items ────────────────────────────── + +export function listThreadItems( + threadId: string, + opts?: { pageSize?: number; pageCursor?: string }, +): Promise> { + return request( + `/v1/threads/${encodeURIComponent(threadId)}/items${qs(opts ?? {})}`, + ); +} + +export function createThreadMessage( + threadId: string, + body: { role: 'user'; content: string }, +): Promise { + return request( + `/v1/threads/${encodeURIComponent(threadId)}/messages`, + { method: 'POST', body: JSON.stringify(body) }, + ); +} + +// ── Runners ─────────────────────────────────── + +export function listRunners(): Promise> { + return request('/v1/runners'); +} + +export function getRunner(runnerId: string): Promise { + return request(`/v1/runners/${encodeURIComponent(runnerId)}`); +} + +export function pingRunner(runnerId: string): Promise { + return request( + `/v1/runners/${encodeURIComponent(runnerId)}:ping`, + { method: 'POST' }, + ); +} + +// ── Runs ────────────────────────────────────── + +export function listRuns(opts?: { + threadId?: string; + pageSize?: number; + pageCursor?: string; +}): Promise> { + return request(`/v1/runs${qs(opts ?? {})}`); +} + +export function getRun(runId: string): Promise { + return request(`/v1/runs/${encodeURIComponent(runId)}`); +} + +export function startRun(body?: StartRunRequest): Promise { + return request('/v1/runs', { + method: 'POST', + body: body ? JSON.stringify(body) : undefined, + }); +} + +export function cancelRun(runId: string): Promise { + return request( + `/v1/runs/${encodeURIComponent(runId)}:cancel`, + { method: 'POST' }, + ); +} + +export function listRunItems( + runId: string, +): Promise> { + return request( + `/v1/runs/${encodeURIComponent(runId)}/items`, + ); +} + +export function getRunLogs(runId: string): Promise { + return request(`/v1/runs/${encodeURIComponent(runId)}/logs`); +} + +export function getRunDiff(runId: string): Promise { + return request(`/v1/runs/${encodeURIComponent(runId)}/diff`); +} + +// ── Approvals ───────────────────────────────── + +export function listApprovals(): Promise> { + return request('/v1/approvals'); +} + +export function getApproval(approvalId: string): Promise { + return request( + `/v1/approvals/${encodeURIComponent(approvalId)}`, + ); +} + +export function decideApproval( + approvalId: string, + body: { decision: 'approved' | 'rejected' }, +): Promise { + return request( + `/v1/approvals/${encodeURIComponent(approvalId)}:decide`, + { method: 'POST', body: JSON.stringify(body) }, + ); +} + +// ── Artifacts ───────────────────────────────── + +export function listArtifacts(): Promise> { + return request('/v1/artifacts'); +} + +export function getArtifact(artifactId: string): Promise { + return request( + `/v1/artifacts/${encodeURIComponent(artifactId)}`, + ); +} + +export function getArtifactContent( + artifactId: string, +): Promise { + return request( + `/v1/artifacts/${encodeURIComponent(artifactId)}/content`, + ); +} + +export function applyArtifact(artifactId: string): Promise { + return request( + `/v1/artifacts/${encodeURIComponent(artifactId)}:apply`, + { method: 'POST' }, + ); +} + +export function discardArtifact( + artifactId: string, +): Promise { + return request( + `/v1/artifacts/${encodeURIComponent(artifactId)}:discard`, + { method: 'POST' }, + ); +} + +// ── Previews ────────────────────────────────── + +export function listPreviews(): Promise> { + return request('/v1/previews'); +} + +export function getPreview(previewId: string): Promise { + return request( + `/v1/previews/${encodeURIComponent(previewId)}`, + ); +} + +export function createPreview(body: { + runId: string; +}): Promise { + return request('/v1/previews', { + method: 'POST', + body: JSON.stringify(body), + }); +} + +// ── Workspaces ──────────────────────────────── + +export function getWorkspace( + workspaceId: string, +): Promise { + return request( + `/v1/workspaces/${encodeURIComponent(workspaceId)}`, + ); +} + +export function listWorkspaceFiles( + workspaceId: string, +): Promise> { + return request( + `/v1/workspaces/${encodeURIComponent(workspaceId)}/files`, + ); +} + +export function readWorkspaceFile( + workspaceId: string, + body: { path: string }, +): Promise<{ path: string; content: string }> { + return request( + `/v1/workspaces/${encodeURIComponent(workspaceId)}/files:read`, + { method: 'POST', body: JSON.stringify(body) }, + ); +} diff --git a/app/shared/src/eventClient.ts b/app/shared/src/eventClient.ts new file mode 100644 index 000000000..f57467a77 --- /dev/null +++ b/app/shared/src/eventClient.ts @@ -0,0 +1,166 @@ +import type { EventEnvelope, AnyEvent } from './events'; + +export type EventListener = (event: AnyEvent) => void; + +export interface EventClientOptions { + baseUrl?: string; + cursor?: string; + reconnectDelayMs?: number; + maxReconnectDelayMs?: number; +} + +// ── EventClient ─────────────────────────────── + +export class EventClient { + private ws: WebSocket | null = null; + private listeners = new Set(); + private typeListeners = new Map>(); + private baseUrl: string; + private cursor: string | undefined; + private reconnectDelayMs: number; + private maxReconnectDelayMs: number; + private currentDelayMs: number; + private reconnectTimer: ReturnType | null = null; + private destroyed = false; + private lastSeq = 0; + + constructor(opts: EventClientOptions = {}) { + this.baseUrl = (opts.baseUrl ?? 'http://127.0.0.1:3210').replace( + /\/+$/, + '', + ); + this.cursor = opts.cursor; + this.reconnectDelayMs = opts.reconnectDelayMs ?? 1000; + this.maxReconnectDelayMs = opts.maxReconnectDelayMs ?? 30000; + this.currentDelayMs = this.reconnectDelayMs; + } + + // ── Connection ───────────────────────────── + + get wsUrl(): string { + const http = this.baseUrl; + const ws = http.replace(/^http/, 'ws'); + const qs = this.cursor + ? `?cursor=${encodeURIComponent(this.cursor)}` + : ''; + return `${ws}/v1/events${qs}`; + } + + connect(): void { + if (this.destroyed) return; + if ( + this.ws && + (this.ws.readyState === WebSocket.OPEN || + this.ws.readyState === WebSocket.CONNECTING) + ) { + return; + } + + this.ws = new WebSocket(this.wsUrl); + + this.ws.onopen = () => { + this.currentDelayMs = this.reconnectDelayMs; + }; + + this.ws.onmessage = (msg: MessageEvent) => { + try { + const raw = JSON.parse(msg.data) as EventEnvelope; + if (raw.seq && raw.seq > this.lastSeq) { + this.lastSeq = raw.seq; + } + if (raw.id) { + this.cursor = raw.id; + } + const event = raw as AnyEvent; + this.dispatch(event); + } catch { + // Ignore unparseable frames. + } + }; + + this.ws.onclose = () => { + this.ws = null; + if (!this.destroyed) { + this.scheduleReconnect(); + } + }; + + this.ws.onerror = () => { + this.ws?.close(); + }; + } + + disconnect(): void { + this.destroyed = true; + if (this.reconnectTimer !== null) { + clearTimeout(this.reconnectTimer); + this.reconnectTimer = null; + } + if (this.ws) { + this.ws.onclose = null; + this.ws.close(); + this.ws = null; + } + } + + // ── Event dispatch ───────────────────────── + + on(listener: EventListener): () => void { + this.listeners.add(listener); + return () => this.listeners.delete(listener); + } + + onType(type: string, listener: EventListener): () => void { + let set = this.typeListeners.get(type); + if (!set) { + set = new Set(); + this.typeListeners.set(type, set); + } + set.add(listener); + return () => set?.delete(listener); + } + + private dispatch(event: AnyEvent): void { + for (const fn of this.listeners) { + try { + fn(event); + } catch { + // Don't let one listener break others. + } + } + const typed = this.typeListeners.get(event.type); + if (typed) { + for (const fn of typed) { + try { + fn(event); + } catch { + // ignore + } + } + } + } + + // ── Reconnection ─────────────────────────── + + private scheduleReconnect(): void { + if (this.destroyed) return; + this.reconnectTimer = setTimeout(() => { + this.reconnectTimer = null; + this.connect(); + this.currentDelayMs = Math.min( + this.currentDelayMs * 2, + this.maxReconnectDelayMs, + ); + }, this.currentDelayMs); + } + + // ── State ────────────────────────────────── + + get connected(): boolean { + return this.ws?.readyState === WebSocket.OPEN; + } + + get currentCursor(): string | undefined { + return this.cursor; + } +} diff --git a/app/shared/src/events.ts b/app/shared/src/events.ts index 0391c88b1..08c453aad 100644 --- a/app/shared/src/events.ts +++ b/app/shared/src/events.ts @@ -1,45 +1,156 @@ // Discriminated event types per api/events.md. // -// Every event has a base envelope, and the payload type is narrowed by `type`. +// Every event wraps a base envelope. The payload type is narrowed by `type`. +// Scope fields follow the event table: projectId, conversationId, threadId, +// runId, edgeId appear as relevant for each event family. // ── Base envelope ───────────────────────────── +export interface EventScope { + projectId?: string; + conversationId?: string; + threadId?: string; + runId?: string; + edgeId?: string; + [key: string]: unknown; +} + export interface EventEnvelope { version: string; id: string; seq: number; type: string; - scope: Record; + scope: EventScope; traceId?: string; sentAt: string; payload: Record; } -// ── Runner events ───────────────────────────── +// ── IM / Project events (P0) ────────────────── + +export interface ProjectCreatedEvent extends EventEnvelope { + type: 'project.created'; + payload: { + projectId: string; + name: string; + [key: string]: unknown; + }; +} -export interface RunnerEvent extends EventEnvelope { - type: 'runner.online' | 'runner.offline'; +export interface ProjectUpdatedEvent extends EventEnvelope { + type: 'project.updated'; + payload: { + projectId: string; + [key: string]: unknown; + }; +} + +export interface ThreadCreatedEvent extends EventEnvelope { + type: 'thread.created'; + payload: { + threadId: string; + projectId: string; + title?: string; + [key: string]: unknown; + }; +} + +export interface ThreadUpdatedEvent extends EventEnvelope { + type: 'thread.updated'; + payload: { + threadId: string; + [key: string]: unknown; + }; +} + +export interface MessageCreatedEvent extends EventEnvelope { + type: 'message.created'; + payload: { + messageId: string; + threadId: string; + role: 'user' | 'agent'; + content: string; + [key: string]: unknown; + }; +} + +export interface MessageDeltaEvent extends EventEnvelope { + type: 'message.delta'; + payload: { + messageId: string; + threadId: string; + delta: string; + offset: number; + [key: string]: unknown; + }; +} + +export interface ItemCreatedEvent extends EventEnvelope { + type: 'item.created'; + payload: { + itemId: string; + threadId: string; + kind: string; + [key: string]: unknown; + }; +} + +export interface ItemUpdatedEvent extends EventEnvelope { + type: 'item.updated'; + payload: { + itemId: string; + threadId: string; + [key: string]: unknown; + }; +} + +// ── Execution / Runner events (P0) ───────────── + +export interface RunnerOnlineEvent extends EventEnvelope { + type: 'runner.online'; payload: { runnerId: string; + name: string; + capabilities?: string; [key: string]: unknown; }; } -// ── Run lifecycle events ────────────────────── +export interface RunnerOfflineEvent extends EventEnvelope { + type: 'runner.offline'; + payload: { + runnerId: string; + [key: string]: unknown; + }; +} -export interface RunLifecycleEvent extends EventEnvelope { - type: 'run.queued' | 'run.started' | 'run.finished' | 'run.failed'; +export interface RunQueuedEvent extends EventEnvelope { + type: 'run.queued'; payload: { runId: string; - status: string; - createdAt?: string; - startedAt?: string; - finishedAt?: string; + projectId: string; + threadId: string; + [key: string]: unknown; + }; +} + +export interface RunStartedEvent extends EventEnvelope { + type: 'run.started'; + payload: { + runId: string; + startedAt: string; [key: string]: unknown; }; } -// ── Run output events ───────────────────────── +export interface RunStatusChangedEvent extends EventEnvelope { + type: 'run.status.changed'; + payload: { + runId: string; + status: string; + [key: string]: unknown; + }; +} export interface RunOutputEvent extends EventEnvelope { type: 'run.output'; @@ -62,7 +173,68 @@ export interface RunOutputBatchEvent extends EventEnvelope { }; } -// ── Error event ─────────────────────────────── +export interface RunFinishedEvent extends EventEnvelope { + type: 'run.finished'; + payload: { + runId: string; + finishedAt: string; + [key: string]: unknown; + }; +} + +export interface RunFailedEvent extends EventEnvelope { + type: 'run.failed'; + payload: { + runId: string; + reason: string; + finishedAt: string; + [key: string]: unknown; + }; +} + +export interface ApprovalRequestedEvent extends EventEnvelope { + type: 'approval.requested'; + payload: { + approvalId: string; + runId: string; + threadId: string; + kind: string; + summary: string; + [key: string]: unknown; + }; +} + +export interface ApprovalDecidedEvent extends EventEnvelope { + type: 'approval.decided'; + payload: { + approvalId: string; + runId: string; + decision: 'approved' | 'rejected'; + [key: string]: unknown; + }; +} + +export interface ArtifactCreatedEvent extends EventEnvelope { + type: 'artifact.created'; + payload: { + artifactId: string; + runId: string; + threadId: string; + kind: string; + path: string; + [key: string]: unknown; + }; +} + +export interface PreviewReadyEvent extends EventEnvelope { + type: 'preview.ready'; + payload: { + previewId: string; + runId: string; + url: string; + [key: string]: unknown; + }; +} export interface ErrorEvent extends EventEnvelope { type: 'error'; @@ -77,9 +249,30 @@ export interface ErrorEvent extends EventEnvelope { // ── Union ───────────────────────────────────── export type AnyEvent = - | RunnerEvent - | RunLifecycleEvent + // IM / Project + | ProjectCreatedEvent + | ProjectUpdatedEvent + | ThreadCreatedEvent + | ThreadUpdatedEvent + | MessageCreatedEvent + | MessageDeltaEvent + | ItemCreatedEvent + | ItemUpdatedEvent + // Execution / Runner + | RunnerOnlineEvent + | RunnerOfflineEvent + | RunQueuedEvent + | RunStartedEvent + | RunStatusChangedEvent | RunOutputEvent | RunOutputBatchEvent + | RunFinishedEvent + | RunFailedEvent + | ApprovalRequestedEvent + | ApprovalDecidedEvent + | ArtifactCreatedEvent + | PreviewReadyEvent + // Common | ErrorEvent + // Fallback | EventEnvelope; diff --git a/app/shared/src/index.ts b/app/shared/src/index.ts index 3f21fe454..c5343d52b 100644 --- a/app/shared/src/index.ts +++ b/app/shared/src/index.ts @@ -1,19 +1,116 @@ export type { HealthResponse, + Project, + ProjectMemory, + Conversation, + Thread, + ThreadItem, + ThreadItemKind, + Message, Runner, + Run, + RunStatus, + StartRunRequest, + RunLogs, + RunDiff, + DiffFile, + Approval, + Artifact, + Preview, + Workspace, + WorkspaceFile, PageInfo, ListResponse, - RunInfo, } from './types'; export type { EventEnvelope, - RunnerEvent, - RunLifecycleEvent, + EventScope, + AnyEvent, + ProjectCreatedEvent, + ProjectUpdatedEvent, + ThreadCreatedEvent, + ThreadUpdatedEvent, + MessageCreatedEvent, + MessageDeltaEvent, + ItemCreatedEvent, + ItemUpdatedEvent, + RunnerOnlineEvent, + RunnerOfflineEvent, + RunQueuedEvent, + RunStartedEvent, + RunStatusChangedEvent, RunOutputEvent, RunOutputBatchEvent, + RunFinishedEvent, + RunFailedEvent, + ApprovalRequestedEvent, + ApprovalDecidedEvent, + ArtifactCreatedEvent, + PreviewReadyEvent, ErrorEvent, - AnyEvent, } from './events'; export { parseError, isErrorResponse, AppError } from './errors'; + +export { + setBaseUrl, + getBaseUrl, + getHealth, + listProjects, + getProject, + createProject, + getProjectMemory, + listThreads, + getThread, + createThread, + updateThread, + archiveThread, + listThreadItems, + createThreadMessage, + listRunners, + getRunner, + pingRunner, + listRuns, + getRun, + startRun, + cancelRun, + listRunItems, + getRunLogs, + getRunDiff, + listApprovals, + getApproval, + decideApproval, + listArtifacts, + getArtifact, + getArtifactContent, + applyArtifact, + discardArtifact, + listPreviews, + getPreview, + createPreview, + getWorkspace, + listWorkspaceFiles, + readWorkspaceFile, +} from './apiClient'; + +export { EventClient } from './eventClient'; +export type { EventListener, EventClientOptions } from './eventClient'; + +export { + mockProject, + mockProjects, + mockThreads, + mockMessages, + mockThreadItems, + mockRunners, + mockRuns, + mockApprovals, + mockArtifacts, + mockPreviews, + mockWorkspaces, + mockWorkspaceFiles, + MockEventStream, + playRunLifecycle, + playMessageStream, +} from './mock'; diff --git a/app/shared/src/mock.ts b/app/shared/src/mock.ts new file mode 100644 index 000000000..aa0c84a70 --- /dev/null +++ b/app/shared/src/mock.ts @@ -0,0 +1,464 @@ +import type { + Project, + Thread, + ThreadItem, + Message, + Runner, + Run, + Approval, + Artifact, + Preview, + Workspace, + WorkspaceFile, + ListResponse, +} from './types'; +import type { AnyEvent, EventEnvelope } from './events'; +import type { EventListener } from './eventClient'; + +// ── ID helpers ──────────────────────────────── + +let _seq = 0; +function seq() { + _seq += 1; + return _seq; +} +function evtId() { + return `evt_${seq().toString(16).padStart(6, '0')}`; +} +function now() { + return new Date().toISOString(); +} + +// ── Static mock data ────────────────────────── + +export const mockProject: Project = { + id: 'proj_1', + name: 'AgentHub', + description: 'Multi-agent collaboration platform', + createdAt: '2026-05-10T08:00:00Z', + updatedAt: '2026-05-22T12:00:00Z', +}; + +export const mockProjects: Project[] = [ + mockProject, + { + id: 'proj_2', + name: 'CLI Tools', + description: 'Developer tooling suite', + createdAt: '2026-05-01T10:00:00Z', + }, + { + id: 'proj_3', + name: 'Web Dashboard', + description: 'Real-time analytics dashboard', + createdAt: '2026-04-15T14:30:00Z', + }, +]; + +export const mockThreads: Thread[] = [ + { + id: 'thread_1', + projectId: 'proj_1', + title: 'Implement auth middleware', + status: 'active', + createdAt: '2026-05-20T09:00:00Z', + updatedAt: '2026-05-22T11:00:00Z', + }, + { + id: 'thread_2', + projectId: 'proj_1', + title: 'Fix pagination bug', + status: 'active', + createdAt: '2026-05-21T14:00:00Z', + updatedAt: '2026-05-22T10:30:00Z', + }, + { + id: 'thread_3', + projectId: 'proj_1', + title: 'Add WebSocket event mock', + status: 'active', + createdAt: '2026-05-22T08:00:00Z', + }, + { + id: 'thread_4', + projectId: 'proj_1', + title: 'Deploy v0.2 to staging', + status: 'archived', + createdAt: '2026-05-18T16:00:00Z', + }, +]; + +export const mockMessages: Message[] = [ + { + id: 'msg_1', + threadId: 'thread_1', + role: 'user', + content: 'Can you implement JWT auth middleware for the API?', + createdAt: '2026-05-20T09:00:00Z', + }, + { + id: 'msg_2', + threadId: 'thread_1', + role: 'agent', + content: + 'I\'ll add JWT authentication middleware. The implementation will:\n\n1. Extract Bearer token from Authorization header\n2. Validate the JWT signature\n3. Attach user claims to request context\n4. Return 401 for invalid/missing tokens', + createdAt: '2026-05-20T09:01:00Z', + }, + { + id: 'msg_3', + threadId: 'thread_1', + role: 'user', + content: 'Looks good. Also add role-based access control.', + createdAt: '2026-05-20T09:05:00Z', + }, + { + id: 'msg_4', + threadId: 'thread_1', + role: 'agent', + content: + 'Added RBAC on top of the JWT middleware. Admin routes now require the `admin` claim.', + createdAt: '2026-05-20T09:06:00Z', + }, +]; + +export const mockThreadItems: ThreadItem[] = [ + ...mockMessages.map((m) => ({ + id: m.id, + threadId: m.threadId, + kind: 'message' as const, + role: m.role, + content: m.content, + createdAt: m.createdAt, + })), + { + id: 'item_1', + threadId: 'thread_1', + kind: 'code', + role: 'agent', + content: '// auth/middleware.ts\n...', + createdAt: '2026-05-20T09:02:00Z', + }, + { + id: 'item_2', + threadId: 'thread_1', + kind: 'diff', + role: 'agent', + content: + '+ import { jwtVerify } from "jose";\n+ export async function authMiddleware(req, next) { ... }', + createdAt: '2026-05-20T09:03:00Z', + }, +]; + +export const mockRunners: Runner[] = [ + { + id: 'runner_1', + name: 'Claude Code', + status: 'online', + capabilities: 'code,review,debug', + }, + { + id: 'runner_2', + name: 'GPT Builder', + status: 'online', + capabilities: 'code,test,docs', + }, + { + id: 'runner_3', + name: 'Local Agent', + status: 'offline', + capabilities: 'shell,file', + }, +]; + +export const mockRuns: Run[] = [ + { + runId: 'run_1', + projectId: 'proj_1', + threadId: 'thread_1', + status: 'running', + createdAt: '2026-05-20T09:01:00Z', + startedAt: '2026-05-20T09:01:01Z', + }, + { + runId: 'run_2', + projectId: 'proj_1', + threadId: 'thread_2', + status: 'finished', + createdAt: '2026-05-21T14:01:00Z', + startedAt: '2026-05-21T14:01:01Z', + finishedAt: '2026-05-21T14:03:30Z', + }, + { + runId: 'run_3', + projectId: 'proj_1', + threadId: 'thread_3', + status: 'queued', + createdAt: '2026-05-22T08:05:00Z', + }, +]; + +export const mockApprovals: Approval[] = [ + { + id: 'apr_1', + runId: 'run_1', + threadId: 'thread_1', + kind: 'file_write', + summary: 'Write to src/middleware/auth.ts', + status: 'pending', + createdAt: '2026-05-20T09:02:00Z', + }, +]; + +export const mockArtifacts: Artifact[] = [ + { + id: 'art_1', + runId: 'run_1', + threadId: 'thread_1', + kind: 'file', + path: 'src/middleware/auth.ts', + sizeBytes: 2048, + createdAt: '2026-05-20T09:02:30Z', + }, +]; + +export const mockPreviews: Preview[] = [ + { + id: 'prev_1', + runId: 'run_1', + threadId: 'thread_1', + url: 'http://127.0.0.1:4173', + status: 'ready', + createdAt: '2026-05-20T09:03:00Z', + }, +]; + +export const mockWorkspaces: Workspace[] = [ + { + id: 'ws_1', + name: 'agenthub-web', + runId: 'run_1', + createdAt: '2026-05-20T09:00:00Z', + }, +]; + +export const mockWorkspaceFiles: WorkspaceFile[] = [ + { path: 'src/App.tsx', sizeBytes: 4500, modifiedAt: '2026-05-22T12:00:00Z' }, + { + path: 'src/pages/workbench/WorkbenchPage.tsx', + sizeBytes: 28000, + modifiedAt: '2026-05-22T11:30:00Z', + }, + { + path: 'package.json', + sizeBytes: 1200, + modifiedAt: '2026-05-21T09:00:00Z', + }, +]; + +// ── Envelope factory ────────────────────────── + +function envelope( + type: string, + payload: Record, + scope: Record = {}, +): EventEnvelope { + return { + version: 'v1', + id: evtId(), + seq: seq(), + type, + scope, + sentAt: now(), + payload, + }; +} + +// ── Mock event stream ───────────────────────── + +/** + * Simulates a WebSocket event stream for local development. + * Supports the same listener interface as EventClient so consumers + * can swap between real and mock with minimal code changes. + */ +export class MockEventStream { + private listeners = new Set(); + private typeListeners = new Map>(); + private timers = new Set>(); + + on(listener: EventListener): () => void { + this.listeners.add(listener); + return () => this.listeners.delete(listener); + } + + onType(type: string, listener: EventListener): () => void { + let set = this.typeListeners.get(type); + if (!set) { + set = new Set(); + this.typeListeners.set(type, set); + } + set.add(listener); + return () => set?.delete(listener); + } + + emit(event: AnyEvent): void { + for (const fn of this.listeners) { + try { + fn(event); + } catch { + // ignore + } + } + const typed = this.typeListeners.get(event.type); + if (typed) { + for (const fn of typed) { + try { + fn(event); + } catch { + // ignore + } + } + } + } + + emitEnvelope(type: string, payload: Record): void { + this.emit(envelope(type, payload) as AnyEvent); + } + + /** Emit an event after a delay. Returns a cancel function. */ + emitAfter( + delayMs: number, + type: string, + payload: Record, + ): () => void { + const timer = setTimeout(() => { + this.timers.delete(timer); + this.emitEnvelope(type, payload); + }, delayMs); + this.timers.add(timer); + return () => { + clearTimeout(timer); + this.timers.delete(timer); + }; + } + + destroy(): void { + for (const t of this.timers) { + clearTimeout(t); + } + this.timers.clear(); + this.listeners.clear(); + this.typeListeners.clear(); + } +} + +// ── Preset scenarios ────────────────────────── + +/** + * Plays a full run lifecycle: queued → started → output (streaming) → finished. + * Returns the MockEventStream so the caller can subscribe before play starts. + */ +export function playRunLifecycle( + stream: MockEventStream, + opts: { + runId?: string; + projectId?: string; + threadId?: string; + outputLines?: string[]; + stepDelayMs?: number; + } = {}, +): MockEventStream { + const { + runId = 'run_mock_1', + projectId = 'proj_1', + threadId = 'thread_1', + outputLines = [ + 'Installing dependencies...\n', + '✓ 142 packages installed\n', + 'Running type checks...\n', + '✓ No type errors\n', + 'Running tests...\n', + '✓ 23 tests passed\n', + 'Building...\n', + '✓ Build complete in 1.2s\n', + ], + stepDelayMs = 400, + } = opts; + + const scope = { projectId, threadId, runId }; + + stream.emitEnvelope('run.queued', { runId, projectId, threadId }); + stream.emitAfter(stepDelayMs, 'run.started', { + runId, + startedAt: now(), + }); + + outputLines.forEach((line, i) => { + stream.emitAfter(stepDelayMs * (i + 2), 'run.output', { + runId, + stream: 'stdout', + offset: i, + text: line, + }); + if (i === outputLines.length - 1) { + stream.emitAfter(stepDelayMs * (i + 3), 'run.status.changed', { + runId, + status: 'finished', + }); + stream.emitAfter(stepDelayMs * (i + 3) + 100, 'run.finished', { + runId, + finishedAt: now(), + }); + } + }); + + return stream; +} + +/** + * Simulates a streaming agent message: message.created (skeleton) then + * several message.delta chunks. + */ +export function playMessageStream( + stream: MockEventStream, + opts: { + messageId?: string; + threadId?: string; + fullText?: string; + chunkSize?: number; + chunkDelayMs?: number; + } = {}, +): MockEventStream { + const { + messageId = 'msg_mock_1', + threadId = 'thread_1', + fullText = "Here's the implementation plan:\n\n1. Add JWT middleware\n2. Add role validation\n3. Add tests\n4. Update docs", + chunkSize = 20, + chunkDelayMs = 100, + } = opts; + + // Send skeleton + stream.emitEnvelope('message.created', { + messageId, + threadId, + role: 'agent', + content: '', + }); + + // Stream deltas + for (let offset = 0; offset < fullText.length; offset += chunkSize) { + const delta = fullText.slice(offset, offset + chunkSize); + const chunkIndex = Math.floor(offset / chunkSize); + stream.emitAfter( + chunkDelayMs * (chunkIndex + 1), + 'message.delta', + { + messageId, + threadId, + delta, + offset, + }, + ); + } + + return stream; +} diff --git a/app/shared/src/types.ts b/app/shared/src/types.ts index cecfc03fb..b2bc68f43 100644 --- a/app/shared/src/types.ts +++ b/app/shared/src/types.ts @@ -1,4 +1,8 @@ -// ── REST API types ────────────────────────────── +// Domain types mirroring api/openapi.yaml schemas and P0 resource shapes. +// Generic responses (ListResponse, PageInfo, ErrorResponse) match the OpenAPI +// components so the API client can reuse them for every endpoint. + +// ── Generic API shapes ───────────────────────── export interface HealthResponse { status: string; @@ -6,13 +10,6 @@ export interface HealthResponse { edgeId: string; } -export interface Runner { - id: string; - name: string; - status: string; - capabilities?: string; -} - export interface PageInfo { nextCursor?: string; hasMore: boolean; @@ -23,8 +20,146 @@ export interface ListResponse { page: PageInfo; } -export interface RunInfo { +// ── IM / Project ─────────────────────────────── + +export interface Project { + id: string; + name: string; + description?: string; + createdAt: string; + updatedAt?: string; +} + +export interface ProjectMemory { + projectId: string; + files: number; + sizeBytes: number; +} + +export interface Conversation { + id: string; + title?: string; + createdAt: string; +} + +export interface Thread { + id: string; + projectId: string; + conversationId?: string; + title?: string; + status: 'active' | 'archived'; + createdAt: string; + updatedAt?: string; +} + +export type ThreadItemKind = 'message' | 'code' | 'file' | 'diff' | 'approval'; + +export interface ThreadItem { + id: string; + threadId: string; + kind: ThreadItemKind; + role: 'user' | 'agent'; + content: string; + createdAt: string; +} + +export interface Message { + id: string; + threadId: string; + role: 'user' | 'agent'; + content: string; + createdAt: string; +} + +// ── Execution / Runner ───────────────────────── + +export interface Runner { + id: string; + name: string; + status: 'online' | 'offline' | 'draining'; + capabilities?: string; +} + +export type RunStatus = + | 'queued' + | 'starting' + | 'running' + | 'waiting_approval' + | 'finished' + | 'failed' + | 'cancelled'; + +export interface Run { runId: string; - status: string; - createdAt?: string; + projectId: string; + threadId: string; + status: RunStatus; + createdAt: string; + startedAt?: string; + finishedAt?: string; +} + +export interface StartRunRequest { + projectId?: string; + threadId?: string; +} + +export interface RunLogs { + runId: string; + stdout: string; + stderr: string; +} + +export interface RunDiff { + runId: string; + files: DiffFile[]; +} + +export interface DiffFile { + path: string; + diff: string; + status: 'added' | 'modified' | 'deleted'; +} + +export interface Approval { + id: string; + runId: string; + threadId: string; + kind: 'file_write' | 'command' | 'publish'; + summary: string; + status: 'pending' | 'approved' | 'rejected'; + createdAt: string; + decidedAt?: string; +} + +export interface Artifact { + id: string; + runId: string; + threadId: string; + kind: string; + path: string; + sizeBytes: number; + createdAt: string; +} + +export interface Preview { + id: string; + runId: string; + threadId: string; + url?: string; + status: 'starting' | 'ready' | 'stopped'; + createdAt: string; +} + +export interface Workspace { + id: string; + name: string; + runId?: string; + createdAt: string; +} + +export interface WorkspaceFile { + path: string; + sizeBytes: number; + modifiedAt: string; } diff --git a/app/web/README.md b/app/web/README.md new file mode 100644 index 000000000..af1dd5d64 --- /dev/null +++ b/app/web/README.md @@ -0,0 +1,98 @@ +# AgentHub Web 页面预览说明 + +本文档说明如何在本地预览 `app/web` 下的前端页面雏形。当前预览壳用于快速查看 5 个页面效果,后续正式路由、导航和业务状态可以在此基础上继续演进。 + +## 前置条件 + +- 已在仓库根目录完成代码同步。 +- 本机可以使用 Node.js / Corepack。 +- 在 Windows PowerShell 中执行命令时,优先使用 `corepack.cmd pnpm`。 + +## 启动预览 + +进入 Web 前端目录: + +```powershell +cd E:\AgentHub\app\web +``` + +首次启动或依赖变更后安装依赖: + +```powershell +corepack.cmd pnpm install --ignore-scripts +``` + +启动本地预览服务: + +```powershell +corepack.cmd pnpm dev --host 127.0.0.1 +``` + +浏览器打开: + +```text +http://127.0.0.1:5174/ +``` + +## 预览页面 + +页面顶部提供 5 个切换入口: + +- `Workbench` +- `Agent Square` +- `Private Chats` +- `Group Workspace` +- `Project` + +点击对应按钮后,会在同一个预览壳中加载对应页面。当前页面文件位置: + +```text +app/web/src/pages/workbench/WorkbenchPage.tsx +app/web/src/pages/agent-square/AgentSquarePage.tsx +app/web/src/pages/private-chats/PrivateChatsPage.tsx +app/web/src/pages/group-workspace/GroupWorkspacePage.tsx +app/web/src/pages/projects/ProjectPage.tsx +``` + +## 验证命令 + +修改页面后,至少运行 TypeScript 检查: + +```powershell +corepack.cmd pnpm typecheck +``` + +需要确认 Vite 能正常打包时运行: + +```powershell +corepack.cmd pnpm build +``` + +仓库提交前建议在根目录检查空白字符问题: + +```powershell +cd E:\AgentHub +git diff --check +``` + +## 常见问题 + +### 端口被占用 + +`app/web/vite.config.ts` 当前使用端口 `5174`。如果启动时报端口占用,先关闭占用该端口的旧服务,再重新运行: + +```powershell +corepack.cmd pnpm dev --host 127.0.0.1 +``` + +### 页面样式没有完全加载 + +当前 5 个页面保留了原始 HTML 雏形中的外部资源,例如 Tailwind CDN、Google Fonts 和远程图片。网络不可用或资源访问较慢时,页面视觉效果可能不完整。 + +### `baseUrl` 显示 TypeScript 7.0 弃用警告 + +这是编辑器对未来 TypeScript 版本的提示,不是当前编译错误。`app/web/tsconfig.json` 当前与仓库已有的 `app/desktop/tsconfig.json` 保持一致,因此暂不为了消除这个警告单独改配置。 + +### 不要提交本地生成目录 + +`node_modules/` 和 `dist/` 是本地依赖与构建产物,不需要提交到仓库。 diff --git a/app/web/index.html b/app/web/index.html new file mode 100644 index 000000000..e71f55094 --- /dev/null +++ b/app/web/index.html @@ -0,0 +1,12 @@ + + + + + + AgentHub Web + + +
+ + + diff --git a/app/web/package.json b/app/web/package.json new file mode 100644 index 000000000..82ee5c736 --- /dev/null +++ b/app/web/package.json @@ -0,0 +1,23 @@ +{ + "name": "agenthub-web", + "private": true, + "version": "0.1.0", + "type": "module", + "scripts": { + "dev": "vite", + "build": "tsc && vite build", + "preview": "vite preview", + "typecheck": "tsc -p tsconfig.json --noEmit" + }, + "dependencies": { + "react": "^19.1.0", + "react-dom": "^19.1.0" + }, + "devDependencies": { + "@types/react": "^19.1.0", + "@types/react-dom": "^19.1.0", + "@vitejs/plugin-react": "^4.4.0", + "typescript": "~5.8.0", + "vite": "^6.3.0" + } +} diff --git a/app/web/pnpm-lock.yaml b/app/web/pnpm-lock.yaml new file mode 100644 index 000000000..956b6e990 --- /dev/null +++ b/app/web/pnpm-lock.yaml @@ -0,0 +1,1134 @@ +lockfileVersion: '9.0' + +settings: + autoInstallPeers: true + excludeLinksFromLockfile: false + +importers: + + .: + dependencies: + react: + specifier: ^19.1.0 + version: 19.2.6 + react-dom: + specifier: ^19.1.0 + version: 19.2.6(react@19.2.6) + devDependencies: + '@types/react': + specifier: ^19.1.0 + version: 19.2.15 + '@types/react-dom': + specifier: ^19.1.0 + version: 19.2.3(@types/react@19.2.15) + '@vitejs/plugin-react': + specifier: ^4.4.0 + version: 4.7.0(vite@6.4.2) + typescript: + specifier: ~5.8.0 + version: 5.8.3 + vite: + specifier: ^6.3.0 + version: 6.4.2 + +packages: + + '@babel/code-frame@7.29.0': + resolution: {integrity: sha512-9NhCeYjq9+3uxgdtp20LSiJXJvN0FeCtNGpJxuMFZ1Kv3cWUNb6DOhJwUvcVCzKGR66cw4njwM6hrJLqgOwbcw==} + engines: {node: '>=6.9.0'} + + '@babel/compat-data@7.29.3': + resolution: {integrity: sha512-LIVqM46zQWZhj17qA8wb4nW/ixr2y1Nw+r1etiAWgRM6U1IqP+LNhL1yg440jYZR72jCWcWbLWzIosH+uP1fqg==} + engines: {node: '>=6.9.0'} + + '@babel/core@7.29.0': + resolution: {integrity: sha512-CGOfOJqWjg2qW/Mb6zNsDm+u5vFQ8DxXfbM09z69p5Z6+mE1ikP2jUXw+j42Pf1XTYED2Rni5f95npYeuwMDQA==} + engines: {node: '>=6.9.0'} + + '@babel/generator@7.29.1': + resolution: {integrity: sha512-qsaF+9Qcm2Qv8SRIMMscAvG4O3lJ0F1GuMo5HR/Bp02LopNgnZBC/EkbevHFeGs4ls/oPz9v+Bsmzbkbe+0dUw==} + engines: {node: '>=6.9.0'} + + '@babel/helper-compilation-targets@7.28.6': + resolution: {integrity: sha512-JYtls3hqi15fcx5GaSNL7SCTJ2MNmjrkHXg4FSpOA/grxK8KwyZ5bubHsCq8FXCkua6xhuaaBit+3b7+VZRfcA==} + engines: {node: '>=6.9.0'} + + '@babel/helper-globals@7.28.0': + resolution: {integrity: sha512-+W6cISkXFa1jXsDEdYA8HeevQT/FULhxzR99pxphltZcVaugps53THCeiWA8SguxxpSp3gKPiuYfSWopkLQ4hw==} + engines: {node: '>=6.9.0'} + + '@babel/helper-module-imports@7.28.6': + resolution: {integrity: sha512-l5XkZK7r7wa9LucGw9LwZyyCUscb4x37JWTPz7swwFE/0FMQAGpiWUZn8u9DzkSBWEcK25jmvubfpw2dnAMdbw==} + engines: {node: '>=6.9.0'} + + '@babel/helper-module-transforms@7.28.6': + resolution: {integrity: sha512-67oXFAYr2cDLDVGLXTEABjdBJZ6drElUSI7WKp70NrpyISso3plG9SAGEF6y7zbha/wOzUByWWTJvEDVNIUGcA==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0 + + '@babel/helper-plugin-utils@7.28.6': + resolution: {integrity: sha512-S9gzZ/bz83GRysI7gAD4wPT/AI3uCnY+9xn+Mx/KPs2JwHJIz1W8PZkg2cqyt3RNOBM8ejcXhV6y8Og7ly/Dug==} + engines: {node: '>=6.9.0'} + + '@babel/helper-string-parser@7.27.1': + resolution: {integrity: sha512-qMlSxKbpRlAridDExk92nSobyDdpPijUq2DW6oDnUqd0iOGxmQjyqhMIihI9+zv4LPyZdRje2cavWPbCbWm3eA==} + engines: {node: '>=6.9.0'} + + '@babel/helper-validator-identifier@7.28.5': + resolution: {integrity: sha512-qSs4ifwzKJSV39ucNjsvc6WVHs6b7S03sOh2OcHF9UHfVPqWWALUsNUVzhSBiItjRZoLHx7nIarVjqKVusUZ1Q==} + engines: {node: '>=6.9.0'} + + '@babel/helper-validator-option@7.27.1': + resolution: {integrity: sha512-YvjJow9FxbhFFKDSuFnVCe2WxXk1zWc22fFePVNEaWJEu8IrZVlda6N0uHwzZrUM1il7NC9Mlp4MaJYbYd9JSg==} + engines: {node: '>=6.9.0'} + + '@babel/helpers@7.29.2': + resolution: {integrity: sha512-HoGuUs4sCZNezVEKdVcwqmZN8GoHirLUcLaYVNBK2J0DadGtdcqgr3BCbvH8+XUo4NGjNl3VOtSjEKNzqfFgKw==} + engines: {node: '>=6.9.0'} + + '@babel/parser@7.29.3': + resolution: {integrity: sha512-b3ctpQwp+PROvU/cttc4OYl4MzfJUWy6FZg+PMXfzmt/+39iHVF0sDfqay8TQM3JA2EUOyKcFZt75jWriQijsA==} + engines: {node: '>=6.0.0'} + hasBin: true + + '@babel/plugin-transform-react-jsx-self@7.27.1': + resolution: {integrity: sha512-6UzkCs+ejGdZ5mFFC/OCUrv028ab2fp1znZmCZjAOBKiBK2jXD1O+BPSfX8X2qjJ75fZBMSnQn3Rq2mrBJK2mw==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + + '@babel/plugin-transform-react-jsx-source@7.27.1': + resolution: {integrity: sha512-zbwoTsBruTeKB9hSq73ha66iFeJHuaFkUbwvqElnygoNbj/jHRsSeokowZFN3CZ64IvEqcmmkVe89OPXc7ldAw==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + + '@babel/template@7.28.6': + resolution: {integrity: sha512-YA6Ma2KsCdGb+WC6UpBVFJGXL58MDA6oyONbjyF/+5sBgxY/dwkhLogbMT2GXXyU84/IhRw/2D1Os1B/giz+BQ==} + engines: {node: '>=6.9.0'} + + '@babel/traverse@7.29.0': + resolution: {integrity: sha512-4HPiQr0X7+waHfyXPZpWPfWL/J7dcN1mx9gL6WdQVMbPnF3+ZhSMs8tCxN7oHddJE9fhNE7+lxdnlyemKfJRuA==} + engines: {node: '>=6.9.0'} + + '@babel/types@7.29.0': + resolution: {integrity: sha512-LwdZHpScM4Qz8Xw2iKSzS+cfglZzJGvofQICy7W7v4caru4EaAmyUuO6BGrbyQ2mYV11W0U8j5mBhd14dd3B0A==} + engines: {node: '>=6.9.0'} + + '@esbuild/aix-ppc64@0.25.12': + resolution: {integrity: sha512-Hhmwd6CInZ3dwpuGTF8fJG6yoWmsToE+vYgD4nytZVxcu1ulHpUQRAB1UJ8+N1Am3Mz4+xOByoQoSZf4D+CpkA==} + engines: {node: '>=18'} + cpu: [ppc64] + os: [aix] + + '@esbuild/android-arm64@0.25.12': + resolution: {integrity: sha512-6AAmLG7zwD1Z159jCKPvAxZd4y/VTO0VkprYy+3N2FtJ8+BQWFXU+OxARIwA46c5tdD9SsKGZ/1ocqBS/gAKHg==} + engines: {node: '>=18'} + cpu: [arm64] + os: [android] + + '@esbuild/android-arm@0.25.12': + resolution: {integrity: sha512-VJ+sKvNA/GE7Ccacc9Cha7bpS8nyzVv0jdVgwNDaR4gDMC/2TTRc33Ip8qrNYUcpkOHUT5OZ0bUcNNVZQ9RLlg==} + engines: {node: '>=18'} + cpu: [arm] + os: [android] + + '@esbuild/android-x64@0.25.12': + resolution: {integrity: sha512-5jbb+2hhDHx5phYR2By8GTWEzn6I9UqR11Kwf22iKbNpYrsmRB18aX/9ivc5cabcUiAT/wM+YIZ6SG9QO6a8kg==} + engines: {node: '>=18'} + cpu: [x64] + os: [android] + + '@esbuild/darwin-arm64@0.25.12': + resolution: {integrity: sha512-N3zl+lxHCifgIlcMUP5016ESkeQjLj/959RxxNYIthIg+CQHInujFuXeWbWMgnTo4cp5XVHqFPmpyu9J65C1Yg==} + engines: {node: '>=18'} + cpu: [arm64] + os: [darwin] + + '@esbuild/darwin-x64@0.25.12': + resolution: {integrity: sha512-HQ9ka4Kx21qHXwtlTUVbKJOAnmG1ipXhdWTmNXiPzPfWKpXqASVcWdnf2bnL73wgjNrFXAa3yYvBSd9pzfEIpA==} + engines: {node: '>=18'} + cpu: [x64] + os: [darwin] + + '@esbuild/freebsd-arm64@0.25.12': + resolution: {integrity: sha512-gA0Bx759+7Jve03K1S0vkOu5Lg/85dou3EseOGUes8flVOGxbhDDh/iZaoek11Y8mtyKPGF3vP8XhnkDEAmzeg==} + engines: {node: '>=18'} + cpu: [arm64] + os: [freebsd] + + '@esbuild/freebsd-x64@0.25.12': + resolution: {integrity: sha512-TGbO26Yw2xsHzxtbVFGEXBFH0FRAP7gtcPE7P5yP7wGy7cXK2oO7RyOhL5NLiqTlBh47XhmIUXuGciXEqYFfBQ==} + engines: {node: '>=18'} + cpu: [x64] + os: [freebsd] + + '@esbuild/linux-arm64@0.25.12': + resolution: {integrity: sha512-8bwX7a8FghIgrupcxb4aUmYDLp8pX06rGh5HqDT7bB+8Rdells6mHvrFHHW2JAOPZUbnjUpKTLg6ECyzvas2AQ==} + engines: {node: '>=18'} + cpu: [arm64] + os: [linux] + + '@esbuild/linux-arm@0.25.12': + resolution: {integrity: sha512-lPDGyC1JPDou8kGcywY0YILzWlhhnRjdof3UlcoqYmS9El818LLfJJc3PXXgZHrHCAKs/Z2SeZtDJr5MrkxtOw==} + engines: {node: '>=18'} + cpu: [arm] + os: [linux] + + '@esbuild/linux-ia32@0.25.12': + resolution: {integrity: sha512-0y9KrdVnbMM2/vG8KfU0byhUN+EFCny9+8g202gYqSSVMonbsCfLjUO+rCci7pM0WBEtz+oK/PIwHkzxkyharA==} + engines: {node: '>=18'} + cpu: [ia32] + os: [linux] + + '@esbuild/linux-loong64@0.25.12': + resolution: {integrity: sha512-h///Lr5a9rib/v1GGqXVGzjL4TMvVTv+s1DPoxQdz7l/AYv6LDSxdIwzxkrPW438oUXiDtwM10o9PmwS/6Z0Ng==} + engines: {node: '>=18'} + cpu: [loong64] + os: [linux] + + '@esbuild/linux-mips64el@0.25.12': + resolution: {integrity: sha512-iyRrM1Pzy9GFMDLsXn1iHUm18nhKnNMWscjmp4+hpafcZjrr2WbT//d20xaGljXDBYHqRcl8HnxbX6uaA/eGVw==} + engines: {node: '>=18'} + cpu: [mips64el] + os: [linux] + + '@esbuild/linux-ppc64@0.25.12': + resolution: {integrity: sha512-9meM/lRXxMi5PSUqEXRCtVjEZBGwB7P/D4yT8UG/mwIdze2aV4Vo6U5gD3+RsoHXKkHCfSxZKzmDssVlRj1QQA==} + engines: {node: '>=18'} + cpu: [ppc64] + os: [linux] + + '@esbuild/linux-riscv64@0.25.12': + resolution: {integrity: sha512-Zr7KR4hgKUpWAwb1f3o5ygT04MzqVrGEGXGLnj15YQDJErYu/BGg+wmFlIDOdJp0PmB0lLvxFIOXZgFRrdjR0w==} + engines: {node: '>=18'} + cpu: [riscv64] + os: [linux] + + '@esbuild/linux-s390x@0.25.12': + resolution: {integrity: sha512-MsKncOcgTNvdtiISc/jZs/Zf8d0cl/t3gYWX8J9ubBnVOwlk65UIEEvgBORTiljloIWnBzLs4qhzPkJcitIzIg==} + engines: {node: '>=18'} + cpu: [s390x] + os: [linux] + + '@esbuild/linux-x64@0.25.12': + resolution: {integrity: sha512-uqZMTLr/zR/ed4jIGnwSLkaHmPjOjJvnm6TVVitAa08SLS9Z0VM8wIRx7gWbJB5/J54YuIMInDquWyYvQLZkgw==} + engines: {node: '>=18'} + cpu: [x64] + os: [linux] + + '@esbuild/netbsd-arm64@0.25.12': + resolution: {integrity: sha512-xXwcTq4GhRM7J9A8Gv5boanHhRa/Q9KLVmcyXHCTaM4wKfIpWkdXiMog/KsnxzJ0A1+nD+zoecuzqPmCRyBGjg==} + engines: {node: '>=18'} + cpu: [arm64] + os: [netbsd] + + '@esbuild/netbsd-x64@0.25.12': + resolution: {integrity: sha512-Ld5pTlzPy3YwGec4OuHh1aCVCRvOXdH8DgRjfDy/oumVovmuSzWfnSJg+VtakB9Cm0gxNO9BzWkj6mtO1FMXkQ==} + engines: {node: '>=18'} + cpu: [x64] + os: [netbsd] + + '@esbuild/openbsd-arm64@0.25.12': + resolution: {integrity: sha512-fF96T6KsBo/pkQI950FARU9apGNTSlZGsv1jZBAlcLL1MLjLNIWPBkj5NlSz8aAzYKg+eNqknrUJ24QBybeR5A==} + engines: {node: '>=18'} + cpu: [arm64] + os: [openbsd] + + '@esbuild/openbsd-x64@0.25.12': + resolution: {integrity: sha512-MZyXUkZHjQxUvzK7rN8DJ3SRmrVrke8ZyRusHlP+kuwqTcfWLyqMOE3sScPPyeIXN/mDJIfGXvcMqCgYKekoQw==} + engines: {node: '>=18'} + cpu: [x64] + os: [openbsd] + + '@esbuild/openharmony-arm64@0.25.12': + resolution: {integrity: sha512-rm0YWsqUSRrjncSXGA7Zv78Nbnw4XL6/dzr20cyrQf7ZmRcsovpcRBdhD43Nuk3y7XIoW2OxMVvwuRvk9XdASg==} + engines: {node: '>=18'} + cpu: [arm64] + os: [openharmony] + + '@esbuild/sunos-x64@0.25.12': + resolution: {integrity: sha512-3wGSCDyuTHQUzt0nV7bocDy72r2lI33QL3gkDNGkod22EsYl04sMf0qLb8luNKTOmgF/eDEDP5BFNwoBKH441w==} + engines: {node: '>=18'} + cpu: [x64] + os: [sunos] + + '@esbuild/win32-arm64@0.25.12': + resolution: {integrity: sha512-rMmLrur64A7+DKlnSuwqUdRKyd3UE7oPJZmnljqEptesKM8wx9J8gx5u0+9Pq0fQQW8vqeKebwNXdfOyP+8Bsg==} + engines: {node: '>=18'} + cpu: [arm64] + os: [win32] + + '@esbuild/win32-ia32@0.25.12': + resolution: {integrity: sha512-HkqnmmBoCbCwxUKKNPBixiWDGCpQGVsrQfJoVGYLPT41XWF8lHuE5N6WhVia2n4o5QK5M4tYr21827fNhi4byQ==} + engines: {node: '>=18'} + cpu: [ia32] + os: [win32] + + '@esbuild/win32-x64@0.25.12': + resolution: {integrity: sha512-alJC0uCZpTFrSL0CCDjcgleBXPnCrEAhTBILpeAp7M/OFgoqtAetfBzX0xM00MUsVVPpVjlPuMbREqnZCXaTnA==} + engines: {node: '>=18'} + cpu: [x64] + os: [win32] + + '@jridgewell/gen-mapping@0.3.13': + resolution: {integrity: sha512-2kkt/7niJ6MgEPxF0bYdQ6etZaA+fQvDcLKckhy1yIQOzaoKjBBjSj63/aLVjYE3qhRt5dvM+uUyfCg6UKCBbA==} + + '@jridgewell/remapping@2.3.5': + resolution: {integrity: sha512-LI9u/+laYG4Ds1TDKSJW2YPrIlcVYOwi2fUC6xB43lueCjgxV4lffOCZCtYFiH6TNOX+tQKXx97T4IKHbhyHEQ==} + + '@jridgewell/resolve-uri@3.1.2': + resolution: {integrity: sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==} + engines: {node: '>=6.0.0'} + + '@jridgewell/sourcemap-codec@1.5.5': + resolution: {integrity: sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og==} + + '@jridgewell/trace-mapping@0.3.31': + resolution: {integrity: sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw==} + + '@rolldown/pluginutils@1.0.0-beta.27': + resolution: {integrity: sha512-+d0F4MKMCbeVUJwG96uQ4SgAznZNSq93I3V+9NHA4OpvqG8mRCpGdKmK8l/dl02h2CCDHwW2FqilnTyDcAnqjA==} + + '@rollup/rollup-android-arm-eabi@4.60.4': + resolution: {integrity: sha512-F5QXMSiFebS9hKZj02XhWLLnRpJ3B3AROP0tWbFBSj+6kCbg5m9j5JoHKd4mmSVy5mS/IMQloYgYxCuJC0fxEQ==} + cpu: [arm] + os: [android] + + '@rollup/rollup-android-arm64@4.60.4': + resolution: {integrity: sha512-GxxTKApUpzRhof7poWvCJHRF51C67u1R7D6DiluBE8wKU1u5GWE8t+v81JvJYtbawoBFX1hLv5Ei4eVjkWokaw==} + cpu: [arm64] + os: [android] + + '@rollup/rollup-darwin-arm64@4.60.4': + resolution: {integrity: sha512-tua0TaJxMOB1R0V0RS1jFZ/RpURFDJIOR2A6jWwQeawuFyS4gBW+rntLRaQd0EQ4bd6Vp44Z2rXW+YYDBsj6IA==} + cpu: [arm64] + os: [darwin] + + '@rollup/rollup-darwin-x64@4.60.4': + resolution: {integrity: sha512-CSKq7MsP+5PFIcydhAiR1K0UhEI1A2jWXVKHPCBZ151yOutENwvnPocgVHkivu2kviURtCEB6zUQw0vs8RrhMg==} + cpu: [x64] + os: [darwin] + + '@rollup/rollup-freebsd-arm64@4.60.4': + resolution: {integrity: sha512-+O8OkVdyvXMtJEciu2wS/pzm1IxntEEQx3z5TAVy4l32G0etZn+RsA48ARRrFm6Ri8fvqPQfgrvNxSjKAbnd3g==} + cpu: [arm64] + os: [freebsd] + + '@rollup/rollup-freebsd-x64@4.60.4': + resolution: {integrity: sha512-Iw3oMskH3AfNuhU0MSN7vNbdi4me/NiYo2azqPz/Le16zHSa+3RRmliCMWWQmh4lcndccU40xcJuTYJZxNo/lw==} + cpu: [x64] + os: [freebsd] + + '@rollup/rollup-linux-arm-gnueabihf@4.60.4': + resolution: {integrity: sha512-EIPRXTVQpHyF8WOo219AD2yEltPehLTcTMz2fn6JsatLYSzQf00hj3rulF+yauOlF9/FtM2WpkT/hJh/KJFGhA==} + cpu: [arm] + os: [linux] + libc: [glibc] + + '@rollup/rollup-linux-arm-musleabihf@4.60.4': + resolution: {integrity: sha512-J3Yh9PzzF1Ovah2At+lHiGQdsYgArxBbXv/zHfSyaiFQEqvNv7DcW98pCrmdjCZBrqBiKrKKe2V+aaSGWuBe/w==} + cpu: [arm] + os: [linux] + libc: [musl] + + '@rollup/rollup-linux-arm64-gnu@4.60.4': + resolution: {integrity: sha512-BFDEZMYfUvLn37ONE1yMBojPxnMlTFsdyNoqncT0qFq1mAfllL+ATMMJd8TeuVMiX84s1KbcxcZbXInmcO2mRg==} + cpu: [arm64] + os: [linux] + libc: [glibc] + + '@rollup/rollup-linux-arm64-musl@4.60.4': + resolution: {integrity: sha512-pc9EYOSlOgdQ2uPl1o9PF6/kLSgaUosia7gOuS8mB69IxJvlclko1MECXysjs5ryez1/5zjYqx3+xYU0TU6R1A==} + cpu: [arm64] + os: [linux] + libc: [musl] + + '@rollup/rollup-linux-loong64-gnu@4.60.4': + resolution: {integrity: sha512-NxnomyxYerDh5n4iLrNa+sH+Z+U4BMEE46V2PgQ/hoB909i8gV1M5wPojWg9fk1jWpO3IQnOs20K4wyZuFLEFQ==} + cpu: [loong64] + os: [linux] + libc: [glibc] + + '@rollup/rollup-linux-loong64-musl@4.60.4': + resolution: {integrity: sha512-nbJnQ8a3z1mtmrwImCYhc6BGpThAyYVRQxw9uKSKG4wR6aAYno9sVjJ0zaZcW9BPJX1GbrDPf+SvdWjgTuDmnw==} + cpu: [loong64] + os: [linux] + libc: [musl] + + '@rollup/rollup-linux-ppc64-gnu@4.60.4': + resolution: {integrity: sha512-2EU6acNrQLd8tYvo/LXW535wupT3m6fo7HKo6lr7ktQoItxTyOL1ZCR/GfGCuXl2vR+zmfI6eRXkSemafv+iVg==} + cpu: [ppc64] + os: [linux] + libc: [glibc] + + '@rollup/rollup-linux-ppc64-musl@4.60.4': + resolution: {integrity: sha512-WeBtoMuaMxiiIrO2IYP3xs6GMWkJP2C0EoT8beTLkUPmzV1i/UcOSVw1d5r9KBODtHKilG5yFxsGRnBbK3wJ4A==} + cpu: [ppc64] + os: [linux] + libc: [musl] + + '@rollup/rollup-linux-riscv64-gnu@4.60.4': + resolution: {integrity: sha512-FJHFfqpKUI3A10WrWKiFbBZ7yVbGT4q4B5o1qKFFojqpaYoh9LrQgqWCmmcxQzVSXYtyB5bzkXrYzlHTs21MYA==} + cpu: [riscv64] + os: [linux] + libc: [glibc] + + '@rollup/rollup-linux-riscv64-musl@4.60.4': + resolution: {integrity: sha512-mcEl6CUT5IAUmQf1m9FYSmVqCJlpQ8r8eyftFUHG8i9OhY7BkBXSUdnLH5DOf0wCOjcP9v/QO93zpmF1SptCCw==} + cpu: [riscv64] + os: [linux] + libc: [musl] + + '@rollup/rollup-linux-s390x-gnu@4.60.4': + resolution: {integrity: sha512-ynt3JxVd2w2buzoKDWIyiV1pJW93xlQic1THVLXilz429oijRpSHivZAgp65KBu+cMcgf1eVVjdnTLvPxgCuoQ==} + cpu: [s390x] + os: [linux] + libc: [glibc] + + '@rollup/rollup-linux-x64-gnu@4.60.4': + resolution: {integrity: sha512-Boiz5+MsaROEWDf+GGEwF8VMHGhlUoQMtIPjOgA5fv4osupqTVnJteQNKJwUcnUog2G55jYXH7KZFFiJe0TEzQ==} + cpu: [x64] + os: [linux] + libc: [glibc] + + '@rollup/rollup-linux-x64-musl@4.60.4': + resolution: {integrity: sha512-+qfSY27qIrFfI/Hom04KYFw3GKZSGU4lXus51wsb5EuySfFlWRwjkKWoE9emgRw/ukoT4Udsj4W/+xxG8VbPKg==} + cpu: [x64] + os: [linux] + libc: [musl] + + '@rollup/rollup-openbsd-x64@4.60.4': + resolution: {integrity: sha512-VpTfOPHgVXEBeeR8hZ2O0F3aSso+JDWqTWmTmzcQKted54IAdUVbxE+j/MVxUsKa8L20HJhv3vUezVPoquqWjA==} + cpu: [x64] + os: [openbsd] + + '@rollup/rollup-openharmony-arm64@4.60.4': + resolution: {integrity: sha512-IPOsh5aRYuLv/nkU51X10Bf75Bsf6+gZdx1X+QP5QM6lIJFHHqbHLG0uJn/hWthzo13UAc2umiUorqZy3axoZg==} + cpu: [arm64] + os: [openharmony] + + '@rollup/rollup-win32-arm64-msvc@4.60.4': + resolution: {integrity: sha512-4QzE9E81OohJ/HKzHhsqU+zcYYojVOXlFMs1DdyMT6qXl/niOH7AVElmmEdUNHHS/oRkc++d5k6Vy85zFs0DEw==} + cpu: [arm64] + os: [win32] + + '@rollup/rollup-win32-ia32-msvc@4.60.4': + resolution: {integrity: sha512-zTPgT1YuHHcd+Tmx7h8aml0FWFVelV5N54oHow9SLj+GfoDy/huQ+UV396N/C7KpMDMiPspRktzM1/0r1usYEA==} + cpu: [ia32] + os: [win32] + + '@rollup/rollup-win32-x64-gnu@4.60.4': + resolution: {integrity: sha512-DRS4G7mi9lJxqEDezIkKCaUIKCrLUUDCUaCsTPCi/rtqaC6D/jjwslMQyiDU50Ka0JKpeXeRBFBAXwArY52vBw==} + cpu: [x64] + os: [win32] + + '@rollup/rollup-win32-x64-msvc@4.60.4': + resolution: {integrity: sha512-QVTUovf40zgTqlFVrKA1uXMVvU2QWEFWfAH8Wdc48IxLvrJMQVMBRjuQyUpzZCDkakImib9eVazbWlC6ksWtJw==} + cpu: [x64] + os: [win32] + + '@types/babel__core@7.20.5': + resolution: {integrity: sha512-qoQprZvz5wQFJwMDqeseRXWv3rqMvhgpbXFfVyWhbx9X47POIA6i/+dXefEmZKoAgOaTdaIgNSMqMIU61yRyzA==} + + '@types/babel__generator@7.27.0': + resolution: {integrity: sha512-ufFd2Xi92OAVPYsy+P4n7/U7e68fex0+Ee8gSG9KX7eo084CWiQ4sdxktvdl0bOPupXtVJPY19zk6EwWqUQ8lg==} + + '@types/babel__template@7.4.4': + resolution: {integrity: sha512-h/NUaSyG5EyxBIp8YRxo4RMe2/qQgvyowRwVMzhYhBCONbW8PUsg4lkFMrhgZhUe5z3L3MiLDuvyJ/CaPa2A8A==} + + '@types/babel__traverse@7.28.0': + resolution: {integrity: sha512-8PvcXf70gTDZBgt9ptxJ8elBeBjcLOAcOtoO/mPJjtji1+CdGbHgm77om1GrsPxsiE+uXIpNSK64UYaIwQXd4Q==} + + '@types/estree@1.0.8': + resolution: {integrity: sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w==} + + '@types/react-dom@19.2.3': + resolution: {integrity: sha512-jp2L/eY6fn+KgVVQAOqYItbF0VY/YApe5Mz2F0aykSO8gx31bYCZyvSeYxCHKvzHG5eZjc+zyaS5BrBWya2+kQ==} + peerDependencies: + '@types/react': ^19.2.0 + + '@types/react@19.2.15': + resolution: {integrity: sha512-eRwcGNHve+E8qtEQSSRl6urh+rFop4v8gm6O8rGv25CodbvFdLjA1vVQ1KkiFE0w0UPOnb8tDiFKL5lp0rtY5Q==} + + '@vitejs/plugin-react@4.7.0': + resolution: {integrity: sha512-gUu9hwfWvvEDBBmgtAowQCojwZmJ5mcLn3aufeCsitijs3+f2NsrPtlAWIR6OPiqljl96GVCUbLe0HyqIpVaoA==} + engines: {node: ^14.18.0 || >=16.0.0} + peerDependencies: + vite: ^4.2.0 || ^5.0.0 || ^6.0.0 || ^7.0.0 + + baseline-browser-mapping@2.10.31: + resolution: {integrity: sha512-MujYO3eP72uvmSE0i4wltsodRfIpZATP3jvzRNRGGxgzId7aVocVJJV3nf01qnzzKFGxQVC9bpWxl5cjxTr/7Q==} + engines: {node: '>=6.0.0'} + hasBin: true + + browserslist@4.28.2: + resolution: {integrity: sha512-48xSriZYYg+8qXna9kwqjIVzuQxi+KYWp2+5nCYnYKPTr0LvD89Jqk2Or5ogxz0NUMfIjhh2lIUX/LyX9B4oIg==} + engines: {node: ^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7} + hasBin: true + + caniuse-lite@1.0.30001793: + resolution: {integrity: sha512-iwSsYWaCOoh26cV8NwNRViHlrfUvYsHDfRVcbtmw0Kg6PJIZZXwMkj1442FYLBGkeUf1juAsU3DTfxW579mrPA==} + + convert-source-map@2.0.0: + resolution: {integrity: sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg==} + + csstype@3.2.3: + resolution: {integrity: sha512-z1HGKcYy2xA8AGQfwrn0PAy+PB7X/GSj3UVJW9qKyn43xWa+gl5nXmU4qqLMRzWVLFC8KusUX8T/0kCiOYpAIQ==} + + debug@4.4.3: + resolution: {integrity: sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==} + engines: {node: '>=6.0'} + peerDependencies: + supports-color: '*' + peerDependenciesMeta: + supports-color: + optional: true + + electron-to-chromium@1.5.360: + resolution: {integrity: sha512-GkcBt6YYAw9SxFWn+xVar4cLVGlXVuswwtRLBozi2zp0GjXs4ZnOrqV4zbXzg35n7w81hCkyJNYicgXlVHAmBA==} + + esbuild@0.25.12: + resolution: {integrity: sha512-bbPBYYrtZbkt6Os6FiTLCTFxvq4tt3JKall1vRwshA3fdVztsLAatFaZobhkBC8/BrPetoa0oksYoKXoG4ryJg==} + engines: {node: '>=18'} + hasBin: true + + escalade@3.2.0: + resolution: {integrity: sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA==} + engines: {node: '>=6'} + + fdir@6.5.0: + resolution: {integrity: sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==} + engines: {node: '>=12.0.0'} + peerDependencies: + picomatch: ^3 || ^4 + peerDependenciesMeta: + picomatch: + optional: true + + fsevents@2.3.3: + resolution: {integrity: sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==} + engines: {node: ^8.16.0 || ^10.6.0 || >=11.0.0} + os: [darwin] + + gensync@1.0.0-beta.2: + resolution: {integrity: sha512-3hN7NaskYvMDLQY55gnW3NQ+mesEAepTqlg+VEbj7zzqEMBVNhzcGYYeqFo/TlYz6eQiFcp1HcsCZO+nGgS8zg==} + engines: {node: '>=6.9.0'} + + js-tokens@4.0.0: + resolution: {integrity: sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==} + + jsesc@3.1.0: + resolution: {integrity: sha512-/sM3dO2FOzXjKQhJuo0Q173wf2KOo8t4I8vHy6lF9poUp7bKT0/NHE8fPX23PwfhnykfqnC2xRxOnVw5XuGIaA==} + engines: {node: '>=6'} + hasBin: true + + json5@2.2.3: + resolution: {integrity: sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg==} + engines: {node: '>=6'} + hasBin: true + + lru-cache@5.1.1: + resolution: {integrity: sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w==} + + ms@2.1.3: + resolution: {integrity: sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==} + + nanoid@3.3.12: + resolution: {integrity: sha512-ZB9RH/39qpq5Vu6Y+NmUaFhQR6pp+M2Xt76XBnEwDaGcVAqhlvxrl3B2bKS5D3NH3QR76v3aSrKaF/Kiy7lEtQ==} + engines: {node: ^10 || ^12 || ^13.7 || ^14 || >=15.0.1} + hasBin: true + + node-releases@2.0.45: + resolution: {integrity: sha512-iIbHXV9eBB2nB0wa7oTsrrXq+qQt+9SIlx9AX3T96YgobtEQfis5n6TJ6vV+3QP8DwdriEAcGhARaFCu37peBg==} + engines: {node: '>=18'} + + picocolors@1.1.1: + resolution: {integrity: sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==} + + picomatch@4.0.4: + resolution: {integrity: sha512-QP88BAKvMam/3NxH6vj2o21R6MjxZUAd6nlwAS/pnGvN9IVLocLHxGYIzFhg6fUQ+5th6P4dv4eW9jX3DSIj7A==} + engines: {node: '>=12'} + + postcss@8.5.15: + resolution: {integrity: sha512-FfR8sjd4em2T6fb3I2MwAJU7HWVMr9zba+enmQeeWFfCbm+UOC/0X4DS8XtpUTMwWMGbjKYP7xjfNekzyGmB3A==} + engines: {node: ^10 || ^12 || >=14} + + react-dom@19.2.6: + resolution: {integrity: sha512-0prMI+hvBbPjsWnxDLxlCGyM8PN6UuWjEUCYmZhO67xIV9Xasa/r/vDnq+Xyq4Lo27g8QSbO5YzARu0D1Sps3g==} + peerDependencies: + react: ^19.2.6 + + react-refresh@0.17.0: + resolution: {integrity: sha512-z6F7K9bV85EfseRCp2bzrpyQ0Gkw1uLoCel9XBVWPg/TjRj94SkJzUTGfOa4bs7iJvBWtQG0Wq7wnI0syw3EBQ==} + engines: {node: '>=0.10.0'} + + react@19.2.6: + resolution: {integrity: sha512-sfWGGfavi0xr8Pg0sVsyHMAOziVYKgPLNrS7ig+ivMNb3wbCBw3KxtflsGBAwD3gYQlE/AEZsTLgToRrSCjb0Q==} + engines: {node: '>=0.10.0'} + + rollup@4.60.4: + resolution: {integrity: sha512-WHeFSbZYsPu3+bLoNRUuAO+wavNlocOPf3wSHTP7hcFKVnJeWsYlCDbr3mTS14FCizf9ccIxXA8sGL8zKeQN3g==} + engines: {node: '>=18.0.0', npm: '>=8.0.0'} + hasBin: true + + scheduler@0.27.0: + resolution: {integrity: sha512-eNv+WrVbKu1f3vbYJT/xtiF5syA5HPIMtf9IgY/nKg0sWqzAUEvqY/xm7OcZc/qafLx/iO9FgOmeSAp4v5ti/Q==} + + semver@6.3.1: + resolution: {integrity: sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==} + hasBin: true + + source-map-js@1.2.1: + resolution: {integrity: sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==} + engines: {node: '>=0.10.0'} + + tinyglobby@0.2.16: + resolution: {integrity: sha512-pn99VhoACYR8nFHhxqix+uvsbXineAasWm5ojXoN8xEwK5Kd3/TrhNn1wByuD52UxWRLy8pu+kRMniEi6Eq9Zg==} + engines: {node: '>=12.0.0'} + + typescript@5.8.3: + resolution: {integrity: sha512-p1diW6TqL9L07nNxvRMM7hMMw4c5XOo/1ibL4aAIGmSAt9slTE1Xgw5KWuof2uTOvCg9BY7ZRi+GaF+7sfgPeQ==} + engines: {node: '>=14.17'} + hasBin: true + + update-browserslist-db@1.2.3: + resolution: {integrity: sha512-Js0m9cx+qOgDxo0eMiFGEueWztz+d4+M3rGlmKPT+T4IS/jP4ylw3Nwpu6cpTTP8R1MAC1kF4VbdLt3ARf209w==} + hasBin: true + peerDependencies: + browserslist: '>= 4.21.0' + + vite@6.4.2: + resolution: {integrity: sha512-2N/55r4JDJ4gdrCvGgINMy+HH3iRpNIz8K6SFwVsA+JbQScLiC+clmAxBgwiSPgcG9U15QmvqCGWzMbqda5zGQ==} + engines: {node: ^18.0.0 || ^20.0.0 || >=22.0.0} + hasBin: true + peerDependencies: + '@types/node': ^18.0.0 || ^20.0.0 || >=22.0.0 + jiti: '>=1.21.0' + less: '*' + lightningcss: ^1.21.0 + sass: '*' + sass-embedded: '*' + stylus: '*' + sugarss: '*' + terser: ^5.16.0 + tsx: ^4.8.1 + yaml: ^2.4.2 + peerDependenciesMeta: + '@types/node': + optional: true + jiti: + optional: true + less: + optional: true + lightningcss: + optional: true + sass: + optional: true + sass-embedded: + optional: true + stylus: + optional: true + sugarss: + optional: true + terser: + optional: true + tsx: + optional: true + yaml: + optional: true + + yallist@3.1.1: + resolution: {integrity: sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g==} + +snapshots: + + '@babel/code-frame@7.29.0': + dependencies: + '@babel/helper-validator-identifier': 7.28.5 + js-tokens: 4.0.0 + picocolors: 1.1.1 + + '@babel/compat-data@7.29.3': {} + + '@babel/core@7.29.0': + dependencies: + '@babel/code-frame': 7.29.0 + '@babel/generator': 7.29.1 + '@babel/helper-compilation-targets': 7.28.6 + '@babel/helper-module-transforms': 7.28.6(@babel/core@7.29.0) + '@babel/helpers': 7.29.2 + '@babel/parser': 7.29.3 + '@babel/template': 7.28.6 + '@babel/traverse': 7.29.0 + '@babel/types': 7.29.0 + '@jridgewell/remapping': 2.3.5 + convert-source-map: 2.0.0 + debug: 4.4.3 + gensync: 1.0.0-beta.2 + json5: 2.2.3 + semver: 6.3.1 + transitivePeerDependencies: + - supports-color + + '@babel/generator@7.29.1': + dependencies: + '@babel/parser': 7.29.3 + '@babel/types': 7.29.0 + '@jridgewell/gen-mapping': 0.3.13 + '@jridgewell/trace-mapping': 0.3.31 + jsesc: 3.1.0 + + '@babel/helper-compilation-targets@7.28.6': + dependencies: + '@babel/compat-data': 7.29.3 + '@babel/helper-validator-option': 7.27.1 + browserslist: 4.28.2 + lru-cache: 5.1.1 + semver: 6.3.1 + + '@babel/helper-globals@7.28.0': {} + + '@babel/helper-module-imports@7.28.6': + dependencies: + '@babel/traverse': 7.29.0 + '@babel/types': 7.29.0 + transitivePeerDependencies: + - supports-color + + '@babel/helper-module-transforms@7.28.6(@babel/core@7.29.0)': + dependencies: + '@babel/core': 7.29.0 + '@babel/helper-module-imports': 7.28.6 + '@babel/helper-validator-identifier': 7.28.5 + '@babel/traverse': 7.29.0 + transitivePeerDependencies: + - supports-color + + '@babel/helper-plugin-utils@7.28.6': {} + + '@babel/helper-string-parser@7.27.1': {} + + '@babel/helper-validator-identifier@7.28.5': {} + + '@babel/helper-validator-option@7.27.1': {} + + '@babel/helpers@7.29.2': + dependencies: + '@babel/template': 7.28.6 + '@babel/types': 7.29.0 + + '@babel/parser@7.29.3': + dependencies: + '@babel/types': 7.29.0 + + '@babel/plugin-transform-react-jsx-self@7.27.1(@babel/core@7.29.0)': + dependencies: + '@babel/core': 7.29.0 + '@babel/helper-plugin-utils': 7.28.6 + + '@babel/plugin-transform-react-jsx-source@7.27.1(@babel/core@7.29.0)': + dependencies: + '@babel/core': 7.29.0 + '@babel/helper-plugin-utils': 7.28.6 + + '@babel/template@7.28.6': + dependencies: + '@babel/code-frame': 7.29.0 + '@babel/parser': 7.29.3 + '@babel/types': 7.29.0 + + '@babel/traverse@7.29.0': + dependencies: + '@babel/code-frame': 7.29.0 + '@babel/generator': 7.29.1 + '@babel/helper-globals': 7.28.0 + '@babel/parser': 7.29.3 + '@babel/template': 7.28.6 + '@babel/types': 7.29.0 + debug: 4.4.3 + transitivePeerDependencies: + - supports-color + + '@babel/types@7.29.0': + dependencies: + '@babel/helper-string-parser': 7.27.1 + '@babel/helper-validator-identifier': 7.28.5 + + '@esbuild/aix-ppc64@0.25.12': + optional: true + + '@esbuild/android-arm64@0.25.12': + optional: true + + '@esbuild/android-arm@0.25.12': + optional: true + + '@esbuild/android-x64@0.25.12': + optional: true + + '@esbuild/darwin-arm64@0.25.12': + optional: true + + '@esbuild/darwin-x64@0.25.12': + optional: true + + '@esbuild/freebsd-arm64@0.25.12': + optional: true + + '@esbuild/freebsd-x64@0.25.12': + optional: true + + '@esbuild/linux-arm64@0.25.12': + optional: true + + '@esbuild/linux-arm@0.25.12': + optional: true + + '@esbuild/linux-ia32@0.25.12': + optional: true + + '@esbuild/linux-loong64@0.25.12': + optional: true + + '@esbuild/linux-mips64el@0.25.12': + optional: true + + '@esbuild/linux-ppc64@0.25.12': + optional: true + + '@esbuild/linux-riscv64@0.25.12': + optional: true + + '@esbuild/linux-s390x@0.25.12': + optional: true + + '@esbuild/linux-x64@0.25.12': + optional: true + + '@esbuild/netbsd-arm64@0.25.12': + optional: true + + '@esbuild/netbsd-x64@0.25.12': + optional: true + + '@esbuild/openbsd-arm64@0.25.12': + optional: true + + '@esbuild/openbsd-x64@0.25.12': + optional: true + + '@esbuild/openharmony-arm64@0.25.12': + optional: true + + '@esbuild/sunos-x64@0.25.12': + optional: true + + '@esbuild/win32-arm64@0.25.12': + optional: true + + '@esbuild/win32-ia32@0.25.12': + optional: true + + '@esbuild/win32-x64@0.25.12': + optional: true + + '@jridgewell/gen-mapping@0.3.13': + dependencies: + '@jridgewell/sourcemap-codec': 1.5.5 + '@jridgewell/trace-mapping': 0.3.31 + + '@jridgewell/remapping@2.3.5': + dependencies: + '@jridgewell/gen-mapping': 0.3.13 + '@jridgewell/trace-mapping': 0.3.31 + + '@jridgewell/resolve-uri@3.1.2': {} + + '@jridgewell/sourcemap-codec@1.5.5': {} + + '@jridgewell/trace-mapping@0.3.31': + dependencies: + '@jridgewell/resolve-uri': 3.1.2 + '@jridgewell/sourcemap-codec': 1.5.5 + + '@rolldown/pluginutils@1.0.0-beta.27': {} + + '@rollup/rollup-android-arm-eabi@4.60.4': + optional: true + + '@rollup/rollup-android-arm64@4.60.4': + optional: true + + '@rollup/rollup-darwin-arm64@4.60.4': + optional: true + + '@rollup/rollup-darwin-x64@4.60.4': + optional: true + + '@rollup/rollup-freebsd-arm64@4.60.4': + optional: true + + '@rollup/rollup-freebsd-x64@4.60.4': + optional: true + + '@rollup/rollup-linux-arm-gnueabihf@4.60.4': + optional: true + + '@rollup/rollup-linux-arm-musleabihf@4.60.4': + optional: true + + '@rollup/rollup-linux-arm64-gnu@4.60.4': + optional: true + + '@rollup/rollup-linux-arm64-musl@4.60.4': + optional: true + + '@rollup/rollup-linux-loong64-gnu@4.60.4': + optional: true + + '@rollup/rollup-linux-loong64-musl@4.60.4': + optional: true + + '@rollup/rollup-linux-ppc64-gnu@4.60.4': + optional: true + + '@rollup/rollup-linux-ppc64-musl@4.60.4': + optional: true + + '@rollup/rollup-linux-riscv64-gnu@4.60.4': + optional: true + + '@rollup/rollup-linux-riscv64-musl@4.60.4': + optional: true + + '@rollup/rollup-linux-s390x-gnu@4.60.4': + optional: true + + '@rollup/rollup-linux-x64-gnu@4.60.4': + optional: true + + '@rollup/rollup-linux-x64-musl@4.60.4': + optional: true + + '@rollup/rollup-openbsd-x64@4.60.4': + optional: true + + '@rollup/rollup-openharmony-arm64@4.60.4': + optional: true + + '@rollup/rollup-win32-arm64-msvc@4.60.4': + optional: true + + '@rollup/rollup-win32-ia32-msvc@4.60.4': + optional: true + + '@rollup/rollup-win32-x64-gnu@4.60.4': + optional: true + + '@rollup/rollup-win32-x64-msvc@4.60.4': + optional: true + + '@types/babel__core@7.20.5': + dependencies: + '@babel/parser': 7.29.3 + '@babel/types': 7.29.0 + '@types/babel__generator': 7.27.0 + '@types/babel__template': 7.4.4 + '@types/babel__traverse': 7.28.0 + + '@types/babel__generator@7.27.0': + dependencies: + '@babel/types': 7.29.0 + + '@types/babel__template@7.4.4': + dependencies: + '@babel/parser': 7.29.3 + '@babel/types': 7.29.0 + + '@types/babel__traverse@7.28.0': + dependencies: + '@babel/types': 7.29.0 + + '@types/estree@1.0.8': {} + + '@types/react-dom@19.2.3(@types/react@19.2.15)': + dependencies: + '@types/react': 19.2.15 + + '@types/react@19.2.15': + dependencies: + csstype: 3.2.3 + + '@vitejs/plugin-react@4.7.0(vite@6.4.2)': + dependencies: + '@babel/core': 7.29.0 + '@babel/plugin-transform-react-jsx-self': 7.27.1(@babel/core@7.29.0) + '@babel/plugin-transform-react-jsx-source': 7.27.1(@babel/core@7.29.0) + '@rolldown/pluginutils': 1.0.0-beta.27 + '@types/babel__core': 7.20.5 + react-refresh: 0.17.0 + vite: 6.4.2 + transitivePeerDependencies: + - supports-color + + baseline-browser-mapping@2.10.31: {} + + browserslist@4.28.2: + dependencies: + baseline-browser-mapping: 2.10.31 + caniuse-lite: 1.0.30001793 + electron-to-chromium: 1.5.360 + node-releases: 2.0.45 + update-browserslist-db: 1.2.3(browserslist@4.28.2) + + caniuse-lite@1.0.30001793: {} + + convert-source-map@2.0.0: {} + + csstype@3.2.3: {} + + debug@4.4.3: + dependencies: + ms: 2.1.3 + + electron-to-chromium@1.5.360: {} + + esbuild@0.25.12: + optionalDependencies: + '@esbuild/aix-ppc64': 0.25.12 + '@esbuild/android-arm': 0.25.12 + '@esbuild/android-arm64': 0.25.12 + '@esbuild/android-x64': 0.25.12 + '@esbuild/darwin-arm64': 0.25.12 + '@esbuild/darwin-x64': 0.25.12 + '@esbuild/freebsd-arm64': 0.25.12 + '@esbuild/freebsd-x64': 0.25.12 + '@esbuild/linux-arm': 0.25.12 + '@esbuild/linux-arm64': 0.25.12 + '@esbuild/linux-ia32': 0.25.12 + '@esbuild/linux-loong64': 0.25.12 + '@esbuild/linux-mips64el': 0.25.12 + '@esbuild/linux-ppc64': 0.25.12 + '@esbuild/linux-riscv64': 0.25.12 + '@esbuild/linux-s390x': 0.25.12 + '@esbuild/linux-x64': 0.25.12 + '@esbuild/netbsd-arm64': 0.25.12 + '@esbuild/netbsd-x64': 0.25.12 + '@esbuild/openbsd-arm64': 0.25.12 + '@esbuild/openbsd-x64': 0.25.12 + '@esbuild/openharmony-arm64': 0.25.12 + '@esbuild/sunos-x64': 0.25.12 + '@esbuild/win32-arm64': 0.25.12 + '@esbuild/win32-ia32': 0.25.12 + '@esbuild/win32-x64': 0.25.12 + + escalade@3.2.0: {} + + fdir@6.5.0(picomatch@4.0.4): + optionalDependencies: + picomatch: 4.0.4 + + fsevents@2.3.3: + optional: true + + gensync@1.0.0-beta.2: {} + + js-tokens@4.0.0: {} + + jsesc@3.1.0: {} + + json5@2.2.3: {} + + lru-cache@5.1.1: + dependencies: + yallist: 3.1.1 + + ms@2.1.3: {} + + nanoid@3.3.12: {} + + node-releases@2.0.45: {} + + picocolors@1.1.1: {} + + picomatch@4.0.4: {} + + postcss@8.5.15: + dependencies: + nanoid: 3.3.12 + picocolors: 1.1.1 + source-map-js: 1.2.1 + + react-dom@19.2.6(react@19.2.6): + dependencies: + react: 19.2.6 + scheduler: 0.27.0 + + react-refresh@0.17.0: {} + + react@19.2.6: {} + + rollup@4.60.4: + dependencies: + '@types/estree': 1.0.8 + optionalDependencies: + '@rollup/rollup-android-arm-eabi': 4.60.4 + '@rollup/rollup-android-arm64': 4.60.4 + '@rollup/rollup-darwin-arm64': 4.60.4 + '@rollup/rollup-darwin-x64': 4.60.4 + '@rollup/rollup-freebsd-arm64': 4.60.4 + '@rollup/rollup-freebsd-x64': 4.60.4 + '@rollup/rollup-linux-arm-gnueabihf': 4.60.4 + '@rollup/rollup-linux-arm-musleabihf': 4.60.4 + '@rollup/rollup-linux-arm64-gnu': 4.60.4 + '@rollup/rollup-linux-arm64-musl': 4.60.4 + '@rollup/rollup-linux-loong64-gnu': 4.60.4 + '@rollup/rollup-linux-loong64-musl': 4.60.4 + '@rollup/rollup-linux-ppc64-gnu': 4.60.4 + '@rollup/rollup-linux-ppc64-musl': 4.60.4 + '@rollup/rollup-linux-riscv64-gnu': 4.60.4 + '@rollup/rollup-linux-riscv64-musl': 4.60.4 + '@rollup/rollup-linux-s390x-gnu': 4.60.4 + '@rollup/rollup-linux-x64-gnu': 4.60.4 + '@rollup/rollup-linux-x64-musl': 4.60.4 + '@rollup/rollup-openbsd-x64': 4.60.4 + '@rollup/rollup-openharmony-arm64': 4.60.4 + '@rollup/rollup-win32-arm64-msvc': 4.60.4 + '@rollup/rollup-win32-ia32-msvc': 4.60.4 + '@rollup/rollup-win32-x64-gnu': 4.60.4 + '@rollup/rollup-win32-x64-msvc': 4.60.4 + fsevents: 2.3.3 + + scheduler@0.27.0: {} + + semver@6.3.1: {} + + source-map-js@1.2.1: {} + + tinyglobby@0.2.16: + dependencies: + fdir: 6.5.0(picomatch@4.0.4) + picomatch: 4.0.4 + + typescript@5.8.3: {} + + update-browserslist-db@1.2.3(browserslist@4.28.2): + dependencies: + browserslist: 4.28.2 + escalade: 3.2.0 + picocolors: 1.1.1 + + vite@6.4.2: + dependencies: + esbuild: 0.25.12 + fdir: 6.5.0(picomatch@4.0.4) + picomatch: 4.0.4 + postcss: 8.5.15 + rollup: 4.60.4 + tinyglobby: 0.2.16 + optionalDependencies: + fsevents: 2.3.3 + + yallist@3.1.1: {} diff --git a/app/web/src/App.module.css b/app/web/src/App.module.css new file mode 100644 index 000000000..e513f24d7 --- /dev/null +++ b/app/web/src/App.module.css @@ -0,0 +1,91 @@ +.root { + display: flex; + flex-direction: column; + height: 100vh; + min-width: 960px; + overflow: hidden; + background: #f7f8fb; + color: #172033; + font-family: + Inter, + "Segoe UI", + Arial, + sans-serif; +} + +.toolbar { + display: flex; + align-items: center; + gap: 24px; + height: 56px; + padding: 0 20px; + border-bottom: 1px solid #d8deea; + background: #ffffff; + flex-shrink: 0; +} + +.brand { + display: flex; + align-items: center; + gap: 10px; + min-width: 220px; + font-size: 14px; + font-weight: 700; +} + +.brandMark { + display: inline-flex; + align-items: center; + justify-content: center; + width: 28px; + height: 28px; + border-radius: 6px; + background: #005faa; + color: #ffffff; + font-size: 12px; + letter-spacing: 0; +} + +.tabs { + display: flex; + align-items: center; + gap: 4px; + height: 100%; + overflow-x: auto; +} + +.tab, +.activeTab { + height: 32px; + padding: 0 12px; + border: 1px solid transparent; + border-radius: 6px; + background: transparent; + color: #48556a; + cursor: pointer; + font: inherit; + font-size: 13px; + white-space: nowrap; +} + +.tab:hover { + background: #eef3fa; + color: #172033; +} + +.activeTab { + border-color: #b8c8dc; + background: #e8f1fb; + color: #004f90; + font-weight: 700; +} + +.preview { + flex: 1; + min-height: 0; + overflow: hidden; +} + +.preview iframe { + height: 100% !important; +} diff --git a/app/web/src/App.tsx b/app/web/src/App.tsx new file mode 100644 index 000000000..eb480fcf6 --- /dev/null +++ b/app/web/src/App.tsx @@ -0,0 +1,80 @@ +import { useMemo, useState } from 'react'; +import AgentSquarePage from '@/pages/agent-square/AgentSquarePage'; +import GroupWorkspacePage from '@/pages/group-workspace/GroupWorkspacePage'; +import PrivateChatsPage from '@/pages/private-chats/PrivateChatsPage'; +import ProjectPage from '@/pages/projects/ProjectPage'; +import WorkbenchPage from '@/pages/workbench/WorkbenchPage'; +import styles from '@/App.module.css'; + +type PreviewPage = { + id: string; + label: string; + component: () => React.ReactElement; +}; + +const previewPages: PreviewPage[] = [ + { + id: 'workbench', + label: 'Workbench', + component: WorkbenchPage, + }, + { + id: 'agent-square', + label: 'Agent Square', + component: AgentSquarePage, + }, + { + id: 'private-chats', + label: 'Private Chats', + component: PrivateChatsPage, + }, + { + id: 'group-workspace', + label: 'Group Workspace', + component: GroupWorkspacePage, + }, + { + id: 'project', + label: 'Project', + component: ProjectPage, + }, +]; + +export default function App() { + const [activePageId, setActivePageId] = useState(previewPages[0].id); + + const activePage = useMemo( + () => previewPages.find((page) => page.id === activePageId) ?? previewPages[0], + [activePageId], + ); + + const ActivePage = activePage.component; + + return ( +
+
+
+ AH + AgentHub Web Preview +
+ + +
+ +
+ +
+
+ ); +} diff --git a/app/web/src/main.tsx b/app/web/src/main.tsx new file mode 100644 index 000000000..e75384576 --- /dev/null +++ b/app/web/src/main.tsx @@ -0,0 +1,9 @@ +import { StrictMode } from 'react'; +import { createRoot } from 'react-dom/client'; +import App from '@/App'; + +createRoot(document.getElementById('root')!).render( + + + , +); diff --git a/app/web/src/pages/agent-square/AgentSquarePage.tsx b/app/web/src/pages/agent-square/AgentSquarePage.tsx new file mode 100644 index 000000000..1b28e08d5 --- /dev/null +++ b/app/web/src/pages/agent-square/AgentSquarePage.tsx @@ -0,0 +1,1548 @@ +import { useEffect, useMemo, useRef, useState } from 'react'; +import { mockRunners } from '@shared/index'; + +type AgentCategory = 'Engineering' | 'Design' | 'Operations' | 'Research'; + +type SortMode = 'popular' | 'rating' | 'recent'; + +type ViewMode = 'all' | 'favorites' | 'installed'; + +type Agent = { + id: string; + name: string; + category: AgentCategory; + icon: string; + tone: 'blue' | 'cyan' | 'purple' | 'green'; + summary: string; + description: string; + tags: string[]; + installs: number; + favoriteCount: number; + rating: number; + updatedDaysAgo: number; + outputs: string[]; +}; + +type Confirmation = { + actionAgentId?: string; + id: string; + message: string; + title: string; +}; + +const agents: Agent[] = [ + { + id: 'refactor', + name: 'Code Refactor Pro', + category: 'Engineering', + icon: 'code_blocks', + tone: 'blue', + summary: 'Modernizes front-end and Go service modules while keeping reviewable diffs visible.', + description: + 'Best for code cleanup passes where the workspace needs scoped patches, local conventions, and clear validation notes before review.', + tags: ['TypeScript', 'Go', 'Review'], + installs: 14820, + favoriteCount: 1824, + rating: 4.9, + updatedDaysAgo: 6, + outputs: ['Scoped patch plan', 'Validation checklist', 'Risk notes'], + }, + { + id: 'designer', + name: 'Interface Critic', + category: 'Design', + icon: 'palette', + tone: 'purple', + summary: 'Audits tool surfaces for hierarchy, responsive density, and component states.', + description: + 'Reviews a workbench page like a product surface: layout rhythm, accessible labels, empty states, and responsive clipping.', + tags: ['UI audit', 'A11y', 'Layout'], + installs: 9360, + favoriteCount: 1211, + rating: 4.8, + updatedDaysAgo: 3, + outputs: ['Visual hierarchy pass', 'A11y notes', 'Responsive risks'], + }, + { + id: 'qa', + name: 'QA Flow Builder', + category: 'Engineering', + icon: 'fact_check', + tone: 'cyan', + summary: 'Creates focused checks for UI flows, command output, and API contract edges.', + description: + 'Useful when a page or workflow needs a compact smoke plan with the right assertions and a clean handoff for testers.', + tags: ['Playwright', 'Unit tests', 'Smoke'], + installs: 12840, + favoriteCount: 1506, + rating: 4.7, + updatedDaysAgo: 9, + outputs: ['Smoke scenarios', 'State matrix', 'Manual QA steps'], + }, + { + id: 'ops', + name: 'Runbook Operator', + category: 'Operations', + icon: 'terminal', + tone: 'green', + summary: 'Turns incidents and release notes into commands, checkpoints, and rollback prompts.', + description: + 'Pairs well with deployment work because it keeps routine operations, checks, and escalation notes in one readable sequence.', + tags: ['Runbook', 'Deploy', 'Monitor'], + installs: 8700, + favoriteCount: 984, + rating: 4.6, + updatedDaysAgo: 2, + outputs: ['Command sequence', 'Rollback prompts', 'Operator handoff'], + }, + { + id: 'research', + name: 'Evidence Synthesizer', + category: 'Research', + icon: 'travel_explore', + tone: 'purple', + summary: 'Groups sources into claims, caveats, contradictions, and decision notes.', + description: + 'Helps teams move from raw references to compact conclusions while preserving the difference between evidence and inference.', + tags: ['Sources', 'Citations', 'Summary'], + installs: 10320, + favoriteCount: 1398, + rating: 4.9, + updatedDaysAgo: 5, + outputs: ['Claim map', 'Source trail', 'Decision summary'], + }, + { + id: 'release', + name: 'Release Steward', + category: 'Operations', + icon: 'rocket_launch', + tone: 'blue', + summary: 'Collects branch status, validation commands, changelog points, and merge readiness.', + description: + 'Keeps release coordination visible across branch status, test evidence, review notes, and handoff copy.', + tags: ['PR', 'Validation', 'Changelog'], + installs: 7600, + favoriteCount: 862, + rating: 4.5, + updatedDaysAgo: 8, + outputs: ['Release checklist', 'Changelog draft', 'Merge summary'], + }, + ...mockRunners.map((runner) => ({ + id: runner.id, + name: runner.name, + category: 'Engineering' as AgentCategory, + icon: runner.status === 'online' ? 'memory' : 'cloud_off', + tone: (runner.status === 'online' ? 'cyan' : 'purple') as Agent['tone'], + summary: runner.capabilities ?? 'Local runner agent', + description: `Runner ${runner.name} — status: ${runner.status}. Capabilities: ${runner.capabilities ?? 'unknown'}.`, + tags: [runner.status, ...(runner.capabilities?.split(',') ?? [])], + installs: runner.status === 'online' ? 5200 : 800, + favoriteCount: runner.status === 'online' ? 340 : 45, + rating: runner.status === 'online' ? 4.5 : 3.2, + updatedDaysAgo: runner.status === 'online' ? 1 : 12, + outputs: ['Run execution', 'Output streaming', 'File artifacts'], + })), +]; + +const categories: Array = ['All', 'Engineering', 'Design', 'Operations', 'Research']; + +const initialFavoriteIds = new Set(['refactor', mockRunners[0]?.id].filter(Boolean) as string[]); +const initialInstalledIds = new Set(['refactor', mockRunners[0]?.id, mockRunners[1]?.id].filter(Boolean) as string[]); +const workspaceLimit = 8; + +const sortLabels: Record = { + popular: 'Most installed', + rating: 'Highest rated', + recent: 'Recently updated', +}; + +const viewModeLabels: Record = { + all: 'All agents', + favorites: 'Only favorites', + installed: 'Only added', +}; + +function formatInstalls(installs: number): string { + return `${(installs / 1000).toFixed(1)}k`; +} + +function cx(...classes: Array): string { + return classes.filter(Boolean).join(' '); +} + +function ParticleCanvas() { + const canvasRef = useRef(null); + + useEffect(() => { + const canvas = canvasRef.current; + const context = canvas?.getContext('2d'); + + if (!canvas || !context) { + return; + } + + type Particle = { + alpha: number; + hue: number; + radius: number; + velocityX: number; + velocityY: number; + x: number; + y: number; + }; + + let animationFrame = 0; + let height = 0; + let width = 0; + let particles: Particle[] = []; + + const createParticle = (index: number): Particle => ({ + alpha: 0.18 + Math.random() * 0.2, + hue: index % 3 === 0 ? 196 : 210, + radius: 1.6 + Math.random() * 2.6, + velocityX: -0.18 + Math.random() * 0.36, + velocityY: -0.18 - Math.random() * 0.48, + x: Math.random() * width, + y: Math.random() * height, + }); + + const resize = () => { + const ratio = window.devicePixelRatio || 1; + width = window.innerWidth; + height = window.innerHeight; + canvas.width = Math.floor(width * ratio); + canvas.height = Math.floor(height * ratio); + canvas.style.width = `${width}px`; + canvas.style.height = `${height}px`; + context.setTransform(ratio, 0, 0, ratio, 0, 0); + particles = Array.from({ length: 56 }, (_, index) => createParticle(index)); + }; + + const draw = () => { + animationFrame = window.requestAnimationFrame(draw); + context.clearRect(0, 0, width, height); + + particles.forEach((particle, index) => { + particle.x += particle.velocityX; + particle.y += particle.velocityY; + + if (particle.y < -16) { + particle.y = height + 16; + particle.x = Math.random() * width; + } + + if (particle.x < -16) { + particle.x = width + 16; + } + + if (particle.x > width + 16) { + particle.x = -16; + } + + context.beginPath(); + context.fillStyle = `hsla(${particle.hue}, 84%, 48%, ${particle.alpha})`; + context.arc(particle.x, particle.y, particle.radius, 0, Math.PI * 2); + context.fill(); + + for (let nextIndex = index + 1; nextIndex < particles.length; nextIndex += 1) { + const nextParticle = particles[nextIndex]; + const dx = particle.x - nextParticle.x; + const dy = particle.y - nextParticle.y; + const distance = Math.sqrt(dx * dx + dy * dy); + + if (distance < 126) { + context.beginPath(); + context.strokeStyle = `rgba(23, 105, 232, ${(1 - distance / 126) * 0.07})`; + context.lineWidth = 1; + context.moveTo(particle.x, particle.y); + context.lineTo(nextParticle.x, nextParticle.y); + context.stroke(); + } + } + }); + }; + + resize(); + draw(); + window.addEventListener('resize', resize); + + return () => { + window.cancelAnimationFrame(animationFrame); + window.removeEventListener('resize', resize); + }; + }, []); + + return