@jmoyers/harness 0.1.0

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 (214) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +145 -0
  3. package/native/ptyd/Cargo.lock +16 -0
  4. package/native/ptyd/Cargo.toml +7 -0
  5. package/native/ptyd/src/main.rs +257 -0
  6. package/package.json +90 -0
  7. package/scripts/build-ptyd.sh +73 -0
  8. package/scripts/control-plane-daemon.ts +277 -0
  9. package/scripts/cursor-hook-relay.ts +82 -0
  10. package/scripts/harness-animate.ts +469 -0
  11. package/scripts/harness-bin.js +77 -0
  12. package/scripts/harness-core.ts +1 -0
  13. package/scripts/harness-inspector.ts +439 -0
  14. package/scripts/harness.ts +2493 -0
  15. package/src/adapters/agent-session-state.ts +390 -0
  16. package/src/cli/gateway-record.ts +173 -0
  17. package/src/codex/live-session.ts +872 -0
  18. package/src/config/config-core.ts +1359 -0
  19. package/src/config/secrets-core.ts +170 -0
  20. package/src/control-plane/agent-realtime-api.ts +2441 -0
  21. package/src/control-plane/codex-session-stream.ts +392 -0
  22. package/src/control-plane/codex-telemetry.ts +1325 -0
  23. package/src/control-plane/lifecycle-hooks.ts +706 -0
  24. package/src/control-plane/session-summary.ts +380 -0
  25. package/src/control-plane/status/agent-status-reducer.ts +21 -0
  26. package/src/control-plane/status/reducer-base.ts +170 -0
  27. package/src/control-plane/status/reducers/claude-status-reducer.ts +37 -0
  28. package/src/control-plane/status/reducers/codex-status-reducer.ts +48 -0
  29. package/src/control-plane/status/reducers/critique-status-reducer.ts +15 -0
  30. package/src/control-plane/status/reducers/cursor-status-reducer.ts +37 -0
  31. package/src/control-plane/status/reducers/terminal-status-reducer.ts +15 -0
  32. package/src/control-plane/status/session-status-engine.ts +76 -0
  33. package/src/control-plane/stream-client.ts +396 -0
  34. package/src/control-plane/stream-command-parser.ts +1673 -0
  35. package/src/control-plane/stream-protocol.ts +1808 -0
  36. package/src/control-plane/stream-server-background.ts +486 -0
  37. package/src/control-plane/stream-server-command.ts +2557 -0
  38. package/src/control-plane/stream-server-connection.ts +234 -0
  39. package/src/control-plane/stream-server-observed-filter.ts +112 -0
  40. package/src/control-plane/stream-server-session-runtime.ts +566 -0
  41. package/src/control-plane/stream-server-state-store.ts +15 -0
  42. package/src/control-plane/stream-server.ts +3192 -0
  43. package/src/cursor/managed-hooks.ts +282 -0
  44. package/src/domain/conversations.ts +414 -0
  45. package/src/domain/directories.ts +78 -0
  46. package/src/domain/repositories.ts +123 -0
  47. package/src/domain/tasks.ts +148 -0
  48. package/src/domain/workspace.ts +156 -0
  49. package/src/events/normalized-events.ts +124 -0
  50. package/src/mux/ansi-integrity.ts +103 -0
  51. package/src/mux/control-plane-op-queue.ts +212 -0
  52. package/src/mux/conversation-rail.ts +339 -0
  53. package/src/mux/double-click.ts +78 -0
  54. package/src/mux/dual-pane-core.ts +435 -0
  55. package/src/mux/harness-core-ui.ts +817 -0
  56. package/src/mux/input-shortcuts.ts +667 -0
  57. package/src/mux/live-mux/actions-conversation.ts +344 -0
  58. package/src/mux/live-mux/actions-repository.ts +246 -0
  59. package/src/mux/live-mux/actions-task.ts +115 -0
  60. package/src/mux/live-mux/args.ts +142 -0
  61. package/src/mux/live-mux/command-menu.ts +298 -0
  62. package/src/mux/live-mux/control-plane-records.ts +546 -0
  63. package/src/mux/live-mux/conversation-state.ts +188 -0
  64. package/src/mux/live-mux/directory-resolution.ts +34 -0
  65. package/src/mux/live-mux/event-mapping.ts +96 -0
  66. package/src/mux/live-mux/gateway-profiler.ts +152 -0
  67. package/src/mux/live-mux/gateway-render-trace.ts +177 -0
  68. package/src/mux/live-mux/gateway-status-timeline.ts +166 -0
  69. package/src/mux/live-mux/git-parsing.ts +131 -0
  70. package/src/mux/live-mux/git-snapshot.ts +263 -0
  71. package/src/mux/live-mux/git-state.ts +136 -0
  72. package/src/mux/live-mux/global-shortcut-handlers.ts +143 -0
  73. package/src/mux/live-mux/home-pane-actions.ts +58 -0
  74. package/src/mux/live-mux/home-pane-drop.ts +44 -0
  75. package/src/mux/live-mux/home-pane-entity-click.ts +96 -0
  76. package/src/mux/live-mux/home-pane-pointer.ts +96 -0
  77. package/src/mux/live-mux/input-forwarding.ts +112 -0
  78. package/src/mux/live-mux/layout.ts +30 -0
  79. package/src/mux/live-mux/left-nav-activation.ts +103 -0
  80. package/src/mux/live-mux/left-nav.ts +85 -0
  81. package/src/mux/live-mux/left-rail-actions.ts +118 -0
  82. package/src/mux/live-mux/left-rail-conversation-click.ts +82 -0
  83. package/src/mux/live-mux/left-rail-pointer.ts +74 -0
  84. package/src/mux/live-mux/modal-command-menu-handler.ts +101 -0
  85. package/src/mux/live-mux/modal-conversation-handlers.ts +217 -0
  86. package/src/mux/live-mux/modal-input-reducers.ts +94 -0
  87. package/src/mux/live-mux/modal-overlays.ts +287 -0
  88. package/src/mux/live-mux/modal-pointer.ts +70 -0
  89. package/src/mux/live-mux/modal-prompt-handlers.ts +187 -0
  90. package/src/mux/live-mux/modal-task-editor-handler.ts +156 -0
  91. package/src/mux/live-mux/observed-stream.ts +87 -0
  92. package/src/mux/live-mux/palette-parsing.ts +128 -0
  93. package/src/mux/live-mux/pointer-routing.ts +108 -0
  94. package/src/mux/live-mux/process-usage.ts +53 -0
  95. package/src/mux/live-mux/project-pane-pointer.ts +44 -0
  96. package/src/mux/live-mux/rail-layout.ts +244 -0
  97. package/src/mux/live-mux/render-trace-analysis.ts +213 -0
  98. package/src/mux/live-mux/render-trace-state.ts +84 -0
  99. package/src/mux/live-mux/repository-folding.ts +207 -0
  100. package/src/mux/live-mux/runtime-shutdown.ts +51 -0
  101. package/src/mux/live-mux/selection.ts +411 -0
  102. package/src/mux/live-mux/startup-utils.ts +187 -0
  103. package/src/mux/live-mux/status-timeline-state.ts +82 -0
  104. package/src/mux/live-mux/task-pane-shortcuts.ts +206 -0
  105. package/src/mux/live-mux/terminal-palette.ts +79 -0
  106. package/src/mux/new-thread-prompt.ts +165 -0
  107. package/src/mux/project-tree.ts +295 -0
  108. package/src/mux/render-frame.ts +113 -0
  109. package/src/mux/runtime-wiring.ts +185 -0
  110. package/src/mux/selector-index.ts +160 -0
  111. package/src/mux/startup-sequencer.ts +238 -0
  112. package/src/mux/task-composer.ts +289 -0
  113. package/src/mux/task-focused-pane.ts +417 -0
  114. package/src/mux/task-screen-keybindings.ts +539 -0
  115. package/src/mux/terminal-input-modes.ts +35 -0
  116. package/src/mux/workspace-path.ts +55 -0
  117. package/src/mux/workspace-rail-model.ts +701 -0
  118. package/src/mux/workspace-rail.ts +247 -0
  119. package/src/perf/perf-core.ts +307 -0
  120. package/src/pty/pty_host.ts +217 -0
  121. package/src/pty/session-broker.ts +158 -0
  122. package/src/recording/terminal-recording.ts +383 -0
  123. package/src/services/control-plane.ts +567 -0
  124. package/src/services/conversation-lifecycle.ts +176 -0
  125. package/src/services/conversation-startup-hydration.ts +47 -0
  126. package/src/services/directory-hydration.ts +49 -0
  127. package/src/services/event-persistence.ts +104 -0
  128. package/src/services/mux-ui-state-persistence.ts +82 -0
  129. package/src/services/output-load-sampler.ts +231 -0
  130. package/src/services/process-usage-refresh.ts +88 -0
  131. package/src/services/recording.ts +75 -0
  132. package/src/services/render-trace-recorder.ts +177 -0
  133. package/src/services/runtime-control-actions.ts +123 -0
  134. package/src/services/runtime-control-plane-ops.ts +131 -0
  135. package/src/services/runtime-conversation-actions.ts +113 -0
  136. package/src/services/runtime-conversation-activation.ts +78 -0
  137. package/src/services/runtime-conversation-starter.ts +171 -0
  138. package/src/services/runtime-conversation-title-edit.ts +149 -0
  139. package/src/services/runtime-directory-actions.ts +164 -0
  140. package/src/services/runtime-envelope-handler.ts +198 -0
  141. package/src/services/runtime-git-state.ts +92 -0
  142. package/src/services/runtime-input-pipeline.ts +50 -0
  143. package/src/services/runtime-input-router.ts +202 -0
  144. package/src/services/runtime-layout-resize.ts +236 -0
  145. package/src/services/runtime-left-rail-render.ts +159 -0
  146. package/src/services/runtime-main-pane-input.ts +230 -0
  147. package/src/services/runtime-modal-input.ts +119 -0
  148. package/src/services/runtime-navigation-input.ts +207 -0
  149. package/src/services/runtime-process-wiring.ts +68 -0
  150. package/src/services/runtime-rail-input.ts +287 -0
  151. package/src/services/runtime-render-flush.ts +146 -0
  152. package/src/services/runtime-render-lifecycle.ts +104 -0
  153. package/src/services/runtime-render-orchestrator.ts +108 -0
  154. package/src/services/runtime-render-pipeline.ts +167 -0
  155. package/src/services/runtime-render-state.ts +72 -0
  156. package/src/services/runtime-repository-actions.ts +197 -0
  157. package/src/services/runtime-right-pane-render.ts +132 -0
  158. package/src/services/runtime-shutdown.ts +79 -0
  159. package/src/services/runtime-stream-subscriptions.ts +56 -0
  160. package/src/services/runtime-task-composer-persistence.ts +139 -0
  161. package/src/services/runtime-task-editor-actions.ts +83 -0
  162. package/src/services/runtime-task-pane-actions.ts +198 -0
  163. package/src/services/runtime-task-pane-shortcuts.ts +189 -0
  164. package/src/services/runtime-task-pane.ts +62 -0
  165. package/src/services/runtime-workspace-actions.ts +153 -0
  166. package/src/services/runtime-workspace-observed-events.ts +190 -0
  167. package/src/services/session-projection-instrumentation.ts +190 -0
  168. package/src/services/startup-background-probe.ts +91 -0
  169. package/src/services/startup-background-resume.ts +65 -0
  170. package/src/services/startup-orchestrator.ts +166 -0
  171. package/src/services/startup-output-tracker.ts +54 -0
  172. package/src/services/startup-paint-tracker.ts +115 -0
  173. package/src/services/startup-persisted-conversation-queue.ts +45 -0
  174. package/src/services/startup-settled-gate.ts +67 -0
  175. package/src/services/startup-shutdown.ts +53 -0
  176. package/src/services/startup-span-tracker.ts +77 -0
  177. package/src/services/startup-state-hydration.ts +94 -0
  178. package/src/services/startup-visibility.ts +35 -0
  179. package/src/services/status-timeline-recorder.ts +144 -0
  180. package/src/services/task-pane-selection-actions.ts +153 -0
  181. package/src/services/task-planning-hydration.ts +58 -0
  182. package/src/services/task-planning-observed-events.ts +89 -0
  183. package/src/services/workspace-observed-events.ts +113 -0
  184. package/src/store/control-plane-store-normalize.ts +760 -0
  185. package/src/store/control-plane-store-types.ts +224 -0
  186. package/src/store/control-plane-store.ts +2951 -0
  187. package/src/store/event-store.ts +253 -0
  188. package/src/store/sqlite.ts +81 -0
  189. package/src/terminal/compat-matrix.ts +345 -0
  190. package/src/terminal/differential-checkpoints.ts +132 -0
  191. package/src/terminal/parity-suite.ts +441 -0
  192. package/src/terminal/snapshot-oracle.ts +1840 -0
  193. package/src/ui/conversation-input-forwarder.ts +114 -0
  194. package/src/ui/conversation-selection-input.ts +103 -0
  195. package/src/ui/debug-footer-notice.ts +39 -0
  196. package/src/ui/global-shortcut-input.ts +126 -0
  197. package/src/ui/input-preflight.ts +68 -0
  198. package/src/ui/input-token-router.ts +312 -0
  199. package/src/ui/input.ts +238 -0
  200. package/src/ui/kit.ts +509 -0
  201. package/src/ui/left-nav-input.ts +80 -0
  202. package/src/ui/left-rail-pointer-input.ts +148 -0
  203. package/src/ui/main-pane-pointer-input.ts +150 -0
  204. package/src/ui/modals/manager.ts +192 -0
  205. package/src/ui/mux-theme.ts +529 -0
  206. package/src/ui/panes/conversation.ts +19 -0
  207. package/src/ui/panes/home-gridfire.ts +302 -0
  208. package/src/ui/panes/home.ts +109 -0
  209. package/src/ui/panes/left-rail.ts +12 -0
  210. package/src/ui/panes/project.ts +44 -0
  211. package/src/ui/pointer-routing-input.ts +158 -0
  212. package/src/ui/repository-fold-input.ts +91 -0
  213. package/src/ui/screen.ts +210 -0
  214. package/src/ui/surface.ts +224 -0
@@ -0,0 +1,539 @@
1
+ interface KeyStroke {
2
+ readonly key: string;
3
+ readonly ctrl: boolean;
4
+ readonly alt: boolean;
5
+ readonly shift: boolean;
6
+ readonly meta: boolean;
7
+ }
8
+
9
+ interface ParsedBinding {
10
+ readonly stroke: KeyStroke;
11
+ readonly originalText: string;
12
+ }
13
+
14
+ export type TaskScreenKeybindingAction =
15
+ | 'mux.home.repo.dropdown.toggle'
16
+ | 'mux.home.repo.next'
17
+ | 'mux.home.repo.previous'
18
+ | 'mux.home.task.submit'
19
+ | 'mux.home.task.queue'
20
+ | 'mux.home.task.newline'
21
+ | 'mux.home.task.status.ready'
22
+ | 'mux.home.task.status.draft'
23
+ | 'mux.home.task.status.complete'
24
+ | 'mux.home.task.reorder.up'
25
+ | 'mux.home.task.reorder.down'
26
+ | 'mux.home.editor.cursor.left'
27
+ | 'mux.home.editor.cursor.right'
28
+ | 'mux.home.editor.cursor.up'
29
+ | 'mux.home.editor.cursor.down'
30
+ | 'mux.home.editor.line.start'
31
+ | 'mux.home.editor.line.end'
32
+ | 'mux.home.editor.word.left'
33
+ | 'mux.home.editor.word.right'
34
+ | 'mux.home.editor.delete.backward'
35
+ | 'mux.home.editor.delete.forward'
36
+ | 'mux.home.editor.delete.word.backward'
37
+ | 'mux.home.editor.delete.line.start'
38
+ | 'mux.home.editor.delete.line.end';
39
+
40
+ const ACTION_ORDER: readonly TaskScreenKeybindingAction[] = [
41
+ 'mux.home.repo.dropdown.toggle',
42
+ 'mux.home.repo.next',
43
+ 'mux.home.repo.previous',
44
+ 'mux.home.task.submit',
45
+ 'mux.home.task.queue',
46
+ 'mux.home.task.newline',
47
+ 'mux.home.task.status.ready',
48
+ 'mux.home.task.status.draft',
49
+ 'mux.home.task.status.complete',
50
+ 'mux.home.task.reorder.up',
51
+ 'mux.home.task.reorder.down',
52
+ 'mux.home.editor.cursor.left',
53
+ 'mux.home.editor.cursor.right',
54
+ 'mux.home.editor.cursor.up',
55
+ 'mux.home.editor.cursor.down',
56
+ 'mux.home.editor.line.start',
57
+ 'mux.home.editor.line.end',
58
+ 'mux.home.editor.word.left',
59
+ 'mux.home.editor.word.right',
60
+ 'mux.home.editor.delete.backward',
61
+ 'mux.home.editor.delete.forward',
62
+ 'mux.home.editor.delete.word.backward',
63
+ 'mux.home.editor.delete.line.start',
64
+ 'mux.home.editor.delete.line.end',
65
+ ] as const;
66
+
67
+ export const DEFAULT_TASK_SCREEN_KEYBINDINGS_RAW: Readonly<
68
+ Record<TaskScreenKeybindingAction, readonly string[]>
69
+ > = {
70
+ 'mux.home.repo.dropdown.toggle': ['ctrl+g'],
71
+ 'mux.home.repo.next': ['ctrl+n'],
72
+ 'mux.home.repo.previous': ['ctrl+p'],
73
+ 'mux.home.task.submit': ['enter'],
74
+ 'mux.home.task.queue': ['tab'],
75
+ 'mux.home.task.newline': ['shift+enter'],
76
+ 'mux.home.task.status.ready': ['alt+r'],
77
+ 'mux.home.task.status.draft': ['alt+d'],
78
+ 'mux.home.task.status.complete': ['alt+c'],
79
+ 'mux.home.task.reorder.up': ['alt+up'],
80
+ 'mux.home.task.reorder.down': ['alt+down'],
81
+ 'mux.home.editor.cursor.left': ['left', 'ctrl+b'],
82
+ 'mux.home.editor.cursor.right': ['right', 'ctrl+f'],
83
+ 'mux.home.editor.cursor.up': ['up'],
84
+ 'mux.home.editor.cursor.down': ['down'],
85
+ 'mux.home.editor.line.start': ['ctrl+a', 'home'],
86
+ 'mux.home.editor.line.end': ['ctrl+e', 'end'],
87
+ 'mux.home.editor.word.left': ['alt+b'],
88
+ 'mux.home.editor.word.right': ['alt+f'],
89
+ 'mux.home.editor.delete.backward': ['backspace'],
90
+ 'mux.home.editor.delete.forward': ['delete'],
91
+ 'mux.home.editor.delete.word.backward': ['ctrl+w', 'alt+backspace'],
92
+ 'mux.home.editor.delete.line.start': ['ctrl+u'],
93
+ 'mux.home.editor.delete.line.end': ['ctrl+k'],
94
+ };
95
+
96
+ const KEY_TOKEN_ALIASES = new Map<string, string>([
97
+ ['cmd', 'meta'],
98
+ ['command', 'meta'],
99
+ ['meta', 'meta'],
100
+ ['super', 'meta'],
101
+ ['ctrl', 'ctrl'],
102
+ ['control', 'ctrl'],
103
+ ['alt', 'alt'],
104
+ ['option', 'alt'],
105
+ ['shift', 'shift'],
106
+ ['esc', 'escape'],
107
+ ['return', 'enter'],
108
+ ['spacebar', 'space'],
109
+ ['del', 'delete'],
110
+ ['bs', 'backspace'],
111
+ ]);
112
+
113
+ const SUPPORTED_NAMED_KEYS = new Set([
114
+ 'enter',
115
+ 'tab',
116
+ 'escape',
117
+ 'space',
118
+ 'up',
119
+ 'down',
120
+ 'left',
121
+ 'right',
122
+ 'home',
123
+ 'end',
124
+ 'delete',
125
+ 'backspace',
126
+ ]);
127
+
128
+ export interface ResolvedTaskScreenKeybindings {
129
+ readonly rawByAction: Readonly<Record<TaskScreenKeybindingAction, readonly string[]>>;
130
+ readonly parsedByAction: Readonly<Record<TaskScreenKeybindingAction, readonly ParsedBinding[]>>;
131
+ }
132
+
133
+ function parseNumericPrefix(value: string): number | null {
134
+ const parsed = Number.parseInt(value, 10);
135
+ return Number.isNaN(parsed) ? null : parsed;
136
+ }
137
+
138
+ function decodeModifiers(modifierCode: number): Omit<KeyStroke, 'key'> | null {
139
+ if (modifierCode <= 0) {
140
+ return null;
141
+ }
142
+ const mask = modifierCode - 1;
143
+ return {
144
+ shift: (mask & 0b0001) !== 0,
145
+ alt: (mask & 0b0010) !== 0,
146
+ ctrl: (mask & 0b0100) !== 0,
147
+ meta: (mask & 0b1000) !== 0,
148
+ };
149
+ }
150
+
151
+ function keyNameFromKeyCode(code: number): string | null {
152
+ if (code === 13) {
153
+ return 'enter';
154
+ }
155
+ if (code === 9) {
156
+ return 'tab';
157
+ }
158
+ if (code === 27) {
159
+ return 'escape';
160
+ }
161
+ if (code === 32) {
162
+ return 'space';
163
+ }
164
+ if (code === 127) {
165
+ return 'backspace';
166
+ }
167
+ if (code >= 33 && code <= 126) {
168
+ return String.fromCharCode(code).toLowerCase();
169
+ }
170
+ return null;
171
+ }
172
+
173
+ function decodeSingleByte(byte: number): KeyStroke | null {
174
+ if (byte === 0x1b) {
175
+ return {
176
+ key: 'escape',
177
+ ctrl: false,
178
+ alt: false,
179
+ shift: false,
180
+ meta: false,
181
+ };
182
+ }
183
+ if (byte === 0x0d || byte === 0x0a) {
184
+ return {
185
+ key: 'enter',
186
+ ctrl: false,
187
+ alt: false,
188
+ shift: false,
189
+ meta: false,
190
+ };
191
+ }
192
+ if (byte === 0x09) {
193
+ return {
194
+ key: 'tab',
195
+ ctrl: false,
196
+ alt: false,
197
+ shift: false,
198
+ meta: false,
199
+ };
200
+ }
201
+ if (byte === 0x20) {
202
+ return {
203
+ key: 'space',
204
+ ctrl: false,
205
+ alt: false,
206
+ shift: false,
207
+ meta: false,
208
+ };
209
+ }
210
+ if (byte === 0x7f || byte === 0x08) {
211
+ return {
212
+ key: 'backspace',
213
+ ctrl: false,
214
+ alt: false,
215
+ shift: false,
216
+ meta: false,
217
+ };
218
+ }
219
+ if (byte >= 0x01 && byte <= 0x1a) {
220
+ return {
221
+ key: String.fromCharCode(byte + 96),
222
+ ctrl: true,
223
+ alt: false,
224
+ shift: false,
225
+ meta: false,
226
+ };
227
+ }
228
+ if (byte >= 32 && byte <= 126) {
229
+ const char = String.fromCharCode(byte);
230
+ const lower = char.toLowerCase();
231
+ return {
232
+ key: lower,
233
+ ctrl: false,
234
+ alt: false,
235
+ shift: char !== lower,
236
+ meta: false,
237
+ };
238
+ }
239
+ return null;
240
+ }
241
+
242
+ function parseAltPrefix(input: Buffer): KeyStroke | null {
243
+ if (input.length !== 2 || input[0] !== 0x1b) {
244
+ return null;
245
+ }
246
+ const inner = decodeSingleByte(input[1]!);
247
+ if (inner === null) {
248
+ return null;
249
+ }
250
+ return {
251
+ ...inner,
252
+ alt: true,
253
+ };
254
+ }
255
+
256
+ function parseKitty(input: string): KeyStroke | null {
257
+ if (!input.startsWith('\u001b[') || !input.endsWith('u')) {
258
+ return null;
259
+ }
260
+ const payload = input.slice(2, -1);
261
+ const match = payload.match(/^(\d+)(?::\d+)?(?:;(\d+)(?::\d+)?)?$/u);
262
+ if (match === null) {
263
+ return null;
264
+ }
265
+ const keyCode = parseNumericPrefix(match[1]!)!;
266
+ const modifiers = decodeModifiers(parseNumericPrefix(match[2] ?? '1') ?? -1);
267
+ const key = keyNameFromKeyCode(keyCode);
268
+ if (modifiers === null || key === null) {
269
+ return null;
270
+ }
271
+ return {
272
+ key,
273
+ ...modifiers,
274
+ };
275
+ }
276
+
277
+ function parseModifyOtherKeys(input: string): KeyStroke | null {
278
+ if (!input.startsWith('\u001b[') || !input.endsWith('~')) {
279
+ return null;
280
+ }
281
+ const payload = input.slice(2, -1);
282
+ const match = payload.match(/^27;(\d+);(\d+)$/u);
283
+ if (match === null) {
284
+ return null;
285
+ }
286
+ const modifiers = decodeModifiers(parseNumericPrefix(match[1]!) ?? -1);
287
+ const key = keyNameFromKeyCode(parseNumericPrefix(match[2]!) ?? -1);
288
+ if (modifiers === null || key === null) {
289
+ return null;
290
+ }
291
+ return {
292
+ key,
293
+ ...modifiers,
294
+ };
295
+ }
296
+
297
+ function csiDirectionKeyFromSuffix(suffix: 'A' | 'B' | 'C' | 'D' | 'H' | 'F'): string {
298
+ if (suffix === 'A') {
299
+ return 'up';
300
+ }
301
+ if (suffix === 'B') {
302
+ return 'down';
303
+ }
304
+ if (suffix === 'C') {
305
+ return 'right';
306
+ }
307
+ if (suffix === 'D') {
308
+ return 'left';
309
+ }
310
+ if (suffix === 'H') {
311
+ return 'home';
312
+ }
313
+ return 'end';
314
+ }
315
+
316
+ function parseCsi(input: string): KeyStroke | null {
317
+ if (!input.startsWith('\u001b[') || input.length < 3) {
318
+ return null;
319
+ }
320
+ const payload = input.slice(2);
321
+ const directionMatch = payload.match(/^(?:(\d+);(\d+)|1;(\d+))?([ABCDHF])$/u);
322
+ if (directionMatch !== null) {
323
+ const key = csiDirectionKeyFromSuffix(directionMatch[4]! as 'A' | 'B' | 'C' | 'D' | 'H' | 'F');
324
+ const modifierCode = directionMatch[2] ?? directionMatch[3] ?? '1';
325
+ const modifiers = decodeModifiers(parseNumericPrefix(modifierCode) ?? -1);
326
+ if (modifiers === null) {
327
+ return null;
328
+ }
329
+ return {
330
+ key,
331
+ ...modifiers,
332
+ };
333
+ }
334
+
335
+ const tildeMatch = payload.match(/^(\d+)(?:;(\d+))?~$/u);
336
+ if (tildeMatch === null) {
337
+ return null;
338
+ }
339
+ const baseCode = parseNumericPrefix(tildeMatch[1]!)!;
340
+ const modifierCode = parseNumericPrefix(tildeMatch[2] ?? '1')!;
341
+ const modifiers = decodeModifiers(modifierCode);
342
+ if (modifiers === null) {
343
+ return null;
344
+ }
345
+
346
+ let key: string | null = null;
347
+ if (baseCode === 1 || baseCode === 7) {
348
+ key = 'home';
349
+ } else if (baseCode === 4 || baseCode === 8) {
350
+ key = 'end';
351
+ } else if (baseCode === 3) {
352
+ key = 'delete';
353
+ } else if (baseCode === 2) {
354
+ key = 'insert';
355
+ }
356
+ if (key === null || key === 'insert') {
357
+ return null;
358
+ }
359
+ return {
360
+ key,
361
+ ...modifiers,
362
+ };
363
+ }
364
+
365
+ function decodeInputToStroke(input: Buffer): KeyStroke | null {
366
+ if (input.length === 1) {
367
+ return decodeSingleByte(input[0]!);
368
+ }
369
+ const altPrefixed = parseAltPrefix(input);
370
+ if (altPrefixed !== null) {
371
+ return altPrefixed;
372
+ }
373
+ const text = input.toString('utf8');
374
+ return parseKitty(text) ?? parseModifyOtherKeys(text) ?? parseCsi(text);
375
+ }
376
+
377
+ function normalizeKeyToken(raw: string): string | null {
378
+ const normalized = raw.trim().toLowerCase();
379
+ if (normalized.length === 0) {
380
+ return null;
381
+ }
382
+ return KEY_TOKEN_ALIASES.get(normalized) ?? normalized;
383
+ }
384
+
385
+ function parseBinding(input: string): ParsedBinding | null {
386
+ const trimmed = input.trim().toLowerCase();
387
+ if (trimmed.length === 0) {
388
+ return null;
389
+ }
390
+ const tokens = trimmed
391
+ .split('+')
392
+ .map((part) => normalizeKeyToken(part))
393
+ .flatMap((entry) => (entry === null ? [] : [entry]));
394
+ if (tokens.length === 0) {
395
+ return null;
396
+ }
397
+
398
+ const modifiers = {
399
+ ctrl: false,
400
+ alt: false,
401
+ shift: false,
402
+ meta: false,
403
+ };
404
+ for (let idx = 0; idx < tokens.length - 1; idx += 1) {
405
+ const token = tokens[idx]!;
406
+ if (token === 'ctrl') {
407
+ modifiers.ctrl = true;
408
+ continue;
409
+ }
410
+ if (token === 'alt') {
411
+ modifiers.alt = true;
412
+ continue;
413
+ }
414
+ if (token === 'shift') {
415
+ modifiers.shift = true;
416
+ continue;
417
+ }
418
+ if (token === 'meta') {
419
+ modifiers.meta = true;
420
+ continue;
421
+ }
422
+ return null;
423
+ }
424
+
425
+ const key = tokens[tokens.length - 1]!;
426
+ if (key.length !== 1 && !SUPPORTED_NAMED_KEYS.has(key)) {
427
+ return null;
428
+ }
429
+ return {
430
+ stroke: {
431
+ key,
432
+ ...modifiers,
433
+ },
434
+ originalText: trimmed,
435
+ };
436
+ }
437
+
438
+ function bindingsForAction(raw: readonly string[]): readonly ParsedBinding[] {
439
+ const parsed: ParsedBinding[] = [];
440
+ for (const value of raw) {
441
+ const next = parseBinding(value);
442
+ if (next !== null) {
443
+ parsed.push(next);
444
+ }
445
+ }
446
+ return parsed;
447
+ }
448
+
449
+ function strokesEqual(left: KeyStroke, right: KeyStroke): boolean {
450
+ return (
451
+ left.key === right.key &&
452
+ left.ctrl === right.ctrl &&
453
+ left.alt === right.alt &&
454
+ left.shift === right.shift &&
455
+ left.meta === right.meta
456
+ );
457
+ }
458
+
459
+ export function resolveTaskScreenKeybindings(
460
+ overrides: Readonly<Record<string, readonly string[]>> = {},
461
+ ): ResolvedTaskScreenKeybindings {
462
+ const rawByAction = {
463
+ ...DEFAULT_TASK_SCREEN_KEYBINDINGS_RAW,
464
+ } as Record<TaskScreenKeybindingAction, readonly string[]>;
465
+ for (const action of ACTION_ORDER) {
466
+ const override = overrides[action];
467
+ if (override !== undefined) {
468
+ rawByAction[action] = override;
469
+ }
470
+ }
471
+ return {
472
+ rawByAction,
473
+ parsedByAction: {
474
+ 'mux.home.repo.dropdown.toggle': bindingsForAction(
475
+ rawByAction['mux.home.repo.dropdown.toggle'],
476
+ ),
477
+ 'mux.home.repo.next': bindingsForAction(rawByAction['mux.home.repo.next']),
478
+ 'mux.home.repo.previous': bindingsForAction(rawByAction['mux.home.repo.previous']),
479
+ 'mux.home.task.submit': bindingsForAction(rawByAction['mux.home.task.submit']),
480
+ 'mux.home.task.queue': bindingsForAction(rawByAction['mux.home.task.queue']),
481
+ 'mux.home.task.newline': bindingsForAction(rawByAction['mux.home.task.newline']),
482
+ 'mux.home.task.status.ready': bindingsForAction(rawByAction['mux.home.task.status.ready']),
483
+ 'mux.home.task.status.draft': bindingsForAction(rawByAction['mux.home.task.status.draft']),
484
+ 'mux.home.task.status.complete': bindingsForAction(
485
+ rawByAction['mux.home.task.status.complete'],
486
+ ),
487
+ 'mux.home.task.reorder.up': bindingsForAction(rawByAction['mux.home.task.reorder.up']),
488
+ 'mux.home.task.reorder.down': bindingsForAction(rawByAction['mux.home.task.reorder.down']),
489
+ 'mux.home.editor.cursor.left': bindingsForAction(rawByAction['mux.home.editor.cursor.left']),
490
+ 'mux.home.editor.cursor.right': bindingsForAction(
491
+ rawByAction['mux.home.editor.cursor.right'],
492
+ ),
493
+ 'mux.home.editor.cursor.up': bindingsForAction(rawByAction['mux.home.editor.cursor.up']),
494
+ 'mux.home.editor.cursor.down': bindingsForAction(rawByAction['mux.home.editor.cursor.down']),
495
+ 'mux.home.editor.line.start': bindingsForAction(rawByAction['mux.home.editor.line.start']),
496
+ 'mux.home.editor.line.end': bindingsForAction(rawByAction['mux.home.editor.line.end']),
497
+ 'mux.home.editor.word.left': bindingsForAction(rawByAction['mux.home.editor.word.left']),
498
+ 'mux.home.editor.word.right': bindingsForAction(rawByAction['mux.home.editor.word.right']),
499
+ 'mux.home.editor.delete.backward': bindingsForAction(
500
+ rawByAction['mux.home.editor.delete.backward'],
501
+ ),
502
+ 'mux.home.editor.delete.forward': bindingsForAction(
503
+ rawByAction['mux.home.editor.delete.forward'],
504
+ ),
505
+ 'mux.home.editor.delete.word.backward': bindingsForAction(
506
+ rawByAction['mux.home.editor.delete.word.backward'],
507
+ ),
508
+ 'mux.home.editor.delete.line.start': bindingsForAction(
509
+ rawByAction['mux.home.editor.delete.line.start'],
510
+ ),
511
+ 'mux.home.editor.delete.line.end': bindingsForAction(
512
+ rawByAction['mux.home.editor.delete.line.end'],
513
+ ),
514
+ },
515
+ };
516
+ }
517
+
518
+ export function firstTaskScreenShortcutText(
519
+ bindings: ResolvedTaskScreenKeybindings,
520
+ action: TaskScreenKeybindingAction,
521
+ ): string {
522
+ return bindings.rawByAction[action][0] ?? '';
523
+ }
524
+
525
+ export function detectTaskScreenKeybindingAction(
526
+ input: Buffer,
527
+ bindings: ResolvedTaskScreenKeybindings,
528
+ ): TaskScreenKeybindingAction | null {
529
+ const stroke = decodeInputToStroke(input);
530
+ if (stroke === null) {
531
+ return null;
532
+ }
533
+ for (const action of ACTION_ORDER) {
534
+ if (bindings.parsedByAction[action].some((binding) => strokesEqual(binding.stroke, stroke))) {
535
+ return action;
536
+ }
537
+ }
538
+ return null;
539
+ }
@@ -0,0 +1,35 @@
1
+ const ENABLE_KEYBOARD_PROTOCOL = '\u001b[>1u';
2
+ const ENABLE_POINTER_AND_FOCUS_MODES = '\u001b[?1000h\u001b[?1002h\u001b[?1004h\u001b[?1006h';
3
+
4
+ // Keep disable broader than enable for robust cleanup from partially configured terminals.
5
+ const DISABLE_POINTER_AND_FOCUS_MODES =
6
+ '\u001b[?2004l\u001b[?1006l\u001b[?1015l\u001b[?1005l\u001b[?1004l\u001b[?1003l\u001b[?1002l\u001b[?1000l';
7
+ const DISABLE_KEYBOARD_PROTOCOL = '\u001b[<u';
8
+
9
+ export const ENABLE_MUX_INPUT_MODES = `${ENABLE_KEYBOARD_PROTOCOL}${ENABLE_POINTER_AND_FOCUS_MODES}`;
10
+ export const DISABLE_MUX_INPUT_MODES = `${DISABLE_POINTER_AND_FOCUS_MODES}${DISABLE_KEYBOARD_PROTOCOL}`;
11
+
12
+ interface MuxInputModeManager {
13
+ enable: () => void;
14
+ restore: () => void;
15
+ isEnabled: () => boolean;
16
+ }
17
+
18
+ export function createMuxInputModeManager(write: (sequence: string) => void): MuxInputModeManager {
19
+ let enabled = false;
20
+
21
+ return {
22
+ enable: (): void => {
23
+ if (enabled) {
24
+ return;
25
+ }
26
+ write(ENABLE_MUX_INPUT_MODES);
27
+ enabled = true;
28
+ },
29
+ restore: (): void => {
30
+ write(DISABLE_MUX_INPUT_MODES);
31
+ enabled = false;
32
+ },
33
+ isEnabled: (): boolean => enabled,
34
+ };
35
+ }
@@ -0,0 +1,55 @@
1
+ import { join, resolve, sep } from 'node:path';
2
+
3
+ function stripWrappingQuotes(value: string): string {
4
+ if (value.length < 2) {
5
+ return value;
6
+ }
7
+ const first = value[0];
8
+ const last = value[value.length - 1];
9
+ if ((first === '"' && last === '"') || (first === "'" && last === "'")) {
10
+ return value.slice(1, -1);
11
+ }
12
+ return value;
13
+ }
14
+
15
+ export function normalizeWorkspacePathInput(value: string): string {
16
+ const trimmed = value.trim();
17
+ const withoutPrefix = trimmed.toLowerCase().startsWith('path:')
18
+ ? trimmed.slice('path:'.length).trim()
19
+ : trimmed;
20
+ return stripWrappingQuotes(withoutPrefix.trim());
21
+ }
22
+
23
+ export function expandHomePath(value: string, homeDirectory: string | null): string {
24
+ const normalized = normalizeWorkspacePathInput(value);
25
+ if (homeDirectory === null || homeDirectory.length === 0) {
26
+ return normalized;
27
+ }
28
+ if (normalized === '~') {
29
+ return homeDirectory;
30
+ }
31
+ if (normalized.startsWith('~/')) {
32
+ return join(homeDirectory, normalized.slice(2));
33
+ }
34
+ return normalized;
35
+ }
36
+
37
+ export function resolveWorkspacePath(
38
+ invocationDirectory: string,
39
+ value: string,
40
+ homeDirectory: string | null,
41
+ ): string {
42
+ const resolvedInvocation = resolve(invocationDirectory);
43
+ const expanded = expandHomePath(value, homeDirectory);
44
+ if (homeDirectory !== null && homeDirectory.length > 0) {
45
+ const invocationTildePrefix = `${resolvedInvocation}${sep}~`;
46
+ if (expanded === invocationTildePrefix) {
47
+ return homeDirectory;
48
+ }
49
+ const invocationTildePathPrefix = `${invocationTildePrefix}${sep}`;
50
+ if (expanded.startsWith(invocationTildePathPrefix)) {
51
+ return resolve(homeDirectory, expanded.slice(invocationTildePathPrefix.length));
52
+ }
53
+ }
54
+ return resolve(resolvedInvocation, expanded);
55
+ }