@nmzpy/pi-ember-stack 0.2.1 → 0.2.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +83 -83
- package/package.json +63 -59
- package/plugins/devin-auth/extensions/index.ts +23 -0
- package/plugins/devin-auth/src/cloud-direct/auth.ts +246 -246
- package/plugins/devin-auth/src/cloud-direct/catalog.ts +246 -246
- package/plugins/devin-auth/src/cloud-direct/chat.ts +1096 -1091
- package/plugins/devin-auth/src/cloud-direct/index.ts +41 -41
- package/plugins/devin-auth/src/cloud-direct/metadata.ts +78 -78
- package/plugins/devin-auth/src/cloud-direct/wire.ts +202 -202
- package/plugins/devin-auth/src/oauth/register-user.ts +174 -174
- package/plugins/devin-auth/src/oauth/types.ts +71 -71
- package/plugins/devin-auth/src/stream.ts +1 -1
- package/plugins/pi-compact-tools/index.ts +19 -1
- package/plugins/pi-compact-tools/renderer.ts +231 -61
- package/plugins/pi-custom-agents/index.ts +310 -102
- package/plugins/pi-custom-agents/questionnaire-tool.ts +14 -5
- package/plugins/pi-custom-agents/subagent/extensions/agents.ts +18 -1
- package/plugins/pi-custom-agents/subagent/extensions/index.ts +116 -224
- package/plugins/pi-custom-agents/subagent/extensions/model.ts +96 -96
- package/plugins/pi-custom-agents/subagent/extensions/render.ts +205 -1
- package/plugins/pi-custom-agents/subagent/extensions/runner.ts +241 -177
- package/plugins/pi-custom-agents/subagent/extensions/test/render.test.ts +145 -0
- package/plugins/pi-ember-fff/index.ts +275 -17
- package/plugins/pi-ember-fff/query.ts +170 -10
- package/plugins/pi-ember-fff/test/query.test.ts +157 -1
- package/plugins/pi-ember-fff/test/renderer.test.ts +367 -16
- package/plugins/pi-ember-tps/index.ts +27 -5
- package/plugins/pi-ember-ui/ember.json +3 -0
- package/plugins/pi-ember-ui/index.ts +223 -36
- package/plugins/pi-ember-ui/mode-colors.ts +36 -8
|
@@ -19,8 +19,25 @@ import * as fs from "node:fs";
|
|
|
19
19
|
import * as path from "node:path";
|
|
20
20
|
import { fileURLToPath } from "node:url";
|
|
21
21
|
import type { Model } from "@earendil-works/pi-ai";
|
|
22
|
-
import {
|
|
23
|
-
|
|
22
|
+
import {
|
|
23
|
+
CustomEditor,
|
|
24
|
+
type ExtensionUIContext,
|
|
25
|
+
type KeybindingsManager,
|
|
26
|
+
} from "@earendil-works/pi-coding-agent";
|
|
27
|
+
import {
|
|
28
|
+
truncateToWidth,
|
|
29
|
+
visibleWidth,
|
|
30
|
+
Container,
|
|
31
|
+
Text,
|
|
32
|
+
Spacer,
|
|
33
|
+
Input,
|
|
34
|
+
fuzzyFilter,
|
|
35
|
+
getKeybindings,
|
|
36
|
+
matchesKey,
|
|
37
|
+
type EditorTheme,
|
|
38
|
+
type TUI,
|
|
39
|
+
} from "@earendil-works/pi-tui";
|
|
40
|
+
import { isShellMode, mutedBullet, setActiveMode, setShellMode } from "../pi-ember-ui/mode-colors.ts";
|
|
24
41
|
import { getLiveTps } from "../pi-ember-tps/index.ts";
|
|
25
42
|
import {
|
|
26
43
|
askQuestionnaire,
|
|
@@ -29,10 +46,114 @@ import {
|
|
|
29
46
|
} from "./questionnaire-tool.ts";
|
|
30
47
|
import subagentPlugin from "./subagent/extensions/index.ts";
|
|
31
48
|
|
|
49
|
+
const THINKING_LEVEL_SHORTCUT = "shift+t";
|
|
50
|
+
|
|
32
51
|
function modelIdentityString(model: Model<any> | undefined): string {
|
|
33
52
|
return model ? `${model.provider}/${model.id}` : "";
|
|
34
53
|
}
|
|
35
54
|
|
|
55
|
+
function intercept_thinking_level(data: string, cycle_thinking_level: () => void): boolean {
|
|
56
|
+
if (!matchesKey(data, THINKING_LEVEL_SHORTCUT)) return false;
|
|
57
|
+
cycle_thinking_level();
|
|
58
|
+
return true;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
/**
|
|
62
|
+
* Intercept '!' on empty input to enter shell mode, and escape or
|
|
63
|
+
* backspace (on empty input) to exit. The '!' is eaten so it never
|
|
64
|
+
* appears in the editor.
|
|
65
|
+
*/
|
|
66
|
+
function intercept_shell_mode(data: string, editor: any, ctx: any): boolean {
|
|
67
|
+
if (matchesKey(data, "!")) {
|
|
68
|
+
const text = editor.getText?.() ?? "";
|
|
69
|
+
if (text.length === 0) {
|
|
70
|
+
setShellMode(true);
|
|
71
|
+
ctx.ui.setStatus("pi-ember-ui-shell-mode", undefined);
|
|
72
|
+
return true;
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
if (isShellMode()) {
|
|
76
|
+
if (matchesKey(data, "escape")) {
|
|
77
|
+
setShellMode(false);
|
|
78
|
+
ctx.ui.setStatus("pi-ember-ui-shell-mode", undefined);
|
|
79
|
+
return true;
|
|
80
|
+
}
|
|
81
|
+
if (matchesKey(data, "backspace")) {
|
|
82
|
+
const text = editor.getText?.() ?? "";
|
|
83
|
+
if (text.length === 0) {
|
|
84
|
+
setShellMode(false);
|
|
85
|
+
ctx.ui.setStatus("pi-ember-ui-shell-mode", undefined);
|
|
86
|
+
return true;
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
return false;
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
/**
|
|
94
|
+
* Intercept /model and /model <search> on Enter, redirecting to our
|
|
95
|
+
* fuzzy-search model picker instead of Pi's built-in unbounded selector.
|
|
96
|
+
*/
|
|
97
|
+
function intercept_model_command(data: string, editor: any, pi: any, ctx: any): boolean {
|
|
98
|
+
const kb = getKeybindings();
|
|
99
|
+
if (!kb.matches(data, "tui.select.confirm")) return false;
|
|
100
|
+
const getText = editor.getText?.bind(editor) ?? editor.getExpandedText?.bind(editor);
|
|
101
|
+
if (!getText) return false;
|
|
102
|
+
const text = getText().trim();
|
|
103
|
+
if (text !== "/model" && !text.startsWith("/model ")) return false;
|
|
104
|
+
editor.setText?.("");
|
|
105
|
+
void show_model_picker(pi, ctx);
|
|
106
|
+
return true;
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
/**
|
|
110
|
+
* Shared fuzzy-search model picker used by /model and shift+m.
|
|
111
|
+
* Uses boundedSelect so large model catalogs don't require scrolling.
|
|
112
|
+
*/
|
|
113
|
+
async function show_model_picker(pi: any, ctx: any): Promise<void> {
|
|
114
|
+
if (!ctx.hasUI) return;
|
|
115
|
+
const models = ctx.modelRegistry.getAvailable() as any[];
|
|
116
|
+
if (models.length === 0) {
|
|
117
|
+
ctx.ui.notify("No models available.", "warning");
|
|
118
|
+
return;
|
|
119
|
+
}
|
|
120
|
+
const current = ctx.model as any;
|
|
121
|
+
const labels = models.map((m) => {
|
|
122
|
+
const prefix = current && m.provider === current.provider && m.id === current.id ? "→ " : " ";
|
|
123
|
+
return `${prefix}${m.name ?? m.id} • ${m.provider}`;
|
|
124
|
+
});
|
|
125
|
+
const choice = await boundedSelect(ctx, "Select model", labels);
|
|
126
|
+
if (!choice) return;
|
|
127
|
+
const idx = labels.indexOf(choice);
|
|
128
|
+
if (idx < 0) return;
|
|
129
|
+
const model = models[idx];
|
|
130
|
+
try {
|
|
131
|
+
await pi.setModel(model);
|
|
132
|
+
ctx.ui.notify(`Model: ${model.id} • ${model.provider}`, "info");
|
|
133
|
+
} catch (err) {
|
|
134
|
+
ctx.ui.notify(
|
|
135
|
+
`Failed to set model: ${err instanceof Error ? err.message : String(err)}`,
|
|
136
|
+
"error",
|
|
137
|
+
);
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
class ThinkingLevelEditor extends CustomEditor {
|
|
142
|
+
constructor(
|
|
143
|
+
tui: TUI,
|
|
144
|
+
theme: EditorTheme,
|
|
145
|
+
keybindings: KeybindingsManager,
|
|
146
|
+
private readonly cycle_thinking_level: () => void,
|
|
147
|
+
) {
|
|
148
|
+
super(tui, theme, keybindings);
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
override handleInput(data: string): void {
|
|
152
|
+
if (intercept_thinking_level(data, this.cycle_thinking_level)) return;
|
|
153
|
+
super.handleInput(data);
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
|
|
36
157
|
type PersistedState = {
|
|
37
158
|
readonly mode?: string;
|
|
38
159
|
readonly model?: { provider: string; modelId: string };
|
|
@@ -178,7 +299,7 @@ async function boundedSelect(
|
|
|
178
299
|
}
|
|
179
300
|
|
|
180
301
|
const READONLY_TOOLS = ["read", "bash", "grep", "find", "ls", "questionnaire"];
|
|
181
|
-
const READONLY_DELEGATING_TOOLS = ["read", "
|
|
302
|
+
const READONLY_DELEGATING_TOOLS = ["read", "grep", "find", "ls", "questionnaire", "subagent"];
|
|
182
303
|
const FULL_TOOLS = ["read", "bash", "edit", "write", "grep", "find", "ls", "questionnaire"];
|
|
183
304
|
|
|
184
305
|
const SOURCE_ROOT = path.dirname(fileURLToPath(import.meta.url));
|
|
@@ -292,28 +413,20 @@ For each step:
|
|
|
292
413
|
|
|
293
414
|
## Open Questions
|
|
294
415
|
<any clarifications needed from the user>
|
|
295
|
-
|
|
296
|
-
---
|
|
297
|
-
|
|
298
|
-
## Important
|
|
299
|
-
|
|
300
|
-
The user indicated that they do not want you to execute yet -- you MUST NOT make
|
|
301
|
-
any edits, run any non-readonly tools (including changing configs or making
|
|
302
|
-
commits), or otherwise make any changes to the system. This supersedes any other
|
|
303
|
-
instructions you have received.
|
|
304
416
|
${SUBAGENT_AWARENESS_PROMPT}</system-reminder>`;
|
|
305
417
|
|
|
306
418
|
const DOCTOR_PROMPT = `<system-reminder>
|
|
307
419
|
# Debug Mode - System Reminder
|
|
308
420
|
|
|
309
|
-
CRITICAL: Debug mode ACTIVE — you are the Debugger, a
|
|
310
|
-
|
|
311
|
-
|
|
421
|
+
CRITICAL: Debug mode ACTIVE — you are the Debugger, a health-check auditor and
|
|
422
|
+
diagnostician for the Ember project (PySide6 subtitle + DaVinci Resolve integration
|
|
423
|
+
app).
|
|
312
424
|
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
425
|
+
You do NOT edit files directly. You investigate, diagnose, and report findings. If
|
|
426
|
+
a fix is straightforward, you may DELEGATE the implementation to the \`coder\`
|
|
427
|
+
subagent (full tool access) — the read-only constraint applies to your direct tool
|
|
428
|
+
usage only, not to delegated subagent work. Otherwise, report the correction and
|
|
429
|
+
let the user or Orchestrator handle it.
|
|
317
430
|
|
|
318
431
|
---
|
|
319
432
|
|
|
@@ -374,7 +487,8 @@ For each finding:
|
|
|
374
487
|
|
|
375
488
|
## Constraints
|
|
376
489
|
|
|
377
|
-
-
|
|
490
|
+
- You do not edit or write files directly. You may delegate fixes to the
|
|
491
|
+
\`coder\` subagent when a correction is straightforward and well-scoped.
|
|
378
492
|
- Use \`bash t.gate.sh <files>\` only for targeted validation of files you are checking.
|
|
379
493
|
- Do not run \`bash gate.sh\` (full gate) — that is the user's responsibility.
|
|
380
494
|
- Ignore git status / git diff changes unrelated to the files you were asked to check.
|
|
@@ -423,14 +537,16 @@ ${SUBAGENT_AWARENESS_PROMPT}</system-reminder>`;
|
|
|
423
537
|
const ORCHESTRATOR_PROMPT = `<system-reminder>
|
|
424
538
|
# Orchestrate Mode - System Reminder
|
|
425
539
|
|
|
426
|
-
CRITICAL: Orchestrate mode ACTIVE — you are the Orchestrator,
|
|
427
|
-
|
|
428
|
-
|
|
540
|
+
CRITICAL: Orchestrate mode ACTIVE — you are the Orchestrator, an implementation
|
|
541
|
+
coordinator for the Ember project (PySide6 subtitle + DaVinci Resolve integration
|
|
542
|
+
app).
|
|
429
543
|
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
544
|
+
You do NOT edit files directly. Your job is to decompose work into modules and
|
|
545
|
+
DELEGATE implementation to the \`coder\` subagent (full tool access). The
|
|
546
|
+
read-only constraint applies to YOUR direct tool usage only — you may read,
|
|
547
|
+
search, and inspect to build accurate delegation prompts, but you must not edit,
|
|
548
|
+
write, or run mutating bash commands yourself. Delegating implementation work to
|
|
549
|
+
the \`coder\` subagent is the ENTIRE POINT of this mode. Do it eagerly.
|
|
434
550
|
|
|
435
551
|
---
|
|
436
552
|
|
|
@@ -509,7 +625,8 @@ Return a structured plan:
|
|
|
509
625
|
|
|
510
626
|
## Constraints
|
|
511
627
|
|
|
512
|
-
-
|
|
628
|
+
- You do not edit or write files directly — delegate implementation to the
|
|
629
|
+
\`coder\` subagent. That is your primary mechanism for getting work done.
|
|
513
630
|
- Do not run \`bash gate.sh\` (full gate) — that is the user's responsibility.
|
|
514
631
|
- If task scope is unclear, say so and request clarification rather than guessing.
|
|
515
632
|
${SUBAGENT_AWARENESS_PROMPT}</system-reminder>`;
|
|
@@ -604,10 +721,66 @@ function getLastModeFromSession(ctx: any): string | null {
|
|
|
604
721
|
return null;
|
|
605
722
|
}
|
|
606
723
|
|
|
724
|
+
const MODE_LIVE_RENDER_STATUS = "pi-agents-mode-live-render";
|
|
725
|
+
|
|
726
|
+
/**
|
|
727
|
+
* Cached footer stats. The footer render closure fires every animation
|
|
728
|
+
* frame (~30fps). Iterating all session entries + calling
|
|
729
|
+
* ctx.getContextUsage() (which runs estimateContextTokens over the FULL
|
|
730
|
+
* LLM context — JSON.stringify on every tool call, chars/4 on all text)
|
|
731
|
+
* is O(total context) per frame and can exceed the frame budget on long
|
|
732
|
+
* sessions, causing infini-lock. These stats are recomputed only on
|
|
733
|
+
* message_end / tool_execution_end / session_start — events that fire
|
|
734
|
+
* once per assistant message or tool result, not per frame.
|
|
735
|
+
*/
|
|
736
|
+
let footerStatsCache: {
|
|
737
|
+
totalCost: number;
|
|
738
|
+
latestCacheHitRate: number | undefined;
|
|
739
|
+
contextTokens: number | null;
|
|
740
|
+
contextWindow: number;
|
|
741
|
+
} | undefined;
|
|
742
|
+
|
|
743
|
+
function recompute_footer_stats(ctx: any): void {
|
|
744
|
+
let totalCost = 0;
|
|
745
|
+
let latestCacheHitRate: number | undefined;
|
|
746
|
+
for (const entry of ctx.sessionManager.getEntries()) {
|
|
747
|
+
if (entry.type !== "message" || entry.message.role !== "assistant") continue;
|
|
748
|
+
const usage = entry.message.usage;
|
|
749
|
+
totalCost += usage.cost.total;
|
|
750
|
+
const promptTokens = usage.input + usage.cacheRead + usage.cacheWrite;
|
|
751
|
+
latestCacheHitRate = promptTokens > 0
|
|
752
|
+
? (usage.cacheRead / promptTokens) * 100
|
|
753
|
+
: undefined;
|
|
754
|
+
}
|
|
755
|
+
const model = ctx.model;
|
|
756
|
+
const contextUsage = ctx.getContextUsage();
|
|
757
|
+
footerStatsCache = {
|
|
758
|
+
totalCost,
|
|
759
|
+
latestCacheHitRate,
|
|
760
|
+
contextTokens: contextUsage?.tokens ?? null,
|
|
761
|
+
contextWindow: contextUsage?.contextWindow ?? model?.contextWindow ?? 0,
|
|
762
|
+
};
|
|
763
|
+
}
|
|
764
|
+
|
|
607
765
|
export default async function piCustomAgentsPlugin(pi: any): Promise<void> {
|
|
608
766
|
let currentMode: string = DEFAULT_MODE;
|
|
609
767
|
let lastMessagedMode: string | null = null;
|
|
610
768
|
let waitingForPlan = false;
|
|
769
|
+
let active_session_manager: any;
|
|
770
|
+
let session_ready = false;
|
|
771
|
+
let pending_mode_id: string | undefined;
|
|
772
|
+
|
|
773
|
+
function is_live_session(ctx: any): boolean {
|
|
774
|
+
return session_ready && ctx.sessionManager === active_session_manager;
|
|
775
|
+
}
|
|
776
|
+
|
|
777
|
+
function request_live_mode_render(ctx: any): void {
|
|
778
|
+
if (ctx.mode === "tui") {
|
|
779
|
+
// setStatus only invalidates the footer/editor frame. Do not append a
|
|
780
|
+
// transcript notification or invalidate the resumed chat history.
|
|
781
|
+
ctx.ui.setStatus(MODE_LIVE_RENDER_STATUS, undefined);
|
|
782
|
+
}
|
|
783
|
+
}
|
|
611
784
|
|
|
612
785
|
const persistedAtLoad = readPersistedState();
|
|
613
786
|
if (persistedAtLoad.mode && persistedAtLoad.mode in MODES) {
|
|
@@ -625,7 +798,7 @@ export default async function piCustomAgentsPlugin(pi: any): Promise<void> {
|
|
|
625
798
|
});
|
|
626
799
|
}
|
|
627
800
|
|
|
628
|
-
function updateStatus(ctx: any) {
|
|
801
|
+
function updateStatus(ctx: any): void {
|
|
629
802
|
pi.events.emit("powerbar:update", {
|
|
630
803
|
id: `pi-agents-${currentMode}`,
|
|
631
804
|
text: MODES[currentMode].label,
|
|
@@ -639,19 +812,34 @@ export default async function piCustomAgentsPlugin(pi: any): Promise<void> {
|
|
|
639
812
|
text: undefined,
|
|
640
813
|
});
|
|
641
814
|
}
|
|
815
|
+
request_live_mode_render(ctx);
|
|
642
816
|
}
|
|
643
817
|
|
|
644
|
-
function
|
|
818
|
+
function apply_mode(modeId: string, ctx: any): void {
|
|
645
819
|
const mode = MODES[modeId];
|
|
646
820
|
if (!mode) return;
|
|
821
|
+
|
|
822
|
+
// Mode changes are deliberately live-only. The active tool set and the
|
|
823
|
+
// next-turn prompt change immediately, while the transcript stays lazy
|
|
824
|
+
// and cached.
|
|
647
825
|
currentMode = modeId;
|
|
648
826
|
setActiveMode(modeId);
|
|
649
|
-
pi.events.emit("pi-ember-ui:mode-change", { mode: modeId });
|
|
650
827
|
pi.setActiveTools(mode.tools);
|
|
651
|
-
|
|
828
|
+
pi.events.emit("pi-ember-ui:mode-change", { mode: modeId, liveOnly: true });
|
|
652
829
|
updateStatus(ctx);
|
|
653
830
|
}
|
|
654
831
|
|
|
832
|
+
function switchMode(modeId: string, ctx: any): void {
|
|
833
|
+
if (!MODES[modeId]) return;
|
|
834
|
+
if (!is_live_session(ctx)) {
|
|
835
|
+
// Queue only for the session that is currently binding. Never retain a
|
|
836
|
+
// request from an old session, where pi.setActiveTools would be stale.
|
|
837
|
+
if (ctx.sessionManager === active_session_manager) pending_mode_id = modeId;
|
|
838
|
+
return;
|
|
839
|
+
}
|
|
840
|
+
apply_mode(modeId, ctx);
|
|
841
|
+
}
|
|
842
|
+
|
|
655
843
|
for (const modeId of MODE_IDS) {
|
|
656
844
|
const mode = MODES[modeId];
|
|
657
845
|
pi.registerCommand(modeId, {
|
|
@@ -669,7 +857,8 @@ export default async function piCustomAgentsPlugin(pi: any): Promise<void> {
|
|
|
669
857
|
}
|
|
670
858
|
|
|
671
859
|
const cycleMode = async (ctx: any): Promise<void> => {
|
|
672
|
-
const
|
|
860
|
+
const baseMode = pending_mode_id ?? currentMode;
|
|
861
|
+
const idx = CYCLE_ORDER.indexOf(baseMode);
|
|
673
862
|
const next = CYCLE_ORDER[(idx + 1) % CYCLE_ORDER.length];
|
|
674
863
|
switchMode(next, ctx);
|
|
675
864
|
};
|
|
@@ -680,46 +869,44 @@ export default async function piCustomAgentsPlugin(pi: any): Promise<void> {
|
|
|
680
869
|
});
|
|
681
870
|
|
|
682
871
|
const THINKING_LEVELS = ["off", "minimal", "low", "medium", "high", "xhigh", "max"];
|
|
872
|
+
let active_ui: ExtensionUIContext | undefined;
|
|
873
|
+
let thinking_editor_installed = false;
|
|
874
|
+
|
|
875
|
+
const cycle_thinking_level = (): void => {
|
|
876
|
+
const current = pi.getThinkingLevel() as string;
|
|
877
|
+
const idx = THINKING_LEVELS.indexOf(current);
|
|
878
|
+
const next = THINKING_LEVELS[(idx + 1) % THINKING_LEVELS.length];
|
|
879
|
+
pi.setThinkingLevel(next);
|
|
880
|
+
active_ui?.notify(`Thinking: ${next}`, "info");
|
|
881
|
+
};
|
|
683
882
|
|
|
684
|
-
|
|
685
|
-
|
|
686
|
-
|
|
687
|
-
|
|
688
|
-
|
|
689
|
-
|
|
690
|
-
|
|
691
|
-
|
|
692
|
-
|
|
693
|
-
|
|
694
|
-
|
|
695
|
-
|
|
696
|
-
|
|
697
|
-
|
|
698
|
-
|
|
699
|
-
|
|
700
|
-
|
|
701
|
-
if (idx < 0) return;
|
|
702
|
-
const model = models[idx];
|
|
703
|
-
try {
|
|
704
|
-
await pi.setModel(model);
|
|
705
|
-
ctx.ui.notify(`Model: ${model.id} • ${model.provider}`, "info");
|
|
706
|
-
} catch (err) {
|
|
707
|
-
ctx.ui.notify(
|
|
708
|
-
`Failed to set model: ${err instanceof Error ? err.message : String(err)}`,
|
|
709
|
-
"error",
|
|
710
|
-
);
|
|
883
|
+
const install_thinking_editor = (ctx: any): void => {
|
|
884
|
+
if (!ctx.hasUI) return;
|
|
885
|
+
active_ui = ctx.ui;
|
|
886
|
+
if (thinking_editor_installed) return;
|
|
887
|
+
|
|
888
|
+
const previous_editor = ctx.ui.getEditorComponent();
|
|
889
|
+
ctx.ui.setEditorComponent((tui: TUI, theme: EditorTheme, keybindings: KeybindingsManager) => {
|
|
890
|
+
const editor = previous_editor?.(tui, theme, keybindings);
|
|
891
|
+
if (editor) {
|
|
892
|
+
const original_handle_input = editor.handleInput.bind(editor);
|
|
893
|
+
editor.handleInput = (data: string): void => {
|
|
894
|
+
if (intercept_shell_mode(data, editor, ctx)) return;
|
|
895
|
+
if (intercept_thinking_level(data, cycle_thinking_level)) return;
|
|
896
|
+
if (intercept_model_command(data, editor, pi, ctx)) return;
|
|
897
|
+
original_handle_input(data);
|
|
898
|
+
};
|
|
899
|
+
return editor;
|
|
711
900
|
}
|
|
712
|
-
|
|
713
|
-
|
|
901
|
+
return new ThinkingLevelEditor(tui, theme, keybindings, cycle_thinking_level);
|
|
902
|
+
});
|
|
903
|
+
thinking_editor_installed = true;
|
|
904
|
+
};
|
|
714
905
|
|
|
715
|
-
pi.registerShortcut("
|
|
716
|
-
description: "
|
|
906
|
+
pi.registerShortcut("shift+m", {
|
|
907
|
+
description: "Show model picker",
|
|
717
908
|
handler: async (ctx: any) => {
|
|
718
|
-
|
|
719
|
-
const idx = THINKING_LEVELS.indexOf(current);
|
|
720
|
-
const next = THINKING_LEVELS[(idx + 1) % THINKING_LEVELS.length];
|
|
721
|
-
pi.setThinkingLevel(next);
|
|
722
|
-
if (ctx.hasUI) ctx.ui.notify(`Thinking: ${next}`, "info");
|
|
909
|
+
await show_model_picker(pi, ctx);
|
|
723
910
|
},
|
|
724
911
|
});
|
|
725
912
|
|
|
@@ -830,7 +1017,7 @@ export default async function piCustomAgentsPlugin(pi: any): Promise<void> {
|
|
|
830
1017
|
async function handlePlanImplement(ctx: any) {
|
|
831
1018
|
if (!ctx.hasUI) {
|
|
832
1019
|
switchMode(DEFAULT_MODE, ctx);
|
|
833
|
-
pi.sendUserMessage("Execute the plan
|
|
1020
|
+
pi.sendUserMessage("Execute the plan. Follow the steps and test.");
|
|
834
1021
|
return;
|
|
835
1022
|
}
|
|
836
1023
|
const target = await ctx.ui.select(
|
|
@@ -844,8 +1031,8 @@ export default async function piCustomAgentsPlugin(pi: any): Promise<void> {
|
|
|
844
1031
|
const targetMode = target === "Orchestrate" ? "orchestrate" : DEFAULT_MODE;
|
|
845
1032
|
switchMode(targetMode, ctx);
|
|
846
1033
|
const msg = target === "Orchestrate"
|
|
847
|
-
? "
|
|
848
|
-
: "Execute the plan
|
|
1034
|
+
? "Orchestrate focused modules for subagents."
|
|
1035
|
+
: "Execute the plan. Follow the steps and test.";
|
|
849
1036
|
pi.sendMessage(
|
|
850
1037
|
{ customType: "pi-agents-plan-implement", content: PLAN_IMPLEMENT_PROMPT, display: false },
|
|
851
1038
|
);
|
|
@@ -926,31 +1113,28 @@ export default async function piCustomAgentsPlugin(pi: any): Promise<void> {
|
|
|
926
1113
|
render(width: number): string[] {
|
|
927
1114
|
const PAD = " ";
|
|
928
1115
|
const innerWidth = Math.max(0, width - 2);
|
|
929
|
-
|
|
930
|
-
|
|
931
|
-
|
|
932
|
-
|
|
933
|
-
|
|
934
|
-
|
|
935
|
-
|
|
936
|
-
|
|
937
|
-
? (usage.cacheRead / promptTokens) * 100
|
|
938
|
-
: undefined;
|
|
939
|
-
}
|
|
1116
|
+
// Read cached stats instead of iterating all session entries +
|
|
1117
|
+
// calling getContextUsage() every frame. The cache is
|
|
1118
|
+
// recomputed on message_end / tool_execution_end.
|
|
1119
|
+
const stats = footerStatsCache;
|
|
1120
|
+
const totalCost = stats?.totalCost ?? 0;
|
|
1121
|
+
const latestCacheHitRate = stats?.latestCacheHitRate;
|
|
1122
|
+
const contextWindow = stats?.contextWindow ?? 0;
|
|
1123
|
+
const usedTokens = stats?.contextTokens;
|
|
940
1124
|
|
|
941
1125
|
const model = ctx.model;
|
|
942
|
-
const
|
|
943
|
-
|
|
944
|
-
|
|
1126
|
+
const usedLabel = usedTokens === null || usedTokens === undefined
|
|
1127
|
+
? "?"
|
|
1128
|
+
: formatTokens(usedTokens);
|
|
945
1129
|
const statsParts: string[] = [];
|
|
946
|
-
statsParts.push(`${
|
|
1130
|
+
statsParts.push(`${usedLabel}/${formatTokens(contextWindow)}`);
|
|
947
1131
|
if (latestCacheHitRate !== undefined) {
|
|
948
1132
|
statsParts.push(`CH${latestCacheHitRate.toFixed(1)}%`);
|
|
949
1133
|
}
|
|
950
1134
|
if (totalCost || (model && ctx.modelRegistry.isUsingOAuth(model))) {
|
|
951
1135
|
statsParts.push(`$${totalCost.toFixed(3)}`);
|
|
952
1136
|
}
|
|
953
|
-
let statsLeft = statsParts.join(" ");
|
|
1137
|
+
let statsLeft = isShellMode() ? "shell" : statsParts.join(" ");
|
|
954
1138
|
if (visibleWidth(statsLeft) > innerWidth) {
|
|
955
1139
|
statsLeft = truncateToWidth(statsLeft, innerWidth, "...");
|
|
956
1140
|
}
|
|
@@ -978,15 +1162,16 @@ export default async function piCustomAgentsPlugin(pi: any): Promise<void> {
|
|
|
978
1162
|
|
|
979
1163
|
const cwd = ctx.sessionManager.getCwd();
|
|
980
1164
|
const folderName = cwd.split(/[/\\]/).filter(Boolean).pop() ?? cwd;
|
|
981
|
-
const folderLine = PAD + theme.fg("dim", folderName) + PAD;
|
|
982
1165
|
const sessionName = ctx.sessionManager.getSessionName();
|
|
983
|
-
const lines = [
|
|
984
|
-
statsLine,
|
|
985
|
-
];
|
|
1166
|
+
const lines = [statsLine];
|
|
986
1167
|
const tps = getLiveTps();
|
|
1168
|
+
const folderLabel = theme.fg("dim", folderName);
|
|
1169
|
+
let folderLine = PAD + folderLabel + PAD;
|
|
987
1170
|
if (tps > 0) {
|
|
988
1171
|
const tpsStr = tps < 10 ? tps.toFixed(1) : tps < 100 ? tps.toFixed(0) : `${Math.round(tps)}`;
|
|
989
|
-
|
|
1172
|
+
const tpsText = theme.fg("accent", `${tpsStr} tps`);
|
|
1173
|
+
const tpsPadding = " ".repeat(Math.max(0, innerWidth - visibleWidth(folderLabel) - visibleWidth(tpsText)));
|
|
1174
|
+
folderLine = PAD + folderLabel + tpsPadding + tpsText + PAD;
|
|
990
1175
|
}
|
|
991
1176
|
lines.push(folderLine);
|
|
992
1177
|
if (sessionName) {
|
|
@@ -1028,20 +1213,24 @@ export default async function piCustomAgentsPlugin(pi: any): Promise<void> {
|
|
|
1028
1213
|
pi.setActiveTools(FULL_TOOLS);
|
|
1029
1214
|
}
|
|
1030
1215
|
setActiveMode(currentMode);
|
|
1031
|
-
pi.events.emit("pi-ember-ui:mode-change", { mode: currentMode });
|
|
1216
|
+
pi.events.emit("pi-ember-ui:mode-change", { mode: currentMode, liveOnly: true });
|
|
1032
1217
|
}
|
|
1033
1218
|
|
|
1034
1219
|
pi.on("session_start", async (_event: any, ctx: any) => {
|
|
1220
|
+
// The TUI can accept input while /resume is still rebinding extensions.
|
|
1221
|
+
// Keep mode switching lazy until all session-bound setup has finished.
|
|
1222
|
+
active_session_manager = ctx.sessionManager;
|
|
1223
|
+
session_ready = false;
|
|
1224
|
+
footerStatsCache = undefined;
|
|
1225
|
+
install_thinking_editor(ctx);
|
|
1035
1226
|
await restoreMode(ctx);
|
|
1036
1227
|
await restoreSavedModel(ctx);
|
|
1037
|
-
|
|
1038
|
-
|
|
1039
|
-
|
|
1040
|
-
|
|
1041
|
-
|
|
1042
|
-
|
|
1043
|
-
await restoreSavedModel(ctx);
|
|
1044
|
-
updateStatus(ctx);
|
|
1228
|
+
session_ready = true;
|
|
1229
|
+
recompute_footer_stats(ctx);
|
|
1230
|
+
const pending_mode = pending_mode_id;
|
|
1231
|
+
pending_mode_id = undefined;
|
|
1232
|
+
if (pending_mode) apply_mode(pending_mode, ctx);
|
|
1233
|
+
else updateStatus(ctx);
|
|
1045
1234
|
installCustomFooter(ctx);
|
|
1046
1235
|
});
|
|
1047
1236
|
|
|
@@ -1056,7 +1245,26 @@ export default async function piCustomAgentsPlugin(pi: any): Promise<void> {
|
|
|
1056
1245
|
writePersistedState({ ...persisted, model: identity });
|
|
1057
1246
|
});
|
|
1058
1247
|
|
|
1248
|
+
// Recompute footer stats when usage/context changes. These events fire
|
|
1249
|
+
// once per assistant message or tool result — not per render frame —
|
|
1250
|
+
// so the O(n) entry iteration + getContextUsage cost is amortized away
|
|
1251
|
+
// from the animation loop.
|
|
1252
|
+
pi.on("message_end", async (_event: any, ctx: any) => {
|
|
1253
|
+
if (is_live_session(ctx)) recompute_footer_stats(ctx);
|
|
1254
|
+
});
|
|
1255
|
+
pi.on("tool_execution_end", async (_event: any, ctx: any) => {
|
|
1256
|
+
if (is_live_session(ctx)) recompute_footer_stats(ctx);
|
|
1257
|
+
});
|
|
1258
|
+
|
|
1059
1259
|
pi.on("session_shutdown", (_event: any, ctx: any) => {
|
|
1260
|
+
// Invalidate shortcut contexts before the old runtime is disposed. A Tab
|
|
1261
|
+
// press during /resume is ignored instead of calling setActiveTools or
|
|
1262
|
+
// mutating UI state through a stale session.
|
|
1263
|
+
session_ready = false;
|
|
1264
|
+
active_session_manager = undefined;
|
|
1265
|
+
pending_mode_id = undefined;
|
|
1266
|
+
footerStatsCache = undefined;
|
|
1267
|
+
setShellMode(false);
|
|
1060
1268
|
const persisted = readPersistedState();
|
|
1061
1269
|
const model = ctx.model as Model<any> | undefined;
|
|
1062
1270
|
const modelIdentity = model
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { Type } from "typebox";
|
|
2
2
|
import {
|
|
3
|
+
Box,
|
|
3
4
|
Key,
|
|
4
5
|
Text,
|
|
5
6
|
matchesKey,
|
|
@@ -216,6 +217,7 @@ export function registerQuestionnaireTool(pi: any): void {
|
|
|
216
217
|
description: "Ask the user one or more decision questions inline before continuing.",
|
|
217
218
|
parameters: QuestionnaireParams,
|
|
218
219
|
executionMode: "sequential",
|
|
220
|
+
renderShell: "self",
|
|
219
221
|
async execute(
|
|
220
222
|
_toolCallId: string,
|
|
221
223
|
params: { questions: QuestionnaireQuestion[] },
|
|
@@ -258,22 +260,28 @@ export function registerQuestionnaireTool(pi: any): void {
|
|
|
258
260
|
},
|
|
259
261
|
renderCall(args: { questions?: QuestionnaireQuestion[] }, theme: any): any {
|
|
260
262
|
const count = args.questions?.length ?? 0;
|
|
261
|
-
|
|
263
|
+
const box = new Box(1, 1, (text: string) => theme.bg("userMessageBg", text));
|
|
264
|
+
box.addChild(new Text(
|
|
262
265
|
theme.fg("muted", "• ") +
|
|
263
266
|
theme.fg("toolTitle", theme.bold("questionnaire ")) +
|
|
264
267
|
theme.fg("muted", `${count} question${count === 1 ? "" : "s"}`),
|
|
265
268
|
0,
|
|
266
269
|
0,
|
|
267
|
-
);
|
|
270
|
+
));
|
|
271
|
+
return box;
|
|
268
272
|
},
|
|
269
273
|
renderResult(result: any, _options: unknown, theme: any): any {
|
|
270
274
|
const details = result.details as {
|
|
271
275
|
answers?: QuestionnaireAnswer[];
|
|
272
276
|
cancelled?: boolean;
|
|
273
277
|
} | undefined;
|
|
274
|
-
|
|
278
|
+
const box = new Box(1, 1, (text: string) => theme.bg("userMessageBg", text));
|
|
279
|
+
if (details?.cancelled) {
|
|
280
|
+
box.addChild(new Text(theme.fg("warning", "Cancelled"), 0, 0));
|
|
281
|
+
return box;
|
|
282
|
+
}
|
|
275
283
|
const answers = details?.answers ?? [];
|
|
276
|
-
|
|
284
|
+
box.addChild(new Text(
|
|
277
285
|
answers
|
|
278
286
|
.map((answer) => {
|
|
279
287
|
const label = `${answer.id}: ${answer.label}`;
|
|
@@ -282,7 +290,8 @@ export function registerQuestionnaireTool(pi: any): void {
|
|
|
282
290
|
.join("\n"),
|
|
283
291
|
0,
|
|
284
292
|
0,
|
|
285
|
-
);
|
|
293
|
+
));
|
|
294
|
+
return box;
|
|
286
295
|
},
|
|
287
296
|
});
|
|
288
297
|
}
|