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 +13 -0
- package/package.json +1 -1
- package/scripts/preview-review.tsx +3 -3
- package/src/ansi-html.ts +4 -0
- package/src/cli.tsx +10 -6
- package/src/components/diff-view.tsx +3 -1
- package/src/components/index.ts +3 -1
- package/src/diff-utils.ts +3 -1
- package/src/dropdown.tsx +4 -0
- package/src/logger.ts +3 -1
- package/src/monochrome.ts +4 -0
- package/src/monotone.ts +4 -0
- package/src/review/acp-client.ts +53 -82
- package/src/review/acp-stream-display.ts +3 -2
- package/src/review/diagram-parser.ts +3 -2
- package/src/review/hunk-parser.ts +3 -1
- package/src/review/index.ts +3 -2
- package/src/review/review-app.test.tsx +224 -0
- package/src/review/review-app.tsx +3 -1
- package/src/review/session-context.ts +3 -1
- package/src/review/storage.ts +11 -5
- package/src/review/stream-display.tsx +3 -2
- package/src/review/types.ts +3 -1
- package/src/review/yaml-watcher.ts +3 -1
- package/src/store.ts +3 -2
- package/src/themes.ts +3 -2
- package/src/utils.ts +3 -0
- package/src/web-utils.ts +3 -2
- package/src/worker.ts +4 -0
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
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
#!/usr/bin/env bun
|
|
2
|
-
//
|
|
3
|
-
//
|
|
4
|
-
//
|
|
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
|
|
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,
|
|
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,
|
|
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"
|
package/src/components/index.ts
CHANGED
package/src/diff-utils.ts
CHANGED
|
@@ -1,4 +1,6 @@
|
|
|
1
|
-
// Shared utilities for diff processing
|
|
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";
|
package/src/review/acp-client.ts
CHANGED
|
@@ -1,4 +1,6 @@
|
|
|
1
|
-
// ACP client for
|
|
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
|
|
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
|
-
|
|
164
|
-
|
|
165
|
-
|
|
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
|
-
|
|
172
|
-
|
|
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
|
-
|
|
180
|
-
|
|
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
|
-
|
|
184
|
-
|
|
185
|
-
|
|
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
|
-
|
|
204
|
-
|
|
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
|
-
|
|
208
|
-
|
|
209
|
-
|
|
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
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
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
|
|
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
|
-
//
|
|
2
|
-
//
|
|
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
|
-
//
|
|
2
|
-
//
|
|
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
|
-
//
|
|
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"
|
package/src/review/index.ts
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
|
-
//
|
|
2
|
-
//
|
|
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
|
-
//
|
|
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"
|
package/src/review/storage.ts
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
|
-
//
|
|
2
|
-
//
|
|
3
|
-
//
|
|
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
|
|
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
|
-
//
|
|
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"
|
package/src/review/types.ts
CHANGED
|
@@ -1,4 +1,6 @@
|
|
|
1
|
-
//
|
|
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
|
-
//
|
|
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
|
|
2
|
-
//
|
|
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
|
|
2
|
-
//
|
|
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
package/src/web-utils.ts
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
|
-
//
|
|
2
|
-
//
|
|
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"
|