@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
@@ -1,3873 +1,3 @@
1
- import { existsSync, mkdirSync } from 'node:fs';
2
- import { dirname } from 'node:path';
3
- import { randomUUID } from 'node:crypto';
4
- import { execFileSync, spawn } from 'node:child_process';
5
- import { startCodexLiveSession } from '../src/codex/live-session.ts';
6
- import {
7
- openCodexControlPlaneClient,
8
- subscribeControlPlaneKeyEvents,
9
- type ControlPlaneKeyEvent,
10
- } from '../src/control-plane/codex-session-stream.ts';
11
- import { startControlPlaneStreamServer } from '../src/control-plane/stream-server.ts';
12
- import type {
13
- StreamObservedEvent,
14
- StreamServerEnvelope,
15
- } from '../src/control-plane/stream-protocol.ts';
16
- import { SqliteEventStore } from '../src/store/event-store.ts';
17
- import { TerminalSnapshotOracle } from '../src/terminal/snapshot-oracle.ts';
18
- import type { PtyExit } from '../src/pty/pty_host.ts';
19
- import { computeDualPaneLayout } from '../src/mux/dual-pane-core.ts';
20
- import {
21
- loadHarnessConfig,
22
- updateHarnessConfig,
23
- updateHarnessMuxUiConfig,
24
- type HarnessMuxThemeConfig,
25
- } from '../src/config/config-core.ts';
26
- import { resolveHarnessRuntimePath } from '../src/config/harness-paths.ts';
27
- import { migrateLegacyHarnessLayout } from '../src/config/harness-runtime-migration.ts';
28
- import { loadHarnessSecrets, upsertHarnessSecret } from '../src/config/secrets-core.ts';
29
- import { detectMuxGlobalShortcut, resolveMuxShortcutBindings } from '../src/mux/input-shortcuts.ts';
30
- import { createMuxInputModeManager } from '../src/mux/terminal-input-modes.ts';
31
- import type { buildWorkspaceRailViewRows } from '../src/mux/workspace-rail-model.ts';
32
- import {
33
- normalizeThreadAgentType,
34
- resolveNewThreadPromptAgentByRow,
35
- } from '../src/mux/new-thread-prompt.ts';
36
- import {
37
- CommandMenuRegistry,
38
- createCommandMenuState,
39
- filterThemePresetActionsForScope,
40
- resolveSelectedCommandMenuActionId,
41
- type CommandMenuActionDescriptor,
42
- type RegisteredCommandMenuAction,
43
- } from '../src/mux/live-mux/command-menu.ts';
44
- import {
45
- buildProjectPaneSnapshot,
46
- projectPaneActionAtRow,
47
- sortedRepositoryList,
48
- sortTasksByOrder,
49
- } from '../src/mux/harness-core-ui.ts';
50
- import {
51
- taskFocusedPaneActionAtCell,
52
- taskFocusedPaneActionAtRow,
53
- taskFocusedPaneRepositoryIdAtRow,
54
- taskFocusedPaneTaskIdAtRow,
55
- } from '../src/mux/task-focused-pane.ts';
56
- import {
57
- createTaskComposerBuffer,
58
- normalizeTaskComposerBuffer,
59
- taskFieldsFromComposerText,
60
- type TaskComposerBuffer,
61
- } from '../src/mux/task-composer.ts';
62
- import { resolveTaskScreenKeybindings } from '../src/mux/task-screen-keybindings.ts';
63
- import { applyMuxControlPlaneKeyEvent } from '../src/mux/runtime-wiring.ts';
64
- import {
65
- applyModalOverlay,
66
- buildRenderRows,
67
- renderCanonicalFrameAnsi,
68
- } from '../src/mux/render-frame.ts';
69
- import { createTerminalRecordingWriter } from '../src/recording/terminal-recording.ts';
70
- import { renderTerminalRecordingToGif } from './terminal-recording-gif-lib.ts';
71
- import {
72
- buildAgentSessionStartArgs,
73
- mergeAdapterStateFromSessionEvent,
74
- normalizeAdapterState,
75
- } from '../src/adapters/agent-session-state.ts';
76
- import {
77
- configurePerfCore,
78
- perfNowNs,
79
- recordPerfEvent,
80
- shutdownPerfCore,
81
- startPerfSpan,
82
- } from '../src/perf/perf-core.ts';
83
- import {
84
- parseConversationRecord,
85
- parseDirectoryRecord,
86
- parseRepositoryRecord,
87
- parseTaskRecord,
88
- } from '../src/mux/live-mux/control-plane-records.ts';
89
- import {
90
- leftColsFromPaneWidthPercent,
91
- paneWidthPercentFromLayout,
92
- } from '../src/mux/live-mux/layout.ts';
93
- import {
94
- normalizeGitHubRemoteUrl,
95
- resolveGitHubDefaultBranchForActions,
96
- repositoryNameFromGitHubRemoteUrl,
97
- resolveGitHubTrackedBranchForActions,
98
- shouldShowGitHubPrActions,
99
- } from '../src/mux/live-mux/git-parsing.ts';
100
- import { readProcessUsageSample, runGitCommand } from '../src/mux/live-mux/git-snapshot.ts';
101
- import { probeTerminalPalette } from '../src/mux/live-mux/terminal-palette.ts';
102
- import { firstDirectoryForRepositoryGroup as firstDirectoryForRepositoryGroupFn } from '../src/mux/live-mux/repository-folding.ts';
103
- import {
104
- readObservedStreamCursorBaseline,
105
- subscribeObservedStream,
106
- unsubscribeObservedStream,
107
- } from '../src/mux/live-mux/observed-stream.ts';
108
- import {
109
- createConversationState,
110
- debugFooterForConversation,
111
- formatCommandForDebugBar,
112
- launchCommandForAgent,
113
- type ConversationState,
114
- } from '../src/mux/live-mux/conversation-state.ts';
115
- import {
116
- formatErrorMessage,
117
- parseBooleanEnv,
118
- parsePositiveInt,
119
- prepareArtifactPath,
120
- readStartupTerminalSize,
121
- resolveWorkspacePathForMux,
122
- restoreTerminalState,
123
- sanitizeProcessEnv,
124
- terminalSize,
125
- } from '../src/mux/live-mux/startup-utils.ts';
126
- import {
127
- normalizeExitCode,
128
- isSessionNotFoundError,
129
- isSessionNotLiveError,
130
- isConversationNotFoundError,
131
- mapTerminalOutputToNormalizedEvent,
132
- mapSessionEventToNormalizedEvent,
133
- observedAtFromSessionEvent,
134
- } from '../src/mux/live-mux/event-mapping.ts';
135
- import { parseMuxArgs } from '../src/mux/live-mux/args.ts';
136
- import {
137
- isCopyShortcutInput,
138
- renderSelectionOverlay,
139
- selectionText,
140
- selectionVisibleRows,
141
- writeTextToClipboard,
142
- } from '../src/mux/live-mux/selection.ts';
143
- import { type GitRepositorySnapshot, type GitSummary } from '../src/mux/live-mux/git-state.ts';
144
- import { resolveDirectoryForAction as resolveDirectoryForActionFn } from '../src/mux/live-mux/directory-resolution.ts';
145
- import { requestStop as requestStopFn } from '../src/mux/live-mux/runtime-shutdown.ts';
146
- import {
147
- hasActiveProfileState,
148
- resolveProfileStatePath,
149
- toggleGatewayProfiler as toggleGatewayProfilerFn,
150
- } from '../src/mux/live-mux/gateway-profiler.ts';
151
- import { toggleGatewayStatusTimeline as toggleGatewayStatusTimelineFn } from '../src/mux/live-mux/gateway-status-timeline.ts';
152
- import { toggleGatewayRenderTrace as toggleGatewayRenderTraceFn } from '../src/mux/live-mux/gateway-render-trace.ts';
153
- import { resolveStatusTimelineStatePath } from '../src/mux/live-mux/status-timeline-state.ts';
154
- import { resolveRenderTraceStatePath } from '../src/mux/live-mux/render-trace-state.ts';
155
- import {
156
- findRenderTraceControlIssues,
157
- renderTraceChunkPreview,
158
- } from '../src/mux/live-mux/render-trace-analysis.ts';
159
- import {
160
- buildCritiqueReviewCommand,
161
- resolveCritiqueReviewAgent,
162
- resolveCritiqueReviewBaseBranch,
163
- } from '../src/mux/live-mux/critique-review.ts';
164
- import { WorkspaceModel } from '../src/domain/workspace.ts';
165
- import { ConversationManager, type ConversationSeed } from '../src/domain/conversations.ts';
166
- import { RepositoryManager } from '../src/domain/repositories.ts';
167
- import { DirectoryManager } from '../src/domain/directories.ts';
168
- import { TaskManager } from '../src/domain/tasks.ts';
169
- import { ControlPlaneService } from '../src/services/control-plane.ts';
170
- import { ConversationLifecycle } from '../src/services/conversation-lifecycle.ts';
171
- import { DirectoryHydrationService } from '../src/services/directory-hydration.ts';
172
- import { EventPersistence } from '../src/services/event-persistence.ts';
173
- import { MuxUiStatePersistence } from '../src/services/mux-ui-state-persistence.ts';
174
- import { OutputLoadSampler } from '../src/services/output-load-sampler.ts';
175
- import { ProcessUsageRefreshService } from '../src/services/process-usage-refresh.ts';
176
- import { RecordingService } from '../src/services/recording.ts';
177
- import { SessionProjectionInstrumentation } from '../src/services/session-projection-instrumentation.ts';
178
- import { StartupOrchestrator } from '../src/services/startup-orchestrator.ts';
179
- import { RuntimeProcessWiring } from '../src/services/runtime-process-wiring.ts';
180
- import { RuntimeControlPlaneOps } from '../src/services/runtime-control-plane-ops.ts';
181
- import { RuntimeControlActions } from '../src/services/runtime-control-actions.ts';
182
- import { RuntimeDirectoryActions } from '../src/services/runtime-directory-actions.ts';
183
- import { RuntimeEnvelopeHandler } from '../src/services/runtime-envelope-handler.ts';
184
- import { RuntimeRenderPipeline } from '../src/services/runtime-render-pipeline.ts';
185
- import { RuntimeRepositoryActions } from '../src/services/runtime-repository-actions.ts';
186
- import { RuntimeGitState } from '../src/services/runtime-git-state.ts';
187
- import { RuntimeLayoutResize } from '../src/services/runtime-layout-resize.ts';
188
- import { RuntimeRenderLifecycle } from '../src/services/runtime-render-lifecycle.ts';
189
- import { RuntimeShutdownService } from '../src/services/runtime-shutdown.ts';
190
- import { RuntimeTaskEditorActions } from '../src/services/runtime-task-editor-actions.ts';
191
- import { RuntimeInputPipeline } from '../src/services/runtime-input-pipeline.ts';
192
- import { RuntimeInputRouter } from '../src/services/runtime-input-router.ts';
193
- import { RuntimeTaskComposerPersistenceService } from '../src/services/runtime-task-composer-persistence.ts';
194
- import { RuntimeTaskPane } from '../src/services/runtime-task-pane.ts';
195
- import { TaskPaneSelectionActions } from '../src/services/task-pane-selection-actions.ts';
196
- import { TaskPlanningHydrationService } from '../src/services/task-planning-hydration.ts';
197
- import { TaskPlanningObservedEvents } from '../src/services/task-planning-observed-events.ts';
198
- import {
199
- RuntimeCommandMenuAgentTools,
200
- type InstallableAgentType,
201
- } from '../src/services/runtime-command-menu-agent-tools.ts';
202
- import { RuntimeWorkspaceActions } from '../src/services/runtime-workspace-actions.ts';
203
- import { WorkspaceObservedEvents } from '../src/services/workspace-observed-events.ts';
204
- import { RuntimeWorkspaceObservedEvents } from '../src/services/runtime-workspace-observed-events.ts';
205
- import { StartupStateHydrationService } from '../src/services/startup-state-hydration.ts';
206
- import {
207
- StatusTimelineRecorder,
208
- type StatusTimelineLabels,
209
- } from '../src/services/status-timeline-recorder.ts';
210
- import {
211
- RenderTraceRecorder,
212
- type RenderTraceLabels,
213
- } from '../src/services/render-trace-recorder.ts';
214
- import { Screen, type ScreenCursorStyle } from '../src/ui/screen.ts';
215
- import { ConversationPane } from '../src/ui/panes/conversation.ts';
216
- import { DebugFooterNotice } from '../src/ui/debug-footer-notice.ts';
217
- import { HomePane } from '../src/ui/panes/home.ts';
218
- import { ProjectPane } from '../src/ui/panes/project.ts';
219
- import { LeftRailPane } from '../src/ui/panes/left-rail.ts';
220
- import { ModalManager } from '../src/ui/modals/manager.ts';
221
- import {
222
- getActiveMuxTheme,
223
- muxThemePresetNames,
224
- resolveConfiguredMuxTheme,
225
- setActiveMuxTheme,
226
- } from '../src/ui/mux-theme.ts';
1
+ import { runCodexLiveMuxRuntimeProcess } from '../src/mux/runtime-app/codex-live-mux-runtime.ts';
227
2
 
228
- type ControlPlaneDirectoryRecord = Awaited<ReturnType<ControlPlaneService['upsertDirectory']>>;
229
- type ControlPlaneConversationRecord = NonNullable<ReturnType<typeof parseConversationRecord>>;
230
- type ControlPlaneRepositoryRecord = NonNullable<ReturnType<typeof parseRepositoryRecord>>;
231
- type ControlPlaneTaskRecord = NonNullable<ReturnType<typeof parseTaskRecord>>;
232
- type ControlPlaneSessionSummary = NonNullable<
233
- Awaited<ReturnType<ControlPlaneService['getSessionStatus']>>
234
- >;
235
- type ControlPlaneDirectoryGitStatusRecord = Awaited<
236
- ReturnType<ControlPlaneService['listDirectoryGitStatuses']>
237
- >[number];
238
-
239
- type ProcessUsageSample = Awaited<ReturnType<typeof readProcessUsageSample>>;
240
-
241
- interface RuntimeCommandMenuContext {
242
- readonly activeDirectoryId: string | null;
243
- readonly activeConversationId: string | null;
244
- readonly selectedText: string;
245
- readonly leftNavSelectionKind: WorkspaceModel['leftNavSelection']['kind'];
246
- readonly profileRunning: boolean;
247
- readonly statusTimelineRunning: boolean;
248
- readonly githubRepositoryId: string | null;
249
- readonly githubRepositoryUrl: string | null;
250
- readonly githubDefaultBranch: string | null;
251
- readonly githubTrackedBranch: string | null;
252
- readonly githubOpenPrUrl: string | null;
253
- readonly githubProjectPrLoading: boolean;
254
- }
255
-
256
- interface CommandMenuGitHubProjectPrState {
257
- readonly directoryId: string;
258
- readonly branchName: string | null;
259
- readonly openPrUrl: string | null;
260
- readonly loading: boolean;
261
- }
262
-
263
- interface GitHubDebugAuthState {
264
- enabled: boolean;
265
- token: 'env' | 'gh' | 'none';
266
- auth: 'ok' | 'no' | 'er' | 'na' | 'uk';
267
- projectPr: 'ok' | 'er' | 'na';
268
- }
269
-
270
- interface ThemePickerSessionState {
271
- readonly initialThemeConfig: HarnessMuxThemeConfig | null;
272
- committed: boolean;
273
- previewActionId: string | null;
274
- }
275
-
276
- interface AllowedCommandMenuApiKey {
277
- readonly actionIdSuffix: string;
278
- readonly envVar: 'ANTHROPIC_API_KEY' | 'OPENAI_API_KEY';
279
- readonly displayName: string;
280
- readonly aliases: readonly string[];
281
- }
282
-
283
- const DEFAULT_RESIZE_MIN_INTERVAL_MS = 33;
284
- const DEFAULT_PTY_RESIZE_SETTLE_MS = 75;
285
- const DEFAULT_STARTUP_SETTLE_QUIET_MS = 300;
286
- const DEFAULT_STARTUP_SETTLE_NONEMPTY_FALLBACK_MS = 1500;
287
- const DEFAULT_BACKGROUND_START_MAX_WAIT_MS = 5000;
288
- const DEFAULT_BACKGROUND_RESUME_PERSISTED = false;
289
- const DEFAULT_BACKGROUND_PROBES_ENABLED = false;
290
- const DEBUG_FOOTER_NOTICE_TTL_MS = 8000;
291
- const DEFAULT_CONVERSATION_TITLE_EDIT_DEBOUNCE_MS = 250;
292
- const DEFAULT_TASK_EDITOR_AUTOSAVE_DEBOUNCE_MS = 250;
293
- const CONVERSATION_TITLE_EDIT_DOUBLE_CLICK_WINDOW_MS = 350;
294
- const HOME_PANE_EDIT_DOUBLE_CLICK_WINDOW_MS = 350;
295
- const HOME_PANE_BACKGROUND_INTERVAL_MS = 80;
296
- const UI_STATE_PERSIST_DEBOUNCE_MS = 200;
297
- const REPOSITORY_TOGGLE_CHORD_TIMEOUT_MS = 1250;
298
- const REPOSITORY_COLLAPSE_ALL_CHORD_PREFIX = Buffer.from([0x0b]);
299
- const UNTRACKED_REPOSITORY_GROUP_ID = 'untracked';
300
- const THEME_PICKER_SCOPE = 'theme-select';
301
- const THEME_ACTION_ID_PREFIX = 'theme.set.';
302
- const API_KEY_ACTION_ID_PREFIX = 'api-key.set.';
303
- const COMMAND_MENU_ALLOWED_API_KEYS: readonly AllowedCommandMenuApiKey[] = [
304
- {
305
- actionIdSuffix: 'anthropic',
306
- envVar: 'ANTHROPIC_API_KEY',
307
- displayName: 'Anthropic API Key',
308
- aliases: ['anthropic api key', 'claude api key'],
309
- },
310
- {
311
- actionIdSuffix: 'openai',
312
- envVar: 'OPENAI_API_KEY',
313
- displayName: 'OpenAI API Key',
314
- aliases: ['openai api key', 'codex api key'],
315
- },
316
- ];
317
- const GIT_SUMMARY_LOADING: GitSummary = {
318
- branch: '(loading)',
319
- changedFiles: 0,
320
- additions: 0,
321
- deletions: 0,
322
- };
323
-
324
- const GIT_REPOSITORY_NONE: GitRepositorySnapshot = {
325
- normalizedRemoteUrl: null,
326
- commitCount: null,
327
- lastCommitAt: null,
328
- shortCommitHash: null,
329
- inferredName: null,
330
- defaultBranch: null,
331
- };
332
-
333
- function asRecord(value: unknown): Record<string, unknown> | null {
334
- if (typeof value !== 'object' || value === null) {
335
- return null;
336
- }
337
- return value as Record<string, unknown>;
338
- }
339
-
340
- function parseGitHubProjectPrState(
341
- directoryId: string,
342
- result: Record<string, unknown>,
343
- ): CommandMenuGitHubProjectPrState {
344
- const branchNameRaw = result['branchName'];
345
- const branchName = typeof branchNameRaw === 'string' ? branchNameRaw : null;
346
- const pr = asRecord(result['pr']);
347
- const prUrlRaw = pr?.['url'];
348
- const openPrUrl = typeof prUrlRaw === 'string' ? prUrlRaw : null;
349
- return {
350
- directoryId,
351
- branchName,
352
- openPrUrl,
353
- loading: false,
354
- };
355
- }
356
-
357
- function parseGitHubPrUrl(result: Record<string, unknown>): string | null {
358
- const pr = asRecord(result['pr']);
359
- if (pr === null) {
360
- return null;
361
- }
362
- const url = pr['url'];
363
- return typeof url === 'string' ? url : null;
364
- }
365
-
366
- function parseGitHubUrl(result: Record<string, unknown>): string | null {
367
- const url = result['url'];
368
- if (typeof url !== 'string') {
369
- return null;
370
- }
371
- const trimmed = url.trim();
372
- return trimmed.length > 0 ? trimmed : null;
373
- }
374
-
375
- function commandMenuProjectPathTail(path: string): string {
376
- const normalized = path.trim().replaceAll('\\', '/').replace(/\/+$/u, '');
377
- if (normalized.length === 0) {
378
- return '(project)';
379
- }
380
- if (normalized === '/') {
381
- return '/';
382
- }
383
- const segments = normalized.split('/').filter((segment) => segment.length > 0);
384
- if (segments.length === 0) {
385
- return normalized;
386
- }
387
- if (segments.length <= 2) {
388
- return segments.join('/');
389
- }
390
- return `…/${segments.slice(-2).join('/')}`;
391
- }
392
-
393
- function openUrlInBrowser(url: string): boolean {
394
- const target = url.trim();
395
- if (target.length === 0) {
396
- return false;
397
- }
398
- try {
399
- if (process.platform === 'darwin') {
400
- const child = spawn('open', [target], {
401
- detached: true,
402
- stdio: 'ignore',
403
- });
404
- child.unref();
405
- return true;
406
- }
407
- if (process.platform === 'win32') {
408
- const child = spawn('cmd', ['/c', 'start', '', target], {
409
- detached: true,
410
- stdio: 'ignore',
411
- windowsHide: true,
412
- });
413
- child.unref();
414
- return true;
415
- }
416
- const child = spawn('xdg-open', [target], {
417
- detached: true,
418
- stdio: 'ignore',
419
- });
420
- child.unref();
421
- return true;
422
- } catch {
423
- return false;
424
- }
425
- }
426
-
427
- function commandExistsOnPath(command: string): boolean {
428
- const normalized = command.trim();
429
- if (normalized.length === 0) {
430
- return false;
431
- }
432
- try {
433
- if (process.platform === 'win32') {
434
- execFileSync('where', [normalized], {
435
- stdio: 'ignore',
436
- });
437
- return true;
438
- }
439
- execFileSync('sh', ['-lc', `command -v ${normalized} >/dev/null 2>&1`], {
440
- stdio: 'ignore',
441
- });
442
- return true;
443
- } catch {
444
- return false;
445
- }
446
- }
447
-
448
- function readGhAuthTokenForDebug(): string | null {
449
- try {
450
- const stdout = execFileSync('gh', ['auth', 'token'], {
451
- encoding: 'utf8',
452
- stdio: ['ignore', 'pipe', 'ignore'],
453
- timeout: 2000,
454
- windowsHide: true,
455
- });
456
- const token = stdout.trim();
457
- return token.length > 0 ? token : null;
458
- } catch {
459
- return null;
460
- }
461
- }
462
-
463
- function formatGitHubDebugTokens(state: GitHubDebugAuthState): string {
464
- if (!state.enabled) {
465
- return '[gh:off tk:na au:na pr:na]';
466
- }
467
- return `[gh:on tk:${state.token} au:${state.auth} pr:${state.projectPr}]`;
468
- }
469
-
470
- async function main(): Promise<number> {
471
- if (!process.stdin.isTTY || !process.stdout.isTTY) {
472
- process.stderr.write('codex:live:mux requires a TTY stdin/stdout\n');
473
- return 2;
474
- }
475
-
476
- const invocationDirectory =
477
- process.env.HARNESS_INVOKE_CWD ?? process.env.INIT_CWD ?? process.cwd();
478
- migrateLegacyHarnessLayout(invocationDirectory, process.env);
479
- loadHarnessSecrets({ cwd: invocationDirectory });
480
- const options = parseMuxArgs(process.argv.slice(2));
481
- const loadedConfig = loadHarnessConfig({
482
- cwd: options.invocationDirectory,
483
- });
484
- const debugConfig = loadedConfig.config.debug;
485
- const perfEnabled = parseBooleanEnv(
486
- process.env.HARNESS_PERF_ENABLED,
487
- debugConfig.enabled && debugConfig.perf.enabled,
488
- );
489
- const perfFilePath = resolveHarnessRuntimePath(
490
- options.invocationDirectory,
491
- process.env.HARNESS_PERF_FILE_PATH ?? debugConfig.perf.filePath,
492
- );
493
- const perfTruncateOnStart = parseBooleanEnv(
494
- process.env.HARNESS_PERF_TRUNCATE_ON_START,
495
- debugConfig.overwriteArtifactsOnStart,
496
- );
497
- if (perfEnabled) {
498
- prepareArtifactPath(perfFilePath, perfTruncateOnStart);
499
- }
500
- configurePerfCore({
501
- enabled: perfEnabled,
502
- filePath: perfFilePath,
503
- });
504
- const startupSpan = startPerfSpan('mux.startup.total', {
505
- invocationDirectory: options.invocationDirectory,
506
- codexArgs: options.codexArgs.length,
507
- });
508
- const muxSessionName =
509
- typeof process.env.HARNESS_SESSION_NAME === 'string' &&
510
- process.env.HARNESS_SESSION_NAME.trim().length > 0
511
- ? process.env.HARNESS_SESSION_NAME.trim()
512
- : null;
513
- recordPerfEvent('mux.startup.begin', {
514
- stdinTty: process.stdin.isTTY ? 1 : 0,
515
- stdoutTty: process.stdout.isTTY ? 1 : 0,
516
- perfFilePath,
517
- });
518
- if (loadedConfig.error !== null) {
519
- process.stderr.write(
520
- `[config] using last-known-good due to parse error: ${loadedConfig.error}\n`,
521
- );
522
- }
523
- const shortcutBindings = resolveMuxShortcutBindings(loadedConfig.config.mux.keybindings);
524
- const taskScreenKeybindings = resolveTaskScreenKeybindings(loadedConfig.config.mux.keybindings);
525
- const modalDismissShortcutBindings = resolveMuxShortcutBindings({
526
- 'mux.app.quit': ['escape'],
527
- 'mux.app.interrupt-all': [],
528
- 'mux.gateway.profile.toggle': [],
529
- 'mux.gateway.status-timeline.toggle': [],
530
- 'mux.conversation.new': [],
531
- 'mux.conversation.critique.open-or-create': [],
532
- 'mux.conversation.next': [],
533
- 'mux.conversation.previous': [],
534
- 'mux.conversation.interrupt': [],
535
- 'mux.conversation.archive': [],
536
- 'mux.conversation.takeover': [],
537
- 'mux.conversation.delete': [],
538
- 'mux.directory.add': [],
539
- 'mux.directory.close': [],
540
- });
541
- const store = new SqliteEventStore(options.storePath);
542
-
543
- let size = await readStartupTerminalSize();
544
- recordPerfEvent('mux.startup.terminal-size', {
545
- cols: size.cols,
546
- rows: size.rows,
547
- });
548
- const configuredMuxUi = loadedConfig.config.mux.ui;
549
- let runtimeThemeConfig: HarnessMuxThemeConfig | null = configuredMuxUi.theme;
550
- const resolveAndApplyRuntimeTheme = (
551
- nextThemeConfig: HarnessMuxThemeConfig | null,
552
- writeErrorToStderr = false,
553
- ) => {
554
- const resolved = resolveConfiguredMuxTheme({
555
- config: nextThemeConfig,
556
- cwd: options.invocationDirectory,
557
- });
558
- if (resolved.error !== null && writeErrorToStderr) {
559
- process.stderr.write(`[theme] ${resolved.error}; using preset fallback\n`);
560
- }
561
- setActiveMuxTheme(resolved.theme);
562
- runtimeThemeConfig = nextThemeConfig;
563
- return resolved;
564
- };
565
- const resolvedMuxTheme = resolveAndApplyRuntimeTheme(runtimeThemeConfig, true);
566
- let currentModalTheme = resolvedMuxTheme.theme.modalTheme;
567
- const configuredMuxGit = loadedConfig.config.mux.git;
568
- const githubTokenEnvVar = loadedConfig.config.github.tokenEnvVar;
569
- const envGitHubTokenRaw = process.env[githubTokenEnvVar];
570
- const hasEnvGitHubToken =
571
- typeof envGitHubTokenRaw === 'string' && envGitHubTokenRaw.trim().length > 0;
572
- const githubDebugAuthState: GitHubDebugAuthState = {
573
- enabled: loadedConfig.config.github.enabled,
574
- token: hasEnvGitHubToken ? 'env' : 'none',
575
- auth: loadedConfig.config.github.enabled ? (hasEnvGitHubToken ? 'ok' : 'uk') : 'na',
576
- projectPr: 'na',
577
- };
578
- if (githubDebugAuthState.enabled && !hasEnvGitHubToken) {
579
- const ghToken = commandExistsOnPath('gh') ? readGhAuthTokenForDebug() : null;
580
- if (ghToken !== null) {
581
- githubDebugAuthState.token = 'gh';
582
- githubDebugAuthState.auth = 'ok';
583
- } else {
584
- githubDebugAuthState.token = 'none';
585
- githubDebugAuthState.auth = 'no';
586
- }
587
- }
588
- const configuredCodexLaunch = loadedConfig.config.codex.launch;
589
- const configuredCritique = loadedConfig.config.critique;
590
- const codexLaunchModeByDirectoryPath: Record<string, 'yolo' | 'standard'> = {};
591
- for (const [directoryPath, mode] of Object.entries(configuredCodexLaunch.directoryModes)) {
592
- const normalizedDirectoryPath = resolveWorkspacePathForMux(
593
- options.invocationDirectory,
594
- directoryPath,
595
- );
596
- codexLaunchModeByDirectoryPath[normalizedDirectoryPath] = mode;
597
- }
598
- const configuredClaudeLaunch = loadedConfig.config.claude.launch;
599
- const claudeLaunchModeByDirectoryPath: Record<string, 'yolo' | 'standard'> = {};
600
- for (const [directoryPath, mode] of Object.entries(configuredClaudeLaunch.directoryModes)) {
601
- const normalizedDirectoryPath = resolveWorkspacePathForMux(
602
- options.invocationDirectory,
603
- directoryPath,
604
- );
605
- claudeLaunchModeByDirectoryPath[normalizedDirectoryPath] = mode;
606
- }
607
- const configuredCursorLaunch = loadedConfig.config.cursor.launch;
608
- const cursorLaunchModeByDirectoryPath: Record<string, 'yolo' | 'standard'> = {};
609
- for (const [directoryPath, mode] of Object.entries(configuredCursorLaunch.directoryModes)) {
610
- const normalizedDirectoryPath = resolveWorkspacePathForMux(
611
- options.invocationDirectory,
612
- directoryPath,
613
- );
614
- cursorLaunchModeByDirectoryPath[normalizedDirectoryPath] = mode;
615
- }
616
- let leftPaneColsOverride: number | null =
617
- configuredMuxUi.paneWidthPercent === null
618
- ? null
619
- : leftColsFromPaneWidthPercent(size.cols, configuredMuxUi.paneWidthPercent);
620
- let layout = computeDualPaneLayout(size.cols, size.rows, {
621
- leftCols: leftPaneColsOverride,
622
- });
623
- const resizeMinIntervalMs = debugConfig.enabled
624
- ? debugConfig.mux.resizeMinIntervalMs
625
- : DEFAULT_RESIZE_MIN_INTERVAL_MS;
626
- const ptyResizeSettleMs = debugConfig.enabled
627
- ? debugConfig.mux.ptyResizeSettleMs
628
- : DEFAULT_PTY_RESIZE_SETTLE_MS;
629
- const startupSettleQuietMs = debugConfig.enabled
630
- ? debugConfig.mux.startupSettleQuietMs
631
- : DEFAULT_STARTUP_SETTLE_QUIET_MS;
632
- const controlPlaneConnectRetryWindowMs = parsePositiveInt(
633
- process.env.HARNESS_CONTROL_PLANE_CONNECT_RETRY_WINDOW_MS,
634
- 0,
635
- );
636
- const controlPlaneConnectRetryDelayMs = Math.max(
637
- 1,
638
- parsePositiveInt(process.env.HARNESS_CONTROL_PLANE_CONNECT_RETRY_DELAY_MS, 50),
639
- );
640
- const backgroundResumePersisted = parseBooleanEnv(
641
- process.env.HARNESS_MUX_BACKGROUND_RESUME,
642
- DEFAULT_BACKGROUND_RESUME_PERSISTED,
643
- );
644
- const backgroundProbesEnabled = parseBooleanEnv(
645
- process.env.HARNESS_MUX_BACKGROUND_PROBES,
646
- DEFAULT_BACKGROUND_PROBES_ENABLED,
647
- );
648
- const validateAnsi = debugConfig.enabled ? debugConfig.mux.validateAnsi : false;
649
-
650
- process.stdin.setRawMode(true);
651
- process.stdin.resume();
652
- const inputModeManager = createMuxInputModeManager((sequence) => {
653
- process.stdout.write(sequence);
654
- });
655
-
656
- const paletteProbeSpan = startPerfSpan('mux.startup.palette-probe');
657
- const probedPalette = await probeTerminalPalette();
658
- paletteProbeSpan.end({
659
- hasForeground: probedPalette.foregroundHex !== undefined,
660
- hasBackground: probedPalette.backgroundHex !== undefined,
661
- });
662
- const resolvedTerminalForegroundHex =
663
- resolvedMuxTheme.theme.terminalForegroundHex ??
664
- process.env.HARNESS_TERM_FG ??
665
- probedPalette.foregroundHex;
666
- const resolvedTerminalBackgroundHex =
667
- resolvedMuxTheme.theme.terminalBackgroundHex ??
668
- process.env.HARNESS_TERM_BG ??
669
- probedPalette.backgroundHex;
670
- let muxRecordingWriter: ReturnType<typeof createTerminalRecordingWriter> | null = null;
671
- let muxRecordingOracle: TerminalSnapshotOracle | null = null;
672
- if (options.recordingPath !== null) {
673
- mkdirSync(dirname(options.recordingPath), { recursive: true });
674
- const recordIntervalMs = Math.max(1, Math.floor(1000 / options.recordingFps));
675
- const recordingWriterOptions: Parameters<typeof createTerminalRecordingWriter>[0] = {
676
- filePath: options.recordingPath,
677
- source: 'codex-live-mux',
678
- defaultForegroundHex: resolvedTerminalForegroundHex ?? 'd0d7de',
679
- defaultBackgroundHex: resolvedTerminalBackgroundHex ?? '0f1419',
680
- minFrameIntervalMs: recordIntervalMs,
681
- };
682
- if (probedPalette.indexedHexByCode !== undefined) {
683
- recordingWriterOptions.ansiPaletteIndexedHex = probedPalette.indexedHexByCode;
684
- }
685
- muxRecordingWriter = createTerminalRecordingWriter(recordingWriterOptions);
686
- muxRecordingOracle = new TerminalSnapshotOracle(size.cols, size.rows);
687
- }
688
- const recordingService = new RecordingService({
689
- recordingWriter: muxRecordingWriter,
690
- recordingPath: options.recordingPath,
691
- recordingGifOutputPath: options.recordingGifOutputPath,
692
- renderTerminalRecordingToGif,
693
- writeStderr: (text) => {
694
- process.stderr.write(text);
695
- },
696
- });
697
- const controlPlaneMode =
698
- options.controlPlaneHost !== null && options.controlPlanePort !== null
699
- ? {
700
- mode: 'remote' as const,
701
- host: options.controlPlaneHost,
702
- port: options.controlPlanePort,
703
- ...(options.controlPlaneAuthToken !== null
704
- ? {
705
- authToken: options.controlPlaneAuthToken,
706
- }
707
- : {}),
708
- connectRetryWindowMs: controlPlaneConnectRetryWindowMs,
709
- connectRetryDelayMs: controlPlaneConnectRetryDelayMs,
710
- }
711
- : {
712
- mode: 'embedded' as const,
713
- };
714
- const closeLiveSessionsOnClientStop = controlPlaneMode.mode === 'embedded';
715
- const controlPlaneOpenSpan = startPerfSpan('mux.startup.control-plane-open');
716
- const controlPlaneClient = await openCodexControlPlaneClient(controlPlaneMode, {
717
- startEmbeddedServer: async () =>
718
- await startControlPlaneStreamServer({
719
- stateStorePath: resolveHarnessRuntimePath(
720
- options.invocationDirectory,
721
- '.harness/control-plane.sqlite',
722
- ),
723
- codexTelemetry: loadedConfig.config.codex.telemetry,
724
- codexHistory: loadedConfig.config.codex.history,
725
- critique: loadedConfig.config.critique,
726
- agentInstall: {
727
- codex: loadedConfig.config.codex.install,
728
- claude: loadedConfig.config.claude.install,
729
- cursor: loadedConfig.config.cursor.install,
730
- critique: loadedConfig.config.critique.install,
731
- },
732
- gitStatus: {
733
- enabled: loadedConfig.config.mux.git.enabled,
734
- pollMs: loadedConfig.config.mux.git.idlePollMs,
735
- maxConcurrency: loadedConfig.config.mux.git.maxConcurrency,
736
- minDirectoryRefreshMs: Math.max(loadedConfig.config.mux.git.idlePollMs, 30_000),
737
- },
738
- github: {
739
- enabled: loadedConfig.config.github.enabled,
740
- apiBaseUrl: loadedConfig.config.github.apiBaseUrl,
741
- tokenEnvVar: loadedConfig.config.github.tokenEnvVar,
742
- pollMs: loadedConfig.config.github.pollMs,
743
- maxConcurrency: loadedConfig.config.github.maxConcurrency,
744
- branchStrategy: loadedConfig.config.github.branchStrategy,
745
- viewerLogin: loadedConfig.config.github.viewerLogin,
746
- },
747
- lifecycleHooks: loadedConfig.config.hooks.lifecycle,
748
- startSession: (input) => {
749
- const sessionOptions: Parameters<typeof startCodexLiveSession>[0] = {
750
- args: input.args,
751
- initialCols: input.initialCols,
752
- initialRows: input.initialRows,
753
- enableSnapshotModel: debugConfig.mux.serverSnapshotModelEnabled,
754
- };
755
- if (input.useNotifyHook !== undefined) {
756
- sessionOptions.useNotifyHook = input.useNotifyHook;
757
- }
758
- if (input.command !== undefined) {
759
- sessionOptions.command = input.command;
760
- }
761
- if (input.baseArgs !== undefined) {
762
- sessionOptions.baseArgs = input.baseArgs;
763
- }
764
- if (input.env !== undefined) {
765
- sessionOptions.env = input.env;
766
- }
767
- if (input.cwd !== undefined) {
768
- sessionOptions.cwd = input.cwd;
769
- }
770
- if (input.terminalForegroundHex !== undefined) {
771
- sessionOptions.terminalForegroundHex = input.terminalForegroundHex;
772
- }
773
- if (input.terminalBackgroundHex !== undefined) {
774
- sessionOptions.terminalBackgroundHex = input.terminalBackgroundHex;
775
- }
776
- return startCodexLiveSession(sessionOptions);
777
- },
778
- }),
779
- });
780
- controlPlaneOpenSpan.end();
781
- const streamClient = controlPlaneClient.client;
782
- const controlPlaneService = new ControlPlaneService(streamClient, {
783
- tenantId: options.scope.tenantId,
784
- userId: options.scope.userId,
785
- workspaceId: options.scope.workspaceId,
786
- });
787
- const startupObservedCursor = await readObservedStreamCursorBaseline(streamClient, options.scope);
788
- const directoryUpsertSpan = startPerfSpan('mux.startup.directory-upsert');
789
- const persistedDirectory = await controlPlaneService.upsertDirectory({
790
- directoryId: `directory-${options.scope.workspaceId}`,
791
- path: options.invocationDirectory,
792
- });
793
- directoryUpsertSpan.end();
794
- const workspace = new WorkspaceModel({
795
- activeDirectoryId: persistedDirectory.directoryId,
796
- leftNavSelection: {
797
- kind: 'project',
798
- directoryId: persistedDirectory.directoryId,
799
- },
800
- latestTaskPaneView: {
801
- rows: [],
802
- taskIds: [],
803
- repositoryIds: [],
804
- actions: [],
805
- actionCells: [],
806
- top: 0,
807
- selectedRepositoryId: null,
808
- },
809
- taskDraftComposer: createTaskComposerBuffer(''),
810
- repositoriesCollapsed: configuredMuxUi.repositoriesCollapsed,
811
- shortcutsCollapsed: configuredMuxUi.shortcutsCollapsed,
812
- });
813
- workspace.repositoryToggleChordPrefixAtMs = null;
814
- workspace.projectPaneSnapshot = null;
815
- workspace.projectPaneScrollTop = 0;
816
- workspace.taskPaneScrollTop = 0;
817
- workspace.taskPaneSelectedTaskId = null;
818
- workspace.taskPaneSelectedRepositoryId = null;
819
- workspace.taskRepositoryDropdownOpen = false;
820
- workspace.taskEditorTarget = {
821
- kind: 'draft',
822
- };
823
- workspace.taskPaneSelectionFocus = 'task';
824
- workspace.taskPaneNotice = null;
825
- workspace.taskPaneTaskEditClickState = null;
826
- workspace.taskPaneRepositoryEditClickState = null;
827
- workspace.homePaneDragState = null;
828
-
829
- const sessionEnv: Record<string, string> = {
830
- ...sanitizeProcessEnv(),
831
- TERM: process.env.TERM ?? 'xterm-256color',
832
- };
833
- const directoryManager = new DirectoryManager<ControlPlaneDirectoryRecord, GitSummary>();
834
- directoryManager.setDirectory(persistedDirectory.directoryId, persistedDirectory);
835
- const directoryRecords = directoryManager.readonlyDirectories();
836
- const gitSummaryByDirectoryId = directoryManager.mutableGitSummaries();
837
- const repositoryManager = new RepositoryManager<
838
- ControlPlaneRepositoryRecord,
839
- GitRepositorySnapshot
840
- >();
841
- const repositories = repositoryManager.mutableRepositories();
842
- const repositoryAssociationByDirectoryId = repositoryManager.mutableDirectoryAssociations();
843
- const directoryRepositorySnapshotByDirectoryId = repositoryManager.mutableDirectorySnapshots();
844
- const muxControllerId = `human-mux-${process.pid}-${randomUUID()}`;
845
- const muxControllerLabel = `human mux ${process.pid}`;
846
- const conversationManager = new ConversationManager();
847
- const conversationRecords = conversationManager.readonlyConversations();
848
- const taskManager = new TaskManager<ControlPlaneTaskRecord, TaskComposerBuffer, NodeJS.Timeout>();
849
- const statusTimelineRecorder = new StatusTimelineRecorder({
850
- statePath: resolveStatusTimelineStatePath(options.invocationDirectory, muxSessionName),
851
- });
852
- const renderTraceRecorder = new RenderTraceRecorder({
853
- statePath: resolveRenderTraceStatePath(options.invocationDirectory, muxSessionName),
854
- });
855
- const resolveTraceLabels = (input: {
856
- sessionId: string | null;
857
- directoryId: string | null;
858
- conversationId: string | null;
859
- }): RenderTraceLabels => {
860
- const conversation =
861
- input.sessionId === null
862
- ? input.conversationId === null
863
- ? null
864
- : (conversationManager.get(input.conversationId) ?? null)
865
- : (conversationManager.get(input.sessionId) ?? null);
866
- const resolvedDirectoryId = input.directoryId ?? conversation?.directoryId ?? null;
867
- const directory =
868
- resolvedDirectoryId === null ? null : directoryManager.getDirectory(resolvedDirectoryId);
869
- const repositoryId =
870
- resolvedDirectoryId === null
871
- ? null
872
- : (repositoryAssociationByDirectoryId.get(resolvedDirectoryId) ?? null);
873
- const repository = repositoryId === null ? null : (repositories.get(repositoryId) ?? null);
874
- return {
875
- repositoryId,
876
- repositoryName: repository?.name ?? null,
877
- projectId: resolvedDirectoryId,
878
- projectPath: directory?.path ?? null,
879
- threadId: input.sessionId ?? conversation?.sessionId ?? null,
880
- threadTitle: conversation?.title ?? null,
881
- agentType: conversation?.agentType ?? null,
882
- conversationId: input.conversationId ?? conversation?.sessionId ?? null,
883
- };
884
- };
885
- const recordStatusTimeline = (input: {
886
- direction: 'incoming' | 'outgoing';
887
- source: string;
888
- eventType: string;
889
- labels: StatusTimelineLabels;
890
- payload: unknown;
891
- dedupeKey?: string;
892
- dedupeValue?: string;
893
- }): void => {
894
- const baseRecordInput = {
895
- direction: input.direction,
896
- source: input.source,
897
- eventType: input.eventType,
898
- labels: input.labels,
899
- payload: input.payload,
900
- };
901
- const recordInput: Parameters<StatusTimelineRecorder['record']>[0] =
902
- input.dedupeKey !== undefined && input.dedupeValue !== undefined
903
- ? {
904
- ...baseRecordInput,
905
- dedupeKey: input.dedupeKey,
906
- dedupeValue: input.dedupeValue,
907
- }
908
- : baseRecordInput;
909
- statusTimelineRecorder.record(recordInput);
910
- };
911
- const recordRenderTrace = (input: {
912
- direction: 'incoming' | 'outgoing';
913
- source: string;
914
- eventType: string;
915
- labels: RenderTraceLabels;
916
- payload: unknown;
917
- dedupeKey?: string;
918
- dedupeValue?: string;
919
- }): void => {
920
- const baseRecordInput = {
921
- direction: input.direction,
922
- source: input.source,
923
- eventType: input.eventType,
924
- labels: input.labels,
925
- payload: input.payload,
926
- };
927
- const recordInput: Parameters<RenderTraceRecorder['record']>[0] =
928
- input.dedupeKey !== undefined && input.dedupeValue !== undefined
929
- ? {
930
- ...baseRecordInput,
931
- dedupeKey: input.dedupeKey,
932
- dedupeValue: input.dedupeValue,
933
- }
934
- : baseRecordInput;
935
- renderTraceRecorder.record(recordInput);
936
- };
937
- let keyEventSubscription: Awaited<ReturnType<typeof subscribeControlPlaneKeyEvents>> | null =
938
- null;
939
- let hydrateStartupStateForStartupOrchestrator = async (
940
- _afterCursor: number | null,
941
- ): Promise<void> => {};
942
- let queuePersistedConversationsForStartupOrchestrator = (
943
- _activeSessionId: string | null,
944
- ): number => 0;
945
- let activateConversationForStartupOrchestrator = async (_sessionId: string): Promise<void> => {};
946
- let shuttingDown = false;
947
- const startupOrchestrator = new StartupOrchestrator({
948
- startupSettleQuietMs,
949
- startupSettleNonemptyFallbackMs: DEFAULT_STARTUP_SETTLE_NONEMPTY_FALLBACK_MS,
950
- backgroundWaitMaxMs: DEFAULT_BACKGROUND_START_MAX_WAIT_MS,
951
- backgroundProbeEnabled: backgroundProbesEnabled,
952
- backgroundResumeEnabled: backgroundResumePersisted,
953
- startPerfSpan,
954
- startupSpan,
955
- recordPerfEvent,
956
- getConversation: (sessionId) => conversationManager.get(sessionId),
957
- isShuttingDown: () => shuttingDown,
958
- refreshProcessUsage: (reason) =>
959
- void processUsageRefreshService.refresh(reason, conversationRecords),
960
- queuePersistedConversationsInBackground: (initialActiveId) =>
961
- queuePersistedConversationsForStartupOrchestrator(initialActiveId),
962
- hydrateStartupState: async (afterCursor) =>
963
- await hydrateStartupStateForStartupOrchestrator(afterCursor),
964
- activateConversation: async (sessionId) =>
965
- await activateConversationForStartupOrchestrator(sessionId),
966
- conversationCount: () => conversationManager.size(),
967
- });
968
-
969
- const resolveActiveDirectoryId = (): string | null => {
970
- workspace.activeDirectoryId = directoryManager.resolveActiveDirectoryId(
971
- workspace.activeDirectoryId,
972
- );
973
- return workspace.activeDirectoryId;
974
- };
975
-
976
- const resolveDirectoryForAction = (): string | null => {
977
- return resolveDirectoryForActionFn({
978
- mainPaneMode: workspace.mainPaneMode,
979
- activeDirectoryId: workspace.activeDirectoryId,
980
- activeConversationId: conversationManager.activeConversationId,
981
- conversations: conversationRecords,
982
- directoriesHas: (directoryId) => directoryManager.hasDirectory(directoryId),
983
- });
984
- };
985
-
986
- const repositoryGroupIdForDirectory = (directoryId: string): string =>
987
- repositoryManager.repositoryGroupIdForDirectory(directoryId, UNTRACKED_REPOSITORY_GROUP_ID);
988
-
989
- const collapseRepositoryGroup = (repositoryGroupId: string): void => {
990
- repositoryManager.collapseRepositoryGroup(repositoryGroupId, workspace.repositoriesCollapsed);
991
- };
992
-
993
- const expandRepositoryGroup = (repositoryGroupId: string): void => {
994
- repositoryManager.expandRepositoryGroup(repositoryGroupId, workspace.repositoriesCollapsed);
995
- };
996
-
997
- const toggleRepositoryGroup = (repositoryGroupId: string): void => {
998
- repositoryManager.toggleRepositoryGroup(repositoryGroupId, workspace.repositoriesCollapsed);
999
- };
1000
-
1001
- const collapseAllRepositoryGroups = (): void => {
1002
- workspace.repositoriesCollapsed = repositoryManager.collapseAllRepositoryGroups();
1003
- queuePersistMuxUiState();
1004
- };
1005
-
1006
- const expandAllRepositoryGroups = (): void => {
1007
- workspace.repositoriesCollapsed = repositoryManager.expandAllRepositoryGroups();
1008
- queuePersistMuxUiState();
1009
- };
1010
-
1011
- const firstDirectoryForRepositoryGroup = (repositoryGroupId: string): string | null => {
1012
- return firstDirectoryForRepositoryGroupFn(
1013
- directoryRecords,
1014
- repositoryGroupIdForDirectory,
1015
- repositoryGroupId,
1016
- );
1017
- };
1018
-
1019
- conversationManager.configureEnsureDependencies({
1020
- resolveDefaultDirectoryId: resolveActiveDirectoryId,
1021
- normalizeAdapterState,
1022
- createConversation: (input) =>
1023
- createConversationState(
1024
- input.sessionId,
1025
- input.directoryId,
1026
- input.title,
1027
- input.agentType,
1028
- input.adapterState,
1029
- `turn-${randomUUID()}`,
1030
- options.scope,
1031
- layout.rightCols,
1032
- layout.paneRows,
1033
- ),
1034
- });
1035
-
1036
- const ensureConversation = (sessionId: string, seed?: ConversationSeed): ConversationState => {
1037
- return conversationManager.ensure(sessionId, seed);
1038
- };
1039
-
1040
- const applyControlPlaneKeyEvent = (event: ControlPlaneKeyEvent): void => {
1041
- const existing = conversationManager.get(event.sessionId);
1042
- const beforeProjection =
1043
- existing === undefined
1044
- ? null
1045
- : sessionProjectionInstrumentation.snapshotForConversation(existing);
1046
- const updated = applyMuxControlPlaneKeyEvent(event, {
1047
- removedConversationIds: conversationManager.removedConversationIds,
1048
- ensureConversation,
1049
- });
1050
- if (updated === null) {
1051
- return;
1052
- }
1053
- if (event.type === 'session-status') {
1054
- if (event.live) {
1055
- void conversationLifecycle.subscribeConversationEvents(event.sessionId).catch(() => {});
1056
- } else {
1057
- void conversationLifecycle.unsubscribeConversationEvents(event.sessionId).catch(() => {});
1058
- }
1059
- }
1060
- sessionProjectionInstrumentation.refreshSelectorSnapshot(
1061
- `event:${event.type}`,
1062
- directoryRecords,
1063
- conversationRecords,
1064
- conversationManager.orderedIds(),
1065
- );
1066
- sessionProjectionInstrumentation.recordTransition(event, beforeProjection, updated);
1067
- };
1068
-
1069
- const directoryHydrationService = new DirectoryHydrationService<ControlPlaneDirectoryRecord>({
1070
- controlPlaneService,
1071
- resolveWorkspacePathForMux: (rawPath) =>
1072
- resolveWorkspacePathForMux(options.invocationDirectory, rawPath),
1073
- clearDirectories: () => {
1074
- directoryManager.clearDirectories();
1075
- },
1076
- setDirectory: (directoryId, directory) => {
1077
- directoryManager.setDirectory(directoryId, directory);
1078
- },
1079
- hasDirectory: (directoryId) => directoryManager.hasDirectory(directoryId),
1080
- persistedDirectory,
1081
- resolveActiveDirectoryId,
1082
- });
1083
-
1084
- const hydrateDirectoryList = async (): Promise<void> => {
1085
- await directoryHydrationService.hydrate();
1086
- };
1087
-
1088
- const syncRepositoryAssociationsWithDirectorySnapshots = (): void => {
1089
- repositoryManager.syncWithDirectories((directoryId) =>
1090
- directoryManager.hasDirectory(directoryId),
1091
- );
1092
- };
1093
-
1094
- const hydratePersistedConversationsForDirectory = async (
1095
- directoryId: string,
1096
- ): Promise<number> => {
1097
- const persistedRows = await controlPlaneService.listConversations(directoryId);
1098
- for (const record of persistedRows) {
1099
- conversationManager.upsertFromPersistedRecord({
1100
- record,
1101
- ensureConversation,
1102
- });
1103
- }
1104
- return persistedRows.length;
1105
- };
1106
-
1107
- const conversationLifecycle = new ConversationLifecycle<
1108
- ConversationState,
1109
- ControlPlaneSessionSummary,
1110
- ConversationState['controller']
1111
- >({
1112
- streamSubscriptions: {
1113
- subscribePtyEvents: async (sessionId) => {
1114
- await controlPlaneService.subscribePtyEvents(sessionId);
1115
- },
1116
- unsubscribePtyEvents: async (sessionId) => {
1117
- await controlPlaneService.unsubscribePtyEvents(sessionId);
1118
- },
1119
- isSessionNotFoundError,
1120
- isSessionNotLiveError,
1121
- subscribeObservedStream: async (afterCursor) => {
1122
- return await subscribeObservedStream(streamClient, options.scope, afterCursor);
1123
- },
1124
- unsubscribeObservedStream: async (subscriptionId) => {
1125
- await unsubscribeObservedStream(streamClient, subscriptionId);
1126
- },
1127
- },
1128
- starter: {
1129
- runWithStartInFlight: async (sessionId, run) => {
1130
- return await conversationManager.runWithStartInFlight(sessionId, run);
1131
- },
1132
- conversationById: (sessionId) => conversationManager.get(sessionId),
1133
- ensureConversation,
1134
- normalizeThreadAgentType,
1135
- codexArgs: options.codexArgs,
1136
- critiqueDefaultArgs: configuredCritique.launch.defaultArgs,
1137
- sessionCwdForConversation: (conversation) => {
1138
- const configuredDirectoryPath =
1139
- conversation.directoryId === null
1140
- ? null
1141
- : (directoryManager.getDirectory(conversation.directoryId)?.path ?? null);
1142
- return resolveWorkspacePathForMux(
1143
- options.invocationDirectory,
1144
- configuredDirectoryPath ?? options.invocationDirectory,
1145
- );
1146
- },
1147
- buildLaunchArgs: (input) => {
1148
- return buildAgentSessionStartArgs(
1149
- input.agentType,
1150
- input.baseArgsForAgent,
1151
- input.adapterState,
1152
- {
1153
- directoryPath: input.sessionCwd,
1154
- codexLaunchDefaultMode: configuredCodexLaunch.defaultMode,
1155
- codexLaunchModeByDirectoryPath: codexLaunchModeByDirectoryPath,
1156
- claudeLaunchDefaultMode: configuredClaudeLaunch.defaultMode,
1157
- claudeLaunchModeByDirectoryPath: claudeLaunchModeByDirectoryPath,
1158
- cursorLaunchDefaultMode: configuredCursorLaunch.defaultMode,
1159
- cursorLaunchModeByDirectoryPath: cursorLaunchModeByDirectoryPath,
1160
- },
1161
- );
1162
- },
1163
- launchCommandForAgent,
1164
- formatCommandForDebugBar,
1165
- startConversationSpan: (sessionId) =>
1166
- startPerfSpan('mux.conversation.start', {
1167
- sessionId,
1168
- }),
1169
- firstPaintTargetSessionId: () => startupOrchestrator.firstPaintTargetSessionId,
1170
- endStartCommandSpan: (input) => {
1171
- startupOrchestrator.endStartCommandSpan(input);
1172
- },
1173
- layout: () => layout,
1174
- startPtySession: async (input) => {
1175
- await controlPlaneService.startPtySession(input);
1176
- },
1177
- setPtySize: (sessionId, size) => {
1178
- ptySizeByConversationId.set(sessionId, size);
1179
- },
1180
- sendResize: (sessionId, cols, rows) => {
1181
- streamClient.sendResize(sessionId, cols, rows);
1182
- },
1183
- sessionEnv,
1184
- worktreeId: options.scope.worktreeId,
1185
- terminalForegroundHex: resolvedTerminalForegroundHex,
1186
- terminalBackgroundHex: resolvedTerminalBackgroundHex,
1187
- recordStartCommand: (sessionId, launchArgs) => {
1188
- recordPerfEvent('mux.conversation.start.command', {
1189
- sessionId,
1190
- argCount: launchArgs.length,
1191
- resumed: launchArgs[0] === 'resume',
1192
- });
1193
- },
1194
- getSessionStatus: async (sessionId) => {
1195
- return await controlPlaneService.getSessionStatus(sessionId);
1196
- },
1197
- upsertFromSessionSummary: (summary) => {
1198
- conversationManager.upsertFromSessionSummary({
1199
- summary,
1200
- ensureConversation,
1201
- });
1202
- },
1203
- },
1204
- startupHydration: {
1205
- startHydrationSpan: () => startPerfSpan('mux.startup.hydrate-conversations'),
1206
- hydrateDirectoryList,
1207
- directoryIds: () => directoryManager.directoryIds(),
1208
- hydratePersistedConversationsForDirectory,
1209
- listSessions: async () => {
1210
- return await controlPlaneService.listSessions({
1211
- worktreeId: options.scope.worktreeId,
1212
- sort: 'started-asc',
1213
- });
1214
- },
1215
- upsertFromSessionSummary: (summary) => {
1216
- conversationManager.upsertFromSessionSummary({
1217
- summary,
1218
- ensureConversation,
1219
- });
1220
- },
1221
- },
1222
- startupQueue: {
1223
- orderedConversationIds: () => conversationManager.orderedIds(),
1224
- conversationById: (sessionId) => conversationManager.get(sessionId),
1225
- queueBackgroundOp: (task, label) => {
1226
- queueBackgroundControlPlaneOp(task, label);
1227
- },
1228
- markDirty: () => {
1229
- markDirty();
1230
- },
1231
- },
1232
- activation: {
1233
- getActiveConversationId: () => conversationManager.activeConversationId,
1234
- setActiveConversationId: (sessionId) => {
1235
- conversationManager.setActiveConversationId(sessionId);
1236
- },
1237
- isConversationPaneMode: () => workspace.mainPaneMode === 'conversation',
1238
- enterConversationPaneForActiveSession: (sessionId) => {
1239
- workspace.mainPaneMode = 'conversation';
1240
- workspace.selectLeftNavConversation(sessionId);
1241
- screen.resetFrameCache();
1242
- },
1243
- enterConversationPaneForSessionSwitch: (sessionId) => {
1244
- workspace.mainPaneMode = 'conversation';
1245
- workspace.selectLeftNavConversation(sessionId);
1246
- workspace.homePaneDragState = null;
1247
- workspace.taskPaneTaskEditClickState = null;
1248
- workspace.taskPaneRepositoryEditClickState = null;
1249
- workspace.projectPaneSnapshot = null;
1250
- workspace.projectPaneScrollTop = 0;
1251
- screen.resetFrameCache();
1252
- },
1253
- stopConversationTitleEditForOtherSession: (sessionId) => {
1254
- if (
1255
- workspace.conversationTitleEdit !== null &&
1256
- workspace.conversationTitleEdit.conversationId !== sessionId
1257
- ) {
1258
- stopConversationTitleEdit(true);
1259
- }
1260
- },
1261
- clearSelectionState: () => {
1262
- workspace.selection = null;
1263
- workspace.selectionDrag = null;
1264
- releaseViewportPinForSelection();
1265
- },
1266
- detachConversation: async (sessionId) => {
1267
- await detachConversation(sessionId);
1268
- },
1269
- conversationById: (sessionId) => conversationManager.get(sessionId),
1270
- noteGitActivity: (directoryId) => {
1271
- noteGitActivity(directoryId);
1272
- },
1273
- attachConversation: async (sessionId) => {
1274
- await attachConversation(sessionId);
1275
- },
1276
- isSessionNotFoundError,
1277
- isSessionNotLiveError,
1278
- markSessionUnavailable: (sessionId) => {
1279
- conversationManager.markSessionUnavailable(sessionId);
1280
- },
1281
- schedulePtyResizeImmediate: () => {
1282
- schedulePtyResize(
1283
- {
1284
- cols: layout.rightCols,
1285
- rows: layout.paneRows,
1286
- },
1287
- true,
1288
- );
1289
- },
1290
- markDirty: () => {
1291
- markDirty();
1292
- },
1293
- },
1294
- actions: {
1295
- controlPlaneService,
1296
- createConversationId: () => `conversation-${randomUUID()}`,
1297
- ensureConversation,
1298
- noteGitActivity: (directoryId) => {
1299
- noteGitActivity(directoryId);
1300
- },
1301
- orderedConversationIds: () => conversationManager.orderedIds(),
1302
- conversationById: (sessionId) => {
1303
- const conversation = conversationManager.get(sessionId);
1304
- if (conversation === undefined) {
1305
- return null;
1306
- }
1307
- return {
1308
- directoryId: conversation.directoryId,
1309
- agentType: conversation.agentType,
1310
- };
1311
- },
1312
- conversationsHas: (sessionId) => conversationManager.has(sessionId),
1313
- applyController: (sessionId, controller) => {
1314
- conversationManager.setController(sessionId, controller);
1315
- },
1316
- setLastEventNow: (sessionId) => {
1317
- conversationManager.setLastEventAt(sessionId, new Date().toISOString());
1318
- },
1319
- muxControllerId,
1320
- muxControllerLabel,
1321
- markDirty: () => {
1322
- markDirty();
1323
- },
1324
- },
1325
- titleEdit: {
1326
- workspace,
1327
- updateConversationTitle: async (input) => {
1328
- return await controlPlaneService.updateConversationTitle(input);
1329
- },
1330
- conversationById: (conversationId) => conversationManager.get(conversationId),
1331
- markDirty: () => {
1332
- markDirty();
1333
- },
1334
- queueControlPlaneOp: (task, label) => {
1335
- queueControlPlaneOp(task, label);
1336
- },
1337
- debounceMs: DEFAULT_CONVERSATION_TITLE_EDIT_DEBOUNCE_MS,
1338
- },
1339
- });
1340
-
1341
- const queuePersistedConversationsInBackground = (activeSessionId: string | null): number => {
1342
- return conversationLifecycle.queuePersistedConversationsInBackground(activeSessionId);
1343
- };
1344
-
1345
- const hydrateConversationList = async (): Promise<void> => {
1346
- await conversationLifecycle.hydrateConversationList();
1347
- };
1348
- const startupStateHydrationService = new StartupStateHydrationService<
1349
- ControlPlaneRepositoryRecord,
1350
- GitSummary,
1351
- GitRepositorySnapshot,
1352
- ControlPlaneDirectoryGitStatusRecord
1353
- >({
1354
- hydrateConversationList,
1355
- listRepositories: async () => {
1356
- return await controlPlaneService.listRepositories();
1357
- },
1358
- clearRepositories: () => {
1359
- repositories.clear();
1360
- },
1361
- setRepository: (repositoryId, repository) => {
1362
- repositories.set(repositoryId, repository);
1363
- },
1364
- syncRepositoryAssociationsWithDirectorySnapshots,
1365
- gitHydrationEnabled: configuredMuxGit.enabled,
1366
- listDirectoryGitStatuses: async () => {
1367
- return await controlPlaneService.listDirectoryGitStatuses();
1368
- },
1369
- setDirectoryGitSummary: (directoryId, summary) => {
1370
- gitSummaryByDirectoryId.set(directoryId, summary);
1371
- },
1372
- setDirectoryRepositorySnapshot: (directoryId, snapshot) => {
1373
- repositoryManager.setDirectoryRepositorySnapshot(directoryId, snapshot);
1374
- },
1375
- setDirectoryRepositoryAssociation: (directoryId, repositoryId) => {
1376
- repositoryManager.setDirectoryRepositoryAssociation(directoryId, repositoryId);
1377
- },
1378
- hydrateTaskPlanningState,
1379
- subscribeTaskPlanningEvents: async (afterCursor) => {
1380
- await conversationLifecycle.subscribeTaskPlanningEvents(afterCursor);
1381
- },
1382
- ensureActiveConversationId: () => {
1383
- conversationManager.ensureActiveConversationId();
1384
- },
1385
- activeConversationId: () => conversationManager.activeConversationId,
1386
- selectLeftNavConversation: (sessionId) => {
1387
- workspace.selectLeftNavConversation(sessionId);
1388
- },
1389
- enterHomePane: () => {
1390
- workspace.enterHomePane();
1391
- },
1392
- });
1393
- queuePersistedConversationsForStartupOrchestrator = queuePersistedConversationsInBackground;
1394
- hydrateStartupStateForStartupOrchestrator = async (afterCursor) =>
1395
- await startupStateHydrationService.hydrateStartupState(afterCursor);
1396
-
1397
- const runtimeGitState = new RuntimeGitState<ControlPlaneRepositoryRecord>({
1398
- enabled: configuredMuxGit.enabled,
1399
- directoryManager,
1400
- directoryRepositorySnapshotByDirectoryId,
1401
- repositoryAssociationByDirectoryId,
1402
- repositories,
1403
- parseRepositoryRecord,
1404
- loadingSummary: GIT_SUMMARY_LOADING,
1405
- emptyRepositorySnapshot: GIT_REPOSITORY_NONE,
1406
- syncRepositoryAssociationsWithDirectorySnapshots,
1407
- syncTaskPaneRepositorySelection: () => {
1408
- syncTaskPaneRepositorySelection();
1409
- },
1410
- markDirty: () => {
1411
- markDirty();
1412
- },
1413
- });
1414
- const deleteDirectoryGitState = (directoryId: string): void => {
1415
- runtimeGitState.deleteDirectoryGitState(directoryId);
1416
- };
1417
- const syncGitStateWithDirectories = (): void => {
1418
- runtimeGitState.syncGitStateWithDirectories();
1419
- };
1420
- const noteGitActivity = (directoryId: string | null): void => {
1421
- runtimeGitState.noteGitActivity(directoryId);
1422
- };
1423
- const applyObservedGitStatusEvent = (observed: StreamObservedEvent): void => {
1424
- runtimeGitState.applyObservedGitStatusEvent(observed);
1425
- };
1426
-
1427
- const idFactory = (): string => `event-${randomUUID()}`;
1428
- let exit: PtyExit | null = null;
1429
- const screen = new Screen({
1430
- writeError: (output) => {
1431
- process.stderr.write(output);
1432
- const prefix = '[mux] ansi-integrity-failed ';
1433
- const prefixIndex = output.indexOf(prefix);
1434
- if (prefixIndex < 0) {
1435
- return;
1436
- }
1437
- const issueText = output.slice(prefixIndex + prefix.length).trim();
1438
- const issues = issueText
1439
- .split(' | ')
1440
- .map((value) => value.trim())
1441
- .filter((value) => value.length > 0);
1442
- const activeConversationId = conversationManager.activeConversationId;
1443
- const labels = resolveTraceLabels({
1444
- sessionId: activeConversationId,
1445
- directoryId: workspace.activeDirectoryId,
1446
- conversationId: activeConversationId,
1447
- });
1448
- recordRenderTrace({
1449
- direction: 'outgoing',
1450
- source: 'screen',
1451
- eventType: 'ansi-integrity-failed',
1452
- labels,
1453
- payload: {
1454
- issues,
1455
- message: issueText,
1456
- },
1457
- dedupeKey: 'ansi-integrity-failed',
1458
- dedupeValue: issueText,
1459
- });
1460
- },
1461
- });
1462
- const conversationPane = new ConversationPane();
1463
- const homePane = new HomePane();
1464
- const projectPane = new ProjectPane();
1465
- const leftRailPane = new LeftRailPane();
1466
- let stop = false;
1467
- let inputRemainder = '';
1468
- const debugFooterNotice = new DebugFooterNotice({
1469
- ttlMs: DEBUG_FOOTER_NOTICE_TTL_MS,
1470
- });
1471
- const commandMenuRegistry = new CommandMenuRegistry<RuntimeCommandMenuContext>();
1472
- let commandMenuGitHubProjectPrState: CommandMenuGitHubProjectPrState | null = null;
1473
- let commandMenuScopedDirectoryId: string | null = null;
1474
- let themePickerSession: ThemePickerSessionState | null = null;
1475
- const isThreadScopedCommandActionId = (actionId: string): boolean =>
1476
- actionId.startsWith('thread.start.') || actionId.startsWith('thread.install.');
1477
- const commandMenuContext = (
1478
- input: {
1479
- readonly preferThreadScope?: boolean;
1480
- } = {},
1481
- ): RuntimeCommandMenuContext => {
1482
- const activeConversation = conversationManager.getActiveConversation();
1483
- const scopedDirectoryId =
1484
- (input.preferThreadScope === true || workspace.commandMenu?.scope === 'thread-start') &&
1485
- commandMenuScopedDirectoryId !== null &&
1486
- directoryManager.hasDirectory(commandMenuScopedDirectoryId)
1487
- ? commandMenuScopedDirectoryId
1488
- : null;
1489
- const activeDirectoryId = scopedDirectoryId ?? resolveDirectoryForAction();
1490
- const activeDirectoryRepositorySnapshot =
1491
- activeDirectoryId === null
1492
- ? null
1493
- : (directoryRepositorySnapshotByDirectoryId.get(activeDirectoryId) ?? null);
1494
- let githubRepositoryId =
1495
- activeDirectoryId === null
1496
- ? null
1497
- : (repositoryAssociationByDirectoryId.get(activeDirectoryId) ?? null);
1498
- if (githubRepositoryId !== null) {
1499
- const associatedRepository = repositories.get(githubRepositoryId);
1500
- if (associatedRepository === undefined || associatedRepository.archivedAt !== null) {
1501
- githubRepositoryId = null;
1502
- }
1503
- }
1504
- const snapshotRemoteUrl = activeDirectoryRepositorySnapshot?.normalizedRemoteUrl ?? null;
1505
- if (githubRepositoryId === null && snapshotRemoteUrl !== null) {
1506
- const snapshotRemote = normalizeGitHubRemoteUrl(snapshotRemoteUrl);
1507
- if (snapshotRemote !== null) {
1508
- for (const repository of repositories.values()) {
1509
- if (repository.archivedAt !== null) {
1510
- continue;
1511
- }
1512
- if (normalizeGitHubRemoteUrl(repository.remoteUrl) === snapshotRemote) {
1513
- githubRepositoryId = repository.repositoryId;
1514
- break;
1515
- }
1516
- }
1517
- }
1518
- }
1519
- const githubRepositoryRecord =
1520
- githubRepositoryId === null ? null : (repositories.get(githubRepositoryId) ?? null);
1521
- const githubProjectPrState =
1522
- activeDirectoryId !== null &&
1523
- commandMenuGitHubProjectPrState !== null &&
1524
- commandMenuGitHubProjectPrState.directoryId === activeDirectoryId
1525
- ? commandMenuGitHubProjectPrState
1526
- : null;
1527
- const currentBranchForActions =
1528
- activeDirectoryId === null
1529
- ? null
1530
- : (gitSummaryByDirectoryId.get(activeDirectoryId)?.branch ?? null);
1531
- const trackedBranchForActions = resolveGitHubTrackedBranchForActions({
1532
- projectTrackedBranch: githubProjectPrState?.branchName ?? null,
1533
- currentBranch: currentBranchForActions,
1534
- });
1535
- const selectedText =
1536
- workspace.selection === null
1537
- ? ''
1538
- : workspace.selection.text.length > 0
1539
- ? workspace.selection.text
1540
- : activeConversation === null
1541
- ? ''
1542
- : selectionText(activeConversation.oracle.snapshotWithoutHash(), workspace.selection);
1543
- return {
1544
- activeDirectoryId,
1545
- activeConversationId: conversationManager.activeConversationId,
1546
- selectedText,
1547
- leftNavSelectionKind: workspace.leftNavSelection.kind,
1548
- profileRunning: hasActiveProfileState(
1549
- resolveProfileStatePath(options.invocationDirectory, muxSessionName),
1550
- ),
1551
- statusTimelineRunning: existsSync(
1552
- resolveStatusTimelineStatePath(options.invocationDirectory, muxSessionName),
1553
- ),
1554
- githubRepositoryId,
1555
- githubRepositoryUrl: activeDirectoryRepositorySnapshot?.normalizedRemoteUrl ?? null,
1556
- githubDefaultBranch: resolveGitHubDefaultBranchForActions({
1557
- repositoryDefaultBranch: githubRepositoryRecord?.defaultBranch ?? null,
1558
- snapshotDefaultBranch: activeDirectoryRepositorySnapshot?.defaultBranch ?? null,
1559
- }),
1560
- githubTrackedBranch: trackedBranchForActions,
1561
- githubOpenPrUrl: githubProjectPrState?.openPrUrl ?? null,
1562
- githubProjectPrLoading: githubProjectPrState?.loading ?? false,
1563
- };
1564
- };
1565
- const resolveVisibleCommandMenuActions = (
1566
- context: RuntimeCommandMenuContext,
1567
- ): readonly RegisteredCommandMenuAction<RuntimeCommandMenuContext>[] => {
1568
- const actions = commandMenuRegistry.resolveActions(context);
1569
- if (workspace.commandMenu?.scope === 'thread-start') {
1570
- return actions.filter(
1571
- (action) =>
1572
- action.id.startsWith('thread.start.') || action.id.startsWith('thread.install.'),
1573
- );
1574
- }
1575
- return filterThemePresetActionsForScope(
1576
- actions,
1577
- workspace.commandMenu?.scope ?? 'all',
1578
- THEME_ACTION_ID_PREFIX,
1579
- );
1580
- };
1581
- const resolveCommandMenuActions = (): readonly CommandMenuActionDescriptor[] => {
1582
- return resolveVisibleCommandMenuActions(commandMenuContext()).map((action) => ({
1583
- id: action.id,
1584
- title: action.title,
1585
- ...(action.aliases === undefined
1586
- ? {}
1587
- : {
1588
- aliases: action.aliases,
1589
- }),
1590
- ...(action.keywords === undefined
1591
- ? {}
1592
- : {
1593
- keywords: action.keywords,
1594
- }),
1595
- ...(action.detail === undefined
1596
- ? {}
1597
- : {
1598
- detail: action.detail,
1599
- }),
1600
- }));
1601
- };
1602
- const executeCommandMenuAction = (actionId: string): void => {
1603
- const context = commandMenuContext({
1604
- preferThreadScope: isThreadScopedCommandActionId(actionId),
1605
- });
1606
- const action =
1607
- resolveVisibleCommandMenuActions(context).find((candidate) => candidate.id === actionId) ??
1608
- null;
1609
- if (action === null) {
1610
- return;
1611
- }
1612
- void Promise.resolve(action.run(context)).catch((error: unknown) => {
1613
- const message = formatErrorMessage(error);
1614
- workspace.taskPaneNotice = `command menu failed: ${message}`;
1615
- debugFooterNotice.set(`command menu failed: ${message}`);
1616
- markDirty();
1617
- });
1618
- };
1619
- const createModalManager = (): ModalManager =>
1620
- new ModalManager({
1621
- theme: currentModalTheme,
1622
- resolveRepositoryName: (repositoryId) => repositories.get(repositoryId)?.name ?? null,
1623
- getCommandMenu: () => workspace.commandMenu,
1624
- resolveCommandMenuActions,
1625
- getNewThreadPrompt: () => workspace.newThreadPrompt,
1626
- getAddDirectoryPrompt: () => workspace.addDirectoryPrompt,
1627
- getApiKeyPrompt: () => workspace.apiKeyPrompt,
1628
- getTaskEditorPrompt: () => workspace.taskEditorPrompt,
1629
- getRepositoryPrompt: () => workspace.repositoryPrompt,
1630
- getConversationTitleEdit: () => workspace.conversationTitleEdit,
1631
- });
1632
- let modalManager = createModalManager();
1633
- let homePaneBackgroundTimer: ReturnType<typeof setInterval> | null = null;
1634
- const ptySizeByConversationId = new Map<string, { cols: number; rows: number }>();
1635
-
1636
- const requestStop = (): void => {
1637
- requestStopFn({
1638
- stop,
1639
- hasConversationTitleEdit: workspace.conversationTitleEdit !== null,
1640
- stopConversationTitleEdit: () => stopConversationTitleEdit(true),
1641
- activeTaskEditorTaskId:
1642
- 'taskId' in workspace.taskEditorTarget &&
1643
- typeof workspace.taskEditorTarget.taskId === 'string'
1644
- ? workspace.taskEditorTarget.taskId
1645
- : null,
1646
- autosaveTaskIds: [...taskManager.autosaveTaskIds()],
1647
- flushTaskComposerPersist,
1648
- closeLiveSessionsOnClientStop,
1649
- orderedConversationIds: conversationManager.orderedIds(),
1650
- conversations: conversationRecords,
1651
- queueControlPlaneOp,
1652
- sendSignal: (sessionId, signal) => {
1653
- streamClient.sendSignal(sessionId, signal);
1654
- },
1655
- closeSession: async (sessionId) => {
1656
- await controlPlaneService.closePtySession(sessionId);
1657
- },
1658
- markDirty,
1659
- setStop: (next) => {
1660
- stop = next;
1661
- },
1662
- });
1663
- };
1664
-
1665
- const runtimeRenderLifecycle = new RuntimeRenderLifecycle({
1666
- screen,
1667
- render: () => {
1668
- render();
1669
- },
1670
- isShuttingDown: () => shuttingDown,
1671
- setShuttingDown: (next) => {
1672
- shuttingDown = next;
1673
- },
1674
- setStop: (next) => {
1675
- stop = next;
1676
- },
1677
- restoreTerminalState: () => {
1678
- restoreTerminalState(true, inputModeManager.restore);
1679
- },
1680
- formatErrorMessage,
1681
- writeStderr: (text) => process.stderr.write(text),
1682
- exitProcess: (code) => {
1683
- process.exit(code);
1684
- },
1685
- });
1686
- const handleRuntimeFatal = (origin: string, error: unknown): void => {
1687
- runtimeRenderLifecycle.handleRuntimeFatal(origin, error);
1688
- };
1689
- const scheduleRender = (): void => {
1690
- runtimeRenderLifecycle.scheduleRender();
1691
- };
1692
- const markDirty = (): void => {
1693
- runtimeRenderLifecycle.markDirty();
1694
- };
1695
- const controlPlaneOps = new RuntimeControlPlaneOps({
1696
- onFatal: (error: unknown) => {
1697
- handleRuntimeFatal('control-plane-pump', error);
1698
- },
1699
- startPerfSpan,
1700
- recordPerfEvent,
1701
- writeStderr: (text) => process.stderr.write(text),
1702
- });
1703
- const waitForControlPlaneDrain = async (): Promise<void> => {
1704
- await controlPlaneOps.waitForDrain();
1705
- };
1706
- const queueControlPlaneOp = (task: () => Promise<void>, label = 'interactive-op'): void => {
1707
- controlPlaneOps.enqueueInteractive(task, label);
1708
- };
1709
- const queueBackgroundControlPlaneOp = (
1710
- task: () => Promise<void>,
1711
- label = 'background-op',
1712
- ): void => {
1713
- controlPlaneOps.enqueueBackground(task, label);
1714
- };
1715
- const commandMenuAgentTools = new RuntimeCommandMenuAgentTools({
1716
- sendCommand: async (command) => await streamClient.sendCommand(command),
1717
- queueControlPlaneOp,
1718
- getCommandMenu: () => workspace.commandMenu,
1719
- markDirty,
1720
- });
1721
- const setCommandNotice = (message: string): void => {
1722
- workspace.taskPaneNotice = message;
1723
- debugFooterNotice.set(message);
1724
- markDirty();
1725
- };
1726
- const buildPresetThemeConfig = (preset: string): HarnessMuxThemeConfig => {
1727
- return {
1728
- preset,
1729
- mode: runtimeThemeConfig?.mode ?? 'dark',
1730
- customThemePath: null,
1731
- };
1732
- };
1733
- const applyThemeConfig = (nextThemeConfig: HarnessMuxThemeConfig | null): void => {
1734
- const resolved = resolveAndApplyRuntimeTheme(nextThemeConfig);
1735
- currentModalTheme = resolved.theme.modalTheme;
1736
- modalManager = createModalManager();
1737
- };
1738
- const persistThemeConfig = (nextThemeConfig: HarnessMuxThemeConfig): string | null => {
1739
- if (loadedConfig.error !== null) {
1740
- return 'config currently using last-known-good due to parse error';
1741
- }
1742
- try {
1743
- const updated = updateHarnessConfig({
1744
- filePath: loadedConfig.filePath,
1745
- update: (current) => {
1746
- return {
1747
- ...current,
1748
- mux: {
1749
- ...current.mux,
1750
- ui: {
1751
- ...current.mux.ui,
1752
- theme: nextThemeConfig,
1753
- },
1754
- },
1755
- };
1756
- },
1757
- });
1758
- runtimeThemeConfig = updated.mux.ui.theme;
1759
- return null;
1760
- } catch (error: unknown) {
1761
- return formatErrorMessage(error);
1762
- }
1763
- };
1764
- const applyThemePreset = (preset: string, persist: boolean): void => {
1765
- const nextThemeConfig = buildPresetThemeConfig(preset);
1766
- applyThemeConfig(nextThemeConfig);
1767
- if (!persist) {
1768
- markDirty();
1769
- return;
1770
- }
1771
- const persistError = persistThemeConfig(nextThemeConfig);
1772
- if (persistError === null) {
1773
- setCommandNotice(`theme set to ${preset}`);
1774
- return;
1775
- }
1776
- setCommandNotice(`theme set to ${preset} (not persisted: ${persistError})`);
1777
- };
1778
- const themePresetFromActionId = (actionId: string | null): string | null => {
1779
- if (actionId === null || !actionId.startsWith(THEME_ACTION_ID_PREFIX)) {
1780
- return null;
1781
- }
1782
- const preset = actionId.slice(THEME_ACTION_ID_PREFIX.length).trim();
1783
- return preset.length > 0 ? preset : null;
1784
- };
1785
- const selectedCommandMenuActionId = (): string | null => {
1786
- return resolveSelectedCommandMenuActionId(resolveCommandMenuActions(), workspace.commandMenu);
1787
- };
1788
- const startThemePickerSession = (): void => {
1789
- const initialThemeConfig =
1790
- runtimeThemeConfig === null
1791
- ? null
1792
- : {
1793
- preset: runtimeThemeConfig.preset,
1794
- mode: runtimeThemeConfig.mode,
1795
- customThemePath: runtimeThemeConfig.customThemePath,
1796
- };
1797
- themePickerSession = {
1798
- initialThemeConfig,
1799
- committed: false,
1800
- previewActionId: null,
1801
- };
1802
- commandMenuScopedDirectoryId = null;
1803
- commandMenuGitHubProjectPrState = null;
1804
- workspace.commandMenu = createCommandMenuState({
1805
- scope: THEME_PICKER_SCOPE,
1806
- });
1807
- markDirty();
1808
- };
1809
- const syncThemePickerPreview = (): void => {
1810
- if (themePickerSession === null) {
1811
- return;
1812
- }
1813
- if (workspace.commandMenu?.scope !== THEME_PICKER_SCOPE) {
1814
- if (!themePickerSession.committed) {
1815
- applyThemeConfig(themePickerSession.initialThemeConfig);
1816
- }
1817
- themePickerSession = null;
1818
- return;
1819
- }
1820
- const selectedActionId = selectedCommandMenuActionId();
1821
- if (selectedActionId === themePickerSession.previewActionId) {
1822
- return;
1823
- }
1824
- themePickerSession.previewActionId = selectedActionId;
1825
- const preset = themePresetFromActionId(selectedActionId);
1826
- if (preset === null) {
1827
- return;
1828
- }
1829
- applyThemePreset(preset, false);
1830
- };
1831
- const githubAuthHintNotice =
1832
- 'GitHub PR actions become available after auth (`gh auth login` or `GITHUB_TOKEN`).';
1833
- const setGitHubDebugAuthState = (
1834
- update: Partial<Pick<GitHubDebugAuthState, 'token' | 'auth' | 'projectPr'>>,
1835
- ): void => {
1836
- githubDebugAuthState.token = update.token ?? githubDebugAuthState.token;
1837
- githubDebugAuthState.auth = update.auth ?? githubDebugAuthState.auth;
1838
- githubDebugAuthState.projectPr = update.projectPr ?? githubDebugAuthState.projectPr;
1839
- };
1840
- const isGitHubAuthUnavailableError = (error: unknown): boolean => {
1841
- const message = formatErrorMessage(error).toLowerCase();
1842
- return (
1843
- message.includes('github token not configured') ||
1844
- message.includes('github integration is disabled')
1845
- );
1846
- };
1847
- const refreshCommandMenuGitHubProjectPrState = (directoryId: string): void => {
1848
- commandMenuGitHubProjectPrState = {
1849
- directoryId,
1850
- branchName: null,
1851
- openPrUrl: null,
1852
- loading: true,
1853
- };
1854
- markDirty();
1855
- queueControlPlaneOp(async () => {
1856
- try {
1857
- const result = await streamClient.sendCommand({
1858
- type: 'github.project-pr',
1859
- directoryId,
1860
- });
1861
- commandMenuGitHubProjectPrState = parseGitHubProjectPrState(directoryId, result);
1862
- setGitHubDebugAuthState({
1863
- projectPr: 'ok',
1864
- });
1865
- } catch (error: unknown) {
1866
- setGitHubDebugAuthState({
1867
- projectPr: 'er',
1868
- auth: isGitHubAuthUnavailableError(error) ? 'no' : githubDebugAuthState.auth,
1869
- });
1870
- commandMenuGitHubProjectPrState = {
1871
- directoryId,
1872
- branchName: null,
1873
- openPrUrl: null,
1874
- loading: false,
1875
- };
1876
- }
1877
- if (workspace.commandMenu !== null) {
1878
- markDirty();
1879
- }
1880
- }, 'command-menu-github-project-pr');
1881
- };
1882
- const processUsageRefreshService = new ProcessUsageRefreshService<
1883
- ConversationState,
1884
- ProcessUsageSample
1885
- >({
1886
- readProcessUsageSample,
1887
- processIdForConversation: (conversation) => conversation.processId,
1888
- processUsageEqual: (left, right) =>
1889
- left.cpuPercent === right.cpuPercent && left.memoryMb === right.memoryMb,
1890
- startPerfSpan,
1891
- onChanged: markDirty,
1892
- });
1893
- const sessionProjectionInstrumentation = new SessionProjectionInstrumentation({
1894
- getProcessUsageSample: (sessionId) => processUsageRefreshService.getSample(sessionId),
1895
- recordPerfEvent,
1896
- onTransition: (transition) => {
1897
- recordStatusTimeline({
1898
- direction: 'outgoing',
1899
- source: 'session-projection',
1900
- eventType: 'projection-transition',
1901
- labels: resolveTraceLabels({
1902
- sessionId: transition.sessionId,
1903
- directoryId: null,
1904
- conversationId: transition.sessionId,
1905
- }),
1906
- payload: transition,
1907
- });
1908
- },
1909
- });
1910
- sessionProjectionInstrumentation.refreshSelectorSnapshot(
1911
- 'startup',
1912
- directoryRecords,
1913
- conversationRecords,
1914
- conversationManager.orderedIds(),
1915
- );
1916
-
1917
- keyEventSubscription = await subscribeControlPlaneKeyEvents(streamClient, {
1918
- tenantId: options.scope.tenantId,
1919
- userId: options.scope.userId,
1920
- workspaceId: options.scope.workspaceId,
1921
- ...(startupObservedCursor === null
1922
- ? {}
1923
- : {
1924
- afterCursor: startupObservedCursor,
1925
- }),
1926
- onEvent: (event) => {
1927
- recordStatusTimeline({
1928
- direction: 'incoming',
1929
- source: 'control-plane-key-events',
1930
- eventType: event.type,
1931
- labels: resolveTraceLabels({
1932
- sessionId: event.sessionId,
1933
- directoryId: event.directoryId,
1934
- conversationId: event.conversationId,
1935
- }),
1936
- payload: event,
1937
- });
1938
- applyControlPlaneKeyEvent(event);
1939
- markDirty();
1940
- },
1941
- });
1942
-
1943
- const muxUiStatePersistence = new MuxUiStatePersistence({
1944
- enabled: loadedConfig.error === null,
1945
- initialState: {
1946
- paneWidthPercent: paneWidthPercentFromLayout(layout),
1947
- repositoriesCollapsed: configuredMuxUi.repositoriesCollapsed,
1948
- shortcutsCollapsed: configuredMuxUi.shortcutsCollapsed,
1949
- },
1950
- debounceMs: UI_STATE_PERSIST_DEBOUNCE_MS,
1951
- persistState: (pending) => {
1952
- const updated = updateHarnessMuxUiConfig(pending, {
1953
- filePath: loadedConfig.filePath,
1954
- });
1955
- return {
1956
- paneWidthPercent:
1957
- updated.mux.ui.paneWidthPercent === null
1958
- ? paneWidthPercentFromLayout(layout)
1959
- : updated.mux.ui.paneWidthPercent,
1960
- repositoriesCollapsed: updated.mux.ui.repositoriesCollapsed,
1961
- shortcutsCollapsed: updated.mux.ui.shortcutsCollapsed,
1962
- };
1963
- },
1964
- applyState: (state) => {
1965
- workspace.repositoriesCollapsed = state.repositoriesCollapsed;
1966
- workspace.shortcutsCollapsed = state.shortcutsCollapsed;
1967
- },
1968
- writeStderr: (text) => process.stderr.write(text),
1969
- });
1970
- const persistMuxUiStateNow = (): void => {
1971
- muxUiStatePersistence.persistNow();
1972
- };
1973
- const queuePersistMuxUiState = (): void => {
1974
- muxUiStatePersistence.queue({
1975
- paneWidthPercent: paneWidthPercentFromLayout(layout),
1976
- repositoriesCollapsed: workspace.repositoriesCollapsed,
1977
- shortcutsCollapsed: workspace.shortcutsCollapsed,
1978
- });
1979
- };
1980
-
1981
- if (configuredMuxGit.enabled) {
1982
- syncGitStateWithDirectories();
1983
- if (workspace.activeDirectoryId !== null) {
1984
- noteGitActivity(workspace.activeDirectoryId);
1985
- }
1986
- } else {
1987
- recordPerfEvent('mux.background.git-summary.skipped', {
1988
- reason: 'disabled',
1989
- });
1990
- }
1991
- startupOrchestrator.startBackgroundProbe();
1992
-
1993
- const eventPersistence = new EventPersistence({
1994
- appendEvents: (events) => store.appendEvents(events),
1995
- startPerfSpan,
1996
- writeStderr: (text) => process.stderr.write(text),
1997
- });
1998
- const outputLoadSampler = new OutputLoadSampler({
1999
- recordPerfEvent,
2000
- getControlPlaneQueueMetrics: () => controlPlaneOps.metrics(),
2001
- getActiveConversationId: () => conversationManager.activeConversationId,
2002
- getPendingPersistedEvents: () => eventPersistence.pendingCount(),
2003
- onStatusRowChanged: markDirty,
2004
- });
2005
- outputLoadSampler.start();
2006
- homePaneBackgroundTimer = setInterval(() => {
2007
- if (shuttingDown || workspace.mainPaneMode !== 'home') {
2008
- return;
2009
- }
2010
- markDirty();
2011
- }, HOME_PANE_BACKGROUND_INTERVAL_MS);
2012
- homePaneBackgroundTimer.unref?.();
2013
-
2014
- const runtimeLayoutResize = new RuntimeLayoutResize<ConversationState>({
2015
- getSize: () => size,
2016
- setSize: (nextSize) => {
2017
- size = nextSize;
2018
- },
2019
- getLayout: () => layout,
2020
- setLayout: (nextLayout) => {
2021
- layout = nextLayout;
2022
- },
2023
- getLeftPaneColsOverride: () => leftPaneColsOverride,
2024
- setLeftPaneColsOverride: (nextLeftPaneColsOverride) => {
2025
- leftPaneColsOverride = nextLeftPaneColsOverride;
2026
- },
2027
- conversationManager,
2028
- ptySizeByConversationId,
2029
- sendResize: (sessionId, cols, rows) => {
2030
- streamClient.sendResize(sessionId, cols, rows);
2031
- },
2032
- markDirty,
2033
- resetFrameCache: () => {
2034
- screen.resetFrameCache();
2035
- },
2036
- resizeRecordingOracle: (nextLayout) => {
2037
- if (muxRecordingOracle !== null) {
2038
- muxRecordingOracle.resize(nextLayout.cols, nextLayout.rows);
2039
- }
2040
- },
2041
- queuePersistMuxUiState,
2042
- resizeMinIntervalMs,
2043
- ptyResizeSettleMs,
2044
- });
2045
-
2046
- const schedulePtyResize = (ptySize: { cols: number; rows: number }, immediate = false): void => {
2047
- runtimeLayoutResize.schedulePtyResize(ptySize, immediate);
2048
- };
2049
-
2050
- const applyLayout = (
2051
- nextSize: { cols: number; rows: number },
2052
- forceImmediatePtyResize = false,
2053
- ): void => {
2054
- runtimeLayoutResize.applyLayout(nextSize, forceImmediatePtyResize);
2055
- };
2056
-
2057
- const queueResize = (nextSize: { cols: number; rows: number }): void => {
2058
- runtimeLayoutResize.queueResize(nextSize);
2059
- };
2060
-
2061
- const applyPaneDividerAtCol = (col: number): void => {
2062
- runtimeLayoutResize.applyPaneDividerAtCol(col);
2063
- };
2064
-
2065
- const scheduleConversationTitlePersist = (): void => {
2066
- conversationLifecycle.scheduleConversationTitlePersist();
2067
- };
2068
-
2069
- const stopConversationTitleEdit = (persistPending: boolean): void => {
2070
- conversationLifecycle.stopConversationTitleEdit(persistPending);
2071
- };
2072
-
2073
- const beginConversationTitleEdit = (conversationId: string): void => {
2074
- conversationLifecycle.beginConversationTitleEdit(conversationId);
2075
- };
2076
-
2077
- const buildNewThreadModalOverlay = (viewportRows: number) => {
2078
- return modalManager.buildNewThreadOverlay(layout.cols, viewportRows);
2079
- };
2080
-
2081
- const buildCommandMenuModalOverlay = (viewportRows: number) => {
2082
- return modalManager.buildCommandMenuOverlay(layout.cols, viewportRows);
2083
- };
2084
-
2085
- const buildConversationTitleModalOverlay = (viewportRows: number) => {
2086
- return modalManager.buildConversationTitleOverlay(layout.cols, viewportRows);
2087
- };
2088
-
2089
- const buildCurrentModalOverlay = () => {
2090
- return modalManager.buildCurrentOverlay(layout.cols, layout.rows);
2091
- };
2092
-
2093
- const dismissModalOnOutsideClick = (
2094
- input: Buffer,
2095
- dismiss: () => void,
2096
- onInsidePointerPress?: (col: number, row: number) => boolean,
2097
- ): boolean => {
2098
- const result = modalManager.dismissOnOutsideClick({
2099
- input,
2100
- inputRemainder,
2101
- layoutCols: layout.cols,
2102
- viewportRows: layout.rows,
2103
- dismiss,
2104
- ...(onInsidePointerPress === undefined
2105
- ? {}
2106
- : {
2107
- onInsidePointerPress,
2108
- }),
2109
- });
2110
- inputRemainder = result.inputRemainder;
2111
- return result.handled;
2112
- };
2113
-
2114
- const attachConversation = async (sessionId: string): Promise<void> => {
2115
- const attachResult = await conversationManager.attachIfLive({
2116
- sessionId,
2117
- attach: async (sinceCursor) => {
2118
- await controlPlaneService.attachPty({
2119
- sessionId,
2120
- sinceCursor,
2121
- });
2122
- },
2123
- });
2124
- if (attachResult.attached && attachResult.sinceCursor !== null) {
2125
- recordPerfEvent('mux.conversation.attach', {
2126
- sessionId,
2127
- sinceCursor: attachResult.sinceCursor,
2128
- });
2129
- }
2130
- };
2131
-
2132
- const detachConversation = async (sessionId: string): Promise<void> => {
2133
- const detachResult = await conversationManager.detachIfAttached({
2134
- sessionId,
2135
- detach: async () => {
2136
- await controlPlaneService.detachPty(sessionId);
2137
- },
2138
- });
2139
- if (detachResult.detached && detachResult.conversation !== null) {
2140
- recordPerfEvent('mux.conversation.detach', {
2141
- sessionId,
2142
- lastOutputCursor: detachResult.conversation.lastOutputCursor,
2143
- });
2144
- }
2145
- };
2146
-
2147
- const refreshProjectPaneSnapshot = (directoryId: string): void => {
2148
- const directory = directoryManager.getDirectory(directoryId);
2149
- if (directory === undefined) {
2150
- workspace.projectPaneSnapshot = null;
2151
- return;
2152
- }
2153
- workspace.projectPaneSnapshot = buildProjectPaneSnapshot(directory.directoryId, directory.path);
2154
- };
2155
-
2156
- const enterProjectPane = (directoryId: string): void => {
2157
- if (!directoryManager.hasDirectory(directoryId)) {
2158
- return;
2159
- }
2160
- workspace.enterProjectPane(directoryId, repositoryGroupIdForDirectory(directoryId));
2161
- noteGitActivity(directoryId);
2162
- refreshProjectPaneSnapshot(directoryId);
2163
- screen.resetFrameCache();
2164
- };
2165
-
2166
- function orderedTaskRecords(): readonly ControlPlaneTaskRecord[] {
2167
- return taskManager.orderedTasks(sortTasksByOrder);
2168
- }
2169
-
2170
- function orderedActiveRepositoryRecords(): readonly ControlPlaneRepositoryRecord[] {
2171
- return sortedRepositoryList(repositories);
2172
- }
2173
-
2174
- const selectedRepositoryTaskRecords = (): readonly ControlPlaneTaskRecord[] => {
2175
- return taskManager.tasksForRepository({
2176
- repositoryId: workspace.taskPaneSelectedRepositoryId,
2177
- sortTasks: sortTasksByOrder,
2178
- taskRepositoryId: (task) => task.repositoryId,
2179
- });
2180
- };
2181
-
2182
- const applyTaskRecord = (task: ControlPlaneTaskRecord): ControlPlaneTaskRecord => {
2183
- taskManager.setTask(task);
2184
- workspace.taskPaneSelectedTaskId = task.taskId;
2185
- if (task.repositoryId !== null && repositories.has(task.repositoryId)) {
2186
- workspace.taskPaneSelectedRepositoryId = task.repositoryId;
2187
- }
2188
- workspace.taskPaneSelectionFocus = 'task';
2189
- syncTaskPaneSelection();
2190
- markDirty();
2191
- return task;
2192
- };
2193
-
2194
- const taskComposerPersistence = new RuntimeTaskComposerPersistenceService<
2195
- ControlPlaneTaskRecord,
2196
- TaskComposerBuffer
2197
- >({
2198
- getTask: (taskId) => taskManager.getTask(taskId),
2199
- getTaskComposer: (taskId) => taskManager.getTaskComposer(taskId),
2200
- setTaskComposer: (taskId, buffer) => {
2201
- taskManager.setTaskComposer(taskId, buffer);
2202
- },
2203
- deleteTaskComposer: (taskId) => {
2204
- taskManager.deleteTaskComposer(taskId);
2205
- },
2206
- getTaskAutosaveTimer: (taskId) => taskManager.getTaskAutosaveTimer(taskId),
2207
- setTaskAutosaveTimer: (taskId, timer) => {
2208
- taskManager.setTaskAutosaveTimer(taskId, timer);
2209
- },
2210
- deleteTaskAutosaveTimer: (taskId) => {
2211
- taskManager.deleteTaskAutosaveTimer(taskId);
2212
- },
2213
- buildComposerFromTask: (task) =>
2214
- createTaskComposerBuffer(
2215
- task.description.length === 0 ? task.title : `${task.title}\n${task.description}`,
2216
- ),
2217
- normalizeTaskComposerBuffer,
2218
- taskFieldsFromComposerText,
2219
- updateTask: async (input) => {
2220
- return await controlPlaneService.updateTask(input);
2221
- },
2222
- applyTaskRecord: (task) => {
2223
- applyTaskRecord(task);
2224
- },
2225
- queueControlPlaneOp,
2226
- setTaskPaneNotice: (text) => {
2227
- workspace.taskPaneNotice = text;
2228
- },
2229
- markDirty,
2230
- autosaveDebounceMs: DEFAULT_TASK_EDITOR_AUTOSAVE_DEBOUNCE_MS,
2231
- });
2232
-
2233
- const taskComposerForTask = (taskId: string): TaskComposerBuffer | null => {
2234
- return taskComposerPersistence.taskComposerForTask(taskId);
2235
- };
2236
-
2237
- const setTaskComposerForTask = (taskId: string, buffer: TaskComposerBuffer): void => {
2238
- taskComposerPersistence.setTaskComposerForTask(taskId, buffer);
2239
- };
2240
-
2241
- const clearTaskAutosaveTimer = (taskId: string): void => {
2242
- taskComposerPersistence.clearTaskAutosaveTimer(taskId);
2243
- };
2244
-
2245
- const scheduleTaskComposerPersist = (taskId: string): void => {
2246
- taskComposerPersistence.scheduleTaskComposerPersist(taskId);
2247
- };
2248
-
2249
- const flushTaskComposerPersist = (taskId: string): void => {
2250
- taskComposerPersistence.flushTaskComposerPersist(taskId);
2251
- };
2252
-
2253
- const activeRepositoryIds = (): readonly string[] => {
2254
- return orderedActiveRepositoryRecords().map((repository) => repository.repositoryId);
2255
- };
2256
-
2257
- const taskPaneSelectionActions = new TaskPaneSelectionActions<ControlPlaneTaskRecord>({
2258
- workspace,
2259
- taskRecordById: (taskId) => taskManager.getTask(taskId),
2260
- hasTask: (taskId) => taskManager.hasTask(taskId),
2261
- hasRepository: (repositoryId) => repositories.has(repositoryId),
2262
- repositoryById: (repositoryId) => repositories.get(repositoryId),
2263
- selectedRepositoryTasks: selectedRepositoryTaskRecords,
2264
- activeRepositoryIds,
2265
- flushTaskComposerPersist,
2266
- markDirty,
2267
- });
2268
-
2269
- const syncTaskPaneSelection = (): void => {
2270
- taskPaneSelectionActions.syncTaskPaneSelection();
2271
- };
2272
-
2273
- const syncTaskPaneRepositorySelection = (): void => {
2274
- taskPaneSelectionActions.syncTaskPaneRepositorySelection();
2275
- };
2276
-
2277
- const focusDraftComposer = (): void => {
2278
- taskPaneSelectionActions.focusDraftComposer();
2279
- };
2280
-
2281
- const focusTaskComposer = (taskId: string): void => {
2282
- taskPaneSelectionActions.focusTaskComposer(taskId);
2283
- };
2284
-
2285
- const selectedTaskRecord = (): ControlPlaneTaskRecord | null => {
2286
- if (workspace.taskPaneSelectedTaskId === null) {
2287
- return null;
2288
- }
2289
- return taskManager.getTask(workspace.taskPaneSelectedTaskId) ?? null;
2290
- };
2291
-
2292
- const selectTaskById = (taskId: string): void => {
2293
- taskPaneSelectionActions.selectTaskById(taskId);
2294
- };
2295
-
2296
- const selectRepositoryById = (repositoryId: string): void => {
2297
- taskPaneSelectionActions.selectRepositoryById(repositoryId);
2298
- };
2299
-
2300
- const enterHomePane = (): void => {
2301
- workspace.enterHomePane();
2302
- workspace.selection = null;
2303
- workspace.selectionDrag = null;
2304
- releaseViewportPinForSelection();
2305
- syncTaskPaneSelection();
2306
- syncTaskPaneRepositorySelection();
2307
- screen.resetFrameCache();
2308
- markDirty();
2309
- };
2310
-
2311
- const taskPlanningHydrationService = new TaskPlanningHydrationService<
2312
- ControlPlaneRepositoryRecord,
2313
- ControlPlaneTaskRecord
2314
- >({
2315
- controlPlaneService,
2316
- clearRepositories: () => {
2317
- repositories.clear();
2318
- },
2319
- setRepository: (repository) => {
2320
- repositories.set(repository.repositoryId, repository);
2321
- },
2322
- syncTaskPaneRepositorySelection,
2323
- clearTasks: () => {
2324
- taskManager.clearTasks();
2325
- },
2326
- setTask: (task) => {
2327
- taskManager.setTask(task);
2328
- },
2329
- syncTaskPaneSelection,
2330
- markDirty,
2331
- taskLimit: 1000,
2332
- });
2333
-
2334
- async function hydrateTaskPlanningState(): Promise<void> {
2335
- await taskPlanningHydrationService.hydrate();
2336
- }
2337
-
2338
- const taskPlanningObservedEvents = new TaskPlanningObservedEvents<
2339
- ControlPlaneRepositoryRecord,
2340
- ControlPlaneTaskRecord
2341
- >({
2342
- parseRepositoryRecord,
2343
- parseTaskRecord,
2344
- getRepository: (repositoryId) => repositories.get(repositoryId),
2345
- setRepository: (repositoryId, repository) => {
2346
- repositories.set(repositoryId, repository);
2347
- },
2348
- setTask: (task) => {
2349
- taskManager.setTask(task);
2350
- },
2351
- deleteTask: (taskId) => taskManager.deleteTask(taskId),
2352
- syncTaskPaneRepositorySelection,
2353
- syncTaskPaneSelection,
2354
- markDirty,
2355
- });
2356
-
2357
- const applyObservedTaskPlanningEvent = (observed: StreamObservedEvent): void => {
2358
- taskPlanningObservedEvents.apply(observed);
2359
- };
2360
-
2361
- const workspaceObservedEvents = new WorkspaceObservedEvents<
2362
- ControlPlaneDirectoryRecord,
2363
- ControlPlaneConversationRecord
2364
- >({
2365
- parseDirectoryRecord,
2366
- parseConversationRecord,
2367
- setDirectory: (directoryId, directory) => {
2368
- directoryManager.setDirectory(directoryId, directory);
2369
- },
2370
- deleteDirectory: (directoryId) => {
2371
- if (!directoryManager.hasDirectory(directoryId)) {
2372
- return false;
2373
- }
2374
- directoryManager.deleteDirectory(directoryId);
2375
- return true;
2376
- },
2377
- deleteDirectoryGitState,
2378
- syncGitStateWithDirectories,
2379
- upsertConversationFromPersistedRecord: (record) => {
2380
- conversationManager.upsertFromPersistedRecord({
2381
- record,
2382
- ensureConversation,
2383
- });
2384
- },
2385
- removeConversation: (sessionId) => {
2386
- if (!conversationManager.has(sessionId)) {
2387
- return false;
2388
- }
2389
- removeConversationState(sessionId);
2390
- return true;
2391
- },
2392
- orderedConversationIds: () => conversationManager.orderedIds(),
2393
- conversationDirectoryId: (sessionId) => conversationManager.directoryIdOf(sessionId),
2394
- });
2395
-
2396
- const runtimeWorkspaceObservedEvents = new RuntimeWorkspaceObservedEvents<StreamObservedEvent>({
2397
- reducer: workspaceObservedEvents,
2398
- workspace,
2399
- orderedConversationIds: () => conversationManager.orderedIds(),
2400
- conversationDirectoryId: (sessionId) => conversationManager.directoryIdOf(sessionId),
2401
- hasConversation: (sessionId) => conversationManager.has(sessionId),
2402
- getActiveConversationId: () => conversationManager.activeConversationId,
2403
- setActiveConversationId: (sessionId) => {
2404
- conversationManager.setActiveConversationId(sessionId);
2405
- },
2406
- hasDirectory: (directoryId) => directoryManager.hasDirectory(directoryId),
2407
- resolveActiveDirectoryId,
2408
- unsubscribeConversationEvents: async (sessionId) => {
2409
- await conversationLifecycle.unsubscribeConversationEvents(sessionId);
2410
- },
2411
- stopConversationTitleEdit: (persistPending) => {
2412
- stopConversationTitleEdit(persistPending);
2413
- },
2414
- enterProjectPane,
2415
- enterHomePane,
2416
- queueControlPlaneOp,
2417
- activateConversation: async (sessionId) => {
2418
- await conversationLifecycle.activateConversation(sessionId);
2419
- },
2420
- markDirty,
2421
- });
2422
-
2423
- const applyObservedWorkspaceEvent = (observed: StreamObservedEvent): void => {
2424
- runtimeWorkspaceObservedEvents.apply(observed);
2425
- };
2426
-
2427
- activateConversationForStartupOrchestrator = async (sessionId: string): Promise<void> => {
2428
- await conversationLifecycle.activateConversation(sessionId);
2429
- };
2430
-
2431
- const removeConversationState = (sessionId: string): void => {
2432
- if (workspace.conversationTitleEdit?.conversationId === sessionId) {
2433
- stopConversationTitleEdit(false);
2434
- }
2435
- conversationManager.remove(sessionId);
2436
- ptySizeByConversationId.delete(sessionId);
2437
- processUsageRefreshService.deleteSession(sessionId);
2438
- };
2439
-
2440
- const openNewThreadPrompt = (directoryId: string): void => {
2441
- if (!directoryManager.hasDirectory(directoryId)) {
2442
- return;
2443
- }
2444
- workspace.newThreadPrompt = null;
2445
- workspace.addDirectoryPrompt = null;
2446
- workspace.apiKeyPrompt = null;
2447
- workspace.taskEditorPrompt = null;
2448
- workspace.repositoryPrompt = null;
2449
- if (workspace.conversationTitleEdit !== null) {
2450
- stopConversationTitleEdit(true);
2451
- }
2452
- workspace.conversationTitleEditClickState = null;
2453
- commandMenuGitHubProjectPrState = null;
2454
- commandMenuScopedDirectoryId = directoryId;
2455
- workspace.commandMenu = createCommandMenuState({
2456
- scope: 'thread-start',
2457
- });
2458
- commandMenuAgentTools.refresh();
2459
- markDirty();
2460
- };
2461
-
2462
- const runtimeRepositoryActions = new RuntimeRepositoryActions<ControlPlaneRepositoryRecord>({
2463
- workspace,
2464
- repositories,
2465
- controlPlaneService,
2466
- normalizeGitHubRemoteUrl,
2467
- repositoryNameFromGitHubRemoteUrl,
2468
- createRepositoryId: () => `repository-${randomUUID()}`,
2469
- stopConversationTitleEdit: () => {
2470
- stopConversationTitleEdit(true);
2471
- },
2472
- syncRepositoryAssociationsWithDirectorySnapshots,
2473
- syncTaskPaneRepositorySelection,
2474
- queueControlPlaneOp,
2475
- markDirty,
2476
- });
2477
-
2478
- const runtimeTaskPane = new RuntimeTaskPane<ControlPlaneTaskRecord>({
2479
- actions: {
2480
- workspace,
2481
- controlPlaneService,
2482
- repositoriesHas: (repositoryId) => repositories.has(repositoryId),
2483
- setTask: (task) => {
2484
- taskManager.setTask(task);
2485
- },
2486
- getTask: (taskId) => taskManager.getTask(taskId),
2487
- taskReorderPayloadIds: (orderedActiveTaskIds) =>
2488
- taskManager.taskReorderPayloadIds({
2489
- orderedActiveTaskIds,
2490
- sortTasks: sortTasksByOrder,
2491
- isCompleted: (task) => task.status === 'completed',
2492
- }),
2493
- reorderedActiveTaskIdsForDrop: (draggedTaskId, targetTaskId) =>
2494
- taskManager.reorderedActiveTaskIdsForDrop({
2495
- draggedTaskId,
2496
- targetTaskId,
2497
- sortTasks: sortTasksByOrder,
2498
- isCompleted: (task) => task.status === 'completed',
2499
- }),
2500
- clearTaskAutosaveTimer,
2501
- deleteTask: (taskId) => {
2502
- taskManager.deleteTask(taskId);
2503
- },
2504
- deleteTaskComposer: (taskId) => {
2505
- taskManager.deleteTaskComposer(taskId);
2506
- },
2507
- focusDraftComposer,
2508
- focusTaskComposer,
2509
- selectedTask: () => selectedTaskRecord(),
2510
- orderedTaskRecords,
2511
- queueControlPlaneOp,
2512
- syncTaskPaneSelection,
2513
- syncTaskPaneRepositorySelection,
2514
- openRepositoryPromptForCreate: () => {
2515
- runtimeRepositoryActions.openRepositoryPromptForCreate();
2516
- },
2517
- openRepositoryPromptForEdit: (repositoryId) => {
2518
- runtimeRepositoryActions.openRepositoryPromptForEdit(repositoryId);
2519
- },
2520
- archiveRepositoryById: async (repositoryId) => {
2521
- await runtimeRepositoryActions.archiveRepositoryById(repositoryId);
2522
- },
2523
- markDirty,
2524
- },
2525
- shortcuts: {
2526
- workspace,
2527
- taskScreenKeybindings,
2528
- repositoriesHas: (repositoryId) => repositories.has(repositoryId),
2529
- activeRepositoryIds,
2530
- selectRepositoryById,
2531
- taskComposerForTask,
2532
- setTaskComposerForTask,
2533
- scheduleTaskComposerPersist,
2534
- selectedRepositoryTaskRecords,
2535
- focusTaskComposer,
2536
- focusDraftComposer,
2537
- queueControlPlaneOp,
2538
- createTask: async (payload) => {
2539
- return await controlPlaneService.createTask(payload);
2540
- },
2541
- syncTaskPaneSelection,
2542
- markDirty,
2543
- },
2544
- });
2545
- const runtimeTaskEditorActions = new RuntimeTaskEditorActions<ControlPlaneTaskRecord>({
2546
- workspace,
2547
- controlPlaneService,
2548
- applyTaskRecord: (task) => runtimeTaskPane.applyTaskRecord(task),
2549
- queueControlPlaneOp,
2550
- markDirty,
2551
- });
2552
-
2553
- const runtimeDirectoryActions = new RuntimeDirectoryActions({
2554
- controlPlaneService,
2555
- conversations: () => conversationRecords,
2556
- orderedConversationIds: () => conversationManager.orderedIds(),
2557
- conversationDirectoryId: (sessionId) => conversationManager.directoryIdOf(sessionId),
2558
- conversationLive: (sessionId) => conversationManager.isLive(sessionId),
2559
- removeConversationState,
2560
- unsubscribeConversationEvents: async (sessionId) => {
2561
- await conversationLifecycle.unsubscribeConversationEvents(sessionId);
2562
- },
2563
- activeConversationId: () => conversationManager.activeConversationId,
2564
- setActiveConversationId: (sessionId) => {
2565
- conversationManager.setActiveConversationId(sessionId);
2566
- },
2567
- activateConversation: async (sessionId) => {
2568
- await conversationLifecycle.activateConversation(sessionId);
2569
- },
2570
- resolveActiveDirectoryId,
2571
- enterProjectPane,
2572
- markDirty,
2573
- isSessionNotFoundError,
2574
- isConversationNotFoundError,
2575
- createDirectoryId: () => `directory-${randomUUID()}`,
2576
- resolveWorkspacePathForMux: (rawPath) =>
2577
- resolveWorkspacePathForMux(options.invocationDirectory, rawPath),
2578
- setDirectory: (directory) => {
2579
- directoryManager.setDirectory(directory.directoryId, directory);
2580
- },
2581
- directoryIdOf: (directory) => directory.directoryId,
2582
- setActiveDirectoryId: (directoryId) => {
2583
- workspace.activeDirectoryId = directoryId;
2584
- },
2585
- syncGitStateWithDirectories,
2586
- noteGitActivity,
2587
- hydratePersistedConversationsForDirectory,
2588
- findConversationIdByDirectory: (directoryId) =>
2589
- conversationManager.findConversationIdByDirectory(
2590
- directoryId,
2591
- conversationManager.orderedIds(),
2592
- ),
2593
- directoriesHas: (directoryId) => directoryManager.hasDirectory(directoryId),
2594
- deleteDirectory: (directoryId) => {
2595
- directoryManager.deleteDirectory(directoryId);
2596
- },
2597
- deleteDirectoryGitState,
2598
- projectPaneSnapshotDirectoryId: () => workspace.projectPaneSnapshot?.directoryId ?? null,
2599
- clearProjectPaneSnapshot: () => {
2600
- workspace.projectPaneSnapshot = null;
2601
- workspace.projectPaneScrollTop = 0;
2602
- },
2603
- directoriesSize: () => directoryManager.directoriesSize(),
2604
- invocationDirectory: options.invocationDirectory,
2605
- activeDirectoryId: () => workspace.activeDirectoryId,
2606
- firstDirectoryId: () => directoryManager.firstDirectoryId(),
2607
- });
2608
- const runtimeControlActions = new RuntimeControlActions({
2609
- conversationById: (sessionId) => conversationManager.get(sessionId),
2610
- interruptSession: async (sessionId) => {
2611
- return await controlPlaneService.interruptSession(sessionId);
2612
- },
2613
- nowIso: () => new Date().toISOString(),
2614
- markDirty,
2615
- toggleGatewayProfiler: async (input) => {
2616
- return await toggleGatewayProfilerFn(input);
2617
- },
2618
- toggleGatewayStatusTimeline: async (input) => {
2619
- return await toggleGatewayStatusTimelineFn(input);
2620
- },
2621
- toggleGatewayRenderTrace: async (input) => {
2622
- return await toggleGatewayRenderTraceFn(input);
2623
- },
2624
- invocationDirectory: options.invocationDirectory,
2625
- sessionName: muxSessionName,
2626
- setTaskPaneNotice: (message) => {
2627
- workspace.taskPaneNotice = message;
2628
- recordStatusTimeline({
2629
- direction: 'outgoing',
2630
- source: 'task-pane-notice',
2631
- eventType: 'status-notice',
2632
- labels: resolveTraceLabels({
2633
- sessionId: conversationManager.activeConversationId,
2634
- directoryId: workspace.activeDirectoryId,
2635
- conversationId: conversationManager.activeConversationId,
2636
- }),
2637
- payload: {
2638
- message,
2639
- },
2640
- });
2641
- },
2642
- setDebugFooterNotice: (message) => {
2643
- debugFooterNotice.set(message);
2644
- recordStatusTimeline({
2645
- direction: 'outgoing',
2646
- source: 'debug-footer-notice',
2647
- eventType: 'status-notice',
2648
- labels: resolveTraceLabels({
2649
- sessionId: conversationManager.activeConversationId,
2650
- directoryId: workspace.activeDirectoryId,
2651
- conversationId: conversationManager.activeConversationId,
2652
- }),
2653
- payload: {
2654
- message,
2655
- },
2656
- });
2657
- },
2658
- listConversationIdsForTitleRefresh: () => conversationManager.orderedIds(),
2659
- conversationAgentTypeForTitleRefresh: (sessionId) =>
2660
- conversationManager.get(sessionId)?.agentType ?? null,
2661
- refreshConversationTitle: async (sessionId) => {
2662
- return await controlPlaneService.refreshConversationTitle(sessionId);
2663
- },
2664
- });
2665
- const runtimeWorkspaceActions = new RuntimeWorkspaceActions({
2666
- conversationActions: conversationLifecycle,
2667
- directoryActions: runtimeDirectoryActions,
2668
- repositoryActions: runtimeRepositoryActions,
2669
- controlActions: runtimeControlActions,
2670
- taskPaneActions: runtimeTaskPane,
2671
- taskPaneShortcuts: runtimeTaskPane,
2672
- orderedActiveRepositoryIds: () =>
2673
- orderedActiveRepositoryRecords().map((repository) => repository.repositoryId),
2674
- });
2675
-
2676
- const toggleCommandMenu = (): void => {
2677
- if (workspace.commandMenu !== null) {
2678
- workspace.commandMenu = null;
2679
- commandMenuGitHubProjectPrState = null;
2680
- commandMenuScopedDirectoryId = null;
2681
- markDirty();
2682
- return;
2683
- }
2684
- workspace.newThreadPrompt = null;
2685
- workspace.addDirectoryPrompt = null;
2686
- workspace.apiKeyPrompt = null;
2687
- workspace.taskEditorPrompt = null;
2688
- workspace.repositoryPrompt = null;
2689
- if (workspace.conversationTitleEdit !== null) {
2690
- stopConversationTitleEdit(true);
2691
- }
2692
- commandMenuScopedDirectoryId = null;
2693
- workspace.commandMenu = createCommandMenuState({
2694
- scope: 'all',
2695
- });
2696
- commandMenuAgentTools.refresh();
2697
- const directoryId = resolveDirectoryForAction();
2698
- if (directoryId === null) {
2699
- commandMenuGitHubProjectPrState = null;
2700
- } else {
2701
- refreshCommandMenuGitHubProjectPrState(directoryId);
2702
- }
2703
- markDirty();
2704
- };
2705
-
2706
- const openApiKeyPrompt = (apiKey: AllowedCommandMenuApiKey): void => {
2707
- workspace.newThreadPrompt = null;
2708
- workspace.addDirectoryPrompt = null;
2709
- workspace.taskEditorPrompt = null;
2710
- workspace.repositoryPrompt = null;
2711
- if (workspace.conversationTitleEdit !== null) {
2712
- stopConversationTitleEdit(true);
2713
- }
2714
- workspace.conversationTitleEditClickState = null;
2715
- const existingRaw = sessionEnv[apiKey.envVar] ?? process.env[apiKey.envVar];
2716
- const hasExistingValue = typeof existingRaw === 'string' && existingRaw.trim().length > 0;
2717
- workspace.apiKeyPrompt = {
2718
- keyName: apiKey.envVar,
2719
- displayName: apiKey.displayName,
2720
- value: '',
2721
- error: null,
2722
- hasExistingValue,
2723
- };
2724
- markDirty();
2725
- };
2726
-
2727
- const persistApiKey = (keyName: string, value: string): void => {
2728
- const result = upsertHarnessSecret({
2729
- cwd: options.invocationDirectory,
2730
- key: keyName,
2731
- value,
2732
- });
2733
- sessionEnv[keyName] = value;
2734
- process.env[keyName] = value;
2735
- const action = result.replacedExisting ? 'updated' : 'saved';
2736
- setCommandNotice(`${keyName} ${action}`);
2737
- };
2738
-
2739
- const startThreadFromCommandMenu = (
2740
- directoryId: string,
2741
- agentType: ReturnType<typeof normalizeThreadAgentType>,
2742
- ): void => {
2743
- queueControlPlaneOp(async () => {
2744
- await runtimeWorkspaceActions.createAndActivateConversationInDirectory(
2745
- directoryId,
2746
- agentType,
2747
- );
2748
- }, `command-menu-start-thread:${agentType}`);
2749
- };
2750
-
2751
- const installAgentToolFromCommandMenu = (
2752
- directoryId: string,
2753
- agentType: InstallableAgentType,
2754
- installCommand: string,
2755
- ): void => {
2756
- queueControlPlaneOp(async () => {
2757
- await runCommandInNewTerminalThread(directoryId, installCommand);
2758
- commandMenuAgentTools.refresh();
2759
- }, `command-menu-install-agent-tool:${agentType}`);
2760
- };
2761
-
2762
- const runCommandInNewTerminalThread = async (
2763
- directoryId: string,
2764
- commandText: string,
2765
- ): Promise<void> => {
2766
- const priorSessionIds = new Set(conversationManager.orderedIds());
2767
- await runtimeWorkspaceActions.createAndActivateConversationInDirectory(directoryId, 'terminal');
2768
- const terminalSessionId =
2769
- conversationManager.orderedIds().find((sessionId) => !priorSessionIds.has(sessionId)) ??
2770
- conversationManager.activeConversationId;
2771
- if (terminalSessionId === null) {
2772
- throw new Error('failed to locate terminal session for command');
2773
- }
2774
- await controlPlaneService.respondToSession(terminalSessionId, `${commandText}\n`);
2775
- };
2776
-
2777
- const resolveCritiqueReviewAgentFromEnvironment = (): 'claude' | 'opencode' | null => {
2778
- const claudeAvailable =
2779
- commandMenuAgentTools.statusForAgent('claude')?.available === true ||
2780
- commandExistsOnPath('claude');
2781
- const opencodeAvailable = commandExistsOnPath('opencode');
2782
- return resolveCritiqueReviewAgent({
2783
- claudeAvailable,
2784
- opencodeAvailable,
2785
- });
2786
- };
2787
-
2788
- const resolveCritiqueReviewBaseBranchForDirectory = async (
2789
- directoryId: string,
2790
- ): Promise<string> => {
2791
- const directory = directoryManager.getDirectory(directoryId);
2792
- if (directory === undefined || directory === null) {
2793
- return 'main';
2794
- }
2795
- return await resolveCritiqueReviewBaseBranch(directory.path, runGitCommand);
2796
- };
2797
-
2798
- const runCritiqueReviewFromCommandMenu = (
2799
- directoryId: string,
2800
- mode: 'unstaged' | 'staged' | 'base-branch',
2801
- ): void => {
2802
- queueControlPlaneOp(async () => {
2803
- const agent = resolveCritiqueReviewAgentFromEnvironment();
2804
- if (mode !== 'base-branch') {
2805
- const commandText = buildCritiqueReviewCommand({
2806
- mode,
2807
- agent,
2808
- });
2809
- await runCommandInNewTerminalThread(directoryId, commandText);
2810
- const reviewLabelByMode: Readonly<Record<'unstaged' | 'staged', string>> = {
2811
- unstaged: 'unstaged',
2812
- staged: 'staged',
2813
- };
2814
- setCommandNotice(
2815
- `running critique ${reviewLabelByMode[mode]} review (${agent ?? 'default'})`,
2816
- );
2817
- return;
2818
- }
2819
- const baseBranch = await resolveCritiqueReviewBaseBranchForDirectory(directoryId);
2820
- const commandText = buildCritiqueReviewCommand({
2821
- mode: 'base-branch',
2822
- baseBranch,
2823
- agent,
2824
- });
2825
- await runCommandInNewTerminalThread(directoryId, commandText);
2826
- setCommandNotice(`running critique review vs ${baseBranch} (${agent ?? 'default'})`);
2827
- }, `command-menu-critique-review:${mode}`);
2828
- };
2829
-
2830
- commandMenuRegistry.registerProvider('critique.review', (context) => {
2831
- const directoryId = context.activeDirectoryId;
2832
- if (directoryId === null) {
2833
- return [];
2834
- }
2835
- const critiqueStatus = commandMenuAgentTools.statusForAgent('critique');
2836
- if (critiqueStatus !== null && !critiqueStatus.available) {
2837
- return [];
2838
- }
2839
- return [
2840
- {
2841
- id: 'critique.review.unstaged',
2842
- title: 'Critique AI Review: Unstaged Changes (git)',
2843
- aliases: ['critique unstaged review', 'review unstaged diff', 'ai review unstaged'],
2844
- keywords: ['critique', 'review', 'unstaged', 'diff', 'ai'],
2845
- detail: 'runs critique review',
2846
- run: () => {
2847
- runCritiqueReviewFromCommandMenu(directoryId, 'unstaged');
2848
- },
2849
- },
2850
- {
2851
- id: 'critique.review.staged',
2852
- title: 'Critique AI Review: Staged Changes (git)',
2853
- aliases: ['critique staged review', 'review staged diff', 'ai review staged'],
2854
- keywords: ['critique', 'review', 'staged', 'diff', 'ai'],
2855
- detail: 'runs critique review --staged',
2856
- run: () => {
2857
- runCritiqueReviewFromCommandMenu(directoryId, 'staged');
2858
- },
2859
- },
2860
- {
2861
- id: 'critique.review.base-branch',
2862
- title: 'Critique AI Review: Current Branch vs Base (git)',
2863
- aliases: ['critique base review', 'review against base branch', 'ai review base'],
2864
- keywords: ['critique', 'review', 'base', 'branch', 'diff', 'ai'],
2865
- detail: 'runs critique review <base> HEAD',
2866
- run: () => {
2867
- runCritiqueReviewFromCommandMenu(directoryId, 'base-branch');
2868
- },
2869
- },
2870
- ];
2871
- });
2872
-
2873
- commandMenuRegistry.registerProvider('thread.start', (context) => {
2874
- const directoryId = context.activeDirectoryId;
2875
- if (directoryId === null) {
2876
- return [];
2877
- }
2878
- const actions: RegisteredCommandMenuAction<RuntimeCommandMenuContext>[] = [
2879
- {
2880
- id: 'thread.start.codex',
2881
- title: 'Start Codex thread',
2882
- aliases: ['codex', 'start codex'],
2883
- keywords: ['start', 'thread', 'codex', 'new'],
2884
- run: () => {
2885
- startThreadFromCommandMenu(directoryId, 'codex');
2886
- },
2887
- },
2888
- {
2889
- id: 'thread.start.claude',
2890
- title: 'Start Claude thread',
2891
- aliases: ['claude', 'start claude'],
2892
- keywords: ['start', 'thread', 'claude', 'new'],
2893
- run: () => {
2894
- startThreadFromCommandMenu(directoryId, 'claude');
2895
- },
2896
- },
2897
- {
2898
- id: 'thread.start.cursor',
2899
- title: 'Start Cursor thread',
2900
- aliases: ['cursor', 'cur', 'start cursor'],
2901
- keywords: ['start', 'thread', 'cursor', 'new'],
2902
- run: () => {
2903
- startThreadFromCommandMenu(directoryId, 'cursor');
2904
- },
2905
- },
2906
- {
2907
- id: 'thread.start.terminal',
2908
- title: 'Start Terminal thread',
2909
- aliases: ['terminal', 'shell', 'start terminal'],
2910
- keywords: ['start', 'thread', 'terminal', 'shell', 'new'],
2911
- run: () => {
2912
- startThreadFromCommandMenu(directoryId, 'terminal');
2913
- },
2914
- },
2915
- {
2916
- id: 'thread.start.critique',
2917
- title: 'Start Critique thread (diff)',
2918
- aliases: ['critique', 'start critique', 'critique diff'],
2919
- keywords: ['start', 'thread', 'critique', 'diff', 'new'],
2920
- run: () => {
2921
- startThreadFromCommandMenu(directoryId, 'critique');
2922
- },
2923
- },
2924
- ];
2925
- const installableByAgent: Readonly<
2926
- Record<InstallableAgentType, { startId: string; installId: string; installTitle: string }>
2927
- > = {
2928
- codex: {
2929
- startId: 'thread.start.codex',
2930
- installId: 'thread.install.codex',
2931
- installTitle: 'Install Codex CLI',
2932
- },
2933
- claude: {
2934
- startId: 'thread.start.claude',
2935
- installId: 'thread.install.claude',
2936
- installTitle: 'Install Claude CLI',
2937
- },
2938
- cursor: {
2939
- startId: 'thread.start.cursor',
2940
- installId: 'thread.install.cursor',
2941
- installTitle: 'Install Cursor CLI',
2942
- },
2943
- critique: {
2944
- startId: 'thread.start.critique',
2945
- installId: 'thread.install.critique',
2946
- installTitle: 'Install Critique CLI',
2947
- },
2948
- };
2949
- const adjusted: RegisteredCommandMenuAction<RuntimeCommandMenuContext>[] = [];
2950
- for (const action of actions) {
2951
- adjusted.push(action);
2952
- }
2953
- for (const agentType of ['codex', 'claude', 'cursor', 'critique'] as const) {
2954
- const status = commandMenuAgentTools.statusForAgent(agentType);
2955
- if (status === null || status.available || status.installCommand === null) {
2956
- continue;
2957
- }
2958
- const installCommand = status.installCommand;
2959
- const mapping = installableByAgent[agentType];
2960
- const startIndex = adjusted.findIndex((action) => action.id === mapping.startId);
2961
- if (startIndex < 0) {
2962
- continue;
2963
- }
2964
- adjusted.splice(startIndex, 1, {
2965
- id: mapping.installId,
2966
- title: mapping.installTitle,
2967
- aliases: [`install ${agentType}`, `${agentType} install`, `setup ${agentType}`],
2968
- keywords: ['install', 'thread', agentType, 'setup'],
2969
- detail: installCommand,
2970
- run: () => {
2971
- installAgentToolFromCommandMenu(directoryId, agentType, installCommand);
2972
- },
2973
- });
2974
- }
2975
- return adjusted;
2976
- });
2977
-
2978
- commandMenuRegistry.registerAction({
2979
- id: 'thread.close.active',
2980
- title: 'Close active thread',
2981
- aliases: ['close thread', 'archive thread'],
2982
- keywords: ['close', 'thread', 'archive'],
2983
- when: (context) => context.activeConversationId !== null,
2984
- run: async (context) => {
2985
- const conversationId = context.activeConversationId;
2986
- if (conversationId === null) {
2987
- return;
2988
- }
2989
- queueControlPlaneOp(async () => {
2990
- await runtimeWorkspaceActions.archiveConversation(conversationId);
2991
- }, 'command-menu-close-thread');
2992
- },
2993
- });
2994
-
2995
- commandMenuRegistry.registerAction({
2996
- id: 'theme.choose',
2997
- title: 'Set a Theme',
2998
- aliases: ['theme', 'change theme', 'set theme'],
2999
- keywords: ['theme', 'appearance', 'colors'],
3000
- run: () => {
3001
- startThemePickerSession();
3002
- },
3003
- });
3004
-
3005
- commandMenuRegistry.registerProvider('api-key.set', () => {
3006
- return COMMAND_MENU_ALLOWED_API_KEYS.map(
3007
- (apiKey): RegisteredCommandMenuAction<RuntimeCommandMenuContext> => ({
3008
- id: `${API_KEY_ACTION_ID_PREFIX}${apiKey.actionIdSuffix}`,
3009
- title: `Set ${apiKey.displayName}`,
3010
- aliases: [...apiKey.aliases],
3011
- keywords: ['api', 'key', 'set', apiKey.envVar.toLowerCase()],
3012
- detail: apiKey.envVar,
3013
- run: () => {
3014
- openApiKeyPrompt(apiKey);
3015
- },
3016
- }),
3017
- );
3018
- });
3019
-
3020
- commandMenuRegistry.registerProvider('theme.set', () => {
3021
- const selectedThemeName = getActiveMuxTheme().name;
3022
- return muxThemePresetNames().map(
3023
- (preset): RegisteredCommandMenuAction<RuntimeCommandMenuContext> => ({
3024
- id: `${THEME_ACTION_ID_PREFIX}${preset}`,
3025
- title: preset,
3026
- aliases: [preset, `theme ${preset}`],
3027
- keywords: ['theme', 'preset', preset],
3028
- ...(selectedThemeName === preset ||
3029
- (preset === 'default' && selectedThemeName === 'legacy-default')
3030
- ? {
3031
- detail: 'current',
3032
- }
3033
- : {}),
3034
- run: () => {
3035
- if (themePickerSession !== null) {
3036
- themePickerSession.committed = true;
3037
- }
3038
- applyThemePreset(preset, true);
3039
- },
3040
- }),
3041
- );
3042
- });
3043
-
3044
- commandMenuRegistry.registerProvider('project.open', () => {
3045
- return [...directoryRecords.values()].map(
3046
- (directory): RegisteredCommandMenuAction<RuntimeCommandMenuContext> => ({
3047
- id: `project.open.${directory.directoryId}`,
3048
- title: `Project ${commandMenuProjectPathTail(directory.path)}`,
3049
- aliases: [
3050
- 'go to project',
3051
- 'project',
3052
- directory.path,
3053
- commandMenuProjectPathTail(directory.path),
3054
- ],
3055
- keywords: ['project', 'open', 'go'],
3056
- run: () => {
3057
- enterProjectPane(directory.directoryId);
3058
- markDirty();
3059
- },
3060
- }),
3061
- );
3062
- });
3063
-
3064
- commandMenuRegistry.registerProvider('github.repo.open', (context) => {
3065
- const repositoryUrl = context.githubRepositoryUrl;
3066
- if (repositoryUrl === null) {
3067
- return [];
3068
- }
3069
- const actions: RegisteredCommandMenuAction<RuntimeCommandMenuContext>[] = [
3070
- {
3071
- id: 'github.repo.open',
3072
- title: 'Open GitHub for This Repo (git)',
3073
- aliases: ['open github for this repo', 'open github repo', 'open repository on github'],
3074
- keywords: ['github', 'repository', 'repo', 'open'],
3075
- detail: repositoryUrl,
3076
- run: () => {
3077
- const opened = openUrlInBrowser(repositoryUrl);
3078
- setCommandNotice(
3079
- opened
3080
- ? 'opened github repository in browser'
3081
- : `open github repository: ${repositoryUrl}`,
3082
- );
3083
- },
3084
- },
3085
- ];
3086
- if (context.githubRepositoryId !== null) {
3087
- const repositoryId = context.githubRepositoryId;
3088
- actions.push({
3089
- id: 'github.repo.my-prs.open',
3090
- title: 'Show My Open Pull Requests (git)',
3091
- aliases: ['show my open pull requests', 'my open pull requests', 'show my prs', 'my prs'],
3092
- keywords: ['github', 'pr', 'pull-request', 'open', 'my'],
3093
- detail: repositoryUrl,
3094
- run: async () => {
3095
- queueControlPlaneOp(async () => {
3096
- const result = await streamClient.sendCommand({
3097
- type: 'github.repo-my-prs-url',
3098
- repositoryId,
3099
- });
3100
- const parsedResult = asRecord(result);
3101
- if (parsedResult === null) {
3102
- setCommandNotice('github my open pull requests url unavailable');
3103
- return;
3104
- }
3105
- const myPrsUrl = parseGitHubUrl(parsedResult);
3106
- if (myPrsUrl === null) {
3107
- setCommandNotice('github my open pull requests url unavailable');
3108
- return;
3109
- }
3110
- const opened = openUrlInBrowser(myPrsUrl);
3111
- setCommandNotice(
3112
- opened
3113
- ? 'opened my open pull requests in browser'
3114
- : `open pull requests: ${myPrsUrl}`,
3115
- );
3116
- }, 'command-menu-open-my-open-prs');
3117
- },
3118
- });
3119
- }
3120
- return actions;
3121
- });
3122
-
3123
- commandMenuRegistry.registerProvider('github.project-pr', (context) => {
3124
- const directoryId = context.activeDirectoryId;
3125
- if (directoryId === null || context.githubProjectPrLoading) {
3126
- return [];
3127
- }
3128
- const showPrActions = shouldShowGitHubPrActions({
3129
- trackedBranch: context.githubTrackedBranch,
3130
- defaultBranch: context.githubDefaultBranch,
3131
- });
3132
- if (!showPrActions) {
3133
- return [];
3134
- }
3135
- if (context.githubOpenPrUrl !== null) {
3136
- return [
3137
- {
3138
- id: 'github.pr.open',
3139
- title: 'Open PR (git)',
3140
- aliases: ['open pull request', 'open pr'],
3141
- keywords: ['github', 'pr', 'open', 'pull-request'],
3142
- detail: context.githubTrackedBranch ?? 'current project',
3143
- run: async () => {
3144
- queueControlPlaneOp(async () => {
3145
- let result: unknown;
3146
- try {
3147
- result = await streamClient.sendCommand({
3148
- type: 'github.project-pr',
3149
- directoryId,
3150
- });
3151
- } catch (error: unknown) {
3152
- if (isGitHubAuthUnavailableError(error)) {
3153
- setGitHubDebugAuthState({
3154
- auth: 'no',
3155
- projectPr: 'er',
3156
- });
3157
- setCommandNotice(githubAuthHintNotice);
3158
- return;
3159
- }
3160
- setGitHubDebugAuthState({
3161
- auth: 'er',
3162
- projectPr: 'er',
3163
- });
3164
- throw error;
3165
- }
3166
- const parsedResult = asRecord(result);
3167
- if (parsedResult === null) {
3168
- setCommandNotice('github project PR state unavailable');
3169
- return;
3170
- }
3171
- const state = parseGitHubProjectPrState(directoryId, parsedResult);
3172
- setGitHubDebugAuthState({
3173
- projectPr: 'ok',
3174
- });
3175
- commandMenuGitHubProjectPrState = state;
3176
- if (state.openPrUrl === null) {
3177
- setCommandNotice('no open pull request for tracked branch');
3178
- return;
3179
- }
3180
- const opened = openUrlInBrowser(state.openPrUrl);
3181
- setCommandNotice(
3182
- opened ? 'opened pull request in browser' : `open pull request: ${state.openPrUrl}`,
3183
- );
3184
- }, 'command-menu-open-pr');
3185
- },
3186
- },
3187
- ];
3188
- }
3189
- if (context.githubTrackedBranch !== null) {
3190
- return [
3191
- {
3192
- id: 'github.pr.create',
3193
- title: 'Create PR (git)',
3194
- aliases: ['create pull request', 'new pr'],
3195
- keywords: ['github', 'pr', 'create', 'pull-request'],
3196
- detail: context.githubTrackedBranch,
3197
- run: async () => {
3198
- queueControlPlaneOp(async () => {
3199
- let result: unknown;
3200
- try {
3201
- result = await streamClient.sendCommand({
3202
- type: 'github.pr-create',
3203
- directoryId,
3204
- });
3205
- } catch (error: unknown) {
3206
- if (isGitHubAuthUnavailableError(error)) {
3207
- setGitHubDebugAuthState({
3208
- auth: 'no',
3209
- });
3210
- setCommandNotice(githubAuthHintNotice);
3211
- return;
3212
- }
3213
- setGitHubDebugAuthState({
3214
- auth: 'er',
3215
- });
3216
- throw error;
3217
- }
3218
- const parsedResult = asRecord(result);
3219
- if (parsedResult === null) {
3220
- setCommandNotice('github PR creation result unavailable');
3221
- return;
3222
- }
3223
- const prUrl = parseGitHubPrUrl(parsedResult);
3224
- if (prUrl === null) {
3225
- throw new Error('github.pr-create returned malformed pr url');
3226
- }
3227
- setGitHubDebugAuthState({
3228
- auth: 'ok',
3229
- projectPr: 'ok',
3230
- });
3231
- refreshCommandMenuGitHubProjectPrState(directoryId);
3232
- const opened = openUrlInBrowser(prUrl);
3233
- setCommandNotice(
3234
- opened ? 'opened pull request in browser' : `open pull request: ${prUrl}`,
3235
- );
3236
- }, 'command-menu-create-pr');
3237
- },
3238
- },
3239
- ];
3240
- }
3241
- return [];
3242
- });
3243
-
3244
- commandMenuRegistry.registerProvider('profile.toggle', (context) => {
3245
- const title = context.profileRunning ? 'Stop profiler' : 'Start profiler';
3246
- const aliases = context.profileRunning
3247
- ? ['stop profile', 'stop profiler']
3248
- : ['start profile', 'start profiler'];
3249
- return [
3250
- {
3251
- id: context.profileRunning ? 'profile.stop' : 'profile.start',
3252
- title,
3253
- aliases,
3254
- keywords: ['profile', 'profiler'],
3255
- run: async () => {
3256
- queueControlPlaneOp(async () => {
3257
- await runtimeWorkspaceActions.toggleGatewayProfiler();
3258
- }, 'command-menu-toggle-profile');
3259
- },
3260
- },
3261
- ];
3262
- });
3263
-
3264
- commandMenuRegistry.registerProvider('status-timeline.toggle', (context) => {
3265
- const title = context.statusTimelineRunning ? 'Stop status logging' : 'Start status logging';
3266
- const aliases = context.statusTimelineRunning
3267
- ? ['stop status logging', 'stop status']
3268
- : ['start status logging', 'start status'];
3269
- return [
3270
- {
3271
- id: context.statusTimelineRunning ? 'status.stop' : 'status.start',
3272
- title,
3273
- aliases,
3274
- keywords: ['status', 'timeline', 'logging'],
3275
- run: async () => {
3276
- queueControlPlaneOp(async () => {
3277
- await runtimeWorkspaceActions.toggleGatewayStatusTimeline();
3278
- }, 'command-menu-toggle-status-timeline');
3279
- },
3280
- },
3281
- ];
3282
- });
3283
-
3284
- commandMenuRegistry.registerAction({
3285
- id: 'app.quit',
3286
- title: 'Quit',
3287
- aliases: ['quit app', 'exit'],
3288
- keywords: ['quit', 'shutdown', 'exit'],
3289
- run: () => {
3290
- requestStop();
3291
- },
3292
- });
3293
-
3294
- const pinViewportForSelection = (): void => {
3295
- if (workspace.selectionPinnedFollowOutput !== null) {
3296
- return;
3297
- }
3298
- const active = conversationManager.getActiveConversation();
3299
- if (active === null) {
3300
- return;
3301
- }
3302
- const follow = active.oracle.snapshotWithoutHash().viewport.followOutput;
3303
- workspace.selectionPinnedFollowOutput = follow;
3304
- if (follow) {
3305
- active.oracle.setFollowOutput(false);
3306
- }
3307
- };
3308
-
3309
- const releaseViewportPinForSelection = (): void => {
3310
- if (workspace.selectionPinnedFollowOutput === null) {
3311
- return;
3312
- }
3313
- const shouldRepin = workspace.selectionPinnedFollowOutput;
3314
- workspace.selectionPinnedFollowOutput = null;
3315
- if (shouldRepin) {
3316
- const active = conversationManager.getActiveConversation();
3317
- if (active === null) {
3318
- return;
3319
- }
3320
- active.oracle.setFollowOutput(true);
3321
- }
3322
- };
3323
-
3324
- const runtimeRenderPipeline = new RuntimeRenderPipeline<
3325
- ConversationState,
3326
- ControlPlaneRepositoryRecord,
3327
- ControlPlaneTaskRecord,
3328
- ControlPlaneDirectoryRecord,
3329
- GitRepositorySnapshot,
3330
- GitSummary,
3331
- ProcessUsageSample,
3332
- ReturnType<typeof resolveMuxShortcutBindings>,
3333
- ReturnType<typeof buildWorkspaceRailViewRows>,
3334
- NonNullable<ReturnType<typeof buildCurrentModalOverlay>>,
3335
- ReturnType<OutputLoadSampler['currentStatusRow']>
3336
- >({
3337
- renderFlush: {
3338
- perfNowNs,
3339
- statusFooterForConversation: (conversation) =>
3340
- `${formatGitHubDebugTokens(githubDebugAuthState)} ${debugFooterForConversation(conversation)}`,
3341
- currentStatusNotice: () => debugFooterNotice.current(),
3342
- currentStatusRow: () => outputLoadSampler.currentStatusRow(),
3343
- onStatusLineComposed: (input) => {
3344
- const activeConversationId =
3345
- input.activeConversation === null ? null : input.activeConversation.sessionId;
3346
- const payload = {
3347
- statusFooter: input.statusFooter,
3348
- statusRow: input.statusRow,
3349
- projectPaneActive: input.projectPaneActive,
3350
- homePaneActive: input.homePaneActive,
3351
- activeConversationId,
3352
- };
3353
- recordStatusTimeline({
3354
- direction: 'outgoing',
3355
- source: 'render-status-line',
3356
- eventType: 'status-line',
3357
- labels: resolveTraceLabels({
3358
- sessionId: activeConversationId,
3359
- directoryId: workspace.activeDirectoryId,
3360
- conversationId: activeConversationId,
3361
- }),
3362
- payload,
3363
- dedupeKey: 'render-status-line',
3364
- dedupeValue: JSON.stringify(payload),
3365
- });
3366
- },
3367
- buildRenderRows: (renderLayout, railRows, rightRows, statusRow, statusFooter) =>
3368
- buildRenderRows(renderLayout, railRows, rightRows, statusRow, statusFooter),
3369
- buildModalOverlay: () => buildCurrentModalOverlay(),
3370
- applyModalOverlay: (rows, overlay) => {
3371
- applyModalOverlay(rows, overlay);
3372
- },
3373
- renderSelectionOverlay: (renderLayout, frame, renderSelection) =>
3374
- renderSelectionOverlay(renderLayout, frame, renderSelection),
3375
- flush: ({ layout: renderLayout, rows, rightFrame, selectionRows, selectionOverlay }) =>
3376
- screen.flush({
3377
- layout: renderLayout,
3378
- rows,
3379
- rightFrame,
3380
- selectionRows,
3381
- selectionOverlay,
3382
- validateAnsi,
3383
- }),
3384
- onFlushOutput: ({ activeConversation, rightFrame, rows, flushResult, changedRowCount }) => {
3385
- startupOrchestrator.onRenderFlush({
3386
- activeConversation,
3387
- activeConversationId: conversationManager.activeConversationId,
3388
- rightFrameVisible: rightFrame !== null,
3389
- changedRowCount,
3390
- });
3391
- if (muxRecordingWriter !== null && muxRecordingOracle !== null) {
3392
- const recordingCursorStyle: ScreenCursorStyle =
3393
- rightFrame === null ? { shape: 'block', blinking: false } : rightFrame.cursor.style;
3394
- const recordingCursorRow = rightFrame === null ? 0 : rightFrame.cursor.row;
3395
- const recordingCursorCol =
3396
- rightFrame === null
3397
- ? layout.rightStartCol - 1
3398
- : layout.rightStartCol + rightFrame.cursor.col - 1;
3399
- const canonicalFrame = renderCanonicalFrameAnsi(
3400
- rows,
3401
- recordingCursorStyle,
3402
- flushResult.shouldShowCursor,
3403
- recordingCursorRow,
3404
- recordingCursorCol,
3405
- );
3406
- muxRecordingOracle.ingest(canonicalFrame);
3407
- try {
3408
- muxRecordingWriter.capture(muxRecordingOracle.snapshot());
3409
- } catch {
3410
- // Recording failures must never break live interaction.
3411
- }
3412
- }
3413
- },
3414
- recordRenderSample: (durationMs, changedRowCount) => {
3415
- outputLoadSampler.recordRenderSample(durationMs, changedRowCount);
3416
- },
3417
- },
3418
- rightPaneRender: {
3419
- workspace,
3420
- repositories,
3421
- taskManager,
3422
- conversationPane,
3423
- homePane,
3424
- projectPane,
3425
- refreshProjectPaneSnapshot: (directoryId) => {
3426
- refreshProjectPaneSnapshot(directoryId);
3427
- return workspace.projectPaneSnapshot;
3428
- },
3429
- emptyTaskPaneView: () => ({
3430
- rows: [],
3431
- taskIds: [],
3432
- repositoryIds: [],
3433
- actions: [],
3434
- actionCells: [],
3435
- top: 0,
3436
- selectedRepositoryId: null,
3437
- }),
3438
- },
3439
- leftRailRender: {
3440
- leftRailPane,
3441
- sessionProjectionInstrumentation,
3442
- workspace,
3443
- repositoryManager,
3444
- repositories,
3445
- repositoryAssociationByDirectoryId,
3446
- directoryRepositorySnapshotByDirectoryId,
3447
- directories: directoryRecords,
3448
- conversations: conversationRecords,
3449
- gitSummaryByDirectoryId: gitSummaryByDirectoryId,
3450
- processUsageBySessionId: () => processUsageRefreshService.readonlyUsage(),
3451
- shortcutBindings,
3452
- loadingGitSummary: GIT_SUMMARY_LOADING,
3453
- activeConversationId: () => conversationManager.activeConversationId,
3454
- orderedConversationIds: () => conversationManager.orderedIds(),
3455
- },
3456
- renderState: {
3457
- workspace,
3458
- hasDirectory: (directoryId) => directoryManager.hasDirectory(directoryId),
3459
- activeConversationId: () => conversationManager.activeConversationId,
3460
- activeConversation: () => conversationManager.getActiveConversation(),
3461
- snapshotFrame: (conversation) => conversation.oracle.snapshotWithoutHash(),
3462
- selectionVisibleRows,
3463
- },
3464
- isScreenDirty: () => screen.isDirty(),
3465
- clearDirty: () => {
3466
- screen.clearDirty();
3467
- },
3468
- setLatestRailViewRows: (rows) => {
3469
- workspace.latestRailViewRows = rows;
3470
- },
3471
- activeDirectoryId: () => workspace.activeDirectoryId,
3472
- });
3473
-
3474
- const render = (): void => {
3475
- syncThemePickerPreview();
3476
- runtimeRenderPipeline.render({
3477
- shuttingDown,
3478
- layout,
3479
- selection: workspace.selection,
3480
- selectionDrag: workspace.selectionDrag,
3481
- });
3482
- };
3483
-
3484
- const runtimeEnvelopeHandler = new RuntimeEnvelopeHandler<
3485
- ConversationState,
3486
- ReturnType<typeof mapTerminalOutputToNormalizedEvent>
3487
- >({
3488
- perfNowNs,
3489
- isRemoved: (sessionId) => conversationManager.isRemoved(sessionId),
3490
- ensureConversation,
3491
- ingestOutputChunk: (input) => conversationManager.ingestOutputChunk(input),
3492
- noteGitActivity,
3493
- recordOutputChunk: (input) => {
3494
- outputLoadSampler.recordOutputChunk(input.sessionId, input.chunkLength, input.active);
3495
- },
3496
- startupOutputChunk: (sessionId, chunkLength) => {
3497
- startupOrchestrator.onOutputChunk(sessionId, chunkLength);
3498
- },
3499
- startupPaintOutputChunk: (sessionId) => {
3500
- startupOrchestrator.onPaintOutputChunk(sessionId);
3501
- },
3502
- recordPerfEvent,
3503
- mapTerminalOutputToNormalizedEvent: (chunk, scope, makeId) =>
3504
- mapTerminalOutputToNormalizedEvent(
3505
- chunk,
3506
- scope as Parameters<typeof mapTerminalOutputToNormalizedEvent>[1],
3507
- makeId,
3508
- ),
3509
- mapSessionEventToNormalizedEvent: (event, scope, makeId) =>
3510
- mapSessionEventToNormalizedEvent(
3511
- event as Parameters<typeof mapSessionEventToNormalizedEvent>[0],
3512
- scope as Parameters<typeof mapSessionEventToNormalizedEvent>[1],
3513
- makeId,
3514
- ),
3515
- observedAtFromSessionEvent: (event) =>
3516
- observedAtFromSessionEvent(event as Parameters<typeof observedAtFromSessionEvent>[0]),
3517
- mergeAdapterStateFromSessionEvent: (agentType, adapterState, event, observedAt) =>
3518
- mergeAdapterStateFromSessionEvent(
3519
- agentType,
3520
- adapterState,
3521
- event as Parameters<typeof mergeAdapterStateFromSessionEvent>[2],
3522
- observedAt,
3523
- ),
3524
- enqueueEvent: (event) => {
3525
- eventPersistence.enqueue(event);
3526
- },
3527
- activeConversationId: () => conversationManager.activeConversationId,
3528
- markSessionExited: (input) => {
3529
- conversationManager.markSessionExited(input);
3530
- },
3531
- deletePtySize: (sessionId) => {
3532
- ptySizeByConversationId.delete(sessionId);
3533
- },
3534
- setExit: (nextExit) => {
3535
- exit = nextExit;
3536
- },
3537
- markDirty,
3538
- nowIso: () => new Date().toISOString(),
3539
- recordOutputHandled: (durationMs) => {
3540
- outputLoadSampler.recordOutputHandled(durationMs);
3541
- },
3542
- conversationById: (sessionId) => conversationManager.get(sessionId),
3543
- applyObservedWorkspaceEvent,
3544
- applyObservedGitStatusEvent,
3545
- applyObservedTaskPlanningEvent,
3546
- idFactory,
3547
- });
3548
- const handleEnvelope = (envelope: StreamServerEnvelope): void => {
3549
- if (envelope.kind === 'pty.output') {
3550
- const conversation = conversationManager.get(envelope.sessionId);
3551
- const labels = resolveTraceLabels({
3552
- sessionId: envelope.sessionId,
3553
- directoryId: conversation?.directoryId ?? null,
3554
- conversationId: envelope.sessionId,
3555
- });
3556
- if (renderTraceRecorder.shouldCaptureConversation(labels.conversationId)) {
3557
- const chunk = Buffer.from(envelope.chunkBase64, 'base64');
3558
- const issues = findRenderTraceControlIssues(chunk);
3559
- if (issues.length > 0) {
3560
- const issueSignature = issues.map((issue) => `${issue.kind}:${issue.sequence}`).join('|');
3561
- recordRenderTrace({
3562
- direction: 'incoming',
3563
- source: 'terminal-output',
3564
- eventType: 'control-sequence-risk',
3565
- labels,
3566
- payload: {
3567
- cursor: envelope.cursor,
3568
- chunkBytes: chunk.length,
3569
- chunkPreview: renderTraceChunkPreview(chunk, 320),
3570
- issues: issues.map((issue) => ({
3571
- ...issue,
3572
- sequencePreview: renderTraceChunkPreview(issue.sequence, 160),
3573
- })),
3574
- },
3575
- dedupeKey: `render-trace-sequence:${envelope.sessionId}`,
3576
- dedupeValue: issueSignature,
3577
- });
3578
- }
3579
- }
3580
- }
3581
- if (envelope.kind !== 'pty.output') {
3582
- let eventType: string = envelope.kind;
3583
- let directoryId: string | null = null;
3584
- let conversationId: string | null = null;
3585
- if (envelope.kind === 'pty.event') {
3586
- eventType = `pty.event.${envelope.event.type}`;
3587
- } else if (envelope.kind === 'stream.event') {
3588
- eventType = `stream.event.${envelope.event.type}`;
3589
- const observedRecord = envelope.event as Record<string, unknown>;
3590
- directoryId =
3591
- typeof observedRecord['directoryId'] === 'string' ? observedRecord['directoryId'] : null;
3592
- conversationId =
3593
- typeof observedRecord['conversationId'] === 'string'
3594
- ? observedRecord['conversationId']
3595
- : null;
3596
- }
3597
- recordStatusTimeline({
3598
- direction: 'incoming',
3599
- source: 'stream-envelope',
3600
- eventType,
3601
- labels: resolveTraceLabels({
3602
- sessionId: 'sessionId' in envelope ? envelope.sessionId : null,
3603
- directoryId,
3604
- conversationId,
3605
- }),
3606
- payload: envelope,
3607
- });
3608
- }
3609
- runtimeEnvelopeHandler.handleEnvelope(envelope);
3610
- };
3611
-
3612
- const removeEnvelopeListener = streamClient.onEnvelope((envelope) => {
3613
- try {
3614
- handleEnvelope(envelope);
3615
- } catch (error: unknown) {
3616
- handleRuntimeFatal('stream-envelope', error);
3617
- }
3618
- });
3619
-
3620
- const initialActiveId = conversationManager.activeConversationId;
3621
- conversationManager.setActiveConversationId(null);
3622
- await startupOrchestrator.activateInitialConversation(initialActiveId);
3623
- startupOrchestrator.finalizeStartup(initialActiveId);
3624
-
3625
- const runtimeInputRouter = new RuntimeInputRouter({
3626
- workspace,
3627
- conversations: conversationRecords,
3628
- runtimeWorkspaceActions,
3629
- runtimeTaskEditorActions: runtimeTaskEditorActions,
3630
- detectShortcut: detectMuxGlobalShortcut,
3631
- modalDismissShortcutBindings,
3632
- shortcutBindings,
3633
- dismissOnOutsideClick: (rawInput, dismiss, onInsidePointerPress) =>
3634
- dismissModalOnOutsideClick(rawInput, dismiss, onInsidePointerPress),
3635
- buildCommandMenuModalOverlay: () => buildCommandMenuModalOverlay(layout.rows),
3636
- buildConversationTitleModalOverlay: () => buildConversationTitleModalOverlay(layout.rows),
3637
- buildNewThreadModalOverlay: () => buildNewThreadModalOverlay(layout.rows),
3638
- resolveNewThreadPromptAgentByRow,
3639
- stopConversationTitleEdit,
3640
- queueControlPlaneOp,
3641
- normalizeGitHubRemoteUrl,
3642
- repositoriesHas: (repositoryId) => repositories.has(repositoryId),
3643
- markDirty,
3644
- scheduleConversationTitlePersist,
3645
- resolveCommandMenuActions,
3646
- executeCommandMenuAction,
3647
- persistApiKey,
3648
- requestStop,
3649
- resolveDirectoryForAction,
3650
- openNewThreadPrompt,
3651
- toggleCommandMenu,
3652
- firstDirectoryForRepositoryGroup,
3653
- enterHomePane,
3654
- enterProjectPane,
3655
- queuePersistMuxUiState,
3656
- repositoryGroupIdForDirectory,
3657
- toggleRepositoryGroup,
3658
- collapseRepositoryGroup,
3659
- expandRepositoryGroup,
3660
- collapseAllRepositoryGroups,
3661
- expandAllRepositoryGroups,
3662
- directoriesHas: (directoryId) => directoryManager.hasDirectory(directoryId),
3663
- conversationDirectoryId: (sessionId) => conversationManager.directoryIdOf(sessionId),
3664
- conversationsHas: (sessionId) => conversationManager.has(sessionId),
3665
- getMainPaneMode: () => workspace.mainPaneMode,
3666
- getActiveConversationId: () => conversationManager.activeConversationId,
3667
- getActiveDirectoryId: () => workspace.activeDirectoryId,
3668
- chordTimeoutMs: REPOSITORY_TOGGLE_CHORD_TIMEOUT_MS,
3669
- collapseAllChordPrefix: REPOSITORY_COLLAPSE_ALL_CHORD_PREFIX,
3670
- releaseViewportPinForSelection,
3671
- beginConversationTitleEdit,
3672
- resetConversationPaneFrameCache: () => {
3673
- screen.resetFrameCache();
3674
- },
3675
- conversationTitleEditDoubleClickWindowMs: CONVERSATION_TITLE_EDIT_DOUBLE_CLICK_WINDOW_MS,
3676
- projectPaneActionAtRow,
3677
- queueCloseDirectory: (directoryId) =>
3678
- queueControlPlaneOp(async () => {
3679
- await runtimeWorkspaceActions.closeDirectory(directoryId);
3680
- }, 'project-pane-close-project'),
3681
- selectTaskById,
3682
- selectRepositoryById,
3683
- taskPaneActionAtCell: taskFocusedPaneActionAtCell,
3684
- taskPaneActionAtRow: taskFocusedPaneActionAtRow,
3685
- taskPaneTaskIdAtRow: taskFocusedPaneTaskIdAtRow,
3686
- taskPaneRepositoryIdAtRow: taskFocusedPaneRepositoryIdAtRow,
3687
- applyPaneDividerAtCol,
3688
- pinViewportForSelection,
3689
- homePaneEditDoubleClickWindowMs: HOME_PANE_EDIT_DOUBLE_CLICK_WINDOW_MS,
3690
- });
3691
- const runtimeInputPipeline = new RuntimeInputPipeline({
3692
- preflight: {
3693
- isShuttingDown: () => shuttingDown,
3694
- routeModalInput: (input) => runtimeInputRouter.routeModalInput(input),
3695
- handleEscapeInput: (input) => {
3696
- if (workspace.selection !== null || workspace.selectionDrag !== null) {
3697
- workspace.selection = null;
3698
- workspace.selectionDrag = null;
3699
- releaseViewportPinForSelection();
3700
- markDirty();
3701
- }
3702
- if (workspace.mainPaneMode === 'conversation') {
3703
- const escapeTarget = conversationManager.getActiveConversation();
3704
- if (escapeTarget !== null) {
3705
- streamClient.sendInput(escapeTarget.sessionId, input);
3706
- }
3707
- }
3708
- },
3709
- onFocusIn: () => {
3710
- inputModeManager.enable();
3711
- markDirty();
3712
- },
3713
- onFocusOut: () => {
3714
- markDirty();
3715
- },
3716
- handleRepositoryFoldInput: (input) => runtimeInputRouter.handleRepositoryFoldInput(input),
3717
- handleGlobalShortcutInput: (input) => runtimeInputRouter.handleGlobalShortcutInput(input),
3718
- handleTaskPaneShortcutInput: (input) =>
3719
- runtimeWorkspaceActions.handleTaskPaneShortcutInput(input),
3720
- handleCopyShortcutInput: (input) => {
3721
- if (
3722
- workspace.mainPaneMode !== 'conversation' ||
3723
- workspace.selection === null ||
3724
- !isCopyShortcutInput(input)
3725
- ) {
3726
- return false;
3727
- }
3728
- const active = conversationManager.getActiveConversation();
3729
- if (active === null) {
3730
- return true;
3731
- }
3732
- const selectedFrame = active.oracle.snapshotWithoutHash();
3733
- const copied = writeTextToClipboard(selectionText(selectedFrame, workspace.selection));
3734
- if (copied) {
3735
- markDirty();
3736
- }
3737
- return true;
3738
- },
3739
- },
3740
- forwarder: {
3741
- getInputRemainder: () => inputRemainder,
3742
- setInputRemainder: (next) => {
3743
- inputRemainder = next;
3744
- },
3745
- getMainPaneMode: () => workspace.mainPaneMode,
3746
- getLayout: () => layout,
3747
- inputTokenRouter: runtimeInputRouter.inputTokenRouter(),
3748
- getActiveConversation: () => conversationManager.getActiveConversation(),
3749
- markDirty,
3750
- isControlledByLocalHuman: (input) => conversationManager.isControlledByLocalHuman(input),
3751
- controllerId: muxControllerId,
3752
- sendInputToSession: (sessionId, chunk) => {
3753
- streamClient.sendInput(sessionId, chunk);
3754
- },
3755
- noteGitActivity,
3756
- },
3757
- });
3758
-
3759
- const onInput = (chunk: Buffer): void => {
3760
- runtimeInputPipeline.handleInput(chunk);
3761
- };
3762
-
3763
- const onResize = (): void => {
3764
- const nextSize = terminalSize();
3765
- queueResize(nextSize);
3766
- };
3767
- const runtimeProcessWiring = new RuntimeProcessWiring({
3768
- onInput,
3769
- onResize,
3770
- requestStop,
3771
- handleRuntimeFatal,
3772
- });
3773
-
3774
- await startupOrchestrator.hydrateStartupState(startupObservedCursor);
3775
-
3776
- runtimeProcessWiring.attach();
3777
-
3778
- inputModeManager.enable();
3779
- applyLayout(size, true);
3780
- scheduleRender();
3781
- const runtimeShutdownService = new RuntimeShutdownService({
3782
- screen,
3783
- outputLoadSampler,
3784
- startupBackgroundProbeService: startupOrchestrator,
3785
- clearResizeTimer: () => {
3786
- runtimeLayoutResize.clearResizeTimer();
3787
- },
3788
- clearPtyResizeTimer: () => {
3789
- runtimeLayoutResize.clearPtyResizeTimer();
3790
- },
3791
- clearHomePaneBackgroundTimer: () => {
3792
- if (homePaneBackgroundTimer !== null) {
3793
- clearInterval(homePaneBackgroundTimer);
3794
- homePaneBackgroundTimer = null;
3795
- }
3796
- },
3797
- persistMuxUiStateNow,
3798
- clearConversationTitleEditTimer: () => {
3799
- conversationLifecycle.clearConversationTitleEditTimer();
3800
- },
3801
- flushTaskComposerPersist: () => {
3802
- if (
3803
- 'taskId' in workspace.taskEditorTarget &&
3804
- typeof workspace.taskEditorTarget.taskId === 'string'
3805
- ) {
3806
- flushTaskComposerPersist(workspace.taskEditorTarget.taskId);
3807
- }
3808
- for (const taskId of taskManager.autosaveTaskIds()) {
3809
- flushTaskComposerPersist(taskId);
3810
- }
3811
- },
3812
- clearRenderScheduled: () => {
3813
- runtimeRenderLifecycle.clearRenderScheduled();
3814
- },
3815
- detachProcessListeners: () => {
3816
- runtimeProcessWiring.detach();
3817
- },
3818
- removeEnvelopeListener,
3819
- unsubscribeTaskPlanningEvents: async () => {
3820
- await conversationLifecycle.unsubscribeTaskPlanningEvents();
3821
- },
3822
- closeKeyEventSubscription: async () => {
3823
- if (keyEventSubscription !== null) {
3824
- await keyEventSubscription.close();
3825
- keyEventSubscription = null;
3826
- }
3827
- },
3828
- clearRuntimeFatalExitTimer: () => {
3829
- runtimeRenderLifecycle.clearRuntimeFatalExitTimer();
3830
- },
3831
- waitForControlPlaneDrain,
3832
- controlPlaneClient,
3833
- eventPersistence,
3834
- recordingService,
3835
- store,
3836
- restoreTerminalState: () => {
3837
- restoreTerminalState(true, inputModeManager.restore);
3838
- },
3839
- startupShutdownService: startupOrchestrator,
3840
- shutdownPerfCore,
3841
- });
3842
-
3843
- try {
3844
- while (!stop) {
3845
- await new Promise((resolve) => {
3846
- setTimeout(resolve, 50);
3847
- });
3848
- }
3849
- } finally {
3850
- shuttingDown = true;
3851
- statusTimelineRecorder.close();
3852
- renderTraceRecorder.close();
3853
- await runtimeShutdownService.finalize();
3854
- }
3855
-
3856
- if (exit === null) {
3857
- if (runtimeRenderLifecycle.hasFatal()) {
3858
- return 1;
3859
- }
3860
- return 0;
3861
- }
3862
- return normalizeExitCode(exit);
3863
- }
3864
-
3865
- try {
3866
- const code = await main();
3867
- process.exitCode = code;
3868
- } catch (error: unknown) {
3869
- shutdownPerfCore();
3870
- restoreTerminalState(true);
3871
- process.stderr.write(`codex:live:mux fatal error: ${formatErrorMessage(error)}\n`);
3872
- process.exitCode = 1;
3873
- }
3
+ await runCodexLiveMuxRuntimeProcess();