feat: add sqlite storage and terminal ui

This commit is contained in:
Mohamed Boudra
2026-03-29 23:20:18 +07:00
parent e5fd61f131
commit 7d02e24d84
189 changed files with 17868 additions and 4663 deletions
@@ -19,6 +19,7 @@ function makeAgent(input: {
serverId: "srv",
id: input.id,
provider: "codex",
terminal: false,
status: "idle",
createdAt,
updatedAt: createdAt,
@@ -31,6 +32,7 @@ function makeAgent(input: {
supportsMcpServers: true,
supportsReasoningStream: true,
supportsToolInvocations: true,
supportsTerminalMode: false,
},
currentModeId: null,
availableModes: [],
@@ -8,7 +8,7 @@ import {
View,
type LayoutChangeEvent,
} from "react-native";
import { Columns2, Rows2, SquarePen, SquareTerminal, X } from "lucide-react-native";
import { Columns2, Plus, Rows2, X } from "lucide-react-native";
import { StyleSheet, useUnistyles } from "react-native-unistyles";
import { SortableInlineList } from "@/components/sortable-inline-list";
import {
@@ -36,11 +36,6 @@ import type { WorkspaceTabDescriptor } from "@/screens/workspace/workspace-tabs-
const DROPDOWN_WIDTH = 220;
const LOADING_TAB_LABEL_SKELETON_WIDTH = 80;
type NewTabOptionId = "__new_tab_agent__" | "__new_tab_terminal__";
type NewTabSelection = {
optionId: NewTabOptionId;
paneId?: string;
};
export interface WorkspaceDesktopTabRowItem {
tab: WorkspaceTabDescriptor;
@@ -64,10 +59,8 @@ type WorkspaceDesktopTabsRowProps = {
onCloseTabsToLeft: (tabId: string) => Promise<void> | void;
onCloseTabsToRight: (tabId: string) => Promise<void> | void;
onCloseOtherTabs: (tabId: string) => Promise<void> | void;
onSelectNewTabOption: (selection: NewTabSelection) => void;
newTabAgentOptionId: NewTabOptionId;
onCreateLauncherTab: (input: { paneId?: string }) => void;
onReorderTabs: (nextTabs: WorkspaceTabDescriptor[]) => void;
onNewTerminalTab: (input: { paneId?: string }) => void;
onSplitRight: () => void;
onSplitDown: () => void;
externalDndContext?: boolean;
@@ -76,6 +69,9 @@ type WorkspaceDesktopTabsRowProps = {
};
function getFallbackTabLabel(tab: WorkspaceTabDescriptor): string {
if (tab.target.kind === "launcher") {
return "New Tab";
}
if (tab.target.kind === "draft") {
return "New Agent";
}
@@ -298,10 +294,8 @@ export function WorkspaceDesktopTabsRow({
onCloseTabsToLeft,
onCloseTabsToRight,
onCloseOtherTabs,
onSelectNewTabOption,
newTabAgentOptionId,
onCreateLauncherTab,
onReorderTabs,
onNewTerminalTab,
onSplitRight,
onSplitDown,
externalDndContext = false,
@@ -309,8 +303,7 @@ export function WorkspaceDesktopTabsRow({
tabDropPreviewIndex = null,
}: WorkspaceDesktopTabsRowProps) {
const { theme } = useUnistyles();
const newAgentTabKeys = useShortcutKeys("workspace-tab-new");
const newTerminalTabKeys = useShortcutKeys("workspace-terminal-new");
const newTabKeys = useShortcutKeys("workspace-tab-new");
const splitRightKeys = useShortcutKeys("workspace-pane-split-right");
const splitDownKeys = useShortcutKeys("workspace-pane-split-down");
const [tabsContainerWidth, setTabsContainerWidth] = useState<number>(0);
@@ -437,48 +430,22 @@ export function WorkspaceDesktopTabsRow({
<View style={styles.tabsActions} onLayout={handleTabsActionsLayout}>
<Tooltip delayDuration={0} enabledOnDesktop enabledOnMobile={false}>
<TooltipTrigger
testID="workspace-new-agent-tab"
onPress={() =>
onSelectNewTabOption({
optionId: newTabAgentOptionId,
paneId,
})
}
testID="workspace-new-tab"
onPress={() => onCreateLauncherTab({ paneId })}
accessibilityRole="button"
accessibilityLabel="New agent tab"
accessibilityLabel="New tab"
style={({ hovered, pressed }) => [
styles.newTabActionButton,
(hovered || pressed) && styles.newTabActionButtonHovered,
]}
>
<SquarePen size={theme.iconSize.sm} color={theme.colors.foregroundMuted} />
<Plus size={theme.iconSize.sm} color={theme.colors.foregroundMuted} />
</TooltipTrigger>
<TooltipContent side="bottom" align="center" offset={8}>
<View style={styles.newTabTooltipRow}>
<Text style={styles.newTabTooltipText}>New agent tab</Text>
{newAgentTabKeys ? (
<Shortcut chord={newAgentTabKeys} style={styles.newTabTooltipShortcut} />
) : null}
</View>
</TooltipContent>
</Tooltip>
<Tooltip delayDuration={0} enabledOnDesktop enabledOnMobile={false}>
<TooltipTrigger
onPress={() => onNewTerminalTab({ paneId })}
accessibilityRole="button"
accessibilityLabel="New terminal tab"
style={({ hovered, pressed }) => [
styles.newTabActionButton,
(hovered || pressed) && styles.newTabActionButtonHovered,
]}
>
<SquareTerminal size={theme.iconSize.sm} color={theme.colors.foregroundMuted} />
</TooltipTrigger>
<TooltipContent side="bottom" align="center" offset={8}>
<View style={styles.newTabTooltipRow}>
<Text style={styles.newTabTooltipText}>New terminal tab</Text>
{newTerminalTabKeys ? (
<Shortcut chord={newTerminalTabKeys} style={styles.newTabTooltipShortcut} />
<Text style={styles.newTabTooltipText}>New tab</Text>
{newTabKeys ? (
<Shortcut chord={newTabKeys} style={styles.newTabTooltipShortcut} />
) : null}
</View>
</TooltipContent>
@@ -0,0 +1,22 @@
import { describe, expect, it } from "vitest";
import { buildWorkspaceDraftAgentConfig } from "./workspace-draft-agent-config";
describe("workspace-draft-agent-config", () => {
it("builds chat-only config for workspace draft agents", () => {
expect(
buildWorkspaceDraftAgentConfig({
provider: "codex",
cwd: "/tmp/project",
modeId: "auto",
model: "gpt-5.4",
thinkingOptionId: "high",
}),
).toEqual({
provider: "codex",
cwd: "/tmp/project",
modeId: "auto",
model: "gpt-5.4",
thinkingOptionId: "high",
});
});
});
@@ -0,0 +1,17 @@
import type { AgentSessionConfig } from "@server/server/agent/agent-sdk-types";
export function buildWorkspaceDraftAgentConfig(input: {
provider: AgentSessionConfig["provider"];
cwd: string;
modeId?: string;
model?: string;
thinkingOptionId?: string;
}): AgentSessionConfig {
return {
provider: input.provider,
cwd: input.cwd,
...(input.modeId ? { modeId: input.modeId } : {}),
...(input.model ? { model: input.model } : {}),
...(input.thinkingOptionId ? { thinkingOptionId: input.thinkingOptionId } : {}),
};
}
@@ -1,22 +1,19 @@
import { useCallback, useEffect, useMemo, useRef } from "react";
import { useCallback, useMemo, useRef } from "react";
import { Keyboard, Platform, ScrollView, Text, View } from "react-native";
import { StyleSheet } from "react-native-unistyles";
import { AgentInputArea } from "@/components/agent-input-area";
import { Composer } from "@/components/composer";
import { FileDropZone } from "@/components/file-drop-zone";
import { AgentStreamView } from "@/components/agent-stream-view";
import type { ImageAttachment } from "@/components/message-input";
import { useAgentFormState } from "@/hooks/use-agent-form-state";
import { useAgentInputDraft } from "@/hooks/use-agent-input-draft";
import { useDraftAgentCreateFlow } from "@/hooks/use-draft-agent-create-flow";
import { useHostRuntimeClient, useHostRuntimeIsConnected } from "@/runtime/host-runtime";
import { buildWorkspaceDraftAgentConfig } from "@/screens/workspace/workspace-draft-agent-config";
import { buildDraftStoreKey } from "@/stores/draft-keys";
import type { Agent } from "@/stores/session-store";
import { encodeImages } from "@/utils/encode-images";
import { shouldAutoFocusWorkspaceDraftComposer } from "@/screens/workspace/workspace-draft-pane-focus";
import type {
AgentCapabilityFlags,
AgentSessionConfig,
} from "@server/server/agent/agent-sdk-types";
import type { AgentCapabilityFlags } from "@server/server/agent/agent-sdk-types";
import type { AgentSnapshotPayload } from "@server/shared/messages";
const EMPTY_PENDING_PERMISSIONS = new Map();
@@ -27,6 +24,7 @@ const DRAFT_CAPABILITIES: AgentCapabilityFlags = {
supportsMcpServers: false,
supportsReasoningStream: false,
supportsToolInvocations: false,
supportsTerminalMode: false,
};
type WorkspaceDraftAgentTabProps = {
@@ -51,65 +49,31 @@ export function WorkspaceDraftAgentTab({
const client = useHostRuntimeClient(serverId);
const isConnected = useHostRuntimeIsConnected(serverId);
const addImagesRef = useRef<((images: ImageAttachment[]) => void) | null>(null);
const draftInput = useAgentInputDraft(
buildDraftStoreKey({
serverId,
agentId: tabId,
draftId,
}),
const draftStoreKey = useMemo(
() =>
buildDraftStoreKey({
serverId,
agentId: tabId,
draftId,
}),
[draftId, serverId, tabId],
);
const {
selectedProvider,
setProviderFromUser,
selectedMode,
setModeFromUser,
selectedModel,
setModelFromUser,
selectedThinkingOptionId,
setThinkingOptionFromUser,
workingDir,
setWorkingDir,
providerDefinitions,
modeOptions,
availableModels,
allProviderModels,
isAllModelsLoading,
availableThinkingOptions,
isModelLoading,
setProviderAndModelFromUser,
persistFormPreferences,
} = useAgentFormState({
initialServerId: serverId,
initialValues: { workingDir: workspaceId },
isVisible: true,
isCreateFlow: true,
onlineServerIds: isConnected ? [serverId] : [],
});
// Lock working directory to workspace.
useEffect(() => {
if (workingDir.trim() === workspaceId.trim()) {
return;
}
setWorkingDir(workspaceId);
}, [setWorkingDir, workingDir, workspaceId]);
const effectiveDraftModelId = useMemo(() => {
if (selectedModel.trim()) {
return selectedModel.trim();
}
return availableModels.find((model) => model.isDefault)?.id ?? availableModels[0]?.id ?? "";
}, [availableModels, selectedModel]);
const effectiveDraftThinkingOptionId = useMemo(() => {
if (selectedThinkingOptionId.trim()) {
return selectedThinkingOptionId.trim();
}
const selectedModelDefinition =
availableModels.find((model) => model.id === effectiveDraftModelId) ?? null;
return selectedModelDefinition?.defaultThinkingOptionId ?? "";
}, [availableModels, effectiveDraftModelId, selectedThinkingOptionId]);
const draftInput = useAgentInputDraft(
{
draftKey: draftStoreKey,
composer: {
initialServerId: serverId,
initialValues: { workingDir: workspaceId },
isVisible: true,
onlineServerIds: isConnected ? [serverId] : [],
lockedWorkingDir: workspaceId,
},
},
);
const composerState = draftInput.composerState;
if (!composerState) {
throw new Error("Workspace draft composer state is required");
}
const {
formErrorMessage,
@@ -124,13 +88,13 @@ export function WorkspaceDraftAgentTab({
if (!text.trim()) {
return "Initial prompt is required";
}
if (providerDefinitions.length === 0) {
if (composerState.providerDefinitions.length === 0) {
return "No available providers on the selected host";
}
if (isModelLoading) {
if (composerState.isModelLoading) {
return "Model defaults are still loading";
}
if (!effectiveDraftModelId) {
if (!composerState.effectiveModelId) {
return "No model is available for the selected provider";
}
if (!client) {
@@ -139,7 +103,7 @@ export function WorkspaceDraftAgentTab({
return null;
},
onBeforeSubmit: () => {
void persistFormPreferences();
void composerState.persistFormPreferences();
if (Platform.OS === "web") {
(document.activeElement as HTMLElement | null)?.blur?.();
}
@@ -147,13 +111,17 @@ export function WorkspaceDraftAgentTab({
},
buildDraftAgent: (attempt) => {
const now = attempt.timestamp;
const model = effectiveDraftModelId || null;
const thinkingOptionId = effectiveDraftThinkingOptionId || null;
const modeId = modeOptions.length > 0 && selectedMode !== "" ? selectedMode : null;
const model = composerState.effectiveModelId || null;
const thinkingOptionId = composerState.effectiveThinkingOptionId || null;
const modeId =
composerState.modeOptions.length > 0 && composerState.selectedMode !== ""
? composerState.selectedMode
: null;
return {
serverId,
id: tabId,
provider: selectedProvider,
provider: composerState.selectedProvider,
terminal: false,
status: "running",
createdAt: now,
updatedAt: now,
@@ -164,7 +132,7 @@ export function WorkspaceDraftAgentTab({
availableModes: [],
pendingPermissions: [],
persistence: null,
runtimeInfo: { provider: selectedProvider, sessionId: null, model, modeId },
runtimeInfo: { provider: composerState.selectedProvider, sessionId: null, model, modeId },
title: "Agent",
cwd: workspaceId,
model,
@@ -177,21 +145,20 @@ export function WorkspaceDraftAgentTab({
throw new Error("Host is not connected");
}
const modeId = modeOptions.length > 0 && selectedMode !== "" ? selectedMode : undefined;
const config: AgentSessionConfig = {
provider: selectedProvider,
const config = buildWorkspaceDraftAgentConfig({
provider: composerState.selectedProvider,
cwd: workspaceId,
...(modeId ? { modeId } : {}),
...(effectiveDraftModelId ? { model: effectiveDraftModelId } : {}),
...(effectiveDraftThinkingOptionId
? { thinkingOptionId: effectiveDraftThinkingOptionId }
...(composerState.modeOptions.length > 0 && composerState.selectedMode !== ""
? { modeId: composerState.selectedMode }
: {}),
};
model: composerState.effectiveModelId || undefined,
thinkingOptionId: composerState.effectiveThinkingOptionId || undefined,
});
const imagesData = await encodeImages(images);
const result = await client.createAgent({
config,
initialPrompt: text,
...(text ? { initialPrompt: text } : {}),
clientMessageId: attempt.clientMessageId,
...(imagesData && imagesData.length > 0 ? { images: imagesData } : {}),
});
@@ -206,25 +173,6 @@ export function WorkspaceDraftAgentTab({
},
});
const draftCommandConfig = useMemo(() => {
return {
provider: selectedProvider,
cwd: workspaceId,
...(modeOptions.length > 0 && selectedMode !== "" ? { modeId: selectedMode } : {}),
...(effectiveDraftModelId ? { model: effectiveDraftModelId } : {}),
...(effectiveDraftThinkingOptionId
? { thinkingOptionId: effectiveDraftThinkingOptionId }
: {}),
};
}, [
effectiveDraftModelId,
effectiveDraftThinkingOptionId,
modeOptions.length,
selectedMode,
selectedProvider,
workspaceId,
]);
const handleFilesDropped = useCallback((files: ImageAttachment[]) => {
addImagesRef.current?.(files);
}, []);
@@ -265,11 +213,12 @@ export function WorkspaceDraftAgentTab({
</View>
<View style={styles.inputAreaWrapper}>
<AgentInputArea
<Composer
agentId={tabId}
serverId={serverId}
isInputActive={isPaneFocused}
onSubmitMessage={handleCreateFromInput}
allowEmptySubmit={false}
isSubmitLoading={isSubmitting}
blurOnSubmit={true}
value={draftInput.text}
@@ -279,24 +228,9 @@ export function WorkspaceDraftAgentTab({
clearDraft={draftInput.clear}
autoFocus={shouldAutoFocusWorkspaceDraftComposer({ isPaneFocused, isSubmitting })}
onAddImages={handleAddImagesCallback}
commandDraftConfig={draftCommandConfig}
commandDraftConfig={composerState.commandDraftConfig}
statusControls={{
providerDefinitions,
selectedProvider,
onSelectProvider: setProviderFromUser,
modeOptions,
selectedMode,
onSelectMode: setModeFromUser,
models: availableModels,
selectedModel,
onSelectModel: setModelFromUser,
isModelLoading,
allProviderModels,
isAllModelsLoading,
onSelectProviderAndModel: setProviderAndModelFromUser,
thinkingOptions: availableThinkingOptions,
selectedThinkingOptionId,
onSelectThinkingOption: setThinkingOptionFromUser,
...composerState.statusControls,
disabled: isSubmitting,
}}
/>
@@ -36,7 +36,7 @@ export function buildWorkspacePaneContentModel({
const registration = getPanelRegistration(tab.kind);
invariant(registration, `No panel registration for kind: ${tab.kind}`);
return {
key: `${normalizedServerId}:${normalizedWorkspaceId}:${tab.tabId}`,
key: `${normalizedServerId}:${normalizedWorkspaceId}:${tab.tabId}:${tab.kind}`,
Component: registration.component,
paneContextValue: {
serverId: normalizedServerId,
@@ -90,7 +90,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";
@@ -107,11 +106,7 @@ import {
import { findAdjacentPane } from "@/utils/split-navigation";
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 = {
@@ -136,6 +131,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";
}
@@ -149,6 +147,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";
}
@@ -798,10 +799,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);
@@ -809,11 +813,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(
@@ -917,7 +916,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;
@@ -926,57 +924,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" &&
!pinnedAgentIds.has(tab.target.agentId) &&
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,
@@ -986,13 +946,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],
@@ -1104,6 +1057,25 @@ 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) {
@@ -1124,21 +1096,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;
@@ -1149,10 +1107,9 @@ function WorkspaceScreenContent({ serverId, workspaceId }: WorkspaceScreenProps)
return;
}
focusWorkspacePane(persistenceKey, paneId);
openWorkspaceDraftTab();
handleCreateLauncherTab({ paneId });
},
[focusWorkspacePane, openWorkspaceDraftTab, persistenceKey, splitWorkspacePaneEmpty],
[handleCreateLauncherTab, persistenceKey, splitWorkspacePaneEmpty],
);
const killTerminalAsync = killTerminalMutation.mutateAsync;
@@ -1267,29 +1224,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;
@@ -1523,7 +1457,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();
@@ -1556,7 +1490,14 @@ function WorkspaceScreenContent({ serverId, workspaceId }: WorkspaceScreenProps)
return false;
}
},
[activeTabId, handleCloseTabById, handleCreateDraftTab, handleCreateTerminal, navigateToTabId, tabs],
[
activeTabId,
handleCloseTabById,
handleCreateLauncherTab,
handleCreateTerminal,
navigateToTabId,
tabs,
],
);
const handleWorkspacePaneAction = useCallback(
@@ -1571,7 +1512,7 @@ function WorkspaceScreenContent({ serverId, workspaceId }: WorkspaceScreenProps)
}
if (action.id === "workspace.pane.split.right") {
handleCreateDraftSplit({
handleCreateLauncherSplit({
targetPaneId: focusedPane.id,
position: "right",
});
@@ -1579,7 +1520,7 @@ function WorkspaceScreenContent({ serverId, workspaceId }: WorkspaceScreenProps)
}
if (action.id === "workspace.pane.split.down") {
handleCreateDraftSplit({
handleCreateLauncherSplit({
targetPaneId: focusedPane.id,
position: "bottom",
});
@@ -1649,7 +1590,7 @@ function WorkspaceScreenContent({ serverId, workspaceId }: WorkspaceScreenProps)
allTabDescriptorsById,
closeWorkspaceTabWithCleanup,
focusWorkspacePane,
handleCreateDraftSplit,
handleCreateLauncherSplit,
moveWorkspaceTabToPane,
persistenceKey,
focusedPaneTabState.activeTabId,
@@ -1732,6 +1673,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) => {
@@ -1750,47 +1695,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);
@@ -2205,13 +2113,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}
@@ -14,7 +14,7 @@ describe("workspace source of truth consumption", () => {
projectDisplayName: "getpaseo/paseo",
projectRootPath: "/repo/main",
projectKind: "git",
workspaceKind: "local_checkout",
workspaceKind: "checkout",
name: "feat/workspace-sot",
status: "running",
activityAt: new Date("2026-03-01T00:00:00.000Z"),
@@ -76,6 +76,9 @@ function getCloseButtonTestId(tab: WorkspaceTabDescriptor): string {
if (tab.target.kind === "draft") {
return `workspace-draft-close-${tab.target.draftId}`;
}
if (tab.target.kind === "launcher") {
return `workspace-launcher-close-${tab.target.launcherId}`;
}
return `workspace-file-close-${encodeFilePathForPathSegment(tab.target.path)}`;
}
@@ -44,7 +44,7 @@ export function WorkspaceTabPresentationResolver({
return (
<WorkspaceTabPresentationResolverInner
key={tab.kind}
key={`${tab.key}:${tab.kind}`}
registration={registration}
tab={tab}
serverId={serverId}