@pellux/goodvibes-tui 0.26.0 → 0.28.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 +40 -0
- package/README.md +12 -7
- package/docs/foundation-artifacts/operator-contract.json +4 -1
- package/package.json +2 -2
- package/src/cli/bundle-command.ts +2 -1
- package/src/cli/service-posture.ts +2 -1
- package/src/core/context-usage.ts +53 -0
- package/src/core/conversation-rendering.ts +2 -2
- package/src/core/conversation.ts +53 -8
- package/src/core/session-recovery.ts +26 -18
- package/src/core/system-message-router.ts +42 -26
- package/src/core/turn-event-wiring.ts +13 -16
- package/src/daemon/handlers/calendar/caldav-client.ts +2 -1
- package/src/daemon/handlers/inbox/index.ts +2 -1
- package/src/daemon/handlers/inbox/poller.ts +2 -1
- package/src/daemon/handlers/inbox/providers/discord.ts +2 -1
- package/src/daemon/handlers/inbox/providers/email.ts +2 -1
- package/src/daemon/handlers/inbox/providers/route-util.ts +2 -1
- package/src/daemon/handlers/inbox/providers/slack.ts +2 -1
- package/src/daemon/handlers/triage/integration.ts +2 -1
- package/src/daemon/handlers/triage/pipeline.ts +2 -1
- package/src/daemon/handlers/triage/tagger/discord.ts +2 -1
- package/src/daemon/handlers/triage/tagger/imap.ts +4 -3
- package/src/export/gist-uploader.ts +3 -1
- package/src/input/command-registry.ts +1 -0
- package/src/input/commands/cloudflare-runtime.ts +2 -1
- package/src/input/commands/eval.ts +4 -3
- package/src/input/commands/guidance-runtime.ts +2 -4
- package/src/input/commands/health-runtime.ts +13 -7
- package/src/input/commands/knowledge.ts +2 -1
- package/src/input/commands/runtime-services.ts +40 -10
- package/src/input/handler-command-route.ts +1 -1
- package/src/input/handler-feed-routes.ts +61 -4
- package/src/input/handler-feed.ts +13 -0
- package/src/input/handler-interactions.ts +2 -1
- package/src/input/handler-modal-routes.ts +2 -1
- package/src/input/handler-onboarding-cloudflare.ts +2 -1
- package/src/input/handler-onboarding.ts +8 -8
- package/src/input/handler-picker-routes.ts +18 -13
- package/src/input/handler-ui-state.ts +5 -1
- package/src/input/keybindings.ts +5 -0
- package/src/input/model-picker.ts +5 -0
- package/src/input/panel-integration-actions.ts +10 -0
- package/src/input/tts-settings-actions.ts +2 -1
- package/src/main.ts +52 -60
- package/src/panels/agent-inspector-panel.ts +90 -15
- package/src/panels/agent-inspector-shared.ts +3 -3
- package/src/panels/agent-logs-panel.ts +149 -72
- package/src/panels/approval-panel.ts +146 -95
- package/src/panels/automation-control-panel.ts +24 -1
- package/src/panels/base-panel.ts +28 -3
- package/src/panels/builtin/agent.ts +3 -1
- package/src/panels/builtin/development.ts +2 -1
- package/src/panels/builtin/session.ts +1 -1
- package/src/panels/cockpit-panel.ts +24 -5
- package/src/panels/cockpit-read-model.ts +2 -1
- package/src/panels/communication-panel.ts +108 -43
- package/src/panels/context-visualizer-panel.ts +49 -15
- package/src/panels/control-plane-panel.ts +27 -1
- package/src/panels/cost-tracker-panel.ts +87 -65
- package/src/panels/debug-panel.ts +61 -37
- package/src/panels/diff-panel.ts +59 -30
- package/src/panels/docs-panel.ts +30 -24
- package/src/panels/eval-panel.ts +130 -81
- package/src/panels/expandable-list-panel.ts +189 -0
- package/src/panels/file-explorer-panel.ts +42 -42
- package/src/panels/file-preview-panel.ts +11 -3
- package/src/panels/forensics-panel.ts +27 -13
- package/src/panels/git-panel.ts +65 -84
- package/src/panels/hooks-panel.ts +36 -2
- package/src/panels/incident-review-panel.ts +31 -10
- package/src/panels/intelligence-panel.ts +152 -71
- package/src/panels/knowledge-graph-panel.ts +89 -27
- package/src/panels/local-auth-panel.ts +17 -11
- package/src/panels/marketplace-panel.ts +33 -13
- package/src/panels/memory-panel.ts +7 -5
- package/src/panels/ops-control-panel.ts +69 -7
- package/src/panels/ops-strategy-panel.ts +61 -43
- package/src/panels/orchestration-panel.ts +34 -4
- package/src/panels/panel-list-panel.ts +33 -8
- package/src/panels/panel-manager.ts +23 -4
- package/src/panels/plan-dashboard-panel.ts +183 -66
- package/src/panels/plugins-panel.ts +48 -10
- package/src/panels/policy-panel.ts +60 -23
- package/src/panels/polish-core.ts +157 -0
- package/src/panels/polish-tables.ts +258 -0
- package/src/panels/polish.ts +44 -145
- package/src/panels/project-planning-panel.ts +27 -4
- package/src/panels/provider-accounts-panel.ts +55 -18
- package/src/panels/provider-health-panel.ts +118 -87
- package/src/panels/provider-stats-panel.ts +47 -22
- package/src/panels/qr-panel.ts +23 -11
- package/src/panels/remote-panel.ts +12 -5
- package/src/panels/routes-panel.ts +27 -5
- package/src/panels/sandbox-panel.ts +62 -27
- package/src/panels/schedule-panel.ts +44 -24
- package/src/panels/scrollable-list-panel.ts +124 -10
- package/src/panels/security-panel.ts +72 -20
- package/src/panels/services-panel.ts +68 -22
- package/src/panels/session-browser-panel.ts +42 -26
- package/src/panels/session-maintenance.ts +2 -2
- package/src/panels/settings-sync-panel.ts +30 -15
- package/src/panels/skills-panel.ts +2 -1
- package/src/panels/subscription-panel.ts +21 -3
- package/src/panels/symbol-outline-panel.ts +40 -47
- package/src/panels/system-messages-panel.ts +60 -27
- package/src/panels/tasks-panel.ts +36 -14
- package/src/panels/thinking-panel.ts +32 -36
- package/src/panels/token-budget-panel.ts +80 -53
- package/src/panels/tool-inspector-panel.ts +37 -39
- package/src/panels/types.ts +25 -0
- package/src/panels/watchers-panel.ts +25 -5
- package/src/panels/work-plan-panel.ts +127 -35
- package/src/panels/worktree-panel.ts +65 -20
- package/src/panels/wrfc-panel-format.ts +133 -0
- package/src/panels/wrfc-panel.ts +53 -147
- package/src/renderer/agent-detail-modal.ts +2 -2
- package/src/renderer/compaction-preview.ts +2 -1
- package/src/renderer/compositor.ts +46 -43
- package/src/renderer/modal-utils.ts +0 -10
- package/src/renderer/model-picker-overlay.ts +9 -4
- package/src/renderer/model-workspace.ts +2 -3
- package/src/renderer/panel-composite.ts +7 -20
- package/src/renderer/panel-workspace-bar.ts +14 -8
- package/src/renderer/process-modal.ts +2 -2
- package/src/renderer/progress.ts +3 -2
- package/src/renderer/tab-strip.ts +148 -34
- package/src/renderer/ui-factory.ts +4 -6
- package/src/runtime/bootstrap-command-context.ts +3 -0
- package/src/runtime/bootstrap-command-parts.ts +3 -1
- package/src/runtime/bootstrap-core.ts +31 -31
- package/src/runtime/bootstrap-shell.ts +1 -0
- package/src/runtime/onboarding/apply.ts +4 -3
- package/src/runtime/onboarding/snapshot.ts +4 -3
- package/src/runtime/process-lifecycle.ts +195 -0
- package/src/runtime/services.ts +4 -1
- package/src/runtime/ui/model-picker/health-enrichment.ts +3 -0
- package/src/runtime/ui/model-picker/types.ts +4 -0
- package/src/runtime/wrfc-persistence.ts +20 -5
- package/src/shell/ui-openers.ts +3 -2
- package/src/utils/format-duration.ts +55 -0
- package/src/utils/format-number.ts +71 -0
- package/src/verification/live-verifier.ts +4 -3
- package/src/version.ts +1 -1
- package/src/core/context-auto-compact.ts +0 -110
- package/src/panels/panel-picker.ts +0 -106
- package/src/renderer/panel-picker-overlay.ts +0 -202
- package/src/renderer/panel-tab-bar.ts +0 -69
|
@@ -1,21 +1,26 @@
|
|
|
1
1
|
import type { Line } from '../types/grid.ts';
|
|
2
2
|
import type { WorkspaceTab } from '../panels/panel-manager.ts';
|
|
3
|
-
import { renderTabStrip } from './tab-strip.ts';
|
|
3
|
+
import { renderTabStrip, type TabHitRegion } from './tab-strip.ts';
|
|
4
|
+
import { UI_TONES } from './ui-primitives.ts';
|
|
4
5
|
|
|
5
|
-
|
|
6
|
-
const
|
|
7
|
-
const
|
|
8
|
-
const
|
|
9
|
-
const
|
|
10
|
-
const
|
|
6
|
+
// Theme tokens (no raw hex) — keeps the bar in sync with the shared palette.
|
|
7
|
+
const ACTIVE_FG = UI_TONES.fg.primary; // selected tab text
|
|
8
|
+
const ACTIVE_BG = UI_TONES.bg.selected; // selected tab fill (stronger contrast)
|
|
9
|
+
const INACTIVE_FG = UI_TONES.fg.muted; // unselected tabs
|
|
10
|
+
const SEPARATOR_FG = UI_TONES.fg.dim; // │ separators
|
|
11
|
+
const LABEL_FG = UI_TONES.fg.secondary; // PANELS prefix label
|
|
12
|
+
const LABEL_BG = UI_TONES.bg.title; // prefix label background when focused
|
|
13
|
+
const FOCUS_ACCENT_FG = UI_TONES.state.active; // the one focused tab
|
|
11
14
|
|
|
12
15
|
export function renderPanelWorkspaceBar(
|
|
13
16
|
tabs: readonly WorkspaceTab[],
|
|
14
17
|
width: number,
|
|
15
18
|
focused: boolean,
|
|
19
|
+
onLayout?: (regions: readonly TabHitRegion[]) => void,
|
|
16
20
|
): Line {
|
|
17
21
|
return renderTabStrip({
|
|
18
22
|
width,
|
|
23
|
+
onLayout,
|
|
19
24
|
tabs: tabs.map((tab) => ({
|
|
20
25
|
// tab.active = selected in its own pane (drives highlighted background).
|
|
21
26
|
// tab.focused = has keyboard focus (drives brighter text / focus indicator).
|
|
@@ -32,7 +37,8 @@ export function renderPanelWorkspaceBar(
|
|
|
32
37
|
activeBold: focused,
|
|
33
38
|
inactiveFg: INACTIVE_FG,
|
|
34
39
|
separatorFg: SEPARATOR_FG,
|
|
35
|
-
|
|
40
|
+
// Accent the PANELS chip when focused so it ties to the focus border.
|
|
41
|
+
labelFg: focused ? FOCUS_ACCENT_FG : LABEL_FG,
|
|
36
42
|
labelBg: focused ? LABEL_BG : '',
|
|
37
43
|
labelBold: focused,
|
|
38
44
|
overflowFg: SEPARATOR_FG,
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { type Line } from '../types/grid.ts';
|
|
2
2
|
import { ModalFactory } from './modal-factory.ts';
|
|
3
|
-
import {
|
|
3
|
+
import { formatElapsed } from '../utils/format-elapsed.ts';
|
|
4
4
|
import type { ProcessManager } from '@pellux/goodvibes-sdk/platform/tools';
|
|
5
5
|
import type { AgentManager, AgentRecord } from '@pellux/goodvibes-sdk/platform/tools';
|
|
6
6
|
import type { WrfcController } from '@pellux/goodvibes-sdk/platform/agents';
|
|
@@ -623,7 +623,7 @@ export function renderProcessModal(modal: ProcessModal, width: number, viewportH
|
|
|
623
623
|
cancelled: '–',
|
|
624
624
|
}[e.status] ?? '•';
|
|
625
625
|
const typeTag = e.type === 'agent' ? '[agent]' : '[exec]';
|
|
626
|
-
const dur =
|
|
626
|
+
const dur = formatElapsed(e.elapsedMs);
|
|
627
627
|
const statusStr = e.streamSnippet ? `streaming ${dur}` : `${e.status} ${dur}`;
|
|
628
628
|
const suffix = ` ${statusStr}`;
|
|
629
629
|
const treePrefix = e.treePrefix ?? '';
|
package/src/renderer/progress.ts
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { type Line } from '../types/grid.ts';
|
|
2
2
|
import { UIFactory } from './ui-factory.ts';
|
|
3
3
|
import { getDisplayWidth, padDisplayEnd } from '../utils/terminal-width.ts';
|
|
4
|
+
import { abbreviateCount } from '../utils/format-number.ts';
|
|
4
5
|
|
|
5
6
|
// Rich spinner frames (used by progress indicators)
|
|
6
7
|
export const SPINNER_FRAMES = ['⠋', '⠙', '⠹', '⠸', '⠼', '⠴', '⠦', '⠧', '⠇', '⠏'];
|
|
@@ -51,8 +52,8 @@ export function renderTokenBar(
|
|
|
51
52
|
const lines: Line[] = [];
|
|
52
53
|
|
|
53
54
|
// Stats row
|
|
54
|
-
const usedK = used
|
|
55
|
-
const maxK = max
|
|
55
|
+
const usedK = abbreviateCount(used, { noM: true });
|
|
56
|
+
const maxK = abbreviateCount(max, { noM: true });
|
|
56
57
|
const pct = max > 0 ? Math.min(100, Math.round((used / max) * 100)) : 0;
|
|
57
58
|
const toolStr = toolCount > 0 ? ` tools:${toolCount}` : '';
|
|
58
59
|
const statsText = ` ${model} in:${usedK}/${maxK} (${pct}%)${toolStr}`;
|
|
@@ -21,46 +21,148 @@ export interface TabStripStyle {
|
|
|
21
21
|
readonly trailingFg?: string;
|
|
22
22
|
}
|
|
23
23
|
|
|
24
|
+
/** Absolute column range a rendered tab occupies, for click hit-testing. */
|
|
25
|
+
export interface TabHitRegion {
|
|
26
|
+
/** Index into the original `tabs` array. */
|
|
27
|
+
readonly index: number;
|
|
28
|
+
/** First column (inclusive) the tab occupies on the line. */
|
|
29
|
+
readonly startCol: number;
|
|
30
|
+
/** Last column (exclusive) the tab occupies on the line. */
|
|
31
|
+
readonly endCol: number;
|
|
32
|
+
}
|
|
33
|
+
|
|
24
34
|
export interface TabStripConfig {
|
|
25
35
|
readonly width: number;
|
|
26
36
|
readonly tabs: readonly TabStripItem[];
|
|
27
37
|
readonly style: TabStripStyle;
|
|
28
38
|
readonly prefixLabel?: string;
|
|
29
39
|
readonly suffixLabel?: string;
|
|
40
|
+
/** Right-side overflow glyph (more tabs after the window). Default '›'. */
|
|
30
41
|
readonly overflowIndicator?: string;
|
|
42
|
+
/** Left-side overflow glyph (more tabs before the window). Default '‹'. */
|
|
43
|
+
readonly overflowIndicatorLeft?: string;
|
|
44
|
+
/**
|
|
45
|
+
* Optional callback invoked with the absolute column ranges of each rendered
|
|
46
|
+
* tab. Used by the compositor to hit-test mouse clicks onto tabs. Additive —
|
|
47
|
+
* the function still returns a `Line` so existing callers are unaffected.
|
|
48
|
+
*/
|
|
49
|
+
readonly onLayout?: (regions: readonly TabHitRegion[]) => void;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
const SEPARATOR = '│';
|
|
53
|
+
|
|
54
|
+
interface WindowResult {
|
|
55
|
+
segments: StyledPanelSegment[];
|
|
56
|
+
/** Per-tab column ranges relative to the start of the tab block. */
|
|
57
|
+
regions: Array<{ index: number; startCol: number; endCol: number }>;
|
|
58
|
+
overflowLeft: boolean;
|
|
59
|
+
overflowRight: boolean;
|
|
31
60
|
}
|
|
32
61
|
|
|
62
|
+
/**
|
|
63
|
+
* Build the visible tab segments, windowed so the ACTIVE tab is always shown.
|
|
64
|
+
*
|
|
65
|
+
* Tabs are laid out left-to-right, but when they don't all fit we keep a
|
|
66
|
+
* contiguous window that contains the active tab and grow it outward (biased
|
|
67
|
+
* toward following tabs) until the budget is exhausted. Left/right overflow is
|
|
68
|
+
* reported so the caller can render `‹`/`›` indicators.
|
|
69
|
+
*/
|
|
33
70
|
function makeSegments(
|
|
34
71
|
tabs: readonly TabStripItem[],
|
|
35
72
|
style: TabStripStyle,
|
|
36
73
|
maxWidth: number,
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
74
|
+
indicatorWidth: number,
|
|
75
|
+
): WindowResult {
|
|
76
|
+
const empty: WindowResult = { segments: [], regions: [], overflowLeft: false, overflowRight: false };
|
|
77
|
+
if (tabs.length === 0 || maxWidth <= 0) return empty;
|
|
78
|
+
|
|
79
|
+
const sepWidth = getDisplayWidth(SEPARATOR);
|
|
80
|
+
// Per-tab entry width: leading separator + (bracketed if active) label + trailing space.
|
|
81
|
+
const labelText = (tab: TabStripItem) => (tab.active ? `[${tab.label}]` : tab.label);
|
|
82
|
+
const entryWidths = tabs.map((tab) => sepWidth + getDisplayWidth(labelText(tab)) + 1);
|
|
83
|
+
|
|
84
|
+
const activeIndex = Math.max(0, tabs.findIndex((t) => t.active));
|
|
85
|
+
const n = tabs.length;
|
|
86
|
+
const closing = sepWidth; // trailing │ after the last visible tab
|
|
87
|
+
const totalAll = entryWidths.reduce((a, b) => a + b, 0) + closing;
|
|
88
|
+
|
|
89
|
+
let start = activeIndex;
|
|
90
|
+
let end = activeIndex; // inclusive
|
|
91
|
+
let used = entryWidths[activeIndex] + closing;
|
|
92
|
+
|
|
93
|
+
if (totalAll <= maxWidth) {
|
|
94
|
+
// Everything fits — show all tabs, no windowing.
|
|
95
|
+
start = 0;
|
|
96
|
+
end = n - 1;
|
|
97
|
+
} else {
|
|
98
|
+
// Need windowing. Reserve room for indicators on whichever side still has
|
|
99
|
+
// hidden tabs; the reserve is recomputed each step so reaching an edge
|
|
100
|
+
// frees its indicator column.
|
|
101
|
+
const budget = maxWidth;
|
|
102
|
+
let leftDone = start === 0;
|
|
103
|
+
let rightDone = end === n - 1;
|
|
104
|
+
let preferRight = true;
|
|
105
|
+
|
|
106
|
+
const fits = (extra: number, nextStart: number, nextEnd: number) => {
|
|
107
|
+
const leftRes = nextStart > 0 ? indicatorWidth : 0;
|
|
108
|
+
const rightRes = nextEnd < n - 1 ? indicatorWidth : 0;
|
|
109
|
+
return used + extra + leftRes + rightRes <= budget;
|
|
110
|
+
};
|
|
111
|
+
|
|
112
|
+
while (!leftDone || !rightDone) {
|
|
113
|
+
const goRight = !rightDone && (preferRight || leftDone);
|
|
114
|
+
if (goRight) {
|
|
115
|
+
const ne = end + 1;
|
|
116
|
+
if (fits(entryWidths[ne], start, ne)) {
|
|
117
|
+
end = ne;
|
|
118
|
+
used += entryWidths[ne];
|
|
119
|
+
if (end === n - 1) rightDone = true;
|
|
120
|
+
preferRight = false;
|
|
121
|
+
} else {
|
|
122
|
+
rightDone = true;
|
|
123
|
+
}
|
|
124
|
+
} else if (!leftDone) {
|
|
125
|
+
const ns = start - 1;
|
|
126
|
+
if (fits(entryWidths[ns], ns, end)) {
|
|
127
|
+
start = ns;
|
|
128
|
+
used += entryWidths[ns];
|
|
129
|
+
if (start === 0) leftDone = true;
|
|
130
|
+
preferRight = true;
|
|
131
|
+
} else {
|
|
132
|
+
leftDone = true;
|
|
133
|
+
}
|
|
134
|
+
} else {
|
|
135
|
+
break;
|
|
136
|
+
}
|
|
49
137
|
}
|
|
50
|
-
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
const segments: StyledPanelSegment[] = [];
|
|
141
|
+
const regions: Array<{ index: number; startCol: number; endCol: number }> = [];
|
|
142
|
+
let col = 0;
|
|
143
|
+
for (let i = start; i <= end; i++) {
|
|
144
|
+
const tab = tabs[i];
|
|
145
|
+
const text = labelText(tab);
|
|
146
|
+
const entryStart = col;
|
|
147
|
+
segments.push({ text: SEPARATOR, fg: style.separatorFg });
|
|
51
148
|
segments.push({
|
|
52
|
-
text: `${
|
|
149
|
+
text: `${text} `,
|
|
53
150
|
fg: tab.active ? style.activeFg : style.inactiveFg,
|
|
54
151
|
bg: tab.active ? style.activeBg : style.inactiveBg,
|
|
55
152
|
bold: tab.active ? (style.activeBold ?? false) : false,
|
|
56
153
|
});
|
|
57
|
-
|
|
154
|
+
col += entryWidths[i];
|
|
155
|
+
regions.push({ index: i, startCol: entryStart, endCol: col });
|
|
58
156
|
}
|
|
157
|
+
// Closing separator.
|
|
158
|
+
segments.push({ text: SEPARATOR, fg: style.separatorFg });
|
|
59
159
|
|
|
60
|
-
|
|
61
|
-
segments
|
|
62
|
-
|
|
63
|
-
|
|
160
|
+
return {
|
|
161
|
+
segments,
|
|
162
|
+
regions,
|
|
163
|
+
overflowLeft: start > 0,
|
|
164
|
+
overflowRight: end < n - 1,
|
|
165
|
+
};
|
|
64
166
|
}
|
|
65
167
|
|
|
66
168
|
export function renderTabStrip(config: TabStripConfig): Line {
|
|
@@ -70,18 +172,18 @@ export function renderTabStrip(config: TabStripConfig): Line {
|
|
|
70
172
|
style,
|
|
71
173
|
prefixLabel,
|
|
72
174
|
suffixLabel,
|
|
73
|
-
|
|
175
|
+
overflowIndicator = '›',
|
|
176
|
+
overflowIndicatorLeft = '‹',
|
|
177
|
+
onLayout,
|
|
74
178
|
} = config;
|
|
75
179
|
|
|
180
|
+
const prefixWidth = prefixLabel ? getDisplayWidth(prefixLabel) + 1 : 0;
|
|
76
181
|
const reservedSuffixWidth = suffixLabel ? getDisplayWidth(suffixLabel) : 0;
|
|
77
|
-
const
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
width
|
|
81
|
-
- (prefixLabel ? getDisplayWidth(prefixLabel) + 1 : 0)
|
|
82
|
-
- (suffixLabel ? reservedSuffixWidth : 0)
|
|
83
|
-
- reservedOverflowWidth,
|
|
182
|
+
const indicatorWidth = Math.max(
|
|
183
|
+
getDisplayWidth(overflowIndicator),
|
|
184
|
+
getDisplayWidth(overflowIndicatorLeft),
|
|
84
185
|
);
|
|
186
|
+
const maxTabWidth = Math.max(0, width - prefixWidth - reservedSuffixWidth);
|
|
85
187
|
|
|
86
188
|
const baseSegments: StyledPanelSegment[] = [];
|
|
87
189
|
if (prefixLabel) {
|
|
@@ -94,15 +196,27 @@ export function renderTabStrip(config: TabStripConfig): Line {
|
|
|
94
196
|
baseSegments.push({ text: ' ', fg: style.separatorFg });
|
|
95
197
|
}
|
|
96
198
|
|
|
97
|
-
const { segments: tabSegments,
|
|
199
|
+
const { segments: tabSegments, regions, overflowLeft, overflowRight } =
|
|
200
|
+
makeSegments(tabs, style, maxTabWidth, indicatorWidth);
|
|
201
|
+
|
|
202
|
+
const leftSegments: StyledPanelSegment[] = [];
|
|
203
|
+
if (overflowLeft) {
|
|
204
|
+
leftSegments.push({ text: overflowIndicatorLeft, fg: style.overflowFg ?? style.separatorFg });
|
|
205
|
+
}
|
|
206
|
+
|
|
98
207
|
const suffixSegments: StyledPanelSegment[] = [];
|
|
99
|
-
if (
|
|
208
|
+
if (overflowRight) {
|
|
209
|
+
suffixSegments.push({ text: overflowIndicator, fg: style.overflowFg ?? style.separatorFg });
|
|
210
|
+
}
|
|
100
211
|
if (suffixLabel) {
|
|
101
|
-
suffixSegments.push({
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
212
|
+
suffixSegments.push({ text: suffixLabel, fg: style.trailingFg ?? style.inactiveFg });
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
// Report absolute hit regions: prefix + optional left indicator, then tabs.
|
|
216
|
+
if (onLayout) {
|
|
217
|
+
const offset = prefixWidth + (overflowLeft ? getDisplayWidth(overflowIndicatorLeft) : 0);
|
|
218
|
+
onLayout(regions.map((r) => ({ index: r.index, startCol: offset + r.startCol, endCol: offset + r.endCol })));
|
|
105
219
|
}
|
|
106
220
|
|
|
107
|
-
return buildStyledPanelLine(width, [...baseSegments, ...tabSegments, ...suffixSegments]);
|
|
221
|
+
return buildStyledPanelLine(width, [...baseSegments, ...leftSegments, ...tabSegments, ...suffixSegments]);
|
|
108
222
|
}
|
|
@@ -6,6 +6,8 @@ import type { GitHeaderInfo } from './git-status.ts';
|
|
|
6
6
|
import { renderConversationFragment, renderConversationStatusLine, type ConversationStatusSegment } from './conversation-surface.ts';
|
|
7
7
|
import { GLYPHS } from './ui-primitives.ts';
|
|
8
8
|
import { formatElapsed } from '../utils/format-elapsed.ts';
|
|
9
|
+
import { abbreviateCount } from '../utils/format-number.ts';
|
|
10
|
+
import { computeContextUsage } from '../core/context-usage.ts';
|
|
9
11
|
|
|
10
12
|
/** Number of frames before the animated gradient completes one full cycle. */
|
|
11
13
|
const GRADIENT_CYCLE_FRAMES = 50;
|
|
@@ -30,11 +32,7 @@ function buildGitSegment(gitInfo: GitHeaderInfo): { text: string; width: number
|
|
|
30
32
|
|
|
31
33
|
/** Format a number: up to 999, then 1.0k, 1.0M, 1.0B, 1.0T */
|
|
32
34
|
function fmtNum(n: number): string {
|
|
33
|
-
|
|
34
|
-
if (n < 1_000_000) return (n / 1000).toFixed(1) + 'k';
|
|
35
|
-
if (n < 1_000_000_000) return (n / 1_000_000).toFixed(1) + 'M';
|
|
36
|
-
if (n < 1_000_000_000_000) return (n / 1_000_000_000).toFixed(1) + 'B';
|
|
37
|
-
return (n / 1_000_000_000_000).toFixed(1) + 'T';
|
|
35
|
+
return abbreviateCount(n, { bSuffix: true });
|
|
38
36
|
}
|
|
39
37
|
|
|
40
38
|
/**
|
|
@@ -312,7 +310,7 @@ export class UIFactory {
|
|
|
312
310
|
const label = ' Context Usage: ';
|
|
313
311
|
const suffix = ` [ ${fmtNum(ctxTokens)} / ${fmtNum(contextWindow)} ]`;
|
|
314
312
|
const barWidth = Math.max(10, Math.min(30, width - getDisplayWidth(label) - getDisplayWidth(suffix) - 8));
|
|
315
|
-
const ctxPct =
|
|
313
|
+
const ctxPct = computeContextUsage(ctxTokens, contextWindow).clampedRatio;
|
|
316
314
|
// Clamp threshold to [0..1]; undefined/0 means no threshold marker.
|
|
317
315
|
const thresholdFraction = (compactThreshold !== undefined && compactThreshold > 0)
|
|
318
316
|
? Math.min(1, compactThreshold)
|
|
@@ -112,6 +112,7 @@ export type CreateBootstrapCommandContextOptions = {
|
|
|
112
112
|
activatePlan: (planId: string, task: string) => void;
|
|
113
113
|
completeModelSelectionSideEffect?: () => void;
|
|
114
114
|
sessionLineageTracker?: import('@pellux/goodvibes-sdk/platform/core').SessionLineageTracker;
|
|
115
|
+
wrfcController?: import('@pellux/goodvibes-sdk/platform/agents').WrfcController;
|
|
115
116
|
componentHealthMonitor: import('@/runtime/index.ts').ComponentHealthMonitor;
|
|
116
117
|
};
|
|
117
118
|
|
|
@@ -164,6 +165,7 @@ export function createBootstrapCommandContext(
|
|
|
164
165
|
webhookNotifier,
|
|
165
166
|
sessionMemoryStore,
|
|
166
167
|
sessionLineageTracker,
|
|
168
|
+
wrfcController,
|
|
167
169
|
changeTracker,
|
|
168
170
|
planManager,
|
|
169
171
|
adaptivePlanner,
|
|
@@ -219,6 +221,7 @@ export function createBootstrapCommandContext(
|
|
|
219
221
|
sessionManager,
|
|
220
222
|
sessionMemoryStore,
|
|
221
223
|
sessionLineageTracker,
|
|
224
|
+
wrfcController,
|
|
222
225
|
changeTracker,
|
|
223
226
|
});
|
|
224
227
|
const provider = createBootstrapCommandProviderSection({
|
|
@@ -107,6 +107,7 @@ export interface BootstrapCommandSectionOptions {
|
|
|
107
107
|
readonly webhookNotifier?: import('@pellux/goodvibes-sdk/platform/integrations').WebhookNotifier;
|
|
108
108
|
readonly sessionMemoryStore?: import('@pellux/goodvibes-sdk/platform/core').SessionMemoryStore;
|
|
109
109
|
readonly sessionLineageTracker?: import('@pellux/goodvibes-sdk/platform/core').SessionLineageTracker;
|
|
110
|
+
readonly wrfcController?: import('@pellux/goodvibes-sdk/platform/agents').WrfcController;
|
|
110
111
|
readonly changeTracker?: import('@pellux/goodvibes-sdk/platform/sessions').SessionChangeTracker;
|
|
111
112
|
readonly agentManager?: ShellAgentManagerService;
|
|
112
113
|
readonly modeManager?: ShellModeManagerService;
|
|
@@ -299,7 +300,7 @@ export function createBootstrapCommandActions(
|
|
|
299
300
|
export function createBootstrapCommandSessionSection(
|
|
300
301
|
options: Pick<
|
|
301
302
|
BootstrapCommandSectionOptions,
|
|
302
|
-
'conversation' | 'runtime' | 'sessionManager' | 'sessionMemoryStore' | 'sessionLineageTracker' | 'changeTracker'
|
|
303
|
+
'conversation' | 'runtime' | 'sessionManager' | 'sessionMemoryStore' | 'sessionLineageTracker' | 'wrfcController' | 'changeTracker'
|
|
303
304
|
>,
|
|
304
305
|
): BootstrapCommandSessionSection {
|
|
305
306
|
return {
|
|
@@ -308,6 +309,7 @@ export function createBootstrapCommandSessionSection(
|
|
|
308
309
|
sessionManager: options.sessionManager,
|
|
309
310
|
sessionMemoryStore: options.sessionMemoryStore,
|
|
310
311
|
sessionLineageTracker: options.sessionLineageTracker,
|
|
312
|
+
wrfcController: options.wrfcController,
|
|
311
313
|
changeTracker: options.changeTracker,
|
|
312
314
|
};
|
|
313
315
|
}
|
|
@@ -399,50 +399,50 @@ export async function initializeBootstrapCore(
|
|
|
399
399
|
});
|
|
400
400
|
|
|
401
401
|
const renderRequestRef = { value: (): void => {} };
|
|
402
|
-
// R1: Coalescing render scheduler — collapses N
|
|
403
|
-
//
|
|
402
|
+
// R1: Coalescing render scheduler — collapses N requestRender() calls into 1
|
|
403
|
+
// and enforces a 16ms minimum interval to cap repaints at ~60fps.
|
|
404
|
+
//
|
|
405
|
+
// renderScheduled stays set for the ENTIRE window (until run() executes), so
|
|
406
|
+
// requestRender() calls that arrive on later event-loop ticks within the same
|
|
407
|
+
// 16ms window are coalesced into the one already-pending tail render instead
|
|
408
|
+
// of each queuing their own setTimeout. (The streaming hot path drives its own
|
|
409
|
+
// direct repaints and does not flow through this scheduler.)
|
|
404
410
|
let renderScheduled = false;
|
|
405
411
|
let lastRenderTime = 0;
|
|
406
412
|
const RENDER_INTERVAL_MS = 16;
|
|
413
|
+
// run() performs the actual render. It clears renderScheduled FIRST — even if
|
|
414
|
+
// the render callback throws — otherwise a single render exception would wedge
|
|
415
|
+
// the entire TUI (no future requestRender() call would schedule anything). The
|
|
416
|
+
// renderer is expected to surface failures via its own error path; we log at
|
|
417
|
+
// error so the next requestRender() can still reschedule.
|
|
418
|
+
const run = (): void => {
|
|
419
|
+
renderScheduled = false;
|
|
420
|
+
lastRenderTime = Date.now();
|
|
421
|
+
try {
|
|
422
|
+
renderRequestRef.value();
|
|
423
|
+
} catch (err) {
|
|
424
|
+
logger.error('Render threw; next requestRender will reschedule', { error: String(err) });
|
|
425
|
+
}
|
|
426
|
+
};
|
|
407
427
|
const requestRender = (): void => {
|
|
408
428
|
if (renderScheduled) return;
|
|
409
429
|
renderScheduled = true;
|
|
410
430
|
setImmediate(() => {
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
if (elapsed < RENDER_INTERVAL_MS) {
|
|
419
|
-
// Too soon — debounce to the tail of the current 16ms window
|
|
420
|
-
const delay = RENDER_INTERVAL_MS - elapsed;
|
|
421
|
-
setTimeout(() => {
|
|
422
|
-
try {
|
|
423
|
-
lastRenderTime = Date.now();
|
|
424
|
-
renderRequestRef.value();
|
|
425
|
-
} catch (err) {
|
|
426
|
-
// Throttled-render error: swallow but log at error so the next
|
|
427
|
-
// requestRender() call can still schedule. The renderer itself
|
|
428
|
-
// is expected to surface failures via its own error path.
|
|
429
|
-
logger.error('Throttled render threw; next requestRender will reschedule', { error: String(err) });
|
|
430
|
-
}
|
|
431
|
-
}, delay);
|
|
432
|
-
} else {
|
|
433
|
-
lastRenderTime = now;
|
|
434
|
-
renderRequestRef.value();
|
|
435
|
-
}
|
|
436
|
-
} catch (err) {
|
|
437
|
-
logger.error('Immediate render threw; next requestRender will reschedule', { error: String(err) });
|
|
431
|
+
const elapsed = Date.now() - lastRenderTime;
|
|
432
|
+
if (elapsed < RENDER_INTERVAL_MS) {
|
|
433
|
+
// Too soon — debounce to the tail of the current 16ms window. The flag
|
|
434
|
+
// stays set until run() fires, so window-local requests coalesce here.
|
|
435
|
+
setTimeout(run, RENDER_INTERVAL_MS - elapsed);
|
|
436
|
+
} else {
|
|
437
|
+
run();
|
|
438
438
|
}
|
|
439
439
|
});
|
|
440
440
|
};
|
|
441
441
|
const permissionPromptRef = {
|
|
442
442
|
requestPermission: (async () => ({ approved: false, remember: false })) as PermissionRequestHandler,
|
|
443
443
|
};
|
|
444
|
-
|
|
445
|
-
|
|
444
|
+
approvalBroker.start().catch((err) => logger.warn('approval broker start failed at bootstrap', { err }));
|
|
445
|
+
sharedSessionBroker.start().catch((err) => logger.warn('shared session broker start failed at bootstrap', { err }));
|
|
446
446
|
const runtimeSessionIdRef = { value: userSessionId };
|
|
447
447
|
const wrfcBuffer = new WrfcPreRouterBuffer();
|
|
448
448
|
// Smart ref: setting .value auto-flushes the pre-router buffer so events
|
|
@@ -240,6 +240,7 @@ export function createBootstrapShell(options: BootstrapShellOptions): BootstrapS
|
|
|
240
240
|
webhookNotifier: services.webhookNotifier,
|
|
241
241
|
sessionMemoryStore: services.sessionMemoryStore,
|
|
242
242
|
sessionLineageTracker: services.sessionLineageTracker,
|
|
243
|
+
wrfcController: services.wrfcController,
|
|
243
244
|
changeTracker: services.sessionChangeTracker,
|
|
244
245
|
planManager: services.planManager,
|
|
245
246
|
adaptivePlanner: services.adaptivePlanner,
|
|
@@ -17,6 +17,7 @@ import type {
|
|
|
17
17
|
OnboardingApplyRequest,
|
|
18
18
|
OnboardingApplyResult,
|
|
19
19
|
} from './types.ts';
|
|
20
|
+
import { summarizeError } from '@pellux/goodvibes-sdk/platform/utils';
|
|
20
21
|
|
|
21
22
|
function getNow(deps: Pick<OnboardingApplyDependencies, 'clock'>): number {
|
|
22
23
|
return deps.clock?.() ?? Date.now();
|
|
@@ -116,7 +117,7 @@ async function runRollbacks(rollbacks: readonly RollbackAction[]): Promise<reado
|
|
|
116
117
|
try {
|
|
117
118
|
await rollback();
|
|
118
119
|
} catch (error) {
|
|
119
|
-
errors.push(
|
|
120
|
+
errors.push(summarizeError(error));
|
|
120
121
|
}
|
|
121
122
|
}
|
|
122
123
|
return errors;
|
|
@@ -523,7 +524,7 @@ function prevalidateApplyRequest(
|
|
|
523
524
|
} catch (error) {
|
|
524
525
|
errors.push({
|
|
525
526
|
kind: operation.kind,
|
|
526
|
-
message:
|
|
527
|
+
message: summarizeError(error),
|
|
527
528
|
});
|
|
528
529
|
}
|
|
529
530
|
}
|
|
@@ -595,7 +596,7 @@ export async function applyOnboardingRequest(
|
|
|
595
596
|
errors.push({
|
|
596
597
|
kind: operation.kind,
|
|
597
598
|
message: [
|
|
598
|
-
|
|
599
|
+
summarizeError(error),
|
|
599
600
|
...rollbackErrors.map((rollbackError) => `rollback: ${rollbackError}`),
|
|
600
601
|
].join('; '),
|
|
601
602
|
});
|
|
@@ -14,6 +14,7 @@ import type {
|
|
|
14
14
|
OnboardingSnapshotState,
|
|
15
15
|
OnboardingSurfaceRecord,
|
|
16
16
|
} from './types.ts';
|
|
17
|
+
import { summarizeError } from '@pellux/goodvibes-sdk/platform/utils';
|
|
17
18
|
|
|
18
19
|
function sortUnique(values: readonly string[]): string[] {
|
|
19
20
|
return [...new Set(values)].sort((left, right) => left.localeCompare(right));
|
|
@@ -100,7 +101,7 @@ async function buildServicesSnapshot(
|
|
|
100
101
|
issues: [
|
|
101
102
|
{
|
|
102
103
|
area: 'services',
|
|
103
|
-
message:
|
|
104
|
+
message: summarizeError(error),
|
|
104
105
|
},
|
|
105
106
|
],
|
|
106
107
|
};
|
|
@@ -147,7 +148,7 @@ async function buildServicesSnapshot(
|
|
|
147
148
|
} satisfies OnboardingServiceState,
|
|
148
149
|
issue: {
|
|
149
150
|
area: 'services',
|
|
150
|
-
message: `${name}: ${
|
|
151
|
+
message: `${name}: ${summarizeError(error)}`,
|
|
151
152
|
} satisfies OnboardingSnapshotCollectionIssue,
|
|
152
153
|
};
|
|
153
154
|
}
|
|
@@ -244,7 +245,7 @@ async function loadOptionalSnapshot<T>(
|
|
|
244
245
|
value: fallback,
|
|
245
246
|
issue: {
|
|
246
247
|
area,
|
|
247
|
-
message:
|
|
248
|
+
message: summarizeError(error),
|
|
248
249
|
},
|
|
249
250
|
};
|
|
250
251
|
}
|