Keep delayed source responses behind the current preview intent
Why: Late source requests must not replace a newer preview or survive conversation navigation. Evidence: Frontend typecheck, 221 unit tests, knip ratchet and build passed; delayed-response regressions reproduced the old failure. Impact: The existing store owns source requests and invalidates stale responses on preview and context changes. Refs: Agent-System-Dev feature-fast-slow-u2-u3-plan; independent review finding 4
This commit is contained in:
@@ -1,7 +1,6 @@
|
||||
// 右栏(第三栏):「工作 / 文件」双入口。
|
||||
// 工作 = 当前任务卡 + 收件箱(待处理事项、后台任务卡、执行记录);
|
||||
// 文件 = 文件列表(搜索/上传)与多标签预览(预览、编辑、下载)——预览不再是独立第四栏。
|
||||
import { api } from '../api'
|
||||
import { store, useStore } from '../store'
|
||||
import type { Run } from '../contracts'
|
||||
import { InboxList, pendingCount, type ActivityCard, type DocBridge, type InboxFilter } from '@ui'
|
||||
@@ -14,15 +13,7 @@ import { PreviewPane } from './PreviewPane'
|
||||
// 工件解析/打开注入:渲染器纯展示,doc-ref 词汇经此桥接本仓状态(D29)。
|
||||
// 打开语义走 requestPreview 唯一入口(第三栏「文件」入口)。
|
||||
export const docBridge: DocBridge = {
|
||||
onOpenTaskSource: async (cardId,source)=>{
|
||||
const context=store.getState().currentConvId,project=store.getState().currentProjectId
|
||||
try{const out=await api.taskSource(cardId,source.id)
|
||||
if(context!==store.getState().currentConvId||project!==store.getState().currentProjectId)return
|
||||
if(!out.artifact){store.toast('来源当前不可用,正在核实工作记录');if(context)void store.refreshCards(context);return}
|
||||
store.requestTaskSourcePreview(out.artifact,{notice:out.versionState==='changed'?'当前文件已更新;下方引用来自任务处理时的版本,不能作为当前文件的结论':'任务处理时引用的来源版本;下方是本项发现引用的片段',quote:out.source.quote??'',expectedHash:out.versionState==='current'?out.source.contentHash:undefined})
|
||||
if(out.versionState==='changed'&&context)void store.refreshCards(context)
|
||||
}catch{if(context===store.getState().currentConvId&&project===store.getState().currentProjectId){store.toast('来源当前无法读取,正在核实工作记录');if(context)void store.refreshCards(context)}}
|
||||
},
|
||||
onOpenTaskSource: (cardId, source) => store.openTaskSource(cardId, source),
|
||||
resolveDoc: id => store.getState().artifacts.find(a => a.id === id),
|
||||
onOpenDoc: id => {
|
||||
const art = store.getState().artifacts.find(a => a.id === id)
|
||||
|
||||
+35
-2
@@ -20,6 +20,7 @@ export function isFontSizeChoice(value: number): value is FontSizeChoice {
|
||||
}
|
||||
import type {
|
||||
ActivityCard,
|
||||
TaskSourceRef,
|
||||
AgentProfile,
|
||||
Artifact,
|
||||
Conversation,
|
||||
@@ -218,7 +219,8 @@ function syncFilesEntry(hasTabs: boolean) {
|
||||
|
||||
function setUI(patch: Partial<UIState>) {
|
||||
const ui = { ...state.ui, ...patch }
|
||||
state = { ...state, ui }
|
||||
const navigation=(['rightTab','filesView','rightCollapsed','activeWorkspace'] as const).some(key=>key in patch&&patch[key]!==state.ui[key])
|
||||
state = { ...state, ui,previewOpenIntent:state.previewOpenIntent+(navigation?1:0) }
|
||||
try { localStorage.setItem('as.ui', JSON.stringify(ui)) } catch { /* ignore */ }
|
||||
applyTheme(ui.theme)
|
||||
applyFontSize(ui.fontSize)
|
||||
@@ -1255,6 +1257,36 @@ export const store = {
|
||||
* 工件预览唯一入口:在第三栏「文件」入口打开或激活标签(多文件多标签)。
|
||||
* 打开第一张自动切到预览子视图;关掉最后一张回到文件列表,并收回展开态。
|
||||
*/
|
||||
async openTaskSource(cardId: string, source: TaskSourceRef) {
|
||||
const conversationId = state.currentConvId
|
||||
const projectId = state.currentProjectId
|
||||
const epoch = conversationEpoch
|
||||
const intent = state.previewOpenIntent + 1
|
||||
set({ previewOpenIntent: intent })
|
||||
const isCurrent = () => conversationId === state.currentConvId && projectId === state.currentProjectId
|
||||
&& epoch === conversationEpoch && intent === state.previewOpenIntent
|
||||
try {
|
||||
const out = await api.taskSource(cardId, source.id)
|
||||
if (!isCurrent()) return
|
||||
if (!out.artifact) {
|
||||
store.toast('来源当前不可用,正在核实工作记录')
|
||||
if (conversationId) void store.refreshCards(conversationId)
|
||||
return
|
||||
}
|
||||
store.requestTaskSourcePreview(out.artifact, {
|
||||
notice: out.versionState === 'changed'
|
||||
? '当前文件已更新;下方引用来自任务处理时的版本,不能作为当前文件的结论'
|
||||
: '任务处理时引用的来源版本;下方是本项发现引用的片段',
|
||||
quote: out.source.quote ?? '',
|
||||
expectedHash: out.versionState === 'current' ? out.source.contentHash : undefined,
|
||||
})
|
||||
if (out.versionState === 'changed' && conversationId) void store.refreshCards(conversationId)
|
||||
} catch {
|
||||
if (!isCurrent()) return
|
||||
store.toast('来源当前无法读取,正在核实工作记录')
|
||||
if (conversationId) void store.refreshCards(conversationId)
|
||||
}
|
||||
},
|
||||
requestTaskSourcePreview(a:Artifact,sourceContext:{notice:string;quote:string;expectedHash?:string}){
|
||||
store.requestPreview(a)
|
||||
const conv=previewTabsKey(state.currentConvId)
|
||||
@@ -1311,7 +1343,7 @@ export const store = {
|
||||
togglePreviewExpanded() { setUI({ previewExpanded: !state.ui.previewExpanded }) },
|
||||
activatePreview(id: string) {
|
||||
const conv = previewTabsKey(state.currentConvId)
|
||||
set({ previewActive: { ...state.previewActive, [conv]: id } })
|
||||
set({ previewActive: { ...state.previewActive, [conv]: id },previewOpenIntent:state.previewOpenIntent+1 })
|
||||
},
|
||||
closePreview(id: string) {
|
||||
const conv = previewTabsKey(state.currentConvId)
|
||||
@@ -1319,6 +1351,7 @@ export const store = {
|
||||
set({
|
||||
previewTabs: { ...state.previewTabs, [conv]: next.tabs },
|
||||
previewActive: { ...state.previewActive, [conv]: next.activeId },
|
||||
previewOpenIntent:state.previewOpenIntent+1,
|
||||
})
|
||||
if (next.tabs.length === 0) setUI({ previewExpanded: false })
|
||||
syncFilesEntry(next.tabs.length > 0)
|
||||
|
||||
@@ -443,3 +443,38 @@ it('rejects old revisions and foreign attempts in the task merge contract',()=>{
|
||||
expect(mergeTaskSnapshot({taskId:'t',attemptId:'a',revision:9},{taskId:'t',attemptId:'a',revision:8}).revision).toBe(9)
|
||||
expect(mergeTaskSnapshot({taskId:'t',attemptId:'a',revision:9},{taskId:'t',attemptId:'b',revision:10}).attemptId).toBe('a')
|
||||
})
|
||||
|
||||
it('keeps the newer explicit preview when a source response arrives late',async()=>{
|
||||
const source={id:'s',artifactId:'a',contentHash:'hash',parserRevision:'v1',blockIndex:0,label:'A'}
|
||||
const pending=deferred<Awaited<ReturnType<typeof api.taskSource>>>()
|
||||
vi.spyOn(api,'taskSource').mockReturnValue(pending.promise)
|
||||
const {docBridge}=await import('../src/components/Workbench')
|
||||
const opening=docBridge.onOpenTaskSource!('card',source)
|
||||
store.requestPreview({id:'b',name:'B',kind:'document',projectId:'p',createdAt:'',updatedAt:'',size:0})
|
||||
pending.resolve({artifact:{id:'a',name:'A',kind:'document',projectId:'p',createdAt:'',updatedAt:'',size:0},source,versionState:'current'})
|
||||
await opening
|
||||
expect(store.getState().previewActive.a).toBe('b')
|
||||
})
|
||||
|
||||
it('rejects a source response after leaving and returning to the same conversation',async()=>{
|
||||
const source={id:'s',artifactId:'a',contentHash:'hash',parserRevision:'v1',blockIndex:0,label:'A'}
|
||||
const pending=deferred<Awaited<ReturnType<typeof api.taskSource>>>()
|
||||
vi.spyOn(api,'taskSource').mockReturnValue(pending.promise)
|
||||
const opening=store.openTaskSource('card',source)
|
||||
await store.openConversation('b');await store.openConversation('a')
|
||||
store.requestPreview({id:'new',name:'New',kind:'document',projectId:'p',createdAt:'',updatedAt:'',size:0})
|
||||
pending.resolve({artifact:{id:'a',name:'A',kind:'document',projectId:'p',createdAt:'',updatedAt:'',size:0},source,versionState:'current'})
|
||||
await opening
|
||||
expect(store.getState().previewActive.a).toBe('new')
|
||||
})
|
||||
it('keeps the activated tab when a source request is still pending',async()=>{
|
||||
store.requestPreview({id:'b',name:'B',kind:'document',projectId:'p',createdAt:'',updatedAt:'',size:0})
|
||||
store.requestPreview({id:'c',name:'C',kind:'document',projectId:'p',createdAt:'',updatedAt:'',size:0})
|
||||
const source={id:'s',artifactId:'a',contentHash:'hash',parserRevision:'v1',blockIndex:0,label:'A'}
|
||||
const pending=deferred<Awaited<ReturnType<typeof api.taskSource>>>()
|
||||
vi.spyOn(api,'taskSource').mockReturnValue(pending.promise)
|
||||
const opening=store.openTaskSource('card',source);store.activatePreview('b')
|
||||
pending.resolve({artifact:{id:'a',name:'A',kind:'document',projectId:'p',createdAt:'',updatedAt:'',size:0},source,versionState:'current'})
|
||||
await opening
|
||||
expect(store.getState().previewActive.a).toBe('b')
|
||||
})
|
||||
|
||||
Reference in New Issue
Block a user