Files
rfc-app/mockups/main-pane.html
T
Ben Stull 779ba6db59 Slice 1: scaffolding + propose-to-super-draft vertical
Brings the §1 bot wrapper, the §4 cache (webhook + reconciler), the
§5 schema (six numbered migrations), Gitea OAuth + §6 user
provisioning, the §7 catalog left pane, and the propose-to-merge
vertical: propose modal opens an idea PR against the meta repo, an
owner merges from the pending-idea view, the cache picks it up via
webhook or reconciler sweep, and the catalog renders the new
super-draft.

Per §1 the bot is the only Git writer; every commit, branch
creation, and PR merge carries the §6.5 On-behalf-of: trailer and
an `actions` audit row. Per §4 the cache is never written from a
user action — it's webhook+reconciler only.

Covered by `backend/tests/test_propose_vertical.py` against an
in-process Gitea simulator.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-24 04:31:11 -07:00

1435 lines
60 KiB
HTML
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8" />
<title>RFC App — main pane mockup</title>
<meta name="viewport" content="width=device-width,initial-scale=1" />
<style>
/* ───────────────────────────────────────────────────────────────────────────
This file is a hand-built mockup, NOT real app code. It exists to make the
main-pane design decisions in SPEC.md §8 concrete enough to argue about.
Vanilla HTML/CSS/JS, no build step. Switch states with the pills at the
top of the page.
─────────────────────────────────────────────────────────────────────────── */
:root {
--bg: #fafaf9;
--surface: #ffffff;
--border: #e5e5e5;
--border-2: #ececec;
--ink: #1a1a1a;
--ink-2: #444;
--ink-3: #777;
--ink-4: #999;
--accent: #5b21b6; /* purple — claude */
--accent-soft: #ede9fe;
--add: #166534; /* green */
--add-bg: #dcfce7;
--add-bg-soft: #ecfdf5;
--rem: #991b1b; /* red */
--rem-bg: #fee2e2;
--rem-bg-soft: #fef2f2;
--warn-bg: #fef3c7;
--warn-ink: #92400e;
--pill: #f1f5f9;
--pill-ink: #475569;
--shadow-1: 0 1px 2px rgba(0,0,0,0.04), 0 1px 3px rgba(0,0,0,0.06);
--shadow-2: 0 4px 16px rgba(0,0,0,0.10);
}
* { box-sizing: border-box; }
body {
margin: 0;
font: 14px/1.5 -apple-system, BlinkMacSystemFont, "Segoe UI", system-ui, sans-serif;
color: var(--ink);
background: var(--bg);
height: 100vh;
overflow: hidden;
display: flex;
flex-direction: column;
}
/* ── Mockup chrome (state-switcher) ──────────────────────────────────── */
.mock-switcher {
background: #0f172a;
color: #e2e8f0;
padding: 6px 12px;
display: flex;
gap: 6px;
align-items: center;
font-size: 12px;
flex-shrink: 0;
}
.mock-switcher b { font-weight: 600; margin-right: 6px; color: #fff; }
.mock-switcher button {
background: rgba(255,255,255,0.08);
border: 1px solid rgba(255,255,255,0.18);
color: #cbd5e1;
border-radius: 999px;
padding: 3px 10px;
font-size: 11px;
cursor: pointer;
}
.mock-switcher button.active {
background: #fff;
color: #0f172a;
border-color: #fff;
font-weight: 600;
}
.mock-switcher .spacer { flex: 1; }
.mock-switcher .note { color: #94a3b8; font-style: italic; }
/* ── App chrome ─────────────────────────────────────────────────────── */
.app { flex: 1; display: flex; flex-direction: column; min-height: 0; }
.app-header {
height: 44px;
background: #1a1a1a;
color: #fff;
display: flex;
align-items: center;
padding: 0 16px;
gap: 16px;
flex-shrink: 0;
}
.app-header .brand { font-weight: 600; font-size: 13px; }
.app-header .crumbs { color: #aaa; font-size: 13px; display: flex; gap: 8px; align-items: center; }
.app-header .crumbs .sep { color: #555; }
.app-header .crumbs .active { color: #fff; }
.app-header .spacer { flex: 1; }
.app-header .me {
width: 28px; height: 28px; border-radius: 50%;
background: linear-gradient(135deg,#7c3aed,#3b82f6);
color: #fff; font-weight: 700; font-size: 12px;
display: flex; align-items: center; justify-content: center;
}
.app-body { flex: 1; display: flex; min-height: 0; overflow: hidden; }
/* ── Left pane ──────────────────────────────────────────────────────── */
.left {
width: 280px;
border-right: 1px solid var(--border);
background: var(--surface);
display: flex;
flex-direction: column;
flex-shrink: 0;
}
.left .search {
padding: 10px;
border-bottom: 1px solid var(--border);
}
.left .search input {
width: 100%;
border: 1px solid var(--border);
border-radius: 6px;
padding: 6px 8px;
font-size: 12px;
}
.rfc-list { flex: 1; overflow: auto; padding: 4px 0; }
.rfc-row {
padding: 6px 12px;
font-size: 13px;
color: var(--ink-2);
cursor: pointer;
display: flex;
align-items: center;
gap: 6px;
}
.rfc-row .id { color: var(--ink-4); font-variant-numeric: tabular-nums; font-size: 11px; }
.rfc-row.super-draft .id { font-style: italic; }
.rfc-row.selected {
background: #eef2ff;
color: var(--ink);
font-weight: 500;
}
/* Tree view inside selected RFC */
.rfc-tree {
background: #f8fafc;
padding: 4px 0;
border-top: 1px solid var(--border-2);
border-bottom: 1px solid var(--border-2);
}
.tree-row {
padding: 4px 12px 4px 28px;
font-size: 12px;
color: var(--ink-3);
cursor: pointer;
display: flex;
align-items: center;
gap: 6px;
}
.tree-row .icon { width: 14px; text-align: center; color: var(--ink-4); }
.tree-row .meta { color: var(--ink-4); font-size: 10px; }
.tree-row.selected {
background: #e0e7ff;
color: var(--ink);
font-weight: 500;
}
.tree-section {
padding: 4px 12px 2px 12px;
font-size: 10px;
text-transform: uppercase;
letter-spacing: 0.04em;
color: var(--ink-4);
font-weight: 600;
}
/* ── Main pane ──────────────────────────────────────────────────────── */
.main {
flex: 1;
display: flex;
flex-direction: column;
min-width: 0;
background: var(--bg);
}
/* Subheader strip — the "what am I looking at?" bar */
.main-sub {
background: var(--surface);
border-bottom: 1px solid var(--border);
padding: 8px 16px;
display: flex;
align-items: center;
gap: 12px;
min-height: 44px;
flex-shrink: 0;
}
.ref-pill {
display: inline-flex;
align-items: center;
gap: 6px;
background: var(--pill);
color: var(--pill-ink);
border-radius: 6px;
padding: 3px 8px;
font-size: 12px;
font-family: ui-monospace, SFMono-Regular, Menlo, monospace;
}
.ref-pill.branch { background: #f0f9ff; color: #075985; }
.ref-pill.pr { background: #fff7ed; color: #9a3412; }
.ref-pill.main { background: #f1f5f9; color: #334155; }
.cmp {
display: inline-flex; align-items: center; gap: 6px;
color: var(--ink-3); font-size: 12px;
}
.cmp .arrow { color: var(--ink-4); }
.sub-spacer { flex: 1; }
/* View toggle (Diff / Edit) */
.view-toggle {
display: inline-flex;
background: var(--pill);
border-radius: 6px;
padding: 2px;
}
.view-toggle button {
background: transparent;
border: none;
border-radius: 4px;
padding: 4px 10px;
font-size: 12px;
color: var(--ink-3);
cursor: pointer;
}
.view-toggle button.active {
background: #fff;
color: var(--ink);
box-shadow: var(--shadow-1);
font-weight: 500;
}
.btn {
background: #fff;
border: 1px solid var(--border);
border-radius: 6px;
padding: 5px 12px;
font-size: 12px;
color: var(--ink-2);
cursor: pointer;
font-weight: 500;
}
.btn.primary {
background: var(--ink);
color: #fff;
border-color: var(--ink);
}
.btn.primary:disabled {
opacity: 0.4;
cursor: default;
}
/* In-flight rail (RFC-root state) */
.inflight-rail {
display: flex;
gap: 8px;
align-items: center;
flex-wrap: wrap;
}
.inflight-rail .label {
color: var(--ink-3); font-size: 12px; margin-right: 4px;
}
.inflight-chip {
display: inline-flex; align-items: center; gap: 6px;
background: #fff; border: 1px solid var(--border);
border-radius: 999px; padding: 3px 10px;
font-size: 12px; color: var(--ink-2);
cursor: pointer;
font-family: ui-monospace, SFMono-Regular, Menlo, monospace;
}
.inflight-chip .dot {
width: 6px; height: 6px; border-radius: 50%; background: #0ea5e9;
}
.inflight-chip.pr .dot { background: #f97316; }
.inflight-chip .age { color: var(--ink-4); }
.inflight-chip:hover { border-color: var(--ink-3); }
/* ── Body: shared ───────────────────────────────────────────────────── */
.main-body {
flex: 1;
overflow: auto;
padding: 28px 56px;
}
.doc { max-width: 760px; margin: 0 auto; }
.doc h1 { font-size: 24px; margin: 0 0 8px; }
.doc h2 {
font-size: 18px; margin: 28px 0 8px;
padding-bottom: 4px; border-bottom: 1px solid var(--border-2);
}
.doc h3 { font-size: 15px; margin: 18px 0 6px; }
.doc p { margin: 8px 0; color: var(--ink-2); }
.doc ul { margin: 6px 0; padding-left: 22px; color: var(--ink-2); }
.doc code {
background: #f1f5f9; padding: 1px 4px; border-radius: 3px;
font-size: 12px;
}
/* RFC-root header block */
.root-header {
background: var(--surface);
border: 1px solid var(--border);
border-radius: 8px;
padding: 14px 18px;
margin-bottom: 18px;
display: flex;
align-items: center;
gap: 16px;
}
.root-header .meta {
color: var(--ink-3); font-size: 12px;
}
.root-header .meta b { color: var(--ink); font-weight: 600; }
.root-header .spacer { flex: 1; }
/* ── Diff view ──────────────────────────────────────────────────────── */
.diff-summary {
background: var(--surface);
border: 1px solid var(--border);
border-radius: 8px;
padding: 10px 14px;
margin-bottom: 16px;
display: flex;
align-items: center;
gap: 14px;
font-size: 12px;
color: var(--ink-3);
}
.diff-summary .count { font-weight: 600; color: var(--ink); }
.diff-summary .add { color: var(--add); }
.diff-summary .rem { color: var(--rem); }
.diff-summary .pend { color: var(--warn-ink); }
/* Mini-toggle inside the diff-summary for full-doc vs changes-only */
.scope-toggle {
display: inline-flex;
background: var(--pill);
border-radius: 5px;
padding: 2px;
margin-left: auto;
}
.scope-toggle button {
background: transparent; border: none; border-radius: 3px;
padding: 3px 8px; font-size: 11px; color: var(--ink-3);
cursor: pointer;
}
.scope-toggle button.active {
background: #fff; color: var(--ink); font-weight: 500;
box-shadow: var(--shadow-1);
}
/* "Hunks" — each region of contiguous change in the diff view */
.hunk {
background: var(--surface);
border: 1px solid var(--border);
border-radius: 8px;
margin-bottom: 14px;
overflow: hidden;
position: relative;
}
.hunk-header {
background: #f8fafc;
border-bottom: 1px solid var(--border-2);
padding: 6px 12px;
font-size: 11px;
color: var(--ink-3);
font-family: ui-monospace, SFMono-Regular, Menlo, monospace;
display: flex; align-items: center; gap: 10px;
}
.hunk-header .loc { color: var(--ink-4); }
.hunk-header .prov {
background: var(--accent-soft); color: var(--accent);
padding: 1px 6px; border-radius: 3px; font-size: 10px;
font-family: inherit; font-weight: 600;
}
.hunk-header .prov.manual { background: var(--pill); color: var(--pill-ink); }
.hunk-header .convo-link {
color: var(--ink-3); text-decoration: underline;
text-underline-offset: 2px; cursor: pointer;
}
.hunk-header .state {
margin-left: auto;
background: var(--warn-bg); color: var(--warn-ink);
padding: 1px 8px; border-radius: 999px;
font-size: 10px; font-weight: 600; text-transform: uppercase;
letter-spacing: 0.04em;
font-family: inherit;
}
.hunk-header .state.accepted { background: var(--add-bg); color: var(--add); }
.hunk-header .state.declined { background: #f3f4f6; color: var(--ink-3); }
.hunk-body {
padding: 8px 0;
font-family: ui-monospace, SFMono-Regular, Menlo, monospace;
font-size: 12.5px;
line-height: 1.55;
}
.line {
display: flex;
gap: 8px;
padding: 0 12px;
white-space: pre-wrap;
word-break: break-word;
}
.line .gutter {
width: 14px;
flex-shrink: 0;
color: var(--ink-4);
text-align: center;
user-select: none;
}
.line.context { background: transparent; color: var(--ink-2); }
.line.add { background: var(--add-bg-soft); }
.line.add .gutter { color: var(--add); }
.line.add .content { color: var(--add); }
.line.rem { background: var(--rem-bg-soft); }
.line.rem .gutter { color: var(--rem); }
.line.rem .content { color: var(--rem); }
.hunk-actions {
border-top: 1px solid var(--border-2);
background: #fafbfc;
padding: 6px 12px;
display: flex; gap: 6px; align-items: center;
font-size: 11px; color: var(--ink-3);
}
.hunk-actions .spacer { flex: 1; }
.hunk-actions .btn { padding: 3px 10px; font-size: 11px; }
.hunk-actions .btn-accept { background: var(--add); color: #fff; border-color: var(--add); }
.hunk-actions .btn-decline { background: #fff; color: var(--rem); border-color: var(--rem-bg); }
/* Anchored conversation marker in margin */
.anchor-bubble {
position: absolute;
right: -260px;
top: 8px;
width: 230px;
background: var(--surface);
border: 1px solid var(--border);
border-radius: 8px;
padding: 8px 10px;
box-shadow: var(--shadow-1);
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif;
font-size: 11px;
color: var(--ink-3);
cursor: pointer;
}
.anchor-bubble:hover { box-shadow: var(--shadow-2); border-color: var(--ink-4); }
.anchor-bubble .who {
font-weight: 600; color: var(--ink);
display: flex; align-items: center; gap: 5px;
margin-bottom: 2px;
}
.anchor-bubble .who .avatar {
width: 14px; height: 14px; border-radius: 50%;
background: var(--accent); color: #fff; font-size: 9px;
display: flex; align-items: center; justify-content: center; font-weight: 700;
}
.anchor-bubble .preview {
color: var(--ink-3);
overflow: hidden;
display: -webkit-box;
-webkit-line-clamp: 2;
-webkit-box-orient: vertical;
}
.anchor-bubble .footer {
margin-top: 6px;
display: flex; gap: 8px;
font-size: 10px; color: var(--ink-4);
}
.anchor-bubble .footer .pending {
color: var(--warn-ink); font-weight: 600;
}
/* Show anchor bubbles only when there's room (wide viewports) */
@media (max-width: 1380px) {
.anchor-bubble { display: none; }
}
/* Unchanged-context prose between hunks: the full document reads naturally,
with diff hunks embedded at their locations. In "changes only" mode the
unchanged regions collapse to one-line skips. */
.ctx { color: var(--ink-2); }
.ctx h2 {
font-size: 18px;
margin: 28px 0 8px;
padding-bottom: 4px;
border-bottom: 1px solid var(--border-2);
}
.ctx p { margin: 8px 0; }
.ctx p .loc {
color: var(--ink-4);
font-family: ui-monospace, SFMono-Regular, Menlo, monospace;
font-size: 10px;
margin-right: 6px;
user-select: none;
opacity: 0;
transition: opacity 0.1s;
}
.ctx p:hover .loc { opacity: 1; }
.skip-region {
text-align: center;
color: var(--ink-4);
font-size: 11px;
padding: 6px 0;
border-top: 1px dashed var(--border-2);
border-bottom: 1px dashed var(--border-2);
margin: 10px 0;
cursor: pointer;
}
.skip-region:hover { color: var(--ink-2); }
/* Changes-only mode: hide all .ctx blocks and h2 headers between hunks,
replacing them with the skip-region markers we leave in place. */
.main-body.changes-only .ctx { display: none; }
.main-body.changes-only .skip-region { display: block; }
.main-body:not(.changes-only) .skip-region { display: none; }
/* ── Edit view (track-changes inline) ──────────────────────────────── */
.edit-doc {
background: var(--surface);
border: 1px solid var(--border);
border-radius: 8px;
padding: 28px 36px;
line-height: 1.7;
color: var(--ink-2);
}
.edit-doc h2 { font-size: 17px; margin-top: 22px; }
.tracked-delete {
background: var(--rem-bg);
color: var(--rem);
text-decoration: line-through;
text-decoration-color: rgba(153, 27, 27, 0.5);
padding: 0 2px; border-radius: 2px;
}
.tracked-insert {
background: var(--add-bg);
color: var(--add);
padding: 0 2px; border-radius: 2px;
}
.anchor-pin {
display: inline-flex;
vertical-align: middle;
width: 16px; height: 16px;
border-radius: 50%;
background: var(--accent);
color: #fff;
font-size: 9px;
font-weight: 700;
align-items: center; justify-content: center;
margin: 0 2px;
cursor: pointer;
}
/* ── PR view extras ─────────────────────────────────────────────────── */
.pr-banner {
background: #fffbeb;
border: 1px solid #fcd34d;
color: #92400e;
border-radius: 8px;
padding: 10px 14px;
font-size: 12px;
margin-bottom: 14px;
display: flex; align-items: center; gap: 10px;
}
.pr-banner b { font-weight: 600; }
/* ── Right pane ─────────────────────────────────────────────────────── */
.right {
width: 360px;
background: var(--surface);
border-left: 1px solid var(--border);
display: flex;
flex-direction: column;
flex-shrink: 0;
min-height: 0;
}
.right-tabs {
display: flex;
border-bottom: 1px solid var(--border);
flex-shrink: 0;
}
.right-tabs button {
flex: 1;
background: transparent;
border: none;
padding: 10px 8px;
font-size: 12px;
color: var(--ink-3);
cursor: pointer;
border-bottom: 2px solid transparent;
}
.right-tabs button.active {
color: var(--ink);
font-weight: 600;
border-bottom-color: var(--ink);
}
.right-tabs button .count {
background: var(--pill);
color: var(--pill-ink);
border-radius: 999px;
padding: 1px 6px;
font-size: 10px;
margin-left: 4px;
}
.right-body { flex: 1; overflow: auto; }
/* Tendril list (overview) */
.tendril-list { padding: 6px 0; }
.tendril-item {
padding: 10px 14px;
border-bottom: 1px solid var(--border-2);
cursor: pointer;
}
.tendril-item:hover { background: #fafbfc; }
.tendril-item.active {
background: #eef2ff;
border-left: 3px solid var(--accent);
padding-left: 11px;
}
.tendril-item .anchor {
font-size: 11px; color: var(--ink-4);
font-family: ui-monospace, SFMono-Regular, Menlo, monospace;
margin-bottom: 2px;
}
.tendril-item .quote {
background: #fef9c3; padding: 2px 6px;
border-radius: 3px; font-size: 12px;
color: var(--ink-2);
overflow: hidden;
display: -webkit-box;
-webkit-line-clamp: 1;
-webkit-box-orient: vertical;
font-style: italic;
margin-bottom: 4px;
}
.tendril-item .summary {
font-size: 12px; color: var(--ink-2);
overflow: hidden;
display: -webkit-box;
-webkit-line-clamp: 2;
-webkit-box-orient: vertical;
}
.tendril-item .meta {
font-size: 10px; color: var(--ink-4);
margin-top: 5px; display: flex; gap: 8px;
}
.tendril-item .meta .pending {
color: var(--warn-ink); font-weight: 600;
}
.tendril-item .meta .accepted {
color: var(--add); font-weight: 600;
}
/* Branch-level chat at top of tendril list */
.branch-thread {
background: #f0f9ff;
padding: 10px 14px;
border-bottom: 1px solid var(--border);
cursor: pointer;
display: flex; align-items: center; gap: 8px;
}
.branch-thread .icon { font-size: 14px; }
.branch-thread .label { font-size: 12px; color: var(--ink); font-weight: 600; flex: 1; }
.branch-thread .meta { font-size: 10px; color: var(--ink-3); }
/* Active conversation detail (when one tendril is opened) */
.convo-detail {
display: flex; flex-direction: column; height: 100%;
}
.convo-detail .convo-header {
padding: 10px 14px;
border-bottom: 1px solid var(--border);
background: #fafbfc;
}
.convo-detail .convo-header .back {
background: none; border: none; color: var(--ink-3);
font-size: 11px; cursor: pointer; padding: 0;
margin-bottom: 4px;
}
.convo-detail .convo-header .anchor {
font-family: ui-monospace, SFMono-Regular, Menlo, monospace;
font-size: 11px; color: var(--ink-4);
}
.convo-detail .convo-header .quote {
background: #fef9c3; padding: 4px 8px;
border-radius: 4px; font-size: 12px;
font-style: italic; color: var(--ink-2);
margin-top: 4px;
}
.convo-messages { flex: 1; overflow: auto; padding: 10px 12px; }
.msg { margin-bottom: 10px; }
.msg .who {
font-size: 11px; font-weight: 600; color: var(--ink-3);
margin-bottom: 3px;
display: flex; align-items: center; gap: 5px;
}
.msg .who .dot {
width: 6px; height: 6px; border-radius: 50%;
}
.msg.user .who .dot { background: #3b82f6; }
.msg.ai .who .dot { background: var(--accent); }
.msg.ai .who .model { color: var(--accent); font-weight: 500; font-size: 10px; }
.msg .bubble {
background: #f8fafc;
border-radius: 8px;
padding: 8px 10px;
font-size: 12.5px;
color: var(--ink);
line-height: 1.5;
}
.msg.ai .bubble { background: var(--accent-soft); }
.msg .change-chip {
margin-top: 4px;
display: inline-flex; align-items: center; gap: 4px;
background: var(--warn-bg); color: var(--warn-ink);
border-radius: 4px; padding: 2px 6px;
font-size: 11px; font-weight: 600;
cursor: pointer;
}
.msg .change-chip.accepted { background: var(--add-bg); color: var(--add); }
/* Compose box */
.compose {
border-top: 1px solid var(--border);
padding: 10px 12px;
background: #fff;
flex-shrink: 0;
}
.compose textarea {
width: 100%;
border: 1px solid var(--border);
border-radius: 6px;
padding: 8px;
font-family: inherit;
font-size: 12.5px;
resize: vertical;
min-height: 56px;
}
.compose-row {
display: flex; align-items: center;
margin-top: 6px; gap: 6px;
}
.compose-row .model-pill {
background: var(--pill); color: var(--pill-ink);
border-radius: 4px; padding: 2px 7px; font-size: 11px;
}
.compose-row .spacer { flex: 1; }
/* Hide alternate states */
[data-state] { display: none; }
[data-state].active { display: block; }
[data-state="root"].active { display: contents; }
[data-state="branch-diff"].active,
[data-state="branch-edit"].active,
[data-state="pr"].active { display: contents; }
/* Right-pane mode-specific overrides */
.right[data-mode="convo"] .tendril-list { display: none; }
.right[data-mode="convo"] .branch-thread { display: none; }
.right[data-mode="list"] .convo-detail { display: none; }
</style>
</head>
<body>
<!-- ── State switcher (mockup chrome) ──────────────────────────────────── -->
<div class="mock-switcher">
<b>Main pane states:</b>
<button class="state-btn" data-target="root">RFC root</button>
<button class="state-btn active" data-target="branch-diff">Branch · Diff view</button>
<button class="state-btn" data-target="branch-edit">Branch · Edit view</button>
<button class="state-btn" data-target="pr">PR</button>
<span class="spacer"></span>
<span class="note">Mockup for SPEC.md §8 — not real app code</span>
</div>
<div class="app">
<!-- ── App header ───────────────────────────────────────────────────── -->
<div class="app-header">
<span class="brand">OHM RFC Contributor</span>
<span class="crumbs">
<span>wiggleverse</span><span class="sep">/</span>
<span>RFC-0042 · Open Human Model</span>
<span class="sep" id="crumb-sep" style="display:none">/</span>
<span class="active" id="crumb-ref"></span>
</span>
<span class="spacer"></span>
<div class="me">B</div>
</div>
<div class="app-body">
<!-- ── Left pane (shared across states) ──────────────────────────── -->
<aside class="left">
<div class="search">
<input type="text" placeholder="Search RFCs by title, slug, or ID…" />
</div>
<div class="rfc-list">
<div class="rfc-row"><span class="id">RFC-0038</span><span>Capability negotiation</span></div>
<div class="rfc-row"><span class="id">RFC-0040</span><span>Provider keys & rotation</span></div>
<div class="rfc-row selected"><span class="id">RFC-0042</span><span>Open Human Model</span></div>
<!-- Tree view appears under the selected RFC -->
<div class="rfc-tree">
<div class="tree-section">Main</div>
<div class="tree-row" data-jump="root">
<span class="icon"></span>
<span>main</span>
<span class="meta">v1 · 4d ago</span>
</div>
<div class="tree-section">Open PRs (1)</div>
<div class="tree-row" data-jump="pr">
<span class="icon"></span>
<span>#4 split §5 into §5/§5a</span>
<span class="meta">alice</span>
</div>
<div class="tree-section">Open branches (3)</div>
<div class="tree-row selected" data-jump="branch-diff">
<span class="icon"></span>
<span>ben/clarify-graduation</span>
<span class="meta">2d</span>
</div>
<div class="tree-row">
<span class="icon"></span>
<span>raj/inline-arbiters</span>
<span class="meta">5d</span>
</div>
<div class="tree-row">
<span class="icon"></span>
<span>jo/spike-id-scheme</span>
<span class="meta">9d</span>
</div>
<div class="tree-section">Closed (3) <span style="float:right;color:#999;cursor:pointer">show ▾</span></div>
</div>
<div class="rfc-row super-draft"><span class="id">super-draft</span><span>Conflict resolution model</span></div>
<div class="rfc-row"><span class="id">RFC-0035</span><span>Bot service account</span></div>
</div>
</aside>
<!-- ── Main pane ─────────────────────────────────────────────────── -->
<main class="main">
<!-- ── State: RFC root ──────────────────────────────────────────── -->
<div data-state="root">
<div class="main-sub">
<span class="ref-pill main">main</span>
<span class="cmp">v1 · merged 4d ago by ben</span>
<span class="inflight-rail">
<span class="label">In flight:</span>
<span class="inflight-chip" data-jump="pr"><span class="dot"></span>PR #4 · split §5 <span class="age">· 1d</span></span>
<span class="inflight-chip" data-jump="branch-diff"><span class="dot"></span>ben/clarify-graduation <span class="age">· 2d</span></span>
<span class="inflight-chip"><span class="dot"></span>raj/inline-arbiters <span class="age">· 5d</span></span>
<span class="inflight-chip"><span class="dot"></span>jo/spike-id-scheme <span class="age">· 9d</span></span>
</span>
<span class="sub-spacer"></span>
<button class="btn">+ New branch</button>
</div>
<div class="main-body">
<div class="doc">
<div class="root-header">
<div>
<div style="font-weight:600;font-size:15px">RFC-0042 · Open Human Model</div>
<div class="meta">Active · owners <b>ben</b>, <b>raj</b> · arbiters <b>ben</b> · tags identity, schema</div>
</div>
<span class="spacer"></span>
<button class="btn">☆ Star</button>
<button class="btn primary">+ Propose change</button>
</div>
<h1>Open Human Model</h1>
<p style="color:var(--ink-3);font-style:italic">This is the canonical, merged-to-main version. Drill into a branch or PR (left pane or chips above) to see proposed changes.</p>
<h2>1. Why this RFC exists</h2>
<p>The Open Human Model (OHM) is the schema that lets a contributor describe themselves to the rest of the system in a way that other tools — RFCs, applications, the meta-repo's directory — can read uniformly.</p>
<h2>2. Scope</h2>
<p>This RFC defines the on-disk shape of an OHM record, its required and optional fields, the versioning policy, and the migration path from the prior ad-hoc <code>profile.yaml</code>.</p>
<h2>3. Schema</h2>
<p>An OHM record is a YAML document with a frontmatter block of canonical fields and an open free-form body. The canonical fields are…</p>
<h2>4. Versioning</h2>
<p>OHM follows semantic versioning at the document level. Major bumps may break consumers; minor bumps add fields…</p>
</div>
</div>
</div>
<!-- ── State: Branch · Diff view ─────────────────────────────────── -->
<div data-state="branch-diff" class="active">
<div class="main-sub">
<span class="ref-pill branch">⎇ ben/clarify-graduation</span>
<span class="cmp"><span class="arrow"></span> compared to <b style="color:var(--ink);font-weight:500">main</b></span>
<span class="sub-spacer"></span>
<div class="view-toggle">
<button class="active" data-toggle="branch-diff">Diff</button>
<button data-toggle="branch-edit">Edit</button>
</div>
<button class="btn">Open PR ▸</button>
</div>
<div class="main-body" id="branch-diff-body">
<div class="doc">
<div class="diff-summary">
<span><span class="count">5 changes</span> across 3 sections</span>
<span class="add">+34 lines</span>
<span class="rem">18 lines</span>
<span>·</span>
<span><b style="color:var(--add)">2 accepted</b>, <b class="pend">2 pending</b>, <b style="color:var(--ink-4)">1 declined</b></span>
<div class="scope-toggle" data-scope-target="branch-diff-body">
<button class="active" data-scope="full">Full doc</button>
<button data-scope="changes">Changes only</button>
</div>
</div>
<!-- ── §1 Why this RFC exists ── -->
<div class="ctx">
<h2>1. Why this RFC exists</h2>
<p><span class="loc">§1 ¶1</span>The Open Human Model (OHM) is the schema that lets a contributor describe themselves to the rest of the system in a way that other tools — RFCs, applications, the meta-repo's directory — can read uniformly.</p>
<p><span class="loc">§1 ¶2</span>Today, every consumer reaches into the ad-hoc <code>profile.yaml</code> and projects its own subset of fields. The result is silent drift: two consumers can read the same file and disagree about what a contributor's handle even is.</p>
</div>
<div class="skip-region">▴ §1 unchanged · 2 paragraphs · expand</div>
<!-- ── §2 Scope ── -->
<div class="ctx">
<h2>2. Scope</h2>
<p><span class="loc">§2 ¶1</span>This RFC defines the on-disk shape of an OHM record, its required and optional fields, the versioning policy, and the migration path from the prior ad-hoc <code>profile.yaml</code>.</p>
<p><span class="loc">§2 ¶2</span>It does <em>not</em> define an API for reading OHM records, nor a publishing protocol — those are independent concerns and will be specified in their own RFCs.</p>
</div>
<div class="skip-region">▴ §2 unchanged · 2 paragraphs · expand</div>
<!-- ── §3 Schema ── -->
<div class="ctx">
<h2>3. Schema</h2>
<p><span class="loc">§3 ¶1</span>An OHM record is a YAML document with a frontmatter block of canonical fields and an open free-form body. The frontmatter is what consumers parse; the body is for the contributor to write in.</p>
</div>
<div class="hunk">
<div class="hunk-header">
<span class="loc">§3 ¶2</span>
<span class="prov">Claude · sonnet-4.5</span>
<span class="convo-link" data-open-tendril="t1">from anchored thread "schema field naming"</span>
<span class="state accepted">Accepted</span>
</div>
<div class="hunk-body">
<div class="line rem"><span class="gutter"></span><span class="content">The canonical fields are <code>name</code>, <code>handle</code>, <code>pronouns</code>, and <code>bio</code>.</span></div>
<div class="line add"><span class="gutter">+</span><span class="content">The canonical fields are <code>display_name</code>, <code>handle</code>, <code>pronouns</code>, <code>bio</code>, and <code>established_at</code>.</span></div>
<div class="line add"><span class="gutter">+</span><span class="content">The <code>display_name</code> rename clarifies that this field is the human-facing label, distinct from the immutable <code>handle</code>.</span></div>
</div>
<div class="hunk-actions">
<span>Accepted by ben · 4h ago · edited before accepting</span>
<span class="spacer"></span>
<button class="btn">↶ Unaccept</button>
<button class="btn">View convo</button>
</div>
<!-- Margin anchor bubble pointing into this hunk -->
<div class="anchor-bubble" data-open-tendril="t1">
<div class="who"><span class="avatar">C</span> schema field naming</div>
<div class="preview">"the name field is ambiguous — display label or stable id?" → Claude proposed renaming…</div>
<div class="footer">
<span>4 msgs</span>
<span class="accepted">1 accepted</span>
</div>
</div>
</div>
<div class="hunk">
<div class="hunk-header">
<span class="loc">§3 ¶3 (new paragraph)</span>
<span class="prov manual">Manual · ben</span>
<span class="state accepted">Accepted</span>
</div>
<div class="hunk-body">
<div class="line add"><span class="gutter">+</span><span class="content">A record's <code>established_at</code> is the date the contributor first claimed this OHM, expressed as ISO-8601. Subsequent edits do not bump it.</span></div>
</div>
<div class="hunk-actions">
<span>Typed directly by ben · 4h ago</span>
<span class="spacer"></span>
<button class="btn">↶ Revert</button>
</div>
</div>
<div class="ctx">
<p><span class="loc">§3 ¶4</span>Each field has a normative type in §3.1 (string, ISO-8601 date, list of strings, etc.) and a non-normative example in §3.2. Consumers MUST validate the type and SHOULD treat any extra field they don't recognize as opaque.</p>
<p><span class="loc">§3 ¶5</span>The free-form body is plain Markdown. It is intended for the contributor's own narrative — bio, working hours, time-zone notes, whatever they want collaborators to know. Consumers that don't render Markdown should pass the body through unchanged.</p>
</div>
<div class="skip-region">▴ §3 ¶45 unchanged · 2 paragraphs · expand</div>
<!-- ── §4 Versioning ── -->
<div class="ctx">
<h2>4. Versioning</h2>
</div>
<div class="hunk">
<div class="hunk-header">
<span class="loc">§4 ¶1</span>
<span class="prov">Claude · sonnet-4.5</span>
<span class="convo-link" data-open-tendril="t2">from anchored thread "is semver overkill?"</span>
<span class="state">Pending</span>
</div>
<div class="hunk-body">
<div class="line context"><span class="gutter"> </span><span class="content">OHM follows semantic versioning at the document level.</span></div>
<div class="line rem"><span class="gutter"></span><span class="content">Major bumps may break consumers; minor bumps add fields without breaking; patch bumps are clarifications.</span></div>
<div class="line add"><span class="gutter">+</span><span class="content">Major bumps may break consumers (renamed or removed fields); minor bumps add optional fields; patch bumps are clarifications that don't change the schema's shape.</span></div>
<div class="line add"><span class="gutter">+</span><span class="content">Consumers MUST tolerate unknown optional fields; producers MUST NOT emit major versions without a written migration note in the RFC's repo.</span></div>
</div>
<div class="hunk-actions">
<span>Proposed by Claude · 3h ago</span>
<span class="spacer"></span>
<button class="btn btn-accept">Accept</button>
<button class="btn">Edit</button>
<button class="btn btn-decline">Decline</button>
</div>
<div class="anchor-bubble" data-open-tendril="t2">
<div class="who"><span class="avatar">C</span> is semver overkill?</div>
<div class="preview">"do we really need MAJOR/MINOR/PATCH for a schema this small?" — discussion led to a more explicit producer/consumer contract…</div>
<div class="footer">
<span>9 msgs</span>
<span class="pending">1 pending</span>
</div>
</div>
</div>
<div class="ctx">
<p><span class="loc">§4 ¶2</span>The current version is <code>v1</code>. The version is encoded as a single integer prefix (<code>v1</code>, <code>v2</code>, …) at the top of the file rather than embedded in the schema URI, so that consumers can read it without parsing.</p>
</div>
<div class="skip-region">▴ §4 ¶2 unchanged · expand</div>
<div class="hunk">
<div class="hunk-header">
<span class="loc">§4 ¶3</span>
<span class="prov">Claude · gemini-2.5</span>
<span class="convo-link" data-open-tendril="t3">from anchored thread "migration ergonomics"</span>
<span class="state">Pending</span>
</div>
<div class="hunk-body">
<div class="line add"><span class="gutter">+</span><span class="content">Migration scripts live alongside the RFC in <code>.ohm/migrations/&lt;from&gt;-&gt;&lt;to&gt;.py</code>. The bot runs them on graduation.</span></div>
</div>
<div class="hunk-actions">
<span>Proposed by Claude · 1h ago</span>
<span class="spacer"></span>
<button class="btn btn-accept">Accept</button>
<button class="btn">Edit</button>
<button class="btn btn-decline">Decline</button>
</div>
<div class="anchor-bubble" data-open-tendril="t3">
<div class="who"><span class="avatar">C</span> migration ergonomics</div>
<div class="preview">Selection asked: "how does a downstream tool know there's a migration to run?" — Claude proposed convention…</div>
<div class="footer">
<span>3 msgs</span>
<span class="pending">1 pending</span>
</div>
</div>
</div>
<div class="ctx">
<p><span class="loc">§4 ¶4</span>A version bump is a commit to this RFC's repo. The commit message MUST start with <code>v&lt;n&gt;:</code> and the body MUST summarize the changes in human-readable terms. Tooling depends on this format.</p>
</div>
<div class="skip-region">▴ §4 ¶4 unchanged · expand</div>
<!-- ── §5 Migration ── -->
<div class="ctx">
<h2>5. Migration from <code>profile.yaml</code></h2>
</div>
<div class="hunk">
<div class="hunk-header">
<span class="loc">§5 ¶1</span>
<span class="prov">Claude · sonnet-4.5</span>
<span class="state declined">Declined</span>
</div>
<div class="hunk-body">
<div class="line rem"><span class="gutter"></span><span class="content">The migration tool reads the legacy <code>profile.yaml</code> and emits a v1 OHM record, preserving unknown fields under a <code>legacy:</code> key.</span></div>
<div class="line add" style="opacity:0.5"><span class="gutter">+</span><span class="content">The migration tool DELETES the legacy profile.yaml after emitting the v1 OHM record. Legacy fields are dropped.</span></div>
</div>
<div class="hunk-actions">
<span>Declined by ben · 2h ago · "we want non-destructive migration"</span>
<span class="spacer"></span>
<button class="btn">↶ Reconsider</button>
</div>
</div>
<div class="ctx">
<p><span class="loc">§5 ¶2</span>The tool runs once per contributor, idempotently. Re-running it on an account that's already migrated is a no-op: the tool reads the existing v1 record, validates it, and exits.</p>
<p><span class="loc">§5 ¶3</span>Failure modes are noisy by design. A malformed legacy file aborts the migration with a clear error pointing at the offending line; nothing is half-migrated.</p>
<p><span class="loc">§5 ¶4</span>For accounts that never had a <code>profile.yaml</code>, the tool seeds a minimal v1 record with only the <code>handle</code> field populated from the Gitea login.</p>
</div>
<div class="skip-region">▴ §5 ¶24 unchanged · 3 paragraphs · expand</div>
</div>
</div>
</div>
<!-- ── State: Branch · Edit view ─────────────────────────────────── -->
<div data-state="branch-edit">
<div class="main-sub">
<span class="ref-pill branch">⎇ ben/clarify-graduation</span>
<span class="cmp"><span class="arrow"></span> compared to <b style="color:var(--ink);font-weight:500">main</b></span>
<span class="sub-spacer"></span>
<div class="view-toggle">
<button data-toggle="branch-diff">Diff</button>
<button class="active" data-toggle="branch-edit">Edit</button>
</div>
<button class="btn">Open PR ▸</button>
</div>
<div class="main-body">
<div class="doc">
<div class="diff-summary">
<span class="count">Editing on ben/clarify-graduation</span>
<span>·</span>
<span>Inline track-changes shown · click a pin to open its conversation</span>
<span class="sub-spacer" style="flex:1"></span>
<span style="color:var(--add)">2 accepted</span>
<span style="color:var(--warn-ink)">2 pending in the diff view</span>
</div>
<div class="edit-doc">
<h2>3. Schema</h2>
<p>
An OHM record is a YAML document with a frontmatter block of canonical
fields and an open free-form body. The canonical fields are
<span class="tracked-delete">name</span><span class="tracked-insert">display_name</span><span class="anchor-pin" data-open-tendril="t1">1</span>,
<code>handle</code>, <code>pronouns</code>, <span class="tracked-delete">and </span><code>bio</code><span class="tracked-insert">, and <code>established_at</code></span>.
<span class="tracked-insert">The <code>display_name</code> rename clarifies that this field is the human-facing label, distinct from the immutable <code>handle</code>.</span>
</p>
<p>
<span class="tracked-insert">A record's <code>established_at</code> is the date the contributor first claimed this OHM, expressed as ISO-8601. Subsequent edits do not bump it.</span>
</p>
<h2>4. Versioning</h2>
<p>
OHM follows semantic versioning at the document level. Major bumps may
break consumers; minor bumps add fields without breaking; patch bumps
are clarifications.
<span class="anchor-pin" data-open-tendril="t2">2</span>
<em style="color:var(--warn-ink);font-size:12px">— 1 pending proposal in this paragraph</em>
</p>
<p>
Migration scripts live alongside the RFC.
<span class="anchor-pin" data-open-tendril="t3">3</span>
<em style="color:var(--warn-ink);font-size:12px">— 1 pending proposal</em>
</p>
<h2>5. Migration from profile.yaml</h2>
<p>
The migration tool reads the legacy <code>profile.yaml</code> and emits
a v1 OHM record, preserving unknown fields under a <code>legacy:</code>
key.
</p>
</div>
</div>
</div>
</div>
<!-- ── State: PR ─────────────────────────────────────────────────── -->
<div data-state="pr">
<div class="main-sub">
<span class="ref-pill pr">⇪ PR #4</span>
<span style="font-size:13px;color:var(--ink)">"Split §5 into §5 / §5a"</span>
<span class="cmp"><span class="arrow"></span> alice → <b style="color:var(--ink);font-weight:500">main</b></span>
<span class="sub-spacer"></span>
<div class="view-toggle">
<button class="active" data-toggle="pr">Diff</button>
<button data-toggle="pr-edit-disabled" title="Fork to edit">Edit (fork)</button>
</div>
<button class="btn primary">Merge PR</button>
</div>
<div class="main-body" id="pr-body">
<div class="doc">
<div class="pr-banner">
<b>PR #4 by alice</b> · opened 1d ago · 2 reviewers · all conversations resolved · <a href="#">discussion thread</a>
<span class="sub-spacer" style="flex:1"></span>
<button class="btn">Fork this PR to edit</button>
</div>
<div class="diff-summary">
<span class="count">8 changes</span>
<span class="add">+47 lines</span>
<span class="rem">12 lines</span>
<span>·</span>
<span><b style="color:var(--add)">All 8 accepted</b> on the branch</span>
<div class="scope-toggle" data-scope-target="pr-body">
<button class="active" data-scope="full">Full doc</button>
<button data-scope="changes">Changes only</button>
</div>
</div>
<div class="ctx">
<h2>1. Why this RFC exists</h2>
<p><span class="loc">§1 ¶1</span>The Open Human Model (OHM) is the schema that lets a contributor describe themselves to the rest of the system in a way that other tools can read uniformly.</p>
<h2>2. Scope</h2>
<p><span class="loc">§2 ¶1</span>This RFC defines the on-disk shape of an OHM record, its required and optional fields, the versioning policy, and the migration path from the prior ad-hoc <code>profile.yaml</code>.</p>
<h2>3. Schema</h2>
<p><span class="loc">§3 ¶1</span>An OHM record is a YAML document with a frontmatter block of canonical fields and an open free-form body. The canonical fields are <code>display_name</code>, <code>handle</code>, <code>pronouns</code>, <code>bio</code>, and <code>established_at</code>.</p>
<h2>4. Versioning</h2>
<p><span class="loc">§4 ¶1</span>OHM follows semantic versioning at the document level. Major bumps may break consumers; minor bumps add optional fields; patch bumps are clarifications.</p>
</div>
<div class="skip-region">▴ §14 unchanged on this PR · expand</div>
<div class="hunk">
<div class="hunk-header">
<span class="loc">§5 → §5 + §5a (split)</span>
<span class="prov">Claude · opus-4</span>
<span class="convo-link" data-open-tendril="t4">from anchored thread "section is too long"</span>
<span class="state accepted">Accepted on branch</span>
</div>
<div class="hunk-body">
<div class="line rem"><span class="gutter"></span><span class="content">## 5. Migration from profile.yaml</span></div>
<div class="line rem"><span class="gutter"></span><span class="content">The migration tool reads the legacy profile.yaml and emits a v1 OHM record…</span></div>
<div class="line add"><span class="gutter">+</span><span class="content">## 5. Migration: legacy → v1</span></div>
<div class="line add"><span class="gutter">+</span><span class="content">Migrating an existing <code>profile.yaml</code> happens once per contributor, on first sign-in after this RFC ships.</span></div>
<div class="line add"><span class="gutter">+</span><span class="content"></span></div>
<div class="line add"><span class="gutter">+</span><span class="content">## 5a. Migration tool reference</span></div>
<div class="line add"><span class="gutter">+</span><span class="content">The tool reads the legacy file, emits a v1 OHM record, and preserves unknown fields under a <code>legacy:</code> key.</span></div>
</div>
<div class="hunk-actions">
<span>Conversation resolved · approved by raj</span>
<span class="spacer"></span>
<button class="btn">View convo</button>
</div>
</div>
<div class="ctx">
<p><span class="loc">§5a ¶2</span>The tool runs once per contributor, idempotently. Re-running it on an account that's already migrated is a no-op.</p>
<p><span class="loc">§5a ¶3</span>Failure modes are noisy by design. A malformed legacy file aborts the migration with a clear error pointing at the offending line; nothing is half-migrated.</p>
<h2>6. Open questions</h2>
<p><span class="loc">§6 ¶1</span>Should the migration tool be runnable offline, or only at first sign-in? Leaving as future work.</p>
</div>
<div class="skip-region">▴ §5a ¶23 + §6 unchanged · expand</div>
</div>
</div>
</div>
</main>
<!-- ── Right pane ────────────────────────────────────────────────── -->
<aside class="right" id="right-pane" data-mode="list">
<div class="right-tabs">
<button class="active" data-rtab="convos">Conversations <span class="count">4</span></button>
<button data-rtab="changes">Changes <span class="count">5</span></button>
</div>
<div class="right-body">
<!-- Mode: tendril list -->
<div class="branch-thread">
<span class="icon">💬</span>
<span class="label">Branch chat</span>
<span class="meta">2 msgs · whole-doc context</span>
</div>
<div class="tendril-list">
<div class="tendril-item active" data-tendril="t1">
<div class="anchor">§3 ¶2 · 24 chars selected</div>
<div class="quote">"The canonical fields are name, handle, pronouns…"</div>
<div class="summary">"the name field is ambiguous — display label or stable id?" — Claude proposed renaming + adding established_at field.</div>
<div class="meta">
<span>4 msgs</span>
<span class="accepted">1 accepted</span>
<span>·</span>
<span>4h ago</span>
</div>
</div>
<div class="tendril-item" data-tendril="t2">
<div class="anchor">§4 ¶1 · 86 chars selected</div>
<div class="quote">"Major bumps may break consumers; minor bumps…"</div>
<div class="summary">"do we really need MAJOR/MINOR/PATCH for a schema this small?" — discussion led to a more explicit producer/consumer contract.</div>
<div class="meta">
<span>9 msgs</span>
<span class="pending">1 pending</span>
<span>·</span>
<span>3h ago</span>
</div>
</div>
<div class="tendril-item" data-tendril="t3">
<div class="anchor">§4 ¶3 · 0 chars (paragraph)</div>
<div class="summary">"how does a downstream tool know there's a migration to run?" — Claude proposed naming convention for migration scripts.</div>
<div class="meta">
<span>3 msgs</span>
<span class="pending">1 pending</span>
<span>·</span>
<span>1h ago</span>
</div>
</div>
<div class="tendril-item" data-tendril="t5">
<div class="anchor">§5 ¶1 · 42 chars selected</div>
<div class="summary">"should we ever delete the legacy file?" — Claude proposed destructive migration; declined by ben.</div>
<div class="meta">
<span>5 msgs</span>
<span style="color:var(--ink-4)">1 declined</span>
<span>·</span>
<span>2h ago</span>
</div>
</div>
</div>
<!-- Mode: convo detail -->
<div class="convo-detail">
<div class="convo-header">
<button class="back" onclick="setRightMode('list')">← All conversations (4)</button>
<div class="anchor">§3 ¶2 — anchored to selection</div>
<div class="quote">"The canonical fields are name, handle, pronouns…"</div>
</div>
<div class="convo-messages">
<div class="msg user">
<div class="who"><span class="dot"></span>ben</div>
<div class="bubble">the name field here is ambiguous — is it the display label or the stable identifier? I keep tripping over it when writing downstream code.</div>
</div>
<div class="msg ai">
<div class="who"><span class="dot"></span>Claude <span class="model">sonnet-4.5</span></div>
<div class="bubble">Good catch — it's currently overloaded. Two clean options: (1) rename <code>name</code><code>display_name</code> and keep <code>handle</code> as the immutable id, or (2) add a separate <code>display_name</code> alongside <code>name</code> and deprecate <code>name</code>. Option 1 is cleaner since <code>name</code> is already unstable in practice.</div>
</div>
<div class="msg user">
<div class="who"><span class="dot"></span>ben</div>
<div class="bubble">go with (1). also — we never wrote down "when did this OHM start existing." add an <code>established_at</code>.</div>
</div>
<div class="msg ai">
<div class="who"><span class="dot"></span>Claude <span class="model">sonnet-4.5</span></div>
<div class="bubble">Done. Proposed two coupled edits to §3 ¶2 — the rename plus the new field, with a one-line note explaining the rename.</div>
<span class="change-chip accepted">✓ 1 change accepted · §3 ¶2</span>
</div>
</div>
<div class="compose">
<textarea placeholder="Continue this conversation (anchored to §3 ¶2)…"></textarea>
<div class="compose-row">
<span class="model-pill">sonnet-4.5</span>
<span class="spacer"></span>
<button class="btn">Propose more changes</button>
<button class="btn primary">Send</button>
</div>
</div>
</div>
</div>
</aside>
</div>
</div>
<script>
// ── State switching ───────────────────────────────────────────────────
const stateBtns = document.querySelectorAll('.state-btn');
const panels = document.querySelectorAll('[data-state]');
const right = document.getElementById('right-pane');
const crumbRef = document.getElementById('crumb-ref');
const crumbSep = document.getElementById('crumb-sep');
const REF_LABELS = {
'root': '',
'branch-diff': '⎇ ben/clarify-graduation',
'branch-edit': '⎇ ben/clarify-graduation',
'pr': '⇪ PR #4 — split §5',
};
function setState(target) {
panels.forEach(p => p.classList.toggle('active', p.dataset.state === target));
stateBtns.forEach(b => b.classList.toggle('active', b.dataset.target === target));
const ref = REF_LABELS[target] || '';
crumbRef.textContent = ref;
crumbSep.style.display = ref ? '' : 'none';
// Reset right pane to list mode when state changes
setRightMode('list');
}
stateBtns.forEach(b => b.addEventListener('click', () => setState(b.dataset.target)));
// ── View toggle (Diff <-> Edit on branch) ─────────────────────────────
document.querySelectorAll('[data-toggle]').forEach(b => {
b.addEventListener('click', () => {
const t = b.dataset.toggle;
if (t === 'pr-edit-disabled') return;
setState(t);
});
});
// ── Tree-row jump links ───────────────────────────────────────────────
document.querySelectorAll('[data-jump]').forEach(el => {
el.addEventListener('click', e => { e.preventDefault(); setState(el.dataset.jump); });
});
// ── Scope toggle (full doc ↔ changes only, inside a diff body) ────────
document.querySelectorAll('.scope-toggle').forEach(t => {
const targetId = t.dataset.scopeTarget;
const target = document.getElementById(targetId);
t.querySelectorAll('button').forEach(b => {
b.addEventListener('click', () => {
t.querySelectorAll('button').forEach(x => x.classList.toggle('active', x === b));
if (target) target.classList.toggle('changes-only', b.dataset.scope === 'changes');
});
});
});
// ── Tendril open ──────────────────────────────────────────────────────
function setRightMode(mode) { right.dataset.mode = mode; }
document.querySelectorAll('[data-open-tendril]').forEach(el => {
el.addEventListener('click', e => {
e.stopPropagation();
setRightMode('convo');
});
});
// Init
setState('branch-diff');
</script>
</body>
</html>