@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.
Files changed (148) hide show
  1. package/CHANGELOG.md +40 -0
  2. package/README.md +12 -7
  3. package/docs/foundation-artifacts/operator-contract.json +4 -1
  4. package/package.json +2 -2
  5. package/src/cli/bundle-command.ts +2 -1
  6. package/src/cli/service-posture.ts +2 -1
  7. package/src/core/context-usage.ts +53 -0
  8. package/src/core/conversation-rendering.ts +2 -2
  9. package/src/core/conversation.ts +53 -8
  10. package/src/core/session-recovery.ts +26 -18
  11. package/src/core/system-message-router.ts +42 -26
  12. package/src/core/turn-event-wiring.ts +13 -16
  13. package/src/daemon/handlers/calendar/caldav-client.ts +2 -1
  14. package/src/daemon/handlers/inbox/index.ts +2 -1
  15. package/src/daemon/handlers/inbox/poller.ts +2 -1
  16. package/src/daemon/handlers/inbox/providers/discord.ts +2 -1
  17. package/src/daemon/handlers/inbox/providers/email.ts +2 -1
  18. package/src/daemon/handlers/inbox/providers/route-util.ts +2 -1
  19. package/src/daemon/handlers/inbox/providers/slack.ts +2 -1
  20. package/src/daemon/handlers/triage/integration.ts +2 -1
  21. package/src/daemon/handlers/triage/pipeline.ts +2 -1
  22. package/src/daemon/handlers/triage/tagger/discord.ts +2 -1
  23. package/src/daemon/handlers/triage/tagger/imap.ts +4 -3
  24. package/src/export/gist-uploader.ts +3 -1
  25. package/src/input/command-registry.ts +1 -0
  26. package/src/input/commands/cloudflare-runtime.ts +2 -1
  27. package/src/input/commands/eval.ts +4 -3
  28. package/src/input/commands/guidance-runtime.ts +2 -4
  29. package/src/input/commands/health-runtime.ts +13 -7
  30. package/src/input/commands/knowledge.ts +2 -1
  31. package/src/input/commands/runtime-services.ts +40 -10
  32. package/src/input/handler-command-route.ts +1 -1
  33. package/src/input/handler-feed-routes.ts +61 -4
  34. package/src/input/handler-feed.ts +13 -0
  35. package/src/input/handler-interactions.ts +2 -1
  36. package/src/input/handler-modal-routes.ts +2 -1
  37. package/src/input/handler-onboarding-cloudflare.ts +2 -1
  38. package/src/input/handler-onboarding.ts +8 -8
  39. package/src/input/handler-picker-routes.ts +18 -13
  40. package/src/input/handler-ui-state.ts +5 -1
  41. package/src/input/keybindings.ts +5 -0
  42. package/src/input/model-picker.ts +5 -0
  43. package/src/input/panel-integration-actions.ts +10 -0
  44. package/src/input/tts-settings-actions.ts +2 -1
  45. package/src/main.ts +52 -60
  46. package/src/panels/agent-inspector-panel.ts +90 -15
  47. package/src/panels/agent-inspector-shared.ts +3 -3
  48. package/src/panels/agent-logs-panel.ts +149 -72
  49. package/src/panels/approval-panel.ts +146 -95
  50. package/src/panels/automation-control-panel.ts +24 -1
  51. package/src/panels/base-panel.ts +28 -3
  52. package/src/panels/builtin/agent.ts +3 -1
  53. package/src/panels/builtin/development.ts +2 -1
  54. package/src/panels/builtin/session.ts +1 -1
  55. package/src/panels/cockpit-panel.ts +24 -5
  56. package/src/panels/cockpit-read-model.ts +2 -1
  57. package/src/panels/communication-panel.ts +108 -43
  58. package/src/panels/context-visualizer-panel.ts +49 -15
  59. package/src/panels/control-plane-panel.ts +27 -1
  60. package/src/panels/cost-tracker-panel.ts +87 -65
  61. package/src/panels/debug-panel.ts +61 -37
  62. package/src/panels/diff-panel.ts +59 -30
  63. package/src/panels/docs-panel.ts +30 -24
  64. package/src/panels/eval-panel.ts +130 -81
  65. package/src/panels/expandable-list-panel.ts +189 -0
  66. package/src/panels/file-explorer-panel.ts +42 -42
  67. package/src/panels/file-preview-panel.ts +11 -3
  68. package/src/panels/forensics-panel.ts +27 -13
  69. package/src/panels/git-panel.ts +65 -84
  70. package/src/panels/hooks-panel.ts +36 -2
  71. package/src/panels/incident-review-panel.ts +31 -10
  72. package/src/panels/intelligence-panel.ts +152 -71
  73. package/src/panels/knowledge-graph-panel.ts +89 -27
  74. package/src/panels/local-auth-panel.ts +17 -11
  75. package/src/panels/marketplace-panel.ts +33 -13
  76. package/src/panels/memory-panel.ts +7 -5
  77. package/src/panels/ops-control-panel.ts +69 -7
  78. package/src/panels/ops-strategy-panel.ts +61 -43
  79. package/src/panels/orchestration-panel.ts +34 -4
  80. package/src/panels/panel-list-panel.ts +33 -8
  81. package/src/panels/panel-manager.ts +23 -4
  82. package/src/panels/plan-dashboard-panel.ts +183 -66
  83. package/src/panels/plugins-panel.ts +48 -10
  84. package/src/panels/policy-panel.ts +60 -23
  85. package/src/panels/polish-core.ts +157 -0
  86. package/src/panels/polish-tables.ts +258 -0
  87. package/src/panels/polish.ts +44 -145
  88. package/src/panels/project-planning-panel.ts +27 -4
  89. package/src/panels/provider-accounts-panel.ts +55 -18
  90. package/src/panels/provider-health-panel.ts +118 -87
  91. package/src/panels/provider-stats-panel.ts +47 -22
  92. package/src/panels/qr-panel.ts +23 -11
  93. package/src/panels/remote-panel.ts +12 -5
  94. package/src/panels/routes-panel.ts +27 -5
  95. package/src/panels/sandbox-panel.ts +62 -27
  96. package/src/panels/schedule-panel.ts +44 -24
  97. package/src/panels/scrollable-list-panel.ts +124 -10
  98. package/src/panels/security-panel.ts +72 -20
  99. package/src/panels/services-panel.ts +68 -22
  100. package/src/panels/session-browser-panel.ts +42 -26
  101. package/src/panels/session-maintenance.ts +2 -2
  102. package/src/panels/settings-sync-panel.ts +30 -15
  103. package/src/panels/skills-panel.ts +2 -1
  104. package/src/panels/subscription-panel.ts +21 -3
  105. package/src/panels/symbol-outline-panel.ts +40 -47
  106. package/src/panels/system-messages-panel.ts +60 -27
  107. package/src/panels/tasks-panel.ts +36 -14
  108. package/src/panels/thinking-panel.ts +32 -36
  109. package/src/panels/token-budget-panel.ts +80 -53
  110. package/src/panels/tool-inspector-panel.ts +37 -39
  111. package/src/panels/types.ts +25 -0
  112. package/src/panels/watchers-panel.ts +25 -5
  113. package/src/panels/work-plan-panel.ts +127 -35
  114. package/src/panels/worktree-panel.ts +65 -20
  115. package/src/panels/wrfc-panel-format.ts +133 -0
  116. package/src/panels/wrfc-panel.ts +53 -147
  117. package/src/renderer/agent-detail-modal.ts +2 -2
  118. package/src/renderer/compaction-preview.ts +2 -1
  119. package/src/renderer/compositor.ts +46 -43
  120. package/src/renderer/modal-utils.ts +0 -10
  121. package/src/renderer/model-picker-overlay.ts +9 -4
  122. package/src/renderer/model-workspace.ts +2 -3
  123. package/src/renderer/panel-composite.ts +7 -20
  124. package/src/renderer/panel-workspace-bar.ts +14 -8
  125. package/src/renderer/process-modal.ts +2 -2
  126. package/src/renderer/progress.ts +3 -2
  127. package/src/renderer/tab-strip.ts +148 -34
  128. package/src/renderer/ui-factory.ts +4 -6
  129. package/src/runtime/bootstrap-command-context.ts +3 -0
  130. package/src/runtime/bootstrap-command-parts.ts +3 -1
  131. package/src/runtime/bootstrap-core.ts +31 -31
  132. package/src/runtime/bootstrap-shell.ts +1 -0
  133. package/src/runtime/onboarding/apply.ts +4 -3
  134. package/src/runtime/onboarding/snapshot.ts +4 -3
  135. package/src/runtime/process-lifecycle.ts +195 -0
  136. package/src/runtime/services.ts +4 -1
  137. package/src/runtime/ui/model-picker/health-enrichment.ts +3 -0
  138. package/src/runtime/ui/model-picker/types.ts +4 -0
  139. package/src/runtime/wrfc-persistence.ts +20 -5
  140. package/src/shell/ui-openers.ts +3 -2
  141. package/src/utils/format-duration.ts +55 -0
  142. package/src/utils/format-number.ts +71 -0
  143. package/src/verification/live-verifier.ts +4 -3
  144. package/src/version.ts +1 -1
  145. package/src/core/context-auto-compact.ts +0 -110
  146. package/src/panels/panel-picker.ts +0 -106
  147. package/src/renderer/panel-picker-overlay.ts +0 -202
  148. 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
- const ACTIVE_FG = '#e2e8f0';
6
- const ACTIVE_BG = '#0f172a';
7
- const INACTIVE_FG = '#94a3b8';
8
- const SEPARATOR_FG = '238';
9
- const LABEL_FG = '#cbd5e1';
10
- const LABEL_BG = '#1e293b';
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
- labelFg: LABEL_FG,
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 { formatDuration } from './modal-utils.ts';
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 = formatDuration(e.elapsedMs);
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 ?? '';
@@ -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 >= 1000 ? `${(used / 1000).toFixed(1)}k` : String(used);
55
- const maxK = max >= 1000 ? `${(max / 1000).toFixed(1)}k` : String(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
- ): { segments: StyledPanelSegment[]; hasOverflow: boolean } {
38
- const segments: StyledPanelSegment[] = [];
39
- let used = 0;
40
- let hasOverflow = false;
41
- for (const tab of tabs) {
42
- const separatorWidth = getDisplayWidth('│');
43
- const labelText = tab.active ? `[${tab.label}]` : tab.label;
44
- const labelWidth = getDisplayWidth(labelText);
45
- const entryWidth = separatorWidth + labelWidth + 1;
46
- if (used + entryWidth > maxWidth) {
47
- hasOverflow = true;
48
- break;
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
- segments.push({ text: '│', fg: style.separatorFg });
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: `${labelText} `,
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
- used += entryWidth;
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
- if (segments.length > 0 && used + getDisplayWidth('│') <= maxWidth) {
61
- segments.push({ text: '│', fg: style.separatorFg });
62
- }
63
- return { segments, hasOverflow };
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
- overflowIndicator = '›',
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 reservedOverflowWidth = getDisplayWidth(overflowIndicator);
78
- const maxTabWidth = Math.max(
79
- 0,
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, hasOverflow } = makeSegments(tabs, style, maxTabWidth);
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 (hasOverflow) suffixSegments.push({ text: overflowIndicator, fg: style.overflowFg ?? style.separatorFg });
208
+ if (overflowRight) {
209
+ suffixSegments.push({ text: overflowIndicator, fg: style.overflowFg ?? style.separatorFg });
210
+ }
100
211
  if (suffixLabel) {
101
- suffixSegments.push({
102
- text: suffixLabel,
103
- fg: style.trailingFg ?? style.inactiveFg,
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
- if (n < 1000) return String(n);
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 = Math.min(1, ctxTokens / contextWindow);
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 same-microtask requestRender() calls into 1.
403
- // Also enforces a 16ms minimum interval to cap at ~60fps during streaming.
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
- // Error Handling: the scheduler flag MUST be cleared even if the render
412
- // callback throws; otherwise a single render exception would wedge the
413
- // entire TUI (no future requestRender() call would schedule anything).
414
- renderScheduled = false;
415
- const now = Date.now();
416
- const elapsed = now - lastRenderTime;
417
- try {
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
- void approvalBroker.start();
445
- void sharedSessionBroker.start();
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(error instanceof Error ? error.message : String(error));
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: error instanceof Error ? error.message : String(error),
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
- error instanceof Error ? error.message : String(error),
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: error instanceof Error ? error.message : String(error),
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}: ${error instanceof Error ? error.message : String(error)}`,
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: error instanceof Error ? error.message : String(error),
248
+ message: summarizeError(error),
248
249
  },
249
250
  };
250
251
  }