{"messages":[{"content":"You are a constitutional council ranking individual git commits for ownership allocation.\n\nCompare these two commits. Decide which contributed more lasting value to the project.\n\nJudge substance, not spectacle:\n- Prefer correct, lasting design and real bugfixes over churn, formatting, renames, or generated noise.\n- Prefer clarity and necessity over sheer line count. A small precise change can beat a large diffuse one.\n- Do not favor a side merely because its patch is longer or noisier.\n- Weight what the change does for the project, not the contributor's name.\n\nReturn ONLY a JSON object: {\"winner\": \"A\" or \"B\", \"ratio\": \"N:M\", \"explanation\": \"...\"}\nThe explanation must cite concrete differences in the patches (1-3 sentences).\n\nSide A — contributor: tommy-mor\nSide A — commit message:\n[dd76f7b1] trim bb.edn\n\nSide A — unified diff (full patch):\ndiff --git a/bb.edn b/bb.edn\nindex f4319094c7864757901ed60df4a87160b7a27e59..5d1d642d75d03f70879d7673e7ad95360336a8b1 100644\n--- a/bb.edn\n+++ b/bb.edn\n@@ -46,18 +46,7 @@\n \"PORT\" \"8080\"\n \"RUST_LOG\" \"info\"})})))}\n \n- test\n- {:doc \"HTTP integration tests via Kaocha (clojure -M:kaocha :http-integration). Full suite: clojure -M:kaocha.\"\n- :task (let [r (deref (p/process [\"clojure\" \"-M:kaocha\" \":http-integration\"] {:inherit true}))]\n- (when-not (zero? (:exit r)) (System/exit (:exit r))))}\n-\n- browser-ui-morph\n- {:doc \"Playwright (Spel): POST /ui __rpc__ DOM morph (expand_post_full). Requires: clojure CLI, Chrome, `npx playwright install chromium`.\"\n- :requires ([babashka.process :as p])\n- :task (let [r (deref (p/process [\"clojure\" \"-M\" \"-m\" \"test.runner\" \"browser-ui-morph\"] {:inherit true}))]\n- (when-not (zero? (:exit r)) (System/exit (:exit r))))}\n-\n- walkthrough-fixture\n+ fixture\n {:doc \"Run local server + mock OAuth + seeded walkthrough data for manual browser demos\"\n :requires ([test.walkthrough-fixture :as walkthrough-fixture])\n :task (walkthrough-fixture/run-fixture)}\n\n\nSide B — contributor: tommy-mor\nSide B — commit message:\n[deb79714] jsbuilder ofc\n\nSide B — unified diff (full patch):\ndiff --git a/mod.rs b/mod.rs\nnew file mode 100644\nindex 0000000000000000000000000000000000000000..b1dfe895515a1ba94304c52ac4ea82ff6bb84c16\n--- /dev/null\n+++ b/mod.rs\n@@ -0,0 +1,929 @@\n+use axum::{\n+ body::Body,\n+ extract::Path,\n+ http::{header, HeaderValue, StatusCode, Uri},\n+ response::{IntoResponse, Response},\n+ Form,\n+};\n+use axum_extra::extract::cookie::CookieJar;\n+use maud::{html, Markup, DOCTYPE};\n+use serde::Deserialize;\n+use std::collections::{HashMap, HashSet};\n+\n+mod auth;\n+mod breadcrumb_path;\n+mod editor;\n+mod forum;\n+mod garden;\n+pub mod routing;\n+mod search;\n+pub mod ui_action;\n+use breadcrumb_path::{ExternalOntologyPath, OntologyPath};\n+\n+pub use auth::{\n+ auth_complete_page, auth_signed_in_fragment, choose_username_error_fragment,\n+ choose_username_page,\n+};\n+pub use editor::{editor_check, editor_page};\n+pub use forum::{\n+ home, room_page, room_thread_post_view, room_thread_view, thread_feed_html,\n+ thread_feed_html_for_room, thread_feed_region_markup, thread_post_view, thread_view, ThreadNav,\n+};\n+\n+pub use forum::user_profile_page;\n+pub(crate) use forum::{\n+ fragment_new_thread_slot, login_to_post_hint_markup, room_members_section_markup,\n+ thread_ui_collapse_redacted_post, thread_ui_copy_thread, thread_ui_expand_post_full, thread_ui_expand_redacted_post,\n+ user_can_post_room, user_can_view_room,\n+};\n+pub(crate) use garden::{\n+ encode_pin_cookie_value, external_resolver_status_markup, vote_compare_post_success_js,\n+ GARDEN_PIN_COOKIE,\n+};\n+pub use garden::{\n+ external_garden_index, external_ontology_path, garden_index, ontology_path,\n+ room_external_garden_index, room_external_ontology_path, room_garden_index, room_ontology_path,\n+ room_vote_compare_page, vote_compare_page,\n+};\n+pub use routing::RouteContext;\n+pub use search::{search_page, search_results_fragment};\n+pub use ui_action::{parse_html_ui_from_form, HtmlUiAction, HtmlUiParseError, UI_RPC_FIELD};\n+\n+/// Public profile URL path for a stored username (no `@`).\n+pub(crate) fn profile_href(username: &str) -> String {\n+ format!(\"/u/{username}\")\n+}\n+\n+/// Cookie name for UI theme (must match document.cookie migration in [`layout`]).\n+pub const SLUG_THEME_COOKIE: &str = \"slug-theme\";\n+\n+/// Normalize a requested theme id to a known stylesheet key.\n+pub fn normalize_theme(raw: &str) -> &'static str {\n+ match raw {\n+ \"retro\" => \"retro\",\n+ \"retro_craft\" => \"retro_craft\",\n+ _ => \"default\",\n+ }\n+}\n+\n+/// Resolved theme for rendering and cookie re-issue.\n+pub fn theme_from_jar(jar: &CookieJar) -> &'static str {\n+ jar.get(SLUG_THEME_COOKIE)\n+ .map(|c| normalize_theme(c.value()))\n+ .unwrap_or(\"default\")\n+}\n+\n+/// `Path` + optional `?query` for round-tripping after `POST /theme`.\n+pub fn theme_next_from_uri(uri: &Uri) -> String {\n+ uri.path_and_query()\n+ .map(|pq| pq.as_str().to_string())\n+ .filter(|s| !s.is_empty())\n+ .unwrap_or_else(|| \"/\".to_string())\n+}\n+\n+/// `Set-Cookie` for a validated theme (ASCII cookie value).\n+pub fn theme_cookie_header_value(theme: &str) -> HeaderValue {\n+ let t = normalize_theme(theme);\n+ let s = format!(\"{SLUG_THEME_COOKIE}={t}; Path=/; SameSite=Lax; Max-Age=31536000\");\n+ HeaderValue::from_str(&s).expect(\"theme cookie must be ASCII\")\n+}\n+\n+/// Re-issue theme cookie on responses that also set `slug_session`, so login does not drop theme.\n+pub fn theme_cookie_header_from_jar(jar: &CookieJar) -> Option {\n+ let c = jar.get(SLUG_THEME_COOKIE)?;\n+ Some(theme_cookie_header_value(c.value()))\n+}\n+\n+fn sanitize_theme_next(next: Option<&str>) -> String {\n+ let s = next.unwrap_or(\"/\").trim();\n+ if s.starts_with('/') && !s.starts_with(\"//\") && s.len() < 8192 {\n+ s.to_string()\n+ } else {\n+ \"/\".to_string()\n+ }\n+}\n+\n+#[derive(Debug, Deserialize)]\n+pub struct ThemeForm {\n+ theme: String,\n+ next: Option,\n+}\n+\n+/// `POST /theme` — set theme cookie and redirect back (full navigation, no fetch).\n+pub async fn post_theme(Form(form): Form) -> impl IntoResponse {\n+ let theme = normalize_theme(&form.theme);\n+ let next = sanitize_theme_next(form.next.as_deref());\n+ let loc =\n+ HeaderValue::try_from(next.as_str()).unwrap_or_else(|_| HeaderValue::from_static(\"/\"));\n+ Response::builder()\n+ .status(StatusCode::SEE_OTHER)\n+ .header(header::LOCATION, loc)\n+ .header(header::SET_COOKIE, theme_cookie_header_value(theme))\n+ .body(Body::empty())\n+ .expect(\"theme redirect response\")\n+}\n+\n+// Embed CSS and shared UI script at compile time\n+const THEME_DEFAULT_CSS: &str = include_str!(\"../../static/theme_default.css\");\n+const THEME_RETRO_CSS: &str = include_str!(\"../../static/theme_retro.css\");\n+const THEME_RETRO_CRAFT_CSS: &str = include_str!(\"../../static/theme_retro_craft.css\");\n+const SLUG_UI_JS: &str = include_str!(\"../../static/slug_ui.js\");\n+\n+pub async fn serve_static(Path(filename): Path) -> impl IntoResponse {\n+ if filename == \"slug_ui.js\" {\n+ return Response::builder()\n+ .status(StatusCode::OK)\n+ .header(header::CONTENT_TYPE, \"text/javascript; charset=utf-8\")\n+ .header(header::CACHE_CONTROL, \"public, max-age=3600\")\n+ .body(SLUG_UI_JS.to_string())\n+ .unwrap()\n+ .into_response();\n+ }\n+\n+ let theme = filename\n+ .strip_prefix(\"theme_\")\n+ .and_then(|s| s.strip_suffix(\".css\"));\n+\n+ let css = match theme {\n+ Some(\"default\") => THEME_DEFAULT_CSS,\n+ Some(\"retro\") => THEME_RETRO_CSS,\n+ Some(\"retro_craft\") => THEME_RETRO_CRAFT_CSS,\n+ _ => return (StatusCode::NOT_FOUND, \"static file not found\").into_response(),\n+ };\n+\n+ Response::builder()\n+ .status(StatusCode::OK)\n+ .header(header::CONTENT_TYPE, \"text/css; charset=utf-8\")\n+ .header(header::CACHE_CONTROL, \"public, max-age=3600\")\n+ .body(css.to_string())\n+ .unwrap()\n+ .into_response()\n+}\n+\n+pub(crate) fn js_string_literal(s: &str) -> String {\n+ serde_json::to_string(s).expect(\"javascript string escaping\")\n+}\n+\n+/// `console.warn` as `text/javascript` — for `POST /ui` parse errors and inline morph failures.\n+pub(crate) fn ui_js_warn(msg: &str) -> Response {\n+ let js = format!(\"console.warn({});\", js_string_literal(msg));\n+ Response::builder()\n+ .status(StatusCode::OK)\n+ .header(header::CONTENT_TYPE, \"text/javascript; charset=utf-8\")\n+ .body(Body::from(js))\n+ .unwrap()\n+}\n+\n+pub(crate) struct JsBuilder {\n+ snippets: Vec,\n+}\n+\n+pub(crate) struct JsQueryBuilder {\n+ builder: JsBuilder,\n+ expr: String,\n+}\n+\n+impl JsBuilder {\n+ pub(crate) fn new() -> Self {\n+ Self {\n+ snippets: Vec::new(),\n+ }\n+ }\n+\n+ pub(crate) fn morph_selector(self, selector: &str, markup: Markup) -> Self {\n+ self.morph_expr(\n+ &format!(\"document.querySelector({})\", js_string_literal(selector)),\n+ markup,\n+ None,\n+ )\n+ }\n+\n+ /// Morph **children** of `selector` so the outer element (e.g. `#new-thread-ui-slot`) keeps its `id`.\n+ pub(crate) fn morph_inner_selector(self, selector: &str, markup: Markup) -> Self {\n+ self.qs(selector).morph_inner(markup)\n+ }\n+\n+ pub(crate) fn morph_expr(\n+ mut self,\n+ expr: &str,\n+ markup: Markup,\n+ morph_style: Option<&str>,\n+ ) -> Self {\n+ let html = js_string_literal(&markup.into_string());\n+ let opts = morph_style\n+ .map(|style| format!(\", {{morphStyle: {}}}\", js_string_literal(style)))\n+ .unwrap_or_default();\n+ self.snippets.push(format!(\n+ \"var __slugEl = {expr}; if (__slugEl) {{ Idiomorph.morph(__slugEl, {html}{opts}); }}\",\n+ ));\n+ self\n+ }\n+\n+ pub(crate) fn qs(self, selector: &str) -> JsQueryBuilder {\n+ JsQueryBuilder {\n+ builder: self,\n+ expr: format!(\"document.querySelector({})\", js_string_literal(selector)),\n+ }\n+ }\n+\n+ pub(crate) fn id(self, id: &str) -> JsQueryBuilder {\n+ self.qs(&format!(\"#{id}\"))\n+ }\n+\n+ pub(crate) fn if_current_path_matches(\n+ mut self,\n+ path: &str,\n+ f: impl FnOnce(JsBuilder) -> JsBuilder,\n+ ) -> Self {\n+ let inner = f(JsBuilder::new()).build();\n+ self.snippets.push(format!(\n+ \"var __slugHere = window.location.pathname + window.location.search; var __slugPath = {path}; if (__slugHere === __slugPath || __slugHere.indexOf(__slugPath + '?') === 0) {{ {inner} }}\",\n+ path = js_string_literal(path),\n+ ));\n+ self\n+ }\n+\n+ pub(crate) fn if_current_path_not_matches(\n+ mut self,\n+ path: &str,\n+ f: impl FnOnce(JsBuilder) -> JsBuilder,\n+ ) -> Self {\n+ let inner = f(JsBuilder::new()).build();\n+ self.snippets.push(format!(\n+ \"var __slugHere = window.location.pathname + window.location.search; var __slugPath = {path}; if (!(__slugHere === __slugPath || __slugHere.indexOf(__slugPath + '?') === 0)) {{ {inner} }}\",\n+ path = js_string_literal(path),\n+ ));\n+ self\n+ }\n+\n+ pub(crate) fn redirect(mut self, to: &str) -> Self {\n+ self.snippets\n+ .push(format!(\"window.location = {};\", js_string_literal(to)));\n+ self\n+ }\n+\n+ pub(crate) fn clipboard_write_text_and_label_btn(\n+ mut self,\n+ text: &str,\n+ btn_id: &str,\n+ copied_label: &str,\n+ ) -> Self {\n+ self.snippets.push(format!(\n+ \"navigator.clipboard.writeText({text}).then(function(){{ var __slugCopyBtn = document.getElementById({btn_id}); if (__slugCopyBtn) {{ __slugCopyBtn.textContent = {label}; }} }}).catch(function(__slugErr){{ console.warn(__slugErr); }});\",\n+ text = js_string_literal(text),\n+ btn_id = js_string_literal(btn_id),\n+ label = js_string_literal(copied_label),\n+ ));\n+ self\n+ }\n+\n+ /// Focus first matching element (e.g. after morphing open a compose form).\n+ pub(crate) fn focus_selector(mut self, selector: &str) -> Self {\n+ self.snippets.push(format!(\n+ \"var __slugF = document.querySelector({}); if (__slugF && __slugF.focus) {{ __slugF.focus(); }}\",\n+ js_string_literal(selector),\n+ ));\n+ self\n+ }\n+\n+ pub(crate) fn build(self) -> String {\n+ self.snippets.join(\" \")\n+ }\n+\n+ pub(crate) fn into_response(self) -> Response {\n+ Response::builder()\n+ .status(StatusCode::OK)\n+ .header(header::CONTENT_TYPE, \"text/javascript; charset=utf-8\")\n+ .body(Body::from(self.build()))\n+ .unwrap()\n+ }\n+}\n+\n+impl JsQueryBuilder {\n+ pub(crate) fn morph(mut self, markup: Markup) -> JsBuilder {\n+ let html = js_string_literal(&markup.into_string());\n+ self.builder.snippets.push(format!(\n+ \"var __slugTarget = {expr}; if (__slugTarget) {{ Idiomorph.morph(__slugTarget, {html}); }}\",\n+ expr = self.expr,\n+ ));\n+ self.builder\n+ }\n+\n+ pub(crate) fn morph_inner(mut self, markup: Markup) -> JsBuilder {\n+ let html = js_string_literal(&markup.into_string());\n+ self.builder.snippets.push(format!(\n+ \"var __slugTarget = {expr}; if (__slugTarget) {{ Idiomorph.morph(__slugTarget, {html}, {{morphStyle: 'innerHTML'}}); }}\",\n+ expr = self.expr,\n+ ));\n+ self.builder\n+ }\n+\n+ pub(crate) fn reset(mut self) -> JsBuilder {\n+ self.builder.snippets.push(format!(\n+ \"var __slugTarget = {expr}; if (__slugTarget) {{ __slugTarget.reset(); }}\",\n+ expr = self.expr,\n+ ));\n+ self.builder\n+ }\n+}\n+\n+#[allow(clippy::too_many_arguments)]\n+pub(super) fn layout(\n+ title: &str,\n+ view: &str,\n+ body: Markup,\n+ views: Option,\n+ theme: &str,\n+ theme_next: &str,\n+ garden_room_wire: Option<&str>,\n+ garden_path_prefix: Option<&str>,\n+) -> Markup {\n+ layout_embed_controls(\n+ title,\n+ view,\n+ body,\n+ views,\n+ theme,\n+ theme_next,\n+ garden_room_wire,\n+ garden_path_prefix,\n+ true,\n+ )\n+}\n+\n+/// Minimal document shell: no bottom controls, no garden HUD data attributes (`data-garden-room` /\n+/// `data-garden-prefix` empty). For routes that own the full viewport (e.g. vote compare).\n+pub(super) fn layout_full_bleed_chromeless(\n+ title: &str,\n+ view: &str,\n+ body: Markup,\n+ views: Option,\n+ theme: &str,\n+ theme_next: &str,\n+) -> Markup {\n+ layout_embed_controls(\n+ title, view, body, views, theme, theme_next, None, None, false,\n+ )\n+}\n+\n+#[allow(clippy::too_many_arguments)]\n+fn layout_embed_controls(\n+ title: &str,\n+ view: &str,\n+ body: Markup,\n+ views: Option,\n+ theme: &str,\n+ theme_next: &str,\n+ garden_room_wire: Option<&str>,\n+ garden_path_prefix: Option<&str>,\n+ show_controls: bool,\n+) -> Markup {\n+ let theme = normalize_theme(theme);\n+ let css_href = format!(\"/static/theme_{theme}.css\");\n+ html! {\n+ (DOCTYPE)\n+ html {\n+ head {\n+ meta charset=\"utf-8\";\n+ meta name=\"viewport\" content=\"width=device-width, initial-scale=1\";\n+ title { (title) }\n+ link rel=\"stylesheet\" href=(css_href) id=\"theme-stylesheet\";\n+ script src=\"https://unpkg.com/idiomorph@0.3.0/dist/idiomorph.min.js\" {}\n+ }\n+ body class=(view)\n+ data-garden-room=(garden_room_wire.unwrap_or(\"\"))\n+ data-garden-prefix=(garden_path_prefix.unwrap_or(\"\")) {\n+ @if let Some(n) = views {\n+ span class=\"view-meta muted\" { (n) \" views\" }\n+ }\n+ div id=\"errors\" {}\n+ (body)\n+ @if show_controls {\n+ div id=\"controls\" {\n+ a href=\"https://github.com/sortersocial/slug\" id=\"src-link\" { \"src\" }\n+ div id=\"spread-control\" {\n+ span { \"spread\" }\n+ input type=\"range\" id=\"spread-slider\" min=\"0\" max=\"1\" step=\"0.05\" value=\"1\";\n+ }\n+ @if let (Some(gr), Some(ref gpx)) = (garden_room_wire, garden_path_prefix) {\n+ @if !gr.is_empty() {\n+ div id=\"slug-pin-hud\" class=\"slug-pin-hud\" data-garden-prefix=(gpx) {}\n+ }\n+ }\n+ a id=\"search-btn\" href=\"/search\" { \"search\" }\n+ form id=\"slug-theme-form\" method=\"post\" action=\"/theme\" {\n+ input type=\"hidden\" name=\"next\" value=(theme_next);\n+ select id=\"theme-select\" name=\"theme\" onchange=\"this.form.submit()\" aria-label=\"Theme\" {\n+ @for (val, label) in [(\"default\", \"default\"), (\"retro\", \"retro\"), (\"retro_craft\", \"craft\")] {\n+ @if theme == val {\n+ option value=(val) selected { (label) }\n+ } @else {\n+ option value=(val) { (label) }\n+ }\n+ }\n+ }\n+ }\n+ }\n+ }\n+ script src=\"/static/slug_ui.js\" {}\n+ }\n+ }\n+ }\n+}\n+\n+pub(super) fn now_ms() -> i64 {\n+ let t = std::time::SystemTime::now()\n+ .duration_since(std::time::UNIX_EPOCH)\n+ .unwrap_or_default();\n+ t.as_millis() as i64\n+}\n+\n+pub(super) fn ratio_pct(left: i32, right: i32) -> f64 {\n+ let l = (left.max(0)) as f64;\n+ let r = (right.max(0)) as f64;\n+ let denom = l + r;\n+ if denom <= 0.0 {\n+ return 50.0;\n+ }\n+ (l / denom) * 100.0\n+}\n+\n+/// Render a single breadcrumb segment with `/` separator.\n+pub(super) fn bc_segment(label: &str, href: &str, is_current: bool) -> Markup {\n+ html! {\n+ span class=\"bc-sep\" { \" / \" }\n+ @if is_current {\n+ a href=(href) class=\"bc-current\" { (label) }\n+ } @else {\n+ a href=(href) { (label) }\n+ }\n+ }\n+}\n+\n+/// Breadcrumb for external ontology `/-/https://…`\n+fn bc_path_external(path: &ExternalOntologyPath) -> Markup {\n+ html! {\n+ a href=\"/\" { \"slug.social\" }\n+ (bc_segment(\"-\", \"/-\", path.is_root()))\n+ @for (i, id) in path.breadcrumb_chain().iter().enumerate() {\n+ @let disp = id.display_path();\n+ @let tail = disp.strip_prefix(\"-/\").unwrap_or(disp.as_str());\n+ @let href = format!(\"/-/{}\", tail);\n+ @let is_last = i + 1 == path.breadcrumb_chain().len();\n+ (bc_segment(id.last_segment(), &href, is_last))\n+ }\n+ }\n+}\n+\n+/// Breadcrumb for any ontology path segment, e.g. \"parables\" or \"parables/counting-the-cost\".\n+fn bc_path(path: &OntologyPath) -> Markup {\n+ html! {\n+ a href=(path.slug_root_href()) { \"slug.social\" }\n+ (bc_segment(\"~\", \"/~\", path.is_root()))\n+ @for (i, seg) in path.segments().iter().enumerate() {\n+ @let href = format!(\"/~/{}\", path.segments()[..=i].join(\"/\"));\n+ @let is_last = i == path.segments().len() - 1;\n+ (bc_segment(seg, &href, is_last))\n+ }\n+ }\n+}\n+\n+/// Render the thread path breadcrumb: `slug.social / #tag` or `… / #tag / post #N` on a single post.\n+/// Root link toggles to `/~` only at thread-root (`/`).\n+pub(super) fn bc_threads(thread_tag: Option<&str>, focused_post: Option) -> Markup {\n+ let root_href = if thread_tag.is_some() { \"/\" } else { \"/~\" };\n+ let root_is_current = thread_tag.is_none();\n+ html! {\n+ @if root_is_current {\n+ a href=(root_href) class=\"bc-current\" { \"slug.social\" }\n+ } @else {\n+ a href=(root_href) { \"slug.social\" }\n+ }\n+ @if let Some(tag) = thread_tag {\n+ @if let Some(idx) = focused_post {\n+ (bc_segment(&format!(\"#{tag}\"), &format!(\"/t/{tag}\"), false))\n+ (bc_segment(&format!(\"post #{idx}\"), &format!(\"/t/{tag}/{idx}\"), true))\n+ } @else {\n+ (bc_segment(&format!(\"#{tag}\"), &format!(\"/t/{tag}\"), true))\n+ }\n+ }\n+ }\n+}\n+\n+/// Short display label for a stored agent id (`uuid:rig:model`, no `@`).\n+pub(super) fn actor_label(agent_naked: &str) -> String {\n+ let a = agent_naked.trim();\n+ let parts: Vec<&str> = a.split(':').collect();\n+ if parts.len() >= 3 {\n+ let rig = parts[1].trim();\n+ let model = parts[2].trim();\n+ // X actors: show @handle instead of uuid hash.\n+ if rig == \"x.com\" {\n+ return format!(\"@{model}\");\n+ }\n+ let uuid = parts[0].trim();\n+ let uuid8 = uuid.chars().take(8).collect::();\n+ if !uuid8.is_empty() && !rig.is_empty() && !model.is_empty() {\n+ return format!(\"{uuid8}:{rig}:{model}\");\n+ }\n+ }\n+ a.to_string()\n+}\n+\n+/// HTML attribution only: human `@name`, or agent `@@uuid8:rig:model` when a delegate is present.\n+pub(super) fn authorship_address(principal: &str, delegate: &Option) -> String {\n+ match delegate {\n+ Some(d) => format!(\"@@{}\", actor_label(d)),\n+ None => format!(\"@{principal}\"),\n+ }\n+}\n+\n+/// Escape HTML special chars for safe injection.\n+fn escape_html(s: &str) -> String {\n+ let mut out = String::with_capacity(s.len());\n+ for c in s.chars() {\n+ match c {\n+ '&' => out.push_str(\"&\"),\n+ '<' => out.push_str(\"<\"),\n+ '>' => out.push_str(\">\"),\n+ '\"' => out.push_str(\""\"),\n+ _ => out.push(c),\n+ }\n+ }\n+ out\n+}\n+\n+/// Max characters from an item body placed in a `title` tooltip (native hover).\n+const ITEM_LINK_TITLE_MAX_CHARS: usize = 500;\n+\n+fn collapse_whitespace_for_title(s: &str) -> String {\n+ let mut out = String::with_capacity(s.len().min(ITEM_LINK_TITLE_MAX_CHARS + 8));\n+ let mut last_was_space = true;\n+ for c in s.chars() {\n+ if c.is_whitespace() {\n+ if !last_was_space {\n+ out.push(' ');\n+ last_was_space = true;\n+ }\n+ } else {\n+ out.push(c);\n+ last_was_space = false;\n+ }\n+ }\n+ out.trim().to_string()\n+}\n+\n+fn item_body_title_snippet(body: &str) -> Option {\n+ let s = collapse_whitespace_for_title(body);\n+ if s.is_empty() {\n+ return None;\n+ }\n+ let truncated: String = s.chars().take(ITEM_LINK_TITLE_MAX_CHARS).collect();\n+ let ellipsis = if s.chars().count() > ITEM_LINK_TITLE_MAX_CHARS {\n+ \"…\"\n+ } else {\n+ \"\"\n+ };\n+ Some(format!(\"{truncated}{ellipsis}\"))\n+}\n+\n+fn garden_href_for_item_ref(\n+ raw_ref: &str,\n+ garden_prefix: &str,\n+) -> Option<(crate::path_types::ItemId, String)> {\n+ let key = slug_types::canonicalize_item(raw_ref);\n+ let id = crate::path_types::ItemId::parse(&key)?;\n+ let href = if let Some(tail) = id.tilde_tail() {\n+ if tail.is_empty() {\n+ garden_prefix.trim_end_matches('/').to_string()\n+ } else {\n+ format!(\"{}/{}\", garden_prefix.trim_end_matches('/'), tail)\n+ }\n+ } else if id.as_str().starts_with(\"https://\") || id.as_str().starts_with(\"http://\") {\n+ let display = id.display_path();\n+ let rest = display.strip_prefix(\"-/\").unwrap_or(display.as_str());\n+ let ext_prefix = format!(\"{}-\", garden_prefix.trim_end_matches('~'));\n+ format!(\"{ext_prefix}/{rest}\")\n+ } else {\n+ return None;\n+ };\n+ Some((id, href))\n+}\n+\n+fn push_item_ref_anchor(\n+ out: &mut String,\n+ raw_ref: &str,\n+ garden_prefix: &str,\n+ item_bodies: Option<&HashMap>,\n+) -> bool {\n+ let Some((id, href)) = garden_href_for_item_ref(raw_ref, garden_prefix) else {\n+ return false;\n+ };\n+ out.push_str(r#\"');\n+ out.push_str(&escape_html(raw_ref));\n+ out.push_str(\"\");\n+ true\n+}\n+\n+/// Replace item refs in raw prose with clickable garden links.\n+///\n+/// When `item_bodies` is set, matching ontology items get a `title` attribute with a truncated\n+/// body preview for native browser tooltips (forum posts, item pages).\n+pub(super) fn linkify_slugs_with_prefix(\n+ raw: &str,\n+ garden_prefix: &str,\n+ item_bodies: Option<&HashMap>,\n+) -> String {\n+ let mut out = String::with_capacity(raw.len() + 64);\n+ for token in crate::dsl::tokenize_prose_item_refs(raw) {\n+ match token {\n+ crate::dsl::ProseToken::Text(text) => out.push_str(&escape_html(&text)),\n+ crate::dsl::ProseToken::ItemRef(raw_ref) => {\n+ if !push_item_ref_anchor(&mut out, &raw_ref, garden_prefix, item_bodies) {\n+ out.push_str(&escape_html(&raw_ref));\n+ }\n+ }\n+ }\n+ }\n+ out\n+}\n+\n+#[derive(Clone)]\n+struct EmbedFrame {\n+ src: String,\n+ title: String,\n+ provider_class: &'static str,\n+}\n+\n+fn clean_media_id(s: &str) -> String {\n+ s.chars()\n+ .take_while(|c| c.is_ascii_alphanumeric() || *c == '-' || *c == '_')\n+ .collect()\n+}\n+\n+fn query_param(url: &str, name: &str) -> Option {\n+ let q = url.split_once('?')?.1;\n+ for pair in q.split('&') {\n+ let (k, v) = pair.split_once('=').unwrap_or((pair, \"\"));\n+ if k == name {\n+ return Some(v.to_string());\n+ }\n+ }\n+ None\n+}\n+\n+fn spotify_embed_src(url: &str) -> Option {\n+ let rest = url\n+ .strip_prefix(\"https://\")\n+ .or_else(|| url.strip_prefix(\"http://\"))?;\n+ let (host, tail) = rest.split_once('/').unwrap_or((rest, \"\"));\n+ let host = host.to_lowercase();\n+ if !(host == \"open.spotify.com\" || host == \"www.open.spotify.com\") {\n+ return None;\n+ }\n+ let path = tail\n+ .split('#')\n+ .next()\n+ .unwrap_or(tail)\n+ .split('?')\n+ .next()\n+ .unwrap_or(tail);\n+ let mut segs: Vec<&str> = path.split('/').filter(|s| !s.is_empty()).collect();\n+ if segs.first().is_some_and(|s| s.starts_with(\"intl-\")) {\n+ segs.remove(0);\n+ }\n+ if segs.len() < 2 {\n+ return None;\n+ }\n+ let kind = segs[0];\n+ let id = clean_media_id(segs[1]);\n+ if id.is_empty() {\n+ return None;\n+ }\n+ let allowed = matches!(kind, \"track\" | \"album\" | \"playlist\" | \"episode\" | \"show\");\n+ if !allowed {\n+ return None;\n+ }\n+ Some(EmbedFrame {\n+ src: format!(\"https://open.spotify.com/embed/{kind}/{id}\"),\n+ title: format!(\"Spotify {kind}\"),\n+ provider_class: \"embed-spotify\",\n+ })\n+}\n+\n+fn youtube_embed_src(url: &str) -> Option {\n+ let rest = url\n+ .strip_prefix(\"https://\")\n+ .or_else(|| url.strip_prefix(\"http://\"))?;\n+ let (host, tail) = rest.split_once('/').unwrap_or((rest, \"\"));\n+ let host = host.to_lowercase();\n+\n+ let video_id = if host == \"youtu.be\" || host == \"www.youtu.be\" {\n+ clean_media_id(\n+ tail.split(['?', '#'])\n+ .next()\n+ .unwrap_or(tail)\n+ .trim_matches('/'),\n+ )\n+ } else if matches!(\n+ host.as_str(),\n+ \"youtube.com\" | \"www.youtube.com\" | \"m.youtube.com\" | \"music.youtube.com\"\n+ ) {\n+ let path = format!(\"/{}\", tail.split('#').next().unwrap_or(tail));\n+ if path.starts_with(\"/watch\") {\n+ clean_media_id(&query_param(url, \"v\")?)\n+ } else if let Some(id) = path.strip_prefix(\"/shorts/\") {\n+ clean_media_id(id.split(['?', '/']).next().unwrap_or(id))\n+ } else if let Some(id) = path.strip_prefix(\"/embed/\") {\n+ clean_media_id(id.split(['?', '/']).next().unwrap_or(id))\n+ } else {\n+ String::new()\n+ }\n+ } else {\n+ String::new()\n+ };\n+\n+ if video_id.is_empty() {\n+ return None;\n+ }\n+ Some(EmbedFrame {\n+ src: format!(\"https://www.youtube.com/embed/{video_id}\"),\n+ title: \"YouTube video\".to_string(),\n+ provider_class: \"embed-youtube\",\n+ })\n+}\n+\n+fn extract_embed_frames(raw: &str) -> Vec {\n+ let mut out = Vec::new();\n+ let mut seen = HashSet::new();\n+ for token in raw.split_whitespace() {\n+ let token = token\n+ .trim_matches(|c: char| c == '(' || c == '[' || c == '{' || c == '\"' || c == '\\'')\n+ .trim_end_matches(|c: char| \".,;:!?)]}\\\"'\".contains(c));\n+ if !(token.starts_with(\"https://\") || token.starts_with(\"http://\")) {\n+ continue;\n+ }\n+ let embed = spotify_embed_src(token).or_else(|| youtube_embed_src(token));\n+ if let Some(frame) = embed {\n+ if seen.insert(frame.src.clone()) {\n+ out.push(frame);\n+ }\n+ }\n+ }\n+ out\n+}\n+\n+pub(super) fn render_linkified_with_embeds_in_scope(\n+ raw: &str,\n+ garden_prefix: &str,\n+ item_bodies: Option<&HashMap>,\n+) -> Markup {\n+ let embeds = extract_embed_frames(raw);\n+ html! {\n+ pre { (maud::PreEscaped(linkify_slugs_with_prefix(raw, garden_prefix, item_bodies))) }\n+ @if !embeds.is_empty() {\n+ div class=\"rich-embeds\" {\n+ @for e in embeds {\n+ div class=(format!(\"rich-embed {}\", e.provider_class)) {\n+ iframe\n+ src=(e.src)\n+ title=(e.title)\n+ loading=\"lazy\"\n+ allow=\"autoplay; clipboard-write; encrypted-media; fullscreen; picture-in-picture; web-share\"\n+ referrerpolicy=\"strict-origin-when-cross-origin\"\n+ allowfullscreen {}\n+ }\n+ }\n+ }\n+ }\n+ }\n+}\n+\n+/// Item page / thread body: resolver-specific rich HTML, else linkified `
` + media embeds.\n+pub(super) fn render_item_body_in_scope(\n+    raw: &str,\n+    garden_prefix: &str,\n+    item_bodies: Option<&HashMap>,\n+) -> Markup {\n+    if let Some(m) = crate::resolvers::try_render_resolver_item_body(raw) {\n+        return html! {\n+            div class=\"item-body-rich\" { (m) }\n+        };\n+    }\n+    render_linkified_with_embeds_in_scope(raw, garden_prefix, item_bodies)\n+}\n+\n+/// CLI strings are embedded in a single-quoted JS literal; they must never need escaping.\n+fn assert_cli_panel_cmd_js_single_quote_safe(s: &str) {\n+    assert!(\n+        !s.contains('\\\\') && !s.contains('\\'') && !s.contains('\\n') && !s.contains('\\r'),\n+        \"cli_panel cmd must not contain `\\\\`, `'`, or newlines (got {s:?})\"\n+    );\n+}\n+\n+/// Small CLI hint panel: one border and title; each line is hover-highlighted and copies on click.\n+pub(super) fn cli_panel>(cmds: &[I]) -> Markup {\n+    if cmds.is_empty() {\n+        return html! {};\n+    }\n+    for cmd in cmds {\n+        assert_cli_panel_cmd_js_single_quote_safe(cmd.as_ref());\n+    }\n+    html! {\n+        div class=\"cli-panel\" {\n+            span class=\"cli-panel-label muted\" { \"cli\" }\n+            div class=\"cli-panel-cmds\" {\n+                @for cmd in cmds {\n+                    @let s = cmd.as_ref();\n+                    button type=\"button\" class=\"cli-panel-row\" title=\"Copy command\" onclick=(format!(\n+                        r#\"navigator.clipboard.writeText('{}');\"#,\n+                        s\n+                    )) {\n+                        code class=\"cli-panel-cmd\" { (s) }\n+                    }\n+                }\n+            }\n+        }\n+    }\n+}\n+\n+/// Age bucket for recency coloring of thread entries.\n+pub(super) fn recency_class(now_ms: i64, ts_ms: i64) -> &'static str {\n+    let age_ms = now_ms.saturating_sub(ts_ms);\n+    let age_secs = age_ms / 1000;\n+    if age_secs < 3600 {\n+        \"age-fresh\" // < 1 hour\n+    } else if age_secs < 86400 {\n+        \"age-recent\" // < 1 day\n+    } else if age_secs < 86400 * 7 {\n+        \"age-week\" // < 1 week\n+    } else {\n+        \"age-old\" // >= 1 week\n+    }\n+}\n+\n+#[cfg(test)]\n+mod linkify_title_tests {\n+    use super::*;\n+    use crate::path_types::ItemId;\n+    use std::collections::HashMap;\n+\n+    #[test]\n+    fn tilde_link_gets_title_from_item_bodies() {\n+        let mut bodies = HashMap::new();\n+        let key = ItemId::parse(&slug_types::canonicalize_item(\"~/foo/bar\")).unwrap();\n+        bodies.insert(key, \"Hello  world\\nline\".to_string());\n+        let html = linkify_slugs_with_prefix(\"see ~/foo/bar ok\", \"/r/x/~\", Some(&bodies));\n+        assert!(html.contains(\"title=\\\"Hello world line\\\"\"));\n+        assert!(html.contains(\"href=\\\"/r/x/~/foo/bar\\\"\"));\n+    }\n+\n+    #[test]\n+    fn raw_url_links_to_public_external_garden_page() {\n+        let html = linkify_slugs_with_prefix(\"see https://example.com/z.\", \"/~\", None);\n+        assert!(html.contains(\n+            r#\"https://example.com/z.\"#\n+        ));\n+    }\n+\n+    #[test]\n+    fn dash_ref_links_to_room_external_garden_page_with_title() {\n+        let mut bodies = HashMap::new();\n+        let key = ItemId::parse(&slug_types::canonicalize_item(\"-/example.com/z\")).unwrap();\n+        bodies.insert(key, \"External body\\npreview\".to_string());\n+        let html =\n+            linkify_slugs_with_prefix(\"see -/example.com/z\", \"/r/9ab12cdroom/~\", Some(&bodies));\n+        assert!(html.contains(r#\"href=\"/r/9ab12cdroom/-/https://example.com/z\"\"#));\n+        assert!(html.contains(r#\"title=\"External body preview\"\"#));\n+    }\n+\n+    #[test]\n+    fn code_fence_urls_are_not_linkified() {\n+        let html = linkify_slugs_with_prefix(\n+            \"```json\\n{\\\"url\\\":\\\"https://example.com/z\\\"}\\n```\\nthen https://example.com/a\",\n+            \"/~\",\n+            None,\n+        );\n+        assert!(!html.contains(r#\"href=\"/-/https://example.com/z\"\"#));\n+        assert!(html.contains(r#\"href=\"/-/https://example.com/a\"\"#));\n+    }\n+\n+    #[test]\n+    fn no_title_when_body_missing_or_empty() {\n+        let html = linkify_slugs_with_prefix(\"x ~/a/b y\", \"/~\", Some(&HashMap::new()));\n+        assert!(!html.contains(\" title=\"));\n+    }\n+}\n","role":"user"}],"model":"~anthropic/claude-sonnet-latest"}