This commit is contained in:
2026-02-20 04:32:50 +00:00
parent d7503ca56b
commit d9f6eb14ed
24 changed files with 1426 additions and 1183 deletions
@@ -108,7 +108,7 @@ const ClaudeInner = ({ sessionId, model }: InnerProps) => {
window.history.replaceState(null, '', '/chat');
}}
/>
<EmbeddableChat chat={chat} provider="claude" availableModels={models} className="flex-1 min-h-0" />
<EmbeddableChat chat={chat} availableModels={models} className="flex-1 min-h-0" />
</div>
);
};
@@ -134,7 +134,7 @@ const OpenCodeInner = ({ sessionId, model }: InnerProps) => {
window.history.replaceState(null, '', '/chat');
}}
/>
<EmbeddableChat chat={chat} provider="opencode" availableModels={models} className="flex-1 min-h-0" />
<EmbeddableChat chat={chat} availableModels={models} className="flex-1 min-h-0" />
</div>
);
};
@@ -160,12 +160,12 @@ const PiMonoInner = ({ sessionId, model }: InnerProps) => {
window.history.replaceState(null, '', '/chat');
}}
/>
<EmbeddableChat chat={chat} provider="pi-mono" availableModels={models} className="flex-1 min-h-0" />
<EmbeddableChat chat={chat} availableModels={models} className="flex-1 min-h-0" />
</div>
);
};
const NewClaudeInner = ({ onProviderChange }: { onProviderChange: (p: 'claude' | 'opencode' | 'pi-mono') => void }) => {
const NewClaudeInner = () => {
const location = useLocation();
const locationState = location.state as ChatLocationState;
const initialSentRef = useRef(false);
@@ -205,9 +205,7 @@ const NewClaudeInner = ({ onProviderChange }: { onProviderChange: (p: 'claude' |
/>
<EmbeddableChat
chat={chat}
provider="claude"
availableModels={models}
onProviderChange={onProviderChange}
defaultInput={locationState?.prefillInput ?? ''}
cwd={locationState?.cwd}
className="flex-1 min-h-0"
@@ -216,7 +214,7 @@ const NewClaudeInner = ({ onProviderChange }: { onProviderChange: (p: 'claude' |
);
};
const NewOpenCodeInner = ({ onProviderChange }: { onProviderChange: (p: 'claude' | 'opencode' | 'pi-mono') => void }) => {
const NewOpenCodeInner = () => {
const location = useLocation();
const locationState = location.state as ChatLocationState;
const initialSentRef = useRef(false);
@@ -256,9 +254,7 @@ const NewOpenCodeInner = ({ onProviderChange }: { onProviderChange: (p: 'claude'
/>
<EmbeddableChat
chat={chat}
provider="opencode"
availableModels={models}
onProviderChange={onProviderChange}
defaultInput={locationState?.prefillInput ?? ''}
className="flex-1 min-h-0"
/>
@@ -266,7 +262,7 @@ const NewOpenCodeInner = ({ onProviderChange }: { onProviderChange: (p: 'claude'
);
};
const NewPiMonoInner = ({ onProviderChange }: { onProviderChange: (p: 'claude' | 'opencode' | 'pi-mono') => void }) => {
const NewPiMonoInner = () => {
const location = useLocation();
const locationState = location.state as ChatLocationState;
const initialSentRef = useRef(false);
@@ -306,9 +302,7 @@ const NewPiMonoInner = ({ onProviderChange }: { onProviderChange: (p: 'claude' |
/>
<EmbeddableChat
chat={chat}
provider="pi-mono"
availableModels={models}
onProviderChange={onProviderChange}
defaultInput={locationState?.prefillInput ?? ''}
className="flex-1 min-h-0"
/>
@@ -320,33 +314,15 @@ type NewChatPanelProps = {
initialProvider?: 'claude' | 'opencode' | 'pi-mono';
};
const NewChatPanel = ({ initialProvider = 'claude' }: NewChatPanelProps) => {
const [selected, setSelected] = usePanelChannel<SelectedSession>(CHANNEL, null);
const provider = selected?.provider ?? initialProvider;
const handleProviderChange = (p: 'claude' | 'opencode' | 'pi-mono') => {
setSelected({ id: 'new', provider: p });
};
const NewChatPanel = ({ initialProvider = 'pi-mono' }: NewChatPanelProps) => {
const [selected] = usePanelChannel<SelectedSession>(CHANNEL, null);
// Once a session is created, the inner component updates selected via the channel
if (selected && selected.id !== 'new') {
if (selected.provider === 'claude') {
return <ClaudeInner key={selected.id} sessionId={selected.id} model={selected.model} />;
}
if (selected.provider === 'opencode') {
return <OpenCodeInner key={selected.id} sessionId={selected.id} model={selected.model} />;
}
return <PiMonoInner key={selected.id} sessionId={selected.id} model={selected.model} />;
}
if (provider === 'claude') {
return <NewClaudeInner key="new-claude" onProviderChange={handleProviderChange} />;
}
if (provider === 'opencode') {
return <NewOpenCodeInner key="new-opencode" onProviderChange={handleProviderChange} />;
}
return <NewPiMonoInner key="new-pi-mono" onProviderChange={handleProviderChange} />;
return <NewPiMonoInner key="new-pi-mono" />;
};
export const ChatDetailPanel = () => {
@@ -364,11 +340,5 @@ export const ChatDetailPanel = () => {
return <NewChatPanel key="new" initialProvider={selected.provider} />;
}
if (selected.provider === 'claude') {
return <ClaudeInner key={selected.id} sessionId={selected.id} model={selected.model} />;
}
if (selected.provider === 'opencode') {
return <OpenCodeInner key={selected.id} sessionId={selected.id} model={selected.model} />;
}
return <PiMonoInner key={selected.id} sessionId={selected.id} model={selected.model} />;
};
@@ -1,13 +1,10 @@
import { useState, useEffect, useRef, useCallback } from 'react';
import { useEffect, useRef, useCallback } from 'react';
import { Plus, MessageSquare, Trash2 } from 'lucide-react';
import { usePanelChannel } from 'hooks/usePanelChannel';
import { useChatSessions } from '@/state/useChatSessions';
import type { SelectedSession } from './ChatDetailPanel';
type Filter = 'all' | 'claude' | 'opencode' | 'pi-mono';
export const SessionList = () => {
const [filter, setFilter] = useState<Filter>('all');
const { sessions, deleteSession } = useChatSessions();
const [selected, setSelected] = usePanelChannel<SelectedSession>('chat:selected-session', null);
const scrolledRef = useRef(false);
@@ -25,17 +22,9 @@ export const SessionList = () => {
scrolledRef.current = false;
}, [selected?.id, selected?.provider]);
const filtered = filter === 'all' ? sessions : sessions.filter((s) => s.provider === filter);
const handleSelect = (session: (typeof sessions)[number]) => {
setSelected({ id: session.id, provider: session.provider, model: session.model ?? null });
const path =
session.provider === 'claude'
? `/chat/${session.id}`
: session.provider === 'opencode'
? `/chat/opencode/${session.id}`
: `/chat/pi-mono/${session.id}`;
window.history.replaceState(null, '', path);
window.history.replaceState(null, '', `/chat/${session.id}`);
};
const handleDelete = async (provider: 'claude' | 'opencode' | 'pi-mono', id: string) => {
@@ -51,51 +40,33 @@ export const SessionList = () => {
{/* Header */}
<div className="shrink-0 flex items-center justify-between px-4 py-2 border-b border-duck-dark/10 dark:border-foreground/10 bg-background/60">
<h2 className="text-sm font-medium text-duck-dark/70 dark:text-foreground/70">Sessions</h2>
<div className="flex items-center gap-2">
{/* Radio filter */}
<div className="flex items-center gap-0.5 rounded-lg bg-duck-dark/5 dark:bg-foreground/5 p-0.5">
{(['all', 'claude', 'opencode', 'pi-mono'] as const).map((value) => (
<button
key={value}
onClick={() => setFilter(value)}
className={`rounded-md px-2.5 py-1 text-xs font-medium transition-colors cursor-pointer ${
filter === value
? 'bg-background dark:bg-foreground/10 text-duck-dark dark:text-foreground shadow-sm'
: 'text-duck-dark/50 dark:text-foreground/50 hover:text-duck-dark/80 dark:hover:text-foreground/80'
}`}
>
{value === 'all' ? 'All' : value === 'claude' ? 'Claude' : value === 'opencode' ? 'OpenCode' : 'Pi'}
</button>
))}
</div>
<button
onClick={() => {
setSelected({ id: 'new', provider: 'claude' });
window.history.replaceState(null, '', '/chat/new');
}}
className="flex items-center gap-1.5 rounded-md bg-duck-teal hover:bg-duck-teal/90 text-duck-yellow cursor-pointer h-7 px-3 text-xs font-medium"
>
<Plus className="h-3.5 w-3.5" />
New Chat
</button>
</div>
<button
onClick={() => {
setSelected({ id: 'new', provider: 'pi-mono' });
window.history.replaceState(null, '', '/chat/new');
}}
className="flex items-center gap-1.5 rounded-md bg-duck-teal hover:bg-duck-teal/90 text-duck-yellow cursor-pointer h-7 px-3 text-xs font-medium"
>
<Plus className="h-3.5 w-3.5" />
New Chat
</button>
</div>
{/* Session list */}
<div className="flex-1 min-h-0 overflow-y-auto p-3 space-y-1.5">
{filtered.length === 0 && (
{sessions.length === 0 && (
<div className="text-center py-16 text-duck-dark/30 dark:text-foreground/30 text-sm">
No sessions yet. Start a new chat!
</div>
)}
{filtered.map((session) => {
{sessions.map((session) => {
const isSelected = selected?.id === session.id && selected?.provider === session.provider;
return (
<div
key={`${session.provider}-${session.id}`}
ref={isSelected ? selectedRef : undefined}
className={`group flex items-center gap-3 rounded-lg border transition-colors cursor-pointer ${
className={`group flex items-center rounded-lg border transition-colors cursor-pointer ${
isSelected
? 'border-duck-teal/30 bg-duck-teal/5 dark:bg-duck-teal/10'
: 'border-duck-dark/10 dark:border-foreground/10 bg-background/80 hover:bg-background/90'
@@ -109,6 +80,9 @@ export const SessionList = () => {
<div className="min-w-0 flex-1">
<div className="text-sm font-medium text-duck-dark/80 dark:text-foreground/80 truncate">
{session.title}
<span className="ml-1.5 font-mono text-xs font-normal text-duck-dark/25 dark:text-foreground/25">
({session.id.slice(0, 8)})
</span>
</div>
<div className="text-xs text-duck-dark/40 dark:text-foreground/40">
{new Date(session.createdAt).toLocaleDateString(undefined, {
@@ -117,21 +91,12 @@ export const SessionList = () => {
hour: '2-digit',
minute: '2-digit',
})}
<span
className={`ml-2 text-xs font-medium ${
session.provider === 'claude'
? 'text-duck-teal'
: session.provider === 'opencode'
? 'text-duck-orange'
: 'text-purple-500'
}`}
>
{session.provider === 'claude' ? 'Claude' : session.provider === 'opencode' ? 'OpenCode' : 'Pi'}
</span>
<span className="ml-2 font-mono text-duck-dark/25 dark:text-foreground/25">
{session.id.slice(0, 8)}
</span>
</div>
{session.model && (
<div className="text-xs text-duck-teal/70 dark:text-duck-teal/60 truncate">
{session.model}
</div>
)}
</div>
</button>
<button
@@ -19,27 +19,30 @@ export const ChatHistory = () => {
className="flex items-center gap-2 px-2 py-1.5 rounded-md hover:bg-duck-dark/5 group"
>
<Link
to={session.provider === 'claude' ? `/chat/${session.id}` : `/chat/opencode/${session.id}`}
to={`/chat/${session.id}`}
className="flex items-center gap-2 flex-1 min-w-0"
>
<MessageSquare className="h-4 w-4 shrink-0 text-duck-teal/60" />
<div className="min-w-0 flex-1">
<span className="text-sm text-duck-dark truncate block">{session.title}</span>
<span className="text-xs text-duck-dark/40 truncate block">
<span className="text-sm text-duck-dark dark:text-foreground truncate block">
{session.title}
<span className="ml-1.5 font-mono text-xs text-duck-dark/25 dark:text-foreground/25">
({session.id.slice(0, 8)})
</span>
</span>
<span className="text-xs text-duck-dark/40 dark:text-foreground/40 truncate block">
{new Date(session.createdAt).toLocaleDateString(undefined, {
month: 'short',
day: 'numeric',
hour: '2-digit',
minute: '2-digit',
})}
<span
className={`ml-1.5 font-medium ${
session.provider === 'claude' ? 'text-duck-teal' : 'text-duck-orange'
}`}
>
{session.provider === 'claude' ? 'Claude' : 'OpenCode'}
</span>
</span>
{session.model && (
<span className="text-xs text-duck-teal/70 dark:text-duck-teal/60 truncate block">
{session.model}
</span>
)}
</div>
</Link>
<button
@@ -22,24 +22,23 @@ const layout: LayoutNode = {
};
type SessionListPageProps = {
provider?: 'claude' | 'opencode' | 'pi-mono';
isNew?: boolean;
};
export const SessionListPage = ({ provider = 'claude', isNew }: SessionListPageProps) => {
export const SessionListPage = ({ isNew }: SessionListPageProps) => {
const { sessionId } = useParams<{ sessionId: string }>();
const { sessions } = useChatSessions();
const [, setSelected] = usePanelChannel<SelectedSession>('chat:selected-session', null);
useEffect(() => {
if (isNew) {
setSelected({ id: 'new', provider });
setSelected({ id: 'new', provider: 'pi-mono' });
return;
}
if (!sessionId) return;
const session = sessions.find((s) => s.id === sessionId && s.provider === provider);
setSelected({ id: sessionId, provider, model: session?.model ?? null });
}, [sessionId, provider, isNew]);
const session = sessions.find((s) => s.id === sessionId);
setSelected({ id: sessionId, provider: session?.provider ?? 'pi-mono', model: session?.model ?? null });
}, [sessionId, isNew]);
const panelComponents: PanelComponents = useMemo(
() => ({