@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
|
@@ -441,10 +441,8 @@ export async function startCodeReviewBrowserSession(
|
|
|
441
441
|
rawPatch = result.rawPatch;
|
|
442
442
|
gitRef = result.gitRef;
|
|
443
443
|
diffError = result.error;
|
|
444
|
-
// Remember which base the initial diff was computed against so it can
|
|
445
|
-
// be forwarded to the server below. Only matters when the caller
|
|
446
|
-
// overrode the detected default; otherwise it matches gitCtx already.
|
|
447
444
|
initialBase = result.base;
|
|
445
|
+
|
|
448
446
|
} else {
|
|
449
447
|
workspace = await buildLocalWorkspaceReview(cwd, {
|
|
450
448
|
requestedDiffType: options.diffType,
|
|
@@ -741,8 +741,6 @@ export async function runGitDiff(
|
|
|
741
741
|
}
|
|
742
742
|
} catch (error) {
|
|
743
743
|
const raw = error instanceof Error ? error.message : String(error);
|
|
744
|
-
// Git dumps its entire --help output on some failures; keep only the
|
|
745
|
-
// first meaningful line so the UI doesn't vomit a wall of text.
|
|
746
744
|
const firstLine = raw.split("\n").find((l) => l.trim().length > 0) ?? raw;
|
|
747
745
|
const message = firstLine.length > 200 ? firstLine.slice(0, 200) + "…" : firstLine;
|
|
748
746
|
return {
|
|
@@ -368,17 +368,24 @@ Use get_subagent_result for full output.`,
|
|
|
368
368
|
/** Helper: build event data for lifecycle events from an AgentRecord. */
|
|
369
369
|
function buildEventData(record: AgentRecord) {
|
|
370
370
|
const durationMs = record.completedAt ? record.completedAt - record.startedAt : Date.now() - record.startedAt;
|
|
371
|
-
let tokens: { input: number; output: number; total: number } | undefined;
|
|
371
|
+
let tokens: { input: number; output: number; cacheRead: number; cacheWrite: number; total: number; cost: number } | undefined;
|
|
372
372
|
try {
|
|
373
373
|
if (record.session) {
|
|
374
374
|
const stats = record.session.getSessionStats();
|
|
375
375
|
tokens = {
|
|
376
376
|
input: stats.tokens?.input ?? 0,
|
|
377
377
|
output: stats.tokens?.output ?? 0,
|
|
378
|
+
cacheRead: stats.tokens?.cacheRead ?? 0,
|
|
379
|
+
cacheWrite: stats.tokens?.cacheWrite ?? 0,
|
|
378
380
|
total: stats.tokens?.total ?? 0,
|
|
381
|
+
cost: typeof stats.cost === "number" ? stats.cost : 0,
|
|
379
382
|
};
|
|
380
383
|
}
|
|
381
384
|
} catch { /* session stats unavailable */ }
|
|
385
|
+
let modelId: string | undefined;
|
|
386
|
+
try {
|
|
387
|
+
modelId = record.session?.model?.id;
|
|
388
|
+
} catch {}
|
|
382
389
|
return {
|
|
383
390
|
id: record.id,
|
|
384
391
|
type: record.type,
|
|
@@ -388,6 +395,7 @@ Use get_subagent_result for full output.`,
|
|
|
388
395
|
status: record.status,
|
|
389
396
|
toolUses: record.toolUses,
|
|
390
397
|
durationMs,
|
|
398
|
+
modelId,
|
|
391
399
|
tokens,
|
|
392
400
|
};
|
|
393
401
|
}
|
|
@@ -58,6 +58,7 @@ export function createBrainstormReviewerAgent(
|
|
|
58
58
|
"",
|
|
59
59
|
"Your task is NOT complete until this file exists. Do NOT finish without writing it.",
|
|
60
60
|
"You MUST NOT write to any other file. Only write .md files inside .pp/state/.",
|
|
61
|
+
"Do NOT implement, fix, or modify any source code — you are a reviewer, not an implementer.",
|
|
61
62
|
"",
|
|
62
63
|
"=== USER REQUEST ===",
|
|
63
64
|
taskArtifacts.userRequest,
|
|
@@ -73,6 +73,7 @@ export function createCodeReviewerAgent(
|
|
|
73
73
|
"",
|
|
74
74
|
"Your task is NOT complete until this file exists. Do NOT finish without writing it.",
|
|
75
75
|
"You MUST NOT write to any other file. Only write .md files inside .pp/state/.",
|
|
76
|
+
"Do NOT implement, fix, or modify any source code — you are a reviewer, not an implementer.",
|
|
76
77
|
"",
|
|
77
78
|
"=== USER REQUEST ===",
|
|
78
79
|
taskArtifacts.userRequest,
|
|
@@ -54,11 +54,14 @@ export function createPlanReviewerAgent(
|
|
|
54
54
|
'You can spawn subagents: Agent(subagent_type="Explore", ...) for codebase, Agent(subagent_type="Librarian", ...) for external docs.',
|
|
55
55
|
"",
|
|
56
56
|
// --- dynamic suffix ---
|
|
57
|
-
"# MANDATORY
|
|
57
|
+
"# MANDATORY OUTPUT",
|
|
58
|
+
"",
|
|
59
|
+
"Write your review to this exact file using the write tool:",
|
|
58
60
|
` ${outputPath}`,
|
|
59
61
|
"",
|
|
60
62
|
"Your task is NOT complete until this file exists. Do NOT finish without writing it.",
|
|
61
63
|
"You MUST NOT write to any other file. Only write .md files inside .pp/state/.",
|
|
64
|
+
"Do NOT implement, fix, or modify any source code — you are a reviewer, not an implementer.",
|
|
62
65
|
"",
|
|
63
66
|
"=== USER REQUEST ===",
|
|
64
67
|
taskArtifacts.userRequest,
|
|
@@ -43,11 +43,22 @@ export function createPlannerAgent(
|
|
|
43
43
|
'You can spawn subagents: Agent(subagent_type="Explore", ...) for codebase, Agent(subagent_type="Librarian", ...) for external docs.',
|
|
44
44
|
"",
|
|
45
45
|
// --- dynamic suffix ---
|
|
46
|
-
"# MANDATORY
|
|
46
|
+
"# MANDATORY OUTPUT",
|
|
47
|
+
"",
|
|
48
|
+
"Write your plan to this exact file using the write tool:",
|
|
47
49
|
` ${outputPath}`,
|
|
48
50
|
"",
|
|
49
51
|
"Your task is NOT complete until this file exists. Do NOT finish without writing it.",
|
|
50
|
-
"
|
|
52
|
+
"",
|
|
53
|
+
"# CRITICAL: DO NOT IMPLEMENT ANYTHING",
|
|
54
|
+
"",
|
|
55
|
+
"You are a PLANNER, not an implementer. Your ONLY output is the plan file above.",
|
|
56
|
+
"- Do NOT write, edit, or create any file outside .pp/state/",
|
|
57
|
+
"- Do NOT create fix scripts, patches, or code files",
|
|
58
|
+
"- Do NOT modify source code, tests, configs, or any project file",
|
|
59
|
+
"- Do NOT use bash to write files (no echo >, sed -i, tee, scripts, etc.)",
|
|
60
|
+
"- If you catch yourself starting to implement — STOP IMMEDIATELY and go back to planning",
|
|
61
|
+
"- Violating this will cause your output to be DISCARDED",
|
|
51
62
|
"",
|
|
52
63
|
"=== USER REQUEST ===",
|
|
53
64
|
taskArtifacts.userRequest,
|
|
@@ -17,6 +17,15 @@ export const WORKING_PRINCIPLES_READONLY = [
|
|
|
17
17
|
"",
|
|
18
18
|
"- Verify, don't assume. Check actual state with tools. Never guess paths, types, or APIs.",
|
|
19
19
|
"- Evidence over claims. Show what you found with file paths and tool output, not guesses.",
|
|
20
|
+
"",
|
|
21
|
+
"# STRICT READ-ONLY CONSTRAINT",
|
|
22
|
+
"",
|
|
23
|
+
"You are a READ-ONLY agent. You MUST NOT modify any source code, configuration, or project files.",
|
|
24
|
+
"- Do NOT use write or edit tools on any file outside .pp/state/",
|
|
25
|
+
"- Do NOT use bash to create, modify, or delete files (no echo >, sed -i, tee, mv, cp, rm, touch, etc.)",
|
|
26
|
+
"- Do NOT create scripts or run commands that modify the filesystem",
|
|
27
|
+
"- You MAY use bash for read-only commands: git log, git diff, go list, npm ls, cat, find, ls, etc.",
|
|
28
|
+
"- Violating this constraint is a critical failure. Your output will be discarded if you modify source files.",
|
|
20
29
|
].join("\n");
|
|
21
30
|
|
|
22
31
|
export const FAILURE_RECOVERY = [
|
|
@@ -5,11 +5,11 @@ import { afterEach, describe, expect, it, vi } from "vitest";
|
|
|
5
5
|
import { registerCommandHandlers } from "./command-handlers.js";
|
|
6
6
|
import { Orchestrator, type ActiveTask } from "./orchestrator.js";
|
|
7
7
|
|
|
8
|
-
vi.mock("./
|
|
9
|
-
|
|
8
|
+
vi.mock("./pp-menu.js", () => ({
|
|
9
|
+
showPpMenu: vi.fn().mockResolvedValue(undefined),
|
|
10
10
|
}));
|
|
11
11
|
|
|
12
|
-
import {
|
|
12
|
+
import { showPpMenu } from "./pp-menu.js";
|
|
13
13
|
|
|
14
14
|
type Handler = (args: string | undefined, ctx: any) => any;
|
|
15
15
|
|
|
@@ -22,7 +22,7 @@ function makeTempDir(): string {
|
|
|
22
22
|
}
|
|
23
23
|
|
|
24
24
|
afterEach(() => {
|
|
25
|
-
vi.mocked(
|
|
25
|
+
vi.mocked(showPpMenu).mockClear();
|
|
26
26
|
while (tempDirs.length > 0) {
|
|
27
27
|
const dir = tempDirs.pop();
|
|
28
28
|
if (dir) rmSync(dir, { recursive: true, force: true });
|
|
@@ -53,6 +53,7 @@ function makeConfig() {
|
|
|
53
53
|
implement: { model: "a/b", thinking: "high" },
|
|
54
54
|
debug: { model: "a/b", thinking: "high" },
|
|
55
55
|
brainstorm: { model: "a/b", thinking: "high" },
|
|
56
|
+
review: { model: "a/b", thinking: "high" },
|
|
56
57
|
},
|
|
57
58
|
planners: {},
|
|
58
59
|
planReviewers: {},
|
|
@@ -109,8 +110,8 @@ function makeCtx() {
|
|
|
109
110
|
};
|
|
110
111
|
}
|
|
111
112
|
|
|
112
|
-
describe("pp
|
|
113
|
-
it("
|
|
113
|
+
describe("pp command", () => {
|
|
114
|
+
it("opens pp menu command", async () => {
|
|
114
115
|
const pi = makePi();
|
|
115
116
|
const orchestrator = new Orchestrator(pi as any);
|
|
116
117
|
const taskDir = makeTempDir();
|
|
@@ -121,30 +122,31 @@ describe("pp:next user gate", () => {
|
|
|
121
122
|
|
|
122
123
|
const ctx = makeCtx();
|
|
123
124
|
|
|
124
|
-
const
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
expect(
|
|
128
|
-
expect(pi.sendUserMessage).toHaveBeenCalledWith("[PI-PI] User wants to continue. Run /pp:next when ready to advance.");
|
|
125
|
+
const pp = pi._commands.get("pp");
|
|
126
|
+
expect(pp).toBeTruthy();
|
|
127
|
+
await pp!.handler(undefined, ctx);
|
|
128
|
+
expect(showPpMenu).toHaveBeenCalledWith(orchestrator, ctx, "command");
|
|
129
129
|
});
|
|
130
130
|
|
|
131
|
-
it("
|
|
131
|
+
it("sends follow-up message when menu returns text", async () => {
|
|
132
132
|
const pi = makePi();
|
|
133
133
|
const orchestrator = new Orchestrator(pi as any);
|
|
134
|
-
|
|
134
|
+
const taskDir = makeTempDir();
|
|
135
|
+
orchestrator.cwd = taskDir;
|
|
135
136
|
orchestrator.config = makeConfig() as any;
|
|
137
|
+
orchestrator.active = makeActiveTask(taskDir);
|
|
136
138
|
registerCommandHandlers(orchestrator);
|
|
137
139
|
|
|
138
140
|
const ctx = makeCtx();
|
|
141
|
+
vi.mocked(showPpMenu).mockResolvedValueOnce("User wants to continue. Run /pp when ready to advance.");
|
|
139
142
|
|
|
140
|
-
const
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
expect(
|
|
144
|
-
expect(runUserGateDialog).not.toHaveBeenCalled();
|
|
143
|
+
const pp = pi._commands.get("pp");
|
|
144
|
+
expect(pp).toBeTruthy();
|
|
145
|
+
await pp!.handler(undefined, ctx);
|
|
146
|
+
expect(pi.sendUserMessage).toHaveBeenCalledWith("[PI-PI] User wants to continue. Run /pp when ready to advance.");
|
|
145
147
|
});
|
|
146
148
|
|
|
147
|
-
it("
|
|
149
|
+
it("registers pp command", () => {
|
|
148
150
|
const pi = makePi();
|
|
149
151
|
const orchestrator = new Orchestrator(pi as any);
|
|
150
152
|
const taskDir = makeTempDir();
|
|
@@ -153,11 +155,6 @@ describe("pp:next user gate", () => {
|
|
|
153
155
|
orchestrator.active = makeActiveTask(taskDir);
|
|
154
156
|
registerCommandHandlers(orchestrator);
|
|
155
157
|
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
const ppNext = pi._commands.get("pp:next");
|
|
159
|
-
await ppNext!.handler(undefined, ctx);
|
|
160
|
-
|
|
161
|
-
expect(runUserGateDialog).toHaveBeenCalledWith(orchestrator, ctx, "Choose next action");
|
|
158
|
+
expect(pi._commands.has("pp")).toBe(true);
|
|
162
159
|
});
|
|
163
160
|
});
|
|
@@ -89,6 +89,7 @@ describe("validateConfig", () => {
|
|
|
89
89
|
implement: { model: "provider/model-1" },
|
|
90
90
|
debug: { model: "provider/model-2" },
|
|
91
91
|
brainstorm: { model: "provider/model-3" },
|
|
92
|
+
review: { model: "provider/model-4" },
|
|
92
93
|
},
|
|
93
94
|
planners: {
|
|
94
95
|
good: { enabled: true, model: "provider/model-4" },
|
|
@@ -37,6 +37,7 @@ export interface PiPiConfig {
|
|
|
37
37
|
implement: ModelConfig;
|
|
38
38
|
debug: ModelConfig;
|
|
39
39
|
brainstorm: ModelConfig;
|
|
40
|
+
review: ModelConfig;
|
|
40
41
|
};
|
|
41
42
|
planners: Record<string, VariantConfig>;
|
|
42
43
|
planReviewers: Record<string, VariantConfig>;
|
|
@@ -61,6 +62,7 @@ const DEFAULT_CONFIG: PiPiConfig = {
|
|
|
61
62
|
implement: { model: "anthropic/claude-opus-4-6", thinking: "high" },
|
|
62
63
|
debug: { model: "openai/gpt-5.4", thinking: "high" },
|
|
63
64
|
brainstorm: { model: "anthropic/claude-opus-4-6", thinking: "high" },
|
|
65
|
+
review: { model: "anthropic/claude-opus-4-6", thinking: "high" },
|
|
64
66
|
},
|
|
65
67
|
planners: {
|
|
66
68
|
opus: { enabled: true, model: "anthropic/claude-opus-4-6", thinking: "high" },
|
|
@@ -128,7 +130,7 @@ export function deepMerge(target: Record<string, any>, source: Record<string, an
|
|
|
128
130
|
|
|
129
131
|
export function validateConfig(config: Record<string, any>): void {
|
|
130
132
|
if (config.mainModel) {
|
|
131
|
-
for (const key of ["implement", "debug", "brainstorm"]) {
|
|
133
|
+
for (const key of ["implement", "debug", "brainstorm", "review"]) {
|
|
132
134
|
const mc = config.mainModel[key];
|
|
133
135
|
if (mc && typeof mc.model === "string" && mc.model.length === 0) {
|
|
134
136
|
throw new Error(`config.mainModel.${key}.model must be non-empty`);
|
|
@@ -0,0 +1,146 @@
|
|
|
1
|
+
import { homedir } from "node:os";
|
|
2
|
+
import type { ExtensionContext, ReadonlyFooterDataProvider, Theme } from "@earendil-works/pi-coding-agent";
|
|
3
|
+
import { truncateToWidth, visibleWidth, type Component, type TUI } from "@earendil-works/pi-tui";
|
|
4
|
+
import type { UsageTracker } from "./usage-tracker.js";
|
|
5
|
+
|
|
6
|
+
let footerCtx: ExtensionContext | undefined;
|
|
7
|
+
let footerTracker: UsageTracker | undefined;
|
|
8
|
+
|
|
9
|
+
export function setFooterContext(ctx: ExtensionContext): void {
|
|
10
|
+
footerCtx = ctx;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
export function setFooterTracker(tracker: UsageTracker): void {
|
|
14
|
+
footerTracker = tracker;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
function formatTokens(count: number): string {
|
|
18
|
+
if (count < 1000) return count.toString();
|
|
19
|
+
if (count < 10000) return `${(count / 1000).toFixed(1)}k`;
|
|
20
|
+
if (count < 1000000) return `${Math.round(count / 1000)}k`;
|
|
21
|
+
if (count < 10000000) return `${(count / 1000000).toFixed(1)}M`;
|
|
22
|
+
return `${Math.round(count / 1000000)}M`;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
function sanitizeStatusText(text: string): string {
|
|
26
|
+
return text.replace(/[\r\n\t]/g, " ").replace(/ +/g, " ").trim();
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
function formatPath(cwd: string): string {
|
|
30
|
+
const home = homedir();
|
|
31
|
+
return cwd.startsWith(home) ? `~${cwd.slice(home.length)}` : cwd;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
function toThinkingLevel(ctx: ExtensionContext | undefined): string {
|
|
35
|
+
if (!ctx) return "off";
|
|
36
|
+
const entries = ctx.sessionManager.getEntries();
|
|
37
|
+
for (let i = entries.length - 1; i >= 0; i--) {
|
|
38
|
+
const entry = entries[i] as any;
|
|
39
|
+
if (entry?.type === "thinking_level_change" && typeof entry.thinkingLevel === "string") {
|
|
40
|
+
return entry.thinkingLevel;
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
return "off";
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
function toContextUsagePart(ctx: ExtensionContext | undefined, theme: Theme): string {
|
|
47
|
+
const usage = ctx?.getContextUsage();
|
|
48
|
+
const contextWindow = usage?.contextWindow ?? 0;
|
|
49
|
+
const percentValue = usage?.percent ?? null;
|
|
50
|
+
const percentText = percentValue === null ? "?" : percentValue.toFixed(1);
|
|
51
|
+
const display = `${percentText}%/${formatTokens(contextWindow)} (auto)`;
|
|
52
|
+
if (percentValue !== null && percentValue > 90) return theme.fg("error", display);
|
|
53
|
+
if (percentValue !== null && percentValue > 70) return theme.fg("warning", display);
|
|
54
|
+
return display;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
function renderStatsLine(width: number, theme: Theme): string {
|
|
58
|
+
const ctx = footerCtx;
|
|
59
|
+
const tracker = footerTracker;
|
|
60
|
+
|
|
61
|
+
const inputTokens = tracker?.getTotalInputTokens() ?? 0;
|
|
62
|
+
const outputTokens = tracker?.getTotalOutputTokens() ?? 0;
|
|
63
|
+
const cacheRate = tracker?.getCacheHitRate() ?? 0;
|
|
64
|
+
const totalCost = tracker?.getTotalCost() ?? 0;
|
|
65
|
+
|
|
66
|
+
const leftParts: string[] = [`↑${formatTokens(inputTokens)}`, `↓${formatTokens(outputTokens)}`];
|
|
67
|
+
if (cacheRate > 0) leftParts.push(`⚡${Math.round(cacheRate * 100)}%`);
|
|
68
|
+
if (totalCost > 0) leftParts.push(`$${totalCost.toFixed(2)}`);
|
|
69
|
+
leftParts.push(toContextUsagePart(ctx, theme));
|
|
70
|
+
let left = leftParts.join(" ");
|
|
71
|
+
|
|
72
|
+
const modelId = ctx?.model?.id ?? "no-model";
|
|
73
|
+
const provider = ctx?.model?.provider ?? "unknown";
|
|
74
|
+
const thinkingLevel = toThinkingLevel(ctx);
|
|
75
|
+
let right = `(${provider}) ${modelId} • ${thinkingLevel}`;
|
|
76
|
+
|
|
77
|
+
let leftWidth = visibleWidth(left);
|
|
78
|
+
if (leftWidth > width) {
|
|
79
|
+
left = truncateToWidth(left, width, "...");
|
|
80
|
+
leftWidth = visibleWidth(left);
|
|
81
|
+
right = "";
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
const minPadding = 2;
|
|
85
|
+
const rightWidth = visibleWidth(right);
|
|
86
|
+
const totalNeeded = leftWidth + minPadding + rightWidth;
|
|
87
|
+
let fullLine: string;
|
|
88
|
+
|
|
89
|
+
if (right && totalNeeded <= width) {
|
|
90
|
+
const padding = " ".repeat(width - leftWidth - rightWidth);
|
|
91
|
+
fullLine = left + padding + right;
|
|
92
|
+
} else if (right) {
|
|
93
|
+
const availableRight = width - leftWidth - minPadding;
|
|
94
|
+
if (availableRight > 0) {
|
|
95
|
+
const truncatedRight = truncateToWidth(right, availableRight, "");
|
|
96
|
+
const truncatedRightWidth = visibleWidth(truncatedRight);
|
|
97
|
+
const padding = " ".repeat(Math.max(0, width - leftWidth - truncatedRightWidth));
|
|
98
|
+
fullLine = left + padding + truncatedRight;
|
|
99
|
+
} else {
|
|
100
|
+
fullLine = left;
|
|
101
|
+
}
|
|
102
|
+
} else {
|
|
103
|
+
fullLine = left;
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
const remainder = fullLine.slice(left.length);
|
|
107
|
+
return theme.fg("dim", left) + theme.fg("dim", remainder);
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
function renderPathLine(width: number, theme: Theme, footerData: ReadonlyFooterDataProvider): string {
|
|
111
|
+
const ctx = footerCtx;
|
|
112
|
+
const path = formatPath(ctx?.cwd ?? process.cwd());
|
|
113
|
+
const branch = footerData.getGitBranch();
|
|
114
|
+
const sessionName = ctx?.sessionManager.getSessionName();
|
|
115
|
+
|
|
116
|
+
let line = path;
|
|
117
|
+
if (branch) line += ` (${branch})`;
|
|
118
|
+
if (sessionName) line += ` • ${sessionName}`;
|
|
119
|
+
|
|
120
|
+
return truncateToWidth(theme.fg("dim", line), width, theme.fg("dim", "..."));
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
function renderStatusLine(width: number, theme: Theme, footerData: ReadonlyFooterDataProvider): string | undefined {
|
|
124
|
+
const statuses = footerData.getExtensionStatuses();
|
|
125
|
+
if (statuses.size === 0) return undefined;
|
|
126
|
+
|
|
127
|
+
const line = Array.from(statuses.entries())
|
|
128
|
+
.sort(([a], [b]) => a.localeCompare(b))
|
|
129
|
+
.map(([, text]) => sanitizeStatusText(text))
|
|
130
|
+
.join(" ");
|
|
131
|
+
|
|
132
|
+
return truncateToWidth(theme.fg("dim", line), width, theme.fg("dim", "..."));
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
export function createCustomFooter(_tui: TUI, theme: Theme, footerData: ReadonlyFooterDataProvider): Component & { dispose?(): void } {
|
|
136
|
+
return {
|
|
137
|
+
render(width: number): string[] {
|
|
138
|
+
const line1 = renderPathLine(width, theme, footerData);
|
|
139
|
+
const line2 = renderStatsLine(width, theme);
|
|
140
|
+
const line3 = renderStatusLine(width, theme, footerData) ?? theme.fg("dim", "");
|
|
141
|
+
return [line1, line2, line3];
|
|
142
|
+
},
|
|
143
|
+
invalidate(): void {},
|
|
144
|
+
dispose(): void {},
|
|
145
|
+
};
|
|
146
|
+
}
|
|
@@ -34,6 +34,7 @@ function makeConfig() {
|
|
|
34
34
|
implement: { model: "a/b", thinking: "high" },
|
|
35
35
|
debug: { model: "a/b", thinking: "high" },
|
|
36
36
|
brainstorm: { model: "a/b", thinking: "high" },
|
|
37
|
+
review: { model: "a/b", thinking: "high" },
|
|
37
38
|
},
|
|
38
39
|
planners: {},
|
|
39
40
|
planReviewers: {},
|