Wrap table cells instead of cutting them off, and hand a batch over in one request

The renderer draws every table cell at one line with an ellipsis, so most
of a table was unreadable on a phone -- and an elided cell looks exactly
like a short one, so nothing said anything had been cut. Cells now take as
many lines as they need and align to the top of the row. Width is the other
half: a column narrows to 136dp and no further, and past that the table
scrolls sideways rather than squeezing. 136 is the widest floor that still
fits three columns across a phone, measured rather than picked; four and up
scroll, which is the right answer for genuinely too many columns.

The import screen used to send one request per selected row, so a handover
was only as atomic as the network: some rows started and the rest were
never asked for, and a row nobody asked for looks exactly like a row nobody
picked. `POST /setups/{id}/importable/delete` and `.../import` now take the
whole list, and every id is registered as in flight before the 202 goes
back. Only the registering is atomic -- the work settles per row, since six
deletes that all roll back together is not something a filesystem offers.

The echo driver grows `/table N`, with cells long enough to have been
truncated: a fixture of tidy one-word values renders fine whether or not
the bug is there.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
irisandClaude Opus 5 committed 2026-08-31 21:27:20 -04:00
1 parent 3c159fa1e1
commit 778b2e3b04
7 files changed
+336 -93

No files matched your search

@@ -629,47 +629,55 @@ fun startSession(settings: ServerSettings, sessionId: String) {
}
/**
* Removes a Claude Code session from the machine.
* Asks the machine to delete Claude Code sessions, and returns as soon as it has accepted the lot.
*
* The transcript *is* the session, so this ends any chance of resuming that conversation. The
* The transcript *is* the session, so this ends any chance of resuming those conversations. The
* caller confirms first; see ImportScreen.
*/
/**
* Asks the machine to delete a Claude Code session, and returns as soon as it has accepted.
*
* The work runs on the server, so this returning is not the same as it being done -- what says that
* is the row's own state, through [fetchImportable] and the change stream. That is the point:
* is each row's own state, through [fetchImportable] and the change stream. That is the point:
* leaving the screen used to cancel the delete it had started.
*
* One request for the whole batch, which is what makes a handover all-or-nothing. Sending one per
* row meant a batch could half-arrive -- four deleted, two never asked for -- and the two that were
* missed looked exactly like two that had not been picked.
*/
fun deleteImportable(settings: ServerSettings, setup: String, sessionId: String) {
requestFromServer(settings, "/setups/$setup/importable/$sessionId", method = "DELETE") {}
fun deleteImportable(settings: ServerSettings, setup: String, sessionIds: List<String>) {
requestFromServer(
settings,
"/setups/$setup/importable/delete",
method = "POST",
jsonBody = JSONObject().put("sessions", JSONArray(sessionIds)).toString(),
) {}
}
/**
* Continues a Claude Code session in the background, returning once the server has accepted.
* Continues Claude Code sessions in the background, returning once the server has accepted them.
*
* Separate from [spawnSession] because the two are asked different questions. That one means "start
* this and take me to it", so it waits and answers with the session. This is the import list's
* batch: several at once, nobody waiting on any particular one, and the result arrives as a row
* changing rather than as a reply -- which is what lets the screen be left.
* changing rather than as a reply -- which is what lets the screen be left. One request for all of
* them, for the reason [deleteImportable] gives.
*/
fun startImport(
settings: ServerSettings,
setup: String,
sessionId: String,
sessionIds: List<String>,
provider: String,
permissionMode: String? = null,
model: String? = null,
) {
val body =
JSONObject().apply {
put("sessions", JSONArray(sessionIds))
put("provider", provider)
permissionMode?.let { put("permissionMode", it) }
model?.let { put("model", it) }
}
requestFromServer(
settings,
"/setups/$setup/importable/$sessionId/import",
"/setups/$setup/importable/import",
method = "POST",
jsonBody = body.toString(),
) {}