@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,11 +1,13 @@
1
1
  import {
2
- COMMAND_MENU_MAX_RESULTS,
3
2
  clampCommandMenuState,
3
+ moveSelectionByDelta,
4
4
  reduceCommandMenuInput,
5
5
  resolveCommandMenuMatches,
6
+ resolveCommandMenuPage,
6
7
  type CommandMenuActionDescriptor,
7
8
  type CommandMenuState,
8
9
  } from './command-menu.ts';
10
+ import { parseMuxInputChunk, wheelDeltaRowsFromCode } from '../dual-pane-core.ts';
9
11
 
10
12
  interface HandleCommandMenuInputOptions {
11
13
  readonly input: Buffer;
@@ -28,29 +30,45 @@ const COMMAND_MENU_BODY_ROW_OFFSET = 2;
28
30
  const COMMAND_MENU_ACTION_ROW_START = 2;
29
31
  const THEME_PICKER_SCOPE = 'theme-select';
30
32
 
33
+ function selectionDeltaFromWheelInput(input: Buffer): number {
34
+ if (!input.includes(0x1b) || !input.includes(0x3c)) {
35
+ return 0;
36
+ }
37
+ const parsed = parseMuxInputChunk('', input);
38
+ let selectionDelta = 0;
39
+ for (const token of parsed.tokens) {
40
+ if (token.kind !== 'mouse') {
41
+ continue;
42
+ }
43
+ const wheelDelta = wheelDeltaRowsFromCode(token.event.code);
44
+ if (wheelDelta !== null) {
45
+ selectionDelta += wheelDelta;
46
+ }
47
+ }
48
+ return selectionDelta;
49
+ }
50
+
31
51
  function resolveCommandMenuActionIdByRow(
32
52
  menu: CommandMenuState,
33
53
  actions: readonly CommandMenuActionDescriptor[],
34
54
  overlayTopRowZeroBased: number,
35
55
  rowOneBased: number,
36
56
  ): string | null {
37
- const matches = resolveCommandMenuMatches(actions, menu.query, null);
38
- if (matches.length === 0) {
57
+ const page = resolveCommandMenuPage(actions, menu);
58
+ if (page.matches.length === 0) {
39
59
  return null;
40
60
  }
41
- const selectedIndex = clampCommandMenuState(menu, matches.length).selectedIndex;
42
- const pageStart = Math.floor(selectedIndex / COMMAND_MENU_MAX_RESULTS) * COMMAND_MENU_MAX_RESULTS;
43
- const visibleMatches = matches.slice(pageStart, pageStart + COMMAND_MENU_MAX_RESULTS);
44
61
  const actionStartBodyLine = COMMAND_MENU_ACTION_ROW_START;
45
62
  const clickedBodyLine = rowOneBased - 1 - (overlayTopRowZeroBased + COMMAND_MENU_BODY_ROW_OFFSET);
46
63
  if (clickedBodyLine < actionStartBodyLine) {
47
64
  return null;
48
65
  }
49
- const visibleIndex = clickedBodyLine - actionStartBodyLine;
50
- if (visibleIndex < 0 || visibleIndex >= visibleMatches.length) {
66
+ const displayEntryIndex = clickedBodyLine - actionStartBodyLine;
67
+ if (displayEntryIndex < 0 || displayEntryIndex >= page.displayEntries.length) {
51
68
  return null;
52
69
  }
53
- return visibleMatches[visibleIndex]?.action.id ?? null;
70
+ const entry = page.displayEntries[displayEntryIndex]!;
71
+ return entry.action.id;
54
72
  }
55
73
 
56
74
  export function handleCommandMenuInput(options: HandleCommandMenuInputOptions): boolean {
@@ -82,6 +100,25 @@ export function handleCommandMenuInput(options: HandleCommandMenuInputOptions):
82
100
  markDirty();
83
101
  return true;
84
102
  }
103
+ const wheelSelectionDelta = selectionDeltaFromWheelInput(input);
104
+ if (wheelSelectionDelta !== 0) {
105
+ const currentMatches = resolveCommandMenuMatches(resolveActions(), menu.query, null);
106
+ const clampedMenu = clampCommandMenuState(menu, currentMatches.length);
107
+ const nextSelectedIndex = moveSelectionByDelta(
108
+ clampedMenu.selectedIndex,
109
+ currentMatches.length,
110
+ wheelSelectionDelta,
111
+ );
112
+ if (nextSelectedIndex !== menu.selectedIndex) {
113
+ setMenu({
114
+ scope: menu.scope,
115
+ query: menu.query,
116
+ selectedIndex: nextSelectedIndex,
117
+ });
118
+ markDirty();
119
+ }
120
+ return true;
121
+ }
85
122
  const maybeMouseSequence = input.includes(0x3c);
86
123
  if (
87
124
  maybeMouseSequence &&
@@ -56,6 +56,8 @@ interface HandleNewThreadPromptInputOptions {
56
56
  setPrompt: (prompt: NewThreadPromptState | null) => void;
57
57
  }
58
58
 
59
+ const MOUSE_EVENT_PREFIX = Buffer.from('\u001b[<', 'utf8');
60
+
59
61
  export function handleConversationTitleEditInput(
60
62
  options: HandleConversationTitleEditInputOptions,
61
63
  ): boolean {
@@ -92,7 +94,9 @@ export function handleConversationTitleEditInput(
92
94
  markDirty();
93
95
  return true;
94
96
  }
97
+ const maybeMouseSequence = input.includes(MOUSE_EVENT_PREFIX);
95
98
  if (
99
+ maybeMouseSequence &&
96
100
  dismissOnOutsideClick(
97
101
  input,
98
102
  () => {
@@ -165,7 +169,7 @@ export function handleNewThreadPromptInput(options: HandleNewThreadPromptInputOp
165
169
  markDirty();
166
170
  return true;
167
171
  }
168
- const maybeMouseSequence = input.includes(0x3c);
172
+ const maybeMouseSequence = input.includes(MOUSE_EVENT_PREFIX);
169
173
  if (
170
174
  maybeMouseSequence &&
171
175
  dismissOnOutsideClick(
@@ -1,6 +1,6 @@
1
1
  export interface TaskEditorPromptInputState {
2
2
  title: string;
3
- description: string;
3
+ body: string;
4
4
  repositoryIds: readonly string[];
5
5
  repositoryIndex: number;
6
6
  fieldIndex: 0 | 1 | 2;
@@ -8,7 +8,7 @@ export interface TaskEditorPromptInputState {
8
8
 
9
9
  interface TaskEditorPromptReduction {
10
10
  title: string;
11
- description: string;
11
+ body: string;
12
12
  repositoryIndex: number;
13
13
  fieldIndex: 0 | 1 | 2;
14
14
  submit: boolean;
@@ -19,8 +19,25 @@ interface LinePromptReduction {
19
19
  submit: boolean;
20
20
  }
21
21
 
22
+ export interface LinePromptInputState {
23
+ readonly inBracketedPaste: boolean;
24
+ readonly pendingSequence: Buffer;
25
+ }
26
+
27
+ interface StatefulLinePromptReduction extends LinePromptReduction {
28
+ readonly lineInputState: LinePromptInputState;
29
+ }
30
+
22
31
  const BRACKETED_PASTE_START = Buffer.from('\u001b[200~', 'utf8');
23
32
  const BRACKETED_PASTE_END = Buffer.from('\u001b[201~', 'utf8');
33
+ const EMPTY_BUFFER = Buffer.alloc(0);
34
+
35
+ export function createLinePromptInputState(): LinePromptInputState {
36
+ return {
37
+ inBracketedPaste: false,
38
+ pendingSequence: EMPTY_BUFFER,
39
+ };
40
+ }
24
41
 
25
42
  function matchesSequence(input: Buffer, startIndex: number, sequence: Buffer): boolean {
26
43
  if (startIndex < 0 || startIndex + sequence.length > input.length) {
@@ -34,22 +51,60 @@ function matchesSequence(input: Buffer, startIndex: number, sequence: Buffer): b
34
51
  return true;
35
52
  }
36
53
 
37
- export function reduceLinePromptInput(value: string, input: Buffer): LinePromptReduction {
54
+ function isTruncatedSequencePrefix(input: Buffer, startIndex: number, sequence: Buffer): boolean {
55
+ const remaining = input.length - startIndex;
56
+ if (remaining >= sequence.length) {
57
+ return false;
58
+ }
59
+ for (let index = 0; index < remaining; index += 1) {
60
+ if (input[startIndex + index] !== sequence[index]) {
61
+ return false;
62
+ }
63
+ }
64
+ return true;
65
+ }
66
+
67
+ export function reduceLinePromptInput(value: string, input: Buffer): LinePromptReduction;
68
+ export function reduceLinePromptInput(
69
+ value: string,
70
+ input: Buffer,
71
+ lineInputState: LinePromptInputState,
72
+ ): StatefulLinePromptReduction;
73
+ export function reduceLinePromptInput(
74
+ value: string,
75
+ input: Buffer,
76
+ lineInputState?: LinePromptInputState,
77
+ ): LinePromptReduction | StatefulLinePromptReduction {
78
+ const activeState = lineInputState ?? createLinePromptInputState();
79
+ const mergedInput =
80
+ activeState.pendingSequence.length === 0
81
+ ? input
82
+ : Buffer.concat([activeState.pendingSequence, input]);
83
+
38
84
  let nextValue = value;
39
85
  let submit = false;
40
- let inBracketedPaste = false;
41
- for (let index = 0; index < input.length; index += 1) {
42
- if (!inBracketedPaste && matchesSequence(input, index, BRACKETED_PASTE_START)) {
86
+ let inBracketedPaste = activeState.inBracketedPaste;
87
+ let pendingSequence = EMPTY_BUFFER;
88
+ for (let index = 0; index < mergedInput.length; index += 1) {
89
+ if (!inBracketedPaste && matchesSequence(mergedInput, index, BRACKETED_PASTE_START)) {
43
90
  inBracketedPaste = true;
44
91
  index += BRACKETED_PASTE_START.length - 1;
45
92
  continue;
46
93
  }
47
- if (inBracketedPaste && matchesSequence(input, index, BRACKETED_PASTE_END)) {
94
+ if (matchesSequence(mergedInput, index, BRACKETED_PASTE_END)) {
48
95
  inBracketedPaste = false;
49
96
  index += BRACKETED_PASTE_END.length - 1;
50
97
  continue;
51
98
  }
52
- const byte = input[index]!;
99
+ if (
100
+ isTruncatedSequencePrefix(mergedInput, index, BRACKETED_PASTE_START) ||
101
+ isTruncatedSequencePrefix(mergedInput, index, BRACKETED_PASTE_END)
102
+ ) {
103
+ pendingSequence = Buffer.from(mergedInput.subarray(index));
104
+ break;
105
+ }
106
+
107
+ const byte = mergedInput[index]!;
53
108
  if (inBracketedPaste) {
54
109
  if (byte >= 32 && byte <= 126) {
55
110
  nextValue += String.fromCharCode(byte);
@@ -68,9 +123,19 @@ export function reduceLinePromptInput(value: string, input: Buffer): LinePromptR
68
123
  nextValue += String.fromCharCode(byte);
69
124
  }
70
125
  }
126
+ if (lineInputState === undefined) {
127
+ return {
128
+ value: nextValue,
129
+ submit,
130
+ };
131
+ }
71
132
  return {
72
133
  value: nextValue,
73
134
  submit,
135
+ lineInputState: {
136
+ inBracketedPaste,
137
+ pendingSequence,
138
+ },
74
139
  };
75
140
  }
76
141
 
@@ -79,7 +144,7 @@ export function reduceTaskEditorPromptInput(
79
144
  input: Buffer,
80
145
  ): TaskEditorPromptReduction {
81
146
  let nextTitle = prompt.title;
82
- let nextDescription = prompt.description;
147
+ let nextBody = prompt.body;
83
148
  let nextFieldIndex = prompt.fieldIndex;
84
149
  let nextRepositoryIndex = prompt.repositoryIndex;
85
150
  let submit = false;
@@ -104,7 +169,7 @@ export function reduceTaskEditorPromptInput(
104
169
  if (nextFieldIndex === 0) {
105
170
  nextTitle = nextTitle.slice(0, -1);
106
171
  } else if (nextFieldIndex === 2) {
107
- nextDescription = nextDescription.slice(0, -1);
172
+ nextBody = nextBody.slice(0, -1);
108
173
  }
109
174
  continue;
110
175
  }
@@ -112,14 +177,14 @@ export function reduceTaskEditorPromptInput(
112
177
  if (nextFieldIndex === 0) {
113
178
  nextTitle += String.fromCharCode(byte);
114
179
  } else if (nextFieldIndex === 2) {
115
- nextDescription += String.fromCharCode(byte);
180
+ nextBody += String.fromCharCode(byte);
116
181
  }
117
182
  }
118
183
  }
119
184
  }
120
185
  return {
121
186
  title: nextTitle,
122
- description: nextDescription,
187
+ body: nextBody,
123
188
  repositoryIndex: nextRepositoryIndex,
124
189
  fieldIndex: nextFieldIndex,
125
190
  submit,
@@ -8,22 +8,27 @@ import {
8
8
  resolveGoldenModalSize,
9
9
  } from '../harness-core-ui.ts';
10
10
  import {
11
- COMMAND_MENU_MAX_RESULTS,
12
- resolveCommandMenuMatches,
11
+ resolveCommandMenuPage,
13
12
  type CommandMenuActionDescriptor,
14
13
  type CommandMenuState,
15
14
  } from './command-menu.ts';
16
15
  import type { createNewThreadPromptState } from '../new-thread-prompt.ts';
17
16
  import { newThreadPromptBodyLines } from '../new-thread-prompt.ts';
18
- import { buildUiModalOverlay } from '../../ui/kit.ts';
17
+ import { UiKit, type UiModalOverlayOptions } from '../../../packages/harness-ui/src/kit.ts';
19
18
 
20
19
  type NewThreadPromptState = ReturnType<typeof createNewThreadPromptState>;
20
+ const uiKit = new UiKit();
21
+
22
+ function buildUiModalOverlay(options: UiModalOverlayOptions) {
23
+ return uiKit.buildModalOverlay(options);
24
+ }
25
+
21
26
  type UiModalThemeInput = NonNullable<Parameters<typeof buildUiModalOverlay>[0]['theme']>;
22
27
 
23
28
  interface TaskEditorPromptOverlayState {
24
29
  mode: 'create' | 'edit';
25
30
  title: string;
26
- description: string;
31
+ body: string;
27
32
  repositoryIds: readonly string[];
28
33
  repositoryIndex: number;
29
34
  fieldIndex: 0 | 1 | 2;
@@ -51,6 +56,69 @@ interface ConversationTitleOverlayState {
51
56
  persistInFlight: boolean;
52
57
  }
53
58
 
59
+ interface ReleaseNotesOverlayState {
60
+ readonly currentVersion: string;
61
+ readonly latestTag: string;
62
+ readonly releasesPageUrl: string;
63
+ readonly releases: readonly {
64
+ tag: string;
65
+ name: string;
66
+ url: string;
67
+ previewLines: readonly string[];
68
+ previewTruncated: boolean;
69
+ }[];
70
+ }
71
+
72
+ const RELEASE_NOTES_UPDATE_ACTION_BODY_LINE_INDEX = 2;
73
+ const RELEASE_NOTES_BODY_START_ROW_OFFSET = 2;
74
+ export const RELEASE_NOTES_UPDATE_ACTION_ROW_OFFSET =
75
+ RELEASE_NOTES_BODY_START_ROW_OFFSET + RELEASE_NOTES_UPDATE_ACTION_BODY_LINE_INDEX;
76
+ export const RELEASE_NOTES_UPDATE_ACTION_LABEL = '[ click to update now ]';
77
+ const COMMAND_PALETTE_MODAL_SIZE = {
78
+ preferredHeight: 18,
79
+ minWidth: 48,
80
+ maxWidth: 96,
81
+ } as const;
82
+
83
+ function truncateColumn(value: string, width: number): string {
84
+ const safeWidth = Math.max(1, width);
85
+ const normalized = value.trim();
86
+ if (normalized.length <= safeWidth) {
87
+ return normalized.padEnd(safeWidth, ' ');
88
+ }
89
+ return safeWidth <= 1 ? normalized.slice(0, safeWidth) : `${normalized.slice(0, safeWidth - 1)}…`;
90
+ }
91
+
92
+ function renderShortcutsTableRows(
93
+ page: ReturnType<typeof resolveCommandMenuPage>,
94
+ totalWidth: number,
95
+ ): readonly string[] {
96
+ const tableWidth = Math.max(30, totalWidth - 2);
97
+ const separatorWidth = 6;
98
+ const baseColumnWidth = Math.max(6, Math.floor((tableWidth - separatorWidth) / 3));
99
+ const leftWidth = baseColumnWidth;
100
+ const middleWidth = baseColumnWidth;
101
+ const rightWidth = Math.max(8, tableWidth - leftWidth - middleWidth - separatorWidth);
102
+ const rows: string[] = [];
103
+ rows.push(
104
+ `${truncateColumn('screen', leftWidth)} | ${truncateColumn('action', middleWidth)} | ${truncateColumn('bindings', rightWidth)}`,
105
+ );
106
+ for (const entry of page.displayEntries) {
107
+ const prefix = entry.absoluteIndex === page.selectedIndex ? '>' : ' ';
108
+ const screen = entry.action.screenLabel ?? 'Global';
109
+ const action = `${prefix} ${entry.action.title}`;
110
+ const bindingText =
111
+ entry.action.bindingHint?.trim() ??
112
+ entry.action.detail?.trim() ??
113
+ entry.action.sectionLabel?.trim() ??
114
+ '';
115
+ rows.push(
116
+ `${truncateColumn(screen, leftWidth)} | ${truncateColumn(action, middleWidth)} | ${truncateColumn(bindingText, rightWidth)}`,
117
+ );
118
+ }
119
+ return rows;
120
+ }
121
+
54
122
  export function buildNewThreadModalOverlay(
55
123
  layoutCols: number,
56
124
  viewportRows: number,
@@ -96,37 +164,43 @@ export function buildCommandMenuModalOverlay(
96
164
  return null;
97
165
  }
98
166
  const isThemePicker = menu.scope === 'theme-select';
167
+ const isShortcutsScope = menu.scope === 'shortcuts';
99
168
  const modalSize = resolveGoldenModalSize(layoutCols, viewportRows, {
100
- preferredHeight: 18,
101
- minWidth: 48,
102
- maxWidth: 96,
169
+ preferredHeight: isShortcutsScope ? 24 : COMMAND_PALETTE_MODAL_SIZE.preferredHeight,
170
+ minWidth: isShortcutsScope ? 84 : COMMAND_PALETTE_MODAL_SIZE.minWidth,
171
+ maxWidth: isShortcutsScope ? 132 : COMMAND_PALETTE_MODAL_SIZE.maxWidth,
103
172
  });
104
- const matches = resolveCommandMenuMatches(actions, menu.query, null);
105
- const selectedIndex =
106
- matches.length === 0 ? 0 : Math.max(0, Math.min(matches.length - 1, menu.selectedIndex));
107
- const pageStart =
108
- matches.length === 0
109
- ? 0
110
- : Math.floor(selectedIndex / COMMAND_MENU_MAX_RESULTS) * COMMAND_MENU_MAX_RESULTS;
111
- const visibleMatches = matches.slice(pageStart, pageStart + COMMAND_MENU_MAX_RESULTS);
112
- const bodyLines: string[] = [`${isThemePicker ? 'theme' : 'search'}: ${menu.query}_`, ''];
113
- if (matches.length === 0) {
173
+ const page = resolveCommandMenuPage(actions, menu);
174
+ const bodyLines: string[] = [
175
+ `${isThemePicker ? 'theme' : isShortcutsScope ? 'shortcuts' : 'search'}: ${menu.query}_`,
176
+ '',
177
+ ];
178
+ if (page.matches.length === 0) {
114
179
  bodyLines.push('no actions match');
115
180
  } else {
116
- for (let index = 0; index < visibleMatches.length; index += 1) {
117
- const match = visibleMatches[index]!;
118
- const absoluteIndex = pageStart + index;
119
- const prefix = absoluteIndex === selectedIndex ? '>' : ' ';
120
- const detail = match.action.detail?.trim() ?? '';
121
- bodyLines.push(
122
- detail.length > 0
123
- ? `${prefix} ${match.action.title} - ${detail}`
124
- : `${prefix} ${match.action.title}`,
125
- );
181
+ if (isShortcutsScope) {
182
+ bodyLines.push(...renderShortcutsTableRows(page, modalSize.width));
183
+ } else {
184
+ for (const entry of page.displayEntries) {
185
+ const prefix = entry.absoluteIndex === page.selectedIndex ? '>' : ' ';
186
+ const detail = entry.action.detail?.trim() ?? '';
187
+ bodyLines.push(
188
+ detail.length > 0
189
+ ? `${prefix} ${entry.action.title} - ${detail}`
190
+ : `${prefix} ${entry.action.title}`,
191
+ );
192
+ }
126
193
  }
127
194
  }
128
- bodyLines.push('', isThemePicker ? 'type to filter themes' : 'type to filter');
129
- const title = isThemePicker ? 'Choose Theme' : 'Command Menu';
195
+ bodyLines.push(
196
+ '',
197
+ isThemePicker
198
+ ? 'type to filter themes'
199
+ : isShortcutsScope
200
+ ? 'type to filter keybindings'
201
+ : 'type to filter',
202
+ );
203
+ const title = isThemePicker ? 'Choose Theme' : isShortcutsScope ? 'Shortcuts' : 'Command Menu';
130
204
  return buildUiModalOverlay({
131
205
  viewportCols: layoutCols,
132
206
  viewportRows,
@@ -136,7 +210,11 @@ export function buildCommandMenuModalOverlay(
136
210
  marginRows: 1,
137
211
  title,
138
212
  bodyLines,
139
- footer: isThemePicker ? 'enter apply esc cancel' : 'enter run esc',
213
+ footer: isThemePicker
214
+ ? 'enter apply esc cancel'
215
+ : isShortcutsScope
216
+ ? 'enter close esc'
217
+ : 'enter run esc',
140
218
  theme,
141
219
  });
142
220
  }
@@ -199,7 +277,7 @@ export function buildTaskEditorModalOverlay(
199
277
  const taskBody = [
200
278
  `${prompt.fieldIndex === 0 ? '>' : ' '} title: ${prompt.title}${prompt.fieldIndex === 0 ? '_' : ''}`,
201
279
  `${prompt.fieldIndex === 1 ? '>' : ' '} repository: ${selectedRepositoryName}`,
202
- `${prompt.fieldIndex === 2 ? '>' : ' '} description: ${prompt.description}${
280
+ `${prompt.fieldIndex === 2 ? '>' : ' '} body: ${prompt.body}${
203
281
  prompt.fieldIndex === 2 ? '_' : ''
204
282
  }`,
205
283
  '',
@@ -270,9 +348,9 @@ export function buildApiKeyModalOverlay(
270
348
  return null;
271
349
  }
272
350
  const modalSize = resolveGoldenModalSize(layoutCols, viewportRows, {
273
- preferredHeight: 16,
274
- minWidth: 34,
275
- maxWidth: 64,
351
+ preferredHeight: COMMAND_PALETTE_MODAL_SIZE.preferredHeight,
352
+ minWidth: COMMAND_PALETTE_MODAL_SIZE.minWidth,
353
+ maxWidth: COMMAND_PALETTE_MODAL_SIZE.maxWidth,
276
354
  });
277
355
  const promptValue = prompt.value.length > 0 ? prompt.value : '(enter value)';
278
356
  const bodyLines = [`${prompt.keyName}: ${promptValue}_`];
@@ -338,3 +416,59 @@ export function buildConversationTitleModalOverlay(
338
416
  theme,
339
417
  });
340
418
  }
419
+
420
+ export function buildReleaseNotesModalOverlay(
421
+ layoutCols: number,
422
+ viewportRows: number,
423
+ prompt: ReleaseNotesOverlayState | null,
424
+ theme: UiModalThemeInput,
425
+ ): ReturnType<typeof buildUiModalOverlay> | null {
426
+ if (prompt === null) {
427
+ return null;
428
+ }
429
+ const modalSize = resolveGoldenModalSize(layoutCols, viewportRows, {
430
+ preferredHeight: 24,
431
+ minWidth: 48,
432
+ maxWidth: 110,
433
+ });
434
+ const bodyLines: string[] = [
435
+ `installed: v${prompt.currentVersion}`,
436
+ `latest: ${prompt.latestTag}`,
437
+ RELEASE_NOTES_UPDATE_ACTION_LABEL,
438
+ '',
439
+ ];
440
+ const hasPreviewContent = prompt.releases.some(
441
+ (release) => release.previewLines.length > 0 || release.previewTruncated,
442
+ );
443
+ if (!hasPreviewContent) {
444
+ bodyLines.push(`version available: ${prompt.latestTag}`);
445
+ bodyLines.push('release notes not published yet');
446
+ bodyLines.push(`cmd+click: ${prompt.releases[0]?.url ?? prompt.releasesPageUrl}`, '');
447
+ } else {
448
+ for (const release of prompt.releases) {
449
+ const heading =
450
+ release.name.trim().length > 0 ? `${release.tag} - ${release.name}` : release.tag;
451
+ bodyLines.push(heading);
452
+ for (const line of release.previewLines) {
453
+ bodyLines.push(` ${line}`);
454
+ }
455
+ if (release.previewTruncated) {
456
+ bodyLines.push(' ...');
457
+ }
458
+ bodyLines.push(` cmd+click: ${release.url}`, '');
459
+ }
460
+ }
461
+ bodyLines.push(`all releases: ${prompt.releasesPageUrl}`);
462
+ return buildUiModalOverlay({
463
+ viewportCols: layoutCols,
464
+ viewportRows,
465
+ width: modalSize.width,
466
+ height: modalSize.height,
467
+ anchor: 'center',
468
+ marginRows: 1,
469
+ title: "What's New",
470
+ bodyLines,
471
+ footer: 'click update enter dismiss u update n never o open latest',
472
+ theme,
473
+ });
474
+ }
@@ -1,18 +1,14 @@
1
1
  import { parseMuxInputChunk } from '../dual-pane-core.ts';
2
- import { type buildUiModalOverlay } from '../../ui/kit.ts';
2
+ import type { UiModalOverlay } from '../../../packages/harness-ui/src/kit.ts';
3
3
  import { isMotionMouseCode, isWheelMouseCode } from './selection.ts';
4
4
 
5
5
  interface DismissModalOnOutsideClickOptions {
6
6
  input: Buffer;
7
7
  inputRemainder: string;
8
8
  dismiss: () => void;
9
- buildCurrentModalOverlay: () => ReturnType<typeof buildUiModalOverlay> | null;
9
+ buildCurrentModalOverlay: () => UiModalOverlay | null;
10
10
  onInsidePointerPress?: (col: number, row: number) => boolean;
11
- isOverlayHit: (
12
- overlay: ReturnType<typeof buildUiModalOverlay>,
13
- col: number,
14
- row: number,
15
- ) => boolean;
11
+ isOverlayHit: (overlay: UiModalOverlay, col: number, row: number) => boolean;
16
12
  }
17
13
 
18
14
  interface DismissModalOnOutsideClickResult {
@@ -1,4 +1,8 @@
1
- import { reduceLinePromptInput } from './modal-input-reducers.ts';
1
+ import {
2
+ createLinePromptInputState,
3
+ reduceLinePromptInput,
4
+ type LinePromptInputState,
5
+ } from './modal-input-reducers.ts';
2
6
 
3
7
  interface AddDirectoryPromptState {
4
8
  value: string;
@@ -18,6 +22,7 @@ interface ApiKeyPromptState {
18
22
  readonly value: string;
19
23
  readonly error: string | null;
20
24
  readonly hasExistingValue: boolean;
25
+ readonly lineInputState?: LinePromptInputState;
21
26
  }
22
27
 
23
28
  interface HandleAddDirectoryPromptInputOptions {
@@ -54,6 +59,8 @@ interface HandleApiKeyPromptInputOptions {
54
59
  persistApiKey: (keyName: string, value: string) => void;
55
60
  }
56
61
 
62
+ const MOUSE_EVENT_PREFIX = Buffer.from('\u001b[<', 'utf8');
63
+
57
64
  export function handleAddDirectoryPromptInput(
58
65
  options: HandleAddDirectoryPromptInputOptions,
59
66
  ): boolean {
@@ -78,7 +85,9 @@ export function handleAddDirectoryPromptInput(
78
85
  markDirty();
79
86
  return true;
80
87
  }
88
+ const maybeMouseSequence = input.includes(MOUSE_EVENT_PREFIX);
81
89
  if (
90
+ maybeMouseSequence &&
82
91
  dismissOnOutsideClick(input, () => {
83
92
  setPrompt(null);
84
93
  markDirty();
@@ -141,7 +150,9 @@ export function handleRepositoryPromptInput(options: HandleRepositoryPromptInput
141
150
  markDirty();
142
151
  return true;
143
152
  }
153
+ const maybeMouseSequence = input.includes(MOUSE_EVENT_PREFIX);
144
154
  if (
155
+ maybeMouseSequence &&
145
156
  dismissOnOutsideClick(input, () => {
146
157
  setPrompt(null);
147
158
  markDirty();
@@ -225,7 +236,9 @@ export function handleApiKeyPromptInput(options: HandleApiKeyPromptInputOptions)
225
236
  markDirty();
226
237
  return true;
227
238
  }
239
+ const maybeMouseSequence = input.includes(MOUSE_EVENT_PREFIX);
228
240
  if (
241
+ maybeMouseSequence &&
229
242
  dismissOnOutsideClick(input, () => {
230
243
  setPrompt(null);
231
244
  markDirty();
@@ -234,13 +247,19 @@ export function handleApiKeyPromptInput(options: HandleApiKeyPromptInputOptions)
234
247
  return true;
235
248
  }
236
249
 
237
- const reduced = reduceLinePromptInput(prompt.value, input);
250
+ const reduced = reduceLinePromptInput(
251
+ prompt.value,
252
+ input,
253
+ prompt.lineInputState ?? createLinePromptInputState(),
254
+ );
238
255
  const value = reduced.value;
256
+ const lineInputState = reduced.lineInputState;
239
257
  if (!reduced.submit) {
240
258
  setPrompt({
241
259
  ...prompt,
242
260
  value,
243
261
  error: null,
262
+ lineInputState,
244
263
  });
245
264
  markDirty();
246
265
  return true;
@@ -252,6 +271,7 @@ export function handleApiKeyPromptInput(options: HandleApiKeyPromptInputOptions)
252
271
  ...prompt,
253
272
  value,
254
273
  error: `${prompt.displayName.toLowerCase()} required`,
274
+ lineInputState,
255
275
  });
256
276
  markDirty();
257
277
  return true;
@@ -265,6 +285,7 @@ export function handleApiKeyPromptInput(options: HandleApiKeyPromptInputOptions)
265
285
  ...prompt,
266
286
  value,
267
287
  error: message,
288
+ lineInputState,
268
289
  });
269
290
  }
270
291
  markDirty();