@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,198 @@
1
+ import type { WorkspaceModel } from '../domain/workspace.ts';
2
+ import type { TaskPaneAction } from '../mux/harness-core-ui.ts';
3
+ import { runTaskPaneAction as runTaskPaneActionFrame } from '../mux/live-mux/actions-task.ts';
4
+
5
+ interface TaskRecordShape {
6
+ readonly taskId: string;
7
+ readonly repositoryId: string | null;
8
+ readonly status: string;
9
+ }
10
+
11
+ interface RuntimeTaskPaneActionService<TTaskRecord extends TaskRecordShape> {
12
+ reorderTasks(orderedTaskIds: readonly string[]): Promise<readonly TTaskRecord[]>;
13
+ deleteTask(taskId: string): Promise<unknown>;
14
+ taskReady(taskId: string): Promise<TTaskRecord>;
15
+ taskDraft(taskId: string): Promise<TTaskRecord>;
16
+ taskComplete(taskId: string): Promise<TTaskRecord>;
17
+ }
18
+
19
+ interface RuntimeTaskPaneActionsOptions<TTaskRecord extends TaskRecordShape> {
20
+ readonly workspace: WorkspaceModel;
21
+ readonly controlPlaneService: RuntimeTaskPaneActionService<TTaskRecord>;
22
+ readonly repositoriesHas: (repositoryId: string) => boolean;
23
+ readonly setTask: (task: TTaskRecord) => void;
24
+ readonly getTask: (taskId: string) => TTaskRecord | undefined;
25
+ readonly taskReorderPayloadIds: (orderedActiveTaskIds: readonly string[]) => readonly string[];
26
+ readonly reorderedActiveTaskIdsForDrop: (
27
+ draggedTaskId: string,
28
+ targetTaskId: string,
29
+ ) => readonly string[] | 'cannot-reorder-completed' | null;
30
+ readonly clearTaskAutosaveTimer: (taskId: string) => void;
31
+ readonly deleteTask: (taskId: string) => void;
32
+ readonly deleteTaskComposer: (taskId: string) => void;
33
+ readonly focusDraftComposer: () => void;
34
+ readonly focusTaskComposer: (taskId: string) => void;
35
+ readonly selectedTask: () => TTaskRecord | null;
36
+ readonly orderedTaskRecords: () => readonly TTaskRecord[];
37
+ readonly queueControlPlaneOp: (task: () => Promise<void>, label?: string) => void;
38
+ readonly syncTaskPaneSelection: () => void;
39
+ readonly syncTaskPaneRepositorySelection: () => void;
40
+ readonly openRepositoryPromptForCreate: () => void;
41
+ readonly openRepositoryPromptForEdit: (repositoryId: string) => void;
42
+ readonly archiveRepositoryById: (repositoryId: string) => Promise<void>;
43
+ readonly markDirty: () => void;
44
+ readonly runTaskPaneAction?: typeof runTaskPaneActionFrame;
45
+ }
46
+
47
+ export class RuntimeTaskPaneActions<TTaskRecord extends TaskRecordShape> {
48
+ private readonly runTaskPaneActionFn: typeof runTaskPaneActionFrame;
49
+
50
+ constructor(private readonly options: RuntimeTaskPaneActionsOptions<TTaskRecord>) {
51
+ this.runTaskPaneActionFn = options.runTaskPaneAction ?? runTaskPaneActionFrame;
52
+ }
53
+
54
+ openTaskCreatePrompt(): void {
55
+ const repositoryId = this.options.workspace.taskPaneSelectedRepositoryId;
56
+ if (repositoryId === null || !this.options.repositoriesHas(repositoryId)) {
57
+ this.options.workspace.taskPaneNotice = 'select a repository first';
58
+ this.options.markDirty();
59
+ return;
60
+ }
61
+ this.options.focusDraftComposer();
62
+ this.options.workspace.taskPaneNotice = null;
63
+ this.options.markDirty();
64
+ }
65
+
66
+ openTaskEditPrompt(taskId: string): void {
67
+ const task = this.options.getTask(taskId);
68
+ if (task === undefined) {
69
+ return;
70
+ }
71
+ if (task.repositoryId !== null) {
72
+ this.options.workspace.taskPaneSelectedRepositoryId = task.repositoryId;
73
+ }
74
+ this.options.focusTaskComposer(task.taskId);
75
+ this.options.workspace.taskPaneNotice = null;
76
+ this.options.markDirty();
77
+ }
78
+
79
+ applyTaskRecord(task: TTaskRecord): TTaskRecord {
80
+ this.options.setTask(task);
81
+ this.options.workspace.taskPaneSelectedTaskId = task.taskId;
82
+ if (task.repositoryId !== null && this.options.repositoriesHas(task.repositoryId)) {
83
+ this.options.workspace.taskPaneSelectedRepositoryId = task.repositoryId;
84
+ }
85
+ this.options.workspace.taskPaneSelectionFocus = 'task';
86
+ this.options.syncTaskPaneSelection();
87
+ this.options.markDirty();
88
+ return task;
89
+ }
90
+
91
+ applyTaskList(tasks: readonly TTaskRecord[]): boolean {
92
+ let changed = false;
93
+ for (const task of tasks) {
94
+ this.options.setTask(task);
95
+ changed = true;
96
+ }
97
+ if (changed) {
98
+ this.options.syncTaskPaneSelection();
99
+ this.options.markDirty();
100
+ }
101
+ return changed;
102
+ }
103
+
104
+ queueTaskReorderByIds(orderedActiveTaskIds: readonly string[], label: string): void {
105
+ this.options.queueControlPlaneOp(async () => {
106
+ const tasks = await this.options.controlPlaneService.reorderTasks(
107
+ this.options.taskReorderPayloadIds(orderedActiveTaskIds),
108
+ );
109
+ this.applyTaskList(tasks);
110
+ }, label);
111
+ }
112
+
113
+ reorderTaskByDrop(draggedTaskId: string, targetTaskId: string): void {
114
+ const reordered = this.options.reorderedActiveTaskIdsForDrop(draggedTaskId, targetTaskId);
115
+ if (reordered === 'cannot-reorder-completed') {
116
+ this.options.workspace.taskPaneNotice = 'cannot reorder completed tasks';
117
+ this.options.markDirty();
118
+ return;
119
+ }
120
+ if (reordered === null) {
121
+ return;
122
+ }
123
+ this.queueTaskReorderByIds(reordered, 'tasks-reorder-drag');
124
+ }
125
+
126
+ runTaskPaneAction(action: TaskPaneAction): void {
127
+ this.runTaskPaneActionFn({
128
+ action,
129
+ openTaskCreatePrompt: () => {
130
+ this.openTaskCreatePrompt();
131
+ },
132
+ openRepositoryPromptForCreate: () => {
133
+ this.options.openRepositoryPromptForCreate();
134
+ },
135
+ selectedRepositoryId: this.options.workspace.taskPaneSelectedRepositoryId,
136
+ repositoryExists: (repositoryId) => this.options.repositoriesHas(repositoryId),
137
+ setTaskPaneNotice: (notice) => {
138
+ this.options.workspace.taskPaneNotice = notice;
139
+ },
140
+ markDirty: () => {
141
+ this.options.markDirty();
142
+ },
143
+ setTaskPaneSelectionFocus: (focus) => {
144
+ this.options.workspace.taskPaneSelectionFocus = focus;
145
+ },
146
+ openRepositoryPromptForEdit: (repositoryId) => {
147
+ this.options.openRepositoryPromptForEdit(repositoryId);
148
+ },
149
+ queueArchiveRepository: (repositoryId) => {
150
+ this.options.queueControlPlaneOp(async () => {
151
+ await this.options.archiveRepositoryById(repositoryId);
152
+ this.options.syncTaskPaneRepositorySelection();
153
+ }, 'tasks-archive-repository');
154
+ },
155
+ selectedTask: this.options.selectedTask(),
156
+ openTaskEditPrompt: (taskId) => {
157
+ this.openTaskEditPrompt(taskId);
158
+ },
159
+ queueDeleteTask: (taskId) => {
160
+ this.options.queueControlPlaneOp(async () => {
161
+ this.options.clearTaskAutosaveTimer(taskId);
162
+ await this.options.controlPlaneService.deleteTask(taskId);
163
+ this.options.deleteTask(taskId);
164
+ this.options.deleteTaskComposer(taskId);
165
+ if (
166
+ this.options.workspace.taskEditorTarget.kind === 'task' &&
167
+ this.options.workspace.taskEditorTarget.taskId === taskId
168
+ ) {
169
+ this.options.workspace.taskEditorTarget = {
170
+ kind: 'draft',
171
+ };
172
+ }
173
+ this.options.syncTaskPaneSelection();
174
+ this.options.markDirty();
175
+ }, 'tasks-delete');
176
+ },
177
+ queueTaskReady: (taskId) => {
178
+ this.options.queueControlPlaneOp(async () => {
179
+ this.applyTaskRecord(await this.options.controlPlaneService.taskReady(taskId));
180
+ }, 'tasks-ready');
181
+ },
182
+ queueTaskDraft: (taskId) => {
183
+ this.options.queueControlPlaneOp(async () => {
184
+ this.applyTaskRecord(await this.options.controlPlaneService.taskDraft(taskId));
185
+ }, 'tasks-draft');
186
+ },
187
+ queueTaskComplete: (taskId) => {
188
+ this.options.queueControlPlaneOp(async () => {
189
+ this.applyTaskRecord(await this.options.controlPlaneService.taskComplete(taskId));
190
+ }, 'tasks-complete');
191
+ },
192
+ orderedTaskRecords: () => this.options.orderedTaskRecords(),
193
+ queueTaskReorderByIds: (orderedTaskIds, label) => {
194
+ this.queueTaskReorderByIds(orderedTaskIds, label);
195
+ },
196
+ });
197
+ }
198
+ }
@@ -0,0 +1,189 @@
1
+ import type { WorkspaceModel } from '../domain/workspace.ts';
2
+ import {
3
+ createTaskComposerBuffer as createTaskComposerBufferFrame,
4
+ normalizeTaskComposerBuffer as normalizeTaskComposerBufferFrame,
5
+ taskFieldsFromComposerText as taskFieldsFromComposerTextFrame,
6
+ type TaskComposerBuffer,
7
+ } from '../mux/task-composer.ts';
8
+ import { handleTaskPaneShortcutInput as handleTaskPaneShortcutInputFrame } from '../mux/live-mux/task-pane-shortcuts.ts';
9
+ import type { ResolvedTaskScreenKeybindings } from '../mux/task-screen-keybindings.ts';
10
+
11
+ type TaskPaneShortcutAction = Parameters<
12
+ Parameters<typeof handleTaskPaneShortcutInputFrame>[0]['runTaskPaneAction']
13
+ >[0];
14
+
15
+ interface TaskRecordShape {
16
+ readonly taskId: string;
17
+ readonly repositoryId: string | null;
18
+ readonly title: string;
19
+ readonly description: string;
20
+ }
21
+
22
+ interface RuntimeTaskPaneShortcutsOptions<TTaskRecord extends TaskRecordShape> {
23
+ readonly workspace: WorkspaceModel;
24
+ readonly taskScreenKeybindings: ResolvedTaskScreenKeybindings;
25
+ readonly repositoriesHas: (repositoryId: string) => boolean;
26
+ readonly activeRepositoryIds: () => readonly string[];
27
+ readonly selectRepositoryById: (repositoryId: string) => void;
28
+ readonly taskComposerForTask: (taskId: string) => TaskComposerBuffer | null;
29
+ readonly setTaskComposerForTask: (taskId: string, buffer: TaskComposerBuffer) => void;
30
+ readonly scheduleTaskComposerPersist: (taskId: string) => void;
31
+ readonly selectedRepositoryTaskRecords: () => readonly TTaskRecord[];
32
+ readonly focusTaskComposer: (taskId: string) => void;
33
+ readonly focusDraftComposer: () => void;
34
+ readonly runTaskPaneAction: (action: TaskPaneShortcutAction) => void;
35
+ readonly queueControlPlaneOp: (task: () => Promise<void>, label?: string) => void;
36
+ readonly createTask: (payload: {
37
+ repositoryId: string;
38
+ title: string;
39
+ description: string;
40
+ }) => Promise<TTaskRecord>;
41
+ readonly applyTaskRecord: (task: TTaskRecord) => void;
42
+ readonly syncTaskPaneSelection: () => void;
43
+ readonly markDirty: () => void;
44
+ readonly createTaskComposerBuffer?: typeof createTaskComposerBufferFrame;
45
+ readonly normalizeTaskComposerBuffer?: typeof normalizeTaskComposerBufferFrame;
46
+ readonly taskFieldsFromComposerText?: typeof taskFieldsFromComposerTextFrame;
47
+ readonly handleTaskPaneShortcutInput?: typeof handleTaskPaneShortcutInputFrame;
48
+ }
49
+
50
+ export class RuntimeTaskPaneShortcuts<TTaskRecord extends TaskRecordShape> {
51
+ private readonly createTaskComposerBuffer: typeof createTaskComposerBufferFrame;
52
+ private readonly normalizeTaskComposerBuffer: typeof normalizeTaskComposerBufferFrame;
53
+ private readonly taskFieldsFromComposerText: typeof taskFieldsFromComposerTextFrame;
54
+ private readonly handleTaskPaneShortcutInput: typeof handleTaskPaneShortcutInputFrame;
55
+
56
+ constructor(private readonly options: RuntimeTaskPaneShortcutsOptions<TTaskRecord>) {
57
+ this.createTaskComposerBuffer =
58
+ options.createTaskComposerBuffer ?? createTaskComposerBufferFrame;
59
+ this.normalizeTaskComposerBuffer =
60
+ options.normalizeTaskComposerBuffer ?? normalizeTaskComposerBufferFrame;
61
+ this.taskFieldsFromComposerText =
62
+ options.taskFieldsFromComposerText ?? taskFieldsFromComposerTextFrame;
63
+ this.handleTaskPaneShortcutInput =
64
+ options.handleTaskPaneShortcutInput ?? handleTaskPaneShortcutInputFrame;
65
+ }
66
+
67
+ homeEditorBuffer(): TaskComposerBuffer {
68
+ const taskEditorTarget = this.options.workspace.taskEditorTarget;
69
+ if (taskEditorTarget.kind === 'task') {
70
+ return (
71
+ this.options.taskComposerForTask(taskEditorTarget.taskId) ??
72
+ this.createTaskComposerBuffer('')
73
+ );
74
+ }
75
+ return this.options.workspace.taskDraftComposer;
76
+ }
77
+
78
+ updateHomeEditorBuffer(next: TaskComposerBuffer): void {
79
+ const taskEditorTarget = this.options.workspace.taskEditorTarget;
80
+ if (taskEditorTarget.kind === 'task') {
81
+ this.options.setTaskComposerForTask(taskEditorTarget.taskId, next);
82
+ this.options.scheduleTaskComposerPersist(taskEditorTarget.taskId);
83
+ } else {
84
+ this.options.workspace.taskDraftComposer = this.normalizeTaskComposerBuffer(next);
85
+ }
86
+ this.options.markDirty();
87
+ }
88
+
89
+ selectRepositoryByDirection(direction: 1 | -1): void {
90
+ const orderedIds = this.options.activeRepositoryIds();
91
+ if (orderedIds.length === 0) {
92
+ return;
93
+ }
94
+ const currentIndex = Math.max(
95
+ 0,
96
+ orderedIds.indexOf(this.options.workspace.taskPaneSelectedRepositoryId ?? ''),
97
+ );
98
+ const nextIndex = Math.max(0, Math.min(orderedIds.length - 1, currentIndex + direction));
99
+ const nextRepositoryId = orderedIds[nextIndex];
100
+ if (nextRepositoryId !== undefined) {
101
+ this.options.selectRepositoryById(nextRepositoryId);
102
+ }
103
+ }
104
+
105
+ submitDraftTaskFromComposer(): void {
106
+ const repositoryId = this.options.workspace.taskPaneSelectedRepositoryId;
107
+ if (repositoryId === null || !this.options.repositoriesHas(repositoryId)) {
108
+ this.options.workspace.taskPaneNotice = 'select a repository first';
109
+ this.options.markDirty();
110
+ return;
111
+ }
112
+ const fields = this.taskFieldsFromComposerText(this.options.workspace.taskDraftComposer.text);
113
+ if (fields.title.length === 0) {
114
+ this.options.workspace.taskPaneNotice = 'first line is required';
115
+ this.options.markDirty();
116
+ return;
117
+ }
118
+ this.options.queueControlPlaneOp(async () => {
119
+ const task = await this.options.createTask({
120
+ repositoryId,
121
+ title: fields.title,
122
+ description: fields.description,
123
+ });
124
+ this.options.applyTaskRecord(task);
125
+ this.options.workspace.taskDraftComposer = this.createTaskComposerBuffer('');
126
+ this.options.workspace.taskPaneNotice = null;
127
+ this.options.syncTaskPaneSelection();
128
+ this.options.markDirty();
129
+ }, 'task-composer-create');
130
+ }
131
+
132
+ moveTaskEditorFocusUp(): void {
133
+ const workspace = this.options.workspace;
134
+ const scopedTasks = this.options.selectedRepositoryTaskRecords();
135
+ if (workspace.taskEditorTarget.kind === 'draft') {
136
+ const fallback = scopedTasks[scopedTasks.length - 1];
137
+ if (fallback !== undefined) {
138
+ this.options.focusTaskComposer(fallback.taskId);
139
+ }
140
+ return;
141
+ }
142
+
143
+ const focusedTaskId = workspace.taskEditorTarget.taskId;
144
+ const index = scopedTasks.findIndex((task) => task.taskId === focusedTaskId);
145
+ if (index <= 0) {
146
+ return;
147
+ }
148
+ const target = scopedTasks[index - 1];
149
+ if (target !== undefined) {
150
+ this.options.focusTaskComposer(target.taskId);
151
+ }
152
+ }
153
+
154
+ handleInput(input: Buffer): boolean {
155
+ const workspace = this.options.workspace;
156
+ return this.handleTaskPaneShortcutInput({
157
+ input,
158
+ mainPaneMode: workspace.mainPaneMode,
159
+ taskScreenKeybindings: this.options.taskScreenKeybindings,
160
+ taskEditorTarget: workspace.taskEditorTarget,
161
+ homeEditorBuffer: () => this.homeEditorBuffer(),
162
+ updateHomeEditorBuffer: (next) => {
163
+ this.updateHomeEditorBuffer(next);
164
+ },
165
+ moveTaskEditorFocusUp: () => {
166
+ this.moveTaskEditorFocusUp();
167
+ },
168
+ focusDraftComposer: () => {
169
+ this.options.focusDraftComposer();
170
+ },
171
+ submitDraftTaskFromComposer: () => {
172
+ this.submitDraftTaskFromComposer();
173
+ },
174
+ runTaskPaneAction: (action) => {
175
+ this.options.runTaskPaneAction(action);
176
+ },
177
+ selectRepositoryByDirection: (direction) => {
178
+ this.selectRepositoryByDirection(direction);
179
+ },
180
+ getTaskRepositoryDropdownOpen: () => workspace.taskRepositoryDropdownOpen,
181
+ setTaskRepositoryDropdownOpen: (open) => {
182
+ workspace.taskRepositoryDropdownOpen = open;
183
+ },
184
+ markDirty: () => {
185
+ this.options.markDirty();
186
+ },
187
+ });
188
+ }
189
+ }
@@ -0,0 +1,62 @@
1
+ import type { TaskPaneAction } from '../mux/harness-core-ui.ts';
2
+ import { RuntimeTaskPaneActions } from './runtime-task-pane-actions.ts';
3
+ import { RuntimeTaskPaneShortcuts } from './runtime-task-pane-shortcuts.ts';
4
+
5
+ interface TaskRecordShape {
6
+ readonly taskId: string;
7
+ readonly repositoryId: string | null;
8
+ readonly status: string;
9
+ readonly title: string;
10
+ readonly description: string;
11
+ }
12
+
13
+ type RuntimeTaskPaneActionsOptions<TTaskRecord extends TaskRecordShape> = ConstructorParameters<
14
+ typeof RuntimeTaskPaneActions<TTaskRecord>
15
+ >[0];
16
+ type RuntimeTaskPaneShortcutsOptions<TTaskRecord extends TaskRecordShape> = Omit<
17
+ ConstructorParameters<typeof RuntimeTaskPaneShortcuts<TTaskRecord>>[0],
18
+ 'runTaskPaneAction' | 'applyTaskRecord'
19
+ >;
20
+
21
+ interface RuntimeTaskPaneOptions<TTaskRecord extends TaskRecordShape> {
22
+ readonly actions: RuntimeTaskPaneActionsOptions<TTaskRecord>;
23
+ readonly shortcuts: RuntimeTaskPaneShortcutsOptions<TTaskRecord>;
24
+ }
25
+
26
+ export class RuntimeTaskPane<TTaskRecord extends TaskRecordShape> {
27
+ private readonly actions: RuntimeTaskPaneActions<TTaskRecord>;
28
+ private readonly shortcuts: RuntimeTaskPaneShortcuts<TTaskRecord>;
29
+
30
+ constructor(options: RuntimeTaskPaneOptions<TTaskRecord>) {
31
+ this.actions = new RuntimeTaskPaneActions<TTaskRecord>(options.actions);
32
+ this.shortcuts = new RuntimeTaskPaneShortcuts<TTaskRecord>({
33
+ ...options.shortcuts,
34
+ runTaskPaneAction: (action) => {
35
+ this.runTaskPaneAction(action);
36
+ },
37
+ applyTaskRecord: (task) => {
38
+ this.applyTaskRecord(task);
39
+ },
40
+ });
41
+ }
42
+
43
+ applyTaskRecord(task: TTaskRecord): TTaskRecord {
44
+ return this.actions.applyTaskRecord(task);
45
+ }
46
+
47
+ runTaskPaneAction(action: TaskPaneAction): void {
48
+ this.actions.runTaskPaneAction(action);
49
+ }
50
+
51
+ openTaskEditPrompt(taskId: string): void {
52
+ this.actions.openTaskEditPrompt(taskId);
53
+ }
54
+
55
+ reorderTaskByDrop(draggedTaskId: string, targetTaskId: string): void {
56
+ this.actions.reorderTaskByDrop(draggedTaskId, targetTaskId);
57
+ }
58
+
59
+ handleInput(input: Buffer): boolean {
60
+ return this.shortcuts.handleInput(input);
61
+ }
62
+ }
@@ -0,0 +1,153 @@
1
+ import type { TaskPaneAction } from '../mux/harness-core-ui.ts';
2
+
3
+ interface RuntimeWorkspaceConversationActions {
4
+ activateConversation(sessionId: string): Promise<void>;
5
+ createAndActivateConversationInDirectory(directoryId: string, agentType: string): Promise<void>;
6
+ openOrCreateCritiqueConversationInDirectory(directoryId: string): Promise<void>;
7
+ takeoverConversation(sessionId: string): Promise<void>;
8
+ }
9
+
10
+ interface RuntimeWorkspaceDirectoryActions {
11
+ archiveConversation(sessionId: string): Promise<void>;
12
+ addDirectoryByPath(rawPath: string): Promise<void>;
13
+ closeDirectory(directoryId: string): Promise<void>;
14
+ }
15
+
16
+ interface RuntimeWorkspaceRepositoryActions {
17
+ openRepositoryPromptForCreate(): void;
18
+ openRepositoryPromptForEdit(repositoryId: string): void;
19
+ reorderRepositoryByDrop(
20
+ draggedRepositoryId: string,
21
+ targetRepositoryId: string,
22
+ orderedRepositoryIds: readonly string[],
23
+ ): void;
24
+ upsertRepositoryByRemoteUrl(remoteUrl: string, existingRepositoryId?: string): Promise<void>;
25
+ archiveRepositoryById(repositoryId: string): Promise<void>;
26
+ }
27
+
28
+ interface RuntimeWorkspaceControlActions {
29
+ interruptConversation(sessionId: string): Promise<void>;
30
+ toggleGatewayProfiler(): Promise<void>;
31
+ toggleGatewayStatusTimeline(): Promise<void>;
32
+ toggleGatewayRenderTrace(conversationId: string | null): Promise<void>;
33
+ }
34
+
35
+ interface RuntimeWorkspaceTaskPaneActions {
36
+ runTaskPaneAction(action: TaskPaneAction): void;
37
+ openTaskEditPrompt(taskId: string): void;
38
+ reorderTaskByDrop(draggedTaskId: string, targetTaskId: string): void;
39
+ }
40
+
41
+ interface RuntimeWorkspaceTaskPaneShortcuts {
42
+ handleInput(input: Buffer): boolean;
43
+ }
44
+
45
+ interface RuntimeWorkspaceActionsOptions {
46
+ readonly conversationActions: RuntimeWorkspaceConversationActions;
47
+ readonly directoryActions: RuntimeWorkspaceDirectoryActions;
48
+ readonly repositoryActions: RuntimeWorkspaceRepositoryActions;
49
+ readonly controlActions: RuntimeWorkspaceControlActions;
50
+ readonly taskPaneActions: RuntimeWorkspaceTaskPaneActions;
51
+ readonly taskPaneShortcuts: RuntimeWorkspaceTaskPaneShortcuts;
52
+ readonly orderedActiveRepositoryIds: () => readonly string[];
53
+ }
54
+
55
+ export class RuntimeWorkspaceActions {
56
+ constructor(private readonly options: RuntimeWorkspaceActionsOptions) {}
57
+
58
+ async activateConversation(sessionId: string): Promise<void> {
59
+ await this.options.conversationActions.activateConversation(sessionId);
60
+ }
61
+
62
+ async createAndActivateConversationInDirectory(
63
+ directoryId: string,
64
+ agentType: string,
65
+ ): Promise<void> {
66
+ await this.options.conversationActions.createAndActivateConversationInDirectory(
67
+ directoryId,
68
+ agentType,
69
+ );
70
+ }
71
+
72
+ async openOrCreateCritiqueConversationInDirectory(directoryId: string): Promise<void> {
73
+ await this.options.conversationActions.openOrCreateCritiqueConversationInDirectory(directoryId);
74
+ }
75
+
76
+ async takeoverConversation(sessionId: string): Promise<void> {
77
+ await this.options.conversationActions.takeoverConversation(sessionId);
78
+ }
79
+
80
+ async archiveConversation(sessionId: string): Promise<void> {
81
+ await this.options.directoryActions.archiveConversation(sessionId);
82
+ }
83
+
84
+ async addDirectoryByPath(rawPath: string): Promise<void> {
85
+ await this.options.directoryActions.addDirectoryByPath(rawPath);
86
+ }
87
+
88
+ async closeDirectory(directoryId: string): Promise<void> {
89
+ await this.options.directoryActions.closeDirectory(directoryId);
90
+ }
91
+
92
+ openRepositoryPromptForCreate(): void {
93
+ this.options.repositoryActions.openRepositoryPromptForCreate();
94
+ }
95
+
96
+ openRepositoryPromptForEdit(repositoryId: string): void {
97
+ this.options.repositoryActions.openRepositoryPromptForEdit(repositoryId);
98
+ }
99
+
100
+ reorderRepositoryByDrop(draggedRepositoryId: string, targetRepositoryId: string): void {
101
+ this.options.repositoryActions.reorderRepositoryByDrop(
102
+ draggedRepositoryId,
103
+ targetRepositoryId,
104
+ this.options.orderedActiveRepositoryIds(),
105
+ );
106
+ }
107
+
108
+ async upsertRepositoryByRemoteUrl(
109
+ remoteUrl: string,
110
+ existingRepositoryId?: string,
111
+ ): Promise<void> {
112
+ await this.options.repositoryActions.upsertRepositoryByRemoteUrl(
113
+ remoteUrl,
114
+ existingRepositoryId,
115
+ );
116
+ }
117
+
118
+ async archiveRepositoryById(repositoryId: string): Promise<void> {
119
+ await this.options.repositoryActions.archiveRepositoryById(repositoryId);
120
+ }
121
+
122
+ async interruptConversation(sessionId: string): Promise<void> {
123
+ await this.options.controlActions.interruptConversation(sessionId);
124
+ }
125
+
126
+ async toggleGatewayProfiler(): Promise<void> {
127
+ await this.options.controlActions.toggleGatewayProfiler();
128
+ }
129
+
130
+ async toggleGatewayStatusTimeline(): Promise<void> {
131
+ await this.options.controlActions.toggleGatewayStatusTimeline();
132
+ }
133
+
134
+ async toggleGatewayRenderTrace(conversationId: string | null): Promise<void> {
135
+ await this.options.controlActions.toggleGatewayRenderTrace(conversationId);
136
+ }
137
+
138
+ runTaskPaneAction(action: TaskPaneAction): void {
139
+ this.options.taskPaneActions.runTaskPaneAction(action);
140
+ }
141
+
142
+ openTaskEditPrompt(taskId: string): void {
143
+ this.options.taskPaneActions.openTaskEditPrompt(taskId);
144
+ }
145
+
146
+ reorderTaskByDrop(draggedTaskId: string, targetTaskId: string): void {
147
+ this.options.taskPaneActions.reorderTaskByDrop(draggedTaskId, targetTaskId);
148
+ }
149
+
150
+ handleTaskPaneShortcutInput(input: Buffer): boolean {
151
+ return this.options.taskPaneShortcuts.handleInput(input);
152
+ }
153
+ }