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'
+ '
"
)
-# 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 = (
"