pi-ui-extend 0.1.13 → 0.1.15
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +1 -1
- package/dist/app/app.d.ts +5 -0
- package/dist/app/app.js +82 -12
- package/dist/app/commands/command-controller.js +1 -0
- package/dist/app/commands/command-host.d.ts +3 -0
- package/dist/app/commands/command-model-actions.d.ts +2 -0
- package/dist/app/commands/command-model-actions.js +40 -4
- package/dist/app/commands/command-navigation-actions.js +3 -0
- package/dist/app/commands/command-registry.d.ts +1 -0
- package/dist/app/commands/command-registry.js +8 -0
- package/dist/app/extensions/extension-ui-controller.d.ts +16 -5
- package/dist/app/extensions/extension-ui-controller.js +99 -61
- package/dist/app/input/input-action-controller.d.ts +1 -0
- package/dist/app/input/input-action-controller.js +8 -2
- package/dist/app/logger.d.ts +25 -0
- package/dist/app/logger.js +90 -0
- package/dist/app/model/model-usage-status.js +30 -15
- package/dist/app/popup/menu-items-controller.d.ts +2 -0
- package/dist/app/popup/menu-items-controller.js +45 -6
- package/dist/app/popup/popup-action-controller.d.ts +2 -1
- package/dist/app/popup/popup-action-controller.js +7 -4
- package/dist/app/popup/popup-menu-controller.d.ts +36 -23
- package/dist/app/popup/popup-menu-controller.js +68 -322
- package/dist/app/rendering/conversation-entry-renderer.js +3 -3
- package/dist/app/rendering/conversation-viewport.d.ts +10 -2
- package/dist/app/rendering/conversation-viewport.js +157 -16
- package/dist/app/rendering/editor-panels.js +4 -2
- package/dist/app/rendering/popup-menu-renderer.d.ts +50 -0
- package/dist/app/rendering/popup-menu-renderer.js +307 -0
- package/dist/app/rendering/render-controller.js +5 -13
- package/dist/app/rendering/status-line-renderer.d.ts +1 -1
- package/dist/app/rendering/status-line-renderer.js +27 -24
- package/dist/app/rendering/toast-controller.d.ts +11 -3
- package/dist/app/rendering/toast-controller.js +53 -12
- package/dist/app/runtime.d.ts +2 -1
- package/dist/app/runtime.js +20 -10
- package/dist/app/screen/mouse-controller.d.ts +2 -2
- package/dist/app/screen/mouse-controller.js +27 -48
- package/dist/app/screen/screen-styler.d.ts +1 -1
- package/dist/app/screen/screen-styler.js +9 -7
- package/dist/app/screen/scroll-controller.d.ts +11 -9
- package/dist/app/screen/scroll-controller.js +50 -45
- package/dist/app/session/lazy-session-manager.d.ts +11 -0
- package/dist/app/session/lazy-session-manager.js +539 -0
- package/dist/app/session/pix-system-message.d.ts +16 -0
- package/dist/app/session/pix-system-message.js +64 -0
- package/dist/app/session/session-event-controller.d.ts +11 -0
- package/dist/app/session/session-event-controller.js +58 -2
- package/dist/app/session/session-history.d.ts +18 -0
- package/dist/app/session/session-history.js +72 -3
- package/dist/app/session/session-lifecycle-controller.d.ts +6 -2
- package/dist/app/session/session-lifecycle-controller.js +7 -2
- package/dist/app/session/tabs-controller.d.ts +13 -1
- package/dist/app/session/tabs-controller.js +248 -27
- package/dist/app/todo/todo-model.d.ts +3 -1
- package/dist/app/todo/todo-model.js +14 -2
- package/dist/app/types.d.ts +5 -2
- package/dist/app/workspace/workspace-actions-controller.d.ts +2 -0
- package/dist/app/workspace/workspace-actions-controller.js +12 -0
- package/dist/config.d.ts +5 -1
- package/dist/config.js +73 -25
- package/dist/default-pix-config.js +2 -0
- package/dist/schemas/pi-tools-suite-schema.d.ts +1 -0
- package/dist/schemas/pi-tools-suite-schema.js +1 -0
- package/dist/schemas/pix-schema.d.ts +2 -1
- package/dist/schemas/pix-schema.js +5 -4
- package/dist/terminal-width.d.ts +2 -0
- package/dist/terminal-width.js +64 -3
- package/external/pi-tools-suite/README.md +1 -0
- package/external/pi-tools-suite/src/antigravity-auth/auth-store.ts +12 -3
- package/external/pi-tools-suite/src/antigravity-auth/commands.ts +2 -4
- package/external/pi-tools-suite/src/antigravity-auth/constants.ts +2 -2
- package/external/pi-tools-suite/src/antigravity-auth/index.ts +8 -2
- package/external/pi-tools-suite/src/antigravity-auth/oauth.ts +102 -50
- package/external/pi-tools-suite/src/antigravity-auth/status.ts +81 -2
- package/external/pi-tools-suite/src/antigravity-auth/stream.ts +29 -8
- package/external/pi-tools-suite/src/config.ts +8 -0
- package/external/pi-tools-suite/src/dcp/index.ts +16 -1
- package/external/pi-tools-suite/src/dcp/state.ts +35 -0
- package/external/pi-tools-suite/src/default-pi-tools-suite-config.ts +3 -0
- package/external/pi-tools-suite/src/todo/index.ts +181 -11
- package/external/pi-tools-suite/src/todo/state/state-reducer.ts +23 -10
- package/external/pi-tools-suite/src/todo/todo.ts +10 -5
- package/external/pi-tools-suite/src/todo/tool/response-envelope.ts +33 -6
- package/external/pi-tools-suite/src/todo/tool/types.ts +9 -1
- package/external/pi-tools-suite/src/todo/view/format.ts +2 -1
- package/external/pi-tools-suite/src/tool-descriptions.ts +2 -1
- package/external/pi-tools-suite/src/usage/index.ts +5 -2
- package/external/pi-tools-suite/src/usage/lib/google.ts +6 -13
- package/package.json +1 -1
- package/schemas/pi-tools-suite.json +4 -0
- package/schemas/pix.json +6 -2
|
@@ -1,5 +1,7 @@
|
|
|
1
|
+
import { resolveToolRule } from "../../config.js";
|
|
2
|
+
import { stringDisplayWidth } from "../../terminal-width.js";
|
|
1
3
|
import { renderConversationEntry as renderConversationEntryLines } from "./conversation-entry-renderer.js";
|
|
2
|
-
import { shortHash } from "./render-text.js";
|
|
4
|
+
import { horizontalPaddingLayout, shortHash } from "./render-text.js";
|
|
3
5
|
export class ConversationViewport {
|
|
4
6
|
host;
|
|
5
7
|
blockCachesByWidth = new Map();
|
|
@@ -23,12 +25,24 @@ export class ConversationViewport {
|
|
|
23
25
|
slice(width, start, count) {
|
|
24
26
|
if (count <= 0)
|
|
25
27
|
return [];
|
|
28
|
+
for (let attempt = 0; attempt < 4; attempt += 1) {
|
|
29
|
+
const layout = this.layoutForWidth(width);
|
|
30
|
+
const visible = this.sliceMeasured(layout, width, start, count);
|
|
31
|
+
if (!visible.changed)
|
|
32
|
+
return visible.lines;
|
|
33
|
+
}
|
|
26
34
|
const layout = this.layoutForWidth(width);
|
|
35
|
+
return this.sliceMeasured(layout, width, start, count, { allowLayoutChanges: false }).lines;
|
|
36
|
+
}
|
|
37
|
+
sliceMeasured(layout, width, start, count, options = {}) {
|
|
38
|
+
const allowLayoutChanges = options.allowLayoutChanges !== false;
|
|
27
39
|
const visible = [];
|
|
28
40
|
const end = start + count;
|
|
29
41
|
let entryIndex = this.entryIndexForOffset(layout.offsets, start);
|
|
30
42
|
for (; entryIndex < layout.entries.length; entryIndex += 1) {
|
|
31
43
|
const entry = layout.entries[entryIndex];
|
|
44
|
+
if (allowLayoutChanges && this.ensureEntryMeasured(layout, width, entryIndex))
|
|
45
|
+
return { lines: [], changed: true };
|
|
32
46
|
const block = this.blockForEntry(entry, width);
|
|
33
47
|
const blockLineCount = layout.lineCounts[entryIndex] ?? 0;
|
|
34
48
|
if (blockLineCount === 0)
|
|
@@ -45,7 +59,7 @@ export class ConversationViewport {
|
|
|
45
59
|
if (visible.length >= count)
|
|
46
60
|
break;
|
|
47
61
|
}
|
|
48
|
-
return visible;
|
|
62
|
+
return { lines: visible, changed: false };
|
|
49
63
|
}
|
|
50
64
|
entries() {
|
|
51
65
|
const queued = this.queuedEntries();
|
|
@@ -80,6 +94,8 @@ export class ConversationViewport {
|
|
|
80
94
|
}
|
|
81
95
|
entryBlockPositions(width) {
|
|
82
96
|
const layout = this.layoutForWidth(width);
|
|
97
|
+
for (let index = 0; index < layout.entries.length; index += 1)
|
|
98
|
+
this.ensureEntryMeasured(layout, width, index);
|
|
83
99
|
return layout.entries.map((entry, index) => ({
|
|
84
100
|
entry,
|
|
85
101
|
offset: layout.offsets[index] ?? 0,
|
|
@@ -87,6 +103,21 @@ export class ConversationViewport {
|
|
|
87
103
|
block: this.blockForEntry(entry, width),
|
|
88
104
|
}));
|
|
89
105
|
}
|
|
106
|
+
measuredLineCountForEntries(width, entryIds) {
|
|
107
|
+
if (entryIds.length === 0)
|
|
108
|
+
return 0;
|
|
109
|
+
const layout = this.layoutForWidth(width);
|
|
110
|
+
const indexes = [...new Set(entryIds
|
|
111
|
+
.map((entryId) => layout.positions.get(entryId))
|
|
112
|
+
.filter((index) => index !== undefined))]
|
|
113
|
+
.sort((left, right) => left - right);
|
|
114
|
+
let lineCount = 0;
|
|
115
|
+
for (const index of indexes) {
|
|
116
|
+
this.ensureEntryMeasured(layout, width, index);
|
|
117
|
+
lineCount += layout.lineCounts[index] ?? 0;
|
|
118
|
+
}
|
|
119
|
+
return lineCount;
|
|
120
|
+
}
|
|
90
121
|
queuedEntries() {
|
|
91
122
|
const session = this.host.session;
|
|
92
123
|
const entries = [];
|
|
@@ -130,7 +161,10 @@ export class ConversationViewport {
|
|
|
130
161
|
const allThinkingExpanded = Boolean(this.host.allThinkingExpanded);
|
|
131
162
|
let layout = this.layoutCachesByWidth.get(width);
|
|
132
163
|
if (!layout || this.layoutStructureChanged(layout, entries, queuedSignature, superCompactTools, allThinkingExpanded)) {
|
|
133
|
-
|
|
164
|
+
const previousLayout = layout && layout.queuedSignature === queuedSignature && layout.superCompactTools === superCompactTools && layout.allThinkingExpanded === allThinkingExpanded
|
|
165
|
+
? layout
|
|
166
|
+
: undefined;
|
|
167
|
+
layout = this.buildLayout(entries, width, queuedSignature, superCompactTools, allThinkingExpanded, previousLayout);
|
|
134
168
|
this.layoutCachesByWidth.set(width, layout);
|
|
135
169
|
}
|
|
136
170
|
else {
|
|
@@ -141,22 +175,40 @@ export class ConversationViewport {
|
|
|
141
175
|
}
|
|
142
176
|
return layout;
|
|
143
177
|
}
|
|
144
|
-
buildLayout(entries, width, queuedSignature, superCompactTools, allThinkingExpanded) {
|
|
178
|
+
buildLayout(entries, width, queuedSignature, superCompactTools, allThinkingExpanded, previousLayout) {
|
|
145
179
|
const entryIds = [];
|
|
146
180
|
const lineCounts = [];
|
|
181
|
+
const measuredLineCounts = [];
|
|
147
182
|
const offsets = [];
|
|
148
183
|
const positions = new Map();
|
|
149
184
|
let totalLineCount = 0;
|
|
185
|
+
const estimatedBlockLineCounts = entries.map((entry) => this.estimatedBlockLineCountForEntry(entry, width));
|
|
150
186
|
for (const [index, entry] of entries.entries()) {
|
|
151
187
|
entryIds.push(entry.id);
|
|
152
188
|
positions.set(entry.id, index);
|
|
153
189
|
offsets.push(totalLineCount);
|
|
154
|
-
const
|
|
190
|
+
const previousLineCount = this.previousMeasuredLineCount(previousLayout, entries, index, entry);
|
|
191
|
+
const lineCount = previousLineCount ?? this.lineCountWithGap(entry, estimatedBlockLineCounts[index] ?? 0, this.nextEstimatedVisibleEntry(entries, estimatedBlockLineCounts, index));
|
|
155
192
|
lineCounts.push(lineCount);
|
|
193
|
+
measuredLineCounts.push(previousLineCount !== undefined);
|
|
156
194
|
totalLineCount += lineCount;
|
|
157
195
|
}
|
|
158
196
|
offsets.push(totalLineCount);
|
|
159
|
-
return { entries, entryIds, lineCounts, offsets, positions, dirtyEntryIds: new Set(), totalLineCount, queuedSignature, superCompactTools, allThinkingExpanded };
|
|
197
|
+
return { entries, entryIds, lineCounts, measuredLineCounts, offsets, positions, dirtyEntryIds: new Set(), totalLineCount, queuedSignature, superCompactTools, allThinkingExpanded };
|
|
198
|
+
}
|
|
199
|
+
previousMeasuredLineCount(previousLayout, entries, index, entry) {
|
|
200
|
+
const previousIndex = previousLayout?.positions.get(entry.id);
|
|
201
|
+
if (previousLayout === undefined || previousIndex === undefined)
|
|
202
|
+
return undefined;
|
|
203
|
+
if (previousLayout.measuredLineCounts[previousIndex] !== true)
|
|
204
|
+
return undefined;
|
|
205
|
+
if (previousLayout.dirtyEntryIds.has(entry.id))
|
|
206
|
+
return undefined;
|
|
207
|
+
const previousNextEntryId = previousLayout.entryIds[previousIndex + 1];
|
|
208
|
+
const nextEntryId = entries[index + 1]?.id;
|
|
209
|
+
if (previousNextEntryId !== nextEntryId)
|
|
210
|
+
return undefined;
|
|
211
|
+
return previousLayout.lineCounts[previousIndex];
|
|
160
212
|
}
|
|
161
213
|
layoutStructureChanged(layout, entries, queuedSignature, superCompactTools, allThinkingExpanded) {
|
|
162
214
|
if (layout.entries.length !== entries.length || layout.queuedSignature !== queuedSignature || layout.superCompactTools !== superCompactTools || layout.allThinkingExpanded !== allThinkingExpanded)
|
|
@@ -168,12 +220,17 @@ export class ConversationViewport {
|
|
|
168
220
|
refreshDirtyLayoutEntries(layout, width) {
|
|
169
221
|
if (layout.dirtyEntryIds.size === 0)
|
|
170
222
|
return;
|
|
223
|
+
const indexes = new Set();
|
|
171
224
|
for (const entryId of layout.dirtyEntryIds) {
|
|
172
225
|
const position = layout.positions.get(entryId);
|
|
173
226
|
if (position === undefined)
|
|
174
227
|
continue;
|
|
175
|
-
|
|
228
|
+
indexes.add(position);
|
|
229
|
+
if (position > 0)
|
|
230
|
+
indexes.add(position - 1);
|
|
176
231
|
}
|
|
232
|
+
for (const position of [...indexes].sort((left, right) => left - right))
|
|
233
|
+
this.refreshLayoutEntry(layout, width, position, true);
|
|
177
234
|
layout.dirtyEntryIds.clear();
|
|
178
235
|
}
|
|
179
236
|
blockCacheForWidth(width) {
|
|
@@ -187,30 +244,80 @@ export class ConversationViewport {
|
|
|
187
244
|
refreshDynamicLayoutEntries(layout, width) {
|
|
188
245
|
for (let index = 0; index < layout.entries.length; index += 1) {
|
|
189
246
|
if (this.host.isDynamicConversationBlock(layout.entries[index]))
|
|
190
|
-
this.refreshLayoutEntry(layout, width, index);
|
|
247
|
+
this.refreshLayoutEntry(layout, width, index, true);
|
|
191
248
|
}
|
|
192
249
|
}
|
|
193
|
-
|
|
250
|
+
ensureEntryMeasured(layout, width, index) {
|
|
194
251
|
const entry = layout.entries[index];
|
|
195
252
|
if (!entry)
|
|
196
|
-
return;
|
|
253
|
+
return false;
|
|
254
|
+
if (layout.measuredLineCounts[index] === true && !this.host.isDynamicConversationBlock(entry))
|
|
255
|
+
return false;
|
|
256
|
+
return this.refreshLayoutEntry(layout, width, index, true);
|
|
257
|
+
}
|
|
258
|
+
refreshLayoutEntry(layout, width, index, measure) {
|
|
259
|
+
const entry = layout.entries[index];
|
|
260
|
+
if (!entry)
|
|
261
|
+
return false;
|
|
197
262
|
const previousLineCount = layout.lineCounts[index] ?? 0;
|
|
198
|
-
const nextLineCount =
|
|
263
|
+
const nextLineCount = measure
|
|
264
|
+
? this.measuredLineCountForEntry(entry, layout.entries, index, width)
|
|
265
|
+
: this.estimatedLineCountForEntry(entry, layout.entries, index, width);
|
|
266
|
+
layout.measuredLineCounts[index] = measure;
|
|
199
267
|
if (previousLineCount === nextLineCount)
|
|
200
|
-
return;
|
|
268
|
+
return false;
|
|
201
269
|
const delta = nextLineCount - previousLineCount;
|
|
202
270
|
layout.lineCounts[index] = nextLineCount;
|
|
203
271
|
layout.totalLineCount += delta;
|
|
204
272
|
for (let offsetIndex = index + 1; offsetIndex < layout.offsets.length; offsetIndex += 1) {
|
|
205
273
|
layout.offsets[offsetIndex] = (layout.offsets[offsetIndex] ?? 0) + delta;
|
|
206
274
|
}
|
|
275
|
+
return true;
|
|
207
276
|
}
|
|
208
|
-
|
|
277
|
+
measuredLineCountForEntry(entry, entries, index, width) {
|
|
209
278
|
const block = this.blockForEntry(entry, width);
|
|
210
|
-
|
|
279
|
+
return this.lineCountWithGap(entry, block.lineCount, this.nextVisibleEntry(entries, index, width));
|
|
280
|
+
}
|
|
281
|
+
estimatedLineCountForEntry(entry, entries, index, width) {
|
|
282
|
+
const blockLineCount = this.estimatedBlockLineCountForEntry(entry, width);
|
|
283
|
+
const blockLineCounts = entries.map((candidate) => this.estimatedBlockLineCountForEntry(candidate, width));
|
|
284
|
+
return this.lineCountWithGap(entry, blockLineCount, this.nextEstimatedVisibleEntry(entries, blockLineCounts, index));
|
|
285
|
+
}
|
|
286
|
+
lineCountWithGap(entry, blockLineCount, nextEntry) {
|
|
287
|
+
if (blockLineCount === 0)
|
|
211
288
|
return 0;
|
|
212
|
-
|
|
213
|
-
|
|
289
|
+
return blockLineCount + (this.gapAfterEntry(entry, nextEntry) ? 1 : 0);
|
|
290
|
+
}
|
|
291
|
+
estimatedBlockLineCountForEntry(entry, width) {
|
|
292
|
+
if (width <= 0)
|
|
293
|
+
return 0;
|
|
294
|
+
switch (entry.kind) {
|
|
295
|
+
case "assistant":
|
|
296
|
+
return estimateWrappedLineCount(entry.text, width);
|
|
297
|
+
case "system":
|
|
298
|
+
case "error":
|
|
299
|
+
case "custom":
|
|
300
|
+
case "session-aborted":
|
|
301
|
+
return estimateWrappedLineCount(entry.text, width);
|
|
302
|
+
case "user": {
|
|
303
|
+
const { contentWidth } = horizontalPaddingLayout(width);
|
|
304
|
+
return 2 + estimateWrappedLineCount(entry.text, contentWidth);
|
|
305
|
+
}
|
|
306
|
+
case "queued": {
|
|
307
|
+
const { contentWidth } = horizontalPaddingLayout(width);
|
|
308
|
+
return estimateWrappedLineCount(entry.text, contentWidth);
|
|
309
|
+
}
|
|
310
|
+
case "thinking": {
|
|
311
|
+
const expanded = entry.expanded || this.host.allThinkingExpanded === true;
|
|
312
|
+
return expanded ? 1 + estimateWrappedLineCount(entry.text, Math.max(1, width - 2)) : 1;
|
|
313
|
+
}
|
|
314
|
+
case "shell":
|
|
315
|
+
return estimateToolLikeLineCount("shell", entry.expanded, `${entry.output}\n${entry.status}`, width, this.host.pixConfig, this.host.superCompactTools === true, true);
|
|
316
|
+
case "tool":
|
|
317
|
+
return estimateToolLikeLineCount(entry.toolName, entry.expanded, entry.output, width, this.host.pixConfig, this.host.superCompactTools === true, false);
|
|
318
|
+
default:
|
|
319
|
+
return 1;
|
|
320
|
+
}
|
|
214
321
|
}
|
|
215
322
|
nextVisibleEntry(entries, index, width) {
|
|
216
323
|
for (let nextIndex = index + 1; nextIndex < entries.length; nextIndex += 1) {
|
|
@@ -222,6 +329,16 @@ export class ConversationViewport {
|
|
|
222
329
|
}
|
|
223
330
|
return undefined;
|
|
224
331
|
}
|
|
332
|
+
nextEstimatedVisibleEntry(entries, lineCounts, index) {
|
|
333
|
+
for (let nextIndex = index + 1; nextIndex < entries.length; nextIndex += 1) {
|
|
334
|
+
const nextEntry = entries[nextIndex];
|
|
335
|
+
if (!nextEntry)
|
|
336
|
+
continue;
|
|
337
|
+
if ((lineCounts[nextIndex] ?? 0) > 0)
|
|
338
|
+
return nextEntry;
|
|
339
|
+
}
|
|
340
|
+
return undefined;
|
|
341
|
+
}
|
|
225
342
|
gapAfterEntry(entry, nextEntry) {
|
|
226
343
|
if (!this.host.superCompactTools)
|
|
227
344
|
return true;
|
|
@@ -249,3 +366,27 @@ export class ConversationViewport {
|
|
|
249
366
|
return result;
|
|
250
367
|
}
|
|
251
368
|
}
|
|
369
|
+
function estimateWrappedLineCount(text, width) {
|
|
370
|
+
const safeWidth = Math.max(1, width);
|
|
371
|
+
if (!text)
|
|
372
|
+
return 0;
|
|
373
|
+
let count = 0;
|
|
374
|
+
for (const line of text.split("\n")) {
|
|
375
|
+
const displayWidth = stringDisplayWidth(line);
|
|
376
|
+
count += Math.max(1, Math.ceil(displayWidth / safeWidth));
|
|
377
|
+
}
|
|
378
|
+
return count;
|
|
379
|
+
}
|
|
380
|
+
function estimateToolLikeLineCount(toolName, expanded, output, width, pixConfig, superCompactTools, includeStatusLine) {
|
|
381
|
+
const rule = resolveToolRule(toolName, pixConfig.toolRenderer);
|
|
382
|
+
if (rule.hidden)
|
|
383
|
+
return 0;
|
|
384
|
+
if (expanded)
|
|
385
|
+
return 1 + estimateWrappedLineCount(output, Math.max(1, width - 2));
|
|
386
|
+
if (rule.compactHidden || (rule.defaultExpanded === true && !superCompactTools))
|
|
387
|
+
return 1;
|
|
388
|
+
const bodyLineCount = estimateWrappedLineCount(output, Math.max(1, width - 2));
|
|
389
|
+
const previewLineCount = Math.min(rule.previewLines, bodyLineCount);
|
|
390
|
+
const extraStatusLine = includeStatusLine && output.trimEnd().length === 0 ? 1 : 0;
|
|
391
|
+
return superCompactTools ? 1 : 1 + Math.max(extraStatusLine, previewLineCount);
|
|
392
|
+
}
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { stringDisplayWidth } from "../../terminal-width.js";
|
|
2
2
|
import { SUBAGENTS_WIDGET_MAX_ROWS } from "../constants.js";
|
|
3
3
|
import { ellipsizeDisplay, padOrTrimPlain, wrapLine } from "./render-text.js";
|
|
4
|
+
import { thinkingLevelThemeColor } from "./status-line-renderer.js";
|
|
4
5
|
import { activeSubagentStates, formatElapsedSince, formatSubagentsPanelStats, subagentModelThinkingLabel, subagentRunName, subagentStatusIcon, taskPreviewMap, } from "../subagents/subagents-model.js";
|
|
5
6
|
import { formatTodoPanelStats, formatTodoTaskLine, hasOpenTodoTasks, shiftSegmentsToSlice, todoTaskLineSegments, visibleTodoTaskRows, visibleTodoTasks, } from "../todo/todo-model.js";
|
|
6
7
|
export function renderTodoPanel(details, expanded, width, colors) {
|
|
@@ -18,12 +19,13 @@ export function renderTodoPanel(details, expanded, width, colors) {
|
|
|
18
19
|
const headerText = `todos ${expanded ? "▾" : "▸"}${stats ? ` ${stats}` : ""}`;
|
|
19
20
|
const todoPanelColor = colors.warning;
|
|
20
21
|
const todoMetaColor = colors.muted;
|
|
22
|
+
const todoThinkingColor = (level) => thinkingLevelThemeColor(level, colors);
|
|
21
23
|
if (!expanded) {
|
|
22
24
|
const prefix = `${headerText} — current: `;
|
|
23
25
|
const current = activeTask ? formatTodoTaskLine(activeTask) : "no active todo";
|
|
24
26
|
const collapsedText = `${prefix}${current}`;
|
|
25
27
|
const segments = activeTask
|
|
26
|
-
? todoTaskLineSegments(activeTask, todoMetaColor).map((segment) => ({
|
|
28
|
+
? todoTaskLineSegments(activeTask, todoMetaColor, { thinkingColor: todoThinkingColor }).map((segment) => ({
|
|
27
29
|
...segment,
|
|
28
30
|
start: segment.start + prefix.length,
|
|
29
31
|
end: segment.end + prefix.length,
|
|
@@ -41,7 +43,7 @@ export function renderTodoPanel(details, expanded, width, colors) {
|
|
|
41
43
|
const lines = [];
|
|
42
44
|
for (const { task, depth } of visibleTodoTaskRows(details)) {
|
|
43
45
|
const text = formatTodoTaskLine(task, { depth });
|
|
44
|
-
const segments = todoTaskLineSegments(task, todoMetaColor, { depth });
|
|
46
|
+
const segments = todoTaskLineSegments(task, todoMetaColor, { depth, thinkingColor: todoThinkingColor });
|
|
45
47
|
let start = 0;
|
|
46
48
|
for (const wrapped of wrapLine(text, contentWidth)) {
|
|
47
49
|
lines.push({
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
import { type Theme } from "../../theme.js";
|
|
2
|
+
import type { PopupMenu } from "../../ui.js";
|
|
3
|
+
import type { ScreenStyler } from "../screen/screen-styler.js";
|
|
4
|
+
import type { Entry, ModelMenuValue, PixMenuItem, PixMenuOptions, QueueMessageMenuValue, RenderedLine, ResumeMenuValue, SlashCommand, ThinkingMenuValue, UserMessageJumpMenuValue, UserMessageMenuValue } from "../types.js";
|
|
5
|
+
import type { AgentSession } from "@earendil-works/pi-coding-agent";
|
|
6
|
+
export type PopupMenuRendererHost = {
|
|
7
|
+
readonly theme: Theme;
|
|
8
|
+
readonly screenStyler: ScreenStyler;
|
|
9
|
+
readonly entries: readonly Entry[];
|
|
10
|
+
readonly session: AgentSession | undefined;
|
|
11
|
+
readonly resumeLoading: boolean;
|
|
12
|
+
readonly resumeSessionCount: number;
|
|
13
|
+
};
|
|
14
|
+
export declare class PopupMenuRenderer {
|
|
15
|
+
private readonly host;
|
|
16
|
+
constructor(host: PopupMenuRendererHost);
|
|
17
|
+
popupMenuWidth(columns: number): number;
|
|
18
|
+
popupMenuMargin(columns: number): number;
|
|
19
|
+
effectivePopupMenuWidth(columns: number): number;
|
|
20
|
+
styleOverlayLine(row: number, line: RenderedLine, width: number, activeMenu: PopupMenu<unknown>): string;
|
|
21
|
+
overlayPlainText(line: RenderedLine, width: number): string;
|
|
22
|
+
renderInlineUserMessageMenu(options: {
|
|
23
|
+
userContentWidth: number;
|
|
24
|
+
userContentLeft: number;
|
|
25
|
+
userLine: (text: string, entryId?: string, syntaxHighlight?: RenderedLine["syntaxHighlight"]) => RenderedLine;
|
|
26
|
+
}, menu: PopupMenu<UserMessageMenuValue>): RenderedLine[];
|
|
27
|
+
renderSlashCommandMenu(width: number, menu: PopupMenu<SlashCommand>): RenderedLine[];
|
|
28
|
+
renderModelMenu(width: number, menu: PopupMenu<ModelMenuValue>): RenderedLine[];
|
|
29
|
+
renderThinkingMenu(width: number, menu: PopupMenu<ThinkingMenuValue>): RenderedLine[];
|
|
30
|
+
renderResumeMenu(width: number, menu: PopupMenu<ResumeMenuValue>, state: {
|
|
31
|
+
directQuery: string;
|
|
32
|
+
allSessionsLoaded: boolean;
|
|
33
|
+
loadedSessionCount: number;
|
|
34
|
+
}): RenderedLine[];
|
|
35
|
+
renderUserMessageJumpMenu(width: number, menu: PopupMenu<UserMessageJumpMenuValue>, directQuery: string): RenderedLine[];
|
|
36
|
+
renderQueueMessageMenu(width: number, menu: PopupMenu<QueueMessageMenuValue>): RenderedLine[];
|
|
37
|
+
renderSdkMenu(width: number, menu: PopupMenu<PixMenuItem<unknown>>, request: {
|
|
38
|
+
options: PixMenuOptions;
|
|
39
|
+
} | undefined, directQuery: string): RenderedLine[];
|
|
40
|
+
private hasPopupActionItems;
|
|
41
|
+
private userMessageActionForeground;
|
|
42
|
+
private selectableItemVariant;
|
|
43
|
+
private queueMessageItemVariant;
|
|
44
|
+
private sdkItemVariant;
|
|
45
|
+
private resumeMenuItemSegments;
|
|
46
|
+
private popupMenuHeader;
|
|
47
|
+
private popupLineForeground;
|
|
48
|
+
private popupLineBackground;
|
|
49
|
+
}
|
|
50
|
+
export declare function formatPopupMenuHeader(title: string, width: number): string;
|
|
@@ -0,0 +1,307 @@
|
|
|
1
|
+
import { colorLine } from "../../theme.js";
|
|
2
|
+
import { stringDisplayWidth } from "../../terminal-width.js";
|
|
3
|
+
import { SLASH_COMMAND_DESCRIPTION_COLUMN, } from "../constants.js";
|
|
4
|
+
import { APP_ICONS } from "../icons.js";
|
|
5
|
+
import { ellipsizeDisplay, padOrTrimPlain, sanitizeText } from "./render-text.js";
|
|
6
|
+
const POPUP_MENU_ESCAPE_BUTTON = "Esc";
|
|
7
|
+
export class PopupMenuRenderer {
|
|
8
|
+
host;
|
|
9
|
+
constructor(host) {
|
|
10
|
+
this.host = host;
|
|
11
|
+
}
|
|
12
|
+
popupMenuWidth(columns) {
|
|
13
|
+
return columns;
|
|
14
|
+
}
|
|
15
|
+
popupMenuMargin(columns) {
|
|
16
|
+
return columns > 44 ? 2 : 0;
|
|
17
|
+
}
|
|
18
|
+
effectivePopupMenuWidth(columns) {
|
|
19
|
+
const sideMargin = this.popupMenuMargin(columns);
|
|
20
|
+
return Math.min(this.popupMenuWidth(columns), Math.max(1, columns - sideMargin * 2));
|
|
21
|
+
}
|
|
22
|
+
styleOverlayLine(row, line, width, activeMenu) {
|
|
23
|
+
const colors = this.host.theme.colors;
|
|
24
|
+
const margin = this.popupMenuMargin(width);
|
|
25
|
+
const menuWidth = this.effectivePopupMenuWidth(width);
|
|
26
|
+
const rightMargin = Math.max(0, width - margin - menuWidth);
|
|
27
|
+
const selected = line.target?.kind === "popup-menu" && activeMenu.selectedIndex === line.target.index;
|
|
28
|
+
const foreground = this.popupLineForeground(line, selected);
|
|
29
|
+
const background = this.popupLineBackground(line, selected);
|
|
30
|
+
const plain = `${" ".repeat(margin)}${padOrTrimPlain(line.text, menuWidth)}${" ".repeat(rightMargin)}`;
|
|
31
|
+
if (this.host.screenStyler.selectionRangeForRow(row, width)) {
|
|
32
|
+
return this.host.screenStyler.styleLine(row, plain, width, { foreground, background });
|
|
33
|
+
}
|
|
34
|
+
return [
|
|
35
|
+
colorLine("", margin, { background: colors.background }),
|
|
36
|
+
line.segments && line.segments.length > 0
|
|
37
|
+
? this.host.screenStyler.styleLineSegments(row, line.text, menuWidth, { foreground, background, bold: selected }, line.segments)
|
|
38
|
+
: colorLine(line.text, menuWidth, { foreground, background, bold: selected }),
|
|
39
|
+
colorLine("", rightMargin, { background: colors.background }),
|
|
40
|
+
].join("");
|
|
41
|
+
}
|
|
42
|
+
overlayPlainText(line, width) {
|
|
43
|
+
const margin = this.popupMenuMargin(width);
|
|
44
|
+
const menuWidth = this.effectivePopupMenuWidth(width);
|
|
45
|
+
const rightMargin = Math.max(0, width - margin - menuWidth);
|
|
46
|
+
return `${" ".repeat(margin)}${padOrTrimPlain(line.text, menuWidth)}${" ".repeat(rightMargin)}`;
|
|
47
|
+
}
|
|
48
|
+
renderInlineUserMessageMenu(options, menu) {
|
|
49
|
+
const headerLine = options.userLine(formatPopupMenuHeader("Message actions", options.userContentWidth));
|
|
50
|
+
headerLine.target = { kind: "popup-menu-close" };
|
|
51
|
+
headerLine.segments = [{
|
|
52
|
+
start: options.userContentLeft,
|
|
53
|
+
end: options.userContentLeft + options.userContentWidth,
|
|
54
|
+
foreground: this.host.theme.colors.accent,
|
|
55
|
+
background: this.host.theme.colors.popupHeaderBackground,
|
|
56
|
+
bold: true,
|
|
57
|
+
}];
|
|
58
|
+
const lines = [headerLine];
|
|
59
|
+
for (const item of menu.visibleItems()) {
|
|
60
|
+
const label = item.label.padEnd(18, " ");
|
|
61
|
+
const description = item.description ?? "";
|
|
62
|
+
const marker = item.selected ? "›" : " ";
|
|
63
|
+
const rawText = `${marker} ${label}${description}`;
|
|
64
|
+
const text = ellipsizeDisplay(rawText, options.userContentWidth);
|
|
65
|
+
const line = options.userLine(text);
|
|
66
|
+
line.target = { kind: "popup-menu", index: item.index };
|
|
67
|
+
const contentStart = options.userContentLeft;
|
|
68
|
+
const labelStart = contentStart + 2;
|
|
69
|
+
const labelEnd = Math.min(contentStart + text.length, labelStart + item.label.length);
|
|
70
|
+
const descriptionStart = contentStart + 2 + label.length;
|
|
71
|
+
line.segments = [
|
|
72
|
+
...(item.selected ? [{ start: contentStart, end: contentStart + 1, foreground: this.host.theme.colors.accent, bold: true }] : []),
|
|
73
|
+
{
|
|
74
|
+
start: labelStart,
|
|
75
|
+
end: labelEnd,
|
|
76
|
+
foreground: this.userMessageActionForeground(item.selected, item.value),
|
|
77
|
+
bold: item.selected,
|
|
78
|
+
},
|
|
79
|
+
...(descriptionStart < contentStart + text.length
|
|
80
|
+
? [{ start: descriptionStart, end: contentStart + text.length, foreground: this.host.theme.colors.muted }]
|
|
81
|
+
: []),
|
|
82
|
+
];
|
|
83
|
+
lines.push(line);
|
|
84
|
+
}
|
|
85
|
+
return lines;
|
|
86
|
+
}
|
|
87
|
+
renderSlashCommandMenu(width, menu) {
|
|
88
|
+
const lines = [this.popupMenuHeader("Commands", width)];
|
|
89
|
+
const visibleItems = menu.visibleItems();
|
|
90
|
+
if (!this.hasPopupActionItems(menu.items)) {
|
|
91
|
+
lines.push({ text: " No matching slash commands", variant: "muted" });
|
|
92
|
+
}
|
|
93
|
+
for (const item of visibleItems) {
|
|
94
|
+
const command = item.label.padEnd(SLASH_COMMAND_DESCRIPTION_COLUMN, " ");
|
|
95
|
+
const description = item.description ?? "";
|
|
96
|
+
lines.push({
|
|
97
|
+
text: `${command}${description}`,
|
|
98
|
+
variant: item.selected ? "accent" : "normal",
|
|
99
|
+
target: { kind: "popup-menu", index: item.index },
|
|
100
|
+
});
|
|
101
|
+
}
|
|
102
|
+
return lines;
|
|
103
|
+
}
|
|
104
|
+
renderModelMenu(width, menu) {
|
|
105
|
+
const lines = [this.popupMenuHeader("Select model", width)];
|
|
106
|
+
const visibleItems = menu.visibleItems();
|
|
107
|
+
if (!this.hasPopupActionItems(menu.items)) {
|
|
108
|
+
lines.push({
|
|
109
|
+
text: this.host.session ? " No matching favorite models" : " Model menu unavailable",
|
|
110
|
+
variant: "muted",
|
|
111
|
+
});
|
|
112
|
+
}
|
|
113
|
+
for (const item of visibleItems) {
|
|
114
|
+
const model = item.label.padEnd(SLASH_COMMAND_DESCRIPTION_COLUMN, " ");
|
|
115
|
+
const description = item.description ?? "";
|
|
116
|
+
lines.push({
|
|
117
|
+
text: `${model}${description}`,
|
|
118
|
+
variant: this.selectableItemVariant(item.selected, item.value),
|
|
119
|
+
target: { kind: "popup-menu", index: item.index },
|
|
120
|
+
});
|
|
121
|
+
}
|
|
122
|
+
return lines;
|
|
123
|
+
}
|
|
124
|
+
renderThinkingMenu(width, menu) {
|
|
125
|
+
const lines = [this.popupMenuHeader("Thinking level", width)];
|
|
126
|
+
const visibleItems = menu.visibleItems();
|
|
127
|
+
if (!this.hasPopupActionItems(menu.items)) {
|
|
128
|
+
lines.push({ text: " No matching thinking levels", variant: "muted" });
|
|
129
|
+
}
|
|
130
|
+
for (const item of visibleItems) {
|
|
131
|
+
const level = item.label.padEnd(SLASH_COMMAND_DESCRIPTION_COLUMN, " ");
|
|
132
|
+
const description = item.description ?? "";
|
|
133
|
+
lines.push({
|
|
134
|
+
text: `${level}${description}`,
|
|
135
|
+
variant: this.selectableItemVariant(item.selected, item.value),
|
|
136
|
+
target: { kind: "popup-menu", index: item.index },
|
|
137
|
+
});
|
|
138
|
+
}
|
|
139
|
+
return lines;
|
|
140
|
+
}
|
|
141
|
+
renderResumeMenu(width, menu, state) {
|
|
142
|
+
const title = this.host.resumeLoading ? `Resume session ${APP_ICONS.timerSand}` : "Resume session";
|
|
143
|
+
const lines = [this.popupMenuHeader(title, width)];
|
|
144
|
+
const visibleItems = menu.visibleItems();
|
|
145
|
+
if (!this.host.resumeLoading && !this.hasPopupActionItems(menu.items)) {
|
|
146
|
+
lines.push({
|
|
147
|
+
text: this.host.resumeSessionCount === 0 ? " No sessions found" : " No matching sessions",
|
|
148
|
+
variant: "muted",
|
|
149
|
+
});
|
|
150
|
+
}
|
|
151
|
+
for (const item of visibleItems) {
|
|
152
|
+
const label = item.label;
|
|
153
|
+
const description = item.description ?? "";
|
|
154
|
+
const text = `${label} ${description}`;
|
|
155
|
+
const segments = this.resumeMenuItemSegments(item.value, label, description, text);
|
|
156
|
+
lines.push({
|
|
157
|
+
text,
|
|
158
|
+
variant: item.selected ? "accent" : "normal",
|
|
159
|
+
...(segments ? { segments } : {}),
|
|
160
|
+
target: { kind: "popup-menu", index: item.index },
|
|
161
|
+
});
|
|
162
|
+
}
|
|
163
|
+
if (!state.allSessionsLoaded && state.loadedSessionCount > 0) {
|
|
164
|
+
lines.push({ text: ` Loaded ${state.loadedSessionCount} sessions · scroll for more`, variant: "muted" });
|
|
165
|
+
}
|
|
166
|
+
if (state.directQuery) {
|
|
167
|
+
lines.push({ text: ` Search: ${state.directQuery}`, variant: "muted" });
|
|
168
|
+
}
|
|
169
|
+
return lines;
|
|
170
|
+
}
|
|
171
|
+
renderUserMessageJumpMenu(width, menu, directQuery) {
|
|
172
|
+
const lines = [this.popupMenuHeader("Jump to user message", width)];
|
|
173
|
+
if (!this.hasPopupActionItems(menu.items)) {
|
|
174
|
+
lines.push({
|
|
175
|
+
text: this.host.entries.some((entry) => entry.kind === "user") ? " No matching user messages" : " No user messages yet",
|
|
176
|
+
variant: "muted",
|
|
177
|
+
});
|
|
178
|
+
}
|
|
179
|
+
const labelWidth = Math.max(1, width);
|
|
180
|
+
for (const item of menu.visibleItems()) {
|
|
181
|
+
const label = ellipsizeDisplay(item.label, labelWidth);
|
|
182
|
+
lines.push({
|
|
183
|
+
text: label,
|
|
184
|
+
variant: item.selected ? "accent" : "normal",
|
|
185
|
+
target: { kind: "popup-menu", index: item.index },
|
|
186
|
+
});
|
|
187
|
+
}
|
|
188
|
+
if (directQuery) {
|
|
189
|
+
lines.push({ text: ` Search: ${directQuery}`, variant: "muted" });
|
|
190
|
+
}
|
|
191
|
+
return lines;
|
|
192
|
+
}
|
|
193
|
+
renderQueueMessageMenu(width, menu) {
|
|
194
|
+
const lines = [this.popupMenuHeader("Queued message", width)];
|
|
195
|
+
for (const item of menu.visibleItems()) {
|
|
196
|
+
const label = item.label.padEnd(18, " ");
|
|
197
|
+
const description = item.description ?? "";
|
|
198
|
+
lines.push({
|
|
199
|
+
text: `${label}${description}`,
|
|
200
|
+
variant: this.queueMessageItemVariant(item.selected, item.value),
|
|
201
|
+
target: { kind: "popup-menu", index: item.index },
|
|
202
|
+
});
|
|
203
|
+
}
|
|
204
|
+
return lines;
|
|
205
|
+
}
|
|
206
|
+
renderSdkMenu(width, menu, request, directQuery) {
|
|
207
|
+
const lines = [this.popupMenuHeader(request?.options.title ?? "Menu", width)];
|
|
208
|
+
if (!this.hasPopupActionItems(menu.items)) {
|
|
209
|
+
lines.push({ text: ` ${request?.options.emptyText ?? "No matching items"}`, variant: "muted" });
|
|
210
|
+
}
|
|
211
|
+
for (const item of menu.visibleItems()) {
|
|
212
|
+
const label = item.label.padEnd(SLASH_COMMAND_DESCRIPTION_COLUMN, " ");
|
|
213
|
+
const description = item.description ?? "";
|
|
214
|
+
lines.push({
|
|
215
|
+
text: `${label}${description}`,
|
|
216
|
+
variant: this.sdkItemVariant(item.selected, item.value),
|
|
217
|
+
target: { kind: "popup-menu", index: item.index },
|
|
218
|
+
});
|
|
219
|
+
}
|
|
220
|
+
if (request?.options.searchable !== false && directQuery) {
|
|
221
|
+
lines.push({ text: ` ${request?.options.placeholder ?? "Search"}: ${directQuery}`, variant: "muted" });
|
|
222
|
+
}
|
|
223
|
+
return lines;
|
|
224
|
+
}
|
|
225
|
+
hasPopupActionItems(items) {
|
|
226
|
+
return items.length > 0;
|
|
227
|
+
}
|
|
228
|
+
userMessageActionForeground(selected, value) {
|
|
229
|
+
if (selected)
|
|
230
|
+
return this.host.theme.colors.accent;
|
|
231
|
+
if (value === "undo")
|
|
232
|
+
return this.host.theme.colors.error;
|
|
233
|
+
return this.host.theme.colors.inputForeground;
|
|
234
|
+
}
|
|
235
|
+
selectableItemVariant(selected, value) {
|
|
236
|
+
if (selected)
|
|
237
|
+
return "accent";
|
|
238
|
+
return value.current ? "muted" : "normal";
|
|
239
|
+
}
|
|
240
|
+
queueMessageItemVariant(selected, value) {
|
|
241
|
+
if (selected)
|
|
242
|
+
return "accent";
|
|
243
|
+
return value === "cancel" ? "error" : "normal";
|
|
244
|
+
}
|
|
245
|
+
sdkItemVariant(selected, value) {
|
|
246
|
+
if (selected)
|
|
247
|
+
return "accent";
|
|
248
|
+
return value.variant ?? "normal";
|
|
249
|
+
}
|
|
250
|
+
resumeMenuItemSegments(value, label, description, text) {
|
|
251
|
+
if (value.kind !== "session")
|
|
252
|
+
return undefined;
|
|
253
|
+
const sessionLabel = value.session.name ?? value.session.firstMessage.slice(0, 50);
|
|
254
|
+
const sessionLabelStart = Math.max(0, label.length - sessionLabel.length);
|
|
255
|
+
const muted = this.host.theme.colors.popupMuted;
|
|
256
|
+
const segments = [];
|
|
257
|
+
if (sessionLabelStart > 0)
|
|
258
|
+
segments.push({ start: 0, end: sessionLabelStart, foreground: muted });
|
|
259
|
+
if (description.length > 0)
|
|
260
|
+
segments.push({ start: label.length, end: text.length, foreground: muted });
|
|
261
|
+
return segments.length > 0 ? segments : undefined;
|
|
262
|
+
}
|
|
263
|
+
popupMenuHeader(title, width) {
|
|
264
|
+
return {
|
|
265
|
+
text: formatPopupMenuHeader(title, width),
|
|
266
|
+
variant: "accent",
|
|
267
|
+
backgroundOverride: this.host.theme.colors.popupHeaderBackground,
|
|
268
|
+
target: { kind: "popup-menu-close" },
|
|
269
|
+
};
|
|
270
|
+
}
|
|
271
|
+
popupLineForeground(line, selected) {
|
|
272
|
+
const colors = this.host.theme.colors;
|
|
273
|
+
if (selected)
|
|
274
|
+
return colors.popupSelectedForeground;
|
|
275
|
+
if (line.colorOverride)
|
|
276
|
+
return line.colorOverride;
|
|
277
|
+
switch (line.variant) {
|
|
278
|
+
case "accent":
|
|
279
|
+
return colors.accent;
|
|
280
|
+
case "muted":
|
|
281
|
+
return colors.popupMuted;
|
|
282
|
+
case "error":
|
|
283
|
+
return colors.error;
|
|
284
|
+
case "normal":
|
|
285
|
+
case undefined:
|
|
286
|
+
return colors.popupForeground;
|
|
287
|
+
}
|
|
288
|
+
return colors.popupForeground;
|
|
289
|
+
}
|
|
290
|
+
popupLineBackground(line, selected) {
|
|
291
|
+
const colors = this.host.theme.colors;
|
|
292
|
+
if (selected)
|
|
293
|
+
return colors.popupSelectedBackground;
|
|
294
|
+
return line.backgroundOverride ?? colors.popupBackground;
|
|
295
|
+
}
|
|
296
|
+
}
|
|
297
|
+
export function formatPopupMenuHeader(title, width) {
|
|
298
|
+
const safeWidth = Math.max(1, width);
|
|
299
|
+
const sanitizedTitle = sanitizeText(title).replace(/\s+/g, " ").trim() || "Menu";
|
|
300
|
+
const buttonWidth = stringDisplayWidth(POPUP_MENU_ESCAPE_BUTTON);
|
|
301
|
+
if (safeWidth <= buttonWidth + 1)
|
|
302
|
+
return padOrTrimPlain(POPUP_MENU_ESCAPE_BUTTON, safeWidth);
|
|
303
|
+
const titleWidth = safeWidth - buttonWidth - 1;
|
|
304
|
+
const titleText = ellipsizeDisplay(sanitizedTitle, titleWidth);
|
|
305
|
+
const gapWidth = Math.max(1, safeWidth - stringDisplayWidth(titleText) - buttonWidth);
|
|
306
|
+
return `${titleText}${" ".repeat(gapWidth)}${POPUP_MENU_ESCAPE_BUTTON}`;
|
|
307
|
+
}
|