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

@@ -8,6 +8,7 @@ import android.content.Context
import android.content.Intent
import android.content.pm.PackageManager
import android.content.pm.ServiceInfo
import android.net.Uri
import android.os.Build
import android.os.IBinder
import androidx.core.app.NotificationChannelCompat
@@ -149,9 +150,7 @@ class NotificationService : Service() {
PendingIntent.getActivity(
this,
0,
Intent(this, MainActivity::class.java)
.setAction(Intent.ACTION_MAIN)
.addCategory(Intent.CATEGORY_LAUNCHER),
sessionIntent(this, notification.sessionId),
PendingIntent.FLAG_UPDATE_CURRENT or PendingIntent.FLAG_IMMUTABLE,
)
val built =
@@ -253,6 +252,31 @@ class NotificationService : Service() {
}
}
/**
* The intent that opens one session, and the id it carries back out.
*
* The two halves are written together so neither can be changed without the other, and the scheme
* is enrollment's `aiapp://` under a different host so that [MainActivity] has one thing to look at
* when an intent arrives rather than two.
*
* The id rides in the intent's **data** rather than in an extra, which is not a style choice:
* PendingIntent identity is `Intent.filterEquals`, and that compares the data while ignoring
* extras. Carried as an extra, every session's notification would update one shared PendingIntent
* and every tap would open whichever session was notified last.
*/
fun sessionIntent(context: Context, sessionId: String): Intent =
Intent(context, MainActivity::class.java)
.setAction(Intent.ACTION_VIEW)
.setData(
// Built rather than concatenated so an id needing escaping survives the round trip;
// lastPathSegment below decodes what appendPath encoded.
Uri.Builder().scheme("aiapp").authority("session").appendPath(sessionId).build()
)
/** The session [sessionIntent] named, or null for any other URI -- enrollment's included. */
fun notifiedSessionId(uri: Uri): String? =
if (uri.scheme == "aiapp" && uri.host == "session") uri.lastPathSegment else null
/** One frame of `GET /notifications`. */
data class SessionNotification(
val sessionId: String,