@grifhinz/logics-manager 2.8.0 → 2.8.1
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/README.md +22 -7
- package/VERSION +1 -1
- package/clients/viewer/browser-host.js +421 -33
- package/clients/viewer/index.html +1 -0
- package/clients/viewer/viewer.css +251 -0
- package/logics_manager/cli.py +17 -17
- package/logics_manager/viewer.py +251 -22
- package/package.json +2 -1
- package/pyproject.toml +1 -1
package/logics_manager/viewer.py
CHANGED
|
@@ -49,6 +49,8 @@ CDX_MISSION_STRENGTHS = {
|
|
|
49
49
|
"deep": {"id": "deep", "label": "Deep", "timeout": 300, "reasoningEffort": "high", "power": "high"},
|
|
50
50
|
"max": {"id": "max", "label": "Max", "timeout": 600, "reasoningEffort": "high", "power": "high"},
|
|
51
51
|
}
|
|
52
|
+
CDX_MISSION_PARENT_TIMEOUT_GRACE_SECONDS = 90
|
|
53
|
+
CDX_WRITABLE_MISSION_MIN_TIMEOUT_SECONDS = 600
|
|
52
54
|
CDX_MISSION_CATALOG = {
|
|
53
55
|
"full-audit": {
|
|
54
56
|
"id": "full-audit",
|
|
@@ -142,6 +144,26 @@ GIT_FILE_PREVIEW_MAX_BYTES = 30000
|
|
|
142
144
|
GIT_FILE_PREVIEW_MAX_CHARS = 20000
|
|
143
145
|
FILE_PREVIEW_MAX_BYTES = 300000
|
|
144
146
|
FILE_PREVIEW_MAX_CHARS = 200000
|
|
147
|
+
WORKSPACE_TREE_MAX_ENTRIES = 250
|
|
148
|
+
WORKSPACE_PREVIEW_MAX_BYTES = 30000
|
|
149
|
+
WORKSPACE_PREVIEW_MAX_CHARS = 20000
|
|
150
|
+
WORKSPACE_IGNORED_DIRS = {
|
|
151
|
+
".git",
|
|
152
|
+
".hg",
|
|
153
|
+
".svn",
|
|
154
|
+
"__pycache__",
|
|
155
|
+
".pytest_cache",
|
|
156
|
+
".mypy_cache",
|
|
157
|
+
".ruff_cache",
|
|
158
|
+
"node_modules",
|
|
159
|
+
"dist",
|
|
160
|
+
"build",
|
|
161
|
+
"coverage",
|
|
162
|
+
".next",
|
|
163
|
+
".turbo",
|
|
164
|
+
".venv",
|
|
165
|
+
"venv",
|
|
166
|
+
}
|
|
145
167
|
REPO_ROOT = Path(__file__).resolve().parents[1]
|
|
146
168
|
PACKAGE_VIEWER_ASSETS_ROOT = Path(__file__).resolve().parent / "viewer_assets"
|
|
147
169
|
VIEWER_ROOT = REPO_ROOT / "clients" / "viewer"
|
|
@@ -442,6 +464,7 @@ def viewer_data_payload(
|
|
|
442
464
|
selected_id: str | None = None,
|
|
443
465
|
*,
|
|
444
466
|
auto_refresh_interval_seconds: int = 15,
|
|
467
|
+
auto_refresh_interval_forced: bool = False,
|
|
445
468
|
projects: list[dict[str, Any]] | None = None,
|
|
446
469
|
) -> dict[str, Any]:
|
|
447
470
|
capabilities = viewer_project_capabilities(repo_root)
|
|
@@ -457,6 +480,7 @@ def viewer_data_payload(
|
|
|
457
480
|
"capabilities": capabilities,
|
|
458
481
|
"projects": projects if projects is not None else viewer_project_registry(repo_root),
|
|
459
482
|
"autoRefreshIntervalSeconds": auto_refresh_interval_seconds,
|
|
483
|
+
"autoRefreshIntervalForced": auto_refresh_interval_forced,
|
|
460
484
|
"items": collect_viewer_items(repo_root),
|
|
461
485
|
"updateInfo": get_update_info(_current_version()).to_payload(),
|
|
462
486
|
"selectedId": selected_id,
|
|
@@ -611,9 +635,16 @@ def viewer_project_capabilities(
|
|
|
611
635
|
else:
|
|
612
636
|
cdx = _viewer_capability("missing", available=False, message="CDX executable is not available.")
|
|
613
637
|
cdx_runs = _viewer_capability("missing", available=False, message="CDX is required before assistant runs can be tracked.")
|
|
638
|
+
workspace = _viewer_capability(
|
|
639
|
+
"ready" if repo_root.is_dir() else "missing",
|
|
640
|
+
available=repo_root.is_dir(),
|
|
641
|
+
message="Workspace root can be inspected." if repo_root.is_dir() else "Workspace root is unavailable.",
|
|
642
|
+
detail={"root": str(repo_root.resolve())} if repo_root.is_dir() else {},
|
|
643
|
+
)
|
|
614
644
|
|
|
615
645
|
return {
|
|
616
646
|
"logics": logics,
|
|
647
|
+
"workspace": workspace,
|
|
617
648
|
"git": git,
|
|
618
649
|
"ci": ci,
|
|
619
650
|
"cdx": cdx,
|
|
@@ -1156,6 +1187,168 @@ def git_file_preview_payload(
|
|
|
1156
1187
|
}
|
|
1157
1188
|
|
|
1158
1189
|
|
|
1190
|
+
def _normalize_workspace_path(rel_path: str) -> str:
|
|
1191
|
+
normalized = unquote(rel_path or "").replace("\\", "/").strip()
|
|
1192
|
+
normalized = normalized.lstrip("/")
|
|
1193
|
+
if normalized in {"", "."}:
|
|
1194
|
+
return ""
|
|
1195
|
+
if normalized.startswith("~") or re.match(r"^[A-Za-z]:", normalized):
|
|
1196
|
+
raise ValueError("Unsafe workspace path.")
|
|
1197
|
+
parts = [part for part in normalized.split("/") if part not in {"", "."}]
|
|
1198
|
+
if any(part == ".." for part in parts):
|
|
1199
|
+
raise ValueError("Workspace path escapes root.")
|
|
1200
|
+
return "/".join(parts)
|
|
1201
|
+
|
|
1202
|
+
|
|
1203
|
+
def _resolve_workspace_path(repo_root: Path, rel_path: str) -> tuple[str, Path]:
|
|
1204
|
+
normalized = _normalize_workspace_path(rel_path)
|
|
1205
|
+
root = repo_root.resolve()
|
|
1206
|
+
target = (root / normalized).resolve()
|
|
1207
|
+
try:
|
|
1208
|
+
target.relative_to(root)
|
|
1209
|
+
except ValueError as exc:
|
|
1210
|
+
raise ValueError("Workspace path escapes root.") from exc
|
|
1211
|
+
return normalized, target
|
|
1212
|
+
|
|
1213
|
+
|
|
1214
|
+
def _workspace_entry_payload(root: Path, path: Path) -> dict[str, Any]:
|
|
1215
|
+
try:
|
|
1216
|
+
stat = path.stat()
|
|
1217
|
+
except OSError:
|
|
1218
|
+
stat = None
|
|
1219
|
+
rel_path = path.relative_to(root).as_posix()
|
|
1220
|
+
is_dir = path.is_dir()
|
|
1221
|
+
ignored = is_dir and path.name in WORKSPACE_IGNORED_DIRS
|
|
1222
|
+
return {
|
|
1223
|
+
"name": path.name or root.name,
|
|
1224
|
+
"path": rel_path,
|
|
1225
|
+
"kind": "directory" if is_dir else "file",
|
|
1226
|
+
"size": stat.st_size if stat else 0,
|
|
1227
|
+
"ignored": ignored,
|
|
1228
|
+
"childrenAvailable": is_dir and not ignored,
|
|
1229
|
+
}
|
|
1230
|
+
|
|
1231
|
+
|
|
1232
|
+
def workspace_tree_payload(
|
|
1233
|
+
repo_root: Path,
|
|
1234
|
+
rel_path: str = "",
|
|
1235
|
+
*,
|
|
1236
|
+
max_entries: int = WORKSPACE_TREE_MAX_ENTRIES,
|
|
1237
|
+
) -> dict[str, Any]:
|
|
1238
|
+
normalized, target = _resolve_workspace_path(repo_root, rel_path)
|
|
1239
|
+
root = repo_root.resolve()
|
|
1240
|
+
if not target.exists():
|
|
1241
|
+
return {"state": "missing", "path": normalized, "message": "Workspace path does not exist."}
|
|
1242
|
+
if not target.is_dir():
|
|
1243
|
+
return {"state": "not-directory", "path": normalized, "message": "Workspace path is not a directory."}
|
|
1244
|
+
entries = []
|
|
1245
|
+
truncated = False
|
|
1246
|
+
try:
|
|
1247
|
+
children = sorted(target.iterdir(), key=lambda path: (not path.is_dir(), path.name.lower()))
|
|
1248
|
+
except OSError as exc:
|
|
1249
|
+
return {"state": "error", "path": normalized, "message": f"Unable to list workspace path: {exc}"}
|
|
1250
|
+
for child in children:
|
|
1251
|
+
if len(entries) >= max_entries:
|
|
1252
|
+
truncated = True
|
|
1253
|
+
break
|
|
1254
|
+
entries.append(_workspace_entry_payload(root, child))
|
|
1255
|
+
return {
|
|
1256
|
+
"state": "ok",
|
|
1257
|
+
"root": str(root),
|
|
1258
|
+
"path": normalized,
|
|
1259
|
+
"entries": entries,
|
|
1260
|
+
"truncated": truncated,
|
|
1261
|
+
"ignoredDirectories": sorted(WORKSPACE_IGNORED_DIRS),
|
|
1262
|
+
}
|
|
1263
|
+
|
|
1264
|
+
|
|
1265
|
+
def workspace_preview_payload(
|
|
1266
|
+
repo_root: Path,
|
|
1267
|
+
rel_path: str,
|
|
1268
|
+
*,
|
|
1269
|
+
max_bytes: int = WORKSPACE_PREVIEW_MAX_BYTES,
|
|
1270
|
+
max_chars: int = WORKSPACE_PREVIEW_MAX_CHARS,
|
|
1271
|
+
) -> dict[str, Any]:
|
|
1272
|
+
normalized, target = _resolve_workspace_path(repo_root, rel_path)
|
|
1273
|
+
if not target.exists():
|
|
1274
|
+
return {"state": "missing", "path": normalized, "message": "Workspace path does not exist."}
|
|
1275
|
+
if target.is_dir():
|
|
1276
|
+
try:
|
|
1277
|
+
count = sum(1 for _ in target.iterdir())
|
|
1278
|
+
except OSError:
|
|
1279
|
+
count = 0
|
|
1280
|
+
return {
|
|
1281
|
+
"state": "directory",
|
|
1282
|
+
"path": normalized,
|
|
1283
|
+
"name": target.name or repo_root.resolve().name,
|
|
1284
|
+
"kind": "directory",
|
|
1285
|
+
"message": f"{count} item(s)",
|
|
1286
|
+
"childrenAvailable": target.name not in WORKSPACE_IGNORED_DIRS,
|
|
1287
|
+
}
|
|
1288
|
+
if not target.is_file():
|
|
1289
|
+
return {"state": "unsupported", "path": normalized, "message": "Workspace object cannot be previewed."}
|
|
1290
|
+
try:
|
|
1291
|
+
size = target.stat().st_size
|
|
1292
|
+
except OSError as exc:
|
|
1293
|
+
return {"state": "error", "path": normalized, "message": f"Unable to inspect file: {exc}"}
|
|
1294
|
+
if size > max_bytes:
|
|
1295
|
+
return {
|
|
1296
|
+
"state": "oversized",
|
|
1297
|
+
"path": normalized,
|
|
1298
|
+
"name": target.name,
|
|
1299
|
+
"size": size,
|
|
1300
|
+
"message": f"File preview is limited to {max_bytes} bytes; this file is {size} bytes.",
|
|
1301
|
+
}
|
|
1302
|
+
try:
|
|
1303
|
+
data = target.read_bytes()
|
|
1304
|
+
except OSError as exc:
|
|
1305
|
+
return {"state": "error", "path": normalized, "message": f"Unable to read file preview: {exc}"}
|
|
1306
|
+
content_type = mimetypes.guess_type(target.name)[0] or ""
|
|
1307
|
+
if content_type.startswith("image/"):
|
|
1308
|
+
return {
|
|
1309
|
+
"state": "image",
|
|
1310
|
+
"path": normalized,
|
|
1311
|
+
"name": target.name,
|
|
1312
|
+
"size": size,
|
|
1313
|
+
"contentType": content_type,
|
|
1314
|
+
"message": "Image preview is available from the workspace file endpoint.",
|
|
1315
|
+
}
|
|
1316
|
+
if b"\x00" in data:
|
|
1317
|
+
return {
|
|
1318
|
+
"state": "unsupported",
|
|
1319
|
+
"path": normalized,
|
|
1320
|
+
"name": target.name,
|
|
1321
|
+
"size": size,
|
|
1322
|
+
"message": "Binary or unsupported file content cannot be previewed.",
|
|
1323
|
+
}
|
|
1324
|
+
try:
|
|
1325
|
+
content = data.decode("utf-8")
|
|
1326
|
+
except UnicodeDecodeError:
|
|
1327
|
+
return {
|
|
1328
|
+
"state": "unsupported",
|
|
1329
|
+
"path": normalized,
|
|
1330
|
+
"name": target.name,
|
|
1331
|
+
"size": size,
|
|
1332
|
+
"message": "Binary or unsupported file encoding cannot be previewed.",
|
|
1333
|
+
}
|
|
1334
|
+
content = content.replace("\r\n", "\n").replace("\r", "\n")
|
|
1335
|
+
truncated = len(content) > max_chars
|
|
1336
|
+
if truncated:
|
|
1337
|
+
content = content[:max_chars]
|
|
1338
|
+
return {
|
|
1339
|
+
"state": "ok",
|
|
1340
|
+
"path": normalized,
|
|
1341
|
+
"name": target.name,
|
|
1342
|
+
"kind": "file",
|
|
1343
|
+
"size": size,
|
|
1344
|
+
"contentType": content_type or "text/plain",
|
|
1345
|
+
"content": content,
|
|
1346
|
+
"truncated": truncated,
|
|
1347
|
+
"logicsType": _logics_doc_type(normalized),
|
|
1348
|
+
"message": "",
|
|
1349
|
+
}
|
|
1350
|
+
|
|
1351
|
+
|
|
1159
1352
|
def _current_git_ci_context(repo_root: Path, *, runner: Any | None = None) -> dict[str, str]:
|
|
1160
1353
|
context = {"branch": "", "headSha": "", "subject": "", "author": ""}
|
|
1161
1354
|
commands = {
|
|
@@ -1195,28 +1388,21 @@ def _is_active_ci_status(run: dict[str, Any]) -> bool:
|
|
|
1195
1388
|
|
|
1196
1389
|
|
|
1197
1390
|
def _select_github_actions_run(runs: list[dict[str, Any]], head_sha: str) -> tuple[dict[str, Any], str]:
|
|
1198
|
-
|
|
1391
|
+
ci_runs = [run for run in runs if str(run.get("name") or "").strip().lower() == "ci"]
|
|
1392
|
+
candidate_runs = ci_runs or runs
|
|
1393
|
+
head_runs = [run for run in candidate_runs if head_sha and str(run.get("head_sha") or "") == head_sha]
|
|
1199
1394
|
active_head_run = next((run for run in head_runs if _is_active_ci_status(run)), None)
|
|
1200
1395
|
if active_head_run is not None:
|
|
1201
1396
|
return active_head_run, "head-active"
|
|
1202
|
-
failing_head_run = next((run for run in head_runs if _ci_badge_state(str(run.get("status") or ""), str(run.get("conclusion") or "")) == "failing"), None)
|
|
1203
|
-
if failing_head_run is not None:
|
|
1204
|
-
return failing_head_run, "head-failing"
|
|
1205
|
-
cancelled_head_run = next((run for run in head_runs if _ci_badge_state(str(run.get("status") or ""), str(run.get("conclusion") or "")) == "cancelled"), None)
|
|
1206
|
-
if cancelled_head_run is not None:
|
|
1207
|
-
return cancelled_head_run, "head-cancelled"
|
|
1208
|
-
unknown_head_run = next((run for run in head_runs if _ci_badge_state(str(run.get("status") or ""), str(run.get("conclusion") or "")) == "unknown"), None)
|
|
1209
|
-
if unknown_head_run is not None:
|
|
1210
|
-
return unknown_head_run, "head-unknown"
|
|
1211
1397
|
if head_runs:
|
|
1398
|
+
head_state = _ci_badge_state(str(head_runs[0].get("status") or ""), str(head_runs[0].get("conclusion") or ""))
|
|
1399
|
+
if head_state in {"failing", "cancelled", "unknown"}:
|
|
1400
|
+
return head_runs[0], f"head-{head_state}"
|
|
1212
1401
|
return head_runs[0], "head"
|
|
1213
|
-
active_branch_run = next((run for run in
|
|
1402
|
+
active_branch_run = next((run for run in candidate_runs if _is_active_ci_status(run)), None)
|
|
1214
1403
|
if active_branch_run is not None:
|
|
1215
1404
|
return active_branch_run, "branch-active"
|
|
1216
|
-
|
|
1217
|
-
if failing_branch_run is not None:
|
|
1218
|
-
return failing_branch_run, "branch-failing"
|
|
1219
|
-
return runs[0], "branch-latest"
|
|
1405
|
+
return candidate_runs[0], "branch-latest"
|
|
1220
1406
|
|
|
1221
1407
|
|
|
1222
1408
|
def _parse_github_actions_run(run: dict[str, Any], *, match_source: str) -> dict[str, Any]:
|
|
@@ -1605,6 +1791,13 @@ def _cdx_mission_prompt(
|
|
|
1605
1791
|
raise ValueError("Unknown CDX mission.")
|
|
1606
1792
|
|
|
1607
1793
|
|
|
1794
|
+
def _cdx_mission_timeout(strength: dict[str, Any], *, allow_file_writes: bool = False, commit_at_end: bool = False) -> int:
|
|
1795
|
+
timeout = int(strength.get("timeout") or 180)
|
|
1796
|
+
if allow_file_writes or commit_at_end:
|
|
1797
|
+
return max(timeout, CDX_WRITABLE_MISSION_MIN_TIMEOUT_SECONDS)
|
|
1798
|
+
return timeout
|
|
1799
|
+
|
|
1800
|
+
|
|
1608
1801
|
def _cdx_mission_command(
|
|
1609
1802
|
repo_root: Path,
|
|
1610
1803
|
mission_id: str,
|
|
@@ -1627,7 +1820,7 @@ def _cdx_mission_command(
|
|
|
1627
1820
|
direct_fixes=mission_inputs.get("directFixes") == "true",
|
|
1628
1821
|
commit_at_end=commit_at_end,
|
|
1629
1822
|
)
|
|
1630
|
-
timeout =
|
|
1823
|
+
timeout = _cdx_mission_timeout(strength, allow_file_writes=allow_file_writes, commit_at_end=commit_at_end)
|
|
1631
1824
|
reasoning_effort = str(strength.get("reasoningEffort") or "medium")
|
|
1632
1825
|
power = str(strength.get("power") or "medium")
|
|
1633
1826
|
permission = "workspace-write" if allow_file_writes else "read-only"
|
|
@@ -1868,6 +2061,7 @@ def cdx_mission_plan_payload(
|
|
|
1868
2061
|
"requestedCommitAtEnd": requested_commit_at_end,
|
|
1869
2062
|
"supportsFileWrites": supports_file_writes,
|
|
1870
2063
|
"permission": permission,
|
|
2064
|
+
"timeoutSeconds": _cdx_mission_timeout(strength_def, allow_file_writes=allow_file_writes, commit_at_end=commit_at_end),
|
|
1871
2065
|
"command": ["cdx", *command],
|
|
1872
2066
|
"arguments": command,
|
|
1873
2067
|
"warnings": warnings,
|
|
@@ -1895,9 +2089,10 @@ def cdx_mission_run_payload(
|
|
|
1895
2089
|
if plan_payload.get("state") != "ok":
|
|
1896
2090
|
return {"state": plan_payload.get("state") or "error", "message": plan_payload.get("message") or "Unable to plan CDX mission.", "plan": plan_payload.get("plan"), "run": None}
|
|
1897
2091
|
plan = plan_payload["plan"]
|
|
1898
|
-
timeout = int(plan["strength"].get("timeout") or 180)
|
|
2092
|
+
timeout = int(plan.get("timeoutSeconds") or plan["strength"].get("timeout") or 180)
|
|
2093
|
+
process_timeout = timeout + CDX_MISSION_PARENT_TIMEOUT_GRACE_SECONDS
|
|
1899
2094
|
try:
|
|
1900
|
-
result = _run_cdx_mission(repo_root, list(plan["arguments"]), timeout=
|
|
2095
|
+
result = _run_cdx_mission(repo_root, list(plan["arguments"]), timeout=process_timeout, runner=cdx_runner)
|
|
1901
2096
|
except subprocess.TimeoutExpired:
|
|
1902
2097
|
return {"state": "timeout", "message": "CDX mission timed out.", "plan": plan, "run": None}
|
|
1903
2098
|
except (OSError, subprocess.SubprocessError) as exc:
|
|
@@ -2123,6 +2318,7 @@ class LogicsViewerServer(ThreadingHTTPServer):
|
|
|
2123
2318
|
repo_root: Path,
|
|
2124
2319
|
*,
|
|
2125
2320
|
auto_refresh_interval_seconds: int = 15,
|
|
2321
|
+
auto_refresh_interval_forced: bool = False,
|
|
2126
2322
|
):
|
|
2127
2323
|
self.launch_repo_root = repo_root.resolve()
|
|
2128
2324
|
self.project_roots = discover_viewer_project_roots(self.launch_repo_root)
|
|
@@ -2130,6 +2326,7 @@ class LogicsViewerServer(ThreadingHTTPServer):
|
|
|
2130
2326
|
self.active_project_id = _viewer_project_id(self.launch_repo_root)
|
|
2131
2327
|
self.repo_root = self.launch_repo_root
|
|
2132
2328
|
self.auto_refresh_interval_seconds = auto_refresh_interval_seconds
|
|
2329
|
+
self.auto_refresh_interval_forced = auto_refresh_interval_forced
|
|
2133
2330
|
super().__init__(server_address, LogicsViewerRequestHandler)
|
|
2134
2331
|
|
|
2135
2332
|
def project_registry_payload(self) -> list[dict[str, Any]]:
|
|
@@ -2140,6 +2337,7 @@ class LogicsViewerServer(ThreadingHTTPServer):
|
|
|
2140
2337
|
self.repo_root,
|
|
2141
2338
|
selected_id=selected_id,
|
|
2142
2339
|
auto_refresh_interval_seconds=self.auto_refresh_interval_seconds,
|
|
2340
|
+
auto_refresh_interval_forced=self.auto_refresh_interval_forced,
|
|
2143
2341
|
projects=self.project_registry_payload(),
|
|
2144
2342
|
)
|
|
2145
2343
|
|
|
@@ -2266,6 +2464,32 @@ class LogicsViewerRequestHandler(BaseHTTPRequestHandler):
|
|
|
2266
2464
|
rel_path = params.get("path", [""])[0]
|
|
2267
2465
|
self._send_json({"ok": True, "payload": git_file_preview_payload(self.server.repo_root, rel_path)})
|
|
2268
2466
|
return
|
|
2467
|
+
if route == "/api/workspace-tree":
|
|
2468
|
+
rel_path = parse_qs(parsed.query).get("path", [""])[0]
|
|
2469
|
+
try:
|
|
2470
|
+
self._send_json({"ok": True, "payload": workspace_tree_payload(self.server.repo_root, rel_path)})
|
|
2471
|
+
except ValueError as exc:
|
|
2472
|
+
self._send_error_json(HTTPStatus.FORBIDDEN, str(exc))
|
|
2473
|
+
return
|
|
2474
|
+
if route == "/api/workspace-preview":
|
|
2475
|
+
rel_path = parse_qs(parsed.query).get("path", [""])[0]
|
|
2476
|
+
try:
|
|
2477
|
+
self._send_json({"ok": True, "payload": workspace_preview_payload(self.server.repo_root, rel_path)})
|
|
2478
|
+
except ValueError as exc:
|
|
2479
|
+
self._send_error_json(HTTPStatus.FORBIDDEN, str(exc))
|
|
2480
|
+
return
|
|
2481
|
+
if route == "/api/workspace-file":
|
|
2482
|
+
rel_path = parse_qs(parsed.query).get("path", [""])[0]
|
|
2483
|
+
try:
|
|
2484
|
+
payload = workspace_preview_payload(self.server.repo_root, rel_path)
|
|
2485
|
+
if payload.get("state") != "image":
|
|
2486
|
+
self._send_error_json(HTTPStatus.BAD_REQUEST, "Workspace file is not an image preview.")
|
|
2487
|
+
return
|
|
2488
|
+
_normalized, absolute = _resolve_workspace_path(self.server.repo_root, rel_path)
|
|
2489
|
+
self._serve_file(absolute)
|
|
2490
|
+
except (FileNotFoundError, ValueError) as exc:
|
|
2491
|
+
self._send_error_json(HTTPStatus.NOT_FOUND, str(exc))
|
|
2492
|
+
return
|
|
2269
2493
|
self._send_error_json(HTTPStatus.NOT_FOUND, "Not found")
|
|
2270
2494
|
|
|
2271
2495
|
def do_POST(self) -> None:
|
|
@@ -2394,11 +2618,13 @@ def create_viewer_server(
|
|
|
2394
2618
|
port: int = 8765,
|
|
2395
2619
|
*,
|
|
2396
2620
|
auto_refresh_interval_seconds: int = 15,
|
|
2621
|
+
auto_refresh_interval_forced: bool = False,
|
|
2397
2622
|
) -> LogicsViewerServer:
|
|
2398
2623
|
return LogicsViewerServer(
|
|
2399
2624
|
(host, port),
|
|
2400
2625
|
repo_root,
|
|
2401
2626
|
auto_refresh_interval_seconds=auto_refresh_interval_seconds,
|
|
2627
|
+
auto_refresh_interval_forced=auto_refresh_interval_forced,
|
|
2402
2628
|
)
|
|
2403
2629
|
|
|
2404
2630
|
|
|
@@ -2446,7 +2672,7 @@ def build_parser() -> argparse.ArgumentParser:
|
|
|
2446
2672
|
parser.add_argument(
|
|
2447
2673
|
"--refresh-interval",
|
|
2448
2674
|
type=int,
|
|
2449
|
-
default=
|
|
2675
|
+
default=None,
|
|
2450
2676
|
help="Automatic refresh interval in seconds. Defaults to 15; positive intervals are allowed.",
|
|
2451
2677
|
)
|
|
2452
2678
|
parser.add_argument("--focus", help="Open the viewer focused on a workflow ref or repo-relative Logics Markdown path.")
|
|
@@ -2459,7 +2685,9 @@ def build_parser() -> argparse.ArgumentParser:
|
|
|
2459
2685
|
def main(argv: list[str]) -> int:
|
|
2460
2686
|
args = build_parser().parse_args(argv)
|
|
2461
2687
|
repo_root = find_repo_root(Path.cwd())
|
|
2462
|
-
|
|
2688
|
+
refresh_interval_forced = args.refresh_interval is not None
|
|
2689
|
+
refresh_interval = args.refresh_interval if args.refresh_interval is not None else 15
|
|
2690
|
+
if refresh_interval <= 0:
|
|
2463
2691
|
raise SystemExit("--refresh-interval must be a positive number of seconds.")
|
|
2464
2692
|
if args.read and not args.focus:
|
|
2465
2693
|
raise SystemExit("--read requires --focus.")
|
|
@@ -2471,7 +2699,8 @@ def main(argv: list[str]) -> int:
|
|
|
2471
2699
|
repo_root,
|
|
2472
2700
|
host=args.host,
|
|
2473
2701
|
port=args.port,
|
|
2474
|
-
auto_refresh_interval_seconds=
|
|
2702
|
+
auto_refresh_interval_seconds=refresh_interval,
|
|
2703
|
+
auto_refresh_interval_forced=refresh_interval_forced,
|
|
2475
2704
|
)
|
|
2476
2705
|
host, port = server.server_address[:2]
|
|
2477
2706
|
url = build_viewer_url(str(host), int(port), focus=focus, read=bool(args.read))
|
|
@@ -2483,7 +2712,7 @@ def main(argv: list[str]) -> int:
|
|
|
2483
2712
|
focus=focus,
|
|
2484
2713
|
network_url=network_url,
|
|
2485
2714
|
bind_host=str(host),
|
|
2486
|
-
auto_refresh_interval_seconds=
|
|
2715
|
+
auto_refresh_interval_seconds=refresh_interval,
|
|
2487
2716
|
),
|
|
2488
2717
|
flush=True,
|
|
2489
2718
|
)
|
package/package.json
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
"name": "@grifhinz/logics-manager",
|
|
3
3
|
"displayName": "Logics Orchestrator",
|
|
4
4
|
"description": "Visual orchestration for Logics workflows inside VS Code.",
|
|
5
|
-
"version": "2.8.
|
|
5
|
+
"version": "2.8.1",
|
|
6
6
|
"publisher": "cdx-logics",
|
|
7
7
|
"icon": "clients/shared-web/media/icon.png",
|
|
8
8
|
"repository": {
|
|
@@ -138,6 +138,7 @@
|
|
|
138
138
|
"audit:logics": "node scripts/run-python.mjs -m logics_manager audit && node scripts/run-python.mjs -m logics_manager lint",
|
|
139
139
|
"audit:logics:strict": "node scripts/run-python.mjs -m logics_manager audit --governance-profile strict && node scripts/run-python.mjs -m logics_manager lint --require-status",
|
|
140
140
|
"audit:ci": "node scripts/check-npm-audit.mjs",
|
|
141
|
+
"docs:check": "node scripts/check-readme-badges.mjs",
|
|
141
142
|
"logics:finish:task": "node scripts/run-python.mjs -m logics_manager flow finish task",
|
|
142
143
|
"ci:fast": "npm run compile && npm run lint && npm run test:coverage && npm run test:smoke && npm run lint:logics && npm run package:ci",
|
|
143
144
|
"ci:check": "node scripts/ci-check.mjs",
|