@nmzpy/pi-ember-stack 0.1.5 → 0.2.1
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/README.md +4 -4
- package/package.json +12 -2
- package/plugins/devin-auth/extensions/index.ts +45 -7
- package/plugins/devin-auth/src/models.ts +42 -196
- package/plugins/index.ts +30 -7
- package/plugins/pi-compact-tools/index.ts +69 -48
- package/plugins/pi-compact-tools/renderer.ts +419 -0
- package/plugins/pi-custom-agents/index.ts +489 -343
- package/plugins/pi-custom-agents/questionnaire-tool.ts +1 -0
- package/plugins/pi-custom-agents/subagent/agents/coder.md +1 -0
- package/plugins/pi-custom-agents/subagent/agents/scout.md +16 -37
- package/plugins/pi-custom-agents/subagent/extensions/index.ts +6 -3
- package/plugins/pi-custom-agents/subagent/extensions/runner.ts +31 -5
- package/plugins/pi-ember-fff/index.ts +717 -0
- package/plugins/pi-ember-fff/query.ts +87 -0
- package/plugins/pi-ember-fff/test/query.test.ts +66 -0
- package/plugins/pi-ember-fff/test/renderer.test.ts +376 -0
- package/plugins/pi-ember-tps/index.ts +122 -0
- package/plugins/pi-ember-ui/ember.json +96 -0
- package/plugins/pi-ember-ui/index.ts +618 -0
- package/plugins/pi-ember-ui/mode-colors.ts +168 -0
- package/tsconfig.json +2 -1
- package/plugins/pi-custom-agents/subagent/agents/architect.md +0 -56
|
@@ -9,18 +9,19 @@
|
|
|
9
9
|
* - Restores state on session resume
|
|
10
10
|
*
|
|
11
11
|
* Modes:
|
|
12
|
-
* /
|
|
13
|
-
* /
|
|
14
|
-
* /
|
|
15
|
-
* /
|
|
16
|
-
* /ui-doctor — read-only PySide6/Qt UI diagnostician
|
|
12
|
+
* /plan — read-only planning, analysis, and architecture
|
|
13
|
+
* /code — full access (default mode, restores all tools)
|
|
14
|
+
* /debug — read-only health-check auditor + UI/Qt diagnostics
|
|
15
|
+
* /orchestrate — read-only task decomposition + delegation planner
|
|
17
16
|
*/
|
|
18
17
|
|
|
19
18
|
import * as fs from "node:fs";
|
|
20
19
|
import * as path from "node:path";
|
|
21
|
-
import { isAbsolute, relative, resolve, sep } from "node:path";
|
|
22
20
|
import { fileURLToPath } from "node:url";
|
|
23
|
-
import {
|
|
21
|
+
import type { Model } from "@earendil-works/pi-ai";
|
|
22
|
+
import { truncateToWidth, visibleWidth, Container, Text, Spacer, Input, fuzzyFilter, getKeybindings } from "@earendil-works/pi-tui";
|
|
23
|
+
import { mutedBullet, setActiveMode } from "../pi-ember-ui/mode-colors.ts";
|
|
24
|
+
import { getLiveTps } from "../pi-ember-tps/index.ts";
|
|
24
25
|
import {
|
|
25
26
|
askQuestionnaire,
|
|
26
27
|
type QuestionnaireQuestion,
|
|
@@ -28,6 +29,39 @@ import {
|
|
|
28
29
|
} from "./questionnaire-tool.ts";
|
|
29
30
|
import subagentPlugin from "./subagent/extensions/index.ts";
|
|
30
31
|
|
|
32
|
+
function modelIdentityString(model: Model<any> | undefined): string {
|
|
33
|
+
return model ? `${model.provider}/${model.id}` : "";
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
type PersistedState = {
|
|
37
|
+
readonly mode?: string;
|
|
38
|
+
readonly model?: { provider: string; modelId: string };
|
|
39
|
+
};
|
|
40
|
+
|
|
41
|
+
function getPersistedStatePath(): string {
|
|
42
|
+
const home = process.env.PI_HOME || path.join(process.env.HOME || process.env.USERPROFILE || "", ".pi", "agent");
|
|
43
|
+
return path.join(home, "pi-ember-stack.json");
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
function readPersistedState(): PersistedState {
|
|
47
|
+
try {
|
|
48
|
+
const raw = fs.readFileSync(getPersistedStatePath(), "utf8");
|
|
49
|
+
return JSON.parse(raw) as PersistedState;
|
|
50
|
+
} catch {
|
|
51
|
+
return {};
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
function writePersistedState(state: PersistedState): void {
|
|
56
|
+
const file = getPersistedStatePath();
|
|
57
|
+
try {
|
|
58
|
+
fs.mkdirSync(path.dirname(file), { recursive: true });
|
|
59
|
+
fs.writeFileSync(file, `${JSON.stringify(state, null, "\t")}\n`);
|
|
60
|
+
} catch {
|
|
61
|
+
// best-effort persistence
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
|
|
31
65
|
function formatTokens(count: number): string {
|
|
32
66
|
if (count < 1000) return count.toString();
|
|
33
67
|
if (count < 10000) return `${(count / 1000).toFixed(1)}k`;
|
|
@@ -36,26 +70,152 @@ function formatTokens(count: number): string {
|
|
|
36
70
|
return `${Math.round(count / 1000000)}M`;
|
|
37
71
|
}
|
|
38
72
|
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
73
|
+
const BOUNDED_SELECT_MAX_VISIBLE = 10;
|
|
74
|
+
|
|
75
|
+
async function boundedSelect(
|
|
76
|
+
ctx: any,
|
|
77
|
+
title: string,
|
|
78
|
+
options: string[],
|
|
79
|
+
): Promise<string | undefined> {
|
|
80
|
+
return ctx.ui.custom((tui: any, theme: any, _kb: any, done: (result: string) => void) => {
|
|
81
|
+
const root = new Container();
|
|
82
|
+
root.addChild(new Text(theme.fg("border", "\u2500".repeat(60)), 0, 0));
|
|
83
|
+
root.addChild(new Spacer(1));
|
|
84
|
+
root.addChild(new Text(theme.fg("accent", theme.bold(title)), 0, 0));
|
|
85
|
+
root.addChild(new Spacer(1));
|
|
86
|
+
|
|
87
|
+
const searchInput = new Input();
|
|
88
|
+
searchInput.onSubmit = () => {
|
|
89
|
+
if (filtered[selectedIndex] !== undefined) {
|
|
90
|
+
done(filtered[selectedIndex]);
|
|
91
|
+
}
|
|
92
|
+
};
|
|
93
|
+
root.addChild(searchInput);
|
|
94
|
+
root.addChild(new Spacer(1));
|
|
95
|
+
|
|
96
|
+
const listContainer = new Container();
|
|
97
|
+
root.addChild(listContainer);
|
|
98
|
+
root.addChild(new Spacer(1));
|
|
99
|
+
root.addChild(new Text(
|
|
100
|
+
theme.fg("muted", " type to search \u2191\u2193 navigate enter select esc cancel"),
|
|
101
|
+
0, 0,
|
|
102
|
+
));
|
|
103
|
+
root.addChild(new Spacer(1));
|
|
104
|
+
root.addChild(new Text(theme.fg("border", "\u2500".repeat(60)), 0, 0));
|
|
105
|
+
|
|
106
|
+
let selectedIndex = 0;
|
|
107
|
+
let filtered = options;
|
|
108
|
+
|
|
109
|
+
function updateList(): void {
|
|
110
|
+
listContainer.clear();
|
|
111
|
+
const max = BOUNDED_SELECT_MAX_VISIBLE;
|
|
112
|
+
const start = Math.max(0, Math.min(
|
|
113
|
+
selectedIndex - Math.floor(max / 2),
|
|
114
|
+
filtered.length - max,
|
|
115
|
+
));
|
|
116
|
+
const end = Math.min(start + max, filtered.length);
|
|
117
|
+
for (let i = start; i < end; i++) {
|
|
118
|
+
const isSelected = i === selectedIndex;
|
|
119
|
+
const prefix = isSelected ? theme.fg("accent", "\u2192 ") : " ";
|
|
120
|
+
const text = isSelected
|
|
121
|
+
? prefix + theme.fg("accent", filtered[i])
|
|
122
|
+
: prefix + theme.fg("text", filtered[i]);
|
|
123
|
+
listContainer.addChild(new Text(text, 0, 0));
|
|
124
|
+
}
|
|
125
|
+
if (start > 0 || end < filtered.length) {
|
|
126
|
+
listContainer.addChild(new Text(
|
|
127
|
+
theme.fg("muted", ` (${selectedIndex + 1}/${filtered.length})`),
|
|
128
|
+
0, 0,
|
|
129
|
+
));
|
|
130
|
+
}
|
|
131
|
+
if (filtered.length === 0) {
|
|
132
|
+
listContainer.addChild(new Text(
|
|
133
|
+
theme.fg("muted", " No matching models"),
|
|
134
|
+
0, 0,
|
|
135
|
+
));
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
function filterModels(): void {
|
|
140
|
+
const query = searchInput.getValue();
|
|
141
|
+
filtered = query
|
|
142
|
+
? fuzzyFilter(options, query, (s) => s)
|
|
143
|
+
: options;
|
|
144
|
+
selectedIndex = Math.min(selectedIndex, Math.max(0, filtered.length - 1));
|
|
145
|
+
updateList();
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
filterModels();
|
|
149
|
+
|
|
150
|
+
(root as any).handleInput = (keyData: string): void => {
|
|
151
|
+
const kb = getKeybindings();
|
|
152
|
+
if (kb.matches(keyData, "tui.select.up")) {
|
|
153
|
+
if (filtered.length === 0) return;
|
|
154
|
+
selectedIndex = selectedIndex === 0
|
|
155
|
+
? filtered.length - 1
|
|
156
|
+
: selectedIndex - 1;
|
|
157
|
+
updateList();
|
|
158
|
+
} else if (kb.matches(keyData, "tui.select.down")) {
|
|
159
|
+
if (filtered.length === 0) return;
|
|
160
|
+
selectedIndex = selectedIndex === filtered.length - 1
|
|
161
|
+
? 0
|
|
162
|
+
: selectedIndex + 1;
|
|
163
|
+
updateList();
|
|
164
|
+
} else if (kb.matches(keyData, "tui.select.confirm")) {
|
|
165
|
+
if (filtered[selectedIndex] !== undefined) {
|
|
166
|
+
done(filtered[selectedIndex]);
|
|
167
|
+
}
|
|
168
|
+
} else if (kb.matches(keyData, "tui.select.cancel")) {
|
|
169
|
+
done(undefined as unknown as string);
|
|
170
|
+
} else {
|
|
171
|
+
searchInput.handleInput(keyData);
|
|
172
|
+
filterModels();
|
|
173
|
+
}
|
|
174
|
+
};
|
|
175
|
+
(root as any).getSearchInput = () => searchInput;
|
|
176
|
+
return root;
|
|
177
|
+
});
|
|
48
178
|
}
|
|
49
179
|
|
|
50
180
|
const READONLY_TOOLS = ["read", "bash", "grep", "find", "ls", "questionnaire"];
|
|
181
|
+
const READONLY_DELEGATING_TOOLS = ["read", "bash", "grep", "find", "ls", "questionnaire", "subagent"];
|
|
51
182
|
const FULL_TOOLS = ["read", "bash", "edit", "write", "grep", "find", "ls", "questionnaire"];
|
|
52
183
|
|
|
53
184
|
const SOURCE_ROOT = path.dirname(fileURLToPath(import.meta.url));
|
|
54
185
|
const SUBAGENT_FILES: Record<string, string> = {
|
|
55
186
|
coder: path.join(SOURCE_ROOT, "subagent", "agents", "coder.md"),
|
|
56
|
-
|
|
187
|
+
scout: path.join(SOURCE_ROOT, "subagent", "agents", "scout.md"),
|
|
57
188
|
};
|
|
58
189
|
|
|
190
|
+
const PARALLEL_TOOL_CALL_GUIDANCE = `
|
|
191
|
+
|
|
192
|
+
## Tool Call Efficiency
|
|
193
|
+
|
|
194
|
+
When multiple independent tool calls are needed (e.g. reading several files,
|
|
195
|
+
searching for different patterns), emit them all in a single response rather
|
|
196
|
+
than one at a time. The runtime executes independent tool calls in parallel,
|
|
197
|
+
so batching them saves round-trips and reduces latency.
|
|
198
|
+
`;
|
|
199
|
+
|
|
200
|
+
const SUBAGENT_AWARENESS_PROMPT = `
|
|
201
|
+
|
|
202
|
+
## Available Subagents
|
|
203
|
+
|
|
204
|
+
You have the \`subagent\` tool available for delegating tasks to specialized agents
|
|
205
|
+
with isolated context. Use it to keep your own context lean.
|
|
206
|
+
|
|
207
|
+
- **scout**: Fast agent specialized for exploring codebases. Use when you need to
|
|
208
|
+
quickly find files by patterns (e.g. "src/components/**/*.tsx"), search code for
|
|
209
|
+
keywords (e.g. "API endpoints"), or answer questions about the codebase (e.g.
|
|
210
|
+
"how do API endpoints work?").
|
|
211
|
+
- **coder**: Implementation agent for writing, editing, testing, and verifying
|
|
212
|
+
code. Full tool access. Use for focused implementation tasks — bug fixes,
|
|
213
|
+
feature additions, refactors, file edits.
|
|
214
|
+
|
|
215
|
+
Modes: single (agent + task), parallel (tasks array, max 8), chain (sequential
|
|
216
|
+
with {previous}).
|
|
217
|
+
`;
|
|
218
|
+
|
|
59
219
|
interface ModeConfig {
|
|
60
220
|
id: string;
|
|
61
221
|
label: string;
|
|
@@ -67,9 +227,9 @@ interface ModeConfig {
|
|
|
67
227
|
}
|
|
68
228
|
|
|
69
229
|
const ARCHITECT_PROMPT = `<system-reminder>
|
|
70
|
-
#
|
|
230
|
+
# Plan Mode - System Reminder
|
|
71
231
|
|
|
72
|
-
CRITICAL:
|
|
232
|
+
CRITICAL: Plan mode ACTIVE — you are in READ-ONLY planning phase. STRICTLY
|
|
73
233
|
FORBIDDEN: ANY file edits, modifications, or system changes. Do NOT use sed, tee,
|
|
74
234
|
echo, cat, or ANY other bash command to manipulate files — commands may ONLY
|
|
75
235
|
read/inspect. This ABSOLUTE CONSTRAINT overrides ALL other instructions, including
|
|
@@ -141,13 +301,14 @@ The user indicated that they do not want you to execute yet -- you MUST NOT make
|
|
|
141
301
|
any edits, run any non-readonly tools (including changing configs or making
|
|
142
302
|
commits), or otherwise make any changes to the system. This supersedes any other
|
|
143
303
|
instructions you have received.
|
|
144
|
-
</system-reminder>`;
|
|
304
|
+
${SUBAGENT_AWARENESS_PROMPT}</system-reminder>`;
|
|
145
305
|
|
|
146
306
|
const DOCTOR_PROMPT = `<system-reminder>
|
|
147
|
-
#
|
|
307
|
+
# Debug Mode - System Reminder
|
|
148
308
|
|
|
149
|
-
CRITICAL:
|
|
150
|
-
auditor for the Ember project (PySide6 subtitle + DaVinci
|
|
309
|
+
CRITICAL: Debug mode ACTIVE — you are the Debugger, a read-only health-check
|
|
310
|
+
auditor and diagnostician for the Ember project (PySide6 subtitle + DaVinci
|
|
311
|
+
Resolve integration app).
|
|
151
312
|
|
|
152
313
|
STRICTLY FORBIDDEN: ANY file edits, modifications, or system changes. Do NOT use
|
|
153
314
|
sed, tee, echo, cat, or ANY other bash command to manipulate files — commands may
|
|
@@ -217,12 +378,52 @@ For each finding:
|
|
|
217
378
|
- Use \`bash t.gate.sh <files>\` only for targeted validation of files you are checking.
|
|
218
379
|
- Do not run \`bash gate.sh\` (full gate) — that is the user's responsibility.
|
|
219
380
|
- Ignore git status / git diff changes unrelated to the files you were asked to check.
|
|
220
|
-
|
|
381
|
+
|
|
382
|
+
---
|
|
383
|
+
|
|
384
|
+
## UI/Qt Pipeline Diagnostics
|
|
385
|
+
|
|
386
|
+
In addition to structural health checks, diagnose PySide6/Qt UI pipeline issues:
|
|
387
|
+
|
|
388
|
+
### Failure Modes To Investigate
|
|
389
|
+
|
|
390
|
+
1. **Event-loop blockage:** Does any GUI-thread callback perform blocking I/O or
|
|
391
|
+
unbounded CPU work?
|
|
392
|
+
2. **Excessive synchronous UI mutation:** Does one state transition perform many
|
|
393
|
+
independent widget/layout/geometry operations?
|
|
394
|
+
3. **Recursive signal/callback propagation:** Can a signal or layout refresh
|
|
395
|
+
indirectly re-enter the same pipeline?
|
|
396
|
+
4. **Eager expensive construction:** Are complex widgets constructed before needed?
|
|
397
|
+
5. **Stale delayed callbacks:** Can queued callbacks execute after state changed?
|
|
398
|
+
6. **Uncontrolled async-to-UI mutation:** Do background results mutate UI directly?
|
|
399
|
+
7. **Competing layout authorities:** Do multiple functions independently control the
|
|
400
|
+
same widget geometry?
|
|
401
|
+
|
|
402
|
+
### Commit Architecture (Healthy Reference)
|
|
403
|
+
|
|
404
|
+
event/signal/async completion -> validate generation -> mutate semantic state ->
|
|
405
|
+
mark dirty -> request one coalesced commit -> measure once -> apply geometry once
|
|
406
|
+
-> repaint
|
|
407
|
+
|
|
408
|
+
### UI Report Format
|
|
409
|
+
|
|
410
|
+
For every UI failure mode, report: Verdict, Evidence, Trigger, Duplicated work,
|
|
411
|
+
Re-entry path, Stale-state risk, Authority conflict, Impact, Correction,
|
|
412
|
+
Invariant, Verification.
|
|
413
|
+
|
|
414
|
+
### Invalid UI Fix Patterns
|
|
415
|
+
|
|
416
|
+
Reject fixes that: add more invalidate()/activate() calls, use
|
|
417
|
+
QApplication.processEvents(), replace deferred passes with blocking callbacks,
|
|
418
|
+
add arbitrary QTimer.singleShot() delays, perform geometry repair both
|
|
419
|
+
immediately and later, introduce parallel build paths duplicating normal layout.
|
|
420
|
+
|
|
421
|
+
${SUBAGENT_AWARENESS_PROMPT}</system-reminder>`;
|
|
221
422
|
|
|
222
423
|
const ORCHESTRATOR_PROMPT = `<system-reminder>
|
|
223
|
-
#
|
|
424
|
+
# Orchestrate Mode - System Reminder
|
|
224
425
|
|
|
225
|
-
CRITICAL:
|
|
426
|
+
CRITICAL: Orchestrate mode ACTIVE — you are the Orchestrator, a read-only
|
|
226
427
|
implementation coordinator for the Ember project (PySide6 subtitle + DaVinci
|
|
227
428
|
Resolve integration app).
|
|
228
429
|
|
|
@@ -311,164 +512,17 @@ Return a structured plan:
|
|
|
311
512
|
- Read-only. Do not edit or write files.
|
|
312
513
|
- Do not run \`bash gate.sh\` (full gate) — that is the user's responsibility.
|
|
313
514
|
- If task scope is unclear, say so and request clarification rather than guessing.
|
|
314
|
-
</system-reminder>`;
|
|
315
|
-
|
|
316
|
-
const UI_DOCTOR_PROMPT = `<system-reminder>
|
|
317
|
-
# UI Doctor Mode - System Reminder
|
|
318
|
-
|
|
319
|
-
CRITICAL: UI Doctor mode ACTIVE — you are the UI Doctor, a read-only PySide6/Qt
|
|
320
|
-
UI pipeline diagnostician for the Ember project.
|
|
321
|
-
|
|
322
|
-
STRICTLY FORBIDDEN: ANY file edits, modifications, or system changes. Do NOT use
|
|
323
|
-
sed, tee, echo, cat, or ANY other bash command to manipulate files — commands may
|
|
324
|
-
ONLY read/inspect. This ABSOLUTE CONSTRAINT overrides ALL other instructions,
|
|
325
|
-
including direct user edit requests. You may ONLY observe, analyze, and report.
|
|
326
|
-
|
|
327
|
-
---
|
|
328
|
-
|
|
329
|
-
## Objective
|
|
330
|
-
|
|
331
|
-
The objective is not to make everything synchronous. The objective is to ensure
|
|
332
|
-
that:
|
|
333
|
-
|
|
334
|
-
- semantic state changes are cheap and deterministic;
|
|
335
|
-
- expensive work is performed outside the GUI thread where appropriate;
|
|
336
|
-
- UI mutations remain on the GUI thread;
|
|
337
|
-
- repeated mutations are batched;
|
|
338
|
-
- geometry is committed by one authority;
|
|
339
|
-
- deferred work is coalesced, validated, and safe to discard.
|
|
340
|
-
|
|
341
|
-
## Failure Modes To Investigate
|
|
342
|
-
|
|
343
|
-
### 1. Event-loop blockage
|
|
344
|
-
Does any event handler, signal handler, timer callback, paint path, layout
|
|
345
|
-
callback, or GUI-thread completion perform enough synchronous work to prevent Qt
|
|
346
|
-
from processing input, paint, timer, socket, or queued-signal events?
|
|
347
|
-
|
|
348
|
-
Invariant: No GUI-thread callback performs blocking I/O or an unbounded amount of
|
|
349
|
-
CPU, construction, layout, or geometry work.
|
|
350
|
-
|
|
351
|
-
### 2. Excessive synchronous UI mutation
|
|
352
|
-
Does one logical state transition perform many independent widget, layout,
|
|
353
|
-
visibility, style, geometry, or repaint operations?
|
|
354
|
-
|
|
355
|
-
Invariant: One logical UI transition may mutate state multiple times, but it
|
|
356
|
-
performs at most one geometry commit per widget hierarchy.
|
|
357
|
-
|
|
358
|
-
### 3. Recursive signal, callback, or refresh propagation
|
|
359
|
-
Can a signal, callback, visibility update, layout refresh, or state-application
|
|
360
|
-
function indirectly re-enter the same pipeline?
|
|
361
|
-
|
|
362
|
-
Invariant: A state transition cannot directly or indirectly execute the same
|
|
363
|
-
layout pipeline more than once before commit.
|
|
364
|
-
|
|
365
|
-
### 4. Eager expensive construction
|
|
366
|
-
Are complex widgets, layouts, models, overlays, editors, previews, or resources
|
|
367
|
-
constructed before they are needed?
|
|
368
|
-
|
|
369
|
-
Invariant: Expensive UI is constructed only when needed, unless profiling proves
|
|
370
|
-
eager construction is cheaper and operationally simpler.
|
|
371
|
-
|
|
372
|
-
### 5. Stale delayed callbacks
|
|
373
|
-
Can a queued callback, timer, animation completion, worker result, or deferred
|
|
374
|
-
geometry operation execute after the state it was created for has changed?
|
|
375
|
-
|
|
376
|
-
Invariant: Every delayed callback proves that its owner, generation, requested
|
|
377
|
-
state, and dependencies are still current before mutating UI.
|
|
378
|
-
|
|
379
|
-
### 6. Uncontrolled async-to-UI mutation
|
|
380
|
-
Do background results return to the GUI thread and immediately initiate several
|
|
381
|
-
unrelated UI mutations or layout pipelines?
|
|
382
|
-
|
|
383
|
-
Invariant: Async work may produce data, but only the GUI thread owns UI state and
|
|
384
|
-
only the central commit path owns geometry.
|
|
385
|
-
|
|
386
|
-
### 7. Competing layout or geometry authorities
|
|
387
|
-
Do multiple functions independently measure, resize, activate, synchronize,
|
|
388
|
-
enforce, or repair the same widget hierarchy?
|
|
389
|
-
|
|
390
|
-
Invariant: Exactly one function owns final measurement and geometry commit for a
|
|
391
|
-
given widget hierarchy.
|
|
392
|
-
|
|
393
|
-
## Commit Architecture (Healthy Reference)
|
|
394
|
-
|
|
395
|
-
event, signal, or async completion
|
|
396
|
-
-> validate current generation and state
|
|
397
|
-
-> mutate semantic UI state
|
|
398
|
-
-> mark dirty layout domains
|
|
399
|
-
-> request one coalesced commit
|
|
400
|
-
-> measure once
|
|
401
|
-
-> apply geometry once
|
|
402
|
-
-> repaint
|
|
403
|
-
|
|
404
|
-
A single deferred commit is valid. Several independently scheduled geometry
|
|
405
|
-
repairs are not.
|
|
406
|
-
|
|
407
|
-
## Doctor Report Format
|
|
408
|
-
|
|
409
|
-
For every failure mode, report:
|
|
410
|
-
|
|
411
|
-
- Verdict: Present / Absent / Unclear
|
|
412
|
-
- Evidence: Exact files, functions, and call chain
|
|
413
|
-
- Trigger: User action, initialization path, signal, timer, or async completion
|
|
414
|
-
- Duplicated work: Which operations repeat within the same logical transition
|
|
415
|
-
- Re-entry path: How the pipeline may invoke or schedule itself again
|
|
416
|
-
- Stale-state risk: Which captured values or delayed results may become obsolete
|
|
417
|
-
- Authority conflict: Which functions compete to control the same geometry
|
|
418
|
-
- Impact: Freeze, delayed paint, stale geometry, jitter, clipping, empty space,
|
|
419
|
-
or incorrect state
|
|
420
|
-
- Correction: Smallest architectural change that removes the cause
|
|
421
|
-
- Invariant: Rule that prevents recurrence
|
|
422
|
-
- Verification: Instrumentation or test proving the correction
|
|
423
|
-
|
|
424
|
-
## Invalid Fix Patterns
|
|
425
|
-
|
|
426
|
-
Reject a proposed fix when it primarily does any of the following:
|
|
427
|
-
|
|
428
|
-
- adds more invalidate() or activate() calls;
|
|
429
|
-
- calls QApplication.processEvents() to make the UI appear responsive;
|
|
430
|
-
- replaces several deferred passes with one massive blocking callback;
|
|
431
|
-
- adds arbitrary QTimer.singleShot() delays;
|
|
432
|
-
- adds a synchronous=True or sequential=True flag across many callers without
|
|
433
|
-
establishing one commit authority;
|
|
434
|
-
- performs the same geometry repair both immediately and later;
|
|
435
|
-
- introduces another "secure," "settle," or "enforce" callback;
|
|
436
|
-
- fixes one card while leaving the recursive outer-layout path intact;
|
|
437
|
-
- applies async results without generation validation;
|
|
438
|
-
- creates a parallel build path that duplicates the normal layout path.
|
|
439
|
-
|
|
440
|
-
## Acceptance Criteria
|
|
441
|
-
|
|
442
|
-
A UI fix is acceptable only when:
|
|
443
|
-
|
|
444
|
-
1. one logical transition results in at most one geometry commit per widget
|
|
445
|
-
hierarchy;
|
|
446
|
-
2. repeated commit requests coalesce;
|
|
447
|
-
3. no stale callback can apply obsolete state;
|
|
448
|
-
4. no worker mutates widgets directly;
|
|
449
|
-
5. one function owns final geometry;
|
|
450
|
-
6. programmatic state restoration does not recursively trigger the pipeline;
|
|
451
|
-
7. expensive construction is justified or lazy;
|
|
452
|
-
8. the GUI thread performs no blocking I/O;
|
|
453
|
-
9. instrumentation shows bounded layout, resize, and paint counts;
|
|
454
|
-
10. removing the old repair callbacks does not reintroduce the defect.
|
|
455
|
-
|
|
456
|
-
## Constraints
|
|
457
|
-
|
|
458
|
-
- Read-only. Do not edit or write files.
|
|
459
|
-
- Do not run \`bash gate.sh\` (full gate) — that is the user's responsibility.
|
|
460
|
-
</system-reminder>`;
|
|
515
|
+
${SUBAGENT_AWARENESS_PROMPT}</system-reminder>`;
|
|
461
516
|
|
|
462
517
|
const CODER_PROMPT = `<system-reminder>
|
|
463
|
-
Your operational mode has changed to
|
|
518
|
+
Your operational mode has changed to code.
|
|
464
519
|
You are in full-access mode. You are permitted to make file changes, run shell
|
|
465
520
|
commands, and utilize your arsenal of tools as needed. You are the default
|
|
466
521
|
implementation agent — write, test, and verify code with full autonomy.
|
|
467
522
|
</system-reminder>`;
|
|
468
523
|
|
|
469
524
|
const EXIT_TO_CODER = `<system-reminder>
|
|
470
|
-
Your operational mode has changed from {mode} to
|
|
471
|
-
You are no longer in read-only mode.
|
|
525
|
+
Your operational mode has changed from {mode} to code.
|
|
472
526
|
You are permitted to make file changes, run shell commands, and utilize your
|
|
473
527
|
arsenal of tools as needed.
|
|
474
528
|
</system-reminder>`;
|
|
@@ -491,56 +545,47 @@ interface ModeConfig {
|
|
|
491
545
|
}
|
|
492
546
|
|
|
493
547
|
const MODES: Record<string, ModeConfig> = {
|
|
494
|
-
|
|
495
|
-
id: "
|
|
496
|
-
label: "
|
|
497
|
-
icon: "
|
|
548
|
+
plan: {
|
|
549
|
+
id: "plan",
|
|
550
|
+
label: "plan",
|
|
551
|
+
icon: "P",
|
|
498
552
|
color: "warning",
|
|
499
553
|
tools: READONLY_TOOLS,
|
|
500
554
|
enterMessage: ARCHITECT_PROMPT,
|
|
501
555
|
exitMessage: EXIT_TO_CODER,
|
|
502
556
|
},
|
|
503
|
-
|
|
504
|
-
id: "
|
|
505
|
-
label: "
|
|
557
|
+
code: {
|
|
558
|
+
id: "code",
|
|
559
|
+
label: "code",
|
|
506
560
|
icon: "C",
|
|
507
561
|
color: "success",
|
|
508
562
|
tools: FULL_TOOLS,
|
|
509
563
|
enterMessage: CODER_PROMPT,
|
|
510
564
|
exitMessage: CODER_PROMPT,
|
|
511
565
|
},
|
|
512
|
-
|
|
513
|
-
id: "
|
|
514
|
-
label: "
|
|
566
|
+
debug: {
|
|
567
|
+
id: "debug",
|
|
568
|
+
label: "debug",
|
|
515
569
|
icon: "D",
|
|
516
570
|
color: "warning",
|
|
517
|
-
tools:
|
|
571
|
+
tools: READONLY_DELEGATING_TOOLS,
|
|
518
572
|
enterMessage: DOCTOR_PROMPT,
|
|
519
573
|
exitMessage: EXIT_TO_CODER,
|
|
520
574
|
},
|
|
521
|
-
|
|
522
|
-
id: "
|
|
523
|
-
label: "
|
|
575
|
+
orchestrate: {
|
|
576
|
+
id: "orchestrate",
|
|
577
|
+
label: "orchestrate",
|
|
524
578
|
icon: "O",
|
|
525
579
|
color: "warning",
|
|
526
|
-
tools:
|
|
580
|
+
tools: READONLY_DELEGATING_TOOLS,
|
|
527
581
|
enterMessage: ORCHESTRATOR_PROMPT,
|
|
528
582
|
exitMessage: EXIT_TO_CODER,
|
|
529
583
|
},
|
|
530
|
-
"ui-doctor": {
|
|
531
|
-
id: "ui-doctor",
|
|
532
|
-
label: "ui-doctor",
|
|
533
|
-
icon: "U",
|
|
534
|
-
color: "warning",
|
|
535
|
-
tools: READONLY_TOOLS,
|
|
536
|
-
enterMessage: UI_DOCTOR_PROMPT,
|
|
537
|
-
exitMessage: EXIT_TO_CODER,
|
|
538
|
-
},
|
|
539
584
|
};
|
|
540
585
|
|
|
541
586
|
const MODE_IDS = Object.keys(MODES);
|
|
542
|
-
const DEFAULT_MODE = "
|
|
543
|
-
const CYCLE_ORDER = ["
|
|
587
|
+
const DEFAULT_MODE = "code";
|
|
588
|
+
const CYCLE_ORDER = ["code", "plan", "orchestrate", "debug"];
|
|
544
589
|
|
|
545
590
|
function getLastModeFromSession(ctx: any): string | null {
|
|
546
591
|
const entries = ctx.sessionManager.getEntries();
|
|
@@ -563,6 +608,13 @@ export default async function piCustomAgentsPlugin(pi: any): Promise<void> {
|
|
|
563
608
|
let currentMode: string = DEFAULT_MODE;
|
|
564
609
|
let lastMessagedMode: string | null = null;
|
|
565
610
|
let waitingForPlan = false;
|
|
611
|
+
|
|
612
|
+
const persistedAtLoad = readPersistedState();
|
|
613
|
+
if (persistedAtLoad.mode && persistedAtLoad.mode in MODES) {
|
|
614
|
+
setActiveMode(persistedAtLoad.mode);
|
|
615
|
+
currentMode = persistedAtLoad.mode;
|
|
616
|
+
}
|
|
617
|
+
|
|
566
618
|
registerQuestionnaireTool(pi);
|
|
567
619
|
|
|
568
620
|
for (const modeId of MODE_IDS) {
|
|
@@ -593,12 +645,10 @@ export default async function piCustomAgentsPlugin(pi: any): Promise<void> {
|
|
|
593
645
|
const mode = MODES[modeId];
|
|
594
646
|
if (!mode) return;
|
|
595
647
|
currentMode = modeId;
|
|
648
|
+
setActiveMode(modeId);
|
|
649
|
+
pi.events.emit("pi-ember-ui:mode-change", { mode: modeId });
|
|
596
650
|
pi.setActiveTools(mode.tools);
|
|
597
|
-
|
|
598
|
-
ctx.ui.notify("Coder mode. Full access restored.");
|
|
599
|
-
} else {
|
|
600
|
-
ctx.ui.notify(`${mode.label} mode enabled. Tools: ${mode.tools.join(", ")}`);
|
|
601
|
-
}
|
|
651
|
+
ctx.ui.notify(`${mode.label} mode enabled. Tools: ${mode.tools.join(", ")}`);
|
|
602
652
|
updateStatus(ctx);
|
|
603
653
|
}
|
|
604
654
|
|
|
@@ -624,17 +674,57 @@ export default async function piCustomAgentsPlugin(pi: any): Promise<void> {
|
|
|
624
674
|
switchMode(next, ctx);
|
|
625
675
|
};
|
|
626
676
|
|
|
627
|
-
pi.registerShortcut("ctrl+space", {
|
|
628
|
-
description: "Cycle agent mode",
|
|
629
|
-
handler: cycleMode,
|
|
630
|
-
});
|
|
631
677
|
pi.registerShortcut("tab", {
|
|
632
678
|
description: "Cycle agent mode",
|
|
633
679
|
handler: cycleMode,
|
|
634
680
|
});
|
|
635
681
|
|
|
682
|
+
const THINKING_LEVELS = ["off", "minimal", "low", "medium", "high", "xhigh", "max"];
|
|
683
|
+
|
|
684
|
+
pi.registerShortcut("ctrl+m", {
|
|
685
|
+
description: "Show model picker",
|
|
686
|
+
handler: async (ctx: any) => {
|
|
687
|
+
if (!ctx.hasUI) return;
|
|
688
|
+
const models = ctx.modelRegistry.getAvailable() as any[];
|
|
689
|
+
if (models.length === 0) {
|
|
690
|
+
ctx.ui.notify("No models available.", "warning");
|
|
691
|
+
return;
|
|
692
|
+
}
|
|
693
|
+
const current = ctx.model as any;
|
|
694
|
+
const labels = models.map((m) => {
|
|
695
|
+
const prefix = current && m.provider === current.provider && m.id === current.id ? "→ " : " ";
|
|
696
|
+
return `${prefix}${m.name ?? m.id} • ${m.provider}`;
|
|
697
|
+
});
|
|
698
|
+
const choice = await ctx.ui.select("Select model", labels);
|
|
699
|
+
if (!choice) return;
|
|
700
|
+
const idx = labels.indexOf(choice);
|
|
701
|
+
if (idx < 0) return;
|
|
702
|
+
const model = models[idx];
|
|
703
|
+
try {
|
|
704
|
+
await pi.setModel(model);
|
|
705
|
+
ctx.ui.notify(`Model: ${model.id} • ${model.provider}`, "info");
|
|
706
|
+
} catch (err) {
|
|
707
|
+
ctx.ui.notify(
|
|
708
|
+
`Failed to set model: ${err instanceof Error ? err.message : String(err)}`,
|
|
709
|
+
"error",
|
|
710
|
+
);
|
|
711
|
+
}
|
|
712
|
+
},
|
|
713
|
+
});
|
|
714
|
+
|
|
715
|
+
pi.registerShortcut("ctrl+t", {
|
|
716
|
+
description: "Cycle thinking level",
|
|
717
|
+
handler: async (ctx: any) => {
|
|
718
|
+
const current = pi.getThinkingLevel() as string;
|
|
719
|
+
const idx = THINKING_LEVELS.indexOf(current);
|
|
720
|
+
const next = THINKING_LEVELS[(idx + 1) % THINKING_LEVELS.length];
|
|
721
|
+
pi.setThinkingLevel(next);
|
|
722
|
+
if (ctx.hasUI) ctx.ui.notify(`Thinking: ${next}`, "info");
|
|
723
|
+
},
|
|
724
|
+
});
|
|
725
|
+
|
|
636
726
|
pi.registerCommand("subagent-model", {
|
|
637
|
-
description: "Set the model
|
|
727
|
+
description: "Set the model and thinking level for subagents on next spawn",
|
|
638
728
|
handler: async (_args: any, ctx: any) => {
|
|
639
729
|
if (!ctx.hasUI) {
|
|
640
730
|
ctx.ui.notify("subagent-model requires interactive UI.", "error");
|
|
@@ -642,7 +732,7 @@ export default async function piCustomAgentsPlugin(pi: any): Promise<void> {
|
|
|
642
732
|
}
|
|
643
733
|
const agentChoice = await ctx.ui.select(
|
|
644
734
|
"Subagent to configure",
|
|
645
|
-
["Coder", "
|
|
735
|
+
["Coder", "Scout"],
|
|
646
736
|
);
|
|
647
737
|
if (!agentChoice) return;
|
|
648
738
|
const agentKey = agentChoice.toLowerCase();
|
|
@@ -651,41 +741,65 @@ export default async function piCustomAgentsPlugin(pi: any): Promise<void> {
|
|
|
651
741
|
ctx.ui.notify(`Agent file not found: ${filePath ?? agentKey}`, "error");
|
|
652
742
|
return;
|
|
653
743
|
}
|
|
654
|
-
const
|
|
655
|
-
if (
|
|
744
|
+
const availableModels = ctx.modelRegistry.getAvailable();
|
|
745
|
+
if (availableModels.length === 0) {
|
|
656
746
|
ctx.ui.notify("No models available in registry.", "error");
|
|
657
747
|
return;
|
|
658
748
|
}
|
|
659
|
-
|
|
660
|
-
(m: any) => `${m.provider}/${m.id} — ${m.name}`,
|
|
661
|
-
);
|
|
662
|
-
const currentContent = fs.readFileSync(filePath, "utf-8");
|
|
749
|
+
let currentContent = fs.readFileSync(filePath, "utf-8");
|
|
663
750
|
const currentModelMatch = currentContent.match(/^model:\s*(.+)$/m);
|
|
664
751
|
const currentModel = currentModelMatch ? currentModelMatch[1].trim() : "inherits parent";
|
|
665
|
-
const
|
|
752
|
+
const modelLabels = availableModels.map(
|
|
753
|
+
(m: any) => `${m.provider}/${m.id} — ${m.name}`,
|
|
754
|
+
);
|
|
755
|
+
const modelChoice = await boundedSelect(
|
|
756
|
+
ctx,
|
|
666
757
|
`Model for ${agentChoice} subagent (current: ${currentModel})`,
|
|
667
758
|
modelLabels,
|
|
668
759
|
);
|
|
669
760
|
if (!modelChoice) return;
|
|
670
761
|
const selectedIdx = modelLabels.indexOf(modelChoice);
|
|
671
762
|
if (selectedIdx < 0) return;
|
|
672
|
-
const selectedModel =
|
|
763
|
+
const selectedModel = availableModels[selectedIdx];
|
|
673
764
|
const modelValue = `${selectedModel.provider}/${selectedModel.id}`;
|
|
674
|
-
let updated: string;
|
|
675
765
|
if (currentModelMatch) {
|
|
676
|
-
|
|
766
|
+
currentContent = currentContent.replace(
|
|
677
767
|
/^model:\s*.+$/m,
|
|
678
768
|
`model: ${modelValue}`,
|
|
679
769
|
);
|
|
680
770
|
} else {
|
|
681
|
-
|
|
771
|
+
currentContent = currentContent.replace(
|
|
682
772
|
/^(---\n)/,
|
|
683
773
|
`$1model: ${modelValue}\n`,
|
|
684
774
|
);
|
|
685
775
|
}
|
|
686
|
-
|
|
776
|
+
const currentThinkingMatch = currentContent.match(/^thinking:\s*(.+)$/m);
|
|
777
|
+
const currentThinking = currentThinkingMatch ? currentThinkingMatch[1].trim() : "off";
|
|
778
|
+
const thinkingChoice = await ctx.ui.select(
|
|
779
|
+
`Thinking level for ${agentChoice} (current: ${currentThinking})`,
|
|
780
|
+
THINKING_LEVELS,
|
|
781
|
+
);
|
|
782
|
+
if (!thinkingChoice) {
|
|
783
|
+
fs.writeFileSync(filePath, currentContent, "utf-8");
|
|
784
|
+
ctx.ui.notify(
|
|
785
|
+
`${agentChoice} subagent model set to ${modelValue}. Will use on next spawn.`,
|
|
786
|
+
);
|
|
787
|
+
return;
|
|
788
|
+
}
|
|
789
|
+
if (currentThinkingMatch) {
|
|
790
|
+
currentContent = currentContent.replace(
|
|
791
|
+
/^thinking:\s*.+$/m,
|
|
792
|
+
`thinking: ${thinkingChoice}`,
|
|
793
|
+
);
|
|
794
|
+
} else {
|
|
795
|
+
currentContent = currentContent.replace(
|
|
796
|
+
/^(---\n)/,
|
|
797
|
+
`$1thinking: ${thinkingChoice}\n`,
|
|
798
|
+
);
|
|
799
|
+
}
|
|
800
|
+
fs.writeFileSync(filePath, currentContent, "utf-8");
|
|
687
801
|
ctx.ui.notify(
|
|
688
|
-
`${agentChoice} subagent model
|
|
802
|
+
`${agentChoice} subagent: model=${modelValue}, thinking=${thinkingChoice}. Will use on next spawn.`,
|
|
689
803
|
);
|
|
690
804
|
},
|
|
691
805
|
});
|
|
@@ -721,30 +835,31 @@ export default async function piCustomAgentsPlugin(pi: any): Promise<void> {
|
|
|
721
835
|
}
|
|
722
836
|
const target = await ctx.ui.select(
|
|
723
837
|
"Implement via",
|
|
724
|
-
["
|
|
838
|
+
["Code", "Orchestrate"],
|
|
725
839
|
);
|
|
726
840
|
if (!target) {
|
|
727
841
|
ctx.ui.notify("Plan implementation cancelled.");
|
|
728
842
|
return;
|
|
729
843
|
}
|
|
730
|
-
const targetMode = target === "
|
|
844
|
+
const targetMode = target === "Orchestrate" ? "orchestrate" : DEFAULT_MODE;
|
|
731
845
|
switchMode(targetMode, ctx);
|
|
732
|
-
const msg = target === "
|
|
846
|
+
const msg = target === "Orchestrate"
|
|
733
847
|
? "Execute the plan above. Decompose it into modules and produce delegation prompts for each."
|
|
734
848
|
: "Execute the plan above. Follow the steps in order, implement, test, and verify each step.";
|
|
735
849
|
pi.sendMessage(
|
|
736
850
|
{ customType: "pi-agents-plan-implement", content: PLAN_IMPLEMENT_PROMPT, display: false },
|
|
737
|
-
{ triggerTurn: true },
|
|
738
851
|
);
|
|
739
|
-
pi.sendUserMessage(msg);
|
|
852
|
+
pi.sendUserMessage(msg, { deliverAs: "followUp" });
|
|
740
853
|
}
|
|
741
854
|
|
|
742
|
-
pi.on("before_agent_start", async () => {
|
|
855
|
+
pi.on("before_agent_start", async (event: any) => {
|
|
856
|
+
const augmentedSystemPrompt = event.systemPrompt + PARALLEL_TOOL_CALL_GUIDANCE;
|
|
743
857
|
if (currentMode !== DEFAULT_MODE && lastMessagedMode !== currentMode) {
|
|
744
858
|
const mode = MODES[currentMode];
|
|
745
859
|
lastMessagedMode = currentMode;
|
|
746
|
-
waitingForPlan = currentMode === "
|
|
860
|
+
waitingForPlan = currentMode === "plan";
|
|
747
861
|
return {
|
|
862
|
+
systemPrompt: augmentedSystemPrompt,
|
|
748
863
|
message: {
|
|
749
864
|
customType: `pi-agents-enter-${currentMode}`,
|
|
750
865
|
content: mode.enterMessage,
|
|
@@ -756,6 +871,7 @@ export default async function piCustomAgentsPlugin(pi: any): Promise<void> {
|
|
|
756
871
|
const prevMode = MODES[lastMessagedMode];
|
|
757
872
|
lastMessagedMode = DEFAULT_MODE;
|
|
758
873
|
return {
|
|
874
|
+
systemPrompt: augmentedSystemPrompt,
|
|
759
875
|
message: {
|
|
760
876
|
customType: "pi-agents-exit",
|
|
761
877
|
content: prevMode.exitMessage.replace("{mode}", prevMode.label),
|
|
@@ -763,11 +879,23 @@ export default async function piCustomAgentsPlugin(pi: any): Promise<void> {
|
|
|
763
879
|
},
|
|
764
880
|
};
|
|
765
881
|
}
|
|
882
|
+
return { systemPrompt: augmentedSystemPrompt };
|
|
883
|
+
});
|
|
884
|
+
|
|
885
|
+
let lastTurnAborted = false;
|
|
886
|
+
|
|
887
|
+
pi.on("turn_end", (event: any) => {
|
|
888
|
+
const msg = event?.message;
|
|
889
|
+
lastTurnAborted = msg?.stopReason === "aborted" || msg?.role === "assistant" && msg?.content?.stopReason === "aborted";
|
|
766
890
|
});
|
|
767
891
|
|
|
768
892
|
pi.on("agent_settled", async (_event: any, ctx: any) => {
|
|
769
|
-
if (waitingForPlan && currentMode === "
|
|
893
|
+
if (waitingForPlan && currentMode === "plan") {
|
|
770
894
|
waitingForPlan = false;
|
|
895
|
+
if (lastTurnAborted) {
|
|
896
|
+
lastTurnAborted = false;
|
|
897
|
+
return;
|
|
898
|
+
}
|
|
771
899
|
const action = await showPlanReview(ctx);
|
|
772
900
|
if (action === "implement") {
|
|
773
901
|
await handlePlanImplement(ctx);
|
|
@@ -775,7 +903,7 @@ export default async function piCustomAgentsPlugin(pi: any): Promise<void> {
|
|
|
775
903
|
waitingForPlan = true;
|
|
776
904
|
ctx.ui.notify("Edit your plan — type your changes and send.");
|
|
777
905
|
} else if (action === "reject") {
|
|
778
|
-
ctx.ui.notify("Plan rejected. Staying in
|
|
906
|
+
ctx.ui.notify("Plan rejected. Staying in plan mode.");
|
|
779
907
|
}
|
|
780
908
|
}
|
|
781
909
|
});
|
|
@@ -794,130 +922,148 @@ export default async function piCustomAgentsPlugin(pi: any): Promise<void> {
|
|
|
794
922
|
function installCustomFooter(ctx: any) {
|
|
795
923
|
if (ctx.mode !== "tui") return;
|
|
796
924
|
ctx.ui.setFooter((_tui: any, theme: any, footerData: any) => {
|
|
797
|
-
|
|
798
|
-
|
|
799
|
-
|
|
800
|
-
|
|
801
|
-
|
|
802
|
-
|
|
803
|
-
|
|
804
|
-
|
|
805
|
-
|
|
806
|
-
|
|
807
|
-
|
|
808
|
-
|
|
809
|
-
|
|
810
|
-
|
|
811
|
-
|
|
812
|
-
|
|
813
|
-
|
|
814
|
-
|
|
815
|
-
|
|
816
|
-
|
|
817
|
-
|
|
818
|
-
|
|
819
|
-
|
|
820
|
-
|
|
821
|
-
|
|
822
|
-
|
|
823
|
-
|
|
824
|
-
|
|
825
|
-
|
|
826
|
-
|
|
827
|
-
|
|
828
|
-
|
|
829
|
-
|
|
830
|
-
|
|
831
|
-
|
|
832
|
-
|
|
833
|
-
|
|
834
|
-
|
|
835
|
-
|
|
836
|
-
|
|
837
|
-
|
|
838
|
-
|
|
839
|
-
|
|
840
|
-
|
|
841
|
-
|
|
842
|
-
|
|
843
|
-
|
|
844
|
-
|
|
845
|
-
|
|
846
|
-
|
|
847
|
-
|
|
848
|
-
|
|
849
|
-
|
|
850
|
-
|
|
851
|
-
|
|
852
|
-
|
|
853
|
-
|
|
854
|
-
|
|
855
|
-
|
|
856
|
-
|
|
857
|
-
|
|
858
|
-
|
|
859
|
-
|
|
860
|
-
|
|
861
|
-
|
|
862
|
-
|
|
863
|
-
|
|
864
|
-
|
|
865
|
-
|
|
866
|
-
|
|
867
|
-
|
|
868
|
-
|
|
869
|
-
|
|
870
|
-
const lines = [
|
|
871
|
-
truncateToWidth(theme.fg("dim", pwd), width, theme.fg("dim", "...")),
|
|
872
|
-
statsLine,
|
|
873
|
-
];
|
|
874
|
-
const statuses = footerData.getExtensionStatuses();
|
|
875
|
-
if (statuses.size > 0) {
|
|
876
|
-
const statusEntries = Array.from(statuses.entries()) as Array<[
|
|
877
|
-
string,
|
|
878
|
-
string,
|
|
879
|
-
]>;
|
|
880
|
-
const statusLine = statusEntries
|
|
881
|
-
.sort(([a], [b]) => a.localeCompare(b))
|
|
882
|
-
.map(([, text]) => text.replace(/[\r\n\t]/g, " ").replace(/ +/g, " ").trim())
|
|
883
|
-
.join(" ");
|
|
884
|
-
lines.push(truncateToWidth(statusLine, width, theme.fg("dim", "...")));
|
|
885
|
-
}
|
|
886
|
-
return lines;
|
|
887
|
-
},
|
|
888
|
-
};
|
|
925
|
+
return {
|
|
926
|
+
render(width: number): string[] {
|
|
927
|
+
const PAD = " ";
|
|
928
|
+
const innerWidth = Math.max(0, width - 2);
|
|
929
|
+
let totalCost = 0;
|
|
930
|
+
let latestCacheHitRate: number | undefined;
|
|
931
|
+
for (const entry of ctx.sessionManager.getEntries()) {
|
|
932
|
+
if (entry.type !== "message" || entry.message.role !== "assistant") continue;
|
|
933
|
+
const usage = entry.message.usage;
|
|
934
|
+
totalCost += usage.cost.total;
|
|
935
|
+
const promptTokens = usage.input + usage.cacheRead + usage.cacheWrite;
|
|
936
|
+
latestCacheHitRate = promptTokens > 0
|
|
937
|
+
? (usage.cacheRead / promptTokens) * 100
|
|
938
|
+
: undefined;
|
|
939
|
+
}
|
|
940
|
+
|
|
941
|
+
const model = ctx.model;
|
|
942
|
+
const contextUsage = ctx.getContextUsage();
|
|
943
|
+
const contextWindow = contextUsage?.contextWindow ?? model?.contextWindow ?? 0;
|
|
944
|
+
const usedTokens = contextUsage?.used ?? 0;
|
|
945
|
+
const statsParts: string[] = [];
|
|
946
|
+
statsParts.push(`${formatTokens(usedTokens)}/${formatTokens(contextWindow)}`);
|
|
947
|
+
if (latestCacheHitRate !== undefined) {
|
|
948
|
+
statsParts.push(`CH${latestCacheHitRate.toFixed(1)}%`);
|
|
949
|
+
}
|
|
950
|
+
if (totalCost || (model && ctx.modelRegistry.isUsingOAuth(model))) {
|
|
951
|
+
statsParts.push(`$${totalCost.toFixed(3)}`);
|
|
952
|
+
}
|
|
953
|
+
let statsLeft = statsParts.join(" ");
|
|
954
|
+
if (visibleWidth(statsLeft) > innerWidth) {
|
|
955
|
+
statsLeft = truncateToWidth(statsLeft, innerWidth, "...");
|
|
956
|
+
}
|
|
957
|
+
|
|
958
|
+
const mode = MODES[currentMode];
|
|
959
|
+
const modeLabel = mode.label.charAt(0).toUpperCase() + mode.label.slice(1);
|
|
960
|
+
const modelName = model?.name ?? model?.id ?? "no model";
|
|
961
|
+
const provider = model?.provider ?? "unknown";
|
|
962
|
+
const thinking = getThinkingLevelFromSession(ctx);
|
|
963
|
+
const variant = thinking !== "off" ? ` ${thinking}` : "";
|
|
964
|
+
const rightSide =
|
|
965
|
+
theme.fg("accent", modeLabel) +
|
|
966
|
+
` ${theme.fg("dim", "\u2022")} ` +
|
|
967
|
+
theme.fg("text", `${modelName}${variant}`) +
|
|
968
|
+
theme.fg("dim", ` ${provider}`);
|
|
969
|
+
const availableForRight = innerWidth - visibleWidth(statsLeft) - 2;
|
|
970
|
+
const displayedRight = availableForRight > 0
|
|
971
|
+
? truncateToWidth(rightSide, availableForRight, "")
|
|
972
|
+
: "";
|
|
973
|
+
const padding = " ".repeat(Math.max(
|
|
974
|
+
0,
|
|
975
|
+
innerWidth - visibleWidth(statsLeft) - visibleWidth(displayedRight),
|
|
976
|
+
));
|
|
977
|
+
const statsLine = PAD + theme.fg("dim", statsLeft) + padding + displayedRight + PAD;
|
|
978
|
+
|
|
979
|
+
const cwd = ctx.sessionManager.getCwd();
|
|
980
|
+
const folderName = cwd.split(/[/\\]/).filter(Boolean).pop() ?? cwd;
|
|
981
|
+
const folderLine = PAD + theme.fg("dim", folderName) + PAD;
|
|
982
|
+
const sessionName = ctx.sessionManager.getSessionName();
|
|
983
|
+
const lines = [
|
|
984
|
+
statsLine,
|
|
985
|
+
];
|
|
986
|
+
const tps = getLiveTps();
|
|
987
|
+
if (tps > 0) {
|
|
988
|
+
const tpsStr = tps < 10 ? tps.toFixed(1) : tps < 100 ? tps.toFixed(0) : `${Math.round(tps)}`;
|
|
989
|
+
lines.push(PAD + theme.fg("accent", `${tpsStr} tps`) + PAD);
|
|
990
|
+
}
|
|
991
|
+
lines.push(folderLine);
|
|
992
|
+
if (sessionName) {
|
|
993
|
+
lines.push(PAD + truncateToWidth(theme.fg("dim", sessionName), innerWidth, theme.fg("dim", "...")) + PAD);
|
|
994
|
+
}
|
|
995
|
+
return lines;
|
|
996
|
+
},
|
|
997
|
+
};
|
|
889
998
|
});
|
|
890
999
|
}
|
|
891
1000
|
|
|
892
|
-
|
|
893
|
-
const
|
|
894
|
-
|
|
895
|
-
|
|
896
|
-
|
|
897
|
-
|
|
1001
|
+
async function restoreSavedModel(ctx: any): Promise<void> {
|
|
1002
|
+
const persisted = readPersistedState();
|
|
1003
|
+
const saved = persisted.model;
|
|
1004
|
+
if (!saved) return;
|
|
1005
|
+
const current = ctx.model as Model<any> | undefined;
|
|
1006
|
+
if (current && modelIdentityString(current) === `${saved.provider}/${saved.modelId}`) {
|
|
1007
|
+
return;
|
|
1008
|
+
}
|
|
1009
|
+
const model = ctx.modelRegistry.find(saved.provider, saved.modelId) as Model<any> | undefined;
|
|
1010
|
+
if (!model || !ctx.modelRegistry.hasConfiguredAuth(model)) {
|
|
1011
|
+
return;
|
|
1012
|
+
}
|
|
1013
|
+
await pi.setModel(model);
|
|
1014
|
+
}
|
|
1015
|
+
|
|
1016
|
+
async function restoreMode(ctx: any): Promise<void> {
|
|
1017
|
+
const persisted = readPersistedState();
|
|
1018
|
+
const savedMode = persisted.mode && persisted.mode in MODES
|
|
1019
|
+
? persisted.mode
|
|
1020
|
+
: getLastModeFromSession(ctx);
|
|
1021
|
+
if (savedMode && savedMode !== DEFAULT_MODE) {
|
|
1022
|
+
currentMode = savedMode;
|
|
1023
|
+
lastMessagedMode = savedMode;
|
|
1024
|
+
pi.setActiveTools(MODES[savedMode].tools);
|
|
898
1025
|
} else {
|
|
899
1026
|
currentMode = DEFAULT_MODE;
|
|
900
1027
|
lastMessagedMode = null;
|
|
901
1028
|
pi.setActiveTools(FULL_TOOLS);
|
|
902
1029
|
}
|
|
1030
|
+
setActiveMode(currentMode);
|
|
1031
|
+
pi.events.emit("pi-ember-ui:mode-change", { mode: currentMode });
|
|
1032
|
+
}
|
|
1033
|
+
|
|
1034
|
+
pi.on("session_start", async (_event: any, ctx: any) => {
|
|
1035
|
+
await restoreMode(ctx);
|
|
1036
|
+
await restoreSavedModel(ctx);
|
|
903
1037
|
installCustomFooter(ctx);
|
|
904
1038
|
updateStatus(ctx);
|
|
905
1039
|
});
|
|
906
1040
|
|
|
907
1041
|
pi.on("session_switch", async (_event: any, ctx: any) => {
|
|
908
|
-
|
|
909
|
-
|
|
910
|
-
currentMode = restored;
|
|
911
|
-
lastMessagedMode = restored;
|
|
912
|
-
pi.setActiveTools(MODES[restored].tools);
|
|
913
|
-
} else {
|
|
914
|
-
currentMode = DEFAULT_MODE;
|
|
915
|
-
lastMessagedMode = null;
|
|
916
|
-
pi.setActiveTools(FULL_TOOLS);
|
|
917
|
-
}
|
|
1042
|
+
await restoreMode(ctx);
|
|
1043
|
+
await restoreSavedModel(ctx);
|
|
918
1044
|
updateStatus(ctx);
|
|
919
1045
|
installCustomFooter(ctx);
|
|
920
1046
|
});
|
|
921
1047
|
|
|
1048
|
+
pi.on("model_select", async (event: any, _ctx: any) => {
|
|
1049
|
+
const model = event.model as Model<any> | undefined;
|
|
1050
|
+
if (!model) return;
|
|
1051
|
+
const persisted = readPersistedState();
|
|
1052
|
+
const identity = { provider: model.provider, modelId: model.id };
|
|
1053
|
+
if (persisted.model?.provider === identity.provider && persisted.model?.modelId === identity.modelId) {
|
|
1054
|
+
return;
|
|
1055
|
+
}
|
|
1056
|
+
writePersistedState({ ...persisted, model: identity });
|
|
1057
|
+
});
|
|
1058
|
+
|
|
1059
|
+
pi.on("session_shutdown", (_event: any, ctx: any) => {
|
|
1060
|
+
const persisted = readPersistedState();
|
|
1061
|
+
const model = ctx.model as Model<any> | undefined;
|
|
1062
|
+
const modelIdentity = model
|
|
1063
|
+
? { provider: model.provider, modelId: model.id }
|
|
1064
|
+
: persisted.model;
|
|
1065
|
+
writePersistedState({ ...persisted, mode: currentMode, model: modelIdentity });
|
|
1066
|
+
});
|
|
1067
|
+
|
|
922
1068
|
await subagentPlugin(pi);
|
|
923
1069
|
}
|