@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,667 @@
1
+ type MuxGlobalShortcutAction =
2
+ | 'mux.app.quit'
3
+ | 'mux.app.interrupt-all'
4
+ | 'mux.command-menu.toggle'
5
+ | 'mux.gateway.profile.toggle'
6
+ | 'mux.gateway.status-timeline.toggle'
7
+ | 'mux.gateway.render-trace.toggle'
8
+ | 'mux.conversation.new'
9
+ | 'mux.conversation.critique.open-or-create'
10
+ | 'mux.conversation.next'
11
+ | 'mux.conversation.previous'
12
+ | 'mux.conversation.interrupt'
13
+ | 'mux.conversation.archive'
14
+ | 'mux.conversation.takeover'
15
+ | 'mux.conversation.delete'
16
+ | 'mux.directory.add'
17
+ | 'mux.directory.close';
18
+
19
+ interface KeyStroke {
20
+ readonly key: string;
21
+ readonly ctrl: boolean;
22
+ readonly alt: boolean;
23
+ readonly shift: boolean;
24
+ readonly meta: boolean;
25
+ }
26
+
27
+ interface ParsedShortcutBinding {
28
+ readonly stroke: KeyStroke;
29
+ readonly originalText: string;
30
+ }
31
+
32
+ interface ResolvedMuxShortcutBindings {
33
+ readonly rawByAction: Readonly<Record<MuxGlobalShortcutAction, readonly string[]>>;
34
+ readonly parsedByAction: Readonly<
35
+ Record<MuxGlobalShortcutAction, readonly ParsedShortcutBinding[]>
36
+ >;
37
+ }
38
+
39
+ const ACTION_ORDER: readonly MuxGlobalShortcutAction[] = [
40
+ 'mux.app.quit',
41
+ 'mux.app.interrupt-all',
42
+ 'mux.command-menu.toggle',
43
+ 'mux.gateway.profile.toggle',
44
+ 'mux.gateway.status-timeline.toggle',
45
+ 'mux.gateway.render-trace.toggle',
46
+ 'mux.conversation.new',
47
+ 'mux.conversation.critique.open-or-create',
48
+ 'mux.conversation.next',
49
+ 'mux.conversation.previous',
50
+ 'mux.conversation.interrupt',
51
+ 'mux.conversation.archive',
52
+ 'mux.conversation.takeover',
53
+ 'mux.conversation.delete',
54
+ 'mux.directory.add',
55
+ 'mux.directory.close',
56
+ ];
57
+
58
+ const DEFAULT_MUX_SHORTCUT_BINDINGS_RAW: Readonly<
59
+ Record<MuxGlobalShortcutAction, readonly string[]>
60
+ > = {
61
+ 'mux.app.quit': [],
62
+ 'mux.app.interrupt-all': ['ctrl+c'],
63
+ 'mux.command-menu.toggle': ['ctrl+p', 'cmd+p'],
64
+ 'mux.gateway.profile.toggle': ['ctrl+shift+p'],
65
+ 'mux.gateway.status-timeline.toggle': ['alt+r'],
66
+ 'mux.gateway.render-trace.toggle': ['ctrl+]'],
67
+ 'mux.conversation.new': ['ctrl+t'],
68
+ 'mux.conversation.critique.open-or-create': ['ctrl+g'],
69
+ 'mux.conversation.next': ['ctrl+j'],
70
+ 'mux.conversation.previous': ['ctrl+k'],
71
+ 'mux.conversation.interrupt': [],
72
+ 'mux.conversation.archive': [],
73
+ 'mux.conversation.takeover': ['ctrl+l'],
74
+ 'mux.conversation.delete': ['ctrl+x'],
75
+ 'mux.directory.add': ['ctrl+o'],
76
+ 'mux.directory.close': ['ctrl+w'],
77
+ };
78
+
79
+ const KEY_TOKEN_ALIASES = new Map<string, string>([
80
+ ['cmd', 'meta'],
81
+ ['command', 'meta'],
82
+ ['meta', 'meta'],
83
+ ['super', 'meta'],
84
+ ['ctrl', 'ctrl'],
85
+ ['control', 'ctrl'],
86
+ ['alt', 'alt'],
87
+ ['option', 'alt'],
88
+ ['shift', 'shift'],
89
+ ['esc', 'escape'],
90
+ ['return', 'enter'],
91
+ ['spacebar', 'space'],
92
+ ]);
93
+
94
+ function parseNumericPrefix(value: string): number | null {
95
+ const parsed = Number.parseInt(value, 10);
96
+ return Number.isNaN(parsed) ? null : parsed;
97
+ }
98
+
99
+ function decodeModifiers(modifierCode: number): Omit<KeyStroke, 'key'> | null {
100
+ if (modifierCode <= 0) {
101
+ return null;
102
+ }
103
+ const mask = modifierCode - 1;
104
+ return {
105
+ shift: (mask & 0b0001) !== 0,
106
+ alt: (mask & 0b0010) !== 0,
107
+ ctrl: (mask & 0b0100) !== 0,
108
+ meta: (mask & 0b1000) !== 0,
109
+ };
110
+ }
111
+
112
+ function keyNameFromKeyCode(keyCode: number): string | null {
113
+ if (keyCode < 0) {
114
+ return null;
115
+ }
116
+ if (keyCode === 13) {
117
+ return 'enter';
118
+ }
119
+ if (keyCode === 9) {
120
+ return 'tab';
121
+ }
122
+ if (keyCode === 27) {
123
+ return 'escape';
124
+ }
125
+ if (keyCode === 32) {
126
+ return 'space';
127
+ }
128
+ if (keyCode >= 33 && keyCode <= 126) {
129
+ return String.fromCharCode(keyCode).toLowerCase();
130
+ }
131
+ return null;
132
+ }
133
+
134
+ function controlByteToKeyStroke(byte: number): KeyStroke | null {
135
+ if (byte === 0x1b) {
136
+ return {
137
+ key: 'escape',
138
+ ctrl: false,
139
+ alt: false,
140
+ shift: false,
141
+ meta: false,
142
+ };
143
+ }
144
+ if (byte === 0x0d) {
145
+ return {
146
+ key: 'enter',
147
+ ctrl: false,
148
+ alt: false,
149
+ shift: false,
150
+ meta: false,
151
+ };
152
+ }
153
+ if (byte === 0x09) {
154
+ return {
155
+ key: 'tab',
156
+ ctrl: false,
157
+ alt: false,
158
+ shift: false,
159
+ meta: false,
160
+ };
161
+ }
162
+ if (byte === 0x20) {
163
+ return {
164
+ key: 'space',
165
+ ctrl: false,
166
+ alt: false,
167
+ shift: false,
168
+ meta: false,
169
+ };
170
+ }
171
+
172
+ if (byte >= 0x01 && byte <= 0x1a) {
173
+ return {
174
+ key: String.fromCharCode(byte + 96),
175
+ ctrl: true,
176
+ alt: false,
177
+ shift: false,
178
+ meta: false,
179
+ };
180
+ }
181
+
182
+ if (byte === 0x1c) {
183
+ return {
184
+ key: '\\',
185
+ ctrl: true,
186
+ alt: false,
187
+ shift: false,
188
+ meta: false,
189
+ };
190
+ }
191
+ if (byte === 0x1d) {
192
+ return {
193
+ key: ']',
194
+ ctrl: true,
195
+ alt: false,
196
+ shift: false,
197
+ meta: false,
198
+ };
199
+ }
200
+ if (byte === 0x1e) {
201
+ return {
202
+ key: '^',
203
+ ctrl: true,
204
+ alt: false,
205
+ shift: false,
206
+ meta: false,
207
+ };
208
+ }
209
+ if (byte === 0x1f) {
210
+ return {
211
+ key: '_',
212
+ ctrl: true,
213
+ alt: false,
214
+ shift: false,
215
+ meta: false,
216
+ };
217
+ }
218
+
219
+ if (byte >= 32 && byte <= 126) {
220
+ const char = String.fromCharCode(byte);
221
+ const lower = char.toLowerCase();
222
+ const isUpper = char !== lower;
223
+ return {
224
+ key: lower,
225
+ ctrl: false,
226
+ alt: false,
227
+ shift: isUpper,
228
+ meta: false,
229
+ };
230
+ }
231
+
232
+ return null;
233
+ }
234
+
235
+ function parseKittyKeyboardProtocol(text: string): KeyStroke | null {
236
+ if (!text.startsWith('\u001b[') || !text.endsWith('u')) {
237
+ return null;
238
+ }
239
+
240
+ const payload = text.slice(2, -1);
241
+ const params = payload.split(';');
242
+ if (params.length > 3) {
243
+ return null;
244
+ }
245
+
246
+ const keyCode = parseNumericPrefix(params[0]!.split(':')[0]!);
247
+ if (keyCode === null) {
248
+ return null;
249
+ }
250
+ const key = keyNameFromKeyCode(keyCode);
251
+ if (key === null) {
252
+ return null;
253
+ }
254
+
255
+ const modifierCode = params.length >= 2 ? parseNumericPrefix(params[1]!.split(':')[0]!) : 1;
256
+ if (modifierCode === null) {
257
+ return null;
258
+ }
259
+ const modifiers = decodeModifiers(modifierCode);
260
+ if (modifiers === null) {
261
+ return null;
262
+ }
263
+
264
+ return {
265
+ key,
266
+ ...modifiers,
267
+ };
268
+ }
269
+
270
+ function parseModifyOtherKeysProtocol(text: string): KeyStroke | null {
271
+ if (!text.startsWith('\u001b[27;') || !text.endsWith('~')) {
272
+ return null;
273
+ }
274
+
275
+ const payload = text.slice('\u001b['.length, -1);
276
+ const params = payload.split(';');
277
+ if (params.length !== 3) {
278
+ return null;
279
+ }
280
+
281
+ const modifierCode = parseNumericPrefix(params[1]!);
282
+ const keyCode = parseNumericPrefix(params[2]!);
283
+ if (modifierCode === null || keyCode === null) {
284
+ return null;
285
+ }
286
+ const modifiers = decodeModifiers(modifierCode);
287
+ const key = keyNameFromKeyCode(keyCode);
288
+ if (modifiers === null || key === null) {
289
+ return null;
290
+ }
291
+
292
+ return {
293
+ key,
294
+ ...modifiers,
295
+ };
296
+ }
297
+
298
+ function parseAltPrefixInput(input: Buffer): KeyStroke | null {
299
+ if (input.length !== 2 || input[0] !== 0x1b) {
300
+ return null;
301
+ }
302
+ const inner = controlByteToKeyStroke(input[1]!);
303
+ if (inner === null) {
304
+ return null;
305
+ }
306
+ return {
307
+ key: inner.key,
308
+ ctrl: inner.ctrl,
309
+ alt: true,
310
+ shift: inner.shift,
311
+ meta: inner.meta,
312
+ };
313
+ }
314
+
315
+ function decodeInputToKeyStroke(input: Buffer): KeyStroke | null {
316
+ if (input.length === 1) {
317
+ return controlByteToKeyStroke(input[0]!);
318
+ }
319
+
320
+ const altPrefixed = parseAltPrefixInput(input);
321
+ if (altPrefixed !== null) {
322
+ return altPrefixed;
323
+ }
324
+
325
+ const text = input.toString('utf8');
326
+ const kitty = parseKittyKeyboardProtocol(text);
327
+ if (kitty !== null) {
328
+ return kitty;
329
+ }
330
+
331
+ return parseModifyOtherKeysProtocol(text);
332
+ }
333
+
334
+ function keyStrokeToLegacyBytes(stroke: KeyStroke): Buffer | null {
335
+ if (stroke.key === 'enter' && stroke.shift) {
336
+ // Preserve Shift+Enter protocol bytes so apps that differentiate it (for newline vs submit)
337
+ // can handle it; collapsing to CR loses intent.
338
+ return null;
339
+ }
340
+ let base: Buffer | null = null;
341
+ if (stroke.ctrl) {
342
+ if (stroke.key === 'space') {
343
+ base = Buffer.from([0x00]);
344
+ } else if (stroke.key === 'enter') {
345
+ base = Buffer.from([0x0d]);
346
+ } else if (stroke.key === 'tab') {
347
+ base = Buffer.from([0x09]);
348
+ } else if (stroke.key === 'escape') {
349
+ base = Buffer.from([0x1b]);
350
+ } else if (stroke.key.length === 1) {
351
+ const key = stroke.key.toLowerCase();
352
+ const code = key.charCodeAt(0);
353
+ if (code >= 97 && code <= 122) {
354
+ base = Buffer.from([code - 96]);
355
+ } else if (key === '@') {
356
+ base = Buffer.from([0x00]);
357
+ } else if (key === '[') {
358
+ base = Buffer.from([0x1b]);
359
+ } else if (key === '\\') {
360
+ base = Buffer.from([0x1c]);
361
+ } else if (key === ']') {
362
+ base = Buffer.from([0x1d]);
363
+ } else if (key === '^') {
364
+ base = Buffer.from([0x1e]);
365
+ } else if (key === '_') {
366
+ base = Buffer.from([0x1f]);
367
+ } else if (key === '?') {
368
+ base = Buffer.from([0x7f]);
369
+ }
370
+ }
371
+ } else if (stroke.key === 'enter') {
372
+ base = Buffer.from([0x0d]);
373
+ } else if (stroke.key === 'tab') {
374
+ base = Buffer.from([0x09]);
375
+ } else if (stroke.key === 'escape') {
376
+ base = Buffer.from([0x1b]);
377
+ } else if (stroke.key === 'space') {
378
+ base = Buffer.from([0x20]);
379
+ } else if (stroke.key.length === 1) {
380
+ const key =
381
+ stroke.shift && stroke.key >= 'a' && stroke.key <= 'z'
382
+ ? stroke.key.toUpperCase()
383
+ : stroke.key;
384
+ base = Buffer.from(key, 'utf8');
385
+ }
386
+
387
+ if (base === null) {
388
+ return null;
389
+ }
390
+ if (!stroke.alt && !stroke.meta) {
391
+ return base;
392
+ }
393
+ return Buffer.concat([Buffer.from([0x1b]), base]);
394
+ }
395
+
396
+ function decodeEncodedKeystrokeSequence(sequence: string): Buffer | null {
397
+ const decodedStroke =
398
+ parseKittyKeyboardProtocol(sequence) ?? parseModifyOtherKeysProtocol(sequence);
399
+ if (decodedStroke === null) {
400
+ return null;
401
+ }
402
+ return keyStrokeToLegacyBytes(decodedStroke);
403
+ }
404
+
405
+ export function normalizeMuxKeyboardInputForPty(input: Buffer): Buffer {
406
+ if (!input.includes(0x1b)) {
407
+ return input;
408
+ }
409
+ const text = input.toString('utf8');
410
+ const parts: Buffer[] = [];
411
+ let cursor = 0;
412
+ while (cursor < text.length) {
413
+ const char = text[cursor]!;
414
+ if (char !== '\u001b' || text[cursor + 1] !== '[') {
415
+ parts.push(Buffer.from(char, 'utf8'));
416
+ cursor += 1;
417
+ continue;
418
+ }
419
+
420
+ let matchedSequence: string | null = null;
421
+ let idx = cursor + 2;
422
+ while (idx < text.length) {
423
+ const tokenChar = text[idx]!;
424
+ const isDigit = tokenChar >= '0' && tokenChar <= '9';
425
+ if (isDigit || tokenChar === ';' || tokenChar === ':') {
426
+ idx += 1;
427
+ continue;
428
+ }
429
+ if (tokenChar === 'u' || tokenChar === '~') {
430
+ const candidate = text.slice(cursor, idx + 1);
431
+ const decoded = decodeEncodedKeystrokeSequence(candidate);
432
+ if (decoded !== null) {
433
+ parts.push(decoded);
434
+ matchedSequence = candidate;
435
+ }
436
+ }
437
+ break;
438
+ }
439
+
440
+ if (matchedSequence !== null) {
441
+ cursor += matchedSequence.length;
442
+ continue;
443
+ }
444
+
445
+ parts.push(Buffer.from('\u001b', 'utf8'));
446
+ cursor += 1;
447
+ }
448
+ return Buffer.concat(parts);
449
+ }
450
+
451
+ function normalizeKeyToken(raw: string): string | null {
452
+ const key = raw.trim().toLowerCase();
453
+ if (key.length === 0) {
454
+ return null;
455
+ }
456
+ return KEY_TOKEN_ALIASES.get(key) ?? key;
457
+ }
458
+
459
+ function parseShortcutBinding(input: string): ParsedShortcutBinding | null {
460
+ const trimmed = input.trim().toLowerCase();
461
+ if (trimmed.length === 0) {
462
+ return null;
463
+ }
464
+ const rawParts = trimmed.split('+');
465
+
466
+ const tokens = rawParts
467
+ .map((part) => normalizeKeyToken(part))
468
+ .flatMap((token) => (token === null ? [] : [token]));
469
+ if (tokens.length === 0) {
470
+ return null;
471
+ }
472
+
473
+ const modifiers = {
474
+ ctrl: false,
475
+ alt: false,
476
+ shift: false,
477
+ meta: false,
478
+ };
479
+
480
+ for (let idx = 0; idx < tokens.length - 1; idx += 1) {
481
+ const token = tokens[idx]!;
482
+ if (token === 'ctrl') {
483
+ modifiers.ctrl = true;
484
+ continue;
485
+ }
486
+ if (token === 'alt') {
487
+ modifiers.alt = true;
488
+ continue;
489
+ }
490
+ if (token === 'shift') {
491
+ modifiers.shift = true;
492
+ continue;
493
+ }
494
+ if (token === 'meta') {
495
+ modifiers.meta = true;
496
+ continue;
497
+ }
498
+ return null;
499
+ }
500
+
501
+ const key = tokens[tokens.length - 1]!;
502
+ const validNamedKeys = new Set([
503
+ 'enter',
504
+ 'tab',
505
+ 'escape',
506
+ 'space',
507
+ 'up',
508
+ 'down',
509
+ 'left',
510
+ 'right',
511
+ 'home',
512
+ 'end',
513
+ 'pageup',
514
+ 'pagedown',
515
+ ]);
516
+ if (key.length !== 1 && !validNamedKeys.has(key)) {
517
+ return null;
518
+ }
519
+
520
+ return {
521
+ stroke: {
522
+ key,
523
+ ...modifiers,
524
+ },
525
+ originalText: trimmed,
526
+ };
527
+ }
528
+
529
+ function strokesEqual(left: KeyStroke, right: KeyStroke): boolean {
530
+ return (
531
+ left.key === right.key &&
532
+ left.ctrl === right.ctrl &&
533
+ left.alt === right.alt &&
534
+ left.shift === right.shift &&
535
+ left.meta === right.meta
536
+ );
537
+ }
538
+
539
+ function parseBindingsForAction(rawBindings: readonly string[]): readonly ParsedShortcutBinding[] {
540
+ const parsed: ParsedShortcutBinding[] = [];
541
+ for (const raw of rawBindings) {
542
+ const normalized = parseShortcutBinding(raw);
543
+ if (normalized !== null) {
544
+ parsed.push(normalized);
545
+ }
546
+ }
547
+ return parsed;
548
+ }
549
+
550
+ function withDefaultBindings(
551
+ overrides: Readonly<Record<string, readonly string[]> | undefined>,
552
+ ): Readonly<Record<MuxGlobalShortcutAction, readonly string[]>> {
553
+ return {
554
+ 'mux.app.quit':
555
+ overrides?.['mux.app.quit'] ?? DEFAULT_MUX_SHORTCUT_BINDINGS_RAW['mux.app.quit'],
556
+ 'mux.app.interrupt-all':
557
+ overrides?.['mux.app.interrupt-all'] ??
558
+ DEFAULT_MUX_SHORTCUT_BINDINGS_RAW['mux.app.interrupt-all'],
559
+ 'mux.command-menu.toggle':
560
+ overrides?.['mux.command-menu.toggle'] ??
561
+ DEFAULT_MUX_SHORTCUT_BINDINGS_RAW['mux.command-menu.toggle'],
562
+ 'mux.gateway.profile.toggle':
563
+ overrides?.['mux.gateway.profile.toggle'] ??
564
+ DEFAULT_MUX_SHORTCUT_BINDINGS_RAW['mux.gateway.profile.toggle'],
565
+ 'mux.gateway.status-timeline.toggle':
566
+ overrides?.['mux.gateway.status-timeline.toggle'] ??
567
+ DEFAULT_MUX_SHORTCUT_BINDINGS_RAW['mux.gateway.status-timeline.toggle'],
568
+ 'mux.gateway.render-trace.toggle':
569
+ overrides?.['mux.gateway.render-trace.toggle'] ??
570
+ DEFAULT_MUX_SHORTCUT_BINDINGS_RAW['mux.gateway.render-trace.toggle'],
571
+ 'mux.conversation.new':
572
+ overrides?.['mux.conversation.new'] ??
573
+ DEFAULT_MUX_SHORTCUT_BINDINGS_RAW['mux.conversation.new'],
574
+ 'mux.conversation.critique.open-or-create':
575
+ overrides?.['mux.conversation.critique.open-or-create'] ??
576
+ DEFAULT_MUX_SHORTCUT_BINDINGS_RAW['mux.conversation.critique.open-or-create'],
577
+ 'mux.conversation.next':
578
+ overrides?.['mux.conversation.next'] ??
579
+ DEFAULT_MUX_SHORTCUT_BINDINGS_RAW['mux.conversation.next'],
580
+ 'mux.conversation.previous':
581
+ overrides?.['mux.conversation.previous'] ??
582
+ DEFAULT_MUX_SHORTCUT_BINDINGS_RAW['mux.conversation.previous'],
583
+ 'mux.conversation.interrupt':
584
+ overrides?.['mux.conversation.interrupt'] ??
585
+ DEFAULT_MUX_SHORTCUT_BINDINGS_RAW['mux.conversation.interrupt'],
586
+ 'mux.conversation.archive':
587
+ overrides?.['mux.conversation.archive'] ??
588
+ DEFAULT_MUX_SHORTCUT_BINDINGS_RAW['mux.conversation.archive'],
589
+ 'mux.conversation.takeover':
590
+ overrides?.['mux.conversation.takeover'] ??
591
+ DEFAULT_MUX_SHORTCUT_BINDINGS_RAW['mux.conversation.takeover'],
592
+ 'mux.conversation.delete':
593
+ overrides?.['mux.conversation.delete'] ??
594
+ DEFAULT_MUX_SHORTCUT_BINDINGS_RAW['mux.conversation.delete'],
595
+ 'mux.directory.add':
596
+ overrides?.['mux.directory.add'] ?? DEFAULT_MUX_SHORTCUT_BINDINGS_RAW['mux.directory.add'],
597
+ 'mux.directory.close':
598
+ overrides?.['mux.directory.close'] ??
599
+ DEFAULT_MUX_SHORTCUT_BINDINGS_RAW['mux.directory.close'],
600
+ };
601
+ }
602
+
603
+ export function resolveMuxShortcutBindings(
604
+ overrides?: Readonly<Record<string, readonly string[]> | undefined>,
605
+ ): ResolvedMuxShortcutBindings {
606
+ const rawByAction = withDefaultBindings(overrides);
607
+ return {
608
+ rawByAction,
609
+ parsedByAction: {
610
+ 'mux.app.quit': parseBindingsForAction(rawByAction['mux.app.quit']),
611
+ 'mux.app.interrupt-all': parseBindingsForAction(rawByAction['mux.app.interrupt-all']),
612
+ 'mux.command-menu.toggle': parseBindingsForAction(rawByAction['mux.command-menu.toggle']),
613
+ 'mux.gateway.profile.toggle': parseBindingsForAction(
614
+ rawByAction['mux.gateway.profile.toggle'],
615
+ ),
616
+ 'mux.gateway.status-timeline.toggle': parseBindingsForAction(
617
+ rawByAction['mux.gateway.status-timeline.toggle'],
618
+ ),
619
+ 'mux.gateway.render-trace.toggle': parseBindingsForAction(
620
+ rawByAction['mux.gateway.render-trace.toggle'],
621
+ ),
622
+ 'mux.conversation.new': parseBindingsForAction(rawByAction['mux.conversation.new']),
623
+ 'mux.conversation.critique.open-or-create': parseBindingsForAction(
624
+ rawByAction['mux.conversation.critique.open-or-create'],
625
+ ),
626
+ 'mux.conversation.next': parseBindingsForAction(rawByAction['mux.conversation.next']),
627
+ 'mux.conversation.previous': parseBindingsForAction(rawByAction['mux.conversation.previous']),
628
+ 'mux.conversation.interrupt': parseBindingsForAction(
629
+ rawByAction['mux.conversation.interrupt'],
630
+ ),
631
+ 'mux.conversation.archive': parseBindingsForAction(rawByAction['mux.conversation.archive']),
632
+ 'mux.conversation.takeover': parseBindingsForAction(rawByAction['mux.conversation.takeover']),
633
+ 'mux.conversation.delete': parseBindingsForAction(rawByAction['mux.conversation.delete']),
634
+ 'mux.directory.add': parseBindingsForAction(rawByAction['mux.directory.add']),
635
+ 'mux.directory.close': parseBindingsForAction(rawByAction['mux.directory.close']),
636
+ },
637
+ };
638
+ }
639
+
640
+ const DEFAULT_SHORTCUT_BINDINGS = resolveMuxShortcutBindings();
641
+
642
+ export function firstShortcutText(
643
+ bindings: ResolvedMuxShortcutBindings,
644
+ action: MuxGlobalShortcutAction,
645
+ ): string {
646
+ return bindings.rawByAction[action][0] ?? '';
647
+ }
648
+
649
+ export function detectMuxGlobalShortcut(
650
+ input: Buffer,
651
+ bindings: ResolvedMuxShortcutBindings = DEFAULT_SHORTCUT_BINDINGS,
652
+ ): MuxGlobalShortcutAction | null {
653
+ const stroke = decodeInputToKeyStroke(input);
654
+ if (stroke === null) {
655
+ return null;
656
+ }
657
+
658
+ for (const action of ACTION_ORDER) {
659
+ const match = bindings.parsedByAction[action].some((binding) =>
660
+ strokesEqual(binding.stroke, stroke),
661
+ );
662
+ if (match) {
663
+ return action;
664
+ }
665
+ }
666
+ return null;
667
+ }