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