Support line ranges in assistant file links (#1067)
This commit is contained in:
@@ -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[];
|
||||
|
||||
Reference in New Issue
Block a user