Something went wrong. Try again.
small gleam coding and (not yet) persistent agent daemon with a detachable cli
Something went wrong. Try again.
TSX
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198import { useEffect, useState } from "react"import { Box, Text } from "ink"import { ChatScreen } from "./chat.js"import { Picker } from "./picker.js"import { Login } from "./login.js"import { ModelPicker } from "./model-picker.js"import { ExtensionPicker } from "./extension-picker.js"import { TreePicker, type TreeCheckpoint, type TreePage } from "./tree-picker.js"import { ContextInspector } from "./context-inspector.js"import { PageView } from "./page-view.js"import { parsePage, type Glance } from "../page.js"import { request, type Connection, type Session } from "../daemon.js"import { commandMenuItems, parseCommandCatalog, parseCommandInvocation, type SessionCommand } from "../commands.js"
export function App({ connection, initial, workspace, quit, login = false }: { connection: Connection; initial?: Session; workspace: string; quit: () => void; login?: boolean }) { const [loggingIn,setLoggingIn]=useState<{ name?: string } | undefined>(login ? {} : undefined) const [notice,setNotice]=useState("") const [selected,setSelected]=useState(initial) const [sessions,setSessions]=useState<Session[]>([]) const [error,setError]=useState("") const [choosing,setChoosing]=useState(!initial) const [choosingModel,setChoosingModel]=useState(false) const [choosingExtensions,setChoosingExtensions]=useState(false) const [choosingTree,setChoosingTree]=useState(false) const [choosingContext,setChoosingContext]=useState(false) const [treePage,setTreePage]=useState<TreePage>() const [treeError,setTreeError]=useState("") const [treeLoading,setTreeLoading]=useState(false) const [treeCursors,setTreeCursors]=useState([0]) const [treePageIndex,setTreePageIndex]=useState(0) const [treeNextCursor,setTreeNextCursor]=useState<number>() const [extensionRevision,setExtensionRevision]=useState(0) const [commandCatalog,setCommandCatalog]=useState<SessionCommand[]>([]) const [openPage,setOpenPage]=useState<string>() const [glances,setGlances]=useState<Glance[]>([]) const [glanceRevision,setGlanceRevision]=useState(0) const pageCommands = commandCatalog.filter(command => command.page).map(command => command.name).join(" ") const listedSessions = selected && !sessions.some(session => session.id === selected.id) ? [selected, ...sessions] : sessions const commandMenu = [ { name: "/login", description: "add or select a named openai-compatible api" }, { name: "/new", description: "new coding session" }, { name: "/sessions", description: "switch session" }, { name: "/extensions", description: "manage this session's extension plugins" }, { name: "/tree", description: "branch this session from a history checkpoint" }, ...commandMenuItems(commandCatalog), ] const workspaceChanged = (workspace: string): void => { setSelected(current => current && { ...current, workspace }) if (selected) setSessions(current => current.map(session => session.id === selected.id ? { ...session, workspace } : session)) } const applySelection = (changed: Partial<Pick<Session, "model" | "provider" | "protocol">>): void => { if (!selected) return setSelected({ ...selected, model: changed.model ?? selected.model, provider: changed.provider ?? selected.provider, protocol: changed.protocol ?? selected.protocol }) } const runCommand = async (name: string, args: Record<string, string> | string): Promise<Record<string, unknown> | undefined> => { if (!selected) return const call = typeof args === "string" ? { name, arguments: args } : { name, args } const outcome = await request<{ result?: Record<string, unknown>; submitted?: boolean }>(connection, `/sessions/${encodeURIComponent(selected.id)}/commands`, call) return outcome.result } const changeModel = async (model: string, provider?: string): Promise<void> => { if (!selected) return if (provider && provider !== selected.provider) { const health = await request<{ capabilities?: string[] }>(connection, "/health") if (!health.capabilities?.includes("session_provider")) throw new Error("daemon upgrade needed to switch providers; when ready, run albedo daemon --stop, then albedo (this clears python variables)") } applySelection((await runCommand("/model", provider ? { model, provider } : { model }) ?? {}) as Partial<Pick<Session, "model" | "provider" | "protocol">>) setChoosingModel(false); setError("") } useEffect(() => { if (!choosing) return const controller = new AbortController() void request<Session[]>(connection,"/sessions").then(items => { if (!controller.signal.aborted) setSessions(items) }).catch(error => setError(String(error))) return () => controller.abort() },[choosing,connection]) useEffect(() => { if (!selected) { setCommandCatalog([]); return } let active = true void request<{ capabilities?: string[] }>(connection, "/health").then(health => { if (!health.capabilities?.includes("session_commands")) throw new Error("daemon upgrade needed for the command menu; when ready, run albedo daemon --stop, then albedo (this clears python variables)") return request<unknown>(connection, `/sessions/${encodeURIComponent(selected.id)}/commands`) }) .then(value => { if (active) setCommandCatalog(parseCommandCatalog(value)) }) .catch(error => { if (!active) return setCommandCatalog([]) if (String(error).includes("daemon upgrade")) setError(String(error)) }) return () => { active = false } }, [connection, selected?.id, selected?.workspace, extensionRevision]) // Extension pages may offer a glance for the chat's sidebar; poll them while chatting. useEffect(() => { const names = pageCommands ? pageCommands.split(" ") : [] if (!selected || !names.length) { setGlances([]); return } let active = true const refresh = (): void => { void Promise.all(names.map(name => runCommand(name, {}).then(parsePage).catch(() => undefined))) .then(pages => { if (active) setGlances(pages.flatMap(page => page?.glance ? [page.glance] : [])) }) } refresh() const timer = setInterval(refresh, 3_000) return () => { active = false; clearInterval(timer) } }, [connection, selected?.id, pageCommands, glanceRevision]) useEffect(() => { if (!choosingTree || !selected) return let active = true setTreeLoading(true); setTreeError("") const after = treeCursors[treePageIndex] ?? 0 void request<{ capabilities?: string[] }>(connection, "/health").then(health => { if (!health.capabilities?.includes("session_tree")) throw new Error("daemon upgrade needed for /tree; when ready, run albedo daemon --stop, then albedo (this clears python variables)") return request<{ items: TreeCheckpoint[]; nextCursor?: number | null; hasMore: boolean }>( connection, `/sessions/${encodeURIComponent(selected.id)}/tree?after=${after}&limit=50`, ) }).then(page => { if (!active) return setTreePage({ items: page.items, hasPrevious: treePageIndex > 0, hasNext: page.hasMore }) setTreeNextCursor(typeof page.nextCursor === "number" ? page.nextCursor : undefined) }).catch(cause => { if (active) setTreeError(String(cause)) }) .finally(() => { if (active) setTreeLoading(false) }) return () => { active = false } },[choosingTree, selected?.id, connection, treeCursors, treePageIndex]) const openTree = (): void => { setTreePage(undefined); setTreeError(""); setTreeCursors([0]); setTreePageIndex(0); setTreeNextCursor(undefined); setChoosingTree(true) } const nextTreePage = (): void => { if (treeNextCursor === undefined) return setTreeCursors(current => current.slice(0, treePageIndex + 1).concat(treeNextCursor)) setTreePageIndex(index => index + 1) } const forkTree = async (checkpoint: TreeCheckpoint): Promise<void> => { if (!selected) return const branch = await request<Session>(connection, `/sessions/${encodeURIComponent(selected.id)}/fork`, { checkpoint: checkpoint.id }) setSelected(branch); setSessions(current => [branch, ...current]); setChoosingTree(false); setTreePage(undefined) } const create = (): void => { void request<Session>(connection,"/sessions",{ workspace }).then(session => { setSelected(session); setChoosing(false) }).catch(error => setError(String(error))) } if (loggingIn) return <Login connection={connection} name={loggingIn.name} onCancel={()=>setLoggingIn(undefined)} onDone={name=>{ setLoggingIn(undefined); setError("") setNotice(`${name} selected for new sessions${selected ? `; use /model to switch this session from ${selected.provider}` : ""}`) if (!selected && !sessions.length) create() }} /> return <Box flexDirection="column"> {notice && (!selected || choosing) && <Text dimColor>{notice}</Text>} {error && (!selected || choosing) && <Text color="red">{error}</Text>} {choosing && sessions.some(session => typeof session.title !== "string") && <Text color="yellow" wrap="wrap"> daemon upgrade needed for prompt labels and recent ordering. when ready, run albedo daemon --stop, then albedo. restarting clears python variables. </Text>} {choosing ? <Picker search title="albedo sessions" initialSelection={selected?.id ?? listedSessions[0]?.id} items={[ { id:"new",label:"new coding session",detail:workspace }, { id:"login",label:"/login",detail:"add or select an api provider" }, ...listedSessions.map(session => ({ id:session.id,label:typeof session.title === "string" ? session.title.trim() || "new session" : "session · label unavailable",detail:`${session.model} · ${session.workspace}` })), ]} onSelect={id => { if (id==="new") create(); else if (id==="login") setLoggingIn({}); else { setSelected(listedSessions.find(session=>session.id===id));setChoosing(false) } }} onCancel={() => selected ? setChoosing(false) : quit()} /> : selected && <><ChatScreen key={selected.id} visible={!choosingModel && !choosingExtensions && !choosingTree && !choosingContext && !openPage} glances={glances} usageResetKey={extensionRevision} baseUrl={`http://127.0.0.1:${connection.port}`} token={connection.token} agentId={selected.id} agentName="albedo" workspace={selected.workspace} model={selected.model} onWorkspaceChanged={workspaceChanged} onBack={()=>setChoosing(true)} onQuit={quit} onCreate={create} notice={notice} errorNotice={error} commands={commandMenu} onCommand={(value,clear)=> { if (value === "/login" || value.startsWith("/login ")) { clear(); setLoggingIn({ name: value.slice(6).trim() || undefined }); return true } if (value.startsWith("/model ")) { clear(); setError("") void runCommand("/model", value.slice(7)) .then(changed => applySelection((changed ?? {}) as Partial<Pick<Session, "model" | "provider" | "protocol">>)) .catch(error=>setError(String(error))) return true } if (value==="/model") { clear();setError("");setChoosingModel(true);return true } if (["/extensions","/plugins"].includes(value)) { clear();setError("");setChoosingExtensions(true);return true } if (value==="/tree") { clear();setError("");openTree();return true } if (value==="/context") { clear();setError("");setChoosingContext(true);return true } if (["/sessions","/agents","/a"].includes(value)) { clear();setChoosing(true);return true } if (value==="/new") { clear();create();return true } if (commandCatalog.some(command => command.page && command.name === value)) { clear();setError("");setOpenPage(value);return true } const command = parseCommandInvocation(value, commandCatalog) if (command) { clear(); setError(""); setNotice(command.name === "/reload" ? "Reloading…" : "") void runCommand(command.name, command.arguments) .then(result => { if (!result) return setNotice("") setNotice(typeof result.message === "string" ? result.message : JSON.stringify(result)) }) .catch(error=>{ setNotice(""); setError(String(error)) }) return true } return false }} /> {choosingModel && <ModelPicker connection={connection} provider={selected.provider} current={selected.model} onSelect={changeModel} onCancel={()=>setChoosingModel(false)} />} {choosingExtensions && <ExtensionPicker connection={connection} sessionId={selected.id} onChanged={()=>setExtensionRevision(value=>value+1)} onCancel={()=>setChoosingExtensions(false)} />} {choosingTree && <TreePicker page={treePage} loading={treeLoading} error={treeError} onPrevious={()=>setTreePageIndex(index=>Math.max(0,index-1))} onNext={nextTreePage} onFork={forkTree} onCancel={()=>setChoosingTree(false)} />} {openPage && <PageView key={openPage} command={openPage} run={args => runCommand(openPage, args)} onChanged={()=>setGlanceRevision(value=>value+1)} onCancel={()=>setOpenPage(undefined)} />} {choosingContext && <ContextInspector connection={connection} sessionId={selected.id} onCancel={()=>setChoosingContext(false)} />}</>} </Box>}