codex-autorunner 1.1.0__py3-none-any.whl → 1.2.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/agents/opencode/client.py +113 -4
- codex_autorunner/agents/opencode/supervisor.py +4 -0
- codex_autorunner/agents/registry.py +17 -7
- codex_autorunner/bootstrap.py +219 -1
- codex_autorunner/core/__init__.py +17 -1
- codex_autorunner/core/about_car.py +114 -1
- codex_autorunner/core/app_server_threads.py +6 -0
- codex_autorunner/core/config.py +236 -1
- codex_autorunner/core/context_awareness.py +38 -0
- codex_autorunner/core/docs.py +0 -122
- codex_autorunner/core/filebox.py +265 -0
- codex_autorunner/core/flows/controller.py +71 -1
- codex_autorunner/core/flows/reconciler.py +4 -1
- codex_autorunner/core/flows/runtime.py +22 -0
- codex_autorunner/core/flows/store.py +61 -9
- codex_autorunner/core/flows/transition.py +23 -16
- codex_autorunner/core/flows/ux_helpers.py +18 -3
- codex_autorunner/core/flows/worker_process.py +32 -6
- codex_autorunner/core/hub.py +198 -41
- codex_autorunner/core/lifecycle_events.py +253 -0
- codex_autorunner/core/path_utils.py +2 -1
- codex_autorunner/core/pma_audit.py +224 -0
- codex_autorunner/core/pma_context.py +496 -0
- codex_autorunner/core/pma_dispatch_interceptor.py +284 -0
- codex_autorunner/core/pma_lifecycle.py +527 -0
- codex_autorunner/core/pma_queue.py +367 -0
- codex_autorunner/core/pma_safety.py +221 -0
- codex_autorunner/core/pma_state.py +115 -0
- codex_autorunner/core/ports/agent_backend.py +2 -5
- codex_autorunner/core/ports/run_event.py +1 -4
- codex_autorunner/core/prompt.py +0 -80
- codex_autorunner/core/prompts.py +56 -172
- codex_autorunner/core/redaction.py +0 -4
- codex_autorunner/core/review_context.py +11 -9
- codex_autorunner/core/runner_controller.py +35 -33
- codex_autorunner/core/runner_state.py +147 -0
- codex_autorunner/core/runtime.py +829 -0
- codex_autorunner/core/sqlite_utils.py +13 -4
- codex_autorunner/core/state.py +7 -10
- codex_autorunner/core/state_roots.py +5 -0
- codex_autorunner/core/templates/__init__.py +39 -0
- codex_autorunner/core/templates/git_mirror.py +234 -0
- codex_autorunner/core/templates/provenance.py +56 -0
- codex_autorunner/core/templates/scan_cache.py +120 -0
- codex_autorunner/core/ticket_linter_cli.py +17 -0
- codex_autorunner/core/ticket_manager_cli.py +154 -92
- codex_autorunner/core/time_utils.py +11 -0
- codex_autorunner/core/types.py +18 -0
- codex_autorunner/core/utils.py +34 -6
- codex_autorunner/flows/review/service.py +23 -25
- codex_autorunner/flows/ticket_flow/definition.py +43 -1
- codex_autorunner/integrations/agents/__init__.py +2 -0
- codex_autorunner/integrations/agents/backend_orchestrator.py +18 -0
- codex_autorunner/integrations/agents/codex_backend.py +19 -8
- codex_autorunner/integrations/agents/runner.py +3 -8
- codex_autorunner/integrations/agents/wiring.py +8 -0
- codex_autorunner/integrations/telegram/doctor.py +228 -6
- codex_autorunner/integrations/telegram/handlers/commands/execution.py +236 -74
- codex_autorunner/integrations/telegram/handlers/commands/files.py +314 -75
- codex_autorunner/integrations/telegram/handlers/commands/flows.py +346 -58
- codex_autorunner/integrations/telegram/handlers/commands/workspace.py +498 -37
- codex_autorunner/integrations/telegram/handlers/commands_runtime.py +202 -45
- codex_autorunner/integrations/telegram/handlers/commands_spec.py +18 -7
- codex_autorunner/integrations/telegram/handlers/messages.py +26 -1
- codex_autorunner/integrations/telegram/helpers.py +1 -3
- codex_autorunner/integrations/telegram/runtime.py +9 -4
- codex_autorunner/integrations/telegram/service.py +30 -0
- codex_autorunner/integrations/telegram/state.py +38 -0
- codex_autorunner/integrations/telegram/ticket_flow_bridge.py +10 -4
- codex_autorunner/integrations/telegram/transport.py +10 -3
- codex_autorunner/integrations/templates/__init__.py +27 -0
- codex_autorunner/integrations/templates/scan_agent.py +312 -0
- codex_autorunner/server.py +2 -2
- codex_autorunner/static/agentControls.js +21 -5
- codex_autorunner/static/app.js +115 -11
- codex_autorunner/static/chatUploads.js +137 -0
- codex_autorunner/static/docChatCore.js +185 -13
- codex_autorunner/static/fileChat.js +68 -40
- codex_autorunner/static/fileboxUi.js +159 -0
- codex_autorunner/static/hub.js +46 -81
- codex_autorunner/static/index.html +303 -24
- codex_autorunner/static/messages.js +82 -4
- codex_autorunner/static/notifications.js +255 -0
- codex_autorunner/static/pma.js +1167 -0
- codex_autorunner/static/settings.js +3 -0
- codex_autorunner/static/streamUtils.js +57 -0
- codex_autorunner/static/styles.css +9125 -6742
- codex_autorunner/static/templateReposSettings.js +225 -0
- codex_autorunner/static/ticketChatActions.js +165 -3
- codex_autorunner/static/ticketChatStream.js +17 -119
- codex_autorunner/static/ticketEditor.js +41 -13
- codex_autorunner/static/ticketTemplates.js +798 -0
- codex_autorunner/static/tickets.js +69 -19
- codex_autorunner/static/turnEvents.js +27 -0
- codex_autorunner/static/turnResume.js +33 -0
- codex_autorunner/static/utils.js +28 -0
- codex_autorunner/static/workspace.js +258 -44
- codex_autorunner/static/workspaceFileBrowser.js +6 -4
- codex_autorunner/surfaces/cli/cli.py +1465 -155
- codex_autorunner/surfaces/cli/pma_cli.py +817 -0
- codex_autorunner/surfaces/web/app.py +253 -49
- codex_autorunner/surfaces/web/routes/__init__.py +4 -0
- codex_autorunner/surfaces/web/routes/analytics.py +29 -22
- codex_autorunner/surfaces/web/routes/file_chat.py +317 -36
- codex_autorunner/surfaces/web/routes/filebox.py +227 -0
- codex_autorunner/surfaces/web/routes/flows.py +219 -29
- codex_autorunner/surfaces/web/routes/messages.py +70 -39
- codex_autorunner/surfaces/web/routes/pma.py +1652 -0
- codex_autorunner/surfaces/web/routes/repos.py +1 -1
- codex_autorunner/surfaces/web/routes/shared.py +0 -3
- codex_autorunner/surfaces/web/routes/templates.py +634 -0
- codex_autorunner/surfaces/web/runner_manager.py +2 -2
- codex_autorunner/surfaces/web/schemas.py +70 -18
- codex_autorunner/tickets/agent_pool.py +27 -0
- codex_autorunner/tickets/files.py +33 -16
- codex_autorunner/tickets/lint.py +50 -0
- codex_autorunner/tickets/models.py +3 -0
- codex_autorunner/tickets/outbox.py +41 -5
- codex_autorunner/tickets/runner.py +350 -69
- {codex_autorunner-1.1.0.dist-info → codex_autorunner-1.2.0.dist-info}/METADATA +15 -19
- {codex_autorunner-1.1.0.dist-info → codex_autorunner-1.2.0.dist-info}/RECORD +125 -94
- codex_autorunner/core/adapter_utils.py +0 -21
- codex_autorunner/core/engine.py +0 -3302
- {codex_autorunner-1.1.0.dist-info → codex_autorunner-1.2.0.dist-info}/WHEEL +0 -0
- {codex_autorunner-1.1.0.dist-info → codex_autorunner-1.2.0.dist-info}/entry_points.txt +0 -0
- {codex_autorunner-1.1.0.dist-info → codex_autorunner-1.2.0.dist-info}/licenses/LICENSE +0 -0
- {codex_autorunner-1.1.0.dist-info → codex_autorunner-1.2.0.dist-info}/top_level.txt +0 -0
|
@@ -0,0 +1,225 @@
|
|
|
1
|
+
// GENERATED FILE - do not edit directly. Source: static_src/
|
|
2
|
+
import { api, confirmModal, flash } from "./utils.js";
|
|
3
|
+
import { checkTemplatesEnabled } from "./ticketTemplates.js";
|
|
4
|
+
function els() {
|
|
5
|
+
return {
|
|
6
|
+
list: document.getElementById("template-repos-list"),
|
|
7
|
+
addBtn: document.getElementById("template-repos-add"),
|
|
8
|
+
form: document.getElementById("template-repo-form"),
|
|
9
|
+
idInput: document.getElementById("repo-id"),
|
|
10
|
+
urlInput: document.getElementById("repo-url"),
|
|
11
|
+
refInput: document.getElementById("repo-ref"),
|
|
12
|
+
trustedInput: document.getElementById("repo-trusted"),
|
|
13
|
+
saveBtn: document.getElementById("repo-save"),
|
|
14
|
+
cancelBtn: document.getElementById("repo-cancel"),
|
|
15
|
+
};
|
|
16
|
+
}
|
|
17
|
+
const state = {
|
|
18
|
+
mode: "create",
|
|
19
|
+
editId: null,
|
|
20
|
+
enabled: false,
|
|
21
|
+
repos: [],
|
|
22
|
+
busy: false,
|
|
23
|
+
};
|
|
24
|
+
function setBusy(busy) {
|
|
25
|
+
state.busy = busy;
|
|
26
|
+
const { saveBtn, addBtn } = els();
|
|
27
|
+
if (saveBtn)
|
|
28
|
+
saveBtn.disabled = busy;
|
|
29
|
+
if (addBtn)
|
|
30
|
+
addBtn.disabled = busy;
|
|
31
|
+
}
|
|
32
|
+
function showForm(show) {
|
|
33
|
+
const { form } = els();
|
|
34
|
+
if (!form)
|
|
35
|
+
return;
|
|
36
|
+
if (show)
|
|
37
|
+
form.classList.remove("hidden");
|
|
38
|
+
else
|
|
39
|
+
form.classList.add("hidden");
|
|
40
|
+
}
|
|
41
|
+
function resetForm() {
|
|
42
|
+
const { idInput, urlInput, refInput, trustedInput } = els();
|
|
43
|
+
if (idInput)
|
|
44
|
+
idInput.value = "";
|
|
45
|
+
if (urlInput)
|
|
46
|
+
urlInput.value = "";
|
|
47
|
+
if (refInput)
|
|
48
|
+
refInput.value = "main";
|
|
49
|
+
if (trustedInput)
|
|
50
|
+
trustedInput.checked = false;
|
|
51
|
+
state.mode = "create";
|
|
52
|
+
state.editId = null;
|
|
53
|
+
if (idInput)
|
|
54
|
+
idInput.disabled = false;
|
|
55
|
+
}
|
|
56
|
+
function openCreateForm() {
|
|
57
|
+
resetForm();
|
|
58
|
+
showForm(true);
|
|
59
|
+
const { idInput } = els();
|
|
60
|
+
idInput?.focus();
|
|
61
|
+
}
|
|
62
|
+
function openEditForm(repo) {
|
|
63
|
+
const { idInput, urlInput, refInput, trustedInput } = els();
|
|
64
|
+
state.mode = "edit";
|
|
65
|
+
state.editId = repo.id;
|
|
66
|
+
if (idInput) {
|
|
67
|
+
idInput.value = repo.id;
|
|
68
|
+
idInput.disabled = true;
|
|
69
|
+
}
|
|
70
|
+
if (urlInput)
|
|
71
|
+
urlInput.value = repo.url;
|
|
72
|
+
if (refInput)
|
|
73
|
+
refInput.value = repo.default_ref || "main";
|
|
74
|
+
if (trustedInput)
|
|
75
|
+
trustedInput.checked = Boolean(repo.trusted);
|
|
76
|
+
showForm(true);
|
|
77
|
+
urlInput?.focus();
|
|
78
|
+
}
|
|
79
|
+
function normalizeRequired(value, label) {
|
|
80
|
+
const v = (value || "").trim();
|
|
81
|
+
if (!v) {
|
|
82
|
+
flash(`${label} is required`, "error");
|
|
83
|
+
return null;
|
|
84
|
+
}
|
|
85
|
+
return v;
|
|
86
|
+
}
|
|
87
|
+
function renderRepos() {
|
|
88
|
+
const { list } = els();
|
|
89
|
+
if (!list)
|
|
90
|
+
return;
|
|
91
|
+
list.innerHTML = "";
|
|
92
|
+
if (!state.enabled) {
|
|
93
|
+
const hint = document.createElement("div");
|
|
94
|
+
hint.className = "muted small";
|
|
95
|
+
hint.textContent = "Templates are disabled (templates.enabled=false).";
|
|
96
|
+
list.appendChild(hint);
|
|
97
|
+
}
|
|
98
|
+
if (!state.repos.length) {
|
|
99
|
+
const empty = document.createElement("div");
|
|
100
|
+
empty.className = "muted small";
|
|
101
|
+
empty.textContent = "No template repos configured.";
|
|
102
|
+
list.appendChild(empty);
|
|
103
|
+
return;
|
|
104
|
+
}
|
|
105
|
+
for (const repo of state.repos) {
|
|
106
|
+
const row = document.createElement("div");
|
|
107
|
+
row.className = "template-repo-item";
|
|
108
|
+
row.innerHTML = `
|
|
109
|
+
<div class="template-repo-meta">
|
|
110
|
+
<span class="template-repo-id">${repo.id}</span>
|
|
111
|
+
<span class="template-repo-url">${repo.url}</span>
|
|
112
|
+
<span class="muted small">ref: ${repo.default_ref}${repo.trusted ? " · trusted" : ""}</span>
|
|
113
|
+
</div>
|
|
114
|
+
<div class="template-repo-actions">
|
|
115
|
+
<button class="ghost sm" data-action="edit" data-id="${repo.id}">Edit</button>
|
|
116
|
+
<button class="danger sm" data-action="delete" data-id="${repo.id}">Delete</button>
|
|
117
|
+
</div>
|
|
118
|
+
`;
|
|
119
|
+
list.appendChild(row);
|
|
120
|
+
}
|
|
121
|
+
list.querySelectorAll("button[data-action]").forEach((btn) => {
|
|
122
|
+
btn.addEventListener("click", async () => {
|
|
123
|
+
const action = btn.dataset.action;
|
|
124
|
+
const id = btn.dataset.id;
|
|
125
|
+
if (!action || !id)
|
|
126
|
+
return;
|
|
127
|
+
const repo = state.repos.find((r) => r.id === id);
|
|
128
|
+
if (!repo)
|
|
129
|
+
return;
|
|
130
|
+
if (action === "edit") {
|
|
131
|
+
openEditForm(repo);
|
|
132
|
+
return;
|
|
133
|
+
}
|
|
134
|
+
if (action === "delete") {
|
|
135
|
+
await deleteRepo(id);
|
|
136
|
+
}
|
|
137
|
+
});
|
|
138
|
+
});
|
|
139
|
+
}
|
|
140
|
+
export async function loadTemplateRepos() {
|
|
141
|
+
const { list } = els();
|
|
142
|
+
if (!list)
|
|
143
|
+
return;
|
|
144
|
+
try {
|
|
145
|
+
const data = (await api("/api/templates/repos"));
|
|
146
|
+
state.enabled = Boolean(data.enabled);
|
|
147
|
+
state.repos = Array.isArray(data.repos) ? data.repos : [];
|
|
148
|
+
renderRepos();
|
|
149
|
+
}
|
|
150
|
+
catch (err) {
|
|
151
|
+
state.enabled = false;
|
|
152
|
+
state.repos = [];
|
|
153
|
+
renderRepos();
|
|
154
|
+
flash(err.message || "Failed to load template repos", "error");
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
async function saveRepo() {
|
|
158
|
+
const { idInput, urlInput, refInput, trustedInput } = els();
|
|
159
|
+
if (!idInput || !urlInput || !refInput || !trustedInput)
|
|
160
|
+
return;
|
|
161
|
+
const id = normalizeRequired(idInput.value, "ID");
|
|
162
|
+
const url = normalizeRequired(urlInput.value, "Git URL");
|
|
163
|
+
const ref = normalizeRequired(refInput.value, "Default ref");
|
|
164
|
+
if (!id || !url || !ref)
|
|
165
|
+
return;
|
|
166
|
+
setBusy(true);
|
|
167
|
+
try {
|
|
168
|
+
if (state.mode === "create") {
|
|
169
|
+
await api("/api/templates/repos", {
|
|
170
|
+
method: "POST",
|
|
171
|
+
body: { id, url, trusted: Boolean(trustedInput.checked), default_ref: ref },
|
|
172
|
+
});
|
|
173
|
+
flash("Template repo added", "success");
|
|
174
|
+
}
|
|
175
|
+
else if (state.mode === "edit" && state.editId) {
|
|
176
|
+
await api(`/api/templates/repos/${encodeURIComponent(state.editId)}`, {
|
|
177
|
+
method: "PUT",
|
|
178
|
+
body: { url, trusted: Boolean(trustedInput.checked), default_ref: ref },
|
|
179
|
+
});
|
|
180
|
+
flash("Template repo updated", "success");
|
|
181
|
+
}
|
|
182
|
+
await loadTemplateRepos();
|
|
183
|
+
await checkTemplatesEnabled();
|
|
184
|
+
showForm(false);
|
|
185
|
+
resetForm();
|
|
186
|
+
}
|
|
187
|
+
catch (err) {
|
|
188
|
+
flash(err.message || "Failed to save template repo", "error");
|
|
189
|
+
}
|
|
190
|
+
finally {
|
|
191
|
+
setBusy(false);
|
|
192
|
+
}
|
|
193
|
+
}
|
|
194
|
+
async function deleteRepo(id) {
|
|
195
|
+
const confirmed = await confirmModal(`Delete template repo "${id}"?`, {
|
|
196
|
+
confirmText: "Delete",
|
|
197
|
+
danger: true,
|
|
198
|
+
});
|
|
199
|
+
if (!confirmed)
|
|
200
|
+
return;
|
|
201
|
+
setBusy(true);
|
|
202
|
+
try {
|
|
203
|
+
await api(`/api/templates/repos/${encodeURIComponent(id)}`, { method: "DELETE" });
|
|
204
|
+
flash("Template repo deleted", "success");
|
|
205
|
+
await loadTemplateRepos();
|
|
206
|
+
await checkTemplatesEnabled();
|
|
207
|
+
}
|
|
208
|
+
catch (err) {
|
|
209
|
+
flash(err.message || "Failed to delete template repo", "error");
|
|
210
|
+
}
|
|
211
|
+
finally {
|
|
212
|
+
setBusy(false);
|
|
213
|
+
}
|
|
214
|
+
}
|
|
215
|
+
export function initTemplateReposSettings() {
|
|
216
|
+
const { list, addBtn, saveBtn, cancelBtn } = els();
|
|
217
|
+
if (!list || !addBtn || !saveBtn || !cancelBtn)
|
|
218
|
+
return;
|
|
219
|
+
addBtn.addEventListener("click", () => openCreateForm());
|
|
220
|
+
saveBtn.addEventListener("click", () => void saveRepo());
|
|
221
|
+
cancelBtn.addEventListener("click", () => {
|
|
222
|
+
showForm(false);
|
|
223
|
+
resetForm();
|
|
224
|
+
});
|
|
225
|
+
}
|
|
@@ -2,16 +2,20 @@
|
|
|
2
2
|
/**
|
|
3
3
|
* Ticket Chat Actions - handles sending messages, applying/discarding patches
|
|
4
4
|
*/
|
|
5
|
-
import { api, flash, splitMarkdownFrontmatter } from "./utils.js";
|
|
5
|
+
import { api, confirmModal, flash, splitMarkdownFrontmatter } from "./utils.js";
|
|
6
6
|
import { performTicketChatRequest } from "./ticketChatStream.js";
|
|
7
|
-
import { renderTicketMessages } from "./ticketChatEvents.js";
|
|
7
|
+
import { renderTicketMessages, renderTicketEvents } from "./ticketChatEvents.js";
|
|
8
8
|
import { publish } from "./bus.js";
|
|
9
9
|
import { createDocChat } from "./docChatCore.js";
|
|
10
10
|
import { saveTicketChatHistory } from "./ticketChatStorage.js";
|
|
11
11
|
import { renderDiff } from "./diffRenderer.js";
|
|
12
|
+
import { newClientTurnId, streamTurnEvents } from "./fileChat.js";
|
|
13
|
+
import { loadPendingTurn, savePendingTurn, clearPendingTurn } from "./turnResume.js";
|
|
14
|
+
import { resumeFileChatTurn } from "./turnEvents.js";
|
|
12
15
|
// Limits for events display
|
|
13
16
|
export const TICKET_CHAT_EVENT_LIMIT = 8;
|
|
14
17
|
export const TICKET_CHAT_EVENT_MAX = 50;
|
|
18
|
+
const pendingKeyForTicket = (index) => index != null ? `car.ticketChat.pending.${index}` : "car.ticketChat.pending";
|
|
15
19
|
export const ticketChat = createDocChat({
|
|
16
20
|
idPrefix: "ticket-chat",
|
|
17
21
|
storage: { keyPrefix: "car-ticket-chat-", maxMessages: 50, version: 1 },
|
|
@@ -39,7 +43,9 @@ export const ticketChat = createDocChat({
|
|
|
39
43
|
export const ticketChatState = Object.assign(ticketChat.state, {
|
|
40
44
|
ticketIndex: null,
|
|
41
45
|
draft: null,
|
|
46
|
+
contextUsagePercent: null,
|
|
42
47
|
});
|
|
48
|
+
let currentTurnEventsController = null;
|
|
43
49
|
export function getTicketChatElements() {
|
|
44
50
|
const base = ticketChat.elements;
|
|
45
51
|
return {
|
|
@@ -75,13 +81,14 @@ export function resetTicketChatState() {
|
|
|
75
81
|
ticketChatState.streamText = "";
|
|
76
82
|
ticketChatState.statusText = "";
|
|
77
83
|
ticketChatState.controller = null;
|
|
84
|
+
ticketChatState.contextUsagePercent = null;
|
|
78
85
|
// Note: events are cleared at the start of each new request, not here
|
|
79
86
|
// Messages persist across requests within the same ticket
|
|
80
87
|
}
|
|
81
88
|
export async function startNewTicketChatThread() {
|
|
82
89
|
if (ticketChatState.ticketIndex == null)
|
|
83
90
|
return;
|
|
84
|
-
const confirmed =
|
|
91
|
+
const confirmed = await confirmModal("Start a new conversation thread for this ticket?");
|
|
85
92
|
if (!confirmed)
|
|
86
93
|
return;
|
|
87
94
|
try {
|
|
@@ -111,6 +118,97 @@ export async function startNewTicketChatThread() {
|
|
|
111
118
|
export function clearTicketEvents() {
|
|
112
119
|
ticketChat.clearEvents();
|
|
113
120
|
}
|
|
121
|
+
function clearTurnEventsStream() {
|
|
122
|
+
if (currentTurnEventsController) {
|
|
123
|
+
try {
|
|
124
|
+
currentTurnEventsController.abort();
|
|
125
|
+
}
|
|
126
|
+
catch {
|
|
127
|
+
// ignore
|
|
128
|
+
}
|
|
129
|
+
currentTurnEventsController = null;
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
function clearPendingTurnState(pendingKey) {
|
|
133
|
+
clearTurnEventsStream();
|
|
134
|
+
clearPendingTurn(pendingKey);
|
|
135
|
+
}
|
|
136
|
+
function handleTicketTurnMeta(update) {
|
|
137
|
+
const threadId = typeof update.thread_id === "string" ? update.thread_id : "";
|
|
138
|
+
const turnId = typeof update.turn_id === "string" ? update.turn_id : "";
|
|
139
|
+
const agent = typeof update.agent === "string" ? update.agent : "codex";
|
|
140
|
+
if (!threadId || !turnId)
|
|
141
|
+
return;
|
|
142
|
+
clearTurnEventsStream();
|
|
143
|
+
currentTurnEventsController = streamTurnEvents({ agent, threadId, turnId }, {
|
|
144
|
+
onEvent: (event) => {
|
|
145
|
+
ticketChat.applyAppEvent(event);
|
|
146
|
+
ticketChat.renderEvents();
|
|
147
|
+
ticketChat.render();
|
|
148
|
+
},
|
|
149
|
+
});
|
|
150
|
+
}
|
|
151
|
+
export function applyTicketChatResult(payload) {
|
|
152
|
+
if (!payload || typeof payload !== "object")
|
|
153
|
+
return;
|
|
154
|
+
const result = payload;
|
|
155
|
+
handleTicketTurnMeta(result);
|
|
156
|
+
if (result.status === "interrupted") {
|
|
157
|
+
ticketChatState.status = "interrupted";
|
|
158
|
+
ticketChatState.error = "";
|
|
159
|
+
addAssistantMessage("Request interrupted", true);
|
|
160
|
+
renderTicketChat();
|
|
161
|
+
renderTicketMessages();
|
|
162
|
+
return;
|
|
163
|
+
}
|
|
164
|
+
if (result.status === "error" || result.error) {
|
|
165
|
+
ticketChatState.status = "error";
|
|
166
|
+
ticketChatState.error =
|
|
167
|
+
result.detail || result.error || "Chat failed";
|
|
168
|
+
addAssistantMessage(`Error: ${ticketChatState.error}`, true);
|
|
169
|
+
renderTicketChat();
|
|
170
|
+
renderTicketMessages();
|
|
171
|
+
return;
|
|
172
|
+
}
|
|
173
|
+
// Success
|
|
174
|
+
ticketChatState.status = "done";
|
|
175
|
+
if (result.message) {
|
|
176
|
+
ticketChatState.streamText = result.message;
|
|
177
|
+
}
|
|
178
|
+
if (result.agent_message || result.agentMessage) {
|
|
179
|
+
ticketChatState.statusText =
|
|
180
|
+
result.agent_message || result.agentMessage || "";
|
|
181
|
+
}
|
|
182
|
+
// Check for draft/patch in response
|
|
183
|
+
const hasDraft = result.has_draft ?? result.hasDraft;
|
|
184
|
+
if (hasDraft === false) {
|
|
185
|
+
ticketChatState.draft = null;
|
|
186
|
+
}
|
|
187
|
+
else if (hasDraft === true || result.draft || result.patch || result.content) {
|
|
188
|
+
ticketChatState.draft = {
|
|
189
|
+
content: result.content || "",
|
|
190
|
+
patch: result.patch || "",
|
|
191
|
+
agentMessage: result.agent_message || result.agentMessage || "",
|
|
192
|
+
createdAt: result.created_at || result.createdAt || "",
|
|
193
|
+
baseHash: result.base_hash || result.baseHash || "",
|
|
194
|
+
};
|
|
195
|
+
}
|
|
196
|
+
// Add assistant message from response
|
|
197
|
+
const responseText = ticketChatState.streamText ||
|
|
198
|
+
ticketChatState.statusText ||
|
|
199
|
+
(ticketChatState.draft ? "Changes ready to apply" : "Done");
|
|
200
|
+
if (responseText && ticketChatState.messages.length > 0) {
|
|
201
|
+
// Only add if we have messages (i.e., a user message was sent)
|
|
202
|
+
const lastMessage = ticketChatState.messages[ticketChatState.messages.length - 1];
|
|
203
|
+
// Avoid duplicate assistant messages
|
|
204
|
+
if (lastMessage.role === "user") {
|
|
205
|
+
addAssistantMessage(responseText, true);
|
|
206
|
+
}
|
|
207
|
+
}
|
|
208
|
+
renderTicketChat();
|
|
209
|
+
renderTicketMessages();
|
|
210
|
+
renderTicketEvents();
|
|
211
|
+
}
|
|
114
212
|
/**
|
|
115
213
|
* Add a user message to the chat history.
|
|
116
214
|
*/
|
|
@@ -129,6 +227,7 @@ export function setTicketIndex(index) {
|
|
|
129
227
|
ticketChatState.ticketIndex = index;
|
|
130
228
|
ticketChatState.draft = null;
|
|
131
229
|
resetTicketChatState();
|
|
230
|
+
clearTurnEventsStream();
|
|
132
231
|
// Clear chat history when switching tickets
|
|
133
232
|
if (changed) {
|
|
134
233
|
ticketChat.setTarget(index != null ? String(index) : null);
|
|
@@ -183,7 +282,16 @@ export async function sendTicketChat() {
|
|
|
183
282
|
resetTicketChatState();
|
|
184
283
|
ticketChatState.status = "running";
|
|
185
284
|
ticketChatState.statusText = "queued";
|
|
285
|
+
clearTurnEventsStream();
|
|
186
286
|
ticketChatState.controller = new AbortController();
|
|
287
|
+
const pendingKey = pendingKeyForTicket(ticketChatState.ticketIndex);
|
|
288
|
+
const clientTurnId = newClientTurnId("ticket");
|
|
289
|
+
savePendingTurn(pendingKey, {
|
|
290
|
+
clientTurnId,
|
|
291
|
+
message,
|
|
292
|
+
startedAtMs: Date.now(),
|
|
293
|
+
target: ticketChatState.ticketIndex != null ? `ticket:${ticketChatState.ticketIndex}` : "ticket",
|
|
294
|
+
});
|
|
187
295
|
renderTicketChat();
|
|
188
296
|
if (els.input) {
|
|
189
297
|
els.input.value = "";
|
|
@@ -196,12 +304,14 @@ export async function sendTicketChat() {
|
|
|
196
304
|
agent,
|
|
197
305
|
model,
|
|
198
306
|
reasoning,
|
|
307
|
+
clientTurnId,
|
|
199
308
|
});
|
|
200
309
|
// Try to load any pending draft
|
|
201
310
|
await loadTicketPending(ticketChatState.ticketIndex, true);
|
|
202
311
|
if (ticketChatState.status === "running") {
|
|
203
312
|
ticketChatState.status = "done";
|
|
204
313
|
}
|
|
314
|
+
clearPendingTurnState(pendingKey);
|
|
205
315
|
}
|
|
206
316
|
catch (err) {
|
|
207
317
|
const error = err;
|
|
@@ -213,6 +323,7 @@ export async function sendTicketChat() {
|
|
|
213
323
|
ticketChatState.status = "error";
|
|
214
324
|
ticketChatState.error = error.message || "Ticket chat failed";
|
|
215
325
|
}
|
|
326
|
+
clearPendingTurnState(pendingKey);
|
|
216
327
|
}
|
|
217
328
|
finally {
|
|
218
329
|
ticketChatState.controller = null;
|
|
@@ -226,6 +337,7 @@ export async function cancelTicketChat() {
|
|
|
226
337
|
if (ticketChatState.controller) {
|
|
227
338
|
ticketChatState.controller.abort();
|
|
228
339
|
}
|
|
340
|
+
clearTurnEventsStream();
|
|
229
341
|
// Send interrupt to server
|
|
230
342
|
if (ticketChatState.ticketIndex != null) {
|
|
231
343
|
try {
|
|
@@ -242,6 +354,56 @@ export async function cancelTicketChat() {
|
|
|
242
354
|
ticketChatState.statusText = "";
|
|
243
355
|
ticketChatState.controller = null;
|
|
244
356
|
renderTicketChat();
|
|
357
|
+
if (ticketChatState.ticketIndex != null) {
|
|
358
|
+
clearPendingTurnState(pendingKeyForTicket(ticketChatState.ticketIndex));
|
|
359
|
+
}
|
|
360
|
+
}
|
|
361
|
+
export async function resumeTicketPendingTurn(index) {
|
|
362
|
+
if (index == null)
|
|
363
|
+
return;
|
|
364
|
+
const pendingKey = pendingKeyForTicket(index);
|
|
365
|
+
const pending = loadPendingTurn(pendingKey);
|
|
366
|
+
if (!pending || pending.target !== `ticket:${index}`)
|
|
367
|
+
return;
|
|
368
|
+
const chatState = ticketChatState;
|
|
369
|
+
chatState.status = "running";
|
|
370
|
+
chatState.statusText = "Recovering previous turn…";
|
|
371
|
+
ticketChat.render();
|
|
372
|
+
ticketChat.renderMessages();
|
|
373
|
+
try {
|
|
374
|
+
const outcome = await resumeFileChatTurn(pending.clientTurnId, {
|
|
375
|
+
onEvent: (event) => {
|
|
376
|
+
ticketChat.applyAppEvent(event);
|
|
377
|
+
ticketChat.renderEvents();
|
|
378
|
+
ticketChat.render();
|
|
379
|
+
},
|
|
380
|
+
onResult: (result) => {
|
|
381
|
+
applyTicketChatResult(result);
|
|
382
|
+
const status = result.status;
|
|
383
|
+
if (status === "ok" || status === "error" || status === "interrupted") {
|
|
384
|
+
clearPendingTurnState(pendingKey);
|
|
385
|
+
}
|
|
386
|
+
},
|
|
387
|
+
onError: (msg) => {
|
|
388
|
+
chatState.statusText = msg;
|
|
389
|
+
renderTicketChat();
|
|
390
|
+
},
|
|
391
|
+
});
|
|
392
|
+
currentTurnEventsController = outcome.controller;
|
|
393
|
+
if (outcome.lastResult && outcome.lastResult.status) {
|
|
394
|
+
applyTicketChatResult(outcome.lastResult);
|
|
395
|
+
clearPendingTurnState(pendingKey);
|
|
396
|
+
return;
|
|
397
|
+
}
|
|
398
|
+
if (!outcome.controller) {
|
|
399
|
+
window.setTimeout(() => void resumeTicketPendingTurn(index), 1000);
|
|
400
|
+
}
|
|
401
|
+
}
|
|
402
|
+
catch (err) {
|
|
403
|
+
const msg = err.message || "Failed to resume turn";
|
|
404
|
+
chatState.statusText = msg;
|
|
405
|
+
renderTicketChat();
|
|
406
|
+
}
|
|
245
407
|
}
|
|
246
408
|
export async function applyTicketPatch() {
|
|
247
409
|
if (ticketChatState.ticketIndex == null) {
|
|
@@ -3,21 +3,14 @@
|
|
|
3
3
|
* Ticket Chat Stream - handles SSE streaming for ticket chat
|
|
4
4
|
*/
|
|
5
5
|
import { resolvePath, getAuthToken } from "./utils.js";
|
|
6
|
-
import { ticketChatState, renderTicketChat, clearTicketEvents, addUserMessage, addAssistantMessage, } from "./ticketChatActions.js";
|
|
6
|
+
import { ticketChatState, renderTicketChat, clearTicketEvents, addUserMessage, addAssistantMessage, applyTicketChatResult, } from "./ticketChatActions.js";
|
|
7
7
|
import { applyTicketEvent, renderTicketEvents, renderTicketMessages } from "./ticketChatEvents.js";
|
|
8
|
-
|
|
9
|
-
function parseMaybeJson(data) {
|
|
10
|
-
try {
|
|
11
|
-
return JSON.parse(data);
|
|
12
|
-
}
|
|
13
|
-
catch {
|
|
14
|
-
return data;
|
|
15
|
-
}
|
|
16
|
-
}
|
|
8
|
+
import { extractContextRemainingPercent, readEventStream, parseMaybeJson } from "./streamUtils.js";
|
|
17
9
|
export async function performTicketChatRequest(ticketIndex, message, signal, options = {}) {
|
|
18
10
|
// Clear events from previous request and add user message to history
|
|
19
11
|
clearTicketEvents();
|
|
20
12
|
addUserMessage(message);
|
|
13
|
+
ticketChatState.contextUsagePercent = null;
|
|
21
14
|
// Render both chat (for container visibility) and messages
|
|
22
15
|
renderTicketChat();
|
|
23
16
|
renderTicketMessages();
|
|
@@ -39,6 +32,8 @@ export async function performTicketChatRequest(ticketIndex, message, signal, opt
|
|
|
39
32
|
payload.model = options.model;
|
|
40
33
|
if (options.reasoning)
|
|
41
34
|
payload.reasoning = options.reasoning;
|
|
35
|
+
if (options.clientTurnId)
|
|
36
|
+
payload.client_turn_id = options.clientTurnId;
|
|
42
37
|
const res = await fetch(endpoint, {
|
|
43
38
|
method: "POST",
|
|
44
39
|
headers,
|
|
@@ -59,7 +54,7 @@ export async function performTicketChatRequest(ticketIndex, message, signal, opt
|
|
|
59
54
|
}
|
|
60
55
|
const contentType = res.headers.get("content-type") || "";
|
|
61
56
|
if (contentType.includes("text/event-stream")) {
|
|
62
|
-
await
|
|
57
|
+
await readEventStream(res, (event, data) => handleTicketStreamEvent(event, data));
|
|
63
58
|
}
|
|
64
59
|
else {
|
|
65
60
|
// Non-streaming response
|
|
@@ -69,54 +64,6 @@ export async function performTicketChatRequest(ticketIndex, message, signal, opt
|
|
|
69
64
|
applyTicketChatResult(responsePayload);
|
|
70
65
|
}
|
|
71
66
|
}
|
|
72
|
-
async function readTicketChatStream(res) {
|
|
73
|
-
if (!res.body)
|
|
74
|
-
throw new Error("Streaming not supported in this browser");
|
|
75
|
-
const reader = res.body.getReader();
|
|
76
|
-
let buffer = "";
|
|
77
|
-
let escapedNewlines = false;
|
|
78
|
-
for (;;) {
|
|
79
|
-
const { value, done } = await reader.read();
|
|
80
|
-
if (done)
|
|
81
|
-
break;
|
|
82
|
-
const decoded = decoder.decode(value, { stream: true });
|
|
83
|
-
// Handle escaped newlines
|
|
84
|
-
if (!escapedNewlines) {
|
|
85
|
-
const combined = buffer + decoded;
|
|
86
|
-
if (!combined.includes("\n") && combined.includes("\\n")) {
|
|
87
|
-
escapedNewlines = true;
|
|
88
|
-
buffer = buffer.replace(/\\n(?=event:|data:|\\n)/g, "\n");
|
|
89
|
-
}
|
|
90
|
-
}
|
|
91
|
-
buffer += escapedNewlines
|
|
92
|
-
? decoded.replace(/\\n(?=event:|data:|\\n)/g, "\n")
|
|
93
|
-
: decoded;
|
|
94
|
-
// Split on double newlines (SSE message delimiter)
|
|
95
|
-
const chunks = buffer.split("\n\n");
|
|
96
|
-
buffer = chunks.pop() || "";
|
|
97
|
-
for (const chunk of chunks) {
|
|
98
|
-
if (!chunk.trim())
|
|
99
|
-
continue;
|
|
100
|
-
let event = "message";
|
|
101
|
-
const dataLines = [];
|
|
102
|
-
chunk.split("\n").forEach((line) => {
|
|
103
|
-
if (line.startsWith("event:")) {
|
|
104
|
-
event = line.slice(6).trim();
|
|
105
|
-
}
|
|
106
|
-
else if (line.startsWith("data:")) {
|
|
107
|
-
dataLines.push(line.slice(5).trimStart());
|
|
108
|
-
}
|
|
109
|
-
else if (line.trim()) {
|
|
110
|
-
dataLines.push(line);
|
|
111
|
-
}
|
|
112
|
-
});
|
|
113
|
-
if (dataLines.length === 0)
|
|
114
|
-
continue;
|
|
115
|
-
const data = dataLines.join("\n");
|
|
116
|
-
handleTicketStreamEvent(event, data);
|
|
117
|
-
}
|
|
118
|
-
}
|
|
119
|
-
}
|
|
120
67
|
function handleTicketStreamEvent(event, rawData) {
|
|
121
68
|
const parsed = parseMaybeJson(rawData);
|
|
122
69
|
switch (event) {
|
|
@@ -154,6 +101,17 @@ function handleTicketStreamEvent(event, rawData) {
|
|
|
154
101
|
renderTicketEvents();
|
|
155
102
|
break;
|
|
156
103
|
}
|
|
104
|
+
case "token_usage": {
|
|
105
|
+
// Token usage events - context window usage
|
|
106
|
+
if (typeof parsed === "object" && parsed !== null) {
|
|
107
|
+
const percentRemaining = extractContextRemainingPercent(parsed);
|
|
108
|
+
if (percentRemaining !== null) {
|
|
109
|
+
ticketChatState.contextUsagePercent = percentRemaining;
|
|
110
|
+
renderTicketChat();
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
break;
|
|
114
|
+
}
|
|
157
115
|
case "error": {
|
|
158
116
|
const message = typeof parsed === "object" && parsed !== null
|
|
159
117
|
? parsed.detail ||
|
|
@@ -202,63 +160,3 @@ function handleTicketStreamEvent(event, rawData) {
|
|
|
202
160
|
break;
|
|
203
161
|
}
|
|
204
162
|
}
|
|
205
|
-
function applyTicketChatResult(payload) {
|
|
206
|
-
if (!payload || typeof payload !== "object")
|
|
207
|
-
return;
|
|
208
|
-
const result = payload;
|
|
209
|
-
if (result.status === "interrupted") {
|
|
210
|
-
ticketChatState.status = "interrupted";
|
|
211
|
-
ticketChatState.error = "";
|
|
212
|
-
addAssistantMessage("Request interrupted", true);
|
|
213
|
-
renderTicketChat();
|
|
214
|
-
renderTicketMessages();
|
|
215
|
-
return;
|
|
216
|
-
}
|
|
217
|
-
if (result.status === "error" || result.error) {
|
|
218
|
-
ticketChatState.status = "error";
|
|
219
|
-
ticketChatState.error =
|
|
220
|
-
result.detail || result.error || "Chat failed";
|
|
221
|
-
addAssistantMessage(`Error: ${ticketChatState.error}`, true);
|
|
222
|
-
renderTicketChat();
|
|
223
|
-
renderTicketMessages();
|
|
224
|
-
return;
|
|
225
|
-
}
|
|
226
|
-
// Success
|
|
227
|
-
ticketChatState.status = "done";
|
|
228
|
-
if (result.message) {
|
|
229
|
-
ticketChatState.streamText = result.message;
|
|
230
|
-
}
|
|
231
|
-
if (result.agent_message || result.agentMessage) {
|
|
232
|
-
ticketChatState.statusText =
|
|
233
|
-
result.agent_message || result.agentMessage || "";
|
|
234
|
-
}
|
|
235
|
-
// Check for draft/patch in response
|
|
236
|
-
const hasDraft = result.has_draft ?? result.hasDraft;
|
|
237
|
-
if (hasDraft === false) {
|
|
238
|
-
ticketChatState.draft = null;
|
|
239
|
-
}
|
|
240
|
-
else if (hasDraft === true || result.draft || result.patch || result.content) {
|
|
241
|
-
ticketChatState.draft = {
|
|
242
|
-
content: result.content || "",
|
|
243
|
-
patch: result.patch || "",
|
|
244
|
-
agentMessage: result.agent_message || result.agentMessage || "",
|
|
245
|
-
createdAt: result.created_at || result.createdAt || "",
|
|
246
|
-
baseHash: result.base_hash || result.baseHash || "",
|
|
247
|
-
};
|
|
248
|
-
}
|
|
249
|
-
// Add assistant message from response
|
|
250
|
-
const responseText = ticketChatState.streamText ||
|
|
251
|
-
ticketChatState.statusText ||
|
|
252
|
-
(ticketChatState.draft ? "Changes ready to apply" : "Done");
|
|
253
|
-
if (responseText && ticketChatState.messages.length > 0) {
|
|
254
|
-
// Only add if we have messages (i.e., a user message was sent)
|
|
255
|
-
const lastMessage = ticketChatState.messages[ticketChatState.messages.length - 1];
|
|
256
|
-
// Avoid duplicate assistant messages
|
|
257
|
-
if (lastMessage.role === "user") {
|
|
258
|
-
addAssistantMessage(responseText, true);
|
|
259
|
-
}
|
|
260
|
-
}
|
|
261
|
-
renderTicketChat();
|
|
262
|
-
renderTicketMessages();
|
|
263
|
-
renderTicketEvents();
|
|
264
|
-
}
|