codex-autorunner 0.1.2__py3-none-any.whl → 1.1.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/__main__.py +4 -0
- codex_autorunner/agents/codex/harness.py +1 -1
- codex_autorunner/agents/opencode/client.py +68 -35
- codex_autorunner/agents/opencode/constants.py +3 -0
- codex_autorunner/agents/opencode/harness.py +6 -1
- codex_autorunner/agents/opencode/logging.py +21 -5
- codex_autorunner/agents/opencode/run_prompt.py +1 -0
- codex_autorunner/agents/opencode/runtime.py +176 -47
- codex_autorunner/agents/opencode/supervisor.py +36 -48
- codex_autorunner/agents/registry.py +155 -8
- codex_autorunner/api.py +25 -0
- codex_autorunner/bootstrap.py +22 -37
- codex_autorunner/cli.py +5 -1156
- codex_autorunner/codex_cli.py +20 -84
- codex_autorunner/core/__init__.py +4 -0
- codex_autorunner/core/about_car.py +49 -32
- codex_autorunner/core/adapter_utils.py +21 -0
- codex_autorunner/core/app_server_ids.py +59 -0
- codex_autorunner/core/app_server_logging.py +7 -3
- codex_autorunner/core/app_server_prompts.py +27 -260
- codex_autorunner/core/app_server_threads.py +26 -28
- codex_autorunner/core/app_server_utils.py +165 -0
- codex_autorunner/core/archive.py +349 -0
- codex_autorunner/core/codex_runner.py +12 -2
- codex_autorunner/core/config.py +587 -103
- codex_autorunner/core/docs.py +10 -2
- codex_autorunner/core/drafts.py +136 -0
- codex_autorunner/core/engine.py +1531 -866
- codex_autorunner/core/exceptions.py +4 -0
- codex_autorunner/core/flows/__init__.py +25 -0
- codex_autorunner/core/flows/controller.py +202 -0
- codex_autorunner/core/flows/definition.py +82 -0
- codex_autorunner/core/flows/models.py +88 -0
- codex_autorunner/core/flows/reasons.py +52 -0
- codex_autorunner/core/flows/reconciler.py +131 -0
- codex_autorunner/core/flows/runtime.py +382 -0
- codex_autorunner/core/flows/store.py +568 -0
- codex_autorunner/core/flows/transition.py +138 -0
- codex_autorunner/core/flows/ux_helpers.py +257 -0
- codex_autorunner/core/flows/worker_process.py +242 -0
- codex_autorunner/core/git_utils.py +62 -0
- codex_autorunner/core/hub.py +136 -16
- codex_autorunner/core/locks.py +4 -0
- codex_autorunner/core/notifications.py +14 -2
- codex_autorunner/core/ports/__init__.py +28 -0
- codex_autorunner/core/ports/agent_backend.py +150 -0
- codex_autorunner/core/ports/backend_orchestrator.py +41 -0
- codex_autorunner/core/ports/run_event.py +91 -0
- codex_autorunner/core/prompt.py +15 -7
- codex_autorunner/core/redaction.py +29 -0
- codex_autorunner/core/review_context.py +5 -8
- codex_autorunner/core/run_index.py +6 -0
- codex_autorunner/core/runner_process.py +5 -2
- codex_autorunner/core/state.py +0 -88
- codex_autorunner/core/state_roots.py +57 -0
- codex_autorunner/core/supervisor_protocol.py +15 -0
- codex_autorunner/core/supervisor_utils.py +67 -0
- codex_autorunner/core/text_delta_coalescer.py +54 -0
- codex_autorunner/core/ticket_linter_cli.py +201 -0
- codex_autorunner/core/ticket_manager_cli.py +432 -0
- codex_autorunner/core/update.py +24 -16
- codex_autorunner/core/update_paths.py +28 -0
- codex_autorunner/core/update_runner.py +2 -0
- codex_autorunner/core/usage.py +164 -12
- codex_autorunner/core/utils.py +120 -11
- codex_autorunner/discovery.py +2 -4
- codex_autorunner/flows/review/__init__.py +17 -0
- codex_autorunner/{core/review.py → flows/review/service.py} +15 -10
- codex_autorunner/flows/ticket_flow/__init__.py +3 -0
- codex_autorunner/flows/ticket_flow/definition.py +98 -0
- codex_autorunner/integrations/agents/__init__.py +17 -0
- codex_autorunner/integrations/agents/backend_orchestrator.py +284 -0
- codex_autorunner/integrations/agents/codex_adapter.py +90 -0
- codex_autorunner/integrations/agents/codex_backend.py +448 -0
- codex_autorunner/integrations/agents/opencode_adapter.py +108 -0
- codex_autorunner/integrations/agents/opencode_backend.py +598 -0
- codex_autorunner/integrations/agents/runner.py +91 -0
- codex_autorunner/integrations/agents/wiring.py +271 -0
- codex_autorunner/integrations/app_server/client.py +583 -152
- 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/app_server/supervisor.py +59 -33
- codex_autorunner/integrations/telegram/adapter.py +204 -165
- codex_autorunner/integrations/telegram/api_schemas.py +120 -0
- codex_autorunner/integrations/telegram/config.py +221 -0
- codex_autorunner/integrations/telegram/constants.py +17 -2
- codex_autorunner/integrations/telegram/dispatch.py +17 -0
- codex_autorunner/integrations/telegram/doctor.py +47 -0
- codex_autorunner/integrations/telegram/handlers/callbacks.py +7 -4
- codex_autorunner/integrations/telegram/handlers/commands/__init__.py +2 -0
- codex_autorunner/integrations/telegram/handlers/commands/execution.py +53 -57
- codex_autorunner/integrations/telegram/handlers/commands/files.py +2 -6
- codex_autorunner/integrations/telegram/handlers/commands/flows.py +1364 -0
- codex_autorunner/integrations/telegram/handlers/commands/formatting.py +1 -1
- codex_autorunner/integrations/telegram/handlers/commands/github.py +41 -582
- codex_autorunner/integrations/telegram/handlers/commands/workspace.py +8 -8
- codex_autorunner/integrations/telegram/handlers/commands_runtime.py +137 -478
- codex_autorunner/integrations/telegram/handlers/commands_spec.py +17 -4
- codex_autorunner/integrations/telegram/handlers/messages.py +121 -9
- codex_autorunner/integrations/telegram/handlers/selections.py +61 -1
- codex_autorunner/integrations/telegram/helpers.py +111 -16
- codex_autorunner/integrations/telegram/outbox.py +208 -37
- codex_autorunner/integrations/telegram/progress_stream.py +3 -10
- codex_autorunner/integrations/telegram/service.py +221 -42
- codex_autorunner/integrations/telegram/state.py +100 -2
- codex_autorunner/integrations/telegram/ticket_flow_bridge.py +611 -0
- codex_autorunner/integrations/telegram/transport.py +39 -4
- codex_autorunner/integrations/telegram/trigger_mode.py +53 -0
- codex_autorunner/manifest.py +2 -0
- codex_autorunner/plugin_api.py +22 -0
- codex_autorunner/routes/__init__.py +37 -67
- codex_autorunner/routes/agents.py +2 -137
- codex_autorunner/routes/analytics.py +3 -0
- codex_autorunner/routes/app_server.py +2 -131
- codex_autorunner/routes/base.py +2 -624
- codex_autorunner/routes/file_chat.py +7 -0
- codex_autorunner/routes/flows.py +7 -0
- codex_autorunner/routes/messages.py +7 -0
- 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 -188
- codex_autorunner/routes/usage.py +3 -0
- codex_autorunner/routes/voice.py +2 -119
- codex_autorunner/routes/workspace.py +3 -0
- codex_autorunner/server.py +3 -2
- codex_autorunner/static/agentControls.js +41 -11
- codex_autorunner/static/agentEvents.js +248 -0
- codex_autorunner/static/app.js +35 -24
- codex_autorunner/static/archive.js +826 -0
- codex_autorunner/static/archiveApi.js +37 -0
- codex_autorunner/static/autoRefresh.js +36 -8
- codex_autorunner/static/bootstrap.js +1 -0
- codex_autorunner/static/bus.js +1 -0
- codex_autorunner/static/cache.js +1 -0
- codex_autorunner/static/constants.js +20 -4
- codex_autorunner/static/dashboard.js +344 -325
- codex_autorunner/static/diffRenderer.js +37 -0
- codex_autorunner/static/docChatCore.js +324 -0
- codex_autorunner/static/docChatStorage.js +65 -0
- codex_autorunner/static/docChatVoice.js +65 -0
- codex_autorunner/static/docEditor.js +133 -0
- codex_autorunner/static/env.js +1 -0
- codex_autorunner/static/eventSummarizer.js +166 -0
- codex_autorunner/static/fileChat.js +182 -0
- codex_autorunner/static/health.js +155 -0
- codex_autorunner/static/hub.js +126 -185
- codex_autorunner/static/index.html +839 -863
- codex_autorunner/static/liveUpdates.js +1 -0
- codex_autorunner/static/loader.js +1 -0
- codex_autorunner/static/messages.js +873 -0
- codex_autorunner/static/mobileCompact.js +2 -1
- codex_autorunner/static/preserve.js +17 -0
- codex_autorunner/static/settings.js +149 -217
- codex_autorunner/static/smartRefresh.js +52 -0
- codex_autorunner/static/styles.css +8850 -3876
- codex_autorunner/static/tabs.js +175 -11
- codex_autorunner/static/terminal.js +32 -0
- codex_autorunner/static/terminalManager.js +34 -59
- codex_autorunner/static/ticketChatActions.js +333 -0
- codex_autorunner/static/ticketChatEvents.js +16 -0
- codex_autorunner/static/ticketChatStorage.js +16 -0
- codex_autorunner/static/ticketChatStream.js +264 -0
- codex_autorunner/static/ticketEditor.js +844 -0
- codex_autorunner/static/ticketVoice.js +9 -0
- codex_autorunner/static/tickets.js +1988 -0
- codex_autorunner/static/utils.js +43 -3
- codex_autorunner/static/voice.js +1 -0
- codex_autorunner/static/workspace.js +765 -0
- codex_autorunner/static/workspaceApi.js +53 -0
- codex_autorunner/static/workspaceFileBrowser.js +504 -0
- codex_autorunner/surfaces/__init__.py +5 -0
- codex_autorunner/surfaces/cli/__init__.py +6 -0
- codex_autorunner/surfaces/cli/cli.py +1224 -0
- codex_autorunner/surfaces/cli/codex_cli.py +20 -0
- codex_autorunner/surfaces/telegram/__init__.py +3 -0
- codex_autorunner/surfaces/web/__init__.py +1 -0
- codex_autorunner/surfaces/web/app.py +2019 -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 +78 -0
- codex_autorunner/surfaces/web/routes/agents.py +138 -0
- codex_autorunner/surfaces/web/routes/analytics.py +277 -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 +836 -0
- codex_autorunner/surfaces/web/routes/flows.py +1164 -0
- codex_autorunner/surfaces/web/routes/messages.py +459 -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 +280 -0
- codex_autorunner/surfaces/web/routes/system.py +196 -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 +417 -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 +27 -0
- codex_autorunner/tickets/agent_pool.py +399 -0
- codex_autorunner/tickets/files.py +89 -0
- codex_autorunner/tickets/frontmatter.py +55 -0
- codex_autorunner/tickets/lint.py +102 -0
- codex_autorunner/tickets/models.py +97 -0
- codex_autorunner/tickets/outbox.py +244 -0
- codex_autorunner/tickets/replies.py +179 -0
- codex_autorunner/tickets/runner.py +881 -0
- codex_autorunner/tickets/spec_ingest.py +77 -0
- codex_autorunner/web/__init__.py +5 -1
- codex_autorunner/web/app.py +2 -1771
- codex_autorunner/web/hub_jobs.py +2 -191
- codex_autorunner/web/middleware.py +2 -587
- codex_autorunner/web/pty_session.py +2 -369
- codex_autorunner/web/runner_manager.py +2 -24
- codex_autorunner/web/schemas.py +2 -396
- codex_autorunner/web/static_assets.py +4 -484
- codex_autorunner/web/static_refresh.py +2 -85
- codex_autorunner/web/terminal_sessions.py +2 -77
- codex_autorunner/workspace/__init__.py +40 -0
- codex_autorunner/workspace/paths.py +335 -0
- codex_autorunner-1.1.0.dist-info/METADATA +154 -0
- codex_autorunner-1.1.0.dist-info/RECORD +308 -0
- {codex_autorunner-0.1.2.dist-info → codex_autorunner-1.1.0.dist-info}/WHEEL +1 -1
- codex_autorunner/agents/execution/policy.py +0 -292
- codex_autorunner/agents/factory.py +0 -52
- codex_autorunner/agents/orchestrator.py +0 -358
- codex_autorunner/core/doc_chat.py +0 -1446
- codex_autorunner/core/snapshot.py +0 -580
- codex_autorunner/integrations/github/chatops.py +0 -268
- codex_autorunner/integrations/github/pr_flow.py +0 -1314
- codex_autorunner/routes/docs.py +0 -381
- codex_autorunner/routes/github.py +0 -327
- codex_autorunner/routes/runs.py +0 -250
- codex_autorunner/spec_ingest.py +0 -812
- codex_autorunner/static/docChatActions.js +0 -287
- codex_autorunner/static/docChatEvents.js +0 -300
- codex_autorunner/static/docChatRender.js +0 -205
- codex_autorunner/static/docChatStream.js +0 -361
- codex_autorunner/static/docs.js +0 -20
- codex_autorunner/static/docsClipboard.js +0 -69
- codex_autorunner/static/docsCrud.js +0 -257
- codex_autorunner/static/docsDocUpdates.js +0 -62
- codex_autorunner/static/docsDrafts.js +0 -16
- codex_autorunner/static/docsElements.js +0 -69
- codex_autorunner/static/docsInit.js +0 -285
- codex_autorunner/static/docsParse.js +0 -160
- codex_autorunner/static/docsSnapshot.js +0 -87
- codex_autorunner/static/docsSpecIngest.js +0 -263
- codex_autorunner/static/docsState.js +0 -127
- codex_autorunner/static/docsThreadRegistry.js +0 -44
- codex_autorunner/static/docsUi.js +0 -153
- codex_autorunner/static/docsVoice.js +0 -56
- codex_autorunner/static/github.js +0 -504
- codex_autorunner/static/logs.js +0 -678
- codex_autorunner/static/review.js +0 -157
- codex_autorunner/static/runs.js +0 -418
- codex_autorunner/static/snapshot.js +0 -124
- codex_autorunner/static/state.js +0 -94
- codex_autorunner/static/todoPreview.js +0 -27
- codex_autorunner/workspace.py +0 -16
- codex_autorunner-0.1.2.dist-info/METADATA +0 -249
- codex_autorunner-0.1.2.dist-info/RECORD +0 -222
- /codex_autorunner/{routes → surfaces/web/routes}/terminal_images.py +0 -0
- {codex_autorunner-0.1.2.dist-info → codex_autorunner-1.1.0.dist-info}/entry_points.txt +0 -0
- {codex_autorunner-0.1.2.dist-info → codex_autorunner-1.1.0.dist-info}/licenses/LICENSE +0 -0
- {codex_autorunner-0.1.2.dist-info → codex_autorunner-1.1.0.dist-info}/top_level.txt +0 -0
|
@@ -0,0 +1,844 @@
|
|
|
1
|
+
// GENERATED FILE - do not edit directly. Source: static_src/
|
|
2
|
+
/**
|
|
3
|
+
* Ticket Editor Modal - handles creating, editing, and deleting tickets
|
|
4
|
+
*/
|
|
5
|
+
import { api, flash, updateUrlParams, splitMarkdownFrontmatter } from "./utils.js";
|
|
6
|
+
import { publish } from "./bus.js";
|
|
7
|
+
import { clearTicketChatHistory } from "./ticketChatStorage.js";
|
|
8
|
+
import { setTicketIndex, sendTicketChat, cancelTicketChat, applyTicketPatch, discardTicketPatch, loadTicketPending, renderTicketChat, resetTicketChatState, ticketChatState, } from "./ticketChatActions.js";
|
|
9
|
+
import { initAgentControls } from "./agentControls.js";
|
|
10
|
+
import { initTicketVoice } from "./ticketVoice.js";
|
|
11
|
+
import { initTicketChatEvents, renderTicketEvents, renderTicketMessages } from "./ticketChatEvents.js";
|
|
12
|
+
import { DocEditor } from "./docEditor.js";
|
|
13
|
+
const DEFAULT_FRONTMATTER = {
|
|
14
|
+
agent: "codex",
|
|
15
|
+
done: false,
|
|
16
|
+
title: "",
|
|
17
|
+
model: "",
|
|
18
|
+
reasoning: "",
|
|
19
|
+
};
|
|
20
|
+
const state = {
|
|
21
|
+
isOpen: false,
|
|
22
|
+
mode: "create",
|
|
23
|
+
ticketIndex: null,
|
|
24
|
+
originalBody: "",
|
|
25
|
+
originalFrontmatter: { ...DEFAULT_FRONTMATTER },
|
|
26
|
+
undoStack: [],
|
|
27
|
+
lastSavedBody: "",
|
|
28
|
+
lastSavedFrontmatter: { ...DEFAULT_FRONTMATTER },
|
|
29
|
+
};
|
|
30
|
+
// Autosave debounce timer
|
|
31
|
+
const AUTOSAVE_DELAY_MS = 1000;
|
|
32
|
+
let ticketDocEditor = null;
|
|
33
|
+
let ticketNavCache = [];
|
|
34
|
+
function isTypingTarget(target) {
|
|
35
|
+
if (!(target instanceof HTMLElement))
|
|
36
|
+
return false;
|
|
37
|
+
const tag = target.tagName;
|
|
38
|
+
return tag === "INPUT" || tag === "TEXTAREA" || tag === "SELECT" || target.isContentEditable;
|
|
39
|
+
}
|
|
40
|
+
async function fetchTicketList() {
|
|
41
|
+
const data = (await api("/api/flows/ticket_flow/tickets"));
|
|
42
|
+
const list = (data?.tickets || []).filter((ticket) => typeof ticket.index === "number");
|
|
43
|
+
list.sort((a, b) => (a.index ?? 0) - (b.index ?? 0));
|
|
44
|
+
return list;
|
|
45
|
+
}
|
|
46
|
+
async function updateTicketNavButtons() {
|
|
47
|
+
const { prevBtn, nextBtn } = els();
|
|
48
|
+
if (!prevBtn || !nextBtn)
|
|
49
|
+
return;
|
|
50
|
+
if (state.mode !== "edit" || state.ticketIndex == null) {
|
|
51
|
+
prevBtn.disabled = true;
|
|
52
|
+
nextBtn.disabled = true;
|
|
53
|
+
return;
|
|
54
|
+
}
|
|
55
|
+
try {
|
|
56
|
+
const list = await fetchTicketList();
|
|
57
|
+
ticketNavCache = list;
|
|
58
|
+
}
|
|
59
|
+
catch {
|
|
60
|
+
// If fetch fails, fall back to the last known list.
|
|
61
|
+
}
|
|
62
|
+
const list = ticketNavCache;
|
|
63
|
+
if (!list.length) {
|
|
64
|
+
prevBtn.disabled = true;
|
|
65
|
+
nextBtn.disabled = true;
|
|
66
|
+
return;
|
|
67
|
+
}
|
|
68
|
+
const idx = list.findIndex((ticket) => ticket.index === state.ticketIndex);
|
|
69
|
+
const hasPrev = idx > 0;
|
|
70
|
+
const hasNext = idx >= 0 && idx < list.length - 1;
|
|
71
|
+
prevBtn.disabled = !hasPrev;
|
|
72
|
+
nextBtn.disabled = !hasNext;
|
|
73
|
+
}
|
|
74
|
+
async function navigateTicket(delta) {
|
|
75
|
+
if (state.mode !== "edit" || state.ticketIndex == null)
|
|
76
|
+
return;
|
|
77
|
+
await performAutosave();
|
|
78
|
+
let list = ticketNavCache;
|
|
79
|
+
if (!list.length) {
|
|
80
|
+
try {
|
|
81
|
+
list = await fetchTicketList();
|
|
82
|
+
ticketNavCache = list;
|
|
83
|
+
}
|
|
84
|
+
catch {
|
|
85
|
+
return;
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
const idx = list.findIndex((ticket) => ticket.index === state.ticketIndex);
|
|
89
|
+
const target = idx >= 0 ? list[idx + delta] : null;
|
|
90
|
+
if (target && target.index != null) {
|
|
91
|
+
openTicketEditor(target);
|
|
92
|
+
}
|
|
93
|
+
void updateTicketNavButtons();
|
|
94
|
+
}
|
|
95
|
+
function els() {
|
|
96
|
+
return {
|
|
97
|
+
modal: document.getElementById("ticket-editor-modal"),
|
|
98
|
+
content: document.getElementById("ticket-editor-content"),
|
|
99
|
+
error: document.getElementById("ticket-editor-error"),
|
|
100
|
+
deleteBtn: document.getElementById("ticket-editor-delete"),
|
|
101
|
+
closeBtn: document.getElementById("ticket-editor-close"),
|
|
102
|
+
newBtn: document.getElementById("ticket-new-btn"),
|
|
103
|
+
insertCheckboxBtn: document.getElementById("ticket-insert-checkbox"),
|
|
104
|
+
undoBtn: document.getElementById("ticket-undo-btn"),
|
|
105
|
+
prevBtn: document.getElementById("ticket-nav-prev"),
|
|
106
|
+
nextBtn: document.getElementById("ticket-nav-next"),
|
|
107
|
+
autosaveStatus: document.getElementById("ticket-autosave-status"),
|
|
108
|
+
// Frontmatter form elements
|
|
109
|
+
fmAgent: document.getElementById("ticket-fm-agent"),
|
|
110
|
+
fmModel: document.getElementById("ticket-fm-model"),
|
|
111
|
+
fmReasoning: document.getElementById("ticket-fm-reasoning"),
|
|
112
|
+
fmDone: document.getElementById("ticket-fm-done"),
|
|
113
|
+
fmTitle: document.getElementById("ticket-fm-title"),
|
|
114
|
+
// Chat elements
|
|
115
|
+
chatInput: document.getElementById("ticket-chat-input"),
|
|
116
|
+
chatSendBtn: document.getElementById("ticket-chat-send"),
|
|
117
|
+
chatVoiceBtn: document.getElementById("ticket-chat-voice"),
|
|
118
|
+
chatCancelBtn: document.getElementById("ticket-chat-cancel"),
|
|
119
|
+
chatStatus: document.getElementById("ticket-chat-status"),
|
|
120
|
+
patchApplyBtn: document.getElementById("ticket-patch-apply"),
|
|
121
|
+
patchDiscardBtn: document.getElementById("ticket-patch-discard"),
|
|
122
|
+
// Agent control selects (for chat)
|
|
123
|
+
agentSelect: document.getElementById("ticket-chat-agent-select"),
|
|
124
|
+
modelSelect: document.getElementById("ticket-chat-model-select"),
|
|
125
|
+
reasoningSelect: document.getElementById("ticket-chat-reasoning-select"),
|
|
126
|
+
};
|
|
127
|
+
}
|
|
128
|
+
/**
|
|
129
|
+
* Insert a checkbox at the current cursor position
|
|
130
|
+
*/
|
|
131
|
+
function insertCheckbox() {
|
|
132
|
+
const { content } = els();
|
|
133
|
+
if (!content)
|
|
134
|
+
return;
|
|
135
|
+
const pos = content.selectionStart;
|
|
136
|
+
const text = content.value;
|
|
137
|
+
const insert = "- [ ] ";
|
|
138
|
+
// If at start of line or after newline, insert directly
|
|
139
|
+
// Otherwise, insert on a new line
|
|
140
|
+
const needsNewline = pos > 0 && text[pos - 1] !== "\n";
|
|
141
|
+
const toInsert = needsNewline ? "\n" + insert : insert;
|
|
142
|
+
content.value = text.slice(0, pos) + toInsert + text.slice(pos);
|
|
143
|
+
const newPos = pos + toInsert.length;
|
|
144
|
+
content.setSelectionRange(newPos, newPos);
|
|
145
|
+
content.focus();
|
|
146
|
+
}
|
|
147
|
+
function showError(message) {
|
|
148
|
+
const { error } = els();
|
|
149
|
+
if (!error)
|
|
150
|
+
return;
|
|
151
|
+
error.textContent = message;
|
|
152
|
+
error.classList.remove("hidden");
|
|
153
|
+
}
|
|
154
|
+
function hideError() {
|
|
155
|
+
const { error } = els();
|
|
156
|
+
if (!error)
|
|
157
|
+
return;
|
|
158
|
+
error.textContent = "";
|
|
159
|
+
error.classList.add("hidden");
|
|
160
|
+
}
|
|
161
|
+
function setButtonsLoading(loading) {
|
|
162
|
+
const { deleteBtn, closeBtn, undoBtn } = els();
|
|
163
|
+
[deleteBtn, closeBtn, undoBtn].forEach((btn) => {
|
|
164
|
+
if (btn)
|
|
165
|
+
btn.disabled = loading;
|
|
166
|
+
});
|
|
167
|
+
}
|
|
168
|
+
/**
|
|
169
|
+
* Update the autosave status indicator
|
|
170
|
+
*/
|
|
171
|
+
function setAutosaveStatus(status) {
|
|
172
|
+
const { autosaveStatus } = els();
|
|
173
|
+
if (!autosaveStatus)
|
|
174
|
+
return;
|
|
175
|
+
switch (status) {
|
|
176
|
+
case "saving":
|
|
177
|
+
autosaveStatus.textContent = "Saving…";
|
|
178
|
+
autosaveStatus.classList.remove("error");
|
|
179
|
+
break;
|
|
180
|
+
case "saved":
|
|
181
|
+
autosaveStatus.textContent = "Saved";
|
|
182
|
+
autosaveStatus.classList.remove("error");
|
|
183
|
+
// Clear after a short delay
|
|
184
|
+
setTimeout(() => {
|
|
185
|
+
if (autosaveStatus.textContent === "Saved") {
|
|
186
|
+
autosaveStatus.textContent = "";
|
|
187
|
+
}
|
|
188
|
+
}, 2000);
|
|
189
|
+
break;
|
|
190
|
+
case "error":
|
|
191
|
+
autosaveStatus.textContent = "Save failed";
|
|
192
|
+
autosaveStatus.classList.add("error");
|
|
193
|
+
break;
|
|
194
|
+
default:
|
|
195
|
+
autosaveStatus.textContent = "";
|
|
196
|
+
autosaveStatus.classList.remove("error");
|
|
197
|
+
}
|
|
198
|
+
}
|
|
199
|
+
/**
|
|
200
|
+
* Push current state to undo stack
|
|
201
|
+
*/
|
|
202
|
+
function pushUndoState() {
|
|
203
|
+
const { content, undoBtn } = els();
|
|
204
|
+
const fm = getFrontmatterFromForm();
|
|
205
|
+
const body = content?.value || "";
|
|
206
|
+
// Don't push if same as last undo state
|
|
207
|
+
const last = state.undoStack[state.undoStack.length - 1];
|
|
208
|
+
if (last && last.body === body &&
|
|
209
|
+
last.frontmatter.agent === fm.agent &&
|
|
210
|
+
last.frontmatter.done === fm.done &&
|
|
211
|
+
last.frontmatter.title === fm.title &&
|
|
212
|
+
last.frontmatter.model === fm.model &&
|
|
213
|
+
last.frontmatter.reasoning === fm.reasoning) {
|
|
214
|
+
return;
|
|
215
|
+
}
|
|
216
|
+
state.undoStack.push({ body, frontmatter: { ...fm } });
|
|
217
|
+
// Limit stack size
|
|
218
|
+
if (state.undoStack.length > 50) {
|
|
219
|
+
state.undoStack.shift();
|
|
220
|
+
}
|
|
221
|
+
// Enable undo button
|
|
222
|
+
if (undoBtn)
|
|
223
|
+
undoBtn.disabled = state.undoStack.length <= 1;
|
|
224
|
+
}
|
|
225
|
+
/**
|
|
226
|
+
* Undo to previous state
|
|
227
|
+
*/
|
|
228
|
+
function undoChange() {
|
|
229
|
+
const { content, undoBtn } = els();
|
|
230
|
+
if (!content || state.undoStack.length <= 1)
|
|
231
|
+
return;
|
|
232
|
+
// Pop current state
|
|
233
|
+
state.undoStack.pop();
|
|
234
|
+
// Get previous state
|
|
235
|
+
const prev = state.undoStack[state.undoStack.length - 1];
|
|
236
|
+
if (!prev)
|
|
237
|
+
return;
|
|
238
|
+
// Restore state
|
|
239
|
+
content.value = prev.body;
|
|
240
|
+
setFrontmatterForm(prev.frontmatter);
|
|
241
|
+
// Trigger autosave for the restored state
|
|
242
|
+
scheduleAutosave();
|
|
243
|
+
// Update undo button
|
|
244
|
+
if (undoBtn)
|
|
245
|
+
undoBtn.disabled = state.undoStack.length <= 1;
|
|
246
|
+
}
|
|
247
|
+
/**
|
|
248
|
+
* Update undo button state
|
|
249
|
+
*/
|
|
250
|
+
function updateUndoButton() {
|
|
251
|
+
const { undoBtn } = els();
|
|
252
|
+
if (undoBtn) {
|
|
253
|
+
undoBtn.disabled = state.undoStack.length <= 1;
|
|
254
|
+
}
|
|
255
|
+
}
|
|
256
|
+
/**
|
|
257
|
+
* Get current frontmatter values from form fields
|
|
258
|
+
*/
|
|
259
|
+
function getFrontmatterFromForm() {
|
|
260
|
+
const { fmAgent, fmModel, fmReasoning, fmDone, fmTitle } = els();
|
|
261
|
+
return {
|
|
262
|
+
agent: fmAgent?.value || "codex",
|
|
263
|
+
done: fmDone?.checked || false,
|
|
264
|
+
title: fmTitle?.value || "",
|
|
265
|
+
model: fmModel?.value || "",
|
|
266
|
+
reasoning: fmReasoning?.value || "",
|
|
267
|
+
};
|
|
268
|
+
}
|
|
269
|
+
/**
|
|
270
|
+
* Set frontmatter form fields from values
|
|
271
|
+
*/
|
|
272
|
+
function setFrontmatterForm(fm) {
|
|
273
|
+
const { fmAgent, fmModel, fmReasoning, fmDone, fmTitle } = els();
|
|
274
|
+
if (fmAgent)
|
|
275
|
+
fmAgent.value = fm.agent;
|
|
276
|
+
if (fmModel)
|
|
277
|
+
fmModel.value = fm.model;
|
|
278
|
+
if (fmReasoning)
|
|
279
|
+
fmReasoning.value = fm.reasoning;
|
|
280
|
+
if (fmDone)
|
|
281
|
+
fmDone.checked = fm.done;
|
|
282
|
+
if (fmTitle)
|
|
283
|
+
fmTitle.value = fm.title;
|
|
284
|
+
}
|
|
285
|
+
/**
|
|
286
|
+
* Extract frontmatter state from ticket data
|
|
287
|
+
*/
|
|
288
|
+
function extractFrontmatter(ticket) {
|
|
289
|
+
const fm = ticket.frontmatter || {};
|
|
290
|
+
return {
|
|
291
|
+
agent: fm.agent || "codex",
|
|
292
|
+
done: Boolean(fm.done),
|
|
293
|
+
title: fm.title || "",
|
|
294
|
+
model: fm.model || "",
|
|
295
|
+
reasoning: fm.reasoning || "",
|
|
296
|
+
};
|
|
297
|
+
}
|
|
298
|
+
/**
|
|
299
|
+
* Build full markdown content from frontmatter form + body textarea
|
|
300
|
+
*/
|
|
301
|
+
function buildTicketContent() {
|
|
302
|
+
const { content } = els();
|
|
303
|
+
const fm = getFrontmatterFromForm();
|
|
304
|
+
const body = content?.value || "";
|
|
305
|
+
// Reconstruct frontmatter YAML
|
|
306
|
+
const lines = ["---"];
|
|
307
|
+
lines.push(`agent: ${fm.agent}`);
|
|
308
|
+
lines.push(`done: ${fm.done}`);
|
|
309
|
+
if (fm.title)
|
|
310
|
+
lines.push(`title: ${fm.title}`);
|
|
311
|
+
if (fm.model)
|
|
312
|
+
lines.push(`model: ${fm.model}`);
|
|
313
|
+
if (fm.reasoning)
|
|
314
|
+
lines.push(`reasoning: ${fm.reasoning}`);
|
|
315
|
+
lines.push("---");
|
|
316
|
+
lines.push("");
|
|
317
|
+
lines.push(body);
|
|
318
|
+
return lines.join("\n");
|
|
319
|
+
}
|
|
320
|
+
// Model catalog cache for frontmatter selects
|
|
321
|
+
const fmModelCatalogs = new Map();
|
|
322
|
+
/**
|
|
323
|
+
* Load and populate the frontmatter model/reasoning selects based on the selected agent
|
|
324
|
+
*/
|
|
325
|
+
async function refreshFmModelOptions(agent, preserveSelection = false) {
|
|
326
|
+
const { fmModel, fmReasoning } = els();
|
|
327
|
+
if (!fmModel || !fmReasoning)
|
|
328
|
+
return;
|
|
329
|
+
const currentModel = preserveSelection ? fmModel.value : "";
|
|
330
|
+
const currentReasoning = preserveSelection ? fmReasoning.value : "";
|
|
331
|
+
// Fetch catalog if not cached
|
|
332
|
+
if (!fmModelCatalogs.has(agent)) {
|
|
333
|
+
try {
|
|
334
|
+
const data = await api(`/api/agents/${encodeURIComponent(agent)}/models`, { method: "GET" });
|
|
335
|
+
const models = Array.isArray(data?.models) ? data.models : [];
|
|
336
|
+
const catalog = {
|
|
337
|
+
default_model: data?.default_model || "",
|
|
338
|
+
models,
|
|
339
|
+
};
|
|
340
|
+
fmModelCatalogs.set(agent, catalog);
|
|
341
|
+
}
|
|
342
|
+
catch {
|
|
343
|
+
fmModelCatalogs.set(agent, null);
|
|
344
|
+
}
|
|
345
|
+
}
|
|
346
|
+
const catalog = fmModelCatalogs.get(agent);
|
|
347
|
+
// Populate model select
|
|
348
|
+
fmModel.innerHTML = "";
|
|
349
|
+
const defaultOption = document.createElement("option");
|
|
350
|
+
defaultOption.value = "";
|
|
351
|
+
defaultOption.textContent = "(default)";
|
|
352
|
+
fmModel.appendChild(defaultOption);
|
|
353
|
+
if (catalog?.models?.length) {
|
|
354
|
+
fmModel.disabled = false;
|
|
355
|
+
for (const m of catalog.models) {
|
|
356
|
+
const opt = document.createElement("option");
|
|
357
|
+
opt.value = m.id;
|
|
358
|
+
opt.textContent = m.display_name && m.display_name !== m.id ? `${m.display_name} (${m.id})` : m.id;
|
|
359
|
+
fmModel.appendChild(opt);
|
|
360
|
+
}
|
|
361
|
+
// Restore selection if valid
|
|
362
|
+
if (currentModel && catalog.models.some((m) => m.id === currentModel)) {
|
|
363
|
+
fmModel.value = currentModel;
|
|
364
|
+
}
|
|
365
|
+
}
|
|
366
|
+
else {
|
|
367
|
+
fmModel.disabled = true;
|
|
368
|
+
}
|
|
369
|
+
// Populate reasoning select based on selected model
|
|
370
|
+
refreshFmReasoningOptions(catalog, fmModel.value, currentReasoning);
|
|
371
|
+
}
|
|
372
|
+
/**
|
|
373
|
+
* Populate reasoning options based on selected model
|
|
374
|
+
*/
|
|
375
|
+
function refreshFmReasoningOptions(catalog, modelId, currentReasoning = "") {
|
|
376
|
+
const { fmReasoning } = els();
|
|
377
|
+
if (!fmReasoning)
|
|
378
|
+
return;
|
|
379
|
+
fmReasoning.innerHTML = "";
|
|
380
|
+
const defaultOption = document.createElement("option");
|
|
381
|
+
defaultOption.value = "";
|
|
382
|
+
defaultOption.textContent = "(default)";
|
|
383
|
+
fmReasoning.appendChild(defaultOption);
|
|
384
|
+
const model = catalog?.models?.find((m) => m.id === modelId);
|
|
385
|
+
if (model?.supports_reasoning && model.reasoning_options?.length) {
|
|
386
|
+
fmReasoning.disabled = false;
|
|
387
|
+
for (const r of model.reasoning_options) {
|
|
388
|
+
const opt = document.createElement("option");
|
|
389
|
+
opt.value = r;
|
|
390
|
+
opt.textContent = r;
|
|
391
|
+
fmReasoning.appendChild(opt);
|
|
392
|
+
}
|
|
393
|
+
// Restore selection if valid
|
|
394
|
+
if (currentReasoning && model.reasoning_options.includes(currentReasoning)) {
|
|
395
|
+
fmReasoning.value = currentReasoning;
|
|
396
|
+
}
|
|
397
|
+
}
|
|
398
|
+
else {
|
|
399
|
+
fmReasoning.disabled = true;
|
|
400
|
+
}
|
|
401
|
+
}
|
|
402
|
+
/**
|
|
403
|
+
* Check if there are unsaved changes (compared to last saved state)
|
|
404
|
+
*/
|
|
405
|
+
function hasUnsavedChanges() {
|
|
406
|
+
const { content } = els();
|
|
407
|
+
const currentFm = getFrontmatterFromForm();
|
|
408
|
+
const currentBody = content?.value || "";
|
|
409
|
+
return (currentBody !== state.lastSavedBody ||
|
|
410
|
+
currentFm.agent !== state.lastSavedFrontmatter.agent ||
|
|
411
|
+
currentFm.done !== state.lastSavedFrontmatter.done ||
|
|
412
|
+
currentFm.title !== state.lastSavedFrontmatter.title ||
|
|
413
|
+
currentFm.model !== state.lastSavedFrontmatter.model ||
|
|
414
|
+
currentFm.reasoning !== state.lastSavedFrontmatter.reasoning);
|
|
415
|
+
}
|
|
416
|
+
/**
|
|
417
|
+
* Schedule autosave with debounce
|
|
418
|
+
*/
|
|
419
|
+
function scheduleAutosave() {
|
|
420
|
+
// DocEditor handles debounced autosave; leave for compatibility
|
|
421
|
+
void ticketDocEditor?.save();
|
|
422
|
+
}
|
|
423
|
+
/**
|
|
424
|
+
* Perform autosave (silent save without closing modal)
|
|
425
|
+
*/
|
|
426
|
+
async function performAutosave() {
|
|
427
|
+
const { content } = els();
|
|
428
|
+
if (!content || !state.isOpen)
|
|
429
|
+
return;
|
|
430
|
+
// Don't autosave if no changes
|
|
431
|
+
if (!hasUnsavedChanges())
|
|
432
|
+
return;
|
|
433
|
+
const fm = getFrontmatterFromForm();
|
|
434
|
+
const fullContent = buildTicketContent();
|
|
435
|
+
// Validate required fields
|
|
436
|
+
if (!fm.agent)
|
|
437
|
+
return;
|
|
438
|
+
setAutosaveStatus("saving");
|
|
439
|
+
try {
|
|
440
|
+
if (state.mode === "create") {
|
|
441
|
+
// Create with form data
|
|
442
|
+
const createRes = await api("/api/flows/ticket_flow/tickets", {
|
|
443
|
+
method: "POST",
|
|
444
|
+
body: {
|
|
445
|
+
agent: fm.agent,
|
|
446
|
+
title: fm.title || undefined,
|
|
447
|
+
body: content.value,
|
|
448
|
+
},
|
|
449
|
+
});
|
|
450
|
+
if (createRes?.index != null) {
|
|
451
|
+
// Switch to edit mode now that ticket exists
|
|
452
|
+
state.mode = "edit";
|
|
453
|
+
state.ticketIndex = createRes.index;
|
|
454
|
+
// If done is true, update to set done flag
|
|
455
|
+
if (fm.done) {
|
|
456
|
+
await api(`/api/flows/ticket_flow/tickets/${createRes.index}`, {
|
|
457
|
+
method: "PUT",
|
|
458
|
+
body: { content: fullContent },
|
|
459
|
+
});
|
|
460
|
+
}
|
|
461
|
+
// Set up chat for this ticket
|
|
462
|
+
setTicketIndex(createRes.index);
|
|
463
|
+
}
|
|
464
|
+
}
|
|
465
|
+
else {
|
|
466
|
+
// Update existing
|
|
467
|
+
if (state.ticketIndex == null)
|
|
468
|
+
return;
|
|
469
|
+
await api(`/api/flows/ticket_flow/tickets/${state.ticketIndex}`, {
|
|
470
|
+
method: "PUT",
|
|
471
|
+
body: { content: fullContent },
|
|
472
|
+
});
|
|
473
|
+
}
|
|
474
|
+
// Update saved state
|
|
475
|
+
state.lastSavedBody = content.value;
|
|
476
|
+
state.lastSavedFrontmatter = { ...fm };
|
|
477
|
+
setAutosaveStatus("saved");
|
|
478
|
+
// Notify that tickets changed
|
|
479
|
+
publish("tickets:updated", {});
|
|
480
|
+
}
|
|
481
|
+
catch {
|
|
482
|
+
setAutosaveStatus("error");
|
|
483
|
+
}
|
|
484
|
+
}
|
|
485
|
+
/**
|
|
486
|
+
* Trigger change tracking and schedule autosave
|
|
487
|
+
*/
|
|
488
|
+
function onContentChange() {
|
|
489
|
+
pushUndoState();
|
|
490
|
+
scheduleAutosave();
|
|
491
|
+
}
|
|
492
|
+
function onFrontmatterChange() {
|
|
493
|
+
pushUndoState();
|
|
494
|
+
void ticketDocEditor?.save(true);
|
|
495
|
+
}
|
|
496
|
+
/**
|
|
497
|
+
* Open the ticket editor modal
|
|
498
|
+
* @param ticket - If provided, opens in edit mode; otherwise creates new ticket
|
|
499
|
+
*/
|
|
500
|
+
export function openTicketEditor(ticket) {
|
|
501
|
+
const { modal, content, deleteBtn, chatInput, fmTitle } = els();
|
|
502
|
+
if (!modal || !content)
|
|
503
|
+
return;
|
|
504
|
+
hideError();
|
|
505
|
+
setAutosaveStatus("");
|
|
506
|
+
if (ticket && ticket.index != null) {
|
|
507
|
+
// Edit mode
|
|
508
|
+
state.mode = "edit";
|
|
509
|
+
state.ticketIndex = ticket.index;
|
|
510
|
+
// Extract and set frontmatter
|
|
511
|
+
const fm = extractFrontmatter(ticket);
|
|
512
|
+
state.originalFrontmatter = { ...fm };
|
|
513
|
+
state.lastSavedFrontmatter = { ...fm };
|
|
514
|
+
setFrontmatterForm(fm);
|
|
515
|
+
// Load model/reasoning options for the agent, then restore selections
|
|
516
|
+
void refreshFmModelOptions(fm.agent, false).then(() => {
|
|
517
|
+
const { fmModel, fmReasoning } = els();
|
|
518
|
+
if (fmModel && fm.model)
|
|
519
|
+
fmModel.value = fm.model;
|
|
520
|
+
if (fmReasoning && fm.reasoning) {
|
|
521
|
+
// Refresh reasoning options based on selected model first
|
|
522
|
+
const catalog = fmModelCatalogs.get(fm.agent);
|
|
523
|
+
refreshFmReasoningOptions(catalog, fm.model, fm.reasoning);
|
|
524
|
+
}
|
|
525
|
+
});
|
|
526
|
+
// Set body (without frontmatter)
|
|
527
|
+
let body = ticket.body || "";
|
|
528
|
+
// If the body itself contains frontmatter, strip it if it's well-formed
|
|
529
|
+
const [fmYaml, strippedBody] = splitMarkdownFrontmatter(body);
|
|
530
|
+
if (fmYaml !== null) {
|
|
531
|
+
body = strippedBody.trimStart();
|
|
532
|
+
}
|
|
533
|
+
else if (body.startsWith("---")) {
|
|
534
|
+
// If it starts with --- but splitMarkdownFrontmatter returned null, it's malformed.
|
|
535
|
+
// We keep it in the body so the user can see/fix it.
|
|
536
|
+
flash("Malformed frontmatter detected in body", "error");
|
|
537
|
+
}
|
|
538
|
+
else {
|
|
539
|
+
// Ensure we don't accumulate whitespace from the backend's normalization
|
|
540
|
+
body = body.trimStart();
|
|
541
|
+
}
|
|
542
|
+
state.originalBody = body;
|
|
543
|
+
state.lastSavedBody = body;
|
|
544
|
+
content.value = body;
|
|
545
|
+
if (deleteBtn)
|
|
546
|
+
deleteBtn.classList.remove("hidden");
|
|
547
|
+
// Set up chat for this ticket
|
|
548
|
+
setTicketIndex(ticket.index);
|
|
549
|
+
// Load any pending draft
|
|
550
|
+
void loadTicketPending(ticket.index, true);
|
|
551
|
+
}
|
|
552
|
+
else {
|
|
553
|
+
// Create mode
|
|
554
|
+
state.mode = "create";
|
|
555
|
+
state.ticketIndex = null;
|
|
556
|
+
// Reset frontmatter to defaults
|
|
557
|
+
state.originalFrontmatter = { ...DEFAULT_FRONTMATTER };
|
|
558
|
+
state.lastSavedFrontmatter = { ...DEFAULT_FRONTMATTER };
|
|
559
|
+
setFrontmatterForm(DEFAULT_FRONTMATTER);
|
|
560
|
+
// Load model/reasoning options for the default agent
|
|
561
|
+
void refreshFmModelOptions(DEFAULT_FRONTMATTER.agent, false);
|
|
562
|
+
// Clear body
|
|
563
|
+
state.originalBody = "";
|
|
564
|
+
state.lastSavedBody = "";
|
|
565
|
+
content.value = "";
|
|
566
|
+
if (deleteBtn)
|
|
567
|
+
deleteBtn.classList.add("hidden");
|
|
568
|
+
// Clear chat state for new ticket
|
|
569
|
+
setTicketIndex(null);
|
|
570
|
+
}
|
|
571
|
+
// Initialize undo stack with current state
|
|
572
|
+
state.undoStack = [{ body: content.value, frontmatter: getFrontmatterFromForm() }];
|
|
573
|
+
updateUndoButton();
|
|
574
|
+
if (ticketDocEditor) {
|
|
575
|
+
ticketDocEditor.destroy();
|
|
576
|
+
}
|
|
577
|
+
ticketDocEditor = new DocEditor({
|
|
578
|
+
target: state.ticketIndex != null ? `ticket:${state.ticketIndex}` : "ticket:new",
|
|
579
|
+
textarea: content,
|
|
580
|
+
statusEl: els().autosaveStatus,
|
|
581
|
+
autoSaveDelay: AUTOSAVE_DELAY_MS,
|
|
582
|
+
onLoad: async () => content.value,
|
|
583
|
+
onSave: async () => {
|
|
584
|
+
await performAutosave();
|
|
585
|
+
},
|
|
586
|
+
});
|
|
587
|
+
// Clear chat input
|
|
588
|
+
if (chatInput)
|
|
589
|
+
chatInput.value = "";
|
|
590
|
+
renderTicketChat();
|
|
591
|
+
renderTicketEvents();
|
|
592
|
+
renderTicketMessages();
|
|
593
|
+
state.isOpen = true;
|
|
594
|
+
modal.classList.remove("hidden");
|
|
595
|
+
// Update URL with ticket index
|
|
596
|
+
if (ticket?.index != null) {
|
|
597
|
+
updateUrlParams({ ticket: ticket.index });
|
|
598
|
+
}
|
|
599
|
+
if (ticket?.path) {
|
|
600
|
+
publish("ticket-editor:opened", { path: ticket.path, index: ticket.index ?? null });
|
|
601
|
+
}
|
|
602
|
+
void updateTicketNavButtons();
|
|
603
|
+
// Focus on title field for new tickets
|
|
604
|
+
if (state.mode === "create" && fmTitle) {
|
|
605
|
+
fmTitle.focus();
|
|
606
|
+
}
|
|
607
|
+
}
|
|
608
|
+
/**
|
|
609
|
+
* Close the ticket editor modal (autosaves on close)
|
|
610
|
+
*/
|
|
611
|
+
export function closeTicketEditor() {
|
|
612
|
+
const { modal } = els();
|
|
613
|
+
if (!modal)
|
|
614
|
+
return;
|
|
615
|
+
// Autosave on close if there are changes
|
|
616
|
+
if (hasUnsavedChanges()) {
|
|
617
|
+
void performAutosave();
|
|
618
|
+
}
|
|
619
|
+
// Cancel any running chat
|
|
620
|
+
if (ticketChatState.status === "running") {
|
|
621
|
+
void cancelTicketChat();
|
|
622
|
+
}
|
|
623
|
+
state.isOpen = false;
|
|
624
|
+
state.ticketIndex = null;
|
|
625
|
+
state.originalBody = "";
|
|
626
|
+
state.originalFrontmatter = { ...DEFAULT_FRONTMATTER };
|
|
627
|
+
state.lastSavedBody = "";
|
|
628
|
+
state.lastSavedFrontmatter = { ...DEFAULT_FRONTMATTER };
|
|
629
|
+
state.undoStack = [];
|
|
630
|
+
modal.classList.add("hidden");
|
|
631
|
+
hideError();
|
|
632
|
+
ticketDocEditor?.destroy();
|
|
633
|
+
ticketDocEditor = null;
|
|
634
|
+
// Clear ticket from URL
|
|
635
|
+
updateUrlParams({ ticket: null });
|
|
636
|
+
void updateTicketNavButtons();
|
|
637
|
+
// Reset chat state
|
|
638
|
+
resetTicketChatState();
|
|
639
|
+
setTicketIndex(null);
|
|
640
|
+
// Notify that editor was closed (for selection state cleanup)
|
|
641
|
+
publish("ticket-editor:closed", {});
|
|
642
|
+
}
|
|
643
|
+
/**
|
|
644
|
+
* Save the current ticket (triggers immediate autosave)
|
|
645
|
+
*/
|
|
646
|
+
export async function saveTicket() {
|
|
647
|
+
await performAutosave();
|
|
648
|
+
}
|
|
649
|
+
/**
|
|
650
|
+
* Delete the current ticket (only available in edit mode)
|
|
651
|
+
*/
|
|
652
|
+
export async function deleteTicket() {
|
|
653
|
+
if (state.mode !== "edit" || state.ticketIndex == null) {
|
|
654
|
+
flash("Cannot delete: no ticket selected", "error");
|
|
655
|
+
return;
|
|
656
|
+
}
|
|
657
|
+
const confirmed = window.confirm(`Delete TICKET-${String(state.ticketIndex).padStart(3, "0")}.md? This cannot be undone.`);
|
|
658
|
+
if (!confirmed)
|
|
659
|
+
return;
|
|
660
|
+
setButtonsLoading(true);
|
|
661
|
+
hideError();
|
|
662
|
+
try {
|
|
663
|
+
await api(`/api/flows/ticket_flow/tickets/${state.ticketIndex}`, {
|
|
664
|
+
method: "DELETE",
|
|
665
|
+
});
|
|
666
|
+
clearTicketChatHistory(state.ticketIndex);
|
|
667
|
+
flash("Ticket deleted");
|
|
668
|
+
// Close modal
|
|
669
|
+
state.isOpen = false;
|
|
670
|
+
state.originalBody = "";
|
|
671
|
+
state.originalFrontmatter = { ...DEFAULT_FRONTMATTER };
|
|
672
|
+
const { modal } = els();
|
|
673
|
+
if (modal)
|
|
674
|
+
modal.classList.add("hidden");
|
|
675
|
+
// Notify that tickets changed
|
|
676
|
+
publish("tickets:updated", {});
|
|
677
|
+
}
|
|
678
|
+
catch (err) {
|
|
679
|
+
showError(err.message || "Failed to delete ticket");
|
|
680
|
+
}
|
|
681
|
+
finally {
|
|
682
|
+
setButtonsLoading(false);
|
|
683
|
+
}
|
|
684
|
+
}
|
|
685
|
+
/**
|
|
686
|
+
* Initialize the ticket editor - wire up event listeners
|
|
687
|
+
*/
|
|
688
|
+
export function initTicketEditor() {
|
|
689
|
+
const { modal, content, deleteBtn, closeBtn, newBtn, insertCheckboxBtn, undoBtn, prevBtn, nextBtn, fmAgent, fmModel, fmReasoning, fmDone, fmTitle, chatInput, chatSendBtn, chatCancelBtn, patchApplyBtn, patchDiscardBtn, agentSelect, modelSelect, reasoningSelect, } = els();
|
|
690
|
+
if (!modal)
|
|
691
|
+
return;
|
|
692
|
+
// Prevent double initialization
|
|
693
|
+
if (modal.dataset.editorInitialized === "1")
|
|
694
|
+
return;
|
|
695
|
+
modal.dataset.editorInitialized = "1";
|
|
696
|
+
// Initialize agent controls for ticket chat (populates agent/model/reasoning selects)
|
|
697
|
+
initAgentControls({
|
|
698
|
+
agentSelect,
|
|
699
|
+
modelSelect,
|
|
700
|
+
reasoningSelect,
|
|
701
|
+
});
|
|
702
|
+
// Initialize voice input for ticket chat
|
|
703
|
+
void initTicketVoice();
|
|
704
|
+
// Initialize rich chat experience (events toggle, etc.)
|
|
705
|
+
initTicketChatEvents();
|
|
706
|
+
// Button handlers
|
|
707
|
+
if (deleteBtn)
|
|
708
|
+
deleteBtn.addEventListener("click", () => void deleteTicket());
|
|
709
|
+
if (closeBtn)
|
|
710
|
+
closeBtn.addEventListener("click", closeTicketEditor);
|
|
711
|
+
if (newBtn)
|
|
712
|
+
newBtn.addEventListener("click", () => openTicketEditor());
|
|
713
|
+
if (insertCheckboxBtn)
|
|
714
|
+
insertCheckboxBtn.addEventListener("click", insertCheckbox);
|
|
715
|
+
if (undoBtn)
|
|
716
|
+
undoBtn.addEventListener("click", undoChange);
|
|
717
|
+
if (prevBtn)
|
|
718
|
+
prevBtn.addEventListener("click", (e) => {
|
|
719
|
+
e.preventDefault();
|
|
720
|
+
void navigateTicket(-1);
|
|
721
|
+
});
|
|
722
|
+
if (nextBtn)
|
|
723
|
+
nextBtn.addEventListener("click", (e) => {
|
|
724
|
+
e.preventDefault();
|
|
725
|
+
void navigateTicket(1);
|
|
726
|
+
});
|
|
727
|
+
// Autosave on content changes
|
|
728
|
+
if (content) {
|
|
729
|
+
content.addEventListener("input", onContentChange);
|
|
730
|
+
}
|
|
731
|
+
// Autosave on frontmatter changes
|
|
732
|
+
if (fmAgent) {
|
|
733
|
+
fmAgent.addEventListener("change", () => {
|
|
734
|
+
// Refresh model/reasoning options when agent changes
|
|
735
|
+
void refreshFmModelOptions(fmAgent.value, false);
|
|
736
|
+
onFrontmatterChange();
|
|
737
|
+
});
|
|
738
|
+
}
|
|
739
|
+
if (fmModel) {
|
|
740
|
+
fmModel.addEventListener("change", () => {
|
|
741
|
+
// Refresh reasoning options when model changes
|
|
742
|
+
const catalog = fmModelCatalogs.get(fmAgent?.value || "codex");
|
|
743
|
+
refreshFmReasoningOptions(catalog, fmModel.value, fmReasoning?.value || "");
|
|
744
|
+
onFrontmatterChange();
|
|
745
|
+
});
|
|
746
|
+
}
|
|
747
|
+
if (fmReasoning)
|
|
748
|
+
fmReasoning.addEventListener("change", onFrontmatterChange);
|
|
749
|
+
if (fmDone)
|
|
750
|
+
fmDone.addEventListener("change", onFrontmatterChange);
|
|
751
|
+
if (fmTitle)
|
|
752
|
+
fmTitle.addEventListener("input", onFrontmatterChange);
|
|
753
|
+
// Chat button handlers
|
|
754
|
+
if (chatSendBtn)
|
|
755
|
+
chatSendBtn.addEventListener("click", () => void sendTicketChat());
|
|
756
|
+
if (chatCancelBtn)
|
|
757
|
+
chatCancelBtn.addEventListener("click", () => void cancelTicketChat());
|
|
758
|
+
if (patchApplyBtn)
|
|
759
|
+
patchApplyBtn.addEventListener("click", () => void applyTicketPatch());
|
|
760
|
+
if (patchDiscardBtn)
|
|
761
|
+
patchDiscardBtn.addEventListener("click", () => void discardTicketPatch());
|
|
762
|
+
// Cmd/Ctrl+Enter in chat input sends message
|
|
763
|
+
if (chatInput) {
|
|
764
|
+
chatInput.addEventListener("keydown", (e) => {
|
|
765
|
+
if ((e.metaKey || e.ctrlKey) && e.key === "Enter") {
|
|
766
|
+
e.preventDefault();
|
|
767
|
+
void sendTicketChat();
|
|
768
|
+
}
|
|
769
|
+
});
|
|
770
|
+
// Auto-resize textarea on input
|
|
771
|
+
chatInput.addEventListener("input", () => {
|
|
772
|
+
chatInput.style.height = "auto";
|
|
773
|
+
chatInput.style.height = Math.min(chatInput.scrollHeight, 100) + "px";
|
|
774
|
+
});
|
|
775
|
+
}
|
|
776
|
+
// Close on backdrop click
|
|
777
|
+
modal.addEventListener("click", (e) => {
|
|
778
|
+
if (e.target === modal) {
|
|
779
|
+
closeTicketEditor();
|
|
780
|
+
}
|
|
781
|
+
});
|
|
782
|
+
// Close on Escape key
|
|
783
|
+
document.addEventListener("keydown", (e) => {
|
|
784
|
+
if (e.key === "Escape" && state.isOpen) {
|
|
785
|
+
closeTicketEditor();
|
|
786
|
+
}
|
|
787
|
+
});
|
|
788
|
+
// Cmd/Ctrl+Z triggers undo
|
|
789
|
+
document.addEventListener("keydown", (e) => {
|
|
790
|
+
if (state.isOpen && (e.metaKey || e.ctrlKey) && e.key === "z" && !e.shiftKey) {
|
|
791
|
+
// Only handle if not in chat input
|
|
792
|
+
const active = document.activeElement;
|
|
793
|
+
if (active === chatInput)
|
|
794
|
+
return;
|
|
795
|
+
e.preventDefault();
|
|
796
|
+
undoChange();
|
|
797
|
+
}
|
|
798
|
+
});
|
|
799
|
+
// Left/Right arrows navigate between tickets when editor is open and not typing
|
|
800
|
+
document.addEventListener("keydown", (e) => {
|
|
801
|
+
if (!state.isOpen)
|
|
802
|
+
return;
|
|
803
|
+
// Check for navigation keys
|
|
804
|
+
if (e.key !== "ArrowLeft" && e.key !== "ArrowRight")
|
|
805
|
+
return;
|
|
806
|
+
// Don't interfere with typing
|
|
807
|
+
if (isTypingTarget(e.target))
|
|
808
|
+
return;
|
|
809
|
+
// Only allow Alt or no modifier (no Ctrl/Meta/Shift)
|
|
810
|
+
if (e.ctrlKey || e.metaKey || e.shiftKey)
|
|
811
|
+
return;
|
|
812
|
+
e.preventDefault();
|
|
813
|
+
void navigateTicket(e.key === "ArrowLeft" ? -1 : 1);
|
|
814
|
+
});
|
|
815
|
+
// Enter key creates new TODO checkbox when on a checkbox line
|
|
816
|
+
if (content) {
|
|
817
|
+
content.addEventListener("keydown", (e) => {
|
|
818
|
+
// Prevent manual frontmatter entry in the body
|
|
819
|
+
if (e.key === "-" && content.selectionStart === 2 && content.value.startsWith("--") && !content.value.includes("\n")) {
|
|
820
|
+
flash("Please use the frontmatter editor above", "error");
|
|
821
|
+
e.preventDefault();
|
|
822
|
+
return;
|
|
823
|
+
}
|
|
824
|
+
if (e.key === "Enter" && !e.isComposing && !e.shiftKey) {
|
|
825
|
+
const text = content.value;
|
|
826
|
+
const pos = content.selectionStart;
|
|
827
|
+
const lineStart = text.lastIndexOf("\n", pos - 1) + 1;
|
|
828
|
+
const lineEnd = text.indexOf("\n", pos);
|
|
829
|
+
const currentLine = text.slice(lineStart, lineEnd === -1 ? text.length : lineEnd);
|
|
830
|
+
const match = currentLine.match(/^(\s*)- \[(x|X| )?\]/);
|
|
831
|
+
if (match) {
|
|
832
|
+
e.preventDefault();
|
|
833
|
+
const indent = match[1];
|
|
834
|
+
const newLine = "\n" + indent + "- [ ] ";
|
|
835
|
+
const endOfCurrentLine = lineEnd === -1 ? text.length : lineEnd;
|
|
836
|
+
const newValue = text.slice(0, endOfCurrentLine) + newLine + text.slice(endOfCurrentLine);
|
|
837
|
+
content.value = newValue;
|
|
838
|
+
const newPos = endOfCurrentLine + newLine.length;
|
|
839
|
+
content.setSelectionRange(newPos, newPos);
|
|
840
|
+
}
|
|
841
|
+
}
|
|
842
|
+
});
|
|
843
|
+
}
|
|
844
|
+
}
|