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,300 @@
1
+ import {
2
+ isMobileViewport,
3
+ setMobileChromeHidden,
4
+ setMobileComposeFixed,
5
+ } from "./utils.js";
6
+ import { subscribe } from "./bus.js";
7
+ import { getTerminalManager } from "./terminal.js";
8
+
9
+ const COMPOSE_INPUT_SELECTOR = "#doc-chat-input, #terminal-textarea";
10
+ const SEND_BUTTON_SELECTOR = "#doc-chat-send, #terminal-text-send";
11
+ let baseViewportHeight = window.innerHeight;
12
+ let viewportPoll = null;
13
+
14
+ function ensureComposeEnterHint() {
15
+ const inputs = Array.from(document.querySelectorAll(COMPOSE_INPUT_SELECTOR));
16
+ for (const input of inputs) {
17
+ if (!(input instanceof HTMLTextAreaElement)) continue;
18
+ input.enterKeyHint = "enter";
19
+ input.setAttribute("enterkeyhint", "enter");
20
+ }
21
+ }
22
+
23
+ function isVisible(el) {
24
+ if (!el) return false;
25
+ return Boolean(el.offsetParent || el.getClientRects().length);
26
+ }
27
+
28
+ function isComposeFocused() {
29
+ const el = document.activeElement;
30
+ if (!el || !(el instanceof HTMLElement)) return false;
31
+ return el.matches(COMPOSE_INPUT_SELECTOR);
32
+ }
33
+
34
+ function hasComposeDraft() {
35
+ const inputs = Array.from(document.querySelectorAll(COMPOSE_INPUT_SELECTOR));
36
+ return inputs.some((input) => {
37
+ if (!(input instanceof HTMLTextAreaElement)) return false;
38
+ if (!isVisible(input)) return false;
39
+ return Boolean(input.value && input.value.trim());
40
+ });
41
+ }
42
+
43
+ function updateViewportInset() {
44
+ const viewportHeight = window.innerHeight;
45
+ if (viewportHeight > baseViewportHeight) {
46
+ baseViewportHeight = viewportHeight;
47
+ }
48
+ let bottom = 0;
49
+ let top = 0;
50
+ const vv = window.visualViewport;
51
+ if (vv) {
52
+ const layoutHeight = document.documentElement?.clientHeight || viewportHeight;
53
+ const vvOffset = Math.max(0, vv.offsetTop);
54
+ top = vvOffset;
55
+ bottom = Math.max(0, layoutHeight - (vv.height + vvOffset));
56
+ }
57
+ const keyboardFallback = vv ? 0 : Math.max(0, baseViewportHeight - viewportHeight);
58
+ const inset = bottom || keyboardFallback;
59
+ document.documentElement.style.setProperty("--vv-bottom", `${inset}px`);
60
+ document.documentElement.style.setProperty("--vv-top", `${top}px`);
61
+ }
62
+
63
+ function isTerminalComposeOpen() {
64
+ const panel = document.getElementById("terminal");
65
+ const input = document.getElementById("terminal-text-input");
66
+ if (!panel || !input) return false;
67
+ if (!panel.classList.contains("active")) return false;
68
+ if (input.classList.contains("hidden")) return false;
69
+ return true;
70
+ }
71
+
72
+ function updateComposeFixed() {
73
+ if (!isMobileViewport()) {
74
+ setMobileComposeFixed(false);
75
+ return;
76
+ }
77
+ const enabled =
78
+ isComposeFocused() || hasComposeDraft() || isTerminalComposeOpen();
79
+ setMobileComposeFixed(enabled);
80
+ // Always update viewport inset when compose state changes so the composer
81
+ // is positioned correctly above the keyboard even when not focused.
82
+ if (enabled) {
83
+ updateViewportInset();
84
+ updateMobileControlsOffset();
85
+ }
86
+ updateDocComposeOffset();
87
+ }
88
+
89
+ /**
90
+ * Measure the actual height of the terminal text input panel and set a CSS
91
+ * variable so the mobile controls can be positioned exactly above it.
92
+ */
93
+ function updateMobileControlsOffset() {
94
+ const textInput = document.getElementById("terminal-text-input");
95
+ const mobileControls = document.getElementById("terminal-mobile-controls");
96
+ if (!textInput || !mobileControls) return;
97
+
98
+ // Get the actual rendered height of the text input panel
99
+ const textInputHeight = textInput.offsetHeight || 0;
100
+ // Add a small gap between controls and text input
101
+ const offset = textInputHeight + 4;
102
+ document.documentElement.style.setProperty(
103
+ "--compose-input-height",
104
+ `${offset}px`
105
+ );
106
+
107
+ // Also set the total height for padding-bottom calculation
108
+ const controlsHeight = mobileControls.offsetHeight || 0;
109
+ const totalHeight = textInputHeight + controlsHeight + 8;
110
+ document.documentElement.style.setProperty(
111
+ "--compose-total-height",
112
+ `${totalHeight}px`
113
+ );
114
+ }
115
+
116
+ function updateDocComposeOffset() {
117
+ const composePanel = document.querySelector("#docs .doc-chat-panel");
118
+ if (!composePanel || !isVisible(composePanel)) return;
119
+ const composeHeight = composePanel.offsetHeight || 0;
120
+ if (!composeHeight) return;
121
+ const offset = composeHeight + 8;
122
+ document.documentElement.style.setProperty(
123
+ "--doc-compose-height",
124
+ `${offset}px`
125
+ );
126
+ }
127
+
128
+ function isTerminalTextarea(el) {
129
+ return Boolean(
130
+ el && el instanceof HTMLElement && el.id === "terminal-textarea"
131
+ );
132
+ }
133
+
134
+ export function initMobileCompact() {
135
+ setMobileChromeHidden(false);
136
+ ensureComposeEnterHint();
137
+
138
+ const maybeHide = () => {
139
+ if (!isMobileViewport()) return;
140
+ if (!isComposeFocused()) return;
141
+ setMobileChromeHidden(true);
142
+ updateDocComposeOffset();
143
+ };
144
+
145
+ const show = () => {
146
+ if (!isMobileViewport()) return;
147
+ setMobileChromeHidden(false);
148
+ updateComposeFixed();
149
+ // Force a visual update
150
+ document.documentElement.style.display = "none";
151
+ document.documentElement.offsetHeight; // trigger reflow
152
+ document.documentElement.style.display = "";
153
+ };
154
+
155
+ window.addEventListener("scroll", maybeHide, { passive: true });
156
+ document.addEventListener("scroll", maybeHide, {
157
+ passive: true,
158
+ capture: true,
159
+ });
160
+ document.addEventListener(
161
+ "touchmove",
162
+ (e) => {
163
+ const target = e.target;
164
+ if (
165
+ target instanceof HTMLElement &&
166
+ target.closest(COMPOSE_INPUT_SELECTOR)
167
+ ) {
168
+ return;
169
+ }
170
+ maybeHide();
171
+ },
172
+ { passive: true }
173
+ );
174
+ document.addEventListener("wheel", maybeHide, { passive: true });
175
+
176
+ document.addEventListener(
177
+ "focusin",
178
+ (e) => {
179
+ if (!isMobileViewport()) return;
180
+ const target = e.target;
181
+ if (!(target instanceof HTMLElement)) return;
182
+ if (!target.matches(COMPOSE_INPUT_SELECTOR)) return;
183
+ ensureComposeEnterHint();
184
+ updateViewportInset();
185
+ updateComposeFixed();
186
+ setMobileChromeHidden(false);
187
+ updateDocComposeOffset();
188
+
189
+ // Start polling for viewport changes (keyboard animation)
190
+ if (viewportPoll) clearInterval(viewportPoll);
191
+ viewportPoll = setInterval(updateViewportInset, 100);
192
+
193
+ if (isTerminalTextarea(target)) {
194
+ getTerminalManager()?.scheduleResizeAfterLayout?.();
195
+ }
196
+ },
197
+ true
198
+ );
199
+
200
+ document.addEventListener(
201
+ "focusout",
202
+ (e) => {
203
+ if (!isMobileViewport()) return;
204
+ const target = e.target;
205
+ if (!(target instanceof HTMLElement)) return;
206
+ if (!target.matches(COMPOSE_INPUT_SELECTOR)) return;
207
+
208
+ if (viewportPoll) {
209
+ clearInterval(viewportPoll);
210
+ viewportPoll = null;
211
+ }
212
+
213
+ setTimeout(() => {
214
+ // Always update viewport inset - keyboard may still be visible or transitioning
215
+ updateViewportInset();
216
+ if (isComposeFocused()) return;
217
+ show();
218
+ getTerminalManager()?.scheduleResizeAfterLayout?.();
219
+ }, 50);
220
+ },
221
+ true
222
+ );
223
+
224
+ document.addEventListener(
225
+ "click",
226
+ (e) => {
227
+ if (!isMobileViewport()) return;
228
+ const target = e.target;
229
+ if (!(target instanceof HTMLElement)) return;
230
+ if (!target.closest(SEND_BUTTON_SELECTOR)) return;
231
+ // Defer show() to allow the click event to reach the button listener (bubbling phase)
232
+ // before potentially forcing a reflow that cancels the event.
233
+ requestAnimationFrame(() => show());
234
+ },
235
+ true
236
+ );
237
+
238
+ document.addEventListener(
239
+ "input",
240
+ (e) => {
241
+ const target = e.target;
242
+ if (!(target instanceof HTMLElement)) return;
243
+ if (!target.matches(COMPOSE_INPUT_SELECTOR)) return;
244
+ updateComposeFixed();
245
+ },
246
+ true
247
+ );
248
+
249
+ if (window.visualViewport) {
250
+ window.visualViewport.addEventListener("resize", updateViewportInset);
251
+ window.visualViewport.addEventListener("scroll", updateViewportInset);
252
+ updateViewportInset();
253
+ }
254
+
255
+ // Update viewport inset on any focus change when terminal compose is open.
256
+ // This ensures the composer stays positioned correctly above the keyboard
257
+ // even when focus moves to buttons (like mobile control keys).
258
+ document.addEventListener(
259
+ "focusin",
260
+ () => {
261
+ if (!isMobileViewport()) return;
262
+ if (isTerminalComposeOpen()) {
263
+ updateViewportInset();
264
+ }
265
+ },
266
+ true
267
+ );
268
+
269
+ window.addEventListener(
270
+ "resize",
271
+ () => {
272
+ if (!isMobileViewport()) {
273
+ setMobileChromeHidden(false);
274
+ }
275
+ updateComposeFixed();
276
+ },
277
+ { passive: true }
278
+ );
279
+
280
+ subscribe("tab:change", () => {
281
+ show();
282
+ });
283
+
284
+ subscribe("terminal:compose", () => {
285
+ updateViewportInset();
286
+ updateComposeFixed();
287
+ // Delay to ensure DOM has updated with new panel visibility
288
+ requestAnimationFrame(() => {
289
+ updateMobileControlsOffset();
290
+ updateDocComposeOffset();
291
+ });
292
+ });
293
+
294
+ updateComposeFixed();
295
+ // Initial measurement after layout
296
+ requestAnimationFrame(() => {
297
+ updateMobileControlsOffset();
298
+ updateDocComposeOffset();
299
+ });
300
+ }
@@ -0,0 +1,116 @@
1
+ import { api, flash } from "./utils.js";
2
+
3
+ const UI = {
4
+ status: document.getElementById("snapshot-status"),
5
+ content: document.getElementById("snapshot-content"),
6
+ generate: document.getElementById("snapshot-generate"),
7
+ update: document.getElementById("snapshot-update"),
8
+ regenerate: document.getElementById("snapshot-regenerate"),
9
+ copy: document.getElementById("snapshot-copy"),
10
+ refresh: document.getElementById("snapshot-refresh"),
11
+ };
12
+
13
+ let latest = { exists: false, content: "", state: {} };
14
+ let busy = false;
15
+
16
+ function setBusy(on) {
17
+ busy = on;
18
+ const disabled = !!on;
19
+ for (const btn of [UI.generate, UI.update, UI.regenerate, UI.copy, UI.refresh]) {
20
+ if (!btn) continue;
21
+ btn.disabled = disabled;
22
+ }
23
+ if (UI.status) UI.status.textContent = on ? "Working…" : "";
24
+ }
25
+
26
+ function render() {
27
+ if (!UI.content) return;
28
+ UI.content.value = latest.content || "";
29
+ // Single default behavior: one "Run snapshot" action, regardless of whether a
30
+ // snapshot already exists.
31
+ if (UI.generate) UI.generate.classList.toggle("hidden", false);
32
+ if (UI.update) UI.update.classList.toggle("hidden", true);
33
+ if (UI.regenerate) UI.regenerate.classList.toggle("hidden", true);
34
+ if (UI.copy) UI.copy.disabled = busy || !(latest.content || "").trim();
35
+ }
36
+
37
+ async function loadSnapshot({ notify = false } = {}) {
38
+ if (busy) return;
39
+ try {
40
+ setBusy(true);
41
+ const data = await api("/api/snapshot");
42
+ latest = {
43
+ exists: !!data?.exists,
44
+ content: data?.content || "",
45
+ state: data?.state || {},
46
+ };
47
+ render();
48
+ if (notify) flash(latest.exists ? "Snapshot loaded" : "No snapshot yet");
49
+ } catch (err) {
50
+ flash(err?.message || "Failed to load snapshot");
51
+ } finally {
52
+ setBusy(false);
53
+ }
54
+ }
55
+
56
+ async function runSnapshot() {
57
+ if (busy) return;
58
+ try {
59
+ setBusy(true);
60
+ const data = await api("/api/snapshot", {
61
+ method: "POST",
62
+ body: {},
63
+ });
64
+ latest = {
65
+ exists: true,
66
+ content: data?.content || "",
67
+ state: data?.state || {},
68
+ };
69
+ render();
70
+ flash("Snapshot generated");
71
+ } catch (err) {
72
+ flash(err?.message || "Snapshot generation failed");
73
+ } finally {
74
+ setBusy(false);
75
+ }
76
+ }
77
+
78
+ async function copyToClipboard() {
79
+ const text = UI.content?.value || "";
80
+ if (!text.trim()) return;
81
+ try {
82
+ if (navigator.clipboard?.writeText) {
83
+ await navigator.clipboard.writeText(text);
84
+ flash("Copied to clipboard");
85
+ return;
86
+ }
87
+ } catch {
88
+ // fall through
89
+ }
90
+ try {
91
+ UI.content.focus();
92
+ UI.content.select();
93
+ const ok = document.execCommand("copy");
94
+ flash(ok ? "Copied to clipboard" : "Copy failed");
95
+ } catch {
96
+ flash("Copy failed");
97
+ } finally {
98
+ try {
99
+ UI.content.setSelectionRange(0, 0);
100
+ } catch {
101
+ // ignore
102
+ }
103
+ }
104
+ }
105
+
106
+ export function initSnapshot() {
107
+ if (!UI.content) return;
108
+
109
+ UI.generate?.addEventListener("click", () => runSnapshot());
110
+ UI.update?.addEventListener("click", () => runSnapshot());
111
+ UI.regenerate?.addEventListener("click", () => runSnapshot());
112
+ UI.copy?.addEventListener("click", copyToClipboard);
113
+ UI.refresh?.addEventListener("click", () => loadSnapshot({ notify: true }));
114
+
115
+ loadSnapshot().catch(() => {});
116
+ }
@@ -0,0 +1,87 @@
1
+ import { api, confirmModal, flash, streamEvents } from "./utils.js";
2
+ import { publish } from "./bus.js";
3
+ import { CONSTANTS } from "./constants.js";
4
+
5
+ let stopStateStream = null;
6
+
7
+ export async function loadState({ notify = true } = {}) {
8
+ try {
9
+ const data = await api(CONSTANTS.API.STATE_ENDPOINT);
10
+ publish("state:update", data);
11
+ return data;
12
+ } catch (err) {
13
+ if (notify) flash(err.message);
14
+ publish("state:error", err);
15
+ throw err;
16
+ }
17
+ }
18
+
19
+ export function startStatePolling() {
20
+ if (stopStateStream) return stopStateStream;
21
+
22
+ let active = true;
23
+ let cancelStream = null;
24
+
25
+ const connect = () => {
26
+ if (!active) return;
27
+ // Initial fetch to ensure immediate state
28
+ loadState({ notify: false }).catch(() => {});
29
+
30
+ cancelStream = streamEvents("/api/state/stream", {
31
+ onMessage: (data) => {
32
+ try {
33
+ const state = JSON.parse(data);
34
+ publish("state:update", state);
35
+ } catch (e) {
36
+ console.error("Bad state payload", e);
37
+ }
38
+ },
39
+ onFinish: () => {
40
+ if (active) {
41
+ // Reconnect after delay
42
+ setTimeout(connect, 2000);
43
+ }
44
+ },
45
+ });
46
+ };
47
+
48
+ connect();
49
+
50
+ stopStateStream = () => {
51
+ active = false;
52
+ if (cancelStream) cancelStream();
53
+ stopStateStream = null;
54
+ };
55
+ return stopStateStream;
56
+ }
57
+
58
+ async function runAction(path, body, successMessage) {
59
+ await api(path, { method: "POST", body });
60
+ if (successMessage) flash(successMessage);
61
+ await loadState({ notify: false });
62
+ }
63
+
64
+ export function startRun(once = false) {
65
+ return runAction("/api/run/start", { once }, once ? "Started one-off run" : "Runner starting");
66
+ }
67
+
68
+ export function stopRun() {
69
+ return runAction("/api/run/stop", null, "Stop signal sent");
70
+ }
71
+
72
+ export function resumeRun() {
73
+ return runAction("/api/run/resume", null, "Resume requested");
74
+ }
75
+
76
+ export async function killRun() {
77
+ const confirmed = await confirmModal(
78
+ "Kill the runner process? This stops it immediately and may leave partial state.",
79
+ { confirmText: "Kill runner", cancelText: "Cancel", danger: true }
80
+ );
81
+ if (!confirmed) return null;
82
+ return runAction("/api/run/kill", null, "Kill signal sent");
83
+ }
84
+
85
+ export function resetRunner() {
86
+ return runAction("/api/run/reset", null, "Runner reset complete");
87
+ }