Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
43 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
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/**'
- 'packages/**'
workflow_dispatch:
inputs:
Expand All @@ -21,6 +22,7 @@ on:
- marketing
- portal
- paste
- room

permissions:
id-token: write
Expand All @@ -33,6 +35,7 @@ jobs:
marketing: ${{ steps.changes.outputs.marketing }}
portal: ${{ steps.changes.outputs.portal }}
paste: ${{ steps.changes.outputs.paste }}
room: ${{ steps.changes.outputs.room }}
steps:
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2

Expand All @@ -55,13 +58,19 @@ 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
else
# For push events, check what changed
git fetch origin ${{ github.event.before }} --depth=1 2>/dev/null || true

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)

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

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

deploy-marketing:
Expand Down Expand Up @@ -171,3 +186,28 @@ jobs:
env:
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 }}
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

install-cmd-windows:
# End-to-end integration test for scripts/install.cmd on real cmd.exe.
Expand Down
14 changes: 14 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -50,3 +50,17 @@ opencode.json
plannotator-local
# Local research/reference docs (not for repo)
/reference/

# Cloudflare Wrangler local state (Miniflare SQLite, caches)
.wrangler/

# Room-service Vite build output (chunked editor bundle served by
# Cloudflare's [assets] binding; regenerated by `bun run build:shell`).
apps/room-service/public/

# Claude Code local scratch files (per-session locks, etc.). Intentionally
# ignored so they can't be committed accidentally.
.claude/scheduled_tasks.lock

# Internal design/spec docs — kept locally, not shipped in PRs.
specs/
61 changes: 56 additions & 5 deletions AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,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 — mounts AppRoot for /c/:roomId
│ │ ├── 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 for built shell
│ └── 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 All @@ -51,16 +61,33 @@ 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
│ │ ├── utils/ # parser.ts, sharing.ts, storage.ts, planSave.ts, agentSwitch.ts, planDiffEngine.ts, planAgentInstructions.ts
│ │ ├── hooks/ # useAnnotationHighlighter.ts, useSharing.ts, usePlanDiff.ts, useSidebar.ts, useLinkedDoc.ts, useAnnotationDraft.ts, useCodeAnnotationDraft.ts, useArchive.ts
│ │ │ ├── sidebar/ # SidebarContainer, SidebarTabs, VersionBrowser, ArchiveBrowser
│ │ │ └── collab/ # RoomStatusBadge, ParticipantAvatars, RoomHeaderControls, RoomMenu, RoomUnavailableScreen, JoinRoomGate, StartRoomModal, RemoteCursorLayer, ImageStripNotice
│ │ ├── 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, useCollabRoom.ts, useCollabRoomSession.ts, useAnnotationController.ts, useRoomMode.ts, usePresenceThrottle.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.tsx
│ │ ├── project.ts # Pure string helpers (sanitizeTag, extractRepoName, extractDirName)
│ │ └── collab/ # Live Rooms protocol, crypto, validators, client runtime, React hook
│ │ ├── types.ts # Protocol types + runtime validators (isRoomAnnotation, isRoomSnapshot, isPresenceState, ...)
│ │ ├── 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_ROOM_UNAVAILABLE, WS_CLOSE_REASON_ROOM_UNAVAILABLE
│ │ ├── canonical-json.ts # canonicalJson for admin command proof binding
│ │ ├── encoding.ts # base64url helpers
│ │ ├── strip-images.ts # toRoomAnnotation, stripRoomAnnotationImages (image stripping for room snapshots)
│ │ ├── redact-url.ts # redactRoomSecrets (scrub #key=/#admin= from telemetry/logs)
│ │ ├── validation.ts # isBase64Url32ByteString / isValidPermissionMode
│ │ ├── client.ts # Client barrel re-exports
│ │ └── 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
│ └── review-editor/ # Code review UI
│ ├── App.tsx # Main review app
│ ├── components/ # DiffViewer, FileTree, ReviewSidebar
Expand Down Expand Up @@ -193,6 +220,17 @@ During normal plan review, an Archive sidebar tab provides the same browsing via

### Plan Server (`packages/server/index.ts`)

Live Rooms V1 does NOT support approve/deny from the room origin.
Approvals always happen on the local editor origin (the tab that
started the hook). Room-side annotations flow back to the local
editor via the existing import paths (static share hash, paste short
URL, "Copy consolidated feedback" → paste).

Local external annotations (`/api/external-annotations` + SSE) remain
local to the localhost editor in the current room integration.
Forwarding those annotations into encrypted room ops is later Slice 6
work; it is not part of the room-origin approve/deny surface.

| Endpoint | Method | Purpose |
| --------------------- | ------ | ------------------------------------------ |
| `/api/plan` | GET | Returns `{ plan, origin, previousPlan, versionInfo }` (plan mode) or `{ plan, origin, mode: "archive", archivePlans }` (archive mode) |
Expand Down Expand Up @@ -278,6 +316,19 @@ 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 |
| --------------------- | ------ | ------------------------------------------ |
| `/health` | GET | Worker liveness probe |
| `/c/:roomId` | GET | Room SPA shell — serves the built editor bundle (hashed chunks under `/assets/`). Response carries `ROOM_CSP`, `Cache-Control: no-store` on the HTML, `Referrer-Policy: no-referrer`. `:roomId` is validated against `isRoomId()` before the asset fetch. |
| `/api/rooms` | POST | Create room. Body: `{ roomId, roomVerifier, adminVerifier, initialSnapshotCiphertext, expiresInDays? }`. Returns `201` on success; `409` on duplicate `roomId`. Response body is intentionally not consumed by `createRoom()`. |
| `/ws/:roomId` | GET | WebSocket upgrade into the room Durable Object. `roomId` is validated via `isRoomId()` before `idFromName()` to prevent arbitrary DO instantiation. |

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

## 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