htmlgraph 0.25.0__py3-none-any.whl → 0.26.2__py3-none-any.whl

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.
Files changed (41) hide show
  1. htmlgraph/.htmlgraph/.session-warning-state.json +6 -0
  2. htmlgraph/.htmlgraph/agents.json +72 -0
  3. htmlgraph/.htmlgraph/htmlgraph.db +0 -0
  4. htmlgraph/__init__.py +1 -1
  5. htmlgraph/api/main.py +252 -47
  6. htmlgraph/api/templates/dashboard.html +11 -0
  7. htmlgraph/api/templates/partials/activity-feed.html +517 -8
  8. htmlgraph/cli.py +1 -1
  9. htmlgraph/config.py +173 -96
  10. htmlgraph/dashboard.html +632 -7237
  11. htmlgraph/db/schema.py +258 -9
  12. htmlgraph/hooks/.htmlgraph/.session-warning-state.json +6 -0
  13. htmlgraph/hooks/.htmlgraph/agents.json +72 -0
  14. htmlgraph/hooks/.htmlgraph/index.sqlite +0 -0
  15. htmlgraph/hooks/cigs_pretool_enforcer.py +2 -2
  16. htmlgraph/hooks/concurrent_sessions.py +208 -0
  17. htmlgraph/hooks/context.py +88 -10
  18. htmlgraph/hooks/drift_handler.py +24 -20
  19. htmlgraph/hooks/event_tracker.py +264 -189
  20. htmlgraph/hooks/orchestrator.py +6 -4
  21. htmlgraph/hooks/orchestrator_reflector.py +4 -4
  22. htmlgraph/hooks/pretooluse.py +63 -36
  23. htmlgraph/hooks/prompt_analyzer.py +14 -25
  24. htmlgraph/hooks/session_handler.py +123 -69
  25. htmlgraph/hooks/state_manager.py +7 -4
  26. htmlgraph/hooks/subagent_stop.py +3 -2
  27. htmlgraph/hooks/validator.py +15 -11
  28. htmlgraph/operations/fastapi_server.py +2 -2
  29. htmlgraph/orchestration/headless_spawner.py +489 -16
  30. htmlgraph/orchestration/live_events.py +377 -0
  31. htmlgraph/server.py +100 -203
  32. htmlgraph-0.26.2.data/data/htmlgraph/dashboard.html +812 -0
  33. {htmlgraph-0.25.0.dist-info → htmlgraph-0.26.2.dist-info}/METADATA +1 -1
  34. {htmlgraph-0.25.0.dist-info → htmlgraph-0.26.2.dist-info}/RECORD +40 -32
  35. htmlgraph-0.25.0.data/data/htmlgraph/dashboard.html +0 -7417
  36. {htmlgraph-0.25.0.data → htmlgraph-0.26.2.data}/data/htmlgraph/styles.css +0 -0
  37. {htmlgraph-0.25.0.data → htmlgraph-0.26.2.data}/data/htmlgraph/templates/AGENTS.md.template +0 -0
  38. {htmlgraph-0.25.0.data → htmlgraph-0.26.2.data}/data/htmlgraph/templates/CLAUDE.md.template +0 -0
  39. {htmlgraph-0.25.0.data → htmlgraph-0.26.2.data}/data/htmlgraph/templates/GEMINI.md.template +0 -0
  40. {htmlgraph-0.25.0.dist-info → htmlgraph-0.26.2.dist-info}/WHEEL +0 -0
  41. {htmlgraph-0.25.0.dist-info → htmlgraph-0.26.2.dist-info}/entry_points.txt +0 -0
@@ -86,12 +86,14 @@
86
86
  <!-- Child Events Container (Hidden by default) -->
87
87
  <div class="turn-children collapsed" id="children-{{ turn.userQuery.event_id }}">
88
88
  {% if turn.children %}
89
- {% for child in turn.children %}
89
+ {% for child in turn.children recursive %}
90
90
  <!-- Child Event Row -->
91
- <div class="child-event-row" data-event-id="{{ child.event_id }}">
91
+ <div class="child-event-row depth-{{ child.depth|default(0) }}"
92
+ data-event-id="{{ child.event_id }}"
93
+ style="margin-left: {{ (child.depth|default(0)) * 20 }}px;">
92
94
  <!-- Tree Connector -->
93
95
  <span class="tree-connector">
94
- {% if loop.last %}└─{% else %}├─{% endif %}
96
+ {% if loop.last and not child.children %}└─{% else %}├─{% endif %}
95
97
  </span>
96
98
 
97
99
  <!-- Tool Name -->
@@ -107,6 +109,9 @@
107
109
  <!-- Spawner delegation: show orchestrator → spawned AI -->
108
110
  <span class="child-agent-badge agent-{{ child.agent|lower|replace(' ', '-') }}">
109
111
  {{ child.agent }}
112
+ {% if child.model %}
113
+ <span class="model-indicator">{{ child.model }}</span>
114
+ {% endif %}
110
115
  </span>
111
116
  <span class="delegation-arrow">→</span>
112
117
  <span class="spawner-badge spawner-{{ child.spawner_type|lower }}">
@@ -116,9 +121,12 @@
116
121
  {% endif %}
117
122
  </span>
118
123
  {% else %}
119
- <!-- Regular agent: just show agent name -->
124
+ <!-- Regular agent: just show agent name + model if available -->
120
125
  <span class="child-agent-badge agent-{{ child.agent|lower|replace(' ', '-') }}">
121
126
  {{ child.agent }}
127
+ {% if child.model %}
128
+ <span class="model-indicator">{{ child.model }}</span>
129
+ {% endif %}
122
130
  </span>
123
131
  {% endif %}
124
132
 
@@ -132,6 +140,10 @@
132
140
  {{ child.timestamp }}
133
141
  </span>
134
142
  </div>
143
+ <!-- Recursively render nested children -->
144
+ {% if child.children %}
145
+ {{ loop(child.children) }}
146
+ {% endif %}
135
147
  {% endfor %}
136
148
  {% else %}
137
149
  <div class="no-children-message">
@@ -300,7 +312,7 @@
300
312
  flex-direction: column;
301
313
  background: rgba(163, 230, 53, 0.02);
302
314
  border-top: 1px solid var(--border-subtle);
303
- padding: 0.5rem 0rem;
315
+ padding: 0.5rem 1rem 0.5rem 2.5rem; /* Indent children under parent prompt */
304
316
  }
305
317
 
306
318
  .turn-children.collapsed {
@@ -328,9 +340,9 @@
328
340
  font-size: 0.9rem;
329
341
  font-weight: bold;
330
342
  font-family: 'JetBrains Mono', 'SF Mono', 'Monaco', monospace;
331
- white-space: pre;
343
+ white-space: nowrap; /* Changed from 'pre' to collapse template whitespace */
332
344
  flex-shrink: 0;
333
- min-width: 16px;
345
+ width: 24px; /* Fixed width for consistent alignment */
334
346
  }
335
347
 
336
348
  /* Child Tool Name */
@@ -356,7 +368,9 @@
356
368
 
357
369
  /* Child Agent Badge */
358
370
  .child-agent-badge {
359
- display: inline-block;
371
+ display: inline-flex;
372
+ align-items: center;
373
+ gap: 0.25rem;
360
374
  padding: 0.1rem 0.35rem;
361
375
  font-size: 0.6rem;
362
376
  font-weight: 600;
@@ -368,6 +382,19 @@
368
382
  flex-shrink: 0;
369
383
  }
370
384
 
385
+ /* Model Indicator Badge */
386
+ .model-indicator {
387
+ display: inline-block;
388
+ padding: 0.05rem 0.2rem;
389
+ font-size: 0.55rem;
390
+ background: rgba(100, 200, 255, 0.15);
391
+ color: #64c8ff;
392
+ border-radius: 1px;
393
+ font-weight: 700;
394
+ letter-spacing: 0.02em;
395
+ text-transform: capitalize;
396
+ }
397
+
371
398
  .child-agent-badge.agent-claude-code,
372
399
  .child-agent-badge.agent-claude {
373
400
  background: rgba(200, 255, 0, 0.15);
@@ -526,6 +553,215 @@
526
553
  width: 100%;
527
554
  }
528
555
  }
556
+
557
+ /* ============================================
558
+ Live Spawner Indicator Styles
559
+ ============================================ */
560
+
561
+ .live-spawner-indicator {
562
+ display: none;
563
+ align-items: center;
564
+ gap: 0.75rem;
565
+ padding: 0.75rem 1rem;
566
+ margin: 0.5rem;
567
+ background: linear-gradient(135deg, rgba(163, 230, 53, 0.1) 0%, rgba(100, 200, 255, 0.1) 100%);
568
+ border: 1px solid rgba(163, 230, 53, 0.3);
569
+ border-radius: 6px;
570
+ font-size: 0.8rem;
571
+ transition: all 0.3s ease;
572
+ overflow: hidden;
573
+ }
574
+
575
+ .live-spawner-indicator.active {
576
+ display: flex;
577
+ border-color: rgba(163, 230, 53, 0.6);
578
+ box-shadow: 0 0 20px rgba(163, 230, 53, 0.2);
579
+ }
580
+
581
+ .live-spawner-indicator.completed {
582
+ display: flex;
583
+ background: linear-gradient(135deg, rgba(34, 197, 94, 0.15) 0%, rgba(34, 197, 94, 0.05) 100%);
584
+ border-color: rgba(34, 197, 94, 0.5);
585
+ }
586
+
587
+ .live-spawner-indicator.failed {
588
+ display: flex;
589
+ background: linear-gradient(135deg, rgba(239, 68, 68, 0.15) 0%, rgba(239, 68, 68, 0.05) 100%);
590
+ border-color: rgba(239, 68, 68, 0.5);
591
+ }
592
+
593
+ .live-spawner-indicator.fade-out {
594
+ opacity: 0;
595
+ transform: translateY(-10px);
596
+ }
597
+
598
+ /* Spawner Icons */
599
+ .live-spawner-icon {
600
+ width: 24px;
601
+ height: 24px;
602
+ border-radius: 50%;
603
+ display: flex;
604
+ align-items: center;
605
+ justify-content: center;
606
+ flex-shrink: 0;
607
+ position: relative;
608
+ }
609
+
610
+ .live-spawner-icon::before {
611
+ content: '';
612
+ width: 12px;
613
+ height: 12px;
614
+ border-radius: 50%;
615
+ }
616
+
617
+ .live-spawner-icon.spawner-gemini {
618
+ background: rgba(74, 222, 128, 0.2);
619
+ }
620
+ .live-spawner-icon.spawner-gemini::before {
621
+ background: #4ade80;
622
+ }
623
+
624
+ .live-spawner-icon.spawner-codex {
625
+ background: rgba(100, 200, 255, 0.2);
626
+ }
627
+ .live-spawner-icon.spawner-codex::before {
628
+ background: #64c8ff;
629
+ }
630
+
631
+ .live-spawner-icon.spawner-copilot {
632
+ background: rgba(168, 85, 247, 0.2);
633
+ }
634
+ .live-spawner-icon.spawner-copilot::before {
635
+ background: #a855f7;
636
+ }
637
+
638
+ /* Pulsing animation for active spawners */
639
+ .live-spawner-icon.pulsing::after {
640
+ content: '';
641
+ position: absolute;
642
+ width: 100%;
643
+ height: 100%;
644
+ border-radius: 50%;
645
+ border: 2px solid currentColor;
646
+ animation: spawner-pulse 1.5s ease-out infinite;
647
+ }
648
+
649
+ .live-spawner-icon.spawner-gemini.pulsing::after { border-color: #4ade80; }
650
+ .live-spawner-icon.spawner-codex.pulsing::after { border-color: #64c8ff; }
651
+ .live-spawner-icon.spawner-copilot.pulsing::after { border-color: #a855f7; }
652
+
653
+ @keyframes spawner-pulse {
654
+ 0% {
655
+ transform: scale(1);
656
+ opacity: 1;
657
+ }
658
+ 100% {
659
+ transform: scale(2);
660
+ opacity: 0;
661
+ }
662
+ }
663
+
664
+ /* Streaming animation */
665
+ .live-spawner-icon.streaming::after {
666
+ content: '';
667
+ position: absolute;
668
+ width: 6px;
669
+ height: 6px;
670
+ background: currentColor;
671
+ border-radius: 50%;
672
+ animation: spawner-stream 0.6s ease-in-out infinite;
673
+ }
674
+
675
+ @keyframes spawner-stream {
676
+ 0%, 100% { opacity: 0.3; transform: translateX(-8px); }
677
+ 50% { opacity: 1; transform: translateX(8px); }
678
+ }
679
+
680
+ /* Done state */
681
+ .live-spawner-icon.done::after {
682
+ content: '';
683
+ position: absolute;
684
+ width: 8px;
685
+ height: 4px;
686
+ border-left: 2px solid #22c55e;
687
+ border-bottom: 2px solid #22c55e;
688
+ transform: rotate(-45deg);
689
+ top: 8px;
690
+ }
691
+
692
+ /* Error state */
693
+ .live-spawner-icon.error::before {
694
+ background: #ef4444;
695
+ }
696
+ .live-spawner-icon.error::after {
697
+ content: '!';
698
+ position: absolute;
699
+ color: white;
700
+ font-size: 10px;
701
+ font-weight: bold;
702
+ }
703
+
704
+ /* Text elements */
705
+ .live-spawner-text {
706
+ color: var(--text-primary);
707
+ white-space: nowrap;
708
+ }
709
+
710
+ .live-spawner-text strong {
711
+ color: var(--accent-lime, #a3e635);
712
+ letter-spacing: 0.05em;
713
+ }
714
+
715
+ .live-spawner-prompt,
716
+ .live-spawner-message {
717
+ flex: 1;
718
+ color: var(--text-secondary);
719
+ overflow: hidden;
720
+ text-overflow: ellipsis;
721
+ white-space: nowrap;
722
+ font-style: italic;
723
+ opacity: 0.8;
724
+ }
725
+
726
+ .live-spawner-progress {
727
+ padding: 0.15rem 0.4rem;
728
+ background: rgba(100, 200, 255, 0.2);
729
+ color: #64c8ff;
730
+ border-radius: 3px;
731
+ font-weight: 600;
732
+ font-size: 0.7rem;
733
+ }
734
+
735
+ .live-spawner-duration {
736
+ padding: 0.15rem 0.4rem;
737
+ background: rgba(34, 197, 94, 0.2);
738
+ color: #22c55e;
739
+ border-radius: 3px;
740
+ font-weight: 600;
741
+ font-size: 0.7rem;
742
+ }
743
+
744
+ .live-spawner-tokens {
745
+ padding: 0.15rem 0.4rem;
746
+ background: rgba(163, 230, 53, 0.2);
747
+ color: #a3e635;
748
+ border-radius: 3px;
749
+ font-size: 0.65rem;
750
+ }
751
+
752
+ .live-spawner-details {
753
+ color: var(--text-muted);
754
+ font-size: 0.7rem;
755
+ }
756
+
757
+ .live-spawner-error {
758
+ color: #ef4444;
759
+ font-size: 0.7rem;
760
+ overflow: hidden;
761
+ text-overflow: ellipsis;
762
+ white-space: nowrap;
763
+ max-width: 300px;
764
+ }
529
765
  </style>
530
766
 
531
767
  <script>
@@ -567,4 +803,277 @@ function collapseAllConversationTurns() {
567
803
  toggle.classList.remove('expanded');
568
804
  });
569
805
  }
806
+
807
+ /**
808
+ * Filter conversation turns by agent type
809
+ * @param {string} filterValue - Filter type: 'all', 'direct', 'spawner', or specific spawner type ('gemini', 'codex', 'copilot')
810
+ */
811
+ function filterByAgentType(filterValue) {
812
+ const conversationTurns = document.querySelectorAll('.conversation-turn');
813
+
814
+ conversationTurns.forEach(turn => {
815
+ const spawnType = turn.getAttribute('data-spawner-type');
816
+ let shouldShow = false;
817
+
818
+ switch (filterValue) {
819
+ case 'all':
820
+ // Show all conversation turns
821
+ shouldShow = true;
822
+ break;
823
+
824
+ case 'direct':
825
+ // Show only direct (non-spawner) turns
826
+ shouldShow = spawnType === 'direct';
827
+ break;
828
+
829
+ case 'spawner':
830
+ // Show only spawner delegation turns
831
+ shouldShow = spawnType === 'spawner';
832
+ break;
833
+
834
+ case 'gemini':
835
+ case 'codex':
836
+ case 'copilot':
837
+ // Show only turns that contain child events with matching spawner type
838
+ shouldShow = hasChildWithSpawnerType(turn, filterValue);
839
+ break;
840
+
841
+ default:
842
+ shouldShow = true;
843
+ }
844
+
845
+ // Apply visibility
846
+ if (shouldShow) {
847
+ turn.style.display = '';
848
+ } else {
849
+ turn.style.display = 'none';
850
+ }
851
+ });
852
+ }
853
+
854
+ /**
855
+ * Check if a conversation turn has any child events with the specified spawner type
856
+ * @param {HTMLElement} turn - The conversation turn element
857
+ * @param {string} spawnerType - The spawner type to search for ('gemini', 'codex', 'copilot')
858
+ * @returns {boolean} True if the turn contains at least one child with the spawner type
859
+ */
860
+ function hasChildWithSpawnerType(turn, spawnerType) {
861
+ // Look for spawner badges that match the spawner type
862
+ const spawnerBadges = turn.querySelectorAll(`.spawner-badge.spawner-${spawnerType}`);
863
+ return spawnerBadges.length > 0;
864
+ }
865
+
866
+ // ============================================
867
+ // Live Spawner Event Handling via WebSocket
868
+ // ============================================
869
+
870
+ // Track active spawner sessions by parent_event_id
871
+ const activeSpawners = new Map();
872
+
873
+ /**
874
+ * Handle incoming spawner events from WebSocket
875
+ * Called from the main WebSocket handler in dashboard
876
+ */
877
+ function handleSpawnerEvent(event) {
878
+ const { event_type, spawner_type, parent_event_id, data, timestamp } = event;
879
+
880
+ console.log('[SpawnerEvent]', event_type, spawner_type, data);
881
+
882
+ switch (event_type) {
883
+ case 'spawner_start':
884
+ showSpawnerStarted(spawner_type, parent_event_id, data, timestamp);
885
+ break;
886
+ case 'spawner_phase':
887
+ updateSpawnerPhase(spawner_type, parent_event_id, data);
888
+ break;
889
+ case 'spawner_complete':
890
+ showSpawnerCompleted(spawner_type, parent_event_id, data, timestamp);
891
+ break;
892
+ case 'spawner_tool_use':
893
+ showSpawnerToolUse(spawner_type, parent_event_id, data);
894
+ break;
895
+ case 'spawner_message':
896
+ showSpawnerMessage(spawner_type, parent_event_id, data);
897
+ break;
898
+ default:
899
+ console.log('[SpawnerEvent] Unknown event type:', event_type);
900
+ }
901
+ }
902
+
903
+ /**
904
+ * Show spawner started indicator
905
+ */
906
+ function showSpawnerStarted(spawnerType, parentEventId, data, timestamp) {
907
+ // Store in active spawners
908
+ activeSpawners.set(parentEventId || `spawner-${Date.now()}`, {
909
+ spawnerType,
910
+ startTime: new Date(timestamp),
911
+ phase: 'initializing',
912
+ data
913
+ });
914
+
915
+ // Create or update the live activity indicator
916
+ const indicator = getOrCreateLiveIndicator();
917
+ indicator.innerHTML = `
918
+ <span class="live-spawner-icon spawner-${spawnerType}"></span>
919
+ <span class="live-spawner-text">
920
+ <strong>${spawnerType.toUpperCase()}</strong> starting...
921
+ </span>
922
+ <span class="live-spawner-prompt">${(data.prompt_preview || '').substring(0, 60)}...</span>
923
+ `;
924
+ indicator.classList.add('active');
925
+ indicator.classList.remove('completed', 'failed');
926
+ }
927
+
928
+ /**
929
+ * Update spawner phase/progress
930
+ */
931
+ function updateSpawnerPhase(spawnerType, parentEventId, data) {
932
+ const indicator = getOrCreateLiveIndicator();
933
+ const phaseText = data.phase || 'processing';
934
+ const progress = data.progress;
935
+
936
+ let progressHtml = '';
937
+ if (progress !== undefined && progress !== null) {
938
+ progressHtml = `<span class="live-spawner-progress">${progress}%</span>`;
939
+ }
940
+
941
+ indicator.innerHTML = `
942
+ <span class="live-spawner-icon spawner-${spawnerType} pulsing"></span>
943
+ <span class="live-spawner-text">
944
+ <strong>${spawnerType.toUpperCase()}</strong> ${phaseText}
945
+ </span>
946
+ ${progressHtml}
947
+ ${data.details ? `<span class="live-spawner-details">${data.details}</span>` : ''}
948
+ `;
949
+ }
950
+
951
+ /**
952
+ * Show spawner completed
953
+ */
954
+ function showSpawnerCompleted(spawnerType, parentEventId, data, timestamp) {
955
+ const indicator = getOrCreateLiveIndicator();
956
+ const success = data.success;
957
+ const duration = data.duration_seconds ? `${data.duration_seconds.toFixed(1)}s` : '';
958
+
959
+ indicator.classList.remove('active');
960
+ indicator.classList.add(success ? 'completed' : 'failed');
961
+
962
+ if (success) {
963
+ indicator.innerHTML = `
964
+ <span class="live-spawner-icon spawner-${spawnerType} done"></span>
965
+ <span class="live-spawner-text">
966
+ <strong>${spawnerType.toUpperCase()}</strong> completed
967
+ </span>
968
+ <span class="live-spawner-duration">${duration}</span>
969
+ ${data.tokens_used ? `<span class="live-spawner-tokens">${data.tokens_used} tokens</span>` : ''}
970
+ `;
971
+ } else {
972
+ indicator.innerHTML = `
973
+ <span class="live-spawner-icon spawner-${spawnerType} error"></span>
974
+ <span class="live-spawner-text">
975
+ <strong>${spawnerType.toUpperCase()}</strong> failed
976
+ </span>
977
+ <span class="live-spawner-error">${data.error || 'Unknown error'}</span>
978
+ `;
979
+ }
980
+
981
+ // Remove from active spawners
982
+ if (parentEventId) {
983
+ activeSpawners.delete(parentEventId);
984
+ }
985
+
986
+ // Auto-hide after 5 seconds
987
+ setTimeout(() => {
988
+ if (!indicator.classList.contains('active')) {
989
+ indicator.classList.add('fade-out');
990
+ setTimeout(() => {
991
+ indicator.classList.remove('fade-out', 'completed', 'failed');
992
+ indicator.innerHTML = '';
993
+ }, 500);
994
+ }
995
+ }, 5000);
996
+ }
997
+
998
+ /**
999
+ * Show spawner tool use (brief flash)
1000
+ */
1001
+ function showSpawnerToolUse(spawnerType, parentEventId, data) {
1002
+ const indicator = getOrCreateLiveIndicator();
1003
+ const toolName = data.tool_name || 'unknown';
1004
+
1005
+ // Quick update showing tool use
1006
+ const currentHtml = indicator.innerHTML;
1007
+ indicator.innerHTML = `
1008
+ <span class="live-spawner-icon spawner-${spawnerType} tool-use"></span>
1009
+ <span class="live-spawner-text">
1010
+ <strong>${spawnerType.toUpperCase()}</strong> using ${toolName}
1011
+ </span>
1012
+ `;
1013
+
1014
+ // Revert after brief display (if still active)
1015
+ setTimeout(() => {
1016
+ if (activeSpawners.has(parentEventId)) {
1017
+ // Still active, show executing state
1018
+ indicator.innerHTML = `
1019
+ <span class="live-spawner-icon spawner-${spawnerType} pulsing"></span>
1020
+ <span class="live-spawner-text">
1021
+ <strong>${spawnerType.toUpperCase()}</strong> executing
1022
+ </span>
1023
+ `;
1024
+ }
1025
+ }, 1500);
1026
+ }
1027
+
1028
+ /**
1029
+ * Show spawner message (streaming text)
1030
+ */
1031
+ function showSpawnerMessage(spawnerType, parentEventId, data) {
1032
+ // Just update the indicator with message preview
1033
+ const indicator = getOrCreateLiveIndicator();
1034
+ const preview = (data.message_preview || '').substring(0, 80);
1035
+
1036
+ indicator.innerHTML = `
1037
+ <span class="live-spawner-icon spawner-${spawnerType} streaming"></span>
1038
+ <span class="live-spawner-text">
1039
+ <strong>${spawnerType.toUpperCase()}</strong> responding
1040
+ </span>
1041
+ <span class="live-spawner-message">${preview}...</span>
1042
+ `;
1043
+ }
1044
+
1045
+ /**
1046
+ * Get or create the live activity indicator element
1047
+ */
1048
+ function getOrCreateLiveIndicator() {
1049
+ let indicator = document.getElementById('live-spawner-indicator');
1050
+ if (!indicator) {
1051
+ indicator = document.createElement('div');
1052
+ indicator.id = 'live-spawner-indicator';
1053
+ indicator.className = 'live-spawner-indicator';
1054
+
1055
+ // Insert at top of conversation feed
1056
+ const feed = document.querySelector('.conversation-feed');
1057
+ if (feed) {
1058
+ feed.insertBefore(indicator, feed.firstChild);
1059
+ } else {
1060
+ // Fallback: insert in activity feed view
1061
+ const view = document.querySelector('.activity-feed-view');
1062
+ if (view) {
1063
+ const header = view.querySelector('.view-header');
1064
+ if (header && header.nextSibling) {
1065
+ view.insertBefore(indicator, header.nextSibling);
1066
+ } else {
1067
+ view.appendChild(indicator);
1068
+ }
1069
+ }
1070
+ }
1071
+ }
1072
+ return indicator;
1073
+ }
1074
+
1075
+ // Export for use by main WebSocket handler
1076
+ if (typeof window !== 'undefined') {
1077
+ window.handleSpawnerEvent = handleSpawnerEvent;
1078
+ }
570
1079
  </script>
htmlgraph/cli.py CHANGED
@@ -150,7 +150,7 @@ def cmd_serve(args: argparse.Namespace) -> None:
150
150
  # Default to database in graph dir if not specified
151
151
  db_path = getattr(args, "db", None)
152
152
  if not db_path:
153
- db_path = str(Path(args.graph_dir) / "index.sqlite")
153
+ db_path = str(Path(args.graph_dir) / "htmlgraph.db")
154
154
 
155
155
  result = start_fastapi_server(
156
156
  port=args.port,