Skip to content

Product rethink: visibility over status, personal libraries, profiles, three surfaces#153

Merged
HamptonMakes merged 20 commits into
mainfrom
worktree-library-rethink
Jul 20, 2026
Merged

Product rethink: visibility over status, personal libraries, profiles, three surfaces#153
HamptonMakes merged 20 commits into
mainfrom
worktree-library-rethink

Conversation

@HamptonMakes

@HamptonMakes HamptonMakes commented Jul 17, 2026

Copy link
Copy Markdown
Collaborator

Implements the product rethink from the CoPlan Rethink PRD end to end, in four phases (one commit each, plus a migration commit and polish).

Note

This supersedes #146, which is now obsolete — the org-global folder design it built on was replaced by personal libraries in the PRD.

1. Status is dead

  • The 5-state status lifecycle is replaced by visibility (draft/published) + archived_at. Migration maps brainstorm→draft, abandoned→published+archived, everything else→published (with developing/live preserved as tags).
  • Plans are born published. Draft is API opt-in ("visibility": "draft" on create), not the starting state.
  • Drafts are unlisted, not locked. Anyone with the URL can read a draft (PlanPolicy#show? is open); discovery — lists, feeds, search, counts, shelves — goes through one predicate (PlanPolicy#listed? / Plan.visible_to), never inline checks.
  • Publishing is explicit, confirmed, and one-way; archive is the off-ramp. Legacy API status still accepted (mapped) and emitted (legacy_status) for the deprecation window; unknown legacy values now 422 with guidance.

2. Folders became personal libraries

  • CoPlan::Library with a polymorphic owner (users only in v1); every user always has one, materialized on first touch (Library.for, race-safe). Folders belong to their library; write access is the library's call (writable_by?).
  • Plan↔folder is now a first-class join, CoPlan::PlanPlacement (who placed it, when; unique per plan per library) — the same plan can sit on many people's shelves. Shelving someone else's published plan is a normal action.
  • Plans::Place is the single write path for shelving (web drag-and-drop, row menu, and API all use it).
  • Everything folder-flavored is viewer-relative: the API's folder_id/folder_path mean "where you shelved it", workspace rows show your shelf, and the sidebar tree is your library.
  • Libraries are browsable read-only (/libraries/:id), and plan pages grew a "Shelved in" folder-jump strip.

3. Profiles + directory adapter

  • /people/:username-or-id — identity, published plans, and the library shelves embedded. Only publicly listed work appears, even on your own profile.
  • New host hook config.directory_profile (People API adapter point): enriches name/avatar/title/team and adds a directory link-out; failures degrade to the local profile. Author names across the app link to profiles.

4. Three surfaces

  • Home (/home, the new signed-in root): activity feed over published work, rolled up per plan per day (CoPlan::HomeFeed), plus search that now finds people as well as plans.
  • Workspace (/plans): your operating center — your plans (drafts included) + your placements, needs-attention strip, folder tree.
  • Nav: Home / Workspace — after design review, the library is no longer a third destination (see design round below).

One deliberate deviation from the PRD: the workspace main pane still groups by plans/drafts rather than by folder — the folder tree + drag-to-file in the sidebar covers folder organization, and the visibility groups are what make drafts-you-never-published visible. Easy to revisit.

Design round (post-review feedback)

  • IA simplified: the Library page stopped being a separate nav concept — the workspace sidebar folder tree is "My library" (with a "Public view →" link to the read-only page others browse from profiles). Org-wide "All plans" browsing left the workspace sidebar; discovery lives on Home, search, and profiles.
  • Published is the unmarked normal state. Hidden states (draft/archived) get a quiet crossed-out-eye flag instead of colored badges; the Home feed says "new" instead of "published".
  • "Shelved in" is now Drive-style folder chips (icon + folder name; ownership in the tooltip).
  • Attachment upload is a designed dropzone (click or drag, auto-uploads); owner actions are consistent icon buttons; tags are edited as chips with suggestions.
  • Liquid-glass visual refresh: gradient mesh backdrop with translucent, blurred panels (nav, cards, popovers, search, floating comment pill), gradient primary buttons, softer radii — light + dark.

Feedback round 2 (workspace as the landing surface)

  • Workspace main pane mirrors your filing tree: collapsible groups per root folder (subtree rollup) plus "Unfiled", each with its own lazy pagination; group headings are drop targets (file a plan / nest a folder by dropping on them). Drafts are no longer a separate group — rows carry the quiet flag wherever they're filed.
  • "Since you last looked" strip: plans updated since your last visit ("updated") or never opened and not yours ("new to you"), driven by per-viewer read tracking. Self-clears as you read.
  • Nav is Workspace (landing) + Feed. Explainer copy removed throughout — the UI says it or it doesn't.
  • One pen. The single edit icon opens the unified editor (title + tag chips + content). The separate title/tags page is gone (old route redirects). The floating comment navigator bar is deleted; keyboard nav (j/k/r/a/d, s for resolved) stays.
  • Add-modal: references and attachments share one Popover-API lightbox pattern instead of always-visible inline forms.
  • Folders: created inline in the sidebar (type a name, Enter), nested by dragging one folder onto another (PATCH /folders/:id, cycle/depth/library rules enforced by the model).
  • Document types are visual: coplan_plan_types.icon (curated lucide set, per-install branding) renders as a leading chip on every row, feed item, and plan header.
  • Mobile pass: nav collapses to icons, popovers/forms clamp to the viewport, workspace stacks main-pane-first.

Polish & audit round (design, testing depth, dead code, Rails 8/Stimulus idioms)

Three-agent audit (dead code, idioms, test gaps), findings triaged and applied:

  • Visibility fix: the needs-attention strip now routes through Plan.visible_to + active — stale notifications can no longer resurface an archived plan or leak another user's draft title. Locked in with request specs.
  • Service extraction: checkbox toggles go through a new Plans::ToggleCheckbox service instead of an inline copy of the versioning pipeline in the controller.
  • Stimulus hygiene: comment-nav's advance-after-action observer is now single + timeboxed + cleaned up on disconnect (was stacking one per keypress); linked-thread retry timer cleared on disconnect; mention picker aborts stale searches; dead submitOnEnter action and unused targets/values removed.
  • Query work: one grouped COUNT for all folder groups; one shared unread-notifications query per request; shelves sort in SQL; notifications index capped; pagination stopped issuing an extra COUNT.
  • Rails 8 idioms: params.expect across web controllers (malformed payloads 400 instead of 500), normalizes for tag/folder names, atomic thread+first-comment create, new workspace_path helper replacing 15 hand-rolled params.permit(...).merge link sites.
  • Dead code deleted: the line-selection feature (helper + controller, broken since the anchor-text system replaced it), orphaned dropdown component (JS + CSS), 8 unreferenced CSS blocks, brainstorm status tokens.
  • A11y: add-modal gets aria-labelledby + autofocus, plan groups get aria-controls + true hidden when collapsed, inbox bell reports aria-expanded.
  • New test coverage: PlanPolicy + visible_to/publicly_listed unit specs (the discovery predicate had none), the unified editor's metadata path incl. metadata surviving an edit conflict, recency-strip bounds, libraries/theme/llms/version-diff request specs, and browser specs for the search modal, inbox, theme switcher, tag chips, folder-drag reparenting, and modal close paths.

Verification

  • Full suite: 1204 examples, 0 failures (isolated per-worktree test DB).
  • Migration cycle verified: migrate → rollback → migrate clean on MySQL 8.
  • Design QA: three screenshot passes (round 3: workspace folder groups, recency strip, add-modals, type chips, mobile 390px, dark mode)
  • Previous rounds: two screenshot passes over Home, Workspace, Library, Profile, plan page (incl. draft state, attachments, edit form) and search in light + dark (seeded demo DB, port 4600); tag chip editor exercised in-browser.
  • PRD updated to rev 7 to match implementation (born-published, unlisted drafts).

🤖 Generated with Claude Code

Feedback round 3 (Drive-style workspace + the 17-item list)

  • The workspace is now one level at a time, like Drive/Dropbox Paper. Each level shows its subfolders (empty ones too — that's where you drop things) and the docs filed right there; the root shows your unfiled docs. There is no "Unfiled" pseudo-folder and no flattened subtree. Breadcrumbs (My Plans › Payments › EBT) replace the "Folder:" chip and are drop targets, so dragging up a level is a thing.
  • Filters behave like search: picking a type/tag/date window switches to flat results scoped to the current folder's subtree. Every sidebar count now answers "what will clicking this show" given the other active filters — the #kmp (2) inside an empty folder bug is gone (tags/types with no matches drop out).
  • Document types are file icons, not tag-looking chips: a colored file glyph (stable tint per type name, name in the tooltip) leads every row, the plan header, search results, home feed, and profiles. Types moved to the top of the filter sidebar; created/updated windows ("Last 7/30 days") joined it.
  • "Since you last looked" now shows what changed: a new section-level markdown differ compares the version you last saw against the current one and the plan page highlights just those sections — once, per viewer; the visit that shows them also spends them. Back-nav can't resurrect the stale strip anymore (workspace opts out of Turbo's snapshot cache).
  • Drag works wherever you grab the card (inner links no longer steal the drag payload), and rows drop onto sidebar folders, main-pane folder rows, and breadcrumb segments alike.
  • New folder is a popover matching the references/attachments add-modals, pre-nested inside the folder you're viewing. (Also fixed it prefilling the current folder's own name.)
  • Draft → "Private": quiet crossed-eye flag on cards/search/OG descriptions, "Publish" is now "Share with everyone", the sidebar's Hidden section has Private/Archived toggles, and the agent API describes visibility: "draft" as a rare, explicit-request-only escape hatch.
  • Anyone signed in can add references and attachments (new contribute? policy — the upload UI no longer vanishes on plans you didn't write); removal stays with the author. The tab bar grew a shared "+" next to References and Attachments that activates the panel and opens its add-modal in one click — this also fixes the dead add-reference popup (popovers can't open from inside a hidden tab panel).
  • Plan header tightened: icon + title + byline in one compact block; shelves and owner actions share one row.
  • Mobile: the comment composer and thread popovers are full-width bottom sheets (the composer had to join the popover top layer — the glass panel's backdrop-filter made it the containing block, pinning the "fixed" sheet off-screen), and the workspace sidebar collapses behind a "Filters & folders" disclosure.
  • Keyboard nav: j/k walk folders and docs, Enter opens, Backspace goes up a level, Esc clears filters.
  • Tests: 1233 examples green, including new specs for the section differ, the once-only highlight semantics, Drive-level navigation/breadcrumbs/keyboard flows, folder-scoped counts, date windows, and the contribute policy. (System specs now run at an explicit 1400px viewport — the Capybara-registered headless-chrome driver silently ignores screen_size.)

Full-review pass (2fcbcb5)

A four-lens review of the whole diff (visibility/authz, correctness, JS/CSS, perf/tests), findings verified before fixing:

  • Visibility: API folder counts and the reference typeahead were the only two discovery surfaces still counting archived plans — both now apply .active like everything else. Everything else checked clean: every list/search/feed/notification path routes through visible_to/listed?, and flash/toast output is escape-safe.
  • Changed-section highlights got a real fix, not a patch: the server differ now sections the rendered document with the same Commonmarker options as the page, so fence toggles, setext/indented headings, CRLF bodies, HTML entities, and body-less headings can't desync server keys from client anchors (6 verified false-positive/false-negative classes, all covered by specs).
  • Sidebar counts respect the Hidden filter — folder links carry ?filter=, so their counts now count what the click will show.
  • Plans::Place hardening: unshelving is always allowed even if the plan has since become unlisted; concurrent double-shelving retries instead of 500ing. The service's guard rails (the web move endpoint's whole security boundary) now have a dedicated spec.
  • JS honesty: dropping a doc on the folder it's already in is a no-op instead of a PATCH + reload + "Moved" toast; Enter/Backspace no longer hijack focused buttons/links; the mobile comment sheet and changed-section note survive Turbo's snapshot cache correctly.
  • Perf: the workspace index was preloading full MEDIUMTEXT version bodies (content, diff, operations) for every row — it now preloads a lean id+sha stub and fetches the body only on preview-cache miss; search typeahead N+1 on plan types fixed.
  • New tests: draft-shelving rejection over both JSON and HTML, web references contribute policy, folder-scoped page-2 pagination, Escape-key navigation. Full suite: 1255 examples, 0 failures.

Feedback round 4 (bookmarks, footnotes, the two-way eye)

  • Saving is bookmarking, and it behaves like one. The flat "Add to Folder" select is gone everywhere. Rows and the plan page get a bookmark button: unsaved, it opens a folder navigator popover — the full hierarchy as a tree (like a filesystem) — to pick where it goes; saved, a second click just removes it, no dialog, no toast (the hover tint hints at it). Filing a plan places a pointer in your library; nothing is copied, and unbookmarking never touches the plan itself. Your own plans always live at My Plans root even when unfiled.
  • No more tabs on the plan page. The document is followed by footnote sections for References and Attachments — visually quieter, anchored ([/] jump between document and footnotes), listed at the bottom of the table of contents with counts. Each footnote header has a round icon-only + that opens the existing add-popovers. History moved to its own page behind a clock icon in the header — Backspace (or the back arrow) returns to the document; old ?tab=history links redirect. The TOC toggle moved from ] to t.
  • Show/hide is a two-way eye (author only, in the header): first click previews the flip — the slash appears/disappears and the button pulses; it reverts by itself after a few seconds if you wander off. Second click commits over fetch, no reload. Hiding takes a shared plan back to Private (unlisted, link keeps working); sharing works the same in reverse. Hides are logged in plan history like publishes.
  • Sidebar reads as one tree: My Plans is the root node with your folders nested beneath it, and the sidebar's "+ New folder" is gone (the breadcrumb bar's New folder button remains — it nests where you're standing).
  • Keyboard parity: arrow keys mirror j/k on the workspace; Backspace on a plan page takes you back to wherever you came from (falls back to the workspace when you arrived cold).
  • Updated filters join the combinatorial-count contract: "Last 7/30 days" links now show counts computed under the other active filters and drop out when empty — same rule every other sidebar count already follows.
  • Scale prep for 50k+ docs: three new indexes — a covering index for the per-folder placement counts and (author, updated_at) / (visibility, updated_at) on plans — keep the workspace queries index-only at that scale, no non-ActiveRecord machinery needed.
  • Rows read like Drive rows: the type icon is full-height across both text lines; the row's ⋯ menu is replaced by the bookmark, which shows on hover/focus.
  • Plan-type admin got its icon field: ActiveAdmin's PlanTypes form now offers the curated built-in icon set (tint stays derived from the type name).
  • Sign-in no longer double-loads. The host layout tracked a different stylesheet than the engine layout, so Turbo force-reloaded the page on every sign-in/out crossing — a wasted full load for users, and the async reload raced the next navigation (the source of an intermittent system-spec failure). Both layouts now track the engine stylesheet; the sign-in page inherits the current design and the stale copy of the old design tokens is deleted.
  • Test infra: system specs now run on Chrome for Testing (pinned major 150) provisioned by selenium-manager, instead of the system Chrome — on managed machines the system browser force-installs IT extensions into test profiles, and one of them intermittently opened a background tab that wedged Capybara's teardown (the long-standing "failed to close window in 20 seconds" flake, now root-caused and gone).

Merged main (post-#140), reconciled (7d10932)

Main moved while this branch was open — this merge adopts its new features and re-expresses their state language in this branch's visibility model:

Full suite after the merge: 1301 examples, 0 failures.

HamptonMakes and others added 7 commits July 17, 2026 15:58
…rchived_at

- Plans are born published; visibility:draft is the opt-in private flag
- Publishing is one-way; archive replaces abandon (opt-in visible, reversible)
- PlanPolicy#show? is now THE visibility predicate (was: true); attachments
  controller's duplicated brainstorm gate collapsed into it
- Legacy API compat: status accepted on writes, legacy_status emitted on reads
- Workspace groups by published/drafts; archived opt-in via ?filter=archived
- developing/live preserved as tags in the data migration

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…adcast helper

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Folders now live in libraries (polymorphic owner, auto-created per
user), plans attach to folders via first-class placements (one shelf
per library, many libraries per plan), and shelving is viewer-relative
everywhere: the workspace, the move menu, and the API's folder_id /
folder_path all speak in terms of the caller's own library.

- Library.for(owner) materializes the per-owner library invariant
- Plans::Place is the single write path for shelving (web + API),
  gated on the plan being listed for the actor
- Drafts are unlisted, not locked: PlanPolicy#show? is open, discovery
  goes through PlanPolicy#listed? / Plan.visible_to only
- Read-only library browsing at /libraries/:id with folder anchors;
  plan pages grew a "Shelved in" folder-jump strip
- API: folders index accepts ?library_id= for read-only browsing,
  legacy status values now 422 with guidance instead of being ignored

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Every person gets a page at /people/:username-or-id — the front door
to their library: identity (enriched by the host's people directory
via the new config.directory_profile hook, degrading to local columns
when absent or failing), published plans recent-first, and their
library shelves embedded read-only. Profiles are them-facing: only
publicly listed work appears, never drafts or archived plans, not
even your own (Plan.publicly_listed is the shared scope).

Author names across the app — plan headers, workspace row avatars,
library shelves, comments — now link to profiles.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Home (/home) is the new org-facing landing: a feed of publicly listed
plan activity rolled up per plan per day (agent editing loops hit rev
29 in days; per-edit entries would drown it), built by CoPlan::HomeFeed
from versions, publish events, and comments over a 14-day window.
Drafts and archived plans never appear.

Search now finds people too — a person result lands on their profile,
which is the road to their library. The nav grew primary links (Home /
Workspace / Library), the signed-in root redirect goes to Home, and
your name in the nav links to your profile. "All plans" stays demoted
to a workspace sidebar filter.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
From the screenshot pass on all four surfaces (light + dark): breathing
room between the brand and the primary nav links, no wrapping in the
nav user block, and profile shelf metadata wraps under the plan title
instead of ellipsizing.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 91cd5816fa

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

folder = resolve_folder_params
return if performed? # resolve_folder_params rendered an error
@plan.folder = folder
result = Plans::Place.call(plan: @plan, folder: folder, actor: current_user)

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Allow folder-only API shelving by non-authors

This new placement path is still inside update, which returns 403 on !policy.update? before reaching Plans::Place. When a user tries to shelve someone else's published plan into their own library, the service would allow it via listed? and the web move_to_folder flow now supports it, but the API rejects the request just because the caller is not the plan author. Split folder-only updates from metadata authorization or let them reach Plans::Place while keeping title/visibility/tag edits author-only.

Useful? React with 👍 / 👎.

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🤖 (replying on Hampton's behalf) Confirmed still valid — API update gates everything on PlanPolicy#update?, so a non-author can't shelve someone else's listed plan via the API even though the web move_to_folder flow (and Plans::Place itself) allows it. Deliberately deferring rather than special-casing folder-only PATCHes inside update: mixing two authorization regimes in one action is how policy bugs happen, and placements are viewer-relative anyway — they deserve their own endpoint (e.g. PUT /plans/:id/placement) that authorizes via Plans::Place's rules, mirroring the web route. Tracking as a follow-up after #153 lands.

Comment on lines +22 to 24
counts = PlanPlacement.where(library_id: library.id)
.visible_to(current_user)
.group(:folder_id)

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Exclude archived plans from folder counts

Archived plans are excluded from /api/v1/plans by default, but these folder counts only apply visible_to and therefore still count archived placements. A folder containing only archived plans will report a positive plans_count while the default plan listing for that folder returns no plans, which also surfaces archived work on a default discovery endpoint. Apply the same active/non-archived predicate here (and in the single-folder serializer) so counts match the plans users can list.

Useful? React with 👍 / 👎.

@HamptonMakes HamptonMakes Jul 20, 2026

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🤖 (replying on Hampton's behalf) Already addressed — the full-review pass (2fcbcb5) applies .where(plan: Plan.active) in both places: the grouped index counts and the single-folder folder_json fallback count. Archived placements no longer inflate plans_count, so counts match what the default plan listing returns.

.pluck(:plan_id, :created_at, :revision)
.each do |plan_id, at, revision|
rollup = note.call(plan_id, at)
revision == 1 ? rollup[:published] = true : rollup[:edits] += 1

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Do not report draft creation as a publish

When a plan is created as a draft and later published on a different day within the feed window, it is included in listed at build time, so this loop treats its revision-1 draft creation as a published activity; the PlanEvent query below then adds the real publish day too. Home can show a bogus publish entry (or two publish entries) for the same plan. Only plans actually born published should mark revision 1 this way.

Useful? React with 👍 / 👎.

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🤖 (replying on Hampton's behalf) Valid — fixed in e8f355d. Revision 1 now counts as the publish moment only when the plan has no "published" event; born-draft plans get their publish day from the event alone, and their revision 1 (which predates being listed) no longer produces a feed item. Covered by new specs in spec/models/home_feed_spec.rb.

HamptonMakes and others added 13 commits July 17, 2026 17:39
Provisioning raised RecordInvalid when the host-supplied username was
already held by a different account, which turned one data collision
into an error on every page for that user. A username is a nicety —
profiles fall back to id — so keep the user's current username (nil
for a new account) when the host sends one that collides.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
- Shelved-in strip is now Drive-style folder chips: icon + folder name,
  ownership in the tooltip; your own chips open the workspace folder.
- Published is the unmarked normal state everywhere. Hidden states
  (draft, archived) get a quiet crossed-out eye instead of colored
  badges; Home says "new" instead of "published"; the workspace group
  is just "Plans".
- Attachment upload: designed dropzone (click or drag, auto-submits)
  replacing the native file input + Upload button.
- IA: Library merged into Workspace. Nav is Home/Workspace; the sidebar
  folder tree is "My library" with a "Public view" link; the library
  page stays as the read-only face others browse. "All plans"/"My
  plans" filters removed — org-wide discovery lives on Home, search,
  and profiles (?scope=all still resolves, shown as a clearable chip).
- Owner actions: consistent icon buttons (publish/edit/title-tags/
  archive), SVG tab icons instead of emoji, chip-based tag editor with
  datalist suggestions.
- Liquid-glass visual refresh: gradient mesh backdrop, translucent
  blurred nav/cards/popovers/search, floating comment-toolbar pill,
  gradient primary buttons, softer radii — both themes.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Workspace & IA (feedback round):
- Workspace main pane groups by root folder (subtree rollup) + Unfiled,
  with per-group turbo-frame pagination and heading drop targets
- "Since you last looked" recency strip driven by PlanViewer.last_seen_at
  (new-to-you / updated badges); own unvisited plans excluded
- Nav is Workspace (landing) + Feed; single pen icon opens the unified
  editor (title + tags + content); comment navigator toolbar deleted
  (keyboard j/k/r/a/d/s retained)
- Reusable add-modal (Popover API) for references + attachments
- Inline folder creation in sidebar; folder-onto-folder drag reparenting
  via new PATCH /folders/:id (model-validated cycles/depth/library)
- Plan types get per-install lucide icons (coplan_plan_types.icon) and
  lead every row/feed/header line as a chip
- Mobile pass: nav collapse, popover clamping, workspace stacking

Audit fixes (testing depth, dead code, Rails 8/Stimulus idioms):
- Fix: needs-attention strip now routes through Plan.visible_to + active
  (stale notifications can no longer resurface archived plans or leak
  another user's draft title)
- Extract Plans::ToggleCheckbox service (controller no longer reimplements
  the versioning pipeline inline)
- comment_nav: single timeboxed MutationObserver (was stacking one per
  keypress, never cleaned up); text_selection: clear linked-thread retry
  timer on disconnect; mention picker: abort stale fetches, drop dead
  submitOnEnter action
- Workspace index: one grouped COUNT for all folder groups; shared unread
  grouped query; .to_a pagination (no extra COUNT); SQL ordering for
  shelves; notifications index capped at 100
- params.expect across web controllers (malformed payloads 400, not 500)
- New workspace_path helper replaces 15 params.permit(...).merge sites
- Dead code deleted: line-selection feature (helper + controller),
  dropdown controller + CSS, 8 orphaned CSS blocks, unused Stimulus
  targets/values, brainstorm status tokens, Plan#to_param
- A11y: add-modal aria-labelledby + autofocus, plan-group aria-controls +
  hidden toggle, inbox bell aria-expanded + hidden attr
- normalizes for Tag/Folder names; atomic thread+first-comment create
- New specs: PlanPolicy + visible_to/publicly_listed units, update_content
  metadata path (incl. conflict survival), recency-strip bounds, libraries
  redirects, settings theme, llms.txt, plan versions/diff, nav chrome
  system specs (search modal, inbox, theme), tag-chip editor, folder
  reparent drag, add-modal close paths, s-in-textarea guard
- Suite: 1204 examples, 0 failures

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…mobile sheets

Workspace becomes one-level-at-a-time navigation like Drive: each level
shows its subfolders (even empty ones) plus the docs filed right there,
root shows unfiled docs, and breadcrumbs replace the "Folder:" filter
chip. Filters (type on top, tags, updated windows, private/archived)
switch to flat subtree results, and every sidebar count now answers
"what does clicking this show" under the other active filters.

- Since-you-last-looked: new Plans::ChangedSections service diffs
  markdown sections between the version you last saw and the current
  one; the plan page highlights those sections once (client slugs
  mirror the server), and the workspace index opts out of Turbo cache
  so back-nav can't resurrect a stale strip. Trailing-blank-line shifts
  from adding a section below no longer flag the section above.
- Plan rows drag as a whole card (inner links draggable=false) onto
  sidebar folders, main-pane folder rows, and breadcrumb segments.
- New-folder is a popover modal nested in the current folder; fixed
  scope: :folder silently binding @folder and prefilling its name.
- Doc types render as colored file icons (CRC-stable tint, tooltip)
  in rows, headers, search, home, and profiles; text chip removed.
- Draft is now a quiet "Private" visibility flag (crossed-eye), the
  publish button reads "Share with everyone", and the agent API frames
  visibility=draft as a rare escape hatch.
- References/attachments: any signed-in user can add (contribute?
  policy), removal stays with the author; tab-bar "+" activates the
  panel before showing its popover (popovers can't open from hidden
  subtrees).
- Mobile: workspace sidebar collapses behind a Filters & folders
  disclosure; comment composer and thread popovers become bottom
  sheets. The composer joins the popover top layer — the glass panel's
  backdrop-filter made it the containing block, so position:fixed
  pinned the sheet off-screen instead of to the viewport.
- Workspace keyboard nav: j/k move, Enter opens, Backspace goes up a
  crumb, Escape clears filters.
- System specs run at an explicit 1400px viewport (the Capybara-
  registered :selenium_chrome_headless driver ignores screen_size).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Visibility/authz:
- API folder counts and reference typeahead now exclude archived plans,
  matching every other discovery surface (.active alongside visible_to).

Correctness:
- ChangedSections rewritten to section the *rendered* document (Commonmarker
  + Nokogiri walk), matching the client renderer exactly — fixes phantom
  changed-section highlights from fence toggles, setext/indented headings,
  CRLF content, HTML entities, and headings with no body.
- Plans::Create normalizes CRLF like ReplaceContent already did.
- Sidebar counts share the Hidden filter with the list they describe —
  a folder count under ?filter=archived now matches what clicking shows.
- Plans::Place: unshelving is always allowed (moved above the listed?
  check) and the placement upsert retries once on RecordNotUnique.

JS resilience:
- folder DnD payloads carry folderId/parentId so a no-op drop (same folder)
  skips the PATCH + reload + toast instead of lying about a move.
- workspace keys leave Enter/Backspace alone when a link/button/summary
  has focus.
- mobile comment sheet closes before Turbo caches the page; changed-section
  note no longer duplicates on snapshot restore.
- web folders create/reparent use params.expect (malformed payload → 400).

Perf:
- index rows preload a lean version stub (id + sha) instead of dragging
  MEDIUMTEXT content/diff/operations per row; preview body fetched only on
  cache miss. PlanVersion after_initialize guards has_attribute? for this.
- search typeahead includes :plan_type (kills an N+1).
- dead .dropdown CSS removed.

Tests: Plans::Place guard rails (8), rendered-section edge cases (6 more),
web references contribute policy, move_to_folder draft rejection (JSON+HTML),
sidebar counts under Hidden filter, folder page-2 pagination, Escape key nav.

Full suite: 1255 examples, 0 failures.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…ibility eye

- Saving is bookmarking: flat "Add to Folder" selects replaced everywhere by
  a bookmark button opening a hierarchical folder-picker popover (tree view,
  current folder marked, "Remove from my library"); placements are pointers,
  never copies
- Plan page loses its tabs: References/Attachments become footnote sections
  under the document (round icon-only "+" add buttons, TOC entries, [ / ]
  section jumps); History is its own page behind a header clock icon, with
  ?tab=history redirecting; TOC toggle moves to t
- Visibility is a two-way author-only eye: click to arm (slash preview,
  4s auto-revert), click again to commit over fetch — no reload; hide is
  policy-gated (published, unarchived) and logged to history
- Sidebar folder tree gets a "My Plans" root node; sidebar "+ New folder"
  removed (breadcrumb-bar button remains)
- Workspace arrows mirror j/k; Backspace on a plan page returns to where you
  came from (workspace fallback)
- Updated 7d/30d filters gain combinatorial counts and drop out at zero;
  covering/scale indexes added for 50k+ doc workspaces
- Rows show full-height two-line type icons, Drive-style; row menu replaced
  by the hover bookmark
- ActiveAdmin PlanTypes form gains the curated icon select
- Sign-in no longer double-loads: host layout now tracks the same engine
  stylesheet (Turbo stops force-reloading on sign-in/out crossings, which
  also raced navigation in system specs); stale host CSS deleted
- System specs run Chrome for Testing (pinned 150) via selenium-manager —
  managed-Chrome force-installed IT extensions were opening a background tab
  in test profiles and wedging Capybara teardown ("failed to close window")

Suite: 1265 examples, 0 failures.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Saved bookmarks (row and plan header) no longer reopen the folder
navigator — clicking them removes the placement with no dialog and no
toast, exactly like a browser bookmark star; click again to re-add.
The picker's "Remove from my library" button is gone (unreachable now),
saved bookmarks tint toward danger on hover as the only removal hint,
and triggers carry data-saved so the controller can tell the two
gestures apart.

Suite: 1265 examples, 0 failures.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…resh

Prefer this branch's visibility model while genuinely adopting main's new
features:

- LinkPreviews/Slack unfurls (#154): adopted; context now speaks the
  visibility language — Private/Archived flagged, published unmarked,
  never "Draft". plan_og_description delegates to the same builder.
- Mermaid comment anchors (#148/#149/#151): adopted; mermaid-settled
  re-highlighting wired into our no-tabs plan page, anchors-updated
  re-anchoring wired into our headless comment nav. Clicking a comment
  mark inside a diagram now opens its thread popover instead of the
  lightbox (showModal force-hides popovers).
- Landing refresh + agent-instructions view (#140): adopted; step copy
  and mock badge updated to the born-published model (type badge, no
  status badge).
- Status badge menu (#146): superseded — visibility is the only state,
  toggled by the header eye; status_menu_controller removed.
- Attachments dropzone (#147): superseded by our add-modal dropzone;
  spec selectors reconciled.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The version-diff spec asserted on raw HTML, but Diffy shells out to the
platform diff binary and GNU vs BSD diff pair changed lines differently,
splitting the phrase with inline word-highlight markup on Linux. Assert
on the rendered text instead.

Chrome on CI runners fills the small /dev/shm tmpfs and exits mid-suite;
--disable-dev-shm-usage spills to /tmp.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…hrome

The runner image ships chromedriver 150 in PATH next to Chrome 151; a
PATH driver takes precedence over selenium-manager, and the mismatched
pair makes Chrome exit at session creation — failing whichever system
spec runs first (the retry on the next spec falls back to managed
downloads, which is why exactly one spec failed per run). Removing the
PATH driver lets selenium-manager provision the matched Chrome for
Testing 150 + driver pair, same as local runs.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The CfT pin + forced browser download work around managed-machine policy
extensions — a local problem. On CI they fight selenium-manager into
mismatched browser/driver pairs (the runner's own Chrome + chromedriver
pair is unmanaged and matched, and is what main's CI has always used).
Also reverts the workflow's chromedriver removal from the previous
attempt. --disable-dev-shm-usage stays: it is what CI Chrome needs.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Typing Enter into the tag field before tag_input_controller connects
submits the form natively instead of committing a chip — on slow CI
boots the navigation detaches the field mid-send_keys ("Node with given
id does not belong to the document"). Waiting for the existing tag's
chip (rendered only on connect) closes the window.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Revision 1 marks the publish moment only for born-published plans.
Plans with a "published" event were listed later — their publish day
comes from the event, and their revision 1 predates being visible, so
it isn't feed activity. (Codex review finding on #153.)

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@HamptonMakes
HamptonMakes merged commit 9ddae3a into main Jul 20, 2026
4 checks passed
@HamptonMakes
HamptonMakes deleted the worktree-library-rethink branch July 20, 2026 18:11
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.

1 participant