Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
60 commits
Select commit Hold shift + click to select a range
c1241ee
feat(collab): add packages/shared/collab protocol contract (Slice 1)
backnotprop Apr 18, 2026
91d732d
feat(collab): add apps/room-service Worker + Durable Object skeleton …
backnotprop Apr 18, 2026
b673ec4
feat(collab): durable room engine — event sequencing, admin, lifecycl…
backnotprop Apr 18, 2026
3166f08
feat(collab): browser/direct-agent client runtime + React hook (Slice 4)
backnotprop Apr 19, 2026
f68c8cd
WIP: pre-consolidation snapshot (anchor for Live Rooms V1 cleanup)
backnotprop Apr 19, 2026
a6380a2
refactor(collab): move 5 collab hooks to packages/ui/hooks/collab/
backnotprop Apr 19, 2026
78481d7
refactor(editor): extract useStartLiveRoom from App.tsx (Phase 1)
backnotprop Apr 19, 2026
4e000a5
refactor(editor): move checkbox pending-state derivation next to useC…
backnotprop Apr 19, 2026
62e97e8
chore(collab): clean stale comments + dedup fake-presence palette (Ph…
backnotprop Apr 19, 2026
68ab47a
refactor(collab): consolidate admin error-code contract (Phase 4)
backnotprop Apr 19, 2026
2a0bca9
fix(collab): hoist ThemeProvider + align room dialogs with canonical …
backnotprop Apr 19, 2026
236be66
feat(collab-agent): package skeleton + dep graph verification (Phase 1)
backnotprop Apr 19, 2026
97006d8
feat(collab-agent): pure agentIdentity module + admin URL guard + hea…
backnotprop Apr 19, 2026
c4920ec
feat(collab-agent): join + read-plan + read-annotations + read-presen…
backnotprop Apr 19, 2026
0c11551
feat(collab): visual marker for agent cursors + avatars (Phase 4)
backnotprop Apr 19, 2026
a3fc8f0
feat(collab-agent): comment subcommand — block-level COMMENT posting …
backnotprop Apr 19, 2026
e21470f
feat(collab-agent): demo subcommand — walk headings with block-space …
backnotprop Apr 19, 2026
a2a2e10
docs(collab-agent): AGENT_INSTRUCTIONS.md + README.md (Phase 7)
backnotprop Apr 19, 2026
cb8b272
test(ui): selection-accuracy matrix + follow-up spec note (Phase 8)
backnotprop Apr 19, 2026
911046b
fix(collab-agent): demo confirms per-heading echoes + cursor x/y rand…
backnotprop Apr 19, 2026
c37055a
feat(collab): Room menu → Copy agent instructions (shell-safe payload)
backnotprop Apr 19, 2026
2827d35
chore: add wrangler to root devDeps
backnotprop Apr 19, 2026
aedde9e
feat(collab): agent instructions nudge default behavior (demo-first)
backnotprop Apr 19, 2026
c421b59
chore: stop tracking internal specs/ docs
backnotprop Apr 22, 2026
968e0cd
fix(collab): route participant.left through message queue
backnotprop Apr 22, 2026
69f242d
chore(room-service): remove fake-presence demo script
backnotprop Apr 22, 2026
ef88358
docs: drop process metadata from code comments
backnotprop Apr 22, 2026
daf0c48
refactor(collab): delete unused AdminControls component
backnotprop Apr 22, 2026
6a5a89c
feat(collab): remove lock/unlock admin commands
backnotprop Apr 22, 2026
9359cc3
docs: scrub lock/unlock references from comments
backnotprop Apr 22, 2026
6cb8023
docs(collab): complete lock/unlock residual sweep
backnotprop Apr 22, 2026
46c1641
docs(collab): third-pass lock/unlock comment cleanup
backnotprop Apr 22, 2026
1bc4988
refactor(collab): remove dormant readOnly prop
backnotprop Apr 22, 2026
7b83dbf
refactor(collab): remove lock-era dead code from admin path
backnotprop Apr 22, 2026
058b01b
feat(collab): 30-day auto-expiry via DO alarm, collapse terminal UX
backnotprop Apr 23, 2026
0190ad5
fix(collab): update straggler roomStatus references
backnotprop Apr 23, 2026
b909ca8
chore(collab): drop imports orphaned by the terminal-UX collapse
backnotprop Apr 23, 2026
ec00de8
chore(collab): sweep final terminal-state remnants + close purge TOCTOU
backnotprop Apr 23, 2026
094f6a4
chore(tests): trim duplicated + trivial test cases (-143 LOC)
backnotprop Apr 23, 2026
6828c4d
fix(collab): fresh joins to gone rooms route to RoomUnavailableScreen
backnotprop Apr 23, 2026
6d5f457
Merge origin/main into feat/collab
backnotprop Apr 23, 2026
b9e1c10
fix(collab-agent): preserve literal string "true" as a flag value
backnotprop Apr 23, 2026
c6ebe7e
ci(room-service): add CD pipeline + custom domain route
backnotprop Apr 24, 2026
fff99cf
chore: remove docs/adversarial_rubric.md (moved to .agents/skills/rel…
backnotprop Apr 25, 2026
58158cd
chore: track adversarial rubric in release skill references
backnotprop Apr 25, 2026
7c010fe
feat(room-service): landing page — upload a document, create a room
backnotprop May 12, 2026
b8f15ee
feat(room-service): never-expiry option, cross-origin cookies, collab…
backnotprop May 12, 2026
27e2437
docs(marketing): add shared rooms architecture page with SVG diagram
backnotprop May 12, 2026
24262a8
Merge origin/main into feat/collab
backnotprop May 12, 2026
810a78c
chore: remove dev artifacts, annotate unused props
backnotprop May 12, 2026
2b5f982
docs: update privacy policy for rooms, fix landing page footer
backnotprop May 12, 2026
53d180b
Merge origin/main into feat/collab (latest)
backnotprop May 12, 2026
d80ef48
chore: restore collab docs in AGENTS.md, remove dead AppHeader import
backnotprop May 12, 2026
07b5253
refactor(editor): unify header into single AppHeader component
backnotprop May 12, 2026
f75d837
Merge origin/main into feat/collab
backnotprop May 13, 2026
7cc21ce
feat(collab): multi-document live rooms with compression
backnotprop May 14, 2026
a0a60e0
Merge remote-tracking branch 'origin/main' into feat/collab
backnotprop May 14, 2026
5f6bef8
refactor(ui): share file tree across room surfaces
backnotprop May 15, 2026
2a23525
feat(collab): redesign StartRoomModal with two-column folder layout
backnotprop Jun 4, 2026
5f0aee1
Merge remote-tracking branch 'origin/main' into feat/collab
backnotprop Jun 4, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
40 changes: 40 additions & 0 deletions .github/workflows/deploy.yml
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ on:
- 'apps/marketing/**'
- 'apps/portal/**'
- 'apps/paste-service/**'
- 'apps/room-service/**'
- 'apps/waitlist-service/**'
- 'packages/**'
workflow_dispatch:
Expand All @@ -22,6 +23,7 @@ on:
- marketing
- portal
- paste
- room
- waitlist

permissions:
Expand All @@ -34,6 +36,7 @@ jobs:
marketing: ${{ steps.changes.outputs.marketing }}
portal: ${{ steps.changes.outputs.portal }}
paste: ${{ steps.changes.outputs.paste }}
room: ${{ steps.changes.outputs.room }}
waitlist: ${{ steps.changes.outputs.waitlist }}
steps:
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
Expand All @@ -57,6 +60,11 @@ jobs:
else
echo "paste=false" >> $GITHUB_OUTPUT
fi
if [[ "${{ inputs.target }}" == "all" || "${{ inputs.target }}" == "room" ]]; then
echo "room=true" >> $GITHUB_OUTPUT
else
echo "room=false" >> $GITHUB_OUTPUT
fi
if [[ "${{ inputs.target }}" == "all" || "${{ inputs.target }}" == "waitlist" ]]; then
echo "waitlist=true" >> $GITHUB_OUTPUT
else
Expand All @@ -69,6 +77,7 @@ jobs:
MARKETING_CHANGED=$(git diff --name-only ${{ github.event.before }} ${{ github.sha }} 2>/dev/null | grep -E '^(apps/marketing/|packages/)' || true)
PORTAL_CHANGED=$(git diff --name-only ${{ github.event.before }} ${{ github.sha }} 2>/dev/null | grep -E '^(apps/portal/|packages/)' || true)
PASTE_CHANGED=$(git diff --name-only ${{ github.event.before }} ${{ github.sha }} 2>/dev/null | grep -E '^apps/paste-service/' || true)
ROOM_CHANGED=$(git diff --name-only ${{ github.event.before }} ${{ github.sha }} 2>/dev/null | grep -E '^(apps/room-service/|packages/shared/collab/|packages/editor/|packages/ui/)' || true)
WAITLIST_CHANGED=$(git diff --name-only ${{ github.event.before }} ${{ github.sha }} 2>/dev/null | grep -E '^apps/waitlist-service/' || true)

if [[ -n "$MARKETING_CHANGED" ]]; then
Expand All @@ -89,6 +98,12 @@ jobs:
echo "paste=false" >> $GITHUB_OUTPUT
fi

if [[ -n "$ROOM_CHANGED" ]]; then
echo "room=true" >> $GITHUB_OUTPUT
else
echo "room=false" >> $GITHUB_OUTPUT
fi

if [[ -n "$WAITLIST_CHANGED" ]]; then
echo "waitlist=true" >> $GITHUB_OUTPUT
else
Expand Down Expand Up @@ -194,6 +209,31 @@ jobs:
CLOUDFLARE_API_TOKEN: ${{ secrets.CLOUDFLARE_API_TOKEN }}
CLOUDFLARE_ACCOUNT_ID: ${{ secrets.CLOUDFLARE_ACCOUNT_ID }}

deploy-room:
needs: detect-changes
if: needs.detect-changes.outputs.room == 'true'
runs-on: ubuntu-latest
environment: production
steps:
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2

- uses: oven-sh/setup-bun@0c5077e51419868618aeaa5fe8019c62421857d6 # v2.2.0
with:
bun-version: latest

- name: Install dependencies
run: bun install

- name: Build browser shell
run: bun run --cwd apps/room-service build:shell

- name: Deploy to Cloudflare
working-directory: apps/room-service
run: npx wrangler deploy
env:
CLOUDFLARE_API_TOKEN: ${{ secrets.CLOUDFLARE_API_TOKEN }}
CLOUDFLARE_ACCOUNT_ID: ${{ secrets.CLOUDFLARE_ACCOUNT_ID }}

deploy-waitlist:
needs: detect-changes
if: needs.detect-changes.outputs.waitlist == 'true'
Expand Down
4 changes: 3 additions & 1 deletion .github/workflows/release.yml
Original file line number Diff line number Diff line change
Expand Up @@ -41,7 +41,9 @@ jobs:
run: bun run typecheck

- name: Run tests
run: bun test
# See .github/workflows/test.yml for why this is `bun run test`
# and not raw `bun test`.
run: bun run test

build:
needs: test
Expand Down
7 changes: 6 additions & 1 deletion .github/workflows/test.yml
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,12 @@ jobs:
run: bun run typecheck

- name: Run tests
run: bun test
# Use the root `test` script (splits non-UI + UI-cwd) so the
# packages/ui/bunfig.toml happy-dom preload is loaded. Raw
# `bun test` from the repo root doesn't pick up that package-
# scoped preload, so UI hook tests would hit "document is not
# defined".
run: bun run test

pi-extension-ai-runtime-windows:
# Exercises the Pi extension's Node/jiti server mirror on Windows with an
Expand Down
5 changes: 5 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -57,6 +57,11 @@ plannotator-local
/goals/
*.bun-build

.wrangler/
apps/room-service/public/
.claude/scheduled_tasks.lock
specs/

# Local meta notes (not upstream)
/meta/

Expand Down
66 changes: 61 additions & 5 deletions AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,16 @@ plannotator/
│ │ ├── index.html
│ │ ├── index.tsx
│ │ └── vite.config.ts
│ ├── room-service/ # Live collaboration rooms (Cloudflare Worker + Durable Object)
│ │ ├── core/ # Handler, DO class, validation, CORS, log, types, csp
│ │ ├── targets/cloudflare.ts # Worker entry + DO re-export
│ │ ├── entry.tsx # Browser shell entry — path switch: / → LandingPage, /c/:roomId → AppRoot
│ │ ├── index.html # Vite template; produces hashed chunks under /assets/
│ │ ├── vite.config.ts # Browser shell build (bun run build:shell)
│ │ ├── tsconfig.browser.json # DOM-lib tsconfig for the shell
│ │ ├── static/ # Root-level static assets copied into public/ by build:shell (favicon.svg)
│ │ ├── scripts/smoke.ts # Integration test against wrangler dev
│ │ └── wrangler.toml # SQLite-backed DO binding + ASSETS binding (run_worker_first, html_handling=none)
│ ├── vscode-extension/ # VS Code extension — opens plans in editor tabs
│ │ ├── bin/ # Router scripts (open-in-vscode, xdg-open)
│ │ ├── src/ # extension.ts, cookie-proxy.ts, ipc-server.ts, panel-manager.ts, editor-annotations.ts, vscode-theme.ts
Expand Down Expand Up @@ -68,24 +78,37 @@ plannotator/
│ │ ├── components/ # Viewer, Toolbar, Settings, etc.
│ │ │ ├── icons/ # Shared SVG icon components (themeIcons, etc.)
│ │ │ ├── plan-diff/ # PlanDiffBadge, PlanDiffViewer, clean/raw diff views
│ │ │ └── sidebar/ # SidebarContainer, SidebarTabs, VersionBrowser, ArchiveBrowser
│ │ │ ├── sidebar/ # SidebarContainer, SidebarTabs, VersionBrowser, ArchiveBrowser
│ │ │ └── collab/ # RoomStatusBadge, ParticipantAvatars, RoomHeaderControls, RoomMenu, RoomUnavailableScreen, JoinRoomGate, StartRoomModal, RemoteCursorLayer, ImageStripNotice, LandingPage, LandingPreview
│ │ ├── shortcuts/ # Keyboard shortcut registry (see Keyboard Shortcuts section below)
│ │ │ ├── core.ts # Engine: parser, formatter, dispatcher, validator
│ │ │ ├── runtime.ts # Engine: useShortcutScope, useDoubleTapShortcuts hooks
│ │ │ ├── index.ts # Barrel — re-exports engine + scopes from both subfolders
│ │ │ ├── plan-review/ # Scopes for plan-editor surfaces (annotationToolbar, annotationPanel, commentPopover, imageAnnotator, inputMethod, viewer)
│ │ │ └── code-review/ # Scopes for review-editor surfaces (ai, allFilesDiff, annotationToolbar, fileTree, prComments, suggestionModal, tourDialog)
│ │ ├── shortcuts.test.ts # Registry unit tests (parser, dispatcher, validator)
│ │ ├── utils/ # parser.ts, sharing.ts, storage.ts, planSave.ts, agentSwitch.ts, planDiffEngine.ts, planAgentInstructions.ts
│ │ ├── utils/ # parser.ts, sharing.ts, storage.ts, planSave.ts, agentSwitch.ts, planDiffEngine.ts, planAgentInstructions.ts, adminSecretStorage.ts, blockTargeting.ts
│ │ ├── hooks/ # useAnnotationHighlighter.ts, useSharing.ts, usePlanDiff.ts, useSidebar.ts, useLinkedDoc.ts, useAnnotationDraft.ts, useCodeAnnotationDraft.ts, useArchive.ts
│ │ │ └── collab/ # useCollabRoom.ts, useCollabRoomSession.ts, useLandingCreateRoom.ts, usePresenceThrottle.ts, useRoomMode.ts, useRoomAdminActions.ts, useStartLiveRoom.ts
│ │ └── types.ts
│ ├── ai/ # Provider-agnostic AI backbone (providers, sessions, endpoints)
│ ├── shared/ # Shared types, utilities, and cross-runtime logic
│ │ ├── storage.ts # Plan saving, version history, archive listing (node:fs only)
│ │ ├── draft.ts # Annotation draft persistence (node:fs only)
│ │ └── project.ts # Pure string helpers (sanitizeTag, extractRepoName, extractDirName)
│ ├── editor/ # Plan review app
│ │ ├── App.tsx # Main plan review app
│ │ ├── project.ts # Pure string helpers (sanitizeTag, extractRepoName, extractDirName)
│ │ └── collab/ # Live Rooms protocol, crypto, validators, client runtime, React hook
│ │ ├── types.ts # Protocol types + runtime validators
│ │ ├── crypto.ts # HKDF key derivation, HMAC proofs, AES-GCM payload encrypt/decrypt
│ │ ├── ids.ts # roomId/secret/opId/clientId generators
│ │ ├── url.ts # parseRoomUrl / buildRoomJoinUrl / buildAdminRoomUrl (client-only)
│ │ ├── constants.ts # ROOM_SECRET_LENGTH_BYTES, ADMIN_SECRET_LENGTH_BYTES, WS_CLOSE_*
│ │ ├── strip-images.ts # toRoomAnnotation, stripRoomAnnotationImages
│ │ ├── redact-url.ts # redactRoomSecrets (scrub #key=/#admin= from telemetry/logs)
│ │ └── client-runtime/ # CollabRoomClient class, createRoom, joinRoom, apply-event reducer
│ ├── editor/ # Plan review app (App.tsx) + room-mode shell
│ │ ├── App.tsx # Plan review editor (local + room-mode prop)
│ │ ├── AppRoot.tsx # Mode fork (local | room | invalid-room); package default export
│ │ └── RoomApp.tsx # Room-mode shell — identity gate, session, overlays, delete/expired fallbacks
│ │ └── shortcuts.ts # planReviewSurface + annotateSurface — composes plan-review scopes into per-surface registries
│ └── review-editor/ # Code review UI
│ ├── App.tsx # Main review app
Expand Down Expand Up @@ -353,6 +376,39 @@ All servers use random ports locally or fixed port (`19432`) in remote mode.

Runs as a separate service on port `19433` (self-hosted) or as a Cloudflare Worker (hosted).

### Room Service (`apps/room-service/`)

Live-collaboration rooms for encrypted multi-user annotation. Zero-knowledge: the Worker + Durable Object stores and relays ciphertext only. Clients hold the room secret in the URL fragment and derive `authKey`/`eventKey`/`presenceKey`/`adminKey` locally.

| Endpoint | Method | Purpose |
| --------------------- | ------ | ------------------------------------------ |
| `/` | GET | Landing page for room creation from uploaded document. Serves the same `index.html` shell; `entry.tsx` path switch renders `LandingPage` (lazy-loaded). |
| `/health` | GET | Worker liveness probe |
| `/c/:roomId` | GET | Room SPA shell — serves the built editor bundle. Response carries CSP, `Cache-Control: no-store`, `Referrer-Policy: no-referrer`. |
| `/api/rooms` | POST | Create room. Body: `{ roomId, roomVerifier, adminVerifier, initialSnapshotCiphertext, expiresInDays? }`. Returns `201` on success; `409` on duplicate. |
| `/api/fetch-markdown` | POST | URL-to-markdown proxy. Body: `{ url }`. Returns `{ markdown, source }`. |
| `/ws/:roomId` | GET | WebSocket upgrade into the room Durable Object. |

Protocol contract lives in `packages/shared/collab/`; the Worker/DO never imports client-only URL helpers.

#### Multi-Document Rooms

Rooms can carry multiple documents via `contentType: 'markdown-multi'`. The snapshot shape extends with:

- `docs: Record<string, string>` — relative path → content (markdown or raw HTML).
- `primaryDoc?: string` — path to auto-open on join (must be a key in `docs`).
- `htmlDocPaths?: string[]` — paths in `docs` whose content is raw HTML (rendered via HtmlViewer). Absent paths are markdown.

Each `RoomAnnotation` in a multi-doc room carries `docPath: string` matching a key in `docs`. The cross-field invariant is enforced in `isRoomSnapshot` (every annotation's `docPath` must resolve to a real doc) and at inbound-event time in `CollabRoomClient` (rejects `annotation.add` events with missing or unknown `docPath`). `docPath` is forbidden in annotation patches — annotations cannot be moved between documents.

`PresenceState` carries `activeDoc?: string` so remote cursors are filtered per-document and the sidebar file list shows per-doc presence avatars.

**Compression.** All room snapshots are deflate-compressed before encryption. The compressed plaintext is prefixed with `c1:` inside the encrypted envelope so `decryptSnapshot` knows to decompress. Legacy uncompressed rooms (no prefix) are supported permanently — never-expiring rooms created before compression can live indefinitely. Compression lives in `packages/shared/collab/crypto.ts` (`encryptSnapshot` / `decryptSnapshot`), reusing `compress` / `decompress` from `packages/shared/compress.ts`.

**Size budget.** The picker enforces a 5 MB plaintext hard cap (submit disabled above it). The server's ciphertext hard cap is 1.5 MB (`MAX_SNAPSHOT_CIPHERTEXT_LENGTH` in `apps/room-service/core/validation.ts`), grounded in the Cloudflare SQLite-backed Durable Object 2 MB max row size. Compression bridges the gap — typical markdown compresses 3–5× and fits comfortably. Content that compresses poorly can still fail with a clean 413; the picker surfaces file-level guidance.

**Server changes: none.** The Worker/DO only sees ciphertext. Multi-doc routing is entirely client-side.

## Plan Version History

Every plan is automatically saved to `~/.plannotator/history/{project}/{slug}/` on arrival, before the user sees the UI. Versions are numbered sequentially (`001.md`, `002.md`, etc.). The slug is derived from the plan's first `# Heading` + today's date via `generateSlug()`, scoped by project name (git repo or cwd). Same heading on the same day = same slug = same plan being iterated on. Identical resubmissions are deduplicated (no new file if content matches the latest version).
Expand Down
Loading
Loading