pi-cc-extensions 0.8.46 → 0.8.48

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.
@@ -1,458 +0,0 @@
1
- import type { ExtensionAPI } from "@earendil-works/pi-coding-agent";
2
- import * as piCodingAgent from "@earendil-works/pi-coding-agent";
3
- import * as piTui from "@earendil-works/pi-tui";
4
- import { visibleWidth } from "@earendil-works/pi-tui";
5
- import { isFullscreenUi, isLazyProxyTui } from "./fullscreen-detect.ts";
6
- import { createJiti } from "jiti";
7
-
8
- type LifecycleHandler = (event: any, ctx: any) => void;
9
- type FixedEditorExtension = (pi: ExtensionAPI) => void;
10
-
11
- export type FixedEditorHitbox = { row: number; startCol: number; endCol: number };
12
- export type FixedEditorViewportSnapshot = {
13
- tui: object;
14
- visibleRootStart: number;
15
- visibleScrollableRows: number;
16
- visibleLines: readonly string[];
17
- generation: number;
18
- };
19
-
20
- type FixedEditorSharedState = {
21
- scrollButtonHitbox: FixedEditorHitbox | null;
22
- viewports: WeakMap<object, FixedEditorViewportSnapshot>;
23
- generation: number;
24
- };
25
- const FIXED_EDITOR_STATE_KEY = Symbol.for("pi.ccstyle.fixed-editor.state");
26
- const fixedEditorState = (() => {
27
- const host = globalThis as typeof globalThis & {
28
- [FIXED_EDITOR_STATE_KEY]?: FixedEditorSharedState;
29
- };
30
- const state = (host[FIXED_EDITOR_STATE_KEY] ??= {
31
- scrollButtonHitbox: null,
32
- viewports: new WeakMap(),
33
- generation: 0,
34
- });
35
- state.viewports ??= new WeakMap();
36
- state.generation ??= 0;
37
- return state;
38
- })();
39
-
40
- export function getFixedEditorScrollButtonHitbox(): FixedEditorHitbox | null {
41
- return fixedEditorState.scrollButtonHitbox;
42
- }
43
-
44
- export function getFixedEditorViewport(tui: object): FixedEditorViewportSnapshot | null {
45
- return fixedEditorState.viewports.get(tui) ?? null;
46
- }
47
-
48
- function clearFixedEditorViewport(): void {
49
- fixedEditorState.viewports = new WeakMap();
50
- }
51
-
52
- type UpstreamFixedEditor = {
53
- start?: LifecycleHandler;
54
- shutdown?: LifecycleHandler;
55
- };
56
-
57
- // 上游按需加载:0.84+ 的 tui 是惰性 Proxy(installFixedEditor 用
58
- // isLazyProxySession 探测),fixed-editor 整体停用 —— 连上游包都不解析,
59
- // jiti 失败或包缺失都不影响主流程。
60
- let upstreamLoaded = false;
61
- let upstream: UpstreamFixedEditor | undefined;
62
- function loadUpstream(): UpstreamFixedEditor | undefined {
63
- if (upstreamLoaded) return upstream;
64
- upstreamLoaded = true;
65
- try {
66
- // Upstream publishes TS only. Nested jiti must reuse Pi's live modules — peer
67
- // deps are not resolvable from ~/.pi/agent/npm/node_modules/pi-cc-extensions.
68
- const jiti = createJiti(import.meta.url, {
69
- virtualModules: {
70
- "@earendil-works/pi-coding-agent": piCodingAgent,
71
- "@earendil-works/pi-tui": piTui,
72
- },
73
- });
74
- // Match pi-fixed-editor's own `.js` import specifier; Jiti caches `.ts` separately.
75
- const terminalSplit = jiti("@tifan/pi-fixed-editor/src/terminal-split.js") as {
76
- TerminalSplitCompositor?: { prototype: Record<PropertyKey, any> };
77
- };
78
-
79
- const FIXED_EDITOR_SPACING_PATCH = Symbol.for("pi.ccstyle.fixed-editor-spacing-patch");
80
- const clusterModule = jiti("@tifan/pi-fixed-editor/src/cluster.js") as {
81
- renderFixedEditorCluster?: (input: any) => any;
82
- };
83
- if (
84
- !(clusterModule as any)[FIXED_EDITOR_SPACING_PATCH] &&
85
- clusterModule.renderFixedEditorCluster
86
- ) {
87
- const renderCluster = clusterModule.renderFixedEditorCluster;
88
- const padWidgets = (lines: string[] | undefined) =>
89
- lines?.map((line) => (line.length > 0 && !line.includes("Ctrl+End") ? ` ${line}` : line));
90
- clusterModule.renderFixedEditorCluster = (input) => {
91
- const aboveWidgetLines = padWidgets(input.aboveWidgetLines);
92
- const result = renderCluster({
93
- ...input,
94
- // pi-fixed-editor strips Pi's leading widget spacer; restore it below the spinner.
95
- aboveWidgetLines:
96
- input.statusLines?.length && aboveWidgetLines?.length
97
- ? ["", ...aboveWidgetLines]
98
- : aboveWidgetLines,
99
- belowWidgetLines: padWidgets(input.belowWidgetLines),
100
- });
101
- const buttonRow = result.lines.findIndex((line: string) => line.includes("Ctrl+End"));
102
- if (buttonRow < 0) {
103
- fixedEditorState.scrollButtonHitbox = null;
104
- } else {
105
- const plain = result.lines[buttonRow]
106
- .replace(/\x1b\][^\x07]*(?:\x07|\x1b\\)/g, "")
107
- .replace(/\x1b\[[0-?]*[ -/]*[@-~]/g, "");
108
- const start = plain.indexOf("[");
109
- fixedEditorState.scrollButtonHitbox = {
110
- row: input.terminalRows - result.lines.length + buttonRow + 1,
111
- startCol: visibleWidth(plain.slice(0, Math.max(0, start))) + 1,
112
- endCol: visibleWidth(plain.trimEnd()),
113
- };
114
- }
115
- return result;
116
- };
117
- (clusterModule as any)[FIXED_EDITOR_SPACING_PATCH] = true;
118
- }
119
- const loaded = jiti("@tifan/pi-fixed-editor/src/index.ts") as {
120
- default?: FixedEditorExtension;
121
- };
122
- const fixedEditor = loaded.default ?? (loaded as unknown as FixedEditorExtension);
123
-
124
- if (terminalSplit.TerminalSplitCompositor) {
125
- installFixedEditorImePatch(terminalSplit.TerminalSplitCompositor);
126
- // 0.84+ 的 tui 是惰性 Proxy:compositor 构造时捕获的 doRender/render 是
127
- // 每次重新解析的包装,install 后执行会解析到 compositor 自身(无限递归)。
128
- // 检测到惰性 Proxy 时跳过安装,渲染管线完全交给官方。
129
- installProxyGuard(terminalSplit.TerminalSplitCompositor);
130
- }
131
-
132
- let start: LifecycleHandler | undefined;
133
- let shutdown: LifecycleHandler | undefined;
134
- fixedEditor({
135
- on(event: string, handler: LifecycleHandler) {
136
- if (event === "session_start") start = handler;
137
- if (event === "session_shutdown") shutdown = handler;
138
- },
139
- } as unknown as ExtensionAPI);
140
-
141
- upstream = { start, shutdown };
142
- } catch (error) {
143
- console.error("pi-cc-extensions: failed to load @tifan/pi-fixed-editor", error);
144
- upstream = undefined;
145
- }
146
- return upstream;
147
- }
148
-
149
- const COMPOSITOR_PROXY_GUARD = Symbol.for("pi.ccstyle.compositor-proxy-guard");
150
- const IME_CURSOR_PATCH = Symbol.for("pi.ccstyle.fixed-editor-ime-cursor-patch");
151
- const VIEWPORT_PATCH = Symbol.for("pi.ccstyle.fixed-editor-viewport-patch");
152
- const MOUSE_WRITE_PATCH = Symbol.for("pi.ccstyle.fixed-editor-mouse-write-patch");
153
- const MOUSE_WRITE_RESTORE_PATCH = Symbol.for("pi.ccstyle.fixed-editor-mouse-write-restore-patch");
154
-
155
- function positionHiddenImeCursor(compositor: any): void {
156
- if (
157
- compositor.disposed ||
158
- compositor.writing ||
159
- compositor.getShowHardwareCursor?.() ||
160
- compositor.hasVisibleOverlay?.()
161
- )
162
- return;
163
- const rows = compositor.getRawRows?.();
164
- if (!Number.isFinite(rows)) return;
165
- const width = Math.max(1, Number(compositor.terminal?.columns) || 80);
166
- const cluster = compositor.getCluster?.(width, rows);
167
- if (!cluster?.cursor) return;
168
- const row = Math.max(1, rows - cluster.lines.length + cluster.cursor.row + 1);
169
- const col = Math.max(1, cluster.cursor.col + 1);
170
- compositor.originalWrite?.(`\x1b[?2026h\x1b[${row};${col}H\x1b[?25l\x1b[?2026l`);
171
- }
172
-
173
- export function installFixedEditorImePatch(compositorClass: {
174
- prototype: Record<PropertyKey, any>;
175
- }): void {
176
- const prototype = compositorClass.prototype;
177
- if (!prototype[IME_CURSOR_PATCH]) {
178
- for (const name of ["write", "requestRepaint"] as const) {
179
- const original = prototype[name];
180
- if (typeof original !== "function") continue;
181
- prototype[name] = function (this: any, ...args: any[]) {
182
- const result = Reflect.apply(original, this, args);
183
- positionHiddenImeCursor(this);
184
- return result;
185
- };
186
- }
187
- prototype[IME_CURSOR_PATCH] = true;
188
- }
189
- if (!prototype[VIEWPORT_PATCH] && typeof prototype.renderScrollableRoot === "function") {
190
- const originalRenderScrollableRoot = prototype.renderScrollableRoot;
191
- prototype.renderScrollableRoot = function (this: any, ...args: any[]) {
192
- const lines = Reflect.apply(originalRenderScrollableRoot, this, args);
193
- if (this.tui && !this.disposed && Array.isArray(lines)) {
194
- fixedEditorState.viewports.set(this.tui, {
195
- tui: this.tui,
196
- visibleRootStart: Number(this.visibleRootStart) || 0,
197
- visibleScrollableRows: Number(this.visibleScrollableRows) || lines.length,
198
- visibleLines: [...lines],
199
- generation: ++fixedEditorState.generation,
200
- });
201
- }
202
- return lines;
203
- };
204
- prototype[VIEWPORT_PATCH] = true;
205
- }
206
- if (!prototype[MOUSE_WRITE_PATCH] && typeof prototype.install === "function") {
207
- const originalInstall = prototype.install;
208
- prototype.install = function (this: any, ...args: any[]) {
209
- if (!Object.hasOwn(this, MOUSE_WRITE_PATCH) && typeof this.originalWrite === "function") {
210
- const originalWrite = this.originalWrite;
211
- this.originalWrite = (data: string) =>
212
- Reflect.apply(originalWrite, this, [data.replaceAll("\x1b[?1002h", "\x1b[?1003h")]);
213
- this[MOUSE_WRITE_PATCH] = originalWrite;
214
- }
215
- const wasInstalled = Boolean(this.installed);
216
- const result = Reflect.apply(originalInstall, this, args);
217
- // Dependents re-wrap doRender only after the compositor actually owns it.
218
- // Read the global each time: the patch survives /reload on the shared
219
- // prototype while the owner callback belongs to the live extension module.
220
- if (!wasInstalled && this.installed && !this.disposed)
221
- getCompositorInstallNotify()?.callback();
222
- return result;
223
- };
224
- prototype[MOUSE_WRITE_PATCH] = true;
225
- }
226
- if (!prototype[MOUSE_WRITE_RESTORE_PATCH] && typeof prototype.dispose === "function") {
227
- const originalDispose = prototype.dispose;
228
- prototype.dispose = function (this: any, ...args: any[]) {
229
- const originalWrite = Object.hasOwn(this, MOUSE_WRITE_PATCH)
230
- ? this[MOUSE_WRITE_PATCH]
231
- : undefined;
232
- try {
233
- return Reflect.apply(originalDispose, this, args);
234
- } finally {
235
- if (typeof originalWrite === "function" && this.terminal) {
236
- this.terminal.write = originalWrite;
237
- }
238
- }
239
- };
240
- prototype[MOUSE_WRITE_RESTORE_PATCH] = true;
241
- }
242
- }
243
-
244
- function installProxyGuard(compositorClass: { prototype: Record<PropertyKey, any> }): void {
245
- const prototype = compositorClass.prototype;
246
- const originalInstall = prototype.install;
247
- if (typeof originalInstall !== "function" || prototype[COMPOSITOR_PROXY_GUARD]) return;
248
- prototype[COMPOSITOR_PROXY_GUARD] = true;
249
- prototype.install = function (this: any, ...args: any[]) {
250
- if (isLazyProxyTui(this.tui)) return;
251
- return Reflect.apply(originalInstall, this, args);
252
- };
253
- }
254
-
255
- export type FixedEditorController = {
256
- setEnabled(enabled: boolean): void;
257
- /** Fires after compositor install/reinstall (session start, enable, footer rebuild). */
258
- onRebuild(listener: () => void): () => void;
259
- };
260
-
261
- type FixedEditorOwner = {
262
- owner: object;
263
- stop: () => void;
264
- };
265
-
266
- const FIXED_EDITOR_OWNER = Symbol.for("pi.ccstyle.fixed-editor-owner");
267
- const FIXED_EDITOR_FOOTER_HOOK = Symbol.for("pi.ccstyle.fixed-editor-footer-hook");
268
-
269
- // /reload keeps the same TerminalSplitCompositor.prototype, so the install hook
270
- // patched by the pre-reload module must read a cross-reload global instead of a
271
- // module-local variable; otherwise notifyRebuild never fires after reload and
272
- // dependents (ccstyle mouse input capture) stay unbound.
273
- const COMPOSITOR_INSTALL_NOTIFY_KEY = Symbol.for("pi.ccstyle.compositor-install-notify");
274
-
275
- type CompositorInstallNotify = { owner: object; callback: () => void };
276
- function getCompositorInstallNotify(): CompositorInstallNotify | undefined {
277
- return (globalThis as Record<PropertyKey, unknown>)[COMPOSITOR_INSTALL_NOTIFY_KEY] as
278
- | CompositorInstallNotify
279
- | undefined;
280
- }
281
- function setCompositorInstallNotify(value: CompositorInstallNotify | undefined): void {
282
- if (value) (globalThis as Record<PropertyKey, unknown>)[COMPOSITOR_INSTALL_NOTIFY_KEY] = value;
283
- else delete (globalThis as Record<PropertyKey, unknown>)[COMPOSITOR_INSTALL_NOTIFY_KEY];
284
- }
285
- /** Runs before fixed-editor start/probe so dependents can unwrap doRender before capture. */
286
- let beforeFixedEditorStart: (() => void) | undefined;
287
-
288
- /** Register a pre-start hook (e.g. unwrap mouse doRender before compositor construction). */
289
- export function setBeforeFixedEditorStart(listener: (() => void) | undefined): void {
290
- beforeFixedEditorStart = listener;
291
- }
292
-
293
- type FooterHookState = { listener?: () => void };
294
-
295
- function observeFooterReplacement(ui: any, listener: () => void): () => void {
296
- let state = ui[FIXED_EDITOR_FOOTER_HOOK] as FooterHookState | undefined;
297
- if (!state) {
298
- const original = ui.setFooter;
299
- state = {};
300
- const hookState = state;
301
- ui.setFooter = function (this: unknown, ...args: unknown[]) {
302
- const result = Reflect.apply(original, this, args);
303
- hookState.listener?.();
304
- return result;
305
- };
306
- ui[FIXED_EDITOR_FOOTER_HOOK] = state;
307
- }
308
- state.listener = listener;
309
- return () => {
310
- if (state.listener === listener) state.listener = undefined;
311
- };
312
- }
313
-
314
- const LAZY_PROXY_PROBE_KEY = "cc-lazy-proxy-probe";
315
-
316
- /**
317
- * 测试钩子:会话未激活时直接加载上游。懒加载下 patch 仅在 activate 时执行,
318
- * 直接 new 上游 compositor / 调用上游 cluster 的测试需要显式触发。
319
- */
320
- export function loadFixedEditorUpstreamForTests(): void {
321
- loadUpstream();
322
- }
323
-
324
- /** 探测当前会话的 TUI 是否为 0.84+ 惰性 Proxy(与 isFullscreenUi 同模式)。 */
325
- function isLazyProxySession(ctx: any): boolean {
326
- if (typeof ctx?.ui?.setWidget !== "function") return false;
327
- let proxyTui = false;
328
- try {
329
- ctx.ui.setWidget(LAZY_PROXY_PROBE_KEY, (tui: any) => {
330
- proxyTui = isLazyProxyTui(tui);
331
- return { render: () => [], invalidate() {} };
332
- });
333
- } catch {
334
- // 探测失败按 regular 处理,避免误伤
335
- }
336
- try {
337
- ctx.ui.setWidget(LAZY_PROXY_PROBE_KEY, undefined);
338
- } catch {
339
- // 忽略
340
- }
341
- return proxyTui;
342
- }
343
-
344
- /** Adds runtime on/off control around pi-fixed-editor's session lifecycle. */
345
- export function installFixedEditor(
346
- pi: ExtensionAPI,
347
- initiallyEnabled: boolean,
348
- ): FixedEditorController {
349
- const owner = {};
350
- let enabled = initiallyEnabled;
351
- let active = false;
352
- let session: { event: any; ctx: any } | undefined;
353
- let unobserveFooter: (() => void) | undefined;
354
- let footerRestartQueued = false;
355
- const rebuildListeners = new Set<() => void>();
356
- const notifyRebuild = () => {
357
- for (const listener of rebuildListeners) listener();
358
- };
359
-
360
- const unhookFooter = () => {
361
- unobserveFooter?.();
362
- unobserveFooter = undefined;
363
- };
364
- const hookFooter = () => {
365
- const ui = session?.ctx?.ui;
366
- if (typeof ui?.setFooter !== "function") return;
367
- unobserveFooter = observeFooterReplacement(ui, () => {
368
- if (!active || footerRestartQueued) return;
369
- footerRestartQueued = true;
370
- const currentSession = session;
371
- queueMicrotask(() => {
372
- footerRestartQueued = false;
373
- if (!active || session !== currentSession) return;
374
- // Dispose old compositor first, then reinstall and rebind dependents.
375
- deactivate();
376
- activate();
377
- });
378
- });
379
- };
380
- function clearInstallNotify() {
381
- if (getCompositorInstallNotify()?.owner === owner) setCompositorInstallNotify(undefined);
382
- }
383
- function deactivate() {
384
- fixedEditorState.scrollButtonHitbox = null;
385
- clearFixedEditorViewport();
386
- const current = (globalThis as any)[FIXED_EDITOR_OWNER] as FixedEditorOwner | undefined;
387
- if (current?.owner === owner) current.stop();
388
- else {
389
- clearInstallNotify();
390
- unhookFooter();
391
- active = false;
392
- }
393
- }
394
- function activate() {
395
- if (!session || session.ctx?.mode !== "tui" || active) return;
396
- // fullscreen 复用官方的 sticky editor,固定编辑器让位
397
- if (isFullscreenUi(session.ctx)) return;
398
- // 0.84+ 的 tui 是惰性 Proxy:fixed-editor 整体停用,不加载上游包。
399
- if (isLazyProxySession(session.ctx)) return;
400
- const loadedUpstream = loadUpstream();
401
- if (!loadedUpstream?.start || !loadedUpstream.shutdown) return;
402
- const { start, shutdown } = loadedUpstream;
403
- const previous = (globalThis as any)[FIXED_EDITOR_OWNER] as FixedEditorOwner | undefined;
404
- if (previous?.owner !== owner) previous?.stop();
405
- const currentSession = session;
406
- // Register before start/probe so the first real compositor.install notifies us.
407
- setCompositorInstallNotify({
408
- owner,
409
- callback: () => {
410
- if (!active) return;
411
- if ((globalThis as any)[FIXED_EDITOR_OWNER]?.owner !== owner) return;
412
- notifyRebuild();
413
- },
414
- });
415
- // Unwrap outer doRender patches before compositor construction captures it.
416
- beforeFixedEditorStart?.();
417
- start?.(currentSession.event, currentSession.ctx);
418
- active = true;
419
- hookFooter();
420
- (globalThis as any)[FIXED_EDITOR_OWNER] = {
421
- owner,
422
- stop: () => {
423
- if (!active) return;
424
- clearInstallNotify();
425
- unhookFooter();
426
- clearFixedEditorViewport();
427
- shutdown?.(currentSession.event, currentSession.ctx);
428
- active = false;
429
- if ((globalThis as any)[FIXED_EDITOR_OWNER]?.owner === owner) {
430
- delete (globalThis as any)[FIXED_EDITOR_OWNER];
431
- }
432
- },
433
- } satisfies FixedEditorOwner;
434
- }
435
-
436
- pi.on("session_start", (event, ctx) => {
437
- session = { event, ctx };
438
- if (enabled) activate();
439
- });
440
- pi.on("session_shutdown", () => {
441
- deactivate();
442
- session = undefined;
443
- });
444
-
445
- return {
446
- setEnabled(nextEnabled) {
447
- enabled = nextEnabled;
448
- if (nextEnabled) activate();
449
- else deactivate();
450
- },
451
- onRebuild(listener) {
452
- rebuildListeners.add(listener);
453
- return () => {
454
- rebuildListeners.delete(listener);
455
- };
456
- },
457
- };
458
- }
@@ -1,121 +0,0 @@
1
- import { CustomMessageComponent, type ExtensionAPI } from "@earendil-works/pi-coding-agent";
2
- import { truncateToWidth } from "@earendil-works/pi-tui";
3
-
4
- const PATCH_KEY = Symbol.for("pi.ccstyle.subagent-notification-patch");
5
- const ANSI_RE = /\x1b\[[0-?]*[ -/]*[@-~]/g;
6
-
7
- type Patch = {
8
- active: boolean;
9
- prototype: any;
10
- originalRender: (width: number) => string[];
11
- installedRender: (width: number) => string[];
12
- };
13
-
14
- function plain(line: string): string {
15
- return line.replace(ANSI_RE, "");
16
- }
17
-
18
- function trimBlankLines(lines: string[]): string[] {
19
- let start = 0;
20
- let end = lines.length;
21
- while (start < end && !plain(lines[start]!).trim()) start++;
22
- while (end > start && !plain(lines[end - 1]!).trim()) end--;
23
- return lines.slice(start, end);
24
- }
25
-
26
- function normalizeStatusGlyph(line: string): string {
27
- return line.replace(
28
- /^((?:\x1b\[[0-?]*[ -/]*[@-~]|[ \t]|[├└│─])*)[✓✔]((?:\x1b\[[0-?]*[ -/]*[@-~])*)(?=\s)/,
29
- "$1●$2",
30
- );
31
- }
32
-
33
- function isHeader(line: string): boolean {
34
- return /^[✓✔✗■●]\s+/.test(plain(line).trimStart());
35
- }
36
-
37
- function isDetail(line: string): boolean {
38
- const text = plain(line).trimStart();
39
- return (
40
- text.startsWith("⎿") ||
41
- text.startsWith("transcript:") ||
42
- text === "No output." ||
43
- /^(?:Done|Wrapped up|Stopped|Error:|Aborted)\b/.test(text)
44
- );
45
- }
46
-
47
- function cleanDetail(line: string): string {
48
- const marker = line.indexOf("⎿");
49
- if (marker >= 0) return line.slice(marker + 1).replace(/^\s+/, "");
50
- return line.replace(/^\s+/, "");
51
- }
52
-
53
- function formatGroup(lines: string[]): string[] {
54
- if (!lines.length) return [];
55
- const header = normalizeStatusGlyph(lines[0]!);
56
- const rest = lines.slice(1);
57
- const detailStart = rest.findIndex(isDetail);
58
- if (detailStart < 0) return [header, ...rest];
59
- const metadata = rest.slice(0, detailStart);
60
- const details = rest
61
- .slice(detailStart)
62
- .map(cleanDetail)
63
- .filter((line) => plain(line).trim());
64
- return [
65
- header,
66
- ...metadata,
67
- ...details.map((line, index) => `${index === details.length - 1 ? "└" : "├"} ${line}`),
68
- ];
69
- }
70
-
71
- export function formatSubagentNotificationLines(lines: string[], width: number): string[] {
72
- const groups: string[][] = [];
73
- for (const line of trimBlankLines(lines)) {
74
- if (isHeader(line) && groups.length && groups.at(-1)!.length) groups.push([]);
75
- if (!groups.length) groups.push([]);
76
- groups.at(-1)!.push(line);
77
- }
78
- return groups.flatMap((group, index) => {
79
- const formatted = formatGroup(group).map((line) =>
80
- truncateToWidth(line ? ` ${line}` : line, Math.max(1, width), ""),
81
- );
82
- return index ? ["", ...formatted] : formatted;
83
- });
84
- }
85
-
86
- export default function subagentNotificationExtension(pi: ExtensionAPI): void {
87
- let patch: Patch | undefined;
88
-
89
- pi.on("session_start", async (_event, ctx) => {
90
- if (patch || ctx?.mode !== "tui" || !ctx?.hasUI) return;
91
- const prototype = CustomMessageComponent.prototype as any;
92
- const host = globalThis as any;
93
- const previous = host[PATCH_KEY] as Patch | undefined;
94
- if (previous) previous.active = false;
95
- const originalRender =
96
- previous && prototype.render === previous.installedRender
97
- ? previous.originalRender
98
- : prototype.render;
99
- patch = {
100
- active: true,
101
- prototype,
102
- originalRender,
103
- installedRender: undefined as any,
104
- };
105
- const installed = patch;
106
- installed.installedRender = function (this: any, width: number): string[] {
107
- const lines = originalRender.call(this, width);
108
- if (!installed.active || this?.message?.customType !== "subagent-notification") return lines;
109
- return formatSubagentNotificationLines(lines, width);
110
- };
111
- prototype.render = installed.installedRender;
112
- host[PATCH_KEY] = installed;
113
- });
114
-
115
- pi.on("session_shutdown", async () => {
116
- if (!patch?.active) return;
117
- patch.active = false;
118
- if (patch.prototype.render === patch.installedRender)
119
- patch.prototype.render = patch.originalRender;
120
- });
121
- }
@@ -1,36 +0,0 @@
1
- # @tifan/pi-fixed-editor
2
-
3
- Keep Pi's editor, footer, and editor-adjacent widgets fixed at the bottom of the terminal while the transcript scrolls.
4
-
5
- ## Install
6
-
7
- ```bash
8
- pi install npm:@tifan/pi-fixed-editor
9
- ```
10
-
11
- ## What it does
12
-
13
- - Pins Pi's existing editor/footer area to the bottom of the terminal.
14
- - Keeps `aboveEditor` and `belowEditor` widgets with the editor.
15
- - Preserves existing editor behavior, footer content, and autocomplete.
16
- - Routes mouse wheel and PageUp/PageDown scrolling to the transcript.
17
- - Jumps the transcript back to the bottom when you submit a query with Enter.
18
-
19
- ## What it does not do
20
-
21
- - No styling.
22
- - No powerline footer.
23
- - No commands.
24
- - No settings.
25
- - No custom shortcuts.
26
- - No print or RPC mode behavior.
27
-
28
- Uninstall or disable the extension to return to Pi's default scrolling behavior.
29
-
30
- ## Credits
31
-
32
- Fixed terminal-region behavior is adapted from [pi-powerline-footer](https://github.com/nicobailon/pi-powerline-footer) by Nico Bailon.
33
-
34
- ## License
35
-
36
- [MIT](LICENSE)
@@ -1,46 +0,0 @@
1
- {
2
- "name": "@tifan/pi-fixed-editor",
3
- "version": "0.2.5",
4
- "description": "Keep the pi editor and footer fixed while the transcript scrolls.",
5
- "keywords": [
6
- "fixed-editor",
7
- "pi",
8
- "pi-extension",
9
- "pi-package",
10
- "terminal"
11
- ],
12
- "homepage": "https://github.com/tifandotme/pi-extensions/tree/master/packages/pi-fixed-editor#readme",
13
- "bugs": "https://github.com/tifandotme/pi-extensions/issues",
14
- "license": "MIT",
15
- "author": "Tifan Dwi Avianto <hi@tifan.me>",
16
- "repository": {
17
- "type": "git",
18
- "url": "git+https://github.com/tifandotme/pi-extensions.git",
19
- "directory": "packages/pi-fixed-editor"
20
- },
21
- "files": [
22
- "src/**/*.ts",
23
- "README.md",
24
- "LICENSE"
25
- ],
26
- "type": "module",
27
- "publishConfig": {
28
- "access": "public",
29
- "provenance": true
30
- },
31
- "scripts": {
32
- "test": "node --experimental-strip-types --test tests/**/*.test.ts"
33
- },
34
- "peerDependencies": {
35
- "@earendil-works/pi-coding-agent": ">=0.78.0",
36
- "@earendil-works/pi-tui": ">=0.78.0"
37
- },
38
- "engines": {
39
- "node": ">=20"
40
- },
41
- "pi": {
42
- "extensions": [
43
- "./src/index.ts"
44
- ]
45
- }
46
- }