@jmoyers/harness 0.1.11 → 0.1.20

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 (232) hide show
  1. package/README.md +31 -39
  2. package/package.json +31 -11
  3. package/packages/harness-ai/src/anthropic-protocol.ts +68 -68
  4. package/packages/harness-ai/src/stream-text.ts +13 -91
  5. package/packages/harness-ui/src/frame-primitives.ts +158 -0
  6. package/packages/harness-ui/src/index.ts +18 -0
  7. package/packages/harness-ui/src/interaction/conversation-input-forwarder.ts +221 -0
  8. package/packages/harness-ui/src/interaction/conversation-selection-input.ts +213 -0
  9. package/packages/harness-ui/src/interaction/global-shortcut-input.ts +172 -0
  10. package/{src/ui → packages/harness-ui/src/interaction}/input-preflight.ts +10 -12
  11. package/{src/ui → packages/harness-ui/src/interaction}/input-token-router.ts +120 -69
  12. package/packages/harness-ui/src/interaction/input.ts +420 -0
  13. package/packages/harness-ui/src/interaction/left-nav-input.ts +166 -0
  14. package/{src/ui → packages/harness-ui/src/interaction}/main-pane-pointer-input.ts +91 -23
  15. package/{src/ui → packages/harness-ui/src/interaction}/pointer-routing-input.ts +112 -48
  16. package/packages/harness-ui/src/interaction/rail-pointer-input.ts +62 -0
  17. package/packages/harness-ui/src/interaction/repository-fold-input.ts +118 -0
  18. package/packages/harness-ui/src/kit.ts +476 -0
  19. package/packages/harness-ui/src/layout.ts +238 -0
  20. package/packages/harness-ui/src/modal-manager.ts +222 -0
  21. package/{src/ui → packages/harness-ui/src}/screen.ts +53 -26
  22. package/packages/harness-ui/src/surface.ts +252 -0
  23. package/packages/harness-ui/src/text-layout.ts +210 -0
  24. package/packages/nim-core/src/contracts.ts +239 -0
  25. package/packages/nim-core/src/event-store.ts +299 -0
  26. package/packages/nim-core/src/events.ts +53 -0
  27. package/packages/nim-core/src/index.ts +9 -0
  28. package/packages/nim-core/src/provider-router.ts +129 -0
  29. package/packages/nim-core/src/providers/anthropic-driver.ts +291 -0
  30. package/packages/nim-core/src/runtime-factory.ts +49 -0
  31. package/packages/nim-core/src/runtime.ts +1797 -0
  32. package/packages/nim-core/src/session-store.ts +516 -0
  33. package/packages/nim-core/src/telemetry.ts +48 -0
  34. package/packages/nim-test-tui/src/index.ts +150 -0
  35. package/packages/nim-ui-core/src/index.ts +1 -0
  36. package/packages/nim-ui-core/src/projection.ts +87 -0
  37. package/scripts/codex-live-mux-runtime.ts +2 -3872
  38. package/scripts/control-plane-daemon.ts +11 -0
  39. package/scripts/harness-bin.js +5 -0
  40. package/scripts/harness-commands.ts +300 -0
  41. package/scripts/harness-runtime.ts +82 -0
  42. package/scripts/harness.ts +33 -3019
  43. package/scripts/nim-tui-smoke.ts +748 -0
  44. package/src/cli/auth/runtime.ts +948 -0
  45. package/src/cli/gateway/runtime.ts +1872 -0
  46. package/src/cli/parsing/flags.ts +23 -0
  47. package/src/cli/parsing/session.ts +42 -0
  48. package/src/cli/runtime/context.ts +193 -0
  49. package/src/cli/runtime-app/application.ts +392 -0
  50. package/src/cli/runtime-infra/gateway-control.ts +729 -0
  51. package/{scripts/harness-inspector.ts → src/cli/workflows/inspector.ts} +14 -11
  52. package/src/cli/workflows/runtime.ts +965 -0
  53. package/src/clients/tui/left-rail-interactions.ts +519 -0
  54. package/src/clients/tui/main-pane-interactions.ts +509 -0
  55. package/src/clients/tui/modal-input-routing.ts +71 -0
  56. package/src/clients/tui/render-snapshot-adapter.ts +88 -0
  57. package/src/clients/web/synced-selectors.ts +132 -0
  58. package/src/codex/live-session.ts +82 -29
  59. package/src/config/config-core.ts +348 -8
  60. package/src/config/harness.config.template.jsonc +33 -0
  61. package/src/control-plane/agent-realtime-api.ts +82 -427
  62. package/src/control-plane/session-summary.ts +10 -81
  63. package/src/control-plane/status/reducer-base.ts +12 -12
  64. package/src/control-plane/status/reducers/claude-status-reducer.ts +3 -3
  65. package/src/control-plane/status/reducers/codex-status-reducer.ts +4 -4
  66. package/src/control-plane/status/reducers/cursor-status-reducer.ts +3 -3
  67. package/src/control-plane/stream-client.ts +12 -2
  68. package/src/control-plane/stream-command-parser.ts +83 -143
  69. package/src/control-plane/stream-protocol.ts +53 -37
  70. package/src/control-plane/stream-server-command.ts +376 -69
  71. package/src/control-plane/stream-server-session-runtime.ts +3 -2
  72. package/src/control-plane/stream-server.ts +864 -70
  73. package/src/control-plane/stream-session-runtime-types.ts +41 -0
  74. package/src/{mux/live-mux/control-plane-records.ts → core/contracts/records.ts} +24 -97
  75. package/src/core/state/observed-stream-cursor.ts +43 -0
  76. package/src/core/state/synced-observed-state.ts +273 -0
  77. package/src/core/store/harness-synced-store.ts +81 -0
  78. package/src/diff/budget.ts +136 -0
  79. package/src/diff/build.ts +289 -0
  80. package/src/diff/chunker.ts +146 -0
  81. package/src/diff/git-invoke.ts +315 -0
  82. package/src/diff/git-parse.ts +472 -0
  83. package/src/diff/hash.ts +70 -0
  84. package/src/diff/index.ts +24 -0
  85. package/src/diff/normalize.ts +134 -0
  86. package/src/diff/types.ts +178 -0
  87. package/src/diff-ui/args.ts +346 -0
  88. package/src/diff-ui/commands.ts +123 -0
  89. package/src/diff-ui/finder.ts +94 -0
  90. package/src/diff-ui/highlight.ts +127 -0
  91. package/src/diff-ui/index.ts +2 -0
  92. package/src/diff-ui/model.ts +141 -0
  93. package/src/diff-ui/pager.ts +412 -0
  94. package/src/diff-ui/render.ts +337 -0
  95. package/src/diff-ui/runtime.ts +379 -0
  96. package/src/diff-ui/state.ts +224 -0
  97. package/src/diff-ui/types.ts +236 -0
  98. package/src/domain/workspace.ts +68 -5
  99. package/src/mux/control-plane-op-queue.ts +93 -7
  100. package/src/mux/conversation-rail.ts +28 -71
  101. package/src/mux/dual-pane-core.ts +13 -13
  102. package/src/mux/harness-core-ui.ts +313 -42
  103. package/src/mux/input-shortcuts.ts +13 -131
  104. package/src/mux/keybinding-catalog.ts +340 -0
  105. package/src/mux/keybinding-registry.ts +103 -0
  106. package/src/mux/live-mux/command-menu-open-in.ts +280 -0
  107. package/src/mux/live-mux/command-menu.ts +167 -4
  108. package/src/mux/live-mux/conversation-state.ts +13 -0
  109. package/src/mux/live-mux/directory-resolution.ts +1 -1
  110. package/src/mux/live-mux/git-snapshot.ts +33 -2
  111. package/src/mux/live-mux/global-shortcut-handlers.ts +6 -0
  112. package/src/mux/live-mux/home-pane-drop.ts +1 -1
  113. package/src/mux/live-mux/home-pane-pointer.ts +10 -0
  114. package/src/mux/live-mux/input-forwarding.ts +59 -2
  115. package/src/mux/live-mux/left-nav-activation.ts +124 -7
  116. package/src/mux/live-mux/left-nav.ts +35 -0
  117. package/src/mux/live-mux/link-click.ts +292 -0
  118. package/src/mux/live-mux/modal-command-menu-handler.ts +46 -9
  119. package/src/mux/live-mux/modal-conversation-handlers.ts +5 -1
  120. package/src/mux/live-mux/modal-input-reducers.ts +77 -12
  121. package/src/mux/live-mux/modal-overlays.ts +168 -34
  122. package/src/mux/live-mux/modal-pointer.ts +3 -7
  123. package/src/mux/live-mux/modal-prompt-handlers.ts +23 -2
  124. package/src/mux/live-mux/modal-release-notes-handler.ts +111 -0
  125. package/src/mux/live-mux/modal-task-editor-handler.ts +16 -11
  126. package/src/mux/live-mux/pointer-routing.ts +5 -2
  127. package/src/mux/live-mux/project-pane-pointer.ts +8 -0
  128. package/src/mux/live-mux/rail-layout.ts +33 -30
  129. package/src/mux/live-mux/release-notes.ts +383 -0
  130. package/src/mux/live-mux/render-trace-analysis.ts +52 -7
  131. package/src/mux/live-mux/repository-folding.ts +3 -0
  132. package/src/mux/live-mux/selection.ts +0 -4
  133. package/src/mux/live-mux/session-diagnostics-paths.ts +21 -0
  134. package/src/mux/project-pane-github-review.ts +271 -0
  135. package/src/mux/render-frame.ts +4 -0
  136. package/src/mux/runtime-app/codex-live-mux-runtime.ts +5191 -0
  137. package/src/mux/task-composer.ts +21 -14
  138. package/src/mux/task-focused-pane.ts +118 -117
  139. package/src/mux/task-screen-keybindings.ts +10 -101
  140. package/src/mux/workspace-rail-model.ts +270 -104
  141. package/src/mux/workspace-rail.ts +45 -22
  142. package/src/pty/session-broker.ts +1 -1
  143. package/{scripts → src/recording}/terminal-recording-gif-lib.ts +2 -2
  144. package/src/services/control-plane.ts +50 -32
  145. package/src/services/conversation-lifecycle.ts +118 -87
  146. package/src/services/conversation-startup-hydration.ts +20 -12
  147. package/src/services/directory-hydration.ts +21 -16
  148. package/src/services/event-persistence.ts +7 -0
  149. package/src/services/left-rail-pointer-handler.ts +329 -0
  150. package/src/services/mux-ui-state-persistence.ts +5 -1
  151. package/src/services/recording.ts +34 -26
  152. package/src/services/runtime-command-menu-agent-tools.ts +1 -1
  153. package/src/services/runtime-control-actions.ts +79 -61
  154. package/src/services/runtime-control-plane-ops.ts +122 -83
  155. package/src/services/runtime-conversation-actions.ts +40 -26
  156. package/src/services/runtime-conversation-activation.ts +73 -46
  157. package/src/services/runtime-conversation-starter.ts +53 -45
  158. package/src/services/runtime-conversation-title-edit.ts +91 -80
  159. package/src/services/runtime-envelope-handler.ts +107 -105
  160. package/src/services/runtime-git-state.ts +42 -29
  161. package/src/services/runtime-layout-resize.ts +3 -1
  162. package/src/services/runtime-left-rail-render.ts +99 -63
  163. package/src/services/runtime-nim-cli-session.ts +438 -0
  164. package/src/services/runtime-nim-session.ts +705 -0
  165. package/src/services/runtime-nim-tool-bridge.ts +141 -0
  166. package/src/services/runtime-observed-event-projection-pipeline.ts +45 -0
  167. package/src/services/runtime-process-wiring.ts +29 -36
  168. package/src/services/runtime-project-pane-github-review-cache.ts +164 -0
  169. package/src/services/runtime-render-flush.ts +63 -70
  170. package/src/services/runtime-render-lifecycle.ts +65 -64
  171. package/src/services/runtime-render-orchestrator.ts +55 -45
  172. package/src/services/runtime-render-pipeline.ts +106 -103
  173. package/src/services/runtime-render-state.ts +62 -49
  174. package/src/services/runtime-repository-actions.ts +97 -72
  175. package/src/services/runtime-right-pane-render.ts +80 -53
  176. package/src/services/runtime-shutdown.ts +38 -35
  177. package/src/services/runtime-stream-subscriptions.ts +35 -27
  178. package/src/services/runtime-task-composer-persistence.ts +71 -59
  179. package/src/services/runtime-task-composer-snapshot.ts +14 -0
  180. package/src/services/runtime-task-editor-actions.ts +46 -29
  181. package/src/services/runtime-task-pane-actions.ts +220 -134
  182. package/src/services/runtime-task-pane-shortcuts.ts +323 -123
  183. package/src/services/runtime-workspace-observed-effect-queue.ts +25 -0
  184. package/src/services/runtime-workspace-observed-events.ts +33 -184
  185. package/src/services/runtime-workspace-observed-transition-policy.ts +228 -0
  186. package/src/services/session-diagnostics-store.ts +217 -0
  187. package/src/services/startup-background-resume.ts +26 -21
  188. package/src/services/startup-orchestrator.ts +16 -13
  189. package/src/services/startup-paint-tracker.ts +29 -21
  190. package/src/services/startup-persisted-conversation-queue.ts +19 -13
  191. package/src/services/startup-settled-gate.ts +25 -15
  192. package/src/services/startup-shutdown.ts +18 -22
  193. package/src/services/startup-state-hydration.ts +44 -34
  194. package/src/services/startup-visibility.ts +12 -4
  195. package/src/services/task-pane-selection-actions.ts +89 -72
  196. package/src/services/task-planning-hydration.ts +24 -18
  197. package/src/services/task-planning-observed-events.ts +50 -52
  198. package/src/services/workspace-observed-events.ts +66 -63
  199. package/src/storage/storage-lifecycle-core.ts +438 -0
  200. package/src/store/control-plane-store-normalize.ts +33 -242
  201. package/src/store/control-plane-store-types.ts +1 -35
  202. package/src/store/control-plane-store.ts +360 -56
  203. package/src/store/event-store.ts +366 -8
  204. package/src/terminal/snapshot-oracle.ts +207 -94
  205. package/src/ui/mux-theme.ts +112 -8
  206. package/src/ui/panes/home-gridfire.ts +40 -31
  207. package/src/ui/panes/home.ts +10 -2
  208. package/src/ui/panes/nim.ts +315 -0
  209. package/src/mux/live-mux/actions-task.ts +0 -115
  210. package/src/mux/live-mux/left-rail-actions.ts +0 -118
  211. package/src/mux/live-mux/left-rail-conversation-click.ts +0 -85
  212. package/src/mux/live-mux/left-rail-pointer.ts +0 -74
  213. package/src/mux/live-mux/task-pane-shortcuts.ts +0 -206
  214. package/src/services/runtime-directory-actions.ts +0 -164
  215. package/src/services/runtime-input-pipeline.ts +0 -50
  216. package/src/services/runtime-input-router.ts +0 -195
  217. package/src/services/runtime-main-pane-input.ts +0 -230
  218. package/src/services/runtime-modal-input.ts +0 -137
  219. package/src/services/runtime-navigation-input.ts +0 -197
  220. package/src/services/runtime-rail-input.ts +0 -279
  221. package/src/services/runtime-task-pane.ts +0 -62
  222. package/src/services/runtime-workspace-actions.ts +0 -158
  223. package/src/ui/conversation-input-forwarder.ts +0 -114
  224. package/src/ui/conversation-selection-input.ts +0 -103
  225. package/src/ui/global-shortcut-input.ts +0 -89
  226. package/src/ui/input.ts +0 -269
  227. package/src/ui/kit.ts +0 -509
  228. package/src/ui/left-nav-input.ts +0 -80
  229. package/src/ui/left-rail-pointer-input.ts +0 -148
  230. package/src/ui/modals/manager.ts +0 -218
  231. package/src/ui/repository-fold-input.ts +0 -91
  232. package/src/ui/surface.ts +0 -224
@@ -7,50 +7,58 @@ export interface RuntimeStreamSubscriptionsOptions {
7
7
  readonly unsubscribeObservedStream: (subscriptionId: string) => Promise<void>;
8
8
  }
9
9
 
10
- export class RuntimeStreamSubscriptions {
11
- private observedStreamSubscriptionId: string | null = null;
10
+ export interface RuntimeStreamSubscriptions {
11
+ subscribeConversationEvents(sessionId: string): Promise<void>;
12
+ unsubscribeConversationEvents(sessionId: string): Promise<void>;
13
+ subscribeTaskPlanningEvents(afterCursor: number | null): Promise<void>;
14
+ unsubscribeTaskPlanningEvents(): Promise<void>;
15
+ }
12
16
 
13
- constructor(private readonly options: RuntimeStreamSubscriptionsOptions) {}
17
+ export function createRuntimeStreamSubscriptions(
18
+ options: RuntimeStreamSubscriptionsOptions,
19
+ ): RuntimeStreamSubscriptions {
20
+ let observedStreamSubscriptionId: string | null = null;
14
21
 
15
- async subscribeConversationEvents(sessionId: string): Promise<void> {
22
+ const subscribeConversationEvents = async (sessionId: string): Promise<void> => {
16
23
  try {
17
- await this.options.subscribePtyEvents(sessionId);
24
+ await options.subscribePtyEvents(sessionId);
18
25
  } catch (error: unknown) {
19
- if (
20
- !this.options.isSessionNotFoundError(error) &&
21
- !this.options.isSessionNotLiveError(error)
22
- ) {
26
+ if (!options.isSessionNotFoundError(error) && !options.isSessionNotLiveError(error)) {
23
27
  throw error;
24
28
  }
25
29
  }
26
- }
30
+ };
27
31
 
28
- async unsubscribeConversationEvents(sessionId: string): Promise<void> {
32
+ const unsubscribeConversationEvents = async (sessionId: string): Promise<void> => {
29
33
  try {
30
- await this.options.unsubscribePtyEvents(sessionId);
34
+ await options.unsubscribePtyEvents(sessionId);
31
35
  } catch (error: unknown) {
32
- if (
33
- !this.options.isSessionNotFoundError(error) &&
34
- !this.options.isSessionNotLiveError(error)
35
- ) {
36
+ if (!options.isSessionNotFoundError(error) && !options.isSessionNotLiveError(error)) {
36
37
  throw error;
37
38
  }
38
39
  }
39
- }
40
+ };
40
41
 
41
- async subscribeTaskPlanningEvents(afterCursor: number | null): Promise<void> {
42
- if (this.observedStreamSubscriptionId !== null) {
42
+ const subscribeTaskPlanningEvents = async (afterCursor: number | null): Promise<void> => {
43
+ if (observedStreamSubscriptionId !== null) {
43
44
  return;
44
45
  }
45
- this.observedStreamSubscriptionId = await this.options.subscribeObservedStream(afterCursor);
46
- }
46
+ observedStreamSubscriptionId = await options.subscribeObservedStream(afterCursor);
47
+ };
47
48
 
48
- async unsubscribeTaskPlanningEvents(): Promise<void> {
49
- if (this.observedStreamSubscriptionId === null) {
49
+ const unsubscribeTaskPlanningEvents = async (): Promise<void> => {
50
+ if (observedStreamSubscriptionId === null) {
50
51
  return;
51
52
  }
52
- const subscriptionId = this.observedStreamSubscriptionId;
53
- this.observedStreamSubscriptionId = null;
54
- await this.options.unsubscribeObservedStream(subscriptionId);
55
- }
53
+ const subscriptionId = observedStreamSubscriptionId;
54
+ observedStreamSubscriptionId = null;
55
+ await options.unsubscribeObservedStream(subscriptionId);
56
+ };
57
+
58
+ return {
59
+ subscribeConversationEvents,
60
+ unsubscribeConversationEvents,
61
+ subscribeTaskPlanningEvents,
62
+ unsubscribeTaskPlanningEvents,
63
+ };
56
64
  }
@@ -1,13 +1,13 @@
1
1
  interface TaskComposerFields {
2
- readonly title: string;
3
- readonly description: string;
2
+ readonly title: string | null;
3
+ readonly body: string;
4
4
  }
5
5
 
6
6
  interface TaskRecordShape {
7
7
  readonly taskId: string;
8
8
  readonly repositoryId: string | null;
9
9
  readonly title: string;
10
- readonly description: string;
10
+ readonly body: string;
11
11
  }
12
12
 
13
13
  interface TaskComposerBufferShape {
@@ -33,7 +33,7 @@ interface RuntimeTaskComposerPersistenceOptions<
33
33
  taskId: string;
34
34
  repositoryId: string | null;
35
35
  title: string;
36
- description: string;
36
+ body: string;
37
37
  }) => Promise<TTaskRecord>;
38
38
  readonly applyTaskRecord: (task: TTaskRecord) => void;
39
39
  readonly queueControlPlaneOp: (task: () => Promise<void>, label: string) => void;
@@ -44,96 +44,108 @@ interface RuntimeTaskComposerPersistenceOptions<
44
44
  readonly clearTimeoutFn?: (timer: TTaskAutosaveTimer) => void;
45
45
  }
46
46
 
47
- export class RuntimeTaskComposerPersistenceService<
48
- TTaskRecord extends TaskRecordShape,
47
+ export interface RuntimeTaskComposerPersistenceService<
49
48
  TTaskComposerBuffer extends TaskComposerBufferShape,
50
- TTaskAutosaveTimer extends { unref?: () => void } = NodeJS.Timeout,
51
49
  > {
52
- private readonly setTimeoutFn: (callback: () => void, ms: number) => TTaskAutosaveTimer;
53
- private readonly clearTimeoutFn: (timer: TTaskAutosaveTimer) => void;
50
+ taskComposerForTask(taskId: string): TTaskComposerBuffer | null;
51
+ setTaskComposerForTask(taskId: string, buffer: TTaskComposerBuffer): void;
52
+ clearTaskAutosaveTimer(taskId: string): void;
53
+ scheduleTaskComposerPersist(taskId: string): void;
54
+ flushTaskComposerPersist(taskId: string): void;
55
+ }
54
56
 
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
- }
57
+ export function createRuntimeTaskComposerPersistenceService<
58
+ TTaskRecord extends TaskRecordShape,
59
+ TTaskComposerBuffer extends TaskComposerBufferShape,
60
+ TTaskAutosaveTimer extends { unref?: () => void } = NodeJS.Timeout,
61
+ >(
62
+ options: RuntimeTaskComposerPersistenceOptions<
63
+ TTaskRecord,
64
+ TTaskComposerBuffer,
65
+ TTaskAutosaveTimer
66
+ >,
67
+ ): RuntimeTaskComposerPersistenceService<TTaskComposerBuffer> {
68
+ const setTimeoutFn =
69
+ options.setTimeoutFn ??
70
+ ((callback, ms) => setTimeout(callback, ms) as unknown as TTaskAutosaveTimer);
71
+ const clearTimeoutFn =
72
+ options.clearTimeoutFn ?? ((timer) => clearTimeout(timer as unknown as NodeJS.Timeout));
68
73
 
69
- taskComposerForTask(taskId: string): TTaskComposerBuffer | null {
70
- const existing = this.options.getTaskComposer(taskId);
74
+ function taskComposerForTask(taskId: string): TTaskComposerBuffer | null {
75
+ const existing = options.getTaskComposer(taskId);
71
76
  if (existing !== undefined) {
72
77
  return existing;
73
78
  }
74
- const task = this.options.getTask(taskId);
79
+ const task = options.getTask(taskId);
75
80
  if (task === undefined) {
76
81
  return null;
77
82
  }
78
- return this.options.buildComposerFromTask(task);
83
+ return options.buildComposerFromTask(task);
79
84
  }
80
85
 
81
- setTaskComposerForTask(taskId: string, buffer: TTaskComposerBuffer): void {
82
- this.options.setTaskComposer(taskId, this.options.normalizeTaskComposerBuffer(buffer));
86
+ function setTaskComposerForTask(taskId: string, buffer: TTaskComposerBuffer): void {
87
+ options.setTaskComposer(taskId, options.normalizeTaskComposerBuffer(buffer));
83
88
  }
84
89
 
85
- clearTaskAutosaveTimer(taskId: string): void {
86
- const timer = this.options.getTaskAutosaveTimer(taskId);
90
+ function clearTaskAutosaveTimer(taskId: string): void {
91
+ const timer = options.getTaskAutosaveTimer(taskId);
87
92
  if (timer !== undefined) {
88
- this.clearTimeoutFn(timer);
89
- this.options.deleteTaskAutosaveTimer(taskId);
93
+ clearTimeoutFn(timer);
94
+ options.deleteTaskAutosaveTimer(taskId);
90
95
  }
91
96
  }
92
97
 
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);
98
+ function scheduleTaskComposerPersist(taskId: string): void {
99
+ clearTaskAutosaveTimer(taskId);
100
+ const timer = setTimeoutFn(() => {
101
+ options.deleteTaskAutosaveTimer(taskId);
102
+ queuePersistTaskComposer(taskId, 'debounced');
103
+ }, options.autosaveDebounceMs);
99
104
  timer.unref?.();
100
- this.options.setTaskAutosaveTimer(taskId, timer);
105
+ options.setTaskAutosaveTimer(taskId, timer);
101
106
  }
102
107
 
103
- flushTaskComposerPersist(taskId: string): void {
104
- this.clearTaskAutosaveTimer(taskId);
105
- this.queuePersistTaskComposer(taskId, 'flush');
108
+ function flushTaskComposerPersist(taskId: string): void {
109
+ clearTaskAutosaveTimer(taskId);
110
+ queuePersistTaskComposer(taskId, 'flush');
106
111
  }
107
112
 
108
- private queuePersistTaskComposer(taskId: string, reason: 'debounced' | 'flush'): void {
109
- const task = this.options.getTask(taskId);
110
- const buffer = this.options.getTaskComposer(taskId);
113
+ function queuePersistTaskComposer(taskId: string, reason: 'debounced' | 'flush'): void {
114
+ const task = options.getTask(taskId);
115
+ const buffer = options.getTaskComposer(taskId);
111
116
  if (task === undefined || buffer === undefined) {
112
117
  return;
113
118
  }
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();
119
+ const fields = options.taskFieldsFromComposerText(buffer.text);
120
+ if (fields.body.trim().length === 0) {
121
+ options.setTaskPaneNotice('task body is required');
122
+ options.markDirty();
118
123
  return;
119
124
  }
120
- if (fields.title === task.title && fields.description === task.description) {
125
+ if (fields.title === task.title && fields.body === task.body) {
121
126
  return;
122
127
  }
123
- this.options.queueControlPlaneOp(async () => {
124
- const parsed = await this.options.updateTask({
128
+ options.queueControlPlaneOp(async () => {
129
+ const parsed = await options.updateTask({
125
130
  taskId,
126
131
  repositoryId: task.repositoryId,
127
- title: fields.title,
128
- description: fields.description,
132
+ title: fields.title ?? '',
133
+ body: fields.body,
129
134
  });
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);
135
+ options.applyTaskRecord(parsed);
136
+ const persistedText = parsed.body.length === 0 ? parsed.title : parsed.body;
137
+ const latestBuffer = options.getTaskComposer(taskId);
134
138
  if (latestBuffer !== undefined && latestBuffer.text === persistedText) {
135
- this.options.deleteTaskComposer(taskId);
139
+ options.deleteTaskComposer(taskId);
136
140
  }
137
141
  }, `task-editor-save:${reason}:${taskId}`);
138
142
  }
143
+
144
+ return {
145
+ taskComposerForTask,
146
+ setTaskComposerForTask,
147
+ clearTaskAutosaveTimer,
148
+ scheduleTaskComposerPersist,
149
+ flushTaskComposerPersist,
150
+ };
139
151
  }
@@ -0,0 +1,14 @@
1
+ import type { TaskComposerBuffer } from '../mux/task-composer.ts';
2
+
3
+ export function snapshotTaskComposerBuffers(
4
+ buffers: ReadonlyMap<string, TaskComposerBuffer>,
5
+ ): ReadonlyMap<string, TaskComposerBuffer> {
6
+ const snapshot = new Map<string, TaskComposerBuffer>();
7
+ for (const [taskId, buffer] of buffers) {
8
+ snapshot.set(taskId, {
9
+ text: buffer.text,
10
+ cursor: buffer.cursor,
11
+ });
12
+ }
13
+ return snapshot;
14
+ }
@@ -6,28 +6,31 @@ interface TaskRecordShape {
6
6
 
7
7
  interface RuntimeTaskEditorActionService<TTaskRecord extends TaskRecordShape> {
8
8
  createTask(input: {
9
- repositoryId: string;
10
- title: string;
11
- description: string;
9
+ repositoryId?: string;
10
+ projectId?: string;
11
+ title?: string | null;
12
+ body: string;
12
13
  }): Promise<TTaskRecord>;
13
14
  updateTask(input: {
14
15
  taskId: string;
15
- repositoryId: string;
16
- title: string;
17
- description: string;
16
+ repositoryId?: string | null;
17
+ projectId?: string | null;
18
+ title?: string | null;
19
+ body?: string;
18
20
  }): Promise<TTaskRecord>;
19
21
  }
20
22
 
21
23
  export interface RuntimeTaskEditorSubmitPayload {
22
24
  readonly mode: 'create' | 'edit';
23
25
  readonly taskId: string | null;
24
- readonly repositoryId: string;
25
- readonly title: string;
26
- readonly description: string;
26
+ readonly repositoryId: string | null;
27
+ readonly projectId?: string | null;
28
+ readonly title: string | null;
29
+ readonly body: string;
27
30
  readonly commandLabel: string;
28
31
  }
29
32
 
30
- interface RuntimeTaskEditorActionsOptions<TTaskRecord extends TaskRecordShape> {
33
+ export interface RuntimeTaskEditorActionsOptions<TTaskRecord extends TaskRecordShape> {
31
34
  readonly workspace: WorkspaceModel;
32
35
  readonly controlPlaneService: RuntimeTaskEditorActionService<TTaskRecord>;
33
36
  readonly applyTaskRecord: (task: TTaskRecord) => void;
@@ -39,45 +42,59 @@ function formatErrorMessage(error: unknown): string {
39
42
  return error instanceof Error ? error.message : String(error);
40
43
  }
41
44
 
42
- export class RuntimeTaskEditorActions<TTaskRecord extends TaskRecordShape> {
43
- constructor(private readonly options: RuntimeTaskEditorActionsOptions<TTaskRecord>) {}
45
+ export interface RuntimeTaskEditorActions {
46
+ submitTaskEditorPayload(payload: RuntimeTaskEditorSubmitPayload): void;
47
+ }
44
48
 
45
- submitTaskEditorPayload(payload: RuntimeTaskEditorSubmitPayload): void {
46
- this.options.queueControlPlaneOp(async () => {
49
+ export function createRuntimeTaskEditorActions<TTaskRecord extends TaskRecordShape>(
50
+ options: RuntimeTaskEditorActionsOptions<TTaskRecord>,
51
+ ): RuntimeTaskEditorActions {
52
+ const submitTaskEditorPayload = (payload: RuntimeTaskEditorSubmitPayload): void => {
53
+ options.queueControlPlaneOp(async () => {
47
54
  try {
48
55
  if (payload.mode === 'create') {
49
- this.options.applyTaskRecord(
50
- await this.options.controlPlaneService.createTask({
51
- repositoryId: payload.repositoryId,
56
+ options.applyTaskRecord(
57
+ await options.controlPlaneService.createTask({
58
+ ...(payload.repositoryId === null ? {} : { repositoryId: payload.repositoryId }),
59
+ ...(payload.projectId === undefined || payload.projectId === null
60
+ ? {}
61
+ : { projectId: payload.projectId }),
52
62
  title: payload.title,
53
- description: payload.description,
63
+ body: payload.body,
54
64
  }),
55
65
  );
56
66
  } else {
57
67
  if (payload.taskId === null) {
58
68
  throw new Error('task edit state missing task id');
59
69
  }
60
- this.options.applyTaskRecord(
61
- await this.options.controlPlaneService.updateTask({
70
+ options.applyTaskRecord(
71
+ await options.controlPlaneService.updateTask({
62
72
  taskId: payload.taskId,
63
- repositoryId: payload.repositoryId,
73
+ ...(payload.repositoryId === null ? {} : { repositoryId: payload.repositoryId }),
74
+ ...(payload.projectId === undefined || payload.projectId === null
75
+ ? {}
76
+ : { projectId: payload.projectId }),
64
77
  title: payload.title,
65
- description: payload.description,
78
+ body: payload.body,
66
79
  }),
67
80
  );
68
81
  }
69
- this.options.workspace.taskEditorPrompt = null;
70
- this.options.workspace.taskPaneNotice = null;
82
+ options.workspace.taskEditorPrompt = null;
83
+ options.workspace.taskPaneNotice = null;
71
84
  } catch (error: unknown) {
72
85
  const message = formatErrorMessage(error);
73
- if (this.options.workspace.taskEditorPrompt !== null) {
74
- this.options.workspace.taskEditorPrompt.error = message;
86
+ if (options.workspace.taskEditorPrompt !== null) {
87
+ options.workspace.taskEditorPrompt.error = message;
75
88
  } else {
76
- this.options.workspace.taskPaneNotice = message;
89
+ options.workspace.taskPaneNotice = message;
77
90
  }
78
91
  } finally {
79
- this.options.markDirty();
92
+ options.markDirty();
80
93
  }
81
94
  }, payload.commandLabel);
82
- }
95
+ };
96
+
97
+ return {
98
+ submitTaskEditorPayload,
99
+ };
83
100
  }