diff options
Diffstat (limited to 'src/features/tabs/tabs.ts')
| -rw-r--r-- | src/features/tabs/tabs.ts | 120 |
1 files changed, 60 insertions, 60 deletions
diff --git a/src/features/tabs/tabs.ts b/src/features/tabs/tabs.ts index 53eda78..bc7e30b 100644 --- a/src/features/tabs/tabs.ts +++ b/src/features/tabs/tabs.ts @@ -1,40 +1,40 @@ export interface Tab { - readonly conversationId: string; - readonly model: string; - readonly title: string; - /** The workspace this tab belongs to (the workspace's URL slug). */ - readonly workspaceId: string; + readonly conversationId: string; + readonly model: string; + readonly title: string; + /** The workspace this tab belongs to (the workspace's URL slug). */ + readonly workspaceId: string; } export interface TabsState { - readonly tabs: readonly Tab[]; - readonly activeConversationId: string | null; + readonly tabs: readonly Tab[]; + readonly activeConversationId: string | null; } const DEFAULT_TITLE = "New chat"; const DEFAULT_MAX_TITLE_LENGTH = 40; export function initialState(persisted?: TabsState): TabsState { - if (persisted !== undefined) { - // Migrate tabs persisted before workspaces: assign them to the "default" - // workspace (the fallback for conversations with no workspace). - const tabs = persisted.tabs.map((t) => { - const wid = (t as { workspaceId?: string }).workspaceId; - return { ...t, workspaceId: wid ?? "default" }; - }); - return { tabs, activeConversationId: persisted.activeConversationId }; - } - return { tabs: [], activeConversationId: null }; + if (persisted !== undefined) { + // Migrate tabs persisted before workspaces: assign them to the "default" + // workspace (the fallback for conversations with no workspace). + const tabs = persisted.tabs.map((t) => { + const wid = (t as { workspaceId?: string }).workspaceId; + return { ...t, workspaceId: wid ?? "default" }; + }); + return { tabs, activeConversationId: persisted.activeConversationId }; + } + return { tabs: [], activeConversationId: null }; } export function newDraft(state: TabsState): TabsState { - return { ...state, activeConversationId: null }; + return { ...state, activeConversationId: null }; } export function createTab(state: TabsState, tab: Tab): TabsState { - const exists = state.tabs.some((t) => t.conversationId === tab.conversationId); - const tabs = exists ? state.tabs : [...state.tabs, tab]; - return { tabs, activeConversationId: tab.conversationId }; + const exists = state.tabs.some((t) => t.conversationId === tab.conversationId); + const tabs = exists ? state.tabs : [...state.tabs, tab]; + return { tabs, activeConversationId: tab.conversationId }; } /** @@ -43,54 +43,54 @@ export function createTab(state: TabsState, tab: Tab): TabsState { * strip but the user stays on their current tab. No-op if already open. */ export function openTab(state: TabsState, tab: Tab): TabsState { - const exists = state.tabs.some((t) => t.conversationId === tab.conversationId); - if (exists) return state; - return { tabs: [...state.tabs, tab], activeConversationId: state.activeConversationId }; + const exists = state.tabs.some((t) => t.conversationId === tab.conversationId); + if (exists) return state; + return { tabs: [...state.tabs, tab], activeConversationId: state.activeConversationId }; } export function selectTab(state: TabsState, conversationId: string): TabsState { - return { ...state, activeConversationId: conversationId }; + return { ...state, activeConversationId: conversationId }; } export function closeTab(state: TabsState, conversationId: string): TabsState { - const idx = state.tabs.findIndex((t) => t.conversationId === conversationId); - if (idx === -1) return state; + const idx = state.tabs.findIndex((t) => t.conversationId === conversationId); + if (idx === -1) return state; - const tabs = state.tabs.filter((t) => t.conversationId !== conversationId); + const tabs = state.tabs.filter((t) => t.conversationId !== conversationId); - if (state.activeConversationId !== conversationId) { - return { tabs, activeConversationId: state.activeConversationId }; - } + if (state.activeConversationId !== conversationId) { + return { tabs, activeConversationId: state.activeConversationId }; + } - if (tabs.length === 0) { - return { tabs, activeConversationId: null }; - } + if (tabs.length === 0) { + return { tabs, activeConversationId: null }; + } - // prefer previous tab, else next - const neighborIdx = idx > 0 ? idx - 1 : 0; - const neighbor = tabs[neighborIdx]; - return { tabs, activeConversationId: neighbor?.conversationId ?? null }; + // prefer previous tab, else next + const neighborIdx = idx > 0 ? idx - 1 : 0; + const neighbor = tabs[neighborIdx]; + return { tabs, activeConversationId: neighbor?.conversationId ?? null }; } export function setModel(state: TabsState, conversationId: string, model: string): TabsState { - const tabs = state.tabs.map((t) => (t.conversationId === conversationId ? { ...t, model } : t)); - return { tabs, activeConversationId: state.activeConversationId }; + const tabs = state.tabs.map((t) => (t.conversationId === conversationId ? { ...t, model } : t)); + return { tabs, activeConversationId: state.activeConversationId }; } export function setTitle(state: TabsState, conversationId: string, title: string): TabsState { - const tabs = state.tabs.map((t) => (t.conversationId === conversationId ? { ...t, title } : t)); - return { tabs, activeConversationId: state.activeConversationId }; + const tabs = state.tabs.map((t) => (t.conversationId === conversationId ? { ...t, title } : t)); + return { tabs, activeConversationId: state.activeConversationId }; } export function activeTab(state: TabsState): Tab | null { - if (state.activeConversationId === null) return null; - return state.tabs.find((t) => t.conversationId === state.activeConversationId) ?? null; + if (state.activeConversationId === null) return null; + return state.tabs.find((t) => t.conversationId === state.activeConversationId) ?? null; } export interface ScrollMetrics { - readonly scrollLeft: number; - readonly clientWidth: number; - readonly scrollWidth: number; + readonly scrollLeft: number; + readonly clientWidth: number; + readonly scrollWidth: number; } const STUCK_EPSILON = 1; @@ -102,16 +102,16 @@ const STUCK_EPSILON = 1; * this returns false. Pure: layout measurements in, boolean out. */ export function isStuckToEnd(m: ScrollMetrics): boolean { - const overflows = m.scrollWidth > m.clientWidth + STUCK_EPSILON; - const notAtEnd = m.scrollLeft + m.clientWidth < m.scrollWidth - STUCK_EPSILON; - return overflows && notAtEnd; + const overflows = m.scrollWidth > m.clientWidth + STUCK_EPSILON; + const notAtEnd = m.scrollLeft + m.clientWidth < m.scrollWidth - STUCK_EPSILON; + return overflows && notAtEnd; } export function deriveTitle(message: string, max: number = DEFAULT_MAX_TITLE_LENGTH): string { - const trimmed = message.trim().replace(/\s+/g, " "); - if (trimmed.length === 0) return DEFAULT_TITLE; - if (trimmed.length <= max) return trimmed; - return `${trimmed.slice(0, max)}\u2026`; + const trimmed = message.trim().replace(/\s+/g, " "); + if (trimmed.length === 0) return DEFAULT_TITLE; + if (trimmed.length <= max) return trimmed; + return `${trimmed.slice(0, max)}\u2026`; } /** Minimum length of a tab handle (git-style short id). */ @@ -125,10 +125,10 @@ export const MIN_HANDLE_LENGTH = 4; * id in, the handle string out. (`allIds` may include `conversationId` itself.) */ export function shortHandle(conversationId: string, allIds: readonly string[]): string { - const others = allIds.filter((id) => id !== conversationId); - for (let len = MIN_HANDLE_LENGTH; len < conversationId.length; len++) { - const candidate = conversationId.slice(0, len); - if (!others.some((id) => id.startsWith(candidate))) return candidate; - } - return conversationId; + const others = allIds.filter((id) => id !== conversationId); + for (let len = MIN_HANDLE_LENGTH; len < conversationId.length; len++) { + const candidate = conversationId.slice(0, len); + if (!others.some((id) => id.startsWith(candidate))) return candidate; + } + return conversationId; } |
