@pellux/goodvibes-tui 0.29.0 → 1.0.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 +32 -0
- package/README.md +1 -1
- package/package.json +1 -1
- package/src/core/conversation-line-cache.ts +432 -0
- package/src/core/conversation-rendering.ts +11 -3
- package/src/core/conversation.ts +50 -4
- package/src/input/handler-content-actions.ts +8 -3
- package/src/input/input-history.ts +17 -4
- package/src/main.ts +14 -10
- package/src/panels/base-panel.ts +1 -1
- package/src/panels/diff-panel.ts +10 -4
- package/src/panels/git-panel.ts +2 -2
- package/src/renderer/agent-detail-modal.ts +37 -8
- 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/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 +21 -8
- 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 +7 -7
- package/src/renderer/ui-primitives.ts +18 -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
|
@@ -351,15 +351,28 @@ export class InputHistory {
|
|
|
351
351
|
return redactedText;
|
|
352
352
|
}
|
|
353
353
|
|
|
354
|
+
/**
|
|
355
|
+
* Entries persisted before CR normalization was deployed (registerPaste now
|
|
356
|
+
* converts the \r line separators terminals send in bracketed pastes to \n)
|
|
357
|
+
* may still carry literal \r bytes; launder them on load so history recall
|
|
358
|
+
* cannot reintroduce \r into the composer.
|
|
359
|
+
*/
|
|
360
|
+
private launderLineBreaks(text: string): string {
|
|
361
|
+
return text.replace(/\r\n?/g, '\n');
|
|
362
|
+
}
|
|
363
|
+
|
|
354
364
|
private normalizeStoredEntry(entry: unknown): StoredInputHistoryEntry | null {
|
|
355
|
-
if (typeof entry === 'string') return entry;
|
|
365
|
+
if (typeof entry === 'string') return this.launderLineBreaks(entry);
|
|
356
366
|
if (!entry || typeof entry !== 'object') return null;
|
|
357
367
|
const record = entry as Record<string, unknown>;
|
|
358
368
|
if (typeof record.text !== 'string') return null;
|
|
359
|
-
const text = record.text.trim();
|
|
369
|
+
const text = this.launderLineBreaks(record.text).trim();
|
|
360
370
|
if (!text) return null;
|
|
361
|
-
|
|
362
|
-
|
|
371
|
+
const recallText = typeof record.recallText === 'string'
|
|
372
|
+
? this.launderLineBreaks(record.recallText).trim()
|
|
373
|
+
: '';
|
|
374
|
+
if (recallText && recallText !== text) {
|
|
375
|
+
return { text, recallText };
|
|
363
376
|
}
|
|
364
377
|
return text;
|
|
365
378
|
}
|
package/src/main.ts
CHANGED
|
@@ -24,6 +24,7 @@ import { GitStatusProvider } from './renderer/git-status.ts';
|
|
|
24
24
|
import type { GitHeaderInfo } from './renderer/git-status.ts';
|
|
25
25
|
import { createShellLayout } from './renderer/layout-engine.ts';
|
|
26
26
|
import { buildShellFooter, estimateShellFooterHeight } from './renderer/shell-surface.ts';
|
|
27
|
+
import { computePromptContentWidth } from './renderer/prompt-content-width.ts';
|
|
27
28
|
import { buildConversationViewport } from './renderer/conversation-layout.ts';
|
|
28
29
|
import { applyConversationOverlays } from './renderer/conversation-overlays.ts';
|
|
29
30
|
import { buildPanelCompositeData } from './renderer/panel-composite.ts';
|
|
@@ -53,6 +54,7 @@ import { wireSpokenTurnRuntime } from './audio/spoken-turn-wiring.ts';
|
|
|
53
54
|
import { attachSpokenTurnModelRouting, createSpokenTurnInputOptions } from './audio/spoken-turn-model-routing.ts';
|
|
54
55
|
import { allowTerminalWrite, installTuiTerminalOutputGuard } from './runtime/terminal-output-guard.ts';
|
|
55
56
|
import { installProcessLifecycle } from './runtime/process-lifecycle.ts';
|
|
57
|
+
import { createRenderScheduler } from './runtime/render-scheduler.ts';
|
|
56
58
|
import { buildCommandArgsHint } from './input/command-args-hint.ts';
|
|
57
59
|
import { summarizeRunningAgents } from './renderer/process-summary.ts';
|
|
58
60
|
import { formatUserFacingErrorLine } from './core/format-user-error.ts';
|
|
@@ -180,19 +182,19 @@ async function main() {
|
|
|
180
182
|
activeToolName: undefined,
|
|
181
183
|
};
|
|
182
184
|
|
|
183
|
-
const getPromptContentWidth = () =>
|
|
184
|
-
const w = stdout.columns || 80;
|
|
185
|
-
const boxMargin = 2;
|
|
186
|
-
const boxWidth = w - (boxMargin * 2);
|
|
187
|
-
return boxWidth - 4 - 3; // minus padding (4) minus prefix width (3: ' > ')
|
|
188
|
-
};
|
|
185
|
+
const getPromptContentWidth = () => computePromptContentWidth(stdout.columns);
|
|
189
186
|
|
|
190
187
|
const getViewportHeight = (): number => {
|
|
191
188
|
if (input.onboardingWizard.active) return stdout.rows || 24;
|
|
192
189
|
const promptLines: number = input.getVisiblePromptLineCount(getPromptContentWidth());
|
|
193
190
|
const currentModel = providerRegistry.getCurrentModel();
|
|
194
191
|
const contextWindow = providerRegistry.getContextWindowForModel(currentModel);
|
|
195
|
-
|
|
192
|
+
const rows = stdout.rows || 24;
|
|
193
|
+
// Compact threshold must match buildShellFooter's `compact: height < 30`
|
|
194
|
+
// posture below — otherwise the cached-height fast path in
|
|
195
|
+
// estimateShellFooterHeight can answer a compact-vs-non-compact query
|
|
196
|
+
// with the wrong cached mode and throw the viewport math off by several rows.
|
|
197
|
+
return rows - 2 - estimateShellFooterHeight(promptLines, contextWindow, rows < 30);
|
|
196
198
|
};
|
|
197
199
|
|
|
198
200
|
const scroll = (delta: number) => {
|
|
@@ -223,7 +225,7 @@ async function main() {
|
|
|
223
225
|
noAltScreen: cli.flags.noAltScreen,
|
|
224
226
|
ansi: { CLEAR_SCREEN, ALT_SCREEN_EXIT, PASTE_DISABLE, KEYBOARD_EXT_DISABLE, MOUSE_DISABLE, CURSOR_SHOW },
|
|
225
227
|
getInput: () => input,
|
|
226
|
-
render: () =>
|
|
228
|
+
render: () => renderScheduler.flushNow(), // resize: synchronous immediate path
|
|
227
229
|
getPromptContentWidth,
|
|
228
230
|
getTerminalOutputGuard: () => terminalOutputGuard,
|
|
229
231
|
buildSessionContinuityHints,
|
|
@@ -432,7 +434,7 @@ async function main() {
|
|
|
432
434
|
lastSessionId: readLastSessionPointer({ workingDirectory: workingDir, homeDirectory, surfaceRoot: 'tui' }) ?? undefined,
|
|
433
435
|
};
|
|
434
436
|
|
|
435
|
-
const
|
|
437
|
+
const renderNow = () => {
|
|
436
438
|
const width = stdout.columns || 80;
|
|
437
439
|
const height = stdout.rows || 24;
|
|
438
440
|
|
|
@@ -648,9 +650,11 @@ async function main() {
|
|
|
648
650
|
panelWidth: panelComposite.panelWidth,
|
|
649
651
|
});
|
|
650
652
|
};
|
|
653
|
+
const renderScheduler = createRenderScheduler(renderNow); // WO-208 same-tick coalescer
|
|
654
|
+
const render = (): void => renderScheduler.schedule();
|
|
651
655
|
const terminalOutputGuard = installTuiTerminalOutputGuard({ stdout, stderr: process.stderr, notify: (message) => { systemMessageRouter.low(message); render(); } });
|
|
652
656
|
|
|
653
|
-
setRenderRequest(
|
|
657
|
+
setRenderRequest(renderNow); // bootstrap's 16ms coalescer calls the composite directly
|
|
654
658
|
orchestratorRefs.requestRender = render;
|
|
655
659
|
commandContext.renderRequest = render;
|
|
656
660
|
wireShellUiOpeners({
|
package/src/panels/base-panel.ts
CHANGED
|
@@ -3,7 +3,7 @@ import type { Panel, PanelCategory } from './types.ts';
|
|
|
3
3
|
import type { ComponentResourceContract, ComponentHealthState } from '../runtime/perf/panel-contracts.ts';
|
|
4
4
|
import type { ComponentHealthMonitor } from '../runtime/perf/panel-health-monitor.ts';
|
|
5
5
|
import { UIFactory } from '../renderer/ui-factory.ts';
|
|
6
|
-
import { SPINNER_FRAMES } from '../renderer/
|
|
6
|
+
import { SPINNER_FRAMES } from '../renderer/ui-primitives.ts';
|
|
7
7
|
import { fitDisplay } from '../utils/terminal-width.ts';
|
|
8
8
|
|
|
9
9
|
/** Canonical error-surface foreground (bad/red), kept out of the render body. */
|
package/src/panels/diff-panel.ts
CHANGED
|
@@ -6,7 +6,7 @@ import type { Line } from '../types/grid.ts';
|
|
|
6
6
|
import { createStyledCell, createEmptyLine } from '../types/grid.ts';
|
|
7
7
|
import { truncateDisplay, getDisplayWidth } from '../utils/terminal-width.ts';
|
|
8
8
|
import { BasePanel } from './base-panel.ts';
|
|
9
|
-
import { UI_TONES } from '../renderer/ui-primitives.ts';
|
|
9
|
+
import { UI_TONES, DIFF_TONES } from '../renderer/ui-primitives.ts';
|
|
10
10
|
import { FilePreviewPanel } from './file-preview-panel.ts';
|
|
11
11
|
import type { PanelIntegrationContext } from './types.ts';
|
|
12
12
|
import { logger } from '@pellux/goodvibes-sdk/platform/utils';
|
|
@@ -30,14 +30,20 @@ import {
|
|
|
30
30
|
// DEFAULT_PANEL_PALETTE.headerBg (WO-002: one title band everywhere).
|
|
31
31
|
// ---------------------------------------------------------------------------
|
|
32
32
|
|
|
33
|
-
|
|
33
|
+
// Hunk blue is the shared DIFF_TONES token (WO-204) — diff-view.ts (conversation)
|
|
34
|
+
// and git-panel.ts's inline diff converge onto this file's pre-existing value.
|
|
35
|
+
const HUNK_BLUE: string = DIFF_TONES.hunk;
|
|
34
36
|
// Context rows and line-number gutter use the shared theme's muted/dim
|
|
35
37
|
// foreground tones rather than dedicated gray hex literals.
|
|
36
38
|
const CONTEXT_GRAY = UI_TONES.fg.muted;
|
|
37
39
|
const FILENAME_WHITE = '#ffffff';
|
|
38
|
-
|
|
40
|
+
// Add/del text colors are the shared DIFF_TONES tokens, whose values ARE this
|
|
41
|
+
// panel's shipped colors — this panel is the reference diff look (diff-view
|
|
42
|
+
// was unwired dead code until WO-204, so "majority of surfaces" was a mirage);
|
|
43
|
+
// the conversation surface converges onto these, not the reverse.
|
|
44
|
+
const ADD_GREEN: string = DIFF_TONES.add;
|
|
39
45
|
const ADD_BG = '#001a0d';
|
|
40
|
-
const DEL_RED =
|
|
46
|
+
const DEL_RED: string = DIFF_TONES.del;
|
|
41
47
|
const DEL_BG = '#1a0000';
|
|
42
48
|
const HUNK_BG = '#0a0a1a';
|
|
43
49
|
const MARKER_GRAY = '#aaaaaa';
|
package/src/panels/git-panel.ts
CHANGED
|
@@ -4,7 +4,7 @@ import { truncateDisplay, getDisplayWidth } from '../utils/terminal-width.ts';
|
|
|
4
4
|
import { GitService } from '@pellux/goodvibes-sdk/platform/git';
|
|
5
5
|
import { logger } from '@pellux/goodvibes-sdk/platform/utils';
|
|
6
6
|
import { summarizeError } from '@pellux/goodvibes-sdk/platform/utils';
|
|
7
|
-
import { UI_TONES } from '../renderer/ui-primitives.ts';
|
|
7
|
+
import { UI_TONES, DIFF_TONES } from '../renderer/ui-primitives.ts';
|
|
8
8
|
import {
|
|
9
9
|
buildEmptyState,
|
|
10
10
|
buildKeyboardHints,
|
|
@@ -77,7 +77,7 @@ const C = extendPalette(DEFAULT_PANEL_PALETTE, {
|
|
|
77
77
|
commitAuthor: '244',
|
|
78
78
|
selected: '#1c1c1c',
|
|
79
79
|
selectedFg: '#ffffff',
|
|
80
|
-
diffMeta:
|
|
80
|
+
diffMeta: DIFF_TONES.hunk, // WO-204: shared diff-hunk token, was a local literal
|
|
81
81
|
diffNeutral: '250',
|
|
82
82
|
// Reuses the existing workflow accent token rather than adding a new hex
|
|
83
83
|
// literal (architecture gate ratchets the raw-hex-literal count).
|
|
@@ -10,6 +10,24 @@ import { getOverlaySurfaceMetrics, getStableOverlayContentRows } from './overlay
|
|
|
10
10
|
import { summarizeError } from '@pellux/goodvibes-sdk/platform/utils';
|
|
11
11
|
import { handleConfirmInput, type ConfirmState } from '../panels/confirm-state.ts';
|
|
12
12
|
import { AGENT_TERMINAL_STATUSES as MODAL_TERMINAL_STATUSES, AGENT_STALL_THRESHOLD_MS as MODAL_STALL_THRESHOLD_MS } 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,7 +277,7 @@ 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
|
|
|
@@ -271,7 +300,7 @@ export function renderAgentDetailModal(
|
|
|
271
300
|
sections.push({
|
|
272
301
|
type: 'text',
|
|
273
302
|
content: 'Addendum : yes (WRFC constraint layer injected)',
|
|
274
|
-
style: { fg:
|
|
303
|
+
style: { fg: AGENT_DETAIL_NEON.mint },
|
|
275
304
|
});
|
|
276
305
|
}
|
|
277
306
|
|
|
@@ -301,7 +330,7 @@ export function renderAgentDetailModal(
|
|
|
301
330
|
sections.push({
|
|
302
331
|
type: 'text',
|
|
303
332
|
content: `Findings : ${findings.length} checked, ${unsatisfied.length} unsatisfied`,
|
|
304
|
-
style: { fg: unsatisfied.length > 0 ?
|
|
333
|
+
style: { fg: unsatisfied.length > 0 ? AGENT_DETAIL_NEON.bad : AGENT_DETAIL_NEON.good },
|
|
305
334
|
});
|
|
306
335
|
}
|
|
307
336
|
}
|
|
@@ -316,7 +345,7 @@ export function renderAgentDetailModal(
|
|
|
316
345
|
sections.push({
|
|
317
346
|
type: 'text',
|
|
318
347
|
content: `Progress: ${rec.progress}`,
|
|
319
|
-
style: { fg:
|
|
348
|
+
style: { fg: AGENT_DETAIL_NEON.accent },
|
|
320
349
|
});
|
|
321
350
|
}
|
|
322
351
|
|
|
@@ -326,7 +355,7 @@ export function renderAgentDetailModal(
|
|
|
326
355
|
sections.push({
|
|
327
356
|
type: 'text',
|
|
328
357
|
content: `Error: ${rec.error}`,
|
|
329
|
-
style: { fg:
|
|
358
|
+
style: { fg: AGENT_DETAIL_NEON.bad },
|
|
330
359
|
});
|
|
331
360
|
}
|
|
332
361
|
|
|
@@ -393,7 +422,7 @@ export function renderAgentDetailModal(
|
|
|
393
422
|
content: truncated
|
|
394
423
|
? `Streaming (last ${STREAMING_MAX_CHARS} of ${content.length} chars \u2191 scroll for more):`
|
|
395
424
|
: 'Streaming:',
|
|
396
|
-
style: { fg:
|
|
425
|
+
style: { fg: AGENT_DETAIL_NEON.accent, dim: true },
|
|
397
426
|
});
|
|
398
427
|
// Split into display lines, capped at width for readability
|
|
399
428
|
const maxLineWidth = Math.max(width - 10, 40);
|
|
@@ -403,7 +432,7 @@ export function renderAgentDetailModal(
|
|
|
403
432
|
sections.push({
|
|
404
433
|
type: 'text',
|
|
405
434
|
content: ` ${trimmed}`,
|
|
406
|
-
style: { fg:
|
|
435
|
+
style: { fg: AGENT_DETAIL_NEON.mint },
|
|
407
436
|
});
|
|
408
437
|
}
|
|
409
438
|
}
|
|
@@ -415,7 +444,7 @@ export function renderAgentDetailModal(
|
|
|
415
444
|
sections.push({
|
|
416
445
|
type: 'text',
|
|
417
446
|
content: `Cancel agent "${modal.confirmCancel.label}"?`,
|
|
418
|
-
style: { fg:
|
|
447
|
+
style: { fg: UI_TONES.state.warn },
|
|
419
448
|
});
|
|
420
449
|
sections.push({
|
|
421
450
|
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 = '';
|