openclaw/apps/web/lib/chat-tabs.ts
kumarabhirup 89289bb31d
feat(chat): multi-session chat tabs with stop controls UI
Tab-based multi-chat system supporting concurrent parent and subagent sessions, per-session stop controls, layout/scroll fixes, and attachment display improvements.
2026-03-15 00:31:55 -07:00

179 lines
4.2 KiB
TypeScript

import {
type Tab,
type TabState,
generateTabId,
openTab,
} from "./tab-state";
export function isChatTab(tab: Tab | undefined | null): tab is Tab {
return tab?.type === "chat";
}
export function isSubagentChatTab(tab: Tab | undefined | null): tab is Tab {
return Boolean(tab?.type === "chat" && tab.sessionKey);
}
export function createBlankChatTab(title = "New Chat"): Tab {
return {
id: generateTabId(),
type: "chat",
title,
};
}
export function createParentChatTab(params: {
sessionId: string;
title?: string;
}): Tab {
return {
id: generateTabId(),
type: "chat",
title: params.title || "New Chat",
sessionId: params.sessionId,
};
}
export function createSubagentChatTab(params: {
sessionKey: string;
parentSessionId: string;
title?: string;
}): Tab {
return {
id: generateTabId(),
type: "chat",
title: params.title || "Subagent",
sessionKey: params.sessionKey,
parentSessionId: params.parentSessionId,
};
}
export function bindParentSessionToChatTab(
state: TabState,
tabId: string,
sessionId: string | null,
): TabState {
return {
...state,
tabs: state.tabs.map((tab) =>
tab.id === tabId
? {
...tab,
sessionId: sessionId ?? undefined,
sessionKey: undefined,
}
: tab,
),
};
}
export function updateChatTabTitle(
state: TabState,
tabId: string,
title: string,
): TabState {
return {
...state,
tabs: state.tabs.map((tab) =>
tab.id === tabId && tab.title !== title
? { ...tab, title }
: tab,
),
};
}
export function syncParentChatTabTitles(
state: TabState,
sessions: Array<{ id: string; title: string }>,
): TabState {
const titleBySessionId = new Map(sessions.map((session) => [session.id, session.title]));
let changed = false;
const tabs = state.tabs.map((tab) => {
if (tab.type !== "chat" || !tab.sessionId) {
return tab;
}
const nextTitle = titleBySessionId.get(tab.sessionId);
if (!nextTitle || nextTitle === tab.title) {
return tab;
}
changed = true;
return { ...tab, title: nextTitle };
});
return changed ? { ...state, tabs } : state;
}
export function syncSubagentChatTabTitles(
state: TabState,
subagents: Array<{ childSessionKey: string; label?: string; task: string }>,
): TabState {
const titleBySessionKey = new Map(
subagents.map((subagent) => [subagent.childSessionKey, subagent.label || subagent.task]),
);
let changed = false;
const tabs = state.tabs.map((tab) => {
if (tab.type !== "chat" || !tab.sessionKey) {
return tab;
}
const nextTitle = titleBySessionKey.get(tab.sessionKey);
if (!nextTitle || nextTitle === tab.title) {
return tab;
}
changed = true;
return { ...tab, title: nextTitle };
});
return changed ? { ...state, tabs } : state;
}
export function openOrFocusParentChatTab(
state: TabState,
params: { sessionId: string; title?: string },
): TabState {
return openTab(state, createParentChatTab(params));
}
export function openOrFocusSubagentChatTab(
state: TabState,
params: { sessionKey: string; parentSessionId: string; title?: string },
): TabState {
return openTab(state, createSubagentChatTab(params));
}
export function closeChatTabsForSession(
state: TabState,
sessionId: string,
): TabState {
const tabs = state.tabs.filter((tab) => {
if (tab.pinned) {
return true;
}
if (tab.type !== "chat") {
return true;
}
return tab.sessionId !== sessionId && tab.parentSessionId !== sessionId;
});
const activeStillExists = tabs.some((tab) => tab.id === state.activeTabId);
return {
tabs,
activeTabId: activeStillExists ? state.activeTabId : tabs[tabs.length - 1]?.id ?? null,
};
}
export function resolveChatIdentityForTab(tab: Tab | undefined | null): {
sessionId: string | null;
subagentKey: string | null;
} {
if (!tab || tab.type !== "chat") {
return { sessionId: null, subagentKey: null };
}
if (tab.sessionKey) {
return {
sessionId: tab.parentSessionId ?? null,
subagentKey: tab.sessionKey,
};
}
return {
sessionId: tab.sessionId ?? null,
subagentKey: null,
};
}