| 90 | }; |
| 91 | |
| 92 | const handle = async (event: TheaterEvent): Promise<boolean> => { |
| 93 | switch (event.type) { |
| 94 | case "user": { |
| 95 | out(`\n${BOLD}${CYAN}┃ you${RESET} `); |
| 96 | for (const ch of event.text) { |
| 97 | out(ch); |
| 98 | await sleep(18); |
| 99 | } |
| 100 | out("\n"); |
| 101 | return true; |
| 102 | } |
| 103 | case "assistant": { |
| 104 | out(`\n${BOLD}${MAGENTA}● agent${RESET} `); |
| 105 | for (const piece of event.text.match(/.{1,3}/gs) ?? []) { |
| 106 | out(piece); |
| 107 | await sleep(12); |
| 108 | } |
| 109 | out("\n"); |
| 110 | return true; |
| 111 | } |
| 112 | case "tool-start": { |
| 113 | startTool(event.name, event.input); |
| 114 | return true; |
| 115 | } |
| 116 | case "tool-end": { |
| 117 | endTool(event.ok, event.result, event.seconds); |
| 118 | return true; |
| 119 | } |
| 120 | case "status": { |
| 121 | out(`\n ${DIM}· ${event.text}${RESET}\n`); |
| 122 | return true; |
| 123 | } |
| 124 | case "done": { |
| 125 | out(`\n${DIM}${"─".repeat(40)}${RESET}\n${GREEN}✦ session complete${RESET}\n`); |
| 126 | if (eventsFifo) { |
| 127 | // Desk mode: ack completion to the driver, then linger so the |
| 128 | // closing frame stays on camera before the xterm vanishes. |
| 129 | writeFileSync(`${eventsFifo}.done`, ""); |
| 130 | await sleep(4_000); |
| 131 | } |
| 132 | return false; |
| 133 | } |
| 134 | } |
| 135 | }; |
| 136 | |
| 137 | // Events arrive faster than they render; process strictly in order so the |
| 138 | // animations (not arrival time) set the pacing. |