feat(FN-3058): session-first quick chat with heartbeat prompt improvements
This merge completes the session-first quick chat FAB flow (Step 5), restores the quick chat hook after fixing a styling regression, and fixes heartbeat prompt composition for autonomous agents. It also adds the planning mode question display with new-session reset, introduces new Droid CLI settings Fusion-Task-Id: FN-3058
This commit is contained in:
@@ -45,9 +45,11 @@ Quick Chat is an optional floating panel for fast, project-scoped assistant conv
|
|||||||
- Supports agent mentions (`@agent`) and file mentions
|
- Supports agent mentions (`@agent`) and file mentions
|
||||||
- Uses the same model/provider infrastructure as full Chat view
|
- Uses the same model/provider infrastructure as full Chat view
|
||||||
- On small screens, compact tool-call summaries in the floating panel intentionally stay single-line (count + tool names + status) to preserve message density
|
- On small screens, compact tool-call summaries in the floating panel intentionally stay single-line (count + tool names + status) to preserve message density
|
||||||
- `switchSession()` resumes the existing thread for the selected target (agent-only or agent+model)
|
- The panel header uses a session-first flow: the main dropdown lists persisted sessions (preferring `session.title`, then falling back to deterministic `Session N` labels)
|
||||||
- `startFreshSession()` is the explicit new-thread action and always creates a new persisted session
|
- Selecting a session from that dropdown resumes the persisted conversation; this keeps `switchSession()` resume-oriented rather than forcing a new thread
|
||||||
- Resume lookups use a targeted session query instead of loading the full active session list first
|
- The `+` action opens an inline new-session chooser (inside the panel, not a modal) with `Model` selected by default and optional switch to `Agent`
|
||||||
|
- Submitting the inline chooser uses explicit fresh-session creation and immediately persists/selects the new thread, then refreshes the session dropdown list
|
||||||
|
- Resume lookups still use targeted session queries instead of loading the full active-session list first
|
||||||
- Tool-call summaries in the floating quick-chat panel are intentionally condensed into a single-line header row (especially on small screens) so tool name + status stay scannable without multi-line wrapping
|
- Tool-call summaries in the floating quick-chat panel are intentionally condensed into a single-line header row (especially on small screens) so tool name + status stay scannable without multi-line wrapping
|
||||||
- On mobile viewports, opening Quick Chat auto-focuses the composer as soon as it is ready so the keyboard opens immediately
|
- On mobile viewports, opening Quick Chat auto-focuses the composer as soon as it is ready so the keyboard opens immediately
|
||||||
|
|
||||||
|
|||||||
@@ -277,9 +277,9 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
.quick-chat-mode-btn {
|
.quick-chat-mode-btn {
|
||||||
min-width: var(--quick-chat-header-control-size);
|
min-width: var(--quick-chat-header-control-size, calc(var(--space-lg) + var(--space-sm) + var(--space-xs)));
|
||||||
height: var(--quick-chat-header-control-size);
|
height: var(--quick-chat-header-control-size, calc(var(--space-lg) + var(--space-sm) + var(--space-xs)));
|
||||||
min-height: var(--quick-chat-header-control-size);
|
min-height: var(--quick-chat-header-control-size, calc(var(--space-lg) + var(--space-sm) + var(--space-xs)));
|
||||||
padding: 0 var(--space-sm);
|
padding: 0 var(--space-sm);
|
||||||
display: inline-flex;
|
display: inline-flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
@@ -340,6 +340,32 @@
|
|||||||
border-bottom: 1px solid var(--border);
|
border-bottom: 1px solid var(--border);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.quick-chat-new-session-chooser {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: var(--space-sm);
|
||||||
|
margin: var(--space-sm) var(--space-md) 0;
|
||||||
|
padding: var(--space-sm);
|
||||||
|
border: 1px solid color-mix(in srgb, var(--todo) 25%, var(--border));
|
||||||
|
border-radius: var(--radius-md);
|
||||||
|
background: color-mix(in srgb, var(--surface) 80%, var(--card));
|
||||||
|
}
|
||||||
|
|
||||||
|
.quick-chat-inline-mode-toggle {
|
||||||
|
--quick-chat-header-control-size: calc(var(--space-lg) + var(--space-sm) + var(--space-xs));
|
||||||
|
|
||||||
|
display: inline-flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: var(--space-xs);
|
||||||
|
}
|
||||||
|
|
||||||
|
.quick-chat-new-session-actions {
|
||||||
|
display: flex;
|
||||||
|
justify-content: flex-end;
|
||||||
|
align-items: center;
|
||||||
|
gap: var(--space-sm);
|
||||||
|
}
|
||||||
|
|
||||||
.quick-chat-panel-agent-select select {
|
.quick-chat-panel-agent-select select {
|
||||||
width: 100%;
|
width: 100%;
|
||||||
background: var(--bg);
|
background: var(--bg);
|
||||||
@@ -746,6 +772,24 @@
|
|||||||
min-width: 0;
|
min-width: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.quick-chat-inline-mode-toggle {
|
||||||
|
--quick-chat-header-control-size: calc(var(--space-xl) + var(--space-md));
|
||||||
|
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.quick-chat-mode-btn {
|
||||||
|
flex: 1 1 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.quick-chat-new-session-actions {
|
||||||
|
justify-content: stretch;
|
||||||
|
}
|
||||||
|
|
||||||
|
.quick-chat-new-session-actions .btn {
|
||||||
|
flex: 1 1 0;
|
||||||
|
}
|
||||||
|
|
||||||
.quick-chat-panel .chat-tool-calls-group-summary {
|
.quick-chat-panel .chat-tool-calls-group-summary {
|
||||||
flex-wrap: nowrap;
|
flex-wrap: nowrap;
|
||||||
flex-direction: row;
|
flex-direction: row;
|
||||||
|
|||||||
@@ -812,6 +812,10 @@ export function QuickChatFAB({
|
|||||||
|
|
||||||
const [chatMode, setChatMode] = useState<"agent" | "model">("agent");
|
const [chatMode, setChatMode] = useState<"agent" | "model">("agent");
|
||||||
const [selectedAgentId, setSelectedAgentId] = useState<string>("");
|
const [selectedAgentId, setSelectedAgentId] = useState<string>("");
|
||||||
|
const [newSessionChooserOpen, setNewSessionChooserOpen] = useState(false);
|
||||||
|
const [newSessionMode, setNewSessionMode] = useState<"agent" | "model">("model");
|
||||||
|
const [newSessionAgentId, setNewSessionAgentId] = useState<string>("");
|
||||||
|
const [newSessionModel, setNewSessionModel] = useState<string>("");
|
||||||
const [models, setModels] = useState<ModelInfo[]>([]);
|
const [models, setModels] = useState<ModelInfo[]>([]);
|
||||||
const [modelsLoading, setModelsLoading] = useState(false);
|
const [modelsLoading, setModelsLoading] = useState(false);
|
||||||
const [selectedModel, setSelectedModel] = useState<string>("");
|
const [selectedModel, setSelectedModel] = useState<string>("");
|
||||||
@@ -876,6 +880,7 @@ export function QuickChatFAB({
|
|||||||
streamingText,
|
streamingText,
|
||||||
streamingThinking,
|
streamingThinking,
|
||||||
streamingToolCalls,
|
streamingToolCalls,
|
||||||
|
sessions,
|
||||||
sessionsLoading,
|
sessionsLoading,
|
||||||
messagesLoading,
|
messagesLoading,
|
||||||
sendMessage,
|
sendMessage,
|
||||||
@@ -883,8 +888,10 @@ export function QuickChatFAB({
|
|||||||
pendingMessage,
|
pendingMessage,
|
||||||
clearPendingMessage,
|
clearPendingMessage,
|
||||||
switchSession,
|
switchSession,
|
||||||
|
selectSession,
|
||||||
startModelChat,
|
startModelChat,
|
||||||
startFreshSession,
|
startFreshSession,
|
||||||
|
refreshSessions,
|
||||||
} = useQuickChat(projectId, addToast);
|
} = useQuickChat(projectId, addToast);
|
||||||
|
|
||||||
const panelRef = useRef<HTMLDivElement | null>(null);
|
const panelRef = useRef<HTMLDivElement | null>(null);
|
||||||
@@ -1078,6 +1085,11 @@ export function QuickChatFAB({
|
|||||||
});
|
});
|
||||||
}, [isOpen, agents.length, selectedModel]);
|
}, [isOpen, agents.length, selectedModel]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (!isOpen) return;
|
||||||
|
void refreshSessions();
|
||||||
|
}, [isOpen, refreshSessions]);
|
||||||
|
|
||||||
// Initialize/switch quick chat session whenever the selected target changes.
|
// Initialize/switch quick chat session whenever the selected target changes.
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!isOpen) {
|
if (!isOpen) {
|
||||||
@@ -1201,20 +1213,12 @@ export function QuickChatFAB({
|
|||||||
};
|
};
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
const handleAgentChange = useCallback((agentId: string) => {
|
|
||||||
setSelectedAgentId(agentId);
|
|
||||||
}, []);
|
|
||||||
|
|
||||||
const handleModelChange = useCallback((value: string) => {
|
|
||||||
setSelectedModel(value);
|
|
||||||
}, []);
|
|
||||||
|
|
||||||
const handleStartFreshChat = useCallback(() => {
|
const handleStartFreshChat = useCallback(() => {
|
||||||
if (!hasChatTarget || sessionsLoading) {
|
setNewSessionChooserOpen(true);
|
||||||
return;
|
setNewSessionMode("model");
|
||||||
}
|
setNewSessionAgentId(agents[0]?.id ?? "");
|
||||||
void startFreshSession();
|
setNewSessionModel(selectedModel || configuredDefaultModelSelection || "");
|
||||||
}, [hasChatTarget, sessionsLoading, startFreshSession]);
|
}, [agents, configuredDefaultModelSelection, selectedModel]);
|
||||||
|
|
||||||
const selectedAgent = useMemo(
|
const selectedAgent = useMemo(
|
||||||
() => agents.find((agent) => agent.id === selectedAgentId) ?? null,
|
() => agents.find((agent) => agent.id === selectedAgentId) ?? null,
|
||||||
@@ -1287,6 +1291,14 @@ export function QuickChatFAB({
|
|||||||
messagesEl.scrollTop = messagesEl.scrollHeight;
|
messagesEl.scrollTop = messagesEl.scrollHeight;
|
||||||
}, [messages, streamingText, streamingThinking, isOpen]);
|
}, [messages, streamingText, streamingThinking, isOpen]);
|
||||||
|
|
||||||
|
const sessionOptions = useMemo(
|
||||||
|
() => sessions.map((session, index) => ({
|
||||||
|
id: session.id,
|
||||||
|
label: session.title?.trim() || `Session ${index + 1}`,
|
||||||
|
})),
|
||||||
|
[sessions],
|
||||||
|
);
|
||||||
|
|
||||||
const inputPlaceholder = useMemo(() => {
|
const inputPlaceholder = useMemo(() => {
|
||||||
if (chatMode === "agent") {
|
if (chatMode === "agent") {
|
||||||
if (selectedAgent) {
|
if (selectedAgent) {
|
||||||
@@ -1301,6 +1313,53 @@ export function QuickChatFAB({
|
|||||||
return "Select a model to start chatting";
|
return "Select a model to start chatting";
|
||||||
}, [chatMode, selectedAgent, selectedModelTag]);
|
}, [chatMode, selectedAgent, selectedModelTag]);
|
||||||
|
|
||||||
|
const handleSessionSwitch = useCallback((sessionId: string) => {
|
||||||
|
const selectedSession = sessions.find((session) => session.id === sessionId);
|
||||||
|
if (!selectedSession) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (selectedSession.modelProvider && selectedSession.modelId) {
|
||||||
|
setChatMode("model");
|
||||||
|
setSelectedModel(`${selectedSession.modelProvider}/${selectedSession.modelId}`);
|
||||||
|
} else {
|
||||||
|
setChatMode("agent");
|
||||||
|
setSelectedAgentId(selectedSession.agentId);
|
||||||
|
}
|
||||||
|
|
||||||
|
void selectSession(selectedSession);
|
||||||
|
}, [selectSession, sessions]);
|
||||||
|
|
||||||
|
const handleCreateFreshSession = useCallback(async () => {
|
||||||
|
if (sessionsLoading) return;
|
||||||
|
|
||||||
|
if (newSessionMode === "agent") {
|
||||||
|
if (!newSessionAgentId) return;
|
||||||
|
setChatMode("agent");
|
||||||
|
setSelectedAgentId(newSessionAgentId);
|
||||||
|
await startFreshSession(newSessionAgentId);
|
||||||
|
} else {
|
||||||
|
const parsed = parseModelSelection(newSessionModel || selectedModel || configuredDefaultModelSelection);
|
||||||
|
if (!parsed) return;
|
||||||
|
setChatMode("model");
|
||||||
|
setSelectedModel(`${parsed.modelProvider}/${parsed.modelId}`);
|
||||||
|
await startFreshSession(FN_AGENT_ID, parsed.modelProvider, parsed.modelId);
|
||||||
|
}
|
||||||
|
|
||||||
|
await refreshSessions();
|
||||||
|
setNewSessionChooserOpen(false);
|
||||||
|
setNewSessionMode("model");
|
||||||
|
}, [
|
||||||
|
configuredDefaultModelSelection,
|
||||||
|
newSessionAgentId,
|
||||||
|
newSessionMode,
|
||||||
|
newSessionModel,
|
||||||
|
refreshSessions,
|
||||||
|
selectedModel,
|
||||||
|
sessionsLoading,
|
||||||
|
startFreshSession,
|
||||||
|
]);
|
||||||
|
|
||||||
const pendingPreview = pendingMessage.length > 50
|
const pendingPreview = pendingMessage.length > 50
|
||||||
? `${pendingMessage.slice(0, 50)}…`
|
? `${pendingMessage.slice(0, 50)}…`
|
||||||
: pendingMessage;
|
: pendingMessage;
|
||||||
@@ -1827,43 +1886,13 @@ export function QuickChatFAB({
|
|||||||
})()}
|
})()}
|
||||||
</div>
|
</div>
|
||||||
<div className="quick-chat-panel-header-actions">
|
<div className="quick-chat-panel-header-actions">
|
||||||
{agents.length > 0 && (
|
|
||||||
<div className="quick-chat-header-mode-toggle" data-testid="quick-chat-mode-toggle">
|
|
||||||
<button
|
|
||||||
type="button"
|
|
||||||
className={`quick-chat-mode-btn${chatMode === "agent" ? " quick-chat-mode-btn--active" : ""}`}
|
|
||||||
data-testid="quick-chat-mode-agent"
|
|
||||||
onClick={() => {
|
|
||||||
setChatMode("agent");
|
|
||||||
setSelectedModel("");
|
|
||||||
}}
|
|
||||||
aria-label="Switch to agent mode"
|
|
||||||
title="Agent mode"
|
|
||||||
>
|
|
||||||
Agent
|
|
||||||
</button>
|
|
||||||
<button
|
|
||||||
type="button"
|
|
||||||
className={`quick-chat-mode-btn${chatMode === "model" ? " quick-chat-mode-btn--active" : ""}`}
|
|
||||||
data-testid="quick-chat-mode-model"
|
|
||||||
onClick={() => {
|
|
||||||
setChatMode("model");
|
|
||||||
setSelectedAgentId("");
|
|
||||||
}}
|
|
||||||
aria-label="Switch to model mode"
|
|
||||||
title="Model mode"
|
|
||||||
>
|
|
||||||
Model
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
className="btn-icon quick-chat-new-chat-btn"
|
className="btn-icon quick-chat-new-chat-btn"
|
||||||
data-testid="quick-chat-new-thread"
|
data-testid="quick-chat-new-thread"
|
||||||
aria-label="Start a new chat"
|
aria-label="Start a new chat"
|
||||||
onClick={handleStartFreshChat}
|
onClick={handleStartFreshChat}
|
||||||
disabled={!hasChatTarget || sessionsLoading}
|
disabled={sessionsLoading}
|
||||||
>
|
>
|
||||||
<Plus size={16} />
|
<Plus size={16} />
|
||||||
</button>
|
</button>
|
||||||
@@ -1879,40 +1908,96 @@ export function QuickChatFAB({
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{chatMode === "agent" && agents.length > 0 && (
|
<div className="quick-chat-panel-agent-select" data-testid="quick-chat-session-select">
|
||||||
<div className="quick-chat-panel-agent-select">
|
<label htmlFor="quick-chat-session-select" className="visually-hidden">Select session</label>
|
||||||
<label htmlFor="quick-chat-agent-select" className="visually-hidden">Select agent</label>
|
<select
|
||||||
<select
|
id="quick-chat-session-select"
|
||||||
id="quick-chat-agent-select"
|
value={activeSession?.id ?? ""}
|
||||||
value={selectedAgentId}
|
onChange={(event) => handleSessionSwitch(event.target.value)}
|
||||||
onChange={(event) => handleAgentChange(event.target.value)}
|
data-testid="quick-chat-session-dropdown"
|
||||||
data-testid="quick-chat-agent-select"
|
>
|
||||||
>
|
<option value="" disabled>{sessionsLoading ? "Loading sessions…" : "Select a session"}</option>
|
||||||
{agents.map((agent) => (
|
{sessionOptions.map((sessionOption) => (
|
||||||
<option key={agent.id} value={agent.id}>
|
<option key={sessionOption.id} value={sessionOption.id}>{sessionOption.label}</option>
|
||||||
{getAgentLabel(agent)}
|
))}
|
||||||
</option>
|
</select>
|
||||||
))}
|
</div>
|
||||||
</select>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
|
|
||||||
{chatMode === "model" && (
|
{newSessionChooserOpen && (
|
||||||
<div className="quick-chat-panel-agent-select" data-testid="quick-chat-model-select">
|
<div className="quick-chat-new-session-chooser" data-testid="quick-chat-new-session-chooser">
|
||||||
<label htmlFor="quick-chat-model-override" className="visually-hidden">Select model override</label>
|
<div className="quick-chat-inline-mode-toggle" data-testid="quick-chat-inline-mode-toggle">
|
||||||
<CustomModelDropdown
|
<button
|
||||||
id="quick-chat-model-override"
|
type="button"
|
||||||
models={models}
|
className={`quick-chat-mode-btn${newSessionMode === "model" ? " quick-chat-mode-btn--active" : ""}`}
|
||||||
value={selectedModel}
|
data-testid="quick-chat-inline-mode-model"
|
||||||
onChange={handleModelChange}
|
onClick={() => setNewSessionMode("model")}
|
||||||
label="Select model override"
|
>
|
||||||
placeholder={modelsLoading ? "Loading models…" : "Select a model"}
|
Model
|
||||||
disabled={modelsLoading || models.length === 0}
|
</button>
|
||||||
favoriteProviders={favoriteProviders}
|
<button
|
||||||
favoriteModels={favoriteModels}
|
type="button"
|
||||||
onToggleFavorite={onToggleFavorite}
|
className={`quick-chat-mode-btn${newSessionMode === "agent" ? " quick-chat-mode-btn--active" : ""}`}
|
||||||
onToggleModelFavorite={onToggleModelFavorite}
|
data-testid="quick-chat-inline-mode-agent"
|
||||||
/>
|
onClick={() => setNewSessionMode("agent")}
|
||||||
|
>
|
||||||
|
Agent
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{newSessionMode === "agent" ? (
|
||||||
|
<div className="quick-chat-panel-agent-select">
|
||||||
|
<label htmlFor="quick-chat-new-agent-select" className="visually-hidden">Select agent for new chat</label>
|
||||||
|
<select
|
||||||
|
id="quick-chat-new-agent-select"
|
||||||
|
value={newSessionAgentId}
|
||||||
|
onChange={(event) => setNewSessionAgentId(event.target.value)}
|
||||||
|
data-testid="quick-chat-new-agent-select"
|
||||||
|
>
|
||||||
|
{agents.map((agent) => (
|
||||||
|
<option key={agent.id} value={agent.id}>{getAgentLabel(agent)}</option>
|
||||||
|
))}
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
) : (
|
||||||
|
<div className="quick-chat-panel-agent-select" data-testid="quick-chat-new-model-select">
|
||||||
|
<CustomModelDropdown
|
||||||
|
id="quick-chat-new-model-select"
|
||||||
|
models={models}
|
||||||
|
value={newSessionModel}
|
||||||
|
onChange={setNewSessionModel}
|
||||||
|
label="Select model override"
|
||||||
|
placeholder={modelsLoading ? "Loading models…" : "Select a model"}
|
||||||
|
disabled={modelsLoading || models.length === 0}
|
||||||
|
favoriteProviders={favoriteProviders}
|
||||||
|
favoriteModels={favoriteModels}
|
||||||
|
onToggleFavorite={onToggleFavorite}
|
||||||
|
onToggleModelFavorite={onToggleModelFavorite}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
<div className="quick-chat-new-session-actions">
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
className="btn"
|
||||||
|
data-testid="quick-chat-new-session-cancel"
|
||||||
|
onClick={() => {
|
||||||
|
setNewSessionChooserOpen(false);
|
||||||
|
setNewSessionMode("model");
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
Cancel
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
className="btn btn-primary"
|
||||||
|
data-testid="quick-chat-new-session-submit"
|
||||||
|
onClick={() => void handleCreateFreshSession()}
|
||||||
|
disabled={sessionsLoading || (newSessionMode === "agent" ? !newSessionAgentId : !parseModelSelection(newSessionModel || selectedModel || configuredDefaultModelSelection))}
|
||||||
|
>
|
||||||
|
Create
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
|
|||||||
File diff suppressed because it is too large
Load Diff
@@ -2,6 +2,7 @@ import { useCallback, useEffect, useMemo, useRef, useState } from "react";
|
|||||||
import type { ChatMessage, ChatSession } from "@fusion/core";
|
import type { ChatMessage, ChatSession } from "@fusion/core";
|
||||||
import {
|
import {
|
||||||
fetchResumeChatSession,
|
fetchResumeChatSession,
|
||||||
|
fetchChatSessions,
|
||||||
createChatSession,
|
createChatSession,
|
||||||
fetchChatMessages,
|
fetchChatMessages,
|
||||||
streamChatResponse,
|
streamChatResponse,
|
||||||
@@ -42,6 +43,7 @@ interface SessionTarget {
|
|||||||
export interface UseQuickChatReturn {
|
export interface UseQuickChatReturn {
|
||||||
// Session state
|
// Session state
|
||||||
activeSession: ChatSession | null;
|
activeSession: ChatSession | null;
|
||||||
|
sessions: ChatSession[];
|
||||||
sessionsLoading: boolean;
|
sessionsLoading: boolean;
|
||||||
|
|
||||||
// Message state
|
// Message state
|
||||||
@@ -58,8 +60,10 @@ export interface UseQuickChatReturn {
|
|||||||
stopStreaming: () => void;
|
stopStreaming: () => void;
|
||||||
clearPendingMessage: () => void;
|
clearPendingMessage: () => void;
|
||||||
switchSession: (agentId: string, modelProvider?: string, modelId?: string) => Promise<void>;
|
switchSession: (agentId: string, modelProvider?: string, modelId?: string) => Promise<void>;
|
||||||
|
selectSession: (session: ChatSession) => Promise<void>;
|
||||||
startModelChat: (modelProvider: string, modelId: string) => Promise<void>;
|
startModelChat: (modelProvider: string, modelId: string) => Promise<void>;
|
||||||
startFreshSession: () => Promise<void>;
|
startFreshSession: (agentId?: string, modelProvider?: string, modelId?: string) => Promise<void>;
|
||||||
|
refreshSessions: () => Promise<void>;
|
||||||
loadMessages: () => Promise<void>;
|
loadMessages: () => Promise<void>;
|
||||||
reloadMessages: () => Promise<void>;
|
reloadMessages: () => Promise<void>;
|
||||||
}
|
}
|
||||||
@@ -152,6 +156,7 @@ export function useQuickChat(
|
|||||||
): UseQuickChatReturn {
|
): UseQuickChatReturn {
|
||||||
// Session state
|
// Session state
|
||||||
const [activeSession, setActiveSession] = useState<ChatSession | null>(null);
|
const [activeSession, setActiveSession] = useState<ChatSession | null>(null);
|
||||||
|
const [sessions, setSessions] = useState<ChatSession[]>([]);
|
||||||
const [sessionsLoading, setSessionsLoading] = useState(false);
|
const [sessionsLoading, setSessionsLoading] = useState(false);
|
||||||
|
|
||||||
// Message state
|
// Message state
|
||||||
@@ -178,6 +183,18 @@ export function useQuickChat(
|
|||||||
pendingMessageRef.current = pendingMessage;
|
pendingMessageRef.current = pendingMessage;
|
||||||
}, [pendingMessage]);
|
}, [pendingMessage]);
|
||||||
|
|
||||||
|
const refreshSessions = useCallback(async () => {
|
||||||
|
setSessionsLoading(true);
|
||||||
|
try {
|
||||||
|
const response = await fetchChatSessions(projectId);
|
||||||
|
setSessions(response.sessions);
|
||||||
|
} catch (err) {
|
||||||
|
console.error("[useQuickChat] Failed to refresh sessions:", err);
|
||||||
|
} finally {
|
||||||
|
setSessionsLoading(false);
|
||||||
|
}
|
||||||
|
}, [projectId]);
|
||||||
|
|
||||||
const createSessionForTarget = useCallback(
|
const createSessionForTarget = useCallback(
|
||||||
async (target: SessionTarget): Promise<ChatSession> => {
|
async (target: SessionTarget): Promise<ChatSession> => {
|
||||||
const newSessionInput: { agentId: string; modelProvider?: string; modelId?: string } = {
|
const newSessionInput: { agentId: string; modelProvider?: string; modelId?: string } = {
|
||||||
@@ -313,6 +330,25 @@ export function useQuickChat(
|
|||||||
[initializeSession, reloadMessages, activeSession],
|
[initializeSession, reloadMessages, activeSession],
|
||||||
);
|
);
|
||||||
|
|
||||||
|
const selectSession = useCallback(async (session: ChatSession) => {
|
||||||
|
const target = resolveSessionTarget(session.agentId, session.modelProvider ?? undefined, session.modelId ?? undefined);
|
||||||
|
if (!target) return;
|
||||||
|
|
||||||
|
currentSessionTargetRef.current = target;
|
||||||
|
currentSessionKeyRef.current = buildSessionKey(target.agentId, target.modelProvider, target.modelId);
|
||||||
|
|
||||||
|
if (streamRef.current) {
|
||||||
|
streamRef.current.close();
|
||||||
|
streamRef.current = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
setStreamingText("");
|
||||||
|
setStreamingThinking("");
|
||||||
|
setStreamingToolCalls([]);
|
||||||
|
setIsStreaming(false);
|
||||||
|
setActiveSession(session);
|
||||||
|
}, []);
|
||||||
|
|
||||||
const startModelChat = useCallback(
|
const startModelChat = useCallback(
|
||||||
async (modelProvider: string, modelId: string) => {
|
async (modelProvider: string, modelId: string) => {
|
||||||
await switchSession(FN_AGENT_ID, modelProvider, modelId);
|
await switchSession(FN_AGENT_ID, modelProvider, modelId);
|
||||||
@@ -320,10 +356,13 @@ export function useQuickChat(
|
|||||||
[switchSession],
|
[switchSession],
|
||||||
);
|
);
|
||||||
|
|
||||||
const startFreshSession = useCallback(async () => {
|
const startFreshSession = useCallback(async (agentId?: string, modelProvider?: string, modelId?: string) => {
|
||||||
const target = currentSessionTargetRef.current;
|
const overrideTarget = resolveSessionTarget(agentId ?? "", modelProvider, modelId);
|
||||||
|
const target = overrideTarget ?? currentSessionTargetRef.current;
|
||||||
if (!target) return;
|
if (!target) return;
|
||||||
|
|
||||||
|
currentSessionTargetRef.current = target;
|
||||||
|
|
||||||
// Explicit "new chat" action: keep the same target key but create a new persisted session.
|
// Explicit "new chat" action: keep the same target key but create a new persisted session.
|
||||||
// This preserves normal switchSession resume behavior while allowing multiple threads per target.
|
// This preserves normal switchSession resume behavior while allowing multiple threads per target.
|
||||||
const targetSessionKey = buildSessionKey(target.agentId, target.modelProvider, target.modelId);
|
const targetSessionKey = buildSessionKey(target.agentId, target.modelProvider, target.modelId);
|
||||||
@@ -345,13 +384,16 @@ export function useQuickChat(
|
|||||||
const newSession = await createSessionForTarget(target);
|
const newSession = await createSessionForTarget(target);
|
||||||
setActiveSession(newSession);
|
setActiveSession(newSession);
|
||||||
currentSessionKeyRef.current = targetSessionKey;
|
currentSessionKeyRef.current = targetSessionKey;
|
||||||
|
|
||||||
|
const sessionList = await fetchChatSessions(projectId);
|
||||||
|
setSessions(sessionList.sessions);
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
console.error("[useQuickChat] Failed to start a fresh session:", err);
|
console.error("[useQuickChat] Failed to start a fresh session:", err);
|
||||||
addToast?.("Failed to start a new chat", "error");
|
addToast?.("Failed to start a new chat", "error");
|
||||||
} finally {
|
} finally {
|
||||||
setSessionsLoading(false);
|
setSessionsLoading(false);
|
||||||
}
|
}
|
||||||
}, [addToast, createSessionForTarget]);
|
}, [addToast, createSessionForTarget, projectId]);
|
||||||
|
|
||||||
const stopStreaming = useCallback(() => {
|
const stopStreaming = useCallback(() => {
|
||||||
if (!activeSession) return;
|
if (!activeSession) return;
|
||||||
@@ -587,6 +629,7 @@ export function useQuickChat(
|
|||||||
|
|
||||||
return useMemo(() => ({
|
return useMemo(() => ({
|
||||||
activeSession,
|
activeSession,
|
||||||
|
sessions,
|
||||||
sessionsLoading,
|
sessionsLoading,
|
||||||
messages,
|
messages,
|
||||||
messagesLoading,
|
messagesLoading,
|
||||||
@@ -599,12 +642,15 @@ export function useQuickChat(
|
|||||||
stopStreaming,
|
stopStreaming,
|
||||||
clearPendingMessage,
|
clearPendingMessage,
|
||||||
switchSession,
|
switchSession,
|
||||||
|
selectSession,
|
||||||
startModelChat,
|
startModelChat,
|
||||||
startFreshSession,
|
startFreshSession,
|
||||||
|
refreshSessions,
|
||||||
loadMessages,
|
loadMessages,
|
||||||
reloadMessages,
|
reloadMessages,
|
||||||
}), [
|
}), [
|
||||||
activeSession,
|
activeSession,
|
||||||
|
sessions,
|
||||||
sessionsLoading,
|
sessionsLoading,
|
||||||
messages,
|
messages,
|
||||||
messagesLoading,
|
messagesLoading,
|
||||||
@@ -617,8 +663,10 @@ export function useQuickChat(
|
|||||||
stopStreaming,
|
stopStreaming,
|
||||||
clearPendingMessage,
|
clearPendingMessage,
|
||||||
switchSession,
|
switchSession,
|
||||||
|
selectSession,
|
||||||
startModelChat,
|
startModelChat,
|
||||||
startFreshSession,
|
startFreshSession,
|
||||||
|
refreshSessions,
|
||||||
loadMessages,
|
loadMessages,
|
||||||
reloadMessages,
|
reloadMessages,
|
||||||
]);
|
]);
|
||||||
|
|||||||
Reference in New Issue
Block a user