@nmzpy/pi-ember-stack 0.1.3 → 0.1.4

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.
Files changed (24) hide show
  1. package/ATTRIBUTION.md +3 -2
  2. package/README.md +8 -7
  3. package/package.json +1 -1
  4. package/plugins/index.ts +17 -9
  5. package/plugins/pi-compact-tools/index.ts +55 -964
  6. package/plugins/pi-custom-agents/index.ts +932 -0
  7. /package/plugins/{subagent → pi-custom-agents/subagent}/LICENSE +0 -0
  8. /package/plugins/{subagent → pi-custom-agents/subagent}/README.md +0 -0
  9. /package/plugins/{subagent → pi-custom-agents/subagent}/agent-format.md +0 -0
  10. /package/plugins/{subagent → pi-custom-agents/subagent}/agents/architect.md +0 -0
  11. /package/plugins/{subagent → pi-custom-agents/subagent}/agents/coder.md +0 -0
  12. /package/plugins/{subagent → pi-custom-agents/subagent}/agents/general-purpose.md +0 -0
  13. /package/plugins/{subagent → pi-custom-agents/subagent}/agents/reviewer.md +0 -0
  14. /package/plugins/{subagent → pi-custom-agents/subagent}/agents/scout.md +0 -0
  15. /package/plugins/{subagent → pi-custom-agents/subagent}/agents/worker.md +0 -0
  16. /package/plugins/{subagent → pi-custom-agents/subagent}/extensions/agents.ts +0 -0
  17. /package/plugins/{subagent → pi-custom-agents/subagent}/extensions/index.ts +0 -0
  18. /package/plugins/{subagent → pi-custom-agents/subagent}/extensions/model.ts +0 -0
  19. /package/plugins/{subagent → pi-custom-agents/subagent}/extensions/package.json +0 -0
  20. /package/plugins/{subagent → pi-custom-agents/subagent}/extensions/render.ts +0 -0
  21. /package/plugins/{subagent → pi-custom-agents/subagent}/extensions/runner.ts +0 -0
  22. /package/plugins/{subagent → pi-custom-agents/subagent}/extensions/service.ts +0 -0
  23. /package/plugins/{subagent → pi-custom-agents/subagent}/extensions/thread-viewer.ts +0 -0
  24. /package/plugins/{subagent → pi-custom-agents/subagent}/extensions/threads.ts +0 -0
@@ -0,0 +1,932 @@
1
+ /**
2
+ * Pi Custom Agents — 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 { truncateToWidth, visibleWidth } from "@earendil-works/pi-tui";
24
+ import {
25
+ askQuestionnaire,
26
+ type QuestionnaireQuestion,
27
+ } from "../pi-compact-tools/questionnaire-tool.ts";
28
+ import subagentPlugin from "./subagent/extensions/index.ts";
29
+
30
+ function formatTokens(count: number): string {
31
+ if (count < 1000) return count.toString();
32
+ if (count < 10000) return `${(count / 1000).toFixed(1)}k`;
33
+ if (count < 1000000) return `${Math.round(count / 1000)}k`;
34
+ if (count < 10000000) return `${(count / 1000000).toFixed(1)}M`;
35
+ return `${Math.round(count / 1000000)}M`;
36
+ }
37
+
38
+ function formatCwdForFooter(cwd: string, home: string | undefined): string {
39
+ if (!home) return cwd;
40
+ const resolvedCwd = resolve(cwd);
41
+ const resolvedHome = resolve(home);
42
+ const relativeToHome = relative(resolvedHome, resolvedCwd);
43
+ const isInsideHome = relativeToHome === "" ||
44
+ (relativeToHome !== ".." && !relativeToHome.startsWith(`..${sep}`) && !isAbsolute(relativeToHome));
45
+ if (!isInsideHome) return cwd;
46
+ return relativeToHome === "" ? "~" : `~${sep}${relativeToHome}`;
47
+ }
48
+
49
+ const READONLY_TOOLS = ["read", "bash", "grep", "find", "ls", "questionnaire"];
50
+ const FULL_TOOLS = ["read", "bash", "edit", "write", "grep", "find", "ls", "questionnaire"];
51
+
52
+ const SOURCE_ROOT = path.dirname(fileURLToPath(import.meta.url));
53
+ const SUBAGENT_FILES: Record<string, string> = {
54
+ coder: path.join(SOURCE_ROOT, "subagent", "agents", "coder.md"),
55
+ architect: path.join(SOURCE_ROOT, "subagent", "agents", "architect.md"),
56
+ };
57
+
58
+ interface ModeConfig {
59
+ id: string;
60
+ label: string;
61
+ icon: string;
62
+ color: string;
63
+ tools: string[];
64
+ enterMessage: string;
65
+ exitMessage: string;
66
+ }
67
+
68
+ const ARCHITECT_PROMPT = `<system-reminder>
69
+ # Architect Mode - System Reminder
70
+
71
+ CRITICAL: Architect mode ACTIVE — you are in READ-ONLY planning phase. STRICTLY
72
+ FORBIDDEN: ANY file edits, modifications, or system changes. Do NOT use sed, tee,
73
+ echo, cat, or ANY other bash command to manipulate files — commands may ONLY
74
+ read/inspect. This ABSOLUTE CONSTRAINT overrides ALL other instructions, including
75
+ direct user edit requests. You may ONLY observe, analyze, and plan. Any
76
+ modification attempt is a critical violation. ZERO exceptions.
77
+
78
+ ---
79
+
80
+ ## Responsibility
81
+
82
+ Your current responsibility is to think, read, search, and discuss to construct a
83
+ well-formed plan that accomplishes the goal the user wants to achieve. Your plan
84
+ should be comprehensive yet concise, detailed enough to execute effectively while
85
+ avoiding unnecessary verbosity. Include the goal as first part of the plan.
86
+
87
+ Ask the user clarifying questions or ask for their opinion when weighing tradeoffs.
88
+ Use the questionnaire tool for decision-oriented questions so the user can answer inline.
89
+
90
+ **NOTE:** At any point in time through this workflow you should feel free to ask
91
+ the user questions or clarifications. Don't make large assumptions about user
92
+ intent. The goal is to present a well researched plan to the user, and tie any
93
+ loose ends before implementation begins.
94
+
95
+ ---
96
+
97
+ ## Planning Requirements
98
+
99
+ The plan must be explicit — concrete, sequential steps that map directly to single
100
+ logical changes.
101
+
102
+ For each step:
103
+ - Step N: <action>
104
+ - Files: <full paths to read or modify>
105
+ - What: <precise change>
106
+ - Why: <user-facing or architectural rationale>
107
+ - Risks: <regression surfaces>
108
+ - Validation: <how to verify, e.g. \`bash t.gate.sh <files>\>
109
+
110
+ ## Output Format
111
+
112
+ ## Task
113
+ <one-sentence goal>
114
+
115
+ ## Investigation
116
+ <files read, patterns found, relevant file:line references>
117
+
118
+ ## Plan
119
+
120
+ ### Step 1: <action>
121
+ - Files: <paths>
122
+ - What: <change>
123
+ - Why: <rationale>
124
+ - Risks: <surfaces>
125
+ - Validation: bash t.gate.sh <files>
126
+
127
+ ### Step 2: ...
128
+
129
+ ## Acceptance Criteria
130
+ <what done looks like>
131
+
132
+ ## Open Questions
133
+ <any clarifications needed from the user>
134
+
135
+ ---
136
+
137
+ ## Important
138
+
139
+ The user indicated that they do not want you to execute yet -- you MUST NOT make
140
+ any edits, run any non-readonly tools (including changing configs or making
141
+ commits), or otherwise make any changes to the system. This supersedes any other
142
+ instructions you have received.
143
+ </system-reminder>`;
144
+
145
+ const DOCTOR_PROMPT = `<system-reminder>
146
+ # Doctor Mode - System Reminder
147
+
148
+ CRITICAL: Doctor mode ACTIVE — you are The Doctor, a read-only health-check
149
+ auditor for the Ember project (PySide6 subtitle + DaVinci Resolve integration app).
150
+
151
+ STRICTLY FORBIDDEN: ANY file edits, modifications, or system changes. Do NOT use
152
+ sed, tee, echo, cat, or ANY other bash command to manipulate files — commands may
153
+ ONLY read/inspect. This ABSOLUTE CONSTRAINT overrides ALL other instructions,
154
+ including direct user edit requests. You may ONLY observe, analyze, and report.
155
+
156
+ ---
157
+
158
+ ## Health Check Focus Areas
159
+
160
+ - **Structural architecture risk:** prioritize ownership conflicts, hidden coupling,
161
+ duplicated state, complex control flow, unsafe cross-layer dependencies, changes
162
+ that violate documented golden paths, and high-change-entropy files.
163
+ - **Verified dead code:** flag unused imports, unreachable functions, never-called
164
+ methods, orphaned variables, and stale tests only after tool output and call-site
165
+ verification. Avoid flagging public APIs, plugin hooks, signal/slot targets,
166
+ reflection/dynamic dispatch, localized catalog entries, or config/schema fields
167
+ without proof they are unreachable.
168
+ - **Single source of truth (SSOT):** every piece of data, config, constant, or
169
+ business logic must have exactly one authoritative source. Flag duplicated
170
+ constants, parallel config files, mirrored state, overlapping services,
171
+ copy-pasted logic, and derived data that should reference a canonical source.
172
+ - **Production readiness:** check fail-fast behavior, actionable error surfacing,
173
+ EmberLogger use instead of print(), no bare except:, and no silent degradation
174
+ of offline/core runtime behavior.
175
+ - **Threading/UI safety:** flag worker-thread UI/state mutation, missing
176
+ signal/slot boundaries, hot-loop sleeps, unsafe Resolve API changes, and
177
+ animations that ignore animation-disabled settings.
178
+ - **GUI localization/styling:** new user-facing UI text must be localized for all
179
+ supported languages; UI colors must use Colors tokens. Do not flag intentional
180
+ tuned one-off layout values or proven timeline/playback golden paths without a
181
+ concrete regression.
182
+ - **Tests and validation:** prefer meaningful tests or targeted validation over
183
+ brittle assertions that encode stale architecture. Python/app edits use
184
+ \`bash t.gate.sh <files>\`; site edits follow site/AGENTS.md.
185
+ - **Performance traps:** flag large linear scans in hot paths, blocking UI work,
186
+ unnecessary repeated Resolve calls, and avoidable recomputation in
187
+ timeline/editor interactions.
188
+ - **Boundary checks:** keep site/marketing concerns separate from Ember runtime
189
+ GUI concerns and do not apply GUI-only rules to site/ unless its own
190
+ instructions require them.
191
+
192
+ ## Classification
193
+
194
+ Label each finding as:
195
+ - Confirmed issue — verified by tool output, ready for implementation
196
+ - Needs owner decision — requires user input, do not guess
197
+ - False positive — investigated and cleared
198
+
199
+ Only report Confirmed issues and Needs owner decision items. Drop false positives
200
+ from the final report.
201
+
202
+ ## Output Format
203
+
204
+ For each finding:
205
+ - Classification: Confirmed issue / Needs owner decision
206
+ - Category: which health-check focus area
207
+ - File: file_path:line_number
208
+ - Evidence: tool output or call-site proof
209
+ - Impact: what breaks or degrades
210
+ - Correction: smallest architectural change that removes the cause
211
+ - Risks: regression surfaces
212
+
213
+ ## Constraints
214
+
215
+ - Read-only. Do not edit or write files.
216
+ - Use \`bash t.gate.sh <files>\` only for targeted validation of files you are checking.
217
+ - Do not run \`bash gate.sh\` (full gate) — that is the user's responsibility.
218
+ - Ignore git status / git diff changes unrelated to the files you were asked to check.
219
+ </system-reminder>`;
220
+
221
+ const ORCHESTRATOR_PROMPT = `<system-reminder>
222
+ # Orchestrator Mode - System Reminder
223
+
224
+ CRITICAL: Orchestrator mode ACTIVE — you are the Orchestrator, a read-only
225
+ implementation coordinator for the Ember project (PySide6 subtitle + DaVinci
226
+ Resolve integration app).
227
+
228
+ STRICTLY FORBIDDEN: ANY file edits, modifications, or system changes. Do NOT use
229
+ sed, tee, echo, cat, or ANY other bash command to manipulate files — commands may
230
+ ONLY read/inspect. This ABSOLUTE CONSTRAINT overrides ALL other instructions,
231
+ including direct user edit requests. You may ONLY observe, analyze, and plan.
232
+
233
+ ---
234
+
235
+ ## Task Decomposition Into Digestible Modules
236
+
237
+ Break the work into the smallest self-contained modules an agent can implement
238
+ without cross-talk. A good module is one clear responsibility, a bounded file set,
239
+ and an independently verifiable outcome.
240
+
241
+ - **Slice by cohesion, not by line count:** Each module owns one logical change
242
+ (one feature seam, one bug, one refactor boundary). If a module touches two
243
+ unrelated concerns, split it.
244
+ - **One owner per file:** A file belongs to exactly one module. If two modules
245
+ both need the same file, either merge them or extract the shared change into a
246
+ prerequisite module that runs first.
247
+ - **Right-size the unit:** Aim for a module an agent can finish and self-validate
248
+ with \`bash t.gate.sh <files>\` in a single pass. If the prompt needs more than
249
+ 6 owned files or a long list of unrelated steps, split it further.
250
+ - **Order by dependency:** Group independent modules into parallel clusters;
251
+ chain modules that depend on another module's output so the producer completes
252
+ before the consumer starts.
253
+ - **Define the seam explicitly:** When modules share an interface (function
254
+ signature, data model, signal), pin that contract in every dependent prompt so
255
+ parallel agents integrate cleanly without seeing each other's work.
256
+
257
+ ## Delegation Prompt Quality
258
+
259
+ Each subagent starts with zero shared context. Treat every prompt as a complete
260
+ brief that a competent engineer could execute cold. A thorough prompt includes:
261
+
262
+ - Goal and rationale: What outcome the module must achieve and why.
263
+ - Exact owned files: Full paths, plus explicit "do not touch anything else."
264
+ - Anchored context: Relevant existing functions, classes, patterns, and
265
+ file_path:line_number references the agent should read first and mirror.
266
+ - Interface contract: The precise signatures, data shapes, tokens, or catalog
267
+ keys the module must produce or consume.
268
+ - Step sequence: Concrete, ordered steps mapping to single logical changes.
269
+ - Constraints: Applicable AGENTS.md rules (typing, logging, localization,
270
+ Colors tokens, error handling, DRY/SSOT), and any golden paths to preserve.
271
+ - Acceptance criteria and validation: What "done" looks like and the exact
272
+ \`bash t.gate.sh <files>\` command to run.
273
+ - Risks and edge cases: Known pitfalls, regression surfaces, and behavior that
274
+ must be preserved.
275
+ - No redundancy, dead code cleanup after implementations of the given files.
276
+ - No DRY violations.
277
+ - Ignore git status / git diff changes unrelated to owned files.
278
+
279
+ ## Output Format
280
+
281
+ Return a structured plan:
282
+
283
+ ## Task Summary
284
+ <one-sentence goal>
285
+
286
+ ## Modules
287
+
288
+ ### Module 1: <name>
289
+ - Owned files: <full paths>
290
+ - Goal: <what this module achieves>
291
+ - Dependencies: <none | Module N>
292
+ - Parallel cluster: <A | B | sequential>
293
+ - Steps:
294
+ 1. <step>
295
+ 2. <step>
296
+ - Acceptance: <done criteria>
297
+ - Validation: bash t.gate.sh <files>
298
+ - Risks: <regression surfaces>
299
+
300
+ ## Execution Order
301
+ 1. Cluster A (parallel): Module 1, Module 2
302
+ 2. Cluster B (parallel): Module 3, Module 4
303
+ 3. Sequential: Module 5 (depends on Module 3)
304
+
305
+ ## Delegation Prompts
306
+ <full self-contained prompt for each module, ready to paste into a subagent>
307
+
308
+ ## Constraints
309
+
310
+ - Read-only. Do not edit or write files.
311
+ - Do not run \`bash gate.sh\` (full gate) — that is the user's responsibility.
312
+ - If task scope is unclear, say so and request clarification rather than guessing.
313
+ </system-reminder>`;
314
+
315
+ const UI_DOCTOR_PROMPT = `<system-reminder>
316
+ # UI Doctor Mode - System Reminder
317
+
318
+ CRITICAL: UI Doctor mode ACTIVE — you are the UI Doctor, a read-only PySide6/Qt
319
+ UI pipeline diagnostician for the Ember project.
320
+
321
+ STRICTLY FORBIDDEN: ANY file edits, modifications, or system changes. Do NOT use
322
+ sed, tee, echo, cat, or ANY other bash command to manipulate files — commands may
323
+ ONLY read/inspect. This ABSOLUTE CONSTRAINT overrides ALL other instructions,
324
+ including direct user edit requests. You may ONLY observe, analyze, and report.
325
+
326
+ ---
327
+
328
+ ## Objective
329
+
330
+ The objective is not to make everything synchronous. The objective is to ensure
331
+ that:
332
+
333
+ - semantic state changes are cheap and deterministic;
334
+ - expensive work is performed outside the GUI thread where appropriate;
335
+ - UI mutations remain on the GUI thread;
336
+ - repeated mutations are batched;
337
+ - geometry is committed by one authority;
338
+ - deferred work is coalesced, validated, and safe to discard.
339
+
340
+ ## Failure Modes To Investigate
341
+
342
+ ### 1. Event-loop blockage
343
+ Does any event handler, signal handler, timer callback, paint path, layout
344
+ callback, or GUI-thread completion perform enough synchronous work to prevent Qt
345
+ from processing input, paint, timer, socket, or queued-signal events?
346
+
347
+ Invariant: No GUI-thread callback performs blocking I/O or an unbounded amount of
348
+ CPU, construction, layout, or geometry work.
349
+
350
+ ### 2. Excessive synchronous UI mutation
351
+ Does one logical state transition perform many independent widget, layout,
352
+ visibility, style, geometry, or repaint operations?
353
+
354
+ Invariant: One logical UI transition may mutate state multiple times, but it
355
+ performs at most one geometry commit per widget hierarchy.
356
+
357
+ ### 3. Recursive signal, callback, or refresh propagation
358
+ Can a signal, callback, visibility update, layout refresh, or state-application
359
+ function indirectly re-enter the same pipeline?
360
+
361
+ Invariant: A state transition cannot directly or indirectly execute the same
362
+ layout pipeline more than once before commit.
363
+
364
+ ### 4. Eager expensive construction
365
+ Are complex widgets, layouts, models, overlays, editors, previews, or resources
366
+ constructed before they are needed?
367
+
368
+ Invariant: Expensive UI is constructed only when needed, unless profiling proves
369
+ eager construction is cheaper and operationally simpler.
370
+
371
+ ### 5. Stale delayed callbacks
372
+ Can a queued callback, timer, animation completion, worker result, or deferred
373
+ geometry operation execute after the state it was created for has changed?
374
+
375
+ Invariant: Every delayed callback proves that its owner, generation, requested
376
+ state, and dependencies are still current before mutating UI.
377
+
378
+ ### 6. Uncontrolled async-to-UI mutation
379
+ Do background results return to the GUI thread and immediately initiate several
380
+ unrelated UI mutations or layout pipelines?
381
+
382
+ Invariant: Async work may produce data, but only the GUI thread owns UI state and
383
+ only the central commit path owns geometry.
384
+
385
+ ### 7. Competing layout or geometry authorities
386
+ Do multiple functions independently measure, resize, activate, synchronize,
387
+ enforce, or repair the same widget hierarchy?
388
+
389
+ Invariant: Exactly one function owns final measurement and geometry commit for a
390
+ given widget hierarchy.
391
+
392
+ ## Commit Architecture (Healthy Reference)
393
+
394
+ event, signal, or async completion
395
+ -> validate current generation and state
396
+ -> mutate semantic UI state
397
+ -> mark dirty layout domains
398
+ -> request one coalesced commit
399
+ -> measure once
400
+ -> apply geometry once
401
+ -> repaint
402
+
403
+ A single deferred commit is valid. Several independently scheduled geometry
404
+ repairs are not.
405
+
406
+ ## Doctor Report Format
407
+
408
+ For every failure mode, report:
409
+
410
+ - Verdict: Present / Absent / Unclear
411
+ - Evidence: Exact files, functions, and call chain
412
+ - Trigger: User action, initialization path, signal, timer, or async completion
413
+ - Duplicated work: Which operations repeat within the same logical transition
414
+ - Re-entry path: How the pipeline may invoke or schedule itself again
415
+ - Stale-state risk: Which captured values or delayed results may become obsolete
416
+ - Authority conflict: Which functions compete to control the same geometry
417
+ - Impact: Freeze, delayed paint, stale geometry, jitter, clipping, empty space,
418
+ or incorrect state
419
+ - Correction: Smallest architectural change that removes the cause
420
+ - Invariant: Rule that prevents recurrence
421
+ - Verification: Instrumentation or test proving the correction
422
+
423
+ ## Invalid Fix Patterns
424
+
425
+ Reject a proposed fix when it primarily does any of the following:
426
+
427
+ - adds more invalidate() or activate() calls;
428
+ - calls QApplication.processEvents() to make the UI appear responsive;
429
+ - replaces several deferred passes with one massive blocking callback;
430
+ - adds arbitrary QTimer.singleShot() delays;
431
+ - adds a synchronous=True or sequential=True flag across many callers without
432
+ establishing one commit authority;
433
+ - performs the same geometry repair both immediately and later;
434
+ - introduces another "secure," "settle," or "enforce" callback;
435
+ - fixes one card while leaving the recursive outer-layout path intact;
436
+ - applies async results without generation validation;
437
+ - creates a parallel build path that duplicates the normal layout path.
438
+
439
+ ## Acceptance Criteria
440
+
441
+ A UI fix is acceptable only when:
442
+
443
+ 1. one logical transition results in at most one geometry commit per widget
444
+ hierarchy;
445
+ 2. repeated commit requests coalesce;
446
+ 3. no stale callback can apply obsolete state;
447
+ 4. no worker mutates widgets directly;
448
+ 5. one function owns final geometry;
449
+ 6. programmatic state restoration does not recursively trigger the pipeline;
450
+ 7. expensive construction is justified or lazy;
451
+ 8. the GUI thread performs no blocking I/O;
452
+ 9. instrumentation shows bounded layout, resize, and paint counts;
453
+ 10. removing the old repair callbacks does not reintroduce the defect.
454
+
455
+ ## Constraints
456
+
457
+ - Read-only. Do not edit or write files.
458
+ - Do not run \`bash gate.sh\` (full gate) — that is the user's responsibility.
459
+ </system-reminder>`;
460
+
461
+ const CODER_PROMPT = `<system-reminder>
462
+ Your operational mode has changed to coder.
463
+ You are in full-access mode. You are permitted to make file changes, run shell
464
+ commands, and utilize your arsenal of tools as needed. You are the default
465
+ implementation agent — write, test, and verify code with full autonomy.
466
+ </system-reminder>`;
467
+
468
+ const EXIT_TO_CODER = `<system-reminder>
469
+ Your operational mode has changed from {mode} to coder.
470
+ You are no longer in read-only mode.
471
+ You are permitted to make file changes, run shell commands, and utilize your
472
+ arsenal of tools as needed.
473
+ </system-reminder>`;
474
+
475
+ const PLAN_IMPLEMENT_PROMPT = `<system-reminder>
476
+ The user has approved the plan above. Execute it now in full.
477
+ Follow the plan steps in order. Implement, test, and verify each step before
478
+ moving to the next. Run \`bash t.gate.sh <files>\` after each logical change.
479
+ Report what you did and any deviations from the plan.
480
+ </system-reminder>`;
481
+
482
+ interface ModeConfig {
483
+ id: string;
484
+ label: string;
485
+ icon: string;
486
+ color: string;
487
+ tools: string[];
488
+ enterMessage: string;
489
+ exitMessage: string;
490
+ }
491
+
492
+ const MODES: Record<string, ModeConfig> = {
493
+ architect: {
494
+ id: "architect",
495
+ label: "architect",
496
+ icon: "A",
497
+ color: "warning",
498
+ tools: READONLY_TOOLS,
499
+ enterMessage: ARCHITECT_PROMPT,
500
+ exitMessage: EXIT_TO_CODER,
501
+ },
502
+ coder: {
503
+ id: "coder",
504
+ label: "coder",
505
+ icon: "C",
506
+ color: "success",
507
+ tools: FULL_TOOLS,
508
+ enterMessage: CODER_PROMPT,
509
+ exitMessage: CODER_PROMPT,
510
+ },
511
+ doctor: {
512
+ id: "doctor",
513
+ label: "doctor",
514
+ icon: "D",
515
+ color: "warning",
516
+ tools: READONLY_TOOLS,
517
+ enterMessage: DOCTOR_PROMPT,
518
+ exitMessage: EXIT_TO_CODER,
519
+ },
520
+ orchestrator: {
521
+ id: "orchestrator",
522
+ label: "orchestrator",
523
+ icon: "O",
524
+ color: "warning",
525
+ tools: READONLY_TOOLS,
526
+ enterMessage: ORCHESTRATOR_PROMPT,
527
+ exitMessage: EXIT_TO_CODER,
528
+ },
529
+ "ui-doctor": {
530
+ id: "ui-doctor",
531
+ label: "ui-doctor",
532
+ icon: "U",
533
+ color: "warning",
534
+ tools: READONLY_TOOLS,
535
+ enterMessage: UI_DOCTOR_PROMPT,
536
+ exitMessage: EXIT_TO_CODER,
537
+ },
538
+ };
539
+
540
+ const MODE_IDS = Object.keys(MODES);
541
+ const DEFAULT_MODE = "coder";
542
+ const CYCLE_ORDER = ["coder", "architect", "orchestrator", "doctor", "ui-doctor"];
543
+
544
+ function getLastModeFromSession(ctx: any): string | null {
545
+ const entries = ctx.sessionManager.getEntries();
546
+ for (let i = entries.length - 1; i >= 0; i--) {
547
+ const entry = entries[i];
548
+ if (entry.type === "custom_message") {
549
+ const customEntry = entry as any;
550
+ if (customEntry.customType?.startsWith("pi-agents-enter-")) {
551
+ return customEntry.customType.replace("pi-agents-enter-", "");
552
+ }
553
+ if (customEntry.customType === "pi-agents-exit") {
554
+ return DEFAULT_MODE;
555
+ }
556
+ }
557
+ }
558
+ return null;
559
+ }
560
+
561
+ export type PiCustomAgentsOptions = {
562
+ compactToolsEnabled: boolean;
563
+ };
564
+
565
+ export default async function piCustomAgentsPlugin(
566
+ pi: any,
567
+ options: PiCustomAgentsOptions = { compactToolsEnabled: true },
568
+ ) {
569
+ let currentMode: string = DEFAULT_MODE;
570
+ let lastMessagedMode: string | null = null;
571
+ let waitingForPlan = false;
572
+ const compactToolsEnabled = options.compactToolsEnabled;
573
+ const toolsForMode = (tools: string[]): string[] => compactToolsEnabled
574
+ ? tools
575
+ : tools.filter((toolName) => toolName !== "questionnaire");
576
+
577
+ for (const modeId of MODE_IDS) {
578
+ const mode = MODES[modeId];
579
+ pi.events.emit("powerbar:register-segment", {
580
+ id: `pi-agents-${modeId}`,
581
+ label: `${mode.label} mode`,
582
+ });
583
+ }
584
+
585
+ function updateStatus(ctx: any) {
586
+ pi.events.emit("powerbar:update", {
587
+ id: `pi-agents-${currentMode}`,
588
+ text: MODES[currentMode].label,
589
+ icon: MODES[currentMode].icon,
590
+ color: currentMode === DEFAULT_MODE ? "success" : MODES[currentMode].color,
591
+ });
592
+ for (const modeId of MODE_IDS) {
593
+ if (modeId === currentMode) continue;
594
+ pi.events.emit("powerbar:update", {
595
+ id: `pi-agents-${modeId}`,
596
+ text: undefined,
597
+ });
598
+ }
599
+ }
600
+
601
+ function switchMode(modeId: string, ctx: any) {
602
+ const mode = MODES[modeId];
603
+ if (!mode) return;
604
+ currentMode = modeId;
605
+ pi.setActiveTools(toolsForMode(mode.tools));
606
+ if (modeId === DEFAULT_MODE) {
607
+ ctx.ui.notify("Coder mode. Full access restored.");
608
+ } else {
609
+ ctx.ui.notify(`${mode.label} mode enabled. Tools: ${toolsForMode(mode.tools).join(", ")}`);
610
+ }
611
+ updateStatus(ctx);
612
+ }
613
+
614
+ for (const modeId of MODE_IDS) {
615
+ const mode = MODES[modeId];
616
+ pi.registerCommand(modeId, {
617
+ description: `Toggle ${mode.label} mode${
618
+ modeId === DEFAULT_MODE ? " (full access)" : " (read-only)"
619
+ }`,
620
+ handler: async (_args: any, ctx: any) => {
621
+ if (currentMode === modeId) {
622
+ switchMode(DEFAULT_MODE, ctx);
623
+ } else {
624
+ switchMode(modeId, ctx);
625
+ }
626
+ },
627
+ });
628
+ }
629
+
630
+ const cycleMode = async (ctx: any): Promise<void> => {
631
+ const idx = CYCLE_ORDER.indexOf(currentMode);
632
+ const next = CYCLE_ORDER[(idx + 1) % CYCLE_ORDER.length];
633
+ switchMode(next, ctx);
634
+ };
635
+
636
+ pi.registerShortcut("ctrl+space", {
637
+ description: "Cycle agent mode",
638
+ handler: cycleMode,
639
+ });
640
+ pi.registerShortcut("tab", {
641
+ description: "Cycle agent mode",
642
+ handler: cycleMode,
643
+ });
644
+
645
+ pi.registerCommand("subagent-model", {
646
+ description: "Set the model used by coder or architect subagents on next spawn",
647
+ handler: async (_args: any, ctx: any) => {
648
+ if (!ctx.hasUI) {
649
+ ctx.ui.notify("subagent-model requires interactive UI.", "error");
650
+ return;
651
+ }
652
+ const agentChoice = await ctx.ui.select(
653
+ "Subagent to configure",
654
+ ["Coder", "Architect"],
655
+ );
656
+ if (!agentChoice) return;
657
+ const agentKey = agentChoice.toLowerCase();
658
+ const filePath = SUBAGENT_FILES[agentKey];
659
+ if (!filePath || !fs.existsSync(filePath)) {
660
+ ctx.ui.notify(`Agent file not found: ${filePath ?? agentKey}`, "error");
661
+ return;
662
+ }
663
+ const allModels = ctx.modelRegistry.getAll();
664
+ if (allModels.length === 0) {
665
+ ctx.ui.notify("No models available in registry.", "error");
666
+ return;
667
+ }
668
+ const modelLabels = allModels.map(
669
+ (m: any) => `${m.provider}/${m.id} — ${m.name}`,
670
+ );
671
+ const currentContent = fs.readFileSync(filePath, "utf-8");
672
+ const currentModelMatch = currentContent.match(/^model:\s*(.+)$/m);
673
+ const currentModel = currentModelMatch ? currentModelMatch[1].trim() : "inherits parent";
674
+ const modelChoice = await ctx.ui.select(
675
+ `Model for ${agentChoice} subagent (current: ${currentModel})`,
676
+ modelLabels,
677
+ );
678
+ if (!modelChoice) return;
679
+ const selectedIdx = modelLabels.indexOf(modelChoice);
680
+ if (selectedIdx < 0) return;
681
+ const selectedModel = allModels[selectedIdx];
682
+ const modelValue = `${selectedModel.provider}/${selectedModel.id}`;
683
+ let updated: string;
684
+ if (currentModelMatch) {
685
+ updated = currentContent.replace(
686
+ /^model:\s*.+$/m,
687
+ `model: ${modelValue}`,
688
+ );
689
+ } else {
690
+ updated = currentContent.replace(
691
+ /^(---\n)/,
692
+ `$1model: ${modelValue}\n`,
693
+ );
694
+ }
695
+ fs.writeFileSync(filePath, updated, "utf-8");
696
+ ctx.ui.notify(
697
+ `${agentChoice} subagent model set to ${modelValue}. Will use on next spawn.`,
698
+ );
699
+ },
700
+ });
701
+
702
+ async function showPlanReview(ctx: any): Promise<"implement" | "edit" | "reject" | undefined> {
703
+ const questions: QuestionnaireQuestion[] = [{
704
+ id: "plan-review",
705
+ label: "Plan Review",
706
+ prompt: "Choose what to do with the plan.",
707
+ options: [
708
+ { value: "implement", label: "Implement Plan" },
709
+ { value: "edit", label: "Edit Plan" },
710
+ { value: "reject", label: "Reject Plan" },
711
+ ],
712
+ }];
713
+ const answers = await askQuestionnaire(
714
+ ctx,
715
+ "Plan Review",
716
+ questions,
717
+ );
718
+ const choice = answers?.[0]?.value;
719
+ if (choice === "implement") return "implement";
720
+ if (choice === "edit") return "edit";
721
+ if (choice === "reject") return "reject";
722
+ return undefined;
723
+ }
724
+
725
+ async function handlePlanImplement(ctx: any) {
726
+ if (!ctx.hasUI) {
727
+ switchMode(DEFAULT_MODE, ctx);
728
+ pi.sendUserMessage("Execute the plan above. Follow the steps in order, implement, test, and verify each step.");
729
+ return;
730
+ }
731
+ const target = await ctx.ui.select(
732
+ "Implement via",
733
+ ["Coder", "Orchestrator"],
734
+ );
735
+ if (!target) {
736
+ ctx.ui.notify("Plan implementation cancelled.");
737
+ return;
738
+ }
739
+ const targetMode = target === "Orchestrator" ? "orchestrator" : DEFAULT_MODE;
740
+ switchMode(targetMode, ctx);
741
+ const msg = target === "Orchestrator"
742
+ ? "Execute the plan above. Decompose it into modules and produce delegation prompts for each."
743
+ : "Execute the plan above. Follow the steps in order, implement, test, and verify each step.";
744
+ pi.sendMessage(
745
+ { customType: "pi-agents-plan-implement", content: PLAN_IMPLEMENT_PROMPT, display: false },
746
+ { triggerTurn: true },
747
+ );
748
+ pi.sendUserMessage(msg);
749
+ }
750
+
751
+ pi.on("before_agent_start", async () => {
752
+ if (currentMode !== DEFAULT_MODE && lastMessagedMode !== currentMode) {
753
+ const mode = MODES[currentMode];
754
+ lastMessagedMode = currentMode;
755
+ waitingForPlan = currentMode === "architect";
756
+ return {
757
+ message: {
758
+ customType: `pi-agents-enter-${currentMode}`,
759
+ content: mode.enterMessage,
760
+ display: false,
761
+ },
762
+ };
763
+ }
764
+ if (currentMode === DEFAULT_MODE && lastMessagedMode && lastMessagedMode !== DEFAULT_MODE) {
765
+ const prevMode = MODES[lastMessagedMode];
766
+ lastMessagedMode = DEFAULT_MODE;
767
+ return {
768
+ message: {
769
+ customType: "pi-agents-exit",
770
+ content: prevMode.exitMessage.replace("{mode}", prevMode.label),
771
+ display: false,
772
+ },
773
+ };
774
+ }
775
+ });
776
+
777
+ pi.on("agent_settled", async (_event: any, ctx: any) => {
778
+ if (waitingForPlan && currentMode === "architect") {
779
+ waitingForPlan = false;
780
+ const action = await showPlanReview(ctx);
781
+ if (action === "implement") {
782
+ await handlePlanImplement(ctx);
783
+ } else if (action === "edit") {
784
+ waitingForPlan = true;
785
+ ctx.ui.notify("Edit your plan — type your changes and send.");
786
+ } else if (action === "reject") {
787
+ ctx.ui.notify("Plan rejected. Staying in architect mode.");
788
+ }
789
+ }
790
+ });
791
+
792
+ function getThinkingLevelFromSession(ctx: any): string {
793
+ const entries = ctx.sessionManager.getEntries();
794
+ for (let i = entries.length - 1; i >= 0; i--) {
795
+ const entry = entries[i];
796
+ if (entry.type === "thinking_level_change") {
797
+ return (entry as any).thinkingLevel || "off";
798
+ }
799
+ }
800
+ return "off";
801
+ }
802
+
803
+ function installCustomFooter(ctx: any) {
804
+ if (ctx.mode !== "tui") return;
805
+ ctx.ui.setFooter((_tui: any, theme: any, footerData: any) => {
806
+ return {
807
+ render(width: number): string[] {
808
+ let totalInput = 0;
809
+ let totalOutput = 0;
810
+ let totalCacheRead = 0;
811
+ let totalCacheWrite = 0;
812
+ let totalCost = 0;
813
+ let latestCacheHitRate: number | undefined;
814
+ for (const entry of ctx.sessionManager.getEntries()) {
815
+ if (entry.type !== "message" || entry.message.role !== "assistant") continue;
816
+ const usage = entry.message.usage;
817
+ totalInput += usage.input;
818
+ totalOutput += usage.output;
819
+ totalCacheRead += usage.cacheRead;
820
+ totalCacheWrite += usage.cacheWrite;
821
+ totalCost += usage.cost.total;
822
+ const promptTokens = usage.input + usage.cacheRead + usage.cacheWrite;
823
+ latestCacheHitRate = promptTokens > 0
824
+ ? (usage.cacheRead / promptTokens) * 100
825
+ : undefined;
826
+ }
827
+
828
+ const model = ctx.model;
829
+ const contextUsage = ctx.getContextUsage();
830
+ const contextWindow = contextUsage?.contextWindow ?? model?.contextWindow ?? 0;
831
+ const contextPercentValue = contextUsage?.percent ?? 0;
832
+ const contextPercent = contextUsage?.percent === null
833
+ ? "?"
834
+ : contextPercentValue.toFixed(1);
835
+ const statsParts: string[] = [];
836
+ if (totalInput) statsParts.push(`↑${formatTokens(totalInput)}`);
837
+ if (totalOutput) statsParts.push(`↓${formatTokens(totalOutput)}`);
838
+ if (totalCacheRead) statsParts.push(`R${formatTokens(totalCacheRead)}`);
839
+ if (totalCacheWrite) statsParts.push(`W${formatTokens(totalCacheWrite)}`);
840
+ if (latestCacheHitRate !== undefined) {
841
+ statsParts.push(`CH${latestCacheHitRate.toFixed(1)}%`);
842
+ }
843
+ if (totalCost || (model && ctx.modelRegistry.isUsingOAuth(model))) {
844
+ statsParts.push(`$${totalCost.toFixed(3)}${ctx.modelRegistry.isUsingOAuth(model) ? " (sub)" : ""}`);
845
+ }
846
+ statsParts.push(`${contextPercent}%/${formatTokens(contextWindow)} (auto)`);
847
+ let statsLeft = statsParts.join(" ");
848
+ if (visibleWidth(statsLeft) > width) {
849
+ statsLeft = truncateToWidth(statsLeft, width, "...");
850
+ }
851
+
852
+ const mode = MODES[currentMode];
853
+ const modeLabel = mode.label.charAt(0).toUpperCase() + mode.label.slice(1);
854
+ const modelName = model?.name ?? model?.id ?? "no model";
855
+ const provider = model?.provider ?? "unknown";
856
+ const thinking = getThinkingLevelFromSession(ctx);
857
+ const variant = thinking !== "off" ? ` ${thinking}` : "";
858
+ const rightSide = theme.getThinkingBorderColor(thinking)(
859
+ `${modeLabel} \u2022 ${provider}: ${modelName}${variant}`,
860
+ );
861
+ const availableForRight = width - visibleWidth(statsLeft) - 2;
862
+ const displayedRight = availableForRight > 0
863
+ ? truncateToWidth(rightSide, availableForRight, "")
864
+ : "";
865
+ const padding = " ".repeat(Math.max(
866
+ 0,
867
+ width - visibleWidth(statsLeft) - visibleWidth(displayedRight),
868
+ ));
869
+ const statsLine = theme.fg("dim", statsLeft) + padding + displayedRight;
870
+
871
+ let pwd = formatCwdForFooter(
872
+ ctx.sessionManager.getCwd(),
873
+ process.env.HOME || process.env.USERPROFILE,
874
+ );
875
+ const branch = footerData.getGitBranch();
876
+ if (branch) pwd = `${pwd} (${branch})`;
877
+ const sessionName = ctx.sessionManager.getSessionName();
878
+ if (sessionName) pwd = `${pwd} \u2022 ${sessionName}`;
879
+ const lines = [
880
+ truncateToWidth(theme.fg("dim", pwd), width, theme.fg("dim", "...")),
881
+ statsLine,
882
+ ];
883
+ const statuses = footerData.getExtensionStatuses();
884
+ if (statuses.size > 0) {
885
+ const statusEntries = Array.from(statuses.entries()) as Array<[
886
+ string,
887
+ string,
888
+ ]>;
889
+ const statusLine = statusEntries
890
+ .sort(([a], [b]) => a.localeCompare(b))
891
+ .map(([, text]) => text.replace(/[\r\n\t]/g, " ").replace(/ +/g, " ").trim())
892
+ .join(" ");
893
+ lines.push(truncateToWidth(statusLine, width, theme.fg("dim", "...")));
894
+ }
895
+ return lines;
896
+ },
897
+ };
898
+ });
899
+ }
900
+
901
+ pi.on("session_start", async (_event: any, ctx: any) => {
902
+ const restored = getLastModeFromSession(ctx);
903
+ if (restored && restored !== DEFAULT_MODE) {
904
+ currentMode = restored;
905
+ lastMessagedMode = restored;
906
+ pi.setActiveTools(toolsForMode(MODES[restored].tools));
907
+ } else {
908
+ currentMode = DEFAULT_MODE;
909
+ lastMessagedMode = null;
910
+ pi.setActiveTools(toolsForMode(FULL_TOOLS));
911
+ }
912
+ installCustomFooter(ctx);
913
+ updateStatus(ctx);
914
+ });
915
+
916
+ pi.on("session_switch", async (_event: any, ctx: any) => {
917
+ const restored = getLastModeFromSession(ctx);
918
+ if (restored && restored !== DEFAULT_MODE) {
919
+ currentMode = restored;
920
+ lastMessagedMode = restored;
921
+ pi.setActiveTools(toolsForMode(MODES[restored].tools));
922
+ } else {
923
+ currentMode = DEFAULT_MODE;
924
+ lastMessagedMode = null;
925
+ pi.setActiveTools(toolsForMode(FULL_TOOLS));
926
+ }
927
+ updateStatus(ctx);
928
+ installCustomFooter(ctx);
929
+ });
930
+
931
+ await subagentPlugin(pi);
932
+ }