@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.
- package/3p/pi-plannotator/apps/pi-extension/plannotator-browser.ts +1 -3
- package/3p/pi-plannotator/packages/shared/review-core.ts +0 -2
- package/3p/pi-subagents/src/index.ts +9 -1
- package/extensions/orchestrator/agents/brainstorm-reviewer.ts +1 -0
- package/extensions/orchestrator/agents/code-reviewer.ts +1 -0
- package/extensions/orchestrator/agents/plan-reviewer.ts +4 -1
- package/extensions/orchestrator/agents/planner.ts +13 -2
- package/extensions/orchestrator/agents/tool-routing.ts +9 -0
- package/extensions/orchestrator/command-handlers.test.ts +22 -25
- package/extensions/orchestrator/config.test.ts +1 -0
- package/extensions/orchestrator/config.ts +3 -1
- package/extensions/orchestrator/custom-footer.ts +146 -0
- package/extensions/orchestrator/event-handlers.test.ts +1 -0
- package/extensions/orchestrator/event-handlers.ts +100 -155
- package/extensions/orchestrator/flant-infra.ts +7 -2
- package/extensions/orchestrator/integration.test.ts +3 -1
- package/extensions/orchestrator/orchestrator.test.ts +1 -0
- package/extensions/orchestrator/orchestrator.ts +27 -4
- package/extensions/orchestrator/phases/machine.ts +38 -0
- package/extensions/orchestrator/phases/review-task.ts +32 -0
- package/extensions/orchestrator/pp-menu.ts +510 -72
- package/extensions/orchestrator/state.ts +8 -9
- package/extensions/orchestrator/usage-tracker.ts +309 -0
- package/package.json +1 -1
|
@@ -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[
|
|
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", [
|
|
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
|
-
|
|
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
|
|
526
|
-
|
|
527
|
-
{ title: "
|
|
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: "
|
|
681
|
-
{ title: "
|
|
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 === "
|
|
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
|
-
|
|
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
|
|
771
|
-
const
|
|
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("
|
|
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("
|
|
793
|
-
|
|
794
|
-
|
|
795
|
-
|
|
796
|
-
|
|
797
|
-
|
|
798
|
-
|
|
799
|
-
|
|
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 === "
|
|
806
|
-
await
|
|
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
|
-
|
|
1161
|
+
if (mode === "command") {
|
|
1162
|
+
await abortCurrentWork(orchestrator, ctx);
|
|
1163
|
+
}
|
|
815
1164
|
|
|
816
|
-
if (choice === "
|
|
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 ===
|
|
826
|
-
const
|
|
827
|
-
|
|
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, "
|
|
1302
|
+
const text = await showActiveTaskMenu(orchestrator, ctx, "/pp", mode);
|
|
865
1303
|
return text || undefined;
|
|
866
1304
|
}
|