@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
@@ -20,100 +20,44 @@ export interface RuntimeConversationTitleEditServiceOptions<
20
20
  readonly clearDebounceTimer?: (timer: NodeJS.Timeout) => void;
21
21
  }
22
22
 
23
- export class RuntimeConversationTitleEditService<
24
- TConversation extends ConversationTitleRecordLike,
25
- > {
26
- private readonly setDebounceTimer: (callback: () => void, ms: number) => NodeJS.Timeout;
27
- private readonly clearDebounceTimer: (timer: NodeJS.Timeout) => void;
28
-
29
- constructor(private readonly options: RuntimeConversationTitleEditServiceOptions<TConversation>) {
30
- this.setDebounceTimer = options.setDebounceTimer ?? setTimeout;
31
- this.clearDebounceTimer = options.clearDebounceTimer ?? clearTimeout;
32
- }
33
-
34
- clearCurrentTimer(): void {
35
- const edit = this.options.workspace.conversationTitleEdit;
36
- if (edit === null) {
37
- return;
38
- }
39
- this.clearTimer(edit);
40
- }
41
-
42
- schedulePersist(): void {
43
- const edit = this.options.workspace.conversationTitleEdit;
44
- if (edit === null) {
45
- return;
46
- }
47
- this.clearTimer(edit);
48
- edit.debounceTimer = this.setDebounceTimer(() => {
49
- const latestEdit = this.options.workspace.conversationTitleEdit;
50
- if (latestEdit === null || latestEdit.conversationId !== edit.conversationId) {
51
- return;
52
- }
53
- latestEdit.debounceTimer = null;
54
- this.queuePersist(latestEdit, 'debounced');
55
- }, this.options.debounceMs);
56
- edit.debounceTimer.unref?.();
57
- }
58
-
59
- stop(persistPending: boolean): void {
60
- const edit = this.options.workspace.conversationTitleEdit;
61
- if (edit === null) {
62
- return;
63
- }
64
- this.clearTimer(edit);
65
- if (persistPending) {
66
- this.queuePersist(edit, 'flush');
67
- }
68
- this.options.workspace.conversationTitleEdit = null;
69
- this.options.markDirty();
70
- }
23
+ export interface RuntimeConversationTitleEditService {
24
+ clearCurrentTimer(): void;
25
+ schedulePersist(): void;
26
+ stop(persistPending: boolean): void;
27
+ begin(conversationId: string): void;
28
+ }
71
29
 
72
- begin(conversationId: string): void {
73
- const target = this.options.conversationById(conversationId);
74
- if (target === undefined) {
75
- return;
76
- }
77
- if (this.options.workspace.conversationTitleEdit?.conversationId === conversationId) {
78
- return;
79
- }
80
- if (this.options.workspace.conversationTitleEdit !== null) {
81
- this.stop(true);
82
- }
83
- this.options.workspace.conversationTitleEdit = {
84
- conversationId,
85
- value: target.title,
86
- lastSavedValue: target.title,
87
- error: null,
88
- persistInFlight: false,
89
- debounceTimer: null,
90
- };
91
- this.options.markDirty();
92
- }
30
+ export function createRuntimeConversationTitleEditService<
31
+ TConversation extends ConversationTitleRecordLike,
32
+ >(
33
+ options: RuntimeConversationTitleEditServiceOptions<TConversation>,
34
+ ): RuntimeConversationTitleEditService {
35
+ const setDebounceTimer = options.setDebounceTimer ?? setTimeout;
36
+ const clearDebounceTimer = options.clearDebounceTimer ?? clearTimeout;
93
37
 
94
- private clearTimer(edit: ConversationTitleEditState): void {
38
+ function clearTimer(edit: ConversationTitleEditState): void {
95
39
  if (edit.debounceTimer !== null) {
96
- this.clearDebounceTimer(edit.debounceTimer);
40
+ clearDebounceTimer(edit.debounceTimer);
97
41
  edit.debounceTimer = null;
98
42
  }
99
43
  }
100
44
 
101
- private queuePersist(edit: ConversationTitleEditState, reason: 'debounced' | 'flush'): void {
45
+ function queuePersist(edit: ConversationTitleEditState, reason: 'debounced' | 'flush'): void {
102
46
  const titleToPersist = edit.value;
103
47
  if (titleToPersist === edit.lastSavedValue) {
104
48
  return;
105
49
  }
106
50
  edit.persistInFlight = true;
107
- this.options.markDirty();
108
- this.options.queueControlPlaneOp(async () => {
51
+ options.markDirty();
52
+ options.queueControlPlaneOp(async () => {
109
53
  try {
110
- const parsed = await this.options.updateConversationTitle({
54
+ const parsed = await options.updateConversationTitle({
111
55
  conversationId: edit.conversationId,
112
56
  title: titleToPersist,
113
57
  });
114
58
  const persistedTitle = parsed?.title ?? titleToPersist;
115
- const latestConversation = this.options.conversationById(edit.conversationId);
116
- const latestEdit = this.options.workspace.conversationTitleEdit;
59
+ const latestConversation = options.conversationById(edit.conversationId);
60
+ const latestEdit = options.workspace.conversationTitleEdit;
117
61
  const shouldApplyToConversation =
118
62
  latestEdit === null ||
119
63
  latestEdit.conversationId !== edit.conversationId ||
@@ -128,7 +72,7 @@ export class RuntimeConversationTitleEditService<
128
72
  }
129
73
  }
130
74
  } catch (error: unknown) {
131
- const latestEdit = this.options.workspace.conversationTitleEdit;
75
+ const latestEdit = options.workspace.conversationTitleEdit;
132
76
  if (
133
77
  latestEdit !== null &&
134
78
  latestEdit.conversationId === edit.conversationId &&
@@ -138,12 +82,79 @@ export class RuntimeConversationTitleEditService<
138
82
  }
139
83
  throw error;
140
84
  } finally {
141
- const latestEdit = this.options.workspace.conversationTitleEdit;
85
+ const latestEdit = options.workspace.conversationTitleEdit;
142
86
  if (latestEdit !== null && latestEdit.conversationId === edit.conversationId) {
143
87
  latestEdit.persistInFlight = false;
144
88
  }
145
- this.options.markDirty();
89
+ options.markDirty();
146
90
  }
147
91
  }, `title-edit-${reason}:${edit.conversationId}`);
148
92
  }
93
+
94
+ function clearCurrentTimer(): void {
95
+ const edit = options.workspace.conversationTitleEdit;
96
+ if (edit === null) {
97
+ return;
98
+ }
99
+ clearTimer(edit);
100
+ }
101
+
102
+ function schedulePersist(): void {
103
+ const edit = options.workspace.conversationTitleEdit;
104
+ if (edit === null) {
105
+ return;
106
+ }
107
+ clearTimer(edit);
108
+ edit.debounceTimer = setDebounceTimer(() => {
109
+ const latestEdit = options.workspace.conversationTitleEdit;
110
+ if (latestEdit === null || latestEdit.conversationId !== edit.conversationId) {
111
+ return;
112
+ }
113
+ latestEdit.debounceTimer = null;
114
+ queuePersist(latestEdit, 'debounced');
115
+ }, options.debounceMs);
116
+ edit.debounceTimer.unref?.();
117
+ }
118
+
119
+ function stop(persistPending: boolean): void {
120
+ const edit = options.workspace.conversationTitleEdit;
121
+ if (edit === null) {
122
+ return;
123
+ }
124
+ clearTimer(edit);
125
+ if (persistPending) {
126
+ queuePersist(edit, 'flush');
127
+ }
128
+ options.workspace.conversationTitleEdit = null;
129
+ options.markDirty();
130
+ }
131
+
132
+ function begin(conversationId: string): void {
133
+ const target = options.conversationById(conversationId);
134
+ if (target === undefined) {
135
+ return;
136
+ }
137
+ if (options.workspace.conversationTitleEdit?.conversationId === conversationId) {
138
+ return;
139
+ }
140
+ if (options.workspace.conversationTitleEdit !== null) {
141
+ stop(true);
142
+ }
143
+ options.workspace.conversationTitleEdit = {
144
+ conversationId,
145
+ value: target.title,
146
+ lastSavedValue: target.title,
147
+ error: null,
148
+ persistInFlight: false,
149
+ debounceTimer: null,
150
+ };
151
+ options.markDirty();
152
+ }
153
+
154
+ return {
155
+ clearCurrentTimer,
156
+ schedulePersist,
157
+ stop,
158
+ begin,
159
+ };
149
160
  }
@@ -18,7 +18,13 @@ interface RuntimeEnvelopeOutputIngestResult<TConversation extends RuntimeEnvelop
18
18
  readonly previousCursor: number;
19
19
  }
20
20
 
21
- interface RuntimeEnvelopeHandlerOptions<
21
+ interface RuntimeObservedEventEnvelopeInput {
22
+ readonly subscriptionId: string;
23
+ readonly cursor: number;
24
+ readonly event: StreamObservedEvent;
25
+ }
26
+
27
+ export interface RuntimeEnvelopeHandlerOptions<
22
28
  TConversation extends RuntimeEnvelopeConversationLike,
23
29
  TNormalizedEvent extends { ts: string },
24
30
  > {
@@ -73,126 +79,122 @@ interface RuntimeEnvelopeHandlerOptions<
73
79
  readonly nowIso: () => string;
74
80
  readonly recordOutputHandled: (durationMs: number) => void;
75
81
  readonly conversationById: (sessionId: string) => TConversation | undefined;
76
- readonly applyObservedWorkspaceEvent: (event: StreamObservedEvent) => void;
77
- readonly applyObservedGitStatusEvent: (event: StreamObservedEvent) => void;
78
- readonly applyObservedTaskPlanningEvent: (event: StreamObservedEvent) => void;
82
+ readonly applyObservedEvent: (input: RuntimeObservedEventEnvelopeInput) => void;
79
83
  readonly idFactory: () => string;
80
84
  }
81
85
 
82
- export class RuntimeEnvelopeHandler<
86
+ export function handleRuntimeEnvelope<
83
87
  TConversation extends RuntimeEnvelopeConversationLike,
84
88
  TNormalizedEvent extends { ts: string },
85
- > {
86
- constructor(
87
- private readonly options: RuntimeEnvelopeHandlerOptions<TConversation, TNormalizedEvent>,
88
- ) {}
89
-
90
- handleEnvelope(envelope: StreamServerEnvelope): void {
91
- if (envelope.kind === 'pty.output') {
92
- const outputHandledStartedAtNs = this.options.perfNowNs();
93
- if (this.options.isRemoved(envelope.sessionId)) {
94
- return;
95
- }
96
- const chunk = Buffer.from(envelope.chunkBase64, 'base64');
97
- const outputIngest = this.options.ingestOutputChunk({
89
+ >(
90
+ options: RuntimeEnvelopeHandlerOptions<TConversation, TNormalizedEvent>,
91
+ envelope: StreamServerEnvelope,
92
+ ): void {
93
+ if (envelope.kind === 'pty.output') {
94
+ const outputHandledStartedAtNs = options.perfNowNs();
95
+ if (options.isRemoved(envelope.sessionId)) {
96
+ return;
97
+ }
98
+ const chunk = Buffer.from(envelope.chunkBase64, 'base64');
99
+ const outputIngest = options.ingestOutputChunk({
100
+ sessionId: envelope.sessionId,
101
+ cursor: envelope.cursor,
102
+ chunk,
103
+ ensureConversation: options.ensureConversation,
104
+ });
105
+ const conversation = outputIngest.conversation;
106
+ options.noteGitActivity(conversation.directoryId);
107
+ options.recordOutputChunk({
108
+ sessionId: envelope.sessionId,
109
+ chunkLength: chunk.length,
110
+ active: options.activeConversationId() === envelope.sessionId,
111
+ });
112
+ options.startupOutputChunk(envelope.sessionId, chunk.length);
113
+ options.startupPaintOutputChunk(envelope.sessionId);
114
+ if (outputIngest.cursorRegressed) {
115
+ options.recordPerfEvent('mux.output.cursor-regression', {
98
116
  sessionId: envelope.sessionId,
117
+ previousCursor: outputIngest.previousCursor,
99
118
  cursor: envelope.cursor,
100
- chunk,
101
- ensureConversation: this.options.ensureConversation,
102
119
  });
103
- const conversation = outputIngest.conversation;
104
- this.options.noteGitActivity(conversation.directoryId);
105
- this.options.recordOutputChunk({
106
- sessionId: envelope.sessionId,
107
- chunkLength: chunk.length,
108
- active: this.options.activeConversationId() === envelope.sessionId,
109
- });
110
- this.options.startupOutputChunk(envelope.sessionId, chunk.length);
111
- this.options.startupPaintOutputChunk(envelope.sessionId);
112
- if (outputIngest.cursorRegressed) {
113
- this.options.recordPerfEvent('mux.output.cursor-regression', {
114
- sessionId: envelope.sessionId,
115
- previousCursor: outputIngest.previousCursor,
116
- cursor: envelope.cursor,
117
- });
118
- }
120
+ }
119
121
 
120
- const normalized = this.options.mapTerminalOutputToNormalizedEvent(
121
- chunk,
122
- conversation.scope,
123
- this.options.idFactory,
124
- );
125
- this.options.enqueueEvent(normalized);
126
- conversation.lastEventAt = normalized.ts;
127
- if (this.options.activeConversationId() === envelope.sessionId) {
128
- this.options.markDirty();
129
- }
130
- const outputHandledDurationMs =
131
- Number(this.options.perfNowNs() - outputHandledStartedAtNs) / 1e6;
132
- this.options.recordOutputHandled(outputHandledDurationMs);
133
- return;
122
+ const normalized = options.mapTerminalOutputToNormalizedEvent(
123
+ chunk,
124
+ conversation.scope,
125
+ options.idFactory,
126
+ );
127
+ options.enqueueEvent(normalized);
128
+ conversation.lastEventAt = normalized.ts;
129
+ if (options.activeConversationId() === envelope.sessionId) {
130
+ options.markDirty();
134
131
  }
132
+ const outputHandledDurationMs = Number(options.perfNowNs() - outputHandledStartedAtNs) / 1e6;
133
+ options.recordOutputHandled(outputHandledDurationMs);
134
+ return;
135
+ }
135
136
 
136
- if (envelope.kind === 'pty.event') {
137
- if (this.options.isRemoved(envelope.sessionId)) {
138
- return;
139
- }
140
- const conversation = this.options.ensureConversation(envelope.sessionId);
141
- this.options.noteGitActivity(conversation.directoryId);
142
- const observedAt = this.options.observedAtFromSessionEvent(envelope.event);
143
- const updatedAdapterState = this.options.mergeAdapterStateFromSessionEvent(
144
- conversation.agentType,
145
- conversation.adapterState,
146
- envelope.event,
147
- observedAt,
148
- );
149
- if (updatedAdapterState !== null) {
150
- conversation.adapterState = updatedAdapterState;
151
- }
152
- const normalized = this.options.mapSessionEventToNormalizedEvent(
153
- envelope.event,
154
- conversation.scope,
155
- this.options.idFactory,
156
- );
157
- if (normalized !== null) {
158
- this.options.enqueueEvent(normalized);
159
- }
160
- if (envelope.event.type === 'session-exit') {
161
- this.options.setExit(envelope.event.exit);
162
- this.options.markSessionExited({
163
- sessionId: envelope.sessionId,
164
- exit: envelope.event.exit,
165
- exitedAt: this.options.nowIso(),
166
- });
167
- this.options.deletePtySize(envelope.sessionId);
168
- }
169
- this.options.markDirty();
137
+ if (envelope.kind === 'pty.event') {
138
+ if (options.isRemoved(envelope.sessionId)) {
170
139
  return;
171
140
  }
141
+ const conversation = options.ensureConversation(envelope.sessionId);
142
+ options.noteGitActivity(conversation.directoryId);
143
+ const observedAt = options.observedAtFromSessionEvent(envelope.event);
144
+ const updatedAdapterState = options.mergeAdapterStateFromSessionEvent(
145
+ conversation.agentType,
146
+ conversation.adapterState,
147
+ envelope.event,
148
+ observedAt,
149
+ );
150
+ if (updatedAdapterState !== null) {
151
+ conversation.adapterState = updatedAdapterState;
152
+ }
153
+ const normalized = options.mapSessionEventToNormalizedEvent(
154
+ envelope.event,
155
+ conversation.scope,
156
+ options.idFactory,
157
+ );
158
+ if (normalized !== null) {
159
+ options.enqueueEvent(normalized);
160
+ }
161
+ if (envelope.event.type === 'session-exit') {
162
+ options.setExit(envelope.event.exit);
163
+ options.markSessionExited({
164
+ sessionId: envelope.sessionId,
165
+ exit: envelope.event.exit,
166
+ exitedAt: options.nowIso(),
167
+ });
168
+ options.deletePtySize(envelope.sessionId);
169
+ }
170
+ options.markDirty();
171
+ return;
172
+ }
172
173
 
173
- if (envelope.kind === 'pty.exit') {
174
- if (this.options.isRemoved(envelope.sessionId)) {
175
- return;
176
- }
177
- const conversation = this.options.conversationById(envelope.sessionId);
178
- if (conversation !== undefined) {
179
- this.options.noteGitActivity(conversation.directoryId);
180
- this.options.setExit(envelope.exit);
181
- this.options.markSessionExited({
182
- sessionId: envelope.sessionId,
183
- exit: envelope.exit,
184
- exitedAt: this.options.nowIso(),
185
- });
186
- this.options.deletePtySize(envelope.sessionId);
187
- }
188
- this.options.markDirty();
174
+ if (envelope.kind === 'pty.exit') {
175
+ if (options.isRemoved(envelope.sessionId)) {
189
176
  return;
190
177
  }
191
-
192
- if (envelope.kind === 'stream.event') {
193
- this.options.applyObservedWorkspaceEvent(envelope.event);
194
- this.options.applyObservedGitStatusEvent(envelope.event);
195
- this.options.applyObservedTaskPlanningEvent(envelope.event);
178
+ const conversation = options.conversationById(envelope.sessionId);
179
+ if (conversation !== undefined) {
180
+ options.noteGitActivity(conversation.directoryId);
181
+ options.setExit(envelope.exit);
182
+ options.markSessionExited({
183
+ sessionId: envelope.sessionId,
184
+ exit: envelope.exit,
185
+ exitedAt: options.nowIso(),
186
+ });
187
+ options.deletePtySize(envelope.sessionId);
196
188
  }
189
+ options.markDirty();
190
+ return;
191
+ }
192
+
193
+ if (envelope.kind === 'stream.event') {
194
+ options.applyObservedEvent({
195
+ subscriptionId: envelope.subscriptionId,
196
+ cursor: envelope.cursor,
197
+ event: envelope.event,
198
+ });
197
199
  }
198
200
  }
@@ -35,42 +35,48 @@ interface RuntimeGitStateOptions<TRepositoryRecord extends RepositoryRecordShape
35
35
  readonly markDirty: () => void;
36
36
  }
37
37
 
38
- export class RuntimeGitState<TRepositoryRecord extends RepositoryRecordShape> {
39
- constructor(private readonly options: RuntimeGitStateOptions<TRepositoryRecord>) {}
38
+ export interface RuntimeGitState {
39
+ deleteDirectoryGitState(directoryId: string): void;
40
+ syncGitStateWithDirectories(): void;
41
+ noteGitActivity(directoryId: string | null): void;
42
+ applyObservedGitStatusEvent(observed: StreamObservedEvent): void;
43
+ }
40
44
 
41
- deleteDirectoryGitState(directoryId: string): void {
45
+ export function createRuntimeGitState<TRepositoryRecord extends RepositoryRecordShape>(
46
+ options: RuntimeGitStateOptions<TRepositoryRecord>,
47
+ ): RuntimeGitState {
48
+ const deleteDirectoryGitState = (directoryId: string): void => {
42
49
  deleteDirectoryGitStateFn(
43
50
  directoryId,
44
- this.options.directoryManager.mutableGitSummaries(),
45
- this.options.directoryRepositorySnapshotByDirectoryId,
46
- this.options.repositoryAssociationByDirectoryId,
51
+ options.directoryManager.mutableGitSummaries(),
52
+ options.directoryRepositorySnapshotByDirectoryId,
53
+ options.repositoryAssociationByDirectoryId,
47
54
  );
48
- }
55
+ };
49
56
 
50
- syncGitStateWithDirectories(): void {
51
- this.options.directoryManager.syncGitSummariesWithDirectories(this.options.loadingSummary);
52
- this.options.syncRepositoryAssociationsWithDirectorySnapshots();
53
- }
57
+ const syncGitStateWithDirectories = (): void => {
58
+ options.directoryManager.syncGitSummariesWithDirectories(options.loadingSummary);
59
+ options.syncRepositoryAssociationsWithDirectorySnapshots();
60
+ };
54
61
 
55
- noteGitActivity(directoryId: string | null): void {
56
- if (directoryId === null || !this.options.directoryManager.hasDirectory(directoryId)) {
62
+ const noteGitActivity = (directoryId: string | null): void => {
63
+ if (directoryId === null || !options.directoryManager.hasDirectory(directoryId)) {
57
64
  return;
58
65
  }
59
- this.options.directoryManager.ensureGitSummary(directoryId, this.options.loadingSummary);
60
- }
66
+ options.directoryManager.ensureGitSummary(directoryId, options.loadingSummary);
67
+ };
61
68
 
62
- applyObservedGitStatusEvent(observed: StreamObservedEvent): void {
69
+ const applyObservedGitStatusEvent = (observed: StreamObservedEvent): void => {
63
70
  const reduced = applyObservedGitStatusEventFn({
64
- enabled: this.options.enabled,
71
+ enabled: options.enabled,
65
72
  observed,
66
- gitSummaryByDirectoryId: this.options.directoryManager.mutableGitSummaries(),
67
- loadingSummary: this.options.loadingSummary,
68
- directoryRepositorySnapshotByDirectoryId:
69
- this.options.directoryRepositorySnapshotByDirectoryId,
70
- emptyRepositorySnapshot: this.options.emptyRepositorySnapshot,
71
- repositoryAssociationByDirectoryId: this.options.repositoryAssociationByDirectoryId,
72
- repositories: this.options.repositories,
73
- parseRepositoryRecord: this.options.parseRepositoryRecord,
73
+ gitSummaryByDirectoryId: options.directoryManager.mutableGitSummaries(),
74
+ loadingSummary: options.loadingSummary,
75
+ directoryRepositorySnapshotByDirectoryId: options.directoryRepositorySnapshotByDirectoryId,
76
+ emptyRepositorySnapshot: options.emptyRepositorySnapshot,
77
+ repositoryAssociationByDirectoryId: options.repositoryAssociationByDirectoryId,
78
+ repositories: options.repositories,
79
+ parseRepositoryRecord: options.parseRepositoryRecord,
74
80
  repositoryRecordChanged: (previous, repository) =>
75
81
  previous === undefined ||
76
82
  previous.name !== repository.name ||
@@ -82,11 +88,18 @@ export class RuntimeGitState<TRepositoryRecord extends RepositoryRecordShape> {
82
88
  return;
83
89
  }
84
90
  if (reduced.repositoryRecordChanged) {
85
- this.options.syncRepositoryAssociationsWithDirectorySnapshots();
86
- this.options.syncTaskPaneRepositorySelection();
91
+ options.syncRepositoryAssociationsWithDirectorySnapshots();
92
+ options.syncTaskPaneRepositorySelection();
87
93
  }
88
94
  if (reduced.changed) {
89
- this.options.markDirty();
95
+ options.markDirty();
90
96
  }
91
- }
97
+ };
98
+
99
+ return {
100
+ deleteDirectoryGitState,
101
+ syncGitStateWithDirectories,
102
+ noteGitActivity,
103
+ applyObservedGitStatusEvent,
104
+ };
92
105
  }
@@ -44,7 +44,9 @@ interface RuntimeLayoutResizeOptions<TConversation extends RuntimeLayoutResizeCo
44
44
  readonly clearTimeoutFn?: (timer: ReturnType<typeof setTimeout>) => void;
45
45
  }
46
46
 
47
- export class RuntimeLayoutResize<TConversation extends RuntimeLayoutResizeConversationRecord> {
47
+ export class RuntimeLayoutResizeEngine<
48
+ TConversation extends RuntimeLayoutResizeConversationRecord,
49
+ > {
48
50
  private resizeTimer: ReturnType<typeof setTimeout> | null = null;
49
51
  private pendingSize: RuntimeLayoutResizeSize | null = null;
50
52
  private lastResizeApplyAtMs = 0;