Let a session be renamed, under the same name everywhere

A gear at the end of the session's own bar opens what can be changed
about that session; the name is the first thing there. Compact is gone
from that bar -- `/compact` typed into the message box is the CLI's own
way to ask and it already worked, so the button was a second way to say
one thing. Echo takes the typed word too now, since it is the rig the
compaction display is checked against and losing the button would have
taken that with it.

The name is this server's, not a driver's: it is what the list shows, it
exists before any process does, and every provider has one. So it is
settled in the config and the driver is *told* -- which is the opposite
of the model and the permission mode, and the difference is written down
at `Driver::set_title`. A driver whose process has no notion of a name
does nothing and says nothing, because there is no failure to report.

Claude Code has one, so the name reaches it: `--name` for a session we
create, and `/rename` afterwards, which is a local command rather than a
control request -- `set_session_name` is not a subtype it knows, which I
established by asking it. A resumed session is deliberately not renamed
at launch: an import already has a name, quite possibly one the person
typing in it chose, and taking that would be helping itself to something
the app was only shown.

Verified end to end rather than argued: renaming from the phone put
"Session renamed to: paging and scroll" in the CLI's own session file,
and the session now lists under that name to other agents.

The gear is drawn rather than set in a font, for the reason Chevron
gives. It was a sun on the first attempt -- thin teeth standing clear of
a thin hub -- which no amount of reading the diff would have shown.
This commit is contained in:
iris committed 2026-08-29 16:06:15 -04:00
1 parent 1629e0911e
commit d3fff3d229
11 files changed
+395 -12

No files matched your search

+36 -2
View File
@@ -336,8 +336,19 @@ impl ClaudeDriver {
if let Some(mode) = &meta.permission_mode {
push("--permission-mode", mode);
}
if let Some(resume) = read_resume_token(session_dir) {
push("--resume", &resume);
// Named at birth, so this session is the same session in the CLI's
// own picker and in what other agents see when they list it.
//
// Only when we are the ones creating it. A resume is a session
// that already existed -- an import, or this server starting again
// -- and it already has whatever name it was given, quite possibly
// by the person who was typing in it. Renaming that from a title
// we derived from its first message would be taking something the
// app was only ever shown. `Driver::set_title` is how it changes
// after this point, and that one is asked for.
match read_resume_token(session_dir) {
Some(resume) => push("--resume", &resume),
None => push("--name", &meta.title),
}
args.push("--include-partial-messages".to_string());
@@ -505,6 +516,29 @@ impl Driver for ClaudeDriver {
);
}
fn set_title(&self, title: &str) {
// The CLI's own mechanism, and a local command rather than a
// control request -- `set_session_name` is not a subtype it
// knows, measured by asking. It answers this the way it answers
// `/compact`: a fresh `init`, then a `result` for a turn with no
// model call in it, so the same "a turn is in flight" bookkeeping
// applies. A name with a newline in it would be two lines and the
// second would be a message, so it is refused rather than sent.
if title.contains('\n') {
let _ = self.sink.send(Event::Error {
message: "a session name cannot contain a line break".to_string(),
});
return;
}
self.queue.lock().unwrap().running = true;
self.send_line(
json!({"type": "user", "message": {"role": "user", "content": [
{"type": "text", "text": format!("/rename {title}")}
]}})
.to_string(),
);
}
fn compact(&self) {
// A turn is in flight from here. The CLI answers `/compact` like
// any other message -- a status line, a boundary, then a `result`