@jmoyers/harness 0.1.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 (214) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +145 -0
  3. package/native/ptyd/Cargo.lock +16 -0
  4. package/native/ptyd/Cargo.toml +7 -0
  5. package/native/ptyd/src/main.rs +257 -0
  6. package/package.json +90 -0
  7. package/scripts/build-ptyd.sh +73 -0
  8. package/scripts/control-plane-daemon.ts +277 -0
  9. package/scripts/cursor-hook-relay.ts +82 -0
  10. package/scripts/harness-animate.ts +469 -0
  11. package/scripts/harness-bin.js +77 -0
  12. package/scripts/harness-core.ts +1 -0
  13. package/scripts/harness-inspector.ts +439 -0
  14. package/scripts/harness.ts +2493 -0
  15. package/src/adapters/agent-session-state.ts +390 -0
  16. package/src/cli/gateway-record.ts +173 -0
  17. package/src/codex/live-session.ts +872 -0
  18. package/src/config/config-core.ts +1359 -0
  19. package/src/config/secrets-core.ts +170 -0
  20. package/src/control-plane/agent-realtime-api.ts +2441 -0
  21. package/src/control-plane/codex-session-stream.ts +392 -0
  22. package/src/control-plane/codex-telemetry.ts +1325 -0
  23. package/src/control-plane/lifecycle-hooks.ts +706 -0
  24. package/src/control-plane/session-summary.ts +380 -0
  25. package/src/control-plane/status/agent-status-reducer.ts +21 -0
  26. package/src/control-plane/status/reducer-base.ts +170 -0
  27. package/src/control-plane/status/reducers/claude-status-reducer.ts +37 -0
  28. package/src/control-plane/status/reducers/codex-status-reducer.ts +48 -0
  29. package/src/control-plane/status/reducers/critique-status-reducer.ts +15 -0
  30. package/src/control-plane/status/reducers/cursor-status-reducer.ts +37 -0
  31. package/src/control-plane/status/reducers/terminal-status-reducer.ts +15 -0
  32. package/src/control-plane/status/session-status-engine.ts +76 -0
  33. package/src/control-plane/stream-client.ts +396 -0
  34. package/src/control-plane/stream-command-parser.ts +1673 -0
  35. package/src/control-plane/stream-protocol.ts +1808 -0
  36. package/src/control-plane/stream-server-background.ts +486 -0
  37. package/src/control-plane/stream-server-command.ts +2557 -0
  38. package/src/control-plane/stream-server-connection.ts +234 -0
  39. package/src/control-plane/stream-server-observed-filter.ts +112 -0
  40. package/src/control-plane/stream-server-session-runtime.ts +566 -0
  41. package/src/control-plane/stream-server-state-store.ts +15 -0
  42. package/src/control-plane/stream-server.ts +3192 -0
  43. package/src/cursor/managed-hooks.ts +282 -0
  44. package/src/domain/conversations.ts +414 -0
  45. package/src/domain/directories.ts +78 -0
  46. package/src/domain/repositories.ts +123 -0
  47. package/src/domain/tasks.ts +148 -0
  48. package/src/domain/workspace.ts +156 -0
  49. package/src/events/normalized-events.ts +124 -0
  50. package/src/mux/ansi-integrity.ts +103 -0
  51. package/src/mux/control-plane-op-queue.ts +212 -0
  52. package/src/mux/conversation-rail.ts +339 -0
  53. package/src/mux/double-click.ts +78 -0
  54. package/src/mux/dual-pane-core.ts +435 -0
  55. package/src/mux/harness-core-ui.ts +817 -0
  56. package/src/mux/input-shortcuts.ts +667 -0
  57. package/src/mux/live-mux/actions-conversation.ts +344 -0
  58. package/src/mux/live-mux/actions-repository.ts +246 -0
  59. package/src/mux/live-mux/actions-task.ts +115 -0
  60. package/src/mux/live-mux/args.ts +142 -0
  61. package/src/mux/live-mux/command-menu.ts +298 -0
  62. package/src/mux/live-mux/control-plane-records.ts +546 -0
  63. package/src/mux/live-mux/conversation-state.ts +188 -0
  64. package/src/mux/live-mux/directory-resolution.ts +34 -0
  65. package/src/mux/live-mux/event-mapping.ts +96 -0
  66. package/src/mux/live-mux/gateway-profiler.ts +152 -0
  67. package/src/mux/live-mux/gateway-render-trace.ts +177 -0
  68. package/src/mux/live-mux/gateway-status-timeline.ts +166 -0
  69. package/src/mux/live-mux/git-parsing.ts +131 -0
  70. package/src/mux/live-mux/git-snapshot.ts +263 -0
  71. package/src/mux/live-mux/git-state.ts +136 -0
  72. package/src/mux/live-mux/global-shortcut-handlers.ts +143 -0
  73. package/src/mux/live-mux/home-pane-actions.ts +58 -0
  74. package/src/mux/live-mux/home-pane-drop.ts +44 -0
  75. package/src/mux/live-mux/home-pane-entity-click.ts +96 -0
  76. package/src/mux/live-mux/home-pane-pointer.ts +96 -0
  77. package/src/mux/live-mux/input-forwarding.ts +112 -0
  78. package/src/mux/live-mux/layout.ts +30 -0
  79. package/src/mux/live-mux/left-nav-activation.ts +103 -0
  80. package/src/mux/live-mux/left-nav.ts +85 -0
  81. package/src/mux/live-mux/left-rail-actions.ts +118 -0
  82. package/src/mux/live-mux/left-rail-conversation-click.ts +82 -0
  83. package/src/mux/live-mux/left-rail-pointer.ts +74 -0
  84. package/src/mux/live-mux/modal-command-menu-handler.ts +101 -0
  85. package/src/mux/live-mux/modal-conversation-handlers.ts +217 -0
  86. package/src/mux/live-mux/modal-input-reducers.ts +94 -0
  87. package/src/mux/live-mux/modal-overlays.ts +287 -0
  88. package/src/mux/live-mux/modal-pointer.ts +70 -0
  89. package/src/mux/live-mux/modal-prompt-handlers.ts +187 -0
  90. package/src/mux/live-mux/modal-task-editor-handler.ts +156 -0
  91. package/src/mux/live-mux/observed-stream.ts +87 -0
  92. package/src/mux/live-mux/palette-parsing.ts +128 -0
  93. package/src/mux/live-mux/pointer-routing.ts +108 -0
  94. package/src/mux/live-mux/process-usage.ts +53 -0
  95. package/src/mux/live-mux/project-pane-pointer.ts +44 -0
  96. package/src/mux/live-mux/rail-layout.ts +244 -0
  97. package/src/mux/live-mux/render-trace-analysis.ts +213 -0
  98. package/src/mux/live-mux/render-trace-state.ts +84 -0
  99. package/src/mux/live-mux/repository-folding.ts +207 -0
  100. package/src/mux/live-mux/runtime-shutdown.ts +51 -0
  101. package/src/mux/live-mux/selection.ts +411 -0
  102. package/src/mux/live-mux/startup-utils.ts +187 -0
  103. package/src/mux/live-mux/status-timeline-state.ts +82 -0
  104. package/src/mux/live-mux/task-pane-shortcuts.ts +206 -0
  105. package/src/mux/live-mux/terminal-palette.ts +79 -0
  106. package/src/mux/new-thread-prompt.ts +165 -0
  107. package/src/mux/project-tree.ts +295 -0
  108. package/src/mux/render-frame.ts +113 -0
  109. package/src/mux/runtime-wiring.ts +185 -0
  110. package/src/mux/selector-index.ts +160 -0
  111. package/src/mux/startup-sequencer.ts +238 -0
  112. package/src/mux/task-composer.ts +289 -0
  113. package/src/mux/task-focused-pane.ts +417 -0
  114. package/src/mux/task-screen-keybindings.ts +539 -0
  115. package/src/mux/terminal-input-modes.ts +35 -0
  116. package/src/mux/workspace-path.ts +55 -0
  117. package/src/mux/workspace-rail-model.ts +701 -0
  118. package/src/mux/workspace-rail.ts +247 -0
  119. package/src/perf/perf-core.ts +307 -0
  120. package/src/pty/pty_host.ts +217 -0
  121. package/src/pty/session-broker.ts +158 -0
  122. package/src/recording/terminal-recording.ts +383 -0
  123. package/src/services/control-plane.ts +567 -0
  124. package/src/services/conversation-lifecycle.ts +176 -0
  125. package/src/services/conversation-startup-hydration.ts +47 -0
  126. package/src/services/directory-hydration.ts +49 -0
  127. package/src/services/event-persistence.ts +104 -0
  128. package/src/services/mux-ui-state-persistence.ts +82 -0
  129. package/src/services/output-load-sampler.ts +231 -0
  130. package/src/services/process-usage-refresh.ts +88 -0
  131. package/src/services/recording.ts +75 -0
  132. package/src/services/render-trace-recorder.ts +177 -0
  133. package/src/services/runtime-control-actions.ts +123 -0
  134. package/src/services/runtime-control-plane-ops.ts +131 -0
  135. package/src/services/runtime-conversation-actions.ts +113 -0
  136. package/src/services/runtime-conversation-activation.ts +78 -0
  137. package/src/services/runtime-conversation-starter.ts +171 -0
  138. package/src/services/runtime-conversation-title-edit.ts +149 -0
  139. package/src/services/runtime-directory-actions.ts +164 -0
  140. package/src/services/runtime-envelope-handler.ts +198 -0
  141. package/src/services/runtime-git-state.ts +92 -0
  142. package/src/services/runtime-input-pipeline.ts +50 -0
  143. package/src/services/runtime-input-router.ts +202 -0
  144. package/src/services/runtime-layout-resize.ts +236 -0
  145. package/src/services/runtime-left-rail-render.ts +159 -0
  146. package/src/services/runtime-main-pane-input.ts +230 -0
  147. package/src/services/runtime-modal-input.ts +119 -0
  148. package/src/services/runtime-navigation-input.ts +207 -0
  149. package/src/services/runtime-process-wiring.ts +68 -0
  150. package/src/services/runtime-rail-input.ts +287 -0
  151. package/src/services/runtime-render-flush.ts +146 -0
  152. package/src/services/runtime-render-lifecycle.ts +104 -0
  153. package/src/services/runtime-render-orchestrator.ts +108 -0
  154. package/src/services/runtime-render-pipeline.ts +167 -0
  155. package/src/services/runtime-render-state.ts +72 -0
  156. package/src/services/runtime-repository-actions.ts +197 -0
  157. package/src/services/runtime-right-pane-render.ts +132 -0
  158. package/src/services/runtime-shutdown.ts +79 -0
  159. package/src/services/runtime-stream-subscriptions.ts +56 -0
  160. package/src/services/runtime-task-composer-persistence.ts +139 -0
  161. package/src/services/runtime-task-editor-actions.ts +83 -0
  162. package/src/services/runtime-task-pane-actions.ts +198 -0
  163. package/src/services/runtime-task-pane-shortcuts.ts +189 -0
  164. package/src/services/runtime-task-pane.ts +62 -0
  165. package/src/services/runtime-workspace-actions.ts +153 -0
  166. package/src/services/runtime-workspace-observed-events.ts +190 -0
  167. package/src/services/session-projection-instrumentation.ts +190 -0
  168. package/src/services/startup-background-probe.ts +91 -0
  169. package/src/services/startup-background-resume.ts +65 -0
  170. package/src/services/startup-orchestrator.ts +166 -0
  171. package/src/services/startup-output-tracker.ts +54 -0
  172. package/src/services/startup-paint-tracker.ts +115 -0
  173. package/src/services/startup-persisted-conversation-queue.ts +45 -0
  174. package/src/services/startup-settled-gate.ts +67 -0
  175. package/src/services/startup-shutdown.ts +53 -0
  176. package/src/services/startup-span-tracker.ts +77 -0
  177. package/src/services/startup-state-hydration.ts +94 -0
  178. package/src/services/startup-visibility.ts +35 -0
  179. package/src/services/status-timeline-recorder.ts +144 -0
  180. package/src/services/task-pane-selection-actions.ts +153 -0
  181. package/src/services/task-planning-hydration.ts +58 -0
  182. package/src/services/task-planning-observed-events.ts +89 -0
  183. package/src/services/workspace-observed-events.ts +113 -0
  184. package/src/store/control-plane-store-normalize.ts +760 -0
  185. package/src/store/control-plane-store-types.ts +224 -0
  186. package/src/store/control-plane-store.ts +2951 -0
  187. package/src/store/event-store.ts +253 -0
  188. package/src/store/sqlite.ts +81 -0
  189. package/src/terminal/compat-matrix.ts +345 -0
  190. package/src/terminal/differential-checkpoints.ts +132 -0
  191. package/src/terminal/parity-suite.ts +441 -0
  192. package/src/terminal/snapshot-oracle.ts +1840 -0
  193. package/src/ui/conversation-input-forwarder.ts +114 -0
  194. package/src/ui/conversation-selection-input.ts +103 -0
  195. package/src/ui/debug-footer-notice.ts +39 -0
  196. package/src/ui/global-shortcut-input.ts +126 -0
  197. package/src/ui/input-preflight.ts +68 -0
  198. package/src/ui/input-token-router.ts +312 -0
  199. package/src/ui/input.ts +238 -0
  200. package/src/ui/kit.ts +509 -0
  201. package/src/ui/left-nav-input.ts +80 -0
  202. package/src/ui/left-rail-pointer-input.ts +148 -0
  203. package/src/ui/main-pane-pointer-input.ts +150 -0
  204. package/src/ui/modals/manager.ts +192 -0
  205. package/src/ui/mux-theme.ts +529 -0
  206. package/src/ui/panes/conversation.ts +19 -0
  207. package/src/ui/panes/home-gridfire.ts +302 -0
  208. package/src/ui/panes/home.ts +109 -0
  209. package/src/ui/panes/left-rail.ts +12 -0
  210. package/src/ui/panes/project.ts +44 -0
  211. package/src/ui/pointer-routing-input.ts +158 -0
  212. package/src/ui/repository-fold-input.ts +91 -0
  213. package/src/ui/screen.ts +210 -0
  214. package/src/ui/surface.ts +224 -0
@@ -0,0 +1,529 @@
1
+ import { readFileSync } from 'node:fs';
2
+ import { resolve } from 'node:path';
3
+ import type { HarnessMuxThemeConfig } from '../config/config-core.ts';
4
+ import type { UiColor, UiStyle } from './surface.ts';
5
+ import type { UiModalTheme } from './kit.ts';
6
+
7
+ type OpenCodeThemeMode = 'dark' | 'light';
8
+
9
+ type OpenCodeColorValue =
10
+ | string
11
+ | {
12
+ readonly dark: string;
13
+ readonly light: string;
14
+ };
15
+
16
+ interface OpenCodeThemeDocument {
17
+ readonly $schema?: string;
18
+ readonly defs?: Readonly<Record<string, OpenCodeColorValue>>;
19
+ readonly theme: Readonly<Record<string, OpenCodeColorValue>>;
20
+ }
21
+
22
+ interface OpenCodeThemeInput {
23
+ readonly mode: OpenCodeThemeMode;
24
+ readonly document: OpenCodeThemeDocument;
25
+ }
26
+
27
+ interface MuxWorkspaceRailStatusColors {
28
+ readonly working: UiColor;
29
+ readonly exited: UiColor;
30
+ readonly needsAction: UiColor;
31
+ readonly starting: UiColor;
32
+ readonly idle: UiColor;
33
+ }
34
+
35
+ export interface MuxWorkspaceRailTheme {
36
+ readonly normalStyle: UiStyle;
37
+ readonly headerStyle: UiStyle;
38
+ readonly activeRowStyle: UiStyle;
39
+ readonly metaStyle: UiStyle;
40
+ readonly conversationBodyStyle: UiStyle;
41
+ readonly processStyle: UiStyle;
42
+ readonly repositoryRowStyle: UiStyle;
43
+ readonly mutedStyle: UiStyle;
44
+ readonly shortcutStyle: UiStyle;
45
+ readonly actionStyle: UiStyle;
46
+ readonly statusColors: MuxWorkspaceRailStatusColors;
47
+ }
48
+
49
+ interface ActiveMuxTheme {
50
+ readonly name: string;
51
+ readonly mode: OpenCodeThemeMode;
52
+ readonly modalTheme: UiModalTheme;
53
+ readonly workspaceRail: MuxWorkspaceRailTheme;
54
+ readonly terminalForegroundHex: string | null;
55
+ readonly terminalBackgroundHex: string | null;
56
+ }
57
+
58
+ interface ResolveConfiguredMuxThemeResult {
59
+ readonly theme: ActiveMuxTheme;
60
+ readonly error: string | null;
61
+ }
62
+
63
+ interface ResolveConfiguredMuxThemeOptions {
64
+ readonly config: HarnessMuxThemeConfig | null;
65
+ readonly cwd: string;
66
+ readonly readFile?: (path: string) => string;
67
+ }
68
+
69
+ const OPENCODE_SCHEMA_URL = 'https://opencode.ai/theme.json';
70
+ const FALLBACK_HEX = {
71
+ text: '#d0d7de',
72
+ textMuted: '#a4adb8',
73
+ conceal: '#5c6370',
74
+ primary: '#6cb6ff',
75
+ success: '#8ccf7e',
76
+ error: '#f47067',
77
+ warning: '#e6c07b',
78
+ info: '#39c5cf',
79
+ background: '#0f1419',
80
+ backgroundPanel: '#1b2128',
81
+ backgroundElement: '#2a313a',
82
+ };
83
+
84
+ const BUILTIN_OPENCODE_PRESETS: Readonly<Record<string, OpenCodeThemeDocument>> = {
85
+ github: {
86
+ $schema: OPENCODE_SCHEMA_URL,
87
+ theme: {
88
+ primary: { dark: '#79c0ff', light: '#0550ae' },
89
+ success: { dark: '#3fb950', light: '#1a7f37' },
90
+ error: { dark: '#f85149', light: '#cf222e' },
91
+ warning: { dark: '#e3b341', light: '#9a6700' },
92
+ info: { dark: '#d29922', light: '#bc4c00' },
93
+ text: { dark: '#e6edf3', light: '#24292f' },
94
+ textMuted: { dark: '#8b949e', light: '#57606a' },
95
+ conceal: { dark: '#484f58', light: '#8c959f' },
96
+ background: { dark: '#0d1117', light: '#ffffff' },
97
+ backgroundPanel: { dark: '#161b22', light: '#f6f8fa' },
98
+ backgroundElement: { dark: '#21262d', light: '#d0d7de' },
99
+ syntaxFunction: { dark: '#d2a8ff', light: '#8250df' },
100
+ },
101
+ },
102
+ 'github-light': {
103
+ $schema: OPENCODE_SCHEMA_URL,
104
+ theme: {
105
+ primary: '#0550ae',
106
+ success: '#1a7f37',
107
+ error: '#cf222e',
108
+ warning: '#9a6700',
109
+ info: '#bc4c00',
110
+ text: '#24292f',
111
+ textMuted: '#57606a',
112
+ conceal: '#8c959f',
113
+ background: '#ffffff',
114
+ backgroundPanel: '#f6f8fa',
115
+ backgroundElement: '#d0d7de',
116
+ syntaxFunction: '#8250df',
117
+ },
118
+ },
119
+ tokyonight: {
120
+ $schema: OPENCODE_SCHEMA_URL,
121
+ theme: {
122
+ primary: '#7aa2f7',
123
+ success: '#9ece6a',
124
+ error: '#f7768e',
125
+ warning: '#e0af68',
126
+ info: '#7dcfff',
127
+ text: '#c0caf5',
128
+ textMuted: '#9aa5ce',
129
+ conceal: '#5f6a94',
130
+ background: '#1a1b26',
131
+ backgroundPanel: '#1f2335',
132
+ backgroundElement: '#2a2f46',
133
+ syntaxFunction: '#bb9af7',
134
+ },
135
+ },
136
+ dracula: {
137
+ $schema: OPENCODE_SCHEMA_URL,
138
+ theme: {
139
+ primary: '#bd93f9',
140
+ success: '#50fa7b',
141
+ error: '#ff5555',
142
+ warning: '#f1fa8c',
143
+ info: '#8be9fd',
144
+ text: '#f8f8f2',
145
+ textMuted: '#b8b8c6',
146
+ conceal: '#6272a4',
147
+ background: '#282a36',
148
+ backgroundPanel: '#303341',
149
+ backgroundElement: '#3a3d4d',
150
+ syntaxFunction: '#ff79c6',
151
+ },
152
+ },
153
+ nord: {
154
+ $schema: OPENCODE_SCHEMA_URL,
155
+ theme: {
156
+ primary: '#81a1c1',
157
+ success: '#a3be8c',
158
+ error: '#bf616a',
159
+ warning: '#ebcb8b',
160
+ info: '#88c0d0',
161
+ text: '#d8dee9',
162
+ textMuted: '#aeb8c9',
163
+ conceal: '#6d7d95',
164
+ background: '#2e3440',
165
+ backgroundPanel: '#3b4252',
166
+ backgroundElement: '#434c5e',
167
+ syntaxFunction: '#b48ead',
168
+ },
169
+ },
170
+ gruvbox: {
171
+ $schema: OPENCODE_SCHEMA_URL,
172
+ theme: {
173
+ primary: '#83a598',
174
+ success: '#b8bb26',
175
+ error: '#fb4934',
176
+ warning: '#fabd2f',
177
+ info: '#8ec07c',
178
+ text: '#ebdbb2',
179
+ textMuted: '#bdae93',
180
+ conceal: '#7c6f64',
181
+ background: '#282828',
182
+ backgroundPanel: '#3c3836',
183
+ backgroundElement: '#504945',
184
+ syntaxFunction: '#d3869b',
185
+ },
186
+ },
187
+ };
188
+
189
+ const LEGACY_MUX_THEME: ActiveMuxTheme = {
190
+ name: 'legacy-default',
191
+ mode: 'dark',
192
+ modalTheme: {
193
+ frameStyle: {
194
+ fg: { kind: 'indexed', index: 252 },
195
+ bg: { kind: 'indexed', index: 236 },
196
+ bold: true,
197
+ },
198
+ titleStyle: {
199
+ fg: { kind: 'indexed', index: 231 },
200
+ bg: { kind: 'indexed', index: 236 },
201
+ bold: true,
202
+ },
203
+ bodyStyle: {
204
+ fg: { kind: 'indexed', index: 253 },
205
+ bg: { kind: 'indexed', index: 236 },
206
+ bold: false,
207
+ },
208
+ footerStyle: {
209
+ fg: { kind: 'indexed', index: 247 },
210
+ bg: { kind: 'indexed', index: 236 },
211
+ bold: false,
212
+ },
213
+ },
214
+ workspaceRail: {
215
+ normalStyle: {
216
+ fg: { kind: 'default' },
217
+ bg: { kind: 'default' },
218
+ bold: false,
219
+ },
220
+ headerStyle: {
221
+ fg: { kind: 'indexed', index: 254 },
222
+ bg: { kind: 'default' },
223
+ bold: true,
224
+ },
225
+ activeRowStyle: {
226
+ fg: { kind: 'indexed', index: 254 },
227
+ bg: { kind: 'indexed', index: 237 },
228
+ bold: false,
229
+ },
230
+ metaStyle: {
231
+ fg: { kind: 'indexed', index: 151 },
232
+ bg: { kind: 'default' },
233
+ bold: false,
234
+ },
235
+ conversationBodyStyle: {
236
+ fg: { kind: 'indexed', index: 151 },
237
+ bg: { kind: 'default' },
238
+ bold: false,
239
+ },
240
+ processStyle: {
241
+ fg: { kind: 'indexed', index: 223 },
242
+ bg: { kind: 'default' },
243
+ bold: false,
244
+ },
245
+ repositoryRowStyle: {
246
+ fg: { kind: 'indexed', index: 181 },
247
+ bg: { kind: 'default' },
248
+ bold: false,
249
+ },
250
+ mutedStyle: {
251
+ fg: { kind: 'indexed', index: 245 },
252
+ bg: { kind: 'default' },
253
+ bold: false,
254
+ },
255
+ shortcutStyle: {
256
+ fg: { kind: 'indexed', index: 250 },
257
+ bg: { kind: 'default' },
258
+ bold: false,
259
+ },
260
+ actionStyle: {
261
+ fg: { kind: 'indexed', index: 230 },
262
+ bg: { kind: 'indexed', index: 237 },
263
+ bold: false,
264
+ },
265
+ statusColors: {
266
+ working: { kind: 'indexed', index: 45 },
267
+ exited: { kind: 'indexed', index: 196 },
268
+ needsAction: { kind: 'indexed', index: 220 },
269
+ starting: { kind: 'indexed', index: 110 },
270
+ idle: { kind: 'indexed', index: 245 },
271
+ },
272
+ },
273
+ terminalForegroundHex: null,
274
+ terminalBackgroundHex: null,
275
+ };
276
+
277
+ let activeMuxTheme: ActiveMuxTheme = LEGACY_MUX_THEME;
278
+
279
+ function normalizeHex(input: string): string | null {
280
+ const trimmed = input.trim();
281
+ if (/^#[0-9a-fA-F]{6}$/.test(trimmed)) {
282
+ return trimmed.toLowerCase();
283
+ }
284
+ if (/^#[0-9a-fA-F]{3}$/.test(trimmed)) {
285
+ const red = trimmed[1]!;
286
+ const green = trimmed[2]!;
287
+ const blue = trimmed[3]!;
288
+ return `#${red}${red}${green}${green}${blue}${blue}`.toLowerCase();
289
+ }
290
+ return null;
291
+ }
292
+
293
+ function hexToUiColor(hex: string): UiColor {
294
+ const normalized = normalizeHex(hex) ?? '#000000';
295
+ return {
296
+ kind: 'rgb',
297
+ r: Number.parseInt(normalized.slice(1, 3), 16),
298
+ g: Number.parseInt(normalized.slice(3, 5), 16),
299
+ b: Number.parseInt(normalized.slice(5, 7), 16),
300
+ };
301
+ }
302
+
303
+ function asThemeDocument(value: unknown): OpenCodeThemeDocument | null {
304
+ if (value === null || typeof value !== 'object' || Array.isArray(value)) {
305
+ return null;
306
+ }
307
+ const record = value as Record<string, unknown>;
308
+ if (
309
+ record['theme'] === null ||
310
+ typeof record['theme'] !== 'object' ||
311
+ Array.isArray(record['theme'])
312
+ ) {
313
+ return null;
314
+ }
315
+ return {
316
+ ...(typeof record['$schema'] === 'string' ? { $schema: record['$schema'] } : {}),
317
+ ...(record['defs'] !== undefined &&
318
+ record['defs'] !== null &&
319
+ typeof record['defs'] === 'object' &&
320
+ !Array.isArray(record['defs'])
321
+ ? { defs: record['defs'] as Readonly<Record<string, OpenCodeColorValue>> }
322
+ : {}),
323
+ theme: record['theme'] as Readonly<Record<string, OpenCodeColorValue>>,
324
+ };
325
+ }
326
+
327
+ function asColorVariant(
328
+ value: OpenCodeColorValue,
329
+ ): { readonly dark: string; readonly light: string } | null {
330
+ if (typeof value !== 'object' || value === null || Array.isArray(value)) {
331
+ return null;
332
+ }
333
+ if (typeof value.dark !== 'string' || typeof value.light !== 'string') {
334
+ return null;
335
+ }
336
+ return {
337
+ dark: value.dark,
338
+ light: value.light,
339
+ };
340
+ }
341
+
342
+ function resolveColorValue(
343
+ input: OpenCodeThemeInput,
344
+ value: OpenCodeColorValue,
345
+ stack: ReadonlySet<string>,
346
+ ): string | null {
347
+ if (typeof value === 'string') {
348
+ const normalizedHex = normalizeHex(value);
349
+ if (normalizedHex !== null) {
350
+ return normalizedHex;
351
+ }
352
+ const nextKey = value.trim();
353
+ if (nextKey.length === 0) {
354
+ return null;
355
+ }
356
+ if (stack.has(nextKey)) {
357
+ return null;
358
+ }
359
+ if (nextKey === 'transparent' || nextKey === 'none') {
360
+ return null;
361
+ }
362
+ const nextValue = input.document.theme[nextKey] ?? input.document.defs?.[nextKey];
363
+ if (nextValue === undefined) {
364
+ return null;
365
+ }
366
+ const nextStack = new Set(stack);
367
+ nextStack.add(nextKey);
368
+ return resolveColorValue(input, nextValue, nextStack);
369
+ }
370
+ const variant = asColorVariant(value);
371
+ if (variant === null) {
372
+ return null;
373
+ }
374
+ return resolveColorValue(input, variant[input.mode], stack);
375
+ }
376
+
377
+ function themeHex(input: OpenCodeThemeInput, key: string, fallback: string): string {
378
+ const value = input.document.theme[key];
379
+ if (value === undefined) {
380
+ return fallback;
381
+ }
382
+ return resolveColorValue(input, value, new Set<string>([key])) ?? fallback;
383
+ }
384
+
385
+ function uiStyle(fgHex: string, bgHex: string | null, bold = false): UiStyle {
386
+ return {
387
+ fg: hexToUiColor(fgHex),
388
+ bg: bgHex === null ? { kind: 'default' } : hexToUiColor(bgHex),
389
+ bold,
390
+ };
391
+ }
392
+
393
+ function resolveThemeDocumentFromFile(
394
+ path: string,
395
+ readFile: (path: string) => string,
396
+ ): OpenCodeThemeDocument {
397
+ const content = readFile(path);
398
+ let parsed: unknown;
399
+ try {
400
+ parsed = JSON.parse(content);
401
+ } catch (error: unknown) {
402
+ const message = error instanceof Error ? error.message : String(error);
403
+ throw new Error(`invalid theme json at ${path}: ${message}`);
404
+ }
405
+ const document = asThemeDocument(parsed);
406
+ if (document === null) {
407
+ throw new Error(`theme at ${path} must be an object with a "theme" record`);
408
+ }
409
+ return document;
410
+ }
411
+
412
+ function buildActiveTheme(name: string, input: OpenCodeThemeInput): ActiveMuxTheme {
413
+ const text = themeHex(input, 'text', FALLBACK_HEX.text);
414
+ const textMuted = themeHex(input, 'textMuted', FALLBACK_HEX.textMuted);
415
+ const conceal = themeHex(input, 'conceal', FALLBACK_HEX.conceal);
416
+ const primary = themeHex(input, 'primary', FALLBACK_HEX.primary);
417
+ const success = themeHex(input, 'success', FALLBACK_HEX.success);
418
+ const error = themeHex(input, 'error', FALLBACK_HEX.error);
419
+ const warning = themeHex(input, 'warning', FALLBACK_HEX.warning);
420
+ const info = themeHex(input, 'info', FALLBACK_HEX.info);
421
+ const background = themeHex(input, 'background', FALLBACK_HEX.background);
422
+ const backgroundPanel = themeHex(input, 'backgroundPanel', FALLBACK_HEX.backgroundPanel);
423
+ const backgroundElement = themeHex(input, 'backgroundElement', FALLBACK_HEX.backgroundElement);
424
+ const syntaxFunction = themeHex(input, 'syntaxFunction', primary);
425
+
426
+ return {
427
+ name,
428
+ mode: input.mode,
429
+ modalTheme: {
430
+ frameStyle: uiStyle(primary, backgroundPanel, true),
431
+ titleStyle: uiStyle(text, backgroundPanel, true),
432
+ bodyStyle: uiStyle(text, backgroundPanel, false),
433
+ footerStyle: uiStyle(textMuted, backgroundPanel, false),
434
+ },
435
+ workspaceRail: {
436
+ normalStyle: uiStyle(text, null, false),
437
+ headerStyle: uiStyle(primary, null, true),
438
+ activeRowStyle: uiStyle(text, backgroundElement, false),
439
+ metaStyle: uiStyle(textMuted, null, false),
440
+ conversationBodyStyle: uiStyle(textMuted, null, false),
441
+ processStyle: uiStyle(info, null, false),
442
+ repositoryRowStyle: uiStyle(syntaxFunction, null, false),
443
+ mutedStyle: uiStyle(conceal, null, false),
444
+ shortcutStyle: uiStyle(textMuted, null, false),
445
+ actionStyle: uiStyle(background, primary, false),
446
+ statusColors: {
447
+ working: hexToUiColor(success),
448
+ exited: hexToUiColor(error),
449
+ needsAction: hexToUiColor(warning),
450
+ starting: hexToUiColor(primary),
451
+ idle: hexToUiColor(conceal),
452
+ },
453
+ },
454
+ terminalForegroundHex: text.slice(1),
455
+ terminalBackgroundHex: background.slice(1),
456
+ };
457
+ }
458
+
459
+ export function muxThemePresetNames(): readonly string[] {
460
+ return Object.keys(BUILTIN_OPENCODE_PRESETS).sort();
461
+ }
462
+
463
+ function resolveBuiltinPreset(name: string): OpenCodeThemeDocument | null {
464
+ const preset = BUILTIN_OPENCODE_PRESETS[name];
465
+ return preset ?? null;
466
+ }
467
+
468
+ export function setActiveMuxTheme(theme: ActiveMuxTheme): void {
469
+ activeMuxTheme = theme;
470
+ }
471
+
472
+ export function getActiveMuxTheme(): ActiveMuxTheme {
473
+ return activeMuxTheme;
474
+ }
475
+
476
+ export function resetActiveMuxThemeForTest(): void {
477
+ activeMuxTheme = LEGACY_MUX_THEME;
478
+ }
479
+
480
+ export function resolveConfiguredMuxTheme(
481
+ options: ResolveConfiguredMuxThemeOptions,
482
+ ): ResolveConfiguredMuxThemeResult {
483
+ const configured = options.config;
484
+ if (configured === null) {
485
+ return {
486
+ theme: LEGACY_MUX_THEME,
487
+ error: null,
488
+ };
489
+ }
490
+
491
+ const normalizedPreset = configured.preset.trim().toLowerCase();
492
+ const presetDocument = resolveBuiltinPreset(normalizedPreset);
493
+ const mode: OpenCodeThemeMode = configured.mode === 'light' ? 'light' : 'dark';
494
+ const readFile = options.readFile ?? ((path: string) => readFileSync(path, 'utf8'));
495
+
496
+ let selectedDocument: OpenCodeThemeDocument | null = presetDocument ?? null;
497
+ let selectedName = normalizedPreset.length === 0 ? 'github' : normalizedPreset;
498
+ let error: string | null = null;
499
+
500
+ if (configured.customThemePath !== null) {
501
+ const resolvedCustomPath = resolve(options.cwd, configured.customThemePath);
502
+ try {
503
+ selectedDocument = resolveThemeDocumentFromFile(resolvedCustomPath, readFile);
504
+ selectedName = `custom:${configured.customThemePath}`;
505
+ } catch (customError: unknown) {
506
+ const message = customError instanceof Error ? customError.message : String(customError);
507
+ error = message;
508
+ }
509
+ }
510
+
511
+ if (selectedDocument === null) {
512
+ const fallbackDocument = BUILTIN_OPENCODE_PRESETS.github as OpenCodeThemeDocument;
513
+ selectedDocument = fallbackDocument;
514
+ selectedName = 'github';
515
+ if (error === null && presetDocument === null) {
516
+ error = `unknown mux theme preset "${configured.preset}"`;
517
+ }
518
+ }
519
+ const resolvedDocument =
520
+ selectedDocument ?? (BUILTIN_OPENCODE_PRESETS.github as OpenCodeThemeDocument);
521
+
522
+ return {
523
+ theme: buildActiveTheme(selectedName, {
524
+ mode,
525
+ document: resolvedDocument,
526
+ }),
527
+ error,
528
+ };
529
+ }
@@ -0,0 +1,19 @@
1
+ import {
2
+ renderSnapshotAnsiRow,
3
+ type TerminalSnapshotFrameCore,
4
+ } from '../../terminal/snapshot-oracle.ts';
5
+
6
+ interface ConversationPaneLayout {
7
+ readonly rightCols: number;
8
+ readonly paneRows: number;
9
+ }
10
+
11
+ export class ConversationPane {
12
+ constructor(private readonly renderRow: typeof renderSnapshotAnsiRow = renderSnapshotAnsiRow) {}
13
+
14
+ render(frame: TerminalSnapshotFrameCore, layout: ConversationPaneLayout): readonly string[] {
15
+ return Array.from({ length: layout.paneRows }, (_value, row) =>
16
+ this.renderRow(frame, row, layout.rightCols),
17
+ );
18
+ }
19
+ }