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
|
@@ -1,157 +0,0 @@
|
|
|
1
|
-
import { api, flash, statusPill } from "./utils.js";
|
|
2
|
-
function $(id) {
|
|
3
|
-
return document.getElementById(id);
|
|
4
|
-
}
|
|
5
|
-
function setText(el, text) {
|
|
6
|
-
if (!el)
|
|
7
|
-
return;
|
|
8
|
-
el.textContent = text ?? "–";
|
|
9
|
-
}
|
|
10
|
-
function setLink(el, { href, text, title } = {}) {
|
|
11
|
-
if (!el)
|
|
12
|
-
return;
|
|
13
|
-
if (href) {
|
|
14
|
-
el.href = href;
|
|
15
|
-
el.target = "_blank";
|
|
16
|
-
el.rel = "noopener noreferrer";
|
|
17
|
-
el.classList.remove("muted");
|
|
18
|
-
el.textContent = text || href;
|
|
19
|
-
if (title)
|
|
20
|
-
el.title = title;
|
|
21
|
-
}
|
|
22
|
-
else {
|
|
23
|
-
el.removeAttribute("href");
|
|
24
|
-
el.removeAttribute("target");
|
|
25
|
-
el.removeAttribute("rel");
|
|
26
|
-
el.classList.add("muted");
|
|
27
|
-
el.textContent = text || "–";
|
|
28
|
-
if (title)
|
|
29
|
-
el.title = title;
|
|
30
|
-
}
|
|
31
|
-
}
|
|
32
|
-
let reviewInterval = null;
|
|
33
|
-
async function loadReviewStatus() {
|
|
34
|
-
try {
|
|
35
|
-
const data = (await api("/api/review/status"));
|
|
36
|
-
const review = data.review || {};
|
|
37
|
-
const statusPillEl = $("review-status-pill");
|
|
38
|
-
const runIdEl = $("review-run-id");
|
|
39
|
-
const startedEl = $("review-started");
|
|
40
|
-
const finishedEl = $("review-finished");
|
|
41
|
-
const startBtn = $("review-start");
|
|
42
|
-
const stopBtn = $("review-stop");
|
|
43
|
-
const resetBtn = $("review-reset");
|
|
44
|
-
const status = review.status || "idle";
|
|
45
|
-
const running = review.running || false;
|
|
46
|
-
statusPill(statusPillEl, status);
|
|
47
|
-
setText(runIdEl, review.id);
|
|
48
|
-
setText(startedEl, review.started_at);
|
|
49
|
-
setText(finishedEl, review.finished_at);
|
|
50
|
-
if (startBtn) {
|
|
51
|
-
startBtn.disabled = running;
|
|
52
|
-
}
|
|
53
|
-
if (stopBtn) {
|
|
54
|
-
stopBtn.disabled = !running || status === "stopped";
|
|
55
|
-
}
|
|
56
|
-
if (resetBtn) {
|
|
57
|
-
resetBtn.disabled = running;
|
|
58
|
-
}
|
|
59
|
-
const finalLink = $("review-final-link");
|
|
60
|
-
const logLink = $("review-log-link");
|
|
61
|
-
const bundleLink = $("review-scratchpad-link");
|
|
62
|
-
setLink(finalLink, {
|
|
63
|
-
href: review.final_output_path ? "/api/review/artifact?kind=final_report" : null,
|
|
64
|
-
text: "Final report",
|
|
65
|
-
title: "Open the final review report",
|
|
66
|
-
});
|
|
67
|
-
setLink(logLink, {
|
|
68
|
-
href: review.run_dir ? "/api/review/artifact?kind=workflow_log" : null,
|
|
69
|
-
text: "Log",
|
|
70
|
-
title: "Open the review workflow log",
|
|
71
|
-
});
|
|
72
|
-
setLink(bundleLink, {
|
|
73
|
-
href: review.scratchpad_bundle_path
|
|
74
|
-
? "/api/review/artifact?kind=scratchpad_bundle"
|
|
75
|
-
: null,
|
|
76
|
-
text: "Scratchpad",
|
|
77
|
-
title: "Download scratchpad files as zip",
|
|
78
|
-
});
|
|
79
|
-
}
|
|
80
|
-
catch (err) {
|
|
81
|
-
console.error("Failed to load review status:", err);
|
|
82
|
-
}
|
|
83
|
-
}
|
|
84
|
-
async function startReview() {
|
|
85
|
-
try {
|
|
86
|
-
const agentEl = $("review-agent");
|
|
87
|
-
const modelEl = $("review-model");
|
|
88
|
-
const reasoningEl = $("review-reasoning");
|
|
89
|
-
const timeoutEl = $("review-timeout");
|
|
90
|
-
const payload = {
|
|
91
|
-
agent: agentEl?.value || "opencode",
|
|
92
|
-
model: modelEl?.value || "zai-coding-plan/glm-4.7",
|
|
93
|
-
reasoning: reasoningEl?.value || null,
|
|
94
|
-
max_wallclock_seconds: timeoutEl?.value
|
|
95
|
-
? parseInt(timeoutEl.value, 10) || null
|
|
96
|
-
: null,
|
|
97
|
-
};
|
|
98
|
-
const data = (await api("/api/review/start", {
|
|
99
|
-
method: "POST",
|
|
100
|
-
body: payload,
|
|
101
|
-
}));
|
|
102
|
-
if (data.status === "ok" || data.review) {
|
|
103
|
-
flash("Review started");
|
|
104
|
-
await loadReviewStatus();
|
|
105
|
-
}
|
|
106
|
-
}
|
|
107
|
-
catch (err) {
|
|
108
|
-
console.error("Failed to start review:", err);
|
|
109
|
-
const message = err instanceof Error ? err.message : "Failed to start review";
|
|
110
|
-
flash(message);
|
|
111
|
-
}
|
|
112
|
-
}
|
|
113
|
-
async function stopReview() {
|
|
114
|
-
try {
|
|
115
|
-
const data = (await api("/api/review/stop", {
|
|
116
|
-
method: "POST",
|
|
117
|
-
}));
|
|
118
|
-
if (data.status === "ok" || data.review) {
|
|
119
|
-
flash("Review stopped");
|
|
120
|
-
await loadReviewStatus();
|
|
121
|
-
}
|
|
122
|
-
}
|
|
123
|
-
catch (err) {
|
|
124
|
-
console.error("Failed to stop review:", err);
|
|
125
|
-
const message = err instanceof Error ? err.message : "Failed to stop review";
|
|
126
|
-
flash(message);
|
|
127
|
-
}
|
|
128
|
-
}
|
|
129
|
-
async function resetReview() {
|
|
130
|
-
try {
|
|
131
|
-
const data = (await api("/api/review/reset", {
|
|
132
|
-
method: "POST",
|
|
133
|
-
}));
|
|
134
|
-
if (data.status === "ok" || data.review) {
|
|
135
|
-
flash("Review state reset");
|
|
136
|
-
await loadReviewStatus();
|
|
137
|
-
}
|
|
138
|
-
}
|
|
139
|
-
catch (err) {
|
|
140
|
-
console.error("Failed to reset review:", err);
|
|
141
|
-
const message = err instanceof Error ? err.message : "Failed to reset review";
|
|
142
|
-
flash(message);
|
|
143
|
-
}
|
|
144
|
-
}
|
|
145
|
-
export function initReview() {
|
|
146
|
-
const startBtn = $("review-start");
|
|
147
|
-
const stopBtn = $("review-stop");
|
|
148
|
-
const resetBtn = $("review-reset");
|
|
149
|
-
startBtn?.addEventListener("click", startReview);
|
|
150
|
-
stopBtn?.addEventListener("click", stopReview);
|
|
151
|
-
resetBtn?.addEventListener("click", resetReview);
|
|
152
|
-
loadReviewStatus();
|
|
153
|
-
if (reviewInterval) {
|
|
154
|
-
clearInterval(reviewInterval);
|
|
155
|
-
}
|
|
156
|
-
reviewInterval = setInterval(loadReviewStatus, 5000);
|
|
157
|
-
}
|
codex_autorunner/static/runs.js
DELETED
|
@@ -1,418 +0,0 @@
|
|
|
1
|
-
import { api, flash, getAuthToken, openModal, resolvePath } from "./utils.js";
|
|
2
|
-
import { registerAutoRefresh, triggerRefresh } from "./autoRefresh.js";
|
|
3
|
-
import { subscribe } from "./bus.js";
|
|
4
|
-
let initialized = false;
|
|
5
|
-
const RUNS_AUTO_REFRESH_INTERVAL = 15000;
|
|
6
|
-
const runsState = {
|
|
7
|
-
runs: [],
|
|
8
|
-
attributionMode: "split",
|
|
9
|
-
todoSearch: "",
|
|
10
|
-
};
|
|
11
|
-
let closeDetailsModal = null;
|
|
12
|
-
const RUN_OUTPUT_MAX_LINES = 160;
|
|
13
|
-
const RUN_DIFF_MAX_LINES = 400;
|
|
14
|
-
const RUN_PLAN_MAX_LINES = 120;
|
|
15
|
-
const ui = {
|
|
16
|
-
refresh: document.getElementById("runs-refresh"),
|
|
17
|
-
tableBody: document.getElementById("runs-table-body"),
|
|
18
|
-
todoList: document.getElementById("runs-todo-list"),
|
|
19
|
-
todoSummary: document.getElementById("runs-todo-summary"),
|
|
20
|
-
todoSearch: document.getElementById("runs-todo-search"),
|
|
21
|
-
attribution: document.getElementById("runs-attribution-mode"),
|
|
22
|
-
modal: document.getElementById("run-details-modal"),
|
|
23
|
-
modalClose: document.getElementById("run-details-close"),
|
|
24
|
-
modalTitle: document.getElementById("run-details-title"),
|
|
25
|
-
modalMeta: document.getElementById("run-details-meta"),
|
|
26
|
-
modalTodos: document.getElementById("run-details-todos"),
|
|
27
|
-
modalTokens: document.getElementById("run-details-tokens"),
|
|
28
|
-
modalPlan: document.getElementById("run-details-plan"),
|
|
29
|
-
modalDiff: document.getElementById("run-details-diff"),
|
|
30
|
-
modalOutput: document.getElementById("run-details-output"),
|
|
31
|
-
modalLog: document.getElementById("run-details-log"),
|
|
32
|
-
};
|
|
33
|
-
const STATUS_LABELS = {
|
|
34
|
-
ok: "ok",
|
|
35
|
-
error: "error",
|
|
36
|
-
running: "running",
|
|
37
|
-
};
|
|
38
|
-
function formatTimestamp(ts) {
|
|
39
|
-
if (!ts)
|
|
40
|
-
return "–";
|
|
41
|
-
const date = new Date(ts);
|
|
42
|
-
if (Number.isNaN(date.getTime()))
|
|
43
|
-
return ts;
|
|
44
|
-
return date.toLocaleString();
|
|
45
|
-
}
|
|
46
|
-
function formatDuration(seconds) {
|
|
47
|
-
if (typeof seconds !== "number" || Number.isNaN(seconds))
|
|
48
|
-
return "–";
|
|
49
|
-
if (seconds < 60)
|
|
50
|
-
return `${seconds.toFixed(1)}s`;
|
|
51
|
-
const mins = Math.floor(seconds / 60);
|
|
52
|
-
const secs = Math.round(seconds % 60);
|
|
53
|
-
return `${mins}m ${secs}s`;
|
|
54
|
-
}
|
|
55
|
-
function formatNumber(value) {
|
|
56
|
-
if (typeof value !== "number" || Number.isNaN(value))
|
|
57
|
-
return "–";
|
|
58
|
-
return value.toLocaleString();
|
|
59
|
-
}
|
|
60
|
-
function truncateLines(text, maxLines) {
|
|
61
|
-
if (!text)
|
|
62
|
-
return "";
|
|
63
|
-
const lines = text.split("\n");
|
|
64
|
-
if (lines.length <= maxLines)
|
|
65
|
-
return text;
|
|
66
|
-
const trimmed = lines.slice(0, maxLines);
|
|
67
|
-
trimmed.push(`… (${lines.length - maxLines} more lines)`);
|
|
68
|
-
return trimmed.join("\n");
|
|
69
|
-
}
|
|
70
|
-
function runStatus(entry) {
|
|
71
|
-
if (entry.exit_code === 0)
|
|
72
|
-
return "ok";
|
|
73
|
-
if (entry.exit_code != null)
|
|
74
|
-
return "error";
|
|
75
|
-
return entry.finished_at ? "error" : "running";
|
|
76
|
-
}
|
|
77
|
-
function renderRunsTable() {
|
|
78
|
-
if (!ui.tableBody)
|
|
79
|
-
return;
|
|
80
|
-
ui.tableBody.innerHTML = "";
|
|
81
|
-
if (!runsState.runs.length) {
|
|
82
|
-
const row = document.createElement("tr");
|
|
83
|
-
row.innerHTML =
|
|
84
|
-
'<td colspan="6" class="runs-empty">No runs recorded yet.</td>';
|
|
85
|
-
ui.tableBody.appendChild(row);
|
|
86
|
-
return;
|
|
87
|
-
}
|
|
88
|
-
runsState.runs.forEach((run) => {
|
|
89
|
-
const status = runStatus(run);
|
|
90
|
-
const row = document.createElement("tr");
|
|
91
|
-
row.className = "runs-row";
|
|
92
|
-
row.dataset.runId = String(run.run_id);
|
|
93
|
-
row.innerHTML = `
|
|
94
|
-
<td>#${run.run_id}</td>
|
|
95
|
-
<td><span class="runs-pill runs-pill-${status}">${STATUS_LABELS[status]}</span></td>
|
|
96
|
-
<td>${formatTimestamp(run.started_at)}</td>
|
|
97
|
-
<td>${run.app_server?.model || "–"}</td>
|
|
98
|
-
<td>${formatNumber(run.token_total)}</td>
|
|
99
|
-
<td>${formatNumber(run.completed_todo_count || 0)}</td>
|
|
100
|
-
`;
|
|
101
|
-
row.addEventListener("click", () => openRunDetails(run));
|
|
102
|
-
ui.tableBody.appendChild(row);
|
|
103
|
-
});
|
|
104
|
-
}
|
|
105
|
-
function computeTodoAttribution() {
|
|
106
|
-
const map = new Map();
|
|
107
|
-
runsState.runs.forEach((run) => {
|
|
108
|
-
const completed = run.todo?.completed;
|
|
109
|
-
if (!Array.isArray(completed) || completed.length === 0)
|
|
110
|
-
return;
|
|
111
|
-
const runTokens = typeof run.token_total === "number" ? run.token_total : 0;
|
|
112
|
-
const perTodo = runsState.attributionMode === "split"
|
|
113
|
-
? runTokens / completed.length
|
|
114
|
-
: runTokens;
|
|
115
|
-
completed.forEach((item) => {
|
|
116
|
-
if (!item)
|
|
117
|
-
return;
|
|
118
|
-
const key = String(item);
|
|
119
|
-
if (!map.has(key)) {
|
|
120
|
-
map.set(key, {
|
|
121
|
-
text: key,
|
|
122
|
-
tokens: 0,
|
|
123
|
-
count: 0,
|
|
124
|
-
runs: new Set(),
|
|
125
|
-
});
|
|
126
|
-
}
|
|
127
|
-
const entry = map.get(key);
|
|
128
|
-
entry.tokens += perTodo;
|
|
129
|
-
entry.count += 1;
|
|
130
|
-
entry.runs.add(run.run_id);
|
|
131
|
-
});
|
|
132
|
-
});
|
|
133
|
-
const list = Array.from(map.values()).map((entry) => ({
|
|
134
|
-
text: entry.text,
|
|
135
|
-
tokens: entry.tokens,
|
|
136
|
-
count: entry.count,
|
|
137
|
-
runs: Array.from(entry.runs).sort((a, b) => b - a),
|
|
138
|
-
}));
|
|
139
|
-
list.sort((a, b) => b.tokens - a.tokens);
|
|
140
|
-
return list;
|
|
141
|
-
}
|
|
142
|
-
function renderTodoAnalytics() {
|
|
143
|
-
if (!ui.todoList)
|
|
144
|
-
return;
|
|
145
|
-
const list = computeTodoAttribution();
|
|
146
|
-
const filtered = list.filter((entry) => entry.text.toLowerCase().includes(runsState.todoSearch.toLowerCase()));
|
|
147
|
-
ui.todoList.innerHTML = "";
|
|
148
|
-
if (!filtered.length) {
|
|
149
|
-
ui.todoList.textContent = "No TODOs yet.";
|
|
150
|
-
}
|
|
151
|
-
else {
|
|
152
|
-
filtered.forEach((entry) => {
|
|
153
|
-
const item = document.createElement("div");
|
|
154
|
-
item.className = "runs-todo-item";
|
|
155
|
-
const runsMarkup = entry.runs
|
|
156
|
-
.map((runId) => `<button class="runs-chip" data-run-id="${runId}" title="Open run #${runId}">#${runId}</button>`)
|
|
157
|
-
.join("");
|
|
158
|
-
item.innerHTML = `
|
|
159
|
-
<div class="runs-todo-text">${entry.text}</div>
|
|
160
|
-
<div class="runs-todo-meta">
|
|
161
|
-
<span>${formatNumber(entry.tokens)} tokens</span>
|
|
162
|
-
<span>${entry.count} completions</span>
|
|
163
|
-
<span class="runs-todo-runs">${runsMarkup}</span>
|
|
164
|
-
</div>
|
|
165
|
-
`;
|
|
166
|
-
item.querySelectorAll(".runs-chip").forEach((btn) => {
|
|
167
|
-
btn.addEventListener("click", (event) => {
|
|
168
|
-
event.stopPropagation();
|
|
169
|
-
const id = Number(btn.dataset.runId);
|
|
170
|
-
const run = runsState.runs.find((candidate) => candidate.run_id === id);
|
|
171
|
-
if (run)
|
|
172
|
-
openRunDetails(run);
|
|
173
|
-
});
|
|
174
|
-
});
|
|
175
|
-
ui.todoList.appendChild(item);
|
|
176
|
-
});
|
|
177
|
-
}
|
|
178
|
-
if (ui.todoSummary) {
|
|
179
|
-
ui.todoSummary.textContent = `${filtered.length} TODOs`;
|
|
180
|
-
}
|
|
181
|
-
}
|
|
182
|
-
function formatPlan(plan) {
|
|
183
|
-
if (Array.isArray(plan)) {
|
|
184
|
-
const lines = plan
|
|
185
|
-
.map((item, index) => {
|
|
186
|
-
if (item && typeof item === "object") {
|
|
187
|
-
const step = item.step || item.task || item.title || "";
|
|
188
|
-
const status = item.status || item.state || "";
|
|
189
|
-
const label = `${index + 1}. ${step || "Step"}`;
|
|
190
|
-
return status ? `${label} [${status}]` : label;
|
|
191
|
-
}
|
|
192
|
-
return `${index + 1}. ${item}`;
|
|
193
|
-
})
|
|
194
|
-
.filter(Boolean);
|
|
195
|
-
return lines.join("\n");
|
|
196
|
-
}
|
|
197
|
-
if (plan && typeof plan === "object") {
|
|
198
|
-
if (Array.isArray(plan.steps)) {
|
|
199
|
-
return formatPlan(plan.steps);
|
|
200
|
-
}
|
|
201
|
-
if (Array.isArray(plan.plan)) {
|
|
202
|
-
return formatPlan(plan.plan);
|
|
203
|
-
}
|
|
204
|
-
return JSON.stringify(plan, null, 2);
|
|
205
|
-
}
|
|
206
|
-
if (typeof plan === "string")
|
|
207
|
-
return plan;
|
|
208
|
-
return "";
|
|
209
|
-
}
|
|
210
|
-
async function fetchArtifactText(path) {
|
|
211
|
-
const target = resolvePath(path);
|
|
212
|
-
const headers = {};
|
|
213
|
-
const token = getAuthToken();
|
|
214
|
-
if (token) {
|
|
215
|
-
headers.Authorization = `Bearer ${token}`;
|
|
216
|
-
}
|
|
217
|
-
const res = await fetch(target, { headers });
|
|
218
|
-
if (!res.ok) {
|
|
219
|
-
const message = await res.text();
|
|
220
|
-
const error = message?.trim() || `Request failed with ${res.status}`;
|
|
221
|
-
throw new Error(error);
|
|
222
|
-
}
|
|
223
|
-
return res.text();
|
|
224
|
-
}
|
|
225
|
-
async function loadPlan(runId) {
|
|
226
|
-
if (!ui.modalPlan)
|
|
227
|
-
return;
|
|
228
|
-
ui.modalPlan.textContent = "Loading…";
|
|
229
|
-
try {
|
|
230
|
-
const planRaw = await fetchArtifactText(`/api/runs/${runId}/plan`);
|
|
231
|
-
let planData = planRaw;
|
|
232
|
-
try {
|
|
233
|
-
planData = JSON.parse(planRaw);
|
|
234
|
-
}
|
|
235
|
-
catch (_err) {
|
|
236
|
-
planData = planRaw;
|
|
237
|
-
}
|
|
238
|
-
const formatted = formatPlan(planData);
|
|
239
|
-
ui.modalPlan.textContent = formatted ? truncateLines(formatted, RUN_PLAN_MAX_LINES) : "Not available.";
|
|
240
|
-
}
|
|
241
|
-
catch (err) {
|
|
242
|
-
const message = err instanceof Error ? err.message : "";
|
|
243
|
-
ui.modalPlan.textContent =
|
|
244
|
-
message.includes("401") || message.includes("403") || message.includes("404")
|
|
245
|
-
? "Not available."
|
|
246
|
-
: "Unable to load.";
|
|
247
|
-
}
|
|
248
|
-
}
|
|
249
|
-
async function loadFinalOutput(runId) {
|
|
250
|
-
if (!ui.modalOutput)
|
|
251
|
-
return;
|
|
252
|
-
ui.modalOutput.textContent = "Loading…";
|
|
253
|
-
try {
|
|
254
|
-
const text = await api(`/api/runs/${runId}/output`);
|
|
255
|
-
if (typeof text === "string" && text.trim()) {
|
|
256
|
-
ui.modalOutput.textContent = truncateLines(text, RUN_OUTPUT_MAX_LINES);
|
|
257
|
-
return;
|
|
258
|
-
}
|
|
259
|
-
if (text !== null && text !== undefined) {
|
|
260
|
-
const formatted = JSON.stringify(text, null, 2);
|
|
261
|
-
ui.modalOutput.textContent = truncateLines(formatted, RUN_OUTPUT_MAX_LINES);
|
|
262
|
-
return;
|
|
263
|
-
}
|
|
264
|
-
ui.modalOutput.textContent = "Not available.";
|
|
265
|
-
}
|
|
266
|
-
catch (err) {
|
|
267
|
-
const message = err instanceof Error ? err.message : "";
|
|
268
|
-
ui.modalOutput.textContent =
|
|
269
|
-
message.includes("401") || message.includes("403") || message.includes("404")
|
|
270
|
-
? "Not available."
|
|
271
|
-
: "Unable to load.";
|
|
272
|
-
}
|
|
273
|
-
}
|
|
274
|
-
async function loadDiff(runId) {
|
|
275
|
-
if (!ui.modalDiff)
|
|
276
|
-
return;
|
|
277
|
-
ui.modalDiff.textContent = "Loading…";
|
|
278
|
-
try {
|
|
279
|
-
const text = await api(`/api/runs/${runId}/diff`);
|
|
280
|
-
if (typeof text === "string" && text.trim()) {
|
|
281
|
-
ui.modalDiff.textContent = truncateLines(text, RUN_DIFF_MAX_LINES);
|
|
282
|
-
return;
|
|
283
|
-
}
|
|
284
|
-
if (text !== null && text !== undefined) {
|
|
285
|
-
const formatted = JSON.stringify(text, null, 2);
|
|
286
|
-
ui.modalDiff.textContent = truncateLines(formatted, RUN_DIFF_MAX_LINES);
|
|
287
|
-
return;
|
|
288
|
-
}
|
|
289
|
-
ui.modalDiff.textContent = "Not available.";
|
|
290
|
-
}
|
|
291
|
-
catch (err) {
|
|
292
|
-
const message = err instanceof Error ? err.message : "";
|
|
293
|
-
ui.modalDiff.textContent =
|
|
294
|
-
message.includes("401") || message.includes("403") || message.includes("404")
|
|
295
|
-
? "Not available."
|
|
296
|
-
: "Unable to load.";
|
|
297
|
-
}
|
|
298
|
-
}
|
|
299
|
-
function renderTokenBreakdown(run) {
|
|
300
|
-
if (!ui.modalTokens)
|
|
301
|
-
return;
|
|
302
|
-
const usage = run.token_usage?.delta;
|
|
303
|
-
if (!usage || typeof usage !== "object") {
|
|
304
|
-
ui.modalTokens.textContent = "Token data unavailable.";
|
|
305
|
-
return;
|
|
306
|
-
}
|
|
307
|
-
ui.modalTokens.innerHTML = `
|
|
308
|
-
<div>Input: ${formatNumber(usage.input_tokens || usage.inputTokens)}</div>
|
|
309
|
-
<div>Cached input: ${formatNumber(usage.cached_input_tokens || usage.cachedInputTokens)}</div>
|
|
310
|
-
<div>Output: ${formatNumber(usage.output_tokens || usage.outputTokens)}</div>
|
|
311
|
-
<div>Reasoning: ${formatNumber(usage.reasoning_output_tokens || usage.reasoningOutputTokens)}</div>
|
|
312
|
-
<div>Total: ${formatNumber(usage.total_tokens || usage.totalTokens || usage.total)}</div>
|
|
313
|
-
`;
|
|
314
|
-
}
|
|
315
|
-
function renderCompletedTodos(run) {
|
|
316
|
-
if (!ui.modalTodos)
|
|
317
|
-
return;
|
|
318
|
-
const completed = Array.isArray(run.todo?.completed) ? run.todo.completed : [];
|
|
319
|
-
ui.modalTodos.innerHTML = "";
|
|
320
|
-
if (!completed.length) {
|
|
321
|
-
ui.modalTodos.textContent = "No TODOs completed in this run.";
|
|
322
|
-
return;
|
|
323
|
-
}
|
|
324
|
-
const list = document.createElement("ul");
|
|
325
|
-
completed.forEach((item) => {
|
|
326
|
-
const li = document.createElement("li");
|
|
327
|
-
li.textContent = item;
|
|
328
|
-
li.addEventListener("click", () => {
|
|
329
|
-
if (ui.todoSearch) {
|
|
330
|
-
ui.todoSearch.value = item;
|
|
331
|
-
runsState.todoSearch = item;
|
|
332
|
-
renderTodoAnalytics();
|
|
333
|
-
}
|
|
334
|
-
});
|
|
335
|
-
list.appendChild(li);
|
|
336
|
-
});
|
|
337
|
-
ui.modalTodos.appendChild(list);
|
|
338
|
-
}
|
|
339
|
-
function openRunDetails(run) {
|
|
340
|
-
if (!ui.modal)
|
|
341
|
-
return;
|
|
342
|
-
if (closeDetailsModal) {
|
|
343
|
-
closeDetailsModal();
|
|
344
|
-
}
|
|
345
|
-
ui.modalTitle.textContent = `Run #${run.run_id}`;
|
|
346
|
-
ui.modalMeta.textContent = `Started ${formatTimestamp(run.started_at)} · Duration ${formatDuration(run.duration_seconds)} · Status ${runStatus(run) || "–"}`;
|
|
347
|
-
if (ui.modalLog) {
|
|
348
|
-
const params = new URLSearchParams({ run_id: String(run.run_id), raw: "1" });
|
|
349
|
-
const token = getAuthToken();
|
|
350
|
-
if (token) {
|
|
351
|
-
params.set("token", token);
|
|
352
|
-
}
|
|
353
|
-
ui.modalLog.href = resolvePath(`/api/logs?${params.toString()}`);
|
|
354
|
-
}
|
|
355
|
-
renderTokenBreakdown(run);
|
|
356
|
-
renderCompletedTodos(run);
|
|
357
|
-
if (ui.modalPlan)
|
|
358
|
-
loadPlan(run.run_id);
|
|
359
|
-
if (ui.modalDiff)
|
|
360
|
-
loadDiff(run.run_id);
|
|
361
|
-
if (ui.modalOutput)
|
|
362
|
-
loadFinalOutput(run.run_id);
|
|
363
|
-
const triggerEl = document.activeElement;
|
|
364
|
-
closeDetailsModal = openModal(ui.modal, {
|
|
365
|
-
initialFocus: ui.modalClose || ui.modal,
|
|
366
|
-
returnFocusTo: triggerEl,
|
|
367
|
-
});
|
|
368
|
-
}
|
|
369
|
-
async function loadRuns() {
|
|
370
|
-
try {
|
|
371
|
-
const data = await api("/api/runs?limit=200");
|
|
372
|
-
runsState.runs = Array.isArray(data?.runs) ? data.runs : [];
|
|
373
|
-
renderRunsTable();
|
|
374
|
-
renderTodoAnalytics();
|
|
375
|
-
}
|
|
376
|
-
catch (err) {
|
|
377
|
-
const message = err instanceof Error ? err.message : "Failed to load runs";
|
|
378
|
-
flash(message, "error");
|
|
379
|
-
}
|
|
380
|
-
}
|
|
381
|
-
export function initRuns() {
|
|
382
|
-
if (initialized)
|
|
383
|
-
return;
|
|
384
|
-
initialized = true;
|
|
385
|
-
if (ui.refresh) {
|
|
386
|
-
ui.refresh.addEventListener("click", () => loadRuns());
|
|
387
|
-
}
|
|
388
|
-
if (ui.todoSearch) {
|
|
389
|
-
ui.todoSearch.addEventListener("input", () => {
|
|
390
|
-
runsState.todoSearch = ui.todoSearch.value || "";
|
|
391
|
-
renderTodoAnalytics();
|
|
392
|
-
});
|
|
393
|
-
}
|
|
394
|
-
if (ui.attribution) {
|
|
395
|
-
ui.attribution.value = runsState.attributionMode;
|
|
396
|
-
ui.attribution.addEventListener("change", () => {
|
|
397
|
-
runsState.attributionMode = (ui.attribution?.value || "split");
|
|
398
|
-
renderTodoAnalytics();
|
|
399
|
-
});
|
|
400
|
-
}
|
|
401
|
-
if (ui.modalClose) {
|
|
402
|
-
ui.modalClose.addEventListener("click", () => {
|
|
403
|
-
if (closeDetailsModal)
|
|
404
|
-
closeDetailsModal();
|
|
405
|
-
});
|
|
406
|
-
}
|
|
407
|
-
registerAutoRefresh("runs-list", {
|
|
408
|
-
callback: loadRuns,
|
|
409
|
-
tabId: "runs",
|
|
410
|
-
interval: RUNS_AUTO_REFRESH_INTERVAL,
|
|
411
|
-
refreshOnActivation: true,
|
|
412
|
-
immediate: false,
|
|
413
|
-
});
|
|
414
|
-
subscribe("runs:invalidate", () => {
|
|
415
|
-
triggerRefresh("runs-list");
|
|
416
|
-
});
|
|
417
|
-
loadRuns();
|
|
418
|
-
}
|
|
@@ -1,124 +0,0 @@
|
|
|
1
|
-
import { api, flash } from "./utils.js";
|
|
2
|
-
const UI = {
|
|
3
|
-
status: document.getElementById("snapshot-status"),
|
|
4
|
-
content: document.getElementById("snapshot-content"),
|
|
5
|
-
generate: document.getElementById("snapshot-generate"),
|
|
6
|
-
update: document.getElementById("snapshot-update"),
|
|
7
|
-
regenerate: document.getElementById("snapshot-regenerate"),
|
|
8
|
-
copy: document.getElementById("snapshot-copy"),
|
|
9
|
-
refresh: document.getElementById("snapshot-refresh"),
|
|
10
|
-
};
|
|
11
|
-
let latest = { exists: false, content: "", state: {} };
|
|
12
|
-
let busy = false;
|
|
13
|
-
function setBusy(on) {
|
|
14
|
-
busy = on;
|
|
15
|
-
const disabled = !!on;
|
|
16
|
-
for (const btn of [UI.generate, UI.update, UI.regenerate, UI.copy, UI.refresh]) {
|
|
17
|
-
if (!btn)
|
|
18
|
-
continue;
|
|
19
|
-
btn.disabled = disabled;
|
|
20
|
-
}
|
|
21
|
-
if (UI.status)
|
|
22
|
-
UI.status.textContent = on ? "Working…" : "";
|
|
23
|
-
}
|
|
24
|
-
function render() {
|
|
25
|
-
if (!UI.content)
|
|
26
|
-
return;
|
|
27
|
-
UI.content.value = latest.content || "";
|
|
28
|
-
if (UI.generate)
|
|
29
|
-
UI.generate.classList.toggle("hidden", false);
|
|
30
|
-
if (UI.update)
|
|
31
|
-
UI.update.classList.toggle("hidden", true);
|
|
32
|
-
if (UI.regenerate)
|
|
33
|
-
UI.regenerate.classList.toggle("hidden", true);
|
|
34
|
-
if (UI.copy)
|
|
35
|
-
UI.copy.disabled = busy || !(latest.content || "").trim();
|
|
36
|
-
}
|
|
37
|
-
async function loadSnapshot({ notify = false } = {}) {
|
|
38
|
-
if (busy)
|
|
39
|
-
return;
|
|
40
|
-
try {
|
|
41
|
-
setBusy(true);
|
|
42
|
-
const data = await api("/api/snapshot");
|
|
43
|
-
latest = {
|
|
44
|
-
exists: !!data?.exists,
|
|
45
|
-
content: data?.content || "",
|
|
46
|
-
state: data?.state || {},
|
|
47
|
-
};
|
|
48
|
-
render();
|
|
49
|
-
if (notify)
|
|
50
|
-
flash(latest.exists ? "Snapshot loaded" : "No snapshot yet");
|
|
51
|
-
}
|
|
52
|
-
catch (err) {
|
|
53
|
-
flash(err?.message || "Failed to load snapshot");
|
|
54
|
-
}
|
|
55
|
-
finally {
|
|
56
|
-
setBusy(false);
|
|
57
|
-
}
|
|
58
|
-
}
|
|
59
|
-
async function runSnapshot() {
|
|
60
|
-
if (busy)
|
|
61
|
-
return;
|
|
62
|
-
try {
|
|
63
|
-
setBusy(true);
|
|
64
|
-
const data = await api("/api/snapshot", {
|
|
65
|
-
method: "POST",
|
|
66
|
-
body: {},
|
|
67
|
-
});
|
|
68
|
-
latest = {
|
|
69
|
-
exists: true,
|
|
70
|
-
content: data?.content || "",
|
|
71
|
-
state: data?.state || {},
|
|
72
|
-
};
|
|
73
|
-
render();
|
|
74
|
-
flash("Snapshot generated");
|
|
75
|
-
}
|
|
76
|
-
catch (err) {
|
|
77
|
-
flash(err?.message || "Snapshot generation failed");
|
|
78
|
-
}
|
|
79
|
-
finally {
|
|
80
|
-
setBusy(false);
|
|
81
|
-
}
|
|
82
|
-
}
|
|
83
|
-
async function copyToClipboard() {
|
|
84
|
-
const text = UI.content?.value || "";
|
|
85
|
-
if (!text.trim())
|
|
86
|
-
return;
|
|
87
|
-
try {
|
|
88
|
-
if (navigator.clipboard?.writeText) {
|
|
89
|
-
await navigator.clipboard.writeText(text);
|
|
90
|
-
flash("Copied to clipboard");
|
|
91
|
-
return;
|
|
92
|
-
}
|
|
93
|
-
}
|
|
94
|
-
catch {
|
|
95
|
-
// fall through
|
|
96
|
-
}
|
|
97
|
-
try {
|
|
98
|
-
UI.content?.focus();
|
|
99
|
-
UI.content?.select();
|
|
100
|
-
const ok = document.execCommand("copy");
|
|
101
|
-
flash(ok ? "Copied to clipboard" : "Copy failed");
|
|
102
|
-
}
|
|
103
|
-
catch {
|
|
104
|
-
flash("Copy failed");
|
|
105
|
-
}
|
|
106
|
-
finally {
|
|
107
|
-
try {
|
|
108
|
-
UI.content?.setSelectionRange(0, 0);
|
|
109
|
-
}
|
|
110
|
-
catch {
|
|
111
|
-
// ignore
|
|
112
|
-
}
|
|
113
|
-
}
|
|
114
|
-
}
|
|
115
|
-
export function initSnapshot() {
|
|
116
|
-
if (!UI.content)
|
|
117
|
-
return;
|
|
118
|
-
UI.generate?.addEventListener("click", () => runSnapshot());
|
|
119
|
-
UI.update?.addEventListener("click", () => runSnapshot());
|
|
120
|
-
UI.regenerate?.addEventListener("click", () => runSnapshot());
|
|
121
|
-
UI.copy?.addEventListener("click", copyToClipboard);
|
|
122
|
-
UI.refresh?.addEventListener("click", () => loadSnapshot({ notify: true }));
|
|
123
|
-
loadSnapshot().catch(() => { });
|
|
124
|
-
}
|