every sessions loads properly without any issue

This commit is contained in:
levxn
2026-03-03 19:46:27 +05:30
parent 3f0b8bff5b
commit 7c7b60a5e9
2 changed files with 51 additions and 17 deletions
+1 -1
View File
@@ -9,7 +9,7 @@ import type { GraphNode } from "@/components/AgentGraph";
export const TAB_STORAGE_KEY = "hive:workspace-tabs";
export interface PersistedTabState {
tabs: Array<{ id: string; agentType: string; label: string; backendSessionId?: string }>;
tabs: Array<{ id: string; agentType: string; tabKey?: string; label: string; backendSessionId?: string }>;
activeSessionByAgent: Record<string, string>;
activeWorker: string;
sessions?: Record<string, { messages: ChatMessage[]; graphNodes: GraphNode[] }>;
+50 -16
View File
@@ -20,6 +20,13 @@ import { ApiError } from "@/api/client";
const makeId = () => Math.random().toString(36).slice(2, 9);
/**
* Strip the instance suffix added when multiple tabs share the same agentType.
* e.g. "exports/deep_research::abc123" → "exports/deep_research"
* First-instance keys (no "::") are returned unchanged.
*/
const baseAgentType = (key: string): string => key.split("::")[0];
/** Format seconds into a compact countdown string. */
function formatCountdown(totalSecs: number): string {
const h = Math.floor(totalSecs / 3600);
@@ -55,6 +62,10 @@ function TimerCountdown({ initialSeconds }: { initialSeconds: number }) {
interface Session {
id: string;
agentType: string;
/** The key used in sessionsByAgent / agentStates for this specific tab instance.
* Equals agentType for the first tab; equals "agentType::frontendSessionId" for
* additional tabs opened for the same agent so each gets its own isolated slot. */
tabKey?: string;
label: string;
messages: ChatMessage[];
graphNodes: GraphNode[];
@@ -291,10 +302,14 @@ export default function Workspace() {
if (persisted) {
for (const tab of persisted.tabs) {
if (!initial[tab.agentType]) initial[tab.agentType] = [];
// tabKey is the actual key used in sessionsByAgent (may contain "::" suffix).
// Fall back to agentType for tabs persisted before this field was added.
const tabKey = tab.tabKey || tab.agentType;
if (!initial[tabKey]) initial[tabKey] = [];
const session = createSession(tab.agentType, tab.label);
session.id = tab.id;
session.backendSessionId = tab.backendSessionId;
session.tabKey = tab.tabKey; // restore so future persistence uses correct key
// Restore messages and graph from localStorage (up to 50 messages).
// If the backend session is still alive, loadAgentForType may
// append additional messages fetched from the server.
@@ -303,7 +318,7 @@ export default function Workspace() {
session.messages = cached.messages || [];
session.graphNodes = cached.graphNodes || [];
}
initial[tab.agentType].push(session);
initial[tabKey].push(session);
}
}
@@ -403,11 +418,14 @@ export default function Workspace() {
const sessions: Record<string, { messages: ChatMessage[]; graphNodes: GraphNode[] }> = {};
for (const agentSessions of Object.values(sessionsByAgent)) {
for (const s of agentSessions) {
const tKey = s.tabKey || s.agentType;
tabs.push({
id: s.id,
agentType: s.agentType,
tabKey: s.tabKey,
label: s.label,
backendSessionId: s.backendSessionId || agentStates[s.agentType]?.sessionId || undefined,
// agentStates is keyed by tabKey (unique per tab), not by base agentType
backendSessionId: s.backendSessionId || agentStates[tKey]?.sessionId || undefined,
});
sessions[s.id] = { messages: s.messages, graphNodes: s.graphNodes };
}
@@ -473,7 +491,10 @@ export default function Workspace() {
// --- Agent loading: loadAgentForType ---
const loadingRef = useRef(new Set<string>());
const loadAgentForType = useCallback(async (agentType: string) => {
if (agentType === "new-agent") {
// agentType may be a unique composite key ("exports/foo::sessionId") for additional
// tabs — extract the real agent path for selector checks and API calls.
const agentPath = baseAgentType(agentType);
if (agentPath === "new-agent") {
// Create a queen-only session (no worker) for agent building
updateAgentState(agentType, { loading: true, error: null, ready: false, sessionId: null });
try {
@@ -616,7 +637,7 @@ export default function Workspace() {
}
try {
liveSession = await sessionsApi.create(agentType);
liveSession = await sessionsApi.create(agentPath);
} catch (loadErr: unknown) {
// 424 = credentials required — open the credentials modal
if (loadErr instanceof ApiError && loadErr.status === 424) {
@@ -662,7 +683,7 @@ export default function Workspace() {
// At this point liveSession is guaranteed set — if both reconnect and create
// failed, the throw inside the catch exits the outer try block.
const session = liveSession!;
const displayName = formatAgentDisplayName(session.worker_name || agentType);
const displayName = formatAgentDisplayName(session.worker_name || agentPath);
updateAgentState(agentType, { sessionId: session.session_id, displayName });
// Update the session label
@@ -1471,13 +1492,13 @@ export default function Workspace() {
case "worker_loaded": {
const workerName = event.data?.worker_name as string | undefined;
const agentPathFromEvent = event.data?.agent_path as string | undefined;
const displayName = formatAgentDisplayName(workerName || agentType);
const displayName = formatAgentDisplayName(workerName || baseAgentType(agentType));
// Invalidate cached credential requirements so the modal fetches
// fresh data the next time it opens (the new agent may have
// different credential needs than the previous one).
clearCredentialCache(agentPathFromEvent);
clearCredentialCache(agentType);
clearCredentialCache(baseAgentType(agentType));
// Update agent state: new display name, reset graph so topology refetch triggers
updateAgentState(agentType, {
@@ -1534,7 +1555,7 @@ export default function Workspace() {
const activeSession = currentSessions.find(s => s.id === activeSessionId) || currentSessions[0];
const currentGraph = activeSession
? { nodes: activeSession.graphNodes, title: activeAgentState?.displayName || formatAgentDisplayName(activeWorker) }
? { nodes: activeSession.graphNodes, title: activeAgentState?.displayName || formatAgentDisplayName(baseAgentType(activeWorker)) }
: { nodes: [] as GraphNode[], title: "" };
// Build a flat list of all agent-type tabs for the tab bar
@@ -1736,22 +1757,35 @@ export default function Workspace() {
// Create a new session for any agent type (used by NewTabPopover)
const addAgentSession = useCallback((agentType: string, agentLabel?: string) => {
const sessions = sessionsByAgent[agentType] || [];
const newIndex = sessions.length + 1;
const existingCreds = sessions.length > 0 ? sessions[0].credentials : undefined;
// Count all existing open tabs for this base agent type (first tab uses agentType
// as key; subsequent tabs use "agentType::frontendSessionId" as unique keys).
const existingTabCount = Object.keys(sessionsByAgent).filter(
k => baseAgentType(k) === agentType && (sessionsByAgent[k] || []).length > 0,
).length;
const newIndex = existingTabCount + 1;
const existingCreds = sessionsByAgent[agentType]?.[0]?.credentials;
const displayLabel = agentLabel || formatAgentDisplayName(agentType);
const label = newIndex === 1 ? displayLabel : `${displayLabel} #${newIndex}`;
const newSession = createSession(agentType, label, existingCreds);
// First tab keeps agentType as its key (backward-compatible with all existing
// logic). Additional tabs get a unique key so each has its own isolated
// agentStates slot, its own backend session, and its own tab-bar entry.
const tabKey = existingTabCount === 0 ? agentType : `${agentType}::${newSession.id}`;
if (tabKey !== agentType) {
newSession.tabKey = tabKey;
}
setSessionsByAgent(prev => ({
...prev,
[agentType]: [...(prev[agentType] || []), newSession],
[tabKey]: [newSession],
}));
setActiveSessionByAgent(prev => ({ ...prev, [agentType]: newSession.id }));
setActiveWorker(agentType);
setActiveSessionByAgent(prev => ({ ...prev, [tabKey]: newSession.id }));
setActiveWorker(tabKey);
}, [sessionsByAgent]);
const activeWorkerLabel = activeAgentState?.displayName || formatAgentDisplayName(activeWorker);
const activeWorkerLabel = activeAgentState?.displayName || formatAgentDisplayName(baseAgentType(activeWorker));
return (