@oh-my-pi/pi-coding-agent 15.7.6 → 15.8.2
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 +172 -198
- package/dist/types/async/job-manager.d.ts +3 -3
- package/dist/types/cli/args.d.ts +1 -0
- package/dist/types/cli/claude-trace-cli.d.ts +54 -0
- package/dist/types/cli/session-picker.d.ts +10 -3
- package/dist/types/cli/update-cli.d.ts +17 -0
- package/dist/types/commands/launch.d.ts +3 -0
- package/dist/types/config/keybindings.d.ts +10 -1
- package/dist/types/config/settings-schema.d.ts +2 -2
- package/dist/types/config/settings.d.ts +13 -0
- package/dist/types/edit/index.d.ts +6 -0
- package/dist/types/edit/streaming.d.ts +8 -0
- package/dist/types/eval/concurrency-bridge.d.ts +26 -0
- package/dist/types/eval/js/tool-bridge.d.ts +2 -1
- package/dist/types/export/ttsr.d.ts +9 -0
- package/dist/types/extensibility/plugins/legacy-pi-compat.d.ts +11 -0
- package/dist/types/main.d.ts +5 -0
- package/dist/types/mcp/transports/stdio.d.ts +19 -0
- package/dist/types/modes/components/custom-editor.d.ts +3 -1
- package/dist/types/modes/components/hook-selector.d.ts +3 -0
- package/dist/types/modes/components/session-selector.d.ts +32 -5
- package/dist/types/modes/components/tool-execution.d.ts +8 -0
- package/dist/types/modes/controllers/extension-ui-controller.d.ts +2 -2
- package/dist/types/modes/controllers/input-controller.d.ts +1 -0
- package/dist/types/modes/interactive-mode.d.ts +9 -2
- package/dist/types/modes/types.d.ts +4 -2
- package/dist/types/registry/agent-registry.d.ts +1 -1
- package/dist/types/sdk.d.ts +2 -2
- package/dist/types/session/agent-session.d.ts +4 -2
- package/dist/types/session/history-storage.d.ts +16 -1
- package/dist/types/session/session-manager.d.ts +4 -0
- package/dist/types/task/output-manager.d.ts +6 -15
- package/dist/types/tools/find.d.ts +0 -9
- package/dist/types/tools/index.d.ts +1 -1
- package/dist/types/tools/path-utils.d.ts +16 -0
- package/dist/types/tools/sqlite-reader.d.ts +25 -8
- package/dist/types/tools/write.d.ts +2 -0
- package/dist/types/utils/clipboard.d.ts +4 -0
- package/dist/types/utils/jj.d.ts +49 -0
- package/dist/types/web/kagi.d.ts +76 -0
- package/dist/types/web/search/providers/exa.d.ts +7 -1
- package/dist/types/web/search/providers/kagi.d.ts +1 -0
- package/package.json +9 -9
- package/src/async/job-manager.ts +3 -3
- package/src/cli/args.ts +6 -2
- package/src/cli/claude-trace-cli.ts +783 -0
- package/src/cli/session-picker.ts +36 -10
- package/src/cli/update-cli.ts +35 -2
- package/src/commands/launch.ts +3 -0
- package/src/config/keybindings.ts +14 -1
- package/src/config/model-registry.ts +18 -7
- package/src/config/settings-schema.ts +2 -2
- package/src/config/settings.ts +23 -0
- package/src/discovery/builtin-rules/index.ts +2 -0
- package/src/discovery/builtin-rules/ts-no-deprecated-leftovers.md +44 -0
- package/src/discovery/claude-plugins.ts +7 -9
- package/src/edit/index.ts +10 -0
- package/src/edit/streaming.ts +65 -0
- package/src/eval/__tests__/agent-bridge.test.ts +58 -4
- package/src/eval/concurrency-bridge.ts +34 -0
- package/src/eval/js/shared/prelude.txt +20 -17
- package/src/eval/js/tool-bridge.ts +5 -0
- package/src/eval/py/prelude.py +23 -15
- package/src/export/ttsr.ts +18 -1
- package/src/extensibility/custom-commands/bundled/review/index.ts +74 -45
- package/src/extensibility/plugins/legacy-pi-compat.ts +115 -131
- package/src/extensibility/skills.ts +0 -1
- package/src/internal-urls/docs-index.generated.ts +12 -11
- package/src/main.ts +92 -24
- package/src/mcp/transports/stdio.ts +55 -22
- package/src/modes/acp/acp-event-mapper.ts +54 -4
- package/src/modes/components/custom-editor.ts +10 -0
- package/src/modes/components/hook-selector.ts +89 -31
- package/src/modes/components/oauth-selector.ts +12 -6
- package/src/modes/components/session-selector.ts +179 -24
- package/src/modes/components/tool-execution.ts +16 -3
- package/src/modes/controllers/command-controller.ts +2 -11
- package/src/modes/controllers/extension-ui-controller.ts +3 -2
- package/src/modes/controllers/input-controller.ts +19 -1
- package/src/modes/controllers/selector-controller.ts +61 -21
- package/src/modes/interactive-mode.ts +125 -15
- package/src/modes/types.ts +5 -2
- package/src/prompts/agents/reviewer.md +2 -2
- package/src/prompts/review-request.md +1 -1
- package/src/prompts/system/empty-stop-retry.md +6 -0
- package/src/prompts/system/orchestrate-notice.md +5 -3
- package/src/prompts/system/workflow-notice.md +2 -2
- package/src/prompts/tools/eval.md +5 -5
- package/src/prompts/tools/find.md +1 -1
- package/src/prompts/tools/irc.md +6 -6
- package/src/prompts/tools/search-tool-bm25.md +9 -2
- package/src/prompts/tools/search.md +1 -1
- package/src/prompts/tools/task.md +1 -1
- package/src/registry/agent-registry.ts +1 -1
- package/src/sdk.ts +85 -31
- package/src/session/agent-session.ts +209 -54
- package/src/session/history-storage.ts +56 -12
- package/src/session/session-manager.ts +34 -0
- package/src/task/output-manager.ts +40 -48
- package/src/task/render.ts +3 -8
- package/src/tools/browser/tab-worker.ts +8 -5
- package/src/tools/find.ts +5 -29
- package/src/tools/index.ts +1 -1
- package/src/tools/path-utils.ts +144 -1
- package/src/tools/read.ts +47 -0
- package/src/tools/search-tool-bm25.ts +7 -1
- package/src/tools/search.ts +2 -27
- package/src/tools/sqlite-reader.ts +92 -9
- package/src/tools/write.ts +6 -0
- package/src/utils/clipboard.ts +38 -1
- package/src/utils/git.ts +19 -23
- package/src/utils/jj.ts +225 -0
- package/src/utils/open.ts +37 -2
- package/src/web/kagi.ts +168 -49
- package/src/web/search/providers/anthropic.ts +1 -1
- package/src/web/search/providers/exa.ts +20 -86
- package/src/web/search/providers/kagi.ts +4 -0
|
@@ -55,15 +55,24 @@ if (!globalThis.__omp_js_prelude_loaded__) {
|
|
|
55
55
|
return hasOwn(o, "schema") ? JSON.parse(text) : text;
|
|
56
56
|
};
|
|
57
57
|
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
58
|
+
// Pool ceiling mirrors the task tool's `task.maxConcurrency` setting so an
|
|
59
|
+
// eval fan-out runs as wide as a `task` batch would. 0 (and any non-finite
|
|
60
|
+
// reply) means unbounded — run every item at once.
|
|
61
|
+
const __concurrencyLimit = async () => {
|
|
62
|
+
try {
|
|
63
|
+
const r = await globalThis.__omp_call_tool__("__concurrency__", {});
|
|
64
|
+
const n = Math.trunc(Number(r && typeof r === "object" ? r.limit : r));
|
|
65
|
+
return Number.isFinite(n) && n > 0 ? n : 0;
|
|
66
|
+
} catch {
|
|
67
|
+
return 0;
|
|
68
|
+
}
|
|
62
69
|
};
|
|
63
70
|
|
|
64
|
-
const __pool = async (items,
|
|
71
|
+
const __pool = async (items, fn) => {
|
|
65
72
|
const list = Array.from(items ?? []);
|
|
66
|
-
|
|
73
|
+
if (list.length === 0) return [];
|
|
74
|
+
const limit = await __concurrencyLimit();
|
|
75
|
+
const concurrency = limit > 0 ? Math.min(limit, list.length) : list.length;
|
|
67
76
|
const results = new Array(list.length);
|
|
68
77
|
let next = 0;
|
|
69
78
|
const worker = async () => {
|
|
@@ -77,23 +86,17 @@ if (!globalThis.__omp_js_prelude_loaded__) {
|
|
|
77
86
|
return results;
|
|
78
87
|
};
|
|
79
88
|
|
|
80
|
-
const parallel = async
|
|
81
|
-
__pool(thunks,
|
|
89
|
+
const parallel = async thunks =>
|
|
90
|
+
__pool(thunks, (thunk, index) => {
|
|
82
91
|
if (typeof thunk !== "function") throw new TypeError("parallel() expects an iterable of functions");
|
|
83
92
|
return thunk(index);
|
|
84
93
|
});
|
|
85
94
|
|
|
86
|
-
const pipeline = async (items, ...
|
|
87
|
-
let opts = {};
|
|
88
|
-
const last = stagesAndOptions.at(-1);
|
|
89
|
-
if (last && typeof last === "object" && !Array.isArray(last)) {
|
|
90
|
-
opts = last;
|
|
91
|
-
stagesAndOptions = stagesAndOptions.slice(0, -1);
|
|
92
|
-
}
|
|
95
|
+
const pipeline = async (items, ...stages) => {
|
|
93
96
|
let current = Array.from(items ?? []);
|
|
94
|
-
for (const stage of
|
|
97
|
+
for (const stage of stages) {
|
|
95
98
|
if (typeof stage !== "function") throw new TypeError("pipeline() stages must be functions");
|
|
96
|
-
current = await __pool(current,
|
|
99
|
+
current = await __pool(current, stage);
|
|
97
100
|
}
|
|
98
101
|
return current;
|
|
99
102
|
};
|
|
@@ -3,6 +3,7 @@ import type { ToolSession } from "../../tools";
|
|
|
3
3
|
import { ToolError } from "../../tools/tool-errors";
|
|
4
4
|
import { EVAL_AGENT_BRIDGE_NAME, runEvalAgent } from "../agent-bridge";
|
|
5
5
|
import { EVAL_BUDGET_BRIDGE_NAME, type EvalBudgetResult, runEvalBudget } from "../budget-bridge";
|
|
6
|
+
import { EVAL_CONCURRENCY_BRIDGE_NAME, type EvalConcurrencyResult, runEvalConcurrency } from "../concurrency-bridge";
|
|
6
7
|
import { EVAL_LLM_BRIDGE_NAME, runEvalLlm } from "../llm-bridge";
|
|
7
8
|
import type { JsStatusEvent } from "./shared/types";
|
|
8
9
|
|
|
@@ -17,6 +18,7 @@ interface ToolBridgeOptions {
|
|
|
17
18
|
type ToolValue =
|
|
18
19
|
| string
|
|
19
20
|
| EvalBudgetResult
|
|
21
|
+
| EvalConcurrencyResult
|
|
20
22
|
| {
|
|
21
23
|
text: string;
|
|
22
24
|
details?: unknown;
|
|
@@ -114,6 +116,9 @@ export async function callSessionTool(name: string, args: unknown, options: Tool
|
|
|
114
116
|
if (name === EVAL_BUDGET_BRIDGE_NAME) {
|
|
115
117
|
return await runEvalBudget(args, options);
|
|
116
118
|
}
|
|
119
|
+
if (name === EVAL_CONCURRENCY_BRIDGE_NAME) {
|
|
120
|
+
return runEvalConcurrency(args, options);
|
|
121
|
+
}
|
|
117
122
|
const tool = getTool(options.session, name);
|
|
118
123
|
const normalizedArgs = normalizeArgs(args);
|
|
119
124
|
const toolCallId = `js-${name}-${crypto.randomUUID()}`;
|
package/src/eval/py/prelude.py
CHANGED
|
@@ -503,27 +503,35 @@ if "__omp_prelude_loaded__" not in globals():
|
|
|
503
503
|
text = res.get("text") if isinstance(res, dict) else res
|
|
504
504
|
return json.loads(text) if schema is not None else text
|
|
505
505
|
|
|
506
|
-
def
|
|
507
|
-
"""
|
|
506
|
+
def _concurrency_limit():
|
|
507
|
+
"""Worker-pool ceiling from the host ``task.maxConcurrency`` setting.
|
|
508
|
+
|
|
509
|
+
An eval fan-out runs as wide as a ``task`` batch would. Returns ``0`` for
|
|
510
|
+
unbounded (run every item at once); falls back to ``0`` if the host
|
|
511
|
+
bridge is unreachable.
|
|
512
|
+
"""
|
|
508
513
|
try:
|
|
509
|
-
|
|
510
|
-
|
|
511
|
-
|
|
512
|
-
|
|
514
|
+
snap = _bridge_call("__concurrency__", {}) or {}
|
|
515
|
+
n = int(snap.get("limit") or 0)
|
|
516
|
+
except Exception:
|
|
517
|
+
return 0
|
|
518
|
+
return n if n > 0 else 0
|
|
513
519
|
|
|
514
|
-
def _pool_map(items, fn
|
|
520
|
+
def _pool_map(items, fn):
|
|
515
521
|
"""Run ``fn`` over ``items`` through a bounded thread pool.
|
|
516
522
|
|
|
517
523
|
Preserves input order, barriers until every task settles, and raises the
|
|
518
524
|
lowest-index exception if any task failed. Each task runs inside a copy
|
|
519
525
|
of the submitting thread's context so the ``_CURRENT_RID`` ContextVar
|
|
520
|
-
propagates and bridge calls (agent(), tool.*, etc.) keep working.
|
|
526
|
+
propagates and bridge calls (agent(), tool.*, etc.) keep working. The
|
|
527
|
+
pool width tracks ``task.maxConcurrency`` (0 = run every item at once).
|
|
521
528
|
"""
|
|
522
529
|
import concurrent.futures, contextvars
|
|
523
530
|
items = list(items)
|
|
524
531
|
if not items:
|
|
525
532
|
return []
|
|
526
|
-
|
|
533
|
+
limit = _concurrency_limit()
|
|
534
|
+
workers = min(limit, len(items)) if limit > 0 else len(items)
|
|
527
535
|
results = [None] * len(items)
|
|
528
536
|
errors = {}
|
|
529
537
|
with concurrent.futures.ThreadPoolExecutor(max_workers=workers) as pool:
|
|
@@ -541,30 +549,30 @@ if "__omp_prelude_loaded__" not in globals():
|
|
|
541
549
|
raise errors[min(errors)]
|
|
542
550
|
return results
|
|
543
551
|
|
|
544
|
-
def parallel(thunks
|
|
552
|
+
def parallel(thunks):
|
|
545
553
|
"""Run zero-arg callables through a bounded pool, preserving input order.
|
|
546
554
|
|
|
547
555
|
Barriers until all finish; re-raises the lowest-index exception if any
|
|
548
|
-
thunk raised.
|
|
556
|
+
thunk raised. Pool width tracks the task tool's ``task.maxConcurrency``.
|
|
549
557
|
"""
|
|
550
558
|
thunks = list(thunks)
|
|
551
559
|
for t in thunks:
|
|
552
560
|
if not callable(t):
|
|
553
561
|
raise TypeError("parallel() expects an iterable of zero-arg callables")
|
|
554
|
-
return _pool_map(thunks, lambda t: t()
|
|
562
|
+
return _pool_map(thunks, lambda t: t())
|
|
555
563
|
|
|
556
|
-
def pipeline(items, *stages
|
|
564
|
+
def pipeline(items, *stages):
|
|
557
565
|
"""Map items left-to-right through one-arg stage callables.
|
|
558
566
|
|
|
559
567
|
Every item clears stage N before any item enters stage N+1 (barrier per
|
|
560
568
|
stage). Stage 1 receives the original item; later stages receive the
|
|
561
|
-
previous stage's result.
|
|
569
|
+
previous stage's result. Pool width tracks ``task.maxConcurrency``.
|
|
562
570
|
"""
|
|
563
571
|
current = list(items)
|
|
564
572
|
for stage in stages:
|
|
565
573
|
if not callable(stage):
|
|
566
574
|
raise TypeError("pipeline() stages must be callables")
|
|
567
|
-
current = _pool_map(current, stage
|
|
575
|
+
current = _pool_map(current, stage)
|
|
568
576
|
return current
|
|
569
577
|
|
|
570
578
|
def log(message):
|
package/src/export/ttsr.ts
CHANGED
|
@@ -339,7 +339,24 @@ export class TtsrManager {
|
|
|
339
339
|
const bufferKey = this.#bufferKey(context);
|
|
340
340
|
const nextBuffer = `${this.#buffers.get(bufferKey) ?? ""}${delta}`;
|
|
341
341
|
this.#buffers.set(bufferKey, nextBuffer);
|
|
342
|
+
return this.#matchBuffer(nextBuffer, context);
|
|
343
|
+
}
|
|
344
|
+
|
|
345
|
+
/**
|
|
346
|
+
* Replace the scoped buffer with a tool-provided normalized snapshot and
|
|
347
|
+
* return matching rules.
|
|
348
|
+
*
|
|
349
|
+
* Used for tools exposing `matcherDigest`: the digest is recomputed from the
|
|
350
|
+
* full (partial) arguments on every delta, so it replaces the buffer instead
|
|
351
|
+
* of being appended to it.
|
|
352
|
+
*/
|
|
353
|
+
checkSnapshot(snapshot: string, context: TtsrMatchContext): Rule[] {
|
|
354
|
+
const bufferKey = this.#bufferKey(context);
|
|
355
|
+
this.#buffers.set(bufferKey, snapshot);
|
|
356
|
+
return this.#matchBuffer(snapshot, context);
|
|
357
|
+
}
|
|
342
358
|
|
|
359
|
+
#matchBuffer(buffer: string, context: TtsrMatchContext): Rule[] {
|
|
343
360
|
const matches: Rule[] = [];
|
|
344
361
|
for (const [name, entry] of this.#rules) {
|
|
345
362
|
if (!this.#canTrigger(name)) {
|
|
@@ -351,7 +368,7 @@ export class TtsrManager {
|
|
|
351
368
|
if (!this.#matchesGlobalPaths(entry, context)) {
|
|
352
369
|
continue;
|
|
353
370
|
}
|
|
354
|
-
if (!this.#matchesCondition(entry,
|
|
371
|
+
if (!this.#matchesCondition(entry, buffer)) {
|
|
355
372
|
continue;
|
|
356
373
|
}
|
|
357
374
|
|
|
@@ -7,7 +7,7 @@
|
|
|
7
7
|
* 3. Review a specific commit
|
|
8
8
|
* 4. Custom review instructions
|
|
9
9
|
*
|
|
10
|
-
* Runs
|
|
10
|
+
* Runs VCS diffs upfront, parses results, filters noise, and provides
|
|
11
11
|
* rich context for the orchestrating agent to distribute work across
|
|
12
12
|
* multiple reviewer agents based on diff weight and locality.
|
|
13
13
|
*/
|
|
@@ -18,6 +18,7 @@ import reviewCustomRequestTemplate from "../../../../prompts/review-custom-reque
|
|
|
18
18
|
import reviewHeadlessRequestTemplate from "../../../../prompts/review-headless-request.md" with { type: "text" };
|
|
19
19
|
import reviewRequestTemplate from "../../../../prompts/review-request.md" with { type: "text" };
|
|
20
20
|
import * as git from "../../../../utils/git";
|
|
21
|
+
import * as jj from "../../../../utils/jj";
|
|
21
22
|
|
|
22
23
|
// ─────────────────────────────────────────────────────────────────────────────
|
|
23
24
|
// Types
|
|
@@ -37,6 +38,13 @@ interface DiffStats {
|
|
|
37
38
|
excluded: { path: string; reason: string; linesAdded: number; linesRemoved: number }[];
|
|
38
39
|
}
|
|
39
40
|
|
|
41
|
+
interface CurrentReviewDiff {
|
|
42
|
+
diffInstruction: string;
|
|
43
|
+
diffText: string;
|
|
44
|
+
emptyMessage?: string;
|
|
45
|
+
mode: string;
|
|
46
|
+
}
|
|
47
|
+
|
|
40
48
|
// ─────────────────────────────────────────────────────────────────────────────
|
|
41
49
|
// Exclusion patterns for noise files
|
|
42
50
|
// ─────────────────────────────────────────────────────────────────────────────
|
|
@@ -195,11 +203,20 @@ function getDiffPreview(hunks: string, maxLines: number): string {
|
|
|
195
203
|
// Thresholds for diff inclusion
|
|
196
204
|
const MAX_DIFF_CHARS = 50_000; // Don't include diff above this
|
|
197
205
|
const MAX_FILES_FOR_INLINE_DIFF = 20; // Don't include diff if more files than this
|
|
206
|
+
const DEFAULT_LARGE_DIFF_INSTRUCTION = "MUST run `git diff`/`git show` for assigned files";
|
|
207
|
+
const GIT_UNCOMMITTED_DIFF_INSTRUCTION =
|
|
208
|
+
"MUST run both `git diff -- <path>` and `git diff --cached -- <path>` for assigned files";
|
|
209
|
+
const JJ_UNCOMMITTED_DIFF_INSTRUCTION = "MUST run `jj --ignore-working-copy diff --git -- <path>` for assigned files";
|
|
198
210
|
|
|
199
211
|
/**
|
|
200
212
|
* Build the full review prompt with diff stats and distribution guidance.
|
|
201
213
|
*/
|
|
202
|
-
function buildReviewPrompt(
|
|
214
|
+
function buildReviewPrompt(
|
|
215
|
+
mode: string,
|
|
216
|
+
stats: DiffStats,
|
|
217
|
+
rawDiff: string,
|
|
218
|
+
options: { additionalInstructions?: string; diffInstruction?: string } = {},
|
|
219
|
+
): string {
|
|
203
220
|
const agentCount = getRecommendedAgentCount(stats);
|
|
204
221
|
const skipDiff = rawDiff.length > MAX_DIFF_CHARS || stats.files.length > MAX_FILES_FOR_INLINE_DIFF;
|
|
205
222
|
const totalLines = stats.totalAdded + stats.totalRemoved;
|
|
@@ -223,7 +240,8 @@ function buildReviewPrompt(mode: string, stats: DiffStats, rawDiff: string, addi
|
|
|
223
240
|
skipDiff,
|
|
224
241
|
rawDiff: rawDiff.trim(),
|
|
225
242
|
linesPerFile,
|
|
226
|
-
additionalInstructions,
|
|
243
|
+
additionalInstructions: options.additionalInstructions,
|
|
244
|
+
diffInstruction: options.diffInstruction ?? DEFAULT_LARGE_DIFF_INSTRUCTION,
|
|
227
245
|
});
|
|
228
246
|
}
|
|
229
247
|
|
|
@@ -305,49 +323,32 @@ export class ReviewCommand implements CustomCommand {
|
|
|
305
323
|
`Reviewing changes between \`${baseBranch}\` and \`${currentBranch}\` (PR-style)`,
|
|
306
324
|
stats,
|
|
307
325
|
diffText,
|
|
308
|
-
extraInstructions,
|
|
326
|
+
{ additionalInstructions: extraInstructions },
|
|
309
327
|
);
|
|
310
328
|
}
|
|
311
329
|
|
|
312
330
|
case 2: {
|
|
313
|
-
|
|
314
|
-
const status = await getGitStatus(this.api);
|
|
315
|
-
if (!status.trim()) {
|
|
316
|
-
ctx.ui.notify("No uncommitted changes found", "warning");
|
|
317
|
-
return undefined;
|
|
318
|
-
}
|
|
319
|
-
|
|
320
|
-
let unstagedDiff: string;
|
|
321
|
-
let stagedDiff: string;
|
|
322
|
-
try {
|
|
323
|
-
[unstagedDiff, stagedDiff] = await Promise.all([
|
|
324
|
-
git.diff(this.api.cwd),
|
|
325
|
-
git.diff(this.api.cwd, { cached: true }),
|
|
326
|
-
]);
|
|
327
|
-
} catch (err) {
|
|
331
|
+
const reviewDiff = await getUncommittedReviewDiff(this.api).catch(err => {
|
|
328
332
|
ctx.ui.notify(`Failed to get diff: ${err instanceof Error ? err.message : String(err)}`, "error");
|
|
329
333
|
return undefined;
|
|
330
|
-
}
|
|
334
|
+
});
|
|
335
|
+
if (!reviewDiff) return undefined;
|
|
331
336
|
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
if (!combinedDiff.trim()) {
|
|
335
|
-
ctx.ui.notify("No diff content found", "warning");
|
|
337
|
+
if (!reviewDiff.diffText.trim()) {
|
|
338
|
+
ctx.ui.notify(reviewDiff.emptyMessage ?? "No diff content found", "warning");
|
|
336
339
|
return undefined;
|
|
337
340
|
}
|
|
338
341
|
|
|
339
|
-
const stats = parseDiff(
|
|
342
|
+
const stats = parseDiff(reviewDiff.diffText);
|
|
340
343
|
if (stats.files.length === 0) {
|
|
341
344
|
ctx.ui.notify("No reviewable files (all changes filtered out)", "warning");
|
|
342
345
|
return undefined;
|
|
343
346
|
}
|
|
344
347
|
|
|
345
|
-
return buildReviewPrompt(
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
extraInstructions,
|
|
350
|
-
);
|
|
348
|
+
return buildReviewPrompt(reviewDiff.mode, stats, reviewDiff.diffText, {
|
|
349
|
+
additionalInstructions: extraInstructions,
|
|
350
|
+
diffInstruction: reviewDiff.diffInstruction,
|
|
351
|
+
});
|
|
351
352
|
}
|
|
352
353
|
|
|
353
354
|
case 3: {
|
|
@@ -383,11 +384,13 @@ export class ReviewCommand implements CustomCommand {
|
|
|
383
384
|
return undefined;
|
|
384
385
|
}
|
|
385
386
|
|
|
386
|
-
return buildReviewPrompt(`Reviewing commit \`${hash}\``, stats, diffText,
|
|
387
|
+
return buildReviewPrompt(`Reviewing commit \`${hash}\``, stats, diffText, {
|
|
388
|
+
additionalInstructions: extraInstructions,
|
|
389
|
+
});
|
|
387
390
|
}
|
|
388
391
|
|
|
389
392
|
case 4: {
|
|
390
|
-
// Custom instructions
|
|
393
|
+
// Custom instructions with opportunistic current-diff context.
|
|
391
394
|
const instructions = await ctx.ui.editor(
|
|
392
395
|
"Enter custom review instructions",
|
|
393
396
|
"Review the following:\n\n",
|
|
@@ -396,23 +399,19 @@ export class ReviewCommand implements CustomCommand {
|
|
|
396
399
|
);
|
|
397
400
|
if (!instructions?.trim()) return undefined;
|
|
398
401
|
|
|
399
|
-
|
|
400
|
-
let diffText: string | undefined;
|
|
401
|
-
try {
|
|
402
|
-
diffText = await git.diff(this.api.cwd, { base: "HEAD" });
|
|
403
|
-
} catch {
|
|
404
|
-
diffText = undefined;
|
|
405
|
-
}
|
|
406
|
-
const reviewDiff = diffText?.trim();
|
|
402
|
+
const reviewDiff = await getUncommittedReviewDiff(this.api).catch(() => undefined);
|
|
407
403
|
|
|
408
|
-
if (reviewDiff) {
|
|
409
|
-
const stats = parseDiff(reviewDiff);
|
|
404
|
+
if (reviewDiff?.diffText.trim()) {
|
|
405
|
+
const stats = parseDiff(reviewDiff.diffText);
|
|
410
406
|
// Even if all files filtered, include the custom instructions
|
|
411
407
|
return buildReviewPrompt(
|
|
412
408
|
`Custom review: ${instructions.split("\n")[0].slice(0, 60)}…`,
|
|
413
409
|
stats,
|
|
414
|
-
reviewDiff,
|
|
415
|
-
|
|
410
|
+
reviewDiff.diffText,
|
|
411
|
+
{
|
|
412
|
+
additionalInstructions: instructions,
|
|
413
|
+
diffInstruction: reviewDiff.diffInstruction,
|
|
414
|
+
},
|
|
416
415
|
);
|
|
417
416
|
}
|
|
418
417
|
|
|
@@ -449,6 +448,36 @@ async function getGitStatus(api: CustomCommandAPI): Promise<string> {
|
|
|
449
448
|
}
|
|
450
449
|
}
|
|
451
450
|
|
|
451
|
+
async function getUncommittedReviewDiff(api: CustomCommandAPI): Promise<CurrentReviewDiff> {
|
|
452
|
+
if (await jj.repo.is(api.cwd)) {
|
|
453
|
+
return {
|
|
454
|
+
diffText: await jj.diff(api.cwd),
|
|
455
|
+
diffInstruction: JJ_UNCOMMITTED_DIFF_INSTRUCTION,
|
|
456
|
+
emptyMessage: "No uncommitted changes found",
|
|
457
|
+
mode: "Reviewing JJ working-copy changes",
|
|
458
|
+
};
|
|
459
|
+
}
|
|
460
|
+
|
|
461
|
+
const status = await getGitStatus(api);
|
|
462
|
+
if (!status.trim()) {
|
|
463
|
+
return {
|
|
464
|
+
diffText: "",
|
|
465
|
+
diffInstruction: GIT_UNCOMMITTED_DIFF_INSTRUCTION,
|
|
466
|
+
emptyMessage: "No uncommitted changes found",
|
|
467
|
+
mode: "Reviewing uncommitted changes (staged + unstaged)",
|
|
468
|
+
};
|
|
469
|
+
}
|
|
470
|
+
|
|
471
|
+
const [unstagedDiff, stagedDiff] = await Promise.all([git.diff(api.cwd), git.diff(api.cwd, { cached: true })]);
|
|
472
|
+
const combinedDiff = [unstagedDiff, stagedDiff].filter(Boolean).join("\n");
|
|
473
|
+
return {
|
|
474
|
+
diffText: combinedDiff,
|
|
475
|
+
diffInstruction: GIT_UNCOMMITTED_DIFF_INSTRUCTION,
|
|
476
|
+
emptyMessage: "No diff content found",
|
|
477
|
+
mode: "Reviewing uncommitted changes (staged + unstaged)",
|
|
478
|
+
};
|
|
479
|
+
}
|
|
480
|
+
|
|
452
481
|
async function getRecentCommits(api: CustomCommandAPI, count: number): Promise<string[]> {
|
|
453
482
|
try {
|
|
454
483
|
return await git.log.onelines(api.cwd, count);
|