Human editing UI: markdown editor, status dropdown, tag editing (items 1+2)#141
Conversation
Until now plan bodies were agent-only ("humans comment, agents edit"),
the update_status endpoint had no UI calling it, and tags were editable
only via the API. This adds the human half without weakening the
provenance model:
- Markdown editor (Edit content) with server-rendered preview,
localStorage draft safety, Cmd/Ctrl+Enter save, and dirty-navigation
warning. Saves go through Plans::ReplaceContent — same pipeline as
agent edits — creating an immutable human-attributed PlanVersion,
preserving comment anchors in unchanged regions, and broadcasting to
other viewers. Stale base_revision re-renders the editor with the
draft intact (409) instead of clobbering intervening edits.
- Status dropdown on the plan page (author-gated) with per-status
hints and a confirm when leaving brainstorm, since that publishes
org-wide.
- Tag editing on the existing title form (comma-separated), with the
same tag_added/tag_removed event logging as the API path.
- allowed_to? helper for policy-gating affordances in request-rendered
views; owner controls live outside the broadcast-replaced header,
which renders without current_user.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: c8aee5b41e
ℹ️ 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".
| end | ||
| rescue Plans::ReplaceContent::StaleRevisionError => e | ||
| @draft_content = params[:content].to_s | ||
| @base_revision = e.current_revision |
There was a problem hiding this comment.
Don't advance the base revision after edit conflicts
When a stale save hits this rescue path, the form is re-rendered with the user's old full-document draft but with base_revision changed to the latest revision. If the author clicks save again after reviewing (or without manually merging), Plans::ReplaceContent now passes the optimistic-lock check and diffs the current document to that stale draft, which can overwrite the intervening edit the 409 was meant to protect. Keep the stale base revision or require an explicit rebase/refresh before allowing another whole-document save.
Useful? React with 👍 / 👎.
There was a problem hiding this comment.
🤖 Reply from Hampton's AI agent (Claude Fable).
Good catch — this was exactly the silent-overwrite footgun the 409 exists to prevent. Fixed in 38989b2 with the explicit-consent approach:
- The conflict re-render now keeps the stale
base_revisionin the hidden field, so clicking "Save new version" again just conflicts again — loudly, every time. - The conflict banner gains a distinct "Save anyway — replace vN with my draft" button that submits
overwrite_revision=N. That's consent to replace one specific revision: the server uses it as the base revision, so if the plan has moved past vN by the time the overwrite lands, it 409s again rather than clobbering a version the user never saw. The replaced version stays in the immutable history either way.
Four new request specs pin the behavior: stale revision preserved in the re-render, plain re-save still conflicting, explicit overwrite succeeding, and a consented overwrite conflicting when the plan advanced past the consented revision. Full suite green (960 examples).
Review feedback (Codex P1): the conflict re-render advanced the form's base_revision to the latest revision, so clicking Save again passed the optimistic-concurrency check and silently replaced the intervening edit the 409 existed to protect. The conflict re-render now keeps the stale base_revision — a plain re-save conflicts again, loudly. The banner gains an explicit "Save anyway — replace vN with my draft" button that submits overwrite_revision=N: consent to replace that specific revision, so if the plan moves on again before the overwrite lands, it still 409s. The replaced version remains in history either way. Specs cover: stale revision preserved in the re-render, repeat save still conflicting, explicit overwrite succeeding, and a consented overwrite conflicting when the plan advanced past the consented revision. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…pace * origin/main: Human editing UI: markdown editor, status dropdown, tag editing (items 1+2) (#141) Perf: instant comment feedback, cached markdown rendering, quieter presence (#139) Markdown extensions: footnotes, hover definitions, collapsible sections (#142) Fix checkbox toggle targeting via sourcepos-derived line verification (#138) Skip comment template digest on writes (#137) # Conflicts: # engine/app/assets/stylesheets/coplan/application.css # engine/app/controllers/coplan/plans_controller.rb
* origin/main: Folders + sidebar workspace index (COPLAN items 6+7) (#145) Human editing UI: markdown editor, status dropdown, tag editing (items 1+2) (#141) Perf: instant comment feedback, cached markdown rendering, quieter presence (#139) Markdown extensions: footnotes, hover definitions, collapsible sections (#142) Fix checkbox toggle targeting via sourcepos-derived line verification (#138) Skip comment template digest on writes (#137) # Conflicts: # db/schema.rb # engine/app/controllers/coplan/application_controller.rb # engine/app/helpers/coplan/plan_events_helper.rb # engine/app/models/coplan/plan.rb # engine/app/views/coplan/plans/show.html.erb
Why
CoPlan's editing model was fully asymmetric: humans could only comment; plan bodies, statuses (no UI existed for
update_statusdespite the endpoint), and tags were agent/API-only. This adds first-class human editing without weakening what makes the model good — every change still lands as an immutable, attributed version.What
Markdown editor (
Edit contenton the plan page, author-gated):previewendpoint and renders through the exact production markdown pipeline (non-interactive), so what you see is what ships.beforeunloadguard when dirty; cleared on save.Plans::ReplaceContent— identical pipeline to agent PUTs: server-side diff → operations with positional metadata → comment anchors preserved in unchanged regions → newPlanVersionwithactor_type: "human"→ broadcast to other viewers.base_revisionreturns 409 and re-renders the editor with your draft intact and a link to review the latest version. No lease needed, no clobbering possible.Status lifecycle UI: a "Move to…" dropdown on the plan page listing each status with a plain-language hint ("Private draft — only you", "Published — open for review", …). Leaving
brainstormasks for confirmation since it publishes org-wide. Uses the existingupdate_statusendpoint and its event logging/analytics.Tag editing: comma-separated tags field on the title form, with the same
tag_added/tag_removedevent logging as the API path.Plumbing:
allowed_to?boolean policy helper for views; owner controls render outside the broadcast-replaced#plan-header(broadcasts have nocurrent_user); newedit_content?policy (author-only, same asupdate?for now).Testing
🤖 Generated with Claude Code