@pellux/goodvibes-tui 0.27.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 +124 -0
- package/README.md +1 -1
- package/package.json +1 -1
- package/src/core/context-usage.ts +53 -0
- package/src/core/conversation.ts +4 -6
- package/src/core/session-recovery.ts +2 -0
- package/src/core/turn-event-wiring.ts +1 -0
- package/src/input/commands/cost-runtime.ts +49 -0
- package/src/input/commands/eval.ts +4 -3
- package/src/input/commands/operator-runtime.ts +5 -1
- package/src/input/commands.ts +2 -0
- package/src/input/handler-feed-routes.ts +60 -26
- package/src/input/handler-feed.ts +17 -8
- package/src/input/handler-picker-routes.ts +18 -13
- package/src/input/handler-shortcuts.ts +51 -0
- package/src/input/handler-ui-state.ts +4 -0
- package/src/input/handler.ts +43 -6
- package/src/input/keybindings.ts +53 -8
- package/src/input/model-picker.ts +5 -0
- package/src/input/panel-integration-actions.ts +117 -1
- package/src/main.ts +5 -2
- package/src/panels/agent-inspector-panel.ts +139 -45
- package/src/panels/agent-inspector-shared.ts +147 -3
- package/src/panels/approval-panel.ts +204 -102
- package/src/panels/automation-control-panel.ts +370 -103
- package/src/panels/base-panel.ts +26 -2
- 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 +253 -75
- package/src/panels/cockpit-read-model.ts +2 -1
- package/src/panels/communication-panel.ts +159 -56
- package/src/panels/confirm-state.ts +8 -1
- package/src/panels/control-plane-panel.ts +354 -95
- package/src/panels/cost-tracker-panel.ts +243 -57
- package/src/panels/debug-panel.ts +347 -159
- package/src/panels/diff-panel.ts +257 -82
- package/src/panels/docs-panel.ts +166 -64
- package/src/panels/eval-panel.ts +336 -109
- package/src/panels/expandable-list-panel.ts +189 -0
- package/src/panels/file-explorer-panel.ts +238 -159
- package/src/panels/file-preview-panel.ts +141 -59
- package/src/panels/git-panel.ts +360 -191
- package/src/panels/hooks-panel.ts +181 -19
- package/src/panels/incident-review-panel.ts +252 -43
- package/src/panels/index.ts +0 -4
- package/src/panels/intelligence-panel.ts +310 -103
- package/src/panels/knowledge-graph-panel.ts +480 -58
- package/src/panels/local-auth-panel.ts +251 -33
- package/src/panels/marketplace-panel.ts +219 -33
- package/src/panels/memory-panel.ts +83 -81
- package/src/panels/ops-control-panel.ts +211 -32
- package/src/panels/ops-strategy-panel.ts +131 -45
- package/src/panels/orchestration-panel.ts +259 -67
- package/src/panels/panel-list-panel.ts +184 -136
- package/src/panels/panel-manager.ts +120 -13
- package/src/panels/plan-dashboard-panel.ts +507 -74
- package/src/panels/plugins-panel.ts +227 -34
- package/src/panels/policy-panel.ts +264 -55
- package/src/panels/polish-core.ts +162 -0
- package/src/panels/polish-tables.ts +258 -0
- package/src/panels/polish.ts +67 -149
- package/src/panels/project-planning-answer-actions.ts +134 -0
- package/src/panels/project-planning-panel.ts +84 -113
- package/src/panels/provider-health-panel.ts +438 -480
- 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 +136 -38
- package/src/panels/remote-panel.ts +123 -38
- package/src/panels/routes-panel.ts +87 -24
- package/src/panels/sandbox-panel.ts +232 -65
- package/src/panels/scrollable-list-panel.ts +143 -145
- package/src/panels/security-panel.ts +204 -52
- package/src/panels/services-panel.ts +174 -76
- package/src/panels/session-browser-panel.ts +101 -6
- package/src/panels/session-maintenance.ts +4 -122
- package/src/panels/settings-sync-panel.ts +346 -68
- package/src/panels/skills-panel.ts +157 -89
- package/src/panels/subscription-panel.ts +105 -34
- package/src/panels/symbol-outline-panel.ts +287 -154
- package/src/panels/system-messages-panel.ts +172 -38
- package/src/panels/tasks-panel.ts +348 -134
- package/src/panels/thinking-panel.ts +66 -32
- package/src/panels/token-budget-panel.ts +233 -46
- package/src/panels/tool-inspector-panel.ts +170 -54
- package/src/panels/types.ts +65 -4
- package/src/panels/work-plan-panel.ts +393 -39
- package/src/panels/worktree-panel.ts +239 -61
- package/src/panels/wrfc-panel-format.ts +133 -0
- package/src/panels/wrfc-panel.ts +146 -150
- package/src/renderer/compaction-preview.ts +2 -1
- package/src/renderer/compositor.ts +46 -43
- 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 +30 -11
- package/src/renderer/model-workspace.ts +2 -3
- package/src/renderer/overlay-box.ts +16 -10
- package/src/renderer/panel-composite.ts +7 -20
- package/src/renderer/panel-workspace-bar.ts +14 -8
- package/src/renderer/process-indicator.ts +3 -1
- package/src/renderer/progress.ts +8 -6
- 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/tab-strip.ts +148 -34
- package/src/renderer/theme.ts +60 -3
- package/src/renderer/ui-factory.ts +45 -36
- 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/runtime/ui/model-picker/health-enrichment.ts +3 -0
- package/src/runtime/ui/model-picker/types.ts +4 -0
- package/src/shell/ui-openers.ts +14 -22
- package/src/utils/format-duration.ts +55 -0
- package/src/utils/format-number.ts +71 -0
- 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 -666
- package/src/panels/agent-logs-shared.ts +0 -129
- package/src/panels/context-visualizer-panel.ts +0 -214
- package/src/panels/forensics-panel.ts +0 -364
- package/src/panels/panel-picker.ts +0 -106
- package/src/panels/provider-account-snapshot.ts +0 -259
- package/src/panels/provider-accounts-panel.ts +0 -218
- package/src/panels/provider-stats-panel.ts +0 -366
- package/src/panels/schedule-panel.ts +0 -342
- package/src/panels/watchers-panel.ts +0 -193
- package/src/renderer/panel-picker-overlay.ts +0 -202
- package/src/renderer/panel-tab-bar.ts +0 -69
- package/src/runtime/diagnostics/panels/panel-resources.ts +0 -118
- package/src/runtime/diagnostics/panels/policy.ts +0 -177
|
@@ -8,7 +8,9 @@ import {
|
|
|
8
8
|
putOverlayText,
|
|
9
9
|
} from './overlay-box.ts';
|
|
10
10
|
import { getOverlayMaxWidth } from './overlay-viewport.ts';
|
|
11
|
-
import { GLYPHS
|
|
11
|
+
import { GLYPHS } from './ui-primitives.ts';
|
|
12
|
+
import { resolveUiTones } from './theme.ts';
|
|
13
|
+
import { HINT_SEPARATOR } from './hint-grammar.ts';
|
|
12
14
|
|
|
13
15
|
// ── Helpers ──────────────────────────────────────────────────────────────────
|
|
14
16
|
|
|
@@ -90,19 +92,24 @@ export interface ModalConfig {
|
|
|
90
92
|
|
|
91
93
|
// ── Defaults ─────────────────────────────────────────────────────────────────
|
|
92
94
|
|
|
95
|
+
// DEFAULT_STYLE is built from the mode-resolved chrome tones (resolveUiTones)
|
|
96
|
+
// rather than the static UI_TONES constant — WO-001 single read path. Mode is
|
|
97
|
+
// fixed to 'dark' until the terminal-bg-probe lands.
|
|
98
|
+
const TONES = resolveUiTones('dark');
|
|
99
|
+
|
|
93
100
|
const DEFAULT_STYLE: Required<ModalStyle> = {
|
|
94
|
-
titleFg:
|
|
95
|
-
borderFg:
|
|
96
|
-
hintFg:
|
|
97
|
-
selectedFg:
|
|
98
|
-
selectedBg:
|
|
99
|
-
textFg:
|
|
100
|
-
accentFg:
|
|
101
|
-
titleRowFg:
|
|
102
|
-
titleBg:
|
|
103
|
-
sectionBg:
|
|
104
|
-
inputBg:
|
|
105
|
-
surfaceBg:
|
|
101
|
+
titleFg: TONES.fg.primary,
|
|
102
|
+
borderFg: TONES.fg.dim,
|
|
103
|
+
hintFg: TONES.fg.muted,
|
|
104
|
+
selectedFg: TONES.fg.primary,
|
|
105
|
+
selectedBg: TONES.bg.selected,
|
|
106
|
+
textFg: TONES.fg.primary,
|
|
107
|
+
accentFg: TONES.state.info,
|
|
108
|
+
titleRowFg: TONES.fg.secondary,
|
|
109
|
+
titleBg: TONES.bg.title,
|
|
110
|
+
sectionBg: TONES.bg.section,
|
|
111
|
+
inputBg: TONES.bg.input,
|
|
112
|
+
surfaceBg: TONES.bg.surface,
|
|
106
113
|
};
|
|
107
114
|
|
|
108
115
|
// ── ModalFactory ─────────────────────────────────────────────────────────────
|
|
@@ -182,9 +189,10 @@ export class ModalFactory {
|
|
|
182
189
|
}
|
|
183
190
|
}
|
|
184
191
|
|
|
185
|
-
// Footer / hint bar
|
|
192
|
+
// Footer / hint bar. Hint segments join with the shared middle-dot grammar
|
|
193
|
+
// separator (WO-151) so every modal footer reads as one dialect.
|
|
186
194
|
const hintStr = config.hints
|
|
187
|
-
? config.hints.join(
|
|
195
|
+
? config.hints.join(HINT_SEPARATOR)
|
|
188
196
|
: (config.footer ?? '');
|
|
189
197
|
lines.push(ModalFactory.renderHints(boxW, margin, hintStr, terminalWidth, style));
|
|
190
198
|
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { type Line } from '../types/grid.ts';
|
|
2
2
|
import { fitDisplay, getDisplayWidth, truncateDisplay } from '../utils/terminal-width.ts';
|
|
3
|
+
import { abbreviateCount } from '../utils/format-number.ts';
|
|
3
4
|
import type { ModelPickerModal } from '../input/model-picker.ts';
|
|
4
5
|
import { EFFORT_DESCRIPTIONS } from '@pellux/goodvibes-sdk/platform/providers';
|
|
5
6
|
import { getQualityTier, getQualityTierFromScore } from '@pellux/goodvibes-sdk/platform/providers';
|
|
@@ -12,12 +13,11 @@ import {
|
|
|
12
13
|
putOverlayText,
|
|
13
14
|
} from './overlay-box.ts';
|
|
14
15
|
import { getOverlaySurfaceMetrics } from './overlay-viewport.ts';
|
|
16
|
+
import { formatHints, type HintSpec } from './hint-grammar.ts';
|
|
15
17
|
|
|
16
18
|
/** Format a context window number into a short human-readable string. */
|
|
17
19
|
function fmtContext(n: number): string {
|
|
18
|
-
|
|
19
|
-
if (n >= 1000) return `${Math.round(n / 1000)}k`;
|
|
20
|
-
return String(n);
|
|
20
|
+
return abbreviateCount(n, { rounding: 'round', decimals: 0 });
|
|
21
21
|
}
|
|
22
22
|
|
|
23
23
|
/** Title text per picker mode. */
|
|
@@ -360,6 +360,12 @@ export function renderModelPickerOverlay(
|
|
|
360
360
|
putRowText(promptRow, layout.margin + 2, contentW, fitDisplay(`${promptLabel} ${inputDisplay}`, contentW), '#ffffff');
|
|
361
361
|
lines.push(promptRow);
|
|
362
362
|
|
|
363
|
+
if (picker.contextCapError) {
|
|
364
|
+
const errRow = createOverlayContentLine(width, layout, borderFg, DEFAULT_OVERLAY_PALETTE.bodyBg);
|
|
365
|
+
putRowText(errRow, layout.margin + 2, contentW, fitDisplay(picker.contextCapError, contentW), '#ff6666');
|
|
366
|
+
lines.push(errRow);
|
|
367
|
+
}
|
|
368
|
+
|
|
363
369
|
lines.push(createOverlayContentLine(width, layout, borderFg, DEFAULT_OVERLAY_PALETTE.bodyBg));
|
|
364
370
|
|
|
365
371
|
const hintText = `Leave blank to use default (current: ${currentCtx}, source: ${provenance})`;
|
|
@@ -408,13 +414,26 @@ export function renderModelPickerOverlay(
|
|
|
408
414
|
const groupByLabel = picker.groupBy ?? 'provider';
|
|
409
415
|
const selectedModel = picker.mode === 'model' ? picker.getSelected() : null;
|
|
410
416
|
const showContextCapHint = selectedModel != null && selectedModel.contextWindowProvenance !== undefined;
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
417
|
+
let hints: string;
|
|
418
|
+
if (picker.mode === 'model') {
|
|
419
|
+
// The model-picker box caps at 72 cols, so the footer stays compact: the
|
|
420
|
+
// list nav (Up/Down/Enter) is the obvious interaction and lives in the
|
|
421
|
+
// /shortcuts overlay; the footer prioritizes the stateful controls. The
|
|
422
|
+
// live filter/group value is folded into each key's verb so it stays
|
|
423
|
+
// adjacent to its control and reads inside the width.
|
|
424
|
+
const modelHints: HintSpec[] = [{ key: '/', verb: 'Search' }];
|
|
425
|
+
if (showContextCapHint) modelHints.push({ key: 'Space', verb: 'Ctx' });
|
|
426
|
+
modelHints.push(
|
|
427
|
+
{ key: 'Tab', verb: `Filter: ${filterLabelFooter}` },
|
|
428
|
+
{ key: 'G', verb: `Group: ${groupByLabel}` },
|
|
429
|
+
{ key: 'Esc', verb: 'Close' },
|
|
430
|
+
);
|
|
431
|
+
hints = formatHints(modelHints);
|
|
432
|
+
} else if (picker.mode === 'contextCap') {
|
|
433
|
+
hints = formatHints([{ key: 'Enter', verb: 'Confirm' }, { key: 'Esc', verb: 'Cancel' }]);
|
|
434
|
+
} else {
|
|
435
|
+
hints = formatHints([{ key: 'Up/Down', verb: 'Move' }, { key: 'Enter', verb: 'Select' }, { key: 'Esc', verb: 'Cancel' }]);
|
|
436
|
+
}
|
|
418
437
|
const footerLine = createOverlayFilledBorderLine(width, layout, OVERLAY_GLYPHS.bottomLeft, OVERLAY_GLYPHS.horizontal, OVERLAY_GLYPHS.bottomRight, borderFg, DEFAULT_OVERLAY_PALETTE.sectionBg);
|
|
419
438
|
putRowText(footerLine, layout.margin + 2, contentW, fitDisplay(truncateDisplay(hints, contentW), contentW), mutedFg, '', false, true);
|
|
420
439
|
lines.push(footerLine);
|
|
@@ -458,7 +477,7 @@ function getRenderCacheKey(
|
|
|
458
477
|
} else if (picker.mode === 'effort') {
|
|
459
478
|
base.push(objectId(picker.effortLevels), picker.effortLevels.join('\u001f'), picker.pendingModel?.registryKey ?? picker.pendingModel?.id ?? '');
|
|
460
479
|
} else if (picker.mode === 'contextCap') {
|
|
461
|
-
base.push(picker.contextCapQuery, picker.contextCapPendingModel?.registryKey ?? picker.contextCapPendingModel?.id ?? '');
|
|
480
|
+
base.push(picker.contextCapQuery, picker.contextCapPendingModel?.registryKey ?? picker.contextCapPendingModel?.id ?? '', picker.contextCapError ?? '');
|
|
462
481
|
}
|
|
463
482
|
|
|
464
483
|
return base.join('\u001e');
|
|
@@ -11,6 +11,7 @@ import type { ModelPickerTargetInfo } from '../input/model-picker.ts';
|
|
|
11
11
|
import type { Line } from '../types/grid.ts';
|
|
12
12
|
import { createEmptyLine, createStyledCell } from '../types/grid.ts';
|
|
13
13
|
import { getDisplayWidth, wrapText } from '../utils/terminal-width.ts';
|
|
14
|
+
import { abbreviateCount } from '../utils/format-number.ts';
|
|
14
15
|
import { GLYPHS, UI_TONES } from './ui-primitives.ts';
|
|
15
16
|
|
|
16
17
|
const PALETTE = {
|
|
@@ -129,9 +130,7 @@ function stableWindow(total: number, selected: number, visible: number): { start
|
|
|
129
130
|
|
|
130
131
|
function formatContext(value: number | undefined): string {
|
|
131
132
|
if (!value) return '-';
|
|
132
|
-
|
|
133
|
-
if (value >= 1000) return `${Math.round(value / 1000)}k`;
|
|
134
|
-
return String(value);
|
|
133
|
+
return abbreviateCount(value, { rounding: 'round', decimals: 0 });
|
|
135
134
|
}
|
|
136
135
|
|
|
137
136
|
function modelKey(model: ModelDefinition): string {
|
|
@@ -1,8 +1,14 @@
|
|
|
1
1
|
import { createStyledCell, type Line } from '../types/grid.ts';
|
|
2
2
|
import { getOverlayMaxWidth } from './overlay-viewport.ts';
|
|
3
|
-
import { GLYPHS
|
|
3
|
+
import { GLYPHS } from './ui-primitives.ts';
|
|
4
|
+
import { resolveUiTones } from './theme.ts';
|
|
4
5
|
import { fillWidth, makeLine, writeText } from './fullscreen-primitives.ts';
|
|
5
6
|
|
|
7
|
+
// DEFAULT_OVERLAY_PALETTE is built from the mode-resolved chrome tones
|
|
8
|
+
// (resolveUiTones) rather than the static UI_TONES constant — WO-001 single
|
|
9
|
+
// read path. Mode is fixed to 'dark' until the terminal-bg-probe lands.
|
|
10
|
+
const TONES = resolveUiTones('dark');
|
|
11
|
+
|
|
6
12
|
export interface OverlayBoxPalette {
|
|
7
13
|
readonly borderFg: string;
|
|
8
14
|
readonly titleFg: string;
|
|
@@ -16,15 +22,15 @@ export interface OverlayBoxPalette {
|
|
|
16
22
|
}
|
|
17
23
|
|
|
18
24
|
export const DEFAULT_OVERLAY_PALETTE: Readonly<OverlayBoxPalette> = {
|
|
19
|
-
borderFg:
|
|
20
|
-
titleFg:
|
|
21
|
-
bodyFg:
|
|
22
|
-
mutedFg:
|
|
23
|
-
selectedBg:
|
|
24
|
-
titleBg:
|
|
25
|
-
sectionBg:
|
|
26
|
-
inputBg:
|
|
27
|
-
bodyBg:
|
|
25
|
+
borderFg: TONES.fg.secondary,
|
|
26
|
+
titleFg: TONES.fg.primary,
|
|
27
|
+
bodyFg: TONES.fg.primary,
|
|
28
|
+
mutedFg: TONES.fg.dim,
|
|
29
|
+
selectedBg: TONES.bg.selected,
|
|
30
|
+
titleBg: TONES.bg.title,
|
|
31
|
+
sectionBg: TONES.bg.section,
|
|
32
|
+
inputBg: TONES.bg.input,
|
|
33
|
+
bodyBg: TONES.bg.surface,
|
|
28
34
|
} as const;
|
|
29
35
|
|
|
30
36
|
export interface OverlayBoxLayout {
|
|
@@ -4,7 +4,6 @@ import type { InputHandler } from '../input/handler.ts';
|
|
|
4
4
|
import type { PanelManager } from '../panels/panel-manager.ts';
|
|
5
5
|
import type { PanelCompositeData } from './compositor.ts';
|
|
6
6
|
import { createSplitPaneLayout } from './layout-engine.ts';
|
|
7
|
-
import { renderPanelTabBar } from './panel-tab-bar.ts';
|
|
8
7
|
import { renderPanelWorkspaceBar } from './panel-workspace-bar.ts';
|
|
9
8
|
|
|
10
9
|
/**
|
|
@@ -108,33 +107,22 @@ export function buildPanelCompositeData(
|
|
|
108
107
|
const workspaceBar = renderPanelWorkspaceBar(workspaceTabs, panelWidth, input.panelFocused);
|
|
109
108
|
|
|
110
109
|
let topContent: Line[];
|
|
111
|
-
let topTabBar: Line | undefined;
|
|
112
|
-
let bottomTabBar: Line | undefined;
|
|
113
110
|
let bottomContent: Line[] | undefined;
|
|
114
111
|
|
|
115
112
|
const topActivePanel = topPane.panels[topPane.activeIndex] ?? null;
|
|
116
113
|
|
|
117
114
|
if (hasBottom) {
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
);
|
|
125
|
-
const paneLayout = createSplitPaneLayout(Math.max(0, panelHeight - 1), verticalSplitRatio);
|
|
115
|
+
// Single consolidated workspace bar (1 row) + h-separator (1 row); the rest
|
|
116
|
+
// splits between the two panes' content.
|
|
117
|
+
const paneLayout = createSplitPaneLayout(Math.max(0, panelHeight - 1), verticalSplitRatio, {
|
|
118
|
+
topTabRows: 0,
|
|
119
|
+
bottomTabRows: 0,
|
|
120
|
+
});
|
|
126
121
|
const topH = paneLayout.topContentRows;
|
|
127
122
|
const bottomH = paneLayout.bottomContentRows;
|
|
128
123
|
topContent = topActivePanel ? renderPanel(topActivePanel, panelWidth, topH) : [];
|
|
129
124
|
|
|
130
125
|
const bottomActivePanel = bottomPane.panels[bottomPane.activeIndex] ?? null;
|
|
131
|
-
bottomTabBar = renderPanelTabBar(
|
|
132
|
-
bottomPane.panels,
|
|
133
|
-
bottomPane.activeIndex,
|
|
134
|
-
panelWidth,
|
|
135
|
-
input.panelFocused && focusedPane === 'bottom',
|
|
136
|
-
'bottom',
|
|
137
|
-
);
|
|
138
126
|
bottomContent = bottomActivePanel ? renderPanel(bottomActivePanel, panelWidth, bottomH) : [];
|
|
139
127
|
} else {
|
|
140
128
|
const topH = Math.max(0, panelHeight - 1);
|
|
@@ -144,10 +132,9 @@ export function buildPanelCompositeData(
|
|
|
144
132
|
return {
|
|
145
133
|
panelData: {
|
|
146
134
|
workspaceBar,
|
|
147
|
-
topTabBar,
|
|
148
135
|
topContent,
|
|
149
136
|
topFocused: input.panelFocused && focusedPane === 'top',
|
|
150
|
-
|
|
137
|
+
hasBottomPane: hasBottom,
|
|
151
138
|
bottomContent,
|
|
152
139
|
bottomFocused: input.panelFocused && focusedPane === 'bottom',
|
|
153
140
|
separator: true,
|
|
@@ -1,21 +1,26 @@
|
|
|
1
1
|
import type { Line } from '../types/grid.ts';
|
|
2
2
|
import type { WorkspaceTab } from '../panels/panel-manager.ts';
|
|
3
|
-
import { renderTabStrip } from './tab-strip.ts';
|
|
3
|
+
import { renderTabStrip, type TabHitRegion } from './tab-strip.ts';
|
|
4
|
+
import { UI_TONES } from './ui-primitives.ts';
|
|
4
5
|
|
|
5
|
-
|
|
6
|
-
const
|
|
7
|
-
const
|
|
8
|
-
const
|
|
9
|
-
const
|
|
10
|
-
const
|
|
6
|
+
// Theme tokens (no raw hex) — keeps the bar in sync with the shared palette.
|
|
7
|
+
const ACTIVE_FG = UI_TONES.fg.primary; // selected tab text
|
|
8
|
+
const ACTIVE_BG = UI_TONES.bg.selected; // selected tab fill (stronger contrast)
|
|
9
|
+
const INACTIVE_FG = UI_TONES.fg.muted; // unselected tabs
|
|
10
|
+
const SEPARATOR_FG = UI_TONES.fg.dim; // │ separators
|
|
11
|
+
const LABEL_FG = UI_TONES.fg.secondary; // PANELS prefix label
|
|
12
|
+
const LABEL_BG = UI_TONES.bg.title; // prefix label background when focused
|
|
13
|
+
const FOCUS_ACCENT_FG = UI_TONES.state.active; // the one focused tab
|
|
11
14
|
|
|
12
15
|
export function renderPanelWorkspaceBar(
|
|
13
16
|
tabs: readonly WorkspaceTab[],
|
|
14
17
|
width: number,
|
|
15
18
|
focused: boolean,
|
|
19
|
+
onLayout?: (regions: readonly TabHitRegion[]) => void,
|
|
16
20
|
): Line {
|
|
17
21
|
return renderTabStrip({
|
|
18
22
|
width,
|
|
23
|
+
onLayout,
|
|
19
24
|
tabs: tabs.map((tab) => ({
|
|
20
25
|
// tab.active = selected in its own pane (drives highlighted background).
|
|
21
26
|
// tab.focused = has keyboard focus (drives brighter text / focus indicator).
|
|
@@ -32,7 +37,8 @@ export function renderPanelWorkspaceBar(
|
|
|
32
37
|
activeBold: focused,
|
|
33
38
|
inactiveFg: INACTIVE_FG,
|
|
34
39
|
separatorFg: SEPARATOR_FG,
|
|
35
|
-
|
|
40
|
+
// Accent the PANELS chip when focused so it ties to the focus border.
|
|
41
|
+
labelFg: focused ? FOCUS_ACCENT_FG : LABEL_FG,
|
|
36
42
|
labelBg: focused ? LABEL_BG : '',
|
|
37
43
|
labelBold: focused,
|
|
38
44
|
overflowFg: SEPARATOR_FG,
|
|
@@ -87,8 +87,10 @@ export function renderProcessIndicator(
|
|
|
87
87
|
*/
|
|
88
88
|
const PROGRESS_RESERVED_CHARS = 43;
|
|
89
89
|
const progressMaxLen = Math.max(0, width - PROGRESS_RESERVED_CHARS); // reserve space for count + hint
|
|
90
|
+
// Truncate by display width (not JS string length) so wide/CJK glyphs in the
|
|
91
|
+
// agent progress text don't overflow the reserved budget.
|
|
90
92
|
const progressSuffix = agentProgress && progressMaxLen > 10
|
|
91
|
-
? ` | ${agentProgress
|
|
93
|
+
? ` | ${getDisplayWidth(agentProgress) > progressMaxLen ? truncateToWidth(agentProgress, Math.max(0, progressMaxLen - 3)) + '...' : agentProgress}`
|
|
92
94
|
: '';
|
|
93
95
|
const label = `${parts.join(` ${GLYPHS.navigation.pipeSeparator} `)}${progressSuffix}`;
|
|
94
96
|
const hint = ` ${GLYPHS.status.pending} Enter to view`;
|
package/src/renderer/progress.ts
CHANGED
|
@@ -1,11 +1,13 @@
|
|
|
1
1
|
import { type Line } from '../types/grid.ts';
|
|
2
2
|
import { UIFactory } from './ui-factory.ts';
|
|
3
3
|
import { getDisplayWidth, padDisplayEnd } from '../utils/terminal-width.ts';
|
|
4
|
+
import { abbreviateCount } from '../utils/format-number.ts';
|
|
5
|
+
import { SPINNER_FRAMES } from './ui-primitives.ts';
|
|
4
6
|
|
|
5
|
-
// Rich spinner frames (used by progress indicators)
|
|
6
|
-
export
|
|
7
|
-
// Braille thinking spinner frames (used by the orchestrator thinking animation)
|
|
8
|
-
export const THINKING_SPINNER_FRAMES =
|
|
7
|
+
// Rich spinner frames (used by progress indicators) — single source in ui-primitives.ts.
|
|
8
|
+
export { SPINNER_FRAMES };
|
|
9
|
+
// Braille thinking spinner frames (used by the orchestrator thinking animation) — same frame set.
|
|
10
|
+
export const THINKING_SPINNER_FRAMES = SPINNER_FRAMES;
|
|
9
11
|
|
|
10
12
|
/**
|
|
11
13
|
* renderSpinner - Render a spinner with label as a single Line.
|
|
@@ -51,8 +53,8 @@ export function renderTokenBar(
|
|
|
51
53
|
const lines: Line[] = [];
|
|
52
54
|
|
|
53
55
|
// Stats row
|
|
54
|
-
const usedK = used
|
|
55
|
-
const maxK = max
|
|
56
|
+
const usedK = abbreviateCount(used, { noM: true });
|
|
57
|
+
const maxK = abbreviateCount(max, { noM: true });
|
|
56
58
|
const pct = max > 0 ? Math.min(100, Math.round((used / max) * 100)) : 0;
|
|
57
59
|
const toolStr = toolCount > 0 ? ` tools:${toolCount}` : '';
|
|
58
60
|
const statsText = ` ${model} in:${usedK}/${maxK} (${pct}%)${toolStr}`;
|
|
@@ -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
|
};
|