@jmoyers/harness 0.1.11 → 0.1.20

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (232) hide show
  1. package/README.md +31 -39
  2. package/package.json +31 -11
  3. package/packages/harness-ai/src/anthropic-protocol.ts +68 -68
  4. package/packages/harness-ai/src/stream-text.ts +13 -91
  5. package/packages/harness-ui/src/frame-primitives.ts +158 -0
  6. package/packages/harness-ui/src/index.ts +18 -0
  7. package/packages/harness-ui/src/interaction/conversation-input-forwarder.ts +221 -0
  8. package/packages/harness-ui/src/interaction/conversation-selection-input.ts +213 -0
  9. package/packages/harness-ui/src/interaction/global-shortcut-input.ts +172 -0
  10. package/{src/ui → packages/harness-ui/src/interaction}/input-preflight.ts +10 -12
  11. package/{src/ui → packages/harness-ui/src/interaction}/input-token-router.ts +120 -69
  12. package/packages/harness-ui/src/interaction/input.ts +420 -0
  13. package/packages/harness-ui/src/interaction/left-nav-input.ts +166 -0
  14. package/{src/ui → packages/harness-ui/src/interaction}/main-pane-pointer-input.ts +91 -23
  15. package/{src/ui → packages/harness-ui/src/interaction}/pointer-routing-input.ts +112 -48
  16. package/packages/harness-ui/src/interaction/rail-pointer-input.ts +62 -0
  17. package/packages/harness-ui/src/interaction/repository-fold-input.ts +118 -0
  18. package/packages/harness-ui/src/kit.ts +476 -0
  19. package/packages/harness-ui/src/layout.ts +238 -0
  20. package/packages/harness-ui/src/modal-manager.ts +222 -0
  21. package/{src/ui → packages/harness-ui/src}/screen.ts +53 -26
  22. package/packages/harness-ui/src/surface.ts +252 -0
  23. package/packages/harness-ui/src/text-layout.ts +210 -0
  24. package/packages/nim-core/src/contracts.ts +239 -0
  25. package/packages/nim-core/src/event-store.ts +299 -0
  26. package/packages/nim-core/src/events.ts +53 -0
  27. package/packages/nim-core/src/index.ts +9 -0
  28. package/packages/nim-core/src/provider-router.ts +129 -0
  29. package/packages/nim-core/src/providers/anthropic-driver.ts +291 -0
  30. package/packages/nim-core/src/runtime-factory.ts +49 -0
  31. package/packages/nim-core/src/runtime.ts +1797 -0
  32. package/packages/nim-core/src/session-store.ts +516 -0
  33. package/packages/nim-core/src/telemetry.ts +48 -0
  34. package/packages/nim-test-tui/src/index.ts +150 -0
  35. package/packages/nim-ui-core/src/index.ts +1 -0
  36. package/packages/nim-ui-core/src/projection.ts +87 -0
  37. package/scripts/codex-live-mux-runtime.ts +2 -3872
  38. package/scripts/control-plane-daemon.ts +11 -0
  39. package/scripts/harness-bin.js +5 -0
  40. package/scripts/harness-commands.ts +300 -0
  41. package/scripts/harness-runtime.ts +82 -0
  42. package/scripts/harness.ts +33 -3019
  43. package/scripts/nim-tui-smoke.ts +748 -0
  44. package/src/cli/auth/runtime.ts +948 -0
  45. package/src/cli/gateway/runtime.ts +1872 -0
  46. package/src/cli/parsing/flags.ts +23 -0
  47. package/src/cli/parsing/session.ts +42 -0
  48. package/src/cli/runtime/context.ts +193 -0
  49. package/src/cli/runtime-app/application.ts +392 -0
  50. package/src/cli/runtime-infra/gateway-control.ts +729 -0
  51. package/{scripts/harness-inspector.ts → src/cli/workflows/inspector.ts} +14 -11
  52. package/src/cli/workflows/runtime.ts +965 -0
  53. package/src/clients/tui/left-rail-interactions.ts +519 -0
  54. package/src/clients/tui/main-pane-interactions.ts +509 -0
  55. package/src/clients/tui/modal-input-routing.ts +71 -0
  56. package/src/clients/tui/render-snapshot-adapter.ts +88 -0
  57. package/src/clients/web/synced-selectors.ts +132 -0
  58. package/src/codex/live-session.ts +82 -29
  59. package/src/config/config-core.ts +348 -8
  60. package/src/config/harness.config.template.jsonc +33 -0
  61. package/src/control-plane/agent-realtime-api.ts +82 -427
  62. package/src/control-plane/session-summary.ts +10 -81
  63. package/src/control-plane/status/reducer-base.ts +12 -12
  64. package/src/control-plane/status/reducers/claude-status-reducer.ts +3 -3
  65. package/src/control-plane/status/reducers/codex-status-reducer.ts +4 -4
  66. package/src/control-plane/status/reducers/cursor-status-reducer.ts +3 -3
  67. package/src/control-plane/stream-client.ts +12 -2
  68. package/src/control-plane/stream-command-parser.ts +83 -143
  69. package/src/control-plane/stream-protocol.ts +53 -37
  70. package/src/control-plane/stream-server-command.ts +376 -69
  71. package/src/control-plane/stream-server-session-runtime.ts +3 -2
  72. package/src/control-plane/stream-server.ts +864 -70
  73. package/src/control-plane/stream-session-runtime-types.ts +41 -0
  74. package/src/{mux/live-mux/control-plane-records.ts → core/contracts/records.ts} +24 -97
  75. package/src/core/state/observed-stream-cursor.ts +43 -0
  76. package/src/core/state/synced-observed-state.ts +273 -0
  77. package/src/core/store/harness-synced-store.ts +81 -0
  78. package/src/diff/budget.ts +136 -0
  79. package/src/diff/build.ts +289 -0
  80. package/src/diff/chunker.ts +146 -0
  81. package/src/diff/git-invoke.ts +315 -0
  82. package/src/diff/git-parse.ts +472 -0
  83. package/src/diff/hash.ts +70 -0
  84. package/src/diff/index.ts +24 -0
  85. package/src/diff/normalize.ts +134 -0
  86. package/src/diff/types.ts +178 -0
  87. package/src/diff-ui/args.ts +346 -0
  88. package/src/diff-ui/commands.ts +123 -0
  89. package/src/diff-ui/finder.ts +94 -0
  90. package/src/diff-ui/highlight.ts +127 -0
  91. package/src/diff-ui/index.ts +2 -0
  92. package/src/diff-ui/model.ts +141 -0
  93. package/src/diff-ui/pager.ts +412 -0
  94. package/src/diff-ui/render.ts +337 -0
  95. package/src/diff-ui/runtime.ts +379 -0
  96. package/src/diff-ui/state.ts +224 -0
  97. package/src/diff-ui/types.ts +236 -0
  98. package/src/domain/workspace.ts +68 -5
  99. package/src/mux/control-plane-op-queue.ts +93 -7
  100. package/src/mux/conversation-rail.ts +28 -71
  101. package/src/mux/dual-pane-core.ts +13 -13
  102. package/src/mux/harness-core-ui.ts +313 -42
  103. package/src/mux/input-shortcuts.ts +13 -131
  104. package/src/mux/keybinding-catalog.ts +340 -0
  105. package/src/mux/keybinding-registry.ts +103 -0
  106. package/src/mux/live-mux/command-menu-open-in.ts +280 -0
  107. package/src/mux/live-mux/command-menu.ts +167 -4
  108. package/src/mux/live-mux/conversation-state.ts +13 -0
  109. package/src/mux/live-mux/directory-resolution.ts +1 -1
  110. package/src/mux/live-mux/git-snapshot.ts +33 -2
  111. package/src/mux/live-mux/global-shortcut-handlers.ts +6 -0
  112. package/src/mux/live-mux/home-pane-drop.ts +1 -1
  113. package/src/mux/live-mux/home-pane-pointer.ts +10 -0
  114. package/src/mux/live-mux/input-forwarding.ts +59 -2
  115. package/src/mux/live-mux/left-nav-activation.ts +124 -7
  116. package/src/mux/live-mux/left-nav.ts +35 -0
  117. package/src/mux/live-mux/link-click.ts +292 -0
  118. package/src/mux/live-mux/modal-command-menu-handler.ts +46 -9
  119. package/src/mux/live-mux/modal-conversation-handlers.ts +5 -1
  120. package/src/mux/live-mux/modal-input-reducers.ts +77 -12
  121. package/src/mux/live-mux/modal-overlays.ts +168 -34
  122. package/src/mux/live-mux/modal-pointer.ts +3 -7
  123. package/src/mux/live-mux/modal-prompt-handlers.ts +23 -2
  124. package/src/mux/live-mux/modal-release-notes-handler.ts +111 -0
  125. package/src/mux/live-mux/modal-task-editor-handler.ts +16 -11
  126. package/src/mux/live-mux/pointer-routing.ts +5 -2
  127. package/src/mux/live-mux/project-pane-pointer.ts +8 -0
  128. package/src/mux/live-mux/rail-layout.ts +33 -30
  129. package/src/mux/live-mux/release-notes.ts +383 -0
  130. package/src/mux/live-mux/render-trace-analysis.ts +52 -7
  131. package/src/mux/live-mux/repository-folding.ts +3 -0
  132. package/src/mux/live-mux/selection.ts +0 -4
  133. package/src/mux/live-mux/session-diagnostics-paths.ts +21 -0
  134. package/src/mux/project-pane-github-review.ts +271 -0
  135. package/src/mux/render-frame.ts +4 -0
  136. package/src/mux/runtime-app/codex-live-mux-runtime.ts +5191 -0
  137. package/src/mux/task-composer.ts +21 -14
  138. package/src/mux/task-focused-pane.ts +118 -117
  139. package/src/mux/task-screen-keybindings.ts +10 -101
  140. package/src/mux/workspace-rail-model.ts +270 -104
  141. package/src/mux/workspace-rail.ts +45 -22
  142. package/src/pty/session-broker.ts +1 -1
  143. package/{scripts → src/recording}/terminal-recording-gif-lib.ts +2 -2
  144. package/src/services/control-plane.ts +50 -32
  145. package/src/services/conversation-lifecycle.ts +118 -87
  146. package/src/services/conversation-startup-hydration.ts +20 -12
  147. package/src/services/directory-hydration.ts +21 -16
  148. package/src/services/event-persistence.ts +7 -0
  149. package/src/services/left-rail-pointer-handler.ts +329 -0
  150. package/src/services/mux-ui-state-persistence.ts +5 -1
  151. package/src/services/recording.ts +34 -26
  152. package/src/services/runtime-command-menu-agent-tools.ts +1 -1
  153. package/src/services/runtime-control-actions.ts +79 -61
  154. package/src/services/runtime-control-plane-ops.ts +122 -83
  155. package/src/services/runtime-conversation-actions.ts +40 -26
  156. package/src/services/runtime-conversation-activation.ts +73 -46
  157. package/src/services/runtime-conversation-starter.ts +53 -45
  158. package/src/services/runtime-conversation-title-edit.ts +91 -80
  159. package/src/services/runtime-envelope-handler.ts +107 -105
  160. package/src/services/runtime-git-state.ts +42 -29
  161. package/src/services/runtime-layout-resize.ts +3 -1
  162. package/src/services/runtime-left-rail-render.ts +99 -63
  163. package/src/services/runtime-nim-cli-session.ts +438 -0
  164. package/src/services/runtime-nim-session.ts +705 -0
  165. package/src/services/runtime-nim-tool-bridge.ts +141 -0
  166. package/src/services/runtime-observed-event-projection-pipeline.ts +45 -0
  167. package/src/services/runtime-process-wiring.ts +29 -36
  168. package/src/services/runtime-project-pane-github-review-cache.ts +164 -0
  169. package/src/services/runtime-render-flush.ts +63 -70
  170. package/src/services/runtime-render-lifecycle.ts +65 -64
  171. package/src/services/runtime-render-orchestrator.ts +55 -45
  172. package/src/services/runtime-render-pipeline.ts +106 -103
  173. package/src/services/runtime-render-state.ts +62 -49
  174. package/src/services/runtime-repository-actions.ts +97 -72
  175. package/src/services/runtime-right-pane-render.ts +80 -53
  176. package/src/services/runtime-shutdown.ts +38 -35
  177. package/src/services/runtime-stream-subscriptions.ts +35 -27
  178. package/src/services/runtime-task-composer-persistence.ts +71 -59
  179. package/src/services/runtime-task-composer-snapshot.ts +14 -0
  180. package/src/services/runtime-task-editor-actions.ts +46 -29
  181. package/src/services/runtime-task-pane-actions.ts +220 -134
  182. package/src/services/runtime-task-pane-shortcuts.ts +323 -123
  183. package/src/services/runtime-workspace-observed-effect-queue.ts +25 -0
  184. package/src/services/runtime-workspace-observed-events.ts +33 -184
  185. package/src/services/runtime-workspace-observed-transition-policy.ts +228 -0
  186. package/src/services/session-diagnostics-store.ts +217 -0
  187. package/src/services/startup-background-resume.ts +26 -21
  188. package/src/services/startup-orchestrator.ts +16 -13
  189. package/src/services/startup-paint-tracker.ts +29 -21
  190. package/src/services/startup-persisted-conversation-queue.ts +19 -13
  191. package/src/services/startup-settled-gate.ts +25 -15
  192. package/src/services/startup-shutdown.ts +18 -22
  193. package/src/services/startup-state-hydration.ts +44 -34
  194. package/src/services/startup-visibility.ts +12 -4
  195. package/src/services/task-pane-selection-actions.ts +89 -72
  196. package/src/services/task-planning-hydration.ts +24 -18
  197. package/src/services/task-planning-observed-events.ts +50 -52
  198. package/src/services/workspace-observed-events.ts +66 -63
  199. package/src/storage/storage-lifecycle-core.ts +438 -0
  200. package/src/store/control-plane-store-normalize.ts +33 -242
  201. package/src/store/control-plane-store-types.ts +1 -35
  202. package/src/store/control-plane-store.ts +360 -56
  203. package/src/store/event-store.ts +366 -8
  204. package/src/terminal/snapshot-oracle.ts +207 -94
  205. package/src/ui/mux-theme.ts +112 -8
  206. package/src/ui/panes/home-gridfire.ts +40 -31
  207. package/src/ui/panes/home.ts +10 -2
  208. package/src/ui/panes/nim.ts +315 -0
  209. package/src/mux/live-mux/actions-task.ts +0 -115
  210. package/src/mux/live-mux/left-rail-actions.ts +0 -118
  211. package/src/mux/live-mux/left-rail-conversation-click.ts +0 -85
  212. package/src/mux/live-mux/left-rail-pointer.ts +0 -74
  213. package/src/mux/live-mux/task-pane-shortcuts.ts +0 -206
  214. package/src/services/runtime-directory-actions.ts +0 -164
  215. package/src/services/runtime-input-pipeline.ts +0 -50
  216. package/src/services/runtime-input-router.ts +0 -195
  217. package/src/services/runtime-main-pane-input.ts +0 -230
  218. package/src/services/runtime-modal-input.ts +0 -137
  219. package/src/services/runtime-navigation-input.ts +0 -197
  220. package/src/services/runtime-rail-input.ts +0 -279
  221. package/src/services/runtime-task-pane.ts +0 -62
  222. package/src/services/runtime-workspace-actions.ts +0 -158
  223. package/src/ui/conversation-input-forwarder.ts +0 -114
  224. package/src/ui/conversation-selection-input.ts +0 -103
  225. package/src/ui/global-shortcut-input.ts +0 -89
  226. package/src/ui/input.ts +0 -269
  227. package/src/ui/kit.ts +0 -509
  228. package/src/ui/left-nav-input.ts +0 -80
  229. package/src/ui/left-rail-pointer-input.ts +0 -148
  230. package/src/ui/modals/manager.ts +0 -218
  231. package/src/ui/repository-fold-input.ts +0 -91
  232. package/src/ui/surface.ts +0 -224
@@ -0,0 +1,329 @@
1
+ import { detectConversationDoubleClick } from '../mux/double-click.ts';
2
+ import {
3
+ actionAtWorkspaceRailCell,
4
+ conversationIdAtWorkspaceRailRow,
5
+ kindAtWorkspaceRailRow,
6
+ projectIdAtWorkspaceRailRow,
7
+ repositoryIdAtWorkspaceRailRow,
8
+ type buildWorkspaceRailViewRows,
9
+ } from '../mux/workspace-rail-model.ts';
10
+ import type {
11
+ RailPointerHitDispatcher,
12
+ RailPointerHitResolver,
13
+ } from '../../packages/harness-ui/src/interaction/rail-pointer-input.ts';
14
+
15
+ type RailAction = ReturnType<typeof actionAtWorkspaceRailCell>;
16
+
17
+ interface ConversationTitleClickState {
18
+ readonly conversationId: string;
19
+ readonly atMs: number;
20
+ }
21
+
22
+ interface LeftRailPointerHit {
23
+ readonly selectedConversationId: string | null;
24
+ readonly selectedProjectId: string | null;
25
+ readonly selectedRepositoryId: string | null;
26
+ readonly selectedAction: RailAction;
27
+ readonly supportsConversationTitleEditClick: boolean;
28
+ }
29
+
30
+ interface LeftRailPointerState {
31
+ readonly railViewState: {
32
+ readLatestRows(): ReturnType<typeof buildWorkspaceRailViewRows>;
33
+ };
34
+ readonly conversationTitleEditConversationId: () => string | null;
35
+ readonly activeConversationId: () => string | null;
36
+ readonly repositoriesCollapsed: () => boolean;
37
+ readonly resolveDirectoryForAction: () => string | null;
38
+ readonly previousConversationClickState: () => ConversationTitleClickState | null;
39
+ readonly nowMs: () => number;
40
+ readonly isConversationPaneActive: () => boolean;
41
+ readonly directoriesHas: (directoryId: string) => boolean;
42
+ }
43
+
44
+ interface LeftRailPointerActions {
45
+ readonly clearConversationTitleEditClickState: () => void;
46
+ readonly openNewThreadPrompt: (directoryId: string) => void;
47
+ readonly queueArchiveConversation: (conversationId: string) => void;
48
+ readonly openAddDirectoryPrompt: () => void;
49
+ readonly openRepositoryPromptForCreate: () => void;
50
+ readonly repositoryExists: (repositoryId: string) => boolean;
51
+ readonly openRepositoryPromptForEdit: (repositoryId: string) => void;
52
+ readonly queueArchiveRepository: (repositoryId: string) => void;
53
+ readonly toggleRepositoryGroup: (repositoryGroupId: string) => void;
54
+ readonly selectLeftNavRepository: (repositoryGroupId: string) => void;
55
+ readonly expandAllRepositoryGroups: () => void;
56
+ readonly collapseAllRepositoryGroups: () => void;
57
+ readonly enterHomePane: () => void;
58
+ readonly enterNimPane?: () => void;
59
+ readonly enterTasksPane?: () => void;
60
+ readonly queueCloseDirectory: (directoryId: string) => void;
61
+ readonly toggleShortcutsCollapsed: () => void;
62
+ readonly setConversationClickState: (next: ConversationTitleClickState | null) => void;
63
+ readonly ensureConversationPaneActive: (conversationId: string) => void;
64
+ readonly beginConversationTitleEdit: (conversationId: string) => void;
65
+ readonly queueActivateConversation: (conversationId: string) => void;
66
+ readonly queueActivateConversationAndEdit: (conversationId: string) => void;
67
+ readonly enterProjectPane: (directoryId: string) => void;
68
+ readonly enterGitHubPane?: (directoryId: string) => void;
69
+ readonly toggleGitHubProjectExpanded?: (directoryId: string) => void;
70
+ readonly markDirty: () => void;
71
+ }
72
+
73
+ interface LeftRailPointerConfig {
74
+ readonly conversationTitleEditDoubleClickWindowMs: number;
75
+ }
76
+
77
+ export class LeftRailPointerHandler
78
+ implements
79
+ RailPointerHitResolver<LeftRailPointerHit>,
80
+ RailPointerHitDispatcher<LeftRailPointerHit>
81
+ {
82
+ constructor(
83
+ private readonly state: LeftRailPointerState,
84
+ private readonly actions: LeftRailPointerActions,
85
+ private readonly config: LeftRailPointerConfig,
86
+ ) {}
87
+
88
+ resolveHit(rowIndex: number, colIndex: number, railCols: number): LeftRailPointerHit {
89
+ const rows = this.state.railViewState.readLatestRows();
90
+ const selectedConversationId = conversationIdAtWorkspaceRailRow(rows, rowIndex);
91
+ const selectedProjectId = projectIdAtWorkspaceRailRow(rows, rowIndex);
92
+ const selectedRepositoryId = repositoryIdAtWorkspaceRailRow(rows, rowIndex);
93
+ const selectedAction = actionAtWorkspaceRailCell(rows, rowIndex, colIndex, railCols);
94
+ const selectedRowKind = kindAtWorkspaceRailRow(rows, rowIndex);
95
+ return {
96
+ selectedConversationId,
97
+ selectedProjectId,
98
+ selectedRepositoryId,
99
+ selectedAction,
100
+ supportsConversationTitleEditClick:
101
+ selectedRowKind === 'conversation-title' || selectedRowKind === 'conversation-body',
102
+ };
103
+ }
104
+
105
+ shouldKeepConversationTitleEditActive(hit: LeftRailPointerHit): boolean {
106
+ const editConversationId = this.state.conversationTitleEditConversationId();
107
+ return (
108
+ editConversationId !== null &&
109
+ hit.selectedConversationId === editConversationId &&
110
+ hit.supportsConversationTitleEditClick
111
+ );
112
+ }
113
+
114
+ dispatchHit(hit: LeftRailPointerHit): boolean {
115
+ if (this.handleAction(hit)) {
116
+ return true;
117
+ }
118
+ this.handleConversation(hit);
119
+ return true;
120
+ }
121
+
122
+ private handleAction(hit: LeftRailPointerHit): boolean {
123
+ const targetDirectoryId = hit.selectedProjectId ?? this.state.resolveDirectoryForAction();
124
+ if (hit.selectedAction === 'conversation.new') {
125
+ this.actions.clearConversationTitleEditClickState();
126
+ if (targetDirectoryId !== null) {
127
+ this.actions.openNewThreadPrompt(targetDirectoryId);
128
+ }
129
+ this.actions.markDirty();
130
+ return true;
131
+ }
132
+
133
+ if (hit.selectedAction === 'conversation.delete') {
134
+ this.actions.clearConversationTitleEditClickState();
135
+ const activeConversationId = this.state.activeConversationId();
136
+ if (activeConversationId !== null) {
137
+ this.actions.queueArchiveConversation(activeConversationId);
138
+ }
139
+ this.actions.markDirty();
140
+ return true;
141
+ }
142
+
143
+ if (hit.selectedAction === 'project.add') {
144
+ this.actions.clearConversationTitleEditClickState();
145
+ this.actions.openAddDirectoryPrompt();
146
+ this.actions.markDirty();
147
+ return true;
148
+ }
149
+
150
+ if (hit.selectedAction === 'repository.add') {
151
+ this.actions.clearConversationTitleEditClickState();
152
+ this.actions.openRepositoryPromptForCreate();
153
+ return true;
154
+ }
155
+
156
+ if (hit.selectedAction === 'repository.edit') {
157
+ this.actions.clearConversationTitleEditClickState();
158
+ if (
159
+ hit.selectedRepositoryId !== null &&
160
+ this.actions.repositoryExists(hit.selectedRepositoryId)
161
+ ) {
162
+ this.actions.openRepositoryPromptForEdit(hit.selectedRepositoryId);
163
+ }
164
+ this.actions.markDirty();
165
+ return true;
166
+ }
167
+
168
+ if (hit.selectedAction === 'repository.archive') {
169
+ this.actions.clearConversationTitleEditClickState();
170
+ if (
171
+ hit.selectedRepositoryId !== null &&
172
+ this.actions.repositoryExists(hit.selectedRepositoryId)
173
+ ) {
174
+ this.actions.queueArchiveRepository(hit.selectedRepositoryId);
175
+ }
176
+ this.actions.markDirty();
177
+ return true;
178
+ }
179
+
180
+ if (hit.selectedAction === 'repository.toggle') {
181
+ this.actions.clearConversationTitleEditClickState();
182
+ if (hit.selectedRepositoryId !== null) {
183
+ this.actions.toggleRepositoryGroup(hit.selectedRepositoryId);
184
+ this.actions.selectLeftNavRepository(hit.selectedRepositoryId);
185
+ }
186
+ this.actions.markDirty();
187
+ return true;
188
+ }
189
+
190
+ if (hit.selectedAction === 'repositories.toggle') {
191
+ this.actions.clearConversationTitleEditClickState();
192
+ if (this.state.repositoriesCollapsed()) {
193
+ this.actions.expandAllRepositoryGroups();
194
+ } else {
195
+ this.actions.collapseAllRepositoryGroups();
196
+ }
197
+ this.actions.markDirty();
198
+ return true;
199
+ }
200
+
201
+ if (hit.selectedAction === 'home.open') {
202
+ this.actions.clearConversationTitleEditClickState();
203
+ this.actions.enterHomePane();
204
+ this.actions.markDirty();
205
+ return true;
206
+ }
207
+
208
+ if (hit.selectedAction === 'nim.open') {
209
+ this.actions.clearConversationTitleEditClickState();
210
+ if (this.actions.enterNimPane !== undefined) {
211
+ this.actions.enterNimPane();
212
+ } else {
213
+ this.actions.enterHomePane();
214
+ }
215
+ this.actions.markDirty();
216
+ return true;
217
+ }
218
+
219
+ if (hit.selectedAction === 'tasks.open') {
220
+ this.actions.clearConversationTitleEditClickState();
221
+ if (this.actions.enterTasksPane !== undefined) {
222
+ this.actions.enterTasksPane();
223
+ } else {
224
+ this.actions.enterHomePane();
225
+ }
226
+ this.actions.markDirty();
227
+ return true;
228
+ }
229
+
230
+ if (hit.selectedAction === 'project.close') {
231
+ this.actions.clearConversationTitleEditClickState();
232
+ if (targetDirectoryId !== null) {
233
+ this.actions.queueCloseDirectory(targetDirectoryId);
234
+ }
235
+ this.actions.markDirty();
236
+ return true;
237
+ }
238
+
239
+ if (hit.selectedAction === 'project.github.open') {
240
+ this.actions.clearConversationTitleEditClickState();
241
+ if (targetDirectoryId !== null && this.state.directoriesHas(targetDirectoryId)) {
242
+ if (this.actions.enterGitHubPane !== undefined) {
243
+ this.actions.enterGitHubPane(targetDirectoryId);
244
+ } else {
245
+ this.actions.enterProjectPane(targetDirectoryId);
246
+ }
247
+ }
248
+ this.actions.markDirty();
249
+ return true;
250
+ }
251
+
252
+ if (hit.selectedAction === 'project.github.toggle') {
253
+ this.actions.clearConversationTitleEditClickState();
254
+ if (
255
+ targetDirectoryId !== null &&
256
+ this.state.directoriesHas(targetDirectoryId) &&
257
+ this.actions.toggleGitHubProjectExpanded !== undefined
258
+ ) {
259
+ this.actions.toggleGitHubProjectExpanded(targetDirectoryId);
260
+ }
261
+ this.actions.markDirty();
262
+ return true;
263
+ }
264
+
265
+ if (hit.selectedAction === 'shortcuts.toggle') {
266
+ this.actions.clearConversationTitleEditClickState();
267
+ this.actions.toggleShortcutsCollapsed();
268
+ this.actions.markDirty();
269
+ return true;
270
+ }
271
+
272
+ return false;
273
+ }
274
+
275
+ private handleConversation(hit: LeftRailPointerHit): boolean {
276
+ const conversationClick =
277
+ hit.selectedConversationId !== null && hit.supportsConversationTitleEditClick
278
+ ? detectConversationDoubleClick(
279
+ this.state.previousConversationClickState(),
280
+ hit.selectedConversationId,
281
+ this.state.nowMs(),
282
+ this.config.conversationTitleEditDoubleClickWindowMs,
283
+ )
284
+ : {
285
+ doubleClick: false,
286
+ nextState: null,
287
+ };
288
+ this.actions.setConversationClickState(conversationClick.nextState);
289
+
290
+ const activeConversationId = this.state.activeConversationId();
291
+ if (
292
+ hit.selectedConversationId !== null &&
293
+ hit.selectedConversationId === activeConversationId
294
+ ) {
295
+ if (!this.state.isConversationPaneActive()) {
296
+ if (conversationClick.doubleClick) {
297
+ this.actions.queueActivateConversationAndEdit(hit.selectedConversationId);
298
+ } else {
299
+ this.actions.queueActivateConversation(hit.selectedConversationId);
300
+ }
301
+ } else if (conversationClick.doubleClick) {
302
+ this.actions.beginConversationTitleEdit(hit.selectedConversationId);
303
+ }
304
+ this.actions.markDirty();
305
+ return true;
306
+ }
307
+
308
+ if (hit.selectedConversationId !== null) {
309
+ if (conversationClick.doubleClick) {
310
+ this.actions.queueActivateConversationAndEdit(hit.selectedConversationId);
311
+ } else {
312
+ this.actions.queueActivateConversation(hit.selectedConversationId);
313
+ }
314
+ this.actions.markDirty();
315
+ return true;
316
+ }
317
+
318
+ if (hit.selectedProjectId !== null && this.state.directoriesHas(hit.selectedProjectId)) {
319
+ this.actions.setConversationClickState(null);
320
+ this.actions.enterProjectPane(hit.selectedProjectId);
321
+ this.actions.markDirty();
322
+ return true;
323
+ }
324
+
325
+ this.actions.setConversationClickState(null);
326
+ this.actions.markDirty();
327
+ return true;
328
+ }
329
+ }
@@ -2,6 +2,8 @@ export interface MuxUiStateSnapshot {
2
2
  paneWidthPercent: number;
3
3
  repositoriesCollapsed: boolean;
4
4
  shortcutsCollapsed: boolean;
5
+ startupPane: 'home' | 'nim';
6
+ showDebugBar: boolean;
5
7
  }
6
8
 
7
9
  interface MuxUiStatePersistenceOptions {
@@ -76,7 +78,9 @@ export class MuxUiStatePersistence {
76
78
  return (
77
79
  left.paneWidthPercent === right.paneWidthPercent &&
78
80
  left.repositoriesCollapsed === right.repositoriesCollapsed &&
79
- left.shortcutsCollapsed === right.shortcutsCollapsed
81
+ left.shortcutsCollapsed === right.shortcutsCollapsed &&
82
+ left.startupPane === right.startupPane &&
83
+ left.showDebugBar === right.showDebugBar
80
84
  );
81
85
  }
82
86
  }
@@ -7,7 +7,7 @@ interface RenderTerminalRecordingToGifInput {
7
7
  readonly outputPath: string;
8
8
  }
9
9
 
10
- interface RecordingServiceOptions {
10
+ export interface RecordingServiceOptions {
11
11
  readonly recordingWriter: RecordingWriter | null;
12
12
  readonly recordingPath: string | null;
13
13
  readonly recordingGifOutputPath: string | null;
@@ -17,37 +17,50 @@ interface RecordingServiceOptions {
17
17
  readonly writeStderr: (text: string) => void;
18
18
  }
19
19
 
20
- export class RecordingService {
21
- constructor(private readonly options: RecordingServiceOptions) {}
20
+ export interface RecordingService {
21
+ closeWriter(): Promise<unknown | null>;
22
+ finalizeAfterShutdown(recordingCloseError: unknown | null): Promise<void>;
23
+ }
24
+
25
+ export function createRecordingService(options: RecordingServiceOptions): RecordingService {
26
+ function formatCloseError(recordingCloseError: unknown): string {
27
+ if (recordingCloseError instanceof Error) {
28
+ return recordingCloseError.message;
29
+ }
30
+ if (typeof recordingCloseError === 'string') {
31
+ return recordingCloseError;
32
+ }
33
+ return 'unknown error';
34
+ }
22
35
 
23
- async closeWriter(): Promise<unknown | null> {
24
- if (this.options.recordingWriter === null) {
36
+ async function closeWriter(): Promise<unknown | null> {
37
+ if (options.recordingWriter === null) {
25
38
  return null;
26
39
  }
27
40
  try {
28
- await this.options.recordingWriter.close();
41
+ await options.recordingWriter.close();
29
42
  return null;
30
43
  } catch (error: unknown) {
31
44
  return error;
32
45
  }
33
46
  }
34
47
 
35
- async finalizeAfterShutdown(recordingCloseError: unknown | null): Promise<void> {
48
+ async function finalizeAfterShutdown(recordingCloseError: unknown | null): Promise<void> {
36
49
  if (
37
- this.options.recordingGifOutputPath !== null &&
38
- this.options.recordingPath !== null &&
50
+ options.recordingGifOutputPath !== null &&
51
+ options.recordingPath !== null &&
39
52
  recordingCloseError === null
40
53
  ) {
41
54
  try {
42
- await this.options.renderTerminalRecordingToGif({
43
- recordingPath: this.options.recordingPath,
44
- outputPath: this.options.recordingGifOutputPath,
55
+ await options.renderTerminalRecordingToGif({
56
+ recordingPath: options.recordingPath,
57
+ outputPath: options.recordingGifOutputPath,
45
58
  });
46
- this.options.writeStderr(
47
- `[mux-recording] jsonl=${this.options.recordingPath} gif=${this.options.recordingGifOutputPath}\n`,
59
+ options.writeStderr(
60
+ `[mux-recording] jsonl=${options.recordingPath} gif=${options.recordingGifOutputPath}\n`,
48
61
  );
49
62
  } catch (error: unknown) {
50
- this.options.writeStderr(
63
+ options.writeStderr(
51
64
  `[mux-recording] gif-export-failed ${
52
65
  error instanceof Error ? error.message : String(error)
53
66
  }\n`,
@@ -57,19 +70,14 @@ export class RecordingService {
57
70
  }
58
71
 
59
72
  if (recordingCloseError !== null) {
60
- this.options.writeStderr(
61
- `[mux-recording] close-failed ${this.formatCloseError(recordingCloseError)}\n`,
73
+ options.writeStderr(
74
+ `[mux-recording] close-failed ${formatCloseError(recordingCloseError)}\n`,
62
75
  );
63
76
  }
64
77
  }
65
78
 
66
- private formatCloseError(recordingCloseError: unknown): string {
67
- if (recordingCloseError instanceof Error) {
68
- return recordingCloseError.message;
69
- }
70
- if (typeof recordingCloseError === 'string') {
71
- return recordingCloseError;
72
- }
73
- return 'unknown error';
74
- }
79
+ return {
80
+ closeWriter,
81
+ finalizeAfterShutdown,
82
+ };
75
83
  }
@@ -75,7 +75,7 @@ interface RuntimeCommandMenuAgentToolsOptions {
75
75
  readonly markDirty: () => void;
76
76
  }
77
77
 
78
- export class RuntimeCommandMenuAgentTools {
78
+ export class RuntimeCommandMenuAgentToolsCache {
79
79
  private readonly statusByAgent = new Map<InstallableAgentType, AgentToolStatusRecord>();
80
80
 
81
81
  constructor(private readonly options: RuntimeCommandMenuAgentToolsOptions) {}
@@ -28,7 +28,9 @@ interface RuntimeConversationTitleRefreshResult {
28
28
 
29
29
  const THREAD_TITLE_AGENT_TYPES = new Set(['codex', 'claude', 'cursor']);
30
30
 
31
- interface RuntimeControlActionsOptions<TConversation extends RuntimeConversationControlState> {
31
+ export interface RuntimeControlActionsOptions<
32
+ TConversation extends RuntimeConversationControlState,
33
+ > {
32
34
  readonly conversationById: (sessionId: string) => TConversation | undefined;
33
35
  readonly interruptSession: (sessionId: string) => Promise<RuntimeInterruptResult>;
34
36
  readonly nowIso: () => string;
@@ -57,81 +59,101 @@ interface RuntimeControlActionsOptions<TConversation extends RuntimeConversation
57
59
  ) => Promise<RuntimeConversationTitleRefreshResult>;
58
60
  }
59
61
 
60
- export class RuntimeControlActions<TConversation extends RuntimeConversationControlState> {
61
- constructor(private readonly options: RuntimeControlActionsOptions<TConversation>) {}
62
+ export interface RuntimeControlActions {
63
+ interruptConversation(sessionId: string): Promise<void>;
64
+ toggleGatewayProfiler(): Promise<void>;
65
+ toggleGatewayStatusTimeline(): Promise<void>;
66
+ toggleGatewayRenderTrace(conversationId: string | null): Promise<void>;
67
+ refreshAllConversationTitles(): Promise<void>;
68
+ }
69
+
70
+ export function createRuntimeControlActions<TConversation extends RuntimeConversationControlState>(
71
+ options: RuntimeControlActionsOptions<TConversation>,
72
+ ): RuntimeControlActions {
73
+ const scopeMessage = (prefix: string, message: string): string => {
74
+ if (options.sessionName === null) {
75
+ return `[${prefix}] ${message}`;
76
+ }
77
+ return `[${prefix}:${options.sessionName}] ${message}`;
78
+ };
62
79
 
63
- async interruptConversation(sessionId: string): Promise<void> {
64
- const conversation = this.options.conversationById(sessionId);
80
+ const setNotices = (message: string): void => {
81
+ options.setTaskPaneNotice(message);
82
+ options.setDebugFooterNotice(message);
83
+ };
84
+
85
+ const interruptConversation = async (sessionId: string): Promise<void> => {
86
+ const conversation = options.conversationById(sessionId);
65
87
  if (conversation === undefined || !conversation.live) {
66
88
  return;
67
89
  }
68
- const result = await this.options.interruptSession(sessionId);
90
+ const result = await options.interruptSession(sessionId);
69
91
  if (!result.interrupted) {
70
92
  return;
71
93
  }
72
94
  conversation.status = 'completed';
73
95
  conversation.attentionReason = null;
74
- conversation.lastEventAt = this.options.nowIso();
75
- this.options.markDirty();
76
- }
96
+ conversation.lastEventAt = options.nowIso();
97
+ options.markDirty();
98
+ };
77
99
 
78
- async toggleGatewayProfiler(): Promise<void> {
100
+ const toggleGatewayProfiler = async (): Promise<void> => {
79
101
  try {
80
- const result = await this.options.toggleGatewayProfiler({
81
- invocationDirectory: this.options.invocationDirectory,
82
- sessionName: this.options.sessionName,
102
+ const result = await options.toggleGatewayProfiler({
103
+ invocationDirectory: options.invocationDirectory,
104
+ sessionName: options.sessionName,
83
105
  });
84
- this.setNotices(this.scopeMessage('profile', result.message));
106
+ setNotices(scopeMessage('profile', result.message));
85
107
  } catch (error: unknown) {
86
108
  const message = error instanceof Error ? error.message : String(error);
87
- this.setNotices(this.scopeMessage('profile', message));
109
+ setNotices(scopeMessage('profile', message));
88
110
  } finally {
89
- this.options.markDirty();
111
+ options.markDirty();
90
112
  }
91
- }
113
+ };
92
114
 
93
- async toggleGatewayStatusTimeline(): Promise<void> {
115
+ const toggleGatewayStatusTimeline = async (): Promise<void> => {
94
116
  try {
95
- const result = await this.options.toggleGatewayStatusTimeline({
96
- invocationDirectory: this.options.invocationDirectory,
97
- sessionName: this.options.sessionName,
117
+ const result = await options.toggleGatewayStatusTimeline({
118
+ invocationDirectory: options.invocationDirectory,
119
+ sessionName: options.sessionName,
98
120
  });
99
- this.setNotices(this.scopeMessage('status-trace', result.message));
121
+ setNotices(scopeMessage('status-trace', result.message));
100
122
  } catch (error: unknown) {
101
123
  const message = error instanceof Error ? error.message : String(error);
102
- this.setNotices(this.scopeMessage('status-trace', message));
124
+ setNotices(scopeMessage('status-trace', message));
103
125
  } finally {
104
- this.options.markDirty();
126
+ options.markDirty();
105
127
  }
106
- }
128
+ };
107
129
 
108
- async toggleGatewayRenderTrace(conversationId: string | null): Promise<void> {
130
+ const toggleGatewayRenderTrace = async (conversationId: string | null): Promise<void> => {
109
131
  try {
110
- const result = await this.options.toggleGatewayRenderTrace({
111
- invocationDirectory: this.options.invocationDirectory,
112
- sessionName: this.options.sessionName,
132
+ const result = await options.toggleGatewayRenderTrace({
133
+ invocationDirectory: options.invocationDirectory,
134
+ sessionName: options.sessionName,
113
135
  conversationId,
114
136
  });
115
- this.setNotices(this.scopeMessage('render-trace', result.message));
137
+ setNotices(scopeMessage('render-trace', result.message));
116
138
  } catch (error: unknown) {
117
139
  const message = error instanceof Error ? error.message : String(error);
118
- this.setNotices(this.scopeMessage('render-trace', message));
140
+ setNotices(scopeMessage('render-trace', message));
119
141
  } finally {
120
- this.options.markDirty();
142
+ options.markDirty();
121
143
  }
122
- }
144
+ };
123
145
 
124
- async refreshAllConversationTitles(): Promise<void> {
125
- const listConversationIds = this.options.listConversationIdsForTitleRefresh;
126
- const resolveAgentType = this.options.conversationAgentTypeForTitleRefresh;
127
- const refreshConversationTitle = this.options.refreshConversationTitle;
146
+ const refreshAllConversationTitles = async (): Promise<void> => {
147
+ const listConversationIds = options.listConversationIdsForTitleRefresh;
148
+ const resolveAgentType = options.conversationAgentTypeForTitleRefresh;
149
+ const refreshConversationTitle = options.refreshConversationTitle;
128
150
  if (
129
151
  listConversationIds === undefined ||
130
152
  resolveAgentType === undefined ||
131
153
  refreshConversationTitle === undefined
132
154
  ) {
133
- this.setNotices(this.scopeMessage('thread-title', 'refresh unavailable'));
134
- this.options.markDirty();
155
+ setNotices(scopeMessage('thread-title', 'refresh unavailable'));
156
+ options.markDirty();
135
157
  return;
136
158
  }
137
159
  const allConversationIds = listConversationIds();
@@ -140,16 +162,16 @@ export class RuntimeControlActions<TConversation extends RuntimeConversationCont
140
162
  return agentType !== undefined && THREAD_TITLE_AGENT_TYPES.has(agentType);
141
163
  });
142
164
  if (eligibleConversationIds.length === 0) {
143
- this.setNotices(this.scopeMessage('thread-title', 'no agent threads to refresh'));
144
- this.options.markDirty();
165
+ setNotices(scopeMessage('thread-title', 'no agent threads to refresh'));
166
+ options.markDirty();
145
167
  return;
146
168
  }
147
169
  const total = eligibleConversationIds.length;
148
170
  let updated = 0;
149
171
  let unchanged = 0;
150
172
  let skipped = 0;
151
- this.setNotices(this.scopeMessage('thread-title', `refreshing names 0/${String(total)}`));
152
- this.options.markDirty();
173
+ setNotices(scopeMessage('thread-title', `refreshing names 0/${String(total)}`));
174
+ options.markDirty();
153
175
  for (let index = 0; index < eligibleConversationIds.length; index += 1) {
154
176
  const sessionId = eligibleConversationIds[index]!;
155
177
  try {
@@ -164,29 +186,25 @@ export class RuntimeControlActions<TConversation extends RuntimeConversationCont
164
186
  } catch {
165
187
  skipped += 1;
166
188
  }
167
- this.setNotices(
168
- this.scopeMessage('thread-title', `refreshing names ${String(index + 1)}/${String(total)}`),
189
+ setNotices(
190
+ scopeMessage('thread-title', `refreshing names ${String(index + 1)}/${String(total)}`),
169
191
  );
170
- this.options.markDirty();
192
+ options.markDirty();
171
193
  }
172
- this.setNotices(
173
- this.scopeMessage(
194
+ setNotices(
195
+ scopeMessage(
174
196
  'thread-title',
175
197
  `refreshed ${String(updated)} updated ${String(unchanged)} unchanged ${String(skipped)} skipped`,
176
198
  ),
177
199
  );
178
- this.options.markDirty();
179
- }
180
-
181
- private scopeMessage(prefix: string, message: string): string {
182
- if (this.options.sessionName === null) {
183
- return `[${prefix}] ${message}`;
184
- }
185
- return `[${prefix}:${this.options.sessionName}] ${message}`;
186
- }
200
+ options.markDirty();
201
+ };
187
202
 
188
- private setNotices(message: string): void {
189
- this.options.setTaskPaneNotice(message);
190
- this.options.setDebugFooterNotice(message);
191
- }
203
+ return {
204
+ interruptConversation,
205
+ toggleGatewayProfiler,
206
+ toggleGatewayStatusTimeline,
207
+ toggleGatewayRenderTrace,
208
+ refreshAllConversationTitles,
209
+ };
192
210
  }