feat(app): allow agents to open workspace files from chat

This commit is contained in:
Mohamed Boudra
2026-03-12 23:43:59 +07:00
parent 999d100464
commit 6db99c03cb
5 changed files with 64 additions and 0 deletions

View File

@@ -86,6 +86,7 @@ export interface AgentStreamViewProps {
pendingPermissions: Map<string, PendingPermission>;
routeBottomAnchorRequest?: BottomAnchorRouteRequest | null;
isAuthoritativeHistoryReady?: boolean;
onOpenWorkspaceFile?: (input: { filePath: string }) => void;
}
export const AgentStreamView = forwardRef<AgentStreamViewHandle, AgentStreamViewProps>(function AgentStreamView({
@@ -96,6 +97,7 @@ export const AgentStreamView = forwardRef<AgentStreamViewHandle, AgentStreamView
pendingPermissions,
routeBottomAnchorRequest = null,
isAuthoritativeHistoryReady = true,
onOpenWorkspaceFile,
}, ref) {
const viewportRef = useRef<StreamViewportHandle | null>(null);
const { theme } = useUnistyles();
@@ -159,6 +161,11 @@ export const AgentStreamView = forwardRef<AgentStreamViewHandle, AgentStreamView
}
if (normalized.file) {
if (onOpenWorkspaceFile) {
onOpenWorkspaceFile({ filePath: normalized.file });
return;
}
const route = buildHostWorkspaceFileRoute(
resolvedServerId,
workspaceId,
@@ -188,6 +195,7 @@ export const AgentStreamView = forwardRef<AgentStreamViewHandle, AgentStreamView
resolvedServerId,
router,
setExplorerTabForCheckout,
onOpenWorkspaceFile,
workspaceId,
]
);

View File

@@ -81,11 +81,13 @@ export function AgentReadyScreen({
agentId,
showExplorerSidebar = true,
wrapWithExplorerSidebarProvider = true,
onOpenWorkspaceFile,
}: {
serverId: string;
agentId: string;
showExplorerSidebar?: boolean;
wrapWithExplorerSidebarProvider?: boolean;
onOpenWorkspaceFile?: (input: { filePath: string }) => void;
}) {
const resolvedAgentId = agentId?.trim() || undefined;
const resolvedServerId = serverId?.trim() || undefined;
@@ -129,6 +131,7 @@ export function AgentReadyScreen({
isConnected={runtimeIsConnected}
connectionStatus={connectionStatus}
showExplorerSidebar={showExplorerSidebar}
onOpenWorkspaceFile={onOpenWorkspaceFile}
/>
);
@@ -146,6 +149,7 @@ type AgentScreenContentProps = {
isConnected: boolean;
connectionStatus: HostRuntimeConnectionStatus;
showExplorerSidebar: boolean;
onOpenWorkspaceFile?: (input: { filePath: string }) => void;
};
function toErrorMessage(error: unknown): string {
@@ -166,6 +170,7 @@ function AgentScreenContent({
isConnected,
connectionStatus,
showExplorerSidebar,
onOpenWorkspaceFile,
}: AgentScreenContentProps) {
const { theme } = useUnistyles();
const toast = useToast();
@@ -904,6 +909,7 @@ function AgentScreenContent({
pendingPermissions={pendingPermissions}
routeBottomAnchorRequest={routeBottomAnchorRequest}
isAuthoritativeHistoryReady={hasAppliedAuthoritativeHistory}
onOpenWorkspaceFile={onOpenWorkspaceFile}
/>
</ReanimatedAnimated.View>
</View>

View File

@@ -29,6 +29,7 @@ type WorkspaceDraftAgentTabProps = {
tabId: string
draftId: string
onCreated: (snapshot: AgentSnapshotPayload) => void
onOpenWorkspaceFile: (input: { filePath: string }) => void
}
export function WorkspaceDraftAgentTab({
@@ -37,6 +38,7 @@ export function WorkspaceDraftAgentTab({
tabId,
draftId,
onCreated,
onOpenWorkspaceFile,
}: WorkspaceDraftAgentTabProps) {
const { client, isConnected } = useHostRuntimeSession(serverId)
const addImagesRef = useRef<((images: ImageAttachment[]) => void) | null>(null)
@@ -204,6 +206,7 @@ export function WorkspaceDraftAgentTab({
agent={draftAgent}
streamItems={optimisticStreamItems}
pendingPermissions={EMPTY_PENDING_PERMISSIONS}
onOpenWorkspaceFile={onOpenWorkspaceFile}
/>
</View>
) : (

View File

@@ -756,6 +756,17 @@ function WorkspaceScreenContent({
[closeToAgent, isMobile, navigateToTabId, normalizedServerId, normalizedWorkspaceId, openOrFocusTab]
);
const handleOpenFileFromChat = useCallback(
({ filePath }: { filePath: string }) => {
const normalizedFilePath = filePath.trim();
if (!normalizedFilePath) {
return;
}
handleOpenFileFromExplorer(normalizedFilePath);
},
[handleOpenFileFromExplorer]
);
const [isTabSwitcherOpen, setIsTabSwitcherOpen] = useState(false);
const [hoveredTabKey, setHoveredTabKey] = useState<string | null>(null);
const [hoveredCloseTabKey, setHoveredCloseTabKey] = useState<string | null>(
@@ -1258,6 +1269,7 @@ function WorkspaceScreenContent({
workspaceId={normalizedWorkspaceId}
tabId={activeTabId ?? target.draftId}
draftId={target.draftId}
onOpenWorkspaceFile={handleOpenFileFromChat}
onCreated={(agentSnapshot) => {
const tabId = activeTabId ?? target.draftId;
const normalized = normalizeAgentSnapshot(agentSnapshot, normalizedServerId);
@@ -1292,6 +1304,7 @@ function WorkspaceScreenContent({
agentId={target.agentId}
showExplorerSidebar={false}
wrapWithExplorerSidebarProvider={false}
onOpenWorkspaceFile={handleOpenFileFromChat}
/>
);
}

View File

@@ -136,4 +136,38 @@ describe("workspace-tabs-store retargetTab", () => {
expect(matchingTabs).toHaveLength(1);
expect(order).toEqual([draftTabId]);
});
it("openOrFocusTab re-focuses an existing file tab after the workspace focus changed", () => {
const key = buildWorkspaceTabPersistenceKey({ serverId: SERVER_ID, workspaceId: WORKSPACE_ID });
expect(key).toBeTruthy();
const workspaceKey = key as string;
const fileTabId = useWorkspaceTabsStore.getState().openOrFocusTab({
serverId: SERVER_ID,
workspaceId: WORKSPACE_ID,
target: { kind: "file", path: "/repo/worktree/src/index.ts" },
});
const terminalTabId = useWorkspaceTabsStore.getState().openOrFocusTab({
serverId: SERVER_ID,
workspaceId: WORKSPACE_ID,
target: { kind: "terminal", terminalId: "term-1" },
});
expect(fileTabId).toBe("file_/repo/worktree/src/index.ts");
expect(terminalTabId).toBe("terminal_term-1");
expect(useWorkspaceTabsStore.getState().focusedTabIdByWorkspace[workspaceKey]).toBe(
terminalTabId
);
const reopenedFileTabId = useWorkspaceTabsStore.getState().openOrFocusTab({
serverId: SERVER_ID,
workspaceId: WORKSPACE_ID,
target: { kind: "file", path: "/repo/worktree/src/index.ts" },
});
expect(reopenedFileTabId).toBe(fileTabId);
expect(useWorkspaceTabsStore.getState().focusedTabIdByWorkspace[workspaceKey]).toBe(
fileTabId
);
});
});