feat(documents): add document workspaces behind an experimental flag - #267
Open
johannesjo wants to merge 12 commits into
Open
feat(documents): add document workspaces behind an experimental flag#267johannesjo wants to merge 12 commits into
johannesjo wants to merge 12 commits into
Conversation
A new project kind for exploring problems in prose: a Git repository plus one Markdown document. Select a passage, send the same instruction to one or more agents, compare the proposals side by side, accept one as a single readable commit. Backend (electron/ipc/documents.ts): - Dispatch commits pending manual edits, snapshots the base, gives every candidate its own worktree and runs the official CLI headlessly (claude -p stream-json with read/edit tools only, codex exec --json, gemini -p json). Process exit means the proposal is ready. - The main session keeps a persistent worktree so its cwd (and the provider's prompt cache) never changes, resumes by session id, and is handed the diff of the canonical document since it last saw it. - Out-of-scope files are reverted and listed; hunks outside the selected passage are counted. The structured rationale the prompt asks for becomes the proposal commit message with Parallel-* trailers. - Acceptance is one squashed integration commit carrying the run record; a base that moved is merged three-way or marked stale. Rejection commits the record alone so history stays readable. - Document watcher, history with parsed trailers, version and diff at a commit, revert. Renderer (src/documents, src/store/documents.ts): - Full-window workspace with Document, Compare and History tabs. - Block-level rendering with source line mapping (src/lib/markdown-blocks), selection by text, block or heading section, and an in-place composer with agent chips and candidate counts. - Compare view: rationale first, rendered base and candidates, changed blocks marked and navigable, model-blind by default, source diff toggle, per-candidate notes, accept or reject. - Document project creation from the sidebar, flag in Settings. Tests cover the prompt and rationale parsing, CLI launch and output parsers, scope checks, block mapping and diffing, and the full lifecycle against a real temporary repository with a fake agent. An env-gated test drives the real claude CLI through a proposal, acceptance and a resumed follow-up. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01UP9JjPPAYEBBNe1VbGnSzU
Backend: - Refuse a new main-session run while the main worktree is still in use, and fail a candidate whose worktree left its branch before commit. - Validate run records on load: worktree paths must sit under .worktrees, branches must be parallel-doc/*, hashes must be hex. A cloned record can no longer name an arbitrary directory for cleanup. - Judge scope against HEAD: unstage before reverting strays, commit with --only, and verify the proposal commit touches the document alone. - Validate sessionLastSha as a hash and forbid a leading dash in session ids so neither can reach git or the CLI as a flag. - Write candidate results under the project lock by re-reading the record, so a note or rejection that landed meanwhile is not overwritten; reject waits for killed processes to settle before removing worktrees. - Ignore app-seeded .claude files when listing out-of-scope files. - Drop OpenCode and Copilot from headless support until their shell can be restricted. - Commit only tracked changes as "Manual edits"; untracked files stay out. - Spawn candidates detached and kill the process group, escalating to SIGKILL; decode stdout as a stream so multi-byte characters survive. - Delete the main worktree's previous branch once its run is settled. - Revert keeps run records and adds a Parallel-Revert trailer. - Parse trailers only from the final paragraph and flatten rationale entries so agent text cannot re-attribute a commit. - Reject .git/ as a document path and unsupported agents before any worktree is created. Renderer: - Run events carry the project root; runs and main sessions are recorded for the project they belong to even after its workspace was closed. - The workspace overlay sits below dialogs so the project editor and settings render on top of it. - Compare view is keyed by run id so notes and rejections no longer remount it; the composer keeps its draft when the selection changes. - Text inside one block stays a native selection so copying works; multi-block drags, block clicks and section buttons scope a run. - Candidate logs are keyed per run, relative ages tick, the base column marks blocks a candidate rewrote or removed, stale runs cannot be accepted, diffs show a loading state, Escape clears the composer before closing the workspace. - Document projects stay out of the new-task project picker. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01UP9JjPPAYEBBNe1VbGnSzU
Bubbles beside the document that never modify it. The composer gains Note and Ask modes next to Task; a note is saved under the passage, a question is answered by a headless read-only agent run in the checkout (claude with Read/Glob/Grep only, codex --sandbox read-only, gemini default approval). Question and answer stay visible together. - Anchors record path, base commit, lines, exact quote, neighbouring text and nearest heading in .parallel/annotations.json (version 1). The renderer relocates each bubble by quote on the current version, disambiguates duplicates by neighbours and heading, and shows the rest as detached rather than attaching them to the wrong passage. - Resolve (r) collapses a bubble to one line; Delete/Backspace removes it with an Undo in the toolbar instead of a confirmation. Make task reopens the composer on the passage with the bubble's text and answer as the instruction and links the bubble to the run. - Annotations are committed alongside content and metadata commits, never on their own, so they stay out of the document history view. - Backend validates every stored entry, keeps pending answers across edits, kills answer processes as a group, and reloads annotations in the renderer when HEAD moves. Tests cover validation, the file round trip, malformed entries, a fake read-only agent, and anchor relocation; the env-gated real-CLI test now also asks a question and checks that nothing in the checkout changed. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01UP9JjPPAYEBBNe1VbGnSzU
…etup Move the feature out of electron/ipc, electron/shared, src/lib and src/store into electron/documents and src/documents, and let it prepare its own project: creating one now runs whatever is missing (git init, the starter document, the first commit) instead of asking the user to do it first. Also from review: app-written commits take an explicit path list via `commit --only`, so nothing the user staged rides along; a candidate that settles while dispatch is still spawning is no longer resurrected as running; Gemini answers annotation questions in `plan`, its read-only mode. Claude-Session: https://claude.ai/code/session_01HG5Ape4rpESujEixxzhwgL
…folder The workspace's 19 handlers were inline in the app-wide registrar and its 20 IPC shapes sat in two shared type barrels, so the feature still reached into four files it did not own. They now live in electron/documents/register.ts and electron/documents/types.ts, with src/documents/types.ts mirroring the shapes for the renderer. register.ts sheds 150 lines, shared-types.ts 204, and main.ts calls one stopAllDocumentWork() instead of three shutdown functions. validatePath joins the other argument validators in ipc/validate.ts so both registrars share it, and the `kind !== 'document'` filter that had been copied into three call sites becomes codeProjects(). Claude-Session: https://claude.ai/code/session_01HG5Ape4rpESujEixxzhwgL
… one Picking a document project still meant creating its folder outside the app first. The Folder field is now an editable path: browse to a parent, type a name onto the end, and that folder is the project. Setup creates it, and inspection treats a path that does not exist yet as empty rather than an error, so the dialog keeps describing what it will do while you type. The path is inspected 250ms after typing stops rather than per keystroke, an enclosing repository is reported inline instead of only on confirm, and the Settings description no longer claims a Git repository is a prerequisite. Claude-Session: https://claude.ai/code/session_01HG5Ape4rpESujEixxzhwgL
…e modal Enabling document workspaces put a native message box in front of every project add, code ones included. It was titled "Confirm", used a dialog system the app does not control, and its Escape cancelled the whole add rather than the question. The + button now opens an in-app menu with the two kinds; with the flag off it goes straight to the folder picker as it always did. Document rows carry the document glyph instead of the colour dot, so a row that opens a full-window workspace no longer looks exactly like one that opens project settings. Fixes found while reviewing the entry points: - Enter on a focused project row always opened Edit Project, so keyboard users got branch prefixes and verify commands for a document project. Both the click and key paths now go through one openProjectRow. - The New Task button and the empty-state layout counted every project, so a user whose only project was a document project was offered a task flow with no project to run it in. - openDocumentWorkspace ignored documentWorkspacesEnabled, so turning the experiment off left the rows live. - EditProjectDialog gated branch and worktree settings on isGitRepo, which a document project sets true; it now gates on whether tasks run there. - The dialog's "that folder is inside the repository at X" dead end is a button that switches to X, instead of prose telling the user to start over. Claude-Session: https://claude.ai/code/session_01HG5Ape4rpESujEixxzhwgL
Git exports GIT_DIR and GIT_INDEX_FILE into every hook environment. `npm test` from .husky/pre-push handed them to each vitest worker, so a `git` a test spawned inherited them and acted on the developer's checkout rather than its own temp folder: `git init` re-initialised the real repository, fixture commits landed on the branch being pushed, and the index was clobbered — leaving the working tree reading as entirely untracked. vitest.setup.ts scrubs the repo-pointing GIT_* vars for both suites, so the suite is hermetic under any git-invoked context, not just this one hook. electron/test-env.test.ts guards the invariant; both assertions fail without the scrub. It checks --absolute-git-dir rather than --show-toplevel, since an inherited GIT_DIR still leaves the work tree at cwd. Claude-Session: https://claude.ai/code/session_01HG5Ape4rpESujEixxzhwgL
.docws-overlay is fixed at inset:0, so the workspace paints over the window
titlebar. main.ts injects `[data-tauri-drag-region] button { no-drag }`, but
that only reaches descendants of the drag region, and the overlay is a
sibling of the titlebar rather than a child. Chromium unions drag rects and
subtracts no-drag rects irrespective of DOM nesting, so the top band stayed
draggable and swallowed clicks landing in it — the tabs and the Project and
Close buttons sit at roughly y=10-38, straight through the 34px band on
Linux and the 32px one on macOS.
Only the controls are carved out, not the whole header: while the workspace
is open it covers the real titlebar, so the header is the only handle left
for moving the window.
Matches the existing precedent for the focus-mode pills at styles.css:2096.
Claude-Session: https://claude.ai/code/session_01HG5Ape4rpESujEixxzhwgL
Add a document editor shortcut and make the full header background a predictable window drag surface while keeping controls interactive.
A document that is a whole HTML page cannot survive the markdown path. marked splits HTML blocks at blank lines per CommonMark, so a page is chopped into fragments; indented markup trips the four-space code rule; and DOMPurify strips <style>, <head> and the doctype by design, which leaves the first block empty and spills the page's CSS out as a paragraph. The result read as "HTML rendering is broken" when the renderer was in fact working exactly as specified on input that was never markdown. Detect a real document preamble — a doctype or <html>, past any comments — and render those in a fully sandboxed iframe, so the page appears with its own stylesheet. sandbox="" allows CSS but no scripts, forms or same-origin access to the app. A Preview/Blocks toggle keeps the block list one click away, since selection and agent scoping are built on it; the block view stays mounted behind the preview so blocks, annotations and scroll position survive the switch. Markdown containing HTML fragments is untouched and keeps the block view. Claude-Session: https://claude.ai/code/session_01HG5Ape4rpESujEixxzhwgL
- Fold the branch/head status strip into header chips; errors become a banner under the header - Render the run composer in the document flow under the selected block (or above the document for a whole-document task) as one instance that moves between passages, so the instruction and agent picks survive a change of selection and the prose after it is never covered - Render annotation bubbles after their block rather than inside it, so clicking a bubble no longer toggles the block selection - Toolbar: singular "Selected line N"; the resolved toggle appears only once something is resolved - Rail: mark the accepted candidate and dim the ones not chosen or rejected - Compare: open every column on its first change or the scoped passage, cap the rationale height, and give the empty state a title, a hint and a way back to the document - Project dialog: hide worktree, verify, coverage and bookmark settings for document projects; new-project plan reads as a sentence - History: diff the root commit against the empty tree instead of showing "No source changes" - Headless parser: report the CLI's `errors`/`subtype` detail instead of a generic "agent reported an error" Claude-Session: https://claude.ai/code/session_01HtLDFZmcK6ASGvVy2YX5rr
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.
A new project kind for exploring problems in prose: a Git repository
plus one Markdown document. Select a passage, send the same instruction
to one or more agents, compare the proposals side by side, accept one as
a single readable commit.
Backend (electron/ipc/documents.ts):
candidate its own worktree and runs the official CLI headlessly
(claude -p stream-json with read/edit tools only, codex exec --json,
gemini -p json). Process exit means the proposal is ready.
provider's prompt cache) never changes, resumes by session id, and is
handed the diff of the canonical document since it last saw it.
passage are counted. The structured rationale the prompt asks for
becomes the proposal commit message with Parallel-* trailers.
a base that moved is merged three-way or marked stale. Rejection
commits the record alone so history stays readable.
commit, revert.
Renderer (src/documents, src/store/documents.ts):
selection by text, block or heading section, and an in-place composer
with agent chips and candidate counts.
blocks marked and navigable, model-blind by default, source diff
toggle, per-candidate notes, accept or reject.
Tests cover the prompt and rationale parsing, CLI launch and output
parsers, scope checks, block mapping and diffing, and the full lifecycle
against a real temporary repository with a fake agent. An env-gated test
drives the real claude CLI through a proposal, acceptance and a resumed
follow-up.
Co-Authored-By: Claude Fable 5.1 noreply@anthropic.com
Claude-Session: https://claude.ai/code/session_01UP9JjPPAYEBBNe1VbGnSzU