@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,114 @@
1
+ import {
2
+ classifyPaneAt as classifyPaneAtFrame,
3
+ parseMuxInputChunk as parseMuxInputChunkFrame,
4
+ wheelDeltaRowsFromCode,
5
+ type computeDualPaneLayout,
6
+ } from '../mux/dual-pane-core.ts';
7
+ import { normalizeMuxKeyboardInputForPty as normalizeMuxKeyboardInputForPtyFrame } from '../mux/input-shortcuts.ts';
8
+ import { routeInputTokensForConversation as routeInputTokensForConversationFrame } from '../mux/live-mux/input-forwarding.ts';
9
+ import { hasShiftModifier } from '../mux/live-mux/selection.ts';
10
+ import type { ConversationState } from '../mux/live-mux/conversation-state.ts';
11
+ import type { InputTokenRouter } from './input-token-router.ts';
12
+
13
+ type MainPaneMode = 'conversation' | 'project' | 'home';
14
+ type DualPaneLayout = ReturnType<typeof computeDualPaneLayout>;
15
+
16
+ interface ConversationInputForwarderOptions {
17
+ readonly getInputRemainder: () => string;
18
+ readonly setInputRemainder: (next: string) => void;
19
+ readonly getMainPaneMode: () => MainPaneMode;
20
+ readonly getLayout: () => DualPaneLayout;
21
+ readonly inputTokenRouter: Pick<InputTokenRouter, 'routeTokens'>;
22
+ readonly getActiveConversation: () => ConversationState | null;
23
+ readonly markDirty: () => void;
24
+ readonly isControlledByLocalHuman: (input: {
25
+ readonly conversation: ConversationState;
26
+ readonly controllerId: string;
27
+ }) => boolean;
28
+ readonly controllerId: string;
29
+ readonly sendInputToSession: (sessionId: string, chunk: Buffer) => void;
30
+ readonly noteGitActivity: (directoryId: string | null) => void;
31
+ }
32
+
33
+ interface ConversationInputForwarderDependencies {
34
+ readonly parseMuxInputChunk?: typeof parseMuxInputChunkFrame;
35
+ readonly routeInputTokensForConversation?: typeof routeInputTokensForConversationFrame;
36
+ readonly classifyPaneAt?: typeof classifyPaneAtFrame;
37
+ readonly normalizeMuxKeyboardInputForPty?: typeof normalizeMuxKeyboardInputForPtyFrame;
38
+ }
39
+
40
+ export class ConversationInputForwarder {
41
+ private readonly parseMuxInputChunk: typeof parseMuxInputChunkFrame;
42
+ private readonly routeInputTokensForConversation: typeof routeInputTokensForConversationFrame;
43
+ private readonly classifyPaneAt: typeof classifyPaneAtFrame;
44
+ private readonly normalizeMuxKeyboardInputForPty: typeof normalizeMuxKeyboardInputForPtyFrame;
45
+
46
+ constructor(
47
+ private readonly options: ConversationInputForwarderOptions,
48
+ dependencies: ConversationInputForwarderDependencies = {},
49
+ ) {
50
+ this.parseMuxInputChunk = dependencies.parseMuxInputChunk ?? parseMuxInputChunkFrame;
51
+ this.routeInputTokensForConversation =
52
+ dependencies.routeInputTokensForConversation ?? routeInputTokensForConversationFrame;
53
+ this.classifyPaneAt = dependencies.classifyPaneAt ?? classifyPaneAtFrame;
54
+ this.normalizeMuxKeyboardInputForPty =
55
+ dependencies.normalizeMuxKeyboardInputForPty ?? normalizeMuxKeyboardInputForPtyFrame;
56
+ }
57
+
58
+ handleInput(input: Buffer): void {
59
+ const parsed = this.parseMuxInputChunk(this.options.getInputRemainder(), input);
60
+ this.options.setInputRemainder(parsed.remainder);
61
+
62
+ const layout = this.options.getLayout();
63
+ const inputConversation = this.options.getActiveConversation();
64
+ const { routedTokens, snapshotForInput } = this.options.inputTokenRouter.routeTokens({
65
+ tokens: parsed.tokens,
66
+ layout,
67
+ conversation: inputConversation,
68
+ snapshotForInput:
69
+ inputConversation === null ? null : inputConversation.oracle.snapshotWithoutHash(),
70
+ });
71
+
72
+ const { mainPaneScrollRows, forwardToSession } = this.routeInputTokensForConversation({
73
+ tokens: routedTokens,
74
+ mainPaneMode: this.options.getMainPaneMode(),
75
+ normalizeMuxKeyboardInputForPty: this.normalizeMuxKeyboardInputForPty,
76
+ classifyPaneAt: (col, row) => this.classifyPaneAt(layout, col, row),
77
+ wheelDeltaRowsFromCode,
78
+ hasShiftModifier,
79
+ layout: {
80
+ paneRows: layout.paneRows,
81
+ rightCols: layout.rightCols,
82
+ rightStartCol: layout.rightStartCol,
83
+ },
84
+ snapshotForInput,
85
+ appMouseTrackingEnabled:
86
+ inputConversation === null ? false : inputConversation.oracle.isMouseTrackingEnabled(),
87
+ });
88
+
89
+ if (mainPaneScrollRows !== 0 && inputConversation !== null) {
90
+ inputConversation.oracle.scrollViewport(mainPaneScrollRows);
91
+ this.options.markDirty();
92
+ }
93
+
94
+ if (inputConversation === null) {
95
+ return;
96
+ }
97
+ if (
98
+ inputConversation.controller !== null &&
99
+ !this.options.isControlledByLocalHuman({
100
+ conversation: inputConversation,
101
+ controllerId: this.options.controllerId,
102
+ })
103
+ ) {
104
+ return;
105
+ }
106
+
107
+ for (const forwardChunk of forwardToSession) {
108
+ this.options.sendInputToSession(inputConversation.sessionId, forwardChunk);
109
+ }
110
+ if (forwardToSession.length > 0) {
111
+ this.options.noteGitActivity(inputConversation.directoryId);
112
+ }
113
+ }
114
+ }
@@ -0,0 +1,103 @@
1
+ import {
2
+ hasAltModifier,
3
+ isLeftButtonPress,
4
+ isMouseRelease,
5
+ isSelectionDrag,
6
+ isWheelMouseCode,
7
+ pointFromMouseEvent as pointFromMouseEventFrame,
8
+ reduceConversationMouseSelection as reduceConversationMouseSelectionFrame,
9
+ selectionText as selectionTextFrame,
10
+ type PaneSelection,
11
+ type PaneSelectionDrag,
12
+ type SelectionLayout,
13
+ } from '../mux/live-mux/selection.ts';
14
+ import type { TerminalSnapshotFrameCore } from '../terminal/snapshot-oracle.ts';
15
+
16
+ interface ConversationSelectionInputOptions {
17
+ readonly getSelection: () => PaneSelection | null;
18
+ readonly setSelection: (next: PaneSelection | null) => void;
19
+ readonly getSelectionDrag: () => PaneSelectionDrag | null;
20
+ readonly setSelectionDrag: (next: PaneSelectionDrag | null) => void;
21
+ readonly pinViewportForSelection: () => void;
22
+ readonly releaseViewportPinForSelection: () => void;
23
+ readonly markDirty: () => void;
24
+ }
25
+
26
+ interface ConversationSelectionInputDependencies {
27
+ readonly pointFromMouseEvent?: typeof pointFromMouseEventFrame;
28
+ readonly reduceConversationMouseSelection?: typeof reduceConversationMouseSelectionFrame;
29
+ readonly selectionText?: typeof selectionTextFrame;
30
+ }
31
+
32
+ interface MouseSelectionInput {
33
+ readonly layout: SelectionLayout;
34
+ readonly frame: TerminalSnapshotFrameCore;
35
+ readonly isMainPaneTarget: boolean;
36
+ readonly resolveSelectionText?: (selection: PaneSelection) => string;
37
+ readonly event: {
38
+ readonly col: number;
39
+ readonly row: number;
40
+ readonly code: number;
41
+ readonly final: 'M' | 'm';
42
+ };
43
+ }
44
+
45
+ export class ConversationSelectionInput {
46
+ private readonly pointFromMouseEvent: typeof pointFromMouseEventFrame;
47
+ private readonly reduceConversationMouseSelection: typeof reduceConversationMouseSelectionFrame;
48
+ private readonly selectionText: typeof selectionTextFrame;
49
+
50
+ constructor(
51
+ private readonly options: ConversationSelectionInputOptions,
52
+ dependencies: ConversationSelectionInputDependencies = {},
53
+ ) {
54
+ this.pointFromMouseEvent = dependencies.pointFromMouseEvent ?? pointFromMouseEventFrame;
55
+ this.reduceConversationMouseSelection =
56
+ dependencies.reduceConversationMouseSelection ?? reduceConversationMouseSelectionFrame;
57
+ this.selectionText = dependencies.selectionText ?? selectionTextFrame;
58
+ }
59
+
60
+ clearSelectionOnTextToken(textLength: number): boolean {
61
+ const hasSelection =
62
+ this.options.getSelection() !== null || this.options.getSelectionDrag() !== null;
63
+ if (textLength <= 0 || !hasSelection) {
64
+ return false;
65
+ }
66
+ this.options.setSelection(null);
67
+ this.options.setSelectionDrag(null);
68
+ this.options.releaseViewportPinForSelection();
69
+ this.options.markDirty();
70
+ return true;
71
+ }
72
+
73
+ handleMouseSelection(input: MouseSelectionInput): boolean {
74
+ const reduced = this.reduceConversationMouseSelection({
75
+ selection: this.options.getSelection(),
76
+ selectionDrag: this.options.getSelectionDrag(),
77
+ point: this.pointFromMouseEvent(input.layout, input.frame, input.event),
78
+ isMainPaneTarget: input.isMainPaneTarget,
79
+ isLeftButtonPress:
80
+ isLeftButtonPress(input.event.code, input.event.final) && !hasAltModifier(input.event.code),
81
+ isSelectionDrag:
82
+ isSelectionDrag(input.event.code, input.event.final) && !hasAltModifier(input.event.code),
83
+ isMouseRelease: isMouseRelease(input.event.final),
84
+ isWheelMouseCode: isWheelMouseCode(input.event.code),
85
+ selectionTextForPane: (nextSelection) =>
86
+ input.resolveSelectionText === undefined
87
+ ? this.selectionText(input.frame, nextSelection)
88
+ : input.resolveSelectionText(nextSelection),
89
+ });
90
+ this.options.setSelection(reduced.selection);
91
+ this.options.setSelectionDrag(reduced.selectionDrag);
92
+ if (reduced.pinViewport) {
93
+ this.options.pinViewportForSelection();
94
+ }
95
+ if (reduced.releaseViewportPin) {
96
+ this.options.releaseViewportPinForSelection();
97
+ }
98
+ if (reduced.markDirty) {
99
+ this.options.markDirty();
100
+ }
101
+ return reduced.consumed;
102
+ }
103
+ }
@@ -0,0 +1,39 @@
1
+ interface DebugFooterNoticeOptions {
2
+ readonly ttlMs: number;
3
+ readonly nowMs?: () => number;
4
+ }
5
+
6
+ export class DebugFooterNotice {
7
+ private readonly nowMs: () => number;
8
+ private currentNotice: {
9
+ text: string;
10
+ expiresAtMs: number;
11
+ } | null = null;
12
+
13
+ constructor(private readonly options: DebugFooterNoticeOptions) {
14
+ this.nowMs = options.nowMs ?? Date.now;
15
+ }
16
+
17
+ set(text: string): void {
18
+ const normalized = text.trim();
19
+ if (normalized.length === 0) {
20
+ this.currentNotice = null;
21
+ return;
22
+ }
23
+ this.currentNotice = {
24
+ text: normalized,
25
+ expiresAtMs: this.nowMs() + this.options.ttlMs,
26
+ };
27
+ }
28
+
29
+ current(): string | null {
30
+ if (this.currentNotice === null) {
31
+ return null;
32
+ }
33
+ if (this.nowMs() > this.currentNotice.expiresAtMs) {
34
+ this.currentNotice = null;
35
+ return null;
36
+ }
37
+ return this.currentNotice.text;
38
+ }
39
+ }
@@ -0,0 +1,126 @@
1
+ import {
2
+ detectMuxGlobalShortcut as detectMuxGlobalShortcutFrame,
3
+ type resolveMuxShortcutBindings,
4
+ } from '../mux/input-shortcuts.ts';
5
+ import { handleGlobalShortcut as handleGlobalShortcutFrame } from '../mux/live-mux/global-shortcut-handlers.ts';
6
+
7
+ type ResolvedMuxShortcutBindings = ReturnType<typeof resolveMuxShortcutBindings>;
8
+ type ShortcutCycleDirection = 'next' | 'previous';
9
+ type MainPaneMode = 'conversation' | 'project' | 'home';
10
+ const DEFAULT_INTERRUPT_ALL_DOUBLE_TAP_WINDOW_MS = 350;
11
+
12
+ interface GlobalShortcutInputOptions {
13
+ readonly shortcutBindings: ResolvedMuxShortcutBindings;
14
+ readonly requestStop: () => void;
15
+ readonly resolveDirectoryForAction: () => string | null;
16
+ readonly openNewThreadPrompt: (directoryId: string) => void;
17
+ readonly toggleCommandMenu: () => void;
18
+ readonly openOrCreateCritiqueConversationInDirectory: (directoryId: string) => Promise<void>;
19
+ readonly toggleGatewayProfile: () => Promise<void>;
20
+ readonly toggleGatewayStatusTimeline: () => Promise<void>;
21
+ readonly toggleGatewayRenderTrace: (conversationId: string | null) => Promise<void>;
22
+ readonly getMainPaneMode: () => MainPaneMode;
23
+ readonly getActiveConversationId: () => string | null;
24
+ readonly conversationsHas: (sessionId: string) => boolean;
25
+ readonly queueControlPlaneOp: (task: () => Promise<void>, label: string) => void;
26
+ readonly archiveConversation: (sessionId: string) => Promise<void>;
27
+ readonly interruptConversation: (sessionId: string) => Promise<void>;
28
+ readonly takeoverConversation: (sessionId: string) => Promise<void>;
29
+ readonly openAddDirectoryPrompt: () => void;
30
+ readonly getActiveDirectoryId: () => string | null;
31
+ readonly directoryExists: (directoryId: string) => boolean;
32
+ readonly closeDirectory: (directoryId: string) => Promise<void>;
33
+ readonly cycleLeftNavSelection: (direction: ShortcutCycleDirection) => void;
34
+ readonly forwardInterruptAllToActiveConversation?: (input: Buffer) => boolean;
35
+ readonly interruptAllDoubleTapWindowMs?: number;
36
+ readonly nowMs?: () => number;
37
+ }
38
+
39
+ interface GlobalShortcutInputDependencies {
40
+ readonly detectMuxGlobalShortcut?: typeof detectMuxGlobalShortcutFrame;
41
+ readonly handleGlobalShortcut?: typeof handleGlobalShortcutFrame;
42
+ }
43
+
44
+ export class GlobalShortcutInput {
45
+ private readonly detectMuxGlobalShortcut: typeof detectMuxGlobalShortcutFrame;
46
+ private readonly handleGlobalShortcut: typeof handleGlobalShortcutFrame;
47
+ private readonly nowMs: () => number;
48
+ private readonly interruptAllDoubleTapWindowMs: number | null;
49
+ private lastInterruptAllAtMs: number | null = null;
50
+
51
+ constructor(
52
+ private readonly options: GlobalShortcutInputOptions,
53
+ dependencies: GlobalShortcutInputDependencies = {},
54
+ ) {
55
+ this.detectMuxGlobalShortcut =
56
+ dependencies.detectMuxGlobalShortcut ?? detectMuxGlobalShortcutFrame;
57
+ this.handleGlobalShortcut = dependencies.handleGlobalShortcut ?? handleGlobalShortcutFrame;
58
+ this.nowMs = this.options.nowMs ?? (() => Date.now());
59
+ const customInterruptAllBehaviorEnabled =
60
+ this.options.forwardInterruptAllToActiveConversation !== undefined ||
61
+ this.options.interruptAllDoubleTapWindowMs !== undefined;
62
+ this.interruptAllDoubleTapWindowMs = customInterruptAllBehaviorEnabled
63
+ ? (this.options.interruptAllDoubleTapWindowMs ?? DEFAULT_INTERRUPT_ALL_DOUBLE_TAP_WINDOW_MS)
64
+ : null;
65
+ }
66
+
67
+ handleInput(input: Buffer): boolean {
68
+ const shortcut = this.detectMuxGlobalShortcut(input, this.options.shortcutBindings);
69
+ if (shortcut === 'mux.app.interrupt-all' && this.interruptAllDoubleTapWindowMs !== null) {
70
+ return this.handleInterruptAllShortcut(input);
71
+ }
72
+ if (shortcut !== 'mux.app.interrupt-all') {
73
+ this.lastInterruptAllAtMs = null;
74
+ }
75
+ return this.handleGlobalShortcut({
76
+ shortcut,
77
+ requestStop: this.options.requestStop,
78
+ resolveDirectoryForAction: this.options.resolveDirectoryForAction,
79
+ openNewThreadPrompt: this.options.openNewThreadPrompt,
80
+ toggleCommandMenu: this.options.toggleCommandMenu,
81
+ openOrCreateCritiqueConversationInDirectory:
82
+ this.options.openOrCreateCritiqueConversationInDirectory,
83
+ toggleGatewayProfile: this.options.toggleGatewayProfile,
84
+ toggleGatewayStatusTimeline: this.options.toggleGatewayStatusTimeline,
85
+ toggleGatewayRenderTrace: this.options.toggleGatewayRenderTrace,
86
+ resolveConversationForAction: () =>
87
+ this.options.getMainPaneMode() === 'conversation'
88
+ ? this.options.getActiveConversationId()
89
+ : null,
90
+ conversationsHas: this.options.conversationsHas,
91
+ queueControlPlaneOp: this.options.queueControlPlaneOp,
92
+ archiveConversation: this.options.archiveConversation,
93
+ interruptConversation: this.options.interruptConversation,
94
+ takeoverConversation: this.options.takeoverConversation,
95
+ openAddDirectoryPrompt: this.options.openAddDirectoryPrompt,
96
+ resolveClosableDirectoryId: () => {
97
+ const activeDirectoryId = this.options.getActiveDirectoryId();
98
+ if (this.options.getMainPaneMode() !== 'project' || activeDirectoryId === null) {
99
+ return null;
100
+ }
101
+ return this.options.directoryExists(activeDirectoryId) ? activeDirectoryId : null;
102
+ },
103
+ closeDirectory: this.options.closeDirectory,
104
+ cycleLeftNavSelection: this.options.cycleLeftNavSelection,
105
+ });
106
+ }
107
+
108
+ private handleInterruptAllShortcut(input: Buffer): boolean {
109
+ const nowMs = this.nowMs();
110
+ const lastInterruptAllAtMs = this.lastInterruptAllAtMs;
111
+ const doubleTapWindowMs = this.interruptAllDoubleTapWindowMs;
112
+ if (
113
+ doubleTapWindowMs !== null &&
114
+ lastInterruptAllAtMs !== null &&
115
+ nowMs >= lastInterruptAllAtMs &&
116
+ nowMs - lastInterruptAllAtMs <= doubleTapWindowMs
117
+ ) {
118
+ this.lastInterruptAllAtMs = null;
119
+ this.options.requestStop();
120
+ return true;
121
+ }
122
+ this.lastInterruptAllAtMs = nowMs;
123
+ this.options.forwardInterruptAllToActiveConversation?.(input);
124
+ return true;
125
+ }
126
+ }
@@ -0,0 +1,68 @@
1
+ import { extractFocusEvents as extractFocusEventsFrame } from '../mux/live-mux/startup-utils.ts';
2
+
3
+ interface InputPreflightOptions {
4
+ readonly isShuttingDown: () => boolean;
5
+ readonly routeModalInput: (input: Buffer) => boolean;
6
+ readonly handleEscapeInput: (input: Buffer) => void;
7
+ readonly onFocusIn: () => void;
8
+ readonly onFocusOut: () => void;
9
+ readonly handleRepositoryFoldInput: (input: Buffer) => boolean;
10
+ readonly handleGlobalShortcutInput: (input: Buffer) => boolean;
11
+ readonly handleTaskPaneShortcutInput: (input: Buffer) => boolean;
12
+ readonly handleCopyShortcutInput: (input: Buffer) => boolean;
13
+ }
14
+
15
+ interface InputPreflightDependencies {
16
+ readonly extractFocusEvents?: typeof extractFocusEventsFrame;
17
+ }
18
+
19
+ export class InputPreflight {
20
+ private readonly extractFocusEvents: typeof extractFocusEventsFrame;
21
+
22
+ constructor(
23
+ private readonly options: InputPreflightOptions,
24
+ dependencies: InputPreflightDependencies = {},
25
+ ) {
26
+ this.extractFocusEvents = dependencies.extractFocusEvents ?? extractFocusEventsFrame;
27
+ }
28
+
29
+ nextInput(input: Buffer): Buffer | null {
30
+ if (this.options.isShuttingDown()) {
31
+ return null;
32
+ }
33
+ if (this.options.routeModalInput(input)) {
34
+ return null;
35
+ }
36
+
37
+ if (input.length === 1 && input[0] === 0x1b) {
38
+ this.options.handleEscapeInput(input);
39
+ return null;
40
+ }
41
+
42
+ const focusExtraction = this.extractFocusEvents(input);
43
+ if (focusExtraction.focusInCount > 0) {
44
+ this.options.onFocusIn();
45
+ }
46
+ if (focusExtraction.focusOutCount > 0) {
47
+ this.options.onFocusOut();
48
+ }
49
+ if (focusExtraction.sanitized.length === 0) {
50
+ return null;
51
+ }
52
+
53
+ if (this.options.handleRepositoryFoldInput(focusExtraction.sanitized)) {
54
+ return null;
55
+ }
56
+ if (this.options.handleGlobalShortcutInput(focusExtraction.sanitized)) {
57
+ return null;
58
+ }
59
+ if (this.options.handleTaskPaneShortcutInput(focusExtraction.sanitized)) {
60
+ return null;
61
+ }
62
+ if (this.options.handleCopyShortcutInput(focusExtraction.sanitized)) {
63
+ return null;
64
+ }
65
+
66
+ return focusExtraction.sanitized;
67
+ }
68
+ }