Open the session a notification is about, and say the two dividers plainly

Tapping a notification landed on whatever the app was last showing. It now
opens the session it named. The id rides in the intent's data rather than an
extra, because PendingIntent identity is Intent.filterEquals -- with an extra
every session's notification would share one PendingIntent and every tap would
open whichever session was notified last. MainActivity sorts the aiapp:// URI
by host, so enrollment and this are one entry point rather than two.

The notification carries only an id, so the session is fetched before there is
a screen; a fetch that fails says so and offers to try again, since somebody
deliberately tapped and an app that opens to the list explains nothing.

That made session-to-session navigation reachable for the first time, and it
crashed: SessionScreen remembers a transcript and an event stream, and without
a key Compose kept both across the change and merged two conversations into
duplicate list keys. Keyed on the session id.

The two transcript dividers now say only what they are, centred between two
rules: "Compacted <bullet> 128,402 -> 9,617 tok" in blue, and "Context cleared"
in red. The rules stay the ordinary divider colour -- they are framing, and the
words are what carries the meaning.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
irisandClaude Opus 5 committed 2026-08-30 01:23:37 -04:00
1 parent 03a8d7d3d3
commit ea2da0896d
7 files changed
+192 -65

No files matched your search

@@ -184,13 +184,6 @@ sealed class TranscriptItem {
/** Placeholder row for events this build can't render (newer kinds). */
data class Note(override val seq: Long, val text: String) : TranscriptItem()
/**
* A compaction that happened, and what it recovered.
*
* In the transcript rather than only in the status line, because the status is gone the moment
* it finishes and this is the part worth keeping: it is the explanation for a gap in the
* conversation, and for a minute or two in which the session was busy with nothing to show.
*/
/**
* A clear that happened: everything above it left the session's context and stayed on screen.
*
@@ -198,11 +191,21 @@ sealed class TranscriptItem {
*/
data class ClearedNote(override val seq: Long) : TranscriptItem()
/**
* A compaction that happened, and what it recovered.
*
* In the transcript rather than only in the status line, because the status is gone the moment
* it finishes and this is the part worth keeping: it is the explanation for a gap in the
* conversation, and for a minute or two in which the session was busy with nothing to show.
*
* The wire also says what triggered it, and this deliberately does not carry that: the row says
* the two sizes and nothing else (see [compactionSummary]), so keeping the trigger here would
* be a field nothing can read.
*/
data class CompactedNote(
override val seq: Long,
val preTokens: Long?,
val postTokens: Long?,
val trigger: String?,
) : TranscriptItem()
}
@@ -430,13 +433,7 @@ fun foldEvent(items: List<TranscriptItem>, entry: SeqEvent): List<TranscriptItem
}
is SessionEvent.Cleared -> items + TranscriptItem.ClearedNote(entry.seq)
is SessionEvent.Compacted ->
items +
TranscriptItem.CompactedNote(
entry.seq,
event.preTokens,
event.postTokens,
event.trigger,
)
items + TranscriptItem.CompactedNote(entry.seq, event.preTokens, event.postTokens)
is SessionEvent.Unknown -> items + TranscriptItem.Note(entry.seq, "[${event.type}]")
// Screen-level state, not transcript rows -- see SessionScreen.
is SessionEvent.UsageDelta -> items