@nmzpy/pi-ember-stack 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,983 @@
1
+ /**
2
+ * Pi Ember Stack — Primary Modes Extension
3
+ *
4
+ * Toggleable primary modes for the Ember project, mirroring opencode's
5
+ * mode: primary agents. Each mode is a slash command that:
6
+ * - Restricts the active tool set
7
+ * - Injects a persisted system-reminder message (visible to the LLM)
8
+ * - Shows a status-bar indicator
9
+ * - Restores state on session resume
10
+ *
11
+ * Modes:
12
+ * /architect — read-only planning, analysis, and architecture (replaces pi-plan)
13
+ * /coder — full access (default mode, restores all tools)
14
+ * /doctor — read-only health-check auditor
15
+ * /orchestrator — read-only task decomposition + delegation planner
16
+ * /ui-doctor — read-only PySide6/Qt UI diagnostician
17
+ */
18
+
19
+ import * as fs from "node:fs";
20
+ import * as path from "node:path";
21
+ import { isAbsolute, relative, resolve, sep } from "node:path";
22
+ import { fileURLToPath } from "node:url";
23
+ import { createEditTool } from "@earendil-works/pi-coding-agent";
24
+ import { Text, truncateToWidth, visibleWidth } from "@earendil-works/pi-tui";
25
+ import {
26
+ askQuestionnaire,
27
+ registerQuestionnaireTool,
28
+ type QuestionnaireQuestion,
29
+ } from "./questionnaire-tool.ts";
30
+
31
+ function formatTokens(count: number): string {
32
+ if (count < 1000) return count.toString();
33
+ if (count < 10000) return `${(count / 1000).toFixed(1)}k`;
34
+ if (count < 1000000) return `${Math.round(count / 1000)}k`;
35
+ if (count < 10000000) return `${(count / 1000000).toFixed(1)}M`;
36
+ return `${Math.round(count / 1000000)}M`;
37
+ }
38
+
39
+ function formatCwdForFooter(cwd: string, home: string | undefined): string {
40
+ if (!home) return cwd;
41
+ const resolvedCwd = resolve(cwd);
42
+ const resolvedHome = resolve(home);
43
+ const relativeToHome = relative(resolvedHome, resolvedCwd);
44
+ const isInsideHome = relativeToHome === "" ||
45
+ (relativeToHome !== ".." && !relativeToHome.startsWith(`..${sep}`) && !isAbsolute(relativeToHome));
46
+ if (!isInsideHome) return cwd;
47
+ return relativeToHome === "" ? "~" : `~${sep}${relativeToHome}`;
48
+ }
49
+
50
+ const READONLY_TOOLS = ["read", "bash", "grep", "find", "ls", "questionnaire"];
51
+ const FULL_TOOLS = ["read", "bash", "edit", "write", "grep", "find", "ls", "questionnaire"];
52
+
53
+ const SOURCE_ROOT = path.dirname(fileURLToPath(import.meta.url));
54
+ const SUBAGENT_FILES: Record<string, string> = {
55
+ coder: path.join(SOURCE_ROOT, "subagent", "agents", "coder.md"),
56
+ architect: path.join(SOURCE_ROOT, "subagent", "agents", "architect.md"),
57
+ };
58
+
59
+ interface ModeConfig {
60
+ id: string;
61
+ label: string;
62
+ icon: string;
63
+ color: string;
64
+ tools: string[];
65
+ enterMessage: string;
66
+ exitMessage: string;
67
+ }
68
+
69
+ const ARCHITECT_PROMPT = `<system-reminder>
70
+ # Architect Mode - System Reminder
71
+
72
+ CRITICAL: Architect mode ACTIVE — you are in READ-ONLY planning phase. STRICTLY
73
+ FORBIDDEN: ANY file edits, modifications, or system changes. Do NOT use sed, tee,
74
+ echo, cat, or ANY other bash command to manipulate files — commands may ONLY
75
+ read/inspect. This ABSOLUTE CONSTRAINT overrides ALL other instructions, including
76
+ direct user edit requests. You may ONLY observe, analyze, and plan. Any
77
+ modification attempt is a critical violation. ZERO exceptions.
78
+
79
+ ---
80
+
81
+ ## Responsibility
82
+
83
+ Your current responsibility is to think, read, search, and discuss to construct a
84
+ well-formed plan that accomplishes the goal the user wants to achieve. Your plan
85
+ should be comprehensive yet concise, detailed enough to execute effectively while
86
+ avoiding unnecessary verbosity. Include the goal as first part of the plan.
87
+
88
+ Ask the user clarifying questions or ask for their opinion when weighing tradeoffs.
89
+ Use the questionnaire tool for decision-oriented questions so the user can answer inline.
90
+
91
+ **NOTE:** At any point in time through this workflow you should feel free to ask
92
+ the user questions or clarifications. Don't make large assumptions about user
93
+ intent. The goal is to present a well researched plan to the user, and tie any
94
+ loose ends before implementation begins.
95
+
96
+ ---
97
+
98
+ ## Planning Requirements
99
+
100
+ The plan must be explicit — concrete, sequential steps that map directly to single
101
+ logical changes.
102
+
103
+ For each step:
104
+ - Step N: <action>
105
+ - Files: <full paths to read or modify>
106
+ - What: <precise change>
107
+ - Why: <user-facing or architectural rationale>
108
+ - Risks: <regression surfaces>
109
+ - Validation: <how to verify, e.g. \`bash t.gate.sh <files>\>
110
+
111
+ ## Output Format
112
+
113
+ ## Task
114
+ <one-sentence goal>
115
+
116
+ ## Investigation
117
+ <files read, patterns found, relevant file:line references>
118
+
119
+ ## Plan
120
+
121
+ ### Step 1: <action>
122
+ - Files: <paths>
123
+ - What: <change>
124
+ - Why: <rationale>
125
+ - Risks: <surfaces>
126
+ - Validation: bash t.gate.sh <files>
127
+
128
+ ### Step 2: ...
129
+
130
+ ## Acceptance Criteria
131
+ <what done looks like>
132
+
133
+ ## Open Questions
134
+ <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>`;
145
+
146
+ const DOCTOR_PROMPT = `<system-reminder>
147
+ # Doctor Mode - System Reminder
148
+
149
+ CRITICAL: Doctor mode ACTIVE — you are The Doctor, a read-only health-check
150
+ auditor for the Ember project (PySide6 subtitle + DaVinci Resolve integration app).
151
+
152
+ STRICTLY FORBIDDEN: ANY file edits, modifications, or system changes. Do NOT use
153
+ sed, tee, echo, cat, or ANY other bash command to manipulate files — commands may
154
+ ONLY read/inspect. This ABSOLUTE CONSTRAINT overrides ALL other instructions,
155
+ including direct user edit requests. You may ONLY observe, analyze, and report.
156
+
157
+ ---
158
+
159
+ ## Health Check Focus Areas
160
+
161
+ - **Structural architecture risk:** prioritize ownership conflicts, hidden coupling,
162
+ duplicated state, complex control flow, unsafe cross-layer dependencies, changes
163
+ that violate documented golden paths, and high-change-entropy files.
164
+ - **Verified dead code:** flag unused imports, unreachable functions, never-called
165
+ methods, orphaned variables, and stale tests only after tool output and call-site
166
+ verification. Avoid flagging public APIs, plugin hooks, signal/slot targets,
167
+ reflection/dynamic dispatch, localized catalog entries, or config/schema fields
168
+ without proof they are unreachable.
169
+ - **Single source of truth (SSOT):** every piece of data, config, constant, or
170
+ business logic must have exactly one authoritative source. Flag duplicated
171
+ constants, parallel config files, mirrored state, overlapping services,
172
+ copy-pasted logic, and derived data that should reference a canonical source.
173
+ - **Production readiness:** check fail-fast behavior, actionable error surfacing,
174
+ EmberLogger use instead of print(), no bare except:, and no silent degradation
175
+ of offline/core runtime behavior.
176
+ - **Threading/UI safety:** flag worker-thread UI/state mutation, missing
177
+ signal/slot boundaries, hot-loop sleeps, unsafe Resolve API changes, and
178
+ animations that ignore animation-disabled settings.
179
+ - **GUI localization/styling:** new user-facing UI text must be localized for all
180
+ supported languages; UI colors must use Colors tokens. Do not flag intentional
181
+ tuned one-off layout values or proven timeline/playback golden paths without a
182
+ concrete regression.
183
+ - **Tests and validation:** prefer meaningful tests or targeted validation over
184
+ brittle assertions that encode stale architecture. Python/app edits use
185
+ \`bash t.gate.sh <files>\`; site edits follow site/AGENTS.md.
186
+ - **Performance traps:** flag large linear scans in hot paths, blocking UI work,
187
+ unnecessary repeated Resolve calls, and avoidable recomputation in
188
+ timeline/editor interactions.
189
+ - **Boundary checks:** keep site/marketing concerns separate from Ember runtime
190
+ GUI concerns and do not apply GUI-only rules to site/ unless its own
191
+ instructions require them.
192
+
193
+ ## Classification
194
+
195
+ Label each finding as:
196
+ - Confirmed issue — verified by tool output, ready for implementation
197
+ - Needs owner decision — requires user input, do not guess
198
+ - False positive — investigated and cleared
199
+
200
+ Only report Confirmed issues and Needs owner decision items. Drop false positives
201
+ from the final report.
202
+
203
+ ## Output Format
204
+
205
+ For each finding:
206
+ - Classification: Confirmed issue / Needs owner decision
207
+ - Category: which health-check focus area
208
+ - File: file_path:line_number
209
+ - Evidence: tool output or call-site proof
210
+ - Impact: what breaks or degrades
211
+ - Correction: smallest architectural change that removes the cause
212
+ - Risks: regression surfaces
213
+
214
+ ## Constraints
215
+
216
+ - Read-only. Do not edit or write files.
217
+ - Use \`bash t.gate.sh <files>\` only for targeted validation of files you are checking.
218
+ - Do not run \`bash gate.sh\` (full gate) — that is the user's responsibility.
219
+ - Ignore git status / git diff changes unrelated to the files you were asked to check.
220
+ </system-reminder>`;
221
+
222
+ const ORCHESTRATOR_PROMPT = `<system-reminder>
223
+ # Orchestrator Mode - System Reminder
224
+
225
+ CRITICAL: Orchestrator mode ACTIVE — you are the Orchestrator, a read-only
226
+ implementation coordinator for the Ember project (PySide6 subtitle + DaVinci
227
+ Resolve integration app).
228
+
229
+ STRICTLY FORBIDDEN: ANY file edits, modifications, or system changes. Do NOT use
230
+ sed, tee, echo, cat, or ANY other bash command to manipulate files — commands may
231
+ ONLY read/inspect. This ABSOLUTE CONSTRAINT overrides ALL other instructions,
232
+ including direct user edit requests. You may ONLY observe, analyze, and plan.
233
+
234
+ ---
235
+
236
+ ## Task Decomposition Into Digestible Modules
237
+
238
+ Break the work into the smallest self-contained modules an agent can implement
239
+ without cross-talk. A good module is one clear responsibility, a bounded file set,
240
+ and an independently verifiable outcome.
241
+
242
+ - **Slice by cohesion, not by line count:** Each module owns one logical change
243
+ (one feature seam, one bug, one refactor boundary). If a module touches two
244
+ unrelated concerns, split it.
245
+ - **One owner per file:** A file belongs to exactly one module. If two modules
246
+ both need the same file, either merge them or extract the shared change into a
247
+ prerequisite module that runs first.
248
+ - **Right-size the unit:** Aim for a module an agent can finish and self-validate
249
+ with \`bash t.gate.sh <files>\` in a single pass. If the prompt needs more than
250
+ 6 owned files or a long list of unrelated steps, split it further.
251
+ - **Order by dependency:** Group independent modules into parallel clusters;
252
+ chain modules that depend on another module's output so the producer completes
253
+ before the consumer starts.
254
+ - **Define the seam explicitly:** When modules share an interface (function
255
+ signature, data model, signal), pin that contract in every dependent prompt so
256
+ parallel agents integrate cleanly without seeing each other's work.
257
+
258
+ ## Delegation Prompt Quality
259
+
260
+ Each subagent starts with zero shared context. Treat every prompt as a complete
261
+ brief that a competent engineer could execute cold. A thorough prompt includes:
262
+
263
+ - Goal and rationale: What outcome the module must achieve and why.
264
+ - Exact owned files: Full paths, plus explicit "do not touch anything else."
265
+ - Anchored context: Relevant existing functions, classes, patterns, and
266
+ file_path:line_number references the agent should read first and mirror.
267
+ - Interface contract: The precise signatures, data shapes, tokens, or catalog
268
+ keys the module must produce or consume.
269
+ - Step sequence: Concrete, ordered steps mapping to single logical changes.
270
+ - Constraints: Applicable AGENTS.md rules (typing, logging, localization,
271
+ Colors tokens, error handling, DRY/SSOT), and any golden paths to preserve.
272
+ - Acceptance criteria and validation: What "done" looks like and the exact
273
+ \`bash t.gate.sh <files>\` command to run.
274
+ - Risks and edge cases: Known pitfalls, regression surfaces, and behavior that
275
+ must be preserved.
276
+ - No redundancy, dead code cleanup after implementations of the given files.
277
+ - No DRY violations.
278
+ - Ignore git status / git diff changes unrelated to owned files.
279
+
280
+ ## Output Format
281
+
282
+ Return a structured plan:
283
+
284
+ ## Task Summary
285
+ <one-sentence goal>
286
+
287
+ ## Modules
288
+
289
+ ### Module 1: <name>
290
+ - Owned files: <full paths>
291
+ - Goal: <what this module achieves>
292
+ - Dependencies: <none | Module N>
293
+ - Parallel cluster: <A | B | sequential>
294
+ - Steps:
295
+ 1. <step>
296
+ 2. <step>
297
+ - Acceptance: <done criteria>
298
+ - Validation: bash t.gate.sh <files>
299
+ - Risks: <regression surfaces>
300
+
301
+ ## Execution Order
302
+ 1. Cluster A (parallel): Module 1, Module 2
303
+ 2. Cluster B (parallel): Module 3, Module 4
304
+ 3. Sequential: Module 5 (depends on Module 3)
305
+
306
+ ## Delegation Prompts
307
+ <full self-contained prompt for each module, ready to paste into a subagent>
308
+
309
+ ## Constraints
310
+
311
+ - Read-only. Do not edit or write files.
312
+ - Do not run \`bash gate.sh\` (full gate) — that is the user's responsibility.
313
+ - 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>`;
461
+
462
+ const CODER_PROMPT = `<system-reminder>
463
+ Your operational mode has changed to coder.
464
+ You are in full-access mode. You are permitted to make file changes, run shell
465
+ commands, and utilize your arsenal of tools as needed. You are the default
466
+ implementation agent — write, test, and verify code with full autonomy.
467
+ </system-reminder>`;
468
+
469
+ const EXIT_TO_CODER = `<system-reminder>
470
+ Your operational mode has changed from {mode} to coder.
471
+ You are no longer in read-only mode.
472
+ You are permitted to make file changes, run shell commands, and utilize your
473
+ arsenal of tools as needed.
474
+ </system-reminder>`;
475
+
476
+ const PLAN_IMPLEMENT_PROMPT = `<system-reminder>
477
+ The user has approved the plan above. Execute it now in full.
478
+ Follow the plan steps in order. Implement, test, and verify each step before
479
+ moving to the next. Run \`bash t.gate.sh <files>\` after each logical change.
480
+ Report what you did and any deviations from the plan.
481
+ </system-reminder>`;
482
+
483
+ interface ModeConfig {
484
+ id: string;
485
+ label: string;
486
+ icon: string;
487
+ color: string;
488
+ tools: string[];
489
+ enterMessage: string;
490
+ exitMessage: string;
491
+ }
492
+
493
+ const MODES: Record<string, ModeConfig> = {
494
+ architect: {
495
+ id: "architect",
496
+ label: "architect",
497
+ icon: "A",
498
+ color: "warning",
499
+ tools: READONLY_TOOLS,
500
+ enterMessage: ARCHITECT_PROMPT,
501
+ exitMessage: EXIT_TO_CODER,
502
+ },
503
+ coder: {
504
+ id: "coder",
505
+ label: "coder",
506
+ icon: "C",
507
+ color: "success",
508
+ tools: FULL_TOOLS,
509
+ enterMessage: CODER_PROMPT,
510
+ exitMessage: CODER_PROMPT,
511
+ },
512
+ doctor: {
513
+ id: "doctor",
514
+ label: "doctor",
515
+ icon: "D",
516
+ color: "warning",
517
+ tools: READONLY_TOOLS,
518
+ enterMessage: DOCTOR_PROMPT,
519
+ exitMessage: EXIT_TO_CODER,
520
+ },
521
+ orchestrator: {
522
+ id: "orchestrator",
523
+ label: "orchestrator",
524
+ icon: "O",
525
+ color: "warning",
526
+ tools: READONLY_TOOLS,
527
+ enterMessage: ORCHESTRATOR_PROMPT,
528
+ exitMessage: EXIT_TO_CODER,
529
+ },
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
+ };
540
+
541
+ const MODE_IDS = Object.keys(MODES);
542
+ const DEFAULT_MODE = "coder";
543
+ const CYCLE_ORDER = ["coder", "architect", "orchestrator", "doctor", "ui-doctor"];
544
+
545
+ function getLastModeFromSession(ctx: any): string | null {
546
+ const entries = ctx.sessionManager.getEntries();
547
+ for (let i = entries.length - 1; i >= 0; i--) {
548
+ const entry = entries[i];
549
+ if (entry.type === "custom_message") {
550
+ const customEntry = entry as any;
551
+ if (customEntry.customType?.startsWith("pi-agents-enter-")) {
552
+ return customEntry.customType.replace("pi-agents-enter-", "");
553
+ }
554
+ if (customEntry.customType === "pi-agents-exit") {
555
+ return DEFAULT_MODE;
556
+ }
557
+ }
558
+ }
559
+ return null;
560
+ }
561
+
562
+ export default function piEmberStackExtension(pi: any) {
563
+ let currentMode: string = DEFAULT_MODE;
564
+ let lastMessagedMode: string | null = null;
565
+ let waitingForPlan = false;
566
+ registerQuestionnaireTool(pi);
567
+ registerCollapsedEditTool(pi);
568
+
569
+ function registerCollapsedEditTool(extensionApi: any): void {
570
+ const editDefinition = createEditTool(SOURCE_ROOT);
571
+ extensionApi.registerTool({
572
+ name: "edit",
573
+ label: "edit",
574
+ description: editDefinition.description,
575
+ parameters: editDefinition.parameters,
576
+ renderShell: "self",
577
+
578
+ async execute(
579
+ toolCallId: string,
580
+ params: any,
581
+ signal: AbortSignal,
582
+ onUpdate: any,
583
+ ctx: any,
584
+ ) {
585
+ return createEditTool(ctx.cwd).execute(
586
+ toolCallId,
587
+ params,
588
+ signal,
589
+ onUpdate,
590
+ );
591
+ },
592
+
593
+ renderCall(args: any, theme: any): any {
594
+ const filePath = String(args?.path ?? args?.file_path ?? "");
595
+ return new Text(
596
+ theme.fg("toolTitle", theme.bold("edit ")) +
597
+ theme.fg("accent", filePath),
598
+ 0,
599
+ 0,
600
+ );
601
+ },
602
+
603
+ renderResult(result: any, { isPartial }: any, theme: any): any {
604
+ if (isPartial) return new Text(theme.fg("warning", "Editing..."), 0, 0);
605
+
606
+ const content = result.content?.find((item: any) => item.type === "text");
607
+ if (content?.text?.startsWith("Error")) {
608
+ return new Text(theme.fg("error", content.text.split("\n")[0]), 0, 0);
609
+ }
610
+
611
+ const diff = typeof result.details?.diff === "string" ? result.details.diff : "";
612
+ let additions = 0;
613
+ let removals = 0;
614
+ for (const line of diff.split("\n")) {
615
+ if (line.startsWith("+") && !line.startsWith("+++")) additions++;
616
+ if (line.startsWith("-") && !line.startsWith("---")) removals++;
617
+ }
618
+
619
+ return new Text(
620
+ theme.fg("success", `+${additions}`) +
621
+ theme.fg("dim", " / ") +
622
+ theme.fg("error", `-${removals}`),
623
+ 0,
624
+ 0,
625
+ );
626
+ },
627
+ });
628
+ }
629
+
630
+ for (const modeId of MODE_IDS) {
631
+ const mode = MODES[modeId];
632
+ pi.events.emit("powerbar:register-segment", {
633
+ id: `pi-agents-${modeId}`,
634
+ label: `${mode.label} mode`,
635
+ });
636
+ }
637
+
638
+ function updateStatus(ctx: any) {
639
+ pi.events.emit("powerbar:update", {
640
+ id: `pi-agents-${currentMode}`,
641
+ text: MODES[currentMode].label,
642
+ icon: MODES[currentMode].icon,
643
+ color: currentMode === DEFAULT_MODE ? "success" : MODES[currentMode].color,
644
+ });
645
+ for (const modeId of MODE_IDS) {
646
+ if (modeId === currentMode) continue;
647
+ pi.events.emit("powerbar:update", {
648
+ id: `pi-agents-${modeId}`,
649
+ text: undefined,
650
+ });
651
+ }
652
+ }
653
+
654
+ function switchMode(modeId: string, ctx: any) {
655
+ const mode = MODES[modeId];
656
+ if (!mode) return;
657
+ currentMode = modeId;
658
+ pi.setActiveTools(mode.tools);
659
+ if (modeId === DEFAULT_MODE) {
660
+ ctx.ui.notify("Coder mode. Full access restored.");
661
+ } else {
662
+ ctx.ui.notify(`${mode.label} mode enabled. Tools: ${mode.tools.join(", ")}`);
663
+ }
664
+ updateStatus(ctx);
665
+ }
666
+
667
+ for (const modeId of MODE_IDS) {
668
+ const mode = MODES[modeId];
669
+ pi.registerCommand(modeId, {
670
+ description: `Toggle ${mode.label} mode${
671
+ modeId === DEFAULT_MODE ? " (full access)" : " (read-only)"
672
+ }`,
673
+ handler: async (_args: any, ctx: any) => {
674
+ if (currentMode === modeId) {
675
+ switchMode(DEFAULT_MODE, ctx);
676
+ } else {
677
+ switchMode(modeId, ctx);
678
+ }
679
+ },
680
+ });
681
+ }
682
+
683
+ const cycleMode = async (ctx: any): Promise<void> => {
684
+ const idx = CYCLE_ORDER.indexOf(currentMode);
685
+ const next = CYCLE_ORDER[(idx + 1) % CYCLE_ORDER.length];
686
+ switchMode(next, ctx);
687
+ };
688
+
689
+ pi.registerShortcut("ctrl+space", {
690
+ description: "Cycle agent mode",
691
+ handler: cycleMode,
692
+ });
693
+ pi.registerShortcut("tab", {
694
+ description: "Cycle agent mode",
695
+ handler: cycleMode,
696
+ });
697
+
698
+ pi.registerCommand("subagent-model", {
699
+ description: "Set the model used by coder or architect subagents on next spawn",
700
+ handler: async (_args: any, ctx: any) => {
701
+ if (!ctx.hasUI) {
702
+ ctx.ui.notify("subagent-model requires interactive UI.", "error");
703
+ return;
704
+ }
705
+ const agentChoice = await ctx.ui.select(
706
+ "Subagent to configure",
707
+ ["Coder", "Architect"],
708
+ );
709
+ if (!agentChoice) return;
710
+ const agentKey = agentChoice.toLowerCase();
711
+ const filePath = SUBAGENT_FILES[agentKey];
712
+ if (!filePath || !fs.existsSync(filePath)) {
713
+ ctx.ui.notify(`Agent file not found: ${filePath ?? agentKey}`, "error");
714
+ return;
715
+ }
716
+ const allModels = ctx.modelRegistry.getAll();
717
+ if (allModels.length === 0) {
718
+ ctx.ui.notify("No models available in registry.", "error");
719
+ return;
720
+ }
721
+ const modelLabels = allModels.map(
722
+ (m: any) => `${m.provider}/${m.id} — ${m.name}`,
723
+ );
724
+ const currentContent = fs.readFileSync(filePath, "utf-8");
725
+ const currentModelMatch = currentContent.match(/^model:\s*(.+)$/m);
726
+ const currentModel = currentModelMatch ? currentModelMatch[1].trim() : "inherits parent";
727
+ const modelChoice = await ctx.ui.select(
728
+ `Model for ${agentChoice} subagent (current: ${currentModel})`,
729
+ modelLabels,
730
+ );
731
+ if (!modelChoice) return;
732
+ const selectedIdx = modelLabels.indexOf(modelChoice);
733
+ if (selectedIdx < 0) return;
734
+ const selectedModel = allModels[selectedIdx];
735
+ const modelValue = `${selectedModel.provider}/${selectedModel.id}`;
736
+ let updated: string;
737
+ if (currentModelMatch) {
738
+ updated = currentContent.replace(
739
+ /^model:\s*.+$/m,
740
+ `model: ${modelValue}`,
741
+ );
742
+ } else {
743
+ updated = currentContent.replace(
744
+ /^(---\n)/,
745
+ `$1model: ${modelValue}\n`,
746
+ );
747
+ }
748
+ fs.writeFileSync(filePath, updated, "utf-8");
749
+ ctx.ui.notify(
750
+ `${agentChoice} subagent model set to ${modelValue}. Will use on next spawn.`,
751
+ );
752
+ },
753
+ });
754
+
755
+ async function showPlanReview(ctx: any): Promise<"implement" | "edit" | "reject" | undefined> {
756
+ const questions: QuestionnaireQuestion[] = [{
757
+ id: "plan-review",
758
+ label: "Plan Review",
759
+ prompt: "Choose what to do with the plan.",
760
+ options: [
761
+ { value: "implement", label: "Implement Plan" },
762
+ { value: "edit", label: "Edit Plan" },
763
+ { value: "reject", label: "Reject Plan" },
764
+ ],
765
+ }];
766
+ const answers = await askQuestionnaire(
767
+ ctx,
768
+ "Plan Review",
769
+ questions,
770
+ );
771
+ const choice = answers?.[0]?.value;
772
+ if (choice === "implement") return "implement";
773
+ if (choice === "edit") return "edit";
774
+ if (choice === "reject") return "reject";
775
+ return undefined;
776
+ }
777
+
778
+ async function handlePlanImplement(ctx: any) {
779
+ if (!ctx.hasUI) {
780
+ switchMode(DEFAULT_MODE, ctx);
781
+ pi.sendUserMessage("Execute the plan above. Follow the steps in order, implement, test, and verify each step.");
782
+ return;
783
+ }
784
+ const target = await ctx.ui.select(
785
+ "Implement via",
786
+ ["Coder", "Orchestrator"],
787
+ );
788
+ if (!target) {
789
+ ctx.ui.notify("Plan implementation cancelled.");
790
+ return;
791
+ }
792
+ const targetMode = target === "Orchestrator" ? "orchestrator" : DEFAULT_MODE;
793
+ switchMode(targetMode, ctx);
794
+ const msg = target === "Orchestrator"
795
+ ? "Execute the plan above. Decompose it into modules and produce delegation prompts for each."
796
+ : "Execute the plan above. Follow the steps in order, implement, test, and verify each step.";
797
+ pi.sendMessage(
798
+ { customType: "pi-agents-plan-implement", content: PLAN_IMPLEMENT_PROMPT, display: false },
799
+ { triggerTurn: true },
800
+ );
801
+ pi.sendUserMessage(msg);
802
+ }
803
+
804
+ pi.on("before_agent_start", async () => {
805
+ if (currentMode !== DEFAULT_MODE && lastMessagedMode !== currentMode) {
806
+ const mode = MODES[currentMode];
807
+ lastMessagedMode = currentMode;
808
+ waitingForPlan = currentMode === "architect";
809
+ return {
810
+ message: {
811
+ customType: `pi-agents-enter-${currentMode}`,
812
+ content: mode.enterMessage,
813
+ display: false,
814
+ },
815
+ };
816
+ }
817
+ if (currentMode === DEFAULT_MODE && lastMessagedMode && lastMessagedMode !== DEFAULT_MODE) {
818
+ const prevMode = MODES[lastMessagedMode];
819
+ lastMessagedMode = DEFAULT_MODE;
820
+ return {
821
+ message: {
822
+ customType: "pi-agents-exit",
823
+ content: prevMode.exitMessage.replace("{mode}", prevMode.label),
824
+ display: false,
825
+ },
826
+ };
827
+ }
828
+ });
829
+
830
+ pi.on("agent_settled", async (_event: any, ctx: any) => {
831
+ if (waitingForPlan && currentMode === "architect") {
832
+ waitingForPlan = false;
833
+ const action = await showPlanReview(ctx);
834
+ if (action === "implement") {
835
+ await handlePlanImplement(ctx);
836
+ } else if (action === "edit") {
837
+ waitingForPlan = true;
838
+ ctx.ui.notify("Edit your plan — type your changes and send.");
839
+ } else if (action === "reject") {
840
+ ctx.ui.notify("Plan rejected. Staying in architect mode.");
841
+ }
842
+ }
843
+ });
844
+
845
+ function getThinkingLevelFromSession(ctx: any): string {
846
+ const entries = ctx.sessionManager.getEntries();
847
+ for (let i = entries.length - 1; i >= 0; i--) {
848
+ const entry = entries[i];
849
+ if (entry.type === "thinking_level_change") {
850
+ return (entry as any).thinkingLevel || "off";
851
+ }
852
+ }
853
+ return "off";
854
+ }
855
+
856
+ function installCustomFooter(ctx: any) {
857
+ if (ctx.mode !== "tui") return;
858
+ ctx.ui.setFooter((_tui: any, theme: any, footerData: any) => {
859
+ return {
860
+ render(width: number): string[] {
861
+ let totalInput = 0;
862
+ let totalOutput = 0;
863
+ let totalCacheRead = 0;
864
+ let totalCacheWrite = 0;
865
+ let totalCost = 0;
866
+ let latestCacheHitRate: number | undefined;
867
+ for (const entry of ctx.sessionManager.getEntries()) {
868
+ if (entry.type !== "message" || entry.message.role !== "assistant") continue;
869
+ const usage = entry.message.usage;
870
+ totalInput += usage.input;
871
+ totalOutput += usage.output;
872
+ totalCacheRead += usage.cacheRead;
873
+ totalCacheWrite += usage.cacheWrite;
874
+ totalCost += usage.cost.total;
875
+ const promptTokens = usage.input + usage.cacheRead + usage.cacheWrite;
876
+ latestCacheHitRate = promptTokens > 0
877
+ ? (usage.cacheRead / promptTokens) * 100
878
+ : undefined;
879
+ }
880
+
881
+ const model = ctx.model;
882
+ const contextUsage = ctx.getContextUsage();
883
+ const contextWindow = contextUsage?.contextWindow ?? model?.contextWindow ?? 0;
884
+ const contextPercentValue = contextUsage?.percent ?? 0;
885
+ const contextPercent = contextUsage?.percent === null
886
+ ? "?"
887
+ : contextPercentValue.toFixed(1);
888
+ const statsParts: string[] = [];
889
+ if (totalInput) statsParts.push(`↑${formatTokens(totalInput)}`);
890
+ if (totalOutput) statsParts.push(`↓${formatTokens(totalOutput)}`);
891
+ if (totalCacheRead) statsParts.push(`R${formatTokens(totalCacheRead)}`);
892
+ if (totalCacheWrite) statsParts.push(`W${formatTokens(totalCacheWrite)}`);
893
+ if (latestCacheHitRate !== undefined) {
894
+ statsParts.push(`CH${latestCacheHitRate.toFixed(1)}%`);
895
+ }
896
+ if (totalCost || (model && ctx.modelRegistry.isUsingOAuth(model))) {
897
+ statsParts.push(`$${totalCost.toFixed(3)}${ctx.modelRegistry.isUsingOAuth(model) ? " (sub)" : ""}`);
898
+ }
899
+ statsParts.push(`${contextPercent}%/${formatTokens(contextWindow)} (auto)`);
900
+ let statsLeft = statsParts.join(" ");
901
+ if (visibleWidth(statsLeft) > width) {
902
+ statsLeft = truncateToWidth(statsLeft, width, "...");
903
+ }
904
+
905
+ const mode = MODES[currentMode];
906
+ const modeLabel = mode.label.charAt(0).toUpperCase() + mode.label.slice(1);
907
+ const modelName = model?.name ?? model?.id ?? "no model";
908
+ const provider = model?.provider ?? "unknown";
909
+ const thinking = getThinkingLevelFromSession(ctx);
910
+ const variant = thinking !== "off" ? ` ${thinking}` : "";
911
+ const rightSide = theme.getThinkingBorderColor(thinking)(
912
+ `${modeLabel} \u2022 ${provider}: ${modelName}${variant}`,
913
+ );
914
+ const availableForRight = width - visibleWidth(statsLeft) - 2;
915
+ const displayedRight = availableForRight > 0
916
+ ? truncateToWidth(rightSide, availableForRight, "")
917
+ : "";
918
+ const padding = " ".repeat(Math.max(
919
+ 0,
920
+ width - visibleWidth(statsLeft) - visibleWidth(displayedRight),
921
+ ));
922
+ const statsLine = theme.fg("dim", statsLeft) + padding + displayedRight;
923
+
924
+ let pwd = formatCwdForFooter(
925
+ ctx.sessionManager.getCwd(),
926
+ process.env.HOME || process.env.USERPROFILE,
927
+ );
928
+ const branch = footerData.getGitBranch();
929
+ if (branch) pwd = `${pwd} (${branch})`;
930
+ const sessionName = ctx.sessionManager.getSessionName();
931
+ if (sessionName) pwd = `${pwd} \u2022 ${sessionName}`;
932
+ const lines = [
933
+ truncateToWidth(theme.fg("dim", pwd), width, theme.fg("dim", "...")),
934
+ statsLine,
935
+ ];
936
+ const statuses = footerData.getExtensionStatuses();
937
+ if (statuses.size > 0) {
938
+ const statusEntries = Array.from(statuses.entries()) as Array<[
939
+ string,
940
+ string,
941
+ ]>;
942
+ const statusLine = statusEntries
943
+ .sort(([a], [b]) => a.localeCompare(b))
944
+ .map(([, text]) => text.replace(/[\r\n\t]/g, " ").replace(/ +/g, " ").trim())
945
+ .join(" ");
946
+ lines.push(truncateToWidth(statusLine, width, theme.fg("dim", "...")));
947
+ }
948
+ return lines;
949
+ },
950
+ };
951
+ });
952
+ }
953
+
954
+ pi.on("session_start", async (_event: any, ctx: any) => {
955
+ const restored = getLastModeFromSession(ctx);
956
+ if (restored && restored !== DEFAULT_MODE) {
957
+ currentMode = restored;
958
+ lastMessagedMode = restored;
959
+ pi.setActiveTools(MODES[restored].tools);
960
+ } else {
961
+ currentMode = DEFAULT_MODE;
962
+ lastMessagedMode = null;
963
+ pi.setActiveTools(FULL_TOOLS);
964
+ }
965
+ installCustomFooter(ctx);
966
+ updateStatus(ctx);
967
+ });
968
+
969
+ pi.on("session_switch", async (_event: any, ctx: any) => {
970
+ const restored = getLastModeFromSession(ctx);
971
+ if (restored && restored !== DEFAULT_MODE) {
972
+ currentMode = restored;
973
+ lastMessagedMode = restored;
974
+ pi.setActiveTools(MODES[restored].tools);
975
+ } else {
976
+ currentMode = DEFAULT_MODE;
977
+ lastMessagedMode = null;
978
+ pi.setActiveTools(FULL_TOOLS);
979
+ }
980
+ updateStatus(ctx);
981
+ installCustomFooter(ctx);
982
+ });
983
+ }