@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.
- package/README.md +31 -39
- package/package.json +31 -11
- package/packages/harness-ai/src/anthropic-protocol.ts +68 -68
- package/packages/harness-ai/src/stream-text.ts +13 -91
- package/packages/harness-ui/src/frame-primitives.ts +158 -0
- package/packages/harness-ui/src/index.ts +18 -0
- package/packages/harness-ui/src/interaction/conversation-input-forwarder.ts +221 -0
- package/packages/harness-ui/src/interaction/conversation-selection-input.ts +213 -0
- package/packages/harness-ui/src/interaction/global-shortcut-input.ts +172 -0
- package/{src/ui → packages/harness-ui/src/interaction}/input-preflight.ts +10 -12
- package/{src/ui → packages/harness-ui/src/interaction}/input-token-router.ts +120 -69
- package/packages/harness-ui/src/interaction/input.ts +420 -0
- package/packages/harness-ui/src/interaction/left-nav-input.ts +166 -0
- package/{src/ui → packages/harness-ui/src/interaction}/main-pane-pointer-input.ts +91 -23
- package/{src/ui → packages/harness-ui/src/interaction}/pointer-routing-input.ts +112 -48
- package/packages/harness-ui/src/interaction/rail-pointer-input.ts +62 -0
- package/packages/harness-ui/src/interaction/repository-fold-input.ts +118 -0
- package/packages/harness-ui/src/kit.ts +476 -0
- package/packages/harness-ui/src/layout.ts +238 -0
- package/packages/harness-ui/src/modal-manager.ts +222 -0
- package/{src/ui → packages/harness-ui/src}/screen.ts +53 -26
- package/packages/harness-ui/src/surface.ts +252 -0
- package/packages/harness-ui/src/text-layout.ts +210 -0
- package/packages/nim-core/src/contracts.ts +239 -0
- package/packages/nim-core/src/event-store.ts +299 -0
- package/packages/nim-core/src/events.ts +53 -0
- package/packages/nim-core/src/index.ts +9 -0
- package/packages/nim-core/src/provider-router.ts +129 -0
- package/packages/nim-core/src/providers/anthropic-driver.ts +291 -0
- package/packages/nim-core/src/runtime-factory.ts +49 -0
- package/packages/nim-core/src/runtime.ts +1797 -0
- package/packages/nim-core/src/session-store.ts +516 -0
- package/packages/nim-core/src/telemetry.ts +48 -0
- package/packages/nim-test-tui/src/index.ts +150 -0
- package/packages/nim-ui-core/src/index.ts +1 -0
- package/packages/nim-ui-core/src/projection.ts +87 -0
- package/scripts/codex-live-mux-runtime.ts +2 -3872
- package/scripts/control-plane-daemon.ts +11 -0
- package/scripts/harness-bin.js +5 -0
- package/scripts/harness-commands.ts +300 -0
- package/scripts/harness-runtime.ts +82 -0
- package/scripts/harness.ts +33 -3019
- package/scripts/nim-tui-smoke.ts +748 -0
- package/src/cli/auth/runtime.ts +948 -0
- package/src/cli/gateway/runtime.ts +1872 -0
- package/src/cli/parsing/flags.ts +23 -0
- package/src/cli/parsing/session.ts +42 -0
- package/src/cli/runtime/context.ts +193 -0
- package/src/cli/runtime-app/application.ts +392 -0
- package/src/cli/runtime-infra/gateway-control.ts +729 -0
- package/{scripts/harness-inspector.ts → src/cli/workflows/inspector.ts} +14 -11
- package/src/cli/workflows/runtime.ts +965 -0
- package/src/clients/tui/left-rail-interactions.ts +519 -0
- package/src/clients/tui/main-pane-interactions.ts +509 -0
- package/src/clients/tui/modal-input-routing.ts +71 -0
- package/src/clients/tui/render-snapshot-adapter.ts +88 -0
- package/src/clients/web/synced-selectors.ts +132 -0
- package/src/codex/live-session.ts +82 -29
- package/src/config/config-core.ts +348 -8
- package/src/config/harness.config.template.jsonc +33 -0
- package/src/control-plane/agent-realtime-api.ts +82 -427
- package/src/control-plane/session-summary.ts +10 -81
- package/src/control-plane/status/reducer-base.ts +12 -12
- package/src/control-plane/status/reducers/claude-status-reducer.ts +3 -3
- package/src/control-plane/status/reducers/codex-status-reducer.ts +4 -4
- package/src/control-plane/status/reducers/cursor-status-reducer.ts +3 -3
- package/src/control-plane/stream-client.ts +12 -2
- package/src/control-plane/stream-command-parser.ts +83 -143
- package/src/control-plane/stream-protocol.ts +53 -37
- package/src/control-plane/stream-server-command.ts +376 -69
- package/src/control-plane/stream-server-session-runtime.ts +3 -2
- package/src/control-plane/stream-server.ts +864 -70
- package/src/control-plane/stream-session-runtime-types.ts +41 -0
- package/src/{mux/live-mux/control-plane-records.ts → core/contracts/records.ts} +24 -97
- package/src/core/state/observed-stream-cursor.ts +43 -0
- package/src/core/state/synced-observed-state.ts +273 -0
- package/src/core/store/harness-synced-store.ts +81 -0
- package/src/diff/budget.ts +136 -0
- package/src/diff/build.ts +289 -0
- package/src/diff/chunker.ts +146 -0
- package/src/diff/git-invoke.ts +315 -0
- package/src/diff/git-parse.ts +472 -0
- package/src/diff/hash.ts +70 -0
- package/src/diff/index.ts +24 -0
- package/src/diff/normalize.ts +134 -0
- package/src/diff/types.ts +178 -0
- package/src/diff-ui/args.ts +346 -0
- package/src/diff-ui/commands.ts +123 -0
- package/src/diff-ui/finder.ts +94 -0
- package/src/diff-ui/highlight.ts +127 -0
- package/src/diff-ui/index.ts +2 -0
- package/src/diff-ui/model.ts +141 -0
- package/src/diff-ui/pager.ts +412 -0
- package/src/diff-ui/render.ts +337 -0
- package/src/diff-ui/runtime.ts +379 -0
- package/src/diff-ui/state.ts +224 -0
- package/src/diff-ui/types.ts +236 -0
- package/src/domain/workspace.ts +68 -5
- package/src/mux/control-plane-op-queue.ts +93 -7
- package/src/mux/conversation-rail.ts +28 -71
- package/src/mux/dual-pane-core.ts +13 -13
- package/src/mux/harness-core-ui.ts +313 -42
- package/src/mux/input-shortcuts.ts +13 -131
- package/src/mux/keybinding-catalog.ts +340 -0
- package/src/mux/keybinding-registry.ts +103 -0
- package/src/mux/live-mux/command-menu-open-in.ts +280 -0
- package/src/mux/live-mux/command-menu.ts +167 -4
- package/src/mux/live-mux/conversation-state.ts +13 -0
- package/src/mux/live-mux/directory-resolution.ts +1 -1
- package/src/mux/live-mux/git-snapshot.ts +33 -2
- package/src/mux/live-mux/global-shortcut-handlers.ts +6 -0
- package/src/mux/live-mux/home-pane-drop.ts +1 -1
- package/src/mux/live-mux/home-pane-pointer.ts +10 -0
- package/src/mux/live-mux/input-forwarding.ts +59 -2
- package/src/mux/live-mux/left-nav-activation.ts +124 -7
- package/src/mux/live-mux/left-nav.ts +35 -0
- package/src/mux/live-mux/link-click.ts +292 -0
- package/src/mux/live-mux/modal-command-menu-handler.ts +46 -9
- package/src/mux/live-mux/modal-conversation-handlers.ts +5 -1
- package/src/mux/live-mux/modal-input-reducers.ts +77 -12
- package/src/mux/live-mux/modal-overlays.ts +168 -34
- package/src/mux/live-mux/modal-pointer.ts +3 -7
- package/src/mux/live-mux/modal-prompt-handlers.ts +23 -2
- package/src/mux/live-mux/modal-release-notes-handler.ts +111 -0
- package/src/mux/live-mux/modal-task-editor-handler.ts +16 -11
- package/src/mux/live-mux/pointer-routing.ts +5 -2
- package/src/mux/live-mux/project-pane-pointer.ts +8 -0
- package/src/mux/live-mux/rail-layout.ts +33 -30
- package/src/mux/live-mux/release-notes.ts +383 -0
- package/src/mux/live-mux/render-trace-analysis.ts +52 -7
- package/src/mux/live-mux/repository-folding.ts +3 -0
- package/src/mux/live-mux/selection.ts +0 -4
- package/src/mux/live-mux/session-diagnostics-paths.ts +21 -0
- package/src/mux/project-pane-github-review.ts +271 -0
- package/src/mux/render-frame.ts +4 -0
- package/src/mux/runtime-app/codex-live-mux-runtime.ts +5191 -0
- package/src/mux/task-composer.ts +21 -14
- package/src/mux/task-focused-pane.ts +118 -117
- package/src/mux/task-screen-keybindings.ts +10 -101
- package/src/mux/workspace-rail-model.ts +270 -104
- package/src/mux/workspace-rail.ts +45 -22
- package/src/pty/session-broker.ts +1 -1
- package/{scripts → src/recording}/terminal-recording-gif-lib.ts +2 -2
- package/src/services/control-plane.ts +50 -32
- package/src/services/conversation-lifecycle.ts +118 -87
- package/src/services/conversation-startup-hydration.ts +20 -12
- package/src/services/directory-hydration.ts +21 -16
- package/src/services/event-persistence.ts +7 -0
- package/src/services/left-rail-pointer-handler.ts +329 -0
- package/src/services/mux-ui-state-persistence.ts +5 -1
- package/src/services/recording.ts +34 -26
- package/src/services/runtime-command-menu-agent-tools.ts +1 -1
- package/src/services/runtime-control-actions.ts +79 -61
- package/src/services/runtime-control-plane-ops.ts +122 -83
- package/src/services/runtime-conversation-actions.ts +40 -26
- package/src/services/runtime-conversation-activation.ts +73 -46
- package/src/services/runtime-conversation-starter.ts +53 -45
- package/src/services/runtime-conversation-title-edit.ts +91 -80
- package/src/services/runtime-envelope-handler.ts +107 -105
- package/src/services/runtime-git-state.ts +42 -29
- package/src/services/runtime-layout-resize.ts +3 -1
- package/src/services/runtime-left-rail-render.ts +99 -63
- package/src/services/runtime-nim-cli-session.ts +438 -0
- package/src/services/runtime-nim-session.ts +705 -0
- package/src/services/runtime-nim-tool-bridge.ts +141 -0
- package/src/services/runtime-observed-event-projection-pipeline.ts +45 -0
- package/src/services/runtime-process-wiring.ts +29 -36
- package/src/services/runtime-project-pane-github-review-cache.ts +164 -0
- package/src/services/runtime-render-flush.ts +63 -70
- package/src/services/runtime-render-lifecycle.ts +65 -64
- package/src/services/runtime-render-orchestrator.ts +55 -45
- package/src/services/runtime-render-pipeline.ts +106 -103
- package/src/services/runtime-render-state.ts +62 -49
- package/src/services/runtime-repository-actions.ts +97 -72
- package/src/services/runtime-right-pane-render.ts +80 -53
- package/src/services/runtime-shutdown.ts +38 -35
- package/src/services/runtime-stream-subscriptions.ts +35 -27
- package/src/services/runtime-task-composer-persistence.ts +71 -59
- package/src/services/runtime-task-composer-snapshot.ts +14 -0
- package/src/services/runtime-task-editor-actions.ts +46 -29
- package/src/services/runtime-task-pane-actions.ts +220 -134
- package/src/services/runtime-task-pane-shortcuts.ts +323 -123
- package/src/services/runtime-workspace-observed-effect-queue.ts +25 -0
- package/src/services/runtime-workspace-observed-events.ts +33 -184
- package/src/services/runtime-workspace-observed-transition-policy.ts +228 -0
- package/src/services/session-diagnostics-store.ts +217 -0
- package/src/services/startup-background-resume.ts +26 -21
- package/src/services/startup-orchestrator.ts +16 -13
- package/src/services/startup-paint-tracker.ts +29 -21
- package/src/services/startup-persisted-conversation-queue.ts +19 -13
- package/src/services/startup-settled-gate.ts +25 -15
- package/src/services/startup-shutdown.ts +18 -22
- package/src/services/startup-state-hydration.ts +44 -34
- package/src/services/startup-visibility.ts +12 -4
- package/src/services/task-pane-selection-actions.ts +89 -72
- package/src/services/task-planning-hydration.ts +24 -18
- package/src/services/task-planning-observed-events.ts +50 -52
- package/src/services/workspace-observed-events.ts +66 -63
- package/src/storage/storage-lifecycle-core.ts +438 -0
- package/src/store/control-plane-store-normalize.ts +33 -242
- package/src/store/control-plane-store-types.ts +1 -35
- package/src/store/control-plane-store.ts +360 -56
- package/src/store/event-store.ts +366 -8
- package/src/terminal/snapshot-oracle.ts +207 -94
- package/src/ui/mux-theme.ts +112 -8
- package/src/ui/panes/home-gridfire.ts +40 -31
- package/src/ui/panes/home.ts +10 -2
- package/src/ui/panes/nim.ts +315 -0
- package/src/mux/live-mux/actions-task.ts +0 -115
- package/src/mux/live-mux/left-rail-actions.ts +0 -118
- package/src/mux/live-mux/left-rail-conversation-click.ts +0 -85
- package/src/mux/live-mux/left-rail-pointer.ts +0 -74
- package/src/mux/live-mux/task-pane-shortcuts.ts +0 -206
- package/src/services/runtime-directory-actions.ts +0 -164
- package/src/services/runtime-input-pipeline.ts +0 -50
- package/src/services/runtime-input-router.ts +0 -195
- package/src/services/runtime-main-pane-input.ts +0 -230
- package/src/services/runtime-modal-input.ts +0 -137
- package/src/services/runtime-navigation-input.ts +0 -197
- package/src/services/runtime-rail-input.ts +0 -279
- package/src/services/runtime-task-pane.ts +0 -62
- package/src/services/runtime-workspace-actions.ts +0 -158
- package/src/ui/conversation-input-forwarder.ts +0 -114
- package/src/ui/conversation-selection-input.ts +0 -103
- package/src/ui/global-shortcut-input.ts +0 -89
- package/src/ui/input.ts +0 -269
- package/src/ui/kit.ts +0 -509
- package/src/ui/left-nav-input.ts +0 -80
- package/src/ui/left-rail-pointer-input.ts +0 -148
- package/src/ui/modals/manager.ts +0 -218
- package/src/ui/repository-fold-input.ts +0 -91
- package/src/ui/surface.ts +0 -224
|
@@ -17,7 +17,7 @@ type RoutedInputToken =
|
|
|
17
17
|
|
|
18
18
|
interface RouteInputTokensForConversationOptions {
|
|
19
19
|
tokens: readonly RoutedInputToken[];
|
|
20
|
-
mainPaneMode: 'conversation' | 'project' | 'home';
|
|
20
|
+
mainPaneMode: 'conversation' | 'project' | 'home' | 'nim';
|
|
21
21
|
normalizeMuxKeyboardInputForPty: (input: Buffer) => Buffer;
|
|
22
22
|
classifyPaneAt: (col: number, row: number) => string;
|
|
23
23
|
wheelDeltaRowsFromCode: (code: number) => number | null;
|
|
@@ -27,8 +27,31 @@ interface RouteInputTokensForConversationOptions {
|
|
|
27
27
|
rightCols: number;
|
|
28
28
|
rightStartCol: number;
|
|
29
29
|
};
|
|
30
|
-
snapshotForInput:
|
|
30
|
+
snapshotForInput:
|
|
31
|
+
| (Pick<TerminalSnapshotFrameCore, 'activeScreen' | 'viewport'> & {
|
|
32
|
+
lines?: readonly string[];
|
|
33
|
+
})
|
|
34
|
+
| null;
|
|
31
35
|
appMouseTrackingEnabled: boolean;
|
|
36
|
+
hasMetaModifier?: (code: number) => boolean;
|
|
37
|
+
handleMetaClick?: (input: {
|
|
38
|
+
event: {
|
|
39
|
+
col: number;
|
|
40
|
+
row: number;
|
|
41
|
+
code: number;
|
|
42
|
+
final: 'M' | 'm';
|
|
43
|
+
};
|
|
44
|
+
layout: {
|
|
45
|
+
paneRows: number;
|
|
46
|
+
rightCols: number;
|
|
47
|
+
rightStartCol: number;
|
|
48
|
+
};
|
|
49
|
+
snapshotForInput:
|
|
50
|
+
| (Pick<TerminalSnapshotFrameCore, 'activeScreen' | 'viewport'> & {
|
|
51
|
+
lines?: readonly string[];
|
|
52
|
+
})
|
|
53
|
+
| null;
|
|
54
|
+
}) => boolean;
|
|
32
55
|
}
|
|
33
56
|
|
|
34
57
|
interface RouteInputTokensForConversationResult {
|
|
@@ -40,6 +63,28 @@ function encodeSgrMouseEvent(code: number, col: number, row: number, final: 'M'
|
|
|
40
63
|
return Buffer.from(`\u001b[<${String(code)};${String(col)};${String(row)}${final}`, 'utf8');
|
|
41
64
|
}
|
|
42
65
|
|
|
66
|
+
function isWheelMouseCode(code: number): boolean {
|
|
67
|
+
return (code & 0b0100_0000) !== 0;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
function isMotionMouseCode(code: number): boolean {
|
|
71
|
+
return (code & 0b0010_0000) !== 0;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
function isLeftButtonPress(code: number, final: 'M' | 'm'): boolean {
|
|
75
|
+
if (final !== 'M') {
|
|
76
|
+
return false;
|
|
77
|
+
}
|
|
78
|
+
if (isWheelMouseCode(code) || isMotionMouseCode(code)) {
|
|
79
|
+
return false;
|
|
80
|
+
}
|
|
81
|
+
return (code & 0b0000_0011) === 0;
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
function hasMetaModifier(code: number): boolean {
|
|
85
|
+
return (code & 0b0000_1000) !== 0;
|
|
86
|
+
}
|
|
87
|
+
|
|
43
88
|
function shouldPassThroughMouseToConversation(
|
|
44
89
|
options: Pick<
|
|
45
90
|
RouteInputTokensForConversationOptions,
|
|
@@ -85,6 +130,18 @@ export function routeInputTokensForConversation(
|
|
|
85
130
|
if (options.mainPaneMode !== 'conversation') {
|
|
86
131
|
continue;
|
|
87
132
|
}
|
|
133
|
+
if (
|
|
134
|
+
options.handleMetaClick !== undefined &&
|
|
135
|
+
isLeftButtonPress(token.event.code, token.event.final) &&
|
|
136
|
+
(options.hasMetaModifier ?? hasMetaModifier)(token.event.code)
|
|
137
|
+
) {
|
|
138
|
+
const handled = options.handleMetaClick({
|
|
139
|
+
event: token.event,
|
|
140
|
+
layout: options.layout,
|
|
141
|
+
snapshotForInput: options.snapshotForInput,
|
|
142
|
+
});
|
|
143
|
+
if (handled) continue;
|
|
144
|
+
}
|
|
88
145
|
if (shouldPassThroughMouseToConversation(options, token.event.code)) {
|
|
89
146
|
const sessionCol = Math.max(
|
|
90
147
|
1,
|
|
@@ -5,33 +5,81 @@ interface ActivateLeftNavTargetOptions {
|
|
|
5
5
|
target: LeftNavSelection;
|
|
6
6
|
direction: 'next' | 'previous';
|
|
7
7
|
enterHomePane: () => void;
|
|
8
|
+
enterNimPane?: () => void;
|
|
9
|
+
enterTasksPane?: () => void;
|
|
8
10
|
firstDirectoryForRepositoryGroup: (repositoryGroupId: string) => string | null;
|
|
9
11
|
enterProjectPane: (directoryId: string) => void;
|
|
12
|
+
enterGitHubPane?: (directoryId: string) => void;
|
|
10
13
|
setMainPaneProjectMode: () => void;
|
|
11
14
|
selectLeftNavRepository: (repositoryGroupId: string) => void;
|
|
15
|
+
selectLeftNavConversation?: (sessionId: string) => void;
|
|
12
16
|
markDirty: () => void;
|
|
13
17
|
directoriesHas: (directoryId: string) => boolean;
|
|
14
18
|
visibleTargetsForState: () => readonly LeftNavSelection[];
|
|
15
19
|
conversationDirectoryId: (sessionId: string) => string | null;
|
|
16
20
|
queueControlPlaneOp: (task: () => Promise<void>, label: string) => void;
|
|
17
|
-
|
|
21
|
+
queueLatestControlPlaneOp?: (
|
|
22
|
+
key: string,
|
|
23
|
+
task: (options: { readonly signal: AbortSignal }) => Promise<void>,
|
|
24
|
+
label: string,
|
|
25
|
+
) => void;
|
|
26
|
+
activateConversation: (
|
|
27
|
+
sessionId: string,
|
|
28
|
+
options?: { readonly signal?: AbortSignal },
|
|
29
|
+
) => Promise<void>;
|
|
18
30
|
conversationsHas: (sessionId: string) => boolean;
|
|
19
31
|
}
|
|
20
32
|
|
|
33
|
+
const LEFT_NAV_ACTIVATION_KEY = 'left-nav:activate-conversation';
|
|
34
|
+
|
|
35
|
+
function queueLeftNavConversationActivation(
|
|
36
|
+
options: Pick<
|
|
37
|
+
ActivateLeftNavTargetOptions,
|
|
38
|
+
'queueControlPlaneOp' | 'queueLatestControlPlaneOp' | 'activateConversation'
|
|
39
|
+
> & {
|
|
40
|
+
readonly sessionId: string;
|
|
41
|
+
readonly label: string;
|
|
42
|
+
},
|
|
43
|
+
): void {
|
|
44
|
+
if (options.queueLatestControlPlaneOp !== undefined) {
|
|
45
|
+
options.queueLatestControlPlaneOp(
|
|
46
|
+
LEFT_NAV_ACTIVATION_KEY,
|
|
47
|
+
async ({ signal }) => {
|
|
48
|
+
if (signal.aborted) {
|
|
49
|
+
return;
|
|
50
|
+
}
|
|
51
|
+
await options.activateConversation(options.sessionId, {
|
|
52
|
+
signal,
|
|
53
|
+
});
|
|
54
|
+
},
|
|
55
|
+
options.label,
|
|
56
|
+
);
|
|
57
|
+
return;
|
|
58
|
+
}
|
|
59
|
+
options.queueControlPlaneOp(async () => {
|
|
60
|
+
await options.activateConversation(options.sessionId);
|
|
61
|
+
}, options.label);
|
|
62
|
+
}
|
|
63
|
+
|
|
21
64
|
export function activateLeftNavTarget(options: ActivateLeftNavTargetOptions): void {
|
|
22
65
|
const {
|
|
23
66
|
target,
|
|
24
67
|
direction,
|
|
25
68
|
enterHomePane,
|
|
69
|
+
enterNimPane,
|
|
70
|
+
enterTasksPane,
|
|
26
71
|
firstDirectoryForRepositoryGroup,
|
|
27
72
|
enterProjectPane,
|
|
73
|
+
enterGitHubPane,
|
|
28
74
|
setMainPaneProjectMode,
|
|
29
75
|
selectLeftNavRepository,
|
|
76
|
+
selectLeftNavConversation,
|
|
30
77
|
markDirty,
|
|
31
78
|
directoriesHas,
|
|
32
79
|
visibleTargetsForState,
|
|
33
80
|
conversationDirectoryId,
|
|
34
81
|
queueControlPlaneOp,
|
|
82
|
+
queueLatestControlPlaneOp,
|
|
35
83
|
activateConversation,
|
|
36
84
|
conversationsHas,
|
|
37
85
|
} = options;
|
|
@@ -39,6 +87,22 @@ export function activateLeftNavTarget(options: ActivateLeftNavTargetOptions): vo
|
|
|
39
87
|
enterHomePane();
|
|
40
88
|
return;
|
|
41
89
|
}
|
|
90
|
+
if (target.kind === 'nim') {
|
|
91
|
+
if (enterNimPane !== undefined) {
|
|
92
|
+
enterNimPane();
|
|
93
|
+
return;
|
|
94
|
+
}
|
|
95
|
+
enterHomePane();
|
|
96
|
+
return;
|
|
97
|
+
}
|
|
98
|
+
if (target.kind === 'tasks') {
|
|
99
|
+
if (enterTasksPane !== undefined) {
|
|
100
|
+
enterTasksPane();
|
|
101
|
+
return;
|
|
102
|
+
}
|
|
103
|
+
enterHomePane();
|
|
104
|
+
return;
|
|
105
|
+
}
|
|
42
106
|
if (target.kind === 'repository') {
|
|
43
107
|
const firstDirectoryId = firstDirectoryForRepositoryGroup(target.repositoryId);
|
|
44
108
|
if (firstDirectoryId !== null) {
|
|
@@ -63,18 +127,71 @@ export function activateLeftNavTarget(options: ActivateLeftNavTargetOptions): vo
|
|
|
63
127
|
conversationDirectoryId(entry.sessionId) === target.directoryId,
|
|
64
128
|
);
|
|
65
129
|
if (fallbackConversation !== undefined) {
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
130
|
+
selectLeftNavConversation?.(fallbackConversation.sessionId);
|
|
131
|
+
markDirty();
|
|
132
|
+
queueLeftNavConversationActivation({
|
|
133
|
+
queueControlPlaneOp,
|
|
134
|
+
...(queueLatestControlPlaneOp === undefined
|
|
135
|
+
? {}
|
|
136
|
+
: {
|
|
137
|
+
queueLatestControlPlaneOp,
|
|
138
|
+
}),
|
|
139
|
+
activateConversation,
|
|
140
|
+
sessionId: fallbackConversation.sessionId,
|
|
141
|
+
label: `shortcut-activate-${direction}-directory-fallback`,
|
|
142
|
+
});
|
|
143
|
+
}
|
|
144
|
+
return;
|
|
145
|
+
}
|
|
146
|
+
if (target.kind === 'github') {
|
|
147
|
+
if (directoriesHas(target.directoryId)) {
|
|
148
|
+
if (enterGitHubPane !== undefined) {
|
|
149
|
+
enterGitHubPane(target.directoryId);
|
|
150
|
+
} else {
|
|
151
|
+
enterProjectPane(target.directoryId);
|
|
152
|
+
}
|
|
153
|
+
markDirty();
|
|
154
|
+
return;
|
|
155
|
+
}
|
|
156
|
+
const visibleTargets = visibleTargetsForState();
|
|
157
|
+
const fallbackConversation = visibleTargets.find(
|
|
158
|
+
(entry): entry is Extract<LeftNavSelection, { kind: 'conversation' }> =>
|
|
159
|
+
entry.kind === 'conversation' &&
|
|
160
|
+
conversationDirectoryId(entry.sessionId) === target.directoryId,
|
|
161
|
+
);
|
|
162
|
+
if (fallbackConversation !== undefined) {
|
|
163
|
+
selectLeftNavConversation?.(fallbackConversation.sessionId);
|
|
164
|
+
markDirty();
|
|
165
|
+
queueLeftNavConversationActivation({
|
|
166
|
+
queueControlPlaneOp,
|
|
167
|
+
...(queueLatestControlPlaneOp === undefined
|
|
168
|
+
? {}
|
|
169
|
+
: {
|
|
170
|
+
queueLatestControlPlaneOp,
|
|
171
|
+
}),
|
|
172
|
+
activateConversation,
|
|
173
|
+
sessionId: fallbackConversation.sessionId,
|
|
174
|
+
label: `shortcut-activate-${direction}-github-fallback`,
|
|
175
|
+
});
|
|
69
176
|
}
|
|
70
177
|
return;
|
|
71
178
|
}
|
|
72
179
|
if (!conversationsHas(target.sessionId)) {
|
|
73
180
|
return;
|
|
74
181
|
}
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
182
|
+
selectLeftNavConversation?.(target.sessionId);
|
|
183
|
+
markDirty();
|
|
184
|
+
queueLeftNavConversationActivation({
|
|
185
|
+
queueControlPlaneOp,
|
|
186
|
+
...(queueLatestControlPlaneOp === undefined
|
|
187
|
+
? {}
|
|
188
|
+
: {
|
|
189
|
+
queueLatestControlPlaneOp,
|
|
190
|
+
}),
|
|
191
|
+
activateConversation,
|
|
192
|
+
sessionId: target.sessionId,
|
|
193
|
+
label: `shortcut-activate-${direction}`,
|
|
194
|
+
});
|
|
78
195
|
}
|
|
79
196
|
|
|
80
197
|
interface CycleLeftNavSelectionOptions {
|
|
@@ -4,6 +4,12 @@ export type LeftNavSelection =
|
|
|
4
4
|
| {
|
|
5
5
|
readonly kind: 'home';
|
|
6
6
|
}
|
|
7
|
+
| {
|
|
8
|
+
readonly kind: 'nim';
|
|
9
|
+
}
|
|
10
|
+
| {
|
|
11
|
+
readonly kind: 'tasks';
|
|
12
|
+
}
|
|
7
13
|
| {
|
|
8
14
|
readonly kind: 'repository';
|
|
9
15
|
readonly repositoryId: string;
|
|
@@ -12,6 +18,10 @@ export type LeftNavSelection =
|
|
|
12
18
|
readonly kind: 'project';
|
|
13
19
|
readonly directoryId: string;
|
|
14
20
|
}
|
|
21
|
+
| {
|
|
22
|
+
readonly kind: 'github';
|
|
23
|
+
readonly directoryId: string;
|
|
24
|
+
}
|
|
15
25
|
| {
|
|
16
26
|
readonly kind: 'conversation';
|
|
17
27
|
readonly sessionId: string;
|
|
@@ -21,12 +31,21 @@ export function leftNavTargetKey(target: LeftNavSelection): string {
|
|
|
21
31
|
if (target.kind === 'home') {
|
|
22
32
|
return 'home';
|
|
23
33
|
}
|
|
34
|
+
if (target.kind === 'nim') {
|
|
35
|
+
return 'nim';
|
|
36
|
+
}
|
|
37
|
+
if (target.kind === 'tasks') {
|
|
38
|
+
return 'tasks';
|
|
39
|
+
}
|
|
24
40
|
if (target.kind === 'repository') {
|
|
25
41
|
return `repository:${target.repositoryId}`;
|
|
26
42
|
}
|
|
27
43
|
if (target.kind === 'project') {
|
|
28
44
|
return `directory:${target.directoryId}`;
|
|
29
45
|
}
|
|
46
|
+
if (target.kind === 'github') {
|
|
47
|
+
return `github:${target.directoryId}`;
|
|
48
|
+
}
|
|
30
49
|
return `conversation:${target.sessionId}`;
|
|
31
50
|
}
|
|
32
51
|
|
|
@@ -43,6 +62,16 @@ function leftNavTargetFromRow(
|
|
|
43
62
|
kind: 'home',
|
|
44
63
|
};
|
|
45
64
|
}
|
|
65
|
+
if (row.railAction === 'nim.open') {
|
|
66
|
+
return {
|
|
67
|
+
kind: 'nim',
|
|
68
|
+
};
|
|
69
|
+
}
|
|
70
|
+
if (row.railAction === 'tasks.open') {
|
|
71
|
+
return {
|
|
72
|
+
kind: 'tasks',
|
|
73
|
+
};
|
|
74
|
+
}
|
|
46
75
|
if (row.kind === 'repository-header' && row.repositoryId !== null) {
|
|
47
76
|
return {
|
|
48
77
|
kind: 'repository',
|
|
@@ -55,6 +84,12 @@ function leftNavTargetFromRow(
|
|
|
55
84
|
directoryId: row.directoryKey,
|
|
56
85
|
};
|
|
57
86
|
}
|
|
87
|
+
if (row.kind === 'github-header' && row.directoryKey !== null) {
|
|
88
|
+
return {
|
|
89
|
+
kind: 'github',
|
|
90
|
+
directoryId: row.directoryKey,
|
|
91
|
+
};
|
|
92
|
+
}
|
|
58
93
|
if (row.kind === 'conversation-title' && row.conversationSessionId !== null) {
|
|
59
94
|
return {
|
|
60
95
|
kind: 'conversation',
|
|
@@ -0,0 +1,292 @@
|
|
|
1
|
+
import { isAbsolute, resolve } from 'node:path';
|
|
2
|
+
import { fileURLToPath } from 'node:url';
|
|
3
|
+
import type { ResolvedCommandMenuOpenInTarget } from './command-menu-open-in.ts';
|
|
4
|
+
|
|
5
|
+
const TOKEN_REGEX = /\S+/gu;
|
|
6
|
+
const LEADING_WRAP_CHARS = new Set(['"', "'", '`', '(', '[', '{', '<']);
|
|
7
|
+
const TRAILING_WRAP_CHARS = new Set(['"', "'", '`', ')', ']', '}', '>', ',', '.', ';', '!', '?']);
|
|
8
|
+
const EDITOR_LINE_TARGET_IDS = new Set(['zed', 'cursor', 'vscode']);
|
|
9
|
+
|
|
10
|
+
type TerminalLinkTarget = TerminalUrlLinkTarget | TerminalFileLinkTarget;
|
|
11
|
+
|
|
12
|
+
interface TerminalUrlLinkTarget {
|
|
13
|
+
readonly kind: 'url';
|
|
14
|
+
readonly url: string;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
interface TerminalFileLinkTarget {
|
|
18
|
+
readonly kind: 'file';
|
|
19
|
+
readonly path: string;
|
|
20
|
+
readonly line: number | null;
|
|
21
|
+
readonly column: number | null;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
function trimTokenWrapper(token: string): string {
|
|
25
|
+
let start = 0;
|
|
26
|
+
let end = token.length;
|
|
27
|
+
while (start < end && LEADING_WRAP_CHARS.has(token[start]!)) {
|
|
28
|
+
start += 1;
|
|
29
|
+
}
|
|
30
|
+
while (end > start && TRAILING_WRAP_CHARS.has(token[end - 1]!)) {
|
|
31
|
+
end -= 1;
|
|
32
|
+
}
|
|
33
|
+
return token.slice(start, end);
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
function parsePositiveInteger(value: string): number | null {
|
|
37
|
+
const parsed = Number.parseInt(value, 10);
|
|
38
|
+
if (!Number.isFinite(parsed) || parsed <= 0) {
|
|
39
|
+
return null;
|
|
40
|
+
}
|
|
41
|
+
return parsed;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
function splitTrailingLineColumn(value: string): {
|
|
45
|
+
readonly path: string;
|
|
46
|
+
readonly line: number | null;
|
|
47
|
+
readonly column: number | null;
|
|
48
|
+
} {
|
|
49
|
+
let remainder = value;
|
|
50
|
+
let column: number | null = null;
|
|
51
|
+
let line: number | null = null;
|
|
52
|
+
|
|
53
|
+
const trailingColumn = /:(\d+)$/u.exec(remainder);
|
|
54
|
+
if (trailingColumn !== null) {
|
|
55
|
+
const parsed = parsePositiveInteger(trailingColumn[1]!);
|
|
56
|
+
if (parsed !== null) {
|
|
57
|
+
column = parsed;
|
|
58
|
+
remainder = remainder.slice(0, -trailingColumn[0].length);
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
const trailingLine = /:(\d+)$/u.exec(remainder);
|
|
63
|
+
if (trailingLine !== null) {
|
|
64
|
+
const parsed = parsePositiveInteger(trailingLine[1]!);
|
|
65
|
+
if (parsed !== null) {
|
|
66
|
+
line = parsed;
|
|
67
|
+
remainder = remainder.slice(0, -trailingLine[0].length);
|
|
68
|
+
}
|
|
69
|
+
} else if (column !== null) {
|
|
70
|
+
line = column;
|
|
71
|
+
column = null;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
return {
|
|
75
|
+
path: remainder,
|
|
76
|
+
line,
|
|
77
|
+
column,
|
|
78
|
+
};
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
function parseUrlTarget(candidate: string): TerminalUrlLinkTarget | null {
|
|
82
|
+
if (!/^https?:\/\//iu.test(candidate)) {
|
|
83
|
+
return null;
|
|
84
|
+
}
|
|
85
|
+
let parsed: URL;
|
|
86
|
+
try {
|
|
87
|
+
parsed = new URL(candidate);
|
|
88
|
+
} catch {
|
|
89
|
+
return null;
|
|
90
|
+
}
|
|
91
|
+
return {
|
|
92
|
+
kind: 'url',
|
|
93
|
+
url: parsed.toString(),
|
|
94
|
+
};
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
function looksLikeFilePath(pathLike: string, hasLineOrColumnSuffix: boolean): boolean {
|
|
98
|
+
if (pathLike.length === 0 || /\s/u.test(pathLike) || pathLike.includes('://')) {
|
|
99
|
+
return false;
|
|
100
|
+
}
|
|
101
|
+
if (
|
|
102
|
+
pathLike.startsWith('/') ||
|
|
103
|
+
pathLike.startsWith('./') ||
|
|
104
|
+
pathLike.startsWith('../') ||
|
|
105
|
+
pathLike.startsWith('~/')
|
|
106
|
+
) {
|
|
107
|
+
return true;
|
|
108
|
+
}
|
|
109
|
+
if (pathLike.includes('/') || pathLike.includes('\\') || /^[A-Za-z]:[\\/]/u.test(pathLike)) {
|
|
110
|
+
return true;
|
|
111
|
+
}
|
|
112
|
+
if (hasLineOrColumnSuffix) {
|
|
113
|
+
return /^[A-Za-z0-9._-]+$/u.test(pathLike);
|
|
114
|
+
}
|
|
115
|
+
return /^[A-Za-z0-9._-]+\.[A-Za-z0-9._-]+$/u.test(pathLike);
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
function parseFileTarget(candidate: string): TerminalFileLinkTarget | null {
|
|
119
|
+
if (candidate.startsWith('file://')) {
|
|
120
|
+
try {
|
|
121
|
+
const fileUrl = new URL(candidate);
|
|
122
|
+
return {
|
|
123
|
+
kind: 'file',
|
|
124
|
+
path: fileURLToPath(fileUrl),
|
|
125
|
+
line: null,
|
|
126
|
+
column: null,
|
|
127
|
+
};
|
|
128
|
+
} catch {
|
|
129
|
+
return null;
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
const split = splitTrailingLineColumn(candidate);
|
|
133
|
+
const hasLineOrColumnSuffix = split.line !== null || split.column !== null;
|
|
134
|
+
if (!looksLikeFilePath(split.path, hasLineOrColumnSuffix)) {
|
|
135
|
+
return null;
|
|
136
|
+
}
|
|
137
|
+
return {
|
|
138
|
+
kind: 'file',
|
|
139
|
+
path: split.path,
|
|
140
|
+
line: split.line,
|
|
141
|
+
column: split.column,
|
|
142
|
+
};
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
export function resolveTerminalLinkTargetAtCell(options: {
|
|
146
|
+
readonly lines: readonly string[];
|
|
147
|
+
readonly row: number;
|
|
148
|
+
readonly col: number;
|
|
149
|
+
}): TerminalLinkTarget | null {
|
|
150
|
+
if (options.row < 1 || options.col < 1) {
|
|
151
|
+
return null;
|
|
152
|
+
}
|
|
153
|
+
const line = options.lines[options.row - 1];
|
|
154
|
+
if (line === undefined || line.length === 0) {
|
|
155
|
+
return null;
|
|
156
|
+
}
|
|
157
|
+
const targetColIndex = options.col - 1;
|
|
158
|
+
for (const match of line.matchAll(TOKEN_REGEX)) {
|
|
159
|
+
const token = match[0];
|
|
160
|
+
const start = match.index ?? -1;
|
|
161
|
+
const end = start + token.length;
|
|
162
|
+
if (start < 0 || targetColIndex < start || targetColIndex >= end) {
|
|
163
|
+
continue;
|
|
164
|
+
}
|
|
165
|
+
const trimmed = trimTokenWrapper(token);
|
|
166
|
+
if (trimmed.length === 0) {
|
|
167
|
+
return null;
|
|
168
|
+
}
|
|
169
|
+
const urlTarget = parseUrlTarget(trimmed);
|
|
170
|
+
if (urlTarget !== null) {
|
|
171
|
+
return urlTarget;
|
|
172
|
+
}
|
|
173
|
+
return parseFileTarget(trimmed);
|
|
174
|
+
}
|
|
175
|
+
return null;
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
export function resolveFileLinkPath(options: {
|
|
179
|
+
readonly path: string;
|
|
180
|
+
readonly directoryPath: string | null;
|
|
181
|
+
readonly homeDirectory: string;
|
|
182
|
+
}): string {
|
|
183
|
+
const normalized = options.path.trim();
|
|
184
|
+
if (normalized.startsWith('~/')) {
|
|
185
|
+
return resolve(options.homeDirectory, normalized.slice(2));
|
|
186
|
+
}
|
|
187
|
+
if (isAbsolute(normalized) || /^[A-Za-z]:[\\/]/u.test(normalized)) {
|
|
188
|
+
return normalized;
|
|
189
|
+
}
|
|
190
|
+
if (options.directoryPath !== null && options.directoryPath.trim().length > 0) {
|
|
191
|
+
return resolve(options.directoryPath, normalized);
|
|
192
|
+
}
|
|
193
|
+
return normalized;
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
export function buildFileLinkPathArgumentForTarget(options: {
|
|
197
|
+
readonly targetId: string;
|
|
198
|
+
readonly path: string;
|
|
199
|
+
readonly line: number | null;
|
|
200
|
+
readonly column: number | null;
|
|
201
|
+
}): string {
|
|
202
|
+
if (options.line === null || !EDITOR_LINE_TARGET_IDS.has(options.targetId)) {
|
|
203
|
+
return options.path;
|
|
204
|
+
}
|
|
205
|
+
if (options.column === null) {
|
|
206
|
+
return `${options.path}:${String(options.line)}`;
|
|
207
|
+
}
|
|
208
|
+
return `${options.path}:${String(options.line)}:${String(options.column)}`;
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
export function prioritizeOpenInTargetsForFileLinks(
|
|
212
|
+
targets: readonly ResolvedCommandMenuOpenInTarget[],
|
|
213
|
+
): readonly ResolvedCommandMenuOpenInTarget[] {
|
|
214
|
+
const preferred = targets.filter((target) => target.id === 'zed');
|
|
215
|
+
const remainder = targets.filter((target) => target.id !== 'zed');
|
|
216
|
+
return [...preferred, ...remainder];
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
function valueForPlaceholder(
|
|
220
|
+
placeholder: string,
|
|
221
|
+
values: {
|
|
222
|
+
readonly path?: string | null;
|
|
223
|
+
readonly url?: string | null;
|
|
224
|
+
readonly line?: number | null;
|
|
225
|
+
readonly column?: number | null;
|
|
226
|
+
},
|
|
227
|
+
): string | null {
|
|
228
|
+
if (placeholder === '{path}') {
|
|
229
|
+
const value = values.path;
|
|
230
|
+
return typeof value === 'string' && value.length > 0 ? value : null;
|
|
231
|
+
}
|
|
232
|
+
if (placeholder === '{url}') {
|
|
233
|
+
const value = values.url;
|
|
234
|
+
return typeof value === 'string' && value.length > 0 ? value : null;
|
|
235
|
+
}
|
|
236
|
+
if (placeholder === '{line}') {
|
|
237
|
+
return typeof values.line === 'number' && values.line > 0 ? String(values.line) : null;
|
|
238
|
+
}
|
|
239
|
+
if (placeholder === '{column}') {
|
|
240
|
+
return typeof values.column === 'number' && values.column > 0 ? String(values.column) : null;
|
|
241
|
+
}
|
|
242
|
+
return null;
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
export function resolveLinkCommandFromTemplate(options: {
|
|
246
|
+
readonly template: readonly string[] | null;
|
|
247
|
+
readonly values: {
|
|
248
|
+
readonly path?: string | null;
|
|
249
|
+
readonly url?: string | null;
|
|
250
|
+
readonly line?: number | null;
|
|
251
|
+
readonly column?: number | null;
|
|
252
|
+
};
|
|
253
|
+
readonly appendPrimaryPlaceholder: '{path}' | '{url}';
|
|
254
|
+
}): { command: string; args: readonly string[] } | null {
|
|
255
|
+
if (options.template === null || options.template.length === 0) {
|
|
256
|
+
return null;
|
|
257
|
+
}
|
|
258
|
+
const command = options.template[0]?.trim() ?? '';
|
|
259
|
+
if (command.length === 0) {
|
|
260
|
+
return null;
|
|
261
|
+
}
|
|
262
|
+
const args: string[] = [];
|
|
263
|
+
let injectedPrimary = false;
|
|
264
|
+
for (const rawPart of options.template.slice(1)) {
|
|
265
|
+
const part = rawPart.trim();
|
|
266
|
+
if (part.length === 0) {
|
|
267
|
+
continue;
|
|
268
|
+
}
|
|
269
|
+
const placeholderValue = valueForPlaceholder(part, options.values);
|
|
270
|
+
if (placeholderValue !== null) {
|
|
271
|
+
args.push(placeholderValue);
|
|
272
|
+
if (part === options.appendPrimaryPlaceholder) {
|
|
273
|
+
injectedPrimary = true;
|
|
274
|
+
}
|
|
275
|
+
continue;
|
|
276
|
+
}
|
|
277
|
+
if (part === '{path}' || part === '{url}' || part === '{line}' || part === '{column}') {
|
|
278
|
+
continue;
|
|
279
|
+
}
|
|
280
|
+
args.push(part);
|
|
281
|
+
}
|
|
282
|
+
if (!injectedPrimary) {
|
|
283
|
+
const fallbackValue = valueForPlaceholder(options.appendPrimaryPlaceholder, options.values);
|
|
284
|
+
if (fallbackValue !== null) {
|
|
285
|
+
args.push(fallbackValue);
|
|
286
|
+
}
|
|
287
|
+
}
|
|
288
|
+
return {
|
|
289
|
+
command,
|
|
290
|
+
args,
|
|
291
|
+
};
|
|
292
|
+
}
|