pi-ui-extend 0.1.17 → 0.1.19

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 (79) hide show
  1. package/dist/app/app.js +8 -6
  2. package/dist/app/constants.d.ts +1 -0
  3. package/dist/app/constants.js +1 -0
  4. package/dist/app/input/input-controller.d.ts +1 -0
  5. package/dist/app/input/input-controller.js +29 -0
  6. package/dist/app/input/input-paste-handler.d.ts +1 -1
  7. package/dist/app/input/input-paste-handler.js +6 -5
  8. package/dist/app/input/voice-controller.js +16 -12
  9. package/dist/app/model/model-usage-status.js +4 -27
  10. package/dist/app/popup/popup-menu-controller.d.ts +1 -5
  11. package/dist/app/popup/popup-menu-controller.js +7 -8
  12. package/dist/app/process.js +7 -0
  13. package/dist/app/rendering/conversation-entry-renderer.js +17 -16
  14. package/dist/app/rendering/conversation-viewport.js +4 -35
  15. package/dist/app/rendering/editor-layout-renderer.d.ts +5 -1
  16. package/dist/app/rendering/editor-layout-renderer.js +25 -16
  17. package/dist/app/rendering/popup-menu-renderer.d.ts +1 -5
  18. package/dist/app/rendering/popup-menu-renderer.js +24 -34
  19. package/dist/app/rendering/render-controller.d.ts +2 -0
  20. package/dist/app/rendering/render-controller.js +38 -33
  21. package/dist/app/rendering/render-text.js +2 -2
  22. package/dist/app/rendering/status-line-renderer.js +1 -1
  23. package/dist/app/rendering/tab-line-renderer.js +3 -3
  24. package/dist/app/runtime.js +29 -3
  25. package/dist/app/screen/file-link-opener.d.ts +2 -0
  26. package/dist/app/screen/file-link-opener.js +84 -17
  27. package/dist/app/screen/mouse-controller.d.ts +1 -2
  28. package/dist/app/screen/mouse-controller.js +19 -28
  29. package/dist/app/screen/screen-styler.js +1 -1
  30. package/dist/app/session/lazy-session-manager.d.ts +1 -1
  31. package/dist/app/session/lazy-session-manager.js +64 -52
  32. package/dist/app/session/queued-message-controller.d.ts +6 -0
  33. package/dist/app/session/queued-message-controller.js +9 -1
  34. package/dist/app/session/queued-message-entries.d.ts +8 -0
  35. package/dist/app/session/queued-message-entries.js +41 -0
  36. package/dist/app/session/session-lifecycle-controller.d.ts +9 -1
  37. package/dist/app/session/session-lifecycle-controller.js +45 -11
  38. package/dist/app/session/tabs-controller.d.ts +11 -1
  39. package/dist/app/session/tabs-controller.js +197 -30
  40. package/dist/app/terminal/terminal-controller.d.ts +2 -0
  41. package/dist/app/terminal/terminal-controller.js +7 -5
  42. package/dist/schemas/pi-tools-suite-schema.d.ts +3 -0
  43. package/dist/schemas/pi-tools-suite-schema.js +3 -0
  44. package/dist/theme.d.ts +3 -0
  45. package/dist/theme.js +8 -2
  46. package/extensions/session-title/config.ts +3 -3
  47. package/extensions/session-title/index.ts +60 -5
  48. package/external/pi-tools-suite/README.md +3 -2
  49. package/external/pi-tools-suite/src/antigravity-auth/auth-store.ts +1 -0
  50. package/external/pi-tools-suite/src/antigravity-auth/oauth.ts +1 -0
  51. package/external/pi-tools-suite/src/async-subagents/core/config.ts +0 -3
  52. package/external/pi-tools-suite/src/async-subagents/core/notifications.ts +64 -0
  53. package/external/pi-tools-suite/src/async-subagents/core/spawn.ts +1 -0
  54. package/external/pi-tools-suite/src/async-subagents/index.ts +54 -8
  55. package/external/pi-tools-suite/src/async-subagents/tools/spawn.ts +4 -4
  56. package/external/pi-tools-suite/src/config.ts +56 -0
  57. package/external/pi-tools-suite/src/dcp/commands.ts +1 -1
  58. package/external/pi-tools-suite/src/dcp/index.ts +21 -1
  59. package/external/pi-tools-suite/src/dcp/state.ts +234 -7
  60. package/external/pi-tools-suite/src/default-pi-tools-suite-config.ts +10 -1
  61. package/external/pi-tools-suite/src/glm-coding-discipline/index.ts +580 -0
  62. package/external/pi-tools-suite/src/index.ts +2 -0
  63. package/external/pi-tools-suite/src/lib/lsp.ts +2 -5
  64. package/external/pi-tools-suite/src/lsp/_shared/config.ts +2 -0
  65. package/external/pi-tools-suite/src/lsp/_shared/types.ts +2 -0
  66. package/external/pi-tools-suite/src/lsp/manager.ts +15 -9
  67. package/external/pi-tools-suite/src/telegram-mirror/README.md +168 -0
  68. package/external/pi-tools-suite/src/telegram-mirror/bot.ts +228 -0
  69. package/external/pi-tools-suite/src/telegram-mirror/events.ts +94 -0
  70. package/external/pi-tools-suite/src/telegram-mirror/format.ts +120 -0
  71. package/external/pi-tools-suite/src/telegram-mirror/index.ts +424 -0
  72. package/external/pi-tools-suite/src/telegram-mirror/ipc.ts +420 -0
  73. package/external/pi-tools-suite/src/telegram-mirror/multiplexer.ts +408 -0
  74. package/external/pi-tools-suite/src/telegram-mirror/renderer.ts +214 -0
  75. package/external/pi-tools-suite/src/todo/index.ts +81 -4
  76. package/external/pi-tools-suite/src/todo/tool/response-envelope.ts +5 -0
  77. package/external/pi-tools-suite/src/tool-descriptions.ts +4 -4
  78. package/package.json +1 -1
  79. package/schemas/pi-tools-suite.json +19 -0
package/dist/app/app.js CHANGED
@@ -169,7 +169,7 @@ export class PiUiExtendApp {
169
169
  noSession: false,
170
170
  sessionPath,
171
171
  }),
172
- activateRuntime: (runtime) => this.activateRuntime(runtime),
172
+ activateRuntime: (runtime, options) => this.activateRuntime(runtime, options),
173
173
  disposeRuntime: (runtime) => this.terminalController.disposeRuntime(runtime),
174
174
  isRunning: () => this.running,
175
175
  setStatus: (status) => this.setStatus(status),
@@ -398,6 +398,7 @@ export class PiUiExtendApp {
398
398
  get subagentsWidgetState() { return app.subagentsWidgetController.widgetState; },
399
399
  get voicePartialText() { return app.voicePartialText; },
400
400
  get autocompleteSuggestion() { return app.autocompleteController.suggestionText(); },
401
+ get queuedMessageWidgetEntries() { return app.queuedMessages.deferredQueuedEntries(); },
401
402
  renderExtensionInputComponent: (width) => this.extensionUiController.renderActiveCustomUi(width),
402
403
  extensionInputUsesEditor: () => this.extensionUiController.activeCustomUiUsesEditor(),
403
404
  widgetTuiHandle: () => this.extensionUiController.widgetTuiHandle(),
@@ -586,6 +587,7 @@ export class PiUiExtendApp {
586
587
  statusLineRenderer: this.statusLineRenderer,
587
588
  tabLineRenderer: this.tabLineRenderer,
588
589
  toastController: this.toastController,
590
+ loadingConversationOverlayText: () => this.tabsController.isSwitching() ? "Loading…" : undefined,
589
591
  voiceProgressOverlayText: () => this.voiceController.progressOverlayText(),
590
592
  });
591
593
  this.requestHistory = new AppRequestHistory({
@@ -767,15 +769,15 @@ export class PiUiExtendApp {
767
769
  // Startup update checks should never interrupt the TUI.
768
770
  }
769
771
  }
770
- async bindCurrentSession() {
771
- await this.sessionLifecycle.bindCurrentSession();
772
+ async bindCurrentSession(options) {
773
+ await this.sessionLifecycle.bindCurrentSession(options);
772
774
  }
773
- async activateRuntime(runtime) {
775
+ async activateRuntime(runtime, options) {
774
776
  this.runtime = runtime;
775
777
  runtime.setRebindSession(async () => {
776
- await this.bindCurrentSession();
778
+ await this.bindCurrentSession({ awaitExtensions: false });
777
779
  });
778
- await this.bindCurrentSession();
780
+ await this.bindCurrentSession(options);
779
781
  }
780
782
  createExtensionEventBus() {
781
783
  return createIsolatedExtensionEventBus((channel, data) => {
@@ -27,6 +27,7 @@ export declare const DISABLE_TERMINAL_WRAP = "\u001B[?7l";
27
27
  export declare const ENABLE_TERMINAL_WRAP = "\u001B[?7h";
28
28
  export declare const HIDE_CURSOR = "\u001B[?25l";
29
29
  export declare const SHOW_CURSOR = "\u001B[?25h";
30
+ export declare const RESET_TERMINAL_VIEWPORT_STATE = "\u001B[?6l\u001B[?69l\u001B[r";
30
31
  export declare const CLEAR_TERMINAL = "\u001B[2J\u001B[3J\u001B[H";
31
32
  export declare const THINKING_TOOL_NAME = "thinking";
32
33
  export declare const SUBAGENTS_TOOL_NAME = "subagents";
@@ -60,6 +60,7 @@ export const DISABLE_TERMINAL_WRAP = "\x1b[?7l";
60
60
  export const ENABLE_TERMINAL_WRAP = "\x1b[?7h";
61
61
  export const HIDE_CURSOR = "\x1b[?25l";
62
62
  export const SHOW_CURSOR = "\x1b[?25h";
63
+ export const RESET_TERMINAL_VIEWPORT_STATE = "\x1b[?6l\x1b[?69l\x1b[r";
63
64
  export const CLEAR_TERMINAL = "\x1b[2J\x1b[3J\x1b[H";
64
65
  export const THINKING_TOOL_NAME = "thinking";
65
66
  export const SUBAGENTS_TOOL_NAME = "subagents";
@@ -37,6 +37,7 @@ export declare class AppInputController {
37
37
  handleChunk(chunk: Buffer): void;
38
38
  private consumeSharedEditorShiftEnter;
39
39
  private drainInputBuffer;
40
+ private consumeBracketedPastePayload;
40
41
  private getEscapeSequences;
41
42
  private isPendingEscapeSequence;
42
43
  private consumeEscapeSequence;
@@ -42,6 +42,8 @@ export class AppInputController {
42
42
  }
43
43
  drainInputBuffer() {
44
44
  while (this.inputBuffer.length > 0) {
45
+ if (this.consumeBracketedPastePayload())
46
+ continue;
45
47
  const mouseMatch = /^\x1b\[<(\d+);(-?\d+);(-?\d+)([mM])/.exec(this.inputBuffer);
46
48
  if (mouseMatch) {
47
49
  this.inputBuffer = this.inputBuffer.slice(mouseMatch[0].length);
@@ -85,6 +87,23 @@ export class AppInputController {
85
87
  this.handleChar(char);
86
88
  }
87
89
  }
90
+ consumeBracketedPastePayload() {
91
+ if (!this.host.inputEditor.isInBracketedPaste)
92
+ return false;
93
+ const endSequence = "\x1b[201~";
94
+ const endIndex = this.inputBuffer.indexOf(endSequence);
95
+ if (endIndex === 0)
96
+ return false;
97
+ const payloadEnd = endIndex === -1
98
+ ? safeBracketedPastePayloadLength(this.inputBuffer, endSequence)
99
+ : endIndex;
100
+ if (payloadEnd === 0)
101
+ return false;
102
+ const payload = this.inputBuffer.slice(0, payloadEnd);
103
+ this.inputBuffer = this.inputBuffer.slice(payloadEnd);
104
+ this.pasteHandler.appendBracketedPasteText(normalizeBracketedPastePayload(payload));
105
+ return true;
106
+ }
88
107
  getEscapeSequences() {
89
108
  return [
90
109
  ["\x1b[13;2u", () => this.insertInputNewline()],
@@ -425,3 +444,13 @@ export class AppInputController {
425
444
  return this.host.isShiftPressed?.() ?? isNativeShiftPressed();
426
445
  }
427
446
  }
447
+ function normalizeBracketedPastePayload(payload) {
448
+ return payload.replace(/\r\n/g, "\n").replace(/\r/g, "\n");
449
+ }
450
+ function safeBracketedPastePayloadLength(buffer, endSequence) {
451
+ for (let length = Math.min(buffer.length, endSequence.length - 1); length > 0; length--) {
452
+ if (endSequence.startsWith(buffer.slice(buffer.length - length)))
453
+ return buffer.length - length;
454
+ }
455
+ return buffer.length;
456
+ }
@@ -7,7 +7,7 @@ export type InputPasteHost = {
7
7
  };
8
8
  export declare class InputPasteHandler {
9
9
  private readonly host;
10
- private pasteBuffer;
10
+ private pasteBufferParts;
11
11
  private readonly recentPasteFingerprints;
12
12
  private suppressImagePathPasteUntil;
13
13
  constructor(host: InputPasteHost);
@@ -7,7 +7,7 @@ import { normalizePastedTextForDuplicateKey } from "../rendering/render-text.js"
7
7
  const PASTE_FINGERPRINT_PREFIX_CHARS = 64 * 1024;
8
8
  export class InputPasteHandler {
9
9
  host;
10
- pasteBuffer = "";
10
+ pasteBufferParts = [];
11
11
  recentPasteFingerprints = new Map();
12
12
  suppressImagePathPasteUntil = 0;
13
13
  constructor(host) {
@@ -31,15 +31,16 @@ export class InputPasteHandler {
31
31
  }
32
32
  beginBracketedPaste() {
33
33
  this.host.inputEditor.beginBracketedPaste();
34
- this.pasteBuffer = "";
34
+ this.pasteBufferParts = [];
35
35
  }
36
36
  appendBracketedPasteText(text) {
37
- this.pasteBuffer += text;
37
+ if (text)
38
+ this.pasteBufferParts.push(text);
38
39
  }
39
40
  endBracketedPaste() {
40
41
  this.host.inputEditor.endBracketedPaste();
41
- const text = this.pasteBuffer;
42
- this.pasteBuffer = "";
42
+ const text = this.pasteBufferParts.join("");
43
+ this.pasteBufferParts = [];
43
44
  this.handlePasteEnd(text);
44
45
  }
45
46
  async handleClipboardImagePaste() {
@@ -5,6 +5,7 @@ import http from "node:http";
5
5
  import https from "node:https";
6
6
  import { createRequire } from "node:module";
7
7
  import { join } from "node:path";
8
+ import { pipeline } from "node:stream/promises";
8
9
  import { fileURLToPath } from "node:url";
9
10
  import { savePixDictationLanguage } from "../../config.js";
10
11
  import { APP_ICONS } from "../icons.js";
@@ -364,33 +365,36 @@ async function pathExists(path) {
364
365
  async function downloadFile(url, destination, redirects = 3) {
365
366
  await new Promise((resolve, reject) => {
366
367
  const client = url.startsWith("https:") ? https : http;
368
+ let settled = false;
369
+ const finish = (callback) => {
370
+ if (settled)
371
+ return;
372
+ settled = true;
373
+ callback();
374
+ };
367
375
  const request = client.get(url, (response) => {
368
376
  const statusCode = response.statusCode ?? 0;
369
377
  const location = response.headers.location;
370
378
  if ([301, 302, 303, 307, 308].includes(statusCode) && location && redirects > 0) {
371
379
  response.resume();
372
380
  const redirectedUrl = new URL(location, url).toString();
373
- downloadFile(redirectedUrl, destination, redirects - 1).then(resolve, reject);
381
+ downloadFile(redirectedUrl, destination, redirects - 1).then(() => finish(resolve), (error) => finish(() => reject(error)));
374
382
  return;
375
383
  }
376
384
  if (statusCode !== 200) {
377
385
  response.resume();
378
- reject(new Error(`download failed with HTTP ${statusCode}`));
386
+ finish(() => reject(new Error(`download failed with HTTP ${statusCode}`)));
379
387
  return;
380
388
  }
381
389
  const file = createWriteStream(destination);
382
- file.on("finish", () => {
383
- file.close((error) => {
384
- if (error)
385
- reject(error);
386
- else
387
- resolve();
388
- });
390
+ response.on("error", (error) => {
391
+ finish(() => reject(error));
389
392
  });
390
- file.on("error", reject);
391
- response.pipe(file);
393
+ pipeline(response, file).then(() => finish(resolve), (error) => finish(() => reject(error)));
394
+ });
395
+ request.on("error", (error) => {
396
+ finish(() => reject(error));
392
397
  });
393
- request.on("error", reject);
394
398
  });
395
399
  }
396
400
  async function extractZip(zipPath, destination) {
@@ -815,9 +815,9 @@ function formatDurationShort(resetAt, now) {
815
815
  const hours = Math.floor((totalMinutes % 1440) / 60);
816
816
  const minutes = totalMinutes % 60;
817
817
  if (days > 0)
818
- return `${days}d ${hours}h`;
818
+ return `${days}d${hours}h`;
819
819
  if (hours > 0)
820
- return `${hours}h ${minutes}m`;
820
+ return `${hours}h${minutes}m`;
821
821
  return `${minutes}m`;
822
822
  }
823
823
  function maskCredential(value) {
@@ -826,29 +826,6 @@ function maskCredential(value) {
826
826
  return visible ? "****" : "unknown";
827
827
  return `${visible.slice(0, 4)}****${visible.slice(-4)}`;
828
828
  }
829
- function formatUsageWindow(prefix, window, now) {
830
- const resetLabel = prefix === "H" ? formatResetTime(window.resetAt, now) : formatGlobalResetLabel(window.resetAt, now);
831
- return `${window.remainingPercent}% ${formatCompactProgressBar(window.remainingPercent)} ${resetLabel}`;
832
- }
833
- function formatGlobalResetLabel(resetAt, now) {
834
- if (resetAt <= now)
835
- return "reset";
836
- return resetAt - now <= DAY_SECONDS * 1000 ? formatResetTime(resetAt, now) : formatResetDate(resetAt, now);
837
- }
838
- function formatResetTime(resetAt, now) {
839
- if (resetAt <= now)
840
- return "reset";
841
- return new Date(resetAt).toLocaleTimeString("ru-RU", {
842
- hour: "2-digit",
843
- minute: "2-digit",
844
- hourCycle: "h23",
845
- });
846
- }
847
- function formatResetDate(resetAt, now) {
848
- if (resetAt <= now)
849
- return "reset";
850
- return new Date(resetAt).toLocaleDateString("ru-RU", {
851
- day: "2-digit",
852
- month: "2-digit",
853
- });
829
+ function formatUsageWindow(_prefix, window, now) {
830
+ return `${window.remainingPercent}% ${formatCompactProgressBar(window.remainingPercent)} ${formatDurationShort(window.resetAt, now)}`;
854
831
  }
@@ -15,11 +15,7 @@ type PopupMenuRendererPort = {
15
15
  effectivePopupMenuWidth(columns: number): number;
16
16
  styleOverlayLine(row: number, line: RenderedLine, width: number, activeMenu: PopupMenu<unknown>): string;
17
17
  overlayPlainText(line: RenderedLine, width: number): string;
18
- renderInlineUserMessageMenu(options: {
19
- userContentWidth: number;
20
- userContentLeft: number;
21
- userLine: (text: string, entryId?: string, syntaxHighlight?: RenderedLine["syntaxHighlight"]) => RenderedLine;
22
- }, menu: PopupMenu<UserMessagePopupMenuValue>): RenderedLine[];
18
+ renderUserMessageMenu(width: number, menu: PopupMenu<UserMessagePopupMenuValue>): RenderedLine[];
23
19
  renderSlashCommandMenu(width: number, menu: PopupMenu<SlashCommandMenuValue>): RenderedLine[];
24
20
  renderModelMenu(width: number, menu: PopupMenu<ModelPopupMenuValue>): RenderedLine[];
25
21
  renderThinkingMenu(width: number, menu: PopupMenu<ThinkingPopupMenuValue>): RenderedLine[];
@@ -435,10 +435,8 @@ export class AppPopupMenuController {
435
435
  renderActivePopupMenu(width) {
436
436
  if (this.syncQueueMessageMenu())
437
437
  return this.renderer.renderQueueMessageMenu(width, this.queueMessageMenu);
438
- // User-message actions are rendered inline inside the selected message block.
439
- // They must never also appear as the global popup above the input editor.
440
438
  if (this.syncUserMessageMenu())
441
- return [];
439
+ return this.renderer.renderUserMessageMenu(width, this.userMessageMenu);
442
440
  if (this.syncUserMessageJumpMenu())
443
441
  return this.renderer.renderUserMessageJumpMenu(width, this.userMessageJumpMenu, this.directPopupMenuQuery);
444
442
  if (this.syncResumeMenu()) {
@@ -474,15 +472,16 @@ export class AppPopupMenuController {
474
472
  return this.renderer.overlayPlainText(line, width);
475
473
  }
476
474
  isDynamicConversationBlock(entry) {
477
- return entry.kind === "user" && this.directPopupMenu === "user-message" && this.activeUserMessageEntryId === entry.id;
475
+ void entry;
476
+ return false;
478
477
  }
479
478
  hasDynamicConversationBlock() {
480
- return this.directPopupMenu === "user-message" && this.activeUserMessageEntryId !== undefined;
479
+ return false;
481
480
  }
482
481
  renderInlineUserMessageMenu(entry, options) {
483
- if (!(this.directPopupMenu === "user-message" && this.activeUserMessageEntryId === entry.id && this.syncUserMessageMenu()))
484
- return [];
485
- return this.renderer.renderInlineUserMessageMenu(options, this.userMessageMenu);
482
+ void entry;
483
+ void options;
484
+ return [];
486
485
  }
487
486
  withoutCloseMenuItems(items) {
488
487
  return items.filter((item) => item.label.trim().toLowerCase() !== "cancel");
@@ -7,6 +7,7 @@ export async function runProcess(command, args = [], options = {}) {
7
7
  let stderr = "";
8
8
  let error;
9
9
  let timedOut = false;
10
+ let forceKillTimer;
10
11
  const child = spawn(command, [...args], {
11
12
  cwd: options.cwd,
12
13
  env: options.env,
@@ -21,6 +22,10 @@ export async function runProcess(command, args = [], options = {}) {
21
22
  : setTimeout(() => {
22
23
  timedOut = true;
23
24
  child.kill("SIGTERM");
25
+ forceKillTimer = setTimeout(() => {
26
+ child.kill("SIGKILL");
27
+ }, 3_000);
28
+ forceKillTimer.unref?.();
24
29
  }, options.timeoutMs);
25
30
  timer?.unref?.();
26
31
  child.stdout.on("data", (chunk) => {
@@ -35,6 +40,8 @@ export async function runProcess(command, args = [], options = {}) {
35
40
  child.once("close", (status, signal) => {
36
41
  if (timer)
37
42
  clearTimeout(timer);
43
+ if (forceKillTimer)
44
+ clearTimeout(forceKillTimer);
38
45
  resolve({
39
46
  status,
40
47
  signal,
@@ -9,26 +9,19 @@ export function renderConversationEntry(entry, width, options) {
9
9
  const { left: userContentLeft, contentWidth: userContentWidth } = horizontalPaddingLayout(width);
10
10
  const userLine = (text, entryId, syntaxHighlight, segments) => ({
11
11
  text: padHorizontalText(text, width),
12
- colorOverride: options.colors.inputForeground,
13
- backgroundOverride: options.colors.userMessageBackground,
12
+ colorOverride: options.colors.warning,
14
13
  ...(segments && segments.length > 0 ? { segments: segments.map((segment) => ({ ...segment, start: segment.start + userContentLeft, end: segment.end + userContentLeft })) } : {}),
15
14
  ...(syntaxHighlight === undefined ? {} : { syntaxHighlight }),
16
15
  ...(entryId === undefined ? {} : { target: { kind: "user-message", id: entryId } }),
17
16
  });
18
17
  const queuedLine = (text, entryId, segments) => ({
19
18
  text,
20
- variant: "muted",
21
- backgroundOverride: options.colors.userMessageBackground,
19
+ colorOverride: options.colors.warning,
22
20
  ...(segments && segments.length > 0 ? { segments } : {}),
23
21
  target: { kind: "queue-message", id: entryId },
24
22
  });
25
23
  const userMessageLines = (userEntry) => {
26
- const lines = [
27
- userLine("", userEntry.id),
28
- ...renderMarkdownTextLines(userEntry.text, userContentWidth, userContentLeft).map((line) => userLine(line.text, userEntry.id, line.syntaxHighlight, line.segments)),
29
- ];
30
- lines.push(...options.renderInlineUserMessageMenu(userEntry, { userContentWidth, userContentLeft, userLine }));
31
- lines.push(userLine("", userEntry.id));
24
+ const lines = renderMarkdownTextLines(userEntry.text, userContentWidth, userContentLeft).map((line) => userLine(line.text, userEntry.id, line.syntaxHighlight, line.segments));
32
25
  return attachImageClickTargets(lines, userEntry.id, userEntry.images, { foreground: options.colors.info, underline: true });
33
26
  };
34
27
  const queuedMessageLines = (queuedEntry) => {
@@ -70,10 +63,18 @@ function renderAssistantLines(text, width, options) {
70
63
  const displayText = applyOutputFilters(text, options.outputFilters).trimEnd();
71
64
  if (!displayText)
72
65
  return [];
73
- return renderMarkdownTextLines(displayText, width).map((line) => ({
74
- text: line.text,
75
- colorOverride: options.colors.assistantForeground,
76
- ...(line.segments && line.segments.length > 0 ? { segments: line.segments } : {}),
77
- ...(line.syntaxHighlight ? { syntaxHighlight: line.syntaxHighlight } : {}),
78
- }));
66
+ const { left: contentLeft, contentWidth } = horizontalPaddingLayout(width);
67
+ const contentLines = renderMarkdownTextLines(displayText, contentWidth, contentLeft);
68
+ if (contentLines.length === 0)
69
+ return [];
70
+ const lines = [];
71
+ for (const line of contentLines) {
72
+ lines.push({
73
+ text: padHorizontalText(line.text, width),
74
+ colorOverride: options.colors.assistantForeground,
75
+ ...(line.segments && line.segments.length > 0 ? { segments: line.segments.map((segment) => ({ ...segment, start: segment.start + contentLeft, end: segment.end + contentLeft })) } : {}),
76
+ ...(line.syntaxHighlight ? { syntaxHighlight: line.syntaxHighlight } : {}),
77
+ });
78
+ }
79
+ return lines;
79
80
  }
@@ -1,7 +1,8 @@
1
1
  import { resolveToolRule } from "../../config.js";
2
2
  import { stringDisplayWidth } from "../../terminal-width.js";
3
3
  import { renderConversationEntry as renderConversationEntryLines } from "./conversation-entry-renderer.js";
4
- import { horizontalPaddingLayout, shortHash } from "./render-text.js";
4
+ import { horizontalPaddingLayout } from "./render-text.js";
5
+ import { sdkQueuedMessageEntries } from "../session/queued-message-entries.js";
5
6
  export class ConversationViewport {
6
7
  host;
7
8
  blockCachesByWidth = new Map();
@@ -119,39 +120,7 @@ export class ConversationViewport {
119
120
  return lineCount;
120
121
  }
121
122
  queuedEntries() {
122
- const session = this.host.session;
123
- const entries = [];
124
- for (const [index, text] of (session?.getSteeringMessages() ?? []).entries()) {
125
- entries.push({
126
- id: `queued-sdk-steering-${index}-${shortHash(text)}`,
127
- kind: "queued",
128
- mode: "steering",
129
- text,
130
- queueSource: "sdk-steering",
131
- queueIndex: index,
132
- });
133
- }
134
- for (const [index, text] of (session?.getFollowUpMessages() ?? []).entries()) {
135
- entries.push({
136
- id: `queued-sdk-follow-up-${index}-${shortHash(text)}`,
137
- kind: "queued",
138
- mode: "follow-up",
139
- text,
140
- queueSource: "sdk-follow-up",
141
- queueIndex: index,
142
- });
143
- }
144
- for (const [index, message] of this.host.deferredUserMessages.entries()) {
145
- entries.push({
146
- id: `${message.id}-${index}`,
147
- kind: "queued",
148
- mode: "steering",
149
- text: message.displayText,
150
- queueSource: "deferred",
151
- queueIndex: index,
152
- });
153
- }
154
- return entries;
123
+ return sdkQueuedMessageEntries(this.host.session);
155
124
  }
156
125
  layoutForWidth(width) {
157
126
  const queued = this.queuedEntries();
@@ -301,7 +270,7 @@ export class ConversationViewport {
301
270
  return estimateWrappedLineCount(entry.text, width);
302
271
  case "user": {
303
272
  const { contentWidth } = horizontalPaddingLayout(width);
304
- return 2 + estimateWrappedLineCount(entry.text, contentWidth);
273
+ return estimateWrappedLineCount(entry.text, contentWidth);
305
274
  }
306
275
  case "queued": {
307
276
  const { contentWidth } = horizontalPaddingLayout(width);
@@ -1,6 +1,6 @@
1
1
  import type { InputEditor } from "../../input-editor.js";
2
2
  import type { Theme } from "../../theme.js";
3
- import type { EditorLayout, ExtensionWidgetRegistration, ExtensionWidgetTheme, SubagentsWidgetState, TodoDetails, WidgetTuiHandle } from "../types.js";
3
+ import type { EditorLayout, Entry, ExtensionWidgetRegistration, ExtensionWidgetTheme, SubagentsWidgetState, TodoDetails, WidgetTuiHandle } from "../types.js";
4
4
  export type EditorLayoutRendererHost = {
5
5
  readonly theme: Theme;
6
6
  readonly inputEditor: InputEditor;
@@ -11,6 +11,9 @@ export type EditorLayoutRendererHost = {
11
11
  readonly subagentsWidgetState: SubagentsWidgetState | undefined;
12
12
  readonly voicePartialText: string | undefined;
13
13
  readonly autocompleteSuggestion: string | undefined;
14
+ readonly queuedMessageWidgetEntries: readonly Extract<Entry, {
15
+ kind: "queued";
16
+ }>[];
14
17
  renderExtensionInputComponent(width: number): string[] | undefined;
15
18
  extensionInputUsesEditor(): boolean;
16
19
  widgetTuiHandle(): WidgetTuiHandle;
@@ -24,6 +27,7 @@ export declare class EditorLayoutRenderer {
24
27
  private renderWidgetRegistration;
25
28
  private renderWidgetComponent;
26
29
  private renderAboveEditorEntities;
30
+ private renderQueuedMessageWidgets;
27
31
  private renderVoicePartial;
28
32
  private renderExtensionWidgets;
29
33
  private limitEntityLines;
@@ -1,8 +1,7 @@
1
1
  import { ABOVE_EDITOR_WIDGET_KEY_GROUPS, BUILT_IN_SUBAGENTS_WIDGET_KEYS, INPUT_MAX_ROWS, LEGACY_TODO_WIDGET_KEYS, } from "../constants.js";
2
2
  import { renderSubagentsPanel, renderTodoPanel } from "./editor-panels.js";
3
- import { ellipsizeDisplay, horizontalPaddingLayout, padHorizontalText, sanitizeText } from "./render-text.js";
3
+ import { ellipsizeDisplay, horizontalPaddingLayout, padHorizontalText, sanitizeText, wrapText } from "./render-text.js";
4
4
  import { APP_ICONS } from "../icons.js";
5
- const INPUT_FRAME_VERTICAL = "│";
6
5
  export class EditorLayoutRenderer {
7
6
  host;
8
7
  constructor(host) {
@@ -12,13 +11,13 @@ export class EditorLayoutRenderer {
12
11
  const maxAvailableInputRows = Math.max(1, rows - 5);
13
12
  const renderedInput = this.renderInput(width, Math.min(INPUT_MAX_ROWS, maxAvailableInputRows), maxAvailableInputRows);
14
13
  const maxEntityRows = Math.max(0, rows - renderedInput.lines.length - 4);
15
- const framedEntityWidth = inputFrameContentWidth(width);
16
- const aboveEditorEntities = this.renderAboveEditorEntities(framedEntityWidth);
14
+ const editorEntityWidth = inputFrameContentWidth(width);
15
+ const aboveEditorEntities = this.renderAboveEditorEntities(editorEntityWidth);
17
16
  let aboveEditorLines = this.limitEntityLines(aboveEditorEntities.lines, maxEntityRows);
18
17
  if (aboveEditorEntities.hasWidgets && aboveEditorLines.length < maxEntityRows) {
19
18
  aboveEditorLines = [...aboveEditorLines, { text: "", variant: "normal" }];
20
19
  }
21
- const belowEditorLines = this.limitEntityLines(this.renderExtensionWidgets("belowEditor", framedEntityWidth), maxEntityRows - aboveEditorLines.length);
20
+ const belowEditorLines = this.limitEntityLines(this.renderExtensionWidgets("belowEditor", editorEntityWidth), maxEntityRows - aboveEditorLines.length);
22
21
  const inputBottomSeparatorRow = rows - 1;
23
22
  const belowEditorStartRow = inputBottomSeparatorRow - belowEditorLines.length;
24
23
  const inputStartRow = belowEditorStartRow - renderedInput.lines.length;
@@ -57,7 +56,8 @@ export class EditorLayoutRenderer {
57
56
  const hasBuiltInTodoPanel = todoPanelLines.length > 0;
58
57
  const subagentsPanelLines = renderSubagentsPanel(this.host.subagentsWidgetState, this.host.subagentsPanelExpanded, width, this.host.theme.colors);
59
58
  const hasBuiltInSubagentsPanel = subagentsPanelLines.length > 0;
60
- const lines = [...todoPanelLines, ...subagentsPanelLines];
59
+ const queuedMessageWidgetLines = this.renderQueuedMessageWidgets(width);
60
+ const lines = [...todoPanelLines, ...subagentsPanelLines, ...queuedMessageWidgetLines];
61
61
  let hasWidgets = lines.length > 0;
62
62
  const consumedWidgetKeys = new Set();
63
63
  for (const widgetKeys of ABOVE_EDITOR_WIDGET_KEY_GROUPS) {
@@ -103,6 +103,22 @@ export class EditorLayoutRenderer {
103
103
  lines.push(...this.renderVoicePartial(width));
104
104
  return { lines, hasWidgets };
105
105
  }
106
+ renderQueuedMessageWidgets(width) {
107
+ const lines = [];
108
+ for (const entry of this.host.queuedMessageWidgetEntries) {
109
+ const icon = entry.queueSource === "deferred" ? APP_ICONS.pause : APP_ICONS.timerSand;
110
+ const wrapped = wrapText(`${icon} ${sanitizeText(entry.text)}`, width);
111
+ for (const [index, text] of wrapped.entries()) {
112
+ lines.push({
113
+ text: padHorizontalText(text, width),
114
+ colorOverride: this.host.theme.colors.warning,
115
+ target: { kind: "queue-message", id: entry.id },
116
+ ...(index === 0 ? { segments: [{ start: 0, end: icon.length, foreground: this.host.theme.colors.info }] } : {}),
117
+ });
118
+ }
119
+ }
120
+ return lines;
121
+ }
106
122
  renderVoicePartial(width) {
107
123
  const partial = this.host.voicePartialText?.trim();
108
124
  if (!partial)
@@ -150,7 +166,7 @@ export class EditorLayoutRenderer {
150
166
  const scrollBar = usesEditor
151
167
  ? inputScrollBarMetrics(rendered.visualLines.length, visibleLines.length, rendered.scrollOffset)
152
168
  : undefined;
153
- const editorLines = usesEditor ? visibleLines.map((vl) => frameInputLine(padHorizontalText(vl.text, width))) : [];
169
+ const editorLines = usesEditor ? visibleLines.map((vl) => padHorizontalText(vl.text, width)) : [];
154
170
  const editorTagSpans = usesEditor
155
171
  ? visibleLines.map((vl) => vl.tagSpans.map((span) => ({
156
172
  start: span.start + left,
@@ -163,7 +179,7 @@ export class EditorLayoutRenderer {
163
179
  end: span.end + left,
164
180
  })))
165
181
  : [];
166
- const paddedCustomLines = customLines.map((line) => frameInputLine(padHorizontalText(line, width)));
182
+ const paddedCustomLines = customLines.map((line) => padHorizontalText(line, width));
167
183
  return {
168
184
  lines: [...paddedCustomLines, ...editorLines],
169
185
  cursorRowOffset: customLines.length + rendered.cursorVisualRow - rendered.scrollOffset,
@@ -196,15 +212,8 @@ export class EditorLayoutRenderer {
196
212
  ];
197
213
  }
198
214
  }
199
- function frameInputLine(line) {
200
- if (line.length <= 0)
201
- return line;
202
- if (line.length === 1)
203
- return INPUT_FRAME_VERTICAL;
204
- return `${INPUT_FRAME_VERTICAL}${line.slice(1, -1)}${INPUT_FRAME_VERTICAL}`;
205
- }
206
215
  function inputFrameContentWidth(width) {
207
- return Math.max(1, width - 2);
216
+ return Math.max(1, width);
208
217
  }
209
218
  function inputScrollBarMetrics(totalLineCount, visibleRowCount, scrollOffset) {
210
219
  if (visibleRowCount <= 0 || totalLineCount <= visibleRowCount)
@@ -22,11 +22,7 @@ export declare class PopupMenuRenderer {
22
22
  effectivePopupMenuWidth(columns: number): number;
23
23
  styleOverlayLine(row: number, line: RenderedLine, width: number, activeMenu: PopupMenu<unknown>): string;
24
24
  overlayPlainText(line: RenderedLine, width: number): string;
25
- renderInlineUserMessageMenu(options: {
26
- userContentWidth: number;
27
- userContentLeft: number;
28
- userLine: (text: string, entryId?: string, syntaxHighlight?: RenderedLine["syntaxHighlight"]) => RenderedLine;
29
- }, menu: PopupMenu<UserMessageMenuValue>): RenderedLine[];
25
+ renderUserMessageMenu(width: number, menu: PopupMenu<UserMessageMenuValue>): RenderedLine[];
30
26
  renderSlashCommandMenu(width: number, menu: PopupMenu<SlashCommand>): RenderedLine[];
31
27
  renderModelMenu(width: number, menu: PopupMenu<ModelMenuValue>): RenderedLine[];
32
28
  renderThinkingMenu(width: number, menu: PopupMenu<ThinkingMenuValue>): RenderedLine[];