import { useEffect, useRef, useState } from 'preact/hooks' import { marked } from 'marked' import type { IdxEntry } from '../lib/api' import type { ChatMessage } from '../lib/chat' import { shortId, truncate } from '../lib/chat' import { PromptBar } from './PromptBar' marked.use({ gfm: true, breaks: false }) interface Props { session: IdxEntry | null cfg: Record | null messages: ChatMessage[] onSend: (text: string) => void onStop: () => void browsePath: string | null browseData: any onBrowse: (path: string) => void onSelectSession: (id: string) => void onRename: (id: string, newName: string) => void } function stateClass(state: string) { if (state === 'idle') return 'idle' if (state === 'stopped') return 'stopped' return 'running' } export function ChatPane({ session, cfg, messages, onSend, onStop, browsePath, browseData, onBrowse, onSelectSession, onRename }: Props) { const listRef = useRef(null) const [atBottom, setAtBottom] = useState(true) const isRunning = !!cfg?.state && cfg.state !== 'idle' && cfg.state !== 'stopped' useEffect(() => { if (atBottom && listRef.current) { listRef.current.scrollTop = listRef.current.scrollHeight } }, [messages.length, isRunning, atBottom]) const handleScroll = () => { const el = listRef.current if (!el) return setAtBottom(el.scrollHeight - el.scrollTop - el.clientHeight < 80) } if (!session || !cfg) { return (
{browsePath && browsePath !== '/mnt/' ? ( ) : ( )}
) } const sid = session.id const state = cfg.state ?? 'stopped' const [renaming, setRenaming] = useState(false) const [renameVal, setRenameVal] = useState('') const startRename = () => { setRenaming(true); setRenameVal(sid) } const commitRename = () => { setRenaming(false) const name = renameVal.trim() if (name && name !== sid) onRename(sid, name) } return (
onBrowse('/mnt/s/')}>↑{' '} /s/{renaming ? ( setRenameVal((e.target as HTMLInputElement).value)} onBlur={() => commitRename()} onKeyDown={e => { if (e.key === 'Enter') commitRename(); if (e.key === 'Escape') setRenaming(false) }} autoFocus /> ) : ( {sid} )}/chat
{cfg.backend ?? session.backend} {truncate(cfg.model ?? session.model, 24)} {cfg.agent && cfg.agent !== 'default' && ( {cfg.agent} )}
{messages.length === 0 && !isRunning && (
/s/{sid}/chat
)} {messages.map((msg, i) => (
{msg.role === 'user' ? (
u: {msg.content}
) : msg.role === 'tool' ? ( ) : msg.role === 'info' ? (
{msg.content}
) : (
/, '

a: ') }} /> )}

))} {isRunning && (
)}
) } function formatToolContent(raw: string): { name: string; detail: string; body: string; output: string } { const m = raw.match(/^(\w+)\((\{[\s\S]*\})\)(.*)$/s) if (!m) return { name: '', detail: '', body: raw, output: '' } const name = m[1] const output = m[3].trim() try { const obj = JSON.parse(m[2]) // Extract a useful detail: tool name or language let detail = '' if (obj.steps?.[0]?.tool) detail = obj.steps[0].tool else if (obj.steps?.[0]?.code) detail = obj.language ?? 'bash' return { name, detail, body: JSON.stringify(obj, null, 2), output } } catch { return { name, detail: '', body: m[2], output } } } function ToolCall({ content }: { content: string }) { const { name, detail, body, output } = formatToolContent(content) const [open, setOpen] = useState(false) return (
setOpen((e.target as HTMLDetailsElement).open)}> 🖥️ {name && {name}} {detail && {detail}}
{body}
{output &&
{output}
}
) } function Welcome({ onBrowse }: { onBrowse: (path: string) => void }) { const tree: [string, string][] = [ ['/mnt/s/', 'sessions'], ['/mnt/m/', 'memory'], ['/mnt/t/', 'tools'], ['/mnt/a/', 'agents'], ['/mnt/sk/', 'skills'], ] return (
ollie
~/mnt/ollie
9p filesystem
{tree.map(([path, desc]) => ( ))}
select a session or browse the filesystem
) } function FsBrowser({ path, data, onNavigate, onSelectSession }: { path: string; data: any; onNavigate: (p: string) => void; onSelectSession: (id: string) => void }) { const parent = path === '/mnt/' ? null : path.replace(/[^/]+\/?$/, '') || '/mnt/' const displayPath = path.startsWith('/mnt') ? path.slice(4) : path const isSessionDir = path === '/mnt/s/' // For /s/, filter to directories only and clicking selects the session const filteredData = isSessionDir && Array.isArray(data) ? data.filter((e: any) => e.is_dir) : data return (
{parent && onNavigate(parent)}>↑} {parent && ' '}{displayPath}
{filteredData === null ? (
loading…
) : Array.isArray(filteredData) && filteredData.length > 0 && typeof filteredData[0] === 'object' ? ( filteredData.map((entry: any) => (
isSessionDir && entry.is_dir ? onSelectSession(entry.name) : entry.is_dir ? onNavigate(path + entry.name + '/') : onNavigate(path + entry.name)} > {entry.name}{entry.is_dir ? '/' : ''} {entry.size > 0 && {entry.size}}
)) ) : Array.isArray(filteredData) ? (
{filteredData.join('\n')}
) : typeof filteredData === 'object' ? (
{Object.entries(filteredData).map(([k, v]) => `${k}=${v}`).join('\n')}
) : (
{String(filteredData)}
)}
) }