claudehq 1.0.0 → 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/README.md +25 -1
- 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 -84
- package/lib/sessions/manager.js +870 -0
- package/package.json +11 -4
- package/public/index.html +6765 -0
- package/screenshots/activity-feed.png +0 -0
- package/screenshots/create-task.png +0 -0
- package/screenshots/task-detail.png +0 -0
- package/screenshots/task-list.png +0 -0
- package/screenshots/task-status.png +0 -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
|
|
|
@@ -4579,7 +4981,6 @@ const HTML = `<!DOCTYPE html>
|
|
|
4579
4981
|
const chevronIcon = '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M9 18l6-6-6-6"/></svg>';
|
|
4580
4982
|
const tasksIcon = '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><rect x="3" y="3" width="18" height="18" rx="2" ry="2"/><line x1="9" y1="9" x2="15" y2="9"/><line x1="9" y1="13" x2="15" y2="13"/><line x1="9" y1="17" x2="12" y2="17"/></svg>';
|
|
4581
4983
|
const todosIcon = '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M9 11l3 3L22 4"/><path d="M21 12v7a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2V5a2 2 0 0 1 2-2h11"/></svg>';
|
|
4582
|
-
const skillsIcon = '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><polygon points="12 2 15.09 8.26 22 9.27 17 14.14 18.18 21.02 12 17.77 5.82 21.02 7 14.14 2 9.27 8.91 8.26 12 2"/></svg>';
|
|
4583
4984
|
const plansIcon = '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M14 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V8z"/><polyline points="14 2 14 8 20 8"/><line x1="16" y1="13" x2="8" y2="13"/><line x1="16" y1="17" x2="8" y2="17"/></svg>';
|
|
4584
4985
|
const inboxIcon = '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><polyline points="22 12 16 12 14 15 10 15 8 12 2 12"/><path d="M5.45 5.11L2 12v6a2 2 0 0 0 2 2h16a2 2 0 0 0 2-2v-6l-3.45-6.89A2 2 0 0 0 16.76 4H7.24a2 2 0 0 0-1.79 1.11z"/></svg>';
|
|
4585
4986
|
|
|
@@ -4625,7 +5026,7 @@ const HTML = `<!DOCTYPE html>
|
|
|
4625
5026
|
'<span class="session-group-name">' + escapeHtml(session.name) + '</span>' +
|
|
4626
5027
|
'<div class="session-group-actions">' +
|
|
4627
5028
|
'<button onclick="event.stopPropagation(); renameManagedSession(\\'' + session.id + '\\')" title="Rename">✎</button>' +
|
|
4628
|
-
'<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>' +
|
|
4629
5030
|
'</div>' +
|
|
4630
5031
|
'</div>' +
|
|
4631
5032
|
'<div class="session-views">' +
|
|
@@ -4644,10 +5045,6 @@ const HTML = `<!DOCTYPE html>
|
|
|
4644
5045
|
'<span class="view-name">Plans</span>' +
|
|
4645
5046
|
(planCount > 0 ? '<span class="view-count">' + planCount + '</span>' : '') +
|
|
4646
5047
|
'</div>' +
|
|
4647
|
-
'<div class="session-view-item disabled" style="opacity: 0.4; cursor: not-allowed;" title="Coming soon">' +
|
|
4648
|
-
'<span class="view-icon">' + skillsIcon + '</span>' +
|
|
4649
|
-
'<span class="view-name">Skills</span>' +
|
|
4650
|
-
'</div>' +
|
|
4651
5048
|
'</div>' +
|
|
4652
5049
|
'</div>';
|
|
4653
5050
|
}).join('');
|
|
@@ -4691,8 +5088,15 @@ const HTML = `<!DOCTYPE html>
|
|
|
4691
5088
|
return days + 'd ago';
|
|
4692
5089
|
}
|
|
4693
5090
|
|
|
5091
|
+
// Track pending data load to prevent race conditions
|
|
5092
|
+
let pendingSessionLoad = null;
|
|
5093
|
+
|
|
4694
5094
|
// Select a managed session (or null for all)
|
|
4695
5095
|
function selectManagedSession(sessionId) {
|
|
5096
|
+
// Cancel any pending load
|
|
5097
|
+
const loadId = Date.now();
|
|
5098
|
+
pendingSessionLoad = loadId;
|
|
5099
|
+
|
|
4696
5100
|
selectedManagedSession = sessionId;
|
|
4697
5101
|
|
|
4698
5102
|
// Auto-expand the selected session
|
|
@@ -4711,35 +5115,62 @@ const HTML = `<!DOCTYPE html>
|
|
|
4711
5115
|
|
|
4712
5116
|
const session = sessionId ? managedSessions.find(s => s.id === sessionId) : null;
|
|
4713
5117
|
|
|
4714
|
-
//
|
|
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)
|
|
4715
5140
|
if (session && session.claudeSessionId) {
|
|
4716
5141
|
ActivityFeedManager.setFilter(session.claudeSessionId);
|
|
4717
|
-
// Open conversation panel
|
|
5142
|
+
// Open conversation panel if not open (skipDataLoad=true because we load data ourselves below)
|
|
4718
5143
|
if (!conversationOpen) {
|
|
4719
|
-
toggleConversation();
|
|
5144
|
+
toggleConversation(true);
|
|
4720
5145
|
}
|
|
5146
|
+
// Load activity feed from cached events (instant)
|
|
4721
5147
|
ActivityFeedManager.loadForSession(session.claudeSessionId);
|
|
4722
|
-
} else if (
|
|
5148
|
+
} else if (session) {
|
|
4723
5149
|
ActivityFeedManager.setFilter('__none__');
|
|
4724
5150
|
} else {
|
|
4725
5151
|
ActivityFeedManager.setFilter(null);
|
|
5152
|
+
// Load activity for all sessions
|
|
5153
|
+
ActivityFeedManager.loadForSession(null);
|
|
4726
5154
|
}
|
|
4727
5155
|
|
|
4728
|
-
//
|
|
5156
|
+
// Check again after filter setup
|
|
5157
|
+
if (pendingSessionLoad !== loadId) return;
|
|
5158
|
+
|
|
5159
|
+
// 2. Load view-specific data
|
|
4729
5160
|
if (session && session.claudeSessionId) {
|
|
4730
5161
|
if (currentSessionView === 'todos') {
|
|
4731
|
-
loadTodosForSession(session.claudeSessionId);
|
|
5162
|
+
await loadTodosForSession(session.claudeSessionId);
|
|
4732
5163
|
} else if (currentSessionView === 'plans') {
|
|
4733
|
-
loadPlansForSession(session.claudeSessionId);
|
|
5164
|
+
await loadPlansForSession(session.claudeSessionId);
|
|
4734
5165
|
} else {
|
|
4735
|
-
loadTasksForSession(session.claudeSessionId);
|
|
5166
|
+
await loadTasksForSession(session.claudeSessionId);
|
|
4736
5167
|
}
|
|
4737
|
-
} else if (!
|
|
5168
|
+
} else if (!session) {
|
|
4738
5169
|
// Show all tasks/todos
|
|
4739
|
-
currentSessionView = 'tasks';
|
|
4740
|
-
loadData(true);
|
|
5170
|
+
currentSessionView = 'tasks';
|
|
5171
|
+
await loadData(true);
|
|
4741
5172
|
} else {
|
|
4742
|
-
// No
|
|
5173
|
+
// No claudeSessionId - show empty state
|
|
4743
5174
|
if (currentSessionView === 'todos') {
|
|
4744
5175
|
currentTodos = [];
|
|
4745
5176
|
renderTodosView();
|
|
@@ -4751,18 +5182,6 @@ const HTML = `<!DOCTYPE html>
|
|
|
4751
5182
|
renderTasks();
|
|
4752
5183
|
}
|
|
4753
5184
|
}
|
|
4754
|
-
|
|
4755
|
-
// Update header title
|
|
4756
|
-
const titleEl = document.getElementById('header-title');
|
|
4757
|
-
if (titleEl) {
|
|
4758
|
-
if (session) {
|
|
4759
|
-
const viewLabel = currentSessionView === 'todos' ? 'Todos' :
|
|
4760
|
-
currentSessionView === 'plans' ? 'Plans' : 'Tasks';
|
|
4761
|
-
titleEl.textContent = session.name + ' - ' + viewLabel;
|
|
4762
|
-
} else {
|
|
4763
|
-
titleEl.textContent = 'All Sessions';
|
|
4764
|
-
}
|
|
4765
|
-
}
|
|
4766
5185
|
}
|
|
4767
5186
|
|
|
4768
5187
|
// Load tasks specifically for a Claude session ID
|
|
@@ -4837,13 +5256,59 @@ const HTML = `<!DOCTYPE html>
|
|
|
4837
5256
|
}
|
|
4838
5257
|
}
|
|
4839
5258
|
|
|
4840
|
-
//
|
|
4841
|
-
async function
|
|
4842
|
-
|
|
4843
|
-
|
|
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
|
+
}
|
|
4844
5278
|
|
|
5279
|
+
// Unhide session
|
|
5280
|
+
async function unhideSession(sessionId) {
|
|
4845
5281
|
try {
|
|
4846
|
-
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', {
|
|
4847
5312
|
method: 'DELETE'
|
|
4848
5313
|
});
|
|
4849
5314
|
const data = await res.json();
|
|
@@ -4852,12 +5317,52 @@ const HTML = `<!DOCTYPE html>
|
|
|
4852
5317
|
selectManagedSession(null);
|
|
4853
5318
|
}
|
|
4854
5319
|
loadManagedSessions();
|
|
5320
|
+
loadHiddenSessions();
|
|
5321
|
+
showToast('Session permanently deleted');
|
|
5322
|
+
} else {
|
|
5323
|
+
showToast('Error: ' + (data.error || 'Failed to delete'));
|
|
4855
5324
|
}
|
|
4856
5325
|
} catch (e) {
|
|
4857
5326
|
console.error('Failed to delete session:', e);
|
|
5327
|
+
showToast('Error deleting session');
|
|
4858
5328
|
}
|
|
4859
5329
|
}
|
|
4860
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
|
+
|
|
4861
5366
|
// Restart managed session
|
|
4862
5367
|
async function restartManagedSession(sessionId) {
|
|
4863
5368
|
try {
|
|
@@ -5355,17 +5860,29 @@ const HTML = `<!DOCTYPE html>
|
|
|
5355
5860
|
this.clear();
|
|
5356
5861
|
},
|
|
5357
5862
|
|
|
5358
|
-
// Load existing events for a session
|
|
5863
|
+
// Load existing events for a session (or all sessions if null)
|
|
5359
5864
|
loadForSession(claudeSessionId) {
|
|
5360
5865
|
this.clear();
|
|
5361
|
-
|
|
5362
|
-
|
|
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
|
+
|
|
5363
5877
|
// Show most recent 50 events
|
|
5364
5878
|
const recent = sessionEvents.slice(-50);
|
|
5365
5879
|
for (const event of recent) {
|
|
5366
5880
|
this.add(event, true); // true = skip scroll
|
|
5367
5881
|
}
|
|
5368
5882
|
this.scrollToBottom();
|
|
5883
|
+
|
|
5884
|
+
// Log for debugging
|
|
5885
|
+
console.log('[ActivityFeed] Loaded', recent.length, 'events for session:', claudeSessionId || 'all');
|
|
5369
5886
|
},
|
|
5370
5887
|
|
|
5371
5888
|
showThinking(sessionId) {
|
|
@@ -7945,12 +8462,16 @@ const HTML = `<!DOCTYPE html>
|
|
|
7945
8462
|
}
|
|
7946
8463
|
|
|
7947
8464
|
// Conversation toggle - uses ConversationManager
|
|
7948
|
-
|
|
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) {
|
|
7949
8468
|
conversationOpen = !conversationOpen;
|
|
7950
8469
|
document.getElementById('conversation-panel').classList.toggle('open', conversationOpen);
|
|
7951
8470
|
document.getElementById('conv-toggle').classList.toggle('active', conversationOpen);
|
|
7952
8471
|
|
|
7953
|
-
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) {
|
|
7954
8475
|
// Load both conversation messages and activity feed
|
|
7955
8476
|
ConversationManager.load(currentSession.sessionId);
|
|
7956
8477
|
ActivityFeedManager.loadForSession(currentSession.sessionId);
|
|
@@ -8033,27 +8554,135 @@ const HTML = `<!DOCTYPE html>
|
|
|
8033
8554
|
AttentionSystem.requestPermission();
|
|
8034
8555
|
setupPromptForm();
|
|
8035
8556
|
setupCreateTaskForm();
|
|
8036
|
-
loadData();
|
|
8037
|
-
connectSSE();
|
|
8038
8557
|
setupDragDropListeners();
|
|
8039
8558
|
|
|
8040
|
-
//
|
|
8041
|
-
|
|
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();
|
|
8042
8564
|
|
|
8043
|
-
|
|
8044
|
-
|
|
8565
|
+
async function initializeApp() {
|
|
8566
|
+
console.log('[Init] Starting app initialization...');
|
|
8045
8567
|
|
|
8046
|
-
|
|
8047
|
-
|
|
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');
|
|
8048
8591
|
|
|
8049
|
-
|
|
8050
|
-
|
|
8051
|
-
|
|
8052
|
-
|
|
8053
|
-
if (savedSessionId && managedSessions.find(s => s.id === savedSessionId)) {
|
|
8054
|
-
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();
|
|
8055
8596
|
}
|
|
8056
|
-
}
|
|
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
|
+
}
|
|
8057
8686
|
|
|
8058
8687
|
// Load Claude events from the server
|
|
8059
8688
|
async function loadClaudeEvents() {
|
|
@@ -8458,21 +9087,86 @@ const server = http.createServer((req, res) => {
|
|
|
8458
9087
|
return;
|
|
8459
9088
|
}
|
|
8460
9089
|
|
|
8461
|
-
//
|
|
8462
|
-
const
|
|
8463
|
-
if (
|
|
8464
|
-
|
|
8465
|
-
|
|
8466
|
-
|
|
8467
|
-
|
|
8468
|
-
|
|
8469
|
-
|
|
8470
|
-
|
|
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
|
+
}
|
|
8471
9132
|
}
|
|
8472
|
-
|
|
8473
|
-
|
|
8474
|
-
res.end(JSON.stringify({ ok: false, error: e.message }));
|
|
9133
|
+
|
|
9134
|
+
return { id, name };
|
|
8475
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
|
+
}
|
|
8476
9170
|
return;
|
|
8477
9171
|
}
|
|
8478
9172
|
|