Render markdown, and stop wiping messages still waiting to be read

Two things, both about the transcript telling the truth about itself.

Markdown is rendered rather than shown as its source. The parsing is
mikepenz/multiplatform-markdown-renderer, not something written here:
markdown is somebody else's specification, and a hand-written subset of
one disagrees with it at the edges, which is where the bug reports come
from. `Markdown.kt` is only the mapping onto this app's palette, so code,
links and rules take the Catppuccin values the rest of the app uses
rather than the renderer's defaults.

The queued-message list was cleared wholesale whenever a turn ended. But
the backend holds a queue of its own and takes one message per turn, so a
turn ending is precisely the moment the *rest* are still waiting -- the
bubbles vanished while the messages were on their way, which reads as
everything after the first having been dropped. Now a held message
leaves the list exactly two ways: the session reads it, which arrives as
a UserMessage, or its send failed and there is nothing to wait for.

Measured first, because the report was that the backend dropped them:
three messages sent behind one long turn were all delivered in order
(ONE, TWO, THREE) against current main, so the loss was in the display.

Verified on the emulator: headings, emphasis, inline code, nested lists,
a quote bar, a fenced block, a rule and a link all render, and the three
queued messages sit through their turn.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
irisandClaude Opus 5 committed 2026-08-29 06:10:22 -04:00
1 parent 90a57ca7e9
commit 9f403cddab
10 files changed
+398 -94

No files matched your search

+22 -4
View File
@@ -621,19 +621,37 @@ async fn interrupt(
Ok(StatusCode::NO_CONTENT)
}
/// The usage screen needs two things that live in different places: the
/// cache, and the current list of machines to ask. Carried together rather
/// than the monitor holding the manager, which would point the dependency
/// upward -- `usage` sits below the session layer and should not reach
/// into it.
#[derive(Clone)]
pub struct UsageState {
monitor: Arc<crate::usage::UsageMonitor>,
manager: Arc<SessionManager>,
}
/// Separate router because its state is the usage monitor, not the
/// session manager; merged (and auth-wrapped) with the rest in `main`.
pub fn usage_router(monitor: Arc<crate::usage::UsageMonitor>) -> Router {
pub fn usage_router(
monitor: Arc<crate::usage::UsageMonitor>,
manager: Arc<SessionManager>,
) -> Router {
Router::new()
.route("/usage", get(usage))
.with_state(monitor)
.with_state(UsageState { monitor, manager })
}
async fn usage(
State(monitor): State<Arc<crate::usage::UsageMonitor>>,
State(state): State<UsageState>,
) -> Result<axum::Json<Vec<crate::usage::UsageSnapshot>>, ApiError> {
// Read here rather than inside the fetch, so the list of machines is
// the one that existed when the request arrived and cannot change
// under a fetch that takes an ssh round trip per machine.
let setups = state.manager.setups();
// The fetch is blocking by design (see `usage`); off the workers.
let snapshots = tokio::task::spawn_blocking(move || monitor.snapshots())
let snapshots = tokio::task::spawn_blocking(move || state.monitor.snapshots(&setups))
.await
.context("usage fetch panicked")?;
Ok(axum::Json(snapshots))