fix(run-menu): make Recent Sessions rows legible on macOS

Closes #273. Every row in the Run dropdown's Recent Sessions list rendered as
`/Users/<user>/co…`, indistinguishable from every other row.

The width was the symptom. The cause is that the home-prefix abbreviation
matched `/home/<user>/` only:

    s.workingDir.replace(/^\/home\/[^/]+\//, '~/')

On macOS the prefix is `/Users/<user>/`, so nothing was stripped and every row
spent its first ~19 characters on an identical prefix, with left-to-right
ellipsis cutting the only part that identifies it. The 250px menu cap was
chosen, per its own comment, as "the width at which the common `~/<dir>/<repo>`
+ timestamp recent-session row still fits whole" — sizing that assumes the
abbreviation ran. On Linux it does. On macOS the menu was permanently too
narrow for content it was never actually shortening, which is why this reads
as fine on one platform and broken on the other.

Changes:

- the regex matches `/home/` and `/Users/`
- the row leads with the identifying folder in semibold, with the parent path
  trailing, dimmed and right-aligned, so truncation removes context instead of
  identity
- the menu goes full width above 769px and the history list grows 200px -> 320px.
  Phones keep the compact popover deliberately: mobile.css positions this menu
  itself and a viewport-wide drawer there would cover the composer
- a worktree pill renders from the fields /api/history/sessions already returns
  unprojected (#266/#269), since a worktree's directory basename is often just
  the worktree name and rows stayed ambiguous without it
- a trailing `/.claude/worktrees` is trimmed from the displayed parent path once
  the pill states it, so the repo name stays visible

Verified in a browser at 1440px against a real 38-session history: menu 1416px,
0 of 34 rows clip their project name (was: all of them), 9 worktree pills
render, no page errors.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016uTqt8ttmsBLXbm5JFHis3
This commit is contained in:
Jordan Ryan
2026-08-10 01:44:14 -04:00
co-authored by Claude Opus 5
parent 752374abc7
commit 3a106bd048
2 changed files with 86 additions and 6 deletions
+38 -6
View File
@@ -489,23 +489,55 @@ Object.assign(CodemanApp.prototype, {
const date = new Date(s.lastModified);
const timeStr = date.toLocaleDateString('en', { month: 'short', day: 'numeric' })
+ ' ' + date.toLocaleTimeString('en', { hour: '2-digit', minute: '2-digit', hour12: false });
const shortDir = s.workingDir.replace(/^\/home\/[^/]+\//, '~/');
// Abbreviate the home prefix. This used to match `/home/<user>/` only, so
// on macOS (`/Users/<user>/`) nothing was stripped and every row spent its
// first ~19 characters on an identical prefix — with the tail ellipsized,
// all rows rendered as `/Users/jordanryan/co…` and became indistinguishable.
const shortDir = s.workingDir.replace(/^\/(?:home|Users)\/[^/]+\//, '~/');
// Lead with the folder that identifies the row; the parent path trails and
// is what gets truncated. Truncation must never eat the identity.
const lastSlash = shortDir.lastIndexOf('/');
const leafName = lastSlash === -1 ? shortDir : shortDir.slice(lastSlash + 1);
// `<repo>/.claude/worktrees` in the parent path is pure noise once the pill
// says which worktree it is — drop it so the repo stays visible instead.
const parentDir = (lastSlash === -1 ? '' : shortDir.slice(0, lastSlash)).replace(/\/\.claude\/worktrees$/, '');
const btn = document.createElement('button');
btn.className = 'run-mode-option';
btn.className = 'run-mode-option run-mode-hist-row';
btn.title = s.workingDir;
btn.dataset.sessionId = s.sessionId;
btn.dataset.workingDir = s.workingDir;
const dirSpan = document.createElement('span');
dirSpan.className = 'hist-dir';
dirSpan.textContent = shortDir;
const nameSpan = document.createElement('span');
nameSpan.className = 'hist-name';
nameSpan.textContent = leafName;
const parts = [nameSpan];
// Worktree pill, same data the session rows use (#266). A worktree's
// directory basename is often just the worktree name, so without this two
// worktrees of one repo still read alike.
const wt = this._worktreeLabel ? this._worktreeLabel(s) : '';
if (wt) {
const wtSpan = document.createElement('span');
wtSpan.className = 'hist-wt';
wtSpan.textContent = wt;
parts.push(wtSpan);
}
if (parentDir) {
const dirSpan = document.createElement('span');
dirSpan.className = 'hist-dir';
dirSpan.textContent = parentDir;
parts.push(dirSpan);
}
const metaSpan = document.createElement('span');
metaSpan.className = 'hist-meta';
metaSpan.textContent = timeStr;
parts.push(metaSpan);
btn.append(dirSpan, metaSpan);
btn.append(...parts);
btn.addEventListener('click', (e) => {
e.stopPropagation();
this.resumeHistorySession(s.sessionId, s.workingDir, s.name);
+48
View File
@@ -4392,6 +4392,24 @@ body.touch-device .terminal-container .xterm .xterm-helper-textarea {
max-width: 250px;
box-shadow: 0 8px 32px rgba(0, 0, 0, 0.5), 0 2px 8px rgba(0, 0, 0, 0.3);
}
/* Above phone width the menu becomes a full-width drawer across the bottom of the
window. The 250px cap above was sized for a `~/<dir>/<repo>` row, which assumed
the home prefix had been abbreviated — it never was on macOS, so real rows blew
straight through it and ellipsized into `/Users/<user>/co…`, identical on every
line. Width is what buys room for the worktree + branch, so the cap is lifted
rather than nudged. Phones keep the compact popover: mobile.css positions this
menu itself and a viewport-wide drawer there would cover the composer. */
@media (min-width: 769px) {
.run-mode-menu {
max-width: none;
width: calc(100vw - 24px);
}
.run-mode-history {
/* More rows are worth showing once each one is legible. */
max-height: 320px;
}
}
.run-mode-menu.active {
display: flex;
flex-direction: column;
@@ -4462,6 +4480,31 @@ body.touch-device .terminal-container .xterm .xterm-helper-textarea {
.run-mode-history .run-mode-option {
max-width: 100%;
}
/* Recent-session row: identity first, path last.
The row reads <folder> [⑂ worktree · branch] <parent path> <time>
with only the parent path allowed to shrink, so truncation can never hide
which project (or which worktree) a row refers to. */
.run-mode-hist-row {
align-items: baseline;
gap: 8px;
}
.run-mode-option .hist-name {
flex: 0 1 auto;
min-width: 0;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
font-weight: 600;
}
.run-mode-option .hist-wt {
flex: 0 0 auto;
font-size: 0.78em;
padding: 0 5px;
border-radius: 3px;
background: color-mix(in srgb, var(--accent) 16%, transparent);
color: var(--accent);
white-space: nowrap;
}
.run-mode-option .hist-dir {
flex: 1;
/* A flex item's default min-width is auto, so without this the path refuses to
@@ -4471,6 +4514,11 @@ body.touch-device .terminal-container .xterm .xterm-helper-textarea {
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
/* The parent path is context, not identity — dim it and let it be the part
that loses characters first. */
color: var(--text-muted);
font-size: 0.85em;
text-align: right;
}
.run-mode-option .hist-meta {
font-size: 0.85em;