fix(viewer): final-review fixes — save base from written bytes, honest poll errors, retryable first read

- write.rs: a save's new base is sha256 of the bytes written; the script's
  post-mv hash comes back as disk_hash, and a mismatch (another writer landed
  after us) shows "Changed on disk" instead of being adopted (ledger M2).
- write.rs: conflict:/gone:/read-only strings are constants with a pure
  saved_file() mapping and tests; app/src/viewer/ipcMessages.ts is the one TS
  copy and a cargo test checks it against the Rust originals.
- write.rs: the comment now says the in-place `cat >` fallback follows a
  planted symlink, and why that is accepted (runs as claude).
- poll.rs: a file deleted between `test -f` and `sha256sum` reads as gone.
- viewerState/EditorPane: poll_failed carries its message; only the
  "Start the project before" refusal reads as Container not running, anything
  else gets its own banner and leaves Save enabled.
- EditorPane: a failed first read shows Retry and is retried by the poll.
- spec §1: refused OSC 8 targets keep the refusal card (Task 9 ruling).

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-09-22 21:52:43 -07:00
co-authored by Claude Opus 5.5
parent 398281c8b5
commit bf2291089a
12 changed files with 535 additions and 73 deletions
+2 -2
View File
@@ -1,5 +1,5 @@
import { invoke } from "@tauri-apps/api/core";
import type { Project, ProjectPath, ProjectRemovalReport, ProjectResetOutcome, ContainerInfo, AppSettings, SettingsImportPreview, SettingsImportOutcome, UpdateInfo, ImageUpdateInfo, FileEntry, FileContents, WebTerminalInfo, SttStatus, GatewayStatus, InstallOptions, ClaudeSession, ContainerCapabilities, ScheduledTask, ScheduledTaskInput, SchedulerNotification, AuthBridgeStatus, BrowserViewStatus, BrowserViewPopoutState, BrowserPageState, PlaywrightDetection, BrowserSetupOutcome, BrowserInstallTarget, ContainerStaleness, MigrationOptions, MigrationReport, MigrationState, ClearTokenOutcome, CaCertInfo, UploadOutcome, Note, ViewerFile, ViewerPoll, ViewerState } from "./types";
import type { Project, ProjectPath, ProjectRemovalReport, ProjectResetOutcome, ContainerInfo, AppSettings, SettingsImportPreview, SettingsImportOutcome, UpdateInfo, ImageUpdateInfo, FileEntry, FileContents, WebTerminalInfo, SttStatus, GatewayStatus, InstallOptions, ClaudeSession, ContainerCapabilities, ScheduledTask, ScheduledTaskInput, SchedulerNotification, AuthBridgeStatus, BrowserViewStatus, BrowserViewPopoutState, BrowserPageState, PlaywrightDetection, BrowserSetupOutcome, BrowserInstallTarget, ContainerStaleness, MigrationOptions, MigrationReport, MigrationState, ClearTokenOutcome, CaCertInfo, UploadOutcome, Note, ViewerFile, ViewerPoll, ViewerSaved, ViewerState } from "./types";
// Docker
export const checkDocker = () => invoke<boolean>("check_docker");
@@ -429,6 +429,6 @@ export const viewerReadFile = (maxBytes: number) =>
invoke<ViewerFile>("viewer_read_file", { maxBytes });
export const viewerPollFile = () => invoke<ViewerPoll>("viewer_poll_file");
export const viewerWriteFile = (contentsBase64: string, baseHash: string) =>
invoke<string>("viewer_write_file", { contentsBase64, baseHash });
invoke<ViewerSaved>("viewer_write_file", { contentsBase64, baseHash });
export const viewerChooseFile = (index: number) =>
invoke<ViewerState>("viewer_choose_file", { index });
+8
View File
@@ -987,6 +987,14 @@ export interface ViewerFile {
readonly_reason: string | null;
}
/** A successful save (`write.rs`'s `SavedFile`). */
export interface ViewerSaved {
/** SHA-256 of the bytes written: the editor's new base hash. */
hash: string;
/** What the container hashed right after the swap; differs from `hash` only if another writer landed first. */
disk_hash: string;
}
export interface ViewerPoll {
exists: boolean;
hash: string | null;