Skip to content

Artifact UX polish: native save-file dialog + ChatToolCallEvent in-progress wiring #3162

Description

@oxoxDev

Summary

Two intentional deviations from issue #2779's acceptance criteria — shipped as follow-up work to keep PR #3017 reviewable while parent issue #1535 closes.

Problem

#3017 landed ArtifactCard + Tauri download command + backend ArtifactReady/ArtifactFailed events, but two of the original AC items from #2779 were intentionally deferred:

  1. AC#3 — native save-file dialog is not wired. The tauri-plugin-dialog crate currently conflicts with the workspace's pinned schemars version, so the renderer can't open a SaveDialog to let the user pick a target path. The shipped behaviour copies the artifact into ~/Downloads/ and reveals it in the OS file manager, which works but is not what AC#3 specified.

  2. AC#1 — ArtifactCard in-progress state only renders once the Rust core publishes DomainEvent::ArtifactReady/Failed. There is no spinner during the actual generate_presentation tool execution, so users see no UI between hitting send and the card appearing. The ChatToolCallEvent already fires when the tool dispatches; the card should subscribe to it and render an in-progress state keyed on the future artifact_id.

Spotted by @graycyrus in #3017 (review).

Solution (optional)

For AC#3 — save-file dialog:

  • Upgrade tauri-plugin-dialog to a version compatible with the workspace's schemars, OR vendor a minimal save-file-dialog command in the Tauri shell that bypasses the plugin entirely (rfd crate is a candidate — already used by the openhuman shell elsewhere).
  • Wire ArtifactCard's "Download" button to invoke the dialog command; fall back to the current Downloads+reveal pattern when the user cancels or the dialog is unavailable on the platform.

For AC#1 — in-progress state:

  • Extend ChatToolCallEvent to carry the artifact_id when the dispatching tool is generate_presentation (or surface it via a sibling ArtifactPending domain event from create_artifact at tool start).
  • Subscribe ArtifactCard to the new event so it renders a "Generating..." skeleton between dispatch and ArtifactReady.
  • Failure path: ArtifactFailed already flips the card to retry-hint; the in-progress skeleton just needs to hand off cleanly.

Acceptance criteria

  • Save-file dialog wired on macOS + Windows + Linux — clicking Download on the ArtifactCard opens a native Save As picker with the suggested filename pre-filled; user-chosen path receives the file; cancel is a no-op.
  • Downloads+reveal fallback retained — when the dialog is unavailable (plugin error, headless build, user cancels with a fallback flag) the existing copy-to-Downloads + reveal behaviour kicks in.
  • ChatToolCallEvent → ArtifactPending wiring — the chat runtime renders an in-progress card the moment generate_presentation is dispatched, before ArtifactReady arrives.
  • State transitions covered by tests — Vitest spec for the in-progress→ready transition; Vitest spec for the in-progress→failed transition; Tauri command unit test for the save-file path (mocked dialog).
  • Diff coverage ≥ 80% — the implementing PR meets the changed-lines coverage gate (Vitest + cargo-llvm-cov, enforced by .github/workflows/pr-ci.yml).

Related

Additional follow-up — Retry button wiring (added 2026-06-02)

Spotted by @graycyrus on PR #3017: ArtifactCard's onRetry prop is optional (declared at ArtifactCard.tsx:33) but never passed at the call site in Conversations.tsx:2162. That means on a failed artifact today the Retry button does not render — the card is a no-op surface for the failed state.

Real retry needs one of:

  • Cheap path: a removeArtifact({threadId, artifactId}) reducer + onRetry={(id) => dispatch(removeArtifact({threadId, id}))} at the call site. Clears the failed card so the user can re-prompt the agent. Not strictly "retry" semantically (it doesn't re-run the original tool call), but unblocks the user.
  • Full path: persist the originating ChatToolCallEvent args + add a retryArtifactGeneration(threadId, artifactId) action that re-dispatches the same tool call. Requires storing tool-call args keyed on artifact_id at dispatch time, plus an RPC re-dispatch path.

Either is out of scope for #3017's merge gate but should land before parent #1535 closes so the failed-artifact UX isn't a dead end.

Updated acceptance criteria

  • Save-file dialog wired on macOS + Windows + Linux (unchanged)
  • Downloads+reveal fallback retained (unchanged)
  • ChatToolCallEvent → ArtifactPending wiring (unchanged)
  • Retry button on failed cards wired to a meaningful action — either cheap-path removeArtifact or full-path re-dispatch.

Metadata

Metadata

Assignees

Labels

taskWork item that is not primarily a bug or a feature.

Type

No type
No fields configured for issues without a type.

Projects

Status
Done

Milestone

No milestone

Relationships

None yet

Development

No branches or pull requests

Issue actions