From e159837b740366a5421f8f07efdbb52dd1fa65b0 Mon Sep 17 00:00:00 2001 From: Patrick Buckley Date: Wed, 18 Mar 2026 15:14:14 -0700 Subject: [PATCH] fix: auto-titler SSE event + SSE reconnection after restart (#125) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * fix: auto-titler SSE event + SSE reconnection after restart _generate_title() now calls self.ui.on_rename() after persisting the title, so the tab bar, bridge, and console all update in real time. Also handles multi-part (vision) content and replaces silent except with log.debug. SSE onerror handler now parses the workstreams response, replaces the stale workstreams map, and switches to the first available workstream if the current ws_id no longer exists (e.g. after server restart). Previously it retried the stale ws_id forever. * fix: address PR #125 review — avoid double reconnect + sync tab bar Return immediately after switchTab/showDashboard on stale ws_id to prevent scheduling a redundant connectContentSSE via setTimeout. Always re-render tab bar after replacing the workstreams map so DOM stays in sync even when currentWsId is still valid. --- turnstone/core/session.py | 11 ++++++++--- turnstone/ui/static/app.js | 29 +++++++++++++++++++++++++---- 2 files changed, 33 insertions(+), 7 deletions(-) diff --git a/turnstone/core/session.py b/turnstone/core/session.py index 9c74a54c..c0636bcc 100644 --- a/turnstone/core/session.py +++ b/turnstone/core/session.py @@ -628,10 +628,14 @@ class ChatSession: user_msg = "" asst_msg = "" for m in self.messages: + content = m.get("content") or "" + # Handle multi-part content (vision messages) + if isinstance(content, list): + content = " ".join(p.get("text", "") for p in content if isinstance(p, dict)) if m["role"] == "user" and not user_msg: - user_msg = (m.get("content") or "")[:300] + user_msg = content[:300] elif m["role"] == "assistant" and not asst_msg: - asst_msg = (m.get("content") or "")[:200] + asst_msg = content[:200] if user_msg and asst_msg: break if not user_msg: @@ -667,8 +671,9 @@ class ChatSession: title = raw.split("\n")[0].strip().strip('"').strip("'") if title: update_workstream_title(self._ws_id, title[:80]) + self.ui.on_rename(title[:80]) except Exception: - pass # Title generation is non-critical + log.debug("Title generation failed for ws=%s", self._ws_id, exc_info=True) def resume(self, ws_id: str) -> bool: """Load messages from a previous workstream and resume it. diff --git a/turnstone/ui/static/app.js b/turnstone/ui/static/app.js index 5fb79eaf..adb5bcb0 100644 --- a/turnstone/ui/static/app.js +++ b/turnstone/ui/static/app.js @@ -592,10 +592,31 @@ function connectContentSSE(wsId) { showLogin(); return; } - setTimeout(function () { - connectContentSSE(currentWsId); - }, contentRetryDelay); - contentRetryDelay = Math.min(contentRetryDelay * 2, 30000); + return r.json().then(function (data) { + // Replace workstream list — IDs may have changed after restart + var freshIds = {}; + workstreams = {}; + (data.workstreams || []).forEach(function (ws) { + workstreams[ws.id] = { name: ws.name, state: ws.state }; + freshIds[ws.id] = true; + }); + // Always re-render tabs since workstreams map was replaced + renderTabBar(); + // If current ws_id is stale, switch to first available + if (currentWsId && !freshIds[currentWsId]) { + var ids = Object.keys(freshIds); + if (ids.length) { + switchTab(ids[0]); + } else { + showDashboard(); + } + return; // switchTab/showDashboard handles SSE connection + } + setTimeout(function () { + connectContentSSE(currentWsId); + }, contentRetryDelay); + contentRetryDelay = Math.min(contentRetryDelay * 2, 30000); + }); }) .catch(function () { setTimeout(function () {