@pellux/goodvibes-tui 1.7.0 → 1.9.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (137) hide show
  1. package/README.md +10 -9
  2. package/docs/foundation-artifacts/operator-contract.json +4045 -1763
  3. package/package.json +2 -2
  4. package/src/audio/spoken-turn-controller.ts +12 -2
  5. package/src/audio/spoken-turn-wiring.ts +2 -1
  6. package/src/cli/help.ts +8 -1
  7. package/src/cli/service-posture.ts +2 -1
  8. package/src/cli/status.ts +2 -1
  9. package/src/cli/surface-command.ts +2 -2
  10. package/src/core/composer-state.ts +11 -3
  11. package/src/core/conversation-line-cache.ts +27 -3
  12. package/src/core/conversation-rendering.ts +71 -14
  13. package/src/core/stream-event-wiring.ts +20 -1
  14. package/src/core/system-message-noise.ts +87 -0
  15. package/src/core/system-message-router.ts +68 -1
  16. package/src/core/turn-cancellation.ts +7 -2
  17. package/src/core/turn-event-wiring.ts +10 -2
  18. package/src/daemon/cli.ts +29 -2
  19. package/src/daemon/handlers/register.ts +8 -1
  20. package/src/daemon/service-commands.ts +329 -0
  21. package/src/input/autocomplete.ts +27 -1
  22. package/src/input/command-registry.ts +46 -4
  23. package/src/input/commands/codebase-runtime.ts +46 -6
  24. package/src/input/commands/config.ts +43 -3
  25. package/src/input/commands/health-runtime.ts +9 -1
  26. package/src/input/commands/memory.ts +68 -35
  27. package/src/input/commands/operator-panel-runtime.ts +31 -7
  28. package/src/input/commands/planning-runtime.ts +95 -6
  29. package/src/input/commands/qrcode-runtime.ts +25 -5
  30. package/src/input/commands/remote-runtime-setup.ts +5 -3
  31. package/src/input/commands/session-content.ts +20 -9
  32. package/src/input/commands/settings-sync-runtime.ts +15 -3
  33. package/src/input/commands/shell-core.ts +10 -1
  34. package/src/input/commands/workstream-runtime.ts +168 -18
  35. package/src/input/config-modal-types.ts +15 -1
  36. package/src/input/config-modal.ts +227 -12
  37. package/src/input/feed-context-factory.ts +3 -0
  38. package/src/input/handler-command-route.ts +10 -3
  39. package/src/input/handler-content-actions.ts +17 -2
  40. package/src/input/handler-feed-routes.ts +43 -121
  41. package/src/input/handler-feed.ts +32 -4
  42. package/src/input/handler-modal-routes.ts +78 -13
  43. package/src/input/handler-modal-stack.ts +11 -2
  44. package/src/input/handler-onboarding-daemon-adopt.ts +149 -0
  45. package/src/input/handler-onboarding.ts +71 -59
  46. package/src/input/handler-picker-routes.ts +15 -8
  47. package/src/input/handler-shortcuts.ts +59 -9
  48. package/src/input/handler.ts +6 -1
  49. package/src/input/keybindings.ts +6 -5
  50. package/src/input/model-picker.ts +19 -2
  51. package/src/input/onboarding/onboarding-runtime-status.ts +10 -1
  52. package/src/input/onboarding/onboarding-wizard-apply.ts +8 -1
  53. package/src/input/onboarding/onboarding-wizard-constants.ts +4 -1
  54. package/src/input/onboarding/onboarding-wizard-network-adopt.ts +136 -0
  55. package/src/input/onboarding/onboarding-wizard-steps.ts +9 -6
  56. package/src/input/onboarding/onboarding-wizard-types.ts +3 -1
  57. package/src/input/panel-mouse-geometry.ts +97 -0
  58. package/src/input/panel-paste-flood-guard.ts +86 -0
  59. package/src/input/selection-modal.ts +11 -0
  60. package/src/input/session-picker-modal.ts +44 -4
  61. package/src/input/settings-modal-data.ts +51 -2
  62. package/src/input/settings-modal-types.ts +4 -2
  63. package/src/main.ts +28 -27
  64. package/src/panels/builtin/shared.ts +9 -3
  65. package/src/panels/fleet-deep-link.ts +31 -0
  66. package/src/panels/fleet-panel-format.ts +62 -0
  67. package/src/panels/fleet-panel-worktree-detail.ts +48 -0
  68. package/src/panels/fleet-panel.ts +89 -131
  69. package/src/panels/fleet-read-model.ts +43 -0
  70. package/src/panels/fleet-steer.ts +35 -2
  71. package/src/panels/fleet-stop.ts +29 -1
  72. package/src/panels/modals/keybindings-modal.ts +16 -1
  73. package/src/panels/modals/modal-theme.ts +35 -29
  74. package/src/panels/modals/pairing-modal.ts +25 -6
  75. package/src/panels/modals/planning-modal.ts +43 -21
  76. package/src/panels/modals/work-plan-modal.ts +28 -0
  77. package/src/panels/panel-manager.ts +15 -4
  78. package/src/panels/polish-core.ts +38 -25
  79. package/src/panels/project-planning-answer-actions.ts +8 -13
  80. package/src/panels/types.ts +24 -0
  81. package/src/permissions/prompt.ts +160 -13
  82. package/src/renderer/autocomplete-overlay.ts +28 -2
  83. package/src/renderer/compositor.ts +2 -3
  84. package/src/renderer/config-modal.ts +19 -5
  85. package/src/renderer/footer-tips.ts +5 -1
  86. package/src/renderer/fullscreen-primitives.ts +32 -22
  87. package/src/renderer/git-status.ts +3 -1
  88. package/src/renderer/layout.ts +0 -4
  89. package/src/renderer/markdown.ts +7 -3
  90. package/src/renderer/modal-factory.ts +25 -20
  91. package/src/renderer/model-workspace.ts +18 -3
  92. package/src/renderer/overlay-box.ts +21 -17
  93. package/src/renderer/process-indicator.ts +14 -3
  94. package/src/renderer/selection-modal-overlay.ts +6 -1
  95. package/src/renderer/session-picker-modal.ts +196 -3
  96. package/src/renderer/settings-modal-helpers.ts +2 -0
  97. package/src/renderer/settings-modal.ts +7 -0
  98. package/src/renderer/shell-surface.ts +8 -1
  99. package/src/renderer/status-glyphs.ts +14 -15
  100. package/src/renderer/system-message.ts +15 -3
  101. package/src/renderer/terminal-bg-probe.ts +339 -0
  102. package/src/renderer/terminal-escapes.ts +20 -0
  103. package/src/renderer/theme-mode-config.ts +67 -0
  104. package/src/renderer/theme.ts +91 -1
  105. package/src/renderer/thinking.ts +11 -3
  106. package/src/renderer/tool-call.ts +15 -9
  107. package/src/renderer/tool-result-summary.ts +148 -0
  108. package/src/renderer/turn-injection.ts +22 -3
  109. package/src/renderer/ui-factory.ts +154 -85
  110. package/src/renderer/ui-primitives.ts +30 -129
  111. package/src/runtime/bootstrap-command-context.ts +6 -0
  112. package/src/runtime/bootstrap-command-parts.ts +8 -4
  113. package/src/runtime/bootstrap-core.ts +33 -11
  114. package/src/runtime/bootstrap-hook-bridge.ts +7 -0
  115. package/src/runtime/bootstrap-shell.ts +19 -1
  116. package/src/runtime/bootstrap.ts +118 -5
  117. package/src/runtime/code-index-services.ts +25 -2
  118. package/src/runtime/legacy-daemon-migration.ts +516 -0
  119. package/src/runtime/memory-fold.ts +26 -0
  120. package/src/runtime/onboarding/derivation.ts +7 -2
  121. package/src/runtime/onboarding/snapshot.ts +27 -1
  122. package/src/runtime/onboarding/types.ts +31 -1
  123. package/src/runtime/operator-token-cleanup.ts +82 -1
  124. package/src/runtime/orchestrator-core-services.ts +10 -0
  125. package/src/runtime/resume-notice.ts +209 -0
  126. package/src/runtime/services.ts +12 -8
  127. package/src/runtime/session-inbound-inputs.ts +252 -0
  128. package/src/runtime/session-spine-transport.ts +64 -0
  129. package/src/runtime/terminal-output-guard.ts +15 -8
  130. package/src/runtime/ui-services.ts +19 -3
  131. package/src/runtime/workstream-services.ts +160 -28
  132. package/src/runtime/wrfc-persistence.ts +124 -17
  133. package/src/shell/blocking-input.ts +46 -3
  134. package/src/shell/recovery-input-helpers.ts +170 -1
  135. package/src/shell/ui-openers.ts +42 -9
  136. package/src/utils/terminal-width.ts +52 -0
  137. package/src/version.ts +1 -1
@@ -5,15 +5,135 @@
5
5
  * Shows a list of saved sessions with:
6
6
  * - name, timestamp (formatted), message count
7
7
  * Footer hints: [Enter] Load [d] Delete [Esc] Close
8
+ *
9
+ * W3-T2: when the modal was wired with a cross-surface session union
10
+ * (`modal.crossSurfaceView.mode !== 'local'`), an additional read-only
11
+ * "Cross-surface sessions" section is appended, badged kind/status/project
12
+ * (parity with the webui SessionsView) with one of three honest states —
13
+ * true-empty, offline, or stale — never a silently-collapsed list. In
14
+ * 'local' mode (no sessionBroker wired, e.g. every pre-existing caller/test)
15
+ * this section is entirely absent and the box sizing is unchanged.
8
16
  */
9
17
 
10
18
  import type { Line } from '../types/grid.ts';
19
+ import type { SharedSessionRecord } from '@pellux/goodvibes-sdk/platform/control-plane';
11
20
  import { ModalFactory } from './modal-factory.ts';
21
+ import { UI_TONES } from './ui-primitives.ts';
12
22
  import type { SessionPickerModal } from '../input/session-picker-modal.ts';
13
23
  import { formatTimestamp } from './modal-utils.ts';
14
24
  import { fitDisplay } from '../utils/terminal-width.ts';
15
25
  import { getOverlaySurfaceMetrics, getStableOverlayContentRows } from './overlay-viewport.ts';
16
26
 
27
+ // ---------------------------------------------------------------------------
28
+ // Cross-surface badge helpers (parity with webui's src/lib/sessions-union.ts)
29
+ // ---------------------------------------------------------------------------
30
+
31
+ /** Verbatim for unknown/future kinds — never dropped, never guessed. */
32
+ function kindLabel(kind: string): string {
33
+ return kind.trim() || 'unknown';
34
+ }
35
+
36
+ function projectLabel(project: string): string {
37
+ return project.trim() || 'unknown';
38
+ }
39
+
40
+ function isClosedStatus(status: string): boolean {
41
+ return status.trim().toLowerCase() === 'closed';
42
+ }
43
+
44
+ /**
45
+ * D-TUI/#A2: closed sessions carry an optional, honest reason for WHY they
46
+ * closed under `metadata.closeReason` (SDK's `SharedSessionCloseReason`,
47
+ * 'closeReason' key — see `@pellux/goodvibes-sdk` platform/control-plane
48
+ * session-broker-sessions.ts's `readSessionCloseReason`). `metadata` is an
49
+ * open record so old readers and records from a build that predates this
50
+ * field ignore it safely — read it duck-typed here rather than importing the
51
+ * SDK's helper, and tolerate `metadata` itself being absent or malformed.
52
+ */
53
+ function readCloseReason(record: SharedSessionRecord): string | undefined {
54
+ const raw = (record.metadata as Record<string, unknown> | undefined)?.['closeReason'];
55
+ return typeof raw === 'string' ? raw : undefined;
56
+ }
57
+
58
+ /**
59
+ * A GC sweep closing an idle session ('idle-reaped') auto-reopens on the next
60
+ * heartbeat — it is NOT the same event as a deliberate user/surface close, so
61
+ * it must never render under the same "closed" badge (W4/#A2). Tolerant of
62
+ * records without the field (pre-feature builds, or a deliberate close).
63
+ */
64
+ function isReapedRecord(record: SharedSessionRecord): boolean {
65
+ return isClosedStatus(record.status) && readCloseReason(record) === 'idle-reaped';
66
+ }
67
+
68
+ /**
69
+ * Wave-4 UX-lens note: 'reaped' names a mechanism (the idle-session sweep),
70
+ * not a state a first-time reader can guess — the webui pairs its own
71
+ * 'reaped' badge with a tooltip explaining it
72
+ * (SessionsView.tsx: "Closed by the idle-session sweep — reopens
73
+ * automatically on the next activity"). The TUI has no hover/tooltip
74
+ * surface, so the plain-language explanation is rendered as its own short
75
+ * line under the cross-surface list instead — shown ONLY when at least one
76
+ * visible row actually carries the badge, so it never adds noise to a list
77
+ * with no reaped rows.
78
+ */
79
+ const REAPED_BADGE_HINT = 'reaped = closed by the idle sweep — reopens on next activity';
80
+
81
+ /** True when at least one of the currently-rendered (post-truncation) cross-surface rows carries the 'reaped' badge. */
82
+ function hasVisibleReapedRow(modal: SessionPickerModal): boolean {
83
+ if (modal.crossSurfaceView.mode === 'local') return false;
84
+ return modal.crossSurfaceSessions.slice(0, MAX_CROSS_SURFACE_ROWS).some(isReapedRecord);
85
+ }
86
+
87
+ function statusLabel(record: SharedSessionRecord): string {
88
+ const trimmed = record.status.trim();
89
+ if (!trimmed) return 'active';
90
+ if (!isClosedStatus(trimmed)) return trimmed;
91
+ return isReapedRecord(record) ? 'reaped' : 'closed';
92
+ }
93
+
94
+ const MAX_CROSS_SURFACE_ROWS = 5;
95
+
96
+ /**
97
+ * The exact honest note for the current state, or null when the union view
98
+ * needs no caveat (fresh/embedded with rows). Precedence: offline > stale >
99
+ * true-empty — the three states from the W3-T2 brief, never collapsed into
100
+ * each other (an offline view never silently renders as "no sessions yet").
101
+ */
102
+ function crossSurfaceNote(view: SessionPickerModal['crossSurfaceView'], rowCount: number): string | null {
103
+ if (view.mode === 'local') return null;
104
+ if (view.offlineNote) return `${view.offlineNote} — showing local sessions only`;
105
+ if (view.stale) {
106
+ if (view.lastSyncAt === null) return 'Union view may be stale.';
107
+ const ageSeconds = Math.max(0, Math.round((Date.now() - view.lastSyncAt) / 1000));
108
+ return `Union view may be stale, last synced ${ageSeconds}s ago`;
109
+ }
110
+ if (rowCount === 0) return 'No sessions yet.';
111
+ return null;
112
+ }
113
+
114
+ function crossSurfaceRowCount(modal: SessionPickerModal): number {
115
+ return modal.crossSurfaceView.mode === 'local' ? 0 : Math.min(MAX_CROSS_SURFACE_ROWS, modal.crossSurfaceSessions.length);
116
+ }
117
+
118
+ /**
119
+ * Extra content rows the cross-surface section needs: separator + header
120
+ * (+note) + rows (+the "[showing N of M]" overflow line when the union has
121
+ * more records than MAX_CROSS_SURFACE_ROWS — previously uncounted here, which
122
+ * let modal-factory's tail-clip silently eat that trailing line even though
123
+ * the rest of the budget accounting held; see W3 Finding 1).
124
+ */
125
+ function crossSurfaceExtraRows(modal: SessionPickerModal): number {
126
+ if (modal.crossSurfaceView.mode === 'local') return 0;
127
+ const rows = crossSurfaceRowCount(modal);
128
+ const noteRow = crossSurfaceNote(modal.crossSurfaceView, rows) ? 1 : 0;
129
+ const overflowRow = modal.crossSurfaceSessions.length > MAX_CROSS_SURFACE_ROWS ? 1 : 0;
130
+ // W4 UX-lens: the plain-language 'reaped' explainer is its own row —
131
+ // uncounted here it would be silently eaten by modal-factory's tail-clip
132
+ // exactly the way W3 Finding 1 describes above.
133
+ const reapedHintRow = hasVisibleReapedRow(modal) ? 1 : 0;
134
+ return 2 + noteRow + overflowRow + reapedHintRow + Math.max(rows, noteRow === 1 && rows === 0 ? 0 : rows);
135
+ }
136
+
17
137
  // ---------------------------------------------------------------------------
18
138
  // Renderer
19
139
  // ---------------------------------------------------------------------------
@@ -29,16 +149,37 @@ export function renderSessionPickerModal(
29
149
  width: number,
30
150
  viewportHeight = 24,
31
151
  ): Line[] {
152
+ const extraRows = crossSurfaceExtraRows(modal);
32
153
  const metrics = getOverlaySurfaceMetrics(width, viewportHeight, {
33
154
  chromeRows: 6,
34
155
  minContentRows: 5,
35
- maxContentRows: 9,
156
+ maxContentRows: 9 + extraRows,
36
157
  });
37
158
  const boxMargin = metrics.margin;
38
159
  const boxW = metrics.boxWidth;
39
160
  const contentW = metrics.contentWidth;
40
- const visibleRows = metrics.contentRows;
41
- const targetContentRows = getStableOverlayContentRows(metrics.contentRows, 8);
161
+ const targetContentRows = getStableOverlayContentRows(metrics.contentRows, 8 + extraRows);
162
+
163
+ // W3 Finding 1: when a cross-surface union section is present, cap the
164
+ // LOCAL list's window to what's left after reserving extraRows for the
165
+ // union — otherwise the local list expands to fill metrics.contentRows
166
+ // (which already includes the union's reservation) and modal-factory's
167
+ // single tail-clip (createModal, targetContentRows) silently drops the
168
+ // union section, or its trailing rows, once the box fills up. In local
169
+ // mode (extraRows === 0, every pre-existing caller/test) this reduces to
170
+ // the exact pre-T2 formula, so local-only output is byte-for-byte
171
+ // unaffected.
172
+ let visibleRows: number;
173
+ if (extraRows === 0) {
174
+ visibleRows = metrics.contentRows;
175
+ } else {
176
+ const localBudget = Math.max(0, targetContentRows - extraRows);
177
+ const overhead = 2; // header + separator row, present whenever sessions.length > 0
178
+ const remaining = Math.max(0, localBudget - overhead);
179
+ visibleRows = modal.sessions.length > remaining
180
+ ? Math.max(3, remaining - 2) // reserve 2 more rows for the local "[x-y of N]" overflow line
181
+ : Math.max(3, remaining);
182
+ }
42
183
  modal.setVisibleRows(visibleRows);
43
184
 
44
185
  const sections: import('./modal-factory.ts').ModalSection[] = [];
@@ -97,6 +238,58 @@ export function renderSessionPickerModal(
97
238
  }
98
239
  }
99
240
 
241
+ // W3-T2: cross-surface session union — visible only when a sessionBroker
242
+ // was wired (mode !== 'local'); absent entirely otherwise, so the box size
243
+ // and content of every pre-existing (local-only) caller is unaffected.
244
+ if (modal.crossSurfaceView.mode !== 'local') {
245
+ sections.push({ type: 'separator' });
246
+ sections.push({
247
+ type: 'text',
248
+ content: `Cross-surface sessions (${modal.crossSurfaceView.mode})`,
249
+ style: { fg: '240', dim: true, bold: true },
250
+ });
251
+ const note = crossSurfaceNote(modal.crossSurfaceView, modal.crossSurfaceSessions.length);
252
+ if (note) {
253
+ sections.push({
254
+ type: 'text',
255
+ content: note,
256
+ style: { fg: modal.crossSurfaceView.offlineNote ? UI_TONES.state.warn : '244', dim: !modal.crossSurfaceView.offlineNote },
257
+ });
258
+ }
259
+ if (modal.crossSurfaceSessions.length > 0) {
260
+ const rows = modal.crossSurfaceSessions.slice(0, MAX_CROSS_SURFACE_ROWS);
261
+ const listItems: import('./modal-factory.ts').ModalListItem[] = rows.map((record) => {
262
+ const reaped = isReapedRecord(record);
263
+ const closed = isClosedStatus(record.status);
264
+ const label = fitDisplay(
265
+ `${record.title || record.id} · ${kindLabel(record.kind)} · ${statusLabel(record)} · ${projectLabel(record.project)}`,
266
+ contentW,
267
+ );
268
+ // Reaped rows get their own tone (not plain dim-closed): the session
269
+ // will auto-reopen on the next heartbeat, unlike a deliberate close.
270
+ const style = reaped ? { fg: UI_TONES.state.info } : closed ? { fg: '244', dim: true } : undefined;
271
+ return { label, style };
272
+ });
273
+ sections.push({ type: 'list', items: listItems });
274
+ if (modal.crossSurfaceSessions.length > MAX_CROSS_SURFACE_ROWS) {
275
+ sections.push({
276
+ type: 'text',
277
+ content: `[showing ${MAX_CROSS_SURFACE_ROWS} of ${modal.crossSurfaceSessions.length}]`,
278
+ style: { fg: '244', dim: true },
279
+ });
280
+ }
281
+ // W4 UX-lens: only ever rendered when a visible row actually carries
282
+ // the 'reaped' badge — never speculative noise.
283
+ if (hasVisibleReapedRow(modal)) {
284
+ sections.push({
285
+ type: 'text',
286
+ content: REAPED_BADGE_HINT,
287
+ style: { fg: UI_TONES.state.info, dim: true },
288
+ });
289
+ }
290
+ }
291
+ }
292
+
100
293
  // Status message if present
101
294
  if (modal.statusMessage) {
102
295
  sections.push({ type: 'separator' });
@@ -59,10 +59,12 @@ export const CATEGORY_LABELS: Record<(typeof SETTINGS_CATEGORIES)[number], strin
59
59
  storage: 'Storage',
60
60
  permissions: 'Permissions',
61
61
  orchestration: 'Orchestration',
62
+ planner: 'Planner',
62
63
  wrfc: 'WRFC',
63
64
  helper: 'Helper',
64
65
  tts: 'TTS',
65
66
  service: 'Service',
67
+ daemon: 'Daemon',
66
68
  controlPlane: 'Control Plane',
67
69
  httpListener: 'HTTP Listener',
68
70
  web: 'Web',
@@ -32,10 +32,12 @@ const CATEGORY_INFO: Record<SettingsCategory, string> = {
32
32
  storage: 'Local storage posture, including secret storage policy and maximum artifact size for knowledge/home graph/document ingestion.',
33
33
  permissions: 'Permission mode and tool-class policy. These settings decide whether the shell prompts before read/write/exec/network/agent actions.',
34
34
  orchestration: 'Agent orchestration limits and recursion controls.',
35
+ planner: 'How /workstream decomposes a goal into work items: agent-driven decomposition (with heuristic fallback) or the forced heuristic path, plus the planning agent\'s turn, token, and wall-clock bounds.',
35
36
  wrfc: 'Work-review-fix-cycle thresholds, retry limits, and automatic commit behavior.',
36
37
  helper: 'Helper model defaults used by helper subsystems when they do not use the main chat route.',
37
38
  tts: 'Text-to-speech provider, voice, and optional spoken-turn LLM overrides.',
38
39
  service: 'Background service posture: enabled state, autostart, restart behavior, service name, platform, and logs.',
40
+ daemon: 'Local session daemon. It hosts the shared session broker and companion chat so a session started here is visible and steerable from other surfaces. On by default, bound to loopback (127.0.0.1) only.',
39
41
  controlPlane: 'Daemon control-plane settings for local admin/API access.',
40
42
  httpListener: 'HTTP listener settings for webhook and integration ingress.',
41
43
  web: 'Browser surface settings for the local or network web UI.',
@@ -57,6 +59,11 @@ const CATEGORY_INFO: Record<SettingsCategory, string> = {
57
59
  };
58
60
 
59
61
  const ENUM_VALUE_DESCRIPTIONS: Record<string, Record<string, string>> = {
62
+ 'display.themeMode': {
63
+ auto: 'Probe the terminal background colour (OSC 11) once at startup and pick light or dark. Falls back to dark on unreadable/unsupported terminals. Only evaluated at startup — selecting auto takes effect next launch.',
64
+ dark: 'Force the dark theme regardless of terminal background. Applies immediately.',
65
+ light: 'Force the light theme regardless of terminal background. Applies immediately.',
66
+ },
60
67
  'behavior.hitlMode': {
61
68
  quiet: 'Minimize operational interruptions and surface fewer Human-in-the-Loop prompts.',
62
69
  balanced: 'Show important Human-in-the-Loop prompts without turning routine work into noise.',
@@ -15,7 +15,7 @@ export interface ShellFooterBuildOptions {
15
15
  * themselves (main.ts does) do not need to also pass this.
16
16
  */
17
17
  readonly panelFocused?: boolean;
18
- readonly usage: { up: number; down: number };
18
+ readonly usage: { up: number; down: number; fleetCostUsd?: number | null };
19
19
  readonly showExitNotice: boolean;
20
20
  readonly lastCopyTime: number;
21
21
  readonly model?: string;
@@ -47,6 +47,12 @@ export interface ShellFooterBuildOptions {
47
47
  * the process indicator, context bar, context-info line and posture block.
48
48
  */
49
49
  readonly compact?: boolean;
50
+ /**
51
+ * S3d: cross-surface session-spine posture for the context-info segment.
52
+ * Set ONLY in adopted-daemon mode ('online'/'offline'); left undefined in
53
+ * embedded/local mode so no segment renders.
54
+ */
55
+ readonly sessionSpineStatus?: 'online' | 'offline';
50
56
  }
51
57
 
52
58
  export interface ShellFooterBuildResult {
@@ -120,6 +126,7 @@ export function buildShellFooter(
120
126
  options.composerFlags,
121
127
  options.composerPendingRisk,
122
128
  options.compact ?? false,
129
+ options.sessionSpineStatus,
123
130
  );
124
131
  // Compact posture drops the process indicator and context hint entirely so
125
132
  // the footer fits within ~5 rows on short terminals.
@@ -4,20 +4,19 @@
4
4
  // Extracted as a neutral module so both status-token.ts and polish.ts can
5
5
  // import from here without creating a circular ESM dependency.
6
6
  //
7
- // STATE_GLYPHS is an alias into GLYPHS.status (ui-primitives.ts) the single
8
- // glyph registry. Previously GLYPHS.status.info ('•') collided with
9
- // GLYPHS.status.pending, and STATE_GLYPHS.info ('○') collided with
10
- // GLYPHS.status.idle. GLYPHS.status.info is now '○' (this module's historical
11
- // value) and idle uses a distinct glyph ('◌').
7
+ // W6-P1: STATE_GLYPHS is no longer hardcoded here. It is the SDK presentation
8
+ // contract (@pellux/goodvibes-sdk/platform/presentation, landed by W4-S1 and
9
+ // already adopted by the agent in W4-R4), aliased to GLYPHS.status so the
10
+ // four semantic glyphs are spelled out in exactly one place and can never
11
+ // drift from the registry again. Re-exported under the historical name so
12
+ // status-token.ts and polish.ts import unchanged.
13
+ //
14
+ // Glyphs (unchanged — this module's historical values, the TUI reference the
15
+ // contract was extracted from):
16
+ // good ✓ (CHECK MARK U+2713) — GLYPHS.status.success
17
+ // warn ⚠ (WARNING SIGN U+26A0) — GLYPHS.status.warn
18
+ // bad ✕ (MULTIPLICATION X U+2715) — GLYPHS.status.failure
19
+ // info ○ (WHITE CIRCLE U+25CB) — GLYPHS.status.info
12
20
  // ---------------------------------------------------------------------------
13
21
 
14
- import { GLYPHS } from './ui-primitives.ts';
15
-
16
- export type StatusState = 'good' | 'warn' | 'bad' | 'info';
17
-
18
- export const STATE_GLYPHS: Record<StatusState, string> = {
19
- good: GLYPHS.status.success, // ✓
20
- warn: GLYPHS.status.warn, // ⚠
21
- bad: GLYPHS.status.failure, // ✕
22
- info: GLYPHS.status.info, // ○
23
- };
22
+ export { STATE_GLYPHS, type StatusState } from '@pellux/goodvibes-sdk/platform/presentation';
@@ -1,6 +1,7 @@
1
1
  import { type Line } from '../types/grid.ts';
2
- import { BORDERS, COLORS } from './layout.ts';
2
+ import { BORDERS } from './layout.ts';
3
3
  import { renderConversationNotice } from './conversation-surface.ts';
4
+ import { activeUiTones } from './theme.ts';
4
5
 
5
6
  /** Exported for use by typeOverride callers and tests. */
6
7
  export type SystemMessageType = 'error' | 'warning' | 'info';
@@ -71,12 +72,23 @@ export function renderSystemMessage(
71
72
  typeOverride?: SystemMessageType,
72
73
  ): Line[] {
73
74
  const msgType = typeOverride ?? classifySystemMessage(content);
75
+ // System-message notices paint the ▌ marker and body text on the TRANSPARENT
76
+ // terminal background (renderConversationNotice passes no bodyBg), so the
77
+ // accent and dim text resolve per-render through activeUiTones() to stay
78
+ // legible on a light terminal. Dark values are byte-identical to the prior
79
+ // static reads (chrome.bad == state.bad, chrome.warn == state.warn,
80
+ // state.info unchanged, chrome.faint == fg.dim). The ▌ marker glyph is still
81
+ // sourced from the BORDERS table.
82
+ const t = activeUiTones();
74
83
  const border = msgType === 'error' ? BORDERS.ERROR
75
84
  : msgType === 'warning' ? BORDERS.WARNING
76
85
  : BORDERS.INFO;
77
- const textColor = msgType === 'info' ? COLORS.DIM_TEXT : border.color;
86
+ const accent = msgType === 'error' ? t.chrome.bad
87
+ : msgType === 'warning' ? t.chrome.warn
88
+ : t.state.info;
89
+ const textColor = msgType === 'info' ? t.chrome.faint : accent;
78
90
  return renderConversationNotice(content, width, {
79
- accent: border.color,
91
+ accent,
80
92
  text: textColor,
81
93
  dim: msgType === 'info',
82
94
  }, border.char);