Merge remote-tracking branch 'origin/main'

# Conflicts:
#	app/androidApp/src/main/kotlin/com/example/aiapp/Sizes.kt
This commit is contained in:
iris committed 2026-09-04 15:03:37 -04:00
commit e3e02d55f7
29 files changed
+2737 -479

No files matched your search

@@ -235,6 +235,8 @@ fun SessionScreen(
settings: ServerSettings,
summary: SessionSummary,
onBack: () -> Unit,
/** Opens the file explorer on this session's machine, starting where this session works. */
onFiles: (FilesTarget) -> Unit,
/** What another app shared in while this session is the one open; see [ShareRequest]. */
share: ShareRequest? = null,
/** Said once [share] has been attached here, so it is not attached again. */
@@ -1347,6 +1349,60 @@ fun SessionScreen(
// interpolated, so there is nothing for a dropped frame to interrupt), so it is what both
// places below fall back to.
val imeVisible = WindowInsets.isImeVisible
// What this session is costing to draw, copied out to somewhere it can be read.
//
// Written here rather than beside the control that runs it, because everything it measures --
// the events, the rows, the units, what the list has on screen, which cards are open -- is this
// composable's own state, and a control in a dialog cannot reach it. The control is a row in
// [SessionSettingsDialog]: that is where the session's other about-the-session controls are,
// and the header is for what a reader presses while reading. It copies rather than opens,
// because what it produces is for somewhere else -- a message to whoever is looking at the
// code -- and a screenful of timings read on the phone is a screenful nobody can act on.
//
// Whatever presses this, it is found by its **name**: `ui-trace`'s tap-by-label action resolves
// "Session settings" and then "Copy render timings" from what is on screen at that moment, so
// `transcript-bench.sh` and `stream-bench.sh` keep working when this moves again. They pressed
// it at a coordinate measured once by hand until 2026-09-03, and anything that moved the header
// made that tap land on whatever now sat there -- reporting a number that was never measured.
val copyRenderReport = {
val report =
debugReport(
device =
"device: ${Build.MODEL} (${Build.MANUFACTURER})," +
" Android ${Build.VERSION.RELEASE}\n" +
// A debuggable build runs Compose at a fraction of release speed, so a
// report that did not say which it came from was read as the app's own
// cost.
"build: ${if (debuggable(context)) "debug" else "release"}",
transcript =
listOf(
" ${items.size} events, ${rows.size} rows, ${units.size} units loaded",
" viewport ${listState.layoutInfo.viewportSize.height}px," +
" ${listState.layoutInfo.visibleItemsInfo.size} units visible",
visibleUnits(units, listState.layoutInfo.visibleItemsInfo, UNITS_START),
" ${expandedTools.size} tool calls and ${expandedGroups.size} groups open",
),
frames = FrameStats.lines(context.refreshHz()),
accounting =
FrameStats.drawPhase().let { (nanos, count) -> drawAccounting(nanos, count) },
crash = lastCrash(context),
)
context.copyToClipboard("ai-app render report", report)
// Also to the log, so a session driving the app over adb can read the same report the
// button copies. The clipboard is not reachable from a shell, and a counter nobody can
// check from here is a counter that only gets checked by asking Iris to press a button
// and paste.
Log.i("ai-app", report)
// Only once it is somewhere it can be read from, so a copy that never happened does not
// throw the stack away with it.
clearCrash(context)
// Emptied by the copy, so pressing it twice measures two separate stretches of scrolling
// rather than one and then the same one again.
FrameStats.reset()
DebugStats.reset()
Toast.makeText(context, "Copied render report", Toast.LENGTH_SHORT).show()
}
Box(Modifier.fillMaxSize()) {
Column(Modifier.fillMaxSize()) {
Row(
@@ -1388,74 +1444,36 @@ fun SessionScreen(
// yellow or red near a limit -- and the theme's plain control colour whenever there
// is no measurement, since blue is the low end of the scale here and would read as
// "checked, and fine" about a machine nobody could reach.
// Usage, files, settings -- widest scope first, narrowing to the right, so the
// cog stays at the end where every other screen keeps it. Asked for in this order
// by Iris on 2026-09-03.
Row {
// Left of the numbers about the *conversation*, because it is the same kind of
// thing about the *app*: what this session is costing to draw. It copies rather
// than opens, because what it produces is for somewhere else -- a message to
// whoever is looking at the code -- and a screenful of timings read on the
// phone
// is a screenful nobody can act on.
GlyphButton(
SPEED_GLYPH,
"Copy render timings",
onClick = {
val report =
debugReport(
device =
"device: ${Build.MODEL} (${Build.MANUFACTURER})," +
" Android ${Build.VERSION.RELEASE}\n" +
// A debuggable build runs Compose at a fraction of
// release speed, so a report that did not say which
// it came from was read as the app's own cost.
"build: ${if (debuggable(context)) "debug" else "release"}",
transcript =
listOf(
" ${items.size} events, ${rows.size} rows," +
" ${units.size} units loaded",
" viewport" +
" ${listState.layoutInfo.viewportSize.height}px," +
" ${listState.layoutInfo.visibleItemsInfo.size}" +
" units visible",
visibleUnits(
units,
listState.layoutInfo.visibleItemsInfo,
UNITS_START,
),
" ${expandedTools.size} tool calls and" +
" ${expandedGroups.size} groups open",
),
frames = FrameStats.lines(context.refreshHz()),
accounting =
FrameStats.drawPhase().let { (nanos, count) ->
drawAccounting(nanos, count)
},
crash = lastCrash(context),
)
context.copyToClipboard("ai-app render report", report)
// Also to the log, so a session driving the app over adb can read the
// same report the button copies. The clipboard is not reachable from a
// shell, and a counter nobody can check from here is a counter that
// only
// gets checked by asking Iris to press a button and paste.
Log.i("ai-app", report)
// Only once it is somewhere it can be read from, so a copy that never
// happened does not throw the stack away with it.
clearCrash(context)
// Emptied by the copy, so pressing it twice measures two separate
// stretches
// of scrolling rather than one and then the same one again.
FrameStats.reset()
DebugStats.reset()
Toast.makeText(context, "Copied render report", Toast.LENGTH_SHORT)
.show()
},
)
GlyphButton(
USAGE_GLYPH,
"Usage",
{ usageOpen = true },
colour = usageGlyphColour(usage),
)
// The machine's files, which is where the answer to "what did it actually
// change" is. It opens *over* this screen rather than replacing it -- see
// [Screen.Session].
GlyphButton(
FOLDER_GLYPH,
"Files",
onClick = {
onFiles(
FilesTarget(
setup = summary.setup,
setupName = summary.setupName,
// Where this session works, and the machine's own home when it
// was never given a directory -- resolved there rather than
// guessed at here, since this app does not know that machine's
// home and must not invent one.
start = summary.cwd?.takeIf { it.isNotBlank() } ?: "~",
)
)
},
)
// What it opens is about this session, so it sits at the end of the session's
// own row. The name is the whole of what it holds today, which is why it is a
// cog
@@ -2092,6 +2110,7 @@ fun SessionScreen(
settingsOpen = false
},
onDismiss = { settingsOpen = false },
onCopyRenderReport = copyRenderReport,
)
}
}