Merge branch 'storage-terminal-ui-dev' into dev
# Conflicts: # nix/package.nix # packages/app/src/app/_layout.tsx # packages/app/src/components/sidebar-workspace-list.tsx # packages/app/src/hooks/use-command-center.ts # packages/app/src/screens/agent/draft-agent-screen.tsx # packages/app/src/screens/workspace/workspace-desktop-tabs-row.tsx # packages/app/src/screens/workspace/workspace-screen.tsx # packages/server/src/server/session.ts # packages/server/src/server/session.workspaces.test.ts # packages/server/src/terminal/terminal.test.ts # packages/server/src/terminal/terminal.ts
This commit is contained in:
@@ -62,8 +62,6 @@ import { useKeyboardActionHandler } from "@/hooks/use-keyboard-action-handler";
|
||||
import type { KeyboardActionDefinition } from "@/keyboard/keyboard-action-dispatcher";
|
||||
import { useCreateFlowStore } from "@/stores/create-flow-store";
|
||||
import { decodeWorkspaceIdFromPathSegment } from "@/utils/host-routes";
|
||||
import { isAbsolutePath } from "@/utils/path";
|
||||
import { normalizeWorkspaceIdentity } from "@/utils/workspace-identity";
|
||||
import {
|
||||
normalizeWorkspaceTabTarget,
|
||||
workspaceTabTargetsEqual,
|
||||
@@ -81,6 +79,10 @@ import { applyArchivedAgentCloseResults, useArchiveAgent } from "@/hooks/use-arc
|
||||
import { useStableEvent } from "@/hooks/use-stable-event";
|
||||
import { buildProviderCommand } from "@/utils/provider-command-templates";
|
||||
import { generateDraftId } from "@/stores/draft-keys";
|
||||
import {
|
||||
resolveWorkspaceExecutionAuthority,
|
||||
resolveWorkspaceRouteId,
|
||||
} from "@/utils/workspace-execution";
|
||||
import {
|
||||
WorkspaceTabPresentationResolver,
|
||||
WorkspaceTabIcon,
|
||||
@@ -98,7 +100,6 @@ import {
|
||||
} from "@/screens/workspace/workspace-header-source";
|
||||
import {
|
||||
deriveWorkspaceAgentVisibility,
|
||||
shouldPruneWorkspaceAgentTab,
|
||||
workspaceAgentVisibilityEqual,
|
||||
} from "@/screens/workspace/workspace-agent-visibility";
|
||||
import { deriveWorkspacePaneState } from "@/screens/workspace/workspace-pane-state";
|
||||
@@ -117,11 +118,7 @@ import { findAdjacentPane } from "@/utils/split-navigation";
|
||||
import { isCompactFormFactor, supportsDesktopPaneSplits } from "@/constants/layout";
|
||||
|
||||
const TERMINALS_QUERY_STALE_TIME = 5_000;
|
||||
const NEW_TAB_AGENT_OPTION_ID = "__new_tab_agent__";
|
||||
const NEW_TAB_TERMINAL_OPTION_ID = "__new_tab_terminal__";
|
||||
type NewTabOptionId = typeof NEW_TAB_AGENT_OPTION_ID | typeof NEW_TAB_TERMINAL_OPTION_ID;
|
||||
const EMPTY_UI_TABS: WorkspaceTab[] = [];
|
||||
const EMPTY_PINNED_AGENT_IDS = new Set<string>();
|
||||
const EMPTY_SET = new Set<string>();
|
||||
|
||||
type WorkspaceScreenProps = {
|
||||
@@ -146,6 +143,9 @@ function decodeSegment(value: string): string {
|
||||
}
|
||||
|
||||
function getFallbackTabOptionLabel(tab: WorkspaceTabDescriptor): string {
|
||||
if (tab.target.kind === "launcher") {
|
||||
return "New Tab";
|
||||
}
|
||||
if (tab.target.kind === "draft") {
|
||||
return "New Agent";
|
||||
}
|
||||
@@ -159,6 +159,9 @@ function getFallbackTabOptionLabel(tab: WorkspaceTabDescriptor): string {
|
||||
}
|
||||
|
||||
function getFallbackTabOptionDescription(tab: WorkspaceTabDescriptor): string {
|
||||
if (tab.target.kind === "launcher") {
|
||||
return "New Tab";
|
||||
}
|
||||
if (tab.target.kind === "draft") {
|
||||
return "New Agent";
|
||||
}
|
||||
@@ -585,7 +588,13 @@ function WorkspaceScreenContent({ serverId, workspaceId }: WorkspaceScreenProps)
|
||||
|
||||
const normalizedServerId = trimNonEmpty(decodeSegment(serverId)) ?? "";
|
||||
const normalizedWorkspaceId =
|
||||
normalizeWorkspaceIdentity(decodeWorkspaceIdFromPathSegment(workspaceId)) ?? "";
|
||||
resolveWorkspaceRouteId({
|
||||
routeWorkspaceId: decodeWorkspaceIdFromPathSegment(workspaceId),
|
||||
}) ?? "";
|
||||
const sessionWorkspaces = useSessionStore(
|
||||
(state) => state.sessions[normalizedServerId]?.workspaces,
|
||||
);
|
||||
|
||||
const workspaceTerminalScopeKey =
|
||||
normalizedServerId && normalizedWorkspaceId
|
||||
? `${normalizedServerId}:${normalizedWorkspaceId}`
|
||||
@@ -597,43 +606,53 @@ function WorkspaceScreenContent({ serverId, workspaceId }: WorkspaceScreenProps)
|
||||
const queryClient = useQueryClient();
|
||||
const client = useHostRuntimeClient(normalizedServerId);
|
||||
const isConnected = useHostRuntimeIsConnected(normalizedServerId);
|
||||
const workspaceDescriptor = sessionWorkspaces?.get(normalizedWorkspaceId) ?? null;
|
||||
const workspaceAuthority = useMemo(
|
||||
() =>
|
||||
resolveWorkspaceExecutionAuthority({
|
||||
workspaces: sessionWorkspaces,
|
||||
workspaceId: normalizedWorkspaceId,
|
||||
}),
|
||||
[normalizedWorkspaceId, sessionWorkspaces],
|
||||
);
|
||||
const workspaceDirectory = workspaceAuthority?.workspaceDirectory ?? null;
|
||||
const isMissingWorkspaceExecutionAuthority = Boolean(workspaceDescriptor && !workspaceAuthority);
|
||||
|
||||
const workspaceAgentVisibility = useStoreWithEqualityFn(
|
||||
useSessionStore,
|
||||
(state) =>
|
||||
deriveWorkspaceAgentVisibility({
|
||||
sessionAgents: state.sessions[normalizedServerId]?.agents,
|
||||
workspaceId: normalizedWorkspaceId,
|
||||
workspaceDirectory,
|
||||
}),
|
||||
workspaceAgentVisibilityEqual,
|
||||
);
|
||||
|
||||
const terminalsQueryKey = useMemo(
|
||||
() => ["terminals", normalizedServerId, normalizedWorkspaceId] as const,
|
||||
[normalizedServerId, normalizedWorkspaceId],
|
||||
() => ["terminals", normalizedServerId, workspaceDirectory] as const,
|
||||
[normalizedServerId, workspaceDirectory],
|
||||
);
|
||||
type ListTerminalsPayload = ListTerminalsResponse["payload"];
|
||||
const terminalsQuery = useQuery({
|
||||
queryKey: terminalsQueryKey,
|
||||
enabled:
|
||||
Boolean(client && isConnected) &&
|
||||
normalizedWorkspaceId.length > 0 &&
|
||||
isAbsolutePath(normalizedWorkspaceId),
|
||||
Boolean(workspaceDirectory),
|
||||
queryFn: async () => {
|
||||
if (!client) {
|
||||
if (!client || !workspaceDirectory) {
|
||||
throw new Error("Host is not connected");
|
||||
}
|
||||
return await client.listTerminals(normalizedWorkspaceId);
|
||||
return await client.listTerminals(workspaceDirectory);
|
||||
},
|
||||
staleTime: TERMINALS_QUERY_STALE_TIME,
|
||||
});
|
||||
const terminals = terminalsQuery.data?.terminals ?? [];
|
||||
const createTerminalMutation = useMutation({
|
||||
mutationFn: async (input?: { paneId?: string }) => {
|
||||
if (!client) {
|
||||
if (!client || !workspaceDirectory) {
|
||||
throw new Error("Host is not connected");
|
||||
}
|
||||
return await client.createTerminal(normalizedWorkspaceId);
|
||||
return await client.createTerminal(workspaceDirectory);
|
||||
},
|
||||
onSuccess: (payload, input) => {
|
||||
const createdTerminal = payload.terminal;
|
||||
@@ -643,8 +662,9 @@ function WorkspaceScreenContent({ serverId, workspaceId }: WorkspaceScreenProps)
|
||||
terminals: current?.terminals ?? [],
|
||||
terminal: createdTerminal,
|
||||
});
|
||||
const cwd = current?.cwd ?? workspaceDirectory;
|
||||
return {
|
||||
cwd: current?.cwd ?? normalizedWorkspaceId,
|
||||
...(cwd ? { cwd } : {}),
|
||||
terminals: nextTerminals,
|
||||
requestId: current?.requestId ?? `terminal-create-${createdTerminal.id}`,
|
||||
};
|
||||
@@ -687,7 +707,7 @@ function WorkspaceScreenContent({ serverId, workspaceId }: WorkspaceScreenProps)
|
||||
const { archiveAgent } = useArchiveAgent();
|
||||
|
||||
useEffect(() => {
|
||||
if (!client || !isConnected || !isAbsolutePath(normalizedWorkspaceId)) {
|
||||
if (!client || !isConnected || !workspaceDirectory) {
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -695,7 +715,7 @@ function WorkspaceScreenContent({ serverId, workspaceId }: WorkspaceScreenProps)
|
||||
if (message.type !== "terminals_changed") {
|
||||
return;
|
||||
}
|
||||
if (message.payload.cwd !== normalizedWorkspaceId) {
|
||||
if (message.payload.cwd !== workspaceDirectory) {
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -706,32 +726,30 @@ function WorkspaceScreenContent({ serverId, workspaceId }: WorkspaceScreenProps)
|
||||
}));
|
||||
});
|
||||
|
||||
client.subscribeTerminals({ cwd: normalizedWorkspaceId });
|
||||
client.subscribeTerminals({ cwd: workspaceDirectory });
|
||||
|
||||
return () => {
|
||||
unsubscribeChanged();
|
||||
client.unsubscribeTerminals({ cwd: normalizedWorkspaceId });
|
||||
client.unsubscribeTerminals({ cwd: workspaceDirectory });
|
||||
};
|
||||
}, [client, isConnected, normalizedWorkspaceId, queryClient, terminalsQueryKey]);
|
||||
}, [client, isConnected, queryClient, terminalsQueryKey, workspaceDirectory]);
|
||||
|
||||
const checkoutQuery = useQuery({
|
||||
queryKey: checkoutStatusQueryKey(normalizedServerId, normalizedWorkspaceId),
|
||||
queryKey: checkoutStatusQueryKey(
|
||||
normalizedServerId,
|
||||
workspaceDirectory ?? `missing-workspace-directory:${normalizedWorkspaceId}`,
|
||||
),
|
||||
enabled:
|
||||
Boolean(client && isConnected) &&
|
||||
normalizedWorkspaceId.length > 0 &&
|
||||
isAbsolutePath(normalizedWorkspaceId),
|
||||
Boolean(workspaceDirectory),
|
||||
queryFn: async () => {
|
||||
if (!client) {
|
||||
if (!client || !workspaceDirectory) {
|
||||
throw new Error("Host is not connected");
|
||||
}
|
||||
return (await client.getCheckoutStatus(normalizedWorkspaceId)) as CheckoutStatusPayload;
|
||||
return (await client.getCheckoutStatus(workspaceDirectory)) as CheckoutStatusPayload;
|
||||
},
|
||||
staleTime: 15_000,
|
||||
});
|
||||
|
||||
const workspaceDescriptor = useSessionStore(
|
||||
(state) => state.sessions[normalizedServerId]?.workspaces.get(normalizedWorkspaceId) ?? null,
|
||||
);
|
||||
const hasHydratedWorkspaces = useSessionStore(
|
||||
(state) => state.sessions[normalizedServerId]?.hasHydratedWorkspaces ?? false,
|
||||
);
|
||||
@@ -761,15 +779,15 @@ function WorkspaceScreenContent({ serverId, workspaceId }: WorkspaceScreenProps)
|
||||
const isExplorerOpen = isMobile ? mobileView === "file-explorer" : desktopFileExplorerOpen;
|
||||
|
||||
const activeExplorerCheckout = useMemo<ExplorerCheckoutContext | null>(() => {
|
||||
if (!normalizedServerId || !isAbsolutePath(normalizedWorkspaceId)) {
|
||||
if (!normalizedServerId || !workspaceDirectory) {
|
||||
return null;
|
||||
}
|
||||
return {
|
||||
serverId: normalizedServerId,
|
||||
cwd: normalizedWorkspaceId,
|
||||
cwd: workspaceDirectory,
|
||||
isGit: isGitCheckout,
|
||||
};
|
||||
}, [isGitCheckout, normalizedServerId, normalizedWorkspaceId]);
|
||||
}, [isGitCheckout, normalizedServerId, workspaceDirectory]);
|
||||
|
||||
useEffect(() => {
|
||||
setActiveExplorerCheckout(activeExplorerCheckout);
|
||||
@@ -829,10 +847,13 @@ function WorkspaceScreenContent({ serverId, workspaceId }: WorkspaceScreenProps)
|
||||
[workspaceLayout],
|
||||
);
|
||||
const openWorkspaceTab = useWorkspaceLayoutStore((state) => state.openTab);
|
||||
const openWorkspaceLauncherTab = useWorkspaceLayoutStore((state) => state.openLauncherTab);
|
||||
const focusWorkspaceTab = useWorkspaceLayoutStore((state) => state.focusTab);
|
||||
const closeWorkspaceTab = useWorkspaceLayoutStore((state) => state.closeTab);
|
||||
const unpinWorkspaceAgent = useWorkspaceLayoutStore((state) => state.unpinAgent);
|
||||
const retargetWorkspaceTab = useWorkspaceLayoutStore((state) => state.retargetTab);
|
||||
const convertWorkspaceDraftToAgent = useWorkspaceLayoutStore((state) => state.convertDraftToAgent);
|
||||
const reconcileWorkspaceTabs = useWorkspaceLayoutStore((state) => state.reconcileTabs);
|
||||
const unpinWorkspaceAgent = useWorkspaceLayoutStore((state) => state.unpinAgent);
|
||||
const splitWorkspacePane = useWorkspaceLayoutStore((state) => state.splitPane);
|
||||
const splitWorkspacePaneEmpty = useWorkspaceLayoutStore((state) => state.splitPaneEmpty);
|
||||
const moveWorkspaceTabToPane = useWorkspaceLayoutStore((state) => state.moveTabToPane);
|
||||
@@ -840,11 +861,6 @@ function WorkspaceScreenContent({ serverId, workspaceId }: WorkspaceScreenProps)
|
||||
const paneFocusSuppressedRef = useRef(false);
|
||||
const resizeWorkspaceSplit = useWorkspaceLayoutStore((state) => state.resizeSplit);
|
||||
const reorderWorkspaceTabsInPane = useWorkspaceLayoutStore((state) => state.reorderTabsInPane);
|
||||
const pinnedAgentIds = useWorkspaceLayoutStore((state) =>
|
||||
persistenceKey
|
||||
? (state.pinnedAgentIdsByWorkspace[persistenceKey] ?? EMPTY_PINNED_AGENT_IDS)
|
||||
: EMPTY_PINNED_AGENT_IDS,
|
||||
);
|
||||
const pendingByDraftId = useCreateFlowStore((state) => state.pendingByDraftId);
|
||||
const { closingTabIds, closeTab } = useCloseTabs();
|
||||
const closeWorkspaceTabWithCleanup = useCallback(
|
||||
@@ -948,7 +964,6 @@ function WorkspaceScreenContent({ serverId, workspaceId }: WorkspaceScreenProps)
|
||||
return;
|
||||
}
|
||||
|
||||
const terminalIds = new Set(terminals.map((terminal) => terminal.id));
|
||||
const hasActivePendingDraftCreateInWorkspace = uiTabs.some((tab) => {
|
||||
if (tab.target.kind !== "draft") {
|
||||
return false;
|
||||
@@ -957,56 +972,19 @@ function WorkspaceScreenContent({ serverId, workspaceId }: WorkspaceScreenProps)
|
||||
return pending?.serverId === normalizedServerId && pending.lifecycle === "active";
|
||||
});
|
||||
|
||||
for (const agentId of workspaceAgentVisibility.activeAgentIds) {
|
||||
const representedByTarget = uiTabs.some(
|
||||
(tab) => tab.target.kind === "agent" && tab.target.agentId === agentId,
|
||||
);
|
||||
const representedByDeterministicTabId = uiTabs.some(
|
||||
(tab) => tab.tabId === `agent_${agentId}`,
|
||||
);
|
||||
if (
|
||||
hasActivePendingDraftCreateInWorkspace &&
|
||||
!representedByTarget &&
|
||||
!representedByDeterministicTabId
|
||||
) {
|
||||
continue;
|
||||
}
|
||||
ensureWorkspaceTab({ kind: "agent", agentId });
|
||||
}
|
||||
for (const terminal of terminals) {
|
||||
ensureWorkspaceTab({ kind: "terminal", terminalId: terminal.id });
|
||||
}
|
||||
|
||||
const canPruneAgentTabs = hasHydratedAgents;
|
||||
const canPruneTerminalTabs = terminalsQuery.isSuccess;
|
||||
for (const tab of uiTabs) {
|
||||
if (
|
||||
canPruneAgentTabs &&
|
||||
tab.target.kind === "agent" &&
|
||||
shouldPruneWorkspaceAgentTab({
|
||||
agentId: tab.target.agentId,
|
||||
agentsHydrated: hasHydratedAgents,
|
||||
knownAgentIds: workspaceAgentVisibility.knownAgentIds,
|
||||
activeAgentIds: workspaceAgentVisibility.activeAgentIds,
|
||||
})
|
||||
) {
|
||||
closeWorkspaceTabWithCleanup({ tabId: tab.tabId, target: tab.target });
|
||||
}
|
||||
if (
|
||||
canPruneTerminalTabs &&
|
||||
tab.target.kind === "terminal" &&
|
||||
!terminalIds.has(tab.target.terminalId)
|
||||
) {
|
||||
closeWorkspaceTabWithCleanup({ tabId: tab.tabId, target: tab.target });
|
||||
}
|
||||
}
|
||||
reconcileWorkspaceTabs(persistenceKey, {
|
||||
agentsHydrated: hasHydratedAgents,
|
||||
terminalsHydrated: terminalsQuery.isSuccess,
|
||||
activeAgentIds: workspaceAgentVisibility.activeAgentIds,
|
||||
knownAgentIds: workspaceAgentVisibility.knownAgentIds,
|
||||
standaloneTerminalIds: terminals.map((terminal) => terminal.id),
|
||||
hasActivePendingDraftCreate: hasActivePendingDraftCreateInWorkspace,
|
||||
});
|
||||
}, [
|
||||
closeWorkspaceTabWithCleanup,
|
||||
ensureWorkspaceTab,
|
||||
hasHydratedAgents,
|
||||
pendingByDraftId,
|
||||
pinnedAgentIds,
|
||||
persistenceKey,
|
||||
reconcileWorkspaceTabs,
|
||||
terminals,
|
||||
terminalsQuery.isSuccess,
|
||||
uiTabs,
|
||||
@@ -1016,13 +994,6 @@ function WorkspaceScreenContent({ serverId, workspaceId }: WorkspaceScreenProps)
|
||||
const activeTabId = focusedPaneTabState.activeTabId;
|
||||
const activeTab = focusedPaneTabState.activeTab;
|
||||
|
||||
useEffect(() => {
|
||||
if (!activeTabId || !persistenceKey) {
|
||||
return;
|
||||
}
|
||||
focusWorkspaceTab(persistenceKey, activeTabId);
|
||||
}, [activeTabId, focusWorkspaceTab, persistenceKey]);
|
||||
|
||||
const tabs = useMemo<WorkspaceTabDescriptor[]>(
|
||||
() => focusedPaneTabState.tabs.map((tab) => tab.descriptor),
|
||||
[focusedPaneTabState.tabs],
|
||||
@@ -1134,17 +1105,36 @@ function WorkspaceScreenContent({ serverId, workspaceId }: WorkspaceScreenProps)
|
||||
openWorkspaceDraftTab();
|
||||
}, [openWorkspaceDraftTab]);
|
||||
|
||||
const handleCreateLauncherTab = useCallback(
|
||||
(input?: { paneId?: string }) => {
|
||||
if (!persistenceKey) {
|
||||
return null;
|
||||
}
|
||||
|
||||
if (input?.paneId) {
|
||||
focusWorkspacePane(persistenceKey, input.paneId);
|
||||
}
|
||||
|
||||
const tabId = openWorkspaceLauncherTab(persistenceKey);
|
||||
if (tabId) {
|
||||
focusWorkspaceTab(persistenceKey, tabId);
|
||||
}
|
||||
return tabId;
|
||||
},
|
||||
[focusWorkspacePane, focusWorkspaceTab, openWorkspaceLauncherTab, persistenceKey],
|
||||
);
|
||||
|
||||
const handleCreateTerminal = useCallback(
|
||||
(input?: { paneId?: string }) => {
|
||||
if (createTerminalMutation.isPending) {
|
||||
return;
|
||||
}
|
||||
if (!isAbsolutePath(normalizedWorkspaceId)) {
|
||||
if (!workspaceDirectory) {
|
||||
return;
|
||||
}
|
||||
createTerminalMutation.mutate(input);
|
||||
},
|
||||
[createTerminalMutation, normalizedWorkspaceId],
|
||||
[createTerminalMutation, workspaceDirectory],
|
||||
);
|
||||
|
||||
const handleSelectSwitcherTab = useCallback(
|
||||
@@ -1154,21 +1144,7 @@ function WorkspaceScreenContent({ serverId, workspaceId }: WorkspaceScreenProps)
|
||||
[navigateToTabId],
|
||||
);
|
||||
|
||||
const handleSelectNewTabOption = useCallback(
|
||||
(selection: { optionId: NewTabOptionId; paneId?: string }) => {
|
||||
if (selection.paneId && persistenceKey) {
|
||||
focusWorkspacePane(persistenceKey, selection.paneId);
|
||||
}
|
||||
if (selection.optionId === NEW_TAB_AGENT_OPTION_ID) {
|
||||
handleCreateDraftTab();
|
||||
} else if (selection.optionId === NEW_TAB_TERMINAL_OPTION_ID) {
|
||||
handleCreateTerminal({ paneId: selection.paneId });
|
||||
}
|
||||
},
|
||||
[focusWorkspacePane, handleCreateDraftTab, handleCreateTerminal, persistenceKey],
|
||||
);
|
||||
|
||||
const handleCreateDraftSplit = useCallback(
|
||||
const handleCreateLauncherSplit = useCallback(
|
||||
(input: { targetPaneId: string; position: "left" | "right" | "top" | "bottom" }) => {
|
||||
if (!persistenceKey) {
|
||||
return;
|
||||
@@ -1179,10 +1155,9 @@ function WorkspaceScreenContent({ serverId, workspaceId }: WorkspaceScreenProps)
|
||||
return;
|
||||
}
|
||||
|
||||
focusWorkspacePane(persistenceKey, paneId);
|
||||
openWorkspaceDraftTab();
|
||||
handleCreateLauncherTab({ paneId });
|
||||
},
|
||||
[focusWorkspacePane, openWorkspaceDraftTab, persistenceKey, splitWorkspacePaneEmpty],
|
||||
[handleCreateLauncherTab, persistenceKey, splitWorkspacePaneEmpty],
|
||||
);
|
||||
|
||||
const killTerminalAsync = killTerminalMutation.mutateAsync;
|
||||
@@ -1297,29 +1272,6 @@ function WorkspaceScreenContent({ serverId, workspaceId }: WorkspaceScreenProps)
|
||||
[allTabDescriptorsById, handleCloseAgentTab, handleCloseDraftOrFileTab, handleCloseTerminalTab],
|
||||
);
|
||||
|
||||
const prevCloseTabDeps = useRef({
|
||||
allTabDescriptorsById,
|
||||
handleCloseAgentTab,
|
||||
handleCloseDraftOrFileTab,
|
||||
handleCloseTerminalTab,
|
||||
});
|
||||
useEffect(() => {
|
||||
const prev = prevCloseTabDeps.current;
|
||||
const changed: string[] = [];
|
||||
if (prev.allTabDescriptorsById !== allTabDescriptorsById) changed.push("allTabDescriptorsById");
|
||||
if (prev.handleCloseAgentTab !== handleCloseAgentTab) changed.push("handleCloseAgentTab");
|
||||
if (prev.handleCloseDraftOrFileTab !== handleCloseDraftOrFileTab)
|
||||
changed.push("handleCloseDraftOrFileTab");
|
||||
if (prev.handleCloseTerminalTab !== handleCloseTerminalTab)
|
||||
changed.push("handleCloseTerminalTab");
|
||||
if (changed.length > 0) console.log("[handleCloseTabById] deps changed:", changed.join(", "));
|
||||
prevCloseTabDeps.current = {
|
||||
allTabDescriptorsById,
|
||||
handleCloseAgentTab,
|
||||
handleCloseDraftOrFileTab,
|
||||
handleCloseTerminalTab,
|
||||
};
|
||||
});
|
||||
const handleCopyAgentId = useCallback(
|
||||
async (agentId: string) => {
|
||||
if (!agentId) return;
|
||||
@@ -1366,18 +1318,18 @@ function WorkspaceScreenContent({ serverId, workspaceId }: WorkspaceScreenProps)
|
||||
);
|
||||
|
||||
const handleCopyWorkspacePath = useCallback(async () => {
|
||||
if (!isAbsolutePath(normalizedWorkspaceId)) {
|
||||
if (!workspaceDirectory) {
|
||||
toast.error("Workspace path not available");
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
await Clipboard.setStringAsync(normalizedWorkspaceId);
|
||||
await Clipboard.setStringAsync(workspaceDirectory);
|
||||
toast.copied("Workspace path");
|
||||
} catch {
|
||||
toast.error("Copy failed");
|
||||
}
|
||||
}, [normalizedWorkspaceId, toast]);
|
||||
}, [toast, workspaceDirectory]);
|
||||
|
||||
const handleCopyBranchName = useCallback(async () => {
|
||||
if (!currentBranchName) {
|
||||
@@ -1535,7 +1487,7 @@ function WorkspaceScreenContent({ serverId, workspaceId }: WorkspaceScreenProps)
|
||||
(action: KeyboardActionDefinition): boolean => {
|
||||
switch (action.id) {
|
||||
case "workspace.tab.new":
|
||||
handleCreateDraftTab();
|
||||
handleCreateLauncherTab();
|
||||
return true;
|
||||
case "workspace.terminal.new":
|
||||
handleCreateTerminal();
|
||||
@@ -1568,7 +1520,14 @@ function WorkspaceScreenContent({ serverId, workspaceId }: WorkspaceScreenProps)
|
||||
return false;
|
||||
}
|
||||
},
|
||||
[activeTabId, handleCloseTabById, handleCreateDraftTab, handleCreateTerminal, navigateToTabId, tabs],
|
||||
[
|
||||
activeTabId,
|
||||
handleCloseTabById,
|
||||
handleCreateLauncherTab,
|
||||
handleCreateTerminal,
|
||||
navigateToTabId,
|
||||
tabs,
|
||||
],
|
||||
);
|
||||
|
||||
const handleWorkspacePaneAction = useCallback(
|
||||
@@ -1583,7 +1542,7 @@ function WorkspaceScreenContent({ serverId, workspaceId }: WorkspaceScreenProps)
|
||||
}
|
||||
|
||||
if (action.id === "workspace.pane.split.right") {
|
||||
handleCreateDraftSplit({
|
||||
handleCreateLauncherSplit({
|
||||
targetPaneId: focusedPane.id,
|
||||
position: "right",
|
||||
});
|
||||
@@ -1591,7 +1550,7 @@ function WorkspaceScreenContent({ serverId, workspaceId }: WorkspaceScreenProps)
|
||||
}
|
||||
|
||||
if (action.id === "workspace.pane.split.down") {
|
||||
handleCreateDraftSplit({
|
||||
handleCreateLauncherSplit({
|
||||
targetPaneId: focusedPane.id,
|
||||
position: "bottom",
|
||||
});
|
||||
@@ -1661,7 +1620,7 @@ function WorkspaceScreenContent({ serverId, workspaceId }: WorkspaceScreenProps)
|
||||
allTabDescriptorsById,
|
||||
closeWorkspaceTabWithCleanup,
|
||||
focusWorkspacePane,
|
||||
handleCreateDraftSplit,
|
||||
handleCreateLauncherSplit,
|
||||
moveWorkspaceTabToPane,
|
||||
persistenceKey,
|
||||
focusedPaneTabState.activeTabId,
|
||||
@@ -1746,6 +1705,10 @@ function WorkspaceScreenContent({ serverId, workspaceId }: WorkspaceScreenProps)
|
||||
if (!persistenceKey) {
|
||||
return;
|
||||
}
|
||||
if (input.tab.kind === "draft" && target.kind === "agent") {
|
||||
convertWorkspaceDraftToAgent(persistenceKey, input.tab.tabId, target.agentId);
|
||||
return;
|
||||
}
|
||||
retargetWorkspaceTab(persistenceKey, input.tab.tabId, target);
|
||||
},
|
||||
onOpenWorkspaceFile: (filePath) => {
|
||||
@@ -1764,47 +1727,10 @@ function WorkspaceScreenContent({ serverId, workspaceId }: WorkspaceScreenProps)
|
||||
normalizedWorkspaceId,
|
||||
openWorkspaceTab,
|
||||
persistenceKey,
|
||||
convertWorkspaceDraftToAgent,
|
||||
retargetWorkspaceTab,
|
||||
],
|
||||
);
|
||||
const prevBuildDeps = useRef({
|
||||
handleCloseTabById,
|
||||
handleOpenFileFromChat,
|
||||
focusWorkspacePane,
|
||||
navigateToTabId,
|
||||
normalizedServerId,
|
||||
normalizedWorkspaceId,
|
||||
openWorkspaceTab,
|
||||
persistenceKey,
|
||||
retargetWorkspaceTab,
|
||||
});
|
||||
useEffect(() => {
|
||||
const prev = prevBuildDeps.current;
|
||||
const changed: string[] = [];
|
||||
if (prev.handleCloseTabById !== handleCloseTabById) changed.push("handleCloseTabById");
|
||||
if (prev.handleOpenFileFromChat !== handleOpenFileFromChat)
|
||||
changed.push("handleOpenFileFromChat");
|
||||
if (prev.focusWorkspacePane !== focusWorkspacePane) changed.push("focusWorkspacePane");
|
||||
if (prev.navigateToTabId !== navigateToTabId) changed.push("navigateToTabId");
|
||||
if (prev.normalizedServerId !== normalizedServerId) changed.push("normalizedServerId");
|
||||
if (prev.normalizedWorkspaceId !== normalizedWorkspaceId) changed.push("normalizedWorkspaceId");
|
||||
if (prev.openWorkspaceTab !== openWorkspaceTab) changed.push("openWorkspaceTab");
|
||||
if (prev.persistenceKey !== persistenceKey) changed.push("persistenceKey");
|
||||
if (prev.retargetWorkspaceTab !== retargetWorkspaceTab) changed.push("retargetWorkspaceTab");
|
||||
if (changed.length > 0)
|
||||
console.log("[buildPaneContentModel] deps changed:", changed.join(", "));
|
||||
prevBuildDeps.current = {
|
||||
handleCloseTabById,
|
||||
handleOpenFileFromChat,
|
||||
focusWorkspacePane,
|
||||
navigateToTabId,
|
||||
normalizedServerId,
|
||||
normalizedWorkspaceId,
|
||||
openWorkspaceTab,
|
||||
persistenceKey,
|
||||
retargetWorkspaceTab,
|
||||
};
|
||||
});
|
||||
const focusedPaneId = focusedPaneTabState.pane?.id ?? null;
|
||||
const focusedPaneTabIds = useMemo(() => tabs.map((tab) => tab.tabId), [tabs]);
|
||||
const focusedPaneTabDescriptorMap = useStableTabDescriptorMap(tabs);
|
||||
@@ -1839,6 +1765,12 @@ function WorkspaceScreenContent({ serverId, workspaceId }: WorkspaceScreenProps)
|
||||
<View style={styles.emptyState}>
|
||||
<ActivityIndicator color={theme.colors.foregroundMuted} />
|
||||
</View>
|
||||
) : isMissingWorkspaceExecutionAuthority ? (
|
||||
<View style={styles.emptyState}>
|
||||
<Text style={styles.emptyStateText}>
|
||||
Workspace execution directory is missing. Reload workspace data before opening tabs.
|
||||
</Text>
|
||||
</View>
|
||||
) : !activeTabDescriptor ? (
|
||||
!hasHydratedAgents ? (
|
||||
<View style={styles.emptyState}>
|
||||
@@ -2057,7 +1989,7 @@ function WorkspaceScreenContent({ serverId, workspaceId }: WorkspaceScreenProps)
|
||||
<DropdownMenuItem
|
||||
testID="workspace-header-copy-path"
|
||||
leading={<Copy size={16} color={theme.colors.foregroundMuted} />}
|
||||
disabled={!isAbsolutePath(normalizedWorkspaceId)}
|
||||
disabled={!workspaceDirectory}
|
||||
onSelect={handleCopyWorkspacePath}
|
||||
>
|
||||
Copy workspace path
|
||||
@@ -2080,10 +2012,12 @@ function WorkspaceScreenContent({ serverId, workspaceId }: WorkspaceScreenProps)
|
||||
<View style={styles.headerRight}>
|
||||
{!isMobile && isGitCheckout ? (
|
||||
<>
|
||||
<WorkspaceGitActions
|
||||
serverId={normalizedServerId}
|
||||
cwd={normalizedWorkspaceId}
|
||||
/>
|
||||
{workspaceDirectory ? (
|
||||
<WorkspaceGitActions
|
||||
serverId={normalizedServerId}
|
||||
cwd={workspaceDirectory}
|
||||
/>
|
||||
) : null}
|
||||
<Tooltip delayDuration={0} enabledOnDesktop enabledOnMobile={false}>
|
||||
<TooltipTrigger asChild>
|
||||
<Pressable
|
||||
@@ -2233,10 +2167,8 @@ function WorkspaceScreenContent({ serverId, workspaceId }: WorkspaceScreenProps)
|
||||
onCloseTabsToLeft={handleCloseTabsToLeft}
|
||||
onCloseTabsToRight={handleCloseTabsToRight}
|
||||
onCloseOtherTabs={handleCloseOtherTabs}
|
||||
onSelectNewTabOption={handleSelectNewTabOption}
|
||||
newTabAgentOptionId={NEW_TAB_AGENT_OPTION_ID}
|
||||
onCreateLauncherTab={handleCreateLauncherTab}
|
||||
onReorderTabs={handleReorderTabsInFocusedPane}
|
||||
onNewTerminalTab={handleCreateTerminal}
|
||||
onSplitRight={() => {}}
|
||||
onSplitDown={() => {}}
|
||||
showPaneSplitActions={false}
|
||||
@@ -2269,13 +2201,11 @@ function WorkspaceScreenContent({ serverId, workspaceId }: WorkspaceScreenProps)
|
||||
onCloseTabsToLeft={handleCloseTabsToLeftInPane}
|
||||
onCloseTabsToRight={handleCloseTabsToRightInPane}
|
||||
onCloseOtherTabs={handleCloseOtherTabsInPane}
|
||||
onSelectNewTabOption={handleSelectNewTabOption}
|
||||
newTabAgentOptionId={NEW_TAB_AGENT_OPTION_ID}
|
||||
onCreateLauncherTab={handleCreateLauncherTab}
|
||||
buildPaneContentModel={buildDesktopPaneContentModel}
|
||||
onFocusPane={handleFocusPane}
|
||||
onNewTerminalTab={handleCreateTerminal}
|
||||
onSplitPane={handleSplitPane}
|
||||
onSplitPaneEmpty={handleCreateDraftSplit}
|
||||
onSplitPaneEmpty={handleCreateLauncherSplit}
|
||||
onMoveTabToPane={handleMoveTabToPane}
|
||||
onResizeSplit={handleResizePaneSplit}
|
||||
onReorderTabsInPane={handleReorderTabsInPane}
|
||||
@@ -2290,13 +2220,15 @@ function WorkspaceScreenContent({ serverId, workspaceId }: WorkspaceScreenProps)
|
||||
</View>
|
||||
|
||||
{(!isFocusModeEnabled || isMobile) && (
|
||||
<ExplorerSidebar
|
||||
serverId={normalizedServerId}
|
||||
workspaceId={normalizedWorkspaceId}
|
||||
workspaceRoot={normalizedWorkspaceId}
|
||||
isGit={isGitCheckout}
|
||||
onOpenFile={handleOpenFileFromExplorer}
|
||||
/>
|
||||
workspaceDirectory ? (
|
||||
<ExplorerSidebar
|
||||
serverId={normalizedServerId}
|
||||
workspaceId={normalizedWorkspaceId}
|
||||
workspaceRoot={workspaceDirectory}
|
||||
isGit={isGitCheckout}
|
||||
onOpenFile={handleOpenFileFromExplorer}
|
||||
/>
|
||||
) : null
|
||||
)}
|
||||
</View>
|
||||
</View>
|
||||
|
||||
Reference in New Issue
Block a user