Phase 3: Session recording/playback and encrypted profile export/import

Session Recording & Playback:
- Add recording package with asciicast v2 format writer and reader
- Recorder captures terminal I/O with timestamps from SSH and Telnet sessions
- Input and output events recorded via hooks in session manager callbacks
- Recording store manages .cast files in ~/Library/Application Support/NexusTerminal/recordings
- Recording browser modal lists all saved recordings with metadata
- Full playback viewer with embedded xterm.js terminal, progress scrubber,
  play/pause/restart controls, and 0.5x-10x speed adjustment
- Recording indicator (pulsing red dot) shown on active tab during capture
- Keyboard shortcut Cmd+Shift+R to toggle recording on active session
- Tests for recording, store listing, deletion, and double-close safety

Encrypted Profile Export/Import:
- Add exportimport package with AES-256-GCM encryption (Argon2id key derivation)
- File format: version byte + 32-byte salt + 12-byte nonce + ciphertext
- Same Argon2id parameters as vault (128 MiB, 3 iterations, 4 threads)
- Export modal with profile selection checkboxes and password entry
- Import modal with password entry and conflict strategy (skip/overwrite/duplicate)
- Native save/open file dialogs for .nexus files
- Tests for round-trip encryption, wrong password rejection, corrupt files
- Replaced placeholder import/export with full encrypted implementation

UI:
- Added export and recordings buttons to sidebar footer
- Recording and playback CSS with charcoal design tokens
- Wired RecordingManager and ExportImportUI into app.js
This commit is contained in:
2026-04-16 00:00:00 +01:00
parent da6320c04b
commit 8c9ce67684
14 changed files with 2108 additions and 33 deletions
+167
View File
@@ -0,0 +1,167 @@
/* ── Session Recording & Playback ────────────────────────────────────── */
/* Recording indicator dot on tabs */
.recording-dot {
display: inline-block;
width: 8px;
height: 8px;
border-radius: 50%;
background: var(--error, #ef4444);
margin-right: 6px;
flex-shrink: 0;
animation: recording-pulse 1.2s ease-in-out infinite;
}
@keyframes recording-pulse {
0%, 100% { opacity: 1; }
50% { opacity: 0.3; }
}
/* Recordings list in browser modal */
.recordings-list {
display: flex;
flex-direction: column;
gap: 4px;
}
.recording-item {
display: flex;
align-items: center;
justify-content: space-between;
padding: 8px 12px;
background: var(--surface-2, #2a2a2e);
border-radius: 6px;
transition: background 0.15s ease;
}
.recording-item:hover {
background: var(--surface-3, #333338);
}
.recording-info {
display: flex;
flex-direction: column;
gap: 2px;
min-width: 0;
}
.recording-title {
font-size: 13px;
font-weight: 500;
color: var(--text-primary, #e4e4e7);
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
.recording-meta {
font-size: 11px;
color: var(--text-tertiary, #71717a);
}
.recording-actions {
display: flex;
gap: 4px;
flex-shrink: 0;
margin-left: 12px;
}
/* Playback modal — larger to fit a terminal */
.modal-playback {
width: 90vw;
max-width: 900px;
max-height: 90vh;
}
.playback-body {
display: flex;
flex-direction: column;
gap: 8px;
padding: 0 !important;
}
.playback-terminal {
flex: 1;
min-height: 300px;
background: #1a1a1e;
border-radius: 4px;
padding: 4px;
margin: 0 12px;
}
.playback-controls {
display: flex;
align-items: center;
gap: 8px;
padding: 8px 12px 12px;
background: var(--surface-1, #1e1e22);
border-top: 1px solid var(--border, #3f3f46);
}
.playback-progress {
flex: 1;
height: 4px;
cursor: pointer;
appearance: none;
-webkit-appearance: none;
background: var(--surface-3, #333338);
border-radius: 2px;
outline: none;
}
.playback-progress::-webkit-slider-thumb {
-webkit-appearance: none;
width: 12px;
height: 12px;
border-radius: 50%;
background: var(--accent, #3b82f6);
cursor: pointer;
border: none;
}
.playback-time {
font-size: 11px;
color: var(--text-tertiary, #71717a);
white-space: nowrap;
min-width: 80px;
text-align: right;
}
.playback-speed {
font-size: 11px;
background: var(--surface-2, #2a2a2e);
color: var(--text-primary, #e4e4e7);
border: 1px solid var(--border, #3f3f46);
border-radius: 4px;
}
/* Export/import profile list */
.export-profile-list {
display: flex;
flex-direction: column;
gap: 4px;
padding: 8px;
background: var(--surface-2, #2a2a2e);
border-radius: 6px;
border: 1px solid var(--border, #3f3f46);
}
.export-profile-item {
display: flex;
align-items: center;
gap: 8px;
padding: 4px 8px;
font-size: 13px;
color: var(--text-primary, #e4e4e7);
cursor: pointer;
border-radius: 4px;
transition: background 0.1s ease;
}
.export-profile-item:hover {
background: var(--surface-3, #333338);
}
.export-profile-item input[type="checkbox"] {
accent-color: var(--accent, #3b82f6);
}