From 673cf00babae18613366dcbed12fff515eb5df5a Mon Sep 17 00:00:00 2001 From: Levi Neely Date: Mon, 6 Jul 2026 18:22:59 +0200 Subject: [PATCH] update webui: chat pane and api changes --- src/app.tsx | 2 +- src/components/ChatPane.tsx | 2 +- src/lib/api.ts | 203 ++++++++++++++++++++++-------------- vite.config.ts | 13 +-- 4 files changed, 132 insertions(+), 88 deletions(-) diff --git a/src/app.tsx b/src/app.tsx index a9ab5a7..84a38e7 100644 --- a/src/app.tsx +++ b/src/app.tsx @@ -82,7 +82,7 @@ export function App() { getChatLines(selectedId), ]) if (!alive) return - setCfg(c) + setCfg(c as Record) setMessages(parseChat(lines)) } catch { // session may have been removed diff --git a/src/components/ChatPane.tsx b/src/components/ChatPane.tsx index 1ef1b83..a9dbb8f 100644 --- a/src/components/ChatPane.tsx +++ b/src/components/ChatPane.tsx @@ -205,7 +205,7 @@ export function ChatPane({ session, cfg, messages, onSend, onStop, browsePath, b const renderedHtml = useMemo(() => messages.map(msg => (msg.role === 'user' || msg.role === 'assistant') - ? linkifyPaths((marked.parse(msg.content) as string), session.cwd) + ? linkifyPaths((marked.parse(msg.content) as string), session.cwd ?? '') : ''), [messages, session.cwd] ) diff --git a/src/lib/api.ts b/src/lib/api.ts index ed8d053..e0404a9 100644 --- a/src/lib/api.ts +++ b/src/lib/api.ts @@ -1,68 +1,157 @@ -export interface IdxEntry { +export interface SessionSummary { id: string state: string - cwd: string - backend: string model: string + agent: string + // Available in 9P mode, not in DBus list (fetch config for these) + cwd?: string + backend?: string } -export async function listSessions(): Promise { - const r = await fetch('/s/idx') +// Compat alias for components still using the old name. +export type IdxEntry = SessionSummary + +export interface SessionConfig { + model?: string + backend?: string + agent?: string + cwd?: string + state?: string + [key: string]: string | undefined +} + +export async function listSessions(): Promise { + const r = await fetch('/sessions') if (!r.ok) return [] const data = await r.json() - return Array.isArray(data) ? (data as IdxEntry[]) : [] + return Array.isArray(data) ? (data as SessionSummary[]) : [] } -export async function getSessionCfg(id: string): Promise> { - const r = await fetch(`/s/${id}/cfg`) - if (!r.ok) throw new Error(`cfg ${r.status}`) +export async function getSessionCfg(id: string): Promise { + const r = await fetch(`/sessions/${id}/config`) + if (!r.ok) throw new Error(`config ${r.status}`) const data = await r.json() return (data && typeof data === 'object' && !Array.isArray(data)) - ? (data as Record) + ? (data as SessionConfig) : {} } -// The gateway returns chat as plain text (one line per key=value pair). export async function getChatLines(id: string): Promise { - const r = await fetch(`/s/${id}/chat`) + const r = await fetch(`/sessions/${id}/chat`) if (!r.ok) return [] const text = await r.text() if (!text.trim()) return [] return text.trimEnd().split('\n') } +export async function getChatWithOffset(id: string, offset: number): Promise<{ text: string; newOffset: number }> { + const r = await fetch(`/sessions/${id}/chat?offset=${offset}`) + if (!r.ok) return { text: '', newOffset: offset } + const text = await r.text() + const newOffset = parseInt(r.headers.get('X-Chat-Offset') ?? String(offset), 10) + return { text, newOffset } +} + export async function sendPrompt(id: string, prompt: string): Promise { - await fetch(`/s/${id}/prompt`, { + await fetch(`/sessions/${id}/submit`, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ prompt }), }) } -export async function sendCtl(id: string, ctl: string): Promise { - await fetch(`/s/${id}/ctl`, { - method: 'POST', - headers: { 'Content-Type': 'application/json' }, - body: JSON.stringify({ ctl }), - }) +export async function interruptSession(id: string): Promise { + await fetch(`/sessions/${id}/interrupt`, { method: 'POST' }) } export async function createSession(params: { - name?: string - cwd: string - backend: string - model: string - agent: string - [key: string]: string | undefined -}): Promise { - await fetch('/s/new', { + cwd?: string + backend?: string + model?: string + agent?: string +}): Promise { + const r = await fetch('/sessions', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(params), }) + if (!r.ok) return null + const data = await r.json() + return data.id ?? null } -// Fetch models grouped by backend. Persisted in localStorage. +export async function killSession(id: string): Promise { + await fetch(`/sessions/${id}`, { method: 'DELETE' }) +} + +export async function renameSession(id: string, newName: string): Promise { + await fetch(`/sessions/${id}`, { + method: 'PATCH', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ name: newName }), + }) +} + +export async function setConfig(id: string, key: string, value: string): Promise { + await fetch(`/sessions/${id}/config`, { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ key, value }), + }) +} + +export async function getState(id: string): Promise { + const r = await fetch(`/sessions/${id}/state`) + if (!r.ok) return 'unknown' + const data = await r.json() + return data.state ?? 'unknown' +} + +export async function getBackends(): Promise { + const r = await fetch('/backends') + if (!r.ok) return [] + const data = await r.json() + return Array.isArray(data) ? (data as string[]) : [] +} + +export async function getAgents(): Promise { + const r = await fetch('/agents') + if (!r.ok) return [] + const data = await r.json() + return Array.isArray(data) ? (data as string[]) : [] +} + +export async function getModels(sessionId: string): Promise { + const r = await fetch(`/sessions/${sessionId}/models`) + if (!r.ok) return [] + const data = await r.json() + return Array.isArray(data) ? (data as string[]) : [] +} + +export async function getUsage(id: string): Promise { + const r = await fetch(`/sessions/${id}/usage`) + if (!r.ok) return '' + const data = await r.json() + return data.usage ?? '' +} + +export async function getCost(id: string): Promise { + const r = await fetch(`/sessions/${id}/cost`) + if (!r.ok) return '' + const data = await r.json() + return data.cost ?? '' +} + +// --- Compat: sendCtl maps to interrupt for "stop" --- + +export async function sendCtl(id: string, ctl: string): Promise { + if (ctl === 'stop') { + await interruptSession(id) + } +} + +// --- Models cache (for NewSession) --- + const MODELS_CACHE_KEY = 'ollie:models' export function loadModelsCache(): Record { @@ -74,57 +163,17 @@ export function loadModelsCache(): Record { } export async function fetchModels(): Promise> { - const r = await fetch('/models') - if (!r.ok) return loadModelsCache() - const data: {backend: string, models: string[]}[] = await r.json() - const out: Record = {} - for (const entry of data) { - out[entry.backend] = entry.models - } - localStorage.setItem(MODELS_CACHE_KEY, JSON.stringify(out)) - return out -} -// Kick off fetch in background to refresh cache. -fetchModels() - -export async function getBackends(): Promise { - const r = await fetch('/backends') - if (!r.ok) return [] - const data = await r.json() - return Array.isArray(data) ? (data as string[]) : [] + // In dbus mode we don't have a global models endpoint. + // Return cached data; individual session model lists are fetched via getModels(). + return loadModelsCache() } -export async function getAgents(): Promise { - const r = await fetch('/a/') - if (!r.ok) return [] - const data = await r.json() - if (!Array.isArray(data)) return [] - return data - .filter((e: any) => e.name?.endsWith('.json')) - .map((e: any) => e.name.replace(/\.json$/, '')) +// --- 9P browsing stubs (not available in dbus mode) --- + +export async function browse9p(_path: string): Promise { + return null } -export async function killSession(id: string): Promise { - await fetch(`/s/${id}`, { method: 'DELETE' }) -} - -export async function browse9p(path: string): Promise { - const r = await fetch(path) - if (!r.ok) return null - const ct = r.headers.get('content-type') ?? '' - if (ct.includes('text/plain')) return await r.text() - return await r.json() -} - -export async function write9p(path: string, content: string): Promise { - const r = await fetch(path, { method: 'PUT', body: content }) - return r.ok -} - -export async function renameSession(id: string, newName: string): Promise { - await fetch(`/s/${id}`, { - method: 'PATCH', - headers: { 'Content-Type': 'application/json' }, - body: JSON.stringify({ name: newName }), - }) +export async function write9p(_path: string, _content: string): Promise { + return false } diff --git a/vite.config.ts b/vite.config.ts index 654704e..5161a44 100644 --- a/vite.config.ts +++ b/vite.config.ts @@ -6,15 +6,10 @@ export default defineConfig({ base: './', server: { proxy: { - '/s/': 'http://localhost:8080', - '/a/': 'http://localhost:8080', - '/m/': 'http://localhost:8080', - '/t/': 'http://localhost:8080', - '/sk/': 'http://localhost:8080', - '/backends': 'http://localhost:8080', - '/models': 'http://localhost:8080', - '/open': 'http://localhost:8080', - '/openapi.json': 'http://localhost:8080', + '/sessions': 'http://localhost:8011', + '/backends': 'http://localhost:8011', + '/agents': 'http://localhost:8011', + '/openapi.json': 'http://localhost:8011', }, }, })