Skip to content

feat(documents): add document workspaces behind an experimental flag - #267

Open
johannesjo wants to merge 12 commits into
mainfrom
claude/document-workspaces-dny8yu
Open

feat(documents): add document workspaces behind an experimental flag#267
johannesjo wants to merge 12 commits into
mainfrom
claude/document-workspaces-dny8yu

Conversation

@johannesjo

Copy link
Copy Markdown
Owner

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

claude and others added 12 commits September 3, 2026 23:08
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
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