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)
+ })
+})