@grifhinz/logics-manager 2.3.3 → 2.4.0

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.
@@ -16,13 +16,21 @@
16
16
  <body class="viewer-shell">
17
17
  <header class="viewer-topbar" aria-label="Viewer status">
18
18
  <div>
19
- <div class="viewer-topbar__title">Logics Viewer</div>
19
+ <div class="viewer-topbar__identity">
20
+ <div class="viewer-topbar__title">Logics Viewer</div>
21
+ <span class="viewer-topbar__repo" id="viewer-repo-pill" title="">repository</span>
22
+ </div>
20
23
  <div class="viewer-topbar__meta" id="viewer-meta">Read-only local viewer</div>
21
24
  </div>
22
25
  <div class="viewer-topbar__actions">
26
+ <label class="viewer-auto-refresh" title="Toggle automatic refresh">
27
+ <input id="viewer-auto-refresh" type="checkbox" checked />
28
+ <span>Auto</span>
29
+ </label>
30
+ <button class="btn" data-action="refresh" type="button" title="Refresh viewer data">Refresh</button>
31
+ <button class="btn" id="viewer-git" type="button" title="Show Git status">Git</button>
23
32
  <button class="btn" id="viewer-insights" type="button" title="Show corpus insights">Insights</button>
24
33
  <button class="btn" id="viewer-health" type="button" title="Show lint and audit health">Health</button>
25
- <button class="btn" data-action="refresh" type="button" title="Refresh viewer data">Refresh</button>
26
34
  </div>
27
35
  </header>
28
36
 
@@ -34,28 +42,6 @@
34
42
  <path d="M4 6h16l-6 7v5l-4 2v-7z" fill="none" stroke="currentColor" stroke-width="2" stroke-linejoin="round" stroke-linecap="round" />
35
43
  </svg>
36
44
  </button>
37
- <div class="toolbar__tools">
38
- <button class="toolbar__filter" id="tools-toggle" aria-label="Open tools menu" aria-haspopup="menu" aria-expanded="false" aria-controls="tools-panel" title="Open tools menu">
39
- <svg viewBox="0 0 24 24" aria-hidden="true" focusable="false">
40
- <rect x="5" y="6" width="14" height="2" rx="1" fill="currentColor" />
41
- <rect x="5" y="11" width="14" height="2" rx="1" fill="currentColor" />
42
- <rect x="5" y="16" width="14" height="2" rx="1" fill="currentColor" />
43
- </svg>
44
- </button>
45
- <div class="tools-panel" id="tools-panel" aria-hidden="true" role="menu">
46
- <div class="tools-panel__header">
47
- <div class="tools-panel__header-label">Tools</div>
48
- <button class="tools-panel__close" type="button" data-tools-panel-close aria-label="Close tools menu" title="Close tools menu">x</button>
49
- </div>
50
- <div class="tools-panel__section" data-tools-section="recommended">
51
- <div class="tools-panel__section-label">Read-only</div>
52
- <div class="tools-panel__section-body" data-tools-body="recommended">
53
- <button class="tools-panel__item" type="button" role="menuitem" data-action="refresh" title="Refresh viewer data">Refresh</button>
54
- <button class="tools-panel__item" type="button" role="menuitem" data-action="about" title="Open project page">About</button>
55
- </div>
56
- </div>
57
- </div>
58
- </div>
59
45
  <button class="toolbar__filter toolbar__filter--view" data-action="toggle-view-mode" aria-label="Switch display mode" title="Switch display mode">
60
46
  <svg viewBox="0 0 24 24" aria-hidden="true" focusable="false">
61
47
  <rect x="4" y="5" width="6" height="6" rx="1.5" fill="none" stroke="currentColor" stroke-width="2" />
@@ -67,11 +53,11 @@
67
53
  <button class="toolbar__filter" id="activity-toggle" aria-label="Toggle activity" aria-pressed="false" title="Toggle activity">
68
54
  <svg viewBox="0 0 24 24" aria-hidden="true" focusable="false"><path d="M4 12h4l2.2-5 3.6 10 2.2-5H20" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" /></svg>
69
55
  </button>
70
- <button class="toolbar__filter" id="attention-toggle" aria-label="Show attention required" aria-pressed="false" title="Show attention required">
71
- <svg viewBox="0 0 24 24" aria-hidden="true" focusable="false"><path d="M12 4l8 14H4z" fill="none" stroke="currentColor" stroke-width="2" /><path d="M12 9v4" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" /><circle cx="12" cy="17" r="1" fill="currentColor" /></svg>
56
+ <button class="toolbar__filter" id="activity-clear" aria-label="Clear local activity history" title="Clear local activity history">
57
+ <svg viewBox="0 0 24 24" aria-hidden="true" focusable="false"><path d="M5 7h14" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" /><path d="M9 7V5h6v2" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" /><path d="M8 10v8m4-8v8m4-8v8" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" /></svg>
72
58
  </button>
73
- <button class="toolbar__filter" id="header-logics-insights" aria-label="Open corpus insights" title="Open corpus insights">
74
- <svg viewBox="0 0 24 24" aria-hidden="true" focusable="false"><circle cx="12" cy="12" r="8" fill="none" stroke="currentColor" stroke-width="2" /><path d="M12 10.5v6" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" /><circle cx="12" cy="7.5" r="1" fill="currentColor" /></svg>
59
+ <button class="toolbar__filter" id="attention-toggle" aria-label="Show blocked, orphaned, unprocessed, or inconsistent items" aria-pressed="false" title="Show blocked, orphaned, unprocessed, or inconsistent items">
60
+ <svg viewBox="0 0 24 24" aria-hidden="true" focusable="false"><path d="M12 4l8 14H4z" fill="none" stroke="currentColor" stroke-width="2" /><path d="M12 9v4" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" /><circle cx="12" cy="17" r="1" fill="currentColor" /></svg>
75
61
  </button>
76
62
  </div>
77
63
  <div class="toolbar__search">
@@ -16,6 +16,27 @@
16
16
  line-height: 1.25;
17
17
  }
18
18
 
19
+ .viewer-topbar__identity {
20
+ display: flex;
21
+ align-items: center;
22
+ gap: 8px;
23
+ min-width: 0;
24
+ }
25
+
26
+ .viewer-topbar__repo {
27
+ max-width: 220px;
28
+ overflow: hidden;
29
+ text-overflow: ellipsis;
30
+ white-space: nowrap;
31
+ border: 1px solid var(--vscode-panel-border, #333333);
32
+ border-radius: 999px;
33
+ padding: 2px 8px;
34
+ background: var(--vscode-badge-background, #4d4d4d);
35
+ color: var(--vscode-badge-foreground, #ffffff);
36
+ font-size: 11px;
37
+ line-height: 1.3;
38
+ }
39
+
19
40
  .viewer-topbar__meta {
20
41
  margin-top: 2px;
21
42
  font-size: 12px;
@@ -30,6 +51,24 @@
30
51
  flex: 0 0 auto;
31
52
  }
32
53
 
54
+ .viewer-auto-refresh {
55
+ display: inline-flex;
56
+ align-items: center;
57
+ gap: 6px;
58
+ min-height: 28px;
59
+ border: 1px solid var(--vscode-button-border, var(--vscode-panel-border, #333333));
60
+ border-radius: 4px;
61
+ padding: 0 8px;
62
+ background: var(--vscode-button-secondaryBackground, transparent);
63
+ color: var(--vscode-button-secondaryForeground, var(--vscode-foreground, #d4d4d4));
64
+ font-size: 12px;
65
+ user-select: none;
66
+ }
67
+
68
+ .viewer-auto-refresh input {
69
+ margin: 0;
70
+ }
71
+
33
72
  .viewer-update {
34
73
  flex: 0 0 auto;
35
74
  display: flex;
@@ -220,6 +259,103 @@
220
259
  background: var(--vscode-textCodeBlock-background, #111111);
221
260
  }
222
261
 
262
+ .viewer-insights__rows,
263
+ .viewer-git__files {
264
+ display: grid;
265
+ gap: 6px;
266
+ margin: 10px 0 0;
267
+ padding: 0;
268
+ list-style: none;
269
+ }
270
+
271
+ .viewer-insights__row {
272
+ display: flex;
273
+ align-items: center;
274
+ justify-content: space-between;
275
+ gap: 10px;
276
+ min-width: 0;
277
+ padding: 7px 9px;
278
+ border: 1px solid var(--vscode-panel-border, #333333);
279
+ border-radius: 6px;
280
+ background: color-mix(in srgb, var(--vscode-editorWidget-background, #202020) 84%, transparent);
281
+ font-size: 12px;
282
+ }
283
+
284
+ .viewer-insights__row[hidden] {
285
+ display: none !important;
286
+ }
287
+
288
+ .viewer-insights__doc,
289
+ .viewer-insights__action,
290
+ .viewer-insights__reveal,
291
+ .viewer-health__path {
292
+ min-width: 0;
293
+ border: 0;
294
+ padding: 0;
295
+ background: transparent;
296
+ color: var(--vscode-textLink-foreground, #4ea1ff);
297
+ font: inherit;
298
+ text-align: left;
299
+ overflow-wrap: anywhere;
300
+ cursor: pointer;
301
+ }
302
+
303
+ .viewer-insights__doc:hover,
304
+ .viewer-insights__action:hover,
305
+ .viewer-insights__reveal:hover,
306
+ .viewer-health__path:hover {
307
+ text-decoration: underline;
308
+ }
309
+
310
+ .viewer-git,
311
+ .viewer-insights,
312
+ .viewer-health {
313
+ display: grid;
314
+ gap: 14px;
315
+ }
316
+
317
+ .viewer-git__summary,
318
+ .viewer-insights__summary,
319
+ .viewer-health__summary {
320
+ display: grid;
321
+ grid-template-columns: repeat(auto-fit, minmax(120px, 1fr));
322
+ gap: 8px;
323
+ }
324
+
325
+ .viewer-git__section h2 {
326
+ margin-bottom: 8px;
327
+ }
328
+
329
+ .viewer-git__files li {
330
+ min-width: 0;
331
+ padding: 7px 9px;
332
+ border: 1px solid var(--vscode-panel-border, #333333);
333
+ border-radius: 6px;
334
+ background: var(--vscode-editorWidget-background, #202020);
335
+ overflow-wrap: anywhere;
336
+ }
337
+
338
+ .viewer-git__commit,
339
+ .viewer-git__state {
340
+ margin: 0;
341
+ overflow-wrap: anywhere;
342
+ }
343
+
344
+ @media (max-width: 700px) {
345
+ .viewer-topbar {
346
+ align-items: stretch;
347
+ flex-direction: column;
348
+ }
349
+
350
+ .viewer-topbar__actions {
351
+ flex-wrap: wrap;
352
+ }
353
+
354
+ .viewer-document {
355
+ inset: 8px;
356
+ }
357
+ }
358
+
223
359
  .viewer-document__content a {
224
360
  color: var(--vscode-textLink-foreground, #4ea1ff);
225
361
  }
@@ -412,12 +548,17 @@
412
548
 
413
549
  .viewer-topbar__actions {
414
550
  width: 100%;
551
+ flex-wrap: wrap;
415
552
  }
416
553
 
417
554
  .viewer-topbar__actions .btn {
418
555
  flex: 1 1 0;
419
556
  }
420
557
 
558
+ .viewer-auto-refresh {
559
+ flex: 0 0 auto;
560
+ }
561
+
421
562
  .viewer-document {
422
563
  inset: 10px;
423
564
  }
@@ -5,6 +5,8 @@ import json
5
5
  import mimetypes
6
6
  import os
7
7
  import re
8
+ import shutil
9
+ import socket
8
10
  import subprocess
9
11
  import sys
10
12
  import webbrowser
@@ -328,9 +330,16 @@ def collect_viewer_items(repo_root: Path) -> list[dict[str, Any]]:
328
330
  return items
329
331
 
330
332
 
331
- def viewer_data_payload(repo_root: Path, selected_id: str | None = None) -> dict[str, Any]:
333
+ def viewer_data_payload(
334
+ repo_root: Path,
335
+ selected_id: str | None = None,
336
+ *,
337
+ auto_refresh_interval_seconds: int = 60,
338
+ ) -> dict[str, Any]:
332
339
  return {
333
340
  "root": str(repo_root.resolve()),
341
+ "repoName": repo_root.resolve().name,
342
+ "autoRefreshIntervalSeconds": auto_refresh_interval_seconds,
334
343
  "items": collect_viewer_items(repo_root),
335
344
  "updateInfo": get_update_info(_current_version()).to_payload(),
336
345
  "selectedId": selected_id,
@@ -384,13 +393,122 @@ def _system_editor_command(path: Path) -> list[str]:
384
393
  return ["xdg-open", str(path)]
385
394
 
386
395
 
396
+ def _run_read_only_git(repo_root: Path, args: list[str], *, runner: Any | None = None) -> subprocess.CompletedProcess[str]:
397
+ command = ["git", *args]
398
+ git_runner = runner or subprocess.run
399
+ return git_runner(command, cwd=repo_root, text=True, capture_output=True, timeout=5)
400
+
401
+
402
+ def _sanitize_git_ref(value: str) -> str:
403
+ ref = value.strip()
404
+ ref = re.sub(r"://[^/@\s]+@", "://", ref)
405
+ ref = re.sub(r"^[^/@\s]+@", "", ref)
406
+ return ref[:200]
407
+
408
+
409
+ def _classify_porcelain_entry(line: str) -> tuple[str, dict[str, str]] | None:
410
+ if not line or line.startswith("## "):
411
+ return None
412
+ if line.startswith("?? "):
413
+ return "untracked", {"path": line[3:].strip()}
414
+ if len(line) < 4:
415
+ return None
416
+ staged = line[0]
417
+ worktree = line[1]
418
+ raw_path = line[3:].strip()
419
+ if " -> " in raw_path:
420
+ before, after = raw_path.split(" -> ", 1)
421
+ return "renamed", {"path": after.strip(), "from": before.strip()}
422
+ if staged == "R":
423
+ return "renamed", {"path": raw_path}
424
+ if staged not in {" ", "?", "!"}:
425
+ return "staged", {"path": raw_path, "code": staged}
426
+ if worktree == "D":
427
+ return "deleted", {"path": raw_path, "code": worktree}
428
+ if worktree not in {" ", "?", "!"}:
429
+ return "modified", {"path": raw_path, "code": worktree}
430
+ return None
431
+
432
+
433
+ def _parse_git_branch_line(line: str) -> dict[str, Any]:
434
+ branch = line[3:].strip() if line.startswith("## ") else ""
435
+ tracking = ""
436
+ ahead = 0
437
+ behind = 0
438
+ if "..." in branch:
439
+ branch, tracking_part = branch.split("...", 1)
440
+ if " [" in tracking_part:
441
+ tracking, details = tracking_part.split(" [", 1)
442
+ for detail in details.rstrip("]").split(", "):
443
+ if detail.startswith("ahead "):
444
+ ahead = int(detail.removeprefix("ahead ") or "0")
445
+ if detail.startswith("behind "):
446
+ behind = int(detail.removeprefix("behind ") or "0")
447
+ else:
448
+ tracking = tracking_part
449
+ return {
450
+ "branch": _sanitize_git_ref(branch or "HEAD"),
451
+ "tracking": _sanitize_git_ref(tracking),
452
+ "ahead": ahead,
453
+ "behind": behind,
454
+ }
455
+
456
+
457
+ def git_status_payload(repo_root: Path, *, runner: Any | None = None, which: Any | None = None) -> dict[str, Any]:
458
+ git_which = which or shutil.which
459
+ if not git_which("git"):
460
+ return {"state": "unavailable", "message": "Git is not available on PATH."}
461
+ try:
462
+ inside = _run_read_only_git(repo_root, ["rev-parse", "--is-inside-work-tree"], runner=runner)
463
+ except (OSError, subprocess.SubprocessError) as exc:
464
+ return {"state": "error", "message": f"Unable to run Git status: {exc}"}
465
+ if inside.returncode != 0 or inside.stdout.strip().lower() != "true":
466
+ return {"state": "not-repository", "message": "This folder is not inside a Git worktree."}
467
+
468
+ try:
469
+ status = _run_read_only_git(repo_root, ["status", "--porcelain=v1", "-b"], runner=runner)
470
+ commit = _run_read_only_git(repo_root, ["log", "-1", "--pretty=format:%h %s"], runner=runner)
471
+ except (OSError, subprocess.SubprocessError) as exc:
472
+ return {"state": "error", "message": f"Unable to collect Git status: {exc}"}
473
+ if status.returncode != 0:
474
+ message = (status.stderr or status.stdout or "Git status failed.").strip().splitlines()[0]
475
+ return {"state": "error", "message": message}
476
+
477
+ lines = status.stdout.splitlines()
478
+ branch_info = _parse_git_branch_line(lines[0]) if lines else {"branch": "HEAD", "tracking": "", "ahead": 0, "behind": 0}
479
+ groups: dict[str, list[dict[str, str]]] = {key: [] for key in ("staged", "modified", "deleted", "renamed", "untracked")}
480
+ for line in lines[1:]:
481
+ classified = _classify_porcelain_entry(line)
482
+ if classified:
483
+ group, entry = classified
484
+ groups[group].append(entry)
485
+ counts = {key: len(value) for key, value in groups.items()}
486
+ dirty = any(counts.values())
487
+ return {
488
+ "state": "ok",
489
+ **branch_info,
490
+ "clean": not dirty,
491
+ "dirty": dirty,
492
+ "counts": counts,
493
+ "groups": groups,
494
+ "latestCommit": (commit.stdout.strip() if commit.returncode == 0 else "")[:300],
495
+ }
496
+
497
+
387
498
  def _json_bytes(payload: Any) -> bytes:
388
499
  return json.dumps(payload, indent=2, sort_keys=True).encode("utf-8")
389
500
 
390
501
 
391
502
  class LogicsViewerServer(ThreadingHTTPServer):
392
- def __init__(self, server_address: tuple[str, int], repo_root: Path):
503
+ def __init__(
504
+ self,
505
+ server_address: tuple[str, int],
506
+ repo_root: Path,
507
+ *,
508
+ auto_refresh_interval_seconds: int = 60,
509
+ ):
393
510
  self.repo_root = repo_root.resolve()
511
+ self.auto_refresh_interval_seconds = auto_refresh_interval_seconds
394
512
  super().__init__(server_address, LogicsViewerRequestHandler)
395
513
 
396
514
 
@@ -453,7 +571,15 @@ class LogicsViewerRequestHandler(BaseHTTPRequestHandler):
453
571
  self._serve_file(media_path)
454
572
  return
455
573
  if route == "/api/items":
456
- self._send_json({"ok": True, "payload": viewer_data_payload(self.server.repo_root)})
574
+ self._send_json(
575
+ {
576
+ "ok": True,
577
+ "payload": viewer_data_payload(
578
+ self.server.repo_root,
579
+ auto_refresh_interval_seconds=self.server.auto_refresh_interval_seconds,
580
+ ),
581
+ }
582
+ )
457
583
  return
458
584
  if route == "/api/doc":
459
585
  rel_path = parse_qs(parsed.query).get("path", [""])[0]
@@ -468,12 +594,23 @@ class LogicsViewerRequestHandler(BaseHTTPRequestHandler):
468
594
  if route == "/api/audit":
469
595
  self._send_json({"ok": True, "payload": audit_payload(self.server.repo_root)})
470
596
  return
597
+ if route == "/api/git-status":
598
+ self._send_json({"ok": True, "payload": git_status_payload(self.server.repo_root)})
599
+ return
471
600
  self._send_error_json(HTTPStatus.NOT_FOUND, "Not found")
472
601
 
473
602
  def do_POST(self) -> None:
474
603
  parsed = urlparse(self.path)
475
604
  if parsed.path == "/api/refresh":
476
- self._send_json({"ok": True, "payload": viewer_data_payload(self.server.repo_root)})
605
+ self._send_json(
606
+ {
607
+ "ok": True,
608
+ "payload": viewer_data_payload(
609
+ self.server.repo_root,
610
+ auto_refresh_interval_seconds=self.server.auto_refresh_interval_seconds,
611
+ ),
612
+ }
613
+ )
477
614
  return
478
615
  if parsed.path == "/api/edit":
479
616
  rel_path = parse_qs(parsed.query).get("path", [""])[0]
@@ -487,19 +624,52 @@ class LogicsViewerRequestHandler(BaseHTTPRequestHandler):
487
624
  self._send_error_json(HTTPStatus.NOT_FOUND, "Not found")
488
625
 
489
626
 
490
- def create_viewer_server(repo_root: Path, host: str = "127.0.0.1", port: int = 8765) -> LogicsViewerServer:
491
- return LogicsViewerServer((host, port), repo_root)
627
+ def create_viewer_server(
628
+ repo_root: Path,
629
+ host: str = "127.0.0.1",
630
+ port: int = 8765,
631
+ *,
632
+ auto_refresh_interval_seconds: int = 60,
633
+ ) -> LogicsViewerServer:
634
+ return LogicsViewerServer(
635
+ (host, port),
636
+ repo_root,
637
+ auto_refresh_interval_seconds=auto_refresh_interval_seconds,
638
+ )
492
639
 
493
640
 
494
- def render_start_status(url: str, repo_root: Path, *, focus: str | None = None) -> str:
641
+ def _network_viewer_url(host: str, port: int, *, focus: str | None = None, read: bool = False) -> str | None:
642
+ if host not in {"0.0.0.0", "::", ""}:
643
+ return None
644
+ try:
645
+ candidate = socket.gethostbyname(socket.gethostname())
646
+ except OSError:
647
+ return None
648
+ if not candidate or candidate.startswith("127."):
649
+ return None
650
+ return build_viewer_url(candidate, port, focus=focus, read=read)
651
+
652
+
653
+ def render_start_status(
654
+ url: str,
655
+ repo_root: Path,
656
+ *,
657
+ focus: str | None = None,
658
+ network_url: str | None = None,
659
+ bind_host: str = "localhost",
660
+ auto_refresh_interval_seconds: int = 60,
661
+ ) -> str:
495
662
  lines = [
496
663
  "Logics viewer running:",
497
- url,
664
+ f"Local: {url}",
498
665
  "",
499
666
  f"Repo: {repo_root.name}",
500
667
  "Mode: read-only",
501
- "Bind: localhost",
668
+ f"Bind: {bind_host}",
669
+ f"Auto refresh: {auto_refresh_interval_seconds}s",
502
670
  ]
671
+ if network_url:
672
+ lines.insert(2, f"Network: {network_url}")
503
673
  if focus:
504
674
  lines.append(f"Focus: {focus}")
505
675
  return "\n".join(lines)
@@ -509,6 +679,12 @@ def build_parser() -> argparse.ArgumentParser:
509
679
  parser = argparse.ArgumentParser(prog="logics-manager view", description="Start the local read-only Logics browser viewer.")
510
680
  parser.add_argument("--host", default="127.0.0.1", help="Bind host. Defaults to 127.0.0.1.")
511
681
  parser.add_argument("--port", type=int, default=8765, help="Bind port. Use 0 to select an available port.")
682
+ parser.add_argument(
683
+ "--refresh-interval",
684
+ type=int,
685
+ default=60,
686
+ help="Automatic refresh interval in seconds. Defaults to 60; positive shorter intervals are allowed.",
687
+ )
512
688
  parser.add_argument("--focus", help="Open the viewer focused on a workflow ref or repo-relative Logics Markdown path.")
513
689
  parser.add_argument("--read", action="store_true", help="Open the focused item in the read preview. Requires --focus.")
514
690
  parser.add_argument("--open", action="store_true", help="Open the viewer in the default browser.")
@@ -519,16 +695,34 @@ def build_parser() -> argparse.ArgumentParser:
519
695
  def main(argv: list[str]) -> int:
520
696
  args = build_parser().parse_args(argv)
521
697
  repo_root = find_repo_root(Path.cwd())
698
+ if args.refresh_interval <= 0:
699
+ raise SystemExit("--refresh-interval must be a positive number of seconds.")
522
700
  if args.read and not args.focus:
523
701
  raise SystemExit("--read requires --focus.")
524
702
  try:
525
703
  focus = normalize_viewer_focus_target(repo_root, args.focus) if args.focus else None
526
704
  except ValueError as exc:
527
705
  raise SystemExit(str(exc)) from exc
528
- server = create_viewer_server(repo_root, host=args.host, port=args.port)
706
+ server = create_viewer_server(
707
+ repo_root,
708
+ host=args.host,
709
+ port=args.port,
710
+ auto_refresh_interval_seconds=args.refresh_interval,
711
+ )
529
712
  host, port = server.server_address[:2]
530
713
  url = build_viewer_url(str(host), int(port), focus=focus, read=bool(args.read))
531
- print(render_start_status(url, repo_root, focus=focus), flush=True)
714
+ network_url = _network_viewer_url(str(host), int(port), focus=focus, read=bool(args.read))
715
+ print(
716
+ render_start_status(
717
+ url,
718
+ repo_root,
719
+ focus=focus,
720
+ network_url=network_url,
721
+ bind_host=str(host),
722
+ auto_refresh_interval_seconds=args.refresh_interval,
723
+ ),
724
+ flush=True,
725
+ )
532
726
  if args.open and not args.no_open:
533
727
  webbrowser.open(url)
534
728
 
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.3.3",
5
+ "version": "2.4.0",
6
6
  "publisher": "cdx-logics",
7
7
  "icon": "clients/shared-web/media/icon.png",
8
8
  "repository": {
@@ -127,6 +127,7 @@
127
127
  "test:coverage:media": "node scripts/run-plugin-coverage.mjs media",
128
128
  "test:coverage": "npm run test:coverage:src && npm run test:coverage:media",
129
129
  "test:smoke": "node tests/run_extension_smoke_checks.mjs",
130
+ "test:viewer-smoke": "node tests/run_local_viewer_visual_smoke.mjs",
130
131
  "test:npm-cli": "node scripts/npm/logics-manager.mjs --help",
131
132
  "test:lifecycle": "node tests/run_plugin_lifecycle_checks.mjs",
132
133
  "test:watch": "vitest",
@@ -155,14 +156,15 @@
155
156
  "@types/vscode": "^1.86.0",
156
157
  "@typescript-eslint/eslint-plugin": "^8.58.1",
157
158
  "@typescript-eslint/parser": "^8.58.1",
158
- "@vscode/vsce": "^3.9.1",
159
159
  "@vitest/coverage-v8": "^4.1.2",
160
+ "@vscode/vsce": "^3.9.1",
160
161
  "esbuild": "^0.25.10",
161
162
  "eslint": "^10.2.0",
162
163
  "jsdom": "^25.0.1",
163
164
  "mermaid": "^11.14.0",
164
165
  "typescript": "^5.3.3",
165
166
  "vitest": "^4.1.2",
167
+ "ws": "8.21.0",
166
168
  "yaml": "^2.8.3",
167
169
  "yauzl": "^3.2.0"
168
170
  },
package/pyproject.toml CHANGED
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "logics-manager"
7
- version = "2.3.3"
7
+ version = "2.4.0"
8
8
  description = "Canonical Logics CLI"
9
9
  requires-python = ">=3.10"
10
10