@pellux/goodvibes-tui 0.29.0 → 1.1.0
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/CHANGELOG.md +69 -98
- package/README.md +16 -7
- package/docs/foundation-artifacts/operator-contract.json +1 -1
- package/package.json +2 -2
- package/src/cli/ensure-goodvibes-gitignore.ts +32 -0
- package/src/cli/entrypoint.ts +2 -0
- package/src/core/conversation-line-cache.ts +432 -0
- package/src/core/conversation-rendering.ts +11 -3
- package/src/core/conversation.ts +90 -4
- package/src/core/stream-event-wiring.ts +104 -2
- package/src/core/stream-stall-watchdog.ts +41 -17
- package/src/export/cost-utils.ts +90 -8
- package/src/input/command-registry.ts +16 -0
- package/src/input/commands/diff-runtime.ts +61 -30
- package/src/input/commands/session-content.ts +12 -0
- package/src/input/commands/session-workflow.ts +3 -0
- package/src/input/commands/share-runtime.ts +9 -2
- package/src/input/handler-content-actions.ts +30 -11
- package/src/input/handler-feed-routes.ts +63 -1
- package/src/input/handler-feed.ts +12 -0
- package/src/input/handler-interactions.ts +2 -0
- package/src/input/handler-types.ts +1 -0
- package/src/input/handler.ts +4 -0
- package/src/input/input-history.ts +17 -4
- package/src/main.ts +27 -23
- package/src/panels/agent-inspector-shared.ts +33 -10
- package/src/panels/base-panel.ts +1 -1
- package/src/panels/builtin/development.ts +1 -1
- package/src/panels/cockpit-read-model.ts +16 -11
- package/src/panels/cost-tracker-panel.ts +28 -5
- package/src/panels/debug-panel.ts +11 -5
- package/src/panels/diff-panel.ts +122 -22
- package/src/panels/docs-panel.ts +9 -0
- package/src/panels/file-explorer-panel.ts +9 -0
- package/src/panels/git-panel.ts +11 -11
- package/src/panels/knowledge-graph-panel.ts +9 -0
- package/src/panels/local-auth-panel.ts +10 -0
- package/src/panels/panel-list-panel.ts +9 -0
- package/src/panels/project-planning-panel.ts +10 -0
- package/src/panels/provider-health-tracker.ts +5 -1
- package/src/panels/provider-health-views.ts +8 -1
- package/src/panels/scrollable-list-panel.ts +9 -0
- package/src/panels/session-browser-panel.ts +9 -0
- package/src/panels/token-budget-panel.ts +5 -3
- package/src/panels/types.ts +13 -0
- package/src/panels/work-plan-panel.ts +10 -0
- package/src/renderer/agent-detail-modal.ts +42 -12
- package/src/renderer/code-block.ts +58 -28
- package/src/renderer/context-inspector.ts +3 -6
- package/src/renderer/conversation-surface.ts +1 -21
- package/src/renderer/diff-view.ts +6 -4
- package/src/renderer/fullscreen-primitives.ts +1 -1
- package/src/renderer/fullscreen-workspace.ts +2 -7
- package/src/renderer/hint-grammar.ts +1 -1
- package/src/renderer/history-search-overlay.ts +10 -16
- package/src/renderer/markdown.ts +9 -1
- package/src/renderer/model-picker-overlay.ts +8 -499
- package/src/renderer/model-workspace.ts +9 -24
- package/src/renderer/overlay-box.ts +7 -9
- package/src/renderer/process-indicator.ts +13 -25
- package/src/renderer/process-modal.ts +17 -2
- package/src/renderer/profile-picker-modal.ts +13 -14
- package/src/renderer/prompt-content-width.ts +16 -0
- package/src/renderer/selection-modal-overlay.ts +3 -1
- package/src/renderer/semantic-diff.ts +1 -1
- package/src/renderer/settings-modal-helpers.ts +10 -34
- package/src/renderer/shell-surface.ts +29 -9
- package/src/renderer/surface-layout.ts +0 -12
- package/src/renderer/term-caps.ts +1 -1
- package/src/renderer/tool-call.ts +6 -20
- package/src/renderer/ui-factory.ts +98 -14
- package/src/renderer/ui-primitives.ts +18 -1
- package/src/runtime/bootstrap-command-context.ts +3 -0
- package/src/runtime/bootstrap-command-parts.ts +3 -1
- package/src/runtime/bootstrap-core.ts +5 -0
- package/src/runtime/bootstrap-hook-bridge.ts +5 -0
- package/src/runtime/bootstrap-shell.ts +12 -1
- package/src/runtime/render-scheduler.ts +80 -0
- package/src/utils/splash-lines.ts +10 -2
- package/src/version.ts +1 -1
- package/src/renderer/file-tree.ts +0 -153
- package/src/renderer/progress.ts +0 -100
- package/src/renderer/status-token.ts +0 -67
|
@@ -1,504 +1,13 @@
|
|
|
1
|
-
import { type Line } from '../types/grid.ts';
|
|
2
|
-
import { fitDisplay, getDisplayWidth, truncateDisplay } from '../utils/terminal-width.ts';
|
|
3
|
-
import { abbreviateCount } from '../utils/format-number.ts';
|
|
4
|
-
import type { ModelPickerModal } from '../input/model-picker.ts';
|
|
5
|
-
import { EFFORT_DESCRIPTIONS } from '@pellux/goodvibes-sdk/platform/providers';
|
|
6
|
-
import { getQualityTier, getQualityTierFromScore } from '@pellux/goodvibes-sdk/platform/providers';
|
|
7
|
-
import {
|
|
8
|
-
createOverlayBoxLayout,
|
|
9
|
-
createOverlayContentLine,
|
|
10
|
-
createOverlayFilledBorderLine,
|
|
11
|
-
DEFAULT_OVERLAY_PALETTE,
|
|
12
|
-
OVERLAY_GLYPHS,
|
|
13
|
-
putOverlayText,
|
|
14
|
-
} from './overlay-box.ts';
|
|
15
|
-
import { getOverlaySurfaceMetrics } from './overlay-viewport.ts';
|
|
16
|
-
import { formatHints, type HintSpec } from './hint-grammar.ts';
|
|
17
|
-
|
|
18
|
-
/** Format a context window number into a short human-readable string. */
|
|
19
|
-
function fmtContext(n: number): string {
|
|
20
|
-
return abbreviateCount(n, { rounding: 'round', decimals: 0 });
|
|
21
|
-
}
|
|
22
|
-
|
|
23
|
-
/** Title text per picker mode. */
|
|
24
|
-
const MODE_TITLES: Record<string, string> = {
|
|
25
|
-
model: 'Select Model',
|
|
26
|
-
provider: 'Select Provider',
|
|
27
|
-
effort: 'Select Effort Level',
|
|
28
|
-
contextCap: 'Set Context Window',
|
|
29
|
-
};
|
|
30
|
-
|
|
31
1
|
/**
|
|
32
2
|
* Number of fixed chrome lines in the model-picker overlay (title + search + divider + detail×2 + footer).
|
|
33
3
|
* Used by callers to compute maxVisible item rows.
|
|
34
|
-
*/
|
|
35
|
-
export const MODEL_PICKER_CHROME_LINES = 7;
|
|
36
|
-
|
|
37
|
-
const renderCache = new WeakMap<ModelPickerModal, { key: string; lines: Line[] }>();
|
|
38
|
-
const objectIds = new WeakMap<object, number>();
|
|
39
|
-
let nextObjectId = 1;
|
|
40
|
-
|
|
41
|
-
function putRowText(line: Line, startX: number, maxWidth: number, text: string, fg: string, bg = '', bold = false, dim = false): void {
|
|
42
|
-
putOverlayText(line, startX, maxWidth, text, { fg, bg, bold, dim });
|
|
43
|
-
}
|
|
44
|
-
|
|
45
|
-
/**
|
|
46
|
-
* Render the model picker modal as Line[] for overlay in the viewport.
|
|
47
|
-
* Handles model, provider, and effort modes.
|
|
48
4
|
*
|
|
49
|
-
*
|
|
50
|
-
*
|
|
5
|
+
* WO-206: this file used to also hold `renderModelPickerOverlay`, the
|
|
6
|
+
* overlay-style model-picker renderer. It was superseded by
|
|
7
|
+
* `renderModelWorkspace` (model-workspace.ts) — conversation-overlays.ts
|
|
8
|
+
* routes `input.modelPicker.active` there, not here — and had no remaining
|
|
9
|
+
* non-test import site, so it (and its dedicated unit/golden tests) were
|
|
10
|
+
* removed. This constant is still live: handler-picker-routes.ts uses it to
|
|
11
|
+
* size the current workspace's visible-row window.
|
|
51
12
|
*/
|
|
52
|
-
export
|
|
53
|
-
picker: ModelPickerModal,
|
|
54
|
-
width: number,
|
|
55
|
-
maxVisible = 20,
|
|
56
|
-
viewportHeight?: number,
|
|
57
|
-
): Line[] {
|
|
58
|
-
const cacheKey = getRenderCacheKey(picker, width, maxVisible, viewportHeight);
|
|
59
|
-
const cached = renderCache.get(picker);
|
|
60
|
-
if (cached?.key === cacheKey) return cached.lines;
|
|
61
|
-
|
|
62
|
-
const lines: Line[] = [];
|
|
63
|
-
const metrics = getOverlaySurfaceMetrics(width, viewportHeight ?? 24, {
|
|
64
|
-
chromeRows: MODEL_PICKER_CHROME_LINES,
|
|
65
|
-
maxWidth: 72,
|
|
66
|
-
minContentRows: 6,
|
|
67
|
-
maxContentRows: Math.max(10, maxVisible),
|
|
68
|
-
});
|
|
69
|
-
const layout = createOverlayBoxLayout(width, metrics.margin, metrics.boxWidth);
|
|
70
|
-
const contentW = layout.innerWidth;
|
|
71
|
-
const borderFg = DEFAULT_OVERLAY_PALETTE.borderFg;
|
|
72
|
-
const titleFg = DEFAULT_OVERLAY_PALETTE.titleFg;
|
|
73
|
-
const bodyFg = DEFAULT_OVERLAY_PALETTE.bodyFg;
|
|
74
|
-
const mutedFg = DEFAULT_OVERLAY_PALETTE.mutedFg;
|
|
75
|
-
const selectedBg = DEFAULT_OVERLAY_PALETTE.selectedBg;
|
|
76
|
-
|
|
77
|
-
// ── Title bar ───────────────────────────────────────────────────────────────────────
|
|
78
|
-
const titleLine = createOverlayFilledBorderLine(width, layout, OVERLAY_GLYPHS.topLeft, OVERLAY_GLYPHS.horizontal, OVERLAY_GLYPHS.topRight, borderFg, DEFAULT_OVERLAY_PALETTE.titleBg);
|
|
79
|
-
putRowText(
|
|
80
|
-
titleLine,
|
|
81
|
-
layout.margin + 2,
|
|
82
|
-
layout.width - 4,
|
|
83
|
-
truncateDisplay((MODE_TITLES[picker.mode] ?? MODE_TITLES.model).replace(/^─\s*/, '').trim(), layout.width - 4),
|
|
84
|
-
titleFg,
|
|
85
|
-
'',
|
|
86
|
-
true,
|
|
87
|
-
);
|
|
88
|
-
lines.push(titleLine);
|
|
89
|
-
|
|
90
|
-
// ── Search bar (model and provider modes) ────────────────────────────────────
|
|
91
|
-
if (picker.mode === 'model' || picker.mode === 'provider') {
|
|
92
|
-
const searchLine = createOverlayContentLine(width, layout, borderFg, DEFAULT_OVERLAY_PALETTE.inputBg);
|
|
93
|
-
const searchPrefix = '/ ';
|
|
94
|
-
const queryDisplay = picker.query + (picker.searchFocused ? OVERLAY_GLYPHS.cursor : '');
|
|
95
|
-
let filterTag = '';
|
|
96
|
-
let filterTagW = 0;
|
|
97
|
-
if (picker.mode === 'model') {
|
|
98
|
-
// Category filter indicator — model mode only
|
|
99
|
-
const filterLabels: Record<string, string> = { all: 'All', free: 'Free', paid: 'Paid', subscription: 'Sub' };
|
|
100
|
-
const filterLabel = filterLabels[picker.categoryFilter] ?? 'All';
|
|
101
|
-
filterTag = `[${filterLabel}]`;
|
|
102
|
-
filterTagW = getDisplayWidth(filterTag);
|
|
103
|
-
}
|
|
104
|
-
const maxQueryW = contentW - getDisplayWidth(searchPrefix) - filterTagW - (filterTagW > 0 ? 2 : 0);
|
|
105
|
-
const queryTrunc = getDisplayWidth(queryDisplay) > maxQueryW
|
|
106
|
-
? truncateDisplay(queryDisplay, maxQueryW)
|
|
107
|
-
: queryDisplay;
|
|
108
|
-
let rowX = layout.margin + 2;
|
|
109
|
-
putRowText(searchLine, rowX, getDisplayWidth(searchPrefix), searchPrefix, picker.searchFocused ? bodyFg : mutedFg);
|
|
110
|
-
rowX += getDisplayWidth(searchPrefix);
|
|
111
|
-
const queryAreaWidth = filterTag
|
|
112
|
-
? Math.max(0, contentW - getDisplayWidth(searchPrefix) - filterTagW - 1)
|
|
113
|
-
: Math.max(0, contentW - getDisplayWidth(searchPrefix));
|
|
114
|
-
putRowText(searchLine, rowX, queryAreaWidth, fitDisplay(queryTrunc, queryAreaWidth), picker.query.length > 0 || picker.searchFocused ? '#ffffff' : mutedFg);
|
|
115
|
-
if (filterTag) {
|
|
116
|
-
putRowText(
|
|
117
|
-
searchLine,
|
|
118
|
-
layout.margin + 2 + contentW - filterTagW,
|
|
119
|
-
filterTagW,
|
|
120
|
-
filterTag,
|
|
121
|
-
mutedFg,
|
|
122
|
-
);
|
|
123
|
-
}
|
|
124
|
-
lines.push(searchLine);
|
|
125
|
-
|
|
126
|
-
// Thin divider under search bar
|
|
127
|
-
lines.push(createOverlayFilledBorderLine(width, layout, OVERLAY_GLYPHS.teeLeft, OVERLAY_GLYPHS.horizontal, OVERLAY_GLYPHS.teeRight, borderFg, DEFAULT_OVERLAY_PALETTE.sectionBg));
|
|
128
|
-
} else {
|
|
129
|
-
lines.push(createOverlayContentLine(width, layout, borderFg, DEFAULT_OVERLAY_PALETTE.sectionBg));
|
|
130
|
-
}
|
|
131
|
-
|
|
132
|
-
if (picker.mode === 'model') {
|
|
133
|
-
// ── Model list (grouped, with scroll window) ────────────────────────────────────
|
|
134
|
-
const filtered = picker.getFilteredModels();
|
|
135
|
-
if (filtered.length === 0) {
|
|
136
|
-
const msg = picker.query.length > 0
|
|
137
|
-
? `No models match "${picker.query.length > 20 ? picker.query.slice(0, 20) + '...' : picker.query}"`
|
|
138
|
-
: 'No models available';
|
|
139
|
-
const noModels = createOverlayContentLine(width, layout, borderFg, DEFAULT_OVERLAY_PALETTE.bodyBg);
|
|
140
|
-
putRowText(noModels, layout.margin + 2, contentW, fitDisplay(truncateDisplay(msg, contentW), contentW), '244', '', false, true);
|
|
141
|
-
lines.push(noModels);
|
|
142
|
-
} else {
|
|
143
|
-
// Determine the visible slice [scrollOffset, scrollOffset + maxVisible)
|
|
144
|
-
const scrollOffset = Math.max(0, Math.min(picker.scrollOffset, Math.max(0, filtered.length - maxVisible)));
|
|
145
|
-
const visibleEnd = Math.min(filtered.length, scrollOffset + maxVisible);
|
|
146
|
-
const visibleModels = filtered.slice(scrollOffset, visibleEnd);
|
|
147
|
-
|
|
148
|
-
// Scroll indicators
|
|
149
|
-
if (scrollOffset > 0) {
|
|
150
|
-
const upHint = createOverlayContentLine(width, layout, borderFg, DEFAULT_OVERLAY_PALETTE.sectionBg);
|
|
151
|
-
putRowText(upHint, layout.margin + 2, contentW, fitDisplay(`${OVERLAY_GLYPHS.moreAbove} ${scrollOffset} more above`, contentW), mutedFg, '', false, true);
|
|
152
|
-
lines.push(upHint);
|
|
153
|
-
}
|
|
154
|
-
|
|
155
|
-
let lastGroupKey = '';
|
|
156
|
-
// Track the absolute index for group header display
|
|
157
|
-
// Use getModelGroupKey for synthetic sub-group support (Top Models / All Synthetic)
|
|
158
|
-
for (let i = 0; i < visibleModels.length; i++) {
|
|
159
|
-
const model = visibleModels[i];
|
|
160
|
-
const absIdx = scrollOffset + i; // index into filtered[] for selectedIndex comparison
|
|
161
|
-
|
|
162
|
-
// Group header — show when group key changes within the visible window
|
|
163
|
-
// For the first visible item, always check if header is needed
|
|
164
|
-
const groupKey = picker.getModelGroupKey(model);
|
|
165
|
-
if (groupKey !== lastGroupKey) {
|
|
166
|
-
const headerRow = createOverlayContentLine(width, layout, borderFg, DEFAULT_OVERLAY_PALETTE.sectionBg);
|
|
167
|
-
putRowText(headerRow, layout.margin + 2, contentW, fitDisplay(`[${groupKey}]`, contentW), '#4488cc');
|
|
168
|
-
lines.push(headerRow);
|
|
169
|
-
lastGroupKey = groupKey;
|
|
170
|
-
}
|
|
171
|
-
|
|
172
|
-
const isSelected = absIdx === picker.selectedIndex;
|
|
173
|
-
const indicator = isSelected ? `${OVERLAY_GLYPHS.selected} ` : ' ';
|
|
174
|
-
|
|
175
|
-
// Pre-compute synthetic info once per model (avoid 3 separate lookups per frame)
|
|
176
|
-
const synthInfo = model.provider === 'synthetic' ? picker.getSyntheticModelInfo(model.id) : null;
|
|
177
|
-
|
|
178
|
-
// Quality tier badge: [S] / [A] / [B] / [C]
|
|
179
|
-
let tier: string | null = null;
|
|
180
|
-
if (model.provider === 'synthetic') {
|
|
181
|
-
if (synthInfo?.bestCompositeScore != null) {
|
|
182
|
-
tier = getQualityTierFromScore(synthInfo.bestCompositeScore);
|
|
183
|
-
}
|
|
184
|
-
} else {
|
|
185
|
-
const bData = picker.getBenchmarkEntry(model);
|
|
186
|
-
tier = bData ? getQualityTier(bData.benchmarks) : null;
|
|
187
|
-
}
|
|
188
|
-
const tierBadge = tier ? `[${tier}]` : ' ';
|
|
189
|
-
// Pin marker: keep the Unicode star instead of ASCII fallback
|
|
190
|
-
const pinStar = picker.pinnedIds.has(model.id) ? '★ ' : ' ';
|
|
191
|
-
// Free badge: dot marker, not an asterisk
|
|
192
|
-
const freeBadge = model.tier === 'free' ? '•' : ' ';
|
|
193
|
-
// Provider count for synthetic models
|
|
194
|
-
let providerCountStr = ' '; // 5 chars wide (fixed)
|
|
195
|
-
if (synthInfo) {
|
|
196
|
-
const countLabel = `(${synthInfo.keyedBackendCount}p)`;
|
|
197
|
-
providerCountStr = countLabel.padEnd(5);
|
|
198
|
-
}
|
|
199
|
-
|
|
200
|
-
// Layout: indicator(2) + pin(2) + id(maxIdLen) + gap(2) + name(remaining) + provCount(5) + free(1) + tier(3)
|
|
201
|
-
const maxIdLen = 20;
|
|
202
|
-
const provCountW = 5;
|
|
203
|
-
const badgesW = 3 + 1 + 2; // tierBadge(3) + freeBadge(1) + gap(2)
|
|
204
|
-
const idStr = model.id.length > maxIdLen
|
|
205
|
-
? model.id.slice(0, maxIdLen - 3) + '...'
|
|
206
|
-
: model.id.padEnd(maxIdLen);
|
|
207
|
-
const remaining = contentW - maxIdLen - 4 - badgesW - 2 - provCountW; // 4 = indicator+pin, 2 = gap before name
|
|
208
|
-
const nameStr = model.displayName.length > Math.max(0, remaining)
|
|
209
|
-
? model.displayName.slice(0, Math.max(0, remaining) - 3) + '...'
|
|
210
|
-
: model.displayName.padEnd(Math.max(0, remaining));
|
|
211
|
-
|
|
212
|
-
const row = createOverlayContentLine(width, layout, borderFg, isSelected ? selectedBg : DEFAULT_OVERLAY_PALETTE.bodyBg);
|
|
213
|
-
let x = layout.margin + 2;
|
|
214
|
-
const rowText = indicator + pinStar + idStr + ' ' + nameStr + providerCountStr + ' ' + freeBadge + tierBadge;
|
|
215
|
-
putRowText(row, x, contentW, fitDisplay(truncateDisplay(rowText, contentW), contentW), isSelected ? titleFg : bodyFg, isSelected ? selectedBg : DEFAULT_OVERLAY_PALETTE.bodyBg, isSelected);
|
|
216
|
-
lines.push(row);
|
|
217
|
-
}
|
|
218
|
-
|
|
219
|
-
if (visibleEnd < filtered.length) {
|
|
220
|
-
const remaining2 = filtered.length - visibleEnd;
|
|
221
|
-
const downHint = createOverlayContentLine(width, layout, borderFg, DEFAULT_OVERLAY_PALETTE.sectionBg);
|
|
222
|
-
putRowText(downHint, layout.margin + 2, contentW, fitDisplay(`${OVERLAY_GLYPHS.moreBelow} ${remaining2} more below`, contentW), mutedFg, '', false, true);
|
|
223
|
-
lines.push(downHint);
|
|
224
|
-
}
|
|
225
|
-
}
|
|
226
|
-
|
|
227
|
-
// ── Divider ────────────────────────────────────────────────────────────────────
|
|
228
|
-
lines.push(createOverlayFilledBorderLine(width, layout, OVERLAY_GLYPHS.teeLeft, OVERLAY_GLYPHS.horizontal, OVERLAY_GLYPHS.teeRight, borderFg, DEFAULT_OVERLAY_PALETTE.sectionBg));
|
|
229
|
-
|
|
230
|
-
// ── Capability detail for selected model ────────────────────────────────────────────
|
|
231
|
-
const selected = picker.getSelected();
|
|
232
|
-
if (selected) {
|
|
233
|
-
const providerLine = createOverlayContentLine(width, layout, borderFg, DEFAULT_OVERLAY_PALETTE.bodyBg);
|
|
234
|
-
putRowText(providerLine, layout.margin + 2, contentW, fitDisplay(`Provider: ${selected.provider}`, contentW), '244');
|
|
235
|
-
lines.push(providerLine);
|
|
236
|
-
|
|
237
|
-
// Synthetic models show fallback ladder rungs instead of capabilities.
|
|
238
|
-
const syntheticChain = selected.provider === 'synthetic' ? picker.getSyntheticChain(selected.id) : null;
|
|
239
|
-
if (syntheticChain !== null && syntheticChain.length > 0) {
|
|
240
|
-
const rung0 = syntheticChain[0];
|
|
241
|
-
const rest = syntheticChain.length - 1;
|
|
242
|
-
const rung0Base = ` 0. ${rung0.provider}/${rung0.model}`;
|
|
243
|
-
const rung0Label = rest > 0 ? `${rung0Base} (+${rest} more)` : rung0Base;
|
|
244
|
-
const chainLine0 = createOverlayContentLine(width, layout, borderFg, DEFAULT_OVERLAY_PALETTE.bodyBg);
|
|
245
|
-
putRowText(chainLine0, layout.margin + 2, contentW, fitDisplay(truncateDisplay(rung0Label, contentW), contentW), '244');
|
|
246
|
-
lines.push(chainLine0);
|
|
247
|
-
} else {
|
|
248
|
-
const caps = selected.capabilities ?? { reasoning: false, multimodal: false, toolCalling: false, codeEditing: false };
|
|
249
|
-
const ctxStr = `Context: ${fmtContext(selected.contextWindow)}`;
|
|
250
|
-
const capParts: string[] = [ctxStr];
|
|
251
|
-
if (caps.reasoning) capParts.push('Reasoning: \u2713');
|
|
252
|
-
if (caps.multimodal) capParts.push('Vision: \u2713');
|
|
253
|
-
if (caps.toolCalling) capParts.push('Tools: \u2713');
|
|
254
|
-
if (caps.codeEditing) capParts.push('Code: \u2713');
|
|
255
|
-
const capText = capParts.join(' ');
|
|
256
|
-
const capLine = createOverlayContentLine(width, layout, borderFg, DEFAULT_OVERLAY_PALETTE.bodyBg);
|
|
257
|
-
putRowText(capLine, layout.margin + 2, contentW, fitDisplay(truncateDisplay(capText, contentW), contentW), '244');
|
|
258
|
-
lines.push(capLine);
|
|
259
|
-
}
|
|
260
|
-
} else {
|
|
261
|
-
lines.push(createOverlayContentLine(width, layout, borderFg, DEFAULT_OVERLAY_PALETTE.bodyBg));
|
|
262
|
-
lines.push(createOverlayContentLine(width, layout, borderFg, DEFAULT_OVERLAY_PALETTE.bodyBg));
|
|
263
|
-
}
|
|
264
|
-
} else if (picker.mode === 'provider') {
|
|
265
|
-
// ── Provider list (grouped: Popular / All Providers) ───────────────────────────────────
|
|
266
|
-
const allProviderItems = picker.getItems(); // includes group headers
|
|
267
|
-
const selectableCount = picker.getFilteredProviders().length;
|
|
268
|
-
if (selectableCount === 0) {
|
|
269
|
-
const msg = picker.query.length > 0
|
|
270
|
-
? `No providers match "${picker.query.length > 20 ? picker.query.slice(0, 20) + '...' : picker.query}"`
|
|
271
|
-
: 'No providers available';
|
|
272
|
-
const noProviders = createOverlayContentLine(width, layout, borderFg, DEFAULT_OVERLAY_PALETTE.bodyBg);
|
|
273
|
-
putRowText(noProviders, layout.margin + 2, contentW, fitDisplay(truncateDisplay(msg, contentW), contentW), '244', '', false, true);
|
|
274
|
-
lines.push(noProviders);
|
|
275
|
-
} else {
|
|
276
|
-
// Build the flat selectable index → item-list-index mapping for scroll tracking
|
|
277
|
-
// scrollOffset / selectedIndex track selectable items only
|
|
278
|
-
const providerScrollOffset = Math.max(0, Math.min(picker.scrollOffset, Math.max(0, selectableCount - maxVisible)));
|
|
279
|
-
const providerVisibleEnd = Math.min(selectableCount, providerScrollOffset + maxVisible);
|
|
280
|
-
|
|
281
|
-
// Scroll indicator — items above
|
|
282
|
-
if (providerScrollOffset > 0) {
|
|
283
|
-
const upHint = createOverlayContentLine(width, layout, borderFg, DEFAULT_OVERLAY_PALETTE.sectionBg);
|
|
284
|
-
putRowText(upHint, layout.margin + 2, contentW, fitDisplay(`${OVERLAY_GLYPHS.moreAbove} ${providerScrollOffset} more above`, contentW), mutedFg, '', false, true);
|
|
285
|
-
lines.push(upHint);
|
|
286
|
-
}
|
|
287
|
-
|
|
288
|
-
// Walk all provider items (headers + selectables), rendering only selectables
|
|
289
|
-
// in [providerScrollOffset, providerVisibleEnd). Headers are shown when the
|
|
290
|
-
// first selectable item in their group is visible.
|
|
291
|
-
let selectableIdx = -1;
|
|
292
|
-
let pendingHeader: string | null = null;
|
|
293
|
-
|
|
294
|
-
for (const item of allProviderItems) {
|
|
295
|
-
if (item.isGroupHeader) {
|
|
296
|
-
pendingHeader = item.label;
|
|
297
|
-
continue;
|
|
298
|
-
}
|
|
299
|
-
selectableIdx++;
|
|
300
|
-
if (selectableIdx < providerScrollOffset) {
|
|
301
|
-
pendingHeader = null; // group header passed, no longer pending
|
|
302
|
-
continue;
|
|
303
|
-
}
|
|
304
|
-
if (selectableIdx >= providerVisibleEnd) break;
|
|
305
|
-
|
|
306
|
-
// Emit pending group header before first visible item in the group
|
|
307
|
-
if (pendingHeader !== null) {
|
|
308
|
-
const headerRow = createOverlayContentLine(width, layout, borderFg, DEFAULT_OVERLAY_PALETTE.sectionBg);
|
|
309
|
-
putRowText(headerRow, layout.margin + 2, contentW, fitDisplay(`[${pendingHeader}]`, contentW), '#4488cc');
|
|
310
|
-
lines.push(headerRow);
|
|
311
|
-
pendingHeader = null;
|
|
312
|
-
}
|
|
313
|
-
|
|
314
|
-
const isSelected = selectableIdx === picker.selectedIndex;
|
|
315
|
-
const indicator = isSelected ? `${OVERLAY_GLYPHS.selected} ` : ' ';
|
|
316
|
-
const checkmark = item.isConfigured ? '✓ ' : ' ';
|
|
317
|
-
// configuredVia badge: right-aligned short label (env/sub/anon)
|
|
318
|
-
const viaBadge = item.configuredVia === 'env' ? ' [env]'
|
|
319
|
-
: item.configuredVia === 'secrets' ? ' [key]'
|
|
320
|
-
: item.configuredVia === 'subscription' ? ' [sub]'
|
|
321
|
-
: item.configuredVia === 'anonymous' ? ' [anon]'
|
|
322
|
-
: '';
|
|
323
|
-
const badgeW = viaBadge.length;
|
|
324
|
-
const labelW = contentW - 2 - 2 - badgeW; // indicator(2) + checkmark(2) + badge
|
|
325
|
-
const labelStr = item.label.length > labelW
|
|
326
|
-
? item.label.slice(0, labelW - 3) + '...'
|
|
327
|
-
: item.label.padEnd(labelW);
|
|
328
|
-
const row = createOverlayContentLine(width, layout, borderFg, isSelected ? selectedBg : DEFAULT_OVERLAY_PALETTE.bodyBg);
|
|
329
|
-
const rowText = indicator + checkmark + labelStr + viaBadge;
|
|
330
|
-
putRowText(row, layout.margin + 2, contentW, fitDisplay(truncateDisplay(rowText, contentW), contentW), isSelected ? titleFg : bodyFg, isSelected ? selectedBg : DEFAULT_OVERLAY_PALETTE.bodyBg, isSelected);
|
|
331
|
-
lines.push(row);
|
|
332
|
-
}
|
|
333
|
-
|
|
334
|
-
// Scroll indicator — items below
|
|
335
|
-
if (providerVisibleEnd < selectableCount) {
|
|
336
|
-
const remaining2 = selectableCount - providerVisibleEnd;
|
|
337
|
-
const downHint = createOverlayContentLine(width, layout, borderFg, DEFAULT_OVERLAY_PALETTE.sectionBg);
|
|
338
|
-
putRowText(downHint, layout.margin + 2, contentW, fitDisplay(`${OVERLAY_GLYPHS.moreBelow} ${remaining2} more below`, contentW), mutedFg, '', false, true);
|
|
339
|
-
lines.push(downHint);
|
|
340
|
-
}
|
|
341
|
-
}
|
|
342
|
-
|
|
343
|
-
// ── Divider + hint ──────────────────────────────────────────────────────────────────
|
|
344
|
-
lines.push(createOverlayFilledBorderLine(width, layout, OVERLAY_GLYPHS.teeLeft, OVERLAY_GLYPHS.horizontal, OVERLAY_GLYPHS.teeRight, borderFg, DEFAULT_OVERLAY_PALETTE.sectionBg));
|
|
345
|
-
const hintLine = createOverlayContentLine(width, layout, borderFg, DEFAULT_OVERLAY_PALETTE.bodyBg);
|
|
346
|
-
putRowText(hintLine, layout.margin + 2, contentW, fitDisplay('Select a provider to browse its models', contentW), '244');
|
|
347
|
-
lines.push(hintLine);
|
|
348
|
-
lines.push(createOverlayContentLine(width, layout, borderFg, DEFAULT_OVERLAY_PALETTE.bodyBg));
|
|
349
|
-
} else if (picker.mode === 'contextCap') {
|
|
350
|
-
// ── Context cap input ──────────────────────────────────────────────────────────────
|
|
351
|
-
const capModel = picker.contextCapPendingModel;
|
|
352
|
-
const modelName = capModel ? capModel.displayName : 'unknown';
|
|
353
|
-
const currentCtx = capModel ? fmtContext(capModel.contextWindow) : '?';
|
|
354
|
-
const provenance = capModel?.contextWindowProvenance ?? 'configured_cap';
|
|
355
|
-
|
|
356
|
-
const promptLabel = 'Context window (tokens):';
|
|
357
|
-
const cursorChar = OVERLAY_GLYPHS.cursor;
|
|
358
|
-
const inputDisplay = picker.contextCapQuery + cursorChar;
|
|
359
|
-
const promptRow = createOverlayContentLine(width, layout, borderFg, DEFAULT_OVERLAY_PALETTE.inputBg);
|
|
360
|
-
putRowText(promptRow, layout.margin + 2, contentW, fitDisplay(`${promptLabel} ${inputDisplay}`, contentW), '#ffffff');
|
|
361
|
-
lines.push(promptRow);
|
|
362
|
-
|
|
363
|
-
if (picker.contextCapError) {
|
|
364
|
-
const errRow = createOverlayContentLine(width, layout, borderFg, DEFAULT_OVERLAY_PALETTE.bodyBg);
|
|
365
|
-
putRowText(errRow, layout.margin + 2, contentW, fitDisplay(picker.contextCapError, contentW), '#ff6666');
|
|
366
|
-
lines.push(errRow);
|
|
367
|
-
}
|
|
368
|
-
|
|
369
|
-
lines.push(createOverlayContentLine(width, layout, borderFg, DEFAULT_OVERLAY_PALETTE.bodyBg));
|
|
370
|
-
|
|
371
|
-
const hintText = `Leave blank to use default (current: ${currentCtx}, source: ${provenance})`;
|
|
372
|
-
const hintTrunc = getDisplayWidth(hintText) > contentW
|
|
373
|
-
? hintText.slice(0, contentW - 3) + '...'
|
|
374
|
-
: hintText;
|
|
375
|
-
const hintRow = createOverlayContentLine(width, layout, borderFg, DEFAULT_OVERLAY_PALETTE.bodyBg);
|
|
376
|
-
putRowText(hintRow, layout.margin + 2, contentW, fitDisplay(hintTrunc, contentW), '244', '', false, true);
|
|
377
|
-
lines.push(hintRow);
|
|
378
|
-
|
|
379
|
-
// Divider + model info
|
|
380
|
-
lines.push(createOverlayFilledBorderLine(width, layout, OVERLAY_GLYPHS.teeLeft, OVERLAY_GLYPHS.horizontal, OVERLAY_GLYPHS.teeRight, borderFg, DEFAULT_OVERLAY_PALETTE.sectionBg));
|
|
381
|
-
const modelInfoLine = createOverlayContentLine(width, layout, borderFg, DEFAULT_OVERLAY_PALETTE.bodyBg);
|
|
382
|
-
putRowText(modelInfoLine, layout.margin + 2, contentW, fitDisplay(`Model: ${modelName}`, contentW), '244');
|
|
383
|
-
lines.push(modelInfoLine);
|
|
384
|
-
lines.push(createOverlayContentLine(width, layout, borderFg, DEFAULT_OVERLAY_PALETTE.bodyBg));
|
|
385
|
-
} else {
|
|
386
|
-
// ── Effort list ────────────────────────────────────────────────────────────────────────
|
|
387
|
-
for (let i = 0; i < picker.effortLevels.length; i++) {
|
|
388
|
-
const level = picker.effortLevels[i];
|
|
389
|
-
const isSelected = i === picker.selectedIndex;
|
|
390
|
-
const indicator = isSelected ? `${OVERLAY_GLYPHS.selected} ` : ' ';
|
|
391
|
-
const desc = EFFORT_DESCRIPTIONS[level] ?? '';
|
|
392
|
-
const labelW = 10;
|
|
393
|
-
const labelStr = level.padEnd(labelW);
|
|
394
|
-
const remaining = contentW - labelW - 4;
|
|
395
|
-
const descStr = desc.length > remaining ? desc.slice(0, remaining - 3) + '...' : desc.padEnd(remaining);
|
|
396
|
-
const row = createOverlayContentLine(width, layout, borderFg, isSelected ? selectedBg : DEFAULT_OVERLAY_PALETTE.bodyBg);
|
|
397
|
-
const rowText = indicator + labelStr + ' ' + descStr;
|
|
398
|
-
putRowText(row, layout.margin + 2, contentW, fitDisplay(truncateDisplay(rowText, contentW), contentW), isSelected ? titleFg : bodyFg, isSelected ? selectedBg : DEFAULT_OVERLAY_PALETTE.bodyBg, isSelected);
|
|
399
|
-
lines.push(row);
|
|
400
|
-
}
|
|
401
|
-
|
|
402
|
-
// ── Divider + model context ──────────────────────────────────────────────────────
|
|
403
|
-
lines.push(createOverlayFilledBorderLine(width, layout, OVERLAY_GLYPHS.teeLeft, OVERLAY_GLYPHS.horizontal, OVERLAY_GLYPHS.teeRight, borderFg, DEFAULT_OVERLAY_PALETTE.sectionBg));
|
|
404
|
-
const modelName = picker.pendingModel ? picker.pendingModel.displayName : 'unknown';
|
|
405
|
-
const modelLine = createOverlayContentLine(width, layout, borderFg, DEFAULT_OVERLAY_PALETTE.bodyBg);
|
|
406
|
-
putRowText(modelLine, layout.margin + 2, contentW, fitDisplay(`Model: ${modelName}`, contentW), '244');
|
|
407
|
-
lines.push(modelLine);
|
|
408
|
-
lines.push(createOverlayContentLine(width, layout, borderFg, DEFAULT_OVERLAY_PALETTE.bodyBg));
|
|
409
|
-
}
|
|
410
|
-
|
|
411
|
-
// ── Bottom border with hints ─────────────────────────────────────────────────────────
|
|
412
|
-
const filterLabelsFooter: Record<string, string> = { all: 'All', free: 'Free', paid: 'Paid', subscription: 'Sub' };
|
|
413
|
-
const filterLabelFooter = filterLabelsFooter[picker.categoryFilter] ?? 'All';
|
|
414
|
-
const groupByLabel = picker.groupBy ?? 'provider';
|
|
415
|
-
const selectedModel = picker.mode === 'model' ? picker.getSelected() : null;
|
|
416
|
-
const showContextCapHint = selectedModel != null && selectedModel.contextWindowProvenance !== undefined;
|
|
417
|
-
let hints: string;
|
|
418
|
-
if (picker.mode === 'model') {
|
|
419
|
-
// The model-picker box caps at 72 cols, so the footer stays compact: the
|
|
420
|
-
// list nav (Up/Down/Enter) is the obvious interaction and lives in the
|
|
421
|
-
// /shortcuts overlay; the footer prioritizes the stateful controls. The
|
|
422
|
-
// live filter/group value is folded into each key's verb so it stays
|
|
423
|
-
// adjacent to its control and reads inside the width.
|
|
424
|
-
const modelHints: HintSpec[] = [{ key: '/', verb: 'Search' }];
|
|
425
|
-
if (showContextCapHint) modelHints.push({ key: 'Space', verb: 'Ctx' });
|
|
426
|
-
modelHints.push(
|
|
427
|
-
{ key: 'Tab', verb: `Filter: ${filterLabelFooter}` },
|
|
428
|
-
{ key: 'G', verb: `Group: ${groupByLabel}` },
|
|
429
|
-
{ key: 'Esc', verb: 'Close' },
|
|
430
|
-
);
|
|
431
|
-
hints = formatHints(modelHints);
|
|
432
|
-
} else if (picker.mode === 'contextCap') {
|
|
433
|
-
hints = formatHints([{ key: 'Enter', verb: 'Confirm' }, { key: 'Esc', verb: 'Cancel' }]);
|
|
434
|
-
} else {
|
|
435
|
-
hints = formatHints([{ key: 'Up/Down', verb: 'Move' }, { key: 'Enter', verb: 'Select' }, { key: 'Esc', verb: 'Cancel' }]);
|
|
436
|
-
}
|
|
437
|
-
const footerLine = createOverlayFilledBorderLine(width, layout, OVERLAY_GLYPHS.bottomLeft, OVERLAY_GLYPHS.horizontal, OVERLAY_GLYPHS.bottomRight, borderFg, DEFAULT_OVERLAY_PALETTE.sectionBg);
|
|
438
|
-
putRowText(footerLine, layout.margin + 2, contentW, fitDisplay(truncateDisplay(hints, contentW), contentW), mutedFg, '', false, true);
|
|
439
|
-
lines.push(footerLine);
|
|
440
|
-
|
|
441
|
-
renderCache.set(picker, { key: cacheKey, lines });
|
|
442
|
-
return lines;
|
|
443
|
-
}
|
|
444
|
-
|
|
445
|
-
function getRenderCacheKey(
|
|
446
|
-
picker: ModelPickerModal,
|
|
447
|
-
width: number,
|
|
448
|
-
maxVisible: number,
|
|
449
|
-
viewportHeight: number | undefined,
|
|
450
|
-
): string {
|
|
451
|
-
const base = [
|
|
452
|
-
width,
|
|
453
|
-
maxVisible,
|
|
454
|
-
viewportHeight ?? '',
|
|
455
|
-
picker.mode,
|
|
456
|
-
picker.target,
|
|
457
|
-
picker.query,
|
|
458
|
-
picker.searchFocused ? 1 : 0,
|
|
459
|
-
picker.selectedIndex,
|
|
460
|
-
picker.scrollOffset,
|
|
461
|
-
picker.categoryFilter,
|
|
462
|
-
picker.capabilityFilter,
|
|
463
|
-
picker.availableOnly ? 1 : 0,
|
|
464
|
-
picker.benchmarkSort,
|
|
465
|
-
picker.groupBy,
|
|
466
|
-
keyForSet(picker.pinnedIds),
|
|
467
|
-
keyForSet(picker.configuredProviders),
|
|
468
|
-
];
|
|
469
|
-
|
|
470
|
-
if (picker.mode === 'model') {
|
|
471
|
-
const filtered = picker.getFilteredModels();
|
|
472
|
-
const selected = filtered[picker.selectedIndex];
|
|
473
|
-
base.push(objectId(picker.models), objectId(filtered), filtered.length, selected?.registryKey ?? selected?.id ?? '');
|
|
474
|
-
} else if (picker.mode === 'provider') {
|
|
475
|
-
const filteredProviders = picker.getFilteredProviders();
|
|
476
|
-
base.push(objectId(picker.providers), objectId(filteredProviders), filteredProviders.length, keyForMap(picker.configuredViaMap));
|
|
477
|
-
} else if (picker.mode === 'effort') {
|
|
478
|
-
base.push(objectId(picker.effortLevels), picker.effortLevels.join('\u001f'), picker.pendingModel?.registryKey ?? picker.pendingModel?.id ?? '');
|
|
479
|
-
} else if (picker.mode === 'contextCap') {
|
|
480
|
-
base.push(picker.contextCapQuery, picker.contextCapPendingModel?.registryKey ?? picker.contextCapPendingModel?.id ?? '', picker.contextCapError ?? '');
|
|
481
|
-
}
|
|
482
|
-
|
|
483
|
-
return base.join('\u001e');
|
|
484
|
-
}
|
|
485
|
-
|
|
486
|
-
function objectId(value: object): number {
|
|
487
|
-
const existing = objectIds.get(value);
|
|
488
|
-
if (existing !== undefined) return existing;
|
|
489
|
-
const next = nextObjectId++;
|
|
490
|
-
objectIds.set(value, next);
|
|
491
|
-
return next;
|
|
492
|
-
}
|
|
493
|
-
|
|
494
|
-
function keyForSet(values: ReadonlySet<string>): string {
|
|
495
|
-
return values.size === 0 ? '' : [...values].sort().join('\u001f');
|
|
496
|
-
}
|
|
497
|
-
|
|
498
|
-
function keyForMap(values: ReadonlyMap<string, string | undefined>): string {
|
|
499
|
-
if (values.size === 0) return '';
|
|
500
|
-
return [...values.entries()]
|
|
501
|
-
.sort(([left], [right]) => left.localeCompare(right))
|
|
502
|
-
.map(([key, value]) => `${key}\u001d${value ?? ''}`)
|
|
503
|
-
.join('\u001f');
|
|
504
|
-
}
|
|
13
|
+
export const MODEL_PICKER_CHROME_LINES = 7;
|
|
@@ -10,22 +10,22 @@ import type { ModelPickerModal } from '../input/model-picker.ts';
|
|
|
10
10
|
import type { ModelPickerTargetInfo } from '../input/model-picker.ts';
|
|
11
11
|
import type { Line } from '../types/grid.ts';
|
|
12
12
|
import { createEmptyLine, createStyledCell } from '../types/grid.ts';
|
|
13
|
-
import { getDisplayWidth, wrapText } from '../utils/terminal-width.ts';
|
|
13
|
+
import { fitDisplay, getDisplayWidth, wrapText } from '../utils/terminal-width.ts';
|
|
14
14
|
import { abbreviateCount } from '../utils/format-number.ts';
|
|
15
15
|
import { GLYPHS, UI_TONES } from './ui-primitives.ts';
|
|
16
16
|
|
|
17
17
|
const PALETTE = {
|
|
18
|
-
border:
|
|
18
|
+
border: UI_TONES.border,
|
|
19
19
|
title: '#67e8f9',
|
|
20
|
-
subtitle:
|
|
21
|
-
text:
|
|
22
|
-
muted:
|
|
23
|
-
dim:
|
|
24
|
-
selectedBg:
|
|
20
|
+
subtitle: UI_TONES.accent.conversation,
|
|
21
|
+
text: UI_TONES.fg.primary,
|
|
22
|
+
muted: UI_TONES.fg.muted,
|
|
23
|
+
dim: UI_TONES.border,
|
|
24
|
+
selectedBg: UI_TONES.bg.selected,
|
|
25
25
|
targetBg: '#141b25',
|
|
26
26
|
detailBg: '#121923',
|
|
27
27
|
bodyBg: '#0f141d',
|
|
28
|
-
footerBg:
|
|
28
|
+
footerBg: UI_TONES.bg.footer,
|
|
29
29
|
good: UI_TONES.state.good,
|
|
30
30
|
warn: UI_TONES.state.warn,
|
|
31
31
|
info: UI_TONES.state.info,
|
|
@@ -100,23 +100,8 @@ function drawVertical(line: Line, x: number, bg = ''): void {
|
|
|
100
100
|
line[x] = createStyledCell(GLYPHS.frame.vertical, { fg: PALETTE.border, bg });
|
|
101
101
|
}
|
|
102
102
|
|
|
103
|
-
function clipDisplay(text: string, width: number): string {
|
|
104
|
-
if (width <= 0) return '';
|
|
105
|
-
let used = 0;
|
|
106
|
-
let output = '';
|
|
107
|
-
for (const ch of text) {
|
|
108
|
-
const chWidth = getDisplayWidth(ch);
|
|
109
|
-
if (chWidth <= 0) continue;
|
|
110
|
-
if (used + chWidth > width) break;
|
|
111
|
-
output += ch;
|
|
112
|
-
used += chWidth;
|
|
113
|
-
}
|
|
114
|
-
return output;
|
|
115
|
-
}
|
|
116
|
-
|
|
117
103
|
function padDisplay(text: string, width: number): string {
|
|
118
|
-
|
|
119
|
-
return clipped + ' '.repeat(Math.max(0, width - getDisplayWidth(clipped)));
|
|
104
|
+
return fitDisplay(text, width, '');
|
|
120
105
|
}
|
|
121
106
|
|
|
122
107
|
function stableWindow(total: number, selected: number, visible: number): { start: number; end: number } {
|
|
@@ -53,7 +53,13 @@ export function createOverlayBoxLayout(
|
|
|
53
53
|
maxWidth: number,
|
|
54
54
|
): OverlayBoxLayout {
|
|
55
55
|
const resolvedMaxWidth = getOverlayMaxWidth(terminalWidth, margin, maxWidth);
|
|
56
|
-
|
|
56
|
+
// The 20-column floor exists so overlays stay legible on ordinary narrow
|
|
57
|
+
// terminals, but on a hostile-narrow terminal (terminalWidth smaller than
|
|
58
|
+
// margin*2 + 20) that floor alone can push the box wider than the real
|
|
59
|
+
// terminal, walking cells off the right edge of the line array. Clamp the
|
|
60
|
+
// floored result back down to what actually fits before returning it.
|
|
61
|
+
const availableWidth = Math.max(1, terminalWidth - margin * 2);
|
|
62
|
+
const width = Math.min(availableWidth, Math.max(20, Math.min(availableWidth, resolvedMaxWidth)));
|
|
57
63
|
const contentWidth = width - 2;
|
|
58
64
|
const innerWidth = contentWidth - 2;
|
|
59
65
|
return { margin, width, contentWidth, innerWidth };
|
|
@@ -128,14 +134,6 @@ export function createOverlayFilledBorderLine(
|
|
|
128
134
|
return line;
|
|
129
135
|
}
|
|
130
136
|
|
|
131
|
-
export function createOverlayFrameLine(
|
|
132
|
-
terminalWidth: number,
|
|
133
|
-
layout: OverlayBoxLayout,
|
|
134
|
-
bg = DEFAULT_OVERLAY_PALETTE.bodyBg,
|
|
135
|
-
): Line {
|
|
136
|
-
return createOverlayContentLine(terminalWidth, layout, DEFAULT_OVERLAY_PALETTE.borderFg, bg);
|
|
137
|
-
}
|
|
138
|
-
|
|
139
137
|
export const OVERLAY_GLYPHS = {
|
|
140
138
|
topLeft: GLYPHS.frame.topLeft,
|
|
141
139
|
topRight: GLYPHS.frame.topRight,
|