pi-ui-extend 0.1.13 → 0.1.17

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 (111) hide show
  1. package/README.md +1 -1
  2. package/dist/app/app.d.ts +7 -0
  3. package/dist/app/app.js +102 -17
  4. package/dist/app/commands/command-controller.js +2 -0
  5. package/dist/app/commands/command-host.d.ts +5 -0
  6. package/dist/app/commands/command-model-actions.d.ts +2 -0
  7. package/dist/app/commands/command-model-actions.js +40 -4
  8. package/dist/app/commands/command-navigation-actions.d.ts +9 -0
  9. package/dist/app/commands/command-navigation-actions.js +62 -0
  10. package/dist/app/commands/command-registry.d.ts +2 -0
  11. package/dist/app/commands/command-registry.js +16 -0
  12. package/dist/app/constants.d.ts +0 -1
  13. package/dist/app/constants.js +0 -1
  14. package/dist/app/extensions/extension-ui-controller.d.ts +16 -5
  15. package/dist/app/extensions/extension-ui-controller.js +99 -61
  16. package/dist/app/icons.d.ts +1 -0
  17. package/dist/app/icons.js +2 -0
  18. package/dist/app/input/input-action-controller.d.ts +2 -0
  19. package/dist/app/input/input-action-controller.js +8 -1
  20. package/dist/app/logger.d.ts +25 -0
  21. package/dist/app/logger.js +90 -0
  22. package/dist/app/model/model-usage-status.js +30 -15
  23. package/dist/app/popup/menu-items-controller.d.ts +4 -0
  24. package/dist/app/popup/menu-items-controller.js +68 -6
  25. package/dist/app/popup/popup-action-controller.d.ts +2 -1
  26. package/dist/app/popup/popup-action-controller.js +7 -4
  27. package/dist/app/popup/popup-menu-controller.d.ts +36 -23
  28. package/dist/app/popup/popup-menu-controller.js +97 -326
  29. package/dist/app/rendering/conversation-entry-renderer.js +3 -3
  30. package/dist/app/rendering/conversation-viewport.d.ts +10 -2
  31. package/dist/app/rendering/conversation-viewport.js +157 -16
  32. package/dist/app/rendering/editor-panels.js +22 -9
  33. package/dist/app/rendering/popup-menu-renderer.d.ts +62 -0
  34. package/dist/app/rendering/popup-menu-renderer.js +405 -0
  35. package/dist/app/rendering/render-controller.js +30 -28
  36. package/dist/app/rendering/render-text.js +5 -2
  37. package/dist/app/rendering/status-line-renderer.d.ts +8 -1
  38. package/dist/app/rendering/status-line-renderer.js +217 -117
  39. package/dist/app/rendering/toast-controller.d.ts +12 -3
  40. package/dist/app/rendering/toast-controller.js +70 -12
  41. package/dist/app/runtime.d.ts +2 -1
  42. package/dist/app/runtime.js +20 -10
  43. package/dist/app/screen/mouse-controller.d.ts +2 -2
  44. package/dist/app/screen/mouse-controller.js +27 -48
  45. package/dist/app/screen/screen-styler.d.ts +1 -1
  46. package/dist/app/screen/screen-styler.js +9 -7
  47. package/dist/app/screen/scroll-controller.d.ts +12 -9
  48. package/dist/app/screen/scroll-controller.js +56 -45
  49. package/dist/app/screen/status-controller.js +2 -1
  50. package/dist/app/session/lazy-session-manager.d.ts +11 -0
  51. package/dist/app/session/lazy-session-manager.js +539 -0
  52. package/dist/app/session/pix-system-message.d.ts +16 -0
  53. package/dist/app/session/pix-system-message.js +64 -0
  54. package/dist/app/session/request-history.d.ts +4 -0
  55. package/dist/app/session/request-history.js +11 -0
  56. package/dist/app/session/session-event-controller.d.ts +11 -0
  57. package/dist/app/session/session-event-controller.js +58 -2
  58. package/dist/app/session/session-history.d.ts +18 -0
  59. package/dist/app/session/session-history.js +72 -3
  60. package/dist/app/session/session-lifecycle-controller.d.ts +6 -2
  61. package/dist/app/session/session-lifecycle-controller.js +7 -2
  62. package/dist/app/session/session-search.js +10 -0
  63. package/dist/app/session/tabs-controller.d.ts +17 -5
  64. package/dist/app/session/tabs-controller.js +308 -29
  65. package/dist/app/todo/todo-model.d.ts +4 -2
  66. package/dist/app/todo/todo-model.js +23 -13
  67. package/dist/app/types.d.ts +17 -6
  68. package/dist/app/workspace/workspace-actions-controller.d.ts +2 -0
  69. package/dist/app/workspace/workspace-actions-controller.js +12 -0
  70. package/dist/config.d.ts +6 -1
  71. package/dist/config.js +82 -25
  72. package/dist/default-pix-config.js +4 -0
  73. package/dist/fuzzy.d.ts +2 -0
  74. package/dist/fuzzy.js +27 -7
  75. package/dist/input-editor.d.ts +9 -0
  76. package/dist/input-editor.js +52 -0
  77. package/dist/schemas/pi-tools-suite-schema.d.ts +1 -0
  78. package/dist/schemas/pi-tools-suite-schema.js +1 -0
  79. package/dist/schemas/pix-schema.d.ts +3 -1
  80. package/dist/schemas/pix-schema.js +6 -4
  81. package/dist/terminal-width.d.ts +2 -0
  82. package/dist/terminal-width.js +64 -3
  83. package/dist/theme.js +6 -6
  84. package/dist/ui.d.ts +8 -0
  85. package/external/pi-tools-suite/README.md +3 -2
  86. package/external/pi-tools-suite/src/antigravity-auth/auth-store.ts +52 -8
  87. package/external/pi-tools-suite/src/antigravity-auth/commands.ts +3 -41
  88. package/external/pi-tools-suite/src/antigravity-auth/constants.ts +0 -2
  89. package/external/pi-tools-suite/src/antigravity-auth/index.ts +11 -18
  90. package/external/pi-tools-suite/src/antigravity-auth/oauth.ts +129 -61
  91. package/external/pi-tools-suite/src/antigravity-auth/status.ts +82 -3
  92. package/external/pi-tools-suite/src/antigravity-auth/stream.ts +20 -7
  93. package/external/pi-tools-suite/src/antigravity-auth/types.ts +21 -0
  94. package/external/pi-tools-suite/src/config.ts +8 -0
  95. package/external/pi-tools-suite/src/dcp/index.ts +16 -1
  96. package/external/pi-tools-suite/src/dcp/state.ts +35 -0
  97. package/external/pi-tools-suite/src/default-pi-tools-suite-config.ts +3 -0
  98. package/external/pi-tools-suite/src/todo/index.ts +123 -14
  99. package/external/pi-tools-suite/src/todo/state/persistence.ts +0 -1
  100. package/external/pi-tools-suite/src/todo/state/state-reducer.ts +26 -43
  101. package/external/pi-tools-suite/src/todo/todo.ts +12 -23
  102. package/external/pi-tools-suite/src/todo/tool/response-envelope.ts +34 -16
  103. package/external/pi-tools-suite/src/todo/tool/types.ts +7 -28
  104. package/external/pi-tools-suite/src/todo/view/format.ts +2 -3
  105. package/external/pi-tools-suite/src/tool-descriptions.ts +6 -4
  106. package/external/pi-tools-suite/src/usage/index.ts +5 -2
  107. package/external/pi-tools-suite/src/usage/lib/google.ts +53 -40
  108. package/external/pi-tools-suite/src/usage/lib/types.ts +12 -2
  109. package/package.json +1 -1
  110. package/schemas/pi-tools-suite.json +4 -0
  111. package/schemas/pix.json +11 -2
@@ -0,0 +1,405 @@
1
+ import { colorLine } from "../../theme.js";
2
+ import { stringDisplayWidth } from "../../terminal-width.js";
3
+ import { resolveColor, resolveModelColor } from "../../config.js";
4
+ import { SLASH_COMMAND_DESCRIPTION_COLUMN, } from "../constants.js";
5
+ import { APP_ICONS } from "../icons.js";
6
+ import { ellipsizeDisplay, padOrTrimPlain, sanitizeText } from "./render-text.js";
7
+ import { modelProviderThemeColor, thinkingLevelThemeColor } from "./status-line-renderer.js";
8
+ const POPUP_MENU_ESCAPE_BUTTON = "Esc";
9
+ const POPUP_MENU_DESCRIPTION_GAP = " ";
10
+ const POPUP_MENU_HEADER_SIDE_PADDING = 2;
11
+ export class PopupMenuRenderer {
12
+ host;
13
+ constructor(host) {
14
+ this.host = host;
15
+ }
16
+ popupMenuWidth(columns) {
17
+ return columns;
18
+ }
19
+ popupMenuMargin(columns) {
20
+ return columns > 44 ? 2 : 0;
21
+ }
22
+ effectivePopupMenuWidth(columns) {
23
+ const sideMargin = this.popupMenuMargin(columns);
24
+ return Math.min(this.popupMenuWidth(columns), Math.max(1, columns - sideMargin * 2));
25
+ }
26
+ styleOverlayLine(row, line, width, activeMenu) {
27
+ const colors = this.host.theme.colors;
28
+ const margin = this.popupMenuMargin(width);
29
+ const menuWidth = this.effectivePopupMenuWidth(width);
30
+ const rightMargin = Math.max(0, width - margin - menuWidth);
31
+ const selected = line.target?.kind === "popup-menu" && activeMenu.selectedIndex === line.target.index;
32
+ const foreground = this.popupLineForeground(line);
33
+ const background = this.popupLineBackground(line, selected);
34
+ const plain = `${" ".repeat(margin)}${padOrTrimPlain(line.text, menuWidth)}${" ".repeat(rightMargin)}`;
35
+ if (this.host.screenStyler.selectionRangeForRow(row, width)) {
36
+ return this.host.screenStyler.styleLine(row, plain, width, { foreground, background });
37
+ }
38
+ return [
39
+ colorLine("", margin, { background: colors.background }),
40
+ line.segments && line.segments.length > 0
41
+ ? this.host.screenStyler.styleLineSegments(row, line.text, menuWidth, { foreground, background, bold: selected }, line.segments)
42
+ : colorLine(line.text, menuWidth, { foreground, background, bold: selected }),
43
+ colorLine("", rightMargin, { background: colors.background }),
44
+ ].join("");
45
+ }
46
+ overlayPlainText(line, width) {
47
+ const margin = this.popupMenuMargin(width);
48
+ const menuWidth = this.effectivePopupMenuWidth(width);
49
+ const rightMargin = Math.max(0, width - margin - menuWidth);
50
+ return `${" ".repeat(margin)}${padOrTrimPlain(line.text, menuWidth)}${" ".repeat(rightMargin)}`;
51
+ }
52
+ renderInlineUserMessageMenu(options, menu) {
53
+ const headerLine = options.userLine(formatPopupMenuHeader("Message actions", options.userContentWidth));
54
+ headerLine.target = { kind: "popup-menu-close" };
55
+ headerLine.segments = [{
56
+ start: options.userContentLeft,
57
+ end: options.userContentLeft + options.userContentWidth,
58
+ foreground: this.host.theme.colors.accent,
59
+ background: this.host.theme.colors.popupHeaderBackground,
60
+ bold: true,
61
+ }];
62
+ const lines = [headerLine];
63
+ for (const item of menu.visibleItems()) {
64
+ const label = item.label.padEnd(18, " ");
65
+ const description = item.description ?? "";
66
+ const marker = item.selected ? "▶" : " ";
67
+ const rawText = `${marker} ${label}${description}`;
68
+ const text = ellipsizeDisplay(rawText, options.userContentWidth);
69
+ const line = options.userLine(text);
70
+ line.target = { kind: "popup-menu", index: item.index };
71
+ const contentStart = options.userContentLeft;
72
+ const labelStart = contentStart + 2;
73
+ const labelEnd = Math.min(contentStart + text.length, labelStart + item.label.length);
74
+ const descriptionStart = contentStart + 2 + label.length;
75
+ line.segments = [
76
+ ...(item.selected ? [{ start: contentStart, end: contentStart + 1, foreground: this.host.theme.colors.accent, bold: true }] : []),
77
+ {
78
+ start: labelStart,
79
+ end: labelEnd,
80
+ foreground: this.userMessageActionForeground(item.value),
81
+ bold: item.selected,
82
+ },
83
+ ...(descriptionStart < contentStart + text.length
84
+ ? [{ start: descriptionStart, end: contentStart + text.length, foreground: this.host.theme.colors.muted }]
85
+ : []),
86
+ ];
87
+ lines.push(line);
88
+ }
89
+ return lines;
90
+ }
91
+ renderSlashCommandMenu(width, menu) {
92
+ const lines = [this.popupMenuHeader("Commands", width)];
93
+ const visibleItems = menu.visibleItems();
94
+ if (!this.hasPopupActionItems(menu.items)) {
95
+ lines.push({ text: " No matching slash commands", variant: "muted" });
96
+ }
97
+ for (const item of visibleItems) {
98
+ const marker = item.selected ? "▶ " : " ";
99
+ const text = `${marker}${this.labelDescriptionText(item.label, item.description, width - 2)}`;
100
+ lines.push({
101
+ text,
102
+ variant: "normal",
103
+ segments: this.itemHighlightSegments(item, text),
104
+ target: { kind: "popup-menu", index: item.index },
105
+ });
106
+ }
107
+ return lines;
108
+ }
109
+ renderModelMenu(width, menu) {
110
+ const lines = [this.popupMenuHeader("Select model", width)];
111
+ const visibleItems = menu.visibleItems();
112
+ if (!this.hasPopupActionItems(menu.items)) {
113
+ lines.push({
114
+ text: this.host.session ? " No matching favorite models" : " Model menu unavailable",
115
+ variant: "muted",
116
+ });
117
+ }
118
+ for (const item of visibleItems) {
119
+ const marker = item.selected ? "▶ " : " ";
120
+ const text = `${marker}${this.labelDescriptionText(item.label, item.description, width - 2)}`;
121
+ lines.push({
122
+ text,
123
+ variant: this.selectableItemVariant(item.value),
124
+ segments: [...this.modelMenuItemSegments(item.value), ...this.itemHighlightSegments(item, text)],
125
+ target: { kind: "popup-menu", index: item.index },
126
+ });
127
+ }
128
+ return lines;
129
+ }
130
+ renderThinkingMenu(width, menu) {
131
+ const lines = [this.popupMenuHeader("Thinking level", width)];
132
+ const visibleItems = menu.visibleItems();
133
+ if (!this.hasPopupActionItems(menu.items)) {
134
+ lines.push({ text: " No matching thinking levels", variant: "muted" });
135
+ }
136
+ for (const item of visibleItems) {
137
+ const marker = item.selected ? "▶ " : " ";
138
+ const text = `${marker}${this.labelDescriptionText(item.label, item.description, width - 2)}`;
139
+ lines.push({
140
+ text,
141
+ variant: this.selectableItemVariant(item.value),
142
+ segments: this.thinkingMenuItemSegments(item.value),
143
+ target: { kind: "popup-menu", index: item.index },
144
+ });
145
+ }
146
+ return lines;
147
+ }
148
+ renderResumeMenu(width, menu, state) {
149
+ const title = this.host.resumeLoading ? `Resume session ${APP_ICONS.timerSand}` : "Resume session";
150
+ const lines = [this.popupMenuHeader(title, width)];
151
+ const visibleItems = menu.visibleItems();
152
+ if (!this.host.resumeLoading && !this.hasPopupActionItems(menu.items)) {
153
+ lines.push({
154
+ text: this.host.resumeSessionCount === 0 ? " No sessions found" : " No matching sessions",
155
+ variant: "muted",
156
+ });
157
+ }
158
+ for (const item of visibleItems) {
159
+ const label = item.label;
160
+ const description = item.description ?? "";
161
+ const marker = item.selected ? "▶ " : " ";
162
+ const text = `${marker}${label} ${description}`;
163
+ const segments = [...(this.resumeMenuItemSegments(item.value, label, description, text) ?? []), ...this.itemHighlightSegments(item, text)];
164
+ lines.push({
165
+ text,
166
+ variant: "normal",
167
+ ...(segments.length === 0 ? {} : { segments }),
168
+ target: { kind: "popup-menu", index: item.index },
169
+ });
170
+ }
171
+ if (!state.allSessionsLoaded && state.loadedSessionCount > 0) {
172
+ lines.push({ text: ` Loaded ${state.loadedSessionCount} sessions · scroll for more`, variant: "muted" });
173
+ }
174
+ if (state.directQuery) {
175
+ lines.push({ text: ` Search: ${state.directQuery}`, variant: "muted" });
176
+ }
177
+ return lines;
178
+ }
179
+ renderUserMessageJumpMenu(width, menu, directQuery) {
180
+ const lines = [this.popupMenuHeader("Jump to user message", width)];
181
+ if (this.host.userMessageJumpLoading) {
182
+ lines.push({ text: ` ${APP_ICONS.timerSand} Loading user messages`, variant: "muted" });
183
+ }
184
+ else if (!this.hasPopupActionItems(menu.items)) {
185
+ lines.push({
186
+ text: this.host.entries.some((entry) => entry.kind === "user") ? " No matching user messages" : " No user messages yet",
187
+ variant: "muted",
188
+ });
189
+ }
190
+ const labelWidth = Math.max(1, width - 2);
191
+ for (const item of menu.visibleItems()) {
192
+ const label = ellipsizeDisplay(item.label, labelWidth);
193
+ const marker = item.selected ? "▶ " : " ";
194
+ const text = `${marker}${label}`;
195
+ lines.push({
196
+ text,
197
+ variant: "normal",
198
+ segments: this.itemHighlightSegments(item, text),
199
+ target: { kind: "popup-menu", index: item.index },
200
+ });
201
+ }
202
+ if (directQuery) {
203
+ lines.push({ text: ` Search: ${directQuery}`, variant: "muted" });
204
+ }
205
+ return lines;
206
+ }
207
+ renderQueueMessageMenu(width, menu) {
208
+ const lines = [this.popupMenuHeader("Queued message", width)];
209
+ for (const item of menu.visibleItems()) {
210
+ const marker = item.selected ? "▶ " : " ";
211
+ lines.push({
212
+ text: `${marker}${this.labelDescriptionText(item.label, item.description, width - 2, 16)}`,
213
+ variant: this.queueMessageItemVariant(item.value),
214
+ target: { kind: "popup-menu", index: item.index },
215
+ });
216
+ }
217
+ return lines;
218
+ }
219
+ renderSdkMenu(width, menu, request, directQuery) {
220
+ const lines = [this.popupMenuHeader(request?.options.title ?? "Menu", width)];
221
+ if (!this.hasPopupActionItems(menu.items)) {
222
+ lines.push({ text: ` ${request?.options.emptyText ?? "No matching items"}`, variant: "muted" });
223
+ }
224
+ for (const item of menu.visibleItems()) {
225
+ const marker = item.selected ? "▶ " : " ";
226
+ const text = `${marker}${this.labelDescriptionText(item.label, item.description, width - 2)}`;
227
+ const segments = this.sdkMenuItemSegments(item, text);
228
+ lines.push({
229
+ text,
230
+ variant: this.sdkItemVariant(item.value),
231
+ ...(segments.length === 0 ? {} : { segments }),
232
+ target: { kind: "popup-menu", index: item.index },
233
+ });
234
+ }
235
+ if (request?.options.searchable !== false && directQuery) {
236
+ lines.push({ text: ` ${request?.options.placeholder ?? "Search"}: ${directQuery}`, variant: "muted" });
237
+ }
238
+ return lines;
239
+ }
240
+ hasPopupActionItems(items) {
241
+ return items.length > 0;
242
+ }
243
+ labelDescriptionText(label, description, width, labelColumn = SLASH_COMMAND_DESCRIPTION_COLUMN) {
244
+ const safeLabel = sanitizeText(label).replace(/\s+/gu, " ");
245
+ const safeDescription = description ? sanitizeText(description).replace(/\s+/gu, " ") : "";
246
+ if (!safeDescription)
247
+ return ellipsizeDisplay(safeLabel, width);
248
+ const gapWidth = stringDisplayWidth(POPUP_MENU_DESCRIPTION_GAP);
249
+ const labelDisplayWidth = stringDisplayWidth(safeLabel);
250
+ const descriptionDisplayWidth = stringDisplayWidth(safeDescription);
251
+ if (width <= gapWidth + 1)
252
+ return ellipsizeDisplay(safeLabel, width);
253
+ if (labelDisplayWidth <= labelColumn && labelColumn + gapWidth + descriptionDisplayWidth <= width) {
254
+ return `${safeLabel}${" ".repeat(labelColumn - labelDisplayWidth)}${POPUP_MENU_DESCRIPTION_GAP}${safeDescription}`;
255
+ }
256
+ if (labelDisplayWidth + gapWidth + descriptionDisplayWidth <= width) {
257
+ return `${safeLabel}${POPUP_MENU_DESCRIPTION_GAP}${safeDescription}`;
258
+ }
259
+ const labelWidth = descriptionDisplayWidth < width - gapWidth - 1
260
+ ? Math.max(1, width - gapWidth - descriptionDisplayWidth)
261
+ : Math.max(1, Math.min(labelColumn, width - gapWidth - 1));
262
+ const visibleLabel = ellipsizeDisplay(safeLabel, labelWidth);
263
+ const padding = " ".repeat(Math.max(0, labelWidth - stringDisplayWidth(visibleLabel)));
264
+ return `${visibleLabel}${padding}${POPUP_MENU_DESCRIPTION_GAP}${safeDescription}`;
265
+ }
266
+ userMessageActionForeground(value) {
267
+ if (value === "undo")
268
+ return this.host.theme.colors.error;
269
+ return this.host.theme.colors.inputForeground;
270
+ }
271
+ selectableItemVariant(value) {
272
+ return value.current ? "muted" : "normal";
273
+ }
274
+ thinkingMenuItemSegments(value) {
275
+ const markerOffset = 2; // "▶ " or " "
276
+ return [{
277
+ start: markerOffset,
278
+ end: markerOffset + value.level.length,
279
+ foreground: thinkingLevelThemeColor(value.level, this.host.theme.colors, this.availableThinkingLevels()),
280
+ }];
281
+ }
282
+ modelMenuItemSegments(value) {
283
+ const markerOffset = 2; // "▶ " or " "
284
+ return [{
285
+ start: markerOffset,
286
+ end: markerOffset + value.ref.length,
287
+ foreground: this.modelMenuItemColor(value),
288
+ }];
289
+ }
290
+ modelMenuItemColor(value) {
291
+ const configuredColor = this.host.modelColors
292
+ ? resolveModelColor(value.ref, this.host.modelColors)
293
+ : undefined;
294
+ return configuredColor
295
+ ? resolveColor(configuredColor, this.host.theme.colors)
296
+ : modelProviderThemeColor(value.model.provider, this.host.theme.colors);
297
+ }
298
+ availableThinkingLevels() {
299
+ const levels = this.host.session?.getAvailableThinkingLevels();
300
+ return Array.isArray(levels) && levels.length > 0 ? levels.map(String) : ["off", "minimal", "low", "medium", "high", "xhigh"];
301
+ }
302
+ queueMessageItemVariant(value) {
303
+ return value === "cancel" ? "error" : "normal";
304
+ }
305
+ sdkItemVariant(value) {
306
+ return value.variant ?? "normal";
307
+ }
308
+ sdkMenuItemSegments(item, text) {
309
+ return [
310
+ ...this.highlightSegments(item.labelHighlightRanges ?? item.value.labelHighlightRanges ?? [], text, 2),
311
+ ...this.descriptionHighlightSegments(item.description, item.descriptionHighlightRanges ?? item.value.descriptionHighlightRanges ?? [], text),
312
+ ];
313
+ }
314
+ itemHighlightSegments(item, text) {
315
+ return this.highlightSegments(item.labelHighlightRanges ?? [], text, 2);
316
+ }
317
+ highlightSegments(ranges, text, markerOffset) {
318
+ if (ranges.length === 0)
319
+ return [];
320
+ return ranges.flatMap((range) => {
321
+ const start = Math.max(markerOffset, markerOffset + range.start);
322
+ const end = Math.min(text.length, markerOffset + range.end);
323
+ if (end <= start)
324
+ return [];
325
+ return [{
326
+ start,
327
+ end,
328
+ foreground: this.host.theme.colors.accent,
329
+ bold: true,
330
+ }];
331
+ });
332
+ }
333
+ descriptionHighlightSegments(description, ranges, text) {
334
+ if (!description || ranges.length === 0)
335
+ return [];
336
+ const safeDescription = sanitizeText(description).replace(/\s+/gu, " ");
337
+ const descriptionStart = text.indexOf(safeDescription, 2);
338
+ if (descriptionStart < 0)
339
+ return [];
340
+ return this.highlightSegments(ranges, text, descriptionStart);
341
+ }
342
+ resumeMenuItemSegments(value, label, description, text) {
343
+ if (value.kind !== "session")
344
+ return undefined;
345
+ const sessionLabel = value.session.name ?? value.session.firstMessage.slice(0, 50);
346
+ const markerOffset = 2; // "▶ " or " "
347
+ const sessionLabelStart = Math.max(0, label.length - sessionLabel.length) + markerOffset;
348
+ const muted = this.host.theme.colors.popupMuted;
349
+ const segments = [];
350
+ if (sessionLabelStart > markerOffset)
351
+ segments.push({ start: markerOffset, end: sessionLabelStart, foreground: muted });
352
+ if (description.length > 0)
353
+ segments.push({ start: markerOffset + label.length, end: text.length, foreground: muted });
354
+ return segments.length > 0 ? segments : undefined;
355
+ }
356
+ popupMenuHeader(title, width) {
357
+ return {
358
+ text: formatPopupMenuHeader(title, width),
359
+ variant: "accent",
360
+ backgroundOverride: this.host.theme.colors.popupHeaderBackground,
361
+ target: { kind: "popup-menu-close" },
362
+ };
363
+ }
364
+ popupLineForeground(line) {
365
+ const colors = this.host.theme.colors;
366
+ if (line.colorOverride)
367
+ return line.colorOverride;
368
+ switch (line.variant) {
369
+ case "accent":
370
+ return colors.accent;
371
+ case "muted":
372
+ return colors.popupMuted;
373
+ case "error":
374
+ return colors.error;
375
+ case "normal":
376
+ case undefined:
377
+ return colors.popupForeground;
378
+ }
379
+ return colors.popupForeground;
380
+ }
381
+ popupLineBackground(line, selected) {
382
+ const colors = this.host.theme.colors;
383
+ if (selected)
384
+ return colors.popupSelectedBackground;
385
+ return line.backgroundOverride ?? colors.popupBackground;
386
+ }
387
+ }
388
+ export function formatPopupMenuHeader(title, width) {
389
+ const safeWidth = Math.max(1, width);
390
+ const sanitizedTitle = sanitizeText(title).replace(/\s+/g, " ").trim() || "Menu";
391
+ const buttonWidth = stringDisplayWidth(POPUP_MENU_ESCAPE_BUTTON);
392
+ if (safeWidth <= buttonWidth + 1)
393
+ return padOrTrimPlain(POPUP_MENU_ESCAPE_BUTTON, safeWidth);
394
+ const sidePadding = safeWidth >= buttonWidth + POPUP_MENU_HEADER_SIDE_PADDING * 2 + 2
395
+ ? POPUP_MENU_HEADER_SIDE_PADDING
396
+ : 1;
397
+ const contentWidth = Math.max(1, safeWidth - sidePadding * 2);
398
+ if (contentWidth <= buttonWidth + 1) {
399
+ return padOrTrimPlain(`${" ".repeat(sidePadding)}${POPUP_MENU_ESCAPE_BUTTON}`, safeWidth);
400
+ }
401
+ const titleWidth = contentWidth - buttonWidth - 1;
402
+ const titleText = ellipsizeDisplay(sanitizedTitle, titleWidth);
403
+ const gapWidth = Math.max(1, contentWidth - stringDisplayWidth(titleText) - buttonWidth);
404
+ return `${" ".repeat(sidePadding)}${titleText}${" ".repeat(gapWidth)}${POPUP_MENU_ESCAPE_BUTTON}${" ".repeat(sidePadding)}`;
405
+ }
@@ -57,7 +57,6 @@ export class AppRenderController {
57
57
  const underTabsOverlayStartRow = Math.min(rows, topReservedRows + 1);
58
58
  const underTabsOverlayLines = menuLines.slice(0, Math.max(0, statusRow - underTabsOverlayStartRow));
59
59
  const { lines: visible, metrics: scrollMetrics } = this.deps.scrollController.conversationView(columns, bodyHeight);
60
- const scrollBar = this.deps.scrollController.scrollBarForMetrics(scrollMetrics);
61
60
  const conversationColumns = Math.max(1, Math.min(columns, scrollMetrics.viewportColumns));
62
61
  this.deps.mouseController.syncConversationSelectionForRender(scrollMetrics.start, bodyHeight, topReservedRows, conversationColumns);
63
62
  this.deps.mouseController.renderedTargets.clear();
@@ -130,17 +129,6 @@ export class AppRenderController {
130
129
  setRenderedBackground(row, rendered?.backgroundOverride);
131
130
  appendFrameOutput("conversation", row, this.renderFrameRow(row, this.deps.screenStyler.styleBaseLine(row, rendered, conversationColumns)));
132
131
  }
133
- if (scrollBar && columns > 0) {
134
- for (let layoutRow = 1; layoutRow <= bodyHeight; layoutRow += 1) {
135
- const row = toScreenRow(layoutRow);
136
- const isThumb = layoutRow >= scrollBar.thumbStartRow && layoutRow <= scrollBar.thumbEndRow;
137
- const marker = isThumb ? " " : "│";
138
- appendFrameOutput("conversation", row, `\x1b[${row};${columns}H${colorize(marker, {
139
- foreground: this.deps.theme.colors.inputBorder,
140
- ...(isThumb ? { background: this.deps.theme.colors.inputBorder } : {}),
141
- })}`);
142
- }
143
- }
144
132
  const aboveEditorStartRow = inputSeparatorRow + 1;
145
133
  for (let index = 0; index < aboveEditorLines.length; index += 1) {
146
134
  const rendered = frameRenderedLine(aboveEditorLines[index], columns, this.deps.theme, this.deps.screenStyler);
@@ -201,18 +189,26 @@ export class AppRenderController {
201
189
  setRenderedBackground(row, rendered.line?.backgroundOverride);
202
190
  appendFrameOutput("inputStatus", row, this.renderFrameRow(row, rendered.output(row)));
203
191
  }
192
+ const statusLayout = this.deps.statusLineRenderer.layout(columns);
193
+ const statusLineRenderer = this.deps.statusLineRenderer;
194
+ const inputBorderWidgetsLayout = statusLineRenderer.inputBorderWidgetsLayout?.(columns);
204
195
  if (inputBottomSeparatorRow > 1) {
205
196
  const separatorText = inputFrameLine(columns, "bottom");
206
197
  const row = toScreenRow(inputBottomSeparatorRow);
207
198
  if (row < statusRow) {
208
- this.deps.mouseController.renderedRowTexts.set(row, separatorText);
209
- appendFrameOutput("inputStatus", row, this.renderFrameRow(row, this.deps.screenStyler.styleLine(row, separatorText, columns, {
210
- foreground: this.deps.theme.colors.inputBorder,
211
- })));
199
+ const text = inputBorderWidgetsLayout
200
+ ? overlayText(separatorText, inputBorderWidgetsLayout.inputBorderWidgetStartColumn ?? 1, inputBorderWidgetsLayout.text)
201
+ : separatorText;
202
+ this.deps.mouseController.renderedRowTexts.set(row, text);
203
+ const output = inputBorderWidgetsLayout && statusLineRenderer.renderInputBorderWidgets
204
+ ? statusLineRenderer.renderInputBorderWidgets(row, inputBorderWidgetsLayout, separatorText, columns)
205
+ : this.deps.screenStyler.styleLine(row, separatorText, columns, {
206
+ foreground: this.deps.theme.colors.inputBorder,
207
+ });
208
+ appendFrameOutput("inputStatus", row, this.renderFrameRow(row, output));
212
209
  }
213
210
  }
214
- const statusLayout = this.deps.statusLineRenderer.layout(columns);
215
- this.updateStatusMouseState(statusLayout, statusRow);
211
+ this.updateStatusMouseState(statusLayout, statusRow, inputBorderWidgetsLayout, toScreenRow(inputBottomSeparatorRow));
216
212
  appendFrameOutput("inputStatus", statusRow, this.renderFrameRow(statusRow, this.deps.statusLineRenderer.render(statusRow, statusLayout, columns)));
217
213
  const voiceProgressOverlay = this.renderVoiceProgressOverlay(this.deps.voiceProgressOverlayText(), columns, statusRow);
218
214
  if (voiceProgressOverlay) {
@@ -245,7 +241,7 @@ export class AppRenderController {
245
241
  appendFrameOutput(regionForOverlayRow(row), row, this.renderFrameRow(row, this.deps.popupMenus.styleOverlayLine(row, line ?? { text: "" }, columns)));
246
242
  }
247
243
  }
248
- for (const toastOverlay of renderToastOverlays(this.deps.toastController.toast.visibleStates, columns, Math.max(0, statusRow - topReservedRows - 1), this.deps.theme)) {
244
+ for (const toastOverlay of renderToastOverlays(visibleToastStates(this.deps.toastController), columns, Math.max(0, statusRow - topReservedRows - 1), this.deps.theme)) {
249
245
  const row = topReservedRows + toastOverlay.row;
250
246
  const rowText = this.deps.mouseController.renderedRowTexts.get(row) ?? "";
251
247
  if (toastOverlay.target)
@@ -290,20 +286,22 @@ export class AppRenderController {
290
286
  const text = fixedCellText(flash.text, width);
291
287
  return `\x1b[${flash.y};${flash.startColumn}H\x1b[7m${text}${ANSI_RESET}`;
292
288
  }
293
- updateStatusMouseState(statusLayout, statusRow) {
289
+ updateStatusMouseState(statusLayout, statusRow, inputBorderWidgetsLayout, inputBorderWidgetsRow) {
290
+ const widgetLayout = inputBorderWidgetsLayout;
291
+ const widgetRow = inputBorderWidgetsRow ?? statusRow;
294
292
  this.deps.mouseController.statusModelTarget = this.deps.statusLineRenderer.modelTarget(statusLayout.text, statusRow);
295
293
  this.deps.mouseController.statusThinkingTarget = this.deps.statusLineRenderer.thinkingTarget(statusLayout.text, statusRow);
296
294
  this.deps.mouseController.statusContextTarget = this.deps.statusLineRenderer.contextTarget(statusLayout.text, statusRow, statusLayout);
297
295
  this.deps.mouseController.statusModelUsageTarget = this.deps.statusLineRenderer.modelUsageTarget(statusLayout.text, statusRow, statusLayout);
298
- this.deps.mouseController.statusDraftQueueTarget = this.deps.statusLineRenderer.draftQueueTarget?.(statusLayout, statusRow);
299
- this.deps.mouseController.statusUserJumpTarget = this.deps.statusLineRenderer.userJumpTarget?.(statusLayout, statusRow);
300
- this.deps.mouseController.statusThinkingExpandTarget = this.deps.statusLineRenderer.thinkingExpandTarget?.(statusLayout, statusRow);
301
- this.deps.mouseController.statusCompactToolsTarget = this.deps.statusLineRenderer.compactToolsTarget?.(statusLayout, statusRow);
302
- this.deps.mouseController.statusTerminalBellSoundTarget = this.deps.statusLineRenderer.terminalBellSoundTarget?.(statusLayout, statusRow);
296
+ this.deps.mouseController.statusDraftQueueTarget = widgetLayout ? this.deps.statusLineRenderer.draftQueueTarget?.(widgetLayout, widgetRow) : undefined;
297
+ this.deps.mouseController.statusUserJumpTarget = widgetLayout ? this.deps.statusLineRenderer.userJumpTarget?.(widgetLayout, widgetRow) : undefined;
298
+ this.deps.mouseController.statusThinkingExpandTarget = widgetLayout ? this.deps.statusLineRenderer.thinkingExpandTarget?.(widgetLayout, widgetRow) : undefined;
299
+ this.deps.mouseController.statusCompactToolsTarget = widgetLayout ? this.deps.statusLineRenderer.compactToolsTarget?.(widgetLayout, widgetRow) : undefined;
300
+ this.deps.mouseController.statusTerminalBellSoundTarget = widgetLayout ? this.deps.statusLineRenderer.terminalBellSoundTarget?.(widgetLayout, widgetRow) : undefined;
303
301
  this.deps.mouseController.statusSessionTarget = this.deps.statusLineRenderer.sessionTarget(statusLayout.text, statusRow, statusLayout.sessionLabel, statusLayout.workspaceLabel);
304
- this.deps.mouseController.statusPromptEnhancerTarget = this.deps.statusLineRenderer.promptEnhancerTarget(statusLayout, statusRow);
305
- this.deps.mouseController.statusVoiceMicTarget = this.deps.statusLineRenderer.voiceMicTarget(statusLayout, statusRow);
306
- this.deps.mouseController.statusVoiceLanguageTarget = this.deps.statusLineRenderer.voiceLanguageTarget(statusLayout, statusRow);
302
+ this.deps.mouseController.statusPromptEnhancerTarget = widgetLayout ? this.deps.statusLineRenderer.promptEnhancerTarget(widgetLayout, widgetRow) : undefined;
303
+ this.deps.mouseController.statusVoiceMicTarget = widgetLayout ? this.deps.statusLineRenderer.voiceMicTarget(widgetLayout, widgetRow) : undefined;
304
+ this.deps.mouseController.statusVoiceLanguageTarget = widgetLayout ? this.deps.statusLineRenderer.voiceLanguageTarget(widgetLayout, widgetRow) : undefined;
307
305
  this.deps.mouseController.renderedRowTexts.set(statusRow, statusLayout.text);
308
306
  }
309
307
  renderVoiceProgressOverlay(message, width, rows) {
@@ -326,6 +324,10 @@ export class AppRenderController {
326
324
  return { row: Math.min(2, rows - 1), text, output };
327
325
  }
328
326
  }
327
+ function visibleToastStates(toastController) {
328
+ const candidate = toastController;
329
+ return typeof candidate.visibleStates === "function" ? candidate.visibleStates() : candidate.toast?.visibleStates ?? [];
330
+ }
329
331
  function inputFrameLine(width, edge) {
330
332
  if (width <= 0)
331
333
  return "";
@@ -21,9 +21,12 @@ export function shortHash(text) {
21
21
  export function hasLspDiagnosticsAfterMutation(output) {
22
22
  return /lsp\s+(?:errors?|warnings?|diagnostics?)\s+after\s+mutation/i.test(output) || /lsp\s+diagnostics\s*:/i.test(output);
23
23
  }
24
- const LSP_DIAGNOSTIC_MUTATION_TOOLS = new Set(["apply_patch", "ast_apply"]);
24
+ const LSP_DIAGNOSTIC_MUTATION_TOOLS = new Set(["apply_patch", "ast_apply", "edit", "write"]);
25
25
  export function hasToolLspDiagnosticsAfterMutation(entry) {
26
- return LSP_DIAGNOSTIC_MUTATION_TOOLS.has(entry.toolName.toLowerCase()) && hasLspDiagnosticsAfterMutation(entry.output);
26
+ return LSP_DIAGNOSTIC_MUTATION_TOOLS.has(normalizedToolName(entry.toolName)) && hasLspDiagnosticsAfterMutation(entry.output);
27
+ }
28
+ function normalizedToolName(toolName) {
29
+ return toolName.split(/[.:/]/).filter(Boolean).at(-1)?.trim().toLowerCase() ?? toolName.toLowerCase();
27
30
  }
28
31
  export function lspDiagnosticSeverityForLine(line) {
29
32
  const counts = lspDiagnosticCounts(line);
@@ -35,7 +35,10 @@ export declare class StatusLineRenderer {
35
35
  private readonly host;
36
36
  constructor(host: StatusLineRendererHost);
37
37
  layout(width: number): StatusLineLayout;
38
+ inputBorderWidgetsLayout(width: number): StatusLineLayout | undefined;
38
39
  render(row: number, layout: StatusLineLayout, width: number): string;
40
+ renderInputBorderWidgets(row: number, layout: StatusLineLayout, borderText: string, width: number): string;
41
+ private inputBorderWidgetSegments;
39
42
  modelTarget(statusText: string, row: number): StatusModelTarget | undefined;
40
43
  thinkingTarget(statusText: string, row: number): StatusThinkingTarget | undefined;
41
44
  contextTarget(statusText: string, row: number, layout: StatusLineLayout): StatusContextTarget | undefined;
@@ -66,11 +69,15 @@ export declare class StatusLineRenderer {
66
69
  private modelUsageProgressColor;
67
70
  private modelProviderColor;
68
71
  private thinkingLevelColor;
72
+ private statusThinkingDisplayLabel;
69
73
  private availableThinkingLevels;
70
- private thinkingRankColor;
71
74
  private contextBarLabel;
72
75
  private widgetLayout;
76
+ private iconButtonText;
77
+ private voiceBorderWidgetText;
73
78
  private voiceWidgetLayout;
79
+ private voiceBorderWidgetParts;
74
80
  private statusDotColor;
75
81
  }
82
+ export declare function thinkingLevelThemeColor(label: string, colors: Theme["colors"], availableLevels?: readonly string[]): string;
76
83
  export declare function modelProviderThemeColor(provider: string, colors: Theme["colors"]): string;