@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,236 @@
1
+ import { computeDualPaneLayout } from '../mux/dual-pane-core.ts';
2
+
3
+ interface RuntimeLayoutResizeConversationRecord {
4
+ readonly sessionId: string;
5
+ readonly live: boolean;
6
+ readonly oracle: {
7
+ resize: (cols: number, rows: number) => void;
8
+ };
9
+ }
10
+
11
+ interface RuntimeLayoutResizeConversationManager<
12
+ TConversation extends RuntimeLayoutResizeConversationRecord,
13
+ > {
14
+ readonly activeConversationId: string | null;
15
+ get(sessionId: string): TConversation | undefined;
16
+ values(): IterableIterator<TConversation>;
17
+ }
18
+
19
+ interface RuntimeLayoutResizeSize {
20
+ readonly cols: number;
21
+ readonly rows: number;
22
+ }
23
+
24
+ type RuntimeLayout = ReturnType<typeof computeDualPaneLayout>;
25
+
26
+ interface RuntimeLayoutResizeOptions<TConversation extends RuntimeLayoutResizeConversationRecord> {
27
+ readonly getSize: () => RuntimeLayoutResizeSize;
28
+ readonly setSize: (nextSize: RuntimeLayoutResizeSize) => void;
29
+ readonly getLayout: () => RuntimeLayout;
30
+ readonly setLayout: (nextLayout: RuntimeLayout) => void;
31
+ readonly getLeftPaneColsOverride: () => number | null;
32
+ readonly setLeftPaneColsOverride: (leftCols: number | null) => void;
33
+ readonly conversationManager: RuntimeLayoutResizeConversationManager<TConversation>;
34
+ readonly ptySizeByConversationId: Map<string, RuntimeLayoutResizeSize>;
35
+ readonly sendResize: (sessionId: string, cols: number, rows: number) => void;
36
+ readonly markDirty: () => void;
37
+ readonly resetFrameCache: () => void;
38
+ readonly resizeRecordingOracle: (nextLayout: RuntimeLayout) => void;
39
+ readonly queuePersistMuxUiState: () => void;
40
+ readonly resizeMinIntervalMs: number;
41
+ readonly ptyResizeSettleMs: number;
42
+ readonly nowMs?: () => number;
43
+ readonly setTimeoutFn?: (callback: () => void, delayMs: number) => ReturnType<typeof setTimeout>;
44
+ readonly clearTimeoutFn?: (timer: ReturnType<typeof setTimeout>) => void;
45
+ }
46
+
47
+ export class RuntimeLayoutResize<TConversation extends RuntimeLayoutResizeConversationRecord> {
48
+ private resizeTimer: ReturnType<typeof setTimeout> | null = null;
49
+ private pendingSize: RuntimeLayoutResizeSize | null = null;
50
+ private lastResizeApplyAtMs = 0;
51
+ private ptyResizeTimer: ReturnType<typeof setTimeout> | null = null;
52
+ private pendingPtySize: RuntimeLayoutResizeSize | null = null;
53
+ private readonly nowMs: () => number;
54
+ private readonly setTimeoutFn: (
55
+ callback: () => void,
56
+ delayMs: number,
57
+ ) => ReturnType<typeof setTimeout>;
58
+ private readonly clearTimeoutFn: (timer: ReturnType<typeof setTimeout>) => void;
59
+
60
+ constructor(private readonly options: RuntimeLayoutResizeOptions<TConversation>) {
61
+ this.nowMs = options.nowMs ?? Date.now;
62
+ this.setTimeoutFn = options.setTimeoutFn ?? setTimeout;
63
+ this.clearTimeoutFn = options.clearTimeoutFn ?? clearTimeout;
64
+ }
65
+
66
+ clearResizeTimer(): void {
67
+ if (this.resizeTimer === null) {
68
+ return;
69
+ }
70
+ this.clearTimeoutFn(this.resizeTimer);
71
+ this.resizeTimer = null;
72
+ }
73
+
74
+ clearPtyResizeTimer(): void {
75
+ if (this.ptyResizeTimer === null) {
76
+ return;
77
+ }
78
+ this.clearTimeoutFn(this.ptyResizeTimer);
79
+ this.ptyResizeTimer = null;
80
+ }
81
+
82
+ schedulePtyResize(ptySize: RuntimeLayoutResizeSize, immediate = false): void {
83
+ this.pendingPtySize = ptySize;
84
+ if (immediate) {
85
+ this.clearPtyResizeTimer();
86
+ this.flushPendingPtyResize();
87
+ return;
88
+ }
89
+
90
+ if (this.ptyResizeTimer !== null) {
91
+ this.clearTimeoutFn(this.ptyResizeTimer);
92
+ }
93
+ this.ptyResizeTimer = this.setTimeoutFn(() => {
94
+ this.flushPendingPtyResize();
95
+ }, this.options.ptyResizeSettleMs);
96
+ }
97
+
98
+ applyLayout(nextSize: RuntimeLayoutResizeSize, forceImmediatePtyResize = false): void {
99
+ const nextLayout = computeDualPaneLayout(nextSize.cols, nextSize.rows, {
100
+ leftCols: this.options.getLeftPaneColsOverride(),
101
+ });
102
+ this.schedulePtyResize(
103
+ {
104
+ cols: nextLayout.rightCols,
105
+ rows: nextLayout.paneRows,
106
+ },
107
+ forceImmediatePtyResize,
108
+ );
109
+ const layout = this.options.getLayout();
110
+ if (
111
+ nextLayout.cols === layout.cols &&
112
+ nextLayout.rows === layout.rows &&
113
+ nextLayout.leftCols === layout.leftCols &&
114
+ nextLayout.rightCols === layout.rightCols &&
115
+ nextLayout.paneRows === layout.paneRows
116
+ ) {
117
+ return;
118
+ }
119
+ this.options.setSize(nextSize);
120
+ this.options.setLayout(nextLayout);
121
+ for (const conversation of this.options.conversationManager.values()) {
122
+ conversation.oracle.resize(nextLayout.rightCols, nextLayout.paneRows);
123
+ if (conversation.live) {
124
+ this.applyPtyResizeToSession(
125
+ conversation.sessionId,
126
+ {
127
+ cols: nextLayout.rightCols,
128
+ rows: nextLayout.paneRows,
129
+ },
130
+ true,
131
+ );
132
+ }
133
+ }
134
+ this.options.resizeRecordingOracle(nextLayout);
135
+ // Force a full clear on actual layout changes to avoid stale diagonal artifacts during drag.
136
+ this.options.resetFrameCache();
137
+ this.options.markDirty();
138
+ }
139
+
140
+ queueResize(nextSize: RuntimeLayoutResizeSize): void {
141
+ this.pendingSize = nextSize;
142
+ if (this.resizeTimer !== null) {
143
+ return;
144
+ }
145
+
146
+ const nowMs = this.nowMs();
147
+ const elapsedMs = nowMs - this.lastResizeApplyAtMs;
148
+ const delayMs =
149
+ elapsedMs >= this.options.resizeMinIntervalMs
150
+ ? 0
151
+ : this.options.resizeMinIntervalMs - elapsedMs;
152
+ this.resizeTimer = this.setTimeoutFn(() => {
153
+ this.flushPendingResize();
154
+ }, delayMs);
155
+ }
156
+
157
+ applyPaneDividerAtCol(col: number): void {
158
+ const size = this.options.getSize();
159
+ const normalizedCol = Math.max(1, Math.min(size.cols, col));
160
+ this.options.setLeftPaneColsOverride(Math.max(1, normalizedCol - 1));
161
+ this.applyLayout(size);
162
+ this.options.queuePersistMuxUiState();
163
+ }
164
+
165
+ private applyPtyResizeToSession(
166
+ sessionId: string,
167
+ ptySize: RuntimeLayoutResizeSize,
168
+ force = false,
169
+ ): void {
170
+ const conversation = this.options.conversationManager.get(sessionId);
171
+ if (conversation === undefined || !conversation.live) {
172
+ return;
173
+ }
174
+ const currentPtySize = this.options.ptySizeByConversationId.get(sessionId);
175
+ if (
176
+ !force &&
177
+ currentPtySize !== undefined &&
178
+ currentPtySize.cols === ptySize.cols &&
179
+ currentPtySize.rows === ptySize.rows
180
+ ) {
181
+ return;
182
+ }
183
+ this.options.ptySizeByConversationId.set(sessionId, {
184
+ cols: ptySize.cols,
185
+ rows: ptySize.rows,
186
+ });
187
+ conversation.oracle.resize(ptySize.cols, ptySize.rows);
188
+ this.options.sendResize(sessionId, ptySize.cols, ptySize.rows);
189
+ this.options.markDirty();
190
+ }
191
+
192
+ private applyPtyResize(ptySize: RuntimeLayoutResizeSize): void {
193
+ const activeConversationId = this.options.conversationManager.activeConversationId;
194
+ if (activeConversationId === null) {
195
+ return;
196
+ }
197
+ this.applyPtyResizeToSession(activeConversationId, ptySize, false);
198
+ }
199
+
200
+ private flushPendingPtyResize(): void {
201
+ this.ptyResizeTimer = null;
202
+ const ptySize = this.pendingPtySize;
203
+ if (ptySize === null) {
204
+ return;
205
+ }
206
+ this.pendingPtySize = null;
207
+ this.applyPtyResize(ptySize);
208
+ }
209
+
210
+ private flushPendingResize(): void {
211
+ this.resizeTimer = null;
212
+ const nextSize = this.pendingSize;
213
+ if (nextSize === null) {
214
+ return;
215
+ }
216
+
217
+ const nowMs = this.nowMs();
218
+ const elapsedMs = nowMs - this.lastResizeApplyAtMs;
219
+ if (elapsedMs < this.options.resizeMinIntervalMs) {
220
+ this.resizeTimer = this.setTimeoutFn(() => {
221
+ this.flushPendingResize();
222
+ }, this.options.resizeMinIntervalMs - elapsedMs);
223
+ return;
224
+ }
225
+
226
+ this.pendingSize = null;
227
+ this.applyLayout(nextSize);
228
+ this.lastResizeApplyAtMs = this.nowMs();
229
+
230
+ if (this.pendingSize !== null && this.resizeTimer === null) {
231
+ this.resizeTimer = this.setTimeoutFn(() => {
232
+ this.flushPendingResize();
233
+ }, this.options.resizeMinIntervalMs);
234
+ }
235
+ }
236
+ }
@@ -0,0 +1,159 @@
1
+ import type { WorkspaceModel } from '../domain/workspace.ts';
2
+ import type { RepositoryManager } from '../domain/repositories.ts';
3
+
4
+ interface RuntimeLeftRailRenderLayout {
5
+ readonly cols: number;
6
+ readonly paneRows: number;
7
+ readonly leftCols: number;
8
+ readonly rightCols: number;
9
+ readonly separatorCol: number;
10
+ readonly rightStartCol: number;
11
+ }
12
+
13
+ interface SessionProjectionInstrumentationLike<TDirectoryRecord, TConversationRecord> {
14
+ refreshSelectorSnapshot(
15
+ source: 'render' | 'observed',
16
+ directories: ReadonlyMap<string, TDirectoryRecord>,
17
+ conversations: ReadonlyMap<string, TConversationRecord>,
18
+ orderedConversationIds: readonly string[],
19
+ ): void;
20
+ }
21
+
22
+ interface LeftRailPaneLike<
23
+ TLayout,
24
+ TRepositoryRecord,
25
+ TRepositorySnapshot,
26
+ TDirectoryRecord,
27
+ TConversationRecord,
28
+ TGitSummary,
29
+ TProcessUsage,
30
+ TShortcutBindings,
31
+ TRailViewRows,
32
+ > {
33
+ render(input: {
34
+ layout: TLayout;
35
+ repositories: ReadonlyMap<string, TRepositoryRecord>;
36
+ repositoryAssociationByDirectoryId: ReadonlyMap<string, string>;
37
+ directoryRepositorySnapshotByDirectoryId: ReadonlyMap<string, TRepositorySnapshot>;
38
+ directories: ReadonlyMap<string, TDirectoryRecord>;
39
+ conversations: ReadonlyMap<string, TConversationRecord>;
40
+ orderedIds: readonly string[];
41
+ activeProjectId: string | null;
42
+ activeRepositoryId: string | null;
43
+ activeConversationId: string | null;
44
+ projectSelectionEnabled: boolean;
45
+ repositorySelectionEnabled: boolean;
46
+ homeSelectionEnabled: boolean;
47
+ repositoriesCollapsed: boolean;
48
+ collapsedRepositoryGroupIds: ReadonlySet<string>;
49
+ shortcutsCollapsed: boolean;
50
+ gitSummaryByDirectoryId: ReadonlyMap<string, TGitSummary>;
51
+ processUsageBySessionId: ReadonlyMap<string, TProcessUsage>;
52
+ shortcutBindings: TShortcutBindings;
53
+ loadingGitSummary: TGitSummary;
54
+ }): {
55
+ readonly ansiRows: readonly string[];
56
+ readonly viewRows: TRailViewRows;
57
+ };
58
+ }
59
+
60
+ interface RuntimeLeftRailRenderOptions<
61
+ TDirectoryRecord,
62
+ TConversationRecord,
63
+ TRepositoryRecord,
64
+ TRepositorySnapshot,
65
+ TGitSummary,
66
+ TProcessUsage,
67
+ TShortcutBindings,
68
+ TRailViewRows,
69
+ > {
70
+ readonly leftRailPane: LeftRailPaneLike<
71
+ RuntimeLeftRailRenderLayout,
72
+ TRepositoryRecord,
73
+ TRepositorySnapshot,
74
+ TDirectoryRecord,
75
+ TConversationRecord,
76
+ TGitSummary,
77
+ TProcessUsage,
78
+ TShortcutBindings,
79
+ TRailViewRows
80
+ >;
81
+ readonly sessionProjectionInstrumentation: SessionProjectionInstrumentationLike<
82
+ TDirectoryRecord,
83
+ TConversationRecord
84
+ >;
85
+ readonly workspace: WorkspaceModel;
86
+ readonly repositoryManager: RepositoryManager<TRepositoryRecord, TRepositorySnapshot>;
87
+ readonly repositories: ReadonlyMap<string, TRepositoryRecord>;
88
+ readonly repositoryAssociationByDirectoryId: ReadonlyMap<string, string>;
89
+ readonly directoryRepositorySnapshotByDirectoryId: ReadonlyMap<string, TRepositorySnapshot>;
90
+ readonly directories: ReadonlyMap<string, TDirectoryRecord>;
91
+ readonly conversations: ReadonlyMap<string, TConversationRecord>;
92
+ readonly gitSummaryByDirectoryId: ReadonlyMap<string, TGitSummary>;
93
+ readonly processUsageBySessionId: () => ReadonlyMap<string, TProcessUsage>;
94
+ readonly shortcutBindings: TShortcutBindings;
95
+ readonly loadingGitSummary: TGitSummary;
96
+ readonly activeConversationId: () => string | null;
97
+ readonly orderedConversationIds: () => readonly string[];
98
+ }
99
+
100
+ export class RuntimeLeftRailRender<
101
+ TDirectoryRecord,
102
+ TConversationRecord,
103
+ TRepositoryRecord,
104
+ TRepositorySnapshot,
105
+ TGitSummary,
106
+ TProcessUsage,
107
+ TShortcutBindings,
108
+ TRailViewRows,
109
+ > {
110
+ constructor(
111
+ private readonly options: RuntimeLeftRailRenderOptions<
112
+ TDirectoryRecord,
113
+ TConversationRecord,
114
+ TRepositoryRecord,
115
+ TRepositorySnapshot,
116
+ TGitSummary,
117
+ TProcessUsage,
118
+ TShortcutBindings,
119
+ TRailViewRows
120
+ >,
121
+ ) {}
122
+
123
+ render(layout: RuntimeLeftRailRenderLayout): {
124
+ readonly ansiRows: readonly string[];
125
+ readonly viewRows: TRailViewRows;
126
+ } {
127
+ const orderedIds = this.options.orderedConversationIds();
128
+ this.options.sessionProjectionInstrumentation.refreshSelectorSnapshot(
129
+ 'render',
130
+ this.options.directories,
131
+ this.options.conversations,
132
+ orderedIds,
133
+ );
134
+ return this.options.leftRailPane.render({
135
+ layout,
136
+ repositories: this.options.repositories,
137
+ repositoryAssociationByDirectoryId: this.options.repositoryAssociationByDirectoryId,
138
+ directoryRepositorySnapshotByDirectoryId:
139
+ this.options.directoryRepositorySnapshotByDirectoryId,
140
+ directories: this.options.directories,
141
+ conversations: this.options.conversations,
142
+ orderedIds,
143
+ activeProjectId: this.options.workspace.activeDirectoryId,
144
+ activeRepositoryId: this.options.workspace.activeRepositorySelectionId,
145
+ activeConversationId: this.options.activeConversationId(),
146
+ projectSelectionEnabled: this.options.workspace.leftNavSelection.kind === 'project',
147
+ repositorySelectionEnabled: this.options.workspace.leftNavSelection.kind === 'repository',
148
+ homeSelectionEnabled: this.options.workspace.leftNavSelection.kind === 'home',
149
+ repositoriesCollapsed: this.options.workspace.repositoriesCollapsed,
150
+ collapsedRepositoryGroupIds:
151
+ this.options.repositoryManager.readonlyCollapsedRepositoryGroupIds(),
152
+ shortcutsCollapsed: this.options.workspace.shortcutsCollapsed,
153
+ gitSummaryByDirectoryId: this.options.gitSummaryByDirectoryId,
154
+ processUsageBySessionId: this.options.processUsageBySessionId(),
155
+ shortcutBindings: this.options.shortcutBindings,
156
+ loadingGitSummary: this.options.loadingGitSummary,
157
+ });
158
+ }
159
+ }
@@ -0,0 +1,230 @@
1
+ import type { WorkspaceModel } from '../domain/workspace.ts';
2
+ import type { LeftRailPointerInput } from '../ui/left-rail-pointer-input.ts';
3
+ import { MainPanePointerInput } from '../ui/main-pane-pointer-input.ts';
4
+ import { PointerRoutingInput } from '../ui/pointer-routing-input.ts';
5
+ import { ConversationSelectionInput } from '../ui/conversation-selection-input.ts';
6
+ import { InputTokenRouter } from '../ui/input-token-router.ts';
7
+
8
+ type ProjectPaneSnapshot = NonNullable<WorkspaceModel['projectPaneSnapshot']>;
9
+ type MainPanePointerInputOptions = ConstructorParameters<
10
+ typeof MainPanePointerInput<ProjectPaneSnapshot>
11
+ >[0];
12
+ type PointerRoutingInputOptions = ConstructorParameters<typeof PointerRoutingInput>[0];
13
+ type ConversationSelectionInputOptions = ConstructorParameters<
14
+ typeof ConversationSelectionInput
15
+ >[0];
16
+ type InputTokenRouterOptions = ConstructorParameters<typeof InputTokenRouter>[0];
17
+
18
+ type RouteTokensInput = Parameters<InputTokenRouter['routeTokens']>[0];
19
+ type RouteTokensResult = ReturnType<InputTokenRouter['routeTokens']>;
20
+
21
+ interface RuntimeMainPaneWorkspaceActions {
22
+ runTaskPaneAction(action: 'task.ready' | 'task.draft' | 'task.complete'): void;
23
+ openTaskEditPrompt(taskId: string): void;
24
+ openRepositoryPromptForEdit(repositoryId: string): void;
25
+ reorderTaskByDrop(draggedTaskId: string, targetTaskId: string): void;
26
+ reorderRepositoryByDrop(draggedRepositoryId: string, targetRepositoryId: string): void;
27
+ }
28
+
29
+ interface RuntimeMainPaneInputOptions {
30
+ readonly workspace: WorkspaceModel;
31
+ readonly leftRailPointerInput: Pick<LeftRailPointerInput, 'handlePointerClick'>;
32
+ readonly workspaceActions: RuntimeMainPaneWorkspaceActions;
33
+ readonly projectPaneActionAtRow: MainPanePointerInputOptions['projectPaneActionAtRow'];
34
+ readonly openNewThreadPrompt: MainPanePointerInputOptions['openNewThreadPrompt'];
35
+ readonly queueCloseDirectory: MainPanePointerInputOptions['queueCloseDirectory'];
36
+ readonly selectTaskById: MainPanePointerInputOptions['selectTaskById'];
37
+ readonly selectRepositoryById: MainPanePointerInputOptions['selectRepositoryById'];
38
+ readonly taskPaneActionAtCell: (
39
+ view: WorkspaceModel['latestTaskPaneView'],
40
+ rowIndex: number,
41
+ colIndex: number,
42
+ ) => ReturnType<MainPanePointerInputOptions['actionAtCell']>;
43
+ readonly taskPaneActionAtRow: (
44
+ view: WorkspaceModel['latestTaskPaneView'],
45
+ rowIndex: number,
46
+ ) => ReturnType<MainPanePointerInputOptions['actionAtRow']>;
47
+ readonly taskPaneTaskIdAtRow: (
48
+ view: WorkspaceModel['latestTaskPaneView'],
49
+ rowIndex: number,
50
+ ) => ReturnType<MainPanePointerInputOptions['taskIdAtRow']>;
51
+ readonly taskPaneRepositoryIdAtRow: (
52
+ view: WorkspaceModel['latestTaskPaneView'],
53
+ rowIndex: number,
54
+ ) => ReturnType<MainPanePointerInputOptions['repositoryIdAtRow']>;
55
+ readonly applyPaneDividerAtCol: PointerRoutingInputOptions['applyPaneDividerAtCol'];
56
+ readonly pinViewportForSelection: ConversationSelectionInputOptions['pinViewportForSelection'];
57
+ readonly releaseViewportPinForSelection: ConversationSelectionInputOptions['releaseViewportPinForSelection'];
58
+ readonly markDirty: () => void;
59
+ readonly homePaneEditDoubleClickWindowMs: number;
60
+ readonly nowMs?: () => number;
61
+ }
62
+
63
+ interface RuntimeMainPaneInputDependencies {
64
+ readonly createMainPanePointerInput?: (
65
+ options: MainPanePointerInputOptions,
66
+ ) => Pick<
67
+ MainPanePointerInput<ProjectPaneSnapshot>,
68
+ 'handleProjectPanePointerClick' | 'handleHomePanePointerClick'
69
+ >;
70
+ readonly createPointerRoutingInput?: (
71
+ options: PointerRoutingInputOptions,
72
+ ) => Pick<
73
+ PointerRoutingInput,
74
+ | 'handlePaneDividerDrag'
75
+ | 'handleHomePaneDragRelease'
76
+ | 'handleSeparatorPointerPress'
77
+ | 'handleMainPaneWheel'
78
+ | 'handleHomePaneDragMove'
79
+ >;
80
+ readonly createConversationSelectionInput?: (
81
+ options: ConversationSelectionInputOptions,
82
+ ) => Pick<ConversationSelectionInput, 'clearSelectionOnTextToken' | 'handleMouseSelection'>;
83
+ readonly createInputTokenRouter?: (
84
+ options: InputTokenRouterOptions,
85
+ ) => Pick<InputTokenRouter, 'routeTokens'>;
86
+ }
87
+
88
+ export class RuntimeMainPaneInput {
89
+ private readonly inputTokenRouter: Pick<InputTokenRouter, 'routeTokens'>;
90
+
91
+ constructor(
92
+ options: RuntimeMainPaneInputOptions,
93
+ dependencies: RuntimeMainPaneInputDependencies = {},
94
+ ) {
95
+ const nowMs = options.nowMs ?? (() => Date.now());
96
+ const createMainPanePointerInput =
97
+ dependencies.createMainPanePointerInput ??
98
+ ((mainPaneOptions: MainPanePointerInputOptions) => new MainPanePointerInput(mainPaneOptions));
99
+ const createPointerRoutingInput =
100
+ dependencies.createPointerRoutingInput ??
101
+ ((pointerOptions: PointerRoutingInputOptions) => new PointerRoutingInput(pointerOptions));
102
+ const createConversationSelectionInput =
103
+ dependencies.createConversationSelectionInput ??
104
+ ((selectionOptions: ConversationSelectionInputOptions) =>
105
+ new ConversationSelectionInput(selectionOptions));
106
+ const createInputTokenRouter =
107
+ dependencies.createInputTokenRouter ??
108
+ ((tokenRouterOptions: InputTokenRouterOptions) => new InputTokenRouter(tokenRouterOptions));
109
+
110
+ const mainPanePointerInput = createMainPanePointerInput({
111
+ getMainPaneMode: () => options.workspace.mainPaneMode,
112
+ getProjectPaneSnapshot: () => options.workspace.projectPaneSnapshot,
113
+ getProjectPaneScrollTop: () => options.workspace.projectPaneScrollTop,
114
+ projectPaneActionAtRow: options.projectPaneActionAtRow,
115
+ openNewThreadPrompt: options.openNewThreadPrompt,
116
+ queueCloseDirectory: options.queueCloseDirectory,
117
+ actionAtCell: (rowIndex, colIndex) =>
118
+ options.taskPaneActionAtCell(options.workspace.latestTaskPaneView, rowIndex, colIndex),
119
+ actionAtRow: (rowIndex) =>
120
+ options.taskPaneActionAtRow(options.workspace.latestTaskPaneView, rowIndex),
121
+ clearTaskEditClickState: () => {
122
+ options.workspace.taskPaneTaskEditClickState = null;
123
+ },
124
+ clearRepositoryEditClickState: () => {
125
+ options.workspace.taskPaneRepositoryEditClickState = null;
126
+ },
127
+ clearHomePaneDragState: () => {
128
+ options.workspace.homePaneDragState = null;
129
+ },
130
+ getTaskRepositoryDropdownOpen: () => options.workspace.taskRepositoryDropdownOpen,
131
+ setTaskRepositoryDropdownOpen: (open) => {
132
+ options.workspace.taskRepositoryDropdownOpen = open;
133
+ },
134
+ taskIdAtRow: (rowIndex) =>
135
+ options.taskPaneTaskIdAtRow(options.workspace.latestTaskPaneView, rowIndex),
136
+ repositoryIdAtRow: (rowIndex) =>
137
+ options.taskPaneRepositoryIdAtRow(options.workspace.latestTaskPaneView, rowIndex),
138
+ selectTaskById: options.selectTaskById,
139
+ selectRepositoryById: options.selectRepositoryById,
140
+ runTaskPaneAction: (action) => {
141
+ options.workspaceActions.runTaskPaneAction(action);
142
+ },
143
+ nowMs,
144
+ homePaneEditDoubleClickWindowMs: options.homePaneEditDoubleClickWindowMs,
145
+ getTaskEditClickState: () => options.workspace.taskPaneTaskEditClickState,
146
+ getRepositoryEditClickState: () => options.workspace.taskPaneRepositoryEditClickState,
147
+ clearTaskPaneNotice: () => {
148
+ options.workspace.taskPaneNotice = null;
149
+ },
150
+ setTaskEditClickState: (next) => {
151
+ options.workspace.taskPaneTaskEditClickState = next;
152
+ },
153
+ setRepositoryEditClickState: (next) => {
154
+ options.workspace.taskPaneRepositoryEditClickState = next;
155
+ },
156
+ setHomePaneDragState: (next) => {
157
+ options.workspace.homePaneDragState = next;
158
+ },
159
+ openTaskEditPrompt: (taskId) => {
160
+ options.workspaceActions.openTaskEditPrompt(taskId);
161
+ },
162
+ openRepositoryPromptForEdit: (repositoryId) => {
163
+ options.workspaceActions.openRepositoryPromptForEdit(repositoryId);
164
+ },
165
+ markDirty: options.markDirty,
166
+ });
167
+
168
+ const pointerRoutingInput = createPointerRoutingInput({
169
+ getPaneDividerDragActive: () => options.workspace.paneDividerDragActive,
170
+ setPaneDividerDragActive: (active) => {
171
+ options.workspace.paneDividerDragActive = active;
172
+ },
173
+ applyPaneDividerAtCol: options.applyPaneDividerAtCol,
174
+ getHomePaneDragState: () => options.workspace.homePaneDragState,
175
+ setHomePaneDragState: (next) => {
176
+ options.workspace.homePaneDragState = next;
177
+ },
178
+ getMainPaneMode: () => options.workspace.mainPaneMode,
179
+ taskIdAtRow: (index) =>
180
+ options.taskPaneTaskIdAtRow(options.workspace.latestTaskPaneView, index),
181
+ repositoryIdAtRow: (index) =>
182
+ options.taskPaneRepositoryIdAtRow(options.workspace.latestTaskPaneView, index),
183
+ reorderTaskByDrop: (draggedTaskId, targetTaskId) => {
184
+ options.workspaceActions.reorderTaskByDrop(draggedTaskId, targetTaskId);
185
+ },
186
+ reorderRepositoryByDrop: (draggedRepositoryId, targetRepositoryId) => {
187
+ options.workspaceActions.reorderRepositoryByDrop(draggedRepositoryId, targetRepositoryId);
188
+ },
189
+ onProjectWheel: (delta) => {
190
+ options.workspace.projectPaneScrollTop = Math.max(
191
+ 0,
192
+ options.workspace.projectPaneScrollTop + delta,
193
+ );
194
+ },
195
+ onHomeWheel: (delta) => {
196
+ options.workspace.taskPaneScrollTop = Math.max(
197
+ 0,
198
+ options.workspace.taskPaneScrollTop + delta,
199
+ );
200
+ },
201
+ markDirty: options.markDirty,
202
+ });
203
+
204
+ const conversationSelectionInput = createConversationSelectionInput({
205
+ getSelection: () => options.workspace.selection,
206
+ setSelection: (next) => {
207
+ options.workspace.selection = next;
208
+ },
209
+ getSelectionDrag: () => options.workspace.selectionDrag,
210
+ setSelectionDrag: (next) => {
211
+ options.workspace.selectionDrag = next;
212
+ },
213
+ pinViewportForSelection: options.pinViewportForSelection,
214
+ releaseViewportPinForSelection: options.releaseViewportPinForSelection,
215
+ markDirty: options.markDirty,
216
+ });
217
+
218
+ this.inputTokenRouter = createInputTokenRouter({
219
+ getMainPaneMode: () => options.workspace.mainPaneMode,
220
+ pointerRoutingInput,
221
+ mainPanePointerInput,
222
+ leftRailPointerInput: options.leftRailPointerInput,
223
+ conversationSelectionInput,
224
+ });
225
+ }
226
+
227
+ routeTokens(input: RouteTokensInput): RouteTokensResult {
228
+ return this.inputTokenRouter.routeTokens(input);
229
+ }
230
+ }