feat: canonical workspace tab IDs with URL routing and draft agent tabs

Rewrite workspace tab management to use stable tab IDs (agent_, terminal_, draft_, file_) with
dedicated /tab/[tabId] routes, replace separate file-tabs store with unified workspace-tabs store,
support multiple concurrent draft agent tabs, and detect text files by content instead of extension allowlist.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
Mohamed Boudra
2026-03-03 20:11:55 +07:00
co-authored by Claude Opus 4.6
parent 97f0f4b266
commit 73eabd2eea
17 changed files with 1732 additions and 768 deletions
@@ -436,16 +436,28 @@ function AgentScreenContent({
);
const streamItems = streamItemsRaw ?? EMPTY_STREAM_ITEMS;
const pendingCreate = useCreateFlowStore((state) => state.pending);
const pendingByDraftId = useCreateFlowStore((state) => state.pendingByDraftId);
const markPendingCreateLifecycle = useCreateFlowStore(
(state) => state.markLifecycle
);
const clearPendingCreate = useCreateFlowStore((state) => state.clear);
const isPendingCreateForRoute =
Boolean(pendingCreate) &&
pendingCreate?.lifecycle === "active" &&
pendingCreate?.serverId === serverId &&
pendingCreate?.agentId === resolvedAgentId;
const pendingCreate = useMemo(() => {
if (!resolvedAgentId) {
return null;
}
const values = Object.values(pendingByDraftId);
for (const entry of values) {
if (
entry.lifecycle === "active" &&
entry.serverId === serverId &&
entry.agentId === resolvedAgentId
) {
return entry;
}
}
return null;
}, [pendingByDraftId, resolvedAgentId, serverId]);
const isPendingCreateForRoute = Boolean(pendingCreate);
// Select only the specific initializing state
const isInitializingFromMap = useSessionStore((state) =>
@@ -740,7 +752,7 @@ function AgentScreenContent({
item.kind === "user_message" &&
item.id === pendingCreate.clientMessageId
);
if (hasUserMessage && canFinalizePendingCreate) {
if (hasUserMessage && canFinalizePendingCreate) {
if (
resolvedAgentId &&
pendingCreate.images &&
@@ -766,8 +778,8 @@ function AgentScreenContent({
return next;
});
}
markPendingCreateLifecycle("sent");
clearPendingCreate();
markPendingCreateLifecycle({ draftId: pendingCreate.draftId, lifecycle: "sent" });
clearPendingCreate({ draftId: pendingCreate.draftId });
}
}, [
canFinalizePendingCreate,
@@ -51,6 +51,7 @@ import { AGENT_PROVIDER_DEFINITIONS } from '@server/server/agent/provider-manife
import { buildHostAgentDetailRoute } from '@/utils/host-routes'
import { useTauriDragHandlers } from '@/utils/tauri-window'
import { useKeyboardShiftStyle } from '@/hooks/use-keyboard-shift-style'
import { normalizeAgentSnapshot } from '@/utils/agent-snapshots'
const DRAFT_AGENT_ID = '__new_agent__'
const EMPTY_PENDING_PERMISSIONS = new Map()
@@ -1023,9 +1024,15 @@ function DraftAgentScreenContent({
git: gitOptions,
})
const agentId = (result as { id?: string })?.id
const agentId = result.id
if (agentId && selectedServerId) {
updatePendingAgentId(agentId)
updatePendingAgentId({ draftId: draftIdRef.current, agentId })
useSessionStore.getState().setAgents(selectedServerId, (prev) => {
const next = new Map(prev)
next.set(agentId, normalizeAgentSnapshot(result, selectedServerId))
return next
})
void createAgentClient.refreshAgent(agentId).catch(() => undefined)
router.replace(buildHostAgentDetailRoute(selectedServerId, agentId) as any)
return
}
@@ -1035,15 +1042,15 @@ function DraftAgentScreenContent({
message: 'Failed to create agent',
})
onCreateFlowActiveChange?.(false)
markPendingCreateLifecycle('abandoned')
clearPendingCreateAttempt()
markPendingCreateLifecycle({ draftId: draftIdRef.current, lifecycle: 'abandoned' })
clearPendingCreateAttempt({ draftId: draftIdRef.current })
throw new Error('Failed to create agent')
} catch (error) {
const message = error instanceof Error ? error.message : 'Failed to create agent'
dispatch({ type: 'CREATE_FAILED', message })
onCreateFlowActiveChange?.(false)
markPendingCreateLifecycle('abandoned')
clearPendingCreateAttempt()
markPendingCreateLifecycle({ draftId: draftIdRef.current, lifecycle: 'abandoned' })
clearPendingCreateAttempt({ draftId: draftIdRef.current })
throw error // Re-throw so AgentInputArea knows it failed
}
},