Link terminal file paths to workspace previews (#1174)
This commit is contained in:
@@ -14,6 +14,10 @@ import type { ITheme } from "@xterm/xterm";
|
||||
import type { TerminalState } from "@server/shared/messages";
|
||||
import type { TerminalInputModeState } from "@server/shared/terminal-input-mode";
|
||||
import type { TerminalOutputData } from "../terminal/runtime/terminal-emulator-runtime";
|
||||
import type {
|
||||
TerminalLocalFileLinkSource,
|
||||
TerminalLocalFileLinkTarget,
|
||||
} from "../terminal/local-links/terminal-local-link-provider";
|
||||
import { terminalEmulatorWebViewHtml } from "../terminal/webview/terminal-emulator-webview-html";
|
||||
import type { PendingTerminalModifiers } from "../utils/terminal-keys";
|
||||
import type { TerminalRendererReadyChange } from "../utils/terminal-renderer-readiness";
|
||||
@@ -48,6 +52,13 @@ interface TerminalEmulatorProps {
|
||||
}) => Promise<void> | void;
|
||||
onPendingModifiersConsumed?: () => Promise<void> | void;
|
||||
onInputModeChange?: (state: TerminalInputModeState) => Promise<void> | void;
|
||||
onResolveLocalFileLink?: (
|
||||
source: TerminalLocalFileLinkSource,
|
||||
) => Promise<TerminalLocalFileLinkTarget | null> | TerminalLocalFileLinkTarget | null;
|
||||
onOpenLocalFileLink?: (
|
||||
target: TerminalLocalFileLinkTarget,
|
||||
disposition: "main" | "side",
|
||||
) => Promise<void> | void;
|
||||
onRendererReadyChange?: (change: TerminalRendererReadyChange) => void;
|
||||
pendingModifiers?: PendingTerminalModifiers;
|
||||
focusRequestToken?: number;
|
||||
@@ -74,7 +85,13 @@ type BridgeInboundMessage =
|
||||
| { type: "setTheme"; streamKey: string; theme: ITheme }
|
||||
| { type: "setScrollback"; streamKey: string; lines: number }
|
||||
| { type: "setPendingModifiers"; streamKey: string; pendingModifiers: PendingTerminalModifiers }
|
||||
| { type: "setSwipeGesturesEnabled"; streamKey: string; enabled: boolean };
|
||||
| { type: "setSwipeGesturesEnabled"; streamKey: string; enabled: boolean }
|
||||
| {
|
||||
type: "resolveLocalFileLinkResponse";
|
||||
streamKey: string;
|
||||
requestId: number;
|
||||
target: TerminalLocalFileLinkTarget | null;
|
||||
};
|
||||
|
||||
type BridgeOutboundMessage =
|
||||
| { type: "bridgeReady" }
|
||||
@@ -93,6 +110,18 @@ type BridgeOutboundMessage =
|
||||
| { type: "pendingModifiersConsumed"; streamKey: string }
|
||||
| { type: "inputModeChange"; streamKey: string; state: TerminalInputModeState }
|
||||
| { type: "openExternalUrl"; streamKey: string; url: string }
|
||||
| {
|
||||
type: "resolveLocalFileLink";
|
||||
streamKey: string;
|
||||
requestId: number;
|
||||
source: TerminalLocalFileLinkSource;
|
||||
}
|
||||
| {
|
||||
type: "openLocalFileLink";
|
||||
streamKey: string;
|
||||
target: TerminalLocalFileLinkTarget;
|
||||
disposition: "main" | "side";
|
||||
}
|
||||
| { type: "swipeLeft"; streamKey: string }
|
||||
| { type: "swipeRight"; streamKey: string }
|
||||
| { type: "debug"; message: string; details?: unknown };
|
||||
@@ -149,6 +178,8 @@ export default function TerminalEmulator({
|
||||
onTerminalKey,
|
||||
onPendingModifiersConsumed,
|
||||
onInputModeChange,
|
||||
onResolveLocalFileLink,
|
||||
onOpenLocalFileLink,
|
||||
onRendererReadyChange,
|
||||
pendingModifiers = { ctrl: false, shift: false, alt: false },
|
||||
focusRequestToken = 0,
|
||||
@@ -188,6 +219,8 @@ export default function TerminalEmulator({
|
||||
onPendingModifiersConsumed,
|
||||
onInputModeChange,
|
||||
onRendererReadyChange,
|
||||
onResolveLocalFileLink,
|
||||
onOpenLocalFileLink,
|
||||
onSwipeLeft,
|
||||
onSwipeRight,
|
||||
});
|
||||
@@ -198,6 +231,8 @@ export default function TerminalEmulator({
|
||||
onPendingModifiersConsumed,
|
||||
onInputModeChange,
|
||||
onRendererReadyChange,
|
||||
onResolveLocalFileLink,
|
||||
onOpenLocalFileLink,
|
||||
onSwipeLeft,
|
||||
onSwipeRight,
|
||||
};
|
||||
@@ -399,10 +434,41 @@ export default function TerminalEmulator({
|
||||
[clearBridgeReadyTimeout, clearRendererReadyTimeout],
|
||||
);
|
||||
|
||||
const resolveLocalFileLink = useCallback(
|
||||
async (message: Extract<BridgeOutboundMessage, { type: "resolveLocalFileLink" }>) => {
|
||||
try {
|
||||
const target = await (callbacksRef.current.onResolveLocalFileLink?.(message.source) ??
|
||||
null);
|
||||
sendToWebView({
|
||||
type: "resolveLocalFileLinkResponse",
|
||||
streamKey: message.streamKey,
|
||||
requestId: message.requestId,
|
||||
target,
|
||||
});
|
||||
} catch {
|
||||
sendToWebView({
|
||||
type: "resolveLocalFileLinkResponse",
|
||||
streamKey: message.streamKey,
|
||||
requestId: message.requestId,
|
||||
target: null,
|
||||
});
|
||||
}
|
||||
},
|
||||
[sendToWebView],
|
||||
);
|
||||
|
||||
const handleTerminalMessage = useCallback(
|
||||
(
|
||||
message: Exclude<BridgeOutboundMessage, { type: "bridgeReady" } | { type: "rendererReady" }>,
|
||||
) => {
|
||||
if (message.type === "resolveLocalFileLink") {
|
||||
void resolveLocalFileLink(message);
|
||||
return;
|
||||
}
|
||||
if (message.type === "openLocalFileLink") {
|
||||
callbacksRef.current.onOpenLocalFileLink?.(message.target, message.disposition);
|
||||
return;
|
||||
}
|
||||
switch (message.type) {
|
||||
case "input":
|
||||
callbacksRef.current.onInput?.(message.data);
|
||||
@@ -438,7 +504,7 @@ export default function TerminalEmulator({
|
||||
break;
|
||||
}
|
||||
},
|
||||
[],
|
||||
[resolveLocalFileLink],
|
||||
);
|
||||
|
||||
const handleMessage = useCallback(
|
||||
|
||||
@@ -23,6 +23,10 @@ import {
|
||||
TerminalEmulatorRuntime,
|
||||
type TerminalOutputData,
|
||||
} from "../terminal/runtime/terminal-emulator-runtime";
|
||||
import type {
|
||||
TerminalLocalFileLinkSource,
|
||||
TerminalLocalFileLinkTarget,
|
||||
} from "../terminal/local-links/terminal-local-link-provider";
|
||||
import type { TerminalRendererReadyChange } from "../utils/terminal-renderer-readiness";
|
||||
import { openExternalUrl } from "../utils/open-external-url";
|
||||
import { focusWithRetries } from "../utils/web-focus";
|
||||
@@ -136,6 +140,13 @@ interface TerminalEmulatorProps {
|
||||
}) => Promise<void> | void;
|
||||
onPendingModifiersConsumed?: () => Promise<void> | void;
|
||||
onInputModeChange?: (state: TerminalInputModeState) => Promise<void> | void;
|
||||
onResolveLocalFileLink?: (
|
||||
source: TerminalLocalFileLinkSource,
|
||||
) => Promise<TerminalLocalFileLinkTarget | null> | TerminalLocalFileLinkTarget | null;
|
||||
onOpenLocalFileLink?: (
|
||||
target: TerminalLocalFileLinkTarget,
|
||||
disposition: "main" | "side",
|
||||
) => Promise<void> | void;
|
||||
onRendererReadyChange?: (change: TerminalRendererReadyChange) => void;
|
||||
pendingModifiers?: PendingTerminalModifiers;
|
||||
focusRequestToken?: number;
|
||||
@@ -203,6 +214,8 @@ export default function TerminalEmulator({
|
||||
onTerminalKey,
|
||||
onPendingModifiersConsumed,
|
||||
onInputModeChange,
|
||||
onResolveLocalFileLink,
|
||||
onOpenLocalFileLink,
|
||||
onRendererReadyChange,
|
||||
pendingModifiers = { ctrl: false, shift: false, alt: false },
|
||||
focusRequestToken = 0,
|
||||
@@ -231,6 +244,8 @@ export default function TerminalEmulator({
|
||||
onTerminalKey,
|
||||
onPendingModifiersConsumed,
|
||||
onInputModeChange,
|
||||
onResolveLocalFileLink,
|
||||
onOpenLocalFileLink,
|
||||
});
|
||||
mountCallbacksRef.current = {
|
||||
onInput,
|
||||
@@ -238,6 +253,8 @@ export default function TerminalEmulator({
|
||||
onTerminalKey,
|
||||
onPendingModifiersConsumed,
|
||||
onInputModeChange,
|
||||
onResolveLocalFileLink,
|
||||
onOpenLocalFileLink,
|
||||
};
|
||||
const initialSnapshotRef = useRef(initialSnapshot);
|
||||
initialSnapshotRef.current = initialSnapshot;
|
||||
@@ -470,10 +487,20 @@ export default function TerminalEmulator({
|
||||
onTerminalKey,
|
||||
onPendingModifiersConsumed,
|
||||
onInputModeChange,
|
||||
onResolveLocalFileLink,
|
||||
onOpenLocalFileLink,
|
||||
onOpenExternalUrl: openExternalUrl,
|
||||
},
|
||||
});
|
||||
}, [onInput, onInputModeChange, onPendingModifiersConsumed, onResize, onTerminalKey]);
|
||||
}, [
|
||||
onInput,
|
||||
onInputModeChange,
|
||||
onOpenLocalFileLink,
|
||||
onPendingModifiersConsumed,
|
||||
onResolveLocalFileLink,
|
||||
onResize,
|
||||
onTerminalKey,
|
||||
]);
|
||||
|
||||
useEffect(() => {
|
||||
runtimeRef.current?.setPendingModifiers({ pendingModifiers });
|
||||
|
||||
@@ -38,6 +38,16 @@ import {
|
||||
type TerminalRendererReadyChange,
|
||||
} from "@/utils/terminal-renderer-readiness";
|
||||
import { useAppSettings } from "@/hooks/use-settings";
|
||||
import { classifyForResolution, fetchDaemonResolution } from "@/assistant-file-links/resolver";
|
||||
import type {
|
||||
TerminalLocalFileLinkSource,
|
||||
TerminalLocalFileLinkTarget,
|
||||
} from "@/terminal/local-links/terminal-local-link-provider";
|
||||
import {
|
||||
normalizeWorkspaceFileLocation,
|
||||
type OpenFileDisposition,
|
||||
type WorkspaceFileOpenRequest,
|
||||
} from "@/workspace/file-open";
|
||||
|
||||
interface TerminalPaneProps {
|
||||
serverId: string;
|
||||
@@ -46,6 +56,7 @@ interface TerminalPaneProps {
|
||||
isWorkspaceFocused: boolean;
|
||||
isPaneFocused: boolean;
|
||||
onOpenFileExplorer: () => void;
|
||||
onOpenWorkspaceFile: (request: WorkspaceFileOpenRequest) => void;
|
||||
}
|
||||
|
||||
const TERMINAL_REFIT_DELAYS_MS = [0, 48, 144, 320];
|
||||
@@ -157,6 +168,7 @@ export function TerminalPane({
|
||||
isWorkspaceFocused,
|
||||
isPaneFocused,
|
||||
onOpenFileExplorer,
|
||||
onOpenWorkspaceFile,
|
||||
}: TerminalPaneProps) {
|
||||
const isAppVisible = useAppVisible();
|
||||
const { theme } = useUnistyles();
|
||||
@@ -630,6 +642,42 @@ export function TerminalPane({
|
||||
const handleInputModeChange = useCallback((state: TerminalInputModeState) => {
|
||||
inputModeRef.current = state;
|
||||
}, []);
|
||||
const handleResolveLocalFileLink = useCallback(
|
||||
async (source: TerminalLocalFileLinkSource): Promise<TerminalLocalFileLinkTarget | null> => {
|
||||
const resolution = classifyForResolution(
|
||||
{ href: source.text, text: source.text, sourceType: "inline-code" },
|
||||
{ workspaceRoot: cwd },
|
||||
);
|
||||
if (resolution.kind === "resolved") {
|
||||
return resolution.value.kind === "file" ? resolution.value.target : null;
|
||||
}
|
||||
if (!client) {
|
||||
return null;
|
||||
}
|
||||
try {
|
||||
return await fetchDaemonResolution({
|
||||
ambiguousQuery: resolution.ambiguousQuery,
|
||||
token: resolution.token,
|
||||
target: resolution.target,
|
||||
workspaceRoot: cwd,
|
||||
getDirectorySuggestions: (input) => client.getDirectorySuggestions(input),
|
||||
});
|
||||
} catch {
|
||||
return null;
|
||||
}
|
||||
},
|
||||
[client, cwd],
|
||||
);
|
||||
const handleOpenLocalFileLink = useCallback(
|
||||
(target: TerminalLocalFileLinkTarget, disposition: OpenFileDisposition) => {
|
||||
const location = normalizeWorkspaceFileLocation(target);
|
||||
if (!location) {
|
||||
return;
|
||||
}
|
||||
onOpenWorkspaceFile({ location, disposition });
|
||||
},
|
||||
[onOpenWorkspaceFile],
|
||||
);
|
||||
|
||||
const toggleModifier = useCallback(
|
||||
(modifier: keyof ModifierState) => {
|
||||
@@ -712,6 +760,8 @@ export function TerminalPane({
|
||||
onResize={handleTerminalResize}
|
||||
onTerminalKey={handleTerminalKey}
|
||||
onInputModeChange={handleInputModeChange}
|
||||
onResolveLocalFileLink={handleResolveLocalFileLink}
|
||||
onOpenLocalFileLink={handleOpenLocalFileLink}
|
||||
onPendingModifiersConsumed={handlePendingModifiersConsumed}
|
||||
pendingModifiers={modifiers}
|
||||
focusRequestToken={focusRequestToken}
|
||||
|
||||
Reference in New Issue
Block a user