Skip to content

Commit ed2d6dc

Browse files
authored
fix web chat refresh active thread (#2330)
1 parent 3cb77fe commit ed2d6dc

2 files changed

Lines changed: 51 additions & 4 deletions

File tree

crates/ironclaw_gateway/static/app.js

Lines changed: 20 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -3220,14 +3220,30 @@ function loadThreads() {
32203220
}
32213221
}
32223222

3223-
// Default to assistant thread on first load if no thread selected
3224-
if (!currentThreadId && assistantThreadId) {
3225-
switchToAssistant();
3223+
// Reopen the server's active thread on first load. This keeps the visible
3224+
// chat attached to an in-flight agent turn after a browser refresh, even
3225+
// when the URL does not carry an explicit thread hash.
3226+
if (!currentThreadId) {
3227+
const activeThreadId = data.active_thread || null;
3228+
if (activeThreadId && activeThreadId === assistantThreadId) {
3229+
switchToAssistant();
3230+
return;
3231+
}
3232+
if (activeThreadId && threads.some(t => t.id === activeThreadId)) {
3233+
switchThread(activeThreadId);
3234+
return;
3235+
}
3236+
if (assistantThreadId) {
3237+
switchToAssistant();
3238+
return;
3239+
}
32263240
}
32273241

32283242
// Enable/disable chat input based on channel type
32293243
if (currentThreadId) {
3230-
const currentThread = threads.find(t => t.id === currentThreadId);
3244+
const currentThread = currentThreadId === assistantThreadId
3245+
? data.assistant_thread
3246+
: threads.find(t => t.id === currentThreadId);
32313247
const ch = currentThread ? currentThread.channel : 'gateway';
32323248
currentThreadIsReadOnly = isReadOnlyChannel(ch);
32333249
if (currentThreadIsReadOnly) {

tests/e2e/scenarios/test_sse_reconnect.py

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -95,6 +95,37 @@ async def test_sse_reconnect_preserves_chat_history(page):
9595
assert user_msgs >= 1, "User message should be preserved after reconnect"
9696

9797

98+
async def test_refresh_without_hash_reopens_active_thread_history(page):
99+
"""Refreshing should reopen the server active thread when the URL has no thread hash."""
100+
await page.locator("#thread-new-btn").click()
101+
await page.wait_for_function(
102+
"() => !!currentThreadId && currentThreadId !== assistantThreadId"
103+
)
104+
thread_id = await page.evaluate("() => currentThreadId")
105+
106+
result = await send_chat_and_wait_for_terminal_message(
107+
page,
108+
"Refresh should keep this thread",
109+
)
110+
assert result["role"] == "assistant"
111+
112+
await page.evaluate(
113+
"() => history.replaceState(null, '', location.pathname + location.search)"
114+
)
115+
await page.reload()
116+
await page.wait_for_selector("#auth-screen", state="hidden", timeout=15000)
117+
await _wait_for_connected(page, timeout=15000)
118+
await page.wait_for_function(
119+
"(threadId) => currentThreadId === threadId",
120+
arg=thread_id,
121+
timeout=15000,
122+
)
123+
124+
await page.locator(SEL["message_user"]).filter(
125+
has_text="Refresh should keep this thread"
126+
).wait_for(state="visible", timeout=15000)
127+
128+
98129
async def test_sse_keepalive_comments_arrive(managed_gateway_server):
99130
"""Idle SSE connections should receive keepalive comments within 30 seconds."""
100131
async with sse_stream(managed_gateway_server.base_url, timeout=50) as response:

0 commit comments

Comments
 (0)