Skip to content

Commit 653a00b

Browse files
authored
Merge pull request #10134 from continuedev/nate/fixes-4858
Rename `cn check` to `cn review` with UX improvements
2 parents ca44973 + eb7062f commit 653a00b

File tree

9 files changed

+234
-220
lines changed

9 files changed

+234
-220
lines changed

extensions/cli/src/commands/commands.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@ import { type AssistantConfig } from "@continuedev/sdk";
22

33
// Export command functions
44
export { chat } from "./chat.js";
5-
export { check } from "./check.js";
5+
export { review } from "./review.js";
66
export { login } from "./login.js";
77
export { logout } from "./logout.js";
88
export { listSessionsCommand } from "./ls.js";
Lines changed: 100 additions & 90 deletions
Original file line numberDiff line numberDiff line change
@@ -7,38 +7,38 @@ import { configureConsoleForHeadless } from "../init.js";
77
import { logger } from "../util/logger.js";
88

99
import { ExtendedCommandOptions } from "./BaseCommandOptions.js";
10-
import type { CheckState } from "./check/CheckProgress.js";
11-
import { CheckProgress } from "./check/CheckProgress.js";
12-
import type { WorkerConfig, WorkerResult } from "./check/checkWorker.js";
13-
import type { DiffContext } from "./check/diffContext.js";
14-
import { computeDiffContext } from "./check/diffContext.js";
15-
import type { CheckResult } from "./check/renderReport.js";
16-
import { renderReport } from "./check/renderReport.js";
17-
import { resolveChecks } from "./check/resolveChecks.js";
18-
import { createWorktree, cleanupWorktree } from "./check/worktree.js";
19-
20-
export interface CheckOptions extends ExtendedCommandOptions {
10+
import type { ReviewState } from "./review/ReviewProgress.js";
11+
import { ReviewProgress } from "./review/ReviewProgress.js";
12+
import type { WorkerConfig, WorkerResult } from "./review/reviewWorker.js";
13+
import type { DiffContext } from "./review/diffContext.js";
14+
import { computeDiffContext } from "./review/diffContext.js";
15+
import type { ReviewResult } from "./review/renderReport.js";
16+
import { renderReport } from "./review/renderReport.js";
17+
import { resolveReviews } from "./review/resolveReviews.js";
18+
import { createWorktree, cleanupWorktree } from "./review/worktree.js";
19+
20+
export interface ReviewOptions extends ExtendedCommandOptions {
2121
base?: string;
2222
format?: string;
2323
fix?: boolean;
2424
patch?: boolean;
2525
failFast?: boolean;
26-
checkAgents?: string[];
26+
reviewAgents?: string[];
2727
}
2828

2929
/**
30-
* Run a single check in a forked worker process.
30+
* Run a single review in a forked worker process.
3131
*/
32-
async function runCheckInWorker(
32+
async function runReviewInWorker(
3333
agentSource: string,
3434
worktreePath: string,
3535
diffContext: DiffContext,
36-
options: CheckOptions,
36+
options: ReviewOptions,
3737
): Promise<WorkerResult> {
3838
return new Promise<WorkerResult>((resolve, _reject) => {
3939
// Fork the current CLI entry point with the internal worker flag
4040
const workerPath = process.argv[1];
41-
const child = fork(workerPath, ["--internal-check-worker"], {
41+
const child = fork(workerPath, ["--internal-review-worker"], {
4242
stdio: ["pipe", "pipe", "pipe", "ipc"],
4343
env: {
4444
...process.env,
@@ -58,7 +58,7 @@ async function runCheckInWorker(
5858
patch: "",
5959
agentOutput: "",
6060
duration: 0,
61-
error: "Check timed out after 5 minutes",
61+
error: "Review timed out after 5 minutes",
6262
});
6363
}
6464
},
@@ -79,7 +79,7 @@ async function runCheckInWorker(
7979
verbose: options.verbose,
8080
},
8181
};
82-
child.send({ type: "run-check", config });
82+
child.send({ type: "run-review", config });
8383
} else if (msg.type === "result" && msg.result) {
8484
if (!settled) {
8585
settled = true;
@@ -135,7 +135,7 @@ async function runCheckInWorker(
135135
/**
136136
* Apply patches to the real working tree (--fix mode).
137137
*/
138-
function applyPatches(results: CheckResult[]): void {
138+
function applyPatches(results: ReviewResult[]): void {
139139
const patchResults = results.filter(
140140
(r) => r.status === "fail" && r.patch.trim(),
141141
);
@@ -179,35 +179,37 @@ function applyPatches(results: CheckResult[]): void {
179179
}
180180

181181
/**
182-
* Mount the Ink live progress UI or fall back to static logs.
182+
* Mutable props bag for the live Ink UI so we can update it progressively.
183+
*/
184+
interface LiveUIProps {
185+
checks: ReviewState[];
186+
baseBranch?: string;
187+
changedFileCount?: number;
188+
loading?: boolean;
189+
}
190+
191+
/**
192+
* Mount the Ink live progress UI or return a no-op for non-TTY / special modes.
183193
*/
184194
async function mountProgressUI(
185-
checkStates: CheckState[],
186-
diffContext: DiffContext,
187-
options: CheckOptions,
188-
): Promise<{ rerender?: () => void; unmount?: () => void }> {
195+
props: LiveUIProps,
196+
options: ReviewOptions,
197+
): Promise<{ rerender: () => void; unmount: () => void }> {
189198
const useLiveUI =
190199
process.stdout.isTTY && !options.patch && options.format !== "json";
191200

192201
if (!useLiveUI) {
193-
console.log(
194-
chalk.dim(
195-
`Running ${checkStates.length} check${checkStates.length > 1 ? "s" : ""}: ${checkStates.map((c) => c.name).join(", ")}`,
196-
),
197-
);
198-
return {};
202+
return {
203+
rerender: () => {},
204+
unmount: () => {},
205+
};
199206
}
200207

201208
const { render } = await import("ink");
202-
const props = {
203-
checks: checkStates,
204-
baseBranch: diffContext.baseBranch,
205-
changedFileCount: diffContext.changedFiles.length,
206-
};
207-
const instance = render(React.createElement(CheckProgress, props));
209+
const instance = render(React.createElement(ReviewProgress, props));
208210
return {
209211
rerender: () =>
210-
instance.rerender(React.createElement(CheckProgress, props)),
212+
instance.rerender(React.createElement(ReviewProgress, props)),
211213
unmount: () => instance.unmount(),
212214
};
213215
}
@@ -216,9 +218,9 @@ async function mountProgressUI(
216218
* Output results and exit with appropriate code.
217219
*/
218220
function outputResultsAndExit(
219-
results: CheckResult[],
221+
results: ReviewResult[],
220222
diffContext: DiffContext,
221-
options: CheckOptions,
223+
options: ReviewOptions,
222224
checksFromHub: boolean,
223225
): void {
224226
const format = options.format === "json" ? "json" : "text";
@@ -254,20 +256,30 @@ function outputResultsAndExit(
254256
}
255257

256258
/**
257-
* Main check command handler.
259+
* Main review command handler.
258260
*/
259-
export async function check(options: CheckOptions = {}): Promise<void> {
261+
export async function review(options: ReviewOptions = {}): Promise<void> {
260262
configureConsoleForHeadless(false);
261263

262264
if (options.verbose) {
263265
logger.setLevel("debug");
264266
}
265267

266-
// Step 1: Check for changes
267-
console.log(chalk.dim("Computing diff..."));
268+
// Mount the live UI immediately with a loading state
269+
const uiProps: LiveUIProps = {
270+
checks: [],
271+
loading: true,
272+
};
273+
const { rerender, unmount: unmountUI } = await mountProgressUI(
274+
uiProps,
275+
options,
276+
);
277+
278+
// Step 1: Compute diff
268279
const diffContext = computeDiffContext(options.base);
269280

270281
if (!diffContext.diff.trim() && diffContext.changedFiles.length === 0) {
282+
unmountUI();
271283
console.log(
272284
chalk.yellow(
273285
"No changes detected. Make some changes or specify a different base branch with --base.",
@@ -276,64 +288,60 @@ export async function check(options: CheckOptions = {}): Promise<void> {
276288
process.exit(0);
277289
}
278290

279-
console.log(
280-
chalk.dim(
281-
`Found ${diffContext.changedFiles.length} changed files against ${diffContext.baseBranch}`,
282-
),
283-
);
291+
// Update UI with diff info (still loading reviews)
292+
uiProps.baseBranch = diffContext.baseBranch;
293+
uiProps.changedFileCount = diffContext.changedFiles.length;
294+
rerender();
284295

285-
// Step 2: Resolve checks
286-
console.log(chalk.dim("Resolving checks..."));
287-
const checks = await resolveChecks(options.checkAgents);
296+
// Step 2: Resolve reviews
297+
const reviews = await resolveReviews(options.reviewAgents);
288298

289-
if (checks.length === 0) {
299+
if (reviews.length === 0) {
300+
unmountUI();
290301
console.log(
291-
chalk.yellow("\nNo checks found. To add checks:\n") +
302+
chalk.yellow("\nNo reviews found. To add reviews:\n") +
292303
chalk.dim(
293-
" 1. Create .continue/agents/my-check.md with agent instructions\n",
304+
" 1. Create .continue/agents/my-review.md with agent instructions\n",
294305
) +
295306
chalk.dim(
296-
" 2. Or specify an agent: cn check --agent org/agent-name\n",
307+
" 2. Or specify an agent: cn review --review-agents org/agent-name\n",
297308
) +
298-
chalk.dim(" 3. Or configure checks on https://continue.dev\n"),
309+
chalk.dim(" 3. Or configure reviews on https://continue.dev\n"),
299310
);
300311
process.exit(0);
301312
}
302313

303-
const checksFromHub = checks.some((c) => c.sourceType === "hub");
314+
const checksFromHub = reviews.some((c) => c.sourceType === "hub");
304315

305-
// Build mutable state for the live UI
306-
const checkStates: CheckState[] = checks.map((c) => ({
316+
// Build mutable state for the live UI and stop loading
317+
const reviewStates: ReviewState[] = reviews.map((c) => ({
307318
name: c.name,
308319
status: "pending" as const,
309320
}));
321+
uiProps.checks = reviewStates;
322+
uiProps.loading = false;
323+
rerender();
310324

311-
const { rerender, unmount: unmountUI } = await mountProgressUI(
312-
checkStates,
313-
diffContext,
314-
options,
315-
);
316-
317-
// Step 3: Create worktrees and run checks
318-
const results: CheckResult[] = [];
325+
// Step 3: Create worktrees and run reviews
326+
const results: ReviewResult[] = [];
319327

320-
const runSingleCheck = async (
321-
resolvedCheck: (typeof checks)[number],
328+
const runSingleReview = async (
329+
resolvedReview: (typeof reviews)[number],
322330
i: number,
323-
): Promise<CheckResult> => {
331+
): Promise<ReviewResult> => {
324332
const startTime = Date.now();
325333
let worktreePath: string | null = null;
326334

327335
try {
328336
// Mark as running
329-
checkStates[i].status = "running";
330-
checkStates[i].startTime = Date.now();
331-
rerender?.();
337+
reviewStates[i].status = "running";
338+
reviewStates[i].startTime = Date.now();
339+
rerender();
332340

333341
worktreePath = await createWorktree(i);
334342

335-
const workerResult = await runCheckInWorker(
336-
resolvedCheck.source,
343+
const workerResult = await runReviewInWorker(
344+
resolvedReview.source,
337345
worktreePath,
338346
diffContext,
339347
options,
@@ -345,31 +353,33 @@ export async function check(options: CheckOptions = {}): Promise<void> {
345353
? ("fail" as const)
346354
: ("pass" as const);
347355

356+
const duration = (Date.now() - startTime) / 1000;
357+
348358
// Mark as complete
349-
checkStates[i].status = status;
350-
checkStates[i].duration = workerResult.duration;
351-
rerender?.();
359+
reviewStates[i].status = status;
360+
reviewStates[i].duration = duration;
361+
rerender();
352362

353363
return {
354-
agent: resolvedCheck.source,
355-
name: resolvedCheck.name,
364+
agent: resolvedReview.source,
365+
name: resolvedReview.name,
356366
status,
357367
patch: workerResult.patch,
358368
output: workerResult.agentOutput,
359-
duration: workerResult.duration,
369+
duration,
360370
error: workerResult.error,
361371
};
362372
} catch (e: any) {
363373
const duration = (Date.now() - startTime) / 1000;
364374

365375
// Mark as error
366-
checkStates[i].status = "error";
367-
checkStates[i].duration = duration;
368-
rerender?.();
376+
reviewStates[i].status = "error";
377+
reviewStates[i].duration = duration;
378+
rerender();
369379

370380
return {
371-
agent: resolvedCheck.source,
372-
name: resolvedCheck.name,
381+
agent: resolvedReview.source,
382+
name: resolvedReview.name,
373383
status: "error" as const,
374384
patch: "",
375385
output: "",
@@ -384,16 +394,16 @@ export async function check(options: CheckOptions = {}): Promise<void> {
384394
};
385395

386396
if (options.failFast) {
387-
for (let i = 0; i < checks.length; i++) {
388-
const result = await runSingleCheck(checks[i], i);
397+
for (let i = 0; i < reviews.length; i++) {
398+
const result = await runSingleReview(reviews[i], i);
389399
results.push(result);
390400
if (result.status === "fail" || result.status === "error") {
391401
break;
392402
}
393403
}
394404
} else {
395405
const settled = await Promise.allSettled(
396-
checks.map((resolvedCheck, i) => runSingleCheck(resolvedCheck, i)),
406+
reviews.map((resolvedReview, i) => runSingleReview(resolvedReview, i)),
397407
);
398408
for (const result of settled) {
399409
if (result.status === "fulfilled") {
@@ -402,6 +412,6 @@ export async function check(options: CheckOptions = {}): Promise<void> {
402412
}
403413
}
404414

405-
unmountUI?.();
415+
unmountUI();
406416
outputResultsAndExit(results, diffContext, options, checksFromHub);
407417
}

0 commit comments

Comments
 (0)