claudehq 1.0.1 → 1.0.2
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/bin/cli.js +2 -2
- package/lib/core/claude-events.js +308 -0
- package/lib/core/config.js +79 -0
- package/lib/core/event-bus.js +127 -0
- package/lib/core/sse.js +96 -0
- package/lib/core/watchers.js +127 -0
- package/lib/data/conversation.js +195 -0
- package/lib/data/plans.js +247 -0
- package/lib/data/tasks.js +263 -0
- package/lib/data/todos.js +205 -0
- package/lib/index.js +400 -0
- package/lib/routes/api.js +611 -0
- package/lib/server.js +778 -79
- package/lib/sessions/manager.js +870 -0
- package/package.json +10 -4
- package/public/index.html +6765 -0
package/lib/server.js
CHANGED
|
@@ -11,6 +11,7 @@ const TODOS_DIR = path.join(os.homedir(), '.claude', 'todos');
|
|
|
11
11
|
const PLANS_DIR = path.join(os.homedir(), '.claude', 'plans');
|
|
12
12
|
const PROJECTS_DIR = path.join(os.homedir(), '.claude', 'projects');
|
|
13
13
|
const CUSTOM_NAMES_FILE = path.join(os.homedir(), '.claude', 'tasks', 'session-names.json');
|
|
14
|
+
const HIDDEN_SESSIONS_FILE = path.join(os.homedir(), '.claude', 'tasks-board', 'hidden-sessions.json');
|
|
14
15
|
const TMUX_SESSION = process.env.TMUX_SESSION || 'main';
|
|
15
16
|
|
|
16
17
|
// ============================================================================
|
|
@@ -440,19 +441,32 @@ function saveManagedSessions() {
|
|
|
440
441
|
}
|
|
441
442
|
|
|
442
443
|
// Get all managed sessions with enriched data
|
|
443
|
-
function getManagedSessions() {
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
444
|
+
function getManagedSessions(includeHidden = false) {
|
|
445
|
+
const customNames = loadCustomNames();
|
|
446
|
+
const hiddenSessions = loadHiddenSessions();
|
|
447
|
+
|
|
448
|
+
return Array.from(managedSessions.values())
|
|
449
|
+
.filter(session => {
|
|
450
|
+
if (includeHidden) return true;
|
|
451
|
+
// Filter out hidden sessions
|
|
452
|
+
return !hiddenSessions.includes(session.id) &&
|
|
453
|
+
!hiddenSessions.includes(session.claudeSessionId);
|
|
454
|
+
})
|
|
455
|
+
.map(session => {
|
|
456
|
+
// Fetch conversation metadata if we have a Claude session ID
|
|
457
|
+
let firstPrompt = null;
|
|
458
|
+
if (session.claudeSessionId) {
|
|
459
|
+
const metadata = getSessionMetadata(session.claudeSessionId);
|
|
460
|
+
firstPrompt = metadata.firstPrompt;
|
|
461
|
+
}
|
|
462
|
+
// Check for custom name (by session ID or claudeSessionId)
|
|
463
|
+
const customName = customNames[session.id] || customNames[session.claudeSessionId] || null;
|
|
464
|
+
return {
|
|
465
|
+
...session,
|
|
466
|
+
name: customName || session.name,
|
|
467
|
+
firstPrompt,
|
|
468
|
+
};
|
|
469
|
+
});
|
|
456
470
|
}
|
|
457
471
|
|
|
458
472
|
// Get a single managed session
|
|
@@ -526,6 +540,13 @@ function discoverSession(event) {
|
|
|
526
540
|
function updateSessionFromEvent(event) {
|
|
527
541
|
if (!event.sessionId) return;
|
|
528
542
|
|
|
543
|
+
// Auto-unhide: If this session is hidden but receiving activity, unhide it
|
|
544
|
+
const hiddenIds = loadHiddenSessions();
|
|
545
|
+
if (hiddenIds.includes(event.sessionId)) {
|
|
546
|
+
console.log(` Auto-unhiding session ${event.sessionId.substring(0, 8)}... (received activity)`);
|
|
547
|
+
unhideSession(event.sessionId);
|
|
548
|
+
}
|
|
549
|
+
|
|
529
550
|
// Try to find managed session by Claude session ID (exact match)
|
|
530
551
|
let managedSession = findManagedSessionByClaudeId(event.sessionId);
|
|
531
552
|
|
|
@@ -649,6 +670,122 @@ function checkWorkingTimeout() {
|
|
|
649
670
|
}
|
|
650
671
|
}
|
|
651
672
|
|
|
673
|
+
// ============================================================================
|
|
674
|
+
// Permission Prompt Detection - Detects when Claude needs user input
|
|
675
|
+
// ============================================================================
|
|
676
|
+
|
|
677
|
+
const pendingPermissions = new Map(); // sessionId -> { tool, detectedAt }
|
|
678
|
+
|
|
679
|
+
/**
|
|
680
|
+
* Parse tmux output to detect Claude Code permission prompts.
|
|
681
|
+
* Returns { tool, context } if a permission prompt is detected, null otherwise.
|
|
682
|
+
*/
|
|
683
|
+
function detectPermissionPrompt(output) {
|
|
684
|
+
const lines = output.split('\n');
|
|
685
|
+
|
|
686
|
+
// Look for "Do you want to proceed?" or "Would you like to proceed?" in recent output
|
|
687
|
+
let proceedLineIdx = -1;
|
|
688
|
+
for (let i = lines.length - 1; i >= Math.max(0, lines.length - 30); i--) {
|
|
689
|
+
if (/(Do you want|Would you like) to proceed\?/i.test(lines[i])) {
|
|
690
|
+
proceedLineIdx = i;
|
|
691
|
+
break;
|
|
692
|
+
}
|
|
693
|
+
}
|
|
694
|
+
|
|
695
|
+
if (proceedLineIdx === -1) return null;
|
|
696
|
+
|
|
697
|
+
// Verify this is a real Claude Code prompt by checking for the footer or selector
|
|
698
|
+
let hasFooter = false;
|
|
699
|
+
let hasSelector = false;
|
|
700
|
+
for (let i = proceedLineIdx + 1; i < Math.min(lines.length, proceedLineIdx + 15); i++) {
|
|
701
|
+
if (/Esc to cancel|ctrl-g to edit/i.test(lines[i])) {
|
|
702
|
+
hasFooter = true;
|
|
703
|
+
break;
|
|
704
|
+
}
|
|
705
|
+
if (/^\s*❯/.test(lines[i])) {
|
|
706
|
+
hasSelector = true;
|
|
707
|
+
}
|
|
708
|
+
}
|
|
709
|
+
|
|
710
|
+
if (!hasFooter && !hasSelector) return null;
|
|
711
|
+
|
|
712
|
+
// Find the tool name
|
|
713
|
+
let tool = 'Permission';
|
|
714
|
+
for (let i = proceedLineIdx; i >= Math.max(0, proceedLineIdx - 20); i--) {
|
|
715
|
+
const toolMatch = lines[i].match(/[●◐·]\s*(\w+)\s*\(/);
|
|
716
|
+
if (toolMatch) {
|
|
717
|
+
tool = toolMatch[1];
|
|
718
|
+
break;
|
|
719
|
+
}
|
|
720
|
+
const cmdMatch = lines[i].match(/^\s*(Bash|Read|Write|Edit|Grep|Glob|Task|WebFetch|WebSearch)\s+\w+/i);
|
|
721
|
+
if (cmdMatch) {
|
|
722
|
+
tool = cmdMatch[1];
|
|
723
|
+
break;
|
|
724
|
+
}
|
|
725
|
+
}
|
|
726
|
+
|
|
727
|
+
return { tool, context: lines.slice(Math.max(0, proceedLineIdx - 5), proceedLineIdx + 8).join('\n') };
|
|
728
|
+
}
|
|
729
|
+
|
|
730
|
+
/**
|
|
731
|
+
* Poll a session for permission prompts via tmux
|
|
732
|
+
*/
|
|
733
|
+
function pollPermissions(session) {
|
|
734
|
+
if (!session.tmuxSession) return;
|
|
735
|
+
|
|
736
|
+
execFile('tmux', ['capture-pane', '-t', session.tmuxSession, '-p', '-S', '-50'],
|
|
737
|
+
{ timeout: 2000, maxBuffer: 1024 * 1024 },
|
|
738
|
+
(error, stdout) => {
|
|
739
|
+
if (error) return;
|
|
740
|
+
|
|
741
|
+
const prompt = detectPermissionPrompt(stdout);
|
|
742
|
+
const existing = pendingPermissions.get(session.id);
|
|
743
|
+
|
|
744
|
+
if (prompt && !existing) {
|
|
745
|
+
// New permission prompt detected
|
|
746
|
+
pendingPermissions.set(session.id, {
|
|
747
|
+
tool: prompt.tool,
|
|
748
|
+
detectedAt: Date.now(),
|
|
749
|
+
});
|
|
750
|
+
|
|
751
|
+
console.log(` Permission prompt detected for "${session.name}": ${prompt.tool}`);
|
|
752
|
+
|
|
753
|
+
// Update session status to WAITING
|
|
754
|
+
if (session.status !== SESSION_STATUS.WAITING) {
|
|
755
|
+
session.status = SESSION_STATUS.WAITING;
|
|
756
|
+
session.currentTool = prompt.tool;
|
|
757
|
+
broadcastManagedSessions();
|
|
758
|
+
saveManagedSessions();
|
|
759
|
+
}
|
|
760
|
+
} else if (!prompt && existing) {
|
|
761
|
+
// Permission prompt was resolved
|
|
762
|
+
pendingPermissions.delete(session.id);
|
|
763
|
+
console.log(` Permission prompt resolved for "${session.name}"`);
|
|
764
|
+
|
|
765
|
+
// Reset session status to WORKING (user responded, Claude continues)
|
|
766
|
+
if (session.status === SESSION_STATUS.WAITING) {
|
|
767
|
+
session.status = SESSION_STATUS.WORKING;
|
|
768
|
+
session.currentTool = undefined;
|
|
769
|
+
broadcastManagedSessions();
|
|
770
|
+
saveManagedSessions();
|
|
771
|
+
}
|
|
772
|
+
}
|
|
773
|
+
}
|
|
774
|
+
);
|
|
775
|
+
}
|
|
776
|
+
|
|
777
|
+
/**
|
|
778
|
+
* Poll all active sessions for permission prompts
|
|
779
|
+
*/
|
|
780
|
+
function checkPermissionPrompts() {
|
|
781
|
+
for (const session of managedSessions.values()) {
|
|
782
|
+
// Only poll sessions that have a tmux session and aren't offline
|
|
783
|
+
if (session.tmuxSession && session.status !== SESSION_STATUS.OFFLINE) {
|
|
784
|
+
pollPermissions(session);
|
|
785
|
+
}
|
|
786
|
+
}
|
|
787
|
+
}
|
|
788
|
+
|
|
652
789
|
// Broadcast managed sessions to all SSE clients
|
|
653
790
|
function broadcastManagedSessions() {
|
|
654
791
|
const data = JSON.stringify({ type: 'sessions', sessions: getManagedSessions() });
|
|
@@ -663,8 +800,11 @@ function startSessionHealthChecks() {
|
|
|
663
800
|
setInterval(checkSessionHealth, 5000);
|
|
664
801
|
// Working timeout check every 10 seconds
|
|
665
802
|
setInterval(checkWorkingTimeout, 10000);
|
|
803
|
+
// Permission prompt polling every 1 second (time-sensitive)
|
|
804
|
+
setInterval(checkPermissionPrompts, 1000);
|
|
666
805
|
// Run initial health check
|
|
667
806
|
checkSessionHealth();
|
|
807
|
+
console.log(' Status monitoring started (health: 5s, timeout: 10s, permissions: 1s)');
|
|
668
808
|
}
|
|
669
809
|
|
|
670
810
|
// Create a new managed session (spawns tmux with claude)
|
|
@@ -879,12 +1019,118 @@ function loadCustomNames() {
|
|
|
879
1019
|
// Save custom session names
|
|
880
1020
|
function saveCustomNames(names) {
|
|
881
1021
|
try {
|
|
1022
|
+
const dir = path.dirname(CUSTOM_NAMES_FILE);
|
|
1023
|
+
if (!fs.existsSync(dir)) {
|
|
1024
|
+
fs.mkdirSync(dir, { recursive: true });
|
|
1025
|
+
}
|
|
882
1026
|
fs.writeFileSync(CUSTOM_NAMES_FILE, JSON.stringify(names, null, 2));
|
|
883
1027
|
} catch (e) {
|
|
884
1028
|
console.error('Failed to save custom names:', e.message);
|
|
885
1029
|
}
|
|
886
1030
|
}
|
|
887
1031
|
|
|
1032
|
+
// Load hidden session IDs
|
|
1033
|
+
function loadHiddenSessions() {
|
|
1034
|
+
try {
|
|
1035
|
+
if (fs.existsSync(HIDDEN_SESSIONS_FILE)) {
|
|
1036
|
+
return JSON.parse(fs.readFileSync(HIDDEN_SESSIONS_FILE, 'utf-8'));
|
|
1037
|
+
}
|
|
1038
|
+
} catch (e) { /* ignore */ }
|
|
1039
|
+
return [];
|
|
1040
|
+
}
|
|
1041
|
+
|
|
1042
|
+
// Save hidden session IDs
|
|
1043
|
+
function saveHiddenSessions(hiddenIds) {
|
|
1044
|
+
try {
|
|
1045
|
+
const dir = path.dirname(HIDDEN_SESSIONS_FILE);
|
|
1046
|
+
if (!fs.existsSync(dir)) {
|
|
1047
|
+
fs.mkdirSync(dir, { recursive: true });
|
|
1048
|
+
}
|
|
1049
|
+
fs.writeFileSync(HIDDEN_SESSIONS_FILE, JSON.stringify(hiddenIds, null, 2));
|
|
1050
|
+
} catch (e) {
|
|
1051
|
+
console.error('Failed to save hidden sessions:', e.message);
|
|
1052
|
+
}
|
|
1053
|
+
}
|
|
1054
|
+
|
|
1055
|
+
// Hide a session
|
|
1056
|
+
function hideSession(sessionId) {
|
|
1057
|
+
const hidden = loadHiddenSessions();
|
|
1058
|
+
if (!hidden.includes(sessionId)) {
|
|
1059
|
+
hidden.push(sessionId);
|
|
1060
|
+
saveHiddenSessions(hidden);
|
|
1061
|
+
}
|
|
1062
|
+
return { success: true };
|
|
1063
|
+
}
|
|
1064
|
+
|
|
1065
|
+
// Unhide a session
|
|
1066
|
+
function unhideSession(sessionId) {
|
|
1067
|
+
let hidden = loadHiddenSessions();
|
|
1068
|
+
hidden = hidden.filter(id => id !== sessionId);
|
|
1069
|
+
saveHiddenSessions(hidden);
|
|
1070
|
+
return { success: true };
|
|
1071
|
+
}
|
|
1072
|
+
|
|
1073
|
+
// Permanently delete a session (removes all data)
|
|
1074
|
+
function permanentDeleteSession(sessionId) {
|
|
1075
|
+
// Find session by ID or claudeSessionId
|
|
1076
|
+
let session = managedSessions.get(sessionId);
|
|
1077
|
+
let managedId = sessionId;
|
|
1078
|
+
|
|
1079
|
+
if (!session) {
|
|
1080
|
+
// Try to find by claudeSessionId
|
|
1081
|
+
for (const [id, s] of managedSessions) {
|
|
1082
|
+
if (s.claudeSessionId === sessionId) {
|
|
1083
|
+
session = s;
|
|
1084
|
+
managedId = id;
|
|
1085
|
+
break;
|
|
1086
|
+
}
|
|
1087
|
+
}
|
|
1088
|
+
}
|
|
1089
|
+
|
|
1090
|
+
if (!session) {
|
|
1091
|
+
return { success: false, error: 'Session not found' };
|
|
1092
|
+
}
|
|
1093
|
+
|
|
1094
|
+
const claudeSessionId = session.claudeSessionId;
|
|
1095
|
+
|
|
1096
|
+
// 1. Remove from managedSessions
|
|
1097
|
+
managedSessions.delete(managedId);
|
|
1098
|
+
|
|
1099
|
+
// 2. Remove from claudeToManagedMap
|
|
1100
|
+
if (claudeSessionId) {
|
|
1101
|
+
claudeToManagedMap.delete(claudeSessionId);
|
|
1102
|
+
}
|
|
1103
|
+
|
|
1104
|
+
// 3. Remove from hidden sessions
|
|
1105
|
+
let hidden = loadHiddenSessions();
|
|
1106
|
+
hidden = hidden.filter(id => id !== managedId && id !== claudeSessionId);
|
|
1107
|
+
saveHiddenSessions(hidden);
|
|
1108
|
+
|
|
1109
|
+
// 4. Remove any custom names
|
|
1110
|
+
const names = loadCustomNames();
|
|
1111
|
+
delete names[managedId];
|
|
1112
|
+
if (claudeSessionId) {
|
|
1113
|
+
delete names[claudeSessionId];
|
|
1114
|
+
}
|
|
1115
|
+
saveCustomNames(names);
|
|
1116
|
+
|
|
1117
|
+
// 5. Save sessions
|
|
1118
|
+
saveManagedSessions();
|
|
1119
|
+
|
|
1120
|
+
// 6. Broadcast updated list
|
|
1121
|
+
broadcastManagedSessions();
|
|
1122
|
+
|
|
1123
|
+
console.log(` Permanently deleted session: ${session.name} (${managedId})`);
|
|
1124
|
+
|
|
1125
|
+
return { success: true };
|
|
1126
|
+
}
|
|
1127
|
+
|
|
1128
|
+
// Check if session is hidden (by ID or claudeSessionId)
|
|
1129
|
+
function isSessionHidden(sessionId, claudeSessionId) {
|
|
1130
|
+
const hidden = loadHiddenSessions();
|
|
1131
|
+
return hidden.includes(sessionId) || (claudeSessionId && hidden.includes(claudeSessionId));
|
|
1132
|
+
}
|
|
1133
|
+
|
|
888
1134
|
// Rename a session
|
|
889
1135
|
function renameSession(sessionId, newName) {
|
|
890
1136
|
const names = loadCustomNames();
|
|
@@ -897,6 +1143,23 @@ function renameSession(sessionId, newName) {
|
|
|
897
1143
|
}
|
|
898
1144
|
saveCustomNames(names);
|
|
899
1145
|
|
|
1146
|
+
// Also update managed session if it exists (check both by ID and by claudeSessionId)
|
|
1147
|
+
let managedSession = managedSessions.get(sessionId);
|
|
1148
|
+
if (!managedSession) {
|
|
1149
|
+
// Try to find by claudeSessionId
|
|
1150
|
+
for (const [id, session] of managedSessions) {
|
|
1151
|
+
if (session.claudeSessionId === sessionId) {
|
|
1152
|
+
managedSession = session;
|
|
1153
|
+
break;
|
|
1154
|
+
}
|
|
1155
|
+
}
|
|
1156
|
+
}
|
|
1157
|
+
if (managedSession) {
|
|
1158
|
+
managedSession.name = newName?.trim() || managedSession.name;
|
|
1159
|
+
saveManagedSessions();
|
|
1160
|
+
broadcastManagedSessions();
|
|
1161
|
+
}
|
|
1162
|
+
|
|
900
1163
|
// Emit event through EventBus
|
|
901
1164
|
eventBus.emit(EventTypes.SESSION_RENAMED, { sessionId, newName: newName?.trim(), previousName });
|
|
902
1165
|
|
|
@@ -2006,6 +2269,120 @@ const HTML = `<!DOCTYPE html>
|
|
|
2006
2269
|
flex-direction: column;
|
|
2007
2270
|
}
|
|
2008
2271
|
|
|
2272
|
+
/* Hidden Sessions Section */
|
|
2273
|
+
.hidden-sessions-section {
|
|
2274
|
+
margin-top: 8px;
|
|
2275
|
+
border-top: 1px solid var(--border-primary);
|
|
2276
|
+
padding-top: 8px;
|
|
2277
|
+
}
|
|
2278
|
+
|
|
2279
|
+
.hidden-sessions-section.collapsed .hidden-sessions-list {
|
|
2280
|
+
display: none;
|
|
2281
|
+
}
|
|
2282
|
+
|
|
2283
|
+
.hidden-sessions-header {
|
|
2284
|
+
display: flex;
|
|
2285
|
+
align-items: center;
|
|
2286
|
+
justify-content: space-between;
|
|
2287
|
+
padding: 4px 12px;
|
|
2288
|
+
font-size: 11px;
|
|
2289
|
+
font-weight: 600;
|
|
2290
|
+
color: var(--text-tertiary);
|
|
2291
|
+
text-transform: uppercase;
|
|
2292
|
+
letter-spacing: 0.5px;
|
|
2293
|
+
cursor: pointer;
|
|
2294
|
+
}
|
|
2295
|
+
|
|
2296
|
+
.hidden-sessions-header:hover {
|
|
2297
|
+
color: var(--text-secondary);
|
|
2298
|
+
}
|
|
2299
|
+
|
|
2300
|
+
.hidden-sessions-header .collapse-icon {
|
|
2301
|
+
font-size: 8px;
|
|
2302
|
+
transition: transform 0.15s;
|
|
2303
|
+
}
|
|
2304
|
+
|
|
2305
|
+
.hidden-sessions-section.collapsed .collapse-icon {
|
|
2306
|
+
transform: rotate(-90deg);
|
|
2307
|
+
}
|
|
2308
|
+
|
|
2309
|
+
.hidden-sessions-list {
|
|
2310
|
+
display: flex;
|
|
2311
|
+
flex-direction: column;
|
|
2312
|
+
padding: 4px;
|
|
2313
|
+
}
|
|
2314
|
+
|
|
2315
|
+
.hidden-session-item {
|
|
2316
|
+
display: flex;
|
|
2317
|
+
align-items: center;
|
|
2318
|
+
gap: 8px;
|
|
2319
|
+
padding: 4px 8px;
|
|
2320
|
+
border-radius: 4px;
|
|
2321
|
+
color: var(--text-tertiary);
|
|
2322
|
+
font-size: 12px;
|
|
2323
|
+
}
|
|
2324
|
+
|
|
2325
|
+
.hidden-session-item:hover {
|
|
2326
|
+
background: var(--bg-hover);
|
|
2327
|
+
}
|
|
2328
|
+
|
|
2329
|
+
.hidden-session-name {
|
|
2330
|
+
flex: 1;
|
|
2331
|
+
white-space: nowrap;
|
|
2332
|
+
overflow: hidden;
|
|
2333
|
+
text-overflow: ellipsis;
|
|
2334
|
+
}
|
|
2335
|
+
|
|
2336
|
+
.unhide-btn {
|
|
2337
|
+
background: none;
|
|
2338
|
+
border: none;
|
|
2339
|
+
width: 20px;
|
|
2340
|
+
height: 20px;
|
|
2341
|
+
padding: 0;
|
|
2342
|
+
display: flex;
|
|
2343
|
+
align-items: center;
|
|
2344
|
+
justify-content: center;
|
|
2345
|
+
cursor: pointer;
|
|
2346
|
+
border-radius: 4px;
|
|
2347
|
+
color: var(--text-tertiary);
|
|
2348
|
+
opacity: 0;
|
|
2349
|
+
transition: opacity 0.15s;
|
|
2350
|
+
}
|
|
2351
|
+
|
|
2352
|
+
.hidden-session-item:hover .unhide-btn {
|
|
2353
|
+
opacity: 1;
|
|
2354
|
+
}
|
|
2355
|
+
|
|
2356
|
+
.unhide-btn:hover {
|
|
2357
|
+
background: var(--bg-tertiary);
|
|
2358
|
+
color: var(--accent-color);
|
|
2359
|
+
}
|
|
2360
|
+
|
|
2361
|
+
.hidden-session-item .delete-btn {
|
|
2362
|
+
background: none;
|
|
2363
|
+
border: none;
|
|
2364
|
+
width: 20px;
|
|
2365
|
+
height: 20px;
|
|
2366
|
+
padding: 0;
|
|
2367
|
+
display: flex;
|
|
2368
|
+
align-items: center;
|
|
2369
|
+
justify-content: center;
|
|
2370
|
+
cursor: pointer;
|
|
2371
|
+
border-radius: 4px;
|
|
2372
|
+
color: var(--text-tertiary);
|
|
2373
|
+
opacity: 0;
|
|
2374
|
+
transition: opacity 0.15s;
|
|
2375
|
+
}
|
|
2376
|
+
|
|
2377
|
+
.hidden-session-item:hover .delete-btn {
|
|
2378
|
+
opacity: 1;
|
|
2379
|
+
}
|
|
2380
|
+
|
|
2381
|
+
.hidden-session-item .delete-btn:hover {
|
|
2382
|
+
background: var(--bg-tertiary);
|
|
2383
|
+
color: #ef4444;
|
|
2384
|
+
}
|
|
2385
|
+
|
|
2009
2386
|
/* Session Group - Expandable container */
|
|
2010
2387
|
.session-group {
|
|
2011
2388
|
margin-bottom: 2px;
|
|
@@ -2076,16 +2453,17 @@ const HTML = `<!DOCTYPE html>
|
|
|
2076
2453
|
background: #666;
|
|
2077
2454
|
}
|
|
2078
2455
|
|
|
2079
|
-
|
|
2456
|
+
/* Status colors: Green=working, Orange=waiting, Red=idle, Grey=offline */
|
|
2080
2457
|
.session-group-icon .status-dot.working {
|
|
2081
|
-
background: #
|
|
2458
|
+
background: #4ade80;
|
|
2082
2459
|
animation: status-pulse 1s infinite;
|
|
2083
2460
|
}
|
|
2084
2461
|
.session-group-icon .status-dot.waiting {
|
|
2085
2462
|
background: #fb923c;
|
|
2086
2463
|
animation: status-pulse 0.5s infinite;
|
|
2087
2464
|
}
|
|
2088
|
-
.session-group-icon .status-dot.
|
|
2465
|
+
.session-group-icon .status-dot.idle { background: #ef4444; }
|
|
2466
|
+
.session-group-icon .status-dot.offline { background: #6b7280; }
|
|
2089
2467
|
|
|
2090
2468
|
@keyframes status-pulse {
|
|
2091
2469
|
0%, 100% { opacity: 1; }
|
|
@@ -4252,6 +4630,15 @@ const HTML = `<!DOCTYPE html>
|
|
|
4252
4630
|
<!-- Managed Sessions List -->
|
|
4253
4631
|
<div class="managed-sessions-list" id="managed-sessions-list"></div>
|
|
4254
4632
|
|
|
4633
|
+
<!-- Hidden Sessions Section -->
|
|
4634
|
+
<div class="hidden-sessions-section" id="hidden-sessions-section" style="display: none;">
|
|
4635
|
+
<div class="hidden-sessions-header" onclick="this.parentElement.classList.toggle('collapsed')">
|
|
4636
|
+
<span>Hidden</span>
|
|
4637
|
+
<span class="collapse-icon">▼</span>
|
|
4638
|
+
</div>
|
|
4639
|
+
<div class="hidden-sessions-list"></div>
|
|
4640
|
+
</div>
|
|
4641
|
+
|
|
4255
4642
|
<!-- Create Session Button -->
|
|
4256
4643
|
<div style="padding: 8px;">
|
|
4257
4644
|
<button class="create-session-btn" onclick="openCreateSessionModal()" title="Launch a new Claude Code session in tmux">
|
|
@@ -4520,6 +4907,7 @@ const HTML = `<!DOCTYPE html>
|
|
|
4520
4907
|
|
|
4521
4908
|
// Managed sessions state
|
|
4522
4909
|
let managedSessions = [];
|
|
4910
|
+
let hiddenSessions = [];
|
|
4523
4911
|
let selectedManagedSession = null; // null = all sessions
|
|
4524
4912
|
|
|
4525
4913
|
// Todos state
|
|
@@ -4560,6 +4948,20 @@ const HTML = `<!DOCTYPE html>
|
|
|
4560
4948
|
}
|
|
4561
4949
|
}
|
|
4562
4950
|
|
|
4951
|
+
// Fetch hidden sessions from API
|
|
4952
|
+
async function loadHiddenSessions() {
|
|
4953
|
+
try {
|
|
4954
|
+
const res = await fetch('/api/hidden-sessions');
|
|
4955
|
+
const data = await res.json();
|
|
4956
|
+
if (data.ok) {
|
|
4957
|
+
hiddenSessions = data.sessions;
|
|
4958
|
+
renderHiddenSessions();
|
|
4959
|
+
}
|
|
4960
|
+
} catch (e) {
|
|
4961
|
+
console.error('Failed to load hidden sessions:', e);
|
|
4962
|
+
}
|
|
4963
|
+
}
|
|
4964
|
+
|
|
4563
4965
|
// Track expanded state for each session
|
|
4564
4966
|
const expandedSessions = new Set();
|
|
4565
4967
|
|
|
@@ -4624,7 +5026,7 @@ const HTML = `<!DOCTYPE html>
|
|
|
4624
5026
|
'<span class="session-group-name">' + escapeHtml(session.name) + '</span>' +
|
|
4625
5027
|
'<div class="session-group-actions">' +
|
|
4626
5028
|
'<button onclick="event.stopPropagation(); renameManagedSession(\\'' + session.id + '\\')" title="Rename">✎</button>' +
|
|
4627
|
-
'<button class="delete-btn" onclick="event.stopPropagation();
|
|
5029
|
+
'<button class="delete-btn" onclick="event.stopPropagation(); hideSession(\\'' + session.id + '\\')" title="Hide session"><svg width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M17.94 17.94A10.07 10.07 0 0 1 12 20c-7 0-11-8-11-8a18.45 18.45 0 0 1 5.06-5.94M9.9 4.24A9.12 9.12 0 0 1 12 4c7 0 11 8 11 8a18.5 18.5 0 0 1-2.16 3.19m-6.72-1.07a3 3 0 1 1-4.24-4.24"/><line x1="1" y1="1" x2="23" y2="23"/></svg></button>' +
|
|
4628
5030
|
'</div>' +
|
|
4629
5031
|
'</div>' +
|
|
4630
5032
|
'<div class="session-views">' +
|
|
@@ -4686,8 +5088,15 @@ const HTML = `<!DOCTYPE html>
|
|
|
4686
5088
|
return days + 'd ago';
|
|
4687
5089
|
}
|
|
4688
5090
|
|
|
5091
|
+
// Track pending data load to prevent race conditions
|
|
5092
|
+
let pendingSessionLoad = null;
|
|
5093
|
+
|
|
4689
5094
|
// Select a managed session (or null for all)
|
|
4690
5095
|
function selectManagedSession(sessionId) {
|
|
5096
|
+
// Cancel any pending load
|
|
5097
|
+
const loadId = Date.now();
|
|
5098
|
+
pendingSessionLoad = loadId;
|
|
5099
|
+
|
|
4691
5100
|
selectedManagedSession = sessionId;
|
|
4692
5101
|
|
|
4693
5102
|
// Auto-expand the selected session
|
|
@@ -4706,35 +5115,62 @@ const HTML = `<!DOCTYPE html>
|
|
|
4706
5115
|
|
|
4707
5116
|
const session = sessionId ? managedSessions.find(s => s.id === sessionId) : null;
|
|
4708
5117
|
|
|
4709
|
-
//
|
|
5118
|
+
// Update header title immediately
|
|
5119
|
+
const titleEl = document.getElementById('header-title');
|
|
5120
|
+
if (titleEl) {
|
|
5121
|
+
if (session) {
|
|
5122
|
+
const viewLabel = currentSessionView === 'todos' ? 'Todos' :
|
|
5123
|
+
currentSessionView === 'plans' ? 'Plans' : 'Tasks';
|
|
5124
|
+
titleEl.textContent = session.name + ' - ' + viewLabel;
|
|
5125
|
+
} else {
|
|
5126
|
+
titleEl.textContent = 'All Sessions';
|
|
5127
|
+
}
|
|
5128
|
+
}
|
|
5129
|
+
|
|
5130
|
+
// Load all data for this session
|
|
5131
|
+
loadSessionData(session, loadId);
|
|
5132
|
+
}
|
|
5133
|
+
|
|
5134
|
+
// Load all data for a session (activity, tasks/todos/plans based on view)
|
|
5135
|
+
async function loadSessionData(session, loadId) {
|
|
5136
|
+
// Check if this load was cancelled by a newer selection
|
|
5137
|
+
if (pendingSessionLoad !== loadId) return;
|
|
5138
|
+
|
|
5139
|
+
// 1. Always set activity feed filter and load activity FIRST (most visible)
|
|
4710
5140
|
if (session && session.claudeSessionId) {
|
|
4711
5141
|
ActivityFeedManager.setFilter(session.claudeSessionId);
|
|
4712
|
-
// Open conversation panel
|
|
5142
|
+
// Open conversation panel if not open (skipDataLoad=true because we load data ourselves below)
|
|
4713
5143
|
if (!conversationOpen) {
|
|
4714
|
-
toggleConversation();
|
|
5144
|
+
toggleConversation(true);
|
|
4715
5145
|
}
|
|
5146
|
+
// Load activity feed from cached events (instant)
|
|
4716
5147
|
ActivityFeedManager.loadForSession(session.claudeSessionId);
|
|
4717
|
-
} else if (
|
|
5148
|
+
} else if (session) {
|
|
4718
5149
|
ActivityFeedManager.setFilter('__none__');
|
|
4719
5150
|
} else {
|
|
4720
5151
|
ActivityFeedManager.setFilter(null);
|
|
5152
|
+
// Load activity for all sessions
|
|
5153
|
+
ActivityFeedManager.loadForSession(null);
|
|
4721
5154
|
}
|
|
4722
5155
|
|
|
4723
|
-
//
|
|
5156
|
+
// Check again after filter setup
|
|
5157
|
+
if (pendingSessionLoad !== loadId) return;
|
|
5158
|
+
|
|
5159
|
+
// 2. Load view-specific data
|
|
4724
5160
|
if (session && session.claudeSessionId) {
|
|
4725
5161
|
if (currentSessionView === 'todos') {
|
|
4726
|
-
loadTodosForSession(session.claudeSessionId);
|
|
5162
|
+
await loadTodosForSession(session.claudeSessionId);
|
|
4727
5163
|
} else if (currentSessionView === 'plans') {
|
|
4728
|
-
loadPlansForSession(session.claudeSessionId);
|
|
5164
|
+
await loadPlansForSession(session.claudeSessionId);
|
|
4729
5165
|
} else {
|
|
4730
|
-
loadTasksForSession(session.claudeSessionId);
|
|
5166
|
+
await loadTasksForSession(session.claudeSessionId);
|
|
4731
5167
|
}
|
|
4732
|
-
} else if (!
|
|
5168
|
+
} else if (!session) {
|
|
4733
5169
|
// Show all tasks/todos
|
|
4734
|
-
currentSessionView = 'tasks';
|
|
4735
|
-
loadData(true);
|
|
5170
|
+
currentSessionView = 'tasks';
|
|
5171
|
+
await loadData(true);
|
|
4736
5172
|
} else {
|
|
4737
|
-
// No
|
|
5173
|
+
// No claudeSessionId - show empty state
|
|
4738
5174
|
if (currentSessionView === 'todos') {
|
|
4739
5175
|
currentTodos = [];
|
|
4740
5176
|
renderTodosView();
|
|
@@ -4746,18 +5182,6 @@ const HTML = `<!DOCTYPE html>
|
|
|
4746
5182
|
renderTasks();
|
|
4747
5183
|
}
|
|
4748
5184
|
}
|
|
4749
|
-
|
|
4750
|
-
// Update header title
|
|
4751
|
-
const titleEl = document.getElementById('header-title');
|
|
4752
|
-
if (titleEl) {
|
|
4753
|
-
if (session) {
|
|
4754
|
-
const viewLabel = currentSessionView === 'todos' ? 'Todos' :
|
|
4755
|
-
currentSessionView === 'plans' ? 'Plans' : 'Tasks';
|
|
4756
|
-
titleEl.textContent = session.name + ' - ' + viewLabel;
|
|
4757
|
-
} else {
|
|
4758
|
-
titleEl.textContent = 'All Sessions';
|
|
4759
|
-
}
|
|
4760
|
-
}
|
|
4761
5185
|
}
|
|
4762
5186
|
|
|
4763
5187
|
// Load tasks specifically for a Claude session ID
|
|
@@ -4832,13 +5256,59 @@ const HTML = `<!DOCTYPE html>
|
|
|
4832
5256
|
}
|
|
4833
5257
|
}
|
|
4834
5258
|
|
|
4835
|
-
//
|
|
4836
|
-
async function
|
|
4837
|
-
|
|
4838
|
-
|
|
5259
|
+
// Hide managed session
|
|
5260
|
+
async function hideSession(sessionId) {
|
|
5261
|
+
try {
|
|
5262
|
+
const res = await fetch('/api/managed-sessions/' + sessionId + '/hide', {
|
|
5263
|
+
method: 'POST'
|
|
5264
|
+
});
|
|
5265
|
+
const data = await res.json();
|
|
5266
|
+
if (data.ok) {
|
|
5267
|
+
if (selectedManagedSession === sessionId) {
|
|
5268
|
+
selectManagedSession(null);
|
|
5269
|
+
}
|
|
5270
|
+
loadManagedSessions();
|
|
5271
|
+
loadHiddenSessions();
|
|
5272
|
+
showToast('Session hidden');
|
|
5273
|
+
}
|
|
5274
|
+
} catch (e) {
|
|
5275
|
+
console.error('Failed to hide session:', e);
|
|
5276
|
+
}
|
|
5277
|
+
}
|
|
4839
5278
|
|
|
5279
|
+
// Unhide session
|
|
5280
|
+
async function unhideSession(sessionId) {
|
|
4840
5281
|
try {
|
|
4841
|
-
const res = await fetch('/api/managed-sessions/' + sessionId, {
|
|
5282
|
+
const res = await fetch('/api/managed-sessions/' + sessionId + '/unhide', {
|
|
5283
|
+
method: 'POST'
|
|
5284
|
+
});
|
|
5285
|
+
const data = await res.json();
|
|
5286
|
+
if (data.ok) {
|
|
5287
|
+
loadManagedSessions();
|
|
5288
|
+
loadHiddenSessions();
|
|
5289
|
+
showToast('Session restored');
|
|
5290
|
+
}
|
|
5291
|
+
} catch (e) {
|
|
5292
|
+
console.error('Failed to unhide session:', e);
|
|
5293
|
+
}
|
|
5294
|
+
}
|
|
5295
|
+
|
|
5296
|
+
// Permanently delete session (with confirmation)
|
|
5297
|
+
async function permanentDeleteSession(sessionId, sessionName) {
|
|
5298
|
+
const confirmed = confirm(
|
|
5299
|
+
'PERMANENTLY DELETE SESSION\\n\\n' +
|
|
5300
|
+
'Session: ' + (sessionName || sessionId) + '\\n\\n' +
|
|
5301
|
+
'This will permanently remove:\\n' +
|
|
5302
|
+
'- Session data and settings\\n' +
|
|
5303
|
+
'- Custom name (if set)\\n\\n' +
|
|
5304
|
+
'This action cannot be undone.\\n\\n' +
|
|
5305
|
+
'Are you sure you want to delete this session?'
|
|
5306
|
+
);
|
|
5307
|
+
|
|
5308
|
+
if (!confirmed) return;
|
|
5309
|
+
|
|
5310
|
+
try {
|
|
5311
|
+
const res = await fetch('/api/managed-sessions/' + sessionId + '/permanent', {
|
|
4842
5312
|
method: 'DELETE'
|
|
4843
5313
|
});
|
|
4844
5314
|
const data = await res.json();
|
|
@@ -4847,12 +5317,52 @@ const HTML = `<!DOCTYPE html>
|
|
|
4847
5317
|
selectManagedSession(null);
|
|
4848
5318
|
}
|
|
4849
5319
|
loadManagedSessions();
|
|
5320
|
+
loadHiddenSessions();
|
|
5321
|
+
showToast('Session permanently deleted');
|
|
5322
|
+
} else {
|
|
5323
|
+
showToast('Error: ' + (data.error || 'Failed to delete'));
|
|
4850
5324
|
}
|
|
4851
5325
|
} catch (e) {
|
|
4852
5326
|
console.error('Failed to delete session:', e);
|
|
5327
|
+
showToast('Error deleting session');
|
|
4853
5328
|
}
|
|
4854
5329
|
}
|
|
4855
5330
|
|
|
5331
|
+
// Render hidden sessions section
|
|
5332
|
+
function renderHiddenSessions() {
|
|
5333
|
+
const container = document.getElementById('hidden-sessions-section');
|
|
5334
|
+
if (!container) return;
|
|
5335
|
+
|
|
5336
|
+
if (hiddenSessions.length === 0) {
|
|
5337
|
+
container.style.display = 'none';
|
|
5338
|
+
return;
|
|
5339
|
+
}
|
|
5340
|
+
|
|
5341
|
+
container.style.display = 'block';
|
|
5342
|
+
const list = container.querySelector('.hidden-sessions-list');
|
|
5343
|
+
if (!list) return;
|
|
5344
|
+
|
|
5345
|
+
const eyeIcon = '<svg width="12" height="12" 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>';
|
|
5346
|
+
|
|
5347
|
+
const trashIcon = '<svg width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M3 6h18M19 6v14a2 2 0 0 1-2 2H7a2 2 0 0 1-2-2V6m3 0V4a2 2 0 0 1 2-2h4a2 2 0 0 1 2 2v2"/></svg>';
|
|
5348
|
+
|
|
5349
|
+
list.innerHTML = hiddenSessions.map(session => {
|
|
5350
|
+
// Generate color for icon
|
|
5351
|
+
const colors = ['#6366f1', '#8b5cf6', '#ec4899', '#f43f5e', '#f97316', '#eab308', '#22c55e', '#14b8a6', '#06b6d4', '#3b82f6'];
|
|
5352
|
+
const colorIndex = session.name.split('').reduce((acc, char) => acc + char.charCodeAt(0), 0) % colors.length;
|
|
5353
|
+
const bgColor = colors[colorIndex];
|
|
5354
|
+
const initials = session.name.substring(0, 2).toUpperCase();
|
|
5355
|
+
const escapedName = escapeHtml(session.name).replace(/'/g, "\\\\'");
|
|
5356
|
+
|
|
5357
|
+
return '<div class="hidden-session-item">' +
|
|
5358
|
+
'<div class="session-group-icon" style="background: ' + bgColor + '; color: white; width: 20px; height: 20px; font-size: 9px;">' + initials + '</div>' +
|
|
5359
|
+
'<span class="hidden-session-name">' + escapeHtml(session.name) + '</span>' +
|
|
5360
|
+
'<button class="unhide-btn" onclick="unhideSession(\\'' + session.id + '\\')" title="Unhide session">' + eyeIcon + '</button>' +
|
|
5361
|
+
'<button class="delete-btn" onclick="permanentDeleteSession(\\'' + session.id + '\\', \\'' + escapedName + '\\')" title="Permanently delete">' + trashIcon + '</button>' +
|
|
5362
|
+
'</div>';
|
|
5363
|
+
}).join('');
|
|
5364
|
+
}
|
|
5365
|
+
|
|
4856
5366
|
// Restart managed session
|
|
4857
5367
|
async function restartManagedSession(sessionId) {
|
|
4858
5368
|
try {
|
|
@@ -5350,17 +5860,29 @@ const HTML = `<!DOCTYPE html>
|
|
|
5350
5860
|
this.clear();
|
|
5351
5861
|
},
|
|
5352
5862
|
|
|
5353
|
-
// Load existing events for a session
|
|
5863
|
+
// Load existing events for a session (or all sessions if null)
|
|
5354
5864
|
loadForSession(claudeSessionId) {
|
|
5355
5865
|
this.clear();
|
|
5356
|
-
|
|
5357
|
-
|
|
5866
|
+
|
|
5867
|
+
// Load events from claudeEvents array
|
|
5868
|
+
let sessionEvents;
|
|
5869
|
+
if (claudeSessionId === null) {
|
|
5870
|
+
// All sessions - show all events
|
|
5871
|
+
sessionEvents = claudeEvents;
|
|
5872
|
+
} else {
|
|
5873
|
+
// Filter by specific session
|
|
5874
|
+
sessionEvents = claudeEvents.filter(e => e.sessionId === claudeSessionId);
|
|
5875
|
+
}
|
|
5876
|
+
|
|
5358
5877
|
// Show most recent 50 events
|
|
5359
5878
|
const recent = sessionEvents.slice(-50);
|
|
5360
5879
|
for (const event of recent) {
|
|
5361
5880
|
this.add(event, true); // true = skip scroll
|
|
5362
5881
|
}
|
|
5363
5882
|
this.scrollToBottom();
|
|
5883
|
+
|
|
5884
|
+
// Log for debugging
|
|
5885
|
+
console.log('[ActivityFeed] Loaded', recent.length, 'events for session:', claudeSessionId || 'all');
|
|
5364
5886
|
},
|
|
5365
5887
|
|
|
5366
5888
|
showThinking(sessionId) {
|
|
@@ -7940,12 +8462,16 @@ const HTML = `<!DOCTYPE html>
|
|
|
7940
8462
|
}
|
|
7941
8463
|
|
|
7942
8464
|
// Conversation toggle - uses ConversationManager
|
|
7943
|
-
|
|
8465
|
+
// Note: This only toggles visibility. Data loading is handled by loadSessionData()
|
|
8466
|
+
// to avoid loading stale data when called during session selection.
|
|
8467
|
+
function toggleConversation(skipDataLoad = false) {
|
|
7944
8468
|
conversationOpen = !conversationOpen;
|
|
7945
8469
|
document.getElementById('conversation-panel').classList.toggle('open', conversationOpen);
|
|
7946
8470
|
document.getElementById('conv-toggle').classList.toggle('active', conversationOpen);
|
|
7947
8471
|
|
|
7948
|
-
if (
|
|
8472
|
+
// Only load data if explicitly requested (e.g., user clicking the toggle button)
|
|
8473
|
+
// When called from loadSessionData, skipDataLoad is true because loadSessionData handles the loading
|
|
8474
|
+
if (!skipDataLoad && conversationOpen && currentSession) {
|
|
7949
8475
|
// Load both conversation messages and activity feed
|
|
7950
8476
|
ConversationManager.load(currentSession.sessionId);
|
|
7951
8477
|
ActivityFeedManager.loadForSession(currentSession.sessionId);
|
|
@@ -8028,27 +8554,135 @@ const HTML = `<!DOCTYPE html>
|
|
|
8028
8554
|
AttentionSystem.requestPermission();
|
|
8029
8555
|
setupPromptForm();
|
|
8030
8556
|
setupCreateTaskForm();
|
|
8031
|
-
loadData();
|
|
8032
|
-
connectSSE();
|
|
8033
8557
|
setupDragDropListeners();
|
|
8034
8558
|
|
|
8035
|
-
//
|
|
8036
|
-
|
|
8559
|
+
// Initialize app with proper loading order
|
|
8560
|
+
// 1. Load all data first (tasks, events, sessions)
|
|
8561
|
+
// 2. Then restore session selection
|
|
8562
|
+
// 3. Finally connect SSE for real-time updates
|
|
8563
|
+
initializeApp();
|
|
8037
8564
|
|
|
8038
|
-
|
|
8039
|
-
|
|
8565
|
+
async function initializeApp() {
|
|
8566
|
+
console.log('[Init] Starting app initialization...');
|
|
8040
8567
|
|
|
8041
|
-
|
|
8042
|
-
|
|
8568
|
+
try {
|
|
8569
|
+
// Step 1: Load all data in parallel
|
|
8570
|
+
const [tasksResult, eventsResult, sessionsResult, todosResult, plansResult, hiddenResult] = await Promise.all([
|
|
8571
|
+
loadData(true), // Load tasks
|
|
8572
|
+
loadClaudeEventsAsync(), // Load claude events
|
|
8573
|
+
loadManagedSessionsAsync(), // Load managed sessions
|
|
8574
|
+
loadAllTodosAsync(), // Load todos
|
|
8575
|
+
loadAllPlansDataAsync(), // Load plans
|
|
8576
|
+
loadHiddenSessionsAsync() // Load hidden sessions
|
|
8577
|
+
]);
|
|
8578
|
+
|
|
8579
|
+
console.log('[Init] All data loaded');
|
|
8580
|
+
|
|
8581
|
+
// Step 2: Restore session selection (AFTER data is loaded)
|
|
8582
|
+
const savedSessionId = localStorage.getItem('selectedManagedSession');
|
|
8583
|
+
if (savedSessionId && managedSessions.find(s => s.id === savedSessionId)) {
|
|
8584
|
+
console.log('[Init] Restoring session:', savedSessionId);
|
|
8585
|
+
selectManagedSession(savedSessionId);
|
|
8586
|
+
}
|
|
8587
|
+
|
|
8588
|
+
// Step 3: Connect SSE for real-time updates (AFTER session is restored)
|
|
8589
|
+
connectSSE();
|
|
8590
|
+
console.log('[Init] SSE connected');
|
|
8043
8591
|
|
|
8044
|
-
|
|
8045
|
-
|
|
8046
|
-
|
|
8047
|
-
|
|
8048
|
-
if (savedSessionId && managedSessions.find(s => s.id === savedSessionId)) {
|
|
8049
|
-
selectManagedSession(savedSessionId);
|
|
8592
|
+
} catch (e) {
|
|
8593
|
+
console.error('[Init] Initialization error:', e);
|
|
8594
|
+
// Still try to connect SSE even if data loading failed
|
|
8595
|
+
connectSSE();
|
|
8050
8596
|
}
|
|
8051
|
-
}
|
|
8597
|
+
}
|
|
8598
|
+
|
|
8599
|
+
// Async versions of data loading functions that return promises
|
|
8600
|
+
async function loadClaudeEventsAsync() {
|
|
8601
|
+
try {
|
|
8602
|
+
const res = await fetch('/api/claude-events');
|
|
8603
|
+
const data = await res.json();
|
|
8604
|
+
if (data.ok && data.events) {
|
|
8605
|
+
claudeEvents.length = 0; // Clear existing
|
|
8606
|
+
claudeEvents.push(...data.events);
|
|
8607
|
+
}
|
|
8608
|
+
return true;
|
|
8609
|
+
} catch (e) {
|
|
8610
|
+
console.error('Failed to load Claude events:', e);
|
|
8611
|
+
return false;
|
|
8612
|
+
}
|
|
8613
|
+
}
|
|
8614
|
+
|
|
8615
|
+
async function loadManagedSessionsAsync() {
|
|
8616
|
+
try {
|
|
8617
|
+
const res = await fetch('/api/managed-sessions');
|
|
8618
|
+
const data = await res.json();
|
|
8619
|
+
if (data.ok) {
|
|
8620
|
+
managedSessions = data.sessions;
|
|
8621
|
+
renderManagedSessions();
|
|
8622
|
+
}
|
|
8623
|
+
return true;
|
|
8624
|
+
} catch (e) {
|
|
8625
|
+
console.error('Failed to load managed sessions:', e);
|
|
8626
|
+
return false;
|
|
8627
|
+
}
|
|
8628
|
+
}
|
|
8629
|
+
|
|
8630
|
+
async function loadAllTodosAsync() {
|
|
8631
|
+
try {
|
|
8632
|
+
const res = await fetch('/api/todos');
|
|
8633
|
+
const data = await res.json();
|
|
8634
|
+
if (data.ok && data.todos) {
|
|
8635
|
+
todosBySession = {};
|
|
8636
|
+
for (const todo of data.todos) {
|
|
8637
|
+
if (todo.sessionId) {
|
|
8638
|
+
todosBySession[todo.sessionId] = todo;
|
|
8639
|
+
}
|
|
8640
|
+
}
|
|
8641
|
+
}
|
|
8642
|
+
return true;
|
|
8643
|
+
} catch (e) {
|
|
8644
|
+
console.error('Failed to load todos:', e);
|
|
8645
|
+
return false;
|
|
8646
|
+
}
|
|
8647
|
+
}
|
|
8648
|
+
|
|
8649
|
+
async function loadAllPlansDataAsync() {
|
|
8650
|
+
try {
|
|
8651
|
+
const res = await fetch('/api/plans');
|
|
8652
|
+
const data = await res.json();
|
|
8653
|
+
if (data.ok && data.plans) {
|
|
8654
|
+
allPlans = data.plans;
|
|
8655
|
+
plansBySession = {};
|
|
8656
|
+
for (const plan of allPlans) {
|
|
8657
|
+
if (plan.sessionId) {
|
|
8658
|
+
if (!plansBySession[plan.sessionId]) {
|
|
8659
|
+
plansBySession[plan.sessionId] = [];
|
|
8660
|
+
}
|
|
8661
|
+
plansBySession[plan.sessionId].push(plan);
|
|
8662
|
+
}
|
|
8663
|
+
}
|
|
8664
|
+
}
|
|
8665
|
+
return true;
|
|
8666
|
+
} catch (e) {
|
|
8667
|
+
console.error('Failed to load plans:', e);
|
|
8668
|
+
return false;
|
|
8669
|
+
}
|
|
8670
|
+
}
|
|
8671
|
+
|
|
8672
|
+
async function loadHiddenSessionsAsync() {
|
|
8673
|
+
try {
|
|
8674
|
+
const res = await fetch('/api/hidden-sessions');
|
|
8675
|
+
const data = await res.json();
|
|
8676
|
+
if (data.ok) {
|
|
8677
|
+
hiddenSessions = data.sessions;
|
|
8678
|
+
renderHiddenSessions();
|
|
8679
|
+
}
|
|
8680
|
+
return true;
|
|
8681
|
+
} catch (e) {
|
|
8682
|
+
console.error('Failed to load hidden sessions:', e);
|
|
8683
|
+
return false;
|
|
8684
|
+
}
|
|
8685
|
+
}
|
|
8052
8686
|
|
|
8053
8687
|
// Load Claude events from the server
|
|
8054
8688
|
async function loadClaudeEvents() {
|
|
@@ -8453,21 +9087,86 @@ const server = http.createServer((req, res) => {
|
|
|
8453
9087
|
return;
|
|
8454
9088
|
}
|
|
8455
9089
|
|
|
8456
|
-
//
|
|
8457
|
-
const
|
|
8458
|
-
if (
|
|
8459
|
-
|
|
8460
|
-
|
|
8461
|
-
|
|
8462
|
-
|
|
8463
|
-
|
|
8464
|
-
|
|
8465
|
-
|
|
9090
|
+
// POST /api/managed-sessions/:id/hide - Hide a session
|
|
9091
|
+
const hideManagedMatch = url.pathname.match(/^\/api\/managed-sessions\/([^/]+)\/hide$/);
|
|
9092
|
+
if (hideManagedMatch && req.method === 'POST') {
|
|
9093
|
+
const session = managedSessions.get(hideManagedMatch[1]);
|
|
9094
|
+
if (session) {
|
|
9095
|
+
// Hide by claudeSessionId if available, otherwise by managed session ID
|
|
9096
|
+
const idToHide = session.claudeSessionId || hideManagedMatch[1];
|
|
9097
|
+
hideSession(idToHide);
|
|
9098
|
+
broadcastManagedSessions();
|
|
9099
|
+
res.writeHead(200, { 'Content-Type': 'application/json' });
|
|
9100
|
+
res.end(JSON.stringify({ ok: true }));
|
|
9101
|
+
} else {
|
|
9102
|
+
res.writeHead(404, { 'Content-Type': 'application/json' });
|
|
9103
|
+
res.end(JSON.stringify({ ok: false, error: 'Session not found' }));
|
|
9104
|
+
}
|
|
9105
|
+
return;
|
|
9106
|
+
}
|
|
9107
|
+
|
|
9108
|
+
// POST /api/managed-sessions/:id/unhide - Unhide a session
|
|
9109
|
+
const unhideManagedMatch = url.pathname.match(/^\/api\/managed-sessions\/([^/]+)\/unhide$/);
|
|
9110
|
+
if (unhideManagedMatch && req.method === 'POST') {
|
|
9111
|
+
unhideSession(unhideManagedMatch[1]);
|
|
9112
|
+
broadcastManagedSessions();
|
|
9113
|
+
res.writeHead(200, { 'Content-Type': 'application/json' });
|
|
9114
|
+
res.end(JSON.stringify({ ok: true }));
|
|
9115
|
+
return;
|
|
9116
|
+
}
|
|
9117
|
+
|
|
9118
|
+
// GET /api/hidden-sessions - Get list of hidden sessions with names
|
|
9119
|
+
if (url.pathname === '/api/hidden-sessions' && req.method === 'GET') {
|
|
9120
|
+
const hiddenIds = loadHiddenSessions();
|
|
9121
|
+
const customNames = loadCustomNames();
|
|
9122
|
+
const sessions = hiddenIds.map(id => {
|
|
9123
|
+
// Try to get name from managedSessions or customNames
|
|
9124
|
+
let name = customNames[id] || id.substring(0, 8);
|
|
9125
|
+
|
|
9126
|
+
// Check if there's a managed session with this claudeSessionId
|
|
9127
|
+
for (const [managedId, session] of managedSessions) {
|
|
9128
|
+
if (session.claudeSessionId === id || managedId === id) {
|
|
9129
|
+
name = session.name || name;
|
|
9130
|
+
break;
|
|
9131
|
+
}
|
|
8466
9132
|
}
|
|
8467
|
-
|
|
8468
|
-
|
|
8469
|
-
res.end(JSON.stringify({ ok: false, error: e.message }));
|
|
9133
|
+
|
|
9134
|
+
return { id, name };
|
|
8470
9135
|
});
|
|
9136
|
+
res.writeHead(200, { 'Content-Type': 'application/json' });
|
|
9137
|
+
res.end(JSON.stringify({ ok: true, sessions }));
|
|
9138
|
+
return;
|
|
9139
|
+
}
|
|
9140
|
+
|
|
9141
|
+
// DELETE /api/managed-sessions/:id - Delete session (legacy, now just hides)
|
|
9142
|
+
const deleteManagedMatch = url.pathname.match(/^\/api\/managed-sessions\/([^/]+)$/);
|
|
9143
|
+
if (deleteManagedMatch && req.method === 'DELETE') {
|
|
9144
|
+
const session = managedSessions.get(deleteManagedMatch[1]);
|
|
9145
|
+
if (session) {
|
|
9146
|
+
// Hide instead of delete
|
|
9147
|
+
const idToHide = session.claudeSessionId || deleteManagedMatch[1];
|
|
9148
|
+
hideSession(idToHide);
|
|
9149
|
+
broadcastManagedSessions();
|
|
9150
|
+
res.writeHead(200, { 'Content-Type': 'application/json' });
|
|
9151
|
+
res.end(JSON.stringify({ ok: true }));
|
|
9152
|
+
} else {
|
|
9153
|
+
res.writeHead(404, { 'Content-Type': 'application/json' });
|
|
9154
|
+
res.end(JSON.stringify({ ok: false, error: 'Session not found' }));
|
|
9155
|
+
}
|
|
9156
|
+
return;
|
|
9157
|
+
}
|
|
9158
|
+
|
|
9159
|
+
// DELETE /api/managed-sessions/:id/permanent - Permanently delete session
|
|
9160
|
+
const permanentDeleteMatch = url.pathname.match(/^\/api\/managed-sessions\/([^/]+)\/permanent$/);
|
|
9161
|
+
if (permanentDeleteMatch && req.method === 'DELETE') {
|
|
9162
|
+
const result = permanentDeleteSession(permanentDeleteMatch[1]);
|
|
9163
|
+
if (result.success) {
|
|
9164
|
+
res.writeHead(200, { 'Content-Type': 'application/json' });
|
|
9165
|
+
res.end(JSON.stringify({ ok: true }));
|
|
9166
|
+
} else {
|
|
9167
|
+
res.writeHead(404, { 'Content-Type': 'application/json' });
|
|
9168
|
+
res.end(JSON.stringify({ ok: false, error: result.error }));
|
|
9169
|
+
}
|
|
8471
9170
|
return;
|
|
8472
9171
|
}
|
|
8473
9172
|
|