@pellux/goodvibes-tui 0.28.0 → 1.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +133 -0
- package/README.md +1 -1
- package/package.json +1 -1
- package/src/core/conversation-line-cache.ts +432 -0
- package/src/core/conversation-rendering.ts +11 -3
- package/src/core/conversation.ts +50 -4
- package/src/input/commands/cost-runtime.ts +49 -0
- package/src/input/commands/operator-runtime.ts +5 -1
- package/src/input/commands.ts +2 -0
- package/src/input/handler-content-actions.ts +8 -3
- package/src/input/handler-feed-routes.ts +12 -35
- package/src/input/handler-feed.ts +4 -8
- package/src/input/handler-shortcuts.ts +51 -0
- package/src/input/handler.ts +43 -6
- package/src/input/input-history.ts +17 -4
- package/src/input/keybindings.ts +48 -8
- package/src/input/panel-integration-actions.ts +107 -1
- package/src/main.ts +19 -11
- package/src/panels/agent-inspector-panel.ts +125 -36
- package/src/panels/agent-inspector-shared.ts +144 -0
- package/src/panels/approval-panel.ts +67 -16
- package/src/panels/automation-control-panel.ts +368 -124
- package/src/panels/base-panel.ts +1 -1
- package/src/panels/builtin/agent.ts +28 -37
- package/src/panels/builtin/development.ts +40 -32
- package/src/panels/builtin/knowledge.ts +15 -6
- package/src/panels/builtin/operations.ts +178 -117
- package/src/panels/builtin/session.ts +35 -9
- package/src/panels/builtin/shared.ts +98 -6
- package/src/panels/cockpit-panel.ts +232 -73
- package/src/panels/communication-panel.ts +58 -20
- package/src/panels/confirm-state.ts +8 -1
- package/src/panels/control-plane-panel.ts +351 -118
- package/src/panels/cost-tracker-panel.ts +165 -7
- package/src/panels/debug-panel.ts +312 -159
- package/src/panels/diff-panel.ts +209 -57
- package/src/panels/docs-panel.ts +152 -66
- package/src/panels/eval-panel.ts +220 -42
- package/src/panels/file-explorer-panel.ts +202 -122
- package/src/panels/file-preview-panel.ts +132 -57
- package/src/panels/git-panel.ts +310 -129
- package/src/panels/hooks-panel.ts +150 -22
- package/src/panels/incident-review-panel.ts +223 -35
- package/src/panels/index.ts +0 -4
- package/src/panels/intelligence-panel.ts +212 -86
- package/src/panels/knowledge-graph-panel.ts +461 -101
- package/src/panels/local-auth-panel.ts +240 -28
- package/src/panels/marketplace-panel.ts +193 -26
- package/src/panels/memory-panel.ts +78 -77
- package/src/panels/ops-control-panel.ts +146 -29
- package/src/panels/ops-strategy-panel.ts +72 -4
- package/src/panels/orchestration-panel.ts +231 -69
- package/src/panels/panel-list-panel.ts +154 -131
- package/src/panels/panel-manager.ts +97 -9
- package/src/panels/plan-dashboard-panel.ts +333 -17
- package/src/panels/plugins-panel.ts +184 -29
- package/src/panels/policy-panel.ts +210 -38
- package/src/panels/polish-core.ts +7 -2
- package/src/panels/polish.ts +23 -4
- package/src/panels/project-planning-answer-actions.ts +134 -0
- package/src/panels/project-planning-panel.ts +62 -113
- package/src/panels/provider-health-panel.ts +434 -518
- package/src/panels/provider-health-routes.ts +203 -0
- package/src/panels/provider-health-tracker.ts +194 -6
- package/src/panels/provider-health-views.ts +560 -0
- package/src/panels/qr-panel.ts +116 -30
- package/src/panels/remote-panel.ts +114 -36
- package/src/panels/routes-panel.ts +63 -22
- package/src/panels/sandbox-panel.ts +174 -42
- package/src/panels/scrollable-list-panel.ts +19 -135
- package/src/panels/security-panel.ts +133 -33
- package/src/panels/services-panel.ts +116 -64
- package/src/panels/session-browser-panel.ts +73 -2
- package/src/panels/session-maintenance.ts +4 -122
- package/src/panels/settings-sync-panel.ts +335 -72
- package/src/panels/skills-panel.ts +157 -89
- package/src/panels/subscription-panel.ts +86 -33
- package/src/panels/symbol-outline-panel.ts +248 -108
- package/src/panels/system-messages-panel.ts +114 -13
- package/src/panels/tasks-panel.ts +326 -139
- package/src/panels/thinking-panel.ts +43 -10
- package/src/panels/token-budget-panel.ts +194 -18
- package/src/panels/tool-inspector-panel.ts +144 -34
- package/src/panels/types.ts +40 -4
- package/src/panels/work-plan-panel.ts +280 -17
- package/src/panels/worktree-panel.ts +175 -42
- package/src/panels/wrfc-panel.ts +116 -24
- package/src/renderer/agent-detail-modal.ts +37 -8
- package/src/renderer/code-block.ts +58 -28
- package/src/renderer/context-inspector.ts +3 -6
- package/src/renderer/conversation-overlays.ts +25 -11
- package/src/renderer/conversation-surface.ts +1 -21
- package/src/renderer/diff-view.ts +6 -4
- package/src/renderer/footer-tips.ts +41 -0
- package/src/renderer/fullscreen-primitives.ts +23 -17
- package/src/renderer/fullscreen-workspace.ts +2 -7
- package/src/renderer/help-overlay.ts +91 -14
- package/src/renderer/hint-grammar.ts +52 -0
- package/src/renderer/history-search-overlay.ts +10 -16
- package/src/renderer/layout.ts +7 -7
- package/src/renderer/markdown.ts +9 -1
- package/src/renderer/modal-factory.ts +23 -15
- package/src/renderer/model-picker-overlay.ts +8 -485
- package/src/renderer/model-workspace.ts +9 -24
- package/src/renderer/overlay-box.ts +23 -19
- package/src/renderer/process-indicator.ts +15 -25
- package/src/renderer/profile-picker-modal.ts +13 -14
- package/src/renderer/prompt-content-width.ts +16 -0
- package/src/renderer/search-overlay.ts +27 -6
- package/src/renderer/selection-modal-overlay.ts +3 -1
- package/src/renderer/semantic-diff.ts +1 -1
- package/src/renderer/session-picker-modal.ts +6 -4
- package/src/renderer/settings-modal-helpers.ts +10 -34
- package/src/renderer/settings-modal.ts +70 -10
- package/src/renderer/shell-surface.ts +54 -15
- package/src/renderer/status-glyphs.ts +11 -9
- package/src/renderer/surface-layout.ts +0 -12
- package/src/renderer/term-caps.ts +1 -1
- package/src/renderer/theme.ts +60 -3
- package/src/renderer/tool-call.ts +6 -20
- package/src/renderer/ui-factory.ts +48 -37
- package/src/renderer/ui-primitives.ts +41 -3
- package/src/runtime/bootstrap-shell.ts +35 -18
- package/src/runtime/diagnostics/panels/index.ts +0 -3
- package/src/runtime/render-scheduler.ts +80 -0
- package/src/shell/ui-openers.ts +14 -22
- package/src/utils/format-duration.ts +2 -2
- package/src/utils/splash-lines.ts +54 -5
- package/src/version.ts +1 -1
- package/src/work-plans/work-plan-store.ts +13 -0
- package/src/panels/agent-logs-panel.ts +0 -635
- package/src/panels/agent-logs-shared.ts +0 -129
- package/src/panels/context-visualizer-panel.ts +0 -238
- package/src/panels/forensics-panel.ts +0 -378
- package/src/panels/provider-account-snapshot.ts +0 -259
- package/src/panels/provider-accounts-panel.ts +0 -255
- package/src/panels/provider-stats-panel.ts +0 -391
- package/src/panels/schedule-panel.ts +0 -365
- package/src/panels/watchers-panel.ts +0 -213
- package/src/renderer/file-tree.ts +0 -153
- package/src/renderer/progress.ts +0 -99
- package/src/renderer/status-token.ts +0 -67
- package/src/runtime/diagnostics/panels/panel-resources.ts +0 -118
- package/src/runtime/diagnostics/panels/policy.ts +0 -177
|
@@ -10,6 +10,24 @@ import { getOverlaySurfaceMetrics, getStableOverlayContentRows } from './overlay
|
|
|
10
10
|
import { summarizeError } from '@pellux/goodvibes-sdk/platform/utils';
|
|
11
11
|
import { handleConfirmInput, type ConfirmState } from '../panels/confirm-state.ts';
|
|
12
12
|
import { AGENT_TERMINAL_STATUSES as MODAL_TERMINAL_STATUSES, AGENT_STALL_THRESHOLD_MS as MODAL_STALL_THRESHOLD_MS } from '../panels/agent-inspector-shared.ts';
|
|
13
|
+
import { UI_TONES } from './ui-primitives.ts';
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* Agent-detail-modal neon accent palette — WRFC/streaming panel-native hues
|
|
17
|
+
* with no UI_TONES.state/accent role. Preserved byte-exact as named
|
|
18
|
+
* accents per WO-207b (2026-07-02-renderer-plan.md decisions: "Agent-detail
|
|
19
|
+
* neon palette: preserved byte-exact as named accents, zero visual change").
|
|
20
|
+
*/
|
|
21
|
+
const AGENT_DETAIL_NEON = {
|
|
22
|
+
/** Addendum note + streaming content text. */
|
|
23
|
+
mint: '#aaffee',
|
|
24
|
+
/** Unsatisfied WRFC findings + agent error text. */
|
|
25
|
+
bad: '#ff6666',
|
|
26
|
+
/** Satisfied WRFC findings. */
|
|
27
|
+
good: '#44ff88',
|
|
28
|
+
/** Progress line + streaming label. */
|
|
29
|
+
accent: '#00ffcc',
|
|
30
|
+
} as const;
|
|
13
31
|
|
|
14
32
|
// ─── Constants ────────────────────────────────────────────────────────────────
|
|
15
33
|
|
|
@@ -19,6 +37,17 @@ const AGENT_ID_DISPLAY_LENGTH = 16;
|
|
|
19
37
|
// MODAL_TERMINAL_STATUSES and MODAL_STALL_THRESHOLD_MS are re-exported aliases
|
|
20
38
|
// from agent-inspector-shared.ts (imported above alongside ConfirmState).
|
|
21
39
|
|
|
40
|
+
/**
|
|
41
|
+
* formatStalledLabel — the "[STALLED — N+ min no activity]" suffix appended
|
|
42
|
+
* to the Status line for a stalled agent. The minute count is derived from
|
|
43
|
+
* the stall threshold (in ms) rather than hardcoded, so the label can never
|
|
44
|
+
* drift out of sync with the actual threshold that decided isStalled.
|
|
45
|
+
*/
|
|
46
|
+
export function formatStalledLabel(thresholdMs: number): string {
|
|
47
|
+
const minutes = Math.floor(thresholdMs / 60000);
|
|
48
|
+
return ` [STALLED — ${minutes}+ min no activity]`;
|
|
49
|
+
}
|
|
50
|
+
|
|
22
51
|
export interface AgentDetailModalDeps {
|
|
23
52
|
readonly agentManager: Pick<AgentManager, 'getStatus' | 'list'>;
|
|
24
53
|
readonly agentMessageBus: Pick<AgentMessageBus, 'getMessages'>;
|
|
@@ -248,7 +277,7 @@ export function renderAgentDetailModal(
|
|
|
248
277
|
sections.push({ type: 'text', content: `Template : ${rec.template}` });
|
|
249
278
|
sections.push({ type: 'text', content: `Model : ${modelStr}` });
|
|
250
279
|
const isStalled = !MODAL_TERMINAL_STATUSES.has(rec.status) && (now - rec.startedAt) >= MODAL_STALL_THRESHOLD_MS;
|
|
251
|
-
sections.push({ type: 'text', content: `Status : ${rec.status}${isStalled ?
|
|
280
|
+
sections.push({ type: 'text', content: `Status : ${rec.status}${isStalled ? formatStalledLabel(MODAL_STALL_THRESHOLD_MS) : ''}` });
|
|
252
281
|
sections.push({ type: 'text', content: `Duration : ${formatElapsed(elapsedMs)}` });
|
|
253
282
|
sections.push({ type: 'separator' });
|
|
254
283
|
|
|
@@ -271,7 +300,7 @@ export function renderAgentDetailModal(
|
|
|
271
300
|
sections.push({
|
|
272
301
|
type: 'text',
|
|
273
302
|
content: 'Addendum : yes (WRFC constraint layer injected)',
|
|
274
|
-
style: { fg:
|
|
303
|
+
style: { fg: AGENT_DETAIL_NEON.mint },
|
|
275
304
|
});
|
|
276
305
|
}
|
|
277
306
|
|
|
@@ -301,7 +330,7 @@ export function renderAgentDetailModal(
|
|
|
301
330
|
sections.push({
|
|
302
331
|
type: 'text',
|
|
303
332
|
content: `Findings : ${findings.length} checked, ${unsatisfied.length} unsatisfied`,
|
|
304
|
-
style: { fg: unsatisfied.length > 0 ?
|
|
333
|
+
style: { fg: unsatisfied.length > 0 ? AGENT_DETAIL_NEON.bad : AGENT_DETAIL_NEON.good },
|
|
305
334
|
});
|
|
306
335
|
}
|
|
307
336
|
}
|
|
@@ -316,7 +345,7 @@ export function renderAgentDetailModal(
|
|
|
316
345
|
sections.push({
|
|
317
346
|
type: 'text',
|
|
318
347
|
content: `Progress: ${rec.progress}`,
|
|
319
|
-
style: { fg:
|
|
348
|
+
style: { fg: AGENT_DETAIL_NEON.accent },
|
|
320
349
|
});
|
|
321
350
|
}
|
|
322
351
|
|
|
@@ -326,7 +355,7 @@ export function renderAgentDetailModal(
|
|
|
326
355
|
sections.push({
|
|
327
356
|
type: 'text',
|
|
328
357
|
content: `Error: ${rec.error}`,
|
|
329
|
-
style: { fg:
|
|
358
|
+
style: { fg: AGENT_DETAIL_NEON.bad },
|
|
330
359
|
});
|
|
331
360
|
}
|
|
332
361
|
|
|
@@ -393,7 +422,7 @@ export function renderAgentDetailModal(
|
|
|
393
422
|
content: truncated
|
|
394
423
|
? `Streaming (last ${STREAMING_MAX_CHARS} of ${content.length} chars \u2191 scroll for more):`
|
|
395
424
|
: 'Streaming:',
|
|
396
|
-
style: { fg:
|
|
425
|
+
style: { fg: AGENT_DETAIL_NEON.accent, dim: true },
|
|
397
426
|
});
|
|
398
427
|
// Split into display lines, capped at width for readability
|
|
399
428
|
const maxLineWidth = Math.max(width - 10, 40);
|
|
@@ -403,7 +432,7 @@ export function renderAgentDetailModal(
|
|
|
403
432
|
sections.push({
|
|
404
433
|
type: 'text',
|
|
405
434
|
content: ` ${trimmed}`,
|
|
406
|
-
style: { fg:
|
|
435
|
+
style: { fg: AGENT_DETAIL_NEON.mint },
|
|
407
436
|
});
|
|
408
437
|
}
|
|
409
438
|
}
|
|
@@ -415,7 +444,7 @@ export function renderAgentDetailModal(
|
|
|
415
444
|
sections.push({
|
|
416
445
|
type: 'text',
|
|
417
446
|
content: `Cancel agent "${modal.confirmCancel.label}"?`,
|
|
418
|
-
style: { fg:
|
|
447
|
+
style: { fg: UI_TONES.state.warn },
|
|
419
448
|
});
|
|
420
449
|
sections.push({
|
|
421
450
|
type: 'text',
|
|
@@ -3,6 +3,32 @@ import { UIFactory } from './ui-factory.ts';
|
|
|
3
3
|
import { getDisplayWidth } from '../utils/terminal-width.ts';
|
|
4
4
|
import { LAYOUT } from './layout.ts';
|
|
5
5
|
import { SyntaxHighlighter, type SyntaxToken as HLToken } from './syntax-highlighter.ts';
|
|
6
|
+
import { UI_TONES } from './ui-primitives.ts';
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* Regex-fallback tokenizer theme. VS Code Dark+ inspired, but the
|
|
10
|
+
* semantic-token hues (string/number/keyword/type/function/operator/
|
|
11
|
+
* property/comment) are pinned to the same values as the tree-sitter
|
|
12
|
+
* Vaporwave palette in syntax-highlighter.ts's TOKEN_STYLES map, so a code
|
|
13
|
+
* block that starts on the regex fallback and gets replaced by the async
|
|
14
|
+
* tree-sitter parse doesn't visibly shift color mid-stream (WO-207c).
|
|
15
|
+
* `accent` and `bg` are chrome (the language-label header bar and the body
|
|
16
|
+
* background), not syntax tokens, and keep their original VS Code Dark+
|
|
17
|
+
* values — folded here from two separate literals (one in the header, one
|
|
18
|
+
* duplicated on the footer line) into a single named source (WO-207b).
|
|
19
|
+
*/
|
|
20
|
+
const FALLBACK_THEME = {
|
|
21
|
+
string: '#00ff88',
|
|
22
|
+
number: '#ffcc00',
|
|
23
|
+
keyword: '#d000ff',
|
|
24
|
+
type: '#ff6b9d',
|
|
25
|
+
function: UI_TONES.accent.brand,
|
|
26
|
+
operator: '#ffffff',
|
|
27
|
+
property: '#87ceeb',
|
|
28
|
+
comment: '#666666',
|
|
29
|
+
bg: '#0d0d0d',
|
|
30
|
+
accent: '#4ec9b0',
|
|
31
|
+
} as const;
|
|
6
32
|
|
|
7
33
|
// ─── Language Keyword Maps ───────────────────────────────────────────────────
|
|
8
34
|
|
|
@@ -61,7 +87,7 @@ function tokenizeTsJs(line: string): SyntaxToken[] {
|
|
|
61
87
|
while (i < line.length) {
|
|
62
88
|
// Line comment
|
|
63
89
|
if (line.slice(i, i + 2) === '//') {
|
|
64
|
-
tokens.push({ text: line.slice(i), fg:
|
|
90
|
+
tokens.push({ text: line.slice(i), fg: FALLBACK_THEME.comment, italic: true });
|
|
65
91
|
break;
|
|
66
92
|
}
|
|
67
93
|
// String (single, double, template)
|
|
@@ -72,7 +98,7 @@ function tokenizeTsJs(line: string): SyntaxToken[] {
|
|
|
72
98
|
if (line[j] === '\\') j++;
|
|
73
99
|
j++;
|
|
74
100
|
}
|
|
75
|
-
tokens.push({ text: line.slice(i, j + 1), fg:
|
|
101
|
+
tokens.push({ text: line.slice(i, j + 1), fg: FALLBACK_THEME.string });
|
|
76
102
|
i = j + 1;
|
|
77
103
|
continue;
|
|
78
104
|
}
|
|
@@ -80,7 +106,7 @@ function tokenizeTsJs(line: string): SyntaxToken[] {
|
|
|
80
106
|
if (/[0-9]/.test(line[i])) {
|
|
81
107
|
let j = i;
|
|
82
108
|
while (j < line.length && /[0-9._xXbBoO]/.test(line[j])) j++;
|
|
83
|
-
tokens.push({ text: line.slice(i, j), fg:
|
|
109
|
+
tokens.push({ text: line.slice(i, j), fg: FALLBACK_THEME.number });
|
|
84
110
|
i = j;
|
|
85
111
|
continue;
|
|
86
112
|
}
|
|
@@ -90,11 +116,11 @@ function tokenizeTsJs(line: string): SyntaxToken[] {
|
|
|
90
116
|
while (j < line.length && /[\w$]/.test(line[j])) j++;
|
|
91
117
|
const word = line.slice(i, j);
|
|
92
118
|
if (TS_JS_KEYWORDS.has(word)) {
|
|
93
|
-
tokens.push({ text: word, fg:
|
|
119
|
+
tokens.push({ text: word, fg: FALLBACK_THEME.keyword, bold: true });
|
|
94
120
|
} else if (TS_TYPES.has(word)) {
|
|
95
|
-
tokens.push({ text: word, fg:
|
|
121
|
+
tokens.push({ text: word, fg: FALLBACK_THEME.type });
|
|
96
122
|
} else if (line[j] === '(') {
|
|
97
|
-
tokens.push({ text: word, fg:
|
|
123
|
+
tokens.push({ text: word, fg: FALLBACK_THEME.function });
|
|
98
124
|
} else {
|
|
99
125
|
tokens.push({ text: word, fg: '' });
|
|
100
126
|
}
|
|
@@ -104,7 +130,7 @@ function tokenizeTsJs(line: string): SyntaxToken[] {
|
|
|
104
130
|
// Operators and punctuation
|
|
105
131
|
const ch = line[i];
|
|
106
132
|
const isOp = '=<>!&|+-*/%^~?:'.includes(ch);
|
|
107
|
-
tokens.push({ text: ch, fg: isOp ?
|
|
133
|
+
tokens.push({ text: ch, fg: isOp ? FALLBACK_THEME.operator : '' });
|
|
108
134
|
i++;
|
|
109
135
|
}
|
|
110
136
|
|
|
@@ -117,21 +143,21 @@ function tokenizePython(line: string): SyntaxToken[] {
|
|
|
117
143
|
|
|
118
144
|
while (i < line.length) {
|
|
119
145
|
if (line[i] === '#') {
|
|
120
|
-
tokens.push({ text: line.slice(i), fg:
|
|
146
|
+
tokens.push({ text: line.slice(i), fg: FALLBACK_THEME.comment, italic: true });
|
|
121
147
|
break;
|
|
122
148
|
}
|
|
123
149
|
if (line[i] === '"' || line[i] === "'") {
|
|
124
150
|
const q = line[i];
|
|
125
151
|
let j = i + 1;
|
|
126
152
|
while (j < line.length && line[j] !== q) { if (line[j] === '\\') j++; j++; }
|
|
127
|
-
tokens.push({ text: line.slice(i, j + 1), fg:
|
|
153
|
+
tokens.push({ text: line.slice(i, j + 1), fg: FALLBACK_THEME.string });
|
|
128
154
|
i = j + 1;
|
|
129
155
|
continue;
|
|
130
156
|
}
|
|
131
157
|
if (/[0-9]/.test(line[i])) {
|
|
132
158
|
let j = i;
|
|
133
159
|
while (j < line.length && /[0-9._]/.test(line[j])) j++;
|
|
134
|
-
tokens.push({ text: line.slice(i, j), fg:
|
|
160
|
+
tokens.push({ text: line.slice(i, j), fg: FALLBACK_THEME.number });
|
|
135
161
|
i = j;
|
|
136
162
|
continue;
|
|
137
163
|
}
|
|
@@ -140,11 +166,11 @@ function tokenizePython(line: string): SyntaxToken[] {
|
|
|
140
166
|
while (j < line.length && /[\w]/.test(line[j])) j++;
|
|
141
167
|
const word = line.slice(i, j);
|
|
142
168
|
if (PYTHON_KEYWORDS.has(word)) {
|
|
143
|
-
tokens.push({ text: word, fg:
|
|
169
|
+
tokens.push({ text: word, fg: FALLBACK_THEME.keyword, bold: true });
|
|
144
170
|
} else if (/^[A-Z]/.test(word)) {
|
|
145
|
-
tokens.push({ text: word, fg:
|
|
171
|
+
tokens.push({ text: word, fg: FALLBACK_THEME.type });
|
|
146
172
|
} else if (line[j] === '(') {
|
|
147
|
-
tokens.push({ text: word, fg:
|
|
173
|
+
tokens.push({ text: word, fg: FALLBACK_THEME.function });
|
|
148
174
|
} else {
|
|
149
175
|
tokens.push({ text: word, fg: '' });
|
|
150
176
|
}
|
|
@@ -163,21 +189,21 @@ function tokenizeBash(line: string): SyntaxToken[] {
|
|
|
163
189
|
|
|
164
190
|
while (i < line.length) {
|
|
165
191
|
if (line[i] === '#') {
|
|
166
|
-
tokens.push({ text: line.slice(i), fg:
|
|
192
|
+
tokens.push({ text: line.slice(i), fg: FALLBACK_THEME.comment, italic: true });
|
|
167
193
|
break;
|
|
168
194
|
}
|
|
169
195
|
if (line[i] === '"' || line[i] === "'") {
|
|
170
196
|
const q = line[i];
|
|
171
197
|
let j = i + 1;
|
|
172
198
|
while (j < line.length && line[j] !== q) { if (line[j] === '\\') j++; j++; }
|
|
173
|
-
tokens.push({ text: line.slice(i, j + 1), fg:
|
|
199
|
+
tokens.push({ text: line.slice(i, j + 1), fg: FALLBACK_THEME.string });
|
|
174
200
|
i = j + 1;
|
|
175
201
|
continue;
|
|
176
202
|
}
|
|
177
203
|
if (line[i] === '$') {
|
|
178
204
|
let j = i + 1;
|
|
179
205
|
while (j < line.length && /[\w{}_]/.test(line[j])) j++;
|
|
180
|
-
tokens.push({ text: line.slice(i, j), fg:
|
|
206
|
+
tokens.push({ text: line.slice(i, j), fg: FALLBACK_THEME.property });
|
|
181
207
|
i = j;
|
|
182
208
|
continue;
|
|
183
209
|
}
|
|
@@ -186,7 +212,7 @@ function tokenizeBash(line: string): SyntaxToken[] {
|
|
|
186
212
|
while (j < line.length && /[\w-]/.test(line[j])) j++;
|
|
187
213
|
const word = line.slice(i, j);
|
|
188
214
|
if (BASH_KEYWORDS.has(word)) {
|
|
189
|
-
tokens.push({ text: word, fg:
|
|
215
|
+
tokens.push({ text: word, fg: FALLBACK_THEME.keyword, bold: true });
|
|
190
216
|
} else {
|
|
191
217
|
tokens.push({ text: word, fg: '' });
|
|
192
218
|
}
|
|
@@ -211,9 +237,9 @@ function tokenizeJson(line: string): SyntaxToken[] {
|
|
|
211
237
|
// JSON key: followed by :
|
|
212
238
|
const rest = line.slice(j + 1).trimStart();
|
|
213
239
|
if (rest.startsWith(':')) {
|
|
214
|
-
tokens.push({ text: str, fg:
|
|
240
|
+
tokens.push({ text: str, fg: FALLBACK_THEME.property });
|
|
215
241
|
} else {
|
|
216
|
-
tokens.push({ text: str, fg:
|
|
242
|
+
tokens.push({ text: str, fg: FALLBACK_THEME.string });
|
|
217
243
|
}
|
|
218
244
|
i = j + 1;
|
|
219
245
|
continue;
|
|
@@ -221,13 +247,13 @@ function tokenizeJson(line: string): SyntaxToken[] {
|
|
|
221
247
|
if (/[0-9-]/.test(line[i])) {
|
|
222
248
|
let j = i;
|
|
223
249
|
while (j < line.length && /[0-9.eE+-]/.test(line[j])) j++;
|
|
224
|
-
tokens.push({ text: line.slice(i, j), fg:
|
|
250
|
+
tokens.push({ text: line.slice(i, j), fg: FALLBACK_THEME.number });
|
|
225
251
|
i = j;
|
|
226
252
|
continue;
|
|
227
253
|
}
|
|
228
254
|
const boolNull = ['true', 'false', 'null'].find(k => line.startsWith(k, i));
|
|
229
255
|
if (boolNull) {
|
|
230
|
-
tokens.push({ text: boolNull, fg:
|
|
256
|
+
tokens.push({ text: boolNull, fg: FALLBACK_THEME.keyword, bold: true });
|
|
231
257
|
i += boolNull.length;
|
|
232
258
|
continue;
|
|
233
259
|
}
|
|
@@ -240,12 +266,12 @@ function tokenizeJson(line: string): SyntaxToken[] {
|
|
|
240
266
|
function tokenizeYaml(line: string): SyntaxToken[] {
|
|
241
267
|
const tokens: SyntaxToken[] = [];
|
|
242
268
|
if (line.trimStart().startsWith('#')) {
|
|
243
|
-
return [{ text: line, fg:
|
|
269
|
+
return [{ text: line, fg: FALLBACK_THEME.comment, italic: true }];
|
|
244
270
|
}
|
|
245
271
|
const keyMatch = line.match(/^(\s*)([^:]+)(:)(\s*.*)/);
|
|
246
272
|
if (keyMatch) {
|
|
247
273
|
if (keyMatch[1]) tokens.push({ text: keyMatch[1], fg: '' });
|
|
248
|
-
tokens.push({ text: keyMatch[2], fg:
|
|
274
|
+
tokens.push({ text: keyMatch[2], fg: FALLBACK_THEME.property });
|
|
249
275
|
tokens.push({ text: keyMatch[3], fg: '244' });
|
|
250
276
|
if (keyMatch[4]) {
|
|
251
277
|
const val = keyMatch[4];
|
|
@@ -254,7 +280,7 @@ function tokenizeYaml(line: string): SyntaxToken[] {
|
|
|
254
280
|
const isStr = /^['"]/.test(trimVal);
|
|
255
281
|
const isBool = trimVal === 'true' || trimVal === 'false' || trimVal === 'null' || trimVal === 'yes' || trimVal === 'no';
|
|
256
282
|
const isNum = /^-?[0-9]/.test(trimVal);
|
|
257
|
-
const valFg = isStr ?
|
|
283
|
+
const valFg = isStr ? FALLBACK_THEME.string : isBool ? FALLBACK_THEME.keyword : isNum ? FALLBACK_THEME.number : '';
|
|
258
284
|
tokens.push({ text: val, fg: valFg });
|
|
259
285
|
}
|
|
260
286
|
return tokens;
|
|
@@ -292,7 +318,7 @@ export function renderCodeBlock(
|
|
|
292
318
|
const showLineNumbers = opts.showLineNumbers ?? true;
|
|
293
319
|
const lineNumW = showLineNumbers ? String(codeLines.length).length + 1 : 0; // e.g. "10 "
|
|
294
320
|
const contentStartX = showLineNumbers ? leftMargin + lineNumW + 1 : leftMargin;
|
|
295
|
-
const BG =
|
|
321
|
+
const BG = FALLBACK_THEME.bg;
|
|
296
322
|
const LINE_NUM_FG = '238';
|
|
297
323
|
const effectiveWidth = width - LAYOUT.RIGHT_MARGIN;
|
|
298
324
|
|
|
@@ -320,7 +346,7 @@ export function renderCodeBlock(
|
|
|
320
346
|
let hx = leftMargin;
|
|
321
347
|
for (const ch of headerStr) {
|
|
322
348
|
if (hx >= effectiveWidth) break;
|
|
323
|
-
headerLine[hx] = createStyledCell(ch, { fg: '#1a1a1a', bg:
|
|
349
|
+
headerLine[hx] = createStyledCell(ch, { fg: '#1a1a1a', bg: FALLBACK_THEME.accent, bold: true });
|
|
324
350
|
hx++;
|
|
325
351
|
}
|
|
326
352
|
lines.push(headerLine);
|
|
@@ -362,7 +388,11 @@ export function renderCodeBlock(
|
|
|
362
388
|
continue;
|
|
363
389
|
}
|
|
364
390
|
line[cx] = createStyledCell(ch, { fg: token.fg, bg: BG, bold: token.bold, italic: token.italic });
|
|
365
|
-
|
|
391
|
+
// Bound the wide-glyph placeholder against the body's own right edge
|
|
392
|
+
// (effectiveWidth), not the full line width — otherwise a 2-column
|
|
393
|
+
// glyph landing on the last body column spills its placeholder cell
|
|
394
|
+
// into the reserved right-margin band the header/footer stop at.
|
|
395
|
+
if (cw === 2 && cx + 1 < effectiveWidth) line[cx + 1] = { ...line[cx], char: '' };
|
|
366
396
|
cx += cw;
|
|
367
397
|
}
|
|
368
398
|
}
|
|
@@ -373,7 +403,7 @@ export function renderCodeBlock(
|
|
|
373
403
|
// Footer line
|
|
374
404
|
const footerLine = createEmptyLine(width);
|
|
375
405
|
for (let fx = leftMargin; fx < effectiveWidth; fx++) {
|
|
376
|
-
footerLine[fx] = createStyledCell(' ', { bg:
|
|
406
|
+
footerLine[fx] = createStyledCell(' ', { bg: BG });
|
|
377
407
|
}
|
|
378
408
|
lines.push(footerLine);
|
|
379
409
|
|
|
@@ -3,6 +3,7 @@ import { ModalFactory } from './modal-factory.ts';
|
|
|
3
3
|
import type { ConversationManager } from '../core/conversation';
|
|
4
4
|
import { getOverlayContentBudget, getOverlaySurfaceMetrics, getStableOverlayContentRows } from './overlay-viewport.ts';
|
|
5
5
|
import { estimateTokens } from '@pellux/goodvibes-sdk/platform/core';
|
|
6
|
+
import { UI_TONES } from './ui-primitives.ts';
|
|
6
7
|
|
|
7
8
|
// ─── ContextInspectorModal ────────────────────────────────────────────────────
|
|
8
9
|
|
|
@@ -84,7 +85,6 @@ export function renderContextInspector(
|
|
|
84
85
|
|
|
85
86
|
const entries: MsgEntry[] = [];
|
|
86
87
|
let totalTokens = 0;
|
|
87
|
-
let largeCount = 0;
|
|
88
88
|
|
|
89
89
|
for (const msg of messages) {
|
|
90
90
|
const role = msg.role;
|
|
@@ -126,9 +126,6 @@ export function renderContextInspector(
|
|
|
126
126
|
// ── Identify large consumers (>10%) ───────────────────────────────────────
|
|
127
127
|
|
|
128
128
|
const largeThreshold = totalTokens * 0.10;
|
|
129
|
-
for (const e of entries) {
|
|
130
|
-
if (e.tokens > largeThreshold) largeCount++;
|
|
131
|
-
}
|
|
132
129
|
|
|
133
130
|
// ── Build sections ────────────────────────────────────────────────────────
|
|
134
131
|
|
|
@@ -148,7 +145,7 @@ export function renderContextInspector(
|
|
|
148
145
|
sections.push({
|
|
149
146
|
type: 'text',
|
|
150
147
|
content: 'WARNING: context is 80%+ full. Run /compact to free space.',
|
|
151
|
-
style: { fg:
|
|
148
|
+
style: { fg: UI_TONES.state.warn, bold: true },
|
|
152
149
|
});
|
|
153
150
|
}
|
|
154
151
|
|
|
@@ -181,7 +178,7 @@ export function renderContextInspector(
|
|
|
181
178
|
sections.push({
|
|
182
179
|
type: 'text',
|
|
183
180
|
content: line,
|
|
184
|
-
style: isLarge ? { fg:
|
|
181
|
+
style: isLarge ? { fg: UI_TONES.state.warn, bold: true } : {},
|
|
185
182
|
});
|
|
186
183
|
}
|
|
187
184
|
|
|
@@ -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);
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { type Line, createEmptyLine, createStyledCell } from '../types/grid.ts';
|
|
2
|
-
import { getDisplayWidth,
|
|
2
|
+
import { getDisplayWidth, wrapText } from '../utils/terminal-width.ts';
|
|
3
3
|
import { LAYOUT } from './layout.ts';
|
|
4
4
|
import { GLYPHS } from './ui-primitives.ts';
|
|
5
5
|
|
|
@@ -185,26 +185,6 @@ export function renderConversationCollapsedFragment(
|
|
|
185
185
|
});
|
|
186
186
|
}
|
|
187
187
|
|
|
188
|
-
export function renderConversationKeyValueRow(
|
|
189
|
-
width: number,
|
|
190
|
-
left: string,
|
|
191
|
-
right: string,
|
|
192
|
-
palette: {
|
|
193
|
-
readonly leftFg: string;
|
|
194
|
-
readonly rightFg: string;
|
|
195
|
-
readonly dimFg?: string;
|
|
196
|
-
readonly bg?: string;
|
|
197
|
-
},
|
|
198
|
-
): Line {
|
|
199
|
-
const line = createEmptyLine(width);
|
|
200
|
-
const leftText = truncateDisplay(left, Math.max(1, width - LAYOUT.RIGHT_MARGIN - 8));
|
|
201
|
-
const rightWidth = getDisplayWidth(right);
|
|
202
|
-
const rightStart = Math.max(LAYOUT.LEFT_MARGIN + 1, width - LAYOUT.RIGHT_MARGIN - rightWidth);
|
|
203
|
-
writeText(line, LAYOUT.LEFT_MARGIN, rightStart - 1, leftText, palette.leftFg, { bg: palette.bg });
|
|
204
|
-
writeText(line, rightStart, width - LAYOUT.RIGHT_MARGIN, right, palette.rightFg, { bg: palette.bg, dim: palette.dimFg === palette.rightFg });
|
|
205
|
-
return line;
|
|
206
|
-
}
|
|
207
|
-
|
|
208
188
|
export function renderConversationStatusLine(
|
|
209
189
|
width: number,
|
|
210
190
|
segments: readonly ConversationStatusSegment[],
|
|
@@ -1,10 +1,12 @@
|
|
|
1
1
|
import { type Line, type Cell, createStyledCell } from '../types/grid.ts';
|
|
2
2
|
import { UIFactory } from './ui-factory.ts';
|
|
3
3
|
import { getDisplayWidth, padDisplayEnd } from '../utils/terminal-width.ts';
|
|
4
|
+
import { DIFF_TONES } from './ui-primitives.ts';
|
|
4
5
|
|
|
5
6
|
/**
|
|
6
7
|
* renderDiffView - Render a unified diff string as styled Line[].
|
|
7
|
-
* '+' lines in green, '-' lines in red, '@@' hunks in
|
|
8
|
+
* '+' lines in green, '-' lines in red, '@@' hunks in blue (DIFF_TONES.hunk,
|
|
9
|
+
* shared with diff-panel.ts and git-panel.ts's inline diff).
|
|
8
10
|
*/
|
|
9
11
|
export function renderDiffView(diffText: string, width: number, filename?: string): Line[] {
|
|
10
12
|
const lines: Line[] = [];
|
|
@@ -34,7 +36,7 @@ export function renderDiffView(diffText: string, width: number, filename?: strin
|
|
|
34
36
|
oldLineNo = parseInt(hunkMatch[1], 10) - 1;
|
|
35
37
|
newLineNo = parseInt(hunkMatch[2], 10) - 1;
|
|
36
38
|
}
|
|
37
|
-
lines.push(makeStyledLine(raw, width,
|
|
39
|
+
lines.push(makeStyledLine(raw, width, DIFF_TONES.hunk, '#0f1f1f', false));
|
|
38
40
|
continue;
|
|
39
41
|
}
|
|
40
42
|
|
|
@@ -49,7 +51,7 @@ export function renderDiffView(diffText: string, width: number, filename?: strin
|
|
|
49
51
|
newLineNo++;
|
|
50
52
|
const lineLabel = `${String(newLineNo).padStart(4)} `;
|
|
51
53
|
const content = raw.slice(1);
|
|
52
|
-
lines.push(makeGutterLine('+', lineLabel, content, width,
|
|
54
|
+
lines.push(makeGutterLine('+', lineLabel, content, width, DIFF_TONES.add, '#0a1a0a'));
|
|
53
55
|
continue;
|
|
54
56
|
}
|
|
55
57
|
|
|
@@ -58,7 +60,7 @@ export function renderDiffView(diffText: string, width: number, filename?: strin
|
|
|
58
60
|
oldLineNo++;
|
|
59
61
|
const lineLabel = `${String(oldLineNo).padStart(4)} `;
|
|
60
62
|
const content = raw.slice(1);
|
|
61
|
-
lines.push(makeGutterLine('-', lineLabel, content, width,
|
|
63
|
+
lines.push(makeGutterLine('-', lineLabel, content, width, DIFF_TONES.del, '#1a0a0a'));
|
|
62
64
|
continue;
|
|
63
65
|
}
|
|
64
66
|
|
|
@@ -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
|
+
}
|