constitution · epochs · watch · epoch 3

comparison

c_7ec4b410de02 (tommy-mor) vs c_64faa3bee86f (tommy-mor)

download prompt · raw event · cmp_5605c8b79e9c62

council reasoning

~anthropic/claude-sonnet-latest · winner B · 6:4 · permalink

Side B unifies the auth flow with the shared layout/JS pattern (removing a bespoke redirect-based layout, adding inline error/success fragments and generalizing the poem-JS morph behavior), which is a real architectural simplification with broader reuse. Side A is a solid, well-tested UX addition (unpin from ranked list) but is more narrowly scoped and mostly wiring/CSS/test changes rather than structural improvement.

~x-ai/grok-latest · winner B · 3:2 · permalink

B redesigns auth POST handling to return HTML fragments and extends the shared poem form interceptor to morph non-empty responses, replacing redirect round-trips with inline error/success—structural UX that affects the whole auth path. A only wires an existing set_garden_pin clear action onto ranked-child pin glyphs (plus CSS/tests), a useful but localized consistency fix.

openai/gpt-chat-latest · winner B · 3:2 · permalink

Side B changes the auth flow architecture by returning HTML fragments instead of redirects, refactoring the username form into reusable rendering helpers, and extending the shared Poem JavaScript to morph a form's innerHTML when a POST returns HTML, enabling inline form updates without page reloads while preserving existing empty-response behavior. Side A is a solid UX improvement that makes ranked child-group pin icons submit the existing unpin action and adds browser coverage, but it is a narrower feature built on an existing flow rather than a reusable interaction pattern.

sides

A — c_7ec4b410de02 (tommy-mor)

message

[02cd761c] Make ranked child group pin icons clickable to unpin.

Lets users clear the garden pin from the ranked child groups list via the same POST /ui set_garden_pin flow as the HUD and item header.

Co-authored-by: Cursor <cursoragent@cursor.com>

diff preview

diff --git a/server/src/html/garden/pin.rs b/server/src/html/garden/pin.rs
index 1820d7fe7ee167ecbf5ad5124f23b2ab92ffb01f..b7ea92bbe67d6d499915edb607a3a0b36178d7f6 100644
--- a/server/src/html/garden/pin.rs
+++ b/server/src/html/garden/pin.rs
@@ -101,6 +101,7 @@ pub(super) fn child_row_pin_or_vote(
     row_item: &ItemId,
     pinned_room_and_item: Option<&(String, ItemId)>,
     scope_content: &ContentState,
+    next_path: &str,
 ) -> maud::Markup {
     let pin_matches_scope = pinned_room_and_item
         .map(|(r, _)| r == nav.room_wire.as_str())
@@ -108,12 +109,25 @@ pub(super) fn child_row_pin_or_vote(
     let pinned_item = pinned_room_and_item
         .filter(|_| pin_matches_scope)
         .map(|(_, i)| i);
+    let unpin_rpc = template_json_compact(&json!({
+        "action": "set_garden_pin",
+        "clear": true,
+        "room_wire": "",
+        "next": next_path,
+        "form_action": "/ui",
+    }))
+    .expect("unpin rpc json");
 
     html! {
         @if let Some(pi) = pinned_item {
             span class="ont-garden-child-actions" data-garden-room=(nav.room_wire.as_str()) {
                 @if pi == row_item {
-                    span class="ont-garden-pinned-here" title="Pinned" aria-label="Pinned" { "📌" }
+                    form method="POST" action="/ui" data-navigate="full" class="ont-garden-pin-form" {
+                        input type="hidden" name=(UI_RPC_FIELD) value=(unpin_rpc);
+                        button type="submit" class="ont-garden-pin-ico ont-garden-pin-ico-active" title="Unpin" aria-label="Unpin from HUD" {
+                            span class="ont-garden-pin-glyph" aria-hidden="true" { "📌" }
+                        }
+                    }
                 } @else {
                     @let nv = edge_vote_count_for_pair(scope_content, pi, row_item);
                     @let tip = format!(
diff --git a/server/src/html/garden/render.rs b/server/src/html/garden/render.rs
index bd8cbce059ee789de6e8dc9b6f69f54beee2f994..7bfaadc0786b734db8973c53b82296a188d26e22 100644
--- a/server/src/html/garden/render.rs
+++ b/server/src/html/garden/render.rs
@@ -216,7 +216,7 @@ pub(super) async fn render_scope_view(
                                     @let item_url = item_href(r.item.as_str(), &nav);
                                     @let score_str = format!("{:.3}", r.score);
                                     li data-garden-item=(r.item.as_str()) {
-                                        (child_row_pin_or_vote(&nav, &r.item, pin_ref.as_ref(), scope_content))
+                                        (child_row_pin_or_vote(&nav, &r.item, pin_ref.as_ref(), scope_content, &next_for_pin))
                                         a class="item-link" href=(item_url) { code { (item_display_path(r.item.as_str())) } }
                                         span class="ont-rank-score" { (score_str) }
                                     }
@@ -232,7 +232,7 @@ pub(super) async fn render_scope_view(
                         ul class="ont-group-list" {
                             @for name in &model.child_rankings.unranked_items {
                                 li data-garden-item=(name.as_str()) {
-                                    (child_row_pin_or_vote(&nav, name, pin_ref.as_ref(), scope_content))
+                                    (child_row_pin_or_vote(&nav, name, pin_ref.as_ref(), scope_content, &next_for_pin))
                                     @let href = item_href(name.as_str(), &nav);
                                     a class="item-link" href=(href) { code { (item_display_path(name.as_str())) } }
                                 }
diff --git a/server/static/theme_default.css b/server/static/theme_default.css
index 2dabf3a7094cecd89d0c9674d27efe30cd01c010..b846ab86cb6eb5fb56be87efb054fb8b18082583 100644
--- a/server/static/theme_default.css
+++ b/server/static/theme_default.css
@@ -891,8 +891,7 @@ button.ont-pin-btn-active {
   vertical-align: middle;
 }
 button.ont-garden-pin-ico,
-a.ont-garden-vote-ico,
-span.ont-garden-pinned-here {
+a.ont-garden-vote-ico {
   font-size: 13px;
   line-height: 1;
   padding: 2px 6px;
@@ -910,6 +909,11 @@ span.ont-garden-pinned-here {
   align-items: center;
   justify-content: center;
 }
+form.ont-garden-pin-form {
+  display: inline;
+  margin: 0;
+  padding: 0;
+}
 a.ont-garden-vote-ico {
   gap: 4px;
 }
@@ -925,10 +929,14 @@ a.ont-garden-vote-ico:hover {
 span.ont-garden-vote-glyph {
   line-height: 1;
 }
-span.ont-garden-pinned-here {
+button.ont-garden-pin-ico-active {
   border-color: var(--link);
   color: var(--signal);
-  cursor: default;
+  background: color-mix(in srgb, var(--link) 12%, var(--g3));
+}
+button.ont-garden-pin-ico-active:hover {
+  color: var(--signal);
+  background: color-mix(in srgb, var(--link) 18%, var(--g4));
 }
 button.ont-garden-pin-ico:focus-visible {
   outline: 2px solid var(--link);
diff --git a/server/static/theme_retro_craft.css b/server/static/theme_retro_craft.css
index bdd9031b82154a5019782e6dcb8bf7589fafce09..5d4954812ad43f2ee7e63c71c7f0ae0b3e3fc95b 100644
--- a/server/static/theme_retro_craft.css
+++ b/server/static/theme_retro_craft.css
@@ -711,8 +711,7 @@ body.view-ontology span.ont-garden-vote-glyph {
   line-height: 1;
 }
 body.view-ontology button.ont-garden-pin-ico,
-body.view-ontology a.ont-garden-vote-ico,
-body.view-ontology span.ont-garden-pinned-here {
+body.view-ontology a.ont-garden-vote-ico {
   font-size: 0.95rem;
   line-height: 1;
   padding: 0.1rem 0.35rem;
@@ -732,10 +731,14 @@ body.view-ontology a.ont-garden-vote-ico:hover {
   border-color: #a68e6b;
   background: #ebe6dc;
 }
-body.view-ontology span.ont-garden-pinned-here {
+body.view-ontology button.ont-garden-pin-ico-active {
   border-color: #1a4a8c;
   background: color-mix(in srgb, #1a4a8c 10%, #f7f3eb);
-  cursor: default;
+  color: #0d2d5c;
+}
+body.view-ontology button.ont-garden-pin-ico-active:hover {
+  border-color: #a68e6b;
+  background: color-mix(in srgb, #1a4a8c 14%, #ebe6dc);
 }
 body.view-ontology form.ont-garden-pin-form {
   display: inline;
diff --git a/test/browser_garden_pin.clj b/test/browser_garden_pin.clj
index d4fa9ed60b45e872ec60e29307cfa61bcd9dd6b2..579976f4c2493a149f9eba081862ca07919f4fe9 100644
--- a/test/browser_garden_pin.clj
+++ b/test/browser_garden_pin.clj
@@ -58,10 +58,11 @@
      (let [alice-token (oauth/fetch-bearer-token! base-url :username "alice")
            thread-tag "browser-garden-pin"
            raw (str "# " thread-tag "\n\n"
-                    "~/gp-pin-a {alpha}\n"
-                    "~/gp-pin-b {beta}\n"
+                    "~/gp-parent {parent}\n"
+                    "~/gp-parent/pin-a {alpha}\n"
+                    "~/gp-parent/pin-b {beta}\n"
                     "{pin test vote}\n"
-                    "~/gp-pin-a 2:1 ~/gp-pin-b\n")
+                    "~/gp-parent/pin-a 2:1 ~/gp-parent/pin-b\n")
            post-resp (oauth/http-post-json
                       (str base-url "/api/v0/rpc")
                       [{"Post" {"room" "public"
@@ -77,23 +78,43 @@
              (core/with-page [pg (core/new-page-from-context ctx)]
                (page/navigate pg (str base-url "/login"))
                (is (wait-for-text pg "body" "@alice" 15000) "alice session after login")
-               (page/navigate pg (str base-url "/~/gp-pin-a"))
-               (is (wait-for-text pg ".ont-item-shell" "~/gp-pin-a" 15000) "on item a page")
+               (page/navigate pg (str base-url "/~/gp-parent/pin-a"))
+               (is (wait-for-text pg ".ont-item-shell" "~/gp-parent/pin-a" 15000) "on item a page")
                ;; Native form POST /ui (data-navigate=full) — not fetch/eval
                (locator/click (page/locator pg ".ont-item-pin-zone form.ont-pin-form button[type=submit]"))
-               (is (wait-for-text pg "#slug-pin-hud" "gp-pin-a" 15000)
+               (is (wait-for-text pg "#slug-pin-hud" "gp-parent/pin-a" 15000)
                    "HUD shows pinned item label after redirect")
-               (page/navigate pg (str base-url "/~/gp-pin-b"))
-               (is (wait-for-text pg ".ont-item-shell" "~/gp-pin-b" 15000) "on item b page")
+               (page/navigate pg (str base-url "/~/gp-parent/pin-b"))
+               (is (wait-for-text pg ".ont-item-shell" "~/gp-parent/pin-b" 15000) "on item b page")
                (is (wait-for-text pg "a.ont-vote-compare-btn" "vote" 10000)
                    "vote link visible vs pinned item")
-               ;; HUD clears pin (POST set_garden_pin clear), not navigate to item
+               ;; Unpin from ranked child groups on parent page
+               (page/navigate pg (str base-url "/~/gp-parent"))
+               (is (wait-for-text pg ".ont-tab-panel-children" "ranked child groups" 15000)
+                   "parent page shows ranked child groups")
+               (is (wait-for-text pg ".ont-ranking-list button.ont-garden-pin-ico-active" "📌" 10000)
+                   "pinned row shows active pin in ranked list")
+               (locator/click (page/locator pg ".ont-ranking-list button.ont-garden-pin-ico-active"))
+               (is (wait-for-absence-substr pg "#slug-pin-hud" "gp-parent/pin-a" 15000)
+                   "HUD clears after unpin from ranked child list")
+               (is (wait-for-absence-substr pg ".ont-ranking-list" "ont-garden-vote-ico" 10000)
+                   "vote icons removed from ranked list after unpin")
+               (page/navigate pg (str base-url "/~/gp-parent/pin-b"))
+               (is (wait-for-text pg ".ont-item-shell" "~/gp-parent/pin-b" 15000) "on item b page again")
+               (is (wait-for-absence-substr pg "body" "ont-vote-compare-btn" 15000)
+                   "compare vote CTA removed after ranked-list unpin")
+               ;; HUD unpin still works from item page context
+               (page/navigate pg (str base-url "/~/gp-parent/pin-a"))
+               (locator/click (page/locator pg ".ont-item-pin-zone form.ont-pin-form button[type=submit]"))
+               (is (wait-for-text pg "#slug-pin-hud" "gp-parent/pin-a" 15000)
+                   "re-pin from item page for HUD unpin test")
+               (page/navigate pg (str base-url "/~/gp-parent/pin-b"))
                (locator/click (page/locator pg "#slug-pin-hud button.slug-pin-hud-unpin-btn"))
-               (is (wait-for-absence-substr pg "#slug-pin-hud" "gp-pin-a" 15000)
+               (is (wait-for-absence-substr pg "#slug-pin-hud" "gp-parent/pin-a" 15000)
                    "HUD clears after unpin from HUD button")
                (is (wait-for-absence-substr pg "body" "ont-vote-compare-btn" 15000)
                    "compare vote CTA removed after HUD unpin on same reload")
-               (is (str/includes? (or (page/url pg) "") "/~/gp-pin-b")
+               (is (str/includes? (or (page/url pg) "") "/~/gp-parent/pin-b")
                    "still on item b after HUD unpin"))))))
 
      (finally

download full diff A

B — c_64faa3bee86f (tommy-mor)

message

[6b6eb0c3] Auth form: poem JS morphs form innerHTML on response; no redirect

- post_choose_username returns HTML fragments instead of redirects:
  success → auth_signed_in_fragment ("you're signed in — return to your agent")
  error   → choose_username_error_fragment (form re-rendered with error inline)
- Poem JS now reads response body; if non-empty, morphs form innerHTML with it
  (existing ingest forms return empty body, so they're unaffected)
- auth.rs: keep full layout() with poem JS — revert to single layout
- auth-success CSS class added to both themes

Co-Authored-By: Claude Sonnet 4.6 (1M context) <noreply@anthropic.com>

diff preview

diff --git a/server/src/api/auth.rs b/server/src/api/auth.rs
index c1194f79fd89fb47fe6b425b494a0ffa667abb2d..cb0faa29b834931e2c2b2f5c174c875e2e2e9346 100644
--- a/server/src/api/auth.rs
+++ b/server/src/api/auth.rs
@@ -16,7 +16,7 @@ use crate::{
         canonicalize_username, validate_agent_format, validate_username,
         Event, TokenIssued, UserRegistered,
     },
-    html::{auth_complete_page, choose_username_page},
+    html::{auth_complete_page, auth_signed_in_fragment, choose_username_error_fragment, choose_username_page},
     state::{AppState, PendingSession},
 };
 
@@ -274,8 +274,6 @@ pub async fn post_choose_username(
         return api_error(StatusCode::BAD_REQUEST, "invalid agent format", Some(msg)).into_response();
     }
 
-    let public_url = std::env::var("SLUG_PUBLIC_URL").unwrap_or_else(|_| "http://127.0.0.1:8080".to_string());
-
     let reduced_arc = state.reduced.clone();
     let reduced = reduced_arc.read().await;
     let provider_key = (provider.to_lowercase(), provider_id.clone());
@@ -284,11 +282,7 @@ pub async fn post_choose_username(
     }
     if reduced.users_by_provider.values().any(|u| u == &canonicalize_username(&form.username)) {
         drop(reduced);
-        return Redirect::to(&format!(
-            "{public_url}/auth/choose-username?session={}&error={}",
-            urlencoding::encode(&form.session),
-            urlencoding::encode("that username is taken — try another"),
-        )).into_response();
+        return choose_username_error_fragment(&form.session, "that username is taken — try another").into_response();
     }
     drop(reduced);
 
@@ -324,7 +318,7 @@ pub async fn post_choose_username(
         s.complete = Some((canon_user.clone(), bearer.clone()));
     }
 
-    Redirect::to(&format!("{public_url}/auth/complete")).into_response()
+    auth_signed_in_fragment().into_response()
 }
 
 pub async fn post_pending_session(
diff --git a/server/src/html/auth.rs b/server/src/html/auth.rs
index 40b1ef30a6c1d4063aa2d8e9c93df8972df4b27c..0a14bdbfb66c65d1bd09993ffd4a7bb6f2fe041e 100644
--- a/server/src/html/auth.rs
+++ b/server/src/html/auth.rs
@@ -1,20 +1,25 @@
-use maud::{html, Markup, DOCTYPE};
+use maud::{html, Markup};
 
-/// Minimal layout for auth pages — no JS interceptor, real form navigation works.
-fn auth_layout(title: &str, body: Markup) -> Markup {
+fn form_inner(session: &str, error: Option<&str>) -> Markup {
     html! {
-        (DOCTYPE)
-        html {
-            head {
-                meta charset="utf-8";
-                meta name="viewport" content="width=device-width, initial-scale=1";
-                title { (title) }
-                link rel="stylesheet" href="/static/theme_default.css";
-            }
-            body class="view-auth" {
-                (body)
-            }
+        input type="hidden" name="session" value=(session);
+        label for="username" { "username" }
+        input
+            type="text"
+            id="username"
+            name="username"
+            placeholder="e.g. alice"
+            pattern="[a-z0-9_\\-]{1,32}"
+            maxlength="32"
+            autocomplete="off"
+            autofocus;
+        p.auth-hint {
+            "lowercase · alphanumeric · hyphens · underscores · max 32"
         }
+        @if let Some(msg) = error {
+            p.auth-error { (msg) }
+        }
+        button type="submit" { "continue" }
     }
 }
 
@@ -28,27 +33,23 @@ pub fn choose_username_page(session: &str, error: Option<&str>) -> Markup {
         h1 { "choose a username" }
         p { "pick a handle for slug.social." }
         form.auth-form method="POST" action="/auth/choose-username" {
-            input type="hidden" name="session" value=(session);
-            label for="username" { "username" }
-            input
-                type="text"
-                id="username"
-                name="username"
-                placeholder="e.g. alice"
-                pattern="[a-z0-9_\\-]{1,32}"
-                maxlength="32"
-                autocomplete="off"
-                autofocus;
-            p.auth-hint {
-                "lowercase · alphanumeric · hyphens · underscores · max 32"
-            }
-            @if let Some(msg) = error {
-                p.auth-error { (msg) }
-            }
-            button type="submit" { "continue" }
+            (form_inner(session, error))
         }
     };
-    auth_layout("join — slug.social", body)
+    super::layout("join — slug.social", "view-auth", body, None)
+}
+
+/// Fragment returned to the poem JS on error — replaces the form's innerHTML.
+pub fn choose_username_error_fragment(session: &str, error: &str) -> Markup {
+    form_inner(session, Some(error))
+}
+
+/// Fragment returned to the poem JS on success — replaces the form's innerHTML.
+pub fn auth_signed_in_fragment() -> Markup {
+    html! {
+        p.auth-success { "you're signed in — return to your agent." }
+        p.auth-hint { "you can close this tab." }
+    }
 }
 
 pub fn auth_complete_page() -> Markup {
@@ -62,5 +63,5 @@ pub fn auth_complete_page() -> Markup {
         p { "Return to your terminal — your agent is polling and will collect your token automatically." }
         p.auth-hint { "You can close this tab." }
     };
-    auth_layout("signed in — slug.social", body)
+    super::layout("signed in — slug.social", "view-auth", body, None)
 }
diff --git a/server/src/html/mod.rs b/server/src/html/mod.rs
index 2f16d701962d703db0c859bb586dfc08ec385690..8b48a25cce79f0eefc7e29849e1a667cae4c7986 100644
--- a/server/src/html/mod.rs
+++ b/server/src/html/mod.rs
@@ -15,7 +15,7 @@ mod search;
 mod tree;
 use breadcrumb_path::OntologyPath;
 
-pub use auth::{auth_complete_page, choose_username_page};
+pub use auth::{auth_complete_page, auth_signed_in_fragment, choose_username_error_fragment, choose_username_page};
 pub use editor::{editor_check, editor_page};
 pub use forum::{index, thread_feed_html, thread_post_expand, thread_post_view, thread_view};
 pub use garden::{garden_index, ontology_path};
@@ -114,6 +114,8 @@ script { (maud::PreEscaped(r#"
                         });
 
                         // Poem: intercept POST forms, send via fetch, await SSE for DOM update.
+                        // If the response body is non-empty HTML, morph the form's innerHTML with it
+                        // (used for inline feedback without a page reload, e.g. auth forms).
                         document.addEventListener('submit', async (e) => {
                             const f = e.target;
                             if (!f || f.tagName !== 'FORM') return;
@@ -121,14 +123,19 @@ script { (maud::PreEscaped(r#"
                             e.preventDefault();
                             const btn = f.querySelector('button[type="submit"], input[type="submit"]');
                             if (btn) { btn.disabled = true; btn.textContent = '…'; }
-                            await fetch(f.action, {
+                            const resp = await fetch(f.action, {
                                 method: 'POST',
                                 body: new URLSearchParams(new FormData(f)),
                                 headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
                                 credentials: 'same-origin',
                             });
-                            if (btn) { btn.disabled = false; btn.textContent = 'submit'; }
-                            f.reset();
+                            const html = await resp.text();
+                            if (html && html.trim()) {
+                                Idiomorph.morph(f, html, {morphStyle: 'innerHTML'});
+                            } else {
+                                if (btn) { btn.disabled = false; btn.textContent = 'submit'; }
+                                f.reset();
+                            }
                         });
 
                         // Search: debounced fetch + idiomorph.
diff --git a/server/static/theme_default.css b/server/static/theme_default.css
index 9e71574da4bed3a0116c347610636780678bd1af..a1d8d1765a7812191edc579125facf1694554c2c 100644
--- a/server/static/theme_default.css
+++ b/server/static/theme_default.css
@@ -250,6 +250,11 @@ p.auth-error {
   font-size: 12px;
   margin: 4px 0 0;
 }
+p.auth-success {
+  color: var(--signal);
+  font-size: 13px;
+  margin: 4px 0 0;
+}
 
 /* ----------------------------------------------------------------
    BUTTONS — raised, press on :active
diff --git a/server/static/theme_retro.css b/server/static/theme_retro.css
index dc9fa4654f529eb1843557fb580cf3982da46901..8ed8fd88efab32b36bd66cf200aa182219b0a8b5 100644
--- a/server/static/theme_retro.css
+++ b/server/static/theme_retro.css
@@ -32,6 +32,7 @@ input[type="text"] {
 input[type="text"]:focus { border-color: #00ff41; }
 p.auth-hint { color: #555; font-family: monospace; font-size: 0.75rem; margin: 0; }
 p.auth-error { color: #ff4444; font-family: monospace; font-size: 0.8rem; margin: 0; }
+p.auth-success { color: #00ff41; font-family: monospace; font-size: 0.8rem; margin: 0; }
 
 /* Ingest form (poem pattern) */
 .ingest-form-wrap { margin-top: 1.5rem; }

download full diff B

Hardlinks — judgments / attempts / prompt

prompt download

judgments

attempts

Prompt text is loaded only by the download route.