diff --git a/api/routes.py b/api/routes.py index 7a6dd6f..115cb34 100644 --- a/api/routes.py +++ b/api/routes.py @@ -214,6 +214,18 @@ def handle_get(handler, parsed): if parsed.path == '/api/list': return _handle_list_dir(handler, parsed) + if parsed.path == '/api/git-info': + qs = parse_qs(parsed.query) + sid = qs.get('session_id', [''])[0] + if not sid: + return bad(handler, 'session_id required') + s = get_session(sid) + if not s: + return bad(handler, 'Session not found', 404) + from api.workspace import git_info_for_workspace + info = git_info_for_workspace(Path(s.workspace)) + return j(handler, {'git': info}) + if parsed.path == '/api/chat/stream/status': stream_id = parse_qs(parsed.query).get('stream_id', [''])[0] return j(handler, {'active': stream_id in STREAMS, 'stream_id': stream_id}) diff --git a/api/workspace.py b/api/workspace.py index 757174b..628f7f3 100644 --- a/api/workspace.py +++ b/api/workspace.py @@ -9,6 +9,7 @@ paths are used as fallback when no profile module is available. """ import json import os +import subprocess from pathlib import Path from api.config import ( @@ -243,3 +244,43 @@ def read_file_content(workspace: Path, rel: str): raise ValueError(f"File too large ({size} bytes, max {MAX_FILE_BYTES})") content = target.read_text(encoding='utf-8', errors='replace') return {'path': rel, 'content': content, 'size': size, 'lines': content.count('\n') + 1} + + +# ── Git detection ────────────────────────────────────────────────────────── + +def _run_git(args, cwd, timeout=3): + """Run a git command and return stdout, or None on failure.""" + try: + r = subprocess.run( + ['git'] + args, cwd=str(cwd), capture_output=True, + text=True, timeout=timeout, + ) + return r.stdout.strip() if r.returncode == 0 else None + except (subprocess.TimeoutExpired, FileNotFoundError, OSError): + return None + + +def git_info_for_workspace(workspace: Path) -> dict: + """Return git info for a workspace directory, or None if not a git repo.""" + if not (workspace / '.git').exists(): + return None + branch = _run_git(['rev-parse', '--abbrev-ref', 'HEAD'], workspace) + if branch is None: + return None + # Status counts + status_out = _run_git(['status', '--porcelain'], workspace) or '' + modified = sum(1 for l in status_out.splitlines() if l and l[0:2].strip() in ('M', 'MM', 'AM')) + untracked = sum(1 for l in status_out.splitlines() if l.startswith('??')) + dirty = len(status_out.splitlines()) if status_out else 0 + # Ahead/behind + ahead = _run_git(['rev-list', '--count', '@{u}..HEAD'], workspace) + behind = _run_git(['rev-list', '--count', 'HEAD..@{u}'], workspace) + return { + 'branch': branch, + 'dirty': dirty, + 'modified': modified, + 'untracked': untracked, + 'ahead': int(ahead) if ahead and ahead.isdigit() else 0, + 'behind': int(behind) if behind and behind.isdigit() else 0, + 'is_git': True, + } diff --git a/static/index.html b/static/index.html index a435eea..c8a3ad5 100644 --- a/static/index.html +++ b/static/index.html @@ -278,6 +278,7 @@