@jmoyers/harness 0.1.10 → 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 (239) hide show
  1. package/README.md +31 -35
  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/{src/ui/modals/manager.ts → packages/harness-ui/src/modal-manager.ts} +94 -64
  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 -3721
  38. package/scripts/control-plane-daemon.ts +24 -2
  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 -3007
  43. package/scripts/nim-tui-smoke.ts +748 -0
  44. package/src/cli/auth/runtime.ts +948 -0
  45. package/src/cli/default-gateway-pointer.ts +193 -0
  46. package/src/cli/gateway/runtime.ts +1872 -0
  47. package/src/cli/parsing/flags.ts +23 -0
  48. package/src/cli/parsing/session.ts +42 -0
  49. package/src/cli/runtime/context.ts +193 -0
  50. package/src/cli/runtime-app/application.ts +392 -0
  51. package/src/cli/runtime-infra/gateway-control.ts +729 -0
  52. package/{scripts/harness-inspector.ts → src/cli/workflows/inspector.ts} +14 -11
  53. package/src/cli/workflows/runtime.ts +965 -0
  54. package/src/clients/tui/left-rail-interactions.ts +519 -0
  55. package/src/clients/tui/main-pane-interactions.ts +509 -0
  56. package/src/clients/tui/modal-input-routing.ts +71 -0
  57. package/src/clients/tui/render-snapshot-adapter.ts +88 -0
  58. package/src/clients/web/synced-selectors.ts +132 -0
  59. package/src/codex/live-session.ts +82 -29
  60. package/src/config/config-core.ts +361 -10
  61. package/src/config/harness-paths.ts +4 -7
  62. package/src/config/harness-runtime-migration.ts +142 -19
  63. package/src/config/harness.config.template.jsonc +33 -0
  64. package/src/config/secrets-core.ts +92 -4
  65. package/src/control-plane/agent-realtime-api.ts +82 -427
  66. package/src/control-plane/prompt/thread-title-namer.ts +49 -23
  67. package/src/control-plane/session-summary.ts +10 -81
  68. package/src/control-plane/status/reducer-base.ts +12 -12
  69. package/src/control-plane/status/reducers/claude-status-reducer.ts +3 -3
  70. package/src/control-plane/status/reducers/codex-status-reducer.ts +4 -4
  71. package/src/control-plane/status/reducers/cursor-status-reducer.ts +3 -3
  72. package/src/control-plane/stream-client.ts +12 -2
  73. package/src/control-plane/stream-command-parser.ts +83 -143
  74. package/src/control-plane/stream-protocol.ts +53 -37
  75. package/src/control-plane/stream-server-background.ts +18 -2
  76. package/src/control-plane/stream-server-command.ts +376 -69
  77. package/src/control-plane/stream-server-session-runtime.ts +3 -2
  78. package/src/control-plane/stream-server.ts +943 -80
  79. package/src/control-plane/stream-session-runtime-types.ts +41 -0
  80. package/src/{mux/live-mux/control-plane-records.ts → core/contracts/records.ts} +24 -97
  81. package/src/core/state/observed-stream-cursor.ts +43 -0
  82. package/src/core/state/synced-observed-state.ts +273 -0
  83. package/src/core/store/harness-synced-store.ts +81 -0
  84. package/src/diff/budget.ts +136 -0
  85. package/src/diff/build.ts +289 -0
  86. package/src/diff/chunker.ts +146 -0
  87. package/src/diff/git-invoke.ts +315 -0
  88. package/src/diff/git-parse.ts +472 -0
  89. package/src/diff/hash.ts +70 -0
  90. package/src/diff/index.ts +24 -0
  91. package/src/diff/normalize.ts +134 -0
  92. package/src/diff/types.ts +178 -0
  93. package/src/diff-ui/args.ts +346 -0
  94. package/src/diff-ui/commands.ts +123 -0
  95. package/src/diff-ui/finder.ts +94 -0
  96. package/src/diff-ui/highlight.ts +127 -0
  97. package/src/diff-ui/index.ts +2 -0
  98. package/src/diff-ui/model.ts +141 -0
  99. package/src/diff-ui/pager.ts +412 -0
  100. package/src/diff-ui/render.ts +337 -0
  101. package/src/diff-ui/runtime.ts +379 -0
  102. package/src/diff-ui/state.ts +224 -0
  103. package/src/diff-ui/types.ts +236 -0
  104. package/src/domain/conversations.ts +11 -7
  105. package/src/domain/workspace.ts +76 -4
  106. package/src/mux/control-plane-op-queue.ts +93 -7
  107. package/src/mux/conversation-rail.ts +28 -71
  108. package/src/mux/dual-pane-core.ts +13 -13
  109. package/src/mux/harness-core-ui.ts +313 -42
  110. package/src/mux/input-shortcuts.ts +22 -112
  111. package/src/mux/keybinding-catalog.ts +340 -0
  112. package/src/mux/keybinding-registry.ts +103 -0
  113. package/src/mux/live-mux/command-menu-open-in.ts +280 -0
  114. package/src/mux/live-mux/command-menu.ts +167 -4
  115. package/src/mux/live-mux/conversation-state.ts +13 -0
  116. package/src/mux/live-mux/directory-resolution.ts +1 -1
  117. package/src/mux/live-mux/git-parsing.ts +16 -0
  118. package/src/mux/live-mux/git-snapshot.ts +33 -2
  119. package/src/mux/live-mux/global-shortcut-handlers.ts +6 -0
  120. package/src/mux/live-mux/home-pane-drop.ts +1 -1
  121. package/src/mux/live-mux/home-pane-pointer.ts +10 -0
  122. package/src/mux/live-mux/input-forwarding.ts +59 -2
  123. package/src/mux/live-mux/left-nav-activation.ts +124 -7
  124. package/src/mux/live-mux/left-nav.ts +35 -0
  125. package/src/mux/live-mux/link-click.ts +292 -0
  126. package/src/mux/live-mux/modal-command-menu-handler.ts +46 -9
  127. package/src/mux/live-mux/modal-conversation-handlers.ts +5 -1
  128. package/src/mux/live-mux/modal-input-reducers.ts +106 -8
  129. package/src/mux/live-mux/modal-overlays.ts +210 -31
  130. package/src/mux/live-mux/modal-pointer.ts +3 -7
  131. package/src/mux/live-mux/modal-prompt-handlers.ts +107 -1
  132. package/src/mux/live-mux/modal-release-notes-handler.ts +111 -0
  133. package/src/mux/live-mux/modal-task-editor-handler.ts +16 -11
  134. package/src/mux/live-mux/pointer-routing.ts +5 -2
  135. package/src/mux/live-mux/project-pane-pointer.ts +8 -0
  136. package/src/mux/live-mux/rail-layout.ts +33 -30
  137. package/src/mux/live-mux/release-notes.ts +383 -0
  138. package/src/mux/live-mux/render-trace-analysis.ts +52 -7
  139. package/src/mux/live-mux/repository-folding.ts +3 -0
  140. package/src/mux/live-mux/selection.ts +0 -4
  141. package/src/mux/live-mux/session-diagnostics-paths.ts +21 -0
  142. package/src/mux/project-pane-github-review.ts +271 -0
  143. package/src/mux/render-frame.ts +4 -0
  144. package/src/mux/runtime-app/codex-live-mux-runtime.ts +5191 -0
  145. package/src/mux/task-composer.ts +21 -14
  146. package/src/mux/task-focused-pane.ts +118 -117
  147. package/src/mux/task-screen-keybindings.ts +19 -82
  148. package/src/mux/workspace-rail-model.ts +270 -104
  149. package/src/mux/workspace-rail.ts +45 -22
  150. package/src/pty/session-broker.ts +1 -1
  151. package/{scripts → src/recording}/terminal-recording-gif-lib.ts +2 -2
  152. package/src/services/control-plane.ts +50 -32
  153. package/src/services/conversation-lifecycle.ts +118 -87
  154. package/src/services/conversation-startup-hydration.ts +20 -12
  155. package/src/services/directory-hydration.ts +21 -16
  156. package/src/services/event-persistence.ts +7 -0
  157. package/src/services/left-rail-pointer-handler.ts +329 -0
  158. package/src/services/mux-ui-state-persistence.ts +5 -1
  159. package/src/services/recording.ts +34 -26
  160. package/src/services/runtime-command-menu-agent-tools.ts +1 -1
  161. package/src/services/runtime-control-actions.ts +79 -61
  162. package/src/services/runtime-control-plane-ops.ts +122 -83
  163. package/src/services/runtime-conversation-actions.ts +40 -26
  164. package/src/services/runtime-conversation-activation.ts +82 -30
  165. package/src/services/runtime-conversation-starter.ts +80 -48
  166. package/src/services/runtime-conversation-title-edit.ts +91 -80
  167. package/src/services/runtime-envelope-handler.ts +107 -105
  168. package/src/services/runtime-git-state.ts +42 -29
  169. package/src/services/runtime-layout-resize.ts +3 -1
  170. package/src/services/runtime-left-rail-render.ts +99 -63
  171. package/src/services/runtime-nim-cli-session.ts +438 -0
  172. package/src/services/runtime-nim-session.ts +705 -0
  173. package/src/services/runtime-nim-tool-bridge.ts +141 -0
  174. package/src/services/runtime-observed-event-projection-pipeline.ts +45 -0
  175. package/src/services/runtime-process-wiring.ts +29 -36
  176. package/src/services/runtime-project-pane-github-review-cache.ts +164 -0
  177. package/src/services/runtime-render-flush.ts +63 -70
  178. package/src/services/runtime-render-lifecycle.ts +65 -64
  179. package/src/services/runtime-render-orchestrator.ts +55 -45
  180. package/src/services/runtime-render-pipeline.ts +106 -103
  181. package/src/services/runtime-render-state.ts +62 -49
  182. package/src/services/runtime-repository-actions.ts +97 -70
  183. package/src/services/runtime-right-pane-render.ts +80 -53
  184. package/src/services/runtime-shutdown.ts +38 -35
  185. package/src/services/runtime-stream-subscriptions.ts +35 -27
  186. package/src/services/runtime-task-composer-persistence.ts +71 -59
  187. package/src/services/runtime-task-composer-snapshot.ts +14 -0
  188. package/src/services/runtime-task-editor-actions.ts +46 -29
  189. package/src/services/runtime-task-pane-actions.ts +220 -134
  190. package/src/services/runtime-task-pane-shortcuts.ts +323 -123
  191. package/src/services/runtime-workspace-observed-effect-queue.ts +25 -0
  192. package/src/services/runtime-workspace-observed-events.ts +33 -184
  193. package/src/services/runtime-workspace-observed-transition-policy.ts +228 -0
  194. package/src/services/session-diagnostics-store.ts +217 -0
  195. package/src/services/startup-background-resume.ts +26 -21
  196. package/src/services/startup-orchestrator.ts +16 -13
  197. package/src/services/startup-paint-tracker.ts +29 -21
  198. package/src/services/startup-persisted-conversation-queue.ts +19 -13
  199. package/src/services/startup-settled-gate.ts +25 -15
  200. package/src/services/startup-shutdown.ts +18 -22
  201. package/src/services/startup-state-hydration.ts +44 -34
  202. package/src/services/startup-visibility.ts +12 -4
  203. package/src/services/task-pane-selection-actions.ts +89 -72
  204. package/src/services/task-planning-hydration.ts +24 -18
  205. package/src/services/task-planning-observed-events.ts +50 -52
  206. package/src/services/workspace-observed-events.ts +66 -63
  207. package/src/storage/storage-lifecycle-core.ts +438 -0
  208. package/src/store/control-plane-store-normalize.ts +33 -242
  209. package/src/store/control-plane-store-types.ts +1 -35
  210. package/src/store/control-plane-store.ts +396 -56
  211. package/src/store/event-store.ts +397 -3
  212. package/src/terminal/snapshot-oracle.ts +207 -94
  213. package/src/ui/mux-theme.ts +112 -8
  214. package/src/ui/panes/home-gridfire.ts +40 -31
  215. package/src/ui/panes/home.ts +10 -2
  216. package/src/ui/panes/nim.ts +315 -0
  217. package/src/mux/live-mux/actions-task.ts +0 -115
  218. package/src/mux/live-mux/left-rail-actions.ts +0 -118
  219. package/src/mux/live-mux/left-rail-conversation-click.ts +0 -82
  220. package/src/mux/live-mux/left-rail-pointer.ts +0 -74
  221. package/src/mux/live-mux/task-pane-shortcuts.ts +0 -206
  222. package/src/services/runtime-directory-actions.ts +0 -164
  223. package/src/services/runtime-input-pipeline.ts +0 -50
  224. package/src/services/runtime-input-router.ts +0 -189
  225. package/src/services/runtime-main-pane-input.ts +0 -230
  226. package/src/services/runtime-modal-input.ts +0 -119
  227. package/src/services/runtime-navigation-input.ts +0 -197
  228. package/src/services/runtime-rail-input.ts +0 -278
  229. package/src/services/runtime-task-pane.ts +0 -62
  230. package/src/services/runtime-workspace-actions.ts +0 -158
  231. package/src/ui/conversation-input-forwarder.ts +0 -114
  232. package/src/ui/conversation-selection-input.ts +0 -103
  233. package/src/ui/global-shortcut-input.ts +0 -89
  234. package/src/ui/input.ts +0 -238
  235. package/src/ui/kit.ts +0 -509
  236. package/src/ui/left-nav-input.ts +0 -80
  237. package/src/ui/left-rail-pointer-input.ts +0 -148
  238. package/src/ui/repository-fold-input.ts +0 -91
  239. 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,10 +19,98 @@ interface LinePromptReduction {
19
19
  submit: boolean;
20
20
  }
21
21
 
22
- export function reduceLinePromptInput(value: string, input: Buffer): LinePromptReduction {
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
+
31
+ const BRACKETED_PASTE_START = Buffer.from('\u001b[200~', 'utf8');
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
+ }
41
+
42
+ function matchesSequence(input: Buffer, startIndex: number, sequence: Buffer): boolean {
43
+ if (startIndex < 0 || startIndex + sequence.length > input.length) {
44
+ return false;
45
+ }
46
+ for (let index = 0; index < sequence.length; index += 1) {
47
+ if (input[startIndex + index] !== sequence[index]) {
48
+ return false;
49
+ }
50
+ }
51
+ return true;
52
+ }
53
+
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
+
23
84
  let nextValue = value;
24
85
  let submit = false;
25
- for (const byte of input) {
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)) {
90
+ inBracketedPaste = true;
91
+ index += BRACKETED_PASTE_START.length - 1;
92
+ continue;
93
+ }
94
+ if (matchesSequence(mergedInput, index, BRACKETED_PASTE_END)) {
95
+ inBracketedPaste = false;
96
+ index += BRACKETED_PASTE_END.length - 1;
97
+ continue;
98
+ }
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]!;
108
+ if (inBracketedPaste) {
109
+ if (byte >= 32 && byte <= 126) {
110
+ nextValue += String.fromCharCode(byte);
111
+ }
112
+ continue;
113
+ }
26
114
  if (byte === 0x0d || byte === 0x0a) {
27
115
  submit = true;
28
116
  break;
@@ -35,9 +123,19 @@ export function reduceLinePromptInput(value: string, input: Buffer): LinePromptR
35
123
  nextValue += String.fromCharCode(byte);
36
124
  }
37
125
  }
126
+ if (lineInputState === undefined) {
127
+ return {
128
+ value: nextValue,
129
+ submit,
130
+ };
131
+ }
38
132
  return {
39
133
  value: nextValue,
40
134
  submit,
135
+ lineInputState: {
136
+ inBracketedPaste,
137
+ pendingSequence,
138
+ },
41
139
  };
42
140
  }
43
141
 
@@ -46,7 +144,7 @@ export function reduceTaskEditorPromptInput(
46
144
  input: Buffer,
47
145
  ): TaskEditorPromptReduction {
48
146
  let nextTitle = prompt.title;
49
- let nextDescription = prompt.description;
147
+ let nextBody = prompt.body;
50
148
  let nextFieldIndex = prompt.fieldIndex;
51
149
  let nextRepositoryIndex = prompt.repositoryIndex;
52
150
  let submit = false;
@@ -71,7 +169,7 @@ export function reduceTaskEditorPromptInput(
71
169
  if (nextFieldIndex === 0) {
72
170
  nextTitle = nextTitle.slice(0, -1);
73
171
  } else if (nextFieldIndex === 2) {
74
- nextDescription = nextDescription.slice(0, -1);
172
+ nextBody = nextBody.slice(0, -1);
75
173
  }
76
174
  continue;
77
175
  }
@@ -79,14 +177,14 @@ export function reduceTaskEditorPromptInput(
79
177
  if (nextFieldIndex === 0) {
80
178
  nextTitle += String.fromCharCode(byte);
81
179
  } else if (nextFieldIndex === 2) {
82
- nextDescription += String.fromCharCode(byte);
180
+ nextBody += String.fromCharCode(byte);
83
181
  }
84
182
  }
85
183
  }
86
184
  }
87
185
  return {
88
186
  title: nextTitle,
89
- description: nextDescription,
187
+ body: nextBody,
90
188
  repositoryIndex: nextRepositoryIndex,
91
189
  fieldIndex: nextFieldIndex,
92
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;
@@ -36,6 +41,14 @@ interface RepositoryPromptOverlayState {
36
41
  readonly error: string | null;
37
42
  }
38
43
 
44
+ interface ApiKeyPromptOverlayState {
45
+ readonly keyName: string;
46
+ readonly displayName: string;
47
+ readonly value: string;
48
+ readonly error: string | null;
49
+ readonly hasExistingValue: boolean;
50
+ }
51
+
39
52
  interface ConversationTitleOverlayState {
40
53
  value: string;
41
54
  lastSavedValue: string;
@@ -43,6 +56,69 @@ interface ConversationTitleOverlayState {
43
56
  persistInFlight: boolean;
44
57
  }
45
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
+
46
122
  export function buildNewThreadModalOverlay(
47
123
  layoutCols: number,
48
124
  viewportRows: number,
@@ -88,37 +164,43 @@ export function buildCommandMenuModalOverlay(
88
164
  return null;
89
165
  }
90
166
  const isThemePicker = menu.scope === 'theme-select';
167
+ const isShortcutsScope = menu.scope === 'shortcuts';
91
168
  const modalSize = resolveGoldenModalSize(layoutCols, viewportRows, {
92
- preferredHeight: 18,
93
- minWidth: 48,
94
- 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,
95
172
  });
96
- const matches = resolveCommandMenuMatches(actions, menu.query, null);
97
- const selectedIndex =
98
- matches.length === 0 ? 0 : Math.max(0, Math.min(matches.length - 1, menu.selectedIndex));
99
- const pageStart =
100
- matches.length === 0
101
- ? 0
102
- : Math.floor(selectedIndex / COMMAND_MENU_MAX_RESULTS) * COMMAND_MENU_MAX_RESULTS;
103
- const visibleMatches = matches.slice(pageStart, pageStart + COMMAND_MENU_MAX_RESULTS);
104
- const bodyLines: string[] = [`${isThemePicker ? 'theme' : 'search'}: ${menu.query}_`, ''];
105
- 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) {
106
179
  bodyLines.push('no actions match');
107
180
  } else {
108
- for (let index = 0; index < visibleMatches.length; index += 1) {
109
- const match = visibleMatches[index]!;
110
- const absoluteIndex = pageStart + index;
111
- const prefix = absoluteIndex === selectedIndex ? '>' : ' ';
112
- const detail = match.action.detail?.trim() ?? '';
113
- bodyLines.push(
114
- detail.length > 0
115
- ? `${prefix} ${match.action.title} - ${detail}`
116
- : `${prefix} ${match.action.title}`,
117
- );
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
+ }
118
193
  }
119
194
  }
120
- bodyLines.push('', isThemePicker ? 'type to filter themes' : 'type to filter');
121
- 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';
122
204
  return buildUiModalOverlay({
123
205
  viewportCols: layoutCols,
124
206
  viewportRows,
@@ -128,7 +210,11 @@ export function buildCommandMenuModalOverlay(
128
210
  marginRows: 1,
129
211
  title,
130
212
  bodyLines,
131
- 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',
132
218
  theme,
133
219
  });
134
220
  }
@@ -191,7 +277,7 @@ export function buildTaskEditorModalOverlay(
191
277
  const taskBody = [
192
278
  `${prompt.fieldIndex === 0 ? '>' : ' '} title: ${prompt.title}${prompt.fieldIndex === 0 ? '_' : ''}`,
193
279
  `${prompt.fieldIndex === 1 ? '>' : ' '} repository: ${selectedRepositoryName}`,
194
- `${prompt.fieldIndex === 2 ? '>' : ' '} description: ${prompt.description}${
280
+ `${prompt.fieldIndex === 2 ? '>' : ' '} body: ${prompt.body}${
195
281
  prompt.fieldIndex === 2 ? '_' : ''
196
282
  }`,
197
283
  '',
@@ -252,6 +338,43 @@ export function buildRepositoryModalOverlay(
252
338
  });
253
339
  }
254
340
 
341
+ export function buildApiKeyModalOverlay(
342
+ layoutCols: number,
343
+ viewportRows: number,
344
+ prompt: ApiKeyPromptOverlayState | null,
345
+ theme: UiModalThemeInput,
346
+ ): ReturnType<typeof buildUiModalOverlay> | null {
347
+ if (prompt === null) {
348
+ return null;
349
+ }
350
+ const modalSize = resolveGoldenModalSize(layoutCols, viewportRows, {
351
+ preferredHeight: COMMAND_PALETTE_MODAL_SIZE.preferredHeight,
352
+ minWidth: COMMAND_PALETTE_MODAL_SIZE.minWidth,
353
+ maxWidth: COMMAND_PALETTE_MODAL_SIZE.maxWidth,
354
+ });
355
+ const promptValue = prompt.value.length > 0 ? prompt.value : '(enter value)';
356
+ const bodyLines = [`${prompt.keyName}: ${promptValue}_`];
357
+ if (prompt.error !== null && prompt.error.length > 0) {
358
+ bodyLines.push(`error: ${prompt.error}`);
359
+ } else if (prompt.hasExistingValue) {
360
+ bodyLines.push('warning: existing value detected (submit will overwrite)');
361
+ } else {
362
+ bodyLines.push('value is saved to user-global secrets.env');
363
+ }
364
+ return buildUiModalOverlay({
365
+ viewportCols: layoutCols,
366
+ viewportRows,
367
+ width: modalSize.width,
368
+ height: modalSize.height,
369
+ anchor: 'center',
370
+ marginRows: 1,
371
+ title: `Set ${prompt.displayName}`,
372
+ bodyLines,
373
+ footer: 'enter save esc',
374
+ theme,
375
+ });
376
+ }
377
+
255
378
  export function buildConversationTitleModalOverlay(
256
379
  layoutCols: number,
257
380
  viewportRows: number,
@@ -293,3 +416,59 @@ export function buildConversationTitleModalOverlay(
293
416
  theme,
294
417
  });
295
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 {