update webui: chat pane and api changes

This commit is contained in:
Levi Neely 2026-07-06 18:22:59 +02:00
parent 0f97e50a49
commit 673cf00bab
4 changed files with 132 additions and 88 deletions

View File

@ -82,7 +82,7 @@ export function App() {
getChatLines(selectedId), getChatLines(selectedId),
]) ])
if (!alive) return if (!alive) return
setCfg(c) setCfg(c as Record<string, string>)
setMessages(parseChat(lines)) setMessages(parseChat(lines))
} catch { } catch {
// session may have been removed // session may have been removed

View File

@ -205,7 +205,7 @@ export function ChatPane({ session, cfg, messages, onSend, onStop, browsePath, b
const renderedHtml = useMemo(() => const renderedHtml = useMemo(() =>
messages.map(msg => messages.map(msg =>
(msg.role === 'user' || msg.role === 'assistant') (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] [messages, session.cwd]
) )

View File

@ -1,68 +1,157 @@
export interface IdxEntry { export interface SessionSummary {
id: string id: string
state: string state: string
cwd: string
backend: string
model: 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<IdxEntry[]> { // Compat alias for components still using the old name.
const r = await fetch('/s/idx') 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<SessionSummary[]> {
const r = await fetch('/sessions')
if (!r.ok) return [] if (!r.ok) return []
const data = await r.json() 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<Record<string, string>> { export async function getSessionCfg(id: string): Promise<SessionConfig> {
const r = await fetch(`/s/${id}/cfg`) const r = await fetch(`/sessions/${id}/config`)
if (!r.ok) throw new Error(`cfg ${r.status}`) if (!r.ok) throw new Error(`config ${r.status}`)
const data = await r.json() const data = await r.json()
return (data && typeof data === 'object' && !Array.isArray(data)) return (data && typeof data === 'object' && !Array.isArray(data))
? (data as Record<string, string>) ? (data as SessionConfig)
: {} : {}
} }
// The gateway returns chat as plain text (one line per key=value pair).
export async function getChatLines(id: string): Promise<string[]> { export async function getChatLines(id: string): Promise<string[]> {
const r = await fetch(`/s/${id}/chat`) const r = await fetch(`/sessions/${id}/chat`)
if (!r.ok) return [] if (!r.ok) return []
const text = await r.text() const text = await r.text()
if (!text.trim()) return [] if (!text.trim()) return []
return text.trimEnd().split('\n') 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<void> { export async function sendPrompt(id: string, prompt: string): Promise<void> {
await fetch(`/s/${id}/prompt`, { await fetch(`/sessions/${id}/submit`, {
method: 'POST', method: 'POST',
headers: { 'Content-Type': 'application/json' }, headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ prompt }), body: JSON.stringify({ prompt }),
}) })
} }
export async function sendCtl(id: string, ctl: string): Promise<void> { export async function interruptSession(id: string): Promise<void> {
await fetch(`/s/${id}/ctl`, { await fetch(`/sessions/${id}/interrupt`, { method: 'POST' })
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ ctl }),
})
} }
export async function createSession(params: { export async function createSession(params: {
name?: string cwd?: string
cwd: string backend?: string
backend: string model?: string
model: string agent?: string
agent: string }): Promise<string | null> {
[key: string]: string | undefined const r = await fetch('/sessions', {
}): Promise<void> {
await fetch('/s/new', {
method: 'POST', method: 'POST',
headers: { 'Content-Type': 'application/json' }, headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(params), 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<void> {
await fetch(`/sessions/${id}`, { method: 'DELETE' })
}
export async function renameSession(id: string, newName: string): Promise<void> {
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<void> {
await fetch(`/sessions/${id}/config`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ key, value }),
})
}
export async function getState(id: string): Promise<string> {
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<string[]> {
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<string[]> {
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<string[]> {
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<string> {
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<string> {
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<void> {
if (ctl === 'stop') {
await interruptSession(id)
}
}
// --- Models cache (for NewSession) ---
const MODELS_CACHE_KEY = 'ollie:models' const MODELS_CACHE_KEY = 'ollie:models'
export function loadModelsCache(): Record<string, string[]> { export function loadModelsCache(): Record<string, string[]> {
@ -74,57 +163,17 @@ export function loadModelsCache(): Record<string, string[]> {
} }
export async function fetchModels(): Promise<Record<string, string[]>> { export async function fetchModels(): Promise<Record<string, string[]>> {
const r = await fetch('/models') // In dbus mode we don't have a global models endpoint.
if (!r.ok) return loadModelsCache() // Return cached data; individual session model lists are fetched via getModels().
const data: {backend: string, models: string[]}[] = await r.json() return loadModelsCache()
const out: Record<string, string[]> = {}
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<string[]> {
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<string[]> { // --- 9P browsing stubs (not available in dbus mode) ---
const r = await fetch('/a/')
if (!r.ok) return [] export async function browse9p(_path: string): Promise<any> {
const data = await r.json() return null
if (!Array.isArray(data)) return []
return data
.filter((e: any) => e.name?.endsWith('.json'))
.map((e: any) => e.name.replace(/\.json$/, ''))
} }
export async function killSession(id: string): Promise<void> { export async function write9p(_path: string, _content: string): Promise<boolean> {
await fetch(`/s/${id}`, { method: 'DELETE' }) return false
}
export async function browse9p(path: string): Promise<any> {
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<boolean> {
const r = await fetch(path, { method: 'PUT', body: content })
return r.ok
}
export async function renameSession(id: string, newName: string): Promise<void> {
await fetch(`/s/${id}`, {
method: 'PATCH',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ name: newName }),
})
} }

View File

@ -6,15 +6,10 @@ export default defineConfig({
base: './', base: './',
server: { server: {
proxy: { proxy: {
'/s/': 'http://localhost:8080', '/sessions': 'http://localhost:8011',
'/a/': 'http://localhost:8080', '/backends': 'http://localhost:8011',
'/m/': 'http://localhost:8080', '/agents': 'http://localhost:8011',
'/t/': 'http://localhost:8080', '/openapi.json': 'http://localhost:8011',
'/sk/': 'http://localhost:8080',
'/backends': 'http://localhost:8080',
'/models': 'http://localhost:8080',
'/open': 'http://localhost:8080',
'/openapi.json': 'http://localhost:8080',
}, },
}, },
}) })