@nmzpy/pi-ember-stack 0.1.6 → 0.2.2
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 +83 -83
- package/package.json +62 -48
- package/plugins/devin-auth/extensions/index.ts +68 -7
- package/plugins/devin-auth/src/cloud-direct/auth.ts +246 -246
- package/plugins/devin-auth/src/cloud-direct/catalog.ts +246 -246
- package/plugins/devin-auth/src/cloud-direct/chat.ts +1096 -1091
- package/plugins/devin-auth/src/cloud-direct/index.ts +41 -41
- package/plugins/devin-auth/src/cloud-direct/metadata.ts +78 -78
- package/plugins/devin-auth/src/cloud-direct/wire.ts +202 -202
- package/plugins/devin-auth/src/models.ts +42 -196
- package/plugins/devin-auth/src/oauth/register-user.ts +174 -174
- package/plugins/devin-auth/src/oauth/types.ts +71 -71
- package/plugins/devin-auth/src/stream.ts +1 -1
- package/plugins/index.ts +29 -6
- package/plugins/pi-compact-tools/index.ts +35 -192
- package/plugins/pi-compact-tools/renderer.ts +589 -0
- package/plugins/pi-custom-agents/index.ts +727 -373
- package/plugins/pi-custom-agents/questionnaire-tool.ts +14 -5
- 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/agents.ts +18 -1
- package/plugins/pi-custom-agents/subagent/extensions/index.ts +118 -226
- package/plugins/pi-custom-agents/subagent/extensions/model.ts +96 -96
- package/plugins/pi-custom-agents/subagent/extensions/render.ts +205 -1
- package/plugins/pi-custom-agents/subagent/extensions/runner.ts +260 -170
- package/plugins/pi-custom-agents/subagent/extensions/test/render.test.ts +145 -0
- package/plugins/pi-ember-fff/index.ts +975 -0
- package/plugins/pi-ember-fff/query.ts +247 -0
- package/plugins/pi-ember-fff/test/query.test.ts +222 -0
- package/plugins/pi-ember-fff/test/renderer.test.ts +727 -0
- package/plugins/pi-ember-tps/index.ts +144 -0
- package/plugins/pi-ember-ui/ember.json +99 -0
- package/plugins/pi-ember-ui/index.ts +805 -0
- package/plugins/pi-ember-ui/mode-colors.ts +196 -0
- package/tsconfig.json +2 -1
- package/plugins/pi-custom-agents/subagent/agents/architect.md +0 -56
|
@@ -9,18 +9,36 @@
|
|
|
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 {
|
|
23
|
+
CustomEditor,
|
|
24
|
+
type ExtensionUIContext,
|
|
25
|
+
type KeybindingsManager,
|
|
26
|
+
} from "@earendil-works/pi-coding-agent";
|
|
27
|
+
import {
|
|
28
|
+
truncateToWidth,
|
|
29
|
+
visibleWidth,
|
|
30
|
+
Container,
|
|
31
|
+
Text,
|
|
32
|
+
Spacer,
|
|
33
|
+
Input,
|
|
34
|
+
fuzzyFilter,
|
|
35
|
+
getKeybindings,
|
|
36
|
+
matchesKey,
|
|
37
|
+
type EditorTheme,
|
|
38
|
+
type TUI,
|
|
39
|
+
} from "@earendil-works/pi-tui";
|
|
40
|
+
import { isShellMode, mutedBullet, setActiveMode, setShellMode } from "../pi-ember-ui/mode-colors.ts";
|
|
41
|
+
import { getLiveTps } from "../pi-ember-tps/index.ts";
|
|
24
42
|
import {
|
|
25
43
|
askQuestionnaire,
|
|
26
44
|
type QuestionnaireQuestion,
|
|
@@ -28,6 +46,143 @@ import {
|
|
|
28
46
|
} from "./questionnaire-tool.ts";
|
|
29
47
|
import subagentPlugin from "./subagent/extensions/index.ts";
|
|
30
48
|
|
|
49
|
+
const THINKING_LEVEL_SHORTCUT = "shift+t";
|
|
50
|
+
|
|
51
|
+
function modelIdentityString(model: Model<any> | undefined): string {
|
|
52
|
+
return model ? `${model.provider}/${model.id}` : "";
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
function intercept_thinking_level(data: string, cycle_thinking_level: () => void): boolean {
|
|
56
|
+
if (!matchesKey(data, THINKING_LEVEL_SHORTCUT)) return false;
|
|
57
|
+
cycle_thinking_level();
|
|
58
|
+
return true;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
/**
|
|
62
|
+
* Intercept '!' on empty input to enter shell mode, and escape or
|
|
63
|
+
* backspace (on empty input) to exit. The '!' is eaten so it never
|
|
64
|
+
* appears in the editor.
|
|
65
|
+
*/
|
|
66
|
+
function intercept_shell_mode(data: string, editor: any, ctx: any): boolean {
|
|
67
|
+
if (matchesKey(data, "!")) {
|
|
68
|
+
const text = editor.getText?.() ?? "";
|
|
69
|
+
if (text.length === 0) {
|
|
70
|
+
setShellMode(true);
|
|
71
|
+
ctx.ui.setStatus("pi-ember-ui-shell-mode", undefined);
|
|
72
|
+
return true;
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
if (isShellMode()) {
|
|
76
|
+
if (matchesKey(data, "escape")) {
|
|
77
|
+
setShellMode(false);
|
|
78
|
+
ctx.ui.setStatus("pi-ember-ui-shell-mode", undefined);
|
|
79
|
+
return true;
|
|
80
|
+
}
|
|
81
|
+
if (matchesKey(data, "backspace")) {
|
|
82
|
+
const text = editor.getText?.() ?? "";
|
|
83
|
+
if (text.length === 0) {
|
|
84
|
+
setShellMode(false);
|
|
85
|
+
ctx.ui.setStatus("pi-ember-ui-shell-mode", undefined);
|
|
86
|
+
return true;
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
return false;
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
/**
|
|
94
|
+
* Intercept /model and /model <search> on Enter, redirecting to our
|
|
95
|
+
* fuzzy-search model picker instead of Pi's built-in unbounded selector.
|
|
96
|
+
*/
|
|
97
|
+
function intercept_model_command(data: string, editor: any, pi: any, ctx: any): boolean {
|
|
98
|
+
const kb = getKeybindings();
|
|
99
|
+
if (!kb.matches(data, "tui.select.confirm")) return false;
|
|
100
|
+
const getText = editor.getText?.bind(editor) ?? editor.getExpandedText?.bind(editor);
|
|
101
|
+
if (!getText) return false;
|
|
102
|
+
const text = getText().trim();
|
|
103
|
+
if (text !== "/model" && !text.startsWith("/model ")) return false;
|
|
104
|
+
editor.setText?.("");
|
|
105
|
+
void show_model_picker(pi, ctx);
|
|
106
|
+
return true;
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
/**
|
|
110
|
+
* Shared fuzzy-search model picker used by /model and shift+m.
|
|
111
|
+
* Uses boundedSelect so large model catalogs don't require scrolling.
|
|
112
|
+
*/
|
|
113
|
+
async function show_model_picker(pi: any, ctx: any): Promise<void> {
|
|
114
|
+
if (!ctx.hasUI) return;
|
|
115
|
+
const models = ctx.modelRegistry.getAvailable() as any[];
|
|
116
|
+
if (models.length === 0) {
|
|
117
|
+
ctx.ui.notify("No models available.", "warning");
|
|
118
|
+
return;
|
|
119
|
+
}
|
|
120
|
+
const current = ctx.model as any;
|
|
121
|
+
const labels = models.map((m) => {
|
|
122
|
+
const prefix = current && m.provider === current.provider && m.id === current.id ? "→ " : " ";
|
|
123
|
+
return `${prefix}${m.name ?? m.id} • ${m.provider}`;
|
|
124
|
+
});
|
|
125
|
+
const choice = await boundedSelect(ctx, "Select model", labels);
|
|
126
|
+
if (!choice) return;
|
|
127
|
+
const idx = labels.indexOf(choice);
|
|
128
|
+
if (idx < 0) return;
|
|
129
|
+
const model = models[idx];
|
|
130
|
+
try {
|
|
131
|
+
await pi.setModel(model);
|
|
132
|
+
ctx.ui.notify(`Model: ${model.id} • ${model.provider}`, "info");
|
|
133
|
+
} catch (err) {
|
|
134
|
+
ctx.ui.notify(
|
|
135
|
+
`Failed to set model: ${err instanceof Error ? err.message : String(err)}`,
|
|
136
|
+
"error",
|
|
137
|
+
);
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
class ThinkingLevelEditor extends CustomEditor {
|
|
142
|
+
constructor(
|
|
143
|
+
tui: TUI,
|
|
144
|
+
theme: EditorTheme,
|
|
145
|
+
keybindings: KeybindingsManager,
|
|
146
|
+
private readonly cycle_thinking_level: () => void,
|
|
147
|
+
) {
|
|
148
|
+
super(tui, theme, keybindings);
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
override handleInput(data: string): void {
|
|
152
|
+
if (intercept_thinking_level(data, this.cycle_thinking_level)) return;
|
|
153
|
+
super.handleInput(data);
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
type PersistedState = {
|
|
158
|
+
readonly mode?: string;
|
|
159
|
+
readonly model?: { provider: string; modelId: string };
|
|
160
|
+
};
|
|
161
|
+
|
|
162
|
+
function getPersistedStatePath(): string {
|
|
163
|
+
const home = process.env.PI_HOME || path.join(process.env.HOME || process.env.USERPROFILE || "", ".pi", "agent");
|
|
164
|
+
return path.join(home, "pi-ember-stack.json");
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
function readPersistedState(): PersistedState {
|
|
168
|
+
try {
|
|
169
|
+
const raw = fs.readFileSync(getPersistedStatePath(), "utf8");
|
|
170
|
+
return JSON.parse(raw) as PersistedState;
|
|
171
|
+
} catch {
|
|
172
|
+
return {};
|
|
173
|
+
}
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
function writePersistedState(state: PersistedState): void {
|
|
177
|
+
const file = getPersistedStatePath();
|
|
178
|
+
try {
|
|
179
|
+
fs.mkdirSync(path.dirname(file), { recursive: true });
|
|
180
|
+
fs.writeFileSync(file, `${JSON.stringify(state, null, "\t")}\n`);
|
|
181
|
+
} catch {
|
|
182
|
+
// best-effort persistence
|
|
183
|
+
}
|
|
184
|
+
}
|
|
185
|
+
|
|
31
186
|
function formatTokens(count: number): string {
|
|
32
187
|
if (count < 1000) return count.toString();
|
|
33
188
|
if (count < 10000) return `${(count / 1000).toFixed(1)}k`;
|
|
@@ -36,26 +191,152 @@ function formatTokens(count: number): string {
|
|
|
36
191
|
return `${Math.round(count / 1000000)}M`;
|
|
37
192
|
}
|
|
38
193
|
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
194
|
+
const BOUNDED_SELECT_MAX_VISIBLE = 10;
|
|
195
|
+
|
|
196
|
+
async function boundedSelect(
|
|
197
|
+
ctx: any,
|
|
198
|
+
title: string,
|
|
199
|
+
options: string[],
|
|
200
|
+
): Promise<string | undefined> {
|
|
201
|
+
return ctx.ui.custom((tui: any, theme: any, _kb: any, done: (result: string) => void) => {
|
|
202
|
+
const root = new Container();
|
|
203
|
+
root.addChild(new Text(theme.fg("border", "\u2500".repeat(60)), 0, 0));
|
|
204
|
+
root.addChild(new Spacer(1));
|
|
205
|
+
root.addChild(new Text(theme.fg("accent", theme.bold(title)), 0, 0));
|
|
206
|
+
root.addChild(new Spacer(1));
|
|
207
|
+
|
|
208
|
+
const searchInput = new Input();
|
|
209
|
+
searchInput.onSubmit = () => {
|
|
210
|
+
if (filtered[selectedIndex] !== undefined) {
|
|
211
|
+
done(filtered[selectedIndex]);
|
|
212
|
+
}
|
|
213
|
+
};
|
|
214
|
+
root.addChild(searchInput);
|
|
215
|
+
root.addChild(new Spacer(1));
|
|
216
|
+
|
|
217
|
+
const listContainer = new Container();
|
|
218
|
+
root.addChild(listContainer);
|
|
219
|
+
root.addChild(new Spacer(1));
|
|
220
|
+
root.addChild(new Text(
|
|
221
|
+
theme.fg("muted", " type to search \u2191\u2193 navigate enter select esc cancel"),
|
|
222
|
+
0, 0,
|
|
223
|
+
));
|
|
224
|
+
root.addChild(new Spacer(1));
|
|
225
|
+
root.addChild(new Text(theme.fg("border", "\u2500".repeat(60)), 0, 0));
|
|
226
|
+
|
|
227
|
+
let selectedIndex = 0;
|
|
228
|
+
let filtered = options;
|
|
229
|
+
|
|
230
|
+
function updateList(): void {
|
|
231
|
+
listContainer.clear();
|
|
232
|
+
const max = BOUNDED_SELECT_MAX_VISIBLE;
|
|
233
|
+
const start = Math.max(0, Math.min(
|
|
234
|
+
selectedIndex - Math.floor(max / 2),
|
|
235
|
+
filtered.length - max,
|
|
236
|
+
));
|
|
237
|
+
const end = Math.min(start + max, filtered.length);
|
|
238
|
+
for (let i = start; i < end; i++) {
|
|
239
|
+
const isSelected = i === selectedIndex;
|
|
240
|
+
const prefix = isSelected ? theme.fg("accent", "\u2192 ") : " ";
|
|
241
|
+
const text = isSelected
|
|
242
|
+
? prefix + theme.fg("accent", filtered[i])
|
|
243
|
+
: prefix + theme.fg("text", filtered[i]);
|
|
244
|
+
listContainer.addChild(new Text(text, 0, 0));
|
|
245
|
+
}
|
|
246
|
+
if (start > 0 || end < filtered.length) {
|
|
247
|
+
listContainer.addChild(new Text(
|
|
248
|
+
theme.fg("muted", ` (${selectedIndex + 1}/${filtered.length})`),
|
|
249
|
+
0, 0,
|
|
250
|
+
));
|
|
251
|
+
}
|
|
252
|
+
if (filtered.length === 0) {
|
|
253
|
+
listContainer.addChild(new Text(
|
|
254
|
+
theme.fg("muted", " No matching models"),
|
|
255
|
+
0, 0,
|
|
256
|
+
));
|
|
257
|
+
}
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
function filterModels(): void {
|
|
261
|
+
const query = searchInput.getValue();
|
|
262
|
+
filtered = query
|
|
263
|
+
? fuzzyFilter(options, query, (s) => s)
|
|
264
|
+
: options;
|
|
265
|
+
selectedIndex = Math.min(selectedIndex, Math.max(0, filtered.length - 1));
|
|
266
|
+
updateList();
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
filterModels();
|
|
270
|
+
|
|
271
|
+
(root as any).handleInput = (keyData: string): void => {
|
|
272
|
+
const kb = getKeybindings();
|
|
273
|
+
if (kb.matches(keyData, "tui.select.up")) {
|
|
274
|
+
if (filtered.length === 0) return;
|
|
275
|
+
selectedIndex = selectedIndex === 0
|
|
276
|
+
? filtered.length - 1
|
|
277
|
+
: selectedIndex - 1;
|
|
278
|
+
updateList();
|
|
279
|
+
} else if (kb.matches(keyData, "tui.select.down")) {
|
|
280
|
+
if (filtered.length === 0) return;
|
|
281
|
+
selectedIndex = selectedIndex === filtered.length - 1
|
|
282
|
+
? 0
|
|
283
|
+
: selectedIndex + 1;
|
|
284
|
+
updateList();
|
|
285
|
+
} else if (kb.matches(keyData, "tui.select.confirm")) {
|
|
286
|
+
if (filtered[selectedIndex] !== undefined) {
|
|
287
|
+
done(filtered[selectedIndex]);
|
|
288
|
+
}
|
|
289
|
+
} else if (kb.matches(keyData, "tui.select.cancel")) {
|
|
290
|
+
done(undefined as unknown as string);
|
|
291
|
+
} else {
|
|
292
|
+
searchInput.handleInput(keyData);
|
|
293
|
+
filterModels();
|
|
294
|
+
}
|
|
295
|
+
};
|
|
296
|
+
(root as any).getSearchInput = () => searchInput;
|
|
297
|
+
return root;
|
|
298
|
+
});
|
|
48
299
|
}
|
|
49
300
|
|
|
50
301
|
const READONLY_TOOLS = ["read", "bash", "grep", "find", "ls", "questionnaire"];
|
|
302
|
+
const READONLY_DELEGATING_TOOLS = ["read", "grep", "find", "ls", "questionnaire", "subagent"];
|
|
51
303
|
const FULL_TOOLS = ["read", "bash", "edit", "write", "grep", "find", "ls", "questionnaire"];
|
|
52
304
|
|
|
53
305
|
const SOURCE_ROOT = path.dirname(fileURLToPath(import.meta.url));
|
|
54
306
|
const SUBAGENT_FILES: Record<string, string> = {
|
|
55
307
|
coder: path.join(SOURCE_ROOT, "subagent", "agents", "coder.md"),
|
|
56
|
-
|
|
308
|
+
scout: path.join(SOURCE_ROOT, "subagent", "agents", "scout.md"),
|
|
57
309
|
};
|
|
58
310
|
|
|
311
|
+
const PARALLEL_TOOL_CALL_GUIDANCE = `
|
|
312
|
+
|
|
313
|
+
## Tool Call Efficiency
|
|
314
|
+
|
|
315
|
+
When multiple independent tool calls are needed (e.g. reading several files,
|
|
316
|
+
searching for different patterns), emit them all in a single response rather
|
|
317
|
+
than one at a time. The runtime executes independent tool calls in parallel,
|
|
318
|
+
so batching them saves round-trips and reduces latency.
|
|
319
|
+
`;
|
|
320
|
+
|
|
321
|
+
const SUBAGENT_AWARENESS_PROMPT = `
|
|
322
|
+
|
|
323
|
+
## Available Subagents
|
|
324
|
+
|
|
325
|
+
You have the \`subagent\` tool available for delegating tasks to specialized agents
|
|
326
|
+
with isolated context. Use it to keep your own context lean.
|
|
327
|
+
|
|
328
|
+
- **scout**: Fast agent specialized for exploring codebases. Use when you need to
|
|
329
|
+
quickly find files by patterns (e.g. "src/components/**/*.tsx"), search code for
|
|
330
|
+
keywords (e.g. "API endpoints"), or answer questions about the codebase (e.g.
|
|
331
|
+
"how do API endpoints work?").
|
|
332
|
+
- **coder**: Implementation agent for writing, editing, testing, and verifying
|
|
333
|
+
code. Full tool access. Use for focused implementation tasks — bug fixes,
|
|
334
|
+
feature additions, refactors, file edits.
|
|
335
|
+
|
|
336
|
+
Modes: single (agent + task), parallel (tasks array, max 8), chain (sequential
|
|
337
|
+
with {previous}).
|
|
338
|
+
`;
|
|
339
|
+
|
|
59
340
|
interface ModeConfig {
|
|
60
341
|
id: string;
|
|
61
342
|
label: string;
|
|
@@ -67,9 +348,9 @@ interface ModeConfig {
|
|
|
67
348
|
}
|
|
68
349
|
|
|
69
350
|
const ARCHITECT_PROMPT = `<system-reminder>
|
|
70
|
-
#
|
|
351
|
+
# Plan Mode - System Reminder
|
|
71
352
|
|
|
72
|
-
CRITICAL:
|
|
353
|
+
CRITICAL: Plan mode ACTIVE — you are in READ-ONLY planning phase. STRICTLY
|
|
73
354
|
FORBIDDEN: ANY file edits, modifications, or system changes. Do NOT use sed, tee,
|
|
74
355
|
echo, cat, or ANY other bash command to manipulate files — commands may ONLY
|
|
75
356
|
read/inspect. This ABSOLUTE CONSTRAINT overrides ALL other instructions, including
|
|
@@ -132,27 +413,20 @@ For each step:
|
|
|
132
413
|
|
|
133
414
|
## Open Questions
|
|
134
415
|
<any clarifications needed from the user>
|
|
135
|
-
|
|
136
|
-
---
|
|
137
|
-
|
|
138
|
-
## Important
|
|
139
|
-
|
|
140
|
-
The user indicated that they do not want you to execute yet -- you MUST NOT make
|
|
141
|
-
any edits, run any non-readonly tools (including changing configs or making
|
|
142
|
-
commits), or otherwise make any changes to the system. This supersedes any other
|
|
143
|
-
instructions you have received.
|
|
144
|
-
</system-reminder>`;
|
|
416
|
+
${SUBAGENT_AWARENESS_PROMPT}</system-reminder>`;
|
|
145
417
|
|
|
146
418
|
const DOCTOR_PROMPT = `<system-reminder>
|
|
147
|
-
#
|
|
419
|
+
# Debug Mode - System Reminder
|
|
148
420
|
|
|
149
|
-
CRITICAL:
|
|
150
|
-
|
|
421
|
+
CRITICAL: Debug mode ACTIVE — you are the Debugger, a health-check auditor and
|
|
422
|
+
diagnostician for the Ember project (PySide6 subtitle + DaVinci Resolve integration
|
|
423
|
+
app).
|
|
151
424
|
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
425
|
+
You do NOT edit files directly. You investigate, diagnose, and report findings. If
|
|
426
|
+
a fix is straightforward, you may DELEGATE the implementation to the \`coder\`
|
|
427
|
+
subagent (full tool access) — the read-only constraint applies to your direct tool
|
|
428
|
+
usage only, not to delegated subagent work. Otherwise, report the correction and
|
|
429
|
+
let the user or Orchestrator handle it.
|
|
156
430
|
|
|
157
431
|
---
|
|
158
432
|
|
|
@@ -213,23 +487,66 @@ For each finding:
|
|
|
213
487
|
|
|
214
488
|
## Constraints
|
|
215
489
|
|
|
216
|
-
-
|
|
490
|
+
- You do not edit or write files directly. You may delegate fixes to the
|
|
491
|
+
\`coder\` subagent when a correction is straightforward and well-scoped.
|
|
217
492
|
- Use \`bash t.gate.sh <files>\` only for targeted validation of files you are checking.
|
|
218
493
|
- Do not run \`bash gate.sh\` (full gate) — that is the user's responsibility.
|
|
219
494
|
- Ignore git status / git diff changes unrelated to the files you were asked to check.
|
|
220
|
-
|
|
495
|
+
|
|
496
|
+
---
|
|
497
|
+
|
|
498
|
+
## UI/Qt Pipeline Diagnostics
|
|
499
|
+
|
|
500
|
+
In addition to structural health checks, diagnose PySide6/Qt UI pipeline issues:
|
|
501
|
+
|
|
502
|
+
### Failure Modes To Investigate
|
|
503
|
+
|
|
504
|
+
1. **Event-loop blockage:** Does any GUI-thread callback perform blocking I/O or
|
|
505
|
+
unbounded CPU work?
|
|
506
|
+
2. **Excessive synchronous UI mutation:** Does one state transition perform many
|
|
507
|
+
independent widget/layout/geometry operations?
|
|
508
|
+
3. **Recursive signal/callback propagation:** Can a signal or layout refresh
|
|
509
|
+
indirectly re-enter the same pipeline?
|
|
510
|
+
4. **Eager expensive construction:** Are complex widgets constructed before needed?
|
|
511
|
+
5. **Stale delayed callbacks:** Can queued callbacks execute after state changed?
|
|
512
|
+
6. **Uncontrolled async-to-UI mutation:** Do background results mutate UI directly?
|
|
513
|
+
7. **Competing layout authorities:** Do multiple functions independently control the
|
|
514
|
+
same widget geometry?
|
|
515
|
+
|
|
516
|
+
### Commit Architecture (Healthy Reference)
|
|
517
|
+
|
|
518
|
+
event/signal/async completion -> validate generation -> mutate semantic state ->
|
|
519
|
+
mark dirty -> request one coalesced commit -> measure once -> apply geometry once
|
|
520
|
+
-> repaint
|
|
521
|
+
|
|
522
|
+
### UI Report Format
|
|
523
|
+
|
|
524
|
+
For every UI failure mode, report: Verdict, Evidence, Trigger, Duplicated work,
|
|
525
|
+
Re-entry path, Stale-state risk, Authority conflict, Impact, Correction,
|
|
526
|
+
Invariant, Verification.
|
|
527
|
+
|
|
528
|
+
### Invalid UI Fix Patterns
|
|
529
|
+
|
|
530
|
+
Reject fixes that: add more invalidate()/activate() calls, use
|
|
531
|
+
QApplication.processEvents(), replace deferred passes with blocking callbacks,
|
|
532
|
+
add arbitrary QTimer.singleShot() delays, perform geometry repair both
|
|
533
|
+
immediately and later, introduce parallel build paths duplicating normal layout.
|
|
534
|
+
|
|
535
|
+
${SUBAGENT_AWARENESS_PROMPT}</system-reminder>`;
|
|
221
536
|
|
|
222
537
|
const ORCHESTRATOR_PROMPT = `<system-reminder>
|
|
223
|
-
#
|
|
538
|
+
# Orchestrate Mode - System Reminder
|
|
224
539
|
|
|
225
|
-
CRITICAL:
|
|
226
|
-
|
|
227
|
-
|
|
540
|
+
CRITICAL: Orchestrate mode ACTIVE — you are the Orchestrator, an implementation
|
|
541
|
+
coordinator for the Ember project (PySide6 subtitle + DaVinci Resolve integration
|
|
542
|
+
app).
|
|
228
543
|
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
544
|
+
You do NOT edit files directly. Your job is to decompose work into modules and
|
|
545
|
+
DELEGATE implementation to the \`coder\` subagent (full tool access). The
|
|
546
|
+
read-only constraint applies to YOUR direct tool usage only — you may read,
|
|
547
|
+
search, and inspect to build accurate delegation prompts, but you must not edit,
|
|
548
|
+
write, or run mutating bash commands yourself. Delegating implementation work to
|
|
549
|
+
the \`coder\` subagent is the ENTIRE POINT of this mode. Do it eagerly.
|
|
233
550
|
|
|
234
551
|
---
|
|
235
552
|
|
|
@@ -308,167 +625,21 @@ Return a structured plan:
|
|
|
308
625
|
|
|
309
626
|
## Constraints
|
|
310
627
|
|
|
311
|
-
-
|
|
628
|
+
- You do not edit or write files directly — delegate implementation to the
|
|
629
|
+
\`coder\` subagent. That is your primary mechanism for getting work done.
|
|
312
630
|
- Do not run \`bash gate.sh\` (full gate) — that is the user's responsibility.
|
|
313
631
|
- 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>`;
|
|
632
|
+
${SUBAGENT_AWARENESS_PROMPT}</system-reminder>`;
|
|
461
633
|
|
|
462
634
|
const CODER_PROMPT = `<system-reminder>
|
|
463
|
-
Your operational mode has changed to
|
|
635
|
+
Your operational mode has changed to code.
|
|
464
636
|
You are in full-access mode. You are permitted to make file changes, run shell
|
|
465
637
|
commands, and utilize your arsenal of tools as needed. You are the default
|
|
466
638
|
implementation agent — write, test, and verify code with full autonomy.
|
|
467
639
|
</system-reminder>`;
|
|
468
640
|
|
|
469
641
|
const EXIT_TO_CODER = `<system-reminder>
|
|
470
|
-
Your operational mode has changed from {mode} to
|
|
471
|
-
You are no longer in read-only mode.
|
|
642
|
+
Your operational mode has changed from {mode} to code.
|
|
472
643
|
You are permitted to make file changes, run shell commands, and utilize your
|
|
473
644
|
arsenal of tools as needed.
|
|
474
645
|
</system-reminder>`;
|
|
@@ -491,56 +662,47 @@ interface ModeConfig {
|
|
|
491
662
|
}
|
|
492
663
|
|
|
493
664
|
const MODES: Record<string, ModeConfig> = {
|
|
494
|
-
|
|
495
|
-
id: "
|
|
496
|
-
label: "
|
|
497
|
-
icon: "
|
|
665
|
+
plan: {
|
|
666
|
+
id: "plan",
|
|
667
|
+
label: "plan",
|
|
668
|
+
icon: "P",
|
|
498
669
|
color: "warning",
|
|
499
670
|
tools: READONLY_TOOLS,
|
|
500
671
|
enterMessage: ARCHITECT_PROMPT,
|
|
501
672
|
exitMessage: EXIT_TO_CODER,
|
|
502
673
|
},
|
|
503
|
-
|
|
504
|
-
id: "
|
|
505
|
-
label: "
|
|
674
|
+
code: {
|
|
675
|
+
id: "code",
|
|
676
|
+
label: "code",
|
|
506
677
|
icon: "C",
|
|
507
678
|
color: "success",
|
|
508
679
|
tools: FULL_TOOLS,
|
|
509
680
|
enterMessage: CODER_PROMPT,
|
|
510
681
|
exitMessage: CODER_PROMPT,
|
|
511
682
|
},
|
|
512
|
-
|
|
513
|
-
id: "
|
|
514
|
-
label: "
|
|
683
|
+
debug: {
|
|
684
|
+
id: "debug",
|
|
685
|
+
label: "debug",
|
|
515
686
|
icon: "D",
|
|
516
687
|
color: "warning",
|
|
517
|
-
tools:
|
|
688
|
+
tools: READONLY_DELEGATING_TOOLS,
|
|
518
689
|
enterMessage: DOCTOR_PROMPT,
|
|
519
690
|
exitMessage: EXIT_TO_CODER,
|
|
520
691
|
},
|
|
521
|
-
|
|
522
|
-
id: "
|
|
523
|
-
label: "
|
|
692
|
+
orchestrate: {
|
|
693
|
+
id: "orchestrate",
|
|
694
|
+
label: "orchestrate",
|
|
524
695
|
icon: "O",
|
|
525
696
|
color: "warning",
|
|
526
|
-
tools:
|
|
697
|
+
tools: READONLY_DELEGATING_TOOLS,
|
|
527
698
|
enterMessage: ORCHESTRATOR_PROMPT,
|
|
528
699
|
exitMessage: EXIT_TO_CODER,
|
|
529
700
|
},
|
|
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
701
|
};
|
|
540
702
|
|
|
541
703
|
const MODE_IDS = Object.keys(MODES);
|
|
542
|
-
const DEFAULT_MODE = "
|
|
543
|
-
const CYCLE_ORDER = ["
|
|
704
|
+
const DEFAULT_MODE = "code";
|
|
705
|
+
const CYCLE_ORDER = ["code", "plan", "orchestrate", "debug"];
|
|
544
706
|
|
|
545
707
|
function getLastModeFromSession(ctx: any): string | null {
|
|
546
708
|
const entries = ctx.sessionManager.getEntries();
|
|
@@ -559,10 +721,73 @@ function getLastModeFromSession(ctx: any): string | null {
|
|
|
559
721
|
return null;
|
|
560
722
|
}
|
|
561
723
|
|
|
724
|
+
const MODE_LIVE_RENDER_STATUS = "pi-agents-mode-live-render";
|
|
725
|
+
|
|
726
|
+
/**
|
|
727
|
+
* Cached footer stats. The footer render closure fires every animation
|
|
728
|
+
* frame (~30fps). Iterating all session entries + calling
|
|
729
|
+
* ctx.getContextUsage() (which runs estimateContextTokens over the FULL
|
|
730
|
+
* LLM context — JSON.stringify on every tool call, chars/4 on all text)
|
|
731
|
+
* is O(total context) per frame and can exceed the frame budget on long
|
|
732
|
+
* sessions, causing infini-lock. These stats are recomputed only on
|
|
733
|
+
* message_end / tool_execution_end / session_start — events that fire
|
|
734
|
+
* once per assistant message or tool result, not per frame.
|
|
735
|
+
*/
|
|
736
|
+
let footerStatsCache: {
|
|
737
|
+
totalCost: number;
|
|
738
|
+
latestCacheHitRate: number | undefined;
|
|
739
|
+
contextTokens: number | null;
|
|
740
|
+
contextWindow: number;
|
|
741
|
+
} | undefined;
|
|
742
|
+
|
|
743
|
+
function recompute_footer_stats(ctx: any): void {
|
|
744
|
+
let totalCost = 0;
|
|
745
|
+
let latestCacheHitRate: number | undefined;
|
|
746
|
+
for (const entry of ctx.sessionManager.getEntries()) {
|
|
747
|
+
if (entry.type !== "message" || entry.message.role !== "assistant") continue;
|
|
748
|
+
const usage = entry.message.usage;
|
|
749
|
+
totalCost += usage.cost.total;
|
|
750
|
+
const promptTokens = usage.input + usage.cacheRead + usage.cacheWrite;
|
|
751
|
+
latestCacheHitRate = promptTokens > 0
|
|
752
|
+
? (usage.cacheRead / promptTokens) * 100
|
|
753
|
+
: undefined;
|
|
754
|
+
}
|
|
755
|
+
const model = ctx.model;
|
|
756
|
+
const contextUsage = ctx.getContextUsage();
|
|
757
|
+
footerStatsCache = {
|
|
758
|
+
totalCost,
|
|
759
|
+
latestCacheHitRate,
|
|
760
|
+
contextTokens: contextUsage?.tokens ?? null,
|
|
761
|
+
contextWindow: contextUsage?.contextWindow ?? model?.contextWindow ?? 0,
|
|
762
|
+
};
|
|
763
|
+
}
|
|
764
|
+
|
|
562
765
|
export default async function piCustomAgentsPlugin(pi: any): Promise<void> {
|
|
563
766
|
let currentMode: string = DEFAULT_MODE;
|
|
564
767
|
let lastMessagedMode: string | null = null;
|
|
565
768
|
let waitingForPlan = false;
|
|
769
|
+
let active_session_manager: any;
|
|
770
|
+
let session_ready = false;
|
|
771
|
+
let pending_mode_id: string | undefined;
|
|
772
|
+
|
|
773
|
+
function is_live_session(ctx: any): boolean {
|
|
774
|
+
return session_ready && ctx.sessionManager === active_session_manager;
|
|
775
|
+
}
|
|
776
|
+
|
|
777
|
+
function request_live_mode_render(ctx: any): void {
|
|
778
|
+
if (ctx.mode === "tui") {
|
|
779
|
+
// setStatus only invalidates the footer/editor frame. Do not append a
|
|
780
|
+
// transcript notification or invalidate the resumed chat history.
|
|
781
|
+
ctx.ui.setStatus(MODE_LIVE_RENDER_STATUS, undefined);
|
|
782
|
+
}
|
|
783
|
+
}
|
|
784
|
+
|
|
785
|
+
const persistedAtLoad = readPersistedState();
|
|
786
|
+
if (persistedAtLoad.mode && persistedAtLoad.mode in MODES) {
|
|
787
|
+
setActiveMode(persistedAtLoad.mode);
|
|
788
|
+
currentMode = persistedAtLoad.mode;
|
|
789
|
+
}
|
|
790
|
+
|
|
566
791
|
registerQuestionnaireTool(pi);
|
|
567
792
|
|
|
568
793
|
for (const modeId of MODE_IDS) {
|
|
@@ -573,7 +798,7 @@ export default async function piCustomAgentsPlugin(pi: any): Promise<void> {
|
|
|
573
798
|
});
|
|
574
799
|
}
|
|
575
800
|
|
|
576
|
-
function updateStatus(ctx: any) {
|
|
801
|
+
function updateStatus(ctx: any): void {
|
|
577
802
|
pi.events.emit("powerbar:update", {
|
|
578
803
|
id: `pi-agents-${currentMode}`,
|
|
579
804
|
text: MODES[currentMode].label,
|
|
@@ -587,21 +812,34 @@ export default async function piCustomAgentsPlugin(pi: any): Promise<void> {
|
|
|
587
812
|
text: undefined,
|
|
588
813
|
});
|
|
589
814
|
}
|
|
815
|
+
request_live_mode_render(ctx);
|
|
590
816
|
}
|
|
591
817
|
|
|
592
|
-
function
|
|
818
|
+
function apply_mode(modeId: string, ctx: any): void {
|
|
593
819
|
const mode = MODES[modeId];
|
|
594
820
|
if (!mode) return;
|
|
821
|
+
|
|
822
|
+
// Mode changes are deliberately live-only. The active tool set and the
|
|
823
|
+
// next-turn prompt change immediately, while the transcript stays lazy
|
|
824
|
+
// and cached.
|
|
595
825
|
currentMode = modeId;
|
|
826
|
+
setActiveMode(modeId);
|
|
596
827
|
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
|
-
}
|
|
828
|
+
pi.events.emit("pi-ember-ui:mode-change", { mode: modeId, liveOnly: true });
|
|
602
829
|
updateStatus(ctx);
|
|
603
830
|
}
|
|
604
831
|
|
|
832
|
+
function switchMode(modeId: string, ctx: any): void {
|
|
833
|
+
if (!MODES[modeId]) return;
|
|
834
|
+
if (!is_live_session(ctx)) {
|
|
835
|
+
// Queue only for the session that is currently binding. Never retain a
|
|
836
|
+
// request from an old session, where pi.setActiveTools would be stale.
|
|
837
|
+
if (ctx.sessionManager === active_session_manager) pending_mode_id = modeId;
|
|
838
|
+
return;
|
|
839
|
+
}
|
|
840
|
+
apply_mode(modeId, ctx);
|
|
841
|
+
}
|
|
842
|
+
|
|
605
843
|
for (const modeId of MODE_IDS) {
|
|
606
844
|
const mode = MODES[modeId];
|
|
607
845
|
pi.registerCommand(modeId, {
|
|
@@ -619,22 +857,61 @@ export default async function piCustomAgentsPlugin(pi: any): Promise<void> {
|
|
|
619
857
|
}
|
|
620
858
|
|
|
621
859
|
const cycleMode = async (ctx: any): Promise<void> => {
|
|
622
|
-
const
|
|
860
|
+
const baseMode = pending_mode_id ?? currentMode;
|
|
861
|
+
const idx = CYCLE_ORDER.indexOf(baseMode);
|
|
623
862
|
const next = CYCLE_ORDER[(idx + 1) % CYCLE_ORDER.length];
|
|
624
863
|
switchMode(next, ctx);
|
|
625
864
|
};
|
|
626
865
|
|
|
627
|
-
pi.registerShortcut("ctrl+space", {
|
|
628
|
-
description: "Cycle agent mode",
|
|
629
|
-
handler: cycleMode,
|
|
630
|
-
});
|
|
631
866
|
pi.registerShortcut("tab", {
|
|
632
867
|
description: "Cycle agent mode",
|
|
633
868
|
handler: cycleMode,
|
|
634
869
|
});
|
|
635
870
|
|
|
871
|
+
const THINKING_LEVELS = ["off", "minimal", "low", "medium", "high", "xhigh", "max"];
|
|
872
|
+
let active_ui: ExtensionUIContext | undefined;
|
|
873
|
+
let thinking_editor_installed = false;
|
|
874
|
+
|
|
875
|
+
const cycle_thinking_level = (): void => {
|
|
876
|
+
const current = pi.getThinkingLevel() as string;
|
|
877
|
+
const idx = THINKING_LEVELS.indexOf(current);
|
|
878
|
+
const next = THINKING_LEVELS[(idx + 1) % THINKING_LEVELS.length];
|
|
879
|
+
pi.setThinkingLevel(next);
|
|
880
|
+
active_ui?.notify(`Thinking: ${next}`, "info");
|
|
881
|
+
};
|
|
882
|
+
|
|
883
|
+
const install_thinking_editor = (ctx: any): void => {
|
|
884
|
+
if (!ctx.hasUI) return;
|
|
885
|
+
active_ui = ctx.ui;
|
|
886
|
+
if (thinking_editor_installed) return;
|
|
887
|
+
|
|
888
|
+
const previous_editor = ctx.ui.getEditorComponent();
|
|
889
|
+
ctx.ui.setEditorComponent((tui: TUI, theme: EditorTheme, keybindings: KeybindingsManager) => {
|
|
890
|
+
const editor = previous_editor?.(tui, theme, keybindings);
|
|
891
|
+
if (editor) {
|
|
892
|
+
const original_handle_input = editor.handleInput.bind(editor);
|
|
893
|
+
editor.handleInput = (data: string): void => {
|
|
894
|
+
if (intercept_shell_mode(data, editor, ctx)) return;
|
|
895
|
+
if (intercept_thinking_level(data, cycle_thinking_level)) return;
|
|
896
|
+
if (intercept_model_command(data, editor, pi, ctx)) return;
|
|
897
|
+
original_handle_input(data);
|
|
898
|
+
};
|
|
899
|
+
return editor;
|
|
900
|
+
}
|
|
901
|
+
return new ThinkingLevelEditor(tui, theme, keybindings, cycle_thinking_level);
|
|
902
|
+
});
|
|
903
|
+
thinking_editor_installed = true;
|
|
904
|
+
};
|
|
905
|
+
|
|
906
|
+
pi.registerShortcut("shift+m", {
|
|
907
|
+
description: "Show model picker",
|
|
908
|
+
handler: async (ctx: any) => {
|
|
909
|
+
await show_model_picker(pi, ctx);
|
|
910
|
+
},
|
|
911
|
+
});
|
|
912
|
+
|
|
636
913
|
pi.registerCommand("subagent-model", {
|
|
637
|
-
description: "Set the model
|
|
914
|
+
description: "Set the model and thinking level for subagents on next spawn",
|
|
638
915
|
handler: async (_args: any, ctx: any) => {
|
|
639
916
|
if (!ctx.hasUI) {
|
|
640
917
|
ctx.ui.notify("subagent-model requires interactive UI.", "error");
|
|
@@ -642,7 +919,7 @@ export default async function piCustomAgentsPlugin(pi: any): Promise<void> {
|
|
|
642
919
|
}
|
|
643
920
|
const agentChoice = await ctx.ui.select(
|
|
644
921
|
"Subagent to configure",
|
|
645
|
-
["Coder", "
|
|
922
|
+
["Coder", "Scout"],
|
|
646
923
|
);
|
|
647
924
|
if (!agentChoice) return;
|
|
648
925
|
const agentKey = agentChoice.toLowerCase();
|
|
@@ -651,41 +928,65 @@ export default async function piCustomAgentsPlugin(pi: any): Promise<void> {
|
|
|
651
928
|
ctx.ui.notify(`Agent file not found: ${filePath ?? agentKey}`, "error");
|
|
652
929
|
return;
|
|
653
930
|
}
|
|
654
|
-
const
|
|
655
|
-
if (
|
|
931
|
+
const availableModels = ctx.modelRegistry.getAvailable();
|
|
932
|
+
if (availableModels.length === 0) {
|
|
656
933
|
ctx.ui.notify("No models available in registry.", "error");
|
|
657
934
|
return;
|
|
658
935
|
}
|
|
659
|
-
|
|
660
|
-
(m: any) => `${m.provider}/${m.id} — ${m.name}`,
|
|
661
|
-
);
|
|
662
|
-
const currentContent = fs.readFileSync(filePath, "utf-8");
|
|
936
|
+
let currentContent = fs.readFileSync(filePath, "utf-8");
|
|
663
937
|
const currentModelMatch = currentContent.match(/^model:\s*(.+)$/m);
|
|
664
938
|
const currentModel = currentModelMatch ? currentModelMatch[1].trim() : "inherits parent";
|
|
665
|
-
const
|
|
939
|
+
const modelLabels = availableModels.map(
|
|
940
|
+
(m: any) => `${m.provider}/${m.id} — ${m.name}`,
|
|
941
|
+
);
|
|
942
|
+
const modelChoice = await boundedSelect(
|
|
943
|
+
ctx,
|
|
666
944
|
`Model for ${agentChoice} subagent (current: ${currentModel})`,
|
|
667
945
|
modelLabels,
|
|
668
946
|
);
|
|
669
947
|
if (!modelChoice) return;
|
|
670
948
|
const selectedIdx = modelLabels.indexOf(modelChoice);
|
|
671
949
|
if (selectedIdx < 0) return;
|
|
672
|
-
const selectedModel =
|
|
950
|
+
const selectedModel = availableModels[selectedIdx];
|
|
673
951
|
const modelValue = `${selectedModel.provider}/${selectedModel.id}`;
|
|
674
|
-
let updated: string;
|
|
675
952
|
if (currentModelMatch) {
|
|
676
|
-
|
|
953
|
+
currentContent = currentContent.replace(
|
|
677
954
|
/^model:\s*.+$/m,
|
|
678
955
|
`model: ${modelValue}`,
|
|
679
956
|
);
|
|
680
957
|
} else {
|
|
681
|
-
|
|
958
|
+
currentContent = currentContent.replace(
|
|
682
959
|
/^(---\n)/,
|
|
683
960
|
`$1model: ${modelValue}\n`,
|
|
684
961
|
);
|
|
685
962
|
}
|
|
686
|
-
|
|
963
|
+
const currentThinkingMatch = currentContent.match(/^thinking:\s*(.+)$/m);
|
|
964
|
+
const currentThinking = currentThinkingMatch ? currentThinkingMatch[1].trim() : "off";
|
|
965
|
+
const thinkingChoice = await ctx.ui.select(
|
|
966
|
+
`Thinking level for ${agentChoice} (current: ${currentThinking})`,
|
|
967
|
+
THINKING_LEVELS,
|
|
968
|
+
);
|
|
969
|
+
if (!thinkingChoice) {
|
|
970
|
+
fs.writeFileSync(filePath, currentContent, "utf-8");
|
|
971
|
+
ctx.ui.notify(
|
|
972
|
+
`${agentChoice} subagent model set to ${modelValue}. Will use on next spawn.`,
|
|
973
|
+
);
|
|
974
|
+
return;
|
|
975
|
+
}
|
|
976
|
+
if (currentThinkingMatch) {
|
|
977
|
+
currentContent = currentContent.replace(
|
|
978
|
+
/^thinking:\s*.+$/m,
|
|
979
|
+
`thinking: ${thinkingChoice}`,
|
|
980
|
+
);
|
|
981
|
+
} else {
|
|
982
|
+
currentContent = currentContent.replace(
|
|
983
|
+
/^(---\n)/,
|
|
984
|
+
`$1thinking: ${thinkingChoice}\n`,
|
|
985
|
+
);
|
|
986
|
+
}
|
|
987
|
+
fs.writeFileSync(filePath, currentContent, "utf-8");
|
|
687
988
|
ctx.ui.notify(
|
|
688
|
-
`${agentChoice} subagent model
|
|
989
|
+
`${agentChoice} subagent: model=${modelValue}, thinking=${thinkingChoice}. Will use on next spawn.`,
|
|
689
990
|
);
|
|
690
991
|
},
|
|
691
992
|
});
|
|
@@ -716,35 +1017,36 @@ export default async function piCustomAgentsPlugin(pi: any): Promise<void> {
|
|
|
716
1017
|
async function handlePlanImplement(ctx: any) {
|
|
717
1018
|
if (!ctx.hasUI) {
|
|
718
1019
|
switchMode(DEFAULT_MODE, ctx);
|
|
719
|
-
pi.sendUserMessage("Execute the plan
|
|
1020
|
+
pi.sendUserMessage("Execute the plan. Follow the steps and test.");
|
|
720
1021
|
return;
|
|
721
1022
|
}
|
|
722
1023
|
const target = await ctx.ui.select(
|
|
723
1024
|
"Implement via",
|
|
724
|
-
["
|
|
1025
|
+
["Code", "Orchestrate"],
|
|
725
1026
|
);
|
|
726
1027
|
if (!target) {
|
|
727
1028
|
ctx.ui.notify("Plan implementation cancelled.");
|
|
728
1029
|
return;
|
|
729
1030
|
}
|
|
730
|
-
const targetMode = target === "
|
|
1031
|
+
const targetMode = target === "Orchestrate" ? "orchestrate" : DEFAULT_MODE;
|
|
731
1032
|
switchMode(targetMode, ctx);
|
|
732
|
-
const msg = target === "
|
|
733
|
-
? "
|
|
734
|
-
: "Execute the plan
|
|
1033
|
+
const msg = target === "Orchestrate"
|
|
1034
|
+
? "Orchestrate focused modules for subagents."
|
|
1035
|
+
: "Execute the plan. Follow the steps and test.";
|
|
735
1036
|
pi.sendMessage(
|
|
736
1037
|
{ customType: "pi-agents-plan-implement", content: PLAN_IMPLEMENT_PROMPT, display: false },
|
|
737
|
-
{ triggerTurn: true },
|
|
738
1038
|
);
|
|
739
|
-
pi.sendUserMessage(msg);
|
|
1039
|
+
pi.sendUserMessage(msg, { deliverAs: "followUp" });
|
|
740
1040
|
}
|
|
741
1041
|
|
|
742
|
-
pi.on("before_agent_start", async () => {
|
|
1042
|
+
pi.on("before_agent_start", async (event: any) => {
|
|
1043
|
+
const augmentedSystemPrompt = event.systemPrompt + PARALLEL_TOOL_CALL_GUIDANCE;
|
|
743
1044
|
if (currentMode !== DEFAULT_MODE && lastMessagedMode !== currentMode) {
|
|
744
1045
|
const mode = MODES[currentMode];
|
|
745
1046
|
lastMessagedMode = currentMode;
|
|
746
|
-
waitingForPlan = currentMode === "
|
|
1047
|
+
waitingForPlan = currentMode === "plan";
|
|
747
1048
|
return {
|
|
1049
|
+
systemPrompt: augmentedSystemPrompt,
|
|
748
1050
|
message: {
|
|
749
1051
|
customType: `pi-agents-enter-${currentMode}`,
|
|
750
1052
|
content: mode.enterMessage,
|
|
@@ -756,6 +1058,7 @@ export default async function piCustomAgentsPlugin(pi: any): Promise<void> {
|
|
|
756
1058
|
const prevMode = MODES[lastMessagedMode];
|
|
757
1059
|
lastMessagedMode = DEFAULT_MODE;
|
|
758
1060
|
return {
|
|
1061
|
+
systemPrompt: augmentedSystemPrompt,
|
|
759
1062
|
message: {
|
|
760
1063
|
customType: "pi-agents-exit",
|
|
761
1064
|
content: prevMode.exitMessage.replace("{mode}", prevMode.label),
|
|
@@ -763,11 +1066,23 @@ export default async function piCustomAgentsPlugin(pi: any): Promise<void> {
|
|
|
763
1066
|
},
|
|
764
1067
|
};
|
|
765
1068
|
}
|
|
1069
|
+
return { systemPrompt: augmentedSystemPrompt };
|
|
1070
|
+
});
|
|
1071
|
+
|
|
1072
|
+
let lastTurnAborted = false;
|
|
1073
|
+
|
|
1074
|
+
pi.on("turn_end", (event: any) => {
|
|
1075
|
+
const msg = event?.message;
|
|
1076
|
+
lastTurnAborted = msg?.stopReason === "aborted" || msg?.role === "assistant" && msg?.content?.stopReason === "aborted";
|
|
766
1077
|
});
|
|
767
1078
|
|
|
768
1079
|
pi.on("agent_settled", async (_event: any, ctx: any) => {
|
|
769
|
-
if (waitingForPlan && currentMode === "
|
|
1080
|
+
if (waitingForPlan && currentMode === "plan") {
|
|
770
1081
|
waitingForPlan = false;
|
|
1082
|
+
if (lastTurnAborted) {
|
|
1083
|
+
lastTurnAborted = false;
|
|
1084
|
+
return;
|
|
1085
|
+
}
|
|
771
1086
|
const action = await showPlanReview(ctx);
|
|
772
1087
|
if (action === "implement") {
|
|
773
1088
|
await handlePlanImplement(ctx);
|
|
@@ -775,7 +1090,7 @@ export default async function piCustomAgentsPlugin(pi: any): Promise<void> {
|
|
|
775
1090
|
waitingForPlan = true;
|
|
776
1091
|
ctx.ui.notify("Edit your plan — type your changes and send.");
|
|
777
1092
|
} else if (action === "reject") {
|
|
778
|
-
ctx.ui.notify("Plan rejected. Staying in
|
|
1093
|
+
ctx.ui.notify("Plan rejected. Staying in plan mode.");
|
|
779
1094
|
}
|
|
780
1095
|
}
|
|
781
1096
|
});
|
|
@@ -794,129 +1109,168 @@ export default async function piCustomAgentsPlugin(pi: any): Promise<void> {
|
|
|
794
1109
|
function installCustomFooter(ctx: any) {
|
|
795
1110
|
if (ctx.mode !== "tui") return;
|
|
796
1111
|
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
|
-
const sessionName = ctx.sessionManager.getSessionName();
|
|
869
|
-
if (sessionName) pwd = `${pwd} \u2022 ${sessionName}`;
|
|
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
|
-
};
|
|
1112
|
+
return {
|
|
1113
|
+
render(width: number): string[] {
|
|
1114
|
+
const PAD = " ";
|
|
1115
|
+
const innerWidth = Math.max(0, width - 2);
|
|
1116
|
+
// Read cached stats instead of iterating all session entries +
|
|
1117
|
+
// calling getContextUsage() every frame. The cache is
|
|
1118
|
+
// recomputed on message_end / tool_execution_end.
|
|
1119
|
+
const stats = footerStatsCache;
|
|
1120
|
+
const totalCost = stats?.totalCost ?? 0;
|
|
1121
|
+
const latestCacheHitRate = stats?.latestCacheHitRate;
|
|
1122
|
+
const contextWindow = stats?.contextWindow ?? 0;
|
|
1123
|
+
const usedTokens = stats?.contextTokens;
|
|
1124
|
+
|
|
1125
|
+
const model = ctx.model;
|
|
1126
|
+
const usedLabel = usedTokens === null || usedTokens === undefined
|
|
1127
|
+
? "?"
|
|
1128
|
+
: formatTokens(usedTokens);
|
|
1129
|
+
const statsParts: string[] = [];
|
|
1130
|
+
statsParts.push(`${usedLabel}/${formatTokens(contextWindow)}`);
|
|
1131
|
+
if (latestCacheHitRate !== undefined) {
|
|
1132
|
+
statsParts.push(`CH${latestCacheHitRate.toFixed(1)}%`);
|
|
1133
|
+
}
|
|
1134
|
+
if (totalCost || (model && ctx.modelRegistry.isUsingOAuth(model))) {
|
|
1135
|
+
statsParts.push(`$${totalCost.toFixed(3)}`);
|
|
1136
|
+
}
|
|
1137
|
+
let statsLeft = isShellMode() ? "shell" : statsParts.join(" ");
|
|
1138
|
+
if (visibleWidth(statsLeft) > innerWidth) {
|
|
1139
|
+
statsLeft = truncateToWidth(statsLeft, innerWidth, "...");
|
|
1140
|
+
}
|
|
1141
|
+
|
|
1142
|
+
const mode = MODES[currentMode];
|
|
1143
|
+
const modeLabel = mode.label.charAt(0).toUpperCase() + mode.label.slice(1);
|
|
1144
|
+
const modelName = model?.name ?? model?.id ?? "no model";
|
|
1145
|
+
const provider = model?.provider ?? "unknown";
|
|
1146
|
+
const thinking = getThinkingLevelFromSession(ctx);
|
|
1147
|
+
const variant = thinking !== "off" ? ` ${thinking}` : "";
|
|
1148
|
+
const rightSide =
|
|
1149
|
+
theme.fg("accent", modeLabel) +
|
|
1150
|
+
` ${theme.fg("dim", "\u2022")} ` +
|
|
1151
|
+
theme.fg("text", `${modelName}${variant}`) +
|
|
1152
|
+
theme.fg("dim", ` ${provider}`);
|
|
1153
|
+
const availableForRight = innerWidth - visibleWidth(statsLeft) - 2;
|
|
1154
|
+
const displayedRight = availableForRight > 0
|
|
1155
|
+
? truncateToWidth(rightSide, availableForRight, "")
|
|
1156
|
+
: "";
|
|
1157
|
+
const padding = " ".repeat(Math.max(
|
|
1158
|
+
0,
|
|
1159
|
+
innerWidth - visibleWidth(statsLeft) - visibleWidth(displayedRight),
|
|
1160
|
+
));
|
|
1161
|
+
const statsLine = PAD + theme.fg("dim", statsLeft) + padding + displayedRight + PAD;
|
|
1162
|
+
|
|
1163
|
+
const cwd = ctx.sessionManager.getCwd();
|
|
1164
|
+
const folderName = cwd.split(/[/\\]/).filter(Boolean).pop() ?? cwd;
|
|
1165
|
+
const sessionName = ctx.sessionManager.getSessionName();
|
|
1166
|
+
const lines = [statsLine];
|
|
1167
|
+
const tps = getLiveTps();
|
|
1168
|
+
const folderLabel = theme.fg("dim", folderName);
|
|
1169
|
+
let folderLine = PAD + folderLabel + PAD;
|
|
1170
|
+
if (tps > 0) {
|
|
1171
|
+
const tpsStr = tps < 10 ? tps.toFixed(1) : tps < 100 ? tps.toFixed(0) : `${Math.round(tps)}`;
|
|
1172
|
+
const tpsText = theme.fg("accent", `${tpsStr} tps`);
|
|
1173
|
+
const tpsPadding = " ".repeat(Math.max(0, innerWidth - visibleWidth(folderLabel) - visibleWidth(tpsText)));
|
|
1174
|
+
folderLine = PAD + folderLabel + tpsPadding + tpsText + PAD;
|
|
1175
|
+
}
|
|
1176
|
+
lines.push(folderLine);
|
|
1177
|
+
if (sessionName) {
|
|
1178
|
+
lines.push(PAD + truncateToWidth(theme.fg("dim", sessionName), innerWidth, theme.fg("dim", "...")) + PAD);
|
|
1179
|
+
}
|
|
1180
|
+
return lines;
|
|
1181
|
+
},
|
|
1182
|
+
};
|
|
889
1183
|
});
|
|
890
1184
|
}
|
|
891
1185
|
|
|
892
|
-
|
|
893
|
-
const
|
|
894
|
-
|
|
895
|
-
|
|
896
|
-
|
|
897
|
-
|
|
1186
|
+
async function restoreSavedModel(ctx: any): Promise<void> {
|
|
1187
|
+
const persisted = readPersistedState();
|
|
1188
|
+
const saved = persisted.model;
|
|
1189
|
+
if (!saved) return;
|
|
1190
|
+
const current = ctx.model as Model<any> | undefined;
|
|
1191
|
+
if (current && modelIdentityString(current) === `${saved.provider}/${saved.modelId}`) {
|
|
1192
|
+
return;
|
|
1193
|
+
}
|
|
1194
|
+
const model = ctx.modelRegistry.find(saved.provider, saved.modelId) as Model<any> | undefined;
|
|
1195
|
+
if (!model || !ctx.modelRegistry.hasConfiguredAuth(model)) {
|
|
1196
|
+
return;
|
|
1197
|
+
}
|
|
1198
|
+
await pi.setModel(model);
|
|
1199
|
+
}
|
|
1200
|
+
|
|
1201
|
+
async function restoreMode(ctx: any): Promise<void> {
|
|
1202
|
+
const persisted = readPersistedState();
|
|
1203
|
+
const savedMode = persisted.mode && persisted.mode in MODES
|
|
1204
|
+
? persisted.mode
|
|
1205
|
+
: getLastModeFromSession(ctx);
|
|
1206
|
+
if (savedMode && savedMode !== DEFAULT_MODE) {
|
|
1207
|
+
currentMode = savedMode;
|
|
1208
|
+
lastMessagedMode = savedMode;
|
|
1209
|
+
pi.setActiveTools(MODES[savedMode].tools);
|
|
898
1210
|
} else {
|
|
899
1211
|
currentMode = DEFAULT_MODE;
|
|
900
1212
|
lastMessagedMode = null;
|
|
901
1213
|
pi.setActiveTools(FULL_TOOLS);
|
|
902
1214
|
}
|
|
1215
|
+
setActiveMode(currentMode);
|
|
1216
|
+
pi.events.emit("pi-ember-ui:mode-change", { mode: currentMode, liveOnly: true });
|
|
1217
|
+
}
|
|
1218
|
+
|
|
1219
|
+
pi.on("session_start", async (_event: any, ctx: any) => {
|
|
1220
|
+
// The TUI can accept input while /resume is still rebinding extensions.
|
|
1221
|
+
// Keep mode switching lazy until all session-bound setup has finished.
|
|
1222
|
+
active_session_manager = ctx.sessionManager;
|
|
1223
|
+
session_ready = false;
|
|
1224
|
+
footerStatsCache = undefined;
|
|
1225
|
+
install_thinking_editor(ctx);
|
|
1226
|
+
await restoreMode(ctx);
|
|
1227
|
+
await restoreSavedModel(ctx);
|
|
1228
|
+
session_ready = true;
|
|
1229
|
+
recompute_footer_stats(ctx);
|
|
1230
|
+
const pending_mode = pending_mode_id;
|
|
1231
|
+
pending_mode_id = undefined;
|
|
1232
|
+
if (pending_mode) apply_mode(pending_mode, ctx);
|
|
1233
|
+
else updateStatus(ctx);
|
|
903
1234
|
installCustomFooter(ctx);
|
|
904
|
-
updateStatus(ctx);
|
|
905
1235
|
});
|
|
906
1236
|
|
|
907
|
-
pi.on("
|
|
908
|
-
const
|
|
909
|
-
if (
|
|
910
|
-
|
|
911
|
-
|
|
912
|
-
|
|
913
|
-
|
|
914
|
-
currentMode = DEFAULT_MODE;
|
|
915
|
-
lastMessagedMode = null;
|
|
916
|
-
pi.setActiveTools(FULL_TOOLS);
|
|
1237
|
+
pi.on("model_select", async (event: any, _ctx: any) => {
|
|
1238
|
+
const model = event.model as Model<any> | undefined;
|
|
1239
|
+
if (!model) return;
|
|
1240
|
+
const persisted = readPersistedState();
|
|
1241
|
+
const identity = { provider: model.provider, modelId: model.id };
|
|
1242
|
+
if (persisted.model?.provider === identity.provider && persisted.model?.modelId === identity.modelId) {
|
|
1243
|
+
return;
|
|
917
1244
|
}
|
|
918
|
-
|
|
919
|
-
|
|
1245
|
+
writePersistedState({ ...persisted, model: identity });
|
|
1246
|
+
});
|
|
1247
|
+
|
|
1248
|
+
// Recompute footer stats when usage/context changes. These events fire
|
|
1249
|
+
// once per assistant message or tool result — not per render frame —
|
|
1250
|
+
// so the O(n) entry iteration + getContextUsage cost is amortized away
|
|
1251
|
+
// from the animation loop.
|
|
1252
|
+
pi.on("message_end", async (_event: any, ctx: any) => {
|
|
1253
|
+
if (is_live_session(ctx)) recompute_footer_stats(ctx);
|
|
1254
|
+
});
|
|
1255
|
+
pi.on("tool_execution_end", async (_event: any, ctx: any) => {
|
|
1256
|
+
if (is_live_session(ctx)) recompute_footer_stats(ctx);
|
|
1257
|
+
});
|
|
1258
|
+
|
|
1259
|
+
pi.on("session_shutdown", (_event: any, ctx: any) => {
|
|
1260
|
+
// Invalidate shortcut contexts before the old runtime is disposed. A Tab
|
|
1261
|
+
// press during /resume is ignored instead of calling setActiveTools or
|
|
1262
|
+
// mutating UI state through a stale session.
|
|
1263
|
+
session_ready = false;
|
|
1264
|
+
active_session_manager = undefined;
|
|
1265
|
+
pending_mode_id = undefined;
|
|
1266
|
+
footerStatsCache = undefined;
|
|
1267
|
+
setShellMode(false);
|
|
1268
|
+
const persisted = readPersistedState();
|
|
1269
|
+
const model = ctx.model as Model<any> | undefined;
|
|
1270
|
+
const modelIdentity = model
|
|
1271
|
+
? { provider: model.provider, modelId: model.id }
|
|
1272
|
+
: persisted.model;
|
|
1273
|
+
writePersistedState({ ...persisted, mode: currentMode, model: modelIdentity });
|
|
920
1274
|
});
|
|
921
1275
|
|
|
922
1276
|
await subagentPlugin(pi);
|