From 6fe9f75c3c264b3646ee8299bbc2a931e01239da Mon Sep 17 00:00:00 2001 From: William <16777628+sillyWillieBilly@users.noreply.github.com> Date: Mon, 6 Apr 2026 08:13:45 +0200 Subject: [PATCH] feat(console): theme-aware banner, judge model support, Google provider in admin - Replace inline-style console banner with CSS classes + light/dark theme - Node ID in banner is now a clickable link back to the node UI - Add judge_model parameter to create_workstream flow - Add Google to model provider list with default URL - Provider-specific placeholder hints in model editor - Detect results populate model name suggestions datalist - Theme changes in admin settings apply immediately - Persist theme selection to server via settings API - Use workstream title field (with name fallback) in collector SSE events - Add judge model dropdown to new-workstream modal --- turnstone/console/collector.py | 7 +- turnstone/console/server.py | 60 +++++++--- turnstone/console/static/admin.js | 168 ++++++++++++++++++---------- turnstone/console/static/app.js | 26 ++++- turnstone/console/static/index.html | 5 + 5 files changed, 187 insertions(+), 79 deletions(-) diff --git a/turnstone/console/collector.py b/turnstone/console/collector.py index 010b8e26..09b18c57 100644 --- a/turnstone/console/collector.py +++ b/turnstone/console/collector.py @@ -394,7 +394,8 @@ class ClusterCollector: { "type": "ws_created", "ws_id": ws_id, - "name": ws.get("name", ""), + "name": ws.get("title", "") or ws.get("name", ""), + "title": ws.get("title", ""), "node_id": node_id, } ) @@ -418,8 +419,8 @@ class ClusterCollector: "content": new_w.get("content", ""), } ) - old_name = old_ws.get("name", "") - new_name = new_w.get("name", "") + old_name = old_ws.get("title", "") or old_ws.get("name", "") + new_name = new_w.get("title", "") or new_w.get("name", "") if old_name != new_name and new_name: pending.append({"type": "ws_rename", "ws_id": ws_id, "name": new_name}) node.workstreams = new_ws diff --git a/turnstone/console/server.py b/turnstone/console/server.py index d40c8629..a37b324c 100644 --- a/turnstone/console/server.py +++ b/turnstone/console/server.py @@ -129,21 +129,36 @@ _JS_PROXY_SHIM = """\ """ _CONSOLE_BANNER_TEMPLATE = ( - '
' - '" - "← Console" - '\u2502' - 'NODE_ID_PLACEHOLDER' + '
' + '← Console' + '\u2502' + '' + "NODE_ID_PLACEHOLDER" "
" ) -# Injected " +# Injected " +) _VALID_NODE_ID = re.compile(r"^[a-zA-Z0-9._-]+$") @@ -446,6 +461,7 @@ async def create_workstream(request: Request) -> JSONResponse: raw_node_id = body.get("node_id", "") raw_name = body.get("name", "") raw_model = body.get("model", "") + raw_judge_model = body.get("judge_model", "") raw_initial_message = body.get("initial_message", "") raw_skill = body.get("skill", "") raw_resume_ws = body.get("resume_ws", "") @@ -455,6 +471,8 @@ async def create_workstream(request: Request) -> JSONResponse: raw_name = "" if raw_name is None else None if not isinstance(raw_model, str): raw_model = "" if raw_model is None else None + if not isinstance(raw_judge_model, str): + raw_judge_model = "" if raw_judge_model is None else None if not isinstance(raw_initial_message, str): raw_initial_message = "" if raw_initial_message is None else None if not isinstance(raw_skill, str): @@ -465,19 +483,21 @@ async def create_workstream(request: Request) -> JSONResponse: raw_node_id is None or raw_name is None or raw_model is None + or raw_judge_model is None or raw_initial_message is None or raw_skill is None or raw_resume_ws is None ): return JSONResponse( { - "error": "node_id, name, model, initial_message, skill, and resume_ws must be strings" + "error": "node_id, name, model, judge_model, initial_message, skill, and resume_ws must be strings" }, status_code=400, ) node_id = raw_node_id name = raw_name[:256] model = raw_model[:128] + judge_model = raw_judge_model[:128] initial_message = raw_initial_message[:4096] skill = raw_skill[:256] resume_ws = raw_resume_ws[:64] @@ -509,6 +529,7 @@ async def create_workstream(request: Request) -> JSONResponse: ws_body = { "name": name, "model": model, + "judge_model": judge_model, "initial_message": initial_message, "skill": skill, "resume_ws": resume_ws, @@ -918,7 +939,7 @@ async def proxy_index(request: Request) -> Response: page = page.replace('href="/shared/', f'href="{prefix}/shared/') page = page.replace('src="/shared/', f'src="{prefix}/shared/') # Inject console-return banner + proxy shim after - banner = _CONSOLE_BANNER_TEMPLATE.replace("NODE_ID_PLACEHOLDER", html.escape(node_id)) + banner = _CONSOLE_BANNER_TEMPLATE.replace("NODE_ID_PLACEHOLDER", html.escape(node_id)).replace("NODE_LINK_PLACEHOLDER", html.escape(prefix + "/")) shim = ( "