Spaces:
Running on CPU Upgrade
Running on CPU Upgrade
Commit ·
ccfb504
1
Parent(s): 84d6321
Persist research subtool steps across page refresh
Browse filesSave research steps and stats to localStorage on every update,
restore atomically with isProcessing on mount so the rolling
display survives page refresh during active research.
frontend/src/components/Chat/ToolCallGroup.tsx
CHANGED
|
@@ -10,6 +10,7 @@ import BlockIcon from '@mui/icons-material/Block';
|
|
| 10 |
import { useAgentStore } from '@/store/agentStore';
|
| 11 |
import { useLayoutStore } from '@/store/layoutStore';
|
| 12 |
import { logger } from '@/utils/logger';
|
|
|
|
| 13 |
import type { UIMessage } from 'ai';
|
| 14 |
|
| 15 |
// ---------------------------------------------------------------------------
|
|
@@ -163,7 +164,7 @@ function formatResearchStep(raw: string): { label: string } {
|
|
| 163 |
/** Rolling 2-line display of research sub-tool calls — hidden when complete. */
|
| 164 |
function ResearchSteps({ steps, isRunning }: { steps: string[]; isRunning: boolean }) {
|
| 165 |
if (!isRunning) return null;
|
| 166 |
-
const visible = steps.slice(-
|
| 167 |
if (visible.length === 0) return null;
|
| 168 |
|
| 169 |
return (
|
|
|
|
| 10 |
import { useAgentStore } from '@/store/agentStore';
|
| 11 |
import { useLayoutStore } from '@/store/layoutStore';
|
| 12 |
import { logger } from '@/utils/logger';
|
| 13 |
+
import { RESEARCH_MAX_STEPS } from '@/lib/research-store';
|
| 14 |
import type { UIMessage } from 'ai';
|
| 15 |
|
| 16 |
// ---------------------------------------------------------------------------
|
|
|
|
| 164 |
/** Rolling 2-line display of research sub-tool calls — hidden when complete. */
|
| 165 |
function ResearchSteps({ steps, isRunning }: { steps: string[]; isRunning: boolean }) {
|
| 166 |
if (!isRunning) return null;
|
| 167 |
+
const visible = steps.slice(-RESEARCH_MAX_STEPS);
|
| 168 |
if (visible.length === 0) return null;
|
| 169 |
|
| 170 |
return (
|
frontend/src/hooks/useAgentChat.ts
CHANGED
|
@@ -12,6 +12,7 @@ import { useChat } from '@ai-sdk/react';
|
|
| 12 |
import { type UIMessage, lastAssistantMessageIsCompleteWithApprovalResponses } from 'ai';
|
| 13 |
import { SSEChatTransport, type SideChannelCallbacks } from '@/lib/sse-chat-transport';
|
| 14 |
import { loadMessages, saveMessages } from '@/lib/chat-message-store';
|
|
|
|
| 15 |
import { llmMessagesToUIMessages } from '@/lib/convert-llm-messages';
|
| 16 |
import { apiFetch } from '@/utils/api';
|
| 17 |
import { useAgentStore } from '@/store/agentStore';
|
|
@@ -92,32 +93,39 @@ export function useAgentChat({ sessionId, isActive, onReady, onError, onSessionD
|
|
| 92 |
const stats = { ...sessState.researchStats };
|
| 93 |
|
| 94 |
if (log === 'Starting research sub-agent...') {
|
|
|
|
| 95 |
updateSession(sessionId, {
|
| 96 |
researchSteps: [],
|
| 97 |
-
researchStats:
|
| 98 |
activityStatus: { type: 'tool', toolName: 'research', description: log },
|
| 99 |
});
|
|
|
|
| 100 |
} else if (log.startsWith('tokens:')) {
|
| 101 |
stats.tokenCount = parseInt(log.slice(7), 10);
|
| 102 |
updateSession(sessionId, { researchStats: stats });
|
|
|
|
| 103 |
} else if (log.startsWith('tools:')) {
|
| 104 |
stats.toolCount = parseInt(log.slice(6), 10);
|
| 105 |
updateSession(sessionId, { researchStats: stats });
|
|
|
|
| 106 |
} else if (log === 'Research complete.') {
|
| 107 |
const elapsed = stats.startedAt
|
| 108 |
? Math.round((Date.now() - stats.startedAt) / 1000)
|
| 109 |
: null;
|
|
|
|
| 110 |
updateSession(sessionId, {
|
| 111 |
-
researchStats:
|
| 112 |
activityStatus: { type: 'tool', toolName: 'research', description: log },
|
| 113 |
});
|
|
|
|
| 114 |
} else {
|
| 115 |
-
// Regular tool call step — append
|
| 116 |
-
const steps = [...sessState.researchSteps, log];
|
| 117 |
updateSession(sessionId, {
|
| 118 |
researchSteps: steps,
|
| 119 |
activityStatus: { type: 'tool', toolName: 'research', description: log },
|
| 120 |
});
|
|
|
|
| 121 |
}
|
| 122 |
return;
|
| 123 |
}
|
|
@@ -372,9 +380,25 @@ export function useAgentChat({ sessionId, isActive, onReady, onError, onSessionD
|
|
| 372 |
// results make tools look "done" even when the agent is still
|
| 373 |
// mid-turn and about to call more tools.
|
| 374 |
if (backendIsProcessing) {
|
| 375 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 376 |
} else if (pendingIds && pendingIds.size > 0) {
|
| 377 |
updateSession(sessionId, { activityStatus: { type: 'waiting-approval' } });
|
|
|
|
|
|
|
|
|
|
| 378 |
}
|
| 379 |
} catch {
|
| 380 |
/* backend unreachable -- localStorage fallback is fine */
|
|
|
|
| 12 |
import { type UIMessage, lastAssistantMessageIsCompleteWithApprovalResponses } from 'ai';
|
| 13 |
import { SSEChatTransport, type SideChannelCallbacks } from '@/lib/sse-chat-transport';
|
| 14 |
import { loadMessages, saveMessages } from '@/lib/chat-message-store';
|
| 15 |
+
import { saveResearch, loadResearch, clearResearch, RESEARCH_MAX_STEPS } from '@/lib/research-store';
|
| 16 |
import { llmMessagesToUIMessages } from '@/lib/convert-llm-messages';
|
| 17 |
import { apiFetch } from '@/utils/api';
|
| 18 |
import { useAgentStore } from '@/store/agentStore';
|
|
|
|
| 93 |
const stats = { ...sessState.researchStats };
|
| 94 |
|
| 95 |
if (log === 'Starting research sub-agent...') {
|
| 96 |
+
const newStats = { toolCount: 0, tokenCount: 0, startedAt: Date.now(), finalElapsed: null };
|
| 97 |
updateSession(sessionId, {
|
| 98 |
researchSteps: [],
|
| 99 |
+
researchStats: newStats,
|
| 100 |
activityStatus: { type: 'tool', toolName: 'research', description: log },
|
| 101 |
});
|
| 102 |
+
saveResearch(sessionId, [], newStats);
|
| 103 |
} else if (log.startsWith('tokens:')) {
|
| 104 |
stats.tokenCount = parseInt(log.slice(7), 10);
|
| 105 |
updateSession(sessionId, { researchStats: stats });
|
| 106 |
+
saveResearch(sessionId, sessState.researchSteps, stats);
|
| 107 |
} else if (log.startsWith('tools:')) {
|
| 108 |
stats.toolCount = parseInt(log.slice(6), 10);
|
| 109 |
updateSession(sessionId, { researchStats: stats });
|
| 110 |
+
saveResearch(sessionId, sessState.researchSteps, stats);
|
| 111 |
} else if (log === 'Research complete.') {
|
| 112 |
const elapsed = stats.startedAt
|
| 113 |
? Math.round((Date.now() - stats.startedAt) / 1000)
|
| 114 |
: null;
|
| 115 |
+
const doneStats = { ...stats, startedAt: null, finalElapsed: elapsed };
|
| 116 |
updateSession(sessionId, {
|
| 117 |
+
researchStats: doneStats,
|
| 118 |
activityStatus: { type: 'tool', toolName: 'research', description: log },
|
| 119 |
});
|
| 120 |
+
clearResearch(sessionId);
|
| 121 |
} else {
|
| 122 |
+
// Regular tool call step — append (trim to max)
|
| 123 |
+
const steps = [...sessState.researchSteps, log].slice(-RESEARCH_MAX_STEPS);
|
| 124 |
updateSession(sessionId, {
|
| 125 |
researchSteps: steps,
|
| 126 |
activityStatus: { type: 'tool', toolName: 'research', description: log },
|
| 127 |
});
|
| 128 |
+
saveResearch(sessionId, steps, stats);
|
| 129 |
}
|
| 130 |
return;
|
| 131 |
}
|
|
|
|
| 380 |
// results make tools look "done" even when the agent is still
|
| 381 |
// mid-turn and about to call more tools.
|
| 382 |
if (backendIsProcessing) {
|
| 383 |
+
// Restore research sub-agent state alongside isProcessing in one
|
| 384 |
+
// atomic update so the UI never sees isProcessing=false with stale
|
| 385 |
+
// tool states (which would coerce them to 'output-available').
|
| 386 |
+
const savedResearch = loadResearch(sessionId);
|
| 387 |
+
updateSession(sessionId, {
|
| 388 |
+
isProcessing: true,
|
| 389 |
+
activityStatus: savedResearch?.stats.startedAt
|
| 390 |
+
? { type: 'tool', toolName: 'research', description: 'Resuming research...' }
|
| 391 |
+
: { type: 'thinking' },
|
| 392 |
+
...(savedResearch && {
|
| 393 |
+
researchSteps: savedResearch.steps,
|
| 394 |
+
researchStats: savedResearch.stats,
|
| 395 |
+
}),
|
| 396 |
+
});
|
| 397 |
} else if (pendingIds && pendingIds.size > 0) {
|
| 398 |
updateSession(sessionId, { activityStatus: { type: 'waiting-approval' } });
|
| 399 |
+
clearResearch(sessionId);
|
| 400 |
+
} else {
|
| 401 |
+
clearResearch(sessionId);
|
| 402 |
}
|
| 403 |
} catch {
|
| 404 |
/* backend unreachable -- localStorage fallback is fine */
|
frontend/src/lib/research-store.ts
ADDED
|
@@ -0,0 +1,56 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
/**
|
| 2 |
+
* Persist research sub-agent state (steps + stats) per session.
|
| 3 |
+
* Survives page refresh so the rolling display isn't lost mid-research.
|
| 4 |
+
*/
|
| 5 |
+
import type { PerSessionState } from '@/store/agentStore';
|
| 6 |
+
|
| 7 |
+
/** Max steps to keep in storage and display. Single source of truth. */
|
| 8 |
+
export const RESEARCH_MAX_STEPS = 40;
|
| 9 |
+
|
| 10 |
+
const STORAGE_KEY = 'hf-agent-research';
|
| 11 |
+
|
| 12 |
+
type ResearchState = {
|
| 13 |
+
steps: string[];
|
| 14 |
+
stats: PerSessionState['researchStats'];
|
| 15 |
+
};
|
| 16 |
+
|
| 17 |
+
type ResearchMap = Record<string, ResearchState>;
|
| 18 |
+
|
| 19 |
+
function readAll(): ResearchMap {
|
| 20 |
+
try {
|
| 21 |
+
const raw = localStorage.getItem(STORAGE_KEY);
|
| 22 |
+
return raw ? JSON.parse(raw) : {};
|
| 23 |
+
} catch {
|
| 24 |
+
return {};
|
| 25 |
+
}
|
| 26 |
+
}
|
| 27 |
+
|
| 28 |
+
function writeAll(map: ResearchMap): void {
|
| 29 |
+
try {
|
| 30 |
+
localStorage.setItem(STORAGE_KEY, JSON.stringify(map));
|
| 31 |
+
} catch { /* quota exceeded — ignore */ }
|
| 32 |
+
}
|
| 33 |
+
|
| 34 |
+
export function saveResearch(
|
| 35 |
+
sessionId: string,
|
| 36 |
+
steps: string[],
|
| 37 |
+
stats: PerSessionState['researchStats'],
|
| 38 |
+
): void {
|
| 39 |
+
const map = readAll();
|
| 40 |
+
map[sessionId] = {
|
| 41 |
+
steps: steps.slice(-RESEARCH_MAX_STEPS),
|
| 42 |
+
stats,
|
| 43 |
+
};
|
| 44 |
+
writeAll(map);
|
| 45 |
+
}
|
| 46 |
+
|
| 47 |
+
export function loadResearch(sessionId: string): ResearchState | null {
|
| 48 |
+
const map = readAll();
|
| 49 |
+
return map[sessionId] ?? null;
|
| 50 |
+
}
|
| 51 |
+
|
| 52 |
+
export function clearResearch(sessionId: string): void {
|
| 53 |
+
const map = readAll();
|
| 54 |
+
delete map[sessionId];
|
| 55 |
+
writeAll(map);
|
| 56 |
+
}
|