From 3c781802c27797854828737159b2626f77d0601f Mon Sep 17 00:00:00 2001 From: limityan Date: Thu, 9 Jul 2026 18:23:04 +0800 Subject: [PATCH] refactor(review): converge strict review entry --- .../src/i18n/generatedLocaleContract.ts | 6 +- docs/architecture/deep-review.md | 39 +- docs/architecture/theme-token-optimization.md | 2 +- docs/development/ui-testids-CN.md | 4 - docs/development/ui-testids.md | 4 - docs/sdlc-harness/README.md | 3 +- .../agent-workflow-staged-plan.md | 12 +- .../architecture/agent-workflow-design.md | 22 +- .../architecture/evidence-pack.md | 2 +- .../architecture/quality-data-plane.md | 4 +- .../architecture/security-boundary.md | 2 +- .../sdlc-harness/features/agent-evaluation.md | 6 +- .../features/configurable-policy-profile.md | 2 +- docs/sdlc-harness/features/pr-quality-gate.md | 8 +- docs/sdlc-harness/features/risk-classifier.md | 4 +- docs/sdlc-harness/governance/metrics-spec.md | 2 +- .../governance/self-governance-notes.md | 2 +- docs/sdlc-harness/implementation-plan.md | 10 +- ...-requirements-agent-workflow-adjustment.md | 42 +- .../research/external-research.md | 4 +- ...review-verify-implementation-guardrails.md | 157 +++++ docs/sdlc-harness/traceability-matrix.md | 1 + scripts/generate-i18n-contract.mjs | 2 +- scripts/i18n-governance-baseline.json | 14 +- .../service/i18n/generated_locale_contract.rs | 8 +- .../assembly/core/src/service/i18n/service.rs | 2 +- .../src/i18n/generatedLocaleContract.ts | 6 +- .../i18n/resources/shared/en-US/terms.json | 2 +- .../i18n/resources/shared/zh-CN/terms.json | 2 +- .../i18n/resources/shared/zh-TW/terms.json | 2 +- .../sections/sessions/SessionsSection.tsx | 4 +- .../review-platform/ReviewPlatformPanel.tsx | 2 +- .../app/scenes/agents/AgentsScene.test.tsx | 19 +- .../src/app/scenes/agents/AgentsScene.tsx | 95 --- .../src/app/scenes/agents/agentsStore.ts | 8 +- .../agents/components/AgentTeamCard.scss | 152 ----- .../agents/components/AgentTeamCard.test.tsx | 61 -- .../agents/components/AgentTeamCard.tsx | 80 --- .../agents/components/ReviewTeamPage.scss | 640 ------------------ .../agents/components/ReviewTeamPage.test.tsx | 377 ----------- .../agents/components/ReviewTeamPage.tsx | 565 ---------------- .../app/scenes/agents/hooks/useAgentsList.ts | 6 +- .../src/app/scenes/settings/settingsConfig.ts | 10 +- .../src/flow_chat/components/ChatInput.tsx | 13 +- .../components/DeepReviewConsentDialog.scss | 211 ++---- .../DeepReviewConsentDialog.test.tsx | 79 +-- .../components/DeepReviewConsentDialog.tsx | 224 ++---- .../TaskDetailPanel/TaskDetailPanel.tsx | 56 +- .../components/btw/BtwSessionPanel.tsx | 2 +- .../btw/DeepReviewActionBar.i18n.test.ts | 37 +- .../btw/DeepReviewActionBar.test.tsx | 32 +- .../modern/SessionFilesBadge.test.tsx | 31 + .../src/flow_chat/deep-review/README.md | 12 +- .../action-bar/CapacityQueueNotice.test.tsx | 46 +- .../action-bar/CapacityQueueNotice.tsx | 72 +- .../action-bar/DeepReviewActionBar.tsx | 16 +- .../action-bar/ReviewActionControls.test.tsx | 4 +- .../action-bar/ReviewActionControls.tsx | 4 - .../action-bar/ReviewActionHeader.tsx | 4 - .../interruptionDiagnostics.test.ts | 7 +- .../action-bar/interruptionDiagnostics.ts | 16 +- .../deep-review/launch/DeepReviewService.ts | 26 +- .../deep-review/launch/commandParser.test.ts | 16 +- .../deep-review/launch/commandParser.ts | 15 +- .../deep-review/launch/launchErrors.test.ts | 10 +- .../deep-review/launch/launchErrors.ts | 18 +- .../deep-review/launch/launchPrompt.ts | 6 +- .../deep-review/report/codeReviewReport.ts | 23 +- .../report/manifestSections.test.ts | 172 ----- .../deep-review/report/manifestSections.ts | 172 ----- .../deep-review/report/markdown.test.ts | 22 +- .../flow_chat/deep-review/report/markdown.ts | 62 +- .../report/reliabilityNotices.test.ts | 6 +- .../deep-review/report/reliabilityNotices.ts | 12 +- .../deep-review/report/reportSections.ts | 2 +- .../report/reviewCoverageSource.test.ts | 13 + .../report/reviewCoverageSource.ts | 69 ++ .../services/DeepReviewService.test.ts | 90 +-- .../store/deepReviewActionBarStore.test.ts | 2 +- .../CodeReviewReportExportActions.test.tsx | 25 +- .../CodeReviewReportExportActions.tsx | 19 +- .../tool-cards/CodeReviewToolCard.test.tsx | 123 +++- .../tool-cards/CodeReviewToolCard.tsx | 346 +++------- .../tool-cards/TaskToolDisplay.test.tsx | 35 + .../flow_chat/tool-cards/TaskToolDisplay.tsx | 59 +- .../utils/codeReviewRemediation.test.ts | 10 +- .../flow_chat/utils/codeReviewRemediation.ts | 6 +- .../flow_chat/utils/codeReviewReport.test.ts | 137 ++-- .../utils/deepReviewCommandGuard.test.ts | 12 +- .../flow_chat/utils/deepReviewConstants.ts | 8 +- .../utils/deepReviewContinuation.test.ts | 31 +- .../flow_chat/utils/deepReviewContinuation.ts | 64 +- .../config/components/ReviewConfig.test.tsx | 274 ++++++++ .../config/components/ReviewConfig.tsx | 329 +-------- .../i18n/presets/generatedLocaleContract.ts | 6 +- src/web-ui/src/locales/en-US/flow-chat.json | 353 +++++----- .../src/locales/en-US/scenes/agents.json | 118 +--- src/web-ui/src/locales/en-US/settings.json | 2 +- .../src/locales/en-US/settings/review.json | 110 +-- src/web-ui/src/locales/zh-CN/flow-chat.json | 345 +++++----- .../src/locales/zh-CN/scenes/agents.json | 118 +--- src/web-ui/src/locales/zh-CN/settings.json | 37 +- .../src/locales/zh-CN/settings/review.json | 100 +-- src/web-ui/src/locales/zh-TW/flow-chat.json | 347 +++++----- .../src/locales/zh-TW/scenes/agents.json | 118 +--- src/web-ui/src/locales/zh-TW/settings.json | 37 +- .../src/locales/zh-TW/settings/review.json | 100 +-- .../shared/services/review-team/defaults.ts | 8 +- .../src/shared/services/review-team/index.ts | 2 +- .../services/review-team/promptBlock.ts | 20 +- .../src/shared/services/review-team/risk.ts | 2 +- .../services/review-team/scopeProfile.ts | 2 +- .../shared/services/review-team/strategy.ts | 14 +- .../reviewTeamLocaleCompleteness.test.ts | 17 +- .../shared/services/reviewTeamService.test.ts | 28 +- 115 files changed, 2192 insertions(+), 5167 deletions(-) create mode 100644 docs/sdlc-harness/review-verify-implementation-guardrails.md delete mode 100644 src/web-ui/src/app/scenes/agents/components/AgentTeamCard.scss delete mode 100644 src/web-ui/src/app/scenes/agents/components/AgentTeamCard.test.tsx delete mode 100644 src/web-ui/src/app/scenes/agents/components/AgentTeamCard.tsx delete mode 100644 src/web-ui/src/app/scenes/agents/components/ReviewTeamPage.scss delete mode 100644 src/web-ui/src/app/scenes/agents/components/ReviewTeamPage.test.tsx delete mode 100644 src/web-ui/src/app/scenes/agents/components/ReviewTeamPage.tsx delete mode 100644 src/web-ui/src/flow_chat/deep-review/report/manifestSections.test.ts delete mode 100644 src/web-ui/src/flow_chat/deep-review/report/manifestSections.ts create mode 100644 src/web-ui/src/flow_chat/deep-review/report/reviewCoverageSource.test.ts create mode 100644 src/web-ui/src/flow_chat/deep-review/report/reviewCoverageSource.ts create mode 100644 src/web-ui/src/infrastructure/config/components/ReviewConfig.test.tsx diff --git a/BitFun-Installer/src/i18n/generatedLocaleContract.ts b/BitFun-Installer/src/i18n/generatedLocaleContract.ts index 3c0bc3123..f87af2a6b 100644 --- a/BitFun-Installer/src/i18n/generatedLocaleContract.ts +++ b/BitFun-Installer/src/i18n/generatedLocaleContract.ts @@ -59,7 +59,7 @@ export const SHARED_TERMS_BY_APP_LANGUAGE = { "features": { "remoteControl": "Remote Control", "codeAgent": "Code Agent", - "deepReview": "Deep Review", + "deepReview": "Review: Strict", "settings": "Settings", "workspace": "Workspace", "accountLogin": "Account Login" @@ -110,7 +110,7 @@ export const SHARED_TERMS_BY_APP_LANGUAGE = { "features": { "remoteControl": "远程控制", "codeAgent": "代码助手", - "deepReview": "深度代码评审", + "deepReview": "严格审查", "settings": "设置", "workspace": "工作区", "accountLogin": "账户登录" @@ -161,7 +161,7 @@ export const SHARED_TERMS_BY_APP_LANGUAGE = { "features": { "remoteControl": "遠端控制", "codeAgent": "程式碼助手", - "deepReview": "深度程式碼審查", + "deepReview": "嚴格審查", "settings": "設定", "workspace": "工作區", "accountLogin": "帳號登入" diff --git a/docs/architecture/deep-review.md b/docs/architecture/deep-review.md index ba5b275c1..b00b2413e 100644 --- a/docs/architecture/deep-review.md +++ b/docs/architecture/deep-review.md @@ -1,14 +1,23 @@ -# DeepReview Architecture +# DeepReview / Strict Review Architecture ## Scope -DeepReview is a child-session workflow that runs a configurable Code Review Team against a review target. The current implementation has three layers: +DeepReview is the compatibility runtime for `Review: Strict`, the highest-strength mode of the unified Review experience. It remains implemented as a child-session workflow that runs a configurable read-only reviewer set against a review target, but it should not be presented as a second ordinary product entry next to Review. + +Product-facing guardrails live in [review-verify-implementation-guardrails.md](../sdlc-harness/review-verify-implementation-guardrails.md): + +- `Review` is the primary user-facing entry. +- `/review` is the intended long-term command entry; `/DeepReview` is only a transitional typed compatibility command for historical strict-review launches. +- `ReviewTeam` is an internal strict-review reviewer-set configuration, not a separate product concept users must learn. +- PR Review consumes review/verify results and readiness projections; it must not own another reviewer executor. + +The current implementation has three layers: - Frontend launch and UI orchestration in `src/web-ui`. - Platform adapter commands in `src/apps/desktop/src/api/agentic_api.rs`. - Platform-agnostic runtime policy, task admission, queue state, retry metadata, and report enrichment in `src/crates/assembly/core/src/agentic`. -The backend does not choose the review target or build the launch manifest. The frontend builds the effective `ReviewTeamRunManifest`, persists it on the DeepReview child session, and sends it with the first user message. +The backend does not choose the review target or build the launch manifest. The frontend builds the effective `ReviewTeamRunManifest`, persists it on the DeepReview child session, and sends it with the first user message. The manifest, session kind, agent type, storage keys, and queue event names stay compatible with historical DeepReview sessions. ## Runtime Roles @@ -29,14 +38,14 @@ The reviewer agents use instruction-only context and read/search/git/diff tools. ## Launch Flow -DeepReview can be launched from session-file review controls or a `/DeepReview` slash command. +Strict Review can currently be launched from session-file review controls or the canonical typed `/review strict` command. Historical `/DeepReview` and `/deepreview` inputs remain compatibility aliases that route into the same strict Review path, but new product entry points should route through unified Review and `/review`, then select strict review only when task scope, difficulty, risk, explicit quality intent, and budget justify L3. Frontend launch code lives in `src/web-ui/src/flow_chat/deep-review/launch`: -- `commandParser.ts` identifies `/DeepReview` commands and optional file or git targets. +- `commandParser.ts` identifies canonical `/review strict` commands, transitional `/DeepReview` compatibility aliases, and optional file or git targets. - `targetResolver.ts` resolves slash-command targets from git status, changed files, and diffs when a workspace is available. - `launchPrompt.ts` formats the user-facing launch prompt. -- `DeepReviewService.ts` builds the review-team manifest, creates a child session, opens it in the auxiliary pane, sends the launch prompt, and inserts the parent-session summary marker. +- `DeepReviewService.ts` builds the review-team manifest, creates a child session, sends the launch prompt, and inserts the parent-session summary marker. Launch does not automatically open the auxiliary pane; the summary-card detail action is the normal user-facing way to inspect the background review run. - `src/web-ui/src/flow_chat/services/DeepReviewService.ts` is a compatibility re-export. `launchDeepReviewSession` creates a child session with: @@ -49,11 +58,11 @@ Frontend launch code lives in `src/web-ui/src/flow_chat/deep-review/launch`: - context compression enabled - `deepReviewRunManifest` stored on the child session metadata -If launch fails after the child session is created, the frontend closes the auxiliary pane, deletes the backend session when possible, discards local session state, and reports cleanup issues with the launch error. +If launch fails after the child session is created, the frontend runs idempotent UI/session cleanup, deletes the backend session when possible, discards local session state, and reports cleanup issues with the launch error. -## Review Team Configuration +## Strict Reviewer Configuration -The default review team contract is mirrored in Rust and TypeScript. +The default strict reviewer configuration contract is mirrored in Rust and TypeScript. Rust source: @@ -72,8 +81,8 @@ The desktop command `get_default_review_team_definition` returns the backend def The persisted config path is `ai.review_teams.default`. The frontend config shape includes: - extra subagent ids -- team strategy level -- per-member strategy overrides +- review strategy level +- per-reviewer strategy overrides - reviewer and judge timeouts - reviewer file-split threshold - max same-role instances @@ -83,7 +92,7 @@ The persisted config path is `ai.review_teams.default`. The frontend config shap - provider capacity queue enablement - bounded auto-retry enablement and elapsed guard -Extra team members must be enabled subagents with read-only review tooling. Core team members, `DeepReview`, and `ReviewFixer` are disallowed as extra members. +Extra reviewers must be enabled subagents with read-only review tooling. Core reviewers, `DeepReview`, and `ReviewFixer` are disallowed as extra reviewers. ## Manifest Shape @@ -290,6 +299,6 @@ The review action bar persists UI state separately through `ReviewActionBarPersi When changing DeepReview behavior, update all affected contracts together: - Backend constants, team definition, execution policy, manifest gate, task adapter, queue events, and report enrichment. -- Frontend review-team defaults/types, manifest builder, prompt block, launch service, action-bar store, event mapping, report rendering, and locales. -- Desktop Tauri command DTOs when queue controls or default team definition contracts change. -- Tests near the touched module, especially policy tests, review-team manifest tests, queue event tests, launch tests, action-bar tests, and locale completeness tests. +- Frontend strict-review defaults/types, manifest builder, prompt block, launch service, action-bar store, event mapping, report rendering, and locales. +- Desktop Tauri command DTOs when capacity controls or default review definition contracts change. +- Tests near the touched module, especially policy tests, strict-review manifest tests, queue event tests, launch tests, action-bar tests, and locale completeness tests. diff --git a/docs/architecture/theme-token-optimization.md b/docs/architecture/theme-token-optimization.md index 4dc9a0871..3ebda7e48 100644 --- a/docs/architecture/theme-token-optimization.md +++ b/docs/architecture/theme-token-optimization.md @@ -465,7 +465,7 @@ Phase 5 决策记录: | low-external implementation key compression | retire preview/editor/gallery/MissionControl/mobile helper keys | `tokens.scss`, Markdown editor/Tiptap styles, component preview CSS/examples, GalleryLayout, MissionControl, NavSearchDialog, ContextMenu, Select, config/profile form styles, `src/mobile-web/src/theme/presets/shared.ts`, mobile SCSS | 将只表达局部实现或严格同义的低外部使用 key 移出 static root:Markdown editor list/task sizing、Markdown line-number gutter、gallery grid/skeleton defaults、preview palette/timing、旧 `flowchat-card-header-pad-*` 别名、`border-focus` 同义别名、glass green/disabled helper、MissionControl group helper 和 slate22 shadow helper。MissionControl 的组别区分保留为组件私有 modifier class,并继续使用 accent/success/warning;active filter 和 thumbnail badge 使用中性底加彩色指示,避免小字号实底语义色造成对比或状态误解;preview 和 gallery 不作为主题扩展入口;`--easing-smooth` 与 `--easing-standard` 当前同值,统一读 standard;mobile-web 同步删除未使用的 `--motion-instant`、同值 `--easing-smooth` 和不应作为主题扩展入口的 `--easing-decelerate`,现有 decelerate 动画由 mobile-local Sass owner 承载并补齐 reduced-motion 覆盖。该轮不触碰 Git added/staged/deleted、button payload、z-index、card/tool-card 跨组件布局等仍可能承担主题或相邻区域语义的 key。static root contract 320 -> 289,low external usage key 116 -> 86;普通 app raw、unresolved、fallback-only、non-contract 均保持 0,mobile color audit 保持通过。 | | installer and utility contract compression | retire simple-surface ramp and utility helper keys | `BitFun-Installer/src/theme/installerThemesData.ts`、`installerThemeRuntime.ts`、`variables.css`、`global.css`、installer language/theme pages、`tokens.scss`、`ThemeService.ts`、web utility CSS | 安装器只保留主题卡和安装流程实际可见的单一 accent seed,弱强调背景、focus 边框和 step shadow 改由局部 `color-mix()` 派生,不再导出完整 accent ramp;同时删除 motion/header、`border-medium`、`element-bg-strong` 等实现型 key。web root 侧删除 z-index、glass shadow、hover/focus opacity 等不表达主题语义的 utility helper;z-index 作为局部层级常量保留,glass shadow 读取 canonical `--shadow-*`,opacity 使用局部常量。generated widget iframe static shell 仍保留历史 helper 名称作为边界 fallback,不重新进入 app/root 主题扩展入口。static root contract 289 -> 274,low external usage key 86 -> 72,runtime contract 114 -> 108;installer color occurrences 76 -> 61,unique colors 62 -> 49,static root 29 -> 17,low external usage key 21 -> 10,runtime contract 18 -> 14。普通 app raw、unresolved、fallback-only、non-contract 和 dynamic family 错误保持 0。 | -| local surface helper compression | remove root defaults for local-only helpers | `tokens.scss`、`surface-stagger.scss`、gallery/agent/skill/miniapp/profile card styles、`UserMessage.*`、`ReviewTeamPage.*`、profile quick input styles | 删除不属于主题扩展入口的局部默认 key:卡片 stagger index 从 root contract 移到共享 Sass mixin,保留同一动态输入名但避免跨文件游离 key;FlowChat inline tag 删除未引用 CSS 和无效颜色透传,继续由组件库 `Tag` 的语义色驱动;profile inline padding 改回 Sass spacing;review team member 默认色复用 `UI_EXCEPTION_ACCENTS` 常量。`--scene-viewport-border-width` 经复审保留静态默认,因为它是 ThemeService layout runtime key 且影响首屏边框宽度。static root contract 274 -> 270,low external usage key 72 -> 69,token contract unique colors 81 -> 80;fallback、unresolved、non-contract 和 dynamic family 错误保持 0。 | +| local surface helper compression | remove root defaults for local-only helpers | `tokens.scss`、`surface-stagger.scss`、gallery/agent/skill/miniapp/profile card styles、`UserMessage.*`、strict review settings styles、profile quick input styles | 删除不属于主题扩展入口的局部默认 key:卡片 stagger index 从 root contract 移到共享 Sass mixin,保留同一动态输入名但避免跨文件游离 key;FlowChat inline tag 删除未引用 CSS 和无效颜色透传,继续由组件库 `Tag` 的语义色驱动;profile inline padding 改回 Sass spacing;strict review member 默认色复用 `UI_EXCEPTION_ACCENTS` 常量。`--scene-viewport-border-width` 经复审保留静态默认,因为它是 ThemeService layout runtime key 且影响首屏边框宽度。static root contract 274 -> 270,low external usage key 72 -> 69,token contract unique colors 81 -> 80;fallback、unresolved、non-contract 和 dynamic family 错误保持 0。 | | root/layout and widget projection compression | retire exact aliases and shrink widget payload | `tokens.scss`、`ThemeService.ts`、generated widget `themePayload.ts`、Card/SplashScreen/tool-card/config/Markdown/Tiptap/utility styles | 删除或本地化不承担主题语义的 exact alias:Splash 直接读 `--color-bg-primary`,Card elevated/subtle 复用 hover/transparent,scrollbar helper 改读 `--scrollbar-thumb`,glass utility 改读 canonical `--blur-*`,tool-card 固定布局 key 改为局部常量或既有 FlowChat token,Markdown/Tiptap table radius、pre radius/border style/font size、td foreground 和 code font 复用现有 contract;Config page 大间距改为本地布局值。generated widget payload 不再读取 shape/spacing/font size/font weight,iframe fallback/static shell 继续提供历史 radius/spacing 和本地布局默认,button component token 继续保留。该轮不触碰 Git added/staged/deleted、button projection、tooltip background、scene viewport border、accent/purple ramp 等仍承担状态或主题识别的 key。static root contract 270 -> 250,low external usage key 69 -> 51,generated widget payload 80 -> 57;普通 app raw、unresolved、fallback-only、non-contract 和 dynamic family 错误保持 0。 | | low-external contract compression | localize card surfaces and retire narrow ramp stops | `tokens.scss`、`ThemeService.ts`、theme types/presets、`ThemeService.test.ts`、Card/Tag/Tabs/WindowControls/utility/FlowChat/workspace/diff styles | `--card-bg-*` 不再作为 root/runtime 主题入口,Card 内部用私有 `--v-card-*` 保留明暗主题 default/hover/active/accent 层级,组件外消费改读既有 `--element-bg-*`、`--color-accent-*`、`--color-purple-*`;`windowControls.close.hoverColor` 旧入口和 `--window-control-close-hover-color` runtime-only key 删除,关闭按钮统一读 `--color-error`;app root/runtime 的 `--color-purple-50/400/800`、`--color-accent-800`、`--font-weight-bold` 及对应 TS theme schema/preset authoring 字段移除或改为局部 `color-mix()`、`--font-weight-semibold`,避免新主题继续维护不投影的扩展字段;旧 custom theme 中的这些退役字段会在归一化、持久化和导出时剥离。generated widget iframe compatibility/static shell 仍保留历史 `--color-accent-800` fallback,不重新进入 app root/runtime contract。static root contract 211 -> 199,low external usage key 26 -> 18,runtime-only required 1 -> 0,token unique 177 -> 171,web unique colors 302 -> 296;普通 app raw、unresolved、fallback-only、non-contract 和 dynamic family 错误保持 0。 | | background and utility contract retirement | retire low-external aliases and unused authoring stops | `tokens.scss`、`ThemeService.ts`、theme types/presets、generated widget `themePayload.ts` / compatibility aliases、mobile theme presets、Tooltip/SceneBar/Insights/utility/FlowChat styles | `colors.background.quaternary`、`colors.background.tooltip`、`colors.element.elevated` 不再作为主题 authoring/runtime/root contract,旧 custom theme 字段在归一化、持久化和导出时剥离;消费侧改读 `--color-bg-elevated`、`--color-bg-secondary` 或 `--element-bg-strong` 的局部派生,generated widget payload 停止读取 `--element-bg-elevated`,历史 iframe 内容通过 alias fallback 映射到 `--element-bg-strong`;app 静态 root 不再导出 `--flowchat-font-size-4xl`,utility 对 `--size-radius-xl` / `--blur-base` 的低价值读取改为局部值,runtime 可配置 `--size-radius-*` / `--blur-*` family 与 widget iframe shell fallback 仍由各自 owner 维护;Welcome/Nav/utility 样式使用局部派生或 Sass 私有 helper,mobile 删除未使用的 quaternary/tooltip/elevated root 默认。复审后保留 button payload 18 个交互 token、`--scene-viewport-border-width` 和 0.12/0.15 黑白 overlay stop,因为它们分别承担 iframe 主题按钮识别、首屏布局边界和相邻 surface/elevation 层级。static root contract 199 -> 193,low external usage key 18 -> 12,generated widget payload 57 -> 56,token unique 171 -> 168,theme preset 166/113 -> 159/112,token contract 82/74 -> 80/72;普通 app raw、unresolved、fallback-only、non-contract 和 dynamic family 错误保持 0。 | diff --git a/docs/development/ui-testids-CN.md b/docs/development/ui-testids-CN.md index d7a80ab07..50280ab10 100644 --- a/docs/development/ui-testids-CN.md +++ b/docs/development/ui-testids-CN.md @@ -356,17 +356,13 @@ | Agent 详情工具项 | `agent-detail-tool-item` | 重复的已启用工具项。包含 `data-tool-name`。 | | Agent 详情关闭按钮 | `agent-detail-close` | 详情弹窗关闭按钮。 | | Core 锚点按钮 | `agents-anchor-core` | 滚动到 core agents 区域。 | -| Teams 锚点按钮 | `agents-anchor-teams` | 滚动到 teams 区域。 | | Custom agents 锚点按钮 | `agents-anchor-custom` | 滚动到 custom agents 区域。 | | Agents 搜索按钮 | `agents-search-btn` | 搜索后缀按钮。 | | Core agents 区域 | `agents-core-zone` | Core agents section。 | -| Teams 区域 | `agents-teams-zone` | Agent teams section。 | | Custom agents 区域 | `agents-custom-zone` | Custom/subagent section。 | -| Review team 配置按钮 | `agents-review-team-configure-btn` | 打开 review team 配置。 | | Agent source 过滤器 | `agents-source-filter` | 重复项。配合 `data-agent-source` 使用。 | | Agent kind 过滤器 | `agents-kind-filter` | 重复项。配合 `data-agent-kind` 使用。 | | 创建 agent 按钮 | `agents-create-agent-btn` | 打开 custom agent 创建页。 | -| Agent team 卡片 | `agents-team-card` | 重复项。配合 `data-team-id` 使用。 | | BTW 停止 review 按钮 | `btw-session-panel-stop-review` | 从 BTW 面板停止 review session。 | | BTW origin 按钮 | `btw-session-panel-origin-button` | 从 BTW 面板打开 origin session。 | diff --git a/docs/development/ui-testids.md b/docs/development/ui-testids.md index 38697873a..d2baec4b2 100644 --- a/docs/development/ui-testids.md +++ b/docs/development/ui-testids.md @@ -356,17 +356,13 @@ Notes: | Agent detail tool item | `agent-detail-tool-item` | Repeated enabled tool item. Includes `data-tool-name`. | | Agent detail close | `agent-detail-close` | Modal close button. | | Core anchor button | `agents-anchor-core` | Scrolls to core agents zone. | -| Teams anchor button | `agents-anchor-teams` | Scrolls to teams zone. | | Custom agents anchor button | `agents-anchor-custom` | Scrolls to custom agents zone. | | Agents search button | `agents-search-btn` | Search suffix button. | | Core agents zone | `agents-core-zone` | Core agents section. | -| Teams zone | `agents-teams-zone` | Agent teams section. | | Custom agents zone | `agents-custom-zone` | Custom/subagent section. | -| Review team configure button | `agents-review-team-configure-btn` | Opens review team configuration. | | Agent source filter | `agents-source-filter` | Repeated item. Pair with `data-agent-source`. | | Agent kind filter | `agents-kind-filter` | Repeated item. Pair with `data-agent-kind`. | | Create agent button | `agents-create-agent-btn` | Opens custom agent creation page. | -| Agent team card | `agents-team-card` | Repeated item. Pair with `data-team-id`. | | BTW stop review button | `btw-session-panel-stop-review` | Stops review session from BTW panel. | | BTW origin button | `btw-session-panel-origin-button` | Opens origin session from BTW panel. | diff --git a/docs/sdlc-harness/README.md b/docs/sdlc-harness/README.md index 214fd18c6..22b6ced0f 100644 --- a/docs/sdlc-harness/README.md +++ b/docs/sdlc-harness/README.md @@ -9,8 +9,9 @@ |---|---|---| | [research/external-research.md](research/external-research.md) | 调研文档 | 外部产品、论文、标准和趋势信号 | | [product-requirements.md](product-requirements.md) | 产品需求 | 产品定位、用户画像、体验路径、产品规格、关键边界、平台差异和成功指标 | -| [product-requirements-agent-workflow-adjustment.md](product-requirements-agent-workflow-adjustment.md) | 非权威候选调整提案 | 智能体工作流、并发 GUI、审查降级、token 成本和任务完成度平衡;采纳前需回填权威文档 | +| [product-requirements-agent-workflow-adjustment.md](product-requirements-agent-workflow-adjustment.md) | 非权威候选调整提案 | 智能体工作流、并发 GUI、Review 范围控制、token 成本和任务完成度平衡;采纳前需回填权威文档 | | [agent-workflow-staged-plan.md](agent-workflow-staged-plan.md) | 场景收敛计划 | 将工作流、审查、并发和成本控制压回真实用户场景,不新增独立阶段路线 | +| [review-verify-implementation-guardrails.md](review-verify-implementation-guardrails.md) | Review / Verify 实施护栏 | 约束统一 Review、Strict Review、ReviewTeam 内部化、PR Review 投影和 Verify 探索的两次大型 PR 边界 | | [design.md](design.md) | 架构设计 | 设计目标、领域模型、配置层级、模块边界和架构风险 | | [implementation-plan.md](implementation-plan.md) | 实施计划 | 按用户收益切片组织快速路径、上下文保障、团队治理、复杂生命周期能力的阶段落地 | | [traceability-matrix.md](traceability-matrix.md) | 追踪矩阵 | 需求、设计、功能规格、执行阶段和测试方法的映射 | diff --git a/docs/sdlc-harness/agent-workflow-staged-plan.md b/docs/sdlc-harness/agent-workflow-staged-plan.md index 636e64443..9301561e6 100644 --- a/docs/sdlc-harness/agent-workflow-staged-plan.md +++ b/docs/sdlc-harness/agent-workflow-staged-plan.md @@ -11,7 +11,7 @@ BitFun 不需要把 dynamic workflow 做成一个新的主产品模式。用户 - 重要改动能被合适强度地看一眼,而不是默认跑重审查。 - 长任务不黑盒,token、耗时、阻塞和下一步清楚。 - 批量任务可以并发,但 GUI 只呈现一个任务、一组状态和少量决策点。 -- 成本增加前必须说明为什么值得,且用户可以降级、暂停或只收敛已完成部分。 +- 成本增加前必须说明为什么值得,且用户可以调整范围、暂停或只收敛已完成部分。 因此,workflow 只作为少数场景的后台执行策略。默认产品体验仍是“任务完成 -> 摘要 -> 已验证/未验证 -> 可选下一步”。 @@ -61,7 +61,7 @@ BitFun 不需要把 dynamic workflow 做成一个新的主产品模式。用户 | 升级 | 命中安全、性能、架构、跨模块、关键 UI 流程或验证缺口时建议 L2 | | L3 条件 | 用户明确要求严格审查、团队强策略、大型 PR 或核心接口变更 | | GUI | 一个 Review 面板,按问题优先级合并输出 | -| 成本 | L2/L3 前显示预计 token/耗时和收益,允许降级 | +| 成本 | L2/L3 前显示预计 token/耗时和收益,允许调整范围 | | 完成标准 | 必须修复、建议确认、已覆盖、未覆盖、下一步清楚 | | 禁止 | 把 PR 审查压进 P0 默认体验,或把 DeepReview 作为普通 review 默认入口 | @@ -111,13 +111,13 @@ BitFun 不需要把 dynamic workflow 做成一个新的主产品模式。用户 | L0 | 快速检查 | 低风险任务、已有验证、小 diff | 默认最低成本 | | L1 | 独立快速审查 | 提交前、PR 前、小到中等风险 | 允许自动建议 | | L2 | 定向审查 | 安全、性能、架构、跨模块、关键 UI 或验证缺口 | 需说明原因和预计成本 | -| L3 | 严格审查 | 大型 PR、核心接口、安全敏感、团队强策略、用户明确要求 | 必须预算确认,可降级 | +| L3 | 严格审查 | 大型 PR、核心接口、安全敏感、团队强策略、用户明确要求 | 必须预算确认,可调整范围 | 原则: - L0/L1 服务多数任务,L2/L3 只服务风险或明确意图。 - reviewer 默认只读;修复必须进入用户批准的执行阶段。 -- 两轮审查没有新增有效问题时,应建议降级或停止。 +- 两轮审查没有新增有效问题时,应建议停止或保留核心检查。 - 缺少上下文或 oracle 时,先提问或诊断,不启动 L3。 ## 6. 成本和解决率平衡 @@ -129,7 +129,7 @@ BitFun 不需要把 dynamic workflow 做成一个新的主产品模式。用户 | 多失败任务 | 优先失败聚类和可运行 oracle,再决定是否队列化 | | 大规模任务 | 优先样本成功率和可收敛性,再考虑并发 | | 预算不足 | 优先高风险/高价值 item,跳过低风险二次审查 | -| 解决率不足 | 先问是否追加预算或降级目标,不静默扩大 token | +| 解决率不足 | 先问是否追加预算或调整目标范围,不静默扩大 token | 用户侧必须能看到: @@ -146,7 +146,7 @@ BitFun 不需要把 dynamic workflow 做成一个新的主产品模式。用户 | 长任务 | 后台任务条:状态、阶段、预算、需决策 | | 批量任务 | 单一任务控制台:执行域、沙箱状态、阶段、数量、异常、预算、可下钻详情 | | 审查 | Review 面板:问题优先、覆盖范围、未覆盖风险、操作 | -| 成本确认 | 说明收益、成本、降级选项和停止选项 | +| 成本确认 | 说明收益、成本、范围控制选项和停止选项 | GUI 不展示多个 agent 窗口,不默认展示完整日志,不把内部术语作为主文案。 diff --git a/docs/sdlc-harness/architecture/agent-workflow-design.md b/docs/sdlc-harness/architecture/agent-workflow-design.md index f60ebcaab..7d968f9d5 100644 --- a/docs/sdlc-harness/architecture/agent-workflow-design.md +++ b/docs/sdlc-harness/architecture/agent-workflow-design.md @@ -24,7 +24,7 @@ |---|---|---| | 任务生命周期、取消、恢复、事件顺序 | Agent Kernel | 只消费状态,不新增并行生命周期 | | 工具执行、验证命令、subagent 调用 | Execution | 只返回工具/验证结果,不写产品结论 | -| DeepReview reviewer 队列、只读 reviewer、report enrichment | 现有 DeepReview 架构 | 仅 L3 严格审查复用,不作为通用 workflow/task queue | +| DeepReview 内部 capacity queue、只读 reviewer、report enrichment | 现有 DeepReview 架构 | 仅 L3 严格审查复用,不作为通用 workflow/task queue | | 权限、执行域、沙箱、凭据和网络 | Security Boundary | 成本或审查确认不能绕过安全确认 | | 证据、指标和回放 | Quality Data Plane | P0/P1 不新增默认事件;先用既有最小事件 | | GUI 展示和用户决策 | Product Surface | 展示状态、预算、风险和下一步,不拥有权威事实 | @@ -46,8 +46,8 @@ Review 是用户唯一需要理解的审查入口。内部可复用 L0-L3,但 入口兼容约束: -- `/DeepReview` 只作为 “Review: strict” 的高级别名、调试入口或过渡兼容入口。 -- child session、auxiliary pane、work packets 和 reviewer queue 默认后台化;普通用户只看到统一 Review 面板。 +- `/DeepReview` 只作为迁移窗口内的历史兼容输入,等价路由到 “Review: Strict”,不作为高级别名、调试入口或长期产品入口。 +- child session、auxiliary pane、work packets 和内部 capacity queue 默认后台化;普通用户只看到统一 Review 面板。 - 如果辅助 pane 因排障需要暴露,必须折叠到高级详情,并同步更新 DeepReview 架构文档,避免形成第二套产品入口。 | 强度 | 用户看到 | 默认限制 | @@ -55,7 +55,7 @@ Review 是用户唯一需要理解的审查入口。内部可复用 L0-L3,但 | L0 | 快速检查摘要 | 不创建 reviewer | | L1 | 独立快速审查结果 | 一个只读 reviewer | | L2 | 定向审查合并结果 | 只选命中风险的 1-3 个方向 | -| L3 | 严格审查结果和覆盖说明 | 预算确认后才启动,且可降级 | +| L3 | 严格审查结果和覆盖说明 | 预算确认后才启动,且可调整范围 | Review 面板只按问题呈现: @@ -73,10 +73,10 @@ Review 面板只按问题呈现: | 触发 | UI 行为 | |---|---| -| 预计 token 或耗时明显增加 | 展示收益、成本和降级选项 | +| 预计 token 或耗时明显增加 | 展示收益、成本和范围控制选项 | | 需要多个 reviewer | 说明新增覆盖范围和预计成本 | | 需要并发 worker | 说明节省墙钟时间和冲突风险 | -| 预算接近上限 | 暂停扩大执行,给出追加预算、降级、只收敛已完成 | +| 预算接近上限 | 暂停扩大执行,给出追加预算、保留核心检查、只收敛已完成 | | oracle 不可靠 | 停止扩大,改为人工确认或小样本建议 | 小任务不显示成本面板。结束摘要只需要说明已验证、未验证和残余风险。 @@ -84,7 +84,7 @@ Review 面板只按问题呈现: 成本确认最低契约: - 只有从轻路径升级为 L2/L3 review、并发 worker、批量队列或长任务控制台时才弹出预算确认。 -- 预算确认必须同时展示估算 token/时间区间、可换来的覆盖或墙钟收益、降级选项和停止条件。 +- 预算确认必须同时展示估算 token/时间区间、可换来的覆盖或墙钟收益、范围控制选项和停止条件。 - 预算确认不能替代安全确认;执行位置、沙箱等级、写入范围、网络/凭据状态仍由安全边界提供。 ## 6. 批量任务 GUI @@ -105,7 +105,7 @@ Review 面板只按问题呈现: - 内部 lease、artifact store、work packet 等实现术语。 - 模型推理过程。 -## 7. 升级和降级规则 +## 7. 升级和范围收敛规则 | 情况 | 默认动作 | |---|---| @@ -113,14 +113,14 @@ Review 面板只按问题呈现: | 用户准备 PR | 建议 L1,风险命中再 L2 | | 验证失败且可分解 | 先聚类,再决定是否队列化 | | 多个 item 共享同一 contract | 串行或请求用户决策 | -| 两轮审查无新增有效问题 | 建议停止或降级 | +| 两轮审查无新增有效问题 | 建议停止或保留核心检查 | | token 接近预算 | 暂停扩大并发或跳过低价值审查 | | 缺少 oracle | 不扩大 workflow,输出人工验收建议 | ## 8. 实现顺序约束 1. 先保证普通任务摘要和未验证项稳定。 -2. 再把 DeepReview 投影为 Review 的严格档,并把 `/DeepReview` 处理为高级别名或调试入口。 +2. 再把 DeepReview 投影为 Review 的严格档,并把 `/DeepReview` 处理为迁移窗口内的历史兼容输入。 3. 再给长任务补轻量后台任务条和预算提示。 4. 只对 CI/测试失败和 PR comments 这类明确场景做队列化。 5. 大规模迁移/审计必须等样本 gate、预算确认和冲突处理稳定后再做。 @@ -130,7 +130,7 @@ Review 面板只按问题呈现: | 验证 | 目的 | |---|---| | 低风险任务不显示 workflow UI | 防止默认体验变重 | -| 本地显式 L1 审查能降级或跳过 | 防止过度审查 | +| 本地显式 L1 审查能调整范围或跳过 | 防止过度审查 | | PR/团队审查不进入 P0 默认路径 | 保持既有阶段边界 | | L2/L3 前有成本说明 | 防止 token 无感知上涨 | | CI 多失败能只收敛已完成部分 | 提升解决率而不是追求全量完成 | diff --git a/docs/sdlc-harness/architecture/evidence-pack.md b/docs/sdlc-harness/architecture/evidence-pack.md index f5f48a1aa..fc9f871fe 100644 --- a/docs/sdlc-harness/architecture/evidence-pack.md +++ b/docs/sdlc-harness/architecture/evidence-pack.md @@ -43,7 +43,7 @@ | 验证证据 | `verification.completed`、CI 检查、命令摘要、制品引用 | | 风险策略提示 | 风险标签、推荐/强制检查、审查强度 | | 安全决策 | allow/ask/deny/应急放行、授权范围、残余风险 | -| 审查证据 | 深度审查问题、人工审查、过期标记 | +| 审查证据 | 严格审查问题、人工审查、过期标记 | | 主动配置证据 | hook、plugin、自定义工具、MCP、智能体规则的发现、hash、权限和信任状态 | | 人工决策 | 覆盖、风险接受、确认、拒绝 | diff --git a/docs/sdlc-harness/architecture/quality-data-plane.md b/docs/sdlc-harness/architecture/quality-data-plane.md index 796e961a1..eb2bd310a 100644 --- a/docs/sdlc-harness/architecture/quality-data-plane.md +++ b/docs/sdlc-harness/architecture/quality-data-plane.md @@ -65,7 +65,7 @@ P0 事件集优先支撑三件事: | 工具事件 | 命令、工具调用、审批、退出码、耗时 | | 文件事件 | diff、重命名、删除、生成文件、文件监听 | | 验证事件 | 命令、退出码、耗时、日志摘要、制品引用 | -| 审查/门禁事件 | 深度审查、问题、就绪度、门禁投影 | +| 审查/门禁事件 | 严格审查、问题、就绪度、门禁投影 | | 性能/成本事件 | 首次有用动作耗时、后台分析耗时、token、模型、墙钟耗时、工具耗时 | | 主动配置事件 | hook、plugin、自定义工具信任状态、hash、权限声明、启用范围 | | 阶段收益事件 | 阶段用户收益、技术前置、延期边界、质量一致性抽样和验收结果 | @@ -215,6 +215,6 @@ P1/P2 再引入: - 普通任务可以生成可解释信心摘要。 - 安全提示、拒绝、应急放行都能追溯到事件和范围。 - 证据包、变更就绪度和门禁复用同一事实层。 -- 深度审查 token、耗时、范围、跳过项上下文可被统一记录。 +- 严格审查 token、耗时、范围、跳过项上下文可被统一记录。 - 事件模型能够导出到至少一种外部标准或平台。 - hook/plugin/custom 工具的信任状态可通过事件追溯到来源、hash、权限和审核人。 diff --git a/docs/sdlc-harness/architecture/security-boundary.md b/docs/sdlc-harness/architecture/security-boundary.md index c7285cad8..38f8e7a43 100644 --- a/docs/sdlc-harness/architecture/security-boundary.md +++ b/docs/sdlc-harness/architecture/security-boundary.md @@ -183,7 +183,7 @@ interface SecurityBoundaryDecision { | 文档注入要求泄露 token | 安全边界 | | 新增 MCP server | 安全边界 + 项目画像 | | 迁移脚本影响生产数据 | 安全边界 + 守护策略 | -| 用户跳过深度审查 | 配置化策略 | +| 用户跳过严格审查 | 配置化策略 | | 用户允许联网下载依赖 | 安全边界应急放行 | ## 8. 边界场景 diff --git a/docs/sdlc-harness/features/agent-evaluation.md b/docs/sdlc-harness/features/agent-evaluation.md index b545d2091..ab6f91106 100644 --- a/docs/sdlc-harness/features/agent-evaluation.md +++ b/docs/sdlc-harness/features/agent-evaluation.md @@ -40,7 +40,7 @@ 范围: -- 评测 Plan、Debug、Review、深度审查、Agentic、Mini App、remote、MCP 等模式。 +- 评测 Plan、Debug、Review、严格审查、Agentic、Mini App、remote、MCP 等模式。 - 评测工具接口、上下文策略、hook 策略、审批/安全策略。 - 评测配置化策略是否误升级、漏提示或过度打断。 - 评测安全边界是否降低风险且不制造无意义确认。 @@ -127,7 +127,7 @@ interface EvalResult { | 模式合规 | 是否遵守只读、审批、审查约束 | | 工具接口 | 工具调用是否正确、输出是否被使用 | | 真实 issue 修复 | 测试通过、diff 符合预期、无回归 | -| 深度审查 | 种子缺陷覆盖率、问题精度、判定一致性 | +| 严格审查 | 种子缺陷覆盖率、问题精度、判定一致性 | | 变更就绪度 / PR 门禁 | 强制检查精度、错误就绪/错误阻断、降级处理 | | Hook 策略 | 权限、脱敏、超时、阻断语义 | @@ -148,7 +148,7 @@ interface EvalResult { |---|---| | P0 | 快速路径、安全边界、配置化策略的 5-10 个小型回放任务和确定性判定 | | P1 | 轨迹回放、失败分类、成本和打断指标对比;不要求完整保留集治理 | -| P2 | 变更就绪度、风险分类器、深度审查、团队策略评测和评测卡治理 | +| P2 | 变更就绪度、风险分类器、严格审查、团队策略评测和评测卡治理 | | P3 | 交付物图谱、需求影响、发布/事故回放和私有保留集 | | P4 | 模型/策略 A/B、线上反馈回流、回归看板和长期趋势 | diff --git a/docs/sdlc-harness/features/configurable-policy-profile.md b/docs/sdlc-harness/features/configurable-policy-profile.md index 787b7df07..6bce98e14 100644 --- a/docs/sdlc-harness/features/configurable-policy-profile.md +++ b/docs/sdlc-harness/features/configurable-policy-profile.md @@ -105,7 +105,7 @@ interface ConfigurablePolicyDecision { | 用户明确要求质量保障要求较低的快速试验 | 保持低干预路径;只在 shell/网络/凭据/删除时打断 | | 单体多项目仓库子目录规则冲突 | 使用最近可信规则;冲突进入提示,不直接升级为阻塞 | | 远程开发路径或权限不明 | 展示执行位置、工作区根和不可支持能力;不自动扩大权限 | -| 生成文件/lockfile/二进制大 diff | 默认过滤深度审查;只展示范围和风险摘要 | +| 生成文件/lockfile/二进制大 diff | 默认过滤严格审查;只展示范围和风险摘要 | | 不稳定 CI 或私有依赖不可运行 | 输出不可验证原因、替代建议和信心影响 | | 热修复 | 允许跳过部分质量建议;发布/回滚/负责人提示前置 | | 用户强制跳过建议 | 若不触及安全底线,可继续;记录范围和残余风险 | diff --git a/docs/sdlc-harness/features/pr-quality-gate.md b/docs/sdlc-harness/features/pr-quality-gate.md index e50557349..6ecb783db 100644 --- a/docs/sdlc-harness/features/pr-quality-gate.md +++ b/docs/sdlc-harness/features/pr-quality-gate.md @@ -7,6 +7,8 @@ 变更就绪度是产品体验层,PR 门禁是其中的可选强治理投影。快速路径输出简洁的改动、已验证项、未验证项和下一步建议;当用户准备 PR、进入受管目录、开启团队策略或触发内部强策略时,再投影为更严格的就绪度或门禁结果。 +Review 与 Verify 的执行策略不归本模块拥有。统一 Review、Strict Review、ReviewTeam 内部化和 Verify 探索的实施边界见 [Review / Verify 收敛实施护栏](../review-verify-implementation-guardrails.md)。本模块只消费审查结果、验证证据、风险提示和人工决策,并把它们投影为本地摘要、PR 文本、检查状态或团队策略结果。 + 在严格场景中,本模块把后台证据包、风险策略提示(Risk Policy Hint)、验证证据和风险接受记录组合成可审查结果。这个结果可以投影到本地报告、PR 描述、GitHub 检查状态或团队强制策略;CI、分支保护、安全扫描、CODEOWNERS 和人类审查人继续作为外部权威信号。 关键边界: @@ -33,7 +35,7 @@ - 强制(`required`)和阻断(`blocking`)只在团队/组织配置、确定性失败、安全拒绝或用户明确选择强策略时启用;风险等级本身只能建议升级,不能单独阻塞。 - 降级(`degraded`)是合法状态,优于错误的就绪/通过结论。 - 人工覆盖必须记录原因、操作者、范围、过期时间和残余风险。 -- 低风险任务不默认触发完整深度审查。 +- 低风险任务不默认触发完整严格审查。 - 未信任 hook、plugin、自定义工具或 MCP 只能产生未关闭风险或降级状态,不提供通过证据。 ## 3. 输入、输出与数据模型 @@ -149,7 +151,7 @@ PR 文本投影示例: | `required` | PR 检查可映射为 `neutral` / `action_required`,不写成 `success` | 若团队策略允许,保持 `neutral` / `action_required` 并记录接受;若缺口属于必需证据,保持 `fail` | | `blocking` | 安全拒绝、组织策略、确定性失败、凭据/发布等高敏缺口 fail-closed | 本地风险接受不能改成 `pass`;只能补齐证据、由组织策略放行,或保留 `fail` | -深度审查预算策略: +严格审查预算策略: | 风险画像 | 默认策略 | |---|---| @@ -191,4 +193,4 @@ PR 文本投影示例: - 准备 PR 时能给出清晰、短、可行动的变更就绪度摘要。 - 团队项目能把就绪度投影为强制要求/阻断策略。 - 高风险变更能解释强制检查、审查触发和残余风险。 -- 安全授权、质量风险接受和深度审查成本分别可追踪。 +- 安全授权、质量风险接受和严格审查成本分别可追踪。 diff --git a/docs/sdlc-harness/features/risk-classifier.md b/docs/sdlc-harness/features/risk-classifier.md index 46bd30562..d1aeb7071 100644 --- a/docs/sdlc-harness/features/risk-classifier.md +++ b/docs/sdlc-harness/features/risk-classifier.md @@ -119,7 +119,7 @@ interface RiskPolicyHint { | 风险等级 | 默认策略 | |---|---| -| low | 保持低干预策略画像,只给推荐检查,不触发深度审查 | +| low | 保持低干预策略画像,只给推荐检查,不触发严格审查 | | medium | 建议相关验证;证据弱时建议定向审查或证据引用 | | high | 列出按策略强制的检查、负责人/审查人、审查强度和覆盖条件 | | unknown | 不降级为 low;保持摘要或建议投影,必要时进入降级状态并等待确认 | @@ -152,7 +152,7 @@ interface RiskPolicyHint { | 普通任务被误升级 | 误升级率必须进入 P0/P1 指标 | | 主动配置被当成纯文档变更 | hook、plugin、自定义工具、MCP、智能体规则变化必须触发 active_config_sensitive | | 强制检查过多 | 每个强制检查必须有触发原因和取消条件 | -| 深度审查成本被放大 | 只有高风险或证据薄弱的中风险才默认触发定向/完整审查 | +| 严格审查成本被放大 | 只有高风险或证据薄弱的中风险才默认触发定向/完整审查 | | 规则长期失准 | 合入后缺陷、审查阻塞项、覆盖和跳过原因必须回流校准 | ## 8. 成功标准 diff --git a/docs/sdlc-harness/governance/metrics-spec.md b/docs/sdlc-harness/governance/metrics-spec.md index 410cbd03a..fcf5b6374 100644 --- a/docs/sdlc-harness/governance/metrics-spec.md +++ b/docs/sdlc-harness/governance/metrics-spec.md @@ -66,7 +66,7 @@ P0 最小采集口径: | 强制检查漏报率 | 后验发现应运行但未推荐的检查 / 后验确认需要的检查总数 | 风险分类器 | CI 失败、审查阻塞项、合入后缺陷 | 每两周 | P2+ 降低错误就绪风险 | | PR 就绪度采用率 | 使用就绪度摘要的 PR 数 / BitFun 准备的 PR 总数 | 变更就绪度 | `readiness.generated`、PR 视图 | 每周 | P2+ 衡量 PR/CI 适配接入后的 PR 体验价值 | | 门禁降级率 | `degraded` 门禁数 / 门禁投影总数 | 变更就绪度 | `gate.projected` | 每周 | 发现模式、证据、工具或信任模型缺口 | -| 深度审查价值率 | 产生有效问题或避免缺陷的深度审查数 / 深度审查总数 | 审查系统 | 审查事件、反馈 | 每两周 | 防止高成本审查泛化 | +| 严格审查价值率 | 产生有效问题或避免缺陷的严格审查数 / 严格审查总数 | 审查系统 | 审查事件、反馈 | 每两周 | 防止高成本审查泛化 | | 风险接受审计覆盖率 | 有操作者/原因/范围/残余风险的风险接受数 / 风险接受总数 | 变更就绪度 | 风险接受事件 | 每周 | 确保人工放行可追踪 | ## 4. P3/P4 复杂生命周期与评测指标 diff --git a/docs/sdlc-harness/governance/self-governance-notes.md b/docs/sdlc-harness/governance/self-governance-notes.md index 8f76269be..4042508fd 100644 --- a/docs/sdlc-harness/governance/self-governance-notes.md +++ b/docs/sdlc-harness/governance/self-governance-notes.md @@ -28,7 +28,7 @@ BitFun 仓库可以作为可配置工程能力的内部验证项目。产品需 - 项目画像示例可以读取 BitFun 的 AGENTS.md、CONTRIBUTING、package/cargo scripts、CI 和模块边界。 - 变更就绪度示例可以使用 BitFun 的 fork PR 流程,但必须标注为项目特定。 -- 风险分类器示例可以覆盖 Rust 工作区、React frontend、Tauri desktop、remote、AI 适配器、深度审查等高风险区域。 +- 风险分类器示例可以覆盖 Rust 工作区、React frontend、Tauri desktop、remote、AI 适配器、严格审查等高风险区域。 - 安全边界示例应覆盖 shell、网络、凭据、MCP、hook、跨目录写、删除、发布 credential 等安全面。 - 证据包示例应表达为“任务和变更证据”,不默认要求所有项目生成完整证据包。 - 评测示例需要同时覆盖快速路径、误升级、安全提示、PR 就绪度和高风险审查。 diff --git a/docs/sdlc-harness/implementation-plan.md b/docs/sdlc-harness/implementation-plan.md index 9b4410f36..a6bb7c364 100644 --- a/docs/sdlc-harness/implementation-plan.md +++ b/docs/sdlc-harness/implementation-plan.md @@ -84,7 +84,7 @@ | 风险类型 | 触发方式 | 可能后果 | 预防和回退 | |---|---|---|---| -| 默认体验变重 | P0/P1 把证据包、图谱、深度审查或团队规则提前显露 | 低风险任务完成变慢,用户认为产品只适合强流程项目 | 默认只展示短摘要和必要安全确认;复杂能力按 PR、团队、发布、事故和合规触发;打断率和首次有用动作耗时超基线时降级 | +| 默认体验变重 | P0/P1 把证据包、图谱、严格审查或团队规则提前显露 | 低风险任务完成变慢,用户认为产品只适合强流程项目 | 默认只展示短摘要和必要安全确认;复杂能力按 PR、团队、发布、事故和合规触发;打断率和首次有用动作耗时超基线时降级 | | 质量要求断层 | 需求或 PR 阶段要求很高,但开发过程中缺少风险提示、验证建议或未验证摘要 | 用户在末端突然被阻断,返工集中在 PR 或发布前 | `quality.consistency.sampled` 抽样检查需求、开发、验证、审查、发布强度;受管路径从开发中就显示规则来源和验证缺口 | | 风险误升级 | 风险分类把文档、小范围非核心改动或个人任务升级到团队治理 | 提示噪音、误阻断、用户绕过安全确认 | 风险等级只驱动建议和展示层级;阻断只能来自安全拒绝、组织策略、确定性失败或明确强制策略 | | 风险漏报 | 小 diff 命中安全、权限、schema、远程、发布或主动配置 | 高风险变更通过轻量路径,后续缺陷或安全事件返工 | 路径、动作、主动配置和历史风险优先于 diff 行数;高风险低置信进入降级状态和推荐检查 | @@ -132,7 +132,7 @@ P-1 是内部跑道,不应作为用户可见“新能力”发布。它的价 | 安全动作有低噪音确认 | 工作区写、shell、网络、凭据、删除、跨根目录写入基础决策,记录执行位置、沙箱等级组合和降级原因 | 不声称具备未实现的运行时沙箱 | | 远程或云端执行位置可见 | 识别本地、SSH、容器、远程工作区、云端异步任务和不可用本地能力 | 不把本地能力呈现为远程能力 | | 主动配置默认安全 | 发现 hook、plugin、MCP、自定义工具、工具复写和智能体规则,默认未信任且不执行 | 不执行未信任主动配置;不把产品架构 P0 的插件来源、诊断和最小候选效果消费路径扩大成 full runtime | -| 低风险任务也有一致质量说明 | 低风险任务摘要必须包含已验证、未验证、跳过原因和建议补证路径 | 不要求深度审查、强制检查或完整需求分析 | +| 低风险任务也有一致质量说明 | 低风险任务摘要必须包含已验证、未验证、跳过原因和建议补证路径 | 不要求严格审查、强制检查或完整需求分析 | | 阶段数据可采样 | 记录任务、工具、验证、安全决策、用户覆盖和摘要生成等最小事件;阶段收益和质量一致性抽样进入 P1+ | 不用指标阻塞 P0 用户任务 | ### 6.3 验收成果 @@ -167,7 +167,7 @@ P-1 是内部跑道,不应作为用户可见“新能力”发布。它的价 | 用户可见收益 | 必要实现 | 明确不做或延期 | |---|---|---| -| 中风险动作有行内弱提示 | 路径、模块、操作、历史信号和安全敏感标签进入风险提示 v0 | 不默认弹窗,不默认深度审查 | +| 中风险动作有行内弱提示 | 路径、模块、操作、历史信号和安全敏感标签进入风险提示 v0 | 不默认弹窗,不默认严格审查 | | 推荐检查可执行或可解释 | 推荐验证命令、替代验证、不可运行原因和跳过后果 | 不把推荐检查等同阻断 | | 远程不可支持状态可恢复 | 私有依赖、本地二进制、端口、凭据或权限不可用时给替代路径 | 不假装本地检查覆盖远程环境 | | 沙箱降级可见 | 本地/远程/ACP/MCP/插件/浏览器/云端执行面的状态块、降级说明和替代路径 | 不把权限确认包装成沙箱 | @@ -176,7 +176,7 @@ P-1 是内部跑道,不应作为用户可见“新能力”发布。它的价 ### 7.3 验收成果 -- 低风险改动不触发深度审查。 +- 低风险改动不触发严格审查。 - 高风险或中风险改动能解释升级原因、建议检查和跳过后果。 - 同类提示不会在同一任务中反复弹窗。 - CI、私有环境或远程能力不可运行时输出替代建议、信心影响和后续补证路径。 @@ -289,7 +289,7 @@ P-1 是内部跑道,不应作为用户可见“新能力”发布。它的价 | 提示与确认 | 同一任务、同一原因、同一范围只出现一次强提示;低价值提示合并到摘要 | 合并、延后、折叠或进入任务结束摘要 | | 证据和图谱 | 只在 PR、团队、发布、事故或合规场景计算完整视图 | 默认只保留摘要和引用,过期重算后台执行 | | QDP 事件 | 新事件字段必须服务用户收益、风险解释、质量一致性或性能看护 | 删除字段、降采样、缩短保留周期或只保存脱敏引用 | -| 模型和工具成本 | 深度审查、影响分析、评测回放、插件适配评估默认不进入低风险路径 | 改为用户触发、团队配置触发或离线任务 | +| 模型和工具成本 | 严格审查、影响分析、评测回放、插件适配评估默认不进入低风险路径 | 改为用户触发、团队配置触发或离线任务 | ## 12. 看护指标 diff --git a/docs/sdlc-harness/product-requirements-agent-workflow-adjustment.md b/docs/sdlc-harness/product-requirements-agent-workflow-adjustment.md index 58e41941f..29560eb97 100644 --- a/docs/sdlc-harness/product-requirements-agent-workflow-adjustment.md +++ b/docs/sdlc-harness/product-requirements-agent-workflow-adjustment.md @@ -2,7 +2,7 @@ > 范围:基于 Bun Rust 迁移文章、Claude Code dynamic workflows、GitHub Copilot cloud agent、Cursor Background Agent / Bugbot、Google Jules 和近期智能体 PR 研究,调整 BitFun 在长任务、并发任务、审查、token 成本和 GUI 交互上的产品需求。 > -> 本文是产品需求层调整,不是技术设计。它回答用户为什么需要这类能力、什么时候应该自动启用、什么时候应该降级、GUI 应如何表达并发,以及 BitFun 如何避免把工作流、证据和审查做成过重流程。 +> 本文是产品需求层调整,不是技术设计。它回答用户为什么需要这类能力、什么时候应该自动启用、什么时候应该收敛范围或停止、GUI 应如何表达并发,以及 BitFun 如何避免把工作流、证据和审查做成过重流程。 承接文档: @@ -19,7 +19,7 @@ BitFun 后续不应把 dynamic workflow 理解成一个需要用户学习的新 2. **把并发能力做成 GUI 中的单一任务控制台**:用户看到的是一个任务、一个进度、一组阶段和异常,而不是 64 个窗口、64 个聊天或 64 条不可理解的日志。 3. **把审查和工作流从概念上后台化**:用户不需要理解 subagent、workflow、evidence pack、artifact graph。默认只看到任务状态、风险原因、成本预算、已验证/未验证项和下一步。 4. **把完成率、token、耗时做成产品级预算选择**:每次升级审查或并发执行,都要说明预估收益和成本;自动化可以启用,但不能让 token 在用户无感知时暴涨。 -5. **把 DeepReview 降级为统一 Review 的最高档策略**:普通 review、快速独立 review、定向多角色 review、深度 review 应合并成一个自适应审查体验。 +5. **把 DeepReview 收敛为统一 Review 的最高档策略**:普通 review、快速独立 review、定向多角色 review、严格审查应合并成一个自适应审查体验。 6. **把复杂治理能力收回到用户价值之后**:证据包、图谱、门禁、风险接受是后台支撑,只有在 PR、团队规则、发布、事故、合规或大规模迁移时显性化。 ## 2. 业界参照与启发 @@ -64,7 +64,7 @@ BitFun 后续不应把 dynamic workflow 理解成一个需要用户学习的新 用户可以手动选择“更快”或“更稳”,但不需要预先知道应该选 DeepReview、workflow 还是 subagent。 -### 4.2 DeepReview 降级为 Review 的最高档 +### 4.2 DeepReview 收敛为 Review 的最高档 建议统一成一个 `Review` 体验,内部有四档: @@ -73,15 +73,15 @@ BitFun 后续不应把 dynamic workflow 理解成一个需要用户学习的新 | L0 | 快速检查 | 小改动、低风险、已有验证或用户追求速度 | 主 agent 自检、已验证/未验证摘要 | | L1 | 独立快速审查 | 小到中等 diff、局部 bugfix、准备提交前 | 一个独立 reviewer 的问题清单和可信度 | | L2 | 定向审查 | 触碰权限、性能、架构、前端体验、跨模块或缺验证 | 2 到 3 个定向 reviewer 的合并结论 | -| L3 | 深度审查 | 大迁移、核心接口、安全敏感、用户显式要求、团队强策略 | 分片审查、队列状态、局部覆盖、修复建议和证据引用 | +| L3 | 严格审查 | 大迁移、核心接口、安全敏感、用户显式要求、团队强策略 | 分片审查、队列状态、局部覆盖、修复建议和证据引用 | 现有 DeepReview 的队列、work packets、judge、partial coverage 和 action surface 仍有价值,但不应作为普通审查入口的默认形态。普通用户只应看到“审查强度已自动选择”。 迁移/兼容规则: - 用户侧唯一主入口是 `Review`。 -- `/DeepReview` 只能作为高级别名、调试入口或过渡兼容入口;默认导航、按钮和普通命令不应并列展示 `Review` 与 `DeepReview`。 -- child session、auxiliary pane、work packets 和 reviewer queue 应后台化为 L3 严格档实现细节;如果仍需要用户可见的辅助 pane,必须同步更新 DeepReview 架构文档并说明它不是第二个产品入口。 +- `/DeepReview` 只能作为迁移窗口内的历史兼容输入,等价路由到 `Review: Strict`;默认导航、按钮和普通命令不应并列展示 `Review` 与 `DeepReview`。 +- child session、auxiliary pane、work packets 和内部 capacity queue 应后台化为 L3 严格档实现细节;如果仍需要用户可见的辅助 pane,必须同步更新 DeepReview 架构文档并说明它不是第二个产品入口。 - 普通 review 输出必须合并到同一个 Review 面板,不能再把 DeepReview report 作为另一个窗口或另一个审查结果呈现。 ## 5. TUI 与 GUI 的并发心智差异 @@ -96,7 +96,7 @@ TUI 用户能接受多个 terminal、多个进程、多个日志,因为核心 | 冲突处理 | 命令行提示或手动 resolve | 明确显示冲突文件、占用 agent、推荐动作 | | 成本 | 用户看 provider usage 或终端统计 | 任务头常驻 token/time/并发预算 | | 审查结果 | 多个 reviewer 输出拼接 | 合并后的结论、分歧、未覆盖范围和可应用修复 | -| 手动控制 | kill process、改脚本、重跑 | 暂停、降级、停止、跳过低优先项、追加预算 | +| 手动控制 | kill process、改脚本、重跑 | 暂停、调整范围、停止、保留核心检查、追加预算 | ### 5.1 GUI 并发控制台 @@ -117,7 +117,7 @@ TUI 用户能接受多个 terminal、多个进程、多个日志,因为核心 需要你决策: - crate X 与 crate Y 同时修改 shared contract,建议暂停 Y 等待 X 合并 - - token 预算预计不足,建议降级低风险 crate 的二次审查 + - token 预算预计不足,建议低风险 crate 只保留核心检查 ``` 这个控制台只在大规模任务中显性出现。普通任务不应进入这样的界面。 @@ -174,7 +174,7 @@ workflow 不应成为通用默认。它适合满足以下条件的任务: 1. **成本阈值前确认**:超过预设 token、时间、并发或文件数量时,必须提示用户;小任务不显示成本面板,只有预计从轻路径升级为多 reviewer、并发 worker、长任务控制台或超过用户选择的预算模式时才弹出确认。 2. **渐进放大**:先抽样或小批量验证,再扩大到全量。 -3. **可随时降级**:用户能从 L3 降为 L2/L1,或停止低优先项,只保留关键验证。 +3. **可随时调整范围**:用户能从严格审查切到更快策略,或停止低优先项,只保留关键验证。 ## 8. Token、耗时和完成率平衡 @@ -200,7 +200,7 @@ BitFun 不能把“任务完成率最高”作为唯一目标。用户通常需 - 小样本成功,用户允许批量处理。 - reviewer 发现阻塞级或重要级问题。 -自动停止或降级应发生在: +自动停止或收敛范围应发生在: - 两轮审查没有新增实质问题。 - token 或时间接近预算。 @@ -217,17 +217,17 @@ BitFun 不能把“任务完成率最高”作为唯一目标。用户通常需 - 当前并发和最大并发。 - 已完成、等待、阻塞、失败和跳过数量。 - 执行位置、沙箱等级、写入范围、网络/凭据状态和授权来源。 -- 为什么建议追加预算或降级。 +- 为什么建议追加预算、调整范围或停止。 小任务不显示复杂成本面板,只在可能超预算时提示。 最低产品契约: - 默认轻路径不主动显示 token 面板;结束摘要只写已验证、未验证和残余风险。 -- 进入 L2/L3 review、并发 worker、批量队列或长任务控制台前,必须展示“预计多花多少 token/时间、能换来哪些覆盖或墙钟收益、有哪些降级选项”。 -- 预算确认至少提供三个动作:继续、降级、只收敛已完成结果。 +- 进入 L2/L3 review、并发 worker、批量队列或长任务控制台前,必须展示“预计多花多少 token/时间、能换来哪些覆盖或墙钟收益、有哪些范围控制和停止选项”。 +- 预算确认至少提供三个动作:继续、保留核心检查、只收敛已完成结果。 - 预算说明只使用估算区间,不承诺精确 token;超过估算上限前必须暂停扩大范围。 -- 无可靠 oracle、连续两轮无新增有效问题、冲突需要人工信息、或协调成本高于 item 处理成本时,默认建议停止或降级。 +- 无可靠 oracle、连续两轮无新增有效问题、冲突需要人工信息、或协调成本高于 item 处理成本时,默认建议停止或保留核心检查。 ## 9. 隔离审查的产品要求 @@ -259,7 +259,7 @@ BitFun 不能把“任务完成率最高”作为唯一目标。用户通常需 兼容要求: -- 既有 `/DeepReview` 可以继续存在,但必须文案化为 “Review: strict” 的高级入口或调试入口。 +- 既有 `/DeepReview` 可以继续存在,但只能文案化为迁移窗口内的历史兼容输入,等价路由到 “Review: Strict”。 - 如果一个场景从 `/DeepReview` 启动,用户仍应回到统一 Review 面板读取合并结果。 - DeepReview 的 child session 和 auxiliary pane 不应成为普通用户的第二套窗口模型;若因排障必须显示,默认折叠并标记为高级详情。 @@ -291,13 +291,13 @@ BitFun 不能把“任务完成率最高”作为唯一目标。用户通常需 - 复制 PR 就绪摘要 ``` -### 10.3 降级要求 +### 10.3 范围收敛要求 -当 DeepReview 过重时,系统应自动降级: +当严格审查过重时,系统应自动选择更合适的范围;用户只应看到 BitFun 为当前目标选择了最合适的 Review 方式: -- diff 很小且没有风险标签,降为 L1。 +- diff 很小且没有风险标签,使用快速独立审查。 - 缺少足够上下文或 oracle,先做诊断和提问,不启动 L3。 -- provider 容量不足,保留核心 reviewer,跳过低风险 reviewer。 +- provider 容量不足,保留核心检查并说明等待或继续的成本。 - 用户选择成本受限,二次审查只覆盖高风险文件。 ## 11. 过度设计和过度审核的防线 @@ -332,12 +332,12 @@ BitFun 的优势不应表现为“每一步都有重流程”,而应表现为 |---|---|---| | WF-CAND-01 | 统一 Review 入口 | 能证明 `/DeepReview`、child session 和 auxiliary pane 不再形成第二套普通用户入口,并有兼容迁移规则 | | WF-CAND-02 | 轻路径保护 | 低风险任务仍默认单 agent、无 workflow UI、无默认 reviewer,且首次有用结果不被拉长 | -| WF-CAND-03 | 成本确认 | 已定义预算触发、估算展示、降级动作和停止条件,并不会把成本面板带入小任务 | +| WF-CAND-03 | 成本确认 | 已定义预算触发、估算展示、范围控制动作和停止条件,并不会把成本面板带入小任务 | | WF-CAND-04 | 场景化 workflow | 仅 CI/测试失败、PR 评论批量修复、大规模迁移/审计等明确场景进入队列或并发 | | WF-CAND-05 | 单一 GUI 任务控制台 | 批量任务只显示一个控制台,并固定展示执行域、沙箱状态、异常、预算和可下钻详情 | | WF-CAND-06 | 审查只读且修复分离 | reviewer 默认只读;修复动作必须经过用户确认后的执行阶段 | | WF-CAND-07 | 样本和 oracle 优先 | 大规模自动化必须先样本验证;没有可靠 oracle 或人工验收标准时不得扩大执行 | -| WF-CAND-08 | 可降级和可收敛 | 长任务必须支持暂停、停止、降级、跳过低优先项和保留已完成结果 | +| WF-CAND-08 | 可调整范围和可收敛 | 长任务必须支持暂停、停止、保留核心检查、跳过低优先项和保留已完成结果 | ## 13. 指标治理调整 diff --git a/docs/sdlc-harness/research/external-research.md b/docs/sdlc-harness/research/external-research.md index e87fcbe71..00f46856e 100644 --- a/docs/sdlc-harness/research/external-research.md +++ b/docs/sdlc-harness/research/external-research.md @@ -35,7 +35,7 @@ | [Agentless](https://arxiv.org/abs/2407.01489) | 简单、可解释的定位/修复/验证流程可达到强基线 | 不宜默认采用全自治或强流程;结构化流程应作为基线 | | [Agentic AI in the SDLC](https://arxiv.org/abs/2604.26275) | Agentic SDLC 需要从架构、证据、生产力和治理同时评估 | BitFun 可扩展到 SDLC,但必须以产品体验和可验证价值逐步推进 | | [Terminal-Bench](https://arxiv.org/abs/2601.11868) / [Terminal-Bench 3.0](https://www.tbench.ai/) | 真实终端任务覆盖软件工程、ML、安全、数据科学等场景 | 需要终端任务回放和工具轨迹评测,防止任务泄漏和基准测试过拟合 | -| [RovoDev Code Reviewer](https://arxiv.org/html/2601.01129v1) | 在线评估显示 AI 审查可缩短 PR 周期,但缺少上下文会产生错误反馈 | 深度审查必须有上下文完整性、问题生命周期、反证和预算控制 | +| [RovoDev Code Reviewer](https://arxiv.org/html/2601.01129v1) | 在线评估显示 AI 审查可缩短 PR 周期,但缺少上下文会产生错误反馈 | 严格审查必须有上下文完整性、问题生命周期、反证和预算控制 | | [TraceLLM](https://arxiv.org/html/2602.01253v1) / [LLM-driven requirements change impact analysis](https://arxiv.org/html/2511.00262v1) | LLM 可辅助需求追踪和变更影响分析,但输出仍需成本、召回、精度和人工确认约束 | 需求变更影响面分析应输出候选集合、置信度和人工检查成本 | | [Testing with AI Agents](https://arxiv.org/abs/2603.13724) | AI 已大量参与测试生成,但测试质量需要结构化衡量 | 测试质量保护要关注质量、稳定性和变异杀伤,而非仅增加测试数量 | | [NIST SP 800-218A](https://csrc.nist.gov/pubs/sp/800/218/a/final) | 将生成式 AI 和基础模型纳入 SSDF 生命周期实践 | AI 参与开发后,安全开发框架需要覆盖模型、工具、数据、权限和供应链风险 | @@ -49,7 +49,7 @@ | [SLSA Provenance](https://slsa.dev/spec/v0.1/provenance) / [in-toto](https://slsa.dev/blog/2023/05/in-toto-and-slsa) | 构建和供应链证据需要说明来源、时间和生成方式 | 证据包应支持来源/证明引用,为发布就绪度和审计预留接口 | | [OWASP LLM Top 10](https://owasp.org/www-project-top-10-for-large-language-model-applications/) | prompt 注入、敏感信息、供应链、过度代理和模型拒绝服务都是 LLM 应用风险 | Hook/Event、plugin、工具、memory、外部适配器必须默认最小权限、脱敏、超时和预算 | | DORA / SPACE / DevEx | 速度、稳定性、协作和开发者体验需要联合衡量 | 指标体系同时覆盖速度、打断、信心、安全、质量和成本 | -| AI 编码成本治理 | 高级模型、AI 审查、CI/Actions 资源和长上下文都会形成显性成本 | 深度审查、评测、Hook 和智能体运行必须将 token、耗时、缓存命中和降级原因作为核心指标 | +| AI 编码成本治理 | 高级模型、AI 审查、CI/Actions 资源和长上下文都会形成显性成本 | 严格审查、评测、Hook 和智能体运行必须将 token、耗时、缓存命中和降级原因作为核心指标 | ## 4. 对抗性审查后的趋势判断 diff --git a/docs/sdlc-harness/review-verify-implementation-guardrails.md b/docs/sdlc-harness/review-verify-implementation-guardrails.md new file mode 100644 index 000000000..d5c199bf1 --- /dev/null +++ b/docs/sdlc-harness/review-verify-implementation-guardrails.md @@ -0,0 +1,157 @@ +# Review / Verify 收敛实施护栏 + +> 范围:约束统一 Review、DeepReview 收敛、ReviewTeam 内部化、PR Review 投影和 Verify 探索的后续两次大型 PR。 +> 本文是实施护栏,不新增 SDLC Harness 阶段路线,不定义新的 workflow DSL,也不替代 [product-requirements.md](product-requirements.md)、[agent-workflow-staged-plan.md](agent-workflow-staged-plan.md) 或 [features/pr-quality-gate.md](features/pr-quality-gate.md)。 + +## 1. 产品目标 + +后续实现必须解决用户问题,而不是展示内部架构能力: + +- 小任务要快,默认不给用户增加审查、门禁或 workflow 心智负担。 +- 用户表达“仔细审核”“提交前检查”“准备 PR”时,BitFun 能按风险选择足够的 review 强度。 +- 严格审查必须站在待审核代码和完成声明的对立面,尝试证伪,而不是替实现者背书。 +- 并发能力在 GUI 中表现为一个任务、一组进度和少量决策点,而不是多个窗口、多个会话入口或多个不可理解的日志流。 +- token 和耗时增长前必须让用户知道收益、成本、范围控制和停止选择;自动化不能在用户无感知时显著放大成本。 +- PR、团队治理和门禁是结果投影,不是所有任务的默认路径。 + +## 2. 名词收敛 + +| 概念 | 用户可见定位 | 内部兼容 | +|---|---|---| +| `Review` / `/review` | 唯一主审查入口;用户不需要先理解审查强度 | 后续统一路由到 L0/L1/L2/L3 | +| `Review: Strict` / 严格审查 | Review 的最高强度,适合高风险、大范围或用户显式要求 | 复用现有 `DeepReview` child session、manifest、queue 和 report enrichment | +| `/DeepReview` | 迁移窗口内的历史兼容命令,不作为长期产品入口 | PR1 保留手输兼容;PR2 起迁移到 `/review`,最终从普通体验中移除 | +| `ReviewTeam` | 严格审查的内部 reviewer 配置 | 类型名、配置路径和 manifest 名称可保留,避免破坏历史设置 | +| PR Review | PR/代码托管平台的评论、线程和就绪度投影 | 不拥有新的 reviewer 调度器 | +| Verify | 任务闭环中的证据生产动作 | 先探索,不在 PR1 中变成强制门禁或独立界面 | + +禁止新增与上表平级的用户概念,例如 `DeepReview`、`ReviewTeam`、`Verify Gate`、`Workflow Queue` 作为普通用户必须理解的入口。`/DeepReview` 只能服务迁移兼容,不能继续被包装成高级用户入口或新增能力承载点。 + +## 3. 两次大型 PR 边界 + +### PR1:统一 Review 入口和可见命名收敛 + +目标: + +- 用户入口从 `Review` / `Deep Review` 并列,收敛为 `Review` / `Review: Strict`。 +- `ReviewTeam` 在界面上表述为 strict review 的覆盖配置,而不是一个独立产品入口。 +- `/DeepReview` 只保留手输历史兼容,不在普通命令发现、设置入口或引导文案中强化。 +- DeepReview 架构文档明确:`deep_review` 会话、`DeepReview` agent、`ReviewTeamRunManifest` 和队列是 L3 strict review 的兼容实现细节。 + +允许: + +- 调整用户可见文案、设置页说明、会话 badge、report 标题、consent dialog 和相关测试。 +- 保留内部类型、API、配置 key、历史 session kind 和旧命令常量。 +- 保留辅助 pane,只要文案不把它包装成第二套审查产品。 + +禁止: + +- 不新增动态决策引擎。 +- 不改变 reviewer admission、queue、retry、manifest shape 或 backend policy。 +- 不把 Verify 设计成新门禁。 +- 不为 PR Review 新建一套 reviewer 执行器。 + +### PR2:动态 Review/Verify 决策和结果投影 + +目标: + +- 引入单一质量决策入口,根据用户意图、diff 规模、风险、验证证据、预算和团队策略选择 L0/L1/L2/L3。 +- 引入 `/review` 统一命令和对应 GUI 入口,由系统根据问题、待审核范围、变更难度、风险、质量诉求和预算动态选择强度。 +- 在任务执行过程中,当用户要求“仔细审核”或风险升高时,自动触发合适强度的只读 adversarial review。 +- Verify 作为证据生产动作进入任务闭环,但默认表现为已验证/未验证摘要,不独立启动新界面。 +- Review、Verify 和 PR 就绪度共用结果投影,PR 面板只消费摘要和证据,不拥有执行策略。 +- `/DeepReview` 开始迁移为 `/review --strict` 或等价内部路由;普通用户不需要学习或选择 DeepReview。 + +允许: + +- 新增小而明确的 policy/decision 模型。 +- 把现有 CodeReview、Strict Review、PR readiness 和 verify evidence 通过同一结果摘要投影。 +- 在高风险或用户明确要求时建议升级,并展示 token/耗时预估、范围控制和停止选项。 +- 对历史 `/DeepReview` 调用给出轻量迁移提示或静默路由到统一 Review,不再扩展 `/DeepReview` 专属交互。 + +禁止: + +- 不新增通用 workflow DSL。 +- 不复制 Agent Kernel 的任务生命周期、scheduler 或队列。 +- 不默认对每个任务启动 reviewer。 +- 不把缺失证据写成通过状态。 +- 不让自动化在没有用户提示或策略原因时显著增加 token。 + +## 4. Review 强度规则 + +| 强度 | 默认适用 | 执行倾向 | 用户呈现 | +|---|---|---|---| +| L0 自检 | 文档、小样式、低风险局部改动、已有验证 | 主 agent 总结已验证/未验证 | 任务完成摘要 | +| L1 快速独立审查 | 提交前、少量代码变更、用户要求“review” | 1 个只读 subagent 或等价隔离 reviewer | 简短问题清单和可信度 | +| L2 定向审查 | 安全、性能、架构、关键 UI、跨模块、验证缺口 | 2-3 个定向只读 reviewer | 合并结论、分歧和未覆盖范围 | +| L3 严格审查 | 大型 PR、核心接口、安全敏感、大迁移、用户明确要求严格 | 复用 DeepReview 内部 capacity queue、work packets 和 judge | Review: Strict,带预算和范围控制选择 | + +所有 L1-L3 review 都必须是对抗性的:reviewer 只能读取、查找、分析和提交审查结果;不能同时执行修复,不能继承实现者的自我证明,不能把“我刚实现的内容看起来没问题”当作独立结论。 + +只读 Review 和修正阶段必须分开判断: + +- 用户只说“review / 仔细审核 / 提交前检查 / 看看有没有问题”时,默认只读 Review,输出问题、证据、可信度和未覆盖项。 +- 用户说“修复并审核 / 修完再检查 / 发现问题就修”,或在 Review 结果页明确选择修复动作时,才进入修正阶段。 +- 修正阶段只能消费已确认的 Review 结论、用户选择的修复项和必要上下文;不能让原 reviewer 在同一轮里直接改代码。 +- 没有明确可验证 oracle、问题需要产品决策、或修复会扩大范围时,先回填输入框或请求用户确认,不自动写入。 +- 修复后是否复审由用户诉求、风险等级、变更范围和预算共同决定;低风险单点修复可快速复核,高风险或跨边界修复才进入严格复审。 + +## 5. GUI 交互原则 + +并发和 review 在 GUI 中必须降低复杂度: + +- 对普通任务,默认仍是聊天/任务结果视图,不弹出控制台。 +- 对 strict review,只显示一个 Review 页面或辅助面板,内部 reviewer 作为进度和结果来源展示。 +- 对批量迁移、CI 聚类或 PR 评论批量修复,显示单一任务控制台:进度、阻塞、预算、冲突和需决策项。 +- 不因为后台存在多个 subagent 就打开多个 GUI 窗口。 +- 不用“workflow”“subagent queue”“evidence graph”等内部术语作为普通用户主标签。 + +## 6. 成本和完成率平衡 + +默认策略: + +- 小任务优先首个有用结果时间。 +- 中风险任务优先 L1/L2 和最近验证,不默认 L3。 +- 大规模任务先做样本 gate,再扩大并发。 +- 预算不足时保留高风险/高价值项,跳过低风险二次审查。 +- 两轮 review 没有新增有效问题时,建议停止或保留核心检查。 +- 缺少 oracle 或上下文时,先说明未验证项,不静默追加 token。 + +产品必须展示: + +- 为什么建议升级 review 或 verify。 +- 预计增加的是解决率、覆盖率还是墙钟速度。 +- 不升级会放弃哪些覆盖。 +- 当前可安全收敛的结果是什么。 + +## 7. 历史兼容和取舍 + +迁移窗口内必须兼容: + +- 历史 `deep_review` session kind。 +- `DeepReview` agent type。 +- `/DeepReview` 命令的手输解析和旧会话恢复。 +- `deepReviewRunManifest`、`ReviewTeamRunManifest` 和 `ai.review_teams.default` 配置路径。 +- 既有 report/action-bar persistence。 + +不得为了兼容牺牲的关键诉求: + +- 默认产品入口必须是统一 Review。 +- 长期命令入口必须收敛到 `/review`,由系统动态选择审查强度。 +- `/DeepReview` 不能成为新的高级入口、能力分支或用户必须学习的概念。 +- ReviewTeam 不应继续被包装成普通用户必须理解的独立产品。 +- PR Review 不应成为第二套执行器。 +- Verify 不应默认变成新门禁。 +- 对抗性 review 不能被实现者自检替代。 + +## 8. PR 前复查清单 + +每个 PR 合入前必须检查: + +- 是否只改了当前 PR 承诺的阶段范围。 +- 是否没有新增重复入口、重复策略对象或重复 UI 文案体系。 +- 是否保留必要的历史 session/config/command 迁移兼容,同时没有把 `/DeepReview` 固化为长期入口。 +- 是否没有把低风险任务默认升级为重流程。 +- 是否说明 token/耗时/完成率的取舍。 +- 是否对 review/verify 的 adversarial 要求保持明确。 +- 是否通过了最小匹配验证,并由独立第三方视角完成对抗性复查。 diff --git a/docs/sdlc-harness/traceability-matrix.md b/docs/sdlc-harness/traceability-matrix.md index 9c87f2a37..8926c4632 100644 --- a/docs/sdlc-harness/traceability-matrix.md +++ b/docs/sdlc-harness/traceability-matrix.md @@ -31,5 +31,6 @@ | [implementation-plan.md](implementation-plan.md) | 阶段用户收益、必要技术前置、延期边界、验收条件和过程风险 | 不替代产品需求或模块契约 | | [product-requirements-agent-workflow-adjustment.md](product-requirements-agent-workflow-adjustment.md) | 提出 workflow、Review、并发 GUI 和成本控制的候选产品调整议题 | 不作为正式 PRD 编号、指标口径、阶段承诺或门禁规则;采纳前必须回填到权威文档 | | [agent-workflow-staged-plan.md](agent-workflow-staged-plan.md) | 将 workflow、审查、并发和成本控制映射到真实用户场景 | 不定义独立阶段路线,不定义新的核心对象模型;通用任务生命周期、scheduler 和队列状态归 Agent Kernel,Harness 只通过 provider/plan/step 参与编排;DeepReview 的 reviewer 调度只服务 L3 严格审查 | +| [review-verify-implementation-guardrails.md](review-verify-implementation-guardrails.md) | 约束后续两次大型 PR 的 Review / Verify 收敛实施边界 | 不定义新的 workflow DSL、任务 scheduler、PR gate 执行器或 Verify 门禁;PR1 只收敛可见入口和命名,PR2 才引入动态质量决策 | | 子模块文档 | 单模块输入、输出、状态、边界场景和成功标准 | 不重新定义产品定位 | | [governance/metrics-spec.md](governance/metrics-spec.md) | 指标公式、分母、窗口、负责人和解释边界 | 不直接作为阻断策略 | diff --git a/scripts/generate-i18n-contract.mjs b/scripts/generate-i18n-contract.mjs index d4f389d66..fe24fefac 100644 --- a/scripts/generate-i18n-contract.mjs +++ b/scripts/generate-i18n-contract.mjs @@ -550,7 +550,7 @@ mod tests { fn generated_contract_contains_shared_terms() { assert_eq!( generated_shared_term(LocaleId::EnUS, "features.deepReview"), - Some("Deep Review") + Some("Review: Strict") ); } } diff --git a/scripts/i18n-governance-baseline.json b/scripts/i18n-governance-baseline.json index 909cce053..96515fe89 100644 --- a/scripts/i18n-governance-baseline.json +++ b/scripts/i18n-governance-baseline.json @@ -6,13 +6,13 @@ "maxTotal": 0 }, "sharedTermDuplicates": { - "maxTotal": 185, + "maxTotal": 176, "bySurface": { "core": 15, "installer": 0, "mobile-web": 0, "relay-static-homepage": 0, - "web-ui": 170 + "web-ui": 161 }, "bySharedKey": { "agents.claw": 3, @@ -22,7 +22,7 @@ "connectionMethods.bitfunServer": 0, "connectionMethods.lan": 0, "features.codeAgent": 1, - "features.deepReview": 2, + "features.deepReview": 0, "features.remoteControl": 0, "features.settings": 1, "features.workspace": 2, @@ -30,11 +30,11 @@ "modes.expert": 2, "modes.review": 0, "product.name": 0, - "statuses.cancelled": 29, - "statuses.done": 26, - "statuses.failed": 44, + "statuses.cancelled": 27, + "statuses.done": 24, + "statuses.failed": 42, "statuses.loading": 5, - "statuses.running": 13, + "statuses.running": 12, "tools.edit": 33, "tools.explore": 2, "tools.search": 12, diff --git a/src/crates/assembly/core/src/service/i18n/generated_locale_contract.rs b/src/crates/assembly/core/src/service/i18n/generated_locale_contract.rs index e302f4c19..897c342e1 100644 --- a/src/crates/assembly/core/src/service/i18n/generated_locale_contract.rs +++ b/src/crates/assembly/core/src/service/i18n/generated_locale_contract.rs @@ -135,7 +135,7 @@ pub const GENERATED_SHARED_TERMS: &[GeneratedSharedTermEntry] = &[ GeneratedSharedTermEntry { locale: LocaleId::ZhCN, key: "features.deepReview", - value: "深度代码评审", + value: "严格审查", }, GeneratedSharedTermEntry { locale: LocaleId::ZhCN, @@ -310,7 +310,7 @@ pub const GENERATED_SHARED_TERMS: &[GeneratedSharedTermEntry] = &[ GeneratedSharedTermEntry { locale: LocaleId::ZhTW, key: "features.deepReview", - value: "深度程式碼審查", + value: "嚴格審查", }, GeneratedSharedTermEntry { locale: LocaleId::ZhTW, @@ -485,7 +485,7 @@ pub const GENERATED_SHARED_TERMS: &[GeneratedSharedTermEntry] = &[ GeneratedSharedTermEntry { locale: LocaleId::EnUS, key: "features.deepReview", - value: "Deep Review", + value: "Review: Strict", }, GeneratedSharedTermEntry { locale: LocaleId::EnUS, @@ -675,7 +675,7 @@ mod tests { fn generated_contract_contains_shared_terms() { assert_eq!( generated_shared_term(LocaleId::EnUS, "features.deepReview"), - Some("Deep Review") + Some("Review: Strict") ); } } diff --git a/src/crates/assembly/core/src/service/i18n/service.rs b/src/crates/assembly/core/src/service/i18n/service.rs index 689a33f9f..aaa5f207e 100644 --- a/src/crates/assembly/core/src/service/i18n/service.rs +++ b/src/crates/assembly/core/src/service/i18n/service.rs @@ -271,7 +271,7 @@ mod tests { service .translate_with_locale(&LocaleId::EnUS, "shared.features.deepReview", None) .await, - "Deep Review" + "Review: Strict" ); } diff --git a/src/mobile-web/src/i18n/generatedLocaleContract.ts b/src/mobile-web/src/i18n/generatedLocaleContract.ts index eca57ef40..978ed51a2 100644 --- a/src/mobile-web/src/i18n/generatedLocaleContract.ts +++ b/src/mobile-web/src/i18n/generatedLocaleContract.ts @@ -43,7 +43,7 @@ export const SHARED_TERMS_BY_LOCALE = { "features": { "remoteControl": "远程控制", "codeAgent": "代码助手", - "deepReview": "深度代码评审", + "deepReview": "严格审查", "settings": "设置", "workspace": "工作区", "accountLogin": "账户登录" @@ -94,7 +94,7 @@ export const SHARED_TERMS_BY_LOCALE = { "features": { "remoteControl": "遠端控制", "codeAgent": "程式碼助手", - "deepReview": "深度程式碼審查", + "deepReview": "嚴格審查", "settings": "設定", "workspace": "工作區", "accountLogin": "帳號登入" @@ -145,7 +145,7 @@ export const SHARED_TERMS_BY_LOCALE = { "features": { "remoteControl": "Remote Control", "codeAgent": "Code Agent", - "deepReview": "Deep Review", + "deepReview": "Review: Strict", "settings": "Settings", "workspace": "Workspace", "accountLogin": "Account Login" diff --git a/src/shared/i18n/resources/shared/en-US/terms.json b/src/shared/i18n/resources/shared/en-US/terms.json index d3e8fb4ff..b25fa3a70 100644 --- a/src/shared/i18n/resources/shared/en-US/terms.json +++ b/src/shared/i18n/resources/shared/en-US/terms.json @@ -6,7 +6,7 @@ "features": { "remoteControl": "Remote Control", "codeAgent": "Code Agent", - "deepReview": "Deep Review", + "deepReview": "Review: Strict", "settings": "Settings", "workspace": "Workspace", "accountLogin": "Account Login" diff --git a/src/shared/i18n/resources/shared/zh-CN/terms.json b/src/shared/i18n/resources/shared/zh-CN/terms.json index 9b3abf59a..cd20a3a21 100644 --- a/src/shared/i18n/resources/shared/zh-CN/terms.json +++ b/src/shared/i18n/resources/shared/zh-CN/terms.json @@ -6,7 +6,7 @@ "features": { "remoteControl": "远程控制", "codeAgent": "代码助手", - "deepReview": "深度代码评审", + "deepReview": "严格审查", "settings": "设置", "workspace": "工作区", "accountLogin": "账户登录" diff --git a/src/shared/i18n/resources/shared/zh-TW/terms.json b/src/shared/i18n/resources/shared/zh-TW/terms.json index 1dac765eb..d82bff943 100644 --- a/src/shared/i18n/resources/shared/zh-TW/terms.json +++ b/src/shared/i18n/resources/shared/zh-TW/terms.json @@ -6,7 +6,7 @@ "features": { "remoteControl": "遠端控制", "codeAgent": "程式碼助手", - "deepReview": "深度程式碼審查", + "deepReview": "嚴格審查", "settings": "設定", "workspace": "工作區", "accountLogin": "帳號登入" diff --git a/src/web-ui/src/app/components/NavPanel/sections/sessions/SessionsSection.tsx b/src/web-ui/src/app/components/NavPanel/sections/sessions/SessionsSection.tsx index c3edac90f..5a6f8ecc6 100644 --- a/src/web-ui/src/app/components/NavPanel/sections/sessions/SessionsSection.tsx +++ b/src/web-ui/src/app/components/NavPanel/sections/sessions/SessionsSection.tsx @@ -118,7 +118,7 @@ const getChildSessionBadge = (kind: Session['sessionKind']): string => { ? kind : 'btw'; const fallback = normalizedKind === 'deep_review' - ? 'Deep' + ? 'Strict' : normalizedKind === 'review' ? 'Review' : normalizedKind === 'subagent' @@ -135,7 +135,7 @@ const getReviewActivityBadge = (kind: 'review' | 'deep_review'): string => ? 'common:nav.sessions.deepReviewRunning' : 'common:nav.sessions.reviewRunning', { - defaultValue: kind === 'deep_review' ? 'Deep reviewing' : 'Reviewing', + defaultValue: 'Reviewing', }, ); diff --git a/src/web-ui/src/app/components/panels/review-platform/ReviewPlatformPanel.tsx b/src/web-ui/src/app/components/panels/review-platform/ReviewPlatformPanel.tsx index c043d366b..5388177f9 100644 --- a/src/web-ui/src/app/components/panels/review-platform/ReviewPlatformPanel.tsx +++ b/src/web-ui/src/app/components/panels/review-platform/ReviewPlatformPanel.tsx @@ -1085,7 +1085,7 @@ export const ReviewPlatformPanel: React.FC = ({ parentSession: marker?.parentSessionId ? flowState.sessions.get(marker.parentSessionId) : undefined, marker, kind, - title: marker?.title || getSessionTitle(session, kind === 'deep_review' ? 'Deep review' : 'Code review'), + title: marker?.title || getSessionTitle(session, kind === 'deep_review' ? 'Review: Strict' : 'Review'), requestedFiles, issueCount: summary.issueCount, riskLevel: summary.riskLevel, diff --git a/src/web-ui/src/app/scenes/agents/AgentsScene.test.tsx b/src/web-ui/src/app/scenes/agents/AgentsScene.test.tsx index 7a737f339..b1d5a1356 100644 --- a/src/web-ui/src/app/scenes/agents/AgentsScene.test.tsx +++ b/src/web-ui/src/app/scenes/agents/AgentsScene.test.tsx @@ -15,11 +15,6 @@ vi.mock('react-i18next', () => ({ }), })); -vi.mock('./components/ReviewTeamPage', () => ({ - default: () =>
review team
, - ReviewTeamErrorBoundary: ({ children }: { children: React.ReactNode }) => <>{children}, -})); - vi.mock('./components/CreateAgentPage', () => ({ default: () =>
create agent
, })); @@ -28,10 +23,6 @@ vi.mock('./components/AgentCard', () => ({ default: () =>
, })); -vi.mock('./components/AgentTeamCard', () => ({ - default: () =>
, -})); - vi.mock('./components/CoreAgentCard', () => ({ default: () =>
, })); @@ -104,10 +95,6 @@ vi.mock('@/infrastructure/api/service-api/SubagentAPI', () => ({ }, })); -vi.mock('@/shared/services/reviewTeamService', () => ({ - loadDefaultReviewTeam: vi.fn(async () => null), -})); - let JSDOMCtor: (new ( html?: string, options?: { pretendToBeVisual?: boolean } @@ -167,15 +154,15 @@ describeWithJsdom('AgentsScene', () => { useAgentsStore.getState().openHome(); }); - it('keeps the review team detail page inside a full-height scene page wrapper', async () => { - useAgentsStore.getState().openReviewTeam(); + it('keeps agent creation inside a full-height scene page wrapper', async () => { + useAgentsStore.getState().openCreateAgent(); const { default: AgentsScene } = await import('./AgentsScene'); await act(async () => { root.render(); }); - expect(container.querySelector('[data-testid="review-team-page"]')).toBeTruthy(); + expect(container.querySelector('[data-testid="create-agent-page"]')).toBeTruthy(); expect(container.querySelector('.bitfun-agents-scene--page')).toBeTruthy(); }, 10_000); diff --git a/src/web-ui/src/app/scenes/agents/AgentsScene.tsx b/src/web-ui/src/app/scenes/agents/AgentsScene.tsx index 69c4bebd9..3d480269d 100644 --- a/src/web-ui/src/app/scenes/agents/AgentsScene.tsx +++ b/src/web-ui/src/app/scenes/agents/AgentsScene.tsx @@ -8,7 +8,6 @@ import { Plus, Puzzle, Search as SearchIcon, - ShieldCheck, Trash2, Wrench, } from 'lucide-react'; @@ -24,10 +23,8 @@ import { GalleryZone, } from '@/app/components'; import AgentCard from './components/AgentCard'; -import AgentTeamCard from './components/AgentTeamCard'; import CoreAgentCard, { type CoreAgentMeta } from './components/CoreAgentCard'; import CreateAgentPage from './components/CreateAgentPage'; -import ReviewTeamPage, { ReviewTeamErrorBoundary } from './components/ReviewTeamPage'; import { type AgentWithCapabilities, useAgentsStore, @@ -46,8 +43,6 @@ import { configManager } from '@/infrastructure/config/services/ConfigManager'; import type { ModeSkillInfo } from '@/infrastructure/config/types'; import type { SubagentInfo } from '@/infrastructure/api/service-api/SubagentAPI'; import { useNotification } from '@/shared/notification-system'; -import { useCurrentWorkspace } from '@/infrastructure/contexts/WorkspaceContext'; -import { loadDefaultReviewTeam, type ReviewTeam } from '@/shared/services/reviewTeamService'; const UNGROUPED_SKILL_GROUP = '__ungrouped__'; @@ -182,7 +177,6 @@ function buildSkillGroups( const AgentsHomeView: React.FC = () => { const { t } = useTranslation('scenes/agents'); const notification = useNotification(); - const { workspacePath } = useCurrentWorkspace(); const [deletingAgent, setDeletingAgent] = useState(false); const { searchQuery, @@ -193,7 +187,6 @@ const AgentsHomeView: React.FC = () => { setAgentFilterType, openCreateAgent, openEditAgent, - openReviewTeam, } = useAgentsStore(); const [selectedAgentId, setSelectedAgentId] = React.useState(null); const [activeCapabilityTab, setActiveCapabilityTab] = React.useState('tools'); @@ -206,7 +199,6 @@ const AgentsHomeView: React.FC = () => { const [savingTools, setSavingTools] = React.useState(false); const [savingSkills, setSavingSkills] = React.useState(false); const [savingSubagents, setSavingSubagents] = React.useState(false); - const [reviewTeam, setReviewTeam] = useState(null); const [computerUseEnabled, setComputerUseEnabled] = useState(true); const { @@ -237,33 +229,9 @@ const AgentsHomeView: React.FC = () => { sceneId: 'agents', refetch: () => { void loadAgents(); - void loadDefaultReviewTeam(workspacePath || undefined).then(setReviewTeam).catch(() => { - setReviewTeam(null); - }); }, }); - useEffect(() => { - let cancelled = false; - - (async () => { - try { - const loadedTeam = await loadDefaultReviewTeam(workspacePath || undefined); - if (!cancelled) { - setReviewTeam(loadedTeam); - } - } catch { - if (!cancelled) { - setReviewTeam(null); - } - } - })(); - - return () => { - cancelled = true; - }; - }, [workspacePath]); - useEffect(() => { let cancelled = false; const loadComputerUseEnabled = () => { @@ -589,14 +557,6 @@ const AgentsHomeView: React.FC = () => { > {t('nav.coreAgents')} - - {reviewTeam ? 1 : 0} - - )} - > - {loading && !reviewTeam ? renderSkeletons('team') : null} - - {!loading && reviewTeam ? ( - - - - ) : null} - - {!loading && !reviewTeam ? ( - } - message={t('teamsZone.empty')} - /> - ) : null} - - { ); } - if (page === 'reviewTeam') { - return ( -
- - - -
- ); - } - return ; }; diff --git a/src/web-ui/src/app/scenes/agents/agentsStore.ts b/src/web-ui/src/app/scenes/agents/agentsStore.ts index 6d36072b1..09c5837d6 100644 --- a/src/web-ui/src/app/scenes/agents/agentsStore.ts +++ b/src/web-ui/src/app/scenes/agents/agentsStore.ts @@ -30,7 +30,7 @@ export interface AgentWithCapabilities extends SubagentInfo { export const CAPABILITY_COLORS: Record = CAPABILITY_ACCENT; -export type AgentsScenePage = 'home' | 'createAgent' | 'reviewTeam'; +export type AgentsScenePage = 'home' | 'createAgent'; export type AgentEditorMode = 'create' | 'edit'; export type AgentFilterLevel = 'all' | 'builtin' | 'user' | 'project'; export type AgentFilterType = 'all' | 'mode' | 'subagent'; @@ -49,7 +49,6 @@ interface AgentsStoreState { openHome: () => void; openCreateAgent: () => void; openEditAgent: (agentId: string) => void; - openReviewTeam: () => void; } export const useAgentsStore = create((set) => ({ @@ -74,9 +73,4 @@ export const useAgentsStore = create((set) => ({ agentEditorMode: 'edit', editingAgentId: agentId, }), - openReviewTeam: () => set({ - page: 'reviewTeam', - agentEditorMode: 'create', - editingAgentId: null, - }), })); diff --git a/src/web-ui/src/app/scenes/agents/components/AgentTeamCard.scss b/src/web-ui/src/app/scenes/agents/components/AgentTeamCard.scss deleted file mode 100644 index a6d22dc95..000000000 --- a/src/web-ui/src/app/scenes/agents/components/AgentTeamCard.scss +++ /dev/null @@ -1,152 +0,0 @@ -@use '../../../../component-library/styles/tokens' as *; -@use './AgentSurfaceCard' as surface; - -.agent-team-card { - --team-card-accent: var(--color-accent-400); - --team-card-gradient: linear-gradient( - 135deg, - color-mix(in srgb, var(--team-card-accent) 28%, transparent) 0%, - color-mix(in srgb, var(--team-card-accent) 9%, transparent) 100% - ); - @include surface.agent-surface-card( - var(--team-card-gradient), - color-mix(in srgb, var(--team-card-accent) 70%, var(--color-accent-500)) - ); - - &__header { - display: flex; - align-items: center; - gap: $size-gap-3; - padding: $size-gap-3; - padding-bottom: 0; - position: relative; - z-index: 1; - } - - &__icon { - display: inline-flex; - align-items: center; - justify-content: center; - width: 40px; - height: 40px; - border-radius: 10px; - color: color-mix(in srgb, var(--team-card-accent) 78%, var(--color-text-primary)); - background: color-mix(in srgb, var(--team-card-accent) 14%, transparent); - border: 1px solid color-mix(in srgb, var(--team-card-accent) 24%, transparent); - box-shadow: inset 0 1px 0 color-mix(in srgb, white 8%, transparent); - flex-shrink: 0; - } - - &__header-copy { - display: flex; - flex-direction: column; - gap: 4px; - min-width: 0; - flex: 1; - overflow: hidden; - } - - &__title-row { - display: flex; - align-items: center; - justify-content: space-between; - gap: $size-gap-2; - min-width: 0; - } - - &__title { - min-width: 0; - font-size: 1.2em; - font-weight: 900; - color: var(--color-text-primary); - line-height: $line-height-base; - white-space: nowrap; - overflow: hidden; - text-overflow: ellipsis; - } - - &__role { - display: inline-flex; - align-items: center; - gap: 4px; - font-size: var(--font-size-xs); - font-weight: $font-weight-medium; - color: var(--team-card-accent); - opacity: 0.9; - } - - &__body { - display: flex; - flex-direction: column; - justify-content: flex-start; - flex: 1; - padding: $size-gap-2 $size-gap-3; - position: relative; - z-index: 1; - } - - &__desc { - margin: 0; - font-size: 0.85em; - font-weight: 300; - color: var(--color-text-secondary); - line-height: $line-height-relaxed; - display: -webkit-box; - -webkit-line-clamp: 2; - -webkit-box-orient: vertical; - overflow: hidden; - word-break: break-word; - } - - &__footer { - display: flex; - align-items: center; - justify-content: space-between; - gap: $size-gap-2; - padding: $size-gap-2 $size-gap-3; - @include surface.agent-surface-footer(var(--team-card-gradient)); - } - - &:hover &__footer::after { - opacity: 1; - } - - &__tags { - display: inline-flex; - align-items: center; - gap: 4px; - flex-wrap: wrap; - min-width: 0; - max-width: 100%; - position: relative; - z-index: 1; - } - - &__tag-chip { - display: inline-flex; - align-items: center; - padding: 2px 8px; - border-radius: $size-radius-full; - border: 1px solid; - background: var(--color-overlay-white-04); - font-size: 10px; - font-weight: $font-weight-medium; - white-space: nowrap; - } -} - -// Shared with ReviewTeamPage for summary metric chips (BEM class reuse). -.agent-team-card__metrics { - display: inline-flex; - align-items: center; - flex-wrap: wrap; - gap: 6px; - min-width: 0; -} - -@media (max-width: 720px) { - .agent-team-card { - width: 100%; - min-height: 190px; - } -} diff --git a/src/web-ui/src/app/scenes/agents/components/AgentTeamCard.test.tsx b/src/web-ui/src/app/scenes/agents/components/AgentTeamCard.test.tsx deleted file mode 100644 index 46409d5c8..000000000 --- a/src/web-ui/src/app/scenes/agents/components/AgentTeamCard.test.tsx +++ /dev/null @@ -1,61 +0,0 @@ -import React from 'react'; -import { readFileSync } from 'node:fs'; -import { fileURLToPath } from 'node:url'; -import { renderToStaticMarkup } from 'react-dom/server'; -import { describe, expect, it } from 'vitest'; -import AgentTeamCard from './AgentTeamCard'; - -function readAgentTeamCardStylesheet(): string { - const stylesheet = readFileSync( - fileURLToPath(new URL('./AgentTeamCard.scss', import.meta.url)), - 'utf8', - ); - return stylesheet.replace(/\r\n/g, '\n'); -} - -function extractBlock(stylesheet: string, selector: string): string { - const escapedSelector = selector.replace(/[.*+?^${}()|[\]\\]/g, '\\$&'); - const match = stylesheet.match(new RegExp(`${escapedSelector}\\s*\\{(?[\\s\\S]*?)\\n\\s*\\}`)); - return match?.groups?.body ?? ''; -} - -describe('AgentTeamCard', () => { - it('keeps role summary compact when the review team grows', () => { - const markup = renderToStaticMarkup( - undefined} - />, - ); - - const chipMatches = markup.match(/agent-team-card__tag-chip/g) ?? []; - expect(chipMatches).toHaveLength(3); - expect(markup).toContain('Business logic'); - expect(markup).toContain('Performance'); - expect(markup).toContain('Security'); - expect(markup).not.toContain('Architecture'); - expect(markup).not.toContain('Frontend'); - expect(markup).not.toContain('Judge'); - }); - - it('keeps role summary tags shrinkable and wrapping instead of clipping chips', () => { - const stylesheet = readAgentTeamCardStylesheet(); - const tagsBlock = extractBlock(stylesheet, '&__tags'); - const tagChipBlock = extractBlock(stylesheet, '&__tag-chip'); - - expect(tagsBlock).toContain('flex-wrap: wrap;'); - expect(tagsBlock).toContain('min-width: 0;'); - expect(tagsBlock).toContain('max-width: 100%;'); - expect(tagChipBlock).toContain('white-space: nowrap;'); - }); -}); diff --git a/src/web-ui/src/app/scenes/agents/components/AgentTeamCard.tsx b/src/web-ui/src/app/scenes/agents/components/AgentTeamCard.tsx deleted file mode 100644 index 16ebe32f9..000000000 --- a/src/web-ui/src/app/scenes/agents/components/AgentTeamCard.tsx +++ /dev/null @@ -1,80 +0,0 @@ -import React from 'react'; -import { ShieldCheck, Sparkles } from 'lucide-react'; -import { AGENT_TEAM_TAG_COLORS } from '../agentTheme'; -import './AgentTeamCard.scss'; - -interface AgentTeamCardProps { - teamId?: string; - index?: number; - title: string; - subtitle: string; - roleName: string; - tagNames: string[]; - onOpen: () => void; -} - -const AgentTeamCard: React.FC = ({ - teamId, - index = 0, - title, - subtitle, - roleName, - tagNames, - onOpen, -}) => { - return ( -
{ - if (event.key === 'Enter') { - onOpen(); - } - }} - aria-label={title} - data-testid="agents-team-card" - data-team-id={teamId ?? ''} - > -
-
- -
-
-
- {title} -
- - - {roleName} - -
-
- -
-

{subtitle}

-
- -
-
- {tagNames.slice(0, 3).map((name, i) => ( - - {name} - - ))} -
-
-
- ); -}; - -export default AgentTeamCard; diff --git a/src/web-ui/src/app/scenes/agents/components/ReviewTeamPage.scss b/src/web-ui/src/app/scenes/agents/components/ReviewTeamPage.scss deleted file mode 100644 index eda134472..000000000 --- a/src/web-ui/src/app/scenes/agents/components/ReviewTeamPage.scss +++ /dev/null @@ -1,640 +0,0 @@ -@use '../../../../component-library/styles/tokens' as *; - -.review-team-page { - /* Aligns overview + member rows with `AgentTeamCard` / gallery team cards */ - --review-team-accent: var(--color-accent-400); - --review-team-shadow-color: rgb(var(--color-overlay-slate-rgb)); - --member-accent: var(--color-accent-500); - - .bitfun-config-page-header, - .bitfun-config-page-content { - padding-inline: clamp(40px, 6vw, 80px); - } - - .bitfun-config-page-header__inner, - .bitfun-config-page-content__inner { - width: min(100%, 1480px); - } - - .bitfun-config-page-header-divider { - width: min(calc(100% - (2 * clamp(40px, 6vw, 80px))), 1480px); - } - - .bitfun-config-page-content__inner { - max-width: 1480px; - } - - &__agent-team-metrics-wrap { - margin-bottom: $size-gap-3; - width: 100%; - min-width: 0; - } - - &__summary-grid { - display: grid; - gap: 8px; - grid-template-columns: repeat(3, minmax(0, 1fr)); - } - - &__summary-card { - display: flex; - flex-direction: column; - gap: 10px; - min-height: 118px; - padding: 12px 14px; - border-radius: 12px; - border: 1px solid color-mix(in srgb, var(--review-team-accent) 16%, var(--border-subtle)); - background: - linear-gradient(180deg, color-mix(in srgb, var(--review-team-accent) 8%, transparent), transparent), - color-mix(in srgb, var(--element-bg-soft) 86%, transparent); - } - - &__summary-card--primary { - background: - radial-gradient(circle at top left, color-mix(in srgb, var(--review-team-accent) 22%, transparent), transparent 62%), - color-mix(in srgb, var(--review-team-accent) 9%, var(--element-bg-soft)); - } - - &__summary-card-head { - display: flex; - align-items: center; - gap: 10px; - min-width: 0; - } - - &__summary-card-icon { - display: inline-flex; - align-items: center; - justify-content: center; - width: 32px; - height: 32px; - border-radius: 10px; - flex-shrink: 0; - color: color-mix(in srgb, var(--review-team-accent) 78%, var(--color-text-primary)); - background: color-mix(in srgb, var(--review-team-accent) 14%, transparent); - border: 1px solid color-mix(in srgb, var(--review-team-accent) 24%, transparent); - box-shadow: inset 0 1px 0 color-mix(in srgb, white 8%, transparent); - - svg { - flex-shrink: 0; - } - } - - &__header-actions, - &__policy-strip { - display: inline-flex; - align-items: center; - flex-wrap: wrap; - gap: 8px; - } - - &__policy-panel { - display: grid; - grid-template-columns: minmax(220px, 1fr) minmax(360px, 1.25fr); - align-items: center; - gap: $size-gap-4; - width: 100%; - padding: 16px; - border: 0; - border-radius: inherit; - background: transparent; - color: var(--color-text-primary); - text-align: left; - cursor: pointer; - transition: background $motion-fast $easing-standard; - - &:hover { - background: color-mix(in srgb, var(--element-bg-medium) 68%, transparent); - } - - &:focus-visible { - background: color-mix(in srgb, var(--element-bg-medium) 68%, transparent); - outline: 2px solid color-mix(in srgb, var(--color-accent-500) 45%, var(--border-medium)); - outline-offset: 2px; - } - } - - &__policy-copy { - display: flex; - flex-direction: column; - gap: 7px; - min-width: 0; - } - - &__policy-eyebrow { - font-size: 11px; - font-weight: $font-weight-semibold; - text-transform: uppercase; - letter-spacing: 0; - color: var(--color-text-muted); - } - - &__policy-description { - margin: 0; - font-size: var(--font-size-sm); - line-height: 1.65; - color: var(--color-text-secondary); - } - - &__policy-metrics { - display: grid; - grid-template-columns: repeat(auto-fit, minmax(120px, 1fr)); - gap: 8px; - min-width: 0; - } - - &__policy-metric { - display: flex; - flex-direction: column; - gap: 4px; - min-width: 0; - min-height: 58px; - padding: 9px 10px; - border: 1px solid color-mix(in srgb, var(--review-team-accent) 16%, var(--border-subtle)); - border-radius: 12px; - background: - linear-gradient(180deg, color-mix(in srgb, var(--review-team-accent) 8%, transparent), transparent), - color-mix(in srgb, var(--element-bg-soft) 86%, transparent); - - span { - overflow: hidden; - text-overflow: ellipsis; - white-space: nowrap; - font-size: 11px; - color: var(--color-text-muted); - } - - strong { - overflow: hidden; - text-overflow: ellipsis; - white-space: nowrap; - font-size: var(--font-size-sm); - font-weight: $font-weight-semibold; - color: var(--color-text-primary); - } - } - - /* Sections that render their own cards should not show the default body frame. */ - &__section--no-body-frame .bitfun-config-page-section__body { - background: transparent; - border: 0; - border-radius: 0; - overflow: visible; - } - - &__error-detail { - max-height: 220px; - overflow: auto; - margin: 0; - padding: 12px; - border-radius: 8px; - background: var(--element-bg-subtle); - color: var(--color-text-secondary); - font-size: 11px; - } - - &__summary-kicker { - flex: 1; - min-width: 0; - font-size: 11px; - font-weight: $font-weight-semibold; - text-transform: uppercase; - letter-spacing: 0; - color: var(--color-text-muted); - } - - &__summary-value { - margin: 0; - font-size: var(--font-size-sm); - line-height: 1.7; - color: var(--color-text-primary); - } - - &__section-badges { - display: inline-flex; - gap: 6px; - flex-wrap: wrap; - justify-content: flex-end; - } - - &__row-control { - display: inline-flex; - align-items: center; - } - - &__execution-grid { - display: grid; - gap: 1px; - grid-template-columns: repeat(2, minmax(0, 1fr)); - margin-top: 0; - overflow: hidden; - border-radius: 8px; - } - - &__execution-card { - display: flex; - flex-direction: column; - gap: 10px; - padding: 16px; - border: 0; - background: color-mix(in srgb, var(--color-bg-primary) 34%, transparent); - } - - &__execution-copy { - margin: 0; - font-size: var(--font-size-sm); - line-height: 1.6; - color: var(--color-text-secondary); - } - - &__strategy-panel { - display: flex; - flex-direction: column; - gap: $size-gap-3; - padding: 16px; - margin-bottom: 0; - border-bottom: 1px solid var(--border-subtle); - background: transparent; - } - - &__strategy-panel-copy { - display: flex; - flex-direction: column; - gap: 8px; - - p { - margin: 0; - max-width: 760px; - font-size: var(--font-size-sm); - line-height: 1.6; - color: var(--color-text-secondary); - } - } - - &__strategy-options { - display: grid; - grid-template-columns: repeat(3, minmax(0, 1fr)); - gap: 8px; - - &--compact { - grid-template-columns: repeat(4, minmax(0, 1fr)); - max-width: 920px; - } - } - - &__strategy-option { - display: flex; - flex-direction: column; - gap: 7px; - min-height: 106px; - padding: 12px; - border-radius: 8px; - border: 1px solid var(--border-subtle); - background: color-mix(in srgb, var(--element-bg-soft) 72%, transparent); - color: var(--color-text-primary); - text-align: left; - cursor: pointer; - transition: - transform $motion-fast $easing-standard, - border-color $motion-fast $easing-standard, - box-shadow $motion-fast $easing-standard; - - &:hover:not(:disabled) { - transform: translateY(-1px); - border-color: color-mix(in srgb, var(--color-accent-500) 36%, var(--border-medium)); - box-shadow: 0 10px 22px color-mix(in srgb, var(--review-team-shadow-color) 10%, transparent); - } - - &.is-selected { - border-color: color-mix(in srgb, var(--color-accent-500) 58%, var(--color-bg-primary)); - background: color-mix(in srgb, var(--color-accent-200) 34%, var(--element-bg-soft)); - box-shadow: 0 0 0 1px color-mix(in srgb, var(--color-accent-500) 20%, transparent); - } - - &:disabled { - cursor: not-allowed; - opacity: 0.66; - } - } - - &__strategy-option-title { - font-size: var(--font-size-sm); - font-weight: $font-weight-semibold; - } - - &__strategy-option-summary, - &__strategy-option-impact { - font-size: var(--font-size-xs); - line-height: 1.45; - color: var(--color-text-secondary); - } - - &__strategy-option-impact { - color: var(--color-text-muted); - } - - &__member-layout { - display: grid; - gap: $size-gap-4; - grid-template-columns: 280px 1fr; - align-items: start; - } - - &__member-list { - display: flex; - flex-direction: column; - gap: 6px; - min-width: 0; - } - - &__member-list-item { - display: flex; - align-items: center; - gap: 10px; - width: 100%; - padding: 10px 12px; - border: 1px solid color-mix(in srgb, var(--review-team-accent) 16%, var(--border-subtle)); - border-radius: 12px; - background: - linear-gradient(180deg, color-mix(in srgb, var(--member-accent) 8%, transparent), transparent), - color-mix(in srgb, var(--element-bg-soft) 86%, transparent); - color: var(--color-text-primary); - text-align: left; - cursor: pointer; - transition: - background $motion-fast $easing-standard, - border-color $motion-fast $easing-standard, - box-shadow $motion-fast $easing-standard; - - &:hover { - border-color: color-mix(in srgb, var(--member-accent) 34%, var(--border-medium)); - background: - linear-gradient(180deg, color-mix(in srgb, var(--member-accent) 10%, transparent), transparent), - color-mix(in srgb, var(--element-bg-soft) 94%, transparent); - } - - &.is-selected { - border-color: color-mix(in srgb, var(--member-accent) 54%, var(--color-bg-primary)); - background: - radial-gradient(circle at top left, color-mix(in srgb, var(--member-accent) 22%, transparent), transparent 62%), - color-mix(in srgb, var(--member-accent) 9%, var(--element-bg-soft)); - box-shadow: 0 0 0 1px color-mix(in srgb, var(--member-accent) 24%, transparent); - } - - &:focus-visible { - outline: 2px solid color-mix(in srgb, var(--member-accent) 50%, var(--border-medium)); - outline-offset: 2px; - } - } - - &__member-list-icon { - display: inline-flex; - align-items: center; - justify-content: center; - width: 32px; - height: 32px; - border-radius: 10px; - background: color-mix(in srgb, var(--member-accent) 14%, transparent); - color: color-mix(in srgb, var(--member-accent) 78%, var(--color-text-primary)); - border: 1px solid color-mix(in srgb, var(--member-accent) 24%, transparent); - box-shadow: inset 0 1px 0 color-mix(in srgb, white 8%, transparent); - flex-shrink: 0; - } - - &__member-list-body { - display: flex; - flex-direction: column; - gap: 2px; - min-width: 0; - flex: 1; - } - - &__member-list-name { - font-size: var(--font-size-sm); - font-weight: $font-weight-semibold; - color: var(--color-text-primary); - line-height: 1.35; - overflow: hidden; - text-overflow: ellipsis; - white-space: nowrap; - } - - &__member-list-meta { - font-size: 11px; - color: var(--color-text-muted); - overflow: hidden; - text-overflow: ellipsis; - white-space: nowrap; - } - - &__member-list-badges { - display: inline-flex; - gap: 6px; - flex-shrink: 0; - } - - &__member-detail-panel { - display: flex; - flex-direction: column; - gap: 16px; - padding: 20px; - border: 1px solid color-mix(in srgb, var(--review-team-accent) 16%, var(--border-subtle)); - border-radius: 12px; - background: - linear-gradient(180deg, color-mix(in srgb, var(--member-accent) 8%, transparent), transparent), - color-mix(in srgb, var(--element-bg-soft) 86%, transparent); - animation: review-team-card-expand $motion-base $easing-standard forwards; - } - - &__detail-hero { - display: flex; - align-items: flex-start; - gap: $size-gap-4; - padding: 0 0 16px; - border-bottom: 1px solid var(--border-subtle); - } - - &__detail-icon { - display: inline-flex; - align-items: center; - justify-content: center; - width: 44px; - height: 44px; - border-radius: 10px; - background: color-mix(in srgb, var(--member-accent) 14%, transparent); - color: color-mix(in srgb, var(--member-accent) 78%, var(--color-text-primary)); - border: 1px solid color-mix(in srgb, var(--member-accent) 24%, transparent); - box-shadow: inset 0 1px 0 color-mix(in srgb, white 8%, transparent); - flex-shrink: 0; - } - - &__detail-copy { - display: flex; - flex-direction: column; - gap: 10px; - min-width: 0; - flex: 1; - } - - &__detail-title-row { - display: flex; - align-items: flex-start; - justify-content: space-between; - gap: $size-gap-3; - } - - &__detail-name { - margin: 0; - font-size: var(--font-size-xl); - font-weight: $font-weight-semibold; - color: var(--color-text-primary); - line-height: 1.3; - } - - &__detail-role { - margin: 4px 0 0; - font-size: var(--font-size-sm); - font-weight: $font-weight-medium; - color: var(--color-text-muted); - } - - &__detail-badges { - display: inline-flex; - gap: 8px; - flex-wrap: wrap; - justify-content: flex-end; - } - - &__detail-description { - margin: 0; - padding: 0 0 16px; - font-size: var(--font-size-sm); - line-height: 1.7; - color: var(--color-text-secondary); - } - - &__responsibilities { - display: flex; - flex-direction: column; - gap: 10px; - padding: 16px 0 0; - } - - &__block-label { - font-size: 11px; - font-weight: $font-weight-semibold; - text-transform: uppercase; - letter-spacing: 0; - color: var(--color-text-muted); - } - - &__responsibility-list { - display: grid; - gap: 10px; - margin: 0; - padding-left: 18px; - color: var(--color-text-primary); - line-height: 1.6; - } - - &__responsibility-item { - font-size: var(--font-size-sm); - } - - &__model-picker { - max-width: 680px; - } - - &__model-fallback-note { - margin: 0 0 12px; - padding: 10px 12px; - border-radius: 8px; - background: color-mix(in srgb, var(--color-warning) 12%, transparent); - color: var(--color-text-secondary); - font-size: var(--font-size-sm); - line-height: 1.55; - } - - &__add-controls { - display: grid; - grid-template-columns: minmax(0, 1fr) auto; - gap: $size-gap-2; - max-width: 720px; - } - - &__empty-candidate { - display: inline-flex; - align-items: center; - gap: 8px; - margin: 12px 0 0; - font-size: var(--font-size-sm); - color: var(--color-text-muted); - } -} - -@keyframes review-team-card-expand { - from { - opacity: 0; - max-height: 0; - } - to { - opacity: 1; - max-height: 800px; - } -} - -@media (max-width: 960px) { - .review-team-page { - &__summary-grid, - &__member-layout, - &__execution-grid, - &__strategy-options, - &__strategy-options--compact { - grid-template-columns: 1fr; - } - - &__policy-panel { - grid-template-columns: 1fr; - align-items: stretch; - } - - &__detail-hero, - &__detail-title-row { - flex-direction: column; - } - - &__member-detail-panel { - animation: none; - } - } -} - -@container config-panel (max-width: 900px) { - .review-team-page { - .bitfun-config-page-header, - .bitfun-config-page-content { - padding-inline: var(--size-gap-4); - } - - .bitfun-config-page-header__inner, - .bitfun-config-page-content__inner { - width: 100%; - } - - .bitfun-config-page-header-divider { - width: calc(100% - (2 * var(--size-gap-4))); - } - } -} - -@media (max-width: 720px) { - .review-team-page { - &__member-list-badges { - display: none; - } - - &__add-controls { - grid-template-columns: 1fr; - } - } -} diff --git a/src/web-ui/src/app/scenes/agents/components/ReviewTeamPage.test.tsx b/src/web-ui/src/app/scenes/agents/components/ReviewTeamPage.test.tsx deleted file mode 100644 index 2cd62b74b..000000000 --- a/src/web-ui/src/app/scenes/agents/components/ReviewTeamPage.test.tsx +++ /dev/null @@ -1,377 +0,0 @@ -import React, { act } from 'react'; -import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'; -import { createRoot, type Root } from 'react-dom/client'; - -const loadDefaultReviewTeam = vi.hoisted(() => vi.fn()); -const notificationFns = vi.hoisted(() => ({ - success: vi.fn(), - error: vi.fn(), - warning: vi.fn(), - info: vi.fn(), -})); -const tMock = vi.hoisted(() => { - const messages: Record = { - 'reviewTeams.default.members': '{{count}} members', - 'reviewTeams.detail.back': 'Back to Agents', - 'reviewTeams.detail.extraCount': '{{count}} extra Sub-Agents', - 'reviewTeams.detail.fileCountValue': '{{count}} files', - 'reviewTeams.detail.fileSplitThreshold': 'File split threshold', - 'reviewTeams.detail.instancesValue': '{{count}} max', - 'reviewTeams.detail.judgeTimeout': 'Judge timeout', - 'reviewTeams.detail.loading': 'Loading code review team...', - 'reviewTeams.detail.localOnly': 'Code review', - 'reviewTeams.detail.localOnlyDescription': 'Runs inside BitFun as read-only Sub-Agents and reports back into this review thread.', - 'reviewTeams.detail.lockedCount': '{{count}} locked roles', - 'reviewTeams.detail.maxSameRoleInstances': 'Max same-role instances', - 'reviewTeams.detail.membersDescription': 'Select a member to review its role, responsibilities, model, and strategy override.', - 'reviewTeams.detail.membersTitle': 'Team Members', - 'reviewTeams.detail.memberTypes.core': 'Core role', - 'reviewTeams.detail.memberTypes.extra': 'Extra Sub-Agent', - 'reviewTeams.detail.memberTypes.locked': 'Locked', - 'reviewTeams.detail.openSettings': 'Review settings', - 'reviewTeams.detail.parallelDescription': 'Logic, performance, security, architecture, and extra reviewers work side by side before the judge validates findings.', - 'reviewTeams.detail.parallelLabel': 'Parallel reviewers', - 'reviewTeams.detail.policyMetricsLabel': 'Current policy values', - 'reviewTeams.detail.policyStrategyLabel': 'Strategy', - 'reviewTeams.detail.policySummaryAction': 'Open Review settings to edit the current policy', - 'reviewTeams.detail.policySummaryDescription': '{{strategy}} review uses {{reviewerTimeout}} per reviewer, gives the judge {{judgeTimeout}}, splits each role after {{splitThreshold}}, and caps same-role parallelism at {{maxSameRoleInstances}}.', - 'reviewTeams.detail.policySummaryEyebrow': 'Configured behavior', - 'reviewTeams.detail.policySummaryIntro': 'This live snapshot comes from Review settings and updates when the team policy changes.', - 'reviewTeams.detail.policySummaryTitle': 'Current Policy', - 'reviewTeams.detail.qualityGate': 'Quality gate', - 'reviewTeams.detail.responsibilities': 'Responsibilities', - 'reviewTeams.detail.reviewerTimeout': 'Reviewer timeout', - 'reviewTeams.detail.secondsValue': '{{seconds}}s', - 'reviewTeams.detail.splitDisabled': 'No split', - 'reviewTeams.detail.subtitle': 'Configure the code review team used by Deep Review and /DeepReview.', - 'reviewTeams.detail.summaryDescription': 'Parallel reviewers cover the requested scope, then the quality gate consolidates the final result.', - 'reviewTeams.detail.summaryTitle': 'Team Overview', - 'reviewTeams.detail.title': 'Code Review Team', - 'reviewTeams.detail.warning': 'Use for higher-risk changes; it can take longer and use more tokens than a standard review.', - 'reviewTeams.strategy.deep.label': 'Deep', - 'reviewTeams.strategy.normal.label': 'Normal', - 'reviewTeams.strategy.quick.label': 'Quick', - 'selection.fast': 'Fast', - 'selection.primary': 'Primary', - }; - - return vi.fn((key: string, options?: Record & { defaultValue?: string }) => { - const template = messages[key] ?? options?.defaultValue ?? key; - return template.replace(/{{(\w+)}}/g, (_match, token: string) => String(options?.[token] ?? _match)); - }); -}); - -vi.mock('react-i18next', () => ({ - initReactI18next: { - type: '3rdParty', - init: vi.fn(), - }, - useTranslation: () => ({ - t: tMock, - }), -})); - -vi.mock('@/component-library', () => ({ - Badge: ({ children }: { children: React.ReactNode }) => {children}, - Button: ({ children, onClick }: { children: React.ReactNode; onClick?: () => void }) => ( - - ), - ConfigPageLoading: ({ text }: { text: string }) =>
{text}
, - NumberInput: () => , - Select: () => , -})); - -vi.mock('@/infrastructure/config/components/common', () => ({ - ConfigPageContent: ({ children }: { children: React.ReactNode }) =>
{children}
, - ConfigPageHeader: ({ title, subtitle, extra }: { title: string; subtitle?: string; extra?: React.ReactNode }) => ( -
-

{title}

- {subtitle ?

{subtitle}

: null} - {extra} -
- ), - ConfigPageLayout: ({ children }: { children: React.ReactNode }) =>
{children}
, - ConfigPageRow: ({ children }: { children: React.ReactNode }) =>
{children}
, - ConfigPageSection: ({ children, title }: { children: React.ReactNode; title?: string }) => ( -
- {title ?

{title}

: null} - {children} -
- ), -})); - -vi.mock('@/infrastructure/config/components/ModelSelectionRadio', () => ({ - ModelSelectionRadio: () =>
, -})); - -vi.mock('@/infrastructure/api/service-api/ConfigAPI', () => ({ - configAPI: { - getConfig: vi.fn(async () => []), - }, -})); - -vi.mock('@/infrastructure/config/services/modelConfigs', () => ({ - getModelDisplayName: (model: { name?: string; id?: string; model_name?: string }) => - model.name ?? model.model_name ?? model.id ?? 'model', -})); - -vi.mock('@/infrastructure/api/service-api/SubagentAPI', () => ({ - SubagentAPI: { - listSubagents: vi.fn(async () => []), - listVisibleSubagents: vi.fn(async () => []), - updateSubagentConfig: vi.fn(async () => undefined), - }, -})); - -vi.mock('@/shared/notification-system', () => ({ - useNotification: () => ({ - success: notificationFns.success, - error: notificationFns.error, - warning: notificationFns.warning, - info: notificationFns.info, - }), -})); - -vi.mock('@/infrastructure/contexts/WorkspaceContext', () => ({ - useCurrentWorkspace: () => ({ workspacePath: 'D:/workspace/project' }), -})); - -vi.mock('@/shared/services/reviewTeamService', () => ({ - DEFAULT_REVIEW_TEAM_CONCURRENCY_POLICY: { - maxConcurrentReviewers: 4, - }, - DEFAULT_REVIEW_TEAM_EXECUTION_POLICY: { - reviewerTimeoutSeconds: 1800, - judgeTimeoutSeconds: 1200, - reviewerFileSplitThreshold: 20, - maxSameRoleInstances: 3, - }, - DEFAULT_REVIEW_TEAM_MODEL: 'fast', - REVIEW_TEAM_MEMBER_ACCENT_DEFAULT: '#64748b', - FALLBACK_REVIEW_TEAM_DEFINITION: { - id: 'default-review-team', - name: 'Code Review Team', - members: [], - }, - REVIEW_STRATEGY_DEFINITIONS: { - quick: { label: 'Quick' }, - normal: { label: 'Normal' }, - deep: { label: 'Deep' }, - }, - loadDefaultReviewTeam, -})); - -let JSDOMCtor: (new ( - html?: string, - options?: { pretendToBeVisual?: boolean } -) => { window: Window & typeof globalThis }) | null = null; - -try { - const jsdom = await import('jsdom'); - JSDOMCtor = jsdom.JSDOM as typeof JSDOMCtor; -} catch { - JSDOMCtor = null; -} - -const describeWithJsdom = JSDOMCtor ? describe : describe.skip; - -describeWithJsdom('ReviewTeamPage', () => { - let dom: { window: Window & typeof globalThis }; - let container: HTMLDivElement; - let root: Root; - - beforeEach(() => { - dom = new JSDOMCtor!('', { - pretendToBeVisual: true, - url: 'http://localhost', - }); - - const { window } = dom; - vi.stubGlobal('window', window); - vi.stubGlobal('document', window.document); - vi.stubGlobal('navigator', window.navigator); - vi.stubGlobal('HTMLElement', window.HTMLElement); - vi.stubGlobal('localStorage', window.localStorage); - vi.stubGlobal('IS_REACT_ACT_ENVIRONMENT', true); - - container = document.createElement('div'); - document.body.appendChild(container); - root = createRoot(container); - loadDefaultReviewTeam.mockResolvedValue({ - id: 'default-review-team', - name: 'Code Review Team', - description: '', - warning: 'Review may take longer.', - strategyLevel: 'normal', - memberStrategyOverrides: {}, - executionPolicy: { - reviewerTimeoutSeconds: 1800, - judgeTimeoutSeconds: 1200, - reviewerFileSplitThreshold: 20, - maxSameRoleInstances: 3, - }, - members: [], - coreMembers: [], - extraMembers: [], - }); - }); - - afterEach(() => { - act(() => { - root.unmount(); - }); - container.remove(); - dom.window.close(); - vi.unstubAllGlobals(); - vi.clearAllMocks(); - }); - - async function waitForText(text: string, timeoutMs = 2_000) { - const deadline = Date.now() + timeoutMs; - while (Date.now() < deadline) { - await act(async () => { - await new Promise(resolve => setTimeout(resolve, 10)); - }); - if (container.textContent?.includes(text)) return; - } - throw new Error(`waitForText: "${text}" not found after ${timeoutMs}ms`); - } - - const jsdomTestTimeoutMs = 10_000; - - it('loads review team data only once on initial render', async () => { - const { default: ReviewTeamPage } = await import('./ReviewTeamPage'); - - await act(async () => { - root.render(); - }); - await waitForText('Team Overview'); - - expect(loadDefaultReviewTeam).toHaveBeenCalledTimes(1); - }, jsdomTestTimeoutMs); - - it('renders a read-only team overview with a settings entry point', async () => { - const { default: ReviewTeamPage } = await import('./ReviewTeamPage'); - - await act(async () => { - root.render(); - }); - await waitForText('Team Overview'); - - expect(container.textContent).toContain('Team Overview'); - expect(container.textContent).toContain('Current Policy'); - expect(container.textContent).toContain('Review settings'); - expect(container.textContent).toContain('Normal'); - expect(container.textContent).toContain('1800s'); - expect(container.textContent).toContain('20 files'); - expect(container.textContent).toContain('3 max'); - }, jsdomTestTimeoutMs); - - it('opens the review settings tab from the overview page', async () => { - const { useSettingsStore } = await import('@/app/scenes/settings/settingsStore'); - const { useSceneStore } = await import('@/app/stores/sceneStore'); - const { default: ReviewTeamPage } = await import('./ReviewTeamPage'); - useSettingsStore.setState({ activeTab: 'basics' }); - useSceneStore.setState({ activeTabId: 'session' }); - - await act(async () => { - root.render(); - }); - await waitForText('Team Overview'); - - const settingsButton = Array.from(container.querySelectorAll('button')) - .find((button) => button.textContent?.includes('Review settings')); - expect(settingsButton).toBeTruthy(); - - await act(async () => { - settingsButton!.dispatchEvent(new dom.window.MouseEvent('click', { bubbles: true })); - await Promise.resolve(); - }); - - expect(useSettingsStore.getState().activeTab).toBe('review'); - expect(useSceneStore.getState().activeTabId).toBe('settings'); - }, jsdomTestTimeoutMs); - - it('opens review settings from the current policy summary', async () => { - const { useSettingsStore } = await import('@/app/scenes/settings/settingsStore'); - const { useSceneStore } = await import('@/app/stores/sceneStore'); - const { default: ReviewTeamPage } = await import('./ReviewTeamPage'); - useSettingsStore.setState({ activeTab: 'basics' }); - useSceneStore.setState({ activeTabId: 'session' }); - - await act(async () => { - root.render(); - }); - await waitForText('Team Overview'); - - const policyPanel = container.querySelector('.review-team-page__policy-panel'); - expect(policyPanel).toBeTruthy(); - - await act(async () => { - policyPanel!.dispatchEvent(new dom.window.MouseEvent('click', { bubbles: true })); - await Promise.resolve(); - }); - - expect(useSettingsStore.getState().activeTab).toBe('review'); - expect(useSceneStore.getState().activeTabId).toBe('settings'); - }, jsdomTestTimeoutMs); - - it('keeps rendering after selecting a review team member with missing optional fields', async () => { - loadDefaultReviewTeam.mockResolvedValue({ - id: 'default-review-team', - name: 'Code Review Team', - description: '', - warning: 'Review may take longer.', - strategyLevel: 'normal', - memberStrategyOverrides: {}, - executionPolicy: { - reviewerTimeoutSeconds: 1800, - judgeTimeoutSeconds: 1200, - reviewerFileSplitThreshold: 20, - maxSameRoleInstances: 3, - }, - members: [ - { - id: 'core:review-business-logic', - subagentId: 'review-business-logic', - definitionKey: 'businessLogic', - displayName: 'Logic reviewer', - roleName: 'Logic', - description: 'Checks behavior.', - responsibilities: undefined, - model: 'fast', - configuredModel: 'fast', - strategyOverride: 'inherit', - strategyLevel: 'normal', - strategySource: 'team', - enabled: true, - available: true, - locked: true, - source: 'core', - subagentSource: undefined, - accentColor: undefined, - }, - ], - coreMembers: [], - extraMembers: [], - }); - const { default: ReviewTeamPage } = await import('./ReviewTeamPage'); - - await act(async () => { - root.render(); - }); - await waitForText('Logic'); - - const memberButton = Array.from(container.querySelectorAll('button')) - .find((button) => button.textContent?.includes('Logic')); - expect(memberButton).toBeTruthy(); - - await act(async () => { - memberButton!.dispatchEvent(new dom.window.MouseEvent('click', { bubbles: true })); - }); - - expect(container.textContent).toContain('Responsibilities'); - expect(container.textContent).toContain('Logic'); - }, jsdomTestTimeoutMs); -}); diff --git a/src/web-ui/src/app/scenes/agents/components/ReviewTeamPage.tsx b/src/web-ui/src/app/scenes/agents/components/ReviewTeamPage.tsx deleted file mode 100644 index 270f796f6..000000000 --- a/src/web-ui/src/app/scenes/agents/components/ReviewTeamPage.tsx +++ /dev/null @@ -1,565 +0,0 @@ -import React, { Component, useCallback, useEffect, useMemo, useState } from 'react'; -import { - ArrowLeft, - BadgeCheck, - Blocks, - Bot, - Gauge, - GitBranch, - Layout, - Lock, - Settings, - Shield, - Users, -} from 'lucide-react'; -import { useTranslation } from 'react-i18next'; -import { Badge, Button, ConfigPageLoading } from '@/component-library'; -import { - ConfigPageContent, - ConfigPageHeader, - ConfigPageLayout, - ConfigPageSection, -} from '@/infrastructure/config/components/common'; -import type { AIModelConfig } from '@/infrastructure/config/types'; -import { getModelDisplayName } from '@/infrastructure/config/services/modelConfigs'; -import { configAPI } from '@/infrastructure/api/service-api/ConfigAPI'; - -import { useNotification } from '@/shared/notification-system'; -import { createLogger } from '@/shared/utils/logger'; -import { useCurrentWorkspace } from '@/infrastructure/contexts/WorkspaceContext'; -import { useSettingsStore } from '@/app/scenes/settings/settingsStore'; -import { useSceneStore } from '@/app/stores/sceneStore'; -import { useAgentsStore } from '../agentsStore'; -import { - DEFAULT_REVIEW_TEAM_CONCURRENCY_POLICY, - DEFAULT_REVIEW_TEAM_EXECUTION_POLICY, - REVIEW_TEAM_MEMBER_ACCENT_DEFAULT, - DEFAULT_REVIEW_TEAM_MODEL, - FALLBACK_REVIEW_TEAM_DEFINITION, - loadDefaultReviewTeam, - type ReviewStrategyLevel, - type ReviewTeam, - type ReviewTeamMember, -} from '@/shared/services/reviewTeamService'; -import '../AgentsView.scss'; -import './AgentTeamCard.scss'; -import './ReviewTeamPage.scss'; - -const rtLog = createLogger('ReviewTeamPage'); -const DEFAULT_MEMBER_ACCENT = REVIEW_TEAM_MEMBER_ACCENT_DEFAULT; - -function getMemberIcon(member: ReviewTeamMember) { - switch (member.definitionKey) { - case 'businessLogic': - return GitBranch; - case 'performance': - return Gauge; - case 'security': - return Shield; - case 'architecture': - return Blocks; - case 'frontend': - return Layout; - case 'judge': - return BadgeCheck; - default: - return Bot; - } -} - -function getMemberResponsibilities(member: ReviewTeamMember): string[] { - return Array.isArray(member.responsibilities) ? member.responsibilities : []; -} - -interface ReviewTeamErrorBoundaryProps { - children: React.ReactNode; -} - -interface ReviewTeamErrorBoundaryState { - hasError: boolean; - error: Error | null; -} - -class ReviewTeamErrorBoundary extends Component { - constructor(props: ReviewTeamErrorBoundaryProps) { - super(props); - this.state = { hasError: false, error: null }; - } - - static getDerivedStateFromError(error: Error): ReviewTeamErrorBoundaryState { - return { hasError: true, error }; - } - - componentDidCatch(error: Error, errorInfo: React.ErrorInfo) { - rtLog.error('ReviewTeamPage render error', { - message: error.message, - stack: error.stack, - componentStack: errorInfo.componentStack, - }); - } - - render() { - if (!this.state.hasError) { - return this.props.children; - } - - return ( - - - -
-              {import.meta.env.DEV ? this.state.error?.stack : null}
-            
-
-
-
- ); - } -} - -const ReviewTeamPage: React.FC = () => { - const { t } = useTranslation('scenes/agents'); - const { t: tModel } = useTranslation('settings/default-model'); - const { openHome } = useAgentsStore(); - const setSettingsTab = useSettingsStore((state) => state.setActiveTab); - const openScene = useSceneStore((state) => state.openScene); - const { workspacePath } = useCurrentWorkspace(); - const { error: notifyError } = useNotification(); - - const [loading, setLoading] = useState(true); - const [team, setTeam] = useState(null); - const [models, setModels] = useState([]); - const [selectedMemberId, setSelectedMemberId] = useState(null); - - const loadData = useCallback(async () => { - setLoading(true); - rtLog.info('loadData started', { workspacePath }); - try { - const [loadedTeam, loadedModels] = await Promise.all([ - loadDefaultReviewTeam(workspacePath || undefined), - configAPI.getConfig('ai.models'), - ]); - - rtLog.info('loadData succeeded', { - teamId: loadedTeam.id, - memberCount: loadedTeam.members.length, - strategyLevel: loadedTeam.strategyLevel, - }); - - setTeam(loadedTeam); - setModels(Array.isArray(loadedModels) ? loadedModels as AIModelConfig[] : []); - setSelectedMemberId((currentId) => - currentId && loadedTeam.members.some((member) => member.id === currentId) - ? currentId - : loadedTeam.members[0]?.id ?? null, - ); - } catch (error) { - rtLog.error('loadData failed', { - message: error instanceof Error ? error.message : String(error), - stack: error instanceof Error ? error.stack : undefined, - }); - setTeam({ - id: 'default-review-team', - name: 'Code Review Team', - description: '', - warning: t('reviewTeams.detail.warning'), - strategyLevel: 'normal', - memberStrategyOverrides: {}, - executionPolicy: { ...DEFAULT_REVIEW_TEAM_EXECUTION_POLICY }, - concurrencyPolicy: { ...DEFAULT_REVIEW_TEAM_CONCURRENCY_POLICY }, - definition: FALLBACK_REVIEW_TEAM_DEFINITION, - members: [], - coreMembers: [], - extraMembers: [], - }); - setModels([]); - notifyError(error instanceof Error ? error.message : String(error)); - } finally { - setLoading(false); - } - }, [notifyError, t, workspacePath]); - - useEffect(() => { - void loadData(); - }, [loadData]); - - const selectedMember = useMemo(() => { - if (!team) return null; - return team.members.find((member) => member.id === selectedMemberId) ?? team.members[0] ?? null; - }, [selectedMemberId, team]); - - const getLocalizedMemberName = useCallback((member: ReviewTeamMember): string => { - if (!member.definitionKey) return member.displayName; - return t(`reviewTeams.members.${member.definitionKey}.role`, { - defaultValue: member.roleName, - }); - }, [t]); - - const getLocalizedMemberDescription = useCallback((member: ReviewTeamMember): string => { - if (!member.definitionKey) { - return t('reviewTeams.extraReviewer.description', { defaultValue: member.description }); - } - return t(`reviewTeams.members.${member.definitionKey}.description`, { - defaultValue: member.description, - }); - }, [t]); - - const getLocalizedResponsibilities = useCallback((member: ReviewTeamMember): string[] => { - const baseKey = member.definitionKey - ? `reviewTeams.members.${member.definitionKey}.responsibilities` - : 'reviewTeams.extraReviewer.responsibilities'; - - return getMemberResponsibilities(member).map((item, index) => - t(`${baseKey}.${index}`, { defaultValue: item }), - ); - }, [t]); - - const getStrategyLabel = useCallback((level: ReviewStrategyLevel): string => { - return t(`reviewTeams.strategy.${level}.label`); - }, [t]); - - const formatPolicySeconds = useCallback((seconds: number): string => { - if (seconds <= 0) { - return t('reviewTeams.detail.noTimeout'); - } - return t('reviewTeams.detail.secondsValue', { - seconds - }); - }, [t]); - - const formatSplitThreshold = useCallback((count: number): string => { - if (count <= 0) { - return t('reviewTeams.detail.splitDisabled'); - } - return t('reviewTeams.detail.fileCountValue', { - count - }); - }, [t]); - - const formatModelLabel = useCallback((modelId: string): string => { - if (!modelId || modelId === DEFAULT_REVIEW_TEAM_MODEL) { - return tModel('selection.fast'); - } - if (modelId === 'primary') { - return tModel('selection.primary'); - } - - const match = models.find((model) => model.id === modelId); - return match ? getModelDisplayName(match) : modelId; - }, [models, tModel]); - - const reviewTeamCoreMemberNames = useMemo( - () => (team?.coreMembers ?? []).map((member) => - member.definitionKey - ? t(`reviewTeams.members.${member.definitionKey}.role`, { - defaultValue: member.roleName, - }) - : member.displayName, - ), - [team?.coreMembers, t], - ); - - const reviewTeamMembersLabel = useMemo( - () => - team - ? t('reviewTeams.default.members', { - count: team.members.length - }) - : '', - [team, t], - ); - - const openReviewSettings = useCallback(() => { - setSettingsTab('review'); - openScene('settings'); - }, [openScene, setSettingsTab]); - - if (loading || !team) { - return ( - - - - ); - } - - - const policy = team.executionPolicy; - const strategyLabel = getStrategyLabel(team.strategyLevel); - const reviewerTimeoutLabel = formatPolicySeconds(policy.reviewerTimeoutSeconds); - const judgeTimeoutLabel = formatPolicySeconds(policy.judgeTimeoutSeconds); - const splitThresholdLabel = formatSplitThreshold(policy.reviewerFileSplitThreshold); - const sameRoleInstancesLabel = t('reviewTeams.detail.instancesValue', { - count: policy.maxSameRoleInstances - }); - const policySummaryDescription = t('reviewTeams.detail.policySummaryDescription', { - strategy: strategyLabel, - reviewerTimeout: reviewerTimeoutLabel, - judgeTimeout: judgeTimeoutLabel, - splitThreshold: splitThresholdLabel, - maxSameRoleInstances: sameRoleInstancesLabel, - }); - - return ( - - - - -
- )} - /> - - - -
-
- - - {reviewTeamMembersLabel} - - - - {t('reviewTeams.detail.localOnly')} - - - - {t('reviewTeams.detail.qualityGate')} - -
-
-
-
-
- - - - - {t('reviewTeams.detail.localOnly')} - -
-

- {t('reviewTeams.detail.localOnlyDescription')} -

-
-
-
- - - - - {t('reviewTeams.detail.parallelLabel')} - -
-

- {t('reviewTeams.detail.parallelDescription')} -

-
-
-
- - - - - {t('reviewTeams.detail.qualityGate')} - -
-

- {t('reviewTeams.detail.warning', { defaultValue: team.warning })} -

-
-
-
- - - - {t('reviewTeams.detail.openSettings')} - - )} - > - - - - - - {t('reviewTeams.detail.lockedCount', { - count: team.coreMembers.length - })} - - - {t('reviewTeams.detail.extraCount', { - count: team.extraMembers.length - })} - -
- )} - > -
-
- {team.members.map((member) => { - const MemberIcon = getMemberIcon(member); - const isSelected = selectedMember?.id === member.id; - - return ( - - ); - })} -
- - {selectedMember ? ( -
-
-
- {(() => { - const DetailIcon = getMemberIcon(selectedMember); - return ; - })()} -
-
-
-
-

- {getLocalizedMemberName(selectedMember)} -

-

- {selectedMember.subagentId} -

-
-
- {formatModelLabel(selectedMember.model)} - - {getStrategyLabel(selectedMember.strategyLevel)} - - {selectedMember.locked ? ( - - {t('reviewTeams.detail.memberTypes.core')} - - ) : null} -
-
-
-
-

- {getLocalizedMemberDescription(selectedMember)} -

- -
- - {t('reviewTeams.detail.responsibilities')} - -
    - {getLocalizedResponsibilities(selectedMember).map((item, index) => ( -
  • - {item} -
  • - ))} -
-
-
- ) : null} -
- - - - ); -}; - -export { ReviewTeamErrorBoundary }; -export default ReviewTeamPage; diff --git a/src/web-ui/src/app/scenes/agents/hooks/useAgentsList.ts b/src/web-ui/src/app/scenes/agents/hooks/useAgentsList.ts index 13c6bfada..6e58362d4 100644 --- a/src/web-ui/src/app/scenes/agents/hooks/useAgentsList.ts +++ b/src/web-ui/src/app/scenes/agents/hooks/useAgentsList.ts @@ -9,7 +9,7 @@ import { useNotification } from '@/shared/notification-system'; import type { DynamicToolInfo } from '@/shared/types/agent-api'; import type { AgentWithCapabilities } from '../agentsStore'; import { enrichCapabilities } from '../utils'; -import { STATIC_HIDDEN_AGENT_IDS, isAgentInOverviewZone } from '../agentVisibility'; +import { HIDDEN_AGENT_IDS, isAgentInOverviewZone } from '../agentVisibility'; import { useCurrentWorkspace } from '@/infrastructure/contexts/WorkspaceContext'; import { loadDefaultReviewTeamDefinition } from '@/shared/services/reviewTeamService'; import { globalEventBus } from '@/infrastructure/event-bus'; @@ -107,7 +107,7 @@ export function useAgentsList({ const [modeConfigs, setModeConfigs] = useState>({}); const [modeManageableSubagents, setModeManageableSubagents] = useState>({}); const [hiddenAgentIds, setHiddenAgentIds] = useState>( - () => new Set(STATIC_HIDDEN_AGENT_IDS), + () => new Set(HIDDEN_AGENT_IDS), ); const loadRequestIdRef = useRef(0); @@ -201,7 +201,7 @@ export function useAgentsList({ setModeConfigs(buildModeConfigsByProfile(modes, configs as Record)); setModeManageableSubagents(manageableSubagentsByProfile); setHiddenAgentIds(new Set([ - ...STATIC_HIDDEN_AGENT_IDS, + ...HIDDEN_AGENT_IDS, ...(reviewTeamDefinition?.hiddenAgentIds ?? []), ])); } finally { diff --git a/src/web-ui/src/app/scenes/settings/settingsConfig.ts b/src/web-ui/src/app/scenes/settings/settingsConfig.ts index ccab78f20..dc3c6eac5 100644 --- a/src/web-ui/src/app/scenes/settings/settingsConfig.ts +++ b/src/web-ui/src/app/scenes/settings/settingsConfig.ts @@ -187,10 +187,12 @@ export const SETTINGS_CATEGORIES: ConfigCategoryDef[] = [ keywords: [ 'review', 'code review', - 'deep review', - 'review team', - 'subagent', - 'readonly', + 'strict review', + 'review coverage', + 'review strategy', + 'capacity', + 'cost', + 'latency', 'audit', ], }, diff --git a/src/web-ui/src/flow_chat/components/ChatInput.tsx b/src/web-ui/src/flow_chat/components/ChatInput.tsx index b084f0948..a160b7655 100644 --- a/src/web-ui/src/flow_chat/components/ChatInput.tsx +++ b/src/web-ui/src/flow_chat/components/ChatInput.tsx @@ -49,7 +49,6 @@ import { useThreadGoalController } from '../hooks/useThreadGoalController'; import { ThreadGoalDialogs } from './thread-goal/ThreadGoalDialogs'; import { FlowChatManager } from '@/flow_chat/services/FlowChatManager'; import { - DEEP_REVIEW_SLASH_COMMAND, getDeepReviewLaunchErrorMessage, buildDeepReviewLaunchFromSlashCommand, buildDeepReviewPreviewFromSlashCommand, @@ -1828,12 +1827,6 @@ export const ChatInput: React.FC = ({ command: '/usage', label: t('chatInput.usageAction'), }, - { - kind: 'action', - id: 'deepreview', - command: DEEP_REVIEW_SLASH_COMMAND, - label: t('chatInput.deepreviewAction'), - }, ...(canUseSkillsForTarget ? [{ kind: 'action' as const, @@ -2057,7 +2050,7 @@ export const ChatInput: React.FC = ({ // Only show the picker for "/..." patterns that are plausibly a command (/ or /b... /d...). // Once the user types a space (starts composing the real question), stop showing the picker - // so Enter can submit "/btw ..." or "/DeepReview ..." instead of selecting from the picker. + // so Enter can submit "/btw ..." or "/review strict ..." instead of selecting from the picker. if (pickerQuery !== null && (query === '' || query.startsWith('b') || query.startsWith('d') || query.startsWith('g') || query.startsWith('u'))) { setSlashCommandState({ isActive: true, @@ -2538,7 +2531,7 @@ export const ChatInput: React.FC = ({ }); dispatchInput({ type: 'DEACTIVATE' }); } catch (error) { - log.error('Failed to trigger /DeepReview', { + log.error('Failed to trigger Review: Strict', { error, sessionId: effectiveTargetSessionId, }); @@ -2987,8 +2980,6 @@ export const ChatInput: React.FC = ({ next = '/usage'; } else if (actionId === 'init') { next = '/init'; - } else if (actionId === 'deepreview') { - next = `${DEEP_REVIEW_SLASH_COMMAND} `; } else if (actionId === 'reload-skills') { // /reload-skills takes no arguments. Setting the value to the bare // command lets the user immediately press Enter to dispatch it diff --git a/src/web-ui/src/flow_chat/components/DeepReviewConsentDialog.scss b/src/web-ui/src/flow_chat/components/DeepReviewConsentDialog.scss index 8828c1975..8b22f947b 100644 --- a/src/web-ui/src/flow_chat/components/DeepReviewConsentDialog.scss +++ b/src/web-ui/src/flow_chat/components/DeepReviewConsentDialog.scss @@ -64,6 +64,14 @@ text-transform: uppercase; } +.deep-review-consent__body { + margin: 8px 0 0; + max-width: 640px; + color: var(--color-text-secondary); + font-size: var(--tool-card-action-font-size); + line-height: var(--flowchat-support-line-height); +} + .deep-review-consent__close { display: inline-flex; align-items: center; @@ -154,6 +162,67 @@ color: color-mix(in srgb, var(--deep-review-warm) 78%, var(--color-text-primary)); } +.deep-review-consent__impact-grid { + display: grid; + grid-template-columns: repeat(3, minmax(0, 1fr)); + gap: 10px; + padding: 10px 0 2px; + + div { + min-width: 0; + } + + span, + strong { + display: block; + min-width: 0; + overflow-wrap: anywhere; + } + + span { + color: var(--color-text-muted); + font-size: var(--flowchat-font-size-xs); + font-weight: 700; + line-height: var(--flowchat-compact-line-height); + } + + strong { + margin-top: 3px; + color: var(--color-text-primary); + font-size: var(--tool-card-action-font-size); + font-weight: 680; + line-height: var(--flowchat-support-line-height); + } +} + +.deep-review-consent__token-estimate { + display: grid; + gap: 3px; + padding: 9px 10px; + border: 1px solid color-mix(in srgb, var(--deep-review-accent) 18%, var(--border-subtle)); + border-radius: 6px; + background: color-mix(in srgb, var(--deep-review-accent) 6%, var(--color-bg-elevated)); + + strong, + span { + min-width: 0; + overflow-wrap: anywhere; + } + + strong { + color: var(--color-text-primary); + font-size: var(--tool-card-action-font-size); + font-weight: 720; + line-height: var(--flowchat-compact-line-height); + } + + span { + color: var(--color-text-secondary); + font-size: var(--flowchat-font-size-xs); + line-height: var(--flowchat-support-line-height); + } +} + .deep-review-consent__reviewer-group { display: flex; flex-direction: column; @@ -195,121 +264,6 @@ } } -.deep-review-consent__strategy-options { - display: grid; - grid-template-columns: repeat(3, minmax(0, 1fr)); - gap: 0; - overflow: hidden; - border: 1px solid var(--border-subtle); - border-radius: 6px; - background: color-mix(in srgb, var(--color-bg-elevated) 88%, transparent); -} - -.deep-review-consent__strategy-option { - position: relative; - display: flex; - min-width: 0; - min-height: 56px; - flex-direction: column; - gap: 5px; - align-items: stretch; - padding: 9px 10px; - border: 0; - border-right: 1px solid var(--border-subtle); - border-radius: 0; - background: transparent; - color: color-mix(in srgb, var(--color-text-secondary) 72%, var(--color-text-primary)); - cursor: pointer; - text-align: left; - font-size: var(--tool-card-action-font-size); - line-height: var(--flowchat-compact-line-height); - transition: - background 160ms ease, - border-color 160ms ease, - color 160ms ease, - box-shadow 160ms ease, - transform 160ms ease; - - &:hover { - background: color-mix(in srgb, var(--deep-review-accent) 9%, var(--color-bg-elevated)); - color: var(--color-text-primary); - } - - &:last-child { - border-right: 0; - } - - &:active { - transform: translateY(1px); - } -} - -.deep-review-consent__strategy-option--active { - background: - linear-gradient(180deg, color-mix(in srgb, var(--deep-review-accent) 22%, transparent), transparent), - color-mix(in srgb, var(--deep-review-accent) 14%, var(--color-bg-elevated)); - box-shadow: - inset 0 0 0 1px color-mix(in srgb, var(--deep-review-accent-strong) 68%, var(--border-base)), - 0 6px 18px color-mix(in srgb, var(--deep-review-accent) 12%, transparent); - color: var(--color-text-primary); -} - -.deep-review-consent__strategy-option--active::before { - content: ''; - position: absolute; - top: 0; - right: 10px; - left: 10px; - height: 2px; - border-radius: 0 0 999px 999px; - background: var(--deep-review-accent); -} - -.deep-review-consent__strategy-option--active .deep-review-consent__strategy-option-label { - color: var(--color-text-primary); -} - -.deep-review-consent__strategy-option--active .deep-review-consent__strategy-option-meta { - color: var(--color-text-primary); -} - -.deep-review-consent__strategy-option-header { - display: flex; - min-width: 0; - align-items: center; - justify-content: space-between; - gap: 8px; -} - -.deep-review-consent__strategy-option-label { - min-width: 0; - color: var(--color-text-primary); - font-size: var(--tool-card-action-font-size); - font-weight: 780; - overflow-wrap: anywhere; -} - -.deep-review-consent__strategy-option-badge { - flex-shrink: 0; - padding: 2px 7px; - border-radius: 999px; - color: var(--color-bg-primary); - background: color-mix(in srgb, var(--deep-review-accent-strong) 86%, var(--color-text-primary)); - font-size: 10.5px; - font-weight: 780; -} - -.deep-review-consent__strategy-option-meta { - display: flex; - flex-direction: column; - gap: 3px; - margin-top: auto; - color: color-mix(in srgb, var(--color-text-muted) 62%, var(--color-text-primary)); - font-size: var(--flowchat-font-size-xs); - font-weight: 680; - line-height: var(--flowchat-compact-line-height); -} - .deep-review-consent__reviewer-group-title { display: inline-flex; align-items: center; @@ -390,26 +344,12 @@ li.deep-review-consent__skipped-more { .deep-review-consent__footer { display: flex; align-items: center; - justify-content: space-between; + justify-content: flex-end; gap: 16px; padding-top: 16px; border-top: 1px solid var(--border-subtle); } -.deep-review-consent__checkbox { - min-width: 0; - color: var(--color-text-secondary); - - .bitfun-checkbox__label { - color: var(--color-text-primary); - } - - .bitfun-checkbox__box { - background: color-mix(in srgb, var(--color-bg-elevated) 92%, transparent); - border-color: color-mix(in srgb, var(--border-base) 90%, var(--deep-review-accent)); - } -} - .deep-review-consent__actions { display: flex; justify-content: flex-end; @@ -435,19 +375,10 @@ li.deep-review-consent__skipped-more { grid-template-columns: 1fr; } - .deep-review-consent__strategy-options { + .deep-review-consent__impact-grid { grid-template-columns: 1fr; } - .deep-review-consent__strategy-option { - border-right: 0; - border-bottom: 1px solid var(--border-subtle); - - &:last-child { - border-bottom: 0; - } - } - .deep-review-consent__footer { flex-direction: column; align-items: stretch; diff --git a/src/web-ui/src/flow_chat/components/DeepReviewConsentDialog.test.tsx b/src/web-ui/src/flow_chat/components/DeepReviewConsentDialog.test.tsx index dd8926447..077b743e6 100644 --- a/src/web-ui/src/flow_chat/components/DeepReviewConsentDialog.test.tsx +++ b/src/web-ui/src/flow_chat/components/DeepReviewConsentDialog.test.tsx @@ -5,11 +5,13 @@ import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'; import { useDeepReviewConsent } from './DeepReviewConsentDialog'; import type { ReviewTeamRunManifest } from '@/shared/services/reviewTeamService'; -const mockSaveReviewTeamProjectStrategyOverride = vi.hoisted(() => vi.fn()); - vi.mock('react-i18next', async () => { const { createTestI18nT } = await import('@/test/i18nTestUtils'); return { + initReactI18next: { + type: '3rdParty', + init: vi.fn(), + }, useTranslation: () => ({ t: createTestI18nT('flow-chat'), }), @@ -51,16 +53,6 @@ vi.mock('@/component-library', () => ({ }) => (isOpen ?
{children}
: null), })); -vi.mock('@/shared/services/reviewTeamService', async (importOriginal) => { - const actual = await importOriginal(); - return { - ...actual, - saveReviewTeamProjectStrategyOverride: ( - ...args: Parameters - ) => mockSaveReviewTeamProjectStrategyOverride(...args), - }; -}); - let JSDOMCtor: (new ( html?: string, options?: { pretendToBeVisual?: boolean; url?: string } @@ -127,7 +119,7 @@ function buildPreview(): ReviewTeamRunManifest { strategyRecommendation: { strategyLevel: 'deep', score: 24, - rationale: 'Large/high-risk change (8 files, 900 lines; 2 security-sensitive files, 3 workspace areas). Deep review recommended.', + rationale: 'Large/high-risk change (8 files, 900 lines; 2 security-sensitive files, 3 workspace areas). Strict review recommended.', factors: { fileCount: 8, totalLinesChanged: 900, @@ -148,6 +140,9 @@ function buildPreview(): ReviewTeamRunManifest { estimatedReviewerCalls: 3, maxReviewerCalls: 4, maxExtraReviewers: 1, + maxPromptBytesPerReviewer: 96_000, + estimatedPromptBytesPerReviewer: 16_000, + promptByteEstimateSource: 'manifest_heuristic', largeDiffSummaryFirst: false, skippedReviewerIds: [], warnings: [], @@ -246,7 +241,6 @@ describeWithJsdom('DeepReviewConsentDialog', () => { let root: Root; beforeEach(() => { - mockSaveReviewTeamProjectStrategyOverride.mockResolvedValue(undefined); dom = new JSDOMCtor!('', { pretendToBeVisual: true, url: 'http://localhost', @@ -287,16 +281,20 @@ describeWithJsdom('DeepReviewConsentDialog', () => { expect(container.textContent).toContain('Launch summary'); expect(container.textContent).toContain('1 file'); - expect(container.textContent).toContain('2 skipped'); - expect(container.textContent).toContain('Run strategy: Normal'); + expect(container.textContent).toContain('2 optional checks not needed'); + expect(container.textContent).toContain('BitFun selected the most relevant checks for this target.'); + expect(container.textContent).toContain('Estimated input:'); + expect(container.textContent).toContain('Approximate prompt input only'); + expect(container.textContent).toContain('Run strategy: Standard'); + expect(container.textContent).not.toContain('Do not show this again'); expect(container.textContent).not.toContain('Risk areas: Backend core'); expect(container.textContent).not.toContain('3 reviewer calls'); expect(container.textContent).not.toContain('1 extra specialist'); expect(container.textContent).not.toContain('Review depth: Risk-expanded'); - expect(container.textContent).toContain('Frontend reviewer'); - expect(container.textContent).toContain('Not applicable to this target'); - expect(container.textContent).toContain('Custom invalid reviewer'); - expect(container.textContent).toContain('Configuration issue'); + expect(container.textContent).not.toContain('Frontend reviewer'); + expect(container.textContent).not.toContain('Not applicable to this target'); + expect(container.textContent).not.toContain('Custom invalid reviewer'); + expect(container.textContent).not.toContain('Configuration issue'); expect(container.textContent).not.toContain('Logic reviewer'); expect(container.textContent).not.toContain('Custom security reviewer'); }); @@ -371,11 +369,11 @@ describeWithJsdom('DeepReviewConsentDialog', () => { expect(container.querySelector('[role="dialog"]')).not.toBeNull(); expect(container.textContent).toContain('Active session is busy'); - expect(container.textContent).toContain('2 running subagent tasks'); + expect(container.textContent).toContain('2 Review tasks running'); expect(result).not.toHaveBeenCalled(); }); - it('persists a selected project strategy override before confirming', async () => { + it('keeps strategy selection out of the launch confirmation', async () => { const result = vi.fn(); await act(async () => { @@ -385,23 +383,14 @@ describeWithJsdom('DeepReviewConsentDialog', () => { container.querySelector('button')?.dispatchEvent(new window.Event('click', { bubbles: true })); }); - const deepStrategyButton = Array.from(container.querySelectorAll('button')) - .find((button) => button.textContent?.includes('Deep')); - expect(deepStrategyButton).not.toBeUndefined(); + expect(container.querySelector('.deep-review-consent__strategy-option')).toBeNull(); - await act(async () => { - deepStrategyButton?.dispatchEvent(new window.Event('click', { bubbles: true })); - }); await act(async () => { Array.from(container.querySelectorAll('button')) - .find((button) => button.textContent === 'Start Deep Review') + .find((button) => button.textContent === 'Start Strict Review') ?.dispatchEvent(new window.Event('click', { bubbles: true })); }); - expect(mockSaveReviewTeamProjectStrategyOverride).toHaveBeenCalledWith( - '/test-fixtures/project-a', - 'deep', - ); expect(result).toHaveBeenCalledWith(true); }); @@ -425,28 +414,16 @@ describeWithJsdom('DeepReviewConsentDialog', () => { expect(container.textContent).not.toContain('Expected cost:'); expect(container.querySelectorAll('.deep-review-consent__strategy-selected-summary')).toHaveLength(0); expect(container.querySelectorAll('.deep-review-consent__strategy-current')).toHaveLength(1); - expect(container.querySelectorAll('.deep-review-consent__strategy-option')).toHaveLength(3); - expect(container.querySelectorAll('.deep-review-consent__strategy-option--active')).toHaveLength(1); + expect(container.querySelectorAll('.deep-review-consent__strategy-option')).toHaveLength(0); + expect(container.querySelectorAll('.deep-review-consent__strategy-option--active')).toHaveLength(0); expect(container.textContent).not.toContain('Team default'); - expect(container.textContent).toContain('Selected'); - expect(container.textContent).toContain('Token: 1x'); - expect(container.textContent).toContain('Time: 1x'); - expect(container.textContent).toContain('Normal stays practical for slower models'); + expect(container.textContent).toContain('Standard balances review coverage with practical evidence'); expect(container.querySelectorAll('.deep-review-consent__strategy-option-summary')).toHaveLength(0); const quickStrategyButton = Array.from(container.querySelectorAll('button')) .find((button) => button.textContent?.includes('Quick')); - expect(quickStrategyButton).not.toBeUndefined(); - - await act(async () => { - quickStrategyButton?.dispatchEvent(new window.Event('click', { bubbles: true })); - }); - - expect(quickStrategyButton?.getAttribute('aria-pressed')).toBe('true'); - expect(quickStrategyButton?.className).toContain('deep-review-consent__strategy-option--active'); - expect(container.textContent).toContain('Run strategy: Quick'); - expect(container.textContent).toContain('Token: 0.4-0.6x'); - expect(container.textContent).toContain('Time: 0.5-0.7x'); - expect(container.textContent).toContain('Quick keeps built-in target-matched reviewers'); + expect(quickStrategyButton).toBeUndefined(); + expect(container.textContent).not.toContain('Run strategy: Quick'); + expect(container.textContent).not.toContain('Quick keeps target-matched checks'); }); }); diff --git a/src/web-ui/src/flow_chat/components/DeepReviewConsentDialog.tsx b/src/web-ui/src/flow_chat/components/DeepReviewConsentDialog.tsx index 73ac98e68..e878a1f98 100644 --- a/src/web-ui/src/flow_chat/components/DeepReviewConsentDialog.tsx +++ b/src/web-ui/src/flow_chat/components/DeepReviewConsentDialog.tsx @@ -1,25 +1,17 @@ import React, { useCallback, useState } from 'react'; import { AlertTriangle, X } from 'lucide-react'; import { useTranslation } from 'react-i18next'; -import { Button, Checkbox, Modal } from '@/component-library'; -import { createLogger } from '@/shared/utils/logger'; +import { Button, Modal } from '@/component-library'; +import { i18nService } from '@/infrastructure/i18n'; import type { ReviewStrategyLevel, - ReviewTeamManifestMember, - ReviewTeamManifestMemberReason, ReviewTeamRunManifest, } from '@/shared/services/reviewTeamService'; -import { - REVIEW_STRATEGY_LEVELS, - getReviewStrategyProfile, - saveReviewTeamProjectStrategyOverride, -} from '@/shared/services/reviewTeamService'; +import { getReviewStrategyProfile } from '@/shared/services/reviewTeamService'; import type { DeepReviewSessionConcurrencyGuard } from '../utils/deepReviewCapacityGuard'; import './DeepReviewConsentDialog.scss'; -const log = createLogger('DeepReviewConsentDialog'); -const SKIP_DEEP_REVIEW_CONFIRMATION_STORAGE_KEY = 'bitfun.deepReview.skipCostConfirmation'; -const MAX_VISIBLE_SKIPPED_REVIEWERS = 3; +const APPROXIMATE_TOKENS_PER_PROMPT_BYTE = 0.25; interface PendingConsent { resolve: (confirmed: boolean) => void; @@ -39,16 +31,27 @@ export interface DeepReviewConsentControls { deepReviewConsentDialog: React.ReactNode; } -function hasSkippedReviewers(preview?: ReviewTeamRunManifest): boolean { - return Boolean(preview?.skippedReviewers?.length); +function estimatePromptTokenCount(promptBytes: number | undefined): number | null { + if (!Number.isFinite(promptBytes) || !promptBytes || promptBytes <= 0) { + return null; + } + return Math.max(1, Math.ceil(promptBytes * APPROXIMATE_TOKENS_PER_PROMPT_BYTE)); } -function hasSessionConcurrencyWarning(launchContext?: DeepReviewConsentLaunchContext): boolean { - return Boolean(launchContext?.sessionConcurrencyGuard?.highActivity); -} +function getReviewPromptTokenRange(preview: ReviewTeamRunManifest): { min: number; max: number } | null { + const perCallEstimate = estimatePromptTokenCount( + preview.tokenBudget.estimatedPromptBytesPerReviewer, + ); + if (!perCallEstimate) { + return null; + } -function getReviewerLabel(member: ReviewTeamManifestMember): string { - return member.displayName || member.subagentId; + const estimatedCalls = Math.max(1, preview.tokenBudget.estimatedReviewerCalls || 1); + const maxCalls = Math.max(estimatedCalls, preview.tokenBudget.maxReviewerCalls || estimatedCalls); + return { + min: perCallEstimate * estimatedCalls, + max: perCallEstimate * maxCalls, + }; } function getReviewTargetFileCount(preview: ReviewTeamRunManifest): number { @@ -110,31 +113,12 @@ function getStrategySummary(strategyLevel: ReviewStrategyLevel, t: ReturnType(null); - const [dontShowAgain, setDontShowAgain] = useState(false); - const [selectedStrategyOverride, setSelectedStrategyOverride] = - useState(null); - const [strategySelectionTouched, setStrategySelectionTouched] = useState(false); const confirmDeepReviewLaunch = useCallback(async ( preview?: ReviewTeamRunManifest, launchContext?: DeepReviewConsentLaunchContext, ) => { - try { - if ( - localStorage.getItem(SKIP_DEEP_REVIEW_CONFIRMATION_STORAGE_KEY) === 'true' && - !hasSkippedReviewers(preview) && - !hasSessionConcurrencyWarning(launchContext) - ) { - return true; - } - } catch (error) { - log.warn('Failed to read Deep Review confirmation preference from local storage', error); - } - return new Promise((resolve) => { - setDontShowAgain(false); - setSelectedStrategyOverride(null); - setStrategySelectionTouched(false); setPendingConsent({ resolve, preview, launchContext }); }); }, []); @@ -145,63 +129,16 @@ export function useDeepReviewConsent(): DeepReviewConsentControls { return; } - if ( - confirmed && - strategySelectionTouched && - pending.preview?.workspacePath - ) { - try { - await saveReviewTeamProjectStrategyOverride( - pending.preview.workspacePath, - selectedStrategyOverride ?? undefined, - ); - } catch (error) { - log.warn('Failed to persist Deep Review project strategy override', error); - } - } - - if (confirmed && dontShowAgain) { - try { - localStorage.setItem(SKIP_DEEP_REVIEW_CONFIRMATION_STORAGE_KEY, 'true'); - } catch (error) { - log.warn('Failed to persist Deep Review confirmation preference to local storage', error); - } - } - setPendingConsent(null); pending.resolve(confirmed); - }, [dontShowAgain, pendingConsent, selectedStrategyOverride, strategySelectionTouched]); - - const selectStrategyOverride = useCallback((strategyLevel: ReviewStrategyLevel | null) => { - setSelectedStrategyOverride(strategyLevel); - setStrategySelectionTouched(true); - }, []); - - const getSkippedReasonLabel = useCallback((reason?: ReviewTeamManifestMemberReason) => { - switch (reason) { - case 'not_applicable': - return t('deepReviewConsent.skippedReasons.notApplicable'); - case 'budget_limited': - return t('deepReviewConsent.skippedReasons.budgetLimited'); - case 'invalid_tooling': - return t('deepReviewConsent.skippedReasons.invalidTooling'); - case 'disabled': - return t('deepReviewConsent.skippedReasons.disabled'); - case 'unavailable': - return t('deepReviewConsent.skippedReasons.unavailable'); - default: - return t('deepReviewConsent.skippedReasons.skipped'); - } - }, [t]); + }, [pendingConsent]); const renderLaunchSummary = useCallback((preview: ReviewTeamRunManifest) => { const skippedReviewers = preview.skippedReviewers; const skippedCount = skippedReviewers.length; - const visibleSkippedReviewers = skippedReviewers.slice(0, MAX_VISIBLE_SKIPPED_REVIEWERS); - const hiddenSkippedCount = Math.max(0, skippedCount - visibleSkippedReviewers.length); - const effectiveStrategy = selectedStrategyOverride ?? preview.strategyLevel; - const selectedStrategyLabel = getStrategyLabel(effectiveStrategy, t); + const selectedStrategyLabel = getStrategyLabel(preview.strategyLevel, t); const targetSummary = getReviewTargetSummary(preview, t); + const tokenRange = getReviewPromptTokenRange(preview); return (
@@ -220,6 +157,32 @@ export function useDeepReviewConsent(): DeepReviewConsentControls { )}
+
+
+ {t('deepReviewConsent.costLabel')} + {t('deepReviewConsent.cost')} +
+
+ {t('deepReviewConsent.timeLabel')} + {t('deepReviewConsent.time')} +
+
+ {t('deepReviewConsent.readonlyLabel')} + {t('deepReviewConsent.readonly')} +
+
+ + {tokenRange && ( +
+ + {t('deepReviewConsent.estimatedTokens', { + min: i18nService.formatNumber(tokenRange.min), + max: i18nService.formatNumber(tokenRange.max), + })} + + {t('deepReviewConsent.estimatedTokensNote')} +
+ )} {preview.workspacePath && (
@@ -229,52 +192,7 @@ export function useDeepReviewConsent(): DeepReviewConsentControls { strategy: selectedStrategyLabel, })} - {getStrategySummary(effectiveStrategy, t)} -
-
- {REVIEW_STRATEGY_LEVELS.map((strategyLevel) => { - const isActive = effectiveStrategy === strategyLevel; - const profile = getReviewStrategyProfile(strategyLevel); - return ( - - ); - })} + {getStrategySummary(preview.strategyLevel, t)}
)} @@ -285,33 +203,16 @@ export function useDeepReviewConsent(): DeepReviewConsentControls { {t('deepReviewConsent.skippedGroupTitle')}
-
    - {visibleSkippedReviewers.map((member) => ( -
  • - {getReviewerLabel(member)} - {getSkippedReasonLabel(member.reason)} -
  • - ))} - {hiddenSkippedCount > 0 && ( -
  • - - {t('deepReviewConsent.skippedMore', { - count: hiddenSkippedCount, - })} - -
  • - )} -
+

+ {t('deepReviewConsent.skippedSummary', { + count: skippedCount, + })} +

)} ); - }, [ - getSkippedReasonLabel, - selectStrategyOverride, - selectedStrategyOverride, - t, - ]); + }, [t]); const deepReviewConsentDialog = pendingConsent ? (

{t('deepReviewConsent.title')}

+

+ {t('deepReviewConsent.body')} +

- {t('toolCards.taskDetailPanel.stopSubagentHint')} + {taskInput?.isReviewCoverageTask + ? t('toolCards.taskDetailPanel.stopReviewWorkHint') + : t('toolCards.taskDetailPanel.stopSubagentHint')} )} diff --git a/src/web-ui/src/flow_chat/components/btw/BtwSessionPanel.tsx b/src/web-ui/src/flow_chat/components/btw/BtwSessionPanel.tsx index fbeea84be..fd73ae088 100644 --- a/src/web-ui/src/flow_chat/components/btw/BtwSessionPanel.tsx +++ b/src/web-ui/src/flow_chat/components/btw/BtwSessionPanel.tsx @@ -114,7 +114,7 @@ export const BtwSessionPanel: React.FC = ({ : 'btw'; const childBadgeLabel = t(`childSession.kinds.${childKind}.short`, { defaultValue: childKind === 'deep_review' - ? 'Deep' + ? 'Strict' : childKind === 'review' ? 'Review' : childKind === 'subagent' diff --git a/src/web-ui/src/flow_chat/components/btw/DeepReviewActionBar.i18n.test.ts b/src/web-ui/src/flow_chat/components/btw/DeepReviewActionBar.i18n.test.ts index 2e9a3776e..6d4de7bd0 100644 --- a/src/web-ui/src/flow_chat/components/btw/DeepReviewActionBar.i18n.test.ts +++ b/src/web-ui/src/flow_chat/components/btw/DeepReviewActionBar.i18n.test.ts @@ -2,9 +2,6 @@ import { describe, expect, it } from 'vitest'; import enFlowChat from '@/locales/en-US/flow-chat.json'; import zhCnFlowChat from '@/locales/zh-CN/flow-chat.json'; import zhTwFlowChat from '@/locales/zh-TW/flow-chat.json'; -import enAgents from '@/locales/en-US/scenes/agents.json'; -import zhCnAgents from '@/locales/zh-CN/scenes/agents.json'; -import zhTwAgents from '@/locales/zh-TW/scenes/agents.json'; const LOCALES = { 'en-US': enFlowChat, @@ -12,12 +9,6 @@ const LOCALES = { 'zh-TW': zhTwFlowChat, }; -const AGENT_LOCALES = { - 'en-US': enAgents, - 'zh-CN': zhCnAgents, - 'zh-TW': zhTwAgents, -}; - const REQUIRED_ACTION_BAR_KEYS = [ 'deepReviewActionBar.minimize', 'deepReviewActionBar.restore', @@ -81,28 +72,17 @@ const REQUIRED_CODE_REVIEW_CARD_KEYS = [ 'toolCards.codeReview.severities.medium', 'toolCards.codeReview.severities.low', 'toolCards.codeReview.severities.info', - 'toolCards.codeReview.reviewerStatuses.completed', - 'toolCards.codeReview.reviewerStatuses.timed_out', - 'toolCards.codeReview.reviewerStatuses.cancelled_by_user', - 'toolCards.codeReview.reviewerStatuses.cancelled', - 'toolCards.codeReview.reviewerStatuses.failed', - 'toolCards.codeReview.reviewerStatuses.skipped', - 'toolCards.codeReview.reviewerStatuses.running', - 'toolCards.codeReview.reviewerStatuses.partial', - 'toolCards.codeReview.reviewerStatuses.unknown', + 'toolCards.codeReview.runManifest.reducedCoverageSummary', ]; const USER_VISIBLE_TEXT_KEYS_MUST_NOT_CONTAIN_ESCAPED_UNICODE = [ 'toolCards.codeReview.remediationActions.fixAndReview', ]; -const REQUIRED_REVIEW_TEAM_PAGE_KEYS = [ - 'reviewTeams.detail.loading', -]; - const REQUIRED_DEEP_REVIEW_CONSENT_KEYS = [ 'deepReviewConsent.sessionConcurrencyTitle', 'deepReviewConsent.sessionConcurrencyBody', + 'deepReviewConsent.skippedSummary', ]; function getMessageValue(messages: unknown, key: string): unknown { @@ -128,7 +108,7 @@ describe('DeepReviewActionBar i18n', () => { } }); - it('keeps review report lineup strings available in every bundled locale', () => { + it('keeps review report coverage strings available in every bundled locale', () => { for (const [locale, messages] of Object.entries(LOCALES)) { const missingKeys = REQUIRED_CODE_REVIEW_CARD_KEYS.filter((key) => { const value = getMessageValue(messages, key); @@ -149,17 +129,6 @@ describe('DeepReviewActionBar i18n', () => { } }); - it('keeps review team page strings available in every bundled locale', () => { - for (const [locale, messages] of Object.entries(AGENT_LOCALES)) { - const missingKeys = REQUIRED_REVIEW_TEAM_PAGE_KEYS.filter((key) => { - const value = getMessageValue(messages, key); - return typeof value !== 'string' || value.trim().length === 0; - }); - - expect(missingKeys, `${locale} missing keys`).toEqual([]); - } - }); - it('keeps Deep Review consent strings available in every bundled locale', () => { for (const [locale, messages] of Object.entries(LOCALES)) { const missingKeys = REQUIRED_DEEP_REVIEW_CONSENT_KEYS.filter((key) => { diff --git a/src/web-ui/src/flow_chat/components/btw/DeepReviewActionBar.test.tsx b/src/web-ui/src/flow_chat/components/btw/DeepReviewActionBar.test.tsx index f5a6c3800..ed9921dbf 100644 --- a/src/web-ui/src/flow_chat/components/btw/DeepReviewActionBar.test.tsx +++ b/src/web-ui/src/flow_chat/components/btw/DeepReviewActionBar.test.tsx @@ -298,8 +298,8 @@ describeWithJsdom('DeepReviewActionBar', () => { expect(sendMessageMock).toHaveBeenCalledTimes(1); const [prompt, sessionId, displayMessage, agentType] = sendMessageMock.mock.calls[0]; - expect(prompt).toContain('selected Code Review findings only'); - expect(prompt).toContain('follow-up standard code review'); + expect(prompt).toContain('selected Review findings only'); + expect(prompt).toContain('follow-up standard review'); expect(sessionId).toBe('review-session'); expect(displayMessage).toBe('Fix Code Review findings and re-review'); expect(agentType).toBe('CodeReview'); @@ -422,9 +422,9 @@ describeWithJsdom('DeepReviewActionBar', () => { root.render(); }); - expect(container.textContent).not.toContain('Reviewers waiting for capacity'); + expect(container.textContent).not.toContain('Review waiting for capacity'); expect(Array.from(container.querySelectorAll('button')).some((button) => ( - button.textContent?.includes('Pause queue') + button.textContent?.includes('Pause waiting') ))).toBe(false); }); @@ -457,14 +457,14 @@ describeWithJsdom('DeepReviewActionBar', () => { expect(container.textContent).toContain('Waiting for model capacity'); expect(container.textContent).toContain('BitFun is waiting for temporary model capacity.'); - expect(container.textContent).toContain('Reason: provider concurrency limit'); + expect(container.textContent).toContain('Reason: model concurrency limit'); expect(container.textContent).toContain('Waited 12s of 1m 0s'); expect(container.textContent).toContain('Your active session is busy.'); expect(container.textContent).not.toContain('Run slower next time'); expect(container.textContent).toContain('Open Review settings'); const pauseButton = Array.from(container.querySelectorAll('button')) - .find((button) => button.textContent?.includes('Pause queue')); + .find((button) => button.textContent?.includes('Pause waiting')); expect(pauseButton).toBeTruthy(); await act(async () => { @@ -475,7 +475,7 @@ describeWithJsdom('DeepReviewActionBar', () => { expect((useReviewActionBarStore.getState() as unknown as { capacityQueueState: { status: string }; }).capacityQueueState.status).toBe('paused_by_user'); - expect(container.textContent).toContain('Queue paused'); + expect(container.textContent).toContain('Review wait paused'); const openSettingsButton = Array.from(container.querySelectorAll('button')) .find((button) => button.textContent?.includes('Open Review settings')); @@ -527,7 +527,7 @@ describeWithJsdom('DeepReviewActionBar', () => { }); const pauseButton = Array.from(container.querySelectorAll('button')) - .find((button) => button.textContent?.includes('Pause queue')); + .find((button) => button.textContent?.includes('Pause waiting')); expect(pauseButton).toBeTruthy(); await act(async () => { @@ -575,7 +575,7 @@ describeWithJsdom('DeepReviewActionBar', () => { }); const pauseButton = Array.from(container.querySelectorAll('button')) - .find((button) => button.textContent?.includes('Pause queue')); + .find((button) => button.textContent?.includes('Pause waiting')); expect(pauseButton).toBeTruthy(); await act(async () => { @@ -629,7 +629,7 @@ describeWithJsdom('DeepReviewActionBar', () => { }); const pauseButton = Array.from(container.querySelectorAll('button')) - .find((button) => button.textContent?.includes('Pause queue')); + .find((button) => button.textContent?.includes('Pause waiting')); expect(pauseButton).toBeTruthy(); await act(async () => { @@ -639,7 +639,7 @@ describeWithJsdom('DeepReviewActionBar', () => { expect(controlDeepReviewQueueMock).toHaveBeenCalledTimes(2); expect(notificationService.error).toHaveBeenCalledWith( - expect.stringContaining('1 of 2 reviewers failed'), + expect.stringContaining('1 of 2 review items failed'), ); expect(notificationService.error).toHaveBeenCalledWith( expect.stringContaining('tool already running'), @@ -649,7 +649,7 @@ describeWithJsdom('DeepReviewActionBar', () => { }).capacityQueueState.status).toBe('queued_for_capacity'); }); - it('starts a structured retry turn for explicit incomplete Deep Review slices', async () => { + it('starts a structured retry turn for explicit incomplete strict review coverage', async () => { flowChatSessionsMock.set('deep-review-session', { sessionId: 'deep-review-session', sessionKind: 'deep_review', @@ -712,7 +712,7 @@ describeWithJsdom('DeepReviewActionBar', () => { }); const retryButton = Array.from(container.querySelectorAll('button')) - .find((button) => button.textContent?.includes('Retry incomplete slices')); + .find((button) => button.textContent?.includes('Retry incomplete review work')); expect(retryButton).toBeTruthy(); await act(async () => { @@ -972,13 +972,13 @@ describeWithJsdom('DeepReviewActionBar', () => { expect(buttonTexts.some((text) => text.includes('Continue review'))).toBe(true); expect(buttonTexts.some((text) => text.includes('Switch model'))).toBe(true); - expect(buttonTexts.some((text) => text.includes('Copy diagnostics'))).toBe(true); + expect(buttonTexts.some((text) => text.includes('Copy troubleshooting summary'))).toBe(true); expect(buttonTexts.some((text) => text.includes('Retry'))).toBe(false); expect(buttonTexts.some((text) => text.includes('Show recovery plan'))).toBe(false); expect(container.querySelectorAll('.deep-review-action-bar__attribution button')).toHaveLength(0); expect(container.querySelector('.deep-review-action-bar__attribution-actions')).toBeNull(); - expect(container.textContent).toContain('1 completed reviewers will be preserved'); - expect(container.textContent).toContain('1 reviewers will be rerun'); + expect(container.textContent).toContain('1 completed review results will be preserved'); + expect(container.textContent).toContain('1 review items will be rerun'); }); it('minimizes and hides stale interruption controls after a resume request starts successfully', async () => { diff --git a/src/web-ui/src/flow_chat/components/modern/SessionFilesBadge.test.tsx b/src/web-ui/src/flow_chat/components/modern/SessionFilesBadge.test.tsx index 15878f509..50423c2c4 100644 --- a/src/web-ui/src/flow_chat/components/modern/SessionFilesBadge.test.tsx +++ b/src/web-ui/src/flow_chat/components/modern/SessionFilesBadge.test.tsx @@ -21,9 +21,18 @@ const mocks = vi.hoisted(() => ({ vi.mock('react-i18next', () => ({ useTranslation: () => ({ t: (key: string, options?: Record) => { + const messages: Record = { + 'sessionFilesBadge.actionsButton': 'Actions', + 'sessionFilesBadge.actionsMenuHint': 'Quick actions', + 'sessionFilesBadge.reviewModeStandard': 'Review', + 'sessionFilesBadge.reviewModeDeep': 'Review: Strict', + }; if (key === 'sessionFilesBadge.filesSummaryCount') { return `${String(options?.count ?? 0)} files`; } + if (messages[key]) { + return messages[key]; + } return typeof options?.defaultValue === 'string' ? options.defaultValue : key; }, }), @@ -217,4 +226,26 @@ describe('SessionFilesBadge', () => { expect(container.textContent).toContain('1 files'); expect(container.textContent).not.toContain('stale-session.ts'); }); + + it('presents strict review as a Review option instead of a second Deep Review product', async () => { + await act(async () => { + root.render(); + }); + + await act(async () => { + vi.advanceTimersByTime(350); + await Promise.resolve(); + }); + + const actionsButton = container.querySelector('.session-files-badge__review-btn') as HTMLButtonElement | null; + expect(actionsButton).not.toBeNull(); + + await act(async () => { + actionsButton?.dispatchEvent(new dom.window.MouseEvent('click', { bubbles: true })); + }); + + expect(container.textContent).toContain('Review'); + expect(container.textContent).toContain('Review: Strict'); + expect(container.textContent).not.toContain('Deep review'); + }); }); diff --git a/src/web-ui/src/flow_chat/deep-review/README.md b/src/web-ui/src/flow_chat/deep-review/README.md index 347c47d75..92edc3201 100644 --- a/src/web-ui/src/flow_chat/deep-review/README.md +++ b/src/web-ui/src/flow_chat/deep-review/README.md @@ -1,21 +1,21 @@ -# Flow Chat Deep Review Ownership +# Flow Chat Strict Review Compatibility Ownership -This directory owns Flow Chat integration for Deep Review. Keep the historical import paths under `src/web-ui/src/flow_chat/services`, `components/btw`, and `utils` as compatibility facades. +This directory owns Flow Chat integration for the `Review: Strict` compatibility runtime. Keep the historical `DeepReview` import paths under `src/web-ui/src/flow_chat/services`, `components/btw`, and `utils` as compatibility facades. ## Module Boundaries | Area | Owns | Should not own | |---|---|---| -| `launch/` | Slash command parsing, review target resolution, launch prompt formatting, launch error shaping, child-session launch orchestration. | Review Team manifest policy internals, direct Tauri calls, action-bar state. | -| `action-bar/` | Shared review action bar rendering, Deep Review queue notice, partial-results panel, recovery-plan preview, remediation selection, action controls, diagnostics text building, compact status/header formatting. | Launch target parsing, report markdown semantics, backend queue classification. | +| `launch/` | Slash command parsing, review target resolution, launch prompt formatting, launch error shaping, child-session launch orchestration. | Strict Review manifest policy internals, direct Tauri calls, action-bar state. | +| `action-bar/` | Shared review action bar rendering, strict-review queue notice, partial-results panel, recovery-plan preview, remediation selection, action controls, diagnostics text building, compact status/header formatting. | Launch target parsing, report markdown semantics, backend queue classification. | | `report/` | Code review report types, retryable slice extraction, reliability notices, run-manifest markdown sections, report section normalization, markdown export. | UI rendering, session launch, raw source/diff/model output storage. | ## Guardrails - Preserve the facade exports from `services/DeepReviewService.ts`, `components/btw/DeepReviewActionBar.tsx`, and `utils/codeReviewReport.ts`. -- Keep Deep Review-only UI gated by `reviewMode === 'deep'` or an explicit Deep Review queue/report context. +- Keep strict-review-only UI gated by `reviewMode === 'deep'` or an explicit strict-review queue/report context. - Standard Code Review remediation and markdown export must keep focused regression coverage when report or action-bar helpers move. -- Diagnostics, markdown export, and evidence summaries must stay metadata-first and must not include source text, full diff text, reviewer output, provider raw bodies, or full file contents. +- Diagnostics, markdown export, and evidence summaries must stay metadata-first and must not include source text, full diff text, raw review output, provider raw bodies, or full file contents. - Add behavior to the narrow helper module first. Only grow the orchestration files when the behavior actually coordinates multiple helpers or adapters. ## Focused Verification diff --git a/src/web-ui/src/flow_chat/deep-review/action-bar/CapacityQueueNotice.test.tsx b/src/web-ui/src/flow_chat/deep-review/action-bar/CapacityQueueNotice.test.tsx index b85c946f5..1ac03f7ca 100644 --- a/src/web-ui/src/flow_chat/deep-review/action-bar/CapacityQueueNotice.test.tsx +++ b/src/web-ui/src/flow_chat/deep-review/action-bar/CapacityQueueNotice.test.tsx @@ -44,11 +44,11 @@ describe('CapacityQueueNotice', () => { expect(html).toContain('Waiting for model capacity'); expect(html).toContain('BitFun is waiting for temporary model capacity.'); - expect(html).toContain('Reason: provider concurrency limit'); - expect(html).toContain('The model provider rejected another concurrent reviewer.'); + expect(html).toContain('Reason: model concurrency limit'); + expect(html).toContain('The model provider rejected another concurrent review request.'); expect(html).toContain('Waited 12s of 1m 0s'); - expect(html).toContain('Pause queue'); - expect(html).toContain('Skip optional extras'); + expect(html).toContain('Pause waiting'); + expect(html).toContain('Keep core checks'); expect(html).not.toContain('Run slower next time'); }); @@ -72,14 +72,14 @@ describe('CapacityQueueNotice', () => { />, ); - expect(html).toContain('Reason: previous launch batch still running'); + expect(html).toContain('Reason: earlier review work still running'); expect(html).toContain('Waiting preserves the planned review order'); - expect(html).toContain('Waiting for running reviewers'); - expect(html).toContain('Running reviewers: 2'); + expect(html).toContain('Waiting for review capacity'); + expect(html).not.toContain('Active review work: 2'); expect(html).not.toContain('Waited 4s of 1m 0s'); }); - it('explains launch-batch waits that outlast the configured queue window', () => { + it('explains launch-batch waits that outlast the configured capacity window', () => { const html = renderToStaticMarkup( { />, ); - expect(html).toContain('waited longer than the configured queue window'); - expect(html).toContain('Cancel queued reviewers'); + expect(html).toContain('waited longer than the configured capacity window'); + expect(html).toContain('Cancel waiting review'); expect(html).toContain('Open Review settings'); expect(html).not.toContain('Run slower next time'); expect(html).not.toContain('Waited 1m 30s of 1m 0s'); }); - it('does not show the long launch-batch detail before the queue window is exceeded', () => { + it('does not show the long launch-batch detail before the capacity window is exceeded', () => { const html = renderToStaticMarkup( { />, ); - expect(html).not.toContain('waited longer than the configured queue window'); + expect(html).not.toContain('waited longer than the configured capacity window'); }); it('explains active-reviewer waits without presenting max wait as a hard timeout', () => { @@ -149,13 +149,13 @@ describe('CapacityQueueNotice', () => { />, ); - expect(html).toContain('Waiting for running reviewers'); - expect(html).toContain('Queued reviewers start when a running reviewer frees capacity.'); - expect(html).toContain('Running reviewers: 2'); + expect(html).toContain('Waiting for review capacity'); + expect(html).toContain('Waiting review work starts when active review work frees capacity.'); + expect(html).not.toContain('Active review work: 2'); expect(html).not.toContain('Waited 1m 10s of 1m 0s'); }); - it('renders the specific reviewers currently waiting', () => { + it('keeps waiting review work summarized instead of listing individual reviewers', () => { const html = renderToStaticMarkup( { />, ); - expect(html).toContain('Waiting reviewers'); - expect(html).toContain('Security reviewer'); - expect(html).toContain('Frontend reviewer'); - expect(html).toContain('Paused'); - expect(html).toContain('Waited 9s'); + expect(html).toContain('Review waiting for capacity'); + expect(html).not.toContain('Security reviewer'); + expect(html).not.toContain('Frontend reviewer'); + expect(html).not.toContain('Paused'); + expect(html).not.toContain('Waited 9s'); }); it('renders the stop hint when inline queue controls are unavailable', () => { @@ -213,7 +213,7 @@ describe('CapacityQueueNotice', () => { />, ); - expect(html).toContain('Use Stop to interrupt this review queue.'); - expect(html).not.toContain('Pause queue'); + expect(html).toContain('Use Stop to interrupt this review.'); + expect(html).not.toContain('Pause waiting'); }); }); diff --git a/src/web-ui/src/flow_chat/deep-review/action-bar/CapacityQueueNotice.tsx b/src/web-ui/src/flow_chat/deep-review/action-bar/CapacityQueueNotice.tsx index 9975ce483..e26836edb 100644 --- a/src/web-ui/src/flow_chat/deep-review/action-bar/CapacityQueueNotice.tsx +++ b/src/web-ui/src/flow_chat/deep-review/action-bar/CapacityQueueNotice.tsx @@ -42,23 +42,23 @@ const CAPACITY_QUEUE_REASON_DETAIL_KEYS: Record[number], -): string { - return reviewer.displayName || reviewer.subagentType || 'Reviewer'; -} - export const CapacityQueueNotice: React.FC = ({ capacityQueueState, supportsInlineQueueControls, @@ -138,11 +132,9 @@ export const CapacityQueueNotice: React.FC = ({ : capacityQueueWaitMode === 'provider_capacity' ? t('deepReviewActionBar.capacityQueue.providerDetail') : t('deepReviewActionBar.capacityQueue.detail'); - const waitingReviewers = capacityQueueState.waitingReviewers ?? []; const showCapacityQueueMeta = Boolean( capacityQueueReasonLabel - || capacityQueueElapsedLabel - || capacityQueueWaitMode === 'active_reviewer', + || capacityQueueElapsedLabel, ); return ( @@ -177,13 +169,6 @@ export const CapacityQueueNotice: React.FC = ({ })} )} - {capacityQueueWaitMode === 'active_reviewer' && activeReviewerCount > 0 && ( - - {t('deepReviewActionBar.capacityQueue.activeReviewerCount', { - count: activeReviewerCount, - })} - - )} )} {capacityQueueReasonDetail && ( @@ -201,51 +186,6 @@ export const CapacityQueueNotice: React.FC = ({ {t('deepReviewActionBar.capacityQueue.sessionBusy')} )} - {waitingReviewers.length > 0 && ( -
- - {t('deepReviewActionBar.capacityQueue.waitingReviewersTitle')} - -
- {waitingReviewers.map((reviewer) => { - const label = reviewerLabel(reviewer); - const reviewerElapsed = reviewer.queueElapsedMs !== undefined - ? formatElapsedTime(reviewer.queueElapsedMs) - : null; - const statusLabel = reviewer.status === 'paused_by_user' - ? t('deepReviewActionBar.capacityQueue.reviewerStatusPaused') - : t('deepReviewActionBar.capacityQueue.reviewerStatusQueued'); - return ( - - - {label} - - - {statusLabel} - {reviewer.optional && ( - <> - {' / '} - {t('deepReviewActionBar.capacityQueue.optionalReviewer')} - - )} - {reviewerElapsed && ( - <> - {' / '} - {t('deepReviewActionBar.capacityQueue.elapsed', { - elapsed: reviewerElapsed, - })} - - )} - - - ); - })} -
-
- )} {!supportsInlineQueueControls && ( {t('deepReviewActionBar.capacityQueue.stopHint')} diff --git a/src/web-ui/src/flow_chat/deep-review/action-bar/DeepReviewActionBar.tsx b/src/web-ui/src/flow_chat/deep-review/action-bar/DeepReviewActionBar.tsx index 80511661e..d18fc6502 100644 --- a/src/web-ui/src/flow_chat/deep-review/action-bar/DeepReviewActionBar.tsx +++ b/src/web-ui/src/flow_chat/deep-review/action-bar/DeepReviewActionBar.tsx @@ -431,15 +431,15 @@ export const ReviewActionBar: React.FC = ({ childSessionId ? 'reviewActionBar.fixAndReviewRequestDisplayDeep' : 'reviewActionBar.fixAndReviewRequestDisplayStandard', { defaultValue: isDeepReview - ? 'Fix Deep Review findings and re-review' - : 'Fix Code Review findings and re-review', + ? 'Fix strict review findings and re-review' + : 'Fix review findings and re-review', }) : t(isDeepReview ? 'reviewActionBar.fixRequestDisplayDeep' : 'reviewActionBar.fixRequestDisplayStandard', { defaultValue: isDeepReview - ? 'Start fixing Deep Review findings' - : 'Start fixing Code Review findings', + ? 'Start fixing strict review findings' + : 'Start fixing review findings', }), isDeepReview ? 'DeepReview' : 'CodeReview', 'agentic', @@ -493,7 +493,7 @@ export const ReviewActionBar: React.FC = ({ childSessionId ); store.minimize(childSessionId); } catch (error) { - log.error('Failed to start DeepReview retry slices', { childSessionId, error }); + log.error('Failed to start DeepReview retry coverage', { childSessionId, error }); const message = error instanceof Error ? error.message : t('deepReviewActionBar.retryIncompleteFailed'); @@ -569,7 +569,7 @@ export const ReviewActionBar: React.FC = ({ childSessionId try { await continueDeepReviewSession(interruption, t('deepReviewActionBar.resumeRequestDisplay'), { force: !interruption.canResume }); } catch (error) { - log.error('Failed to continue interrupted Deep Review', { childSessionId, error }); + log.error('Failed to continue interrupted strict review', { childSessionId, error }); const message = t('deepReviewActionBar.resumeFailedMessage'); store.updatePhase('resume_failed', message, childSessionId ?? undefined); store.restore(childSessionId ?? undefined); @@ -655,11 +655,11 @@ export const ReviewActionBar: React.FC = ({ childSessionId switch (phase) { case 'review_running': return t(isDeepReview ? 'deepReviewActionBar.reviewRunningDeep' : 'deepReviewActionBar.reviewRunningStandard', { - defaultValue: isDeepReview ? 'Deep review in progress...' : 'Review in progress...', + defaultValue: isDeepReview ? 'Strict review in progress...' : 'Review in progress...', }); case 'review_completed': return t(isDeepReview ? 'reviewActionBar.reviewCompletedDeep' : 'reviewActionBar.reviewCompletedStandard', { - defaultValue: isDeepReview ? 'Deep review completed' : 'Review completed', + defaultValue: isDeepReview ? 'Strict review completed' : 'Review completed', }); case 'fix_running': if (lastSubmittedAction === 'fix-review') { diff --git a/src/web-ui/src/flow_chat/deep-review/action-bar/ReviewActionControls.test.tsx b/src/web-ui/src/flow_chat/deep-review/action-bar/ReviewActionControls.test.tsx index d600bb83c..c0adb388f 100644 --- a/src/web-ui/src/flow_chat/deep-review/action-bar/ReviewActionControls.test.tsx +++ b/src/web-ui/src/flow_chat/deep-review/action-bar/ReviewActionControls.test.tsx @@ -70,7 +70,7 @@ describe('ReviewActionControls', () => { />, ); - expect(html).toContain('Retry incomplete slices (2)'); + expect(html).toContain('Retry incomplete review work (2)'); expect(html).toContain('Start fixing'); expect(html).toContain('Fix & re-review'); expect(html).toContain('Fill to input'); @@ -104,7 +104,7 @@ describe('ReviewActionControls', () => { expect(html).toContain('Continue review'); expect(html).toContain('Switch model'); - expect(html).toContain('Copy diagnostics'); + expect(html).toContain('Copy troubleshooting summary'); expect(html).toContain('View partial results'); }); diff --git a/src/web-ui/src/flow_chat/deep-review/action-bar/ReviewActionControls.tsx b/src/web-ui/src/flow_chat/deep-review/action-bar/ReviewActionControls.tsx index 254ea73fb..b7b423695 100644 --- a/src/web-ui/src/flow_chat/deep-review/action-bar/ReviewActionControls.tsx +++ b/src/web-ui/src/flow_chat/deep-review/action-bar/ReviewActionControls.tsx @@ -6,7 +6,6 @@ import type { ReviewActionPhase } from '../../store/deepReviewActionBarStore'; import { CodeReviewReportExportActions } from '../../tool-cards/CodeReviewReportExportActions'; type ExportableReviewData = React.ComponentProps['reviewData']; -type ExportableRunManifest = React.ComponentProps['runManifest']; interface ReviewActionControlsProps { phase: ReviewActionPhase; @@ -21,7 +20,6 @@ interface ReviewActionControlsProps { remainingFixIds: string[]; modelRecoveryAction: 'switch_model' | 'open_model_settings' | null; reviewData?: ExportableReviewData | null; - runManifest?: ExportableRunManifest; onRetryIncompleteSlices: () => void | Promise; onStartFixing: (rerunReview: boolean) => void | Promise; onFillBackInput: () => void | Promise; @@ -47,7 +45,6 @@ export const ReviewActionControls: React.FC = ({ remainingFixIds, modelRecoveryAction, reviewData, - runManifest, onRetryIncompleteSlices, onStartFixing, onFillBackInput, @@ -113,7 +110,6 @@ export const ReviewActionControls: React.FC = ({ {phase === 'review_completed' && reviewData && ( diff --git a/src/web-ui/src/flow_chat/deep-review/action-bar/ReviewActionHeader.tsx b/src/web-ui/src/flow_chat/deep-review/action-bar/ReviewActionHeader.tsx index 0718e885d..3822b310b 100644 --- a/src/web-ui/src/flow_chat/deep-review/action-bar/ReviewActionHeader.tsx +++ b/src/web-ui/src/flow_chat/deep-review/action-bar/ReviewActionHeader.tsx @@ -3,7 +3,6 @@ import { Minus } from 'lucide-react'; import { CodeReviewReportExportActions } from '../../tool-cards/CodeReviewReportExportActions'; type ExportableReviewData = React.ComponentProps['reviewData']; -type ExportableRunManifest = React.ComponentProps['runManifest']; interface ReviewActionHeaderProps { reviewData?: ExportableReviewData | null; @@ -15,7 +14,6 @@ interface ReviewActionHeaderProps { phaseIconClass: string; phaseTitle: string; errorMessage?: string | null; - runManifest?: ExportableRunManifest; minimizeLabel: string; onMinimize: () => void; } @@ -26,7 +24,6 @@ export const ReviewActionHeader: React.FC = ({ phaseIconClass, phaseTitle, errorMessage, - runManifest, minimizeLabel, onMinimize, }) => ( @@ -35,7 +32,6 @@ export const ReviewActionHeader: React.FC = ({ {reviewData && ( )} diff --git a/src/web-ui/src/flow_chat/deep-review/action-bar/interruptionDiagnostics.test.ts b/src/web-ui/src/flow_chat/deep-review/action-bar/interruptionDiagnostics.test.ts index 4b2d6bef5..8c741210e 100644 --- a/src/web-ui/src/flow_chat/deep-review/action-bar/interruptionDiagnostics.test.ts +++ b/src/web-ui/src/flow_chat/deep-review/action-bar/interruptionDiagnostics.test.ts @@ -44,7 +44,7 @@ describe('buildInterruptionDiagnostics', () => { t, ); - expect(diagnostics).toContain('=== Deep Review Interruption Diagnostics ==='); + expect(diagnostics).toContain('=== Strict Review Interruption Diagnostics ==='); expect(diagnostics).toContain('Error type: provider_unavailable (provider_unavailable)'); expect(diagnostics).toContain('Suggested actions: wait_and_retry, copy_diagnostics'); expect(diagnostics).toContain(' - provider: anthropic'); @@ -69,9 +69,10 @@ describe('buildInterruptionDiagnostics', () => { t, ); - expect(diagnostics).toContain('=== Deep Review Interruption Diagnostics ==='); + expect(diagnostics).toContain('=== Strict Review Interruption Diagnostics ==='); expect(diagnostics).toContain('Error type: unknown (unknown)'); - expect(diagnostics).toContain('raw message: Conversation execution failed after ReviewSecurity completed.'); + expect(diagnostics).toContain('raw message: Conversation execution failed after Security coverage completed.'); + expect(diagnostics).not.toContain('ReviewSecurity'); expect(diagnostics).not.toBe('category=unknown'); }); }); diff --git a/src/web-ui/src/flow_chat/deep-review/action-bar/interruptionDiagnostics.ts b/src/web-ui/src/flow_chat/deep-review/action-bar/interruptionDiagnostics.ts index b64b69cf9..49136ceab 100644 --- a/src/web-ui/src/flow_chat/deep-review/action-bar/interruptionDiagnostics.ts +++ b/src/web-ui/src/flow_chat/deep-review/action-bar/interruptionDiagnostics.ts @@ -2,11 +2,23 @@ import type { AiErrorDetail, AiErrorPresentation, } from '@/shared/ai-errors/aiErrorPresenter'; +import { + DEFAULT_REVIEW_COVERAGE_SOURCE_LABELS, + resolveReviewCoverageSourceLabelKey, +} from '../report/reviewCoverageSource'; type Translate = (key: string, options?: Record & { defaultValue?: string }) => string; function truncateDiagnosticValue(value: string): string { - return value.length > 500 ? `${value.slice(0, 500)}... [truncated]` : value; + const sanitizedValue = value.replace(/\bReview[A-Za-z0-9_]+\b/g, (source) => { + const labelKey = resolveReviewCoverageSourceLabelKey(source); + return labelKey + ? DEFAULT_REVIEW_COVERAGE_SOURCE_LABELS[labelKey] + : 'review coverage'; + }); + return sanitizedValue.length > 500 + ? `${sanitizedValue.slice(0, 500)}... [truncated]` + : sanitizedValue; } export function buildInterruptionDiagnostics( @@ -19,7 +31,7 @@ export function buildInterruptionDiagnostics( } const lines: string[] = []; - lines.push(t('deepReviewActionBar.diagnosticsTitle', { defaultValue: '=== Deep Review Interruption Diagnostics ===' })); + lines.push(t('deepReviewActionBar.diagnosticsTitle', { defaultValue: '=== Strict Review Interruption Diagnostics ===' })); lines.push(''); const categoryLabel = t(presentation.titleKey, { defaultValue: presentation.category }); diff --git a/src/web-ui/src/flow_chat/deep-review/launch/DeepReviewService.ts b/src/web-ui/src/flow_chat/deep-review/launch/DeepReviewService.ts index 4b5f52dbb..84e483432 100644 --- a/src/web-ui/src/flow_chat/deep-review/launch/DeepReviewService.ts +++ b/src/web-ui/src/flow_chat/deep-review/launch/DeepReviewService.ts @@ -1,7 +1,7 @@ import { agentAPI } from '@/infrastructure/api'; import { createLogger } from '@/shared/utils/logger'; import { createBtwChildSession } from '../../services/BtwThreadService'; -import { closeBtwSessionInAuxPane, openBtwSessionInAuxPane } from '../../services/btwSessionPane'; +import { closeBtwSessionInAuxPane } from '../../services/btwSessionPane'; import { FlowChatManager } from '../../services/FlowChatManager'; import { flowChatStore } from '../../store/FlowChatStore'; import { insertReviewSessionSummaryMarker } from '../../services/ReviewSessionMarkerService'; @@ -72,14 +72,14 @@ async function cleanupFailedDeepReviewLaunch( try { closeBtwSessionInAuxPane(childSessionId); } catch (error) { - const message = `Failed to close the deep review pane during cleanup: ${normalizeErrorMessage(error)}`; + const message = `Failed to close the strict review pane during cleanup: ${normalizeErrorMessage(error)}`; cleanupIssues.push(message); log.warn(message, { childSessionId, launchStep, error }); } let backendSessionRemoved = false; if (!workspacePath) { - const message = 'Workspace path is missing, so backend deep review session cleanup could not run.'; + const message = 'Workspace path is missing, so backend strict review session cleanup could not run.'; cleanupIssues.push(message); log.warn(message, { childSessionId, launchStep }); } else { @@ -95,7 +95,7 @@ async function cleanupFailedDeepReviewLaunch( if (isSessionMissingError(error)) { backendSessionRemoved = true; } else { - const message = `Failed to delete the backend deep review session: ${normalizeErrorMessage(error)}`; + const message = `Failed to delete the backend strict review session: ${normalizeErrorMessage(error)}`; cleanupIssues.push(message); log.warn(message, { childSessionId, launchStep, error }); } @@ -107,7 +107,7 @@ async function cleanupFailedDeepReviewLaunch( const flowChatManager = FlowChatManager.getInstance(); flowChatManager.discardLocalSession(childSessionId); } catch (error) { - const message = `Failed to remove the local deep review session state: ${normalizeErrorMessage(error)}`; + const message = `Failed to remove the local strict review session state: ${normalizeErrorMessage(error)}`; cleanupIssues.push(message); log.warn(message, { childSessionId, launchStep, error }); } @@ -126,12 +126,12 @@ async function buildReviewTeamManifestWithRuntimeSignals( const manifestOptions = options ?? {}; const [rateLimitStatus, strategyOverride] = await Promise.all([ loadReviewTeamRateLimitStatus().catch((error) => { - log.warn('Failed to load Deep Review rate limit status', { error }); + log.warn('Failed to load strict review rate limit status', { error }); return null; }), manifestOptions.workspacePath ? loadReviewTeamProjectStrategyOverride(manifestOptions.workspacePath).catch((error) => { - log.warn('Failed to load Deep Review project strategy override', { error }); + log.warn('Failed to load strict review project strategy override', { error }); return undefined; }) : Promise.resolve(undefined), @@ -285,7 +285,7 @@ export async function launchDeepReviewSession({ workspacePath, prompt, displayMessage, - childSessionName = 'Deep review', + childSessionName = 'Review: Strict', requestedFiles = [], runManifest, }: LaunchDeepReviewSessionParams): Promise<{ childSessionId: string }> { @@ -308,14 +308,6 @@ export async function launchDeepReviewSession({ }); childSessionId = created.childSessionId; - launchStep = 'open_aux_pane'; - openBtwSessionInAuxPane({ - childSessionId, - parentSessionId, - workspacePath, - expand: true, - }); - launchStep = 'send_start_message'; const flowChatManager = FlowChatManager.getInstance(); if (runManifest) { @@ -362,7 +354,7 @@ export async function launchDeepReviewSession({ cleanupResult, ); - log.error('Deep review launch failed', { + log.error('Strict review launch failed', { parentSessionId, childSessionId, launchStep, diff --git a/src/web-ui/src/flow_chat/deep-review/launch/commandParser.test.ts b/src/web-ui/src/flow_chat/deep-review/launch/commandParser.test.ts index e9ef396d0..6ae61d21f 100644 --- a/src/web-ui/src/flow_chat/deep-review/launch/commandParser.test.ts +++ b/src/web-ui/src/flow_chat/deep-review/launch/commandParser.test.ts @@ -10,18 +10,22 @@ import { } from './commandParser'; describe('Deep Review launch command parser', () => { - it('recognizes only the canonical slash command', () => { - expect(DEEP_REVIEW_SLASH_COMMAND).toBe('/DeepReview'); + it('recognizes strict Review typed commands and compatibility aliases', () => { + expect(DEEP_REVIEW_SLASH_COMMAND).toBe('/review strict'); + expect(isDeepReviewSlashCommand('/review strict')).toBe(true); + expect(isDeepReviewSlashCommand('/review strict commit abc123')).toBe(true); + expect(isDeepReviewSlashCommand('/review deep commit abc123')).toBe(false); expect(isDeepReviewSlashCommand('/DeepReview')).toBe(true); expect(isDeepReviewSlashCommand('/DeepReview review commit abc123')).toBe(true); - expect(isDeepReviewSlashCommand('/deepreview review commit abc123')).toBe(false); + expect(isDeepReviewSlashCommand('/deepreview review commit abc123')).toBe(true); + expect(isDeepReviewSlashCommand('/review')).toBe(false); expect(isDeepReviewSlashCommand('/DeepReviewer review commit abc123')).toBe(false); }); it('strips the canonical command before target parsing', () => { - expect(getDeepReviewCommandFocus('/DeepReview review commit abc123')).toBe( - 'review commit abc123', - ); + expect(getDeepReviewCommandFocus('/review strict commit abc123')).toBe('commit abc123'); + expect(getDeepReviewCommandFocus('/DeepReview review commit abc123')).toBe('review commit abc123'); + expect(getDeepReviewCommandFocus('/deepreview review commit abc123')).toBe('review commit abc123'); expect(getDeepReviewCommandFocus('/DeepReview')).toBe(''); }); diff --git a/src/web-ui/src/flow_chat/deep-review/launch/commandParser.ts b/src/web-ui/src/flow_chat/deep-review/launch/commandParser.ts index a7703faf6..8bde05812 100644 --- a/src/web-ui/src/flow_chat/deep-review/launch/commandParser.ts +++ b/src/web-ui/src/flow_chat/deep-review/launch/commandParser.ts @@ -4,9 +4,14 @@ import type { GitStatus, } from '@/infrastructure/api/service-api/GitAPI'; import { normalizeReviewPath } from '@/shared/services/reviewTargetClassifier'; -import { DEEP_REVIEW_COMMAND_RE } from '../../utils/deepReviewConstants'; +import { + DEEP_REVIEW_COMMAND_RE, + DEEP_REVIEW_COMPAT_COMMAND_PREFIX_RE, + REVIEW_STRICT_COMMAND_PREFIX_RE, + REVIEW_STRICT_SLASH_COMMAND, +} from '../../utils/deepReviewConstants'; -export const DEEP_REVIEW_SLASH_COMMAND = '/DeepReview'; +export const DEEP_REVIEW_SLASH_COMMAND = REVIEW_STRICT_SLASH_COMMAND; const EXPLICIT_REVIEW_FILE_EXTENSIONS = new Set([ '.ts', @@ -28,7 +33,11 @@ export function isDeepReviewSlashCommand(commandText: string): boolean { } export function getDeepReviewCommandFocus(commandText: string): string { - return commandText.trim().replace(/^\/DeepReview\b/, '').trim(); + return commandText + .trim() + .replace(REVIEW_STRICT_COMMAND_PREFIX_RE, '') + .replace(DEEP_REVIEW_COMPAT_COMMAND_PREFIX_RE, '') + .trim(); } function cleanPotentialFileToken(token: string): string { diff --git a/src/web-ui/src/flow_chat/deep-review/launch/launchErrors.test.ts b/src/web-ui/src/flow_chat/deep-review/launch/launchErrors.test.ts index 1aa8d29ff..dac158544 100644 --- a/src/web-ui/src/flow_chat/deep-review/launch/launchErrors.test.ts +++ b/src/web-ui/src/flow_chat/deep-review/launch/launchErrors.test.ts @@ -12,7 +12,7 @@ describe('Deep Review launch errors', () => { it('normalizes empty and string errors', () => { expect(normalizeErrorMessage(' network down ')).toBe('network down'); expect(normalizeErrorMessage(new Error(' model missing '))).toBe('model missing'); - expect(normalizeErrorMessage(null)).toBe('Deep review failed to start'); + expect(normalizeErrorMessage(null)).toBe('Strict review failed to start'); }); it('recognizes missing-session cleanup failures as non-fatal', () => { @@ -29,7 +29,7 @@ describe('Deep Review launch errors', () => { { cleanupCompleted: true, cleanupIssues: [] }, ); - expect(error.message).toBe('Network connection was interrupted before Deep Review could start.'); + expect(error.message).toBe('Network connection was interrupted before strict review could start.'); expect(error.launchErrorMessageKey).toBe('deepReviewActionBar.launchError.network'); expect(error.launchErrorCategory).toBe('network'); expect(error.childSessionId).toBe('child-123'); @@ -39,7 +39,7 @@ describe('Deep Review launch errors', () => { }); it('keeps original launch error when cleanup completed', () => { - const original = new Error('Pane open failed'); + const original = new Error('Session preparation failed'); const cleanupResult: FailedDeepReviewCleanupResult = { cleanupCompleted: true, cleanupIssues: [], @@ -62,8 +62,8 @@ describe('Deep Review launch errors', () => { expect(buildLaunchCleanupError( 'open_aux_pane', 'child-123', - new Error('Pane open failed'), + new Error('Session preparation failed'), cleanupResult, - ).message).toContain('The partially created deep review session (child-123) may need manual cleanup.'); + ).message).toContain('The partially created strict review session (child-123) may need manual cleanup.'); }); }); diff --git a/src/web-ui/src/flow_chat/deep-review/launch/launchErrors.ts b/src/web-ui/src/flow_chat/deep-review/launch/launchErrors.ts index 6d9f6f746..945a9cbfa 100644 --- a/src/web-ui/src/flow_chat/deep-review/launch/launchErrors.ts +++ b/src/web-ui/src/flow_chat/deep-review/launch/launchErrors.ts @@ -22,9 +22,9 @@ export interface DeepReviewLaunchError extends Error { } const LAUNCH_ERROR_DEFAULT_MESSAGES: Record = { - 'deepReviewActionBar.launchError.modelConfig': 'Deep review could not create a review session. Check the model configuration.', - 'deepReviewActionBar.launchError.network': 'Network connection was interrupted before Deep Review could start.', - 'deepReviewActionBar.launchError.unknown': 'Deep review failed to start. Please try again.', + 'deepReviewActionBar.launchError.modelConfig': 'Strict review could not create a review session. Check the model configuration.', + 'deepReviewActionBar.launchError.network': 'Network connection was interrupted before strict review could start.', + 'deepReviewActionBar.launchError.unknown': 'Strict review failed to start. Please try again.', }; export function normalizeErrorMessage(error: unknown): string { @@ -36,7 +36,7 @@ export function normalizeErrorMessage(error: unknown): string { return error.trim(); } - return 'Deep review failed to start'; + return 'Strict review failed to start'; } export function isSessionMissingError(error: unknown): boolean { @@ -47,13 +47,13 @@ export function isSessionMissingError(error: unknown): boolean { function describeLaunchStep(step: DeepReviewLaunchStep): string { switch (step) { case 'create_child_session': - return 'creating the deep review session'; + return 'creating the strict review session'; case 'open_aux_pane': - return 'opening the deep review pane'; + return 'preparing the strict review session'; case 'send_start_message': - return 'starting the deep review run'; + return 'starting the strict review run'; default: - return 'launching deep review'; + return 'launching strict review'; } } @@ -118,6 +118,6 @@ export function buildLaunchCleanupError( const cleanupSummary = cleanupResult.cleanupIssues.join(' '); return new Error( `${originalMessage} Cleanup was incomplete after failure while ${describeLaunchStep(launchStep)}. ` + - `The partially created deep review session (${childSessionId}) may need manual cleanup. ${cleanupSummary}`.trim(), + `The partially created strict review session (${childSessionId}) may need manual cleanup. ${cleanupSummary}`.trim(), ); } diff --git a/src/web-ui/src/flow_chat/deep-review/launch/launchPrompt.ts b/src/web-ui/src/flow_chat/deep-review/launch/launchPrompt.ts index 258fd77e7..da92bafca 100644 --- a/src/web-ui/src/flow_chat/deep-review/launch/launchPrompt.ts +++ b/src/web-ui/src/flow_chat/deep-review/launch/launchPrompt.ts @@ -31,7 +31,7 @@ export function formatSessionFilesLaunchPrompt({ : 'User-provided focus:\nNone.'; return [ - 'Run a deep code review using the parallel Code Review Team.', + 'Run a strict code review using the assigned read-only Review execution plan.', 'Review scope: ONLY inspect the following files modified in this session.', formatFileList(filePaths), contextBlock, @@ -54,7 +54,7 @@ export function formatPullRequestLaunchPrompt({ : 'Pull request provider diff:\nNo provider diff was included. Confirm findings against the listed files and PR metadata.'; return [ - 'Run a deep code review using the parallel Code Review Team.', + 'Run a strict code review using the assigned read-only Review execution plan.', 'Review scope: ONLY inspect the following files changed by this pull request.', formatFileList(filePaths), contextBlock, @@ -74,7 +74,7 @@ export function formatSlashCommandLaunchPrompt({ : 'User-provided focus or target:\nNone. If no explicit target is given, review the current workspace changes relative to HEAD.'; return [ - 'Run a deep code review using the parallel Code Review Team.', + 'Run a strict code review using the assigned read-only Review execution plan.', 'Interpret the user command below to determine the review target.', 'If the user mentions a commit, ref, branch, or explicit file set, review that target.', 'Otherwise, review the current workspace changes relative to HEAD.', diff --git a/src/web-ui/src/flow_chat/deep-review/report/codeReviewReport.ts b/src/web-ui/src/flow_chat/deep-review/report/codeReviewReport.ts index 28fd5e00e..c32891dd6 100644 --- a/src/web-ui/src/flow_chat/deep-review/report/codeReviewReport.ts +++ b/src/web-ui/src/flow_chat/deep-review/report/codeReviewReport.ts @@ -1,4 +1,5 @@ import type { ReviewTeamRunManifest } from '@/shared/services/reviewTeamService'; +import type { ReviewCoverageSourceLabelKey } from './reviewCoverageSource'; export { buildCodeReviewReliabilityNotices } from './reliabilityNotices'; export { @@ -9,6 +10,12 @@ export { DEFAULT_CODE_REVIEW_MARKDOWN_LABELS, formatCodeReviewReportMarkdown, } from './markdown'; +export { + DEFAULT_REVIEW_COVERAGE_SOURCE_LABELS, + formatReviewCoverageSource, + resolveReviewCoverageSourceLabelKey, +} from './reviewCoverageSource'; +export type { ReviewCoverageSourceLabelKey } from './reviewCoverageSource'; export type ReviewRiskLevel = 'low' | 'medium' | 'high' | 'critical'; export type ReviewAction = 'approve' | 'approve_with_suggestions' | 'request_changes' | 'block'; @@ -21,8 +28,6 @@ export type ReviewSectionId = | 'issues' | 'remediation' | 'strengths' - | 'runManifest' - | 'team' | 'coverage'; export type RemediationGroupId = 'must_fix' | 'should_improve' | 'needs_decision' | 'verification'; export type StrengthGroupId = @@ -178,30 +183,20 @@ export interface CodeReviewReportMarkdownLabels { titleDeep: string; executiveSummary: string; reviewDecision: string; - runManifest: string; riskLevel: string; recommendedAction: string; scope: string; - target: string; - budget: string; - estimatedCalls: string; - activeReviewers: string; - skippedReviewers: string; issues: string; noIssues: string; remediationPlan: string; strengths: string; - reviewTeam: string; reliabilitySignals: string; coverageNotes: string; - status: string; - packet: string; - partialOutput: string; - findings: string; validation: string; suggestion: string; source: string; noItems: string; + coverageSourceLabels: Record; groupTitles: Record; reliabilityNoticeLabels: Record; } @@ -413,7 +408,7 @@ export function buildDeepReviewRetryPrompt(slices: DeepReviewRetryableSlice[]): })); return [ - 'Retry only the listed incomplete Deep Review slices in this same session.', + 'Retry only the listed incomplete strict review coverage in this same session.', 'Use the Task tool once for each retry task below. Do not retry files outside retry_scope_files.', 'After these retry tasks finish, run ReviewJudge and submit an updated code review report with honest coverage notes.', '', diff --git a/src/web-ui/src/flow_chat/deep-review/report/manifestSections.test.ts b/src/web-ui/src/flow_chat/deep-review/report/manifestSections.test.ts deleted file mode 100644 index 7a0db7d10..000000000 --- a/src/web-ui/src/flow_chat/deep-review/report/manifestSections.test.ts +++ /dev/null @@ -1,172 +0,0 @@ -import { describe, expect, it } from 'vitest'; -import type { - ReviewTeamManifestMember, - ReviewTeamRunManifest, -} from '@/shared/services/reviewTeamService'; -import { DEFAULT_CODE_REVIEW_MARKDOWN_LABELS } from './codeReviewReport'; -import { formatRunManifestMarkdownSection } from './manifestSections'; - -function manifestMember( - subagentId: string, - displayName: string, - reason?: ReviewTeamManifestMember['reason'], -): ReviewTeamManifestMember { - return { - subagentId, - displayName, - roleName: displayName, - model: 'fast', - configuredModel: 'fast', - defaultModelSlot: 'fast', - strategyLevel: 'normal', - strategySource: 'team', - strategyDirective: 'Review the target.', - locked: !subagentId.startsWith('Custom'), - source: subagentId.startsWith('Custom') ? 'extra' : 'core', - subagentSource: subagentId.startsWith('Custom') ? 'user' : 'builtin', - ...(reason ? { reason } : {}), - }; -} - -function buildRunManifest(): ReviewTeamRunManifest { - return { - reviewMode: 'deep', - workspacePath: '/test-fixtures/project-a', - policySource: 'default-review-team-config', - target: { - source: 'session_files', - resolution: 'resolved', - tags: ['frontend'], - files: ['src/App.tsx'], - warnings: [], - }, - strategyLevel: 'normal', - strategyRecommendation: { - strategyLevel: 'deep', - score: 24, - rationale: 'Large/high-risk change.', - factors: { - fileCount: 8, - totalLinesChanged: 900, - lineCountSource: 'diff_stat', - securityFileCount: 2, - workspaceAreaCount: 3, - contractSurfaceChanged: true, - }, - }, - executionPolicy: { - reviewerTimeoutSeconds: 1800, - judgeTimeoutSeconds: 1200, - reviewerFileSplitThreshold: 20, - maxSameRoleInstances: 3, - maxRetriesPerRole: 1, - }, - concurrencyPolicy: { - maxParallelInstances: 4, - staggerSeconds: 0, - maxQueueWaitSeconds: 1200, - batchExtrasSeparately: true, - allowProviderCapacityQueue: true, - allowBoundedAutoRetry: false, - autoRetryElapsedGuardSeconds: 180, - }, - preReviewSummary: { - source: 'target_manifest', - summary: '1 file, 12 changed lines across 1 workspace area: web-ui (1)', - fileCount: 1, - excludedFileCount: 0, - lineCount: 12, - lineCountSource: 'diff_stat', - targetTags: ['frontend'], - workspaceAreas: [ - { - key: 'web-ui', - fileCount: 1, - sampleFiles: ['src/App.tsx'], - }, - ], - warnings: [], - }, - sharedContextCache: { - source: 'work_packets', - strategy: 'reuse_readonly_file_context_by_cache_key', - entries: [ - { - cacheKey: 'shared-context:1', - path: 'src/App.tsx', - workspaceArea: 'web-ui', - recommendedTools: ['GetFileDiff', 'Read'], - consumerPacketIds: [ - 'reviewer:ReviewBusinessLogic', - 'reviewer:CustomSecurity', - ], - }, - ], - omittedEntryCount: 0, - }, - incrementalReviewCache: { - source: 'target_manifest', - strategy: 'reuse_completed_packets_when_fingerprint_matches', - cacheKey: 'incremental-review:abc12345', - fingerprint: 'abc12345', - filePaths: ['src/App.tsx'], - workspaceAreas: ['web-ui'], - targetTags: ['frontend'], - reviewerPacketIds: [ - 'reviewer:ReviewBusinessLogic', - 'reviewer:CustomSecurity', - ], - lineCount: 12, - lineCountSource: 'diff_stat', - invalidatesOn: [ - 'target_file_set_changed', - 'target_line_count_changed', - 'reviewer_roster_changed', - ], - }, - tokenBudget: { - mode: 'balanced', - estimatedReviewerCalls: 3, - maxReviewerCalls: 4, - maxExtraReviewers: 1, - largeDiffSummaryFirst: false, - skippedReviewerIds: ['CustomInvalid'], - warnings: [], - }, - coreReviewers: [ - manifestMember('ReviewBusinessLogic', 'Logic reviewer'), - ], - qualityGateReviewer: manifestMember('ReviewJudge', 'Quality inspector'), - enabledExtraReviewers: [ - manifestMember('CustomSecurity', 'Custom security reviewer'), - ], - skippedReviewers: [ - manifestMember('ReviewFrontend', 'Frontend reviewer', 'not_applicable'), - manifestMember('CustomInvalid', 'Custom invalid reviewer', 'invalid_tooling'), - ], - }; -} - -describe('manifestSections', () => { - it('formats Deep Review manifest markdown without content payload fields', () => { - const markdown = formatRunManifestMarkdownSection( - buildRunManifest(), - DEFAULT_CODE_REVIEW_MARKDOWN_LABELS, - ); - - expect(markdown).toContain('## Run manifest'); - expect(markdown).toContain('- Target: frontend'); - expect(markdown).toContain('- Logic reviewer (ReviewBusinessLogic)'); - expect(markdown).toContain('- Quality inspector (ReviewJudge)'); - expect(markdown).toContain('- Custom invalid reviewer (CustomInvalid): invalid_tooling'); - expect(markdown).toContain('### Shared context cache'); - expect(markdown).toContain( - '- shared-context:1: src/App.tsx -> reviewer:ReviewBusinessLogic, reviewer:CustomSecurity', - ); - expect(markdown).not.toContain('source_text'); - expect(markdown).not.toContain('full_diff'); - expect(markdown).not.toContain('model_output'); - expect(markdown).not.toContain('provider_raw_body'); - expect(markdown).not.toContain('full_file_contents'); - }); -}); diff --git a/src/web-ui/src/flow_chat/deep-review/report/manifestSections.ts b/src/web-ui/src/flow_chat/deep-review/report/manifestSections.ts deleted file mode 100644 index fc95ff538..000000000 --- a/src/web-ui/src/flow_chat/deep-review/report/manifestSections.ts +++ /dev/null @@ -1,172 +0,0 @@ -import { - getActiveReviewTeamManifestMembers, - type ReviewTeamManifestMember, - type ReviewTeamRunManifest, -} from '@/shared/services/reviewTeamService'; -import type { CodeReviewReportMarkdownLabels } from './codeReviewReport'; - -function pushList(lines: string[], items: string[], emptyLabel: string): void { - if (items.length === 0) { - lines.push(`- ${emptyLabel}`); - return; - } - - for (const item of items) { - lines.push(`- ${item}`); - } -} - -function manifestTarget(manifest: ReviewTeamRunManifest): string { - return manifest.target.tags.length > 0 - ? manifest.target.tags.join(', ') - : manifest.target.source; -} - -function manifestMemberLabel(member: ReviewTeamManifestMember): string { - return member.displayName || member.subagentId; -} - -function manifestMemberLine(member: ReviewTeamManifestMember): string { - return `${manifestMemberLabel(member)} (${member.subagentId})`; -} - -function pluralize(count: number, singular: string): string { - return `${count} ${singular}${count === 1 ? '' : 's'}`; -} - -function pushPreReviewSummarySection( - lines: string[], - manifest: ReviewTeamRunManifest, -): void { - const summary = manifest.preReviewSummary; - if (!summary) { - return; - } - - lines.push(`### Pre-review summary`); - lines.push(`- ${summary.summary}`); - lines.push(`- Files: ${summary.fileCount}`); - if (summary.lineCount !== undefined) { - lines.push(`- Lines changed: ${summary.lineCount} (${summary.lineCountSource})`); - } else { - lines.push(`- Lines changed: unknown (${summary.lineCountSource})`); - } - if (summary.workspaceAreas.length > 0) { - for (const area of summary.workspaceAreas) { - const sampleFiles = area.sampleFiles.length > 0 - ? ` (${area.sampleFiles.join(', ')})` - : ''; - lines.push(`- ${area.key}: ${pluralize(area.fileCount, 'file')}${sampleFiles}`); - } - } - lines.push(''); -} - -function pushEvidencePackSection( - lines: string[], - manifest: ReviewTeamRunManifest, -): void { - const pack = manifest.evidencePack; - if (!pack) { - return; - } - - lines.push(`### Evidence pack`); - lines.push(`- Source: ${pack.source}; privacy: ${pack.privacy.content}`); - lines.push( - `- Changed files: ${pack.changedFiles.length}; hunk hints: ${pack.hunkHints.length}; contract hints: ${pack.contractHints.length}; packet ids: ${pack.packetIds.length}`, - ); - lines.push( - `- Omitted metadata: changed files ${pack.budget.omittedChangedFileCount}, hunk hints ${pack.budget.omittedHunkHintCount}, contract hints ${pack.budget.omittedContractHintCount}`, - ); - lines.push('- Hints are orientation only and require tool confirmation before findings.'); - lines.push(''); -} - -function pushSharedContextCacheSection( - lines: string[], - manifest: ReviewTeamRunManifest, -): void { - const cachePlan = manifest.sharedContextCache; - if (!cachePlan) { - return; - } - - lines.push(`### Shared context cache`); - if (cachePlan.entries.length === 0) { - lines.push('- None.'); - } else { - for (const entry of cachePlan.entries) { - lines.push( - `- ${entry.cacheKey}: ${entry.path} -> ${entry.consumerPacketIds.join(', ')}`, - ); - } - } - if (cachePlan.omittedEntryCount > 0) { - lines.push(`- Omitted entries: ${cachePlan.omittedEntryCount}`); - } - lines.push(''); -} - -function pushIncrementalReviewCacheSection( - lines: string[], - manifest: ReviewTeamRunManifest, -): void { - const cachePlan = manifest.incrementalReviewCache; - if (!cachePlan) { - return; - } - - lines.push(`### Incremental review cache`); - lines.push(`- Cache key: ${cachePlan.cacheKey}`); - lines.push(`- Fingerprint: ${cachePlan.fingerprint}`); - lines.push(`- Strategy: ${cachePlan.strategy}`); - lines.push(`- Reviewer packets: ${cachePlan.reviewerPacketIds.join(', ') || 'none'}`); - lines.push(`- Invalidates on: ${cachePlan.invalidatesOn.join(', ') || 'none'}`); - lines.push(''); -} - -export function formatRunManifestMarkdownSection( - manifest: ReviewTeamRunManifest, - labels: CodeReviewReportMarkdownLabels, -): string { - const lines: string[] = []; - const activeReviewers = getActiveReviewTeamManifestMembers(manifest); - - lines.push(`## ${labels.runManifest}`); - lines.push(`- ${labels.target}: ${manifestTarget(manifest)}`); - lines.push(`- ${labels.budget}: ${manifest.tokenBudget.mode}`); - lines.push(`- ${labels.estimatedCalls}: ${manifest.tokenBudget.estimatedReviewerCalls}`); - if (manifest.scopeProfile) { - lines.push(`- Review depth: ${manifest.scopeProfile.reviewDepth}`); - lines.push(`- Coverage expectation: ${manifest.scopeProfile.coverageExpectation}`); - } - if (manifest.strategyRecommendation) { - lines.push(`- Recommended strategy: ${manifest.strategyRecommendation.strategyLevel}`); - lines.push(`- Recommendation score: ${manifest.strategyRecommendation.score}`); - lines.push(`- Recommendation rationale: ${manifest.strategyRecommendation.rationale}`); - } - lines.push(''); - lines.push(`### ${labels.activeReviewers}`); - pushList( - lines, - activeReviewers.map((member) => manifestMemberLine(member)), - labels.noItems, - ); - lines.push(''); - lines.push(`### ${labels.skippedReviewers}`); - pushList( - lines, - manifest.skippedReviewers.map((member) => - `${manifestMemberLine(member)}: ${member.reason ?? 'skipped'}`, - ), - labels.noItems, - ); - lines.push(''); - pushPreReviewSummarySection(lines, manifest); - pushEvidencePackSection(lines, manifest); - pushSharedContextCacheSection(lines, manifest); - pushIncrementalReviewCacheSection(lines, manifest); - - return lines.join('\n').trimEnd(); -} diff --git a/src/web-ui/src/flow_chat/deep-review/report/markdown.test.ts b/src/web-ui/src/flow_chat/deep-review/report/markdown.test.ts index 820e4493c..592046a8e 100644 --- a/src/web-ui/src/flow_chat/deep-review/report/markdown.test.ts +++ b/src/web-ui/src/flow_chat/deep-review/report/markdown.test.ts @@ -16,6 +16,26 @@ describe('markdown', () => { expect(markdown).toContain('# Code Review Report'); expect(markdown).toContain('## Executive Summary'); expect(markdown).toContain('- Looks good.'); - expect(markdown).not.toContain('## Run manifest'); + expect(markdown).not.toContain('## Review Coverage And Cost'); + }); + + it('maps internal reviewer source ids to coverage labels', () => { + const markdown = formatCodeReviewReportMarkdown({ + review_mode: 'deep', + summary: { + risk_level: 'medium', + recommended_action: 'request_changes', + }, + issues: [{ + severity: 'high', + certainty: 'likely', + title: 'Token leak', + description: 'A token is logged.', + source_reviewer: 'ReviewSecurity', + }], + }); + + expect(markdown).toContain('- Source: Security coverage'); + expect(markdown).not.toContain('ReviewSecurity'); }); }); diff --git a/src/web-ui/src/flow_chat/deep-review/report/markdown.ts b/src/web-ui/src/flow_chat/deep-review/report/markdown.ts index 42031e121..443c4a340 100644 --- a/src/web-ui/src/flow_chat/deep-review/report/markdown.ts +++ b/src/web-ui/src/flow_chat/deep-review/report/markdown.ts @@ -6,43 +6,36 @@ import type { RemediationGroupId, StrengthGroupId, } from './codeReviewReport'; -import { formatRunManifestMarkdownSection } from './manifestSections'; import { buildCodeReviewReliabilityNotices, RELIABILITY_NOTICE_FALLBACK_LABELS, reliabilityNoticeMarkdownLine, } from './reliabilityNotices'; import { buildCodeReviewReportSections } from './reportSections'; +import { + DEFAULT_REVIEW_COVERAGE_SOURCE_LABELS, + formatReviewCoverageSource, +} from './reviewCoverageSource'; export const DEFAULT_CODE_REVIEW_MARKDOWN_LABELS: CodeReviewReportMarkdownLabels = { titleStandard: 'Code Review Report', - titleDeep: 'Deep Review Report', + titleDeep: 'Strict Review Report', executiveSummary: 'Executive Summary', reviewDecision: 'Review Decision', - runManifest: 'Run manifest', riskLevel: 'Risk Level', recommendedAction: 'Recommended Action', scope: 'Scope', - target: 'Target', - budget: 'Budget', - estimatedCalls: 'Estimated calls', - activeReviewers: 'Active reviewers', - skippedReviewers: 'Skipped reviewers', issues: 'Issues', noIssues: 'No validated issues.', remediationPlan: 'Remediation Plan', strengths: 'Strengths', - reviewTeam: 'Code Review Team', reliabilitySignals: 'Review Reliability', coverageNotes: 'Coverage Notes', - status: 'Status', - packet: 'Packet', - partialOutput: 'Partial output', - findings: 'Findings', validation: 'Validation', suggestion: 'Suggestion', source: 'Source', noItems: 'None.', + coverageSourceLabels: DEFAULT_REVIEW_COVERAGE_SOURCE_LABELS, reliabilityNoticeLabels: RELIABILITY_NOTICE_FALLBACK_LABELS, groupTitles: { must_fix: 'Must Fix', @@ -71,6 +64,10 @@ function mergeLabels(labels?: Partial): CodeRevi ...DEFAULT_CODE_REVIEW_MARKDOWN_LABELS.reliabilityNoticeLabels, ...labels?.reliabilityNoticeLabels, }, + coverageSourceLabels: { + ...DEFAULT_CODE_REVIEW_MARKDOWN_LABELS.coverageSourceLabels, + ...labels?.coverageSourceLabels, + }, }; } @@ -101,7 +98,6 @@ export function formatCodeReviewReportMarkdown( const mergedLabels = mergeLabels(labels); const sections = buildCodeReviewReportSections(report); const issues = report.issues ?? []; - const reviewers = report.reviewers ?? []; const lines: string[] = []; lines.push(`# ${report.review_mode === 'deep' ? mergedLabels.titleDeep : mergedLabels.titleStandard}`); @@ -116,10 +112,6 @@ export function formatCodeReviewReportMarkdown( lines.push(`- ${mergedLabels.scope}: ${report.review_scope.trim()}`); } lines.push(''); - if (report.review_mode === 'deep' && options?.runManifest) { - lines.push(formatRunManifestMarkdownSection(options.runManifest, mergedLabels)); - lines.push(''); - } const reliabilityNotices = buildCodeReviewReliabilityNotices(report, options?.runManifest); if (reliabilityNotices.length > 0) { lines.push(`## ${mergedLabels.reliabilitySignals}`); @@ -145,8 +137,12 @@ export function formatCodeReviewReportMarkdown( if (issue.category) { lines.push(` - ${issue.category}`); } - if (issue.source_reviewer) { - lines.push(` - ${mergedLabels.source}: ${issue.source_reviewer}`); + const coverageSource = formatReviewCoverageSource( + issue.source_reviewer, + mergedLabels.coverageSourceLabels, + ); + if (coverageSource) { + lines.push(` - ${mergedLabels.source}: ${coverageSource}`); } if (issue.description) { lines.push(` - ${issue.description}`); @@ -180,32 +176,6 @@ export function formatCodeReviewReportMarkdown( lines.push(`- ${mergedLabels.noItems}`); lines.push(''); } - lines.push(`## ${mergedLabels.reviewTeam}`); - if (reviewers.length === 0) { - lines.push(`- ${mergedLabels.noItems}`); - } else { - for (const reviewer of reviewers) { - const issueCount = typeof reviewer.issue_count === 'number' - ? `; ${mergedLabels.findings}: ${reviewer.issue_count}` - : ''; - lines.push(`- ${reviewer.name} (${reviewer.specialty}; ${mergedLabels.status}: ${reviewer.status}${issueCount})`); - if (reviewer.summary) { - lines.push(` - ${reviewer.summary}`); - } - const packetId = reviewer.packet_id?.trim(); - if (packetId || reviewer.packet_status_source) { - const packetLabel = packetId || 'missing'; - const sourceLabel = reviewer.packet_status_source - ? ` (${reviewer.packet_status_source})` - : ''; - lines.push(` - ${mergedLabels.packet}: ${packetLabel}${sourceLabel}`); - } - if (reviewer.partial_output?.trim()) { - lines.push(` - ${mergedLabels.partialOutput}: ${reviewer.partial_output.trim()}`); - } - } - } - lines.push(''); lines.push(`## ${mergedLabels.coverageNotes}`); pushList(lines, sections.coverageNotes, mergedLabels.noItems); diff --git a/src/web-ui/src/flow_chat/deep-review/report/reliabilityNotices.test.ts b/src/web-ui/src/flow_chat/deep-review/report/reliabilityNotices.test.ts index 22a081857..b3b9ab3f9 100644 --- a/src/web-ui/src/flow_chat/deep-review/report/reliabilityNotices.test.ts +++ b/src/web-ui/src/flow_chat/deep-review/report/reliabilityNotices.test.ts @@ -14,7 +14,7 @@ describe('reliabilityNotices', () => { kind: 'retry_guidance', severity: 'warning', source: 'runtime', - detail: 'Retry one reduced reviewer packet.', + detail: 'Retry one optional check outside this run.', }, ], }); @@ -24,14 +24,14 @@ describe('reliabilityNotices', () => { kind: 'retry_guidance', severity: 'warning', source: 'runtime', - detail: 'Retry one reduced reviewer packet.', + detail: 'Retry one optional check outside this run.', }, ]); expect(reliabilityNoticeMarkdownLine( notices[0], DEFAULT_CODE_REVIEW_MARKDOWN_LABELS, )).toBe( - '- Retry guidance emitted [warning/runtime]: Retry one reduced reviewer packet.', + '- Retry guidance emitted [warning/runtime]: Retry one optional check outside this run.', ); }); }); diff --git a/src/web-ui/src/flow_chat/deep-review/report/reliabilityNotices.ts b/src/web-ui/src/flow_chat/deep-review/report/reliabilityNotices.ts index e5885038e..827239d48 100644 --- a/src/web-ui/src/flow_chat/deep-review/report/reliabilityNotices.ts +++ b/src/web-ui/src/flow_chat/deep-review/report/reliabilityNotices.ts @@ -33,14 +33,14 @@ const RELIABILITY_NOTICE_ORDER: ReviewReliabilityNoticeKind[] = [ export const RELIABILITY_NOTICE_FALLBACK_LABELS: Record = { context_pressure: 'Context pressure rising', compression_preserved: 'Compression preserved key facts', - cache_hit: 'Incremental cache reused reviewer output', + cache_hit: 'Incremental cache reused review output', cache_miss: 'Incremental cache missed or refreshed', - concurrency_limited: 'Reviewer launch was concurrency-limited', - partial_reviewer: 'Reviewer returned partial result', - reduced_scope: 'Reduced-depth coverage', + concurrency_limited: 'Review launch was concurrency-limited', + partial_reviewer: 'Review returned partial result', + reduced_scope: 'Focused review scope', retry_guidance: 'Retry guidance emitted', - skipped_reviewers: 'Skipped reviewers', - token_budget_limited: 'Token budget limited reviewer coverage', + skipped_reviewers: 'Review scope tailored', + token_budget_limited: 'Token budget limited review coverage', user_decision: 'User decision needed', }; diff --git a/src/web-ui/src/flow_chat/deep-review/report/reportSections.ts b/src/web-ui/src/flow_chat/deep-review/report/reportSections.ts index 8366ee9d5..a8fa15108 100644 --- a/src/web-ui/src/flow_chat/deep-review/report/reportSections.ts +++ b/src/web-ui/src/flow_chat/deep-review/report/reportSections.ts @@ -134,7 +134,7 @@ function buildPartialReviewerCoverageNotes(reviewers: CodeReviewReviewer[] = []) if (!partialOutput || !PARTIAL_TIMEOUT_REVIEWER_STATUSES.has(reviewer.status)) { return null; } - return `${reviewer.name} stopped before completion after producing partial output: ${partialOutput}`; + return `A review coverage check stopped before completion after producing partial output: ${partialOutput}`; }) .filter((note): note is string => Boolean(note)); } diff --git a/src/web-ui/src/flow_chat/deep-review/report/reviewCoverageSource.test.ts b/src/web-ui/src/flow_chat/deep-review/report/reviewCoverageSource.test.ts new file mode 100644 index 000000000..364d0c23a --- /dev/null +++ b/src/web-ui/src/flow_chat/deep-review/report/reviewCoverageSource.test.ts @@ -0,0 +1,13 @@ +import { describe, expect, it } from 'vitest'; +import { formatReviewCoverageSource } from './reviewCoverageSource'; + +describe('formatReviewCoverageSource', () => { + it('maps known read-only review roles to user-facing labels', () => { + expect(formatReviewCoverageSource('ReviewSecurity')).toBe('Security coverage'); + expect(formatReviewCoverageSource('ReviewJudge')).toBe('Quality check'); + }); + + it('does not hide Review-prefixed remediation sources', () => { + expect(formatReviewCoverageSource('ReviewFixer')).toBe('ReviewFixer'); + }); +}); diff --git a/src/web-ui/src/flow_chat/deep-review/report/reviewCoverageSource.ts b/src/web-ui/src/flow_chat/deep-review/report/reviewCoverageSource.ts new file mode 100644 index 000000000..72c736c65 --- /dev/null +++ b/src/web-ui/src/flow_chat/deep-review/report/reviewCoverageSource.ts @@ -0,0 +1,69 @@ +export type ReviewCoverageSourceLabelKey = + | 'businessLogic' + | 'performance' + | 'security' + | 'architecture' + | 'frontend' + | 'qualityGate'; + +export const DEFAULT_REVIEW_COVERAGE_SOURCE_LABELS: Record< + ReviewCoverageSourceLabelKey, + string +> = { + businessLogic: 'Logic coverage', + performance: 'Performance coverage', + security: 'Security coverage', + architecture: 'Architecture coverage', + frontend: 'Frontend coverage', + qualityGate: 'Quality check', +}; + +const REVIEW_SOURCE_ALIASES: Record = { + reviewbusinesslogic: 'businessLogic', + logicreviewer: 'businessLogic', + businesslogicreviewer: 'businessLogic', + reviewperformance: 'performance', + performancereviewer: 'performance', + reviewsecurity: 'security', + securityreviewer: 'security', + reviewarchitecture: 'architecture', + architecturereviewer: 'architecture', + reviewfrontend: 'frontend', + frontendreviewer: 'frontend', + reviewjudge: 'qualityGate', + reviewarbiter: 'qualityGate', + reviewqualityinspector: 'qualityGate', + qualityinspector: 'qualityGate', +}; + +function normalizeReviewSource(value: string): string { + return value.toLowerCase().replace(/[^a-z0-9]/g, ''); +} + +export function resolveReviewCoverageSourceLabelKey( + sourceReviewer?: string | null, +): ReviewCoverageSourceLabelKey | null { + const normalized = sourceReviewer?.trim(); + if (!normalized) { + return null; + } + + return REVIEW_SOURCE_ALIASES[normalizeReviewSource(normalized)] ?? null; +} + +export function formatReviewCoverageSource( + sourceReviewer: string | undefined, + labels: Record = DEFAULT_REVIEW_COVERAGE_SOURCE_LABELS, +): string | null { + const normalized = sourceReviewer?.trim(); + if (!normalized) { + return null; + } + + const labelKey = resolveReviewCoverageSourceLabelKey(normalized); + if (labelKey) { + return labels[labelKey]; + } + + return normalized; +} diff --git a/src/web-ui/src/flow_chat/services/DeepReviewService.test.ts b/src/web-ui/src/flow_chat/services/DeepReviewService.test.ts index bb091f8c7..c16fa7ed2 100644 --- a/src/web-ui/src/flow_chat/services/DeepReviewService.test.ts +++ b/src/web-ui/src/flow_chat/services/DeepReviewService.test.ts @@ -95,26 +95,30 @@ describe('DeepReviewService slash command', () => { mockGitGetDiff.mockResolvedValue(''); }); - it('uses /DeepReview as the canonical command', () => { - expect(DEEP_REVIEW_SLASH_COMMAND).toBe('/DeepReview'); + it('uses /review strict as the canonical typed command', () => { + expect(DEEP_REVIEW_SLASH_COMMAND).toBe('/review strict'); }); - it('recognizes canonical deep review commands and rejects near matches', () => { + it('recognizes strict Review typed commands and compatibility aliases', () => { + expect(isDeepReviewSlashCommand('/review strict')).toBe(true); + expect(isDeepReviewSlashCommand('/review strict commit abc123')).toBe(true); + expect(isDeepReviewSlashCommand('/review deep commit abc123')).toBe(false); expect(isDeepReviewSlashCommand('/DeepReview')).toBe(true); expect(isDeepReviewSlashCommand('/DeepReview review commit abc123')).toBe(true); - expect(isDeepReviewSlashCommand('/deepreview review commit abc123')).toBe(false); + expect(isDeepReviewSlashCommand('/deepreview review commit abc123')).toBe(true); + expect(isDeepReviewSlashCommand('/review')).toBe(false); expect(isDeepReviewSlashCommand('/DeepReviewer review commit abc123')).toBe(false); }); it('strips the canonical command before building the focus block', async () => { const prompt = await buildDeepReviewPromptFromSlashCommand( - '/DeepReview review commit abc123 for security', + '/review strict commit abc123 for security', 'D:\\workspace\\repo', ); - expect(prompt).toContain('Original command:\n/DeepReview review commit abc123 for security'); - expect(prompt).toContain('User-provided focus or target:\nreview commit abc123 for security'); - expect(prompt).not.toContain('User-provided focus or target:\n/DeepReview'); + expect(prompt).toContain('Original command:\n/review strict commit abc123 for security'); + expect(prompt).toContain('User-provided focus or target:\ncommit abc123 for security'); + expect(prompt).not.toContain('User-provided focus or target:\n/review'); }); it('classifies explicit slash-command file paths before building the review team manifest', async () => { @@ -491,7 +495,7 @@ describe('launchDeepReviewSession', () => { parentSessionId: 'parent-123', workspacePath: 'D:\\workspace\\repo', prompt: 'Review these files', - displayMessage: 'Deep review started', + displayMessage: 'Strict review started', }); expect(result.childSessionId).toBe('child-123'); @@ -503,13 +507,11 @@ describe('launchDeepReviewSession', () => { agentType: 'DeepReview', }), ); - expect(mockOpenBtwSessionInAuxPane).toHaveBeenCalledWith( - expect.objectContaining({ childSessionId: 'child-123' }), - ); + expect(mockOpenBtwSessionInAuxPane).not.toHaveBeenCalled(); expect(mockSendMessage).toHaveBeenCalledWith( 'Review these files', 'child-123', - 'Deep review started', + 'Strict review started', ); expect(mockInsertReviewSessionSummaryMarker).toHaveBeenCalledWith( expect.objectContaining({ @@ -532,7 +534,7 @@ describe('launchDeepReviewSession', () => { parentSessionId: 'parent-123', workspacePath: 'D:\\workspace\\repo', prompt: 'Review these files', - displayMessage: 'Deep review started', + displayMessage: 'Strict review started', runManifest: runManifest as any, }); @@ -555,14 +557,14 @@ describe('launchDeepReviewSession', () => { parentSessionId: 'parent-123', workspacePath: 'D:\\workspace\\repo', prompt: 'Review these files', - displayMessage: 'Deep review started', + displayMessage: 'Strict review started', runManifest: runManifest as any, }); expect(mockSendMessage).toHaveBeenCalledWith( 'Review these files', 'child-123', - 'Deep review started', + 'Strict review started', undefined, undefined, { @@ -582,14 +584,14 @@ describe('launchDeepReviewSession', () => { parentSessionId: 'parent-123', workspacePath: 'D:\\workspace\\repo', prompt: 'Review these files', - displayMessage: 'Deep review started', + displayMessage: 'Strict review started', }); } catch (error) { caughtError = error; } expect(caughtError).toBeInstanceOf(Error); - expect((caughtError as Error).message).toBe('Deep review failed to start. Please try again.'); + expect((caughtError as Error).message).toBe('Strict review failed to start. Please try again.'); expect((caughtError as { launchErrorMessageKey?: string }).launchErrorMessageKey).toBe( 'deepReviewActionBar.launchError.unknown', ); @@ -602,36 +604,6 @@ describe('launchDeepReviewSession', () => { expect(mockDiscardLocalSession).not.toHaveBeenCalled(); }); - it('throws and performs full cleanup when openBtwSessionInAuxPane fails', async () => { - mockCreateBtwChildSession.mockResolvedValue({ - childSessionId: 'child-123', - parentDialogTurnId: 'turn-456', - }); - mockOpenBtwSessionInAuxPane.mockImplementation(() => { - throw new Error('Pane open failed'); - }); - mockDeleteSession.mockResolvedValue(undefined); - mockSessionsMap.set('child-123', { workspacePath: 'D:\\workspace\\repo' }); - - await expect( - launchDeepReviewSession({ - parentSessionId: 'parent-123', - workspacePath: 'D:\\workspace\\repo', - prompt: 'Review these files', - displayMessage: 'Deep review started', - }), - ).rejects.toThrow('Pane open failed'); - - expect(mockCloseBtwSessionInAuxPane).toHaveBeenCalledWith('child-123'); - expect(mockDeleteSession).toHaveBeenCalledWith( - 'child-123', - 'D:\\workspace\\repo', - undefined, - undefined, - ); - expect(mockDiscardLocalSession).toHaveBeenCalledWith('child-123'); - }); - it('classifies sendMessage launch failures after cleanup', async () => { mockCreateBtwChildSession.mockResolvedValue({ childSessionId: 'child-123', @@ -647,14 +619,14 @@ describe('launchDeepReviewSession', () => { parentSessionId: 'parent-123', workspacePath: 'D:\\workspace\\repo', prompt: 'Review these files', - displayMessage: 'Deep review started', + displayMessage: 'Strict review started', }); } catch (error) { caughtError = error; } expect(caughtError).toBeInstanceOf(Error); - expect((caughtError as Error).message).toBe('Network connection was interrupted before Deep Review could start.'); + expect((caughtError as Error).message).toBe('Network connection was interrupted before strict review could start.'); expect((caughtError as { launchErrorMessageKey?: string }).launchErrorMessageKey).toBe( 'deepReviewActionBar.launchError.network', ); @@ -665,14 +637,12 @@ describe('launchDeepReviewSession', () => { expect(mockDiscardLocalSession).toHaveBeenCalledWith('child-123'); }); - it('skips backend cleanup when workspace path is missing', async () => { + it('skips backend cleanup when sendMessage fails and workspace path is missing', async () => { mockCreateBtwChildSession.mockResolvedValue({ childSessionId: 'child-123', parentDialogTurnId: 'turn-456', }); - mockOpenBtwSessionInAuxPane.mockImplementation(() => { - throw new Error('Pane open failed'); - }); + mockSendMessage.mockRejectedValue(new Error('SSE stream connection timeout')); // No workspacePath in session mockSessionsMap.set('child-123', {}); @@ -681,9 +651,9 @@ describe('launchDeepReviewSession', () => { parentSessionId: 'parent-123', workspacePath: 'D:\\workspace\\repo', prompt: 'Review these files', - displayMessage: 'Deep review started', + displayMessage: 'Strict review started', }), - ).rejects.toThrow('Pane open failed'); + ).rejects.toThrow('Cleanup was incomplete'); expect(mockCloseBtwSessionInAuxPane).toHaveBeenCalledWith('child-123'); expect(mockDeleteSession).not.toHaveBeenCalled(); @@ -695,9 +665,7 @@ describe('launchDeepReviewSession', () => { childSessionId: 'child-123', parentDialogTurnId: 'turn-456', }); - mockOpenBtwSessionInAuxPane.mockImplementation(() => { - throw new Error('Pane open failed'); - }); + mockSendMessage.mockRejectedValue(new Error('SSE stream connection timeout')); mockDeleteSession.mockRejectedValue(new Error('Session does not exist')); mockSessionsMap.set('child-123', { workspacePath: 'D:\\workspace\\repo' }); @@ -706,9 +674,9 @@ describe('launchDeepReviewSession', () => { parentSessionId: 'parent-123', workspacePath: 'D:\\workspace\\repo', prompt: 'Review these files', - displayMessage: 'Deep review started', + displayMessage: 'Strict review started', }), - ).rejects.toThrow('Pane open failed'); + ).rejects.toThrow('Network connection was interrupted before strict review could start.'); expect(mockDeleteSession).toHaveBeenCalled(); // discardLocalSession should still be called because backend reports session missing diff --git a/src/web-ui/src/flow_chat/store/deepReviewActionBarStore.test.ts b/src/web-ui/src/flow_chat/store/deepReviewActionBarStore.test.ts index ff2628fb5..b5983d3fb 100644 --- a/src/web-ui/src/flow_chat/store/deepReviewActionBarStore.test.ts +++ b/src/web-ui/src/flow_chat/store/deepReviewActionBarStore.test.ts @@ -288,7 +288,7 @@ describe('deepReviewActionBarStore', () => { expect(state.optionalReviewerCount).toBe(0); }); - it('merges reviewer queue events and closes the queue when the last reviewer leaves', () => { + it('merges capacity-wait events and closes waiting state when the last reviewer leaves', () => { bar().showCapacityQueueBar({ childSessionId: 'child-1', parentSessionId: 'parent-1', diff --git a/src/web-ui/src/flow_chat/tool-cards/CodeReviewReportExportActions.test.tsx b/src/web-ui/src/flow_chat/tool-cards/CodeReviewReportExportActions.test.tsx index b861301d0..bb133b2d6 100644 --- a/src/web-ui/src/flow_chat/tool-cards/CodeReviewReportExportActions.test.tsx +++ b/src/web-ui/src/flow_chat/tool-cards/CodeReviewReportExportActions.test.tsx @@ -2,6 +2,9 @@ import React from 'react'; import { renderToStaticMarkup } from 'react-dom/server'; import { describe, expect, it, vi } from 'vitest'; import { CodeReviewReportExportActions } from './CodeReviewReportExportActions'; +import type { ReviewTeamRunManifest } from '@/shared/services/reviewTeamService'; + +const formatCodeReviewReportMarkdownMock = vi.hoisted(() => vi.fn(() => '# Review')); function Icon({ name }: { name: string }) { return ; @@ -53,7 +56,7 @@ vi.mock('@/shared/utils/tabUtils', () => ({ })); vi.mock('../utils/codeReviewReport', () => ({ - formatCodeReviewReportMarkdown: () => '# Review', + formatCodeReviewReportMarkdown: (...args: unknown[]) => formatCodeReviewReportMarkdownMock(...args), })); describe('CodeReviewReportExportActions', () => { @@ -89,4 +92,24 @@ describe('CodeReviewReportExportActions', () => { expect(html).toContain('aria-label="Save Markdown"'); expect(html).not.toContain('aria-label="Open as Markdown"'); }); + + it('passes the review run manifest into Markdown formatting', () => { + const runManifest = { + strategyLevel: 'quick', + skippedReviewers: [], + }; + + renderToStaticMarkup( + , + ); + + expect(formatCodeReviewReportMarkdownMock).toHaveBeenCalledWith( + { summary: { recommended_action: 'approve' } }, + expect.any(Object), + { runManifest }, + ); + }); }); diff --git a/src/web-ui/src/flow_chat/tool-cards/CodeReviewReportExportActions.tsx b/src/web-ui/src/flow_chat/tool-cards/CodeReviewReportExportActions.tsx index 3b2cf84ad..98a1f2734 100644 --- a/src/web-ui/src/flow_chat/tool-cards/CodeReviewReportExportActions.tsx +++ b/src/web-ui/src/flow_chat/tool-cards/CodeReviewReportExportActions.tsx @@ -60,30 +60,27 @@ export const CodeReviewReportExportActions: React.FC { dom.window.close(); }); - it('echoes the deep review run manifest from the review session', () => { + it('summarizes deep review coverage without exposing the run manifest', () => { const toolItem: FlowToolItem = { id: 'tool-1', type: 'tool', @@ -224,30 +224,24 @@ describe('CodeReviewToolCard', () => { ); }); - expect(container.textContent).toContain('Run manifest'); - expect(container.textContent).toContain('3 active'); - expect(container.textContent).toContain('2 skipped'); - - const manifestSectionButton = Array.from(container.querySelectorAll('button')) - .find((button) => button.textContent?.includes('Run manifest')); - - act(() => { - manifestSectionButton?.dispatchEvent(new window.Event('click', { bubbles: true })); - }); - - expect(container.textContent).toContain('Logic reviewer'); - expect(container.textContent).toContain('Quality inspector'); - expect(container.textContent).toContain('Custom security reviewer'); - expect(container.textContent).toContain('Frontend reviewer'); - expect(container.textContent).toContain('Not applicable to this target'); - expect(container.textContent).toContain('Custom invalid reviewer'); - expect(container.textContent).toContain('Configuration issue'); - expect(container.textContent).toContain('Recommended strategy'); - expect(container.textContent).toContain('deep'); - expect(container.textContent).toContain('Large/high-risk change'); + expect(container.textContent).toContain('Review status'); + expect(container.textContent).toContain('Review scope tailored'); + expect(container.textContent).toContain('Token budget limited review coverage'); + expect(container.textContent).toContain('2 optional check was outside this run'); + expect(container.textContent).toContain('Token budget mode kept 1 optional check outside this run'); + expect(container.textContent).not.toContain('Coverage and cost'); + expect(container.textContent).not.toContain('Target'); + expect(container.textContent).not.toContain('Budget'); + expect(container.textContent).not.toContain('Estimated review checks'); + expect(container.textContent).not.toContain('Recommended strategy'); + expect(container.textContent).not.toContain('Frontend reviewer'); + expect(container.textContent).not.toContain('Not applicable to this target'); + expect(container.textContent).not.toContain('Custom invalid reviewer'); + expect(container.textContent).not.toContain('Configuration issue'); + expect(container.textContent).not.toContain('Large/high-risk change'); }); - it('updates the manifest echo when session metadata arrives after render', () => { + it('updates coverage reliability when session metadata arrives after render', () => { flowState.current = { sessions: new Map([ ['review-session', { id: 'review-session' }], @@ -302,7 +296,7 @@ describe('CodeReviewToolCard', () => { ); }); - expect(container.textContent).not.toContain('Run manifest'); + expect(container.textContent).not.toContain('Review scope tailored'); act(() => { flowState.current = { @@ -314,8 +308,9 @@ describe('CodeReviewToolCard', () => { notifyFlowState(); }); - expect(container.textContent).toContain('Run manifest'); - expect(container.textContent).toContain('3 active'); + expect(container.textContent).toContain('Review scope tailored'); + expect(container.textContent).toContain('Token budget limited review coverage'); + expect(container.textContent).not.toContain('Coverage and cost'); }); it('renders compact reliability status when a reviewer returned partial evidence', () => { @@ -375,11 +370,11 @@ describe('CodeReviewToolCard', () => { }); expect(container.textContent).toContain('Review status'); - expect(container.textContent).toContain('Reviewer returned partial result'); - expect(container.textContent).toContain('1 reviewer result is partial; confidence is reduced.'); + expect(container.textContent).toContain('Review returned partial result'); + expect(container.textContent).toContain('1 review result is partial; confidence is limited.'); }); - it('renders reduced-depth reliability status from structured report signals', () => { + it('renders focused-scope reliability status from structured report signals', () => { const toolItem: FlowToolItem = { id: 'tool-1', type: 'tool', @@ -436,7 +431,75 @@ describe('CodeReviewToolCard', () => { ); }); - expect(container.textContent).toContain('Reduced-depth coverage'); + expect(container.textContent).toContain('Focused review scope'); expect(container.textContent).toContain('High-risk-only pass; changed files remain visible.'); }); + + it('maps internal reviewer source ids before rendering issues', () => { + const toolItem: FlowToolItem = { + id: 'tool-1', + type: 'tool', + timestamp: Date.now(), + toolName: 'submit_code_review', + status: 'completed', + toolCall: { + id: 'call-1', + input: {}, + }, + toolResult: { + success: true, + result: { + review_mode: 'deep', + summary: { + overall_assessment: 'Security issue found.', + risk_level: 'high', + recommended_action: 'request_changes', + }, + issues: [{ + severity: 'high', + certainty: 'likely', + title: 'Token leak', + description: 'A token is logged.', + source_reviewer: 'ReviewSecurity', + }], + reviewers: [], + }, + }, + }; + const config: ToolCardConfig = { + toolName: 'submit_code_review', + displayName: 'Code Review', + icon: 'REVIEW', + requiresConfirmation: false, + resultDisplayType: 'detailed', + }; + + act(() => { + root.render( + , + ); + }); + + act(() => { + container.querySelector('.preview-toggle-btn')?.dispatchEvent( + new window.Event('click', { bubbles: true }), + ); + }); + const issuesSectionButton = Array.from( + container.querySelectorAll('.review-report-section__header'), + ).find((button) => button.textContent?.includes('Issues')); + expect(issuesSectionButton).toBeTruthy(); + act(() => { + issuesSectionButton!.dispatchEvent( + new window.Event('click', { bubbles: true }), + ); + }); + + expect(container.textContent).toContain('Security coverage'); + expect(container.textContent).not.toContain('ReviewSecurity'); + }); }); diff --git a/src/web-ui/src/flow_chat/tool-cards/CodeReviewToolCard.tsx b/src/web-ui/src/flow_chat/tool-cards/CodeReviewToolCard.tsx index 5e0e78614..37c8ca822 100644 --- a/src/web-ui/src/flow_chat/tool-cards/CodeReviewToolCard.tsx +++ b/src/web-ui/src/flow_chat/tool-cards/CodeReviewToolCard.tsx @@ -28,8 +28,8 @@ import { buildCodeReviewReliabilityNotices, buildCodeReviewReportSections, getDefaultExpandedCodeReviewSectionIds, + formatReviewCoverageSource, type CodeReviewReportData, - type CodeReviewReviewer, type ReviewReliabilityNotice, type RemediationGroupId, type ReviewReportGroup, @@ -40,12 +40,7 @@ import { CodeReviewReportExportActions } from './CodeReviewReportExportActions'; import { DEEP_REVIEW_SCROLL_TO_EVENT, type DeepReviewScrollToRequest } from '../events/flowchatNavigation'; import { globalEventBus } from '@/infrastructure/event-bus'; import { normalizeDecisionEntry, type DecisionContext } from '../utils/codeReviewReport'; -import { - getActiveReviewTeamManifestMembers, - type ReviewTeamManifestMember, - type ReviewTeamManifestMemberReason, - type ReviewTeamRunManifest, -} from '@/shared/services/reviewTeamService'; +import type { ReviewTeamRunManifest } from '@/shared/services/reviewTeamService'; import './CodeReviewToolCard.scss'; const log = createLogger('CodeReviewToolCard'); @@ -105,6 +100,29 @@ function getStrengthGroupTitle(id: StrengthGroupId, t: Translate): string { }); } +function getCoverageSourceLabel(sourceReviewer: string | undefined, t: Translate): string | null { + return formatReviewCoverageSource(sourceReviewer, { + businessLogic: t('toolCards.codeReview.coverageSources.businessLogic', { + defaultValue: 'Logic coverage', + }), + performance: t('toolCards.codeReview.coverageSources.performance', { + defaultValue: 'Performance coverage', + }), + security: t('toolCards.codeReview.coverageSources.security', { + defaultValue: 'Security coverage', + }), + architecture: t('toolCards.codeReview.coverageSources.architecture', { + defaultValue: 'Architecture coverage', + }), + frontend: t('toolCards.codeReview.coverageSources.frontend', { + defaultValue: 'Frontend coverage', + }), + qualityGate: t('toolCards.codeReview.coverageSources.qualityGate', { + defaultValue: 'Quality check', + }), + }); +} + function formatIssueStats(stats: { critical: number; high: number; medium: number; low: number; info: number; total: number }, t: Translate): string { if (stats.total === 0) { return t('toolCards.codeReview.noIssues', { defaultValue: 'No issues' }); @@ -113,27 +131,7 @@ function formatIssueStats(stats: { critical: number; high: number; medium: numbe return (['critical', 'high', 'medium', 'low', 'info'] as const) .filter((severity) => stats[severity] > 0) .map((severity) => `${stats[severity]} ${t(`toolCards.codeReview.severities.${severity}`, { defaultValue: severity })}`) - .join(' · '); -} - -function formatReviewerStats(stats: { total: number; completed: number; degraded: number }, t: Translate): string { - return t('toolCards.codeReview.reviewerTeamSummary', { - total: stats.total, - completed: stats.completed, - degraded: stats.degraded, - defaultValue: '{{total}} reviewers · {{completed}} completed · {{degraded}} attention', - }); -} - -function formatReviewerStatus(status: string, t: Translate): string { - const normalizedStatus = status - .trim() - .toLowerCase() - .replace(/[\s-]+/g, '_'); - - return t(`toolCards.codeReview.reviewerStatuses.${normalizedStatus}`, { - defaultValue: status, - }); + .join(' / '); } function getReliabilityNoticeLabel(notice: ReviewReliabilityNotice, t: Translate): string { @@ -141,14 +139,14 @@ function getReliabilityNoticeLabel(notice: ReviewReliabilityNotice, t: Translate defaultValue: { context_pressure: 'Context pressure rising', compression_preserved: 'Compression preserved key facts', - cache_hit: 'Incremental cache reused reviewer output', + cache_hit: 'Incremental cache reused review output', cache_miss: 'Incremental cache missed or refreshed', - concurrency_limited: 'Reviewer launch was concurrency-limited', - partial_reviewer: 'Reviewer returned partial result', - reduced_scope: 'Reduced-depth coverage', + concurrency_limited: 'Review launch was concurrency-limited', + partial_reviewer: 'Review returned partial result', + reduced_scope: 'Focused review scope', retry_guidance: 'Retry guidance emitted', - skipped_reviewers: 'Skipped reviewers', - token_budget_limited: 'Token budget limited reviewer coverage', + skipped_reviewers: 'Review scope tailored', + token_budget_limited: 'Token budget limited review coverage', user_decision: 'User decision needed', }[notice.kind], }); @@ -162,16 +160,16 @@ function getReliabilityNoticeDetail(notice: ReviewReliabilityNotice, t: Translat return t(`toolCards.codeReview.reliabilityStatus.${notice.kind}.detail`, { count: notice.count ?? 0, defaultValue: { - context_pressure: '{{count}} reviewer calls planned for a large or constrained target.', + context_pressure: '{{count}} review checks planned for a large or constrained target.', compression_preserved: 'Coverage notes include preserved context from compression.', - cache_hit: '{{count}} reviewer packet reused matching cached output.', - cache_miss: '{{count}} reviewer packet ran fresh or refreshed stale cache.', - concurrency_limited: '{{count}} reviewer launch hit a concurrency cap.', - partial_reviewer: '{{count}} reviewer result is partial; confidence is reduced.', - reduced_scope: 'This review used a reduced-depth scope profile.', + cache_hit: '{{count}} previous review result reused matching cached output.', + cache_miss: '{{count}} review result ran fresh or refreshed stale cache.', + concurrency_limited: '{{count}} review launch hit a concurrency cap.', + partial_reviewer: '{{count}} review result is partial; confidence is limited.', + reduced_scope: 'This review used a focused scope profile.', retry_guidance: '{{count}} retry guidance item was emitted for partial review coverage.', - skipped_reviewers: '{{count}} reviewer was skipped by applicability, configuration, or budget.', - token_budget_limited: '{{count}} reviewer was skipped by token budget mode.', + skipped_reviewers: '{{count}} optional check was outside this run because of applicability, configuration, or budget.', + token_budget_limited: 'Token budget mode kept {{count}} optional check outside this run.', user_decision: '{{count}} review item needs your decision before fixing.', }[notice.kind], }); @@ -199,71 +197,6 @@ function getDeepReviewRunManifestForSession(sessionId?: string): ReviewTeamRunMa return flowChatStore.getState().sessions.get(sessionId)?.deepReviewRunManifest; } -function getReviewerLabel(member: ReviewTeamManifestMember): string { - return member.displayName || member.subagentId; -} - -function getSkippedReasonLabel( - reason: ReviewTeamManifestMemberReason | undefined, - t: Translate, -): string { - switch (reason) { - case 'not_applicable': - return t('toolCards.codeReview.runManifest.skippedReasons.notApplicable', { - defaultValue: 'Not applicable to this target', - }); - case 'budget_limited': - return t('toolCards.codeReview.runManifest.skippedReasons.budgetLimited', { - defaultValue: 'Limited by token budget', - }); - case 'invalid_tooling': - return t('toolCards.codeReview.runManifest.skippedReasons.invalidTooling', { - defaultValue: 'Configuration issue', - }); - case 'disabled': - return t('toolCards.codeReview.runManifest.skippedReasons.disabled', { - defaultValue: 'Disabled', - }); - case 'unavailable': - return t('toolCards.codeReview.runManifest.skippedReasons.unavailable', { - defaultValue: 'Unavailable', - }); - default: - return t('toolCards.codeReview.runManifest.skippedReasons.skipped', { - defaultValue: 'Skipped', - }); - } -} - -function formatRunManifestSummary( - manifest: ReviewTeamRunManifest, - activeReviewers: ReviewTeamManifestMember[], - t: Translate, -): string { - return t('toolCards.codeReview.runManifest.summary', { - active: activeReviewers.length, - skipped: manifest.skippedReviewers.length, - calls: manifest.tokenBudget.estimatedReviewerCalls, - defaultValue: '{{active}} active / {{skipped}} skipped / {{calls}} calls', - }); -} - -function formatReviewDepthLabel(reviewDepth: string, t: Translate): string { - return t(`toolCards.codeReview.runManifest.reviewDepthLabels.${reviewDepth}`, { - defaultValue: { - high_risk_only: 'High-risk-only', - risk_expanded: 'Risk-expanded', - full_depth: 'Full-depth', - }[reviewDepth] ?? reviewDepth, - }); -} - -function formatRunManifestTarget(manifest: ReviewTeamRunManifest): string { - return manifest.target.tags.length > 0 - ? manifest.target.tags.join(', ') - : manifest.target.source; -} - function renderReportGroupList( groups: Array>, titleForGroup: (id: TId) => string, @@ -600,7 +533,7 @@ export const CodeReviewToolCard: React.FC = React.memo(({ {hasData && reviewData && ( )} {hasData && ( @@ -630,13 +563,9 @@ export const CodeReviewToolCard: React.FC = React.memo(({ const issues = reviewData.issues ?? []; const review_mode = reviewData.review_mode; const review_scope = reviewData.review_scope; - const reviewers = reviewData.reviewers ?? []; const runManifest = review_mode === 'deep' ? sessionRunManifest : undefined; - const activeRunManifestReviewers = runManifest - ? getActiveReviewTeamManifestMembers(runManifest) - : []; const reportSections = buildCodeReviewReportSections(reviewData); const reliabilityNotices = buildCodeReviewReliabilityNotices(reviewData, runManifest); const riskLevel = summary.risk_level ?? 'low'; @@ -648,8 +577,6 @@ export const CodeReviewToolCard: React.FC = React.memo(({ const remediationExpanded = expandedReportSectionIds.has('remediation'); const issuesExpanded = expandedReportSectionIds.has('issues'); const strengthsExpanded = expandedReportSectionIds.has('strengths'); - const runManifestExpanded = expandedReportSectionIds.has('runManifest'); - const teamExpanded = expandedReportSectionIds.has('team'); const coverageExpanded = expandedReportSectionIds.has('coverage'); return ( @@ -733,89 +660,6 @@ export const CodeReviewToolCard: React.FC = React.memo(({ - {runManifest && ( - -
-
-
- {t('toolCards.codeReview.runManifest.target')} - {formatRunManifestTarget(runManifest)} -
-
- {t('toolCards.codeReview.runManifest.budget')} - {runManifest.tokenBudget.mode} -
-
- {t('toolCards.codeReview.runManifest.estimatedCalls')} - {runManifest.tokenBudget.estimatedReviewerCalls} -
- {runManifest.strategyRecommendation && ( -
- - {t('toolCards.codeReview.runManifest.recommendedStrategy')} - - {runManifest.strategyRecommendation.strategyLevel} -
- )} - {runManifest.scopeProfile && ( -
- - {t('toolCards.codeReview.runManifest.reviewDepth')} - - {formatReviewDepthLabel(runManifest.scopeProfile.reviewDepth, t)} -
- )} -
- - {runManifest.strategyRecommendation && ( -
-
- {t('toolCards.codeReview.runManifest.riskRecommendationTitle')} -
-

{runManifest.strategyRecommendation.rationale}

-
- )} - - {activeRunManifestReviewers.length > 0 && ( -
-
- {t('toolCards.codeReview.runManifest.activeGroupTitle')} -
-
- {activeRunManifestReviewers.map((member) => ( - - {getReviewerLabel(member)} - {member.roleName} - - ))} -
-
- )} - - {runManifest.skippedReviewers.length > 0 && ( -
-
- {t('toolCards.codeReview.runManifest.skippedGroupTitle')} -
-
    - {runManifest.skippedReviewers.map((member) => ( -
  • - {getReviewerLabel(member)} - {getSkippedReasonLabel(member.reason, t)} -
  • - ))} -
-
- )} -
-
- )} - {issues.length > 0 && ( = React.memo(({ >
{issues.map((issue, index) => ( -
-
-
- {getSeverityIcon(issue.severity ?? 'info')} - {issue.category && ( - [{issue.category}] - )} - {issue.source_reviewer && ( - {issue.source_reviewer} - )} - {issue.file && ( - - {issue.file}{issue.line ? `:${issue.line}` : ''} + (() => { + const coverageSource = getCoverageSourceLabel(issue.source_reviewer, t); + return ( +
+
+
+ {getSeverityIcon(issue.severity ?? 'info')} + {issue.category && ( + [{issue.category}] + )} + {coverageSource && ( + {coverageSource} + )} + {issue.file && ( + + {issue.file}{issue.line ? `:${issue.line}` : ''} + + )} +
+ + {t(`toolCards.codeReview.certainties.${issue.certainty ?? 'possible'}`)} +
+
{issue.title}
+
{issue.description}
+ {issue.validation_note && ( +
+ {issue.validation_note} +
+ )} + {issue.suggestion && ( +
+ {t('toolCards.codeReview.suggestion')}: + {issue.suggestion} +
)}
- - {t(`toolCards.codeReview.certainties.${issue.certainty ?? 'possible'}`)} - -
-
{issue.title}
-
{issue.description}
- {issue.validation_note && ( -
- {issue.validation_note} -
- )} - {issue.suggestion && ( -
- {t('toolCards.codeReview.suggestion')}: - {issue.suggestion} -
- )} -
+ ); + })() ))}
@@ -1055,39 +904,6 @@ export const CodeReviewToolCard: React.FC = React.memo(({ )} - {reviewers.length > 0 && ( - -
- {reviewers.map((reviewer: CodeReviewReviewer, index: number) => ( -
-
-
- {reviewer.name} - {reviewer.specialty} -
-
- {formatReviewerStatus(reviewer.status, t)} - - {typeof reviewer.issue_count === 'number' - ? t('toolCards.codeReview.reviewerIssues', { - count: reviewer.issue_count, - }) - : t('toolCards.codeReview.reviewerIssuesUnknown')} - -
-
-
{reviewer.summary}
-
- ))} -
-
- )} - {reportSections.coverageNotes.length > 0 && ( { expect(taskCollapseStateManager.isCollapsed('task-tool-1')).toBe(true); }); + it('does not treat Review-prefixed remediation agents as read-only coverage tasks', async () => { + const completedItem: FlowToolItem = { + ...reviewTaskItem('completed', 'ReviewFixer', 'Fix reviewed issues'), + subagentSessionId: 'review-fixer-session', + }; + const runningItem: FlowToolItem = { + ...reviewTaskItem('running', 'ReviewFixer', 'Fix reviewed issues'), + subagentSessionId: 'review-fixer-session', + }; + + await act(async () => { + root.render( + , + ); + }); + + await act(async () => { + root.render( + , + ); + }); + + expect(taskCollapseStateManager.isCollapsed('task-tool-1')).toBe(false); + expect(container.textContent).toContain('ReviewFixer'); + expect(container.querySelector('.task-subagent-stop-button')).toBeTruthy(); + }); + it('opens the real subagent session in the aux pane when the task card rail is clicked', async () => { const toolItem: FlowToolItem = { ...reviewTaskItem('completed', 'Explore', 'Investigate task card behavior'), diff --git a/src/web-ui/src/flow_chat/tool-cards/TaskToolDisplay.tsx b/src/web-ui/src/flow_chat/tool-cards/TaskToolDisplay.tsx index c65f78450..2c6f06310 100644 --- a/src/web-ui/src/flow_chat/tool-cards/TaskToolDisplay.tsx +++ b/src/web-ui/src/flow_chat/tool-cards/TaskToolDisplay.tsx @@ -129,6 +129,19 @@ function readTaskRunInBackground(input: unknown, toolResult: FlowToolItem['toolR return false; } +const INTERNAL_READONLY_REVIEW_AGENT_IDS = new Set([ + 'ReviewBusinessLogic', + 'ReviewPerformance', + 'ReviewSecurity', + 'ReviewArchitecture', + 'ReviewFrontend', + 'ReviewJudge', +]); + +function isInternalReadonlyReviewAgent(subagentType: string): boolean { + return INTERNAL_READONLY_REVIEW_AGENT_IDS.has(subagentType); +} + function readTaskWasCancelled( status: FlowToolItem['status'], toolResult: FlowToolItem['toolResult'] | undefined, @@ -160,7 +173,7 @@ function isDeepReviewReviewerTask(toolItem: FlowToolItem): boolean { return false; } - if (getReviewerContextBySubagentId(subagentType) || /^Review[A-Z0-9_]/.test(subagentType)) { + if (getReviewerContextBySubagentId(subagentType) || isInternalReadonlyReviewAgent(subagentType)) { return true; } @@ -186,6 +199,7 @@ export const TaskToolDisplay: React.FC = ({ const rawTaskAction = readTaskAction(toolCall?.input, toolResult); const isCancelAction = rawTaskAction === 'cancel'; const isBackgroundTask = readTaskRunInBackground(toolCall?.input, toolResult); + const isReviewCoverageTask = isDeepReviewReviewerTask(toolItem); const [isStoppingSubagent, setIsStoppingSubagent] = useState(false); // Restore collapse state; default to collapsed. @@ -198,7 +212,7 @@ export const TaskToolDisplay: React.FC = ({ }); const isRunning = status === 'preparing' || status === 'streaming' || status === 'running'; - const keepCollapsedWhileRunning = isCancelAction || isDeepReviewReviewerTask(toolItem); + const keepCollapsedWhileRunning = isCancelAction || isReviewCoverageTask; const { cardRootRef, applyExpandedState } = useToolCardHeightContract({ toolId, @@ -309,8 +323,19 @@ export const TaskToolDisplay: React.FC = ({ readStringValue(toolCall.input.model_id) || readStringValue(toolCall.input.modelId); - // For built-in review-team reviewers, surface role context instead of - // the raw prompt so internal directives stay private. + if (isReviewCoverageTask) { + return { + description: t('toolCards.taskTool.reviewCoverageDescription'), + prompt: 'Not provided', + agentType: t('toolCards.taskTool.reviewCoverageLabel'), + modelName, + reviewerContext: null, + isReviewCoverageTask: true, + }; + } + + // For built-in review-team reviewers outside the unified Review flow, + // surface role context instead of the raw prompt so internal directives stay private. const reviewerContext: ReviewerContext | null = agentType !== 'Not provided' ? getReviewerContextBySubagentId(agentType) @@ -322,6 +347,7 @@ export const TaskToolDisplay: React.FC = ({ agentType, modelName, reviewerContext, + isReviewCoverageTask: false, }; }; @@ -385,6 +411,7 @@ export const TaskToolDisplay: React.FC = ({ ); const showSubagentExecModel = isTaskTool && + !isReviewCoverageTask && ( Boolean(linkedSubagentSessionId) || Boolean(resolvedSubagentModel) @@ -441,7 +468,7 @@ export const TaskToolDisplay: React.FC = ({ hasInterruptionNote || hasRealPrompt || needsConfirmation || - Boolean(taskInput?.reviewerContext) + (Boolean(taskInput?.reviewerContext) && !taskInput?.isReviewCoverageTask) ); const { taskHeaderLine, taskAgentTypeLabel, taskDesc } = useMemo(() => { @@ -450,8 +477,10 @@ export const TaskToolDisplay: React.FC = ({ const raw = taskInput?.agentType; let agentTypeLabel: string; if (raw && raw !== 'Not provided') { - const rc = taskInput?.reviewerContext; - agentTypeLabel = rc + const rc = taskInput?.isReviewCoverageTask ? null : taskInput?.reviewerContext; + agentTypeLabel = taskInput?.isReviewCoverageTask + ? t('toolCards.taskTool.reviewCoverageLabel') + : rc ? tAgents(`reviewTeams.members.${rc.definitionKey}.funName`, { defaultValue: rc.roleName, }) @@ -476,7 +505,7 @@ export const TaskToolDisplay: React.FC = ({ return; } - if (linkedSubagentSessionId && sessionId) { + if (linkedSubagentSessionId && sessionId && !isReviewCoverageTask) { const parentSession = flowChatStore.getState().sessions.get(sessionId); openBtwSessionInAuxPane({ childSessionId: linkedSubagentSessionId, @@ -507,7 +536,7 @@ export const TaskToolDisplay: React.FC = ({ window.dispatchEvent(new CustomEvent('agent-create-tab', { detail: tabInfo })); } }, - [isCancelAction, linkedSubagentSessionId, onOpenInPanel, sessionId, taskInput, toolCall?.id, toolItem, taskHeaderLine], + [isCancelAction, isReviewCoverageTask, linkedSubagentSessionId, onOpenInPanel, sessionId, taskInput, toolCall?.id, toolItem, taskHeaderLine], ); const renderToolIcon = () => { @@ -568,12 +597,16 @@ export const TaskToolDisplay: React.FC = ({ aria-label={ isStoppingSubagent ? t('toolCards.taskDetailPanel.stoppingSubagent') - : t('toolCards.taskDetailPanel.stopSubagent') + : isReviewCoverageTask + ? t('toolCards.taskDetailPanel.stopReviewWork') + : t('toolCards.taskDetailPanel.stopSubagent') } title={ isStoppingSubagent ? t('toolCards.taskDetailPanel.stoppingSubagent') - : t('toolCards.taskDetailPanel.stopSubagent') + : isReviewCoverageTask + ? t('toolCards.taskDetailPanel.stopReviewWork') + : t('toolCards.taskDetailPanel.stopSubagent') } > {isStoppingSubagent ? ( @@ -616,7 +649,7 @@ export const TaskToolDisplay: React.FC = ({ return null; } - const rc = taskInput?.reviewerContext; + const rc = taskInput?.isReviewCoverageTask ? null : taskInput?.reviewerContext; if ( !hasInterruptionNote && @@ -635,7 +668,7 @@ export const TaskToolDisplay: React.FC = ({ {interruptionNote}
- {(hasRealPrompt || needsConfirmation || taskInput?.reviewerContext) && ( + {(hasRealPrompt || needsConfirmation || rc) && (
)} diff --git a/src/web-ui/src/flow_chat/utils/codeReviewRemediation.test.ts b/src/web-ui/src/flow_chat/utils/codeReviewRemediation.test.ts index a8da159cf..13f1ba81e 100644 --- a/src/web-ui/src/flow_chat/utils/codeReviewRemediation.test.ts +++ b/src/web-ui/src/flow_chat/utils/codeReviewRemediation.test.ts @@ -88,7 +88,7 @@ describe('buildSelectedReviewRemediationPrompt', () => { expect(prompt).toBe(''); }); - it('builds prompt with selected items for deep review', () => { + it('builds prompt with selected items for strict review', () => { const prompt = buildSelectedReviewRemediationPrompt({ reviewData: { summary: { recommended_action: 'request_changes' }, @@ -99,7 +99,7 @@ describe('buildSelectedReviewRemediationPrompt', () => { reviewMode: 'deep', }); - expect(prompt).toContain('Deep Review findings only'); + expect(prompt).toContain('Review: Strict findings only'); expect(prompt).toContain('Fix issue 1'); expect(prompt).toContain('Selected Remediation Plan'); }); @@ -115,8 +115,8 @@ describe('buildSelectedReviewRemediationPrompt', () => { reviewMode: 'deep', }); - expect(prompt).toContain('follow-up deep review'); - expect(prompt).toContain('dispatching all enabled review team reviewers'); + expect(prompt).toContain('follow-up strict review'); + expect(prompt).toContain('assigned read-only reviewers'); }); it('builds prompt with standard review mode', () => { @@ -130,7 +130,7 @@ describe('buildSelectedReviewRemediationPrompt', () => { reviewMode: 'standard', }); - expect(prompt).toContain('Code Review findings only'); + expect(prompt).toContain('Review findings only'); }); it('appends continuation context when completedItems provided', () => { diff --git a/src/web-ui/src/flow_chat/utils/codeReviewRemediation.ts b/src/web-ui/src/flow_chat/utils/codeReviewRemediation.ts index 01ae8ddee..1756069c0 100644 --- a/src/web-ui/src/flow_chat/utils/codeReviewRemediation.ts +++ b/src/web-ui/src/flow_chat/utils/codeReviewRemediation.ts @@ -294,10 +294,10 @@ export function buildSelectedReviewRemediationPrompt(params: { .map((item) => formatIssueForPrompt(item, params.decisionSelections?.[item.id])) .join('\n\n'); const isDeepReview = params.reviewMode === 'deep'; - const reviewLabel = isDeepReview ? 'Deep Review' : 'Code Review'; + const reviewLabel = isDeepReview ? 'Review: Strict' : 'Review'; const rerunInstruction = isDeepReview - ? 'After implementing fixes, run the most relevant verification. Then launch a full follow-up deep review of the fix diff by dispatching all enabled review team reviewers in parallel, followed by ReviewJudge. Submit the follow-up review result via submit_code_review.' - : 'After implementing fixes, run the most relevant verification. Then submit a follow-up standard code review of the fix diff via submit_code_review.'; + ? 'After implementing fixes, run the most relevant verification. Then launch a full follow-up strict review of the fix diff by dispatching the assigned read-only reviewers in parallel, followed by ReviewJudge. Submit the follow-up review result via submit_code_review.' + : 'After implementing fixes, run the most relevant verification. Then submit a follow-up standard review of the fix diff via submit_code_review.'; const lines: string[] = [ `The user approved remediation for selected ${reviewLabel} findings only.`, diff --git a/src/web-ui/src/flow_chat/utils/codeReviewReport.test.ts b/src/web-ui/src/flow_chat/utils/codeReviewReport.test.ts index 73a029925..2990b7edd 100644 --- a/src/web-ui/src/flow_chat/utils/codeReviewReport.test.ts +++ b/src/web-ui/src/flow_chat/utils/codeReviewReport.test.ts @@ -51,7 +51,7 @@ function buildRunManifest(): ReviewTeamRunManifest { strategyRecommendation: { strategyLevel: 'deep', score: 24, - rationale: 'Large/high-risk change (8 files, 900 lines; 2 security-sensitive files, 3 workspace areas). Deep review recommended.', + rationale: 'Large/high-risk change (8 files, 900 lines; 2 security-sensitive files, 3 workspace areas). Strict review recommended.', factors: { fileCount: 8, totalLinesChanged: 900, @@ -296,7 +296,7 @@ describe('codeReviewReport', () => { ]); }); - it('surfaces partial reviewer output in coverage notes', () => { + it('surfaces partial review output in coverage notes', () => { const sections = buildCodeReviewReportSections({ summary: { overall_assessment: 'Review completed with reduced confidence.', @@ -316,7 +316,7 @@ describe('codeReviewReport', () => { expect(sections.reviewerStats).toMatchObject({ total: 1, completed: 0, degraded: 1 }); expect(sections.coverageNotes).toEqual([ - 'Security Reviewer stopped before completion after producing partial output: Found likely token logging in src/auth.ts before timeout.', + 'A review coverage check stopped before completion after producing partial output: Found likely token logging in src/auth.ts before timeout.', ]); }); @@ -398,7 +398,7 @@ describe('codeReviewReport', () => { severity: 'warning', count: 7, source: 'runtime', - detail: 'Runtime profile capped reviewer fan-out for this large target.', + detail: 'Runtime profile reduced review coverage for this large target.', }, { kind: 'compression_preserved', @@ -411,28 +411,28 @@ describe('codeReviewReport', () => { severity: 'info', count: 2, source: 'runtime', - detail: 'Two reviewer packets reused matching cached output.', + detail: 'Two review results reused matching cached output.', }, { kind: 'cache_miss', severity: 'info', count: 1, source: 'runtime', - detail: 'One reviewer packet ran fresh and updated the cache.', + detail: 'One review result ran fresh and updated the cache.', }, { kind: 'concurrency_limited', severity: 'warning', count: 1, source: 'runtime', - detail: 'One reviewer launch hit the configured concurrency cap.', + detail: 'One review launch hit the configured concurrency cap.', }, { kind: 'retry_guidance', severity: 'warning', count: 1, source: 'runtime', - detail: 'Retry guidance was emitted for a partial reviewer.', + detail: 'Retry guidance was emitted for partial review coverage.', }, ], }; @@ -445,7 +445,7 @@ describe('codeReviewReport', () => { severity: 'warning', count: 7, source: 'runtime', - detail: 'Runtime profile capped reviewer fan-out for this large target.', + detail: 'Runtime profile reduced review coverage for this large target.', }, { kind: 'compression_preserved', @@ -458,28 +458,28 @@ describe('codeReviewReport', () => { severity: 'info', count: 2, source: 'runtime', - detail: 'Two reviewer packets reused matching cached output.', + detail: 'Two review results reused matching cached output.', }, { kind: 'cache_miss', severity: 'info', count: 1, source: 'runtime', - detail: 'One reviewer packet ran fresh and updated the cache.', + detail: 'One review result ran fresh and updated the cache.', }, { kind: 'concurrency_limited', severity: 'warning', count: 1, source: 'runtime', - detail: 'One reviewer launch hit the configured concurrency cap.', + detail: 'One review launch hit the configured concurrency cap.', }, { kind: 'retry_guidance', severity: 'warning', count: 1, source: 'runtime', - detail: 'Retry guidance was emitted for a partial reviewer.', + detail: 'Retry guidance was emitted for partial review coverage.', }, ]); @@ -487,29 +487,29 @@ describe('codeReviewReport', () => { expect(markdown).toContain('## Review Reliability'); expect(markdown).toContain( - '- Context pressure rising [warning/runtime]: Runtime profile capped reviewer fan-out for this large target.', + '- Context pressure rising [warning/runtime]: Runtime profile reduced review coverage for this large target.', ); expect(markdown).toContain( '- Compression preserved key facts [info/runtime]: Compression contract retained modified files and failed commands.', ); expect(markdown).toContain( - '- Incremental cache reused reviewer output [info/runtime]: Two reviewer packets reused matching cached output.', + '- Incremental cache reused review output [info/runtime]: Two review results reused matching cached output.', ); expect(markdown).toContain( - '- Incremental cache missed or refreshed [info/runtime]: One reviewer packet ran fresh and updated the cache.', + '- Incremental cache missed or refreshed [info/runtime]: One review result ran fresh and updated the cache.', ); expect(markdown).toContain( - '- Reviewer launch was concurrency-limited [warning/runtime]: One reviewer launch hit the configured concurrency cap.', + '- Review launch was concurrency-limited [warning/runtime]: One review launch hit the configured concurrency cap.', ); expect(markdown).toContain( - '- Retry guidance emitted [warning/runtime]: Retry guidance was emitted for a partial reviewer.', + '- Retry guidance emitted [warning/runtime]: Retry guidance was emitted for partial review coverage.', ); }); - it('summarizes skipped reviewer and token budget tradeoffs from the run manifest', () => { + it('summarizes tailored review scope and token budget tradeoffs from the run manifest', () => { const report = { summary: { - overall_assessment: 'Review completed with one skipped reviewer.', + overall_assessment: 'Review completed with tailored scope.', risk_level: 'medium' as const, recommended_action: 'request_changes' as const, }, @@ -542,11 +542,11 @@ describe('codeReviewReport', () => { const markdown = formatCodeReviewReportMarkdown(report, undefined, { runManifest: buildRunManifest() }); - expect(markdown).toContain('- Skipped reviewers [info/manifest]: Count: 2'); - expect(markdown).toContain('- Token budget limited reviewer coverage [warning/manifest]: Count: 1'); + expect(markdown).toContain('- Review scope tailored [info/manifest]: Count: 2'); + expect(markdown).toContain('- Token budget limited review coverage [warning/manifest]: Count: 1'); }); - it('surfaces reduced-depth scope profile in reliability notices and markdown export', () => { + it('surfaces focused scope profile in reliability notices and markdown export', () => { const report = { summary: { overall_assessment: 'No blocking issues found in the high-risk pass.', @@ -569,14 +569,14 @@ describe('codeReviewReport', () => { const markdown = formatCodeReviewReportMarkdown(report, undefined, { runManifest }); - expect(markdown).toContain('- Review depth: high_risk_only'); - expect(markdown).toContain('- Coverage expectation: High-risk-only pass; changed files remain visible.'); + expect(markdown).not.toContain('- Review depth:'); + expect(markdown).not.toContain('- Coverage expectation:'); expect(markdown).toContain( - '- Reduced-depth coverage [info/manifest]: High-risk-only pass; changed files remain visible.', + '- Focused review scope [info/manifest]: High-risk-only pass; changed files remain visible.', ); }); - it('exports a compact metadata-only evidence pack summary without content', () => { + it('keeps evidence pack metadata out of exported markdown', () => { const report = { summary: { overall_assessment: 'Review completed.', @@ -637,11 +637,12 @@ describe('codeReviewReport', () => { const markdown = formatCodeReviewReportMarkdown(report, undefined, { runManifest }); - expect(markdown).toContain('### Evidence pack'); - expect(markdown).toContain('- Source: target_manifest; privacy: metadata_only'); - expect(markdown).toContain('- Changed files: 1; hunk hints: 1; contract hints: 1; packet ids: 2'); - expect(markdown).toContain('- Omitted metadata: changed files 0, hunk hints 0, contract hints 0'); - expect(markdown).toContain('- Hints are orientation only and require tool confirmation before findings.'); + expect(markdown).not.toContain('### Evidence scope'); + expect(markdown).not.toContain('- Changed files: 1; hunk hints: 1; contract hints: 1'); + expect(markdown).not.toContain('- Hints are orientation only and require tool confirmation before findings.'); + expect(markdown).not.toContain('packet ids'); + expect(markdown).not.toContain('target_manifest'); + expect(markdown).not.toContain('metadata_only'); expect(markdown).not.toContain('source_text'); expect(markdown).not.toContain('full_diff'); expect(markdown).not.toContain('model_output'); @@ -691,16 +692,16 @@ describe('codeReviewReport', () => { reviewers: [{ name: 'Business Logic Reviewer', specialty: 'logic', status: 'completed', summary: 'Found one issue.' }], }); - expect(markdown).toContain('# Deep Review Report'); + expect(markdown).toContain('# Strict Review Report'); expect(markdown).toContain('## Executive Summary'); expect(markdown).toContain('- One fix required.'); expect(markdown).toContain('## Issues'); expect(markdown).toContain('src/auth.ts:12'); expect(markdown).toContain('## Remediation Plan'); - expect(markdown).toContain('## Code Review Team'); + expect(markdown).not.toContain('## Review Execution Plan'); }); - it('exports partial reviewer output in markdown', () => { + it('exports partial review output in markdown', () => { const markdown = formatCodeReviewReportMarkdown({ summary: { overall_assessment: 'Review completed with partial security evidence.', @@ -720,14 +721,14 @@ describe('codeReviewReport', () => { ], }); - expect(markdown).toContain('Security Reviewer (security; Status: partial_timeout)'); - expect(markdown).toContain('Partial output: Found likely token logging in src/auth.ts before timeout.'); + expect(markdown).not.toContain('Security Reviewer (security; Status: partial_timeout)'); + expect(markdown).not.toContain('Partial output: Found likely token logging in src/auth.ts before timeout.'); expect(markdown).toContain( - 'Security Reviewer stopped before completion after producing partial output: Found likely token logging in src/auth.ts before timeout.', + 'A review coverage check stopped before completion after producing partial output: Found likely token logging in src/auth.ts before timeout.', ); }); - it('exports reviewer packet fallback metadata in markdown', () => { + it('keeps internal packet fallback metadata out of exported markdown', () => { const markdown = formatCodeReviewReportMarkdown({ summary: { overall_assessment: 'Review completed with inferred packet metadata.', @@ -748,10 +749,11 @@ describe('codeReviewReport', () => { ], }); - expect(markdown).toContain('Packet: reviewer:ReviewSecurity:group-1-of-3 (inferred)'); + expect(markdown).not.toContain('Security Reviewer (security; Status: completed)'); + expect(markdown).not.toContain('reviewer:ReviewSecurity:group-1-of-3'); }); - it('extracts explicit retry slices for partial timeout reviewers', () => { + it('extracts explicit retry coverage for partial timeout reviewers', () => { const slices = extractDeepReviewRetryableSlices({ review_mode: 'deep', reviewers: [ @@ -823,7 +825,7 @@ describe('codeReviewReport', () => { expect(slices).toEqual([]); }); - it('includes the run manifest when exporting a deep review report', () => { + it('keeps run manifest details out of strict review markdown export', () => { const markdown = formatCodeReviewReportMarkdown( { summary: { @@ -839,26 +841,35 @@ describe('codeReviewReport', () => { { runManifest: buildRunManifest() }, ); - expect(markdown).toContain('## Run manifest'); - expect(markdown).toContain('- Target: frontend'); - expect(markdown).toContain('- Budget: balanced'); - expect(markdown).toContain('- Estimated calls: 3'); - expect(markdown).toContain('- Recommended strategy: deep'); - expect(markdown).toContain('- Recommendation score: 24'); - expect(markdown).toContain('- Recommendation rationale: Large/high-risk change'); - expect(markdown).toContain('- Logic reviewer (ReviewBusinessLogic)'); - expect(markdown).toContain('- Custom security reviewer (CustomSecurity)'); - expect(markdown).toContain('- Quality inspector (ReviewJudge)'); - expect(markdown).toContain('- Frontend reviewer (ReviewFrontend): not_applicable'); - expect(markdown).toContain('- Custom invalid reviewer (CustomInvalid): invalid_tooling'); - expect(markdown).toContain('### Pre-review summary'); - expect(markdown).toContain('- 1 file, 12 changed lines across 1 workspace area: web-ui (1)'); - expect(markdown).toContain('- web-ui: 1 file (src/App.tsx)'); - expect(markdown).toContain('### Shared context cache'); - expect(markdown).toContain('- shared-context:1: src/App.tsx -> reviewer:ReviewBusinessLogic, reviewer:CustomSecurity'); - expect(markdown).toContain('### Incremental review cache'); - expect(markdown).toContain('- Cache key: incremental-review:abc12345'); - expect(markdown).toContain('- Fingerprint: abc12345'); - expect(markdown).toContain('- Invalidates on: target_file_set_changed, target_line_count_changed, reviewer_roster_changed'); + expect(markdown).not.toContain('## Review Coverage And Cost'); + expect(markdown).not.toContain('- Target: frontend'); + expect(markdown).not.toContain('- Budget: balanced'); + expect(markdown).not.toContain('- Estimated review checks: 3'); + expect(markdown).not.toContain('- Recommended strategy: Deep'); + expect(markdown).not.toContain('- Recommendation score: 24'); + expect(markdown).not.toContain('- Recommendation rationale: Large/high-risk change'); + expect(markdown).not.toContain('- Included coverage: 3 coverage areas.'); + expect(markdown).not.toContain('- Review scope tailored: 2 optional checks outside this run.'); + expect(markdown).not.toContain('Logic reviewer - Logic reviewer'); + expect(markdown).not.toContain('Custom security reviewer - Custom security reviewer'); + expect(markdown).not.toContain('Quality inspector - Quality inspector'); + expect(markdown).not.toContain('Frontend reviewer'); + expect(markdown).not.toContain('not_applicable'); + expect(markdown).not.toContain('Custom invalid reviewer'); + expect(markdown).not.toContain('invalid_tooling'); + expect(markdown).not.toContain('### Pre-review summary'); + expect(markdown).not.toContain('- 1 file, 12 changed lines across 1 workspace area: web-ui (1)'); + expect(markdown).not.toContain('- web-ui: 1 file (src/App.tsx)'); + expect(markdown).not.toContain('### Context reuse'); + expect(markdown).not.toContain('- Reused context entries: 1'); + expect(markdown).not.toContain('### Incremental coverage'); + expect(markdown).not.toContain('- Reused review work: 2'); + expect(markdown).not.toContain('- Refresh triggers: target_file_set_changed, target_line_count_changed, reviewer_roster_changed'); + expect(markdown).toContain('- Review scope tailored [info/manifest]: Count: 2'); + expect(markdown).toContain('- Token budget limited review coverage [warning/manifest]: Count: 1'); + expect(markdown).not.toContain('reviewer:ReviewBusinessLogic'); + expect(markdown).not.toContain('shared-context:1'); + expect(markdown).not.toContain('incremental-review:abc12345'); + expect(markdown).not.toContain('Fingerprint: abc12345'); }); }); diff --git a/src/web-ui/src/flow_chat/utils/deepReviewCommandGuard.test.ts b/src/web-ui/src/flow_chat/utils/deepReviewCommandGuard.test.ts index 27c3b9b5b..94d285632 100644 --- a/src/web-ui/src/flow_chat/utils/deepReviewCommandGuard.test.ts +++ b/src/web-ui/src/flow_chat/utils/deepReviewCommandGuard.test.ts @@ -16,17 +16,19 @@ function activity(overrides: Partial = {}): SessionReview } describe('shouldBlockDeepReviewCommand', () => { - it('blocks /DeepReview while the parent session already has a blocking review activity', () => { - expect(shouldBlockDeepReviewCommand('/DeepReview', activity())).toBe(true); + it('blocks strict Review typed commands while the parent session already has a blocking review activity', () => { + expect(shouldBlockDeepReviewCommand('/review strict', activity())).toBe(true); + expect(shouldBlockDeepReviewCommand('/review deep focus on auth', activity())).toBe(false); expect(shouldBlockDeepReviewCommand('/DeepReview focus on auth', activity())).toBe(true); + expect(shouldBlockDeepReviewCommand('/deepreview focus on auth', activity())).toBe(true); }); - it('does not block non-DeepReview input, lowercase aliases, or completed review activity', () => { + it('does not block non-strict Review input or completed review activity', () => { expect(shouldBlockDeepReviewCommand('please review this', activity())).toBe(false); - expect(shouldBlockDeepReviewCommand('/deepreview', activity())).toBe(false); + expect(shouldBlockDeepReviewCommand('/review', activity())).toBe(false); expect( shouldBlockDeepReviewCommand( - '/DeepReview', + '/review strict', activity({ lifecycle: 'completed', isBlocking: false, diff --git a/src/web-ui/src/flow_chat/utils/deepReviewConstants.ts b/src/web-ui/src/flow_chat/utils/deepReviewConstants.ts index 6a9798070..2dd0b1e6e 100644 --- a/src/web-ui/src/flow_chat/utils/deepReviewConstants.ts +++ b/src/web-ui/src/flow_chat/utils/deepReviewConstants.ts @@ -2,5 +2,9 @@ * Shared constants for Deep Review feature to avoid duplication across modules. */ -/** Regex matching the canonical /DeepReview slash command (case-sensitive). */ -export const DEEP_REVIEW_COMMAND_RE = /^\/DeepReview(\s+.*)?$/; +/** Canonical typed command for strict Review. Historical DeepReview forms are compatibility aliases. */ +export const REVIEW_STRICT_SLASH_COMMAND = '/review strict'; +export const DEEP_REVIEW_SLASH_COMMAND_ALIAS = '/DeepReview'; +export const DEEP_REVIEW_COMMAND_RE = /^\/(?:review\s+strict|DeepReview|deepreview)(?:\s+.*)?$/; +export const REVIEW_STRICT_COMMAND_PREFIX_RE = /^\/review\s+strict\b/i; +export const DEEP_REVIEW_COMPAT_COMMAND_PREFIX_RE = /^\/(?:DeepReview|deepreview)\b/; diff --git a/src/web-ui/src/flow_chat/utils/deepReviewContinuation.test.ts b/src/web-ui/src/flow_chat/utils/deepReviewContinuation.test.ts index c422c5ec2..b80f179a0 100644 --- a/src/web-ui/src/flow_chat/utils/deepReviewContinuation.test.ts +++ b/src/web-ui/src/flow_chat/utils/deepReviewContinuation.test.ts @@ -10,7 +10,7 @@ import type { AiErrorDetail } from '@/shared/ai-errors/aiErrorPresenter'; function createDeepReviewSession(overrides: Partial = {}): Session { return { sessionId: 'deep-review-session', - title: 'Deep Review', + title: 'Review: Strict', dialogTurns: [], status: 'idle', config: { @@ -51,7 +51,7 @@ describe('deepReviewContinuation', () => { status: 'error', userMessage: { id: 'user-1', - content: 'Run a deep code review using the parallel Code Review Team.', + content: 'Run a strict code review using the assigned read-only Review execution plan.', timestamp: 1, }, modelRounds: [], @@ -79,7 +79,7 @@ describe('deepReviewContinuation', () => { status: 'error', userMessage: { id: 'user-1', - content: 'Run a deep code review using the parallel Code Review Team.', + content: 'Run a strict code review using the assigned read-only Review execution plan.', timestamp: 1, }, modelRounds: [], @@ -166,8 +166,8 @@ describe('deepReviewContinuation', () => { const interruption = deriveDeepReviewInterruption(session, { category: 'timeout' }); const prompt = buildDeepReviewContinuationPrompt(interruption!); - expect(prompt).toContain('Continue the interrupted Deep Review'); - expect(prompt).toContain('Do not restart completed reviewer work'); + expect(prompt).toContain('Continue the interrupted strict review'); + expect(prompt).toContain('Do not restart completed review work'); expect(prompt).toContain('ReviewPerformance: completed'); expect(prompt).toContain('ReviewSecurity: timed_out'); }); @@ -248,7 +248,7 @@ describe('deepReviewContinuation', () => { status: 'error', userMessage: { id: 'user-1', - content: 'Run a deep review for src/crates/assembly/core/src/lib.rs', + content: 'Run a strict review for src/crates/assembly/core/src/lib.rs', timestamp: 1, }, startTime: 1, @@ -297,7 +297,7 @@ describe('deepReviewContinuation', () => { }), ]); expect(prompt).toContain('ReviewFrontend: skipped'); - expect(prompt).toContain('Do not re-run skipped, non-applicable, or policy-ineligible reviewers'); + expect(prompt).toContain('Do not re-run skipped, non-applicable, or policy-ineligible coverage'); }); it('derives a resumable interruption for manually cancelled deep reviews without model recovery actions', () => { @@ -365,7 +365,7 @@ describe('deepReviewContinuation', () => { expect(actionCodes).not.toContain('switch_model'); expect(actionCodes).not.toContain('wait_and_retry'); expect(prompt).toContain('The previous interruption was requested by the user.'); - expect(prompt).toContain('User cancellation does not consume reviewer retry budget.'); + expect(prompt).toContain('User cancellation does not consume review retry budget.'); expect(prompt).toContain('Do not expose internal retry-budget settings or names'); expect(prompt).toContain('Last interruption:'); expect(prompt).not.toContain('Last error:'); @@ -490,7 +490,7 @@ describe('deepReviewContinuation', () => { expect(prompt).toContain('max_retries_per_role = 1'); expect(prompt).toContain('retry = true'); - expect(prompt).toContain('reduce the scope'); + expect(prompt).toContain('focus the scope'); }); it('includes persisted manifest skips when continuing an interrupted review', () => { @@ -526,7 +526,7 @@ describe('deepReviewContinuation', () => { const interruption = deriveDeepReviewInterruption(session, { category: 'timeout' }); const prompt = buildDeepReviewContinuationPrompt(interruption!); - expect(prompt).toContain('Do not run reviewers skipped as not_applicable.'); + expect(prompt).toContain('Do not run coverage marked not_applicable.'); expect(prompt).toContain('ReviewFrontend: skipped (not_applicable)'); }); @@ -581,8 +581,9 @@ describe('deepReviewContinuation', () => { expect(prompt).toContain('Incremental review cache guidance:'); expect(prompt).toContain('cache_key: incremental-review:abc12345'); expect(prompt).toContain('fingerprint: abc12345'); - expect(prompt).toContain('Only reuse completed reviewer outputs when the current review target fingerprint still matches.'); - expect(prompt).toContain('reviewer:ReviewBusinessLogic'); + expect(prompt).toContain('Only reuse completed review outputs when the current review target fingerprint still matches.'); + expect(prompt).toContain('completed_review_work_count: 2'); + expect(prompt).not.toContain('reviewer:ReviewBusinessLogic'); expect(prompt).toContain('target_file_set_changed'); }); @@ -647,9 +648,9 @@ describe('deepReviewContinuation', () => { 'continue', 'copy_diagnostics', ]); - expect(prompt).toContain('The previous Deep Review ended without a usable structured submit_code_review result.'); - expect(prompt).toContain('First reconstruct and submit the missing final report from preserved reviewer outputs.'); - expect(prompt).toContain('Do not rerun completed reviewer work just to regenerate the report.'); + expect(prompt).toContain('The previous strict review ended without a usable structured submit_code_review result.'); + expect(prompt).toContain('First reconstruct and submit the missing final report from preserved review outputs.'); + expect(prompt).toContain('Do not rerun completed review work just to regenerate the report.'); expect(prompt).toContain('ReviewSecurity: completed'); }); }); diff --git a/src/web-ui/src/flow_chat/utils/deepReviewContinuation.ts b/src/web-ui/src/flow_chat/utils/deepReviewContinuation.ts index 23bd9c25c..0b05cf82a 100644 --- a/src/web-ui/src/flow_chat/utils/deepReviewContinuation.ts +++ b/src/web-ui/src/flow_chat/utils/deepReviewContinuation.ts @@ -51,11 +51,11 @@ const RESUME_BLOCKING_CATEGORIES = new Set([ const RESULT_RECOVERY_MESSAGES: Record = { missing_submit_code_review: - 'Deep Review completed, but BitFun did not receive a structured submit_code_review result.', + 'Strict review completed, but BitFun did not receive a structured submit_code_review result.', invalid_submit_code_review: - 'Deep Review submitted a structured result that BitFun could not read.', + 'Strict review submitted a structured result that BitFun could not read.', wrong_review_mode: - 'Deep Review submitted a standard Code Review result instead of a Deep Review result.', + 'Strict review submitted a standard Review result instead of a strict review result.', }; export function deriveDeepReviewInterruption( @@ -76,7 +76,7 @@ export function deriveDeepReviewInterruption( const fallbackMessage = session.error ?? lastTurn?.error ?? - (wasManuallyCancelled ? 'Deep Review was stopped by the user.' : ''); + (wasManuallyCancelled ? 'Strict review was stopped by the user.' : ''); const effectiveErrorDetail = errorDetail ?? (wasManuallyCancelled @@ -147,18 +147,18 @@ export function buildDeepReviewContinuationPrompt(interruption: DeepReviewInterr return `- ${reviewer.reviewer}: ${reviewer.status}${suffix}${partialOutput}`; }) .join('\n') - : '- No reliable reviewer progress was detected. Reconstruct progress from this session before deciding what to rerun.'; + : '- No reliable review progress was detected. Reconstruct progress from this session before deciding what to rerun.'; const skippedReviewers = interruption.runManifest?.skippedReviewers ?? []; const manifestSkippedReviewers = formatManifestSkippedReviewers(skippedReviewers); const manifestRules = skippedReviewers.some((reviewer) => reviewer.reason === 'not_applicable') ? [ - '- Do not run reviewers skipped as not_applicable.', + '- Do not run coverage marked not_applicable.', ] : []; const manifestBlock = manifestSkippedReviewers.length ? [ '', - 'Run manifest reviewer skips:', + 'Review scope selected by the prior review plan:', manifestSkippedReviewers.join('\n'), ] : []; @@ -170,37 +170,37 @@ export function buildDeepReviewContinuationPrompt(interruption: DeepReviewInterr ); const manualCancelRules = wasManuallyCancelled ? [ - '- The previous interruption was requested by the user. Treat it as a user stop/pause, not as a model failure or reviewer timeout.', - '- Preserve completed reviewer output and continue only unfinished reviewer work where enough context exists.', + '- The previous interruption was requested by the user. Treat it as a user stop/pause, not as a model failure or review timeout.', + '- Preserve completed review output and continue only unfinished review work where enough context exists.', ] : []; const resultRecoveryRules = interruption.resultRecoveryReason ? [ - '- The previous Deep Review ended without a usable structured submit_code_review result.', - '- First reconstruct and submit the missing final report from preserved reviewer outputs.', - '- Do not rerun completed reviewer work just to regenerate the report.', - '- If preserved reviewer output is insufficient, rerun only missing, failed, timed-out, or cancelled reviewers before submitting the report.', + '- The previous strict review ended without a usable structured submit_code_review result.', + '- First reconstruct and submit the missing final report from preserved review outputs.', + '- Do not rerun completed review work just to regenerate the report.', + '- If preserved review output is insufficient, rerun only missing, failed, timed-out, or cancelled review work before submitting the report.', ] : []; return [ - 'Continue the interrupted Deep Review in this same session.', + 'Continue the interrupted strict review in this same session.', '', 'Recovery rules:', ...manualCancelRules, ...resultRecoveryRules, - '- Do not restart completed reviewer work unless the existing result is clearly incomplete or unusable.', - '- Do not re-run skipped, non-applicable, or policy-ineligible reviewers; keep them recorded as skipped coverage.', + '- Do not restart completed review work unless the existing result is clearly incomplete or unusable.', + '- Do not re-run skipped, non-applicable, or policy-ineligible coverage; keep it recorded as skipped coverage.', ...retryBudgetRules, ...manifestRules, - '- Re-run only missing, failed, timed-out, or cancelled reviewers when enough context exists.', - '- If reviewer coverage remains incomplete, say that explicitly and mark the final report as lower confidence.', - '- Run ReviewJudge before the final submit_code_review result when reviewer findings exist.', + '- Re-run only missing, failed, timed-out, or cancelled review work when enough context exists.', + '- If review coverage remains incomplete, say that explicitly and mark the final report as lower confidence.', + '- Run ReviewJudge before the final submit_code_review result when findings exist.', '', 'Original review target:', interruption.originalTarget, '', - 'Known reviewer progress:', + 'Known review progress:', reviewerLines, ...manifestBlock, ...incrementalCacheBlock, @@ -222,10 +222,10 @@ function formatIncrementalReviewCacheGuidance( `- cache_key: ${cachePlan.cacheKey}`, `- fingerprint: ${cachePlan.fingerprint}`, `- strategy: ${cachePlan.strategy}`, - `- reviewer_packet_ids: ${cachePlan.reviewerPacketIds.join(', ') || 'none'}`, + `- completed_review_work_count: ${cachePlan.reviewerPacketIds.length}`, `- invalidates_on: ${cachePlan.invalidatesOn.join(', ') || 'none'}`, - '- Only reuse completed reviewer outputs when the current review target fingerprint still matches.', - '- If any invalidates_on condition changed, rerun affected reviewer packets and explain the fresh review boundary.', + '- Only reuse completed review outputs when the current review target fingerprint still matches.', + '- If any invalidates_on condition changed, rerun affected review work and explain the fresh review boundary.', ]; } @@ -234,37 +234,37 @@ function formatRetryBudgetRules( ): string[] { const maxRetriesPerRole = runManifest?.executionPolicy?.maxRetriesPerRole; const baseRules = [ - '- Treat partial_timeout reviewers as preserved partial evidence. Re-run them only when useful evidence is missing or unusable.', + '- Treat partial_timeout review work as preserved partial evidence. Re-run it only when useful evidence is missing or unusable.', ]; if (typeof maxRetriesPerRole !== 'number') { return [ ...baseRules, - '- Respect the original retry budget if it is recoverable from context; do not retry the same reviewer repeatedly.', + '- Respect the original retry budget if it is recoverable from context; do not retry the same review work repeatedly.', ]; } if (maxRetriesPerRole <= 0) { return [ ...baseRules, - '- Retry budget from manifest: max_retries_per_role = 0. Do not re-run failed, timed-out, or partial reviewers automatically; report remaining gaps instead.', + '- Retry budget from manifest: max_retries_per_role = 0. Do not re-run failed, timed-out, or partial review work automatically; report remaining gaps instead.', ]; } return [ ...baseRules, `- Retry budget from manifest: max_retries_per_role = ${maxRetriesPerRole}.`, - '- For each retry, use the same subagent_type with retry = true, reduce the scope to missing evidence, downgrade strategy when possible, and use a shorter timeout.', + '- For each retry, use the same subagent_type with retry = true, focus the scope on missing evidence, use a lower-cost strategy when possible, and use a shorter timeout.', ]; } function formatManualCancelRetryBudgetRules(): string[] { return [ - '- Treat partial_timeout reviewers as preserved partial evidence. Re-run them only when useful evidence is missing or unusable.', - '- User cancellation does not consume reviewer retry budget.', + '- Treat partial_timeout review work as preserved partial evidence. Re-run it only when useful evidence is missing or unusable.', + '- User cancellation does not consume review retry budget.', '- Do not expose internal retry-budget settings or names such as max retry counts in the user-facing reply.', - '- An initial timed-out or cancelled reviewer attempt does not by itself mean reviewer retry budget is exhausted.', - '- Use retry=true only when the Task tool permits structured retry_coverage for partial_timeout or transient capacity failure. Otherwise continue missing or user-cancelled reviewer work as normal continuation work when possible, or report reduced coverage without internal budget jargon.', + '- An initial timed-out or cancelled review attempt does not by itself mean review retry budget is exhausted.', + '- Use retry=true only when the Task tool permits structured retry_coverage for partial_timeout or transient capacity failure. Otherwise continue missing or user-cancelled review work as normal continuation work when possible, or report scope limits without internal budget jargon.', ]; } @@ -302,7 +302,7 @@ function formatManifestSkippedReviewers( function findOriginalTarget(session: Session): string { const firstTurn = session.dialogTurns[0]; - return firstTurn?.userMessage?.content?.trim() || 'Unknown Deep Review target.'; + return firstTurn?.userMessage?.content?.trim() || 'Unknown strict review target.'; } export function collectReviewerProgress(session: Session): DeepReviewReviewerProgress[] { diff --git a/src/web-ui/src/infrastructure/config/components/ReviewConfig.test.tsx b/src/web-ui/src/infrastructure/config/components/ReviewConfig.test.tsx new file mode 100644 index 000000000..b690d7e55 --- /dev/null +++ b/src/web-ui/src/infrastructure/config/components/ReviewConfig.test.tsx @@ -0,0 +1,274 @@ +// @vitest-environment jsdom + +import React, { act } from 'react'; +import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'; +import { createRoot, type Root } from 'react-dom/client'; +import ReviewConfig from './ReviewConfig'; + +const loadDefaultReviewTeamMock = vi.hoisted(() => vi.fn()); +const saveDefaultReviewTeamStrategyLevelMock = vi.hoisted(() => vi.fn()); +const saveDefaultReviewTeamConcurrencyPolicyMock = vi.hoisted(() => vi.fn()); +const notifySuccessMock = vi.hoisted(() => vi.fn()); +const notifyErrorMock = vi.hoisted(() => vi.fn()); +const translateMock = vi.hoisted(() => (key: string, params?: Record) => { + const translations: Record = { + title: 'Review', + subtitle: 'Review chooses the right depth for each request without exposing reviewer orchestration.', + 'overview.title': 'Review entry', + 'overview.description': 'Use /review for normal and strict review requests.', + 'overview.command.title': '/review', + 'overview.command.description': 'One command adapts to task size, risk, and requested care.', + 'overview.reviewers.title': 'Adaptive coverage', + 'overview.reviewers.description': 'BitFun chooses enough independent checks for the current task.', + 'overview.qualityGate.title': 'Quality gate', + 'overview.qualityGate.description': 'Findings stay adversarial and evidence based.', + 'strategy.title': 'Review depth', + 'strategy.description': 'Set the default balance between cost, latency, and confidence.', + 'strategy.quick.label': 'Quick', + 'strategy.quick.summary': 'Fast check for small changes.', + 'strategy.normal.label': 'Standard', + 'strategy.normal.summary': 'Balanced review for typical changes.', + 'strategy.deep.label': 'Strict', + 'strategy.deep.summary': 'Higher confidence for larger or riskier changes.', + 'capacity.title': 'Capacity', + 'capacity.description': 'Limit parallel Review work so cost and latency stay predictable.', + 'capacity.maxParallelReviewers.label': 'Parallel checks', + 'capacity.maxParallelReviewers.description': 'Higher values may start more model requests in parallel.', + 'capacity.maxQueueWaitSeconds.label': 'Queue wait', + 'capacity.maxQueueWaitSeconds.description': 'Maximum time Review waits for capacity.', + 'shared:features.deepReview': 'Review', + 'messages.saved': 'Saved', + 'messages.loadFailed': 'Failed to load Review settings.', + 'messages.saveFailed': 'Failed to save Review settings.', + }; + if (key in translations) return translations[key]; + return key; +}); + +vi.mock('react-i18next', () => ({ + initReactI18next: { + type: '3rdParty', + init: vi.fn(), + }, + useTranslation: () => ({ + t: translateMock, + }), +})); + +vi.mock('@/component-library', () => ({ + Badge: ({ children }: { children: React.ReactNode }) => {children}, + Button: ({ + children, + disabled, + onClick, + }: { + children: React.ReactNode; + disabled?: boolean; + onClick?: () => void; + }) => ( + + ), + ConfigPageLoading: ({ text }: { text: string }) =>
{text}
, + NumberInput: ({ + disabled, + value, + onChange, + }: { + disabled?: boolean; + value: number; + onChange: (value: number) => void; + }) => ( + onChange(Number(event.currentTarget.value))} + /> + ), +})); + +vi.mock('./common', () => ({ + ConfigPageContent: ({ children }: { children: React.ReactNode }) =>
{children}
, + ConfigPageHeader: ({ title, subtitle }: { title: string; subtitle: string }) => ( +
+

{title}

+

{subtitle}

+
+ ), + ConfigPageLayout: ({ children }: { children: React.ReactNode }) =>
{children}
, + ConfigPageRow: ({ + children, + description, + label, + }: { + children: React.ReactNode; + description: string; + label: string; + }) => ( + + ), + ConfigPageSection: ({ + children, + description, + title, + titleSuffix, + }: { + children: React.ReactNode; + description?: string; + title: string; + titleSuffix?: React.ReactNode; + }) => ( +
+

{title}

+ {titleSuffix} + {description ?

{description}

: null} + {children} +
+ ), +})); + +vi.mock('@/infrastructure/contexts/WorkspaceContext', () => ({ + useCurrentWorkspace: () => ({ workspacePath: 'D:/workspace/project' }), +})); + +vi.mock('@/shared/notification-system', () => ({ + useNotification: () => ({ + success: notifySuccessMock, + error: notifyErrorMock, + }), +})); + +vi.mock('@/shared/services/reviewTeamService', () => ({ + REVIEW_STRATEGY_LEVELS: ['quick', 'normal', 'deep'], + loadDefaultReviewTeam: loadDefaultReviewTeamMock, + saveDefaultReviewTeamConcurrencyPolicy: saveDefaultReviewTeamConcurrencyPolicyMock, + saveDefaultReviewTeamStrategyLevel: saveDefaultReviewTeamStrategyLevelMock, +})); + +function createReviewTeam() { + const coreMember = { + id: 'member-logic', + subagentId: 'ReviewBusinessLogic', + displayName: 'Logic Reviewer', + roleName: 'Business Logic Reviewer', + definitionKey: 'businessLogic', + source: 'core', + locked: true, + model: 'fast', + strategyLevel: 'normal', + strategyOverride: 'inherit', + strategySource: 'team', + }; + const extraMember = { + id: 'member-extra', + subagentId: 'ReviewDocs', + displayName: 'Docs Reviewer', + roleName: 'Documentation Reviewer', + source: 'extra', + locked: false, + model: 'fast', + strategyLevel: 'normal', + strategyOverride: 'inherit', + strategySource: 'team', + }; + + return { + id: 'default-review-team', + name: 'Strict Review Coverage', + description: 'Strict review coverage.', + warning: 'Review may take longer.', + strategyLevel: 'normal', + memberStrategyOverrides: {}, + executionPolicy: {}, + concurrencyPolicy: { + maxParallelInstances: 2, + maxQueueWaitSeconds: 300, + providerCapacityQueueEnabled: true, + autoRetryEnabled: true, + autoRetryElapsedGuardSeconds: 120, + }, + members: [coreMember, extraMember], + coreMembers: [coreMember], + extraMembers: [extraMember], + }; +} + +async function flushPromises() { + await act(async () => { + await Promise.resolve(); + }); +} + +describe('ReviewConfig', () => { + let container: HTMLDivElement; + let root: Root; + + beforeEach(() => { + container = document.createElement('div'); + document.body.appendChild(container); + root = createRoot(container); + vi.clearAllMocks(); + + loadDefaultReviewTeamMock.mockResolvedValue(createReviewTeam()); + saveDefaultReviewTeamStrategyLevelMock.mockResolvedValue(undefined); + saveDefaultReviewTeamConcurrencyPolicyMock.mockResolvedValue(undefined); + }); + + afterEach(() => { + act(() => { + root.unmount(); + }); + container.remove(); + }); + + it('keeps strict Review settings as the active configuration surface', async () => { + await act(async () => { + root.render(); + }); + await flushPromises(); + + expect(container.textContent).toContain('Review'); + expect(container.textContent).toContain('/review'); + expect(container.textContent).toContain('Review depth'); + expect(container.textContent).toContain('Capacity'); + expect(container.textContent).not.toContain('DeepReview'); + expect(container.textContent).not.toContain('Sub-Agent'); + expect(container.textContent).not.toContain('members.title'); + expect(container.textContent).not.toContain('extra.title'); + expect(container.textContent).not.toContain('team management'); + expect(container.textContent).not.toContain('orchestration controls'); + }); + + it('saves strategy and capacity changes', async () => { + await act(async () => { + root.render(); + }); + await flushPromises(); + + const strictStrategyButton = Array.from(container.querySelectorAll('button')) + .find((button) => button.textContent?.includes('Strict')); + expect(strictStrategyButton).toBeTruthy(); + await act(async () => { + strictStrategyButton!.dispatchEvent(new MouseEvent('click', { bubbles: true })); + }); + expect(saveDefaultReviewTeamStrategyLevelMock).toHaveBeenCalledWith('deep'); + + const numberInputs = Array.from(container.querySelectorAll('input[type="number"]')); + expect(numberInputs).toHaveLength(2); + await act(async () => { + const valueSetter = Object.getOwnPropertyDescriptor(window.HTMLInputElement.prototype, 'value')?.set; + valueSetter?.call(numberInputs[0], '4'); + numberInputs[0].dispatchEvent(new Event('input', { bubbles: true })); + await Promise.resolve(); + }); + expect(saveDefaultReviewTeamConcurrencyPolicyMock).toHaveBeenCalledWith( + expect.objectContaining({ maxParallelInstances: 4 }), + ); + }); +}); diff --git a/src/web-ui/src/infrastructure/config/components/ReviewConfig.tsx b/src/web-ui/src/infrastructure/config/components/ReviewConfig.tsx index 20ef06cc1..cbf07cee3 100644 --- a/src/web-ui/src/infrastructure/config/components/ReviewConfig.tsx +++ b/src/web-ui/src/infrastructure/config/components/ReviewConfig.tsx @@ -1,6 +1,6 @@ -import React, { useCallback, useEffect, useMemo, useState } from 'react'; +import React, { useCallback, useEffect, useState } from 'react'; import { useTranslation } from 'react-i18next'; -import { Badge, Button, ConfigPageLoading, NumberInput, Select } from '@/component-library'; +import { Badge, ConfigPageLoading, NumberInput } from '@/component-library'; import { ConfigPageContent, ConfigPageHeader, @@ -8,127 +8,41 @@ import { ConfigPageRow, ConfigPageSection, } from './common'; -import type { AIModelConfig } from '../types'; -import { getModelDisplayName } from '../services/modelConfigs'; -import { configAPI } from '@/infrastructure/api/service-api/ConfigAPI'; -import { SubagentAPI, type SubagentInfo } from '@/infrastructure/api/service-api/SubagentAPI'; import { useCurrentWorkspace } from '@/infrastructure/contexts/WorkspaceContext'; import { useNotification } from '@/shared/notification-system'; import { - addDefaultReviewTeamMember, - canUseSubagentAsReviewTeamMember, - DEFAULT_REVIEW_MEMBER_STRATEGY_LEVEL, - DEFAULT_REVIEW_TEAM_MODEL, loadDefaultReviewTeam, - removeDefaultReviewTeamMember, REVIEW_STRATEGY_LEVELS, saveDefaultReviewTeamConcurrencyPolicy, - saveDefaultReviewTeamMemberStrategyOverride, saveDefaultReviewTeamStrategyLevel, - type ReviewMemberStrategyLevel, type ReviewStrategyLevel, type ReviewTeam, type ReviewTeamConcurrencyPolicy, - type ReviewTeamMember, } from '@/shared/services/reviewTeamService'; import './ReviewConfig.scss'; -const MEMBER_STRATEGY_OPTIONS: ReviewMemberStrategyLevel[] = [ - DEFAULT_REVIEW_MEMBER_STRATEGY_LEVEL, - ...REVIEW_STRATEGY_LEVELS, -]; - -function updateMemberStrategy( - member: ReviewTeamMember, - strategyOverride: ReviewMemberStrategyLevel, - teamStrategyLevel: ReviewStrategyLevel, -): ReviewTeamMember { - const inheritsTeamStrategy = strategyOverride === DEFAULT_REVIEW_MEMBER_STRATEGY_LEVEL; - return { - ...member, - strategyOverride, - strategyLevel: inheritsTeamStrategy ? teamStrategyLevel : strategyOverride, - strategySource: inheritsTeamStrategy ? 'team' : 'member', - }; -} - function updateTeamStrategy(team: ReviewTeam, strategyLevel: ReviewStrategyLevel): ReviewTeam { - const memberStrategyOverrides: Record = {}; - const members = team.members.map((member) => { - if (member.strategyOverride === DEFAULT_REVIEW_MEMBER_STRATEGY_LEVEL) { - return updateMemberStrategy(member, DEFAULT_REVIEW_MEMBER_STRATEGY_LEVEL, strategyLevel); - } - memberStrategyOverrides[member.subagentId] = member.strategyOverride; - return member; - }); - return { ...team, strategyLevel, - memberStrategyOverrides, - members, - coreMembers: members.filter((member) => member.source === 'core'), - extraMembers: members.filter((member) => member.source === 'extra'), - }; -} - -function updateTeamMemberStrategy( - team: ReviewTeam, - memberId: string, - strategyLevel: ReviewMemberStrategyLevel, -): ReviewTeam { - const members = team.members.map((member) => - member.id === memberId - ? updateMemberStrategy(member, strategyLevel, team.strategyLevel) - : member, - ); - const memberStrategyOverrides: Record = {}; - members.forEach((member) => { - if (member.strategyOverride !== DEFAULT_REVIEW_MEMBER_STRATEGY_LEVEL) { - memberStrategyOverrides[member.subagentId] = member.strategyOverride; - } - }); - - return { - ...team, - memberStrategyOverrides, - members, - coreMembers: members.filter((member) => member.source === 'core'), - extraMembers: members.filter((member) => member.source === 'extra'), }; } const ReviewConfig: React.FC = () => { const { t } = useTranslation('settings/review'); - const { t: tModel } = useTranslation('settings/default-model'); const { workspacePath } = useCurrentWorkspace(); const { error: notifyError, success: notifySuccess } = useNotification(); const [loading, setLoading] = useState(true); const [team, setTeam] = useState(null); - const [models, setModels] = useState([]); - const [subagents, setSubagents] = useState([]); - const [candidateId, setCandidateId] = useState(''); const [savingConcurrencyKey, setSavingConcurrencyKey] = useState(null); - const [savingMemberId, setSavingMemberId] = useState(null); const [savingStrategyTarget, setSavingStrategyTarget] = useState(null); - const [addingMember, setAddingMember] = useState(false); - const [removingMemberId, setRemovingMemberId] = useState(null); const loadData = useCallback(async () => { setLoading(true); try { - const [loadedTeam, loadedModels, loadedSubagents] = await Promise.all([ - loadDefaultReviewTeam(workspacePath || undefined), - configAPI.getConfig('ai.models'), - SubagentAPI.listVisibleSubagents({ - workspacePath: workspacePath || undefined, - parentAgentType: 'DeepReview', - }), - ]); + const loadedTeam = await loadDefaultReviewTeam(workspacePath || undefined); setTeam(loadedTeam); - setModels(Array.isArray(loadedModels) ? loadedModels as AIModelConfig[] : []); - setSubagents(loadedSubagents); } catch (error) { notifyError(error instanceof Error ? error.message : t('messages.loadFailed')); } finally { @@ -140,16 +54,6 @@ const ReviewConfig: React.FC = () => { void loadData(); }, [loadData]); - const getMemberName = useCallback((member: ReviewTeamMember) => { - if (!member.definitionKey) return member.displayName; - return t(`members.${member.definitionKey}.name`, { defaultValue: member.roleName }); - }, [t]); - - const getMemberRole = useCallback((member: ReviewTeamMember) => { - if (!member.definitionKey) return t('extra.role', { defaultValue: member.roleName }); - return t(`members.${member.definitionKey}.role`, { defaultValue: member.roleName }); - }, [t]); - const getStrategyLabel = useCallback((level: ReviewStrategyLevel) => ( t(`strategy.${level}.label`) ), [t]); @@ -158,56 +62,6 @@ const ReviewConfig: React.FC = () => { t(`strategy.${level}.summary`) ), [t]); - const getMemberStrategyLabel = useCallback((level: ReviewMemberStrategyLevel) => { - if (level === DEFAULT_REVIEW_MEMBER_STRATEGY_LEVEL) { - return t('strategy.inherit', { - level: team ? getStrategyLabel(team.strategyLevel) : '', - }); - } - return getStrategyLabel(level); - }, [getStrategyLabel, t, team]); - - const formatModelLabel = useCallback((modelId: string): string => { - if (!modelId || modelId === DEFAULT_REVIEW_TEAM_MODEL) { - return tModel('selection.fast'); - } - if (modelId === 'primary') { - return tModel('selection.primary'); - } - - const match = models.find((model) => model.id === modelId); - return match ? getModelDisplayName(match) : modelId; - }, [models, tModel]); - - const extraCandidates = useMemo(() => { - if (!team) return []; - const existingIds = new Set(team.members.map((member) => member.subagentId)); - - return subagents - .filter((subagent) => !existingIds.has(subagent.id)) - .filter(canUseSubagentAsReviewTeamMember) - .sort((left, right) => left.name.localeCompare(right.name) || left.id.localeCompare(right.id)); - }, [subagents, team]); - - const modelOptions = useMemo(() => [ - { value: DEFAULT_REVIEW_TEAM_MODEL, label: tModel('selection.fast') }, - { value: 'primary', label: tModel('selection.primary') }, - ...models - .filter((model) => Boolean(model.id)) - .map((model) => ({ - value: model.id as string, - label: getModelDisplayName(model), - })), - ], [models, tModel]); - - useEffect(() => { - setCandidateId((currentId) => - currentId && extraCandidates.some((candidate) => candidate.id === currentId) - ? currentId - : extraCandidates[0]?.id ?? '', - ); - }, [extraCandidates]); - const handleTeamStrategyChange = useCallback(async (strategyLevel: ReviewStrategyLevel) => { if (!team || team.strategyLevel === strategyLevel) return; @@ -224,26 +78,6 @@ const ReviewConfig: React.FC = () => { } }, [loadData, notifyError, notifySuccess, t, team]); - const handleMemberStrategyChange = useCallback(async ( - member: ReviewTeamMember, - strategyLevel: ReviewMemberStrategyLevel, - ) => { - if (!team || member.strategyOverride === strategyLevel) return; - - const target = `member:${member.id}`; - setSavingStrategyTarget(target); - setTeam(updateTeamMemberStrategy(team, member.id, strategyLevel)); - try { - await saveDefaultReviewTeamMemberStrategyOverride(member.subagentId, strategyLevel); - notifySuccess(t('messages.saved')); - } catch (error) { - await loadData(); - notifyError(error instanceof Error ? error.message : t('messages.saveFailed')); - } finally { - setSavingStrategyTarget(null); - } - }, [loadData, notifyError, notifySuccess, t, team]); - const handleConcurrencyPolicyChange = useCallback(async ( key: keyof ReviewTeamConcurrencyPolicy, value: ReviewTeamConcurrencyPolicy[keyof ReviewTeamConcurrencyPolicy], @@ -267,73 +101,6 @@ const ReviewConfig: React.FC = () => { } }, [loadData, notifyError, notifySuccess, t, team]); - const handleModelChange = useCallback(async (member: ReviewTeamMember, modelId: string) => { - if (!team) return; - - setSavingMemberId(member.id); - const nextTeam: ReviewTeam = { - ...team, - members: team.members.map((m) => - m.id === member.id ? { ...m, model: modelId } : m, - ), - coreMembers: team.coreMembers.map((m) => - m.id === member.id ? { ...m, model: modelId } : m, - ), - extraMembers: team.extraMembers.map((m) => - m.id === member.id ? { ...m, model: modelId } : m, - ), - }; - setTeam(nextTeam); - try { - await SubagentAPI.updateSubagentConfig({ - subagentId: member.subagentId, - model: modelId, - workspacePath: workspacePath || undefined, - }); - notifySuccess(t('messages.saved')); - } catch (error) { - await loadData(); - notifyError(error instanceof Error ? error.message : t('messages.saveFailed')); - } finally { - setSavingMemberId(null); - } - }, [loadData, notifyError, notifySuccess, t, team, workspacePath]); - - const handleAddMember = useCallback(async () => { - if (!candidateId) return; - setAddingMember(true); - try { - await addDefaultReviewTeamMember(candidateId); - await SubagentAPI.updateSubagentConfig({ - subagentId: candidateId, - parentAgentType: 'DeepReview', - enabled: true, - workspacePath: workspacePath || undefined, - }); - await loadData(); - notifySuccess(t('messages.memberAdded')); - } catch (error) { - notifyError(error instanceof Error ? error.message : t('messages.saveFailed')); - } finally { - setAddingMember(false); - } - }, [candidateId, loadData, notifyError, notifySuccess, t, workspacePath]); - - const handleRemoveMember = useCallback(async (member: ReviewTeamMember) => { - if (member.locked) return; - - setRemovingMemberId(member.id); - try { - await removeDefaultReviewTeamMember(member.subagentId); - await loadData(); - notifySuccess(t('messages.memberRemoved')); - } catch (error) { - notifyError(error instanceof Error ? error.message : t('messages.saveFailed')); - } finally { - setRemovingMemberId(null); - } - }, [loadData, notifyError, notifySuccess, t]); - if (loading || !team) { return ( @@ -353,7 +120,6 @@ const ReviewConfig: React.FC = () => { {t('shared:features.deepReview')}} >
@@ -423,95 +189,6 @@ const ReviewConfig: React.FC = () => { - - {t('members.count', { count: team.members.length })}} - > -
- {team.members.map((member) => ( -
-
-
- {getMemberName(member)} - - {member.locked ? t('members.locked') : t('members.extra')} - -
-

{getMemberRole(member)}

-
- -
- void handleModelChange( - member, - String(Array.isArray(value) ? value[0] || DEFAULT_REVIEW_TEAM_MODEL : value), - )} - /> - {formatModelLabel(member.model)} - {!member.locked ? ( - - ) : null} -
-
- ))} -
-
- - - -
-