feat: track assistant turn changes#408
Conversation
📝 WalkthroughWalkthroughAdds per-session, per-assistant-turn file-change tracking with DB schema, a TurnChange service (record/finalize/get/undo/redo), tool integrations recording writes and redacting sensitive diffs, server routes for fetch/undo/redo, client prefetch/UI wiring and styles, runtime/test wiring, and tests. Turn-Change + Sensitivity Cross‑Cut
sequenceDiagram
actor User
participant Client as Client (message-timeline)
participant UI as UI (session-turn)
participant Server as Server (session routes)
participant DB as Database
participant FS as Filesystem
User->>Client: Open session view
Client->>Server: GET /session/:id/turn-change/:messageID
Server->>DB: Load turn_change_display + restore rows
DB-->>Server: Display + restores
Server-->>Client: TurnChangeDisplay (or null)
Client->>UI: Provide turnChanges + actions
User->>UI: Click Undo (confirm)
UI->>Client: turnChangeActions.undo(messageID)
Client->>Server: POST /session/:id/turn-change/:messageID/undo
Server->>DB: Validate display state & load restores
Server->>FS: Preflight check & apply file state changes
FS-->>Server: write results
Server->>DB: Persist updated display state
Server-->>Client: MutationResult {status:"applied"|"blocked", display?}
Client->>UI: Update displayed turnChange
sequenceDiagram
participant Tool as Tool (edit/apply_patch/write)
participant Sensitive as Sensitive (sanitizer)
participant TurnChange as TurnChange
participant DB as Database
participant Processor as Processor
Tool->>Sensitive: isSensitivePath / sanitize inputs
Sensitive-->>Tool: sanitized metadata / sensitive flag
Tool->>TurnChange: recordWrite(sessionID,messageID,path,before,after)
TurnChange->>DB: Insert/Upsert turn_change_restore rows
DB-->>TurnChange: OK
Processor->>TurnChange: finalize(sessionID,messageID)
TurnChange->>DB: Read restore rows, compute Display
TurnChange->>Sensitive: sanitizeSensitiveDiffs(diffs)
Sensitive-->>TurnChange: redacted diffs
TurnChange->>DB: Upsert turn_change_display, mark restore rows finalized
DB-->>TurnChange: OK
🎯 4 (Complex) | ⏱️ ~65 minutes
Suggested labels:
🚥 Pre-merge checks | ✅ 4 | ❌ 1❌ Failed checks (1 warning)
✅ Passed checks (4 passed)
✏️ Tip: You can configure your own custom pre-merge checks in the settings. ✨ Finishing Touches📝 Generate docstrings
🧪 Generate unit tests (beta)
Review rate limit: 8/10 reviews remaining, refill in 6 minutes and 40 seconds. Comment |
There was a problem hiding this comment.
Code Review
This pull request implements a turn-based file change tracking system, allowing users to view, undo, and redo modifications made by the assistant. It introduces a new TurnChange module, database migrations for persistence, and UI components for the message timeline. Additionally, a sensitive data sanitization layer is added to redact secrets and credentials from diffs and tool outputs. The review feedback identifies several critical improvements for the TurnChange logic, including preventing binary file corruption during restoration, using relative paths for database portability, and optimizing database queries to avoid performance bottlenecks when recording multiple file changes.
There was a problem hiding this comment.
Actionable comments posted: 9
Caution
Some comments are outside the diff and can’t be posted inline due to platform limitations.
⚠️ Outside diff range comments (1)
packages/opencode/src/tool/apply_patch.ts (1)
153-167:⚠️ Potential issue | 🟠 Major | ⚡ Quick winPreserve the overwritten destination state for move undo.
The move path is always recorded as
before: { exists: false }, but this tool already allows moving onto an existing destination. In that case, undo will delete the destination instead of restoring the file that was overwritten there.Also applies to: 307-321
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@packages/opencode/src/tool/apply_patch.ts` around lines 153 - 167, The move operation currently records the destination (movePath) as always non-existent so undos will delete overwritten files instead of restoring them; update the logic around movePath in apply_patch (where movePath is computed and fileChanges.push is called, and the related similar block at lines ~307-321) to detect if movePath exists before applying the move, capture its prior state (exists, content, mode/bom/sensitive flags as appropriate) and include that "before" snapshot in the fileChanges entry so the undo path can restore an overwritten destination rather than deleting it; keep using assertExternalDirectoryEffect(movePath) but add a read/stat of movePath and populate the fileChanges move entry with the destination's previous metadata.
🧹 Nitpick comments (2)
packages/opencode/src/session/session.sql.ts (1)
150-151: ⚡ Quick winAlign index names with the repo naming convention.
The new index names don’t follow the
<table>_<column>_idxpattern consistently, which makes schema naming less predictable.Proposed rename example
- uniqueIndex("turn_change_display_message_idx").on(table.session_id, table.message_id), + uniqueIndex("turn_change_display_session_id_message_id_idx").on(table.session_id, table.message_id), - uniqueIndex("turn_change_restore_file_idx").on(table.session_id, table.message_id, table.file_path), - index("turn_change_restore_message_idx").on(table.session_id, table.message_id), + uniqueIndex("turn_change_restore_session_id_message_id_file_path_idx").on(table.session_id, table.message_id, table.file_path), + index("turn_change_restore_session_id_message_id_idx").on(table.session_id, table.message_id),As per coding guidelines, indexes should follow
<table>_<column>_idxformat.Also applies to: 173-174
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@packages/opencode/src/session/session.sql.ts` around lines 150 - 151, The index names for the turn_change_display indexes don't follow the <table>_<column>_idx pattern; update the uniqueIndex and index calls so they use the table name plus the full column name and _idx suffix (e.g., change uniqueIndex("turn_change_display_message_idx") to "turn_change_display_message_id_idx" and change index("turn_change_display_session_idx") to "turn_change_display_session_id_idx"), and make the same renames for the other two index calls referenced at lines 173-174; locate these in the code where uniqueIndex(...) and index(...) are called on the table.session_id and table.message_id columns and replace the string names accordingly.packages/opencode/test/share/share-next.test.ts (1)
366-432: ⚡ Quick winAdd a regression for the
messagesync path too.This only proves
Session.Event.Diffis redacted.ShareNextalso syncsmessagerecords, and those can embedsummary.diffs, so the current suite would miss the leak if that path regresses. A small case that publishes or full-syncs a message carrying a sensitive embedded diff would close the gap.🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@packages/opencode/test/share/share-next.test.ts` around lines 366 - 432, Add a regression test that covers the message sync path by publishing or full-syncing a message that contains an embedded sensitive diff in its summary (e.g., summary.diffs with a patch containing secrets) and assert ShareNext redacts those secrets the same way as Session.Event.Diff; locate the test harness in share-next.test.ts and reuse the existing HttpClient mock (`seen` array) and wired environment, then publish a Session.Event.Message event (or insert a message that will be synced) for the same session id and assert the recorded sync payload body does not contain secret strings or patch markers and that the serialized message entry marks the diff as sensitive (use the same expectations pattern as the existing Session.Event.Diff assertions).
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.
Inline comments:
In `@packages/app/src/pages/session/message-timeline.tsx`:
- Around line 371-376: The code adds key (`${id}:${message.id}`) to
fetchedTurnChanges before awaiting turnChangeFetch, so transient failures leave
the key cached and prevent retries; fix by only adding the key after a
successful fetch (move fetchedTurnChanges.add(key) into the success path of the
promise returned by turnChangeFetch) or, if you prefer to keep the pre-add,
remove the key in the catch handler (call fetchedTurnChanges.delete(key)) so
failed attempts are not permanently recorded; update the code around
turnChangeFetch, fetchedTurnChanges, and setTurnChanges to ensure keys are only
retained on success.
In `@packages/opencode/migration/20260503120000_turn_changes/migration.sql`:
- Around line 14-28: The migration adds a NOT NULL column `position` on table
`turn_change_restore` but TurnChange.recordWrite (in TurnChange.recordWrite)
does not supply `position`, causing insert failures; either make `position`
nullable or give it a sensible DEFAULT (e.g., 0) in the CREATE TABLE statement,
or update the writer function TurnChange.recordWrite to include and populate
`position` on insert—pick one consistent fix and adjust the CREATE TABLE or the
TurnChange.recordWrite insert logic accordingly.
In `@packages/opencode/src/server/instance/session.ts`:
- Around line 567-623: Add the same session-busy guard to the turn-change
undo/redo route handlers: before calling TurnChange.undo(params) and
TurnChange.redo(params), call SessionRunState.assertNotBusy(...) (using the
sessionID from params) to block mutations when a session run is in progress;
place the check at the start of each POST handler so the request returns/throws
if the session is busy, mirroring other routes in this file.
In `@packages/opencode/src/session/turn-change.ts`:
- Around line 126-127: The code uses string.length to check against
DISPLAY_LIMIT (2 MiB) which measures UTF-16 code units, not bytes; change the
checks to use byte length (e.g., use Buffer.byteLength(s, 'utf8') or
TextEncoder().encode(s).length) for beforeText and afterText, and update the
computation of the `large` boolean accordingly. Replace occurrences where
`beforeText.length` or `afterText.length` are compared to DISPLAY_LIMIT
(including the other occurrence referenced around the 253-254 area) so they
compute byte length first and compare bytes to the DISPLAY_LIMIT (ensure
DISPLAY_LIMIT is interpreted as bytes).
- Around line 253-254: The current early returns drop oversized edits (checks
against RESTORE_LIMIT on input.before/input.after) so large-file writes never
appear in turn tracking; instead, detect when either input.before.exists or
input.after.exists has content.length > RESTORE_LIMIT and emit a status-only
(non-expandable) turn-change entry via the existing turn-change creation path
rather than returning early, preserving normal processing for other cases;
reference RESTORE_LIMIT and the input.before/input.after checks and reuse the
same turn-change emission logic but mark the entry as
non-expandable/status-only.
In `@packages/opencode/src/share/share-next.ts`:
- Around line 279-286: The sync payload currently includes message infos
verbatim which lets MessageV2.info.summary.diffs leak; when building the sync
batch in the sync(...) call update the mapping over messages (where you
currently spread ({ type: "message", data: item.info })) to sanitize or remove
embedded diffs by running item.info.summary.diffs through sanitizeSensitiveDiffs
(or deleting that field) before emitting each message record, and apply the same
sanitization logic in the incremental handler for MessageV2.Event.Updated so
that any updated MessageV2.info is also stripped of summary.diffs; reference the
sync(...) call, the messages.map(...) emission, sanitizeSensitiveDiffs,
MessageV2.Event.Updated and sanitizeSensitiveToolPart to locate both places to
change.
In `@packages/opencode/src/tool/edit.ts`:
- Around line 117-118: TurnChange.recordWrite currently stores only { exists,
content } so when you strip a BOM into contentOld/contentNew (variables
bomDiscarded, contentOld, contentNew) undo/redo restores BOM-less bytes; update
all calls to TurnChange.recordWrite in edit.ts and the matching call in write.ts
to include the BOM state (e.g., { exists, content, bomDiscarded } or a boolean
like hadBOM) in the payload, ensure TurnChange.recordWrite signature and any
consumers accept and persist that extra field, and when restoring apply the BOM
flag to reinsert the original BOM bytes so the restore path is byte-accurate
(also update the other two occurrences that mirror this logic).
In `@packages/opencode/src/tool/sensitive.ts`:
- Around line 80-89: sanitizeSensitiveDiffs currently returns a reduced object
for sensitive files which strips keys downstream code expects (e.g. patch,
additions, deletions) causing runtime errors; update sanitizeSensitiveDiffs to
preserve the original diff shape by spreading the original item and only
overriding/redacting sensitive values: keep all original keys from T (use
{...item}) and set/override sensitive: true, status: item.status ?? "modified",
and replace sensitive content fields (e.g. patch -> "[REDACTED]",
additions/deletions -> null or 0) rather than removing them; use isSensitivePath
to detect sensitive files and ensure the returned object type still conforms to
the expected diff shape while retaining SensitiveStatus handling.
In `@packages/ui/src/components/session-turn.tsx`:
- Around line 567-570: The fallback inside the Show component currently
hardcodes "Updated" when file.additions and file.deletions are undefined;
instead, use the file's actual status field (e.g., file.status or
file.changeType) in that fallback so binary/large adds or deletes are labeled
correctly. Update the fallback span at the Show block to render a mapped value
from file.status (translate statuses like "added"/"deleted"/"modified" to the
same UI strings) so entries without numeric counts show their real change type
rather than always "Updated".
---
Outside diff comments:
In `@packages/opencode/src/tool/apply_patch.ts`:
- Around line 153-167: The move operation currently records the destination
(movePath) as always non-existent so undos will delete overwritten files instead
of restoring them; update the logic around movePath in apply_patch (where
movePath is computed and fileChanges.push is called, and the related similar
block at lines ~307-321) to detect if movePath exists before applying the move,
capture its prior state (exists, content, mode/bom/sensitive flags as
appropriate) and include that "before" snapshot in the fileChanges entry so the
undo path can restore an overwritten destination rather than deleting it; keep
using assertExternalDirectoryEffect(movePath) but add a read/stat of movePath
and populate the fileChanges move entry with the destination's previous
metadata.
---
Nitpick comments:
In `@packages/opencode/src/session/session.sql.ts`:
- Around line 150-151: The index names for the turn_change_display indexes don't
follow the <table>_<column>_idx pattern; update the uniqueIndex and index calls
so they use the table name plus the full column name and _idx suffix (e.g.,
change uniqueIndex("turn_change_display_message_idx") to
"turn_change_display_message_id_idx" and change
index("turn_change_display_session_idx") to
"turn_change_display_session_id_idx"), and make the same renames for the other
two index calls referenced at lines 173-174; locate these in the code where
uniqueIndex(...) and index(...) are called on the table.session_id and
table.message_id columns and replace the string names accordingly.
In `@packages/opencode/test/share/share-next.test.ts`:
- Around line 366-432: Add a regression test that covers the message sync path
by publishing or full-syncing a message that contains an embedded sensitive diff
in its summary (e.g., summary.diffs with a patch containing secrets) and assert
ShareNext redacts those secrets the same way as Session.Event.Diff; locate the
test harness in share-next.test.ts and reuse the existing HttpClient mock
(`seen` array) and wired environment, then publish a Session.Event.Message event
(or insert a message that will be synced) for the same session id and assert the
recorded sync payload body does not contain secret strings or patch markers and
that the serialized message entry marks the diff as sensitive (use the same
expectations pattern as the existing Session.Event.Diff assertions).
🪄 Autofix (Beta)
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: Path: .coderabbit.yaml
Review profile: CHILL
Plan: Pro Plus
Run ID: 94d4c0f4-1f0e-4cf2-b933-d4cca0871f11
📒 Files selected for processing (23)
packages/app/src/pages/session/message-timeline.tsxpackages/opencode/migration/20260503120000_turn_changes/migration.sqlpackages/opencode/src/server/instance/session.tspackages/opencode/src/session/export.tspackages/opencode/src/session/processor.tspackages/opencode/src/session/revert.tspackages/opencode/src/session/session.sql.tspackages/opencode/src/session/summary.tspackages/opencode/src/session/turn-change.tspackages/opencode/src/share/share-next.tspackages/opencode/src/tool/apply_patch.tspackages/opencode/src/tool/edit.tspackages/opencode/src/tool/sensitive.tspackages/opencode/src/tool/write.tspackages/opencode/test/session/export.test.tspackages/opencode/test/session/session-artifacts.test.tspackages/opencode/test/session/turn-change.test.tspackages/opencode/test/share/share-next.test.tspackages/opencode/test/tool/apply_patch.test.tspackages/opencode/test/tool/edit.test.tspackages/opencode/test/tool/write.test.tspackages/ui/src/components/session-turn.csspackages/ui/src/components/session-turn.tsx
There was a problem hiding this comment.
Actionable comments posted: 4
🧹 Nitpick comments (1)
packages/opencode/test/session/turn-change.test.ts (1)
149-172: ⚡ Quick winAdd redo assertion for the BOM round-trip path.
This test proves undo BOM restoration, but not redo back to non-BOM. Adding that assertion will catch BOM preflight regressions.
Suggested test extension
expect((await TurnChange.undo(turn)).status).toBe("applied") const bytes = await fs.readFile(target) expect([...bytes.slice(0, 3)]).toEqual([0xef, 0xbb, 0xbf]) expect(bytes.toString("utf-8")).toBe("\uFEFFbefore\n") + + expect((await TurnChange.redo(turn)).status).toBe("applied") + const bytesRedo = await fs.readFile(target) + expect(bytesRedo.toString("utf-8")).toBe("after\n")🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@packages/opencode/test/session/turn-change.test.ts` around lines 149 - 172, Extend the test to assert redo restores the file back to the non‑BOM state: after the existing undo assertions (using TurnChange.undo(turn) and verifying the file contains BOM/"\uFEFFbefore\n"), call TurnChange.redo(turn) and expect its status toBe("applied"), then read the file bytes and assert the BOM bytes are removed (e.g., first three bytes are not 0xEF,0xBB,0xBF) and the UTF‑8 string equals "after\n"; use the same fixtures/turn used with TurnChange.recordWrite and TurnChange.finalize so it targets the same file.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.
Inline comments:
In `@packages/opencode/src/server/instance/session.ts`:
- Around line 45-59: The code always publishes File.Event.Edited even when the
computed watcher event is "unlink", which causes removed files to be treated as
edited; update the logic around File.Event.Edited (the bus.publish call that
uses File.Event.Edited and file.openPath) to skip publishing when the derived
event === "unlink" (use the same computed `event` used for
FileWatcher.Event.Updated), and ensure lsp.touchFile is only called for
non-"unlink" cases (touchFile(file.openPath, true) already uses this check—align
the Edited publish to the same condition). Reference: File.Event.Edited,
FileWatcher.Event.Updated, event, file.openPath, lsp.touchFile, mode,
file.status.
In `@packages/opencode/src/session/turn-change.ts`:
- Around line 184-193: In currentState(), after reading the file content with
fs.readFile, normalize/remove a UTF‑8 BOM by calling Bom.split(content) and use
the split result's content (not the raw string with BOM) for the returned
FileState.content and for computing the SHA256 hash; this ensures the BOM is
handled consistently with other snapshots (edit.ts, apply_patch.ts) and prevents
false preflight diffs—use the Bom.split API and keep changes localized inside
currentState().
In `@packages/opencode/src/tool/apply_patch.ts`:
- Around line 328-335: The move branch currently records the source deletion via
TurnChange.recordWrite but omits the file BOM, so restores can lose byte-order
info; update the before object in the move branch to include the source BOM
(e.g., add a bom property set from the change object such as change.bom or
change.oldBom) alongside exists and content when calling TurnChange.recordWrite
for move operations (keep using change.filePath, change.oldContent, and
change.movePath as-is).
In `@packages/opencode/test/share/share-next.test.ts`:
- Around line 494-507: The test assumes the message payload is at body.data[0];
instead, find the entry whose parsed body contains an object with type ===
"message" before asserting redaction. Modify the assertions to iterate/search
the seen array (variable seen), parse each entry.body into body, locate const
msg = body.data.find(d => d.type === "message"), then assert on
msg.data.summary.diffs (and on serialized) instead of body.data[0]; keep the
existing checks for absence of secrets and sensitive: true in the diff object.
---
Nitpick comments:
In `@packages/opencode/test/session/turn-change.test.ts`:
- Around line 149-172: Extend the test to assert redo restores the file back to
the non‑BOM state: after the existing undo assertions (using
TurnChange.undo(turn) and verifying the file contains BOM/"\uFEFFbefore\n"),
call TurnChange.redo(turn) and expect its status toBe("applied"), then read the
file bytes and assert the BOM bytes are removed (e.g., first three bytes are not
0xEF,0xBB,0xBF) and the UTF‑8 string equals "after\n"; use the same
fixtures/turn used with TurnChange.recordWrite and TurnChange.finalize so it
targets the same file.
🪄 Autofix (Beta)
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: Path: .coderabbit.yaml
Review profile: CHILL
Plan: Pro Plus
Run ID: a0be23c0-25ab-4c2f-ba6f-a65c14d51884
📒 Files selected for processing (15)
packages/app/src/pages/session/message-timeline.tsxpackages/opencode/migration/20260503120000_turn_changes/migration.sqlpackages/opencode/src/server/instance/session.tspackages/opencode/src/session/session.sql.tspackages/opencode/src/session/turn-change.tspackages/opencode/src/share/share-next.tspackages/opencode/src/tool/apply_patch.tspackages/opencode/src/tool/edit.tspackages/opencode/src/tool/sensitive.tspackages/opencode/src/tool/write.tspackages/opencode/test/session/export.test.tspackages/opencode/test/session/session-artifacts.test.tspackages/opencode/test/session/turn-change.test.tspackages/opencode/test/share/share-next.test.tspackages/ui/src/components/session-turn.tsx
✅ Files skipped from review due to trivial changes (2)
- packages/opencode/migration/20260503120000_turn_changes/migration.sql
- packages/opencode/test/session/export.test.ts
🚧 Files skipped from review as they are similar to previous changes (6)
- packages/opencode/test/session/session-artifacts.test.ts
- packages/opencode/src/share/share-next.ts
- packages/app/src/pages/session/message-timeline.tsx
- packages/opencode/src/tool/edit.ts
- packages/opencode/src/tool/sensitive.ts
- packages/opencode/src/session/session.sql.ts
There was a problem hiding this comment.
Actionable comments posted: 1
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.
Inline comments:
In `@packages/opencode/src/session/turn-change.ts`:
- Around line 452-476: In the mutate function (and the similar block at 487-494)
some permission failures are being classified as generic
"conflict"/"write_failed"; instead, when permission checks fail (the branches
where canRestore(...) indicates the restore is unavailable or where
currentState(...) indicates lack of permission/availability), return a
MutationResult with reason: "permission_denied" and the appropriate files
payload instead of mapping them to "conflict" or
"unsupported_size"/"unavailable"; update the logic that builds the blocked array
and the final return so any item whose failure originated from a permission
check yields reason "permission_denied" (and keep existing mappings only for
true content conflicts/write errors).
🪄 Autofix (Beta)
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: Path: .coderabbit.yaml
Review profile: CHILL
Plan: Pro Plus
Run ID: 7fc1675c-c076-4d3a-b9b5-71f1879afc42
📒 Files selected for processing (1)
packages/opencode/src/session/turn-change.ts
There was a problem hiding this comment.
Actionable comments posted: 1
🧹 Nitpick comments (1)
packages/opencode/test/session/turn-change.test.ts (1)
241-266: ⚡ Quick winAdd a mutate-path persistence failure regression test.
You already isolate
finalizeDB failures; please add the analogous case forTurnChange.undo/redowhere files were applied but display-state update fails, and assert rollback +write_failed.🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@packages/opencode/test/session/turn-change.test.ts` around lines 241 - 266, Add a new test similar to the "finalize failure is isolated from caller cleanup" case that simulates a persistence/display-state update failure during TurnChange.undo and TurnChange.redo: create a turn via createTurn(), record a write with TurnChange.recordWrite(...) that applies a file change to the temp fixture, then mock the persistence/display update used by undo/redo (e.g., spyOn Database.transaction or the specific method that updates display state) to throw an error; call TurnChange.undo(turn) and TurnChange.redo(turn) and assert they do not throw, that the filesystem changes are rolled back, and that the TurnChange entry reflects a write_failed state (use TurnChange.get(turn) to verify); restore the mock afterward.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.
Inline comments:
In `@packages/opencode/src/session/turn-change.ts`:
- Around line 532-538: The DB update to TurnChangeDisplayTable after applying
file mutations can fail and leave files changed while display state remains
stale; modify the flow in the code around Database.use/Database.use(...) and the
code that applies file mutations so the DB persistence and file changes are
atomic: either (a) perform the TurnChangeDisplayTable update inside the same
transactional boundary as file metadata/state persistence (use a DB transaction)
before committing file mutations, or (b) if you must apply files first, catch
errors from the Database.use(...) call and immediately revert the applied file
changes (invoke the existing undo routine for the change or run a compensating
operation) then rethrow or surface the error. Ensure you reference the
persistedDisplay/nextState/time variables and update logic so the rollback path
uses the same identifiers to restore previous file state and keep undo/redo
consistent.
---
Nitpick comments:
In `@packages/opencode/test/session/turn-change.test.ts`:
- Around line 241-266: Add a new test similar to the "finalize failure is
isolated from caller cleanup" case that simulates a persistence/display-state
update failure during TurnChange.undo and TurnChange.redo: create a turn via
createTurn(), record a write with TurnChange.recordWrite(...) that applies a
file change to the temp fixture, then mock the persistence/display update used
by undo/redo (e.g., spyOn Database.transaction or the specific method that
updates display state) to throw an error; call TurnChange.undo(turn) and
TurnChange.redo(turn) and assert they do not throw, that the filesystem changes
are rolled back, and that the TurnChange entry reflects a write_failed state
(use TurnChange.get(turn) to verify); restore the mock afterward.
🪄 Autofix (Beta)
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: Path: .coderabbit.yaml
Review profile: CHILL
Plan: Pro Plus
Run ID: 70f2f113-016e-4dea-a3fd-cc67b55b2e7f
📒 Files selected for processing (2)
packages/opencode/src/session/turn-change.tspackages/opencode/test/session/turn-change.test.ts
There was a problem hiding this comment.
🧹 Nitpick comments (1)
packages/ui/src/components/session-turn.tsx (1)
371-376: 💤 Low valueConsider edge case for root-level files.
parentPathreturns the originalvaluewhen there's no separator or when the separator is at index 0. For a root-level file like"file.txt"(no slash),indexis-1, so it returns the filename itself rather than"."or an empty string.This is likely fine since
showInFolderwill receive the file path (and platform APIs typically handle this), but worth verifying the platform behavior for root-level deleted files.,
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@packages/ui/src/components/session-turn.tsx` around lines 371 - 376, parentPath currently returns the original value when there is no separator or when the separator is at index 0, which yields incorrect results for root-level paths; update parentPath so that if index < 0 (no separator) it returns "." (current directory) and if index === 0 (path like "/file.txt") it returns "/" (root), otherwise return value.slice(0, index); change this logic inside the parentPath function referenced in session-turn.tsx so showInFolder receives a canonical parent path.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.
Nitpick comments:
In `@packages/ui/src/components/session-turn.tsx`:
- Around line 371-376: parentPath currently returns the original value when
there is no separator or when the separator is at index 0, which yields
incorrect results for root-level paths; update parentPath so that if index < 0
(no separator) it returns "." (current directory) and if index === 0 (path like
"/file.txt") it returns "/" (root), otherwise return value.slice(0, index);
change this logic inside the parentPath function referenced in session-turn.tsx
so showInFolder receives a canonical parent path.
ℹ️ Review info
⚙️ Run configuration
Configuration used: Path: .coderabbit.yaml
Review profile: CHILL
Plan: Pro Plus
Run ID: f25cf12c-a634-48f0-b59d-6ba9825c2864
📒 Files selected for processing (7)
packages/app/src/i18n/en.tspackages/app/src/i18n/zh.tspackages/app/src/pages/session/files-tab.tsxpackages/app/src/pages/session/message-timeline.tsxpackages/ui/src/components/session-turn.tsxpackages/ui/src/i18n/en.tspackages/ui/src/i18n/zh.ts
✅ Files skipped from review due to trivial changes (3)
- packages/ui/src/i18n/en.ts
- packages/app/src/i18n/en.ts
- packages/app/src/pages/session/message-timeline.tsx
There was a problem hiding this comment.
Actionable comments posted: 1
Caution
Some comments are outside the diff and can’t be posted inline due to platform limitations.
⚠️ Outside diff range comments (1)
packages/opencode/src/tool/write.ts (1)
76-92:⚠️ Potential issue | 🟠 Major | 🏗️ Heavy liftMake the file write and turn-change recording atomic.
The file is already written, optionally reformatted, and
File.Event.Editedis already published beforeturnChange.recordWrite()runs. If that tracking step fails, the outerEffect.orDiestill turns the tool into a hard failure even though the filesystem was mutated, and the new undo/redo flow has no restore record for that write.Please either persist the turn-change record before emitting success side effects, or roll the file back/delete the newly created file when tracking fails.
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@packages/opencode/src/tool/write.ts` around lines 76 - 92, The write flow currently writes the file and publishes File.Event.Edited before calling turnChange.recordWrite, risking filesystem mutation without a persisted undo record; change the sequence so you persist the turn-change record first (call turnChange.recordWrite(...) using finalContent and desiredBom, with the same before/after payload) and only after that succeeds call bus.publish(File.Event.Edited, ...); if you cannot move recordWrite earlier due to relying on post-format content from Bom.syncFile/format.file, then wrap the recordWrite call in a try/catch and on failure rollback the filesystem to the original state (use fs.writeWithDirs(filepath, Bom.join(contentOld, source.bom)) or delete the new file when exists was false) before rethrowing the error so the file system and recorded state remain atomic; reference functions/variables: fs.writeWithDirs, format.file, Bom.syncFile, bus.publish, turnChange.recordWrite, contentOld, finalContent, desiredBom, exists, source.bom, ctx.sessionID, ctx.messageID.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.
Inline comments:
In `@packages/opencode/src/tool/write.ts`:
- Around line 61-68: The code calls isSensitivePath(filepath) with an absolute
path which misclassifies sensitivity; compute the project-relative path (the
same value passed to patterns, e.g. path.relative(Instance.worktree, filepath))
and pass that relative path into isSensitivePath instead of the absolute
filepath so sensitivity is determined from the repo-relative segments (update
the variable used in the call near where permission/patterns/metadata are built
around isSensitivePath, safeFilepathMetadata, and patterns).
---
Outside diff comments:
In `@packages/opencode/src/tool/write.ts`:
- Around line 76-92: The write flow currently writes the file and publishes
File.Event.Edited before calling turnChange.recordWrite, risking filesystem
mutation without a persisted undo record; change the sequence so you persist the
turn-change record first (call turnChange.recordWrite(...) using finalContent
and desiredBom, with the same before/after payload) and only after that succeeds
call bus.publish(File.Event.Edited, ...); if you cannot move recordWrite earlier
due to relying on post-format content from Bom.syncFile/format.file, then wrap
the recordWrite call in a try/catch and on failure rollback the filesystem to
the original state (use fs.writeWithDirs(filepath, Bom.join(contentOld,
source.bom)) or delete the new file when exists was false) before rethrowing the
error so the file system and recorded state remain atomic; reference
functions/variables: fs.writeWithDirs, format.file, Bom.syncFile, bus.publish,
turnChange.recordWrite, contentOld, finalContent, desiredBom, exists,
source.bom, ctx.sessionID, ctx.messageID.
🪄 Autofix (Beta)
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: Path: .coderabbit.yaml
Review profile: CHILL
Plan: Pro Plus
Run ID: 8d63c1ba-ee75-40dc-b33d-f286a2bfca98
📒 Files selected for processing (23)
packages/app/src/i18n/en.tspackages/app/src/i18n/zh.tspackages/app/src/pages/session/message-timeline.tsxpackages/opencode/src/effect/app-runtime.tspackages/opencode/src/server/instance/session.tspackages/opencode/src/session/processor.tspackages/opencode/src/session/turn-change.tspackages/opencode/src/tool/apply_patch.tspackages/opencode/src/tool/edit.tspackages/opencode/src/tool/registry.tspackages/opencode/src/tool/write.tspackages/opencode/test/session/compaction.test.tspackages/opencode/test/session/processor-effect.test.tspackages/opencode/test/session/prompt-effect.test.tspackages/opencode/test/session/snapshot-tool-race.test.tspackages/opencode/test/session/turn-change.test.tspackages/opencode/test/tool/apply_patch.test.tspackages/opencode/test/tool/edit.test.tspackages/opencode/test/tool/write.test.tspackages/ui/src/components/session-turn.csspackages/ui/src/components/session-turn.tsxpackages/ui/src/i18n/en.tspackages/ui/src/i18n/zh.ts
✅ Files skipped from review due to trivial changes (5)
- packages/opencode/src/tool/registry.ts
- packages/ui/src/i18n/zh.ts
- packages/opencode/src/tool/edit.ts
- packages/app/src/i18n/en.ts
- packages/app/src/i18n/zh.ts
🚧 Files skipped from review as they are similar to previous changes (11)
- packages/opencode/src/session/processor.ts
- packages/ui/src/i18n/en.ts
- packages/opencode/test/tool/write.test.ts
- packages/opencode/src/server/instance/session.ts
- packages/opencode/test/tool/edit.test.ts
- packages/ui/src/components/session-turn.css
- packages/opencode/test/session/turn-change.test.ts
- packages/app/src/pages/session/message-timeline.tsx
- packages/opencode/src/session/turn-change.ts
- packages/ui/src/components/session-turn.tsx
- packages/opencode/src/tool/apply_patch.ts
There was a problem hiding this comment.
Actionable comments posted: 1
🧹 Nitpick comments (1)
packages/opencode/test/session/turn-change.test.ts (1)
432-475: ⚡ Quick winAssert the redo call is actually rejected after invalidation.
This test only checks the display flag. It would be stronger to also call
TurnChange.redo(first)and assert that the old turn can no longer be reapplied, so the behavioral contract stays covered too.🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@packages/opencode/test/session/turn-change.test.ts` around lines 432 - 475, After checking the redoAvailable flag is false, actually attempt to redo the original turn and assert it fails: call await TurnChange.redo(first) and expect the returned status to indicate failure (e.g., "rejected" or the appropriate failure status used by TurnChange) so the test covers behavioral invalidation as well as the display flag; keep the existing checks on TurnChange.get(first)?.redoAvailable and use the same first messageID/sessionID fixture.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.
Inline comments:
In `@packages/opencode/src/tool/apply_patch.ts`:
- Line 104: The current catch around Bom.readFile(...) at the call site in
apply_patch (and the similar block at lines 171-173) turns every read failure
into undefined which can cause recordWrite(...) to mark non-existent state and
lead to deleting real files; change the error handling so only a “file not
found” / not-found specific error is converted to undefined and all other errors
are rethrown (i.e., let the Effect fail) so the patch aborts on genuine read
failures; update the Bom.readFile(...) pipe/Effect.catch logic to inspect the
thrown error (or its tag) and return undefined only for the not-found case,
otherwise re-raise the error.
---
Nitpick comments:
In `@packages/opencode/test/session/turn-change.test.ts`:
- Around line 432-475: After checking the redoAvailable flag is false, actually
attempt to redo the original turn and assert it fails: call await
TurnChange.redo(first) and expect the returned status to indicate failure (e.g.,
"rejected" or the appropriate failure status used by TurnChange) so the test
covers behavioral invalidation as well as the display flag; keep the existing
checks on TurnChange.get(first)?.redoAvailable and use the same first
messageID/sessionID fixture.
🪄 Autofix (Beta)
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: Path: .coderabbit.yaml
Review profile: CHILL
Plan: Pro Plus
Run ID: 531a8611-e0aa-4282-9c2b-ec5c7a79a8ca
📒 Files selected for processing (7)
packages/opencode/src/session/turn-change.tspackages/opencode/src/tool/apply_patch.tspackages/opencode/src/tool/edit.tspackages/opencode/src/tool/sensitive.tspackages/opencode/src/tool/write.tspackages/opencode/test/session/turn-change.test.tspackages/opencode/test/tool/sensitive.test.ts
✅ Files skipped from review due to trivial changes (1)
- packages/opencode/src/tool/write.ts
🚧 Files skipped from review as they are similar to previous changes (3)
- packages/opencode/src/session/turn-change.ts
- packages/opencode/src/tool/edit.ts
- packages/opencode/src/tool/sensitive.ts
There was a problem hiding this comment.
Actionable comments posted: 1
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.
Inline comments:
In `@packages/opencode/src/tool/sensitive.ts`:
- Around line 161-164: The redaction for tool === "apply_patch" currently
hardcodes every file's status to "modified", losing add/delete info; update the
logic so sensitivePatchPaths returns (or provide a new helper that returns)
objects with both file and status (e.g., {file, status}), then map those results
to ({ file, status, sensitive: true }) instead of setting status: "modified";
reference sensitivePatchPaths, the apply_patch branch, and value.patchText to
locate and change the mapping.
🪄 Autofix (Beta)
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: Path: .coderabbit.yaml
Review profile: CHILL
Plan: Pro Plus
Run ID: 972da3e2-f532-4ecf-909a-48ef38592844
📒 Files selected for processing (7)
packages/opencode/src/session/turn-change.tspackages/opencode/src/tool/apply_patch.tspackages/opencode/src/tool/edit.tspackages/opencode/src/tool/sensitive.tspackages/opencode/src/tool/write.tspackages/opencode/test/session/turn-change.test.tspackages/opencode/test/tool/sensitive.test.ts
✅ Files skipped from review due to trivial changes (1)
- packages/opencode/src/session/turn-change.ts
🚧 Files skipped from review as they are similar to previous changes (2)
- packages/opencode/test/tool/sensitive.test.ts
- packages/opencode/test/session/turn-change.test.ts
There was a problem hiding this comment.
Actionable comments posted: 2
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.
Inline comments:
In `@packages/ui/src/components/session-turn.tsx`:
- Around line 349-353: The custom parentPath function strips root separators and
can produce incorrect targets; replace the splitter logic by calling the shared
getDirectory helper (already imported) wherever parentPath or the similar
slice-based splitter is used (both the parentPath function and the second
occurrence around lines 593-597), i.e., remove the replaceAll/lastIndexOf/slice
logic and return getDirectory(value) instead so root paths (e.g., /foo.txt or
C:\foo.txt) are preserved.
- Around line 521-531: The button for undo/redo is rendered whenever
turnActionLabel() is truthy even if the actual handler (turnChangeActions.undo
or turnChangeActions.redo) is undefined, producing a clickable no-op; update the
render guard so the button is only shown when both turnActionLabel() is set and
the corresponding handler exists (check turnChangeActions.undo or
turnChangeActions.redo based on action) or require the handler props when turn
changes are enabled, and ensure mutateTurnChange is only called when the handler
is present (also keep resetConfirm behavior); reference turnActionLabel,
mutateTurnChange, turnChangeActions.undo, turnChangeActions.redo, Show, and
resetConfirm to locate where to add the conditional.
🪄 Autofix (Beta)
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: Path: .coderabbit.yaml
Review profile: CHILL
Plan: Pro Plus
Run ID: bc897144-d467-4ae7-b8cb-c069795e739c
📒 Files selected for processing (3)
packages/ui/src/components/session-turn-changes.tspackages/ui/src/components/session-turn-turn-changes.test.tspackages/ui/src/components/session-turn.tsx
✅ Files skipped from review due to trivial changes (1)
- packages/ui/src/components/session-turn-turn-changes.test.ts
There was a problem hiding this comment.
🧹 Nitpick comments (1)
packages/ui/src/components/session-turn.tsx (1)
328-338: 💤 Low valueConsider wrapping the async action in try-catch.
mutateTurnChangeawaits the undo/redo handler but doesn't catch errors. IfturnChangeFetchthrows on a network failure (e.g.,!res.ok), this would result in an unhandled promise rejection. The blocked-operation toast is handled insideturnChangeFetch, but actual exceptions would bubble up silently.🛡️ Optional defensive fix
const mutateTurnChange = async () => { const current = turnChange() const id = current?.messageID if (!id) return const action = turnChangeAction(current) if (!action || !hasTurnChangeActionHandler(current, props.turnChangeActions)) return if (!primeConfirm(action)) return resetConfirm() - if (action === "undo") await props.turnChangeActions?.undo?.(id) - else await props.turnChangeActions?.redo?.(id) + try { + if (action === "undo") await props.turnChangeActions?.undo?.(id) + else await props.turnChangeActions?.redo?.(id) + } catch { + // Network errors are logged globally; UI remains consistent + } }🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@packages/ui/src/components/session-turn.tsx` around lines 328 - 338, mutateTurnChange currently awaits the undo/redo handlers (props.turnChangeActions?.undo/redo) but does not catch exceptions, so any thrown errors (e.g., from turnChangeFetch) will bubble as unhandled rejections; wrap the core async operation in a try-catch around the awaited call (after confirming action and calling resetConfirm) and in the catch log or surface the error (e.g., via process logger/toast) and ensure confirm state is reset appropriately; reference mutateTurnChange, turnChangeAction, hasTurnChangeActionHandler, primeConfirm, resetConfirm, and props.turnChangeActions?.undo/redo when making the change.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.
Nitpick comments:
In `@packages/ui/src/components/session-turn.tsx`:
- Around line 328-338: mutateTurnChange currently awaits the undo/redo handlers
(props.turnChangeActions?.undo/redo) but does not catch exceptions, so any
thrown errors (e.g., from turnChangeFetch) will bubble as unhandled rejections;
wrap the core async operation in a try-catch around the awaited call (after
confirming action and calling resetConfirm) and in the catch log or surface the
error (e.g., via process logger/toast) and ensure confirm state is reset
appropriately; reference mutateTurnChange, turnChangeAction,
hasTurnChangeActionHandler, primeConfirm, resetConfirm, and
props.turnChangeActions?.undo/redo when making the change.
ℹ️ Review info
⚙️ Run configuration
Configuration used: Path: .coderabbit.yaml
Review profile: CHILL
Plan: Pro Plus
Run ID: fd534774-61e8-4d0b-b86a-60fca35b046e
📒 Files selected for processing (3)
packages/ui/src/components/session-turn-changes.tspackages/ui/src/components/session-turn-turn-changes.test.tspackages/ui/src/components/session-turn.tsx
✅ Files skipped from review due to trivial changes (1)
- packages/ui/src/components/session-turn-turn-changes.test.ts
Summary
Why
Fixes #339.
The previous diff block could show the current branch snapshot instead of the files changed by the current assistant turn. That made the UI feel unrelated to the conversation. This PR changes the product model to show the latest assistant turn's tracked file-writing results, regardless of Git state, while keeping the old snapshot diff as a fallback for sessions without turn-change records.
Related Issue
Fixes #339
Human Review Status
Completed. This PR was squash-merged into
devafter final review, green checks, and zero unresolved review threads.Review Focus
Risk Notes
turn_change_displayandturn_change_restore.turn-change.updatedevent.Follow-up Candidates
These are intentionally not blockers for this PR and do not need a new umbrella issue right now:
turn-change.updatedevent if assistant-completion fetch plus delayed retry proves flaky in real use.config.jsoncan be downgraded to status-only when they contain secrets.How To Verify
Screenshots or Recordings
Visible UI changed.
I attempted Browser Use against the local Vite app, but the Browser Use runtime timed out twice in this session. I then used local Playwright as a fallback to load
http://127.0.0.1:49720/and capture/tmp/pawwork-i339-app.png. The page loaded with titlePawWork; console errors were expected sidecar connection failures because only the frontend Vite server was running, not the backend sidecar. The actual turn-change block was covered by component build/typecheck rather than a live sidecar-backed session screenshot.Checklist
dev, and my PR title and commit messages use Conventional Commits in EnglishSummary by CodeRabbit
New Features
Bug Fixes / UX
Security & Privacy
Chores
Tests