harness uniformization
This commit is contained in:
@@ -1,6 +1,8 @@
|
||||
import { useRef, useEffect, useState } from 'react';
|
||||
import { useNavigate, useLocation } from 'react-router';
|
||||
import { useSessions, useOpenCodeSessions, useSlashCommands, SessionBar } from 'widgets/ChatHistory';
|
||||
import { useChatSessions } from '@/state/useChatSessions';
|
||||
import { useSlashCommands } from '@/state/useSlashCommands';
|
||||
import { SessionBar } from 'widgets/ChatHistory';
|
||||
import type { ModelOption } from '@/state/useModels';
|
||||
import type { useClaude } from './useClaude';
|
||||
import { EmbeddableChat, type Attachment } from './EmbeddableChat';
|
||||
@@ -24,13 +26,7 @@ export const ChatPanel = ({ chat, provider = 'claude', availableModels = [], onP
|
||||
const [fullscreen, setFullscreen] = useState(false);
|
||||
const initialSentRef = useRef(false);
|
||||
|
||||
const claudeSessions = useSessions();
|
||||
const opencodeSessions = useOpenCodeSessions();
|
||||
const { archiveSession, deleteSession } =
|
||||
provider === 'claude'
|
||||
? claudeSessions
|
||||
: { archiveSession: undefined, deleteSession: opencodeSessions.deleteSession };
|
||||
const sessions = provider === 'claude' ? claudeSessions.sessions : opencodeSessions.sessions;
|
||||
const { sessions, archiveSession, deleteSession } = useChatSessions();
|
||||
const slashCommands = useSlashCommands({ sessionId });
|
||||
const sessionTitle = sessionId ? sessions.find((s) => s.id === sessionId)?.title : undefined;
|
||||
const listPath = '/chat';
|
||||
@@ -89,16 +85,16 @@ export const ChatPanel = ({ chat, provider = 'claude', availableModels = [], onP
|
||||
isGenerating={isGenerating}
|
||||
fullscreen={fullscreen}
|
||||
onArchive={
|
||||
archiveSession && sessionId
|
||||
provider === 'claude' && sessionId
|
||||
? async () => {
|
||||
await archiveSession(sessionId);
|
||||
await archiveSession(provider, sessionId);
|
||||
navigate(listPath);
|
||||
}
|
||||
: undefined
|
||||
}
|
||||
onDelete={async () => {
|
||||
if (!sessionId) return;
|
||||
await deleteSession(sessionId);
|
||||
await deleteSession(provider, sessionId);
|
||||
navigate(listPath);
|
||||
}}
|
||||
onToggleFullscreen={() => setFullscreen((f) => !f)}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { useState, useEffect, useRef } from 'react';
|
||||
import { useChatWebSocket } from 'hooks/useChatWebSocket';
|
||||
import { useSessions } from 'widgets/ChatHistory';
|
||||
import { useChatSessions } from '@/state/useChatSessions';
|
||||
import type { ChatMessage, ServerMessage, TaskInfo } from 'widgets/Chat';
|
||||
|
||||
const SAVE_DEBOUNCE_MS = 1000;
|
||||
@@ -31,7 +31,7 @@ export const useClaude = (initialSessionId?: string, initialModel?: string | nul
|
||||
const sessionIdRef = useRef<string | null>(initialSessionId ?? null);
|
||||
const saveTimerRef = useRef<number | null>(null);
|
||||
|
||||
const { getMessages, saveMessages } = useSessions();
|
||||
const { getMessages, saveMessages } = useChatSessions();
|
||||
|
||||
const token = localStorage.getItem('BEARER_TOKEN');
|
||||
const protocol = window.location.protocol === 'https:' ? 'wss:' : 'ws:';
|
||||
@@ -137,7 +137,7 @@ export const useClaude = (initialSessionId?: string, initialModel?: string | nul
|
||||
return;
|
||||
}
|
||||
if (!initialSessionId) return;
|
||||
getMessages(initialSessionId)
|
||||
getMessages('claude', initialSessionId)
|
||||
.then((data) => {
|
||||
if (Array.isArray(data) && data.length > 0) setMessages(data);
|
||||
})
|
||||
@@ -156,7 +156,7 @@ export const useClaude = (initialSessionId?: string, initialModel?: string | nul
|
||||
if (storage) {
|
||||
storage.save(sid, snapshot).catch(() => {});
|
||||
} else {
|
||||
saveMessages(sid, snapshot).catch(() => {});
|
||||
saveMessages('claude', sid, snapshot).catch(() => {});
|
||||
}
|
||||
saveTimerRef.current = null;
|
||||
}, SAVE_DEBOUNCE_MS);
|
||||
|
||||
@@ -2,7 +2,7 @@ import { useState, useEffect, useRef } from 'react';
|
||||
import { useChatWebSocket } from 'hooks/useChatWebSocket';
|
||||
import { useSettings } from '@/state/useSettings';
|
||||
import { useVisibleOpenCodeModels } from '@/state/useModels';
|
||||
import { useOpenCodeSessions } from 'widgets/ChatHistory';
|
||||
import { useChatSessions } from '@/state/useChatSessions';
|
||||
import type { ChatMessage, ServerMessage, TaskInfo } from 'widgets/Chat';
|
||||
|
||||
type UseOpenCodeOptions = {
|
||||
@@ -29,7 +29,7 @@ export const useOpenCode = (initialSessionId?: string, initialModel?: string | n
|
||||
setSelectedModel(value);
|
||||
};
|
||||
|
||||
const { getMessages } = useOpenCodeSessions();
|
||||
const { getMessages } = useChatSessions();
|
||||
const { settings } = useSettings();
|
||||
const openCodeModels = useVisibleOpenCodeModels();
|
||||
|
||||
@@ -144,7 +144,7 @@ export const useOpenCode = (initialSessionId?: string, initialModel?: string | n
|
||||
// Load messages from OpenCode on mount when resuming a session
|
||||
useEffect(() => {
|
||||
if (!initialSessionId) return;
|
||||
getMessages(initialSessionId)
|
||||
getMessages('opencode', initialSessionId)
|
||||
.then((data) => {
|
||||
if (Array.isArray(data) && data.length > 0) setMessages(data);
|
||||
})
|
||||
|
||||
@@ -1,28 +1,17 @@
|
||||
import { useMemo, useState } from 'react';
|
||||
import { useState } from 'react';
|
||||
import { Link } from 'react-router';
|
||||
import { Plus, MessageSquare, Trash2 } from 'lucide-react';
|
||||
import { Button } from '@/components/ui/button';
|
||||
import { Card } from '@/components/Card';
|
||||
import { useSessions, useOpenCodeSessions } from 'widgets/ChatHistory';
|
||||
import { useChatSessions } from '@/state/useChatSessions';
|
||||
|
||||
type Filter = 'all' | 'claude' | 'opencode';
|
||||
|
||||
export const SessionList = () => {
|
||||
const [filter, setFilter] = useState<Filter>('all');
|
||||
const claude = useSessions();
|
||||
const opencode = useOpenCodeSessions();
|
||||
const { sessions, deleteSession } = useChatSessions();
|
||||
|
||||
const merged = useMemo(
|
||||
() => [...claude.sessions, ...opencode.sessions].sort((a, b) => b.createdAt - a.createdAt),
|
||||
[claude.sessions, opencode.sessions],
|
||||
);
|
||||
|
||||
const filtered = filter === 'all' ? merged : merged.filter((s) => s.provider === filter);
|
||||
|
||||
const handleDelete = (id: string, provider: 'claude' | 'opencode') => {
|
||||
if (provider === 'claude') claude.deleteSession(id);
|
||||
else opencode.deleteSession(id);
|
||||
};
|
||||
const filtered = filter === 'all' ? sessions : sessions.filter((s) => s.provider === filter);
|
||||
|
||||
return (
|
||||
<div className="flex flex-col h-full items-center p-4 md:p-6">
|
||||
@@ -90,7 +79,7 @@ export const SessionList = () => {
|
||||
</div>
|
||||
</Link>
|
||||
<button
|
||||
onClick={() => handleDelete(session.id, session.provider)}
|
||||
onClick={() => deleteSession(session.provider, session.id)}
|
||||
className="shrink-0 p-2 mr-2 text-duck-dark/20 hover:text-red-500 md:opacity-0 md:group-hover:opacity-100 transition-opacity cursor-pointer"
|
||||
>
|
||||
<Trash2 className="h-4 w-4" />
|
||||
|
||||
@@ -1,25 +1,12 @@
|
||||
import { useMemo } from 'react';
|
||||
import { Link } from 'react-router';
|
||||
import { MessageSquare, Trash2, ChevronDown, ChevronUp } from 'lucide-react';
|
||||
import { Card } from '@/components/Card';
|
||||
import { useSessions } from 'widgets/ChatHistory';
|
||||
import { useOpenCodeSessions } from 'widgets/ChatHistory';
|
||||
import { useChatSessions } from '@/state/useChatSessions';
|
||||
import { useUserState } from '@/state/useUserState';
|
||||
|
||||
export const ChatHistory = () => {
|
||||
const [collapsed, setCollapsed] = useUserState('widget:chatHistory:collapsed', true);
|
||||
const claude = useSessions();
|
||||
const opencode = useOpenCodeSessions();
|
||||
|
||||
const sessions = useMemo(
|
||||
() => [...claude.sessions, ...opencode.sessions].sort((a, b) => b.createdAt - a.createdAt),
|
||||
[claude.sessions, opencode.sessions],
|
||||
);
|
||||
|
||||
const handleDelete = (id: string, provider: 'claude' | 'opencode') => {
|
||||
if (provider === 'claude') claude.deleteSession(id);
|
||||
else opencode.deleteSession(id);
|
||||
};
|
||||
const { sessions, deleteSession } = useChatSessions();
|
||||
|
||||
return (
|
||||
<div className="w-full">
|
||||
@@ -72,7 +59,7 @@ export const ChatHistory = () => {
|
||||
</div>
|
||||
</Link>
|
||||
<button
|
||||
onClick={() => handleDelete(session.id, session.provider)}
|
||||
onClick={() => deleteSession(session.provider, session.id)}
|
||||
className="shrink-0 p-1 rounded text-duck-dark/20 md:opacity-0 md:group-hover:opacity-100 hover:text-red-500 transition-opacity cursor-pointer"
|
||||
>
|
||||
<Trash2 className="h-3.5 w-3.5" />
|
||||
|
||||
@@ -0,0 +1,56 @@
|
||||
import type { SessionEntry, ChatMessage } from 'widgets/Chat';
|
||||
import type { SlashCommandResult } from './useSlashCommands';
|
||||
import { useAuth } from 'hooks/useAuth';
|
||||
import { useClient } from 'hooks/useClient';
|
||||
import { useQuery, useQueryClient } from '@tanstack/react-query';
|
||||
|
||||
export const useChatSessions = () => {
|
||||
const client = useClient();
|
||||
const queryClient = useQueryClient();
|
||||
const { isAuthenticated } = useAuth();
|
||||
|
||||
const { data: sessions = [] } = useQuery<SessionEntry[]>({
|
||||
queryKey: ['SESSIONS'],
|
||||
enabled: isAuthenticated,
|
||||
queryFn: () => client.get<SessionEntry[]>('/sessions'),
|
||||
});
|
||||
|
||||
const getMessages = (provider: 'claude' | 'opencode', sessionId: string) =>
|
||||
client.get<ChatMessage[]>(`/sessions/${provider}/${sessionId}/messages`);
|
||||
|
||||
const saveMessages = (provider: 'claude' | 'opencode', sessionId: string, messages: ChatMessage[]) =>
|
||||
client.put(`/sessions/${provider}/${sessionId}/messages`, messages);
|
||||
|
||||
const renameSession = async (
|
||||
provider: 'claude' | 'opencode',
|
||||
sessionId: string | null,
|
||||
args: string,
|
||||
): Promise<SlashCommandResult> => {
|
||||
if (!args) return { handled: true, feedback: 'Usage: /rename <new title>' };
|
||||
if (!sessionId) return { handled: true, feedback: 'No active session to rename.' };
|
||||
|
||||
const title = args.slice(0, 200);
|
||||
try {
|
||||
await client.put(`/sessions/${provider}/${sessionId}`, { title });
|
||||
queryClient.setQueryData<SessionEntry[]>(
|
||||
['SESSIONS'],
|
||||
(prev) => prev?.map((s) => (s.id === sessionId ? { ...s, title } : s)) ?? [],
|
||||
);
|
||||
return { handled: true, feedback: `Session renamed to "${title}"` };
|
||||
} catch {
|
||||
return { handled: true, feedback: 'Failed to rename session.' };
|
||||
}
|
||||
};
|
||||
|
||||
const archiveSession = async (provider: 'claude' | 'opencode', sessionId: string) => {
|
||||
await client.post(`/sessions/${provider}/${sessionId}/archive`);
|
||||
queryClient.setQueryData<SessionEntry[]>(['SESSIONS'], (prev) => prev?.filter((s) => s.id !== sessionId) ?? []);
|
||||
};
|
||||
|
||||
const deleteSession = async (provider: 'claude' | 'opencode', sessionId: string) => {
|
||||
await client.delete(`/sessions/${provider}/${sessionId}`);
|
||||
queryClient.setQueryData<SessionEntry[]>(['SESSIONS'], (prev) => prev?.filter((s) => s.id !== sessionId) ?? []);
|
||||
};
|
||||
|
||||
return { sessions, getMessages, saveMessages, renameSession, archiveSession, deleteSession };
|
||||
};
|
||||
@@ -1,10 +1,10 @@
|
||||
import { useSessions } from 'widgets/ChatHistory';
|
||||
import { useChatSessions } from '@/state/useChatSessions';
|
||||
import { usePlans } from './usePlans';
|
||||
import { useSettings } from './useSettings';
|
||||
import { useThemeSync } from './useThemeSync';
|
||||
|
||||
export const useInitialData = () => {
|
||||
const { sessions } = useSessions();
|
||||
const { sessions } = useChatSessions();
|
||||
const { plans } = usePlans();
|
||||
const { settings } = useSettings();
|
||||
useThemeSync();
|
||||
|
||||
@@ -0,0 +1,29 @@
|
||||
import { useChatSessions } from '@/state/useChatSessions';
|
||||
|
||||
export type SlashCommandResult = { handled: true; feedback: string } | { handled: false };
|
||||
|
||||
type UseSlashCommandsParams = {
|
||||
sessionId: string | null;
|
||||
};
|
||||
|
||||
export const useSlashCommands = ({ sessionId }: UseSlashCommandsParams) => {
|
||||
const { renameSession } = useChatSessions();
|
||||
|
||||
const execute = async (input: string): Promise<SlashCommandResult> => {
|
||||
const trimmed = input.trim();
|
||||
if (!trimmed.startsWith('/')) return { handled: false };
|
||||
|
||||
const spaceIndex = trimmed.indexOf(' ');
|
||||
const command = spaceIndex === -1 ? trimmed.slice(1) : trimmed.slice(1, spaceIndex);
|
||||
const args = spaceIndex === -1 ? '' : trimmed.slice(spaceIndex + 1).trim();
|
||||
|
||||
switch (command) {
|
||||
case 'rename':
|
||||
return renameSession('claude', sessionId, args);
|
||||
default:
|
||||
return { handled: false };
|
||||
}
|
||||
};
|
||||
|
||||
return { execute };
|
||||
};
|
||||
Reference in New Issue
Block a user