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,1315 @@
|
|
|
1
|
+
// GENERATED FILE - do not edit directly. Source: static_src/
|
|
2
|
+
import { api, flash, getUrlParams, resolvePath, statusPill, getAuthToken, openModal } from "./utils.js";
|
|
3
|
+
import { activateTab } from "./tabs.js";
|
|
4
|
+
import { registerAutoRefresh } from "./autoRefresh.js";
|
|
5
|
+
import { CONSTANTS } from "./constants.js";
|
|
6
|
+
import { subscribe } from "./bus.js";
|
|
7
|
+
import { isRepoHealthy } from "./health.js";
|
|
8
|
+
import { closeTicketEditor, initTicketEditor, openTicketEditor } from "./ticketEditor.js";
|
|
9
|
+
import { parseAppServerEvent } from "./agentEvents.js";
|
|
10
|
+
import { summarizeEvents, renderCompactSummary, COMPACT_MAX_TEXT_LENGTH } from "./eventSummarizer.js";
|
|
11
|
+
import { refreshBell, renderMarkdown } from "./messages.js";
|
|
12
|
+
let currentRunId = null;
|
|
13
|
+
let ticketsExist = false;
|
|
14
|
+
let currentActiveTicket = null;
|
|
15
|
+
let currentFlowStatus = null;
|
|
16
|
+
let elapsedTimerId = null;
|
|
17
|
+
let flowStartedAt = null;
|
|
18
|
+
let eventSource = null;
|
|
19
|
+
let lastActivityTime = null;
|
|
20
|
+
let lastActivityTimerId = null;
|
|
21
|
+
let liveOutputDetailExpanded = false; // Start with summary view, one click for full
|
|
22
|
+
let liveOutputBuffer = [];
|
|
23
|
+
const MAX_OUTPUT_LINES = 200;
|
|
24
|
+
const LIVE_EVENT_MAX = 50;
|
|
25
|
+
let liveOutputEvents = [];
|
|
26
|
+
let liveOutputEventIndex = {};
|
|
27
|
+
let currentReasonFull = null; // Full reason text for modal display
|
|
28
|
+
// Dispatch panel collapse state (persisted to localStorage)
|
|
29
|
+
const DISPATCH_PANEL_COLLAPSED_KEY = "car-dispatch-panel-collapsed";
|
|
30
|
+
let dispatchPanelCollapsed = false;
|
|
31
|
+
// Throttling state
|
|
32
|
+
let liveOutputRenderPending = false;
|
|
33
|
+
let liveOutputTextPending = false;
|
|
34
|
+
function scheduleLiveOutputRender() {
|
|
35
|
+
if (liveOutputRenderPending)
|
|
36
|
+
return;
|
|
37
|
+
liveOutputRenderPending = true;
|
|
38
|
+
requestAnimationFrame(() => {
|
|
39
|
+
renderLiveOutputView();
|
|
40
|
+
liveOutputRenderPending = false;
|
|
41
|
+
});
|
|
42
|
+
}
|
|
43
|
+
function scheduleLiveOutputTextUpdate() {
|
|
44
|
+
if (liveOutputTextPending)
|
|
45
|
+
return;
|
|
46
|
+
liveOutputTextPending = true;
|
|
47
|
+
requestAnimationFrame(() => {
|
|
48
|
+
const outputEl = document.getElementById("ticket-live-output-text");
|
|
49
|
+
if (outputEl) {
|
|
50
|
+
const newText = liveOutputBuffer.join("\n");
|
|
51
|
+
if (outputEl.textContent !== newText) {
|
|
52
|
+
outputEl.textContent = newText;
|
|
53
|
+
}
|
|
54
|
+
// Auto-scroll to bottom when detail view is showing
|
|
55
|
+
const detailEl = document.getElementById("ticket-live-output-detail");
|
|
56
|
+
if (detailEl && liveOutputDetailExpanded) {
|
|
57
|
+
detailEl.scrollTop = detailEl.scrollHeight;
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
liveOutputTextPending = false;
|
|
61
|
+
});
|
|
62
|
+
}
|
|
63
|
+
/**
|
|
64
|
+
* Initialize dispatch panel collapse state from localStorage
|
|
65
|
+
*/
|
|
66
|
+
function initDispatchPanelToggle() {
|
|
67
|
+
const { dispatchPanel, dispatchPanelToggle } = els();
|
|
68
|
+
if (!dispatchPanel || !dispatchPanelToggle)
|
|
69
|
+
return;
|
|
70
|
+
// Restore collapsed state from localStorage
|
|
71
|
+
const stored = localStorage.getItem(DISPATCH_PANEL_COLLAPSED_KEY);
|
|
72
|
+
dispatchPanelCollapsed = stored === "true";
|
|
73
|
+
if (dispatchPanelCollapsed) {
|
|
74
|
+
dispatchPanel.classList.add("collapsed");
|
|
75
|
+
}
|
|
76
|
+
// Handle toggle click
|
|
77
|
+
dispatchPanelToggle.addEventListener("click", () => {
|
|
78
|
+
dispatchPanelCollapsed = !dispatchPanelCollapsed;
|
|
79
|
+
dispatchPanel.classList.toggle("collapsed", dispatchPanelCollapsed);
|
|
80
|
+
localStorage.setItem(DISPATCH_PANEL_COLLAPSED_KEY, String(dispatchPanelCollapsed));
|
|
81
|
+
});
|
|
82
|
+
}
|
|
83
|
+
/**
|
|
84
|
+
* Render mini dispatch items for collapsed panel view.
|
|
85
|
+
* Shows compact dispatch indicators that can be clicked to expand.
|
|
86
|
+
*/
|
|
87
|
+
function renderDispatchMiniList(entries) {
|
|
88
|
+
const { dispatchMiniList, dispatchPanel } = els();
|
|
89
|
+
if (!dispatchMiniList)
|
|
90
|
+
return;
|
|
91
|
+
dispatchMiniList.innerHTML = "";
|
|
92
|
+
// Only show first 8 items in mini view
|
|
93
|
+
const maxMiniItems = 8;
|
|
94
|
+
entries.slice(0, maxMiniItems).forEach((entry) => {
|
|
95
|
+
const dispatch = entry.dispatch;
|
|
96
|
+
const isTurnSummary = dispatch?.mode === "turn_summary" || dispatch?.extra?.is_turn_summary;
|
|
97
|
+
const isNotify = dispatch?.mode === "notify";
|
|
98
|
+
const mini = document.createElement("div");
|
|
99
|
+
mini.className = `dispatch-mini-item${isNotify ? " notify" : ""}`;
|
|
100
|
+
mini.textContent = `#${entry.seq || "?"}`;
|
|
101
|
+
mini.title = isTurnSummary
|
|
102
|
+
? "Agent turn output"
|
|
103
|
+
: dispatch?.title || `Dispatch #${entry.seq}`;
|
|
104
|
+
// Click to expand panel and scroll to this item
|
|
105
|
+
mini.addEventListener("click", () => {
|
|
106
|
+
if (dispatchPanel && dispatchPanelCollapsed) {
|
|
107
|
+
dispatchPanelCollapsed = false;
|
|
108
|
+
dispatchPanel.classList.remove("collapsed");
|
|
109
|
+
localStorage.setItem(DISPATCH_PANEL_COLLAPSED_KEY, "false");
|
|
110
|
+
}
|
|
111
|
+
});
|
|
112
|
+
dispatchMiniList.appendChild(mini);
|
|
113
|
+
});
|
|
114
|
+
// Show overflow indicator if more items
|
|
115
|
+
if (entries.length > maxMiniItems) {
|
|
116
|
+
const more = document.createElement("div");
|
|
117
|
+
more.className = "dispatch-mini-item";
|
|
118
|
+
more.textContent = `+${entries.length - maxMiniItems}`;
|
|
119
|
+
more.title = `${entries.length - maxMiniItems} more dispatches`;
|
|
120
|
+
more.addEventListener("click", () => {
|
|
121
|
+
if (dispatchPanel && dispatchPanelCollapsed) {
|
|
122
|
+
dispatchPanelCollapsed = false;
|
|
123
|
+
dispatchPanel.classList.remove("collapsed");
|
|
124
|
+
localStorage.setItem(DISPATCH_PANEL_COLLAPSED_KEY, "false");
|
|
125
|
+
}
|
|
126
|
+
});
|
|
127
|
+
dispatchMiniList.appendChild(more);
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
function formatElapsed(startTime) {
|
|
131
|
+
const now = new Date();
|
|
132
|
+
const diffMs = now.getTime() - startTime.getTime();
|
|
133
|
+
const diffSecs = Math.floor(diffMs / 1000);
|
|
134
|
+
if (diffSecs < 60) {
|
|
135
|
+
return `${diffSecs}s`;
|
|
136
|
+
}
|
|
137
|
+
const mins = Math.floor(diffSecs / 60);
|
|
138
|
+
const secs = diffSecs % 60;
|
|
139
|
+
if (mins < 60) {
|
|
140
|
+
return `${mins}m ${secs}s`;
|
|
141
|
+
}
|
|
142
|
+
const hours = Math.floor(mins / 60);
|
|
143
|
+
const remainingMins = mins % 60;
|
|
144
|
+
return `${hours}h ${remainingMins}m`;
|
|
145
|
+
}
|
|
146
|
+
function startElapsedTimer() {
|
|
147
|
+
stopElapsedTimer();
|
|
148
|
+
if (!flowStartedAt)
|
|
149
|
+
return;
|
|
150
|
+
const update = () => {
|
|
151
|
+
const { elapsed } = els();
|
|
152
|
+
if (elapsed && flowStartedAt) {
|
|
153
|
+
elapsed.textContent = formatElapsed(flowStartedAt);
|
|
154
|
+
}
|
|
155
|
+
};
|
|
156
|
+
update(); // Update immediately
|
|
157
|
+
elapsedTimerId = setInterval(update, 1000);
|
|
158
|
+
}
|
|
159
|
+
function stopElapsedTimer() {
|
|
160
|
+
if (elapsedTimerId) {
|
|
161
|
+
clearInterval(elapsedTimerId);
|
|
162
|
+
elapsedTimerId = null;
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
// ---- SSE Event Stream Functions ----
|
|
166
|
+
function formatTimeAgo(timestamp) {
|
|
167
|
+
const now = new Date();
|
|
168
|
+
const diffMs = now.getTime() - timestamp.getTime();
|
|
169
|
+
const diffSecs = Math.floor(diffMs / 1000);
|
|
170
|
+
if (diffSecs < 5)
|
|
171
|
+
return "just now";
|
|
172
|
+
if (diffSecs < 60)
|
|
173
|
+
return `${diffSecs}s ago`;
|
|
174
|
+
const mins = Math.floor(diffSecs / 60);
|
|
175
|
+
if (mins < 60)
|
|
176
|
+
return `${mins}m ago`;
|
|
177
|
+
const hours = Math.floor(mins / 60);
|
|
178
|
+
return `${hours}h ago`;
|
|
179
|
+
}
|
|
180
|
+
function updateLastActivityDisplay() {
|
|
181
|
+
const el = document.getElementById("ticket-flow-last-activity");
|
|
182
|
+
if (el && lastActivityTime) {
|
|
183
|
+
el.textContent = formatTimeAgo(lastActivityTime);
|
|
184
|
+
}
|
|
185
|
+
}
|
|
186
|
+
function startLastActivityTimer() {
|
|
187
|
+
stopLastActivityTimer();
|
|
188
|
+
updateLastActivityDisplay();
|
|
189
|
+
lastActivityTimerId = setInterval(updateLastActivityDisplay, 1000);
|
|
190
|
+
}
|
|
191
|
+
function stopLastActivityTimer() {
|
|
192
|
+
if (lastActivityTimerId) {
|
|
193
|
+
clearInterval(lastActivityTimerId);
|
|
194
|
+
lastActivityTimerId = null;
|
|
195
|
+
}
|
|
196
|
+
}
|
|
197
|
+
function appendToLiveOutput(text) {
|
|
198
|
+
if (!text)
|
|
199
|
+
return;
|
|
200
|
+
const segments = text.split("\n");
|
|
201
|
+
// Merge first segment into the last buffered line to avoid artificial newlines between deltas
|
|
202
|
+
if (liveOutputBuffer.length === 0) {
|
|
203
|
+
liveOutputBuffer.push(segments[0]);
|
|
204
|
+
}
|
|
205
|
+
else {
|
|
206
|
+
liveOutputBuffer[liveOutputBuffer.length - 1] += segments[0];
|
|
207
|
+
}
|
|
208
|
+
// Remaining segments represent real new lines
|
|
209
|
+
for (let i = 1; i < segments.length; i++) {
|
|
210
|
+
liveOutputBuffer.push(segments[i]);
|
|
211
|
+
}
|
|
212
|
+
// Trim buffer if it exceeds max lines
|
|
213
|
+
while (liveOutputBuffer.length > MAX_OUTPUT_LINES) {
|
|
214
|
+
liveOutputBuffer.shift();
|
|
215
|
+
}
|
|
216
|
+
scheduleLiveOutputTextUpdate();
|
|
217
|
+
}
|
|
218
|
+
function addLiveOutputEvent(parsed) {
|
|
219
|
+
const { event, mergeStrategy } = parsed;
|
|
220
|
+
const itemId = event.itemId;
|
|
221
|
+
if (mergeStrategy && itemId && liveOutputEventIndex[itemId] !== undefined) {
|
|
222
|
+
const existingIndex = liveOutputEventIndex[itemId];
|
|
223
|
+
const existing = liveOutputEvents[existingIndex];
|
|
224
|
+
if (mergeStrategy === "append") {
|
|
225
|
+
existing.summary = `${existing.summary || ""}${event.summary}`;
|
|
226
|
+
}
|
|
227
|
+
else if (mergeStrategy === "newline") {
|
|
228
|
+
existing.summary = `${existing.summary || ""}\n\n`;
|
|
229
|
+
}
|
|
230
|
+
existing.time = event.time;
|
|
231
|
+
return;
|
|
232
|
+
}
|
|
233
|
+
liveOutputEvents.push(event);
|
|
234
|
+
if (liveOutputEvents.length > LIVE_EVENT_MAX) {
|
|
235
|
+
liveOutputEvents = liveOutputEvents.slice(-LIVE_EVENT_MAX);
|
|
236
|
+
liveOutputEventIndex = {};
|
|
237
|
+
liveOutputEvents.forEach((evt, idx) => {
|
|
238
|
+
if (evt.itemId)
|
|
239
|
+
liveOutputEventIndex[evt.itemId] = idx;
|
|
240
|
+
});
|
|
241
|
+
}
|
|
242
|
+
else if (itemId) {
|
|
243
|
+
liveOutputEventIndex[itemId] = liveOutputEvents.length - 1;
|
|
244
|
+
}
|
|
245
|
+
}
|
|
246
|
+
function renderLiveOutputEvents() {
|
|
247
|
+
const container = document.getElementById("ticket-live-output-events");
|
|
248
|
+
const list = document.getElementById("ticket-live-output-events-list");
|
|
249
|
+
const count = document.getElementById("ticket-live-output-events-count");
|
|
250
|
+
if (!container || !list || !count)
|
|
251
|
+
return;
|
|
252
|
+
const hasEvents = liveOutputEvents.length > 0;
|
|
253
|
+
if (count.textContent !== String(liveOutputEvents.length)) {
|
|
254
|
+
count.textContent = String(liveOutputEvents.length);
|
|
255
|
+
}
|
|
256
|
+
const shouldHide = !hasEvents || !liveOutputDetailExpanded;
|
|
257
|
+
if (container.classList.contains("hidden") !== shouldHide) {
|
|
258
|
+
container.classList.toggle("hidden", shouldHide);
|
|
259
|
+
}
|
|
260
|
+
if (shouldHide) {
|
|
261
|
+
if (list.innerHTML !== "")
|
|
262
|
+
list.innerHTML = "";
|
|
263
|
+
return;
|
|
264
|
+
}
|
|
265
|
+
// Track which IDs are currently in the list to remove stale ones
|
|
266
|
+
const currentIds = new Set();
|
|
267
|
+
liveOutputEvents.forEach((entry) => {
|
|
268
|
+
const id = entry.id;
|
|
269
|
+
currentIds.add(id);
|
|
270
|
+
// Safer lookup than querySelector with arbitrary ID
|
|
271
|
+
let wrapper = null;
|
|
272
|
+
for (let i = 0; i < list.children.length; i++) {
|
|
273
|
+
const child = list.children[i];
|
|
274
|
+
if (child.dataset.eventId === id) {
|
|
275
|
+
wrapper = child;
|
|
276
|
+
break;
|
|
277
|
+
}
|
|
278
|
+
}
|
|
279
|
+
if (!wrapper) {
|
|
280
|
+
wrapper = document.createElement("div");
|
|
281
|
+
wrapper.className = `ticket-chat-event ${entry.kind || ""}`.trim();
|
|
282
|
+
wrapper.dataset.eventId = id;
|
|
283
|
+
const title = document.createElement("div");
|
|
284
|
+
title.className = "ticket-chat-event-title";
|
|
285
|
+
wrapper.appendChild(title);
|
|
286
|
+
const summary = document.createElement("div");
|
|
287
|
+
summary.className = "ticket-chat-event-summary";
|
|
288
|
+
wrapper.appendChild(summary);
|
|
289
|
+
const detail = document.createElement("div");
|
|
290
|
+
detail.className = "ticket-chat-event-detail";
|
|
291
|
+
wrapper.appendChild(detail);
|
|
292
|
+
const meta = document.createElement("div");
|
|
293
|
+
meta.className = "ticket-chat-event-meta";
|
|
294
|
+
wrapper.appendChild(meta);
|
|
295
|
+
list.appendChild(wrapper);
|
|
296
|
+
}
|
|
297
|
+
// Efficiently update content only if changed
|
|
298
|
+
const titleEl = wrapper.querySelector(".ticket-chat-event-title");
|
|
299
|
+
const newTitle = entry.title || entry.method || "Update";
|
|
300
|
+
if (titleEl && titleEl.textContent !== newTitle) {
|
|
301
|
+
titleEl.textContent = newTitle;
|
|
302
|
+
}
|
|
303
|
+
const summaryEl = wrapper.querySelector(".ticket-chat-event-summary");
|
|
304
|
+
const newSummary = entry.summary || "";
|
|
305
|
+
if (summaryEl && summaryEl.textContent !== newSummary) {
|
|
306
|
+
summaryEl.textContent = newSummary;
|
|
307
|
+
}
|
|
308
|
+
const detailEl = wrapper.querySelector(".ticket-chat-event-detail");
|
|
309
|
+
const newDetail = entry.detail || "";
|
|
310
|
+
if (detailEl && detailEl.textContent !== newDetail) {
|
|
311
|
+
detailEl.textContent = newDetail;
|
|
312
|
+
}
|
|
313
|
+
const metaEl = wrapper.querySelector(".ticket-chat-event-meta");
|
|
314
|
+
if (metaEl) {
|
|
315
|
+
const newMeta = entry.time
|
|
316
|
+
? new Date(entry.time).toLocaleTimeString([], {
|
|
317
|
+
hour: "2-digit",
|
|
318
|
+
minute: "2-digit",
|
|
319
|
+
})
|
|
320
|
+
: "";
|
|
321
|
+
if (metaEl.textContent !== newMeta) {
|
|
322
|
+
metaEl.textContent = newMeta;
|
|
323
|
+
}
|
|
324
|
+
}
|
|
325
|
+
});
|
|
326
|
+
// Remove stale events
|
|
327
|
+
Array.from(list.children).forEach((child) => {
|
|
328
|
+
const el = child;
|
|
329
|
+
if (el.dataset.eventId && !currentIds.has(el.dataset.eventId)) {
|
|
330
|
+
el.remove();
|
|
331
|
+
}
|
|
332
|
+
});
|
|
333
|
+
// Only scroll if near bottom or if height changed significantly?
|
|
334
|
+
// For now, just scroll as it's the expected behavior for live logs
|
|
335
|
+
list.scrollTop = list.scrollHeight;
|
|
336
|
+
}
|
|
337
|
+
function renderLiveOutputCompact() {
|
|
338
|
+
const compactEl = document.getElementById("ticket-live-output-compact");
|
|
339
|
+
if (!compactEl)
|
|
340
|
+
return;
|
|
341
|
+
const summary = summarizeEvents(liveOutputEvents, {
|
|
342
|
+
maxActions: 1, // Show only 1 action + thinking to fit in 3-line compact view
|
|
343
|
+
maxTextLength: COMPACT_MAX_TEXT_LENGTH,
|
|
344
|
+
startTime: flowStartedAt?.getTime(),
|
|
345
|
+
});
|
|
346
|
+
const text = liveOutputEvents.length ? renderCompactSummary(summary) : "";
|
|
347
|
+
const newText = text || "Waiting for agent output...";
|
|
348
|
+
if (compactEl.textContent !== newText) {
|
|
349
|
+
compactEl.textContent = newText;
|
|
350
|
+
}
|
|
351
|
+
}
|
|
352
|
+
function updateLiveOutputViewToggle() {
|
|
353
|
+
const viewToggle = document.getElementById("ticket-live-output-view-toggle");
|
|
354
|
+
if (!viewToggle)
|
|
355
|
+
return;
|
|
356
|
+
if (liveOutputDetailExpanded) {
|
|
357
|
+
if (!viewToggle.classList.contains("active"))
|
|
358
|
+
viewToggle.classList.add("active");
|
|
359
|
+
if (viewToggle.textContent !== "≡")
|
|
360
|
+
viewToggle.textContent = "≡";
|
|
361
|
+
if (viewToggle.title !== "Show summary")
|
|
362
|
+
viewToggle.title = "Show summary";
|
|
363
|
+
}
|
|
364
|
+
else {
|
|
365
|
+
if (viewToggle.classList.contains("active"))
|
|
366
|
+
viewToggle.classList.remove("active");
|
|
367
|
+
if (viewToggle.textContent !== "⋯")
|
|
368
|
+
viewToggle.textContent = "⋯";
|
|
369
|
+
if (viewToggle.title !== "Show full output")
|
|
370
|
+
viewToggle.title = "Show full output";
|
|
371
|
+
}
|
|
372
|
+
}
|
|
373
|
+
function renderLiveOutputView() {
|
|
374
|
+
const compactEl = document.getElementById("ticket-live-output-compact");
|
|
375
|
+
const detailEl = document.getElementById("ticket-live-output-detail");
|
|
376
|
+
const eventsEl = document.getElementById("ticket-live-output-events");
|
|
377
|
+
if (compactEl) {
|
|
378
|
+
compactEl.classList.toggle("hidden", liveOutputDetailExpanded);
|
|
379
|
+
}
|
|
380
|
+
if (detailEl) {
|
|
381
|
+
detailEl.classList.toggle("hidden", !liveOutputDetailExpanded);
|
|
382
|
+
}
|
|
383
|
+
if (eventsEl) {
|
|
384
|
+
eventsEl.classList.toggle("hidden", !liveOutputDetailExpanded);
|
|
385
|
+
}
|
|
386
|
+
renderLiveOutputCompact();
|
|
387
|
+
renderLiveOutputEvents();
|
|
388
|
+
updateLiveOutputViewToggle();
|
|
389
|
+
}
|
|
390
|
+
function clearLiveOutput() {
|
|
391
|
+
liveOutputBuffer = [];
|
|
392
|
+
const outputEl = document.getElementById("ticket-live-output-text");
|
|
393
|
+
if (outputEl)
|
|
394
|
+
outputEl.textContent = "";
|
|
395
|
+
liveOutputEvents = [];
|
|
396
|
+
liveOutputEventIndex = {};
|
|
397
|
+
scheduleLiveOutputRender();
|
|
398
|
+
}
|
|
399
|
+
function setLiveOutputStatus(status) {
|
|
400
|
+
const statusEl = document.getElementById("ticket-live-output-status");
|
|
401
|
+
if (!statusEl)
|
|
402
|
+
return;
|
|
403
|
+
statusEl.className = "ticket-live-output-status";
|
|
404
|
+
switch (status) {
|
|
405
|
+
case "disconnected":
|
|
406
|
+
statusEl.textContent = "Disconnected";
|
|
407
|
+
break;
|
|
408
|
+
case "connected":
|
|
409
|
+
statusEl.textContent = "Connected";
|
|
410
|
+
statusEl.classList.add("connected");
|
|
411
|
+
break;
|
|
412
|
+
case "streaming":
|
|
413
|
+
statusEl.textContent = "Streaming";
|
|
414
|
+
statusEl.classList.add("streaming");
|
|
415
|
+
break;
|
|
416
|
+
}
|
|
417
|
+
}
|
|
418
|
+
function handleFlowEvent(event) {
|
|
419
|
+
// Update last activity time
|
|
420
|
+
lastActivityTime = new Date(event.timestamp);
|
|
421
|
+
updateLastActivityDisplay();
|
|
422
|
+
// Handle agent stream delta events
|
|
423
|
+
if (event.event_type === "agent_stream_delta") {
|
|
424
|
+
setLiveOutputStatus("streaming");
|
|
425
|
+
const delta = event.data?.delta || "";
|
|
426
|
+
if (delta) {
|
|
427
|
+
appendToLiveOutput(delta);
|
|
428
|
+
}
|
|
429
|
+
}
|
|
430
|
+
// Handle rich app-server events (tools, commands, files, thinking, etc.)
|
|
431
|
+
if (event.event_type === "app_server_event") {
|
|
432
|
+
const parsed = parseAppServerEvent(event.data);
|
|
433
|
+
if (parsed) {
|
|
434
|
+
addLiveOutputEvent(parsed);
|
|
435
|
+
scheduleLiveOutputRender();
|
|
436
|
+
}
|
|
437
|
+
}
|
|
438
|
+
// Handle flow lifecycle events
|
|
439
|
+
if (event.event_type === "flow_completed" ||
|
|
440
|
+
event.event_type === "flow_failed" ||
|
|
441
|
+
event.event_type === "flow_stopped") {
|
|
442
|
+
setLiveOutputStatus("connected");
|
|
443
|
+
// Refresh the flow state
|
|
444
|
+
void loadTicketFlow();
|
|
445
|
+
}
|
|
446
|
+
// Handle step events
|
|
447
|
+
if (event.event_type === "step_started") {
|
|
448
|
+
const stepName = event.data?.step_name || "";
|
|
449
|
+
if (stepName) {
|
|
450
|
+
appendToLiveOutput(`\n--- Step: ${stepName} ---\n`);
|
|
451
|
+
}
|
|
452
|
+
}
|
|
453
|
+
}
|
|
454
|
+
function connectEventStream(runId) {
|
|
455
|
+
disconnectEventStream();
|
|
456
|
+
const token = getAuthToken();
|
|
457
|
+
let url = resolvePath(`/api/flows/${runId}/events`);
|
|
458
|
+
if (token) {
|
|
459
|
+
url += `?token=${encodeURIComponent(token)}`;
|
|
460
|
+
}
|
|
461
|
+
eventSource = new EventSource(url);
|
|
462
|
+
eventSource.onopen = () => {
|
|
463
|
+
setLiveOutputStatus("connected");
|
|
464
|
+
};
|
|
465
|
+
eventSource.onmessage = (event) => {
|
|
466
|
+
try {
|
|
467
|
+
const data = JSON.parse(event.data);
|
|
468
|
+
handleFlowEvent(data);
|
|
469
|
+
}
|
|
470
|
+
catch (err) {
|
|
471
|
+
// Ignore parse errors
|
|
472
|
+
}
|
|
473
|
+
};
|
|
474
|
+
eventSource.onerror = () => {
|
|
475
|
+
setLiveOutputStatus("disconnected");
|
|
476
|
+
// Don't auto-reconnect here - loadTicketFlow will handle it
|
|
477
|
+
};
|
|
478
|
+
}
|
|
479
|
+
function disconnectEventStream() {
|
|
480
|
+
if (eventSource) {
|
|
481
|
+
eventSource.close();
|
|
482
|
+
eventSource = null;
|
|
483
|
+
}
|
|
484
|
+
setLiveOutputStatus("disconnected");
|
|
485
|
+
}
|
|
486
|
+
function initLiveOutputPanel() {
|
|
487
|
+
const viewToggleBtn = document.getElementById("ticket-live-output-view-toggle");
|
|
488
|
+
// Toggle between summary and full view (one click)
|
|
489
|
+
const toggleView = () => {
|
|
490
|
+
liveOutputDetailExpanded = !liveOutputDetailExpanded;
|
|
491
|
+
renderLiveOutputView();
|
|
492
|
+
};
|
|
493
|
+
if (viewToggleBtn) {
|
|
494
|
+
viewToggleBtn.addEventListener("click", toggleView);
|
|
495
|
+
}
|
|
496
|
+
// Initial render
|
|
497
|
+
updateLiveOutputViewToggle();
|
|
498
|
+
renderLiveOutputView();
|
|
499
|
+
}
|
|
500
|
+
/**
|
|
501
|
+
* Initialize the reason modal click handler.
|
|
502
|
+
*/
|
|
503
|
+
function initReasonModal() {
|
|
504
|
+
const reasonEl = document.getElementById("ticket-flow-reason");
|
|
505
|
+
const modalOverlay = document.getElementById("reason-modal");
|
|
506
|
+
const modalContent = document.getElementById("reason-modal-content");
|
|
507
|
+
const closeBtn = document.getElementById("reason-modal-close");
|
|
508
|
+
if (!reasonEl || !modalOverlay || !modalContent)
|
|
509
|
+
return;
|
|
510
|
+
let closeModal = null;
|
|
511
|
+
const showReasonModal = () => {
|
|
512
|
+
if (!currentReasonFull || !reasonEl.classList.contains("has-details"))
|
|
513
|
+
return;
|
|
514
|
+
modalContent.textContent = currentReasonFull;
|
|
515
|
+
closeModal = openModal(modalOverlay, {
|
|
516
|
+
closeOnEscape: true,
|
|
517
|
+
closeOnOverlay: true,
|
|
518
|
+
returnFocusTo: reasonEl,
|
|
519
|
+
});
|
|
520
|
+
};
|
|
521
|
+
reasonEl.addEventListener("click", showReasonModal);
|
|
522
|
+
if (closeBtn) {
|
|
523
|
+
closeBtn.addEventListener("click", () => {
|
|
524
|
+
if (closeModal)
|
|
525
|
+
closeModal();
|
|
526
|
+
});
|
|
527
|
+
}
|
|
528
|
+
}
|
|
529
|
+
function els() {
|
|
530
|
+
return {
|
|
531
|
+
card: document.getElementById("ticket-card"),
|
|
532
|
+
status: document.getElementById("ticket-flow-status"),
|
|
533
|
+
run: document.getElementById("ticket-flow-run"),
|
|
534
|
+
current: document.getElementById("ticket-flow-current"),
|
|
535
|
+
turn: document.getElementById("ticket-flow-turn"),
|
|
536
|
+
elapsed: document.getElementById("ticket-flow-elapsed"),
|
|
537
|
+
progress: document.getElementById("ticket-flow-progress"),
|
|
538
|
+
reason: document.getElementById("ticket-flow-reason"),
|
|
539
|
+
lastActivity: document.getElementById("ticket-flow-last-activity"),
|
|
540
|
+
dir: document.getElementById("ticket-flow-dir"),
|
|
541
|
+
tickets: document.getElementById("ticket-flow-tickets"),
|
|
542
|
+
history: document.getElementById("ticket-dispatch-history"),
|
|
543
|
+
dispatchNote: document.getElementById("ticket-dispatch-note"),
|
|
544
|
+
dispatchPanel: document.getElementById("dispatch-panel"),
|
|
545
|
+
dispatchPanelToggle: document.getElementById("dispatch-panel-toggle"),
|
|
546
|
+
dispatchMiniList: document.getElementById("dispatch-mini-list"),
|
|
547
|
+
bootstrapBtn: document.getElementById("ticket-flow-bootstrap"),
|
|
548
|
+
resumeBtn: document.getElementById("ticket-flow-resume"),
|
|
549
|
+
refreshBtn: document.getElementById("ticket-flow-refresh"),
|
|
550
|
+
stopBtn: document.getElementById("ticket-flow-stop"),
|
|
551
|
+
restartBtn: document.getElementById("ticket-flow-restart"),
|
|
552
|
+
archiveBtn: document.getElementById("ticket-flow-archive"),
|
|
553
|
+
};
|
|
554
|
+
}
|
|
555
|
+
function setButtonsDisabled(disabled) {
|
|
556
|
+
const { bootstrapBtn, resumeBtn, refreshBtn, stopBtn, restartBtn, archiveBtn } = els();
|
|
557
|
+
[bootstrapBtn, resumeBtn, refreshBtn, stopBtn, restartBtn, archiveBtn].forEach((btn) => {
|
|
558
|
+
if (btn)
|
|
559
|
+
btn.disabled = disabled;
|
|
560
|
+
});
|
|
561
|
+
}
|
|
562
|
+
function truncate(text, max = 100) {
|
|
563
|
+
if (text.length <= max)
|
|
564
|
+
return text;
|
|
565
|
+
return `${text.slice(0, max).trim()}…`;
|
|
566
|
+
}
|
|
567
|
+
function renderTickets(data) {
|
|
568
|
+
const { tickets, dir, bootstrapBtn } = els();
|
|
569
|
+
if (dir)
|
|
570
|
+
dir.textContent = data?.ticket_dir || "–";
|
|
571
|
+
if (!tickets)
|
|
572
|
+
return;
|
|
573
|
+
tickets.innerHTML = "";
|
|
574
|
+
const list = (data?.tickets || []);
|
|
575
|
+
ticketsExist = list.length > 0;
|
|
576
|
+
// Disable start button if no tickets exist
|
|
577
|
+
if (bootstrapBtn && !bootstrapBtn.disabled) {
|
|
578
|
+
bootstrapBtn.disabled = !ticketsExist;
|
|
579
|
+
if (!ticketsExist) {
|
|
580
|
+
bootstrapBtn.title = "Create a ticket first";
|
|
581
|
+
}
|
|
582
|
+
else {
|
|
583
|
+
bootstrapBtn.title = "";
|
|
584
|
+
}
|
|
585
|
+
}
|
|
586
|
+
if (!list.length) {
|
|
587
|
+
tickets.textContent = "No tickets found. Create TICKET-001.md to begin.";
|
|
588
|
+
return;
|
|
589
|
+
}
|
|
590
|
+
list.forEach((ticket) => {
|
|
591
|
+
const item = document.createElement("div");
|
|
592
|
+
const fm = (ticket.frontmatter || {});
|
|
593
|
+
const done = Boolean(fm?.done);
|
|
594
|
+
// Check if this ticket is currently being worked on
|
|
595
|
+
const isActive = currentActiveTicket && ticket.path === currentActiveTicket && currentFlowStatus === "running";
|
|
596
|
+
item.className = `ticket-item ${done ? "done" : ""} ${isActive ? "active" : ""} clickable`;
|
|
597
|
+
item.title = "Click to edit";
|
|
598
|
+
// Make ticket item clickable to open editor
|
|
599
|
+
item.addEventListener("click", () => {
|
|
600
|
+
openTicketEditor(ticket);
|
|
601
|
+
});
|
|
602
|
+
const head = document.createElement("div");
|
|
603
|
+
head.className = "ticket-item-head";
|
|
604
|
+
// Extract ticket number from path (e.g., "TICKET-001" from ".codex-autorunner/tickets/TICKET-001.md")
|
|
605
|
+
const ticketPath = ticket.path || "";
|
|
606
|
+
const ticketMatch = ticketPath.match(/TICKET-\d+/);
|
|
607
|
+
const ticketNumber = ticketMatch ? ticketMatch[0] : "TICKET";
|
|
608
|
+
const ticketTitle = fm?.title ? String(fm.title) : "";
|
|
609
|
+
const name = document.createElement("span");
|
|
610
|
+
name.className = "ticket-name";
|
|
611
|
+
// Split number and title into separate spans for responsive control
|
|
612
|
+
const numSpan = document.createElement("span");
|
|
613
|
+
numSpan.className = "ticket-num";
|
|
614
|
+
// Extract just the number (e.g., "001" from "TICKET-001")
|
|
615
|
+
const numMatch = ticketNumber.match(/\d+/);
|
|
616
|
+
numSpan.textContent = numMatch ? numMatch[0] : ticketNumber;
|
|
617
|
+
name.appendChild(numSpan);
|
|
618
|
+
if (ticketTitle) {
|
|
619
|
+
const titleSpan = document.createElement("span");
|
|
620
|
+
titleSpan.className = "ticket-title-text";
|
|
621
|
+
titleSpan.textContent = `: ${ticketTitle}`;
|
|
622
|
+
name.appendChild(titleSpan);
|
|
623
|
+
}
|
|
624
|
+
// Set full text as title attribute for tooltip on hover
|
|
625
|
+
item.title = ticketTitle ? `${ticketNumber}: ${ticketTitle}` : ticketNumber;
|
|
626
|
+
head.appendChild(name);
|
|
627
|
+
// Badge container for status + agent badges
|
|
628
|
+
const badges = document.createElement("span");
|
|
629
|
+
badges.className = "ticket-badges";
|
|
630
|
+
// Add WORKING badge for active ticket (to the left of agent badge)
|
|
631
|
+
if (isActive) {
|
|
632
|
+
const workingBadge = document.createElement("span");
|
|
633
|
+
workingBadge.className = "ticket-working-badge";
|
|
634
|
+
workingBadge.textContent = "Working";
|
|
635
|
+
badges.appendChild(workingBadge);
|
|
636
|
+
}
|
|
637
|
+
// Add DONE badge for completed tickets
|
|
638
|
+
if (done && !isActive) {
|
|
639
|
+
const doneBadge = document.createElement("span");
|
|
640
|
+
doneBadge.className = "ticket-done-badge";
|
|
641
|
+
doneBadge.textContent = "Done";
|
|
642
|
+
badges.appendChild(doneBadge);
|
|
643
|
+
}
|
|
644
|
+
const agent = document.createElement("span");
|
|
645
|
+
agent.className = "ticket-agent";
|
|
646
|
+
agent.textContent = fm?.agent || "codex";
|
|
647
|
+
badges.appendChild(agent);
|
|
648
|
+
head.appendChild(badges);
|
|
649
|
+
item.appendChild(head);
|
|
650
|
+
if (ticket.errors && ticket.errors.length) {
|
|
651
|
+
const errors = document.createElement("div");
|
|
652
|
+
errors.className = "ticket-errors";
|
|
653
|
+
errors.textContent = `Frontmatter issues: ${ticket.errors.join("; ")}`;
|
|
654
|
+
item.appendChild(errors);
|
|
655
|
+
}
|
|
656
|
+
if (ticket.body) {
|
|
657
|
+
const body = document.createElement("div");
|
|
658
|
+
body.className = "ticket-body";
|
|
659
|
+
body.textContent = truncate(ticket.body.replace(/\s+/g, " ").trim());
|
|
660
|
+
item.appendChild(body);
|
|
661
|
+
}
|
|
662
|
+
tickets.appendChild(item);
|
|
663
|
+
});
|
|
664
|
+
}
|
|
665
|
+
function renderDispatchHistory(runId, data) {
|
|
666
|
+
const { history, dispatchNote } = els();
|
|
667
|
+
if (!history)
|
|
668
|
+
return;
|
|
669
|
+
history.innerHTML = "";
|
|
670
|
+
const { dispatchMiniList } = els();
|
|
671
|
+
if (!runId) {
|
|
672
|
+
history.textContent = "Start the ticket flow to see agent dispatches.";
|
|
673
|
+
if (dispatchNote)
|
|
674
|
+
dispatchNote.textContent = "–";
|
|
675
|
+
if (dispatchMiniList)
|
|
676
|
+
dispatchMiniList.innerHTML = "";
|
|
677
|
+
return;
|
|
678
|
+
}
|
|
679
|
+
const entries = (data?.history || []);
|
|
680
|
+
if (!entries.length) {
|
|
681
|
+
history.textContent = "No dispatches yet.";
|
|
682
|
+
if (dispatchNote)
|
|
683
|
+
dispatchNote.textContent = "–";
|
|
684
|
+
if (dispatchMiniList)
|
|
685
|
+
dispatchMiniList.innerHTML = "";
|
|
686
|
+
return;
|
|
687
|
+
}
|
|
688
|
+
if (dispatchNote)
|
|
689
|
+
dispatchNote.textContent = `Latest #${entries[0]?.seq ?? "–"}`;
|
|
690
|
+
// Also render mini list for collapsed panel view
|
|
691
|
+
renderDispatchMiniList(entries);
|
|
692
|
+
entries.forEach((entry) => {
|
|
693
|
+
const dispatch = entry.dispatch;
|
|
694
|
+
const isTurnSummary = dispatch?.mode === "turn_summary" || dispatch?.extra?.is_turn_summary;
|
|
695
|
+
const isHandoff = dispatch?.mode === "pause";
|
|
696
|
+
const container = document.createElement("div");
|
|
697
|
+
container.className = `dispatch-item${isTurnSummary ? " turn-summary" : ""} clickable`;
|
|
698
|
+
container.title = isTurnSummary ? "Agent turn output" : "Click to view in Inbox";
|
|
699
|
+
// Add click handler to navigate to inbox (skip for turn summaries)
|
|
700
|
+
if (!isTurnSummary) {
|
|
701
|
+
container.addEventListener("click", () => {
|
|
702
|
+
if (runId) {
|
|
703
|
+
// Update URL with run_id so inbox tab loads the right thread
|
|
704
|
+
const url = new URL(window.location.href);
|
|
705
|
+
url.searchParams.set("run_id", runId);
|
|
706
|
+
window.history.replaceState({}, "", url.toString());
|
|
707
|
+
// Switch to inbox tab
|
|
708
|
+
activateTab("inbox");
|
|
709
|
+
}
|
|
710
|
+
});
|
|
711
|
+
}
|
|
712
|
+
// Determine mode label
|
|
713
|
+
let modeLabel;
|
|
714
|
+
if (isTurnSummary) {
|
|
715
|
+
modeLabel = "TURN";
|
|
716
|
+
}
|
|
717
|
+
else if (isHandoff) {
|
|
718
|
+
modeLabel = "HANDOFF";
|
|
719
|
+
}
|
|
720
|
+
else {
|
|
721
|
+
modeLabel = (dispatch?.mode || "notify").toUpperCase();
|
|
722
|
+
}
|
|
723
|
+
const head = document.createElement("div");
|
|
724
|
+
head.className = "dispatch-item-head";
|
|
725
|
+
const seq = document.createElement("span");
|
|
726
|
+
seq.className = "ticket-name";
|
|
727
|
+
seq.textContent = `#${entry.seq || "?"}`;
|
|
728
|
+
const mode = document.createElement("span");
|
|
729
|
+
mode.className = `ticket-agent${isTurnSummary ? " turn-summary-badge" : ""}`;
|
|
730
|
+
mode.textContent = modeLabel;
|
|
731
|
+
head.append(seq, mode);
|
|
732
|
+
// Add ticket reference if present
|
|
733
|
+
const ticketId = dispatch?.extra?.ticket_id;
|
|
734
|
+
if (ticketId) {
|
|
735
|
+
// Extract ticket number from path (e.g., "TICKET-009" from ".codex-autorunner/tickets/TICKET-009.md")
|
|
736
|
+
const ticketMatch = ticketId.match(/TICKET-\d+/);
|
|
737
|
+
if (ticketMatch) {
|
|
738
|
+
const ticketLabel = document.createElement("span");
|
|
739
|
+
ticketLabel.className = "dispatch-ticket-ref";
|
|
740
|
+
ticketLabel.textContent = ticketMatch[0];
|
|
741
|
+
ticketLabel.title = ticketId;
|
|
742
|
+
head.appendChild(ticketLabel);
|
|
743
|
+
}
|
|
744
|
+
}
|
|
745
|
+
container.appendChild(head);
|
|
746
|
+
if (entry.errors && entry.errors.length) {
|
|
747
|
+
const err = document.createElement("div");
|
|
748
|
+
err.className = "ticket-errors";
|
|
749
|
+
err.textContent = entry.errors.join("; ");
|
|
750
|
+
container.appendChild(err);
|
|
751
|
+
}
|
|
752
|
+
const title = dispatch?.title;
|
|
753
|
+
if (title) {
|
|
754
|
+
const titleEl = document.createElement("div");
|
|
755
|
+
titleEl.className = "ticket-body ticket-dispatch-title";
|
|
756
|
+
titleEl.textContent = title;
|
|
757
|
+
container.appendChild(titleEl);
|
|
758
|
+
}
|
|
759
|
+
const bodyText = dispatch?.body;
|
|
760
|
+
if (bodyText) {
|
|
761
|
+
const body = document.createElement("div");
|
|
762
|
+
body.className = "ticket-body ticket-dispatch-body messages-markdown";
|
|
763
|
+
body.innerHTML = renderMarkdown(bodyText);
|
|
764
|
+
container.appendChild(body);
|
|
765
|
+
}
|
|
766
|
+
const attachments = (entry.attachments || []);
|
|
767
|
+
if (attachments.length) {
|
|
768
|
+
const wrap = document.createElement("div");
|
|
769
|
+
wrap.className = "ticket-attachments";
|
|
770
|
+
attachments.forEach((att) => {
|
|
771
|
+
if (!att.url)
|
|
772
|
+
return;
|
|
773
|
+
const link = document.createElement("a");
|
|
774
|
+
link.href = resolvePath(att.url);
|
|
775
|
+
link.textContent = att.name || att.rel_path || "attachment";
|
|
776
|
+
link.target = "_blank";
|
|
777
|
+
link.rel = "noreferrer noopener";
|
|
778
|
+
link.title = att.path || "";
|
|
779
|
+
wrap.appendChild(link);
|
|
780
|
+
});
|
|
781
|
+
container.appendChild(wrap);
|
|
782
|
+
}
|
|
783
|
+
history.appendChild(container);
|
|
784
|
+
});
|
|
785
|
+
}
|
|
786
|
+
const MAX_REASON_LENGTH = 60;
|
|
787
|
+
/**
|
|
788
|
+
* Get the full reason text (summary + details) for modal display.
|
|
789
|
+
*/
|
|
790
|
+
function getFullReason(run) {
|
|
791
|
+
if (!run)
|
|
792
|
+
return null;
|
|
793
|
+
const state = (run.state || {});
|
|
794
|
+
const engine = (state.ticket_engine || {});
|
|
795
|
+
const reason = engine.reason || run.error_message || "";
|
|
796
|
+
const details = engine.reason_details || "";
|
|
797
|
+
if (!reason && !details)
|
|
798
|
+
return null;
|
|
799
|
+
if (details) {
|
|
800
|
+
return `${reason}\n\n${details}`.trim();
|
|
801
|
+
}
|
|
802
|
+
return reason;
|
|
803
|
+
}
|
|
804
|
+
/**
|
|
805
|
+
* Get a truncated reason summary for display in the grid.
|
|
806
|
+
* Also updates currentReasonFull for modal access.
|
|
807
|
+
*/
|
|
808
|
+
function summarizeReason(run) {
|
|
809
|
+
if (!run) {
|
|
810
|
+
currentReasonFull = null;
|
|
811
|
+
return "No ticket flow run yet.";
|
|
812
|
+
}
|
|
813
|
+
const state = (run.state || {});
|
|
814
|
+
const engine = (state.ticket_engine || {});
|
|
815
|
+
const fullReason = getFullReason(run);
|
|
816
|
+
currentReasonFull = fullReason;
|
|
817
|
+
const shortReason = engine.reason ||
|
|
818
|
+
run.error_message ||
|
|
819
|
+
(engine.current_ticket ? `Working on ${engine.current_ticket}` : "") ||
|
|
820
|
+
run.status ||
|
|
821
|
+
"";
|
|
822
|
+
// Truncate if too long
|
|
823
|
+
if (shortReason.length > MAX_REASON_LENGTH) {
|
|
824
|
+
return shortReason.slice(0, MAX_REASON_LENGTH - 3) + "...";
|
|
825
|
+
}
|
|
826
|
+
return shortReason;
|
|
827
|
+
}
|
|
828
|
+
async function loadTicketFiles() {
|
|
829
|
+
const { tickets } = els();
|
|
830
|
+
if (tickets)
|
|
831
|
+
tickets.textContent = "Loading tickets…";
|
|
832
|
+
try {
|
|
833
|
+
const data = (await api("/api/flows/ticket_flow/tickets"));
|
|
834
|
+
renderTickets(data);
|
|
835
|
+
}
|
|
836
|
+
catch (err) {
|
|
837
|
+
renderTickets(null);
|
|
838
|
+
flash(err.message || "Failed to load tickets", "error");
|
|
839
|
+
}
|
|
840
|
+
}
|
|
841
|
+
/**
|
|
842
|
+
* Open a ticket by its index
|
|
843
|
+
*/
|
|
844
|
+
async function openTicketByIndex(index) {
|
|
845
|
+
try {
|
|
846
|
+
const data = (await api("/api/flows/ticket_flow/tickets"));
|
|
847
|
+
const ticket = data.tickets?.find((t) => t.index === index);
|
|
848
|
+
if (ticket) {
|
|
849
|
+
openTicketEditor(ticket);
|
|
850
|
+
}
|
|
851
|
+
else {
|
|
852
|
+
flash(`Ticket TICKET-${String(index).padStart(3, "0")} not found`, "error");
|
|
853
|
+
}
|
|
854
|
+
}
|
|
855
|
+
catch (err) {
|
|
856
|
+
flash(`Failed to open ticket: ${err.message}`, "error");
|
|
857
|
+
}
|
|
858
|
+
}
|
|
859
|
+
async function loadDispatchHistory(runId) {
|
|
860
|
+
const { history } = els();
|
|
861
|
+
if (history)
|
|
862
|
+
history.textContent = "Loading dispatch history…";
|
|
863
|
+
if (!runId) {
|
|
864
|
+
renderDispatchHistory(null, null);
|
|
865
|
+
return;
|
|
866
|
+
}
|
|
867
|
+
try {
|
|
868
|
+
// Use dispatch_history endpoint
|
|
869
|
+
const data = (await api(`/api/flows/${runId}/dispatch_history`));
|
|
870
|
+
renderDispatchHistory(runId, data);
|
|
871
|
+
}
|
|
872
|
+
catch (err) {
|
|
873
|
+
renderDispatchHistory(runId, null);
|
|
874
|
+
flash(err.message || "Failed to load dispatch history", "error");
|
|
875
|
+
}
|
|
876
|
+
}
|
|
877
|
+
async function loadTicketFlow() {
|
|
878
|
+
const { status, run, current, turn, elapsed, progress, reason, lastActivity, resumeBtn, bootstrapBtn, stopBtn, archiveBtn } = els();
|
|
879
|
+
if (!isRepoHealthy()) {
|
|
880
|
+
if (status)
|
|
881
|
+
statusPill(status, "error");
|
|
882
|
+
if (run)
|
|
883
|
+
run.textContent = "–";
|
|
884
|
+
if (current)
|
|
885
|
+
current.textContent = "–";
|
|
886
|
+
if (turn)
|
|
887
|
+
turn.textContent = "–";
|
|
888
|
+
if (elapsed)
|
|
889
|
+
elapsed.textContent = "–";
|
|
890
|
+
if (progress)
|
|
891
|
+
progress.textContent = "–";
|
|
892
|
+
if (lastActivity)
|
|
893
|
+
lastActivity.textContent = "–";
|
|
894
|
+
if (reason)
|
|
895
|
+
reason.textContent = "Repo offline or uninitialized.";
|
|
896
|
+
setButtonsDisabled(true);
|
|
897
|
+
stopElapsedTimer();
|
|
898
|
+
stopLastActivityTimer();
|
|
899
|
+
disconnectEventStream();
|
|
900
|
+
return;
|
|
901
|
+
}
|
|
902
|
+
try {
|
|
903
|
+
const runs = (await api("/api/flows/runs?flow_type=ticket_flow"));
|
|
904
|
+
// Only consider the newest run - if it's terminal, flow is idle.
|
|
905
|
+
// This matches the backend's _active_or_paused_run() logic which only checks runs[0].
|
|
906
|
+
// Using find() would incorrectly pick up older paused runs when a newer run has completed.
|
|
907
|
+
const newest = runs?.[0] || null;
|
|
908
|
+
// Keep the newest run even if terminal, so we can archive it or see its final state
|
|
909
|
+
const latest = newest;
|
|
910
|
+
currentRunId = latest?.id || null;
|
|
911
|
+
currentFlowStatus = latest?.status || null;
|
|
912
|
+
// Extract ticket engine state
|
|
913
|
+
const ticketEngine = latest?.state?.ticket_engine;
|
|
914
|
+
currentActiveTicket = ticketEngine?.current_ticket || null;
|
|
915
|
+
const ticketTurns = ticketEngine?.ticket_turns ?? null;
|
|
916
|
+
const totalTurns = ticketEngine?.total_turns ?? null;
|
|
917
|
+
if (status)
|
|
918
|
+
statusPill(status, latest?.status || "idle");
|
|
919
|
+
if (run)
|
|
920
|
+
run.textContent = latest?.id || "–";
|
|
921
|
+
if (current)
|
|
922
|
+
current.textContent = currentActiveTicket || "–";
|
|
923
|
+
// Display turn counter
|
|
924
|
+
if (turn) {
|
|
925
|
+
if (ticketTurns !== null && currentFlowStatus === "running") {
|
|
926
|
+
turn.textContent = `${ticketTurns}${totalTurns !== null ? ` (${totalTurns} total)` : ""}`;
|
|
927
|
+
}
|
|
928
|
+
else {
|
|
929
|
+
turn.textContent = "–";
|
|
930
|
+
}
|
|
931
|
+
}
|
|
932
|
+
// Handle elapsed time
|
|
933
|
+
if (latest?.started_at && (latest.status === "running" || latest.status === "pending")) {
|
|
934
|
+
flowStartedAt = new Date(latest.started_at);
|
|
935
|
+
startElapsedTimer();
|
|
936
|
+
}
|
|
937
|
+
else {
|
|
938
|
+
stopElapsedTimer();
|
|
939
|
+
flowStartedAt = null;
|
|
940
|
+
if (elapsed)
|
|
941
|
+
elapsed.textContent = "–";
|
|
942
|
+
}
|
|
943
|
+
if (reason) {
|
|
944
|
+
reason.textContent = summarizeReason(latest) || "–";
|
|
945
|
+
// Add clickable class if there are details to show
|
|
946
|
+
const state = (latest?.state || {});
|
|
947
|
+
const engine = (state.ticket_engine || {});
|
|
948
|
+
const hasDetails = Boolean(engine.reason_details ||
|
|
949
|
+
(currentReasonFull && currentReasonFull.length > MAX_REASON_LENGTH));
|
|
950
|
+
reason.classList.toggle("has-details", hasDetails);
|
|
951
|
+
}
|
|
952
|
+
if (resumeBtn) {
|
|
953
|
+
resumeBtn.disabled = !latest?.id || latest.status !== "paused";
|
|
954
|
+
}
|
|
955
|
+
if (stopBtn) {
|
|
956
|
+
const stoppable = latest?.status === "running" || latest?.status === "pending";
|
|
957
|
+
stopBtn.disabled = !latest?.id || !stoppable;
|
|
958
|
+
}
|
|
959
|
+
await loadTicketFiles();
|
|
960
|
+
// Calculate and display ticket progress (scoped to tickets container only)
|
|
961
|
+
if (progress) {
|
|
962
|
+
const ticketsContainer = document.getElementById("ticket-flow-tickets");
|
|
963
|
+
const doneCount = ticketsContainer?.querySelectorAll(".ticket-item.done").length ?? 0;
|
|
964
|
+
const totalCount = ticketsContainer?.querySelectorAll(".ticket-item").length ?? 0;
|
|
965
|
+
if (totalCount > 0) {
|
|
966
|
+
progress.textContent = `${doneCount} of ${totalCount} done`;
|
|
967
|
+
}
|
|
968
|
+
else {
|
|
969
|
+
progress.textContent = "–";
|
|
970
|
+
}
|
|
971
|
+
}
|
|
972
|
+
// Connect/disconnect event stream based on flow status
|
|
973
|
+
if (currentRunId && (latest?.status === "running" || latest?.status === "pending")) {
|
|
974
|
+
// Only connect if not already connected to this run
|
|
975
|
+
if (!eventSource || eventSource.url?.indexOf(currentRunId) === -1) {
|
|
976
|
+
connectEventStream(currentRunId);
|
|
977
|
+
startLastActivityTimer();
|
|
978
|
+
}
|
|
979
|
+
}
|
|
980
|
+
else {
|
|
981
|
+
disconnectEventStream();
|
|
982
|
+
stopLastActivityTimer();
|
|
983
|
+
if (lastActivity)
|
|
984
|
+
lastActivity.textContent = "–";
|
|
985
|
+
lastActivityTime = null;
|
|
986
|
+
}
|
|
987
|
+
if (bootstrapBtn) {
|
|
988
|
+
const busy = latest?.status === "running" || latest?.status === "pending";
|
|
989
|
+
// Disable if busy OR if no tickets exist
|
|
990
|
+
bootstrapBtn.disabled = busy || !ticketsExist;
|
|
991
|
+
bootstrapBtn.textContent = busy ? "Running…" : "Start Ticket Flow";
|
|
992
|
+
if (!ticketsExist && !busy) {
|
|
993
|
+
bootstrapBtn.title = "Create a ticket first";
|
|
994
|
+
}
|
|
995
|
+
else {
|
|
996
|
+
bootstrapBtn.title = "";
|
|
997
|
+
}
|
|
998
|
+
}
|
|
999
|
+
// Show restart button when flow is paused, stopping, or in terminal state (allows starting fresh)
|
|
1000
|
+
const { restartBtn } = els();
|
|
1001
|
+
if (restartBtn) {
|
|
1002
|
+
const isPaused = latest?.status === "paused";
|
|
1003
|
+
const isStopping = latest?.status === "stopping";
|
|
1004
|
+
const isTerminal = latest?.status === "completed" ||
|
|
1005
|
+
latest?.status === "stopped" ||
|
|
1006
|
+
latest?.status === "failed";
|
|
1007
|
+
const canRestart = (isPaused || isStopping || isTerminal) && ticketsExist && Boolean(currentRunId);
|
|
1008
|
+
restartBtn.style.display = canRestart ? "" : "none";
|
|
1009
|
+
restartBtn.disabled = !canRestart;
|
|
1010
|
+
}
|
|
1011
|
+
// Show archive button when flow is paused, stopping, or in terminal state and has tickets
|
|
1012
|
+
if (archiveBtn) {
|
|
1013
|
+
const isPaused = latest?.status === "paused";
|
|
1014
|
+
const isStopping = latest?.status === "stopping";
|
|
1015
|
+
const isTerminal = latest?.status === "completed" ||
|
|
1016
|
+
latest?.status === "stopped" ||
|
|
1017
|
+
latest?.status === "failed";
|
|
1018
|
+
const canArchive = (isPaused || isStopping || isTerminal) && ticketsExist && Boolean(currentRunId);
|
|
1019
|
+
archiveBtn.style.display = canArchive ? "" : "none";
|
|
1020
|
+
archiveBtn.disabled = !canArchive;
|
|
1021
|
+
}
|
|
1022
|
+
await loadDispatchHistory(currentRunId);
|
|
1023
|
+
}
|
|
1024
|
+
catch (err) {
|
|
1025
|
+
if (reason)
|
|
1026
|
+
reason.textContent = err.message || "Ticket flow unavailable";
|
|
1027
|
+
flash(err.message || "Failed to load ticket flow state", "error");
|
|
1028
|
+
}
|
|
1029
|
+
}
|
|
1030
|
+
async function bootstrapTicketFlow() {
|
|
1031
|
+
const { bootstrapBtn } = els();
|
|
1032
|
+
if (!bootstrapBtn)
|
|
1033
|
+
return;
|
|
1034
|
+
if (!isRepoHealthy()) {
|
|
1035
|
+
flash("Repo offline; cannot start ticket flow.", "error");
|
|
1036
|
+
return;
|
|
1037
|
+
}
|
|
1038
|
+
if (!ticketsExist) {
|
|
1039
|
+
flash("Create a ticket first before starting the flow.", "error");
|
|
1040
|
+
return;
|
|
1041
|
+
}
|
|
1042
|
+
setButtonsDisabled(true);
|
|
1043
|
+
bootstrapBtn.textContent = "Starting…";
|
|
1044
|
+
try {
|
|
1045
|
+
const res = (await api("/api/flows/ticket_flow/bootstrap", {
|
|
1046
|
+
method: "POST",
|
|
1047
|
+
body: {},
|
|
1048
|
+
}));
|
|
1049
|
+
currentRunId = res?.id || null;
|
|
1050
|
+
if (res?.state?.hint === "active_run_reused") {
|
|
1051
|
+
flash("Ticket flow already running; continuing existing run", "info");
|
|
1052
|
+
}
|
|
1053
|
+
else {
|
|
1054
|
+
flash("Ticket flow started");
|
|
1055
|
+
clearLiveOutput(); // Clear output for new run
|
|
1056
|
+
}
|
|
1057
|
+
await loadTicketFlow();
|
|
1058
|
+
}
|
|
1059
|
+
catch (err) {
|
|
1060
|
+
flash(err.message || "Failed to start ticket flow", "error");
|
|
1061
|
+
}
|
|
1062
|
+
finally {
|
|
1063
|
+
bootstrapBtn.textContent = "Start Ticket Flow";
|
|
1064
|
+
setButtonsDisabled(false);
|
|
1065
|
+
}
|
|
1066
|
+
}
|
|
1067
|
+
async function resumeTicketFlow() {
|
|
1068
|
+
const { resumeBtn } = els();
|
|
1069
|
+
if (!resumeBtn)
|
|
1070
|
+
return;
|
|
1071
|
+
if (!isRepoHealthy()) {
|
|
1072
|
+
flash("Repo offline; cannot resume ticket flow.", "error");
|
|
1073
|
+
return;
|
|
1074
|
+
}
|
|
1075
|
+
if (!currentRunId) {
|
|
1076
|
+
flash("No ticket flow run to resume", "info");
|
|
1077
|
+
return;
|
|
1078
|
+
}
|
|
1079
|
+
setButtonsDisabled(true);
|
|
1080
|
+
resumeBtn.textContent = "Resuming…";
|
|
1081
|
+
try {
|
|
1082
|
+
await api(`/api/flows/${currentRunId}/resume`, { method: "POST", body: {} });
|
|
1083
|
+
flash("Ticket flow resumed");
|
|
1084
|
+
await loadTicketFlow();
|
|
1085
|
+
}
|
|
1086
|
+
catch (err) {
|
|
1087
|
+
flash(err.message || "Failed to resume", "error");
|
|
1088
|
+
}
|
|
1089
|
+
finally {
|
|
1090
|
+
resumeBtn.textContent = "Resume";
|
|
1091
|
+
setButtonsDisabled(false);
|
|
1092
|
+
}
|
|
1093
|
+
}
|
|
1094
|
+
async function stopTicketFlow() {
|
|
1095
|
+
const { stopBtn } = els();
|
|
1096
|
+
if (!stopBtn)
|
|
1097
|
+
return;
|
|
1098
|
+
if (!isRepoHealthy()) {
|
|
1099
|
+
flash("Repo offline; cannot stop ticket flow.", "error");
|
|
1100
|
+
return;
|
|
1101
|
+
}
|
|
1102
|
+
if (!currentRunId) {
|
|
1103
|
+
flash("No ticket flow run to stop", "info");
|
|
1104
|
+
return;
|
|
1105
|
+
}
|
|
1106
|
+
setButtonsDisabled(true);
|
|
1107
|
+
stopBtn.textContent = "Stopping…";
|
|
1108
|
+
try {
|
|
1109
|
+
await api(`/api/flows/${currentRunId}/stop`, { method: "POST", body: {} });
|
|
1110
|
+
flash("Ticket flow stopping");
|
|
1111
|
+
await loadTicketFlow();
|
|
1112
|
+
}
|
|
1113
|
+
catch (err) {
|
|
1114
|
+
flash(err.message || "Failed to stop ticket flow", "error");
|
|
1115
|
+
}
|
|
1116
|
+
finally {
|
|
1117
|
+
stopBtn.textContent = "Stop";
|
|
1118
|
+
setButtonsDisabled(false);
|
|
1119
|
+
}
|
|
1120
|
+
}
|
|
1121
|
+
async function restartTicketFlow() {
|
|
1122
|
+
const { restartBtn } = els();
|
|
1123
|
+
if (!restartBtn)
|
|
1124
|
+
return;
|
|
1125
|
+
if (!isRepoHealthy()) {
|
|
1126
|
+
flash("Repo offline; cannot restart ticket flow.", "error");
|
|
1127
|
+
return;
|
|
1128
|
+
}
|
|
1129
|
+
if (!ticketsExist) {
|
|
1130
|
+
flash("Create a ticket first before restarting the flow.", "error");
|
|
1131
|
+
return;
|
|
1132
|
+
}
|
|
1133
|
+
if (!confirm("Restart ticket flow? This will stop the current run and start a new one.")) {
|
|
1134
|
+
return;
|
|
1135
|
+
}
|
|
1136
|
+
setButtonsDisabled(true);
|
|
1137
|
+
restartBtn.textContent = "Restarting…";
|
|
1138
|
+
try {
|
|
1139
|
+
// Stop the current run first if it exists
|
|
1140
|
+
if (currentRunId) {
|
|
1141
|
+
await api(`/api/flows/${currentRunId}/stop`, { method: "POST", body: {} });
|
|
1142
|
+
}
|
|
1143
|
+
// Start a new run with force_new to bypass reuse logic
|
|
1144
|
+
const res = (await api("/api/flows/ticket_flow/bootstrap", {
|
|
1145
|
+
method: "POST",
|
|
1146
|
+
body: { metadata: { force_new: true } },
|
|
1147
|
+
}));
|
|
1148
|
+
currentRunId = res?.id || null;
|
|
1149
|
+
flash("Ticket flow restarted");
|
|
1150
|
+
clearLiveOutput();
|
|
1151
|
+
await loadTicketFlow();
|
|
1152
|
+
}
|
|
1153
|
+
catch (err) {
|
|
1154
|
+
flash(err.message || "Failed to restart ticket flow", "error");
|
|
1155
|
+
}
|
|
1156
|
+
finally {
|
|
1157
|
+
restartBtn.textContent = "Restart";
|
|
1158
|
+
setButtonsDisabled(false);
|
|
1159
|
+
}
|
|
1160
|
+
}
|
|
1161
|
+
async function archiveTicketFlow() {
|
|
1162
|
+
const { archiveBtn, reason } = els();
|
|
1163
|
+
if (!archiveBtn)
|
|
1164
|
+
return;
|
|
1165
|
+
if (!isRepoHealthy()) {
|
|
1166
|
+
flash("Repo offline; cannot archive ticket flow.", "error");
|
|
1167
|
+
return;
|
|
1168
|
+
}
|
|
1169
|
+
if (!currentRunId) {
|
|
1170
|
+
flash("No ticket flow run to archive", "info");
|
|
1171
|
+
return;
|
|
1172
|
+
}
|
|
1173
|
+
if (!confirm("Archive all tickets from this flow? They will be moved to the run's artifact directory.")) {
|
|
1174
|
+
return;
|
|
1175
|
+
}
|
|
1176
|
+
setButtonsDisabled(true);
|
|
1177
|
+
archiveBtn.textContent = "Archiving…";
|
|
1178
|
+
try {
|
|
1179
|
+
// Force archive if flow is stuck in stopping or paused state
|
|
1180
|
+
const force = currentFlowStatus === "stopping" || currentFlowStatus === "paused";
|
|
1181
|
+
const res = (await api(`/api/flows/${currentRunId}/archive?force=${force}`, {
|
|
1182
|
+
method: "POST",
|
|
1183
|
+
body: {},
|
|
1184
|
+
}));
|
|
1185
|
+
const count = res?.tickets_archived ?? 0;
|
|
1186
|
+
flash(`Archived ${count} ticket${count !== 1 ? "s" : ""}`);
|
|
1187
|
+
clearLiveOutput();
|
|
1188
|
+
// Reset all state variables
|
|
1189
|
+
currentRunId = null;
|
|
1190
|
+
currentFlowStatus = null;
|
|
1191
|
+
currentActiveTicket = null;
|
|
1192
|
+
currentReasonFull = null;
|
|
1193
|
+
// Reset all UI elements to idle state directly (avoid re-fetching stale data)
|
|
1194
|
+
const { status, run, current, turn, elapsed, progress, lastActivity, bootstrapBtn, resumeBtn, stopBtn, restartBtn } = els();
|
|
1195
|
+
if (status)
|
|
1196
|
+
statusPill(status, "idle");
|
|
1197
|
+
if (run)
|
|
1198
|
+
run.textContent = "–";
|
|
1199
|
+
if (current)
|
|
1200
|
+
current.textContent = "–";
|
|
1201
|
+
if (turn)
|
|
1202
|
+
turn.textContent = "–";
|
|
1203
|
+
if (elapsed)
|
|
1204
|
+
elapsed.textContent = "–";
|
|
1205
|
+
if (progress)
|
|
1206
|
+
progress.textContent = "–";
|
|
1207
|
+
if (lastActivity)
|
|
1208
|
+
lastActivity.textContent = "–";
|
|
1209
|
+
if (reason) {
|
|
1210
|
+
reason.textContent = "No ticket flow run yet.";
|
|
1211
|
+
reason.classList.remove("has-details");
|
|
1212
|
+
}
|
|
1213
|
+
renderDispatchHistory(null, null);
|
|
1214
|
+
// Stop timers and disconnect event stream
|
|
1215
|
+
disconnectEventStream();
|
|
1216
|
+
stopElapsedTimer();
|
|
1217
|
+
stopLastActivityTimer();
|
|
1218
|
+
lastActivityTime = null;
|
|
1219
|
+
// Update button states for no active run
|
|
1220
|
+
if (bootstrapBtn) {
|
|
1221
|
+
bootstrapBtn.disabled = false;
|
|
1222
|
+
bootstrapBtn.textContent = "Start Ticket Flow";
|
|
1223
|
+
bootstrapBtn.title = "";
|
|
1224
|
+
}
|
|
1225
|
+
if (resumeBtn)
|
|
1226
|
+
resumeBtn.disabled = true;
|
|
1227
|
+
if (stopBtn)
|
|
1228
|
+
stopBtn.disabled = true;
|
|
1229
|
+
if (restartBtn)
|
|
1230
|
+
restartBtn.style.display = "none";
|
|
1231
|
+
if (archiveBtn)
|
|
1232
|
+
archiveBtn.style.display = "none";
|
|
1233
|
+
// Refresh inbox badge and ticket list (tickets were archived/moved)
|
|
1234
|
+
void refreshBell();
|
|
1235
|
+
await loadTicketFiles();
|
|
1236
|
+
}
|
|
1237
|
+
catch (err) {
|
|
1238
|
+
flash(err.message || "Failed to archive ticket flow", "error");
|
|
1239
|
+
}
|
|
1240
|
+
finally {
|
|
1241
|
+
if (archiveBtn) {
|
|
1242
|
+
archiveBtn.textContent = "Archive Flow";
|
|
1243
|
+
}
|
|
1244
|
+
setButtonsDisabled(false);
|
|
1245
|
+
}
|
|
1246
|
+
}
|
|
1247
|
+
export function initTicketFlow() {
|
|
1248
|
+
const { card, bootstrapBtn, resumeBtn, refreshBtn, stopBtn, restartBtn, archiveBtn } = els();
|
|
1249
|
+
if (!card || card.dataset.ticketInitialized === "1")
|
|
1250
|
+
return;
|
|
1251
|
+
card.dataset.ticketInitialized = "1";
|
|
1252
|
+
if (bootstrapBtn)
|
|
1253
|
+
bootstrapBtn.addEventListener("click", bootstrapTicketFlow);
|
|
1254
|
+
if (resumeBtn)
|
|
1255
|
+
resumeBtn.addEventListener("click", resumeTicketFlow);
|
|
1256
|
+
if (stopBtn)
|
|
1257
|
+
stopBtn.addEventListener("click", stopTicketFlow);
|
|
1258
|
+
if (restartBtn)
|
|
1259
|
+
restartBtn.addEventListener("click", restartTicketFlow);
|
|
1260
|
+
if (archiveBtn)
|
|
1261
|
+
archiveBtn.addEventListener("click", archiveTicketFlow);
|
|
1262
|
+
if (refreshBtn)
|
|
1263
|
+
refreshBtn.addEventListener("click", loadTicketFlow);
|
|
1264
|
+
// Initialize reason click handler for modal
|
|
1265
|
+
initReasonModal();
|
|
1266
|
+
// Initialize live output panel
|
|
1267
|
+
initLiveOutputPanel();
|
|
1268
|
+
// Initialize dispatch panel toggle for medium screens
|
|
1269
|
+
initDispatchPanelToggle();
|
|
1270
|
+
const newThreadBtn = document.getElementById("ticket-chat-new-thread");
|
|
1271
|
+
if (newThreadBtn) {
|
|
1272
|
+
newThreadBtn.addEventListener("click", async () => {
|
|
1273
|
+
const { startNewTicketChatThread } = await import("./ticketChatActions.js");
|
|
1274
|
+
await startNewTicketChatThread();
|
|
1275
|
+
});
|
|
1276
|
+
}
|
|
1277
|
+
// Initialize the ticket editor modal
|
|
1278
|
+
initTicketEditor();
|
|
1279
|
+
loadTicketFlow();
|
|
1280
|
+
registerAutoRefresh("ticket-flow", {
|
|
1281
|
+
callback: loadTicketFlow,
|
|
1282
|
+
tabId: null,
|
|
1283
|
+
interval: CONSTANTS.UI?.AUTO_REFRESH_INTERVAL ||
|
|
1284
|
+
15000,
|
|
1285
|
+
refreshOnActivation: true,
|
|
1286
|
+
immediate: false,
|
|
1287
|
+
});
|
|
1288
|
+
subscribe("repo:health", (payload) => {
|
|
1289
|
+
const status = payload?.status || "";
|
|
1290
|
+
if (status === "ok" || status === "degraded") {
|
|
1291
|
+
void loadTicketFlow();
|
|
1292
|
+
}
|
|
1293
|
+
});
|
|
1294
|
+
// Refresh ticket list when tickets are updated (from editor)
|
|
1295
|
+
subscribe("tickets:updated", () => {
|
|
1296
|
+
void loadTicketFiles();
|
|
1297
|
+
});
|
|
1298
|
+
// Handle browser navigation (back/forward)
|
|
1299
|
+
window.addEventListener("popstate", () => {
|
|
1300
|
+
const params = getUrlParams();
|
|
1301
|
+
const ticketIndex = params.get("ticket");
|
|
1302
|
+
if (ticketIndex) {
|
|
1303
|
+
void openTicketByIndex(parseInt(ticketIndex, 10));
|
|
1304
|
+
}
|
|
1305
|
+
else {
|
|
1306
|
+
closeTicketEditor();
|
|
1307
|
+
}
|
|
1308
|
+
});
|
|
1309
|
+
// Check URL for ticket param on initial load
|
|
1310
|
+
const params = getUrlParams();
|
|
1311
|
+
const ticketIndex = params.get("ticket");
|
|
1312
|
+
if (ticketIndex) {
|
|
1313
|
+
void openTicketByIndex(parseInt(ticketIndex, 10));
|
|
1314
|
+
}
|
|
1315
|
+
}
|