@jmoyers/harness 0.1.11 → 0.1.20

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 (232) hide show
  1. package/README.md +31 -39
  2. package/package.json +31 -11
  3. package/packages/harness-ai/src/anthropic-protocol.ts +68 -68
  4. package/packages/harness-ai/src/stream-text.ts +13 -91
  5. package/packages/harness-ui/src/frame-primitives.ts +158 -0
  6. package/packages/harness-ui/src/index.ts +18 -0
  7. package/packages/harness-ui/src/interaction/conversation-input-forwarder.ts +221 -0
  8. package/packages/harness-ui/src/interaction/conversation-selection-input.ts +213 -0
  9. package/packages/harness-ui/src/interaction/global-shortcut-input.ts +172 -0
  10. package/{src/ui → packages/harness-ui/src/interaction}/input-preflight.ts +10 -12
  11. package/{src/ui → packages/harness-ui/src/interaction}/input-token-router.ts +120 -69
  12. package/packages/harness-ui/src/interaction/input.ts +420 -0
  13. package/packages/harness-ui/src/interaction/left-nav-input.ts +166 -0
  14. package/{src/ui → packages/harness-ui/src/interaction}/main-pane-pointer-input.ts +91 -23
  15. package/{src/ui → packages/harness-ui/src/interaction}/pointer-routing-input.ts +112 -48
  16. package/packages/harness-ui/src/interaction/rail-pointer-input.ts +62 -0
  17. package/packages/harness-ui/src/interaction/repository-fold-input.ts +118 -0
  18. package/packages/harness-ui/src/kit.ts +476 -0
  19. package/packages/harness-ui/src/layout.ts +238 -0
  20. package/packages/harness-ui/src/modal-manager.ts +222 -0
  21. package/{src/ui → packages/harness-ui/src}/screen.ts +53 -26
  22. package/packages/harness-ui/src/surface.ts +252 -0
  23. package/packages/harness-ui/src/text-layout.ts +210 -0
  24. package/packages/nim-core/src/contracts.ts +239 -0
  25. package/packages/nim-core/src/event-store.ts +299 -0
  26. package/packages/nim-core/src/events.ts +53 -0
  27. package/packages/nim-core/src/index.ts +9 -0
  28. package/packages/nim-core/src/provider-router.ts +129 -0
  29. package/packages/nim-core/src/providers/anthropic-driver.ts +291 -0
  30. package/packages/nim-core/src/runtime-factory.ts +49 -0
  31. package/packages/nim-core/src/runtime.ts +1797 -0
  32. package/packages/nim-core/src/session-store.ts +516 -0
  33. package/packages/nim-core/src/telemetry.ts +48 -0
  34. package/packages/nim-test-tui/src/index.ts +150 -0
  35. package/packages/nim-ui-core/src/index.ts +1 -0
  36. package/packages/nim-ui-core/src/projection.ts +87 -0
  37. package/scripts/codex-live-mux-runtime.ts +2 -3872
  38. package/scripts/control-plane-daemon.ts +11 -0
  39. package/scripts/harness-bin.js +5 -0
  40. package/scripts/harness-commands.ts +300 -0
  41. package/scripts/harness-runtime.ts +82 -0
  42. package/scripts/harness.ts +33 -3019
  43. package/scripts/nim-tui-smoke.ts +748 -0
  44. package/src/cli/auth/runtime.ts +948 -0
  45. package/src/cli/gateway/runtime.ts +1872 -0
  46. package/src/cli/parsing/flags.ts +23 -0
  47. package/src/cli/parsing/session.ts +42 -0
  48. package/src/cli/runtime/context.ts +193 -0
  49. package/src/cli/runtime-app/application.ts +392 -0
  50. package/src/cli/runtime-infra/gateway-control.ts +729 -0
  51. package/{scripts/harness-inspector.ts → src/cli/workflows/inspector.ts} +14 -11
  52. package/src/cli/workflows/runtime.ts +965 -0
  53. package/src/clients/tui/left-rail-interactions.ts +519 -0
  54. package/src/clients/tui/main-pane-interactions.ts +509 -0
  55. package/src/clients/tui/modal-input-routing.ts +71 -0
  56. package/src/clients/tui/render-snapshot-adapter.ts +88 -0
  57. package/src/clients/web/synced-selectors.ts +132 -0
  58. package/src/codex/live-session.ts +82 -29
  59. package/src/config/config-core.ts +348 -8
  60. package/src/config/harness.config.template.jsonc +33 -0
  61. package/src/control-plane/agent-realtime-api.ts +82 -427
  62. package/src/control-plane/session-summary.ts +10 -81
  63. package/src/control-plane/status/reducer-base.ts +12 -12
  64. package/src/control-plane/status/reducers/claude-status-reducer.ts +3 -3
  65. package/src/control-plane/status/reducers/codex-status-reducer.ts +4 -4
  66. package/src/control-plane/status/reducers/cursor-status-reducer.ts +3 -3
  67. package/src/control-plane/stream-client.ts +12 -2
  68. package/src/control-plane/stream-command-parser.ts +83 -143
  69. package/src/control-plane/stream-protocol.ts +53 -37
  70. package/src/control-plane/stream-server-command.ts +376 -69
  71. package/src/control-plane/stream-server-session-runtime.ts +3 -2
  72. package/src/control-plane/stream-server.ts +864 -70
  73. package/src/control-plane/stream-session-runtime-types.ts +41 -0
  74. package/src/{mux/live-mux/control-plane-records.ts → core/contracts/records.ts} +24 -97
  75. package/src/core/state/observed-stream-cursor.ts +43 -0
  76. package/src/core/state/synced-observed-state.ts +273 -0
  77. package/src/core/store/harness-synced-store.ts +81 -0
  78. package/src/diff/budget.ts +136 -0
  79. package/src/diff/build.ts +289 -0
  80. package/src/diff/chunker.ts +146 -0
  81. package/src/diff/git-invoke.ts +315 -0
  82. package/src/diff/git-parse.ts +472 -0
  83. package/src/diff/hash.ts +70 -0
  84. package/src/diff/index.ts +24 -0
  85. package/src/diff/normalize.ts +134 -0
  86. package/src/diff/types.ts +178 -0
  87. package/src/diff-ui/args.ts +346 -0
  88. package/src/diff-ui/commands.ts +123 -0
  89. package/src/diff-ui/finder.ts +94 -0
  90. package/src/diff-ui/highlight.ts +127 -0
  91. package/src/diff-ui/index.ts +2 -0
  92. package/src/diff-ui/model.ts +141 -0
  93. package/src/diff-ui/pager.ts +412 -0
  94. package/src/diff-ui/render.ts +337 -0
  95. package/src/diff-ui/runtime.ts +379 -0
  96. package/src/diff-ui/state.ts +224 -0
  97. package/src/diff-ui/types.ts +236 -0
  98. package/src/domain/workspace.ts +68 -5
  99. package/src/mux/control-plane-op-queue.ts +93 -7
  100. package/src/mux/conversation-rail.ts +28 -71
  101. package/src/mux/dual-pane-core.ts +13 -13
  102. package/src/mux/harness-core-ui.ts +313 -42
  103. package/src/mux/input-shortcuts.ts +13 -131
  104. package/src/mux/keybinding-catalog.ts +340 -0
  105. package/src/mux/keybinding-registry.ts +103 -0
  106. package/src/mux/live-mux/command-menu-open-in.ts +280 -0
  107. package/src/mux/live-mux/command-menu.ts +167 -4
  108. package/src/mux/live-mux/conversation-state.ts +13 -0
  109. package/src/mux/live-mux/directory-resolution.ts +1 -1
  110. package/src/mux/live-mux/git-snapshot.ts +33 -2
  111. package/src/mux/live-mux/global-shortcut-handlers.ts +6 -0
  112. package/src/mux/live-mux/home-pane-drop.ts +1 -1
  113. package/src/mux/live-mux/home-pane-pointer.ts +10 -0
  114. package/src/mux/live-mux/input-forwarding.ts +59 -2
  115. package/src/mux/live-mux/left-nav-activation.ts +124 -7
  116. package/src/mux/live-mux/left-nav.ts +35 -0
  117. package/src/mux/live-mux/link-click.ts +292 -0
  118. package/src/mux/live-mux/modal-command-menu-handler.ts +46 -9
  119. package/src/mux/live-mux/modal-conversation-handlers.ts +5 -1
  120. package/src/mux/live-mux/modal-input-reducers.ts +77 -12
  121. package/src/mux/live-mux/modal-overlays.ts +168 -34
  122. package/src/mux/live-mux/modal-pointer.ts +3 -7
  123. package/src/mux/live-mux/modal-prompt-handlers.ts +23 -2
  124. package/src/mux/live-mux/modal-release-notes-handler.ts +111 -0
  125. package/src/mux/live-mux/modal-task-editor-handler.ts +16 -11
  126. package/src/mux/live-mux/pointer-routing.ts +5 -2
  127. package/src/mux/live-mux/project-pane-pointer.ts +8 -0
  128. package/src/mux/live-mux/rail-layout.ts +33 -30
  129. package/src/mux/live-mux/release-notes.ts +383 -0
  130. package/src/mux/live-mux/render-trace-analysis.ts +52 -7
  131. package/src/mux/live-mux/repository-folding.ts +3 -0
  132. package/src/mux/live-mux/selection.ts +0 -4
  133. package/src/mux/live-mux/session-diagnostics-paths.ts +21 -0
  134. package/src/mux/project-pane-github-review.ts +271 -0
  135. package/src/mux/render-frame.ts +4 -0
  136. package/src/mux/runtime-app/codex-live-mux-runtime.ts +5191 -0
  137. package/src/mux/task-composer.ts +21 -14
  138. package/src/mux/task-focused-pane.ts +118 -117
  139. package/src/mux/task-screen-keybindings.ts +10 -101
  140. package/src/mux/workspace-rail-model.ts +270 -104
  141. package/src/mux/workspace-rail.ts +45 -22
  142. package/src/pty/session-broker.ts +1 -1
  143. package/{scripts → src/recording}/terminal-recording-gif-lib.ts +2 -2
  144. package/src/services/control-plane.ts +50 -32
  145. package/src/services/conversation-lifecycle.ts +118 -87
  146. package/src/services/conversation-startup-hydration.ts +20 -12
  147. package/src/services/directory-hydration.ts +21 -16
  148. package/src/services/event-persistence.ts +7 -0
  149. package/src/services/left-rail-pointer-handler.ts +329 -0
  150. package/src/services/mux-ui-state-persistence.ts +5 -1
  151. package/src/services/recording.ts +34 -26
  152. package/src/services/runtime-command-menu-agent-tools.ts +1 -1
  153. package/src/services/runtime-control-actions.ts +79 -61
  154. package/src/services/runtime-control-plane-ops.ts +122 -83
  155. package/src/services/runtime-conversation-actions.ts +40 -26
  156. package/src/services/runtime-conversation-activation.ts +73 -46
  157. package/src/services/runtime-conversation-starter.ts +53 -45
  158. package/src/services/runtime-conversation-title-edit.ts +91 -80
  159. package/src/services/runtime-envelope-handler.ts +107 -105
  160. package/src/services/runtime-git-state.ts +42 -29
  161. package/src/services/runtime-layout-resize.ts +3 -1
  162. package/src/services/runtime-left-rail-render.ts +99 -63
  163. package/src/services/runtime-nim-cli-session.ts +438 -0
  164. package/src/services/runtime-nim-session.ts +705 -0
  165. package/src/services/runtime-nim-tool-bridge.ts +141 -0
  166. package/src/services/runtime-observed-event-projection-pipeline.ts +45 -0
  167. package/src/services/runtime-process-wiring.ts +29 -36
  168. package/src/services/runtime-project-pane-github-review-cache.ts +164 -0
  169. package/src/services/runtime-render-flush.ts +63 -70
  170. package/src/services/runtime-render-lifecycle.ts +65 -64
  171. package/src/services/runtime-render-orchestrator.ts +55 -45
  172. package/src/services/runtime-render-pipeline.ts +106 -103
  173. package/src/services/runtime-render-state.ts +62 -49
  174. package/src/services/runtime-repository-actions.ts +97 -72
  175. package/src/services/runtime-right-pane-render.ts +80 -53
  176. package/src/services/runtime-shutdown.ts +38 -35
  177. package/src/services/runtime-stream-subscriptions.ts +35 -27
  178. package/src/services/runtime-task-composer-persistence.ts +71 -59
  179. package/src/services/runtime-task-composer-snapshot.ts +14 -0
  180. package/src/services/runtime-task-editor-actions.ts +46 -29
  181. package/src/services/runtime-task-pane-actions.ts +220 -134
  182. package/src/services/runtime-task-pane-shortcuts.ts +323 -123
  183. package/src/services/runtime-workspace-observed-effect-queue.ts +25 -0
  184. package/src/services/runtime-workspace-observed-events.ts +33 -184
  185. package/src/services/runtime-workspace-observed-transition-policy.ts +228 -0
  186. package/src/services/session-diagnostics-store.ts +217 -0
  187. package/src/services/startup-background-resume.ts +26 -21
  188. package/src/services/startup-orchestrator.ts +16 -13
  189. package/src/services/startup-paint-tracker.ts +29 -21
  190. package/src/services/startup-persisted-conversation-queue.ts +19 -13
  191. package/src/services/startup-settled-gate.ts +25 -15
  192. package/src/services/startup-shutdown.ts +18 -22
  193. package/src/services/startup-state-hydration.ts +44 -34
  194. package/src/services/startup-visibility.ts +12 -4
  195. package/src/services/task-pane-selection-actions.ts +89 -72
  196. package/src/services/task-planning-hydration.ts +24 -18
  197. package/src/services/task-planning-observed-events.ts +50 -52
  198. package/src/services/workspace-observed-events.ts +66 -63
  199. package/src/storage/storage-lifecycle-core.ts +438 -0
  200. package/src/store/control-plane-store-normalize.ts +33 -242
  201. package/src/store/control-plane-store-types.ts +1 -35
  202. package/src/store/control-plane-store.ts +360 -56
  203. package/src/store/event-store.ts +366 -8
  204. package/src/terminal/snapshot-oracle.ts +207 -94
  205. package/src/ui/mux-theme.ts +112 -8
  206. package/src/ui/panes/home-gridfire.ts +40 -31
  207. package/src/ui/panes/home.ts +10 -2
  208. package/src/ui/panes/nim.ts +315 -0
  209. package/src/mux/live-mux/actions-task.ts +0 -115
  210. package/src/mux/live-mux/left-rail-actions.ts +0 -118
  211. package/src/mux/live-mux/left-rail-conversation-click.ts +0 -85
  212. package/src/mux/live-mux/left-rail-pointer.ts +0 -74
  213. package/src/mux/live-mux/task-pane-shortcuts.ts +0 -206
  214. package/src/services/runtime-directory-actions.ts +0 -164
  215. package/src/services/runtime-input-pipeline.ts +0 -50
  216. package/src/services/runtime-input-router.ts +0 -195
  217. package/src/services/runtime-main-pane-input.ts +0 -230
  218. package/src/services/runtime-modal-input.ts +0 -137
  219. package/src/services/runtime-navigation-input.ts +0 -197
  220. package/src/services/runtime-rail-input.ts +0 -279
  221. package/src/services/runtime-task-pane.ts +0 -62
  222. package/src/services/runtime-workspace-actions.ts +0 -158
  223. package/src/ui/conversation-input-forwarder.ts +0 -114
  224. package/src/ui/conversation-selection-input.ts +0 -103
  225. package/src/ui/global-shortcut-input.ts +0 -89
  226. package/src/ui/input.ts +0 -269
  227. package/src/ui/kit.ts +0 -509
  228. package/src/ui/left-nav-input.ts +0 -80
  229. package/src/ui/left-rail-pointer-input.ts +0 -148
  230. package/src/ui/modals/manager.ts +0 -218
  231. package/src/ui/repository-fold-input.ts +0 -91
  232. package/src/ui/surface.ts +0 -224
@@ -1,5 +1,9 @@
1
1
  import { createHash } from 'node:crypto';
2
2
  import { StringDecoder } from 'node:string_decoder';
3
+ import {
4
+ measureDisplayWidth as measureHarnessUiDisplayWidth,
5
+ wrapTextForColumns as wrapHarnessUiTextForColumns,
6
+ } from '../../packages/harness-ui/src/text-layout.ts';
3
7
 
4
8
  type ParserMode =
5
9
  | 'normal'
@@ -119,11 +123,32 @@ interface InternalLine {
119
123
  snapshotCacheWrapped: boolean;
120
124
  }
121
125
 
122
- const DEFAULT_COLOR: TerminalColor = { kind: 'default' };
126
+ const DEFAULT_COLOR: TerminalColor = Object.freeze({ kind: 'default' } as const);
123
127
  const DEFAULT_CURSOR_STYLE: TerminalCursorStyle = {
124
128
  shape: 'block',
125
129
  blinking: true,
126
130
  };
131
+ const DEFAULT_CELL_STYLE: Readonly<TerminalCellStyle> = Object.freeze({
132
+ bold: false,
133
+ dim: false,
134
+ italic: false,
135
+ underline: false,
136
+ inverse: false,
137
+ fg: DEFAULT_COLOR,
138
+ bg: DEFAULT_COLOR,
139
+ });
140
+ const DEFAULT_BLANK_CELL: Readonly<TerminalCell> = Object.freeze({
141
+ glyph: ' ',
142
+ width: 1,
143
+ continued: false,
144
+ style: DEFAULT_CELL_STYLE,
145
+ });
146
+ const DEFAULT_CONTINUATION_CELL: Readonly<TerminalCell> = Object.freeze({
147
+ glyph: '',
148
+ width: 0,
149
+ continued: true,
150
+ style: DEFAULT_CELL_STYLE,
151
+ });
127
152
 
128
153
  function cloneCursorStyle(style: TerminalCursorStyle): TerminalCursorStyle {
129
154
  return {
@@ -155,18 +180,26 @@ function cloneColor(color: TerminalColor): TerminalColor {
155
180
  }
156
181
 
157
182
  function defaultCellStyle(): TerminalCellStyle {
158
- return {
159
- bold: false,
160
- dim: false,
161
- italic: false,
162
- underline: false,
163
- inverse: false,
164
- fg: DEFAULT_COLOR,
165
- bg: DEFAULT_COLOR,
166
- };
183
+ return DEFAULT_CELL_STYLE as TerminalCellStyle;
184
+ }
185
+
186
+ function isDefaultStyle(style: TerminalCellStyle): boolean {
187
+ return (
188
+ style === DEFAULT_CELL_STYLE ||
189
+ (!style.bold &&
190
+ !style.dim &&
191
+ !style.italic &&
192
+ !style.underline &&
193
+ !style.inverse &&
194
+ style.fg.kind === 'default' &&
195
+ style.bg.kind === 'default')
196
+ );
167
197
  }
168
198
 
169
199
  function cloneStyle(style: TerminalCellStyle): TerminalCellStyle {
200
+ if (isDefaultStyle(style)) {
201
+ return DEFAULT_CELL_STYLE as TerminalCellStyle;
202
+ }
170
203
  return {
171
204
  bold: style.bold,
172
205
  dim: style.dim,
@@ -179,6 +212,7 @@ function cloneStyle(style: TerminalCellStyle): TerminalCellStyle {
179
212
  }
180
213
 
181
214
  function styleEqual(left: TerminalCellStyle, right: TerminalCellStyle): boolean {
215
+ if (left === right) return true;
182
216
  return (
183
217
  left.bold === right.bold &&
184
218
  left.dim === right.dim &&
@@ -208,6 +242,9 @@ function colorEqual(left: TerminalColor, right: TerminalColor): boolean {
208
242
  }
209
243
 
210
244
  function blankCell(style: TerminalCellStyle): TerminalCell {
245
+ if (isDefaultStyle(style)) {
246
+ return DEFAULT_BLANK_CELL as TerminalCell;
247
+ }
211
248
  return {
212
249
  glyph: ' ',
213
250
  width: 1,
@@ -217,6 +254,9 @@ function blankCell(style: TerminalCellStyle): TerminalCell {
217
254
  }
218
255
 
219
256
  function continuationCell(style: TerminalCellStyle): TerminalCell {
257
+ if (isDefaultStyle(style)) {
258
+ return DEFAULT_CONTINUATION_CELL as TerminalCell;
259
+ }
220
260
  return {
221
261
  glyph: '',
222
262
  width: 0,
@@ -441,7 +481,16 @@ class ScreenBuffer {
441
481
  if (targetCell === undefined || targetCell.continued) {
442
482
  return;
443
483
  }
444
- targetCell.glyph += combiningChar;
484
+ if (Object.isFrozen(targetCell)) {
485
+ line.cells[targetCol] = {
486
+ glyph: targetCell.glyph + combiningChar,
487
+ width: targetCell.width,
488
+ continued: targetCell.continued,
489
+ style: targetCell.style,
490
+ };
491
+ } else {
492
+ targetCell.glyph += combiningChar;
493
+ }
445
494
  this.touchLine(line);
446
495
  }
447
496
 
@@ -891,76 +940,12 @@ function parseOscRgbColor(value: string): { r: number; g: number; b: number } |
891
940
  return null;
892
941
  }
893
942
 
894
- function isWideCodePoint(codePoint: number): boolean {
895
- const ranges: ReadonlyArray<readonly [number, number]> = [
896
- [0x1100, 0x115f],
897
- [0x2329, 0x232a],
898
- [0x2e80, 0xa4cf],
899
- [0xac00, 0xd7a3],
900
- [0xf900, 0xfaff],
901
- [0xfe10, 0xfe19],
902
- [0xfe30, 0xfe6f],
903
- [0xff00, 0xff60],
904
- [0xffe0, 0xffe6],
905
- [0x1f300, 0x1faff],
906
- ];
907
-
908
- for (const [start, end] of ranges) {
909
- if (codePoint >= start && codePoint <= end) {
910
- return true;
911
- }
912
- }
913
- return false;
914
- }
915
-
916
943
  export function measureDisplayWidth(text: string): number {
917
- let width = 0;
918
- for (const char of text) {
919
- const codePoint = char.codePointAt(0)!;
920
-
921
- if (codePoint < 0x20 || (codePoint >= 0x7f && codePoint < 0xa0)) {
922
- continue;
923
- }
924
-
925
- if (/\p{Mark}/u.test(char)) {
926
- continue;
927
- }
928
-
929
- width += isWideCodePoint(codePoint) ? 2 : 1;
930
- }
931
- return width;
944
+ return measureHarnessUiDisplayWidth(text);
932
945
  }
933
946
 
934
947
  export function wrapTextForColumns(text: string, cols: number): string[] {
935
- if (cols <= 0) {
936
- return [''];
937
- }
938
-
939
- const lines: string[] = [];
940
- let current = '';
941
- let currentWidth = 0;
942
-
943
- for (const char of text) {
944
- if (char === '\n') {
945
- lines.push(current);
946
- current = '';
947
- currentWidth = 0;
948
- continue;
949
- }
950
-
951
- const charWidth = Math.max(1, measureDisplayWidth(char));
952
- if (currentWidth + charWidth > cols) {
953
- lines.push(current);
954
- current = '';
955
- currentWidth = 0;
956
- }
957
-
958
- current += char;
959
- currentWidth += charWidth;
960
- }
961
-
962
- lines.push(current);
963
- return lines;
948
+ return [...wrapHarnessUiTextForColumns(text, cols)];
964
949
  }
965
950
 
966
951
  function resolveIndexedColor(
@@ -982,19 +967,121 @@ function resolveIndexedColor(
982
967
  };
983
968
  }
984
969
 
970
+ function mutableCloneStyle(style: TerminalCellStyle): TerminalCellStyle {
971
+ return {
972
+ bold: style.bold,
973
+ dim: style.dim,
974
+ italic: style.italic,
975
+ underline: style.underline,
976
+ inverse: style.inverse,
977
+ fg: cloneColor(style.fg),
978
+ bg: cloneColor(style.bg),
979
+ };
980
+ }
981
+
982
+ function parseCsiParamToken(rawParams: string, start: number, end: number): number {
983
+ if (start >= end) {
984
+ return Number.NaN;
985
+ }
986
+ let value = 0;
987
+ for (let index = start; index < end; index += 1) {
988
+ const code = rawParams.charCodeAt(index);
989
+ if (code < 0x30 || code > 0x39) {
990
+ return Number(rawParams.slice(start, end));
991
+ }
992
+ value = value * 10 + (code - 0x30);
993
+ }
994
+ return value;
995
+ }
996
+
997
+ function parseCsiParams(rawParams: string, startIndex = 0): number[] {
998
+ if (startIndex >= rawParams.length) {
999
+ return [];
1000
+ }
1001
+ const params: number[] = [];
1002
+ let tokenStart = startIndex;
1003
+ for (let index = startIndex; index <= rawParams.length; index += 1) {
1004
+ if (index < rawParams.length && rawParams.charCodeAt(index) !== 0x3b) {
1005
+ continue;
1006
+ }
1007
+ params.push(parseCsiParamToken(rawParams, tokenStart, index));
1008
+ tokenStart = index + 1;
1009
+ }
1010
+ return params;
1011
+ }
1012
+
1013
+ function isDigitsOnly(value: string): boolean {
1014
+ for (let index = 0; index < value.length; index += 1) {
1015
+ const code = value.charCodeAt(index);
1016
+ if (code < 0x30 || code > 0x39) {
1017
+ return false;
1018
+ }
1019
+ }
1020
+ return true;
1021
+ }
1022
+
1023
+ function isDigitsOrSemicolons(value: string): boolean {
1024
+ for (let index = 0; index < value.length; index += 1) {
1025
+ const code = value.charCodeAt(index);
1026
+ if (code === 0x3b) {
1027
+ continue;
1028
+ }
1029
+ if (code < 0x30 || code > 0x39) {
1030
+ return false;
1031
+ }
1032
+ }
1033
+ return true;
1034
+ }
1035
+
1036
+ function shouldDispatchCsiHook(rawParams: string, finalByte: string): boolean {
1037
+ if (finalByte === 'c') {
1038
+ return rawParams === '' || rawParams === '0' || rawParams === '>' || rawParams === '>0';
1039
+ }
1040
+ if (finalByte === 'n') {
1041
+ return isDigitsOnly(rawParams);
1042
+ }
1043
+ if (finalByte === 't') {
1044
+ return rawParams === '14' || rawParams === '16' || rawParams === '18';
1045
+ }
1046
+ if (finalByte === 'p') {
1047
+ return (
1048
+ rawParams.length >= 2 &&
1049
+ rawParams.charCodeAt(0) === 0x3f &&
1050
+ rawParams.endsWith('$') &&
1051
+ isDigitsOrSemicolons(rawParams.slice(1, -1))
1052
+ );
1053
+ }
1054
+ if (finalByte === 'q') {
1055
+ return rawParams === '>0';
1056
+ }
1057
+ if (finalByte === 'm') {
1058
+ if (rawParams === '>4' || rawParams === '>4;?') {
1059
+ return true;
1060
+ }
1061
+ return rawParams.startsWith('>4;') && isDigitsOnly(rawParams.slice(3));
1062
+ }
1063
+ if (finalByte === 'u') {
1064
+ if (rawParams === '?') {
1065
+ return true;
1066
+ }
1067
+ return rawParams.startsWith('>') && rawParams.length > 1 && isDigitsOnly(rawParams.slice(1));
1068
+ }
1069
+ return false;
1070
+ }
1071
+
985
1072
  function applySgrParams(
986
1073
  style: TerminalCellStyle,
987
1074
  params: number[],
988
1075
  indexedPaletteOverrides: ReadonlyMap<number, { r: number; g: number; b: number }>,
989
1076
  ): TerminalCellStyle {
990
- let nextStyle = cloneStyle(style);
991
- const queue = params.length === 0 ? [0] : [...params];
1077
+ let nextStyle = mutableCloneStyle(style);
1078
+ const queue = params.length === 0 ? [0] : params;
992
1079
 
993
1080
  for (let idx = 0; idx < queue.length; idx += 1) {
994
1081
  const param = queue[idx]!;
995
1082
 
996
1083
  if (param === 0) {
997
- nextStyle = defaultCellStyle();
1084
+ nextStyle = mutableCloneStyle(DEFAULT_CELL_STYLE);
998
1085
  continue;
999
1086
  }
1000
1087
  if (param === 1) {
@@ -1150,8 +1237,19 @@ export class TerminalSnapshotOracle {
1150
1237
 
1151
1238
  ingest(chunk: string | Uint8Array): void {
1152
1239
  const text = typeof chunk === 'string' ? chunk : this.decoder.write(Buffer.from(chunk));
1153
- for (const char of text) {
1154
- this.processChar(char);
1240
+ const len = text.length;
1241
+ let i = 0;
1242
+ while (i < len) {
1243
+ const code = text.charCodeAt(i);
1244
+ if (code < 0xd800 || code > 0xdfff) {
1245
+ this.processChar(text[i]!);
1246
+ i += 1;
1247
+ } else if (code <= 0xdbff && i + 1 < len) {
1248
+ this.processChar(text[i]! + text[i + 1]!);
1249
+ i += 2;
1250
+ } else {
1251
+ i += 1;
1252
+ }
1155
1253
  }
1156
1254
  }
1157
1255
 
@@ -1320,6 +1418,17 @@ export class TerminalSnapshotOracle {
1320
1418
  return;
1321
1419
  }
1322
1420
 
1421
+ // ASCII printable: width is always 1, never a combining mark
1422
+ if (codePoint >= 0x20 && codePoint < 0x7f) {
1423
+ if (this.pendingWrap) {
1424
+ this.currentScreen().lineFeed(this.cursor, this.style);
1425
+ this.cursor.col = 0;
1426
+ this.pendingWrap = false;
1427
+ }
1428
+ this.pendingWrap = this.currentScreen().putGlyph(this.cursor, char, 1, this.style);
1429
+ return;
1430
+ }
1431
+
1323
1432
  if (this.pendingWrap) {
1324
1433
  this.currentScreen().lineFeed(this.cursor, this.style);
1325
1434
  this.cursor.col = 0;
@@ -1419,7 +1528,7 @@ export class TerminalSnapshotOracle {
1419
1528
  const rawParams = this.csiBuffer;
1420
1529
  this.mode = 'normal';
1421
1530
  this.csiBuffer = '';
1422
- this.emitCsiQuery(`${rawParams}${finalByte}`);
1531
+ this.emitCsiQuery(rawParams, finalByte);
1423
1532
  this.applyCsi(rawParams, finalByte);
1424
1533
  return;
1425
1534
  }
@@ -1478,15 +1587,10 @@ export class TerminalSnapshotOracle {
1478
1587
  private applyCsi(rawParams: string, finalByte: string): void {
1479
1588
  const privateMode = rawParams.startsWith('?');
1480
1589
  const privateKeyboardMode = rawParams.startsWith('>');
1481
- const params = (privateMode ? rawParams.slice(1) : rawParams).split(';').map((part) => {
1482
- if (part.length === 0) {
1483
- return NaN;
1484
- }
1485
- return Number(part);
1486
- });
1487
- const first = Number.isFinite(params[0]) ? (params[0] as number) : 1;
1590
+ const paramsStart = privateMode || privateKeyboardMode ? 1 : 0;
1488
1591
 
1489
1592
  if (privateMode) {
1593
+ const params = parseCsiParams(rawParams, paramsStart);
1490
1594
  if (finalByte === 'h') {
1491
1595
  this.applyPrivateMode(params, true);
1492
1596
  return;
@@ -1510,9 +1614,11 @@ export class TerminalSnapshotOracle {
1510
1614
  return;
1511
1615
  }
1512
1616
 
1617
+ const params = parseCsiParams(rawParams, paramsStart);
1618
+ const first = Number.isFinite(params[0]) ? (params[0] as number) : 1;
1619
+
1513
1620
  if (finalByte === 'm') {
1514
- const cleaned = params.filter((value) => Number.isFinite(value));
1515
- this.style = applySgrParams(this.style, cleaned, this.indexedPaletteOverrides);
1621
+ this.style = applySgrParams(this.style, params, this.indexedPaletteOverrides);
1516
1622
  return;
1517
1623
  }
1518
1624
 
@@ -1626,8 +1732,15 @@ export class TerminalSnapshotOracle {
1626
1732
  }
1627
1733
  }
1628
1734
 
1629
- private emitCsiQuery(payload: string): void {
1630
- this.queryHooks?.onCsiQuery?.(payload, () => this.queryState());
1735
+ private emitCsiQuery(rawParams: string, finalByte: string): void {
1736
+ const onCsiQuery = this.queryHooks?.onCsiQuery;
1737
+ if (onCsiQuery === undefined) {
1738
+ return;
1739
+ }
1740
+ if (!shouldDispatchCsiHook(rawParams, finalByte)) {
1741
+ return;
1742
+ }
1743
+ onCsiQuery(`${rawParams}${finalByte}`, () => this.queryState());
1631
1744
  }
1632
1745
 
1633
1746
  private emitOscQuery(useBellTerminator: boolean): void {
@@ -1,8 +1,8 @@
1
1
  import { readFileSync } from 'node:fs';
2
2
  import { resolve } from 'node:path';
3
3
  import type { HarnessMuxThemeConfig } from '../config/config-core.ts';
4
- import type { UiColor, UiStyle } from './surface.ts';
5
- import type { UiModalTheme } from './kit.ts';
4
+ import type { UiModalTheme } from '../../packages/harness-ui/src/kit.ts';
5
+ import type { UiColor, UiStyle } from '../../packages/harness-ui/src/surface.ts';
6
6
  import { BUILTIN_MUX_THEME_PRESETS } from './mux-theme-presets.ts';
7
7
 
8
8
  type OpenCodeThemeMode = 'dark' | 'light';
@@ -47,11 +47,28 @@ export interface MuxWorkspaceRailTheme {
47
47
  readonly statusColors: MuxWorkspaceRailStatusColors;
48
48
  }
49
49
 
50
+ interface MuxConversationRailTheme {
51
+ readonly headerStyle: UiStyle;
52
+ readonly normalRowStyle: UiStyle;
53
+ readonly activeRowStyle: UiStyle;
54
+ readonly activeIndicatorStyle: UiStyle;
55
+ readonly normalTextStyle: UiStyle;
56
+ readonly activeTextStyle: UiStyle;
57
+ readonly deadTextStyle: UiStyle;
58
+ readonly statusBadgeStyles: {
59
+ readonly needsInput: UiStyle;
60
+ readonly running: UiStyle;
61
+ readonly completed: UiStyle;
62
+ readonly exited: UiStyle;
63
+ };
64
+ }
65
+
50
66
  interface ActiveMuxTheme {
51
67
  readonly name: string;
52
68
  readonly mode: OpenCodeThemeMode;
53
69
  readonly modalTheme: UiModalTheme;
54
70
  readonly workspaceRail: MuxWorkspaceRailTheme;
71
+ readonly conversationRail: MuxConversationRailTheme;
55
72
  readonly terminalForegroundHex: string | null;
56
73
  readonly terminalBackgroundHex: string | null;
57
74
  }
@@ -170,6 +187,65 @@ const LEGACY_MUX_THEME: ActiveMuxTheme = {
170
187
  idle: { kind: 'indexed', index: 245 },
171
188
  },
172
189
  },
190
+ conversationRail: {
191
+ headerStyle: {
192
+ fg: { kind: 'indexed', index: 250 },
193
+ bg: { kind: 'indexed', index: 236 },
194
+ bold: false,
195
+ },
196
+ normalRowStyle: {
197
+ fg: { kind: 'default' },
198
+ bg: { kind: 'default' },
199
+ bold: false,
200
+ },
201
+ activeRowStyle: {
202
+ fg: { kind: 'indexed', index: 255 },
203
+ bg: { kind: 'indexed', index: 238 },
204
+ bold: false,
205
+ },
206
+ activeIndicatorStyle: {
207
+ fg: { kind: 'indexed', index: 231 },
208
+ bg: { kind: 'indexed', index: 238 },
209
+ bold: true,
210
+ },
211
+ normalTextStyle: {
212
+ fg: { kind: 'default' },
213
+ bg: { kind: 'default' },
214
+ bold: false,
215
+ },
216
+ activeTextStyle: {
217
+ fg: { kind: 'indexed', index: 255 },
218
+ bg: { kind: 'indexed', index: 238 },
219
+ bold: false,
220
+ },
221
+ deadTextStyle: {
222
+ fg: { kind: 'indexed', index: 245 },
223
+ bg: { kind: 'default' },
224
+ bold: false,
225
+ },
226
+ statusBadgeStyles: {
227
+ needsInput: {
228
+ fg: { kind: 'indexed', index: 231 },
229
+ bg: { kind: 'indexed', index: 166 },
230
+ bold: true,
231
+ },
232
+ running: {
233
+ fg: { kind: 'indexed', index: 231 },
234
+ bg: { kind: 'indexed', index: 24 },
235
+ bold: true,
236
+ },
237
+ completed: {
238
+ fg: { kind: 'indexed', index: 231 },
239
+ bg: { kind: 'indexed', index: 28 },
240
+ bold: true,
241
+ },
242
+ exited: {
243
+ fg: { kind: 'indexed', index: 250 },
244
+ bg: { kind: 'indexed', index: 239 },
245
+ bold: true,
246
+ },
247
+ },
248
+ },
173
249
  terminalForegroundHex: null,
174
250
  terminalBackgroundHex: null,
175
251
  };
@@ -290,6 +366,15 @@ function uiStyle(fgHex: string, bgHex: string | null, bold = false): UiStyle {
290
366
  };
291
367
  }
292
368
 
369
+ function preferredForegroundHex(backgroundHex: string): string {
370
+ const normalized = normalizeHex(backgroundHex) ?? '#000000';
371
+ const red = Number.parseInt(normalized.slice(1, 3), 16);
372
+ const green = Number.parseInt(normalized.slice(3, 5), 16);
373
+ const blue = Number.parseInt(normalized.slice(5, 7), 16);
374
+ const luminance = 0.299 * red + 0.587 * green + 0.114 * blue;
375
+ return luminance > 150 ? '#111111' : '#f5f7fa';
376
+ }
377
+
293
378
  function resolveThemeDocumentFromFile(
294
379
  path: string,
295
380
  readFile: (path: string) => string,
@@ -310,10 +395,12 @@ function resolveThemeDocumentFromFile(
310
395
  }
311
396
 
312
397
  function buildActiveTheme(name: string, input: OpenCodeThemeInput): ActiveMuxTheme {
398
+ const hasSelectedListItemText = input.document.theme['selectedListItemText'] !== undefined;
313
399
  const text = themeHex(input, 'text', FALLBACK_HEX.text);
314
400
  const textMuted = themeHex(input, 'textMuted', FALLBACK_HEX.textMuted);
315
401
  const conceal = themeHex(input, 'conceal', FALLBACK_HEX.conceal);
316
402
  const primary = themeHex(input, 'primary', FALLBACK_HEX.primary);
403
+ const accent = themeHex(input, 'accent', primary);
317
404
  const success = themeHex(input, 'success', FALLBACK_HEX.success);
318
405
  const error = themeHex(input, 'error', FALLBACK_HEX.error);
319
406
  const warning = themeHex(input, 'warning', FALLBACK_HEX.warning);
@@ -321,7 +408,9 @@ function buildActiveTheme(name: string, input: OpenCodeThemeInput): ActiveMuxThe
321
408
  const background = themeHex(input, 'background', FALLBACK_HEX.background);
322
409
  const backgroundPanel = themeHex(input, 'backgroundPanel', FALLBACK_HEX.backgroundPanel);
323
410
  const backgroundElement = themeHex(input, 'backgroundElement', FALLBACK_HEX.backgroundElement);
324
- const syntaxFunction = themeHex(input, 'syntaxFunction', primary);
411
+ const selectedListItemText = themeHex(input, 'selectedListItemText', text);
412
+ const selectedForegroundFor = (backgroundHex: string): string =>
413
+ hasSelectedListItemText ? selectedListItemText : preferredForegroundHex(backgroundHex);
325
414
 
326
415
  return {
327
416
  name,
@@ -334,23 +423,38 @@ function buildActiveTheme(name: string, input: OpenCodeThemeInput): ActiveMuxThe
334
423
  },
335
424
  workspaceRail: {
336
425
  normalStyle: uiStyle(text, null, false),
337
- headerStyle: uiStyle(primary, null, true),
426
+ headerStyle: uiStyle(text, null, true),
338
427
  activeRowStyle: uiStyle(text, backgroundElement, false),
339
428
  metaStyle: uiStyle(textMuted, null, false),
340
429
  conversationBodyStyle: uiStyle(textMuted, null, false),
341
- processStyle: uiStyle(info, null, false),
342
- repositoryRowStyle: uiStyle(syntaxFunction, null, false),
430
+ processStyle: uiStyle(textMuted, null, false),
431
+ repositoryRowStyle: uiStyle(text, null, false),
343
432
  mutedStyle: uiStyle(conceal, null, false),
344
433
  shortcutStyle: uiStyle(textMuted, null, false),
345
- actionStyle: uiStyle(primary, backgroundElement, false),
434
+ actionStyle: uiStyle(accent, backgroundElement, false),
346
435
  statusColors: {
347
436
  working: hexToUiColor(success),
348
437
  exited: hexToUiColor(error),
349
438
  needsAction: hexToUiColor(warning),
350
- starting: hexToUiColor(primary),
439
+ starting: hexToUiColor(info),
351
440
  idle: hexToUiColor(conceal),
352
441
  },
353
442
  },
443
+ conversationRail: {
444
+ headerStyle: uiStyle(textMuted, backgroundPanel, false),
445
+ normalRowStyle: uiStyle(text, null, false),
446
+ activeRowStyle: uiStyle(text, backgroundElement, false),
447
+ activeIndicatorStyle: uiStyle(accent, backgroundElement, true),
448
+ normalTextStyle: uiStyle(text, null, false),
449
+ activeTextStyle: uiStyle(text, backgroundElement, false),
450
+ deadTextStyle: uiStyle(conceal, null, false),
451
+ statusBadgeStyles: {
452
+ needsInput: uiStyle(selectedForegroundFor(warning), warning, true),
453
+ running: uiStyle(selectedForegroundFor(info), info, true),
454
+ completed: uiStyle(selectedForegroundFor(success), success, true),
455
+ exited: uiStyle(selectedForegroundFor(backgroundElement), backgroundElement, true),
456
+ },
457
+ },
354
458
  terminalForegroundHex: text.slice(1),
355
459
  terminalBackgroundHex: background.slice(1),
356
460
  };