@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
@@ -10,7 +10,7 @@ interface RuntimeControlPlaneOpEvent {
10
10
  readonly waitMs: number;
11
11
  }
12
12
 
13
- interface RuntimeControlPlaneOpsOptions {
13
+ export interface RuntimeControlPlaneOpsOptions {
14
14
  readonly onFatal: (error: unknown) => void;
15
15
  readonly startPerfSpan: (
16
16
  name: string,
@@ -23,94 +23,43 @@ interface RuntimeControlPlaneOpsOptions {
23
23
  readonly schedule?: (callback: () => void) => void;
24
24
  }
25
25
 
26
- export class RuntimeControlPlaneOps {
27
- private readonly opSpans = new Map<
26
+ export interface RuntimeControlPlaneOps {
27
+ enqueueInteractive(task: () => Promise<void>, label?: string): void;
28
+ enqueueInteractiveLatest(
29
+ key: string,
30
+ task: (options: { readonly signal: AbortSignal }) => Promise<void>,
31
+ label?: string,
32
+ ): void;
33
+ enqueueBackgroundLatest(
34
+ key: string,
35
+ task: (options: { readonly signal: AbortSignal }) => Promise<void>,
36
+ label?: string,
37
+ ): void;
38
+ enqueueBackground(task: () => Promise<void>, label?: string): void;
39
+ waitForDrain(): Promise<void>;
40
+ metrics(): {
41
+ readonly interactiveQueued: number;
42
+ readonly backgroundQueued: number;
43
+ readonly running: boolean;
44
+ };
45
+ }
46
+
47
+ export function createRuntimeControlPlaneOps(
48
+ options: RuntimeControlPlaneOpsOptions,
49
+ ): RuntimeControlPlaneOps {
50
+ const opSpans = new Map<
28
51
  number,
29
52
  {
30
53
  end: (attrs?: PerfAttrs) => void;
31
54
  }
32
55
  >();
33
56
 
34
- private readonly queue: ControlPlaneOpQueue;
35
-
36
- constructor(private readonly options: RuntimeControlPlaneOpsOptions) {
37
- this.queue = new ControlPlaneOpQueue({
38
- ...(options.nowMs === undefined
39
- ? {}
40
- : {
41
- nowMs: options.nowMs,
42
- }),
43
- ...(options.schedule === undefined
44
- ? {}
45
- : {
46
- schedule: options.schedule,
47
- }),
48
- onFatal: (error: unknown) => {
49
- this.options.onFatal(error);
50
- },
51
- onEnqueued: (event, metrics) => {
52
- this.options.recordPerfEvent('mux.control-plane.op.enqueued', {
53
- id: event.id,
54
- label: event.label,
55
- priority: event.priority,
56
- interactiveQueued: metrics.interactiveQueued,
57
- backgroundQueued: metrics.backgroundQueued,
58
- });
59
- },
60
- onStart: (event, metrics) => {
61
- const opSpan = this.options.startPerfSpan('mux.control-plane.op', {
62
- id: event.id,
63
- label: event.label,
64
- priority: event.priority,
65
- waitMs: event.waitMs,
66
- });
67
- this.opSpans.set(event.id, opSpan);
68
- this.options.recordPerfEvent('mux.control-plane.op.start', {
69
- id: event.id,
70
- label: event.label,
71
- priority: event.priority,
72
- waitMs: event.waitMs,
73
- interactiveQueued: metrics.interactiveQueued,
74
- backgroundQueued: metrics.backgroundQueued,
75
- });
76
- },
77
- onSuccess: (event) => {
78
- this.endSpan(event, 'ok');
79
- },
80
- onError: (event, _metrics, error) => {
81
- const message = error instanceof Error ? error.message : String(error);
82
- this.endSpan(event, 'error', message);
83
- this.options.writeStderr(`[mux] control-plane error ${message}\n`);
84
- },
85
- });
86
- }
87
-
88
- enqueueInteractive(task: () => Promise<void>, label = 'interactive-op'): void {
89
- this.queue.enqueueInteractive(task, label);
90
- }
91
-
92
- enqueueBackground(task: () => Promise<void>, label = 'background-op'): void {
93
- this.queue.enqueueBackground(task, label);
94
- }
95
-
96
- async waitForDrain(): Promise<void> {
97
- await this.queue.waitForDrain();
98
- }
99
-
100
- metrics(): {
101
- readonly interactiveQueued: number;
102
- readonly backgroundQueued: number;
103
- readonly running: boolean;
104
- } {
105
- return this.queue.metrics();
106
- }
107
-
108
- private endSpan(
57
+ const endSpan = (
109
58
  event: RuntimeControlPlaneOpEvent,
110
- status: 'ok' | 'error',
59
+ status: 'ok' | 'error' | 'canceled',
111
60
  message?: string,
112
- ): void {
113
- const opSpan = this.opSpans.get(event.id);
61
+ ): void => {
62
+ const opSpan = opSpans.get(event.id);
114
63
  if (opSpan === undefined) {
115
64
  return;
116
65
  }
@@ -126,6 +75,96 @@ export class RuntimeControlPlaneOps {
126
75
  message,
127
76
  }),
128
77
  });
129
- this.opSpans.delete(event.id);
130
- }
78
+ opSpans.delete(event.id);
79
+ };
80
+
81
+ const queue = new ControlPlaneOpQueue({
82
+ ...(options.nowMs === undefined
83
+ ? {}
84
+ : {
85
+ nowMs: options.nowMs,
86
+ }),
87
+ ...(options.schedule === undefined
88
+ ? {}
89
+ : {
90
+ schedule: options.schedule,
91
+ }),
92
+ onFatal: (error: unknown) => {
93
+ options.onFatal(error);
94
+ },
95
+ onEnqueued: (event, metrics) => {
96
+ options.recordPerfEvent('mux.control-plane.op.enqueued', {
97
+ id: event.id,
98
+ label: event.label,
99
+ priority: event.priority,
100
+ interactiveQueued: metrics.interactiveQueued,
101
+ backgroundQueued: metrics.backgroundQueued,
102
+ });
103
+ },
104
+ onStart: (event, metrics) => {
105
+ const opSpan = options.startPerfSpan('mux.control-plane.op', {
106
+ id: event.id,
107
+ label: event.label,
108
+ priority: event.priority,
109
+ waitMs: event.waitMs,
110
+ });
111
+ opSpans.set(event.id, opSpan);
112
+ options.recordPerfEvent('mux.control-plane.op.start', {
113
+ id: event.id,
114
+ label: event.label,
115
+ priority: event.priority,
116
+ waitMs: event.waitMs,
117
+ interactiveQueued: metrics.interactiveQueued,
118
+ backgroundQueued: metrics.backgroundQueued,
119
+ });
120
+ },
121
+ onSuccess: (event) => {
122
+ endSpan(event, 'ok');
123
+ },
124
+ onError: (event, _metrics, error) => {
125
+ const message = error instanceof Error ? error.message : String(error);
126
+ endSpan(event, 'error', message);
127
+ options.writeStderr(`[mux] control-plane error ${message}\n`);
128
+ },
129
+ onCanceled: (event) => {
130
+ endSpan(event, 'canceled');
131
+ },
132
+ });
133
+
134
+ return {
135
+ enqueueInteractive: (task: () => Promise<void>, label = 'interactive-op'): void => {
136
+ queue.enqueueInteractive(async () => {
137
+ await task();
138
+ }, label);
139
+ },
140
+ enqueueInteractiveLatest: (
141
+ key: string,
142
+ task: (options: { readonly signal: AbortSignal }) => Promise<void>,
143
+ label = 'interactive-op',
144
+ ): void => {
145
+ queue.enqueueInteractive(task, label, {
146
+ key,
147
+ supersede: 'pending-and-running',
148
+ });
149
+ },
150
+ enqueueBackgroundLatest: (
151
+ key: string,
152
+ task: (options: { readonly signal: AbortSignal }) => Promise<void>,
153
+ label = 'background-op',
154
+ ): void => {
155
+ queue.enqueueBackground(task, label, {
156
+ key,
157
+ supersede: 'pending-and-running',
158
+ });
159
+ },
160
+ enqueueBackground: (task: () => Promise<void>, label = 'background-op'): void => {
161
+ queue.enqueueBackground(async () => {
162
+ await task();
163
+ }, label);
164
+ },
165
+ waitForDrain: async (): Promise<void> => {
166
+ await queue.waitForDrain();
167
+ },
168
+ metrics: () => queue.metrics(),
169
+ };
131
170
  }
@@ -52,19 +52,25 @@ export interface RuntimeConversationActionsOptions<TControllerRecord> {
52
52
  readonly markDirty: () => void;
53
53
  }
54
54
 
55
- export class RuntimeConversationActions<TControllerRecord> {
56
- constructor(private readonly options: RuntimeConversationActionsOptions<TControllerRecord>) {}
55
+ export interface RuntimeConversationActions {
56
+ createAndActivateConversationInDirectory(directoryId: string, agentType: string): Promise<void>;
57
+ openOrCreateCritiqueConversationInDirectory(directoryId: string): Promise<void>;
58
+ takeoverConversation(sessionId: string): Promise<void>;
59
+ }
57
60
 
58
- async createAndActivateConversationInDirectory(
61
+ export function createRuntimeConversationActions<TControllerRecord>(
62
+ options: RuntimeConversationActionsOptions<TControllerRecord>,
63
+ ): RuntimeConversationActions {
64
+ const createAndActivateConversationInDirectory = async (
59
65
  directoryId: string,
60
66
  agentType: string,
61
- ): Promise<void> {
67
+ ): Promise<void> => {
62
68
  await createAndActivateConversationInDirectoryFn({
63
69
  directoryId,
64
70
  agentType,
65
- createConversationId: this.options.createConversationId,
71
+ createConversationId: options.createConversationId,
66
72
  createConversationRecord: async (sessionId, targetDirectoryId, targetAgentType) => {
67
- await this.options.controlPlaneService.createConversation({
73
+ await options.controlPlaneService.createConversation({
68
74
  conversationId: sessionId,
69
75
  directoryId: targetDirectoryId,
70
76
  title: '',
@@ -72,42 +78,50 @@ export class RuntimeConversationActions<TControllerRecord> {
72
78
  adapterState: {},
73
79
  });
74
80
  },
75
- ensureConversation: this.options.ensureConversation,
76
- noteGitActivity: this.options.noteGitActivity,
77
- startConversation: this.options.startConversation,
78
- activateConversation: this.options.activateConversation,
81
+ ensureConversation: options.ensureConversation,
82
+ noteGitActivity: options.noteGitActivity,
83
+ startConversation: options.startConversation,
84
+ activateConversation: options.activateConversation,
79
85
  });
80
- }
86
+ };
81
87
 
82
- async openOrCreateCritiqueConversationInDirectory(directoryId: string): Promise<void> {
88
+ const openOrCreateCritiqueConversationInDirectory = async (
89
+ directoryId: string,
90
+ ): Promise<void> => {
83
91
  await openOrCreateCritiqueConversationInDirectoryFn({
84
92
  directoryId,
85
- orderedConversationIds: this.options.orderedConversationIds,
86
- conversationById: this.options.conversationById,
87
- activateConversation: this.options.activateConversation,
93
+ orderedConversationIds: options.orderedConversationIds,
94
+ conversationById: options.conversationById,
95
+ activateConversation: options.activateConversation,
88
96
  createAndActivateCritiqueConversationInDirectory: async (targetDirectoryId) => {
89
- await this.createAndActivateConversationInDirectory(targetDirectoryId, 'critique');
97
+ await createAndActivateConversationInDirectory(targetDirectoryId, 'critique');
90
98
  },
91
99
  });
92
- }
100
+ };
93
101
 
94
- async takeoverConversation(sessionId: string): Promise<void> {
102
+ const takeoverConversation = async (sessionId: string): Promise<void> => {
95
103
  await takeoverConversationFn({
96
104
  sessionId,
97
- conversationsHas: this.options.conversationsHas,
105
+ conversationsHas: options.conversationsHas,
98
106
  claimSession: async (targetSessionId) => {
99
- return await this.options.controlPlaneService.claimSession({
107
+ return await options.controlPlaneService.claimSession({
100
108
  sessionId: targetSessionId,
101
- controllerId: this.options.muxControllerId,
109
+ controllerId: options.muxControllerId,
102
110
  controllerType: 'human',
103
- controllerLabel: this.options.muxControllerLabel,
111
+ controllerLabel: options.muxControllerLabel,
104
112
  reason: 'human takeover',
105
113
  takeover: true,
106
114
  });
107
115
  },
108
- applyController: this.options.applyController,
109
- setLastEventNow: this.options.setLastEventNow,
110
- markDirty: this.options.markDirty,
116
+ applyController: options.applyController,
117
+ setLastEventNow: options.setLastEventNow,
118
+ markDirty: options.markDirty,
111
119
  });
112
- }
120
+ };
121
+
122
+ return {
123
+ createAndActivateConversationInDirectory,
124
+ openOrCreateCritiqueConversationInDirectory,
125
+ takeoverConversation,
126
+ };
113
127
  }
@@ -24,80 +24,107 @@ export interface RuntimeConversationActivationOptions {
24
24
  readonly markDirty: () => void;
25
25
  }
26
26
 
27
- export class RuntimeConversationActivation {
28
- constructor(private readonly options: RuntimeConversationActivationOptions) {}
27
+ export interface RuntimeConversationActivation {
28
+ activateConversation(sessionId: string, input?: { readonly signal?: AbortSignal }): Promise<void>;
29
+ }
30
+
31
+ export function createRuntimeConversationActivation(
32
+ options: RuntimeConversationActivationOptions,
33
+ ): RuntimeConversationActivation {
34
+ async function attachConversationWithRecovery(
35
+ sessionId: string,
36
+ signal: AbortSignal | undefined,
37
+ ): Promise<boolean> {
38
+ try {
39
+ await options.attachConversation(sessionId);
40
+ return !(signal?.aborted ?? false);
41
+ } catch (error: unknown) {
42
+ if (!options.isSessionNotFoundError(error) && !options.isSessionNotLiveError(error)) {
43
+ throw error;
44
+ }
45
+ options.markSessionUnavailable(sessionId);
46
+ await options.startConversation(sessionId);
47
+ if (signal?.aborted) {
48
+ return false;
49
+ }
50
+ await options.attachConversation(sessionId);
51
+ return !(signal?.aborted ?? false);
52
+ }
53
+ }
29
54
 
30
- async activateConversation(sessionId: string): Promise<void> {
31
- if (this.options.getActiveConversationId() === sessionId) {
32
- if (!this.options.isConversationPaneMode()) {
33
- const targetConversation = this.options.conversationById(sessionId);
34
- this.options.enterConversationPaneForActiveSession(sessionId);
35
- this.options.noteGitActivity(targetConversation?.directoryId ?? null);
55
+ async function activateConversation(
56
+ sessionId: string,
57
+ input: { readonly signal?: AbortSignal } = {},
58
+ ): Promise<void> {
59
+ const signal = input.signal;
60
+ if (signal?.aborted) {
61
+ return;
62
+ }
63
+ if (options.getActiveConversationId() === sessionId) {
64
+ if (!options.isConversationPaneMode()) {
65
+ const targetConversation = options.conversationById(sessionId);
36
66
  if (
37
67
  targetConversation !== undefined &&
38
68
  !targetConversation.live &&
39
69
  targetConversation.status !== 'exited'
40
70
  ) {
41
- await this.options.startConversation(sessionId);
71
+ await options.startConversation(sessionId);
72
+ if (signal?.aborted) {
73
+ return;
74
+ }
42
75
  }
43
76
  if (targetConversation?.status !== 'exited') {
44
- try {
45
- await this.options.attachConversation(sessionId);
46
- } catch (error: unknown) {
47
- if (
48
- !this.options.isSessionNotFoundError(error) &&
49
- !this.options.isSessionNotLiveError(error)
50
- ) {
51
- throw error;
52
- }
53
- this.options.markSessionUnavailable(sessionId);
54
- await this.options.startConversation(sessionId);
55
- await this.options.attachConversation(sessionId);
77
+ const attached = await attachConversationWithRecovery(sessionId, signal);
78
+ if (!attached) {
79
+ return;
56
80
  }
57
81
  }
58
- this.options.schedulePtyResizeImmediate();
59
- this.options.markDirty();
82
+ options.enterConversationPaneForActiveSession(sessionId);
83
+ options.noteGitActivity(targetConversation?.directoryId ?? null);
84
+ options.schedulePtyResizeImmediate();
85
+ options.markDirty();
60
86
  }
61
87
  return;
62
88
  }
63
89
 
64
- this.options.stopConversationTitleEditForOtherSession(sessionId);
65
- const previousActiveId = this.options.getActiveConversationId();
66
- this.options.clearSelectionState();
90
+ options.stopConversationTitleEditForOtherSession(sessionId);
91
+ const previousActiveId = options.getActiveConversationId();
92
+ options.clearSelectionState();
67
93
  if (previousActiveId !== null) {
68
- await this.options.detachConversation(previousActiveId);
94
+ await options.detachConversation(previousActiveId);
95
+ if (signal?.aborted) {
96
+ return;
97
+ }
69
98
  }
70
- this.options.setActiveConversationId(sessionId);
71
- this.options.enterConversationPaneForSessionSwitch(sessionId);
72
99
 
73
- const targetConversation = this.options.conversationById(sessionId);
74
- this.options.noteGitActivity(targetConversation?.directoryId ?? null);
100
+ const targetConversation = options.conversationById(sessionId);
75
101
 
76
102
  if (
77
103
  targetConversation !== undefined &&
78
104
  !targetConversation.live &&
79
105
  targetConversation.status !== 'exited'
80
106
  ) {
81
- await this.options.startConversation(sessionId);
107
+ await options.startConversation(sessionId);
108
+ if (signal?.aborted) {
109
+ return;
110
+ }
82
111
  }
83
112
 
84
113
  if (targetConversation?.status !== 'exited') {
85
- try {
86
- await this.options.attachConversation(sessionId);
87
- } catch (error: unknown) {
88
- if (
89
- !this.options.isSessionNotFoundError(error) &&
90
- !this.options.isSessionNotLiveError(error)
91
- ) {
92
- throw error;
93
- }
94
- this.options.markSessionUnavailable(sessionId);
95
- await this.options.startConversation(sessionId);
96
- await this.options.attachConversation(sessionId);
114
+ const attached = await attachConversationWithRecovery(sessionId, signal);
115
+ if (!attached) {
116
+ return;
97
117
  }
98
118
  }
99
119
 
100
- this.options.schedulePtyResizeImmediate();
101
- this.options.markDirty();
120
+ options.setActiveConversationId(sessionId);
121
+ options.enterConversationPaneForSessionSwitch(sessionId);
122
+ options.noteGitActivity(targetConversation?.directoryId ?? null);
123
+ options.schedulePtyResizeImmediate();
124
+ options.markDirty();
102
125
  }
126
+
127
+ return {
128
+ activateConversation,
129
+ };
103
130
  }
@@ -83,99 +83,113 @@ export interface RuntimeConversationStarterOptions<
83
83
  readonly subscribeConversationEvents: (sessionId: string) => Promise<void>;
84
84
  }
85
85
 
86
- export class RuntimeConversationStarter<
86
+ export interface RuntimeConversationStarter<
87
87
  TConversation extends RuntimeConversationStarterConversationRecord,
88
- TSessionSummary,
89
88
  > {
90
- constructor(
91
- private readonly options: RuntimeConversationStarterOptions<TConversation, TSessionSummary>,
92
- ) {}
89
+ startConversation(sessionId: string): Promise<TConversation>;
90
+ }
91
+
92
+ export function createRuntimeConversationStarter<
93
+ TConversation extends RuntimeConversationStarterConversationRecord,
94
+ TSessionSummary,
95
+ >(
96
+ options: RuntimeConversationStarterOptions<TConversation, TSessionSummary>,
97
+ ): RuntimeConversationStarter<TConversation> {
98
+ function endStartCommandSpanIfTarget(
99
+ sessionId: string,
100
+ payload: RuntimeConversationStarterSpanAttributes,
101
+ ): void {
102
+ if (options.firstPaintTargetSessionId() !== sessionId) {
103
+ return;
104
+ }
105
+ options.endStartCommandSpan(payload);
106
+ }
93
107
 
94
- async startConversation(sessionId: string): Promise<TConversation> {
95
- return await this.options.runWithStartInFlight(sessionId, async () => {
96
- const existing = this.options.conversationById(sessionId);
97
- const targetConversation = existing ?? this.options.ensureConversation(sessionId);
98
- const agentType = this.options.normalizeThreadAgentType(targetConversation.agentType);
108
+ async function startConversation(sessionId: string): Promise<TConversation> {
109
+ return await options.runWithStartInFlight(sessionId, async () => {
110
+ const existing = options.conversationById(sessionId);
111
+ const targetConversation = existing ?? options.ensureConversation(sessionId);
112
+ const agentType = options.normalizeThreadAgentType(targetConversation.agentType);
99
113
  const baseArgsForAgent =
100
114
  agentType === 'codex'
101
- ? this.options.codexArgs
115
+ ? options.codexArgs
102
116
  : agentType === 'critique'
103
- ? this.options.critiqueDefaultArgs
117
+ ? options.critiqueDefaultArgs
104
118
  : [];
105
- const sessionCwd = this.options.sessionCwdForConversation(targetConversation);
106
- const launchArgs = this.options.buildLaunchArgs({
119
+ const sessionCwd = options.sessionCwdForConversation(targetConversation);
120
+ const launchArgs = options.buildLaunchArgs({
107
121
  agentType,
108
122
  baseArgsForAgent,
109
123
  adapterState: targetConversation.adapterState,
110
124
  sessionCwd,
111
125
  });
112
- targetConversation.launchCommand = this.options.formatCommandForDebugBar(
113
- this.options.launchCommandForAgent(agentType),
126
+ targetConversation.launchCommand = options.formatCommandForDebugBar(
127
+ options.launchCommandForAgent(agentType),
114
128
  launchArgs,
115
129
  );
116
130
 
117
131
  if (existing?.live === true) {
118
- this.endStartCommandSpanIfTarget(sessionId, {
132
+ endStartCommandSpanIfTarget(sessionId, {
119
133
  alreadyLive: true,
120
134
  });
121
135
  return existing;
122
136
  }
123
137
 
124
- const startSpan = this.options.startConversationSpan(sessionId);
138
+ const startSpan = options.startConversationSpan(sessionId);
125
139
  targetConversation.lastOutputCursor = 0;
126
- const layout = this.options.layout();
140
+ const layout = options.layout();
127
141
  const ptyStartInput: RuntimeConversationStarterPtyStartInput = {
128
142
  sessionId,
129
143
  args: launchArgs,
130
- env: this.options.sessionEnv,
144
+ env: options.sessionEnv,
131
145
  cwd: sessionCwd,
132
146
  initialCols: layout.rightCols,
133
147
  initialRows: layout.paneRows,
134
148
  };
135
- if (this.options.worktreeId !== undefined) {
136
- ptyStartInput.worktreeId = this.options.worktreeId;
149
+ if (options.worktreeId !== undefined) {
150
+ ptyStartInput.worktreeId = options.worktreeId;
137
151
  }
138
- if (this.options.terminalForegroundHex !== undefined) {
139
- ptyStartInput.terminalForegroundHex = this.options.terminalForegroundHex;
152
+ if (options.terminalForegroundHex !== undefined) {
153
+ ptyStartInput.terminalForegroundHex = options.terminalForegroundHex;
140
154
  }
141
- if (this.options.terminalBackgroundHex !== undefined) {
142
- ptyStartInput.terminalBackgroundHex = this.options.terminalBackgroundHex;
155
+ if (options.terminalBackgroundHex !== undefined) {
156
+ ptyStartInput.terminalBackgroundHex = options.terminalBackgroundHex;
143
157
  }
144
158
  let startedSession = false;
145
159
  try {
146
- await this.options.startPtySession(ptyStartInput);
160
+ await options.startPtySession(ptyStartInput);
147
161
  startedSession = true;
148
162
  } catch (error: unknown) {
149
163
  if (!isSessionAlreadyExistsError(error)) {
150
164
  throw error;
151
165
  }
152
166
  }
153
- this.options.setPtySize(sessionId, {
167
+ options.setPtySize(sessionId, {
154
168
  cols: layout.rightCols,
155
169
  rows: layout.paneRows,
156
170
  });
157
- this.options.sendResize(sessionId, layout.rightCols, layout.paneRows);
171
+ options.sendResize(sessionId, layout.rightCols, layout.paneRows);
158
172
  if (startedSession) {
159
- this.endStartCommandSpanIfTarget(sessionId, {
173
+ endStartCommandSpanIfTarget(sessionId, {
160
174
  alreadyLive: false,
161
175
  argCount: launchArgs.length,
162
176
  resumed: launchArgs[0] === 'resume',
163
177
  });
164
178
  } else {
165
- this.endStartCommandSpanIfTarget(sessionId, {
179
+ endStartCommandSpanIfTarget(sessionId, {
166
180
  alreadyLive: true,
167
181
  recoveredDuplicateStart: true,
168
182
  });
169
183
  }
170
- const state = this.options.ensureConversation(sessionId);
184
+ const state = options.ensureConversation(sessionId);
171
185
  if (startedSession) {
172
- this.options.recordStartCommand(sessionId, launchArgs);
186
+ options.recordStartCommand(sessionId, launchArgs);
173
187
  }
174
- const statusSummary = await this.options.getSessionStatus(sessionId);
188
+ const statusSummary = await options.getSessionStatus(sessionId);
175
189
  if (statusSummary !== null) {
176
- this.options.upsertFromSessionSummary(statusSummary);
190
+ options.upsertFromSessionSummary(statusSummary);
177
191
  }
178
- await this.options.subscribeConversationEvents(sessionId);
192
+ await options.subscribeConversationEvents(sessionId);
179
193
  startSpan.end({
180
194
  live: state.live,
181
195
  });
@@ -183,13 +197,7 @@ export class RuntimeConversationStarter<
183
197
  });
184
198
  }
185
199
 
186
- private endStartCommandSpanIfTarget(
187
- sessionId: string,
188
- payload: RuntimeConversationStarterSpanAttributes,
189
- ): void {
190
- if (this.options.firstPaintTargetSessionId() !== sessionId) {
191
- return;
192
- }
193
- this.options.endStartCommandSpan(payload);
194
- }
200
+ return {
201
+ startConversation,
202
+ };
195
203
  }