@ilya-lesikov/pi-pi 0.6.0 → 0.8.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-ask-user/index.ts +1 -1
- package/3p/pi-plannotator/apps/pi-extension/README.md +6 -5
- package/3p/pi-plannotator/apps/pi-extension/plannotator-browser.ts +25 -10
- package/3p/pi-plannotator/apps/pi-extension/plannotator-events.code-review.test.ts +172 -0
- package/3p/pi-plannotator/apps/pi-extension/plannotator-events.ts +50 -12
- package/3p/pi-plannotator/apps/pi-extension/server/project.test.ts +45 -0
- package/3p/pi-plannotator/apps/pi-extension/server/project.ts +7 -6
- package/3p/pi-plannotator/apps/pi-extension/server/serverReview.ts +41 -25
- package/3p/pi-subagents/src/agent-manager.ts +34 -1
- package/3p/pi-subagents/src/agent-runner.ts +66 -33
- package/3p/pi-subagents/src/index.ts +3 -38
- package/3p/pi-subagents/src/types.ts +4 -0
- package/extensions/orchestrator/agents/advisor.ts +35 -0
- package/extensions/orchestrator/agents/brainstorm-reviewer.ts +7 -7
- package/extensions/orchestrator/agents/code-reviewer.ts +7 -7
- package/extensions/orchestrator/agents/constraints.test.ts +44 -0
- package/extensions/orchestrator/agents/constraints.ts +3 -0
- package/extensions/orchestrator/agents/deep-debugger.ts +35 -0
- package/extensions/orchestrator/agents/plan-reviewer.ts +7 -7
- package/extensions/orchestrator/agents/planner.ts +9 -8
- package/extensions/orchestrator/agents/prompts.test.ts +120 -0
- package/extensions/orchestrator/agents/reviewer.ts +48 -0
- package/extensions/orchestrator/agents/task.ts +6 -20
- package/extensions/orchestrator/agents/tool-routing.ts +23 -1
- package/extensions/orchestrator/command-handlers.ts +1 -1
- package/extensions/orchestrator/config.ts +8 -4
- package/extensions/orchestrator/context.test.ts +54 -0
- package/extensions/orchestrator/context.ts +65 -2
- package/extensions/orchestrator/custom-footer.ts +5 -2
- package/extensions/orchestrator/doctor.test.ts +3 -1
- package/extensions/orchestrator/doctor.ts +40 -2
- package/extensions/orchestrator/event-handlers.test.ts +97 -1
- package/extensions/orchestrator/event-handlers.ts +222 -48
- package/extensions/orchestrator/flant-infra.test.ts +312 -0
- package/extensions/orchestrator/flant-infra.ts +407 -44
- package/extensions/orchestrator/index.ts +1 -1
- package/extensions/orchestrator/integration.test.ts +312 -18
- package/extensions/orchestrator/messages.test.ts +30 -0
- package/extensions/orchestrator/messages.ts +6 -0
- package/extensions/orchestrator/model-registry.test.ts +124 -13
- package/extensions/orchestrator/model-registry.ts +91 -33
- package/extensions/orchestrator/orchestrator.test.ts +113 -0
- package/extensions/orchestrator/orchestrator.ts +163 -3
- package/extensions/orchestrator/phases/brainstorm.ts +9 -20
- package/extensions/orchestrator/phases/implementation.test.ts +11 -0
- package/extensions/orchestrator/phases/implementation.ts +4 -6
- package/extensions/orchestrator/phases/planning.test.ts +16 -0
- package/extensions/orchestrator/phases/planning.ts +11 -4
- package/extensions/orchestrator/phases/review-task.ts +1 -4
- package/extensions/orchestrator/phases/review.test.ts +62 -0
- package/extensions/orchestrator/phases/review.ts +58 -15
- package/extensions/orchestrator/plannotator.ts +9 -6
- package/extensions/orchestrator/pp-menu.test.ts +74 -1
- package/extensions/orchestrator/pp-menu.ts +366 -94
- package/extensions/orchestrator/pp-state-tools.test.ts +192 -0
- package/extensions/orchestrator/pp-state-tools.ts +249 -0
- package/extensions/orchestrator/rate-limit-fallback-flow.test.ts +128 -0
- package/extensions/orchestrator/rate-limit-fallback.test.ts +98 -0
- package/extensions/orchestrator/rate-limit-fallback.ts +243 -0
- package/extensions/orchestrator/state.ts +41 -20
- package/extensions/orchestrator/test-helpers.ts +18 -3
- package/extensions/orchestrator/usage-tracker.test.ts +131 -3
- package/extensions/orchestrator/usage-tracker.ts +78 -11
- package/extensions/orchestrator/validate-artifacts.test.ts +20 -0
- package/extensions/orchestrator/validate-artifacts.ts +2 -2
- package/package.json +1 -1
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { existsSync, readdirSync, writeFileSync } from "fs";
|
|
2
|
-
import { join, relative } from "path";
|
|
2
|
+
import { join, relative, basename } from "path";
|
|
3
3
|
import { isDeepStrictEqual } from "util";
|
|
4
|
-
import { askUser, isCancel } from "../../3p/pi-ask-user/index.js";
|
|
4
|
+
import { askUser, isCancel, type CancelReason } from "../../3p/pi-ask-user/index.js";
|
|
5
5
|
import { unregisterAgentDefinitions } from "./agents/registry.js";
|
|
6
6
|
import {
|
|
7
7
|
type AfterEditCommandConfig,
|
|
@@ -27,7 +27,8 @@ import {
|
|
|
27
27
|
} from "./context.js";
|
|
28
28
|
import { detectDefaultBranch, enterReviewCycle, finalizeReviewCycle } from "./event-handlers.js";
|
|
29
29
|
import { Orchestrator } from "./orchestrator.js";
|
|
30
|
-
import { cancelPendingPlannotatorWait } from "./plannotator.js";
|
|
30
|
+
import { cancelPendingPlannotatorWait, openPlannotator, waitForPlannotatorResult } from "./plannotator.js";
|
|
31
|
+
import { advanceBanner } from "./messages.js";
|
|
31
32
|
import { spawnPlanners, spawnPlanReviewers } from "./phases/planning.js";
|
|
32
33
|
import { spawnCodeReviewers } from "./phases/review.js";
|
|
33
34
|
import { spawnBrainstormReviewers } from "./phases/brainstorm.js";
|
|
@@ -44,6 +45,8 @@ import {
|
|
|
44
45
|
saveTask,
|
|
45
46
|
taskAge,
|
|
46
47
|
taskName,
|
|
48
|
+
taskNameFromState,
|
|
49
|
+
taskShortId,
|
|
47
50
|
type AutonomousConfig,
|
|
48
51
|
type TaskMode,
|
|
49
52
|
type TaskInfo,
|
|
@@ -53,6 +56,8 @@ import {
|
|
|
53
56
|
clearFlantGeneratedConfig,
|
|
54
57
|
getFlantGeneratedConfig,
|
|
55
58
|
loadFlantSettings,
|
|
59
|
+
readClaudeOAuthToken,
|
|
60
|
+
readGatewayApiKey,
|
|
56
61
|
saveFlantSettings,
|
|
57
62
|
unregisterFlantProviders,
|
|
58
63
|
updateFlantInfra,
|
|
@@ -67,6 +72,13 @@ type MenuMode = "command" | "tool";
|
|
|
67
72
|
|
|
68
73
|
const BACK = "back" as const;
|
|
69
74
|
|
|
75
|
+
// Sentinel returned by showActiveTaskMenu when the user dismissed the top-level
|
|
76
|
+
// menu with a deliberate ESC (cancelReason "user"), as opposed to selecting the
|
|
77
|
+
// "Back" option or a programmatic dismissal. Callers (pp_phase_complete) use it
|
|
78
|
+
// to stop the turn cleanly instead of returning reminder text that starts a new
|
|
79
|
+
// LLM turn. It is intentionally distinct from the empty-string "Back" return.
|
|
80
|
+
export const USER_CANCELLED = "\u0000user-cancelled" as const;
|
|
81
|
+
|
|
70
82
|
type OptionInput = string | { title: string; description?: string };
|
|
71
83
|
|
|
72
84
|
type TimeoutKey =
|
|
@@ -81,6 +93,18 @@ function isEnabled(value: { enabled?: boolean } | undefined): boolean {
|
|
|
81
93
|
}
|
|
82
94
|
|
|
83
95
|
async function selectOption(ctx: any, question: string, options: OptionInput[]): Promise<string | undefined> {
|
|
96
|
+
return (await selectOptionCancelable(ctx, question, options)).choice;
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
// Like selectOption but also surfaces the cancel reason so callers can tell a
|
|
100
|
+
// deliberate user ESC (reason "user") apart from a normal non-selection. Used by
|
|
101
|
+
// showActiveTaskMenu so pp_phase_complete can stop the turn cleanly on ESC
|
|
102
|
+
// (mirroring ask_user) while keeping the "Back" navigation reminder.
|
|
103
|
+
async function selectOptionCancelable(
|
|
104
|
+
ctx: any,
|
|
105
|
+
question: string,
|
|
106
|
+
options: OptionInput[],
|
|
107
|
+
): Promise<{ choice?: string; cancelReason?: CancelReason }> {
|
|
84
108
|
const result = await askUser(ctx, {
|
|
85
109
|
question,
|
|
86
110
|
options,
|
|
@@ -88,8 +112,9 @@ async function selectOption(ctx: any, question: string, options: OptionInput[]):
|
|
|
88
112
|
allowComment: false,
|
|
89
113
|
allowMultiple: false,
|
|
90
114
|
});
|
|
91
|
-
if (
|
|
92
|
-
|
|
115
|
+
if (result && isCancel(result)) return { cancelReason: result.reason };
|
|
116
|
+
if (!result || result.kind !== "selection") return {};
|
|
117
|
+
return { choice: result.selections[0] };
|
|
93
118
|
}
|
|
94
119
|
|
|
95
120
|
function opt(title: string, description: string): OptionInput {
|
|
@@ -201,6 +226,44 @@ async function pickCommitForRepo(orchestrator: Orchestrator, ctx: any, repo: Rep
|
|
|
201
226
|
return pickedHash || null;
|
|
202
227
|
}
|
|
203
228
|
|
|
229
|
+
async function repoHasReviewableChanges(
|
|
230
|
+
orchestrator: Orchestrator,
|
|
231
|
+
repo: RepoInfo,
|
|
232
|
+
base: string,
|
|
233
|
+
): Promise<{ changed: boolean; error?: string }> {
|
|
234
|
+
const run = async (args: string[]): Promise<{ out: string; failed: boolean }> => {
|
|
235
|
+
try {
|
|
236
|
+
const res = await orchestrator.pi.exec("git", args, { cwd: repo.path, timeout: 5000 });
|
|
237
|
+
if (res.code !== 0) return { out: "", failed: true };
|
|
238
|
+
return { out: res.stdout, failed: false };
|
|
239
|
+
} catch {
|
|
240
|
+
return { out: "", failed: true };
|
|
241
|
+
}
|
|
242
|
+
};
|
|
243
|
+
|
|
244
|
+
const committed = await run(["diff", "--name-only", `${base}...HEAD`]);
|
|
245
|
+
const unstaged = await run(["diff", "--name-only"]);
|
|
246
|
+
const staged = await run(["diff", "--cached", "--name-only"]);
|
|
247
|
+
const status = await run(["status", "--porcelain"]);
|
|
248
|
+
|
|
249
|
+
const hasCommitted = committed.out.trim().length > 0;
|
|
250
|
+
const hasUnstaged = unstaged.out.trim().length > 0;
|
|
251
|
+
const hasStaged = staged.out.trim().length > 0;
|
|
252
|
+
const hasUntracked = status.out.split("\n").some((line) => line.startsWith("??"));
|
|
253
|
+
const changed = hasCommitted || hasUnstaged || hasStaged || hasUntracked;
|
|
254
|
+
|
|
255
|
+
if (changed) return { changed: true };
|
|
256
|
+
|
|
257
|
+
// No change detected — but a failed probe (e.g. a missing/invalid base ref)
|
|
258
|
+
// could be masking real changes, so surface it rather than silently hiding
|
|
259
|
+
// the repo.
|
|
260
|
+
if (committed.failed || unstaged.failed || staged.failed || status.failed) {
|
|
261
|
+
return { changed: false, error: "git status/diff failed" };
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
return { changed: false };
|
|
265
|
+
}
|
|
266
|
+
|
|
204
267
|
interface RepoPrContext {
|
|
205
268
|
repoPath: string;
|
|
206
269
|
prUrl: string | null;
|
|
@@ -228,6 +291,11 @@ function showStatus(orchestrator: Orchestrator, ctx: any): void {
|
|
|
228
291
|
}
|
|
229
292
|
|
|
230
293
|
async function abortCurrentWork(orchestrator: Orchestrator, ctx: any): Promise<void> {
|
|
294
|
+
// Clear pi-pi's own delayed post-error retry (timer + ESC interrupt). Unlike
|
|
295
|
+
// pause/finish, abortCurrentWork does NOT go through cleanupActive/
|
|
296
|
+
// resetTaskScopedState, so without this a scheduled retry would survive the
|
|
297
|
+
// abort and re-nudge seconds later.
|
|
298
|
+
orchestrator.cancelPendingRetry();
|
|
231
299
|
orchestrator.abortAllSubagents();
|
|
232
300
|
orchestrator.transitionController.abortMainAgent(ctx.abort?.bind(ctx));
|
|
233
301
|
await ctx.waitForIdle?.();
|
|
@@ -771,6 +839,9 @@ function collectRoleAssignments(config: Partial<PiPiConfig> | null): string[] {
|
|
|
771
839
|
add("agents.subagents.simple.explore", config.agents?.subagents?.simple?.explore?.model);
|
|
772
840
|
add("agents.subagents.simple.librarian", config.agents?.subagents?.simple?.librarian?.model);
|
|
773
841
|
add("agents.subagents.simple.task", config.agents?.subagents?.simple?.task?.model);
|
|
842
|
+
add("agents.subagents.simple.advisor", config.agents?.subagents?.simple?.advisor?.model);
|
|
843
|
+
add("agents.subagents.simple.deep-debugger", config.agents?.subagents?.simple?.["deep-debugger"]?.model);
|
|
844
|
+
add("agents.subagents.simple.reviewer", config.agents?.subagents?.simple?.reviewer?.model);
|
|
774
845
|
return out;
|
|
775
846
|
}
|
|
776
847
|
|
|
@@ -783,6 +854,21 @@ function flantStatusText(settings: FlantSettings): string {
|
|
|
783
854
|
`Last updated: ${settings.lastUpdated ?? "never"}`,
|
|
784
855
|
`Providers: pp-flant-anthropic (${providers.anthropic} models), pp-flant-openai (${providers.openai} models)`,
|
|
785
856
|
];
|
|
857
|
+
if (settings.subscription) {
|
|
858
|
+
const hasOAuth = !!readClaudeOAuthToken();
|
|
859
|
+
const hasGatewayKey = !!readGatewayApiKey();
|
|
860
|
+
const subActive = hasOAuth && hasGatewayKey;
|
|
861
|
+
lines.push(
|
|
862
|
+
`Personal subscription: on (${subActive ? `active — pp-flant-anthropic-sub, ${providers.anthropic} models` : "inactive"})`,
|
|
863
|
+
);
|
|
864
|
+
lines.push(`Rate-limit switch-back check: every ${settings.switchBackIntervalMinutes} min`);
|
|
865
|
+
if (!subActive) {
|
|
866
|
+
if (!hasOAuth) lines.push(" - missing Claude OAuth token (run pi /login for Anthropic)");
|
|
867
|
+
if (!hasGatewayKey) lines.push(" - missing gateway key (set LLM_API_KEY or FLANT_API_KEY)");
|
|
868
|
+
}
|
|
869
|
+
} else {
|
|
870
|
+
lines.push("Personal subscription: off");
|
|
871
|
+
}
|
|
786
872
|
if (assignments.length === 0) {
|
|
787
873
|
lines.push("Role assignments: none");
|
|
788
874
|
} else {
|
|
@@ -811,13 +897,22 @@ async function showFlantInfraMenu(orchestrator: Orchestrator, ctx: any): Promise
|
|
|
811
897
|
while (true) {
|
|
812
898
|
const settings = loadFlantSettings();
|
|
813
899
|
const enableLabel = `Enable: ${settings.enabled ? "ON" : "OFF"}`;
|
|
814
|
-
const options: OptionInput[] = [
|
|
900
|
+
const options: OptionInput[] = [
|
|
901
|
+
{ title: enableLabel, description: "Turn the Flant AI model providers on or off" },
|
|
902
|
+
];
|
|
903
|
+
const subscriptionLabel = `Personal Claude subscription: ${settings.subscription ? "ON" : "OFF"}`;
|
|
815
904
|
if (settings.enabled) {
|
|
816
905
|
options.push(
|
|
817
|
-
|
|
818
|
-
`
|
|
819
|
-
"
|
|
820
|
-
|
|
906
|
+
{ title: subscriptionLabel, description: "Route Claude roles through your personal Claude subscription instead of the gateway" },
|
|
907
|
+
{ title: `Auto-update on startup: ${settings.autoUpdate ? "ON" : "OFF"}`, description: "Refresh the available model list automatically each time pi starts" },
|
|
908
|
+
{ title: `Cache period: ${settings.cacheTTLDays} ${settings.cacheTTLDays === 1 ? "day" : "days"}`, description: "How long the fetched model list is reused before it is refreshed" },
|
|
909
|
+
);
|
|
910
|
+
if (settings.subscription) {
|
|
911
|
+
options.push({ title: `Rate-limit switch-back check: every ${settings.switchBackIntervalMinutes} min`, description: "How often to retry your subscription after it was rate-limited and traffic fell back to the gateway" });
|
|
912
|
+
}
|
|
913
|
+
options.push(
|
|
914
|
+
{ title: "Update now", description: "Fetch the latest model list from Flant right away" },
|
|
915
|
+
{ title: "Current status", description: "Show the current Flant configuration, providers, and model counts" },
|
|
821
916
|
);
|
|
822
917
|
}
|
|
823
918
|
options.push({ title: "Back", description: "Return to the previous menu" });
|
|
@@ -852,6 +947,50 @@ async function showFlantInfraMenu(orchestrator: Orchestrator, ctx: any): Promise
|
|
|
852
947
|
continue;
|
|
853
948
|
}
|
|
854
949
|
|
|
950
|
+
if (choice === subscriptionLabel) {
|
|
951
|
+
const turningOn = !settings.subscription;
|
|
952
|
+
if (turningOn) {
|
|
953
|
+
if (!readClaudeOAuthToken()) {
|
|
954
|
+
ctx.ui.notify("No Claude OAuth token found. Log in to your personal Claude subscription in pi first (/login → Anthropic), then retry.", "warning");
|
|
955
|
+
continue;
|
|
956
|
+
}
|
|
957
|
+
if (!readGatewayApiKey()) {
|
|
958
|
+
ctx.ui.notify("Set LLM_API_KEY (or FLANT_API_KEY) for the gateway first.", "warning");
|
|
959
|
+
continue;
|
|
960
|
+
}
|
|
961
|
+
}
|
|
962
|
+
const next = { ...settings, subscription: turningOn };
|
|
963
|
+
saveFlantSettings(next);
|
|
964
|
+
const result = await updateFlantInfra(orchestrator.pi);
|
|
965
|
+
if (!result.ok) {
|
|
966
|
+
ctx.ui.notify(`Personal subscription ${turningOn ? "enable" : "disable"} failed: ${result.error ?? "unknown error"}`, "error");
|
|
967
|
+
} else {
|
|
968
|
+
ctx.ui.notify(
|
|
969
|
+
turningOn
|
|
970
|
+
? "Personal Claude subscription ON — Claude roles now route through sub/claude-* (billed to your subscription); non-Claude roles stay on llm-api.flant.ru."
|
|
971
|
+
: "Personal Claude subscription OFF — Claude roles reverted to pp-flant-anthropic.",
|
|
972
|
+
"info",
|
|
973
|
+
);
|
|
974
|
+
}
|
|
975
|
+
continue;
|
|
976
|
+
}
|
|
977
|
+
|
|
978
|
+
if (choice.startsWith("Rate-limit switch-back check:")) {
|
|
979
|
+
const selected = await selectOption(ctx, "Switch-back check interval", [
|
|
980
|
+
{ title: "15 min", description: "Probe the subscription limit every 15 minutes" },
|
|
981
|
+
{ title: "30 min", description: "Default — probe every 30 minutes" },
|
|
982
|
+
{ title: "60 min", description: "Probe hourly" },
|
|
983
|
+
{ title: "120 min", description: "Probe every two hours" },
|
|
984
|
+
{ title: "Back", description: "Return to the previous menu" },
|
|
985
|
+
]);
|
|
986
|
+
if (!selected || selected === "Back") continue;
|
|
987
|
+
const mins = Number(selected.split(" ")[0]);
|
|
988
|
+
if (!Number.isFinite(mins) || mins <= 0) continue;
|
|
989
|
+
saveFlantSettings({ ...settings, switchBackIntervalMinutes: mins });
|
|
990
|
+
ctx.ui.notify(`Switch-back check interval set to ${mins} min.`, "info");
|
|
991
|
+
continue;
|
|
992
|
+
}
|
|
993
|
+
|
|
855
994
|
if (choice.startsWith("Cache period:")) {
|
|
856
995
|
const selected = await selectOption(ctx, "Cache period", [
|
|
857
996
|
{ title: "1 day", description: "Refresh model metadata daily" },
|
|
@@ -899,17 +1038,18 @@ function formatElapsedDuration(ms: number): string {
|
|
|
899
1038
|
return remMin > 0 ? `${hr}h ${remMin}m` : `${hr}h`;
|
|
900
1039
|
}
|
|
901
1040
|
|
|
902
|
-
function showUsage(ctx: any): void {
|
|
1041
|
+
export function showUsage(ctx: any): void {
|
|
903
1042
|
const tracker = (globalThis as any)[Symbol.for("pi-pi:usage-tracker")] as
|
|
904
1043
|
| {
|
|
905
1044
|
getTotalInputTokens(): number; getTotalOutputTokens(): number;
|
|
906
1045
|
getTotalCacheReadTokens(): number; getTotalCacheWriteTokens(): number;
|
|
1046
|
+
getTotalProcessedInputTokens(): number;
|
|
907
1047
|
getTotalCost(): number; getCacheHitRate(): number;
|
|
908
1048
|
getMainInputTokens(): number; getMainOutputTokens(): number;
|
|
909
1049
|
getMainCacheReadTokens(): number; getMainCacheWriteTokens(): number;
|
|
910
1050
|
getMainCost(): number;
|
|
911
|
-
getPerModelUsage(): Record<string, { inputTokens: number; outputTokens: number; cacheReadTokens: number; cacheWriteTokens: number; cacheSupported: boolean; turns: number }>;
|
|
912
|
-
getSubagentList(): Array<{ description: string; agentType: string; modelId: string; inputTokens: number; outputTokens: number; cacheReadTokens: number; cacheWriteTokens: number; cacheSupported: boolean; cost: number; durationMs: number; toolUses: number }>;
|
|
1051
|
+
getPerModelUsage(): Record<string, { inputTokens: number; outputTokens: number; cacheReadTokens: number; cacheWriteTokens: number; cacheSupported: boolean; turns: number; subscription: boolean }>;
|
|
1052
|
+
getSubagentList(): Array<{ description: string; agentType: string; modelId: string; inputTokens: number; outputTokens: number; cacheReadTokens: number; cacheWriteTokens: number; cacheSupported: boolean; cost: number; durationMs: number; toolUses: number; subscription: boolean }>;
|
|
913
1053
|
}
|
|
914
1054
|
| undefined;
|
|
915
1055
|
|
|
@@ -918,30 +1058,52 @@ function showUsage(ctx: any): void {
|
|
|
918
1058
|
return;
|
|
919
1059
|
}
|
|
920
1060
|
|
|
921
|
-
|
|
1061
|
+
// Token-weighted cache hit rate: cache reads over all processed input
|
|
1062
|
+
// (uncached + cache read + cache write). Same formula everywhere so the
|
|
1063
|
+
// total, per-model, and per-agent percentages are directly comparable.
|
|
1064
|
+
const hitRate = (uncached: number, cacheRead: number, cacheWrite: number): number => {
|
|
1065
|
+
const processed = uncached + cacheRead + cacheWrite;
|
|
1066
|
+
return processed > 0 ? cacheRead / processed : 0;
|
|
1067
|
+
};
|
|
1068
|
+
|
|
1069
|
+
// Compact one-line input part: total processed input with an inline
|
|
1070
|
+
// uncached / cache read / cache write breakdown, e.g. "↑1.3k (u84 r1.0k w200)".
|
|
1071
|
+
const inputPart = (uncached: number, cacheRead: number, cacheWrite: number, cacheSupported: boolean): string => {
|
|
1072
|
+
const processed = uncached + cacheRead + cacheWrite;
|
|
1073
|
+
const head = `↑${formatTokenCount(processed)}`;
|
|
1074
|
+
if (!cacheSupported) return head;
|
|
1075
|
+
return `${head} (u${formatTokenCount(uncached)} r${formatTokenCount(cacheRead)} w${formatTokenCount(cacheWrite)})`;
|
|
1076
|
+
};
|
|
1077
|
+
|
|
1078
|
+
const totalUncachedInput = tracker.getTotalInputTokens();
|
|
922
1079
|
const totalOutput = tracker.getTotalOutputTokens();
|
|
923
1080
|
const totalCacheRead = tracker.getTotalCacheReadTokens();
|
|
1081
|
+
const totalCacheWrite = tracker.getTotalCacheWriteTokens();
|
|
1082
|
+
const totalProcessedInput = tracker.getTotalProcessedInputTokens();
|
|
924
1083
|
const totalCost = tracker.getTotalCost();
|
|
925
|
-
const totalCacheRate = (
|
|
926
|
-
? totalCacheRead / (totalCacheRead + totalInput)
|
|
927
|
-
: 0;
|
|
1084
|
+
const totalCacheRate = tracker.getCacheHitRate();
|
|
928
1085
|
|
|
929
1086
|
const mainInput = tracker.getMainInputTokens();
|
|
930
1087
|
const mainOutput = tracker.getMainOutputTokens();
|
|
931
1088
|
const mainCacheRead = tracker.getMainCacheReadTokens();
|
|
1089
|
+
const mainCacheWrite = tracker.getMainCacheWriteTokens();
|
|
932
1090
|
const mainCost = tracker.getMainCost();
|
|
933
1091
|
const models = tracker.getPerModelUsage();
|
|
934
1092
|
const subagents = tracker.getSubagentList();
|
|
935
1093
|
|
|
936
|
-
const byModel = new Map<string, { input: number; output: number; cacheRead: number; cacheWrite: number; cacheSupported: boolean; cost: number }>();
|
|
1094
|
+
const byModel = new Map<string, { input: number; output: number; cacheRead: number; cacheWrite: number; cacheSupported: boolean; cost: number; subscription: boolean }>();
|
|
937
1095
|
const mainModelEntries = Object.entries(models);
|
|
938
|
-
|
|
1096
|
+
// Subscription (flat-rate) models contribute no dollars, so exclude their
|
|
1097
|
+
// tokens from the proportional-share denominator or paid rows would be
|
|
1098
|
+
// inflated and the sub/ rows would receive a spurious share.
|
|
1099
|
+
const mainTotalTokens = mainModelEntries.reduce((s, [, u]) => s + (u.subscription ? 0 : u.inputTokens + u.outputTokens), 0);
|
|
939
1100
|
for (const [modelId, usage] of mainModelEntries) {
|
|
940
1101
|
const modelTokens = usage.inputTokens + usage.outputTokens;
|
|
941
|
-
const modelCostShare = mainTotalTokens
|
|
1102
|
+
const modelCostShare = usage.subscription || mainTotalTokens <= 0 ? 0 : mainCost * (modelTokens / mainTotalTokens);
|
|
942
1103
|
byModel.set(modelId, {
|
|
943
1104
|
input: usage.inputTokens, output: usage.outputTokens,
|
|
944
1105
|
cacheRead: usage.cacheReadTokens, cacheWrite: usage.cacheWriteTokens, cacheSupported: usage.cacheSupported, cost: modelCostShare,
|
|
1106
|
+
subscription: usage.subscription,
|
|
945
1107
|
});
|
|
946
1108
|
}
|
|
947
1109
|
for (const sa of subagents) {
|
|
@@ -954,28 +1116,37 @@ function showUsage(ctx: any): void {
|
|
|
954
1116
|
existing.cacheWrite += sa.cacheWriteTokens;
|
|
955
1117
|
if (sa.cacheSupported) existing.cacheSupported = true;
|
|
956
1118
|
existing.cost += sa.cost;
|
|
1119
|
+
if (sa.subscription) existing.subscription = true;
|
|
957
1120
|
} else {
|
|
958
1121
|
byModel.set(key, {
|
|
959
1122
|
input: sa.inputTokens, output: sa.outputTokens,
|
|
960
1123
|
cacheRead: sa.cacheReadTokens, cacheWrite: sa.cacheWriteTokens, cacheSupported: sa.cacheSupported, cost: sa.cost,
|
|
1124
|
+
subscription: sa.subscription,
|
|
961
1125
|
});
|
|
962
1126
|
}
|
|
963
1127
|
}
|
|
964
1128
|
|
|
1129
|
+
// Total input the model processed = uncached + cache read + cache write.
|
|
1130
|
+
// Shown as an explicit breakdown so the (often tiny) uncached sliver no
|
|
1131
|
+
// longer masquerades as the whole "Input" figure.
|
|
965
1132
|
const lines: string[] = ["Session usage (total):"];
|
|
966
|
-
lines.push(` Input: ${formatTokenCount(
|
|
1133
|
+
lines.push(` Input: ${formatTokenCount(totalProcessedInput)} tokens`);
|
|
1134
|
+
lines.push(` • uncached: ${formatTokenCount(totalUncachedInput)}`);
|
|
1135
|
+
lines.push(` • cache read: ${formatTokenCount(totalCacheRead)}`);
|
|
1136
|
+
lines.push(` • cache write: ${formatTokenCount(totalCacheWrite)}`);
|
|
967
1137
|
lines.push(` Output: ${formatTokenCount(totalOutput)} tokens`);
|
|
968
1138
|
if (totalCacheRead > 0) lines.push(` Cache: ⚡${Math.round(totalCacheRate * 100)}% hit rate`);
|
|
969
|
-
|
|
1139
|
+
lines.push(` Cost: $${totalCost.toFixed(2)}`);
|
|
970
1140
|
|
|
971
1141
|
if (byModel.size > 0) {
|
|
972
1142
|
lines.push("");
|
|
973
1143
|
lines.push("By model:");
|
|
974
1144
|
for (const [modelId, m] of byModel) {
|
|
975
|
-
const cr =
|
|
976
|
-
const parts = [
|
|
1145
|
+
const cr = Math.round(hitRate(m.input, m.cacheRead, m.cacheWrite) * 100);
|
|
1146
|
+
const parts = [inputPart(m.input, m.cacheRead, m.cacheWrite, m.cacheSupported), `↓${formatTokenCount(m.output)}`];
|
|
977
1147
|
if (m.cacheSupported) parts.push(`⚡${cr}%`);
|
|
978
|
-
if (m.
|
|
1148
|
+
if (m.subscription) parts.push("subscription");
|
|
1149
|
+
else if (m.cost > 0) parts.push(`$${m.cost.toFixed(2)}`);
|
|
979
1150
|
lines.push(` ${modelId}: ${parts.join(" ")}`);
|
|
980
1151
|
}
|
|
981
1152
|
}
|
|
@@ -985,13 +1156,15 @@ function showUsage(ctx: any): void {
|
|
|
985
1156
|
const agentModelNames = Object.keys(models);
|
|
986
1157
|
if (agentModelNames.length > 0) {
|
|
987
1158
|
const mainCacheSupported = mainModelEntries.some(([, u]) => u.cacheSupported);
|
|
988
|
-
const
|
|
989
|
-
const
|
|
1159
|
+
const mainAllSubscription = mainModelEntries.every(([, u]) => u.subscription);
|
|
1160
|
+
const mainParts = [inputPart(mainInput, mainCacheRead, mainCacheWrite, mainCacheSupported), `↓${formatTokenCount(mainOutput)}`];
|
|
1161
|
+
const mainCR = Math.round(hitRate(mainInput, mainCacheRead, mainCacheWrite) * 100);
|
|
990
1162
|
if (mainCacheSupported) mainParts.push(`⚡${mainCR}%`);
|
|
991
|
-
if (
|
|
1163
|
+
if (mainAllSubscription) mainParts.push("subscription");
|
|
1164
|
+
else if (mainCost > 0) mainParts.push(`$${mainCost.toFixed(2)}`);
|
|
992
1165
|
lines.push(` Main (${agentModelNames.join(", ")}): ${mainParts.join(" ")}`);
|
|
993
1166
|
}
|
|
994
|
-
const byAgentType = new Map<string, { input: number; output: number; cacheRead: number; cacheSupported: boolean; cost: number; durationMs: number; toolUses: number; count: number }>();
|
|
1167
|
+
const byAgentType = new Map<string, { input: number; output: number; cacheRead: number; cacheWrite: number; cacheSupported: boolean; cost: number; durationMs: number; toolUses: number; count: number; subscriptionRuns: number }>();
|
|
995
1168
|
for (const sa of subagents) {
|
|
996
1169
|
const key = sa.agentType || sa.description;
|
|
997
1170
|
const existing = byAgentType.get(key);
|
|
@@ -999,24 +1172,29 @@ function showUsage(ctx: any): void {
|
|
|
999
1172
|
existing.input += sa.inputTokens;
|
|
1000
1173
|
existing.output += sa.outputTokens;
|
|
1001
1174
|
existing.cacheRead += sa.cacheReadTokens;
|
|
1175
|
+
existing.cacheWrite += sa.cacheWriteTokens;
|
|
1002
1176
|
if (sa.cacheSupported) existing.cacheSupported = true;
|
|
1003
1177
|
existing.cost += sa.cost;
|
|
1004
1178
|
existing.durationMs += sa.durationMs;
|
|
1005
1179
|
existing.toolUses += sa.toolUses;
|
|
1006
1180
|
existing.count += 1;
|
|
1181
|
+
if (sa.subscription) existing.subscriptionRuns += 1;
|
|
1007
1182
|
} else {
|
|
1008
1183
|
byAgentType.set(key, {
|
|
1009
|
-
input: sa.inputTokens, output: sa.outputTokens, cacheRead: sa.cacheReadTokens,
|
|
1184
|
+
input: sa.inputTokens, output: sa.outputTokens, cacheRead: sa.cacheReadTokens, cacheWrite: sa.cacheWriteTokens,
|
|
1010
1185
|
cacheSupported: sa.cacheSupported, cost: sa.cost, durationMs: sa.durationMs, toolUses: sa.toolUses, count: 1,
|
|
1186
|
+
subscriptionRuns: sa.subscription ? 1 : 0,
|
|
1011
1187
|
});
|
|
1012
1188
|
}
|
|
1013
1189
|
}
|
|
1014
1190
|
for (const [agentType, agg] of byAgentType) {
|
|
1015
|
-
const saCR = (agg.cacheRead
|
|
1016
|
-
|
|
1017
|
-
const parts = [`↑${formatTokenCount(agg.input)}`, `↓${formatTokenCount(agg.output)}`];
|
|
1191
|
+
const saCR = Math.round(hitRate(agg.input, agg.cacheRead, agg.cacheWrite) * 100);
|
|
1192
|
+
const parts = [inputPart(agg.input, agg.cacheRead, agg.cacheWrite, agg.cacheSupported), `↓${formatTokenCount(agg.output)}`];
|
|
1018
1193
|
if (agg.cacheSupported) parts.push(`⚡${saCR}%`);
|
|
1019
|
-
|
|
1194
|
+
// All runs subscription-routed → flat-rate label; otherwise show the
|
|
1195
|
+
// paid-only summed cost (subscription runs already contribute $0).
|
|
1196
|
+
if (agg.subscriptionRuns === agg.count) parts.push("subscription");
|
|
1197
|
+
else if (agg.cost > 0) parts.push(`$${agg.cost.toFixed(2)}`);
|
|
1020
1198
|
if (agg.durationMs > 0) parts.push(formatElapsedDuration(agg.durationMs));
|
|
1021
1199
|
if (agg.toolUses > 0) parts.push(`${agg.toolUses} tools`);
|
|
1022
1200
|
const countSuffix = agg.count > 1 ? ` (×${agg.count})` : "";
|
|
@@ -1085,12 +1263,16 @@ const ORCHESTRATOR_ROLES: Array<{ role: MainModelRole; label: string; descriptio
|
|
|
1085
1263
|
{ role: "plan", label: "Planner", description: "agents.orchestrators.plan" },
|
|
1086
1264
|
{ role: "debug", label: "Debugger", description: "agents.orchestrators.debug" },
|
|
1087
1265
|
{ role: "review", label: "Reviewer", description: "agents.orchestrators.review" },
|
|
1266
|
+
{ role: "quick", label: "Quick", description: "agents.orchestrators.quick" },
|
|
1088
1267
|
];
|
|
1089
1268
|
|
|
1090
1269
|
const SUBAGENT_ROLES: Array<{ role: AgentRole; label: string; description: string }> = [
|
|
1091
1270
|
{ role: "explore", label: "Explore", description: "agents.subagents.simple.explore" },
|
|
1092
1271
|
{ role: "librarian", label: "Librarian", description: "agents.subagents.simple.librarian" },
|
|
1093
1272
|
{ role: "task", label: "Task", description: "agents.subagents.simple.task" },
|
|
1273
|
+
{ role: "advisor", label: "Advisor", description: "agents.subagents.simple.advisor" },
|
|
1274
|
+
{ role: "deep-debugger", label: "Deep debugger", description: "agents.subagents.simple.deep-debugger" },
|
|
1275
|
+
{ role: "reviewer", label: "Reviewer", description: "agents.subagents.simple.reviewer" },
|
|
1094
1276
|
];
|
|
1095
1277
|
|
|
1096
1278
|
const PRESET_GROUP_ITEMS: Array<{ group: PresetGroup; label: string }> = [
|
|
@@ -1816,7 +1998,7 @@ async function showPresetVariantEditor(
|
|
|
1816
1998
|
continue;
|
|
1817
1999
|
}
|
|
1818
2000
|
if (choice.startsWith("Enabled:")) {
|
|
1819
|
-
await showBooleanSetting(orchestrator, ctx, "Enabled", [...variantPath, "enabled"]);
|
|
2001
|
+
await showBooleanSetting(orchestrator, ctx, "Enabled", [...variantPath, "enabled"], "Make this agent available for use", "Disable this agent so it is not used");
|
|
1820
2002
|
continue;
|
|
1821
2003
|
}
|
|
1822
2004
|
const confirm = await selectOption(ctx, "Confirm delete?", [
|
|
@@ -1957,8 +2139,8 @@ async function showPresetEnabledSetting(
|
|
|
1957
2139
|
const yesTitle = withTags("Yes", formatSourceTags(true, info));
|
|
1958
2140
|
const noTitle = withTags("No", formatSourceTags(false, info));
|
|
1959
2141
|
const choice = await selectOption(ctx, "Enabled", [
|
|
1960
|
-
yesTitle,
|
|
1961
|
-
noTitle,
|
|
2142
|
+
{ title: yesTitle, description: "Make this preset available for selection" },
|
|
2143
|
+
{ title: noTitle, description: "Hide this preset so it can no longer be selected" },
|
|
1962
2144
|
opt("Back", "Return to the previous menu"),
|
|
1963
2145
|
]);
|
|
1964
2146
|
if (!choice || choice === "Back") return;
|
|
@@ -2139,7 +2321,7 @@ async function showAfterEditCommands(orchestrator: Orchestrator, ctx: any): Prom
|
|
|
2139
2321
|
continue;
|
|
2140
2322
|
}
|
|
2141
2323
|
if (commandChoice.startsWith("Enabled:")) {
|
|
2142
|
-
await showBooleanSetting(orchestrator, ctx, "Enabled", [...commandPath, "enabled"]);
|
|
2324
|
+
await showBooleanSetting(orchestrator, ctx, "Enabled", [...commandPath, "enabled"], "Run this command when its triggers fire", "Keep this command configured but stop running it");
|
|
2143
2325
|
continue;
|
|
2144
2326
|
}
|
|
2145
2327
|
if (commandChoice === "Triggers") {
|
|
@@ -2256,7 +2438,7 @@ async function showAfterImplementCommands(orchestrator: Orchestrator, ctx: any):
|
|
|
2256
2438
|
continue;
|
|
2257
2439
|
}
|
|
2258
2440
|
if (commandChoice.startsWith("Enabled:")) {
|
|
2259
|
-
await showBooleanSetting(orchestrator, ctx, "Enabled", [...commandPath, "enabled"]);
|
|
2441
|
+
await showBooleanSetting(orchestrator, ctx, "Enabled", [...commandPath, "enabled"], "Run this command when its triggers fire", "Keep this command configured but stop running it");
|
|
2260
2442
|
continue;
|
|
2261
2443
|
}
|
|
2262
2444
|
const confirm = await selectOption(ctx, "Confirm delete?", [
|
|
@@ -2379,14 +2561,16 @@ async function showBooleanSetting(
|
|
|
2379
2561
|
ctx: any,
|
|
2380
2562
|
title: string,
|
|
2381
2563
|
keyPath: string[],
|
|
2564
|
+
yesDescription = "Turn this setting on",
|
|
2565
|
+
noDescription = "Turn this setting off",
|
|
2382
2566
|
): Promise<void> {
|
|
2383
2567
|
while (true) {
|
|
2384
2568
|
const info = getConfigSourceInfo(orchestrator, keyPath);
|
|
2385
2569
|
const yesTitle = withTags("Yes", formatSourceTags(true, info));
|
|
2386
2570
|
const noTitle = withTags("No", formatSourceTags(false, info));
|
|
2387
2571
|
const choice = await selectOption(ctx, title, [
|
|
2388
|
-
yesTitle,
|
|
2389
|
-
noTitle,
|
|
2572
|
+
{ title: yesTitle, description: yesDescription },
|
|
2573
|
+
{ title: noTitle, description: noDescription },
|
|
2390
2574
|
...buildResetOptions(orchestrator, keyPath),
|
|
2391
2575
|
opt("Back", "Return to the previous menu"),
|
|
2392
2576
|
]);
|
|
@@ -2408,14 +2592,16 @@ async function showInvertedBooleanSetting(
|
|
|
2408
2592
|
ctx: any,
|
|
2409
2593
|
title: string,
|
|
2410
2594
|
keyPath: string[],
|
|
2595
|
+
yesDescription = "Turn this setting on",
|
|
2596
|
+
noDescription = "Turn this setting off",
|
|
2411
2597
|
): Promise<void> {
|
|
2412
2598
|
while (true) {
|
|
2413
2599
|
const info = getConfigSourceInfo(orchestrator, keyPath);
|
|
2414
2600
|
const yesTitle = withTags("Yes", formatSourceTags(false, info));
|
|
2415
2601
|
const noTitle = withTags("No", formatSourceTags(true, info));
|
|
2416
2602
|
const choice = await selectOption(ctx, title, [
|
|
2417
|
-
yesTitle,
|
|
2418
|
-
noTitle,
|
|
2603
|
+
{ title: yesTitle, description: yesDescription },
|
|
2604
|
+
{ title: noTitle, description: noDescription },
|
|
2419
2605
|
...buildResetOptions(orchestrator, keyPath),
|
|
2420
2606
|
opt("Back", "Return to the previous menu"),
|
|
2421
2607
|
]);
|
|
@@ -2433,15 +2619,15 @@ async function showInvertedBooleanSetting(
|
|
|
2433
2619
|
}
|
|
2434
2620
|
|
|
2435
2621
|
async function showLogLevelSetting(orchestrator: Orchestrator, ctx: any): Promise<void> {
|
|
2436
|
-
const levels: Array<{ value: PiPiConfig["general"]["logLevel"]; label: string }> = [
|
|
2437
|
-
{ value: "debug", label: "Debug" },
|
|
2438
|
-
{ value: "info", label: "Info" },
|
|
2439
|
-
{ value: "warn", label: "Warning" },
|
|
2440
|
-
{ value: "error", label: "Error" },
|
|
2622
|
+
const levels: Array<{ value: PiPiConfig["general"]["logLevel"]; label: string; description: string }> = [
|
|
2623
|
+
{ value: "debug", label: "Debug", description: "Log everything, including detailed diagnostics for troubleshooting" },
|
|
2624
|
+
{ value: "info", label: "Info", description: "Log normal activity plus warnings and errors" },
|
|
2625
|
+
{ value: "warn", label: "Warning", description: "Log only warnings and errors" },
|
|
2626
|
+
{ value: "error", label: "Error", description: "Log only errors" },
|
|
2441
2627
|
];
|
|
2442
2628
|
while (true) {
|
|
2443
2629
|
const info = getConfigSourceInfo(orchestrator, ["general", "logLevel"]);
|
|
2444
|
-
const options: OptionInput[] = levels.map((entry) => withTags(entry.label, formatSourceTags(entry.value, info)));
|
|
2630
|
+
const options: OptionInput[] = levels.map((entry) => ({ title: withTags(entry.label, formatSourceTags(entry.value, info)), description: entry.description }));
|
|
2445
2631
|
options.push(...buildResetOptions(orchestrator, ["general", "logLevel"]));
|
|
2446
2632
|
options.push(opt("Back", "Return to the previous menu"));
|
|
2447
2633
|
const choice = await selectOption(ctx, "Log level", options);
|
|
@@ -2471,11 +2657,11 @@ async function showGeneralSettings(orchestrator: Orchestrator, ctx: any): Promis
|
|
|
2471
2657
|
]);
|
|
2472
2658
|
if (!choice || choice === "Back") return BACK;
|
|
2473
2659
|
if (choice.startsWith("Commit automatically:")) {
|
|
2474
|
-
await showBooleanSetting(orchestrator, ctx, "Commit automatically", ["general", "autoCommit"]);
|
|
2660
|
+
await showBooleanSetting(orchestrator, ctx, "Commit automatically", ["general", "autoCommit"], "Commit changes automatically as work progresses", "Leave committing to you");
|
|
2475
2661
|
continue;
|
|
2476
2662
|
}
|
|
2477
2663
|
if (choice.startsWith("Ignore configs from other repos:")) {
|
|
2478
|
-
await showInvertedBooleanSetting(orchestrator, ctx, "Ignore configs from other repos", ["general", "loadExtraRepoConfigs"]);
|
|
2664
|
+
await showInvertedBooleanSetting(orchestrator, ctx, "Ignore configs from other repos", ["general", "loadExtraRepoConfigs"], "Use only the root repo config and ignore configs from other registered repos", "Also load configs from the other registered repos");
|
|
2479
2665
|
continue;
|
|
2480
2666
|
}
|
|
2481
2667
|
if (choice.startsWith("Log level:")) {
|
|
@@ -2483,7 +2669,7 @@ async function showGeneralSettings(orchestrator: Orchestrator, ctx: any): Promis
|
|
|
2483
2669
|
continue;
|
|
2484
2670
|
}
|
|
2485
2671
|
if (choice.startsWith("Tracing:")) {
|
|
2486
|
-
await showBooleanSetting(orchestrator, ctx, "Tracing", ["general", "tracing"]);
|
|
2672
|
+
await showBooleanSetting(orchestrator, ctx, "Tracing", ["general", "tracing"], "Capture full session traces to .pp/logs/traces/", "Do not record session traces");
|
|
2487
2673
|
continue;
|
|
2488
2674
|
}
|
|
2489
2675
|
await showFlantInfraMenu(orchestrator, ctx);
|
|
@@ -2737,14 +2923,59 @@ async function pickModeForTaskStart(
|
|
|
2737
2923
|
}
|
|
2738
2924
|
}
|
|
2739
2925
|
|
|
2926
|
+
// Title: scannable human-readable intent + age. Callers must guarantee
|
|
2927
|
+
// uniqueness across the menu (see buildResumeOptions) so the option->task
|
|
2928
|
+
// mapping stays stable.
|
|
2740
2929
|
function resumeOptionTitle(t: TaskInfo): string {
|
|
2741
|
-
return
|
|
2930
|
+
return `${taskNameFromState(t.dir, t.state)} — ${taskAge(t.state)}`;
|
|
2742
2931
|
}
|
|
2743
2932
|
|
|
2744
|
-
|
|
2745
|
-
|
|
2746
|
-
|
|
2747
|
-
|
|
2933
|
+
// Description: rich per-entry detail sourced entirely from the already-loaded
|
|
2934
|
+
// TaskState (no extra disk reads). Empty/irrelevant fields are omitted.
|
|
2935
|
+
function resumeOptionDescription(t: TaskInfo, cwd: string): string {
|
|
2936
|
+
const s = t.state;
|
|
2937
|
+
const parts: string[] = [];
|
|
2938
|
+
|
|
2939
|
+
const phaseStep = s.step ? `${s.phase}/${s.step}` : s.phase;
|
|
2940
|
+
parts.push(`${t.type} · ${phaseStep}`);
|
|
2941
|
+
|
|
2942
|
+
const mode = s.effectiveMode ?? s.mode;
|
|
2943
|
+
if (mode) parts.push(mode);
|
|
2944
|
+
|
|
2945
|
+
if (s.reviewCycle && s.reviewCycle.pass > 0) parts.push(`review pass ${s.reviewCycle.pass}`);
|
|
2946
|
+
|
|
2947
|
+
const fileCount = s.modifiedFiles?.length ?? 0;
|
|
2948
|
+
if (fileCount > 0) parts.push(`${fileCount} file${fileCount === 1 ? "" : "s"}`);
|
|
2949
|
+
|
|
2950
|
+
// Only surface the repo when it is informative: a multi-repo task, or a root
|
|
2951
|
+
// repo that differs from the current project directory.
|
|
2952
|
+
const rootRepo = s.repos?.find((r) => r.isRoot) ?? s.repos?.[0];
|
|
2953
|
+
if (rootRepo && ((s.repos?.length ?? 0) > 1 || basename(rootRepo.path) !== basename(cwd))) {
|
|
2954
|
+
parts.push(`repo: ${basename(rootRepo.path)}`);
|
|
2955
|
+
}
|
|
2956
|
+
|
|
2957
|
+
parts.push(`id ${taskShortId(t.dir)}`);
|
|
2958
|
+
return parts.join(" · ");
|
|
2959
|
+
}
|
|
2960
|
+
|
|
2961
|
+
// Build menu options with a stable option->task index. Titles are made unique
|
|
2962
|
+
// (short id appended on collision) so selection maps back deterministically even
|
|
2963
|
+
// if two tasks share an intent, and the age component can't break the mapping
|
|
2964
|
+
// after the picker returns.
|
|
2965
|
+
function buildResumeOptions(tasks: TaskInfo[], cwd: string): { options: OptionInput[]; byTitle: Map<string, TaskInfo> } {
|
|
2966
|
+
const seen = new Map<string, number>();
|
|
2967
|
+
const byTitle = new Map<string, TaskInfo>();
|
|
2968
|
+
const options: OptionInput[] = [];
|
|
2969
|
+
for (const t of tasks) {
|
|
2970
|
+
let title = resumeOptionTitle(t);
|
|
2971
|
+
const count = seen.get(title) ?? 0;
|
|
2972
|
+
seen.set(title, count + 1);
|
|
2973
|
+
if (count > 0 || byTitle.has(title)) title = `${title} [${taskShortId(t.dir)}]`;
|
|
2974
|
+
while (byTitle.has(title)) title = `${title} ·`;
|
|
2975
|
+
byTitle.set(title, t);
|
|
2976
|
+
options.push({ title, description: resumeOptionDescription(t, cwd) });
|
|
2977
|
+
}
|
|
2978
|
+
return { options, byTitle };
|
|
2748
2979
|
}
|
|
2749
2980
|
|
|
2750
2981
|
async function showResumeMenu(
|
|
@@ -2760,16 +2991,13 @@ async function showResumeMenu(
|
|
|
2760
2991
|
return BACK;
|
|
2761
2992
|
}
|
|
2762
2993
|
|
|
2763
|
-
const options
|
|
2764
|
-
title: resumeOptionTitle(t),
|
|
2765
|
-
description: resumeOptionDescription(t),
|
|
2766
|
-
}));
|
|
2994
|
+
const { options, byTitle } = buildResumeOptions(tasks, orchestrator.cwd);
|
|
2767
2995
|
options.push({ title: "Back", description: "Return to the previous menu" });
|
|
2768
2996
|
|
|
2769
2997
|
const choice = await selectOption(ctx, "Resume", options);
|
|
2770
2998
|
if (!choice || choice === "Back") return BACK;
|
|
2771
2999
|
|
|
2772
|
-
const task =
|
|
3000
|
+
const task = byTitle.get(choice);
|
|
2773
3001
|
if (!task) continue;
|
|
2774
3002
|
const result = await resumeTask(orchestrator, ctx, task);
|
|
2775
3003
|
if (result.ok) return "started";
|
|
@@ -2906,30 +3134,24 @@ async function openCodeReviewInPlannotator(
|
|
|
2906
3134
|
if (payload.diffType) requestPayload.diffType = payload.diffType;
|
|
2907
3135
|
if (payload.defaultBranch) requestPayload.defaultBranch = payload.defaultBranch;
|
|
2908
3136
|
|
|
2909
|
-
|
|
2910
|
-
|
|
2911
|
-
|
|
2912
|
-
|
|
2913
|
-
|
|
2914
|
-
|
|
2915
|
-
|
|
2916
|
-
|
|
2917
|
-
|
|
2918
|
-
|
|
2919
|
-
|
|
2920
|
-
|
|
2921
|
-
|
|
2922
|
-
|
|
2923
|
-
|
|
2924
|
-
|
|
2925
|
-
|
|
2926
|
-
|
|
2927
|
-
? response.result.feedback
|
|
2928
|
-
: undefined;
|
|
2929
|
-
resolve({ status: approved ? "approved" : "needs_changes", feedback });
|
|
2930
|
-
},
|
|
2931
|
-
});
|
|
2932
|
-
});
|
|
3137
|
+
const { opened, reviewId } = await openPlannotator(orchestrator.pi, "code-review", requestPayload);
|
|
3138
|
+
if (!opened) {
|
|
3139
|
+
return { status: "error", error: "Plannotator is not available." };
|
|
3140
|
+
}
|
|
3141
|
+
|
|
3142
|
+
let result: { approved: boolean; feedback?: string; error?: string };
|
|
3143
|
+
try {
|
|
3144
|
+
result = await waitForPlannotatorResult(orchestrator, reviewId, null);
|
|
3145
|
+
} catch (err) {
|
|
3146
|
+
return { status: "error", error: err instanceof Error ? err.message : "Plannotator review failed." };
|
|
3147
|
+
}
|
|
3148
|
+
if (result.error) {
|
|
3149
|
+
return { status: "error", error: result.error };
|
|
3150
|
+
}
|
|
3151
|
+
const feedback = typeof result.feedback === "string" && result.feedback.trim().length > 0
|
|
3152
|
+
? result.feedback
|
|
3153
|
+
: undefined;
|
|
3154
|
+
return { status: result.approved ? "approved" : "needs_changes", feedback };
|
|
2933
3155
|
}
|
|
2934
3156
|
|
|
2935
3157
|
async function startReviewTask(
|
|
@@ -3251,13 +3473,16 @@ async function showQuickTaskMenu(
|
|
|
3251
3473
|
if (summary !== "/pp") headerLines.push(`\n\n${summary}`);
|
|
3252
3474
|
const menuTitle = headerLines.join("");
|
|
3253
3475
|
|
|
3254
|
-
const choice = await
|
|
3476
|
+
const { choice, cancelReason } = await selectOptionCancelable(ctx, menuTitle, [
|
|
3255
3477
|
opt("Complete", "Mark task as done and clean up"),
|
|
3256
3478
|
opt("Pause", "Suspend task to resume later"),
|
|
3257
3479
|
opt("Info", "Subagents, usage, and task status"),
|
|
3258
3480
|
opt("Settings", "Flant AI and other configuration"),
|
|
3259
3481
|
opt("Back", "Return to the prompt and keep working"),
|
|
3260
3482
|
]);
|
|
3483
|
+
// A deliberate ESC in tool mode must stop the turn cleanly (mirror the
|
|
3484
|
+
// guided/autonomous branches); in command mode ESC just closes the menu.
|
|
3485
|
+
if (cancelReason === "user" && mode === "tool") return USER_CANCELLED;
|
|
3261
3486
|
if (!choice || choice === "Back") return "";
|
|
3262
3487
|
if (choice === "Info") {
|
|
3263
3488
|
await showInfoMenu(orchestrator, ctx);
|
|
@@ -3282,7 +3507,7 @@ export async function showActiveTaskMenu(
|
|
|
3282
3507
|
summary: string,
|
|
3283
3508
|
mode: MenuMode = "command",
|
|
3284
3509
|
): Promise<string> {
|
|
3285
|
-
const continueMessage = "User wants to continue. Run /pp when ready to advance.";
|
|
3510
|
+
const continueMessage = advanceBanner("[PI-PI] User wants to continue. Run /pp when ready to advance.");
|
|
3286
3511
|
|
|
3287
3512
|
while (true) {
|
|
3288
3513
|
if (!orchestrator.active) return "No active task.";
|
|
@@ -3306,13 +3531,17 @@ export async function showActiveTaskMenu(
|
|
|
3306
3531
|
const opt = (title: string, description: string): OptionInput => ({ title, description });
|
|
3307
3532
|
|
|
3308
3533
|
if (effectiveMode === "autonomous") {
|
|
3309
|
-
const autoChoice = await
|
|
3534
|
+
const { choice: autoChoice, cancelReason } = await selectOptionCancelable(ctx, `/pp\n\nTask: ${task.type}\nPhase: ${phase}${summary !== "/pp" ? `\n\n${summary}` : ""}`, [
|
|
3310
3535
|
opt("Complete task", "Mark task as done and clean up"),
|
|
3311
3536
|
opt("Pause task", "Suspend task to resume later"),
|
|
3312
3537
|
opt("Info", "Subagents, usage, and task status"),
|
|
3313
3538
|
opt("Settings", "Flant AI and other configuration"),
|
|
3314
3539
|
opt("Back", "Return to the prompt and keep working"),
|
|
3315
3540
|
]);
|
|
3541
|
+
// A deliberate ESC only needs distinct handling in tool mode (so
|
|
3542
|
+
// pp_phase_complete can stop the turn); in command mode ESC just closes
|
|
3543
|
+
// the menu like "Back".
|
|
3544
|
+
if (cancelReason === "user" && mode === "tool") return USER_CANCELLED;
|
|
3316
3545
|
if (!autoChoice || autoChoice === "Back") return "";
|
|
3317
3546
|
if (autoChoice === "Info") {
|
|
3318
3547
|
await showInfoMenu(orchestrator, ctx);
|
|
@@ -3342,7 +3571,8 @@ export async function showActiveTaskMenu(
|
|
|
3342
3571
|
const headerLines = [`/pp\n\nTask: ${task.type}\nPhase: ${phase}`];
|
|
3343
3572
|
if (summary !== "/pp") headerLines.push(`\n\n${summary}`);
|
|
3344
3573
|
const menuTitle = headerLines.join("");
|
|
3345
|
-
const choice = await
|
|
3574
|
+
const { choice, cancelReason } = await selectOptionCancelable(ctx, menuTitle, options);
|
|
3575
|
+
if (cancelReason === "user" && mode === "tool") return USER_CANCELLED;
|
|
3346
3576
|
if (!choice || choice === "Back") {
|
|
3347
3577
|
return "";
|
|
3348
3578
|
}
|
|
@@ -3417,7 +3647,9 @@ export async function showActiveTaskMenu(
|
|
|
3417
3647
|
if (hasPlannotator) {
|
|
3418
3648
|
reviewOptions.push(opt("Review in Plannotator", phase === "plan" ? "Open plan review in browser" : "Open code diff review in browser"));
|
|
3419
3649
|
}
|
|
3420
|
-
reviewOptions.push(opt("Review on my own",
|
|
3650
|
+
reviewOptions.push(opt("Review on my own", phase === "implement"
|
|
3651
|
+
? "Review in your editor: mark spots with AI_REVIEW: comments, then have the agent address them"
|
|
3652
|
+
: "Review manually, then continue"));
|
|
3421
3653
|
reviewOptions.push(opt("Back", "Return to the previous menu"));
|
|
3422
3654
|
|
|
3423
3655
|
const reviewChoice = await selectOption(ctx, "Review", reviewOptions);
|
|
@@ -3433,8 +3665,23 @@ export async function showActiveTaskMenu(
|
|
|
3433
3665
|
if (handled.continueLoop) continue;
|
|
3434
3666
|
return handled.text ?? text;
|
|
3435
3667
|
}
|
|
3436
|
-
const
|
|
3668
|
+
const allRepos = getRegisteredRepos(orchestrator);
|
|
3437
3669
|
const summaries: string[] = [];
|
|
3670
|
+
const repos: RepoInfo[] = [];
|
|
3671
|
+
for (const repo of allRepos) {
|
|
3672
|
+
const base = await detectDefaultBranch(orchestrator, allRepos, repo.path);
|
|
3673
|
+
const { changed, error } = await repoHasReviewableChanges(orchestrator, repo, base);
|
|
3674
|
+
if (error) {
|
|
3675
|
+
summaries.push(`${formatRepoLabel(repo)}: ERROR — ${error}`);
|
|
3676
|
+
repos.push(repo);
|
|
3677
|
+
} else if (changed) {
|
|
3678
|
+
repos.push(repo);
|
|
3679
|
+
}
|
|
3680
|
+
}
|
|
3681
|
+
if (repos.length === 0) {
|
|
3682
|
+
ctx.ui.notify("No registered repositories have changes to review.", "info");
|
|
3683
|
+
continue;
|
|
3684
|
+
}
|
|
3438
3685
|
let stopReviewing = false;
|
|
3439
3686
|
|
|
3440
3687
|
for (const repo of repos) {
|
|
@@ -3503,10 +3750,35 @@ export async function showActiveTaskMenu(
|
|
|
3503
3750
|
if (reviewChoice === "Review on my own") {
|
|
3504
3751
|
if (phase === "plan") {
|
|
3505
3752
|
setStep(orchestrator, "synthesize");
|
|
3506
|
-
|
|
3753
|
+
return continueMessage;
|
|
3754
|
+
}
|
|
3755
|
+
if (phase !== "implement") {
|
|
3507
3756
|
setStep(orchestrator, "llm_work");
|
|
3757
|
+
return continueMessage;
|
|
3508
3758
|
}
|
|
3509
|
-
|
|
3759
|
+
|
|
3760
|
+
const gate = await selectOption(ctx, "Editor review", [
|
|
3761
|
+
opt("Done", "I've added AI_REVIEW: markers and saved my files"),
|
|
3762
|
+
opt("Skip markers", "Continue without the marker workflow"),
|
|
3763
|
+
opt("Back", "Return to the review menu"),
|
|
3764
|
+
]);
|
|
3765
|
+
if (!gate || gate === "Back") continue;
|
|
3766
|
+
setStep(orchestrator, "llm_work");
|
|
3767
|
+
if (gate === "Skip markers") {
|
|
3768
|
+
return continueMessage;
|
|
3769
|
+
}
|
|
3770
|
+
return advanceBanner(
|
|
3771
|
+
"[PI-PI] The user reviewed the changes in their editor and left inline `AI_REVIEW:` markers " +
|
|
3772
|
+
"(inside each file's native comment syntax, e.g. `// AI_REVIEW: ...`, `# AI_REVIEW: ...`, " +
|
|
3773
|
+
"`<!-- AI_REVIEW: ... -->`).\n\n" +
|
|
3774
|
+
"For each registered repo, enumerate the CHANGED files only — union of `git diff --name-only <base>...HEAD`, " +
|
|
3775
|
+
"`git diff --name-only`, `git diff --cached --name-only`, and untracked-non-ignored files from `git status --porcelain` " +
|
|
3776
|
+
"(base = each repo's configured base branch) — and search WITHIN those files for `AI_REVIEW:`. Do NOT grep the whole " +
|
|
3777
|
+
"worktree (avoid vendored/generated/node_modules and historical markers).\n\n" +
|
|
3778
|
+
"For each marker: address the request, then remove that marker in the SAME edit. After a pass, re-scan the changed " +
|
|
3779
|
+
"files and repeat until no `AI_REVIEW:` markers remain. Then verify your work and report what you changed per marker. " +
|
|
3780
|
+
"When complete, call pp_phase_complete.",
|
|
3781
|
+
);
|
|
3510
3782
|
}
|
|
3511
3783
|
|
|
3512
3784
|
const reviewPreset = await pickPreset(ctx, orchestrator, getReviewPresetGroup(phase), "Review preset");
|