clawview 0.1.0__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.
- clawview/__init__.py +0 -0
- clawview/__main__.py +5 -0
- clawview/app.py +140 -0
- clawview/ide.py +144 -0
- clawview/insights.py +1044 -0
- clawview/models.py +212 -0
- clawview/pricing.py +80 -0
- clawview/sessions.py +1734 -0
- clawview/stats.py +153 -0
- clawview/web/assets/InsightsPanel-XsanyCrx.js +36 -0
- clawview/web/assets/Tableau10-DYOhFoF_.js +1 -0
- clawview/web/assets/ar-SA-G6X2FPQ2-arIHp6y7.js +10 -0
- clawview/web/assets/arc-D0XJP_1w.js +1 -0
- clawview/web/assets/array-CPI_glx8.js +1 -0
- clawview/web/assets/az-AZ-76LH7QW2-B9S9q3QM.js +1 -0
- clawview/web/assets/band-BvgvOus1.js +1 -0
- clawview/web/assets/bg-BG-XCXSNQG7-B0DtsGcY.js +5 -0
- clawview/web/assets/blockDiagram-38ab4fdb-DddPqUt3.js +118 -0
- clawview/web/assets/bn-BD-2XOGV67Q-BOp1iUKN.js +5 -0
- clawview/web/assets/c4Diagram-3d4e48cf-BEtR2lrc.js +10 -0
- clawview/web/assets/ca-ES-6MX7JW3Y-TGrEO4jg.js +8 -0
- clawview/web/assets/channel-BQLZtiIk.js +1 -0
- clawview/web/assets/chunk-6U3AYISY-DTaCc_M3.js +12 -0
- clawview/web/assets/chunk-DseTPa7n.js +1 -0
- clawview/web/assets/chunk-EIO257PC-BgNTvmi4.js +22 -0
- clawview/web/assets/chunk-FX7ZIABN-zAkwZGjJ.js +35 -0
- clawview/web/assets/chunk-SQ5PDB2P-DHb1BAm-.js +7 -0
- clawview/web/assets/chunk-SRAX5OIU-DXkTQhLw.js +1 -0
- clawview/web/assets/chunk-Z3N5DIM6-Crgr0ERq.js +1 -0
- clawview/web/assets/classDiagram-70f12bd4-KCdG01OW.js +2 -0
- clawview/web/assets/classDiagram-v2-f2320105-Rx62xVFT.js +2 -0
- clawview/web/assets/clone-Bwsfh07r.js +1 -0
- clawview/web/assets/createText-2e5e7dd3-Cj375Ntc.js +7 -0
- clawview/web/assets/cs-CZ-2BRQDIVT-DrGxeLdy.js +11 -0
- clawview/web/assets/da-DK-5WZEPLOC-Bpu-r3Iv.js +5 -0
- clawview/web/assets/dagre-CVBS5NFI.js +1 -0
- clawview/web/assets/de-DE-XR44H4JA-GLhY2Ymr.js +8 -0
- clawview/web/assets/directory-open-01563666-DS7WJIfP.js +1 -0
- clawview/web/assets/directory-open-4ed118d0-d8uxSLmg.js +1 -0
- clawview/web/assets/dist-C9I7GKwI.js +1 -0
- clawview/web/assets/dist-CtElfPOo.js +7 -0
- clawview/web/assets/edges-e0da2a9e-CEkO_wx2.js +4 -0
- clawview/web/assets/el-GR-BZB4AONW-DcU9-C4i.js +10 -0
- clawview/web/assets/en-B4ZKOASM-B0JFK1jd.js +1 -0
- clawview/web/assets/erDiagram-9861fffd-BtkTd9nr.js +51 -0
- clawview/web/assets/es-ES-U4NZUMDT-DyufXalF.js +9 -0
- clawview/web/assets/eu-ES-A7QVB2H4-BjYdTz1f.js +11 -0
- clawview/web/assets/fa-IR-HGAKTJCU-B2j-XNlU.js +8 -0
- clawview/web/assets/fi-FI-Z5N7JZ37-DiKQ5AnI.js +6 -0
- clawview/web/assets/file-open-002ab408-Dvn3z30a.js +1 -0
- clawview/web/assets/file-open-7c801643-D8k8uIk5.js +1 -0
- clawview/web/assets/file-save-3189631c-DFENdDWg.js +1 -0
- clawview/web/assets/file-save-745eba88-CvN2mPEj.js +1 -0
- clawview/web/assets/flowDb-956e92f1-BRXT6BKy.js +10 -0
- clawview/web/assets/flowDiagram-66a62f08-Bl7lPH0M.js +4 -0
- clawview/web/assets/flowDiagram-v2-96b9c2cf-BxbAebjp.js +1 -0
- clawview/web/assets/flowchart-elk-definition-4a651766-Cib-VcrC.js +139 -0
- clawview/web/assets/fr-FR-RHASNOE6-C0gNrvQh.js +9 -0
- clawview/web/assets/ganttDiagram-c361ad54-CaAjUZ4Z.js +257 -0
- clawview/web/assets/gitGraphDiagram-72cf32ee-DgEpv1yR.js +70 -0
- clawview/web/assets/gl-ES-HMX3MZ6V-CVl7e3Yp.js +10 -0
- clawview/web/assets/graphlib-ByALzjAA.js +1 -0
- clawview/web/assets/he-IL-6SHJWFNN-BCBbdGZY.js +10 -0
- clawview/web/assets/hi-IN-IWLTKZ5I-CogRThUX.js +4 -0
- clawview/web/assets/hu-HU-A5ZG7DT2-B5ssBdl9.js +7 -0
- clawview/web/assets/id-ID-SAP4L64H-GwVSh0YI.js +10 -0
- clawview/web/assets/image-7KUKJ7J4-0bm1m09Z.js +1 -0
- clawview/web/assets/image-blob-reduce.esm-Bwolu1Yk.js +2 -0
- clawview/web/assets/index-3862675e-IUUc-T7r.js +1 -0
- clawview/web/assets/index-9IQHIY9t.css +2 -0
- clawview/web/assets/index-DMSYUwjY.js +61 -0
- clawview/web/assets/infoDiagram-f8f76790-QIlL2NYx.js +7 -0
- clawview/web/assets/init-BXY6xdg3.js +1 -0
- clawview/web/assets/it-IT-JPQ66NNP-DBEDRqCS.js +11 -0
- clawview/web/assets/ja-JP-DBVTYXUO-kE1CvFOC.js +8 -0
- clawview/web/assets/journeyDiagram-49397b02-CL29eFer.js +139 -0
- clawview/web/assets/jsx-runtime-B-G0DNmd.js +1 -0
- clawview/web/assets/kaa-6HZHGXH3-Dxf6RSvj.js +1 -0
- clawview/web/assets/kab-KAB-ZGHBKWFO-KdcpqtTg.js +8 -0
- clawview/web/assets/katex-DxfKw5bk.js +265 -0
- clawview/web/assets/kk-KZ-P5N5QNE5-D_UQhVok.js +1 -0
- clawview/web/assets/km-KH-HSX4SM5Z-D0SFzLhj.js +11 -0
- clawview/web/assets/ko-KR-MTYHY66A-CscmgsuS.js +9 -0
- clawview/web/assets/ku-TR-6OUDTVRD-DzYXt_-h.js +9 -0
- clawview/web/assets/line-CJPnuAmr.js +1 -0
- clawview/web/assets/linear-DzNjjHnD.js +1 -0
- clawview/web/assets/lt-LT-XHIRWOB4-BoqLc8my.js +3 -0
- clawview/web/assets/lv-LV-5QDEKY6T-BXMihRmq.js +7 -0
- clawview/web/assets/mermaid-b5860b54-DFGNN6kZ.js +89 -0
- clawview/web/assets/mindmap-definition-fc14e90a-DmSmJ5gU.js +415 -0
- clawview/web/assets/mr-IN-CRQNXWMA-Cht2SRn9.js +13 -0
- clawview/web/assets/my-MM-5M5IBNSE-BIf_PG2o.js +1 -0
- clawview/web/assets/nb-NO-T6EIAALU-D5FydAW6.js +10 -0
- clawview/web/assets/nl-NL-IS3SIHDZ-sbWnbazh.js +8 -0
- clawview/web/assets/nn-NO-6E72VCQL-DaA5Ng4z.js +8 -0
- clawview/web/assets/oc-FR-POXYY2M6-GIJFFa0g.js +8 -0
- clawview/web/assets/ordinal-DG-mcJZ_.js +1 -0
- clawview/web/assets/pa-IN-N4M65BXN-cdarwAvH.js +4 -0
- clawview/web/assets/path-CJP_60cg.js +1 -0
- clawview/web/assets/percentages-BXMCSKIN-Ddxggvgb.js +1 -0
- clawview/web/assets/pica-D00_uJyn.js +2 -0
- clawview/web/assets/pieDiagram-8a3498a8-C_Xy1L1P.js +35 -0
- clawview/web/assets/pl-PL-T2D74RX3-CBlAjM-a.js +9 -0
- clawview/web/assets/preload-helper-rov5CBGT.js +1 -0
- clawview/web/assets/prod-DsC-08Ys.js +149 -0
- clawview/web/assets/pt-BR-5N22H2LF-BVcpQZ6q.js +9 -0
- clawview/web/assets/pt-PT-UZXXM6DQ-B5v1EhyM.js +9 -0
- clawview/web/assets/quadrantDiagram-120e2f19-DNlYwwOD.js +7 -0
- clawview/web/assets/react-dom-yFv2xNpf.js +1 -0
- clawview/web/assets/requirementDiagram-deff3bca-DhuVcdo5.js +52 -0
- clawview/web/assets/ro-RO-JPDTUUEW-BEiz1UdL.js +11 -0
- clawview/web/assets/roundRect-BXf7JfZJ.js +1 -0
- clawview/web/assets/ru-RU-B4JR7IUQ-B41o3rg1.js +9 -0
- clawview/web/assets/sankeyDiagram-04a897e0-DwtfTQoP.js +8 -0
- clawview/web/assets/sequenceDiagram-704730f1-C-LO9nc3.js +122 -0
- clawview/web/assets/si-LK-N5RQ5JYF-wk9oo4_g.js +1 -0
- clawview/web/assets/sk-SK-C5VTKIMK-BKokcJeH.js +6 -0
- clawview/web/assets/sl-SI-NN7IZMDC-DRC4tvvu.js +6 -0
- clawview/web/assets/stateDiagram-587899a1-dEgNeBFL.js +1 -0
- clawview/web/assets/stateDiagram-v2-d93cdb3a-OiMRr12b.js +1 -0
- clawview/web/assets/step-CltBpEWI.js +1 -0
- clawview/web/assets/styles-6aaf32cf-yExJhkpA.js +207 -0
- clawview/web/assets/styles-9a916d00-0qGooOxP.js +160 -0
- clawview/web/assets/styles-c10674c1-BukgWV1G.js +116 -0
- clawview/web/assets/subset-shared.chunk-DnUq6rce.js +1 -0
- clawview/web/assets/subset-worker.chunk-Bx98g3iN.js +1 -0
- clawview/web/assets/sv-SE-XGPEYMSR-CMXGGiL0.js +10 -0
- clawview/web/assets/svgDrawCommon-08f97a94-ptfOfFLH.js +1 -0
- clawview/web/assets/ta-IN-2NMHFXQM-DQx8qTR7.js +9 -0
- clawview/web/assets/th-TH-HPSO5L25-DaUzuT7b.js +2 -0
- clawview/web/assets/time-DgeWXGcM.js +1 -0
- clawview/web/assets/timeline-definition-85554ec2-9SZ5X3yK.js +61 -0
- clawview/web/assets/tr-TR-DEFEU3FU-BYu0yuC4.js +7 -0
- clawview/web/assets/uk-UA-QMV73CPH--Hhr12a0.js +6 -0
- clawview/web/assets/vi-VN-M7AON7JQ-k5MWllYs.js +5 -0
- clawview/web/assets/with-selector-CF8hm-Bx.js +1 -0
- clawview/web/assets/xychartDiagram-e933f94c-uwNWTS5J.js +7 -0
- clawview/web/assets/zh-CN-LNUGB5OW-mt-jcU0T.js +10 -0
- clawview/web/assets/zh-HK-E62DVLB3-CqQFdXPt.js +1 -0
- clawview/web/assets/zh-TW-RAJ6MFWO-CGYv94Kb.js +9 -0
- clawview/web/favicon.svg +1 -0
- clawview/web/icons.svg +24 -0
- clawview/web/index.html +18 -0
- clawview/ws.py +208 -0
- clawview-0.1.0.dist-info/METADATA +123 -0
- clawview-0.1.0.dist-info/RECORD +149 -0
- clawview-0.1.0.dist-info/WHEEL +4 -0
- clawview-0.1.0.dist-info/entry_points.txt +2 -0
- clawview-0.1.0.dist-info/licenses/LICENSE +21 -0
clawview/__init__.py
ADDED
|
File without changes
|
clawview/__main__.py
ADDED
clawview/app.py
ADDED
|
@@ -0,0 +1,140 @@
|
|
|
1
|
+
"""ClawView – real-time Claude Code session dashboard."""
|
|
2
|
+
|
|
3
|
+
import argparse
|
|
4
|
+
import asyncio
|
|
5
|
+
import os
|
|
6
|
+
from pathlib import Path
|
|
7
|
+
|
|
8
|
+
import uvicorn
|
|
9
|
+
from fastapi import FastAPI, Request, WebSocket
|
|
10
|
+
from fastapi.responses import FileResponse, JSONResponse
|
|
11
|
+
from fastapi.staticfiles import StaticFiles
|
|
12
|
+
|
|
13
|
+
from clawview.sessions import (
|
|
14
|
+
enrich_session_detail,
|
|
15
|
+
find_session_file,
|
|
16
|
+
load_memory_files,
|
|
17
|
+
load_skill_content,
|
|
18
|
+
parse_session_detail,
|
|
19
|
+
)
|
|
20
|
+
from clawview.ws import (
|
|
21
|
+
session_detail_websocket,
|
|
22
|
+
session_insights_websocket,
|
|
23
|
+
session_memory_websocket,
|
|
24
|
+
websocket_endpoint,
|
|
25
|
+
)
|
|
26
|
+
|
|
27
|
+
# Static files bundled inside the package (src/clawview/web/)
|
|
28
|
+
_DIST_DIR = Path(__file__).resolve().parent / "web"
|
|
29
|
+
|
|
30
|
+
app = FastAPI(title="ClawView")
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
def _setup_static_files() -> None:
|
|
34
|
+
"""Mount static file serving if the dist directory exists."""
|
|
35
|
+
if not _DIST_DIR.is_dir():
|
|
36
|
+
return
|
|
37
|
+
|
|
38
|
+
# Serve /assets/ directly for hashed JS/CSS bundles
|
|
39
|
+
assets_dir = _DIST_DIR / "assets"
|
|
40
|
+
if assets_dir.is_dir():
|
|
41
|
+
app.mount("/assets", StaticFiles(directory=str(assets_dir)), name="assets")
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
_setup_static_files()
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
@app.websocket("/ws")
|
|
48
|
+
async def ws_route(ws: WebSocket) -> None:
|
|
49
|
+
await websocket_endpoint(ws)
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
@app.websocket("/ws/sessions/{session_id}")
|
|
53
|
+
async def ws_session_detail_route(ws: WebSocket, session_id: str) -> None:
|
|
54
|
+
await session_detail_websocket(ws, session_id)
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
@app.websocket("/ws/sessions/{session_id}/memory")
|
|
58
|
+
async def ws_session_memory_route(ws: WebSocket, session_id: str) -> None:
|
|
59
|
+
await session_memory_websocket(ws, session_id)
|
|
60
|
+
|
|
61
|
+
|
|
62
|
+
@app.websocket("/ws/insights/{session_id}")
|
|
63
|
+
async def ws_insights_route(ws: WebSocket, session_id: str) -> None:
|
|
64
|
+
await session_insights_websocket(ws, session_id)
|
|
65
|
+
|
|
66
|
+
|
|
67
|
+
@app.get("/api/sessions/{session_id}")
|
|
68
|
+
async def get_session_detail(session_id: str) -> JSONResponse:
|
|
69
|
+
"""Return full session detail for a given session ID."""
|
|
70
|
+
fpath = await asyncio.to_thread(find_session_file, session_id)
|
|
71
|
+
if fpath is None:
|
|
72
|
+
return JSONResponse(
|
|
73
|
+
status_code=404,
|
|
74
|
+
content={"error": f"Session {session_id} not found"},
|
|
75
|
+
)
|
|
76
|
+
|
|
77
|
+
detail = await asyncio.to_thread(parse_session_detail, fpath)
|
|
78
|
+
if detail is None:
|
|
79
|
+
return JSONResponse(
|
|
80
|
+
status_code=500,
|
|
81
|
+
content={"error": f"Failed to parse session {session_id}"},
|
|
82
|
+
)
|
|
83
|
+
|
|
84
|
+
await asyncio.to_thread(enrich_session_detail, detail, fpath)
|
|
85
|
+
|
|
86
|
+
return JSONResponse(
|
|
87
|
+
content=detail.model_dump(by_alias=True, mode="json"),
|
|
88
|
+
)
|
|
89
|
+
|
|
90
|
+
|
|
91
|
+
@app.get("/api/sessions/{session_id}/memory")
|
|
92
|
+
async def get_session_memory(session_id: str) -> JSONResponse:
|
|
93
|
+
"""Return memory files for the project that owns the given session."""
|
|
94
|
+
files = await asyncio.to_thread(load_memory_files, session_id)
|
|
95
|
+
return JSONResponse(
|
|
96
|
+
content=[f.model_dump(by_alias=True) for f in files],
|
|
97
|
+
)
|
|
98
|
+
|
|
99
|
+
|
|
100
|
+
@app.get("/api/sessions/{session_id}/skills/{skill_name:path}")
|
|
101
|
+
async def get_skill_content(session_id: str, skill_name: str) -> JSONResponse:
|
|
102
|
+
"""Return the content of a skill file by name."""
|
|
103
|
+
result = await asyncio.to_thread(load_skill_content, session_id, skill_name)
|
|
104
|
+
if result is None:
|
|
105
|
+
return JSONResponse(
|
|
106
|
+
status_code=404,
|
|
107
|
+
content={"error": f"Skill '{skill_name}' not found"},
|
|
108
|
+
)
|
|
109
|
+
return JSONResponse(content={
|
|
110
|
+
"name": skill_name,
|
|
111
|
+
"content": result["content"],
|
|
112
|
+
"source": result["source"],
|
|
113
|
+
"path": result["path"],
|
|
114
|
+
})
|
|
115
|
+
|
|
116
|
+
|
|
117
|
+
@app.get("/{full_path:path}")
|
|
118
|
+
async def spa_fallback(request: Request, full_path: str) -> FileResponse:
|
|
119
|
+
"""Serve static files or fall back to index.html for SPA routing."""
|
|
120
|
+
# Try to serve the exact file first (favicon.svg, icons.svg, etc.)
|
|
121
|
+
file_path = _DIST_DIR / full_path
|
|
122
|
+
if full_path and file_path.is_file():
|
|
123
|
+
return FileResponse(str(file_path))
|
|
124
|
+
|
|
125
|
+
# Fall back to index.html for all other paths (SPA routing)
|
|
126
|
+
return FileResponse(
|
|
127
|
+
str(_DIST_DIR / "index.html"),
|
|
128
|
+
headers={
|
|
129
|
+
"Cache-Control": "no-cache, no-store, must-revalidate",
|
|
130
|
+
"Pragma": "no-cache",
|
|
131
|
+
"Expires": "0",
|
|
132
|
+
},
|
|
133
|
+
)
|
|
134
|
+
|
|
135
|
+
|
|
136
|
+
def main() -> None:
|
|
137
|
+
parser = argparse.ArgumentParser(description="ClawView dashboard server")
|
|
138
|
+
parser.add_argument("--port", type=int, default=3333)
|
|
139
|
+
args = parser.parse_args()
|
|
140
|
+
uvicorn.run(app, host="0.0.0.0", port=args.port)
|
clawview/ide.py
ADDED
|
@@ -0,0 +1,144 @@
|
|
|
1
|
+
"""IDE / terminal detection — reads ~/.claude/ide/*.lock and inspects process ancestry."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import json
|
|
6
|
+
import logging
|
|
7
|
+
import os
|
|
8
|
+
import subprocess
|
|
9
|
+
from pathlib import Path
|
|
10
|
+
|
|
11
|
+
logger = logging.getLogger(__name__)
|
|
12
|
+
|
|
13
|
+
# Known terminal emulators: lowercased substring -> display name.
|
|
14
|
+
_KNOWN_TERMINALS: dict[str, str] = {
|
|
15
|
+
"ghostty": "Ghostty",
|
|
16
|
+
"iterm2": "iTerm2",
|
|
17
|
+
"iterm": "iTerm2",
|
|
18
|
+
"alacritty": "Alacritty",
|
|
19
|
+
"kitty": "Kitty",
|
|
20
|
+
"wezterm": "WezTerm",
|
|
21
|
+
"hyper": "Hyper",
|
|
22
|
+
"warp": "Warp",
|
|
23
|
+
"terminal.app": "Terminal",
|
|
24
|
+
"apple_terminal": "Terminal",
|
|
25
|
+
"windowsterminal": "Windows Terminal",
|
|
26
|
+
"terminal": "Terminal",
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
def _is_pid_alive(pid: int) -> bool:
|
|
31
|
+
"""Check whether a process with the given PID is still running."""
|
|
32
|
+
try:
|
|
33
|
+
os.kill(pid, 0)
|
|
34
|
+
return True
|
|
35
|
+
except (OSError, ProcessLookupError):
|
|
36
|
+
return False
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
def _build_process_table() -> tuple[dict[int, int], dict[int, str]]:
|
|
40
|
+
"""Return (pid_to_ppid, pid_to_comm) maps from ``ps``."""
|
|
41
|
+
pid_to_ppid: dict[int, int] = {}
|
|
42
|
+
pid_to_comm: dict[int, str] = {}
|
|
43
|
+
try:
|
|
44
|
+
result = subprocess.run(
|
|
45
|
+
["ps", "-o", "pid=,ppid=,comm=", "-ax"],
|
|
46
|
+
capture_output=True, text=True, timeout=5,
|
|
47
|
+
)
|
|
48
|
+
except (OSError, subprocess.TimeoutExpired):
|
|
49
|
+
return pid_to_ppid, pid_to_comm
|
|
50
|
+
|
|
51
|
+
for line in result.stdout.splitlines():
|
|
52
|
+
parts = line.split(None, 2)
|
|
53
|
+
if len(parts) >= 2:
|
|
54
|
+
try:
|
|
55
|
+
pid = int(parts[0])
|
|
56
|
+
ppid = int(parts[1])
|
|
57
|
+
except ValueError:
|
|
58
|
+
continue
|
|
59
|
+
pid_to_ppid[pid] = ppid
|
|
60
|
+
if len(parts) == 3:
|
|
61
|
+
pid_to_comm[pid] = parts[2]
|
|
62
|
+
|
|
63
|
+
return pid_to_ppid, pid_to_comm
|
|
64
|
+
|
|
65
|
+
|
|
66
|
+
def _get_ancestor_pids(pid: int, pid_to_ppid: dict[int, int]) -> list[int]:
|
|
67
|
+
"""Return ancestor PIDs for *pid* (excluding pid 0/1), ordered from nearest to farthest."""
|
|
68
|
+
ancestors: list[int] = []
|
|
69
|
+
seen: set[int] = set()
|
|
70
|
+
current = pid
|
|
71
|
+
while current in pid_to_ppid:
|
|
72
|
+
parent = pid_to_ppid[current]
|
|
73
|
+
if parent <= 1 or parent in seen:
|
|
74
|
+
break
|
|
75
|
+
ancestors.append(parent)
|
|
76
|
+
seen.add(parent)
|
|
77
|
+
current = parent
|
|
78
|
+
return ancestors
|
|
79
|
+
|
|
80
|
+
|
|
81
|
+
def load_ide_pid_map(ide_dir: str) -> dict[int, str]:
|
|
82
|
+
"""Read all .lock files in *ide_dir* and return ``{ide_pid: ide_name}``.
|
|
83
|
+
|
|
84
|
+
Lock files whose PID is no longer running are skipped (stale).
|
|
85
|
+
"""
|
|
86
|
+
result: dict[int, str] = {}
|
|
87
|
+
|
|
88
|
+
ide_path = Path(ide_dir)
|
|
89
|
+
if not ide_path.is_dir():
|
|
90
|
+
return result
|
|
91
|
+
|
|
92
|
+
for entry in ide_path.iterdir():
|
|
93
|
+
if entry.is_dir() or entry.suffix != ".lock":
|
|
94
|
+
continue
|
|
95
|
+
|
|
96
|
+
try:
|
|
97
|
+
data = json.loads(entry.read_text())
|
|
98
|
+
except (OSError, json.JSONDecodeError) as exc:
|
|
99
|
+
logger.warning("Skipping malformed lock file %s: %s", entry, exc)
|
|
100
|
+
continue
|
|
101
|
+
|
|
102
|
+
ide_name: str = data.get("ideName", "")
|
|
103
|
+
if not ide_name:
|
|
104
|
+
continue
|
|
105
|
+
|
|
106
|
+
pid = data.get("pid")
|
|
107
|
+
if not isinstance(pid, int):
|
|
108
|
+
continue
|
|
109
|
+
|
|
110
|
+
if not _is_pid_alive(pid):
|
|
111
|
+
logger.debug("Skipping stale lock file %s (pid %d not running)", entry.name, pid)
|
|
112
|
+
continue
|
|
113
|
+
|
|
114
|
+
result[pid] = ide_name
|
|
115
|
+
|
|
116
|
+
return result
|
|
117
|
+
|
|
118
|
+
|
|
119
|
+
def resolve_client_for_pid(session_pid: int, ide_pid_map: dict[int, str]) -> str:
|
|
120
|
+
"""Return the client name for *session_pid*.
|
|
121
|
+
|
|
122
|
+
First checks if the session is a descendant of a known IDE PID.
|
|
123
|
+
If not, walks the ancestor process names looking for a known terminal emulator.
|
|
124
|
+
Returns ``""`` if nothing matches.
|
|
125
|
+
"""
|
|
126
|
+
pid_to_ppid, pid_to_comm = _build_process_table()
|
|
127
|
+
ancestors = _get_ancestor_pids(session_pid, pid_to_ppid)
|
|
128
|
+
|
|
129
|
+
# Check IDE ancestry first.
|
|
130
|
+
for ancestor_pid in ancestors:
|
|
131
|
+
if ancestor_pid in ide_pid_map:
|
|
132
|
+
return ide_pid_map[ancestor_pid]
|
|
133
|
+
|
|
134
|
+
# Fallback: detect terminal emulator from ancestor command names.
|
|
135
|
+
for ancestor_pid in ancestors:
|
|
136
|
+
comm = pid_to_comm.get(ancestor_pid, "")
|
|
137
|
+
if not comm:
|
|
138
|
+
continue
|
|
139
|
+
comm_lower = comm.lower()
|
|
140
|
+
for key, name in _KNOWN_TERMINALS.items():
|
|
141
|
+
if key in comm_lower:
|
|
142
|
+
return name
|
|
143
|
+
|
|
144
|
+
return ""
|