Skip to content

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

Open
badideasforsale wants to merge 5 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 5 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.

Five 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); "Reveal in Finder" stays available and the actions return if polling sees the file reappear.

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.

<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.

<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.

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