Order the session list by when each agent was turned on
A running session no longer moves: the ones with a process come first, oldest start first, so starting one appends it to the bottom of that group and nothing it goes on to do -- beginning a turn, finishing one, asking a question -- can shift it. Sorting by activity with the awaiting-answer ones floated to the top is what this replaces; the status word and its colour already say which session wants something without the row having to move to say it. Stopped sessions are a group below, most recently active first. The order is the server's: `SessionConfig::started` is written each time a process is started for a session and reported as `started`, so it is the same on every device and survives a backend restart -- which adopts processes rather than starting them, and so could not work the times out for itself. Applied on the phone, because presentation order is a display decision. `LiveSession::info` takes the session's config entry rather than a parameter per field read from it, which is what `AutoResumeView` existed to bundle; that goes. Verified on the emulator against the sandbox: three echo sessions kept their order while the newest-active one was messaged; a stopped and restarted session moved below one started after it; a stopped session dropped below every running one; and after a backend restart the recorded times came back unchanged. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
1 parent
a9cfea89e5
commit
cbae7ee8c0
6 files changed
+255
-64
No files matched your search
@@ -225,6 +225,15 @@ data class SessionSummary(
|
||||
val usageProvider: String?,
|
||||
val status: String,
|
||||
val lastActivity: Double,
|
||||
/**
|
||||
* Epoch seconds a process was last started for this session -- when this agent was last turned
|
||||
* on. What the list orders the running sessions by; see [sessionsInListOrder].
|
||||
*
|
||||
* The server's own, so the order is the same on every device and across a backend restart. An
|
||||
* older server does not send it, and those sessions fall back to when the session was last
|
||||
* active, which is the best this app can do without inventing a time.
|
||||
*/
|
||||
val started: Double,
|
||||
/** Latest measured number of live background tasks; zero also covers older servers. */
|
||||
val backgroundTasks: Int,
|
||||
/**
|
||||
@@ -265,6 +274,7 @@ private fun parseSession(session: JSONObject) =
|
||||
usageProvider = session.optString("usageProvider").ifEmpty { null },
|
||||
status = session.getString("status"),
|
||||
lastActivity = session.getDouble("lastActivity"),
|
||||
started = session.optDouble("started", session.getDouble("lastActivity")),
|
||||
backgroundTasks = session.optInt("backgroundTasks", 0),
|
||||
subagents = session.optInt("subagents", 0),
|
||||
)
|
||||
|
||||
@@ -257,12 +257,7 @@ fun SessionListScreen(
|
||||
color = MaterialTheme.colorScheme.onSurfaceVariant,
|
||||
)
|
||||
}
|
||||
// Awaiting-answer first (the point of the screen), then most recently active.
|
||||
val ordered =
|
||||
state.value.sortedWith(
|
||||
compareByDescending<SessionSummary> { it.status == "awaitingInput" }
|
||||
.thenByDescending { it.lastActivity }
|
||||
)
|
||||
val ordered = sessionsInListOrder(state.value)
|
||||
LazyColumn {
|
||||
uniqueItems(ordered, key = { it.id }) { session ->
|
||||
SessionCard(
|
||||
@@ -472,6 +467,27 @@ fun SessionListScreen(
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* The order sessions are drawn in: the ones with a process, oldest agent first, then the stopped
|
||||
* ones, most recently active first. A display decision, made here rather than on the server, which
|
||||
* answers in config order -- see UI_RULES.
|
||||
*
|
||||
* The running group is ordered by when each agent was *turned on* rather than by what it is doing,
|
||||
* so a session cannot move by beginning a turn, finishing one or asking a question, and a list a
|
||||
* reader has their place in stays still. That replaces sorting by activity and floating the
|
||||
* awaiting-answer ones to the top: the status word and its colour already say which session wants
|
||||
* something, without the row having to move to say it. [SessionSummary.started] is the server's, so
|
||||
* the order is the same on every device and survives a backend restart.
|
||||
*
|
||||
* Stopped sessions are a group of their own because "turned on" is what the order above is made of.
|
||||
* A session nobody could ask about is not one of them -- it has a process, and dropping it out of
|
||||
* the running group the moment a machine goes quiet is the move this exists to prevent.
|
||||
*/
|
||||
fun sessionsInListOrder(sessions: List<SessionSummary>): List<SessionSummary> {
|
||||
val (running, stopped) = sessions.partition { it.status != "exited" }
|
||||
return running.sortedBy { it.started } + stopped.sortedByDescending { it.lastActivity }
|
||||
}
|
||||
|
||||
/**
|
||||
* The subagents picked out inside one session's card, and which card that is.
|
||||
*
|
||||
|
||||
@@ -0,0 +1,86 @@
|
||||
package com.example.aiapp
|
||||
|
||||
import kotlin.test.Test
|
||||
import kotlin.test.assertEquals
|
||||
|
||||
class SessionOrderTest {
|
||||
@Test
|
||||
fun `running sessions keep the order their agents were turned on in`() {
|
||||
val first = session("first", status = "running", started = 10.0, lastActivity = 900.0)
|
||||
val second =
|
||||
session("second", status = "awaitingInput", started = 20.0, lastActivity = 20.0)
|
||||
val third = session("third", status = "waiting", started = 30.0, lastActivity = 500.0)
|
||||
|
||||
assertEquals(
|
||||
listOf("first", "second", "third"),
|
||||
sessionsInListOrder(listOf(third, second, first)).map { it.id },
|
||||
)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `a session started again joins the bottom of the running group`() {
|
||||
val old = session("old", status = "idle", started = 10.0, lastActivity = 10.0)
|
||||
val restarted = session("restarted", status = "idle", started = 99.0, lastActivity = 99.0)
|
||||
|
||||
assertEquals(
|
||||
listOf("old", "restarted"),
|
||||
sessionsInListOrder(listOf(restarted, old)).map { it.id },
|
||||
)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `stopped sessions come after the running ones, most recent first`() {
|
||||
val running = session("running", status = "idle", started = 100.0, lastActivity = 100.0)
|
||||
val stale = session("stale", status = "exited", started = 1.0, lastActivity = 5.0)
|
||||
val recent = session("recent", status = "exited", started = 2.0, lastActivity = 50.0)
|
||||
|
||||
assertEquals(
|
||||
listOf("running", "recent", "stale"),
|
||||
sessionsInListOrder(listOf(stale, recent, running)).map { it.id },
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
* A session whose process nobody could ask about still has one, so it stays where it was rather
|
||||
* than dropping into the stopped group the moment a machine goes quiet.
|
||||
*/
|
||||
@Test
|
||||
fun `a session of unknown state is one of the running ones`() {
|
||||
val unknown = session("unknown", status = "unknown", started = 10.0, lastActivity = 10.0)
|
||||
val stopped = session("stopped", status = "exited", started = 5.0, lastActivity = 999.0)
|
||||
|
||||
assertEquals(
|
||||
listOf("unknown", "stopped"),
|
||||
sessionsInListOrder(listOf(stopped, unknown)).map { it.id },
|
||||
)
|
||||
}
|
||||
|
||||
private fun session(id: String, status: String, started: Double, lastActivity: Double) =
|
||||
SessionSummary(
|
||||
id = id,
|
||||
machine = "machine",
|
||||
machineName = "machine",
|
||||
provider = "echo",
|
||||
title = id,
|
||||
model = null,
|
||||
keepsOwnTranscript = false,
|
||||
ownTranscriptName = null,
|
||||
permissionMode = null,
|
||||
effort = null,
|
||||
takesEffort = false,
|
||||
imported = false,
|
||||
notify = true,
|
||||
autoResume = false,
|
||||
autoResumeMessage = DEFAULT_RESUME_MESSAGE,
|
||||
resumeAt = null,
|
||||
cwd = null,
|
||||
contextTokens = null,
|
||||
maxImageEdge = null,
|
||||
usageProvider = null,
|
||||
status = status,
|
||||
lastActivity = lastActivity,
|
||||
started = started,
|
||||
backgroundTasks = 0,
|
||||
subagents = 0,
|
||||
)
|
||||
}
|
||||
Reference in new issue
Block a user