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.
- codex_autorunner/__main__.py +4 -0
- codex_autorunner/agents/opencode/client.py +68 -35
- codex_autorunner/agents/opencode/logging.py +21 -5
- codex_autorunner/agents/opencode/run_prompt.py +1 -0
- codex_autorunner/agents/opencode/runtime.py +118 -30
- codex_autorunner/agents/opencode/supervisor.py +36 -48
- codex_autorunner/agents/registry.py +136 -8
- codex_autorunner/api.py +25 -0
- codex_autorunner/bootstrap.py +16 -35
- codex_autorunner/cli.py +157 -139
- codex_autorunner/core/about_car.py +44 -32
- codex_autorunner/core/adapter_utils.py +21 -0
- codex_autorunner/core/app_server_logging.py +7 -3
- codex_autorunner/core/app_server_prompts.py +27 -260
- codex_autorunner/core/app_server_threads.py +15 -26
- codex_autorunner/core/codex_runner.py +6 -0
- codex_autorunner/core/config.py +390 -100
- codex_autorunner/core/docs.py +10 -2
- codex_autorunner/core/drafts.py +82 -0
- codex_autorunner/core/engine.py +278 -262
- codex_autorunner/core/flows/__init__.py +25 -0
- codex_autorunner/core/flows/controller.py +178 -0
- codex_autorunner/core/flows/definition.py +82 -0
- codex_autorunner/core/flows/models.py +75 -0
- codex_autorunner/core/flows/runtime.py +351 -0
- codex_autorunner/core/flows/store.py +485 -0
- codex_autorunner/core/flows/transition.py +133 -0
- codex_autorunner/core/flows/worker_process.py +242 -0
- codex_autorunner/core/hub.py +15 -9
- codex_autorunner/core/locks.py +4 -0
- codex_autorunner/core/prompt.py +15 -7
- codex_autorunner/core/redaction.py +29 -0
- codex_autorunner/core/review_context.py +5 -8
- codex_autorunner/core/run_index.py +6 -0
- codex_autorunner/core/runner_process.py +5 -2
- codex_autorunner/core/state.py +0 -88
- codex_autorunner/core/static_assets.py +55 -0
- codex_autorunner/core/supervisor_utils.py +67 -0
- codex_autorunner/core/update.py +20 -11
- codex_autorunner/core/update_runner.py +2 -0
- codex_autorunner/core/utils.py +29 -2
- codex_autorunner/discovery.py +2 -4
- codex_autorunner/flows/ticket_flow/__init__.py +3 -0
- codex_autorunner/flows/ticket_flow/definition.py +91 -0
- codex_autorunner/integrations/agents/__init__.py +27 -0
- codex_autorunner/integrations/agents/agent_backend.py +142 -0
- codex_autorunner/integrations/agents/codex_backend.py +307 -0
- codex_autorunner/integrations/agents/opencode_backend.py +325 -0
- codex_autorunner/integrations/agents/run_event.py +71 -0
- codex_autorunner/integrations/app_server/client.py +576 -92
- codex_autorunner/integrations/app_server/supervisor.py +59 -33
- codex_autorunner/integrations/telegram/adapter.py +141 -167
- codex_autorunner/integrations/telegram/api_schemas.py +120 -0
- codex_autorunner/integrations/telegram/config.py +175 -0
- codex_autorunner/integrations/telegram/constants.py +16 -1
- codex_autorunner/integrations/telegram/dispatch.py +17 -0
- codex_autorunner/integrations/telegram/doctor.py +47 -0
- codex_autorunner/integrations/telegram/handlers/callbacks.py +0 -4
- codex_autorunner/integrations/telegram/handlers/commands/__init__.py +2 -0
- codex_autorunner/integrations/telegram/handlers/commands/execution.py +53 -57
- codex_autorunner/integrations/telegram/handlers/commands/files.py +2 -6
- codex_autorunner/integrations/telegram/handlers/commands/flows.py +227 -0
- codex_autorunner/integrations/telegram/handlers/commands/formatting.py +1 -1
- codex_autorunner/integrations/telegram/handlers/commands/github.py +41 -582
- codex_autorunner/integrations/telegram/handlers/commands/workspace.py +8 -8
- codex_autorunner/integrations/telegram/handlers/commands_runtime.py +133 -475
- codex_autorunner/integrations/telegram/handlers/commands_spec.py +11 -4
- codex_autorunner/integrations/telegram/handlers/messages.py +120 -9
- codex_autorunner/integrations/telegram/helpers.py +88 -16
- codex_autorunner/integrations/telegram/outbox.py +208 -37
- codex_autorunner/integrations/telegram/progress_stream.py +3 -10
- codex_autorunner/integrations/telegram/service.py +214 -40
- codex_autorunner/integrations/telegram/state.py +100 -2
- codex_autorunner/integrations/telegram/ticket_flow_bridge.py +322 -0
- codex_autorunner/integrations/telegram/transport.py +36 -3
- codex_autorunner/integrations/telegram/trigger_mode.py +53 -0
- codex_autorunner/manifest.py +2 -0
- codex_autorunner/plugin_api.py +22 -0
- codex_autorunner/routes/__init__.py +23 -14
- codex_autorunner/routes/analytics.py +239 -0
- codex_autorunner/routes/base.py +81 -109
- codex_autorunner/routes/file_chat.py +836 -0
- codex_autorunner/routes/flows.py +980 -0
- codex_autorunner/routes/messages.py +459 -0
- codex_autorunner/routes/system.py +6 -1
- codex_autorunner/routes/usage.py +87 -0
- codex_autorunner/routes/workspace.py +271 -0
- codex_autorunner/server.py +2 -1
- codex_autorunner/static/agentControls.js +1 -0
- codex_autorunner/static/agentEvents.js +248 -0
- codex_autorunner/static/app.js +25 -22
- codex_autorunner/static/autoRefresh.js +29 -1
- codex_autorunner/static/bootstrap.js +1 -0
- codex_autorunner/static/bus.js +1 -0
- codex_autorunner/static/cache.js +1 -0
- codex_autorunner/static/constants.js +20 -4
- codex_autorunner/static/dashboard.js +162 -196
- codex_autorunner/static/diffRenderer.js +37 -0
- codex_autorunner/static/docChatCore.js +324 -0
- codex_autorunner/static/docChatStorage.js +65 -0
- codex_autorunner/static/docChatVoice.js +65 -0
- codex_autorunner/static/docEditor.js +133 -0
- codex_autorunner/static/env.js +1 -0
- codex_autorunner/static/eventSummarizer.js +166 -0
- codex_autorunner/static/fileChat.js +182 -0
- codex_autorunner/static/health.js +155 -0
- codex_autorunner/static/hub.js +41 -118
- codex_autorunner/static/index.html +787 -858
- codex_autorunner/static/liveUpdates.js +1 -0
- codex_autorunner/static/loader.js +1 -0
- codex_autorunner/static/messages.js +470 -0
- codex_autorunner/static/mobileCompact.js +2 -1
- codex_autorunner/static/settings.js +24 -211
- codex_autorunner/static/styles.css +7567 -3865
- codex_autorunner/static/tabs.js +28 -5
- codex_autorunner/static/terminal.js +14 -0
- codex_autorunner/static/terminalManager.js +34 -59
- codex_autorunner/static/ticketChatActions.js +333 -0
- codex_autorunner/static/ticketChatEvents.js +16 -0
- codex_autorunner/static/ticketChatStorage.js +16 -0
- codex_autorunner/static/ticketChatStream.js +264 -0
- codex_autorunner/static/ticketEditor.js +750 -0
- codex_autorunner/static/ticketVoice.js +9 -0
- codex_autorunner/static/tickets.js +1315 -0
- codex_autorunner/static/utils.js +32 -3
- codex_autorunner/static/voice.js +1 -0
- codex_autorunner/static/workspace.js +672 -0
- codex_autorunner/static/workspaceApi.js +53 -0
- codex_autorunner/static/workspaceFileBrowser.js +504 -0
- codex_autorunner/tickets/__init__.py +20 -0
- codex_autorunner/tickets/agent_pool.py +377 -0
- codex_autorunner/tickets/files.py +85 -0
- codex_autorunner/tickets/frontmatter.py +55 -0
- codex_autorunner/tickets/lint.py +102 -0
- codex_autorunner/tickets/models.py +95 -0
- codex_autorunner/tickets/outbox.py +232 -0
- codex_autorunner/tickets/replies.py +179 -0
- codex_autorunner/tickets/runner.py +823 -0
- codex_autorunner/tickets/spec_ingest.py +77 -0
- codex_autorunner/web/app.py +269 -91
- codex_autorunner/web/middleware.py +3 -4
- codex_autorunner/web/schemas.py +89 -109
- codex_autorunner/web/static_assets.py +1 -44
- codex_autorunner/workspace/__init__.py +40 -0
- codex_autorunner/workspace/paths.py +319 -0
- {codex_autorunner-0.1.2.dist-info → codex_autorunner-1.0.0.dist-info}/METADATA +18 -21
- codex_autorunner-1.0.0.dist-info/RECORD +251 -0
- {codex_autorunner-0.1.2.dist-info → codex_autorunner-1.0.0.dist-info}/WHEEL +1 -1
- codex_autorunner/agents/execution/policy.py +0 -292
- codex_autorunner/agents/factory.py +0 -52
- codex_autorunner/agents/orchestrator.py +0 -358
- codex_autorunner/core/doc_chat.py +0 -1446
- codex_autorunner/core/snapshot.py +0 -580
- codex_autorunner/integrations/github/chatops.py +0 -268
- codex_autorunner/integrations/github/pr_flow.py +0 -1314
- codex_autorunner/routes/docs.py +0 -381
- codex_autorunner/routes/github.py +0 -327
- codex_autorunner/routes/runs.py +0 -250
- codex_autorunner/spec_ingest.py +0 -812
- codex_autorunner/static/docChatActions.js +0 -287
- codex_autorunner/static/docChatEvents.js +0 -300
- codex_autorunner/static/docChatRender.js +0 -205
- codex_autorunner/static/docChatStream.js +0 -361
- codex_autorunner/static/docs.js +0 -20
- codex_autorunner/static/docsClipboard.js +0 -69
- codex_autorunner/static/docsCrud.js +0 -257
- codex_autorunner/static/docsDocUpdates.js +0 -62
- codex_autorunner/static/docsDrafts.js +0 -16
- codex_autorunner/static/docsElements.js +0 -69
- codex_autorunner/static/docsInit.js +0 -285
- codex_autorunner/static/docsParse.js +0 -160
- codex_autorunner/static/docsSnapshot.js +0 -87
- codex_autorunner/static/docsSpecIngest.js +0 -263
- codex_autorunner/static/docsState.js +0 -127
- codex_autorunner/static/docsThreadRegistry.js +0 -44
- codex_autorunner/static/docsUi.js +0 -153
- codex_autorunner/static/docsVoice.js +0 -56
- codex_autorunner/static/github.js +0 -504
- codex_autorunner/static/logs.js +0 -678
- codex_autorunner/static/review.js +0 -157
- codex_autorunner/static/runs.js +0 -418
- codex_autorunner/static/snapshot.js +0 -124
- codex_autorunner/static/state.js +0 -94
- codex_autorunner/static/todoPreview.js +0 -27
- codex_autorunner/workspace.py +0 -16
- codex_autorunner-0.1.2.dist-info/RECORD +0 -222
- {codex_autorunner-0.1.2.dist-info → codex_autorunner-1.0.0.dist-info}/entry_points.txt +0 -0
- {codex_autorunner-0.1.2.dist-info → codex_autorunner-1.0.0.dist-info}/licenses/LICENSE +0 -0
- {codex_autorunner-0.1.2.dist-info → codex_autorunner-1.0.0.dist-info}/top_level.txt +0 -0
|
@@ -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("#
|
|
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;
|