@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,143 @@
1
+ type ShortcutCycleDirection = 'next' | 'previous';
2
+
3
+ interface HandleGlobalShortcutOptions {
4
+ shortcut: string | null;
5
+ requestStop: () => void;
6
+ resolveDirectoryForAction: () => string | null;
7
+ openNewThreadPrompt: (directoryId: string) => void;
8
+ toggleCommandMenu: () => void;
9
+ openOrCreateCritiqueConversationInDirectory: (directoryId: string) => Promise<void>;
10
+ toggleGatewayProfile: () => Promise<void>;
11
+ toggleGatewayStatusTimeline: () => Promise<void>;
12
+ toggleGatewayRenderTrace: (conversationId: string | null) => Promise<void>;
13
+ resolveConversationForAction: () => string | null;
14
+ conversationsHas: (sessionId: string) => boolean;
15
+ queueControlPlaneOp: (task: () => Promise<void>, label: string) => void;
16
+ archiveConversation: (sessionId: string) => Promise<void>;
17
+ interruptConversation: (sessionId: string) => Promise<void>;
18
+ takeoverConversation: (sessionId: string) => Promise<void>;
19
+ openAddDirectoryPrompt: () => void;
20
+ resolveClosableDirectoryId: () => string | null;
21
+ closeDirectory: (directoryId: string) => Promise<void>;
22
+ cycleLeftNavSelection: (direction: ShortcutCycleDirection) => void;
23
+ }
24
+
25
+ export function handleGlobalShortcut(options: HandleGlobalShortcutOptions): boolean {
26
+ const {
27
+ shortcut,
28
+ requestStop,
29
+ resolveDirectoryForAction,
30
+ openNewThreadPrompt,
31
+ toggleCommandMenu,
32
+ openOrCreateCritiqueConversationInDirectory,
33
+ toggleGatewayProfile,
34
+ toggleGatewayStatusTimeline,
35
+ toggleGatewayRenderTrace,
36
+ resolveConversationForAction,
37
+ conversationsHas,
38
+ queueControlPlaneOp,
39
+ archiveConversation,
40
+ interruptConversation,
41
+ takeoverConversation,
42
+ openAddDirectoryPrompt,
43
+ resolveClosableDirectoryId,
44
+ closeDirectory,
45
+ cycleLeftNavSelection,
46
+ } = options;
47
+ if (shortcut === null) {
48
+ return false;
49
+ }
50
+ if (shortcut === 'mux.app.interrupt-all' || shortcut === 'mux.app.quit') {
51
+ requestStop();
52
+ return true;
53
+ }
54
+ if (shortcut === 'mux.conversation.new') {
55
+ const targetDirectoryId = resolveDirectoryForAction();
56
+ if (targetDirectoryId !== null) {
57
+ openNewThreadPrompt(targetDirectoryId);
58
+ }
59
+ return true;
60
+ }
61
+ if (shortcut === 'mux.command-menu.toggle') {
62
+ toggleCommandMenu();
63
+ return true;
64
+ }
65
+ if (shortcut === 'mux.conversation.critique.open-or-create') {
66
+ const targetDirectoryId = resolveDirectoryForAction();
67
+ if (targetDirectoryId !== null) {
68
+ queueControlPlaneOp(async () => {
69
+ await openOrCreateCritiqueConversationInDirectory(targetDirectoryId);
70
+ }, 'shortcut-open-or-create-critique-conversation');
71
+ }
72
+ return true;
73
+ }
74
+ if (shortcut === 'mux.gateway.profile.toggle') {
75
+ queueControlPlaneOp(async () => {
76
+ await toggleGatewayProfile();
77
+ }, 'shortcut-toggle-gateway-profile');
78
+ return true;
79
+ }
80
+ if (shortcut === 'mux.gateway.status-timeline.toggle') {
81
+ queueControlPlaneOp(async () => {
82
+ await toggleGatewayStatusTimeline();
83
+ }, 'shortcut-toggle-gateway-status-timeline');
84
+ return true;
85
+ }
86
+ if (shortcut === 'mux.gateway.render-trace.toggle') {
87
+ const targetConversationId = resolveConversationForAction();
88
+ queueControlPlaneOp(async () => {
89
+ await toggleGatewayRenderTrace(targetConversationId);
90
+ }, 'shortcut-toggle-gateway-render-trace');
91
+ return true;
92
+ }
93
+ if (shortcut === 'mux.conversation.archive' || shortcut === 'mux.conversation.delete') {
94
+ const targetConversationId = resolveConversationForAction();
95
+ if (targetConversationId !== null && conversationsHas(targetConversationId)) {
96
+ queueControlPlaneOp(
97
+ async () => {
98
+ await archiveConversation(targetConversationId);
99
+ },
100
+ shortcut === 'mux.conversation.archive'
101
+ ? 'shortcut-archive-conversation'
102
+ : 'shortcut-delete-conversation',
103
+ );
104
+ }
105
+ return true;
106
+ }
107
+ if (shortcut === 'mux.conversation.interrupt') {
108
+ const targetConversationId = resolveConversationForAction();
109
+ if (targetConversationId !== null && conversationsHas(targetConversationId)) {
110
+ queueControlPlaneOp(async () => {
111
+ await interruptConversation(targetConversationId);
112
+ }, 'shortcut-interrupt-conversation');
113
+ }
114
+ return true;
115
+ }
116
+ if (shortcut === 'mux.conversation.takeover') {
117
+ const targetConversationId = resolveConversationForAction();
118
+ if (targetConversationId !== null && conversationsHas(targetConversationId)) {
119
+ queueControlPlaneOp(async () => {
120
+ await takeoverConversation(targetConversationId);
121
+ }, 'shortcut-takeover-conversation');
122
+ }
123
+ return true;
124
+ }
125
+ if (shortcut === 'mux.directory.add') {
126
+ openAddDirectoryPrompt();
127
+ return true;
128
+ }
129
+ if (shortcut === 'mux.directory.close') {
130
+ const targetDirectoryId = resolveClosableDirectoryId();
131
+ if (targetDirectoryId !== null) {
132
+ queueControlPlaneOp(async () => {
133
+ await closeDirectory(targetDirectoryId);
134
+ }, 'shortcut-close-directory');
135
+ }
136
+ return true;
137
+ }
138
+ if (shortcut === 'mux.conversation.next' || shortcut === 'mux.conversation.previous') {
139
+ cycleLeftNavSelection(shortcut === 'mux.conversation.next' ? 'next' : 'previous');
140
+ return true;
141
+ }
142
+ return false;
143
+ }
@@ -0,0 +1,58 @@
1
+ interface HandleHomePaneActionClickOptions {
2
+ action: string | null;
3
+ rowIndex: number;
4
+ clearTaskEditClickState: () => void;
5
+ clearRepositoryEditClickState: () => void;
6
+ clearHomePaneDragState: () => void;
7
+ getTaskRepositoryDropdownOpen: () => boolean;
8
+ setTaskRepositoryDropdownOpen: (open: boolean) => void;
9
+ taskIdAtRow: (rowIndex: number) => string | null;
10
+ repositoryIdAtRow: (rowIndex: number) => string | null;
11
+ selectTaskById: (taskId: string) => void;
12
+ selectRepositoryById: (repositoryId: string) => void;
13
+ runTaskPaneAction: (action: 'task.ready' | 'task.draft' | 'task.complete') => void;
14
+ markDirty: () => void;
15
+ }
16
+
17
+ export function handleHomePaneActionClick(options: HandleHomePaneActionClickOptions): boolean {
18
+ if (options.action === null) {
19
+ return false;
20
+ }
21
+ options.clearTaskEditClickState();
22
+ options.clearRepositoryEditClickState();
23
+ options.clearHomePaneDragState();
24
+
25
+ if (options.action === 'repository.dropdown.toggle') {
26
+ options.setTaskRepositoryDropdownOpen(!options.getTaskRepositoryDropdownOpen());
27
+ } else if (options.action === 'repository.select') {
28
+ const repositoryId = options.repositoryIdAtRow(options.rowIndex);
29
+ if (repositoryId !== null) {
30
+ options.selectRepositoryById(repositoryId);
31
+ }
32
+ } else if (options.action === 'task.focus') {
33
+ const taskId = options.taskIdAtRow(options.rowIndex);
34
+ if (taskId !== null) {
35
+ options.selectTaskById(taskId);
36
+ }
37
+ } else if (options.action === 'task.status.ready') {
38
+ const taskId = options.taskIdAtRow(options.rowIndex);
39
+ if (taskId !== null) {
40
+ options.selectTaskById(taskId);
41
+ options.runTaskPaneAction('task.ready');
42
+ }
43
+ } else if (options.action === 'task.status.draft') {
44
+ const taskId = options.taskIdAtRow(options.rowIndex);
45
+ if (taskId !== null) {
46
+ options.selectTaskById(taskId);
47
+ options.runTaskPaneAction('task.draft');
48
+ }
49
+ } else if (options.action === 'task.status.complete') {
50
+ const taskId = options.taskIdAtRow(options.rowIndex);
51
+ if (taskId !== null) {
52
+ options.selectTaskById(taskId);
53
+ options.runTaskPaneAction('task.complete');
54
+ }
55
+ }
56
+ options.markDirty();
57
+ return true;
58
+ }
@@ -0,0 +1,44 @@
1
+ interface HomePaneDragState {
2
+ readonly kind: 'task' | 'repository';
3
+ readonly itemId: string;
4
+ readonly startedRowIndex: number;
5
+ readonly latestRowIndex: number;
6
+ readonly hasDragged: boolean;
7
+ }
8
+
9
+ interface HandleHomePaneDragReleaseOptions {
10
+ homePaneDragState: HomePaneDragState | null;
11
+ isMouseRelease: boolean;
12
+ mainPaneMode: 'conversation' | 'project' | 'home';
13
+ target: string;
14
+ rowIndex: number;
15
+ taskIdAtRow: (rowIndex: number) => string | null;
16
+ repositoryIdAtRow: (rowIndex: number) => string | null;
17
+ reorderTaskByDrop: (draggedTaskId: string, targetTaskId: string) => void;
18
+ reorderRepositoryByDrop: (draggedRepositoryId: string, targetRepositoryId: string) => void;
19
+ setHomePaneDragState: (next: HomePaneDragState | null) => void;
20
+ markDirty: () => void;
21
+ }
22
+
23
+ export function handleHomePaneDragRelease(options: HandleHomePaneDragReleaseOptions): boolean {
24
+ if (options.homePaneDragState === null || !options.isMouseRelease) {
25
+ return false;
26
+ }
27
+ const drag = options.homePaneDragState;
28
+ options.setHomePaneDragState(null);
29
+ if (options.mainPaneMode === 'home' && options.target === 'right' && drag.hasDragged) {
30
+ if (drag.kind === 'task') {
31
+ const targetTaskId = options.taskIdAtRow(options.rowIndex);
32
+ if (targetTaskId !== null) {
33
+ options.reorderTaskByDrop(drag.itemId, targetTaskId);
34
+ }
35
+ } else {
36
+ const targetRepositoryId = options.repositoryIdAtRow(options.rowIndex);
37
+ if (targetRepositoryId !== null) {
38
+ options.reorderRepositoryByDrop(drag.itemId, targetRepositoryId);
39
+ }
40
+ }
41
+ }
42
+ options.markDirty();
43
+ return true;
44
+ }
@@ -0,0 +1,96 @@
1
+ import { detectEntityDoubleClick } from '../double-click.ts';
2
+
3
+ interface EntityDoubleClickState {
4
+ readonly entityId: string;
5
+ readonly atMs: number;
6
+ }
7
+
8
+ interface HomePaneDragState {
9
+ readonly kind: 'task' | 'repository';
10
+ readonly itemId: string;
11
+ readonly startedRowIndex: number;
12
+ readonly latestRowIndex: number;
13
+ readonly hasDragged: boolean;
14
+ }
15
+
16
+ interface HandleHomePaneEntityClickOptions {
17
+ rowIndex: number;
18
+ nowMs: number;
19
+ homePaneEditDoubleClickWindowMs: number;
20
+ taskEditClickState: EntityDoubleClickState | null;
21
+ repositoryEditClickState: EntityDoubleClickState | null;
22
+ taskIdAtRow: (rowIndex: number) => string | null;
23
+ repositoryIdAtRow: (rowIndex: number) => string | null;
24
+ selectTaskById: (taskId: string) => void;
25
+ selectRepositoryById: (repositoryId: string) => void;
26
+ clearTaskPaneNotice: () => void;
27
+ setTaskEditClickState: (next: EntityDoubleClickState | null) => void;
28
+ setRepositoryEditClickState: (next: EntityDoubleClickState | null) => void;
29
+ setHomePaneDragState: (next: HomePaneDragState | null) => void;
30
+ openTaskEditPrompt: (taskId: string) => void;
31
+ openRepositoryPromptForEdit: (repositoryId: string) => void;
32
+ markDirty: () => void;
33
+ }
34
+
35
+ export function handleHomePaneEntityClick(options: HandleHomePaneEntityClickOptions): boolean {
36
+ const taskId = options.taskIdAtRow(options.rowIndex);
37
+ if (taskId !== null) {
38
+ const click = detectEntityDoubleClick(
39
+ options.taskEditClickState,
40
+ taskId,
41
+ options.nowMs,
42
+ options.homePaneEditDoubleClickWindowMs,
43
+ );
44
+ options.selectTaskById(taskId);
45
+ options.clearTaskPaneNotice();
46
+ options.setTaskEditClickState(click.nextState);
47
+ options.setRepositoryEditClickState(null);
48
+ if (click.doubleClick) {
49
+ options.setHomePaneDragState(null);
50
+ options.openTaskEditPrompt(taskId);
51
+ } else {
52
+ options.setHomePaneDragState({
53
+ kind: 'task',
54
+ itemId: taskId,
55
+ startedRowIndex: options.rowIndex,
56
+ latestRowIndex: options.rowIndex,
57
+ hasDragged: false,
58
+ });
59
+ }
60
+ options.markDirty();
61
+ return true;
62
+ }
63
+
64
+ const repositoryId = options.repositoryIdAtRow(options.rowIndex);
65
+ if (repositoryId !== null) {
66
+ const click = detectEntityDoubleClick(
67
+ options.repositoryEditClickState,
68
+ repositoryId,
69
+ options.nowMs,
70
+ options.homePaneEditDoubleClickWindowMs,
71
+ );
72
+ options.selectRepositoryById(repositoryId);
73
+ options.clearTaskPaneNotice();
74
+ options.setRepositoryEditClickState(click.nextState);
75
+ options.setTaskEditClickState(null);
76
+ if (click.doubleClick) {
77
+ options.setHomePaneDragState(null);
78
+ options.openRepositoryPromptForEdit(repositoryId);
79
+ } else {
80
+ options.setHomePaneDragState({
81
+ kind: 'repository',
82
+ itemId: repositoryId,
83
+ startedRowIndex: options.rowIndex,
84
+ latestRowIndex: options.rowIndex,
85
+ hasDragged: false,
86
+ });
87
+ }
88
+ options.markDirty();
89
+ return true;
90
+ }
91
+
92
+ options.setTaskEditClickState(null);
93
+ options.setRepositoryEditClickState(null);
94
+ options.setHomePaneDragState(null);
95
+ return false;
96
+ }
@@ -0,0 +1,96 @@
1
+ import { handleHomePaneActionClick } from './home-pane-actions.ts';
2
+ import { handleHomePaneEntityClick } from './home-pane-entity-click.ts';
3
+
4
+ interface EntityDoubleClickState {
5
+ readonly entityId: string;
6
+ readonly atMs: number;
7
+ }
8
+
9
+ interface HomePaneDragState {
10
+ readonly kind: 'task' | 'repository';
11
+ readonly itemId: string;
12
+ readonly startedRowIndex: number;
13
+ readonly latestRowIndex: number;
14
+ readonly hasDragged: boolean;
15
+ }
16
+
17
+ interface HandleHomePanePointerClickOptions {
18
+ clickEligible: boolean;
19
+ paneRows: number;
20
+ rightCols: number;
21
+ rightStartCol: number;
22
+ pointerRow: number;
23
+ pointerCol: number;
24
+ actionAtCell: (rowIndex: number, colIndex: number) => string | null;
25
+ actionAtRow: (rowIndex: number) => string | null;
26
+ clearTaskEditClickState: () => void;
27
+ clearRepositoryEditClickState: () => void;
28
+ clearHomePaneDragState: () => void;
29
+ getTaskRepositoryDropdownOpen: () => boolean;
30
+ setTaskRepositoryDropdownOpen: (open: boolean) => void;
31
+ taskIdAtRow: (rowIndex: number) => string | null;
32
+ repositoryIdAtRow: (rowIndex: number) => string | null;
33
+ selectTaskById: (taskId: string) => void;
34
+ selectRepositoryById: (repositoryId: string) => void;
35
+ runTaskPaneAction: (action: 'task.ready' | 'task.draft' | 'task.complete') => void;
36
+ nowMs: number;
37
+ homePaneEditDoubleClickWindowMs: number;
38
+ taskEditClickState: EntityDoubleClickState | null;
39
+ repositoryEditClickState: EntityDoubleClickState | null;
40
+ clearTaskPaneNotice: () => void;
41
+ setTaskEditClickState: (next: EntityDoubleClickState | null) => void;
42
+ setRepositoryEditClickState: (next: EntityDoubleClickState | null) => void;
43
+ setHomePaneDragState: (next: HomePaneDragState | null) => void;
44
+ openTaskEditPrompt: (taskId: string) => void;
45
+ openRepositoryPromptForEdit: (repositoryId: string) => void;
46
+ markDirty: () => void;
47
+ }
48
+
49
+ export function handleHomePanePointerClick(options: HandleHomePanePointerClickOptions): boolean {
50
+ if (!options.clickEligible) {
51
+ return false;
52
+ }
53
+ const rowIndex = Math.max(0, Math.min(options.paneRows - 1, options.pointerRow - 1));
54
+ const colIndex = Math.max(
55
+ 0,
56
+ Math.min(options.rightCols - 1, options.pointerCol - options.rightStartCol),
57
+ );
58
+ const action = options.actionAtCell(rowIndex, colIndex) ?? options.actionAtRow(rowIndex);
59
+ if (
60
+ handleHomePaneActionClick({
61
+ action,
62
+ rowIndex,
63
+ clearTaskEditClickState: options.clearTaskEditClickState,
64
+ clearRepositoryEditClickState: options.clearRepositoryEditClickState,
65
+ clearHomePaneDragState: options.clearHomePaneDragState,
66
+ getTaskRepositoryDropdownOpen: options.getTaskRepositoryDropdownOpen,
67
+ setTaskRepositoryDropdownOpen: options.setTaskRepositoryDropdownOpen,
68
+ taskIdAtRow: options.taskIdAtRow,
69
+ repositoryIdAtRow: options.repositoryIdAtRow,
70
+ selectTaskById: options.selectTaskById,
71
+ selectRepositoryById: options.selectRepositoryById,
72
+ runTaskPaneAction: options.runTaskPaneAction,
73
+ markDirty: options.markDirty,
74
+ })
75
+ ) {
76
+ return true;
77
+ }
78
+ return handleHomePaneEntityClick({
79
+ rowIndex,
80
+ nowMs: options.nowMs,
81
+ homePaneEditDoubleClickWindowMs: options.homePaneEditDoubleClickWindowMs,
82
+ taskEditClickState: options.taskEditClickState,
83
+ repositoryEditClickState: options.repositoryEditClickState,
84
+ taskIdAtRow: options.taskIdAtRow,
85
+ repositoryIdAtRow: options.repositoryIdAtRow,
86
+ selectTaskById: options.selectTaskById,
87
+ selectRepositoryById: options.selectRepositoryById,
88
+ clearTaskPaneNotice: options.clearTaskPaneNotice,
89
+ setTaskEditClickState: options.setTaskEditClickState,
90
+ setRepositoryEditClickState: options.setRepositoryEditClickState,
91
+ setHomePaneDragState: options.setHomePaneDragState,
92
+ openTaskEditPrompt: options.openTaskEditPrompt,
93
+ openRepositoryPromptForEdit: options.openRepositoryPromptForEdit,
94
+ markDirty: options.markDirty,
95
+ });
96
+ }
@@ -0,0 +1,112 @@
1
+ import type { TerminalSnapshotFrameCore } from '../../terminal/snapshot-oracle.ts';
2
+
3
+ type RoutedInputToken =
4
+ | {
5
+ kind: 'passthrough';
6
+ text: string;
7
+ }
8
+ | {
9
+ kind: 'mouse';
10
+ event: {
11
+ col: number;
12
+ row: number;
13
+ code: number;
14
+ final: 'M' | 'm';
15
+ };
16
+ };
17
+
18
+ interface RouteInputTokensForConversationOptions {
19
+ tokens: readonly RoutedInputToken[];
20
+ mainPaneMode: 'conversation' | 'project' | 'home';
21
+ normalizeMuxKeyboardInputForPty: (input: Buffer) => Buffer;
22
+ classifyPaneAt: (col: number, row: number) => string;
23
+ wheelDeltaRowsFromCode: (code: number) => number | null;
24
+ hasShiftModifier: (code: number) => boolean;
25
+ layout: {
26
+ paneRows: number;
27
+ rightCols: number;
28
+ rightStartCol: number;
29
+ };
30
+ snapshotForInput: Pick<TerminalSnapshotFrameCore, 'activeScreen' | 'viewport'> | null;
31
+ appMouseTrackingEnabled: boolean;
32
+ }
33
+
34
+ interface RouteInputTokensForConversationResult {
35
+ readonly mainPaneScrollRows: number;
36
+ readonly forwardToSession: readonly Buffer[];
37
+ }
38
+
39
+ function encodeSgrMouseEvent(code: number, col: number, row: number, final: 'M' | 'm'): Buffer {
40
+ return Buffer.from(`\u001b[<${String(code)};${String(col)};${String(row)}${final}`, 'utf8');
41
+ }
42
+
43
+ function shouldPassThroughMouseToConversation(
44
+ options: Pick<
45
+ RouteInputTokensForConversationOptions,
46
+ 'snapshotForInput' | 'appMouseTrackingEnabled' | 'hasShiftModifier'
47
+ >,
48
+ code: number,
49
+ ): boolean {
50
+ if (options.snapshotForInput === null) {
51
+ return false;
52
+ }
53
+ if (!options.appMouseTrackingEnabled) {
54
+ return false;
55
+ }
56
+ if (options.snapshotForInput.activeScreen !== 'alternate') {
57
+ return false;
58
+ }
59
+ if (!options.snapshotForInput.viewport.followOutput) {
60
+ return false;
61
+ }
62
+ if (options.hasShiftModifier(code)) {
63
+ return false;
64
+ }
65
+ return true;
66
+ }
67
+
68
+ export function routeInputTokensForConversation(
69
+ options: RouteInputTokensForConversationOptions,
70
+ ): RouteInputTokensForConversationResult {
71
+ let mainPaneScrollRows = 0;
72
+ const forwardToSession: Buffer[] = [];
73
+ for (const token of options.tokens) {
74
+ if (token.kind === 'passthrough') {
75
+ if (options.mainPaneMode === 'conversation' && token.text.length > 0) {
76
+ forwardToSession.push(
77
+ options.normalizeMuxKeyboardInputForPty(Buffer.from(token.text, 'utf8')),
78
+ );
79
+ }
80
+ continue;
81
+ }
82
+ if (options.classifyPaneAt(token.event.col, token.event.row) !== 'right') {
83
+ continue;
84
+ }
85
+ if (options.mainPaneMode !== 'conversation') {
86
+ continue;
87
+ }
88
+ if (shouldPassThroughMouseToConversation(options, token.event.code)) {
89
+ const sessionCol = Math.max(
90
+ 1,
91
+ Math.min(options.layout.rightCols, token.event.col - options.layout.rightStartCol + 1),
92
+ );
93
+ const sessionRow = Math.max(1, Math.min(options.layout.paneRows, token.event.row));
94
+ forwardToSession.push(
95
+ encodeSgrMouseEvent(token.event.code, sessionCol, sessionRow, token.event.final),
96
+ );
97
+ continue;
98
+ }
99
+ const wheelDelta = options.wheelDeltaRowsFromCode(token.event.code);
100
+ if (wheelDelta !== null) {
101
+ mainPaneScrollRows += wheelDelta;
102
+ continue;
103
+ }
104
+ // The mux owns mouse interactions. Forwarding raw SGR mouse sequences to shell-style
105
+ // threads produces visible control garbage (for example on initial click-to-focus).
106
+ continue;
107
+ }
108
+ return {
109
+ mainPaneScrollRows,
110
+ forwardToSession,
111
+ };
112
+ }
@@ -0,0 +1,30 @@
1
+ const DEFAULT_PANE_WIDTH_PERCENT = 30;
2
+ const MIN_PANE_WIDTH_PERCENT = 1;
3
+ const MAX_PANE_WIDTH_PERCENT = 99;
4
+
5
+ export function normalizePaneWidthPercent(value: number): number {
6
+ if (!Number.isFinite(value)) {
7
+ return DEFAULT_PANE_WIDTH_PERCENT;
8
+ }
9
+ if (value < MIN_PANE_WIDTH_PERCENT) {
10
+ return MIN_PANE_WIDTH_PERCENT;
11
+ }
12
+ if (value > MAX_PANE_WIDTH_PERCENT) {
13
+ return MAX_PANE_WIDTH_PERCENT;
14
+ }
15
+ return value;
16
+ }
17
+
18
+ export function leftColsFromPaneWidthPercent(cols: number, paneWidthPercent: number): number {
19
+ const availablePaneCols = Math.max(2, cols - 1);
20
+ const normalizedPercent = normalizePaneWidthPercent(paneWidthPercent);
21
+ const requestedLeftCols = Math.round((availablePaneCols * normalizedPercent) / 100);
22
+ return Math.max(1, Math.min(availablePaneCols - 1, requestedLeftCols));
23
+ }
24
+
25
+ export function paneWidthPercentFromLayout(layout: { cols: number; leftCols: number }): number {
26
+ const availablePaneCols = Math.max(2, layout.cols - 1);
27
+ const percent = (layout.leftCols / availablePaneCols) * 100;
28
+ const rounded = Math.round(percent * 100) / 100;
29
+ return normalizePaneWidthPercent(rounded);
30
+ }
@@ -0,0 +1,103 @@
1
+ import { cycleConversationId } from '../conversation-rail.ts';
2
+ import { leftNavTargetKey, type LeftNavSelection } from './left-nav.ts';
3
+
4
+ interface ActivateLeftNavTargetOptions {
5
+ target: LeftNavSelection;
6
+ direction: 'next' | 'previous';
7
+ enterHomePane: () => void;
8
+ firstDirectoryForRepositoryGroup: (repositoryGroupId: string) => string | null;
9
+ enterProjectPane: (directoryId: string) => void;
10
+ setMainPaneProjectMode: () => void;
11
+ selectLeftNavRepository: (repositoryGroupId: string) => void;
12
+ markDirty: () => void;
13
+ directoriesHas: (directoryId: string) => boolean;
14
+ visibleTargetsForState: () => readonly LeftNavSelection[];
15
+ conversationDirectoryId: (sessionId: string) => string | null;
16
+ queueControlPlaneOp: (task: () => Promise<void>, label: string) => void;
17
+ activateConversation: (sessionId: string) => Promise<void>;
18
+ conversationsHas: (sessionId: string) => boolean;
19
+ }
20
+
21
+ export function activateLeftNavTarget(options: ActivateLeftNavTargetOptions): void {
22
+ const {
23
+ target,
24
+ direction,
25
+ enterHomePane,
26
+ firstDirectoryForRepositoryGroup,
27
+ enterProjectPane,
28
+ setMainPaneProjectMode,
29
+ selectLeftNavRepository,
30
+ markDirty,
31
+ directoriesHas,
32
+ visibleTargetsForState,
33
+ conversationDirectoryId,
34
+ queueControlPlaneOp,
35
+ activateConversation,
36
+ conversationsHas,
37
+ } = options;
38
+ if (target.kind === 'home') {
39
+ enterHomePane();
40
+ return;
41
+ }
42
+ if (target.kind === 'repository') {
43
+ const firstDirectoryId = firstDirectoryForRepositoryGroup(target.repositoryId);
44
+ if (firstDirectoryId !== null) {
45
+ enterProjectPane(firstDirectoryId);
46
+ } else {
47
+ setMainPaneProjectMode();
48
+ }
49
+ selectLeftNavRepository(target.repositoryId);
50
+ markDirty();
51
+ return;
52
+ }
53
+ if (target.kind === 'project') {
54
+ if (directoriesHas(target.directoryId)) {
55
+ enterProjectPane(target.directoryId);
56
+ markDirty();
57
+ return;
58
+ }
59
+ const visibleTargets = visibleTargetsForState();
60
+ const fallbackConversation = visibleTargets.find(
61
+ (entry): entry is Extract<LeftNavSelection, { kind: 'conversation' }> =>
62
+ entry.kind === 'conversation' &&
63
+ conversationDirectoryId(entry.sessionId) === target.directoryId,
64
+ );
65
+ if (fallbackConversation !== undefined) {
66
+ queueControlPlaneOp(async () => {
67
+ await activateConversation(fallbackConversation.sessionId);
68
+ }, `shortcut-activate-${direction}-directory-fallback`);
69
+ }
70
+ return;
71
+ }
72
+ if (!conversationsHas(target.sessionId)) {
73
+ return;
74
+ }
75
+ queueControlPlaneOp(async () => {
76
+ await activateConversation(target.sessionId);
77
+ }, `shortcut-activate-${direction}`);
78
+ }
79
+
80
+ interface CycleLeftNavSelectionOptions {
81
+ visibleTargets: readonly LeftNavSelection[];
82
+ currentSelection: LeftNavSelection;
83
+ direction: 'next' | 'previous';
84
+ activateTarget: (target: LeftNavSelection, direction: 'next' | 'previous') => void;
85
+ }
86
+
87
+ export function cycleLeftNavSelection(options: CycleLeftNavSelectionOptions): boolean {
88
+ const { visibleTargets, currentSelection, direction, activateTarget } = options;
89
+ if (visibleTargets.length === 0) {
90
+ return false;
91
+ }
92
+ const targetKeys = visibleTargets.map((target) => leftNavTargetKey(target));
93
+ const targetKey = cycleConversationId(targetKeys, leftNavTargetKey(currentSelection), direction);
94
+ if (targetKey === null) {
95
+ return false;
96
+ }
97
+ const target = visibleTargets.find((entry) => leftNavTargetKey(entry) === targetKey);
98
+ if (target === undefined) {
99
+ return false;
100
+ }
101
+ activateTarget(target, direction);
102
+ return true;
103
+ }