import { Box, Text, useInput } from '@hermes/ink' import { useEffect, useState } from 'react' import type { GatewayClient } from '../gatewayClient.js' import { asRpcResult } from '../lib/rpc.js' import type { Theme } from '../theme.js' import { TextInput } from './textInput.js' export function rosterViewport(height: number, count: number, cursor: number) { const timelineRows = height >= 32 ? Math.min(4, count) : 0 const rows = Math.max(1, height - 7 - (timelineRows ? timelineRows + 4 : 0)) const start = Math.max(0, Math.min(Math.max(0, count - rows), cursor - Math.floor(rows / 2))) return { rows, start, timelineRows } } export async function sendAgentSteer(gw: GatewayClient, sid: string, id: string, text: string) { const result = asRpcResult<{ status: string }>( await gw.request('subagent.steer', { session_id: sid, subagent_id: id, text }) ) const accepted = result?.status === 'queued' return { accepted, message: accepted ? 'Queued for child — applied at the next tool boundary.' : 'Not queued: child has finished or is no longer accepting guidance.' } } interface ControlProps { gw: GatewayClient sid: string id: string t: Theme } export function AgentSteerForm({ gw, sid, id, t, cols, onClose }: ControlProps & { cols: number; onClose: () => void }) { const [text, setText] = useState('') const [feedback, setFeedback] = useState('') const [pending, setPending] = useState(false) useInput((_ch, key) => { if (key.escape && !pending) { onClose() } }) const submit = async () => { if (!text.trim() || pending) { return } setPending(true) try { const result = await sendAgentSteer(gw, sid, id, text) setFeedback(result.message) if (result.accepted) { setText('') } } catch (error) { setFeedback(`Not queued: ${error instanceof Error ? error.message : String(error)}`) } finally { setPending(false) } } return ( Steer {id} Guidance queues at the next tool boundary; current work is not interrupted. ❯ void submit()} value={text} /> {pending ? 'Queueing…' : feedback} Enter queue · Esc back · main composer draft is preserved ) } export function AgentLiveTail({ gw, sid, id, t }: ControlProps) { const [tail, setTail] = useState('Loading live transcript…') useEffect(() => { let active = true let pending = false const refresh = async () => { if (pending) { return } pending = true try { const result = asRpcResult<{ available: boolean; text: string; truncated: boolean }>( await gw.request('subagent.tail', { session_id: sid, subagent_id: id }) ) if (active) { setTail( result?.available ? `${result.truncated ? '[last 16 KiB]\n' : ''}${result.text}` : 'Live transcript unavailable; child may have finished. Progress and output remain below.' ) } } catch { if (active) { setTail('Could not refresh live transcript.') } } finally { pending = false } } void refresh() const timer = setInterval(() => void refresh(), 1500) return () => { active = false clearInterval(timer) } }, [gw, sid, id]) return ( Live transcript {tail} ) }