Skip to content

fix(chat): restore artifact open actions ("Open in editor") broken by provider scoping - #267

Open
badideasforsale wants to merge 10 commits into
mainfrom
fix/artifact-policy-provider-scope
Open

fix(chat): restore artifact open actions ("Open in editor") broken by provider scoping#267
badideasforsale wants to merge 10 commits into
mainfrom
fix/artifact-policy-provider-scope

Conversation

@badideasforsale

@badideasforsale badideasforsale commented Sep 1, 2026

Copy link
Copy Markdown
Contributor

Summary

"Open in editor" in the artifact viewer's ⋯ menu did nothing — a silent no-op. This PR fixes the root cause and four adjacent defects in the same failure family, found during pre-review and dogfooding of the fix.

Root cause: #178 moved ArtifactPolicyProvider from ChatView into the extracted ChatTranscriptSurface. The transcript kept its provider, but sibling consumers in the chat row — ArtifactViewerPanel ("Open in editor"), the right rail's ArtifactsWidget (row opens), and ArtifactAutoOpenMount (artifact auto-open) — fell outside it and received the inert default context, whose openResolvedPath is an async no-op and whose artifact list is empty. Call sites swallow errors (.catch(() => {})), so nothing surfaced anywhere.

Seven commits, each independently revertable:

  1. fix(chat): outer ArtifactPolicyProvider around ChatView's chat row. Restores context for the viewer panel, right-rail artifacts widget, and auto-open. The transcript's inner provider stays (Home canvas cards rely on it); nesting is benign — actions are memoized and artifact collection is signature-cached. The outer provider intentionally receives ungated controller.messages: the auto-open hook baselines whatever is present while history loads and on its first settled pass, so history artifacts cannot pop the viewer — and baselining the real list is safer than the paint-gated (transiently empty) one.
  2. fix(tauri): session-window opener capability. Popped-out session:* windows only allowed opener:allow-open-path for $DOWNLOAD, so even with the provider fixed, "Open in editor" was rejected by capability scope in popped-out windows — same silent symptom. The scope now mirrors the main window's.
  3. fix(home): canvas card composer provider. Same add interactive chats to the Home canvas #178 family: CanvasCardComposer is a sibling of ChatTranscriptSurface, so its @-file mentions silently read an empty session-artifact list. It now gets its own provider fed the full session messages.
  4. fix(chat): failed opens no longer consume the retry debounce. openResolvedPath recorded its 1200 ms dedupe timestamp before awaiting the opener, so a rejected hand-off blocked an immediate retry.
  5. fix(chat): no "Open in editor" for deleted files. The viewer already knows when the file is gone (the "File deleted from disk." strip), yet the error body still offered an "Open in editor" button and the ⋯ menu kept the action enabled — guaranteed dead clicks on a nonexistent file. The error-body button now hides and the menu item disables when the divergence kind is missing (the same signal that already hides the strip's Reload button).
  6. fix(chat) (review follow-up): the outer provider is keyed by the effective session. The outer provider received the raw requested sessionId while its messages/cwd came from the controller's effective session; during session replacement those can briefly disagree, letting artifact policy or viewer-store writes act under a stale identity. The provider, ArtifactAutoOpenMount, and ArtifactViewerPanel now all use timelineSessionId, with a discriminating test where the requested and effective ids differ.
  7. fix(chat) (review follow-up): Reveal is also disabled for deleted files. Commit 5 kept Reveal enabled claiming file managers could show the containing folder — but the implementation passes the deleted file's path (not the parent) to revealItemInDir and swallows the rejection: another silent dead end. Both hand-offs now disable together and re-enable when polling sees the file reappear (new reappearance test; the deleted-state test verifies clicks reach neither OS boundary).

Screenshots (deleted-file viewer states)

Captured via the Playwright harness (tauri-mock driven to the missing stat state), viewer panel shown at 2x:

Before (PR head 90cfe94, as reviewed) After (current head)
⋯ menu on a deleted file: "Open in editor" disabled but Reveal still enabled (dead click) Both OS hand-offs disabled; re-enabled automatically if the file reappears

(images attached in PR comments below)

Known follow-ups deliberately not in this PR (happy to file issues):

  • Surface open/reveal failures to the user (toast) instead of .catch(() => {}) — that swallowing is what made both the add interactive chats to the Home canvas #178 regression and the capability gap invisible.
  • Make the default actions context warn or throw in dev; the silent no-op default is what let this regress unnoticed.
  • AutomationBuilderView renders MessageTimeline/ChatInput with no artifact provider (artifact links/opens inert there); pre-existing, out of scope here.

Related issue

None found for this exact bug. Closest: #171 (Files panel opens Markdown externally instead of the viewer) — adjacent surface, different defect; not addressed here.

Testing

  • New regression test: the ArtifactPolicyProvider test mock now stamps its props on a structural marker; a ChatView test asserts the viewer panel mounts inside a provider that received real session data. Fails against the unfixed ChatView (verified both ways via stash).
  • New integration-style tests (ArtifactViewer.openActions.test.tsx) render the real provider with only the Tauri boundary mocked: viewer "Open in editor" and right-rail artifact row both reach openPath; a failed open doesn't consume the retry debounce (also verified to fail pre-fix). The provider-scope bug class is invisible to tests that mock the context, so these keep the real wiring covered.
  • just check clean; just tauri-check clean (capability schema regenerated); full chat + home suites: 3,116 tests pass.
  • Manual verification of "Open in editor" pending a dev build; the wiring is covered by the integration tests above.

Two independent adversarial review passes were run on this branch before opening; findings from both (session-window capability gap, canvas composer orphan, debounce lockout, test-mock drift risks, commit-message inaccuracies) are incorporated.

@badideasforsale
badideasforsale requested a review from a team September 1, 2026 06:05
**Category:** bug-fix
**User Impact:** "Open in editor" in the artifact viewer's ⋯ menu and the
right rail's artifact rows work again in full chat, and artifact auto-open
is restored.

**Problem:** PR #178 moved ArtifactPolicyProvider from ChatView into the
extracted ChatTranscriptSurface. The transcript kept its provider, but
sibling consumers in the chat row — ArtifactViewerPanel (the file viewer
that mounts beside the conversation), the right rail's ArtifactsWidget,
and ArtifactAutoOpenMount — were left outside it. They received the inert
default context, whose openResolvedPath is an async no-op and whose
artifact list is empty, so viewer/right-rail opens did nothing (silently —
call sites swallow errors) and auto-open never fired. The main composer's
artifact @-mentions survived only because the composer renders inside the
transcript via the footer prop.

**Solution:** Add an outer ArtifactPolicyProvider around ChatView's chat
row. ChatTranscriptSurface keeps its own inner provider (Home canvas cards
rely on it), so the transcript nests one provider inside the other. Unlike
the pre-#178 provider, the outer one receives the ungated
controller.messages rather than the paint-gated timeline list; that is
safe for auto-open (the hook baselines whatever is present while history
loads and on its first settled pass) and means the baseline is computed
from the real message list instead of a transiently empty one.

The ChatView test previously mocked the provider as a pass-through, which
is why this was invisible to the suite. The mock now stamps a structural
marker with the provider's props, and a regression test asserts the viewer
panel mounts inside a provider that received real session data; it fails
against the unfixed ChatView.
**Category:** bug-fix
**User Impact:** "Open in editor" works in popped-out chat windows, not
just the main window.

**Problem:** The session-window capability granted opener:allow-open-path
only for $DOWNLOAD, while the main window allows the artifact roots users
actually work in ($HOME, $TEMP, mounted volumes, workspace paths). A
popped-out session window renders the same ChatView and artifact viewer,
so even with the provider fix its "Open in editor" hand-off reached Tauri
and was rejected by capability scope — the same silent click-does-nothing
symptom, since call sites swallow the rejection.

**Solution:** Mirror the main window's opener:allow-open-path scope in the
session-window capability. Generated capability schema regenerated via
cargo check.
**Category:** bug-fix
**User Impact:** @-file mentions in a Home canvas chat card's composer can
suggest the session's artifacts again.

**Problem:** Same regression family as the chat-row fix: in ChatCanvasCard
the composer is a sibling of ChatTranscriptSurface, so it sits outside the
transcript's ArtifactPolicyProvider. Its mention handlers read the session
artifact list from context and silently saw an empty list. (In full chat
the composer is passed as the transcript's footer, so it renders inside
the provider — the canvas card was the only orphaned composer.)

**Solution:** Wrap CanvasCardComposer in an ArtifactPolicyProvider fed the
full (unbounded) session messages, so mentions cover the whole session
rather than the card's bounded projection.
**Category:** bug-fix
**User Impact:** If handing a file to the OS fails, clicking again
immediately retries instead of being silently absorbed.

**Problem:** openResolvedPath recorded its per-path debounce timestamp
before awaiting the Tauri opener. When the open was rejected (capability
scope, missing handler), the failure still occupied the 1200ms dedupe
window, so an immediate retry returned early and did nothing.

**Solution:** Clear the debounce entry when openPath rejects, then
rethrow.

Also adds integration-style tests that render the REAL
ArtifactPolicyProvider (only the Tauri boundary mocked) and assert that
"Open in editor" in the viewer and a right-rail artifact row open both
reach the opener — the provider-scope regression class is invisible to
tests that mock the context, so these keep the real wiring covered. The
debounce test fails against the pre-fix openResolvedPath.
@badideasforsale
badideasforsale force-pushed the fix/artifact-policy-provider-scope branch from b53957e to 1244c8b Compare September 2, 2026 00:37
@badideasforsale

Copy link
Copy Markdown
Contributor Author

🤖 Rebased onto main (8e7e350) after #252 (remote SSH sessions) landed — it touches the same two files (ChatView.tsx, ArtifactPolicyContext.tsx). Clean rebase, no conflicts; the remote-session guard in openResolvedPath composes correctly with the debounce fix (guard throws before the debounce bookkeeping). just check clean and full chat+home suites (3,205 tests) pass on the rebased head.

**Category:** bug-fix
**User Impact:** The artifact viewer no longer offers "Open in editor"
for a file it knows is deleted from disk.

**Problem:** When the viewed file is gone, the viewer shows the "File
deleted from disk." strip — but the error body still rendered an "Open
in editor" button, and the header's ⋯ menu still offered the same
action. Nothing can open a file that no longer exists, so both were
guaranteed dead clicks (made worse by call sites swallowing the
failure).

**Solution:** When the divergence state is missing (the same signal that
already hides the strip's pointless Reload button), hide the error
body's "Open in editor" button and disable the ⋯ menu item. "Reveal in
Finder" stays enabled — the containing folder still exists. Polling
keeps watching the path, so if the file reappears the view heals and
the actions return.

Tests: error body offers no editor button for a missing file but keeps
it for unreadable-but-present files; the menu item is disabled and its
click never reaches the OS hand-off. Both fail against the unfixed
viewer.

@morgmart morgmart left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🤖 Automated code review

REQUEST_CHANGES: the new outer artifact policy can apply filesystem behavior using the wrong session identity during reconciliation, and the deleted-file menu deliberately retains a Reveal action whose deleted target may fail silently. This PR changes graphical artifact-viewer states but the supplied PR evidence contains no screenshots or recording; please add screenshots or a short screen recording. The supplied GitHub evidence is structurally valid: all captured check runs passed, while combined status remained pending, so required checks still govern merge readiness.

Deterministic publication result: 2 blocking and 0 non-blocking inline finding(s) publishable; 0 duplicate(s) suppressed; 1 blocking screenshot-evidence requirement(s) in this review body.

🤖 Blocking · Screenshots needed

This PR changes Berd’s graphical interface. Please add screenshots or a short screen recording so the visual result can be reviewed. Screenshots are review evidence; they do not replace accessibility, responsive, theme, localization, or behavior validation.

Comment thread src/features/chat/ui/ChatView.tsx Outdated
<ArtifactPolicyProvider
messages={controller.messages}
sessionCwd={controller.sessionArtifactCwd}
sessionId={sessionId}

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🤖 P1 · Provider uses stale session identity (blocking)

ChatView derives timelineSessionId from effectiveSession?.id ?? sessionId and gives that identity to the rendered transcript, but the newly added outer ArtifactPolicyProvider receives the raw requested sessionId while receiving controller messages and cwd. During session replacement or reconciliation, its identity can disagree with the session snapshot it governs. This provider owns artifact behavior for the viewer, rail, and auto-open subtree, including remote/local policy and viewer-store identity.

User effect: While a chat is being restored or reconciled, a remote artifact can incorrectly expose a local file action, a valid local action can be blocked, or an artifact can open under the obsolete chat session.

Recommended fix: Pass timelineSessionId, or the same effective-session identity used by the timeline, to the outer ArtifactPolicyProvider so its ID, messages, cwd, and remote/local policy describe one session snapshot.

Test: Add a discriminating ChatView test where the requested sessionId differs from controller.session.id and assert that the enclosing artifact policy provider receives the effective controller session ID; the current matching-ID test does not catch this case.

Comment thread src/features/chat/ui/ArtifactViewer.tsx Outdated
<DropdownMenuContent align="end">
{/* A deleted file cannot be handed to an editor, so the
action is disabled rather than left as a dead click.
Reveal stays enabled: file managers can still show the

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🤖 P1 · Deleted reveal can fail silently (blocking)

The changed code explicitly keeps Reveal enabled after the artifact is known to be missing, asserting that file managers can show the containing folder. The action still passes the deleted artifact path to revealItemInDir rather than targeting an existing parent directory, and its rejection is swallowed. The implementation therefore does not establish the recovery behavior promised by the new state decision.

User effect: After Berd reports that a file was deleted, the remaining enabled Reveal action can appear to do nothing, leaving another silent dead end beside the action this PR removes.

Recommended fix: For a missing artifact, either disable or hide Reveal, or implement an explicit containing-folder action that targets an existing parent directory and uses an accurate label.

Test: Add a discriminating missing-file test that selects the remaining action and verifies either that it is unavailable or that the file-manager boundary receives the existing parent-directory path. Do not only assert the absence of aria-disabled.

**Category:** bug-fix (review follow-up)
**User Impact:** During session replacement or reconciliation, artifact
actions and the viewer panel can no longer act under a stale session
identity.

**Problem:** The outer ArtifactPolicyProvider received the raw requested
sessionId while its messages and cwd came from the controller's effective
session (timelineSessionId = effectiveSession?.id ?? sessionId). When
those briefly disagree, the provider's remote/local policy and
viewer-store writes describe a different session than the snapshot it
governs, and ArtifactViewerPanel (keyed by the raw id) reads a different
store entry than openInApp writes.

**Solution:** Pass timelineSessionId to the outer provider,
ArtifactAutoOpenMount, and ArtifactViewerPanel so identity, messages, and
cwd describe one session snapshot. Adds the discriminating test the
reviewer asked for: requested id differs from controller.session.id and
the provider (and panel) must carry the effective id.
**Category:** bug-fix (review follow-up)
**User Impact:** No dead 'Reveal in Finder' click on a deleted file; both
OS hand-offs come back automatically if the file reappears.

**Problem:** The previous commit disabled 'Open in editor' for deleted
files but deliberately kept Reveal enabled, claiming file managers could
show the containing folder. That claim didn't match the implementation:
the action passes the deleted file's path (not the parent directory) to
revealItemInDir and swallows the rejection — the same silent dead end the
commit claimed to remove.

**Solution:** Disable both menu items on the shared fileIsMissing state
(same signal that hides the strip's Reload button). Polling keeps
watching the path, so recovery re-enables them — covered by a new
reappearance test. The deleted-state test now verifies clicks on both
disabled items never reach either OS boundary, not just aria-disabled.
@badideasforsale

Copy link
Copy Markdown
Contributor Author

🤖 Both blocking findings addressed, and screenshot evidence added below.

P1 · Provider uses stale session identity → fixed in dcf8b24. The outer ArtifactPolicyProvider, ArtifactAutoOpenMount, and ArtifactViewerPanel now all key off timelineSessionId (the controller's effective session), so identity, messages, cwd, and remote/local policy describe one session snapshot — and the viewer panel reads the same viewer-store entry that openInApp writes. Added the requested discriminating test: the requested sessionId differs from controller.session.id, and both the enclosing provider and the panel must carry the effective id (fails against the previous head).

P1 · Deleted reveal can fail silently → fixed in 3dd1684, taking the disable option. The review is right that the code contradicted the comment — Reveal passes the deleted file's path (not the parent directory) to revealItemInDir with the rejection swallowed. Both OS hand-offs now disable together on the shared fileIsMissing state, and polling re-enables them if the file reappears (new reappearance test). The deleted-state test now verifies clicks on both disabled items reach neither OS boundary (openResolvedPath / revealInFileManager), not just aria-disabled.

Screenshots (deleted-file viewer, captured via the Playwright harness with the tauri-mock driven to a missing stat verdict):

Before — PR head 90cfe94 as reviewed: "Open in editor" disabled but Reveal still enabled (silent dead click):

before: reveal still enabled on a deleted file

After — current head: both OS hand-offs disabled:

after: both hand-offs disabled on a deleted file

After — error body for a deleted file: "File deleted from disk." strip with no dead "Open in editor" button:

after: error body without the dead button

All chat + home suites pass (3,210 tests); just check clean.

@morgmart morgmart left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🤖 Automated code review

REQUEST_CHANGES: the follow-up fixes both prior blocking issues, but the artifact viewer's layout still reads open state under the requested session while the panel now renders under the effective session, and multiple policy providers give one visible session separate artifact/open lifecycles. Supplied visual evidence is present. All captured GitHub check runs passed; combined status remained pending, so required checks still govern merge readiness.

Deterministic publication result: 1 blocking and 1 non-blocking inline finding(s) publishable; 0 duplicate(s) suppressed; 0 blocking screenshot-evidence requirement(s) in this review body.

<ArtifactViewerPanel sessionId={sessionId} />
// Keyed by the same effective identity the providers use, so the
// panel reads the viewer-store entry that openInApp writes.
<ArtifactViewerPanel sessionId={timelineSessionId} />

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🤖 P1 · Viewer layout reads stale session (blocking)

ArtifactViewerPanel now correctly reads timelineSessionId, but isArtifactViewerOpen is still derived near the top of ChatView with useOpenArtifact(sessionId). During the reconciliation state this follow-up explicitly supports, the panel and the layout calculation therefore read different artifact-store entries. Width allocation and compact-mode decisions can disagree with the viewer actually rendered.

User effect: While a chat is being restored or reconciled, an open artifact viewer can appear without reserved space and overlap or compress the chat, while a stale viewer can leave an empty reserved column.

Recommended fix: Derive the effective timeline session identity before the viewer-store lookup and use it consistently for useOpenArtifact, ArtifactViewerPanel, and every layout calculation driven by viewer visibility.

Test: Extend the reconciliation test so requested and effective session IDs differ, seed viewer state only for the effective session, and assert that the rendered chat layout enters its viewer-open configuration; also verify stale requested-session viewer state does not reserve space.

// timelineSessionId (the controller's effective session) rather than the
// raw requested sessionId, which can briefly disagree with
// controller.messages during session replacement or reconciliation.
<ArtifactPolicyProvider

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🤖 P2 · Artifact policy has duplicate owners (non-blocking)

The new ChatView provider wraps a ChatTranscriptSurface that still creates its own provider, and ChatCanvasCard similarly gives its transcript and composer separate providers. Each provider independently derives artifact inventory and owns its own per-path open debounce. The same visible session can therefore process different message projections and bypass duplicate-open suppression when actions originate from different surfaces.

User effect: Opening the same artifact quickly from two parts of one chat can hand it to the operating system twice, and artifact availability can drift between the transcript and adjacent viewer, rail, or composer surfaces.

Recommended fix: Mount one ArtifactPolicyProvider per rendered session boundary, placing transcript, composer, viewer, auto-open mount, and artifact widgets beneath it. Keep standalone transcript support through an explicit wrapper rather than implicit nested ownership.

Test: Add a provider-boundary test proving one policy owner covers all full-chat consumers and one covers both canvas transcript and composer; verify opening one path across two consumer surfaces shares the same debounce lifecycle.

**Category:** bug-fix (review follow-up)
**User Impact:** During session replacement/reconciliation, the artifact
viewer's reserved width always agrees with the panel actually rendered —
no overlapped chat under an unreserved viewer, no empty reserved column
from a stale entry.

**Problem:** The previous follow-up moved ArtifactViewerPanel (and the
providers) to timelineSessionId but left isArtifactViewerOpen reading
useOpenArtifact(sessionId) with the raw requested id. The layout math
(chat-row occlusion, gap, conversation min-width) could therefore
disagree with the panel about which viewer-store entry is authoritative.

**Solution:** Hoist the timelineSessionId derivation above the store
lookup and key useOpenArtifact by it, so the panel, providers, and every
viewer-driven layout decision read one store entry. All useOpenArtifact
call sites in ChatView audited (rg) — this was the only remaining
raw-sessionId reader. Extends the reconciliation test per the review
spec: viewer state seeded only for the effective session must reserve
the conversation's viewer-open floor; stale requested-session state must
not.
**Category:** bug-fix (review follow-up, P2)
**User Impact:** Opening the same artifact quickly from two surfaces of
one chat hands it to the OS once, and artifact availability can no
longer drift between the transcript and its sibling surfaces.

**Problem:** ChatTranscriptSurface mounted its own ArtifactPolicyProvider
inside ChatView's outer one, and ChatCanvasCard gave its transcript and
composer separate providers. Each provider derives artifact inventory
independently and owns a private per-path open debounce, so one visible
session had duplicate policy owners: double collection work per message
change, divergent projections (gated vs ungated), and duplicate-open
suppression that surfaces could bypass.

**Solution:** Remove the transcript's implicit provider. The surface that
composes a session boundary now mounts exactly one provider: ChatView's
chat row (transcript + viewer panel + right rail + auto-open) and
ChatCanvasCard (transcript + composer). Provider-boundary tests assert
one owner covers all consumers on both surfaces (fail against the nested
structure), and a debounce test proves an open from the viewer and the
artifacts widget shares one per-path lifecycle.

All ChatTranscriptSurface consumers audited (rg): ChatView and
ChatCanvasCard are the only mounts; both now provide the context above
it.
@badideasforsale

Copy link
Copy Markdown
Contributor Author

🤖 Both round-2 findings addressed.

P1 · Viewer layout reads stale session → fixed in 3887555. timelineSessionId is now derived immediately after effectiveSessionbefore the viewer-store lookup — and isArtifactViewerOpen reads useOpenArtifact(timelineSessionId), so the panel, providers, and every viewer-driven layout decision (row occlusion, gap, conversation min-width) describe one store entry. All useOpenArtifact call sites in ChatView were enumerated (rg) — this was the only remaining raw-sessionId reader. The reconciliation test now covers layout per your spec: viewer state seeded only under the effective session must reserve the conversation's viewer-open floor, and stale requested-session state must not (fails against the previous head).

P2 · Artifact policy has duplicate owners → taken in this PR rather than deferred, in 5f1a940. ChatTranscriptSurface no longer mounts its own provider; the surface composing a session boundary owns exactly one: ChatView's chat row (transcript + viewer panel + right rail + auto-open) and ChatCanvasCard (transcript + composer, replacing the composer-only provider). Both requested tests added:

  • provider-boundary tests assert one owner covers all consumers on both surfaces (each fails against the nested structure), and
  • a cross-surface debounce test opens the same path from the viewer's ⋯ menu and the artifacts widget under one real provider and asserts a single OS hand-off.

ChatTranscriptSurface consumers audited (rg): ChatView and ChatCanvasCard are the only mounts, and both now provide the context above it — standalone transcript support is explicit wrapping by the composing surface, per your recommendation.

All chat + home suites pass (3,213 tests); just check clean.

@morgmart morgmart left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🤖 Automated code review

COMMENT: all four previously reported issues are fixed in the current comparison. One new non-blocking documentation finding remains: ChatView's changed ownership comment says ChatTranscriptSurface creates a provider even though this range removes it. Supplied visual evidence is present. GitHub evidence is structurally valid; several checks were still in progress at capture time, so required checks govern merge readiness.

Deterministic publication result: 0 blocking and 1 non-blocking inline finding(s) publishable; 0 duplicate(s) suppressed; 0 blocking screenshot-evidence requirement(s) in this review body.

Comment thread src/features/chat/ui/ChatView.tsx Outdated
// because siblings of the transcript consume the artifact context too:
// ArtifactViewerPanel ("Open in editor"), the right rail's
// ArtifactsWidget (row opens), and ArtifactAutoOpenMount (the artifact
// list). ChatTranscriptSurface renders its own provider for the

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🤖 P3 · Comment reverses provider ownership (non-blocking)

The changed ChatView comment says ChatTranscriptSurface renders its own ArtifactPolicyProvider, but this same comparison removes that provider and establishes the composing session surface as the single owner. The comment now documents the opposite of the corrected architecture beside the load-bearing provider boundary.

User effect: A future maintainer can follow the comment and reintroduce nested providers, bringing back inconsistent artifact inventory and duplicate open-action lifecycles across one chat.

Recommended fix: Update the comment to state that ChatTranscriptSurface intentionally does not own artifact policy and that ChatView provides the single shared boundary for the transcript and its sibling consumers.

Test: No runtime test is needed for prose alone; keep the existing provider-boundary tests that enforce one owner across the composed chat surface.

**Category:** documentation (review follow-up, P3)
**User Impact:** None at runtime; prevents a future maintainer from
following stale prose and reintroducing nested providers.

**Problem:** The comment beside ChatView's ArtifactPolicyProvider still
said ChatTranscriptSurface renders its own provider — the opposite of
the single-owner architecture the previous commit established.

**Solution:** The comment now states that ChatView owns the single
policy boundary and that ChatTranscriptSurface intentionally does not
mount a provider (enforced by the provider-boundary tests). Prose
audited for other stale ownership claims (rg): the ChatView test-file
mock comment carried the same stale claim and is corrected in the same
commit; no other hits.
@badideasforsale

Copy link
Copy Markdown
Contributor Author

🤖 P3 addressed in 2eba344 — the ChatView comment now documents the single-owner architecture (ChatView owns the policy boundary; ChatTranscriptSurface intentionally mounts no provider, enforced by the boundary tests). Audited for other stale ownership prose (rg): the test-file mock comment carried the same claim and is fixed in the same commit; no other hits.

@morgmart morgmart left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🤖 Automated code review

COMMENT: all five prior review findings are fixed in the current comparison. One new non-blocking lifecycle issue remains: the artifact-policy provider changes session identity without remounting, so provider-local debounce state can leak briefly across reconciled sessions. Supplied visual evidence is present. GitHub evidence is structurally valid; macOS Tauri checks and frontend checks were still in progress at capture time, so required checks govern merge readiness.

Deterministic publication result: 0 blocking and 1 non-blocking inline finding(s) publishable; 0 duplicate(s) suppressed; 0 blocking screenshot-evidence requirement(s) in this review body.

// timelineSessionId (the controller's effective session) rather than the
// raw requested sessionId, which can briefly disagree with
// controller.messages during session replacement or reconciliation.
<ArtifactPolicyProvider

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🤖 P2 · Provider state crosses session replacement (non-blocking)

The moved ArtifactPolicyProvider receives timelineSessionId, but React reuses the same provider instance when that prop changes. Provider-local refs, including the per-path open debounce map, therefore survive when reconciliation replaces the effective session without unmounting ChatView. A path opened in the previous session remains temporarily marked as recently opened in the replacement session.

User effect: If someone opens the same file path immediately after a chat session is replaced or reconciled, the first open in the new session can appear to do nothing because it is suppressed by the previous session's debounce state.

Recommended fix: Key the ArtifactPolicyProvider by timelineSessionId, or explicitly reset all session-scoped provider state when the session identity changes, so each effective session starts with an independent artifact-action lifecycle.

Test: Add a reconciliation test that opens a path, changes the effective session identity without unmounting ChatView, and verifies that opening the same path in the replacement session reaches the OS boundary immediately.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants