Read a session's transcript as the file it is

The conversation on screen is a drawing of the record, and when the two
disagree -- or when something in the record is what has gone wrong -- there
was no way to see the record itself from the phone.

View raw in the session settings dialog opens the file explorer on the
transcript, which is its third caller and needed no new screen: the file
name and path in the header, a back button, and the lines as code. The
session says where the file is, because only the backend knows, and it
names this backend's machine rather than the session's -- for a remote
session those are two different filesystems. Back from the file lands in
the session's own directory, where the log and the process record are.

The paragraph under Reload goes, and both buttons move to a line of their
own: two buttons and a measurement do not fit a phone's width.
This commit is contained in:
iris-ai committed 2026-09-21 02:51:19 -04:00
1 parent 049780fda6
commit 66b3403a71
8 files changed
+123 -12

No files matched your search

+4 -1
View File
@@ -37,7 +37,10 @@
//! PUT /machines/{id} rename {name?} and/or re-probe {rediscover?}
//! DELETE /machines/{id} remove, refused while sessions use it
//! GET /sessions list (id, provider, title, model, status, last activity)
//! GET /sessions/{id} one session, for refetching after a change
//! GET /sessions/{id} one session, for refetching after a change. Its
//! `transcriptFile` is where the record itself is --
//! {machine, machineName, path} -- so the explorer can be
//! pointed at it
//! POST /sessions spawn {machine, provider, title?, model?, cwd?, params?}
//! POST /sessions/order {sessions} -- the list in the order it is drawn in,
//! as the reader dragged it; ids left out keep their
+41 -1
View File
@@ -316,6 +316,10 @@ pub struct SessionInfo {
/// Absent until it reports one; absence is not a measured zero.
#[serde(skip_serializing_if = "Option::is_none")]
pub background_tasks: Option<usize>,
/// Where the transcript file itself is, for a reader who wants the record
/// rather than the conversation drawn from it -- see [`TranscriptFile`].
#[serde(skip_serializing_if = "Option::is_none")]
pub transcript_file: Option<TranscriptFile>,
/// How many subagents this session has started, from a directory
/// listing rather than reading each one's status -- see
/// `GET /sessions/{id}/subagents` for that. 0 when it has none, not
@@ -323,6 +327,37 @@ pub struct SessionInfo {
pub subagents: usize,
}
/// Where a session's transcript file is, as somewhere the file explorer can
/// be pointed at.
///
/// The machine as well as the path, and it is **this backend's** machine
/// rather than the session's: the file is written here whichever machine the
/// session runs on, and for a remote session those are two different
/// filesystems. `None` where this backend's own machine is not in the config
/// at all, which is the state in which nothing could read the file anyway --
/// and the phone then offers no way in, rather than one that fails.
#[derive(Debug, Clone, Serialize)]
#[serde(rename_all = "camelCase")]
pub struct TranscriptFile {
pub machine: String,
pub machine_name: String,
pub path: String,
}
/// Where `id`'s transcript is, said the way the explorer takes it.
fn transcript_file(config: &Config, data_dir: &Path, id: &str) -> Option<TranscriptFile> {
let machine = config.machine(crate::config::LOCAL_MACHINE_ID)?;
Some(TranscriptFile {
machine: machine.id.clone(),
machine_name: machine.name.clone(),
path: data_dir
.join(id)
.join("transcript.jsonl")
.to_string_lossy()
.into_owned(),
})
}
/// What is running a session at this moment, and `None` when nothing is.
///
/// Behind a lock because a session outlives its process: stopping one and
@@ -673,6 +708,7 @@ impl LiveSession {
current: &SessionConfig,
imported: bool,
kind: Option<DriverKind>,
transcript_file: Option<TranscriptFile>,
) -> SessionInfo {
SessionInfo {
id: self.meta.id.clone(),
@@ -717,6 +753,7 @@ impl LiveSession {
.driver()
.and_then(|driver| driver.background_tasks())
.map(|tasks| tasks.len()),
transcript_file,
subagents: subagent::count(self.dir()),
}
}
@@ -1234,6 +1271,7 @@ impl SessionManager {
meta,
import::read_cursor(&self.data_dir.join(&meta.id)).is_some(),
kind_of(&inner.config, &meta.machine, &meta.provider),
transcript_file(&inner.config, &self.data_dir, &meta.id),
),
None => SessionInfo {
id: meta.id.clone(),
@@ -1274,6 +1312,7 @@ impl SessionManager {
last_activity: meta.created,
created: meta.created,
background_tasks: None,
transcript_file: transcript_file(&inner.config, &self.data_dir, &meta.id),
subagents: subagent::count(&self.data_dir.join(&meta.id)),
},
})
@@ -1491,6 +1530,7 @@ impl SessionManager {
&session.meta,
import::read_cursor(&self.data_dir.join(&id)).is_some(),
Some(provider.kind),
transcript_file(&inner.config, &self.data_dir, &id),
);
inner.live.insert(id, session);
Ok(info)
@@ -3487,7 +3527,7 @@ mod tests {
// open to look one up on.
assert_eq!(
first.title,
session.info("m", &session.meta, false, None).title
session.info("m", &session.meta, false, None, None).title
);
manager.set_session_notify(&info.id, false).expect("off");