codex-autorunner 1.0.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/__init__.py +12 -1
- codex_autorunner/agents/codex/harness.py +1 -1
- codex_autorunner/agents/opencode/client.py +113 -4
- codex_autorunner/agents/opencode/constants.py +3 -0
- codex_autorunner/agents/opencode/harness.py +6 -1
- codex_autorunner/agents/opencode/runtime.py +59 -18
- codex_autorunner/agents/opencode/supervisor.py +4 -0
- codex_autorunner/agents/registry.py +36 -7
- codex_autorunner/bootstrap.py +226 -4
- codex_autorunner/cli.py +5 -1174
- codex_autorunner/codex_cli.py +20 -84
- codex_autorunner/core/__init__.py +20 -0
- codex_autorunner/core/about_car.py +119 -1
- codex_autorunner/core/app_server_ids.py +59 -0
- codex_autorunner/core/app_server_threads.py +17 -2
- codex_autorunner/core/app_server_utils.py +165 -0
- codex_autorunner/core/archive.py +349 -0
- codex_autorunner/core/codex_runner.py +6 -2
- codex_autorunner/core/config.py +433 -4
- codex_autorunner/core/context_awareness.py +38 -0
- codex_autorunner/core/docs.py +0 -122
- codex_autorunner/core/drafts.py +58 -4
- codex_autorunner/core/exceptions.py +4 -0
- codex_autorunner/core/filebox.py +265 -0
- codex_autorunner/core/flows/controller.py +96 -2
- codex_autorunner/core/flows/models.py +13 -0
- codex_autorunner/core/flows/reasons.py +52 -0
- codex_autorunner/core/flows/reconciler.py +134 -0
- codex_autorunner/core/flows/runtime.py +57 -4
- codex_autorunner/core/flows/store.py +142 -7
- codex_autorunner/core/flows/transition.py +27 -15
- codex_autorunner/core/flows/ux_helpers.py +272 -0
- codex_autorunner/core/flows/worker_process.py +32 -6
- codex_autorunner/core/git_utils.py +62 -0
- codex_autorunner/core/hub.py +291 -20
- codex_autorunner/core/lifecycle_events.py +253 -0
- codex_autorunner/core/notifications.py +14 -2
- 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/__init__.py +28 -0
- codex_autorunner/{integrations/agents → core/ports}/agent_backend.py +13 -8
- codex_autorunner/core/ports/backend_orchestrator.py +41 -0
- codex_autorunner/{integrations/agents → core/ports}/run_event.py +23 -6
- 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 +62 -0
- codex_autorunner/core/supervisor_protocol.py +15 -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/text_delta_coalescer.py +54 -0
- codex_autorunner/core/ticket_linter_cli.py +218 -0
- codex_autorunner/core/ticket_manager_cli.py +494 -0
- codex_autorunner/core/time_utils.py +11 -0
- codex_autorunner/core/types.py +18 -0
- codex_autorunner/core/update.py +4 -5
- codex_autorunner/core/update_paths.py +28 -0
- codex_autorunner/core/usage.py +164 -12
- codex_autorunner/core/utils.py +125 -15
- codex_autorunner/flows/review/__init__.py +17 -0
- codex_autorunner/{core/review.py → flows/review/service.py} +37 -34
- codex_autorunner/flows/ticket_flow/definition.py +52 -3
- codex_autorunner/integrations/agents/__init__.py +11 -19
- codex_autorunner/integrations/agents/backend_orchestrator.py +302 -0
- codex_autorunner/integrations/agents/codex_adapter.py +90 -0
- codex_autorunner/integrations/agents/codex_backend.py +177 -25
- codex_autorunner/integrations/agents/opencode_adapter.py +108 -0
- codex_autorunner/integrations/agents/opencode_backend.py +305 -32
- codex_autorunner/integrations/agents/runner.py +86 -0
- codex_autorunner/integrations/agents/wiring.py +279 -0
- codex_autorunner/integrations/app_server/client.py +7 -60
- codex_autorunner/integrations/app_server/env.py +2 -107
- codex_autorunner/{core/app_server_events.py → integrations/app_server/event_buffer.py} +15 -8
- codex_autorunner/integrations/telegram/adapter.py +65 -0
- codex_autorunner/integrations/telegram/config.py +46 -0
- codex_autorunner/integrations/telegram/constants.py +1 -1
- codex_autorunner/integrations/telegram/doctor.py +228 -6
- codex_autorunner/integrations/telegram/handlers/callbacks.py +7 -0
- 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 +1496 -71
- codex_autorunner/integrations/telegram/handlers/commands/workspace.py +498 -37
- codex_autorunner/integrations/telegram/handlers/commands_runtime.py +206 -48
- codex_autorunner/integrations/telegram/handlers/commands_spec.py +20 -3
- codex_autorunner/integrations/telegram/handlers/messages.py +27 -1
- codex_autorunner/integrations/telegram/handlers/selections.py +61 -1
- codex_autorunner/integrations/telegram/helpers.py +22 -1
- codex_autorunner/integrations/telegram/runtime.py +9 -4
- codex_autorunner/integrations/telegram/service.py +45 -10
- codex_autorunner/integrations/telegram/state.py +38 -0
- codex_autorunner/integrations/telegram/ticket_flow_bridge.py +338 -43
- codex_autorunner/integrations/telegram/transport.py +13 -4
- codex_autorunner/integrations/templates/__init__.py +27 -0
- codex_autorunner/integrations/templates/scan_agent.py +312 -0
- codex_autorunner/routes/__init__.py +37 -76
- codex_autorunner/routes/agents.py +2 -137
- codex_autorunner/routes/analytics.py +2 -238
- codex_autorunner/routes/app_server.py +2 -131
- codex_autorunner/routes/base.py +2 -596
- codex_autorunner/routes/file_chat.py +4 -833
- codex_autorunner/routes/flows.py +4 -977
- codex_autorunner/routes/messages.py +4 -456
- codex_autorunner/routes/repos.py +2 -196
- codex_autorunner/routes/review.py +2 -147
- codex_autorunner/routes/sessions.py +2 -175
- codex_autorunner/routes/settings.py +2 -168
- codex_autorunner/routes/shared.py +2 -275
- codex_autorunner/routes/system.py +4 -193
- codex_autorunner/routes/usage.py +2 -86
- codex_autorunner/routes/voice.py +2 -119
- codex_autorunner/routes/workspace.py +2 -270
- codex_autorunner/server.py +4 -4
- codex_autorunner/static/agentControls.js +61 -16
- codex_autorunner/static/app.js +126 -14
- codex_autorunner/static/archive.js +826 -0
- codex_autorunner/static/archiveApi.js +37 -0
- codex_autorunner/static/autoRefresh.js +7 -7
- codex_autorunner/static/chatUploads.js +137 -0
- codex_autorunner/static/dashboard.js +224 -171
- 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 +114 -131
- codex_autorunner/static/index.html +375 -49
- codex_autorunner/static/messages.js +568 -87
- codex_autorunner/static/notifications.js +255 -0
- codex_autorunner/static/pma.js +1167 -0
- codex_autorunner/static/preserve.js +17 -0
- codex_autorunner/static/settings.js +128 -6
- codex_autorunner/static/smartRefresh.js +52 -0
- codex_autorunner/static/streamUtils.js +57 -0
- codex_autorunner/static/styles.css +9798 -6143
- codex_autorunner/static/tabs.js +152 -11
- codex_autorunner/static/templateReposSettings.js +225 -0
- codex_autorunner/static/terminal.js +18 -0
- codex_autorunner/static/ticketChatActions.js +165 -3
- codex_autorunner/static/ticketChatStream.js +17 -119
- codex_autorunner/static/ticketEditor.js +137 -15
- codex_autorunner/static/ticketTemplates.js +798 -0
- codex_autorunner/static/tickets.js +821 -98
- codex_autorunner/static/turnEvents.js +27 -0
- codex_autorunner/static/turnResume.js +33 -0
- codex_autorunner/static/utils.js +39 -0
- codex_autorunner/static/workspace.js +389 -82
- codex_autorunner/static/workspaceFileBrowser.js +15 -13
- codex_autorunner/surfaces/__init__.py +5 -0
- codex_autorunner/surfaces/cli/__init__.py +6 -0
- codex_autorunner/surfaces/cli/cli.py +2534 -0
- codex_autorunner/surfaces/cli/codex_cli.py +20 -0
- codex_autorunner/surfaces/cli/pma_cli.py +817 -0
- codex_autorunner/surfaces/telegram/__init__.py +3 -0
- codex_autorunner/surfaces/web/__init__.py +1 -0
- codex_autorunner/surfaces/web/app.py +2223 -0
- codex_autorunner/surfaces/web/hub_jobs.py +192 -0
- codex_autorunner/surfaces/web/middleware.py +587 -0
- codex_autorunner/surfaces/web/pty_session.py +370 -0
- codex_autorunner/surfaces/web/review.py +6 -0
- codex_autorunner/surfaces/web/routes/__init__.py +82 -0
- codex_autorunner/surfaces/web/routes/agents.py +138 -0
- codex_autorunner/surfaces/web/routes/analytics.py +284 -0
- codex_autorunner/surfaces/web/routes/app_server.py +132 -0
- codex_autorunner/surfaces/web/routes/archive.py +357 -0
- codex_autorunner/surfaces/web/routes/base.py +615 -0
- codex_autorunner/surfaces/web/routes/file_chat.py +1117 -0
- codex_autorunner/surfaces/web/routes/filebox.py +227 -0
- codex_autorunner/surfaces/web/routes/flows.py +1354 -0
- codex_autorunner/surfaces/web/routes/messages.py +490 -0
- codex_autorunner/surfaces/web/routes/pma.py +1652 -0
- codex_autorunner/surfaces/web/routes/repos.py +197 -0
- codex_autorunner/surfaces/web/routes/review.py +148 -0
- codex_autorunner/surfaces/web/routes/sessions.py +176 -0
- codex_autorunner/surfaces/web/routes/settings.py +169 -0
- codex_autorunner/surfaces/web/routes/shared.py +277 -0
- codex_autorunner/surfaces/web/routes/system.py +196 -0
- codex_autorunner/surfaces/web/routes/templates.py +634 -0
- codex_autorunner/surfaces/web/routes/usage.py +89 -0
- codex_autorunner/surfaces/web/routes/voice.py +120 -0
- codex_autorunner/surfaces/web/routes/workspace.py +271 -0
- codex_autorunner/surfaces/web/runner_manager.py +25 -0
- codex_autorunner/surfaces/web/schemas.py +469 -0
- codex_autorunner/surfaces/web/static_assets.py +490 -0
- codex_autorunner/surfaces/web/static_refresh.py +86 -0
- codex_autorunner/surfaces/web/terminal_sessions.py +78 -0
- codex_autorunner/tickets/__init__.py +8 -1
- codex_autorunner/tickets/agent_pool.py +53 -4
- codex_autorunner/tickets/files.py +37 -16
- codex_autorunner/tickets/lint.py +50 -0
- codex_autorunner/tickets/models.py +6 -1
- codex_autorunner/tickets/outbox.py +50 -2
- codex_autorunner/tickets/runner.py +396 -57
- codex_autorunner/web/__init__.py +5 -1
- codex_autorunner/web/app.py +2 -1949
- codex_autorunner/web/hub_jobs.py +2 -191
- codex_autorunner/web/middleware.py +2 -586
- codex_autorunner/web/pty_session.py +2 -369
- codex_autorunner/web/runner_manager.py +2 -24
- codex_autorunner/web/schemas.py +2 -376
- codex_autorunner/web/static_assets.py +4 -441
- codex_autorunner/web/static_refresh.py +2 -85
- codex_autorunner/web/terminal_sessions.py +2 -77
- codex_autorunner/workspace/paths.py +49 -33
- codex_autorunner-1.2.0.dist-info/METADATA +150 -0
- codex_autorunner-1.2.0.dist-info/RECORD +339 -0
- codex_autorunner/core/adapter_utils.py +0 -21
- codex_autorunner/core/engine.py +0 -2653
- codex_autorunner/core/static_assets.py +0 -55
- codex_autorunner-1.0.0.dist-info/METADATA +0 -246
- codex_autorunner-1.0.0.dist-info/RECORD +0 -251
- /codex_autorunner/{routes → surfaces/web/routes}/terminal_images.py +0 -0
- {codex_autorunner-1.0.0.dist-info → codex_autorunner-1.2.0.dist-info}/WHEEL +0 -0
- {codex_autorunner-1.0.0.dist-info → codex_autorunner-1.2.0.dist-info}/entry_points.txt +0 -0
- {codex_autorunner-1.0.0.dist-info → codex_autorunner-1.2.0.dist-info}/licenses/LICENSE +0 -0
- {codex_autorunner-1.0.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
|
+
}
|