@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.
@@ -442,12 +442,7 @@ export async function startCodeReviewBrowserSession(
442
442
  gitRef = result.gitRef;
443
443
  diffError = result.error;
444
444
  initialBase = result.base;
445
- if (typeof diffType === "string" && diffType.startsWith("range:") && gitCtx) {
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:") || diffType.startsWith("range:");
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\nUser wants to continue. Run /pp when ready to advance.`;
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: `range:${review.range}`,
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\nFix the issues noted above, then call pp_phase_complete when done.` }],
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
- return { content: [{ type: "text" as const, text: "Phase transition in progress." }], details: {} };
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: "No action selected." }], details: {} };
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
- if (orchestrator.active.modifiedFiles.size === 0) {
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
- return { content: [{ type: "text" as const, text: "Phase transition in progress." }], details: {} };
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: "No action selected." }], details: {} };
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
- if (hasText && !hasToolCalls) {
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
- for (const [modelId, usage] of Object.entries(models)) {
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: 0,
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(3)}`);
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(3)}`);
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 mainModelEntries = Object.entries(models);
678
- if (mainModelEntries.length > 0) {
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(3)}`);
683
- const mainModel = mainModelEntries.map(([id]) => id).join(", ");
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 saCR = (sa.cacheReadTokens + sa.inputTokens) > 0
688
- ? Math.round(sa.cacheReadTokens / (sa.cacheReadTokens + sa.inputTokens) * 100) : 0;
689
- const parts = [`↑${formatTokenCount(sa.inputTokens)}`, `↓${formatTokenCount(sa.outputTokens)}`];
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 (sa.cost > 0) parts.push(`$${sa.cost.toFixed(3)}`);
692
- if (sa.durationMs > 0) parts.push(formatDuration(sa.durationMs));
693
- if (sa.toolUses > 0) parts.push(`${sa.toolUses} tools`);
694
- lines.push(` ${sa.description}: ${parts.join(" ")}`);
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
- { title: "Usage", description: "Show session token usage and cost breakdown" },
704
- { title: "LSP", description: "Language server status and controls" },
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 openReviewTaskInPlannotator(orchestrator: Orchestrator): Promise<string> {
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(autoLabel, "Run automated review with configured reviewers"));
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("Status", "Show current task phase, step, and timing"));
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 choice = await selectOption(ctx, summary, options);
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 in Plannotator" && isReviewPhase) {
1183
- const text = await openReviewTaskInPlannotator(orchestrator);
1184
- ctx.ui.notify(text, "info");
1185
- continue;
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
- finalizeReviewCycle(task);
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
- if (choice === autoLabel || choice === deepLabel || choice === "Review in Plannotator") {
1191
- const kind = choice === autoLabel ? "auto" as const : choice === deepLabel ? "auto-deep" as const : "plannotator" as const;
1192
- if (kind !== "plannotator" && !hasEnabledReviewers(orchestrator, kind)) {
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, "Choose next action", mode);
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),
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@ilya-lesikov/pi-pi",
3
- "version": "0.2.0",
3
+ "version": "0.3.0",
4
4
  "description": "Pi-Pi Coding Agent: based on Pi Coding Agent, but twice as good",
5
5
  "keywords": [
6
6
  "pi-package",