perf(tui): async session.create — sid live in ~250ms instead of ~1350ms

Previously `session.create` blocked for ~1.2s on `_make_agent` (mostly
`run_agent` transitive imports + AIAgent constructor). The UI waited
through that whole window before sid became known and the banner/panel
could render.

Now `session.create` returns immediately with `{session_id, info:
{model, cwd, tools:{}, skills:{}}}` and spawns a background thread that
does the real `_make_agent` + `_init_session`. When the agent is live,
the thread emits `session.info` with the full payload.

Python side:
- `_sessions[sid]` gets a placeholder dict with `agent=None` and a
  `threading.Event()` named `agent_ready`
- `_wait_agent(session, rid, timeout=30)` blocks until the event is set
  (no-op when already set or absent, e.g. for `session.resume`)
- `_sess()` now calls `_wait_agent` — so every handler routed through it
  (prompt.submit, session.usage, session.compress, session.branch,
  rollback.*, tools.configure, etc.) automatically holds until the agent
  is live, but only during the ~1s startup window
- `terminal.resize` and `input.detect_drop` bypass the wait via direct
  dict lookup — they don't touch the agent and would otherwise block
  the first post-startup RPCs unnecessarily

TS side:
- `session.info` event handler now patches the intro message's `info`
  in-place so the seeded banner upgrades to the full session panel when
  the agent finishes initializing
- `appLayout` gates `SessionPanel` on `info.version` being present
  (only set by `_session_info(agent)`, not by the partial payload from
  `session.create`) — so the panel only appears when real data arrives

Net effect on cold start:
  T=~400ms  banner paints (seeded intro)
  T=~245ms  ui.sid set (session.create responds in ~1ms after ready)
  T=~1400ms session panel fills in (real session.info event)

Pre-session keystrokes queue as before (already handled by the flush
effect); `prompt.submit` will wait on `agent_ready` on the Python side
when the flush tries to send before the agent is live.
This commit is contained in:
Brooklyn Nicholson 2026-04-16 15:39:19 -05:00
parent 842a122964
commit a8e0a1148f
3 changed files with 120 additions and 24 deletions

View file

@ -167,6 +167,10 @@ export function createGatewayEventHandler(ctx: GatewayEventHandlerContext): (ev:
info: ev.payload,
usage: ev.payload.usage ? { ...state.usage, ...ev.payload.usage } : state.usage
}))
// Agent init is async in session.create, so the intro message may
// have been seeded with partial info (just model/cwd). Upgrade it
// in-place when the real session.info lands.
setHistoryItems(prev => prev.map(m => (m.kind === 'intro' ? { ...m, info: ev.payload } : m)))
return
case 'thinking.delta': {