@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
@@ -12,24 +12,25 @@ interface StartupBackgroundResumeOptions {
12
12
  readonly clearTimeoutFn?: (handle: TimeoutHandle) => void;
13
13
  }
14
14
 
15
- export class StartupBackgroundResumeService {
16
- private readonly setTimeoutFn: (handler: () => void, ms: number) => TimeoutHandle;
17
- private readonly clearTimeoutFn: (handle: TimeoutHandle) => void;
15
+ export interface StartupBackgroundResumeService {
16
+ run(initialActiveId: string | null): Promise<void>;
17
+ }
18
18
 
19
- constructor(private readonly options: StartupBackgroundResumeOptions) {
20
- this.setTimeoutFn = options.setTimeoutFn ?? setTimeout;
21
- this.clearTimeoutFn = options.clearTimeoutFn ?? clearTimeout;
22
- }
19
+ export function createStartupBackgroundResumeService(
20
+ options: StartupBackgroundResumeOptions,
21
+ ): StartupBackgroundResumeService {
22
+ const setTimeoutFn = options.setTimeoutFn ?? setTimeout;
23
+ const clearTimeoutFn = options.clearTimeoutFn ?? clearTimeout;
23
24
 
24
- async run(initialActiveId: string | null): Promise<void> {
25
+ async function run(initialActiveId: string | null): Promise<void> {
25
26
  const sessionId = initialActiveId ?? 'none';
26
- this.options.recordPerfEvent('mux.startup.background-start.wait', {
27
+ options.recordPerfEvent('mux.startup.background-start.wait', {
27
28
  sessionId,
28
- maxWaitMs: this.options.maxWaitMs,
29
- enabled: this.options.enabled ? 1 : 0,
29
+ maxWaitMs: options.maxWaitMs,
30
+ enabled: options.enabled ? 1 : 0,
30
31
  });
31
- if (!this.options.enabled) {
32
- this.options.recordPerfEvent('mux.startup.background-start.skipped', {
32
+ if (!options.enabled) {
33
+ options.recordPerfEvent('mux.startup.background-start.skipped', {
33
34
  sessionId,
34
35
  reason: 'disabled',
35
36
  });
@@ -39,27 +40,31 @@ export class StartupBackgroundResumeService {
39
40
  let timedOut = false;
40
41
  let timeoutHandle: TimeoutHandle | null = null;
41
42
  await Promise.race([
42
- this.options.waitForSettled(),
43
+ options.waitForSettled(),
43
44
  new Promise<void>((resolve) => {
44
- timeoutHandle = this.setTimeoutFn(() => {
45
+ timeoutHandle = setTimeoutFn(() => {
45
46
  timedOut = true;
46
47
  resolve();
47
- }, this.options.maxWaitMs);
48
+ }, options.maxWaitMs);
48
49
  }),
49
50
  ]);
50
51
  if (timeoutHandle !== null) {
51
- this.clearTimeoutFn(timeoutHandle);
52
+ clearTimeoutFn(timeoutHandle);
52
53
  }
53
54
 
54
- this.options.recordPerfEvent('mux.startup.background-start.begin', {
55
+ options.recordPerfEvent('mux.startup.background-start.begin', {
55
56
  sessionId,
56
57
  timedOut,
57
- settledObserved: this.options.settledObserved(),
58
+ settledObserved: options.settledObserved(),
58
59
  });
59
- const queued = this.options.queuePersistedConversationsInBackground(initialActiveId);
60
- this.options.recordPerfEvent('mux.startup.background-start.queued', {
60
+ const queued = options.queuePersistedConversationsInBackground(initialActiveId);
61
+ options.recordPerfEvent('mux.startup.background-start.queued', {
61
62
  sessionId,
62
63
  queued,
63
64
  });
64
65
  }
66
+
67
+ return {
68
+ run,
69
+ };
65
70
  }
@@ -1,13 +1,16 @@
1
1
  import type { ConversationState } from '../mux/live-mux/conversation-state.ts';
2
2
  import { StartupSequencer } from '../mux/startup-sequencer.ts';
3
3
  import { StartupBackgroundProbeService } from './startup-background-probe.ts';
4
- import { StartupBackgroundResumeService } from './startup-background-resume.ts';
4
+ import {
5
+ createStartupBackgroundResumeService,
6
+ type StartupBackgroundResumeService,
7
+ } from './startup-background-resume.ts';
5
8
  import { StartupOutputTracker } from './startup-output-tracker.ts';
6
- import { StartupPaintTracker } from './startup-paint-tracker.ts';
7
- import { StartupSettledGate } from './startup-settled-gate.ts';
8
- import { StartupShutdownService } from './startup-shutdown.ts';
9
+ import { createStartupPaintTracker, type StartupPaintTracker } from './startup-paint-tracker.ts';
10
+ import { createStartupSettledGate } from './startup-settled-gate.ts';
11
+ import { finalizeStartupShutdown, type StartupShutdownServiceOptions } from './startup-shutdown.ts';
9
12
  import { StartupSpanTracker } from './startup-span-tracker.ts';
10
- import { StartupVisibility } from './startup-visibility.ts';
13
+ import { createStartupVisibility } from './startup-visibility.ts';
11
14
 
12
15
  type PerfAttrs = Record<string, boolean | number | string>;
13
16
 
@@ -47,7 +50,7 @@ export class StartupOrchestrator {
47
50
  private readonly startupPaintTracker: StartupPaintTracker;
48
51
  private readonly startupBackgroundProbeService: StartupBackgroundProbeService;
49
52
  private readonly startupBackgroundResumeService: StartupBackgroundResumeService;
50
- private readonly startupShutdownService: StartupShutdownService;
53
+ private readonly startupShutdownOptions: StartupShutdownServiceOptions;
51
54
 
52
55
  constructor(private readonly options: StartupOrchestratorOptions) {
53
56
  this.startupSequencer = new StartupSequencer({
@@ -58,8 +61,8 @@ export class StartupOrchestrator {
58
61
  options.startPerfSpan,
59
62
  options.startupSettleQuietMs,
60
63
  );
61
- const startupVisibility = new StartupVisibility();
62
- const startupSettledGate = new StartupSettledGate({
64
+ const startupVisibility = createStartupVisibility();
65
+ const startupSettledGate = createStartupSettledGate({
63
66
  startupSequencer: this.startupSequencer,
64
67
  startupSpanTracker: this.startupSpanTracker,
65
68
  getConversation: options.getConversation,
@@ -72,7 +75,7 @@ export class StartupOrchestrator {
72
75
  startupSpanTracker: this.startupSpanTracker,
73
76
  recordPerfEvent: options.recordPerfEvent,
74
77
  });
75
- this.startupPaintTracker = new StartupPaintTracker({
78
+ this.startupPaintTracker = createStartupPaintTracker({
76
79
  startupSequencer: this.startupSequencer,
77
80
  startupSpanTracker: this.startupSpanTracker,
78
81
  startupVisibility,
@@ -88,7 +91,7 @@ export class StartupOrchestrator {
88
91
  refreshProcessUsage: options.refreshProcessUsage,
89
92
  recordPerfEvent: options.recordPerfEvent,
90
93
  });
91
- this.startupBackgroundResumeService = new StartupBackgroundResumeService({
94
+ this.startupBackgroundResumeService = createStartupBackgroundResumeService({
92
95
  enabled: options.backgroundResumeEnabled,
93
96
  maxWaitMs: options.backgroundWaitMaxMs,
94
97
  waitForSettled: () => this.startupSequencer.waitForSettled(),
@@ -96,11 +99,11 @@ export class StartupOrchestrator {
96
99
  queuePersistedConversationsInBackground: options.queuePersistedConversationsInBackground,
97
100
  recordPerfEvent: options.recordPerfEvent,
98
101
  });
99
- this.startupShutdownService = new StartupShutdownService({
102
+ this.startupShutdownOptions = {
100
103
  startupSequencer: this.startupSequencer,
101
104
  startupSpanTracker: this.startupSpanTracker,
102
105
  startupSettledGate,
103
- });
106
+ };
104
107
  }
105
108
 
106
109
  get firstPaintTargetSessionId(): string | null {
@@ -161,6 +164,6 @@ export class StartupOrchestrator {
161
164
  }
162
165
 
163
166
  finalize(): void {
164
- this.startupShutdownService.finalize();
167
+ finalizeStartupShutdown(this.startupShutdownOptions);
165
168
  }
166
169
  }
@@ -26,7 +26,7 @@ interface StartupSettledGateLike {
26
26
  scheduleProbe(sessionId: string): void;
27
27
  }
28
28
 
29
- interface StartupPaintTrackerOptions {
29
+ export interface StartupPaintTrackerOptions {
30
30
  readonly startupSequencer: StartupSequencerLike;
31
31
  readonly startupSpanTracker: StartupSpanTrackerLike;
32
32
  readonly startupVisibility: StartupVisibilityLike;
@@ -41,11 +41,16 @@ interface StartupRenderFlushInput {
41
41
  readonly changedRowCount: number;
42
42
  }
43
43
 
44
- export class StartupPaintTracker {
45
- constructor(private readonly options: StartupPaintTrackerOptions) {}
44
+ export interface StartupPaintTracker {
45
+ onRenderFlush(input: StartupRenderFlushInput): void;
46
+ onOutputChunk(sessionId: string): void;
47
+ }
46
48
 
47
- onRenderFlush(input: StartupRenderFlushInput): void {
48
- const targetSessionId = this.options.startupSpanTracker.firstPaintTargetSessionId;
49
+ export function createStartupPaintTracker(
50
+ options: StartupPaintTrackerOptions,
51
+ ): StartupPaintTracker {
52
+ function onRenderFlush(input: StartupRenderFlushInput): void {
53
+ const targetSessionId = options.startupSpanTracker.firstPaintTargetSessionId;
49
54
  if (targetSessionId === null) {
50
55
  return;
51
56
  }
@@ -56,24 +61,22 @@ export class StartupPaintTracker {
56
61
  ) {
57
62
  return;
58
63
  }
59
- const startupSnapshot = this.options.startupSequencer.snapshot();
64
+ const startupSnapshot = options.startupSequencer.snapshot();
60
65
  if (!startupSnapshot.firstOutputObserved) {
61
66
  return;
62
67
  }
63
68
 
64
- const glyphCells = this.options.startupVisibility.visibleGlyphCellCount(
65
- input.activeConversation,
66
- );
69
+ const glyphCells = options.startupVisibility.visibleGlyphCellCount(input.activeConversation);
67
70
  if (
68
71
  !startupSnapshot.firstPaintObserved &&
69
- this.options.startupSequencer.markFirstPaintVisible(targetSessionId, glyphCells)
72
+ options.startupSequencer.markFirstPaintVisible(targetSessionId, glyphCells)
70
73
  ) {
71
- this.options.recordPerfEvent('mux.startup.active-first-visible-paint', {
74
+ options.recordPerfEvent('mux.startup.active-first-visible-paint', {
72
75
  sessionId: targetSessionId,
73
76
  changedRows: input.changedRowCount,
74
77
  glyphCells,
75
78
  });
76
- this.options.startupSpanTracker.endFirstPaintSpan({
79
+ options.startupSpanTracker.endFirstPaintSpan({
77
80
  observed: true,
78
81
  changedRows: input.changedRowCount,
79
82
  glyphCells,
@@ -81,35 +84,40 @@ export class StartupPaintTracker {
81
84
  }
82
85
 
83
86
  if (
84
- this.options.startupSequencer.markHeaderVisible(
87
+ options.startupSequencer.markHeaderVisible(
85
88
  targetSessionId,
86
- this.options.startupVisibility.codexHeaderVisible(input.activeConversation),
89
+ options.startupVisibility.codexHeaderVisible(input.activeConversation),
87
90
  )
88
91
  ) {
89
- this.options.recordPerfEvent('mux.startup.active-header-visible', {
92
+ options.recordPerfEvent('mux.startup.active-header-visible', {
90
93
  sessionId: targetSessionId,
91
94
  glyphCells,
92
95
  });
93
96
  }
94
- const selectedGate = this.options.startupSequencer.maybeSelectSettleGate(
97
+ const selectedGate = options.startupSequencer.maybeSelectSettleGate(
95
98
  targetSessionId,
96
99
  glyphCells,
97
100
  );
98
101
  if (selectedGate !== null) {
99
- this.options.recordPerfEvent('mux.startup.active-settle-gate', {
102
+ options.recordPerfEvent('mux.startup.active-settle-gate', {
100
103
  sessionId: targetSessionId,
101
104
  gate: selectedGate,
102
105
  glyphCells,
103
106
  });
104
107
  }
105
- this.options.startupSettledGate.scheduleProbe(targetSessionId);
108
+ options.startupSettledGate.scheduleProbe(targetSessionId);
106
109
  }
107
110
 
108
- onOutputChunk(sessionId: string): void {
109
- const targetSessionId = this.options.startupSpanTracker.firstPaintTargetSessionId;
111
+ function onOutputChunk(sessionId: string): void {
112
+ const targetSessionId = options.startupSpanTracker.firstPaintTargetSessionId;
110
113
  if (targetSessionId === null || sessionId !== targetSessionId) {
111
114
  return;
112
115
  }
113
- this.options.startupSettledGate.scheduleProbe(sessionId);
116
+ options.startupSettledGate.scheduleProbe(sessionId);
114
117
  }
118
+
119
+ return {
120
+ onRenderFlush,
121
+ onOutputChunk,
122
+ };
115
123
  }
@@ -12,34 +12,40 @@ export interface StartupPersistedConversationQueueServiceOptions<
12
12
  readonly markDirty: () => void;
13
13
  }
14
14
 
15
- export class StartupPersistedConversationQueueService<
16
- TConversation extends StartupQueueConversationRecord,
17
- > {
18
- constructor(
19
- private readonly options: StartupPersistedConversationQueueServiceOptions<TConversation>,
20
- ) {}
15
+ export interface StartupPersistedConversationQueueService {
16
+ queuePersistedConversationsInBackground(activeSessionId: string | null): number;
17
+ }
21
18
 
22
- queuePersistedConversationsInBackground(activeSessionId: string | null): number {
23
- const ordered = this.options.orderedConversationIds();
19
+ export function createStartupPersistedConversationQueueService<
20
+ TConversation extends StartupQueueConversationRecord,
21
+ >(
22
+ options: StartupPersistedConversationQueueServiceOptions<TConversation>,
23
+ ): StartupPersistedConversationQueueService {
24
+ function queuePersistedConversationsInBackground(activeSessionId: string | null): number {
25
+ const ordered = options.orderedConversationIds();
24
26
  let queued = 0;
25
27
  for (const sessionId of ordered) {
26
28
  if (activeSessionId !== null && sessionId === activeSessionId) {
27
29
  continue;
28
30
  }
29
- const conversation = this.options.conversationById(sessionId);
31
+ const conversation = options.conversationById(sessionId);
30
32
  if (conversation === undefined || conversation.live) {
31
33
  continue;
32
34
  }
33
- this.options.queueBackgroundOp(async () => {
34
- const latest = this.options.conversationById(sessionId);
35
+ options.queueBackgroundOp(async () => {
36
+ const latest = options.conversationById(sessionId);
35
37
  if (latest === undefined || latest.live) {
36
38
  return;
37
39
  }
38
- await this.options.startConversation(sessionId);
39
- this.options.markDirty();
40
+ await options.startConversation(sessionId);
41
+ options.markDirty();
40
42
  }, `background-start:${sessionId}`);
41
43
  queued += 1;
42
44
  }
43
45
  return queued;
44
46
  }
47
+
48
+ return {
49
+ queuePersistedConversationsInBackground,
50
+ };
45
51
  }
@@ -22,7 +22,7 @@ interface StartupSpanTrackerLike {
22
22
  endSettledSpan(attrs: PerfAttrs): void;
23
23
  }
24
24
 
25
- interface StartupSettledGateOptions {
25
+ export interface StartupSettledGateOptions {
26
26
  readonly startupSequencer: StartupSequencerLike;
27
27
  readonly startupSpanTracker: StartupSpanTrackerLike;
28
28
  readonly getConversation: (sessionId: string) => ConversationState | undefined;
@@ -30,38 +30,48 @@ interface StartupSettledGateOptions {
30
30
  readonly recordPerfEvent: (name: string, attrs: PerfAttrs) => void;
31
31
  }
32
32
 
33
- export class StartupSettledGate {
34
- constructor(private readonly options: StartupSettledGateOptions) {}
33
+ export interface StartupSettledGate {
34
+ clearTimer(): void;
35
+ signalSettled(): void;
36
+ scheduleProbe(sessionId: string): void;
37
+ }
35
38
 
36
- clearTimer(): void {
37
- this.options.startupSequencer.clearSettledTimer();
39
+ export function createStartupSettledGate(options: StartupSettledGateOptions): StartupSettledGate {
40
+ function clearTimer(): void {
41
+ options.startupSequencer.clearSettledTimer();
38
42
  }
39
43
 
40
- signalSettled(): void {
41
- this.options.startupSequencer.signalSettled();
44
+ function signalSettled(): void {
45
+ options.startupSequencer.signalSettled();
42
46
  }
43
47
 
44
- scheduleProbe(sessionId: string): void {
45
- this.options.startupSequencer.scheduleSettledProbe(sessionId, (event) => {
46
- if (this.options.startupSpanTracker.firstPaintTargetSessionId !== event.sessionId) {
48
+ function scheduleProbe(sessionId: string): void {
49
+ options.startupSequencer.scheduleSettledProbe(sessionId, (event) => {
50
+ if (options.startupSpanTracker.firstPaintTargetSessionId !== event.sessionId) {
47
51
  return;
48
52
  }
49
- const conversation = this.options.getConversation(event.sessionId);
53
+ const conversation = options.getConversation(event.sessionId);
50
54
  const glyphCells =
51
- conversation === undefined ? 0 : this.options.visibleGlyphCellCount(conversation);
52
- this.options.recordPerfEvent('mux.startup.active-settled', {
55
+ conversation === undefined ? 0 : options.visibleGlyphCellCount(conversation);
56
+ options.recordPerfEvent('mux.startup.active-settled', {
53
57
  sessionId: event.sessionId,
54
58
  gate: event.gate,
55
59
  quietMs: event.quietMs,
56
60
  glyphCells,
57
61
  });
58
- this.options.startupSpanTracker.endSettledSpan({
62
+ options.startupSpanTracker.endSettledSpan({
59
63
  observed: true,
60
64
  gate: event.gate,
61
65
  quietMs: event.quietMs,
62
66
  glyphCells,
63
67
  });
64
- this.options.startupSequencer.signalSettled();
68
+ options.startupSequencer.signalSettled();
65
69
  });
66
70
  }
71
+
72
+ return {
73
+ clearTimer,
74
+ signalSettled,
75
+ scheduleProbe,
76
+ };
67
77
  }
@@ -23,31 +23,27 @@ interface StartupSettledGateLike {
23
23
  signalSettled(): void;
24
24
  }
25
25
 
26
- interface StartupShutdownServiceOptions {
26
+ export interface StartupShutdownServiceOptions {
27
27
  readonly startupSequencer: StartupSequencerLike;
28
28
  readonly startupSpanTracker: StartupSpanTrackerLike;
29
29
  readonly startupSettledGate: StartupSettledGateLike;
30
30
  }
31
31
 
32
- export class StartupShutdownService {
33
- constructor(private readonly options: StartupShutdownServiceOptions) {}
34
-
35
- finalize(): void {
36
- this.options.startupSpanTracker.endStartCommandSpan({
37
- observed: false,
38
- });
39
- const startupSnapshot = this.options.startupSequencer.snapshot();
40
- this.options.startupSpanTracker.endFirstOutputSpan({
41
- observed: startupSnapshot.firstOutputObserved,
42
- });
43
- this.options.startupSpanTracker.endFirstPaintSpan({
44
- observed: startupSnapshot.firstPaintObserved,
45
- });
46
- this.options.startupSettledGate.clearTimer();
47
- this.options.startupSpanTracker.endSettledSpan({
48
- observed: startupSnapshot.settledObserved,
49
- gate: startupSnapshot.settleGate ?? 'none',
50
- });
51
- this.options.startupSettledGate.signalSettled();
52
- }
32
+ export function finalizeStartupShutdown(options: StartupShutdownServiceOptions): void {
33
+ options.startupSpanTracker.endStartCommandSpan({
34
+ observed: false,
35
+ });
36
+ const startupSnapshot = options.startupSequencer.snapshot();
37
+ options.startupSpanTracker.endFirstOutputSpan({
38
+ observed: startupSnapshot.firstOutputObserved,
39
+ });
40
+ options.startupSpanTracker.endFirstPaintSpan({
41
+ observed: startupSnapshot.firstPaintObserved,
42
+ });
43
+ options.startupSettledGate.clearTimer();
44
+ options.startupSpanTracker.endSettledSpan({
45
+ observed: startupSnapshot.settledObserved,
46
+ gate: startupSnapshot.settleGate ?? 'none',
47
+ });
48
+ options.startupSettledGate.signalSettled();
53
49
  }
@@ -10,7 +10,7 @@ interface DirectoryGitStatusLike<TRepository, TSummary, TSnapshot> {
10
10
  readonly repository: TRepository | null;
11
11
  }
12
12
 
13
- interface StartupStateHydrationServiceOptions<
13
+ export interface StartupStateHydrationServiceOptions<
14
14
  TRepository extends RepositoryRecordLike,
15
15
  TSummary,
16
16
  TSnapshot,
@@ -34,56 +34,66 @@ interface StartupStateHydrationServiceOptions<
34
34
  readonly ensureActiveConversationId: () => void;
35
35
  readonly activeConversationId: () => string | null;
36
36
  readonly selectLeftNavConversation: (sessionId: string) => void;
37
- readonly enterHomePane: () => void;
37
+ readonly enterStartupPane: () => void;
38
38
  }
39
39
 
40
- export class StartupStateHydrationService<
40
+ export interface StartupStateHydrationService {
41
+ hydrateRepositoryList(): Promise<void>;
42
+ hydrateDirectoryGitStatus(): Promise<void>;
43
+ hydrateStartupState(afterCursor: number | null): Promise<void>;
44
+ }
45
+
46
+ export function createStartupStateHydrationService<
41
47
  TRepository extends RepositoryRecordLike,
42
48
  TSummary,
43
49
  TSnapshot,
44
50
  TDirectoryGitStatus extends DirectoryGitStatusLike<TRepository, TSummary, TSnapshot>,
45
- > {
46
- constructor(
47
- private readonly options: StartupStateHydrationServiceOptions<
48
- TRepository,
49
- TSummary,
50
- TSnapshot,
51
- TDirectoryGitStatus
52
- >,
53
- ) {}
54
-
55
- async hydrateRepositoryList(): Promise<void> {
56
- const rows = await this.options.listRepositories();
57
- this.options.clearRepositories();
51
+ >(
52
+ options: StartupStateHydrationServiceOptions<
53
+ TRepository,
54
+ TSummary,
55
+ TSnapshot,
56
+ TDirectoryGitStatus
57
+ >,
58
+ ): StartupStateHydrationService {
59
+ async function hydrateRepositoryList(): Promise<void> {
60
+ const rows = await options.listRepositories();
61
+ options.clearRepositories();
58
62
  for (const record of rows) {
59
- this.options.setRepository(record.repositoryId, record);
63
+ options.setRepository(record.repositoryId, record);
60
64
  }
61
- this.options.syncRepositoryAssociationsWithDirectorySnapshots();
65
+ options.syncRepositoryAssociationsWithDirectorySnapshots();
62
66
  }
63
67
 
64
- async hydrateDirectoryGitStatus(): Promise<void> {
65
- if (!this.options.gitHydrationEnabled) {
68
+ async function hydrateDirectoryGitStatus(): Promise<void> {
69
+ if (!options.gitHydrationEnabled) {
66
70
  return;
67
71
  }
68
- const rows = await this.options.listDirectoryGitStatuses();
72
+ const rows = await options.listDirectoryGitStatuses();
69
73
  for (const record of rows) {
70
- this.options.setDirectoryGitSummary(record.directoryId, record.summary);
71
- this.options.setDirectoryRepositorySnapshot(record.directoryId, record.repositorySnapshot);
72
- this.options.setDirectoryRepositoryAssociation(record.directoryId, record.repositoryId);
74
+ options.setDirectoryGitSummary(record.directoryId, record.summary);
75
+ options.setDirectoryRepositorySnapshot(record.directoryId, record.repositorySnapshot);
76
+ options.setDirectoryRepositoryAssociation(record.directoryId, record.repositoryId);
73
77
  if (record.repository !== null) {
74
- this.options.setRepository(record.repository.repositoryId, record.repository);
78
+ options.setRepository(record.repository.repositoryId, record.repository);
75
79
  }
76
80
  }
77
- this.options.syncRepositoryAssociationsWithDirectorySnapshots();
81
+ options.syncRepositoryAssociationsWithDirectorySnapshots();
78
82
  }
79
83
 
80
- async hydrateStartupState(afterCursor: number | null): Promise<void> {
81
- await this.options.hydrateConversationList();
82
- await this.hydrateRepositoryList();
83
- await this.options.hydrateTaskPlanningState();
84
- await this.hydrateDirectoryGitStatus();
85
- await this.options.subscribeTaskPlanningEvents(afterCursor);
86
- this.options.ensureActiveConversationId();
87
- this.options.enterHomePane();
84
+ async function hydrateStartupState(afterCursor: number | null): Promise<void> {
85
+ await options.hydrateConversationList();
86
+ await hydrateRepositoryList();
87
+ await options.hydrateTaskPlanningState();
88
+ await hydrateDirectoryGitStatus();
89
+ await options.subscribeTaskPlanningEvents(afterCursor);
90
+ options.ensureActiveConversationId();
91
+ options.enterStartupPane();
88
92
  }
93
+
94
+ return {
95
+ hydrateRepositoryList,
96
+ hydrateDirectoryGitStatus,
97
+ hydrateStartupState,
98
+ };
89
99
  }
@@ -1,9 +1,12 @@
1
1
  import type { ConversationState } from '../mux/live-mux/conversation-state.ts';
2
2
 
3
- export class StartupVisibility {
4
- constructor() {}
3
+ export interface StartupVisibility {
4
+ visibleGlyphCellCount(conversation: ConversationState): number;
5
+ codexHeaderVisible(conversation: ConversationState): boolean;
6
+ }
5
7
 
6
- visibleGlyphCellCount(conversation: ConversationState): number {
8
+ export function createStartupVisibility(): StartupVisibility {
9
+ function visibleGlyphCellCount(conversation: ConversationState): number {
7
10
  const frame = conversation.oracle.snapshotWithoutHash();
8
11
  let count = 0;
9
12
  for (const line of frame.richLines) {
@@ -16,7 +19,7 @@ export class StartupVisibility {
16
19
  return count;
17
20
  }
18
21
 
19
- codexHeaderVisible(conversation: ConversationState): boolean {
22
+ function codexHeaderVisible(conversation: ConversationState): boolean {
20
23
  const frame = conversation.oracle.snapshotWithoutHash();
21
24
  const rows: string[] = [];
22
25
  for (const line of frame.richLines) {
@@ -32,4 +35,9 @@ export class StartupVisibility {
32
35
  const text = rows.join('\n');
33
36
  return text.includes('OpenAI Codex') && text.includes('model:') && text.includes('directory:');
34
37
  }
38
+
39
+ return {
40
+ visibleGlyphCellCount,
41
+ codexHeaderVisible,
42
+ };
35
43
  }