Slice 2: the §8 active-RFC view in full

Per the §19.1 brief: the three-column shape (§8.1) opens on main
in discuss mode (§8.2), supports the §8.3 discuss-vs-contribute
flip on non-main branches, hosts §8.4's per-branch chat with AI
participation (§18's <change> protocol → §8.14 changes rows), the
§8.8 change-card panel with §8.9 accept/decline/edit-before-accept,
the §8.10 tracked-change markup + DiffView toggle, the §8.11
manual-edit flushes with the stale-change mechanic, the §8.12
range and paragraph sub-threads, the §8.13 flag affordance, and
the §8.14 discuss-mode buffer.

Backend: bot.py grew per-RFC-repo write ops (cut_branch_from_main,
commit_accepted_change with the structured original/proposed/reason
body and Change-Id + Source-Message-Id + On-behalf-of trailers,
commit_manual_flush, ensure_rfc_repo_seed). cache.py grew
refresh_rfc_repo and the webhook dispatches on repository.full_name.
providers.py and chat.py port the §18 carryovers — multi-provider
LLM abstraction and SSE-streaming chat against the §5 threads /
thread_messages / changes schema. api_branches.py mounts the §17
branches/<branch>/* and threads/<thread_id>/* routes with the §6
/ §11 permission checks inline.

Frontend: RFCView.jsx rebuilt as the §8 surface; Editor.jsx,
ChatPanel.jsx, ChangePanel.jsx, PromptBar.jsx, SelectionTooltip.jsx,
DiffView.jsx, ModelPicker.jsx, modelStyles.js lifted from the
prototype and adapted to the canonical schema.

Covered by `backend/tests/test_rfc_view_vertical.py` — eleven new
integration tests against an extended FakeGitea (PUT contents,
POST orgs/{org}/repos, seed_rfc_repo): main-view read,
promote-to-branch, accept (with and without edit-before-accept),
decline, manual flush + system message, flag creation, visibility
flip, anonymous read-but-no-contribute, stale-change refusal, and
the chat-streaming path with a fake provider injected. The 5
Slice 1 tests continue to pass alongside.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
Ben Stull
2026-05-24 04:35:14 -07:00
parent 779ba6db59
commit 3bc8fe92af
24 changed files with 5433 additions and 151 deletions
+123 -43
View File
@@ -49,6 +49,34 @@ chips, pending-ideas disclosure), and one end-to-end vertical: propose
→ idea PR opens → owner merges → super-draft appears in the catalog →
super-draft view renders the body.
### Slice 2 — shipped
The §8 active-RFC view in full. The bot wrapper grew per-RFC-repo
write operations — branch cut from main, accept-change commit with
the structured `original`/`proposed`/`reason` body and trailers,
manual-edit flush, and a `ensure_rfc_repo_seed` seam Slice 5's
graduation will eventually replace. The §4 cache now mirrors per-RFC
repos via a new `refresh_rfc_repo` path; the webhook receiver
dispatches on `repository.full_name` so per-RFC events refresh just
that repo, and the reconciler sweeps every active entry. The §18
carryovers landed as `backend/app/providers.py` (the multi-provider
abstraction, unchanged from the prototype) and `backend/app/chat.py`
(an adapter that runs the provider's streaming interface against
`thread_messages` rows, parses `<change>` blocks, and materializes
`changes` rows per §8.14). The §17 endpoints owned by Slice 2 — the
`branches/<branch>/*` and `threads/<thread_id>/*` families — live in
`backend/app/api_branches.py`, mounted alongside Slice 1's routes via
`api.make_router`. On the frontend, `RFCView.jsx` was rebuilt as the
§8 three-column surface; `Editor.jsx`, `ChatPanel.jsx`,
`ChangePanel.jsx`, `PromptBar.jsx`, `SelectionTooltip.jsx`,
`DiffView.jsx`, `ModelPicker.jsx`, and `modelStyles.js` were lifted
from the prototype and adapted to the canonical `threads` /
`thread_messages` / `changes` shape rather than the prototype's
global session_id. The §18 carryovers explicitly preserved: SSE
streaming with base64-encoded chunks, Tiptap + ProseMirror plugin for
the paragraph-margin gutter accent, the prompt-bar selection-quote
machinery, the model picker.
The §17 endpoints exercised so far:
| Method | Path | § |
@@ -64,29 +92,70 @@ The §17 endpoints exercised so far:
| POST | `/api/proposals/{pr_number}/withdraw` | §9.3 |
| POST | `/api/webhooks/gitea` | §4.1 |
| GET | `/auth/login` / `/auth/callback` / `/auth/logout` | §18 |
| GET | `/api/models` | §18 |
| GET | `/api/rfcs/{slug}/main` | §8.1, §8.2, §17 |
| GET | `/api/rfcs/{slug}/branches/{branch}` | §8.4, §17 |
| POST | `/api/rfcs/{slug}/branches/main/promote-to-branch` | §8.14, §17 |
| POST | `/api/rfcs/{slug}/branches/{branch}/changes/{id}/accept` | §8.9, §17 |
| POST | `/api/rfcs/{slug}/branches/{branch}/changes/{id}/decline` | §8.9, §17 |
| POST | `/api/rfcs/{slug}/branches/{branch}/changes/{id}/reask` | §8.11, §17 |
| POST | `/api/rfcs/{slug}/branches/{branch}/manual-flush` | §8.11, §17 |
| POST | `/api/rfcs/{slug}/branches/{branch}/visibility` | §11.1, §17 |
| POST | `/api/rfcs/{slug}/branches/{branch}/grants` | §6.4, §17 |
| DELETE | `/api/rfcs/{slug}/branches/{branch}/grants/{login}` | §6.4 |
| GET | `/api/rfcs/{slug}/branches/{branch}/threads` | §8.12, §17 |
| POST | `/api/rfcs/{slug}/branches/{branch}/threads` | §8.12, §8.13 |
| GET | `/api/rfcs/{slug}/branches/{branch}/threads/{id}/messages` | §8.12 |
| POST | `/api/rfcs/{slug}/branches/{branch}/threads/{id}/messages` | §8.12 |
| POST | `/api/rfcs/{slug}/branches/{branch}/threads/{id}/resolve` | §8.12 |
| POST | `/api/rfcs/{slug}/branches/{branch}/threads/{id}/chat` | §18 |
### What's deferred from slice 1
Slice 2 ships covered by `backend/tests/test_rfc_view_vertical.py`
the FakeGitea simulator from Slice 1 grew per-RFC-repo support (PUT
contents, POST `orgs/{org}/repos`, `seed_rfc_repo`), and a new test
file walks the §8 vertical end-to-end: main-view read, promote-to-
branch, accept (with and without edit-before-accept), decline, manual
flush + system message, flag creation, visibility flip, anonymous
read-but-no-contribute, stale-change refusal, and the chat-streaming
path with a fake provider injected.
These were on the §9.1 spec but pushed to Slice 2 because they belong
with surfaces that haven't been built yet:
### What's deferred from Slice 2
- The propose modal's **AI-suggested tags** (§9.1) — the AI surface
lands with Slice 2's chat wiring. The tag chip input works manually
in the meantime.
- The propose modal's **AI-drafted PR description** (§9.2) — same
reason. The PR description is the pitch text for now.
- The decline ceremony's **two-step composer-then-preview dialog**
(§9.3) — the single-step required-comment input is in place; the
preview-and-confirm beat is the kind of UX polish that the §19.2
topic "pending-idea view's interaction design (remainder)" should
pick up alongside the merge-confirmation ceremony.
- The §9.3 **pre-merge chat thread on a pending-idea view** and the
migration of those threads to the super-draft on merge — depends
on Slice 2's chat infrastructure.
These were in the §8 spec but lean on infrastructure later slices
build, so they were scoped out of this slice without altering the
spec:
These are deferred in the build's working sense — surfaces exist in
the spec, but they share infrastructure that's wired in a later slice
and would otherwise have to be wired twice.
- **Super-draft body editing on the meta repo (§9.5).** The
`branches/<branch>` machinery is structurally general enough that
meta-repo edit branches fall out of it once Slice 4 wires the
super-draft view's "Start Contributing" gesture to cut against the
meta repo. The Slice 2 RFCView renders a placeholder for
super-draft entries pointing at Slice 4.
- **The §10.4 review threads on PRs.** `thread_kind='review'` is in
the schema and the threads endpoints honor it generically, but the
PR-page surface where review threads anchor to diff hunks lands
with Slice 3.
- **DiffView's full reconstruction from `changes` history.** Slice 2
renders the editor's current HTML (which carries the
session-local tracked-change markup from the accepts that happened
in this session) into DiffView; rebuilding the full accepted-change
markup from `changes` for a returning contributor needs a render
pipeline DiffView doesn't yet own. The current behavior matches
§8.10's "session-local" framing exactly; the §19.2 "persistent
accepted-change markup" topic is the durable extension when
evidence demands it.
- **The §10.6 PR-side commit / chat reconciliation.** Manual-edit
flushes drop a system-author message into branch chat per §10.6
in Slice 2, but the PR-side seen-cursor that uses the marker
ships with Slice 3.
- **Branch-name path conversion for slashes.** The auto-generated
branch name in Slice 2 is `<login>-draft-<hex>` (no slash) so the
FastAPI `{branch}` path segment matches without `{branch:path}`.
Users can still rename to a slashed name, but the routes will
404 on read; the proper fix is `{branch:path}` everywhere, which
lands cleanly when Slice 3 makes the same change to the PR routes
(PR numbers don't have this problem, but resolving the routing
shape once across both surfaces is the right hop).
## Environment notes
@@ -123,31 +192,42 @@ and would otherwise have to be wired twice.
## Next slice
**Slice 2: the active-RFC view per §8.**
**Slice 3: the PR flow per §10.**
The active-RFC view inherits the three-column shape (§8.1), opens
on `main` in discuss mode by default (§8.2), supports the §8.3
discuss-vs-contribute mode flip on non-main branches, hosts §8.4's
per-branch chat with AI participation (§18's `<change>` protocol
parsing into `changes` rows per §8.6), the §8.8 change-card panel
with §8.9's accept / decline / edit-before-accept resolution, the
§8.10 tracked-change markup and DiffView toggle, the §8.11 manual-
edit flushes, the §8.12 range and paragraph sub-threads, the §8.13
flag affordance, and the §8.14 discuss-mode buffer.
§8 settled the within-branch surface; §10 settles the bridge between
a branch and main. The work covers the `Open PR` affordance from
§10.1 (with the §11.3 universal-public confirmation when the branch
is private), the §10.2 AI-drafted creation modal (title +
description from the diff plus the branch chat), the §10.3 review
page (three-column, diff in the center, compressed conversation
right, per-user seen-cursor accenting new hunks and new messages),
the §10.4 `thread_kind='review'` threads anchored to diff hunks
inline in branch chat, §10.5 merge (no-fast-forward, preserving the
per-acceptance commits), §10.6 update-after-open (commits and chat
arriving on the open PR, the manual-flush system message that
already lands per Slice 2), §10.7 post-merge (`Merged` banner,
chat read-only, 90-day deletion timer starts), §10.8 withdraw, and
§10.9 conflict-replay with the resolution-branch path. The shared
seen-cursor mechanism in §15.7 (the `pr_seen` and
`branch_chat_seen` cursors are in the schema already; Slice 3 wires
the advance-on-view reconciler).
The carryover assets that belong to Slice 2 are in the prototype
under `/Users/benstull/projects/wiggleverse/rfc-app-prototype/`:
The carryovers Slice 3 inherits — none new from the prototype; the
prototype's `PRModal.jsx` had a one-shot PR-creation flow that the
spec's §10 expanded considerably. The `backend/app/bot.py` operations
Slice 3 needs are: `open_pr`, `merge_pr` (style='merge' to preserve
the per-accepted-change commit nodes per §10.5), `close_pr` (for
withdraw), and the resolution-branch replay sequence from §10.9 —
which is structurally a `cut_branch_from_main` plus a series of
`commit_accepted_change` calls plus an `open_pr`.
- `frontend/src/components/Editor.jsx`, `ChatPanel.jsx`,
`ChangePanel.jsx`, `PromptBar.jsx`, `SelectionTooltip.jsx`,
`DiffView.jsx`, `ModelPicker.jsx` — Tiptap config, the
`<change>` parser, the selection-quote machinery, the
model-picker UX.
- `backend/providers.py`, `backend/chat.py` — the multi-provider
abstraction and the SSE-streaming chat layer.
The frontend needs a `PRView.jsx` sibling to `RFCView.jsx` that
inherits the §8.1 three-column shape but renders the diff instead
of the editor. The route is `/rfc/<slug>/prs/<n>`.
These are §18 carryovers; reuse the working code rather than
rewriting. The prototype's *data model* and *permission shape* do
not carry; this codebase's `threads`, `thread_messages`, `changes`,
`changes.thread_id`, the §6 four-role model, and the per-branch
chat thread are the canonical shape for Slice 2 to wire against.
The next build session should read `SPEC.md`, `README.md`, and
`docs/DEV.md` and pick up Slice 3 cleanly without re-briefing. The
working agreement in §19.3 continues to apply: implement the slice,
correct the spec only where running code reveals it was wrong at a
structural level, accumulate new candidate topics in §19.2, do not
extend the spec beyond what the slice requires.