portacode 1.3.32__py3-none-any.whl → 1.4.11.dev5__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.
Potentially problematic release.
This version of portacode might be problematic. Click here for more details.
- portacode/_version.py +2 -2
- portacode/cli.py +158 -14
- portacode/connection/client.py +127 -8
- portacode/connection/handlers/WEBSOCKET_PROTOCOL.md +370 -4
- portacode/connection/handlers/__init__.py +16 -1
- portacode/connection/handlers/diff_handlers.py +603 -0
- portacode/connection/handlers/file_handlers.py +674 -17
- portacode/connection/handlers/project_aware_file_handlers.py +11 -0
- portacode/connection/handlers/project_state/file_system_watcher.py +31 -61
- portacode/connection/handlers/project_state/git_manager.py +139 -572
- portacode/connection/handlers/project_state/handlers.py +28 -14
- portacode/connection/handlers/project_state/manager.py +226 -101
- portacode/connection/handlers/proxmox_infra.py +790 -0
- portacode/connection/handlers/session.py +465 -84
- portacode/connection/handlers/system_handlers.py +181 -8
- portacode/connection/handlers/tab_factory.py +1 -47
- portacode/connection/handlers/update_handler.py +61 -0
- portacode/connection/terminal.py +55 -10
- portacode/keypair.py +63 -1
- portacode/link_capture/__init__.py +38 -0
- portacode/link_capture/__pycache__/__init__.cpython-311.pyc +0 -0
- portacode/link_capture/bin/__pycache__/link_capture_wrapper.cpython-311.pyc +0 -0
- portacode/link_capture/bin/elinks +3 -0
- portacode/link_capture/bin/gio-open +3 -0
- portacode/link_capture/bin/gnome-open +3 -0
- portacode/link_capture/bin/gvfs-open +3 -0
- portacode/link_capture/bin/kde-open +3 -0
- portacode/link_capture/bin/kfmclient +3 -0
- portacode/link_capture/bin/link_capture_exec.sh +11 -0
- portacode/link_capture/bin/link_capture_wrapper.py +75 -0
- portacode/link_capture/bin/links +3 -0
- portacode/link_capture/bin/links2 +3 -0
- portacode/link_capture/bin/lynx +3 -0
- portacode/link_capture/bin/mate-open +3 -0
- portacode/link_capture/bin/netsurf +3 -0
- portacode/link_capture/bin/sensible-browser +3 -0
- portacode/link_capture/bin/w3m +3 -0
- portacode/link_capture/bin/x-www-browser +3 -0
- portacode/link_capture/bin/xdg-open +3 -0
- portacode/pairing.py +103 -0
- portacode/static/js/utils/ntp-clock.js +170 -79
- portacode/utils/diff_apply.py +456 -0
- portacode/utils/diff_renderer.py +371 -0
- portacode/utils/ntp_clock.py +45 -131
- {portacode-1.3.32.dist-info → portacode-1.4.11.dev5.dist-info}/METADATA +71 -3
- portacode-1.4.11.dev5.dist-info/RECORD +97 -0
- test_modules/test_device_online.py +1 -1
- test_modules/test_login_flow.py +8 -4
- test_modules/test_play_store_screenshots.py +294 -0
- testing_framework/.env.example +4 -1
- testing_framework/core/playwright_manager.py +63 -9
- portacode-1.3.32.dist-info/RECORD +0 -70
- {portacode-1.3.32.dist-info → portacode-1.4.11.dev5.dist-info}/WHEEL +0 -0
- {portacode-1.3.32.dist-info → portacode-1.4.11.dev5.dist-info}/entry_points.txt +0 -0
- {portacode-1.3.32.dist-info → portacode-1.4.11.dev5.dist-info}/licenses/LICENSE +0 -0
- {portacode-1.3.32.dist-info → portacode-1.4.11.dev5.dist-info}/top_level.txt +0 -0
|
@@ -9,6 +9,7 @@ import asyncio
|
|
|
9
9
|
import hashlib
|
|
10
10
|
import logging
|
|
11
11
|
import os
|
|
12
|
+
import threading
|
|
12
13
|
import time
|
|
13
14
|
from pathlib import Path
|
|
14
15
|
from typing import Any, Callable, Dict, List, Optional, Tuple, Union
|
|
@@ -36,30 +37,25 @@ except ImportError:
|
|
|
36
37
|
DIFF_MATCH_PATCH_AVAILABLE = False
|
|
37
38
|
diff_match_patch = None
|
|
38
39
|
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
from pygments.util import ClassNotFound
|
|
45
|
-
PYGMENTS_AVAILABLE = True
|
|
46
|
-
except ImportError:
|
|
47
|
-
PYGMENTS_AVAILABLE = False
|
|
48
|
-
highlight = None
|
|
49
|
-
get_lexer_for_filename = None
|
|
50
|
-
get_lexer_by_name = None
|
|
51
|
-
HtmlFormatter = None
|
|
52
|
-
ClassNotFound = Exception
|
|
40
|
+
from portacode.utils import diff_renderer
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
_GIT_PROCESS_REF_COUNTS: Dict[int, int] = {}
|
|
44
|
+
_GIT_PROCESS_LOCK = threading.Lock()
|
|
53
45
|
|
|
54
46
|
|
|
55
47
|
class GitManager:
|
|
56
48
|
"""Manages Git operations for project state."""
|
|
57
49
|
|
|
58
|
-
def __init__(self, project_path: str, change_callback: Optional[Callable] = None):
|
|
50
|
+
def __init__(self, project_path: str, change_callback: Optional[Callable] = None, owner_session_id: Optional[str] = None):
|
|
59
51
|
self.project_path = project_path
|
|
60
52
|
self.repo: Optional[Repo] = None
|
|
61
53
|
self.is_git_repo = False
|
|
62
54
|
self._change_callback = change_callback
|
|
55
|
+
self.owner_session_id = owner_session_id
|
|
56
|
+
|
|
57
|
+
# Track git processes spawned by this specific GitManager instance
|
|
58
|
+
self._tracked_git_processes = set()
|
|
63
59
|
|
|
64
60
|
# Periodic monitoring attributes
|
|
65
61
|
self._monitoring_task: Optional[asyncio.Task] = None
|
|
@@ -84,9 +80,33 @@ class GitManager:
|
|
|
84
80
|
self.repo = Repo(self.project_path)
|
|
85
81
|
self.is_git_repo = True
|
|
86
82
|
logger.info("Initialized Git repo for project: %s", self.project_path)
|
|
83
|
+
|
|
84
|
+
# Track initial git processes after repo creation
|
|
85
|
+
self._track_current_git_processes()
|
|
86
|
+
|
|
87
87
|
except (InvalidGitRepositoryError, Exception) as e:
|
|
88
88
|
logger.debug("Not a Git repository or Git error: %s", e)
|
|
89
89
|
|
|
90
|
+
def _track_current_git_processes(self):
|
|
91
|
+
"""Track currently running git cat-file processes for this repo."""
|
|
92
|
+
try:
|
|
93
|
+
import psutil
|
|
94
|
+
|
|
95
|
+
for proc in psutil.process_iter(['pid', 'cmdline', 'cwd']):
|
|
96
|
+
try:
|
|
97
|
+
cmdline = proc.info['cmdline']
|
|
98
|
+
if (cmdline and len(cmdline) >= 2 and
|
|
99
|
+
'git' in cmdline[0] and 'cat-file' in cmdline[1] and
|
|
100
|
+
proc.info['cwd'] == self.project_path):
|
|
101
|
+
pid = proc.pid
|
|
102
|
+
self._tracked_git_processes.add(pid)
|
|
103
|
+
with _GIT_PROCESS_LOCK:
|
|
104
|
+
_GIT_PROCESS_REF_COUNTS[pid] = _GIT_PROCESS_REF_COUNTS.get(pid, 0) + 1
|
|
105
|
+
except (psutil.NoSuchProcess, psutil.AccessDenied):
|
|
106
|
+
continue
|
|
107
|
+
except Exception as e:
|
|
108
|
+
logger.debug("Error tracking git processes: %s", e)
|
|
109
|
+
|
|
90
110
|
def reinitialize(self):
|
|
91
111
|
"""Reinitialize git repo detection (useful when .git directory is created after initialization)."""
|
|
92
112
|
logger.info("Reinitializing git repo detection for: %s", self.project_path)
|
|
@@ -634,543 +654,9 @@ class GitManager:
|
|
|
634
654
|
logger.error("Error computing diff details: %s", e)
|
|
635
655
|
return None
|
|
636
656
|
|
|
637
|
-
def _get_pygments_lexer(self, file_path: str) -> Optional[object]:
|
|
638
|
-
"""Get Pygments lexer for a file path using built-in detection."""
|
|
639
|
-
if not PYGMENTS_AVAILABLE:
|
|
640
|
-
return None
|
|
641
|
-
|
|
642
|
-
try:
|
|
643
|
-
# Use Pygments' built-in filename detection
|
|
644
|
-
lexer = get_lexer_for_filename(file_path)
|
|
645
|
-
return lexer
|
|
646
|
-
except ClassNotFound:
|
|
647
|
-
# If no lexer found, return None (will fall back to plain text)
|
|
648
|
-
logger.debug("No Pygments lexer found for file: %s", file_path)
|
|
649
|
-
return None
|
|
650
|
-
except Exception as e:
|
|
651
|
-
logger.debug("Error getting Pygments lexer: %s", e)
|
|
652
|
-
return None
|
|
653
|
-
|
|
654
657
|
def _generate_html_diff(self, original_content: str, modified_content: str, file_path: str) -> Optional[Dict[str, str]]:
|
|
655
|
-
"""
|
|
656
|
-
|
|
657
|
-
logger.debug("Pygments not available for HTML diff generation")
|
|
658
|
-
return None
|
|
659
|
-
|
|
660
|
-
# Add performance safeguards to prevent blocking
|
|
661
|
-
max_content_size = 500000 # 500KB max per file (more reasonable)
|
|
662
|
-
max_lines = 5000 # Max 5000 lines per file (more reasonable for real projects)
|
|
663
|
-
|
|
664
|
-
original_line_count = original_content.count('\n')
|
|
665
|
-
modified_line_count = modified_content.count('\n')
|
|
666
|
-
max_line_count = max(original_line_count, modified_line_count)
|
|
667
|
-
|
|
668
|
-
# Check if file is too large for full processing
|
|
669
|
-
is_large_file = (len(original_content) > max_content_size or
|
|
670
|
-
len(modified_content) > max_content_size or
|
|
671
|
-
max_line_count > max_lines)
|
|
672
|
-
|
|
673
|
-
if is_large_file:
|
|
674
|
-
logger.warning(f"Large file detected for diff generation: {file_path} ({max_line_count} lines)")
|
|
675
|
-
# Generate simplified diff without syntax highlighting for large files
|
|
676
|
-
return self._generate_simple_diff_html(original_content, modified_content, file_path)
|
|
677
|
-
|
|
678
|
-
try:
|
|
679
|
-
import difflib
|
|
680
|
-
import time
|
|
681
|
-
|
|
682
|
-
start_time = time.time()
|
|
683
|
-
timeout_seconds = 5 # 5 second timeout
|
|
684
|
-
|
|
685
|
-
# Get line-based diff using Python's difflib (similar to git diff)
|
|
686
|
-
original_lines = original_content.splitlines(keepends=True)
|
|
687
|
-
modified_lines = modified_content.splitlines(keepends=True)
|
|
688
|
-
|
|
689
|
-
# Check timeout
|
|
690
|
-
if time.time() - start_time > timeout_seconds:
|
|
691
|
-
logger.warning(f"Diff generation timeout for {file_path}")
|
|
692
|
-
return None
|
|
693
|
-
|
|
694
|
-
# Generate both minimal and full diff with performance safeguards
|
|
695
|
-
minimal_diff_lines = list(difflib.unified_diff(
|
|
696
|
-
original_lines,
|
|
697
|
-
modified_lines,
|
|
698
|
-
fromfile='a/' + os.path.basename(file_path),
|
|
699
|
-
tofile='b/' + os.path.basename(file_path),
|
|
700
|
-
lineterm='',
|
|
701
|
-
n=3 # 3 lines of context (default)
|
|
702
|
-
))
|
|
703
|
-
|
|
704
|
-
# Check timeout
|
|
705
|
-
if time.time() - start_time > timeout_seconds:
|
|
706
|
-
logger.warning(f"Diff generation timeout for {file_path}")
|
|
707
|
-
return None
|
|
708
|
-
|
|
709
|
-
# Generate full context diff only if file is small enough
|
|
710
|
-
if len(original_lines) + len(modified_lines) < 2000: # Increased threshold for better UX
|
|
711
|
-
full_diff_lines = list(difflib.unified_diff(
|
|
712
|
-
original_lines,
|
|
713
|
-
modified_lines,
|
|
714
|
-
fromfile='a/' + os.path.basename(file_path),
|
|
715
|
-
tofile='b/' + os.path.basename(file_path),
|
|
716
|
-
lineterm='',
|
|
717
|
-
n=len(original_lines) + len(modified_lines) # Show all lines
|
|
718
|
-
))
|
|
719
|
-
else:
|
|
720
|
-
full_diff_lines = minimal_diff_lines # Use minimal for large files
|
|
721
|
-
|
|
722
|
-
# Parse diffs (simplified but restored)
|
|
723
|
-
minimal_parsed = self._parse_unified_diff_simple(minimal_diff_lines)
|
|
724
|
-
full_parsed = self._parse_unified_diff_simple(full_diff_lines)
|
|
725
|
-
|
|
726
|
-
# Check timeout
|
|
727
|
-
if time.time() - start_time > timeout_seconds:
|
|
728
|
-
logger.warning(f"Diff generation timeout for {file_path}")
|
|
729
|
-
return None
|
|
730
|
-
|
|
731
|
-
# Generate HTML for both versions
|
|
732
|
-
minimal_html = self._generate_diff_html(minimal_parsed, file_path, 'minimal')
|
|
733
|
-
full_html = self._generate_diff_html(full_parsed, file_path, 'full')
|
|
734
|
-
|
|
735
|
-
return {
|
|
736
|
-
'minimal': minimal_html,
|
|
737
|
-
'full': full_html
|
|
738
|
-
}
|
|
739
|
-
|
|
740
|
-
except Exception as e:
|
|
741
|
-
logger.error("Error generating HTML diff: %s", e)
|
|
742
|
-
return None
|
|
743
|
-
|
|
744
|
-
def _generate_simple_diff_html(self, original_content: str, modified_content: str, file_path: str) -> Dict[str, str]:
|
|
745
|
-
"""Generate simplified diff HTML for large files without syntax highlighting."""
|
|
746
|
-
try:
|
|
747
|
-
import difflib
|
|
748
|
-
|
|
749
|
-
# Get line-based diff using Python's difflib
|
|
750
|
-
original_lines = original_content.splitlines(keepends=True)
|
|
751
|
-
modified_lines = modified_content.splitlines(keepends=True)
|
|
752
|
-
|
|
753
|
-
# Generate minimal diff only for large files
|
|
754
|
-
diff_lines = list(difflib.unified_diff(
|
|
755
|
-
original_lines,
|
|
756
|
-
modified_lines,
|
|
757
|
-
fromfile='a/' + os.path.basename(file_path),
|
|
758
|
-
tofile='b/' + os.path.basename(file_path),
|
|
759
|
-
lineterm='',
|
|
760
|
-
n=3 # Keep minimal context
|
|
761
|
-
))
|
|
762
|
-
|
|
763
|
-
# Parse with simple parser (no syntax highlighting)
|
|
764
|
-
parsed = self._parse_unified_diff_simple(diff_lines)
|
|
765
|
-
|
|
766
|
-
# Limit to reasonable size for large files
|
|
767
|
-
max_simple_diff_lines = 500
|
|
768
|
-
if len(parsed) > max_simple_diff_lines:
|
|
769
|
-
parsed = parsed[:max_simple_diff_lines]
|
|
770
|
-
logger.info(f"Truncated large diff to {max_simple_diff_lines} lines for {file_path}")
|
|
771
|
-
|
|
772
|
-
# Generate HTML without syntax highlighting but with good UI
|
|
773
|
-
html = self._generate_simple_diff_html_content(parsed, file_path)
|
|
774
|
-
|
|
775
|
-
return {
|
|
776
|
-
'minimal': html,
|
|
777
|
-
'full': html # Same for both to keep UI consistent
|
|
778
|
-
}
|
|
779
|
-
|
|
780
|
-
except Exception as e:
|
|
781
|
-
logger.error(f"Error generating simple diff HTML: {e}")
|
|
782
|
-
return {
|
|
783
|
-
'minimal': self._generate_fallback_diff_html(file_path),
|
|
784
|
-
'full': self._generate_fallback_diff_html(file_path)
|
|
785
|
-
}
|
|
786
|
-
|
|
787
|
-
def _generate_simple_diff_html_content(self, parsed_diff: List[Dict], file_path: str) -> str:
|
|
788
|
-
"""Generate simple HTML diff content without syntax highlighting but with good UI."""
|
|
789
|
-
html_parts = []
|
|
790
|
-
html_parts.append('<div class="unified-diff-container" data-view-mode="minimal">')
|
|
791
|
-
|
|
792
|
-
# Add stats header (no toggle for large files to keep it simple)
|
|
793
|
-
line_additions = sum(1 for line in parsed_diff if line['type'] == 'add')
|
|
794
|
-
line_deletions = sum(1 for line in parsed_diff if line['type'] == 'delete')
|
|
795
|
-
|
|
796
|
-
html_parts.append(f'''
|
|
797
|
-
<div class="diff-stats">
|
|
798
|
-
<div class="diff-stats-left">
|
|
799
|
-
<span class="additions">+{line_additions}</span>
|
|
800
|
-
<span class="deletions">-{line_deletions}</span>
|
|
801
|
-
<span class="file-path">{os.path.basename(file_path)} (Large file - simplified view)</span>
|
|
802
|
-
</div>
|
|
803
|
-
</div>
|
|
804
|
-
''')
|
|
805
|
-
|
|
806
|
-
# Generate content without syntax highlighting
|
|
807
|
-
html_parts.append('<div class="diff-content">')
|
|
808
|
-
html_parts.append('<table class="diff-table">')
|
|
809
|
-
|
|
810
|
-
for line_info in parsed_diff:
|
|
811
|
-
if line_info['type'] == 'header':
|
|
812
|
-
continue # Skip headers
|
|
813
|
-
|
|
814
|
-
line_type = line_info['type']
|
|
815
|
-
old_line_num = line_info.get('old_line_num', '')
|
|
816
|
-
new_line_num = line_info.get('new_line_num', '')
|
|
817
|
-
content = line_info['content']
|
|
818
|
-
|
|
819
|
-
# Simple HTML escaping without syntax highlighting
|
|
820
|
-
escaped_content = self._escape_html(content)
|
|
821
|
-
|
|
822
|
-
row_class = f'diff-line diff-{line_type}'
|
|
823
|
-
html_parts.append(f'''
|
|
824
|
-
<tr class="{row_class}">
|
|
825
|
-
<td class="line-num old-line-num">{old_line_num}</td>
|
|
826
|
-
<td class="line-num new-line-num">{new_line_num}</td>
|
|
827
|
-
<td class="line-content">{escaped_content}</td>
|
|
828
|
-
</tr>
|
|
829
|
-
''')
|
|
830
|
-
|
|
831
|
-
html_parts.append('</table>')
|
|
832
|
-
html_parts.append('</div>')
|
|
833
|
-
html_parts.append('</div>')
|
|
834
|
-
|
|
835
|
-
return ''.join(html_parts)
|
|
836
|
-
|
|
837
|
-
def _generate_fallback_diff_html(self, file_path: str) -> str:
|
|
838
|
-
"""Generate minimal fallback HTML when all else fails."""
|
|
839
|
-
return f'''
|
|
840
|
-
<div class="unified-diff-container" data-view-mode="minimal">
|
|
841
|
-
<div class="diff-stats">
|
|
842
|
-
<div class="diff-stats-left">
|
|
843
|
-
<span class="file-path">{os.path.basename(file_path)} (Diff unavailable)</span>
|
|
844
|
-
</div>
|
|
845
|
-
</div>
|
|
846
|
-
<div class="diff-content">
|
|
847
|
-
<div style="padding: 2rem; text-align: center; color: var(--text-secondary);">
|
|
848
|
-
<i class="fas fa-exclamation-triangle" style="font-size: 2rem; margin-bottom: 1rem;"></i>
|
|
849
|
-
<p>Diff view unavailable for this file</p>
|
|
850
|
-
<p style="font-size: 0.9rem;">File may be too large or binary</p>
|
|
851
|
-
</div>
|
|
852
|
-
</div>
|
|
853
|
-
</div>
|
|
854
|
-
'''
|
|
855
|
-
|
|
856
|
-
def _parse_unified_diff_simple(self, diff_lines):
|
|
857
|
-
"""Simple unified diff parser without intra-line highlighting for better performance."""
|
|
858
|
-
parsed = []
|
|
859
|
-
old_line_num = 0
|
|
860
|
-
new_line_num = 0
|
|
861
|
-
|
|
862
|
-
for line in diff_lines:
|
|
863
|
-
if line.startswith('@@'):
|
|
864
|
-
# Parse hunk header to get line numbers
|
|
865
|
-
import re
|
|
866
|
-
match = re.match(r'@@ -(\d+)(?:,\d+)? \+(\d+)(?:,\d+)? @@', line)
|
|
867
|
-
if match:
|
|
868
|
-
old_line_num = int(match.group(1)) - 1
|
|
869
|
-
new_line_num = int(match.group(2)) - 1
|
|
870
|
-
|
|
871
|
-
parsed.append({
|
|
872
|
-
'type': 'header',
|
|
873
|
-
'content': line,
|
|
874
|
-
'old_line_num': '',
|
|
875
|
-
'new_line_num': ''
|
|
876
|
-
})
|
|
877
|
-
elif line.startswith('---') or line.startswith('+++'):
|
|
878
|
-
# Skip diff file headers (--- a/file, +++ b/file)
|
|
879
|
-
parsed.append({
|
|
880
|
-
'type': 'header',
|
|
881
|
-
'content': line,
|
|
882
|
-
'old_line_num': '',
|
|
883
|
-
'new_line_num': ''
|
|
884
|
-
})
|
|
885
|
-
elif line.startswith('-'):
|
|
886
|
-
old_line_num += 1
|
|
887
|
-
parsed.append({
|
|
888
|
-
'type': 'delete',
|
|
889
|
-
'old_line_num': old_line_num,
|
|
890
|
-
'new_line_num': '',
|
|
891
|
-
'content': line
|
|
892
|
-
})
|
|
893
|
-
elif line.startswith('+'):
|
|
894
|
-
new_line_num += 1
|
|
895
|
-
parsed.append({
|
|
896
|
-
'type': 'add',
|
|
897
|
-
'old_line_num': '',
|
|
898
|
-
'new_line_num': new_line_num,
|
|
899
|
-
'content': line
|
|
900
|
-
})
|
|
901
|
-
elif line.startswith(' '):
|
|
902
|
-
old_line_num += 1
|
|
903
|
-
new_line_num += 1
|
|
904
|
-
parsed.append({
|
|
905
|
-
'type': 'context',
|
|
906
|
-
'old_line_num': old_line_num,
|
|
907
|
-
'new_line_num': new_line_num,
|
|
908
|
-
'content': line
|
|
909
|
-
})
|
|
910
|
-
|
|
911
|
-
return parsed
|
|
912
|
-
|
|
913
|
-
def _generate_diff_html(self, parsed_diff: List[Dict], file_path: str, view_mode: str) -> str:
|
|
914
|
-
"""Generate HTML for a parsed diff."""
|
|
915
|
-
# Limit diff size to prevent performance issues
|
|
916
|
-
max_diff_lines = 1000 # Increased limit for better UX
|
|
917
|
-
if len(parsed_diff) > max_diff_lines:
|
|
918
|
-
logger.warning(f"Diff too large, truncating: {file_path} ({len(parsed_diff)} lines)")
|
|
919
|
-
parsed_diff = parsed_diff[:max_diff_lines]
|
|
920
|
-
|
|
921
|
-
# Get Pygments lexer for syntax highlighting
|
|
922
|
-
lexer = self._get_pygments_lexer(file_path)
|
|
923
|
-
|
|
924
|
-
# Pre-highlight all unique lines for better context-aware syntax highlighting
|
|
925
|
-
unique_lines = set()
|
|
926
|
-
for line_info in parsed_diff:
|
|
927
|
-
if line_info['type'] != 'header' and 'content' in line_info:
|
|
928
|
-
content = line_info['content']
|
|
929
|
-
if content and content[0] in '+- ':
|
|
930
|
-
clean_line = content[1:].rstrip('\n')
|
|
931
|
-
if clean_line.strip():
|
|
932
|
-
unique_lines.add(clean_line)
|
|
933
|
-
|
|
934
|
-
# Pre-highlight all unique lines as a batch for better context
|
|
935
|
-
highlighted_cache = {}
|
|
936
|
-
if lexer and unique_lines:
|
|
937
|
-
try:
|
|
938
|
-
# Combine all lines to give Pygments better context
|
|
939
|
-
combined_content = '\n'.join(unique_lines)
|
|
940
|
-
combined_highlighted = highlight(combined_content, lexer, HtmlFormatter(nowrap=True, noclasses=False, style='monokai'))
|
|
941
|
-
|
|
942
|
-
# Split back into individual lines
|
|
943
|
-
highlighted_lines = combined_highlighted.split('\n')
|
|
944
|
-
unique_lines_list = list(unique_lines)
|
|
945
|
-
|
|
946
|
-
for i, line in enumerate(unique_lines_list):
|
|
947
|
-
if i < len(highlighted_lines):
|
|
948
|
-
highlighted_cache[line] = highlighted_lines[i]
|
|
949
|
-
except Exception as e:
|
|
950
|
-
logger.debug(f"Error in batch syntax highlighting: {e}")
|
|
951
|
-
highlighted_cache = {}
|
|
952
|
-
|
|
953
|
-
# Build HTML
|
|
954
|
-
html_parts = []
|
|
955
|
-
html_parts.append(f'<div class="unified-diff-container" data-view-mode="{view_mode}">')
|
|
956
|
-
|
|
957
|
-
# Add stats header with toggle
|
|
958
|
-
line_additions = sum(1 for line in parsed_diff if line['type'] == 'add')
|
|
959
|
-
line_deletions = sum(1 for line in parsed_diff if line['type'] == 'delete')
|
|
960
|
-
|
|
961
|
-
html_parts.append(f'''
|
|
962
|
-
<div class="diff-stats">
|
|
963
|
-
<div class="diff-stats-left">
|
|
964
|
-
<span class="additions">+{line_additions}</span>
|
|
965
|
-
<span class="deletions">-{line_deletions}</span>
|
|
966
|
-
<span class="file-path">{os.path.basename(file_path)}</span>
|
|
967
|
-
</div>
|
|
968
|
-
<div class="diff-stats-right">
|
|
969
|
-
<button class="diff-toggle-btn" data-current-mode="{view_mode}">
|
|
970
|
-
<i class="fas fa-eye"></i>
|
|
971
|
-
<span class="toggle-text"></span>
|
|
972
|
-
</button>
|
|
973
|
-
</div>
|
|
974
|
-
</div>
|
|
975
|
-
''')
|
|
976
|
-
|
|
977
|
-
# Generate unified diff view
|
|
978
|
-
html_parts.append('<div class="diff-content">')
|
|
979
|
-
html_parts.append('<table class="diff-table">')
|
|
980
|
-
|
|
981
|
-
for line_info in parsed_diff:
|
|
982
|
-
if line_info['type'] == 'header':
|
|
983
|
-
continue # Skip all diff headers including --- and +++ lines
|
|
984
|
-
|
|
985
|
-
line_type = line_info['type']
|
|
986
|
-
old_line_num = line_info.get('old_line_num', '')
|
|
987
|
-
new_line_num = line_info.get('new_line_num', '')
|
|
988
|
-
content = line_info['content']
|
|
989
|
-
|
|
990
|
-
# Apply syntax highlighting using pre-highlighted cache for better accuracy
|
|
991
|
-
if content and content[0] in '+- ':
|
|
992
|
-
prefix = content[0] if content[0] in '+-' else ' '
|
|
993
|
-
clean_content = content[1:].rstrip('\n')
|
|
994
|
-
|
|
995
|
-
# Use pre-highlighted cache if available
|
|
996
|
-
if clean_content.strip() and clean_content in highlighted_cache:
|
|
997
|
-
final_content = prefix + highlighted_cache[clean_content]
|
|
998
|
-
elif clean_content.strip():
|
|
999
|
-
# Fallback to individual line highlighting
|
|
1000
|
-
try:
|
|
1001
|
-
highlighted = highlight(clean_content, lexer, HtmlFormatter(nowrap=True, noclasses=False, style='monokai'))
|
|
1002
|
-
final_content = prefix + highlighted
|
|
1003
|
-
except Exception as e:
|
|
1004
|
-
logger.debug("Error applying syntax highlighting: %s", e)
|
|
1005
|
-
final_content = self._escape_html(content)
|
|
1006
|
-
else:
|
|
1007
|
-
final_content = self._escape_html(content)
|
|
1008
|
-
else:
|
|
1009
|
-
final_content = self._escape_html(content)
|
|
1010
|
-
|
|
1011
|
-
# CSS classes for different line types
|
|
1012
|
-
row_class = f'diff-line diff-{line_type}'
|
|
1013
|
-
|
|
1014
|
-
html_parts.append(f'''
|
|
1015
|
-
<tr class="{row_class}">
|
|
1016
|
-
<td class="line-num old-line-num">{old_line_num}</td>
|
|
1017
|
-
<td class="line-num new-line-num">{new_line_num}</td>
|
|
1018
|
-
<td class="line-content">{final_content}</td>
|
|
1019
|
-
</tr>
|
|
1020
|
-
''')
|
|
1021
|
-
|
|
1022
|
-
html_parts.append('</table>')
|
|
1023
|
-
html_parts.append('</div>')
|
|
1024
|
-
html_parts.append('</div>')
|
|
1025
|
-
|
|
1026
|
-
return ''.join(html_parts)
|
|
1027
|
-
|
|
1028
|
-
def _parse_unified_diff_with_intraline(self, diff_lines, original_lines, modified_lines):
|
|
1029
|
-
"""Parse unified diff and add intra-line character highlighting."""
|
|
1030
|
-
parsed = []
|
|
1031
|
-
old_line_num = 0
|
|
1032
|
-
new_line_num = 0
|
|
1033
|
-
|
|
1034
|
-
pending_deletes = []
|
|
1035
|
-
pending_adds = []
|
|
1036
|
-
|
|
1037
|
-
def flush_pending():
|
|
1038
|
-
"""Process pending delete/add pairs for intra-line highlighting."""
|
|
1039
|
-
if pending_deletes and pending_adds:
|
|
1040
|
-
# Apply intra-line highlighting to delete/add pairs
|
|
1041
|
-
for i, (del_line, add_line) in enumerate(zip(pending_deletes, pending_adds)):
|
|
1042
|
-
del_content = del_line['content'][1:] # Remove '-' prefix
|
|
1043
|
-
add_content = add_line['content'][1:] # Remove '+' prefix
|
|
1044
|
-
|
|
1045
|
-
del_highlighted, add_highlighted = self._generate_intraline_diff(del_content, add_content)
|
|
1046
|
-
|
|
1047
|
-
# Update the parsed lines with intra-line highlighting
|
|
1048
|
-
del_line['intraline_html'] = '-' + del_highlighted
|
|
1049
|
-
add_line['intraline_html'] = '+' + add_highlighted
|
|
1050
|
-
|
|
1051
|
-
parsed.append(del_line)
|
|
1052
|
-
parsed.append(add_line)
|
|
1053
|
-
|
|
1054
|
-
# Handle remaining unmatched deletes/adds
|
|
1055
|
-
for del_line in pending_deletes[len(pending_adds):]:
|
|
1056
|
-
parsed.append(del_line)
|
|
1057
|
-
for add_line in pending_adds[len(pending_deletes):]:
|
|
1058
|
-
parsed.append(add_line)
|
|
1059
|
-
else:
|
|
1060
|
-
# No pairs to highlight, just add them as-is
|
|
1061
|
-
parsed.extend(pending_deletes)
|
|
1062
|
-
parsed.extend(pending_adds)
|
|
1063
|
-
|
|
1064
|
-
pending_deletes.clear()
|
|
1065
|
-
pending_adds.clear()
|
|
1066
|
-
|
|
1067
|
-
for line in diff_lines:
|
|
1068
|
-
if line.startswith('@@'):
|
|
1069
|
-
# Flush any pending changes before hunk header
|
|
1070
|
-
flush_pending()
|
|
1071
|
-
|
|
1072
|
-
# Parse hunk header to get line numbers
|
|
1073
|
-
import re
|
|
1074
|
-
match = re.match(r'@@ -(\d+)(?:,\d+)? \+(\d+)(?:,\d+)? @@', line)
|
|
1075
|
-
if match:
|
|
1076
|
-
old_line_num = int(match.group(1)) - 1
|
|
1077
|
-
new_line_num = int(match.group(2)) - 1
|
|
1078
|
-
|
|
1079
|
-
parsed.append({
|
|
1080
|
-
'type': 'header',
|
|
1081
|
-
'content': line,
|
|
1082
|
-
'old_line_num': '',
|
|
1083
|
-
'new_line_num': ''
|
|
1084
|
-
})
|
|
1085
|
-
elif line.startswith('---') or line.startswith('+++'):
|
|
1086
|
-
# Skip diff file headers (--- a/file, +++ b/file)
|
|
1087
|
-
parsed.append({
|
|
1088
|
-
'type': 'header',
|
|
1089
|
-
'content': line,
|
|
1090
|
-
'old_line_num': '',
|
|
1091
|
-
'new_line_num': ''
|
|
1092
|
-
})
|
|
1093
|
-
elif line.startswith('-'):
|
|
1094
|
-
pending_deletes.append({
|
|
1095
|
-
'type': 'delete',
|
|
1096
|
-
'old_line_num': old_line_num + 1,
|
|
1097
|
-
'new_line_num': '',
|
|
1098
|
-
'content': line
|
|
1099
|
-
})
|
|
1100
|
-
old_line_num += 1
|
|
1101
|
-
elif line.startswith('+'):
|
|
1102
|
-
pending_adds.append({
|
|
1103
|
-
'type': 'add',
|
|
1104
|
-
'old_line_num': '',
|
|
1105
|
-
'new_line_num': new_line_num + 1,
|
|
1106
|
-
'content': line
|
|
1107
|
-
})
|
|
1108
|
-
new_line_num += 1
|
|
1109
|
-
elif line.startswith(' '):
|
|
1110
|
-
# Flush pending changes before context line
|
|
1111
|
-
flush_pending()
|
|
1112
|
-
|
|
1113
|
-
old_line_num += 1
|
|
1114
|
-
new_line_num += 1
|
|
1115
|
-
parsed.append({
|
|
1116
|
-
'type': 'context',
|
|
1117
|
-
'old_line_num': old_line_num,
|
|
1118
|
-
'new_line_num': new_line_num,
|
|
1119
|
-
'content': line
|
|
1120
|
-
})
|
|
1121
|
-
elif line.startswith('---') or line.startswith('+++'):
|
|
1122
|
-
parsed.append({
|
|
1123
|
-
'type': 'header',
|
|
1124
|
-
'content': line,
|
|
1125
|
-
'old_line_num': '',
|
|
1126
|
-
'new_line_num': ''
|
|
1127
|
-
})
|
|
1128
|
-
|
|
1129
|
-
# Flush any remaining pending changes
|
|
1130
|
-
flush_pending()
|
|
1131
|
-
|
|
1132
|
-
return parsed
|
|
1133
|
-
|
|
1134
|
-
def _generate_intraline_diff(self, old_text: str, new_text: str) -> Tuple[str, str]:
|
|
1135
|
-
"""Generate intra-line character-level diff highlighting."""
|
|
1136
|
-
# Temporarily disable intraline highlighting to fix performance issues
|
|
1137
|
-
return self._escape_html(old_text), self._escape_html(new_text)
|
|
1138
|
-
|
|
1139
|
-
if not DIFF_MATCH_PATCH_AVAILABLE:
|
|
1140
|
-
return self._escape_html(old_text), self._escape_html(new_text)
|
|
1141
|
-
|
|
1142
|
-
try:
|
|
1143
|
-
dmp = diff_match_patch()
|
|
1144
|
-
diffs = dmp.diff_main(old_text, new_text)
|
|
1145
|
-
dmp.diff_cleanupSemantic(diffs)
|
|
1146
|
-
|
|
1147
|
-
old_parts = []
|
|
1148
|
-
new_parts = []
|
|
1149
|
-
|
|
1150
|
-
for op, text in diffs:
|
|
1151
|
-
escaped_text = self._escape_html(text)
|
|
1152
|
-
|
|
1153
|
-
if op == 0: # EQUAL
|
|
1154
|
-
old_parts.append(escaped_text)
|
|
1155
|
-
new_parts.append(escaped_text)
|
|
1156
|
-
elif op == -1: # DELETE
|
|
1157
|
-
old_parts.append(f'<span class="intraline-delete">{escaped_text}</span>')
|
|
1158
|
-
elif op == 1: # INSERT
|
|
1159
|
-
new_parts.append(f'<span class="intraline-add">{escaped_text}</span>')
|
|
1160
|
-
|
|
1161
|
-
return ''.join(old_parts), ''.join(new_parts)
|
|
1162
|
-
|
|
1163
|
-
except Exception as e:
|
|
1164
|
-
logger.debug("Error generating intra-line diff: %s", e)
|
|
1165
|
-
return self._escape_html(old_text), self._escape_html(new_text)
|
|
1166
|
-
|
|
1167
|
-
def _escape_html(self, text: str) -> str:
|
|
1168
|
-
"""Escape HTML special characters."""
|
|
1169
|
-
return (text.replace('&', '&')
|
|
1170
|
-
.replace('<', '<')
|
|
1171
|
-
.replace('>', '>')
|
|
1172
|
-
.replace('"', '"')
|
|
1173
|
-
.replace("'", '''))
|
|
658
|
+
"""Proxy to the stateless diff renderer so tab views keep their HTML output."""
|
|
659
|
+
return diff_renderer.generate_html_diff(original_content, modified_content, file_path)
|
|
1174
660
|
|
|
1175
661
|
def get_head_commit_hash(self) -> Optional[str]:
|
|
1176
662
|
"""Get the hash of the HEAD commit."""
|
|
@@ -1476,8 +962,8 @@ class GitManager:
|
|
|
1476
962
|
# For submodules, use git add directly to stage only the submodule reference
|
|
1477
963
|
self.repo.git.add(rel_path)
|
|
1478
964
|
else:
|
|
1479
|
-
#
|
|
1480
|
-
self.repo.
|
|
965
|
+
# Use git add -A on the specific file to handle deletions as well
|
|
966
|
+
self.repo.git.add('-A', '--', rel_path)
|
|
1481
967
|
|
|
1482
968
|
logger.info("Successfully staged file: %s", rel_path)
|
|
1483
969
|
return True
|
|
@@ -1539,8 +1025,8 @@ class GitManager:
|
|
|
1539
1025
|
has_head = False
|
|
1540
1026
|
|
|
1541
1027
|
if has_head:
|
|
1542
|
-
# Restore
|
|
1543
|
-
self.repo.git.restore(rel_path)
|
|
1028
|
+
# Restore both index and working tree from HEAD
|
|
1029
|
+
self.repo.git.restore('--staged', '--worktree', '--', rel_path)
|
|
1544
1030
|
logger.info("Successfully reverted file: %s", rel_path)
|
|
1545
1031
|
else:
|
|
1546
1032
|
# For repositories with no commits, we can't revert to HEAD
|
|
@@ -1584,10 +1070,10 @@ class GitManager:
|
|
|
1584
1070
|
for submodule_path in submodule_paths:
|
|
1585
1071
|
self.repo.git.add(submodule_path)
|
|
1586
1072
|
|
|
1587
|
-
# Stage regular files using
|
|
1073
|
+
# Stage regular files using git add -A to capture deletions
|
|
1588
1074
|
if rel_paths:
|
|
1589
1075
|
logger.info("Staging regular files: %s", rel_paths)
|
|
1590
|
-
self.repo.
|
|
1076
|
+
self.repo.git.add('-A', '--', *rel_paths)
|
|
1591
1077
|
|
|
1592
1078
|
logger.info("Successfully staged %d files (%d submodules, %d regular)",
|
|
1593
1079
|
len(file_paths), len(submodule_paths), len(rel_paths))
|
|
@@ -1672,7 +1158,7 @@ class GitManager:
|
|
|
1672
1158
|
regular_files.append(rel_paths[i])
|
|
1673
1159
|
|
|
1674
1160
|
if regular_files:
|
|
1675
|
-
self.repo.git.restore(*regular_files)
|
|
1161
|
+
self.repo.git.restore('--staged', '--worktree', '--', *regular_files)
|
|
1676
1162
|
logger.info("Successfully reverted %d files", len(regular_files))
|
|
1677
1163
|
else:
|
|
1678
1164
|
# For repositories with no commits, remove files to "revert" them
|
|
@@ -1809,7 +1295,7 @@ class GitManager:
|
|
|
1809
1295
|
logger.debug("Git monitoring already running for %s", self.project_path)
|
|
1810
1296
|
return
|
|
1811
1297
|
|
|
1812
|
-
logger.info("Starting periodic git monitoring for %s", self.project_path)
|
|
1298
|
+
logger.info("Starting periodic git monitoring for %s (session=%s)", self.project_path, self.owner_session_id)
|
|
1813
1299
|
self._monitoring_enabled = True
|
|
1814
1300
|
|
|
1815
1301
|
# Initialize cached status
|
|
@@ -1822,10 +1308,23 @@ class GitManager:
|
|
|
1822
1308
|
"""Stop periodic monitoring of git status changes."""
|
|
1823
1309
|
self._monitoring_enabled = False
|
|
1824
1310
|
|
|
1825
|
-
|
|
1311
|
+
task = self._monitoring_task
|
|
1312
|
+
if task and not task.done():
|
|
1826
1313
|
logger.info("Stopping periodic git monitoring for %s", self.project_path)
|
|
1827
|
-
|
|
1828
|
-
|
|
1314
|
+
task.cancel()
|
|
1315
|
+
try:
|
|
1316
|
+
loop = asyncio.get_event_loop()
|
|
1317
|
+
if loop.is_running():
|
|
1318
|
+
loop.create_task(self._await_monitor_stop(task))
|
|
1319
|
+
except Exception:
|
|
1320
|
+
pass
|
|
1321
|
+
self._monitoring_task = None
|
|
1322
|
+
|
|
1323
|
+
async def _await_monitor_stop(self, task):
|
|
1324
|
+
try:
|
|
1325
|
+
await task
|
|
1326
|
+
except asyncio.CancelledError:
|
|
1327
|
+
logger.debug("Git monitoring task cancelled for %s", self.project_path)
|
|
1829
1328
|
|
|
1830
1329
|
def _update_cached_status(self):
|
|
1831
1330
|
"""Update cached git status for comparison."""
|
|
@@ -1844,16 +1343,17 @@ class GitManager:
|
|
|
1844
1343
|
"""Monitor git changes periodically and trigger callback when changes are detected."""
|
|
1845
1344
|
try:
|
|
1846
1345
|
while self._monitoring_enabled:
|
|
1847
|
-
await asyncio.sleep(
|
|
1848
|
-
|
|
1346
|
+
await asyncio.sleep(5.0) # Check every 5000ms
|
|
1347
|
+
|
|
1849
1348
|
if not self._monitoring_enabled or not self.is_git_repo:
|
|
1850
1349
|
break
|
|
1851
|
-
|
|
1350
|
+
|
|
1852
1351
|
try:
|
|
1853
|
-
# Get current status
|
|
1854
|
-
|
|
1855
|
-
|
|
1856
|
-
|
|
1352
|
+
# Get current status - run in executor to avoid blocking event loop
|
|
1353
|
+
loop = asyncio.get_event_loop()
|
|
1354
|
+
current_status_summary = await loop.run_in_executor(None, self.get_status_summary)
|
|
1355
|
+
current_detailed_status = await loop.run_in_executor(None, self.get_detailed_status)
|
|
1356
|
+
current_branch = await loop.run_in_executor(None, self.get_branch_name)
|
|
1857
1357
|
|
|
1858
1358
|
# Compare with cached status
|
|
1859
1359
|
status_changed = (
|
|
@@ -1862,8 +1362,10 @@ class GitManager:
|
|
|
1862
1362
|
self._detailed_status_changed(current_detailed_status, self._cached_detailed_status)
|
|
1863
1363
|
)
|
|
1864
1364
|
|
|
1365
|
+
if not self._monitoring_enabled:
|
|
1366
|
+
continue
|
|
1865
1367
|
if status_changed:
|
|
1866
|
-
logger.info("Git status change detected for %s", self.project_path)
|
|
1368
|
+
logger.info("Git status change detected for %s (session=%s)", self.project_path, self.owner_session_id)
|
|
1867
1369
|
logger.debug("Status summary: %s -> %s", self._cached_status_summary, current_status_summary)
|
|
1868
1370
|
logger.debug("Branch: %s -> %s", self._cached_branch, current_branch)
|
|
1869
1371
|
|
|
@@ -1931,5 +1433,70 @@ class GitManager:
|
|
|
1931
1433
|
|
|
1932
1434
|
def cleanup(self):
|
|
1933
1435
|
"""Cleanup resources when GitManager is being destroyed."""
|
|
1934
|
-
logger.info("Cleaning up GitManager for %s", self.project_path)
|
|
1935
|
-
self.stop_periodic_monitoring()
|
|
1436
|
+
logger.info("Cleaning up GitManager for %s (session=%s)", self.project_path, self.owner_session_id)
|
|
1437
|
+
self.stop_periodic_monitoring()
|
|
1438
|
+
|
|
1439
|
+
# CRITICAL: Close GitPython repo to cleanup git cat-file processes
|
|
1440
|
+
if self.repo:
|
|
1441
|
+
try:
|
|
1442
|
+
# Force cleanup of git command processes
|
|
1443
|
+
if hasattr(self.repo.git, 'clear_cache'):
|
|
1444
|
+
self.repo.git.clear_cache()
|
|
1445
|
+
self.repo.close()
|
|
1446
|
+
logger.info("Successfully closed GitPython repo for %s", self.project_path)
|
|
1447
|
+
except Exception as e:
|
|
1448
|
+
logger.warning("Error during git repo cleanup for %s: %s", self.project_path, e)
|
|
1449
|
+
finally:
|
|
1450
|
+
self.repo = None
|
|
1451
|
+
|
|
1452
|
+
# Clean up only the git processes tracked by this specific GitManager instance
|
|
1453
|
+
try:
|
|
1454
|
+
import psutil
|
|
1455
|
+
|
|
1456
|
+
killed_count = 0
|
|
1457
|
+
for pid in list(self._tracked_git_processes):
|
|
1458
|
+
should_terminate = False
|
|
1459
|
+
with _GIT_PROCESS_LOCK:
|
|
1460
|
+
if pid in _GIT_PROCESS_REF_COUNTS:
|
|
1461
|
+
_GIT_PROCESS_REF_COUNTS[pid] -= 1
|
|
1462
|
+
if _GIT_PROCESS_REF_COUNTS[pid] <= 0:
|
|
1463
|
+
should_terminate = True
|
|
1464
|
+
_GIT_PROCESS_REF_COUNTS.pop(pid, None)
|
|
1465
|
+
else:
|
|
1466
|
+
should_terminate = True
|
|
1467
|
+
|
|
1468
|
+
if should_terminate:
|
|
1469
|
+
try:
|
|
1470
|
+
proc = psutil.Process(pid)
|
|
1471
|
+
if proc.is_running():
|
|
1472
|
+
proc.terminate()
|
|
1473
|
+
killed_count += 1
|
|
1474
|
+
logger.info("Terminated tracked git process %d", pid)
|
|
1475
|
+
except (psutil.NoSuchProcess, psutil.AccessDenied):
|
|
1476
|
+
pass
|
|
1477
|
+
|
|
1478
|
+
self._tracked_git_processes.discard(pid)
|
|
1479
|
+
|
|
1480
|
+
if killed_count > 0:
|
|
1481
|
+
logger.info("Cleaned up %d tracked git processes for session", killed_count)
|
|
1482
|
+
|
|
1483
|
+
except Exception as e:
|
|
1484
|
+
logger.warning("Error cleaning up tracked git processes: %s", e)
|
|
1485
|
+
|
|
1486
|
+
# Clear the tracking set
|
|
1487
|
+
self._tracked_git_processes.clear()
|
|
1488
|
+
|
|
1489
|
+
def get_tracked_git_process_count(self) -> int:
|
|
1490
|
+
"""Return how many git helper processes this manager is tracking."""
|
|
1491
|
+
return len(self._tracked_git_processes)
|
|
1492
|
+
|
|
1493
|
+
def get_diagnostics(self) -> Dict[str, Any]:
|
|
1494
|
+
"""Expose lightweight stats for health monitoring."""
|
|
1495
|
+
return {
|
|
1496
|
+
"project_path": self.project_path,
|
|
1497
|
+
"is_git_repo": self.is_git_repo,
|
|
1498
|
+
"tracked_git_processes": self.get_tracked_git_process_count(),
|
|
1499
|
+
"monitoring_enabled": self._monitoring_enabled,
|
|
1500
|
+
"monitoring_task_active": bool(self._monitoring_task and not self._monitoring_task.done()),
|
|
1501
|
+
"session_id": self.owner_session_id,
|
|
1502
|
+
}
|