A subagent is a second transcript owned by a session, in the same event model, with no process and no controls. The claude translator routes lines carrying parent_tool_use_id to a per-subagent translator and transcript under <session>/subagents/<tool_use_id>; three routes expose the list, a transcript page and the SSE stream. Echo grows /subagent [n] as the rig. On the phone a card with subagents ends in a chevron expander, collapsed by default, opening to outlined subcards styled like dev-updater's components; a subcard opens SessionScreen in read-only form, addressed through TranscriptAddress so paging, cache and stream are shared. Design in SUBAGENTS.md; choices awaiting review in DECISIONS.md. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
45 lines
1.8 KiB
Kotlin
45 lines
1.8 KiB
Kotlin
package com.example.aiapp
|
|
|
|
/**
|
|
* The frame name the server uses to say a cursor was too far behind to continue from. Must match
|
|
* `send_backlog` in the backend's routes.rs.
|
|
*/
|
|
private const val RESET_EVENT = "reset"
|
|
|
|
/**
|
|
* The SSE half of the API: one long-lived GET per open session screen, replaying the transcript
|
|
* after a cursor and then following it live.
|
|
*
|
|
* The connection and its framing belong to [Sse]; what stays here is what this stream's frames
|
|
* mean. [close] from any thread ends it, and the caller owns reconnecting -- with the last seq it
|
|
* saw as the new cursor.
|
|
*/
|
|
class EventStream(settings: ServerSettings, private val address: TranscriptAddress) {
|
|
private val stream = Sse(settings)
|
|
|
|
fun close() = stream.close()
|
|
|
|
/**
|
|
* Streams events after [after] into [onEvent] until the stream drops.
|
|
*
|
|
* [onReset] fires when the server answers that the cursor is too far behind to continue from:
|
|
* everything already displayed is stale and the events that follow are a fresh window, so the
|
|
* caller drops what it holds and rebuilds. It arrives before those events, so a caller that
|
|
* clears on it stays in order.
|
|
*/
|
|
fun run(
|
|
after: Long,
|
|
onOpen: () -> Unit,
|
|
onReset: () -> Unit,
|
|
// The frame's own text as well as the event parsed from it: the transcript cache stores the
|
|
// one and the screen folds the other, and they have to be the same line.
|
|
onEvent: (raw: String, event: SeqEvent) -> Unit,
|
|
) {
|
|
stream.run("/${address.urlPath}/events?after=$after", onOpen) { name, data ->
|
|
// A named frame carries no payload and a data frame has no name.
|
|
if (name == RESET_EVENT) onReset()
|
|
else if (data.isNotEmpty()) onEvent(data, parseSeqEvent(data))
|
|
}
|
|
}
|
|
}
|