@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,146 @@
1
+ interface RuntimeRenderFlushResult {
2
+ readonly changedRowCount: number;
3
+ readonly wroteOutput: boolean;
4
+ readonly shouldShowCursor: boolean;
5
+ }
6
+
7
+ interface RuntimeRenderFlushInput<TConversation, TFrame, TSelection, TLayout> {
8
+ readonly layout: TLayout;
9
+ readonly projectPaneActive: boolean;
10
+ readonly homePaneActive: boolean;
11
+ readonly activeConversation: TConversation | null;
12
+ readonly rightFrame: TFrame | null;
13
+ readonly renderSelection: TSelection | null;
14
+ readonly selectionRows: readonly number[];
15
+ readonly railAnsiRows: readonly string[];
16
+ readonly rightRows: readonly string[];
17
+ }
18
+
19
+ interface RuntimeRenderFlushOptions<
20
+ TConversation,
21
+ TFrame,
22
+ TSelection,
23
+ TLayout,
24
+ TModalOverlay,
25
+ TStatusRow,
26
+ > {
27
+ readonly perfNowNs: () => bigint;
28
+ readonly statusFooterForConversation: (conversation: TConversation) => string;
29
+ readonly currentStatusNotice: () => string | null;
30
+ readonly currentStatusRow: () => TStatusRow;
31
+ readonly onStatusLineComposed?: (input: {
32
+ activeConversation: TConversation | null;
33
+ statusFooter: string;
34
+ statusRow: TStatusRow;
35
+ projectPaneActive: boolean;
36
+ homePaneActive: boolean;
37
+ }) => void;
38
+ readonly buildRenderRows: (
39
+ layout: TLayout,
40
+ railRows: readonly string[],
41
+ rightRows: readonly string[],
42
+ statusRow: TStatusRow,
43
+ statusFooter: string,
44
+ ) => string[];
45
+ readonly buildModalOverlay: () => TModalOverlay | null;
46
+ readonly applyModalOverlay: (rows: string[], overlay: TModalOverlay) => void;
47
+ readonly renderSelectionOverlay: (
48
+ layout: TLayout,
49
+ rightFrame: TFrame,
50
+ selection: TSelection | null,
51
+ ) => string;
52
+ readonly flush: (input: {
53
+ layout: TLayout;
54
+ rows: readonly string[];
55
+ rightFrame: TFrame | null;
56
+ selectionRows: readonly number[];
57
+ selectionOverlay: string;
58
+ }) => RuntimeRenderFlushResult;
59
+ readonly onFlushOutput: (input: {
60
+ activeConversation: TConversation | null;
61
+ rightFrame: TFrame | null;
62
+ rows: readonly string[];
63
+ flushResult: RuntimeRenderFlushResult;
64
+ changedRowCount: number;
65
+ }) => void;
66
+ readonly recordRenderSample: (durationMs: number, changedRowCount: number) => void;
67
+ }
68
+
69
+ export class RuntimeRenderFlush<
70
+ TConversation,
71
+ TFrame,
72
+ TSelection,
73
+ TLayout,
74
+ TModalOverlay,
75
+ TStatusRow,
76
+ > {
77
+ constructor(
78
+ private readonly options: RuntimeRenderFlushOptions<
79
+ TConversation,
80
+ TFrame,
81
+ TSelection,
82
+ TLayout,
83
+ TModalOverlay,
84
+ TStatusRow
85
+ >,
86
+ ) {}
87
+
88
+ flushRender(input: RuntimeRenderFlushInput<TConversation, TFrame, TSelection, TLayout>): void {
89
+ const renderStartedAtNs = this.options.perfNowNs();
90
+ const baseStatusFooter =
91
+ !input.projectPaneActive && !input.homePaneActive && input.activeConversation !== null
92
+ ? this.options.statusFooterForConversation(input.activeConversation)
93
+ : '';
94
+ const statusNotice = this.options.currentStatusNotice();
95
+ const statusFooter =
96
+ statusNotice === null || statusNotice.length === 0
97
+ ? baseStatusFooter
98
+ : `${baseStatusFooter.length > 0 ? `${baseStatusFooter} ` : ''}${statusNotice}`;
99
+ const statusRow = this.options.currentStatusRow();
100
+ this.options.onStatusLineComposed?.({
101
+ activeConversation: input.activeConversation,
102
+ statusFooter,
103
+ statusRow,
104
+ projectPaneActive: input.projectPaneActive,
105
+ homePaneActive: input.homePaneActive,
106
+ });
107
+ const rows = this.options.buildRenderRows(
108
+ input.layout,
109
+ input.railAnsiRows,
110
+ input.rightRows,
111
+ statusRow,
112
+ statusFooter,
113
+ );
114
+ const modalOverlay = this.options.buildModalOverlay();
115
+ if (modalOverlay !== null) {
116
+ this.options.applyModalOverlay(rows, modalOverlay);
117
+ }
118
+ const selectionOverlay =
119
+ input.rightFrame === null
120
+ ? ''
121
+ : this.options.renderSelectionOverlay(
122
+ input.layout,
123
+ input.rightFrame,
124
+ input.renderSelection,
125
+ );
126
+ const flushResult = this.options.flush({
127
+ layout: input.layout,
128
+ rows,
129
+ rightFrame: input.rightFrame,
130
+ selectionRows: input.selectionRows,
131
+ selectionOverlay,
132
+ });
133
+ const changedRowCount = flushResult.changedRowCount;
134
+ if (flushResult.wroteOutput) {
135
+ this.options.onFlushOutput({
136
+ activeConversation: input.activeConversation,
137
+ rightFrame: input.rightFrame,
138
+ rows,
139
+ flushResult,
140
+ changedRowCount,
141
+ });
142
+ }
143
+ const renderDurationMs = Number(this.options.perfNowNs() - renderStartedAtNs) / 1e6;
144
+ this.options.recordRenderSample(renderDurationMs, changedRowCount);
145
+ }
146
+ }
@@ -0,0 +1,104 @@
1
+ interface ScreenLike {
2
+ clearDirty(): void;
3
+ isDirty(): boolean;
4
+ markDirty(): void;
5
+ }
6
+
7
+ interface RuntimeRenderLifecycleOptions {
8
+ readonly screen: ScreenLike;
9
+ readonly render: () => void;
10
+ readonly isShuttingDown: () => boolean;
11
+ readonly setShuttingDown: (next: boolean) => void;
12
+ readonly setStop: (next: boolean) => void;
13
+ readonly restoreTerminalState: () => void;
14
+ readonly formatErrorMessage: (error: unknown) => string;
15
+ readonly writeStderr: (text: string) => void;
16
+ readonly exitProcess: (code: number) => void;
17
+ readonly setImmediateFn?: (callback: () => void) => void;
18
+ readonly setTimeoutFn?: (callback: () => void, delayMs: number) => ReturnType<typeof setTimeout>;
19
+ readonly clearTimeoutFn?: (timer: ReturnType<typeof setTimeout>) => void;
20
+ }
21
+
22
+ const FATAL_EXIT_DELAY_MS = 1200;
23
+
24
+ export class RuntimeRenderLifecycle {
25
+ private readonly setImmediateFn: (callback: () => void) => void;
26
+ private readonly setTimeoutFn: (
27
+ callback: () => void,
28
+ delayMs: number,
29
+ ) => ReturnType<typeof setTimeout>;
30
+ private readonly clearTimeoutFn: (timer: ReturnType<typeof setTimeout>) => void;
31
+ private renderScheduled = false;
32
+ private runtimeFatal: { origin: string; error: unknown } | null = null;
33
+ private runtimeFatalExitTimer: ReturnType<typeof setTimeout> | null = null;
34
+
35
+ constructor(private readonly options: RuntimeRenderLifecycleOptions) {
36
+ this.setImmediateFn = options.setImmediateFn ?? setImmediate;
37
+ this.setTimeoutFn = options.setTimeoutFn ?? setTimeout;
38
+ this.clearTimeoutFn = options.clearTimeoutFn ?? clearTimeout;
39
+ }
40
+
41
+ hasFatal(): boolean {
42
+ return this.runtimeFatal !== null;
43
+ }
44
+
45
+ clearRenderScheduled(): void {
46
+ this.renderScheduled = false;
47
+ }
48
+
49
+ clearRuntimeFatalExitTimer(): void {
50
+ if (this.runtimeFatalExitTimer === null) {
51
+ return;
52
+ }
53
+ this.clearTimeoutFn(this.runtimeFatalExitTimer);
54
+ this.runtimeFatalExitTimer = null;
55
+ }
56
+
57
+ markDirty(): void {
58
+ if (this.options.isShuttingDown()) {
59
+ return;
60
+ }
61
+ this.options.screen.markDirty();
62
+ this.scheduleRender();
63
+ }
64
+
65
+ scheduleRender(): void {
66
+ if (this.options.isShuttingDown() || this.renderScheduled) {
67
+ return;
68
+ }
69
+ this.renderScheduled = true;
70
+ this.setImmediateFn(() => {
71
+ this.renderScheduled = false;
72
+ try {
73
+ this.options.render();
74
+ if (this.options.screen.isDirty()) {
75
+ this.scheduleRender();
76
+ }
77
+ } catch (error: unknown) {
78
+ this.handleRuntimeFatal('render', error);
79
+ }
80
+ });
81
+ }
82
+
83
+ handleRuntimeFatal(origin: string, error: unknown): void {
84
+ if (this.runtimeFatal !== null) {
85
+ return;
86
+ }
87
+ this.runtimeFatal = {
88
+ origin,
89
+ error,
90
+ };
91
+ this.options.setShuttingDown(true);
92
+ this.options.setStop(true);
93
+ this.options.screen.clearDirty();
94
+ this.options.writeStderr(
95
+ `[mux] fatal runtime error (${origin}): ${this.options.formatErrorMessage(error)}\n`,
96
+ );
97
+ this.options.restoreTerminalState();
98
+ this.runtimeFatalExitTimer = this.setTimeoutFn(() => {
99
+ this.options.writeStderr('[mux] fatal runtime error forced exit\n');
100
+ this.options.exitProcess(1);
101
+ }, FATAL_EXIT_DELAY_MS);
102
+ this.runtimeFatalExitTimer.unref?.();
103
+ }
104
+ }
@@ -0,0 +1,108 @@
1
+ interface RuntimeRenderStateResult<TConversation, TFrame, TSelection> {
2
+ readonly projectPaneActive: boolean;
3
+ readonly homePaneActive: boolean;
4
+ readonly activeConversation: TConversation | null;
5
+ readonly rightFrame: TFrame | null;
6
+ readonly renderSelection: TSelection | null;
7
+ readonly selectionRows: readonly number[];
8
+ }
9
+
10
+ interface RuntimeLeftRailRenderResult<TRailViewRows> {
11
+ readonly ansiRows: readonly string[];
12
+ readonly viewRows: TRailViewRows;
13
+ }
14
+
15
+ interface RuntimeRenderOrchestratorOptions<
16
+ TLayout,
17
+ TConversation,
18
+ TFrame,
19
+ TSelection,
20
+ TSelectionDrag,
21
+ TRailViewRows,
22
+ > {
23
+ readonly isScreenDirty: () => boolean;
24
+ readonly clearDirty: () => void;
25
+ readonly prepareRenderState: (
26
+ selection: TSelection | null,
27
+ selectionDrag: TSelectionDrag | null,
28
+ ) => RuntimeRenderStateResult<TConversation, TFrame, TSelection> | null;
29
+ readonly renderLeftRail: (layout: TLayout) => RuntimeLeftRailRenderResult<TRailViewRows>;
30
+ readonly setLatestRailViewRows: (rows: TRailViewRows) => void;
31
+ readonly renderRightRows: (input: {
32
+ layout: TLayout;
33
+ rightFrame: TFrame | null;
34
+ homePaneActive: boolean;
35
+ projectPaneActive: boolean;
36
+ activeDirectoryId: string | null;
37
+ }) => readonly string[];
38
+ readonly flushRender: (input: {
39
+ layout: TLayout;
40
+ projectPaneActive: boolean;
41
+ homePaneActive: boolean;
42
+ activeConversation: TConversation | null;
43
+ rightFrame: TFrame | null;
44
+ renderSelection: TSelection | null;
45
+ selectionRows: readonly number[];
46
+ railAnsiRows: readonly string[];
47
+ rightRows: readonly string[];
48
+ }) => void;
49
+ readonly activeDirectoryId: () => string | null;
50
+ }
51
+
52
+ interface RuntimeRenderOrchestratorInput<TLayout, TSelection, TSelectionDrag> {
53
+ readonly shuttingDown: boolean;
54
+ readonly layout: TLayout;
55
+ readonly selection: TSelection | null;
56
+ readonly selectionDrag: TSelectionDrag | null;
57
+ }
58
+
59
+ export class RuntimeRenderOrchestrator<
60
+ TLayout,
61
+ TConversation,
62
+ TFrame,
63
+ TSelection,
64
+ TSelectionDrag,
65
+ TRailViewRows,
66
+ > {
67
+ constructor(
68
+ private readonly options: RuntimeRenderOrchestratorOptions<
69
+ TLayout,
70
+ TConversation,
71
+ TFrame,
72
+ TSelection,
73
+ TSelectionDrag,
74
+ TRailViewRows
75
+ >,
76
+ ) {}
77
+
78
+ render(input: RuntimeRenderOrchestratorInput<TLayout, TSelection, TSelectionDrag>): void {
79
+ if (input.shuttingDown || !this.options.isScreenDirty()) {
80
+ return;
81
+ }
82
+ const renderState = this.options.prepareRenderState(input.selection, input.selectionDrag);
83
+ if (renderState === null) {
84
+ this.options.clearDirty();
85
+ return;
86
+ }
87
+ const rail = this.options.renderLeftRail(input.layout);
88
+ this.options.setLatestRailViewRows(rail.viewRows);
89
+ const rightRows = this.options.renderRightRows({
90
+ layout: input.layout,
91
+ rightFrame: renderState.rightFrame,
92
+ homePaneActive: renderState.homePaneActive,
93
+ projectPaneActive: renderState.projectPaneActive,
94
+ activeDirectoryId: this.options.activeDirectoryId(),
95
+ });
96
+ this.options.flushRender({
97
+ layout: input.layout,
98
+ projectPaneActive: renderState.projectPaneActive,
99
+ homePaneActive: renderState.homePaneActive,
100
+ activeConversation: renderState.activeConversation,
101
+ rightFrame: renderState.rightFrame,
102
+ renderSelection: renderState.renderSelection,
103
+ selectionRows: renderState.selectionRows,
104
+ railAnsiRows: rail.ansiRows,
105
+ rightRows,
106
+ });
107
+ }
108
+ }
@@ -0,0 +1,167 @@
1
+ import type { computeDualPaneLayout } from '../mux/dual-pane-core.ts';
2
+ import type { PaneSelection, PaneSelectionDrag } from '../mux/live-mux/selection.ts';
3
+ import type {
4
+ TaskFocusedPaneRepositoryRecord,
5
+ TaskFocusedPaneTaskRecord,
6
+ } from '../mux/task-focused-pane.ts';
7
+ import type { TerminalSnapshotFrameCore } from '../terminal/snapshot-oracle.ts';
8
+ import { RuntimeLeftRailRender } from './runtime-left-rail-render.ts';
9
+ import { RuntimeRenderFlush } from './runtime-render-flush.ts';
10
+ import { RuntimeRenderOrchestrator } from './runtime-render-orchestrator.ts';
11
+ import { RuntimeRenderState } from './runtime-render-state.ts';
12
+ import { RuntimeRightPaneRender } from './runtime-right-pane-render.ts';
13
+
14
+ type RuntimeLayout = ReturnType<typeof computeDualPaneLayout>;
15
+
16
+ type RuntimeRenderFlushOptions<TConversation, TModalOverlay, TStatusRow> = ConstructorParameters<
17
+ typeof RuntimeRenderFlush<
18
+ TConversation,
19
+ TerminalSnapshotFrameCore,
20
+ PaneSelection,
21
+ RuntimeLayout,
22
+ TModalOverlay,
23
+ TStatusRow
24
+ >
25
+ >[0];
26
+
27
+ type RuntimeRightPaneRenderOptions<
28
+ TRepositoryRecord extends TaskFocusedPaneRepositoryRecord,
29
+ TTaskRecord extends TaskFocusedPaneTaskRecord,
30
+ > = ConstructorParameters<typeof RuntimeRightPaneRender<TRepositoryRecord, TTaskRecord>>[0];
31
+
32
+ type RuntimeLeftRailRenderOptions<
33
+ TDirectoryRecord,
34
+ TConversation,
35
+ TRepositoryRecord,
36
+ TRepositorySnapshot,
37
+ TGitSummary,
38
+ TProcessUsage,
39
+ TShortcutBindings,
40
+ TRailViewRows,
41
+ > = ConstructorParameters<
42
+ typeof RuntimeLeftRailRender<
43
+ TDirectoryRecord,
44
+ TConversation,
45
+ TRepositoryRecord,
46
+ TRepositorySnapshot,
47
+ TGitSummary,
48
+ TProcessUsage,
49
+ TShortcutBindings,
50
+ TRailViewRows
51
+ >
52
+ >[0];
53
+
54
+ type RuntimeRenderStateOptions<TConversation> = ConstructorParameters<
55
+ typeof RuntimeRenderState<TConversation, TerminalSnapshotFrameCore>
56
+ >[0];
57
+
58
+ interface RuntimeRenderPipelineOptions<
59
+ TConversation,
60
+ TRepositoryRecord extends TaskFocusedPaneRepositoryRecord,
61
+ TTaskRecord extends TaskFocusedPaneTaskRecord,
62
+ TDirectoryRecord,
63
+ TRepositorySnapshot,
64
+ TGitSummary,
65
+ TProcessUsage,
66
+ TShortcutBindings,
67
+ TRailViewRows,
68
+ TModalOverlay,
69
+ TStatusRow,
70
+ > {
71
+ readonly renderFlush: RuntimeRenderFlushOptions<TConversation, TModalOverlay, TStatusRow>;
72
+ readonly rightPaneRender: RuntimeRightPaneRenderOptions<TRepositoryRecord, TTaskRecord>;
73
+ readonly leftRailRender: RuntimeLeftRailRenderOptions<
74
+ TDirectoryRecord,
75
+ TConversation,
76
+ TRepositoryRecord,
77
+ TRepositorySnapshot,
78
+ TGitSummary,
79
+ TProcessUsage,
80
+ TShortcutBindings,
81
+ TRailViewRows
82
+ >;
83
+ readonly renderState: RuntimeRenderStateOptions<TConversation>;
84
+ readonly isScreenDirty: () => boolean;
85
+ readonly clearDirty: () => void;
86
+ readonly setLatestRailViewRows: (rows: TRailViewRows) => void;
87
+ readonly activeDirectoryId: () => string | null;
88
+ }
89
+
90
+ type RuntimeRenderPipelineInput = Parameters<
91
+ RuntimeRenderOrchestrator<
92
+ RuntimeLayout,
93
+ unknown,
94
+ TerminalSnapshotFrameCore,
95
+ PaneSelection,
96
+ PaneSelectionDrag,
97
+ unknown
98
+ >['render']
99
+ >[0];
100
+
101
+ export class RuntimeRenderPipeline<
102
+ TConversation,
103
+ TRepositoryRecord extends TaskFocusedPaneRepositoryRecord,
104
+ TTaskRecord extends TaskFocusedPaneTaskRecord,
105
+ TDirectoryRecord,
106
+ TRepositorySnapshot,
107
+ TGitSummary,
108
+ TProcessUsage,
109
+ TShortcutBindings,
110
+ TRailViewRows,
111
+ TModalOverlay,
112
+ TStatusRow,
113
+ > {
114
+ private readonly renderOrchestrator: RuntimeRenderOrchestrator<
115
+ RuntimeLayout,
116
+ TConversation,
117
+ TerminalSnapshotFrameCore,
118
+ PaneSelection,
119
+ PaneSelectionDrag,
120
+ TRailViewRows
121
+ >;
122
+
123
+ constructor(
124
+ options: RuntimeRenderPipelineOptions<
125
+ TConversation,
126
+ TRepositoryRecord,
127
+ TTaskRecord,
128
+ TDirectoryRecord,
129
+ TRepositorySnapshot,
130
+ TGitSummary,
131
+ TProcessUsage,
132
+ TShortcutBindings,
133
+ TRailViewRows,
134
+ TModalOverlay,
135
+ TStatusRow
136
+ >,
137
+ ) {
138
+ const renderFlush = new RuntimeRenderFlush(options.renderFlush);
139
+ const rightPaneRender = new RuntimeRightPaneRender(options.rightPaneRender);
140
+ const leftRailRender = new RuntimeLeftRailRender(options.leftRailRender);
141
+ const renderState = new RuntimeRenderState(options.renderState);
142
+ this.renderOrchestrator = new RuntimeRenderOrchestrator({
143
+ isScreenDirty: options.isScreenDirty,
144
+ clearDirty: options.clearDirty,
145
+ prepareRenderState: (selection, selectionDrag) =>
146
+ renderState.prepareRenderState(selection, selectionDrag),
147
+ renderLeftRail: (layout) => leftRailRender.render(layout),
148
+ setLatestRailViewRows: options.setLatestRailViewRows,
149
+ renderRightRows: (input) =>
150
+ rightPaneRender.renderRightRows({
151
+ layout: input.layout,
152
+ rightFrame: input.rightFrame,
153
+ homePaneActive: input.homePaneActive,
154
+ projectPaneActive: input.projectPaneActive,
155
+ activeDirectoryId: input.activeDirectoryId,
156
+ }),
157
+ flushRender: (input) => {
158
+ renderFlush.flushRender(input);
159
+ },
160
+ activeDirectoryId: options.activeDirectoryId,
161
+ });
162
+ }
163
+
164
+ render(input: RuntimeRenderPipelineInput): void {
165
+ this.renderOrchestrator.render(input);
166
+ }
167
+ }
@@ -0,0 +1,72 @@
1
+ import type { WorkspaceModel } from '../domain/workspace.ts';
2
+ import type { PaneSelection, PaneSelectionDrag } from '../mux/live-mux/selection.ts';
3
+
4
+ interface RuntimeRenderStateOptions<TConversation, TFrame> {
5
+ readonly workspace: WorkspaceModel;
6
+ readonly hasDirectory: (directoryId: string) => boolean;
7
+ readonly activeConversationId: () => string | null;
8
+ readonly activeConversation: () => TConversation | null;
9
+ readonly snapshotFrame: (conversation: TConversation) => TFrame;
10
+ readonly selectionVisibleRows: (
11
+ frame: TFrame,
12
+ selection: PaneSelection | null,
13
+ ) => readonly number[];
14
+ }
15
+
16
+ interface RuntimeRenderStateResult<TConversation, TFrame> {
17
+ readonly projectPaneActive: boolean;
18
+ readonly homePaneActive: boolean;
19
+ readonly activeConversation: TConversation | null;
20
+ readonly rightFrame: TFrame | null;
21
+ readonly renderSelection: PaneSelection | null;
22
+ readonly selectionRows: readonly number[];
23
+ }
24
+
25
+ export class RuntimeRenderState<TConversation, TFrame> {
26
+ constructor(private readonly options: RuntimeRenderStateOptions<TConversation, TFrame>) {}
27
+
28
+ prepareRenderState(
29
+ selection: PaneSelection | null,
30
+ selectionDrag: PaneSelectionDrag | null,
31
+ ): RuntimeRenderStateResult<TConversation, TFrame> | null {
32
+ const workspace = this.options.workspace;
33
+ const projectPaneActive =
34
+ workspace.mainPaneMode === 'project' &&
35
+ workspace.activeDirectoryId !== null &&
36
+ this.options.hasDirectory(workspace.activeDirectoryId);
37
+ const homePaneActive = workspace.mainPaneMode === 'home';
38
+ if (!projectPaneActive && !homePaneActive && this.options.activeConversationId() === null) {
39
+ return null;
40
+ }
41
+
42
+ const activeConversation = this.options.activeConversation();
43
+ if (!projectPaneActive && !homePaneActive && activeConversation === null) {
44
+ return null;
45
+ }
46
+
47
+ const rightFrame =
48
+ !projectPaneActive && !homePaneActive && activeConversation !== null
49
+ ? this.options.snapshotFrame(activeConversation)
50
+ : null;
51
+ const renderSelection =
52
+ rightFrame !== null && selectionDrag !== null && selectionDrag.hasDragged
53
+ ? {
54
+ anchor: selectionDrag.anchor,
55
+ focus: selectionDrag.focus,
56
+ text: '',
57
+ }
58
+ : rightFrame !== null
59
+ ? selection
60
+ : null;
61
+ const selectionRows =
62
+ rightFrame === null ? [] : this.options.selectionVisibleRows(rightFrame, renderSelection);
63
+ return {
64
+ projectPaneActive,
65
+ homePaneActive,
66
+ activeConversation,
67
+ rightFrame,
68
+ renderSelection,
69
+ selectionRows,
70
+ };
71
+ }
72
+ }