From f3e7c9899432986ce5fd6df172580617ebd8ec03 Mon Sep 17 00:00:00 2001 From: CRT Date: Fri, 23 Oct 2026 11:21:51 +0800 Subject: [PATCH] desktop reads: a card that joins a read or gets the 'read just now' answer applies it (no bus event for those: a tab opened again stayed on its skeleton); invalidate before a write's after-refresh, editor save re-reads with force --- apps/desktop/src/screens/editors/TabEditor.tsx | 2 +- apps/desktop/src/store.tsx | 14 +++++++++----- 2 files changed, 10 insertions(+), 6 deletions(-) diff --git a/apps/desktop/src/screens/editors/TabEditor.tsx b/apps/desktop/src/screens/editors/TabEditor.tsx index 109eff5..b38ded1 100644 --- a/apps/desktop/src/screens/editors/TabEditor.tsx +++ b/apps/desktop/src/screens/editors/TabEditor.tsx @@ -23,7 +23,7 @@ export function TabEditor({ tab, project }: { tab: AppTab; project: Project }) { const r = useEditable(project); if (!EDIT_TABS[tab]) return null; if (!r.data) return r.error ? :
; - const p = { project, data: r.data, onSaved: () => { r.refresh(); } }; + const p = { project, data: r.data, onSaved: () => { r.refresh(true); } }; switch (tab) { case 'listings': return ; case 'appinfo': return ; diff --git a/apps/desktop/src/store.tsx b/apps/desktop/src/store.tsx index ada04ce..877b9ca 100644 --- a/apps/desktop/src/store.tsx +++ b/apps/desktop/src/store.tsx @@ -608,7 +608,8 @@ export interface ReadState { justNow?: boolean; /** Its store account waits on a quota pause or 429 backoff until then (epoch ms). */ pausedUntil?: number | null; - refresh: () => Promise; + /** Reads again (joins / memo); `true` skips the "read just now" memo. */ + refresh: (force?: unknown) => Promise; } /** preview: false when the screen already showed this dry run (the editors' Review step); the native dialog still follows. */ @@ -638,7 +639,7 @@ export function useWrite(projectId: string | null) { const o = await api.writeWithConfirm(w.method, projectId, w.args, job); if (o.stage === 'dry-run') d.updateToast(tid, { kind: 'bad', title: t('write.dryFailed'), detail: withHint(failureText(o.dryRun)), progress: undefined, job: undefined }); else if (o.stage === 'dialog') d.updateToast(tid, { kind: 'info', title: t('write.notApplied'), detail: t('write.notAppliedDetail'), progress: undefined, job: undefined }); - else if (o.result?.ok) { d.updateToast(tid, { kind: 'ok', title: w.done, detail: lastLine(o.result.output) || undefined, progress: undefined, job: undefined }); w.after?.(); d.invalidate(projectId, w.method); } + else if (o.result?.ok) { d.updateToast(tid, { kind: 'ok', title: w.done, detail: lastLine(o.result.output) || undefined, progress: undefined, job: undefined }); d.invalidate(projectId, w.method); w.after?.(); } // exit 3: the command stopped for an owner decision it did not get (a price change of a live product: the // Catalog tab's owner action applies it); everything else it could do was done else if (o.result?.exitCode === 3) { d.updateToast(tid, { kind: 'info', title: t('write.declined'), detail: lastLine(o.result.output) || undefined, progress: undefined, job: undefined }); w.after?.(); } @@ -702,11 +703,14 @@ export function useRead(target: string, method: string, o: { network: boolean const skipRef = useRef(o.skip); skipRef.current = o.skip; - const refresh = useCallback(async () => { + const refresh = useCallback(async (force?: unknown) => { if (skipRef.current) return; set(x => ({ ...x, loading: false })); - // a click: highest priority, joins a read in flight, answered from the memo when it finished just now - await dRef.current.schedule(target, method, JSON.parse(argsKey) as Record, 0).catch(() => {}); + // a click: highest priority, joins a read in flight, answered from the memo when it finished just now. The answer + // is applied here too: a join or a memo answer sends no bus event, and a card that mounted after the read (a tab + // opened again) would otherwise wait forever. `force === true` (after a save of this app's files) skips the memo. + const r = await dRef.current.schedule(target, method, JSON.parse(argsKey) as Record, 0, force === true).catch(() => null); + if (r && usable(method, r)) set(x => (x.data === r.data ? x : { data: r.data as T, at: new Date().toISOString(), fromCache: false, loading: false, stale: false, error: null, command: r.command })); }, [target, method, argsKey]); // results of this key, whoever asked for them (this card, the background reader, a write's invalidation)