codex-autorunner 0.1.1__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 (226) hide show
  1. codex_autorunner/__main__.py +4 -0
  2. codex_autorunner/agents/__init__.py +20 -0
  3. codex_autorunner/agents/base.py +2 -2
  4. codex_autorunner/agents/codex/harness.py +1 -1
  5. codex_autorunner/agents/opencode/__init__.py +4 -0
  6. codex_autorunner/agents/opencode/agent_config.py +104 -0
  7. codex_autorunner/agents/opencode/client.py +305 -28
  8. codex_autorunner/agents/opencode/harness.py +71 -20
  9. codex_autorunner/agents/opencode/logging.py +225 -0
  10. codex_autorunner/agents/opencode/run_prompt.py +261 -0
  11. codex_autorunner/agents/opencode/runtime.py +1202 -132
  12. codex_autorunner/agents/opencode/supervisor.py +194 -68
  13. codex_autorunner/agents/registry.py +258 -0
  14. codex_autorunner/agents/types.py +2 -2
  15. codex_autorunner/api.py +25 -0
  16. codex_autorunner/bootstrap.py +19 -40
  17. codex_autorunner/cli.py +234 -151
  18. codex_autorunner/core/about_car.py +44 -32
  19. codex_autorunner/core/adapter_utils.py +21 -0
  20. codex_autorunner/core/app_server_events.py +15 -6
  21. codex_autorunner/core/app_server_logging.py +55 -15
  22. codex_autorunner/core/app_server_prompts.py +28 -259
  23. codex_autorunner/core/app_server_threads.py +15 -26
  24. codex_autorunner/core/circuit_breaker.py +183 -0
  25. codex_autorunner/core/codex_runner.py +6 -0
  26. codex_autorunner/core/config.py +555 -133
  27. codex_autorunner/core/docs.py +54 -9
  28. codex_autorunner/core/drafts.py +82 -0
  29. codex_autorunner/core/engine.py +828 -274
  30. codex_autorunner/core/exceptions.py +60 -0
  31. codex_autorunner/core/flows/__init__.py +25 -0
  32. codex_autorunner/core/flows/controller.py +178 -0
  33. codex_autorunner/core/flows/definition.py +82 -0
  34. codex_autorunner/core/flows/models.py +75 -0
  35. codex_autorunner/core/flows/runtime.py +351 -0
  36. codex_autorunner/core/flows/store.py +485 -0
  37. codex_autorunner/core/flows/transition.py +133 -0
  38. codex_autorunner/core/flows/worker_process.py +242 -0
  39. codex_autorunner/core/hub.py +21 -13
  40. codex_autorunner/core/locks.py +118 -1
  41. codex_autorunner/core/logging_utils.py +9 -6
  42. codex_autorunner/core/path_utils.py +123 -0
  43. codex_autorunner/core/prompt.py +15 -7
  44. codex_autorunner/core/redaction.py +29 -0
  45. codex_autorunner/core/retry.py +61 -0
  46. codex_autorunner/core/review.py +888 -0
  47. codex_autorunner/core/review_context.py +161 -0
  48. codex_autorunner/core/run_index.py +223 -0
  49. codex_autorunner/core/runner_controller.py +44 -1
  50. codex_autorunner/core/runner_process.py +30 -1
  51. codex_autorunner/core/sqlite_utils.py +32 -0
  52. codex_autorunner/core/state.py +273 -44
  53. codex_autorunner/core/static_assets.py +55 -0
  54. codex_autorunner/core/supervisor_utils.py +67 -0
  55. codex_autorunner/core/text_delta_coalescer.py +43 -0
  56. codex_autorunner/core/update.py +20 -11
  57. codex_autorunner/core/update_runner.py +2 -0
  58. codex_autorunner/core/usage.py +107 -75
  59. codex_autorunner/core/utils.py +167 -3
  60. codex_autorunner/discovery.py +3 -3
  61. codex_autorunner/flows/ticket_flow/__init__.py +3 -0
  62. codex_autorunner/flows/ticket_flow/definition.py +91 -0
  63. codex_autorunner/integrations/agents/__init__.py +27 -0
  64. codex_autorunner/integrations/agents/agent_backend.py +142 -0
  65. codex_autorunner/integrations/agents/codex_backend.py +307 -0
  66. codex_autorunner/integrations/agents/opencode_backend.py +325 -0
  67. codex_autorunner/integrations/agents/run_event.py +71 -0
  68. codex_autorunner/integrations/app_server/client.py +708 -153
  69. codex_autorunner/integrations/app_server/supervisor.py +59 -33
  70. codex_autorunner/integrations/telegram/adapter.py +474 -185
  71. codex_autorunner/integrations/telegram/api_schemas.py +120 -0
  72. codex_autorunner/integrations/telegram/config.py +239 -1
  73. codex_autorunner/integrations/telegram/constants.py +19 -1
  74. codex_autorunner/integrations/telegram/dispatch.py +44 -8
  75. codex_autorunner/integrations/telegram/doctor.py +47 -0
  76. codex_autorunner/integrations/telegram/handlers/approvals.py +12 -10
  77. codex_autorunner/integrations/telegram/handlers/callbacks.py +15 -1
  78. codex_autorunner/integrations/telegram/handlers/commands/__init__.py +29 -0
  79. codex_autorunner/integrations/telegram/handlers/commands/approvals.py +173 -0
  80. codex_autorunner/integrations/telegram/handlers/commands/execution.py +2595 -0
  81. codex_autorunner/integrations/telegram/handlers/commands/files.py +1408 -0
  82. codex_autorunner/integrations/telegram/handlers/commands/flows.py +227 -0
  83. codex_autorunner/integrations/telegram/handlers/commands/formatting.py +81 -0
  84. codex_autorunner/integrations/telegram/handlers/commands/github.py +1688 -0
  85. codex_autorunner/integrations/telegram/handlers/commands/shared.py +190 -0
  86. codex_autorunner/integrations/telegram/handlers/commands/voice.py +112 -0
  87. codex_autorunner/integrations/telegram/handlers/commands/workspace.py +2043 -0
  88. codex_autorunner/integrations/telegram/handlers/commands_runtime.py +954 -5689
  89. codex_autorunner/integrations/telegram/handlers/{commands.py → commands_spec.py} +11 -4
  90. codex_autorunner/integrations/telegram/handlers/messages.py +374 -49
  91. codex_autorunner/integrations/telegram/handlers/questions.py +389 -0
  92. codex_autorunner/integrations/telegram/handlers/selections.py +6 -4
  93. codex_autorunner/integrations/telegram/handlers/utils.py +171 -0
  94. codex_autorunner/integrations/telegram/helpers.py +90 -18
  95. codex_autorunner/integrations/telegram/notifications.py +126 -35
  96. codex_autorunner/integrations/telegram/outbox.py +214 -43
  97. codex_autorunner/integrations/telegram/progress_stream.py +42 -19
  98. codex_autorunner/integrations/telegram/runtime.py +24 -13
  99. codex_autorunner/integrations/telegram/service.py +500 -129
  100. codex_autorunner/integrations/telegram/state.py +1278 -330
  101. codex_autorunner/integrations/telegram/ticket_flow_bridge.py +322 -0
  102. codex_autorunner/integrations/telegram/transport.py +37 -4
  103. codex_autorunner/integrations/telegram/trigger_mode.py +53 -0
  104. codex_autorunner/integrations/telegram/types.py +22 -2
  105. codex_autorunner/integrations/telegram/voice.py +14 -15
  106. codex_autorunner/manifest.py +2 -0
  107. codex_autorunner/plugin_api.py +22 -0
  108. codex_autorunner/routes/__init__.py +25 -14
  109. codex_autorunner/routes/agents.py +18 -78
  110. codex_autorunner/routes/analytics.py +239 -0
  111. codex_autorunner/routes/base.py +142 -113
  112. codex_autorunner/routes/file_chat.py +836 -0
  113. codex_autorunner/routes/flows.py +980 -0
  114. codex_autorunner/routes/messages.py +459 -0
  115. codex_autorunner/routes/repos.py +17 -0
  116. codex_autorunner/routes/review.py +148 -0
  117. codex_autorunner/routes/sessions.py +16 -8
  118. codex_autorunner/routes/settings.py +22 -0
  119. codex_autorunner/routes/shared.py +33 -3
  120. codex_autorunner/routes/system.py +22 -1
  121. codex_autorunner/routes/usage.py +87 -0
  122. codex_autorunner/routes/voice.py +5 -13
  123. codex_autorunner/routes/workspace.py +271 -0
  124. codex_autorunner/server.py +2 -1
  125. codex_autorunner/static/agentControls.js +9 -1
  126. codex_autorunner/static/agentEvents.js +248 -0
  127. codex_autorunner/static/app.js +27 -22
  128. codex_autorunner/static/autoRefresh.js +29 -1
  129. codex_autorunner/static/bootstrap.js +1 -0
  130. codex_autorunner/static/bus.js +1 -0
  131. codex_autorunner/static/cache.js +1 -0
  132. codex_autorunner/static/constants.js +20 -4
  133. codex_autorunner/static/dashboard.js +162 -150
  134. codex_autorunner/static/diffRenderer.js +37 -0
  135. codex_autorunner/static/docChatCore.js +324 -0
  136. codex_autorunner/static/docChatStorage.js +65 -0
  137. codex_autorunner/static/docChatVoice.js +65 -0
  138. codex_autorunner/static/docEditor.js +133 -0
  139. codex_autorunner/static/env.js +1 -0
  140. codex_autorunner/static/eventSummarizer.js +166 -0
  141. codex_autorunner/static/fileChat.js +182 -0
  142. codex_autorunner/static/health.js +155 -0
  143. codex_autorunner/static/hub.js +67 -126
  144. codex_autorunner/static/index.html +788 -807
  145. codex_autorunner/static/liveUpdates.js +59 -0
  146. codex_autorunner/static/loader.js +1 -0
  147. codex_autorunner/static/messages.js +470 -0
  148. codex_autorunner/static/mobileCompact.js +2 -1
  149. codex_autorunner/static/settings.js +24 -205
  150. codex_autorunner/static/styles.css +7577 -3758
  151. codex_autorunner/static/tabs.js +28 -5
  152. codex_autorunner/static/terminal.js +14 -0
  153. codex_autorunner/static/terminalManager.js +53 -59
  154. codex_autorunner/static/ticketChatActions.js +333 -0
  155. codex_autorunner/static/ticketChatEvents.js +16 -0
  156. codex_autorunner/static/ticketChatStorage.js +16 -0
  157. codex_autorunner/static/ticketChatStream.js +264 -0
  158. codex_autorunner/static/ticketEditor.js +750 -0
  159. codex_autorunner/static/ticketVoice.js +9 -0
  160. codex_autorunner/static/tickets.js +1315 -0
  161. codex_autorunner/static/utils.js +32 -3
  162. codex_autorunner/static/voice.js +21 -7
  163. codex_autorunner/static/workspace.js +672 -0
  164. codex_autorunner/static/workspaceApi.js +53 -0
  165. codex_autorunner/static/workspaceFileBrowser.js +504 -0
  166. codex_autorunner/tickets/__init__.py +20 -0
  167. codex_autorunner/tickets/agent_pool.py +377 -0
  168. codex_autorunner/tickets/files.py +85 -0
  169. codex_autorunner/tickets/frontmatter.py +55 -0
  170. codex_autorunner/tickets/lint.py +102 -0
  171. codex_autorunner/tickets/models.py +95 -0
  172. codex_autorunner/tickets/outbox.py +232 -0
  173. codex_autorunner/tickets/replies.py +179 -0
  174. codex_autorunner/tickets/runner.py +823 -0
  175. codex_autorunner/tickets/spec_ingest.py +77 -0
  176. codex_autorunner/voice/capture.py +7 -7
  177. codex_autorunner/voice/service.py +51 -9
  178. codex_autorunner/web/app.py +419 -199
  179. codex_autorunner/web/hub_jobs.py +13 -2
  180. codex_autorunner/web/middleware.py +47 -13
  181. codex_autorunner/web/pty_session.py +26 -13
  182. codex_autorunner/web/schemas.py +114 -109
  183. codex_autorunner/web/static_assets.py +55 -42
  184. codex_autorunner/web/static_refresh.py +86 -0
  185. codex_autorunner/workspace/__init__.py +40 -0
  186. codex_autorunner/workspace/paths.py +319 -0
  187. {codex_autorunner-0.1.1.dist-info → codex_autorunner-1.0.0.dist-info}/METADATA +20 -21
  188. codex_autorunner-1.0.0.dist-info/RECORD +251 -0
  189. {codex_autorunner-0.1.1.dist-info → codex_autorunner-1.0.0.dist-info}/WHEEL +1 -1
  190. codex_autorunner/core/doc_chat.py +0 -1415
  191. codex_autorunner/core/snapshot.py +0 -580
  192. codex_autorunner/integrations/github/chatops.py +0 -268
  193. codex_autorunner/integrations/github/pr_flow.py +0 -1314
  194. codex_autorunner/routes/docs.py +0 -381
  195. codex_autorunner/routes/github.py +0 -327
  196. codex_autorunner/routes/runs.py +0 -118
  197. codex_autorunner/spec_ingest.py +0 -788
  198. codex_autorunner/static/docChatActions.js +0 -279
  199. codex_autorunner/static/docChatEvents.js +0 -300
  200. codex_autorunner/static/docChatRender.js +0 -205
  201. codex_autorunner/static/docChatStream.js +0 -361
  202. codex_autorunner/static/docs.js +0 -20
  203. codex_autorunner/static/docsClipboard.js +0 -69
  204. codex_autorunner/static/docsCrud.js +0 -257
  205. codex_autorunner/static/docsDocUpdates.js +0 -62
  206. codex_autorunner/static/docsDrafts.js +0 -16
  207. codex_autorunner/static/docsElements.js +0 -69
  208. codex_autorunner/static/docsInit.js +0 -274
  209. codex_autorunner/static/docsParse.js +0 -160
  210. codex_autorunner/static/docsSnapshot.js +0 -87
  211. codex_autorunner/static/docsSpecIngest.js +0 -263
  212. codex_autorunner/static/docsState.js +0 -127
  213. codex_autorunner/static/docsThreadRegistry.js +0 -44
  214. codex_autorunner/static/docsUi.js +0 -153
  215. codex_autorunner/static/docsVoice.js +0 -56
  216. codex_autorunner/static/github.js +0 -442
  217. codex_autorunner/static/logs.js +0 -640
  218. codex_autorunner/static/runs.js +0 -409
  219. codex_autorunner/static/snapshot.js +0 -124
  220. codex_autorunner/static/state.js +0 -86
  221. codex_autorunner/static/todoPreview.js +0 -27
  222. codex_autorunner/workspace.py +0 -16
  223. codex_autorunner-0.1.1.dist-info/RECORD +0 -191
  224. {codex_autorunner-0.1.1.dist-info → codex_autorunner-1.0.0.dist-info}/entry_points.txt +0 -0
  225. {codex_autorunner-0.1.1.dist-info → codex_autorunner-1.0.0.dist-info}/licenses/LICENSE +0 -0
  226. {codex_autorunner-0.1.1.dist-info → codex_autorunner-1.0.0.dist-info}/top_level.txt +0 -0
@@ -0,0 +1,59 @@
1
+ // GENERATED FILE - do not edit directly. Source: static_src/
2
+ import { publish, subscribe } from "./bus.js";
3
+ const INVALIDATION_DEBOUNCE_MS = 750;
4
+ let initialized = false;
5
+ let lastState = null;
6
+ let flushTimer = null;
7
+ const pendingInvalidations = new Set();
8
+ function normalizeState(payload) {
9
+ return {
10
+ last_run_id: payload.last_run_id ?? null,
11
+ last_run_finished_at: payload.last_run_finished_at ?? null,
12
+ outstanding_count: payload.outstanding_count ?? null,
13
+ done_count: payload.done_count ?? null,
14
+ status: payload.status ?? null,
15
+ runner_pid: payload.runner_pid ?? null,
16
+ };
17
+ }
18
+ function queueInvalidation(key) {
19
+ pendingInvalidations.add(key);
20
+ if (flushTimer)
21
+ return;
22
+ flushTimer = setTimeout(flushInvalidations, INVALIDATION_DEBOUNCE_MS);
23
+ }
24
+ function flushInvalidations() {
25
+ flushTimer = null;
26
+ if (!pendingInvalidations.size)
27
+ return;
28
+ const keys = Array.from(pendingInvalidations);
29
+ pendingInvalidations.clear();
30
+ keys.forEach((key) => publish(key, { source: "state" }));
31
+ }
32
+ function handleStateUpdate(payload) {
33
+ if (!payload || typeof payload !== "object")
34
+ return;
35
+ const next = normalizeState(payload);
36
+ if (!lastState) {
37
+ lastState = next;
38
+ return;
39
+ }
40
+ if (lastState.last_run_id !== next.last_run_id ||
41
+ lastState.last_run_finished_at !== next.last_run_finished_at) {
42
+ queueInvalidation("runs:invalidate");
43
+ }
44
+ if (lastState.outstanding_count !== next.outstanding_count ||
45
+ lastState.done_count !== next.done_count) {
46
+ queueInvalidation("todo:invalidate");
47
+ }
48
+ if (lastState.status !== next.status ||
49
+ lastState.runner_pid !== next.runner_pid) {
50
+ queueInvalidation("runner:status");
51
+ }
52
+ lastState = next;
53
+ }
54
+ export function initLiveUpdates() {
55
+ if (initialized)
56
+ return;
57
+ initialized = true;
58
+ subscribe("state:update", handleStateUpdate);
59
+ }
@@ -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;