@pellux/goodvibes-tui 0.28.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 +133 -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/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-content-actions.ts +8 -3
- 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/input-history.ts +17 -4
- package/src/input/keybindings.ts +48 -8
- package/src/input/panel-integration-actions.ts +107 -1
- package/src/main.ts +19 -11
- 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/base-panel.ts +1 -1
- 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 +209 -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 +310 -129
- 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/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-overlays.ts +25 -11
- package/src/renderer/conversation-surface.ts +1 -21
- package/src/renderer/diff-view.ts +6 -4
- package/src/renderer/footer-tips.ts +41 -0
- package/src/renderer/fullscreen-primitives.ts +23 -17
- package/src/renderer/fullscreen-workspace.ts +2 -7
- package/src/renderer/help-overlay.ts +91 -14
- package/src/renderer/hint-grammar.ts +52 -0
- package/src/renderer/history-search-overlay.ts +10 -16
- package/src/renderer/layout.ts +7 -7
- package/src/renderer/markdown.ts +9 -1
- package/src/renderer/modal-factory.ts +23 -15
- package/src/renderer/model-picker-overlay.ts +8 -485
- package/src/renderer/model-workspace.ts +9 -24
- package/src/renderer/overlay-box.ts +23 -19
- package/src/renderer/process-indicator.ts +15 -25
- package/src/renderer/profile-picker-modal.ts +13 -14
- package/src/renderer/prompt-content-width.ts +16 -0
- package/src/renderer/search-overlay.ts +27 -6
- package/src/renderer/selection-modal-overlay.ts +3 -1
- package/src/renderer/semantic-diff.ts +1 -1
- package/src/renderer/session-picker-modal.ts +6 -4
- package/src/renderer/settings-modal-helpers.ts +10 -34
- package/src/renderer/settings-modal.ts +70 -10
- package/src/renderer/shell-surface.ts +54 -15
- package/src/renderer/status-glyphs.ts +11 -9
- package/src/renderer/surface-layout.ts +0 -12
- package/src/renderer/term-caps.ts +1 -1
- package/src/renderer/theme.ts +60 -3
- package/src/renderer/tool-call.ts +6 -20
- package/src/renderer/ui-factory.ts +48 -37
- package/src/renderer/ui-primitives.ts +41 -3
- package/src/runtime/bootstrap-shell.ts +35 -18
- package/src/runtime/diagnostics/panels/index.ts +0 -3
- package/src/runtime/render-scheduler.ts +80 -0
- package/src/shell/ui-openers.ts +14 -22
- package/src/utils/format-duration.ts +2 -2
- package/src/utils/splash-lines.ts +54 -5
- 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/renderer/file-tree.ts +0 -153
- package/src/renderer/progress.ts +0 -99
- package/src/renderer/status-token.ts +0 -67
- package/src/runtime/diagnostics/panels/panel-resources.ts +0 -118
- package/src/runtime/diagnostics/panels/policy.ts +0 -177
|
@@ -1,28 +1,16 @@
|
|
|
1
1
|
import { type Line } from '../types/grid.ts';
|
|
2
2
|
import { UIFactory } from './ui-factory.ts';
|
|
3
|
-
import {
|
|
4
|
-
import { GLYPHS } from './ui-primitives.ts';
|
|
5
|
-
|
|
6
|
-
/** Truncate a string to fit within maxWidth display columns. */
|
|
7
|
-
function truncateToWidth(text: string, maxWidth: number): string {
|
|
8
|
-
let width = 0;
|
|
9
|
-
let i = 0;
|
|
10
|
-
for (const char of text) {
|
|
11
|
-
const cw = getDisplayWidth(char);
|
|
12
|
-
if (width + cw > maxWidth) break;
|
|
13
|
-
width += cw;
|
|
14
|
-
i += char.length;
|
|
15
|
-
}
|
|
16
|
-
return text.slice(0, i);
|
|
17
|
-
}
|
|
3
|
+
import { truncateDisplay } from '../utils/terminal-width.ts';
|
|
4
|
+
import { GLYPHS, UI_TONES } from './ui-primitives.ts';
|
|
5
|
+
import { formatHints } from './hint-grammar.ts';
|
|
18
6
|
|
|
19
7
|
/**
|
|
20
8
|
* renderProcessIndicator — shows a one-line summary of active background
|
|
21
9
|
* processes below the input area.
|
|
22
10
|
*
|
|
23
11
|
* Dimmed when no processes are active, highlighted (cyan) when agents or
|
|
24
|
-
* background exec processes are running. Includes an `Enter
|
|
25
|
-
* when active.
|
|
12
|
+
* background exec processes are running. Includes an `[Enter] View` hint
|
|
13
|
+
* (hint-grammar bracket form) when active.
|
|
26
14
|
*/
|
|
27
15
|
export function renderProcessIndicator(
|
|
28
16
|
width: number,
|
|
@@ -38,10 +26,10 @@ export function renderProcessIndicator(
|
|
|
38
26
|
const renderFocusedStatus = (text: string): Line[] => {
|
|
39
27
|
const bg = '#31506f';
|
|
40
28
|
const fg = '#eefaff';
|
|
41
|
-
const markerFg =
|
|
29
|
+
const markerFg = UI_TONES.accent.browser;
|
|
42
30
|
const line = UIFactory.stringToLine(' '.repeat(width), width, { fg: '238' });
|
|
43
31
|
const prefix = `${GLYPHS.navigation.selected} `;
|
|
44
|
-
const body =
|
|
32
|
+
const body = truncateDisplay(text, Math.max(0, width - 8), '');
|
|
45
33
|
const highlighted = ` ${prefix}${body} `;
|
|
46
34
|
const startX = 2;
|
|
47
35
|
for (let i = 0; i < highlighted.length && startX + i < width - 2; i++) {
|
|
@@ -62,8 +50,8 @@ export function renderProcessIndicator(
|
|
|
62
50
|
if (agentCount > 0) parts.push(`${agentCount} agent${agentCount !== 1 ? 's' : ''}`);
|
|
63
51
|
if (toolCount > 0) parts.push(`${toolCount} tool${toolCount !== 1 ? 's' : ''} running`);
|
|
64
52
|
const label = total === 0
|
|
65
|
-
? `No background processes ${
|
|
66
|
-
: `${parts.join(` ${GLYPHS.navigation.pipeSeparator} `)} ${
|
|
53
|
+
? `No background processes ${formatHints([{ key: 'Esc', verb: 'Back to input' }])}`
|
|
54
|
+
: `${parts.join(` ${GLYPHS.navigation.pipeSeparator} `)} ${formatHints([{ key: 'Enter', verb: 'Open' }, { key: 'Esc', verb: 'Back to input' }])}`;
|
|
67
55
|
return renderFocusedStatus(label);
|
|
68
56
|
}
|
|
69
57
|
|
|
@@ -83,14 +71,16 @@ export function renderProcessIndicator(
|
|
|
83
71
|
/**
|
|
84
72
|
* Number of columns reserved for the agent count label and hint text.
|
|
85
73
|
* Breakdown: "bg: N agents" prefix (~15 chars) + " | " separator (~3)
|
|
86
|
-
* + " Enter
|
|
74
|
+
* + " [Enter] View " hint (~16) + padding (~9) ≈ 43 chars.
|
|
87
75
|
*/
|
|
88
76
|
const PROGRESS_RESERVED_CHARS = 43;
|
|
89
77
|
const progressMaxLen = Math.max(0, width - PROGRESS_RESERVED_CHARS); // reserve space for count + hint
|
|
78
|
+
// Truncate by display width (not JS string length) so wide/CJK glyphs in the
|
|
79
|
+
// agent progress text don't overflow the reserved budget.
|
|
90
80
|
const progressSuffix = agentProgress && progressMaxLen > 10
|
|
91
|
-
? ` | ${agentProgress
|
|
81
|
+
? ` | ${truncateDisplay(agentProgress, progressMaxLen, '...')}`
|
|
92
82
|
: '';
|
|
93
83
|
const label = `${parts.join(` ${GLYPHS.navigation.pipeSeparator} `)}${progressSuffix}`;
|
|
94
|
-
const hint = ` ${
|
|
95
|
-
return renderPlainStatus(`${label}${hint}`, { fg:
|
|
84
|
+
const hint = ` ${formatHints([{ key: 'Enter', verb: 'View' }])}`;
|
|
85
|
+
return renderPlainStatus(`${label}${hint}`, { fg: UI_TONES.accent.brand, bold: true });
|
|
96
86
|
}
|
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
* using ModalFactory.
|
|
4
4
|
*
|
|
5
5
|
* Shows a list of saved profiles with:
|
|
6
|
-
* - name, timestamp (formatted)
|
|
6
|
+
* - name, timestamp (formatted)
|
|
7
7
|
* Footer hints: [Up/Down] Navigate [Enter] Load [d] Arm/Delete [s] Save current [Esc] Close
|
|
8
8
|
*/
|
|
9
9
|
|
|
@@ -55,18 +55,21 @@ export function renderProfilePickerModal(
|
|
|
55
55
|
style: { fg: '240', dim: true },
|
|
56
56
|
});
|
|
57
57
|
} else {
|
|
58
|
-
//
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
58
|
+
// Proportional column widths that adapt to the modal's content width:
|
|
59
|
+
// timestamp ~22% (clamped 10..16); the name column absorbs the remainder
|
|
60
|
+
// (ported from session-picker-modal.ts). Reserves 4 cols — 2 for the
|
|
61
|
+
// name/timestamp separator and 2 for the list row's selection indicator
|
|
62
|
+
// ("▸ ") that ModalFactory prepends outside the wrapped label — so the
|
|
63
|
+
// row never spills the timestamp's embedded space onto a wrapped line.
|
|
64
|
+
const tsW = Math.min(16, Math.max(10, Math.floor(contentW * 0.22)));
|
|
65
|
+
const nameW = Math.max(8, contentW - tsW - 4);
|
|
62
66
|
|
|
63
67
|
// Column header
|
|
64
|
-
const nameHdr
|
|
65
|
-
const tsHdr
|
|
66
|
-
const previewHdr = fitDisplay('Settings', previewW);
|
|
68
|
+
const nameHdr = fitDisplay('Name', nameW);
|
|
69
|
+
const tsHdr = fitDisplay('Saved', tsW);
|
|
67
70
|
sections.push({
|
|
68
71
|
type: 'text',
|
|
69
|
-
content: `${nameHdr} ${tsHdr}
|
|
72
|
+
content: `${nameHdr} ${tsHdr}`,
|
|
70
73
|
style: { fg: '240', dim: true },
|
|
71
74
|
});
|
|
72
75
|
sections.push({ type: 'separator' });
|
|
@@ -79,11 +82,7 @@ export function renderProfilePickerModal(
|
|
|
79
82
|
|
|
80
83
|
const tsStr = fitDisplay(formatTimestamp(prof.timestamp), tsW);
|
|
81
84
|
|
|
82
|
-
|
|
83
|
-
// (We only have name/timestamp in ProfileInfo, so show a placeholder)
|
|
84
|
-
const preview = fitDisplay('(display/provider/behavior)', previewW);
|
|
85
|
-
|
|
86
|
-
const label = `${nameStr} ${tsStr} ${preview}`;
|
|
85
|
+
const label = `${nameStr} ${tsStr}`;
|
|
87
86
|
return { label, selected: isSelected };
|
|
88
87
|
});
|
|
89
88
|
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* computePromptContentWidth — content width available for prompt text inside
|
|
3
|
+
* the footer input box, after the box margin, inner padding, and the ' > '
|
|
4
|
+
* prefix are subtracted.
|
|
5
|
+
*
|
|
6
|
+
* Floors at 1: on a hostile-narrow terminal the raw subtraction
|
|
7
|
+
* (boxWidth - 4 - 3) can go zero or negative, and a non-positive content
|
|
8
|
+
* width breaks downstream text-wrapping and cursor-position math (which
|
|
9
|
+
* assume at least one column to place a character in).
|
|
10
|
+
*/
|
|
11
|
+
export function computePromptContentWidth(terminalColumns: number): number {
|
|
12
|
+
const w = terminalColumns || 80;
|
|
13
|
+
const boxMargin = 2;
|
|
14
|
+
const boxWidth = w - (boxMargin * 2);
|
|
15
|
+
return Math.max(1, boxWidth - 4 - 3); // minus padding (4) minus prefix width (3: ' > ')
|
|
16
|
+
}
|
|
@@ -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,
|
|
@@ -196,10 +196,12 @@ export function renderSelectionModalOverlay(
|
|
|
196
196
|
: selectedItem?.primaryAction === 'delete'
|
|
197
197
|
? '[Enter] Delete'
|
|
198
198
|
: '[Enter] Select';
|
|
199
|
-
let hints = `[Up/Down] Navigate ${primaryVerb}
|
|
199
|
+
let hints = `[Up/Down] Navigate ${primaryVerb}`;
|
|
200
200
|
if (modal.allowSearch) hints += ' [/] Search';
|
|
201
201
|
if (selectedItem?.primaryAction === 'toggle' && !selectedItem.actions) hints += ' [Space] Toggle';
|
|
202
202
|
if (selectedItem?.actions) hints += ` ${selectedItem.actions}`;
|
|
203
|
+
// hint-grammar: Esc is the conventional "way out" and always sorts last.
|
|
204
|
+
hints += ' [Esc] Close';
|
|
203
205
|
putText(
|
|
204
206
|
footerLine,
|
|
205
207
|
layout.margin + 2,
|
|
@@ -328,7 +328,7 @@ const CHANGE_GLYPH: Record<ChangeKind, string> = {
|
|
|
328
328
|
* + import ./utils { A, B }
|
|
329
329
|
* ~ import ./types (+NewType, -OldType)
|
|
330
330
|
*/
|
|
331
|
-
|
|
331
|
+
function formatSemanticDiff(diff: SemanticDiff): string[] {
|
|
332
332
|
const lines: string[] = [];
|
|
333
333
|
|
|
334
334
|
// Symbol changes first
|
|
@@ -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);
|
|
@@ -7,6 +7,13 @@
|
|
|
7
7
|
import type { SettingEntry, McpEntry, SubscriptionEntry } from '../input/settings-modal-types.ts';
|
|
8
8
|
import { SETTINGS_CATEGORIES } from '../input/settings-modal-types.ts';
|
|
9
9
|
import { isSecretConfigKey, isSecretReferenceValue } from '../config/secret-config.ts';
|
|
10
|
+
import { UI_TONES } from './ui-primitives.ts';
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* "Modified / active" accent for settings rows — cyan-green, no UI_TONES
|
|
14
|
+
* role matches it (WO-207b: preserved byte-exact as a named constant).
|
|
15
|
+
*/
|
|
16
|
+
const SETTINGS_ACCENT = '#00ffcc';
|
|
10
17
|
|
|
11
18
|
function maskSecretValue(value: string): string {
|
|
12
19
|
if (value.length === 0) return '(empty)';
|
|
@@ -25,43 +32,12 @@ export function formatValue(entry: SettingEntry): string {
|
|
|
25
32
|
}
|
|
26
33
|
|
|
27
34
|
export function valueColor(entry: SettingEntry): string {
|
|
28
|
-
if (!entry.isDefault) return
|
|
29
|
-
return '244';
|
|
35
|
+
if (!entry.isDefault) return SETTINGS_ACCENT; // cyan-green = modified
|
|
36
|
+
return '244'; // dim = default
|
|
30
37
|
}
|
|
31
38
|
|
|
32
|
-
export function flagStateColor(state: string, killed: boolean): string {
|
|
33
|
-
if (killed) return '#ef4444'; // red
|
|
34
|
-
if (state === 'enabled') return '#00ffcc'; // cyan-green
|
|
35
|
-
return '244'; // dim
|
|
36
|
-
}
|
|
37
39
|
|
|
38
|
-
export function mcpTrustColor(mode: McpEntry['trustMode']): string {
|
|
39
|
-
switch (mode) {
|
|
40
|
-
case 'allow-all':
|
|
41
|
-
return '#ef4444';
|
|
42
|
-
case 'ask-on-risk':
|
|
43
|
-
return '#eab308';
|
|
44
|
-
case 'constrained':
|
|
45
|
-
return '#00ffcc';
|
|
46
|
-
case 'blocked':
|
|
47
|
-
return '244';
|
|
48
|
-
default:
|
|
49
|
-
return '244';
|
|
50
|
-
}
|
|
51
|
-
}
|
|
52
40
|
|
|
53
|
-
export function subscriptionStateColor(state: SubscriptionEntry['state']): string {
|
|
54
|
-
switch (state) {
|
|
55
|
-
case 'active':
|
|
56
|
-
return '#00ffcc';
|
|
57
|
-
case 'pending':
|
|
58
|
-
return '#eab308';
|
|
59
|
-
case 'available':
|
|
60
|
-
return '#38bdf8';
|
|
61
|
-
default:
|
|
62
|
-
return '244';
|
|
63
|
-
}
|
|
64
|
-
}
|
|
65
41
|
|
|
66
42
|
export function inferSubscriptionRouteReason(entry: SubscriptionEntry): string | undefined {
|
|
67
43
|
if (entry.routeReason?.trim()) return entry.routeReason;
|
|
@@ -107,7 +83,7 @@ export const CATEGORY_LABELS: Record<(typeof SETTINGS_CATEGORIES)[number], strin
|
|
|
107
83
|
network: 'Network',
|
|
108
84
|
};
|
|
109
85
|
|
|
110
|
-
|
|
86
|
+
const SETTING_LABELS: Partial<Record<string, string>> = {
|
|
111
87
|
'ui.systemMessages': 'System Message Target',
|
|
112
88
|
'ui.operationalMessages': 'Operational Message Target',
|
|
113
89
|
'ui.wrfcMessages': 'WRFC Message Target',
|
|
@@ -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,15 +47,42 @@ 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;
|
|
57
|
+
// Compact posture drops the context-info line (and never shows the context
|
|
58
|
+
// bar or process indicator), leaving just: prompt-box top border, bottom
|
|
59
|
+
// border, token-usage line, and the help/exit line.
|
|
60
|
+
const COMPACT_FOOTER_BASE_ROWS = 4;
|
|
61
|
+
|
|
62
|
+
/**
|
|
63
|
+
* Real height of the most recently rendered footer, tagged with the compact
|
|
64
|
+
* posture it was rendered under. estimateShellFooterHeight prefers this so
|
|
65
|
+
* the pre-prompt viewport math accounts for the composer posture block and
|
|
66
|
+
* context hint that the static formula cannot see. Reset to null before any
|
|
67
|
+
* footer has rendered (cold start), where the formula is exact for the
|
|
68
|
+
* common posture-free, hint-free case. The compact flag is part of the cache
|
|
69
|
+
* key: a cached non-compact height must never answer a compact query (and
|
|
70
|
+
* vice versa), since the two postures differ by several rows.
|
|
71
|
+
*/
|
|
72
|
+
let lastRenderedFooterHeight: { compact: boolean; height: number } | null = null;
|
|
47
73
|
|
|
48
74
|
export function estimateShellFooterHeight(
|
|
49
75
|
promptLineCount: number,
|
|
50
76
|
contextWindow?: number,
|
|
77
|
+
compact = false,
|
|
51
78
|
): number {
|
|
79
|
+
if (lastRenderedFooterHeight !== null && lastRenderedFooterHeight.compact === compact) {
|
|
80
|
+
return lastRenderedFooterHeight.height;
|
|
81
|
+
}
|
|
52
82
|
const safePromptLines = Math.max(1, promptLineCount);
|
|
83
|
+
if (compact) {
|
|
84
|
+
return COMPACT_FOOTER_BASE_ROWS + safePromptLines;
|
|
85
|
+
}
|
|
53
86
|
const progressRows = contextWindow && contextWindow > 0 ? CONTEXT_PROGRESS_ROWS : 0;
|
|
54
87
|
return FOOTER_BASE_ROWS + safePromptLines + progressRows + PROCESS_INDICATOR_ROWS;
|
|
55
88
|
}
|
|
@@ -79,20 +112,26 @@ export function buildShellFooter(
|
|
|
79
112
|
options.composerStatus,
|
|
80
113
|
options.composerFlags,
|
|
81
114
|
options.composerPendingRisk,
|
|
115
|
+
options.compact ?? false,
|
|
82
116
|
);
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
117
|
+
// Compact posture drops the process indicator and context hint entirely so
|
|
118
|
+
// the footer fits within ~5 rows on short terminals.
|
|
119
|
+
if (!options.compact) {
|
|
120
|
+
const processIndicator = renderProcessIndicator(
|
|
121
|
+
options.width,
|
|
122
|
+
options.runningAgentCount,
|
|
123
|
+
options.runningProcessCount,
|
|
124
|
+
options.indicatorFocused,
|
|
125
|
+
options.runningAgentProgress,
|
|
126
|
+
);
|
|
127
|
+
const inputBoxRows = Math.max(1, options.promptLineCount) + 2;
|
|
128
|
+
lines.splice(inputBoxRows, 0, ...processIndicator);
|
|
129
|
+
// Passive context status hint — rendered as a dim informational line before the prompt.
|
|
130
|
+
if (options.contextStatusHint) {
|
|
131
|
+
const hintLine = UIFactory.stringToLine(options.contextStatusHint, options.width, { fg: '#64748b' });
|
|
132
|
+
lines.unshift(hintLine);
|
|
133
|
+
}
|
|
96
134
|
}
|
|
135
|
+
lastRenderedFooterHeight = { compact: options.compact ?? false, height: lines.length };
|
|
97
136
|
return { lines, height: lines.length };
|
|
98
137
|
}
|
|
@@ -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
|
};
|
|
@@ -87,15 +87,3 @@ export function getTrackedVisibleWindow(
|
|
|
87
87
|
}
|
|
88
88
|
return { start, end, count: end - start, total };
|
|
89
89
|
}
|
|
90
|
-
|
|
91
|
-
export function sliceVisibleWindow<T>(
|
|
92
|
-
items: readonly T[],
|
|
93
|
-
selectedIndex: number,
|
|
94
|
-
visibleCount: number,
|
|
95
|
-
): { readonly items: readonly T[]; readonly window: VisibleWindow } {
|
|
96
|
-
const window = getVisibleWindow(items.length, selectedIndex, visibleCount);
|
|
97
|
-
return {
|
|
98
|
-
items: items.slice(window.start, window.end),
|
|
99
|
-
window,
|
|
100
|
-
};
|
|
101
|
-
}
|
|
@@ -223,7 +223,7 @@ export function nearestAnsi16Fg(r: number, g: number, b: number): number {
|
|
|
223
223
|
* Convert an ANSI16 fg code to the corresponding bg code.
|
|
224
224
|
* fg 30-37 → bg 40-47; fg 90-97 → bg 100-107.
|
|
225
225
|
*/
|
|
226
|
-
|
|
226
|
+
function ansi16FgToBg(fgCode: number): number {
|
|
227
227
|
// Both ranges (30-37 and 90-97) shift by +10 to reach their bg equivalents
|
|
228
228
|
// (30-37 → 40-47, 90-97 → 100-107).
|
|
229
229
|
return fgCode + 10;
|
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
|
+
}
|