From 07378f3fe866bcf7b01d3a5bc5969bd4f4383739 Mon Sep 17 00:00:00 2001 From: Thomas Marchand Date: Sat, 3 Jan 2026 15:32:34 +0000 Subject: [PATCH] Fix mission viewing UX issues - Fix URL not updating when switching missions (add router.replace) - Fix "Agent is working..." showing for non-running missions - Fix streaming indicator showing for wrong mission - Improve thinking bubble: larger max-height, scroll, "Processing..." placeholder - Don't auto-collapse thinking for sessions > 30 seconds - Better error messages for failed/not_feasible missions --- dashboard/src/app/control/control-client.tsx | 55 ++++++++++++++----- .../Views/Control/ControlView.swift | 42 +++++++++----- 2 files changed, 69 insertions(+), 28 deletions(-) diff --git a/dashboard/src/app/control/control-client.tsx b/dashboard/src/app/control/control-client.tsx index 15b081f..a6658d6 100644 --- a/dashboard/src/app/control/control-client.tsx +++ b/dashboard/src/app/control/control-client.tsx @@ -286,15 +286,22 @@ function ThinkingItem({ }, [item.done, item.startTime]); // Auto-collapse when thinking is done (with delay) + // Longer delay for extended thinking sessions to let user see the summary useEffect(() => { if (item.done && expanded && !hasAutoCollapsedRef.current) { + const duration = Math.floor((Date.now() - item.startTime) / 1000); + // Don't auto-collapse if thinking was > 30 seconds (user may want to review) + if (duration > 30) { + hasAutoCollapsedRef.current = true; // Mark as handled but don't collapse + return; + } const timer = setTimeout(() => { setExpanded(false); hasAutoCollapsedRef.current = true; - }, 500); + }, 1500); // Longer delay return () => clearTimeout(timer); } - }, [item.done, expanded]); + }, [item.done, expanded, item.startTime]); const formatDuration = (seconds: number) => { if (seconds < 60) return `${seconds}s`; @@ -340,12 +347,12 @@ function ThinkingItem({
-
- {item.content} +
+ {item.content || Processing...}
@@ -1007,16 +1014,19 @@ export default function ControlClient() { setViewingMissionId(missionId); fetchingMissionIdRef.current = missionId; + // Update URL immediately so it's shareable/bookmarkable + router.replace(`/control?mission=${missionId}`, { scroll: false }); + // Always load fresh history from API when switching missions // This ensures we don't show stale cached events try { const mission = await getMission(missionId); - + // Race condition guard: only update if this is still the mission we want if (fetchingMissionIdRef.current !== missionId) { return; // Another mission was requested, discard this response } - + const historyItems = missionHistoryToItems(mission); setItems(historyItems); // Update cache with fresh data @@ -1027,30 +1037,33 @@ export default function ControlClient() { } } catch (err) { console.error("Failed to load mission:", err); - + // Race condition guard: only update if this is still the mission we want if (fetchingMissionIdRef.current !== missionId) { return; } - + // Revert viewing state to avoid filtering out events const fallbackMission = previousViewingMission ?? currentMissionRef.current; if (fallbackMission) { setViewingMissionId(fallbackMission.id); setViewingMission(fallbackMission); setItems(missionHistoryToItems(fallbackMission)); + router.replace(`/control?mission=${fallbackMission.id}`, { scroll: false }); } else if (previousViewingId && missionItems[previousViewingId]) { setViewingMissionId(previousViewingId); setViewingMission(null); setItems(missionItems[previousViewingId]); + router.replace(`/control?mission=${previousViewingId}`, { scroll: false }); } else { setViewingMissionId(null); setViewingMission(null); setItems([]); + router.replace(`/control`, { scroll: false }); } } }, - [missionItems, missionHistoryToItems] + [missionItems, missionHistoryToItems, router] ); // Sync viewingMissionId with currentMission only when there's no explicit viewing mission set @@ -1721,11 +1734,19 @@ export default function ControlClient() { {/* Queue count */}
-
+
0 ? `${queueLen} message${queueLen > 1 ? 's' : ''} waiting to be processed` : 'No messages queued'} + > Queue - + 0 && queueLen < 3 && "text-amber-400", + queueLen >= 3 && "text-orange-400" + )}> {queueLen}
@@ -1872,7 +1893,7 @@ export default function ControlClient() {
- {runState === "running" ? ( + {viewingMissionIsRunning ? ( ) : ( @@ -1880,7 +1901,7 @@ export default function ControlClient() {
{missionLoading ? ( - ) : runState === "running" ? ( + ) : viewingMissionIsRunning ? ( <>

Agent is working... @@ -1904,6 +1925,10 @@ export default function ControlClient() { <>This mission was interrupted (server shutdown or cancellation). Click the Resume button in the mission menu to continue where you left off. ) : activeMission.status === "blocked" ? ( <>The agent reached its iteration limit ({maxIterations}). You can continue the mission to give it more iterations. + ) : activeMission.status === "failed" ? ( + <>This mission failed without producing any messages. + ) : activeMission.status === "not_feasible" ? ( + <>The agent determined this task was not feasible. ) : ( <>This mission was {activeMission.status} without any messages. {activeMission.status === "completed" && " You can reactivate it to continue."} @@ -2232,7 +2257,7 @@ export default function ControlClient() { })} {/* Show streaming indicator when running but no active thinking/phase */} - {runState === "running" && + {viewingMissionIsRunning && items.length > 0 && !items.some( (it) => diff --git a/ios_dashboard/OpenAgentDashboard/Views/Control/ControlView.swift b/ios_dashboard/OpenAgentDashboard/Views/Control/ControlView.swift index 7b06200..cfafca2 100644 --- a/ios_dashboard/OpenAgentDashboard/Views/Control/ControlView.swift +++ b/ios_dashboard/OpenAgentDashboard/Views/Control/ControlView.swift @@ -1435,20 +1435,29 @@ private struct ThinkingBubble: View { // Expandable content if isExpanded && !message.content.isEmpty { - Text(message.content) + ScrollView { + Text(message.content) + .font(.caption) + .foregroundStyle(Theme.textTertiary) + .frame(maxWidth: .infinity, alignment: .leading) + } + .frame(maxHeight: 300) // Allow scrolling for long thinking content + .padding(.horizontal, 12) + .padding(.vertical, 8) + .background(Color.white.opacity(0.02)) + .clipShape(RoundedRectangle(cornerRadius: 12, style: .continuous)) + .overlay( + RoundedRectangle(cornerRadius: 12, style: .continuous) + .stroke(Theme.border, lineWidth: 0.5) + ) + .transition(.opacity.combined(with: .scale(scale: 0.95, anchor: .top))) + } else if isExpanded && message.content.isEmpty { + Text("Processing...") .font(.caption) - .foregroundStyle(Theme.textTertiary) - .lineLimit(message.thinkingDone ? 8 : nil) + .italic() + .foregroundStyle(Theme.textMuted) .padding(.horizontal, 12) .padding(.vertical, 8) - .frame(maxWidth: .infinity, alignment: .leading) - .background(Color.white.opacity(0.02)) - .clipShape(RoundedRectangle(cornerRadius: 12, style: .continuous)) - .overlay( - RoundedRectangle(cornerRadius: 12, style: .continuous) - .stroke(Theme.border, lineWidth: 0.5) - ) - .transition(.opacity.combined(with: .scale(scale: 0.95, anchor: .top))) } } .onAppear { @@ -1456,8 +1465,15 @@ private struct ThinkingBubble: View { } .onChange(of: message.thinkingDone) { _, done in if done && !hasAutoCollapsed { - // Auto-collapse after a brief delay - DispatchQueue.main.asyncAfter(deadline: .now() + 0.5) { + // Don't auto-collapse for extended thinking (> 30 seconds) + // User may want to review what the agent was thinking about + let duration = message.thinkingStartTime.map { Int(Date().timeIntervalSince($0)) } ?? 0 + if duration > 30 { + hasAutoCollapsed = true // Mark as handled but don't collapse + return + } + // Auto-collapse shorter thinking after a delay + DispatchQueue.main.asyncAfter(deadline: .now() + 1.5) { withAnimation(.spring(duration: 0.25)) { isExpanded = false hasAutoCollapsed = true