mirror of
https://github.com/stablyai/orca.git
synced 2026-10-26 16:02:42 +00:00
feat(editor): move-echo provenance + autosave gate on OpenFile (stage 3 store)
Adds pendingSelfMoveEcho {operationId,targetPath} to OpenFile and has the
rekey action install it + pendingLiveDiskVerification on dirty autosave-capable
destinations (moveOperationId arg), atomically in the same commit that re-homes
the tab — so the verify gate survives the rekey and its op-id token supersedes a
stale in-flight verification. Replaces the module-scoped generation map (which
broke under rekey). Verification-reader wiring + coordinator follow.
Co-authored-by: Orca <help@stably.ai>
This commit is contained in:
@@ -189,6 +189,39 @@ describe('rekeyOpenFilesForPathChange', () => {
|
||||
expect(moved.filePath).toBe('/repo/named.md')
|
||||
})
|
||||
|
||||
it('installs the move-echo gate + provenance on a dirty destination when moveOperationId is set', () => {
|
||||
seedEditTab()
|
||||
const oldId = useAppStore.getState().openFiles[0]!.id
|
||||
const newPath = '/repo/sub/a.md'
|
||||
|
||||
useAppStore.getState().rekeyOpenFilesForPathChange({
|
||||
worktreeId: 'wt-1',
|
||||
rekeys: [rekeyFor(oldId, newPath, 'sub/a.md')],
|
||||
moveOperationId: 'op-42'
|
||||
})
|
||||
|
||||
const moved = useAppStore.getState().openFiles[0]!
|
||||
// Autosave is gated synchronously by the same commit that re-homes the tab.
|
||||
expect(moved.pendingLiveDiskVerification).toBe(true)
|
||||
expect(moved.pendingSelfMoveEcho).toEqual({ operationId: 'op-42', targetPath: newPath })
|
||||
})
|
||||
|
||||
it('does not gate a clean destination', () => {
|
||||
seedEditTab()
|
||||
const oldId = useAppStore.getState().openFiles[0]!.id
|
||||
useAppStore.getState().markFileDirty(oldId, false)
|
||||
|
||||
useAppStore.getState().rekeyOpenFilesForPathChange({
|
||||
worktreeId: 'wt-1',
|
||||
rekeys: [rekeyFor(oldId, '/repo/sub/a.md', 'sub/a.md')],
|
||||
moveOperationId: 'op-42'
|
||||
})
|
||||
|
||||
const moved = useAppStore.getState().openFiles[0]!
|
||||
expect(moved.pendingLiveDiskVerification).toBeUndefined()
|
||||
expect(moved.pendingSelfMoveEcho).toBeUndefined()
|
||||
})
|
||||
|
||||
it('migrates a pending editor reveal to the new path', () => {
|
||||
seedEditTab()
|
||||
const oldId = useAppStore.getState().openFiles[0]!.id
|
||||
|
||||
@@ -273,6 +273,11 @@ export type OpenFile = {
|
||||
* Separate owner from the restored scan's flag so the two can't clear each
|
||||
* other's gate. Transient, not persisted, not carried across a re-home. */
|
||||
pendingLiveDiskVerification?: boolean
|
||||
/** Routes the destination watcher echo of an Orca-owned move into content
|
||||
* verification. Stored ON the tab so it (and its operationId token) survive
|
||||
* the atomic rekey; the token also supersedes a stale in-flight verification
|
||||
* when the tab is moved again. Transient, not persisted. */
|
||||
pendingSelfMoveEcho?: { operationId: string; targetPath: string }
|
||||
/** Why: diff bodies are cached in EditorPanel. Re-selecting an existing diff
|
||||
* tab from the tree bumps this so the panel refetches instead of reusing a
|
||||
* stale snapshot. */
|
||||
@@ -540,6 +545,9 @@ export type EditorSlice = {
|
||||
rekeyOpenFilesForPathChange: (args: {
|
||||
worktreeId: string
|
||||
rekeys: readonly OpenFilePathRekey[]
|
||||
/** When set, dirty autosave-capable destinations get a move-echo provenance
|
||||
* + a synchronous autosave gate so the watcher can content-verify the echo. */
|
||||
moveOperationId?: string
|
||||
}) => RekeyOpenFilesResult
|
||||
clearUntitled: (fileId: string) => void
|
||||
openDiff: (
|
||||
@@ -2633,7 +2641,7 @@ export const createEditorSlice: StateCreator<AppState, [], [], EditorSlice> = (s
|
||||
}
|
||||
}),
|
||||
|
||||
rekeyOpenFilesForPathChange: ({ worktreeId, rekeys }) => {
|
||||
rekeyOpenFilesForPathChange: ({ worktreeId, rekeys, moveOperationId }) => {
|
||||
if (rekeys.length === 0) {
|
||||
return { ok: true }
|
||||
}
|
||||
@@ -2674,6 +2682,13 @@ export const createEditorSlice: StateCreator<AppState, [], [], EditorSlice> = (s
|
||||
}
|
||||
// Spread the whole OpenFile so every field — including ones this action
|
||||
// doesn't know about — survives; change only the path-derived ones.
|
||||
// Install the move-echo gate atomically here so autosave is suspended
|
||||
// before any watcher echo can be verified (only a dirty autosave-capable
|
||||
// tab can be clobbered by autosave, so others need no gate).
|
||||
const gatesEcho =
|
||||
moveOperationId !== undefined &&
|
||||
f.isDirty &&
|
||||
(f.mode === 'edit' || (f.mode === 'diff' && f.diffSource === 'unstaged'))
|
||||
return {
|
||||
...f,
|
||||
id: rekey.newFileId,
|
||||
@@ -2683,7 +2698,18 @@ export const createEditorSlice: StateCreator<AppState, [], [], EditorSlice> = (s
|
||||
...(rekey.newMarkdownPreviewSourceFileId !== undefined
|
||||
? { markdownPreviewSourceFileId: rekey.newMarkdownPreviewSourceFileId }
|
||||
: {}),
|
||||
...(rekey.consumeUntitled ? { isUntitled: false, deleteUntouchedOnClose: undefined } : {})
|
||||
...(rekey.consumeUntitled
|
||||
? { isUntitled: false, deleteUntouchedOnClose: undefined }
|
||||
: {}),
|
||||
...(gatesEcho
|
||||
? {
|
||||
pendingLiveDiskVerification: true,
|
||||
pendingSelfMoveEcho: {
|
||||
operationId: moveOperationId,
|
||||
targetPath: rekey.newFilePath
|
||||
}
|
||||
}
|
||||
: {})
|
||||
}
|
||||
})
|
||||
|
||||
|
||||
Reference in New Issue
Block a user