Why: reproduced with headless Chromium - clicking an artifact row with no conversation selected closed the workspace overlay and nothing else appeared. requestPreview stores tabs under currentConvId ?? '_' while the render side required a non-null currentConvId, so the preview pane never mounted in the empty state. The empty state was itself a trap: currentConvId was not persisted, so every reload landed there. Evidence: vitest 85/85 (new preview-empty-state tests for the key contract and orphan-tab merge); browser replay on the fixed build shows the preview pane mounting with rendered docx content from the empty state and the conversation restoring after reload (screenshots in the Dev task card). Impact: previewTabsKey() unifies the read/write key; entering a conversation adopts orphan '_' tabs; the last conversation id persists and restores once on boot; deliverable cards toast when the artifact is gone instead of silently doing nothing. Refs: Dev task feature/empty-state-preview-fix; diagnosis session 2026-10-10.
48 lines
1.9 KiB
TypeScript
48 lines
1.9 KiB
TypeScript
// 预览区标签的纯操作:打开已有标签只激活,关闭激活标签时选中相邻的一个。
|
|
|
|
export interface PreviewTab {
|
|
id: string
|
|
name: string
|
|
}
|
|
|
|
export function openPreviewTab(tabs: PreviewTab[], tab: PreviewTab): { tabs: PreviewTab[]; activeId: string } {
|
|
const i = tabs.findIndex(t => t.id === tab.id)
|
|
if (i >= 0) {
|
|
const next = tabs.slice()
|
|
next[i] = { ...next[i], name: tab.name || next[i].name }
|
|
return { tabs: next, activeId: tab.id }
|
|
}
|
|
return { tabs: [...tabs, tab], activeId: tab.id }
|
|
}
|
|
|
|
export function closePreviewTab(
|
|
tabs: PreviewTab[], activeId: string | null, id: string,
|
|
): { tabs: PreviewTab[]; activeId: string | null } {
|
|
const i = tabs.findIndex(t => t.id === id)
|
|
if (i < 0) return { tabs, activeId }
|
|
const next = tabs.filter(t => t.id !== id)
|
|
if (activeId !== id) return { tabs: next, activeId }
|
|
const neighbor = next[Math.min(i, next.length - 1)] ?? null
|
|
return { tabs: next, activeId: neighbor?.id ?? null }
|
|
}
|
|
|
|
/** 预览标签的会话键。空态(未选会话)统一落在 '_':写入(requestPreview)与
|
|
* 渲染(previewOpen/PreviewPane)共用同一口径,空态点开工件才能出现预览区。 */
|
|
export function previewTabsKey(currentConvId: string | null | undefined): string {
|
|
return currentConvId ?? '_'
|
|
}
|
|
|
|
/** 进入会话时把空态打开的孤儿标签('_' 键)并入会话键下;按 id 去重,
|
|
* 会话侧顺序在前,'_' 键清空。无孤儿或目标即 '_' 时原样返回。 */
|
|
export function mergeOrphanPreviewTabs(
|
|
tabsByConv: Record<string, PreviewTab[]>,
|
|
convId: string,
|
|
): Record<string, PreviewTab[]> {
|
|
const orphan = tabsByConv['_'] ?? []
|
|
if (orphan.length === 0 || convId === '_') return tabsByConv
|
|
const own = tabsByConv[convId] ?? []
|
|
const ids = new Set(own.map(t => t.id))
|
|
const merged = [...own, ...orphan.filter(t => !ids.has(t.id))]
|
|
return { ...tabsByConv, [convId]: merged, '_': [] }
|
|
}
|