Skip to content

feat: artifactAutoOpen — auto-open artifact detail panels (default on) - #892

Open
ankit-thesys wants to merge 19 commits into
mainfrom
ankit/artifact-view-mode
Open

feat: artifactAutoOpen — auto-open artifact detail panels (default on)#892
ankit-thesys wants to merge 19 commits into
mainfrom
ankit/artifact-view-mode

Conversation

@ankit-thesys

@ankit-thesys ankit-thesys commented Jul 28, 2026

Copy link
Copy Markdown
Contributor

Problem

Artifact detail panels only open on an explicit user click. There is no way for a chat shell to open the panel automatically while an artifact streams in — the behavior the original c1 chat shipped.

Change

New boolean prop artifactAutoOpen (default true, per review) on ChatProvider, AgentInterface, and the OpenUIChat withChatProvider family.

When enabled:

  • The first artifact of a stream opens by itself, once, while it streams. Further artifacts arriving in the same stream are ignored.
  • A user's close sticks — nothing re-opens for the rest of the stream (or ever, for that artifact).
  • Edits re-open the panel (old-c1 parity): the claim is keyed by id:version, so an edit's new version gets its own chance and re-points the panel as it streams. Re-registrations of the same version stay quiet, so a close still sticks.
  • Thread reloads stay quiet — historical artifacts register while nothing is running and burn their chance without opening.
  • A new stream's first artifact takes over the panel: if the panel is already open (user-opened or from a previous stream), it re-points to the new artifact as it starts streaming.

Pass artifactAutoOpen={false} for pure click-to-open — the watcher then subscribes to nothing.

Architecture: registration-driven, entirely in react-headless

ChatProvider runs a store-level watcher (artifactAutoOpenWatcher) subscribed to the ThreadContext artifact registry. Registration is the trigger — whoever renders an artifact registers it, and that registration is the moment it may present itself. The store layer needs no renderer or parsing knowledge, and react-ui's entire footprint is the prop pass-through (2 lines in AgentInterface, 2 in withChatProvider).

Design notes:

  • Claim first, ask later: every first-seen id burns its latch claim whether or not it opens. Historical artifacts can therefore never pop open later, and host remounts / StrictMode re-registrations are invisible.
  • One open per stream: the watcher tracks isRunning transitions; after the stream's first auto-open, later artifacts in the same run are claimed but ignored.
  • Both latches live inside the watcher hook — the detailed-view store is untouched by this PR.
  • Evaluation is O(registered ids) with zero parsing; already-claimed ids short-circuit.

Artifact sources that bypass the registry (an SDK's chat-library artifacts) are out of scope: a useArtifactAutoOpen escape-hatch hook was prototyped and then removed as unused (a87874dd; archived in workspace notes). When the chat-shell integration lands it will either register those artifacts in the ThreadContext (watcher covers them) or reintroduce the hook.

Core files

File What it does
react-headless/store/artifactAutoOpenWatcher.ts The whole feature in one file. evaluateRegisteredArtifacts (one pass: claim once → allowed now → open/take over the panel, at most one per pass) and the ChatProvider hook that subscribes it to the artifact registry, holds both latches (per-artifact claims, cleared on thread switch; per-stream open flag, re-armed on each run), and enforces one open per stream.
react-headless/store/ChatProvider.tsx Wires the watcher to the three stores — why no other wiring exists anywhere.
react-headless/store/types.ts The artifactAutoOpen?: boolean prop (default true) on ChatProviderProps.
react-headless/store/artifactViewId.ts artifactViewId / parseArtifactViewId — single source of truth for the id:version view-id format (exported; previously hand-rolled in four places across both packages).
react-ui: AgentInterface.tsx + withChatProvider.tsx Two lines each — accept the prop, pass it to ChatProvider. ToolActivityRenderer + Workspace now build/parse view ids via the shared helpers instead of hand-rolled strings.
Tests (1 suite) Watcher policy edge cases (close sticks, edits quiet, historical never fires, first-of-pass wins, reset re-arms) and the latch itself.

Verification

  • react-headless: 122 tests green; typecheck + eslint + prettier clean. react-ui: typecheck, 27 tests, build clean.
  • Live e2e on the genui-sdk testing/openui-cloud bench (PR head injected, no prop set — exercising the default), asserted programmatically and recorded: settled thread load stays quiet; a live generation auto-opens the stream's first artifact mid-stream; a second generation re-points the open panel to its new artifact; an edit re-opens a closed panel on its new version; a mid-stream close sticks through stream end. Labeled recording in the PR comments below.

🤖 Generated with Claude Code

https://claude.ai/code/session_011nPH2Rn2XGXETgx5Yu5seX

Adds an artifactViewMode prop ('auto-open' | 'open-on-mount' | 'overview',
default 'overview') to ChatProvider, AgentInterface, and the OpenUIChat
withChatProvider family. In 'auto-open' the detail panel opens by itself
while an artifact's tool call streams live; 'open-on-mount' opens on every
artifact mount (deep-link/kiosk); the default keeps today's click-to-open
behavior, so the change is inert for existing consumers.

One effect in ToolActivityRenderer serves every artifact renderer. It
re-gates the error/parsed-null shapes (those hooks sit above the fallback
early-return), only fires for live streams (historical activities mount as
'complete', so thread reloads stay quiet), and latches on the detailed-view
store keyed by TOOL-CALL id — not meta id:version, because a streamed edit
often carries no version in its args and would collide with the generate's
key, swallowing the edit's auto-open. The store latch survives host
remounts mid-stream (a per-instance ref would re-fire over a user's
mid-stream close) and resets on thread switch.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_019hnic4tGtRh3MSSaCdGJYE
Comment on lines 21 to 30
set({ activeDetailedViewId: null, _autoOpenedArtifactKeys: new Set() });
},

_autoOpenedArtifactKeys: new Set<string>(),
_markAutoOpened: (key) => {
const keys = get()._autoOpenedArtifactKeys;
if (keys.has(key)) return false;
set({ _autoOpenedArtifactKeys: new Set(keys).add(key) });
return true;
},

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Explain this part?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

This is the auto-open latch's check-and-claim, done as one atomic call so there's no gap between "has it fired?" and "mark it fired":

  • keys.has(key) → already claimed → return false, and the caller skips opening. This is what makes a user's mid-stream close stick: if the renderer host remounts during streaming (it does — React StrictMode and streaming re-renders both remount it), the fresh instance asks again, gets false, and does NOT re-open over the close.
  • Otherwise new Set(keys).add(key) — a copy, not keys.add(key) in place, because zustand subscribers compare by reference; mutating the existing Set would be an invisible state change. Copying keeps the store honest.
  • reset() (one line up) clears the Set together with the active view on thread switch, so a fresh thread starts with a clean slate.

Comment on lines +38 to +51
/**
* Artifact keys (`id:version`) that already auto-opened once, so a user's
* mid-stream close sticks even when the renderer host remounts during
* streaming. Cleared by `reset()` (thread switch).
* @internal
*/
_autoOpenedArtifactKeys: ReadonlySet<string>;
/**
* Atomically records an auto-open for `key`. Returns `false` when the key
* already fired (the caller must not open again), `true` when this call
* claimed it.
* @internal
*/
_markAutoOpened: (key: string) => boolean;

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Can you also explain this part?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

This is the type contract for that latch: _markAutoOpened(key) returns a boolean that IS the decision — true = this call claimed the key, go ahead and open; false = something already auto-opened for this key, do nothing. The caller never reads the Set directly (it's exposed as ReadonlySet and underscore-internal, so it's not public API surface).

One thing your comment flushed out: the docstring still said keys are id:version from an earlier iteration. The final implementation latches by tool-call id — a streamed edit often carries no version in its args, so id:version collides with the generate's key and would swallow the edit's auto-open (caught in live testing). Tool-call id is unique per generate/edit call and stable across remounts, which is exactly the "once per call" semantics we want. Doc fixed in 4dd3c4c.

Comment on lines +153 to +171
const viewMode = useArtifactViewMode();
const openedThisMountRef = useRef(false);
const isError = activity.isError ?? false;
const parsedIsNull = parsed === null;
useEffect(() => {
if (viewMode === "overview") return;
if (isError || parsedIsNull) return;
const dv = dvStore.getState();
if (viewMode === "auto-open") {
if (!isStreaming) return;
if (!dv._markAutoOpened(activity.id)) return;
} else {
if (openedThisMountRef.current) return;
}
openedThisMountRef.current = true;
dv.setActiveDetailedView(viewId);
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [viewMode, isStreaming, isError, parsedIsNull, viewId, activity.id, dvStore]);

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Can you tell me why we are doing it here? We can do this in react-headless

Just trying to understand what is happening ?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Fair question — the split is deliberate, and react-headless does own everything it can own:

In react-headless already: the artifactViewMode value (ChatProvider prop → context) and the latch (_markAutoOpened on the detailed-view store, reset on thread switch). That's the policy + state.

Why the trigger effect lives here and not in react-headless: the open decision needs per-activity render context that a renderless package doesn't have —

  1. parsed / activity.isError — a failed or unparseable tool call renders only the fallback card and must never open a panel. Whether a parse succeeded is only known here, where the renderer's parser actually ran.
  2. viewId — derived in this component, including the useId() fallback for early frames before meta lands. useId is a React render-tree concern; headless can't produce it.
  3. isStreaming for this mounted activity — the reload-safety gate (historical activities mount as complete).

ToolActivityRenderer is the one place every artifact renderer is mounted and bound to a viewId — the same place that already runs the two open-preserving effects (viewId migration, edit-version follow) this one must cooperate with. If we moved the effect into a react-headless hook, react-ui would still have to call that hook from this exact spot with these exact inputs — same coupling, one extra layer of indirection. So: state/policy headless, render-coupled trigger here.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Update: done your way — 12758b7 moves the whole behavior into react-headless. New exported hook useArtifactAutoOpen({ viewId, latchKey, isStreaming, enabled }) owns the mode context, the shouldAutoOpen predicate, the per-mount latch, and the store-level auto-open latch; ToolActivityRenderer shrinks to a single call feeding it the render-derived facts (viewId, tool-call id, streaming, error/parse eligibility) — the only things a renderless package can't know. Bonus: any headless consumer building its own thread UI now gets auto-open from the same hook, and the c1 companion PR deletes its local copy and calls this one (−90 lines there). Predicate matrix test moved into react-headless (118 tests green); full auto-open suite re-verified live on the linked pair after the refactor.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Follow-through: 00a81646 moves the invocation too — this file is back to zero auto-open code.

ChatProvider now runs a store-level watcher (react-headless artifactAutoOpenWatcher): it subscribes to the thread's messages, derives each artifact tool call's status from the store, parses via a shared runArtifactRenderer envelope (now also the single definition this renderer uses — the two sides can never disagree on what a parser sees), and opens once per tool call via the existing latch. react-ui's entire diff is the 2+2-line prop pass-through.

Deliberate semantics with the move:

  • open-on-mount = once per tool call per thread session (loading a thread opens its newest artifact) — a store watcher can't observe mounts.
  • Renderers that never yield meta (useId-fallback hosts) can't be auto-opened from the store; useArtifactAutoOpen stays public for those and for non-tool-call artifact sources (the SDK's chat-library path).
  • Cost is latch-bounded: already-opened (or user-closed) calls are skipped before any parsing.

140 tests green (11 new watcher tests); full live matrix re-verified on a linked build: auto-open hands-off, mid-stream close sticks, reload quiet, edit re-opens, presentation, and open-on-mount opening a settled thread.

llm={llm}
artifactRenderers={artifactRenderers}
artifactCategories={artifactCategories}
artifactViewMode={artifactViewMode}

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

This looks good.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

👍 — and note it's Omit<ChatProviderProps, "children"> inheritance, so the prop rides the existing type: no separate AgentInterface-side type to keep in sync.

ankit-thesys and others added 4 commits July 28, 2026 20:01
Review feedback: keep the logic in react-headless. useArtifactAutoOpen now
owns the complete behavior — mode context, shouldAutoOpen predicate,
open-on-mount per-mount latch, and the store-level auto-open latch — and is
exported for any headless consumer building its own thread UI.
ToolActivityRenderer shrinks to one call supplying the render-derived
facts (viewId, tool-call latch key, streaming, error/parse eligibility).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_019hnic4tGtRh3MSSaCdGJYE
…I wiring

Tool-call auto-open now lives entirely in react-headless: ChatProvider
subscribes to the thread's messages and opens an artifact's detailed view
per the mode, once per tool call, latched on the detailed-view store. The
parser envelope moves to a shared runArtifactRenderer export so the watcher
and react-ui's tool renderer can never disagree on what a parser sees.

react-ui keeps only the artifactViewMode prop pass-through; its tool
renderer loses the hook call and its local envelope copy (net deletion).
open-on-mount is redefined store-side — once per tool call per thread
session, so loading a thread opens its newest artifact — since a store
watcher cannot observe mounts. useArtifactAutoOpen stays public for
artifact sources the chat store can't see (SDK chat-library paths,
custom hosts).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_011nPH2Rn2XGXETgx5Yu5seX
Simpler trigger, simpler policy (per review): an artifact may auto-open
exactly once — when its id first registers in the ThreadContext — and only
into an empty panel. Edits share their generate's id, so they never force
a panel open (an open panel still follows versions via the existing
follow effect); an open panel is never stolen (first wins); historical
registrations burn their one chance quietly, so reloads never fire.

The watcher now subscribes to the artifact registry instead of walking
messages: no parsing, no renderer knowledge at the store layer, and the
shared parser envelope (runArtifactRenderer) is no longer needed —
react-ui's tool renderer returns to exactly its pre-branch shape.
auto-open gates on the thread running; open-on-mount opens regardless.
useArtifactAutoOpen (non-registry sources) adopts the same first-wins
policy.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_011nPH2Rn2XGXETgx5Yu5seX
@ankit-thesys

ankit-thesys commented Jul 30, 2026

Copy link
Copy Markdown
Contributor Author

Semantics + architecture update (a4ec3537), decided in review: the trigger is now artifact registration, and the policy is "present once" — an artifact may auto-open exactly one time, when its id first registers, and only into an empty panel.

What changed vs the previous revision:

  • Edits no longer re-open. An edit shares its generate's artifact id, whose one chance is already spent. An open panel still follows edit versions live (existing follow effect); a closed one stays closed — the user is never interrupted twice by the same artifact.
  • First wins. An open panel is never stolen — not by a second artifact in the same turn, not by anything.
  • Simpler machinery. The watcher subscribes to the ThreadContext registry instead of walking messages: no parsing at the store layer, no shared parser envelope needed (runArtifactRenderer deleted), and ToolActivityRenderer is back to exactly its pre-branch shape. Net −100 lines.

Trade-off made deliberately: this drops old c1's "edit re-opens the panel" behavior in exchange for a much simpler mechanism and a calmer UX. Everything re-verified live: auto-open on first registration, close-sticks, edit stays quiet, reload quiet, new-id opens, open-on-mount presents a settled thread. 128 headless tests green.

ankit-thesys and others added 2 commits July 31, 2026 06:10
…modes

Post-review pass on artifactViewMode:

- Trim the public barrel to types-only — `ArtifactViewMode` stays exported;
  `shouldAutoOpen`, `useArtifactAutoOpen`, `UseArtifactAutoOpenOptions`,
  `ArtifactViewModeContext`, and `useArtifactViewMode` are now internal.
  The hook goes public in the PR that consumes it.
- Unify `useArtifactAutoOpen`'s once-latch on the detailed-view store for
  all modes — previously `open-on-mount` used a per-mount ref, so a host
  remount could re-open a panel the user had closed.
- Condense JSDoc/comments.

Co-authored-by: Cursor <cursoragent@cursor.com>
Co-authored-by: Cursor <cursoragent@cursor.com>
Comment on lines 35 to 44
_detailedViewPanelNode: HTMLElement | null;
/** @internal */
_setDetailedViewPanelNode: (node: HTMLElement | null) => void;
/** @internal */
_autoOpenedArtifactKeys: ReadonlySet<string>;
/** @internal */
_markAutoOpened: (key: string) => boolean;
};

/** Combined detailed-view store type (state + actions + internals). */

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

What is happening here and why are we changing this ?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

This is the state behind the new artifactViewMode prop on ChatProvider. The policy is "present once": an artifact may auto-open the side panel exactly once, when its id first registers — _autoOpenedArtifactKeys is the set of keys that already used that chance, and _markAutoOpened is the atomic check-and-claim (returns false when the key is already burned, so the caller skips opening). It lives on the detailed-view store rather than component state so the latch survives host remounts and StrictMode double-effects, and reset() clears it on thread switch so a new thread starts clean. The underscore + @internal marking follows this file's existing convention (_detailedViewPanelNode) — nothing here is public API; the barrel only exports the ArtifactViewMode type.

…nto watcher

The hook had no callers: the SDK's artifact renderers go through the
registry path (covered by the watcher), and the chat-shell integration
that might need a non-registry path is deferred. Archived for later
reintroduction if needed. Its shouldAutoOpen predicate moves into
artifactAutoOpenWatcher.ts; the decision-matrix test folds into the
watcher suite (still 128 tests).

Co-authored-by: Cursor <cursoragent@cursor.com>
@vercel

vercel Bot commented Aug 4, 2026

Copy link
Copy Markdown

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated (UTC)
openui-docs Ready Ready Preview Aug 6, 2026 9:38am

Request Review

Comment on lines +84 to +86
<ArtifactViewModeContext.Provider
value={artifactViewMode ?? DEFAULT_ARTIFACT_VIEW_MODE}
>

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

not required

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Removed in 813be89 — the context (and its provider layer here) had no readers left once the escape-hatch hook was deleted. As of 0b751b9 the mode is gone entirely: ChatProvider just passes the boolean autoOpenArtifact to the watcher.

@@ -0,0 +1,53 @@
import { useEffect } from "react";

@abhithesys abhithesys Aug 5, 2026

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

only this file plus updating props to pass to useArtifactAutoOpen is needed.

  1. while streaming first artifact should open in that stream.
  2. if another artifact comes in same stream, we should ignore it.
  3. no need for artifact open on mount. replace it with boolean prop: autoOpenArtifact

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Done in 0b751b9 — exactly this file plus prop plumbing:

  1. ✅ While streaming, the stream's first artifact opens (registration-driven, while isRunning).
  2. ✅ Another artifact in the same stream is ignored — the watcher latches one open per run (isRunning transition resets it), on top of the per-artifact once-latch.
  3. open-on-mount removed; the prop is now boolean autoOpenArtifact (default false) on ChatProvider/AgentInterface/withChatProvider. The ArtifactViewMode type and mode context are deleted — no new barrel exports at all.

(useArtifactAutoOpen itself was already removed earlier as unused — a87874d.)

120 tests green incl. a new first-of-stream-only case; re-verified live on the genui-sdk bench: settled thread loads stay quiet, live generation auto-opens its first artifact mid-stream, mid-stream close sticks through stream end.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Update per the Linear thread on TH-2454 (eb58c9d8): final prop name is artifactAutoOpen and it now defaults to true — hosts pass false to keep click-to-open. Re-verified live on the bench with no prop set: settled thread quiet, live generation auto-opens its first artifact, mid-stream close sticks.

The context's only reader was the useArtifactAutoOpen hook removed in
a87874d. The file shrinks to the ArtifactViewMode type + default
(renamed artifactViewMode.ts) and ChatProvider loses the dead provider
layer. Reintroduce alongside the hook if the SDK integration needs it.

Co-authored-by: Cursor <cursoragent@cursor.com>
Per review: drop the open-on-mount mode and the mode union entirely.
autoOpenArtifact (default false) opens the stream's FIRST artifact
while it streams; further artifacts in the same stream are ignored
(claimed but never opened). Close-sticks, edit-quiet, reload-quiet,
and first-wins semantics unchanged. Public surface is now just the
boolean prop - the ArtifactViewMode type export is gone.

Co-authored-by: Cursor <cursoragent@cursor.com>
@ankit-thesys ankit-thesys changed the title feat: artifactViewMode — opt-in auto-open for artifact detail panels feat: autoOpenArtifact — opt-in auto-open for artifact detail panels Aug 5, 2026
Per Linear review decisions on TH-2454: the boolean prop is
artifactAutoOpen and auto-open is the default — the stream's first
artifact presents itself unless the host passes false.

Co-authored-by: Cursor <cursoragent@cursor.com>
@ankit-thesys ankit-thesys changed the title feat: autoOpenArtifact — opt-in auto-open for artifact detail panels feat: artifactAutoOpen — auto-open artifact detail panels (default on) Aug 5, 2026
Co-authored-by: Cursor <cursoragent@cursor.com>
ankit-thesys and others added 2 commits August 6, 2026 06:40
With the escape-hatch hook gone the watcher is the latch's only
consumer, so it no longer needs to live on the detailed-view store.
createDetailedViewStore/detailedViewTypes revert to main; the claimed
set is a hook ref cleared on thread switch. The feature is now a
single file plus prop plumbing.

Co-authored-by: Cursor <cursoragent@cursor.com>
Co-authored-by: Cursor <cursoragent@cursor.com>
ankit-thesys and others added 2 commits August 6, 2026 06:54
Co-authored-by: Cursor <cursoragent@cursor.com>
Co-authored-by: Cursor <cursoragent@cursor.com>
The `${id}:${version}` string was hand-rolled in four places across
two packages with nothing enforcing agreement. New artifactViewId /
parseArtifactViewId helpers in react-headless (exported — react-ui
consumes the public barrel) now build and parse it everywhere: the
auto-open watcher, ToolActivityRenderer (build + edit-follow parse),
and the Workspace rail.

Co-authored-by: Cursor <cursoragent@cursor.com>
@ankit-thesys

Copy link
Copy Markdown
Contributor Author

E2E demo — final design (artifactAutoOpen, default on)

Recorded on the genui-sdk testing/openui-cloud bench with no prop set (exercising the default), PR head 5644e40c injected. Real generations, zero artifact clicks; each phase labeled on screen (purple = running, green = passed) and asserted by the driver — the run fails if any behavior is wrong:

artifactAutoOpen final demo

# Behavior Result
1 Loading an old thread — nothing streaming → panel stays closed
2 Live generation — the stream's first artifact opens by itself mid-stream
3 Close mid-stream — must not re-open for the rest of the stream

(The earlier demo comments showed the since-removed mode-based API and have been deleted.) Full-quality mp4: gist · Linear: TH-2454

Drop the "never steal an open panel" guard: when a new generation
streams in a new artifact, the open panel re-points to it. Close-sticks
(per-artifact claim) and one-open-per-stream are unchanged. Verified
live: deck panel open -> report requested -> panel re-pointed
mid-stream; close mid-stream stayed closed.

Co-authored-by: Cursor <cursoragent@cursor.com>
An edit's new version now gets its own auto-open chance (old-c1
parity): the claim set is keyed by id:version instead of artifact id.
Same-version re-registrations stay quiet, so a user's close still
sticks. Verified live: deck generated, panel closed, edit requested ->
panel re-opened on the new version mid-stream; closing during the edit
stream stuck.

Co-authored-by: Cursor <cursoragent@cursor.com>
@ankit-thesys

Copy link
Copy Markdown
Contributor Author

Two late behavior changes — recorded and live-verified

Both requested during review of the running build; commits fd85a15f and 3d6bde6a. Driver-asserted runs on the bench (real generations, zero artifact clicks):

1. Panel take-over (fd85a15f)

A new stream's first artifact re-points an already-open panel (previously "first wins" — an open panel was never stolen). In the video: Q2 deck auto-opens → panel left open → EV report requested → panel re-points to the report mid-stream → close mid-stream sticks.

panel take-over demo

2. Edit re-open (3d6bde6a, old-c1 parity)

The once-claim is keyed by id:version instead of artifact id, so an edit's new version gets a fresh chance and re-opens even a user-closed panel; same-version re-registrations stay quiet (close sticks per version). In the video: deck auto-opens → user closes → "add one more slide" → panel re-opens on the new version mid-stream → closing during the edit stream sticks.

edit re-open demo

122 unit tests green · PR description updated with both semantics · full-quality mp4s on TH-2454 and in the gist

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