The preview pane is explicit and correct that reads are a snapshot rather than a live tail (FilePreviewPane.tsx:68-72):
* 内容是「打开时快照 + 手动刷新」,刻意不跟 watch 自动重读:agent 每
* 秒追写日志会冲掉正在编辑的草稿 — 快照语义才编辑得安稳。
Good call. The problem is that the write side never reconciles that snapshot with reality. FilePreviewPane.tsx:196-208:
const save = () =>
mutations.upload.mutate(
{ path: selected.path, file: new File([draft], selected.name) },
uploadFile (envd-client.ts:152-173) POSTs the bytes to /files?path=... unconditionally. There is no If-Match, no mtime check, no size check against the EnvdEntry we opened with — nothing that would notice the file changed underneath.
The failure is the classic lost update, and this product's whole premise makes it likely rather than theoretical, because the other writer is an autonomous agent:
- Operator opens
~/project/config.json in the workbench. content resolves; draft is seeded from it at line 140-144.
- The agent in the sandbox rewrites
config.json — adds a key, appends a log line, whatever.
- Operator fixes a typo in their editor and clicks 保存.
- Observed: a toast reading 已保存 config.json (line 201), the agent's write is gone with no trace, and
content.refetch() at line 204 immediately re-establishes the clobbered content as the new baseline so dirty goes false and the UI looks clean.
- Expected: at minimum a "this file changed on disk since you opened it — overwrite / reload / diff?" prompt.
Note the watcher is already running on the focus directory (FileTreePane.tsx:244) and already knows the file changed — that signal is currently only used to invalidate the directory listing, not to warn the person holding a stale draft.
A cheap first cut, no protocol change needed: capture selected.modifiedTime and selected.size when the read resolves, and before uploading re-listDir the parent (or re-stat via the same call the tree already makes) and compare. If they differ, open a confirm dialog instead of writing. The honest-error philosophy stated at line 71 ("下一次读或刷新以诚实的错误态收场,不猜") is exactly right and should extend to the write path.
A second, smaller thing: new File([draft], selected.name) gives the blob no MIME type, so it uploads as application/octet-stream. Harmless with the current server, but it means a round-trip through the editor can change what the server later reports for the same file.
The preview pane is explicit and correct that reads are a snapshot rather than a live tail (
FilePreviewPane.tsx:68-72):Good call. The problem is that the write side never reconciles that snapshot with reality.
FilePreviewPane.tsx:196-208:uploadFile(envd-client.ts:152-173) POSTs the bytes to/files?path=...unconditionally. There is no If-Match, no mtime check, no size check against theEnvdEntrywe opened with — nothing that would notice the file changed underneath.The failure is the classic lost update, and this product's whole premise makes it likely rather than theoretical, because the other writer is an autonomous agent:
~/project/config.jsonin the workbench.contentresolves;draftis seeded from it at line 140-144.config.json— adds a key, appends a log line, whatever.content.refetch()at line 204 immediately re-establishes the clobbered content as the new baseline sodirtygoes false and the UI looks clean.Note the watcher is already running on the focus directory (
FileTreePane.tsx:244) and already knows the file changed — that signal is currently only used to invalidate the directory listing, not to warn the person holding a stale draft.A cheap first cut, no protocol change needed: capture
selected.modifiedTimeandselected.sizewhen the read resolves, and before uploading re-listDirthe parent (or re-stat via the same call the tree already makes) and compare. If they differ, open a confirm dialog instead of writing. The honest-error philosophy stated at line 71 ("下一次读或刷新以诚实的错误态收场,不猜") is exactly right and should extend to the write path.A second, smaller thing:
new File([draft], selected.name)gives the blob no MIME type, so it uploads asapplication/octet-stream. Harmless with the current server, but it means a round-trip through the editor can change what the server later reports for the same file.