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),
])
if (!alive) return
setCfg(c)
setCfg(c as Record<string, string>)
setMessages(parseChat(lines))
} catch {
// session may have been removed

View File

@ -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]
)

View File

@ -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<IdxEntry[]> {
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<SessionSummary[]> {
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<Record<string, string>> {
const r = await fetch(`/s/${id}/cfg`)
if (!r.ok) throw new Error(`cfg ${r.status}`)
export async function getSessionCfg(id: string): Promise<SessionConfig> {
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<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[]> {
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<void> {
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<void> {
await fetch(`/s/${id}/ctl`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ ctl }),
})
export async function interruptSession(id: string): Promise<void> {
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<void> {
await fetch('/s/new', {
cwd?: string
backend?: string
model?: string
agent?: string
}): Promise<string | null> {
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<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'
export function loadModelsCache(): Record<string, string[]> {
@ -74,57 +163,17 @@ export function loadModelsCache(): Record<string, string[]> {
}
export async function fetchModels(): Promise<Record<string, string[]>> {
const r = await fetch('/models')
if (!r.ok) return loadModelsCache()
const data: {backend: string, models: string[]}[] = await r.json()
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[]) : []
// 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<string[]> {
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<any> {
return null
}
export async function killSession(id: string): Promise<void> {
await fetch(`/s/${id}`, { method: 'DELETE' })
}
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 }),
})
export async function write9p(_path: string, _content: string): Promise<boolean> {
return false
}

View File

@ -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',
},
},
})