diff --git a/tui_gateway/server.py b/tui_gateway/server.py
index e5b1447d76..2ba156587d 100644
--- a/tui_gateway/server.py
+++ b/tui_gateway/server.py
@@ -1726,6 +1726,17 @@ def _(rid, params: dict) -> dict:
"transport": current_transport() or _stdio_transport,
}
+ # Return the lightweight session immediately so Ink can paint the composer
+ # + skeleton panel, then build the real AIAgent just after this response is
+ # flushed. This keeps startup responsive while still hydrating tools/skills
+ # without requiring the user to submit a first prompt.
+ def _deferred_build() -> None:
+ session = _sessions.get(sid)
+ if session is not None:
+ _start_agent_build(sid, session)
+
+ threading.Timer(0.05, _deferred_build).start()
+
return _ok(
rid,
{
diff --git a/ui-tui/src/components/branding.tsx b/ui-tui/src/components/branding.tsx
index 0a7509f696..84e502aada 100644
--- a/ui-tui/src/components/branding.tsx
+++ b/ui-tui/src/components/branding.tsx
@@ -1,10 +1,32 @@
import { Box, Text, useStdout } from '@hermes/ink'
+import { useEffect, useState } from 'react'
+import unicodeSpinners from 'unicode-animations'
import { artWidth, caduceus, CADUCEUS_WIDTH, logo, LOGO_WIDTH } from '../banner.js'
import { flat } from '../lib/text.js'
import type { Theme } from '../theme.js'
import type { PanelSection, SessionInfo } from '../types.js'
+const LOADER_TICK_MS = 120
+
+function InlineLoader({ label, t }: { label: string; t: Theme }) {
+ const [tick, setTick] = useState(0)
+ const spinner = unicodeSpinners.braille
+ const frame = spinner.frames[tick % spinner.frames.length] ?? '⠋'
+
+ useEffect(() => {
+ const id = setInterval(() => setTick(n => n + 1), Math.max(LOADER_TICK_MS, spinner.interval))
+
+ return () => clearInterval(id)
+ }, [spinner.interval])
+
+ return (
+
+ {frame} {label}
+
+ )
+}
+
export function ArtLines({ lines }: { lines: [string, string][] }) {
return (
<>
@@ -64,7 +86,6 @@ export function SessionPanel({ info, sid, t }: SessionPanelProps) {
}
const section = (title: string, data: Record, max = 8, overflowLabel = 'more…') => {
- const skeletonRows = title === 'Tools' ? ['browser', 'terminal', 'file'] : ['apple', 'creative', 'software-development']
const entries = Object.entries(data).sort()
const shown = entries.slice(0, max)
const overflow = entries.length - max
@@ -76,19 +97,16 @@ export function SessionPanel({ info, sid, t }: SessionPanelProps) {
Available {title}
- {skeleton
- ? skeletonRows.map(k => (
-
- {k}:
- ━━━━━━━━━━━━━━
-
- ))
- : shown.map(([k, vs]) => (
-
- {strip(k)}:
- {truncLine(strip(k) + ': ', vs)}
-
- ))}
+ {skeleton ? (
+
+ ) : (
+ shown.map(([k, vs]) => (
+
+ {strip(k)}:
+ {truncLine(strip(k) + ': ', vs)}
+
+ ))
+ )}
{overflow > 0 && (