ai-agent-session-center 2.0.2 → 2.0.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +484 -429
- package/docs/3D/ADAPTATION_GUIDE.md +592 -0
- package/docs/3D/index.html +754 -0
- package/docs/AGENT_TEAM_TASKS.md +716 -0
- package/docs/CYBERDROME_V2_SPEC.md +531 -0
- package/docs/ERROR_185_ANALYSIS.md +263 -0
- package/docs/PLATFORM_FEATURES_PROMPT.md +296 -0
- package/docs/SESSION_DETAIL_FEATURES.md +98 -0
- package/docs/_3d_multimedia_features.md +1080 -0
- package/docs/_frontend_features.md +1057 -0
- package/docs/_server_features.md +1077 -0
- package/docs/session-duplication-fixes.md +271 -0
- package/docs/session-terminal-linkage.md +412 -0
- package/package.json +63 -5
- package/public/apple-touch-icon.svg +21 -0
- package/public/css/dashboard.css +0 -161
- package/public/css/detail-panel.css +25 -0
- package/public/css/layout.css +18 -1
- package/public/css/modals.css +0 -26
- package/public/css/settings.css +0 -150
- package/public/css/terminal.css +34 -0
- package/public/favicon.svg +18 -0
- package/public/index.html +6 -26
- package/public/js/alarmManager.js +0 -21
- package/public/js/app.js +21 -7
- package/public/js/detailPanel.js +63 -64
- package/public/js/historyPanel.js +61 -55
- package/public/js/quickActions.js +132 -48
- package/public/js/sessionCard.js +5 -20
- package/public/js/sessionControls.js +8 -0
- package/public/js/settingsManager.js +0 -142
- package/server/apiRouter.js +60 -15
- package/server/apiRouter.ts +774 -0
- package/server/approvalDetector.ts +94 -0
- package/server/authManager.ts +144 -0
- package/server/autoIdleManager.ts +110 -0
- package/server/config.ts +121 -0
- package/server/constants.ts +150 -0
- package/server/db.ts +475 -0
- package/server/hookInstaller.d.ts +3 -0
- package/server/hookProcessor.ts +108 -0
- package/server/hookRouter.ts +18 -0
- package/server/hookStats.ts +116 -0
- package/server/index.js +15 -1
- package/server/index.ts +230 -0
- package/server/logger.ts +75 -0
- package/server/mqReader.ts +349 -0
- package/server/portManager.ts +55 -0
- package/server/processMonitor.ts +239 -0
- package/server/serverConfig.ts +29 -0
- package/server/sessionMatcher.js +17 -6
- package/server/sessionMatcher.ts +403 -0
- package/server/sessionStore.js +109 -3
- package/server/sessionStore.ts +1145 -0
- package/server/sshManager.js +167 -24
- package/server/sshManager.ts +671 -0
- package/server/teamManager.ts +289 -0
- package/server/wsManager.ts +200 -0
package/public/index.html
CHANGED
|
@@ -4,7 +4,9 @@
|
|
|
4
4
|
<meta charset="UTF-8">
|
|
5
5
|
<meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no, viewport-fit=cover">
|
|
6
6
|
<title>AI Agent Session Center</title>
|
|
7
|
-
<link rel="icon" type="image/svg+xml" href="
|
|
7
|
+
<link rel="icon" type="image/svg+xml" href="/favicon.svg">
|
|
8
|
+
<link rel="apple-touch-icon" href="/apple-touch-icon.svg">
|
|
9
|
+
<meta name="theme-color" content="#0a0a1a">
|
|
8
10
|
<link rel="preconnect" href="https://fonts.googleapis.com">
|
|
9
11
|
<link href="https://fonts.googleapis.com/css2?family=JetBrains+Mono:wght@300;400;500;700&display=swap" rel="stylesheet">
|
|
10
12
|
<link rel="stylesheet" href="css/main.css">
|
|
@@ -56,9 +58,6 @@
|
|
|
56
58
|
<div id="nav-actions-items">
|
|
57
59
|
<button id="qa-new-session" class="qa-btn qa-btn-terminal" title="Launch a new Claude session via SSH">+ NEW SESSION</button>
|
|
58
60
|
<button id="qa-quick-session" class="qa-btn qa-btn-quick" title="Quick launch with last config + label">⚡ QUICK</button>
|
|
59
|
-
<button id="qa-oneoff" class="qa-btn qa-btn-oneoff" title="Launch a one-off session (auto-review reminder)">ONEOFF</button>
|
|
60
|
-
<button id="qa-heavy" class="qa-btn qa-btn-heavy" title="Launch a high-priority session (auto-pinned, bold frame)">★ HEAVY</button>
|
|
61
|
-
<button id="qa-important" class="qa-btn qa-btn-important" title="Launch an important session (alert on completion)">⚠ IMPORTANT</button>
|
|
62
61
|
<button id="qa-new-group" class="qa-btn" title="Create a new group">+ NEW GROUP</button>
|
|
63
62
|
<div class="qa-separator"></div>
|
|
64
63
|
<button id="qa-mute-all" class="qa-btn" title="Mute all sessions">♫ MUTE ALL</button>
|
|
@@ -98,18 +97,6 @@
|
|
|
98
97
|
<span class="mobile-qa-icon">⚡</span>
|
|
99
98
|
<span class="mobile-qa-label">QUICK</span>
|
|
100
99
|
</button>
|
|
101
|
-
<button class="mobile-qa-item" data-action="oneoff" title="One-off Session">
|
|
102
|
-
<span class="mobile-qa-icon">🔄</span>
|
|
103
|
-
<span class="mobile-qa-label">ONEOFF</span>
|
|
104
|
-
</button>
|
|
105
|
-
<button class="mobile-qa-item" data-action="heavy" title="Heavy Session">
|
|
106
|
-
<span class="mobile-qa-icon">★</span>
|
|
107
|
-
<span class="mobile-qa-label">HEAVY</span>
|
|
108
|
-
</button>
|
|
109
|
-
<button class="mobile-qa-item" data-action="important" title="Important Session">
|
|
110
|
-
<span class="mobile-qa-icon">⚠</span>
|
|
111
|
-
<span class="mobile-qa-label">IMPORTANT</span>
|
|
112
|
-
</button>
|
|
113
100
|
<button class="mobile-qa-item" data-action="new-group" title="New Group">
|
|
114
101
|
<span class="mobile-qa-icon">📂</span>
|
|
115
102
|
<span class="mobile-qa-label">GROUP</span>
|
|
@@ -284,6 +271,9 @@
|
|
|
284
271
|
<h3 id="detail-project-name"></h3>
|
|
285
272
|
<div class="detail-title-row">
|
|
286
273
|
<input type="text" id="detail-title" placeholder="Add session title..." class="detail-title-input">
|
|
274
|
+
<button class="detail-title-edit-btn" id="detail-title-edit-btn" title="Edit title">
|
|
275
|
+
<svg width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M17 3a2.85 2.85 0 1 1 4 4L7.5 20.5 2 22l1.5-5.5Z"/><path d="m15 5 4 4"/></svg>
|
|
276
|
+
</button>
|
|
287
277
|
<input type="text" id="detail-label" list="detail-label-suggestions" placeholder="Label..." class="detail-label-input" autocomplete="off">
|
|
288
278
|
<datalist id="detail-label-suggestions"></datalist>
|
|
289
279
|
</div>
|
|
@@ -487,7 +477,6 @@
|
|
|
487
477
|
<div class="settings-tabs">
|
|
488
478
|
<button class="settings-tab active" data-settings-tab="appearance">Appearance</button>
|
|
489
479
|
<button class="settings-tab" data-settings-tab="sounds">Sounds</button>
|
|
490
|
-
<button class="settings-tab" data-settings-tab="labels">Labels</button>
|
|
491
480
|
<button class="settings-tab" data-settings-tab="advanced">Advanced</button>
|
|
492
481
|
</div>
|
|
493
482
|
|
|
@@ -764,15 +753,6 @@
|
|
|
764
753
|
</div>
|
|
765
754
|
</div>
|
|
766
755
|
|
|
767
|
-
<!-- Labels Tab -->
|
|
768
|
-
<div class="settings-tab-content" id="settings-tab-labels">
|
|
769
|
-
<div class="settings-section">
|
|
770
|
-
<h4>Built-in Label Alerts</h4>
|
|
771
|
-
<p class="settings-hint">Configure which sound and movement effect plays when a labeled session completes. These alerts fire on session end for ONEOFF, HEAVY, and IMPORTANT sessions.</p>
|
|
772
|
-
<div id="label-settings-grid" class="label-settings-grid"></div>
|
|
773
|
-
</div>
|
|
774
|
-
</div>
|
|
775
|
-
|
|
776
756
|
<!-- Advanced Tab -->
|
|
777
757
|
<div class="settings-tab-content" id="settings-tab-advanced">
|
|
778
758
|
<div class="settings-section">
|
|
@@ -86,24 +86,3 @@ export function checkAlarms(session, allSessions) {
|
|
|
86
86
|
approvalAlarmTimers.delete('input-' + session.sessionId);
|
|
87
87
|
}
|
|
88
88
|
}
|
|
89
|
-
|
|
90
|
-
// Label completion alerts
|
|
91
|
-
export function handleLabelAlerts(session) {
|
|
92
|
-
if (session.status !== 'ended' || isMuted(session.sessionId)) return;
|
|
93
|
-
|
|
94
|
-
const labelUpper = (session.label || '').toUpperCase();
|
|
95
|
-
const labelCfg = settingsManager.getLabelSettings();
|
|
96
|
-
if (labelCfg[labelUpper]) {
|
|
97
|
-
const cfg = labelCfg[labelUpper];
|
|
98
|
-
if (cfg.sound && cfg.sound !== 'none') soundManager.previewSound(cfg.sound);
|
|
99
|
-
if (cfg.movement && cfg.movement !== 'none') movementManager.trigger('alert', session.sessionId);
|
|
100
|
-
const card = document.querySelector(`.session-card[data-session-id="${session.sessionId}"] .css-robot`);
|
|
101
|
-
if (card && cfg.movement && cfg.movement !== 'none') {
|
|
102
|
-
card.removeAttribute('data-movement');
|
|
103
|
-
void card.offsetWidth;
|
|
104
|
-
card.setAttribute('data-movement', cfg.movement);
|
|
105
|
-
setTimeout(() => card.removeAttribute('data-movement'), 5000);
|
|
106
|
-
}
|
|
107
|
-
}
|
|
108
|
-
|
|
109
|
-
}
|
package/public/js/app.js
CHANGED
|
@@ -14,7 +14,7 @@ import { openDB, persistSessionUpdate, put, del, getAll, clear, getQueue, migrat
|
|
|
14
14
|
import { escapeHtml as utilEscapeHtml, debugLog, debugWarn } from './utils.js';
|
|
15
15
|
import { initKeyboardShortcuts } from './keyboardShortcuts.js';
|
|
16
16
|
import { initQuickActions, initShortcutsPanel } from './quickActions.js';
|
|
17
|
-
import { handleEventSounds, checkAlarms,
|
|
17
|
+
import { handleEventSounds, checkAlarms, clearAlarm } from './alarmManager.js';
|
|
18
18
|
|
|
19
19
|
let allSessions = {};
|
|
20
20
|
let hasRestoredSelection = false;
|
|
@@ -150,18 +150,35 @@ async function init() {
|
|
|
150
150
|
},
|
|
151
151
|
onSnapshotCb(sessions, teams) {
|
|
152
152
|
const serverIds = new Set(Object.keys(sessions));
|
|
153
|
+
// Also build a set of sessionId values to catch key/sessionId mismatches
|
|
154
|
+
const serverSessionIds = new Set();
|
|
155
|
+
for (const s of Object.values(sessions)) {
|
|
156
|
+
if (s.sessionId) serverSessionIds.add(s.sessionId);
|
|
157
|
+
}
|
|
153
158
|
for (const cachedId of Object.keys(allSessions)) {
|
|
154
|
-
|
|
159
|
+
// Remove cached entries not on the server (by key OR by sessionId)
|
|
160
|
+
if (!serverIds.has(cachedId) && !serverSessionIds.has(cachedId)) {
|
|
155
161
|
removeCard(cachedId);
|
|
156
162
|
robotManager.removeRobot(cachedId);
|
|
157
163
|
del('sessions', cachedId).catch(() => {});
|
|
158
164
|
delete allSessions[cachedId];
|
|
159
165
|
}
|
|
160
166
|
}
|
|
167
|
+
// Deduplicate: if server sends entries with different keys but same sessionId,
|
|
168
|
+
// keep only the most recent one to prevent duplicate cards.
|
|
169
|
+
const deduped = new Map();
|
|
161
170
|
for (const [id, session] of Object.entries(sessions)) {
|
|
162
|
-
|
|
171
|
+
const sid = session.sessionId || id;
|
|
172
|
+
const existing = deduped.get(sid);
|
|
173
|
+
if (!existing || (session.lastActivityAt || 0) > (existing.lastActivityAt || 0)) {
|
|
174
|
+
deduped.set(sid, session);
|
|
175
|
+
}
|
|
163
176
|
}
|
|
164
|
-
|
|
177
|
+
// Use sessionId as the allSessions key (not the server's Map key) for consistency
|
|
178
|
+
for (const [sid, session] of deduped) {
|
|
179
|
+
allSessions[sid] = session;
|
|
180
|
+
}
|
|
181
|
+
for (const session of deduped.values()) {
|
|
165
182
|
createOrUpdateCard(session);
|
|
166
183
|
robotManager.updateRobot(session);
|
|
167
184
|
persistSessionUpdate(session).catch(() => {});
|
|
@@ -230,9 +247,6 @@ async function init() {
|
|
|
230
247
|
addActivityEntry(session);
|
|
231
248
|
toggleEmptyState(Object.keys(allSessions).length === 0);
|
|
232
249
|
|
|
233
|
-
// Label completion alerts
|
|
234
|
-
handleLabelAlerts(session);
|
|
235
|
-
|
|
236
250
|
// SSH sessions persist as disconnected cards; non-SSH auto-remove
|
|
237
251
|
if (session.status === 'ended' && session.source !== 'ssh') {
|
|
238
252
|
setTimeout(() => {
|
package/public/js/detailPanel.js
CHANGED
|
@@ -219,10 +219,38 @@ export function populateDetailPanel(session) {
|
|
|
219
219
|
}
|
|
220
220
|
});
|
|
221
221
|
} else if (activeTab && activeTab.dataset.tab === 'terminal' && !session.terminalId) {
|
|
222
|
-
import('./terminalManager.js').then(tm =>
|
|
222
|
+
import('./terminalManager.js').then(tm => {
|
|
223
|
+
tm.detachTerminal();
|
|
224
|
+
_showExternalSourceHint(session);
|
|
225
|
+
});
|
|
223
226
|
}
|
|
224
227
|
}
|
|
225
228
|
|
|
229
|
+
const SOURCE_LABELS = {
|
|
230
|
+
vscode: 'VS Code', jetbrains: 'JetBrains', iterm: 'iTerm',
|
|
231
|
+
warp: 'Warp', kitty: 'Kitty', ghostty: 'Ghostty',
|
|
232
|
+
alacritty: 'Alacritty', wezterm: 'WezTerm', hyper: 'Hyper',
|
|
233
|
+
terminal: 'Terminal', tmux: 'tmux',
|
|
234
|
+
};
|
|
235
|
+
|
|
236
|
+
/** Show an informational hint in the terminal container for external-source sessions. */
|
|
237
|
+
function _showExternalSourceHint(session) {
|
|
238
|
+
const isExternal = session.source && session.source !== 'ssh';
|
|
239
|
+
const container = document.getElementById('terminal-container');
|
|
240
|
+
if (!container || !isExternal) return;
|
|
241
|
+
|
|
242
|
+
const label = SOURCE_LABELS[session.source] || session.source;
|
|
243
|
+
container.innerHTML = `
|
|
244
|
+
<div class="terminal-external-hint">
|
|
245
|
+
<div class="terminal-external-hint-icon">📡</div>
|
|
246
|
+
<div class="terminal-external-hint-title">External Session — ${escapeHtml(label)}</div>
|
|
247
|
+
<div class="terminal-external-hint-text">
|
|
248
|
+
This session was detected from <strong>${escapeHtml(label)}</strong> via hooks.<br>
|
|
249
|
+
No managed terminal is attached. Use the <strong>RECONNECT</strong> button to open a terminal and resume this session, or interact with it directly in ${escapeHtml(label)}.
|
|
250
|
+
</div>
|
|
251
|
+
</div>`;
|
|
252
|
+
}
|
|
253
|
+
|
|
226
254
|
/**
|
|
227
255
|
* Restore the previously selected session + active tab after a page refresh.
|
|
228
256
|
* Called from app.js after the snapshot arrives and sessions are populated.
|
|
@@ -328,77 +356,44 @@ export async function openSessionDetailFromHistory(sessionId) {
|
|
|
328
356
|
data.session.accentColor || null
|
|
329
357
|
);
|
|
330
358
|
|
|
331
|
-
// Populate conversation tab
|
|
359
|
+
// Populate conversation tab — tag raw data in-place, sort once
|
|
360
|
+
const prompts = data.prompts || [];
|
|
361
|
+
const toolCalls = data.tool_calls || [];
|
|
362
|
+
const responses = data.responses || [];
|
|
363
|
+
const events = data.events || [];
|
|
364
|
+
|
|
332
365
|
const histConvItems = [];
|
|
333
|
-
for (const p of (
|
|
334
|
-
|
|
335
|
-
}
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
}
|
|
339
|
-
for (const r of (data.responses || [])) {
|
|
340
|
-
histConvItems.push({ type: 'claude', text: r.textExcerpt, timestamp: r.timestamp });
|
|
341
|
-
}
|
|
342
|
-
histConvItems.sort((a, b) => b.timestamp - a.timestamp);
|
|
366
|
+
for (const p of prompts) histConvItems.push({ k: 'u', ts: p.timestamp, a: p.text, b: '' });
|
|
367
|
+
for (const t of toolCalls) histConvItems.push({ k: 't', ts: t.timestamp, a: t.toolName, b: t.toolInputSummary });
|
|
368
|
+
for (const r of responses) histConvItems.push({ k: 'c', ts: r.timestamp, a: r.textExcerpt, b: '' });
|
|
369
|
+
histConvItems.sort((a, b) => b.ts - a.ts);
|
|
370
|
+
const CONV_CFG = { u: ['conv-user', 'USER'], t: ['conv-tool', 'TOOL'], c: ['conv-claude', 'CLAUDE'] };
|
|
343
371
|
const histConvContainer = document.getElementById('detail-conversation');
|
|
344
372
|
histConvContainer.innerHTML = histConvItems.length > 0
|
|
345
|
-
? histConvItems.map(
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
</div>`;
|
|
351
|
-
} else if (item.type === 'tool') {
|
|
352
|
-
return `<div class="conv-entry conv-tool">
|
|
353
|
-
<div class="conv-header"><span class="conv-role">TOOL</span><span class="conv-time">${formatTime(item.timestamp)}</span><button class="conv-copy" title="Copy">COPY</button></div>
|
|
354
|
-
<span class="conv-tool-name">${escapeHtml(item.tool)}</span>
|
|
355
|
-
<span class="conv-tool-input">${escapeHtml(item.input)}</span>
|
|
356
|
-
</div>`;
|
|
357
|
-
} else {
|
|
358
|
-
return `<div class="conv-entry conv-claude">
|
|
359
|
-
<div class="conv-header"><span class="conv-role">CLAUDE</span><span class="conv-time">${formatTime(item.timestamp)}</span><button class="conv-copy" title="Copy">COPY</button></div>
|
|
360
|
-
<div class="conv-text">${escapeHtml(item.text)}</div>
|
|
361
|
-
</div>`;
|
|
362
|
-
}
|
|
373
|
+
? histConvItems.map(h => {
|
|
374
|
+
const [cls, role] = CONV_CFG[h.k];
|
|
375
|
+
const header = `<div class="conv-header"><span class="conv-role">${role}</span><span class="conv-time">${formatTime(h.ts)}</span><button class="conv-copy" title="Copy">COPY</button></div>`;
|
|
376
|
+
return h.k === 't'
|
|
377
|
+
? `<div class="conv-entry ${cls}">${header}<span class="conv-tool-name">${escapeHtml(h.a)}</span><span class="conv-tool-input">${escapeHtml(h.b)}</span></div>`
|
|
378
|
+
: `<div class="conv-entry ${cls}">${header}<div class="conv-text">${escapeHtml(h.a)}</div></div>`;
|
|
363
379
|
}).join('')
|
|
364
380
|
: '<div class="tab-empty">No conversation recorded</div>';
|
|
365
381
|
|
|
366
|
-
// Populate activity tab
|
|
367
|
-
const histActivityItems = [];
|
|
368
|
-
for (const t of (data.tool_calls || [])) {
|
|
369
|
-
histActivityItems.push({ kind: 'tool', tool: t.toolName, input: t.toolInputSummary, timestamp: t.timestamp });
|
|
370
|
-
}
|
|
371
|
-
for (const e of (data.events || [])) {
|
|
372
|
-
histActivityItems.push({ kind: 'event', type: e.eventType, detail: e.detail, timestamp: e.timestamp });
|
|
373
|
-
}
|
|
374
|
-
for (const r of (data.responses || [])) {
|
|
375
|
-
histActivityItems.push({ kind: 'response', text: r.textExcerpt || r.text, timestamp: r.timestamp });
|
|
376
|
-
}
|
|
377
|
-
histActivityItems.sort((a, b) => b.timestamp - a.timestamp);
|
|
382
|
+
// Populate activity tab — reuse raw arrays, no intermediate copies
|
|
378
383
|
const actEl = document.getElementById('detail-activity-log');
|
|
379
384
|
if (actEl) {
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
<span class="activity-
|
|
391
|
-
|
|
392
|
-
<span class="activity-detail">${escapeHtml(item.text)}</span>
|
|
393
|
-
</div>`;
|
|
394
|
-
} else {
|
|
395
|
-
return `<div class="activity-entry activity-event">
|
|
396
|
-
<span class="activity-time">${formatTime(item.timestamp)}</span>
|
|
397
|
-
<span class="activity-badge activity-badge-event">${escapeHtml(item.type)}</span>
|
|
398
|
-
<span class="activity-detail">${escapeHtml(item.detail)}</span>
|
|
399
|
-
</div>`;
|
|
400
|
-
}
|
|
401
|
-
}).join('')
|
|
385
|
+
const actItems = [];
|
|
386
|
+
for (const t of toolCalls) actItems.push({ k: 't', ts: t.timestamp, a: t.toolName, b: t.toolInputSummary });
|
|
387
|
+
for (const e of events) actItems.push({ k: 'e', ts: e.timestamp, a: e.eventType, b: e.detail });
|
|
388
|
+
for (const r of responses) actItems.push({ k: 'r', ts: r.timestamp, a: 'RESPONSE', b: r.textExcerpt || r.text });
|
|
389
|
+
actItems.sort((a, b) => b.ts - a.ts);
|
|
390
|
+
const ACT_CLS = { t: 'tool', e: 'event', r: 'response' };
|
|
391
|
+
actEl.innerHTML = actItems.length > 0
|
|
392
|
+
? actItems.map(h => `<div class="activity-entry activity-${ACT_CLS[h.k]}">
|
|
393
|
+
<span class="activity-time">${formatTime(h.ts)}</span>
|
|
394
|
+
<span class="activity-badge activity-badge-${ACT_CLS[h.k]}">${escapeHtml(h.a)}</span>
|
|
395
|
+
<span class="activity-detail">${escapeHtml(h.b)}</span>
|
|
396
|
+
</div>`).join('')
|
|
402
397
|
: '<div class="tab-empty">No activity recorded</div>';
|
|
403
398
|
}
|
|
404
399
|
|
|
@@ -492,6 +487,10 @@ export function initDetailPanelHandlers() {
|
|
|
492
487
|
const showBtn = !!(session.terminalId || session.lastTerminalId || session.status === 'ended');
|
|
493
488
|
rbtn.classList.toggle('hidden', !showBtn);
|
|
494
489
|
}
|
|
490
|
+
// Show external source hint when no terminal is attached
|
|
491
|
+
if (session && !session.terminalId) {
|
|
492
|
+
_showExternalSourceHint(session);
|
|
493
|
+
}
|
|
495
494
|
}
|
|
496
495
|
}
|
|
497
496
|
});
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { escapeHtml as _escapeHtml, formatDuration as _formatDuration, formatTime as _formatTime, sanitizeColor } from './utils.js';
|
|
2
|
+
import { switchTo } from './navController.js';
|
|
2
3
|
|
|
3
4
|
let currentPage = 1;
|
|
4
5
|
let debounceTimer = null;
|
|
@@ -73,19 +74,7 @@ async function loadSessions() {
|
|
|
73
74
|
|
|
74
75
|
const result = await apiFetch(`/api/db/sessions?${params}`);
|
|
75
76
|
|
|
76
|
-
|
|
77
|
-
const mapped = result.sessions.map(s => ({
|
|
78
|
-
id: s.id,
|
|
79
|
-
title: s.title || '',
|
|
80
|
-
project_name: s.project_name || '',
|
|
81
|
-
started_at: s.started_at,
|
|
82
|
-
ended_at: s.ended_at,
|
|
83
|
-
status: s.status,
|
|
84
|
-
total_prompts: s.total_prompts || 0,
|
|
85
|
-
total_tool_calls: s.total_tool_calls || 0,
|
|
86
|
-
git_branch: '',
|
|
87
|
-
}));
|
|
88
|
-
renderResults(mapped, result.total, result.page, result.pageSize);
|
|
77
|
+
renderResults(result.sessions, result.total, result.page, result.pageSize);
|
|
89
78
|
}
|
|
90
79
|
|
|
91
80
|
function renderResults(sessions, total, page, pageSize) {
|
|
@@ -104,15 +93,16 @@ function renderResults(sessions, total, page, pageSize) {
|
|
|
104
93
|
year: 'numeric', month: 'short', day: 'numeric',
|
|
105
94
|
hour: '2-digit', minute: '2-digit', hour12: false,
|
|
106
95
|
});
|
|
107
|
-
return `<div class="history-row" data-session-id="${s.id}">
|
|
108
|
-
<span class="history-title">${escapeHtml(s.title)}</span>
|
|
109
|
-
<span class="history-project">${escapeHtml(s.project_name)}</span>
|
|
96
|
+
return `<div class="history-row" data-session-id="${s.id}" data-project-path="${escapeHtml(s.project_path || '')}">
|
|
97
|
+
<span class="history-title">${escapeHtml(s.title || '')}</span>
|
|
98
|
+
<span class="history-project">${escapeHtml(s.project_name || '')}</span>
|
|
110
99
|
<span class="history-date">${date}</span>
|
|
111
100
|
<span class="history-duration">${duration}</span>
|
|
112
101
|
<span class="history-status ${s.status}">${s.status.toUpperCase()}</span>
|
|
113
|
-
<span class="history-prompts">${s.total_prompts} prompts</span>
|
|
114
|
-
<span class="history-tools">${s.total_tool_calls} tools</span>
|
|
102
|
+
<span class="history-prompts">${s.total_prompts || 0} prompts</span>
|
|
103
|
+
<span class="history-tools">${s.total_tool_calls || 0} tools</span>
|
|
115
104
|
<span class="history-branch">${escapeHtml(s.git_branch || '')}</span>
|
|
105
|
+
<button class="history-resume" title="Resume session">▶</button>
|
|
116
106
|
<button class="history-delete" title="Delete session">×</button>
|
|
117
107
|
</div>`;
|
|
118
108
|
}).join('');
|
|
@@ -120,11 +110,44 @@ function renderResults(sessions, total, page, pageSize) {
|
|
|
120
110
|
// Click handler for rows
|
|
121
111
|
container.querySelectorAll('.history-row').forEach(row => {
|
|
122
112
|
row.addEventListener('click', (e) => {
|
|
123
|
-
if (e.target.closest('.history-delete')) return;
|
|
113
|
+
if (e.target.closest('.history-delete') || e.target.closest('.history-resume')) return;
|
|
124
114
|
openHistoryDetail(row.dataset.sessionId);
|
|
125
115
|
});
|
|
126
116
|
});
|
|
127
117
|
|
|
118
|
+
// Resume button handler — create terminal with claude --resume
|
|
119
|
+
container.querySelectorAll('.history-resume').forEach(btn => {
|
|
120
|
+
btn.addEventListener('click', async (e) => {
|
|
121
|
+
e.stopPropagation();
|
|
122
|
+
const row = btn.closest('.history-row');
|
|
123
|
+
const sid = row.dataset.sessionId;
|
|
124
|
+
if (!sid || !/^[a-zA-Z0-9_-]+$/.test(sid)) return;
|
|
125
|
+
const projectPath = row.dataset.projectPath || '';
|
|
126
|
+
btn.disabled = true;
|
|
127
|
+
btn.textContent = '...';
|
|
128
|
+
try {
|
|
129
|
+
const body = { command: `claude --resume ${sid}` };
|
|
130
|
+
if (projectPath) body.workingDir = projectPath;
|
|
131
|
+
const resp = await fetch('/api/terminals', {
|
|
132
|
+
method: 'POST',
|
|
133
|
+
headers: { 'Content-Type': 'application/json' },
|
|
134
|
+
body: JSON.stringify(body),
|
|
135
|
+
});
|
|
136
|
+
const result = await resp.json();
|
|
137
|
+
if (result.ok) {
|
|
138
|
+
switchTo('live');
|
|
139
|
+
} else {
|
|
140
|
+
alert(result.error || 'Failed to resume session');
|
|
141
|
+
}
|
|
142
|
+
} catch (err) {
|
|
143
|
+
alert('Resume error: ' + err.message);
|
|
144
|
+
} finally {
|
|
145
|
+
btn.disabled = false;
|
|
146
|
+
btn.textContent = '\u25B6';
|
|
147
|
+
}
|
|
148
|
+
});
|
|
149
|
+
});
|
|
150
|
+
|
|
128
151
|
// Delete button handler — delete from server DB
|
|
129
152
|
container.querySelectorAll('.history-delete').forEach(btn => {
|
|
130
153
|
btn.addEventListener('click', async (e) => {
|
|
@@ -196,8 +219,8 @@ async function openHistoryDetail(sessionId) {
|
|
|
196
219
|
|
|
197
220
|
const sess = data.session;
|
|
198
221
|
const prompts = data.prompts || [];
|
|
199
|
-
const responses =
|
|
200
|
-
const tools =
|
|
222
|
+
const responses = data.responses || [];
|
|
223
|
+
const tools = data.tool_calls || [];
|
|
201
224
|
const events = data.events || [];
|
|
202
225
|
|
|
203
226
|
// Populate header
|
|
@@ -236,47 +259,30 @@ async function openHistoryDetail(sessionId) {
|
|
|
236
259
|
});
|
|
237
260
|
}
|
|
238
261
|
|
|
239
|
-
// Conversation tab (interleaved prompts + responses)
|
|
262
|
+
// Conversation tab (interleaved prompts + responses — single pass)
|
|
240
263
|
const convoEl = document.getElementById('detail-conversation');
|
|
241
|
-
const allEntries = [
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
convoEl.innerHTML = allEntries.map(e => {
|
|
246
|
-
const cls = e.type === 'prompt' ? 'prompt-entry' : 'response-entry';
|
|
247
|
-
return `<div class="${cls}">
|
|
264
|
+
const allEntries = [];
|
|
265
|
+
for (const p of prompts) allEntries.push({ type: 'prompt', timestamp: p.timestamp, text: p.text });
|
|
266
|
+
for (const r of responses) allEntries.push({ type: 'response', timestamp: r.timestamp, text: r.text_excerpt || r.text || '' });
|
|
267
|
+
allEntries.sort((a, b) => a.timestamp - b.timestamp);
|
|
268
|
+
convoEl.innerHTML = allEntries.map(e => `<div class="${e.type}-entry">
|
|
248
269
|
<span class="${e.type}-time">${formatTime(e.timestamp)}</span>
|
|
249
270
|
<div class="${e.type}-text">${escapeHtml(e.text)}</div>
|
|
250
|
-
</div
|
|
251
|
-
}).join('');
|
|
271
|
+
</div>`).join('');
|
|
252
272
|
|
|
253
|
-
// Activity tab (merged tool calls + events)
|
|
254
|
-
const histItems = [];
|
|
255
|
-
for (const t of tools) {
|
|
256
|
-
histItems.push({ kind: 'tool', tool: t.tool, input: t.input, timestamp: t.timestamp });
|
|
257
|
-
}
|
|
258
|
-
for (const e of events) {
|
|
259
|
-
histItems.push({ kind: 'event', type: e.event_type, detail: e.detail, timestamp: e.timestamp });
|
|
260
|
-
}
|
|
261
|
-
histItems.sort((a, b) => b.timestamp - a.timestamp);
|
|
273
|
+
// Activity tab (merged tool calls + events — tag in-place, sort once, render)
|
|
262
274
|
const actEl = document.getElementById('detail-activity-log');
|
|
263
275
|
if (actEl) {
|
|
276
|
+
const histItems = [];
|
|
277
|
+
for (const t of tools) histItems.push({ k: 't', ts: t.timestamp, a: t.tool_name, b: t.tool_input_summary || '' });
|
|
278
|
+
for (const e of events) histItems.push({ k: 'e', ts: e.timestamp, a: e.event_type, b: e.detail });
|
|
279
|
+
histItems.sort((a, b) => b.ts - a.ts);
|
|
264
280
|
actEl.innerHTML = histItems.length > 0
|
|
265
|
-
? histItems.map(
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
<span class="activity-
|
|
269
|
-
|
|
270
|
-
<span class="activity-detail">${escapeHtml(item.input)}</span>
|
|
271
|
-
</div>`;
|
|
272
|
-
} else {
|
|
273
|
-
return `<div class="activity-entry activity-event">
|
|
274
|
-
<span class="activity-time">${formatTime(item.timestamp)}</span>
|
|
275
|
-
<span class="activity-badge activity-badge-event">${escapeHtml(item.type)}</span>
|
|
276
|
-
<span class="activity-detail">${escapeHtml(item.detail)}</span>
|
|
277
|
-
</div>`;
|
|
278
|
-
}
|
|
279
|
-
}).join('')
|
|
281
|
+
? histItems.map(h => `<div class="activity-entry activity-${h.k === 't' ? 'tool' : 'event'}">
|
|
282
|
+
<span class="activity-time">${formatTime(h.ts)}</span>
|
|
283
|
+
<span class="activity-badge activity-badge-${h.k === 't' ? 'tool' : 'event'}">${escapeHtml(h.a)}</span>
|
|
284
|
+
<span class="activity-detail">${escapeHtml(h.b)}</span>
|
|
285
|
+
</div>`).join('')
|
|
280
286
|
: '<div class="tab-empty">No activity recorded</div>';
|
|
281
287
|
}
|
|
282
288
|
|