Support line ranges in assistant file links (#1067)

This commit is contained in:
Mohamed Boudra
2026-05-17 22:55:46 +08:00
committed by GitHub
parent efc9c2d345
commit ee44b536f5
29 changed files with 504 additions and 115 deletions
@@ -20,7 +20,7 @@ import type { Agent } from "@/stores/session-store";
import { useWorkspaceExecutionAuthority } from "@/stores/session-store-hooks";
import { useWorkspaceDraftSubmissionStore } from "@/stores/workspace-draft-submission-store";
import { encodeImages } from "@/utils/encode-images";
import type { OpenFileDisposition } from "@/utils/workspace-file-open";
import type { WorkspaceFileOpenRequest } from "@/workspace/file-open";
import { shouldAutoFocusWorkspaceDraftComposer } from "@/screens/workspace/workspace-draft-pane-focus";
import type { AgentCapabilityFlags } from "@server/server/agent/agent-sdk-types";
import type { AgentSnapshotPayload } from "@server/shared/messages";
@@ -319,7 +319,7 @@ interface WorkspaceDraftAgentTabProps {
initialSetup?: WorkspaceDraftTabSetup;
isPaneFocused: boolean;
onCreated: (snapshot: AgentSnapshotPayload) => void;
onOpenWorkspaceFile: (input: { filePath: string; disposition: OpenFileDisposition }) => void;
onOpenWorkspaceFile: (request: WorkspaceFileOpenRequest) => void;
onOpenImportSheet?: () => void;
}
@@ -9,7 +9,7 @@ import {
import { getPanelRegistration } from "@/panels/panel-registry";
import { ensurePanelsRegistered } from "@/panels/register-panels";
import type { WorkspaceTabDescriptor } from "@/screens/workspace/workspace-tabs-types";
import type { OpenFileDisposition } from "@/utils/workspace-file-open";
import type { WorkspaceFileOpenRequest } from "@/workspace/file-open";
export interface WorkspacePaneContentModel {
key: string;
@@ -24,7 +24,7 @@ export interface BuildWorkspacePaneContentModelInput {
onOpenTab: (target: WorkspaceTabDescriptor["target"]) => void;
onCloseCurrentTab: () => void;
onRetargetCurrentTab: (target: WorkspaceTabDescriptor["target"]) => void;
onOpenWorkspaceFile: (filePath: string, disposition: OpenFileDisposition) => void;
onOpenWorkspaceFile: (request: WorkspaceFileOpenRequest) => void;
onOpenImportSheet: () => void;
}
@@ -130,6 +130,30 @@ describe("workspace-pane-state", () => {
).toEqual({ kind: "open-in-source" });
});
it("resolves side file opens to an existing file tab when only the line range differs", () => {
const layout: WorkspaceLayout = {
root: {
kind: "pane",
pane: {
id: "main",
tabIds: ["file_/repo/README.md"],
focusedTabId: "file_/repo/README.md",
},
},
focusedPaneId: "main",
};
const tabs = [createTab("file_/repo/README.md", { kind: "file", path: "/repo/README.md" })];
expect(
resolveSideFileOpenPlacement({
layout,
sourcePaneId: "main",
tabs,
target: { kind: "file", path: "/repo/README.md", lineStart: 12, lineEnd: 20 },
}),
).toEqual({ kind: "open-in-source" });
});
it("resolves side file opens to an existing right pane", () => {
const layout: WorkspaceLayout = {
root: {
@@ -9,7 +9,7 @@ import {
buildDeterministicWorkspaceTabId,
normalizeWorkspaceTabTarget,
workspaceTabTargetsEqual,
} from "@/utils/workspace-tab-identity";
} from "@/workspace-tabs/identity";
import { findAdjacentPane } from "@/utils/split-navigation";
export interface WorkspaceDerivedTab {
@@ -209,7 +209,10 @@ export function resolveSideFileOpenPlacement(input: {
tabs: WorkspaceTab[];
target: WorkspaceTabTarget;
}): WorkspaceSideFileOpenPlacement {
const existingTab = input.tabs.find((tab) => workspaceTabTargetsEqual(tab.target, input.target));
const targetTabId = buildDeterministicWorkspaceTabId(input.target);
const existingTab = input.tabs.find(
(tab) => tab.tabId === targetTabId || workspaceTabTargetsEqual(tab.target, input.target),
);
if (existingTab) {
return { kind: "open-in-source" };
}
@@ -77,10 +77,7 @@ import type { WorkspaceTab, WorkspaceTabTarget } from "@/stores/workspace-tabs-s
import { useKeyboardActionHandler } from "@/hooks/use-keyboard-action-handler";
import type { KeyboardActionDefinition } from "@/keyboard/keyboard-action-dispatcher";
import { useCreateFlowStore } from "@/stores/create-flow-store";
import {
normalizeWorkspaceTabTarget,
workspaceTabTargetsEqual,
} from "@/utils/workspace-tab-identity";
import { normalizeWorkspaceTabTarget, workspaceTabTargetsEqual } from "@/workspace-tabs/identity";
import {
getHostRuntimeStore,
useHostRuntimeClient,
@@ -161,6 +158,12 @@ import { useContainerWidthBelow } from "@/hooks/use-container-width";
import { buildHostRootRoute, buildSettingsHostRoute } from "@/utils/host-routes";
import { canCreateWorkspaceTerminal } from "@/screens/workspace/terminals/state";
import { useWorkspaceTerminals } from "@/screens/workspace/terminals/use-workspace-terminals";
import {
createWorkspaceFileTabTarget,
normalizeWorkspaceFileLocation,
type WorkspaceFileLocation,
type WorkspaceFileOpenRequest,
} from "@/workspace/file-open";
const WORKSPACE_SETUP_AUTO_OPEN_WINDOW_MS = 30_000;
const EMPTY_UI_TABS: WorkspaceTab[] = [];
@@ -2023,7 +2026,11 @@ function WorkspaceScreenContent({
if (!persistenceKey) {
return;
}
const tabId = openWorkspaceTabFocused(persistenceKey, { kind: "file", path: filePath });
const location = normalizeWorkspaceFileLocation({ path: filePath });
if (!location) {
return;
}
const tabId = openWorkspaceTabFocused(persistenceKey, createWorkspaceFileTabTarget(location));
if (tabId) {
navigateToTabId(tabId);
}
@@ -2032,28 +2039,40 @@ function WorkspaceScreenContent({
);
const handleOpenFileFromChat = useCallback(
(filePath: string) => {
const normalizedFilePath = filePath.trim();
if (!normalizedFilePath) {
(location: WorkspaceFileLocation) => {
const normalizedLocation = normalizeWorkspaceFileLocation(location);
if (!normalizedLocation) {
return;
}
handleOpenFileFromExplorer(normalizedFilePath);
if (isMobile) {
showMobileAgent();
}
if (!persistenceKey) {
return;
}
const tabId = openWorkspaceTabFocused(
persistenceKey,
createWorkspaceFileTabTarget(normalizedLocation),
);
if (tabId) {
navigateToTabId(tabId);
}
},
[handleOpenFileFromExplorer],
[isMobile, navigateToTabId, openWorkspaceTabFocused, persistenceKey, showMobileAgent],
);
const handleOpenFileFromChatInSidePane = useCallback(
(input: { filePath: string; sourcePaneId?: string }) => {
const normalizedFilePath = input.filePath.trim();
if (!normalizedFilePath) {
(input: { location: WorkspaceFileLocation; sourcePaneId?: string }) => {
const location = normalizeWorkspaceFileLocation(input.location);
if (!location) {
return;
}
if (!persistenceKey || isMobile || !input.sourcePaneId) {
handleOpenFileFromExplorer(normalizedFilePath);
handleOpenFileFromChat(location);
return;
}
const target: WorkspaceTabTarget = { kind: "file", path: normalizedFilePath };
const target: WorkspaceTabTarget = createWorkspaceFileTabTarget(location);
const placement = resolveSideFileOpenPlacement({
layout: workspaceLayout,
sourcePaneId: input.sourcePaneId,
@@ -2075,7 +2094,7 @@ function WorkspaceScreenContent({
}
},
[
handleOpenFileFromExplorer,
handleOpenFileFromChat,
isMobile,
focusWorkspacePane,
navigateToTabId,
@@ -2755,18 +2774,18 @@ function WorkspaceScreenContent({
}
retargetWorkspaceTab(persistenceKey, input.tab.tabId, target);
},
onOpenWorkspaceFile: (filePath, disposition) => {
onOpenWorkspaceFile: (request: WorkspaceFileOpenRequest) => {
if (input.focusPaneBeforeOpen && input.paneId && persistenceKey) {
focusWorkspacePane(persistenceKey, input.paneId);
}
if (disposition === "side") {
if (request.disposition === "side") {
handleOpenFileFromChatInSidePane({
filePath,
location: request.location,
sourcePaneId: input.paneId ?? undefined,
});
return;
}
handleOpenFileFromChat(filePath);
handleOpenFileFromChat(request.location);
},
onOpenImportSheet: openImportSheet,
}),
@@ -3,7 +3,7 @@ import {
deriveWorkspacePaneState,
type WorkspaceDerivedTab,
} from "@/screens/workspace/workspace-pane-state";
import { buildDeterministicWorkspaceTabId } from "@/utils/workspace-tab-identity";
import { buildDeterministicWorkspaceTabId } from "@/workspace-tabs/identity";
export interface WorkspaceTabModel {
tabs: WorkspaceDerivedTab[];