feat(cli): conversation sidebar with list/create/switch/delete (TUI-008)
This commit is contained in:
@@ -1,12 +1,15 @@
|
|||||||
import React from 'react';
|
import React, { useState, useCallback } from 'react';
|
||||||
import { Box, useApp, useInput } from 'ink';
|
import { Box, useApp, useInput } from 'ink';
|
||||||
import { TopBar } from './components/top-bar.js';
|
import { TopBar } from './components/top-bar.js';
|
||||||
import { BottomBar } from './components/bottom-bar.js';
|
import { BottomBar } from './components/bottom-bar.js';
|
||||||
import { MessageList } from './components/message-list.js';
|
import { MessageList } from './components/message-list.js';
|
||||||
import { InputBar } from './components/input-bar.js';
|
import { InputBar } from './components/input-bar.js';
|
||||||
|
import { Sidebar } from './components/sidebar.js';
|
||||||
import { useSocket } from './hooks/use-socket.js';
|
import { useSocket } from './hooks/use-socket.js';
|
||||||
import { useGitInfo } from './hooks/use-git-info.js';
|
import { useGitInfo } from './hooks/use-git-info.js';
|
||||||
import { useViewport } from './hooks/use-viewport.js';
|
import { useViewport } from './hooks/use-viewport.js';
|
||||||
|
import { useAppMode } from './hooks/use-app-mode.js';
|
||||||
|
import { useConversations } from './hooks/use-conversations.js';
|
||||||
|
|
||||||
export interface TuiAppProps {
|
export interface TuiAppProps {
|
||||||
gatewayUrl: string;
|
gatewayUrl: string;
|
||||||
@@ -17,6 +20,7 @@ export interface TuiAppProps {
|
|||||||
export function TuiApp({ gatewayUrl, conversationId, sessionCookie }: TuiAppProps) {
|
export function TuiApp({ gatewayUrl, conversationId, sessionCookie }: TuiAppProps) {
|
||||||
const { exit } = useApp();
|
const { exit } = useApp();
|
||||||
const gitInfo = useGitInfo();
|
const gitInfo = useGitInfo();
|
||||||
|
const appMode = useAppMode();
|
||||||
|
|
||||||
const socket = useSocket({
|
const socket = useSocket({
|
||||||
gatewayUrl,
|
gatewayUrl,
|
||||||
@@ -24,18 +28,47 @@ export function TuiApp({ gatewayUrl, conversationId, sessionCookie }: TuiAppProp
|
|||||||
initialConversationId: conversationId,
|
initialConversationId: conversationId,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
const conversations = useConversations({ gatewayUrl, sessionCookie });
|
||||||
|
|
||||||
const viewport = useViewport({ totalItems: socket.messages.length });
|
const viewport = useViewport({ totalItems: socket.messages.length });
|
||||||
|
|
||||||
|
const [sidebarSelectedIndex, setSidebarSelectedIndex] = useState(0);
|
||||||
|
|
||||||
|
const handleSwitchConversation = useCallback(
|
||||||
|
(id: string) => {
|
||||||
|
socket.switchConversation(id);
|
||||||
|
appMode.setMode('chat');
|
||||||
|
},
|
||||||
|
[socket, appMode],
|
||||||
|
);
|
||||||
|
|
||||||
|
const handleDeleteConversation = useCallback(
|
||||||
|
(id: string) => {
|
||||||
|
void conversations.deleteConversation(id).then((ok) => {
|
||||||
|
if (ok && id === socket.conversationId) {
|
||||||
|
socket.clearMessages();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
},
|
||||||
|
[conversations, socket],
|
||||||
|
);
|
||||||
|
|
||||||
useInput((ch, key) => {
|
useInput((ch, key) => {
|
||||||
if (key.ctrl && ch === 'c') {
|
if (key.ctrl && ch === 'c') {
|
||||||
exit();
|
exit();
|
||||||
}
|
}
|
||||||
// Page Up / Page Down: scroll message history
|
// Ctrl+B: toggle sidebar
|
||||||
if (key.pageUp) {
|
if (key.ctrl && ch === 'b') {
|
||||||
viewport.scrollBy(-viewport.viewportSize);
|
appMode.toggleSidebar();
|
||||||
}
|
}
|
||||||
if (key.pageDown) {
|
// Page Up / Page Down: scroll message history (only in chat mode)
|
||||||
viewport.scrollBy(viewport.viewportSize);
|
if (appMode.mode === 'chat') {
|
||||||
|
if (key.pageUp) {
|
||||||
|
viewport.scrollBy(-viewport.viewportSize);
|
||||||
|
}
|
||||||
|
if (key.pageDown) {
|
||||||
|
viewport.scrollBy(viewport.viewportSize);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
// Ctrl+T: cycle thinking level
|
// Ctrl+T: cycle thinking level
|
||||||
if (key.ctrl && ch === 't') {
|
if (key.ctrl && ch === 't') {
|
||||||
@@ -49,22 +82,17 @@ export function TuiApp({ gatewayUrl, conversationId, sessionCookie }: TuiAppProp
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
// Escape: return to chat from sidebar
|
||||||
|
if (key.escape && appMode.mode === 'sidebar') {
|
||||||
|
appMode.setMode('chat');
|
||||||
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
return (
|
const inputPlaceholder =
|
||||||
<Box flexDirection="column" height="100%">
|
appMode.mode !== 'chat' ? 'focus is on sidebar… press Esc to return' : undefined;
|
||||||
<Box marginTop={1} />
|
|
||||||
<TopBar
|
|
||||||
gatewayUrl={gatewayUrl}
|
|
||||||
version="0.0.0"
|
|
||||||
modelName={socket.modelName}
|
|
||||||
thinkingLevel={socket.thinkingLevel}
|
|
||||||
contextWindow={socket.tokenUsage.contextWindow}
|
|
||||||
agentName="default"
|
|
||||||
connected={socket.connected}
|
|
||||||
connecting={socket.connecting}
|
|
||||||
/>
|
|
||||||
|
|
||||||
|
const messageArea = (
|
||||||
|
<Box flexDirection="column" flexGrow={1}>
|
||||||
<MessageList
|
<MessageList
|
||||||
messages={socket.messages}
|
messages={socket.messages}
|
||||||
isStreaming={socket.isStreaming}
|
isStreaming={socket.isStreaming}
|
||||||
@@ -80,7 +108,43 @@ export function TuiApp({ gatewayUrl, conversationId, sessionCookie }: TuiAppProp
|
|||||||
onSubmit={socket.sendMessage}
|
onSubmit={socket.sendMessage}
|
||||||
isStreaming={socket.isStreaming}
|
isStreaming={socket.isStreaming}
|
||||||
connected={socket.connected}
|
connected={socket.connected}
|
||||||
|
placeholder={inputPlaceholder}
|
||||||
/>
|
/>
|
||||||
|
</Box>
|
||||||
|
);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Box flexDirection="column" height="100%">
|
||||||
|
<Box marginTop={1} />
|
||||||
|
<TopBar
|
||||||
|
gatewayUrl={gatewayUrl}
|
||||||
|
version="0.0.0"
|
||||||
|
modelName={socket.modelName}
|
||||||
|
thinkingLevel={socket.thinkingLevel}
|
||||||
|
contextWindow={socket.tokenUsage.contextWindow}
|
||||||
|
agentName="default"
|
||||||
|
connected={socket.connected}
|
||||||
|
connecting={socket.connecting}
|
||||||
|
/>
|
||||||
|
|
||||||
|
{appMode.sidebarOpen ? (
|
||||||
|
<Box flexDirection="row" flexGrow={1}>
|
||||||
|
<Sidebar
|
||||||
|
conversations={conversations.conversations}
|
||||||
|
activeConversationId={socket.conversationId}
|
||||||
|
selectedIndex={sidebarSelectedIndex}
|
||||||
|
onSelectIndex={setSidebarSelectedIndex}
|
||||||
|
onSwitchConversation={handleSwitchConversation}
|
||||||
|
onDeleteConversation={handleDeleteConversation}
|
||||||
|
loading={conversations.loading}
|
||||||
|
focused={appMode.mode === 'sidebar'}
|
||||||
|
width={30}
|
||||||
|
/>
|
||||||
|
{messageArea}
|
||||||
|
</Box>
|
||||||
|
) : (
|
||||||
|
<Box flexGrow={1}>{messageArea}</Box>
|
||||||
|
)}
|
||||||
|
|
||||||
<BottomBar
|
<BottomBar
|
||||||
gitInfo={gitInfo}
|
gitInfo={gitInfo}
|
||||||
|
|||||||
@@ -6,9 +6,15 @@ export interface InputBarProps {
|
|||||||
onSubmit: (value: string) => void;
|
onSubmit: (value: string) => void;
|
||||||
isStreaming: boolean;
|
isStreaming: boolean;
|
||||||
connected: boolean;
|
connected: boolean;
|
||||||
|
placeholder?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function InputBar({ onSubmit, isStreaming, connected }: InputBarProps) {
|
export function InputBar({
|
||||||
|
onSubmit,
|
||||||
|
isStreaming,
|
||||||
|
connected,
|
||||||
|
placeholder: placeholderOverride,
|
||||||
|
}: InputBarProps) {
|
||||||
const [input, setInput] = useState('');
|
const [input, setInput] = useState('');
|
||||||
|
|
||||||
const handleSubmit = useCallback(
|
const handleSubmit = useCallback(
|
||||||
@@ -20,11 +26,13 @@ export function InputBar({ onSubmit, isStreaming, connected }: InputBarProps) {
|
|||||||
[onSubmit, isStreaming, connected],
|
[onSubmit, isStreaming, connected],
|
||||||
);
|
);
|
||||||
|
|
||||||
const placeholder = !connected
|
const placeholder =
|
||||||
? 'disconnected — waiting for gateway…'
|
placeholderOverride ??
|
||||||
: isStreaming
|
(!connected
|
||||||
? 'waiting for response…'
|
? 'disconnected — waiting for gateway…'
|
||||||
: 'message mosaic…';
|
: isStreaming
|
||||||
|
? 'waiting for response…'
|
||||||
|
: 'message mosaic…');
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Box paddingX={1} borderStyle="single" borderColor="gray">
|
<Box paddingX={1} borderStyle="single" borderColor="gray">
|
||||||
|
|||||||
143
packages/cli/src/tui/components/sidebar.tsx
Normal file
143
packages/cli/src/tui/components/sidebar.tsx
Normal file
@@ -0,0 +1,143 @@
|
|||||||
|
import React from 'react';
|
||||||
|
import { Box, Text, useInput } from 'ink';
|
||||||
|
import type { ConversationSummary } from '../hooks/use-conversations.js';
|
||||||
|
|
||||||
|
export interface SidebarProps {
|
||||||
|
conversations: ConversationSummary[];
|
||||||
|
activeConversationId: string | undefined;
|
||||||
|
selectedIndex: number;
|
||||||
|
onSelectIndex: (index: number) => void;
|
||||||
|
onSwitchConversation: (id: string) => void;
|
||||||
|
onDeleteConversation: (id: string) => void;
|
||||||
|
loading: boolean;
|
||||||
|
focused: boolean;
|
||||||
|
width: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
function formatRelativeTime(dateStr: string): string {
|
||||||
|
const date = new Date(dateStr);
|
||||||
|
const now = new Date();
|
||||||
|
const diffMs = now.getTime() - date.getTime();
|
||||||
|
const diffDays = Math.floor(diffMs / (1000 * 60 * 60 * 24));
|
||||||
|
|
||||||
|
if (diffDays === 0) {
|
||||||
|
const hh = String(date.getHours()).padStart(2, '0');
|
||||||
|
const mm = String(date.getMinutes()).padStart(2, '0');
|
||||||
|
return `${hh}:${mm}`;
|
||||||
|
}
|
||||||
|
if (diffDays < 7) {
|
||||||
|
return `${diffDays}d ago`;
|
||||||
|
}
|
||||||
|
const months = [
|
||||||
|
'Jan',
|
||||||
|
'Feb',
|
||||||
|
'Mar',
|
||||||
|
'Apr',
|
||||||
|
'May',
|
||||||
|
'Jun',
|
||||||
|
'Jul',
|
||||||
|
'Aug',
|
||||||
|
'Sep',
|
||||||
|
'Oct',
|
||||||
|
'Nov',
|
||||||
|
'Dec',
|
||||||
|
];
|
||||||
|
const mon = months[date.getMonth()];
|
||||||
|
const dd = String(date.getDate()).padStart(2, '0');
|
||||||
|
return `${mon} ${dd}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
function truncate(text: string, maxLen: number): string {
|
||||||
|
if (text.length <= maxLen) return text;
|
||||||
|
return text.slice(0, maxLen - 1) + '…';
|
||||||
|
}
|
||||||
|
|
||||||
|
export function Sidebar({
|
||||||
|
conversations,
|
||||||
|
activeConversationId,
|
||||||
|
selectedIndex,
|
||||||
|
onSelectIndex,
|
||||||
|
onSwitchConversation,
|
||||||
|
onDeleteConversation,
|
||||||
|
loading,
|
||||||
|
focused,
|
||||||
|
width,
|
||||||
|
}: SidebarProps) {
|
||||||
|
useInput(
|
||||||
|
(_input, key) => {
|
||||||
|
if (key.upArrow) {
|
||||||
|
onSelectIndex(Math.max(0, selectedIndex - 1));
|
||||||
|
}
|
||||||
|
if (key.downArrow) {
|
||||||
|
onSelectIndex(Math.min(conversations.length - 1, selectedIndex + 1));
|
||||||
|
}
|
||||||
|
if (key.return) {
|
||||||
|
const conv = conversations[selectedIndex];
|
||||||
|
if (conv) {
|
||||||
|
onSwitchConversation(conv.id);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (_input === 'd') {
|
||||||
|
const conv = conversations[selectedIndex];
|
||||||
|
if (conv) {
|
||||||
|
onDeleteConversation(conv.id);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{ isActive: focused },
|
||||||
|
);
|
||||||
|
|
||||||
|
const borderColor = focused ? 'cyan' : 'gray';
|
||||||
|
// Available width for content inside border + padding
|
||||||
|
const innerWidth = width - 4; // 2 border + 2 padding
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Box
|
||||||
|
flexDirection="column"
|
||||||
|
width={width}
|
||||||
|
borderStyle="single"
|
||||||
|
borderColor={borderColor}
|
||||||
|
paddingX={1}
|
||||||
|
>
|
||||||
|
<Text bold color="cyan">
|
||||||
|
Conversations
|
||||||
|
</Text>
|
||||||
|
<Box marginTop={0} flexDirection="column" flexGrow={1}>
|
||||||
|
{loading && conversations.length === 0 ? (
|
||||||
|
<Text dimColor>Loading…</Text>
|
||||||
|
) : conversations.length === 0 ? (
|
||||||
|
<Text dimColor>No conversations</Text>
|
||||||
|
) : (
|
||||||
|
conversations.map((conv, idx) => {
|
||||||
|
const isActive = conv.id === activeConversationId;
|
||||||
|
const isSelected = idx === selectedIndex && focused;
|
||||||
|
const marker = isActive ? '● ' : ' ';
|
||||||
|
const time = formatRelativeTime(conv.updatedAt);
|
||||||
|
const title = conv.title ?? 'Untitled';
|
||||||
|
// marker(2) + title + space(1) + time
|
||||||
|
const maxTitleLen = Math.max(4, innerWidth - marker.length - time.length - 1);
|
||||||
|
const displayTitle = truncate(title, maxTitleLen);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Box key={conv.id}>
|
||||||
|
<Text
|
||||||
|
inverse={isSelected}
|
||||||
|
color={isActive ? 'cyan' : undefined}
|
||||||
|
dimColor={!isActive && !isSelected}
|
||||||
|
>
|
||||||
|
{marker}
|
||||||
|
{displayTitle}
|
||||||
|
{' '.repeat(
|
||||||
|
Math.max(0, innerWidth - marker.length - displayTitle.length - time.length),
|
||||||
|
)}
|
||||||
|
{time}
|
||||||
|
</Text>
|
||||||
|
</Box>
|
||||||
|
);
|
||||||
|
})
|
||||||
|
)}
|
||||||
|
</Box>
|
||||||
|
{focused && <Text dimColor>↑↓ navigate • enter switch • d delete</Text>}
|
||||||
|
</Box>
|
||||||
|
);
|
||||||
|
}
|
||||||
37
packages/cli/src/tui/hooks/use-app-mode.ts
Normal file
37
packages/cli/src/tui/hooks/use-app-mode.ts
Normal file
@@ -0,0 +1,37 @@
|
|||||||
|
import { useState, useCallback } from 'react';
|
||||||
|
|
||||||
|
export type AppMode = 'chat' | 'sidebar' | 'search';
|
||||||
|
|
||||||
|
export interface UseAppModeReturn {
|
||||||
|
mode: AppMode;
|
||||||
|
setMode: (mode: AppMode) => void;
|
||||||
|
toggleSidebar: () => void;
|
||||||
|
sidebarOpen: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function useAppMode(): UseAppModeReturn {
|
||||||
|
const [mode, setModeState] = useState<AppMode>('chat');
|
||||||
|
const [sidebarOpen, setSidebarOpen] = useState(false);
|
||||||
|
|
||||||
|
const setMode = useCallback((next: AppMode) => {
|
||||||
|
setModeState(next);
|
||||||
|
if (next === 'sidebar') {
|
||||||
|
setSidebarOpen(true);
|
||||||
|
}
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
const toggleSidebar = useCallback(() => {
|
||||||
|
setSidebarOpen((prev) => {
|
||||||
|
if (prev) {
|
||||||
|
// Closing sidebar — return to chat
|
||||||
|
setModeState('chat');
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
// Opening sidebar — set mode to sidebar
|
||||||
|
setModeState('sidebar');
|
||||||
|
return true;
|
||||||
|
});
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
return { mode, setMode, toggleSidebar, sidebarOpen };
|
||||||
|
}
|
||||||
139
packages/cli/src/tui/hooks/use-conversations.ts
Normal file
139
packages/cli/src/tui/hooks/use-conversations.ts
Normal file
@@ -0,0 +1,139 @@
|
|||||||
|
import { useState, useEffect, useRef, useCallback } from 'react';
|
||||||
|
|
||||||
|
export interface ConversationSummary {
|
||||||
|
id: string;
|
||||||
|
title: string | null;
|
||||||
|
archived: boolean;
|
||||||
|
createdAt: string;
|
||||||
|
updatedAt: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface UseConversationsOptions {
|
||||||
|
gatewayUrl: string;
|
||||||
|
sessionCookie?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface UseConversationsReturn {
|
||||||
|
conversations: ConversationSummary[];
|
||||||
|
loading: boolean;
|
||||||
|
error: string | null;
|
||||||
|
refresh: () => Promise<void>;
|
||||||
|
createConversation: (title?: string) => Promise<ConversationSummary | null>;
|
||||||
|
deleteConversation: (id: string) => Promise<boolean>;
|
||||||
|
renameConversation: (id: string, title: string) => Promise<boolean>;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function useConversations(opts: UseConversationsOptions): UseConversationsReturn {
|
||||||
|
const { gatewayUrl, sessionCookie } = opts;
|
||||||
|
|
||||||
|
const [conversations, setConversations] = useState<ConversationSummary[]>([]);
|
||||||
|
const [loading, setLoading] = useState(true);
|
||||||
|
const [error, setError] = useState<string | null>(null);
|
||||||
|
const mountedRef = useRef(true);
|
||||||
|
|
||||||
|
const headers = useCallback((): Record<string, string> => {
|
||||||
|
const h: Record<string, string> = { 'Content-Type': 'application/json' };
|
||||||
|
if (sessionCookie) h['Cookie'] = sessionCookie;
|
||||||
|
return h;
|
||||||
|
}, [sessionCookie]);
|
||||||
|
|
||||||
|
const refresh = useCallback(async () => {
|
||||||
|
if (!mountedRef.current) return;
|
||||||
|
setLoading(true);
|
||||||
|
setError(null);
|
||||||
|
try {
|
||||||
|
const res = await fetch(`${gatewayUrl}/api/conversations`, { headers: headers() });
|
||||||
|
if (!res.ok) throw new Error(`HTTP ${res.status}`);
|
||||||
|
const data = (await res.json()) as ConversationSummary[];
|
||||||
|
if (mountedRef.current) {
|
||||||
|
setConversations(data);
|
||||||
|
}
|
||||||
|
} catch (err) {
|
||||||
|
if (mountedRef.current) {
|
||||||
|
setError(err instanceof Error ? err.message : 'Unknown error');
|
||||||
|
}
|
||||||
|
} finally {
|
||||||
|
if (mountedRef.current) {
|
||||||
|
setLoading(false);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}, [gatewayUrl, headers]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
mountedRef.current = true;
|
||||||
|
void refresh();
|
||||||
|
return () => {
|
||||||
|
mountedRef.current = false;
|
||||||
|
};
|
||||||
|
}, [refresh]);
|
||||||
|
|
||||||
|
const createConversation = useCallback(
|
||||||
|
async (title?: string): Promise<ConversationSummary | null> => {
|
||||||
|
try {
|
||||||
|
const res = await fetch(`${gatewayUrl}/api/conversations`, {
|
||||||
|
method: 'POST',
|
||||||
|
headers: headers(),
|
||||||
|
body: JSON.stringify({ title: title ?? null }),
|
||||||
|
});
|
||||||
|
if (!res.ok) throw new Error(`HTTP ${res.status}`);
|
||||||
|
const data = (await res.json()) as ConversationSummary;
|
||||||
|
if (mountedRef.current) {
|
||||||
|
setConversations((prev) => [data, ...prev]);
|
||||||
|
}
|
||||||
|
return data;
|
||||||
|
} catch {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
[gatewayUrl, headers],
|
||||||
|
);
|
||||||
|
|
||||||
|
const deleteConversation = useCallback(
|
||||||
|
async (id: string): Promise<boolean> => {
|
||||||
|
try {
|
||||||
|
const res = await fetch(`${gatewayUrl}/api/conversations/${id}`, {
|
||||||
|
method: 'DELETE',
|
||||||
|
headers: headers(),
|
||||||
|
});
|
||||||
|
if (!res.ok) return false;
|
||||||
|
if (mountedRef.current) {
|
||||||
|
setConversations((prev) => prev.filter((c) => c.id !== id));
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
} catch {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
[gatewayUrl, headers],
|
||||||
|
);
|
||||||
|
|
||||||
|
const renameConversation = useCallback(
|
||||||
|
async (id: string, title: string): Promise<boolean> => {
|
||||||
|
try {
|
||||||
|
const res = await fetch(`${gatewayUrl}/api/conversations/${id}`, {
|
||||||
|
method: 'PATCH',
|
||||||
|
headers: headers(),
|
||||||
|
body: JSON.stringify({ title }),
|
||||||
|
});
|
||||||
|
if (!res.ok) return false;
|
||||||
|
if (mountedRef.current) {
|
||||||
|
setConversations((prev) => prev.map((c) => (c.id === id ? { ...c, title } : c)));
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
} catch {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
[gatewayUrl, headers],
|
||||||
|
);
|
||||||
|
|
||||||
|
return {
|
||||||
|
conversations,
|
||||||
|
loading,
|
||||||
|
error,
|
||||||
|
refresh,
|
||||||
|
createConversation,
|
||||||
|
deleteConversation,
|
||||||
|
renameConversation,
|
||||||
|
};
|
||||||
|
}
|
||||||
@@ -59,6 +59,8 @@ export interface UseSocketReturn {
|
|||||||
availableThinkingLevels: string[];
|
availableThinkingLevels: string[];
|
||||||
sendMessage: (content: string) => void;
|
sendMessage: (content: string) => void;
|
||||||
setThinkingLevel: (level: string) => void;
|
setThinkingLevel: (level: string) => void;
|
||||||
|
switchConversation: (id: string) => void;
|
||||||
|
clearMessages: () => void;
|
||||||
connectionError: string | null;
|
connectionError: string | null;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -239,6 +241,22 @@ export function useSocket(opts: UseSocketOptions): UseSocketReturn {
|
|||||||
});
|
});
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
|
const clearMessages = useCallback(() => {
|
||||||
|
setMessages([]);
|
||||||
|
setCurrentStreamText('');
|
||||||
|
setCurrentThinkingText('');
|
||||||
|
setActiveToolCalls([]);
|
||||||
|
setIsStreaming(false);
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
const switchConversation = useCallback(
|
||||||
|
(id: string) => {
|
||||||
|
clearMessages();
|
||||||
|
setConversationId(id);
|
||||||
|
},
|
||||||
|
[clearMessages],
|
||||||
|
);
|
||||||
|
|
||||||
return {
|
return {
|
||||||
connected,
|
connected,
|
||||||
connecting,
|
connecting,
|
||||||
@@ -255,6 +273,8 @@ export function useSocket(opts: UseSocketOptions): UseSocketReturn {
|
|||||||
availableThinkingLevels,
|
availableThinkingLevels,
|
||||||
sendMessage,
|
sendMessage,
|
||||||
setThinkingLevel,
|
setThinkingLevel,
|
||||||
|
switchConversation,
|
||||||
|
clearMessages,
|
||||||
connectionError,
|
connectionError,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user