diff --git a/assets/SETUP_FEISHU.md b/docs/SETUP_FEISHU.md
similarity index 100%
rename from assets/SETUP_FEISHU.md
rename to docs/SETUP_FEISHU.md
diff --git a/frontends/desktop/static/app.js b/frontends/desktop/static/app.js
index 281abc16..849cf282 100644
--- a/frontends/desktop/static/app.js
+++ b/frontends/desktop/static/app.js
@@ -655,6 +655,32 @@ function setActiveSession(id) {
renderDiagnostics();
const runtime = getSessionRuntime(sess);
setBusy(runtime.busy, runtime.busy ? 'Agent is responding…' : null, sess);
+ // When switching to a session that is still running, ensure the live draft
+ // is rendered immediately and polling is active (it may have been started
+ // earlier but its render calls were no-ops because the session wasn't active).
+ if (runtime.busy) {
+ const draft = runtime.assistantDraft;
+ if (draft && !draft.finalized) {
+ renderAssistantDraftInPlace(sess, draft);
+ }
+ // Restart polling if it stopped (e.g. page reload or race condition)
+ if (!runtime.polling) {
+ runtime.forcePollOnce = true;
+ pollSessionMessages(sess);
+ } else {
+ // Polling is running but was rendering as no-op while we were away.
+ // Do an immediate one-shot poll to refresh the view right now.
+ (async () => {
+ try {
+ const res = await GaBridge.pollSession(sess.bridgeSessionId || sess.id, runtime.lastPolledMessageId || 0);
+ if (res?.error) return;
+ const result = res.result || res;
+ for (const msg of (result.messages || [])) upsertPolledMessage(sess, msg, { partial: false });
+ if (result.partial) upsertPolledMessage(sess, result.partial, { partial: true });
+ } catch(e) { /* ignore, regular polling will handle it */ }
+ })();
+ }
+ }
}
function renderSessionList() {
@@ -837,6 +863,15 @@ function renderMessages() {
messagesEl.innerHTML = '';
messagesEl.appendChild(cached.fragment);
_domCache.delete(state.activeId);
+ // If there's a live draft, the cached DOM is stale — re-render the draft portion
+ if (hasDraft) {
+ // Remove the stale assistant wrap (last unfinalized msg-assistant element)
+ const last = messagesEl.lastElementChild;
+ if (last?.classList?.contains('msg-assistant') && last.dataset.finalized !== '1') {
+ last.remove();
+ }
+ renderAssistantDraft(sess, runtime.assistantDraft);
+ }
messagesEl.scrollTop = cached.scrollTop;
} else {
messagesEl.innerHTML = '';
@@ -953,6 +988,30 @@ function scrollToBottom(smooth = true) {
// ACP sends method='session/update' with params.update.sessionUpdate=
// agent_message_chunk | agent_thought_chunk | tool_call | tool_call_update | plan | available_commands_update
function handleNotification(msg) {
+ // Handle WS session-state notifications from the bridge backend.
+ // These have {type: "session-state", sessionId, state, status, seq, ...}
+ // and are used to kick-start polling for sessions that became active
+ // (e.g. after page reload, or when a background session starts running).
+ if (msg.type === 'session-state') {
+ const sess = findSessionByBridgeId(msg.sessionId);
+ if (!sess) return;
+ const runtime = getSessionRuntime(sess);
+ if ((msg.state === 'running' || msg.status === 'running') && !runtime.polling) {
+ runtime.busy = true;
+ runtime.forcePollOnce = true;
+ setBusy(true, 'Thinking…', sess);
+ pollSessionMessages(sess);
+ } else if (msg.state === 'idle' || msg.state === 'error' || msg.status === 'idle') {
+ // Session finished in background — do a final poll to pick up remaining messages
+ if (!runtime.polling && runtime.busy) {
+ runtime.forcePollOnce = true;
+ pollSessionMessages(sess);
+ }
+ }
+ // Update tab dot regardless
+ renderSessionList();
+ return;
+ }
if (msg.method !== 'session/update') return;
const update = msg.params?.update;
if (!update) return;
@@ -1264,6 +1323,10 @@ function finalizeAssistantReply(sess) {
wrap.dataset.taskEndedAt = String(endedAt);
ensureAssistantTaskElapsed(wrap, wrap.dataset.taskStartedAt || runtime.taskStartedAt || draft?.taskStartedAt, endedAt);
renderMessages();
+ } else if (!isActiveSession(sess)) {
+ // Session finished in background — its DOM cache is stale, discard it
+ // so that switching to it will do a full re-render from sess.messages
+ _domCache.delete(sess.id);
}
stopTaskTimer(sess);
}
diff --git a/frontends/desktop/static/index.html b/frontends/desktop/static/index.html
index 3fa70a5a..e86ba27e 100644
--- a/frontends/desktop/static/index.html
+++ b/frontends/desktop/static/index.html
@@ -111,6 +111,6 @@
-
+