The agents dashboard (spec §2b; Ink agentsOverlay) — the last first-class
interactive surface. Subagent delegations are tracked from the `subagent.*`
event stream and shown in a full-height overlay.
- store: subagents[] built from subagent.{spawn_requested,start,thinking,tool,
progress,complete} by subagent_id (status·goal·model·depth·lastTool·summary);
clearTranscript clears them. dashboard flag + openDashboard/closeDashboard.
- view/overlays/agentsDashboard.tsx: full-height overlay (replaces transcript+
composer), depth-indented subagent rows colored by status, scroll via
scrollBy/scrollTo, Esc/q close. Empty state prompts to delegate.
- view/App.tsx: content zone is now a <Switch> — pager / agents dashboard /
(transcript + input zone).
- logic/slash.ts: /agents, /tasks → openDashboard (SlashContext.openDashboard).
Verified: bun run check green (53 tests / 7 files) — subagent reducer + a
dashboard frame test (seeded tree renders, transcript replaced) + /agents
dispatch. LIVE tmux: /agents opened empty; then a REAL delegation spawned a
subagent → /agents showed "⛓ Agents · 1 subagent · ● completed <goal>
(model) ⚡terminal". ALL 7 first-class surfaces are now ✅+tested+smoked
(blocking prompts, pager, session switcher, model picker, skills hub,
completions, agents dashboard). Smoke P5e + matrix updated. Remaining: chrome
(5b), agent-feature polish (5d), launcher (8).
112 lines
4.4 KiB
TypeScript
112 lines
4.4 KiB
TypeScript
/**
|
|
* App — the Solid view shell (spec v4 §2 `view/App.tsx`). Header + a content zone
|
|
* that is either the PAGER overlay (long slash output) or the normal
|
|
* transcript + input zone; the input zone is one of: blocking prompt, session
|
|
* switcher, generic picker (model/skills), or the composer. Fully themed (§7.5).
|
|
*
|
|
* header flexShrink:0 (top chrome line)
|
|
* content flexGrow:1, minHeight:0 — Pager OR (transcript + input zone)
|
|
* transcript flexGrow:1, minHeight:0 (the one <scrollbox>; §8 #2 gotchas)
|
|
* input zone flexShrink:0 (PromptOverlay | SessionSwitcher | Picker | Composer)
|
|
*
|
|
* Overlays REPLACE rather than stack (a `<Switch>`), so the composer remounts +
|
|
* refocuses when an overlay closes; the key that closed an overlay can't leak
|
|
* into it because the close is deferred a tick.
|
|
*/
|
|
import { Match, Switch } from 'solid-js'
|
|
|
|
import type { SessionStore } from '../logic/store.ts'
|
|
import { Composer } from './composer.tsx'
|
|
import { Header } from './header.tsx'
|
|
import { AgentsDashboard } from './overlays/agentsDashboard.tsx'
|
|
import { Pager } from './overlays/pager.tsx'
|
|
import { Picker } from './overlays/picker.tsx'
|
|
import { SessionSwitcher } from './overlays/sessionSwitcher.tsx'
|
|
import { PromptOverlay } from './prompts/promptOverlay.tsx'
|
|
import { Transcript } from './transcript.tsx'
|
|
|
|
export interface AppProps {
|
|
readonly store: SessionStore
|
|
readonly onSubmit?: (text: string) => void
|
|
readonly onType?: (text: string) => void
|
|
readonly onRespond?: (method: string, params: Record<string, unknown>) => void
|
|
readonly onResume?: (sessionId: string) => void
|
|
readonly sessionId?: () => string | undefined
|
|
}
|
|
|
|
const NOOP = () => {}
|
|
const NOOP_RESPOND = () => {}
|
|
const NOOP_RESUME = () => {}
|
|
const NO_SESSION = () => undefined
|
|
|
|
export function App(props: AppProps) {
|
|
const blocked = () => props.store.state.prompt !== undefined
|
|
const pager = () => props.store.state.pager
|
|
const dashboard = () => props.store.state.dashboard
|
|
const switcher = () => props.store.state.switcher
|
|
const picker = () => props.store.state.picker
|
|
// Defer the close so the key that closed an overlay (Esc/q/Enter) can't land in
|
|
// the freshly-remounted composer.
|
|
const closePager = () => setTimeout(() => props.store.closePager(), 0)
|
|
const closeDashboard = () => setTimeout(() => props.store.closeDashboard(), 0)
|
|
const closeSwitcher = () => setTimeout(() => props.store.closeSwitcher(), 0)
|
|
const closePicker = () => setTimeout(() => props.store.closePicker(), 0)
|
|
const resume = (id: string) => {
|
|
;(props.onResume ?? NOOP_RESUME)(id)
|
|
closeSwitcher()
|
|
}
|
|
|
|
return (
|
|
<box style={{ flexDirection: 'column', flexGrow: 1, padding: 1 }}>
|
|
<Header store={props.store} />
|
|
{/* content zone: a full-screen overlay (pager / agents dashboard) OR the transcript + input zone */}
|
|
<Switch
|
|
fallback={
|
|
<>
|
|
<Transcript store={props.store} />
|
|
<Switch
|
|
fallback={
|
|
<Composer
|
|
onSubmit={props.onSubmit ?? NOOP}
|
|
onType={props.onType}
|
|
completions={() => props.store.state.completions ?? []}
|
|
onDismiss={() => props.store.clearCompletions()}
|
|
/>
|
|
}
|
|
>
|
|
<Match when={blocked()}>
|
|
<PromptOverlay
|
|
store={props.store}
|
|
onRespond={props.onRespond ?? NOOP_RESPOND}
|
|
sessionId={props.sessionId ?? NO_SESSION}
|
|
/>
|
|
</Match>
|
|
<Match when={switcher()}>
|
|
{sessions => <SessionSwitcher sessions={sessions()} onPick={resume} onClose={closeSwitcher} />}
|
|
</Match>
|
|
<Match when={picker()}>
|
|
{p => (
|
|
<Picker
|
|
title={p().title}
|
|
items={p().items}
|
|
onPick={value => {
|
|
p().onPick(value)
|
|
closePicker()
|
|
}}
|
|
onClose={closePicker}
|
|
/>
|
|
)}
|
|
</Match>
|
|
</Switch>
|
|
</>
|
|
}
|
|
>
|
|
<Match when={pager()}>{p => <Pager title={p().title} text={p().text} onClose={closePager} />}</Match>
|
|
<Match when={dashboard()}>
|
|
<AgentsDashboard subagents={props.store.state.subagents} onClose={closeDashboard} />
|
|
</Match>
|
|
</Switch>
|
|
</box>
|
|
)
|
|
}
|