@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
|
@@ -40,12 +40,21 @@ export function applyConversationOverlays(
|
|
|
40
40
|
let next = viewport;
|
|
41
41
|
const bottomDockInset = 1 + (input.searchManager.active || input.historySearch.active ? 1 : 0);
|
|
42
42
|
|
|
43
|
+
// Overlay posture rule: workspaces (onboarding, model, settings, mcp, help,
|
|
44
|
+
// shortcuts) claim the full viewport and are mutually exclusive with docked
|
|
45
|
+
// pickers/modals. Once a fullscreen workspace claims the screen, no docked
|
|
46
|
+
// overlay is drawn on top of it — enforcing a single visible overlay even if
|
|
47
|
+
// stray state survives the handler's clearModalStack. The input layer keeps
|
|
48
|
+
// the modal stack to one entry; this is the renderer-side backstop.
|
|
49
|
+
let fullscreenClaimed = false;
|
|
50
|
+
|
|
43
51
|
if (input.onboardingWizard.active) {
|
|
44
52
|
const lines = renderOnboardingWizard(input.onboardingWizard, conversationWidth, viewportHeight);
|
|
45
53
|
next = replaceViewportWithOverlay(lines, conversationWidth, viewportHeight);
|
|
54
|
+
fullscreenClaimed = true;
|
|
46
55
|
}
|
|
47
56
|
|
|
48
|
-
if (input.filePicker.active) {
|
|
57
|
+
if (!fullscreenClaimed && input.filePicker.active) {
|
|
49
58
|
const lines = renderFilePickerOverlay(input.filePicker, conversationWidth, viewportHeight);
|
|
50
59
|
next = overlayViewportBottom(next, lines, conversationWidth, viewportHeight, bottomDockInset);
|
|
51
60
|
}
|
|
@@ -53,9 +62,10 @@ export function applyConversationOverlays(
|
|
|
53
62
|
if (input.modelPicker.active) {
|
|
54
63
|
const lines = renderModelWorkspace(input.modelPicker, conversationWidth, viewportHeight);
|
|
55
64
|
next = replaceViewportWithOverlay(lines, conversationWidth, viewportHeight);
|
|
65
|
+
fullscreenClaimed = true;
|
|
56
66
|
}
|
|
57
67
|
|
|
58
|
-
if (input.selectionModal.active) {
|
|
68
|
+
if (!fullscreenClaimed && input.selectionModal.active) {
|
|
59
69
|
const lines = renderSelectionModalOverlay(input.selectionModal, conversationWidth, viewportHeight);
|
|
60
70
|
next = overlayViewportBottom(next, lines, conversationWidth, viewportHeight, bottomDockInset);
|
|
61
71
|
}
|
|
@@ -68,22 +78,22 @@ export function applyConversationOverlays(
|
|
|
68
78
|
next.push(...renderHistorySearchOverlay(input.historySearch, conversationWidth));
|
|
69
79
|
}
|
|
70
80
|
|
|
71
|
-
if (input.processModal.active) {
|
|
81
|
+
if (!fullscreenClaimed && input.processModal.active) {
|
|
72
82
|
const lines = renderProcessModal(input.processModal, conversationWidth, viewportHeight);
|
|
73
83
|
next = overlayViewportBottom(next, lines, conversationWidth, viewportHeight, bottomDockInset);
|
|
74
84
|
}
|
|
75
85
|
|
|
76
|
-
if (input.agentDetailModal.active) {
|
|
77
|
-
const lines = renderAgentDetailModal(input.agentDetailModal, conversationWidth);
|
|
86
|
+
if (!fullscreenClaimed && input.agentDetailModal.active) {
|
|
87
|
+
const lines = renderAgentDetailModal(input.agentDetailModal, conversationWidth, viewportHeight);
|
|
78
88
|
next = overlayViewportBottom(next, lines, conversationWidth, viewportHeight, bottomDockInset);
|
|
79
89
|
}
|
|
80
90
|
|
|
81
|
-
if (input.liveTailModal.active) {
|
|
91
|
+
if (!fullscreenClaimed && input.liveTailModal.active) {
|
|
82
92
|
const lines = renderLiveTailModal(input.liveTailModal, conversationWidth, viewportHeight);
|
|
83
93
|
next = overlayViewportBottom(next, lines, conversationWidth, viewportHeight, bottomDockInset);
|
|
84
94
|
}
|
|
85
95
|
|
|
86
|
-
if (input.contextInspectorModal.active) {
|
|
96
|
+
if (!fullscreenClaimed && input.contextInspectorModal.active) {
|
|
87
97
|
const lines = renderContextInspector(conversation, conversationWidth, viewportHeight, contextWindow);
|
|
88
98
|
next = overlayViewportBottom(next, lines, conversationWidth, viewportHeight, bottomDockInset);
|
|
89
99
|
}
|
|
@@ -91,24 +101,26 @@ export function applyConversationOverlays(
|
|
|
91
101
|
if (input.settingsModal.active) {
|
|
92
102
|
const lines = renderSettingsModal(input.settingsModal, conversationWidth, viewportHeight);
|
|
93
103
|
next = replaceViewportWithOverlay(lines, conversationWidth, viewportHeight);
|
|
104
|
+
fullscreenClaimed = true;
|
|
94
105
|
}
|
|
95
106
|
|
|
96
107
|
if (input.mcpWorkspace.active) {
|
|
97
108
|
const lines = renderMcpWorkspace(input.mcpWorkspace, conversationWidth, viewportHeight);
|
|
98
109
|
next = replaceViewportWithOverlay(lines, conversationWidth, viewportHeight);
|
|
110
|
+
fullscreenClaimed = true;
|
|
99
111
|
}
|
|
100
112
|
|
|
101
|
-
if (input.sessionPickerModal.active) {
|
|
113
|
+
if (!fullscreenClaimed && input.sessionPickerModal.active) {
|
|
102
114
|
const lines = renderSessionPickerModal(input.sessionPickerModal, conversationWidth, viewportHeight);
|
|
103
115
|
next = overlayViewportBottom(next, lines, conversationWidth, viewportHeight, bottomDockInset);
|
|
104
116
|
}
|
|
105
117
|
|
|
106
|
-
if (input.profilePickerModal.active) {
|
|
118
|
+
if (!fullscreenClaimed && input.profilePickerModal.active) {
|
|
107
119
|
const lines = renderProfilePickerModal(input.profilePickerModal, conversationWidth, viewportHeight);
|
|
108
120
|
next = overlayViewportBottom(next, lines, conversationWidth, viewportHeight, bottomDockInset);
|
|
109
121
|
}
|
|
110
122
|
|
|
111
|
-
if (input.bookmarkModal.active) {
|
|
123
|
+
if (!fullscreenClaimed && input.bookmarkModal.active) {
|
|
112
124
|
const lines = renderBookmarkModal(input.bookmarkModal, conversationWidth, viewportHeight);
|
|
113
125
|
next = overlayViewportBottom(next, lines, conversationWidth, viewportHeight, bottomDockInset);
|
|
114
126
|
}
|
|
@@ -116,14 +128,16 @@ export function applyConversationOverlays(
|
|
|
116
128
|
if (input.helpOverlayActive) {
|
|
117
129
|
const lines = renderHelpOverlay(conversationWidth, keybindingsManager, commandRegistry.getAll(), input.helpScrollOffset, viewportHeight);
|
|
118
130
|
next = replaceViewportWithOverlay(lines, conversationWidth, viewportHeight);
|
|
131
|
+
fullscreenClaimed = true;
|
|
119
132
|
}
|
|
120
133
|
|
|
121
134
|
if (input.shortcutsOverlayActive) {
|
|
122
135
|
const lines = renderShortcutsOverlay(conversationWidth, keybindingsManager, input.shortcutsScrollOffset, viewportHeight);
|
|
123
136
|
next = replaceViewportWithOverlay(lines, conversationWidth, viewportHeight);
|
|
137
|
+
fullscreenClaimed = true;
|
|
124
138
|
}
|
|
125
139
|
|
|
126
|
-
if (input.commandMode && input.autocomplete?.isActive) {
|
|
140
|
+
if (!fullscreenClaimed && input.commandMode && input.autocomplete?.isActive) {
|
|
127
141
|
const lines = renderAutocompleteOverlay(input.autocomplete, conversationWidth, viewportHeight);
|
|
128
142
|
if (lines.length > 0) {
|
|
129
143
|
next = overlayViewportBottom(next, lines, conversationWidth, viewportHeight, bottomDockInset);
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* footer-tips.ts — the persistent discoverability hint shown in the shell
|
|
3
|
+
* footer's tip slot.
|
|
4
|
+
*
|
|
5
|
+
* WO-151: instead of a single frozen "/help for commands" line, the footer
|
|
6
|
+
* surfaces a rotating set of the highest-value affordances (panels, process
|
|
7
|
+
* monitor, help, quit). Rotation is *contextual, not timed*: when an agent turn
|
|
8
|
+
* is actively running, the process-monitor tip is promoted to the front so the
|
|
9
|
+
* operator can jump to F2 while work is in flight. Selection is a pure function
|
|
10
|
+
* of context so the footer renders deterministically (golden frames stay
|
|
11
|
+
* stable) — no wall-clock input.
|
|
12
|
+
*/
|
|
13
|
+
|
|
14
|
+
export interface FooterTipContext {
|
|
15
|
+
/** True while an agent turn is actively running (streaming / tools / hooks). */
|
|
16
|
+
readonly agentActive: boolean;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
/** Composer status labels that mean an agent turn is actively in flight. */
|
|
20
|
+
const ACTIVE_TURN_STATUSES = new Set(['preflight', 'streaming', 'tools', 'post-hooks']);
|
|
21
|
+
|
|
22
|
+
/** Derive agent-active state from the composer status label passed to the footer. */
|
|
23
|
+
export function isAgentActive(composerStatus: string | undefined): boolean {
|
|
24
|
+
return composerStatus !== undefined && ACTIVE_TURN_STATUSES.has(composerStatus);
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
const TIP_PANELS = 'Ctrl+P panels';
|
|
28
|
+
const TIP_PROCESSES = 'F2 processes';
|
|
29
|
+
const TIP_HELP = '? help';
|
|
30
|
+
const TIP_QUIT = 'Ctrl+C quit';
|
|
31
|
+
|
|
32
|
+
/**
|
|
33
|
+
* Build the footer discoverability tip. Segments join with the shared middle
|
|
34
|
+
* dot. Agent-active state promotes the process-monitor tip to the front.
|
|
35
|
+
*/
|
|
36
|
+
export function buildFooterTip(ctx: FooterTipContext): string {
|
|
37
|
+
const lead = ctx.agentActive
|
|
38
|
+
? [TIP_PROCESSES, TIP_PANELS, TIP_HELP]
|
|
39
|
+
: [TIP_PANELS, TIP_PROCESSES, TIP_HELP];
|
|
40
|
+
return [...lead, TIP_QUIT].join(' · ');
|
|
41
|
+
}
|
|
@@ -1,24 +1,30 @@
|
|
|
1
1
|
import type { Line } from '../types/grid.ts';
|
|
2
2
|
import { createEmptyLine, createStyledCell } from '../types/grid.ts';
|
|
3
3
|
import { getDisplayWidth } from '../utils/terminal-width.ts';
|
|
4
|
-
import { GLYPHS
|
|
4
|
+
import { GLYPHS } from './ui-primitives.ts';
|
|
5
|
+
import { resolveUiTones } from './theme.ts';
|
|
6
|
+
|
|
7
|
+
// FULLSCREEN_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');
|
|
5
11
|
|
|
6
12
|
export const FULLSCREEN_PALETTE = {
|
|
7
|
-
border:
|
|
8
|
-
title:
|
|
9
|
-
subtitle:
|
|
10
|
-
text:
|
|
11
|
-
muted:
|
|
12
|
-
dim:
|
|
13
|
-
selectedBg:
|
|
14
|
-
categoryBg:
|
|
15
|
-
contextBg:
|
|
16
|
-
controlsBg:
|
|
17
|
-
footerBg:
|
|
18
|
-
good:
|
|
19
|
-
warn:
|
|
20
|
-
bad:
|
|
21
|
-
info:
|
|
13
|
+
border: TONES.border,
|
|
14
|
+
title: TONES.accent.browser,
|
|
15
|
+
subtitle: TONES.accent.conversation,
|
|
16
|
+
text: TONES.fg.primary,
|
|
17
|
+
muted: TONES.fg.muted,
|
|
18
|
+
dim: TONES.border,
|
|
19
|
+
selectedBg: TONES.bg.selected,
|
|
20
|
+
categoryBg: TONES.bg.section,
|
|
21
|
+
contextBg: TONES.bg.surface,
|
|
22
|
+
controlsBg: TONES.bg.base,
|
|
23
|
+
footerBg: TONES.bg.footer,
|
|
24
|
+
good: TONES.state.good,
|
|
25
|
+
warn: TONES.state.warn,
|
|
26
|
+
bad: TONES.state.bad,
|
|
27
|
+
info: TONES.state.info,
|
|
22
28
|
} as const;
|
|
23
29
|
|
|
24
30
|
export type FullscreenTextStyle = Partial<Omit<Line[number], 'char'>>;
|
|
@@ -10,6 +10,7 @@ import type { SlashCommand } from '../input/command-registry.ts';
|
|
|
10
10
|
import type { KeybindingsManager } from '../input/keybindings.ts';
|
|
11
11
|
import { getOverlaySurfaceMetrics } from './overlay-viewport.ts';
|
|
12
12
|
import { getVisibleWindow } from './surface-layout.ts';
|
|
13
|
+
import { formatHints } from './hint-grammar.ts';
|
|
13
14
|
import { logger } from '@pellux/goodvibes-sdk/platform/utils';
|
|
14
15
|
|
|
15
16
|
function toModalSections(rows: readonly string[]): import('./modal-factory.ts').ModalSection[] {
|
|
@@ -40,7 +41,28 @@ export function renderHelpOverlay(
|
|
|
40
41
|
): Line[] {
|
|
41
42
|
const kb = (action: Parameters<typeof keybindingsManager.getComboLabel>[0]) => keybindingsManager.getComboLabel(action);
|
|
42
43
|
|
|
43
|
-
const hasCommand = (name: string): boolean =>
|
|
44
|
+
const hasCommand = (name: string): boolean => {
|
|
45
|
+
if (!commands) return false;
|
|
46
|
+
for (const command of commands) {
|
|
47
|
+
// A broken plugin may expose a throwing `aliases` getter; skip it rather
|
|
48
|
+
// than crash the overlay (mirrors the Quick Start traversal guard below).
|
|
49
|
+
try {
|
|
50
|
+
if (command.name === name || (command.aliases ?? []).includes(name)) return true;
|
|
51
|
+
} catch { /* skip this command */ }
|
|
52
|
+
}
|
|
53
|
+
return false;
|
|
54
|
+
};
|
|
55
|
+
|
|
56
|
+
const shortcutLine = (label: string, desc: string): string => ` ${label.padEnd(20)} ${desc}`;
|
|
57
|
+
|
|
58
|
+
// Enumerate EVERY workspace/panel binding straight from the live keybindings
|
|
59
|
+
// table so each rebindable action (including user overrides) is discoverable
|
|
60
|
+
// here and stays in lockstep with /keybindings. Any action id prefixed
|
|
61
|
+
// `panel-` is a workspace affordance.
|
|
62
|
+
const panelBindingRows: string[] = keybindingsManager
|
|
63
|
+
.getAll()
|
|
64
|
+
.filter((entry) => entry.action.startsWith('panel-'))
|
|
65
|
+
.map((entry) => shortcutLine(keybindingsManager.getComboLabel(entry.action), entry.description));
|
|
44
66
|
|
|
45
67
|
// Keyboard shortcut sections
|
|
46
68
|
const shortcutRows: string[] = [
|
|
@@ -59,9 +81,20 @@ export function renderHelpOverlay(
|
|
|
59
81
|
'',
|
|
60
82
|
' Overlays And Panels',
|
|
61
83
|
' ' + '\u2500'.repeat(40),
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
84
|
+
shortcutLine('?', 'Toggle help'),
|
|
85
|
+
shortcutLine('/shortcuts', 'Full keyboard shortcuts'),
|
|
86
|
+
shortcutLine('Tab', 'Swap focus between input and workspace'),
|
|
87
|
+
...panelBindingRows,
|
|
88
|
+
'',
|
|
89
|
+
// The shared in-panel contract every workspace panel honors. These are not
|
|
90
|
+
// rebindable global actions \u2014 they are the common controls a focused panel
|
|
91
|
+
// interprets \u2014 so they are documented here rather than pulled from getAll().
|
|
92
|
+
' In-Panel Controls',
|
|
93
|
+
' ' + '\u2500'.repeat(40),
|
|
94
|
+
shortcutLine('j / k', 'Move selection down / up'),
|
|
95
|
+
shortcutLine('g / G', 'Jump to top / bottom'),
|
|
96
|
+
shortcutLine('Enter / Esc', 'Activate / dismiss or leave panel'),
|
|
97
|
+
shortcutLine('/', 'Filter the list'),
|
|
65
98
|
'',
|
|
66
99
|
];
|
|
67
100
|
|
|
@@ -109,7 +142,33 @@ export function renderHelpOverlay(
|
|
|
109
142
|
}
|
|
110
143
|
}
|
|
111
144
|
|
|
145
|
+
// Essentials \u2014 the handful of commands worth memorizing, listed first and
|
|
146
|
+
// filtered to what the live registry actually exposes.
|
|
147
|
+
const ESSENTIAL_COMMANDS: Array<[name: string, desc: string]> = [
|
|
148
|
+
['model', 'Select provider or model'],
|
|
149
|
+
['sessions', 'Browse and resume saved sessions'],
|
|
150
|
+
['save', 'Save the current session'],
|
|
151
|
+
['compact', 'Compact the conversation history'],
|
|
152
|
+
['clear', 'Clear the conversation'],
|
|
153
|
+
['keybindings', 'List and customize key bindings'],
|
|
154
|
+
['panel', 'Open, focus, or manage panels'],
|
|
155
|
+
];
|
|
156
|
+
|
|
112
157
|
const commandRows: string[] = [];
|
|
158
|
+
// Track command names already surfaced in a curated group so the exhaustive
|
|
159
|
+
// list below does not repeat them.
|
|
160
|
+
const seen = new Set<string>();
|
|
161
|
+
|
|
162
|
+
const essentialRows: string[] = [];
|
|
163
|
+
for (const [name, desc] of ESSENTIAL_COMMANDS) {
|
|
164
|
+
if (!hasCommand(name)) continue;
|
|
165
|
+
seen.add(name);
|
|
166
|
+
essentialRows.push(` ${`/${name}`.padEnd(18)} ${desc}`);
|
|
167
|
+
}
|
|
168
|
+
if (essentialRows.length > 0) {
|
|
169
|
+
commandRows.push(' Essentials', ' ' + '\u2500'.repeat(40), ...essentialRows, '');
|
|
170
|
+
}
|
|
171
|
+
|
|
113
172
|
if (quickStartRows.length > 0) {
|
|
114
173
|
commandRows.push(' Quick Start', ' ' + '\u2500'.repeat(40), ...quickStartRows, '');
|
|
115
174
|
}
|
|
@@ -117,18 +176,18 @@ export function renderHelpOverlay(
|
|
|
117
176
|
if (commands && commands.length > 0) {
|
|
118
177
|
commandRows.push('', ' Available Slash Commands', ' ' + '\u2500'.repeat(40));
|
|
119
178
|
const preferred = ['setup', 'cockpit', 'settings', 'provider', 'subscription', 'marketplace', 'remote', 'sandbox', 'security', 'policy', 'incident', 'knowledge', 'hooks', 'orchestration', 'communication', 'tasks'];
|
|
120
|
-
const seen = new Set<string>();
|
|
121
179
|
for (const name of preferred) {
|
|
122
180
|
const cmd = commands.find((entry) => entry.name === name);
|
|
123
|
-
if (!cmd) continue;
|
|
181
|
+
if (!cmd || seen.has(cmd.name)) continue;
|
|
124
182
|
seen.add(cmd.name);
|
|
125
183
|
const nameCol = `/${cmd.name}`.padEnd(18);
|
|
126
184
|
commandRows.push(` ${nameCol} ${cmd.description}`);
|
|
127
185
|
}
|
|
186
|
+
// No command cap: the overlay windows and scrolls (getVisibleWindow below),
|
|
187
|
+
// so the full remaining registry is listed rather than truncated at 24.
|
|
128
188
|
const remainder = [...commands]
|
|
129
189
|
.filter((cmd) => !seen.has(cmd.name))
|
|
130
|
-
.sort((a, b) => a.name.localeCompare(b.name))
|
|
131
|
-
.slice(0, 24);
|
|
190
|
+
.sort((a, b) => a.name.localeCompare(b.name));
|
|
132
191
|
if (remainder.length > 0) {
|
|
133
192
|
commandRows.push('', ' More Commands', ' ' + '\u2500'.repeat(40));
|
|
134
193
|
for (const cmd of remainder) {
|
|
@@ -163,15 +222,19 @@ export function renderHelpOverlay(
|
|
|
163
222
|
width: metrics.boxWidth,
|
|
164
223
|
margin: metrics.margin,
|
|
165
224
|
targetContentRows: metrics.contentRows,
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
],
|
|
225
|
+
// Single tab: the old second 'Commands' tab had no switch handler (Left/
|
|
226
|
+
// Right did nothing), so it was a dead affordance. The overlay is one
|
|
227
|
+
// scrolling surface, so it advertises exactly one tab.
|
|
228
|
+
tabs: [{ label: 'Overview', active: true }],
|
|
170
229
|
sections: toModalSections(visibleRows),
|
|
171
230
|
helpers: allRows.length > maxVisible
|
|
172
231
|
? [{ content: `[${window.start + 1}-${Math.min(allRows.length, clampedOffset + visibleRows.length)} of ${allRows.length}]` }]
|
|
173
232
|
: undefined,
|
|
174
|
-
hints: [
|
|
233
|
+
hints: [formatHints([
|
|
234
|
+
{ key: 'Up/Down', verb: 'Scroll' },
|
|
235
|
+
{ key: '?', verb: 'Help' },
|
|
236
|
+
{ key: 'Esc', verb: 'Close' },
|
|
237
|
+
])],
|
|
175
238
|
},
|
|
176
239
|
width,
|
|
177
240
|
);
|
|
@@ -236,6 +299,17 @@ export function renderShortcutsOverlay(
|
|
|
236
299
|
row(kb('panel-picker'), 'Open / focus / hide panel workspace'),
|
|
237
300
|
row(kb('panel-tab-next'), 'Next workspace panel tab'),
|
|
238
301
|
row(kb('panel-tab-prev'), 'Previous workspace panel tab'),
|
|
302
|
+
row(`${kb('panel-tab-1')}\u2026${kb('panel-tab-9')}`, 'Jump to workspace tab 1-9'),
|
|
303
|
+
row(kb('panel-focus-toggle'), 'Swap focus between top / bottom pane'),
|
|
304
|
+
row(kb('panel-close'), 'Close active panel'),
|
|
305
|
+
row(kb('panel-close-all'), 'Close all panels'),
|
|
306
|
+
row(kb('panel-ops'), 'Open the Ops Control panel'),
|
|
307
|
+
'',
|
|
308
|
+
' In-Panel Controls',
|
|
309
|
+
' ' + '\u2500'.repeat(40),
|
|
310
|
+
row('j / k', 'Move selection down / up'),
|
|
311
|
+
row('g / G', 'Jump to top / bottom'),
|
|
312
|
+
row('/', 'Filter the list'),
|
|
239
313
|
'',
|
|
240
314
|
` Config: /keybindings to list and customize`,
|
|
241
315
|
];
|
|
@@ -261,7 +335,10 @@ export function renderShortcutsOverlay(
|
|
|
261
335
|
helpers: allRows.length > maxVisible
|
|
262
336
|
? [{ content: `[${window.start + 1}-${Math.min(allRows.length, clampedOffset + visibleRows.length)} of ${allRows.length}]` }]
|
|
263
337
|
: undefined,
|
|
264
|
-
hints: [
|
|
338
|
+
hints: [formatHints([
|
|
339
|
+
{ key: 'Up/Down', verb: 'Scroll' },
|
|
340
|
+
{ key: 'Esc', verb: 'Close' },
|
|
341
|
+
])],
|
|
265
342
|
},
|
|
266
343
|
width,
|
|
267
344
|
);
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* hint-grammar.ts — the single hint-bar grammar for every overlay footer.
|
|
3
|
+
*
|
|
4
|
+
* WO-151: one grammar across the model picker, settings modal, help/shortcuts
|
|
5
|
+
* overlays, and ModalFactory variants so footers stop drifting into three
|
|
6
|
+
* different dialects (verbless brackets, prose sentences, ad-hoc separators).
|
|
7
|
+
*
|
|
8
|
+
* The grammar is:
|
|
9
|
+
* - each hint renders as `[Key] Verb` (a bracketed key followed by its verb),
|
|
10
|
+
* - hints are joined by a middle-dot separator (` · `),
|
|
11
|
+
* - any Escape hint is always sorted last (the conventional "way out").
|
|
12
|
+
*
|
|
13
|
+
* State segments that are not key hints (e.g. `Filter: All`) can be appended
|
|
14
|
+
* verbatim via {@link joinHints}; they are never reordered.
|
|
15
|
+
*/
|
|
16
|
+
|
|
17
|
+
/** A single key hint. `verb` may be omitted for a bare key affordance. */
|
|
18
|
+
export interface HintSpec {
|
|
19
|
+
key: string;
|
|
20
|
+
verb?: string;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
/** The canonical separator between hint segments. */
|
|
24
|
+
export const HINT_SEPARATOR = ' · ';
|
|
25
|
+
|
|
26
|
+
function isEscapeHint(spec: HintSpec): boolean {
|
|
27
|
+
return spec.key.trim().toLowerCase() === 'esc';
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
/** Render one hint as `[Key] Verb` (or `[Key]` when it has no verb). */
|
|
31
|
+
export function formatHint(spec: HintSpec): string {
|
|
32
|
+
return spec.verb ? `[${spec.key}] ${spec.verb}` : `[${spec.key}]`;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
/**
|
|
36
|
+
* Format a list of key hints into a single footer string using the shared
|
|
37
|
+
* grammar. Escape hints are moved to the end (stable for everything else).
|
|
38
|
+
*/
|
|
39
|
+
export function formatHints(specs: readonly HintSpec[]): string {
|
|
40
|
+
// Stable partition: non-escape hints keep their order, escape hints trail.
|
|
41
|
+
const ordered = [...specs].sort((a, b) => Number(isEscapeHint(a)) - Number(isEscapeHint(b)));
|
|
42
|
+
return ordered.map(formatHint).join(HINT_SEPARATOR);
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
/**
|
|
46
|
+
* Join already-rendered segments (hint bars, state tags) with the shared
|
|
47
|
+
* separator, dropping empties. Order is preserved — use this to append state
|
|
48
|
+
* indicators after a {@link formatHints} bar.
|
|
49
|
+
*/
|
|
50
|
+
export function joinHints(...segments: Array<string | null | undefined>): string {
|
|
51
|
+
return segments.filter((s): s is string => !!s && s.length > 0).join(HINT_SEPARATOR);
|
|
52
|
+
}
|
package/src/renderer/layout.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { GLYPHS } from './ui-primitives.ts';
|
|
1
|
+
import { GLYPHS, SPINNER_FRAMES, UI_TONES } from './ui-primitives.ts';
|
|
2
2
|
|
|
3
3
|
/**
|
|
4
4
|
* Layout constants — single source of truth for margins and content width.
|
|
@@ -14,19 +14,19 @@ export const LAYOUT = {
|
|
|
14
14
|
|
|
15
15
|
export const TOOL_STATUS = {
|
|
16
16
|
SUCCESS_ICON: GLYPHS.status.success,
|
|
17
|
-
SPINNER_FRAMES
|
|
17
|
+
SPINNER_FRAMES,
|
|
18
18
|
FAIL_ICON: GLYPHS.status.failure,
|
|
19
19
|
PENDING_ICON: GLYPHS.status.pending,
|
|
20
20
|
TOOL_NAME_PAD: 8,
|
|
21
21
|
} as const;
|
|
22
22
|
|
|
23
23
|
export const COLORS = {
|
|
24
|
-
DIM_TEXT:
|
|
24
|
+
DIM_TEXT: UI_TONES.fg.dim,
|
|
25
25
|
} as const;
|
|
26
26
|
|
|
27
27
|
export const BORDERS = {
|
|
28
|
-
THINKING: { char: '▌', color:
|
|
29
|
-
ERROR: { char: '▌', color:
|
|
30
|
-
WARNING: { char: '▌', color:
|
|
31
|
-
INFO: { char: '▌', color:
|
|
28
|
+
THINKING: { char: '▌', color: UI_TONES.state.reasoning },
|
|
29
|
+
ERROR: { char: '▌', color: UI_TONES.state.bad },
|
|
30
|
+
WARNING: { char: '▌', color: UI_TONES.state.warn },
|
|
31
|
+
INFO: { char: '▌', color: UI_TONES.state.info },
|
|
32
32
|
} as const;
|
|
@@ -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
|
|
|
@@ -13,6 +13,7 @@ import {
|
|
|
13
13
|
putOverlayText,
|
|
14
14
|
} from './overlay-box.ts';
|
|
15
15
|
import { getOverlaySurfaceMetrics } from './overlay-viewport.ts';
|
|
16
|
+
import { formatHints, type HintSpec } from './hint-grammar.ts';
|
|
16
17
|
|
|
17
18
|
/** Format a context window number into a short human-readable string. */
|
|
18
19
|
function fmtContext(n: number): string {
|
|
@@ -413,13 +414,26 @@ export function renderModelPickerOverlay(
|
|
|
413
414
|
const groupByLabel = picker.groupBy ?? 'provider';
|
|
414
415
|
const selectedModel = picker.mode === 'model' ? picker.getSelected() : null;
|
|
415
416
|
const showContextCapHint = selectedModel != null && selectedModel.contextWindowProvenance !== undefined;
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
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
|
+
}
|
|
423
437
|
const footerLine = createOverlayFilledBorderLine(width, layout, OVERLAY_GLYPHS.bottomLeft, OVERLAY_GLYPHS.horizontal, OVERLAY_GLYPHS.bottomRight, borderFg, DEFAULT_OVERLAY_PALETTE.sectionBg);
|
|
424
438
|
putRowText(footerLine, layout.margin + 2, contentW, fitDisplay(truncateDisplay(hints, contentW), contentW), mutedFg, '', false, true);
|
|
425
439
|
lines.push(footerLine);
|
|
@@ -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 {
|
|
@@ -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
|
@@ -2,11 +2,12 @@ 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
4
|
import { abbreviateCount } from '../utils/format-number.ts';
|
|
5
|
+
import { SPINNER_FRAMES } from './ui-primitives.ts';
|
|
5
6
|
|
|
6
|
-
// Rich spinner frames (used by progress indicators)
|
|
7
|
-
export
|
|
8
|
-
// Braille thinking spinner frames (used by the orchestrator thinking animation)
|
|
9
|
-
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;
|
|
10
11
|
|
|
11
12
|
/**
|
|
12
13
|
* renderSpinner - Render a spinner with label as a single Line.
|