@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
@@ -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
  }