@ilya-lesikov/pi-pi 0.2.0 → 0.3.0
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/3p/pi-plannotator/apps/pi-extension/plannotator-browser.ts +1 -6
- package/3p/pi-plannotator/packages/shared/review-core.ts +1 -22
- package/3p/pi-plannotator/packages/shared/vcs-core.ts +1 -1
- package/extensions/orchestrator/event-handlers.ts +29 -11
- package/extensions/orchestrator/orchestrator.ts +15 -0
- package/extensions/orchestrator/pp-menu.ts +148 -66
- package/extensions/orchestrator/usage-tracker.ts +5 -2
- package/package.json +1 -1
|
@@ -442,12 +442,7 @@ export async function startCodeReviewBrowserSession(
|
|
|
442
442
|
gitRef = result.gitRef;
|
|
443
443
|
diffError = result.error;
|
|
444
444
|
initialBase = result.base;
|
|
445
|
-
|
|
446
|
-
const range = diffType.slice("range:".length);
|
|
447
|
-
if (!gitCtx.diffOptions.some((o: { id: string }) => o.id === diffType)) {
|
|
448
|
-
gitCtx.diffOptions.push({ id: diffType, label: `Commits ${range}` });
|
|
449
|
-
}
|
|
450
|
-
}
|
|
445
|
+
|
|
451
446
|
} else {
|
|
452
447
|
workspace = await buildLocalWorkspaceReview(cwd, {
|
|
453
448
|
requestedDiffType: options.diffType,
|
|
@@ -22,7 +22,6 @@ export type DiffType =
|
|
|
22
22
|
| "branch"
|
|
23
23
|
| "merge-base"
|
|
24
24
|
| "all"
|
|
25
|
-
| `range:${string}`
|
|
26
25
|
| `worktree:${string}`
|
|
27
26
|
| "p4-default"
|
|
28
27
|
| `p4-changelist:${string}`;
|
|
@@ -737,28 +736,8 @@ export async function runGitDiff(
|
|
|
737
736
|
break;
|
|
738
737
|
}
|
|
739
738
|
|
|
740
|
-
default:
|
|
741
|
-
if (effectiveDiffType.startsWith("range:")) {
|
|
742
|
-
const range = effectiveDiffType.slice("range:".length);
|
|
743
|
-
const rangeArgs = [
|
|
744
|
-
"diff",
|
|
745
|
-
"--no-ext-diff",
|
|
746
|
-
...wFlag,
|
|
747
|
-
"--src-prefix=a/",
|
|
748
|
-
"--dst-prefix=b/",
|
|
749
|
-
"--end-of-options",
|
|
750
|
-
range,
|
|
751
|
-
];
|
|
752
|
-
const rangeDiff = assertGitSuccess(
|
|
753
|
-
await runtime.runGit(rangeArgs, { cwd }),
|
|
754
|
-
rangeArgs,
|
|
755
|
-
);
|
|
756
|
-
patch = rangeDiff.stdout;
|
|
757
|
-
label = `Commits ${range}`;
|
|
758
|
-
break;
|
|
759
|
-
}
|
|
739
|
+
default:
|
|
760
740
|
return { patch: "", label: "Unknown diff type" };
|
|
761
|
-
}
|
|
762
741
|
}
|
|
763
742
|
} catch (error) {
|
|
764
743
|
const raw = error instanceof Error ? error.message : String(error);
|
|
@@ -158,7 +158,7 @@ export function createGitProvider(runtime: ReviewGitRuntime): VcsProvider {
|
|
|
158
158
|
},
|
|
159
159
|
|
|
160
160
|
ownsDiffType(diffType: string): boolean {
|
|
161
|
-
return GIT_DIFF_TYPES.has(diffType) || diffType.startsWith("worktree:")
|
|
161
|
+
return GIT_DIFF_TYPES.has(diffType) || diffType.startsWith("worktree:");
|
|
162
162
|
},
|
|
163
163
|
|
|
164
164
|
canStageFiles(diffType: string): boolean {
|
|
@@ -150,7 +150,7 @@ export async function enterReviewCycle(orchestrator: Orchestrator, ctx: any, kin
|
|
|
150
150
|
orchestrator.active.state.step = "synthesize";
|
|
151
151
|
saveTask(orchestrator.active.dir, orchestrator.active.state);
|
|
152
152
|
const feedback = result.feedback ? `\n\nFeedback:\n${result.feedback}` : "";
|
|
153
|
-
return `Plannotator requested changes.${feedback}\n\
|
|
153
|
+
return `Plannotator requested changes.${feedback}\n\nAddress the user's feedback. If the feedback contains questions, answer them. If it requests changes, make the changes. Then call pp_phase_complete when done.`;
|
|
154
154
|
}
|
|
155
155
|
|
|
156
156
|
orchestrator.active.state.reviewCycle = null;
|
|
@@ -276,9 +276,11 @@ function registerSpecifyReviewsTool(orchestrator: Orchestrator): void {
|
|
|
276
276
|
let hasNeedsChanges = false;
|
|
277
277
|
for (const review of params.reviews) {
|
|
278
278
|
ctx.ui?.setWorkingMessage?.(`Waiting for Plannotator review: ${review.range}…`);
|
|
279
|
+
const rangeBase = review.range.includes("..") ? review.range.split("..")[0] : review.range;
|
|
279
280
|
const result = await openCodeReviewDirect(pi, {
|
|
280
281
|
cwd: review.cwd,
|
|
281
|
-
diffType:
|
|
282
|
+
diffType: "branch",
|
|
283
|
+
defaultBranch: rangeBase,
|
|
282
284
|
});
|
|
283
285
|
if ("error" in result) {
|
|
284
286
|
results.push(`${review.cwd} (${review.range}): ${result.error}`);
|
|
@@ -299,7 +301,7 @@ function registerSpecifyReviewsTool(orchestrator: Orchestrator): void {
|
|
|
299
301
|
saveTask(orchestrator.active.dir, orchestrator.active.state);
|
|
300
302
|
}
|
|
301
303
|
return {
|
|
302
|
-
content: [{ type: "text" as const, text: `Plannotator review complete.\n\n${summary}\n\
|
|
304
|
+
content: [{ type: "text" as const, text: `Plannotator review complete.\n\n${summary}\n\nAddress the user's feedback. If the feedback contains questions, answer them. If it requests changes, make the changes. Then call pp_phase_complete when done.` }],
|
|
303
305
|
details: {},
|
|
304
306
|
};
|
|
305
307
|
}
|
|
@@ -309,10 +311,11 @@ function registerSpecifyReviewsTool(orchestrator: Orchestrator): void {
|
|
|
309
311
|
const { showActiveTaskMenu } = await import("./pp-menu.js");
|
|
310
312
|
const text = await showActiveTaskMenu(orchestrator, ctx, `Plannotator review complete.\n\n${summary}`, "tool");
|
|
311
313
|
if (orchestrator.phaseCompactionPending || orchestrator.taskDoneCompactionPending) {
|
|
312
|
-
|
|
314
|
+
await orchestrator.waitForCompaction();
|
|
315
|
+
return { content: [{ type: "text" as const, text: "Phase transition complete." }], details: {} };
|
|
313
316
|
}
|
|
314
317
|
if (!text) {
|
|
315
|
-
return { content: [{ type: "text" as const, text: "
|
|
318
|
+
return { content: [{ type: "text" as const, text: "User dismissed the menu. Wait for the user's next message. When you resume work, update USER_REQUEST.md and RESEARCH.md with any new findings before calling pp_phase_complete." }], details: {} };
|
|
316
319
|
}
|
|
317
320
|
return { content: [{ type: "text" as const, text }], details: {} };
|
|
318
321
|
} finally {
|
|
@@ -348,11 +351,22 @@ function registerCommitTool(orchestrator: Orchestrator): void {
|
|
|
348
351
|
if (!orchestrator.config.autoCommit) {
|
|
349
352
|
return { content: [{ type: "text" as const, text: "autoCommit is disabled in config." }], details: {} };
|
|
350
353
|
}
|
|
351
|
-
|
|
354
|
+
const files: string[] = [];
|
|
355
|
+
try {
|
|
356
|
+
const gitResult = await pi.exec("git", ["diff", "--name-only"], { cwd: orchestrator.cwd, timeout: 5000 });
|
|
357
|
+
if (gitResult.code === 0 && gitResult.stdout.trim()) {
|
|
358
|
+
files.push(...gitResult.stdout.trim().split("\n").filter(Boolean));
|
|
359
|
+
}
|
|
360
|
+
const stagedResult = await pi.exec("git", ["diff", "--name-only", "--cached"], { cwd: orchestrator.cwd, timeout: 5000 });
|
|
361
|
+
if (stagedResult.code === 0 && stagedResult.stdout.trim()) {
|
|
362
|
+
for (const f of stagedResult.stdout.trim().split("\n").filter(Boolean)) {
|
|
363
|
+
if (!files.includes(f)) files.push(f);
|
|
364
|
+
}
|
|
365
|
+
}
|
|
366
|
+
} catch {}
|
|
367
|
+
if (files.length === 0) {
|
|
352
368
|
return { content: [{ type: "text" as const, text: "No modified files to commit." }], details: {} };
|
|
353
369
|
}
|
|
354
|
-
|
|
355
|
-
const files = [...orchestrator.active.modifiedFiles];
|
|
356
370
|
const result = autoCommit(files, params.message, orchestrator.cwd);
|
|
357
371
|
if (result.ok) {
|
|
358
372
|
orchestrator.active.modifiedFiles.clear();
|
|
@@ -395,10 +409,11 @@ function registerPhaseCompleteTool(orchestrator: Orchestrator): void {
|
|
|
395
409
|
const { showActiveTaskMenu } = await import("./pp-menu.js");
|
|
396
410
|
const text = await showActiveTaskMenu(orchestrator, ctx, params.summary, "tool");
|
|
397
411
|
if (orchestrator.phaseCompactionPending || orchestrator.taskDoneCompactionPending) {
|
|
398
|
-
|
|
412
|
+
await orchestrator.waitForCompaction();
|
|
413
|
+
return { content: [{ type: "text" as const, text: "Phase transition complete." }], details: {} };
|
|
399
414
|
}
|
|
400
415
|
if (!text) {
|
|
401
|
-
return { content: [{ type: "text" as const, text: "
|
|
416
|
+
return { content: [{ type: "text" as const, text: "User dismissed the menu. Wait for the user's next message. When you resume work, update USER_REQUEST.md and RESEARCH.md with any new findings before calling pp_phase_complete." }], details: {} };
|
|
402
417
|
}
|
|
403
418
|
return { content: [{ type: "text" as const, text }], details: {} };
|
|
404
419
|
} finally {
|
|
@@ -728,6 +743,7 @@ export function registerEventHandlers(orchestrator: Orchestrator): void {
|
|
|
728
743
|
if (usageTracker && data?.tokens) {
|
|
729
744
|
usageTracker.recordSubagentCompletion(data.tokens, undefined, {
|
|
730
745
|
description: data.description || data.type || data.id || "unknown",
|
|
746
|
+
agentType: data.type || "unknown",
|
|
731
747
|
modelId: data.modelId || "unknown",
|
|
732
748
|
durationMs: data.durationMs,
|
|
733
749
|
toolUses: data.toolUses,
|
|
@@ -1258,7 +1274,9 @@ export function registerEventHandlers(orchestrator: Orchestrator): void {
|
|
|
1258
1274
|
const turnWasEmpty = !hasText && !hasToolCalls && !hasToolResults;
|
|
1259
1275
|
|
|
1260
1276
|
if (!turnWasEmpty) {
|
|
1261
|
-
|
|
1277
|
+
const lastPart = contentParts.length > 0 ? contentParts[contentParts.length - 1] : null;
|
|
1278
|
+
const endsWithText = lastPart?.type === "text" && lastPart?.text?.trim();
|
|
1279
|
+
if (hasText && (!hasToolCalls || endsWithText)) {
|
|
1262
1280
|
const step = orchestrator.active.state.step;
|
|
1263
1281
|
if (step !== "await_planners" && step !== "await_reviewers") {
|
|
1264
1282
|
pi.sendMessage(
|
|
@@ -458,6 +458,13 @@ export class Orchestrator {
|
|
|
458
458
|
}
|
|
459
459
|
}
|
|
460
460
|
|
|
461
|
+
waitForCompaction(): Promise<void> {
|
|
462
|
+
if (!this.phaseCompactionPending && !this.taskDoneCompactionPending) return Promise.resolve();
|
|
463
|
+
return new Promise((resolve) => {
|
|
464
|
+
this.phaseCompactionResolve = resolve;
|
|
465
|
+
});
|
|
466
|
+
}
|
|
467
|
+
|
|
461
468
|
compactAndTransition(ctx: ExtensionContext, taskDir: string, phase: Phase): void {
|
|
462
469
|
this.phaseCompactionPending = true;
|
|
463
470
|
ctx.compact({
|
|
@@ -469,6 +476,10 @@ export class Orchestrator {
|
|
|
469
476
|
this.phaseCompactionResolve = null;
|
|
470
477
|
}
|
|
471
478
|
this.phaseStartTime = Date.now();
|
|
479
|
+
if (this.active && (phase === "plan" || phase === "implement")) {
|
|
480
|
+
const modelConfig = this.config.mainModel.implement;
|
|
481
|
+
this.switchModel(ctx, modelConfig.model, modelConfig.thinking).catch(() => {});
|
|
482
|
+
}
|
|
472
483
|
this.injectContextAndArtifacts(taskDir, phase);
|
|
473
484
|
if (this.active?.state.phase === "plan" && this.active.state.step === "await_planners") {
|
|
474
485
|
ctx.ui.notify("Entered plan phase. Waiting for planners to complete before synthesis.", "info");
|
|
@@ -484,6 +495,10 @@ export class Orchestrator {
|
|
|
484
495
|
this.phaseCompactionResolve = null;
|
|
485
496
|
}
|
|
486
497
|
this.phaseStartTime = Date.now();
|
|
498
|
+
if (this.active && (phase === "plan" || phase === "implement")) {
|
|
499
|
+
const modelConfig = this.config.mainModel.implement;
|
|
500
|
+
this.switchModel(ctx, modelConfig.model, modelConfig.thinking).catch(() => {});
|
|
501
|
+
}
|
|
487
502
|
this.injectContextAndArtifacts(taskDir, phase);
|
|
488
503
|
if (this.active?.state.phase === "plan" && this.active.state.step === "await_planners") {
|
|
489
504
|
ctx.ui.notify("Entered plan phase. Waiting for planners to complete before synthesis.", "info");
|
|
@@ -592,7 +592,7 @@ function showUsage(ctx: any): void {
|
|
|
592
592
|
getTotalCacheReadTokens(): number; getTotalCacheWriteTokens(): number;
|
|
593
593
|
getTotalCost(): number; getCacheHitRate(): number;
|
|
594
594
|
getPerModelUsage(): Record<string, { inputTokens: number; outputTokens: number; cacheReadTokens: number; cacheWriteTokens: number; turns: number }>;
|
|
595
|
-
getSubagentList(): Array<{ description: string; modelId: string; inputTokens: number; outputTokens: number; cacheReadTokens: number; cacheWriteTokens: number; cost: number; durationMs: number; toolUses: number }>;
|
|
595
|
+
getSubagentList(): Array<{ description: string; agentType: string; modelId: string; inputTokens: number; outputTokens: number; cacheReadTokens: number; cacheWriteTokens: number; cost: number; durationMs: number; toolUses: number }>;
|
|
596
596
|
}
|
|
597
597
|
| undefined;
|
|
598
598
|
|
|
@@ -627,18 +627,18 @@ function showUsage(ctx: any): void {
|
|
|
627
627
|
: 0;
|
|
628
628
|
|
|
629
629
|
const byModel = new Map<string, { input: number; output: number; cacheRead: number; cacheWrite: number; cost: number }>();
|
|
630
|
-
|
|
630
|
+
const mainModelEntries = Object.entries(models);
|
|
631
|
+
const mainTotalTokens = mainModelEntries.reduce((s, [, u]) => s + u.inputTokens + u.outputTokens, 0);
|
|
632
|
+
for (const [modelId, usage] of mainModelEntries) {
|
|
633
|
+
const modelTokens = usage.inputTokens + usage.outputTokens;
|
|
634
|
+
const modelCostShare = mainTotalTokens > 0 ? mainCost * (modelTokens / mainTotalTokens) : 0;
|
|
631
635
|
byModel.set(modelId, {
|
|
632
636
|
input: usage.inputTokens, output: usage.outputTokens,
|
|
633
|
-
cacheRead: usage.cacheReadTokens, cacheWrite: usage.cacheWriteTokens, cost:
|
|
637
|
+
cacheRead: usage.cacheReadTokens, cacheWrite: usage.cacheWriteTokens, cost: modelCostShare,
|
|
634
638
|
});
|
|
635
639
|
}
|
|
636
|
-
if (mainCost > 0 && Object.keys(models).length === 1) {
|
|
637
|
-
const entry = byModel.values().next().value;
|
|
638
|
-
if (entry) entry.cost = mainCost;
|
|
639
|
-
}
|
|
640
640
|
for (const sa of subagents) {
|
|
641
|
-
const key = sa.modelId !== "unknown" ? sa.modelId : sa.description
|
|
641
|
+
const key = sa.modelId !== "unknown" ? sa.modelId : `subagent:${sa.description}`;
|
|
642
642
|
const existing = byModel.get(key);
|
|
643
643
|
if (existing) {
|
|
644
644
|
existing.input += sa.inputTokens;
|
|
@@ -658,7 +658,7 @@ function showUsage(ctx: any): void {
|
|
|
658
658
|
lines.push(` Input: ${formatTokenCount(totalInput)} tokens`);
|
|
659
659
|
lines.push(` Output: ${formatTokenCount(totalOutput)} tokens`);
|
|
660
660
|
if (totalCacheRead > 0) lines.push(` Cache: ⚡${Math.round(totalCacheRate * 100)}% hit rate`);
|
|
661
|
-
if (totalCost > 0) lines.push(` Cost: $${totalCost.toFixed(
|
|
661
|
+
if (totalCost > 0) lines.push(` Cost: $${totalCost.toFixed(2)}`);
|
|
662
662
|
|
|
663
663
|
if (byModel.size > 0) {
|
|
664
664
|
lines.push("");
|
|
@@ -667,31 +667,50 @@ function showUsage(ctx: any): void {
|
|
|
667
667
|
const cr = (m.cacheRead + m.input) > 0 ? Math.round(m.cacheRead / (m.cacheRead + m.input) * 100) : 0;
|
|
668
668
|
const parts = [`↑${formatTokenCount(m.input)}`, `↓${formatTokenCount(m.output)}`];
|
|
669
669
|
if (cr > 0) parts.push(`⚡${cr}%`);
|
|
670
|
-
if (m.cost > 0) parts.push(`$${m.cost.toFixed(
|
|
670
|
+
if (m.cost > 0) parts.push(`$${m.cost.toFixed(2)}`);
|
|
671
671
|
lines.push(` ${modelId}: ${parts.join(" ")}`);
|
|
672
672
|
}
|
|
673
673
|
}
|
|
674
674
|
|
|
675
675
|
lines.push("");
|
|
676
676
|
lines.push("By agent:");
|
|
677
|
-
const
|
|
678
|
-
if (
|
|
677
|
+
const agentModelNames = Object.keys(models);
|
|
678
|
+
if (agentModelNames.length > 0) {
|
|
679
679
|
const mainParts = [`↑${formatTokenCount(mainInput)}`, `↓${formatTokenCount(mainOutput)}`];
|
|
680
680
|
const mainCR = (mainCacheRead + mainInput) > 0 ? Math.round(mainCacheRead / (mainCacheRead + mainInput) * 100) : 0;
|
|
681
681
|
if (mainCR > 0) mainParts.push(`⚡${mainCR}%`);
|
|
682
|
-
if (mainCost > 0) mainParts.push(`$${mainCost.toFixed(
|
|
683
|
-
|
|
684
|
-
lines.push(` Main (${mainModel}): ${mainParts.join(" ")}`);
|
|
682
|
+
if (mainCost > 0) mainParts.push(`$${mainCost.toFixed(2)}`);
|
|
683
|
+
lines.push(` Main (${agentModelNames.join(", ")}): ${mainParts.join(" ")}`);
|
|
685
684
|
}
|
|
685
|
+
const byAgentType = new Map<string, { input: number; output: number; cacheRead: number; cost: number; durationMs: number; toolUses: number; count: number }>();
|
|
686
686
|
for (const sa of subagents) {
|
|
687
|
-
const
|
|
688
|
-
|
|
689
|
-
|
|
687
|
+
const key = sa.agentType || sa.description;
|
|
688
|
+
const existing = byAgentType.get(key);
|
|
689
|
+
if (existing) {
|
|
690
|
+
existing.input += sa.inputTokens;
|
|
691
|
+
existing.output += sa.outputTokens;
|
|
692
|
+
existing.cacheRead += sa.cacheReadTokens;
|
|
693
|
+
existing.cost += sa.cost;
|
|
694
|
+
existing.durationMs += sa.durationMs;
|
|
695
|
+
existing.toolUses += sa.toolUses;
|
|
696
|
+
existing.count += 1;
|
|
697
|
+
} else {
|
|
698
|
+
byAgentType.set(key, {
|
|
699
|
+
input: sa.inputTokens, output: sa.outputTokens, cacheRead: sa.cacheReadTokens,
|
|
700
|
+
cost: sa.cost, durationMs: sa.durationMs, toolUses: sa.toolUses, count: 1,
|
|
701
|
+
});
|
|
702
|
+
}
|
|
703
|
+
}
|
|
704
|
+
for (const [agentType, agg] of byAgentType) {
|
|
705
|
+
const saCR = (agg.cacheRead + agg.input) > 0
|
|
706
|
+
? Math.round(agg.cacheRead / (agg.cacheRead + agg.input) * 100) : 0;
|
|
707
|
+
const parts = [`↑${formatTokenCount(agg.input)}`, `↓${formatTokenCount(agg.output)}`];
|
|
690
708
|
if (saCR > 0) parts.push(`⚡${saCR}%`);
|
|
691
|
-
if (
|
|
692
|
-
if (
|
|
693
|
-
if (
|
|
694
|
-
|
|
709
|
+
if (agg.cost > 0) parts.push(`$${agg.cost.toFixed(2)}`);
|
|
710
|
+
if (agg.durationMs > 0) parts.push(formatDuration(agg.durationMs));
|
|
711
|
+
if (agg.toolUses > 0) parts.push(`${agg.toolUses} tools`);
|
|
712
|
+
const countSuffix = agg.count > 1 ? ` (×${agg.count})` : "";
|
|
713
|
+
lines.push(` ${agentType}${countSuffix}: ${parts.join(" ")}`);
|
|
695
714
|
}
|
|
696
715
|
|
|
697
716
|
ctx.ui.notify(lines.join("\n"), "info");
|
|
@@ -699,10 +718,12 @@ function showUsage(ctx: any): void {
|
|
|
699
718
|
|
|
700
719
|
async function showSettingsMenu(orchestrator: Orchestrator, ctx: any, showFlant = true): Promise<typeof BACK> {
|
|
701
720
|
while (true) {
|
|
702
|
-
const options: OptionInput[] = [
|
|
703
|
-
|
|
704
|
-
{ title: "
|
|
705
|
-
|
|
721
|
+
const options: OptionInput[] = [];
|
|
722
|
+
if (orchestrator.active) {
|
|
723
|
+
options.push({ title: "Task status", description: "Show current task phase, step, and timing" });
|
|
724
|
+
}
|
|
725
|
+
options.push({ title: "Usage", description: "Show session token usage and cost breakdown" });
|
|
726
|
+
options.push({ title: "LSP", description: "Language server status and controls" });
|
|
706
727
|
if (showFlant) {
|
|
707
728
|
options.push({ title: "Flant AI Infrastructure", description: "Configure corporate AI model provider" });
|
|
708
729
|
}
|
|
@@ -710,6 +731,10 @@ async function showSettingsMenu(orchestrator: Orchestrator, ctx: any, showFlant
|
|
|
710
731
|
|
|
711
732
|
const choice = await selectOption(ctx, "Settings", options);
|
|
712
733
|
if (!choice || choice === "Back") return BACK;
|
|
734
|
+
if (choice === "Task status") {
|
|
735
|
+
showStatus(orchestrator, ctx);
|
|
736
|
+
continue;
|
|
737
|
+
}
|
|
713
738
|
if (choice === "Usage") {
|
|
714
739
|
showUsage(ctx);
|
|
715
740
|
continue;
|
|
@@ -878,9 +903,10 @@ async function detectCurrentPrContext(orchestrator: Orchestrator): Promise<{ prU
|
|
|
878
903
|
}
|
|
879
904
|
}
|
|
880
905
|
|
|
881
|
-
async function
|
|
882
|
-
if (!orchestrator.active) return "No active task.";
|
|
906
|
+
async function openCodeReviewInPlannotator(orchestrator: Orchestrator, diffType?: string, defaultBranch?: string): Promise<string> {
|
|
883
907
|
const payload: Record<string, unknown> = { cwd: orchestrator.cwd };
|
|
908
|
+
if (diffType) payload.diffType = diffType;
|
|
909
|
+
if (defaultBranch) payload.defaultBranch = defaultBranch;
|
|
884
910
|
|
|
885
911
|
return await new Promise((resolve) => {
|
|
886
912
|
let handled = false;
|
|
@@ -1109,33 +1135,20 @@ export async function showActiveTaskMenu(
|
|
|
1109
1135
|
const options: OptionInput[] = [];
|
|
1110
1136
|
options.push(opt("Finish", "Complete, pause, or continue to next phase"));
|
|
1111
1137
|
if (!waiting) {
|
|
1112
|
-
options.push(opt(
|
|
1113
|
-
options.push(opt(deepLabel, "Run automated review with higher thinking level"));
|
|
1114
|
-
if (hasPlannotator) {
|
|
1115
|
-
if (isReviewPhase) {
|
|
1116
|
-
options.push(opt("Review in Plannotator", "Open visual diff review in browser"));
|
|
1117
|
-
} else {
|
|
1118
|
-
options.push(opt("Review in Plannotator", "Open visual review in browser"));
|
|
1119
|
-
}
|
|
1120
|
-
if (!isReviewPhase) {
|
|
1121
|
-
options.push(opt("Review on my own", "Review manually, then continue"));
|
|
1122
|
-
}
|
|
1123
|
-
}
|
|
1138
|
+
options.push(opt("Review", "Auto review, Plannotator, or manual review"));
|
|
1124
1139
|
}
|
|
1125
1140
|
options.push(opt("Subagents", "Manage running agents"));
|
|
1126
|
-
options.push(opt("
|
|
1127
|
-
options.push(opt("Settings", "LSP and other configuration"));
|
|
1141
|
+
options.push(opt("Settings", "Task status, usage, and LSP"));
|
|
1128
1142
|
options.push(opt("Back", "Return to the prompt and keep working"));
|
|
1129
1143
|
|
|
1130
|
-
const
|
|
1144
|
+
const headerLines = [`/pp\n\nTask: ${task.type}\nPhase: ${phase}`];
|
|
1145
|
+
if (summary !== "/pp") headerLines.push(`\n${summary}`);
|
|
1146
|
+
const menuTitle = headerLines.join("");
|
|
1147
|
+
const choice = await selectOption(ctx, menuTitle, options);
|
|
1131
1148
|
if (!choice || choice === "Back") {
|
|
1132
1149
|
return "";
|
|
1133
1150
|
}
|
|
1134
1151
|
|
|
1135
|
-
if (choice === "Status") {
|
|
1136
|
-
showStatus(orchestrator, ctx);
|
|
1137
|
-
continue;
|
|
1138
|
-
}
|
|
1139
1152
|
if (choice === "Subagents") {
|
|
1140
1153
|
await showSubagentsMenu(ctx);
|
|
1141
1154
|
continue;
|
|
@@ -1179,17 +1192,95 @@ export async function showActiveTaskMenu(
|
|
|
1179
1192
|
return "";
|
|
1180
1193
|
}
|
|
1181
1194
|
|
|
1182
|
-
if (choice === "Review
|
|
1183
|
-
const
|
|
1184
|
-
|
|
1185
|
-
|
|
1186
|
-
|
|
1195
|
+
if (choice === "Review") {
|
|
1196
|
+
const reviewOptions: OptionInput[] = [
|
|
1197
|
+
opt(autoLabel, "Run automated review with configured reviewers"),
|
|
1198
|
+
opt(deepLabel, "Run automated review with higher thinking level"),
|
|
1199
|
+
];
|
|
1200
|
+
if (hasPlannotator) {
|
|
1201
|
+
reviewOptions.push(opt("Review in Plannotator", phase === "plan" ? "Open plan review in browser" : "Open code diff review in browser"));
|
|
1202
|
+
}
|
|
1203
|
+
reviewOptions.push(opt("Review on my own", "Review manually, then continue"));
|
|
1204
|
+
reviewOptions.push(opt("Back", "Return to the previous menu"));
|
|
1205
|
+
|
|
1206
|
+
const reviewChoice = await selectOption(ctx, "Review", reviewOptions);
|
|
1207
|
+
if (!reviewChoice || reviewChoice === "Back") continue;
|
|
1208
|
+
|
|
1209
|
+
if (reviewChoice === "Review in Plannotator") {
|
|
1210
|
+
if (phase === "plan") {
|
|
1211
|
+
finalizeReviewCycle(task);
|
|
1212
|
+
const text = await enterReviewCycle(orchestrator, ctx, "plannotator");
|
|
1213
|
+
const curStep = orchestrator.active?.state.step;
|
|
1214
|
+
if (curStep === "await_reviewers") return "";
|
|
1215
|
+
const handled = handleReviewResult(ctx, text);
|
|
1216
|
+
if (handled.continueLoop) continue;
|
|
1217
|
+
return handled.text ?? text;
|
|
1218
|
+
}
|
|
1219
|
+
const diffChoice = await selectOption(ctx, "Review in Plannotator", [
|
|
1220
|
+
opt("All branch changes", "Committed changes vs base branch"),
|
|
1221
|
+
opt("Last commit", "Changes in the most recent commit"),
|
|
1222
|
+
opt("Recent commits", "Choose how many recent commits to review"),
|
|
1223
|
+
opt("Uncommitted changes", "Working directory changes"),
|
|
1224
|
+
opt("Back", "Return to the previous menu"),
|
|
1225
|
+
]);
|
|
1226
|
+
if (!diffChoice || diffChoice === "Back") continue;
|
|
1227
|
+
|
|
1228
|
+
let diffType: string | undefined;
|
|
1229
|
+
let defaultBranch: string | undefined;
|
|
1230
|
+
if (diffChoice === "All branch changes") {
|
|
1231
|
+
diffType = "branch";
|
|
1232
|
+
} else if (diffChoice === "Last commit") {
|
|
1233
|
+
diffType = "last-commit";
|
|
1234
|
+
} else if (diffChoice === "Recent commits") {
|
|
1235
|
+
let commits: Array<{ hash: string; message: string; age: string }> = [];
|
|
1236
|
+
try {
|
|
1237
|
+
const logResult = await orchestrator.pi.exec(
|
|
1238
|
+
"git", ["log", "--oneline", "--format=%h\t%s\t%cr", "-15"],
|
|
1239
|
+
{ cwd: orchestrator.cwd, timeout: 5000 },
|
|
1240
|
+
);
|
|
1241
|
+
if (logResult.code === 0 && logResult.stdout.trim()) {
|
|
1242
|
+
commits = logResult.stdout.trim().split("\n").map((line) => {
|
|
1243
|
+
const [hash, message, age] = line.split("\t");
|
|
1244
|
+
return { hash: hash || "", message: message || "", age: age || "" };
|
|
1245
|
+
}).filter((c) => c.hash);
|
|
1246
|
+
}
|
|
1247
|
+
} catch {}
|
|
1248
|
+
if (commits.length === 0) {
|
|
1249
|
+
ctx.ui.notify("No commits found.", "info");
|
|
1250
|
+
continue;
|
|
1251
|
+
}
|
|
1252
|
+
const commitOptions: OptionInput[] = commits.map((c) => ({
|
|
1253
|
+
title: `${c.hash} ${c.message}`,
|
|
1254
|
+
description: c.age,
|
|
1255
|
+
}));
|
|
1256
|
+
commitOptions.push(opt("Back", "Return to the previous menu"));
|
|
1257
|
+
const picked = await selectOption(ctx, "Review changes since:", commitOptions);
|
|
1258
|
+
if (!picked || picked === "Back") continue;
|
|
1259
|
+
const pickedHash = picked.split(" ")[0];
|
|
1260
|
+
if (!pickedHash) continue;
|
|
1261
|
+
diffType = "branch";
|
|
1262
|
+
defaultBranch = pickedHash;
|
|
1263
|
+
} else {
|
|
1264
|
+
diffType = "uncommitted";
|
|
1265
|
+
}
|
|
1266
|
+
|
|
1267
|
+
const text = await openCodeReviewInPlannotator(orchestrator, diffType, defaultBranch);
|
|
1268
|
+
ctx.ui.notify(text, "info");
|
|
1269
|
+
continue;
|
|
1270
|
+
}
|
|
1187
1271
|
|
|
1188
|
-
|
|
1272
|
+
if (reviewChoice === "Review on my own") {
|
|
1273
|
+
if (phase === "plan") {
|
|
1274
|
+
setStep(orchestrator, "synthesize");
|
|
1275
|
+
} else {
|
|
1276
|
+
setStep(orchestrator, "llm_work");
|
|
1277
|
+
}
|
|
1278
|
+
return continueMessage;
|
|
1279
|
+
}
|
|
1189
1280
|
|
|
1190
|
-
|
|
1191
|
-
const kind =
|
|
1192
|
-
if (
|
|
1281
|
+
finalizeReviewCycle(task);
|
|
1282
|
+
const kind = reviewChoice === autoLabel ? "auto" as const : "auto-deep" as const;
|
|
1283
|
+
if (!hasEnabledReviewers(orchestrator, kind)) {
|
|
1193
1284
|
const label = phase === "brainstorm" ? "brainstorm" : phase === "plan" ? "plan" : "code";
|
|
1194
1285
|
ctx.ui.notify(`No ${label} reviewers enabled.`, "info");
|
|
1195
1286
|
continue;
|
|
@@ -1201,15 +1292,6 @@ export async function showActiveTaskMenu(
|
|
|
1201
1292
|
if (handled.continueLoop) continue;
|
|
1202
1293
|
return handled.text ?? text;
|
|
1203
1294
|
}
|
|
1204
|
-
|
|
1205
|
-
if (choice === "Review on my own") {
|
|
1206
|
-
if (phase === "plan") {
|
|
1207
|
-
setStep(orchestrator, "synthesize");
|
|
1208
|
-
} else {
|
|
1209
|
-
setStep(orchestrator, "llm_work");
|
|
1210
|
-
}
|
|
1211
|
-
return continueMessage;
|
|
1212
|
-
}
|
|
1213
1295
|
}
|
|
1214
1296
|
}
|
|
1215
1297
|
|
|
@@ -1217,6 +1299,6 @@ export async function showPpMenu(orchestrator: Orchestrator, ctx: any, mode: Men
|
|
|
1217
1299
|
if (!orchestrator.active) {
|
|
1218
1300
|
return showNoActiveMenu(orchestrator, ctx);
|
|
1219
1301
|
}
|
|
1220
|
-
const text = await showActiveTaskMenu(orchestrator, ctx, "
|
|
1302
|
+
const text = await showActiveTaskMenu(orchestrator, ctx, "/pp", mode);
|
|
1221
1303
|
return text || undefined;
|
|
1222
1304
|
}
|
|
@@ -12,7 +12,7 @@ export interface ModelUsage {
|
|
|
12
12
|
|
|
13
13
|
export interface UsageTracker {
|
|
14
14
|
recordTurn(modelId: string, provider: string, input: number, output: number, cacheRead: number, cacheWrite: number, cost: number): void;
|
|
15
|
-
recordSubagentCompletion(tokens: { input?: number; output?: number; total?: number }, cost?: number, meta?: { description?: string; modelId?: string; durationMs?: number; toolUses?: number }): void;
|
|
15
|
+
recordSubagentCompletion(tokens: { input?: number; output?: number; total?: number }, cost?: number, meta?: { description?: string; agentType?: string; modelId?: string; durationMs?: number; toolUses?: number }): void;
|
|
16
16
|
loadFromSummary(summary: Record<string, unknown>): void;
|
|
17
17
|
getTotalInputTokens(): number;
|
|
18
18
|
getTotalOutputTokens(): number;
|
|
@@ -29,6 +29,7 @@ export interface UsageTracker {
|
|
|
29
29
|
|
|
30
30
|
export interface SubagentUsage {
|
|
31
31
|
description: string;
|
|
32
|
+
agentType: string;
|
|
32
33
|
modelId: string;
|
|
33
34
|
inputTokens: number;
|
|
34
35
|
outputTokens: number;
|
|
@@ -124,7 +125,7 @@ export function createUsageTracker(): UsageTracker {
|
|
|
124
125
|
recordSubagentCompletion(
|
|
125
126
|
tokens: { input?: number; output?: number; cacheRead?: number; cacheWrite?: number; total?: number; cost?: number },
|
|
126
127
|
cost?: number,
|
|
127
|
-
meta?: { description?: string; modelId?: string; durationMs?: number; toolUses?: number },
|
|
128
|
+
meta?: { description?: string; agentType?: string; modelId?: string; durationMs?: number; toolUses?: number },
|
|
128
129
|
): void {
|
|
129
130
|
const safeInput = toFiniteNumber(tokens.input);
|
|
130
131
|
const safeOutput = toFiniteNumber(tokens.output);
|
|
@@ -141,6 +142,7 @@ export function createUsageTracker(): UsageTracker {
|
|
|
141
142
|
|
|
142
143
|
state.subagents.push({
|
|
143
144
|
description: meta?.description ?? "unknown",
|
|
145
|
+
agentType: meta?.agentType ?? "unknown",
|
|
144
146
|
modelId: meta?.modelId ?? "unknown",
|
|
145
147
|
inputTokens: effectiveInput,
|
|
146
148
|
outputTokens: safeOutput,
|
|
@@ -168,6 +170,7 @@ export function createUsageTracker(): UsageTracker {
|
|
|
168
170
|
for (const sa of summary.subagents as Record<string, unknown>[]) {
|
|
169
171
|
const entry: SubagentUsage = {
|
|
170
172
|
description: typeof sa.description === "string" ? sa.description : "unknown",
|
|
173
|
+
agentType: typeof sa.agentType === "string" ? sa.agentType : "unknown",
|
|
171
174
|
modelId: typeof sa.modelId === "string" ? sa.modelId : "unknown",
|
|
172
175
|
inputTokens: toFiniteNumber(sa.inputTokens),
|
|
173
176
|
outputTokens: toFiniteNumber(sa.outputTokens),
|