diff --git a/crates/daemon/assets/index.html b/crates/daemon/assets/index.html index 1af12beb..f92cbad9 100644 --- a/crates/daemon/assets/index.html +++ b/crates/daemon/assets/index.html @@ -1973,6 +1973,7 @@ function refreshCurrentSessionAfterReconnect() { if (!state.currentId) return; const s = state.sessions.find((x) => x.id === state.currentId); + hydrateWidgets(state.currentId, { force: true }); if (state.mode === "terminal" || isTerminalSession(s)) { // Keep the live xterm buffer exactly where the user left it. Replaying // transcript or PTY history on reconnect appends old output into xterm, @@ -3851,8 +3852,8 @@ target.appendChild(row); } -async function hydrateWidgets(id) { - if (!id || state.widgetsById.has(id)) { +async function hydrateWidgets(id, opts = {}) { + if (!id || (!opts.force && state.widgetsById.has(id))) { renderWidgets(); return; } diff --git a/crates/e2e/tests/web_smoke.rs b/crates/e2e/tests/web_smoke.rs index b31becce..265fddf7 100644 --- a/crates/e2e/tests/web_smoke.rs +++ b/crates/e2e/tests/web_smoke.rs @@ -1120,15 +1120,32 @@ async fn web_client_loads_and_websocket_connects() { const oldCurrent = state.currentId; const oldSessions = state.sessions; const oldSize = state.lastReportedSize; + const oldWidgets = state.widgetsById; const oldRpc = rpc; const calls = []; rpc = async (method, params) => { calls.push({ method, params }); + if (method === 'session.get') { + return { + ui_panels: [{ + id: 'status', + title: 'Status', + markdown: '# Status\nupdated while offline', + }], + }; + } return method === 'session.transcript' ? { events: [] } : null; }; state.mode = 'terminal'; state.currentId = 's-reconnect-terminal'; state.sessions = [{ id: 's-reconnect-terminal', has_pty: true, mode: 'interactive' }]; + state.widgetsById = new Map([ + ['s-reconnect-terminal', [{ + id: 'status', + title: 'Status', + markdown: '# Status\nstale', + }]], + ]); state.lastReportedSize = { cols: 100, rows: 40 }; state.term = { cols: 100, @@ -1139,7 +1156,10 @@ async fn web_client_loads_and_websocket_connects() { state.fitAddon = { fit: () => calls.push('fit') }; refreshCurrentSessionAfterReconnect(); + await new Promise((resolve) => setTimeout(resolve, 0)); await new Promise((resolve) => requestAnimationFrame(resolve)); + const widgets = state.widgetsById.get('s-reconnect-terminal') || []; + const widgetMarkdown = widgets[0]?.markdown || ''; state.term = oldTerm; state.fitAddon = oldFit; @@ -1147,8 +1167,9 @@ async fn web_client_loads_and_websocket_connects() { state.currentId = oldCurrent; state.sessions = oldSessions; state.lastReportedSize = oldSize; + state.widgetsById = oldWidgets; rpc = oldRpc; - return { calls }; + return { calls, widgetMarkdown }; })() "#, ) @@ -1169,6 +1190,18 @@ async fn web_client_loads_and_websocket_connects() { }), "terminal reconnect should not replay transcript/PTY history: {reconnect_terminal:?}" ); + assert!( + reconnect_calls + .iter() + .any(|v| v["method"].as_str() == Some("session.get")), + "terminal reconnect should refresh widget panels: {reconnect_terminal:?}" + ); + assert!( + reconnect_terminal["widgetMarkdown"] + .as_str() + .is_some_and(|markdown| markdown.contains("updated while offline")), + "terminal reconnect should replace stale widget cache: {reconnect_terminal:?}" + ); // The remote client mirrors zarvis EditorState events in a // terminal-mode strip so PTY-backed zarvis input is visible even