@pellux/goodvibes-agent 1.5.5 → 1.5.6

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 (165) hide show
  1. package/CHANGELOG.md +9 -0
  2. package/README.md +1 -1
  3. package/dist/package/main.js +94130 -79335
  4. package/docs/tools-and-commands.md +2 -1
  5. package/package.json +6 -3
  6. package/src/agent/autonomy-schedule-format.ts +1 -1
  7. package/src/agent/autonomy-schedule.ts +3 -3
  8. package/src/agent/calendar/calendar-oauth-service.ts +324 -0
  9. package/src/agent/calendar-subscription-registry.ts +497 -0
  10. package/src/agent/email/email-service.ts +57 -0
  11. package/src/agent/email/smtp-client.ts +30 -10
  12. package/src/agent/memory-prompt.ts +143 -12
  13. package/src/agent/operator-actions.ts +26 -22
  14. package/src/agent/prompt-context-receipts.ts +45 -18
  15. package/src/agent/reminder-schedule-format.ts +1 -1
  16. package/src/agent/reminder-schedule.ts +3 -3
  17. package/src/agent/routine-schedule-format.ts +2 -2
  18. package/src/agent/routine-schedule-promotion.ts +5 -5
  19. package/src/agent/routine-schedule-receipts.ts +2 -2
  20. package/src/agent/schedule-edit-format.ts +1 -1
  21. package/src/agent/schedule-edit.ts +5 -5
  22. package/src/agent/session-registration.ts +281 -0
  23. package/src/agent/vibe-file.ts +50 -0
  24. package/src/cli/local-library-command-shared.ts +146 -0
  25. package/src/cli/management.ts +2 -1
  26. package/src/cli/personas-command.ts +252 -0
  27. package/src/cli/resume-relaunch-notice.ts +151 -0
  28. package/src/cli/routines-command.ts +25 -2
  29. package/src/cli/service-posture.ts +3 -2
  30. package/src/cli/skill-bundle-command.ts +175 -0
  31. package/src/cli/skills-command.ts +309 -0
  32. package/src/cli/status.ts +43 -7
  33. package/src/cli/tui-startup.ts +24 -1
  34. package/src/config/agent-settings-policy.ts +0 -1
  35. package/src/config/secret-config.ts +4 -0
  36. package/src/core/conversation-rendering.ts +20 -11
  37. package/src/core/focus-tracker.ts +41 -0
  38. package/src/core/system-message-noise.ts +108 -0
  39. package/src/core/system-message-router.ts +54 -1
  40. package/src/core/thinking-overlay.ts +83 -0
  41. package/src/input/agent-workspace-access-command-editor-submission.ts +75 -108
  42. package/src/input/agent-workspace-access-command-editors.ts +108 -128
  43. package/src/input/agent-workspace-activation.ts +15 -0
  44. package/src/input/agent-workspace-basic-command-editor-submission.ts +292 -513
  45. package/src/input/agent-workspace-basic-command-editors.ts +433 -564
  46. package/src/input/agent-workspace-calendar-connect-editor.ts +116 -0
  47. package/src/input/agent-workspace-calendar-oauth-editor.ts +152 -0
  48. package/src/input/agent-workspace-calendar-subscribe-editor.ts +128 -0
  49. package/src/input/agent-workspace-categories.ts +7 -2
  50. package/src/input/agent-workspace-channel-command-editor-submission.ts +38 -52
  51. package/src/input/agent-workspace-channel-command-editors.ts +42 -46
  52. package/src/input/agent-workspace-command-editor-engine.ts +133 -0
  53. package/src/input/agent-workspace-command-editor.ts +4 -0
  54. package/src/input/agent-workspace-direct-editor-submission.ts +59 -0
  55. package/src/input/agent-workspace-email-connect-editor.ts +162 -0
  56. package/src/input/agent-workspace-knowledge-command-editor-submission.ts +52 -81
  57. package/src/input/agent-workspace-knowledge-command-editors.ts +71 -71
  58. package/src/input/agent-workspace-library-command-editor-submission.ts +10 -28
  59. package/src/input/agent-workspace-library-command-editors.ts +16 -2
  60. package/src/input/agent-workspace-live-counters.ts +55 -0
  61. package/src/input/agent-workspace-mcp-command-editor-submission.ts +60 -82
  62. package/src/input/agent-workspace-mcp-command-editors.ts +52 -0
  63. package/src/input/agent-workspace-media-command-editor-submission.ts +12 -39
  64. package/src/input/agent-workspace-media-command-editors.ts +10 -8
  65. package/src/input/agent-workspace-memory-command-editor-submission.ts +76 -151
  66. package/src/input/agent-workspace-memory-command-editors.ts +116 -141
  67. package/src/input/agent-workspace-operations-command-editor-submission.ts +131 -184
  68. package/src/input/agent-workspace-operations-command-editors.ts +150 -162
  69. package/src/input/agent-workspace-provider-command-editor-submission.ts +60 -106
  70. package/src/input/agent-workspace-provider-command-editors.ts +58 -68
  71. package/src/input/agent-workspace-search.ts +8 -1
  72. package/src/input/agent-workspace-session-command-editor-submission.ts +104 -132
  73. package/src/input/agent-workspace-session-command-editors.ts +160 -195
  74. package/src/input/agent-workspace-settings.ts +40 -1
  75. package/src/input/agent-workspace-skill-bundle-command-editor-submission.ts +55 -71
  76. package/src/input/agent-workspace-skill-bundle-command-editors.ts +65 -69
  77. package/src/input/agent-workspace-snapshot-builders.ts +431 -0
  78. package/src/input/agent-workspace-snapshot-config.ts +43 -0
  79. package/src/input/agent-workspace-snapshot.ts +198 -432
  80. package/src/input/agent-workspace-task-command-editor-submission.ts +34 -44
  81. package/src/input/agent-workspace-task-command-editors.ts +27 -28
  82. package/src/input/agent-workspace-types.ts +35 -1
  83. package/src/input/agent-workspace.ts +5 -14
  84. package/src/input/command-registry.ts +23 -1
  85. package/src/input/commands/calendar-connect-runtime.ts +226 -0
  86. package/src/input/commands/calendar-runtime.ts +110 -7
  87. package/src/input/commands/calendar-subscription-runtime.ts +225 -0
  88. package/src/input/commands/email-runtime.ts +100 -40
  89. package/src/input/commands/knowledge.ts +1 -1
  90. package/src/input/commands/network-scan-runtime.ts +75 -0
  91. package/src/input/commands/operator-actions-runtime.ts +9 -6
  92. package/src/input/commands/personas-runtime.ts +1 -1
  93. package/src/input/commands/schedule-runtime.ts +4 -4
  94. package/src/input/commands/session-content.ts +13 -1
  95. package/src/input/commands/session-workflow.ts +15 -20
  96. package/src/input/commands/session.ts +3 -1
  97. package/src/input/commands/shell-core.ts +20 -6
  98. package/src/input/commands.ts +2 -2
  99. package/src/input/delete-key-policy.ts +46 -0
  100. package/src/input/feed-context-factory.ts +10 -0
  101. package/src/input/handler-feed.ts +87 -0
  102. package/src/input/handler-modal-routes.ts +6 -1
  103. package/src/input/handler.ts +5 -0
  104. package/src/input/panel-paste-flood-guard.ts +94 -0
  105. package/src/input/settings-modal-types.ts +5 -3
  106. package/src/input/settings-modal.ts +21 -0
  107. package/src/main.ts +36 -36
  108. package/src/permissions/approval-posture.ts +141 -0
  109. package/src/renderer/agent-workspace-context-lines.ts +10 -2
  110. package/src/renderer/compositor.ts +27 -4
  111. package/src/renderer/diff.ts +61 -18
  112. package/src/renderer/fullscreen-primitives.ts +37 -18
  113. package/src/renderer/markdown.ts +20 -10
  114. package/src/renderer/modal-factory.ts +25 -15
  115. package/src/renderer/overlay-box.ts +23 -12
  116. package/src/renderer/process-indicator.ts +8 -3
  117. package/src/renderer/prompt-content-width.ts +16 -0
  118. package/src/renderer/settings-modal-helpers.ts +2 -0
  119. package/src/renderer/settings-modal.ts +2 -0
  120. package/src/renderer/startup-theme-probe.ts +35 -0
  121. package/src/renderer/status-glyphs.ts +11 -15
  122. package/src/renderer/system-message.ts +17 -2
  123. package/src/renderer/term-caps.ts +318 -0
  124. package/src/renderer/terminal-bg-probe.ts +373 -0
  125. package/src/renderer/terminal-escapes.ts +24 -0
  126. package/src/renderer/theme-mode-config.ts +87 -0
  127. package/src/renderer/theme.ts +241 -0
  128. package/src/renderer/thinking.ts +12 -3
  129. package/src/renderer/tool-call.ts +7 -3
  130. package/src/renderer/ui-factory.ts +92 -36
  131. package/src/renderer/ui-primitives.ts +33 -93
  132. package/src/runtime/bootstrap-core.ts +15 -0
  133. package/src/runtime/bootstrap-hook-bridge.ts +6 -0
  134. package/src/runtime/bootstrap-shell.ts +2 -0
  135. package/src/runtime/bootstrap.ts +68 -5
  136. package/src/runtime/calendar-boot-refresh.ts +105 -0
  137. package/src/runtime/lan-scan-consent.ts +253 -0
  138. package/src/runtime/services.ts +127 -2
  139. package/src/runtime/session-spine-rest-transport.ts +160 -0
  140. package/src/runtime/terminal-output-guard.ts +6 -1
  141. package/src/runtime/ui-services.ts +3 -0
  142. package/src/shell/agent-workspace-fullscreen.ts +5 -0
  143. package/src/shell/terminal-focus-mode.ts +120 -0
  144. package/src/shell/ui-openers.ts +13 -4
  145. package/src/tools/agent-harness-autonomy-intake.ts +5 -5
  146. package/src/tools/agent-harness-autonomy-queue.ts +1 -1
  147. package/src/tools/agent-harness-background-processes.ts +2 -1
  148. package/src/tools/agent-harness-metadata.ts +16 -8
  149. package/src/tools/agent-harness-operator-methods.ts +44 -8
  150. package/src/tools/agent-harness-personal-ops-discovery.ts +57 -10
  151. package/src/tools/agent-harness-personal-ops-lanes.ts +17 -5
  152. package/src/tools/agent-harness-personal-ops-operations.ts +7 -7
  153. package/src/tools/agent-harness-personal-ops-types.ts +6 -0
  154. package/src/tools/agent-harness-prompt-context.ts +26 -12
  155. package/src/tools/agent-harness-workspace-actions.ts +4 -0
  156. package/src/tools/agent-local-registry-args.ts +117 -0
  157. package/src/tools/agent-local-registry-memory.ts +227 -0
  158. package/src/tools/agent-local-registry-tool.ts +19 -237
  159. package/src/tools/agent-operator-briefing-tool.ts +2 -2
  160. package/src/tools/agent-operator-method-tool.ts +13 -0
  161. package/src/tools/agent-policy-explanation.ts +39 -4
  162. package/src/tools/agent-schedule-tool.ts +5 -5
  163. package/src/utils/terminal-width.ts +98 -1
  164. package/src/version.ts +1 -1
  165. package/src/cli/local-library-command.ts +0 -825
@@ -0,0 +1,24 @@
1
+ /**
2
+ * terminal-escapes — the raw control sequences main.ts writes to enter/leave the
3
+ * TUI's terminal mode (alt screen, mouse, cursor, keyboard-extension, paste, and
4
+ * focus reporting). Extracted as plain constants so callers share one definition
5
+ * and the entry file stays within the source-file line-count gate.
6
+ *
7
+ * FOCUS_ENABLE / FOCUS_DISABLE (DECSET ?1004) are the shared home for the OS
8
+ * window-focus reporting mode consumed by the focus-tracker (W4-R3): R3 imports
9
+ * FOCUS_ENABLE from here rather than redefining it.
10
+ */
11
+
12
+ export const ALT_SCREEN_ENTER = '\x1b[?1049h';
13
+ export const ALT_SCREEN_EXIT = '\x1b[?1049l';
14
+ export const MOUSE_ENABLE = '\x1b[?1000h\x1b[?1002h\x1b[?1006h';
15
+ export const MOUSE_DISABLE = '\x1b[?1006l\x1b[?1002l\x1b[?1000l';
16
+ export const CURSOR_HIDE = '\x1b[?25l';
17
+ export const CURSOR_SHOW = '\x1b[?25h';
18
+ export const CLEAR_SCREEN = '\x1b[2J\x1b[3J\x1b[H';
19
+ export const KEYBOARD_EXT_ENABLE = '\x1b[>4;2m' + '\x1b[?1u';
20
+ export const KEYBOARD_EXT_DISABLE = '\x1b[>4;0m' + '\x1b[?1l';
21
+ export const PASTE_ENABLE = '\x1b[?2004h';
22
+ export const PASTE_DISABLE = '\x1b[?2004l';
23
+ export const FOCUS_ENABLE = '\x1b[?1004h';
24
+ export const FOCUS_DISABLE = '\x1b[?1004l';
@@ -0,0 +1,87 @@
1
+ /**
2
+ * theme-mode-config — the appearance/theme-mode preference surface (W4-R4).
3
+ *
4
+ * The preference lives at the config key `display.themeMode` (auto | dark |
5
+ * light, default auto). It is stored under the existing SDK `display` section
6
+ * (which exists in DEFAULT_CONFIG) as a TUI-local synthetic setting — no SDK
7
+ * schema change — the same pattern the config already uses for other
8
+ * display.* toggles.
9
+ *
10
+ * DIVERGENCE FROM THE W4 BRIEF (recorded): the brief listed theme-mode-config.ts
11
+ * as the NEW home for the config-read helpers, but W4-R2 landed FIRST and
12
+ * inlined resolveConfiguredThemeMode / coerceThemeModeSetting /
13
+ * THEME_MODE_CONFIG_KEY / ThemeModeSetting into terminal-bg-probe.ts (so R2
14
+ * could ship the probe before this file existed). Rather than duplicate those
15
+ * (which would let the two copies drift — the exact failure S1 exists to
16
+ * prevent), this module RE-EXPORTS them from terminal-bg-probe as the single
17
+ * source, and adds only the settings-modal-facing metadata (the value list,
18
+ * default, and human description) that R2 did not need.
19
+ *
20
+ * Deliberately free of terminal/probe state so the settings-modal data layer
21
+ * can import it without pulling in the stateful probe class.
22
+ */
23
+
24
+ import { setActiveThemeMode, type ThemeModeSetting } from './theme.ts';
25
+ import { coerceThemeModeSetting, THEME_MODE_CONFIG_KEY, THEME_MODE_DEFAULT } from './terminal-bg-probe.ts';
26
+ import type { ConfigKey, ConfigSetting } from '@pellux/goodvibes-sdk/platform/config';
27
+
28
+ export {
29
+ THEME_MODE_CONFIG_KEY,
30
+ THEME_MODE_DEFAULT,
31
+ coerceThemeModeSetting,
32
+ resolveConfiguredThemeMode,
33
+ } from './terminal-bg-probe.ts';
34
+
35
+ /** The three valid preference values, in settings-cycle order. */
36
+ export const THEME_MODE_VALUES: readonly ThemeModeSetting[] = ['auto', 'dark', 'light'];
37
+
38
+ /**
39
+ * Human-facing description for the settings-modal entry. States the honest
40
+ * timing contract: forced modes apply on the next full paint immediately;
41
+ * `auto` only re-probes at startup, so switching TO auto takes effect next
42
+ * launch.
43
+ */
44
+ export const THEME_MODE_DESCRIPTION =
45
+ 'Terminal background theme. auto probes the terminal background colour once at '
46
+ + 'startup (OSC 11) and picks light or dark; dark/light force a fixed theme. '
47
+ + 'Forced modes take effect immediately; auto is only evaluated at startup, so '
48
+ + 'selecting auto takes effect on the next launch. Unreadable/unsupported '
49
+ + 'terminals fall back to dark. Scope (honest): transcript markdown, tool-call '
50
+ + 'and system-message accents, and the header/footer/thinking chrome all flip '
51
+ + 'with this setting; only the background colour itself follows your terminal.';
52
+
53
+ /**
54
+ * The synthetic ConfigSetting descriptor for display.themeMode (the TUI's
55
+ * settings-modal-data.ts pattern). Agent-local — the key is NOT in the SDK
56
+ * ConfigKey union, so it is cast; safe because the `display` section exists in
57
+ * DEFAULT_CONFIG and ConfigManager.setDynamic/get round-trip unknown fields
58
+ * under an existing section. Injected into the settings modal's display group
59
+ * so the preference is discoverable and cycles like any other enum setting.
60
+ */
61
+ export const THEME_MODE_SYNTHETIC_SETTING: ConfigSetting = {
62
+ key: THEME_MODE_CONFIG_KEY as ConfigKey,
63
+ type: 'enum',
64
+ default: THEME_MODE_DEFAULT,
65
+ enumValues: [...THEME_MODE_VALUES],
66
+ description: THEME_MODE_DESCRIPTION,
67
+ };
68
+
69
+ /**
70
+ * Apply a display.themeMode settings change with the honest timing contract:
71
+ * forced dark/light flip the active mode NOW (caller passes its full-repaint
72
+ * hook); auto is only evaluated by the startup probe, so it takes effect on the
73
+ * next launch — the returned message states which happened. Called by the
74
+ * settings-modal onSettingApplied hook (ui-openers).
75
+ */
76
+ export function applyThemeModeSettingChange(
77
+ value: unknown,
78
+ requestFullRepaint?: () => void,
79
+ ): { message: string } {
80
+ const next = coerceThemeModeSetting(value);
81
+ if (next === 'dark' || next === 'light') {
82
+ setActiveThemeMode(next);
83
+ requestFullRepaint?.();
84
+ return { message: `Theme mode: ${next} (applied now)` };
85
+ }
86
+ return { message: 'Theme mode: auto (probes terminal on next startup)' };
87
+ }
@@ -0,0 +1,241 @@
1
+ /**
2
+ * theme.ts — Semantic colour token layer (W4-R4 port of the TUI theme system).
3
+ *
4
+ * Two token layers, resolved per background mode:
5
+ *
6
+ * - ThemeTokens (transcript): every colour decision in the markdown /
7
+ * conversation-rendering / tool-call / system-message pipeline. Agent-local
8
+ * (the SDK presentation contract hoisted only the CHROME tone table, not
9
+ * these transcript-semantic tokens). Read live per render via activeTheme()
10
+ * so a dark→light repaint re-resolves with no module reload.
11
+ *
12
+ * - UiToneTokens (chrome): the panel/modal/overlay/header/footer/thinking tone
13
+ * table. This IS the SDK presentation contract — resolveUiTones() / and
14
+ * activeUiTones() compose the ThemeMode dimension over the SDK's
15
+ * resolveTones(), so the agent never mints its own light chrome variant.
16
+ *
17
+ * Dark mode values are the historically used colours. Light mode values exist
18
+ * for correctness parity and are consumed once the terminal-bg-probe
19
+ * (terminal-escapes / OSC 11, W4-R2) resolves the mode and setActiveThemeMode
20
+ * is called. Callers that do not yet have mode detection get 'dark' — the safe
21
+ * default (activeMode starts dark).
22
+ *
23
+ * IMPORTANT: inline code has NO background token. The bg:#1a1a1a hardcode that
24
+ * previously existed caused a near-black box on light terminals. Differentiate
25
+ * inline code via inlineCodeFg + bold only; bg inherits the terminal.
26
+ */
27
+
28
+ import { UI_TONES } from './ui-primitives.ts';
29
+ import { resolveTones, type ThemeMode, type ToneTokens } from '@pellux/goodvibes-sdk/platform/presentation';
30
+
31
+ /** Background mode — dark is the safe default until the bg-probe resolves. */
32
+ export type { ThemeMode };
33
+
34
+ /** Resolved semantic colour tokens (concrete hex strings or ANSI-256 indices). */
35
+ export interface ThemeTokens {
36
+ /** H1 heading foreground + table header accent */
37
+ heading1: string;
38
+ /** H2 heading foreground */
39
+ heading2: string;
40
+ /** H3 heading foreground (ANSI-256 — falls back to nearest on ansi256 terminals) */
41
+ heading3: string;
42
+ /** Inline code foreground (bold is applied separately by caller) */
43
+ inlineCodeFg: string;
44
+ /** Hyperlink and bare-URL foreground */
45
+ link: string;
46
+ /** Non-current search match background */
47
+ searchMatchBg: string;
48
+ /** Non-current search match foreground */
49
+ searchMatchFg: string;
50
+ /** Current (focused) search match background */
51
+ searchCurrentBg: string;
52
+ /** Current (focused) search match foreground */
53
+ searchCurrentFg: string;
54
+ /** Strikethrough / muted text foreground */
55
+ strikethrough: string;
56
+ /** Blockquote / dim text foreground */
57
+ blockquote: string;
58
+ /** Assistant event-line marker + label accent */
59
+ assistantHeader: string;
60
+ /** Reasoning / thinking block accent */
61
+ reasoningAccent: string;
62
+ /** Tool call / active status accent (also diff/tool result label) */
63
+ toolAccent: string;
64
+ /** Collapsed-fragment body background (tool result preview bg) */
65
+ collapsedBodyBg: string;
66
+ /** Checked task-list checkbox foreground (✓ in green) */
67
+ checkboxChecked: string;
68
+ /** Error / cancelled message bar background */
69
+ errorBarBg: string;
70
+ /** Model name / provider dim label foreground */
71
+ modelNameDim: string;
72
+ /** Tool name foreground in tool-result event line */
73
+ toolNameFg: string;
74
+ /** Diff block accent — marker, label, and collapsed-prefix foreground */
75
+ diffAccent: string;
76
+ }
77
+
78
+ // ---------------------------------------------------------------------------
79
+ // Dark palette (byte-identical to the agent's prior static reads).
80
+ // ---------------------------------------------------------------------------
81
+ const DARK: ThemeTokens = {
82
+ heading1: '#00ffff',
83
+ heading2: '#00ffff',
84
+ heading3: '111',
85
+ inlineCodeFg: '#ffcc00',
86
+ link: '#00aaff',
87
+ searchMatchBg: '#806600',
88
+ searchMatchFg: '#ffffff',
89
+ searchCurrentBg: '#ffff00',
90
+ searchCurrentFg: '#000000',
91
+ strikethrough: '244',
92
+ blockquote: '244',
93
+ assistantHeader: UI_TONES.accent.control,
94
+ reasoningAccent: UI_TONES.state.reasoning,
95
+ toolAccent: UI_TONES.state.info,
96
+ collapsedBodyBg: '#1a1a1a',
97
+ checkboxChecked: '#22c55e',
98
+ errorBarBg: '#3a1a1a',
99
+ modelNameDim: '#94a3b8',
100
+ toolNameFg: '#e2e8f0',
101
+ diffAccent: '#f59e0b',
102
+ };
103
+
104
+ // ---------------------------------------------------------------------------
105
+ // Light palette — dark-on-light legibility (contrast ratios against #ffffff):
106
+ // heading1/2: Deep teal (#0077aa) — readable on white/cream terminals
107
+ // heading3: ANSI-256 24 (dark cyan) — the light equivalent of 111
108
+ // inlineCodeFg: Dark orange (#b45309) — distinguishable without a box bg
109
+ // link: Standard blue (#0055cc) — browser-link convention
110
+ // searchMatchBg: Muted yellow (#ffe066) on black fg — visible on light bg
111
+ // searchCurrentBg: Strong amber (#f59e0b) — current match is more vivid
112
+ // blockquote: Dim blue-gray (ANSI-256 67)
113
+ // assistantHeader: Dark cyan (#0e7490)
114
+ // reasoningAccent: Dark purple (#7c3aed)
115
+ // toolAccent: Dark sky (#0369a1)
116
+ // collapsedBodyBg: Very light gray (#f3f4f6)
117
+ // checkboxChecked: Forest green (#15803d) — ~5.2:1 on #fff
118
+ // errorBarBg: Soft rose (#fee2e2) — legible text on top
119
+ // modelNameDim: Slate-500 (#64748b) — ~4.6:1 on #fff
120
+ // toolNameFg: Slate-800 (#334155) — strong enough for tool names
121
+ // diffAccent: Amber-700 (#b45309) — ~4.7:1 on #fff
122
+ // ---------------------------------------------------------------------------
123
+ const LIGHT: ThemeTokens = {
124
+ heading1: '#0077aa',
125
+ heading2: '#0077aa',
126
+ heading3: '24',
127
+ inlineCodeFg: '#b45309',
128
+ link: '#0055cc',
129
+ searchMatchBg: '#ffe066',
130
+ searchMatchFg: '#000000',
131
+ searchCurrentBg: '#f59e0b',
132
+ searchCurrentFg: '#000000',
133
+ strikethrough: '244',
134
+ blockquote: '67',
135
+ assistantHeader: '#0e7490',
136
+ reasoningAccent: '#7c3aed',
137
+ toolAccent: '#0369a1',
138
+ collapsedBodyBg: '#f3f4f6',
139
+ checkboxChecked: '#15803d',
140
+ errorBarBg: '#fee2e2',
141
+ modelNameDim: '#64748b',
142
+ toolNameFg: '#334155',
143
+ diffAccent: '#b45309',
144
+ };
145
+
146
+ Object.freeze(DARK);
147
+ Object.freeze(LIGHT);
148
+
149
+ /**
150
+ * resolveTheme — Return the transcript token set for the given background mode.
151
+ * The returned object is frozen; callers should not mutate it.
152
+ */
153
+ export function resolveTheme(mode: ThemeMode): Readonly<ThemeTokens> {
154
+ return mode === 'light' ? LIGHT : DARK;
155
+ }
156
+
157
+ /** Default dark-mode token set, exported for convenience. Frozen. */
158
+ export const DARK_THEME: Readonly<ThemeTokens> = DARK;
159
+
160
+ // ---------------------------------------------------------------------------
161
+ // Chrome tokens (UiToneTokens) — the SDK presentation contract, mode-resolved.
162
+ //
163
+ // The dark table is the SDK's TONE_TOKENS (re-exported here as UI_TONES); the
164
+ // light variant is the SDK's resolveTones('light'). The agent does NOT mint its
165
+ // own light chrome variant — that duplication is exactly what W4-S1 ended.
166
+ // ---------------------------------------------------------------------------
167
+
168
+ /** The chrome tone-token shape (the SDK ToneTokens contract). */
169
+ export type UiToneTokens = ToneTokens;
170
+
171
+ /**
172
+ * resolveUiTones — chrome token set for the given mode. Single read path;
173
+ * 'dark' is byte-identical to the UI_TONES constant (same SDK object).
174
+ * Prefer activeUiTones() at call sites.
175
+ */
176
+ export function resolveUiTones(mode: ThemeMode): Readonly<UiToneTokens> {
177
+ return resolveTones(mode);
178
+ }
179
+
180
+ // ===========================================================================
181
+ // Active-mode runtime.
182
+ //
183
+ // The mode is decided ONCE at startup — from appearance config
184
+ // (display.themeMode forced dark/light) or the terminal-background probe
185
+ // (auto) — and is then stable for the session. Transcript tokens (activeTheme)
186
+ // and chrome tokens (activeUiTones) are both read live per render, so a
187
+ // dark→light repaint (auto mode, light wins within the probe window)
188
+ // re-resolves without any module reload.
189
+ //
190
+ // registerThemeRefresh exists for owners that BAKE tone values into
191
+ // module-level constants (which cannot be re-resolved per call). Nothing
192
+ // registers this wave — the agent's opaque panel palettes (polish.ts
193
+ // DEFAULT_PANEL_PALETTE and the modal/overlay/fullscreen surfaces built from
194
+ // it) paint OPAQUE dark boxes whose fg/state tokens stay dark in both modes, so
195
+ // they need no rebuild for dark parity (the opaque-surface trio deferral). The
196
+ // hook is kept at parity with the TUI so those surfaces can register when full
197
+ // light-chrome coherence lands.
198
+ // ===========================================================================
199
+
200
+ /** User-facing appearance preference: auto probes the terminal; dark/light force. */
201
+ export type ThemeModeSetting = 'auto' | ThemeMode;
202
+
203
+ /** The resolved mode in effect for the current session. Dark is the safe default. */
204
+ let activeMode: ThemeMode = 'dark';
205
+
206
+ /** In-place palette rebuilders, run (in registration order) on every mode flip. */
207
+ const themeRefreshers: Array<() => void> = [];
208
+
209
+ /** The active mode, for diagnostics / tests. */
210
+ export function getActiveThemeMode(): ThemeMode {
211
+ return activeMode;
212
+ }
213
+
214
+ /**
215
+ * Register an in-place palette rebuild to run whenever the active mode changes.
216
+ * Base-palette owners register at their own module-eval time (before any
217
+ * derived palette), so refreshers run base-first.
218
+ */
219
+ export function registerThemeRefresh(rebuild: () => void): void {
220
+ themeRefreshers.push(rebuild);
221
+ }
222
+
223
+ /**
224
+ * Set the active background mode and rebuild every registered chrome palette in
225
+ * place. Idempotent and reversible. Callers: startup (forced mode or probe
226
+ * result, wired via installBackgroundThemeProbe's applyThemeMode injection).
227
+ */
228
+ export function setActiveThemeMode(mode: ThemeMode): void {
229
+ activeMode = mode;
230
+ for (const rebuild of themeRefreshers) rebuild();
231
+ }
232
+
233
+ /** Transcript tokens for the active mode — read live, per render. */
234
+ export function activeTheme(): Readonly<ThemeTokens> {
235
+ return resolveTheme(activeMode);
236
+ }
237
+
238
+ /** Chrome tokens for the active mode — read live, per render. */
239
+ export function activeUiTones(): Readonly<UiToneTokens> {
240
+ return resolveUiTones(activeMode);
241
+ }
@@ -1,14 +1,23 @@
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
  export function renderThinkingBlock(text: string, width: number): Line[] {
7
+ // Thinking notices paint the ▌ marker and italic body on the TRANSPARENT
8
+ // terminal background (renderConversationNotice passes no bodyBg), so both
9
+ // colours resolve per-render through activeUiTones() to stay legible on a
10
+ // light terminal. In dark mode the accent adopts the shared reasoning purple
11
+ // (state.reasoning) and the body adopts chrome.faint (== fg.dim) — a small,
12
+ // deliberate convergence to the reference tokens from the agent's prior local
13
+ // BORDERS.THINKING.color / COLORS.DIM_TEXT (see the W4-R4 visible-changes note).
14
+ const t = activeUiTones();
6
15
  return renderConversationNotice(
7
16
  text,
8
17
  width,
9
18
  {
10
- accent: BORDERS.THINKING.color,
11
- text: COLORS.DIM_TEXT,
19
+ accent: t.state.reasoning,
20
+ text: t.chrome.faint,
12
21
  dim: true,
13
22
  italic: true,
14
23
  },
@@ -4,6 +4,7 @@ import { getDisplayWidth, truncateDisplay } from '../utils/terminal-width.ts';
4
4
  import type { ToolCall } from '@pellux/goodvibes-sdk/platform/types';
5
5
  import { stripDangerousAnsi } from './ansi-sanitize.ts';
6
6
  import { friendlyToolLabel } from './tool-labels.ts';
7
+ import { activeUiTones } from './theme.ts';
7
8
 
8
9
  const TOOL_NAME_MIN_WIDTH = 8;
9
10
  const TOOL_NAME_MAX_WIDTH = 30;
@@ -55,6 +56,9 @@ function buildLeftSegments(
55
56
  ): Array<{ text: string; fg: string; bold?: boolean; dim?: boolean }> {
56
57
  if (leftBudget <= 0) return [];
57
58
 
59
+ // Read live tones so the tool-call row is legible in light mode. Dark values
60
+ // are byte-identical (t.fg.primary == #e2e8f0, t.chrome.bad == #ef4444).
61
+ const t = activeUiTones();
58
62
  const segments: Array<{ text: string; fg: string; bold?: boolean; dim?: boolean }> = [];
59
63
  const suffixBudget = suffixText ? Math.min(Math.max(12, Math.floor(leftBudget * 0.3)), 20) : 0;
60
64
  const suffixDisplay = suffixBudget > 0 ? truncateDisplay(suffixText, suffixBudget) : '';
@@ -83,14 +87,14 @@ function buildLeftSegments(
83
87
  segments.push({ text: toolNameDisplay, fg: '#00ffcc', bold: true });
84
88
  }
85
89
  if (keyArgDisplay) {
86
- segments.push({ text: ' ', fg: '#e2e8f0' });
90
+ segments.push({ text: ' ', fg: t.fg.primary });
87
91
  segments.push({ text: keyArgDisplay, fg: '252' });
88
92
  }
89
93
  if (suffixDisplay) {
90
- segments.push({ text: ' ', fg: '#e2e8f0' });
94
+ segments.push({ text: ' ', fg: t.fg.primary });
91
95
  segments.push({
92
96
  text: suffixDisplay,
93
- fg: suffixText.startsWith('- ') ? '#ef4444' : '244',
97
+ fg: suffixText.startsWith('- ') ? t.chrome.bad : '244',
94
98
  dim: true,
95
99
  });
96
100
  }
@@ -4,11 +4,34 @@ import { VERSION } from '../version.ts';
4
4
  import { fitDisplay, getDisplayWidth, truncateDisplay, wrapText, interpolateColor } from '../utils/terminal-width.ts';
5
5
  import { renderConversationFragment, renderConversationStatusLine, type ConversationStatusSegment } from './conversation-surface.ts';
6
6
  import { GLYPHS } from './ui-primitives.ts';
7
+ import { activeUiTones } from './theme.ts';
8
+ import {
9
+ THINKING_PHRASES,
10
+ waitingPhrase,
11
+ type WaitingState,
12
+ } from '@pellux/goodvibes-sdk/platform/presentation';
7
13
 
8
14
  /** Number of frames before the animated gradient completes one full cycle. */
9
15
  const GRADIENT_CYCLE_FRAMES = 50;
10
16
  /** Number of frames before rotating to the next thinking phrase (~30 seconds at 80ms/frame). */
11
17
  const PHRASE_ROTATION_FRAMES = 375;
18
+ /**
19
+ * Silence threshold before the whimsical phrase rotation freezes and an honest
20
+ * label (Waiting for model Ns / Stalled Ns) takes over. Matches the TUI.
21
+ */
22
+ const THINKING_STALL_FREEZE_MS = 2_500;
23
+
24
+ /**
25
+ * Per-turn stall signal derived from stream metrics — computed from the last
26
+ * delta clock every render (not from any event), so it degrades gracefully with
27
+ * zero new SDK events. `reconnect` is set only when the transport surfaces retry
28
+ * counters (the agent's SDK orchestrator does not today — see computeStallInfo).
29
+ */
30
+ export interface ThinkingStallInfo {
31
+ /** Ms since the last output-token advance (or the turn start if none yet). */
32
+ readonly msSinceLastDelta: number;
33
+ readonly reconnect?: { readonly attempt: number; readonly maxAttempts: number };
34
+ }
12
35
 
13
36
  /** Format a number: up to 999, then 1.0k, 1.0M, 1.0B, 1.0T */
14
37
  function fmtNum(n: number): string {
@@ -236,9 +259,12 @@ export class UIFactory {
236
259
  if (inp > 0 || out > 0) {
237
260
  statusTokens.push({ text: `↑${fmtNum(inp)} ↓${fmtNum(out)}`, fg: '240' });
238
261
  }
239
- if (composerPendingRisk === 'approval-wait') {
240
- statusTokens.push({ text: 'waiting for your approval', fg: '#f59e0b', bold: true });
241
- }
262
+ // W4-R4: the disconnected footer 'waiting for your approval' token is retired
263
+ // here the approval-wait truth now lives in the unified waiting state of the
264
+ // thinking indicator (createThinkingFragment's approvalPending path) and in the
265
+ // permission prompt itself, so the footer no longer carries a separate,
266
+ // easily-desynced copy. composerPendingRisk stays in the signature for the
267
+ // composer flags row; it just no longer mints its own status token.
242
268
  const rightNotice = isRecentlyCopied
243
269
  ? { text: `copied ${GLYPHS.status.success} `, fg: '81', bold: true }
244
270
  : dangerMode
@@ -277,39 +303,69 @@ export class UIFactory {
277
303
  return lines;
278
304
  }
279
305
 
280
- /** Rotating thinking phrases — vaporwave / good vibes themed. */
281
- private static readonly THINKING_PHRASES = [
282
- 'Thinking...',
283
- 'Vibing...',
284
- 'Manifesting...',
285
- 'Channeling energy...',
286
- 'Tuning frequencies...',
287
- 'Riding the wave...',
288
- 'Aligning chakras...',
289
- 'Entering flow state...',
290
- 'Consulting the void...',
291
- 'Absorbing aesthetics...',
292
- 'Synthesizing vibes...',
293
- 'Transcending...',
294
- 'Dreaming in neon...',
295
- 'Parsing the cosmos...',
296
- 'Loading good vibes...',
297
- 'Meditating...',
298
- 'Catching a vibe...',
299
- 'Harmonizing...',
300
- 'Feeling it...',
301
- 'In the zone...',
302
- ];
306
+ /**
307
+ * Per-frame stall info from stream metrics — computed from a last-delta clock
308
+ * every render (not from any event) so it degrades gracefully with zero new
309
+ * SDK events. Undefined until a delta clock exists this turn. Renderer-local
310
+ * by design (per the S1 decision record: the SDK owns state->wording; deriving
311
+ * WHICH state applies stays with each renderer's own stream-metrics shape).
312
+ */
313
+ public static computeStallInfo(
314
+ lastDeltaAtMs: number | undefined,
315
+ reconnectAttempt: number | undefined,
316
+ reconnectMaxAttempts: number | undefined,
317
+ nowMs: number,
318
+ ): ThinkingStallInfo | undefined {
319
+ if (lastDeltaAtMs === undefined) return undefined;
320
+ const reconnect = reconnectAttempt !== undefined && reconnectMaxAttempts !== undefined
321
+ ? { attempt: reconnectAttempt, maxAttempts: reconnectMaxAttempts }
322
+ : undefined;
323
+ return { msSinceLastDelta: nowMs - lastDeltaAtMs, reconnect };
324
+ }
303
325
 
304
- /** Gradient colors for thinking text — cyan to purple (matches splash). */
305
- private static readonly THINK_GRADIENT_START = '#00ffff';
306
- private static readonly THINK_GRADIENT_END = '#d000ff';
326
+ /**
327
+ * Render-loop stall decision: suppress stall detection entirely while a tool
328
+ * is actively executing. The last-delta clock only advances on output-token
329
+ * deltas and is never advanced during tool execution (the model isn't
330
+ * producing tokens then), so without this gate any tool call longer than
331
+ * THINKING_STALL_FREEZE_MS would print "Stalled Ns..." above a ticking tool
332
+ * row — a false positive. Genuine no-delta silence while waiting on the
333
+ * provider (including pre-first-token) still stall-detects here, since no tool
334
+ * is active then — the honest stall case this indicator exists for.
335
+ */
336
+ public static computeRenderStallInfo(
337
+ metrics: { toolActive: boolean; lastDeltaAtMs: number | undefined; nowMs: number },
338
+ ): ThinkingStallInfo | undefined {
339
+ return metrics.toolActive
340
+ ? undefined
341
+ : this.computeStallInfo(metrics.lastDeltaAtMs, undefined, undefined, metrics.nowMs);
342
+ }
307
343
 
308
- public static createThinkingFragment(width: number, spinner: string, frame: number = 0, tokenSpeed?: number, toolPreview?: string, inputTokens?: number, outputTokens?: number): Line[] {
309
- // Rotate phrase every ~30 seconds (frame ticks at 80ms, so ~375 frames)
310
- const phraseIndex = Math.floor(frame / PHRASE_ROTATION_FRAMES) % this.THINKING_PHRASES.length;
311
- const phrase = this.THINKING_PHRASES[phraseIndex];
312
- const speedSuffix = (tokenSpeed !== undefined && tokenSpeed > 0) ? ` (${Math.round(tokenSpeed)} tok/s)` : '';
344
+ public static createThinkingFragment(width: number, spinner: string, frame: number = 0, tokenSpeed?: number, toolPreview?: string, inputTokens?: number, outputTokens?: number, stallInfo?: ThinkingStallInfo, approvalPending?: boolean): Line[] {
345
+ // Live thinking row paints on the transparent terminal bg read the
346
+ // mode-resolved chrome tones per render (gradient/brand flip in light mode).
347
+ const tones = activeUiTones();
348
+ // Decide WHICH honest waiting state applies (renderer-local), then defer the
349
+ // exact wording to the SDK presentation contract's waitingPhrase(). Precedence
350
+ // matches the contract: approval > reconnecting > pre-first-token > stalled >
351
+ // thinking. Pre-first-token silence reads "Waiting for model Ns..." (blame-free
352
+ // for reasoning models that think before emitting), NOT "Stalled".
353
+ const isStalled = stallInfo !== undefined && stallInfo.msSinceLastDelta >= THINKING_STALL_FREEZE_MS;
354
+ let state: WaitingState;
355
+ if (approvalPending) state = 'approval';
356
+ else if (stallInfo?.reconnect) state = 'reconnecting';
357
+ else if (isStalled && (outputTokens ?? 0) === 0) state = 'pre-first-token';
358
+ else if (isStalled) state = 'stalled';
359
+ else state = 'thinking';
360
+ const phrase = waitingPhrase(state, {
361
+ reconnectAttempt: stallInfo?.reconnect?.attempt,
362
+ reconnectMaxAttempts: stallInfo?.reconnect?.maxAttempts,
363
+ msSinceLastDelta: stallInfo?.msSinceLastDelta,
364
+ frame,
365
+ });
366
+ // A tok/s figure next to an approval prompt reads as if the model were still
367
+ // working — suppress it while waiting on the user.
368
+ const speedSuffix = (!approvalPending && tokenSpeed !== undefined && tokenSpeed > 0) ? ` (${Math.round(tokenSpeed)} tok/s)` : '';
313
369
  const text = ` ${spinner} ${phrase}${speedSuffix} `;
314
370
 
315
371
  const textWidth = Math.max(1, getDisplayWidth(text) - 1);
@@ -319,7 +375,7 @@ export class UIFactory {
319
375
  const gradientPos = raw <= 0.5 ? raw * 2 : (1 - raw) * 2;
320
376
  return {
321
377
  text: char,
322
- fg: interpolateColor(this.THINK_GRADIENT_START, this.THINK_GRADIENT_END, gradientPos),
378
+ fg: interpolateColor(tones.accent.gradientStart, tones.accent.gradientEnd, gradientPos),
323
379
  bold: true,
324
380
  };
325
381
  });
@@ -327,7 +383,7 @@ export class UIFactory {
327
383
  const inTok = inputTokens ?? 0;
328
384
  const outTok = outputTokens ?? 0;
329
385
  segments.push({ text: ` in ${fmtNum(inTok)} `, fg: '243', dim: true });
330
- segments.push({ text: `out ${fmtNum(outTok)}`, fg: '#00ffff' });
386
+ segments.push({ text: `out ${fmtNum(outTok)}`, fg: tones.accent.brand });
331
387
  }
332
388
  const line = createEmptyLine(width);
333
389
  let col = 1;