@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,34 +1,49 @@
1
- import {
2
- classifyPaneAt as classifyPaneAtFrame,
3
- type computeDualPaneLayout,
4
- type parseMuxInputChunk,
5
- } from '../mux/dual-pane-core.ts';
6
- import {
7
- hasAltModifier as hasAltModifierFrame,
8
- hasShiftModifier as hasShiftModifierFrame,
9
- isLeftButtonPress as isLeftButtonPressFrame,
10
- isMotionMouseCode as isMotionMouseCodeFrame,
11
- type PaneSelection,
12
- } from '../mux/live-mux/selection.ts';
13
- import type { TerminalSnapshotFrameCore } from '../terminal/snapshot-oracle.ts';
1
+ import type {
2
+ ConversationSelectionSnapshotFrame,
3
+ PaneSelection,
4
+ } from './conversation-selection-input.ts';
14
5
 
15
- type MainPaneMode = 'conversation' | 'project' | 'home';
16
- type DualPaneLayout = ReturnType<typeof computeDualPaneLayout>;
17
- type MuxInputToken = ReturnType<typeof parseMuxInputChunk>['tokens'][number];
18
- type PointerTarget = ReturnType<typeof classifyPaneAtFrame>;
6
+ type MainPaneMode = 'conversation' | 'project' | 'home' | 'nim';
7
+ type PointerTarget = 'left' | 'right' | 'separator' | 'status' | 'outside';
19
8
 
20
9
  interface MouseSelectionEvent {
10
+ readonly sequence: string;
21
11
  readonly col: number;
22
12
  readonly row: number;
23
13
  readonly code: number;
24
14
  readonly final: 'M' | 'm';
25
15
  }
26
16
 
17
+ export interface InputTokenRouterLayout {
18
+ readonly paneRows: number;
19
+ readonly leftCols: number;
20
+ readonly rightCols: number;
21
+ readonly rightStartCol: number;
22
+ }
23
+
24
+ export interface InputTokenRouterMouseToken {
25
+ readonly kind: 'mouse';
26
+ readonly event: MouseSelectionEvent;
27
+ }
28
+
29
+ export interface InputTokenRouterPassthroughToken {
30
+ readonly kind: 'passthrough';
31
+ readonly text: string;
32
+ }
33
+
34
+ type MuxInputToken = InputTokenRouterMouseToken | InputTokenRouterPassthroughToken;
35
+
36
+ interface HomePaneSelectionContext {
37
+ readonly viewportTop: number;
38
+ readonly totalRows: number;
39
+ readonly resolveSelectionText: (selection: PaneSelection) => string;
40
+ }
41
+
27
42
  interface ConversationInputLike {
28
43
  readonly oracle: {
29
44
  isMouseTrackingEnabled: () => boolean;
30
45
  scrollViewport: (delta: number) => void;
31
- snapshotWithoutHash: () => TerminalSnapshotFrameCore;
46
+ snapshotWithoutHash: () => ConversationSelectionSnapshotFrame;
32
47
  selectionText: (anchor: PaneSelection['anchor'], focus: PaneSelection['focus']) => string;
33
48
  };
34
49
  }
@@ -94,58 +109,85 @@ interface LeftRailPointerInputLike {
94
109
  interface ConversationSelectionInputLike {
95
110
  clearSelectionOnTextToken(textLength: number): boolean;
96
111
  handleMouseSelection(input: {
97
- layout: DualPaneLayout;
98
- frame: TerminalSnapshotFrameCore;
112
+ layout: InputTokenRouterLayout;
113
+ frame: ConversationSelectionSnapshotFrame;
99
114
  isMainPaneTarget: boolean;
100
115
  resolveSelectionText?: (selection: PaneSelection) => string;
101
116
  event: MouseSelectionEvent;
102
117
  }): boolean;
103
118
  }
104
119
 
105
- interface InputTokenRouterOptions {
120
+ export interface InputTokenRouterOptions {
106
121
  readonly getMainPaneMode: () => MainPaneMode;
122
+ readonly getHomePaneSelectionContext?: () => HomePaneSelectionContext | null;
107
123
  readonly pointerRoutingInput: PointerRoutingInputLike;
108
124
  readonly mainPanePointerInput: MainPanePointerInputLike;
109
125
  readonly leftRailPointerInput: LeftRailPointerInputLike;
110
126
  readonly conversationSelectionInput: ConversationSelectionInputLike;
111
127
  }
112
128
 
113
- interface InputTokenRouterDependencies {
114
- readonly classifyPaneAt?: typeof classifyPaneAtFrame;
115
- readonly isLeftButtonPress?: typeof isLeftButtonPressFrame;
116
- readonly hasAltModifier?: typeof hasAltModifierFrame;
117
- readonly hasShiftModifier?: typeof hasShiftModifierFrame;
118
- readonly isMotionMouseCode?: typeof isMotionMouseCodeFrame;
129
+ export interface InputTokenRouterStrategies {
130
+ classifyPaneAt(layout: InputTokenRouterLayout, col: number, row: number): PointerTarget;
131
+ isLeftButtonPress(code: number, final: 'M' | 'm'): boolean;
132
+ hasAltModifier(code: number): boolean;
133
+ hasShiftModifier(code: number): boolean;
134
+ isMotionMouseCode(code: number): boolean;
119
135
  }
120
136
 
121
137
  interface RouteTokensInput {
122
138
  readonly tokens: readonly MuxInputToken[];
123
- readonly layout: DualPaneLayout;
139
+ readonly layout: InputTokenRouterLayout;
124
140
  readonly conversation: ConversationInputLike | null;
125
- readonly snapshotForInput: TerminalSnapshotFrameCore | null;
141
+ readonly snapshotForInput: ConversationSelectionSnapshotFrame | null;
126
142
  }
127
143
 
128
144
  interface RouteTokensResult {
129
145
  readonly routedTokens: MuxInputToken[];
130
- readonly snapshotForInput: TerminalSnapshotFrameCore | null;
146
+ readonly snapshotForInput: ConversationSelectionSnapshotFrame | null;
131
147
  }
132
148
 
133
149
  export class InputTokenRouter {
134
- private readonly classifyPaneAt: typeof classifyPaneAtFrame;
135
- private readonly isLeftButtonPress: typeof isLeftButtonPressFrame;
136
- private readonly hasAltModifier: typeof hasAltModifierFrame;
137
- private readonly hasShiftModifier: typeof hasShiftModifierFrame;
138
- private readonly isMotionMouseCode: typeof isMotionMouseCodeFrame;
139
-
140
150
  constructor(
141
151
  private readonly options: InputTokenRouterOptions,
142
- dependencies: InputTokenRouterDependencies = {},
143
- ) {
144
- this.classifyPaneAt = dependencies.classifyPaneAt ?? classifyPaneAtFrame;
145
- this.isLeftButtonPress = dependencies.isLeftButtonPress ?? isLeftButtonPressFrame;
146
- this.hasAltModifier = dependencies.hasAltModifier ?? hasAltModifierFrame;
147
- this.hasShiftModifier = dependencies.hasShiftModifier ?? hasShiftModifierFrame;
148
- this.isMotionMouseCode = dependencies.isMotionMouseCode ?? isMotionMouseCodeFrame;
152
+ private readonly strategies: InputTokenRouterStrategies,
153
+ ) {}
154
+
155
+ private buildHomeSelectionFrame(
156
+ layout: InputTokenRouterLayout,
157
+ context: HomePaneSelectionContext | null,
158
+ ): ConversationSelectionSnapshotFrame | null {
159
+ if (context === null) {
160
+ return null;
161
+ }
162
+ return {
163
+ rows: Math.max(1, layout.paneRows),
164
+ cols: Math.max(1, layout.rightCols),
165
+ activeScreen: 'primary',
166
+ modes: {
167
+ bracketedPaste: false,
168
+ decMouseX10: false,
169
+ decMouseButtonEvent: false,
170
+ decMouseAnyEvent: false,
171
+ decFocusTracking: false,
172
+ decMouseSgrEncoding: false,
173
+ },
174
+ cursor: {
175
+ row: 0,
176
+ col: 0,
177
+ visible: false,
178
+ style: {
179
+ shape: 'block',
180
+ blinking: false,
181
+ },
182
+ },
183
+ viewport: {
184
+ top: Math.max(0, context.viewportTop),
185
+ totalRows: Math.max(1, context.totalRows),
186
+ followOutput: true,
187
+ },
188
+ lines: [],
189
+ richLines: [],
190
+ };
149
191
  }
150
192
 
151
193
  routeTokens(input: RouteTokensInput): RouteTokensResult {
@@ -174,7 +216,7 @@ export class InputTokenRouter {
174
216
  continue;
175
217
  }
176
218
 
177
- const target = this.classifyPaneAt(input.layout, token.event.col, token.event.row);
219
+ const target = this.strategies.classifyPaneAt(input.layout, token.event.col, token.event.row);
178
220
  const rowIndex = Math.max(0, Math.min(input.layout.paneRows - 1, token.event.row - 1));
179
221
  const passThroughConversationMouse =
180
222
  input.conversation !== null &&
@@ -184,7 +226,7 @@ export class InputTokenRouter {
184
226
  snapshotForInput.activeScreen === 'alternate' &&
185
227
  snapshotForInput.viewport.followOutput &&
186
228
  input.conversation.oracle.isMouseTrackingEnabled() &&
187
- !this.hasShiftModifier(token.event.code);
229
+ !this.strategies.hasShiftModifier(token.event.code);
188
230
  if (
189
231
  this.options.pointerRoutingInput.handleHomePaneDragRelease({
190
232
  final: token.event.final,
@@ -265,9 +307,9 @@ export class InputTokenRouter {
265
307
  }
266
308
  const leftPaneConversationSelect =
267
309
  target === 'left' &&
268
- this.isLeftButtonPress(token.event.code, token.event.final) &&
269
- !this.hasAltModifier(token.event.code) &&
270
- !this.isMotionMouseCode(token.event.code);
310
+ this.strategies.isLeftButtonPress(token.event.code, token.event.final) &&
311
+ !this.strategies.hasAltModifier(token.event.code) &&
312
+ !this.strategies.isMotionMouseCode(token.event.code);
271
313
  if (
272
314
  this.options.leftRailPointerInput.handlePointerClick({
273
315
  clickEligible: leftPaneConversationSelect,
@@ -279,27 +321,36 @@ export class InputTokenRouter {
279
321
  ) {
280
322
  continue;
281
323
  }
282
- if (snapshotForInput === null || this.options.getMainPaneMode() !== 'conversation') {
283
- routedTokens.push(token);
284
- continue;
285
- }
286
- const mouseSelectionInput =
287
- resolveSelectionText === null
288
- ? {
289
- layout: input.layout,
290
- frame: snapshotForInput,
291
- isMainPaneTarget,
292
- event: token.event,
293
- }
294
- : {
295
- layout: input.layout,
296
- frame: snapshotForInput,
297
- isMainPaneTarget,
298
- resolveSelectionText,
299
- event: token.event,
300
- };
301
- if (this.options.conversationSelectionInput.handleMouseSelection(mouseSelectionInput)) {
302
- continue;
324
+ const mainPaneMode = this.options.getMainPaneMode();
325
+ const homeSelectionContext =
326
+ mainPaneMode === 'home' ? (this.options.getHomePaneSelectionContext?.() ?? null) : null;
327
+ const selectionFrame =
328
+ mainPaneMode === 'conversation'
329
+ ? snapshotForInput
330
+ : this.buildHomeSelectionFrame(input.layout, homeSelectionContext);
331
+ const selectionResolver =
332
+ mainPaneMode === 'conversation'
333
+ ? resolveSelectionText
334
+ : (homeSelectionContext?.resolveSelectionText ?? null);
335
+ if (selectionFrame !== null) {
336
+ const mouseSelectionInput =
337
+ selectionResolver === null
338
+ ? {
339
+ layout: input.layout,
340
+ frame: selectionFrame,
341
+ isMainPaneTarget,
342
+ event: token.event,
343
+ }
344
+ : {
345
+ layout: input.layout,
346
+ frame: selectionFrame,
347
+ isMainPaneTarget,
348
+ resolveSelectionText: selectionResolver,
349
+ event: token.event,
350
+ };
351
+ if (this.options.conversationSelectionInput.handleMouseSelection(mouseSelectionInput)) {
352
+ continue;
353
+ }
303
354
  }
304
355
 
305
356
  routedTokens.push(token);
@@ -0,0 +1,420 @@
1
+ export type ThreadAgentType = 'codex' | 'claude' | 'cursor' | 'terminal' | 'critique';
2
+
3
+ type CommandMenuScope = 'all' | 'thread-start' | 'theme-select' | 'shortcuts';
4
+
5
+ export interface CommandMenuState {
6
+ readonly scope: CommandMenuScope;
7
+ readonly query: string;
8
+ readonly selectedIndex: number;
9
+ }
10
+
11
+ export interface CommandMenuActionDescriptor {
12
+ readonly id: string;
13
+ readonly title: string;
14
+ readonly aliases?: readonly string[];
15
+ readonly keywords?: readonly string[];
16
+ readonly detail?: string;
17
+ readonly screenLabel?: string;
18
+ readonly sectionLabel?: string;
19
+ readonly bindingHint?: string;
20
+ readonly priority?: number;
21
+ }
22
+
23
+ export interface NewThreadPromptState {
24
+ readonly directoryId: string;
25
+ readonly selectedAgentType: ThreadAgentType;
26
+ }
27
+
28
+ export interface TaskEditorPromptState {
29
+ readonly mode: 'create' | 'edit';
30
+ readonly taskId: string | null;
31
+ readonly title: string;
32
+ readonly body: string;
33
+ readonly repositoryIds: readonly string[];
34
+ readonly repositoryIndex: number;
35
+ readonly fieldIndex: 0 | 1 | 2;
36
+ readonly error: string | null;
37
+ }
38
+
39
+ export interface RepositoryPromptState {
40
+ readonly mode: 'add' | 'edit';
41
+ readonly repositoryId: string | null;
42
+ readonly value: string;
43
+ readonly error: string | null;
44
+ }
45
+
46
+ export interface ConversationTitleEditState {
47
+ readonly conversationId: string;
48
+ readonly value: string;
49
+ readonly lastSavedValue: string;
50
+ readonly error: string | null;
51
+ readonly persistInFlight: boolean;
52
+ readonly debounceTimer: NodeJS.Timeout | null;
53
+ }
54
+
55
+ export interface TaskEditorSubmitPayload {
56
+ readonly mode: 'create' | 'edit';
57
+ readonly taskId: string | null;
58
+ readonly repositoryId: string | null;
59
+ readonly projectId?: string | null;
60
+ readonly title: string | null;
61
+ readonly body: string;
62
+ readonly commandLabel: string;
63
+ }
64
+
65
+ export interface ApiKeyPromptState {
66
+ readonly keyName: string;
67
+ readonly displayName: string;
68
+ readonly value: string;
69
+ readonly error: string | null;
70
+ readonly hasExistingValue: boolean;
71
+ }
72
+
73
+ export interface InputRouterShortcutPorts {
74
+ readonly isModalDismissShortcut: (input: Buffer) => boolean;
75
+ readonly isCommandMenuToggleShortcut: (input: Buffer) => boolean;
76
+ readonly isArchiveConversationShortcut: (input: Buffer) => boolean;
77
+ }
78
+
79
+ export interface InputRouterOverlayPorts {
80
+ readonly dismissOnOutsideClick: (
81
+ input: Buffer,
82
+ dismiss: () => void,
83
+ onInsidePointerPress?: (col: number, row: number) => boolean,
84
+ ) => boolean;
85
+ readonly buildConversationTitleModalOverlay: () => { top: number } | null;
86
+ readonly buildCommandMenuModalOverlay: () => { top: number } | null;
87
+ readonly buildNewThreadModalOverlay: () => { top: number } | null;
88
+ readonly resolveNewThreadPromptAgentByRow: (
89
+ overlayTop: number,
90
+ row: number,
91
+ ) => ThreadAgentType | null;
92
+ }
93
+
94
+ export interface InputRouterActionPorts {
95
+ readonly stopConversationTitleEdit: (persistPending: boolean) => void;
96
+ readonly queueControlPlaneOp: (task: () => Promise<void>, label: string) => void;
97
+ readonly archiveConversation: (sessionId: string) => Promise<void>;
98
+ readonly createAndActivateConversationInDirectory: (
99
+ directoryId: string,
100
+ agentType: ThreadAgentType,
101
+ ) => Promise<void>;
102
+ readonly addDirectoryByPath: (path: string) => Promise<void>;
103
+ readonly normalizeGitHubRemoteUrl: (remoteUrl: string) => string | null;
104
+ readonly upsertRepositoryByRemoteUrl: (
105
+ remoteUrl: string,
106
+ existingRepositoryId?: string,
107
+ ) => Promise<void>;
108
+ readonly repositoriesHas: (repositoryId: string) => boolean;
109
+ readonly submitTaskEditorPayload: (payload: TaskEditorSubmitPayload) => void;
110
+ readonly resolveCommandMenuActions: () => readonly CommandMenuActionDescriptor[];
111
+ readonly executeCommandMenuAction: (actionId: string) => void;
112
+ readonly persistApiKey?: (keyName: string, value: string) => void;
113
+ }
114
+
115
+ export interface InputRouterStatePorts {
116
+ readonly markDirty: () => void;
117
+ readonly conversations: ReadonlyMap<string, { title: string }>;
118
+ readonly scheduleConversationTitlePersist: () => void;
119
+ readonly getTaskEditorPrompt: () => TaskEditorPromptState | null;
120
+ readonly setTaskEditorPrompt: (next: TaskEditorPromptState | null) => void;
121
+ readonly getApiKeyPrompt?: () => ApiKeyPromptState | null;
122
+ readonly setApiKeyPrompt?: (next: ApiKeyPromptState | null) => void;
123
+ readonly getConversationTitleEdit: () => ConversationTitleEditState | null;
124
+ readonly getCommandMenu: () => CommandMenuState | null;
125
+ readonly setCommandMenu: (menu: CommandMenuState | null) => void;
126
+ readonly getNewThreadPrompt: () => NewThreadPromptState | null;
127
+ readonly setNewThreadPrompt: (prompt: NewThreadPromptState | null) => void;
128
+ readonly getAddDirectoryPrompt: () => { value: string; error: string | null } | null;
129
+ readonly setAddDirectoryPrompt: (
130
+ next: {
131
+ value: string;
132
+ error: string | null;
133
+ } | null,
134
+ ) => void;
135
+ readonly getRepositoryPrompt: () => RepositoryPromptState | null;
136
+ readonly setRepositoryPrompt: (next: RepositoryPromptState | null) => void;
137
+ }
138
+
139
+ export interface InputRouterOptions {
140
+ readonly shortcuts: InputRouterShortcutPorts;
141
+ readonly overlays: InputRouterOverlayPorts;
142
+ readonly actions: InputRouterActionPorts;
143
+ readonly state: InputRouterStatePorts;
144
+ }
145
+
146
+ export interface HandleCommandMenuInputOptions {
147
+ readonly input: Buffer;
148
+ readonly menu: CommandMenuState | null;
149
+ readonly isQuitShortcut: (input: Buffer) => boolean;
150
+ readonly isToggleShortcut: (input: Buffer) => boolean;
151
+ readonly dismissOnOutsideClick: (
152
+ input: Buffer,
153
+ dismiss: () => void,
154
+ onInsidePointerPress?: (col: number, row: number) => boolean,
155
+ ) => boolean;
156
+ readonly buildCommandMenuModalOverlay: () => { top: number } | null;
157
+ readonly resolveActions: () => readonly CommandMenuActionDescriptor[];
158
+ readonly executeAction: (actionId: string) => void;
159
+ readonly setMenu: (next: CommandMenuState | null) => void;
160
+ readonly markDirty: () => void;
161
+ }
162
+
163
+ export interface HandleTaskEditorPromptInputOptions {
164
+ readonly input: Buffer;
165
+ readonly prompt: TaskEditorPromptState | null;
166
+ readonly isQuitShortcut: (input: Buffer) => boolean;
167
+ readonly dismissOnOutsideClick: (input: Buffer, dismiss: () => void) => boolean;
168
+ }
169
+
170
+ export interface HandleTaskEditorPromptInputResult {
171
+ readonly handled: boolean;
172
+ readonly nextPrompt?: TaskEditorPromptState | null;
173
+ readonly markDirty: boolean;
174
+ readonly submitPayload?: TaskEditorSubmitPayload;
175
+ }
176
+
177
+ export interface HandleApiKeyPromptInputOptions {
178
+ readonly input: Buffer;
179
+ readonly prompt: ApiKeyPromptState | null;
180
+ readonly isQuitShortcut: (input: Buffer) => boolean;
181
+ readonly dismissOnOutsideClick: (input: Buffer, dismiss: () => void) => boolean;
182
+ readonly setPrompt: (next: ApiKeyPromptState | null) => void;
183
+ readonly markDirty: () => void;
184
+ readonly persistApiKey: (keyName: string, value: string) => void;
185
+ }
186
+
187
+ export interface HandleConversationTitleEditInputOptions {
188
+ readonly input: Buffer;
189
+ readonly edit: ConversationTitleEditState | null;
190
+ readonly isQuitShortcut: (input: Buffer) => boolean;
191
+ readonly isArchiveShortcut: (input: Buffer) => boolean;
192
+ readonly dismissOnOutsideClick: (
193
+ input: Buffer,
194
+ dismiss: () => void,
195
+ onInsidePointerPress?: (col: number, row: number) => boolean,
196
+ ) => boolean;
197
+ readonly buildConversationTitleModalOverlay: () => { top: number } | null;
198
+ readonly stopConversationTitleEdit: (persistPending: boolean) => void;
199
+ readonly queueControlPlaneOp: (task: () => Promise<void>, label: string) => void;
200
+ readonly archiveConversation: (sessionId: string) => Promise<void>;
201
+ readonly markDirty: () => void;
202
+ readonly conversations: ReadonlyMap<string, { title: string }>;
203
+ readonly scheduleConversationTitlePersist: () => void;
204
+ }
205
+
206
+ export interface HandleNewThreadPromptInputOptions {
207
+ readonly input: Buffer;
208
+ readonly prompt: NewThreadPromptState | null;
209
+ readonly isQuitShortcut: (input: Buffer) => boolean;
210
+ readonly dismissOnOutsideClick: (
211
+ input: Buffer,
212
+ dismiss: () => void,
213
+ onInsidePointerPress?: (col: number, row: number) => boolean,
214
+ ) => boolean;
215
+ readonly buildNewThreadModalOverlay: () => { top: number } | null;
216
+ readonly resolveNewThreadPromptAgentByRow: (
217
+ overlayTop: number,
218
+ row: number,
219
+ ) => ThreadAgentType | null;
220
+ readonly queueControlPlaneOp: (task: () => Promise<void>, label: string) => void;
221
+ readonly createAndActivateConversationInDirectory: (
222
+ directoryId: string,
223
+ agentType: ThreadAgentType,
224
+ ) => Promise<void>;
225
+ readonly markDirty: () => void;
226
+ readonly setPrompt: (prompt: NewThreadPromptState | null) => void;
227
+ }
228
+
229
+ export interface HandleAddDirectoryPromptInputOptions {
230
+ readonly input: Buffer;
231
+ readonly prompt: { value: string; error: string | null } | null;
232
+ readonly isQuitShortcut: (input: Buffer) => boolean;
233
+ readonly dismissOnOutsideClick: (input: Buffer, dismiss: () => void) => boolean;
234
+ readonly setPrompt: (
235
+ next: {
236
+ value: string;
237
+ error: string | null;
238
+ } | null,
239
+ ) => void;
240
+ readonly markDirty: () => void;
241
+ readonly queueControlPlaneOp: (task: () => Promise<void>, label: string) => void;
242
+ readonly addDirectoryByPath: (path: string) => Promise<void>;
243
+ }
244
+
245
+ export interface HandleRepositoryPromptInputOptions {
246
+ readonly input: Buffer;
247
+ readonly prompt: RepositoryPromptState | null;
248
+ readonly isQuitShortcut: (input: Buffer) => boolean;
249
+ readonly dismissOnOutsideClick: (input: Buffer, dismiss: () => void) => boolean;
250
+ readonly setPrompt: (next: RepositoryPromptState | null) => void;
251
+ readonly markDirty: () => void;
252
+ readonly repositoriesHas: (repositoryId: string) => boolean;
253
+ readonly normalizeGitHubRemoteUrl: (remoteUrl: string) => string | null;
254
+ readonly queueControlPlaneOp: (task: () => Promise<void>, label: string) => void;
255
+ readonly upsertRepositoryByRemoteUrl: (
256
+ remoteUrl: string,
257
+ existingRepositoryId?: string,
258
+ ) => Promise<void>;
259
+ }
260
+
261
+ export interface InputRouterStrategies {
262
+ handleCommandMenuInput(options: HandleCommandMenuInputOptions): boolean;
263
+ handleTaskEditorPromptInput(
264
+ options: HandleTaskEditorPromptInputOptions,
265
+ ): HandleTaskEditorPromptInputResult;
266
+ handleApiKeyPromptInput(options: HandleApiKeyPromptInputOptions): boolean;
267
+ handleConversationTitleEditInput(options: HandleConversationTitleEditInputOptions): boolean;
268
+ handleNewThreadPromptInput(options: HandleNewThreadPromptInputOptions): boolean;
269
+ handleAddDirectoryPromptInput(options: HandleAddDirectoryPromptInputOptions): boolean;
270
+ handleRepositoryPromptInput(options: HandleRepositoryPromptInputOptions): boolean;
271
+ }
272
+
273
+ export class InputRouter {
274
+ constructor(
275
+ private readonly options: InputRouterOptions,
276
+ private readonly strategies: InputRouterStrategies,
277
+ ) {}
278
+
279
+ handleCommandMenuInput(input: Buffer): boolean {
280
+ return this.strategies.handleCommandMenuInput({
281
+ input,
282
+ menu: this.options.state.getCommandMenu(),
283
+ isQuitShortcut: this.options.shortcuts.isModalDismissShortcut,
284
+ isToggleShortcut: this.options.shortcuts.isCommandMenuToggleShortcut,
285
+ dismissOnOutsideClick: this.options.overlays.dismissOnOutsideClick,
286
+ buildCommandMenuModalOverlay: this.options.overlays.buildCommandMenuModalOverlay,
287
+ resolveActions: this.options.actions.resolveCommandMenuActions,
288
+ executeAction: this.options.actions.executeCommandMenuAction,
289
+ setMenu: this.options.state.setCommandMenu,
290
+ markDirty: this.options.state.markDirty,
291
+ });
292
+ }
293
+
294
+ handleTaskEditorPromptInput(input: Buffer): boolean {
295
+ const handled = this.strategies.handleTaskEditorPromptInput({
296
+ input,
297
+ prompt: this.options.state.getTaskEditorPrompt(),
298
+ isQuitShortcut: this.options.shortcuts.isModalDismissShortcut,
299
+ dismissOnOutsideClick: this.options.overlays.dismissOnOutsideClick,
300
+ });
301
+ if (!handled.handled) {
302
+ return false;
303
+ }
304
+ if (handled.nextPrompt !== undefined) {
305
+ this.options.state.setTaskEditorPrompt(handled.nextPrompt);
306
+ }
307
+ if (handled.markDirty) {
308
+ this.options.state.markDirty();
309
+ }
310
+ if (handled.submitPayload !== undefined) {
311
+ this.options.actions.submitTaskEditorPayload(handled.submitPayload);
312
+ }
313
+ return true;
314
+ }
315
+
316
+ handleRepositoryPromptInput(input: Buffer): boolean {
317
+ return this.strategies.handleRepositoryPromptInput({
318
+ input,
319
+ prompt: this.options.state.getRepositoryPrompt(),
320
+ isQuitShortcut: this.options.shortcuts.isModalDismissShortcut,
321
+ dismissOnOutsideClick: this.options.overlays.dismissOnOutsideClick,
322
+ setPrompt: this.options.state.setRepositoryPrompt,
323
+ markDirty: this.options.state.markDirty,
324
+ repositoriesHas: this.options.actions.repositoriesHas,
325
+ normalizeGitHubRemoteUrl: this.options.actions.normalizeGitHubRemoteUrl,
326
+ queueControlPlaneOp: this.options.actions.queueControlPlaneOp,
327
+ upsertRepositoryByRemoteUrl: this.options.actions.upsertRepositoryByRemoteUrl,
328
+ });
329
+ }
330
+
331
+ handleApiKeyPromptInput(input: Buffer): boolean {
332
+ if (
333
+ this.options.state.getApiKeyPrompt === undefined ||
334
+ this.options.state.setApiKeyPrompt === undefined ||
335
+ this.options.actions.persistApiKey === undefined
336
+ ) {
337
+ return false;
338
+ }
339
+ return this.strategies.handleApiKeyPromptInput({
340
+ input,
341
+ prompt: this.options.state.getApiKeyPrompt(),
342
+ isQuitShortcut: this.options.shortcuts.isModalDismissShortcut,
343
+ dismissOnOutsideClick: this.options.overlays.dismissOnOutsideClick,
344
+ setPrompt: this.options.state.setApiKeyPrompt,
345
+ markDirty: this.options.state.markDirty,
346
+ persistApiKey: this.options.actions.persistApiKey,
347
+ });
348
+ }
349
+
350
+ handleNewThreadPromptInput(input: Buffer): boolean {
351
+ return this.strategies.handleNewThreadPromptInput({
352
+ input,
353
+ prompt: this.options.state.getNewThreadPrompt(),
354
+ isQuitShortcut: this.options.shortcuts.isModalDismissShortcut,
355
+ dismissOnOutsideClick: this.options.overlays.dismissOnOutsideClick,
356
+ buildNewThreadModalOverlay: this.options.overlays.buildNewThreadModalOverlay,
357
+ resolveNewThreadPromptAgentByRow: this.options.overlays.resolveNewThreadPromptAgentByRow,
358
+ queueControlPlaneOp: this.options.actions.queueControlPlaneOp,
359
+ createAndActivateConversationInDirectory:
360
+ this.options.actions.createAndActivateConversationInDirectory,
361
+ markDirty: this.options.state.markDirty,
362
+ setPrompt: this.options.state.setNewThreadPrompt,
363
+ });
364
+ }
365
+
366
+ handleConversationTitleEditInput(input: Buffer): boolean {
367
+ return this.strategies.handleConversationTitleEditInput({
368
+ input,
369
+ edit: this.options.state.getConversationTitleEdit(),
370
+ isQuitShortcut: this.options.shortcuts.isModalDismissShortcut,
371
+ isArchiveShortcut: this.options.shortcuts.isArchiveConversationShortcut,
372
+ dismissOnOutsideClick: this.options.overlays.dismissOnOutsideClick,
373
+ buildConversationTitleModalOverlay: this.options.overlays.buildConversationTitleModalOverlay,
374
+ stopConversationTitleEdit: this.options.actions.stopConversationTitleEdit,
375
+ queueControlPlaneOp: this.options.actions.queueControlPlaneOp,
376
+ archiveConversation: this.options.actions.archiveConversation,
377
+ markDirty: this.options.state.markDirty,
378
+ conversations: this.options.state.conversations,
379
+ scheduleConversationTitlePersist: this.options.state.scheduleConversationTitlePersist,
380
+ });
381
+ }
382
+
383
+ handleAddDirectoryPromptInput(input: Buffer): boolean {
384
+ return this.strategies.handleAddDirectoryPromptInput({
385
+ input,
386
+ prompt: this.options.state.getAddDirectoryPrompt(),
387
+ isQuitShortcut: this.options.shortcuts.isModalDismissShortcut,
388
+ dismissOnOutsideClick: this.options.overlays.dismissOnOutsideClick,
389
+ setPrompt: this.options.state.setAddDirectoryPrompt,
390
+ markDirty: this.options.state.markDirty,
391
+ queueControlPlaneOp: this.options.actions.queueControlPlaneOp,
392
+ addDirectoryByPath: this.options.actions.addDirectoryByPath,
393
+ });
394
+ }
395
+
396
+ routeModalInput(input: Buffer): boolean {
397
+ if (this.handleCommandMenuInput(input)) {
398
+ return true;
399
+ }
400
+ if (this.handleTaskEditorPromptInput(input)) {
401
+ return true;
402
+ }
403
+ if (this.handleRepositoryPromptInput(input)) {
404
+ return true;
405
+ }
406
+ if (this.handleApiKeyPromptInput(input)) {
407
+ return true;
408
+ }
409
+ if (this.handleNewThreadPromptInput(input)) {
410
+ return true;
411
+ }
412
+ if (this.handleConversationTitleEditInput(input)) {
413
+ return true;
414
+ }
415
+ if (this.handleAddDirectoryPromptInput(input)) {
416
+ return true;
417
+ }
418
+ return false;
419
+ }
420
+ }