@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
@@ -0,0 +1,18 @@
1
+ export * from './text-layout.ts';
2
+ export * from './surface.ts';
3
+ export * from './kit.ts';
4
+ export * from './frame-primitives.ts';
5
+ export * from './screen.ts';
6
+ export * from './modal-manager.ts';
7
+ export * from './layout.ts';
8
+ export * from './interaction/conversation-input-forwarder.ts';
9
+ export * from './interaction/conversation-selection-input.ts';
10
+ export * from './interaction/global-shortcut-input.ts';
11
+ export * from './interaction/input-preflight.ts';
12
+ export * from './interaction/input-token-router.ts';
13
+ export * from './interaction/input.ts';
14
+ export * from './interaction/left-nav-input.ts';
15
+ export * from './interaction/rail-pointer-input.ts';
16
+ export * from './interaction/main-pane-pointer-input.ts';
17
+ export * from './interaction/pointer-routing-input.ts';
18
+ export * from './interaction/repository-fold-input.ts';
@@ -0,0 +1,221 @@
1
+ type MainPaneMode = 'conversation' | 'project' | 'home' | 'nim';
2
+
3
+ interface ConversationSnapshotViewport {
4
+ readonly top: number;
5
+ readonly totalRows: number;
6
+ readonly followOutput: boolean;
7
+ }
8
+
9
+ export interface ConversationSnapshotFrame {
10
+ readonly activeScreen: 'primary' | 'alternate';
11
+ readonly viewport: ConversationSnapshotViewport;
12
+ readonly lines: readonly string[];
13
+ }
14
+
15
+ interface ConversationInputOracle<TSnapshotForInput extends ConversationSnapshotFrame> {
16
+ snapshotWithoutHash: () => TSnapshotForInput;
17
+ isMouseTrackingEnabled: () => boolean;
18
+ scrollViewport: (deltaRows: number) => void;
19
+ }
20
+
21
+ export interface ConversationInputState<
22
+ TSnapshotForInput extends ConversationSnapshotFrame = ConversationSnapshotFrame,
23
+ > {
24
+ readonly sessionId: string;
25
+ readonly directoryId: string | null;
26
+ readonly controller: unknown | null;
27
+ readonly oracle: ConversationInputOracle<TSnapshotForInput>;
28
+ }
29
+
30
+ interface ConversationInputLayout {
31
+ readonly cols: number;
32
+ readonly rows: number;
33
+ readonly paneRows: number;
34
+ readonly statusRow: number;
35
+ readonly leftCols: number;
36
+ readonly rightCols: number;
37
+ readonly separatorCol: number;
38
+ readonly rightStartCol: number;
39
+ }
40
+
41
+ type ConversationInputToken =
42
+ | {
43
+ readonly kind: 'passthrough';
44
+ readonly text: string;
45
+ }
46
+ | {
47
+ readonly kind: 'mouse';
48
+ readonly event: {
49
+ readonly sequence: string;
50
+ readonly code: number;
51
+ readonly col: number;
52
+ readonly row: number;
53
+ readonly final: 'M' | 'm';
54
+ };
55
+ };
56
+
57
+ interface ParsedConversationInputChunk {
58
+ readonly tokens: readonly ConversationInputToken[];
59
+ readonly remainder: string;
60
+ }
61
+
62
+ interface InputTokenRouterLike<
63
+ TConversation extends ConversationInputState<TSnapshotForInput>,
64
+ TSnapshotForInput extends ConversationSnapshotFrame,
65
+ > {
66
+ routeTokens(input: {
67
+ readonly tokens: readonly ConversationInputToken[];
68
+ readonly layout: ConversationInputLayout;
69
+ readonly conversation: TConversation | null;
70
+ readonly snapshotForInput: TSnapshotForInput | null;
71
+ }): {
72
+ readonly routedTokens: readonly ConversationInputToken[];
73
+ readonly snapshotForInput: TSnapshotForInput | null;
74
+ };
75
+ }
76
+
77
+ interface RouteConversationTokensOptions<TSnapshotForInput extends ConversationSnapshotFrame> {
78
+ readonly tokens: readonly ConversationInputToken[];
79
+ readonly mainPaneMode: MainPaneMode;
80
+ readonly normalizeMuxKeyboardInputForPty: (input: Buffer) => Buffer;
81
+ readonly classifyPaneAt: (col: number, row: number) => string;
82
+ readonly wheelDeltaRowsFromCode: (code: number) => number | null;
83
+ readonly hasShiftModifier: (code: number) => boolean;
84
+ readonly layout: {
85
+ readonly paneRows: number;
86
+ readonly rightCols: number;
87
+ readonly rightStartCol: number;
88
+ };
89
+ readonly snapshotForInput: TSnapshotForInput | null;
90
+ readonly appMouseTrackingEnabled: boolean;
91
+ }
92
+
93
+ interface RouteConversationTokensResult {
94
+ readonly mainPaneScrollRows: number;
95
+ readonly forwardToSession: readonly Buffer[];
96
+ }
97
+
98
+ function hasShiftModifier(code: number): boolean {
99
+ return (code & 0b0000_0100) !== 0;
100
+ }
101
+
102
+ function wheelDeltaRowsFromCode(code: number): number | null {
103
+ if ((code & 0b0100_0000) === 0) {
104
+ return null;
105
+ }
106
+ return (code & 0b0000_0001) === 0 ? -1 : 1;
107
+ }
108
+
109
+ export interface ConversationInputForwarderOptions<
110
+ TSnapshotForInput extends ConversationSnapshotFrame = ConversationSnapshotFrame,
111
+ TConversation extends ConversationInputState<TSnapshotForInput> =
112
+ ConversationInputState<TSnapshotForInput>,
113
+ > {
114
+ readonly getInputRemainder: () => string;
115
+ readonly setInputRemainder: (next: string) => void;
116
+ readonly getMainPaneMode: () => MainPaneMode;
117
+ readonly getLayout: () => ConversationInputLayout;
118
+ readonly inputTokenRouter: InputTokenRouterLike<TConversation, TSnapshotForInput>;
119
+ readonly getActiveConversation: () => TConversation | null;
120
+ readonly markDirty: () => void;
121
+ isControlledByLocalHuman(input: {
122
+ readonly conversation: TConversation;
123
+ readonly controllerId: string;
124
+ }): boolean;
125
+ readonly controllerId: string;
126
+ readonly sendInputToSession: (sessionId: string, chunk: Buffer) => void;
127
+ readonly noteGitActivity: (directoryId: string | null) => void;
128
+ parseMuxInputChunk(previousRemainder: string, chunk: Buffer): ParsedConversationInputChunk;
129
+ routeInputTokensForConversation(
130
+ options: RouteConversationTokensOptions<TSnapshotForInput>,
131
+ ): RouteConversationTokensResult;
132
+ classifyPaneAt(layout: ConversationInputLayout, col: number, row: number): string;
133
+ normalizeMuxKeyboardInputForPty(input: Buffer): Buffer;
134
+ handlePassthroughTextInMainPaneMode?: (input: {
135
+ readonly mainPaneMode: Exclude<MainPaneMode, 'conversation'>;
136
+ readonly text: string;
137
+ }) => void;
138
+ }
139
+
140
+ export class ConversationInputForwarder<
141
+ TSnapshotForInput extends ConversationSnapshotFrame = ConversationSnapshotFrame,
142
+ TConversation extends ConversationInputState<TSnapshotForInput> =
143
+ ConversationInputState<TSnapshotForInput>,
144
+ > {
145
+ constructor(
146
+ private readonly options: ConversationInputForwarderOptions<TSnapshotForInput, TConversation>,
147
+ ) {}
148
+
149
+ handleInput(input: Buffer): void {
150
+ const mainPaneMode = this.options.getMainPaneMode();
151
+ const parsed = this.options.parseMuxInputChunk(this.options.getInputRemainder(), input);
152
+ this.options.setInputRemainder(parsed.remainder);
153
+
154
+ const layout = this.options.getLayout();
155
+ const inputConversation = this.options.getActiveConversation();
156
+ const { routedTokens, snapshotForInput } = this.options.inputTokenRouter.routeTokens({
157
+ tokens: parsed.tokens,
158
+ layout,
159
+ conversation: inputConversation,
160
+ snapshotForInput:
161
+ inputConversation === null ? null : inputConversation.oracle.snapshotWithoutHash(),
162
+ });
163
+
164
+ const { mainPaneScrollRows, forwardToSession } = this.options.routeInputTokensForConversation({
165
+ tokens: routedTokens,
166
+ mainPaneMode,
167
+ normalizeMuxKeyboardInputForPty: this.options.normalizeMuxKeyboardInputForPty,
168
+ classifyPaneAt: (col, row) => this.options.classifyPaneAt(layout, col, row),
169
+ wheelDeltaRowsFromCode,
170
+ hasShiftModifier,
171
+ layout: {
172
+ paneRows: layout.paneRows,
173
+ rightCols: layout.rightCols,
174
+ rightStartCol: layout.rightStartCol,
175
+ },
176
+ snapshotForInput,
177
+ appMouseTrackingEnabled:
178
+ inputConversation === null ? false : inputConversation.oracle.isMouseTrackingEnabled(),
179
+ });
180
+
181
+ if (
182
+ mainPaneMode !== 'conversation' &&
183
+ this.options.handlePassthroughTextInMainPaneMode !== undefined
184
+ ) {
185
+ for (const token of routedTokens) {
186
+ if (token.kind !== 'passthrough' || token.text.length === 0) {
187
+ continue;
188
+ }
189
+ this.options.handlePassthroughTextInMainPaneMode({
190
+ mainPaneMode,
191
+ text: token.text,
192
+ });
193
+ }
194
+ }
195
+
196
+ if (mainPaneScrollRows !== 0 && inputConversation !== null) {
197
+ inputConversation.oracle.scrollViewport(mainPaneScrollRows);
198
+ this.options.markDirty();
199
+ }
200
+
201
+ if (inputConversation === null) {
202
+ return;
203
+ }
204
+ if (
205
+ inputConversation.controller !== null &&
206
+ !this.options.isControlledByLocalHuman({
207
+ conversation: inputConversation,
208
+ controllerId: this.options.controllerId,
209
+ })
210
+ ) {
211
+ return;
212
+ }
213
+
214
+ for (const forwardChunk of forwardToSession) {
215
+ this.options.sendInputToSession(inputConversation.sessionId, forwardChunk);
216
+ }
217
+ if (forwardToSession.length > 0) {
218
+ this.options.noteGitActivity(inputConversation.directoryId);
219
+ }
220
+ }
221
+ }
@@ -0,0 +1,213 @@
1
+ export interface SelectionPoint {
2
+ readonly rowAbs: number;
3
+ readonly col: number;
4
+ }
5
+
6
+ export interface PaneSelection {
7
+ readonly anchor: SelectionPoint;
8
+ readonly focus: SelectionPoint;
9
+ readonly text: string;
10
+ }
11
+
12
+ export interface PaneSelectionDrag {
13
+ readonly anchor: SelectionPoint;
14
+ readonly focus: SelectionPoint;
15
+ readonly hasDragged: boolean;
16
+ }
17
+
18
+ export interface SelectionLayout {
19
+ readonly paneRows: number;
20
+ readonly rightCols: number;
21
+ readonly rightStartCol: number;
22
+ }
23
+
24
+ export interface ConversationSelectionSnapshotFrame {
25
+ readonly colorKind?: 'default' | 'indexed' | 'rgb';
26
+ readonly rows: number;
27
+ readonly cols: number;
28
+ readonly activeScreen: 'primary' | 'alternate';
29
+ readonly modes: {
30
+ readonly bracketedPaste: boolean;
31
+ readonly decMouseX10: boolean;
32
+ readonly decMouseButtonEvent: boolean;
33
+ readonly decMouseAnyEvent: boolean;
34
+ readonly decFocusTracking: boolean;
35
+ readonly decMouseSgrEncoding: boolean;
36
+ };
37
+ readonly cursor: {
38
+ readonly row: number;
39
+ readonly col: number;
40
+ readonly visible: boolean;
41
+ readonly style: {
42
+ readonly shape: 'block' | 'bar' | 'underline';
43
+ readonly blinking: boolean;
44
+ };
45
+ };
46
+ readonly viewport: {
47
+ readonly top: number;
48
+ readonly totalRows: number;
49
+ readonly followOutput: boolean;
50
+ };
51
+ readonly lines: string[];
52
+ readonly richLines: Array<{
53
+ readonly wrapped: boolean;
54
+ readonly text: string;
55
+ readonly cells: Array<{
56
+ readonly glyph: string;
57
+ readonly width: number;
58
+ readonly continued: boolean;
59
+ readonly style: {
60
+ readonly bold: boolean;
61
+ readonly dim: boolean;
62
+ readonly italic: boolean;
63
+ readonly underline: boolean;
64
+ readonly inverse: boolean;
65
+ readonly fg:
66
+ | { readonly kind: 'default' }
67
+ | { readonly kind: 'indexed'; readonly index: number }
68
+ | { readonly kind: 'rgb'; readonly r: number; readonly g: number; readonly b: number };
69
+ readonly bg:
70
+ | { readonly kind: 'default' }
71
+ | { readonly kind: 'indexed'; readonly index: number }
72
+ | { readonly kind: 'rgb'; readonly r: number; readonly g: number; readonly b: number };
73
+ };
74
+ }>;
75
+ }>;
76
+ }
77
+
78
+ function isWheelMouseCode(code: number): boolean {
79
+ return (code & 0b0100_0000) !== 0;
80
+ }
81
+
82
+ function hasAltModifier(code: number): boolean {
83
+ return (code & 0b0000_1000) !== 0;
84
+ }
85
+
86
+ function isMotionMouseCode(code: number): boolean {
87
+ return (code & 0b0010_0000) !== 0;
88
+ }
89
+
90
+ function isLeftButtonPress(code: number, final: 'M' | 'm'): boolean {
91
+ if (final !== 'M') {
92
+ return false;
93
+ }
94
+ if (isWheelMouseCode(code) || isMotionMouseCode(code)) {
95
+ return false;
96
+ }
97
+ return (code & 0b0000_0011) === 0;
98
+ }
99
+
100
+ function isMouseRelease(final: 'M' | 'm'): boolean {
101
+ return final === 'm';
102
+ }
103
+
104
+ function isSelectionDrag(code: number, final: 'M' | 'm'): boolean {
105
+ return final === 'M' && isMotionMouseCode(code);
106
+ }
107
+
108
+ export interface ConversationSelectionInputOptions {
109
+ readonly getSelection: () => PaneSelection | null;
110
+ readonly setSelection: (next: PaneSelection | null) => void;
111
+ readonly getSelectionDrag: () => PaneSelectionDrag | null;
112
+ readonly setSelectionDrag: (next: PaneSelectionDrag | null) => void;
113
+ readonly pinViewportForSelection: () => void;
114
+ readonly releaseViewportPinForSelection: () => void;
115
+ readonly markDirty: () => void;
116
+ }
117
+
118
+ export interface ReduceConversationMouseSelectionInput {
119
+ readonly selection: PaneSelection | null;
120
+ readonly selectionDrag: PaneSelectionDrag | null;
121
+ readonly point: SelectionPoint;
122
+ readonly isMainPaneTarget: boolean;
123
+ readonly isLeftButtonPress: boolean;
124
+ readonly isSelectionDrag: boolean;
125
+ readonly isMouseRelease: boolean;
126
+ readonly isWheelMouseCode: boolean;
127
+ readonly selectionTextForPane: (selection: PaneSelection) => string;
128
+ }
129
+
130
+ export interface ReduceConversationMouseSelectionResult {
131
+ readonly selection: PaneSelection | null;
132
+ readonly selectionDrag: PaneSelectionDrag | null;
133
+ readonly pinViewport: boolean;
134
+ readonly releaseViewportPin: boolean;
135
+ readonly markDirty: boolean;
136
+ readonly consumed: boolean;
137
+ }
138
+
139
+ export interface ConversationSelectionStrategies {
140
+ pointFromMouseEvent(
141
+ layout: SelectionLayout,
142
+ frame: ConversationSelectionSnapshotFrame,
143
+ event: { col: number; row: number },
144
+ ): SelectionPoint;
145
+ reduceConversationMouseSelection(
146
+ options: ReduceConversationMouseSelectionInput,
147
+ ): ReduceConversationMouseSelectionResult;
148
+ selectionText(frame: ConversationSelectionSnapshotFrame, selection: PaneSelection | null): string;
149
+ }
150
+
151
+ interface MouseSelectionInput {
152
+ readonly layout: SelectionLayout;
153
+ readonly frame: ConversationSelectionSnapshotFrame;
154
+ readonly isMainPaneTarget: boolean;
155
+ readonly resolveSelectionText?: (selection: PaneSelection) => string;
156
+ readonly event: {
157
+ readonly col: number;
158
+ readonly row: number;
159
+ readonly code: number;
160
+ readonly final: 'M' | 'm';
161
+ };
162
+ }
163
+
164
+ export class ConversationSelectionInput {
165
+ constructor(
166
+ private readonly options: ConversationSelectionInputOptions,
167
+ private readonly strategies: ConversationSelectionStrategies,
168
+ ) {}
169
+
170
+ clearSelectionOnTextToken(textLength: number): boolean {
171
+ const hasSelection =
172
+ this.options.getSelection() !== null || this.options.getSelectionDrag() !== null;
173
+ if (textLength <= 0 || !hasSelection) {
174
+ return false;
175
+ }
176
+ this.options.setSelection(null);
177
+ this.options.setSelectionDrag(null);
178
+ this.options.releaseViewportPinForSelection();
179
+ this.options.markDirty();
180
+ return true;
181
+ }
182
+
183
+ handleMouseSelection(input: MouseSelectionInput): boolean {
184
+ const reduced = this.strategies.reduceConversationMouseSelection({
185
+ selection: this.options.getSelection(),
186
+ selectionDrag: this.options.getSelectionDrag(),
187
+ point: this.strategies.pointFromMouseEvent(input.layout, input.frame, input.event),
188
+ isMainPaneTarget: input.isMainPaneTarget,
189
+ isLeftButtonPress:
190
+ isLeftButtonPress(input.event.code, input.event.final) && !hasAltModifier(input.event.code),
191
+ isSelectionDrag:
192
+ isSelectionDrag(input.event.code, input.event.final) && !hasAltModifier(input.event.code),
193
+ isMouseRelease: isMouseRelease(input.event.final),
194
+ isWheelMouseCode: isWheelMouseCode(input.event.code),
195
+ selectionTextForPane: (nextSelection) =>
196
+ input.resolveSelectionText === undefined
197
+ ? this.strategies.selectionText(input.frame, nextSelection)
198
+ : input.resolveSelectionText(nextSelection),
199
+ });
200
+ this.options.setSelection(reduced.selection);
201
+ this.options.setSelectionDrag(reduced.selectionDrag);
202
+ if (reduced.pinViewport) {
203
+ this.options.pinViewportForSelection();
204
+ }
205
+ if (reduced.releaseViewportPin) {
206
+ this.options.releaseViewportPinForSelection();
207
+ }
208
+ if (reduced.markDirty) {
209
+ this.options.markDirty();
210
+ }
211
+ return reduced.consumed;
212
+ }
213
+ }
@@ -0,0 +1,172 @@
1
+ type ShortcutCycleDirection = 'next' | 'previous';
2
+ type MainPaneMode = 'conversation' | 'project' | 'home' | 'nim';
3
+
4
+ function parseNumericPrefix(value: string): number | null {
5
+ const parsed = Number.parseInt(value, 10);
6
+ return Number.isNaN(parsed) ? null : parsed;
7
+ }
8
+
9
+ function isCtrlOnlyShortcutInput(input: Buffer): boolean {
10
+ if (input.length === 1) {
11
+ const byte = input[0]!;
12
+ return (
13
+ (byte >= 0x01 && byte <= 0x1a) ||
14
+ byte === 0x1c ||
15
+ byte === 0x1d ||
16
+ byte === 0x1e ||
17
+ byte === 0x1f
18
+ );
19
+ }
20
+
21
+ const text = input.toString('utf8');
22
+ if (text.startsWith('\u001b[') && text.endsWith('u')) {
23
+ const kittyMatch = text.slice(2, -1).match(/^(\d+)(?::\d+)?(?:;(\d+)(?::\d+)?)?$/u);
24
+ if (kittyMatch !== null) {
25
+ const modifierCode = parseNumericPrefix(kittyMatch[2] ?? '1');
26
+ return modifierCode === 5;
27
+ }
28
+ }
29
+
30
+ if (text.startsWith('\u001b[27;') && text.endsWith('~')) {
31
+ const modifyOtherKeysMatch = text.slice(2, -1).match(/^27;(\d+);(\d+)$/u);
32
+ if (modifyOtherKeysMatch !== null) {
33
+ const modifierCode = parseNumericPrefix(modifyOtherKeysMatch[1]!);
34
+ return modifierCode === 5;
35
+ }
36
+ }
37
+
38
+ return false;
39
+ }
40
+
41
+ function isTerminalAgentType(agentType: string | null): boolean {
42
+ return agentType !== null && agentType.trim().toLowerCase() === 'terminal';
43
+ }
44
+
45
+ function shouldBypassCtrlOnlyShortcutInTerminalConversation(shortcut: string | null): boolean {
46
+ if (
47
+ shortcut === 'mux.conversation.archive' ||
48
+ shortcut === 'mux.conversation.delete' ||
49
+ shortcut === 'mux.conversation.next' ||
50
+ shortcut === 'mux.conversation.previous'
51
+ ) {
52
+ return false;
53
+ }
54
+ return true;
55
+ }
56
+
57
+ export interface GlobalShortcutState {
58
+ readonly mainPaneMode: () => MainPaneMode;
59
+ readonly activeConversationId: () => string | null;
60
+ readonly activeConversationAgentType?: () => string | null;
61
+ readonly resolveConversationForAction?: () => string | null;
62
+ readonly conversationsHas: (sessionId: string) => boolean;
63
+ readonly activeDirectoryId: () => string | null;
64
+ readonly directoryExists: (directoryId: string) => boolean;
65
+ }
66
+
67
+ export interface GlobalShortcutActions {
68
+ readonly requestStop: () => void;
69
+ readonly resolveDirectoryForAction: () => string | null;
70
+ readonly openNewThreadPrompt: (directoryId: string) => void;
71
+ readonly toggleCommandMenu: () => void;
72
+ readonly openOrCreateCritiqueConversationInDirectory: (directoryId: string) => Promise<void>;
73
+ readonly toggleGatewayProfile: () => Promise<void>;
74
+ readonly toggleGatewayStatusTimeline: () => Promise<void>;
75
+ readonly toggleGatewayRenderTrace: (conversationId: string | null) => Promise<void>;
76
+ readonly queueControlPlaneOp: (task: () => Promise<void>, label: string) => void;
77
+ readonly archiveConversation: (sessionId: string) => Promise<void>;
78
+ readonly refreshAllConversationTitles: () => Promise<void>;
79
+ readonly interruptConversation: (sessionId: string) => Promise<void>;
80
+ readonly takeoverConversation: (sessionId: string) => Promise<void>;
81
+ readonly openAddDirectoryPrompt: () => void;
82
+ readonly closeDirectory: (directoryId: string) => Promise<void>;
83
+ readonly cycleLeftNavSelection: (direction: ShortcutCycleDirection) => void;
84
+ }
85
+
86
+ export interface GlobalShortcutHandlerInput {
87
+ readonly shortcut: string | null;
88
+ readonly requestStop: () => void;
89
+ readonly resolveDirectoryForAction: () => string | null;
90
+ readonly openNewThreadPrompt: (directoryId: string) => void;
91
+ readonly toggleCommandMenu: () => void;
92
+ readonly openOrCreateCritiqueConversationInDirectory: (directoryId: string) => Promise<void>;
93
+ readonly toggleGatewayProfile: () => Promise<void>;
94
+ readonly toggleGatewayStatusTimeline: () => Promise<void>;
95
+ readonly toggleGatewayRenderTrace: (conversationId: string | null) => Promise<void>;
96
+ readonly resolveConversationForAction: () => string | null;
97
+ readonly conversationsHas: (sessionId: string) => boolean;
98
+ readonly queueControlPlaneOp: (task: () => Promise<void>, label: string) => void;
99
+ readonly archiveConversation: (sessionId: string) => Promise<void>;
100
+ readonly refreshAllConversationTitles: () => Promise<void>;
101
+ readonly interruptConversation: (sessionId: string) => Promise<void>;
102
+ readonly takeoverConversation: (sessionId: string) => Promise<void>;
103
+ readonly openAddDirectoryPrompt: () => void;
104
+ readonly resolveClosableDirectoryId: () => string | null;
105
+ readonly closeDirectory: (directoryId: string) => Promise<void>;
106
+ readonly cycleLeftNavSelection: (direction: ShortcutCycleDirection) => void;
107
+ }
108
+
109
+ export interface GlobalShortcutStrategies<TShortcutBindings> {
110
+ detectShortcut(input: Buffer, bindings: TShortcutBindings): string | null;
111
+ handleShortcut(input: GlobalShortcutHandlerInput): boolean;
112
+ }
113
+
114
+ export class GlobalShortcutInput<TShortcutBindings> {
115
+ constructor(
116
+ private readonly shortcutBindings: TShortcutBindings,
117
+ private readonly state: GlobalShortcutState,
118
+ private readonly actions: GlobalShortcutActions,
119
+ private readonly strategies: GlobalShortcutStrategies<TShortcutBindings>,
120
+ ) {}
121
+
122
+ handleInput(input: Buffer): boolean {
123
+ const shortcut = this.strategies.detectShortcut(input, this.shortcutBindings);
124
+ if (
125
+ shortcut !== null &&
126
+ shouldBypassCtrlOnlyShortcutInTerminalConversation(shortcut) &&
127
+ this.state.mainPaneMode() === 'conversation' &&
128
+ isTerminalAgentType(
129
+ this.state.activeConversationAgentType === undefined
130
+ ? null
131
+ : this.state.activeConversationAgentType(),
132
+ ) &&
133
+ isCtrlOnlyShortcutInput(input)
134
+ ) {
135
+ return false;
136
+ }
137
+ return this.strategies.handleShortcut({
138
+ shortcut,
139
+ requestStop: this.actions.requestStop,
140
+ resolveDirectoryForAction: this.actions.resolveDirectoryForAction,
141
+ openNewThreadPrompt: this.actions.openNewThreadPrompt,
142
+ toggleCommandMenu: this.actions.toggleCommandMenu,
143
+ openOrCreateCritiqueConversationInDirectory:
144
+ this.actions.openOrCreateCritiqueConversationInDirectory,
145
+ toggleGatewayProfile: this.actions.toggleGatewayProfile,
146
+ toggleGatewayStatusTimeline: this.actions.toggleGatewayStatusTimeline,
147
+ toggleGatewayRenderTrace: this.actions.toggleGatewayRenderTrace,
148
+ resolveConversationForAction: () =>
149
+ this.state.resolveConversationForAction !== undefined
150
+ ? this.state.resolveConversationForAction()
151
+ : this.state.mainPaneMode() === 'conversation'
152
+ ? this.state.activeConversationId()
153
+ : null,
154
+ conversationsHas: this.state.conversationsHas,
155
+ queueControlPlaneOp: this.actions.queueControlPlaneOp,
156
+ archiveConversation: this.actions.archiveConversation,
157
+ refreshAllConversationTitles: this.actions.refreshAllConversationTitles,
158
+ interruptConversation: this.actions.interruptConversation,
159
+ takeoverConversation: this.actions.takeoverConversation,
160
+ openAddDirectoryPrompt: this.actions.openAddDirectoryPrompt,
161
+ resolveClosableDirectoryId: () => {
162
+ const activeDirectoryId = this.state.activeDirectoryId();
163
+ if (this.state.mainPaneMode() !== 'project' || activeDirectoryId === null) {
164
+ return null;
165
+ }
166
+ return this.state.directoryExists(activeDirectoryId) ? activeDirectoryId : null;
167
+ },
168
+ closeDirectory: this.actions.closeDirectory,
169
+ cycleLeftNavSelection: this.actions.cycleLeftNavSelection,
170
+ });
171
+ }
172
+ }
@@ -1,6 +1,4 @@
1
- import { extractFocusEvents as extractFocusEventsFrame } from '../mux/live-mux/startup-utils.ts';
2
-
3
- interface InputPreflightOptions {
1
+ export interface InputPreflightOptions {
4
2
  readonly isShuttingDown: () => boolean;
5
3
  readonly routeModalInput: (input: Buffer) => boolean;
6
4
  readonly handleEscapeInput: (input: Buffer) => void;
@@ -12,19 +10,19 @@ interface InputPreflightOptions {
12
10
  readonly handleCopyShortcutInput: (input: Buffer) => boolean;
13
11
  }
14
12
 
15
- interface InputPreflightDependencies {
16
- readonly extractFocusEvents?: typeof extractFocusEventsFrame;
13
+ export interface InputPreflightStrategies {
14
+ extractFocusEvents(input: Buffer): {
15
+ readonly sanitized: Buffer;
16
+ readonly focusInCount: number;
17
+ readonly focusOutCount: number;
18
+ };
17
19
  }
18
20
 
19
21
  export class InputPreflight {
20
- private readonly extractFocusEvents: typeof extractFocusEventsFrame;
21
-
22
22
  constructor(
23
23
  private readonly options: InputPreflightOptions,
24
- dependencies: InputPreflightDependencies = {},
25
- ) {
26
- this.extractFocusEvents = dependencies.extractFocusEvents ?? extractFocusEventsFrame;
27
- }
24
+ private readonly strategies: InputPreflightStrategies,
25
+ ) {}
28
26
 
29
27
  nextInput(input: Buffer): Buffer | null {
30
28
  if (this.options.isShuttingDown()) {
@@ -39,7 +37,7 @@ export class InputPreflight {
39
37
  return null;
40
38
  }
41
39
 
42
- const focusExtraction = this.extractFocusEvents(input);
40
+ const focusExtraction = this.strategies.extractFocusEvents(input);
43
41
  if (focusExtraction.focusInCount > 0) {
44
42
  this.options.onFocusIn();
45
43
  }