@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
|
@@ -9,7 +9,25 @@ import { logger } from '@pellux/goodvibes-sdk/platform/utils';
|
|
|
9
9
|
import { getOverlaySurfaceMetrics, getStableOverlayContentRows } from './overlay-viewport.ts';
|
|
10
10
|
import { summarizeError } from '@pellux/goodvibes-sdk/platform/utils';
|
|
11
11
|
import { handleConfirmInput, type ConfirmState } from '../panels/confirm-state.ts';
|
|
12
|
-
import { AGENT_TERMINAL_STATUSES as MODAL_TERMINAL_STATUSES, AGENT_STALL_THRESHOLD_MS as MODAL_STALL_THRESHOLD_MS } from '../panels/agent-inspector-shared.ts';
|
|
12
|
+
import { AGENT_TERMINAL_STATUSES as MODAL_TERMINAL_STATUSES, AGENT_STALL_THRESHOLD_MS as MODAL_STALL_THRESHOLD_MS, hasReportedUsage } from '../panels/agent-inspector-shared.ts';
|
|
13
|
+
import { UI_TONES } from './ui-primitives.ts';
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* Agent-detail-modal neon accent palette — WRFC/streaming panel-native hues
|
|
17
|
+
* with no UI_TONES.state/accent role. Preserved byte-exact as named
|
|
18
|
+
* accents per WO-207b (2026-07-02-renderer-plan.md decisions: "Agent-detail
|
|
19
|
+
* neon palette: preserved byte-exact as named accents, zero visual change").
|
|
20
|
+
*/
|
|
21
|
+
const AGENT_DETAIL_NEON = {
|
|
22
|
+
/** Addendum note + streaming content text. */
|
|
23
|
+
mint: '#aaffee',
|
|
24
|
+
/** Unsatisfied WRFC findings + agent error text. */
|
|
25
|
+
bad: '#ff6666',
|
|
26
|
+
/** Satisfied WRFC findings. */
|
|
27
|
+
good: '#44ff88',
|
|
28
|
+
/** Progress line + streaming label. */
|
|
29
|
+
accent: '#00ffcc',
|
|
30
|
+
} as const;
|
|
13
31
|
|
|
14
32
|
// ─── Constants ────────────────────────────────────────────────────────────────
|
|
15
33
|
|
|
@@ -19,6 +37,17 @@ const AGENT_ID_DISPLAY_LENGTH = 16;
|
|
|
19
37
|
// MODAL_TERMINAL_STATUSES and MODAL_STALL_THRESHOLD_MS are re-exported aliases
|
|
20
38
|
// from agent-inspector-shared.ts (imported above alongside ConfirmState).
|
|
21
39
|
|
|
40
|
+
/**
|
|
41
|
+
* formatStalledLabel — the "[STALLED — N+ min no activity]" suffix appended
|
|
42
|
+
* to the Status line for a stalled agent. The minute count is derived from
|
|
43
|
+
* the stall threshold (in ms) rather than hardcoded, so the label can never
|
|
44
|
+
* drift out of sync with the actual threshold that decided isStalled.
|
|
45
|
+
*/
|
|
46
|
+
export function formatStalledLabel(thresholdMs: number): string {
|
|
47
|
+
const minutes = Math.floor(thresholdMs / 60000);
|
|
48
|
+
return ` [STALLED — ${minutes}+ min no activity]`;
|
|
49
|
+
}
|
|
50
|
+
|
|
22
51
|
export interface AgentDetailModalDeps {
|
|
23
52
|
readonly agentManager: Pick<AgentManager, 'getStatus' | 'list'>;
|
|
24
53
|
readonly agentMessageBus: Pick<AgentMessageBus, 'getMessages'>;
|
|
@@ -248,16 +277,17 @@ export function renderAgentDetailModal(
|
|
|
248
277
|
sections.push({ type: 'text', content: `Template : ${rec.template}` });
|
|
249
278
|
sections.push({ type: 'text', content: `Model : ${modelStr}` });
|
|
250
279
|
const isStalled = !MODAL_TERMINAL_STATUSES.has(rec.status) && (now - rec.startedAt) >= MODAL_STALL_THRESHOLD_MS;
|
|
251
|
-
sections.push({ type: 'text', content: `Status : ${rec.status}${isStalled ?
|
|
280
|
+
sections.push({ type: 'text', content: `Status : ${rec.status}${isStalled ? formatStalledLabel(MODAL_STALL_THRESHOLD_MS) : ''}` });
|
|
252
281
|
sections.push({ type: 'text', content: `Duration : ${formatElapsed(elapsedMs)}` });
|
|
253
282
|
sections.push({ type: 'separator' });
|
|
254
283
|
|
|
255
284
|
// Metrics
|
|
256
285
|
sections.push({ type: 'text', content: `Tool calls : ${rec.toolCallCount}` });
|
|
257
|
-
if (rec.usage) {
|
|
258
|
-
const
|
|
286
|
+
if (hasReportedUsage(rec.usage)) {
|
|
287
|
+
const usage = rec.usage;
|
|
288
|
+
const totalIn = usage.inputTokens + (usage.cacheReadTokens ?? 0) + (usage.cacheWriteTokens ?? 0);
|
|
259
289
|
sections.push({ type: 'text', content: `Tokens in : ${totalIn.toLocaleString()}` });
|
|
260
|
-
sections.push({ type: 'text', content: `Tokens out : ${
|
|
290
|
+
sections.push({ type: 'text', content: `Tokens out : ${usage.outputTokens.toLocaleString()}` });
|
|
261
291
|
} else {
|
|
262
292
|
sections.push({
|
|
263
293
|
type: 'text',
|
|
@@ -271,7 +301,7 @@ export function renderAgentDetailModal(
|
|
|
271
301
|
sections.push({
|
|
272
302
|
type: 'text',
|
|
273
303
|
content: 'Addendum : yes (WRFC constraint layer injected)',
|
|
274
|
-
style: { fg:
|
|
304
|
+
style: { fg: AGENT_DETAIL_NEON.mint },
|
|
275
305
|
});
|
|
276
306
|
}
|
|
277
307
|
|
|
@@ -301,7 +331,7 @@ export function renderAgentDetailModal(
|
|
|
301
331
|
sections.push({
|
|
302
332
|
type: 'text',
|
|
303
333
|
content: `Findings : ${findings.length} checked, ${unsatisfied.length} unsatisfied`,
|
|
304
|
-
style: { fg: unsatisfied.length > 0 ?
|
|
334
|
+
style: { fg: unsatisfied.length > 0 ? AGENT_DETAIL_NEON.bad : AGENT_DETAIL_NEON.good },
|
|
305
335
|
});
|
|
306
336
|
}
|
|
307
337
|
}
|
|
@@ -316,7 +346,7 @@ export function renderAgentDetailModal(
|
|
|
316
346
|
sections.push({
|
|
317
347
|
type: 'text',
|
|
318
348
|
content: `Progress: ${rec.progress}`,
|
|
319
|
-
style: { fg:
|
|
349
|
+
style: { fg: AGENT_DETAIL_NEON.accent },
|
|
320
350
|
});
|
|
321
351
|
}
|
|
322
352
|
|
|
@@ -326,7 +356,7 @@ export function renderAgentDetailModal(
|
|
|
326
356
|
sections.push({
|
|
327
357
|
type: 'text',
|
|
328
358
|
content: `Error: ${rec.error}`,
|
|
329
|
-
style: { fg:
|
|
359
|
+
style: { fg: AGENT_DETAIL_NEON.bad },
|
|
330
360
|
});
|
|
331
361
|
}
|
|
332
362
|
|
|
@@ -393,7 +423,7 @@ export function renderAgentDetailModal(
|
|
|
393
423
|
content: truncated
|
|
394
424
|
? `Streaming (last ${STREAMING_MAX_CHARS} of ${content.length} chars \u2191 scroll for more):`
|
|
395
425
|
: 'Streaming:',
|
|
396
|
-
style: { fg:
|
|
426
|
+
style: { fg: AGENT_DETAIL_NEON.accent, dim: true },
|
|
397
427
|
});
|
|
398
428
|
// Split into display lines, capped at width for readability
|
|
399
429
|
const maxLineWidth = Math.max(width - 10, 40);
|
|
@@ -403,7 +433,7 @@ export function renderAgentDetailModal(
|
|
|
403
433
|
sections.push({
|
|
404
434
|
type: 'text',
|
|
405
435
|
content: ` ${trimmed}`,
|
|
406
|
-
style: { fg:
|
|
436
|
+
style: { fg: AGENT_DETAIL_NEON.mint },
|
|
407
437
|
});
|
|
408
438
|
}
|
|
409
439
|
}
|
|
@@ -415,7 +445,7 @@ export function renderAgentDetailModal(
|
|
|
415
445
|
sections.push({
|
|
416
446
|
type: 'text',
|
|
417
447
|
content: `Cancel agent "${modal.confirmCancel.label}"?`,
|
|
418
|
-
style: { fg:
|
|
448
|
+
style: { fg: UI_TONES.state.warn },
|
|
419
449
|
});
|
|
420
450
|
sections.push({
|
|
421
451
|
type: 'text',
|
|
@@ -3,6 +3,32 @@ import { UIFactory } from './ui-factory.ts';
|
|
|
3
3
|
import { getDisplayWidth } from '../utils/terminal-width.ts';
|
|
4
4
|
import { LAYOUT } from './layout.ts';
|
|
5
5
|
import { SyntaxHighlighter, type SyntaxToken as HLToken } from './syntax-highlighter.ts';
|
|
6
|
+
import { UI_TONES } from './ui-primitives.ts';
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* Regex-fallback tokenizer theme. VS Code Dark+ inspired, but the
|
|
10
|
+
* semantic-token hues (string/number/keyword/type/function/operator/
|
|
11
|
+
* property/comment) are pinned to the same values as the tree-sitter
|
|
12
|
+
* Vaporwave palette in syntax-highlighter.ts's TOKEN_STYLES map, so a code
|
|
13
|
+
* block that starts on the regex fallback and gets replaced by the async
|
|
14
|
+
* tree-sitter parse doesn't visibly shift color mid-stream (WO-207c).
|
|
15
|
+
* `accent` and `bg` are chrome (the language-label header bar and the body
|
|
16
|
+
* background), not syntax tokens, and keep their original VS Code Dark+
|
|
17
|
+
* values — folded here from two separate literals (one in the header, one
|
|
18
|
+
* duplicated on the footer line) into a single named source (WO-207b).
|
|
19
|
+
*/
|
|
20
|
+
const FALLBACK_THEME = {
|
|
21
|
+
string: '#00ff88',
|
|
22
|
+
number: '#ffcc00',
|
|
23
|
+
keyword: '#d000ff',
|
|
24
|
+
type: '#ff6b9d',
|
|
25
|
+
function: UI_TONES.accent.brand,
|
|
26
|
+
operator: '#ffffff',
|
|
27
|
+
property: '#87ceeb',
|
|
28
|
+
comment: '#666666',
|
|
29
|
+
bg: '#0d0d0d',
|
|
30
|
+
accent: '#4ec9b0',
|
|
31
|
+
} as const;
|
|
6
32
|
|
|
7
33
|
// ─── Language Keyword Maps ───────────────────────────────────────────────────
|
|
8
34
|
|
|
@@ -61,7 +87,7 @@ function tokenizeTsJs(line: string): SyntaxToken[] {
|
|
|
61
87
|
while (i < line.length) {
|
|
62
88
|
// Line comment
|
|
63
89
|
if (line.slice(i, i + 2) === '//') {
|
|
64
|
-
tokens.push({ text: line.slice(i), fg:
|
|
90
|
+
tokens.push({ text: line.slice(i), fg: FALLBACK_THEME.comment, italic: true });
|
|
65
91
|
break;
|
|
66
92
|
}
|
|
67
93
|
// String (single, double, template)
|
|
@@ -72,7 +98,7 @@ function tokenizeTsJs(line: string): SyntaxToken[] {
|
|
|
72
98
|
if (line[j] === '\\') j++;
|
|
73
99
|
j++;
|
|
74
100
|
}
|
|
75
|
-
tokens.push({ text: line.slice(i, j + 1), fg:
|
|
101
|
+
tokens.push({ text: line.slice(i, j + 1), fg: FALLBACK_THEME.string });
|
|
76
102
|
i = j + 1;
|
|
77
103
|
continue;
|
|
78
104
|
}
|
|
@@ -80,7 +106,7 @@ function tokenizeTsJs(line: string): SyntaxToken[] {
|
|
|
80
106
|
if (/[0-9]/.test(line[i])) {
|
|
81
107
|
let j = i;
|
|
82
108
|
while (j < line.length && /[0-9._xXbBoO]/.test(line[j])) j++;
|
|
83
|
-
tokens.push({ text: line.slice(i, j), fg:
|
|
109
|
+
tokens.push({ text: line.slice(i, j), fg: FALLBACK_THEME.number });
|
|
84
110
|
i = j;
|
|
85
111
|
continue;
|
|
86
112
|
}
|
|
@@ -90,11 +116,11 @@ function tokenizeTsJs(line: string): SyntaxToken[] {
|
|
|
90
116
|
while (j < line.length && /[\w$]/.test(line[j])) j++;
|
|
91
117
|
const word = line.slice(i, j);
|
|
92
118
|
if (TS_JS_KEYWORDS.has(word)) {
|
|
93
|
-
tokens.push({ text: word, fg:
|
|
119
|
+
tokens.push({ text: word, fg: FALLBACK_THEME.keyword, bold: true });
|
|
94
120
|
} else if (TS_TYPES.has(word)) {
|
|
95
|
-
tokens.push({ text: word, fg:
|
|
121
|
+
tokens.push({ text: word, fg: FALLBACK_THEME.type });
|
|
96
122
|
} else if (line[j] === '(') {
|
|
97
|
-
tokens.push({ text: word, fg:
|
|
123
|
+
tokens.push({ text: word, fg: FALLBACK_THEME.function });
|
|
98
124
|
} else {
|
|
99
125
|
tokens.push({ text: word, fg: '' });
|
|
100
126
|
}
|
|
@@ -104,7 +130,7 @@ function tokenizeTsJs(line: string): SyntaxToken[] {
|
|
|
104
130
|
// Operators and punctuation
|
|
105
131
|
const ch = line[i];
|
|
106
132
|
const isOp = '=<>!&|+-*/%^~?:'.includes(ch);
|
|
107
|
-
tokens.push({ text: ch, fg: isOp ?
|
|
133
|
+
tokens.push({ text: ch, fg: isOp ? FALLBACK_THEME.operator : '' });
|
|
108
134
|
i++;
|
|
109
135
|
}
|
|
110
136
|
|
|
@@ -117,21 +143,21 @@ function tokenizePython(line: string): SyntaxToken[] {
|
|
|
117
143
|
|
|
118
144
|
while (i < line.length) {
|
|
119
145
|
if (line[i] === '#') {
|
|
120
|
-
tokens.push({ text: line.slice(i), fg:
|
|
146
|
+
tokens.push({ text: line.slice(i), fg: FALLBACK_THEME.comment, italic: true });
|
|
121
147
|
break;
|
|
122
148
|
}
|
|
123
149
|
if (line[i] === '"' || line[i] === "'") {
|
|
124
150
|
const q = line[i];
|
|
125
151
|
let j = i + 1;
|
|
126
152
|
while (j < line.length && line[j] !== q) { if (line[j] === '\\') j++; j++; }
|
|
127
|
-
tokens.push({ text: line.slice(i, j + 1), fg:
|
|
153
|
+
tokens.push({ text: line.slice(i, j + 1), fg: FALLBACK_THEME.string });
|
|
128
154
|
i = j + 1;
|
|
129
155
|
continue;
|
|
130
156
|
}
|
|
131
157
|
if (/[0-9]/.test(line[i])) {
|
|
132
158
|
let j = i;
|
|
133
159
|
while (j < line.length && /[0-9._]/.test(line[j])) j++;
|
|
134
|
-
tokens.push({ text: line.slice(i, j), fg:
|
|
160
|
+
tokens.push({ text: line.slice(i, j), fg: FALLBACK_THEME.number });
|
|
135
161
|
i = j;
|
|
136
162
|
continue;
|
|
137
163
|
}
|
|
@@ -140,11 +166,11 @@ function tokenizePython(line: string): SyntaxToken[] {
|
|
|
140
166
|
while (j < line.length && /[\w]/.test(line[j])) j++;
|
|
141
167
|
const word = line.slice(i, j);
|
|
142
168
|
if (PYTHON_KEYWORDS.has(word)) {
|
|
143
|
-
tokens.push({ text: word, fg:
|
|
169
|
+
tokens.push({ text: word, fg: FALLBACK_THEME.keyword, bold: true });
|
|
144
170
|
} else if (/^[A-Z]/.test(word)) {
|
|
145
|
-
tokens.push({ text: word, fg:
|
|
171
|
+
tokens.push({ text: word, fg: FALLBACK_THEME.type });
|
|
146
172
|
} else if (line[j] === '(') {
|
|
147
|
-
tokens.push({ text: word, fg:
|
|
173
|
+
tokens.push({ text: word, fg: FALLBACK_THEME.function });
|
|
148
174
|
} else {
|
|
149
175
|
tokens.push({ text: word, fg: '' });
|
|
150
176
|
}
|
|
@@ -163,21 +189,21 @@ function tokenizeBash(line: string): SyntaxToken[] {
|
|
|
163
189
|
|
|
164
190
|
while (i < line.length) {
|
|
165
191
|
if (line[i] === '#') {
|
|
166
|
-
tokens.push({ text: line.slice(i), fg:
|
|
192
|
+
tokens.push({ text: line.slice(i), fg: FALLBACK_THEME.comment, italic: true });
|
|
167
193
|
break;
|
|
168
194
|
}
|
|
169
195
|
if (line[i] === '"' || line[i] === "'") {
|
|
170
196
|
const q = line[i];
|
|
171
197
|
let j = i + 1;
|
|
172
198
|
while (j < line.length && line[j] !== q) { if (line[j] === '\\') j++; j++; }
|
|
173
|
-
tokens.push({ text: line.slice(i, j + 1), fg:
|
|
199
|
+
tokens.push({ text: line.slice(i, j + 1), fg: FALLBACK_THEME.string });
|
|
174
200
|
i = j + 1;
|
|
175
201
|
continue;
|
|
176
202
|
}
|
|
177
203
|
if (line[i] === '$') {
|
|
178
204
|
let j = i + 1;
|
|
179
205
|
while (j < line.length && /[\w{}_]/.test(line[j])) j++;
|
|
180
|
-
tokens.push({ text: line.slice(i, j), fg:
|
|
206
|
+
tokens.push({ text: line.slice(i, j), fg: FALLBACK_THEME.property });
|
|
181
207
|
i = j;
|
|
182
208
|
continue;
|
|
183
209
|
}
|
|
@@ -186,7 +212,7 @@ function tokenizeBash(line: string): SyntaxToken[] {
|
|
|
186
212
|
while (j < line.length && /[\w-]/.test(line[j])) j++;
|
|
187
213
|
const word = line.slice(i, j);
|
|
188
214
|
if (BASH_KEYWORDS.has(word)) {
|
|
189
|
-
tokens.push({ text: word, fg:
|
|
215
|
+
tokens.push({ text: word, fg: FALLBACK_THEME.keyword, bold: true });
|
|
190
216
|
} else {
|
|
191
217
|
tokens.push({ text: word, fg: '' });
|
|
192
218
|
}
|
|
@@ -211,9 +237,9 @@ function tokenizeJson(line: string): SyntaxToken[] {
|
|
|
211
237
|
// JSON key: followed by :
|
|
212
238
|
const rest = line.slice(j + 1).trimStart();
|
|
213
239
|
if (rest.startsWith(':')) {
|
|
214
|
-
tokens.push({ text: str, fg:
|
|
240
|
+
tokens.push({ text: str, fg: FALLBACK_THEME.property });
|
|
215
241
|
} else {
|
|
216
|
-
tokens.push({ text: str, fg:
|
|
242
|
+
tokens.push({ text: str, fg: FALLBACK_THEME.string });
|
|
217
243
|
}
|
|
218
244
|
i = j + 1;
|
|
219
245
|
continue;
|
|
@@ -221,13 +247,13 @@ function tokenizeJson(line: string): SyntaxToken[] {
|
|
|
221
247
|
if (/[0-9-]/.test(line[i])) {
|
|
222
248
|
let j = i;
|
|
223
249
|
while (j < line.length && /[0-9.eE+-]/.test(line[j])) j++;
|
|
224
|
-
tokens.push({ text: line.slice(i, j), fg:
|
|
250
|
+
tokens.push({ text: line.slice(i, j), fg: FALLBACK_THEME.number });
|
|
225
251
|
i = j;
|
|
226
252
|
continue;
|
|
227
253
|
}
|
|
228
254
|
const boolNull = ['true', 'false', 'null'].find(k => line.startsWith(k, i));
|
|
229
255
|
if (boolNull) {
|
|
230
|
-
tokens.push({ text: boolNull, fg:
|
|
256
|
+
tokens.push({ text: boolNull, fg: FALLBACK_THEME.keyword, bold: true });
|
|
231
257
|
i += boolNull.length;
|
|
232
258
|
continue;
|
|
233
259
|
}
|
|
@@ -240,12 +266,12 @@ function tokenizeJson(line: string): SyntaxToken[] {
|
|
|
240
266
|
function tokenizeYaml(line: string): SyntaxToken[] {
|
|
241
267
|
const tokens: SyntaxToken[] = [];
|
|
242
268
|
if (line.trimStart().startsWith('#')) {
|
|
243
|
-
return [{ text: line, fg:
|
|
269
|
+
return [{ text: line, fg: FALLBACK_THEME.comment, italic: true }];
|
|
244
270
|
}
|
|
245
271
|
const keyMatch = line.match(/^(\s*)([^:]+)(:)(\s*.*)/);
|
|
246
272
|
if (keyMatch) {
|
|
247
273
|
if (keyMatch[1]) tokens.push({ text: keyMatch[1], fg: '' });
|
|
248
|
-
tokens.push({ text: keyMatch[2], fg:
|
|
274
|
+
tokens.push({ text: keyMatch[2], fg: FALLBACK_THEME.property });
|
|
249
275
|
tokens.push({ text: keyMatch[3], fg: '244' });
|
|
250
276
|
if (keyMatch[4]) {
|
|
251
277
|
const val = keyMatch[4];
|
|
@@ -254,7 +280,7 @@ function tokenizeYaml(line: string): SyntaxToken[] {
|
|
|
254
280
|
const isStr = /^['"]/.test(trimVal);
|
|
255
281
|
const isBool = trimVal === 'true' || trimVal === 'false' || trimVal === 'null' || trimVal === 'yes' || trimVal === 'no';
|
|
256
282
|
const isNum = /^-?[0-9]/.test(trimVal);
|
|
257
|
-
const valFg = isStr ?
|
|
283
|
+
const valFg = isStr ? FALLBACK_THEME.string : isBool ? FALLBACK_THEME.keyword : isNum ? FALLBACK_THEME.number : '';
|
|
258
284
|
tokens.push({ text: val, fg: valFg });
|
|
259
285
|
}
|
|
260
286
|
return tokens;
|
|
@@ -292,7 +318,7 @@ export function renderCodeBlock(
|
|
|
292
318
|
const showLineNumbers = opts.showLineNumbers ?? true;
|
|
293
319
|
const lineNumW = showLineNumbers ? String(codeLines.length).length + 1 : 0; // e.g. "10 "
|
|
294
320
|
const contentStartX = showLineNumbers ? leftMargin + lineNumW + 1 : leftMargin;
|
|
295
|
-
const BG =
|
|
321
|
+
const BG = FALLBACK_THEME.bg;
|
|
296
322
|
const LINE_NUM_FG = '238';
|
|
297
323
|
const effectiveWidth = width - LAYOUT.RIGHT_MARGIN;
|
|
298
324
|
|
|
@@ -320,7 +346,7 @@ export function renderCodeBlock(
|
|
|
320
346
|
let hx = leftMargin;
|
|
321
347
|
for (const ch of headerStr) {
|
|
322
348
|
if (hx >= effectiveWidth) break;
|
|
323
|
-
headerLine[hx] = createStyledCell(ch, { fg: '#1a1a1a', bg:
|
|
349
|
+
headerLine[hx] = createStyledCell(ch, { fg: '#1a1a1a', bg: FALLBACK_THEME.accent, bold: true });
|
|
324
350
|
hx++;
|
|
325
351
|
}
|
|
326
352
|
lines.push(headerLine);
|
|
@@ -362,7 +388,11 @@ export function renderCodeBlock(
|
|
|
362
388
|
continue;
|
|
363
389
|
}
|
|
364
390
|
line[cx] = createStyledCell(ch, { fg: token.fg, bg: BG, bold: token.bold, italic: token.italic });
|
|
365
|
-
|
|
391
|
+
// Bound the wide-glyph placeholder against the body's own right edge
|
|
392
|
+
// (effectiveWidth), not the full line width — otherwise a 2-column
|
|
393
|
+
// glyph landing on the last body column spills its placeholder cell
|
|
394
|
+
// into the reserved right-margin band the header/footer stop at.
|
|
395
|
+
if (cw === 2 && cx + 1 < effectiveWidth) line[cx + 1] = { ...line[cx], char: '' };
|
|
366
396
|
cx += cw;
|
|
367
397
|
}
|
|
368
398
|
}
|
|
@@ -373,7 +403,7 @@ export function renderCodeBlock(
|
|
|
373
403
|
// Footer line
|
|
374
404
|
const footerLine = createEmptyLine(width);
|
|
375
405
|
for (let fx = leftMargin; fx < effectiveWidth; fx++) {
|
|
376
|
-
footerLine[fx] = createStyledCell(' ', { bg:
|
|
406
|
+
footerLine[fx] = createStyledCell(' ', { bg: BG });
|
|
377
407
|
}
|
|
378
408
|
lines.push(footerLine);
|
|
379
409
|
|
|
@@ -3,6 +3,7 @@ import { ModalFactory } from './modal-factory.ts';
|
|
|
3
3
|
import type { ConversationManager } from '../core/conversation';
|
|
4
4
|
import { getOverlayContentBudget, getOverlaySurfaceMetrics, getStableOverlayContentRows } from './overlay-viewport.ts';
|
|
5
5
|
import { estimateTokens } from '@pellux/goodvibes-sdk/platform/core';
|
|
6
|
+
import { UI_TONES } from './ui-primitives.ts';
|
|
6
7
|
|
|
7
8
|
// ─── ContextInspectorModal ────────────────────────────────────────────────────
|
|
8
9
|
|
|
@@ -84,7 +85,6 @@ export function renderContextInspector(
|
|
|
84
85
|
|
|
85
86
|
const entries: MsgEntry[] = [];
|
|
86
87
|
let totalTokens = 0;
|
|
87
|
-
let largeCount = 0;
|
|
88
88
|
|
|
89
89
|
for (const msg of messages) {
|
|
90
90
|
const role = msg.role;
|
|
@@ -126,9 +126,6 @@ export function renderContextInspector(
|
|
|
126
126
|
// ── Identify large consumers (>10%) ───────────────────────────────────────
|
|
127
127
|
|
|
128
128
|
const largeThreshold = totalTokens * 0.10;
|
|
129
|
-
for (const e of entries) {
|
|
130
|
-
if (e.tokens > largeThreshold) largeCount++;
|
|
131
|
-
}
|
|
132
129
|
|
|
133
130
|
// ── Build sections ────────────────────────────────────────────────────────
|
|
134
131
|
|
|
@@ -148,7 +145,7 @@ export function renderContextInspector(
|
|
|
148
145
|
sections.push({
|
|
149
146
|
type: 'text',
|
|
150
147
|
content: 'WARNING: context is 80%+ full. Run /compact to free space.',
|
|
151
|
-
style: { fg:
|
|
148
|
+
style: { fg: UI_TONES.state.warn, bold: true },
|
|
152
149
|
});
|
|
153
150
|
}
|
|
154
151
|
|
|
@@ -181,7 +178,7 @@ export function renderContextInspector(
|
|
|
181
178
|
sections.push({
|
|
182
179
|
type: 'text',
|
|
183
180
|
content: line,
|
|
184
|
-
style: isLarge ? { fg:
|
|
181
|
+
style: isLarge ? { fg: UI_TONES.state.warn, bold: true } : {},
|
|
185
182
|
});
|
|
186
183
|
}
|
|
187
184
|
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { type Line, createEmptyLine, createStyledCell } from '../types/grid.ts';
|
|
2
|
-
import { getDisplayWidth,
|
|
2
|
+
import { getDisplayWidth, wrapText } from '../utils/terminal-width.ts';
|
|
3
3
|
import { LAYOUT } from './layout.ts';
|
|
4
4
|
import { GLYPHS } from './ui-primitives.ts';
|
|
5
5
|
|
|
@@ -185,26 +185,6 @@ export function renderConversationCollapsedFragment(
|
|
|
185
185
|
});
|
|
186
186
|
}
|
|
187
187
|
|
|
188
|
-
export function renderConversationKeyValueRow(
|
|
189
|
-
width: number,
|
|
190
|
-
left: string,
|
|
191
|
-
right: string,
|
|
192
|
-
palette: {
|
|
193
|
-
readonly leftFg: string;
|
|
194
|
-
readonly rightFg: string;
|
|
195
|
-
readonly dimFg?: string;
|
|
196
|
-
readonly bg?: string;
|
|
197
|
-
},
|
|
198
|
-
): Line {
|
|
199
|
-
const line = createEmptyLine(width);
|
|
200
|
-
const leftText = truncateDisplay(left, Math.max(1, width - LAYOUT.RIGHT_MARGIN - 8));
|
|
201
|
-
const rightWidth = getDisplayWidth(right);
|
|
202
|
-
const rightStart = Math.max(LAYOUT.LEFT_MARGIN + 1, width - LAYOUT.RIGHT_MARGIN - rightWidth);
|
|
203
|
-
writeText(line, LAYOUT.LEFT_MARGIN, rightStart - 1, leftText, palette.leftFg, { bg: palette.bg });
|
|
204
|
-
writeText(line, rightStart, width - LAYOUT.RIGHT_MARGIN, right, palette.rightFg, { bg: palette.bg, dim: palette.dimFg === palette.rightFg });
|
|
205
|
-
return line;
|
|
206
|
-
}
|
|
207
|
-
|
|
208
188
|
export function renderConversationStatusLine(
|
|
209
189
|
width: number,
|
|
210
190
|
segments: readonly ConversationStatusSegment[],
|
|
@@ -1,10 +1,12 @@
|
|
|
1
1
|
import { type Line, type Cell, createStyledCell } from '../types/grid.ts';
|
|
2
2
|
import { UIFactory } from './ui-factory.ts';
|
|
3
3
|
import { getDisplayWidth, padDisplayEnd } from '../utils/terminal-width.ts';
|
|
4
|
+
import { DIFF_TONES } from './ui-primitives.ts';
|
|
4
5
|
|
|
5
6
|
/**
|
|
6
7
|
* renderDiffView - Render a unified diff string as styled Line[].
|
|
7
|
-
* '+' lines in green, '-' lines in red, '@@' hunks in
|
|
8
|
+
* '+' lines in green, '-' lines in red, '@@' hunks in blue (DIFF_TONES.hunk,
|
|
9
|
+
* shared with diff-panel.ts and git-panel.ts's inline diff).
|
|
8
10
|
*/
|
|
9
11
|
export function renderDiffView(diffText: string, width: number, filename?: string): Line[] {
|
|
10
12
|
const lines: Line[] = [];
|
|
@@ -34,7 +36,7 @@ export function renderDiffView(diffText: string, width: number, filename?: strin
|
|
|
34
36
|
oldLineNo = parseInt(hunkMatch[1], 10) - 1;
|
|
35
37
|
newLineNo = parseInt(hunkMatch[2], 10) - 1;
|
|
36
38
|
}
|
|
37
|
-
lines.push(makeStyledLine(raw, width,
|
|
39
|
+
lines.push(makeStyledLine(raw, width, DIFF_TONES.hunk, '#0f1f1f', false));
|
|
38
40
|
continue;
|
|
39
41
|
}
|
|
40
42
|
|
|
@@ -49,7 +51,7 @@ export function renderDiffView(diffText: string, width: number, filename?: strin
|
|
|
49
51
|
newLineNo++;
|
|
50
52
|
const lineLabel = `${String(newLineNo).padStart(4)} `;
|
|
51
53
|
const content = raw.slice(1);
|
|
52
|
-
lines.push(makeGutterLine('+', lineLabel, content, width,
|
|
54
|
+
lines.push(makeGutterLine('+', lineLabel, content, width, DIFF_TONES.add, '#0a1a0a'));
|
|
53
55
|
continue;
|
|
54
56
|
}
|
|
55
57
|
|
|
@@ -58,7 +60,7 @@ export function renderDiffView(diffText: string, width: number, filename?: strin
|
|
|
58
60
|
oldLineNo++;
|
|
59
61
|
const lineLabel = `${String(oldLineNo).padStart(4)} `;
|
|
60
62
|
const content = raw.slice(1);
|
|
61
|
-
lines.push(makeGutterLine('-', lineLabel, content, width,
|
|
63
|
+
lines.push(makeGutterLine('-', lineLabel, content, width, DIFF_TONES.del, '#1a0a0a'));
|
|
62
64
|
continue;
|
|
63
65
|
}
|
|
64
66
|
|
|
@@ -107,7 +107,7 @@ export function drawHorizontalRule(line: Line, startX: number, endX: number, fg:
|
|
|
107
107
|
}
|
|
108
108
|
}
|
|
109
109
|
|
|
110
|
-
|
|
110
|
+
function clipDisplay(text: string, width: number): string {
|
|
111
111
|
if (width <= 0) return '';
|
|
112
112
|
let used = 0;
|
|
113
113
|
let output = '';
|
|
@@ -13,14 +13,9 @@ import {
|
|
|
13
13
|
} from './fullscreen-primitives.ts';
|
|
14
14
|
|
|
15
15
|
export {
|
|
16
|
-
borderLine,
|
|
17
16
|
clamp,
|
|
18
|
-
contentLine,
|
|
19
|
-
fillRange,
|
|
20
|
-
makeLine,
|
|
21
17
|
padDisplay,
|
|
22
18
|
stableWindow,
|
|
23
|
-
writeText,
|
|
24
19
|
} from './fullscreen-primitives.ts';
|
|
25
20
|
export { FULLSCREEN_PALETTE as WORKSPACE_PALETTE } from './fullscreen-primitives.ts';
|
|
26
21
|
|
|
@@ -63,12 +58,12 @@ export interface FullscreenWorkspaceMetrics {
|
|
|
63
58
|
readonly controlRows: number;
|
|
64
59
|
}
|
|
65
60
|
|
|
66
|
-
|
|
61
|
+
function drawVertical(line: Line, x: number, bg = ''): void {
|
|
67
62
|
if (x <= 0 || x >= line.length - 1) return;
|
|
68
63
|
drawVerticalRule(line, x, WORKSPACE_PALETTE.border, bg);
|
|
69
64
|
}
|
|
70
65
|
|
|
71
|
-
|
|
66
|
+
function drawHorizontalRange(line: Line, startX: number, endX: number, bg = ''): void {
|
|
72
67
|
drawHorizontalRule(line, Math.max(1, startX), Math.min(line.length - 2, endX), WORKSPACE_PALETTE.border, bg);
|
|
73
68
|
}
|
|
74
69
|
|
|
@@ -28,7 +28,7 @@ function isEscapeHint(spec: HintSpec): boolean {
|
|
|
28
28
|
}
|
|
29
29
|
|
|
30
30
|
/** Render one hint as `[Key] Verb` (or `[Key]` when it has no verb). */
|
|
31
|
-
|
|
31
|
+
function formatHint(spec: HintSpec): string {
|
|
32
32
|
return spec.verb ? `[${spec.key}] ${spec.verb}` : `[${spec.key}]`;
|
|
33
33
|
}
|
|
34
34
|
|
|
@@ -1,27 +1,15 @@
|
|
|
1
1
|
import type { Line } from '../types/grid.ts';
|
|
2
|
-
import { getDisplayWidth } from '../utils/terminal-width.ts';
|
|
2
|
+
import { fitDisplay, getDisplayWidth } from '../utils/terminal-width.ts';
|
|
3
3
|
import type { HistorySearch } from '../input/input-history.ts';
|
|
4
4
|
import { createBottomBarLine, writeBottomBarText } from './bottom-bar.ts';
|
|
5
|
+
import { formatHints } from './hint-grammar.ts';
|
|
5
6
|
|
|
6
7
|
/**
|
|
7
8
|
* Truncate `text` to at most `maxWidth` display columns, then pad with spaces
|
|
8
9
|
* to exactly `maxWidth` columns. CJK/emoji wide characters count as 2 columns.
|
|
9
10
|
*/
|
|
10
11
|
function truncateToWidth(text: string, maxWidth: number): string {
|
|
11
|
-
|
|
12
|
-
let result = '';
|
|
13
|
-
let i = 0;
|
|
14
|
-
while (i < text.length) {
|
|
15
|
-
const code = text.codePointAt(i)!;
|
|
16
|
-
const charLen = code > 0xFFFF ? 2 : 1;
|
|
17
|
-
const charWidth = getDisplayWidth(text.slice(i, i + charLen));
|
|
18
|
-
if (usedWidth + charWidth > maxWidth) break;
|
|
19
|
-
result += text.slice(i, i + charLen);
|
|
20
|
-
usedWidth += charWidth;
|
|
21
|
-
i += charLen;
|
|
22
|
-
}
|
|
23
|
-
// Pad to exactly maxWidth columns with spaces
|
|
24
|
-
return result + ' '.repeat(maxWidth - usedWidth);
|
|
12
|
+
return fitDisplay(text, maxWidth, '');
|
|
25
13
|
}
|
|
26
14
|
|
|
27
15
|
/**
|
|
@@ -49,7 +37,13 @@ export function renderHistorySearchOverlay(
|
|
|
49
37
|
|
|
50
38
|
// Build the display string
|
|
51
39
|
const label = prefix + queryPart;
|
|
52
|
-
const
|
|
40
|
+
const hints = formatHints([
|
|
41
|
+
{ key: 'Ctrl+R/↑', verb: 'Older' },
|
|
42
|
+
{ key: 'Ctrl+S/↓', verb: 'Newer' },
|
|
43
|
+
{ key: 'Enter', verb: 'Accept' },
|
|
44
|
+
{ key: 'Esc', verb: 'Cancel' },
|
|
45
|
+
]);
|
|
46
|
+
const full = truncateToWidth(`${label}${matchText} ${hints}`, width);
|
|
53
47
|
|
|
54
48
|
const line = createBottomBarLine(width, { fg: '#000000', bg: '#00ffcc' });
|
|
55
49
|
writeBottomBarText(line, 0, width, full, { fg: '#000000', bg: '#00ffcc' });
|
package/src/renderer/markdown.ts
CHANGED
|
@@ -310,7 +310,15 @@ function renderTable(rows: string[], width: number, indent: number): Line[] {
|
|
|
310
310
|
for (const ch of text) {
|
|
311
311
|
const cw = getDisplayWidth(ch);
|
|
312
312
|
if (w + cw > maxW) {
|
|
313
|
-
// Truncate with ellipsis
|
|
313
|
+
// Truncate with ellipsis. If the last cell pushed is a wide-glyph
|
|
314
|
+
// placeholder (the empty second column of a 2-wide character),
|
|
315
|
+
// overwriting it in place would leave the wide glyph's first
|
|
316
|
+
// column dangling next to the ellipsis with no placeholder cell \u2014
|
|
317
|
+
// step back one more cell so the ellipsis replaces the actual
|
|
318
|
+
// glyph, not its placeholder.
|
|
319
|
+
if (cells.length > 0 && cells[cells.length - 1].char === '' && cells.length > 1) {
|
|
320
|
+
cells.pop();
|
|
321
|
+
}
|
|
314
322
|
if (cells.length > 0) cells[cells.length - 1] = createStyledCell('\u2026', cells[cells.length - 1]);
|
|
315
323
|
return cells;
|
|
316
324
|
}
|