@jmoyers/harness 0.1.10 → 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 (239) hide show
  1. package/README.md +31 -35
  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/{src/ui/modals/manager.ts → packages/harness-ui/src/modal-manager.ts} +94 -64
  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 -3721
  38. package/scripts/control-plane-daemon.ts +24 -2
  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 -3007
  43. package/scripts/nim-tui-smoke.ts +748 -0
  44. package/src/cli/auth/runtime.ts +948 -0
  45. package/src/cli/default-gateway-pointer.ts +193 -0
  46. package/src/cli/gateway/runtime.ts +1872 -0
  47. package/src/cli/parsing/flags.ts +23 -0
  48. package/src/cli/parsing/session.ts +42 -0
  49. package/src/cli/runtime/context.ts +193 -0
  50. package/src/cli/runtime-app/application.ts +392 -0
  51. package/src/cli/runtime-infra/gateway-control.ts +729 -0
  52. package/{scripts/harness-inspector.ts → src/cli/workflows/inspector.ts} +14 -11
  53. package/src/cli/workflows/runtime.ts +965 -0
  54. package/src/clients/tui/left-rail-interactions.ts +519 -0
  55. package/src/clients/tui/main-pane-interactions.ts +509 -0
  56. package/src/clients/tui/modal-input-routing.ts +71 -0
  57. package/src/clients/tui/render-snapshot-adapter.ts +88 -0
  58. package/src/clients/web/synced-selectors.ts +132 -0
  59. package/src/codex/live-session.ts +82 -29
  60. package/src/config/config-core.ts +361 -10
  61. package/src/config/harness-paths.ts +4 -7
  62. package/src/config/harness-runtime-migration.ts +142 -19
  63. package/src/config/harness.config.template.jsonc +33 -0
  64. package/src/config/secrets-core.ts +92 -4
  65. package/src/control-plane/agent-realtime-api.ts +82 -427
  66. package/src/control-plane/prompt/thread-title-namer.ts +49 -23
  67. package/src/control-plane/session-summary.ts +10 -81
  68. package/src/control-plane/status/reducer-base.ts +12 -12
  69. package/src/control-plane/status/reducers/claude-status-reducer.ts +3 -3
  70. package/src/control-plane/status/reducers/codex-status-reducer.ts +4 -4
  71. package/src/control-plane/status/reducers/cursor-status-reducer.ts +3 -3
  72. package/src/control-plane/stream-client.ts +12 -2
  73. package/src/control-plane/stream-command-parser.ts +83 -143
  74. package/src/control-plane/stream-protocol.ts +53 -37
  75. package/src/control-plane/stream-server-background.ts +18 -2
  76. package/src/control-plane/stream-server-command.ts +376 -69
  77. package/src/control-plane/stream-server-session-runtime.ts +3 -2
  78. package/src/control-plane/stream-server.ts +943 -80
  79. package/src/control-plane/stream-session-runtime-types.ts +41 -0
  80. package/src/{mux/live-mux/control-plane-records.ts → core/contracts/records.ts} +24 -97
  81. package/src/core/state/observed-stream-cursor.ts +43 -0
  82. package/src/core/state/synced-observed-state.ts +273 -0
  83. package/src/core/store/harness-synced-store.ts +81 -0
  84. package/src/diff/budget.ts +136 -0
  85. package/src/diff/build.ts +289 -0
  86. package/src/diff/chunker.ts +146 -0
  87. package/src/diff/git-invoke.ts +315 -0
  88. package/src/diff/git-parse.ts +472 -0
  89. package/src/diff/hash.ts +70 -0
  90. package/src/diff/index.ts +24 -0
  91. package/src/diff/normalize.ts +134 -0
  92. package/src/diff/types.ts +178 -0
  93. package/src/diff-ui/args.ts +346 -0
  94. package/src/diff-ui/commands.ts +123 -0
  95. package/src/diff-ui/finder.ts +94 -0
  96. package/src/diff-ui/highlight.ts +127 -0
  97. package/src/diff-ui/index.ts +2 -0
  98. package/src/diff-ui/model.ts +141 -0
  99. package/src/diff-ui/pager.ts +412 -0
  100. package/src/diff-ui/render.ts +337 -0
  101. package/src/diff-ui/runtime.ts +379 -0
  102. package/src/diff-ui/state.ts +224 -0
  103. package/src/diff-ui/types.ts +236 -0
  104. package/src/domain/conversations.ts +11 -7
  105. package/src/domain/workspace.ts +76 -4
  106. package/src/mux/control-plane-op-queue.ts +93 -7
  107. package/src/mux/conversation-rail.ts +28 -71
  108. package/src/mux/dual-pane-core.ts +13 -13
  109. package/src/mux/harness-core-ui.ts +313 -42
  110. package/src/mux/input-shortcuts.ts +22 -112
  111. package/src/mux/keybinding-catalog.ts +340 -0
  112. package/src/mux/keybinding-registry.ts +103 -0
  113. package/src/mux/live-mux/command-menu-open-in.ts +280 -0
  114. package/src/mux/live-mux/command-menu.ts +167 -4
  115. package/src/mux/live-mux/conversation-state.ts +13 -0
  116. package/src/mux/live-mux/directory-resolution.ts +1 -1
  117. package/src/mux/live-mux/git-parsing.ts +16 -0
  118. package/src/mux/live-mux/git-snapshot.ts +33 -2
  119. package/src/mux/live-mux/global-shortcut-handlers.ts +6 -0
  120. package/src/mux/live-mux/home-pane-drop.ts +1 -1
  121. package/src/mux/live-mux/home-pane-pointer.ts +10 -0
  122. package/src/mux/live-mux/input-forwarding.ts +59 -2
  123. package/src/mux/live-mux/left-nav-activation.ts +124 -7
  124. package/src/mux/live-mux/left-nav.ts +35 -0
  125. package/src/mux/live-mux/link-click.ts +292 -0
  126. package/src/mux/live-mux/modal-command-menu-handler.ts +46 -9
  127. package/src/mux/live-mux/modal-conversation-handlers.ts +5 -1
  128. package/src/mux/live-mux/modal-input-reducers.ts +106 -8
  129. package/src/mux/live-mux/modal-overlays.ts +210 -31
  130. package/src/mux/live-mux/modal-pointer.ts +3 -7
  131. package/src/mux/live-mux/modal-prompt-handlers.ts +107 -1
  132. package/src/mux/live-mux/modal-release-notes-handler.ts +111 -0
  133. package/src/mux/live-mux/modal-task-editor-handler.ts +16 -11
  134. package/src/mux/live-mux/pointer-routing.ts +5 -2
  135. package/src/mux/live-mux/project-pane-pointer.ts +8 -0
  136. package/src/mux/live-mux/rail-layout.ts +33 -30
  137. package/src/mux/live-mux/release-notes.ts +383 -0
  138. package/src/mux/live-mux/render-trace-analysis.ts +52 -7
  139. package/src/mux/live-mux/repository-folding.ts +3 -0
  140. package/src/mux/live-mux/selection.ts +0 -4
  141. package/src/mux/live-mux/session-diagnostics-paths.ts +21 -0
  142. package/src/mux/project-pane-github-review.ts +271 -0
  143. package/src/mux/render-frame.ts +4 -0
  144. package/src/mux/runtime-app/codex-live-mux-runtime.ts +5191 -0
  145. package/src/mux/task-composer.ts +21 -14
  146. package/src/mux/task-focused-pane.ts +118 -117
  147. package/src/mux/task-screen-keybindings.ts +19 -82
  148. package/src/mux/workspace-rail-model.ts +270 -104
  149. package/src/mux/workspace-rail.ts +45 -22
  150. package/src/pty/session-broker.ts +1 -1
  151. package/{scripts → src/recording}/terminal-recording-gif-lib.ts +2 -2
  152. package/src/services/control-plane.ts +50 -32
  153. package/src/services/conversation-lifecycle.ts +118 -87
  154. package/src/services/conversation-startup-hydration.ts +20 -12
  155. package/src/services/directory-hydration.ts +21 -16
  156. package/src/services/event-persistence.ts +7 -0
  157. package/src/services/left-rail-pointer-handler.ts +329 -0
  158. package/src/services/mux-ui-state-persistence.ts +5 -1
  159. package/src/services/recording.ts +34 -26
  160. package/src/services/runtime-command-menu-agent-tools.ts +1 -1
  161. package/src/services/runtime-control-actions.ts +79 -61
  162. package/src/services/runtime-control-plane-ops.ts +122 -83
  163. package/src/services/runtime-conversation-actions.ts +40 -26
  164. package/src/services/runtime-conversation-activation.ts +82 -30
  165. package/src/services/runtime-conversation-starter.ts +80 -48
  166. package/src/services/runtime-conversation-title-edit.ts +91 -80
  167. package/src/services/runtime-envelope-handler.ts +107 -105
  168. package/src/services/runtime-git-state.ts +42 -29
  169. package/src/services/runtime-layout-resize.ts +3 -1
  170. package/src/services/runtime-left-rail-render.ts +99 -63
  171. package/src/services/runtime-nim-cli-session.ts +438 -0
  172. package/src/services/runtime-nim-session.ts +705 -0
  173. package/src/services/runtime-nim-tool-bridge.ts +141 -0
  174. package/src/services/runtime-observed-event-projection-pipeline.ts +45 -0
  175. package/src/services/runtime-process-wiring.ts +29 -36
  176. package/src/services/runtime-project-pane-github-review-cache.ts +164 -0
  177. package/src/services/runtime-render-flush.ts +63 -70
  178. package/src/services/runtime-render-lifecycle.ts +65 -64
  179. package/src/services/runtime-render-orchestrator.ts +55 -45
  180. package/src/services/runtime-render-pipeline.ts +106 -103
  181. package/src/services/runtime-render-state.ts +62 -49
  182. package/src/services/runtime-repository-actions.ts +97 -70
  183. package/src/services/runtime-right-pane-render.ts +80 -53
  184. package/src/services/runtime-shutdown.ts +38 -35
  185. package/src/services/runtime-stream-subscriptions.ts +35 -27
  186. package/src/services/runtime-task-composer-persistence.ts +71 -59
  187. package/src/services/runtime-task-composer-snapshot.ts +14 -0
  188. package/src/services/runtime-task-editor-actions.ts +46 -29
  189. package/src/services/runtime-task-pane-actions.ts +220 -134
  190. package/src/services/runtime-task-pane-shortcuts.ts +323 -123
  191. package/src/services/runtime-workspace-observed-effect-queue.ts +25 -0
  192. package/src/services/runtime-workspace-observed-events.ts +33 -184
  193. package/src/services/runtime-workspace-observed-transition-policy.ts +228 -0
  194. package/src/services/session-diagnostics-store.ts +217 -0
  195. package/src/services/startup-background-resume.ts +26 -21
  196. package/src/services/startup-orchestrator.ts +16 -13
  197. package/src/services/startup-paint-tracker.ts +29 -21
  198. package/src/services/startup-persisted-conversation-queue.ts +19 -13
  199. package/src/services/startup-settled-gate.ts +25 -15
  200. package/src/services/startup-shutdown.ts +18 -22
  201. package/src/services/startup-state-hydration.ts +44 -34
  202. package/src/services/startup-visibility.ts +12 -4
  203. package/src/services/task-pane-selection-actions.ts +89 -72
  204. package/src/services/task-planning-hydration.ts +24 -18
  205. package/src/services/task-planning-observed-events.ts +50 -52
  206. package/src/services/workspace-observed-events.ts +66 -63
  207. package/src/storage/storage-lifecycle-core.ts +438 -0
  208. package/src/store/control-plane-store-normalize.ts +33 -242
  209. package/src/store/control-plane-store-types.ts +1 -35
  210. package/src/store/control-plane-store.ts +396 -56
  211. package/src/store/event-store.ts +397 -3
  212. package/src/terminal/snapshot-oracle.ts +207 -94
  213. package/src/ui/mux-theme.ts +112 -8
  214. package/src/ui/panes/home-gridfire.ts +40 -31
  215. package/src/ui/panes/home.ts +10 -2
  216. package/src/ui/panes/nim.ts +315 -0
  217. package/src/mux/live-mux/actions-task.ts +0 -115
  218. package/src/mux/live-mux/left-rail-actions.ts +0 -118
  219. package/src/mux/live-mux/left-rail-conversation-click.ts +0 -82
  220. package/src/mux/live-mux/left-rail-pointer.ts +0 -74
  221. package/src/mux/live-mux/task-pane-shortcuts.ts +0 -206
  222. package/src/services/runtime-directory-actions.ts +0 -164
  223. package/src/services/runtime-input-pipeline.ts +0 -50
  224. package/src/services/runtime-input-router.ts +0 -189
  225. package/src/services/runtime-main-pane-input.ts +0 -230
  226. package/src/services/runtime-modal-input.ts +0 -119
  227. package/src/services/runtime-navigation-input.ts +0 -197
  228. package/src/services/runtime-rail-input.ts +0 -278
  229. package/src/services/runtime-task-pane.ts +0 -62
  230. package/src/services/runtime-workspace-actions.ts +0 -158
  231. package/src/ui/conversation-input-forwarder.ts +0 -114
  232. package/src/ui/conversation-selection-input.ts +0 -103
  233. package/src/ui/global-shortcut-input.ts +0 -89
  234. package/src/ui/input.ts +0 -238
  235. package/src/ui/kit.ts +0 -509
  236. package/src/ui/left-nav-input.ts +0 -80
  237. package/src/ui/left-rail-pointer-input.ts +0 -148
  238. package/src/ui/repository-fold-input.ts +0 -91
  239. 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
  }