@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.
- package/ATTRIBUTION.md +3 -2
- package/README.md +8 -7
- package/package.json +1 -1
- package/plugins/index.ts +17 -9
- package/plugins/pi-compact-tools/index.ts +55 -964
- package/plugins/pi-custom-agents/index.ts +932 -0
- /package/plugins/{subagent → pi-custom-agents/subagent}/LICENSE +0 -0
- /package/plugins/{subagent → pi-custom-agents/subagent}/README.md +0 -0
- /package/plugins/{subagent → pi-custom-agents/subagent}/agent-format.md +0 -0
- /package/plugins/{subagent → pi-custom-agents/subagent}/agents/architect.md +0 -0
- /package/plugins/{subagent → pi-custom-agents/subagent}/agents/coder.md +0 -0
- /package/plugins/{subagent → pi-custom-agents/subagent}/agents/general-purpose.md +0 -0
- /package/plugins/{subagent → pi-custom-agents/subagent}/agents/reviewer.md +0 -0
- /package/plugins/{subagent → pi-custom-agents/subagent}/agents/scout.md +0 -0
- /package/plugins/{subagent → pi-custom-agents/subagent}/agents/worker.md +0 -0
- /package/plugins/{subagent → pi-custom-agents/subagent}/extensions/agents.ts +0 -0
- /package/plugins/{subagent → pi-custom-agents/subagent}/extensions/index.ts +0 -0
- /package/plugins/{subagent → pi-custom-agents/subagent}/extensions/model.ts +0 -0
- /package/plugins/{subagent → pi-custom-agents/subagent}/extensions/package.json +0 -0
- /package/plugins/{subagent → pi-custom-agents/subagent}/extensions/render.ts +0 -0
- /package/plugins/{subagent → pi-custom-agents/subagent}/extensions/runner.ts +0 -0
- /package/plugins/{subagent → pi-custom-agents/subagent}/extensions/service.ts +0 -0
- /package/plugins/{subagent → pi-custom-agents/subagent}/extensions/thread-viewer.ts +0 -0
- /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
|
+
}
|