{"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[d88719a1] refactor(html): unify public and room new-thread forms\n\n- Single DOM contract: #new-thread-ui-slot, #new-thread-compose, #new-thread-form,\n #new-thread-tag; shared check_ingest + post_ingest error wiring for all scopes.\n- Replace ExpandPublic/ExpandRoom/SetRoomNewThreadComposeExpanded with\n ExpandNewThreadForm { room_wire } and SetNewThreadComposeExpanded { room_wire, expanded };\n room_wire \"public\" covers the home toolbar flow.\n- SSE refresh resets #new-thread-compose form for any room_key.\n- Browser test selectors updated for the unified ids.\n\nMade-with: Cursor\n\nSide A — unified diff (full patch):\ndiff --git a/server/src/api/rpc.rs b/server/src/api/rpc.rs\nindex 03f4f7efe08df3ec160e50d08652bde7d6a9c4d2..422b520f527f64571f86f0ee3cb57edcbc21c0c3 100644\n--- a/server/src/api/rpc.rs\n+++ b/server/src/api/rpc.rs\n@@ -63,11 +63,7 @@ async fn broadcast_web_refresh(state: &AppState, room_key: &str, thread_id: &str\n crate::html::thread_feed_region_markup(state, Some(room_key), thread_id, None).await;\n \n let builder = JsBuilder::new().morph_selector(&format!(\"#{feed_id}\"), feed_markup);\n- let builder = if room_key == \"public\" {\n- builder.qs(\"#public-new-thread-compose form\").reset()\n- } else {\n- builder.qs(\"#room-new-thread-compose form\").reset()\n- };\n+ let builder = builder.qs(\"#new-thread-compose form\").reset();\n let builder = builder.if_current_path_matches(&thread_url, |builder| {\n builder.morph_selector(\"#thread-feed-region\", thread_feed_markup)\n });\ndiff --git a/server/src/api/ui_html.rs b/server/src/api/ui_html.rs\nindex 6ff9f1319e91f61bf8546eb65637c20f3a58483c..e979053ff1ba8c0e2bf55add0a32b7de11cf1e56 100644\n--- a/server/src/api/ui_html.rs\n+++ b/server/src/api/ui_html.rs\n@@ -19,7 +19,7 @@ use crate::{\n },\n canonical_path::canonicalize_tag,\n html::{\n- fragment_public_new_thread_form, fragment_room_new_thread_form, login_to_post_hint_markup,\n+ fragment_new_thread_slot, login_to_post_hint_markup,\n parse_html_ui_from_form, room_members_section_markup, thread_feed_html,\n thread_feed_html_for_room, thread_feed_region_markup, thread_ui_collapse_redacted_post,\n thread_ui_expand_post_full, thread_ui_expand_redacted_post, ui_js_warn, user_can_post_room,\n@@ -139,24 +139,24 @@ async fn dispatch_ui_action(\n }\n }\n }\n- HtmlUiAction::ExpandPublicNewThreadForm => {\n- let reduced = state.reduced.read().await;\n- let user = session.map(|s| s.username.as_str());\n- drop(reduced);\n- let markup = if user.is_some() {\n- fragment_public_new_thread_form(true)\n- } else {\n- login_to_post_hint_markup()\n- };\n- JsBuilder::new()\n- .morph_inner_selector(\"#public-new-thread-ui-slot\", markup)\n- .into_response()\n- }\n- HtmlUiAction::ExpandRoomNewThreadForm { room_wire } => {\n+ HtmlUiAction::ExpandNewThreadForm { room_wire } => {\n let room_wire = room_wire.trim().to_string();\n if room_wire.is_empty() {\n return ui_js_warn(\"missing room\").into_response();\n }\n+ if room_wire == \"public\" {\n+ let reduced = state.reduced.read().await;\n+ let user = session.map(|s| s.username.as_str());\n+ drop(reduced);\n+ let markup = if user.is_some() {\n+ fragment_new_thread_slot(&ThreadNav::public(), true, false)\n+ } else {\n+ login_to_post_hint_markup()\n+ };\n+ return JsBuilder::new()\n+ .morph_inner_selector(\"#new-thread-ui-slot\", markup)\n+ .into_response();\n+ }\n let reduced = state.reduced.read().await;\n let user = session.map(|s| s.username.as_str());\n if !reduced.rooms.contains(&room_wire) {\n@@ -176,12 +176,12 @@ async fn dispatch_ui_action(\n return ui_js_warn(\"bad room\").into_response();\n };\n let markup = if can_post {\n- fragment_room_new_thread_form(&nav, true, false)\n+ fragment_new_thread_slot(&nav, true, false)\n } else {\n login_to_post_hint_markup()\n };\n JsBuilder::new()\n- .morph_inner_selector(\"#room-new-thread-ui-slot\", markup)\n+ .morph_inner_selector(\"#new-thread-ui-slot\", markup)\n .into_response()\n }\n HtmlUiAction::SetRoomMembersExpanded { room_wire, expanded } => {\n@@ -205,11 +205,28 @@ async fn dispatch_ui_action(\n .morph_selector(\"#room-members-section\", markup)\n .into_response()\n }\n- HtmlUiAction::SetRoomNewThreadComposeExpanded { room_wire, expanded } => {\n+ HtmlUiAction::SetNewThreadComposeExpanded { room_wire, expanded } => {\n let room_wire = room_wire.trim().to_string();\n if room_wire.is_empty() {\n return ui_js_warn(\"missing room\").into_response();\n }\n+ if room_wire == \"public\" {\n+ let reduced = state.reduced.read().await;\n+ let _user = session.map(|s| s.username.as_str());\n+ drop(reduced);\n+ let can_post = session.is_some();\n+ let nav = ThreadNav::public();\n+ let markup = if can_post {\n+ fragment_new_thread_slot(&nav, true, expanded)\n+ } else {\n+ login_to_post_hint_markup()\n+ };\n+ let mut b = JsBuilder::new().morph_inner_selector(\"#new-thread-ui-slot\", markup);\n+ if expanded && can_post {\n+ b = b.focus_selector(\"#new-thread-tag\");\n+ }\n+ return b.into_response();\n+ }\n let reduced = state.reduced.read().await;\n let user = session.map(|s| s.username.as_str());\n if !reduced.rooms.contains(&room_wire) {\n@@ -229,13 +246,13 @@ async fn dispatch_ui_action(\n return ui_js_warn(\"bad room\").into_response();\n };\n let markup = if can_post {\n- fragment_room_new_thread_form(&nav, true, expanded)\n+ fragment_new_thread_slot(&nav, true, expanded)\n } else {\n login_to_post_hint_markup()\n };\n- let mut b = JsBuilder::new().morph_inner_selector(\"#room-new-thread-ui-slot\", markup);\n+ let mut b = JsBuilder::new().morph_inner_selector(\"#new-thread-ui-slot\", markup);\n if expanded && can_post {\n- b = b.focus_selector(\"#room-new-tag\");\n+ b = b.focus_selector(\"#new-thread-tag\");\n }\n b.into_response()\n }\ndiff --git a/server/src/html/forum/feed.rs b/server/src/html/forum/feed.rs\nindex 30b95ff6e85245a82199c96e41484d03c24989dd..1b4ae7baa3ad4757b74172d7b67f3f2b33d1075d 100644\n--- a/server/src/html/forum/feed.rs\n+++ b/server/src/html/forum/feed.rs\n@@ -245,11 +245,13 @@ pub async fn home(\n p class=\"muted\" { \"dark = time-ordered · light = vote-ranked\" }\n div class=\"thread-feed-toolbar\" {\n form method=\"POST\" action=\"/ui\" {\n- input type=\"hidden\" name=(UI_RPC_FIELD) value=(template_json_compact(&HtmlUiAction::ExpandPublicNewThreadForm).expect(\"static json\"));\n+ input type=\"hidden\" name=(UI_RPC_FIELD) value=(template_json_compact(&HtmlUiAction::ExpandNewThreadForm {\n+ room_wire: \"public\".into(),\n+ }).expect(\"static json\"));\n button type=\"submit\" class=\"section-add-btn\" { \"+\" }\n }\n }\n- div id=\"public-new-thread-ui-slot\" {}\n+ div id=\"new-thread-ui-slot\" {}\n (render_thread_feed(Some(&nav), \"thread-feed\", &public_rows, now))\n (cli_panel(&[\"npx slugsocial public forum list\"]))\n },\ndiff --git a/server/src/html/forum/mod.rs b/server/src/html/forum/mod.rs\nindex 8fbf3b71f41fafb474999ebb3d622e6bd5b5f313..bd60703924eee5b0e5a7f69c0a2dc526442512a2 100644\n--- a/server/src/html/forum/mod.rs\n+++ b/server/src/html/forum/mod.rs\n@@ -20,9 +20,7 @@ pub use profile::user_profile_page;\n pub use views::{room_page, room_thread_view, thread_view};\n \n pub(crate) use access::{user_can_post_room, user_can_view_room};\n-pub(crate) use new_thread::{\n- fragment_public_new_thread_form, fragment_room_new_thread_form, login_to_post_hint_markup,\n-};\n+pub(crate) use new_thread::{fragment_new_thread_slot, login_to_post_hint_markup};\n pub(crate) use room_members::room_members_section_markup;\n pub(crate) use thread_morph::{\n thread_ui_collapse_redacted_post, thread_ui_expand_post_full, thread_ui_expand_redacted_post,\ndiff --git a/server/src/html/forum/new_thread.rs b/server/src/html/forum/new_thread.rs\nindex b2d24b8ab3ec7eee0edd16bed8ee6605e49bf408..4dd2654f953a7db6ee9b25c348f99906da93a94a 100644\n--- a/server/src/html/forum/new_thread.rs\n+++ b/server/src/html/forum/new_thread.rs\n@@ -5,120 +5,48 @@ use serde_json::json;\n \n use super::nav::ThreadNav;\n \n-struct NewThreadIds {\n- compose_section_id: &'static str,\n- errors_id: &'static str,\n- form_id: &'static str,\n- tag_input_id: &'static str,\n- text_input_id: Option<&'static str>,\n-}\n-\n-const PUBLIC_IDS: NewThreadIds = NewThreadIds {\n- compose_section_id: \"public-new-thread-compose\",\n- errors_id: \"public-new-thread-errors\",\n- form_id: \"public-new-thread-form\",\n- tag_input_id: \"new-thread-tag\",\n- text_input_id: Some(\"new-thread-text\"),\n-};\n-\n-const ROOM_IDS: NewThreadIds = NewThreadIds {\n- compose_section_id: \"room-new-thread-compose\",\n- errors_id: \"room-new-thread-errors\",\n- form_id: \"room-new-thread-form\",\n- tag_input_id: \"room-new-tag\",\n- text_input_id: None,\n-};\n-\n-#[derive(Clone, Copy)]\n-enum NewThreadComposeKind {\n- /// Home page: no client-side check RPC; post template omits `error_target` / `form_id`.\n- Public,\n- /// Room page: `check_ingest` + error targets on post (matches thread compose).\n- Room,\n-}\n-\n-/// Shared `
` for creating a thread + first post.\n-fn new_thread_compose_section(room_wire: &str, ids: &NewThreadIds, kind: NewThreadComposeKind) -> Markup {\n- let client_check = matches!(kind, NewThreadComposeKind::Room);\n- let (tag_placeholder, text_placeholder, submit_label) = match kind {\n- NewThreadComposeKind::Public => (\n- \"thread-title-slug-here\",\n- \"Hello threadgoers!! Behold my new thread!\",\n- \"create thread / make first post\",\n- ),\n- NewThreadComposeKind::Room => (\n- \"thread-topic-slug-here\",\n- \"First post body…\",\n- \"post\",\n- ),\n- };\n+/// Stable ids shared by public home and private room “new thread” UI (`#new-thread-ui-slot`).\n+const COMPOSE_SECTION_ID: &str = \"new-thread-compose\";\n+const ERRORS_ID: &str = \"new-thread-errors\";\n+const FORM_ID: &str = \"new-thread-form\";\n+const TAG_INPUT_ID: &str = \"new-thread-tag\";\n \n+/// Shared `
`: check + post with `error_target` / `form_id` (same as thread reply compose).\n+fn new_thread_compose_section(room_wire: &str) -> Markup {\n html! {\n- section class=\"compose\" id=(ids.compose_section_id) {\n- div id=(ids.errors_id) {}\n- @if client_check {\n- form id=(ids.form_id) method=\"POST\" action=\"/ui\" data-check-action=\"/ui\" data-check-rpc=(template_json_compact(&json!({\n- \"action\": \"check_ingest\",\n+ section class=\"compose\" id=(COMPOSE_SECTION_ID) {\n+ div id=(ERRORS_ID) {}\n+ form id=(FORM_ID) method=\"POST\" action=\"/ui\" data-check-action=\"/ui\" data-check-rpc=(template_json_compact(&json!({\n+ \"action\": \"check_ingest\",\n+ \"room\": room_wire,\n+ \"thread_tag\": {\"$form\": \"thread_tag\"},\n+ \"text\": {\"$form\": \"text\"},\n+ \"error_target\": ERRORS_ID,\n+ \"form_id\": FORM_ID,\n+ })).unwrap()) {\n+ input type=\"hidden\" name=(UI_RPC_FIELD) value=(template_json_compact(&json!({\n+ \"action\": \"post_ingest\",\n \"room\": room_wire,\n \"thread_tag\": {\"$form\": \"thread_tag\"},\n \"text\": {\"$form\": \"text\"},\n- \"error_target\": ids.errors_id,\n- \"form_id\": ids.form_id,\n- })).unwrap()) {\n- input type=\"hidden\" name=(UI_RPC_FIELD) value=(template_json_compact(&json!({\n- \"action\": \"post_ingest\",\n- \"room\": room_wire,\n- \"thread_tag\": {\"$form\": \"thread_tag\"},\n- \"text\": {\"$form\": \"text\"},\n- \"error_target\": ids.errors_id,\n- \"form_id\": ids.form_id,\n- })).unwrap());\n- input type=\"text\" id=(ids.tag_input_id) name=\"thread_tag\" pattern=\"[a-z0-9_\\\\-]{1,64}\" required placeholder=(tag_placeholder);\n- @if let Some(tid) = ids.text_input_id {\n- textarea id=(tid) name=\"text\" rows=\"4\" placeholder=(text_placeholder) required {}\n- } @else {\n- textarea name=\"text\" rows=\"4\" placeholder=(text_placeholder) required {}\n- }\n- p { button type=\"submit\" { (submit_label) } }\n- }\n- } @else {\n- form id=(ids.form_id) method=\"POST\" action=\"/ui\" {\n- input type=\"hidden\" name=(UI_RPC_FIELD) value=(template_json_compact(&json!({\n- \"action\": \"post_ingest\",\n- \"room\": room_wire,\n- \"thread_tag\": {\"$form\": \"thread_tag\"},\n- \"text\": {\"$form\": \"text\"},\n- })).unwrap());\n- input type=\"text\" id=(ids.tag_input_id) name=\"thread_tag\" pattern=\"[a-z0-9_\\\\-]{1,64}\" placeholder=(tag_placeholder);\n- @if let Some(tid) = ids.text_input_id {\n- textarea id=(tid) name=\"text\" rows=\"4\" placeholder=(text_placeholder) {}\n- } @else {\n- textarea name=\"text\" rows=\"4\" placeholder=(text_placeholder) {}\n- }\n- p { button type=\"submit\" { (submit_label) } }\n- }\n+ \"error_target\": ERRORS_ID,\n+ \"form_id\": FORM_ID,\n+ })).unwrap());\n+ input type=\"text\" id=(TAG_INPUT_ID) name=\"thread_tag\" pattern=\"[a-z0-9_\\\\-]{1,64}\" required placeholder=\"thread-topic-slug-here\";\n+ textarea name=\"text\" rows=\"4\" placeholder=\"First post body…\" required {}\n+ p { button type=\"submit\" { \"create thread / post\" } }\n }\n }\n }\n }\n \n-fn new_thread_form_public(show: bool) -> Markup {\n- if !show {\n- return html! {};\n- }\n- new_thread_compose_section(\"public\", &PUBLIC_IDS, NewThreadComposeKind::Public)\n-}\n-\n-fn new_thread_form_for_room(nav: &ThreadNav, show: bool, compose_expanded: bool) -> Markup {\n- if !show {\n- return html! {};\n- }\n- // Single root for Idiomorph when morphing `#room-new-thread-ui-slot` (expanded has form + section).\n+/// Inner fragment morphed into `#new-thread-ui-slot` (Idiomorph replaces children; outer `id` stays).\n+fn new_thread_slot_inner(nav: &ThreadNav, compose_expanded: bool) -> Markup {\n html! {\n- div class=\"room-new-thread-slot-inner\" {\n+ div class=\"new-thread-slot-inner\" {\n @if compose_expanded {\n form method=\"POST\" action=\"/ui\" {\n- input type=\"hidden\" name=(UI_RPC_FIELD) value=(template_json_compact(&HtmlUiAction::SetRoomNewThreadComposeExpanded {\n+ input type=\"hidden\" name=(UI_RPC_FIELD) value=(template_json_compact(&HtmlUiAction::SetNewThreadComposeExpanded {\n room_wire: nav.room_wire.clone(),\n expanded: false,\n }).expect(\"static json\"));\n@@ -126,10 +54,10 @@ fn new_thread_form_for_room(nav: &ThreadNav, show: bool, compose_expanded: bool)\n \"-\"\n }\n }\n- (new_thread_compose_section(&nav.room_wire, &ROOM_IDS, NewThreadComposeKind::Room))\n+ (new_thread_compose_section(&nav.room_wire))\n } @else {\n form method=\"POST\" action=\"/ui\" {\n- input type=\"hidden\" name=(UI_RPC_FIELD) value=(template_json_compact(&HtmlUiAction::SetRoomNewThreadComposeExpanded {\n+ input type=\"hidden\" name=(UI_RPC_FIELD) value=(template_json_compact(&HtmlUiAction::SetNewThreadComposeExpanded {\n room_wire: nav.room_wire.clone(),\n expanded: true,\n }).expect(\"static json\"));\n@@ -142,16 +70,19 @@ fn new_thread_form_for_room(nav: &ThreadNav, show: bool, compose_expanded: bool)\n }\n }\n \n-pub(crate) fn login_to_post_hint_markup() -> Markup {\n- html! {\n- p class=\"muted\" { \"log in to post\" }\n+pub(crate) fn new_thread_slot_markup(nav: &ThreadNav, show: bool, compose_expanded: bool) -> Markup {\n+ if !show {\n+ return html! {};\n }\n+ new_thread_slot_inner(nav, compose_expanded)\n }\n \n-pub(crate) fn fragment_public_new_thread_form(show: bool) -> Markup {\n- new_thread_form_public(show)\n+pub(crate) fn fragment_new_thread_slot(nav: &ThreadNav, show: bool, compose_expanded: bool) -> Markup {\n+ new_thread_slot_markup(nav, show, compose_expanded)\n }\n \n-pub(crate) fn fragment_room_new_thread_form(nav: &ThreadNav, show: bool, compose_expanded: bool) -> Markup {\n- new_thread_form_for_room(nav, show, compose_expanded)\n+pub(crate) fn login_to_post_hint_markup() -> Markup {\n+ html! {\n+ p class=\"muted\" { \"log in to post\" }\n+ }\n }\ndiff --git a/server/src/html/forum/views.rs b/server/src/html/forum/views.rs\nindex be08491b893f5960723e77c7f7fb9d416100f0fb..f9c425634d1dfa2cbbc2e22c3c745717a6486173 100644\n--- a/server/src/html/forum/views.rs\n+++ b/server/src/html/forum/views.rs\n@@ -19,7 +19,7 @@ use super::access::user_can_view_room;\n use super::feed::{collect_thread_rows_for_scope, render_thread_feed};\n use super::ingest::ingest_entry_markup;\n use super::nav::ThreadNav;\n-use super::new_thread::fragment_room_new_thread_form;\n+use super::new_thread::fragment_new_thread_slot;\n use super::page::{auth_strip, bc_room};\n use super::paginator::{render_thread_paginator, PAGE_SIZE};\n use super::room_members::room_members_section_markup;\n@@ -276,8 +276,8 @@ pub async fn room_page(\n h3 { \"threads\" }\n (render_thread_feed(Some(&nav), \"room-thread-feed\", &rows, now))\n @if show_new {\n- div id=\"room-new-thread-ui-slot\" {\n- (fragment_room_new_thread_form(&nav, true, false))\n+ div id=\"new-thread-ui-slot\" {\n+ (fragment_new_thread_slot(&nav, true, false))\n }\n }\n (cli_panel(&[forum_cli, garden_cli, audit_cli]))\ndiff --git a/server/src/html/mod.rs b/server/src/html/mod.rs\nindex 6617781a2e8e86c2e2693788ea7cd0eb0e3659a2..e142240c95c8e92c22ab631df98268253cf0cbdf 100644\n--- a/server/src/html/mod.rs\n+++ b/server/src/html/mod.rs\n@@ -27,9 +27,9 @@ pub use forum::{\n };\n \n pub(crate) use forum::{\n- fragment_public_new_thread_form, fragment_room_new_thread_form, login_to_post_hint_markup,\n- room_members_section_markup, thread_ui_collapse_redacted_post, thread_ui_expand_post_full,\n- thread_ui_expand_redacted_post, user_can_post_room, user_can_view_room,\n+ fragment_new_thread_slot, login_to_post_hint_markup, room_members_section_markup,\n+ thread_ui_collapse_redacted_post, thread_ui_expand_post_full, thread_ui_expand_redacted_post,\n+ user_can_post_room, user_can_view_room,\n };\n pub use garden::{garden_index, ontology_path, room_garden_index, room_ontology_path};\n pub use search::{search_page, search_results_fragment};\n@@ -183,7 +183,7 @@ impl JsBuilder {\n )\n }\n \n- /// Morph **children** of `selector` so the outer element (e.g. `#room-new-thread-ui-slot`) keeps its `id`.\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 }\ndiff --git a/server/src/html/ui_action.rs b/server/src/html/ui_action.rs\nindex 0c4d33e58ad2bff9661bf70ec994ac30f7ca3efe..5031ebfeb928f28e471f23210b8c644654adb7c7 100644\n--- a/server/src/html/ui_action.rs\n+++ b/server/src/html/ui_action.rs\n@@ -38,10 +38,9 @@ pub enum HtmlUiAction {\n RedactPost {\n post_id: String,\n },\n- /// Morph `#public-new-thread-ui-slot` to the new-thread form (or login hint).\n- ExpandPublicNewThreadForm,\n- /// Morph `#room-new-thread-ui-slot` for the given room wire id.\n- ExpandRoomNewThreadForm {\n+ /// Morph `#new-thread-ui-slot` inner to the collapsed compose toggle (or login hint).\n+ /// Use `room_wire: \"public\"` for the public forum home; otherwise a private room id (`short/slug`).\n+ ExpandNewThreadForm {\n room_wire: String,\n },\n /// Morph `#room-members-section` — members list open or collapsed (server-rendered).\n@@ -50,8 +49,8 @@ pub enum HtmlUiAction {\n #[serde(default)]\n expanded: bool,\n },\n- /// Morph `#room-new-thread-ui-slot` — compose body open or collapsed (server-rendered).\n- SetRoomNewThreadComposeExpanded {\n+ /// Morph `#new-thread-ui-slot` inner — compose open or collapsed (`room_wire: \"public\"` for home).\n+ SetNewThreadComposeExpanded {\n room_wire: String,\n #[serde(default)]\n expanded: bool,\n@@ -133,15 +132,23 @@ mod tests {\n }\n \n #[test]\n- fn expand_public_unit_variant() {\n- let template = serde_json::json!({ \"action\": \"expand_public_new_thread_form\" });\n+ fn expand_new_thread_form_public() {\n+ let template = serde_json::json!({\n+ \"action\": \"expand_new_thread_form\",\n+ \"room_wire\": \"public\",\n+ });\n let mut form = HashMap::new();\n form.insert(\n UI_RPC_FIELD.to_string(),\n serde_json::to_string(&template).unwrap(),\n );\n let a = parse_html_ui_from_form(&form).unwrap();\n- assert_eq!(a, HtmlUiAction::ExpandPublicNewThreadForm);\n+ assert_eq!(\n+ a,\n+ HtmlUiAction::ExpandNewThreadForm {\n+ room_wire: \"public\".into(),\n+ }\n+ );\n }\n \n #[test]\n@@ -190,9 +197,9 @@ mod tests {\n }\n \n #[test]\n- fn set_room_new_thread_compose_expanded_true() {\n+ fn set_new_thread_compose_expanded_true() {\n let template = serde_json::json!({\n- \"action\": \"set_room_new_thread_compose_expanded\",\n+ \"action\": \"set_new_thread_compose_expanded\",\n \"room_wire\": \"ab/cd\",\n \"expanded\": true,\n });\n@@ -204,7 +211,7 @@ mod tests {\n let a = parse_html_ui_from_form(&form).unwrap();\n assert_eq!(\n a,\n- HtmlUiAction::SetRoomNewThreadComposeExpanded {\n+ HtmlUiAction::SetNewThreadComposeExpanded {\n room_wire: \"ab/cd\".into(),\n expanded: true,\n }\ndiff --git a/test/browser_sse.clj b/test/browser_sse.clj\nindex 3ac71a2986cbb71ed2c513f50cd5ef3f2582c945..8bb064dbc2dc0feb6caaa67f520a461645958277 100644\n--- a/test/browser_sse.clj\n+++ b/test/browser_sse.clj\n@@ -80,19 +80,17 @@\n room-url (str base-url \"/r/\" room-short \"/\" room-slug)\n thread-url (str room-url \"/t/sse-thread\")]\n ;; Object under test: slug_ui.js intercepts POST /ui, evals JS, morphs\n- ;; #room-new-thread-ui-slot (expand compose), then post_ingest redirects to thread.\n+ ;; #new-thread-ui-slot (expand compose), then post_ingest redirects to thread.\n (page/navigate alice-pg room-url)\n (page/wait-for-load-state alice-pg :load)\n- (is (wait-for-text alice-pg \"#room-new-thread-ui-slot\"\n- \"new thread in this room\" 30000)\n+ (is (wait-for-text alice-pg \"#new-thread-ui-slot\" \"+\" 30000)\n \"collapsed new-thread control in slot (page + slug_ui.js)\")\n- (locator/click (page/locator alice-pg \"#room-new-thread-ui-slot button.form-toggle\"))\n- (is (wait-for-text alice-pg \"#room-new-thread-ui-slot\"\n- \"hide new thread form\" 30000)\n+ (locator/click (page/locator alice-pg \"#new-thread-ui-slot button.form-toggle\"))\n+ (is (wait-for-text alice-pg \"#new-thread-compose\" \"create thread / post\" 30000)\n \"compose expanded via POST /ui morph (slug_ui.js)\")\n- (locator/fill (page/locator alice-pg \"#room-new-tag\") \"sse-thread\")\n- (locator/fill (page/locator alice-pg \"#room-new-thread-compose textarea\") \"seed thread\")\n- (locator/click (page/locator alice-pg \"#room-new-thread-form button[type='submit']\"))\n+ (locator/fill (page/locator alice-pg \"#new-thread-tag\") \"sse-thread\")\n+ (locator/fill (page/locator alice-pg \"#new-thread-compose textarea\") \"seed thread\")\n+ (locator/click (page/locator alice-pg \"#new-thread-form button[type='submit']\"))\n (page/wait-for-url alice-pg thread-url {:timeout 90000.0})\n (page/wait-for-load-state alice-pg :load)\n (is (wait-for-text alice-pg \"#thread-feed-region\" \"seed thread\" 45000)\n\n\nSide B — contributor: tommy-mor\nSide B — commit message:\n[d91e9431] fix(server): drop reduced read lock before room page layout\n\nPrecompute room_members_section_markup under the guard, then release\nbefore layout/html stringify. Tokio RwLock queues new readers behind a\nwaiting writer; holding the read guard across rendering could stall\nnavigations when ingests take the write lock.\n\nMade-with: Cursor\n\nSide B — unified diff (full patch):\ndiff --git a/server/src/html/forum.rs b/server/src/html/forum.rs\nindex f6e45b05bb92126966e304619f80ef1d45be7a4b..a2d36c0645cd5cdd97da78151a2fd574175e67f8 100644\n--- a/server/src/html/forum.rs\n+++ b/server/src/html/forum.rs\n@@ -965,18 +965,23 @@ pub async fn room_page(\n drop(reduced);\n return (StatusCode::NOT_FOUND, \"room not found\").into_response();\n };\n- let slug_display = room_slug.as_str();\n+ // Precompute markup that needs `&ReducerState`, then release the lock before `layout`.\n+ // `tokio::sync::RwLock` queues new readers behind a waiting writer; holding the read\n+ // guard across HTML/stringify could stall other navigations when an ingest needs a write lock.\n+ let members_markup = room_members_section_markup(&reduced, &room_id, false);\n let forum_cli = format!(\"npx slugsocial private {room_id} forum list\");\n let garden_cli = format!(\"npx slugsocial private {room_id} garden tree\");\n let audit_cli = format!(\"npx slugsocial private {room_id} audit\");\n+ drop(reduced);\n \n+ let slug_display = room_slug.as_str();\n let page = layout(\n &format!(\"room {slug_display} — slug.social\"),\n \"view-thread\",\n html! {\n (strip)\n nav class=\"breadcrumb\" { (bc_room(&nav, slug_display, None)) }\n- (room_members_section_markup(&reduced, &room_id, false))\n+ (members_markup)\n h3 { \"threads\" }\n (render_thread_feed(Some(&nav), \"room-thread-feed\", &rows, now))\n @if show_new {\n@@ -992,7 +997,6 @@ pub async fn room_page(\n theme_from_jar(&jar),\n &theme_next_from_uri(&uri),\n );\n- drop(reduced);\n Html(page.into_string()).into_response()\n }\n \n","role":"user"}],"model":"openai/gpt-chat-latest"}