@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,197 @@
1
+ import type { WorkspaceModel } from '../domain/workspace.ts';
2
+
3
+ interface RepositoryRecordShape {
4
+ readonly repositoryId: string;
5
+ readonly remoteUrl: string;
6
+ readonly metadata: Record<string, unknown>;
7
+ }
8
+
9
+ interface RuntimeRepositoryActionService<TRepository extends RepositoryRecordShape> {
10
+ upsertRepository(input: {
11
+ repositoryId?: string;
12
+ name: string;
13
+ remoteUrl: string;
14
+ defaultBranch?: string;
15
+ metadata?: Record<string, unknown>;
16
+ }): Promise<TRepository>;
17
+ updateRepository(input: {
18
+ repositoryId: string;
19
+ name?: string;
20
+ remoteUrl?: string;
21
+ defaultBranch?: string;
22
+ metadata?: Record<string, unknown>;
23
+ }): Promise<TRepository>;
24
+ archiveRepository(repositoryId: string): Promise<unknown>;
25
+ }
26
+
27
+ interface RuntimeRepositoryActionsOptions<TRepository extends RepositoryRecordShape> {
28
+ readonly workspace: WorkspaceModel;
29
+ readonly repositories: Map<string, TRepository>;
30
+ readonly controlPlaneService: RuntimeRepositoryActionService<TRepository>;
31
+ readonly normalizeGitHubRemoteUrl: (value: string) => string | null;
32
+ readonly repositoryNameFromGitHubRemoteUrl: (value: string) => string;
33
+ readonly createRepositoryId: () => string;
34
+ readonly stopConversationTitleEdit: () => void;
35
+ readonly syncRepositoryAssociationsWithDirectorySnapshots: () => void;
36
+ readonly syncTaskPaneRepositorySelection: () => void;
37
+ readonly queueControlPlaneOp: (task: () => Promise<void>, label: string) => void;
38
+ readonly markDirty: () => void;
39
+ }
40
+
41
+ function repositoryHomePriority(repository: RepositoryRecordShape): number | null {
42
+ const raw = repository.metadata['homePriority'];
43
+ if (typeof raw !== 'number' || !Number.isFinite(raw)) {
44
+ return null;
45
+ }
46
+ if (!Number.isInteger(raw) || raw < 0) {
47
+ return null;
48
+ }
49
+ return raw;
50
+ }
51
+
52
+ export class RuntimeRepositoryActions<TRepository extends RepositoryRecordShape> {
53
+ constructor(private readonly options: RuntimeRepositoryActionsOptions<TRepository>) {}
54
+
55
+ openRepositoryPromptForCreate(): void {
56
+ this.options.workspace.newThreadPrompt = null;
57
+ this.options.workspace.addDirectoryPrompt = null;
58
+ if (this.options.workspace.conversationTitleEdit !== null) {
59
+ this.options.stopConversationTitleEdit();
60
+ }
61
+ this.options.workspace.conversationTitleEditClickState = null;
62
+ this.options.workspace.repositoryPrompt = {
63
+ mode: 'add',
64
+ repositoryId: null,
65
+ value: '',
66
+ error: null,
67
+ };
68
+ this.options.markDirty();
69
+ }
70
+
71
+ openRepositoryPromptForEdit(repositoryId: string): void {
72
+ const repository = this.options.repositories.get(repositoryId);
73
+ if (repository === undefined) {
74
+ return;
75
+ }
76
+ this.options.workspace.newThreadPrompt = null;
77
+ this.options.workspace.addDirectoryPrompt = null;
78
+ if (this.options.workspace.conversationTitleEdit !== null) {
79
+ this.options.stopConversationTitleEdit();
80
+ }
81
+ this.options.workspace.conversationTitleEditClickState = null;
82
+ this.options.workspace.repositoryPrompt = {
83
+ mode: 'edit',
84
+ repositoryId,
85
+ value: repository.remoteUrl,
86
+ error: null,
87
+ };
88
+ this.options.workspace.taskPaneSelectionFocus = 'repository';
89
+ this.options.markDirty();
90
+ }
91
+
92
+ queueRepositoryPriorityOrder(orderedRepositoryIds: readonly string[], label: string): void {
93
+ const updates: Array<{ repositoryId: string; metadata: Record<string, unknown> }> = [];
94
+ for (let index = 0; index < orderedRepositoryIds.length; index += 1) {
95
+ const repositoryId = orderedRepositoryIds[index]!;
96
+ const repository = this.options.repositories.get(repositoryId);
97
+ if (repository === undefined) {
98
+ continue;
99
+ }
100
+ if (repositoryHomePriority(repository) === index) {
101
+ continue;
102
+ }
103
+ updates.push({
104
+ repositoryId,
105
+ metadata: {
106
+ ...repository.metadata,
107
+ homePriority: index,
108
+ },
109
+ });
110
+ }
111
+ if (updates.length === 0) {
112
+ return;
113
+ }
114
+ this.options.queueControlPlaneOp(async () => {
115
+ for (const update of updates) {
116
+ const repository = await this.options.controlPlaneService.updateRepository({
117
+ repositoryId: update.repositoryId,
118
+ metadata: update.metadata,
119
+ });
120
+ this.options.repositories.set(repository.repositoryId, repository);
121
+ }
122
+ this.options.syncTaskPaneRepositorySelection();
123
+ this.options.markDirty();
124
+ }, label);
125
+ }
126
+
127
+ reorderRepositoryByDrop(
128
+ draggedRepositoryId: string,
129
+ targetRepositoryId: string,
130
+ orderedRepositoryIds: readonly string[],
131
+ ): void {
132
+ const reordered = this.reorderIdsByMove(
133
+ orderedRepositoryIds,
134
+ draggedRepositoryId,
135
+ targetRepositoryId,
136
+ );
137
+ if (reordered === null) {
138
+ return;
139
+ }
140
+ this.queueRepositoryPriorityOrder(reordered, 'repositories-reorder-drag');
141
+ }
142
+
143
+ async upsertRepositoryByRemoteUrl(
144
+ remoteUrl: string,
145
+ existingRepositoryId?: string,
146
+ ): Promise<void> {
147
+ const normalizedRemoteUrl = this.options.normalizeGitHubRemoteUrl(remoteUrl);
148
+ if (normalizedRemoteUrl === null) {
149
+ throw new Error('github url required');
150
+ }
151
+ const repositoryName = this.options.repositoryNameFromGitHubRemoteUrl(normalizedRemoteUrl);
152
+ const repository =
153
+ existingRepositoryId === undefined
154
+ ? await this.options.controlPlaneService.upsertRepository({
155
+ repositoryId: this.options.createRepositoryId(),
156
+ name: repositoryName,
157
+ remoteUrl: normalizedRemoteUrl,
158
+ defaultBranch: 'main',
159
+ metadata: {
160
+ source: 'mux-manual',
161
+ },
162
+ })
163
+ : await this.options.controlPlaneService.updateRepository({
164
+ repositoryId: existingRepositoryId,
165
+ name: repositoryName,
166
+ remoteUrl: normalizedRemoteUrl,
167
+ });
168
+ this.options.repositories.set(repository.repositoryId, repository);
169
+ this.options.syncRepositoryAssociationsWithDirectorySnapshots();
170
+ this.options.syncTaskPaneRepositorySelection();
171
+ this.options.markDirty();
172
+ }
173
+
174
+ async archiveRepositoryById(repositoryId: string): Promise<void> {
175
+ await this.options.controlPlaneService.archiveRepository(repositoryId);
176
+ this.options.repositories.delete(repositoryId);
177
+ this.options.syncRepositoryAssociationsWithDirectorySnapshots();
178
+ this.options.syncTaskPaneRepositorySelection();
179
+ this.options.markDirty();
180
+ }
181
+
182
+ private reorderIdsByMove(
183
+ orderedIds: readonly string[],
184
+ movedId: string,
185
+ targetId: string,
186
+ ): readonly string[] | null {
187
+ const fromIndex = orderedIds.indexOf(movedId);
188
+ const targetIndex = orderedIds.indexOf(targetId);
189
+ if (fromIndex < 0 || targetIndex < 0 || fromIndex === targetIndex) {
190
+ return null;
191
+ }
192
+ const reordered = [...orderedIds];
193
+ const moved = reordered.splice(fromIndex, 1)[0]!;
194
+ reordered.splice(targetIndex, 0, moved);
195
+ return reordered;
196
+ }
197
+ }
@@ -0,0 +1,132 @@
1
+ import type { WorkspaceModel } from '../domain/workspace.ts';
2
+ import type { TaskManager } from '../domain/tasks.ts';
3
+ import type { ProjectPaneSnapshot } from '../mux/harness-core-ui.ts';
4
+ import type { TaskComposerBuffer } from '../mux/task-composer.ts';
5
+ import type {
6
+ TaskFocusedPaneRepositoryRecord,
7
+ TaskFocusedPaneTaskRecord,
8
+ TaskFocusedPaneView,
9
+ } from '../mux/task-focused-pane.ts';
10
+ import type { TerminalSnapshotFrameCore } from '../terminal/snapshot-oracle.ts';
11
+
12
+ interface RuntimeRightPaneLayout {
13
+ readonly rightCols: number;
14
+ readonly paneRows: number;
15
+ }
16
+
17
+ interface ConversationPaneLike {
18
+ render(frame: TerminalSnapshotFrameCore, layout: RuntimeRightPaneLayout): readonly string[];
19
+ }
20
+
21
+ interface HomePaneRenderInput<
22
+ TRepositoryRecord extends TaskFocusedPaneRepositoryRecord,
23
+ TTaskRecord extends TaskFocusedPaneTaskRecord,
24
+ > {
25
+ readonly layout: RuntimeRightPaneLayout;
26
+ readonly repositories: ReadonlyMap<string, TRepositoryRecord>;
27
+ readonly tasks: ReadonlyMap<string, TTaskRecord>;
28
+ readonly selectedRepositoryId: string | null;
29
+ readonly repositoryDropdownOpen: boolean;
30
+ readonly editorTarget: WorkspaceModel['taskEditorTarget'];
31
+ readonly draftBuffer: TaskComposerBuffer;
32
+ readonly taskBufferById: ReadonlyMap<string, TaskComposerBuffer>;
33
+ readonly notice: string | null;
34
+ readonly scrollTop: number;
35
+ }
36
+
37
+ interface HomePaneLike<
38
+ TRepositoryRecord extends TaskFocusedPaneRepositoryRecord,
39
+ TTaskRecord extends TaskFocusedPaneTaskRecord,
40
+ > {
41
+ render(input: HomePaneRenderInput<TRepositoryRecord, TTaskRecord>): TaskFocusedPaneView;
42
+ }
43
+
44
+ interface ProjectPaneLike {
45
+ render(input: {
46
+ layout: RuntimeRightPaneLayout;
47
+ snapshot: ProjectPaneSnapshot | null;
48
+ scrollTop: number;
49
+ }): {
50
+ readonly rows: readonly string[];
51
+ readonly scrollTop: number;
52
+ };
53
+ }
54
+
55
+ interface RuntimeRightPaneRenderInput {
56
+ readonly layout: RuntimeRightPaneLayout;
57
+ readonly rightFrame: TerminalSnapshotFrameCore | null;
58
+ readonly homePaneActive: boolean;
59
+ readonly projectPaneActive: boolean;
60
+ readonly activeDirectoryId: string | null;
61
+ }
62
+
63
+ interface RuntimeRightPaneRenderOptions<
64
+ TRepositoryRecord extends TaskFocusedPaneRepositoryRecord,
65
+ TTaskRecord extends TaskFocusedPaneTaskRecord,
66
+ > {
67
+ readonly workspace: WorkspaceModel;
68
+ readonly repositories: ReadonlyMap<string, TRepositoryRecord>;
69
+ readonly taskManager: TaskManager<TTaskRecord, TaskComposerBuffer, NodeJS.Timeout>;
70
+ readonly conversationPane: ConversationPaneLike;
71
+ readonly homePane: HomePaneLike<TRepositoryRecord, TTaskRecord>;
72
+ readonly projectPane: ProjectPaneLike;
73
+ readonly refreshProjectPaneSnapshot: (directoryId: string) => ProjectPaneSnapshot | null;
74
+ readonly emptyTaskPaneView: () => TaskFocusedPaneView;
75
+ }
76
+
77
+ export class RuntimeRightPaneRender<
78
+ TRepositoryRecord extends TaskFocusedPaneRepositoryRecord,
79
+ TTaskRecord extends TaskFocusedPaneTaskRecord,
80
+ > {
81
+ constructor(
82
+ private readonly options: RuntimeRightPaneRenderOptions<TRepositoryRecord, TTaskRecord>,
83
+ ) {}
84
+
85
+ renderRightRows(input: RuntimeRightPaneRenderInput): readonly string[] {
86
+ const workspace = this.options.workspace;
87
+ workspace.latestTaskPaneView = this.options.emptyTaskPaneView();
88
+
89
+ if (input.rightFrame !== null) {
90
+ return this.options.conversationPane.render(input.rightFrame, input.layout);
91
+ }
92
+
93
+ if (input.homePaneActive) {
94
+ const view = this.options.homePane.render({
95
+ layout: input.layout,
96
+ repositories: this.options.repositories,
97
+ tasks: this.options.taskManager.readonlyTasks(),
98
+ selectedRepositoryId: workspace.taskPaneSelectedRepositoryId,
99
+ repositoryDropdownOpen: workspace.taskRepositoryDropdownOpen,
100
+ editorTarget: workspace.taskEditorTarget,
101
+ draftBuffer: workspace.taskDraftComposer,
102
+ taskBufferById: this.options.taskManager.readonlyTaskComposers(),
103
+ notice: workspace.taskPaneNotice,
104
+ scrollTop: workspace.taskPaneScrollTop,
105
+ });
106
+ workspace.taskPaneSelectedRepositoryId = view.selectedRepositoryId;
107
+ workspace.taskPaneScrollTop = view.top;
108
+ workspace.latestTaskPaneView = view;
109
+ return view.rows;
110
+ }
111
+
112
+ if (input.projectPaneActive && input.activeDirectoryId !== null) {
113
+ const needsSnapshotRefresh =
114
+ workspace.projectPaneSnapshot === null ||
115
+ workspace.projectPaneSnapshot.directoryId !== input.activeDirectoryId;
116
+ if (needsSnapshotRefresh) {
117
+ workspace.projectPaneSnapshot = this.options.refreshProjectPaneSnapshot(
118
+ input.activeDirectoryId,
119
+ );
120
+ }
121
+ const view = this.options.projectPane.render({
122
+ layout: input.layout,
123
+ snapshot: workspace.projectPaneSnapshot,
124
+ scrollTop: workspace.projectPaneScrollTop,
125
+ });
126
+ workspace.projectPaneScrollTop = view.scrollTop;
127
+ return view.rows;
128
+ }
129
+
130
+ return Array.from({ length: input.layout.paneRows }, () => ' '.repeat(input.layout.rightCols));
131
+ }
132
+ }
@@ -0,0 +1,79 @@
1
+ interface RuntimeShutdownServiceOptions {
2
+ readonly screen: {
3
+ clearDirty: () => void;
4
+ };
5
+ readonly outputLoadSampler: {
6
+ stop: () => void;
7
+ };
8
+ readonly startupBackgroundProbeService: {
9
+ stop: () => void;
10
+ };
11
+ readonly clearResizeTimer: () => void;
12
+ readonly clearPtyResizeTimer: () => void;
13
+ readonly clearHomePaneBackgroundTimer: () => void;
14
+ readonly persistMuxUiStateNow: () => void;
15
+ readonly clearConversationTitleEditTimer: () => void;
16
+ readonly flushTaskComposerPersist: () => void;
17
+ readonly clearRenderScheduled: () => void;
18
+ readonly detachProcessListeners: () => void;
19
+ readonly removeEnvelopeListener: () => void;
20
+ readonly unsubscribeTaskPlanningEvents: () => Promise<void>;
21
+ readonly closeKeyEventSubscription: () => Promise<void>;
22
+ readonly clearRuntimeFatalExitTimer: () => void;
23
+ readonly waitForControlPlaneDrain: () => Promise<void>;
24
+ readonly controlPlaneClient: {
25
+ close: () => Promise<void>;
26
+ };
27
+ readonly eventPersistence: {
28
+ flush: (reason: 'timer' | 'immediate' | 'shutdown') => void;
29
+ };
30
+ readonly recordingService: {
31
+ closeWriter: () => Promise<unknown>;
32
+ finalizeAfterShutdown: (recordingCloseError: unknown) => Promise<void>;
33
+ };
34
+ readonly store: {
35
+ close: () => void;
36
+ };
37
+ readonly restoreTerminalState: () => void;
38
+ readonly startupShutdownService: {
39
+ finalize: () => void;
40
+ };
41
+ readonly shutdownPerfCore: () => void;
42
+ }
43
+
44
+ export class RuntimeShutdownService {
45
+ constructor(private readonly options: RuntimeShutdownServiceOptions) {}
46
+
47
+ async finalize(): Promise<void> {
48
+ this.options.screen.clearDirty();
49
+ this.options.outputLoadSampler.stop();
50
+ this.options.startupBackgroundProbeService.stop();
51
+ this.options.clearResizeTimer();
52
+ this.options.clearPtyResizeTimer();
53
+ this.options.clearHomePaneBackgroundTimer();
54
+ this.options.persistMuxUiStateNow();
55
+ this.options.clearConversationTitleEditTimer();
56
+ this.options.flushTaskComposerPersist();
57
+ this.options.clearRenderScheduled();
58
+ this.options.detachProcessListeners();
59
+ this.options.removeEnvelopeListener();
60
+ await this.options.unsubscribeTaskPlanningEvents();
61
+ await this.options.closeKeyEventSubscription();
62
+ this.options.clearRuntimeFatalExitTimer();
63
+
64
+ try {
65
+ await this.options.waitForControlPlaneDrain();
66
+ await this.options.controlPlaneClient.close();
67
+ } catch {
68
+ // Best-effort shutdown only.
69
+ }
70
+
71
+ this.options.eventPersistence.flush('shutdown');
72
+ const recordingCloseError = await this.options.recordingService.closeWriter();
73
+ this.options.store.close();
74
+ this.options.restoreTerminalState();
75
+ await this.options.recordingService.finalizeAfterShutdown(recordingCloseError);
76
+ this.options.startupShutdownService.finalize();
77
+ this.options.shutdownPerfCore();
78
+ }
79
+ }
@@ -0,0 +1,56 @@
1
+ export interface RuntimeStreamSubscriptionsOptions {
2
+ readonly subscribePtyEvents: (sessionId: string) => Promise<void>;
3
+ readonly unsubscribePtyEvents: (sessionId: string) => Promise<void>;
4
+ readonly isSessionNotFoundError: (error: unknown) => boolean;
5
+ readonly isSessionNotLiveError: (error: unknown) => boolean;
6
+ readonly subscribeObservedStream: (afterCursor: number | null) => Promise<string>;
7
+ readonly unsubscribeObservedStream: (subscriptionId: string) => Promise<void>;
8
+ }
9
+
10
+ export class RuntimeStreamSubscriptions {
11
+ private observedStreamSubscriptionId: string | null = null;
12
+
13
+ constructor(private readonly options: RuntimeStreamSubscriptionsOptions) {}
14
+
15
+ async subscribeConversationEvents(sessionId: string): Promise<void> {
16
+ try {
17
+ await this.options.subscribePtyEvents(sessionId);
18
+ } catch (error: unknown) {
19
+ if (
20
+ !this.options.isSessionNotFoundError(error) &&
21
+ !this.options.isSessionNotLiveError(error)
22
+ ) {
23
+ throw error;
24
+ }
25
+ }
26
+ }
27
+
28
+ async unsubscribeConversationEvents(sessionId: string): Promise<void> {
29
+ try {
30
+ await this.options.unsubscribePtyEvents(sessionId);
31
+ } catch (error: unknown) {
32
+ if (
33
+ !this.options.isSessionNotFoundError(error) &&
34
+ !this.options.isSessionNotLiveError(error)
35
+ ) {
36
+ throw error;
37
+ }
38
+ }
39
+ }
40
+
41
+ async subscribeTaskPlanningEvents(afterCursor: number | null): Promise<void> {
42
+ if (this.observedStreamSubscriptionId !== null) {
43
+ return;
44
+ }
45
+ this.observedStreamSubscriptionId = await this.options.subscribeObservedStream(afterCursor);
46
+ }
47
+
48
+ async unsubscribeTaskPlanningEvents(): Promise<void> {
49
+ if (this.observedStreamSubscriptionId === null) {
50
+ return;
51
+ }
52
+ const subscriptionId = this.observedStreamSubscriptionId;
53
+ this.observedStreamSubscriptionId = null;
54
+ await this.options.unsubscribeObservedStream(subscriptionId);
55
+ }
56
+ }
@@ -0,0 +1,139 @@
1
+ interface TaskComposerFields {
2
+ readonly title: string;
3
+ readonly description: string;
4
+ }
5
+
6
+ interface TaskRecordShape {
7
+ readonly taskId: string;
8
+ readonly repositoryId: string | null;
9
+ readonly title: string;
10
+ readonly description: string;
11
+ }
12
+
13
+ interface TaskComposerBufferShape {
14
+ readonly text: string;
15
+ }
16
+
17
+ interface RuntimeTaskComposerPersistenceOptions<
18
+ TTaskRecord extends TaskRecordShape,
19
+ TTaskComposerBuffer extends TaskComposerBufferShape,
20
+ TTaskAutosaveTimer extends { unref?: () => void },
21
+ > {
22
+ readonly getTask: (taskId: string) => TTaskRecord | undefined;
23
+ readonly getTaskComposer: (taskId: string) => TTaskComposerBuffer | undefined;
24
+ readonly setTaskComposer: (taskId: string, buffer: TTaskComposerBuffer) => void;
25
+ readonly deleteTaskComposer: (taskId: string) => void;
26
+ readonly getTaskAutosaveTimer: (taskId: string) => TTaskAutosaveTimer | undefined;
27
+ readonly setTaskAutosaveTimer: (taskId: string, timer: TTaskAutosaveTimer) => void;
28
+ readonly deleteTaskAutosaveTimer: (taskId: string) => void;
29
+ readonly buildComposerFromTask: (task: TTaskRecord) => TTaskComposerBuffer;
30
+ readonly normalizeTaskComposerBuffer: (buffer: TTaskComposerBuffer) => TTaskComposerBuffer;
31
+ readonly taskFieldsFromComposerText: (text: string) => TaskComposerFields;
32
+ readonly updateTask: (input: {
33
+ taskId: string;
34
+ repositoryId: string | null;
35
+ title: string;
36
+ description: string;
37
+ }) => Promise<TTaskRecord>;
38
+ readonly applyTaskRecord: (task: TTaskRecord) => void;
39
+ readonly queueControlPlaneOp: (task: () => Promise<void>, label: string) => void;
40
+ readonly setTaskPaneNotice: (text: string | null) => void;
41
+ readonly markDirty: () => void;
42
+ readonly autosaveDebounceMs: number;
43
+ readonly setTimeoutFn?: (callback: () => void, ms: number) => TTaskAutosaveTimer;
44
+ readonly clearTimeoutFn?: (timer: TTaskAutosaveTimer) => void;
45
+ }
46
+
47
+ export class RuntimeTaskComposerPersistenceService<
48
+ TTaskRecord extends TaskRecordShape,
49
+ TTaskComposerBuffer extends TaskComposerBufferShape,
50
+ TTaskAutosaveTimer extends { unref?: () => void } = NodeJS.Timeout,
51
+ > {
52
+ private readonly setTimeoutFn: (callback: () => void, ms: number) => TTaskAutosaveTimer;
53
+ private readonly clearTimeoutFn: (timer: TTaskAutosaveTimer) => void;
54
+
55
+ constructor(
56
+ private readonly options: RuntimeTaskComposerPersistenceOptions<
57
+ TTaskRecord,
58
+ TTaskComposerBuffer,
59
+ TTaskAutosaveTimer
60
+ >,
61
+ ) {
62
+ this.setTimeoutFn =
63
+ options.setTimeoutFn ??
64
+ ((callback, ms) => setTimeout(callback, ms) as unknown as TTaskAutosaveTimer);
65
+ this.clearTimeoutFn =
66
+ options.clearTimeoutFn ?? ((timer) => clearTimeout(timer as unknown as NodeJS.Timeout));
67
+ }
68
+
69
+ taskComposerForTask(taskId: string): TTaskComposerBuffer | null {
70
+ const existing = this.options.getTaskComposer(taskId);
71
+ if (existing !== undefined) {
72
+ return existing;
73
+ }
74
+ const task = this.options.getTask(taskId);
75
+ if (task === undefined) {
76
+ return null;
77
+ }
78
+ return this.options.buildComposerFromTask(task);
79
+ }
80
+
81
+ setTaskComposerForTask(taskId: string, buffer: TTaskComposerBuffer): void {
82
+ this.options.setTaskComposer(taskId, this.options.normalizeTaskComposerBuffer(buffer));
83
+ }
84
+
85
+ clearTaskAutosaveTimer(taskId: string): void {
86
+ const timer = this.options.getTaskAutosaveTimer(taskId);
87
+ if (timer !== undefined) {
88
+ this.clearTimeoutFn(timer);
89
+ this.options.deleteTaskAutosaveTimer(taskId);
90
+ }
91
+ }
92
+
93
+ scheduleTaskComposerPersist(taskId: string): void {
94
+ this.clearTaskAutosaveTimer(taskId);
95
+ const timer = this.setTimeoutFn(() => {
96
+ this.options.deleteTaskAutosaveTimer(taskId);
97
+ this.queuePersistTaskComposer(taskId, 'debounced');
98
+ }, this.options.autosaveDebounceMs);
99
+ timer.unref?.();
100
+ this.options.setTaskAutosaveTimer(taskId, timer);
101
+ }
102
+
103
+ flushTaskComposerPersist(taskId: string): void {
104
+ this.clearTaskAutosaveTimer(taskId);
105
+ this.queuePersistTaskComposer(taskId, 'flush');
106
+ }
107
+
108
+ private queuePersistTaskComposer(taskId: string, reason: 'debounced' | 'flush'): void {
109
+ const task = this.options.getTask(taskId);
110
+ const buffer = this.options.getTaskComposer(taskId);
111
+ if (task === undefined || buffer === undefined) {
112
+ return;
113
+ }
114
+ const fields = this.options.taskFieldsFromComposerText(buffer.text);
115
+ if (fields.title.length === 0) {
116
+ this.options.setTaskPaneNotice('first line is required');
117
+ this.options.markDirty();
118
+ return;
119
+ }
120
+ if (fields.title === task.title && fields.description === task.description) {
121
+ return;
122
+ }
123
+ this.options.queueControlPlaneOp(async () => {
124
+ const parsed = await this.options.updateTask({
125
+ taskId,
126
+ repositoryId: task.repositoryId,
127
+ title: fields.title,
128
+ description: fields.description,
129
+ });
130
+ this.options.applyTaskRecord(parsed);
131
+ const persistedText =
132
+ parsed.description.length === 0 ? parsed.title : `${parsed.title}\n${parsed.description}`;
133
+ const latestBuffer = this.options.getTaskComposer(taskId);
134
+ if (latestBuffer !== undefined && latestBuffer.text === persistedText) {
135
+ this.options.deleteTaskComposer(taskId);
136
+ }
137
+ }, `task-editor-save:${reason}:${taskId}`);
138
+ }
139
+ }
@@ -0,0 +1,83 @@
1
+ import type { WorkspaceModel } from '../domain/workspace.ts';
2
+
3
+ interface TaskRecordShape {
4
+ readonly taskId: string;
5
+ }
6
+
7
+ interface RuntimeTaskEditorActionService<TTaskRecord extends TaskRecordShape> {
8
+ createTask(input: {
9
+ repositoryId: string;
10
+ title: string;
11
+ description: string;
12
+ }): Promise<TTaskRecord>;
13
+ updateTask(input: {
14
+ taskId: string;
15
+ repositoryId: string;
16
+ title: string;
17
+ description: string;
18
+ }): Promise<TTaskRecord>;
19
+ }
20
+
21
+ export interface RuntimeTaskEditorSubmitPayload {
22
+ readonly mode: 'create' | 'edit';
23
+ readonly taskId: string | null;
24
+ readonly repositoryId: string;
25
+ readonly title: string;
26
+ readonly description: string;
27
+ readonly commandLabel: string;
28
+ }
29
+
30
+ interface RuntimeTaskEditorActionsOptions<TTaskRecord extends TaskRecordShape> {
31
+ readonly workspace: WorkspaceModel;
32
+ readonly controlPlaneService: RuntimeTaskEditorActionService<TTaskRecord>;
33
+ readonly applyTaskRecord: (task: TTaskRecord) => void;
34
+ readonly queueControlPlaneOp: (task: () => Promise<void>, label: string) => void;
35
+ readonly markDirty: () => void;
36
+ }
37
+
38
+ function formatErrorMessage(error: unknown): string {
39
+ return error instanceof Error ? error.message : String(error);
40
+ }
41
+
42
+ export class RuntimeTaskEditorActions<TTaskRecord extends TaskRecordShape> {
43
+ constructor(private readonly options: RuntimeTaskEditorActionsOptions<TTaskRecord>) {}
44
+
45
+ submitTaskEditorPayload(payload: RuntimeTaskEditorSubmitPayload): void {
46
+ this.options.queueControlPlaneOp(async () => {
47
+ try {
48
+ if (payload.mode === 'create') {
49
+ this.options.applyTaskRecord(
50
+ await this.options.controlPlaneService.createTask({
51
+ repositoryId: payload.repositoryId,
52
+ title: payload.title,
53
+ description: payload.description,
54
+ }),
55
+ );
56
+ } else {
57
+ if (payload.taskId === null) {
58
+ throw new Error('task edit state missing task id');
59
+ }
60
+ this.options.applyTaskRecord(
61
+ await this.options.controlPlaneService.updateTask({
62
+ taskId: payload.taskId,
63
+ repositoryId: payload.repositoryId,
64
+ title: payload.title,
65
+ description: payload.description,
66
+ }),
67
+ );
68
+ }
69
+ this.options.workspace.taskEditorPrompt = null;
70
+ this.options.workspace.taskPaneNotice = null;
71
+ } catch (error: unknown) {
72
+ const message = formatErrorMessage(error);
73
+ if (this.options.workspace.taskEditorPrompt !== null) {
74
+ this.options.workspace.taskEditorPrompt.error = message;
75
+ } else {
76
+ this.options.workspace.taskPaneNotice = message;
77
+ }
78
+ } finally {
79
+ this.options.markDirty();
80
+ }
81
+ }, payload.commandLabel);
82
+ }
83
+ }