@pellux/goodvibes-tui 1.7.0 → 1.9.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/README.md +10 -9
- package/docs/foundation-artifacts/operator-contract.json +4045 -1763
- package/package.json +2 -2
- package/src/audio/spoken-turn-controller.ts +12 -2
- package/src/audio/spoken-turn-wiring.ts +2 -1
- package/src/cli/help.ts +8 -1
- package/src/cli/service-posture.ts +2 -1
- package/src/cli/status.ts +2 -1
- package/src/cli/surface-command.ts +2 -2
- package/src/core/composer-state.ts +11 -3
- package/src/core/conversation-line-cache.ts +27 -3
- package/src/core/conversation-rendering.ts +71 -14
- package/src/core/stream-event-wiring.ts +20 -1
- package/src/core/system-message-noise.ts +87 -0
- package/src/core/system-message-router.ts +68 -1
- package/src/core/turn-cancellation.ts +7 -2
- package/src/core/turn-event-wiring.ts +10 -2
- package/src/daemon/cli.ts +29 -2
- package/src/daemon/handlers/register.ts +8 -1
- package/src/daemon/service-commands.ts +329 -0
- package/src/input/autocomplete.ts +27 -1
- package/src/input/command-registry.ts +46 -4
- package/src/input/commands/codebase-runtime.ts +46 -6
- package/src/input/commands/config.ts +43 -3
- package/src/input/commands/health-runtime.ts +9 -1
- package/src/input/commands/memory.ts +68 -35
- package/src/input/commands/operator-panel-runtime.ts +31 -7
- package/src/input/commands/planning-runtime.ts +95 -6
- package/src/input/commands/qrcode-runtime.ts +25 -5
- package/src/input/commands/remote-runtime-setup.ts +5 -3
- package/src/input/commands/session-content.ts +20 -9
- package/src/input/commands/settings-sync-runtime.ts +15 -3
- package/src/input/commands/shell-core.ts +10 -1
- package/src/input/commands/workstream-runtime.ts +168 -18
- package/src/input/config-modal-types.ts +15 -1
- package/src/input/config-modal.ts +227 -12
- package/src/input/feed-context-factory.ts +3 -0
- package/src/input/handler-command-route.ts +10 -3
- package/src/input/handler-content-actions.ts +17 -2
- package/src/input/handler-feed-routes.ts +43 -121
- package/src/input/handler-feed.ts +32 -4
- package/src/input/handler-modal-routes.ts +78 -13
- package/src/input/handler-modal-stack.ts +11 -2
- package/src/input/handler-onboarding-daemon-adopt.ts +149 -0
- package/src/input/handler-onboarding.ts +71 -59
- package/src/input/handler-picker-routes.ts +15 -8
- package/src/input/handler-shortcuts.ts +59 -9
- package/src/input/handler.ts +6 -1
- package/src/input/keybindings.ts +6 -5
- package/src/input/model-picker.ts +19 -2
- package/src/input/onboarding/onboarding-runtime-status.ts +10 -1
- package/src/input/onboarding/onboarding-wizard-apply.ts +8 -1
- package/src/input/onboarding/onboarding-wizard-constants.ts +4 -1
- package/src/input/onboarding/onboarding-wizard-network-adopt.ts +136 -0
- package/src/input/onboarding/onboarding-wizard-steps.ts +9 -6
- package/src/input/onboarding/onboarding-wizard-types.ts +3 -1
- package/src/input/panel-mouse-geometry.ts +97 -0
- package/src/input/panel-paste-flood-guard.ts +86 -0
- package/src/input/selection-modal.ts +11 -0
- package/src/input/session-picker-modal.ts +44 -4
- package/src/input/settings-modal-data.ts +51 -2
- package/src/input/settings-modal-types.ts +4 -2
- package/src/main.ts +28 -27
- package/src/panels/builtin/shared.ts +9 -3
- package/src/panels/fleet-deep-link.ts +31 -0
- package/src/panels/fleet-panel-format.ts +62 -0
- package/src/panels/fleet-panel-worktree-detail.ts +48 -0
- package/src/panels/fleet-panel.ts +89 -131
- package/src/panels/fleet-read-model.ts +43 -0
- package/src/panels/fleet-steer.ts +35 -2
- package/src/panels/fleet-stop.ts +29 -1
- package/src/panels/modals/keybindings-modal.ts +16 -1
- package/src/panels/modals/modal-theme.ts +35 -29
- package/src/panels/modals/pairing-modal.ts +25 -6
- package/src/panels/modals/planning-modal.ts +43 -21
- package/src/panels/modals/work-plan-modal.ts +28 -0
- package/src/panels/panel-manager.ts +15 -4
- package/src/panels/polish-core.ts +38 -25
- package/src/panels/project-planning-answer-actions.ts +8 -13
- package/src/panels/types.ts +24 -0
- package/src/permissions/prompt.ts +160 -13
- package/src/renderer/autocomplete-overlay.ts +28 -2
- package/src/renderer/compositor.ts +2 -3
- package/src/renderer/config-modal.ts +19 -5
- package/src/renderer/footer-tips.ts +5 -1
- package/src/renderer/fullscreen-primitives.ts +32 -22
- package/src/renderer/git-status.ts +3 -1
- package/src/renderer/layout.ts +0 -4
- package/src/renderer/markdown.ts +7 -3
- package/src/renderer/modal-factory.ts +25 -20
- package/src/renderer/model-workspace.ts +18 -3
- package/src/renderer/overlay-box.ts +21 -17
- package/src/renderer/process-indicator.ts +14 -3
- package/src/renderer/selection-modal-overlay.ts +6 -1
- package/src/renderer/session-picker-modal.ts +196 -3
- package/src/renderer/settings-modal-helpers.ts +2 -0
- package/src/renderer/settings-modal.ts +7 -0
- package/src/renderer/shell-surface.ts +8 -1
- package/src/renderer/status-glyphs.ts +14 -15
- package/src/renderer/system-message.ts +15 -3
- package/src/renderer/terminal-bg-probe.ts +339 -0
- package/src/renderer/terminal-escapes.ts +20 -0
- package/src/renderer/theme-mode-config.ts +67 -0
- package/src/renderer/theme.ts +91 -1
- package/src/renderer/thinking.ts +11 -3
- package/src/renderer/tool-call.ts +15 -9
- package/src/renderer/tool-result-summary.ts +148 -0
- package/src/renderer/turn-injection.ts +22 -3
- package/src/renderer/ui-factory.ts +154 -85
- package/src/renderer/ui-primitives.ts +30 -129
- package/src/runtime/bootstrap-command-context.ts +6 -0
- package/src/runtime/bootstrap-command-parts.ts +8 -4
- package/src/runtime/bootstrap-core.ts +33 -11
- package/src/runtime/bootstrap-hook-bridge.ts +7 -0
- package/src/runtime/bootstrap-shell.ts +19 -1
- package/src/runtime/bootstrap.ts +118 -5
- package/src/runtime/code-index-services.ts +25 -2
- package/src/runtime/legacy-daemon-migration.ts +516 -0
- package/src/runtime/memory-fold.ts +26 -0
- package/src/runtime/onboarding/derivation.ts +7 -2
- package/src/runtime/onboarding/snapshot.ts +27 -1
- package/src/runtime/onboarding/types.ts +31 -1
- package/src/runtime/operator-token-cleanup.ts +82 -1
- package/src/runtime/orchestrator-core-services.ts +10 -0
- package/src/runtime/resume-notice.ts +209 -0
- package/src/runtime/services.ts +12 -8
- package/src/runtime/session-inbound-inputs.ts +252 -0
- package/src/runtime/session-spine-transport.ts +64 -0
- package/src/runtime/terminal-output-guard.ts +15 -8
- package/src/runtime/ui-services.ts +19 -3
- package/src/runtime/workstream-services.ts +160 -28
- package/src/runtime/wrfc-persistence.ts +124 -17
- package/src/shell/blocking-input.ts +46 -3
- package/src/shell/recovery-input-helpers.ts +170 -1
- package/src/shell/ui-openers.ts +42 -9
- package/src/utils/terminal-width.ts +52 -0
- package/src/version.ts +1 -1
|
@@ -81,12 +81,35 @@ export function renderAutocompleteOverlay(
|
|
|
81
81
|
}
|
|
82
82
|
const endIdx = Math.min(startIdx + maxVisible, total);
|
|
83
83
|
|
|
84
|
+
// UX-C palette curation (item 4): on a bare '/' (query === ''), the results
|
|
85
|
+
// list is "common tier" (score 2) followed by "alphabetical rest" (score
|
|
86
|
+
// 1) — see CommandRegistry.fuzzyMatch. commonCount marks that boundary;
|
|
87
|
+
// draw a one-row separator there when it falls inside the visible window,
|
|
88
|
+
// consuming one of the maxVisible slots so the box height never changes.
|
|
89
|
+
const hasCommonSeparator = state.query === '' && state.commonCount > 0 && state.commonCount < total;
|
|
90
|
+
type DisplayRow = { type: 'item'; index: number } | { type: 'separator' };
|
|
91
|
+
const displayRows: DisplayRow[] = [];
|
|
92
|
+
for (let i = startIdx; i < endIdx && displayRows.length < maxVisible; i++) {
|
|
93
|
+
if (hasCommonSeparator && i === state.commonCount) {
|
|
94
|
+
displayRows.push({ type: 'separator' });
|
|
95
|
+
if (displayRows.length >= maxVisible) break;
|
|
96
|
+
}
|
|
97
|
+
displayRows.push({ type: 'item', index: i });
|
|
98
|
+
}
|
|
99
|
+
|
|
84
100
|
const indicatorWidth = 2;
|
|
85
101
|
const maxCommandWidth = Math.min(18, Math.max(10, Math.floor(layout.innerWidth * 0.28)));
|
|
86
102
|
const gapWidth = 2;
|
|
87
103
|
const descWidth = Math.max(0, layout.innerWidth - indicatorWidth - maxCommandWidth - gapWidth);
|
|
88
104
|
|
|
89
|
-
for (
|
|
105
|
+
for (const row of displayRows) {
|
|
106
|
+
if (row.type === 'separator') {
|
|
107
|
+
const sepLine = createOverlayContentLine(width, layout);
|
|
108
|
+
putText(sepLine, layout.margin + 2, layout.innerWidth, '─'.repeat(layout.innerWidth), { fg: BORDER_FG, dim: true });
|
|
109
|
+
lines.push(sepLine);
|
|
110
|
+
continue;
|
|
111
|
+
}
|
|
112
|
+
const i = row.index;
|
|
90
113
|
const { command } = results[i];
|
|
91
114
|
const isSelected = i === state.selectedIndex;
|
|
92
115
|
const line = createOverlayContentLine(width, layout, BORDER_FG, isSelected ? SELECTED_BG : '');
|
|
@@ -139,7 +162,10 @@ export function renderAutocompleteOverlay(
|
|
|
139
162
|
}
|
|
140
163
|
|
|
141
164
|
const footerLine = createOverlayContentLine(width, layout);
|
|
142
|
-
|
|
165
|
+
// UX-C vocab unification: "Run"/"Close" match the /help selection modal's
|
|
166
|
+
// hint bar (selection-modal-overlay.ts) — both surfaces execute a command
|
|
167
|
+
// on Enter, so they now say so the same way.
|
|
168
|
+
const hints = '[Tab] Complete [Up/Down] Navigate [Enter] Run [Esc] Close';
|
|
143
169
|
putText(
|
|
144
170
|
footerLine,
|
|
145
171
|
layout.margin + 2,
|
|
@@ -5,11 +5,9 @@ import { getDisplayWidth } from '../utils/terminal-width.ts';
|
|
|
5
5
|
import type { SearchManager } from '../input/search.ts';
|
|
6
6
|
import { allowTerminalWrite } from '../runtime/terminal-output-guard.ts';
|
|
7
7
|
import { probeTermCaps, type TermColorCaps } from './term-caps.ts';
|
|
8
|
-
import {
|
|
8
|
+
import { activeTheme } from './theme.ts';
|
|
9
9
|
import { UI_TONES } from './ui-primitives.ts';
|
|
10
10
|
|
|
11
|
-
const T = DARK_THEME;
|
|
12
|
-
|
|
13
11
|
// Accent / dim colors for the panel focus border. The focused pane's left
|
|
14
12
|
// border column is drawn in the accent tone; the unfocused pane stays dim.
|
|
15
13
|
const PANEL_FOCUS_ACCENT = UI_TONES.state.active; // bright blue
|
|
@@ -252,6 +250,7 @@ export class Compositor {
|
|
|
252
250
|
|
|
253
251
|
// Apply Search Match Highlighting Overlay (left side only)
|
|
254
252
|
if (search && search.manager.active && search.manager.query.length > 0 && i >= offset) {
|
|
253
|
+
const T = activeTheme();
|
|
255
254
|
const absoluteRow = search.scrollTop + (i - offset);
|
|
256
255
|
const lineMatches = search.manager.getMatchesOnLine(absoluteRow);
|
|
257
256
|
for (const match of lineMatches) {
|
|
@@ -1,10 +1,15 @@
|
|
|
1
1
|
import type { Line } from '../types/grid.ts';
|
|
2
2
|
import { ModalFactory, type ModalSection, type ModalTab } from './modal-factory.ts';
|
|
3
|
-
import {
|
|
3
|
+
import { getOverlayMaxWidth } from './overlay-viewport.ts';
|
|
4
|
+
import { activeUiTones } from './theme.ts';
|
|
4
5
|
import type { ConfigModal, ConfigModalRenderModel } from '../input/config-modal.ts';
|
|
5
6
|
|
|
6
|
-
|
|
7
|
-
|
|
7
|
+
// Matches ModalFactory.createModal's own box-width computation (margin 4,
|
|
8
|
+
// requested max width 76 — see renderConfigModalModel below) so the
|
|
9
|
+
// wrap-clamp in ConfigModal.getRenderModel() measures against the SAME width
|
|
10
|
+
// the list section will actually wrap at, at the current terminal size.
|
|
11
|
+
const MODAL_MARGIN = 4;
|
|
12
|
+
const MODAL_MAX_WIDTH = 76;
|
|
8
13
|
/**
|
|
9
14
|
* renderConfigModal — the single render path for every ConfigModalSurface.
|
|
10
15
|
* Reads the host's frozen-structure-plus-live-values render model and maps it
|
|
@@ -17,12 +22,21 @@ export function renderConfigModal(modal: ConfigModal, width: number, height: num
|
|
|
17
22
|
// rather than overflowing (posture header + separator + chrome ≈ 8 rows).
|
|
18
23
|
const visible = Math.max(3, Math.min(16, height - 8));
|
|
19
24
|
modal.setViewportRows(visible);
|
|
20
|
-
|
|
25
|
+
// DEBT-5 item 2: the list section wraps a row label at (contentW - 2),
|
|
26
|
+
// where contentW = boxW - 4 (see ModalFactory._renderListSection). Compute
|
|
27
|
+
// that exact column here so getRenderModel's wrap-clamp measures against
|
|
28
|
+
// reality instead of a fixed guess.
|
|
29
|
+
const boxW = Math.max(24, getOverlayMaxWidth(width, MODAL_MARGIN, MODAL_MAX_WIDTH));
|
|
30
|
+
const labelWrapWidth = Math.max(8, boxW - 4 - 2);
|
|
31
|
+
const model = modal.getRenderModel(labelWrapWidth);
|
|
21
32
|
return renderConfigModalModel(model, width);
|
|
22
33
|
}
|
|
23
34
|
|
|
24
35
|
/** Pure model→Line[] mapping — the shared inner step of renderConfigModal. */
|
|
25
36
|
function renderConfigModalModel(model: ConfigModalRenderModel, width: number): Line[] {
|
|
37
|
+
// Read the active-mode chrome tones per call (this render path is not on a
|
|
38
|
+
// hot loop; the modal renders once per keystroke).
|
|
39
|
+
const TONES = activeUiTones();
|
|
26
40
|
const sections: ModalSection[] = [];
|
|
27
41
|
|
|
28
42
|
if (model.degraded) {
|
|
@@ -69,7 +83,7 @@ function renderConfigModalModel(model: ConfigModalRenderModel, width: number): L
|
|
|
69
83
|
return ModalFactory.createModal(
|
|
70
84
|
{
|
|
71
85
|
title: model.title,
|
|
72
|
-
width:
|
|
86
|
+
width: MODAL_MAX_WIDTH,
|
|
73
87
|
tabs,
|
|
74
88
|
sections,
|
|
75
89
|
targetContentRows,
|
|
@@ -24,7 +24,11 @@ export function isAgentActive(composerStatus: string | undefined): boolean {
|
|
|
24
24
|
return composerStatus !== undefined && ACTIVE_TURN_STATUSES.has(composerStatus);
|
|
25
25
|
}
|
|
26
26
|
|
|
27
|
-
|
|
27
|
+
// UX-C: Ctrl+P is a TOGGLE (open+focus when nothing is open or unfocused,
|
|
28
|
+
// hide when the workspace already has focus — see openPanelPicker in
|
|
29
|
+
// shell/ui-openers.ts). The bare noun 'panels' undersold that; naming the verb
|
|
30
|
+
// keeps the tip honest about what the chord actually does.
|
|
31
|
+
const TIP_PANELS = 'Ctrl+P toggle panels';
|
|
28
32
|
// W6.2 e: F2 now opens the Fleet panel (the process modal was retired), so the
|
|
29
33
|
// tip names 'fleet', not 'processes'.
|
|
30
34
|
const TIP_PROCESSES = 'F2 fleet';
|
|
@@ -2,30 +2,40 @@ 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
4
|
import { GLYPHS } from './ui-primitives.ts';
|
|
5
|
-
import {
|
|
5
|
+
import { activeUiTones, registerThemeRefresh } from './theme.ts';
|
|
6
6
|
|
|
7
|
-
//
|
|
8
|
-
//
|
|
9
|
-
//
|
|
10
|
-
|
|
7
|
+
// Built from the mode-resolved chrome tones (activeUiTones) and rebuilt IN PLACE
|
|
8
|
+
// on a mode flip via the registered refresher (read by reference across the
|
|
9
|
+
// fullscreen workspace surfaces — see theme.ts's active-mode runtime note).
|
|
10
|
+
function buildFullscreenPalette(): Record<string, string> {
|
|
11
|
+
const t = activeUiTones();
|
|
12
|
+
return {
|
|
13
|
+
border: t.border,
|
|
14
|
+
title: t.accent.browser,
|
|
15
|
+
subtitle: t.accent.conversation,
|
|
16
|
+
text: t.fg.primary,
|
|
17
|
+
muted: t.fg.muted,
|
|
18
|
+
dim: t.border,
|
|
19
|
+
selectedBg: t.bg.selected,
|
|
20
|
+
categoryBg: t.bg.section,
|
|
21
|
+
contextBg: t.bg.surface,
|
|
22
|
+
controlsBg: t.bg.base,
|
|
23
|
+
footerBg: t.bg.footer,
|
|
24
|
+
good: t.state.good,
|
|
25
|
+
warn: t.state.warn,
|
|
26
|
+
bad: t.state.bad,
|
|
27
|
+
info: t.state.info,
|
|
28
|
+
};
|
|
29
|
+
}
|
|
11
30
|
|
|
12
|
-
export const FULLSCREEN_PALETTE = {
|
|
13
|
-
border:
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
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,
|
|
28
|
-
} as const;
|
|
31
|
+
export const FULLSCREEN_PALETTE = buildFullscreenPalette() as {
|
|
32
|
+
readonly border: string; readonly title: string; readonly subtitle: string;
|
|
33
|
+
readonly text: string; readonly muted: string; readonly dim: string;
|
|
34
|
+
readonly selectedBg: string; readonly categoryBg: string; readonly contextBg: string;
|
|
35
|
+
readonly controlsBg: string; readonly footerBg: string; readonly good: string;
|
|
36
|
+
readonly warn: string; readonly bad: string; readonly info: string;
|
|
37
|
+
};
|
|
38
|
+
registerThemeRefresh(() => Object.assign(FULLSCREEN_PALETTE as Record<string, string>, buildFullscreenPalette()));
|
|
29
39
|
|
|
30
40
|
export type FullscreenTextStyle = Partial<Omit<Line[number], 'char'>>;
|
|
31
41
|
|
|
@@ -110,7 +110,9 @@ export class GitStatusProvider {
|
|
|
110
110
|
statusResult.conflicted.length > 0 ||
|
|
111
111
|
statusResult.not_added.length > 0;
|
|
112
112
|
this.cache = {
|
|
113
|
-
|
|
113
|
+
// Empty `current` means an unborn HEAD — a repo with no commits yet.
|
|
114
|
+
// Show "new" instead of an unfriendly "?" for that first-run case. (5a)
|
|
115
|
+
branch: branchResult.current || 'new',
|
|
114
116
|
dirty,
|
|
115
117
|
ahead: statusResult.ahead ?? 0,
|
|
116
118
|
behind: statusResult.behind ?? 0,
|
package/src/renderer/layout.ts
CHANGED
|
@@ -20,10 +20,6 @@ export const TOOL_STATUS = {
|
|
|
20
20
|
TOOL_NAME_PAD: 8,
|
|
21
21
|
} as const;
|
|
22
22
|
|
|
23
|
-
export const COLORS = {
|
|
24
|
-
DIM_TEXT: UI_TONES.fg.dim,
|
|
25
|
-
} as const;
|
|
26
|
-
|
|
27
23
|
export const BORDERS = {
|
|
28
24
|
THINKING: { char: '▌', color: UI_TONES.state.reasoning },
|
|
29
25
|
ERROR: { char: '▌', color: UI_TONES.state.bad },
|
package/src/renderer/markdown.ts
CHANGED
|
@@ -3,10 +3,11 @@ import { UIFactory } from './ui-factory.ts';
|
|
|
3
3
|
import { renderCodeBlock } from './code-block.ts';
|
|
4
4
|
import { getDisplayWidth } from '../utils/terminal-width.ts';
|
|
5
5
|
import { LAYOUT } from './layout.ts';
|
|
6
|
-
import {
|
|
6
|
+
import { activeTheme } from './theme.ts';
|
|
7
7
|
|
|
8
|
-
|
|
9
|
-
|
|
8
|
+
// Transcript tokens are read live per render (const T = activeTheme() at the top
|
|
9
|
+
// of each render function below) so a dark→light repaint re-resolves with no
|
|
10
|
+
// module reload. See theme.ts's active-mode runtime note.
|
|
10
11
|
|
|
11
12
|
export interface MarkdownRenderOptions {
|
|
12
13
|
codeBlockLineNumbers?: boolean;
|
|
@@ -62,6 +63,7 @@ export function renderMarkdownTracked(
|
|
|
62
63
|
width: number,
|
|
63
64
|
options: MarkdownRenderOptions = {},
|
|
64
65
|
): { lines: ReturnType<typeof renderMarkdown>; codeBlocks: CodeBlockSpan[] } {
|
|
66
|
+
const T = activeTheme();
|
|
65
67
|
const lines: ReturnType<typeof renderMarkdown> = [];
|
|
66
68
|
const codeBlocks: CodeBlockSpan[] = [];
|
|
67
69
|
const rawLines = text.split('\n');
|
|
@@ -236,6 +238,7 @@ function stripMarkdown(text: string): string {
|
|
|
236
238
|
* truncates long content, and renders with proper styling.
|
|
237
239
|
*/
|
|
238
240
|
function renderTable(rows: string[], width: number, indent: number): Line[] {
|
|
241
|
+
const T = activeTheme();
|
|
239
242
|
const lines: Line[] = [];
|
|
240
243
|
|
|
241
244
|
// Parse rows into cells, skip separator
|
|
@@ -542,6 +545,7 @@ function compositeInlineLine(
|
|
|
542
545
|
prefixStyle: Partial<Cell>,
|
|
543
546
|
textStartX: number
|
|
544
547
|
): Line[] {
|
|
548
|
+
const T = activeTheme();
|
|
545
549
|
const lines: Line[] = [];
|
|
546
550
|
|
|
547
551
|
// Flatten tokens to [char, style] pairs
|
|
@@ -9,7 +9,7 @@ import {
|
|
|
9
9
|
} from './overlay-box.ts';
|
|
10
10
|
import { getOverlayMaxWidth } from './overlay-viewport.ts';
|
|
11
11
|
import { GLYPHS } from './ui-primitives.ts';
|
|
12
|
-
import {
|
|
12
|
+
import { activeUiTones, registerThemeRefresh } from './theme.ts';
|
|
13
13
|
import { HINT_SEPARATOR } from './hint-grammar.ts';
|
|
14
14
|
|
|
15
15
|
// ── Helpers ──────────────────────────────────────────────────────────────────
|
|
@@ -92,25 +92,30 @@ export interface ModalConfig {
|
|
|
92
92
|
|
|
93
93
|
// ── Defaults ─────────────────────────────────────────────────────────────────
|
|
94
94
|
|
|
95
|
-
// DEFAULT_STYLE is built from the mode-resolved chrome tones (
|
|
96
|
-
//
|
|
97
|
-
//
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
const
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
95
|
+
// DEFAULT_STYLE is built from the mode-resolved chrome tones (activeUiTones) and
|
|
96
|
+
// rebuilt IN PLACE on a mode flip via the registered refresher. createModal reads
|
|
97
|
+
// it per call via `{ ...DEFAULT_STYLE, ...config.style }`, so the in-place rebuild
|
|
98
|
+
// reaches every modal — see theme.ts's active-mode runtime note.
|
|
99
|
+
function buildDefaultModalStyle(): Required<ModalStyle> {
|
|
100
|
+
const t = activeUiTones();
|
|
101
|
+
return {
|
|
102
|
+
titleFg: t.fg.primary,
|
|
103
|
+
borderFg: t.fg.dim,
|
|
104
|
+
hintFg: t.fg.muted,
|
|
105
|
+
selectedFg: t.fg.primary,
|
|
106
|
+
selectedBg: t.bg.selected,
|
|
107
|
+
textFg: t.fg.primary,
|
|
108
|
+
accentFg: t.state.info,
|
|
109
|
+
titleRowFg: t.fg.secondary,
|
|
110
|
+
titleBg: t.bg.title,
|
|
111
|
+
sectionBg: t.bg.section,
|
|
112
|
+
inputBg: t.bg.input,
|
|
113
|
+
surfaceBg: t.bg.surface,
|
|
114
|
+
};
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
const DEFAULT_STYLE: Required<ModalStyle> = buildDefaultModalStyle();
|
|
118
|
+
registerThemeRefresh(() => Object.assign(DEFAULT_STYLE, buildDefaultModalStyle()));
|
|
114
119
|
|
|
115
120
|
// ── ModalFactory ─────────────────────────────────────────────────────────────
|
|
116
121
|
|
|
@@ -189,7 +189,15 @@ function detailLines(picker: ModelPickerModal, width: number): string[] {
|
|
|
189
189
|
} else {
|
|
190
190
|
lines.push(`Context cap overrides the detected local-model context window for this selection.`);
|
|
191
191
|
}
|
|
192
|
-
|
|
192
|
+
// W3-T2: a visible cursor glyph on the query itself when search is focused —
|
|
193
|
+
// the only prior affordance was a footer-hint text swap ('/ search' vs.
|
|
194
|
+
// 'Typing filters search...'), buried at the end of a long hint line and
|
|
195
|
+
// easy to miss (confirmed via live tmux repro). This puts the "you are
|
|
196
|
+
// typing here" signal where the eye already is, on the Search: value.
|
|
197
|
+
const searchDisplay = picker.query.length > 0
|
|
198
|
+
? `${picker.query}${picker.searchFocused ? GLYPHS.surface.cursor : ''}`
|
|
199
|
+
: (picker.searchFocused ? GLYPHS.surface.cursor : '(none)');
|
|
200
|
+
const filterText = `Search: ${searchDisplay} | Price: ${picker.categoryFilter} | Capability: ${picker.capabilityFilter} | Group: ${picker.groupBy} | Available only: ${picker.availableOnly ? 'yes' : 'no'}`;
|
|
193
201
|
lines.push(filterText);
|
|
194
202
|
return lines.flatMap((line) => wrapText(line, Math.max(1, width)));
|
|
195
203
|
}
|
|
@@ -426,8 +434,15 @@ export function renderModelWorkspace(picker: ModelPickerModal, width: number, vi
|
|
|
426
434
|
|
|
427
435
|
const footer = contentLine(safeWidth, PALETTE.footerBg);
|
|
428
436
|
const targetHint = picker.focusPane === 'targets' ? 'Focus targets' : 'Focus list';
|
|
429
|
-
|
|
430
|
-
|
|
437
|
+
// W3 Finding 2: while search is focused, every printable key (including
|
|
438
|
+
// c/a/b/g) types into the query — handler-picker-routes.ts guards those
|
|
439
|
+
// single-key shortcuts with `!searchFocused` (Up/Down blur search first).
|
|
440
|
+
// Advertising them here regardless of focus state was a footer that lied
|
|
441
|
+
// about what the keys currently do. Tab still cycles price in both states
|
|
442
|
+
// (its handler has no searchFocused guard), so it stays in both hint sets.
|
|
443
|
+
const hints = picker.searchFocused
|
|
444
|
+
? `${targetHint} • Typing filters search; Esc clears search • Down: list shortcuts • Tab price • Esc close`
|
|
445
|
+
: `${targetHint} • Up/Down navigate • Left/Right pane • Enter select • / search • Tab price • C caps • A available • B benchmark • G group • Esc close`;
|
|
431
446
|
writeText(footer, 2, safeWidth - 4, hints, { fg: PALETTE.muted, bg: PALETTE.footerBg });
|
|
432
447
|
lines.push(footer);
|
|
433
448
|
lines.push(borderLine(safeWidth, GLYPHS.frame.bottomLeft, GLYPHS.frame.horizontal, GLYPHS.frame.bottomRight));
|
|
@@ -1,14 +1,9 @@
|
|
|
1
1
|
import { createStyledCell, type Line } from '../types/grid.ts';
|
|
2
2
|
import { getOverlayMaxWidth } from './overlay-viewport.ts';
|
|
3
3
|
import { GLYPHS } from './ui-primitives.ts';
|
|
4
|
-
import {
|
|
4
|
+
import { activeUiTones, registerThemeRefresh } from './theme.ts';
|
|
5
5
|
import { fillWidth, makeLine, writeText } from './fullscreen-primitives.ts';
|
|
6
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
|
-
|
|
12
7
|
export interface OverlayBoxPalette {
|
|
13
8
|
readonly borderFg: string;
|
|
14
9
|
readonly titleFg: string;
|
|
@@ -21,17 +16,26 @@ export interface OverlayBoxPalette {
|
|
|
21
16
|
readonly bodyBg: string;
|
|
22
17
|
}
|
|
23
18
|
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
19
|
+
// Built from the mode-resolved chrome tones (activeUiTones) and rebuilt IN PLACE
|
|
20
|
+
// on a mode flip via the registered refresher (60+ call sites read it by
|
|
21
|
+
// reference — see theme.ts's active-mode runtime note).
|
|
22
|
+
function buildOverlayPalette(): OverlayBoxPalette {
|
|
23
|
+
const t = activeUiTones();
|
|
24
|
+
return {
|
|
25
|
+
borderFg: t.fg.secondary,
|
|
26
|
+
titleFg: t.fg.primary,
|
|
27
|
+
bodyFg: t.fg.primary,
|
|
28
|
+
mutedFg: t.fg.dim,
|
|
29
|
+
selectedBg: t.bg.selected,
|
|
30
|
+
titleBg: t.bg.title,
|
|
31
|
+
sectionBg: t.bg.section,
|
|
32
|
+
inputBg: t.bg.input,
|
|
33
|
+
bodyBg: t.bg.surface,
|
|
34
|
+
};
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
export const DEFAULT_OVERLAY_PALETTE: Readonly<OverlayBoxPalette> = buildOverlayPalette();
|
|
38
|
+
registerThemeRefresh(() => Object.assign(DEFAULT_OVERLAY_PALETTE as OverlayBoxPalette, buildOverlayPalette()));
|
|
35
39
|
|
|
36
40
|
export interface OverlayBoxLayout {
|
|
37
41
|
readonly margin: number;
|
|
@@ -2,6 +2,7 @@ import { type Line } from '../types/grid.ts';
|
|
|
2
2
|
import { UIFactory } from './ui-factory.ts';
|
|
3
3
|
import { truncateDisplay } from '../utils/terminal-width.ts';
|
|
4
4
|
import { GLYPHS, UI_TONES } from './ui-primitives.ts';
|
|
5
|
+
import { activeUiTones } from './theme.ts';
|
|
5
6
|
import { formatHints } from './hint-grammar.ts';
|
|
6
7
|
|
|
7
8
|
/**
|
|
@@ -12,6 +13,16 @@ import { formatHints } from './hint-grammar.ts';
|
|
|
12
13
|
* background exec processes are running. Includes an `[Enter] View` hint
|
|
13
14
|
* (hint-grammar bracket form) when active.
|
|
14
15
|
*/
|
|
16
|
+
/**
|
|
17
|
+
* Agent-count label. The footer counts only ACTIVE agents, while the fleet
|
|
18
|
+
* lists every node (running, terminal, chains, watchers). Label the count
|
|
19
|
+
* "active" so it is never misread as a grand total — the [Enter] View hint
|
|
20
|
+
* opens the fleet for the full picture. (UX-B item 5d.)
|
|
21
|
+
*/
|
|
22
|
+
function agentCountLabel(agentCount: number): string {
|
|
23
|
+
return `${agentCount} agent${agentCount !== 1 ? 's' : ''} active`;
|
|
24
|
+
}
|
|
25
|
+
|
|
15
26
|
export function renderProcessIndicator(
|
|
16
27
|
width: number,
|
|
17
28
|
agentCount: number,
|
|
@@ -47,7 +58,7 @@ export function renderProcessIndicator(
|
|
|
47
58
|
// --- Focused state: always render before idle/active branches ---
|
|
48
59
|
if (focused) {
|
|
49
60
|
const parts: string[] = [];
|
|
50
|
-
if (agentCount > 0) parts.push(
|
|
61
|
+
if (agentCount > 0) parts.push(agentCountLabel(agentCount));
|
|
51
62
|
if (toolCount > 0) parts.push(`${toolCount} tool${toolCount !== 1 ? 's' : ''} running`);
|
|
52
63
|
const label = total === 0
|
|
53
64
|
? `No background processes ${formatHints([{ key: 'Esc', verb: 'Back to input' }])}`
|
|
@@ -62,7 +73,7 @@ export function renderProcessIndicator(
|
|
|
62
73
|
// Build the label: "bg: 2 agents | Turn 3 | write - src/foo.ts"
|
|
63
74
|
const parts: string[] = [];
|
|
64
75
|
if (agentCount > 0) {
|
|
65
|
-
parts.push(
|
|
76
|
+
parts.push(agentCountLabel(agentCount));
|
|
66
77
|
}
|
|
67
78
|
if (toolCount > 0) {
|
|
68
79
|
parts.push(`${toolCount} tool${toolCount !== 1 ? 's' : ''} running`);
|
|
@@ -82,5 +93,5 @@ export function renderProcessIndicator(
|
|
|
82
93
|
: '';
|
|
83
94
|
const label = `${parts.join(` ${GLYPHS.navigation.pipeSeparator} `)}${progressSuffix}`;
|
|
84
95
|
const hint = ` ${formatHints([{ key: 'Enter', verb: 'View' }])}`;
|
|
85
|
-
return renderPlainStatus(`${label}${hint}`, { fg:
|
|
96
|
+
return renderPlainStatus(`${label}${hint}`, { fg: activeUiTones().accent.brand, bold: true });
|
|
86
97
|
}
|
|
@@ -189,7 +189,12 @@ export function renderSelectionModalOverlay(
|
|
|
189
189
|
|
|
190
190
|
const footerLine = createOverlayContentLine(width, layout, BORDER_FG, DEFAULT_OVERLAY_PALETTE.sectionBg);
|
|
191
191
|
const selectedItem = modal.getSelected();
|
|
192
|
-
|
|
192
|
+
// UX-C vocab unification: a caller-supplied primaryVerbLabel (e.g. /help's
|
|
193
|
+
// "Run", matching the slash-command palette) wins over the generic
|
|
194
|
+
// per-primaryAction default.
|
|
195
|
+
const primaryVerb = modal.primaryVerbLabel
|
|
196
|
+
? `[Enter] ${modal.primaryVerbLabel}`
|
|
197
|
+
: selectedItem?.primaryAction === 'toggle'
|
|
193
198
|
? '[Enter] Toggle'
|
|
194
199
|
: selectedItem?.primaryAction === 'edit'
|
|
195
200
|
? '[Enter] Edit'
|