Skip to content

feat: 多项目并行监听 + 项目切换 UI(参考 codex app)(#35) - #63

Merged
ghbvf merged 5 commits into
developfrom
Feature/35-multi-project
Jun 17, 2026
Merged

ghbvf merged 5 commits into
developfrom
Feature/35-multi-project

Conversation

@ghbvf

@ghbvf ghbvf commented Jun 17, 2026

Copy link
Copy Markdown
Owner

Summary

多项目并行监听 + 项目切换 UI:单项目硬结构 → projects: [Project] + activeProjectId,N 个 enabled 项目各自独立 poll 循环 / ledger / review 会话并行互不阻塞,事件按 projectId 分流,codex-app 风格项目侧栏切换视图,旧单项目 config.json 自动迁移。

Why / 背景

现状配置与监听是单项目硬结构(扁平 AppConfig 单 repo/repoRoot、单例 Scheduler、全局 ledger.json/prs.json/SessionRegistry、事件无 projectId),用户监控多仓只能反复改配置重启、无法并行、UI 无多项目概念。这是 epic #33 中架构改动最大的一项。目标:可同时监控 ≥2 个仓库,每项目独立调度/去重/review 会话,UI 可切项目且视图随切。

Refs

Closes #35(epic #33 子项)。计划见 ~/.claude/plans/35-memoized-barto.md。对标本仓既有切片结构、trait seam(PrSource/ReviewEngine 未改、仅按项目透传)、serde camelCase golden、assertNever 穷尽性;项目侧栏对标 codex-app 多会话侧栏。

Risk / 兼容性

  • serde wire 契约变更(前后端 lockstep):AppConfig 结构变形(projects[]+activeProjectId+7 全局 webhook 字段)、新增 Project、PrEvent/ReviewEvent/SessionInfo 全部加 projectId。已同步更新所有 serde golden(config/model.rs、events.rs、review/session.rs)+ src/types.ts/src/config/types.ts/src/review/types.ts,golden 测试锁 camelCase 形状。
  • 数据迁移:旧单项目 config.json 经纯函数 migrate_value(detect-by-key、幂等)自动升级为 projects:[{id:"default",…}] + 顶层 webhook 字段;{}→空 projects 触发 onboarding。5 条表驱动测试覆盖。#[serde(default)] 前向兼容保留。
  • 命令面变更:get_prs/set_pr_archived/poll_now/start_review 加 projectId;start_polling/stop_polling/reschedule = reconcile/stop_all 全项目;新增 set_active_project。
  • 存储分区:ledger.json/prs.json 按 :{projectId} store-key 前缀分区(record 形状不变,dedup/tracked golden 未动);各保留单文件单全局写锁(整文件重写,跨项目同锁)。
  • codex 单例不分片:per-turn cwd 已 scope 每条 thread,一个 app-server 服务多项目 thread;冷启 cwd 取 active 项目 repo_root。
  • 已知限制(非阻塞):webhook 全局单端点按 repo 路由,路由表是 start_webhook 时的 enabled 项目快照——运行中新增项目需手动重启 webhook 刷新路由(quick 隧道重启会换公网 URL,故不自动重启)。已在代码注释标注。
  • OOS(follow-up):per-project 并行焦点 review 面板(多条流同屏)超出 MVP,本 PR 保 single-active 焦点 + 列表按项目过滤。

Test plan

  • pnpm build 本地通过(vue-tsc 类型检查 + vite build)
  • cargo build --manifest-path src-tauri/Cargo.toml --locked 本地通过
  • cargo clippy --manifest-path src-tauri/Cargo.toml --all-targets --locked -- -D warnings 0 告警
  • cargo fmt --manifest-path src-tauri/Cargo.toml --all -- --check 干净
  • cargo test --manifest-path src-tauri/Cargo.toml --locked 通过(200 单测 + 3 集成;含迁移表驱动 / golden / session 按项目隔离 / ledger 按项目去重)
  • 前端 vitest 106 通过(含 usePrStore/useReviewStore 分区、fields.test.ts、slice-boundary)

🤖 Generated with Claude Code

ghbvf and others added 3 commits June 18, 2026 00:07
契约重塑(批次1,多项目并行监听基础):
- config/model.rs: 新增 Project struct(id/name/enabled + 11 per-project 字段);
  AppConfig 改为 { projects, activeProjectId, +7 全局 webhook };validate 拆
  validate_project + 顶层 validate(per-enabled 校验 + 拒重复 id/repo);重写
  serde golden(projects/activeProjectId/Project camelCase)+ 首启 marker 改
  default_has_no_projects。
- config/service.rs: 纯 migrate_value(detect-by-key:旧扁平→单 default 项目 +
  webhook 提升;幂等);project(app,id) 查找;5 条迁移表驱动测试。
- events.rs: PrEvent/ReviewEvent 全 7 arm 加 project_id + golden 更新。
- src/types.ts / config/types.ts / fields.ts(+test) / projects.ts: TS 契约镜像,
  PROJECT_GROUPS/GLOBAL_GROUPS 拆分 + useProjects 根模块。

注:跨切片下游(scheduler/ledger/registry/session/webhook/lib)尚未适配,
本 commit 单独不编译,后续批次补齐。

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
批次2后端 + 批次3组装:
- pr/scheduler.rs: 单例 Scheduler → SchedulerSet(project_id→Arc<Scheduler> 映射 +
  reconcile 起停/重配);ProjectDispatcher 载体(替代旧 Dispatcher,带 project_id);
  per-project cycle 发 PrEvent{projectId} + 按项目派发。
- pr/commands.rs: discover/get_prs/set_pr_archived/poll_now 带 project_id;
  start_polling/stop_polling/reschedule = reconcile/stop_all;gate_dispatchable 按项目;
  start_webhook 从 enabled 项目构建 ProjectRoute 列表。
- pr/ledger.rs: 键 dispatched:{pid}/events:{pid} 分区 + LEDGER_WRITE_LOCK;dedup-key
  形状不变。pr/registry.rs: 键 tracked:{pid},grace 取该项目 period。
- pr/webhook.rs: 单端点按 incoming repo 路由到匹配项目,payload_to_candidate→
  (project_id, Candidate)。
- review/session.rs: reserved 按 (projectId,pr,kind),SessionInfo 加 project_id,
  active_pairs(pid) 按项目过滤,pump 注入 projectId。engine.rs: CodexEngine 加
  project_id。commands.rs: start_review(projectId,…) + 用 active_repo_root/
  project_validated。
- config/service.rs: project_validated + active_repo_root + 惰性 set_active_project
  helper。config/commands.rs + lib.rs: set_active_project 命令;run_auto_dispatch
  (project_id) per-project engine/ledger/active;调度+webhook dispatcher 闭包带
  project_id;state.rs scheduler → SchedulerSet。

后端全绿:build + 200 tests + fmt + clippy(-D warnings) 0 告警。

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
批次2前端 + 批次3 App 组装:
- pr/usePrStore.ts: prs/loading/error/polling/hasNewPr/snapshotLoaded 按 projectId
  分区;参数化 …For(id) getter + active 包装(读 useProjects);subscribe 按
  e.projectId 路由 + 非 active 项目 hasNewPr 标记;init 订阅在先 + loadSnapshot(active)
  兜底;switchTo 懒加载。pr/api.ts 命令带 projectId(poll_now/get_prs/set_pr_archived)。
- pr/ProjectSwitcher.vue(新): 最左项目侧栏(名称 + 状态点 + PR 计数 + active 高亮),
  点击 switchTo。PrList/PrRow/PollControls 读 active 项目 getter。
- review/useReviewStore.ts: dispatchError 按 projectId 分区;start(projectId,…);
  threadId 归属不变(assertNever 穷尽性保持)。review/api.ts startReview 带 projectId;
  types.ts ReviewSession 加 projectId;ReviewSessions 按 active 过滤;ReviewPanel
  start 传 active projectId。
- config: ProjectsManager.vue + ProjectCard.vue(新,增删编辑项目卡 + per-card
  authorsInput);SettingsView 左导加项目段(webhook 组仍全局);OnboardingWizard
  单 Project draft(首项目 id=default)。projects.ts useProjects 根模块。
- App.vue: ProjectSwitcher 入 layout;selectedPr 作用域到 active 项目;切项目重置
  selection;首启 gate 改 projects.length===0;config 存/onboarding 后 hydrate +
  per-project polling;dispatchError 按 active 项目分流。

前端全绿:vue-tsc + vite build + 106 vitest(含 slice-boundary)通过。

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@ghbvf ghbvf added the pr-status/in-progress ship 实施 + 内置 review/fix 中 label Jun 17, 2026
6 维 reviewer findings 的 small+IN_SCOPE 修复:
- F1 [P1·安全] config/model.rs: project.id 字符集校验——对所有项目(含 disabled)
  拒含 `:`/空白的 id(id 作 ledger/registry store-key 前缀,`:` 会污染分区→去重失效
  →重复 review storm);加 validate 测试。Medium 载体(id 格式锁;Hard 路径 ProjectId
  newtype,future)。
- F2 [测试] pr/webhook.rs: 加「repo 不在任何路由→None」fail-closed 用例。
- F6 [DX] 单源化 "default":service.rs `MIGRATED_PROJECT_ID` 常量;新 src/config/
  defaults.ts(DEFAULT_PROJECT_ID + NEW_PROJECT_DEFAULTS),OnboardingWizard +
  ProjectsManager 共用。
- F7 [DX] pr/registry.rs `project` → `project_snapshot`(消与 config::service::project
  重名歧义)+ commands.rs 调用点。
- F8 [DX] config/api.ts 加 setActiveProject wrapper,projects.ts 改 import(命令名
  集中,Soft→Medium)。
- F9 [架构] config::service `pub use Project` re-export;pr/scheduler.rs 经 service
  依赖 Project(不直接 reach config::model)。
- F3 [UX] PrList 折叠态随切项目重置。F4 [UX] hydrateActiveSession 按 activeProjectId
  过滤。F5 [UX] 删项目加确认。F11 [UX] loadSnapshot 置 loading 防空态闪现。F13 [UX]
  WebhookPanel 加「增删项目需重启隧道」静态提示。
- F10 [测试] session.rs SessionInfo golden 加 kind 断言。F12 [可靠性] 4 处并发/生命周期
  假设补注释(ledger stale-read+registry 兜底 / stop project_id="" / codex spawn cwd /
  set_active 无锁 trade-off)。

全绿:cargo test 202 + fmt + clippy(-D warnings) 0;前端 vitest 106 + vue-tsc + vite build。
遗留 large(人工决策):broadcast ring 容量并发洪泛(见 PR pm:ship 评论)。

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@ghbvf

ghbvf commented Jun 17, 2026

Copy link
Copy Markdown
Owner Author

🛠 ship review + fix

reviewer 6(按 diff 4277 行 → 六维度各 1)· Findings 16(已修 small 13 · 遗留 large 1 · OOS/已文档化 2 + 数条 P3 延后)

  • F1 [P1·small·安全] src-tauri/src/config/model.rs — project.id 无字符集校验,: 污染 store-key 分区→去重失效 → ✅ 已修(commit f23a017)
  • F2 [P2·small·测试] src-tauri/src/pr/webhook.rs — 缺 repo-未匹配→None fail-closed 用例 → ✅ 已修
  • F3 [P2·small·UX] src/pr/PrList.vue — 折叠态不随切项目重置 → ✅ 已修
  • F4 [P2·small·UX] src/review/useReviewStore.ts:285 — hydrateActiveSession 不按 projectId 过滤 → ✅ 已修
  • F5 [P2·small·UX] src/config/ProjectsManager.vue — 删项目无确认 → ✅ 已修
  • F6 [P2·small·DX] config/service.rs + src/config/defaults.ts — "default"/默认字段单源化 → ✅ 已修
  • F7 [P2·small·DX] src-tauri/src/pr/registry.rs — project→project_snapshot(消重名歧义)→ ✅ 已修
  • F8 [P2·small·DX] src/config/api.ts + src/projects.ts — set_active_project 集中到 api wrapper(Soft→Medium)→ ✅ 已修
  • F9 [P2·small·架构] src-tauri/src/pr/scheduler.rs — 经 config::service re-export 依赖 Project → ✅ 已修
  • F10 [P3·small·测试] src-tauri/src/review/session.rs — SessionInfo golden 补 kind 断言(events.rs kind pin 已存在)→ ✅ 已修
  • F11 [P3·small·UX] src/pr/usePrStore.ts — loadSnapshot 置 loading 防空态闪现 → ✅ 已修
  • F12 [P2·small·可靠性] session/ledger/service — 4 处并发/生命周期假设补注释 → ✅ 已修
  • F13 [P2·small·UX] src/pr/WebhookPanel.vue — 加「增删项目需重启隧道」静态提示 → ✅ 已修
  • L1 [P2·large·可靠性] src-tauri/src/review/session.rs broadcast ring cap=1024 → ⏸ 遗留(需人工决策)
  • L2 [P2·large·UX] webhook 路由表 start 时快照,运行期增删项目需手动重启 → 🚦 已文档化(核心限制,OOS)
  • A3 [P3·DX] SessionInfo↔TS 契约 funnel 下游手维护 → 🚦 既有 Medium,Hard 路径=codegen(全 golden 共有,future)
完整详表(根因 + 证据 + 建议 + 方案种子,/fix 读此)

L1 [P2·large·可靠性] src-tauri/src/review/session.rs:~536 / src-tauri/src/review/engines/codex/process.rs:~23

L2 [P2·large·UX] src-tauri/src/pr/commands.rs:~322 start_webhook / src/pr/WebhookPanel.vue

  • 根因:webhook 单端点按 repo 路由,路由表是 start_webhook 时 enabled 项目的快照,运行期增删项目不刷新。
  • 取舍:quick 隧道重启会换公网 URL(破坏已粘贴到 GitHub 的 URL),故不自动重启。已加静态 UI 提示(F13)+ Rust 注释。
  • 处置:🚦 OOS(核心设计限制,已文档化)。彻底路径(future)= 路由动态解析 per-request,免重启。

A3 [P3·DX] src-tauri/src/review/session.rs SessionInfo golden(上游 Medium 已锁)↔ src/review/types.ts ReviewSession(下游手维护 Soft)

  • 非闭环 funnel 下游开口——与本仓所有 serde golden 一致(model.rs/events.rs/config/model.rs 均如此),既有代码注释已标 future Hard 路径 = 从 Rust 模型 codegen TS + git diff --exit-code。非本 PR 新增缺口。
  • 处置:🚦 既有 Medium,保持;Hard 化是项目级工具链决策。

其余 P3 延后(非阻塞,低价值/可后续顺带):ProjectCard 项目名空值校验、ProjectsManager 重复 repo 的主动 inline 提示(后端 validate 已拒+save 报错)、store-key 前缀常量布局、usePrStore active-getter 样板、skill_abs_path 在 lib.rs/review/commands.rs 的重复、ProjectSwitcher 归属 pr 切片(运行时无越界,slice-boundary 测试过)。

全量门:cargo test 202 + fmt + clippy -D warnings 0 告警;前端 vitest 106 + vue-tsc + vite build 全绿。

下一步:切 pr-status/needs-review-again(待再审:codex / /pr-review #63;有需改再 /fix #63)。L1(ring 容量)需人工决策是否本 PR 处理或登 follow-up issue。


🤖 PR #63 · Generated with Claude Code · branch Feature/35-multi-project · worktree worktrees/Feature/35-multi-project

@ghbvf ghbvf added pr-status/needs-review-again ship 交接后首审一次(review changes-requested 后转 needs-fix) and removed pr-status/in-progress ship 实施 + 内置 review/fix 中 labels Jun 17, 2026
@ghbvf

ghbvf commented Jun 17, 2026

Copy link
Copy Markdown
Owner Author

✅ CI 收敛 — 全绿

check 结果 用时
Frontend (vue-tsc + build) ✅ pass 17s
Rust (fmt + clippy + build) ✅ pass 1m54s

run: https://github.com/ghbvf/prmonitor/actions/runs/27705438969

遗留 large finding L1(broadcast ring 并发洪泛)已按决策登 follow-up issue #64,本 PR 不改(非阻塞)。

状态:pr-status/needs-review-again(待 codex / /pr-review #63 再审)。

@ghbvf

ghbvf commented Jun 17, 2026

Copy link
Copy Markdown
Owner Author

🔍 pr-review(六维度分级审查)

根因簇 4 · Findings 6(P0 0·P1 1·P2 4·P3 1 | small 4·large 2)· 结论 需修复

根因簇

  • C1 配置 slice 的 Project 被当成跨切片契约外泄,pr/review 直接依赖 config service,而不是经 model.rs 或 composition root 注入最小上下文。(维度 架构/切片边界;系统性 Grep 25 处)→ F1
  • C2 项目/Review 命令边界缺少规范化与白名单校验,导致同仓库大小写变体、悬空 active 项目、任意 review kind 进入核心状态。(维度 安全/健壮、可靠性;系统性 Grep 6+ 处)→ F2,F3,F4
  • C3 前端 review 焦点是全局单例,切项目只清 PR selection,没有清空/按项目隔离 focused review session。(维度 可靠性/生命周期、产品/用户体验;系统性 Grep 69 处)→ F5
  • C4 项目切换先乐观更新本地 active id,再持久化;失败时 UI 无回滚/错误状态。(维度 产品/用户体验;系统性 Grep 2 处)→ F6

Findings(每条带 file:line,/fix 无损提取)

  • F1 [P1·large·架构/切片边界] src-tauri/src/config/service.rs:19 — Project 被 re-export 为跨切片类型,pr/review 直接读取 config service → 簇 C1
  • F2 [P2·small·安全/健壮] src-tauri/src/config/model.rs:305 — repo 去重按原始字符串,大小写变体可绕过唯一约束 → 簇 C2
  • F3 [P2·small·安全/健壮] src-tauri/src/config/model.rs:316 — 非空 projects 时未验证 active id 指向现有项目 → 簇 C2
  • F4 [P2·small·安全/健壮] src-tauri/src/review/commands.rs:56 — start_review kind 未白名单,非 check 字符串会作为 review 执行 → 簇 C2
  • F5 [P2·large·可靠性/生命周期/产品] src/App.vue:162 — 切项目后 ReviewPanel 仍可显示/停止前一项目 session → 簇 C3
  • F6 [P3·small·产品/用户体验] src/projects.ts:17 — 项目切换先更新本地 active id,持久化失败无回滚提示 → 簇 C4
完整详表(证据 + 建议 + 根因 + 方案种子,/fix 读此)

F1 [P1·large·架构/切片边界] src-tauri/src/config/service.rs:19(→ C1)

  • 证据:pub use super::model::Project;,且 src-tauri/src/pr/*、src-tauri/src/review/* 多处直接 use crate::config::service as config_service 并读取 Project 字段。
  • 根因:config slice 的 domain model 被包装成 service public API 后仍作为跨切片契约传播,绕开了 reviewer 约束里的 model.rs/composition-root 边界。
  • 三级方案种子:最小 将 project()/project_validated() 返回拆成 pr/review 所需最小 DTO;彻底 将跨切片 DTO 放入 model.rs 并禁止 slice 直接 import config;重构 由 lib.rs/composition root 解析 config 后向 pr/review 注入纯参数。

F2 [P2·small·安全/健壮] src-tauri/src/config/model.rs:305(→ C2)

  • 证据:seen_repos.insert(project.repo.as_str());webhook 路由却用 r.repo.eq_ignore_ascii_case(event_repo),GitHub repo 语义大小写不敏感。
  • 建议:保存前对 repo 做规范化去重,例如 lower-case 后进入 seen_repos,并加 Owner/Repo 与 owner/repo 重复测试。

F3 [P2·small·安全/健壮] src-tauri/src/config/model.rs:316(→ C2)

  • 证据:项目循环完成后直接 Ok(()),没有检查 active_project_id;前端 hydrate 直接使用 cfg.activeProjectId。
  • 建议:当 projects 非空时要求 active_project_id 命中现有项目,或保存时规范化为首个项目;空 projects 仍保留 first-launch 语义。

F4 [P2·small·安全/健壮] src-tauri/src/review/commands.rs:56(→ C2)

  • 证据:Tauri command 接收 kind: String;session.rs:635 中 kind == "check" 才走 check,其余任意值都执行 /pr-review <N>,但 registry key 仍保留原始 kind。
  • 建议:在 command 边界拒绝非 review | check,并补 kind="foo" 不会启动 session 的测试;后续可提升为 Rust enum/TS union。

F5 [P2·large·可靠性/生命周期/产品] src/App.vue:162(→ C3)

  • 证据:active project watcher 只执行 selectedNumber.value = null;useReviewStore.ts 的 activeThreadId/activePr/running/finalStatus/items 是全局单例;ReviewPanel.vue:75 仍按全局 activePr 渲染,停止按钮直接 stop 当前 thread。
  • 三级方案种子:最小 activeProjectId 变化时 clear focused review;彻底 active focus 建模为 { projectId, threadId } 并在 render/stop 前校验;重构 review store 按 projectId 分区 active focus 与 stream state。

F6 [P3·small·产品/用户体验] src/projects.ts:17(→ C4)

  • 证据:activeProjectId.value = id 发生在 await setActiveProject(id) 前;后端写配置失败或 id stale 时 UI 已切走,且 ProjectSwitcher 没有错误状态。
  • 建议:持久化成功后再更新本地 active id,或失败时 rollback 到旧值并在 active project UI 上显示错误。

复杂度汇总:按根因簇 small 2 / large 2;按 Finding small 4 / large 2。

修复分流:small → /fix #63(C2、C4);large → 需人工决策(C1、C3,方案种子见详表)。← issue #35

结论:需修复。PR 的多项目主路径方向成立,但当前跨切片契约、输入边界和 review 焦点隔离都有会影响后续演进或用户操作正确性的缺口。


🤖 PR #63 · Generated with Claude Code · branch Feature/35-multi-project · worktree /Users/shengming/Documents/code/prmonitor/worktrees/Feature/35-multi-project · session —

@ghbvf ghbvf added pr-review/changes-requested review 提出需改项 pr-status/needs-fix review 出 changes-requested,待 /fix and removed pr-status/needs-review-again ship 交接后首审一次(review changes-requested 后转 needs-fix) labels Jun 17, 2026
/pr-review #63 最新一轮 small+IN_SCOPE findings:
- F2 [P2·安全] config/model.rs: repo 去重按小写规范化(GitHub repo 大小写不敏感,
  webhook 已用 eq_ignore_ascii_case;Owner/Repo 与 owner/repo 视为同仓拒重)+ 测试。
- F3 [P2·安全] config/model.rs: projects 非空时校验 activeProjectId 命中现有项目
  (悬空指针会 strand UI / 丢失 codex 握手 cwd);空 projects 保留首启语义 + 测试。
- F4 [P2·安全] review/commands.rs: start_review 命令边界白名单 kind ∈ {review,check}
  (session.rs 把非 check 一律当 review,未校验会以伪 key 跑 review)+ validate_kind
  纯函数测试。
- F5 [P2·可靠性/产品] useReviewStore 加 clearFocus();App.vue watch(activeProjectId)
  切项目时清焦点——焦点流是全局单例,否则 ReviewPanel 仍显示/可停止前一项目的
  session(最小修;per-project 焦点分区彻底方案 deferred)。
- F6 [P3·产品] projects.ts setActive 改 persist-first(setActiveProject 成功后再更新
  本地 activeProjectId),持久化失败不切走 UI + projects.test.ts 锁定。

遗留 F1 [P1·large·架构] Project 跨切片 re-export——不可自动执行,surface 人工决策。

全绿:cargo test 205 + fmt + clippy 0;前端 vitest 109 + vue-tsc + vite build。

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@ghbvf

ghbvf commented Jun 17, 2026

Copy link
Copy Markdown
Owner Author

🔁 fix(findings triage + fix)

Findings 6(已修 small 5 · 遗留 large 1 · OUT_OF_SCOPE 0)· 来源 pm:pr-review

  • F2 [P2·small·安全] src-tauri/src/config/model.rs:305 — repo 去重大小写规范化(同仓大小写变体拒重)→ ✅ 已修(commit 15be9d7)
  • F3 [P2·small·安全] src-tauri/src/config/model.rs:316 — projects 非空时校验 activeProjectId 命中现有项目 → ✅ 已修
  • F4 [P2·small·安全] src-tauri/src/review/commands.rs:56 — start_review 白名单 kind ∈ {review,check} → ✅ 已修
  • F5 [P2·large·可靠性/产品] src/App.vue:162 — 切项目清 review 焦点(clearFocus,最小修)→ ✅ 已修
  • F6 [P3·small·产品] src/projects.ts:17 — 切项目 persist-first,失败不切走 → ✅ 已修
  • F1 [P1·large·架构] src-tauri/src/config/service.rs:19 — Project 跨切片 re-export → ⏸ 遗留(需人工决策)
完整详表(triage 依据 + 证据 + 建议,下次 fix 读此)

F2 [P2·small·安全] config/model.rs(IN_SCOPE)

  • 证据:seen_repos.insert(project.repo.as_str()) 按原始字符串;webhook 路由用 eq_ignore_ascii_case。GitHub repo 大小写不敏感。
  • 修复:seen_repos: HashSet<String>,插入 project.repo.to_ascii_lowercase();加 validate_rejects_duplicate_project_repos_case_insensitive 测试。→ ✅ commit 15be9d7

F3 [P2·small·安全] config/model.rs(IN_SCOPE)

  • 证据:per-project 循环后直接 Ok(()),未校验 active_project_id。
  • 修复:projects 非空时要求 active id 命中某项目(悬空指针会 strand UI + 丢 codex 握手 cwd);空 projects 保留首启语义;加 validate_rejects_dangling_active_project_id 测试。→ ✅

F4 [P2·small·安全] review/commands.rs(IN_SCOPE)

  • 证据:kind: String 透传;session.rs kind=="check" 才走 check,其余任意值跑 review,registry key 仍存原始 kind。
  • 修复:提取纯函数 validate_kind,在 start_review 入口(任何副作用前)拒绝非 {review,check};加 validate_kind_accepts_review_and_check_only 测试。后续可升 Rust enum/TS union(Hard)。→ ✅

F5 [P2·large·可靠性/产品] App.vue + review/useReviewStore.ts(IN_SCOPE,最小修)

  • 证据:active project watcher 只 selectedNumber=null;focused review(activeThreadId/activePr/running/items)是全局单例,ReviewPanel 仍按全局 activePr 渲染、停止按钮停当前 thread。
  • 修复(最小 A):useReviewStore 加 clearFocus(),App.vue watch(activeProjectId) 切项目时调用——清焦点(session 仍在后端运行、仍在按项目过滤的 ReviewSessions 列表可见,用户点选重新聚焦)。
  • 三级方案种子:最小 切项目 clearFocus(本次已做)/ 彻底 active focus 建模 {projectId, threadId} render/stop 前校验 / 重构 review store 按 projectId 分区 focus+stream。
  • 遗留:彻底/重构(per-project 焦点分区)超出 single-active MVP 范围,deferred;最小修已消除「显示/停止错项目 session」的用户可见正确性问题。

F6 [P3·small·产品] projects.ts(IN_SCOPE)

  • 证据:activeProjectId.value = id 在 await setActiveProject(id) 之前;持久化失败 UI 已切走且无回滚。
  • 修复:persist-first(成功后再更新本地 id),失败抛错且本地停留旧项目;加 projects.test.ts 锁定 persist-first + 失败不切走。→ ✅

F1 [P1·large·架构] config/service.rs:19(IN_SCOPE,遗留——不可自动执行)

  • 证据:pub use super::model::Project;(本是 /ship 阶段为满足上一轮 arch reviewer「pr 勿直接 import config::model」而加的 re-export);pr/review 经 config::service 读 Project 字段(Grep 25 处)。
  • 根因/取舍:config domain model 经 service 外泄为跨切片契约。但这是 [feat] 多项目并行监听 + 项目切换 UI(参考 codex app) #35 之前既有的模式(pr 早已经 config::service::load_validated 消费 config::model::AppConfig),[feat] 多项目并行监听 + 项目切换 UI(参考 codex app) #35 只是扩展。完整修复(最小 DTO / 移 model.rs / composition-root 注入纯参数)是跨 25 处、跨切片契约的 large 改动,属「不可自动执行」。
  • 三级方案种子:最小 project()/project_validated() 返回拆成 pr/review 所需最小 DTO;彻底 跨切片 DTO 放 model.rs 并禁 slice 直接 import config;重构 由 lib.rs 解析 config 后向 pr/review 注入纯参数。
  • 遗留原因:large 架构改动 + 与既有模式纠缠 + 本 PR 已极大;建议登 follow-up issue 专门评估,不在本 PR 扩面。

下一步:切 pr-status/needs-check-fix,待 /pr-review #63 --check 验证(fix 不自证完成)。F1(large 架构)需人工决策——见 PR 窗口/下方。


🤖 PR #63 · Generated with Claude Code · branch Feature/35-multi-project · worktree worktrees/Feature/35-multi-project

@ghbvf ghbvf added pr-status/needs-check-fix /fix 已修,待 /pr-review --check 验证 and removed pr-status/needs-fix review 出 changes-requested,待 /fix labels Jun 17, 2026
@ghbvf

ghbvf commented Jun 17, 2026

Copy link
Copy Markdown
Owner Author

✅ CI 收敛 — 全绿(fix 轮)

check 结果 用时
Frontend (vue-tsc + build) ✅ pass 19s
Rust (fmt + clippy + build) ✅ pass 1m42s

run: https://github.com/ghbvf/prmonitor/actions/runs/27709055054

/pr-review #63 6 findings:F2–F6(small)已修(commit 15be9d7);遗留 F1(P1·large·架构 Project 跨切片 re-export)按决策登 follow-up issue #65,本 PR 不改。

状态:pr-status/needs-check-fix(待 /pr-review #63 --check 验证)。

@ghbvf

ghbvf commented Jun 17, 2026 •

Copy link
Copy Markdown
Owner Author

🔍 pr-review(--check 修复验证)

Findings 6(已修复 5·已转 follow-up 1·未修复 0·回归 0·部分 0)· 结论 通过

验证表

  • F1 [P1·large·架构/切片边界] src-tauri/src/config/service.rs:19 — 🟡 已转 follow-up。当前代码仍保留 Project re-export 和跨切片 config service 依赖,但该项已按人工决策登记为 issue [architecture] config Project 经 service 外泄为跨切片契约——pr/review 应经 model.rs/注入解耦(#35 follow-up) #65,且 PR 评论明确说明本 PR 不改;不作为本轮合入阻塞。
  • F2 [P2·small·安全/健壮] src-tauri/src/config/model.rs:305 — ✅ 已修复。repo 去重已改为 project.repo.to_ascii_lowercase() 后写入 seen_repos,并有大小写重复 repo 测试覆盖。
  • F3 [P2·small·安全/健壮] src-tauri/src/config/model.rs:316 — ✅ 已修复。非空 projects 时现在校验 active_project_id 必须命中现有项目,空项目仍保留首启语义,并有 dangling active id 测试覆盖。
  • F4 [P2·small·安全/健壮] src-tauri/src/review/commands.rs:56 — ✅ 已修复。start_review 在任何副作用前调用 validate_kind,仅接受 review | check,并有非法 kind 测试覆盖。
  • F5 [P2·large·可靠性/生命周期/产品] src/App.vue:162 — ✅ 已修复。切换 activeProjectId 时现在调用 clearFocus(),会清空 activeThreadId/activePr/running/finalStatus/error/items;ReviewSessions 仍按 active project 过滤,未看到该站点直接回归。
  • F6 [P3·small·产品/用户体验] src/projects.ts:17 — ✅ 已修复。setActive 现在先 await setActiveProject(id),成功后才更新本地 activeProjectId;失败不会切走,并有 persist-first / failure 测试覆盖。
完整详表(验证证据 + 测试)

F1 [P1·large·架构/切片边界] src-tauri/src/config/service.rs:19

F2 [P2·small·安全/健壮] src-tauri/src/config/model.rs:305

  • 状态:✅ 已修复
  • 证据:repo 唯一性检查现在使用 seen_repos.insert(project.repo.to_ascii_lowercase()),与 webhook 的大小写不敏感匹配语义一致。
  • 测试:validate_rejects_duplicate_project_repos_case_insensitive 覆盖 Owner/Repo 与 owner/repo。

F3 [P2·small·安全/健壮] src-tauri/src/config/model.rs:316

  • 状态:✅ 已修复
  • 证据:!config.projects.is_empty() 时现在要求 active_project_id 命中 config.projects 中某个 id;空项目仍允许 onboarding。
  • 测试:validate_rejects_dangling_active_project_id 覆盖悬空 id、有效 id、空项目首启语义。

F4 [P2·small·安全/健壮] src-tauri/src/review/commands.rs:56

  • 状态:✅ 已修复
  • 证据:start_review 在解析 project / resume codex 前调用 validate_kind(&kind)?;validate_kind 只接受 review 或 check。
  • 测试:validate_kind_accepts_review_and_check_only 覆盖空串、大小写变体和任意字符串拒绝。

F5 [P2·large·可靠性/生命周期/产品] src/App.vue:162

  • 状态:✅ 已修复
  • 证据:watch(activeProjectId, ...) 同时清 selectedNumber 和 clearFocus();clearFocus() 清空 focused stream 的全部关键 refs。ReviewSessions.vue 继续用 s.projectId === activeProjectId.value 过滤列表,切回原项目可重新聚焦。
  • 回归检查:未发现停止按钮继续指向旧项目 session 的路径;目标测试覆盖 clearFocus() 清空 focused session。

F6 [P3·small·产品/用户体验] src/projects.ts:17

  • 状态:✅ 已修复
  • 证据:setActive 现在 await setActiveProject(id) 后才 activeProjectId.value = id。
  • 测试:projects.test.ts 覆盖 persist-first 顺序和 persist 失败不切换本地 active id。

测试

  • pnpm vitest run src/projects.test.ts src/review/useReviewStore.test.ts:42 tests passed。
  • cargo test --manifest-path src-tauri/Cargo.toml --locked validate:21 tests passed。

汇总:已修复 5 / 已转 follow-up 1 / 未修复 0 / 回归 0 / 部分 0。

结论:通过。F2-F6 的修复已验证通过,未看到对应站点回归;F1 是已决策转 follow-up 的 large 架构项,issue #65 持续跟踪,非本 PR 阻塞。


🤖 PR #63 · Generated with Claude Code · branch Feature/35-multi-project · worktree /Users/shengming/Documents/code/prmonitor/worktrees/Feature/35-multi-project · session —

@ghbvf ghbvf added pr-status/needs-fix review 出 changes-requested,待 /fix pr-status/ready --check 验证全修复,可合并 pr-review/approved review 无需改 and removed pr-status/needs-check-fix /fix 已修,待 /pr-review --check 验证 pr-status/needs-fix review 出 changes-requested,待 /fix pr-review/changes-requested review 提出需改项 labels Jun 17, 2026
@ghbvf
ghbvf merged commit 49df794 into develop Jun 17, 2026
2 checks passed
@ghbvf
ghbvf deleted the Feature/35-multi-project branch July 4, 2026 16:02
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

pr-review/approved review 无需改 pr-status/ready --check 验证全修复,可合并

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[feat] 多项目并行监听 + 项目切换 UI(参考 codex app)

1 participant