@ilya-lesikov/pi-pi 0.1.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.
@@ -1,4 +1,4 @@
1
- import { existsSync, readdirSync } from "fs";
1
+ import { existsSync, readdirSync, writeFileSync } from "fs";
2
2
  import { join, relative } from "path";
3
3
  import { askUser } from "../../3p/pi-ask-user/index.js";
4
4
  import { unregisterAgentDefinitions } from "./agents/registry.js";
@@ -8,12 +8,13 @@ import {
8
8
  loadCodeReviewOutputs,
9
9
  loadPlanReviewOutputs,
10
10
  } from "./context.js";
11
- import { enterReviewCycle, finalizeReviewCycle } from "./event-handlers.js";
11
+ import { detectDefaultBranch, enterReviewCycle, finalizeReviewCycle } from "./event-handlers.js";
12
12
  import { Orchestrator, deepReviewConfig } from "./orchestrator.js";
13
13
  import { cancelPendingPlannotatorWait } from "./plannotator.js";
14
14
  import { spawnPlanners, spawnPlanReviewers } from "./phases/planning.js";
15
15
  import { spawnCodeReviewers } from "./phases/review.js";
16
16
  import { spawnBrainstormReviewers } from "./phases/brainstorm.js";
17
+
17
18
  import {
18
19
  listTasks,
19
20
  loadTask,
@@ -72,6 +73,38 @@ function showStatus(orchestrator: Orchestrator, ctx: any): void {
72
73
  );
73
74
  }
74
75
 
76
+ async function abortCurrentWork(orchestrator: Orchestrator, ctx: any): Promise<void> {
77
+ orchestrator.abortAllSubagents();
78
+ ctx.abort?.();
79
+ await ctx.waitForIdle?.();
80
+ const taskStore = (globalThis as any)[Symbol.for("pi-tasks:store")];
81
+ taskStore?.clearAll?.();
82
+ taskStore?.refreshWidget?.(ctx.ui);
83
+ }
84
+
85
+ async function pauseTask(orchestrator: Orchestrator, ctx: any): Promise<string> {
86
+ if (!orchestrator.active) return "No active task.";
87
+
88
+ cancelPendingPlannotatorWait(orchestrator);
89
+ orchestrator.abortAllSubagents();
90
+ ctx.abort?.();
91
+ await ctx.waitForIdle?.();
92
+
93
+ const name = orchestrator.active.description;
94
+
95
+ saveTask(orchestrator.active.dir, orchestrator.active.state);
96
+ unregisterAgentDefinitions(orchestrator.pi);
97
+ await orchestrator.cleanupActive();
98
+
99
+ const taskStore = (globalThis as any)[Symbol.for("pi-tasks:store")];
100
+ taskStore?.clearAll?.();
101
+ taskStore?.refreshWidget?.(ctx.ui);
102
+
103
+ orchestrator.updateStatus(ctx);
104
+ ctx.ui.notify(`Task "${name}" paused. Use /pp → Resume to continue.`, "info");
105
+ return `Task "${name}" paused.`;
106
+ }
107
+
75
108
  async function finishTask(orchestrator: Orchestrator, ctx: any): Promise<string> {
76
109
  if (!orchestrator.active) return "No active task.";
77
110
 
@@ -162,7 +195,12 @@ export async function resumeTask(
162
195
  description: task.state.description,
163
196
  };
164
197
 
165
- const modelConfig = orchestrator.config.mainModel[task.type === "debug" ? "debug" : task.type === "brainstorm" ? "brainstorm" : "implement"];
198
+ const modelConfig = orchestrator.config.mainModel[
199
+ task.type === "debug" ? "debug"
200
+ : task.type === "brainstorm" ? "brainstorm"
201
+ : task.type === "review" ? "review"
202
+ : "implement"
203
+ ];
166
204
  const modelOk = await orchestrator.switchModel(ctx, modelConfig.model, modelConfig.thinking);
167
205
  if (!modelOk) {
168
206
  ctx.ui.notify(`Model "${modelConfig.model}" not found — using current model`, "warning");
@@ -401,6 +439,7 @@ function collectRoleAssignments(config: Partial<PiPiConfig> | null): string[] {
401
439
  add("mainModel.implement", config.mainModel?.implement?.model);
402
440
  add("mainModel.debug", config.mainModel?.debug?.model);
403
441
  add("mainModel.brainstorm", config.mainModel?.brainstorm?.model);
442
+ add("mainModel.review", config.mainModel?.review?.model);
404
443
 
405
444
  for (const [name, variant] of Object.entries(config.planners ?? {})) {
406
445
  if (variant.enabled) add(`planners.${name}`, variant.model);
@@ -500,7 +539,14 @@ async function showFlantInfraMenu(orchestrator: Orchestrator, ctx: any): Promise
500
539
  }
501
540
 
502
541
  if (choice.startsWith("Cache period:")) {
503
- const selected = await selectOption(ctx, "Cache period", ["1 day", "3 days", "7 days", "14 days", "30 days", "Back"]);
542
+ const selected = await selectOption(ctx, "Cache period", [
543
+ { title: "1 day", description: "Refresh model metadata daily" },
544
+ { title: "3 days", description: "Refresh model metadata every three days" },
545
+ { title: "7 days", description: "Default — refresh weekly" },
546
+ { title: "14 days", description: "Refresh model metadata every two weeks" },
547
+ { title: "30 days", description: "Refresh model metadata monthly" },
548
+ { title: "Back", description: "Return to the previous menu" },
549
+ ]);
504
550
  if (!selected || selected === "Back") continue;
505
551
  const days = Number(selected.split(" ")[0]);
506
552
  if (!Number.isFinite(days) || days <= 0) continue;
@@ -520,13 +566,183 @@ async function showFlantInfraMenu(orchestrator: Orchestrator, ctx: any): Promise
520
566
  }
521
567
  }
522
568
 
523
- async function showSettingsMenu(orchestrator: Orchestrator, ctx: any): Promise<typeof BACK> {
569
+ function formatTokenCount(count: number): string {
570
+ if (count < 1000) return String(count);
571
+ if (count < 10000) return `${(count / 1000).toFixed(1)}k`;
572
+ if (count < 1000000) return `${Math.round(count / 1000)}k`;
573
+ if (count < 10000000) return `${(count / 1000000).toFixed(1)}M`;
574
+ return `${Math.round(count / 1000000)}M`;
575
+ }
576
+
577
+ function formatDuration(ms: number): string {
578
+ const sec = Math.floor(ms / 1000);
579
+ if (sec < 60) return `${sec}s`;
580
+ const min = Math.floor(sec / 60);
581
+ const remSec = sec % 60;
582
+ if (min < 60) return remSec > 0 ? `${min}m ${remSec}s` : `${min}m`;
583
+ const hr = Math.floor(min / 60);
584
+ const remMin = min % 60;
585
+ return remMin > 0 ? `${hr}h ${remMin}m` : `${hr}h`;
586
+ }
587
+
588
+ function showUsage(ctx: any): void {
589
+ const tracker = (globalThis as any)[Symbol.for("pi-pi:usage-tracker")] as
590
+ | {
591
+ getTotalInputTokens(): number; getTotalOutputTokens(): number;
592
+ getTotalCacheReadTokens(): number; getTotalCacheWriteTokens(): number;
593
+ getTotalCost(): number; getCacheHitRate(): number;
594
+ getPerModelUsage(): Record<string, { inputTokens: number; outputTokens: number; cacheReadTokens: number; cacheWriteTokens: number; turns: 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
+ }
597
+ | undefined;
598
+
599
+ if (!tracker) {
600
+ ctx.ui.notify("No usage data available.", "info");
601
+ return;
602
+ }
603
+
604
+ const mainInput = tracker.getTotalInputTokens();
605
+ const mainOutput = tracker.getTotalOutputTokens();
606
+ const mainCacheRead = tracker.getTotalCacheReadTokens();
607
+ const mainCacheWrite = tracker.getTotalCacheWriteTokens();
608
+ const mainCost = tracker.getTotalCost();
609
+ const models = tracker.getPerModelUsage();
610
+ const subagents = tracker.getSubagentList();
611
+
612
+ let saInput = 0, saOutput = 0, saCacheRead = 0, saCacheWrite = 0, saCost = 0;
613
+ for (const sa of subagents) {
614
+ saInput += sa.inputTokens;
615
+ saOutput += sa.outputTokens;
616
+ saCacheRead += sa.cacheReadTokens;
617
+ saCacheWrite += sa.cacheWriteTokens;
618
+ saCost += sa.cost;
619
+ }
620
+
621
+ const totalInput = mainInput + saInput;
622
+ const totalOutput = mainOutput + saOutput;
623
+ const totalCacheRead = mainCacheRead + saCacheRead;
624
+ const totalCost = mainCost + saCost;
625
+ const totalCacheRate = (totalCacheRead + totalInput) > 0
626
+ ? totalCacheRead / (totalCacheRead + totalInput)
627
+ : 0;
628
+
629
+ const byModel = new Map<string, { input: number; output: number; cacheRead: number; cacheWrite: number; cost: number }>();
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;
635
+ byModel.set(modelId, {
636
+ input: usage.inputTokens, output: usage.outputTokens,
637
+ cacheRead: usage.cacheReadTokens, cacheWrite: usage.cacheWriteTokens, cost: modelCostShare,
638
+ });
639
+ }
640
+ for (const sa of subagents) {
641
+ const key = sa.modelId !== "unknown" ? sa.modelId : `subagent:${sa.description}`;
642
+ const existing = byModel.get(key);
643
+ if (existing) {
644
+ existing.input += sa.inputTokens;
645
+ existing.output += sa.outputTokens;
646
+ existing.cacheRead += sa.cacheReadTokens;
647
+ existing.cacheWrite += sa.cacheWriteTokens;
648
+ existing.cost += sa.cost;
649
+ } else {
650
+ byModel.set(key, {
651
+ input: sa.inputTokens, output: sa.outputTokens,
652
+ cacheRead: sa.cacheReadTokens, cacheWrite: sa.cacheWriteTokens, cost: sa.cost,
653
+ });
654
+ }
655
+ }
656
+
657
+ const lines: string[] = ["Session usage (total):"];
658
+ lines.push(` Input: ${formatTokenCount(totalInput)} tokens`);
659
+ lines.push(` Output: ${formatTokenCount(totalOutput)} tokens`);
660
+ if (totalCacheRead > 0) lines.push(` Cache: ⚡${Math.round(totalCacheRate * 100)}% hit rate`);
661
+ if (totalCost > 0) lines.push(` Cost: $${totalCost.toFixed(2)}`);
662
+
663
+ if (byModel.size > 0) {
664
+ lines.push("");
665
+ lines.push("By model:");
666
+ for (const [modelId, m] of byModel) {
667
+ const cr = (m.cacheRead + m.input) > 0 ? Math.round(m.cacheRead / (m.cacheRead + m.input) * 100) : 0;
668
+ const parts = [`↑${formatTokenCount(m.input)}`, `↓${formatTokenCount(m.output)}`];
669
+ if (cr > 0) parts.push(`⚡${cr}%`);
670
+ if (m.cost > 0) parts.push(`$${m.cost.toFixed(2)}`);
671
+ lines.push(` ${modelId}: ${parts.join(" ")}`);
672
+ }
673
+ }
674
+
675
+ lines.push("");
676
+ lines.push("By agent:");
677
+ const agentModelNames = Object.keys(models);
678
+ if (agentModelNames.length > 0) {
679
+ const mainParts = [`↑${formatTokenCount(mainInput)}`, `↓${formatTokenCount(mainOutput)}`];
680
+ const mainCR = (mainCacheRead + mainInput) > 0 ? Math.round(mainCacheRead / (mainCacheRead + mainInput) * 100) : 0;
681
+ if (mainCR > 0) mainParts.push(`⚡${mainCR}%`);
682
+ if (mainCost > 0) mainParts.push(`$${mainCost.toFixed(2)}`);
683
+ lines.push(` Main (${agentModelNames.join(", ")}): ${mainParts.join(" ")}`);
684
+ }
685
+ const byAgentType = new Map<string, { input: number; output: number; cacheRead: number; cost: number; durationMs: number; toolUses: number; count: number }>();
686
+ for (const sa of subagents) {
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)}`];
708
+ if (saCR > 0) parts.push(`⚡${saCR}%`);
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(" ")}`);
714
+ }
715
+
716
+ ctx.ui.notify(lines.join("\n"), "info");
717
+ }
718
+
719
+ async function showSettingsMenu(orchestrator: Orchestrator, ctx: any, showFlant = true): Promise<typeof BACK> {
524
720
  while (true) {
525
- const choice = await selectOption(ctx, "Settings", [
526
- { title: "Flant AI Infrastructure", description: "Configure corporate AI model provider" },
527
- { title: "Back", description: "Return to the previous menu" },
528
- ]);
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" });
727
+ if (showFlant) {
728
+ options.push({ title: "Flant AI Infrastructure", description: "Configure corporate AI model provider" });
729
+ }
730
+ options.push({ title: "Back", description: "Return to the previous menu" });
731
+
732
+ const choice = await selectOption(ctx, "Settings", options);
529
733
  if (!choice || choice === "Back") return BACK;
734
+ if (choice === "Task status") {
735
+ showStatus(orchestrator, ctx);
736
+ continue;
737
+ }
738
+ if (choice === "Usage") {
739
+ showUsage(ctx);
740
+ continue;
741
+ }
742
+ if (choice === "LSP") {
743
+ await showLspMenu(ctx);
744
+ continue;
745
+ }
530
746
  await showFlantInfraMenu(orchestrator, ctx);
531
747
  }
532
748
  }
@@ -643,6 +859,149 @@ async function showImplementMenu(orchestrator: Orchestrator, ctx: any): Promise<
643
859
  }
644
860
  }
645
861
 
862
+ function buildPrContext(parsed: any): { prUrl: string | null; prContext: string | null } {
863
+ const title = typeof parsed?.title === "string" ? parsed.title.trim() : "";
864
+ const body = typeof parsed?.body === "string" ? parsed.body.trim() : "";
865
+ const commentsRaw = Array.isArray(parsed?.comments)
866
+ ? parsed.comments
867
+ : Array.isArray(parsed?.comments?.nodes)
868
+ ? parsed.comments.nodes
869
+ : [];
870
+ const comments: string[] = commentsRaw
871
+ .map((comment: any) => {
872
+ const text = typeof comment?.body === "string"
873
+ ? comment.body.trim()
874
+ : typeof comment?.bodyText === "string"
875
+ ? comment.bodyText.trim()
876
+ : "";
877
+ return text;
878
+ })
879
+ .filter((text: string): text is string => text.length > 0);
880
+
881
+ const parts: string[] = [];
882
+ if (title) parts.push(`Title: ${title}`);
883
+ if (body) parts.push(`Body:\n${body}`);
884
+ if (comments.length > 0) {
885
+ parts.push(`Comments:\n${comments.map((comment, index) => `${index + 1}. ${comment}`).join("\n\n")}`);
886
+ }
887
+
888
+ const prUrl = typeof parsed?.url === "string" && parsed.url.trim().length > 0 ? parsed.url.trim() : null;
889
+ return { prUrl, prContext: parts.length > 0 ? parts.join("\n\n") : null };
890
+ }
891
+
892
+ async function detectCurrentPrContext(orchestrator: Orchestrator): Promise<{ prUrl: string | null; prContext: string | null }> {
893
+ try {
894
+ const prResult = await orchestrator.pi.exec("gh", ["pr", "view", "--json", "url,title,body,comments"], {
895
+ cwd: orchestrator.cwd,
896
+ timeout: 10000,
897
+ });
898
+ if (prResult.code !== 0) return { prUrl: null, prContext: null };
899
+ const parsed = JSON.parse(prResult.stdout);
900
+ return buildPrContext(parsed);
901
+ } catch {
902
+ return { prUrl: null, prContext: null };
903
+ }
904
+ }
905
+
906
+ async function openCodeReviewInPlannotator(orchestrator: Orchestrator, diffType?: string, defaultBranch?: string): Promise<string> {
907
+ const payload: Record<string, unknown> = { cwd: orchestrator.cwd };
908
+ if (diffType) payload.diffType = diffType;
909
+ if (defaultBranch) payload.defaultBranch = defaultBranch;
910
+
911
+ return await new Promise((resolve) => {
912
+ let handled = false;
913
+ orchestrator.pi.events.emit("plannotator:request", {
914
+ requestId: crypto.randomUUID(),
915
+ action: "code-review",
916
+ payload,
917
+ respond: (response: any) => {
918
+ handled = true;
919
+ if (response?.status !== "handled") {
920
+ resolve(`Plannotator is not available${response?.error ? `: ${response.error}` : "."}`);
921
+ return;
922
+ }
923
+ const approved = !!response?.result?.approved;
924
+ const feedback = typeof response?.result?.feedback === "string" && response.result.feedback.trim().length > 0
925
+ ? `\n\nFeedback:\n${response.result.feedback}`
926
+ : "";
927
+ resolve(approved ? "Plannotator approved the review." : `Plannotator requested changes.${feedback}`);
928
+ },
929
+ });
930
+ setTimeout(() => {
931
+ if (!handled) resolve("Plannotator is not available.");
932
+ }, 30000);
933
+ });
934
+ }
935
+
936
+ async function startReviewTask(
937
+ orchestrator: Orchestrator,
938
+ ctx: any,
939
+ userRequestContent: string,
940
+ researchContent: string | null,
941
+ description: string,
942
+ ): Promise<"started" | typeof BACK> {
943
+ await orchestrator.startTask(ctx, "review", description);
944
+ if (!orchestrator.active || orchestrator.active.type !== "review") return BACK;
945
+ writeFileSync(join(orchestrator.active.dir, "USER_REQUEST.md"), userRequestContent, "utf-8");
946
+ if (researchContent) {
947
+ writeFileSync(join(orchestrator.active.dir, "RESEARCH.md"), researchContent, "utf-8");
948
+ }
949
+ return "started";
950
+ }
951
+
952
+ async function showReviewMenu(orchestrator: Orchestrator, ctx: any): Promise<typeof BACK | "started"> {
953
+ while (true) {
954
+ const choice = await selectOption(ctx, "Review", [
955
+ { title: "Current branch", description: "Review changes on current branch vs base" },
956
+ { title: "Uncommitted changes", description: "Review working directory changes" },
957
+ { title: "Describe", description: "Describe what to review and let the agent figure it out" },
958
+ { title: "Resume", description: "Resume a previously unfinished review" },
959
+ { title: "Back", description: "Return to the previous menu" },
960
+ ]);
961
+ if (!choice || choice === "Back") return BACK;
962
+
963
+ if (choice === "Resume") {
964
+ const result = await showResumeMenu(orchestrator, ctx, "review", "No paused review tasks found.");
965
+ if (result === "started") return result;
966
+ continue;
967
+ }
968
+
969
+ if (choice === "Current branch") {
970
+ const base = await detectDefaultBranch(orchestrator.pi, orchestrator.cwd, orchestrator.config);
971
+ const pr = await detectCurrentPrContext(orchestrator);
972
+
973
+ const urLines = [`# User Request\nReview current branch changes (${base}..HEAD)`];
974
+ if (pr.prUrl) urLines.push(`PR: ${pr.prUrl}`);
975
+ urLines.push("", "## Problem", "Review and identify issues in the code changes.", "", "## Constraints", "Focus on correctness, edge cases, style, missing tests, potential bugs.");
976
+ const urContent = urLines.join("\n") + "\n";
977
+
978
+ let resContent: string | null = null;
979
+ if (pr.prContext) {
980
+ resContent = ["## PR Context", pr.prContext, "", "## Affected Code", "(to be filled during review)", "", "## Architecture Context", "(to be filled during review)"].join("\n") + "\n";
981
+ }
982
+
983
+ const description = await promptDescription(ctx, "Describe the review (optional)", "review");
984
+ if (!description) continue;
985
+ return startReviewTask(orchestrator, ctx, urContent, resContent, description);
986
+ }
987
+
988
+ if (choice === "Uncommitted changes") {
989
+ const urContent = "# User Request\nReview uncommitted changes\n\n## Problem\nReview and identify issues in uncommitted working directory changes.\n\n## Constraints\nFocus on correctness, edge cases, style, missing tests, potential bugs.\n";
990
+ const description = await promptDescription(ctx, "Describe the review (optional)", "review");
991
+ if (!description) continue;
992
+ return startReviewTask(orchestrator, ctx, urContent, null, description);
993
+ }
994
+
995
+ const input = await ctx.ui.input("Describe what to review");
996
+ if (input === undefined || input === null) continue;
997
+ const trimmed = String(input).trim();
998
+ if (!trimmed) continue;
999
+
1000
+ const urContent = `# User Request\n${trimmed}\n\n## Problem\n${trimmed}\n\n## Constraints\nFocus on correctness, edge cases, style, missing tests, potential bugs.\n`;
1001
+ return startReviewTask(orchestrator, ctx, urContent, null, trimmed);
1002
+ }
1003
+ }
1004
+
646
1005
  async function showTaskTypeMenu(
647
1006
  orchestrator: Orchestrator,
648
1007
  ctx: any,
@@ -675,13 +1034,13 @@ async function showNoActiveMenu(orchestrator: Orchestrator, ctx: any): Promise<s
675
1034
  { title: "Debug", description: "Diagnose an issue. Then (optionally) fix it" },
676
1035
  { title: "Brainstorm", description: "Explore and brainstorm. Then (optionally) plan and implement" },
677
1036
  { title: "Implement", description: "Brainstorm, plan and implement" },
1037
+ { title: "Review", description: "Review code changes, diffs, or pull requests" },
678
1038
  { title: "Resume", description: "Resume a previously unfinished task" },
679
1039
  { title: "Subagents", description: "Manage running agents" },
680
- { title: "LSP", description: "Language server status and controls" },
681
- { title: "Settings", description: "Configure pi-pi extension" },
682
- { title: "Close", description: "Close this menu" },
1040
+ { title: "Settings", description: "LSP, Flant AI, and other configuration" },
1041
+ { title: "Back", description: "Close this menu" },
683
1042
  ]);
684
- if (!choice || choice === "Close") return undefined;
1043
+ if (!choice || choice === "Back") return undefined;
685
1044
 
686
1045
  if (choice === "Debug") {
687
1046
  const result = await showTaskTypeMenu(orchestrator, ctx, "debug", "Describe the task");
@@ -701,6 +1060,12 @@ async function showNoActiveMenu(orchestrator: Orchestrator, ctx: any): Promise<s
701
1060
  continue;
702
1061
  }
703
1062
 
1063
+ if (choice === "Review") {
1064
+ const result = await showReviewMenu(orchestrator, ctx);
1065
+ if (result === "started") return undefined;
1066
+ continue;
1067
+ }
1068
+
704
1069
  if (choice === "Resume") {
705
1070
  const result = await showResumeMenu(orchestrator, ctx, undefined, "No paused tasks found.");
706
1071
  if (result === "started") return undefined;
@@ -712,12 +1077,7 @@ async function showNoActiveMenu(orchestrator: Orchestrator, ctx: any): Promise<s
712
1077
  continue;
713
1078
  }
714
1079
 
715
- if (choice === "LSP") {
716
- await showLspMenu(ctx);
717
- continue;
718
- }
719
-
720
- await showSettingsMenu(orchestrator, ctx);
1080
+ await showSettingsMenu(orchestrator, ctx, true);
721
1081
  }
722
1082
  }
723
1083
 
@@ -767,53 +1127,63 @@ export async function showActiveTaskMenu(
767
1127
 
768
1128
  const waiting = step === "await_planners" || step === "await_reviewers";
769
1129
  const { autoLabel, deepLabel } = getReviewLabels(orchestrator);
770
- const hasPlannotator = phase === "plan" || phase === "implement";
771
- const canFinishPhase = (phase === "brainstorm" && task.type === "brainstorm") || phase === "debug";
1130
+ const isReviewPhase = phase === "review";
1131
+ const hasPlannotator = phase === "plan" || phase === "implement" || isReviewPhase;
772
1132
 
773
1133
  const opt = (title: string, description: string): OptionInput => ({ title, description });
774
1134
 
775
1135
  const options: OptionInput[] = [];
1136
+ options.push(opt("Finish", "Complete, pause, or continue to next phase"));
776
1137
  if (!waiting) {
777
- options.push(opt("Approve & continue", "Advance to the next phase"));
778
- options.push(opt(autoLabel, "Run automated review with configured reviewers"));
779
- options.push(opt(deepLabel, "Run automated review with higher thinking level"));
780
- if (hasPlannotator) {
781
- options.push(opt("Review in Plannotator", "Open visual review in browser"));
782
- options.push(opt("Review on my own", "Review manually, then continue"));
783
- }
784
- options.push(opt("Back to prompt", "Return to the prompt and keep working"));
785
- if (canFinishPhase) {
786
- options.push(opt("Finish", "Complete this phase and end the task"));
787
- }
1138
+ options.push(opt("Review", "Auto review, Plannotator, or manual review"));
788
1139
  }
789
- options.push(opt("Abort", "Stop the task without completing"));
790
- options.push(opt("Status", "Show current task phase, step, and timing"));
791
1140
  options.push(opt("Subagents", "Manage running agents"));
792
- options.push(opt("LSP", "Language server status and controls"));
793
-
794
- const choice = await selectOption(ctx, summary, options);
795
- if (!choice) return mode === "tool" ? "No action selected." : "";
796
-
797
- if (choice === "Status") {
798
- showStatus(orchestrator, ctx);
799
- continue;
1141
+ options.push(opt("Settings", "Task status, usage, and LSP"));
1142
+ options.push(opt("Back", "Return to the prompt and keep working"));
1143
+
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);
1148
+ if (!choice || choice === "Back") {
1149
+ return "";
800
1150
  }
1151
+
801
1152
  if (choice === "Subagents") {
802
1153
  await showSubagentsMenu(ctx);
803
1154
  continue;
804
1155
  }
805
- if (choice === "LSP") {
806
- await showLspMenu(ctx);
1156
+ if (choice === "Settings") {
1157
+ await showSettingsMenu(orchestrator, ctx, false);
807
1158
  continue;
808
1159
  }
809
- if (choice === "Abort" || choice === "Finish") {
810
- const text = await finishTask(orchestrator, ctx);
811
- return mode === "tool" ? text : "";
812
- }
813
1160
 
814
- finalizeReviewCycle(task);
1161
+ if (mode === "command") {
1162
+ await abortCurrentWork(orchestrator, ctx);
1163
+ }
815
1164
 
816
- if (choice === "Approve & continue") {
1165
+ if (choice === "Finish") {
1166
+ const canContinue = phase !== "implement" && !waiting;
1167
+ const continueLabel = phase === "plan" ? "Continue to implement" : "Continue to plan & implement";
1168
+ const finishOptions: OptionInput[] = [];
1169
+ if (canContinue) {
1170
+ finishOptions.push(opt(continueLabel, "Approve and advance to the next phase"));
1171
+ }
1172
+ finishOptions.push(opt("Complete", "Mark task as done and clean up"));
1173
+ finishOptions.push(opt("Pause", "Suspend task to resume later"));
1174
+ finishOptions.push(opt("Back", "Return to the previous menu"));
1175
+
1176
+ const finishChoice = await selectOption(ctx, "Finish", finishOptions);
1177
+ if (!finishChoice || finishChoice === "Back") continue;
1178
+ if (finishChoice === "Pause") {
1179
+ const text = await pauseTask(orchestrator, ctx);
1180
+ return mode === "tool" ? text : "";
1181
+ }
1182
+ if (finishChoice === "Complete") {
1183
+ const text = await finishTask(orchestrator, ctx);
1184
+ return mode === "tool" ? text : "";
1185
+ }
1186
+ finalizeReviewCycle(task);
817
1187
  const result = await orchestrator.transitionToNextPhase(ctx);
818
1188
  if (!result.ok) return `Transition blocked: ${result.error}`;
819
1189
  if (orchestrator.phaseCompactionPending || orchestrator.taskDoneCompactionPending) return "";
@@ -822,9 +1192,95 @@ export async function showActiveTaskMenu(
822
1192
  return "";
823
1193
  }
824
1194
 
825
- if (choice === autoLabel || choice === deepLabel || choice === "Review in Plannotator") {
826
- const kind = choice === autoLabel ? "auto" as const : choice === deepLabel ? "auto-deep" as const : "plannotator" as const;
827
- if (kind !== "plannotator" && !hasEnabledReviewers(orchestrator, kind)) {
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
+ }
1271
+
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
+ }
1280
+
1281
+ finalizeReviewCycle(task);
1282
+ const kind = reviewChoice === autoLabel ? "auto" as const : "auto-deep" as const;
1283
+ if (!hasEnabledReviewers(orchestrator, kind)) {
828
1284
  const label = phase === "brainstorm" ? "brainstorm" : phase === "plan" ? "plan" : "code";
829
1285
  ctx.ui.notify(`No ${label} reviewers enabled.`, "info");
830
1286
  continue;
@@ -836,24 +1292,6 @@ export async function showActiveTaskMenu(
836
1292
  if (handled.continueLoop) continue;
837
1293
  return handled.text ?? text;
838
1294
  }
839
-
840
- if (choice === "Review on my own") {
841
- if (phase === "plan") {
842
- setStep(orchestrator, "synthesize");
843
- } else {
844
- setStep(orchestrator, "llm_work");
845
- }
846
- return continueMessage;
847
- }
848
-
849
- if (choice === "Back to prompt") {
850
- if (phase === "plan") {
851
- setStep(orchestrator, "synthesize");
852
- } else {
853
- setStep(orchestrator, "llm_work");
854
- }
855
- return continueMessage;
856
- }
857
1295
  }
858
1296
  }
859
1297
 
@@ -861,6 +1299,6 @@ export async function showPpMenu(orchestrator: Orchestrator, ctx: any, mode: Men
861
1299
  if (!orchestrator.active) {
862
1300
  return showNoActiveMenu(orchestrator, ctx);
863
1301
  }
864
- const text = await showActiveTaskMenu(orchestrator, ctx, "Choose next action", mode);
1302
+ const text = await showActiveTaskMenu(orchestrator, ctx, "/pp", mode);
865
1303
  return text || undefined;
866
1304
  }