feat(app): allow agents to open workspace files from chat
This commit is contained in:
@@ -86,6 +86,7 @@ export interface AgentStreamViewProps {
|
|||||||
pendingPermissions: Map<string, PendingPermission>;
|
pendingPermissions: Map<string, PendingPermission>;
|
||||||
routeBottomAnchorRequest?: BottomAnchorRouteRequest | null;
|
routeBottomAnchorRequest?: BottomAnchorRouteRequest | null;
|
||||||
isAuthoritativeHistoryReady?: boolean;
|
isAuthoritativeHistoryReady?: boolean;
|
||||||
|
onOpenWorkspaceFile?: (input: { filePath: string }) => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
export const AgentStreamView = forwardRef<AgentStreamViewHandle, AgentStreamViewProps>(function AgentStreamView({
|
export const AgentStreamView = forwardRef<AgentStreamViewHandle, AgentStreamViewProps>(function AgentStreamView({
|
||||||
@@ -96,6 +97,7 @@ export const AgentStreamView = forwardRef<AgentStreamViewHandle, AgentStreamView
|
|||||||
pendingPermissions,
|
pendingPermissions,
|
||||||
routeBottomAnchorRequest = null,
|
routeBottomAnchorRequest = null,
|
||||||
isAuthoritativeHistoryReady = true,
|
isAuthoritativeHistoryReady = true,
|
||||||
|
onOpenWorkspaceFile,
|
||||||
}, ref) {
|
}, ref) {
|
||||||
const viewportRef = useRef<StreamViewportHandle | null>(null);
|
const viewportRef = useRef<StreamViewportHandle | null>(null);
|
||||||
const { theme } = useUnistyles();
|
const { theme } = useUnistyles();
|
||||||
@@ -159,6 +161,11 @@ export const AgentStreamView = forwardRef<AgentStreamViewHandle, AgentStreamView
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (normalized.file) {
|
if (normalized.file) {
|
||||||
|
if (onOpenWorkspaceFile) {
|
||||||
|
onOpenWorkspaceFile({ filePath: normalized.file });
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
const route = buildHostWorkspaceFileRoute(
|
const route = buildHostWorkspaceFileRoute(
|
||||||
resolvedServerId,
|
resolvedServerId,
|
||||||
workspaceId,
|
workspaceId,
|
||||||
@@ -188,6 +195,7 @@ export const AgentStreamView = forwardRef<AgentStreamViewHandle, AgentStreamView
|
|||||||
resolvedServerId,
|
resolvedServerId,
|
||||||
router,
|
router,
|
||||||
setExplorerTabForCheckout,
|
setExplorerTabForCheckout,
|
||||||
|
onOpenWorkspaceFile,
|
||||||
workspaceId,
|
workspaceId,
|
||||||
]
|
]
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -81,11 +81,13 @@ export function AgentReadyScreen({
|
|||||||
agentId,
|
agentId,
|
||||||
showExplorerSidebar = true,
|
showExplorerSidebar = true,
|
||||||
wrapWithExplorerSidebarProvider = true,
|
wrapWithExplorerSidebarProvider = true,
|
||||||
|
onOpenWorkspaceFile,
|
||||||
}: {
|
}: {
|
||||||
serverId: string;
|
serverId: string;
|
||||||
agentId: string;
|
agentId: string;
|
||||||
showExplorerSidebar?: boolean;
|
showExplorerSidebar?: boolean;
|
||||||
wrapWithExplorerSidebarProvider?: boolean;
|
wrapWithExplorerSidebarProvider?: boolean;
|
||||||
|
onOpenWorkspaceFile?: (input: { filePath: string }) => void;
|
||||||
}) {
|
}) {
|
||||||
const resolvedAgentId = agentId?.trim() || undefined;
|
const resolvedAgentId = agentId?.trim() || undefined;
|
||||||
const resolvedServerId = serverId?.trim() || undefined;
|
const resolvedServerId = serverId?.trim() || undefined;
|
||||||
@@ -129,6 +131,7 @@ export function AgentReadyScreen({
|
|||||||
isConnected={runtimeIsConnected}
|
isConnected={runtimeIsConnected}
|
||||||
connectionStatus={connectionStatus}
|
connectionStatus={connectionStatus}
|
||||||
showExplorerSidebar={showExplorerSidebar}
|
showExplorerSidebar={showExplorerSidebar}
|
||||||
|
onOpenWorkspaceFile={onOpenWorkspaceFile}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
|
|
||||||
@@ -146,6 +149,7 @@ type AgentScreenContentProps = {
|
|||||||
isConnected: boolean;
|
isConnected: boolean;
|
||||||
connectionStatus: HostRuntimeConnectionStatus;
|
connectionStatus: HostRuntimeConnectionStatus;
|
||||||
showExplorerSidebar: boolean;
|
showExplorerSidebar: boolean;
|
||||||
|
onOpenWorkspaceFile?: (input: { filePath: string }) => void;
|
||||||
};
|
};
|
||||||
|
|
||||||
function toErrorMessage(error: unknown): string {
|
function toErrorMessage(error: unknown): string {
|
||||||
@@ -166,6 +170,7 @@ function AgentScreenContent({
|
|||||||
isConnected,
|
isConnected,
|
||||||
connectionStatus,
|
connectionStatus,
|
||||||
showExplorerSidebar,
|
showExplorerSidebar,
|
||||||
|
onOpenWorkspaceFile,
|
||||||
}: AgentScreenContentProps) {
|
}: AgentScreenContentProps) {
|
||||||
const { theme } = useUnistyles();
|
const { theme } = useUnistyles();
|
||||||
const toast = useToast();
|
const toast = useToast();
|
||||||
@@ -904,6 +909,7 @@ function AgentScreenContent({
|
|||||||
pendingPermissions={pendingPermissions}
|
pendingPermissions={pendingPermissions}
|
||||||
routeBottomAnchorRequest={routeBottomAnchorRequest}
|
routeBottomAnchorRequest={routeBottomAnchorRequest}
|
||||||
isAuthoritativeHistoryReady={hasAppliedAuthoritativeHistory}
|
isAuthoritativeHistoryReady={hasAppliedAuthoritativeHistory}
|
||||||
|
onOpenWorkspaceFile={onOpenWorkspaceFile}
|
||||||
/>
|
/>
|
||||||
</ReanimatedAnimated.View>
|
</ReanimatedAnimated.View>
|
||||||
</View>
|
</View>
|
||||||
|
|||||||
@@ -29,6 +29,7 @@ type WorkspaceDraftAgentTabProps = {
|
|||||||
tabId: string
|
tabId: string
|
||||||
draftId: string
|
draftId: string
|
||||||
onCreated: (snapshot: AgentSnapshotPayload) => void
|
onCreated: (snapshot: AgentSnapshotPayload) => void
|
||||||
|
onOpenWorkspaceFile: (input: { filePath: string }) => void
|
||||||
}
|
}
|
||||||
|
|
||||||
export function WorkspaceDraftAgentTab({
|
export function WorkspaceDraftAgentTab({
|
||||||
@@ -37,6 +38,7 @@ export function WorkspaceDraftAgentTab({
|
|||||||
tabId,
|
tabId,
|
||||||
draftId,
|
draftId,
|
||||||
onCreated,
|
onCreated,
|
||||||
|
onOpenWorkspaceFile,
|
||||||
}: WorkspaceDraftAgentTabProps) {
|
}: WorkspaceDraftAgentTabProps) {
|
||||||
const { client, isConnected } = useHostRuntimeSession(serverId)
|
const { client, isConnected } = useHostRuntimeSession(serverId)
|
||||||
const addImagesRef = useRef<((images: ImageAttachment[]) => void) | null>(null)
|
const addImagesRef = useRef<((images: ImageAttachment[]) => void) | null>(null)
|
||||||
@@ -204,6 +206,7 @@ export function WorkspaceDraftAgentTab({
|
|||||||
agent={draftAgent}
|
agent={draftAgent}
|
||||||
streamItems={optimisticStreamItems}
|
streamItems={optimisticStreamItems}
|
||||||
pendingPermissions={EMPTY_PENDING_PERMISSIONS}
|
pendingPermissions={EMPTY_PENDING_PERMISSIONS}
|
||||||
|
onOpenWorkspaceFile={onOpenWorkspaceFile}
|
||||||
/>
|
/>
|
||||||
</View>
|
</View>
|
||||||
) : (
|
) : (
|
||||||
|
|||||||
@@ -756,6 +756,17 @@ function WorkspaceScreenContent({
|
|||||||
[closeToAgent, isMobile, navigateToTabId, normalizedServerId, normalizedWorkspaceId, openOrFocusTab]
|
[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 [isTabSwitcherOpen, setIsTabSwitcherOpen] = useState(false);
|
||||||
const [hoveredTabKey, setHoveredTabKey] = useState<string | null>(null);
|
const [hoveredTabKey, setHoveredTabKey] = useState<string | null>(null);
|
||||||
const [hoveredCloseTabKey, setHoveredCloseTabKey] = useState<string | null>(
|
const [hoveredCloseTabKey, setHoveredCloseTabKey] = useState<string | null>(
|
||||||
@@ -1258,6 +1269,7 @@ function WorkspaceScreenContent({
|
|||||||
workspaceId={normalizedWorkspaceId}
|
workspaceId={normalizedWorkspaceId}
|
||||||
tabId={activeTabId ?? target.draftId}
|
tabId={activeTabId ?? target.draftId}
|
||||||
draftId={target.draftId}
|
draftId={target.draftId}
|
||||||
|
onOpenWorkspaceFile={handleOpenFileFromChat}
|
||||||
onCreated={(agentSnapshot) => {
|
onCreated={(agentSnapshot) => {
|
||||||
const tabId = activeTabId ?? target.draftId;
|
const tabId = activeTabId ?? target.draftId;
|
||||||
const normalized = normalizeAgentSnapshot(agentSnapshot, normalizedServerId);
|
const normalized = normalizeAgentSnapshot(agentSnapshot, normalizedServerId);
|
||||||
@@ -1292,6 +1304,7 @@ function WorkspaceScreenContent({
|
|||||||
agentId={target.agentId}
|
agentId={target.agentId}
|
||||||
showExplorerSidebar={false}
|
showExplorerSidebar={false}
|
||||||
wrapWithExplorerSidebarProvider={false}
|
wrapWithExplorerSidebarProvider={false}
|
||||||
|
onOpenWorkspaceFile={handleOpenFileFromChat}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -136,4 +136,38 @@ describe("workspace-tabs-store retargetTab", () => {
|
|||||||
expect(matchingTabs).toHaveLength(1);
|
expect(matchingTabs).toHaveLength(1);
|
||||||
expect(order).toEqual([draftTabId]);
|
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
|
||||||
|
);
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
Reference in New Issue
Block a user