@jmoyers/harness 0.1.10 → 0.1.20

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