codex-autorunner 1.0.0__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/agents/codex/harness.py +1 -1
- 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/registry.py +22 -3
- codex_autorunner/bootstrap.py +7 -3
- codex_autorunner/cli.py +5 -1174
- codex_autorunner/codex_cli.py +20 -84
- codex_autorunner/core/__init__.py +4 -0
- codex_autorunner/core/about_car.py +6 -1
- codex_autorunner/core/app_server_ids.py +59 -0
- codex_autorunner/core/app_server_threads.py +11 -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 +197 -3
- codex_autorunner/core/drafts.py +58 -4
- codex_autorunner/core/engine.py +1329 -680
- codex_autorunner/core/exceptions.py +4 -0
- codex_autorunner/core/flows/controller.py +25 -1
- codex_autorunner/core/flows/models.py +13 -0
- codex_autorunner/core/flows/reasons.py +52 -0
- codex_autorunner/core/flows/reconciler.py +131 -0
- codex_autorunner/core/flows/runtime.py +35 -4
- codex_autorunner/core/flows/store.py +83 -0
- codex_autorunner/core/flows/transition.py +5 -0
- codex_autorunner/core/flows/ux_helpers.py +257 -0
- codex_autorunner/core/git_utils.py +62 -0
- codex_autorunner/core/hub.py +121 -7
- codex_autorunner/core/notifications.py +14 -2
- codex_autorunner/core/ports/__init__.py +28 -0
- codex_autorunner/{integrations/agents → core/ports}/agent_backend.py +11 -3
- codex_autorunner/core/ports/backend_orchestrator.py +41 -0
- codex_autorunner/{integrations/agents → core/ports}/run_event.py +22 -2
- codex_autorunner/core/state_roots.py +57 -0
- codex_autorunner/core/supervisor_protocol.py +15 -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 +4 -5
- codex_autorunner/core/update_paths.py +28 -0
- codex_autorunner/core/usage.py +164 -12
- codex_autorunner/core/utils.py +91 -9
- codex_autorunner/flows/review/__init__.py +17 -0
- codex_autorunner/{core/review.py → flows/review/service.py} +15 -10
- codex_autorunner/flows/ticket_flow/definition.py +9 -2
- codex_autorunner/integrations/agents/__init__.py +9 -19
- 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 +158 -17
- codex_autorunner/integrations/agents/opencode_adapter.py +108 -0
- codex_autorunner/integrations/agents/opencode_backend.py +305 -32
- codex_autorunner/integrations/agents/runner.py +91 -0
- codex_autorunner/integrations/agents/wiring.py +271 -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/handlers/callbacks.py +7 -0
- codex_autorunner/integrations/telegram/handlers/commands/flows.py +1203 -66
- codex_autorunner/integrations/telegram/handlers/commands_runtime.py +4 -3
- codex_autorunner/integrations/telegram/handlers/commands_spec.py +8 -2
- codex_autorunner/integrations/telegram/handlers/messages.py +1 -0
- codex_autorunner/integrations/telegram/handlers/selections.py +61 -1
- codex_autorunner/integrations/telegram/helpers.py +24 -1
- codex_autorunner/integrations/telegram/service.py +15 -10
- codex_autorunner/integrations/telegram/ticket_flow_bridge.py +329 -40
- codex_autorunner/integrations/telegram/transport.py +3 -1
- 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 +2 -2
- codex_autorunner/static/agentControls.js +40 -11
- codex_autorunner/static/app.js +11 -3
- codex_autorunner/static/archive.js +826 -0
- codex_autorunner/static/archiveApi.js +37 -0
- codex_autorunner/static/autoRefresh.js +7 -7
- codex_autorunner/static/dashboard.js +224 -171
- codex_autorunner/static/hub.js +112 -94
- codex_autorunner/static/index.html +80 -33
- codex_autorunner/static/messages.js +486 -83
- codex_autorunner/static/preserve.js +17 -0
- codex_autorunner/static/settings.js +125 -6
- codex_autorunner/static/smartRefresh.js +52 -0
- codex_autorunner/static/styles.css +1373 -101
- codex_autorunner/static/tabs.js +152 -11
- codex_autorunner/static/terminal.js +18 -0
- codex_autorunner/static/ticketEditor.js +99 -5
- codex_autorunner/static/tickets.js +760 -87
- codex_autorunner/static/utils.js +11 -0
- codex_autorunner/static/workspace.js +133 -40
- codex_autorunner/static/workspaceFileBrowser.js +9 -9
- 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 +8 -1
- codex_autorunner/tickets/agent_pool.py +26 -4
- codex_autorunner/tickets/files.py +6 -2
- codex_autorunner/tickets/models.py +3 -1
- codex_autorunner/tickets/outbox.py +12 -0
- codex_autorunner/tickets/runner.py +63 -5
- 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.1.0.dist-info/METADATA +154 -0
- codex_autorunner-1.1.0.dist-info/RECORD +308 -0
- 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.1.0.dist-info}/WHEEL +0 -0
- {codex_autorunner-1.0.0.dist-info → codex_autorunner-1.1.0.dist-info}/entry_points.txt +0 -0
- {codex_autorunner-1.0.0.dist-info → codex_autorunner-1.1.0.dist-info}/licenses/LICENSE +0 -0
- {codex_autorunner-1.0.0.dist-info → codex_autorunner-1.1.0.dist-info}/top_level.txt +0 -0
codex_autorunner/static/tabs.js
CHANGED
|
@@ -2,8 +2,16 @@
|
|
|
2
2
|
import { publish } from "./bus.js";
|
|
3
3
|
import { escapeHtml, getUrlParams, updateUrlParams } from "./utils.js";
|
|
4
4
|
const tabs = [];
|
|
5
|
+
const hamburgerActions = [];
|
|
6
|
+
let hamburgerMenuOpen = false;
|
|
7
|
+
let hamburgerMenuEl = null;
|
|
8
|
+
let hamburgerBtnEl = null;
|
|
9
|
+
let hamburgerBackdropEl = null;
|
|
5
10
|
export function registerTab(id, label, opts = {}) {
|
|
6
|
-
tabs.push({ id, label, hidden: Boolean(opts.hidden) });
|
|
11
|
+
tabs.push({ id, label, hidden: Boolean(opts.hidden), menuTab: Boolean(opts.menuTab), icon: opts.icon });
|
|
12
|
+
}
|
|
13
|
+
export function registerHamburgerAction(id, label, icon, onClick) {
|
|
14
|
+
hamburgerActions.push({ id, label, icon, onClick });
|
|
7
15
|
}
|
|
8
16
|
let setActivePanelFn = null;
|
|
9
17
|
let pendingActivate = null;
|
|
@@ -15,24 +23,55 @@ export function activateTab(id) {
|
|
|
15
23
|
pendingActivate = id;
|
|
16
24
|
}
|
|
17
25
|
}
|
|
26
|
+
function closeHamburgerMenu() {
|
|
27
|
+
if (!hamburgerMenuOpen)
|
|
28
|
+
return;
|
|
29
|
+
hamburgerMenuOpen = false;
|
|
30
|
+
hamburgerMenuEl?.classList.remove("open");
|
|
31
|
+
hamburgerBtnEl?.classList.remove("active");
|
|
32
|
+
hamburgerBackdropEl?.classList.remove("open");
|
|
33
|
+
}
|
|
34
|
+
function toggleHamburgerMenu() {
|
|
35
|
+
hamburgerMenuOpen = !hamburgerMenuOpen;
|
|
36
|
+
hamburgerMenuEl?.classList.toggle("open", hamburgerMenuOpen);
|
|
37
|
+
hamburgerBtnEl?.classList.toggle("active", hamburgerMenuOpen);
|
|
38
|
+
hamburgerBackdropEl?.classList.toggle("open", hamburgerMenuOpen);
|
|
39
|
+
}
|
|
40
|
+
function updateHamburgerActiveState(activeTabId) {
|
|
41
|
+
if (!hamburgerMenuEl)
|
|
42
|
+
return;
|
|
43
|
+
const items = hamburgerMenuEl.querySelectorAll(".hamburger-item[data-target]");
|
|
44
|
+
items.forEach((item) => {
|
|
45
|
+
const target = item.dataset.target;
|
|
46
|
+
item.classList.toggle("active", target === activeTabId);
|
|
47
|
+
});
|
|
48
|
+
// Also update hamburger button active state if a menu tab is active
|
|
49
|
+
const isMenuTabActive = tabs.some((t) => t.menuTab && t.id === activeTabId);
|
|
50
|
+
hamburgerBtnEl?.classList.toggle("has-active", isMenuTabActive);
|
|
51
|
+
}
|
|
18
52
|
export function initTabs(defaultTab = "analytics") {
|
|
19
53
|
const container = document.querySelector(".tabs");
|
|
54
|
+
const navBar = document.querySelector(".nav-bar");
|
|
20
55
|
if (!container)
|
|
21
56
|
return;
|
|
22
57
|
container.innerHTML = "";
|
|
23
58
|
const panels = document.querySelectorAll(".panel");
|
|
24
59
|
const setActivePanel = (id) => {
|
|
25
60
|
panels.forEach((p) => p.classList.toggle("active", p.id === id));
|
|
61
|
+
// Update primary tab buttons
|
|
26
62
|
const buttons = container.querySelectorAll(".tab");
|
|
27
63
|
buttons.forEach((btn) => btn.classList.toggle("active", btn.dataset.target === id));
|
|
64
|
+
// Update hamburger menu items
|
|
65
|
+
updateHamburgerActiveState(id);
|
|
28
66
|
updateUrlParams({ tab: id });
|
|
29
67
|
publish("tab:change", id);
|
|
30
68
|
};
|
|
31
69
|
setActivePanelFn = setActivePanel;
|
|
32
|
-
tabs
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
70
|
+
// Separate primary tabs from menu tabs
|
|
71
|
+
const primaryTabs = tabs.filter((t) => !t.hidden && !t.menuTab);
|
|
72
|
+
const menuTabs = tabs.filter((t) => !t.hidden && t.menuTab);
|
|
73
|
+
// Render primary tabs
|
|
74
|
+
primaryTabs.forEach(tab => {
|
|
36
75
|
const btn = document.createElement("button");
|
|
37
76
|
btn.className = "tab";
|
|
38
77
|
btn.dataset.target = tab.id;
|
|
@@ -43,20 +82,122 @@ export function initTabs(defaultTab = "analytics") {
|
|
|
43
82
|
btn.addEventListener("click", () => setActivePanel(tab.id));
|
|
44
83
|
container.appendChild(btn);
|
|
45
84
|
});
|
|
85
|
+
// Create hamburger menu if there are menu tabs or actions
|
|
86
|
+
if (menuTabs.length > 0 || hamburgerActions.length > 0) {
|
|
87
|
+
const wrapper = document.createElement("div");
|
|
88
|
+
wrapper.className = "hamburger-wrapper";
|
|
89
|
+
// Hamburger button
|
|
90
|
+
const btn = document.createElement("button");
|
|
91
|
+
btn.className = "hamburger-btn";
|
|
92
|
+
btn.setAttribute("aria-label", "More options");
|
|
93
|
+
btn.setAttribute("aria-expanded", "false");
|
|
94
|
+
btn.innerHTML = `
|
|
95
|
+
<span class="hamburger-icon">
|
|
96
|
+
<span></span>
|
|
97
|
+
<span></span>
|
|
98
|
+
<span></span>
|
|
99
|
+
</span>
|
|
100
|
+
`;
|
|
101
|
+
hamburgerBtnEl = btn;
|
|
102
|
+
// Hamburger menu dropdown
|
|
103
|
+
const menu = document.createElement("div");
|
|
104
|
+
menu.className = "hamburger-menu";
|
|
105
|
+
menu.setAttribute("role", "menu");
|
|
106
|
+
hamburgerMenuEl = menu;
|
|
107
|
+
// Add menu tab items
|
|
108
|
+
menuTabs.forEach((tab) => {
|
|
109
|
+
const item = document.createElement("button");
|
|
110
|
+
item.className = "hamburger-item";
|
|
111
|
+
item.dataset.target = tab.id;
|
|
112
|
+
item.setAttribute("role", "menuitem");
|
|
113
|
+
const iconHtml = tab.icon ? `<span class="hamburger-item-icon">${tab.icon}</span>` : "";
|
|
114
|
+
item.innerHTML = `${iconHtml}<span>${escapeHtml(tab.label)}</span>`;
|
|
115
|
+
item.addEventListener("click", () => {
|
|
116
|
+
setActivePanel(tab.id);
|
|
117
|
+
closeHamburgerMenu();
|
|
118
|
+
});
|
|
119
|
+
menu.appendChild(item);
|
|
120
|
+
});
|
|
121
|
+
// Add divider if there are both tabs and actions
|
|
122
|
+
if (menuTabs.length > 0 && hamburgerActions.length > 0) {
|
|
123
|
+
const divider = document.createElement("div");
|
|
124
|
+
divider.className = "hamburger-divider";
|
|
125
|
+
menu.appendChild(divider);
|
|
126
|
+
}
|
|
127
|
+
// Add action items (like Settings)
|
|
128
|
+
hamburgerActions.forEach((action) => {
|
|
129
|
+
const item = document.createElement("button");
|
|
130
|
+
item.className = "hamburger-item";
|
|
131
|
+
item.dataset.action = action.id;
|
|
132
|
+
item.setAttribute("role", "menuitem");
|
|
133
|
+
const iconHtml = action.icon ? `<span class="hamburger-item-icon">${action.icon}</span>` : "";
|
|
134
|
+
item.innerHTML = `${iconHtml}<span>${escapeHtml(action.label)}</span>`;
|
|
135
|
+
item.addEventListener("click", () => {
|
|
136
|
+
action.onClick();
|
|
137
|
+
closeHamburgerMenu();
|
|
138
|
+
});
|
|
139
|
+
menu.appendChild(item);
|
|
140
|
+
});
|
|
141
|
+
// Mobile backdrop - appended to body for proper z-index stacking
|
|
142
|
+
const backdrop = document.createElement("div");
|
|
143
|
+
backdrop.className = "hamburger-backdrop";
|
|
144
|
+
backdrop.addEventListener("click", closeHamburgerMenu);
|
|
145
|
+
hamburgerBackdropEl = backdrop;
|
|
146
|
+
document.body.appendChild(backdrop);
|
|
147
|
+
// Append menu to body for mobile z-index stacking (above backdrop)
|
|
148
|
+
// On mobile, the nav-bar has z-index:100 which creates a stacking context
|
|
149
|
+
// that would trap the menu below the backdrop (z-index:1999)
|
|
150
|
+
document.body.appendChild(menu);
|
|
151
|
+
// Toggle menu on button click
|
|
152
|
+
const toggleHandler = (e) => {
|
|
153
|
+
e.stopPropagation();
|
|
154
|
+
// Prevent ghost clicks on touch devices
|
|
155
|
+
if (e.type === "touchend") {
|
|
156
|
+
e.preventDefault();
|
|
157
|
+
}
|
|
158
|
+
toggleHamburgerMenu();
|
|
159
|
+
btn.setAttribute("aria-expanded", String(hamburgerMenuOpen));
|
|
160
|
+
};
|
|
161
|
+
btn.addEventListener("click", toggleHandler);
|
|
162
|
+
btn.addEventListener("touchend", toggleHandler);
|
|
163
|
+
// Close menu on outside click (check both wrapper and menu since menu is in body)
|
|
164
|
+
document.addEventListener("click", (e) => {
|
|
165
|
+
if (hamburgerMenuOpen && !wrapper.contains(e.target) && !menu.contains(e.target)) {
|
|
166
|
+
closeHamburgerMenu();
|
|
167
|
+
}
|
|
168
|
+
});
|
|
169
|
+
// Close menu on Escape
|
|
170
|
+
document.addEventListener("keydown", (e) => {
|
|
171
|
+
if (e.key === "Escape" && hamburgerMenuOpen) {
|
|
172
|
+
closeHamburgerMenu();
|
|
173
|
+
hamburgerBtnEl?.focus();
|
|
174
|
+
}
|
|
175
|
+
});
|
|
176
|
+
wrapper.appendChild(btn);
|
|
177
|
+
// Insert hamburger after tabs or at the end of nav bar
|
|
178
|
+
const navActions = navBar?.querySelector(".nav-actions");
|
|
179
|
+
if (navActions) {
|
|
180
|
+
navBar?.insertBefore(wrapper, navActions);
|
|
181
|
+
}
|
|
182
|
+
else {
|
|
183
|
+
navBar?.appendChild(wrapper);
|
|
184
|
+
}
|
|
185
|
+
}
|
|
46
186
|
const params = getUrlParams();
|
|
47
187
|
const requested = params.get("tab");
|
|
48
|
-
const
|
|
188
|
+
const allVisibleTabs = tabs.filter((t) => !t.hidden);
|
|
189
|
+
const initialTab = allVisibleTabs.some((t) => t.id === requested)
|
|
49
190
|
? requested
|
|
50
|
-
:
|
|
191
|
+
: allVisibleTabs.some((t) => t.id === defaultTab)
|
|
51
192
|
? defaultTab
|
|
52
|
-
:
|
|
193
|
+
: allVisibleTabs[0]?.id;
|
|
53
194
|
if (initialTab) {
|
|
54
195
|
setActivePanel(initialTab);
|
|
55
196
|
}
|
|
56
|
-
else if (
|
|
57
|
-
setActivePanel(
|
|
197
|
+
else if (allVisibleTabs.length > 0) {
|
|
198
|
+
setActivePanel(allVisibleTabs[0].id);
|
|
58
199
|
}
|
|
59
|
-
if (pendingActivate &&
|
|
200
|
+
if (pendingActivate && allVisibleTabs.some((t) => t.id === pendingActivate)) {
|
|
60
201
|
const id = pendingActivate;
|
|
61
202
|
pendingActivate = null;
|
|
62
203
|
setActivePanel(id);
|
|
@@ -1,6 +1,10 @@
|
|
|
1
1
|
// GENERATED FILE - do not edit directly. Source: static_src/
|
|
2
2
|
import { TerminalManager } from "./terminalManager.js";
|
|
3
|
+
import { refreshAgentControls } from "./agentControls.js";
|
|
4
|
+
import { subscribe } from "./bus.js";
|
|
5
|
+
import { isRepoHealthy } from "./health.js";
|
|
3
6
|
let terminalManager = null;
|
|
7
|
+
let terminalHealthRefreshInitialized = false;
|
|
4
8
|
export function getTerminalManager() {
|
|
5
9
|
return terminalManager;
|
|
6
10
|
}
|
|
@@ -15,6 +19,7 @@ export function initTerminal() {
|
|
|
15
19
|
}
|
|
16
20
|
terminalManager = new TerminalManager();
|
|
17
21
|
terminalManager.init();
|
|
22
|
+
initTerminalHealthRefresh();
|
|
18
23
|
// Ensure terminal is resized to fit container after initialization
|
|
19
24
|
if (terminalManager) {
|
|
20
25
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
@@ -24,3 +29,16 @@ export function initTerminal() {
|
|
|
24
29
|
}
|
|
25
30
|
}
|
|
26
31
|
}
|
|
32
|
+
function initTerminalHealthRefresh() {
|
|
33
|
+
if (terminalHealthRefreshInitialized)
|
|
34
|
+
return;
|
|
35
|
+
terminalHealthRefreshInitialized = true;
|
|
36
|
+
subscribe("repo:health", (payload) => {
|
|
37
|
+
const status = payload?.status || "";
|
|
38
|
+
if (status !== "ok" && status !== "degraded")
|
|
39
|
+
return;
|
|
40
|
+
if (!isRepoHealthy())
|
|
41
|
+
return;
|
|
42
|
+
void refreshAgentControls({ reason: "background" });
|
|
43
|
+
});
|
|
44
|
+
}
|
|
@@ -30,6 +30,68 @@ const state = {
|
|
|
30
30
|
// Autosave debounce timer
|
|
31
31
|
const AUTOSAVE_DELAY_MS = 1000;
|
|
32
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
|
+
}
|
|
33
95
|
function els() {
|
|
34
96
|
return {
|
|
35
97
|
modal: document.getElementById("ticket-editor-modal"),
|
|
@@ -40,6 +102,8 @@ function els() {
|
|
|
40
102
|
newBtn: document.getElementById("ticket-new-btn"),
|
|
41
103
|
insertCheckboxBtn: document.getElementById("ticket-insert-checkbox"),
|
|
42
104
|
undoBtn: document.getElementById("ticket-undo-btn"),
|
|
105
|
+
prevBtn: document.getElementById("ticket-nav-prev"),
|
|
106
|
+
nextBtn: document.getElementById("ticket-nav-next"),
|
|
43
107
|
autosaveStatus: document.getElementById("ticket-autosave-status"),
|
|
44
108
|
// Frontmatter form elements
|
|
45
109
|
fmAgent: document.getElementById("ticket-fm-agent"),
|
|
@@ -532,13 +596,14 @@ export function openTicketEditor(ticket) {
|
|
|
532
596
|
if (ticket?.index != null) {
|
|
533
597
|
updateUrlParams({ ticket: ticket.index });
|
|
534
598
|
}
|
|
535
|
-
|
|
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
|
|
536
604
|
if (state.mode === "create" && fmTitle) {
|
|
537
605
|
fmTitle.focus();
|
|
538
606
|
}
|
|
539
|
-
else {
|
|
540
|
-
content.focus();
|
|
541
|
-
}
|
|
542
607
|
}
|
|
543
608
|
/**
|
|
544
609
|
* Close the ticket editor modal (autosaves on close)
|
|
@@ -568,9 +633,12 @@ export function closeTicketEditor() {
|
|
|
568
633
|
ticketDocEditor = null;
|
|
569
634
|
// Clear ticket from URL
|
|
570
635
|
updateUrlParams({ ticket: null });
|
|
636
|
+
void updateTicketNavButtons();
|
|
571
637
|
// Reset chat state
|
|
572
638
|
resetTicketChatState();
|
|
573
639
|
setTicketIndex(null);
|
|
640
|
+
// Notify that editor was closed (for selection state cleanup)
|
|
641
|
+
publish("ticket-editor:closed", {});
|
|
574
642
|
}
|
|
575
643
|
/**
|
|
576
644
|
* Save the current ticket (triggers immediate autosave)
|
|
@@ -618,7 +686,7 @@ export async function deleteTicket() {
|
|
|
618
686
|
* Initialize the ticket editor - wire up event listeners
|
|
619
687
|
*/
|
|
620
688
|
export function initTicketEditor() {
|
|
621
|
-
const { modal, content, deleteBtn, closeBtn, newBtn, insertCheckboxBtn, undoBtn, fmAgent, fmModel, fmReasoning, fmDone, fmTitle, chatInput, chatSendBtn, chatCancelBtn, patchApplyBtn, patchDiscardBtn, agentSelect, modelSelect, reasoningSelect, } = els();
|
|
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();
|
|
622
690
|
if (!modal)
|
|
623
691
|
return;
|
|
624
692
|
// Prevent double initialization
|
|
@@ -646,6 +714,16 @@ export function initTicketEditor() {
|
|
|
646
714
|
insertCheckboxBtn.addEventListener("click", insertCheckbox);
|
|
647
715
|
if (undoBtn)
|
|
648
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
|
+
});
|
|
649
727
|
// Autosave on content changes
|
|
650
728
|
if (content) {
|
|
651
729
|
content.addEventListener("input", onContentChange);
|
|
@@ -718,6 +796,22 @@ export function initTicketEditor() {
|
|
|
718
796
|
undoChange();
|
|
719
797
|
}
|
|
720
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
|
+
});
|
|
721
815
|
// Enter key creates new TODO checkbox when on a checkbox line
|
|
722
816
|
if (content) {
|
|
723
817
|
content.addEventListener("keydown", (e) => {
|