From c17a978773373a37986b9c91206c621709ff1440 Mon Sep 17 00:00:00 2001 From: Test Date: Mon, 7 Sep 2026 23:29:20 +0200 Subject: [PATCH 1/5] feat(documents): add document workspaces MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit An experimental project type for exploring problems in prose — architecture notes, specs, ADRs, research. Several agents get the same passage and the same base version; you read the alternatives and decide what enters the document. Behind the Document workspaces switch in Settings → Experimental. - Markdown documents and whole HTML pages, the latter rendered with their own stylesheet and their innermost elements as the blocks, matching what a browser shows: a page's custom properties, `rem` basis, reset and `position` survive, and page CSS cannot reach the app's own controls. - A composer that is always there: it rests at the foot of the column and acts on the whole document until you pick a passage by text, block or heading, then moves under the passage and follows it. - A task either goes to the long-running agent session, which edits the checkout as you watch, or runs as headless candidates in their own worktrees, with the agents, candidate counts, model and reasoning level chosen per project and remembered. - Compare proposals against the base in a modal, accept one or reject all, or take a candidate's changes one at a time and preview the result before it lands. The columns are resizable and remember their widths. - Refine a candidate instead of accepting it: a fresh isolated run seeded with that proposal's content, reviewed whole, leaving the original available. - Edit a block's Markdown or HTML source in place from its hover pencil or the toolbar; the rest of the file, and its line endings, are preserved, and a save refuses to overwrite a document that changed meanwhile. - Notes and agent-answered questions hang off a passage as margin markers, reachable with the mouse and marked while an answer is still coming. - History is git log for the document with the Parallel-* trailers parsed, showing diffs, rendering older versions and reverting entries. - A right panel with the agent terminal, the runs and the project's files; relative links inside a document open the file they point at. Hardened against a multi-agent review: the watcher compares snapshots rather than a proxy with itself, a block that parted from its file reports so in the dialog instead of tripping the window's error boundary, page stylesheets are brace-balanced before scoping, markup the app did not write goes through one sanitize policy, documents are not read through symlinks, proposals on an uncommitted document are refused, and a candidate finishing early no longer strands its peers as running. Also corrects `EMPTY_TREE` in electron/ipc/git.ts, which was not the empty tree, so the pre-existing root-commit diff paths never matched. Claude-Session: https://claude.ai/code/session_01Gso9dUvEZF5orpAwWhHACi Claude-Session: https://claude.ai/code/session_01EQcZpLJ71Nbf7XsZPJeYeK Claude-Session: https://claude.ai/code/session_01NZFMpgnLkK1nLq3AKf63nd --- .dependency-cruiser.cjs | 2 + .gitignore | 1 + docs/document-workspaces.md | 314 +++ electron/documents/agents.test.ts | 244 ++ electron/documents/agents.ts | 331 +++ electron/documents/annotations.test.ts | 183 ++ electron/documents/annotations.ts | 400 ++++ electron/documents/editing.test.ts | 100 + electron/documents/files.test.ts | 39 + electron/documents/files.ts | 27 + electron/documents/git.ts | 24 + electron/documents/logs.ts | 62 + electron/documents/prompt.test.ts | 111 + electron/documents/prompt.ts | 145 ++ .../documents/refinement.integration.test.ts | 86 + electron/documents/register.ts | 193 ++ electron/documents/runs.integration.test.ts | 577 +++++ .../runs.real-claude.integration.test.ts | 211 ++ electron/documents/runs.test.ts | 138 ++ electron/documents/runs.ts | 1550 +++++++++++++ electron/documents/setup.test.ts | 205 ++ electron/documents/setup.ts | 180 ++ electron/documents/shared.ts | 58 + electron/documents/types.ts | 219 ++ electron/ipc/channel-manifest.json | 27 +- electron/ipc/git.ts | 2 +- electron/ipc/register.ts | 11 +- electron/ipc/validate.ts | 8 + electron/main.ts | 10 +- electron/preload.cjs | 25 + electron/test-env.test.ts | 38 + package-lock.json | 3 +- package.json | 1 + src/App.tsx | 17 + src/components/AddProjectMenu.tsx | 114 + src/components/EditProjectDialog.tsx | 352 +-- src/components/IconButton.tsx | 2 +- src/components/NewTaskDialog.tsx | 5 +- src/components/PlanViewerDialog.tsx | 17 +- src/components/ProjectSelect.tsx | 4 +- src/components/SettingsDialog.tsx | 9 + src/components/Sidebar.tsx | 88 +- src/components/TaskNotesBody.client.test.tsx | 91 + src/components/TaskNotesBody.tsx | 32 +- src/components/TilingLayout.tsx | 3 +- src/documents/AgentTerminal.tsx | 103 + src/documents/AnnotationBubble.tsx | 224 ++ .../AnnotationMarker.client.test.tsx | 116 + src/documents/AnnotationMarker.tsx | 108 + src/documents/BlockActions.client.test.tsx | 128 ++ src/documents/BlockActions.tsx | 111 + src/documents/BlockEditor.client.test.tsx | 92 + src/documents/BlockEditor.tsx | 112 + src/documents/CandidateOutputDialog.tsx | 101 + .../CandidateRefinement.client.test.tsx | 80 + src/documents/CandidateRefinement.tsx | 76 + src/documents/CompareDialog.tsx | 87 + src/documents/CompareView.client.test.tsx | 289 +++ src/documents/CompareView.tsx | 656 ++++++ src/documents/DocumentIcon.tsx | 19 + src/documents/DocumentViewer.client.test.tsx | 313 +++ src/documents/DocumentViewer.tsx | 298 +++ .../DocumentWorkspaceOverlay.client.test.tsx | 184 ++ src/documents/DocumentWorkspaceOverlay.tsx | 591 +++++ src/documents/FileTreePanel.tsx | 121 + src/documents/HistoryView.tsx | 219 ++ src/documents/ModelRows.tsx | 77 + .../NewDocumentProjectDialog.client.test.tsx | 65 + src/documents/NewDocumentProjectDialog.tsx | 324 +++ src/documents/PageBlocks.tsx | 128 ++ src/documents/RightPanel.tsx | 119 + .../RunComposer.annotate.client.test.tsx | 125 + src/documents/RunComposer.client.test.tsx | 130 ++ .../RunComposer.draft.client.test.tsx | 88 + src/documents/RunComposer.tsx | 529 +++++ src/documents/RunsRail.tsx | 173 ++ src/documents/SourceDiff.tsx | 43 + src/documents/agent-terminal.ts | 59 + src/documents/annotation-anchor.test.ts | 236 ++ src/documents/annotation-anchor.ts | 268 +++ src/documents/block-edit.test.ts | 30 + src/documents/block-edit.ts | 27 + src/documents/block-merge.test.ts | 263 +++ src/documents/block-merge.ts | 257 +++ src/documents/documents-styles.test.ts | 42 + src/documents/documents.css | 2017 +++++++++++++++++ src/documents/html-blocks.test.ts | 183 ++ src/documents/html-blocks.ts | 391 ++++ src/documents/html-document.test.ts | 29 + src/documents/html-document.ts | 11 + src/documents/interactive-prompt.test.ts | 59 + src/documents/interactive-prompt.ts | 39 + src/documents/links.test.ts | 50 + src/documents/links.ts | 73 + src/documents/markdown-blocks.test.ts | 129 ++ src/documents/markdown-blocks.ts | 230 ++ src/documents/path-tree.test.ts | 15 + src/documents/path-tree.ts | 41 + src/documents/render-document.test.ts | 24 + src/documents/render-document.ts | 19 + src/documents/store.ts | 837 +++++++ src/documents/types.ts | 2 + src/documents/use-blocks.ts | 48 + src/documents/workspace-ui.ts | 63 + src/lib/marked-shiki.ts | 10 +- src/lib/mermaid.client.test.tsx | 68 + src/lib/mermaid.ts | 33 + src/lib/sanitize.ts | 11 + src/store/autosave.ts | 1 + src/store/core.ts | 2 + src/store/persistence.ts | 3 + src/store/projects.ts | 42 + src/store/remoteTaskHandler.ts | 3 +- src/store/store.ts | 4 + src/store/tasks.ts | 3 + src/store/types.ts | 30 + src/store/ui.ts | 5 + vitest.client.config.ts | 1 + vitest.config.ts | 1 + vitest.setup.ts | 22 + 120 files changed, 17532 insertions(+), 237 deletions(-) create mode 100644 docs/document-workspaces.md create mode 100644 electron/documents/agents.test.ts create mode 100644 electron/documents/agents.ts create mode 100644 electron/documents/annotations.test.ts create mode 100644 electron/documents/annotations.ts create mode 100644 electron/documents/editing.test.ts create mode 100644 electron/documents/files.test.ts create mode 100644 electron/documents/files.ts create mode 100644 electron/documents/git.ts create mode 100644 electron/documents/logs.ts create mode 100644 electron/documents/prompt.test.ts create mode 100644 electron/documents/prompt.ts create mode 100644 electron/documents/refinement.integration.test.ts create mode 100644 electron/documents/register.ts create mode 100644 electron/documents/runs.integration.test.ts create mode 100644 electron/documents/runs.real-claude.integration.test.ts create mode 100644 electron/documents/runs.test.ts create mode 100644 electron/documents/runs.ts create mode 100644 electron/documents/setup.test.ts create mode 100644 electron/documents/setup.ts create mode 100644 electron/documents/shared.ts create mode 100644 electron/documents/types.ts create mode 100644 electron/test-env.test.ts create mode 100644 src/components/AddProjectMenu.tsx create mode 100644 src/components/TaskNotesBody.client.test.tsx create mode 100644 src/documents/AgentTerminal.tsx create mode 100644 src/documents/AnnotationBubble.tsx create mode 100644 src/documents/AnnotationMarker.client.test.tsx create mode 100644 src/documents/AnnotationMarker.tsx create mode 100644 src/documents/BlockActions.client.test.tsx create mode 100644 src/documents/BlockActions.tsx create mode 100644 src/documents/BlockEditor.client.test.tsx create mode 100644 src/documents/BlockEditor.tsx create mode 100644 src/documents/CandidateOutputDialog.tsx create mode 100644 src/documents/CandidateRefinement.client.test.tsx create mode 100644 src/documents/CandidateRefinement.tsx create mode 100644 src/documents/CompareDialog.tsx create mode 100644 src/documents/CompareView.client.test.tsx create mode 100644 src/documents/CompareView.tsx create mode 100644 src/documents/DocumentIcon.tsx create mode 100644 src/documents/DocumentViewer.client.test.tsx create mode 100644 src/documents/DocumentViewer.tsx create mode 100644 src/documents/DocumentWorkspaceOverlay.client.test.tsx create mode 100644 src/documents/DocumentWorkspaceOverlay.tsx create mode 100644 src/documents/FileTreePanel.tsx create mode 100644 src/documents/HistoryView.tsx create mode 100644 src/documents/ModelRows.tsx create mode 100644 src/documents/NewDocumentProjectDialog.client.test.tsx create mode 100644 src/documents/NewDocumentProjectDialog.tsx create mode 100644 src/documents/PageBlocks.tsx create mode 100644 src/documents/RightPanel.tsx create mode 100644 src/documents/RunComposer.annotate.client.test.tsx create mode 100644 src/documents/RunComposer.client.test.tsx create mode 100644 src/documents/RunComposer.draft.client.test.tsx create mode 100644 src/documents/RunComposer.tsx create mode 100644 src/documents/RunsRail.tsx create mode 100644 src/documents/SourceDiff.tsx create mode 100644 src/documents/agent-terminal.ts create mode 100644 src/documents/annotation-anchor.test.ts create mode 100644 src/documents/annotation-anchor.ts create mode 100644 src/documents/block-edit.test.ts create mode 100644 src/documents/block-edit.ts create mode 100644 src/documents/block-merge.test.ts create mode 100644 src/documents/block-merge.ts create mode 100644 src/documents/documents-styles.test.ts create mode 100644 src/documents/documents.css create mode 100644 src/documents/html-blocks.test.ts create mode 100644 src/documents/html-blocks.ts create mode 100644 src/documents/html-document.test.ts create mode 100644 src/documents/html-document.ts create mode 100644 src/documents/interactive-prompt.test.ts create mode 100644 src/documents/interactive-prompt.ts create mode 100644 src/documents/links.test.ts create mode 100644 src/documents/links.ts create mode 100644 src/documents/markdown-blocks.test.ts create mode 100644 src/documents/markdown-blocks.ts create mode 100644 src/documents/path-tree.test.ts create mode 100644 src/documents/path-tree.ts create mode 100644 src/documents/render-document.test.ts create mode 100644 src/documents/render-document.ts create mode 100644 src/documents/store.ts create mode 100644 src/documents/types.ts create mode 100644 src/documents/use-blocks.ts create mode 100644 src/documents/workspace-ui.ts create mode 100644 src/lib/mermaid.client.test.tsx create mode 100644 src/lib/mermaid.ts create mode 100644 src/lib/sanitize.ts create mode 100644 vitest.setup.ts diff --git a/.dependency-cruiser.cjs b/.dependency-cruiser.cjs index 35f5dd564..88fb383e6 100644 --- a/.dependency-cruiser.cjs +++ b/.dependency-cruiser.cjs @@ -16,12 +16,14 @@ module.exports = { // - electron/mcp/validation.ts — string-only branch/UUID validators; the renderer // shares them so it never offers a branch the IPC layer would reject // - electron/shared/ — renderer-safe constants/pure helpers (no Node/Electron imports) + // - electron/documents/shared.ts — agent ids/labels the document workspace shows pathNot: [ '^electron/ipc/channels\\.ts', '^electron/mcp/prompt-detect\\.ts', '^electron/mcp/validation\\.ts', '^electron/agent-hooks/status\\.ts', '^electron/shared/', + '^electron/documents/shared\\.ts', ], }, }, diff --git a/.gitignore b/.gitignore index 7d5ed2181..ff871a58d 100644 --- a/.gitignore +++ b/.gitignore @@ -15,6 +15,7 @@ update-test/ # Keep generated docs content ignored, but track the authored architecture overview. docs/* !docs/architecture-overview.html +!docs/document-workspaces.md # Sandbox bind-mount artifacts from user home (not project files). # Root-anchored so legitimate nested files with these names are still tracked. diff --git a/docs/document-workspaces.md b/docs/document-workspaces.md new file mode 100644 index 000000000..9b0203edf --- /dev/null +++ b/docs/document-workspaces.md @@ -0,0 +1,314 @@ +# Document Workspaces + +**A project type in Parallel Code for exploring problems in prose** + +Status: experimental, behind the _Document workspaces_ switch in Settings → Experimental. +Slices 1 to 4 of the plan below are built (the loop, comparison, annotations, HTML pages). + +Parallel Code already runs several agents against the same codebase and keeps the results +isolated, attributable, and comparable. Document Workspaces applies the same machine to +writing and thinking: architecture notes, specs, ADRs, research. Several agents get the +same passage and the same base version. You read the alternatives and decide what enters +the document. + +``` + ┌────────────── Claude Code ──────────────● accepted + │ \ + ────────● base ─┤ ●────── + │ / + └────────────── Codex ───────────────○ discarded +``` + +## What is built + +### Using it + +1. Enable _Document workspaces_ under Settings → Experimental. +2. Click **+** next to Projects and choose **Document project…**. Type or browse to a + folder and name the project; a new Markdown document is named after it. Start with a + spec, architecture decision, or design note. Existing Markdown and HTML documents can + still be opened from the folder list. Neither has to exist: type a new name onto + the end of the path and that folder is the project. A folder that already holds + documents lists them, to open one of those instead. The dialog says what it will do — + create the folder, `git init`, create the file, make the first commit — and does it on + confirm. +3. The workspace opens full-window with two tabs, **Document** and **History**; comparing + proposals happens in a modal over either. + A **Files** tab in the right panel lists every file of the project; click one to open + it, and click a relative link inside a document to follow it to the file (and heading) + it points at. Web links open in the browser. The history, the composer and the runs + follow the open document. +4. The composer is a popover over the prose, never inside it, and is always there: with + nothing picked it rests at the foot of the column, acts on the whole document, and fades + back while you read. Select text, click a block, or press **§** next to a heading to + narrow it to a passage: it moves right under the passage (above it when the foot is + close), at full strength and with the cursor in it, and follows the passage as you + scroll. It steps back once you leave it. Type an instruction and press Enter. Hovering a + block shows five icons in the gap just above it, clear of the prose so a click meant + for the passage cannot land on one, each named as you point at it: task, proposals, + note and ask pick the block and open the composer on that mode (its tabs carry the same + icons), and the pencil, set apart, opens the block's source for editing. The picked + block's icons step aside while the composer is up on it; the other blocks keep theirs, + so one click moves the composer to another block. The **×** in the composer's corner, + or Esc, lets go of the passage and returns it to the foot. + The composer's four tabs are the four things one does with a passage; **Task** and + **Proposals** are the same instruction sent two ways: + - **Task** types it into the long-running agent session on the right, which + works in the checkout as you watch. Its edits show up in the viewer as they land and + are committed as `Manual edits` before the next one-shot run. + - **Proposals** runs headless candidates in their own worktrees. Expand + **Agents and models** to choose agents and candidate counts. Each candidate gets a + **Model** field and, where the CLI has one, a **Reasoning** level, so one agent can run + its main session on one model and an + alternate on another; both are the CLI's defaults until you choose, and the choice is + remembered per project, agent and candidate. +5. The right-hand panel has three tabs and a draggable seam (double-click it to reset the + width; the width is remembered). **Agent** is the interactive session in the app's + terminal, with a status chip that says _needs you_ while it waits for an answer; its + process survives closing the workspace, and **Restart**, or picking another agent, + starts it afresh. **Runs** lists the one-shot runs as they finish; click any candidate to + read everything it printed. + Runs that wait for a decision also appear as a strip over the agent, so nothing sits + unnoticed behind another tab. **Review** (one candidate) or **Compare** (several), or the + _n to review_ button in the header, opens the compare view as a large modal over the + workspace, with **Fullscreen** to fill the window: base on the left, candidates to the + right, each starting with its rationale. Accept one, reject all, or choose **Refine this candidate** and describe + what should change. A candidate with more than one change carries a checkbox beside each + of them: every change is kept until you clear its box, the passages you dropped recede + in place so you can still read what you turned down, and the button then says + **Accept n of m changes** and takes only those. **Preview result** shows the complete + document with the selected changes applied and declined passages restored from the base. + **Back to changes** preserves your choices. Acceptance waits for the combination to be + verified and is disabled if it cannot be combined cleanly. Refinement uses the same agent and model in a fresh isolated run, + starts from that candidate's content, and leaves the original proposal available. + The revision appears in Runs and still needs acceptance. +6. **History** is `git log` for the document with the `Parallel-*` trailers parsed: + what changed, which agent, which instruction, which base. Show the diff or render the + older version. Revert any entry. +7. **Annotations.** The same composer offers **Note** and **Ask** beside **Task**. A note + attaches to the passage as a symbol in its margin; hover it, tab to it, or click it to + pin it open, and the bubble opens over the document. A question is a bubble an agent + answers into, running read-only in the checkout; question and answer stay visible + together. Resolve (`r`) collapses a bubble to one line, Delete removes it with an Undo + in the toolbar, and **Make task** reopens the composer on the passage with the bubble's + text (and answer) as the instruction; the bubble then collapses as `task`. + +8. **Edit a block.** Hover a block and click its pencil, or click the block and choose + **Edit block** in the toolbar. Edit its + Markdown or HTML source, then **Save block** to update the document directly. Surrounding + content and line endings are preserved. If the file changed meanwhile, saving refuses + to overwrite it and keeps your text available to copy. + +### How it works + +- **Editing happens in the block editor, your editor, or the interactive session.** The app + watches the open file and re-renders; an external change drops any active selection. The session + on the Agent tab is an ordinary terminal (`TerminalView`) running the chosen agent's + CLI in the checkout, one per project, keyed `doc-agent-`; a scoped instruction + is pasted into it as _Document, Scope, the passage verbatim, your words_. Because it + edits the checkout directly, its work is committed as `Manual edits` by the next + one-shot dispatch rather than as a proposal to compare. A restart, or a switch to another + agent, spawns fresh instead of attaching: the spawn replaces the live session in one + step, so the new terminal never inherits the exit of the one it replaced. +- **Every file of the project is one click away.** The file tree is `git ls-files` + (tracked and untracked, ignores respected) minus `.parallel/`, `.worktrees/` and `.git/`, + capped at 5,000 entries, refreshed on every new head. Opening a file swaps the watcher + and the snapshot; annotations are filtered to the open file's path, and runs against + other documents say which. Links resolve against the open document's folder, never + above the project root. +- **Output is kept.** Each candidate's readable log is appended to + `.parallel/logs//.log` as it streams (the rail shows the tail); the + folder is git-excluded through `.git/info/exclude`. The output dialog reads it back, + capped at the last 2 MB, and re-reads every second while the candidate runs. +- **Dispatch commits pending edits first** as a plain `Manual edits` commit so every run + has a real base. Only tracked files count as pending edits: untracked scratch files + and everything under `.parallel/` stay out of that commit. +- **Headless agents.** Each candidate runs the official CLI in print mode inside its own + worktree under `.worktrees/parallel-doc/`: `claude -p --output-format stream-json` + with tools limited to Read/Edit/Write/Glob/Grep, `codex exec --json --sandbox +workspace-write` (the sandbox blocks writes outside the worktree), + `gemini -p --output-format json --approval-mode auto_edit` (`plan`, Gemini's read-only + mode, for annotation questions). Process exit means the + proposal is ready. Cancelling kills the CLI's whole process group. OpenCode and Copilot + expose an unrestricted shell in print mode and are not offered until they can be + restricted. A model or reasoning level chosen in the composer goes to the CLI as + `claude --model … --effort …`, `codex --model … -c model_reasoning_effort=…` or + `gemini --model …`, is kept on the candidate in the run record, and lands in the + `Parallel-Model` and `Parallel-Effort` trailers of the proposal and integration commits. +- **The main session stays warm.** One agent owns the project's main session (choose it in + the composer). Its worktree is persistent (`.worktrees/parallel-doc-main`) so the working + directory, and with it the provider's prompt cache, never changes; each run resumes the + session by id (`claude --resume`, `codex exec resume`). After the canonical document + moves, the next prompt to that session carries the diff since it last saw the file. + Other agents, and extra candidates from the main agent, are one-shot alternates. While + the main session is working, a new run can only use alternates. +- **Scope is enforced, not trusted.** Files the agent touched or staged outside the + document are reverted before the proposal commit and listed on the candidate; the + proposal commit is verified to contain the document alone. Hunks inside the document + but outside the selected passage are counted and flagged. +- **Structured rationale.** Every prompt asks the agent to end with a JSON block: + summary, changes, assumptions, questions, warnings. It opens each candidate in the + compare view and becomes the commit message. +- **One proposal commit per candidate** on a `parallel-doc/-