diff --git a/src/components/ChatPane.tsx b/src/components/ChatPane.tsx index 2eaecb9..d50e74d 100644 --- a/src/components/ChatPane.tsx +++ b/src/components/ChatPane.tsx @@ -222,6 +222,7 @@ export function ChatPane({ session, cfg, messages, onSend, onStop, browsePath, b
{cfg.backend ?? session.backend} {truncate(cfg.model ?? session.model, 24)} + {session.cwd} {cfg.agent && cfg.agent !== 'default' && ( {cfg.agent} )} diff --git a/src/components/PromptBar.tsx b/src/components/PromptBar.tsx index 75844c5..b0f58d0 100644 --- a/src/components/PromptBar.tsx +++ b/src/components/PromptBar.tsx @@ -10,7 +10,6 @@ export function PromptBar({ running, onSend, onStop }: Props) { const [value, setValue] = useState('') const ref = useRef(null) - useEffect(() => { ref.current?.focus() }) const submit = () => { const text = value.trim() diff --git a/src/components/Sidebar.tsx b/src/components/Sidebar.tsx index 4fdc73c..27d7438 100644 --- a/src/components/Sidebar.tsx +++ b/src/components/Sidebar.tsx @@ -1,7 +1,6 @@ import { useState } from 'preact/hooks' import type { IdxEntry } from '../lib/api' import type { Theme } from '../lib/theme' -import { truncate } from '../lib/chat' interface Props { sessions: IdxEntry[] @@ -18,12 +17,44 @@ interface Props { function stateClass(state: string) { if (state === 'idle') return 'idle' if (state === 'stopped') return 'stopped' - return 'running' // thinking, calling: , etc. + return 'running' +} + +interface SessionNode { + session: IdxEntry | null + children: Map +} + +function buildTree(sessions: IdxEntry[]): SessionNode { + const root: SessionNode = { session: null, children: new Map() } + for (const s of sessions) { + const parts = s.id.split('__') + let node = root + for (let i = 0; i < parts.length - 1; i++) { + const key = parts.slice(0, i + 1).join('__') + if (!node.children.has(key)) { + node.children.set(key, { session: null, children: new Map() }) + } + node = node.children.get(key)! + } + if (node.children.has(s.id)) { + node.children.get(s.id)!.session = s + } else { + node.children.set(s.id, { session: s, children: new Map() }) + } + } + return root +} + +function leafName(id: string): string { + const parts = id.split('__') + return parts[parts.length - 1] } export function Sidebar({ sessions, selectedId, onSelect, onNew, onKill, onRename, themes, currentTheme, onThemeChange }: Props) { const [editingId, setEditingId] = useState(null) const [editValue, setEditValue] = useState('') + const [collapsed, setCollapsed] = useState>(new Set()) const startRename = (e: MouseEvent, id: string) => { e.stopPropagation() @@ -36,6 +67,70 @@ export function Sidebar({ sessions, selectedId, onSelect, onNew, onKill, onRenam setEditingId(null) if (name && name !== oldId) onRename(oldId, name) } + + const toggle = (id: string, e: MouseEvent) => { + e.stopPropagation() + setCollapsed(prev => { + const next = new Set(prev) + if (next.has(id)) next.delete(id); else next.add(id) + return next + }) + } + + const tree = buildTree(sessions) + + function renderNode(node: SessionNode, depth: number): any { + const items: any[] = [] + for (const [id, child] of node.children) { + const s = child.session + const hasChildren = child.children.size > 0 + const isCollapsed = collapsed.has(id) + + if (s) { + items.push( +
+
onSelect(s.id === selectedId ? null : s.id)} + > + {hasChildren && ( + toggle(id, e)}>▾ + )} + + {editingId === s.id ? ( + setEditValue((e.target as HTMLInputElement).value)} + onBlur={() => commitRename(s.id)} + onKeyDown={e => { if (e.key === 'Enter') commitRename(s.id); if (e.key === 'Escape') setEditingId(null) }} + onClick={e => e.stopPropagation()} + autoFocus + /> + ) : ( + startRename(e, s.id)}>{leafName(s.id)} + )} + {s.state} + +
+ {hasChildren && !isCollapsed && renderNode(child, depth + 1)} +
+ ) + } else if (hasChildren) { + items.push( +
+ {renderNode(child, depth + 1)} +
+ ) + } + } + return items + } + return (