pi-cc-extensions 0.8.32 → 0.8.33
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/extensions/claude-code-style.ts +419 -410
- package/extensions/compact-thinking.ts +147 -21
- package/extensions/fixed-editor.ts +186 -16
- package/extensions/tool-grouping.ts +4 -0
- package/package.json +1 -1
|
@@ -1,5 +1,9 @@
|
|
|
1
|
+
import * as piAi from "@earendil-works/pi-ai";
|
|
2
|
+
import * as piCodingAgent from "@earendil-works/pi-coding-agent";
|
|
1
3
|
import type { ExtensionAPI } from "@earendil-works/pi-coding-agent";
|
|
2
|
-
import
|
|
4
|
+
import * as piTui from "@earendil-works/pi-tui";
|
|
5
|
+
import { mkdirSync, readFileSync, writeFileSync } from "node:fs";
|
|
6
|
+
import { createRequire } from "node:module";
|
|
3
7
|
import { homedir } from "node:os";
|
|
4
8
|
import { join } from "node:path";
|
|
5
9
|
import { createJiti } from "jiti";
|
|
@@ -14,41 +18,163 @@ export type CompactThinkingController = {
|
|
|
14
18
|
updateConfig(next: CompactThinkingConfig): void;
|
|
15
19
|
};
|
|
16
20
|
|
|
21
|
+
type CompactThinkingOwner = {
|
|
22
|
+
owner: object;
|
|
23
|
+
stop(event?: any, ctx?: any): void;
|
|
24
|
+
};
|
|
25
|
+
|
|
26
|
+
const COMPACT_THINKING_OWNER = Symbol.for("pi.ccstyle.compact-thinking-owner");
|
|
27
|
+
|
|
17
28
|
export function installCompactThinking(
|
|
18
29
|
pi: ExtensionAPI,
|
|
19
30
|
initialConfig: CompactThinkingConfig,
|
|
20
31
|
): CompactThinkingController {
|
|
32
|
+
const owner = {};
|
|
33
|
+
const host = globalThis as typeof globalThis & {
|
|
34
|
+
[COMPACT_THINKING_OWNER]?: CompactThinkingOwner;
|
|
35
|
+
};
|
|
36
|
+
host[COMPACT_THINKING_OWNER]?.stop();
|
|
37
|
+
|
|
21
38
|
const agentDir = process.env.PI_CODING_AGENT_DIR ?? join(homedir(), ".pi", "agent");
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
39
|
+
// Upstream requires this file while loading; mirror ccstyle into it and never read it back.
|
|
40
|
+
mkdirSync(agentDir, { recursive: true });
|
|
41
|
+
writeFileSync(
|
|
42
|
+
join(agentDir, "compact-thinking.json"),
|
|
43
|
+
`${JSON.stringify(initialConfig, null, 2)}\n`,
|
|
44
|
+
"utf8",
|
|
45
|
+
);
|
|
46
|
+
|
|
47
|
+
// Fork the upstream entry so a running subagent tool keeps the thinking
|
|
48
|
+
// loading animation until the model emits the next text/thinking boundary.
|
|
49
|
+
// Upstream finalizes on tool_execution_start, freezing the summary into a
|
|
50
|
+
// static "Thought for Xs" for the whole subagent run.
|
|
51
|
+
const upstreamRequire = createRequire(import.meta.url);
|
|
52
|
+
const upstreamIndexPath = upstreamRequire.resolve("pi-compact-thinking/index.ts");
|
|
53
|
+
const toolStartOriginal = ` pi.on("tool_execution_start", finishThinking);`;
|
|
54
|
+
const toolStartPatched = ` // Subagent tools can run for minutes: keep the thinking loading animation
|
|
55
|
+
// alive for the whole execution and only finalize once the model emits the
|
|
56
|
+
// next text/thinking boundary or the message ends.
|
|
57
|
+
function resumeAgentThinking(message: AssistantMessage | undefined) {
|
|
58
|
+
if (activeThinking) return;
|
|
59
|
+
const content = message?.content;
|
|
60
|
+
if (!Array.isArray(content)) return;
|
|
61
|
+
const index = content.findIndex((item) => item?.type === "thinking");
|
|
62
|
+
if (index < 0) return;
|
|
63
|
+
startThinking(message, index);
|
|
64
|
+
}
|
|
65
|
+
pi.on("tool_execution_start", (event: any) => {
|
|
66
|
+
if (event.toolName === "Agent" || event.toolName === "Agents") {
|
|
67
|
+
resumeAgentThinking(latestComponent?.lastMessage ?? undefined);
|
|
68
|
+
} else {
|
|
69
|
+
finishThinking();
|
|
70
|
+
}
|
|
71
|
+
});`;
|
|
72
|
+
const updateBoundaryOriginal = ` } else if (
|
|
73
|
+
update.type === "text_start" ||
|
|
74
|
+
update.type === "toolcall_start" ||
|
|
75
|
+
update.type === "toolcall_delta"
|
|
76
|
+
) {
|
|
77
|
+
finishThinking();
|
|
78
|
+
}`;
|
|
79
|
+
const updateBoundaryPatched = ` } else if (update.type === "text_start") {
|
|
80
|
+
finishThinking();
|
|
81
|
+
} else if (
|
|
82
|
+
update.type === "toolcall_start" ||
|
|
83
|
+
update.type === "toolcall_delta"
|
|
84
|
+
) {
|
|
85
|
+
if (
|
|
86
|
+
Array.isArray(event.message?.content) &&
|
|
87
|
+
event.message.content.some(
|
|
88
|
+
(content) =>
|
|
89
|
+
content?.type === "toolCall" &&
|
|
90
|
+
(content.name === "Agent" ||
|
|
91
|
+
content.name === "Agents" ||
|
|
92
|
+
content.args?.subagent_type != null),
|
|
93
|
+
)
|
|
94
|
+
) {
|
|
95
|
+
resumeAgentThinking(event.message);
|
|
96
|
+
} else {
|
|
97
|
+
finishThinking();
|
|
98
|
+
}
|
|
99
|
+
}`;
|
|
100
|
+
let upstreamIndexSource = readFileSync(upstreamIndexPath, "utf8");
|
|
101
|
+
if (
|
|
102
|
+
upstreamIndexSource.includes(toolStartOriginal) &&
|
|
103
|
+
upstreamIndexSource.includes(updateBoundaryOriginal)
|
|
104
|
+
) {
|
|
105
|
+
upstreamIndexSource = upstreamIndexSource
|
|
106
|
+
.replace(toolStartOriginal, toolStartPatched)
|
|
107
|
+
.replace(updateBoundaryOriginal, updateBoundaryPatched);
|
|
108
|
+
} else {
|
|
109
|
+
// Upstream changed shape: fall back to the untouched entry so the loader
|
|
110
|
+
// keeps working; only the subagent animation nicety is lost.
|
|
111
|
+
upstreamIndexSource = readFileSync(upstreamIndexPath, "utf8");
|
|
38
112
|
}
|
|
39
113
|
|
|
40
|
-
//
|
|
41
|
-
const jiti = createJiti(import.meta.url
|
|
114
|
+
// Reuse Pi's live modules so the upstream prototype patch reaches runtime components.
|
|
115
|
+
const jiti = createJiti(import.meta.url, {
|
|
116
|
+
virtualModules: {
|
|
117
|
+
"@earendil-works/pi-ai": piAi,
|
|
118
|
+
"@earendil-works/pi-coding-agent": piCodingAgent,
|
|
119
|
+
"@earendil-works/pi-tui": piTui,
|
|
120
|
+
},
|
|
121
|
+
});
|
|
42
122
|
const upstreamConfig = (
|
|
43
123
|
jiti("pi-compact-thinking/lib/config.ts") as { config: CompactThinkingConfig }
|
|
44
124
|
).config;
|
|
45
125
|
const compactThinking = (
|
|
46
|
-
jiti(
|
|
126
|
+
jiti.evalModule(upstreamIndexSource, { filename: upstreamIndexPath }) as {
|
|
47
127
|
default: (api: ExtensionAPI) => void;
|
|
48
128
|
}
|
|
49
129
|
).default;
|
|
50
130
|
Object.assign(upstreamConfig, initialConfig);
|
|
51
|
-
|
|
131
|
+
|
|
132
|
+
// Upstream restores completedDurations from sessionManager.getBranch() (the
|
|
133
|
+
// current leaf path). After compaction or a branch switch, duration entries
|
|
134
|
+
// of older messages leave that path, so scrolling back renders a bare
|
|
135
|
+
// "Thinking..." instead of "Thought for Xs". Restore from every entry.
|
|
136
|
+
const restoreAllDurations = (ctx: any): any => {
|
|
137
|
+
const sessionManager = ctx?.sessionManager;
|
|
138
|
+
if (!sessionManager || typeof sessionManager.getEntries !== "function") return ctx;
|
|
139
|
+
return {
|
|
140
|
+
...ctx,
|
|
141
|
+
sessionManager: {
|
|
142
|
+
...sessionManager,
|
|
143
|
+
getBranch: () => sessionManager.getEntries(),
|
|
144
|
+
},
|
|
145
|
+
};
|
|
146
|
+
};
|
|
147
|
+
|
|
148
|
+
let session: { event: any; ctx: any } | undefined;
|
|
149
|
+
let shutdown: ((event: any, ctx: any) => void) | undefined;
|
|
150
|
+
let stopped = false;
|
|
151
|
+
const stop = (event?: any, ctx?: any) => {
|
|
152
|
+
if (stopped) return;
|
|
153
|
+
stopped = true;
|
|
154
|
+
shutdown?.(event ?? session?.event ?? {}, ctx ?? session?.ctx ?? { mode: "rpc", ui: {} });
|
|
155
|
+
if (host[COMPACT_THINKING_OWNER]?.owner === owner) delete host[COMPACT_THINKING_OWNER];
|
|
156
|
+
};
|
|
157
|
+
compactThinking({
|
|
158
|
+
on(event: string, handler: (event: any, ctx: any) => void) {
|
|
159
|
+
if (event === "session_start") {
|
|
160
|
+
pi.on(event, (startEvent, ctx) => {
|
|
161
|
+
session = { event: startEvent, ctx };
|
|
162
|
+
handler(startEvent, restoreAllDurations(ctx));
|
|
163
|
+
});
|
|
164
|
+
} else if (event === "session_shutdown") {
|
|
165
|
+
shutdown = handler;
|
|
166
|
+
pi.on(event, (shutdownEvent, ctx) => {
|
|
167
|
+
if (host[COMPACT_THINKING_OWNER]?.owner === owner) stop(shutdownEvent, ctx);
|
|
168
|
+
});
|
|
169
|
+
} else {
|
|
170
|
+
pi.on(event as any, (e: any, ctx: any) =>
|
|
171
|
+
handler(e, event === "session_tree" ? restoreAllDurations(ctx) : ctx),
|
|
172
|
+
);
|
|
173
|
+
}
|
|
174
|
+
},
|
|
175
|
+
appendEntry: (...args: any[]) => (pi.appendEntry as any)(...args),
|
|
176
|
+
} as unknown as ExtensionAPI);
|
|
177
|
+
host[COMPACT_THINKING_OWNER] = { owner, stop };
|
|
52
178
|
|
|
53
179
|
return {
|
|
54
180
|
updateConfig(next) {
|
|
@@ -12,21 +12,46 @@ const terminalSplit = jiti("@tifan/pi-fixed-editor/src/terminal-split.js") as {
|
|
|
12
12
|
TerminalSplitCompositor?: { prototype: Record<PropertyKey, any> };
|
|
13
13
|
};
|
|
14
14
|
export type FixedEditorHitbox = { row: number; startCol: number; endCol: number };
|
|
15
|
+
export type FixedEditorViewportSnapshot = {
|
|
16
|
+
tui: object;
|
|
17
|
+
visibleRootStart: number;
|
|
18
|
+
visibleScrollableRows: number;
|
|
19
|
+
visibleLines: readonly string[];
|
|
20
|
+
generation: number;
|
|
21
|
+
};
|
|
15
22
|
|
|
16
|
-
type FixedEditorSharedState = {
|
|
23
|
+
type FixedEditorSharedState = {
|
|
24
|
+
scrollButtonHitbox: FixedEditorHitbox | null;
|
|
25
|
+
viewports: WeakMap<object, FixedEditorViewportSnapshot>;
|
|
26
|
+
generation: number;
|
|
27
|
+
};
|
|
17
28
|
const FIXED_EDITOR_STATE_KEY = Symbol.for("pi.ccstyle.fixed-editor.state");
|
|
18
29
|
const fixedEditorState = (() => {
|
|
19
30
|
const host = globalThis as typeof globalThis & {
|
|
20
31
|
[FIXED_EDITOR_STATE_KEY]?: FixedEditorSharedState;
|
|
21
32
|
};
|
|
22
|
-
host[FIXED_EDITOR_STATE_KEY] ??= {
|
|
23
|
-
|
|
33
|
+
const state = (host[FIXED_EDITOR_STATE_KEY] ??= {
|
|
34
|
+
scrollButtonHitbox: null,
|
|
35
|
+
viewports: new WeakMap(),
|
|
36
|
+
generation: 0,
|
|
37
|
+
});
|
|
38
|
+
state.viewports ??= new WeakMap();
|
|
39
|
+
state.generation ??= 0;
|
|
40
|
+
return state;
|
|
24
41
|
})();
|
|
25
42
|
|
|
26
43
|
export function getFixedEditorScrollButtonHitbox(): FixedEditorHitbox | null {
|
|
27
44
|
return fixedEditorState.scrollButtonHitbox;
|
|
28
45
|
}
|
|
29
46
|
|
|
47
|
+
export function getFixedEditorViewport(tui: object): FixedEditorViewportSnapshot | null {
|
|
48
|
+
return fixedEditorState.viewports.get(tui) ?? null;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
function clearFixedEditorViewport(): void {
|
|
52
|
+
fixedEditorState.viewports = new WeakMap();
|
|
53
|
+
}
|
|
54
|
+
|
|
30
55
|
const FIXED_EDITOR_SPACING_PATCH = Symbol.for("pi.ccstyle.fixed-editor-spacing-patch");
|
|
31
56
|
const clusterModule = jiti("@tifan/pi-fixed-editor/src/cluster.js") as {
|
|
32
57
|
renderFixedEditorCluster?: (input: any) => any;
|
|
@@ -70,6 +95,9 @@ const loaded = jiti("@tifan/pi-fixed-editor/src/index.ts") as {
|
|
|
70
95
|
const fixedEditor = loaded.default ?? (loaded as unknown as FixedEditorExtension);
|
|
71
96
|
|
|
72
97
|
const IME_CURSOR_PATCH = Symbol.for("pi.ccstyle.fixed-editor-ime-cursor-patch");
|
|
98
|
+
const VIEWPORT_PATCH = Symbol.for("pi.ccstyle.fixed-editor-viewport-patch");
|
|
99
|
+
const MOUSE_WRITE_PATCH = Symbol.for("pi.ccstyle.fixed-editor-mouse-write-patch");
|
|
100
|
+
const MOUSE_WRITE_RESTORE_PATCH = Symbol.for("pi.ccstyle.fixed-editor-mouse-write-restore-patch");
|
|
73
101
|
|
|
74
102
|
function positionHiddenImeCursor(compositor: any): void {
|
|
75
103
|
if (
|
|
@@ -93,17 +121,68 @@ export function installFixedEditorImePatch(compositorClass: {
|
|
|
93
121
|
prototype: Record<PropertyKey, any>;
|
|
94
122
|
}): void {
|
|
95
123
|
const prototype = compositorClass.prototype;
|
|
96
|
-
if (prototype[IME_CURSOR_PATCH])
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
124
|
+
if (!prototype[IME_CURSOR_PATCH]) {
|
|
125
|
+
for (const name of ["write", "requestRepaint"] as const) {
|
|
126
|
+
const original = prototype[name];
|
|
127
|
+
if (typeof original !== "function") continue;
|
|
128
|
+
prototype[name] = function (this: any, ...args: any[]) {
|
|
129
|
+
const result = Reflect.apply(original, this, args);
|
|
130
|
+
positionHiddenImeCursor(this);
|
|
131
|
+
return result;
|
|
132
|
+
};
|
|
133
|
+
}
|
|
134
|
+
prototype[IME_CURSOR_PATCH] = true;
|
|
135
|
+
}
|
|
136
|
+
if (!prototype[VIEWPORT_PATCH] && typeof prototype.renderScrollableRoot === "function") {
|
|
137
|
+
const originalRenderScrollableRoot = prototype.renderScrollableRoot;
|
|
138
|
+
prototype.renderScrollableRoot = function (this: any, ...args: any[]) {
|
|
139
|
+
const lines = Reflect.apply(originalRenderScrollableRoot, this, args);
|
|
140
|
+
if (this.tui && !this.disposed && Array.isArray(lines)) {
|
|
141
|
+
fixedEditorState.viewports.set(this.tui, {
|
|
142
|
+
tui: this.tui,
|
|
143
|
+
visibleRootStart: Number(this.visibleRootStart) || 0,
|
|
144
|
+
visibleScrollableRows: Number(this.visibleScrollableRows) || lines.length,
|
|
145
|
+
visibleLines: [...lines],
|
|
146
|
+
generation: ++fixedEditorState.generation,
|
|
147
|
+
});
|
|
148
|
+
}
|
|
149
|
+
return lines;
|
|
150
|
+
};
|
|
151
|
+
prototype[VIEWPORT_PATCH] = true;
|
|
152
|
+
}
|
|
153
|
+
if (!prototype[MOUSE_WRITE_PATCH] && typeof prototype.install === "function") {
|
|
154
|
+
const originalInstall = prototype.install;
|
|
155
|
+
prototype.install = function (this: any, ...args: any[]) {
|
|
156
|
+
if (!Object.hasOwn(this, MOUSE_WRITE_PATCH) && typeof this.originalWrite === "function") {
|
|
157
|
+
const originalWrite = this.originalWrite;
|
|
158
|
+
this.originalWrite = (data: string) =>
|
|
159
|
+
Reflect.apply(originalWrite, this, [data.replaceAll("\x1b[?1002h", "\x1b[?1003h")]);
|
|
160
|
+
this[MOUSE_WRITE_PATCH] = originalWrite;
|
|
161
|
+
}
|
|
162
|
+
const wasInstalled = Boolean(this.installed);
|
|
163
|
+
const result = Reflect.apply(originalInstall, this, args);
|
|
164
|
+
// Dependents re-wrap doRender only after the compositor actually owns it.
|
|
165
|
+
if (!wasInstalled && this.installed && !this.disposed) compositorInstallNotify?.();
|
|
103
166
|
return result;
|
|
104
167
|
};
|
|
168
|
+
prototype[MOUSE_WRITE_PATCH] = true;
|
|
169
|
+
}
|
|
170
|
+
if (!prototype[MOUSE_WRITE_RESTORE_PATCH] && typeof prototype.dispose === "function") {
|
|
171
|
+
const originalDispose = prototype.dispose;
|
|
172
|
+
prototype.dispose = function (this: any, ...args: any[]) {
|
|
173
|
+
const originalWrite = Object.hasOwn(this, MOUSE_WRITE_PATCH)
|
|
174
|
+
? this[MOUSE_WRITE_PATCH]
|
|
175
|
+
: undefined;
|
|
176
|
+
try {
|
|
177
|
+
return Reflect.apply(originalDispose, this, args);
|
|
178
|
+
} finally {
|
|
179
|
+
if (typeof originalWrite === "function" && this.terminal) {
|
|
180
|
+
this.terminal.write = originalWrite;
|
|
181
|
+
}
|
|
182
|
+
}
|
|
183
|
+
};
|
|
184
|
+
prototype[MOUSE_WRITE_RESTORE_PATCH] = true;
|
|
105
185
|
}
|
|
106
|
-
prototype[IME_CURSOR_PATCH] = true;
|
|
107
186
|
}
|
|
108
187
|
|
|
109
188
|
if (terminalSplit.TerminalSplitCompositor) {
|
|
@@ -112,6 +191,8 @@ if (terminalSplit.TerminalSplitCompositor) {
|
|
|
112
191
|
|
|
113
192
|
export type FixedEditorController = {
|
|
114
193
|
setEnabled(enabled: boolean): void;
|
|
194
|
+
/** Fires after compositor install/reinstall (session start, enable, footer rebuild). */
|
|
195
|
+
onRebuild(listener: () => void): () => void;
|
|
115
196
|
};
|
|
116
197
|
|
|
117
198
|
type FixedEditorOwner = {
|
|
@@ -120,6 +201,39 @@ type FixedEditorOwner = {
|
|
|
120
201
|
};
|
|
121
202
|
|
|
122
203
|
const FIXED_EDITOR_OWNER = Symbol.for("pi.ccstyle.fixed-editor-owner");
|
|
204
|
+
const FIXED_EDITOR_FOOTER_HOOK = Symbol.for("pi.ccstyle.fixed-editor-footer-hook");
|
|
205
|
+
|
|
206
|
+
/** Active owner callback; fired from compositor.install after a fresh install succeeds. */
|
|
207
|
+
let compositorInstallNotify: (() => void) | undefined;
|
|
208
|
+
let compositorInstallNotifyOwner: object | undefined;
|
|
209
|
+
/** Runs before fixed-editor start/probe so dependents can unwrap doRender before capture. */
|
|
210
|
+
let beforeFixedEditorStart: (() => void) | undefined;
|
|
211
|
+
|
|
212
|
+
/** Register a pre-start hook (e.g. unwrap mouse doRender before compositor construction). */
|
|
213
|
+
export function setBeforeFixedEditorStart(listener: (() => void) | undefined): void {
|
|
214
|
+
beforeFixedEditorStart = listener;
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
type FooterHookState = { listener?: () => void };
|
|
218
|
+
|
|
219
|
+
function observeFooterReplacement(ui: any, listener: () => void): () => void {
|
|
220
|
+
let state = ui[FIXED_EDITOR_FOOTER_HOOK] as FooterHookState | undefined;
|
|
221
|
+
if (!state) {
|
|
222
|
+
const original = ui.setFooter;
|
|
223
|
+
state = {};
|
|
224
|
+
const hookState = state;
|
|
225
|
+
ui.setFooter = function (this: unknown, ...args: unknown[]) {
|
|
226
|
+
const result = Reflect.apply(original, this, args);
|
|
227
|
+
hookState.listener?.();
|
|
228
|
+
return result;
|
|
229
|
+
};
|
|
230
|
+
ui[FIXED_EDITOR_FOOTER_HOOK] = state;
|
|
231
|
+
}
|
|
232
|
+
state.listener = listener;
|
|
233
|
+
return () => {
|
|
234
|
+
if (state.listener === listener) state.listener = undefined;
|
|
235
|
+
};
|
|
236
|
+
}
|
|
123
237
|
|
|
124
238
|
/** Adds runtime on/off control around pi-fixed-editor's session lifecycle. */
|
|
125
239
|
export function installFixedEditor(
|
|
@@ -139,24 +253,74 @@ export function installFixedEditor(
|
|
|
139
253
|
let enabled = initiallyEnabled;
|
|
140
254
|
let active = false;
|
|
141
255
|
let session: { event: any; ctx: any } | undefined;
|
|
256
|
+
let unobserveFooter: (() => void) | undefined;
|
|
257
|
+
let footerRestartQueued = false;
|
|
258
|
+
const rebuildListeners = new Set<() => void>();
|
|
259
|
+
const notifyRebuild = () => {
|
|
260
|
+
for (const listener of rebuildListeners) listener();
|
|
261
|
+
};
|
|
142
262
|
|
|
143
|
-
const
|
|
263
|
+
const unhookFooter = () => {
|
|
264
|
+
unobserveFooter?.();
|
|
265
|
+
unobserveFooter = undefined;
|
|
266
|
+
};
|
|
267
|
+
const hookFooter = () => {
|
|
268
|
+
const ui = session?.ctx?.ui;
|
|
269
|
+
if (typeof ui?.setFooter !== "function") return;
|
|
270
|
+
unobserveFooter = observeFooterReplacement(ui, () => {
|
|
271
|
+
if (!active || footerRestartQueued) return;
|
|
272
|
+
footerRestartQueued = true;
|
|
273
|
+
const currentSession = session;
|
|
274
|
+
queueMicrotask(() => {
|
|
275
|
+
footerRestartQueued = false;
|
|
276
|
+
if (!active || session !== currentSession) return;
|
|
277
|
+
// Dispose old compositor first, then reinstall and rebind dependents.
|
|
278
|
+
deactivate();
|
|
279
|
+
activate();
|
|
280
|
+
});
|
|
281
|
+
});
|
|
282
|
+
};
|
|
283
|
+
function clearInstallNotify() {
|
|
284
|
+
if (compositorInstallNotifyOwner === owner) {
|
|
285
|
+
compositorInstallNotify = undefined;
|
|
286
|
+
compositorInstallNotifyOwner = undefined;
|
|
287
|
+
}
|
|
288
|
+
}
|
|
289
|
+
function deactivate() {
|
|
144
290
|
fixedEditorState.scrollButtonHitbox = null;
|
|
291
|
+
clearFixedEditorViewport();
|
|
145
292
|
const current = (globalThis as any)[FIXED_EDITOR_OWNER] as FixedEditorOwner | undefined;
|
|
146
293
|
if (current?.owner === owner) current.stop();
|
|
147
|
-
else
|
|
148
|
-
|
|
149
|
-
|
|
294
|
+
else {
|
|
295
|
+
clearInstallNotify();
|
|
296
|
+
unhookFooter();
|
|
297
|
+
active = false;
|
|
298
|
+
}
|
|
299
|
+
}
|
|
300
|
+
function activate() {
|
|
150
301
|
if (!session || session.ctx?.mode !== "tui" || active) return;
|
|
151
302
|
const previous = (globalThis as any)[FIXED_EDITOR_OWNER] as FixedEditorOwner | undefined;
|
|
152
303
|
if (previous?.owner !== owner) previous?.stop();
|
|
153
304
|
const currentSession = session;
|
|
305
|
+
// Register before start/probe so the first real compositor.install notifies us.
|
|
306
|
+
compositorInstallNotify = () => {
|
|
307
|
+
if (!active) return;
|
|
308
|
+
if ((globalThis as any)[FIXED_EDITOR_OWNER]?.owner !== owner) return;
|
|
309
|
+
notifyRebuild();
|
|
310
|
+
};
|
|
311
|
+
compositorInstallNotifyOwner = owner;
|
|
312
|
+
// Unwrap outer doRender patches before compositor construction captures it.
|
|
313
|
+
beforeFixedEditorStart?.();
|
|
154
314
|
start?.(currentSession.event, currentSession.ctx);
|
|
155
315
|
active = true;
|
|
316
|
+
hookFooter();
|
|
156
317
|
(globalThis as any)[FIXED_EDITOR_OWNER] = {
|
|
157
318
|
owner,
|
|
158
319
|
stop: () => {
|
|
159
320
|
if (!active) return;
|
|
321
|
+
clearInstallNotify();
|
|
322
|
+
unhookFooter();
|
|
323
|
+
clearFixedEditorViewport();
|
|
160
324
|
shutdown?.(currentSession.event, currentSession.ctx);
|
|
161
325
|
active = false;
|
|
162
326
|
if ((globalThis as any)[FIXED_EDITOR_OWNER]?.owner === owner) {
|
|
@@ -164,7 +328,7 @@ export function installFixedEditor(
|
|
|
164
328
|
}
|
|
165
329
|
},
|
|
166
330
|
} satisfies FixedEditorOwner;
|
|
167
|
-
}
|
|
331
|
+
}
|
|
168
332
|
|
|
169
333
|
pi.on("session_start", (event, ctx) => {
|
|
170
334
|
session = { event, ctx };
|
|
@@ -181,5 +345,11 @@ export function installFixedEditor(
|
|
|
181
345
|
if (nextEnabled) activate();
|
|
182
346
|
else deactivate();
|
|
183
347
|
},
|
|
348
|
+
onRebuild(listener) {
|
|
349
|
+
rebuildListeners.add(listener);
|
|
350
|
+
return () => {
|
|
351
|
+
rebuildListeners.delete(listener);
|
|
352
|
+
};
|
|
353
|
+
},
|
|
184
354
|
};
|
|
185
355
|
}
|
|
@@ -154,6 +154,10 @@ function toolSummary(tool: any): { main: string; detail: string } {
|
|
|
154
154
|
return `${titled} ${oneLine(found || fallback)}`;
|
|
155
155
|
};
|
|
156
156
|
if (lowerName === "agent" || lowerName === "agents") {
|
|
157
|
+
const displayName = args.subagent_type ?? args.agent_type ?? args.agent;
|
|
158
|
+
if (typeof displayName === "string" && displayName) {
|
|
159
|
+
return { main: `${titled} ${displayName}`, detail: "" };
|
|
160
|
+
}
|
|
157
161
|
return {
|
|
158
162
|
main: value(
|
|
159
163
|
lowerName === "agent" ? "launch agent" : "launch agents",
|
package/package.json
CHANGED