Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
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
137 changes: 62 additions & 75 deletions .cursor/docs/design-system.md

Large diffs are not rendered by default.

8 changes: 0 additions & 8 deletions .cursor/rules/agent-rules-and-memory.mdc
Original file line number Diff line number Diff line change
Expand Up @@ -61,14 +61,6 @@ files you are editing, and the long-form docs in `.cursor/docs/`.
When guidance overlaps, `AGENTS.md` and the area `CLAUDE.md` win; `.mdc` files are authoring
reference.

## Local working notes

`Notes/` is gitignored. Untracked reports, briefs, and the hocuspocus backlog
live in `Notes/local-docs/`. Open that folder's `INDEX.md` when the task is
inject-content, Last left, occupancy, restore, REST gaps, scale, auth, composer,
extension launch, TOC rail, or chat sash. That index has a route-by-task table and a keyword list.
Search with `grep -rl <pattern> Notes/local-docs/`.

## Simplified English is mandatory

Every sentence an agent writes here is Simplified English. It binds chat replies, reports, code
Expand Down
14 changes: 5 additions & 9 deletions .github/workflows/backend-ci.yml
Original file line number Diff line number Diff line change
@@ -1,14 +1,11 @@
name: Backend CI

# Pull requests run the backend suites without services.
# Production build triggers also run E2E against Postgres and Redis.
# Reusable backend quality gates (no-infra unit/integration + real-infra E2E).
# Called only by the prod pipeline, gated on `has_back || app_deploy` — every
# deploy migrates the database and rolls the backend, so every deploy runs these.
# Stage and pull requests never invoke it.
on:
workflow_call:
inputs:
run_e2e:
description: Run the Postgres and Redis E2E suites
type: boolean
default: true
workflow_call: {}

permissions:
contents: read
Expand Down Expand Up @@ -42,7 +39,6 @@ jobs:
run: bun run test

backend-e2e:
if: inputs.run_e2e
name: 🧪 Backend E2E (real Postgres + Redis)
runs-on: ubuntu-latest
timeout-minutes: 20
Expand Down
6 changes: 1 addition & 5 deletions .github/workflows/prod.docs.plus.yml
Original file line number Diff line number Diff line change
Expand Up @@ -262,10 +262,8 @@ jobs:
# runs the Prisma migration and rolls all three backend services. Gating on
# `has_back` alone let a `(build): front` push deploy backend code no test
# had ever touched. `has_back` stays so `(build): back no-deploy` still runs.
if: github.event_name == 'pull_request' || needs.triage.outputs.has_back == 'true' || needs.triage.outputs.app_deploy == 'true'
if: needs.triage.outputs.has_back == 'true' || needs.triage.outputs.app_deploy == 'true'
uses: ./.github/workflows/backend-ci.yml
with:
run_e2e: ${{ github.event_name != 'pull_request' }}

extension-tests:
name: 🧪 Ext (${{ matrix.ext }})
Expand Down Expand Up @@ -388,8 +386,6 @@ jobs:
- name: 🏗️ Build Webapp
run: bun run --filter @docs.plus/webapp build:ci
env:
# This build checks compilation only; deployment builds use the host's environment.
NEXT_PUBLIC_RESTAPI_URL: http://localhost:4000
NEXT_PUBLIC_SUPABASE_URL: ${{ secrets.NEXT_PUBLIC_SUPABASE_URL || 'http://localhost:54321' }}
NEXT_PUBLIC_SUPABASE_ANON_KEY: ${{ secrets.NEXT_PUBLIC_SUPABASE_ANON_KEY || 'dummy-key' }}

Expand Down
2 changes: 0 additions & 2 deletions .github/workflows/stage.docs.plus.yml
Original file line number Diff line number Diff line change
Expand Up @@ -173,8 +173,6 @@ jobs:
- name: 🏗️ Build Webapp
run: bun run --filter @docs.plus/webapp build:ci
env:
# This build checks compilation only; deployment builds use the host's environment.
NEXT_PUBLIC_RESTAPI_URL: http://localhost:4000
NEXT_PUBLIC_SUPABASE_URL: ${{ secrets.NEXT_PUBLIC_SUPABASE_URL || 'http://localhost:54321' }}
NEXT_PUBLIC_SUPABASE_ANON_KEY: ${{ secrets.NEXT_PUBLIC_SUPABASE_ANON_KEY || 'dummy-key' }}

Expand Down
13 changes: 1 addition & 12 deletions AGENTS.md

Large diffs are not rendered by default.

70 changes: 0 additions & 70 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -12,17 +12,6 @@ This file is the product changelog. The webapp package keeps [`apps/webapp/CHANG

### Added

- Play chat audio in a themed player, with seek and 1×, 1.5×, or 2× speed. A
voice note sent from now on shows its waveform and length before you press
play. Starting one clip pauses the others.

- Show what a chat member is doing besides typing. A small smile on their
avatar means they are choosing an emoji, and a mic means they are recording
a voice note. The chip shows in the chat header and on the heading's row in
the table of contents. It bounces for about five seconds and then rests.
With reduced motion it stays still. A face shows one thing at a time, so
the typing bounce stops while a chip shows. Reactions do not show a chip.

- Show a Pad title rename in History and in workspace chat. After a signed-in
rename, chat shows who changed it and the old and new titles. History paints
the latest live notice above the editor and uses the snapshot username. Chat
Expand Down Expand Up @@ -56,13 +45,6 @@ This file is the product changelog. The webapp package keeps [`apps/webapp/CHANG

### Changed

- Send a voice note when you let go of the mic on a phone. It sends by itself
only when the composer holds nothing else; otherwise it waits for your Send.
A hold under one second sends nothing and shows how to record. While you
hold, a lock rises above the mic and Slide to cancel follows your finger.
Android phones buzz when the note starts, locks, or is ready to cancel. After
a lock, Stop opens a preview with Send and Discard.

- Check a sign-in email on rest-api. Next keeps only the webapp health probe.

- Move Report a problem out of the pad title. It now sits in Settings, after
Expand All @@ -81,58 +63,6 @@ This file is the product changelog. The webapp package keeps [`apps/webapp/CHANG

### Fixed

- Mark your first message in an empty chat as sent. It was saved at once, but
it kept the clock icon until you reloaded the page.

- Keep a chat message you send while scrolled far up. The jump to the newest
messages could drop it from the list. If that send then failed, its text was
lost.

- Keep your chat draft and files across sends, replies, edits, and comments. A
failed comment gives back its text, its mode, and its files. Ending a reply or
a comment no longer deletes a file you just sent.

- Let Escape close only what is on top. Closing the mention list, a link
popover, or the media gallery no longer also cancels a reply, an edit, or a
comment. Starting a voice note closes the emoji panel.

- Split a long chat message by the length the database stores. A piece no
longer fails or holds no text. An edit that is too long is refused before it
saves, and your text stays in the composer.

- Make slide up to lock, slide left to cancel, and the 5-minute limit work
while you hold the mic on a phone. A composer style let the browser read a
slide as a scroll, and that ended the hold. Cancel discards the note. Letting
go while the browser asks for the microphone turns the microphone off.

- Save a voice note recorded on an iPhone as `.m4a`. It was named `.webm`,
though it holds MP4 audio.

- Show a file at once when you add it while three others upload, and count it
toward the 10-file limit. It used to stay hidden until its upload started.

- Notify the right people when a chat message holds an @. A capital letter
after a username no longer mentions a shorter name. An email address in a
message no longer stops the alert to members on All notifications. Your own
message never mentions you.

- Bounce the avatar of a chat member who is typing. The bounce was defined but
never played, because another animation on the same face replaced it.

- Make the chat composer work with a keyboard, a screen reader, and touch. Each
button's name matches its tooltip, and each format button says when it is on,
on the phone grid too. The + menu, the mention list, and the emoji panel work
from the keyboard. Small controls on a phone are 44 px targets.

- Fix smaller chat composer faults. A file over 10 MB is refused when you add
it, unless it is a picture that can shrink. A file that cannot fit shows no
Retry. A failed chat load shows the error, not a skeleton that never ends. An
edit with nothing left shows the mic, not a Send that fails. Add link and Edit
link put the cursor in the address field. Opening a comment focuses the right
composer. Strikethrough survives a send, and the composer no longer offers a
heading or a divider that a send removes. The first comment on a heading with
no chat works for a user who joined late.

- Keep Pad title open on a document with no owner. First edit no longer stamps
the signed-in visitor as owner, so anyone can rename it. An owned document
still answers only to its owner.
Expand Down
4 changes: 1 addition & 3 deletions CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ Entry point for Claude Code working on **docs.plus**. Read [AGENTS.md](AGENTS.md
3. **Package-local `AGENTS.md`** when working inside a package (e.g. [extensions/extension-hyperlink/AGENTS.md](extensions/extension-hyperlink/AGENTS.md)). Read in addition to the above.
4. The relevant `.cursor/rules/*.mdc` for the file you are editing (see index below).

Durable memory is at `.agents/memory/` (gitignored symlink to the Claude Code store, shared with Cursor); `.agents/memory/MEMORY.md` is the index. **Cursor does not read `CLAUDE.md`** — it gets the same rules through [.cursor/rules/agent-rules-and-memory.mdc](.cursor/rules/agent-rules-and-memory.mdc). Keep that map current when a rule file moves. Local working notes live in `Notes/` (gitignored). Current reports sit in `Notes/local-docs/`. Start at that folder's `INDEX.md`. Open it for inject-content, Last left, occupancy, restore, REST gaps, scale, auth, composer, extension launch, TOC rail, or chat sash.
Durable memory is at `.agents/memory/` (gitignored symlink to the Claude Code store, shared with Cursor); `.agents/memory/MEMORY.md` is the index. **Cursor does not read `CLAUDE.md`** — it gets the same rules through [.cursor/rules/agent-rules-and-memory.mdc](.cursor/rules/agent-rules-and-memory.mdc). Keep that map current when a rule file moves.

If guidance overlaps, project policy in `AGENTS.md` and `.cursor/docs/` wins; `.mdc` files are reference material for authoring.

Expand All @@ -39,7 +39,6 @@ Each was decided, and in most cases built and reverted. If you want to change on

- **AGENTS.md structure.** Split into per-directory `CLAUDE.md` files: tried 2026-07-27, reverted; re-applied and kept 2026-08-03. See AGENTS.md §Filed by directory.
- **TOC channel-map rework.** Built, then reverted at maintainer request by commit `9c535100c` on 2026-07-07 (do not run that hash as a command — reverting the revert re-applies the rework). The TOC is deliberately on the older behaviour. Do not re-propose the data-level type ladder, chat-open accent bar, or scroll-spy wash.
- **Desktop TOC tick rail.** Session-only 32px rail in the pad row so docked chat can go full width. Persist last committed wide width only (`docsy:toc-width`, greater than 240). Stored 240 or less is missing and opens at 320. Do not persist 32 or 240. Do not export `TocTickRail` from `toc/index.ts`. Do not put the rail beside the wide TOC (that kills full-width chat). Do not inset rail height for the sash. Do not add `SideContinuum`. Do not hide the preview clone behind a skeleton. Do not drop media because it is taller than the card. Do not drop a whole next heading rank if that leaves a short centered stack. Do not always-center the spy tick. Do not lock a short tick stack to the window mid-line; center it in the live rail. Rail spy tick is `bg-primary`; wide TOC spy stays `menu-focus` / `base-300`. Preview is an L1 clone card, not the house Tooltip. See [apps/webapp/CLAUDE.md](apps/webapp/CLAUDE.md) §TOC And Heading Actions and `CONTEXT.md` §Pad outline.
- **Collab storage design.** Delta storage, Yjs V2 encoding, content-addressed rows, time partitioning, and blobs-to-object-storage were each measured against the real corpus and rejected, 2026-07-27. New numbers required. See [apps/hocuspocus.server/CLAUDE.md](apps/hocuspocus.server/CLAUDE.md).
- **Dark mode mechanism.** `color-scheme` + `light-dark()` + semantic tokens, settled 2026-07-09. Never reintroduce a theming `data-mode` attribute or a `dark:`-enumerating `@custom-variant` — both were built and deleted. The unrelated `data-mode` on chat message cards is live and correct.
- **TOC presence overhang.** Rendering presence beyond the TOC column edge was evaluated and rejected; the `overflow: visible` hack breaks column scrolling. See [apps/webapp/CLAUDE.md](apps/webapp/CLAUDE.md) §TOC And Heading Actions.
Expand All @@ -50,7 +49,6 @@ Each was decided, and in most cases built and reverted. If you want to change on
- **Report route storage.** The Report action posts to the published contact address in `LEGAL_CONTACT_EMAIL` and keeps nothing. That was the deliberate scope, taken 2026-09-06 with the launch-legal review. Build storage or triage tooling only when real report volume shows it is needed, and bring the count rather than a design. One canonical home, `apps/webapp/src/utils/reportContent.ts` — do not add a second report path. Document report is a Settings row for signed-in users on a pad (`/[...slugs]`). Hide it on Home. Do not restore a pad-title control. Chat Report is unchanged. Contract in [apps/webapp/CLAUDE.md](apps/webapp/CLAUDE.md) §Report Route.
- **Passkeys are off.** Removed 2026-09-07. No sign-in button, no Conditional UI, no Settings card, no `auth.experimental.passkey`. Local `[auth.passkey] enabled = false`. Sign-in is Google or an email link. Do not rebuild the client. A production dashboard toggle that is still on does not restore the UI.
- **Next product APIs.** Health stays on Next. Validate lives on the email router as `POST /api/email/validate`. Status does not become a Hono route. Confirm is deleted and stays deleted. Leave `apps/webapp/src/utils/supabase/api.ts`. Do not edit `src/proxy.ts`, Next health rewrites, Traefik, or Docker HEALTHCHECK. Names live in `CONTEXT.md` §Product HTTP.
- **History restore is signed-in only.** Visitors may read history. They never see Restore. Do not replace the hidden control with a sign-in toast on the same button. Server refusal is `unauthorized` / `read-only`. See webapp §Document Version History and `CONTEXT.md` §Document access.

## Cursor rules — `.cursor/rules/`

Expand Down
11 changes: 1 addition & 10 deletions CONTEXT.md
Original file line number Diff line number Diff line change
Expand Up @@ -12,12 +12,6 @@ Shared names for docs.plus domain concepts. Architecture reviews and deepenings
- **Subtitle** — a paragraph style, not a heading. It is not a TOC heading.
- **HeadingScale** — visual size by rank inside a section. Distinct from Block style. The same heading level can look larger or smaller depending on the section.
- **Heading slug trail** (`?h=`) — outline ancestry from the target heading up through each parent to Title. Not a join of every heading before the target. `id=` is the resolver.
- **Wide TOC** — the desktop outline column. Painted width ≥ 240. Wrap titles, nest, fold, Heading Chat Surface, unread, and presence live here.
_Avoid_: Tick rail, persist-rail
- **Tick rail** — session-only 32px heading minimap (`TocTickRail`) in the pad row. Desktop only. Hides folded descendants. Depth-fits to `min(84% of the viewport, live rail)` and fills leftover slots around the spy. Windows the 8px list. Keep-in-view spy follow. A short stack sits in the middle of the live rail and recenters with `--motion-panel` when chat opens or closes. Persist is last committed Wide TOC width only (`docsy:toc-width`, greater than 240). Refresh and workspace change restore Wide TOC. Not a second outline product.
_Avoid_: SideContinuum, Outline minimap, persist-rail
- **Painted width** — the TOC column width this frame. Locks to 32 in `rail` and `settle-to-rail`.
- **tocWidth** — last committed Wide TOC width only (>240). Never the rail or the 240 abort floor. `useTocResize` writes it on release, not on every pointer move.

## Documents list

Expand All @@ -42,8 +36,6 @@ Shared names for docs.plus domain concepts. Architecture reviews and deepenings
- **Open document** — a document with no `ownerId`. Anyone may retitle it, signed in or not. First edit and first persist do not stamp an owner. A signed-in `PUT` or `POST` create still does. Private/Read-only locks stay off until ownership handoff ships.
- **Live seal** — REST publish → Redis `doc:{id}:access` → WS broadcast/close → client `applyAccessStateless`.
- **Editing lock** — client cannot edit: content-fork error, `authorizedScope === 'readonly'`, or metadata Read-only for a non-owner (`selectDocumentEditingLocked`).
- **Restore** — a signed-in writer putting a stored version back into the live document through `history.revert`. History is readable without a session. Restore is not. The control is hidden when there is no profile and when the editing lock is on. The server refuses the same cases (`unauthorized`, `read-only`).
_Avoid_: client `setContent` as the write; showing Restore to a visitor

## Document changes

Expand All @@ -52,7 +44,7 @@ Shared names for docs.plus domain concepts. Architecture reviews and deepenings
- **Anchor** — the newest stored version at or before an instant. `since` and `until` each resolve to one, so a window is two Anchors, never two timestamps.
- **Change window** — the version rows between the two Anchors. It supplies the version count, the triggers, and the contributors, and it is read separately from the section compare.
- **Magnitude** — words added and removed for a changed Section, plus its block counts. Null when the edit moved formatting rather than words, while the status still says modified.
- **Change digest** — the email that consumes this route. It runs in the worker process, which imports the compute factory by deep path instead of calling the route. The body is a heading, then its passage. A heading link opens that place on the pad. The default is one mail per document. Admin Notifications can send one combined mail and set the HTML cap. The default cap is 90 KB.
- **Change digest** — the email that consumes this route. It runs in the worker process, which imports the compute factory by deep path instead of calling the route.
- **Last left** — the instant a person's last live session on one document closed. Stored as `workspace_members.last_connection_closed_at`. `public.mark_document_connection_closed` is the only writer, and the service-role worker is the only caller (`packages/supabase/scripts/10-8-func-workspace_members.sql:329-369`). A returning reader's Change window starts here.
_Avoid_: `left_at`, a different fact that ends the membership (`packages/supabase/scripts/03-1-workspace_members.sql:8`); `updated_at`, the arrival stamp `join_workspace` writes, which the roster renders as "Last seen" (`packages/supabase/scripts/10-8-func-workspace_members.sql:238`); Last opened, the owner-only pad-open stamp in §Documents list

Expand Down Expand Up @@ -93,7 +85,6 @@ Shared names for docs.plus domain concepts. Architecture reviews and deepenings
- **presenceSync** — broadcast of `{id,channelId}` rows only (`workspacePresenceSync.ts`). Never creates face-less stub Profiles; attaches `channelId` to existing rows or buffers until a full track/broadcast profile arrives. Buffers die with the subscription (`clearPresenceSyncBuffers` via `clearAllPresenceShareTimers` on resubscribe).
- **selectPresenceOthers** — filter self out of presence stacks (`PresentUsers`, `usePresentUsers`); pass `map.values()` or a channel list.
- Avatar stacks never render stub-only id/`channelId` rows.
- **Activity** — what a chat member does now, other than typing: `choosingEmoji` or `recordingVoice` (`PresenceActivity`). It lives in client-only `Profile.activity`. An **activity chip** shows it on the avatar, one activity per face. _Avoid_: status (the Postgres enum `user_status`), typing.

## Document swarm

Expand Down
Loading