claudehq 1.0.2 → 1.0.5

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "claudehq",
3
- "version": "1.0.2",
3
+ "version": "1.0.5",
4
4
  "description": "Claude HQ - A real-time command center for Claude Code sessions",
5
5
  "main": "lib/index.js",
6
6
  "bin": {
package/public/index.html CHANGED
@@ -447,6 +447,70 @@
447
447
  color: #ef4444;
448
448
  }
449
449
 
450
+ /* Offline Sessions Section */
451
+ .offline-sessions-section {
452
+ margin-top: 8px;
453
+ border-top: 1px solid var(--border-primary);
454
+ padding-top: 8px;
455
+ }
456
+
457
+ .offline-sessions-section.collapsed .offline-sessions-list {
458
+ display: none;
459
+ }
460
+
461
+ .offline-sessions-header {
462
+ display: flex;
463
+ align-items: center;
464
+ gap: 8px;
465
+ padding: 4px 12px;
466
+ font-size: 11px;
467
+ font-weight: 600;
468
+ color: var(--text-tertiary);
469
+ text-transform: uppercase;
470
+ letter-spacing: 0.5px;
471
+ cursor: pointer;
472
+ }
473
+
474
+ .offline-sessions-header:hover {
475
+ color: var(--text-secondary);
476
+ }
477
+
478
+ .offline-sessions-header .collapse-icon {
479
+ font-size: 8px;
480
+ transition: transform 0.15s;
481
+ }
482
+
483
+ .offline-sessions-section.collapsed .collapse-icon {
484
+ transform: rotate(-90deg);
485
+ }
486
+
487
+ .offline-sessions-count {
488
+ background: var(--bg-tertiary);
489
+ color: var(--text-tertiary);
490
+ padding: 1px 6px;
491
+ border-radius: 10px;
492
+ font-size: 10px;
493
+ font-weight: 500;
494
+ }
495
+
496
+ .offline-sessions-list {
497
+ display: flex;
498
+ flex-direction: column;
499
+ }
500
+
501
+ /* Offline session groups have muted styling */
502
+ .offline-sessions-list .session-group {
503
+ opacity: 0.7;
504
+ }
505
+
506
+ .offline-sessions-list .session-group:hover {
507
+ opacity: 1;
508
+ }
509
+
510
+ .offline-sessions-list .session-group-icon {
511
+ filter: grayscale(40%);
512
+ }
513
+
450
514
  /* Session Group - Expandable container */
451
515
  .session-group {
452
516
  margin-bottom: 2px;
@@ -2552,6 +2616,77 @@
2552
2616
  min-height: 80px;
2553
2617
  }
2554
2618
 
2619
+ .form-group select {
2620
+ width: 100%;
2621
+ padding: 10px 12px;
2622
+ background: var(--bg-primary);
2623
+ border: 1px solid var(--border);
2624
+ border-radius: 6px;
2625
+ font-size: 14px;
2626
+ color: var(--text-primary);
2627
+ font-family: inherit;
2628
+ cursor: pointer;
2629
+ }
2630
+
2631
+ .form-group select:focus {
2632
+ outline: none;
2633
+ border-color: var(--accent);
2634
+ box-shadow: 0 0 0 2px rgba(99, 102, 241, 0.15);
2635
+ }
2636
+
2637
+ .form-group {
2638
+ position: relative;
2639
+ }
2640
+
2641
+ .autocomplete-dropdown {
2642
+ position: absolute;
2643
+ top: 100%;
2644
+ left: 0;
2645
+ right: 0;
2646
+ background: var(--bg-secondary);
2647
+ border: 1px solid var(--border);
2648
+ border-radius: 6px;
2649
+ max-height: 200px;
2650
+ overflow-y: auto;
2651
+ z-index: 1000;
2652
+ box-shadow: 0 4px 12px rgba(0, 0, 0, 0.3);
2653
+ margin-top: 4px;
2654
+ }
2655
+
2656
+ .autocomplete-item {
2657
+ padding: 10px 12px;
2658
+ cursor: pointer;
2659
+ font-size: 13px;
2660
+ border-bottom: 1px solid var(--border);
2661
+ }
2662
+
2663
+ .autocomplete-item:last-child {
2664
+ border-bottom: none;
2665
+ }
2666
+
2667
+ .autocomplete-item:hover {
2668
+ background: var(--bg-tertiary);
2669
+ }
2670
+
2671
+ .autocomplete-item .path {
2672
+ color: var(--text-primary);
2673
+ font-family: 'SF Mono', monospace;
2674
+ }
2675
+
2676
+ .autocomplete-item .name {
2677
+ color: var(--text-tertiary);
2678
+ font-size: 11px;
2679
+ margin-top: 2px;
2680
+ }
2681
+
2682
+ .autocomplete-item.type-known {
2683
+ border-left: 3px solid var(--accent);
2684
+ }
2685
+
2686
+ .autocomplete-item.type-filesystem {
2687
+ border-left: 3px solid var(--text-tertiary);
2688
+ }
2689
+
2555
2690
  .modal-actions {
2556
2691
  display: flex;
2557
2692
  justify-content: flex-end;
@@ -2705,7 +2840,7 @@
2705
2840
 
2706
2841
  <!-- Create Session Button -->
2707
2842
  <div style="padding: 8px;">
2708
- <button class="create-session-btn" onclick="openCreateSessionModal()" title="Launch a new Claude Code session in tmux">
2843
+ <button class="create-session-btn" onclick="alert('clicked'); document.getElementById('launch-session-modal').classList.add('open');" title="Launch a new Claude Code session in tmux">
2709
2844
  <svg width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
2710
2845
  <path d="M12 5v14M5 12h14"/>
2711
2846
  </svg>
@@ -2952,6 +3087,41 @@
2952
3087
  </div>
2953
3088
  </div>
2954
3089
 
3090
+ <!-- Launch Session Modal -->
3091
+ <div class="modal-overlay" id="launch-session-modal" onclick="closeLaunchSessionModal(event)">
3092
+ <div class="modal" onclick="event.stopPropagation()" style="max-width: 500px;">
3093
+ <div class="modal-header">
3094
+ <h3>Launch New Session</h3>
3095
+ <button class="close-btn" onclick="closeLaunchSessionModal()">&times;</button>
3096
+ </div>
3097
+ <form class="modal-form" id="launch-session-form" onsubmit="submitLaunchSession(event)">
3098
+ <div class="form-group">
3099
+ <label for="launch-session-name">Session Name (optional)</label>
3100
+ <input type="text" id="launch-session-name" placeholder="My Project">
3101
+ </div>
3102
+ <div class="form-group">
3103
+ <label for="launch-session-cwd">Working Directory</label>
3104
+ <input type="text" id="launch-session-cwd" placeholder="/path/to/project" required autocomplete="off">
3105
+ <div id="cwd-autocomplete" class="autocomplete-dropdown" style="display: none;"></div>
3106
+ <div class="form-hint">The directory where Claude Code will run</div>
3107
+ </div>
3108
+ <div class="form-group">
3109
+ <label for="launch-session-model">Model (optional)</label>
3110
+ <select id="launch-session-model">
3111
+ <option value="">Default (Sonnet)</option>
3112
+ <option value="sonnet">Sonnet</option>
3113
+ <option value="opus">Opus</option>
3114
+ <option value="haiku">Haiku</option>
3115
+ </select>
3116
+ </div>
3117
+ <div class="modal-actions">
3118
+ <button type="button" class="btn-secondary" onclick="closeLaunchSessionModal()">Cancel</button>
3119
+ <button type="submit" class="btn-primary">Launch Session</button>
3120
+ </div>
3121
+ </form>
3122
+ </div>
3123
+ </div>
3124
+
2955
3125
  <!-- Bulk Action Bar -->
2956
3126
  <div id="bulk-action-bar" class="bulk-action-bar" style="display: none;">
2957
3127
  <span class="bulk-count">0 selected</span>
@@ -3029,6 +3199,15 @@
3029
3199
  // Track expanded state for each session
3030
3200
  const expandedSessions = new Set();
3031
3201
 
3202
+ // Track collapsed state for offline sessions section (collapsed by default)
3203
+ let offlineSectionCollapsed = true;
3204
+
3205
+ // Toggle offline sessions section
3206
+ function toggleOfflineSessionsSection() {
3207
+ offlineSectionCollapsed = !offlineSectionCollapsed;
3208
+ renderManagedSessions();
3209
+ }
3210
+
3032
3211
  // Render managed sessions list - Linear-style with expandable groups
3033
3212
  function renderManagedSessions() {
3034
3213
  const list = document.getElementById('managed-sessions-list');
@@ -3036,9 +3215,13 @@
3036
3215
 
3037
3216
  if (!list) return;
3038
3217
 
3039
- // Update count
3218
+ // Separate active and offline sessions
3219
+ const activeSessions = managedSessions.filter(s => s.status !== 'offline');
3220
+ const offlineSessions = managedSessions.filter(s => s.status === 'offline');
3221
+
3222
+ // Update count (show only active sessions count)
3040
3223
  if (countEl) {
3041
- countEl.textContent = managedSessions.length;
3224
+ countEl.textContent = activeSessions.length;
3042
3225
  }
3043
3226
 
3044
3227
  // SVG icons
@@ -3048,14 +3231,8 @@
3048
3231
  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>';
3049
3232
  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>';
3050
3233
 
3051
- // "All Sessions" item at top
3052
- const allSessionsHtml = '<div class="session-view-item ' + (selectedManagedSession === null ? 'active' : '') + '" onclick="selectManagedSession(null)" style="margin: 0 4px 8px 4px;">' +
3053
- '<span class="view-icon">' + inboxIcon + '</span>' +
3054
- '<span class="view-name">All Sessions</span>' +
3055
- '</div>';
3056
-
3057
- // Render session groups
3058
- list.innerHTML = allSessionsHtml + managedSessions.map(session => {
3234
+ // Helper function to render a session group
3235
+ function renderSessionGroup(session) {
3059
3236
  const isExpanded = expandedSessions.has(session.id);
3060
3237
  const isActiveSession = selectedManagedSession === session.id;
3061
3238
  const statusClass = session.status || 'offline';
@@ -3111,7 +3288,34 @@
3111
3288
  '</div>' +
3112
3289
  '</div>' +
3113
3290
  '</div>';
3114
- }).join('');
3291
+ }
3292
+
3293
+ // "All Sessions" item at top
3294
+ const allSessionsHtml = '<div class="session-view-item ' + (selectedManagedSession === null ? 'active' : '') + '" onclick="selectManagedSession(null)" style="margin: 0 4px 8px 4px;">' +
3295
+ '<span class="view-icon">' + inboxIcon + '</span>' +
3296
+ '<span class="view-name">All Sessions</span>' +
3297
+ '</div>';
3298
+
3299
+ // Render active sessions
3300
+ const activeSessionsHtml = activeSessions.map(renderSessionGroup).join('');
3301
+
3302
+ // Render offline sessions section (only if there are offline sessions)
3303
+ let offlineSessionsHtml = '';
3304
+ if (offlineSessions.length > 0) {
3305
+ offlineSessionsHtml = '<div class="offline-sessions-section ' + (offlineSectionCollapsed ? 'collapsed' : '') + '">' +
3306
+ '<div class="offline-sessions-header" onclick="toggleOfflineSessionsSection()">' +
3307
+ '<span class="collapse-icon">▼</span>' +
3308
+ '<span>Offline</span>' +
3309
+ '<span class="offline-sessions-count">' + offlineSessions.length + '</span>' +
3310
+ '</div>' +
3311
+ '<div class="offline-sessions-list">' +
3312
+ offlineSessions.map(renderSessionGroup).join('') +
3313
+ '</div>' +
3314
+ '</div>';
3315
+ }
3316
+
3317
+ // Combine all HTML
3318
+ list.innerHTML = allSessionsHtml + activeSessionsHtml + offlineSessionsHtml;
3115
3319
  }
3116
3320
 
3117
3321
  // Track current view within a session
@@ -3445,16 +3649,193 @@
3445
3649
  }
3446
3650
  }
3447
3651
 
3448
- // Open create session modal
3652
+ // Open launch session modal
3449
3653
  function openCreateSessionModal() {
3450
- const name = prompt('Session name (optional):');
3451
- const cwd = prompt('Working directory:', process?.cwd?.() || '');
3452
- if (cwd === null) return; // Cancelled
3654
+ alert('Function called!'); // Debug - remove after testing
3655
+
3656
+ const modal = document.getElementById('launch-session-modal');
3657
+ if (!modal) {
3658
+ alert('Modal not found!');
3659
+ return;
3660
+ }
3661
+
3662
+ alert('Modal found, adding open class'); // Debug - remove after testing
3663
+
3664
+ const form = document.getElementById('launch-session-form');
3665
+ const cwdInput = document.getElementById('launch-session-cwd');
3666
+ const nameInput = document.getElementById('launch-session-name');
3667
+
3668
+ if (form) form.reset();
3669
+
3670
+ // Load recent projects for autocomplete
3671
+ loadRecentProjects();
3672
+
3673
+ // Set up autocomplete
3674
+ if (cwdInput) setupCwdAutocomplete(cwdInput);
3675
+
3676
+ modal.classList.add('open');
3677
+
3678
+ if (nameInput) nameInput.focus();
3679
+ }
3680
+
3681
+ function closeLaunchSessionModal(event) {
3682
+ if (event && event.target !== event.currentTarget) return;
3683
+ const modal = document.getElementById('launch-session-modal');
3684
+ modal.classList.remove('open');
3685
+ hideAutocomplete();
3686
+ }
3687
+
3688
+ // Set up working directory autocomplete
3689
+ let autocompleteTimeout = null;
3690
+ function setupCwdAutocomplete(input) {
3691
+ input.addEventListener('input', (e) => {
3692
+ clearTimeout(autocompleteTimeout);
3693
+ autocompleteTimeout = setTimeout(() => {
3694
+ fetchAutocomplete(e.target.value);
3695
+ }, 150);
3696
+ });
3697
+
3698
+ input.addEventListener('keydown', (e) => {
3699
+ const dropdown = document.getElementById('cwd-autocomplete');
3700
+ const items = dropdown.querySelectorAll('.autocomplete-item');
3701
+ const activeItem = dropdown.querySelector('.autocomplete-item.active');
3702
+
3703
+ if (e.key === 'ArrowDown') {
3704
+ e.preventDefault();
3705
+ if (!activeItem && items.length > 0) {
3706
+ items[0].classList.add('active');
3707
+ } else if (activeItem && activeItem.nextElementSibling) {
3708
+ activeItem.classList.remove('active');
3709
+ activeItem.nextElementSibling.classList.add('active');
3710
+ }
3711
+ } else if (e.key === 'ArrowUp') {
3712
+ e.preventDefault();
3713
+ if (activeItem && activeItem.previousElementSibling) {
3714
+ activeItem.classList.remove('active');
3715
+ activeItem.previousElementSibling.classList.add('active');
3716
+ }
3717
+ } else if (e.key === 'Enter' && activeItem) {
3718
+ e.preventDefault();
3719
+ selectAutocompleteItem(activeItem.dataset.path);
3720
+ } else if (e.key === 'Escape') {
3721
+ hideAutocomplete();
3722
+ }
3723
+ });
3724
+
3725
+ input.addEventListener('blur', () => {
3726
+ // Delay hiding to allow click on item
3727
+ setTimeout(hideAutocomplete, 200);
3728
+ });
3729
+ }
3730
+
3731
+ async function fetchAutocomplete(query) {
3732
+ try {
3733
+ const res = await fetch(`/api/spawner/projects/autocomplete?q=${encodeURIComponent(query)}`);
3734
+ const data = await res.json();
3735
+ if (data.ok && data.suggestions) {
3736
+ showAutocomplete(data.suggestions);
3737
+ }
3738
+ } catch (e) {
3739
+ console.error('Autocomplete error:', e);
3740
+ }
3741
+ }
3742
+
3743
+ function showAutocomplete(suggestions) {
3744
+ const dropdown = document.getElementById('cwd-autocomplete');
3745
+
3746
+ if (suggestions.length === 0) {
3747
+ hideAutocomplete();
3748
+ return;
3749
+ }
3750
+
3751
+ dropdown.innerHTML = suggestions.map(s => `
3752
+ <div class="autocomplete-item type-${s.type || 'filesystem'}"
3753
+ data-path="${escapeHtml(s.path)}"
3754
+ onclick="selectAutocompleteItem(this.dataset.path)">
3755
+ <div class="path">${escapeHtml(s.path)}</div>
3756
+ ${s.name && s.name !== s.path ? `<div class="name">${escapeHtml(s.name)}</div>` : ''}
3757
+ </div>
3758
+ `).join('');
3759
+
3760
+ dropdown.style.display = 'block';
3761
+ }
3762
+
3763
+ function hideAutocomplete() {
3764
+ const dropdown = document.getElementById('cwd-autocomplete');
3765
+ dropdown.style.display = 'none';
3766
+ }
3767
+
3768
+ function selectAutocompleteItem(path) {
3769
+ const input = document.getElementById('launch-session-cwd');
3770
+ input.value = path;
3771
+ hideAutocomplete();
3772
+ input.focus();
3773
+ }
3774
+
3775
+ async function loadRecentProjects() {
3776
+ try {
3777
+ const res = await fetch('/api/spawner/projects?limit=5');
3778
+ const data = await res.json();
3779
+ // Could show recent projects as suggestions, but for now just preload
3780
+ } catch (e) {
3781
+ // Ignore
3782
+ }
3783
+ }
3784
+
3785
+ // Submit launch session form
3786
+ async function submitLaunchSession(event) {
3787
+ event.preventDefault();
3788
+
3789
+ const name = document.getElementById('launch-session-name').value.trim();
3790
+ const cwd = document.getElementById('launch-session-cwd').value.trim();
3791
+ const model = document.getElementById('launch-session-model').value;
3792
+
3793
+ if (!cwd) {
3794
+ alert('Working directory is required');
3795
+ return;
3796
+ }
3797
+
3798
+ try {
3799
+ // Try the new spawner API first
3800
+ const res = await fetch('/api/spawner/sessions', {
3801
+ method: 'POST',
3802
+ headers: { 'Content-Type': 'application/json' },
3803
+ body: JSON.stringify({
3804
+ name: name || undefined,
3805
+ cwd,
3806
+ model: model || undefined
3807
+ })
3808
+ });
3809
+ const data = await res.json();
3810
+
3811
+ if (data.ok) {
3812
+ closeLaunchSessionModal();
3813
+ loadManagedSessions();
3814
+ // Also refresh spawned sessions
3815
+ loadSpawnedSessions();
3816
+ showToast('Session launched successfully');
3817
+ } else {
3818
+ alert('Failed to launch session: ' + (data.error || 'Unknown error'));
3819
+ }
3820
+ } catch (e) {
3821
+ console.error('Failed to launch session:', e);
3822
+ alert('Failed to launch session: ' + e.message);
3823
+ }
3824
+ }
3453
3825
 
3454
- createManagedSession(name || undefined, cwd || undefined);
3826
+ // Load spawned sessions (from new spawner API)
3827
+ async function loadSpawnedSessions() {
3828
+ try {
3829
+ const res = await fetch('/api/spawner/sessions');
3830
+ const data = await res.json();
3831
+ // For now, just log - could merge with managed sessions display
3832
+ console.log('Spawned sessions:', data.sessions);
3833
+ } catch (e) {
3834
+ // Ignore errors
3835
+ }
3455
3836
  }
3456
3837
 
3457
- // Create managed session
3838
+ // Legacy function for backwards compatibility
3458
3839
  async function createManagedSession(name, cwd) {
3459
3840
  try {
3460
3841
  const res = await fetch('/api/managed-sessions', {