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,690 @@
1
+ import { api, flash, streamEvents, getUrlParams, updateUrlParams } from "./utils.js";
2
+ import { publish, subscribe } from "./bus.js";
3
+ import { saveToCache, loadFromCache } from "./cache.js";
4
+ import { CONSTANTS } from "./constants.js";
5
+
6
+ const logRunIdInput = document.getElementById("log-run-id");
7
+ const logTailInput = document.getElementById("log-tail");
8
+ const toggleLogStreamButton = document.getElementById("toggle-log-stream");
9
+ const showTimestampToggle = document.getElementById("log-show-timestamp");
10
+ const showRunToggle = document.getElementById("log-show-run");
11
+ const jumpBottomButton = document.getElementById("log-jump-bottom");
12
+ const loadOlderButton = document.getElementById("log-load-older");
13
+ let stopLogStream = null;
14
+ let lastKnownRunId = null;
15
+ let rawLogLines = [];
16
+ let autoScrollEnabled = true;
17
+ let renderedStartIndex = 0;
18
+ let renderedEndIndex = 0;
19
+ let isViewingTail = true;
20
+ let renderState = null;
21
+ let logContexts = [];
22
+ let logContextState = { inPromptBlock: false, inDiffBlock: false };
23
+ // Matches doc-chat metadata lines (start/result) that we might want to hide for cleaner view
24
+ const DOC_CHAT_META_RE = /doc-chat id=[a-f0-9]+ (result=|exit_code=)/i;
25
+
26
+ // Log line classification patterns
27
+ const LINE_PATTERNS = {
28
+ // Run boundaries
29
+ runStart: /^=== run \d+ start/,
30
+ runEnd: /^=== run \d+ end/,
31
+
32
+ // Agent thinking/reasoning
33
+ thinking: /^thinking$/i,
34
+ thinkingContent: /^\*\*.+\*\*$/,
35
+ thinkingMultiline:
36
+ /^I'm (preparing|planning|considering|reviewing|analyzing|checking|looking|reading|searching)/i,
37
+
38
+ // Tool execution
39
+ execStart: /^exec$/i,
40
+ execCommand: /^\/bin\/(zsh|bash|sh)\s+-[a-z]+\s+['"]?.+in\s+\//i,
41
+ applyPatch: /^apply_patch\(/i,
42
+ fileUpdate: /^file update:?$/i,
43
+ fileModified: /^M\s+[\w./]/,
44
+
45
+ // Diff patterns - need context tracking to avoid false positives
46
+ // These patterns identify the START of a diff block
47
+ diffGitHeader: /^diff --git /,
48
+ diffFileHeader: /^(---|\+\+\+)\s+[ab]\//,
49
+ diffIndex: /^index [a-f0-9]+\.\.[a-f0-9]+/,
50
+ diffHunk: /^@@\s+-\d+,?\d*\s+\+\d+,?\d*\s+@@/,
51
+
52
+ // Prompt/context markers (verbose)
53
+ promptMarker:
54
+ /^<(SPEC|WORK_DOCS|TODO|PROGRESS|OPINIONS|TARGET_DOC|RECENT_RUN|SYSTEM|USER|ASSISTANT)>$/,
55
+ promptMarkerEnd:
56
+ /^<\/(SPEC|WORK_DOCS|TODO|PROGRESS|OPINIONS|TARGET_DOC|RECENT_RUN|SYSTEM|USER|ASSISTANT)>$/,
57
+
58
+ // System messages
59
+ mcpStartup: /^mcp startup:/i,
60
+ tokensUsed: /^tokens used/i,
61
+
62
+ // Agent summary/output (lines after tokens used)
63
+ agentOutput: /^Agent:\s*/i,
64
+
65
+ // Success/error indicators
66
+ success: /succeeded in \d+ms/i,
67
+ exitCode: /exited \d+ in \d+ms/i,
68
+
69
+ // Additional patterns for better classification
70
+ testOutput:
71
+ /^(={3,}\s*(test session|.*passed|.*failed)|PASSED|FAILED|ERROR)/i,
72
+ pythonTraceback: /^(Traceback \(most recent|File ".*", line \d+|.*Error:)/i,
73
+
74
+ // Markdown list items - explicitly NOT diff lines
75
+ markdownList: /^- (\[[ x]\]\s)?[A-Z]/,
76
+ };
77
+
78
+ // Determine the type of a log line
79
+ function classifyLine(line, context = {}) {
80
+ const stripped = line
81
+ .replace(/^\[[^\]]*]\s*/, "")
82
+ .replace(/^(run=\d+\s*)?(stdout|stderr):\s*/, "")
83
+ .replace(/^doc-chat id=[a-f0-9]+ stdout:\s*/i, "")
84
+ .trim();
85
+
86
+ // Run boundaries - highest priority (also resets diff context)
87
+ if (LINE_PATTERNS.runStart.test(stripped))
88
+ return { type: "run-start", priority: 1, resetDiff: true };
89
+ if (LINE_PATTERNS.runEnd.test(stripped))
90
+ return { type: "run-end", priority: 1, resetDiff: true };
91
+
92
+ // Agent output (summary) - also high priority as this is final output for user
93
+ if (LINE_PATTERNS.agentOutput.test(stripped))
94
+ return { type: "agent-output", priority: 1 };
95
+
96
+ // Thinking/reasoning
97
+ if (LINE_PATTERNS.thinking.test(stripped))
98
+ return { type: "thinking-label", priority: 2 };
99
+ if (LINE_PATTERNS.thinkingContent.test(stripped))
100
+ return { type: "thinking", priority: 2 };
101
+ if (LINE_PATTERNS.thinkingMultiline.test(stripped))
102
+ return { type: "thinking", priority: 2 };
103
+
104
+ // Tool execution
105
+ if (LINE_PATTERNS.execStart.test(stripped))
106
+ return { type: "exec-label", priority: 3 };
107
+ if (LINE_PATTERNS.execCommand.test(stripped))
108
+ return { type: "exec-command", priority: 3 };
109
+ if (LINE_PATTERNS.applyPatch.test(stripped))
110
+ return { type: "exec-command", priority: 3 };
111
+ if (LINE_PATTERNS.fileUpdate.test(stripped))
112
+ return { type: "file-update-label", priority: 3, startDiff: true };
113
+ if (LINE_PATTERNS.fileModified.test(stripped))
114
+ return { type: "file-modified", priority: 3 };
115
+
116
+ // Test output
117
+ if (LINE_PATTERNS.testOutput.test(stripped))
118
+ return { type: "test-output", priority: 3 };
119
+
120
+ // Error/traceback
121
+ if (LINE_PATTERNS.pythonTraceback.test(stripped))
122
+ return { type: "error-output", priority: 2 };
123
+
124
+ // Diff headers - mark start of diff context
125
+ if (LINE_PATTERNS.diffGitHeader.test(stripped))
126
+ return { type: "diff-header", priority: 4, startDiff: true };
127
+ if (LINE_PATTERNS.diffFileHeader.test(stripped))
128
+ return { type: "diff-header", priority: 4 };
129
+ if (LINE_PATTERNS.diffIndex.test(stripped))
130
+ return { type: "diff-header", priority: 4 };
131
+ if (LINE_PATTERNS.diffHunk.test(stripped))
132
+ return { type: "diff-hunk", priority: 4 };
133
+
134
+ // Diff add/del lines - ONLY if we're in diff context
135
+ if (context.inDiffBlock) {
136
+ // Check for actual diff lines (not markdown lists)
137
+ if (/^\+[^+]/.test(stripped) && !LINE_PATTERNS.markdownList.test(stripped))
138
+ return { type: "diff-add", priority: 4 };
139
+ if (/^-[^-]/.test(stripped) && !LINE_PATTERNS.markdownList.test(stripped))
140
+ return { type: "diff-del", priority: 4 };
141
+ }
142
+
143
+ // Prompt/context (verbose - collapsible)
144
+ if (LINE_PATTERNS.promptMarker.test(stripped))
145
+ return { type: "prompt-marker", priority: 5 };
146
+ if (LINE_PATTERNS.promptMarkerEnd.test(stripped))
147
+ return { type: "prompt-marker-end", priority: 5 };
148
+
149
+ // System messages
150
+ if (LINE_PATTERNS.mcpStartup.test(stripped))
151
+ return { type: "system", priority: 6 };
152
+ if (LINE_PATTERNS.tokensUsed.test(stripped))
153
+ return { type: "tokens", priority: 6 };
154
+
155
+ // Success/error in command output
156
+ if (LINE_PATTERNS.success.test(stripped))
157
+ return { type: "success", priority: 3 };
158
+ if (LINE_PATTERNS.exitCode.test(stripped))
159
+ return { type: "exit-code", priority: 3 };
160
+
161
+ // If we're in a context block, mark as context
162
+ if (context.inPromptBlock) return { type: "prompt-context", priority: 5 };
163
+
164
+ // Default: regular output
165
+ return { type: "output", priority: 4 };
166
+ }
167
+
168
+ function processLine(line) {
169
+ let next = line;
170
+ // Normalize run markers that include "chat"
171
+ next = next.replace(/^=== run (\d+)\s+chat(\s|$)/, "=== run $1$2");
172
+
173
+ if (!showTimestampToggle.checked) {
174
+ next = next.replace(/^\[[^\]]*]\s*/, "");
175
+ }
176
+ if (!showRunToggle.checked) {
177
+ if (next.startsWith("[")) {
178
+ next = next.replace(/^(\[[^\]]+]\s*)run=\d+\s*/, "$1");
179
+ } else {
180
+ next = next.replace(/^run=\d+\s*/, "");
181
+ }
182
+ }
183
+ // Remove redundant channel prefix
184
+ next = next.replace(/^(\[[^\]]+]\s*)?(run=\d+\s*)?chat:\s*/, "$1$2");
185
+ // Strip stdout/stderr markers that make logs noisy
186
+ next = next.replace(
187
+ /^(\[[^\]]+]\s*)?(run=\d+\s*)?(stdout|stderr):\s*/,
188
+ "$1$2"
189
+ );
190
+ // Strip doc-chat id prefix for cleaner display
191
+ next = next.replace(
192
+ /^(\[[^\]]+]\s*)?(run=\d+\s*)?doc-chat id=[a-f0-9]+ stdout:\s*/i,
193
+ "$1$2"
194
+ );
195
+ return next.trimEnd();
196
+ }
197
+
198
+ function shouldOmitLine(line) {
199
+ // Only omit doc-chat metadata lines (result=, exit_code=) when Run toggle is off
200
+ // We still want to show the actual content from doc-chat
201
+ if (!showRunToggle.checked && DOC_CHAT_META_RE.test(line)) {
202
+ return true;
203
+ }
204
+ return false;
205
+ }
206
+
207
+ function resetRenderState() {
208
+ renderState = {
209
+ inPromptBlock: false,
210
+ promptBlockDetails: null,
211
+ promptBlockContent: null,
212
+ promptBlockType: null,
213
+ promptLineCount: 0,
214
+ inDiffBlock: false,
215
+ };
216
+ }
217
+
218
+ function resetLogContexts() {
219
+ logContexts = [];
220
+ logContextState = { inPromptBlock: false, inDiffBlock: false };
221
+ }
222
+
223
+ function updateLogContextForLine(line) {
224
+ logContexts.push({ ...logContextState });
225
+ const classification = classifyLine(line, logContextState);
226
+ if (classification.startDiff) {
227
+ logContextState.inDiffBlock = true;
228
+ }
229
+ if (classification.resetDiff) {
230
+ logContextState.inDiffBlock = false;
231
+ }
232
+ if (classification.type === "prompt-marker") {
233
+ logContextState.inPromptBlock = true;
234
+ logContextState.inDiffBlock = false;
235
+ } else if (classification.type === "prompt-marker-end") {
236
+ logContextState.inPromptBlock = false;
237
+ }
238
+ }
239
+
240
+ function rebuildLogContexts() {
241
+ resetLogContexts();
242
+ rawLogLines.forEach((line) => updateLogContextForLine(line));
243
+ }
244
+
245
+ function finalizePromptBlock() {
246
+ if (!renderState || !renderState.promptBlockDetails) return;
247
+ const countEl = renderState.promptBlockDetails.querySelector(
248
+ ".log-context-count"
249
+ );
250
+ if (countEl) {
251
+ countEl.textContent = `(${renderState.promptLineCount} lines)`;
252
+ }
253
+ }
254
+
255
+ function startPromptBlock(output, label) {
256
+ renderState.promptBlockType = label;
257
+ renderState.promptBlockDetails = document.createElement("details");
258
+ renderState.promptBlockDetails.className = "log-context-block";
259
+ const summary = document.createElement("summary");
260
+ summary.className = "log-context-summary";
261
+ summary.innerHTML = `<span class="log-context-icon">▶</span> ${label} <span class="log-context-count"></span>`;
262
+ renderState.promptBlockDetails.appendChild(summary);
263
+ renderState.promptBlockContent = document.createElement("div");
264
+ renderState.promptBlockContent.className = "log-context-content";
265
+ renderState.promptBlockDetails.appendChild(renderState.promptBlockContent);
266
+ renderState.promptLineCount = 0;
267
+ output.appendChild(renderState.promptBlockDetails);
268
+ }
269
+
270
+ function appendRenderedLine(line, output) {
271
+ if (!renderState) resetRenderState();
272
+ if (shouldOmitLine(line)) return;
273
+
274
+ const processed = processLine(line).trimEnd();
275
+ const classification = classifyLine(line, renderState);
276
+
277
+ if (classification.startDiff) {
278
+ renderState.inDiffBlock = true;
279
+ }
280
+ if (classification.resetDiff) {
281
+ renderState.inDiffBlock = false;
282
+ }
283
+
284
+ if (classification.type === "prompt-marker") {
285
+ renderState.inPromptBlock = true;
286
+ renderState.inDiffBlock = false;
287
+ const match = processed.match(/<(\w+)>/);
288
+ const blockLabel = match ? match[1] : "CONTEXT";
289
+ startPromptBlock(output, blockLabel);
290
+ return;
291
+ }
292
+
293
+ if (classification.type === "prompt-marker-end") {
294
+ finalizePromptBlock();
295
+ renderState.promptBlockDetails = null;
296
+ renderState.promptBlockContent = null;
297
+ renderState.promptBlockType = null;
298
+ renderState.promptLineCount = 0;
299
+ renderState.inPromptBlock = false;
300
+ return;
301
+ }
302
+
303
+ if (
304
+ renderState.promptBlockContent &&
305
+ renderState.inPromptBlock &&
306
+ (classification.type === "prompt-context" || classification.type === "output")
307
+ ) {
308
+ const div = document.createElement("div");
309
+ div.textContent = processed;
310
+ div.className = "log-line log-prompt-context";
311
+ renderState.promptBlockContent.appendChild(div);
312
+ renderState.promptLineCount++;
313
+ return;
314
+ }
315
+
316
+ const isBlank = processed.trim() === "";
317
+ const div = document.createElement("div");
318
+ div.textContent = processed;
319
+
320
+ if (isBlank) {
321
+ div.className = "log-line log-blank";
322
+ } else {
323
+ div.className = `log-line log-${classification.type}`;
324
+ div.dataset.logType = classification.type;
325
+ div.dataset.priority = classification.priority;
326
+ }
327
+
328
+ if (classification.type === "thinking-label" || classification.type === "thinking") {
329
+ div.dataset.icon = "💭";
330
+ } else if (classification.type === "exec-label" || classification.type === "exec-command") {
331
+ div.dataset.icon = "⚡";
332
+ } else if (
333
+ classification.type === "file-update-label" ||
334
+ classification.type === "file-modified"
335
+ ) {
336
+ div.dataset.icon = "📝";
337
+ } else if (classification.type === "agent-output") {
338
+ div.dataset.icon = "✨";
339
+ } else if (classification.type === "run-start" || classification.type === "run-end") {
340
+ div.dataset.icon = "🔄";
341
+ } else if (classification.type === "success") {
342
+ div.dataset.icon = "✓";
343
+ } else if (classification.type === "tokens") {
344
+ div.dataset.icon = "📊";
345
+ }
346
+
347
+ output.appendChild(div);
348
+ }
349
+
350
+ function trimLogBuffer() {
351
+ const maxLines = CONSTANTS.UI.MAX_LOG_LINES_IN_MEMORY;
352
+ if (!maxLines || rawLogLines.length <= maxLines) return;
353
+ const overflow = rawLogLines.length - maxLines;
354
+ rawLogLines = rawLogLines.slice(overflow);
355
+ if (logContexts.length > overflow) {
356
+ logContexts = logContexts.slice(overflow);
357
+ } else {
358
+ logContexts = [];
359
+ }
360
+ renderedStartIndex = Math.max(0, renderedStartIndex - overflow);
361
+ renderedEndIndex = Math.max(0, renderedEndIndex - overflow);
362
+ }
363
+
364
+ function updateLoadOlderButton() {
365
+ if (!loadOlderButton) return;
366
+ if (renderedStartIndex > 0) {
367
+ loadOlderButton.classList.remove("hidden");
368
+ } else {
369
+ loadOlderButton.classList.add("hidden");
370
+ }
371
+ }
372
+
373
+ function applyLogUrlState() {
374
+ const params = getUrlParams();
375
+ const runId = params.get("run");
376
+ const tail = params.get("tail");
377
+ if (runId !== null && logRunIdInput) {
378
+ logRunIdInput.value = runId;
379
+ }
380
+ if (tail !== null && logTailInput) {
381
+ logTailInput.value = tail;
382
+ }
383
+ if (runId) {
384
+ isViewingTail = false;
385
+ }
386
+ }
387
+
388
+ function syncLogUrlState() {
389
+ const runId = logRunIdInput?.value?.trim() || "";
390
+ const tail = logTailInput?.value?.trim() || "";
391
+ updateUrlParams({
392
+ run: runId || null,
393
+ tail: runId ? null : tail || null,
394
+ });
395
+ }
396
+
397
+ function renderLogWindow({ startIndex = null, followTail = true } = {}) {
398
+ const output = document.getElementById("log-output");
399
+
400
+ if (rawLogLines.length === 0) {
401
+ output.innerHTML = "";
402
+ output.textContent = "(empty log)";
403
+ output.dataset.isPlaceholder = "true";
404
+ renderedStartIndex = 0;
405
+ renderedEndIndex = 0;
406
+ isViewingTail = true;
407
+ updateLoadOlderButton();
408
+ return;
409
+ }
410
+
411
+ const endIndex = rawLogLines.length;
412
+ let windowStart = startIndex;
413
+ if (followTail || windowStart === null) {
414
+ windowStart = Math.max(0, endIndex - CONSTANTS.UI.MAX_LOG_LINES_IN_DOM);
415
+ }
416
+ const windowEnd = Math.min(
417
+ endIndex,
418
+ windowStart + CONSTANTS.UI.MAX_LOG_LINES_IN_DOM
419
+ );
420
+
421
+ output.innerHTML = "";
422
+ delete output.dataset.isPlaceholder;
423
+ resetRenderState();
424
+ const startContext = logContexts[windowStart];
425
+ if (startContext) {
426
+ renderState.inPromptBlock = startContext.inPromptBlock;
427
+ renderState.inDiffBlock = startContext.inDiffBlock;
428
+ if (renderState.inPromptBlock) {
429
+ startPromptBlock(output, "CONTEXT (continued)");
430
+ }
431
+ }
432
+
433
+ for (let i = windowStart; i < windowEnd; i += 1) {
434
+ appendRenderedLine(rawLogLines[i], output);
435
+ }
436
+ finalizePromptBlock();
437
+
438
+ renderedStartIndex = windowStart;
439
+ renderedEndIndex = windowEnd;
440
+ isViewingTail = followTail && windowEnd === endIndex;
441
+ updateLoadOlderButton();
442
+
443
+ if (isViewingTail) {
444
+ scrollLogsToBottom(true);
445
+ }
446
+ }
447
+
448
+ function appendLogLine(line) {
449
+ const output = document.getElementById("log-output");
450
+ if (output.dataset.isPlaceholder === "true") {
451
+ output.innerHTML = "";
452
+ delete output.dataset.isPlaceholder;
453
+ rawLogLines = [];
454
+ resetRenderState();
455
+ resetLogContexts();
456
+ renderedStartIndex = 0;
457
+ renderedEndIndex = 0;
458
+ isViewingTail = true;
459
+ }
460
+
461
+ rawLogLines.push(line);
462
+ updateLogContextForLine(line);
463
+ trimLogBuffer();
464
+
465
+ if (!isViewingTail) {
466
+ publish("logs:line", line);
467
+ updateLoadOlderButton();
468
+ return;
469
+ }
470
+
471
+ appendRenderedLine(line, output);
472
+ renderedEndIndex = rawLogLines.length;
473
+ if (output.childElementCount > CONSTANTS.UI.MAX_LOG_LINES_IN_DOM) {
474
+ output.firstElementChild.remove();
475
+ }
476
+ renderedStartIndex = Math.max(
477
+ 0,
478
+ renderedEndIndex - output.childElementCount
479
+ );
480
+ updateLoadOlderButton();
481
+ publish("logs:line", line);
482
+ scrollLogsToBottom();
483
+ }
484
+
485
+ function scrollLogsToBottom(force = false) {
486
+ const output = document.getElementById("log-output");
487
+ if (!output) return;
488
+ if (!autoScrollEnabled && !force) return;
489
+
490
+ requestAnimationFrame(() => {
491
+ output.scrollTop = output.scrollHeight;
492
+ });
493
+ }
494
+
495
+ function updateJumpButtonVisibility() {
496
+ const output = document.getElementById("log-output");
497
+ if (!output || !jumpBottomButton) return;
498
+
499
+ const isNearBottom =
500
+ output.scrollHeight - output.scrollTop - output.clientHeight < 100;
501
+
502
+ if (isNearBottom) {
503
+ jumpBottomButton.classList.add("hidden");
504
+ autoScrollEnabled = true;
505
+ } else {
506
+ jumpBottomButton.classList.remove("hidden");
507
+ autoScrollEnabled = false;
508
+ }
509
+ }
510
+
511
+ function setLogStreamButton(active) {
512
+ toggleLogStreamButton.textContent = active ? "Stop stream" : "Start stream";
513
+ }
514
+
515
+ async function loadLogs() {
516
+ syncLogUrlState();
517
+ const runId = logRunIdInput.value;
518
+ const tail = logTailInput.value || "200";
519
+ const params = new URLSearchParams();
520
+ if (runId) {
521
+ params.set("run_id", runId);
522
+ } else if (tail) {
523
+ params.set("tail", tail);
524
+ }
525
+ const path = params.toString()
526
+ ? `/api/logs?${params.toString()}`
527
+ : "/api/logs";
528
+ try {
529
+ const data = await api(path);
530
+ const text = typeof data === "string" ? data : data.log || "";
531
+ const output = document.getElementById("log-output");
532
+
533
+ if (text) {
534
+ rawLogLines = text.split("\n");
535
+ trimLogBuffer();
536
+ rebuildLogContexts();
537
+ delete output.dataset.isPlaceholder;
538
+ isViewingTail = true;
539
+ renderLogs();
540
+
541
+ // Update cache if we are looking at the latest logs (no specific run ID)
542
+ if (!runId) {
543
+ // Limit to last 200 lines to avoid localStorage quota issues
544
+ const lines = rawLogLines.slice(-200);
545
+ saveToCache("logs:tail", lines.join("\n"));
546
+ }
547
+ } else {
548
+ output.textContent = "(empty log)";
549
+ output.dataset.isPlaceholder = "true";
550
+ rawLogLines = [];
551
+ resetRenderState();
552
+ resetLogContexts();
553
+ renderedStartIndex = 0;
554
+ renderedEndIndex = 0;
555
+ isViewingTail = true;
556
+ updateLoadOlderButton();
557
+ if (!runId) {
558
+ saveToCache("logs:tail", "");
559
+ }
560
+ }
561
+
562
+ flash("Logs loaded");
563
+ publish("logs:loaded", { runId, tail, text });
564
+ } catch (err) {
565
+ flash(err.message);
566
+ }
567
+ }
568
+
569
+ function stopLogStreaming() {
570
+ if (stopLogStream) {
571
+ stopLogStream();
572
+ stopLogStream = null;
573
+ }
574
+ setLogStreamButton(false);
575
+ publish("logs:streaming", false);
576
+ }
577
+
578
+ function startLogStreaming() {
579
+ if (stopLogStream) return;
580
+ const output = document.getElementById("log-output");
581
+ output.textContent = "(listening...)";
582
+ output.dataset.isPlaceholder = "true";
583
+ rawLogLines = [];
584
+ resetRenderState();
585
+ resetLogContexts();
586
+ renderedStartIndex = 0;
587
+ renderedEndIndex = 0;
588
+ isViewingTail = true;
589
+ updateLoadOlderButton();
590
+
591
+ stopLogStream = streamEvents("/api/logs/stream", {
592
+ onMessage: (data) => {
593
+ appendLogLine(data || "");
594
+ },
595
+ onError: (err) => {
596
+ flash(err.message);
597
+ stopLogStreaming();
598
+ },
599
+ onFinish: () => {
600
+ stopLogStream = null;
601
+ setLogStreamButton(false);
602
+ publish("logs:streaming", false);
603
+ },
604
+ });
605
+ setLogStreamButton(true);
606
+ publish("logs:streaming", true);
607
+ flash("Streaming logs…");
608
+ }
609
+
610
+ function syncRunIdPlaceholder(state) {
611
+ lastKnownRunId = state?.last_run_id ?? null;
612
+ logRunIdInput.placeholder = lastKnownRunId
613
+ ? `latest (${lastKnownRunId})`
614
+ : "latest";
615
+ }
616
+
617
+ function renderLogs() {
618
+ renderLogWindow({ followTail: isViewingTail });
619
+ }
620
+
621
+ export function initLogs() {
622
+ applyLogUrlState();
623
+ document.getElementById("load-logs").addEventListener("click", loadLogs);
624
+ toggleLogStreamButton.addEventListener("click", () => {
625
+ if (stopLogStream) {
626
+ stopLogStreaming();
627
+ } else {
628
+ startLogStreaming();
629
+ }
630
+ });
631
+
632
+ subscribe("state:update", syncRunIdPlaceholder);
633
+ subscribe("tab:change", (tab) => {
634
+ if (tab !== "logs" && stopLogStream) {
635
+ stopLogStreaming();
636
+ }
637
+ });
638
+
639
+ showTimestampToggle.addEventListener("change", renderLogs);
640
+ showRunToggle.addEventListener("change", renderLogs);
641
+
642
+ // Jump to bottom button
643
+ if (jumpBottomButton) {
644
+ jumpBottomButton.addEventListener("click", () => {
645
+ if (!isViewingTail) {
646
+ isViewingTail = true;
647
+ renderLogs();
648
+ }
649
+ autoScrollEnabled = true;
650
+ scrollLogsToBottom(true);
651
+ jumpBottomButton.classList.add("hidden");
652
+ });
653
+ }
654
+
655
+ if (loadOlderButton) {
656
+ loadOlderButton.addEventListener("click", () => {
657
+ if (renderedStartIndex <= 0) return;
658
+ const nextStart = Math.max(
659
+ 0,
660
+ renderedStartIndex - CONSTANTS.UI.LOG_PAGE_SIZE
661
+ );
662
+ isViewingTail = false;
663
+ autoScrollEnabled = false;
664
+ renderLogWindow({ startIndex: nextStart, followTail: false });
665
+ });
666
+ }
667
+
668
+ // Track scroll position to show/hide jump button
669
+ const output = document.getElementById("log-output");
670
+ if (output) {
671
+ output.addEventListener("scroll", updateJumpButtonVisibility);
672
+ }
673
+
674
+ // Try loading from cache first
675
+ const cachedLogs = loadFromCache("logs:tail");
676
+ if (cachedLogs) {
677
+ const output = document.getElementById("log-output");
678
+ rawLogLines = cachedLogs.split("\n");
679
+ if (rawLogLines.length > 0) {
680
+ trimLogBuffer();
681
+ rebuildLogContexts();
682
+ delete output.dataset.isPlaceholder;
683
+ isViewingTail = true;
684
+ renderLogs();
685
+ scrollLogsToBottom(true);
686
+ }
687
+ }
688
+
689
+ loadLogs();
690
+ }