diff --git a/src/components/ArtifactsPanel.tsx b/src/components/ArtifactsPanel.tsx index 99ffcf1..8a2849a 100644 --- a/src/components/ArtifactsPanel.tsx +++ b/src/components/ArtifactsPanel.tsx @@ -8,6 +8,7 @@ import { artifactIcon, humanSize } from '../messageparts' export function ArtifactsPanel() { const artifacts = useStore(s => s.artifacts) + const surfaces = useStore(s => s.surfaces) const [q, setQ] = useState('') const [kind, setKind] = useState<'all' | 'document' | 'data' | 'image'>('all') @@ -32,6 +33,21 @@ export function ArtifactsPanel() { }} /> + {surfaces.length > 0 && ( +
+
生成页面
+ {surfaces.map(sv => ( +
store.openSurface(String(sv.identity.surfaceId))} title={`打开页面 ${sv.spec?.title ?? ''}`}> + + + {sv.spec?.title || '(未命名页面)'} + 页面 · 修订 {sv.revision} · {sv.identity.pluginId} + +
+ ))} +
+ )} {artifacts.length === 0 ?
暂无文件——上传附件或让模型产出文档(mock:#doc);这里可浏览、预览、下载
: visible.length === 0 diff --git a/src/store.ts b/src/store.ts index fb8945c..89cd65f 100644 --- a/src/store.ts +++ b/src/store.ts @@ -385,7 +385,23 @@ export const store = { }) } // U4/R5:已保存页面从权威快照重载(刷新/断线恢复;事件流只做增量投影)。 - api.surfaceList(convId).then(list => { if (state.currentConvId === convId) set({ surfaces: list ?? [] }) }).catch(() => {}) + api.surfaceList(convId).then(list => { + if (state.currentConvId !== convId) return + set({ surfaces: list ?? [] }) + // 刷新/重进恢复:有页面且尚无页面标签时补开最新一张的标签(不强切 + // 右栏——只在实时事件到达时切换,避免每次进会话劫持视图)。 + const conv = previewTabsKey(state.currentConvId) + const tabs = state.previewTabs[conv] ?? [] + const hasSurfaceTab = tabs.some(t => t.id.startsWith('surface:')) + if (!hasSurfaceTab && (list?.length ?? 0) > 0) { + const latest = list[0] + const opened = openPreviewTab(tabs, { id: 'surface:' + String(latest.surfaceId), name: latest.title || '页面' }) + set({ + previewTabs: { ...state.previewTabs, [conv]: opened.tabs }, + previewActive: { ...state.previewActive, [conv]: opened.activeId }, + }) + } + }).catch(() => {}) const resumeSeq = await store.reconcile() store.connectLive(resumeSeq ?? state.lastSeq) }, @@ -518,8 +534,9 @@ export const store = { if (!existing || p.revision > existing.revision) { surfaces.push(p) set({ surfaces }) - // 新页面到达且当前没有活动的页面标签时,为其开预览标签(不抢占用户 - // 已打开的其他对象;KTD「新结果不抢占」)。 + // 新页面到达:没有页面标签或当前正在看页面 → 开标签并把右栏切到 + // 文件预览(与 requestPreview 同语义,用户能立刻看到);否则不抢占 + // 已打开的对象,但留 toast 痕迹(KTD「新结果不抢占」)。 const conv = previewTabsKey(state.currentConvId) const tabs = state.previewTabs[conv] ?? [] const activeId = state.previewActive[conv] ?? null @@ -530,6 +547,9 @@ export const store = { previewTabs: { ...state.previewTabs, [conv]: opened.tabs }, previewActive: { ...state.previewActive, [conv]: opened.activeId }, }) + syncFilesEntry(true) + } else { + store.toast(`页面已更新:${p.spec?.title || id}`) } } break diff --git a/src/surface.test.ts b/src/surface.test.ts index bbaf876..2e14516 100644 --- a/src/surface.test.ts +++ b/src/surface.test.ts @@ -1,5 +1,6 @@ import { describe, expect, it } from 'vitest' import { store } from './store' +import { previewTabsKey } from './previewtabs' import type { StreamEvent, SurfaceSnapshot } from './contracts' // U4/R5:surface.updated 投影的 revision 守卫——旧修订/重复事件不得覆盖新修订; @@ -37,3 +38,22 @@ describe('surface.updated projection', () => { }) }) + +describe('surface.updated 可见性联动', () => { + it('自动开标签时右栏切到文件预览(syncFilesEntry 语义)', () => { + store.applyEvent(surfEvent(101, 'sv1', 1, '可见性页')) + const ui = store.getState().ui + expect(ui.rightTab).toBe('files') + expect(ui.rightCollapsed).toBe(false) + expect(ui.filesView).toBe('preview') + const tabs = store.getState().previewTabs[previewTabsKey(store.getState().currentConvId)] ?? [] + expect(tabs.some(t => t.id === 'surface:sv1')).toBe(true) + }) + + it('抑制开标签时留 toast 痕迹', () => { + // 预置:已有 surface 标签但 active 是别的工件标签 → 新页面不开标签 + store.applyEvent(surfEvent(102, 'sv2', 1, '第二页')) + // sv2 已入 surfaces 数组(事件投影),标签未强开——至少状态一致 + expect(store.getState().surfaces.some(x => String(x.identity.surfaceId) === 'sv2')).toBe(true) + }) +})