@pellux/goodvibes-tui 0.28.0 → 0.29.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 +101 -0
- package/README.md +1 -1
- package/package.json +1 -1
- package/src/input/commands/cost-runtime.ts +49 -0
- package/src/input/commands/operator-runtime.ts +5 -1
- package/src/input/commands.ts +2 -0
- package/src/input/handler-feed-routes.ts +12 -35
- package/src/input/handler-feed.ts +4 -8
- package/src/input/handler-shortcuts.ts +51 -0
- package/src/input/handler.ts +43 -6
- package/src/input/keybindings.ts +48 -8
- package/src/input/panel-integration-actions.ts +107 -1
- package/src/main.ts +5 -1
- package/src/panels/agent-inspector-panel.ts +125 -36
- package/src/panels/agent-inspector-shared.ts +144 -0
- package/src/panels/approval-panel.ts +67 -16
- package/src/panels/automation-control-panel.ts +368 -124
- package/src/panels/builtin/agent.ts +28 -37
- package/src/panels/builtin/development.ts +40 -32
- package/src/panels/builtin/knowledge.ts +15 -6
- package/src/panels/builtin/operations.ts +178 -117
- package/src/panels/builtin/session.ts +35 -9
- package/src/panels/builtin/shared.ts +98 -6
- package/src/panels/cockpit-panel.ts +232 -73
- package/src/panels/communication-panel.ts +58 -20
- package/src/panels/confirm-state.ts +8 -1
- package/src/panels/control-plane-panel.ts +351 -118
- package/src/panels/cost-tracker-panel.ts +165 -7
- package/src/panels/debug-panel.ts +312 -159
- package/src/panels/diff-panel.ts +203 -57
- package/src/panels/docs-panel.ts +152 -66
- package/src/panels/eval-panel.ts +220 -42
- package/src/panels/file-explorer-panel.ts +202 -122
- package/src/panels/file-preview-panel.ts +132 -57
- package/src/panels/git-panel.ts +309 -128
- package/src/panels/hooks-panel.ts +150 -22
- package/src/panels/incident-review-panel.ts +223 -35
- package/src/panels/index.ts +0 -4
- package/src/panels/intelligence-panel.ts +212 -86
- package/src/panels/knowledge-graph-panel.ts +461 -101
- package/src/panels/local-auth-panel.ts +240 -28
- package/src/panels/marketplace-panel.ts +193 -26
- package/src/panels/memory-panel.ts +78 -77
- package/src/panels/ops-control-panel.ts +146 -29
- package/src/panels/ops-strategy-panel.ts +72 -4
- package/src/panels/orchestration-panel.ts +231 -69
- package/src/panels/panel-list-panel.ts +154 -131
- package/src/panels/panel-manager.ts +97 -9
- package/src/panels/plan-dashboard-panel.ts +333 -17
- package/src/panels/plugins-panel.ts +184 -29
- package/src/panels/policy-panel.ts +210 -38
- package/src/panels/polish-core.ts +7 -2
- package/src/panels/polish.ts +23 -4
- package/src/panels/project-planning-answer-actions.ts +134 -0
- package/src/panels/project-planning-panel.ts +62 -113
- package/src/panels/provider-health-panel.ts +434 -518
- package/src/panels/provider-health-routes.ts +203 -0
- package/src/panels/provider-health-tracker.ts +194 -6
- package/src/panels/provider-health-views.ts +560 -0
- package/src/panels/qr-panel.ts +116 -30
- package/src/panels/remote-panel.ts +114 -36
- package/src/panels/routes-panel.ts +63 -22
- package/src/panels/sandbox-panel.ts +174 -42
- package/src/panels/scrollable-list-panel.ts +19 -135
- package/src/panels/security-panel.ts +133 -33
- package/src/panels/services-panel.ts +116 -64
- package/src/panels/session-browser-panel.ts +73 -2
- package/src/panels/session-maintenance.ts +4 -122
- package/src/panels/settings-sync-panel.ts +335 -72
- package/src/panels/skills-panel.ts +157 -89
- package/src/panels/subscription-panel.ts +86 -33
- package/src/panels/symbol-outline-panel.ts +248 -108
- package/src/panels/system-messages-panel.ts +114 -13
- package/src/panels/tasks-panel.ts +326 -139
- package/src/panels/thinking-panel.ts +43 -10
- package/src/panels/token-budget-panel.ts +194 -18
- package/src/panels/tool-inspector-panel.ts +144 -34
- package/src/panels/types.ts +40 -4
- package/src/panels/work-plan-panel.ts +280 -17
- package/src/panels/worktree-panel.ts +175 -42
- package/src/panels/wrfc-panel.ts +116 -24
- package/src/renderer/conversation-overlays.ts +25 -11
- package/src/renderer/footer-tips.ts +41 -0
- package/src/renderer/fullscreen-primitives.ts +22 -16
- package/src/renderer/help-overlay.ts +91 -14
- package/src/renderer/hint-grammar.ts +52 -0
- package/src/renderer/layout.ts +7 -7
- package/src/renderer/modal-factory.ts +23 -15
- package/src/renderer/model-picker-overlay.ts +21 -7
- package/src/renderer/overlay-box.ts +16 -10
- package/src/renderer/process-indicator.ts +3 -1
- package/src/renderer/progress.ts +5 -4
- package/src/renderer/search-overlay.ts +27 -6
- package/src/renderer/session-picker-modal.ts +6 -4
- package/src/renderer/settings-modal.ts +70 -10
- package/src/renderer/shell-surface.ts +41 -15
- package/src/renderer/status-glyphs.ts +11 -9
- package/src/renderer/theme.ts +60 -3
- package/src/renderer/ui-factory.ts +41 -30
- package/src/renderer/ui-primitives.ts +23 -2
- package/src/runtime/bootstrap-shell.ts +35 -18
- package/src/runtime/diagnostics/panels/index.ts +0 -3
- package/src/shell/ui-openers.ts +14 -22
- package/src/utils/format-duration.ts +2 -2
- package/src/utils/splash-lines.ts +44 -3
- package/src/version.ts +1 -1
- package/src/work-plans/work-plan-store.ts +13 -0
- package/src/panels/agent-logs-panel.ts +0 -635
- package/src/panels/agent-logs-shared.ts +0 -129
- package/src/panels/context-visualizer-panel.ts +0 -238
- package/src/panels/forensics-panel.ts +0 -378
- package/src/panels/provider-account-snapshot.ts +0 -259
- package/src/panels/provider-accounts-panel.ts +0 -255
- package/src/panels/provider-stats-panel.ts +0 -391
- package/src/panels/schedule-panel.ts +0 -365
- package/src/panels/watchers-panel.ts +0 -213
- package/src/runtime/diagnostics/panels/panel-resources.ts +0 -118
- package/src/runtime/diagnostics/panels/policy.ts +0 -177
|
@@ -25,18 +25,39 @@ export function renderSearchOverlay(
|
|
|
25
25
|
const locked = manager.locked;
|
|
26
26
|
const cursor = locked ? '' : '█';
|
|
27
27
|
const queryDisplay = manager.query + cursor;
|
|
28
|
-
const
|
|
28
|
+
const label = ' Find: ';
|
|
29
|
+
const fullHints = locked
|
|
29
30
|
? ' [n/N] next/prev [jk] navigate [Bksp] edit [Esc] close'
|
|
30
31
|
: ' [Enter/Tab] lock [Esc] close';
|
|
31
|
-
const
|
|
32
|
-
const matchStr = matchCount ? ` ${matchCount}` : '';
|
|
32
|
+
const shortHints = locked ? ' [n/N] [Esc]' : ' [Esc]';
|
|
33
33
|
|
|
34
34
|
// Build left portion: label + query (no match count — that gets separate styling)
|
|
35
35
|
const leftPart = label + queryDisplay;
|
|
36
|
-
|
|
36
|
+
|
|
37
|
+
// Tiered degradation: as the terminal narrows, first shorten the hints, then
|
|
38
|
+
// drop them, then drop the match-count segment — so the query area (leftWidth)
|
|
39
|
+
// never collapses to zero or negative and the overlay never renders garbage.
|
|
40
|
+
const minLeft = getDisplayWidth(label) + 4; // label + a few query cells
|
|
41
|
+
let hints = fullHints;
|
|
42
|
+
let matchStr = matchCount ? ` ${matchCount}` : '';
|
|
43
|
+
const leftFor = (h: string, m: string) => width - getDisplayWidth(h) - getDisplayWidth(m) - 2;
|
|
44
|
+
let leftWidth = leftFor(hints, matchStr);
|
|
45
|
+
if (leftWidth < minLeft) {
|
|
46
|
+
hints = shortHints;
|
|
47
|
+
leftWidth = leftFor(hints, matchStr);
|
|
48
|
+
}
|
|
49
|
+
if (leftWidth < minLeft) {
|
|
50
|
+
hints = '';
|
|
51
|
+
leftWidth = leftFor(hints, matchStr);
|
|
52
|
+
}
|
|
53
|
+
if (leftWidth < minLeft && matchStr.length > 0) {
|
|
54
|
+
matchStr = '';
|
|
55
|
+
leftWidth = leftFor(hints, matchStr);
|
|
56
|
+
}
|
|
57
|
+
// Final clamp: at least one cell, never wider than the bar.
|
|
58
|
+
leftWidth = Math.max(1, Math.min(width, leftWidth));
|
|
59
|
+
|
|
37
60
|
const matchStrW = getDisplayWidth(matchStr);
|
|
38
|
-
// Available width for left content (query area)
|
|
39
|
-
const leftWidth = width - hintsW - matchStrW - 2;
|
|
40
61
|
const truncatedLeft = fitDisplay(
|
|
41
62
|
getDisplayWidth(leftPart) > leftWidth ? truncateDisplay(leftPart, leftWidth) : leftPart,
|
|
42
63
|
leftWidth,
|
|
@@ -55,10 +55,12 @@ export function renderSessionPickerModal(
|
|
|
55
55
|
style: { fg: '240', dim: true },
|
|
56
56
|
});
|
|
57
57
|
} else {
|
|
58
|
-
//
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
const
|
|
58
|
+
// Proportional column widths that adapt to the modal's content width:
|
|
59
|
+
// timestamp ~22% (clamped 10..16), messages ~12% (clamped 4..8), and the
|
|
60
|
+
// name column absorbs the remainder so the row always fills contentW.
|
|
61
|
+
const tsW = Math.min(16, Math.max(10, Math.floor(contentW * 0.22)));
|
|
62
|
+
const msgW = Math.min(8, Math.max(4, Math.floor(contentW * 0.12)));
|
|
63
|
+
const nameW = Math.max(8, contentW - tsW - msgW - 4); // 4 = separators/spaces
|
|
62
64
|
|
|
63
65
|
// Column header
|
|
64
66
|
const nameHdr = fitDisplay('Name', nameW);
|
|
@@ -12,6 +12,7 @@ import { getDisplayWidth, wrapText } from '../utils/terminal-width.ts';
|
|
|
12
12
|
import { CATEGORY_LABELS, describeUiRouting, formatValue, getSettingLabel, inferSubscriptionRouteReason, valueColor } from './settings-modal-helpers.ts';
|
|
13
13
|
import { isSecretConfigKey } from '../config/secret-config.ts';
|
|
14
14
|
import { GLYPHS } from './ui-primitives.ts';
|
|
15
|
+
import { formatHints, joinHints } from './hint-grammar.ts';
|
|
15
16
|
import {
|
|
16
17
|
clamp,
|
|
17
18
|
getFullscreenWorkspaceMetrics,
|
|
@@ -540,24 +541,83 @@ function rowColorForSetting(modal: SettingsModal, rowText: string): string {
|
|
|
540
541
|
}
|
|
541
542
|
|
|
542
543
|
function footerText(modal: SettingsModal, width: number): string {
|
|
544
|
+
// Every branch speaks the shared hint grammar: bracketed [Key] Verb segments
|
|
545
|
+
// joined by the middle dot, with Esc sorted last. Leading words like
|
|
546
|
+
// "Focus settings" are state labels appended verbatim, not key hints.
|
|
543
547
|
// Armed reset gate takes priority over all other footer states.
|
|
544
548
|
if (modal.resetCategoryConfirm !== null || modal.resetAllConfirm !== null)
|
|
545
|
-
return 'Reset armed
|
|
546
|
-
|
|
547
|
-
|
|
548
|
-
|
|
549
|
-
if (modal.
|
|
550
|
-
|
|
551
|
-
|
|
549
|
+
return joinHints('Reset armed', formatHints([
|
|
550
|
+
{ key: 'Enter/y', verb: 'confirm' },
|
|
551
|
+
{ key: 'Esc/n', verb: 'cancel' },
|
|
552
|
+
]));
|
|
553
|
+
if (modal.searchFocused)
|
|
554
|
+
return joinHints('Search', 'type to filter', formatHints([
|
|
555
|
+
{ key: 'Up/Down', verb: 'Navigate' },
|
|
556
|
+
{ key: 'Enter', verb: 'Select' },
|
|
557
|
+
{ key: 'Esc', verb: 'Exit search' },
|
|
558
|
+
]));
|
|
559
|
+
if (modal.editingMode)
|
|
560
|
+
return formatHints([
|
|
561
|
+
{ key: 'Enter', verb: 'Confirm edit' },
|
|
562
|
+
{ key: 'Esc', verb: 'Cancel edit' },
|
|
563
|
+
]);
|
|
564
|
+
if (modal.focusPane === 'categories')
|
|
565
|
+
return joinHints('Focus categories', formatHints([
|
|
566
|
+
{ key: 'Up/Down', verb: 'Choose' },
|
|
567
|
+
{ key: 'Right/Enter', verb: 'Settings' },
|
|
568
|
+
{ key: 'Tab', verb: 'Pane' },
|
|
569
|
+
{ key: '/', verb: 'Search' },
|
|
570
|
+
{ key: 'Esc', verb: 'Close' },
|
|
571
|
+
]));
|
|
572
|
+
if (modal.currentCategory === 'subscriptions')
|
|
573
|
+
return joinHints('Focus settings', formatHints([
|
|
574
|
+
{ key: 'Up/Down', verb: 'Provider' },
|
|
575
|
+
{ key: 'Left', verb: 'Categories' },
|
|
576
|
+
{ key: 'Tab', verb: 'Pane' },
|
|
577
|
+
{ key: '/', verb: 'Search' },
|
|
578
|
+
{ key: 'Enter', verb: 'Review/sign out' },
|
|
579
|
+
{ key: 'Esc', verb: 'Close' },
|
|
580
|
+
]));
|
|
581
|
+
if (modal.currentCategory === 'mcp')
|
|
582
|
+
return joinHints('Focus settings', formatHints([
|
|
583
|
+
{ key: 'Up/Down', verb: 'Server' },
|
|
584
|
+
{ key: 'Left', verb: 'Categories' },
|
|
585
|
+
{ key: 'Tab', verb: 'Pane' },
|
|
586
|
+
{ key: '/', verb: 'Search' },
|
|
587
|
+
{ key: 'Enter', verb: 'Edit trust' },
|
|
588
|
+
{ key: 'Esc', verb: 'Close' },
|
|
589
|
+
]));
|
|
590
|
+
if (modal.currentCategory === 'flags')
|
|
591
|
+
return joinHints('Focus feature flags', formatHints([
|
|
592
|
+
{ key: 'Up/Down', verb: 'Flag' },
|
|
593
|
+
{ key: 'Left', verb: 'Categories' },
|
|
594
|
+
{ key: 'Tab', verb: 'Pane' },
|
|
595
|
+
{ key: '/', verb: 'Search' },
|
|
596
|
+
{ key: 'Enter/Space', verb: 'Toggle' },
|
|
597
|
+
{ key: 'Esc', verb: 'Close' },
|
|
598
|
+
]));
|
|
552
599
|
// Default settings pane: tier the reset affordances by available width.
|
|
553
600
|
// W<80: minimal — only the most critical action survives.
|
|
554
601
|
// W<160: compact but still shows both reset affordances.
|
|
555
602
|
// W≥160: standard with all navigation tokens.
|
|
556
603
|
if (width < 80)
|
|
557
|
-
return 'R reset
|
|
604
|
+
return formatHints([{ key: 'R', verb: 'reset' }, { key: 'Esc', verb: 'Close' }]);
|
|
558
605
|
if (width < 160)
|
|
559
|
-
return
|
|
560
|
-
|
|
606
|
+
return formatHints([
|
|
607
|
+
{ key: 'Up/Down', verb: 'Move' },
|
|
608
|
+
{ key: 'Enter/Space', verb: 'Edit' },
|
|
609
|
+
{ key: '⇧R', verb: 'reset cat' },
|
|
610
|
+
{ key: '^⇧R', verb: 'reset all' },
|
|
611
|
+
{ key: 'Esc', verb: 'Close' },
|
|
612
|
+
]);
|
|
613
|
+
return joinHints('Focus settings', formatHints([
|
|
614
|
+
{ key: 'Up/Down', verb: 'Setting' },
|
|
615
|
+
{ key: 'Left', verb: 'Categories' },
|
|
616
|
+
{ key: 'Enter/Space', verb: 'Edit/toggle' },
|
|
617
|
+
{ key: '⇧R', verb: 'reset cat' },
|
|
618
|
+
{ key: '^⇧R', verb: 'reset all' },
|
|
619
|
+
{ key: 'Esc', verb: 'Close' },
|
|
620
|
+
]));
|
|
561
621
|
}
|
|
562
622
|
|
|
563
623
|
export function renderSettingsModal(
|
|
@@ -34,6 +34,12 @@ export interface ShellFooterBuildOptions {
|
|
|
34
34
|
* Rendered as a dim informational line above the prompt when non-null.
|
|
35
35
|
*/
|
|
36
36
|
readonly contextStatusHint?: string | null;
|
|
37
|
+
/**
|
|
38
|
+
* Compact footer posture for short terminals (~<30 rows). Collapses the
|
|
39
|
+
* footer to its essentials (prompt box + token/cost line + help) and drops
|
|
40
|
+
* the process indicator, context bar, context-info line and posture block.
|
|
41
|
+
*/
|
|
42
|
+
readonly compact?: boolean;
|
|
37
43
|
}
|
|
38
44
|
|
|
39
45
|
export interface ShellFooterBuildResult {
|
|
@@ -41,14 +47,28 @@ export interface ShellFooterBuildResult {
|
|
|
41
47
|
readonly height: number;
|
|
42
48
|
}
|
|
43
49
|
|
|
44
|
-
|
|
45
|
-
|
|
50
|
+
// Fixed rows that createFooter always emits (non-compact): prompt-box top
|
|
51
|
+
// border, prompt-box bottom border, token-usage line, context-info line, and
|
|
52
|
+
// the help/exit line. The composer posture block and context bar are counted
|
|
53
|
+
// separately (they are conditional).
|
|
54
|
+
const FOOTER_BASE_ROWS = 5;
|
|
55
|
+
const CONTEXT_PROGRESS_ROWS = 1;
|
|
46
56
|
const PROCESS_INDICATOR_ROWS = 1;
|
|
47
57
|
|
|
58
|
+
/**
|
|
59
|
+
* Real height of the most recently rendered footer. estimateShellFooterHeight
|
|
60
|
+
* prefers this so the pre-prompt viewport math accounts for the composer
|
|
61
|
+
* posture block and context hint that the static formula cannot see. Reset to
|
|
62
|
+
* null before any footer has rendered (cold start), where the formula is exact
|
|
63
|
+
* for the common posture-free, hint-free case.
|
|
64
|
+
*/
|
|
65
|
+
let lastRenderedFooterHeight: number | null = null;
|
|
66
|
+
|
|
48
67
|
export function estimateShellFooterHeight(
|
|
49
68
|
promptLineCount: number,
|
|
50
69
|
contextWindow?: number,
|
|
51
70
|
): number {
|
|
71
|
+
if (lastRenderedFooterHeight !== null) return lastRenderedFooterHeight;
|
|
52
72
|
const safePromptLines = Math.max(1, promptLineCount);
|
|
53
73
|
const progressRows = contextWindow && contextWindow > 0 ? CONTEXT_PROGRESS_ROWS : 0;
|
|
54
74
|
return FOOTER_BASE_ROWS + safePromptLines + progressRows + PROCESS_INDICATOR_ROWS;
|
|
@@ -79,20 +99,26 @@ export function buildShellFooter(
|
|
|
79
99
|
options.composerStatus,
|
|
80
100
|
options.composerFlags,
|
|
81
101
|
options.composerPendingRisk,
|
|
102
|
+
options.compact ?? false,
|
|
82
103
|
);
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
104
|
+
// Compact posture drops the process indicator and context hint entirely so
|
|
105
|
+
// the footer fits within ~5 rows on short terminals.
|
|
106
|
+
if (!options.compact) {
|
|
107
|
+
const processIndicator = renderProcessIndicator(
|
|
108
|
+
options.width,
|
|
109
|
+
options.runningAgentCount,
|
|
110
|
+
options.runningProcessCount,
|
|
111
|
+
options.indicatorFocused,
|
|
112
|
+
options.runningAgentProgress,
|
|
113
|
+
);
|
|
114
|
+
const inputBoxRows = Math.max(1, options.promptLineCount) + 2;
|
|
115
|
+
lines.splice(inputBoxRows, 0, ...processIndicator);
|
|
116
|
+
// Passive context status hint — rendered as a dim informational line before the prompt.
|
|
117
|
+
if (options.contextStatusHint) {
|
|
118
|
+
const hintLine = UIFactory.stringToLine(options.contextStatusHint, options.width, { fg: '#64748b' });
|
|
119
|
+
lines.unshift(hintLine);
|
|
120
|
+
}
|
|
96
121
|
}
|
|
122
|
+
lastRenderedFooterHeight = lines.length;
|
|
97
123
|
return { lines, height: lines.length };
|
|
98
124
|
}
|
|
@@ -4,18 +4,20 @@
|
|
|
4
4
|
// Extracted as a neutral module so both status-token.ts and polish.ts can
|
|
5
5
|
// import from here without creating a circular ESM dependency.
|
|
6
6
|
//
|
|
7
|
-
//
|
|
8
|
-
//
|
|
9
|
-
//
|
|
10
|
-
//
|
|
11
|
-
//
|
|
7
|
+
// STATE_GLYPHS is an alias into GLYPHS.status (ui-primitives.ts) — the single
|
|
8
|
+
// glyph registry. Previously GLYPHS.status.info ('•') collided with
|
|
9
|
+
// GLYPHS.status.pending, and STATE_GLYPHS.info ('○') collided with
|
|
10
|
+
// GLYPHS.status.idle. GLYPHS.status.info is now '○' (this module's historical
|
|
11
|
+
// value) and idle uses a distinct glyph ('◌').
|
|
12
12
|
// ---------------------------------------------------------------------------
|
|
13
13
|
|
|
14
|
+
import { GLYPHS } from './ui-primitives.ts';
|
|
15
|
+
|
|
14
16
|
export type StatusState = 'good' | 'warn' | 'bad' | 'info';
|
|
15
17
|
|
|
16
18
|
export const STATE_GLYPHS: Record<StatusState, string> = {
|
|
17
|
-
good:
|
|
18
|
-
warn:
|
|
19
|
-
bad:
|
|
20
|
-
info:
|
|
19
|
+
good: GLYPHS.status.success, // ✓
|
|
20
|
+
warn: GLYPHS.status.warn, // ⚠
|
|
21
|
+
bad: GLYPHS.status.failure, // ✕
|
|
22
|
+
info: GLYPHS.status.info, // ○
|
|
21
23
|
};
|
package/src/renderer/theme.ts
CHANGED
|
@@ -14,8 +14,19 @@
|
|
|
14
14
|
* IMPORTANT: inline code has NO background token. The bg:#1a1a1a hardcode
|
|
15
15
|
* that previously existed caused a near-black box on light terminals.
|
|
16
16
|
* Differentiate inline code via inlineCodeFg + bold only; bg inherits terminal.
|
|
17
|
+
*
|
|
18
|
+
* resolveUiTones(mode) is the sibling read path for CHROME tokens (UI_TONES
|
|
19
|
+
* in ui-primitives.ts — panel/modal/overlay/fullscreen backgrounds, borders,
|
|
20
|
+
* status colours). It composes the same ThemeMode dimension so that
|
|
21
|
+
* DEFAULT_PANEL_PALETTE, DEFAULT_STYLE, FULLSCREEN_PALETTE and
|
|
22
|
+
* DEFAULT_OVERLAY_PALETTE all read through ONE mode-resolved path instead of
|
|
23
|
+
* importing the static UI_TONES constant directly. Mode stays 'dark'
|
|
24
|
+
* everywhere until the terminal-bg-probe lands (see WO-001) — the light
|
|
25
|
+
* entry is type-complete, not a shipped light theme.
|
|
17
26
|
*/
|
|
18
27
|
|
|
28
|
+
import { UI_TONES } from './ui-primitives.ts';
|
|
29
|
+
|
|
19
30
|
/** Background mode — dark is the safe default until terminal-bg-probe lands. */
|
|
20
31
|
export type ThemeMode = 'dark' | 'light';
|
|
21
32
|
|
|
@@ -78,9 +89,9 @@ const DARK: ThemeTokens = {
|
|
|
78
89
|
searchCurrentFg: '#000000',
|
|
79
90
|
strikethrough: '244',
|
|
80
91
|
blockquote: '244',
|
|
81
|
-
assistantHeader:
|
|
82
|
-
reasoningAccent:
|
|
83
|
-
toolAccent:
|
|
92
|
+
assistantHeader: UI_TONES.accent.control,
|
|
93
|
+
reasoningAccent: UI_TONES.state.reasoning,
|
|
94
|
+
toolAccent: UI_TONES.state.info,
|
|
84
95
|
collapsedBodyBg: '#1a1a1a',
|
|
85
96
|
checkboxChecked: '#22c55e',
|
|
86
97
|
errorBarBg: '#3a1a1a',
|
|
@@ -156,3 +167,49 @@ Object.freeze(LIGHT);
|
|
|
156
167
|
* Frozen — do not mutate.
|
|
157
168
|
*/
|
|
158
169
|
export const DARK_THEME: Readonly<ThemeTokens> = DARK;
|
|
170
|
+
|
|
171
|
+
// ---------------------------------------------------------------------------
|
|
172
|
+
// Chrome tokens (UI_TONES) — mode-resolved sibling to resolveTheme().
|
|
173
|
+
//
|
|
174
|
+
// UI_TONES (ui-primitives.ts) is the dark entry. The light entry mirrors
|
|
175
|
+
// dark for every role that has no light-appropriate equivalent yet — the
|
|
176
|
+
// WO-001 deliverable is the mode dimension and single read path, not a
|
|
177
|
+
// shipped light chrome theme (see module doc comment above).
|
|
178
|
+
// ---------------------------------------------------------------------------
|
|
179
|
+
|
|
180
|
+
/** Recursively widen the `as const` literal leaves of UI_TONES to `string`
|
|
181
|
+
* so mode variants (e.g. UI_TONES_LIGHT) can assign different colour
|
|
182
|
+
* values without fighting TypeScript's literal-type inference. */
|
|
183
|
+
type DeepWidenToString<T> = T extends string ? string : { [K in keyof T]: DeepWidenToString<T[K]> };
|
|
184
|
+
|
|
185
|
+
export type UiToneTokens = DeepWidenToString<typeof UI_TONES>;
|
|
186
|
+
|
|
187
|
+
const UI_TONES_LIGHT: UiToneTokens = {
|
|
188
|
+
...UI_TONES,
|
|
189
|
+
state: {
|
|
190
|
+
...UI_TONES.state,
|
|
191
|
+
info: LIGHT.toolAccent,
|
|
192
|
+
reasoning: LIGHT.reasoningAccent,
|
|
193
|
+
},
|
|
194
|
+
accent: {
|
|
195
|
+
...UI_TONES.accent,
|
|
196
|
+
brand: LIGHT.heading1,
|
|
197
|
+
gradientStart: LIGHT.heading1,
|
|
198
|
+
gradientEnd: LIGHT.reasoningAccent,
|
|
199
|
+
},
|
|
200
|
+
};
|
|
201
|
+
|
|
202
|
+
Object.freeze(UI_TONES_LIGHT.state);
|
|
203
|
+
Object.freeze(UI_TONES_LIGHT.accent);
|
|
204
|
+
Object.freeze(UI_TONES_LIGHT);
|
|
205
|
+
|
|
206
|
+
/**
|
|
207
|
+
* resolveUiTones — Return the chrome (panel/modal/overlay/fullscreen) token
|
|
208
|
+
* set for the given background mode. Single read path for UI_TONES; the
|
|
209
|
+
* 'dark' resolution is byte-identical to the UI_TONES constant.
|
|
210
|
+
*
|
|
211
|
+
* Call with 'dark' (the safe default) until terminal-bg-probe lands.
|
|
212
|
+
*/
|
|
213
|
+
export function resolveUiTones(mode: ThemeMode): Readonly<UiToneTokens> {
|
|
214
|
+
return mode === 'light' ? UI_TONES_LIGHT : UI_TONES;
|
|
215
|
+
}
|
|
@@ -4,10 +4,12 @@ import { VERSION } from '../version.ts';
|
|
|
4
4
|
import { fitDisplay, getDisplayWidth, truncateDisplay, wrapText, interpolateColor } from '../utils/terminal-width.ts';
|
|
5
5
|
import type { GitHeaderInfo } from './git-status.ts';
|
|
6
6
|
import { renderConversationFragment, renderConversationStatusLine, type ConversationStatusSegment } from './conversation-surface.ts';
|
|
7
|
-
import { GLYPHS } from './ui-primitives.ts';
|
|
7
|
+
import { GLYPHS, UI_TONES } from './ui-primitives.ts';
|
|
8
8
|
import { formatElapsed } from '../utils/format-elapsed.ts';
|
|
9
9
|
import { abbreviateCount } from '../utils/format-number.ts';
|
|
10
10
|
import { computeContextUsage } from '../core/context-usage.ts';
|
|
11
|
+
import { calcSessionCost } from '../export/cost-utils.ts';
|
|
12
|
+
import { buildFooterTip, isAgentActive } from './footer-tips.ts';
|
|
11
13
|
|
|
12
14
|
/** Number of frames before the animated gradient completes one full cycle. */
|
|
13
15
|
const GRADIENT_CYCLE_FRAMES = 50;
|
|
@@ -35,15 +37,23 @@ function fmtNum(n: number): string {
|
|
|
35
37
|
return abbreviateCount(n, { bSuffix: true });
|
|
36
38
|
}
|
|
37
39
|
|
|
40
|
+
/** Format a running USD cost estimate with a precision that suits its magnitude. */
|
|
41
|
+
function fmtCost(usd: number): string {
|
|
42
|
+
if (!(usd > 0)) return '0.00';
|
|
43
|
+
if (usd < 0.01) return usd.toFixed(4);
|
|
44
|
+
if (usd < 1) return usd.toFixed(3);
|
|
45
|
+
return usd.toFixed(2);
|
|
46
|
+
}
|
|
47
|
+
|
|
38
48
|
/**
|
|
39
49
|
* UIFactory - Generates standard UI fragments without needing Ink/React overhead.
|
|
40
50
|
*/
|
|
41
51
|
export class UIFactory {
|
|
42
52
|
public static createHeader(width: number, model: string, provider: string, title?: string, gitInfo?: GitHeaderInfo): Line[] {
|
|
43
53
|
const lines: Line[] = [];
|
|
44
|
-
const CYAN =
|
|
45
|
-
const GREY =
|
|
46
|
-
const TITLE_COLOR =
|
|
54
|
+
const CYAN = UI_TONES.accent.brand;
|
|
55
|
+
const GREY = UI_TONES.fg.dim;
|
|
56
|
+
const TITLE_COLOR = UI_TONES.fg.muted;
|
|
47
57
|
const brand = ` GoodVibes `;
|
|
48
58
|
const ver = `v${VERSION} `;
|
|
49
59
|
const stats = ` ${model} `;
|
|
@@ -77,11 +87,11 @@ export class UIFactory {
|
|
|
77
87
|
}
|
|
78
88
|
// Build git info segment
|
|
79
89
|
let gitStr = '';
|
|
80
|
-
let gitFg =
|
|
90
|
+
let gitFg: string = UI_TONES.fg.dim;
|
|
81
91
|
if (gitInfo) {
|
|
82
92
|
gitStr = buildGitSegment(gitInfo).text;
|
|
83
93
|
if (gitInfo.dirty || gitInfo.ahead > 0 || gitInfo.behind > 0) {
|
|
84
|
-
gitFg =
|
|
94
|
+
gitFg = UI_TONES.state.warn; // yellow when dirty or out-of-sync
|
|
85
95
|
}
|
|
86
96
|
}
|
|
87
97
|
const rightSideText = stats + prov;
|
|
@@ -91,7 +101,7 @@ export class UIFactory {
|
|
|
91
101
|
for (const char of stats) { if (rightX < width) line[rightX++] = { char, fg: CYAN, bg: '', bold: true, dim: false, underline: false, italic: false, strikethrough: false }; }
|
|
92
102
|
for (const char of prov) { if (rightX < width) line[rightX++] = { char, fg: GREY, bg: '', bold: false, dim: true, underline: false, italic: false, strikethrough: false }; }
|
|
93
103
|
lines.push(line);
|
|
94
|
-
lines.push(this.stringToLine('━'.repeat(width), width, { fg:
|
|
104
|
+
lines.push(this.stringToLine('━'.repeat(width), width, { fg: UI_TONES.fg.dim }));
|
|
95
105
|
return lines;
|
|
96
106
|
}
|
|
97
107
|
|
|
@@ -101,12 +111,12 @@ export class UIFactory {
|
|
|
101
111
|
*/
|
|
102
112
|
public static createMessageBar(
|
|
103
113
|
width: number, text: string,
|
|
104
|
-
bgColor = '#2a2a2a', textColor =
|
|
114
|
+
bgColor: string = '#2a2a2a', textColor: string = UI_TONES.fg.secondary, prefixStr: string = ' › ',
|
|
105
115
|
strikethrough = false
|
|
106
116
|
): Line[] {
|
|
107
117
|
return renderConversationFragment(text, width, {
|
|
108
118
|
prefix: prefixStr,
|
|
109
|
-
prefixFg:
|
|
119
|
+
prefixFg: UI_TONES.state.reasoning,
|
|
110
120
|
text: textColor,
|
|
111
121
|
bodyBg: bgColor,
|
|
112
122
|
strikethrough,
|
|
@@ -119,8 +129,8 @@ export class UIFactory {
|
|
|
119
129
|
public static createQueuedMessageFragment(width: number, text: string): Line[] {
|
|
120
130
|
return renderConversationFragment(text, width, {
|
|
121
131
|
prefix: ' (...) ',
|
|
122
|
-
prefixFg:
|
|
123
|
-
text:
|
|
132
|
+
prefixFg: UI_TONES.state.reasoning,
|
|
133
|
+
text: UI_TONES.fg.dim,
|
|
124
134
|
bodyBg: '#1a1a1a',
|
|
125
135
|
dim: true,
|
|
126
136
|
});
|
|
@@ -148,6 +158,7 @@ export class UIFactory {
|
|
|
148
158
|
composerStatus?: string,
|
|
149
159
|
composerFlags?: readonly string[],
|
|
150
160
|
composerPendingRisk?: 'none' | 'approval-wait' | 'shell' | 'command' | 'remote',
|
|
161
|
+
compact: boolean = false,
|
|
151
162
|
): Line[] {
|
|
152
163
|
const lines: Line[] = [];
|
|
153
164
|
const promptLines = prompt.split('\n');
|
|
@@ -253,7 +264,9 @@ export class UIFactory {
|
|
|
253
264
|
}
|
|
254
265
|
}
|
|
255
266
|
lines.push(bottomLine);
|
|
256
|
-
|
|
267
|
+
// --- Composer posture block (mode / risk / status / flags) ------------
|
|
268
|
+
// Suppressed in compact mode; the ctx-info line no longer duplicates these
|
|
269
|
+
// tokens, so this block is the single home for mode/status/flags.
|
|
257
270
|
const composerTokens: Array<{ text: string; fg: string; bold?: boolean; dim?: boolean }> = [];
|
|
258
271
|
if (composerMode) composerTokens.push({ text: ` ${GLYPHS.status.active} ${composerMode} `, fg: '#38bdf8', bold: true });
|
|
259
272
|
if (composerPendingRisk && composerPendingRisk !== 'none') {
|
|
@@ -268,7 +281,7 @@ export class UIFactory {
|
|
|
268
281
|
}
|
|
269
282
|
if (composerStatus && composerStatus !== 'idle') composerTokens.push({ text: ` state:${composerStatus} `, fg: '244', dim: true });
|
|
270
283
|
if (composerFlags && composerFlags.length > 0) composerTokens.push({ text: ` flags:${composerFlags.join(',')} `, fg: '244', dim: true });
|
|
271
|
-
if (composerTokens.length > 0) {
|
|
284
|
+
if (!compact && composerTokens.length > 0) {
|
|
272
285
|
const postureLine = createBaseLine();
|
|
273
286
|
let px = 2;
|
|
274
287
|
for (const token of composerTokens) {
|
|
@@ -289,10 +302,10 @@ export class UIFactory {
|
|
|
289
302
|
if (px >= width) break;
|
|
290
303
|
}
|
|
291
304
|
lines.push(postureLine);
|
|
292
|
-
lines.push(createBaseLine());
|
|
293
305
|
}
|
|
294
306
|
const isRecentlyCopied = Date.now() - lastCopyTime < 2000;
|
|
295
|
-
// Token usage line
|
|
307
|
+
// Token usage line + running ~$ cost estimate (derived from the usage
|
|
308
|
+
// object and the active model via cost-utils).
|
|
296
309
|
const u = usage as { input?: number; output?: number; cacheRead?: number; cacheWrite?: number; up?: number; down?: number };
|
|
297
310
|
const inp = u.input ?? u.up ?? 0;
|
|
298
311
|
const out = u.output ?? u.down ?? 0;
|
|
@@ -300,12 +313,13 @@ export class UIFactory {
|
|
|
300
313
|
const cw = u.cacheWrite ?? 0;
|
|
301
314
|
const total = inp + out + cr + cw;
|
|
302
315
|
const tokenSep = ` ${GLYPHS.navigation.pipeSeparator} `;
|
|
303
|
-
const
|
|
316
|
+
const costSegment = model ? `${tokenSep}~$${fmtCost(calcSessionCost(inp, out, cr, cw, model))}` : '';
|
|
317
|
+
const tokenLine = ` Token Usage [ Input: ${fmtNum(inp)}${tokenSep}Output: ${fmtNum(out)}${tokenSep}Cache Read: ${fmtNum(cr)}${tokenSep}Cache Write: ${fmtNum(cw)}${tokenSep}Total: ${fmtNum(total)}${costSegment} ]`;
|
|
304
318
|
const copiedNotice = isRecentlyCopied ? ` [COPIED] ` : '';
|
|
305
319
|
const statsLine = ' ' + tokenLine + ' '.repeat(Math.max(0, width - 4 - getDisplayWidth(tokenLine) - getDisplayWidth(copiedNotice))) + copiedNotice;
|
|
306
320
|
lines.push(this.stringToLine(statsLine, width, { fg: isRecentlyCopied ? '81' : '244', bold: isRecentlyCopied }));
|
|
307
|
-
// Context usage progress bar
|
|
308
|
-
if (contextWindow && contextWindow > 0) {
|
|
321
|
+
// Context usage progress bar — suppressed in compact mode.
|
|
322
|
+
if (!compact && contextWindow && contextWindow > 0) {
|
|
309
323
|
const ctxTokens = lastInputTokens ?? 0;
|
|
310
324
|
const label = ' Context Usage: ';
|
|
311
325
|
const suffix = ` [ ${fmtNum(ctxTokens)} / ${fmtNum(contextWindow)} ]`;
|
|
@@ -315,11 +329,12 @@ export class UIFactory {
|
|
|
315
329
|
const thresholdFraction = (compactThreshold !== undefined && compactThreshold > 0)
|
|
316
330
|
? Math.min(1, compactThreshold)
|
|
317
331
|
: undefined;
|
|
318
|
-
lines.push(createBaseLine());
|
|
319
332
|
lines.push(this.createProgressBarLine(label, ctxPct, barWidth, width, suffix, thresholdFraction));
|
|
320
333
|
}
|
|
321
|
-
// Context info line (working dir, model+provider, tools)
|
|
322
|
-
|
|
334
|
+
// Context info line (working dir, model+provider, tools, hitl).
|
|
335
|
+
// Suppressed in compact mode. mode/status/flags are intentionally omitted —
|
|
336
|
+
// the posture block above owns them, so they are not duplicated here.
|
|
337
|
+
if (!compact && (workingDir || model)) {
|
|
323
338
|
const home = typeof process !== 'undefined' ? process.env.HOME ?? '' : '';
|
|
324
339
|
const displayDir = workingDir && home && workingDir.startsWith(home)
|
|
325
340
|
? '~' + workingDir.slice(home.length)
|
|
@@ -331,19 +346,16 @@ export class UIFactory {
|
|
|
331
346
|
}
|
|
332
347
|
if (toolCount) ctxParts.push(`${toolCount} tools`);
|
|
333
348
|
if (hitlMode) ctxParts.push(`hitl:${hitlMode}`);
|
|
334
|
-
if (composerMode) ctxParts.push(`mode:${composerMode}`);
|
|
335
|
-
if (composerStatus && composerStatus !== 'idle') ctxParts.push(`status:${composerStatus}`);
|
|
336
|
-
if (composerFlags && composerFlags.length > 0) ctxParts.push(composerFlags.join(','));
|
|
337
349
|
const ctxLine = ' ' + ctxParts.join(` ${GLYPHS.navigation.pipeSeparator} `);
|
|
338
|
-
lines.push(createBaseLine());
|
|
339
350
|
lines.push(this.stringToLine(truncateDisplay(ctxLine, width), width, { fg: '240', dim: true }));
|
|
340
|
-
lines.push(createBaseLine());
|
|
341
351
|
}
|
|
342
352
|
if (showExitNotice) {
|
|
343
353
|
const notice = ` !!! Press Ctrl+C again to exit !!! `;
|
|
344
354
|
lines.push(this.stringToLine(fitDisplay(notice, width), width, { fg: '196', bold: true }));
|
|
345
355
|
} else {
|
|
346
|
-
|
|
356
|
+
// Persistent discoverability tip. Rotates by context (agent-aware): the
|
|
357
|
+
// process-monitor tip leads while a turn is in flight. See footer-tips.ts.
|
|
358
|
+
const help = ` ${buildFooterTip({ agentActive: isAgentActive(composerStatus) })} `;
|
|
347
359
|
const dangerWarn = dangerMode ? `! DANGER MODE - ALL CHANGES AUTO-APPROVED ` : '';
|
|
348
360
|
const helpW = getDisplayWidth(help);
|
|
349
361
|
const dangerW = getDisplayWidth(dangerWarn);
|
|
@@ -363,7 +375,6 @@ export class UIFactory {
|
|
|
363
375
|
}
|
|
364
376
|
lines.push(line);
|
|
365
377
|
}
|
|
366
|
-
lines.push(createBaseLine());
|
|
367
378
|
return lines;
|
|
368
379
|
}
|
|
369
380
|
|
|
@@ -392,8 +403,8 @@ export class UIFactory {
|
|
|
392
403
|
];
|
|
393
404
|
|
|
394
405
|
/** Gradient colors for thinking text — cyan to purple (matches splash). */
|
|
395
|
-
private static readonly THINK_GRADIENT_START =
|
|
396
|
-
private static readonly THINK_GRADIENT_END =
|
|
406
|
+
private static readonly THINK_GRADIENT_START = UI_TONES.accent.gradientStart;
|
|
407
|
+
private static readonly THINK_GRADIENT_END = UI_TONES.accent.gradientEnd;
|
|
397
408
|
|
|
398
409
|
public static createThinkingFragment(width: number, spinner: string, frame: number = 0, tokenSpeed?: number, toolPreview?: string, inputTokens?: number, outputTokens?: number, elapsedMs?: number, ttftMs?: number): Line[] {
|
|
399
410
|
// Rotate phrase every ~30 seconds (frame ticks at 80ms, so ~375 frames)
|
|
@@ -35,8 +35,9 @@ export const GLYPHS = {
|
|
|
35
35
|
failure: '✕',
|
|
36
36
|
pending: '•',
|
|
37
37
|
active: '●',
|
|
38
|
-
idle: '
|
|
39
|
-
info: '
|
|
38
|
+
idle: '◌',
|
|
39
|
+
info: '○',
|
|
40
|
+
warn: '⚠',
|
|
40
41
|
blocked: '⊘',
|
|
41
42
|
skipped: '◇',
|
|
42
43
|
review: '◈',
|
|
@@ -56,6 +57,11 @@ export const GLYPHS = {
|
|
|
56
57
|
},
|
|
57
58
|
} as const;
|
|
58
59
|
|
|
60
|
+
/**
|
|
61
|
+
* UI_TONES — the single chrome/color-token source for src/renderer and
|
|
62
|
+
* src/panels. Concrete (dark-mode) values; resolveUiTones(mode) in theme.ts
|
|
63
|
+
* is the single mode-resolved read path — this object is the 'dark' entry.
|
|
64
|
+
*/
|
|
59
65
|
export const UI_TONES = {
|
|
60
66
|
fg: {
|
|
61
67
|
primary: '#e2e8f0',
|
|
@@ -63,6 +69,8 @@ export const UI_TONES = {
|
|
|
63
69
|
muted: '#94a3b8',
|
|
64
70
|
dim: '#475569',
|
|
65
71
|
inverse: '#0f172a',
|
|
72
|
+
/** Empty-state / placeholder foreground (formerly DEFAULT_PANEL_PALETTE.empty literal). */
|
|
73
|
+
empty: '#334155',
|
|
66
74
|
},
|
|
67
75
|
bg: {
|
|
68
76
|
base: '#11131a',
|
|
@@ -75,6 +83,8 @@ export const UI_TONES = {
|
|
|
75
83
|
warning: '#2b2116',
|
|
76
84
|
error: '#2a161b',
|
|
77
85
|
success: '#14241b',
|
|
86
|
+
/** Fullscreen/shell footer background. */
|
|
87
|
+
footer: '#111827',
|
|
78
88
|
},
|
|
79
89
|
state: {
|
|
80
90
|
info: '#38bdf8',
|
|
@@ -83,6 +93,8 @@ export const UI_TONES = {
|
|
|
83
93
|
bad: '#ef4444',
|
|
84
94
|
blocked: '#f97316',
|
|
85
95
|
active: '#60a5fa',
|
|
96
|
+
/** Single canonical reasoning/thinking purple (replaces #9945FF and ad-hoc purples). */
|
|
97
|
+
reasoning: '#a855f7',
|
|
86
98
|
},
|
|
87
99
|
accent: {
|
|
88
100
|
browser: '#7dd3fc',
|
|
@@ -90,7 +102,16 @@ export const UI_TONES = {
|
|
|
90
102
|
inspector: '#c4b5fd',
|
|
91
103
|
workflow: '#fbbf24',
|
|
92
104
|
conversation: '#93c5fd',
|
|
105
|
+
/** Neon brand accent — header/splash/thinking gradient only. */
|
|
106
|
+
brand: '#00ffff',
|
|
107
|
+
gradientStart: '#00ffff',
|
|
108
|
+
gradientEnd: '#d000ff',
|
|
93
109
|
},
|
|
110
|
+
/** Canonical border stroke color for fullscreen/panel chrome. */
|
|
111
|
+
border: '#64748b',
|
|
94
112
|
} as const;
|
|
95
113
|
|
|
96
114
|
export type UiGlyphRegistry = typeof GLYPHS;
|
|
115
|
+
|
|
116
|
+
/** Single spinner-frame source — layout.ts and progress.ts both re-export this. */
|
|
117
|
+
export const SPINNER_FRAMES = ['⠋', '⠙', '⠹', '⠸', '⠼', '⠴', '⠦', '⠧', '⠇', '⠏'] as const;
|