feat(artifacts): redesigned workspace — in-place editing, comments, version history#213
Open
ianu82 wants to merge 15 commits into
Open
feat(artifacts): redesigned workspace — in-place editing, comments, version history#213ianu82 wants to merge 15 commits into
ianu82 wants to merge 15 commits into
Conversation
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>, &→&, 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>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
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
How to review a ~15k-line diff
The line count is misleading. Here's where it actually lives:
components/artifact/ArtifactWorkspace.jsxcomponents/artifact/redesign/*views/ArtifactsView.jsx,views/ProjectsView.jsx,api.jsSuggested reading order inside
redesign/:ArtifactWorkspaceRedesign.jsx— the composition layer that wires everything; start here.StoryRail.jsx— the unified versions + comments + chat timeline.useIframeInlineEdit.js+saveArtifactContent.js— the in-place-edit → save-one-version mechanism (the trickiest part; see note below).CommentLayer.jsx,useArtifactChat.js,VersionDiff.jsx— comments, streaming chat, version diff.How it's kept safe to merge
localStorage['anton:artifact-workspace-direction-2'] = 'false'.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:webis 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
main— it'll want a merge/rebase withmainbefore it goes in.Companion PR (backend): mindsdb/cowork-server#78
🤖 Generated with Claude Code