critique 0.1.39 → 0.1.41

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/CHANGELOG.md CHANGED
@@ -1,3 +1,16 @@
1
+ # 0.1.41
2
+
3
+ - `review` command:
4
+ - Filter `--resume` reviews by current working directory (only shows reviews from cwd or subdirectories)
5
+ - Use ACP `unstable_listSessions` for OpenCode instead of parsing JSON files directly
6
+ - Falls back to file-based parsing for Claude Code when ACP method unavailable
7
+ - Add instruction to always close code blocks before new text (fixes unclosed diagram blocks)
8
+
9
+ # 0.1.40
10
+
11
+ - `review` command:
12
+ - Increased session/review picker limits from 10/20 to 25 for both ACP sessions and `--resume`
13
+
1
14
  # 0.1.39
2
15
 
3
16
  - `review` command:
package/package.json CHANGED
@@ -2,7 +2,7 @@
2
2
  "name": "critique",
3
3
  "module": "src/diff.tsx",
4
4
  "type": "module",
5
- "version": "0.1.39",
5
+ "version": "0.1.41",
6
6
  "private": false,
7
7
  "bin": "./src/cli.tsx",
8
8
  "scripts": {
@@ -1,7 +1,7 @@
1
1
  #!/usr/bin/env bun
2
- // Preview script for ReviewApp component
3
- // Run with: bun run scripts/preview-review.tsx
4
- // Web mode: bun run scripts/preview-review.tsx --web
2
+ // Development preview script for testing ReviewApp styles without running AI.
3
+ // Renders example hunks and review data to preview TUI appearance.
4
+ // Run with: bun run scripts/preview-review.tsx (TUI) or --web (HTML upload).
5
5
 
6
6
  import { createCliRenderer } from "@opentui/core"
7
7
  import { createRoot } from "@opentui/react"
package/src/ansi-html.ts CHANGED
@@ -1,3 +1,7 @@
1
+ // ANSI terminal output to HTML converter for web preview generation.
2
+ // Uses ghostty-opentui to parse PTY output and generates responsive HTML documents
3
+ // with proper font scaling to fit terminal content within viewport width.
4
+
1
5
  import { ptyToJson, StyleFlags, type TerminalData, type TerminalLine, type TerminalSpan } from "ghostty-opentui"
2
6
 
3
7
  export interface AnsiToHtmlOptions {
package/src/cli.tsx CHANGED
@@ -1,4 +1,8 @@
1
1
  #!/usr/bin/env bun
2
+ // CLI entrypoint for the critique diff viewer.
3
+ // Provides TUI diff viewing, AI-powered review generation, and web preview upload.
4
+ // Commands: default (diff), review (AI analysis), web (HTML upload), pick (cherry-pick files).
5
+
2
6
  import { cac } from "cac";
3
7
  import {
4
8
  createRoot,
@@ -338,7 +342,7 @@ async function runReviewMode(
338
342
  return `${days}d ago`;
339
343
  };
340
344
 
341
- // Filter out critique-generated sessions and ACP sessions, limit to first 10
345
+ // Filter out critique-generated sessions and ACP sessions, limit to first 25
342
346
  const filteredSessions = sessions
343
347
  .filter((s) => {
344
348
  // Filter by _meta if the agent supports it
@@ -350,7 +354,7 @@ async function runReviewMode(
350
354
  if (title.includes("review a git diff")) return false
351
355
  return true
352
356
  })
353
- .slice(0, 10);
357
+ .slice(0, 25);
354
358
 
355
359
  // Non-TTY mode: log available sessions for agents to use with --session
356
360
  if (!process.stdin.isTTY) {
@@ -667,19 +671,19 @@ async function runResumeMode(options: ResumeModeOptions) {
667
671
 
668
672
  let reviewId = options.reviewId;
669
673
 
670
- // If no ID provided, show select
674
+ // If no ID provided, show select (filtered to current cwd and children)
671
675
  if (!reviewId) {
672
- const reviews = listReviews();
676
+ const reviews = listReviews(process.cwd());
673
677
 
674
678
  if (reviews.length === 0) {
675
- clack.log.warn("No saved reviews found");
679
+ clack.log.warn("No saved reviews found for this directory");
676
680
  clack.outro("");
677
681
  process.exit(0);
678
682
  }
679
683
 
680
684
  const selected = await clack.select({
681
685
  message: "Select a review to display",
682
- options: reviews.slice(0, 20).map((r) => {
686
+ options: reviews.slice(0, 25).map((r) => {
683
687
  // Show status and time in label to avoid layout shifts (hints only show on focus)
684
688
  const status = r.status === "in_progress" ? pc.default.yellow(" (in progress)") : "";
685
689
  const time = formatTimeAgo(r.updatedAt);
@@ -1,4 +1,6 @@
1
- // Shared DiffView component for rendering git diffs with syntax highlighting
1
+ // Shared DiffView component for rendering git diffs with syntax highlighting.
2
+ // Wraps opentui's <diff> element with theme-aware colors and syntax styles.
3
+ // Supports split and unified view modes with line numbers.
2
4
 
3
5
  import * as React from "react"
4
6
  import { SyntaxStyle } from "@opentui/core"
@@ -1,2 +1,4 @@
1
- // Shared components
1
+ // Shared React components for the TUI interface.
2
+ // Exports reusable components used across main diff view and review mode.
3
+
2
4
  export { DiffView, type DiffViewProps } from "./diff-view.tsx"
package/src/diff-utils.ts CHANGED
@@ -1,4 +1,6 @@
1
- // Shared utilities for diff processing between CLI commands
1
+ // Shared utilities for git diff processing across CLI commands.
2
+ // Builds git commands, parses diff files, detects filetypes for syntax highlighting,
3
+ // and provides helpers for unified/split view mode selection.
2
4
 
3
5
  export const IGNORED_FILES = [
4
6
  "pnpm-lock.yaml",
package/src/dropdown.tsx CHANGED
@@ -1,3 +1,7 @@
1
+ // Searchable dropdown component for file and theme selection in TUI.
2
+ // Supports keyboard navigation, fuzzy search filtering, and mouse interaction.
3
+ // Used by main diff view for file picker and theme picker overlays.
4
+
1
5
  import React, { useState, useEffect, useRef, useCallback, type ReactNode } from "react";
2
6
  import { useKeyboard } from "@opentui/react";
3
7
  import { TextAttributes, TextareaRenderable } from "@opentui/core";
package/src/logger.ts CHANGED
@@ -1,4 +1,6 @@
1
- // Debug logger that writes to console and app.log when DEBUG=true
1
+ // Debug logger that writes to console and app.log when DEBUG=true.
2
+ // Provides log levels (log, info, warn, error, debug) with timestamps.
3
+ // Disabled in production; enable with DEBUG=true or DEBUG=1 environment variable.
2
4
 
3
5
  import fs from "fs"
4
6
  import { join } from "path"
package/src/monochrome.ts CHANGED
@@ -1,3 +1,7 @@
1
+ // Monochrome theme generator for creating grayscale color schemes.
2
+ // Generates VS Code-compatible themes by blending between background and foreground.
3
+ // Supports various contrast levels from subtle to amplified.
4
+
1
5
  type VSCodeTheme = {
2
6
  name: string;
3
7
  type: "light" | "dark";
package/src/monotone.ts CHANGED
@@ -1,3 +1,7 @@
1
+ // Monotone theme generator for creating single-hue color schemes.
2
+ // Generates VS Code-compatible themes from a base hue with configurable saturation.
3
+ // Used for creating consistent color palettes across light and dark variants.
4
+
1
5
  type VSCodeTheme = {
2
6
  name: string;
3
7
  type: "light" | "dark";
@@ -1,4 +1,6 @@
1
- // ACP client for connecting to AI agents (opencode or claude)
1
+ // ACP (Agent Client Protocol) client for communicating with AI coding assistants.
2
+ // Supports OpenCode and Claude Code agents for session listing, content loading,
3
+ // and creating AI-powered review sessions with streaming updates.
2
4
 
3
5
  import {
4
6
  ClientSideConnection,
@@ -157,100 +159,66 @@ export class AcpClient {
157
159
 
158
160
  /**
159
161
  * List sessions for the given working directory
160
- * Uses CLI command for opencode, parses JSONL files for claude
162
+ * Uses ACP unstable_listSessions, falls back to file parsing for Claude
163
+ * @param cwd - Working directory to filter sessions by
164
+ * @param limit - Maximum number of sessions to return (default: 10)
161
165
  */
162
- async listSessions(cwd: string): Promise<SessionInfo[]> {
163
- if (this.agent === "opencode") {
164
- return this.listOpencodeSessions(cwd)
165
- } else {
166
- return this.listClaudeSessions(cwd)
166
+ async listSessions(cwd: string, limit = 10): Promise<SessionInfo[]> {
167
+ await this.ensureConnected()
168
+ if (!this.client) {
169
+ throw new Error("Client connection failed")
167
170
  }
168
- }
169
171
 
170
- /**
171
- * List OpenCode sessions by reading directly from storage
172
- * Storage location: ~/.local/share/opencode/storage/
173
- */
174
- private async listOpencodeSessions(cwd: string): Promise<SessionInfo[]> {
175
- const storageDir = path.join(os.homedir(), ".local", "share", "opencode", "storage")
176
- const projectsDir = path.join(storageDir, "project")
177
- const sessionsDir = path.join(storageDir, "session")
172
+ // Try ACP unstable_listSessions first
173
+ try {
174
+ const sessions: SessionInfo[] = []
175
+ let cursor: string | undefined
178
176
 
179
- if (!fs.existsSync(projectsDir)) {
180
- return []
181
- }
177
+ // Paginate until we have enough sessions or no more results
178
+ while (sessions.length < limit) {
179
+ const response = await this.client.unstable_listSessions({
180
+ cwd,
181
+ cursor,
182
+ })
182
183
 
183
- // Find ALL project IDs for the given cwd (there can be multiple)
184
- const projectIds: string[] = []
185
- try {
186
- const projectFiles = fs.readdirSync(projectsDir).filter(f => f.endsWith(".json"))
187
- for (const file of projectFiles) {
188
- try {
189
- const content = fs.readFileSync(path.join(projectsDir, file), "utf-8")
190
- const project = JSON.parse(content) as { id: string; worktree: string }
191
- if (project.worktree === cwd) {
192
- projectIds.push(project.id)
193
- }
194
- } catch {
195
- // Skip invalid project files
196
- }
197
- }
198
- } catch (e) {
199
- logger.debug("Failed to scan opencode projects", { error: e })
200
- return []
201
- }
184
+ // Convert ACP SessionInfo to our local type
185
+ for (const acpSession of response.sessions) {
186
+ if (sessions.length >= limit) break
202
187
 
203
- if (projectIds.length === 0) {
204
- return []
205
- }
188
+ sessions.push({
189
+ sessionId: acpSession.sessionId,
190
+ cwd: acpSession.cwd,
191
+ title: acpSession.title ?? undefined,
192
+ // Convert ISO 8601 string to timestamp (milliseconds)
193
+ updatedAt: acpSession.updatedAt ? new Date(acpSession.updatedAt).getTime() : undefined,
194
+ _meta: acpSession._meta,
195
+ })
196
+ }
206
197
 
207
- // Read sessions from ALL matching projects
208
- const sessions: SessionInfo[] = []
209
- for (const projectId of projectIds) {
210
- const projectSessionsDir = path.join(sessionsDir, projectId)
211
- if (!fs.existsSync(projectSessionsDir)) {
212
- continue
198
+ // Check if there are more pages
199
+ if (!response.nextCursor) break
200
+ cursor = response.nextCursor
213
201
  }
214
202
 
215
- try {
216
- const sessionFiles = fs.readdirSync(projectSessionsDir).filter(f => f.endsWith(".json"))
217
- for (const file of sessionFiles) {
218
- try {
219
- const content = fs.readFileSync(path.join(projectSessionsDir, file), "utf-8")
220
- const session = JSON.parse(content) as {
221
- id: string
222
- title?: string
223
- directory: string
224
- time: { created: number; updated: number }
225
- _meta?: { [key: string]: unknown } | null
226
- }
227
- sessions.push({
228
- sessionId: session.id,
229
- cwd: session.directory,
230
- title: session.title,
231
- updatedAt: session.time.updated,
232
- _meta: session._meta,
233
- })
234
- } catch {
235
- // Skip invalid session files
236
- }
237
- }
238
- } catch (e) {
239
- logger.debug("Failed to read opencode sessions for project", { projectId, error: e })
203
+ // Sessions should already be sorted by the server, but ensure descending order
204
+ return sessions.sort((a, b) => (b.updatedAt || 0) - (a.updatedAt || 0))
205
+ } catch (error) {
206
+ // Fall back to file-based listing for agents that don't support listSessions
207
+ logger.debug("ACP listSessions not supported, falling back to file parsing", { error })
208
+ if (this.agent === "claude") {
209
+ return this.listClaudeSessions(cwd, limit)
240
210
  }
211
+ throw error
241
212
  }
242
-
243
- // Filter to exact cwd match and sort by updatedAt descending
244
- return sessions
245
- .filter(s => s.cwd === cwd)
246
- .sort((a, b) => (b.updatedAt || 0) - (a.updatedAt || 0))
247
213
  }
248
214
 
249
215
  /**
250
- * List Claude Code sessions by parsing JSONL files
216
+ * List Claude Code sessions by parsing JSONL files (fallback)
251
217
  * Sessions are stored in ~/.claude/projects/<path-encoded>/
218
+ * @param cwd - Working directory to filter sessions by
219
+ * @param limit - Maximum number of sessions to return
252
220
  */
253
- private async listClaudeSessions(cwd: string): Promise<SessionInfo[]> {
221
+ private async listClaudeSessions(cwd: string, limit: number): Promise<SessionInfo[]> {
254
222
  // Claude stores sessions in ~/.claude/projects/ with path encoded (/ -> -)
255
223
  const claudeDir = path.join(os.homedir(), ".claude", "projects")
256
224
  const encodedPath = cwd.replace(/\//g, "-")
@@ -313,8 +281,10 @@ export class AcpClient {
313
281
  return []
314
282
  }
315
283
 
316
- // Sort by updatedAt descending (most recent first)
317
- return sessions.sort((a, b) => (b.updatedAt || 0) - (a.updatedAt || 0))
284
+ // Sort by updatedAt descending (most recent first) and apply limit
285
+ return sessions
286
+ .sort((a, b) => (b.updatedAt || 0) - (a.updatedAt || 0))
287
+ .slice(0, limit)
318
288
  }
319
289
 
320
290
  /**
@@ -552,7 +522,8 @@ HOW TO EXPLAIN - Diagrams First, Text Last
552
522
  ═══════════════════════════════════════════════════════════════════════════════
553
523
 
554
524
  PREFER ASCII DIAGRAMS - they explain better than words.
555
- ALWAYS wrap diagrams in \`\`\`diagram code blocks - never render them as plain text:
525
+ ALWAYS wrap diagrams in \`\`\`diagram code blocks - never render them as plain text.
526
+ CRITICAL: Always close each code block with \`\`\` before any new text or heading. Never leave code blocks unclosed.
556
527
 
557
528
  \`\`\`diagram
558
529
  ┌─────────────┐ ┌─────────────┐ ┌────────────┐
@@ -1,5 +1,6 @@
1
- // Format ACP session notifications for streaming display
2
- // Shows agent activity while waiting for YAML to be parsed
1
+ // Formatter for ACP session notifications into displayable stream lines.
2
+ // Accumulates consecutive chunks (thinking, messages) and formats tool calls
3
+ // with symbols and colors for the streaming display component.
3
4
 
4
5
  import type { SessionNotification } from "@agentclientprotocol/sdk"
5
6
 
@@ -1,5 +1,6 @@
1
- // Diagram parser - parses ASCII/Unicode diagrams into colored segments
2
- // Used to highlight structural characters (boxes, arrows, lines) differently from text
1
+ // ASCII/Unicode diagram parser for syntax highlighting in markdown code blocks.
2
+ // Separates structural characters (box-drawing, arrows) from text content
3
+ // to render diagrams with muted structural elements and highlighted labels.
3
4
 
4
5
  /**
5
6
  * A segment of text with a specific color type
@@ -1,4 +1,6 @@
1
- // Parse git diff into indexed hunks for AI review
1
+ // Git diff parser that creates indexed hunks for AI review processing.
2
+ // Supports hunk splitting for progressive disclosure, coverage tracking,
3
+ // and generates context XML for AI prompts with cat -n style line numbers.
2
4
 
3
5
  import type { IndexedHunk, HunkCoverage, ReviewCoverage, UncoveredPortion, ReviewGroup } from "./types.ts"
4
6
  import { IGNORED_FILES } from "../diff-utils.ts"
@@ -1,5 +1,6 @@
1
- // Review module - AI-powered diff review using ACP
2
- // Exports for use in CLI
1
+ // AI-powered diff review module using Agent Client Protocol (ACP).
2
+ // Coordinates with OpenCode or Claude Code to generate progressive disclosure reviews.
3
+ // Re-exports all review functionality for use by the CLI.
3
4
 
4
5
  export {
5
6
  parseHunksWithIds,
@@ -1281,6 +1281,230 @@ Added logger import.`,
1281
1281
 
1282
1282
 
1283
1283
 
1284
+ (1 section) t theme run with --web to share & collaborate
1285
+
1286
+ "
1287
+ `)
1288
+ })
1289
+
1290
+ it("should document behavior with unclosed diagram code block (AI-generated malformed markdown)", async () => {
1291
+ // This test documents the behavior when AI generates malformed markdown with an unclosed
1292
+ // diagram code block. The first \`\`\`diagram is never closed before **The Solution:**
1293
+ // appears, causing the parser to treat everything as one code block.
1294
+ //
1295
+ // ROOT CAUSE: The AI generated markdown like this:
1296
+ // **The Problem:**
1297
+ // \`\`\`diagram
1298
+ // ...first diagram...
1299
+ // **The Solution:** <-- MISSING closing \`\`\` before this!
1300
+ // \`\`\`diagram
1301
+ // ...second diagram...
1302
+ // \`\`\`
1303
+ //
1304
+ // RESULT: marked parser correctly follows markdown spec - everything from first
1305
+ // \`\`\`diagram to the final \`\`\` becomes ONE code block, including:
1306
+ // - "**The Solution:**" as literal text (not bold)
1307
+ // - "\`\`\`diagram" as literal text (not a new code block)
1308
+ //
1309
+ // FIX: AI should generate proper markdown with closing \`\`\` after each diagram.
1310
+ const bugHunk = createHunk(1, "src/config.ts", 0, 1, 1, [
1311
+ "+export const x = 1",
1312
+ ])
1313
+
1314
+ // MALFORMED markdown - first diagram missing closing backticks
1315
+ const malformedMarkdown = `## ActionPanel captures actions to zustand, ActionsDialog renders them
1316
+
1317
+ **The Problem:**
1318
+ \`\`\`diagram
1319
+ BEFORE: Context lost when rendering in dialog
1320
+ +-----------------------+ +------------------+
1321
+ | ListItem | push() | DialogOverlay |
1322
+ | (has useNavigation, | -------> | (different React |
1323
+ | useFormContext, etc) | | tree, no access |
1324
+ +-----------------------+ | to contexts) |
1325
+ +------------------+
1326
+
1327
+ **The Solution:**
1328
+ \`\`\`diagram
1329
+ AFTER: Closures preserve context
1330
+ +------------------------+ capture +----------------+
1331
+ | ListItem | ---------> | zustand |
1332
+ | <Offscreen> | execute() | capturedActions|
1333
+ | <ActionPanel> | closures +-------+--------+
1334
+ | <Action execute={ | |
1335
+ | () => push(...) | <----- closure | read
1336
+ | }/> | retains context v
1337
+ | </ActionPanel> | +----------------+
1338
+ | </Offscreen> | | ActionsDialog |
1339
+ +------------------------+ | (calls execute)|
1340
+ +----------------+
1341
+ \`\`\`
1342
+
1343
+ \`ActionsDialog\` groups actions by section.`
1344
+
1345
+ const bugReviewData: ReviewYaml = {
1346
+ hunks: [{
1347
+ hunkIds: [1],
1348
+ markdownDescription: malformedMarkdown,
1349
+ }],
1350
+ }
1351
+
1352
+ testSetup = await testRender(
1353
+ <ReviewAppView
1354
+ hunks={[bugHunk]}
1355
+ reviewData={bugReviewData}
1356
+ isGenerating={false}
1357
+ themeName="github"
1358
+ width={100}
1359
+ />,
1360
+ { width: 100, height: 50 },
1361
+ )
1362
+
1363
+ await testSetup.renderOnce()
1364
+ const frame = testSetup.captureCharFrame()
1365
+ // Documents the incorrect rendering when markdown is malformed:
1366
+ // - "**The Solution:**" appears as literal text inside code block
1367
+ // - "\`\`\`diagram" appears as literal text
1368
+ // - Both diagrams merge into one big code block
1369
+ expect(frame).toMatchInlineSnapshot(`
1370
+ "
1371
+ ActionPanel captures actions to zustand, ActionsDialog renders them
1372
+
1373
+ The Problem:
1374
+
1375
+ BEFORE: Context lost when rendering in dialog
1376
+ +-----------------------+ +------------------+
1377
+ | ListItem | push() | DialogOverlay |
1378
+ | (has useNavigation, | -------> | (different React |
1379
+ | useFormContext, etc) | | tree, no access |
1380
+ +-----------------------+ | to contexts) |
1381
+ +------------------+
1382
+
1383
+ **The Solution:**
1384
+ \`\`\`diagram
1385
+ AFTER: Closures preserve context
1386
+ +------------------------+ capture +----------------+
1387
+ | ListItem | ---------> | zustand |
1388
+ | <Offscreen> | execute() | capturedActions|
1389
+ | <ActionPanel> | closures +-------+--------+
1390
+ | <Action execute={ | |
1391
+ | () => push(...) | <----- closure | read
1392
+ | }/> | retains context v
1393
+ | </ActionPanel> | +----------------+
1394
+ | </Offscreen> | | ActionsDialog |
1395
+ +------------------------+ | (calls execute)|
1396
+ +----------------+
1397
+
1398
+ ActionsDialog groups actions by section.
1399
+
1400
+
1401
+ rc/config.ts +1-0
1402
+
1403
+ + export const x = 1
1404
+
1405
+
1406
+
1407
+
1408
+
1409
+
1410
+
1411
+
1412
+
1413
+
1414
+
1415
+
1416
+
1417
+
1418
+ (1 section) t theme run with --web to share & collaborate
1419
+
1420
+ "
1421
+ `)
1422
+ })
1423
+
1424
+ it("should render TWO separate diagrams when markdown is properly formatted", async () => {
1425
+ // This test shows the CORRECT behavior when markdown has proper closing backticks
1426
+ const goodHunk = createHunk(1, "src/config.ts", 0, 1, 1, [
1427
+ "+export const x = 1",
1428
+ ])
1429
+
1430
+ // CORRECT markdown - each diagram properly closed with \`\`\`
1431
+ const correctMarkdown = `## ActionPanel captures actions to zustand
1432
+
1433
+ **The Problem:**
1434
+ \`\`\`diagram
1435
+ BEFORE: Context lost
1436
+ +-----------+ +-----------+
1437
+ | ListItem |-->| Dialog |
1438
+ +-----------+ +-----------+
1439
+ \`\`\`
1440
+
1441
+ **The Solution:**
1442
+ \`\`\`diagram
1443
+ AFTER: Closures preserve
1444
+ +-----------+ +-----------+
1445
+ | Offscreen |-->| zustand |
1446
+ +-----------+ +-----------+
1447
+ \`\`\`
1448
+
1449
+ \`ActionsDialog\` groups actions by section.`
1450
+
1451
+ const goodReviewData: ReviewYaml = {
1452
+ hunks: [{
1453
+ hunkIds: [1],
1454
+ markdownDescription: correctMarkdown,
1455
+ }],
1456
+ }
1457
+
1458
+ testSetup = await testRender(
1459
+ <ReviewAppView
1460
+ hunks={[goodHunk]}
1461
+ reviewData={goodReviewData}
1462
+ isGenerating={false}
1463
+ themeName="github"
1464
+ width={80}
1465
+ />,
1466
+ { width: 80, height: 35 },
1467
+ )
1468
+
1469
+ await testSetup.renderOnce()
1470
+ const frame = testSetup.captureCharFrame()
1471
+ // With proper markdown:
1472
+ // - "**The Problem:**" and "**The Solution:**" should render as bold text
1473
+ // - Each diagram should be a separate code block
1474
+ expect(frame).toMatchInlineSnapshot(`
1475
+ "
1476
+ ActionPanel captures actions to zustand
1477
+
1478
+ The Problem:
1479
+
1480
+ BEFORE: Context lost
1481
+ +-----------+ +-----------+
1482
+ | ListItem |-->| Dialog |
1483
+ +-----------+ +-----------+
1484
+
1485
+ The Solution:
1486
+
1487
+ AFTER: Closures preserve
1488
+ +-----------+ +-----------+
1489
+ | Offscreen |-->| zustand |
1490
+ +-----------+ +-----------+
1491
+
1492
+ ActionsDialog groups actions by section.
1493
+
1494
+
1495
+ rc/config.ts +1-0
1496
+
1497
+ + export const x = 1
1498
+
1499
+
1500
+
1501
+
1502
+
1503
+
1504
+
1505
+
1506
+
1507
+
1284
1508
  (1 section) t theme run with --web to share & collaborate
1285
1509
 
1286
1510
  "
@@ -1,4 +1,6 @@
1
- // ReviewApp - TUI component for AI-powered diff review
1
+ // ReviewApp - Main TUI component for AI-powered diff review.
2
+ // Renders streaming review content with markdown descriptions and diff hunks.
3
+ // Supports live generation updates, theme switching, and resume from saved reviews.
2
4
 
3
5
  import * as React from "react"
4
6
  import { useKeyboard, useRenderer, useTerminalDimensions } from "@opentui/react"
@@ -1,4 +1,6 @@
1
- // Compress ACP session content into context for AI prompt
1
+ // Session context compression for including coding session history in AI prompts.
2
+ // Extracts key actions (tool calls, messages, thinking) from ACP notifications
3
+ // and formats them as XML context to help the AI understand why changes were made.
2
4
 
3
5
  import type { SessionNotification } from "@agentclientprotocol/sdk"
4
6
  import type { CompressedSession, SessionContent } from "./types.ts"
@@ -1,6 +1,6 @@
1
- // Storage utilities for persisting review sessions
2
- // Reviews are stored as JSON files in ~/.critique/reviews/
3
- // File is written on process exit/completion to prevent concurrent access issues
1
+ // Persistent storage for review sessions in ~/.critique/reviews/.
2
+ // Saves review state on exit/completion, enables resume of interrupted reviews,
3
+ // and provides listing with automatic cleanup of old reviews (max 50).
4
4
 
5
5
  import fs from "fs"
6
6
  import { join } from "path"
@@ -77,10 +77,11 @@ export function saveReview(review: StoredReview): void {
77
77
  }
78
78
 
79
79
  /**
80
- * List all reviews, sorted by updatedAt descending (most recent first)
80
+ * List reviews, sorted by updatedAt descending (most recent first)
81
81
  * Returns metadata only (no full content) for performance
82
+ * @param cwd - If provided, only return reviews from this directory or its children
82
83
  */
83
- export function listReviews(): ReviewMetadata[] {
84
+ export function listReviews(cwd?: string): ReviewMetadata[] {
84
85
  ensureReviewsDir()
85
86
 
86
87
  const files = fs.readdirSync(REVIEWS_DIR)
@@ -94,6 +95,11 @@ export function listReviews(): ReviewMetadata[] {
94
95
  const content = fs.readFileSync(filepath, "utf-8")
95
96
  const data = JSON.parse(content) as StoredReview
96
97
 
98
+ // Filter by cwd if provided: include reviews from cwd or its children
99
+ if (cwd && !data.cwd.startsWith(cwd)) {
100
+ continue
101
+ }
102
+
97
103
  reviews.push({
98
104
  id: data.id,
99
105
  createdAt: data.createdAt,
@@ -1,5 +1,6 @@
1
- // StreamDisplay - TUI component for showing ACP streaming events
2
- // Displays agent activity while waiting for YAML to be parsed
1
+ // StreamDisplay - TUI component for showing ACP streaming events.
2
+ // Renders real-time agent activity (thinking, messages, tool calls) during review generation.
3
+ // Shows formatted tool operations with file names and edit statistics.
3
4
 
4
5
  import * as React from "react"
5
6
  import { SyntaxStyle } from "@opentui/core"
@@ -1,4 +1,6 @@
1
- // Types for the AI-powered diff review feature
1
+ // Type definitions for the AI-powered diff review feature.
2
+ // Defines IndexedHunk, ReviewYaml, ReviewGroup structures for diff analysis,
3
+ // plus coverage tracking and session context types for ACP integration.
2
4
 
3
5
  import type { SessionNotification } from "@agentclientprotocol/sdk"
4
6
 
@@ -1,4 +1,6 @@
1
- // Watch and parse streaming YAML output from AI review
1
+ // File watcher for streaming YAML output from AI review generation.
2
+ // Parses partial YAML as the AI writes it, enabling progressive UI updates.
3
+ // Handles incomplete YAML gracefully during generation.
2
4
 
3
5
  import fs from "fs"
4
6
  import YAML from "js-yaml"
package/src/store.ts CHANGED
@@ -1,5 +1,6 @@
1
- // Global zustand store for critique app
2
- // Shared between main diff view and review view
1
+ // Global Zustand store for persistent application state.
2
+ // Manages theme selection with automatic persistence to ~/.critique/state.json.
3
+ // Shared between main diff view and review view components.
3
4
 
4
5
  import { create } from "zustand"
5
6
  import fs from "fs"
package/src/themes.ts CHANGED
@@ -1,5 +1,6 @@
1
- // Syntax highlighting themes for critique
2
- // Source: https://github.com/sst/opencode/tree/main/packages/opencode/src/cli/cmd/tui/context/theme
1
+ // Syntax highlighting theme system with 30+ themes from OpenCode.
2
+ // Loads JSON theme files lazily on demand, resolves color references,
3
+ // and provides both UI colors and Tree-sitter compatible syntax styles.
3
4
 
4
5
  import { parseColor, RGBA } from "@opentui/core";
5
6
 
package/src/utils.ts CHANGED
@@ -1,3 +1,6 @@
1
+ // General utility functions used across the application.
2
+ // Currently provides debounce for rate-limiting file watcher callbacks.
3
+
1
4
  export function debounce<T extends (...args: any[]) => any>(
2
5
  fn: T,
3
6
  delay: number
package/src/web-utils.ts CHANGED
@@ -1,5 +1,6 @@
1
- // Shared utilities for web preview generation
2
- // Used by both `web` and `explain --web` commands
1
+ // Web preview generation utilities for uploading diffs to critique.work.
2
+ // Captures PTY output using Bun.Terminal, converts to HTML with responsive layout,
3
+ // and uploads desktop/mobile versions for shareable diff viewing.
3
4
 
4
5
  import { exec } from "child_process"
5
6
  import { promisify } from "util"
package/src/worker.ts CHANGED
@@ -1,3 +1,7 @@
1
+ // Cloudflare Worker for hosting HTML diff previews at critique.work.
2
+ // Handles upload, storage (KV), and responsive serving of desktop/mobile HTML versions.
3
+ // Endpoints: POST /upload, GET /v/:id (view), GET /raw/:id (debug).
4
+
1
5
  import { Hono } from "hono"
2
6
  import { cors } from "hono/cors"
3
7
  import { stream } from "hono/streaming"