| 81 | * token. Streamdown parses for the streaming case, and its code plugin |
| 82 | * highlights with the same two Shiki themes the app already switches |
| 83 | * between; `cjk` keeps Chinese from being broken mid-word. |
| 84 | */ |
| 85 | const PLUGINS = { code, cjk }; |
| 86 | |
| 87 | // Turns that produced nothing (a failed stream, or history saved before the |
| 88 | // empty-reply guard) would otherwise render as a blank bubble with action |
| 89 | // buttons under it. |
| 90 | const visible = messages.filter( |
| 91 | (m) => m.role === 'user' || m.content?.trim() |
| 92 | ); |
| 93 | |
| 94 | /** |
| 95 | * The bubble still being written: the last one, from the agent, while a turn |
| 96 | * is in flight. `loadingSubmit` stays true for the whole turn — it is |
| 97 | * cleared in the request's `finally` — so it covers the streaming window and |
| 98 | * not just the wait before the first token. |
| 99 | */ |
| 100 | /** |
| 101 | * The gap between pressing send and the first thing the agent says. |
| 102 | * |
| 103 | * Once its bubble exists it carries its own progress — the notes open with |
| 104 | * a pulse, the text types — so leaving this up as well put the same fact on |
| 105 | * screen three times, the last of them an empty message. |
| 106 | */ |
| 107 | const waiting = |