codex-autorunner 0.1.2__py3-none-any.whl → 1.0.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 (189) hide show
  1. codex_autorunner/__main__.py +4 -0
  2. codex_autorunner/agents/opencode/client.py +68 -35
  3. codex_autorunner/agents/opencode/logging.py +21 -5
  4. codex_autorunner/agents/opencode/run_prompt.py +1 -0
  5. codex_autorunner/agents/opencode/runtime.py +118 -30
  6. codex_autorunner/agents/opencode/supervisor.py +36 -48
  7. codex_autorunner/agents/registry.py +136 -8
  8. codex_autorunner/api.py +25 -0
  9. codex_autorunner/bootstrap.py +16 -35
  10. codex_autorunner/cli.py +157 -139
  11. codex_autorunner/core/about_car.py +44 -32
  12. codex_autorunner/core/adapter_utils.py +21 -0
  13. codex_autorunner/core/app_server_logging.py +7 -3
  14. codex_autorunner/core/app_server_prompts.py +27 -260
  15. codex_autorunner/core/app_server_threads.py +15 -26
  16. codex_autorunner/core/codex_runner.py +6 -0
  17. codex_autorunner/core/config.py +390 -100
  18. codex_autorunner/core/docs.py +10 -2
  19. codex_autorunner/core/drafts.py +82 -0
  20. codex_autorunner/core/engine.py +278 -262
  21. codex_autorunner/core/flows/__init__.py +25 -0
  22. codex_autorunner/core/flows/controller.py +178 -0
  23. codex_autorunner/core/flows/definition.py +82 -0
  24. codex_autorunner/core/flows/models.py +75 -0
  25. codex_autorunner/core/flows/runtime.py +351 -0
  26. codex_autorunner/core/flows/store.py +485 -0
  27. codex_autorunner/core/flows/transition.py +133 -0
  28. codex_autorunner/core/flows/worker_process.py +242 -0
  29. codex_autorunner/core/hub.py +15 -9
  30. codex_autorunner/core/locks.py +4 -0
  31. codex_autorunner/core/prompt.py +15 -7
  32. codex_autorunner/core/redaction.py +29 -0
  33. codex_autorunner/core/review_context.py +5 -8
  34. codex_autorunner/core/run_index.py +6 -0
  35. codex_autorunner/core/runner_process.py +5 -2
  36. codex_autorunner/core/state.py +0 -88
  37. codex_autorunner/core/static_assets.py +55 -0
  38. codex_autorunner/core/supervisor_utils.py +67 -0
  39. codex_autorunner/core/update.py +20 -11
  40. codex_autorunner/core/update_runner.py +2 -0
  41. codex_autorunner/core/utils.py +29 -2
  42. codex_autorunner/discovery.py +2 -4
  43. codex_autorunner/flows/ticket_flow/__init__.py +3 -0
  44. codex_autorunner/flows/ticket_flow/definition.py +91 -0
  45. codex_autorunner/integrations/agents/__init__.py +27 -0
  46. codex_autorunner/integrations/agents/agent_backend.py +142 -0
  47. codex_autorunner/integrations/agents/codex_backend.py +307 -0
  48. codex_autorunner/integrations/agents/opencode_backend.py +325 -0
  49. codex_autorunner/integrations/agents/run_event.py +71 -0
  50. codex_autorunner/integrations/app_server/client.py +576 -92
  51. codex_autorunner/integrations/app_server/supervisor.py +59 -33
  52. codex_autorunner/integrations/telegram/adapter.py +141 -167
  53. codex_autorunner/integrations/telegram/api_schemas.py +120 -0
  54. codex_autorunner/integrations/telegram/config.py +175 -0
  55. codex_autorunner/integrations/telegram/constants.py +16 -1
  56. codex_autorunner/integrations/telegram/dispatch.py +17 -0
  57. codex_autorunner/integrations/telegram/doctor.py +47 -0
  58. codex_autorunner/integrations/telegram/handlers/callbacks.py +0 -4
  59. codex_autorunner/integrations/telegram/handlers/commands/__init__.py +2 -0
  60. codex_autorunner/integrations/telegram/handlers/commands/execution.py +53 -57
  61. codex_autorunner/integrations/telegram/handlers/commands/files.py +2 -6
  62. codex_autorunner/integrations/telegram/handlers/commands/flows.py +227 -0
  63. codex_autorunner/integrations/telegram/handlers/commands/formatting.py +1 -1
  64. codex_autorunner/integrations/telegram/handlers/commands/github.py +41 -582
  65. codex_autorunner/integrations/telegram/handlers/commands/workspace.py +8 -8
  66. codex_autorunner/integrations/telegram/handlers/commands_runtime.py +133 -475
  67. codex_autorunner/integrations/telegram/handlers/commands_spec.py +11 -4
  68. codex_autorunner/integrations/telegram/handlers/messages.py +120 -9
  69. codex_autorunner/integrations/telegram/helpers.py +88 -16
  70. codex_autorunner/integrations/telegram/outbox.py +208 -37
  71. codex_autorunner/integrations/telegram/progress_stream.py +3 -10
  72. codex_autorunner/integrations/telegram/service.py +214 -40
  73. codex_autorunner/integrations/telegram/state.py +100 -2
  74. codex_autorunner/integrations/telegram/ticket_flow_bridge.py +322 -0
  75. codex_autorunner/integrations/telegram/transport.py +36 -3
  76. codex_autorunner/integrations/telegram/trigger_mode.py +53 -0
  77. codex_autorunner/manifest.py +2 -0
  78. codex_autorunner/plugin_api.py +22 -0
  79. codex_autorunner/routes/__init__.py +23 -14
  80. codex_autorunner/routes/analytics.py +239 -0
  81. codex_autorunner/routes/base.py +81 -109
  82. codex_autorunner/routes/file_chat.py +836 -0
  83. codex_autorunner/routes/flows.py +980 -0
  84. codex_autorunner/routes/messages.py +459 -0
  85. codex_autorunner/routes/system.py +6 -1
  86. codex_autorunner/routes/usage.py +87 -0
  87. codex_autorunner/routes/workspace.py +271 -0
  88. codex_autorunner/server.py +2 -1
  89. codex_autorunner/static/agentControls.js +1 -0
  90. codex_autorunner/static/agentEvents.js +248 -0
  91. codex_autorunner/static/app.js +25 -22
  92. codex_autorunner/static/autoRefresh.js +29 -1
  93. codex_autorunner/static/bootstrap.js +1 -0
  94. codex_autorunner/static/bus.js +1 -0
  95. codex_autorunner/static/cache.js +1 -0
  96. codex_autorunner/static/constants.js +20 -4
  97. codex_autorunner/static/dashboard.js +162 -196
  98. codex_autorunner/static/diffRenderer.js +37 -0
  99. codex_autorunner/static/docChatCore.js +324 -0
  100. codex_autorunner/static/docChatStorage.js +65 -0
  101. codex_autorunner/static/docChatVoice.js +65 -0
  102. codex_autorunner/static/docEditor.js +133 -0
  103. codex_autorunner/static/env.js +1 -0
  104. codex_autorunner/static/eventSummarizer.js +166 -0
  105. codex_autorunner/static/fileChat.js +182 -0
  106. codex_autorunner/static/health.js +155 -0
  107. codex_autorunner/static/hub.js +41 -118
  108. codex_autorunner/static/index.html +787 -858
  109. codex_autorunner/static/liveUpdates.js +1 -0
  110. codex_autorunner/static/loader.js +1 -0
  111. codex_autorunner/static/messages.js +470 -0
  112. codex_autorunner/static/mobileCompact.js +2 -1
  113. codex_autorunner/static/settings.js +24 -211
  114. codex_autorunner/static/styles.css +7567 -3865
  115. codex_autorunner/static/tabs.js +28 -5
  116. codex_autorunner/static/terminal.js +14 -0
  117. codex_autorunner/static/terminalManager.js +34 -59
  118. codex_autorunner/static/ticketChatActions.js +333 -0
  119. codex_autorunner/static/ticketChatEvents.js +16 -0
  120. codex_autorunner/static/ticketChatStorage.js +16 -0
  121. codex_autorunner/static/ticketChatStream.js +264 -0
  122. codex_autorunner/static/ticketEditor.js +750 -0
  123. codex_autorunner/static/ticketVoice.js +9 -0
  124. codex_autorunner/static/tickets.js +1315 -0
  125. codex_autorunner/static/utils.js +32 -3
  126. codex_autorunner/static/voice.js +1 -0
  127. codex_autorunner/static/workspace.js +672 -0
  128. codex_autorunner/static/workspaceApi.js +53 -0
  129. codex_autorunner/static/workspaceFileBrowser.js +504 -0
  130. codex_autorunner/tickets/__init__.py +20 -0
  131. codex_autorunner/tickets/agent_pool.py +377 -0
  132. codex_autorunner/tickets/files.py +85 -0
  133. codex_autorunner/tickets/frontmatter.py +55 -0
  134. codex_autorunner/tickets/lint.py +102 -0
  135. codex_autorunner/tickets/models.py +95 -0
  136. codex_autorunner/tickets/outbox.py +232 -0
  137. codex_autorunner/tickets/replies.py +179 -0
  138. codex_autorunner/tickets/runner.py +823 -0
  139. codex_autorunner/tickets/spec_ingest.py +77 -0
  140. codex_autorunner/web/app.py +269 -91
  141. codex_autorunner/web/middleware.py +3 -4
  142. codex_autorunner/web/schemas.py +89 -109
  143. codex_autorunner/web/static_assets.py +1 -44
  144. codex_autorunner/workspace/__init__.py +40 -0
  145. codex_autorunner/workspace/paths.py +319 -0
  146. {codex_autorunner-0.1.2.dist-info → codex_autorunner-1.0.0.dist-info}/METADATA +18 -21
  147. codex_autorunner-1.0.0.dist-info/RECORD +251 -0
  148. {codex_autorunner-0.1.2.dist-info → codex_autorunner-1.0.0.dist-info}/WHEEL +1 -1
  149. codex_autorunner/agents/execution/policy.py +0 -292
  150. codex_autorunner/agents/factory.py +0 -52
  151. codex_autorunner/agents/orchestrator.py +0 -358
  152. codex_autorunner/core/doc_chat.py +0 -1446
  153. codex_autorunner/core/snapshot.py +0 -580
  154. codex_autorunner/integrations/github/chatops.py +0 -268
  155. codex_autorunner/integrations/github/pr_flow.py +0 -1314
  156. codex_autorunner/routes/docs.py +0 -381
  157. codex_autorunner/routes/github.py +0 -327
  158. codex_autorunner/routes/runs.py +0 -250
  159. codex_autorunner/spec_ingest.py +0 -812
  160. codex_autorunner/static/docChatActions.js +0 -287
  161. codex_autorunner/static/docChatEvents.js +0 -300
  162. codex_autorunner/static/docChatRender.js +0 -205
  163. codex_autorunner/static/docChatStream.js +0 -361
  164. codex_autorunner/static/docs.js +0 -20
  165. codex_autorunner/static/docsClipboard.js +0 -69
  166. codex_autorunner/static/docsCrud.js +0 -257
  167. codex_autorunner/static/docsDocUpdates.js +0 -62
  168. codex_autorunner/static/docsDrafts.js +0 -16
  169. codex_autorunner/static/docsElements.js +0 -69
  170. codex_autorunner/static/docsInit.js +0 -285
  171. codex_autorunner/static/docsParse.js +0 -160
  172. codex_autorunner/static/docsSnapshot.js +0 -87
  173. codex_autorunner/static/docsSpecIngest.js +0 -263
  174. codex_autorunner/static/docsState.js +0 -127
  175. codex_autorunner/static/docsThreadRegistry.js +0 -44
  176. codex_autorunner/static/docsUi.js +0 -153
  177. codex_autorunner/static/docsVoice.js +0 -56
  178. codex_autorunner/static/github.js +0 -504
  179. codex_autorunner/static/logs.js +0 -678
  180. codex_autorunner/static/review.js +0 -157
  181. codex_autorunner/static/runs.js +0 -418
  182. codex_autorunner/static/snapshot.js +0 -124
  183. codex_autorunner/static/state.js +0 -94
  184. codex_autorunner/static/todoPreview.js +0 -27
  185. codex_autorunner/workspace.py +0 -16
  186. codex_autorunner-0.1.2.dist-info/RECORD +0 -222
  187. {codex_autorunner-0.1.2.dist-info → codex_autorunner-1.0.0.dist-info}/entry_points.txt +0 -0
  188. {codex_autorunner-0.1.2.dist-info → codex_autorunner-1.0.0.dist-info}/licenses/LICENSE +0 -0
  189. {codex_autorunner-0.1.2.dist-info → codex_autorunner-1.0.0.dist-info}/top_level.txt +0 -0
@@ -1,3 +1,4 @@
1
+ // GENERATED FILE - do not edit directly. Source: static_src/
1
2
  import { publish, subscribe } from "./bus.js";
2
3
  const INVALIDATION_DEBOUNCE_MS = 750;
3
4
  let initialized = false;
@@ -1,3 +1,4 @@
1
+ // GENERATED FILE - do not edit directly. Source: static_src/
1
2
  "use strict";
2
3
  (() => {
3
4
  const loadScripts = () => {
@@ -0,0 +1,470 @@
1
+ // GENERATED FILE - do not edit directly. Source: static_src/
2
+ import { api, escapeHtml, flash, getUrlParams, resolvePath, updateUrlParams, } from "./utils.js";
3
+ import { subscribe } from "./bus.js";
4
+ import { isRepoHealthy } from "./health.js";
5
+ let bellInitialized = false;
6
+ let messagesInitialized = false;
7
+ let activeRunId = null;
8
+ let selectedRunId = null;
9
+ const threadsEl = document.getElementById("messages-thread-list");
10
+ const detailEl = document.getElementById("messages-thread-detail");
11
+ const refreshEl = document.getElementById("messages-refresh");
12
+ const replyBodyEl = document.getElementById("messages-reply-body");
13
+ const replyFilesEl = document.getElementById("messages-reply-files");
14
+ const replySendEl = document.getElementById("messages-reply-send");
15
+ function formatTimestamp(ts) {
16
+ if (!ts)
17
+ return "–";
18
+ const date = new Date(ts);
19
+ if (Number.isNaN(date.getTime()))
20
+ return ts;
21
+ return date.toLocaleString();
22
+ }
23
+ function setBadge(count) {
24
+ const badge = document.getElementById("tab-badge-inbox");
25
+ if (!badge)
26
+ return;
27
+ if (count > 0) {
28
+ badge.textContent = String(count);
29
+ badge.classList.remove("hidden");
30
+ }
31
+ else {
32
+ badge.textContent = "";
33
+ badge.classList.add("hidden");
34
+ }
35
+ }
36
+ export async function refreshBell() {
37
+ if (!isRepoHealthy()) {
38
+ activeRunId = null;
39
+ setBadge(0);
40
+ return;
41
+ }
42
+ try {
43
+ const res = (await api("/api/messages/active"));
44
+ if (res?.active && res.run_id) {
45
+ activeRunId = res.run_id;
46
+ setBadge(1);
47
+ }
48
+ else {
49
+ activeRunId = null;
50
+ setBadge(0);
51
+ }
52
+ }
53
+ catch (_err) {
54
+ // Best-effort.
55
+ activeRunId = null;
56
+ setBadge(0);
57
+ }
58
+ }
59
+ export function initMessageBell() {
60
+ if (bellInitialized)
61
+ return;
62
+ bellInitialized = true;
63
+ // Cheap polling. (The repo shell already does other polling; keep this light.)
64
+ refreshBell();
65
+ window.setInterval(() => {
66
+ if (document.hidden)
67
+ return;
68
+ if (!isRepoHealthy())
69
+ return;
70
+ refreshBell();
71
+ }, 15000);
72
+ subscribe("repo:health", (payload) => {
73
+ const status = payload?.status || "";
74
+ if (status === "ok" || status === "degraded") {
75
+ void refreshBell();
76
+ }
77
+ });
78
+ }
79
+ function renderThreadItem(thread) {
80
+ const latestDispatch = thread.latest?.dispatch;
81
+ const isHandoff = latestDispatch?.is_handoff || latestDispatch?.mode === "pause";
82
+ const title = latestDispatch?.title || (isHandoff ? "Handoff" : "Dispatch");
83
+ const subtitle = latestDispatch?.body ? latestDispatch.body.slice(0, 120) : "";
84
+ const isPaused = thread.status === "paused";
85
+ // Only show action indicator if there's an unreplied handoff (pause)
86
+ // Compare dispatch_seq vs reply_seq to check if user has responded
87
+ const ticketState = thread.ticket_state;
88
+ const dispatchSeq = ticketState?.dispatch_seq ?? 0;
89
+ const replySeq = ticketState?.reply_seq ?? 0;
90
+ const hasUnrepliedHandoff = isPaused && (dispatchSeq > replySeq || (isHandoff && replySeq === 0));
91
+ const indicator = hasUnrepliedHandoff ? `<span class="messages-thread-indicator" title="Action required"></span>` : "";
92
+ const dispatches = thread.dispatch_count ?? 0;
93
+ const replies = thread.reply_count ?? 0;
94
+ const metaLine = `${dispatches} dispatch${dispatches !== 1 ? "es" : ""} · ${replies} repl${replies !== 1 ? "ies" : "y"}`;
95
+ return `
96
+ <button class="messages-thread" data-run-id="${escapeHtml(thread.run_id)}">
97
+ <div class="messages-thread-title">${indicator}${escapeHtml(title)}</div>
98
+ <div class="messages-thread-subtitle muted">${escapeHtml(subtitle)}</div>
99
+ <div class="messages-thread-meta-line">${escapeHtml(metaLine)}</div>
100
+ </button>
101
+ `;
102
+ }
103
+ async function loadThreads() {
104
+ if (!threadsEl)
105
+ return;
106
+ threadsEl.innerHTML = "Loading…";
107
+ if (!isRepoHealthy()) {
108
+ threadsEl.innerHTML = "<div class=\"muted\">Repo offline or uninitialized</div>";
109
+ return;
110
+ }
111
+ let res;
112
+ try {
113
+ res = (await api("/api/messages/threads"));
114
+ }
115
+ catch (err) {
116
+ threadsEl.innerHTML = "";
117
+ flash("Failed to load inbox", "error");
118
+ return;
119
+ }
120
+ const conversations = res?.conversations || [];
121
+ if (!conversations.length) {
122
+ threadsEl.innerHTML = "<div class=\"muted\">No dispatches</div>";
123
+ return;
124
+ }
125
+ threadsEl.innerHTML = conversations.map(renderThreadItem).join("");
126
+ threadsEl.querySelectorAll(".messages-thread").forEach((btn) => {
127
+ btn.addEventListener("click", () => {
128
+ const runId = btn.dataset.runId || "";
129
+ if (!runId)
130
+ return;
131
+ updateUrlParams({ tab: "inbox", run_id: runId });
132
+ void loadThread(runId);
133
+ });
134
+ });
135
+ }
136
+ function formatBytes(size) {
137
+ if (typeof size !== "number" || Number.isNaN(size))
138
+ return "";
139
+ if (size >= 1000000)
140
+ return `${(size / 1000000).toFixed(1)} MB`;
141
+ if (size >= 1000)
142
+ return `${(size / 1000).toFixed(0)} KB`;
143
+ return `${size} B`;
144
+ }
145
+ export function renderMarkdown(body) {
146
+ if (!body)
147
+ return "";
148
+ let text = escapeHtml(body);
149
+ // Extract fenced code blocks to avoid mutating their contents later.
150
+ const codeBlocks = [];
151
+ text = text.replace(/```([\s\S]*?)```/g, (_m, code) => {
152
+ const placeholder = `@@CODEBLOCK_${codeBlocks.length}@@`;
153
+ codeBlocks.push(`<pre class="md-code"><code>${code}</code></pre>`);
154
+ return placeholder;
155
+ });
156
+ // Inline code
157
+ text = text.replace(/`([^`]+)`/g, "<code>$1</code>");
158
+ // Bold and italic (simple, non-nested)
159
+ text = text.replace(/\*\*([^*]+)\*\*/g, "<strong>$1</strong>");
160
+ text = text.replace(/\*([^*]+)\*/g, "<em>$1</em>");
161
+ // Links [text](url) only for http/https
162
+ text = text.replace(/\[([^\]]+)\]\((https?:[^)]+)\)/g, (_m, label, url) => {
163
+ return `<a href="${escapeHtml(url)}" target="_blank" rel="noopener">${escapeHtml(label)}</a>`;
164
+ });
165
+ // Lists (skip placeholders so code fences remain untouched)
166
+ const lines = text.split(/\n/);
167
+ const out = [];
168
+ let inList = false;
169
+ lines.forEach((line) => {
170
+ if (/^@@CODEBLOCK_\d+@@$/.test(line)) {
171
+ if (inList) {
172
+ out.push("</ul>");
173
+ inList = false;
174
+ }
175
+ out.push(line);
176
+ return;
177
+ }
178
+ if (/^[-*]\s+/.test(line)) {
179
+ if (!inList) {
180
+ out.push("<ul>");
181
+ inList = true;
182
+ }
183
+ out.push(`<li>${line.replace(/^[-*]\s+/, "")}</li>`);
184
+ }
185
+ else {
186
+ if (inList) {
187
+ out.push("</ul>");
188
+ inList = false;
189
+ }
190
+ out.push(line);
191
+ }
192
+ });
193
+ if (inList)
194
+ out.push("</ul>");
195
+ // Paragraphs and placeholder restoration
196
+ const joined = out.join("\n");
197
+ return joined
198
+ .split(/\n\n+/)
199
+ .map((block) => {
200
+ const match = block.match(/^@@CODEBLOCK_(\d+)@@$/);
201
+ if (match) {
202
+ const idx = Number(match[1]);
203
+ return codeBlocks[idx] ?? "";
204
+ }
205
+ return `<p>${block.replace(/\n/g, "<br>")}</p>`;
206
+ })
207
+ .join("");
208
+ }
209
+ function renderFiles(files) {
210
+ if (!files || !files.length)
211
+ return "";
212
+ const items = files
213
+ .map((f) => {
214
+ const size = formatBytes(f.size);
215
+ const href = resolvePath(f.url || "");
216
+ return `<li class="messages-file">
217
+ <span class="messages-file-icon">📎</span>
218
+ <a href="${escapeHtml(href)}" target="_blank" rel="noopener">${escapeHtml(f.name)}</a>
219
+ ${size ? `<span class="messages-file-size muted small">${escapeHtml(size)}</span>` : ""}
220
+ </li>`;
221
+ })
222
+ .join("");
223
+ return `<ul class="messages-files">${items}</ul>`;
224
+ }
225
+ function renderDispatch(entry, isLatest, runStatus) {
226
+ const dispatch = entry.dispatch;
227
+ const isHandoff = dispatch?.is_handoff || dispatch?.mode === "pause";
228
+ const title = dispatch?.title || (isHandoff ? "Handoff" : "Agent update");
229
+ let modeClass = "pill-info";
230
+ let modeLabel = "INFO";
231
+ if (isHandoff) {
232
+ // Only show "ACTION REQUIRED" if this is the latest dispatch AND the run is actually paused.
233
+ // Otherwise, show "HANDOFF" to indicate a historical pause point.
234
+ if (isLatest && runStatus === "paused") {
235
+ modeClass = "pill-action";
236
+ modeLabel = "ACTION REQUIRED";
237
+ }
238
+ else {
239
+ modeClass = "pill-idle";
240
+ modeLabel = "HANDOFF";
241
+ }
242
+ }
243
+ const modePill = dispatch?.mode ? ` <span class="pill pill-small ${modeClass}">${escapeHtml(modeLabel)}</span>` : "";
244
+ const body = dispatch?.body ? `<div class="messages-body messages-markdown">${renderMarkdown(dispatch.body)}</div>` : "";
245
+ const ts = entry.created_at ? formatTimestamp(entry.created_at) : "";
246
+ return `
247
+ <div class="messages-entry" data-seq="${entry.seq}" data-type="dispatch" data-created="${escapeHtml(entry.created_at || "")}">
248
+ <div class="messages-entry-header">
249
+ <span class="messages-entry-seq">#${entry.seq.toString().padStart(4, "0")}</span>
250
+ <span class="messages-entry-title">${escapeHtml(title)}</span>
251
+ ${modePill}
252
+ <span class="messages-entry-time">${escapeHtml(ts)}</span>
253
+ </div>
254
+ ${body}
255
+ ${renderFiles(entry.files)}
256
+ </div>
257
+ `;
258
+ }
259
+ function renderReply(entry, parentSeq) {
260
+ const rep = entry.reply;
261
+ const title = rep?.title || "Your reply";
262
+ const body = rep?.body ? `<div class="messages-body messages-markdown">${renderMarkdown(rep.body)}</div>` : "";
263
+ const ts = entry.created_at ? formatTimestamp(entry.created_at) : "";
264
+ const replyIndicator = parentSeq !== undefined
265
+ ? `<div class="messages-reply-indicator">In response to #${parentSeq.toString().padStart(4, "0")}</div>`
266
+ : "";
267
+ return `
268
+ <div class="messages-entry messages-entry-reply" data-seq="${entry.seq}" data-type="reply" data-created="${escapeHtml(entry.created_at || "")}">
269
+ ${replyIndicator}
270
+ <div class="messages-entry-header">
271
+ <span class="messages-entry-seq">#${entry.seq.toString().padStart(4, "0")}</span>
272
+ <span class="messages-entry-title">${escapeHtml(title)}</span>
273
+ <span class="pill pill-small pill-idle">you</span>
274
+ <span class="messages-entry-time">${escapeHtml(ts)}</span>
275
+ </div>
276
+ ${body}
277
+ ${renderFiles(entry.files)}
278
+ </div>
279
+ `;
280
+ }
281
+ function buildThreadedTimeline(dispatches, replies, runStatus) {
282
+ // Combine all entries into a single timeline
283
+ const timeline = [];
284
+ // Find the latest dispatch sequence number to identify the most recent agent message
285
+ let maxDispatchSeq = -1;
286
+ dispatches.forEach((d) => {
287
+ if (d.seq > maxDispatchSeq)
288
+ maxDispatchSeq = d.seq;
289
+ timeline.push({
290
+ type: "dispatch",
291
+ seq: d.seq,
292
+ created_at: d.created_at || null,
293
+ dispatch: d,
294
+ });
295
+ });
296
+ replies.forEach((r) => {
297
+ timeline.push({
298
+ type: "reply",
299
+ seq: r.seq,
300
+ created_at: r.created_at || null,
301
+ reply: r,
302
+ });
303
+ });
304
+ // Sort chronologically by created_at, fallback to seq
305
+ timeline.sort((a, b) => {
306
+ if (a.created_at && b.created_at) {
307
+ return new Date(a.created_at).getTime() - new Date(b.created_at).getTime();
308
+ }
309
+ return a.seq - b.seq;
310
+ });
311
+ // Render timeline, associating replies with preceding dispatches
312
+ let lastDispatchSeq;
313
+ const rendered = [];
314
+ timeline.forEach((entry) => {
315
+ if (entry.type === "dispatch" && entry.dispatch) {
316
+ lastDispatchSeq = entry.dispatch.seq;
317
+ const isLatest = entry.dispatch.seq === maxDispatchSeq;
318
+ rendered.push(renderDispatch(entry.dispatch, isLatest, runStatus));
319
+ }
320
+ else if (entry.type === "reply" && entry.reply) {
321
+ rendered.push(renderReply(entry.reply, lastDispatchSeq));
322
+ }
323
+ });
324
+ return rendered.join("");
325
+ }
326
+ async function loadThread(runId) {
327
+ selectedRunId = runId;
328
+ if (!detailEl)
329
+ return;
330
+ detailEl.innerHTML = "Loading…";
331
+ if (!isRepoHealthy()) {
332
+ detailEl.innerHTML = "<div class=\"muted\">Repo offline or uninitialized.</div>";
333
+ return;
334
+ }
335
+ let detail;
336
+ try {
337
+ detail = (await api(`/api/messages/threads/${encodeURIComponent(runId)}`));
338
+ }
339
+ catch (_err) {
340
+ detailEl.innerHTML = "";
341
+ flash("Failed to load message thread", "error");
342
+ return;
343
+ }
344
+ const runStatus = (detail.run?.status || "").toString();
345
+ const isPaused = runStatus === "paused";
346
+ const dispatchHistory = detail.dispatch_history || [];
347
+ const replyHistory = detail.reply_history || [];
348
+ const dispatchCount = detail.dispatch_count ?? dispatchHistory.length;
349
+ const replyCount = detail.reply_count ?? replyHistory.length;
350
+ const ticketState = detail.ticket_state;
351
+ const turns = ticketState?.total_turns ?? null;
352
+ // Truncate run ID for display
353
+ const shortRunId = runId.length > 12 ? runId.slice(0, 8) + "…" : runId;
354
+ // Build compact stats line
355
+ const statsParts = [];
356
+ statsParts.push(`${dispatchCount} dispatch${dispatchCount !== 1 ? "es" : ""}`);
357
+ statsParts.push(`${replyCount} repl${replyCount !== 1 ? "ies" : "y"}`);
358
+ if (turns != null)
359
+ statsParts.push(`${turns} turn${turns !== 1 ? "s" : ""}`);
360
+ const statsLine = statsParts.join(" · ");
361
+ // Status pill
362
+ const statusPillClass = isPaused ? "pill-action" : "pill-idle";
363
+ const statusLabel = isPaused ? "paused" : runStatus || "idle";
364
+ // Build threaded timeline
365
+ const threadedContent = buildThreadedTimeline(dispatchHistory, replyHistory, runStatus);
366
+ detailEl.innerHTML = `
367
+ <div class="messages-thread-history">
368
+ ${threadedContent || '<div class="muted">No dispatches yet</div>'}
369
+ </div>
370
+ <div class="messages-thread-footer">
371
+ <code title="${escapeHtml(runId)}">${escapeHtml(shortRunId)}</code>
372
+ <span class="pill pill-small ${statusPillClass}">${escapeHtml(statusLabel)}</span>
373
+ <span class="messages-footer-stats">${escapeHtml(statsLine)}</span>
374
+ </div>
375
+ `;
376
+ // Only show reply box for paused runs - replies to other states won't be seen
377
+ const replyBoxEl = document.querySelector(".messages-reply-box");
378
+ if (replyBoxEl) {
379
+ replyBoxEl.classList.toggle("hidden", !isPaused);
380
+ }
381
+ // Always scroll to bottom of the thread detail (the scrollable container)
382
+ requestAnimationFrame(() => {
383
+ if (detailEl) {
384
+ detailEl.scrollTop = detailEl.scrollHeight;
385
+ }
386
+ });
387
+ }
388
+ async function sendReply() {
389
+ const runId = selectedRunId;
390
+ if (!runId) {
391
+ flash("Select a message thread first", "error");
392
+ return;
393
+ }
394
+ if (!isRepoHealthy()) {
395
+ flash("Repo offline; cannot send reply.", "error");
396
+ return;
397
+ }
398
+ const body = replyBodyEl?.value || "";
399
+ const fd = new FormData();
400
+ fd.append("body", body);
401
+ if (replyFilesEl?.files) {
402
+ Array.from(replyFilesEl.files).forEach((f) => fd.append("files", f));
403
+ }
404
+ try {
405
+ await api(`/api/messages/${encodeURIComponent(runId)}/reply`, {
406
+ method: "POST",
407
+ body: fd,
408
+ });
409
+ if (replyBodyEl)
410
+ replyBodyEl.value = "";
411
+ if (replyFilesEl)
412
+ replyFilesEl.value = "";
413
+ flash("Reply sent", "success");
414
+ // Always resume after sending
415
+ await api(`/api/flows/${encodeURIComponent(runId)}/resume`, { method: "POST" });
416
+ flash("Run resumed", "success");
417
+ void refreshBell();
418
+ void loadThread(runId);
419
+ }
420
+ catch (_err) {
421
+ flash("Failed to send reply", "error");
422
+ }
423
+ }
424
+ export function initMessages() {
425
+ if (messagesInitialized)
426
+ return;
427
+ if (!threadsEl || !detailEl)
428
+ return;
429
+ messagesInitialized = true;
430
+ refreshEl?.addEventListener("click", () => {
431
+ void loadThreads();
432
+ const runId = selectedRunId;
433
+ if (runId)
434
+ void loadThread(runId);
435
+ });
436
+ replySendEl?.addEventListener("click", () => {
437
+ void sendReply();
438
+ });
439
+ // Load threads immediately, and try to open run_id from URL if present.
440
+ void loadThreads().then(() => {
441
+ const params = getUrlParams();
442
+ const runId = params.get("run_id");
443
+ if (runId) {
444
+ selectedRunId = runId;
445
+ void loadThread(runId);
446
+ return;
447
+ }
448
+ // Fall back to active message if any.
449
+ if (activeRunId) {
450
+ selectedRunId = activeRunId;
451
+ updateUrlParams({ run_id: activeRunId });
452
+ void loadThread(activeRunId);
453
+ }
454
+ });
455
+ subscribe("tab:change", (tabId) => {
456
+ if (tabId === "inbox") {
457
+ void refreshBell();
458
+ void loadThreads();
459
+ const params = getUrlParams();
460
+ const runId = params.get("run_id");
461
+ if (runId) {
462
+ selectedRunId = runId;
463
+ void loadThread(runId);
464
+ }
465
+ }
466
+ });
467
+ subscribe("state:update", () => {
468
+ void refreshBell();
469
+ });
470
+ }
@@ -1,3 +1,4 @@
1
+ // GENERATED FILE - do not edit directly. Source: static_src/
1
2
  import { isMobileViewport, setMobileChromeHidden, setMobileComposeFixed, } from "./utils.js";
2
3
  import { subscribe } from "./bus.js";
3
4
  import { getTerminalManager } from "./terminal.js";
@@ -100,7 +101,7 @@ function updateMobileControlsOffset() {
100
101
  document.documentElement.style.setProperty("--compose-total-height", `${totalHeight}px`);
101
102
  }
102
103
  function updateDocComposeOffset() {
103
- const composePanel = document.querySelector("#docs .doc-chat-panel");
104
+ const composePanel = document.querySelector("#workspace .doc-chat-panel, #workspace .ticket-chat-panel");
104
105
  if (!composePanel || !isVisible(composePanel))
105
106
  return;
106
107
  const composeHeight = composePanel.offsetHeight || 0;