Skip to content

feat(artifacts): redesigned workspace — in-place editing, comments, version history#213

Open
ianu82 wants to merge 15 commits into
mainfrom
codex/artifact-workspace-ui
Open

feat(artifacts): redesigned workspace — in-place editing, comments, version history#213
ianu82 wants to merge 15 commits into
mainfrom
codex/artifact-workspace-ui

Conversation

@ianu82

@ianu82 ianu82 commented Jun 22, 2026

Copy link
Copy Markdown
Contributor

What this is

This PR adds the artifacts workspace to MindsHub Cowork — the full-screen surface where you open something Anton made (a doc, a landing page, a slide deck, a dashboard) and actually work on it: edit it in place, comment on it, watch its version history, chat with Anton about it, and publish/share it.

It's a big PR because it's a whole feature area, not a tweak. This description is a map so you don't have to reverse-engineer it from the diff.

What you get

  • One workspace, one timeline. A single right-hand "Story" rail interleaves everything that has happened to the artifact — versions, comments, and your live chat with Anton — in time order.
  • Edit in place. Click any text in a prose doc or an HTML deck and type. Hit Save and it lands as one new version (not a version per keystroke).
  • Talk to Anton in context. The rail composer streams Anton's reply straight into the feed and can rewrite a selected section ("Fix with AI") without leaving the artifact.
  • Comments that point at things. Drop a pin on the exact spot (slide-scoped for decks). Open comments show on the canvas; resolved ones live in the rail.
  • Version history + diff. See every version, compare against the current one, restore an earlier one.
  • Present + Share. Fullscreen present mode and per-artifact publish/sharing.
  • "You", not a cryptic handle. Identity is unified from your sign-in, so your own activity reads as "You".

How to review a ~15k-line diff

The line count is misleading. Here's where it actually lives:

Area Where ~Lines What to do with it
Original workspace (fallback) components/artifact/ArtifactWorkspace.jsx ~6.2k The first-cut workspace. Ships as the flag-off fallback; the redesign is what's active — you don't need to read this one line-by-line to judge the feature.
The redesign (real review surface) components/artifact/redesign/* ~6k The new workspace, on by default. This is what to read.
Integration + API client views/ArtifactsView.jsx, views/ProjectsView.jsx, api.js ~2k Wiring the workspace into the app and the artifact API calls.

Suggested reading order inside redesign/:

  1. ArtifactWorkspaceRedesign.jsx — the composition layer that wires everything; start here.
  2. StoryRail.jsx — the unified versions + comments + chat timeline.
  3. useIframeInlineEdit.js + saveArtifactContent.js — the in-place-edit → save-one-version mechanism (the trickiest part; see note below).
  4. CommentLayer.jsx, useArtifactChat.js, VersionDiff.jsx — comments, streaming chat, version diff.

How it's kept safe to merge

  • Off-switch built in. The redesign defaults ON but flips to the original workspace per-machine with no rebuild: localStorage['anton:artifact-workspace-direction-2'] = 'false'.
  • Error-boundary wrapped. If the redesign throws, it falls back rather than taking down the app.
  • One save mechanism, chosen deliberately. In-place edits are saved by re-parsing the stored source with DOMParser, patching the edited node by a structural locator, and swapping the whole file through the backend's propose/accept flow — not by serializing the live DOM (which baked script-generated content into decks and corrupted them). This sidesteps that whole class of bug.

Testing

npm run build:web is green. The feature has been driven end-to-end against the companion backend (versions, comments, edits, publish) through several rounds of real-use feedback and a final hardening pass.

Heads-up for the reviewer

  • This branch is ~5 commits behind main — it'll want a merge/rebase with main before it goes in.
  • It talks to the backend in the companion PR below; they should land together.
  • Deliberately not built yet (none block the feature): real-time multiplayer cursors, the reviewer "verdict loop", and comparing two arbitrary (non-current) versions.

Companion PR (backend): mindsdb/cowork-server#78

🤖 Generated with Claude Code

ianu82 and others added 15 commits June 20, 2026 22:04
Flag-gated (localStorage anton:artifact-workspace-direction-2, default OFF)
redesign composed from a new self-contained library under
components/artifact/redesign/: app shell (WorkspaceShell/IconRail/TopBar),
unified Story rail (chat+versions+reviews, event coalescing), select→puck→AI
inline track-changes diff with Keep/Undo (409 → rebase against current
version), version scrubber + history (forward-restore), review surfaces
(owner "requested changes"+Fix-with-AI banner, reviewer in-app + guest-link,
forced verdict). M1 wired to /artifacts/edits/propose|accept (degrades to
mock on 404). Legacy ArtifactWorkspace.jsx untouched; only api.js + the
barrel edited. `npm run build:web` green.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
… single rail

Addresses review feedback, fully wired (no stubs in the active path):
- Single right rail (Story = chat·versions·comments·reviews); removed the
  redundant History/Review dock + the dead left icon rail.
- Removed macOS traffic lights; added a real Close (✕) button in the top bar.
- Inline chat: the rail composer streams Anton's reply IN the workspace via
  useArtifactChat (no more navigating to the task screen).
- Comment mark-up: a Comment-mode toggle + CommentLayer overlay drops pinned
  comments on the canvas (works over the HTML iframe too) → createArtifactComment
  with an x/y anchor; pins render from anchored comments.
- "Fix with AI" hands the open review notes to Anton via the inline chat.
- Present → real fullscreen (Fullscreen API on the canvas).
- Full-width review banner (fixes the cramped char-wrap in the old dock).
- Redesign default-ON for this branch + an error boundary so a runtime error
  shows a readable panel instead of white-screening.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…w fix

Round 2 of review feedback — all wired against the live backend, no stubs:
- STALE PREVIEW (the "Anton edited but I can't see it" bug): a reloadToken now
  bumps on every AI edit / restore / inline save and re-fetches the prose
  canvas + cache-busts the HTML iframe, with a "preview refreshed" toast.
- DIRECT IN-PLACE EDITING: a top-bar "Edit" toggle makes text directly typeable —
  contentEditable prose blocks (EditableProse) and the same-origin slide iframe
  (useIframeInlineEdit) — saved as a real new version via the edit pipeline.
  saveArtifactContent re-fetches the stored bytes as the whole-file find-string
  so the OCC swap always matches (verified end-to-end: propose 200 → accept 200
  → version advanced). Instant, no agent round-trip.
- STORY RAIL: header trimmed to just "Story"; Versions/Comments/Reviews now read
  as filters of All with live counts; comment/review rows show full text + per-item
  Resolve / Dismiss / Fix-with-AI (wired to /resolve, reject, and inline chat).
- Fix-with-AI hands review notes to Anton via the inline chat.
- Per-block AI "Keep" degrades to a local mock on 400/422 instead of throwing
  (backend model-generation in propose is still a TODO).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…p-save, Anton context

From the Loom walkthrough:
- Saving a direct edit no longer auto-skips the slide: direct edits don't bump
  the reload token (the DOM already reflects the typed change), so the iframe
  isn't remounted/reset.
- Timestamps: parse bare server datetimes as UTC (they lack an offset) so a
  3-min-old edit reads "3m ago", not "1h ago".
- Version authorship/labels: clean single label + correct WHO — a user's typed
  edit is "manual_edit" → "You · Edited"; AI → "Anton · AI edit" / "Generated
  update". (No more "Unknown · Manual · AI edit".)
- Anton artifact-awareness: the inline chat prepends the artifact's folder/title
  to the first turn, so "Fix with AI" edits THIS artifact directly instead of
  searching for it (saves tokens, avoids confusion/failures).
- Versions list: per-version "Restore" (+ Compare) action, with a "Current" tag
  on the latest (StoryRail onRestoreVersion/onCompareVersion).
- Top bar grouped: presence · [Edit | Comment] · [Share | Present | Close].
- api.js acceptArtifactEdit forwards operationType; saveArtifactContent marks
  direct edits manual_edit; per-block AI Keep degrades on 400/422 instead of throwing.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…opbar polish

From the second video pass:
- Compare now shows a real diff: VersionDiff modal fetches both versions' content
  and renders a line-level red/green diff (wired to the Versions "Compare").
- Versions time-grouped (Google-Docs style): consecutive version events within a
  10-min window collapse into one expandable "N edits · time-range" row (version
  events now carry an epoch `ts`).
- Resolved comments show "Reopen" (not "Fix with AI"); onReopenEvent → reopen.
- "Reviews" filter chip no longer truncates (filter row wraps + tighter chips).
- Removed the redundant bottom version scrubber (versions live in the Story rail now).
- Wider spacing between top-bar groups (presence · Edit|Comment · Share|Present|Close).

Note: slide-nav double-dot was diagnosed as the preview iframe double-initialising
the (correct) deck script; the reduced remounts here should resolve it — to confirm.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
From the latest screenshots:
- Story filter chips now fit on ONE row: the rail widened to 376px and the
  segmented track is a single nowrap row (dropped the redundant funnel icon).
- Presence shows the signed-in user as "Me" (email on hover), never the
  anonymous-looking "AN". New useCurrentUser() decodes the Keycloak JWT from
  host.getAccessToken(); other collaborators show their email initial + hover.
- Comments authored by "You" now read "You" (the mappers read `actorName`,
  not the absent `author`) — fixes "Someone commented".
- Only OPEN comments are marked up on the canvas; resolved/dismissed notes no
  longer leave a pin on the page (they stay in the Story rail).
- Comment pins are high-contrast and easy to spot: bright accent, white ring,
  cyan halo + drop shadow (no more dim author-coloured marker).
- Comment pins are scoped to the slide they belong to: HtmlCanvas reads the
  active `.slide` from the deck (same-origin) via a MutationObserver and reports
  it up; a new comment stamps its slide, and pins only show on that slide.
- Publish dialog's "open review items" now reflects LIVE comments (computed in
  handleShare and handed to the publish dialog) instead of a stale server count,
  so resolving everything correctly reads "Review is clear".

Verified: all comments in the test DB are resolved → the publish warning now
clears; reviewed by an adversarial pass (no blockers, chips fit with headroom).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Edit mode auto-saved a new version on every element blur, so you couldn't make
a second change without the version churn (and the save's mtime bump even
remounted the iframe, resetting the deck slide). Now you edit freely and persist
ONCE, on Save / exit:

- useIframeInlineEdit (HTML/deck): no commit on blur; edits accumulate. A `dirty`
  flag (tracked from real `input`) drives the button, and commit happens on exit
  (Save button, Edit toggle off, or close). commit() early-returns when not dirty,
  so navigating slides in edit mode no longer writes a spurious no-text version.
- EditableProse (prose/markdown): same model — accumulate locally, snapshot a
  session baseline, commit ONE version on exit; a still-focused block is flushed
  on exit so closing/Escape can't drop it. Optimistic read-only update avoids a
  pre-edit flash during the save round-trip.
- The floating button now reads "✓ Save changes" when there are edits (else
  "Done"); added the same affordance to the prose canvas.
- handleDirectSave bumpReloads only for prose (HTML keeps its live DOM, so the
  slide is preserved); HtmlCanvas preview-mount no longer depends on `artifact`
  identity, so the post-save mtime bump can't remount the iframe.
- Closing the workspace mid-edit flushes a pending edit as one version
  (HtmlCanvas registers commit via a ref; prose flushes on unmount).

Reviewed adversarially; the two real findings (spurious slide-nav version,
focused-block-on-close) are fixed here.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Editing a deck repeatedly bricked it: nav showed ~50 dots for 12 slides and
navigation died. Root cause — useIframeInlineEdit serialized the whole LIVE DOM
on save. The pitch deck builds its nav dots at runtime (script appendChild into
an empty #dots), so serialize() baked those generated dots into the file; each
save→reload then appended another set, compounding to 36 baked + 12 script-built.

Fix: stop serializing the live DOM. The hook now snapshots each editable
element's innerHTML at engage and, on commit, emits a list of { find, replace }
fragments. saveArtifactContent applies them to the SOURCE bytes, so only the
user's edited text changes — runtime-generated DOM is never written back. A find
that no longer matches is skipped (fail-safe; never corrupts).

Also: saveArtifactContent now reads its find-content from the live working copy
(no versionId) instead of a stored version snapshot — accept_edit stages and
patches the on-disk folder, so the find must match DISK. This keeps future edits
building on the real file (and stops a diverged version snapshot from breaking saves).

(The already-corrupted pitch deck working copy was repaired separately by
emptying its #dots container so the script rebuilds exactly 12.)

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Adversarial review of the per-element source-patch path found two real bugs:

- $-sequence mangling: result.replace(find, e.replace) let String.replace
  interpret $&, $', $1… in the REPLACEMENT, so typing "$5 & up" or code with $'
  would corrupt the saved bytes. Use the function replacer `() => e.replace`,
  which inserts the text verbatim. (Find stays a literal string.)
- Silent edit loss: a find that no longer matches the source was skipped with no
  feedback, and since HTML doesn't reload, the unsaved edit looked saved until it
  later vanished. saveArtifactContent now reports `skipped`; handleDirectSave
  toasts ("Couldn't save N changes…") and bumpReloads to re-sync.

Also:
- Only apply an edit when its find is UNAMBIGUOUS (exactly one occurrence) —
  zero = normalization mismatch, >1 = identical sibling elements; skip rather
  than edit the wrong text (counted toward `skipped`).
- Guard truncated (>200 KB) previews: fail clearly instead of building a swap on
  a partial prefix.
- Remove the now-dead `serialize()` (we no longer serialize the live DOM) and
  drop it from the effect deps.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
commit() cleared the dirty flag and advanced its per-element baselines optimistically,
before the async save resolved. So when a save failed (e.g. the 500 from the slug
collision), the edit session was left un-saveable: Save became a no-op and new edits
diffed from the wrong baseline.

- commit() now advances baselines + clears dirty ONLY after the host confirms success
  (handleDirectSave returns { ok }). A failed save keeps the session dirty → re-savable.
- savingRef guards against the Save button + exit-cleanup both firing the same save.
- handleDirectSave returns { ok: true|false } (false on conflict / error).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…ad code

Driven by a parallel 6-agent review + adversarial verification.

THE SAVE BUG (inline edit saved nothing / no new version, stale "19m ago"):
Per-element edits matched by the browser-normalized innerHTML, which never
byte-matched the on-disk source (<br/>→<br>, &→&amp;, attr quoting), so every
edit was skipped → no version, no write. Rewrite: useIframeInlineEdit emits a
structural { locator, html }; saveArtifactContent re-parses the SOURCE with
DOMParser (no script execution → a deck's runtime nav-dots are never baked in),
resolves the locator, sets innerHTML, and re-serializes. Fixes the no-save AND
the dot-duplication corruption with one mechanism, in DOM space (no byte match).

STORY FEED:
- Interleave versions + comments CHRONOLOGICALLY (newest-first); live chat stays
  by the composer. Was concatenated by category (old version above new comment).
- Stop mapping activity events (they double-counted every version and were
  unlabelable) — versions come from versionsToEvents, comments from mapStoryEvents.
- One self-identity ("You"/"Me") across versions, comments, chat (was Me/ME/YO);
  presence reads actorEmail from notificationState.
- Use the server's real version_number; current row + compare "to" derive from one
  row so id and number always agree.

RELIABILITY / HONESTY:
- A chat Q&A no longer remounts the deck (slide reset) or claims "Anton updated" —
  gated on the version id actually advancing.
- AI "Keep" no longer fabricates a fake v-local id + lies "saved"; degrade only on
  404/405/501 (not 400/422 — those surface the real error).
- chat errors toast; "Fix with AI" only claims to send when a turn started; the
  composer disables while replying and keeps the draft if a send is rejected.
- Reset edit/comment/compare/view state when the artifact changes (was bleeding
  across artifacts). Fix the leaked `input` listener in teardown.
- Stop the mock-feed leak (empty rail showed fake "Maya approved v5" rows).

DEAD CODE / QUALITY (~1,200 lines):
- Delete the orphaned VersionScrubber + HistoryPanel + scrubberIndex (superseded by
  the Story rail) and 3 stale build-note .md files; drop unused rd-* CSS keyframes;
  fix stale 332px / "Direction A" / "bottom scrubber" comments and dead .md refs.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…-with-AI

Three hardening improvements to the comment ↔ location ↔ agent loop:

1. Click-to-locate comments. Clicking a comment PIN opens a popover showing the
   comment AT its spot (author, text, area, Resolve / Fix with AI). Clicking a
   comment in the Story jumps the deck to the slide it's anchored on and opens its
   pin (the selected pin gets a stronger halo). Replaces the old dead-end toast.

2. Informative version entries. AI versions now show the instruction that drove
   them (e.g. the user's request) instead of a bare "Generated update" — read from
   the version's stored prompt, with our injected "[Context — …]" prefix stripped.

3. Surgical Fix-with-AI. The comment's anchor (slide, area, and the text of the
   element it was dropped on — captured via the deck's elementFromPoint at creation)
   is now passed to Anton with an explicit "make a surgical, targeted edit to this
   specific part" instruction, so it stops making broad whole-artifact changes.

All cross-iframe access (elementFromPoint, deck goTo) is same-origin-guarded; build green.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
- onLocateComment: if a comment is anchored on another slide and the deck can't be
  navigated programmatically (no global goTo), tell the user which slide instead of
  leaving the click silently inert (popover would never appear). Drive the deck only
  when the target slide differs from the current one.
- CommentLayer: Esc with a pin popover open closes only the popover, not also exit
  comment mode (the popover Esc handler now wins).
- cleanPrompt: anchor the injected "[Context …]" strip on the blank-line separator
  so a `]` inside an artifact title can't truncate it and leak prefix text.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Bugs:
- Story "click to locate" rows could point at a pin that was never rendered
  (locatable used a looser anchor predicate than the pin). Unify both on one
  commentHasPin() so a row can never advertise a jump to a missing pin.
- Pin numbers were assigned across all open comments then slide-filtered,
  leaving non-contiguous numbers (a lone "Comment 5" on a slide). Number
  within each slide instead.
- ReviewBanner showed the OLDEST review (reviewItems[len-1]); comments are
  DESC, so show [0] (newest).
- useIframeInlineEdit only kept an edit re-savable on an explicit {ok:false};
  a falsy/shapeless save result silently dropped it. Require ok===true.

Orphan code removed (~960 lines net): ReviewerView, VerdictBar (locked
verdict-loop), IconRail (mounted null), EditableBlockDemo + DEMO_TOKENS, the
single-symbol barrels (storyRailIndex/editIndex/reviewIndex), the dead
viewingN/viewingVersionId version-preview path, Pin's unused `active` prop,
VersionGroup's dead comment-action props, ReviewBanner's approved/onView
branches, the .rd-hov CSS rule, and stale comments.

Tightening: extract toUtcDate (one bug-prone UTC regex, not two), SaveExitButton
(shared by prose + HTML canvases), reuse useInlineEdit's mockRewrite (was a
diverging copy), keyboard-accessible locatable Story rows.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
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.

1 participant