Skip to content
Merged
28 changes: 28 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@
- **Themes** — Dark (Tokyo Night), Light, Soft Light, and Ultra Dark — terminal theme syncs automatically
- **Custom buttons** — Configurable action buttons that run shell commands or Claude prompts with placement, scope, and shortcut options
- **Session startup prompts** — Pre-configure per-project prompts (e.g. `/notion-ticket {{input}}`) that auto-run in a new session's agent terminal
- **Review loop** — One-click review → triage → fix cycle on a branch with stop conditions (clean rounds, iteration cap, cost cap) and a sticky PR comment for skipped findings
- **Claude Web sessions** — Surface your own `claude/*` branches from Claude Code on the web in the sidebar; click to open them locally as worktrees
- **Keyboard navigable** — Full keyboard support: arrow keys, focus trapping, roving tabindex, accessible by default

Expand Down Expand Up @@ -362,6 +363,33 @@ Create configurable action buttons that run shell commands or Claude prompts fro

</details>

<details>
<summary><strong>Review loop</strong></summary>

Automate the review → triage → fix cycle on a branch. Each round runs three phases against the branch's diff vs. its base:

1. **Review** — A fresh Claude reviews the diff and writes structured findings to `.crucible/review-loop/round-N-issues.json`.
2. **Triage** — Another Claude reads those findings and fans out a sub-agent per issue. Each sub-agent decides `fix` / `skip` / `defer` / `noop` and writes a short justification.
3. **Fix** — Claude applies the fixes, commits, and pushes.

The loop stops on the first of: N consecutive clean rounds (default 2), iteration cap (default 5), cost cap (default $5), or manual cancel. Workspace defaults and per-project overrides for all four live in **Settings → Review Loop**, including a per-project toggle that hides the toolbar button and prevents the loop from running for that scope.

Skipped or deferred items get summarised in a single sticky comment on the open PR (using a hidden marker so subsequent rounds update the same comment instead of re-posting). That gives reviewers a record of what was knowingly left undone and why.

The Review Loop tab in the session workspace shows live progress: status pill, current phase, cumulative cost, per-round triage decisions, and a per-round log.

<table>
<tr>
<td><img src="docs/screenshots/review-loop-running.png" alt="Review Loop tab while a loop is mid-triage" /></td>
<td><img src="docs/screenshots/review-loop-completed.png" alt="Review Loop tab after the loop converged" /></td>
</tr>
<tr>
<td colspan="2"><img src="docs/screenshots/review-loop-settings.png" alt="Workspace defaults and per-project overrides for the review loop" /></td>
</tr>
</table>

</details>

<details>
<summary><strong>Permissions sync</strong></summary>

Expand Down
Binary file modified docs/screenshots/button-settings.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file modified docs/screenshots/code-attention.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file modified docs/screenshots/custom-buttons.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file modified docs/screenshots/editor-branch-picker.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file modified docs/screenshots/editor-worktree.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file modified docs/screenshots/editor.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file modified docs/screenshots/git-diff.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file modified docs/screenshots/hero.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file modified docs/screenshots/new-session-dialog-with-input.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file modified docs/screenshots/new-session-dialog.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file modified docs/screenshots/opened-as-main-branch.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file modified docs/screenshots/pr-attention.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file modified docs/screenshots/pr-review-contextmenu-changedfiles.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file modified docs/screenshots/pr-review-contextmenu-fileexplorer.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file modified docs/screenshots/pr-review-inlinethread-open.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file modified docs/screenshots/pr-review-inlinethread-resolved.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file modified docs/screenshots/pr-review-inlinethread-suggestion.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file modified docs/screenshots/pr-review-reviewers-empty.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file modified docs/screenshots/pr-review-reviewers-mixed.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file modified docs/screenshots/pr-review-reviewers-pending-only.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file modified docs/screenshots/pr-review-suggestion-disabled.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file modified docs/screenshots/pr-review-suggestion-multiline.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file modified docs/screenshots/pr-review-suggestion-singleline.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file modified docs/screenshots/pr-review.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file modified docs/screenshots/pr-sort-filter-menu-active.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file modified docs/screenshots/pr-sort-filter-menu-people.png
Binary file modified docs/screenshots/pr-sort-filter-menu.png
Binary file added docs/screenshots/review-loop-completed.png
Binary file added docs/screenshots/review-loop-running.png
Binary file added docs/screenshots/review-loop-settings.png
Binary file modified docs/screenshots/sessions.png
Binary file modified docs/screenshots/settings.png
Binary file modified docs/screenshots/startup-prompt-editor.png
Binary file modified docs/screenshots/startup-prompt-settings.png
Binary file modified docs/screenshots/tab-attention.png
Binary file modified docs/screenshots/theme-dark.png
Binary file modified docs/screenshots/theme-light.png
Binary file modified docs/screenshots/theme-soft-light.png
Binary file modified docs/screenshots/theme-ultra-dark.png
19 changes: 19 additions & 0 deletions mock/mockApi.ts
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,8 @@ import {
mockButtons,
mockButtonGroups,
mockStartupPrompts,
mockReviewLoopSettings,
mockReviewLoopRunning,
} from './mockData'

// Collect terminal.onData callbacks so we can push fake output
Expand Down Expand Up @@ -214,6 +216,23 @@ export const mockApi = {
save: async () => {},
},

reviewLoop: {
getSettings: async () => mockReviewLoopSettings,
setSettings: async () => {},
start: async () => {},
cancel: async () => {},
/**
* Stories can override the returned state by setting
* `(window as any).__mockReviewLoopState = state` before mount.
* Falls back to the running snapshot.
*/
getState: async () => {
const override = (globalThis as any).__mockReviewLoopState
return override ?? mockReviewLoopRunning
},
onStateUpdate: () => noop(),
},

claudeWeb: {
listSessions: async () => [
{
Expand Down
204 changes: 204 additions & 0 deletions mock/mockData.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,8 @@ import type {
CustomButton,
CustomButtonGroup,
StartupPrompt,
ReviewLoopState,
ReviewLoopSettings,
} from '../src/shared/types'

// --- Accounts ---
Expand Down Expand Up @@ -639,3 +641,205 @@ export const mockStartupPrompts: Record<string, StartupPrompt[]> = {
],
'proj-2': [],
}

// --- Review loop ---

export const mockReviewLoopSettings: ReviewLoopSettings = {
workspace: {
enabled: true,
maxIterations: 5,
consecutiveCleanRounds: 2,
costCapUsd: 5,
},
projectOverrides: {
'proj-2': { enabled: false },
},
}

const baseTime = Date.parse('2026-04-30T13:00:00Z')

/**
* A representative "running" review loop snapshot — round 1 has finished
* (with 2 fixes applied + 1 deferred), round 2 is mid-triage. Designed to
* exercise every UI element in the panel: status pill, cost, decisions,
* skipped/deferred summary, log.
*/
export const mockReviewLoopRunning: ReviewLoopState = {
sessionId: 'sess-1',
branch: 'session/add-pr-review',
baseBranch: 'main',
worktreePath: '/Users/dev/.codecrucible-worktrees/CodeCrucible/add-pr-review',
status: 'running',
currentPhase: 'triage',
iteration: 2,
cumulativeCostUsd: 1.42,
startedAt: new Date(baseTime).toISOString(),
skippedIssues: [
{
id: 'r1-3',
title: 'Duplicate retry logic in pr.service vs git.service',
description: 'Both files re-implement exponential backoff. Could be extracted but is pre-existing code.',
file: 'src/main/services/pr.service.ts',
line: 88,
category: 'style',
introducedInPR: false,
decision: 'defer',
justification: 'Pre-existing duplication on main; out of scope for this PR. File a follow-up to extract a shared backoff helper.',
},
],
rounds: [
{
index: 1,
startedAt: new Date(baseTime + 1_000).toISOString(),
endedAt: new Date(baseTime + 92_000).toISOString(),
phase: 'idle',
costUsd: 0.84,
log: [
'[2026-04-30T13:00:01Z] Reviewing diff between session/add-pr-review and main…',
'[2026-04-30T13:00:34Z] Review found 3 candidate issues.',
'[2026-04-30T13:00:34Z] Triaging 3 issues (one sub-agent each)…',
'[2026-04-30T13:01:18Z] Triage complete: 2 to fix, 1 skipped/deferred.',
'[2026-04-30T13:01:18Z] Applying fixes, committing, and pushing…',
'[2026-04-30T13:01:32Z] Fix phase complete.',
],
rawIssues: [
{
id: 'r1-1',
title: 'Race condition between PR list refresh and selection',
description: 'When refresh fires while the user is mid-click, the previous selection can land on a removed PR.',
file: 'src/renderer/stores/prStore.ts',
line: 142,
category: 'bug',
},
{
id: 'r1-2',
title: 'Missing await on github.markPRSeen',
description: 'Promise rejection silently swallowed; seen state never persists if the API errors.',
file: 'src/renderer/components/pullrequests/PRListItem.tsx',
line: 56,
category: 'bug',
},
{
id: 'r1-3',
title: 'Duplicate retry logic in pr.service vs git.service',
description: 'Both files re-implement exponential backoff. Could be extracted but is pre-existing code.',
file: 'src/main/services/pr.service.ts',
line: 88,
category: 'style',
},
],
triaged: [
{
id: 'r1-1',
title: 'Race condition between PR list refresh and selection',
description: 'When refresh fires while the user is mid-click, the previous selection can land on a removed PR.',
file: 'src/renderer/stores/prStore.ts',
line: 142,
category: 'bug',
introducedInPR: true,
decision: 'fix',
justification: 'Real bug introduced by the new refresh polling. Guard the selection update against the in-flight click.',
},
{
id: 'r1-2',
title: 'Missing await on github.markPRSeen',
description: 'Promise rejection silently swallowed; seen state never persists if the API errors.',
file: 'src/renderer/components/pullrequests/PRListItem.tsx',
line: 56,
category: 'bug',
introducedInPR: true,
decision: 'fix',
justification: 'Confirmed missing await — added in this PR. Awaiting and surfacing the error keeps state consistent.',
},
{
id: 'r1-3',
title: 'Duplicate retry logic in pr.service vs git.service',
description: 'Both files re-implement exponential backoff. Could be extracted but is pre-existing code.',
file: 'src/main/services/pr.service.ts',
line: 88,
category: 'style',
introducedInPR: false,
decision: 'defer',
justification: 'Pre-existing duplication on main; out of scope for this PR. File a follow-up to extract a shared backoff helper.',
},
],
},
{
index: 2,
startedAt: new Date(baseTime + 95_000).toISOString(),
phase: 'triage',
costUsd: 0.58,
log: [
'[2026-04-30T13:01:35Z] Reviewing diff between session/add-pr-review and main…',
'[2026-04-30T13:02:03Z] Review found 2 candidate issues.',
'[2026-04-30T13:02:03Z] Triaging 2 issues (one sub-agent each)…',
],
rawIssues: [
{
id: 'r2-1',
title: 'PR review thread loader leaks listener on unmount',
description: 'effect adds an event listener but the cleanup is keyed off the wrong dependency.',
file: 'src/renderer/components/pullrequests/PRReviewPanel.tsx',
line: 211,
category: 'bug',
},
{
id: 'r2-2',
title: 'Inline thread CSS class typo',
description: '`text-sucess` should be `text-success`; falls back to default colour.',
file: 'src/renderer/components/pullrequests/InlineThread.tsx',
line: 47,
category: 'style',
},
],
triaged: [],
},
],
}

/** A "completed" snapshot — converged after 2 clean rounds following round 1. */
export const mockReviewLoopCompleted: ReviewLoopState = {
...mockReviewLoopRunning,
status: 'completed',
currentPhase: 'idle',
iteration: 3,
cumulativeCostUsd: 1.64,
endedAt: new Date(baseTime + 240_000).toISOString(),
stopReason: 'converged',
rounds: [
// Reuse round 1 from the running mock (with its 2 fixes + 1 defer)…
mockReviewLoopRunning.rounds[0],
// …then two clean rounds that triggered the converged stop.
{
index: 2,
startedAt: new Date(baseTime + 95_000).toISOString(),
endedAt: new Date(baseTime + 165_000).toISOString(),
phase: 'idle',
costUsd: 0.41,
log: [
'[2026-04-30T13:01:35Z] Reviewing diff between session/add-pr-review and main…',
'[2026-04-30T13:02:08Z] Review found 0 candidate issues.',
'[2026-04-30T13:02:08Z] No issues to triage; skipping.',
'[2026-04-30T13:02:08Z] No fixable issues in this round (1 clean round so far).',
],
rawIssues: [],
triaged: [],
},
{
index: 3,
startedAt: new Date(baseTime + 170_000).toISOString(),
endedAt: new Date(baseTime + 240_000).toISOString(),
phase: 'idle',
costUsd: 0.39,
log: [
'[2026-04-30T13:02:50Z] Reviewing diff between session/add-pr-review and main…',
'[2026-04-30T13:03:24Z] Review found 0 candidate issues.',
'[2026-04-30T13:03:24Z] No issues to triage; skipping.',
'[2026-04-30T13:03:24Z] No fixable issues in this round (2 clean rounds so far).',
],
rawIssues: [],
triaged: [],
},
],
}

22 changes: 21 additions & 1 deletion scripts/capture-screenshots.ts
Original file line number Diff line number Diff line change
Expand Up @@ -145,6 +145,22 @@ const targets: ScreenshotTarget[] = [
delay: 2000,
scrollTo: 'Session Startup Prompts',
},
{
name: 'review-loop-running',
storyId: 'app-full-layout--review-loop-running',
delay: 2000,
},
{
name: 'review-loop-completed',
storyId: 'app-full-layout--review-loop-completed',
delay: 2000,
},
{
name: 'review-loop-settings',
storyId: 'app-full-layout--review-loop-settings',
delay: 2000,
scrollTo: 'Review Loop',
},
{
name: 'code-attention',
storyId: 'app-full-layout--code-attention',
Expand Down Expand Up @@ -275,7 +291,11 @@ const targets: ScreenshotTarget[] = [
async function captureScreenshots() {
fs.mkdirSync(OUTPUT_DIR, { recursive: true })

const browser = await chromium.launch()
const launchOptions: Parameters<typeof chromium.launch>[0] = {}
if (process.env.CHROME_BIN) {
launchOptions.executablePath = process.env.CHROME_BIN
}
const browser = await chromium.launch(launchOptions)

// Capture main targets
for (const target of targets) {
Expand Down
2 changes: 2 additions & 0 deletions src/main/ipc/register.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ import { registerFileHandlers } from './file.ipc'
import { registerPermissionsHandlers } from './permissions.ipc'
import { registerButtonHandlers } from './button.ipc'
import { registerStartupPromptHandlers } from './startup-prompt.ipc'
import { registerReviewLoopHandlers } from './review-loop.ipc'
import { registerClaudeWebHandlers } from './claudeWeb.ipc'
import {
registerContextMapping,
Expand All @@ -34,6 +35,7 @@ export function registerAllHandlers(window: BrowserWindow) {
registerPermissionsHandlers()
registerButtonHandlers(window)
registerStartupPromptHandlers()
registerReviewLoopHandlers(window)
registerClaudeWebHandlers()

// Context mapping management for notification routing.
Expand Down
Loading
Loading