feat(sessions): a renamed tab stays renamed, and a reload comes back to it (#54)#55
Merged
Merged
Conversation
…to it (#54) A name the user chose used to live only in the page that typed it. The strip read it out of its own state, nothing stored it, and the next page load — a reload, a second window, a restart — rebuilt every tab from the name the session was created with. The one moment the labels matter, coming back to a set of long-running sessions, was exactly the moment they were gone. The chosen name is now a field of its own on the session record, stored beside the created name rather than over it, so a session nobody renamed still goes through the generated-name rules and shows its folder. A new nullable `custom_name` column carries it across a restart. `PATCH /api/sessions/:id/name` takes the rename and tells every socket the user has open, so a window that is already looking at the same sessions follows without reloading; the page that asked renames optimistically and puts the old label back if the server refuses. The same reload also used to lose the user's place. The selected tab is now remembered in the browser — sessionStorage first, so two windows keep their own tab, with localStorage behind it so a newly opened window still lands somewhere sensible — and a remembered id for a session that is gone falls back to the first tab rather than an empty screen. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Contributor
There was a problem hiding this comment.
Pull request overview
This PR implements persistence and cross-window synchronization for renamed session tabs, and adds per-window tab selection persistence so reloads return users to the previously active session.
Changes:
- Introduces
customNameon sessions (DB persistence + API + UI) and addsPATCH /api/sessions/:id/namewith websocket broadcast (session_renamed) for live updates across windows. - Persists the active tab selection to
sessionStorage(per-window) with alocalStoragefallback (per-browser), and uses it at boot to select the initial tab after reload. - Adds/extends unit and browser checks covering rename validation/persistence/broadcasting and long-name layout behavior.
Reviewed changes
Copilot reviewed 14 out of 14 changed files in this pull request and generated 3 comments.
Show a summary per file
| File | Description |
|---|---|
| test/tab-manager-state.test.js | Adds tests for optimistic renaming, remote rename application, and active-tab persistence across reloads/windows. |
| test/session-store.test.js | Adds persistence/migration coverage for the new custom_name column. |
| test/session-rename.test.js | New route-level tests for rename validation, persistence, ownership, and websocket fan-out behavior. |
| test/browser/checks.ts | Adds a browser check ensuring very long tab names don’t break desktop/mobile layouts. |
| src/server/types.ts | Adds customName to SessionRecord and SessionListItem. |
| src/server/services/session-store.ts | Persists customName to/from SQLite as custom_name. |
| src/server/services/database.ts | Adds additive migration to create custom_name column if missing. |
| src/server/routes/sessions.ts | Adds rename route, includes customName in listings, and uses display name in resumable/export contexts. |
| src/client/types.ts | Adds customName to client SessionListItem and introduces WsSessionRenamedMessage. |
| src/client/terminal/message-handler.ts | Handles session_renamed websocket messages and applies them to the tab manager. |
| src/client/shell/dialogs/SessionsDialog.tsx | Displays customName when present. |
| src/client/sessions/tab-manager.ts | Adds customName handling, optimistic rename + revert, remote rename application, and active-tab persistence. |
| src/client/app.ts | Boots into the remembered/valid active tab instead of always the first tab. |
| CHANGELOG.md | Documents the new behavior for tab renames and reload tab restoration. |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
| // refuses — a session that has since been deleted, a name it will not take — | ||
| // the old label goes back, so the strip never keeps a name that was not | ||
| // stored. | ||
| void fetch(`/api/sessions/${sessionId}/name`, { |
Comment on lines
+246
to
+254
| // A name is what the user typed with the whitespace taken off, and a name | ||
| // that is nothing but whitespace is not a name. Capped because this label is | ||
| // rendered in a fixed-width strip and stored on every autosave, and neither | ||
| // has any use for a paragraph. | ||
| const trimmed = name.trim().slice(0, MAX_NAME_LENGTH); | ||
| if (!trimmed) { | ||
| res.status(400).json({ error: 'empty_name', message: 'Session name cannot be empty' }); | ||
| return; | ||
| } |
| ); | ||
|
|
||
| res.write(`# ${session.name}\n\n`); | ||
| res.write(`# ${displayName(session)}\n\n`); |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Closes #54.
What changed
Names. A chosen name is now a field of its own on the session record
(
customName), stored beside the created name rather than over it — so asession nobody renamed still goes through today's generated-name rules and shows
its folder, including when someone renames a tab to something that looks like a
generated name. A new nullable
custom_namecolumn carries it across a restart.PATCH /api/sessions/:id/namevalidates (string, trimmed, non-empty, capped at200) and broadcasts
session_renamedto every socket the owning user has open,so an already-open window follows without reloading. The page that asked renames
optimistically and puts the old label back if the server refuses.
The selected tab. Remembered in the browser on every switch: sessionStorage
first, so two windows each keep their own tab across reloads, with localStorage
behind it so a newly opened window still lands where the user last was. A
remembered id for a session that is gone falls back to the first tab.
Acceptance criteria
All ten verified against the running app (probe under
.work/probes/, drivingreal Chrome over CDP against a seeded server):
out of
/api/sessions/list);switcher off the side (new browser check);
Tests
test/session-rename.test.js(new): route validation, persistence, thebroadcast fan-out and who it reaches, ownership, the length cap.
test/tab-manager-state.test.js: the optimistic rename and its revert, thestored-name settle, remote renames, the boot tab and the two-window case.
test/session-store.test.js:custom_nameround trip and the additivemigration on a database that predates the column.
test/browser/checks.ts: a very long label on desktop and on a phone.npm test1400 passing ·npm run test:browser302 checks, no failures ·npm run typecheckclean.Open question from the issue
Clearing a custom name: renaming again is enough, and an empty name stays
rejected per the acceptance criteria — so no "reset to automatic" affordance was
added.
🤖 Generated with Claude Code