COD-39 attachment history drawer

Stacks on COD-38: accumulates a per-session attachment history and exposes it
through a slide-in drawer with an unread badge, so attachments stay reachable
after their cards are dismissed.

Backend:
- session-attachment-history: history state — dedupe by source path / relative
  path, newest-first, 100-item cap, and externalPath sanitization (the absolute
  host path is server-private and never leaves toState()).
- session.ts: _attachmentHistory + getter (sanitized) / upsert / restore /
  getAttachmentHistoryForPersist; restored from saved state in the constructor.
- file-routes: GET /attachments (list — resolves each entry to live metadata +
  routes; external entries are re-registered) and GET /attachments/:id
  (metadata poll). The by-id route guards via the registry's TOCTOU-safe
  resolveServableAttachmentPath.
- server.ts: detected/registered attachments upsert into history and persist;
  the private (externalPath-bearing) history rides on disk under
  __attachmentHistory, separate from the sanitized public copy, and is restored
  on mux-session recovery.
- types/session.ts: SessionAttachmentHistoryItem + SessionState.attachmentHistory.

Frontend:
- panels-ui: the drawer (lazy-built), unread badge, list render with per-item
  preview/download/open/"Card" (reshow) actions, and live refresh of the open
  drawer on new detections.
- app.js: history state + per-session badge/cleanup wiring.
- index.html / styles.css / mobile.css: header button + badge and the drawer.

Verified: tsc / eslint / prettier / frontend-syntax / public-assets clean; new
history-module unit tests pass; full test:ci green (2866 passed); badge, drawer
open/render/reshow/close verified in-browser.
This commit is contained in:
Aamer Akhter
2026-06-14 09:05:17 +02:00
committed by Claude (Codeman maintainer)
parent 5eacb1cf03
commit 577b6d7384
11 changed files with 1025 additions and 6 deletions
+12
View File
@@ -390,6 +390,9 @@ class CodemanApp {
this.imagePopupZIndex = ZINDEX_IMAGE_POPUP_BASE;
this.attachmentCards = new Map(); // Map<attachmentId, { element, sessionId, filePath }>
this.attachmentCardStack = null;
this.attachmentHistoryCounts = new Map(); // Map<sessionId, count>
this.attachmentHistoryItems = [];
this.attachmentHistoryDrawerOpen = false;
// File browser state (methods in panels-ui.js)
this.fileBrowserData = null;
@@ -2222,6 +2225,7 @@ class CodemanApp {
KeyboardHandler.init();
// Clear tab alerts
this.tabAlerts.clear();
this.attachmentHistoryCounts.clear();
// Clear shown completions (used for duplicate notification prevention)
if (this._shownCompletions) {
this._shownCompletions.clear();
@@ -3225,6 +3229,10 @@ class CodemanApp {
// Instant active-class toggle (no 100ms debounce), then schedule full render for badges/status
this._updateActiveTabImmediate(sessionId);
this.renderSessionTabs();
this.updateAttachmentHistoryBadge?.();
if (this.attachmentHistoryDrawerOpen) {
this.loadAttachmentHistory?.(sessionId);
}
this._updateLocalEchoState();
// Restore flushed offset AND text IMMEDIATELY so backspace/typing work during
@@ -3595,6 +3603,10 @@ class CodemanApp {
this.projectInsights.delete(sessionId);
this.pendingHooks.delete(sessionId);
this.tabAlerts.delete(sessionId);
this.attachmentHistoryCounts.delete(sessionId);
if (this.attachmentHistoryDrawerOpen && this.activeSessionId === sessionId) {
this.closeAttachmentHistory?.();
}
this.terminalLoadStates.delete(sessionId);
this.clearCountdownTimers(sessionId);
this.closeSessionLogViewerWindows(sessionId);