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/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
- return Array.from(managedSessions.values()).map(session => {
445
- // Fetch conversation metadata if we have a Claude session ID
446
- let firstPrompt = null;
447
- if (session.claudeSessionId) {
448
- const metadata = getSessionMetadata(session.claudeSessionId);
449
- firstPrompt = metadata.firstPrompt;
450
- }
451
- return {
452
- ...session,
453
- firstPrompt,
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
- .session-group-icon .status-dot.idle { background: #4ade80; }
2456
+ /* Status colors: Green=working, Orange=waiting, Red=idle, Grey=offline */
2080
2457
  .session-group-icon .status-dot.working {
2081
- background: #fbbf24;
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.offline { background: #ef4444; }
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(); deleteManagedSession(\\'' + session.id + '\\')" title="Remove">×</button>' +
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
- // Filter activity feed and open conversation panel
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 and load activity
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 (sessionId) {
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
- // Load data based on current view
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 (!sessionId) {
5168
+ } else if (!session) {
4738
5169
  // Show all tasks/todos
4739
- currentSessionView = 'tasks'; // Reset to tasks for "All Sessions"
4740
- loadData(true);
5170
+ currentSessionView = 'tasks';
5171
+ await loadData(true);
4741
5172
  } else {
4742
- // No data for this session yet
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
- // Delete managed session
4841
- async function deleteManagedSession(sessionId) {
4842
- const session = managedSessions.find(s => s.id === sessionId);
4843
- if (!confirm('Delete session "' + (session?.name || sessionId) + '"?')) return;
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
- // Load events from claudeEvents array for this session
5362
- const sessionEvents = claudeEvents.filter(e => e.sessionId === claudeSessionId);
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
- function toggleConversation() {
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 (conversationOpen && currentSession) {
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
- // Load existing Claude events from server
8041
- loadClaudeEvents();
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
- // Load all todos for sidebar counts
8044
- loadAllTodos();
8565
+ async function initializeApp() {
8566
+ console.log('[Init] Starting app initialization...');
8045
8567
 
8046
- // Load all plans for sidebar counts
8047
- loadAllPlansData();
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
- // Initialize managed sessions
8050
- loadManagedSessions().then(() => {
8051
- // Restore selected managed session from localStorage
8052
- const savedSessionId = localStorage.getItem('selectedManagedSession');
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
- // DELETE /api/managed-sessions/:id - Delete session
8462
- const deleteManagedMatch = url.pathname.match(/^\/api\/managed-sessions\/([^/]+)$/);
8463
- if (deleteManagedMatch && req.method === 'DELETE') {
8464
- deleteManagedSession(deleteManagedMatch[1]).then((success) => {
8465
- if (success) {
8466
- res.writeHead(200, { 'Content-Type': 'application/json' });
8467
- res.end(JSON.stringify({ ok: true }));
8468
- } else {
8469
- res.writeHead(404, { 'Content-Type': 'application/json' });
8470
- res.end(JSON.stringify({ ok: false, error: 'Session not found' }));
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
- }).catch((e) => {
8473
- res.writeHead(500, { 'Content-Type': 'application/json' });
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