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.
Files changed (127) hide show
  1. codex_autorunner/agents/opencode/client.py +113 -4
  2. codex_autorunner/agents/opencode/supervisor.py +4 -0
  3. codex_autorunner/agents/registry.py +17 -7
  4. codex_autorunner/bootstrap.py +219 -1
  5. codex_autorunner/core/__init__.py +17 -1
  6. codex_autorunner/core/about_car.py +114 -1
  7. codex_autorunner/core/app_server_threads.py +6 -0
  8. codex_autorunner/core/config.py +236 -1
  9. codex_autorunner/core/context_awareness.py +38 -0
  10. codex_autorunner/core/docs.py +0 -122
  11. codex_autorunner/core/filebox.py +265 -0
  12. codex_autorunner/core/flows/controller.py +71 -1
  13. codex_autorunner/core/flows/reconciler.py +4 -1
  14. codex_autorunner/core/flows/runtime.py +22 -0
  15. codex_autorunner/core/flows/store.py +61 -9
  16. codex_autorunner/core/flows/transition.py +23 -16
  17. codex_autorunner/core/flows/ux_helpers.py +18 -3
  18. codex_autorunner/core/flows/worker_process.py +32 -6
  19. codex_autorunner/core/hub.py +198 -41
  20. codex_autorunner/core/lifecycle_events.py +253 -0
  21. codex_autorunner/core/path_utils.py +2 -1
  22. codex_autorunner/core/pma_audit.py +224 -0
  23. codex_autorunner/core/pma_context.py +496 -0
  24. codex_autorunner/core/pma_dispatch_interceptor.py +284 -0
  25. codex_autorunner/core/pma_lifecycle.py +527 -0
  26. codex_autorunner/core/pma_queue.py +367 -0
  27. codex_autorunner/core/pma_safety.py +221 -0
  28. codex_autorunner/core/pma_state.py +115 -0
  29. codex_autorunner/core/ports/agent_backend.py +2 -5
  30. codex_autorunner/core/ports/run_event.py +1 -4
  31. codex_autorunner/core/prompt.py +0 -80
  32. codex_autorunner/core/prompts.py +56 -172
  33. codex_autorunner/core/redaction.py +0 -4
  34. codex_autorunner/core/review_context.py +11 -9
  35. codex_autorunner/core/runner_controller.py +35 -33
  36. codex_autorunner/core/runner_state.py +147 -0
  37. codex_autorunner/core/runtime.py +829 -0
  38. codex_autorunner/core/sqlite_utils.py +13 -4
  39. codex_autorunner/core/state.py +7 -10
  40. codex_autorunner/core/state_roots.py +5 -0
  41. codex_autorunner/core/templates/__init__.py +39 -0
  42. codex_autorunner/core/templates/git_mirror.py +234 -0
  43. codex_autorunner/core/templates/provenance.py +56 -0
  44. codex_autorunner/core/templates/scan_cache.py +120 -0
  45. codex_autorunner/core/ticket_linter_cli.py +17 -0
  46. codex_autorunner/core/ticket_manager_cli.py +154 -92
  47. codex_autorunner/core/time_utils.py +11 -0
  48. codex_autorunner/core/types.py +18 -0
  49. codex_autorunner/core/utils.py +34 -6
  50. codex_autorunner/flows/review/service.py +23 -25
  51. codex_autorunner/flows/ticket_flow/definition.py +43 -1
  52. codex_autorunner/integrations/agents/__init__.py +2 -0
  53. codex_autorunner/integrations/agents/backend_orchestrator.py +18 -0
  54. codex_autorunner/integrations/agents/codex_backend.py +19 -8
  55. codex_autorunner/integrations/agents/runner.py +3 -8
  56. codex_autorunner/integrations/agents/wiring.py +8 -0
  57. codex_autorunner/integrations/telegram/doctor.py +228 -6
  58. codex_autorunner/integrations/telegram/handlers/commands/execution.py +236 -74
  59. codex_autorunner/integrations/telegram/handlers/commands/files.py +314 -75
  60. codex_autorunner/integrations/telegram/handlers/commands/flows.py +346 -58
  61. codex_autorunner/integrations/telegram/handlers/commands/workspace.py +498 -37
  62. codex_autorunner/integrations/telegram/handlers/commands_runtime.py +202 -45
  63. codex_autorunner/integrations/telegram/handlers/commands_spec.py +18 -7
  64. codex_autorunner/integrations/telegram/handlers/messages.py +26 -1
  65. codex_autorunner/integrations/telegram/helpers.py +1 -3
  66. codex_autorunner/integrations/telegram/runtime.py +9 -4
  67. codex_autorunner/integrations/telegram/service.py +30 -0
  68. codex_autorunner/integrations/telegram/state.py +38 -0
  69. codex_autorunner/integrations/telegram/ticket_flow_bridge.py +10 -4
  70. codex_autorunner/integrations/telegram/transport.py +10 -3
  71. codex_autorunner/integrations/templates/__init__.py +27 -0
  72. codex_autorunner/integrations/templates/scan_agent.py +312 -0
  73. codex_autorunner/server.py +2 -2
  74. codex_autorunner/static/agentControls.js +21 -5
  75. codex_autorunner/static/app.js +115 -11
  76. codex_autorunner/static/chatUploads.js +137 -0
  77. codex_autorunner/static/docChatCore.js +185 -13
  78. codex_autorunner/static/fileChat.js +68 -40
  79. codex_autorunner/static/fileboxUi.js +159 -0
  80. codex_autorunner/static/hub.js +46 -81
  81. codex_autorunner/static/index.html +303 -24
  82. codex_autorunner/static/messages.js +82 -4
  83. codex_autorunner/static/notifications.js +255 -0
  84. codex_autorunner/static/pma.js +1167 -0
  85. codex_autorunner/static/settings.js +3 -0
  86. codex_autorunner/static/streamUtils.js +57 -0
  87. codex_autorunner/static/styles.css +9125 -6742
  88. codex_autorunner/static/templateReposSettings.js +225 -0
  89. codex_autorunner/static/ticketChatActions.js +165 -3
  90. codex_autorunner/static/ticketChatStream.js +17 -119
  91. codex_autorunner/static/ticketEditor.js +41 -13
  92. codex_autorunner/static/ticketTemplates.js +798 -0
  93. codex_autorunner/static/tickets.js +69 -19
  94. codex_autorunner/static/turnEvents.js +27 -0
  95. codex_autorunner/static/turnResume.js +33 -0
  96. codex_autorunner/static/utils.js +28 -0
  97. codex_autorunner/static/workspace.js +258 -44
  98. codex_autorunner/static/workspaceFileBrowser.js +6 -4
  99. codex_autorunner/surfaces/cli/cli.py +1465 -155
  100. codex_autorunner/surfaces/cli/pma_cli.py +817 -0
  101. codex_autorunner/surfaces/web/app.py +253 -49
  102. codex_autorunner/surfaces/web/routes/__init__.py +4 -0
  103. codex_autorunner/surfaces/web/routes/analytics.py +29 -22
  104. codex_autorunner/surfaces/web/routes/file_chat.py +317 -36
  105. codex_autorunner/surfaces/web/routes/filebox.py +227 -0
  106. codex_autorunner/surfaces/web/routes/flows.py +219 -29
  107. codex_autorunner/surfaces/web/routes/messages.py +70 -39
  108. codex_autorunner/surfaces/web/routes/pma.py +1652 -0
  109. codex_autorunner/surfaces/web/routes/repos.py +1 -1
  110. codex_autorunner/surfaces/web/routes/shared.py +0 -3
  111. codex_autorunner/surfaces/web/routes/templates.py +634 -0
  112. codex_autorunner/surfaces/web/runner_manager.py +2 -2
  113. codex_autorunner/surfaces/web/schemas.py +70 -18
  114. codex_autorunner/tickets/agent_pool.py +27 -0
  115. codex_autorunner/tickets/files.py +33 -16
  116. codex_autorunner/tickets/lint.py +50 -0
  117. codex_autorunner/tickets/models.py +3 -0
  118. codex_autorunner/tickets/outbox.py +41 -5
  119. codex_autorunner/tickets/runner.py +350 -69
  120. {codex_autorunner-1.1.0.dist-info → codex_autorunner-1.2.0.dist-info}/METADATA +15 -19
  121. {codex_autorunner-1.1.0.dist-info → codex_autorunner-1.2.0.dist-info}/RECORD +125 -94
  122. codex_autorunner/core/adapter_utils.py +0 -21
  123. codex_autorunner/core/engine.py +0 -3302
  124. {codex_autorunner-1.1.0.dist-info → codex_autorunner-1.2.0.dist-info}/WHEEL +0 -0
  125. {codex_autorunner-1.1.0.dist-info → codex_autorunner-1.2.0.dist-info}/entry_points.txt +0 -0
  126. {codex_autorunner-1.1.0.dist-info → codex_autorunner-1.2.0.dist-info}/licenses/LICENSE +0 -0
  127. {codex_autorunner-1.1.0.dist-info → codex_autorunner-1.2.0.dist-info}/top_level.txt +0 -0
@@ -0,0 +1,798 @@
1
+ // GENERATED FILE - do not edit directly. Source: static_src/
2
+ /**
3
+ * Ticket Templates - Template picker for creating tickets from templates
4
+ */
5
+ import { api, flash } from "./utils.js";
6
+ import { openTicketEditor } from "./ticketEditor.js";
7
+ const TEMPLATE_HISTORY_KEY = "car:ticket-template-history";
8
+ const MAX_HISTORY_ITEMS = 10;
9
+ const FETCH_DEBOUNCE_MS = 500;
10
+ const state = {
11
+ isOpen: false,
12
+ enabled: false,
13
+ loading: false,
14
+ previewContent: null,
15
+ lastFetchedRef: null,
16
+ repos: [],
17
+ fetchDebounceTimer: null,
18
+ };
19
+ function els() {
20
+ return {
21
+ modal: document.getElementById("ticket-template-modal"),
22
+ refInput: document.getElementById("ticket-template-ref"),
23
+ clearBtn: document.getElementById("ticket-template-clear"),
24
+ agentSelect: document.getElementById("ticket-template-agent"),
25
+ preview: document.getElementById("ticket-template-preview"),
26
+ previewStatus: document.getElementById("ticket-template-preview-status"),
27
+ error: document.getElementById("ticket-template-error"),
28
+ cancelBtn: document.getElementById("ticket-template-cancel"),
29
+ applyBtn: document.getElementById("ticket-template-apply"),
30
+ closeBtn: document.getElementById("ticket-template-close"),
31
+ reposContainer: document.getElementById("ticket-template-repos"),
32
+ recentContainer: document.getElementById("ticket-template-recent"),
33
+ inputHint: document.getElementById("ticket-template-hint"),
34
+ // Split button
35
+ dropdownToggle: document.getElementById("ticket-new-dropdown-toggle"),
36
+ dropdown: document.getElementById("ticket-new-dropdown"),
37
+ fromTemplateBtn: document.getElementById("ticket-new-from-template"),
38
+ // Mobile
39
+ overflowTemplate: document.getElementById("ticket-overflow-template"),
40
+ };
41
+ }
42
+ /**
43
+ * Parse a GitHub URL into template reference format
44
+ * Supports:
45
+ * - https://github.com/owner/repo/blob/branch/path/to/file.md
46
+ * - https://raw.githubusercontent.com/owner/repo/branch/path/to/file.md
47
+ * Returns null if not a valid GitHub URL
48
+ */
49
+ function parseGitHubUrl(input) {
50
+ const trimmed = input.trim();
51
+ // GitHub blob URL: https://github.com/owner/repo/blob/branch/path/to/file.md
52
+ const blobMatch = trimmed.match(/^https?:\/\/github\.com\/([^/]+)\/([^/]+)\/blob\/([^/]+)\/(.+)$/);
53
+ if (blobMatch) {
54
+ const [, owner, repo, ref, path] = blobMatch;
55
+ const ownerRepo = `${owner}/${repo}`;
56
+ // Try to find matching repo by URL pattern
57
+ const configuredRepoId = findRepoIdByUrl(`github.com/${ownerRepo}`);
58
+ return {
59
+ repoId: configuredRepoId || ownerRepo,
60
+ path,
61
+ ref,
62
+ isConfigured: configuredRepoId !== null,
63
+ originalOwnerRepo: ownerRepo,
64
+ };
65
+ }
66
+ // Raw GitHub URL: https://raw.githubusercontent.com/owner/repo/branch/path/to/file.md
67
+ const rawMatch = trimmed.match(/^https?:\/\/raw\.githubusercontent\.com\/([^/]+)\/([^/]+)\/([^/]+)\/(.+)$/);
68
+ if (rawMatch) {
69
+ const [, owner, repo, ref, path] = rawMatch;
70
+ const ownerRepo = `${owner}/${repo}`;
71
+ const configuredRepoId = findRepoIdByUrl(`github.com/${ownerRepo}`);
72
+ return {
73
+ repoId: configuredRepoId || ownerRepo,
74
+ path,
75
+ ref,
76
+ isConfigured: configuredRepoId !== null,
77
+ originalOwnerRepo: ownerRepo,
78
+ };
79
+ }
80
+ return null;
81
+ }
82
+ /**
83
+ * Find a configured repo ID by matching URL pattern
84
+ */
85
+ function findRepoIdByUrl(urlFragment) {
86
+ for (const repo of state.repos) {
87
+ if (repo.url.includes(urlFragment)) {
88
+ return repo.id;
89
+ }
90
+ }
91
+ return null;
92
+ }
93
+ /**
94
+ * Convert input to template reference format
95
+ * Handles both direct refs and GitHub URLs
96
+ */
97
+ function normalizeTemplateRef(input) {
98
+ const trimmed = input.trim();
99
+ if (!trimmed)
100
+ return { ref: "", isGitHubUrl: false, isConfigured: true };
101
+ // Check if it's a GitHub URL
102
+ const parsed = parseGitHubUrl(trimmed);
103
+ if (parsed) {
104
+ return {
105
+ ref: `${parsed.repoId}:${parsed.path}@${parsed.ref}`,
106
+ isGitHubUrl: true,
107
+ isConfigured: parsed.isConfigured,
108
+ originalOwnerRepo: parsed.originalOwnerRepo,
109
+ };
110
+ }
111
+ // Already in correct format - check if repo part is configured
112
+ const colonIdx = trimmed.indexOf(":");
113
+ if (colonIdx > 0) {
114
+ const repoId = trimmed.slice(0, colonIdx);
115
+ const isConfigured = state.repos.some((r) => r.id === repoId);
116
+ return { ref: trimmed, isGitHubUrl: false, isConfigured };
117
+ }
118
+ return { ref: trimmed, isGitHubUrl: false, isConfigured: true };
119
+ }
120
+ /**
121
+ * Load template history from localStorage
122
+ */
123
+ function loadHistory() {
124
+ try {
125
+ const raw = localStorage.getItem(TEMPLATE_HISTORY_KEY);
126
+ if (!raw)
127
+ return [];
128
+ const parsed = JSON.parse(raw);
129
+ return Array.isArray(parsed) ? parsed.filter((x) => typeof x === "string") : [];
130
+ }
131
+ catch {
132
+ return [];
133
+ }
134
+ }
135
+ /**
136
+ * Save template ref to history
137
+ */
138
+ function saveToHistory(ref) {
139
+ const history = loadHistory();
140
+ const filtered = history.filter((h) => h !== ref);
141
+ filtered.unshift(ref);
142
+ const trimmed = filtered.slice(0, MAX_HISTORY_ITEMS);
143
+ try {
144
+ localStorage.setItem(TEMPLATE_HISTORY_KEY, JSON.stringify(trimmed));
145
+ }
146
+ catch {
147
+ // Ignore storage errors
148
+ }
149
+ }
150
+ /**
151
+ * Get the current repo prefix from the input
152
+ */
153
+ function getCurrentRepoPrefix() {
154
+ const { refInput } = els();
155
+ if (!refInput)
156
+ return null;
157
+ const value = refInput.value.trim();
158
+ const colonIdx = value.indexOf(":");
159
+ if (colonIdx > 0) {
160
+ return value.slice(0, colonIdx);
161
+ }
162
+ return null;
163
+ }
164
+ /**
165
+ * Render available repos as chips (toggleable)
166
+ */
167
+ function renderRepos() {
168
+ const { reposContainer } = els();
169
+ if (!reposContainer)
170
+ return;
171
+ if (state.repos.length === 0) {
172
+ reposContainer.innerHTML = '<span class="muted small">No template repos configured</span>';
173
+ return;
174
+ }
175
+ const currentPrefix = getCurrentRepoPrefix();
176
+ reposContainer.innerHTML = "";
177
+ for (const repo of state.repos) {
178
+ const chip = document.createElement("button");
179
+ chip.type = "button";
180
+ chip.className = "ticket-template-chip";
181
+ chip.dataset.repoId = repo.id;
182
+ // Highlight if this repo is currently selected
183
+ if (currentPrefix === repo.id) {
184
+ chip.classList.add("active");
185
+ }
186
+ chip.textContent = repo.id;
187
+ chip.title = `${repo.url}${repo.trusted ? " (trusted)" : ""}`;
188
+ chip.addEventListener("click", () => {
189
+ const { refInput } = els();
190
+ if (!refInput)
191
+ return;
192
+ const isCurrentlyActive = chip.classList.contains("active");
193
+ if (isCurrentlyActive) {
194
+ // Deselect - clear the repo prefix but keep the path if any
195
+ const colonIdx = refInput.value.indexOf(":");
196
+ if (colonIdx > 0) {
197
+ refInput.value = refInput.value.slice(colonIdx + 1);
198
+ }
199
+ }
200
+ else {
201
+ // Select - prepend repo prefix
202
+ const colonIdx = refInput.value.indexOf(":");
203
+ const pathPart = colonIdx > 0 ? refInput.value.slice(colonIdx + 1) : refInput.value;
204
+ refInput.value = `${repo.id}:${pathPart}`;
205
+ }
206
+ refInput.focus();
207
+ refInput.setSelectionRange(refInput.value.length, refInput.value.length);
208
+ // Update chip states
209
+ updateRepoChipStates();
210
+ // Clear preview since ref changed
211
+ clearPreview();
212
+ hideError();
213
+ updateInputHint();
214
+ });
215
+ reposContainer.appendChild(chip);
216
+ }
217
+ }
218
+ /**
219
+ * Update repo chip active states based on current input
220
+ */
221
+ function updateRepoChipStates() {
222
+ const { reposContainer } = els();
223
+ if (!reposContainer)
224
+ return;
225
+ const currentPrefix = getCurrentRepoPrefix();
226
+ const chips = reposContainer.querySelectorAll(".ticket-template-chip");
227
+ chips.forEach((chip) => {
228
+ const repoId = chip.dataset.repoId;
229
+ if (repoId === currentPrefix) {
230
+ chip.classList.add("active");
231
+ }
232
+ else {
233
+ chip.classList.remove("active");
234
+ }
235
+ });
236
+ }
237
+ /**
238
+ * Clear preview state
239
+ */
240
+ function clearPreview() {
241
+ const { preview, applyBtn, previewStatus } = els();
242
+ if (preview) {
243
+ preview.textContent = "Template content will appear here after you enter a reference above.";
244
+ preview.classList.remove("has-content", "loading");
245
+ }
246
+ if (applyBtn)
247
+ applyBtn.disabled = true;
248
+ if (previewStatus)
249
+ previewStatus.textContent = "";
250
+ state.previewContent = null;
251
+ state.lastFetchedRef = null;
252
+ }
253
+ /**
254
+ * Update the input hint based on current state
255
+ */
256
+ function updateInputHint() {
257
+ const { refInput, inputHint } = els();
258
+ if (!inputHint || !refInput)
259
+ return;
260
+ const value = refInput.value.trim();
261
+ if (!value) {
262
+ inputHint.textContent = "or paste GitHub URL";
263
+ inputHint.classList.remove("hidden");
264
+ }
265
+ else if (value.includes(":") && value.includes("/")) {
266
+ // Looks like a complete reference
267
+ inputHint.classList.add("hidden");
268
+ }
269
+ else if (value.includes(":")) {
270
+ // Has repo prefix, needs path
271
+ inputHint.textContent = "add path to template file";
272
+ inputHint.classList.remove("hidden");
273
+ }
274
+ else {
275
+ inputHint.textContent = "or paste GitHub URL";
276
+ inputHint.classList.remove("hidden");
277
+ }
278
+ }
279
+ /**
280
+ * Render recent templates as clickable chips
281
+ */
282
+ function renderRecentTemplates() {
283
+ const { recentContainer } = els();
284
+ if (!recentContainer)
285
+ return;
286
+ const history = loadHistory();
287
+ if (history.length === 0) {
288
+ recentContainer.classList.add("hidden");
289
+ return;
290
+ }
291
+ recentContainer.classList.remove("hidden");
292
+ recentContainer.innerHTML = "";
293
+ const label = document.createElement("span");
294
+ label.className = "ticket-template-label";
295
+ label.textContent = "Recent";
296
+ recentContainer.appendChild(label);
297
+ const chipsDiv = document.createElement("div");
298
+ chipsDiv.className = "ticket-template-chips";
299
+ // Show up to 5 recent templates
300
+ for (const ref of history.slice(0, 5)) {
301
+ const chip = document.createElement("button");
302
+ chip.type = "button";
303
+ chip.className = "ticket-template-chip ticket-template-chip-recent";
304
+ // Show shortened version: just the path part
305
+ const parts = ref.split(":");
306
+ const displayText = parts.length > 1 ? parts.slice(1).join(":").split("@")[0] : ref;
307
+ chip.textContent = displayText.length > 30 ? "..." + displayText.slice(-27) : displayText;
308
+ chip.title = ref;
309
+ chip.addEventListener("click", () => {
310
+ const { refInput } = els();
311
+ if (refInput) {
312
+ refInput.value = ref;
313
+ void fetchTemplatePreview();
314
+ }
315
+ });
316
+ chipsDiv.appendChild(chip);
317
+ }
318
+ recentContainer.appendChild(chipsDiv);
319
+ }
320
+ /**
321
+ * Format a user-friendly error message
322
+ */
323
+ function formatErrorMessage(rawMessage) {
324
+ // Try to parse JSON error format from backend
325
+ try {
326
+ // Check if it looks like JSON
327
+ if (rawMessage.startsWith("{") || rawMessage.includes('"code"')) {
328
+ const parsed = JSON.parse(rawMessage);
329
+ if (parsed.message) {
330
+ return parsed.message;
331
+ }
332
+ }
333
+ }
334
+ catch {
335
+ // Not JSON, use as-is
336
+ }
337
+ // Clean up common error patterns
338
+ let msg = rawMessage;
339
+ // Remove JSON-like prefixes
340
+ msg = msg.replace(/^\{"code":"[^"]+","message":"?/, "").replace(/"?\}$/, "");
341
+ // Make repo errors more helpful
342
+ if (msg.includes("Template repo not configured")) {
343
+ const match = msg.match(/not configured:\s*(.+)/);
344
+ if (match) {
345
+ const repoId = match[1].trim();
346
+ const availableRepos = state.repos.map((r) => r.id);
347
+ if (availableRepos.length > 0) {
348
+ return `Repository "${repoId}" is not configured. Available repos: ${availableRepos.join(", ")}`;
349
+ }
350
+ return `Repository "${repoId}" is not configured. Add it to your templates config first.`;
351
+ }
352
+ }
353
+ return msg;
354
+ }
355
+ /**
356
+ * Show error message
357
+ */
358
+ function showError(message) {
359
+ const { error } = els();
360
+ if (!error)
361
+ return;
362
+ error.textContent = formatErrorMessage(message);
363
+ error.classList.remove("hidden");
364
+ }
365
+ /**
366
+ * Hide error message
367
+ */
368
+ function hideError() {
369
+ const { error } = els();
370
+ if (!error)
371
+ return;
372
+ error.textContent = "";
373
+ error.classList.add("hidden");
374
+ error.classList.remove("warning");
375
+ }
376
+ /**
377
+ * Update loading state
378
+ */
379
+ function setLoading(loading) {
380
+ state.loading = loading;
381
+ const { applyBtn, previewStatus, preview } = els();
382
+ if (applyBtn)
383
+ applyBtn.disabled = loading || !state.previewContent;
384
+ if (previewStatus) {
385
+ previewStatus.textContent = loading ? "Loading..." : "";
386
+ }
387
+ if (preview && loading) {
388
+ preview.classList.add("loading");
389
+ }
390
+ else if (preview) {
391
+ preview.classList.remove("loading");
392
+ }
393
+ }
394
+ /**
395
+ * Check if templates are enabled and update UI accordingly
396
+ */
397
+ export async function checkTemplatesEnabled() {
398
+ try {
399
+ const data = (await api("/api/templates/repos"));
400
+ state.enabled = data.enabled;
401
+ state.repos = data.repos || [];
402
+ const { dropdownToggle, overflowTemplate } = els();
403
+ if (state.enabled) {
404
+ dropdownToggle?.classList.remove("hidden");
405
+ overflowTemplate?.classList.remove("hidden");
406
+ }
407
+ else {
408
+ dropdownToggle?.classList.add("hidden");
409
+ overflowTemplate?.classList.add("hidden");
410
+ }
411
+ return state.enabled;
412
+ }
413
+ catch {
414
+ state.enabled = false;
415
+ state.repos = [];
416
+ return false;
417
+ }
418
+ }
419
+ /**
420
+ * Fetch template content for preview (debounced version)
421
+ */
422
+ function debouncedFetchPreview() {
423
+ if (state.fetchDebounceTimer) {
424
+ clearTimeout(state.fetchDebounceTimer);
425
+ }
426
+ state.fetchDebounceTimer = setTimeout(() => {
427
+ void fetchTemplatePreview();
428
+ }, FETCH_DEBOUNCE_MS);
429
+ }
430
+ /**
431
+ * Fetch template content for preview
432
+ */
433
+ async function fetchTemplatePreview() {
434
+ const { refInput, preview, applyBtn, previewStatus } = els();
435
+ if (!refInput || !preview)
436
+ return;
437
+ const rawInput = refInput.value.trim();
438
+ if (!rawInput) {
439
+ preview.textContent = "Enter a template reference to see preview.";
440
+ preview.classList.remove("has-content");
441
+ state.previewContent = null;
442
+ state.lastFetchedRef = null;
443
+ if (applyBtn)
444
+ applyBtn.disabled = true;
445
+ if (previewStatus)
446
+ previewStatus.textContent = "";
447
+ return;
448
+ }
449
+ // Normalize the input (handle GitHub URLs)
450
+ const normalized = normalizeTemplateRef(rawInput);
451
+ const templateRef = normalized.ref;
452
+ // Handle non-configured repos before making API call
453
+ if (!normalized.isConfigured) {
454
+ const availableRepos = state.repos.map((r) => r.id);
455
+ if (normalized.isGitHubUrl) {
456
+ // GitHub URL to non-configured repo
457
+ let msg = `Repository "${normalized.originalOwnerRepo}" is not configured. `;
458
+ msg += "To use templates from this repo, add it to templates.repos in your config (can be trusted or untrusted).";
459
+ if (availableRepos.length > 0) {
460
+ msg += ` Currently available: ${availableRepos.join(", ")}`;
461
+ }
462
+ showError(msg);
463
+ }
464
+ else {
465
+ // Direct reference to non-configured repo
466
+ const colonIdx = templateRef.indexOf(":");
467
+ const repoId = colonIdx > 0 ? templateRef.slice(0, colonIdx) : templateRef;
468
+ let msg = `Repository "${repoId}" is not configured. Add it to templates.repos in your config first.`;
469
+ if (availableRepos.length > 0) {
470
+ msg += ` Available: ${availableRepos.join(", ")}`;
471
+ }
472
+ showError(msg);
473
+ }
474
+ preview.textContent = "";
475
+ preview.classList.remove("has-content");
476
+ state.previewContent = null;
477
+ state.lastFetchedRef = null;
478
+ if (applyBtn)
479
+ applyBtn.disabled = true;
480
+ if (previewStatus)
481
+ previewStatus.textContent = "";
482
+ return;
483
+ }
484
+ // Update input if we normalized a GitHub URL
485
+ if (normalized.isGitHubUrl && templateRef !== rawInput) {
486
+ refInput.value = templateRef;
487
+ if (previewStatus)
488
+ previewStatus.textContent = "Converted from GitHub URL";
489
+ // Update chip states since input changed
490
+ updateRepoChipStates();
491
+ }
492
+ // Don't refetch if same ref
493
+ if (templateRef === state.lastFetchedRef && state.previewContent) {
494
+ return;
495
+ }
496
+ setLoading(true);
497
+ hideError();
498
+ try {
499
+ const data = (await api("/api/templates/fetch", {
500
+ method: "POST",
501
+ body: { template: templateRef },
502
+ }));
503
+ state.previewContent = data.content;
504
+ state.lastFetchedRef = templateRef;
505
+ // Show preview with truncation for very long templates
506
+ const maxPreviewLines = 30;
507
+ const lines = data.content.split("\n");
508
+ if (lines.length > maxPreviewLines) {
509
+ preview.textContent = lines.slice(0, maxPreviewLines).join("\n") + "\n... (truncated)";
510
+ }
511
+ else {
512
+ preview.textContent = data.content;
513
+ }
514
+ preview.classList.add("has-content");
515
+ // Show scan info if untrusted
516
+ if (!data.trusted && data.scan_decision) {
517
+ if (previewStatus) {
518
+ previewStatus.textContent = `Scanned: ${data.scan_decision.decision} (${data.scan_decision.severity})`;
519
+ }
520
+ }
521
+ else if (previewStatus && !previewStatus.textContent?.includes("Converted")) {
522
+ previewStatus.textContent = data.trusted ? "Trusted" : "Scanned";
523
+ }
524
+ if (applyBtn)
525
+ applyBtn.disabled = false;
526
+ }
527
+ catch (err) {
528
+ const error = err;
529
+ const message = error.detail?.message || error.message || "Failed to fetch template";
530
+ showError(message);
531
+ preview.textContent = "";
532
+ preview.classList.remove("has-content");
533
+ state.previewContent = null;
534
+ state.lastFetchedRef = null;
535
+ if (applyBtn)
536
+ applyBtn.disabled = true;
537
+ if (previewStatus)
538
+ previewStatus.textContent = "";
539
+ }
540
+ finally {
541
+ setLoading(false);
542
+ }
543
+ }
544
+ /**
545
+ * Apply template to create a new ticket
546
+ */
547
+ async function applyTemplate() {
548
+ const { refInput, agentSelect } = els();
549
+ if (!refInput)
550
+ return;
551
+ const rawInput = refInput.value.trim();
552
+ if (!rawInput) {
553
+ showError("Please enter a template reference.");
554
+ return;
555
+ }
556
+ const normalized = normalizeTemplateRef(rawInput);
557
+ const templateRef = normalized.ref;
558
+ if (!normalized.isConfigured) {
559
+ showError("This repository is not configured. Please use a configured template repo.");
560
+ return;
561
+ }
562
+ setLoading(true);
563
+ hideError();
564
+ try {
565
+ const body = { template: templateRef };
566
+ const agentOverride = agentSelect?.value;
567
+ if (agentOverride) {
568
+ body.set_agent = agentOverride;
569
+ }
570
+ const result = (await api("/api/templates/apply", {
571
+ method: "POST",
572
+ body,
573
+ }));
574
+ // Save to history on success
575
+ saveToHistory(templateRef);
576
+ // Close template modal
577
+ closeTemplateModal();
578
+ // Fetch the created ticket and open in editor
579
+ try {
580
+ const ticketData = await api(`/api/flows/ticket_flow/tickets/${result.index}`);
581
+ openTicketEditor(ticketData);
582
+ }
583
+ catch {
584
+ flash(`Created ${result.filename}`, "success");
585
+ }
586
+ }
587
+ catch (err) {
588
+ const error = err;
589
+ const message = error.detail?.message || error.message || "Failed to create ticket from template";
590
+ showError(message);
591
+ }
592
+ finally {
593
+ setLoading(false);
594
+ }
595
+ }
596
+ /**
597
+ * Open the template picker modal
598
+ */
599
+ export function openTemplateModal() {
600
+ const { modal, refInput, preview, applyBtn, previewStatus, clearBtn } = els();
601
+ if (!modal)
602
+ return;
603
+ // Reset state
604
+ state.isOpen = true;
605
+ state.previewContent = null;
606
+ state.lastFetchedRef = null;
607
+ // Clear form and set dynamic placeholder
608
+ if (refInput) {
609
+ refInput.value = "";
610
+ // Set placeholder with actual repo name if available
611
+ const repoId = state.repos.length > 0 ? state.repos[0].id : "repo";
612
+ refInput.placeholder = `${repoId}:path/to/template.md`;
613
+ }
614
+ if (preview) {
615
+ preview.textContent = "Template content will appear here after you enter a reference above.";
616
+ preview.classList.remove("has-content", "loading");
617
+ }
618
+ if (applyBtn)
619
+ applyBtn.disabled = true;
620
+ if (previewStatus)
621
+ previewStatus.textContent = "";
622
+ // Hide clear button initially
623
+ if (clearBtn)
624
+ clearBtn.classList.add("hidden");
625
+ hideError();
626
+ renderRepos();
627
+ renderRecentTemplates();
628
+ updateInputHint();
629
+ // Show modal
630
+ modal.classList.remove("hidden");
631
+ // Focus input
632
+ refInput?.focus();
633
+ // Close any open dropdowns
634
+ const { dropdown } = els();
635
+ dropdown?.classList.add("hidden");
636
+ }
637
+ /**
638
+ * Close the template picker modal
639
+ */
640
+ export function closeTemplateModal() {
641
+ const { modal } = els();
642
+ if (!modal)
643
+ return;
644
+ // Clear any pending debounce
645
+ if (state.fetchDebounceTimer) {
646
+ clearTimeout(state.fetchDebounceTimer);
647
+ state.fetchDebounceTimer = null;
648
+ }
649
+ state.isOpen = false;
650
+ modal.classList.add("hidden");
651
+ }
652
+ /**
653
+ * Toggle the split button dropdown
654
+ */
655
+ function toggleDropdown() {
656
+ const { dropdown } = els();
657
+ if (!dropdown)
658
+ return;
659
+ dropdown.classList.toggle("hidden");
660
+ }
661
+ /**
662
+ * Close dropdown when clicking outside
663
+ */
664
+ function handleDocumentClick(e) {
665
+ const { dropdown, dropdownToggle } = els();
666
+ if (!dropdown || !dropdownToggle)
667
+ return;
668
+ if (!dropdown.contains(e.target) && !dropdownToggle.contains(e.target)) {
669
+ dropdown.classList.add("hidden");
670
+ }
671
+ }
672
+ /**
673
+ * Update clear button visibility
674
+ */
675
+ function updateClearButton() {
676
+ const { refInput, clearBtn } = els();
677
+ if (!clearBtn || !refInput)
678
+ return;
679
+ if (refInput.value.trim()) {
680
+ clearBtn.classList.remove("hidden");
681
+ }
682
+ else {
683
+ clearBtn.classList.add("hidden");
684
+ }
685
+ }
686
+ /**
687
+ * Clear input and reset state
688
+ */
689
+ function clearInput() {
690
+ const { refInput } = els();
691
+ if (!refInput)
692
+ return;
693
+ refInput.value = "";
694
+ clearPreview();
695
+ hideError();
696
+ updateRepoChipStates();
697
+ updateInputHint();
698
+ updateClearButton();
699
+ refInput.focus();
700
+ }
701
+ /**
702
+ * Initialize the template picker
703
+ */
704
+ export function initTicketTemplates() {
705
+ const { modal, refInput, clearBtn, cancelBtn, applyBtn, closeBtn, dropdownToggle, fromTemplateBtn, overflowTemplate, } = els();
706
+ if (!modal)
707
+ return;
708
+ // Prevent double initialization
709
+ if (modal.dataset.templateInitialized === "1")
710
+ return;
711
+ modal.dataset.templateInitialized = "1";
712
+ // Check if templates are enabled and load repos
713
+ void checkTemplatesEnabled();
714
+ // Split button dropdown toggle
715
+ dropdownToggle?.addEventListener("click", (e) => {
716
+ e.stopPropagation();
717
+ toggleDropdown();
718
+ });
719
+ // "From Template" button in dropdown
720
+ fromTemplateBtn?.addEventListener("click", () => {
721
+ openTemplateModal();
722
+ });
723
+ // Mobile overflow template button
724
+ overflowTemplate?.addEventListener("click", () => {
725
+ const overflowDropdown = document.getElementById("ticket-overflow-dropdown");
726
+ overflowDropdown?.classList.add("hidden");
727
+ openTemplateModal();
728
+ });
729
+ // Modal close button
730
+ closeBtn?.addEventListener("click", closeTemplateModal);
731
+ cancelBtn?.addEventListener("click", closeTemplateModal);
732
+ // Clear button
733
+ clearBtn?.addEventListener("click", clearInput);
734
+ // Apply button
735
+ applyBtn?.addEventListener("click", () => void applyTemplate());
736
+ // Auto-fetch on input change (debounced)
737
+ refInput?.addEventListener("input", () => {
738
+ const value = refInput.value.trim();
739
+ updateClearButton();
740
+ updateRepoChipStates();
741
+ updateInputHint();
742
+ if (value) {
743
+ debouncedFetchPreview();
744
+ }
745
+ else {
746
+ clearPreview();
747
+ hideError();
748
+ }
749
+ });
750
+ // Handle paste - immediately try to fetch
751
+ refInput?.addEventListener("paste", () => {
752
+ // Wait for paste to complete
753
+ setTimeout(() => {
754
+ updateClearButton();
755
+ updateRepoChipStates();
756
+ updateInputHint();
757
+ const value = refInput.value.trim();
758
+ if (value) {
759
+ void fetchTemplatePreview();
760
+ }
761
+ }, 0);
762
+ });
763
+ // Enter key: if preview loaded, apply; otherwise fetch preview
764
+ refInput?.addEventListener("keydown", (e) => {
765
+ if (e.key === "Enter") {
766
+ e.preventDefault();
767
+ if (state.previewContent && !state.loading) {
768
+ void applyTemplate();
769
+ }
770
+ else {
771
+ void fetchTemplatePreview();
772
+ }
773
+ }
774
+ });
775
+ // Cmd/Ctrl+Enter always applies if preview is ready
776
+ document.addEventListener("keydown", (e) => {
777
+ if (state.isOpen && (e.metaKey || e.ctrlKey) && e.key === "Enter") {
778
+ if (state.previewContent && !state.loading) {
779
+ e.preventDefault();
780
+ void applyTemplate();
781
+ }
782
+ }
783
+ });
784
+ // Close modal on backdrop click
785
+ modal.addEventListener("click", (e) => {
786
+ if (e.target === modal) {
787
+ closeTemplateModal();
788
+ }
789
+ });
790
+ // Close modal on Escape
791
+ document.addEventListener("keydown", (e) => {
792
+ if (e.key === "Escape" && state.isOpen) {
793
+ closeTemplateModal();
794
+ }
795
+ });
796
+ // Close dropdown when clicking elsewhere
797
+ document.addEventListener("click", handleDocumentClick);
798
+ }