codex-autorunner 1.1.0__py3-none-any.whl → 1.2.1__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 +124 -11
- codex_autorunner/core/app_server_threads.py +6 -0
- codex_autorunner/core/config.py +238 -3
- codex_autorunner/core/context_awareness.py +39 -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 +683 -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/adapter.py +1 -1
- codex_autorunner/integrations/telegram/config.py +1 -1
- 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 +34 -3
- 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/archive.js +274 -81
- codex_autorunner/static/archiveApi.js +21 -0
- codex_autorunner/static/chatUploads.js +137 -0
- codex_autorunner/static/constants.js +1 -1
- 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 +288 -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 +9141 -6742
- codex_autorunner/static/templateReposSettings.js +225 -0
- codex_autorunner/static/terminalManager.js +22 -3
- 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/archive.py +197 -0
- codex_autorunner/surfaces/web/routes/file_chat.py +297 -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 +81 -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.1.dist-info}/METADATA +15 -19
- {codex_autorunner-1.1.0.dist-info → codex_autorunner-1.2.1.dist-info}/RECORD +132 -101
- 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.1.dist-info}/WHEEL +0 -0
- {codex_autorunner-1.1.0.dist-info → codex_autorunner-1.2.1.dist-info}/entry_points.txt +0 -0
- {codex_autorunner-1.1.0.dist-info → codex_autorunner-1.2.1.dist-info}/licenses/LICENSE +0 -0
- {codex_autorunner-1.1.0.dist-info → codex_autorunner-1.2.1.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
|
+
}
|
|
@@ -49,6 +49,17 @@ const CAR_CONTEXT_HINT = wrapInjectedContext(CONSTANTS.PROMPTS.CAR_CONTEXT_HINT)
|
|
|
49
49
|
const VOICE_TRANSCRIPT_DISCLAIMER_TEXT = CONSTANTS.PROMPTS?.VOICE_TRANSCRIPT_DISCLAIMER ||
|
|
50
50
|
"Note: transcribed from user voice. If confusing or possibly inaccurate and you cannot infer the intention please clarify before proceeding.";
|
|
51
51
|
const INJECTED_CONTEXT_TAG_RE = /<injected context>/i;
|
|
52
|
+
const CAR_CONTEXT_COMMAND_RE = [
|
|
53
|
+
/^\/\S/,
|
|
54
|
+
/^\.\/\S/,
|
|
55
|
+
/^git(\s|$)/,
|
|
56
|
+
/^cd(\s|$)/,
|
|
57
|
+
/^ls(\s|$)/,
|
|
58
|
+
/^make(\s|$)/,
|
|
59
|
+
/^pnpm(\s|$)/,
|
|
60
|
+
/^npm(\s|$)/,
|
|
61
|
+
/^python3?(\s|$)/,
|
|
62
|
+
];
|
|
52
63
|
function wrapInjectedContext(text) {
|
|
53
64
|
return `<injected context>\n${text}\n</injected context>`;
|
|
54
65
|
}
|
|
@@ -57,6 +68,13 @@ function wrapInjectedContextIfNeeded(text) {
|
|
|
57
68
|
return text;
|
|
58
69
|
return INJECTED_CONTEXT_TAG_RE.test(text) ? text : wrapInjectedContext(text);
|
|
59
70
|
}
|
|
71
|
+
function looksLikeCommand(text) {
|
|
72
|
+
const trimmed = text.trim();
|
|
73
|
+
if (!trimmed)
|
|
74
|
+
return false;
|
|
75
|
+
const lowered = trimmed.toLowerCase();
|
|
76
|
+
return CAR_CONTEXT_COMMAND_RE.some((pattern) => pattern.test(lowered));
|
|
77
|
+
}
|
|
60
78
|
const LEGACY_SESSION_STORAGE_KEY = "codex_terminal_session_id";
|
|
61
79
|
const SESSION_STORAGE_PREFIX = "codex_terminal_session_id:";
|
|
62
80
|
const SESSION_STORAGE_TS_PREFIX = "codex_terminal_session_ts:";
|
|
@@ -620,12 +638,13 @@ export class TerminalManager {
|
|
|
620
638
|
return null;
|
|
621
639
|
if (manager._hasTextInputHookFired(CAR_CONTEXT_HOOK_ID))
|
|
622
640
|
return null;
|
|
623
|
-
|
|
624
|
-
const hit = CONSTANTS.KEYWORDS.CAR_CONTEXT.some((kw) => lowered.includes(kw));
|
|
625
|
-
if (!hit)
|
|
641
|
+
if (looksLikeCommand(text))
|
|
626
642
|
return null;
|
|
643
|
+
const lowered = text.toLowerCase();
|
|
627
644
|
if (lowered.includes("about_car.md"))
|
|
628
645
|
return null;
|
|
646
|
+
if (lowered.includes(".codex-autorunner"))
|
|
647
|
+
return null;
|
|
629
648
|
if (text.includes(CONSTANTS.PROMPTS.CAR_CONTEXT_HINT) ||
|
|
630
649
|
text.includes(CAR_CONTEXT_HINT)) {
|
|
631
650
|
return null;
|
|
@@ -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) {
|