fix(review): rebuild Session Manager additions on the merged #146 implementation (PR #153)

- Hide the new btn-session-manager header button on phones: add it to the
  @media (max-width: 430px) display:none block in mobile.css (next to
  .btn-away-digest) and to KNOWN_PHONE_HIDDEN in the mobile-header policy
  test, closing the recurring phone-header-leak regression that was PR
  #153's red CI job.
- Put the session-manager header button on its own line in index.html
  (was crammed onto the away-digest line).
- app.js: drop session:updated from the unified-list SSE refresh trigger —
  it is batch-broadcast ~every 500ms per active session and would turn an
  open modal / visible welcome list into a sustained ~1 Hz full projects
  rescan loop; created/deleted (structural changes) are sufficient.
- terminal-ui.js _fetchUnifiedSessions: check the ApiResponse envelope and
  throw on failure so a 5xx surfaces via the caller's catch instead of
  rendering an empty history.
- terminal-ui.js _openSessionRowMenu: on re-entry, invoke the previous
  menu's close fn (stored as _openRowMenuClose) so its document/window
  listeners are detached rather than leaked; use claudeSessionId ||
  sessionId in the 'Resume session' menu item to match the main-row and
  Session Manager resume routing.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
Codeman maintainer
2026-07-13 00:38:56 +02:00
parent f55f035690
commit efe2d8966a
5 changed files with 35 additions and 16 deletions
+5 -2
View File
@@ -1426,8 +1426,11 @@ class CodemanApp {
// visible welcome list) on session structural changes. Extra listeners on the // visible welcome list) on session structural changes. Extra listeners on the
// same EventSource — EventSource supports multiple listeners per event — so the // same EventSource — EventSource supports multiple listeners per event — so the
// existing handlers above are untouched. Registered through addListener so they // existing handlers above are untouched. Registered through addListener so they
// are torn down with the rest on reconnect. // are torn down with the rest on reconnect. Only structural events (created /
for (const event of [SSE_EVENTS.SESSION_CREATED, SSE_EVENTS.SESSION_UPDATED, SSE_EVENTS.SESSION_DELETED]) { // deleted) trigger a refetch: session:updated is batch-broadcast every ~500ms
// per active session, which would otherwise turn an open modal / visible welcome
// list into a sustained ~1 Hz full ~/.claude/projects rescan loop.
for (const event of [SSE_EVENTS.SESSION_CREATED, SSE_EVENTS.SESSION_DELETED]) {
addListener(event, () => this._onSessionListMaybeChanged()); addListener(event, () => this._onSessionListMaybeChanged());
} }
} }
+2 -1
View File
@@ -118,7 +118,8 @@
</div> </div>
<button class="btn-icon-header btn-redraw-terminal btn-redraw-terminal--hidden" onclick="app.restoreTerminalSize()" title="Redraw terminal to fit current screen (Ctrl+Shift+R)" aria-label="Redraw terminal"><svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><polyline points="1 4 1 10 7 10"/><polyline points="23 20 23 14 17 14"/><path d="M20.49 9A9 9 0 0 0 5.64 5.64L1 10m22 4l-4.64 4.36A9 9 0 0 1 3.51 15"/></svg></button> <button class="btn-icon-header btn-redraw-terminal btn-redraw-terminal--hidden" onclick="app.restoreTerminalSize()" title="Redraw terminal to fit current screen (Ctrl+Shift+R)" aria-label="Redraw terminal"><svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><polyline points="1 4 1 10 7 10"/><polyline points="23 20 23 14 17 14"/><path d="M20.49 9A9 9 0 0 0 5.64 5.64L1 10m22 4l-4.64 4.36A9 9 0 0 1 3.51 15"/></svg></button>
<button class="btn-icon-header btn-response-viewer-header btn-response-viewer-header--hidden" onclick="app.toggleResponseViewer()" title="View last response" aria-label="View last response"><svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M1 12s4-8 11-8 11 8 11 8-4 8-11 8-11-8-11-8z"/><circle cx="12" cy="12" r="3"/></svg></button> <button class="btn-icon-header btn-response-viewer-header btn-response-viewer-header--hidden" onclick="app.toggleResponseViewer()" title="View last response" aria-label="View last response"><svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M1 12s4-8 11-8 11 8 11 8-4 8-11 8-11-8-11-8z"/><circle cx="12" cy="12" r="3"/></svg></button>
<button class="btn-icon-header btn-away-digest" onclick="app.openAwayDigest()" title="Away Digest" aria-label="Open away digest"><svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M8 6h13"/><path d="M8 12h13"/><path d="M8 18h13"/><path d="M3 6h.01"/><path d="M3 12h.01"/><path d="M3 18h.01"/></svg></button> <button class="btn-icon-header btn-session-manager" onclick="app.openSessionManager()" title="Session Manager" aria-label="Open session manager"><svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><polyline points="12 2 2 7 12 12 22 7 12 2"/><polyline points="2 17 12 22 22 17"/><polyline points="2 12 12 17 22 12"/></svg></button> <button class="btn-icon-header btn-away-digest" onclick="app.openAwayDigest()" title="Away Digest" aria-label="Open away digest"><svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M8 6h13"/><path d="M8 12h13"/><path d="M8 18h13"/><path d="M3 6h.01"/><path d="M3 12h.01"/><path d="M3 18h.01"/></svg></button>
<button class="btn-icon-header btn-session-manager" onclick="app.openSessionManager()" title="Session Manager" aria-label="Open session manager"><svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><polyline points="12 2 2 7 12 12 22 7 12 2"/><polyline points="2 17 12 22 22 17"/><polyline points="2 12 12 17 22 12"/></svg></button>
<button class="btn-icon-header btn-attachments-history btn-attachments-history--hidden" id="attachmentsHistoryBtn" onclick="app.toggleAttachmentHistory()" title="Attachments" aria-label="Open attachment history" aria-expanded="false"> <button class="btn-icon-header btn-attachments-history btn-attachments-history--hidden" id="attachmentsHistoryBtn" onclick="app.toggleAttachmentHistory()" title="Attachments" aria-label="Open attachment history" aria-expanded="false">
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="m21.44 11.05-9.19 9.19a6 6 0 0 1-8.49-8.49l9.19-9.19a4 4 0 0 1 5.66 5.66l-9.2 9.19a2 2 0 0 1-2.83-2.83l8.49-8.48"/></svg> <svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="m21.44 11.05-9.19 9.19a6 6 0 0 1-8.49-8.49l9.19-9.19a4 4 0 0 1 5.66 5.66l-9.2 9.19a2 2 0 0 1-2.83-2.83l8.49-8.48"/></svg>
<span class="attachment-history-badge" id="attachmentHistoryBadge" style="display:none;">0</span> <span class="attachment-history-badge" id="attachmentHistoryBadge" style="display:none;">0</span>
+6 -4
View File
@@ -459,14 +459,16 @@ html.mobile-init .file-browser-panel {
height: 12px; height: 12px;
} }
/* Hide header settings gear, lifecycle log, and away digest on mobile - settings /* Hide header settings gear, lifecycle log, away digest, and session manager on
moved to toolbar; away digest is a secondary informational control that doesn't mobile - settings moved to toolbar; away digest and the session manager are
belong on the cramped phone header. secondary controls that don't belong on the cramped phone header (the session
manager stays reachable via the Ctrl+K palette's "Browse all sessions" item).
(The attachments button is opt-in / default-hidden everywhere via its own (The attachments button is opt-in / default-hidden everywhere via its own
--hidden marker, so it needs no mobile-specific rule here.) */ --hidden marker, so it needs no mobile-specific rule here.) */
.btn-icon-header.btn-settings, .btn-icon-header.btn-settings,
.btn-icon-header.btn-lifecycle-log, .btn-icon-header.btn-lifecycle-log,
.btn-icon-header.btn-away-digest { .btn-icon-header.btn-away-digest,
.btn-icon-header.btn-session-manager {
display: none !important; display: none !important;
} }
+21 -8
View File
@@ -1152,8 +1152,14 @@ Object.assign(CodemanApp.prototype, {
*/ */
async _fetchUnifiedSessions(limit = 60) { async _fetchUnifiedSessions(limit = 60) {
const res = await fetch('/api/sessions/unified?limit=' + limit); const res = await fetch('/api/sessions/unified?limit=' + limit);
const data = await res.json(); // ApiResponse envelope: { success, data: { sessions } }. Throw on failure so
return data.data?.sessions || []; // callers (loadHistorySessions) hit their catch instead of rendering a 5xx as
// an empty history.
const data = await res.json().catch(() => null);
if (!res.ok || !data || data.success === false || !data.data) {
throw new Error(data?.error || `unified sessions request failed (HTTP ${res.status})`);
}
return data.data.sessions || [];
}, },
/** /**
@@ -1385,14 +1391,15 @@ Object.assign(CodemanApp.prototype, {
* @param {HTMLElement} detail the inline detail panel element * @param {HTMLElement} detail the inline detail panel element
*/ */
_openSessionRowMenu(anchorEl, s, cases, item, detail) { _openSessionRowMenu(anchorEl, s, cases, item, detail) {
// Close any already-open row menu first. // Close any already-open row menu first — call its own close fn so the
if (this._openRowMenuEl) { // previous menu's document/window listeners are detached (a raw .remove()
// would leave them dangling until the next event self-cleans).
if (this._openRowMenuClose) {
try { try {
this._openRowMenuEl.remove(); this._openRowMenuClose();
} catch { } catch {
/* noop */ /* noop */
} }
this._openRowMenuEl = null;
} }
const isLiveOpen = const isLiveOpen =
@@ -1416,7 +1423,10 @@ Object.assign(CodemanApp.prototype, {
} catch { } catch {
/* noop */ /* noop */
} }
if (this._openRowMenuEl === menu) this._openRowMenuEl = null; if (this._openRowMenuEl === menu) {
this._openRowMenuEl = null;
this._openRowMenuClose = null;
}
}; };
// Helper: build one menu item button. // Helper: build one menu item button.
@@ -1451,7 +1461,9 @@ Object.assign(CodemanApp.prototype, {
if (isLiveOpen) { if (isLiveOpen) {
this.selectSession(s.sessionId); this.selectSession(s.sessionId);
} else { } else {
this.resumeHistorySession(s.sessionId, s.workingDir || ''); // Resume by the Claude conversation UUID when present (resumed sessions
// carry theirs separately from their Codeman id).
this.resumeHistorySession(s.claudeSessionId || s.sessionId, s.workingDir || '');
} }
this.closeSessionManager?.(); this.closeSessionManager?.();
closeMenu(); closeMenu();
@@ -1521,6 +1533,7 @@ Object.assign(CodemanApp.prototype, {
window.addEventListener('resize', onScrollResize, true); window.addEventListener('resize', onScrollResize, true);
this._openRowMenuEl = menu; this._openRowMenuEl = menu;
this._openRowMenuClose = closeMenu;
}, },
/** Number of history items shown before "Show More" */ /** Number of history items shown before "Show More" */
+1 -1
View File
@@ -38,7 +38,7 @@ const MOBILE_VISIBLE_ALLOWLIST = new Set<string>([]);
// that removes a hide rule fails loudly (not silently). The attachments button is // that removes a hide rule fails loudly (not silently). The attachments button is
// NOT here: it's opt-in (default-hidden everywhere via its own --hidden marker), so // NOT here: it's opt-in (default-hidden everywhere via its own --hidden marker), so
// it's excluded from the default-visible enumeration rather than mobile-hidden. // it's excluded from the default-visible enumeration rather than mobile-hidden.
const KNOWN_PHONE_HIDDEN = ['btn-settings', 'btn-lifecycle-log']; const KNOWN_PHONE_HIDDEN = ['btn-settings', 'btn-lifecycle-log', 'btn-session-manager'];
function attrOf(openTag: string, name: string): string { function attrOf(openTag: string, name: string): string {
const m = openTag.match(new RegExp(`${name}="([^"]*)"`)); const m = openTag.match(new RegExp(`${name}="([^"]*)"`));