@duckmind/dm-darwin-x64 0.35.3 → 0.35.5

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (43) hide show
  1. package/dm +0 -0
  2. package/extensions/.dm-extensions.json +1 -27
  3. package/package.json +1 -1
  4. package/extensions/dm-alps/LICENSE +0 -21
  5. package/extensions/dm-alps/README.md +0 -22
  6. package/extensions/dm-alps/index.ts +0 -172
  7. package/extensions/dm-alps/package.json +0 -49
  8. package/extensions/dm-alps/src/commands.ts +0 -208
  9. package/extensions/dm-alps/src/features/animations/debug.ts +0 -160
  10. package/extensions/dm-alps/src/features/animations/index.ts +0 -33
  11. package/extensions/dm-alps/src/features/animations/patch.ts +0 -112
  12. package/extensions/dm-alps/src/features/animations/preview.ts +0 -117
  13. package/extensions/dm-alps/src/features/animations/registry.ts +0 -593
  14. package/extensions/dm-alps/src/features/animations/runtime.ts +0 -563
  15. package/extensions/dm-alps/src/features/animations/settings.ts +0 -69
  16. package/extensions/dm-alps/src/features/bottom-input/cluster.ts +0 -2
  17. package/extensions/dm-alps/src/features/bottom-input/compositor.ts +0 -2
  18. package/extensions/dm-alps/src/features/bottom-input/editor.ts +0 -148
  19. package/extensions/dm-alps/src/features/bottom-input/frame.ts +0 -224
  20. package/extensions/dm-alps/src/features/bottom-input/icons.ts +0 -36
  21. package/extensions/dm-alps/src/features/bottom-input/index.ts +0 -8
  22. package/extensions/dm-alps/src/features/bottom-input/runtime.ts +0 -1197
  23. package/extensions/dm-alps/src/features/bottom-input/shortcuts.ts +0 -286
  24. package/extensions/dm-alps/src/features/bottom-input/status.ts +0 -663
  25. package/extensions/dm-alps/src/features/bottom-status/index.ts +0 -2
  26. package/extensions/dm-alps/src/features/chrome-frame/chrome.ts +0 -222
  27. package/extensions/dm-alps/src/features/chrome-frame/debug.ts +0 -212
  28. package/extensions/dm-alps/src/features/chrome-frame/image.ts +0 -11
  29. package/extensions/dm-alps/src/features/chrome-frame/index.ts +0 -4
  30. package/extensions/dm-alps/src/features/chrome-frame/osc.ts +0 -111
  31. package/extensions/dm-alps/src/features/chrome-frame/patch.ts +0 -769
  32. package/extensions/dm-alps/src/features/chrome-frame/preview.ts +0 -67
  33. package/extensions/dm-alps/src/features/chrome-frame/styles.ts +0 -105
  34. package/extensions/dm-alps/src/features/fixed-bottom-editor/cluster.ts +0 -161
  35. package/extensions/dm-alps/src/features/fixed-bottom-editor/compositor.ts +0 -1149
  36. package/extensions/dm-alps/src/features/fixed-bottom-editor/index.ts +0 -3
  37. package/extensions/dm-alps/src/features/fixed-bottom-editor/runtime.ts +0 -3
  38. package/extensions/dm-alps/src/settings-store.ts +0 -194
  39. package/extensions/dm-alps/src/settings-ui.ts +0 -653
  40. package/extensions/dm-alps/src/settings.ts +0 -102
  41. package/extensions/dm-alps/src/terminal-sanitizer.ts +0 -91
  42. package/extensions/dm-alps/themes/LICENSE.synthwave-84 +0 -21
  43. package/extensions/dm-alps/themes/alps.json +0 -93
@@ -1,1197 +0,0 @@
1
-
2
- import * as PiAgent from "@mariozechner/pi-coding-agent";
3
- import type { ExtensionAPI } from "@mariozechner/pi-coding-agent";
4
- import type { FixedBottomEditorStatus } from "../../settings.ts";
5
- import { renderFixedEditorCluster } from "./cluster.ts";
6
- import {
7
- FixedBottomEditorCompositor,
8
- type FixedBottomEditorCompositorOptions,
9
- type FixedEditorRenderable,
10
- type FixedEditorTerminal,
11
- } from "./compositor.ts";
12
- import {
13
- DEFAULT_BOTTOM_INPUT_SHORTCUTS,
14
- isStashShortcutInput,
15
- matchesConfiguredShortcut,
16
- resolveBottomInputShortcuts,
17
- type BottomInputShortcutKey,
18
- type BottomInputShortcuts,
19
- } from "./shortcuts.ts";
20
- import {
21
- getUsageTokenTotal,
22
- isAssistantUsage,
23
- normalizePromptText,
24
- renderBottomInputStatus,
25
- type AssistantUsage,
26
- type BottomInputFrameStatus,
27
- } from "./status.ts";
28
- import { createBottomInputEditor } from "./editor.ts";
29
- import { getBottomInputIcons } from "./icons.ts";
30
-
31
- export type { FixedBottomEditorStatus } from "../../settings.ts";
32
-
33
- export type BottomInputRuntime = {
34
- bindSession(ctx: any): void;
35
- setEnabled(enabled: boolean): FixedBottomEditorStatus;
36
- configure?(settings: { fixedEnabled?: boolean; beautifiedInputEnabled?: boolean }): FixedBottomEditorStatus;
37
- dispose(): void;
38
- getStatus(): FixedBottomEditorStatus;
39
- setBeautifiedInputEnabled?(enabled: boolean): void;
40
- resetSessionStartTime(): void;
41
- setLastPrompt(prompt: unknown): void;
42
- setThinkingLevel(level: unknown): void;
43
- setStreaming?(streaming: boolean): void;
44
- setLiveUsage(usage: unknown): void;
45
- clearLiveUsage(): void;
46
- requestRender(options?: { full?: boolean }): void;
47
- stashOrRestoreEditorText(ctx?: any): void;
48
- copyEditorText?(ctx?: any): void;
49
- cutEditorText?(ctx?: any): void;
50
- setShortcuts?(shortcuts: Partial<BottomInputShortcuts> | undefined): void;
51
- };
52
-
53
- type RuntimeUI = {
54
- setEditorComponent?: (factory: ((tui: any, theme: any, keybindings: any) => any) | undefined) => void;
55
- getEditorComponent?: () => unknown;
56
- setFooter?: (factory: ((tui: any, theme: any, footerData: any) => any) | undefined) => void;
57
- setStatus?: (key: string, value: string | undefined) => void;
58
- getEditorText?: () => string;
59
- setEditorText?: (text: string) => void;
60
- notify?: (message: string, level: "info" | "warning" | "error") => void;
61
- theme?: any;
62
- tui?: any;
63
- onTerminalInput?: (handler: (data: string) => { consume?: boolean } | undefined) => (() => void) | void;
64
- };
65
-
66
- type RuntimeUIReadResult =
67
- | { stale: false; ui?: RuntimeUI }
68
- | { stale: true };
69
-
70
- type CompositorLike = Pick<
71
- FixedBottomEditorCompositor,
72
- "install" | "dispose" | "hideRenderable" | "renderHidden" | "requestRepaint" | "setKeyboardScrollShortcuts" | "jumpToPreviousRootTarget" | "jumpToNextRootTarget" | "jumpToRootBottom"
73
- >;
74
-
75
- type FixedEditorContainers = {
76
- statusContainer: FixedEditorRenderable | null;
77
- widgetContainerAbove: FixedEditorRenderable | null;
78
- editorContainer: FixedEditorRenderable;
79
- widgetContainerBelow: FixedEditorRenderable | null;
80
- };
81
-
82
- type BottomInputRuntimeOptions = {
83
- createCompositor?: (options: FixedBottomEditorCompositorOptions) => CompositorLike;
84
- startClock?: boolean;
85
- now?: () => number;
86
- shortcuts?: Partial<BottomInputShortcuts>;
87
- copyToClipboard?: (text: string) => Promise<void> | void;
88
- };
89
-
90
- const FALLBACK_EDITOR_THEME = {
91
- borderColor: (text: string) => text,
92
- selectList: {},
93
- };
94
- const STASH_STATUS_KEY = "dm-alps-stash";
95
- const STATUS_RENDER_INTERVAL_MS = 1_000;
96
- const STATUS_RENDER_DEBOUNCE_MS = 33;
97
- const LAYOUT_CACHE_TTL_MS = 250;
98
- const STREAMING_LAYOUT_CACHE_TTL_MS = 1000;
99
-
100
- export function createBottomInputRuntime(options: BottomInputRuntimeOptions = {}): BottomInputRuntime {
101
- return new BottomInputRuntimeImpl(options);
102
- }
103
-
104
- class BottomInputRuntimeImpl implements BottomInputRuntime {
105
- private readonly createCompositor: (options: FixedBottomEditorCompositorOptions) => CompositorLike;
106
- private readonly startClock: boolean;
107
- private readonly now: () => number;
108
- private readonly copyToClipboardImpl: (text: string) => Promise<void> | void;
109
- private ctx: any;
110
- private ui: RuntimeUI | undefined;
111
- private generation = 0;
112
- private enabled = false;
113
- private beautifiedInputEnabled = true;
114
- private installed = false;
115
- private failure: string | undefined;
116
- private layoutInstalled = false;
117
- private creatingFooter = false;
118
- private compositor: CompositorLike | null = null;
119
- private editorInstance: any;
120
- private editorFactory: ((tui: any, theme: any, keybindings: any) => any) | undefined;
121
- private footerFactory: ((tui: any, theme: any, footerData: any) => any) | undefined;
122
- private footerComponent: any;
123
- private footerData: any;
124
- private footerDataRestore: (() => void) | null = null;
125
- private theme: any;
126
- private editorTheme: any;
127
- private tui: any;
128
- private removeInputListener: (() => void) | null = null;
129
- private timer: ReturnType<typeof setInterval> | null = null;
130
- private renderTimer: ReturnType<typeof setTimeout> | null = null;
131
- private renderPending = false;
132
- private renderPendingFull = false;
133
- private layoutOwnerGeneration: number | null = null;
134
- private cachedLayout: { key: string; width: number; expiresAt: number; result: { topLines: string[]; secondaryLines: string[]; lastPromptLines: string[]; frameStatus: BottomInputFrameStatus } } | null = null;
135
- private stashedEditorText: string | null = null;
136
- private liveUsage: AssistantUsage | null = null;
137
- private latestAssistantUsage: AssistantUsage | null = null;
138
- private isStreaming = false;
139
- private currentThinkingLevel: string | null = null;
140
- private lastPrompt = "";
141
- private sessionStartTime: number;
142
- private shortcuts: BottomInputShortcuts;
143
-
144
- constructor(options: BottomInputRuntimeOptions) {
145
- this.createCompositor = options.createCompositor ?? ((compositorOptions) => new FixedBottomEditorCompositor(compositorOptions));
146
- this.startClock = options.startClock !== false;
147
- this.now = options.now ?? (() => Date.now());
148
- this.copyToClipboardImpl = options.copyToClipboard ?? ((PiAgent as { copyToClipboard?: (text: string) => Promise<void> | void }).copyToClipboard ?? (() => undefined));
149
- this.sessionStartTime = this.now();
150
- this.shortcuts = resolveBottomInputShortcuts(options.shortcuts);
151
- }
152
-
153
- bindSession(ctx: any): void {
154
- const next = readRuntimeUI(ctx);
155
- if (next.stale) return;
156
- const previousCtx = this.ctx;
157
- const previousUi = this.ui;
158
- const nextUi = next.ui;
159
- const sameUiSession = Boolean(previousUi && nextUi && previousUi === nextUi);
160
- if (previousCtx && previousCtx !== ctx && !sameUiSession && (this.installed || this.layoutInstalled)) {
161
- this.disable();
162
- }
163
- if (!previousCtx && ctx) {
164
- this.sessionStartTime = this.now();
165
- }
166
- if ((previousCtx !== ctx || previousUi !== nextUi) && !sameUiSession) {
167
- this.generation += 1;
168
- this.stopRenderTimer();
169
- }
170
- this.ctx = ctx;
171
- this.ui = nextUi;
172
- this.failure = undefined;
173
- }
174
-
175
- setEnabled(enabled: boolean): FixedBottomEditorStatus {
176
- return this.configure({ fixedEnabled: enabled });
177
- }
178
-
179
- configure(settings: { fixedEnabled?: boolean; beautifiedInputEnabled?: boolean }): FixedBottomEditorStatus {
180
- if (typeof settings.beautifiedInputEnabled === "boolean") {
181
- this.beautifiedInputEnabled = settings.beautifiedInputEnabled;
182
- }
183
- const fixedEnabled = settings.fixedEnabled ?? this.enabled;
184
- return this.syncLayout(fixedEnabled, this.beautifiedInputEnabled);
185
- }
186
-
187
- dispose(): void {
188
- this.generation += 1;
189
- this.disable();
190
- this.ctx = undefined;
191
- this.ui = undefined;
192
- this.editorInstance = undefined;
193
- this.editorFactory = undefined;
194
- this.footerFactory = undefined;
195
- this.footerComponent = undefined;
196
- this.restoreFooterDataHook();
197
- this.footerData = undefined;
198
- this.tui = undefined;
199
- this.theme = undefined;
200
- this.editorTheme = undefined;
201
- this.stashedEditorText = null;
202
- this.liveUsage = null;
203
- this.latestAssistantUsage = null;
204
- this.currentThinkingLevel = null;
205
- this.lastPrompt = "";
206
- this.sessionStartTime = this.now();
207
- }
208
-
209
- getStatus(): FixedBottomEditorStatus {
210
- return this.toStatus();
211
- }
212
-
213
- setBeautifiedInputEnabled(enabled: boolean): void {
214
- this.configure({ beautifiedInputEnabled: enabled });
215
- }
216
-
217
- resetSessionStartTime(): void {
218
- this.sessionStartTime = this.now();
219
- this.resetLayoutCache();
220
- this.requestRender();
221
- }
222
-
223
- setLastPrompt(prompt: unknown): void {
224
- this.lastPrompt = normalizePromptText(prompt);
225
- this.resetLayoutCache();
226
- this.requestRender();
227
- }
228
-
229
- setThinkingLevel(level: unknown): void {
230
- this.currentThinkingLevel = typeof level === "string" && level ? level : null;
231
- this.resetLayoutCache();
232
- this.requestRender();
233
- }
234
-
235
- setStreaming(streaming: boolean): void {
236
- this.isStreaming = streaming;
237
- if (streaming) this.liveUsage = null;
238
- this.resetLayoutCache();
239
- this.requestRender();
240
- }
241
-
242
- setLiveUsage(usage: unknown): void {
243
- if (isAssistantUsage(usage) && getUsageTokenTotal(usage) > 0) {
244
- this.liveUsage = usage;
245
- this.latestAssistantUsage = usage;
246
- }
247
- this.resetLayoutCache();
248
- this.requestRender();
249
- }
250
-
251
- clearLiveUsage(): void {
252
- this.isStreaming = false;
253
- this.liveUsage = null;
254
- this.resetLayoutCache();
255
- this.requestRender();
256
- }
257
-
258
- requestRender(options: { full?: boolean } = {}): void {
259
- if (!this.layoutInstalled) return;
260
- if (options.full) this.renderPendingFull = true;
261
- if (this.renderPending) return;
262
- const generation = this.generation;
263
- this.renderPending = true;
264
- this.renderTimer = setTimeout(() => {
265
- if (generation !== this.generation || !this.layoutInstalled) {
266
- this.renderPending = false;
267
- this.renderPendingFull = false;
268
- this.renderTimer = null;
269
- return;
270
- }
271
- const shouldRenderFull = this.renderPendingFull;
272
- this.renderPending = false;
273
- this.renderPendingFull = false;
274
- this.renderTimer = null;
275
- if (this.enabled && this.compositor) {
276
- if (shouldRenderFull && typeof this.tui?.requestRender === "function") {
277
- this.tui.requestRender();
278
- return;
279
- }
280
- this.compositor.requestRepaint();
281
- return;
282
- }
283
- if (typeof this.tui?.requestRender === "function") this.tui.requestRender();
284
- }, STATUS_RENDER_DEBOUNCE_MS);
285
- this.renderTimer.unref?.();
286
- }
287
-
288
- stashOrRestoreEditorText(ctx: any = this.ctx): void {
289
- if (!ctx?.hasUI || !ctx.ui) return;
290
- const rawText = getCurrentEditorText(ctx, this.editorInstance);
291
- const hasStash = this.stashedEditorText !== null;
292
-
293
- if (!hasNonWhitespaceText(rawText)) {
294
- if (!hasStash) {
295
- notify(ctx, "Nothing to stash", "info");
296
- return;
297
- }
298
- setEditorText(ctx, this.editorInstance, this.stashedEditorText ?? "");
299
- this.stashedEditorText = null;
300
- ctx.ui.setStatus?.(STASH_STATUS_KEY, undefined);
301
- notify(ctx, "Stash restored", "info");
302
- this.requestRender();
303
- return;
304
- }
305
-
306
- this.stashedEditorText = rawText;
307
- setEditorText(ctx, this.editorInstance, "");
308
- ctx.ui.setStatus?.(STASH_STATUS_KEY, "stash");
309
- notify(ctx, hasStash ? "Stash updated" : "Text stashed", "info");
310
- this.requestRender();
311
- }
312
-
313
- copyEditorText(ctx: any = this.ctx): void {
314
- const text = getCurrentEditorText(ctx, this.editorInstance);
315
- if (!hasNonWhitespaceText(text)) {
316
- notify(ctx, "Nothing to copy", "info");
317
- return;
318
- }
319
- const generation = this.generation;
320
- void this.copyTextToClipboard(text)
321
- .then(() => {
322
- if (generation !== this.generation) return;
323
- notify(ctx, "Copied editor text", "info");
324
- })
325
- .catch(() => {
326
- if (generation !== this.generation) return;
327
- notify(ctx, "Copy failed", "warning");
328
- });
329
- }
330
-
331
- cutEditorText(ctx: any = this.ctx): void {
332
- const text = getCurrentEditorText(ctx, this.editorInstance);
333
- if (!hasNonWhitespaceText(text)) {
334
- notify(ctx, "Nothing to cut", "info");
335
- return;
336
- }
337
- const generation = this.generation;
338
- const editor = this.editorInstance;
339
- void this.copyTextToClipboard(text)
340
- .then(() => {
341
- if (generation !== this.generation) return;
342
- setEditorText(ctx, editor, "");
343
- notify(ctx, "Cut editor text", "info");
344
- this.requestRender();
345
- })
346
- .catch(() => {
347
- if (generation !== this.generation) return;
348
- notify(ctx, "Cut failed", "warning");
349
- });
350
- }
351
-
352
- setShortcuts(shortcuts: Partial<BottomInputShortcuts> | undefined): void {
353
- this.shortcuts = resolveBottomInputShortcuts(shortcuts);
354
- this.compositor?.setKeyboardScrollShortcuts({
355
- up: this.shortcuts.scrollChatUp,
356
- down: this.shortcuts.scrollChatDown,
357
- });
358
- }
359
-
360
- private syncLayout(fixedEnabled: boolean, beautifiedInputEnabled: boolean): FixedBottomEditorStatus {
361
- this.enabled = fixedEnabled;
362
- this.beautifiedInputEnabled = beautifiedInputEnabled;
363
- this.resetLayoutCache();
364
-
365
- const needsLayout = fixedEnabled || beautifiedInputEnabled;
366
- if (!needsLayout) return this.disable();
367
-
368
- const ui = this.getBoundUI();
369
- if (!ui) {
370
- if (fixedEnabled) return this.failClosed("bottom input requires a bound UI session");
371
- this.enabled = false;
372
- return this.toStatus();
373
- }
374
-
375
- try {
376
- this.validateUI(ui);
377
- this.failure = undefined;
378
- if (!this.layoutInstalled) {
379
- const layoutGeneration = this.generation;
380
- this.editorInstance = undefined;
381
- this.footerComponent = undefined;
382
- this.restoreFooterDataHook();
383
- this.footerData = undefined;
384
- this.layoutOwnerGeneration = layoutGeneration;
385
- const editorFactory = this.createEditorFactory(layoutGeneration);
386
- this.editorFactory = editorFactory;
387
- ui.setEditorComponent!(editorFactory);
388
- this.layoutInstalled = true;
389
- this.installInputListener();
390
- this.startClockTimer();
391
-
392
- this.creatingFooter = true;
393
- try {
394
- const footerFactory = this.createFooterFactory(layoutGeneration);
395
- this.footerFactory = footerFactory;
396
- ui.setFooter!(footerFactory);
397
- } finally {
398
- this.creatingFooter = false;
399
- }
400
- } else if (fixedEnabled && this.tui && !this.installed) {
401
- this.installCompositor(this.tui);
402
- } else if (!fixedEnabled && this.installed) {
403
- this.teardownCompositor();
404
- this.installed = false;
405
- revealFixedEditorContainers(this.tui);
406
- if (!beautifiedInputEnabled) {
407
- this.stopClockTimer();
408
- this.stopRenderTimer();
409
- this.removeInputListener?.();
410
- this.removeInputListener = null;
411
- this.restoreDefaultLayout();
412
- this.layoutInstalled = false;
413
- this.layoutOwnerGeneration = null;
414
- this.editorFactory = undefined;
415
- this.footerFactory = undefined;
416
- this.editorInstance = undefined;
417
- this.footerComponent = undefined;
418
- this.restoreFooterDataHook();
419
- this.footerData = undefined;
420
- this.tui = undefined;
421
- this.theme = undefined;
422
- this.editorTheme = undefined;
423
- } else {
424
- this.tui?.requestRender?.(true);
425
- }
426
- }
427
- this.requestRender();
428
- return this.toStatus();
429
- } catch (error) {
430
- this.creatingFooter = false;
431
- return fixedEnabled ? this.failClosed(formatFailure(error)) : this.disableWithFailure(formatFailure(error));
432
- }
433
- }
434
-
435
- private disable(): FixedBottomEditorStatus {
436
- if (!this.enabled && !this.installed && !this.layoutInstalled && !this.failure) {
437
- return this.toStatus();
438
- }
439
-
440
- this.enabled = false;
441
- this.failure = undefined;
442
- this.stopClockTimer();
443
- this.stopRenderTimer();
444
- this.removeInputListener?.();
445
- this.removeInputListener = null;
446
- this.teardownCompositor();
447
- this.restoreDefaultLayout();
448
- this.installed = false;
449
- this.layoutInstalled = false;
450
- this.layoutOwnerGeneration = null;
451
- this.editorFactory = undefined;
452
- this.footerFactory = undefined;
453
- this.editorInstance = undefined;
454
- this.footerComponent = undefined;
455
- this.restoreFooterDataHook();
456
- this.footerData = undefined;
457
- this.tui = undefined;
458
- this.theme = undefined;
459
- this.editorTheme = undefined;
460
- this.resetLayoutCache();
461
- return this.toStatus();
462
- }
463
-
464
- private getBoundUI(): RuntimeUI | undefined {
465
- return this.ui;
466
- }
467
-
468
- private validateUI(ui: RuntimeUI): void {
469
- if (typeof ui.setEditorComponent !== "function") {
470
- throw new Error("bottom input expected ctx.ui.setEditorComponent(factory) to exist");
471
- }
472
- if (typeof ui.getEditorComponent !== "function") {
473
- throw new Error("bottom input expected ctx.ui.getEditorComponent() to exist");
474
- }
475
- if (typeof ui.setFooter !== "function") {
476
- throw new Error("bottom input expected ctx.ui.setFooter(factory) to exist");
477
- }
478
- }
479
-
480
- private createEditorFactory(ownerGeneration: number): (tui: any, theme: any, keybindings: any) => any {
481
- return (tui: any, theme: any, keybindings: any) => {
482
- if (ownerGeneration !== this.generation || ownerGeneration !== this.layoutOwnerGeneration) return createStaleEditorFallback();
483
- this.tui = tui;
484
- this.editorTheme = theme ?? FALLBACK_EDITOR_THEME;
485
- const editorStateOwner = this;
486
- const editor = createBottomInputEditor(tui, this.editorTheme, keybindings, {
487
- get beautifiedInputEnabled() {
488
- return editorStateOwner.beautifiedInputEnabled;
489
- },
490
- getTheme: () => editorStateOwner.getRenderTheme(),
491
- getFrameStatus: (width) => editorStateOwner.getStatusLayout(width).frameStatus,
492
- });
493
- this.editorInstance = editor;
494
- this.patchEditorInput(editor);
495
- return editor;
496
- };
497
- }
498
-
499
- private createFooterFactory(ownerGeneration: number): (tui: any, theme: any, footerData: any) => any {
500
- return (tui: any, theme: any, footerData: any) => {
501
- if (ownerGeneration !== this.generation || ownerGeneration !== this.layoutOwnerGeneration) return createStaleFooterFallback();
502
- const generation = this.generation;
503
- this.tui = tui;
504
- this.theme = theme ?? FALLBACK_EDITOR_THEME;
505
- this.restoreFooterDataHook();
506
- this.footerData = footerData;
507
- this.installFooterStatusRepaintHook(footerData);
508
- if (this.enabled) {
509
- try {
510
- this.installCompositor(tui);
511
- } catch (error) {
512
- if (this.creatingFooter) throw error;
513
- this.failClosed(formatFailure(error));
514
- }
515
- }
516
-
517
- let footerActive = true;
518
- const unsubscribeBranch = typeof footerData?.onBranchChange === "function"
519
- ? footerData.onBranchChange(() => {
520
- if (generation !== this.generation) return;
521
- this.resetLayoutCache();
522
- this.requestRender();
523
- })
524
- : undefined;
525
- const footer = {
526
- __alpsBottomInputOwner: true,
527
- get __alpsBottomInputActive() {
528
- return footerActive;
529
- },
530
- dispose: () => {
531
- footerActive = false;
532
- unsubscribeBranch?.();
533
- if (generation !== this.generation) return;
534
- if (this.footerComponent === footer) {
535
- this.restoreFooterDataHook();
536
- this.teardownCompositor();
537
- this.installed = false;
538
- }
539
- },
540
- invalidate: () => {
541
- if (generation !== this.generation) return;
542
- this.requestRender();
543
- },
544
- render: (width: number) => {
545
- if (generation !== this.generation) return [];
546
- if (this.enabled) return [];
547
- const rendered = this.getStatusLayout(width);
548
- return [...rendered.secondaryLines, ...rendered.lastPromptLines];
549
- },
550
- };
551
- this.footerComponent = footer;
552
- return footer;
553
- };
554
- }
555
-
556
- private installCompositor(tui: any): void {
557
- if (this.installed) return;
558
- const terminal = this.getTerminal(tui);
559
- const containers = findFixedEditorContainers(tui, this.editorInstance);
560
- if (!containers) {
561
- throw new Error("bottom input could not find the editor container in TUI children");
562
- }
563
-
564
- let compositor: CompositorLike | null = null;
565
- const generation = this.generation;
566
- try {
567
- compositor = this.createCompositor({
568
- tui,
569
- terminal,
570
- getShowHardwareCursor: () => typeof tui?.getShowHardwareCursor === "function" ? Boolean(tui.getShowHardwareCursor()) : true,
571
- onCopySelection: (text) => {
572
- void this.copyTextToClipboard(text)
573
- .then(() => {
574
- if (generation !== this.generation) return;
575
- notify(this.ctx, "Copied selection", "info");
576
- })
577
- .catch(() => {
578
- if (generation !== this.generation) return;
579
- notify(this.ctx, "Copy selection failed", "warning");
580
- });
581
- },
582
- keyboardScrollShortcuts: {
583
- up: this.shortcuts.scrollChatUp,
584
- down: this.shortcuts.scrollChatDown,
585
- },
586
- renderCluster: (width, terminalRows) => this.renderCluster(compositor, containers, width, terminalRows),
587
- });
588
- hideRenderableIfPresent(compositor, containers.statusContainer);
589
- hideRenderableIfPresent(compositor, containers.widgetContainerAbove);
590
- compositor.hideRenderable(containers.editorContainer);
591
- hideRenderableIfPresent(compositor, containers.widgetContainerBelow);
592
- compositor.install();
593
- this.compositor = compositor;
594
- this.installed = true;
595
- this.failure = undefined;
596
- if (typeof tui?.requestRender === "function") tui.requestRender(true);
597
- } catch (error) {
598
- compositor?.dispose();
599
- throw error;
600
- }
601
- }
602
-
603
- private renderCluster(compositor: CompositorLike | null, containers: FixedEditorContainers, width: number, terminalRows: number) {
604
- const maxHeight = Math.max(1, Math.floor(terminalRows) - 1);
605
- const hiddenStatusLines = compositor ? renderHiddenLines(compositor, [containers.statusContainer], width) : [];
606
- const hiddenAboveWidgetLines = compositor ? renderHiddenLines(compositor, [containers.widgetContainerAbove], width) : [];
607
- const editorLines = compositor ? compositor.renderHidden(containers.editorContainer, width) : [];
608
- const hiddenBelowWidgetLines = compositor ? renderHiddenLines(compositor, [containers.widgetContainerBelow], width) : [];
609
- const rendered = this.getStatusLayout(width);
610
- return renderFixedEditorCluster({
611
- statusLines: hiddenStatusLines,
612
- topLines: hiddenAboveWidgetLines,
613
- editorLines: editorLines,
614
- secondaryLines: [...hiddenBelowWidgetLines, ...rendered.secondaryLines],
615
- lastPromptLines: rendered.lastPromptLines,
616
- width,
617
- maxHeight,
618
- });
619
- }
620
-
621
- private getRenderTheme(): any {
622
- return this.theme ?? this.ui?.theme ?? FALLBACK_EDITOR_THEME;
623
- }
624
-
625
- private getStatusLayout(width: number): { topLines: string[]; secondaryLines: string[]; lastPromptLines: string[]; frameStatus: BottomInputFrameStatus } {
626
- const now = this.now();
627
- const safeWidth = Number.isFinite(width) ? Math.max(1, Math.floor(width)) : 1;
628
- if (this.cachedLayout && this.cachedLayout.width === safeWidth && this.cachedLayout.expiresAt > now) {
629
- return cloneStatusLayout(this.cachedLayout.result);
630
- }
631
-
632
- const theme = this.getRenderTheme();
633
- const result = this.beautifiedInputEnabled ? renderBottomInputStatus({
634
- ctx: this.ctx,
635
- footerData: this.footerData,
636
- theme,
637
- width: safeWidth,
638
- beautifiedInputEnabled: this.beautifiedInputEnabled,
639
- isStreaming: this.isStreaming,
640
- liveUsage: this.liveUsage,
641
- latestAssistantUsage: this.latestAssistantUsage,
642
- currentThinkingLevel: this.currentThinkingLevel,
643
- sessionStartTime: this.sessionStartTime,
644
- now,
645
- lastPrompt: this.lastPrompt,
646
- icons: getBottomInputIcons(),
647
- }) : renderBottomInputStatus({
648
- ctx: undefined,
649
- footerData: this.footerData,
650
- theme,
651
- width: safeWidth,
652
- beautifiedInputEnabled: false,
653
- isStreaming: false,
654
- liveUsage: null,
655
- latestAssistantUsage: null,
656
- currentThinkingLevel: null,
657
- sessionStartTime: this.sessionStartTime,
658
- now,
659
- lastPrompt: this.lastPrompt,
660
- icons: { model: "", time: "◷" },
661
- });
662
- const ttl = this.isStreaming ? STREAMING_LAYOUT_CACHE_TTL_MS : LAYOUT_CACHE_TTL_MS;
663
- const layout = {
664
- topLines: [...result.topLines],
665
- secondaryLines: [...result.secondaryLines],
666
- lastPromptLines: [...result.lastPromptLines],
667
- frameStatus: { ...result.frameStatus },
668
- };
669
- this.cachedLayout = { key: result.cacheKey, width: safeWidth, expiresAt: now + ttl, result: layout };
670
- return cloneStatusLayout(layout);
671
- }
672
-
673
- private getTerminal(tui: any): FixedEditorTerminal {
674
- const terminal = tui?.terminal as FixedEditorTerminal | undefined;
675
- if (!terminal || typeof terminal.write !== "function") {
676
- throw new Error("bottom input could not find tui.terminal.write()");
677
- }
678
- return terminal;
679
- }
680
-
681
- private patchEditorInput(editor: any): void {
682
- if (!editor || typeof editor.handleInput !== "function" || editor.__alpsBottomInputPatched) return;
683
- const requestEditorRender = () => this.requestRender();
684
- const originalHandleInput = editor.handleInput.bind(editor);
685
- editor.handleInput = (data: string) => {
686
- if (this.handleShortcutInput(data)) return;
687
- originalHandleInput(data);
688
- requestEditorRender();
689
- };
690
- if (typeof editor.setText === "function") {
691
- const originalSetText = editor.setText.bind(editor);
692
- editor.setText = (text: string) => {
693
- const result = originalSetText(text);
694
- requestEditorRender();
695
- return result;
696
- };
697
- }
698
- if (typeof editor.insertTextAtCursor === "function") {
699
- const originalInsertTextAtCursor = editor.insertTextAtCursor.bind(editor);
700
- editor.insertTextAtCursor = (text: string) => {
701
- const result = originalInsertTextAtCursor(text);
702
- requestEditorRender();
703
- return result;
704
- };
705
- }
706
- editor.__alpsBottomInputPatched = true;
707
- }
708
-
709
- private installInputListener(): void {
710
- if (this.removeInputListener || typeof this.ui?.onTerminalInput !== "function") return;
711
- const generation = this.generation;
712
- this.removeInputListener = this.ui.onTerminalInput((data: string) => {
713
- if (generation !== this.generation) return undefined;
714
- return this.handleShortcutInput(data) ? { consume: true } : undefined;
715
- }) ?? null;
716
- }
717
-
718
- private handleShortcutInput(data: string): boolean {
719
- if ((!this.enabled && !this.beautifiedInputEnabled) || hasOverlay(this.ctx, this.tui)) return false;
720
- if (isStashShortcutInput(data, this.shortcuts.stashEditor)) {
721
- this.stashOrRestoreEditorText(this.ctx);
722
- return true;
723
- }
724
- if (matchesConfiguredShortcut(data, this.shortcuts.copyEditor)) {
725
- this.copyEditorText(this.ctx);
726
- return true;
727
- }
728
- if (matchesConfiguredShortcut(data, this.shortcuts.cutEditor)) {
729
- this.cutEditorText(this.ctx);
730
- return true;
731
- }
732
- if (matchesConfiguredShortcut(data, this.shortcuts.editorStart)) {
733
- return moveEditorToBoundary(this.editorInstance, "start");
734
- }
735
- if (matchesConfiguredShortcut(data, this.shortcuts.editorEnd)) {
736
- return moveEditorToBoundary(this.editorInstance, "end");
737
- }
738
- const jump = getJumpAction(data, this.shortcuts);
739
- if (jump) {
740
- this.runJumpAction(jump);
741
- return true;
742
- }
743
- return false;
744
- }
745
-
746
- private copyTextToClipboard(text: string): Promise<void> {
747
- return copyTextToClipboard(text, this.copyToClipboardImpl);
748
- }
749
-
750
- private runJumpAction(action: JumpAction): void {
751
- if (!this.compositor || !this.tui) return;
752
- if (action.kind === "bottom") {
753
- this.compositor.jumpToRootBottom?.();
754
- return;
755
- }
756
- const targets = collectChatMessageStartLines(this.tui, action.role);
757
- if (targets.length === 0) return;
758
- if (action.direction === "previous") {
759
- this.compositor.jumpToPreviousRootTarget?.(targets);
760
- } else {
761
- this.compositor.jumpToNextRootTarget?.(targets);
762
- }
763
- }
764
-
765
- private installFooterStatusRepaintHook(footerData: any): void {
766
- const generation = this.generation;
767
- const writableFooterData = footerData as {
768
- setExtensionStatus?: (key: string, text: string | undefined) => void;
769
- clearExtensionStatuses?: () => void;
770
- };
771
- if (typeof writableFooterData?.setExtensionStatus !== "function") return;
772
- const originalSetExtensionStatus = writableFooterData.setExtensionStatus;
773
- const originalClearExtensionStatuses = writableFooterData.clearExtensionStatuses;
774
- const request = () => {
775
- if (generation !== this.generation) return;
776
- this.resetLayoutCache();
777
- this.requestRender();
778
- };
779
- writableFooterData.setExtensionStatus = function setExtensionStatusAndRepaint(this: unknown, key: string, text: string | undefined) {
780
- originalSetExtensionStatus.call(this, key, text);
781
- request();
782
- };
783
- if (typeof originalClearExtensionStatuses === "function") {
784
- writableFooterData.clearExtensionStatuses = function clearExtensionStatusesAndRepaint(this: unknown) {
785
- originalClearExtensionStatuses.call(this);
786
- request();
787
- };
788
- }
789
- this.footerDataRestore = () => {
790
- writableFooterData.setExtensionStatus = originalSetExtensionStatus;
791
- if (originalClearExtensionStatuses) writableFooterData.clearExtensionStatuses = originalClearExtensionStatuses;
792
- };
793
- }
794
-
795
- private restoreFooterDataHook(): void {
796
- const restore = this.footerDataRestore;
797
- this.footerDataRestore = null;
798
- try {
799
- restore?.();
800
- } catch {
801
- }
802
- }
803
-
804
- private teardownCompositor(): void {
805
- const compositor = this.compositor;
806
- this.compositor = null;
807
- if (compositor) compositor.dispose();
808
- }
809
-
810
- private restoreDefaultLayout(): void {
811
- if (!this.layoutInstalled) return;
812
- const ui = this.ui;
813
- if (!ui) return;
814
- this.clearEditorIfOwned(ui);
815
- this.clearFooterIfOwned(ui);
816
- try {
817
- ui.setStatus?.(STASH_STATUS_KEY, undefined);
818
- } catch (error) {
819
- if (!isStaleCtxError(error)) this.failure = formatFailure(error);
820
- }
821
- }
822
-
823
- private clearEditorIfOwned(ui: RuntimeUI): void {
824
- try {
825
- if (typeof ui.getEditorComponent !== "function") return;
826
- if (ui.getEditorComponent() !== this.editorFactory) return;
827
- ui.setEditorComponent?.(undefined);
828
- } catch (error) {
829
- if (!isStaleCtxError(error)) this.failure = formatFailure(error);
830
- }
831
- }
832
-
833
- private clearFooterIfOwned(ui: RuntimeUI): void {
834
- try {
835
- if (isActiveAlpsFooterComponent(this.footerComponent)) {
836
- ui.setFooter?.(undefined);
837
- return;
838
- }
839
- if (this.footerComponent === undefined && this.footerFactory) {
840
- ui.setFooter?.(undefined);
841
- }
842
- } catch (error) {
843
- if (!isStaleCtxError(error)) this.failure = formatFailure(error);
844
- }
845
- }
846
-
847
- private failClosed(reason: string): FixedBottomEditorStatus {
848
- this.enabled = false;
849
- this.installed = false;
850
- this.failure = reason;
851
- this.stopClockTimer();
852
- this.stopRenderTimer();
853
- this.removeInputListener?.();
854
- this.removeInputListener = null;
855
- this.teardownCompositor();
856
- this.restoreDefaultLayout();
857
- this.layoutInstalled = false;
858
- this.layoutOwnerGeneration = null;
859
- this.editorFactory = undefined;
860
- this.footerFactory = undefined;
861
- this.editorInstance = undefined;
862
- this.footerComponent = undefined;
863
- this.restoreFooterDataHook();
864
- this.footerData = undefined;
865
- return this.toStatus();
866
- }
867
-
868
- private disableWithFailure(reason: string): FixedBottomEditorStatus {
869
- this.disable();
870
- this.failure = reason;
871
- return this.toStatus();
872
- }
873
-
874
- private startClockTimer(): void {
875
- if (!this.startClock || this.timer) return;
876
- const generation = this.generation;
877
- this.timer = setInterval(() => {
878
- if (generation !== this.generation) return;
879
- this.requestRender();
880
- }, STATUS_RENDER_INTERVAL_MS);
881
- this.timer.unref?.();
882
- }
883
-
884
- private stopClockTimer(): void {
885
- if (!this.timer) return;
886
- clearInterval(this.timer);
887
- this.timer = null;
888
- }
889
-
890
- private stopRenderTimer(): void {
891
- if (!this.renderTimer) return;
892
- clearTimeout(this.renderTimer);
893
- this.renderTimer = null;
894
- this.renderPending = false;
895
- this.renderPendingFull = false;
896
- }
897
-
898
- private resetLayoutCache(): void {
899
- this.cachedLayout = null;
900
- }
901
-
902
- private toStatus(): FixedBottomEditorStatus {
903
- return this.failure
904
- ? { enabled: this.enabled, installed: this.installed, failure: this.failure }
905
- : { enabled: this.enabled, installed: this.installed };
906
- }
907
- }
908
-
909
- type JumpAction =
910
- | { kind: "message"; role: "user" | "assistant"; direction: "previous" | "next" }
911
- | { kind: "bottom" };
912
-
913
- type StatusLayout = { topLines: string[]; secondaryLines: string[]; lastPromptLines: string[]; frameStatus: BottomInputFrameStatus };
914
-
915
- function isActiveAlpsFooterComponent(component: any): boolean {
916
- return Boolean(component?.__alpsBottomInputOwner && component.__alpsBottomInputActive === true);
917
- }
918
-
919
- function createEmptyStatusLayout(): StatusLayout {
920
- return { topLines: [], secondaryLines: [], lastPromptLines: [], frameStatus: { model: null, thinking: null, context: null, elapsed: null } };
921
- }
922
-
923
- function createStaleEditorFallback(): { render: () => string[]; handleInput: () => void } {
924
- return {
925
- render: () => [],
926
- handleInput: () => undefined,
927
- };
928
- }
929
-
930
- function createStaleFooterFallback(): { render: () => string[]; dispose: () => void; invalidate: () => void } {
931
- return {
932
- render: () => [],
933
- dispose: () => undefined,
934
- invalidate: () => undefined,
935
- };
936
- }
937
-
938
- function cloneStatusLayout(layout: StatusLayout): StatusLayout {
939
- return {
940
- topLines: [...layout.topLines],
941
- secondaryLines: [...layout.secondaryLines],
942
- lastPromptLines: [...layout.lastPromptLines],
943
- frameStatus: { ...layout.frameStatus },
944
- };
945
- }
946
-
947
- function findFixedEditorContainers(tui: any, editor: any): FixedEditorContainers | null {
948
- if (!editor) return null;
949
- const explicit = findExplicitFixedEditorContainers(tui);
950
- if (explicit) return explicit;
951
-
952
- const children = Array.isArray(tui?.children) ? tui.children : [];
953
- for (const [index, child] of children.entries()) {
954
- if (child === editor && isRenderable(child)) {
955
- return inferAdjacentContainers(children, index, child);
956
- }
957
- const nestedChildren = Array.isArray(child?.children) ? child.children : [];
958
- if (nestedChildren.includes(editor) && isRenderable(child)) {
959
- return inferAdjacentContainers(children, index, child);
960
- }
961
- }
962
- return null;
963
- }
964
-
965
- function findExplicitFixedEditorContainers(tui: any): FixedEditorContainers | null {
966
- const editorContainer = asRenderable(tui?.editorContainer);
967
- if (!editorContainer) return null;
968
- return {
969
- statusContainer: asRenderable(tui?.statusContainer),
970
- widgetContainerAbove: asRenderable(tui?.widgetContainerAbove),
971
- editorContainer,
972
- widgetContainerBelow: asRenderable(tui?.widgetContainerBelow),
973
- };
974
- }
975
-
976
- function inferAdjacentContainers(children: any[], editorIndex: number, editorContainer: FixedEditorRenderable): FixedEditorContainers {
977
- return {
978
- statusContainer: asRenderable(children[editorIndex - 2]),
979
- widgetContainerAbove: asRenderable(children[editorIndex - 1]),
980
- editorContainer,
981
- widgetContainerBelow: asRenderable(children[editorIndex + 1]),
982
- };
983
- }
984
-
985
- function renderHiddenLines(compositor: CompositorLike, containers: Array<FixedEditorRenderable | null>, width: number): string[] {
986
- return containers.flatMap((container) => container ? compositor.renderHidden(container, width) : []);
987
- }
988
-
989
- function hideRenderableIfPresent(compositor: CompositorLike, container: FixedEditorRenderable | null): void {
990
- if (container) compositor.hideRenderable(container);
991
- }
992
-
993
- function revealFixedEditorContainers(tui: any): void {
994
- for (const container of [tui?.statusContainer, tui?.widgetContainerAbove, tui?.editorContainer, tui?.widgetContainerBelow]) {
995
- try {
996
- if (container && typeof container.render === "function") delete container.render;
997
- } catch {
998
- }
999
- }
1000
- }
1001
-
1002
- function asRenderable(value: unknown): FixedEditorRenderable | null {
1003
- return isRenderable(value) ? value : null;
1004
- }
1005
-
1006
- function isRenderable(value: unknown): value is FixedEditorRenderable {
1007
- return typeof value === "object" && value !== null && typeof (value as FixedEditorRenderable).render === "function";
1008
- }
1009
-
1010
- function getCurrentEditorText(ctx: any, editor: any): string {
1011
- try {
1012
- if (typeof editor?.getText === "function") {
1013
- const text = editor.getText();
1014
- return typeof text === "string" ? text : "";
1015
- }
1016
- const ui = readRuntimeUI(ctx);
1017
- if (ui.stale) return "";
1018
- const text = ui.ui?.getEditorText?.();
1019
- return typeof text === "string" ? text : "";
1020
- } catch {
1021
- return "";
1022
- }
1023
- }
1024
-
1025
- function setEditorText(ctx: any, editor: any, text: string): void {
1026
- try {
1027
- if (typeof editor?.setText === "function") {
1028
- editor.setText(text);
1029
- return;
1030
- }
1031
- const ui = readRuntimeUI(ctx);
1032
- if (ui.stale) return;
1033
- ui.ui?.setEditorText?.(text);
1034
- } catch {
1035
- }
1036
- }
1037
-
1038
- function moveEditorToBoundary(editor: any, boundary: "start" | "end"): boolean {
1039
- try {
1040
- if (boundary === "start" && typeof editor?.moveToStart === "function") {
1041
- editor.moveToStart();
1042
- return true;
1043
- }
1044
- if (boundary === "end" && typeof editor?.moveToEnd === "function") {
1045
- editor.moveToEnd();
1046
- return true;
1047
- }
1048
- const state = editor?.state;
1049
- if (state && Array.isArray(state.lines)) {
1050
- if (boundary === "start") {
1051
- state.cursorLine = 0;
1052
- if (typeof editor.setCursorCol === "function") editor.setCursorCol(0);
1053
- else state.cursorCol = 0;
1054
- } else {
1055
- state.cursorLine = Math.max(0, state.lines.length - 1);
1056
- const col = String(state.lines[state.cursorLine] ?? "").length;
1057
- if (typeof editor.setCursorCol === "function") editor.setCursorCol(col);
1058
- else state.cursorCol = col;
1059
- }
1060
- return true;
1061
- }
1062
- if (boundary === "start" && typeof editor?.handleInput === "function") {
1063
- editor.handleInput("\x1b[H");
1064
- return true;
1065
- }
1066
- if (boundary === "end" && typeof editor?.handleInput === "function") {
1067
- editor.handleInput("\x1b[F");
1068
- return true;
1069
- }
1070
- } catch {
1071
- return false;
1072
- }
1073
- return false;
1074
- }
1075
-
1076
- function getJumpAction(data: string, shortcuts: BottomInputShortcuts): JumpAction | null {
1077
- const table: Array<{ key: BottomInputShortcutKey; action: JumpAction }> = [
1078
- { key: "jumpPreviousUserMessage", action: { kind: "message", role: "user", direction: "previous" } },
1079
- { key: "jumpNextUserMessage", action: { kind: "message", role: "user", direction: "next" } },
1080
- { key: "jumpPreviousAssistantMessage", action: { kind: "message", role: "assistant", direction: "previous" } },
1081
- { key: "jumpNextAssistantMessage", action: { kind: "message", role: "assistant", direction: "next" } },
1082
- { key: "jumpChatBottom", action: { kind: "bottom" } },
1083
- ];
1084
- return table.find((entry) => matchesConfiguredShortcut(data, shortcuts[entry.key]))?.action ?? null;
1085
- }
1086
-
1087
- function collectChatMessageStartLines(tui: any, role: "user" | "assistant"): number[] {
1088
- const children = Array.isArray(tui?.children) ? tui.children : [];
1089
- const width = Math.max(1, tui?.terminal?.columns ?? 80);
1090
- const targets: number[] = [];
1091
- let offset = 0;
1092
- for (const child of children) {
1093
- const result = collectMessageStartLines(child, width, role, offset);
1094
- targets.push(...result.targets);
1095
- offset += result.lineCount;
1096
- }
1097
- return [...new Set(targets)].sort((a, b) => a - b);
1098
- }
1099
-
1100
- function collectMessageStartLines(component: unknown, width: number, role: "user" | "assistant", offset: number): { targets: number[]; lineCount: number } {
1101
- const lineCount = renderLineCount(component, width);
1102
- if (isChatMessageComponentForRole(component, role)) return { targets: [offset], lineCount };
1103
- const children = typeof component === "object" && component !== null ? Reflect.get(component, "children") : null;
1104
- if (!Array.isArray(children) || children.length === 0) return { targets: [], lineCount };
1105
-
1106
- const targets: number[] = [];
1107
- let childOffset = offset;
1108
- let childrenLineCount = 0;
1109
- for (const child of children) {
1110
- const result = collectMessageStartLines(child, width, role, childOffset);
1111
- targets.push(...result.targets);
1112
- childOffset += result.lineCount;
1113
- childrenLineCount += result.lineCount;
1114
- }
1115
- return { targets, lineCount: Math.max(lineCount, childrenLineCount) };
1116
- }
1117
-
1118
- function isChatMessageComponentForRole(component: unknown, role: "user" | "assistant"): boolean {
1119
- if (typeof component !== "object" || component === null) return false;
1120
- const componentName = Reflect.get(component, "constructor")?.name;
1121
- if (role === "assistant") return componentName === "AssistantMessageComponent";
1122
- return componentName === "UserMessageComponent" || componentName === "SkillInvocationMessageComponent";
1123
- }
1124
-
1125
- function renderLineCount(component: unknown, width: number): number {
1126
- if (typeof component !== "object" || component === null) return 0;
1127
- const render = Reflect.get(component, "render");
1128
- if (typeof render !== "function") return 0;
1129
- try {
1130
- const lines = render.call(component, width);
1131
- return Array.isArray(lines) ? lines.length : 0;
1132
- } catch {
1133
- return 0;
1134
- }
1135
- }
1136
-
1137
- function copyTextToClipboard(text: string, copyToClipboard: (text: string) => Promise<void> | void): Promise<void> {
1138
- try {
1139
- return Promise.resolve(copyToClipboard(text));
1140
- } catch {
1141
- return Promise.reject(new Error("copy failed"));
1142
- }
1143
- }
1144
-
1145
- export function registerBottomInputShortcuts(pi: ExtensionAPI, runtime: BottomInputRuntime): void {
1146
- pi.registerShortcut?.("alt+s", {
1147
- description: "Stash/restore the current input text",
1148
- handler: (ctx: any) => {
1149
- runtime.stashOrRestoreEditorText(ctx);
1150
- },
1151
- });
1152
- }
1153
-
1154
- function notify(ctx: any, message: string, level: "info" | "warning" | "error"): void {
1155
- try {
1156
- const ui = readRuntimeUI(ctx);
1157
- if (ui.stale) return;
1158
- ui.ui?.notify?.(message, level);
1159
- } catch {
1160
- }
1161
- }
1162
-
1163
- function hasOverlay(ctx: any, fallbackTui?: any): boolean {
1164
- try {
1165
- const ui = readRuntimeUI(ctx);
1166
- if (ui.stale) return false;
1167
- const tui = fallbackTui ?? ui.ui?.tui ?? ctx?.tui;
1168
- if (typeof tui?.hasOverlay === "function") return Boolean(tui.hasOverlay());
1169
- const overlayStack = Array.isArray(tui?.overlayStack) ? tui.overlayStack : [];
1170
- return overlayStack.some((entry: any) => entry?.visible !== false && entry?.hidden !== true);
1171
- } catch {
1172
- return false;
1173
- }
1174
- }
1175
-
1176
- function readRuntimeUI(ctx: any): RuntimeUIReadResult {
1177
- try {
1178
- if (!ctx || ctx.hasUI !== true || !ctx.ui) return { stale: false };
1179
- return { stale: false, ui: ctx.ui as RuntimeUI };
1180
- } catch (error) {
1181
- if (isStaleCtxError(error)) return { stale: true };
1182
- return { stale: false };
1183
- }
1184
- }
1185
-
1186
- function isStaleCtxError(error: unknown): boolean {
1187
- const message = error instanceof Error ? error.message : String(error);
1188
- return message.includes("extension ctx is stale") || message.includes("stale ctx");
1189
- }
1190
-
1191
- function hasNonWhitespaceText(value: string): boolean {
1192
- return /\S/.test(value);
1193
- }
1194
-
1195
- function formatFailure(error: unknown): string {
1196
- return error instanceof Error ? error.message : String(error);
1197
- }