@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,280 @@
1
+ import {
2
+ HARNESS_MUX_OPEN_IN_TARGET_IDS,
3
+ type HarnessMuxOpenInTargetId,
4
+ type HarnessMuxOpenInTargetOverrideConfig,
5
+ } from '../../config/config-core.ts';
6
+ import type { RegisteredCommandMenuAction } from './command-menu.ts';
7
+
8
+ interface OpenInTargetDefaults {
9
+ readonly title: string;
10
+ readonly aliases: readonly string[];
11
+ readonly keywords: readonly string[];
12
+ readonly macAppName: string | null;
13
+ readonly launchCommand: string | null;
14
+ }
15
+
16
+ const OPEN_IN_TARGET_DEFAULTS: Readonly<Record<HarnessMuxOpenInTargetId, OpenInTargetDefaults>> = {
17
+ iterm2: {
18
+ title: 'iTerm2',
19
+ aliases: ['iterm2', 'terminal'],
20
+ keywords: ['iterm', 'terminal'],
21
+ macAppName: 'iTerm',
22
+ launchCommand: 'iterm2',
23
+ },
24
+ ghostty: {
25
+ title: 'Ghostty',
26
+ aliases: ['ghostty', 'terminal'],
27
+ keywords: ['ghostty', 'terminal'],
28
+ macAppName: 'Ghostty',
29
+ launchCommand: 'ghostty',
30
+ },
31
+ zed: {
32
+ title: 'Zed',
33
+ aliases: ['zed', 'editor'],
34
+ keywords: ['zed', 'editor'],
35
+ macAppName: 'Zed',
36
+ launchCommand: 'zed',
37
+ },
38
+ cursor: {
39
+ title: 'Cursor',
40
+ aliases: ['cursor', 'cursor ide', 'editor'],
41
+ keywords: ['cursor', 'editor', 'ide'],
42
+ macAppName: 'Cursor',
43
+ launchCommand: 'cursor',
44
+ },
45
+ vscode: {
46
+ title: 'VSCode',
47
+ aliases: ['vscode', 'vs code', 'code', 'editor'],
48
+ keywords: ['vscode', 'editor', 'ide'],
49
+ macAppName: 'Visual Studio Code',
50
+ launchCommand: 'code',
51
+ },
52
+ warp: {
53
+ title: 'Warp',
54
+ aliases: ['warp', 'terminal'],
55
+ keywords: ['warp', 'terminal'],
56
+ macAppName: 'Warp',
57
+ launchCommand: 'warp',
58
+ },
59
+ finder: {
60
+ title: 'Finder',
61
+ aliases: ['finder', 'file manager'],
62
+ keywords: ['finder', 'files'],
63
+ macAppName: 'Finder',
64
+ launchCommand: null,
65
+ },
66
+ };
67
+
68
+ function normalizedCommandPart(part: string): string {
69
+ return part.trim();
70
+ }
71
+
72
+ function normalizeLaunchCommandOverride(
73
+ value: readonly string[] | undefined,
74
+ ): readonly string[] | null {
75
+ if (value === undefined) {
76
+ return null;
77
+ }
78
+ const normalized = value.map(normalizedCommandPart).filter((part) => part.length > 0);
79
+ return normalized.length > 0 ? normalized : [];
80
+ }
81
+
82
+ function defaultLaunchCommand(
83
+ platform: NodeJS.Platform,
84
+ defaults: OpenInTargetDefaults,
85
+ appName: string | null,
86
+ ): readonly string[] {
87
+ if (platform === 'darwin' && appName !== null) {
88
+ return ['open', '-a', appName, '{path}'];
89
+ }
90
+ if (defaults.launchCommand === null) {
91
+ return [];
92
+ }
93
+ return [defaults.launchCommand, '{path}'];
94
+ }
95
+
96
+ function defaultDetectCommand(
97
+ platform: NodeJS.Platform,
98
+ defaults: OpenInTargetDefaults,
99
+ ): string | null {
100
+ if (platform === 'darwin') {
101
+ return null;
102
+ }
103
+ return defaults.launchCommand;
104
+ }
105
+
106
+ function isAutoDetectedTargetAvailable(options: {
107
+ platform: NodeJS.Platform;
108
+ appName: string | null;
109
+ detectCommand: string | null;
110
+ isMacApplicationInstalled: (appName: string) => boolean;
111
+ isCommandAvailable: (command: string) => boolean;
112
+ }): boolean {
113
+ if (options.platform === 'darwin' && options.appName !== null) {
114
+ return options.isMacApplicationInstalled(options.appName);
115
+ }
116
+ if (options.detectCommand === null) {
117
+ return false;
118
+ }
119
+ return options.isCommandAvailable(options.detectCommand);
120
+ }
121
+
122
+ export interface ResolvedCommandMenuOpenInTarget {
123
+ readonly id: HarnessMuxOpenInTargetId;
124
+ readonly title: string;
125
+ readonly aliases: readonly string[];
126
+ readonly keywords: readonly string[];
127
+ readonly launchCommand: readonly string[];
128
+ }
129
+
130
+ export function resolveCommandMenuOpenInTargets(options: {
131
+ platform: NodeJS.Platform;
132
+ overrides: Readonly<
133
+ Partial<Record<HarnessMuxOpenInTargetId, HarnessMuxOpenInTargetOverrideConfig>>
134
+ >;
135
+ isCommandAvailable: (command: string) => boolean;
136
+ isMacApplicationInstalled: (appName: string) => boolean;
137
+ }): readonly ResolvedCommandMenuOpenInTarget[] {
138
+ const resolved: ResolvedCommandMenuOpenInTarget[] = [];
139
+ for (const targetId of HARNESS_MUX_OPEN_IN_TARGET_IDS) {
140
+ const defaults = OPEN_IN_TARGET_DEFAULTS[targetId];
141
+ const override = options.overrides[targetId];
142
+ if (override?.enabled === false) {
143
+ continue;
144
+ }
145
+ const appName =
146
+ override?.appName === undefined || override.appName.trim().length === 0
147
+ ? defaults.macAppName
148
+ : override.appName.trim();
149
+ const detectCommand =
150
+ override?.detectCommand === undefined
151
+ ? defaultDetectCommand(options.platform, defaults)
152
+ : override.detectCommand === null || override.detectCommand.trim().length === 0
153
+ ? null
154
+ : override.detectCommand.trim();
155
+ const overrideLaunchCommand = normalizeLaunchCommandOverride(override?.launchCommand);
156
+ const launchCommand =
157
+ overrideLaunchCommand === null
158
+ ? defaultLaunchCommand(options.platform, defaults, appName)
159
+ : overrideLaunchCommand;
160
+ if (launchCommand.length === 0) {
161
+ continue;
162
+ }
163
+ const available =
164
+ override?.enabled === true
165
+ ? true
166
+ : isAutoDetectedTargetAvailable({
167
+ platform: options.platform,
168
+ appName,
169
+ detectCommand,
170
+ isCommandAvailable: options.isCommandAvailable,
171
+ isMacApplicationInstalled: options.isMacApplicationInstalled,
172
+ });
173
+ if (!available) {
174
+ continue;
175
+ }
176
+ resolved.push({
177
+ id: targetId,
178
+ title: defaults.title,
179
+ aliases: defaults.aliases,
180
+ keywords: defaults.keywords,
181
+ launchCommand,
182
+ });
183
+ }
184
+ return resolved;
185
+ }
186
+
187
+ export function resolveCommandMenuOpenInCommand(
188
+ target: ResolvedCommandMenuOpenInTarget,
189
+ directoryPath: string,
190
+ ): { command: string; args: readonly string[] } | null {
191
+ const command = target.launchCommand[0]?.trim() ?? '';
192
+ if (command.length === 0) {
193
+ return null;
194
+ }
195
+ let pathInjected = false;
196
+ const args = target.launchCommand.slice(1).map((part) => {
197
+ if (part === '{path}') {
198
+ pathInjected = true;
199
+ return directoryPath;
200
+ }
201
+ return part;
202
+ });
203
+ if (!pathInjected) {
204
+ args.push(directoryPath);
205
+ }
206
+ return {
207
+ command,
208
+ args,
209
+ };
210
+ }
211
+
212
+ interface CommandMenuOpenInProviderDirectory {
213
+ readonly directoryId: string;
214
+ readonly path: string;
215
+ }
216
+
217
+ interface CommandMenuOpenInProviderOptions<TContext> {
218
+ readonly registerProvider: (
219
+ providerId: string,
220
+ provider: (context: TContext) => readonly RegisteredCommandMenuAction<TContext>[],
221
+ ) => () => void;
222
+ readonly providerId?: string;
223
+ readonly resolveDirectories: (context: TContext) => readonly CommandMenuOpenInProviderDirectory[];
224
+ readonly resolveTargets: () => readonly ResolvedCommandMenuOpenInTarget[];
225
+ readonly projectPathTail: (directoryPath: string) => string;
226
+ readonly openInTarget: (
227
+ target: ResolvedCommandMenuOpenInTarget,
228
+ directoryPath: string,
229
+ ) => boolean;
230
+ readonly copyPath: (directoryPath: string) => boolean;
231
+ readonly setNotice: (message: string) => void;
232
+ }
233
+
234
+ export function registerCommandMenuOpenInProvider<TContext>(
235
+ options: CommandMenuOpenInProviderOptions<TContext>,
236
+ ): () => void {
237
+ const providerId = options.providerId ?? 'project.open-in';
238
+ return options.registerProvider(providerId, (context) => {
239
+ const actions: RegisteredCommandMenuAction<TContext>[] = [];
240
+ const targets = options.resolveTargets();
241
+ for (const directory of options.resolveDirectories(context)) {
242
+ const projectLabel = options.projectPathTail(directory.path);
243
+ for (const target of targets) {
244
+ actions.push({
245
+ id: `project.open-in.${target.id}.${directory.directoryId}`,
246
+ title: `Open in ${target.title}: ${projectLabel}`,
247
+ aliases: [
248
+ ...target.aliases,
249
+ 'open project in',
250
+ `open in ${target.title.toLowerCase()}`,
251
+ directory.path,
252
+ projectLabel,
253
+ ],
254
+ keywords: ['open', 'project', 'directory', 'path', ...target.keywords],
255
+ detail: directory.path,
256
+ run: () => {
257
+ const opened = options.openInTarget(target, directory.path);
258
+ options.setNotice(
259
+ opened
260
+ ? `opened ${projectLabel} in ${target.title}`
261
+ : `failed to open ${projectLabel} in ${target.title}`,
262
+ );
263
+ },
264
+ });
265
+ }
266
+ actions.push({
267
+ id: `project.copy-path.${directory.directoryId}`,
268
+ title: `Copy Path: ${projectLabel}`,
269
+ aliases: ['copy path', 'copy project path', directory.path, projectLabel],
270
+ keywords: ['copy', 'path', 'clipboard', 'project', 'directory'],
271
+ detail: directory.path,
272
+ run: () => {
273
+ const copied = options.copyPath(directory.path);
274
+ options.setNotice(copied ? `copied path: ${directory.path}` : 'failed to copy path');
275
+ },
276
+ });
277
+ }
278
+ return actions;
279
+ });
280
+ }
@@ -1,6 +1,18 @@
1
- export const COMMAND_MENU_MAX_RESULTS = 8;
2
-
3
- type CommandMenuScope = 'all' | 'thread-start' | 'theme-select';
1
+ const COMMAND_MENU_MAX_RESULTS = 8;
2
+ const THREAD_ACTION_ID_PATTERN = /^thread\.(?:start|install)\.([a-z0-9-]+)$/u;
3
+ const TASK_SUMMARY_MAX_CHARS = 96;
4
+ const DEFAULT_TASK_SUMMARY = 'untitled task';
5
+
6
+ type CommandMenuScope = 'all' | 'thread-start' | 'theme-select' | 'shortcuts';
7
+ type CommandMenuInitialGroup = 'agent-types' | 'actions';
8
+
9
+ const COMMAND_MENU_AGENT_TYPE_ORDER: Readonly<Record<string, number>> = {
10
+ codex: 0,
11
+ claude: 1,
12
+ cursor: 2,
13
+ terminal: 3,
14
+ critique: 4,
15
+ };
4
16
 
5
17
  export interface CommandMenuState {
6
18
  readonly scope: CommandMenuScope;
@@ -14,6 +26,10 @@ export interface CommandMenuActionDescriptor {
14
26
  readonly aliases?: readonly string[];
15
27
  readonly keywords?: readonly string[];
16
28
  readonly detail?: string;
29
+ readonly screenLabel?: string;
30
+ readonly sectionLabel?: string;
31
+ readonly bindingHint?: string;
32
+ readonly priority?: number;
17
33
  }
18
34
 
19
35
  export function filterThemePresetActionsForScope<TAction extends CommandMenuActionDescriptor>(
@@ -27,6 +43,27 @@ export function filterThemePresetActionsForScope<TAction extends CommandMenuActi
27
43
  return actions.filter((action) => !action.id.startsWith(themeActionIdPrefix));
28
44
  }
29
45
 
46
+ export function filterCommandMenuActionsForScope<TAction extends CommandMenuActionDescriptor>(
47
+ actions: readonly TAction[],
48
+ scope: CommandMenuScope,
49
+ options: {
50
+ readonly themeActionIdPrefix: string;
51
+ readonly shortcutsActionIdPrefix: string;
52
+ },
53
+ ): readonly TAction[] {
54
+ const { themeActionIdPrefix, shortcutsActionIdPrefix } = options;
55
+ if (scope === 'theme-select') {
56
+ return actions.filter((action) => action.id.startsWith(themeActionIdPrefix));
57
+ }
58
+ if (scope === 'shortcuts') {
59
+ return actions.filter((action) => action.id.startsWith(shortcutsActionIdPrefix));
60
+ }
61
+ return actions.filter(
62
+ (action) =>
63
+ !action.id.startsWith(themeActionIdPrefix) && !action.id.startsWith(shortcutsActionIdPrefix),
64
+ );
65
+ }
66
+
30
67
  export interface RegisteredCommandMenuAction<TContext> extends CommandMenuActionDescriptor {
31
68
  readonly when?: (context: TContext) => boolean;
32
69
  readonly run: (context: TContext) => Promise<void> | void;
@@ -37,6 +74,20 @@ interface CommandMenuMatch<TAction extends CommandMenuActionDescriptor> {
37
74
  readonly score: number;
38
75
  }
39
76
 
77
+ interface CommandMenuDisplayEntry<TAction extends CommandMenuActionDescriptor> {
78
+ readonly absoluteIndex: number;
79
+ readonly action: TAction;
80
+ readonly score: number;
81
+ }
82
+
83
+ interface CommandMenuPage<TAction extends CommandMenuActionDescriptor> {
84
+ readonly matches: readonly CommandMenuMatch<TAction>[];
85
+ readonly selectedIndex: number;
86
+ readonly pageStart: number;
87
+ readonly visibleMatches: readonly CommandMenuMatch<TAction>[];
88
+ readonly displayEntries: readonly CommandMenuDisplayEntry<TAction>[];
89
+ }
90
+
40
91
  type CommandMenuActionProvider<TContext> = (
41
92
  context: TContext,
42
93
  ) => readonly RegisteredCommandMenuAction<TContext>[];
@@ -50,6 +101,29 @@ function normalizeQuery(value: string): string {
50
101
  return value.trim().toLowerCase().replace(/\s+/g, ' ');
51
102
  }
52
103
 
104
+ function truncateCommandMenuSummary(value: string, maxChars: number): string {
105
+ if (value.length <= maxChars) {
106
+ return value;
107
+ }
108
+ if (maxChars <= 3) {
109
+ return value.slice(0, Math.max(1, maxChars));
110
+ }
111
+ return `${value.slice(0, maxChars - 3)}...`;
112
+ }
113
+
114
+ export function summarizeTaskForCommandMenu(body: string, title: string): string {
115
+ const bodyLines = body.split('\n').map((line) => line.trim());
116
+ const firstBodyLine = bodyLines.find((line) => line.length > 0) ?? '';
117
+ const fallbackTitle = title.trim();
118
+ const summarySource =
119
+ firstBodyLine.length > 0
120
+ ? firstBodyLine
121
+ : fallbackTitle.length > 0
122
+ ? fallbackTitle
123
+ : DEFAULT_TASK_SUMMARY;
124
+ return truncateCommandMenuSummary(summarySource, TASK_SUMMARY_MAX_CHARS);
125
+ }
126
+
53
127
  function normalizedTokens(query: string): readonly string[] {
54
128
  const normalized = normalizeQuery(query);
55
129
  if (normalized.length === 0) {
@@ -79,6 +153,46 @@ function searchableParts(action: CommandMenuActionDescriptor): readonly string[]
79
153
  return parts;
80
154
  }
81
155
 
156
+ function threadAgentTypeFromActionId(actionId: string): string | null {
157
+ const match = THREAD_ACTION_ID_PATTERN.exec(actionId);
158
+ if (match === null) {
159
+ return null;
160
+ }
161
+ const value = (match[1] ?? '').trim().toLowerCase();
162
+ return value.length > 0 ? value : null;
163
+ }
164
+
165
+ function initialGroupForAction(action: CommandMenuActionDescriptor): CommandMenuInitialGroup {
166
+ return threadAgentTypeFromActionId(action.id) === null ? 'actions' : 'agent-types';
167
+ }
168
+
169
+ function initialGroupRank(action: CommandMenuActionDescriptor): number {
170
+ return initialGroupForAction(action) === 'agent-types' ? 0 : 1;
171
+ }
172
+
173
+ function initialAgentTypeRank(action: CommandMenuActionDescriptor): number {
174
+ const agentType = threadAgentTypeFromActionId(action.id);
175
+ if (agentType === null) {
176
+ return Number.MAX_SAFE_INTEGER;
177
+ }
178
+ return COMMAND_MENU_AGENT_TYPE_ORDER[agentType] ?? 100;
179
+ }
180
+
181
+ function usesInitialTypeSort(query: string): boolean {
182
+ return normalizedTokens(query).length === 0;
183
+ }
184
+
185
+ function buildCommandMenuDisplayEntries<TAction extends CommandMenuActionDescriptor>(
186
+ visibleMatches: readonly CommandMenuMatch<TAction>[],
187
+ pageStart: number,
188
+ ): readonly CommandMenuDisplayEntry<TAction>[] {
189
+ return visibleMatches.map((match, index) => ({
190
+ absoluteIndex: pageStart + index,
191
+ action: match.action,
192
+ score: match.score,
193
+ }));
194
+ }
195
+
82
196
  function actionScore(action: CommandMenuActionDescriptor, query: string): number | null {
83
197
  const tokens = normalizedTokens(query);
84
198
  if (tokens.length === 0) {
@@ -136,7 +250,11 @@ function clampSelectedIndex(selectedIndex: number, resultCount: number): number
136
250
  return selectedIndex;
137
251
  }
138
252
 
139
- function moveSelectionByDelta(selectedIndex: number, resultCount: number, delta: number): number {
253
+ export function moveSelectionByDelta(
254
+ selectedIndex: number,
255
+ resultCount: number,
256
+ delta: number,
257
+ ): number {
140
258
  if (resultCount <= 0) {
141
259
  return 0;
142
260
  }
@@ -183,15 +301,39 @@ export function resolveCommandMenuMatches<TAction extends CommandMenuActionDescr
183
301
  query: string,
184
302
  limit: number | null = COMMAND_MENU_MAX_RESULTS,
185
303
  ): readonly CommandMenuMatch<TAction>[] {
304
+ const useInitialSort = usesInitialTypeSort(query);
186
305
  const scored = actions
187
306
  .flatMap((action) => {
188
307
  const score = actionScore(action, query);
189
308
  return score === null ? [] : [{ action, score }];
190
309
  })
191
310
  .sort((left, right) => {
311
+ const priorityDelta = (right.action.priority ?? 0) - (left.action.priority ?? 0);
312
+ if (priorityDelta !== 0) {
313
+ return priorityDelta;
314
+ }
315
+ if (useInitialSort) {
316
+ const groupCompare = initialGroupRank(left.action) - initialGroupRank(right.action);
317
+ if (groupCompare !== 0) {
318
+ return groupCompare;
319
+ }
320
+ const leftGroup = initialGroupForAction(left.action);
321
+ if (leftGroup === 'agent-types') {
322
+ const agentCompare =
323
+ initialAgentTypeRank(left.action) - initialAgentTypeRank(right.action);
324
+ if (agentCompare !== 0) {
325
+ return agentCompare;
326
+ }
327
+ }
328
+ return left.action.title.localeCompare(right.action.title);
329
+ }
192
330
  if (left.score !== right.score) {
193
331
  return left.score - right.score;
194
332
  }
333
+ const groupCompare = initialGroupRank(left.action) - initialGroupRank(right.action);
334
+ if (groupCompare !== 0) {
335
+ return groupCompare;
336
+ }
195
337
  return left.action.title.localeCompare(right.action.title);
196
338
  });
197
339
  if (limit === null) {
@@ -200,6 +342,27 @@ export function resolveCommandMenuMatches<TAction extends CommandMenuActionDescr
200
342
  return scored.slice(0, Math.max(0, limit));
201
343
  }
202
344
 
345
+ export function resolveCommandMenuPage<TAction extends CommandMenuActionDescriptor>(
346
+ actions: readonly TAction[],
347
+ menu: CommandMenuState,
348
+ ): CommandMenuPage<TAction> {
349
+ const matches = resolveCommandMenuMatches(actions, menu.query, null);
350
+ const selectedIndex = clampSelectedIndex(menu.selectedIndex, matches.length);
351
+ const pageStart =
352
+ matches.length === 0
353
+ ? 0
354
+ : Math.floor(selectedIndex / COMMAND_MENU_MAX_RESULTS) * COMMAND_MENU_MAX_RESULTS;
355
+ const visibleMatches = matches.slice(pageStart, pageStart + COMMAND_MENU_MAX_RESULTS);
356
+ const displayEntries = buildCommandMenuDisplayEntries(visibleMatches, pageStart);
357
+ return {
358
+ matches,
359
+ selectedIndex,
360
+ pageStart,
361
+ visibleMatches,
362
+ displayEntries,
363
+ };
364
+ }
365
+
203
366
  export function resolveSelectedCommandMenuActionId<TAction extends CommandMenuActionDescriptor>(
204
367
  actions: readonly TAction[],
205
368
  menu: CommandMenuState | null,
@@ -187,3 +187,16 @@ export function debugFooterForConversation(conversation: ConversationState): str
187
187
  : compactDebugText(conversation.launchCommand);
188
188
  return `[dbg] ${launchCommand}`;
189
189
  }
190
+
191
+ export function composeDebugStatusFooter(
192
+ showDebugBar: boolean,
193
+ githubDebugTokens: string,
194
+ conversation: ConversationState,
195
+ ): string {
196
+ if (!showDebugBar) {
197
+ return '';
198
+ }
199
+ const githubPrefix = githubDebugTokens.trim();
200
+ const debugFooter = debugFooterForConversation(conversation);
201
+ return githubPrefix.length === 0 ? debugFooter : `${githubPrefix} ${debugFooter}`;
202
+ }
@@ -3,7 +3,7 @@ interface ResolveDirectoryForActionConversationState {
3
3
  }
4
4
 
5
5
  interface ResolveDirectoryForActionOptions {
6
- mainPaneMode: 'conversation' | 'project' | 'home';
6
+ mainPaneMode: 'conversation' | 'project' | 'home' | 'nim';
7
7
  activeDirectoryId: string | null;
8
8
  activeConversationId: string | null;
9
9
  conversations: ReadonlyMap<string, ResolveDirectoryForActionConversationState>;
@@ -1,4 +1,6 @@
1
1
  import { execFile } from 'node:child_process';
2
+ import { basename } from 'node:path';
3
+ import { fileURLToPath, pathToFileURL } from 'node:url';
2
4
  import { promisify } from 'node:util';
3
5
  import {
4
6
  normalizeGitHubRemoteUrl,
@@ -120,6 +122,24 @@ export async function runGitCommand(
120
122
  }
121
123
  }
122
124
 
125
+ function normalizeLocalRepositoryLocator(pathValue: string): string | null {
126
+ const trimmed = pathValue.trim();
127
+ if (trimmed.length === 0) {
128
+ return null;
129
+ }
130
+ return pathToFileURL(trimmed).toString();
131
+ }
132
+
133
+ function repositoryNameFromRepositoryLocator(locator: string): string | null {
134
+ const normalizedGitHub = normalizeGitHubRemoteUrl(locator);
135
+ if (normalizedGitHub !== null) {
136
+ return repositoryNameFromGitHubRemoteUrl(normalizedGitHub);
137
+ }
138
+ const resolvedPath = fileURLToPath(locator);
139
+ const inferred = basename(resolvedPath.trim());
140
+ return inferred.length > 0 ? inferred : null;
141
+ }
142
+
123
143
  async function readNormalizedGitHubRemoteUrl(
124
144
  cwd: string,
125
145
  runCommand: GitCommandRunner,
@@ -146,6 +166,17 @@ async function readNormalizedGitHubRemoteUrl(
146
166
  return null;
147
167
  }
148
168
 
169
+ async function readRepositoryLocator(
170
+ cwd: string,
171
+ runCommand: GitCommandRunner,
172
+ ): Promise<string | null> {
173
+ const normalizedGitHubRemoteUrl = await readNormalizedGitHubRemoteUrl(cwd, runCommand);
174
+ if (normalizedGitHubRemoteUrl !== null) {
175
+ return normalizedGitHubRemoteUrl;
176
+ }
177
+ return normalizeLocalRepositoryLocator(await runCommand(cwd, ['rev-parse', '--show-toplevel']));
178
+ }
179
+
149
180
  export async function readGitDirectorySnapshot(
150
181
  cwd: string,
151
182
  runCommand: GitCommandRunner = runGitCommand,
@@ -162,7 +193,7 @@ export async function readGitDirectorySnapshot(
162
193
  const statusOutputPromise = runCommand(cwd, ['status', '--porcelain=1', '--branch']);
163
194
  const unstagedShortstatPromise = runCommand(cwd, ['diff', '--shortstat']);
164
195
  const stagedShortstatPromise = runCommand(cwd, ['diff', '--cached', '--shortstat']);
165
- const remoteUrlPromise = readNormalizedGitHubRemoteUrl(cwd, runCommand);
196
+ const remoteUrlPromise = readRepositoryLocator(cwd, runCommand);
166
197
  const lastCommitPromise = runCommand(cwd, ['log', '-1', '--format=%ct %h']);
167
198
  const commitCountPromise =
168
199
  options.includeCommitCount === false
@@ -209,7 +240,7 @@ export async function readGitDirectorySnapshot(
209
240
  inferredName:
210
241
  normalizedRemoteUrl === null
211
242
  ? null
212
- : repositoryNameFromGitHubRemoteUrl(normalizedRemoteUrl),
243
+ : repositoryNameFromRepositoryLocator(normalizedRemoteUrl),
213
244
  defaultBranch: branch === '(detached)' ? null : branch,
214
245
  },
215
246
  };
@@ -6,6 +6,7 @@ interface HandleGlobalShortcutOptions {
6
6
  resolveDirectoryForAction: () => string | null;
7
7
  openNewThreadPrompt: (directoryId: string) => void;
8
8
  toggleCommandMenu: () => void;
9
+ toggleDebugBar: () => void;
9
10
  openOrCreateCritiqueConversationInDirectory: (directoryId: string) => Promise<void>;
10
11
  toggleGatewayProfile: () => Promise<void>;
11
12
  toggleGatewayStatusTimeline: () => Promise<void>;
@@ -30,6 +31,7 @@ export function handleGlobalShortcut(options: HandleGlobalShortcutOptions): bool
30
31
  resolveDirectoryForAction,
31
32
  openNewThreadPrompt,
32
33
  toggleCommandMenu,
34
+ toggleDebugBar,
33
35
  openOrCreateCritiqueConversationInDirectory,
34
36
  toggleGatewayProfile,
35
37
  toggleGatewayStatusTimeline,
@@ -64,6 +66,10 @@ export function handleGlobalShortcut(options: HandleGlobalShortcutOptions): bool
64
66
  toggleCommandMenu();
65
67
  return true;
66
68
  }
69
+ if (shortcut === 'mux.debug-bar.toggle') {
70
+ toggleDebugBar();
71
+ return true;
72
+ }
67
73
  if (shortcut === 'mux.conversation.critique.open-or-create') {
68
74
  const targetDirectoryId = resolveDirectoryForAction();
69
75
  if (targetDirectoryId !== null) {
@@ -9,7 +9,7 @@ interface HomePaneDragState {
9
9
  interface HandleHomePaneDragReleaseOptions {
10
10
  homePaneDragState: HomePaneDragState | null;
11
11
  isMouseRelease: boolean;
12
- mainPaneMode: 'conversation' | 'project' | 'home';
12
+ mainPaneMode: 'conversation' | 'project' | 'home' | 'nim';
13
13
  target: string;
14
14
  rowIndex: number;
15
15
  taskIdAtRow: (rowIndex: number) => string | null;
@@ -30,6 +30,7 @@ interface HandleHomePanePointerClickOptions {
30
30
  setTaskRepositoryDropdownOpen: (open: boolean) => void;
31
31
  taskIdAtRow: (rowIndex: number) => string | null;
32
32
  repositoryIdAtRow: (rowIndex: number) => string | null;
33
+ rowTextAtRow?: (rowIndex: number) => string | null;
33
34
  selectTaskById: (taskId: string) => void;
34
35
  selectRepositoryById: (repositoryId: string) => void;
35
36
  runTaskPaneAction: (action: 'task.ready' | 'task.draft' | 'task.complete') => void;
@@ -55,6 +56,15 @@ export function handleHomePanePointerClick(options: HandleHomePanePointerClickOp
55
56
  0,
56
57
  Math.min(options.rightCols - 1, options.pointerCol - options.rightStartCol),
57
58
  );
59
+ const rowText = options.rowTextAtRow?.(rowIndex) ?? null;
60
+ const isTaskEditorContentRow =
61
+ options.taskIdAtRow(rowIndex) !== null &&
62
+ rowText !== null &&
63
+ rowText.trimEnd().startsWith(' │') &&
64
+ rowText.trimEnd().includes('│');
65
+ if (isTaskEditorContentRow) {
66
+ return false;
67
+ }
58
68
  const action = options.actionAtCell(rowIndex, colIndex) ?? options.actionAtRow(rowIndex);
59
69
  if (
60
70
  handleHomePaneActionClick({