codex-autorunner 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.
Files changed (147) hide show
  1. codex_autorunner/__init__.py +3 -0
  2. codex_autorunner/bootstrap.py +151 -0
  3. codex_autorunner/cli.py +886 -0
  4. codex_autorunner/codex_cli.py +79 -0
  5. codex_autorunner/codex_runner.py +17 -0
  6. codex_autorunner/core/__init__.py +1 -0
  7. codex_autorunner/core/about_car.py +125 -0
  8. codex_autorunner/core/codex_runner.py +100 -0
  9. codex_autorunner/core/config.py +1465 -0
  10. codex_autorunner/core/doc_chat.py +547 -0
  11. codex_autorunner/core/docs.py +37 -0
  12. codex_autorunner/core/engine.py +720 -0
  13. codex_autorunner/core/git_utils.py +206 -0
  14. codex_autorunner/core/hub.py +756 -0
  15. codex_autorunner/core/injected_context.py +9 -0
  16. codex_autorunner/core/locks.py +57 -0
  17. codex_autorunner/core/logging_utils.py +158 -0
  18. codex_autorunner/core/notifications.py +465 -0
  19. codex_autorunner/core/optional_dependencies.py +41 -0
  20. codex_autorunner/core/prompt.py +107 -0
  21. codex_autorunner/core/prompts.py +275 -0
  22. codex_autorunner/core/request_context.py +21 -0
  23. codex_autorunner/core/runner_controller.py +116 -0
  24. codex_autorunner/core/runner_process.py +29 -0
  25. codex_autorunner/core/snapshot.py +576 -0
  26. codex_autorunner/core/state.py +156 -0
  27. codex_autorunner/core/update.py +567 -0
  28. codex_autorunner/core/update_runner.py +44 -0
  29. codex_autorunner/core/usage.py +1221 -0
  30. codex_autorunner/core/utils.py +108 -0
  31. codex_autorunner/discovery.py +102 -0
  32. codex_autorunner/housekeeping.py +423 -0
  33. codex_autorunner/integrations/__init__.py +1 -0
  34. codex_autorunner/integrations/app_server/__init__.py +6 -0
  35. codex_autorunner/integrations/app_server/client.py +1386 -0
  36. codex_autorunner/integrations/app_server/supervisor.py +206 -0
  37. codex_autorunner/integrations/github/__init__.py +10 -0
  38. codex_autorunner/integrations/github/service.py +889 -0
  39. codex_autorunner/integrations/telegram/__init__.py +1 -0
  40. codex_autorunner/integrations/telegram/adapter.py +1401 -0
  41. codex_autorunner/integrations/telegram/commands_registry.py +104 -0
  42. codex_autorunner/integrations/telegram/config.py +450 -0
  43. codex_autorunner/integrations/telegram/constants.py +154 -0
  44. codex_autorunner/integrations/telegram/dispatch.py +162 -0
  45. codex_autorunner/integrations/telegram/handlers/__init__.py +0 -0
  46. codex_autorunner/integrations/telegram/handlers/approvals.py +241 -0
  47. codex_autorunner/integrations/telegram/handlers/callbacks.py +72 -0
  48. codex_autorunner/integrations/telegram/handlers/commands.py +160 -0
  49. codex_autorunner/integrations/telegram/handlers/commands_runtime.py +5262 -0
  50. codex_autorunner/integrations/telegram/handlers/messages.py +477 -0
  51. codex_autorunner/integrations/telegram/handlers/selections.py +545 -0
  52. codex_autorunner/integrations/telegram/helpers.py +2084 -0
  53. codex_autorunner/integrations/telegram/notifications.py +164 -0
  54. codex_autorunner/integrations/telegram/outbox.py +174 -0
  55. codex_autorunner/integrations/telegram/rendering.py +102 -0
  56. codex_autorunner/integrations/telegram/retry.py +37 -0
  57. codex_autorunner/integrations/telegram/runtime.py +270 -0
  58. codex_autorunner/integrations/telegram/service.py +921 -0
  59. codex_autorunner/integrations/telegram/state.py +1223 -0
  60. codex_autorunner/integrations/telegram/transport.py +318 -0
  61. codex_autorunner/integrations/telegram/types.py +57 -0
  62. codex_autorunner/integrations/telegram/voice.py +413 -0
  63. codex_autorunner/manifest.py +150 -0
  64. codex_autorunner/routes/__init__.py +53 -0
  65. codex_autorunner/routes/base.py +470 -0
  66. codex_autorunner/routes/docs.py +275 -0
  67. codex_autorunner/routes/github.py +197 -0
  68. codex_autorunner/routes/repos.py +121 -0
  69. codex_autorunner/routes/sessions.py +137 -0
  70. codex_autorunner/routes/shared.py +137 -0
  71. codex_autorunner/routes/system.py +175 -0
  72. codex_autorunner/routes/terminal_images.py +107 -0
  73. codex_autorunner/routes/voice.py +128 -0
  74. codex_autorunner/server.py +23 -0
  75. codex_autorunner/spec_ingest.py +113 -0
  76. codex_autorunner/static/app.js +95 -0
  77. codex_autorunner/static/autoRefresh.js +209 -0
  78. codex_autorunner/static/bootstrap.js +105 -0
  79. codex_autorunner/static/bus.js +23 -0
  80. codex_autorunner/static/cache.js +52 -0
  81. codex_autorunner/static/constants.js +48 -0
  82. codex_autorunner/static/dashboard.js +795 -0
  83. codex_autorunner/static/docs.js +1514 -0
  84. codex_autorunner/static/env.js +99 -0
  85. codex_autorunner/static/github.js +168 -0
  86. codex_autorunner/static/hub.js +1511 -0
  87. codex_autorunner/static/index.html +622 -0
  88. codex_autorunner/static/loader.js +28 -0
  89. codex_autorunner/static/logs.js +690 -0
  90. codex_autorunner/static/mobileCompact.js +300 -0
  91. codex_autorunner/static/snapshot.js +116 -0
  92. codex_autorunner/static/state.js +87 -0
  93. codex_autorunner/static/styles.css +4966 -0
  94. codex_autorunner/static/tabs.js +50 -0
  95. codex_autorunner/static/terminal.js +21 -0
  96. codex_autorunner/static/terminalManager.js +3535 -0
  97. codex_autorunner/static/todoPreview.js +25 -0
  98. codex_autorunner/static/types.d.ts +8 -0
  99. codex_autorunner/static/utils.js +597 -0
  100. codex_autorunner/static/vendor/LICENSE.xterm +24 -0
  101. codex_autorunner/static/vendor/fonts/jetbrains-mono/JetBrainsMono-400-cyrillic-ext.woff2 +0 -0
  102. codex_autorunner/static/vendor/fonts/jetbrains-mono/JetBrainsMono-400-cyrillic.woff2 +0 -0
  103. codex_autorunner/static/vendor/fonts/jetbrains-mono/JetBrainsMono-400-greek.woff2 +0 -0
  104. codex_autorunner/static/vendor/fonts/jetbrains-mono/JetBrainsMono-400-latin-ext.woff2 +0 -0
  105. codex_autorunner/static/vendor/fonts/jetbrains-mono/JetBrainsMono-400-latin.woff2 +0 -0
  106. codex_autorunner/static/vendor/fonts/jetbrains-mono/JetBrainsMono-400-vietnamese.woff2 +0 -0
  107. codex_autorunner/static/vendor/fonts/jetbrains-mono/JetBrainsMono-500-cyrillic-ext.woff2 +0 -0
  108. codex_autorunner/static/vendor/fonts/jetbrains-mono/JetBrainsMono-500-cyrillic.woff2 +0 -0
  109. codex_autorunner/static/vendor/fonts/jetbrains-mono/JetBrainsMono-500-greek.woff2 +0 -0
  110. codex_autorunner/static/vendor/fonts/jetbrains-mono/JetBrainsMono-500-latin-ext.woff2 +0 -0
  111. codex_autorunner/static/vendor/fonts/jetbrains-mono/JetBrainsMono-500-latin.woff2 +0 -0
  112. codex_autorunner/static/vendor/fonts/jetbrains-mono/JetBrainsMono-500-vietnamese.woff2 +0 -0
  113. codex_autorunner/static/vendor/fonts/jetbrains-mono/JetBrainsMono-600-cyrillic-ext.woff2 +0 -0
  114. codex_autorunner/static/vendor/fonts/jetbrains-mono/JetBrainsMono-600-cyrillic.woff2 +0 -0
  115. codex_autorunner/static/vendor/fonts/jetbrains-mono/JetBrainsMono-600-greek.woff2 +0 -0
  116. codex_autorunner/static/vendor/fonts/jetbrains-mono/JetBrainsMono-600-latin-ext.woff2 +0 -0
  117. codex_autorunner/static/vendor/fonts/jetbrains-mono/JetBrainsMono-600-latin.woff2 +0 -0
  118. codex_autorunner/static/vendor/fonts/jetbrains-mono/JetBrainsMono-600-vietnamese.woff2 +0 -0
  119. codex_autorunner/static/vendor/fonts/jetbrains-mono/OFL.txt +93 -0
  120. codex_autorunner/static/vendor/xterm-addon-fit.js +2 -0
  121. codex_autorunner/static/vendor/xterm.css +209 -0
  122. codex_autorunner/static/vendor/xterm.js +2 -0
  123. codex_autorunner/static/voice.js +591 -0
  124. codex_autorunner/voice/__init__.py +39 -0
  125. codex_autorunner/voice/capture.py +349 -0
  126. codex_autorunner/voice/config.py +167 -0
  127. codex_autorunner/voice/provider.py +66 -0
  128. codex_autorunner/voice/providers/__init__.py +7 -0
  129. codex_autorunner/voice/providers/openai_whisper.py +345 -0
  130. codex_autorunner/voice/resolver.py +36 -0
  131. codex_autorunner/voice/service.py +210 -0
  132. codex_autorunner/web/__init__.py +1 -0
  133. codex_autorunner/web/app.py +1037 -0
  134. codex_autorunner/web/hub_jobs.py +181 -0
  135. codex_autorunner/web/middleware.py +552 -0
  136. codex_autorunner/web/pty_session.py +357 -0
  137. codex_autorunner/web/runner_manager.py +25 -0
  138. codex_autorunner/web/schemas.py +253 -0
  139. codex_autorunner/web/static_assets.py +430 -0
  140. codex_autorunner/web/terminal_sessions.py +78 -0
  141. codex_autorunner/workspace.py +16 -0
  142. codex_autorunner-0.1.0.dist-info/METADATA +240 -0
  143. codex_autorunner-0.1.0.dist-info/RECORD +147 -0
  144. codex_autorunner-0.1.0.dist-info/WHEEL +5 -0
  145. codex_autorunner-0.1.0.dist-info/entry_points.txt +3 -0
  146. codex_autorunner-0.1.0.dist-info/licenses/LICENSE +21 -0
  147. codex_autorunner-0.1.0.dist-info/top_level.txt +1 -0
@@ -0,0 +1,23 @@
1
+ from importlib import resources
2
+
3
+ from .core.engine import Engine, LockError, clear_stale_lock, doctor
4
+ from .web.app import create_app, create_hub_app
5
+ from .web.middleware import BasePathRouterMiddleware
6
+ from .web.static_assets import resolve_static_dir
7
+
8
+
9
+ def _static_dir():
10
+ return resolve_static_dir()
11
+
12
+
13
+ __all__ = [
14
+ "Engine",
15
+ "LockError",
16
+ "BasePathRouterMiddleware",
17
+ "clear_stale_lock",
18
+ "create_app",
19
+ "create_hub_app",
20
+ "doctor",
21
+ "resources",
22
+ "_static_dir",
23
+ ]
@@ -0,0 +1,113 @@
1
+ import re
2
+ import subprocess
3
+ from pathlib import Path
4
+ from typing import Dict, Optional
5
+
6
+ from .core.codex_runner import build_codex_command
7
+ from .core.engine import Engine
8
+ from .core.prompts import SPEC_INGEST_PROMPT
9
+ from .core.utils import atomic_write
10
+
11
+
12
+ class SpecIngestError(Exception):
13
+ """Raised when ingesting a SPEC fails."""
14
+
15
+
16
+ def _extract_section(text: str, tag: str) -> Optional[str]:
17
+ pattern = re.compile(rf"<{tag}>\s*(.*?)\s*</{tag}>", re.DOTALL | re.IGNORECASE)
18
+ match = pattern.search(text)
19
+ if not match:
20
+ return None
21
+ return match.group(1).strip()
22
+
23
+
24
+ def build_spec_ingest_prompt(spec: str, todo: str, progress: str, opinions: str) -> str:
25
+ return SPEC_INGEST_PROMPT.format(
26
+ spec=spec.strip(),
27
+ todo=todo.strip(),
28
+ progress=progress.strip(),
29
+ opinions=opinions.strip(),
30
+ )
31
+
32
+
33
+ def parse_spec_ingest_output(text: str) -> Dict[str, str]:
34
+ todo = _extract_section(text, "TODO")
35
+ progress = _extract_section(text, "PROGRESS")
36
+ opinions = _extract_section(text, "OPINIONS")
37
+ if not todo or not progress or not opinions:
38
+ raise SpecIngestError(
39
+ "Failed to parse ingest output; missing TODO/PROGRESS/OPINIONS sections"
40
+ )
41
+ return {"todo": todo, "progress": progress, "opinions": opinions}
42
+
43
+
44
+ def generate_docs_from_spec(
45
+ engine: Engine, spec_path: Optional[Path] = None
46
+ ) -> Dict[str, str]:
47
+ path = spec_path or engine.config.doc_path("spec")
48
+ if not path.exists():
49
+ raise SpecIngestError(f"SPEC not found at {path}")
50
+ spec_text = path.read_text(encoding="utf-8")
51
+ if not spec_text.strip():
52
+ raise SpecIngestError(f"SPEC at {path} is empty")
53
+
54
+ prompt = build_spec_ingest_prompt(
55
+ spec_text,
56
+ engine.docs.read_doc("todo"),
57
+ engine.docs.read_doc("progress"),
58
+ engine.docs.read_doc("opinions"),
59
+ )
60
+ cmd = build_codex_command(engine.config, prompt)
61
+ try:
62
+ result = subprocess.run(
63
+ cmd,
64
+ capture_output=True,
65
+ text=True,
66
+ cwd=str(engine.repo_root),
67
+ )
68
+ except FileNotFoundError as exc:
69
+ raise SpecIngestError(
70
+ f"Codex binary not found: {engine.config.codex_binary}"
71
+ ) from exc
72
+
73
+ if result.returncode != 0:
74
+ stderr = result.stderr.strip() if result.stderr else ""
75
+ stdout_tail = (result.stdout or "").strip()[-400:]
76
+ raise SpecIngestError(
77
+ f"Codex ingest failed (code {result.returncode}). {stderr or stdout_tail}"
78
+ )
79
+
80
+ return parse_spec_ingest_output(result.stdout or "")
81
+
82
+
83
+ def ensure_can_overwrite(engine: Engine, force: bool) -> None:
84
+ if force:
85
+ return
86
+ for key in ("todo", "progress", "opinions"):
87
+ existing = engine.docs.read_doc(key).strip()
88
+ if existing:
89
+ raise SpecIngestError(
90
+ "TODO/PROGRESS/OPINIONS already contain content; rerun with --force to overwrite"
91
+ )
92
+
93
+
94
+ def write_ingested_docs(
95
+ engine: Engine, docs: Dict[str, str], force: bool = False
96
+ ) -> None:
97
+ ensure_can_overwrite(engine, force)
98
+ for key, content in docs.items():
99
+ target = engine.config.doc_path(key)
100
+ text = content if content.endswith("\n") else content + "\n"
101
+ atomic_write(target, text)
102
+
103
+
104
+ def clear_work_docs(engine: Engine) -> Dict[str, str]:
105
+ defaults = {
106
+ "todo": "# TODO\n\n",
107
+ "progress": "# Progress\n\n",
108
+ "opinions": "# Opinions\n\n",
109
+ }
110
+ for key, content in defaults.items():
111
+ atomic_write(engine.config.doc_path(key), content)
112
+ # Read back to reflect actual on-disk content.
113
+ return {k: engine.docs.read_doc(k) for k in defaults.keys()}
@@ -0,0 +1,95 @@
1
+ import { detectContext, REPO_ID, HUB_BASE } from "./env.js";
2
+ import { initHub } from "./hub.js";
3
+ import { initTabs, registerTab } from "./tabs.js";
4
+ import { initDashboard } from "./dashboard.js";
5
+ import { initDocs } from "./docs.js";
6
+ import { initLogs } from "./logs.js";
7
+ import { initTerminal } from "./terminal.js";
8
+ import { loadState } from "./state.js";
9
+ import { initGitHub } from "./github.js";
10
+ import { initMobileCompact } from "./mobileCompact.js";
11
+ import { subscribe } from "./bus.js";
12
+
13
+ function initRepoShell() {
14
+ // If this is a repo under a hub, show back button and repo name
15
+ if (REPO_ID) {
16
+ const navBar = document.querySelector(".nav-bar");
17
+ if (navBar) {
18
+ const backBtn = document.createElement("a");
19
+ backBtn.href = HUB_BASE || "/";
20
+ backBtn.className = "hub-back-btn";
21
+ backBtn.textContent = "← Hub";
22
+ backBtn.title = "Back to Hub";
23
+ navBar.insertBefore(backBtn, navBar.firstChild);
24
+ }
25
+ // Add repo name after the CAR brand (keep CAR visible)
26
+ const brand = document.querySelector(".nav-brand");
27
+ if (brand) {
28
+ const repoName = document.createElement("span");
29
+ repoName.className = "nav-repo-name";
30
+ repoName.textContent = REPO_ID;
31
+ brand.insertAdjacentElement("afterend", repoName);
32
+ }
33
+ }
34
+
35
+ registerTab("dashboard", "Dashboard");
36
+ registerTab("docs", "Docs");
37
+ registerTab("logs", "Logs");
38
+ registerTab("terminal", "Terminal");
39
+
40
+ const initializedTabs = new Set();
41
+ const lazyInit = (tabId) => {
42
+ if (initializedTabs.has(tabId)) return;
43
+ if (tabId === "docs") {
44
+ initDocs();
45
+ } else if (tabId === "logs") {
46
+ initLogs();
47
+ }
48
+ initializedTabs.add(tabId);
49
+ };
50
+
51
+ subscribe("tab:change", (tabId) => {
52
+ if (tabId === "terminal") {
53
+ initTerminal();
54
+ }
55
+ lazyInit(tabId);
56
+ });
57
+
58
+ initTabs();
59
+ const activePanel = document.querySelector(".panel.active");
60
+ if (activePanel?.id) {
61
+ lazyInit(activePanel.id);
62
+ }
63
+ const terminalPanel = document.getElementById("terminal");
64
+ terminalPanel?.addEventListener(
65
+ "pointerdown",
66
+ () => {
67
+ lazyInit("terminal");
68
+ },
69
+ { once: true }
70
+ );
71
+ initDashboard();
72
+ initGitHub();
73
+ initMobileCompact();
74
+
75
+ loadState();
76
+ }
77
+
78
+ async function bootstrap() {
79
+ const { mode } = await detectContext();
80
+ const hubShell = document.getElementById("hub-shell");
81
+ const repoShell = document.getElementById("repo-shell");
82
+
83
+ if (mode === "hub") {
84
+ if (hubShell) hubShell.classList.remove("hidden");
85
+ if (repoShell) repoShell.classList.add("hidden");
86
+ initHub();
87
+ return;
88
+ }
89
+
90
+ if (repoShell) repoShell.classList.remove("hidden");
91
+ if (hubShell) hubShell.classList.add("hidden");
92
+ initRepoShell();
93
+ }
94
+
95
+ bootstrap();
@@ -0,0 +1,209 @@
1
+ /**
2
+ * Auto-refresh utility for managing periodic data fetching.
3
+ *
4
+ * Features:
5
+ * - Pauses when page is hidden (respects Page Visibility API)
6
+ * - Only refreshes active tab's components
7
+ * - Configurable intervals per component
8
+ * - Immediate refresh on tab activation
9
+ * - Debounces rapid activations
10
+ */
11
+
12
+ import { subscribe } from "./bus.js";
13
+ import { CONSTANTS } from "./constants.js";
14
+
15
+ // Track registered refreshers: { id: { callback, interval, tabId, timerId, lastRefresh } }
16
+ const refreshers = new Map();
17
+
18
+ // Track current active tab
19
+ let activeTab = null;
20
+
21
+ // Track page visibility
22
+ let pageVisible = true;
23
+
24
+ /**
25
+ * Register a component for auto-refresh.
26
+ *
27
+ * @param {string} id - Unique identifier for this refresher
28
+ * @param {Object} options
29
+ * @param {Function} options.callback - Async function to call for refresh
30
+ * @param {string} options.tabId - Tab this refresher belongs to (null for global)
31
+ * @param {number} [options.interval] - Refresh interval in ms (default: AUTO_REFRESH_INTERVAL)
32
+ * @param {boolean} [options.refreshOnActivation=true] - Refresh when tab becomes active
33
+ * @param {boolean} [options.immediate=false] - Refresh immediately on registration
34
+ * @returns {Function} Unregister function
35
+ */
36
+ export function registerAutoRefresh(id, options) {
37
+ const {
38
+ callback,
39
+ tabId = null,
40
+ interval = CONSTANTS.UI.AUTO_REFRESH_INTERVAL,
41
+ refreshOnActivation = true,
42
+ immediate = false,
43
+ } = options;
44
+
45
+ const refresher = {
46
+ callback,
47
+ tabId,
48
+ interval,
49
+ refreshOnActivation,
50
+ timerId: null,
51
+ lastRefresh: 0,
52
+ isRefreshing: false,
53
+ };
54
+
55
+ refreshers.set(id, refresher);
56
+
57
+ // Start timer if applicable
58
+ maybeStartTimer(id, refresher);
59
+
60
+ // Immediate refresh if requested
61
+ if (immediate) {
62
+ doRefresh(id, refresher);
63
+ }
64
+
65
+ return () => unregisterAutoRefresh(id);
66
+ }
67
+
68
+ /**
69
+ * Unregister a refresher.
70
+ */
71
+ export function unregisterAutoRefresh(id) {
72
+ const refresher = refreshers.get(id);
73
+ if (refresher) {
74
+ if (refresher.timerId) {
75
+ clearInterval(refresher.timerId);
76
+ }
77
+ refreshers.delete(id);
78
+ }
79
+ }
80
+
81
+ /**
82
+ * Trigger an immediate refresh for a specific refresher.
83
+ */
84
+ export function triggerRefresh(id) {
85
+ const refresher = refreshers.get(id);
86
+ if (refresher) {
87
+ doRefresh(id, refresher);
88
+ }
89
+ }
90
+
91
+ /**
92
+ * Check if conditions allow refresh for this refresher.
93
+ */
94
+ function canRefresh(refresher) {
95
+ // Don't refresh if page is hidden
96
+ if (!pageVisible) return false;
97
+
98
+ // Don't refresh if already refreshing
99
+ if (refresher.isRefreshing) return false;
100
+
101
+ // If refresher is tab-specific, only refresh when that tab is active
102
+ if (refresher.tabId && refresher.tabId !== activeTab) return false;
103
+
104
+ return true;
105
+ }
106
+
107
+ /**
108
+ * Perform the actual refresh.
109
+ */
110
+ async function doRefresh(id, refresher) {
111
+ if (!canRefresh(refresher)) return;
112
+
113
+ refresher.isRefreshing = true;
114
+ refresher.lastRefresh = Date.now();
115
+
116
+ try {
117
+ await refresher.callback();
118
+ } catch (err) {
119
+ console.error(`Auto-refresh error for '${id}':`, err);
120
+ } finally {
121
+ refresher.isRefreshing = false;
122
+ }
123
+ }
124
+
125
+ /**
126
+ * Start or restart the interval timer for a refresher.
127
+ */
128
+ function maybeStartTimer(id, refresher) {
129
+ // Clear existing timer
130
+ if (refresher.timerId) {
131
+ clearInterval(refresher.timerId);
132
+ refresher.timerId = null;
133
+ }
134
+
135
+ // Only start timer if page is visible and tab is active (or global)
136
+ if (!pageVisible) return;
137
+ if (refresher.tabId && refresher.tabId !== activeTab) return;
138
+
139
+ refresher.timerId = setInterval(() => {
140
+ doRefresh(id, refresher);
141
+ }, refresher.interval);
142
+ }
143
+
144
+ /**
145
+ * Handle tab change - refresh components on the new tab and manage timers.
146
+ */
147
+ function handleTabChange(tabId) {
148
+ activeTab = tabId;
149
+
150
+ refreshers.forEach((refresher, id) => {
151
+ // Stop timers for inactive tabs
152
+ if (refresher.tabId && refresher.tabId !== tabId) {
153
+ if (refresher.timerId) {
154
+ clearInterval(refresher.timerId);
155
+ refresher.timerId = null;
156
+ }
157
+ return;
158
+ }
159
+
160
+ // Start/restart timers for active tab
161
+ maybeStartTimer(id, refresher);
162
+
163
+ // Refresh on activation if enabled (with debounce)
164
+ if (refresher.refreshOnActivation) {
165
+ const timeSinceLastRefresh = Date.now() - refresher.lastRefresh;
166
+ // Only refresh if it's been at least 5 seconds since last refresh
167
+ if (timeSinceLastRefresh > 5000) {
168
+ doRefresh(id, refresher);
169
+ }
170
+ }
171
+ });
172
+ }
173
+
174
+ /**
175
+ * Handle page visibility change.
176
+ */
177
+ function handleVisibilityChange() {
178
+ pageVisible = !document.hidden;
179
+
180
+ if (pageVisible) {
181
+ // Page became visible - restart timers and optionally refresh
182
+ refreshers.forEach((refresher, id) => {
183
+ maybeStartTimer(id, refresher);
184
+
185
+ // Refresh if it's been a while since last refresh
186
+ const timeSinceLastRefresh = Date.now() - refresher.lastRefresh;
187
+ if (timeSinceLastRefresh > refresher.interval) {
188
+ doRefresh(id, refresher);
189
+ }
190
+ });
191
+ } else {
192
+ // Page hidden - stop all timers
193
+ refreshers.forEach((refresher) => {
194
+ if (refresher.timerId) {
195
+ clearInterval(refresher.timerId);
196
+ refresher.timerId = null;
197
+ }
198
+ });
199
+ }
200
+ }
201
+
202
+ // Initialize event listeners
203
+ subscribe("tab:change", handleTabChange);
204
+
205
+ // Only set up visibility listener if in browser environment
206
+ if (typeof document !== "undefined" && document.addEventListener) {
207
+ document.addEventListener("visibilitychange", handleVisibilityChange);
208
+ }
209
+
@@ -0,0 +1,105 @@
1
+ (() => {
2
+ const AUTH_TOKEN_KEY = "car_auth_token";
3
+ const url = new URL(window.location.href);
4
+ const params = url.searchParams;
5
+ let token = params.get("token");
6
+ if (token) {
7
+ window.__CAR_AUTH_TOKEN = token;
8
+ try {
9
+ sessionStorage.setItem(AUTH_TOKEN_KEY, token);
10
+ } catch (_err) {
11
+ // Ignore storage errors; token can still be used for this load.
12
+ }
13
+ params.delete("token");
14
+ if (typeof history !== "undefined" && history.replaceState) {
15
+ history.replaceState(null, "", url.toString());
16
+ }
17
+ } else {
18
+ try {
19
+ token = sessionStorage.getItem(AUTH_TOKEN_KEY);
20
+ if (token) window.__CAR_AUTH_TOKEN = token;
21
+ } catch (_err) {
22
+ // Ignore storage errors; token may still be in memory.
23
+ }
24
+ }
25
+
26
+ const normalizeBase = (base) => {
27
+ if (!base || base === "/") return "";
28
+ let normalized = base.startsWith("/") ? base : `/${base}`;
29
+ while (normalized.endsWith("/") && normalized.length > 1) {
30
+ normalized = normalized.slice(0, -1);
31
+ }
32
+ return normalized === "/" ? "" : normalized;
33
+ };
34
+ const detectBasePrefix = (path) => {
35
+ const prefixes = ["/repos/", "/hub/", "/api/", "/static/", "/cat/"];
36
+ let idx = -1;
37
+ for (const prefix of prefixes) {
38
+ const found = path.indexOf(prefix);
39
+ if (found === 0) return "";
40
+ if (found > 0 && (idx === -1 || found < idx)) idx = found;
41
+ }
42
+ if (idx > 0) return normalizeBase(path.slice(0, idx));
43
+ const parts = path.split("/").filter(Boolean);
44
+ if (parts.length) return normalizeBase(`/${parts[0]}`);
45
+ return "";
46
+ };
47
+
48
+ const pathname = window.location.pathname || "/";
49
+ const basePrefix = detectBasePrefix(pathname);
50
+ const repoMatch = pathname.match(/\/repos\/([^/]+)/);
51
+ const repoId = repoMatch && repoMatch[1] ? repoMatch[1] : null;
52
+
53
+ window.__CAR_BASE_PREFIX = basePrefix || "";
54
+ window.__CAR_REPO_ID = repoId;
55
+ window.__CAR_BASE_PATH = repoId ? `${basePrefix}/repos/${repoId}` : basePrefix;
56
+ window.__AUTH_TOKEN_PRESENT = Boolean(window.__CAR_AUTH_TOKEN);
57
+
58
+ const base = basePrefix || "/";
59
+ const baseTag = document.createElement("base");
60
+ baseTag.href = base.endsWith("/") ? base : `${base}/`;
61
+ document.head.appendChild(baseTag);
62
+
63
+ const readAssetVersion = () => {
64
+ const queryVersion = new URLSearchParams(window.location.search).get("v");
65
+ if (queryVersion) return queryVersion;
66
+ const bootstrapScript =
67
+ document.currentScript ||
68
+ document.querySelector('script[data-car-bootstrap]');
69
+ if (bootstrapScript && bootstrapScript.src) {
70
+ try {
71
+ const scriptUrl = new URL(bootstrapScript.src, window.location.href);
72
+ return scriptUrl.searchParams.get("v") || "";
73
+ } catch (_err) {
74
+ return "";
75
+ }
76
+ }
77
+ return "";
78
+ };
79
+
80
+ const version = readAssetVersion();
81
+ const suffix = version ? `?v=${encodeURIComponent(version)}` : "";
82
+ window.__assetSuffix = suffix;
83
+
84
+ const addStylesheet = (href) => {
85
+ const link = document.createElement("link");
86
+ link.rel = "stylesheet";
87
+ link.href = `${href}${suffix}`;
88
+ document.head.appendChild(link);
89
+ };
90
+
91
+ addStylesheet("static/styles.css");
92
+ addStylesheet("static/vendor/xterm.css");
93
+
94
+ fetch("api/version", { cache: "no-store" })
95
+ .then((res) => (res.ok ? res.json() : null))
96
+ .then((data) => {
97
+ const assetVersion = data && data.asset_version;
98
+ if (assetVersion && assetVersion !== version) {
99
+ const next = new URL(window.location.href);
100
+ next.searchParams.set("v", assetVersion);
101
+ window.location.replace(next.toString());
102
+ }
103
+ })
104
+ .catch(() => {});
105
+ })();
@@ -0,0 +1,23 @@
1
+ const listeners = new Map();
2
+
3
+ export function subscribe(event, handler) {
4
+ if (!listeners.has(event)) {
5
+ listeners.set(event, new Set());
6
+ }
7
+ const set = listeners.get(event);
8
+ set.add(handler);
9
+ return () => set.delete(handler);
10
+ }
11
+
12
+ export function publish(event, payload) {
13
+ const set = listeners.get(event);
14
+ if (!set) return;
15
+ for (const handler of Array.from(set)) {
16
+ try {
17
+ handler(payload);
18
+ } catch (err) {
19
+ // Keep a misbehaving subscriber from breaking others
20
+ console.error(`Error in '${event}' subscriber`, err);
21
+ }
22
+ }
23
+ }
@@ -0,0 +1,52 @@
1
+ import { BASE_PATH, REPO_ID } from "./env.js";
2
+
3
+ function cachePrefix() {
4
+ const scope = REPO_ID ? `repo:${REPO_ID}` : `base:${BASE_PATH || ""}`;
5
+ return `car:${encodeURIComponent(scope)}:`;
6
+ }
7
+
8
+ function scopedKey(key) {
9
+ return cachePrefix() + key;
10
+ }
11
+
12
+ /**
13
+ * Save data to localStorage with error handling.
14
+ * @param {string} key - The key to store the data under (will be scoped per repo/base path)
15
+ * @param {any} data - The data to store (will be JSON stringified)
16
+ */
17
+ export function saveToCache(key, data) {
18
+ try {
19
+ const json = JSON.stringify(data);
20
+ localStorage.setItem(scopedKey(key), json);
21
+ } catch (err) {
22
+ console.warn("Failed to save to cache", key, err);
23
+ }
24
+ }
25
+
26
+ /**
27
+ * Load data from localStorage with error handling.
28
+ * @param {string} key - The key to retrieve data from (will be scoped per repo/base path)
29
+ * @returns {any|null} The parsed data, or null if not found or invalid
30
+ */
31
+ export function loadFromCache(key) {
32
+ try {
33
+ const json = localStorage.getItem(scopedKey(key));
34
+ if (!json) return null;
35
+ return JSON.parse(json);
36
+ } catch (err) {
37
+ console.warn("Failed to load from cache", key, err);
38
+ return null;
39
+ }
40
+ }
41
+
42
+ /**
43
+ * Remove data from localStorage.
44
+ * @param {string} key - The key to remove (will be scoped per repo/base path)
45
+ */
46
+ export function clearCache(key) {
47
+ try {
48
+ localStorage.removeItem(scopedKey(key));
49
+ } catch (err) {
50
+ console.warn("Failed to clear cache", key, err);
51
+ }
52
+ }
@@ -0,0 +1,48 @@
1
+ export const CONSTANTS = {
2
+ UI: {
3
+ TOAST_DURATION: 2200,
4
+ POLLING_INTERVAL: 15000,
5
+ LOG_SCROLL_THRESHOLD: 50,
6
+ MAX_LOG_LINES_IN_DOM: 2000, // Limit DOM nodes for performance
7
+ MAX_LOG_LINES_IN_MEMORY: 10000, // Cap memory usage for long-running logs
8
+ LOG_PAGE_SIZE: 500,
9
+ // Auto-refresh intervals (in ms)
10
+ AUTO_REFRESH_INTERVAL: 30000, // 30 seconds for periodic refresh
11
+ AUTO_REFRESH_USAGE_INTERVAL: 60000, // 60 seconds for usage data (less critical)
12
+ },
13
+ THEME: {
14
+ XTERM: {
15
+ background: '#0a0c12',
16
+ foreground: '#e5ecff',
17
+ cursor: '#6cf5d8',
18
+ selectionBackground: 'rgba(108, 245, 216, 0.3)',
19
+ black: '#000000',
20
+ red: '#ff5566',
21
+ green: '#6cf5d8',
22
+ yellow: '#f1fa8c',
23
+ blue: '#6ca8ff',
24
+ magenta: '#bd93f9',
25
+ cyan: '#8be9fd',
26
+ white: '#e5ecff',
27
+ brightBlack: '#6272a4',
28
+ brightRed: '#ff6e6e',
29
+ brightGreen: '#69ff94',
30
+ brightYellow: '#ffffa5',
31
+ brightBlue: '#d6acff',
32
+ brightMagenta: '#ff92df',
33
+ brightCyan: '#a4ffff',
34
+ brightWhite: '#ffffff',
35
+ }
36
+ },
37
+ PROMPTS: {
38
+ VOICE_TRANSCRIPT_DISCLAIMER:
39
+ "Note: transcribed from user voice. If confusing or possibly inaccurate and you cannot infer the intention please clarify before proceeding.",
40
+ },
41
+ API: {
42
+ STATE_ENDPOINT: "/api/state",
43
+ LOGS_ENDPOINT: "/api/logs",
44
+ DOCS_ENDPOINT: "/api/docs",
45
+ TERMINAL_ENDPOINT: "/api/terminal",
46
+ TERMINAL_IMAGE_ENDPOINT: "/api/terminal/image",
47
+ }
48
+ };