Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions src/services/unified-session-service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@ export type UnifiedSessionItem = {
claudeSessionId?: string;
firstPrompt?: string;
sizeBytes?: number;
projectKey?: string;
remote?: boolean;
sources: string[];
stats?: { memoryMB: number; cpuPercent: number };
Expand Down Expand Up @@ -76,6 +77,7 @@ export type HistoryInput = {
sizeBytes: number;
lastModified: string;
firstPrompt?: string;
projectKey?: string;
};

/** Mux process-stat view. */
Expand Down Expand Up @@ -147,6 +149,7 @@ export function mergeUnifiedSessions(sources: UnifiedSources): UnifiedSessionIte
overwrite(item, 'workingDir', h.workingDir);
overwrite(item, 'sizeBytes', h.sizeBytes);
overwrite(item, 'firstPrompt', h.firstPrompt);
overwrite(item, 'projectKey', h.projectKey);
const ms = Date.parse(h.lastModified);
if (!Number.isNaN(ms) && item.lastActivityAt === undefined) item.lastActivityAt = ms;
}
Expand Down
12 changes: 12 additions & 0 deletions src/web/public/app.js
Original file line number Diff line number Diff line change
Expand Up @@ -1421,6 +1421,18 @@ class CodemanApp {
for (const [event] of _SSE_HANDLER_MAP) {
addListener(event, this._sseHandlerWrappers.get(event));
}

// COD-121: live-refresh the unified session list (Session Manager modal +
// visible welcome list) on session structural changes. Extra listeners on the
// same EventSource — EventSource supports multiple listeners per event — so the
// existing handlers above are untouched. Registered through addListener so they
// are torn down with the rest on reconnect. Only structural events (created /
// 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());
}
}

// ═══════════════════════════════════════════════════════════════
Expand Down
1 change: 1 addition & 0 deletions src/web/public/index.html
Original file line number Diff line number Diff line change
Expand Up @@ -119,6 +119,7 @@
<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-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">
<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>
Expand Down
10 changes: 6 additions & 4 deletions src/web/public/mobile.css
Original file line number Diff line number Diff line change
Expand Up @@ -459,14 +459,16 @@ html.mobile-init .file-browser-panel {
height: 12px;
}

/* Hide header settings gear, lifecycle log, and away digest on mobile - settings
moved to toolbar; away digest is a secondary informational control that doesn't
belong on the cramped phone header.
/* Hide header settings gear, lifecycle log, away digest, and session manager on
mobile - settings moved to toolbar; away digest and the session manager are
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
--hidden marker, so it needs no mobile-specific rule here.) */
.btn-icon-header.btn-settings,
.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;
}

Expand Down
22 changes: 22 additions & 0 deletions src/web/public/panels-ui.js
Original file line number Diff line number Diff line change
Expand Up @@ -671,6 +671,28 @@ Object.assign(CodemanApp.prototype, {
if (modal) modal.classList.remove('active');
},

/**
* COD-121: live-refresh the unified session list when sessions change
* (created/updated/deleted via SSE). Only touches surfaces that are currently
* showing — the open Session Manager modal and/or the visible welcome list —
* and is debounced so an event burst collapses into one re-fetch. The current
* search query is preserved.
*/
_onSessionListMaybeChanged() {
const modal = document.getElementById('sessionManagerModal');
if (modal && modal.classList.contains('active')) {
this._debouncedCall(
'sessionManagerRefresh',
() => this._loadSessionManagerList(this._sessionManagerQuery || ''),
400
);
}
const welcome = document.getElementById('welcomeOverlay');
if (welcome && welcome.classList.contains('visible')) {
this._debouncedCall('welcomeHistoryRefresh', () => this.loadHistorySessions(), 600);
}
},

setAwayDigestRange(range) {
this.awayDigestRange = range;
this._awayDigestLoadedSuccessfully = false;
Expand Down
71 changes: 71 additions & 0 deletions src/web/public/styles.css
Original file line number Diff line number Diff line change
Expand Up @@ -2831,6 +2831,32 @@ body.touch-device .terminal-container .xterm .xterm-helper-textarea {
font-weight: 500;
}

/* Badge row (mode + LIVE pills) sits between the title and subtitle (COD-121). */
.history-item-badges {
display: inline-flex;
flex-wrap: wrap;
gap: 0.3rem;
align-items: center;
}

.history-item-badge {
font-size: 0.55rem;
font-weight: 600;
text-transform: uppercase;
letter-spacing: 0.03em;
line-height: 1;
padding: 0.12rem 0.35rem;
border-radius: 0.35rem;
background: var(--bg-hover);
color: var(--text-muted);
white-space: nowrap;
}

.history-item-badge-live {
background: color-mix(in srgb, var(--green) 22%, transparent);
color: var(--green);
}

.history-item-meta {
font-size: 0.7rem;
color: var(--text-muted);
Expand Down Expand Up @@ -5713,6 +5739,51 @@ kbd {
overflow-y: auto;
}

/* COD-130: per-row ⋯ kebab context menu (appended to <body>, fixed-positioned).
z-index must clear the Session Manager modal (.modal is z-index: 1000). */
.session-row-menu {
position: fixed;
z-index: 10010;
min-width: 180px;
display: flex;
flex-direction: column;
gap: 2px;
padding: 4px;
background: rgba(22, 22, 28, 0.97);
backdrop-filter: blur(20px);
-webkit-backdrop-filter: blur(20px);
border: 1px solid var(--border);
border-radius: 10px;
box-shadow: 0 8px 32px rgba(0, 0, 0, 0.5), 0 2px 8px rgba(0, 0, 0, 0.3);
}

.session-row-menu-item {
display: flex;
flex-direction: column;
align-items: flex-start;
gap: 1px;
width: 100%;
padding: 7px 12px;
background: none;
border: none;
border-radius: var(--btn-radius);
color: var(--text);
cursor: pointer;
font-size: 0.8rem;
text-align: left;
white-space: nowrap;
transition: background var(--transition-smooth);
}

.session-row-menu-item:hover {
background: var(--bg-hover);
}

.session-row-menu-sublabel {
color: var(--text-muted);
font-size: 0.72rem;
}

/* Shortcut settings rows (App Settings → Shortcuts, COD-157) */
.shortcut-setting-row {
display: grid;
Expand Down
Loading