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.
Files changed (227) hide show
  1. codex_autorunner/__init__.py +12 -1
  2. codex_autorunner/agents/codex/harness.py +1 -1
  3. codex_autorunner/agents/opencode/client.py +113 -4
  4. codex_autorunner/agents/opencode/constants.py +3 -0
  5. codex_autorunner/agents/opencode/harness.py +6 -1
  6. codex_autorunner/agents/opencode/runtime.py +59 -18
  7. codex_autorunner/agents/opencode/supervisor.py +4 -0
  8. codex_autorunner/agents/registry.py +36 -7
  9. codex_autorunner/bootstrap.py +226 -4
  10. codex_autorunner/cli.py +5 -1174
  11. codex_autorunner/codex_cli.py +20 -84
  12. codex_autorunner/core/__init__.py +20 -0
  13. codex_autorunner/core/about_car.py +119 -1
  14. codex_autorunner/core/app_server_ids.py +59 -0
  15. codex_autorunner/core/app_server_threads.py +17 -2
  16. codex_autorunner/core/app_server_utils.py +165 -0
  17. codex_autorunner/core/archive.py +349 -0
  18. codex_autorunner/core/codex_runner.py +6 -2
  19. codex_autorunner/core/config.py +433 -4
  20. codex_autorunner/core/context_awareness.py +38 -0
  21. codex_autorunner/core/docs.py +0 -122
  22. codex_autorunner/core/drafts.py +58 -4
  23. codex_autorunner/core/exceptions.py +4 -0
  24. codex_autorunner/core/filebox.py +265 -0
  25. codex_autorunner/core/flows/controller.py +96 -2
  26. codex_autorunner/core/flows/models.py +13 -0
  27. codex_autorunner/core/flows/reasons.py +52 -0
  28. codex_autorunner/core/flows/reconciler.py +134 -0
  29. codex_autorunner/core/flows/runtime.py +57 -4
  30. codex_autorunner/core/flows/store.py +142 -7
  31. codex_autorunner/core/flows/transition.py +27 -15
  32. codex_autorunner/core/flows/ux_helpers.py +272 -0
  33. codex_autorunner/core/flows/worker_process.py +32 -6
  34. codex_autorunner/core/git_utils.py +62 -0
  35. codex_autorunner/core/hub.py +291 -20
  36. codex_autorunner/core/lifecycle_events.py +253 -0
  37. codex_autorunner/core/notifications.py +14 -2
  38. codex_autorunner/core/path_utils.py +2 -1
  39. codex_autorunner/core/pma_audit.py +224 -0
  40. codex_autorunner/core/pma_context.py +496 -0
  41. codex_autorunner/core/pma_dispatch_interceptor.py +284 -0
  42. codex_autorunner/core/pma_lifecycle.py +527 -0
  43. codex_autorunner/core/pma_queue.py +367 -0
  44. codex_autorunner/core/pma_safety.py +221 -0
  45. codex_autorunner/core/pma_state.py +115 -0
  46. codex_autorunner/core/ports/__init__.py +28 -0
  47. codex_autorunner/{integrations/agents → core/ports}/agent_backend.py +13 -8
  48. codex_autorunner/core/ports/backend_orchestrator.py +41 -0
  49. codex_autorunner/{integrations/agents → core/ports}/run_event.py +23 -6
  50. codex_autorunner/core/prompt.py +0 -80
  51. codex_autorunner/core/prompts.py +56 -172
  52. codex_autorunner/core/redaction.py +0 -4
  53. codex_autorunner/core/review_context.py +11 -9
  54. codex_autorunner/core/runner_controller.py +35 -33
  55. codex_autorunner/core/runner_state.py +147 -0
  56. codex_autorunner/core/runtime.py +829 -0
  57. codex_autorunner/core/sqlite_utils.py +13 -4
  58. codex_autorunner/core/state.py +7 -10
  59. codex_autorunner/core/state_roots.py +62 -0
  60. codex_autorunner/core/supervisor_protocol.py +15 -0
  61. codex_autorunner/core/templates/__init__.py +39 -0
  62. codex_autorunner/core/templates/git_mirror.py +234 -0
  63. codex_autorunner/core/templates/provenance.py +56 -0
  64. codex_autorunner/core/templates/scan_cache.py +120 -0
  65. codex_autorunner/core/text_delta_coalescer.py +54 -0
  66. codex_autorunner/core/ticket_linter_cli.py +218 -0
  67. codex_autorunner/core/ticket_manager_cli.py +494 -0
  68. codex_autorunner/core/time_utils.py +11 -0
  69. codex_autorunner/core/types.py +18 -0
  70. codex_autorunner/core/update.py +4 -5
  71. codex_autorunner/core/update_paths.py +28 -0
  72. codex_autorunner/core/usage.py +164 -12
  73. codex_autorunner/core/utils.py +125 -15
  74. codex_autorunner/flows/review/__init__.py +17 -0
  75. codex_autorunner/{core/review.py → flows/review/service.py} +37 -34
  76. codex_autorunner/flows/ticket_flow/definition.py +52 -3
  77. codex_autorunner/integrations/agents/__init__.py +11 -19
  78. codex_autorunner/integrations/agents/backend_orchestrator.py +302 -0
  79. codex_autorunner/integrations/agents/codex_adapter.py +90 -0
  80. codex_autorunner/integrations/agents/codex_backend.py +177 -25
  81. codex_autorunner/integrations/agents/opencode_adapter.py +108 -0
  82. codex_autorunner/integrations/agents/opencode_backend.py +305 -32
  83. codex_autorunner/integrations/agents/runner.py +86 -0
  84. codex_autorunner/integrations/agents/wiring.py +279 -0
  85. codex_autorunner/integrations/app_server/client.py +7 -60
  86. codex_autorunner/integrations/app_server/env.py +2 -107
  87. codex_autorunner/{core/app_server_events.py → integrations/app_server/event_buffer.py} +15 -8
  88. codex_autorunner/integrations/telegram/adapter.py +65 -0
  89. codex_autorunner/integrations/telegram/config.py +46 -0
  90. codex_autorunner/integrations/telegram/constants.py +1 -1
  91. codex_autorunner/integrations/telegram/doctor.py +228 -6
  92. codex_autorunner/integrations/telegram/handlers/callbacks.py +7 -0
  93. codex_autorunner/integrations/telegram/handlers/commands/execution.py +236 -74
  94. codex_autorunner/integrations/telegram/handlers/commands/files.py +314 -75
  95. codex_autorunner/integrations/telegram/handlers/commands/flows.py +1496 -71
  96. codex_autorunner/integrations/telegram/handlers/commands/workspace.py +498 -37
  97. codex_autorunner/integrations/telegram/handlers/commands_runtime.py +206 -48
  98. codex_autorunner/integrations/telegram/handlers/commands_spec.py +20 -3
  99. codex_autorunner/integrations/telegram/handlers/messages.py +27 -1
  100. codex_autorunner/integrations/telegram/handlers/selections.py +61 -1
  101. codex_autorunner/integrations/telegram/helpers.py +22 -1
  102. codex_autorunner/integrations/telegram/runtime.py +9 -4
  103. codex_autorunner/integrations/telegram/service.py +45 -10
  104. codex_autorunner/integrations/telegram/state.py +38 -0
  105. codex_autorunner/integrations/telegram/ticket_flow_bridge.py +338 -43
  106. codex_autorunner/integrations/telegram/transport.py +13 -4
  107. codex_autorunner/integrations/templates/__init__.py +27 -0
  108. codex_autorunner/integrations/templates/scan_agent.py +312 -0
  109. codex_autorunner/routes/__init__.py +37 -76
  110. codex_autorunner/routes/agents.py +2 -137
  111. codex_autorunner/routes/analytics.py +2 -238
  112. codex_autorunner/routes/app_server.py +2 -131
  113. codex_autorunner/routes/base.py +2 -596
  114. codex_autorunner/routes/file_chat.py +4 -833
  115. codex_autorunner/routes/flows.py +4 -977
  116. codex_autorunner/routes/messages.py +4 -456
  117. codex_autorunner/routes/repos.py +2 -196
  118. codex_autorunner/routes/review.py +2 -147
  119. codex_autorunner/routes/sessions.py +2 -175
  120. codex_autorunner/routes/settings.py +2 -168
  121. codex_autorunner/routes/shared.py +2 -275
  122. codex_autorunner/routes/system.py +4 -193
  123. codex_autorunner/routes/usage.py +2 -86
  124. codex_autorunner/routes/voice.py +2 -119
  125. codex_autorunner/routes/workspace.py +2 -270
  126. codex_autorunner/server.py +4 -4
  127. codex_autorunner/static/agentControls.js +61 -16
  128. codex_autorunner/static/app.js +126 -14
  129. codex_autorunner/static/archive.js +826 -0
  130. codex_autorunner/static/archiveApi.js +37 -0
  131. codex_autorunner/static/autoRefresh.js +7 -7
  132. codex_autorunner/static/chatUploads.js +137 -0
  133. codex_autorunner/static/dashboard.js +224 -171
  134. codex_autorunner/static/docChatCore.js +185 -13
  135. codex_autorunner/static/fileChat.js +68 -40
  136. codex_autorunner/static/fileboxUi.js +159 -0
  137. codex_autorunner/static/hub.js +114 -131
  138. codex_autorunner/static/index.html +375 -49
  139. codex_autorunner/static/messages.js +568 -87
  140. codex_autorunner/static/notifications.js +255 -0
  141. codex_autorunner/static/pma.js +1167 -0
  142. codex_autorunner/static/preserve.js +17 -0
  143. codex_autorunner/static/settings.js +128 -6
  144. codex_autorunner/static/smartRefresh.js +52 -0
  145. codex_autorunner/static/streamUtils.js +57 -0
  146. codex_autorunner/static/styles.css +9798 -6143
  147. codex_autorunner/static/tabs.js +152 -11
  148. codex_autorunner/static/templateReposSettings.js +225 -0
  149. codex_autorunner/static/terminal.js +18 -0
  150. codex_autorunner/static/ticketChatActions.js +165 -3
  151. codex_autorunner/static/ticketChatStream.js +17 -119
  152. codex_autorunner/static/ticketEditor.js +137 -15
  153. codex_autorunner/static/ticketTemplates.js +798 -0
  154. codex_autorunner/static/tickets.js +821 -98
  155. codex_autorunner/static/turnEvents.js +27 -0
  156. codex_autorunner/static/turnResume.js +33 -0
  157. codex_autorunner/static/utils.js +39 -0
  158. codex_autorunner/static/workspace.js +389 -82
  159. codex_autorunner/static/workspaceFileBrowser.js +15 -13
  160. codex_autorunner/surfaces/__init__.py +5 -0
  161. codex_autorunner/surfaces/cli/__init__.py +6 -0
  162. codex_autorunner/surfaces/cli/cli.py +2534 -0
  163. codex_autorunner/surfaces/cli/codex_cli.py +20 -0
  164. codex_autorunner/surfaces/cli/pma_cli.py +817 -0
  165. codex_autorunner/surfaces/telegram/__init__.py +3 -0
  166. codex_autorunner/surfaces/web/__init__.py +1 -0
  167. codex_autorunner/surfaces/web/app.py +2223 -0
  168. codex_autorunner/surfaces/web/hub_jobs.py +192 -0
  169. codex_autorunner/surfaces/web/middleware.py +587 -0
  170. codex_autorunner/surfaces/web/pty_session.py +370 -0
  171. codex_autorunner/surfaces/web/review.py +6 -0
  172. codex_autorunner/surfaces/web/routes/__init__.py +82 -0
  173. codex_autorunner/surfaces/web/routes/agents.py +138 -0
  174. codex_autorunner/surfaces/web/routes/analytics.py +284 -0
  175. codex_autorunner/surfaces/web/routes/app_server.py +132 -0
  176. codex_autorunner/surfaces/web/routes/archive.py +357 -0
  177. codex_autorunner/surfaces/web/routes/base.py +615 -0
  178. codex_autorunner/surfaces/web/routes/file_chat.py +1117 -0
  179. codex_autorunner/surfaces/web/routes/filebox.py +227 -0
  180. codex_autorunner/surfaces/web/routes/flows.py +1354 -0
  181. codex_autorunner/surfaces/web/routes/messages.py +490 -0
  182. codex_autorunner/surfaces/web/routes/pma.py +1652 -0
  183. codex_autorunner/surfaces/web/routes/repos.py +197 -0
  184. codex_autorunner/surfaces/web/routes/review.py +148 -0
  185. codex_autorunner/surfaces/web/routes/sessions.py +176 -0
  186. codex_autorunner/surfaces/web/routes/settings.py +169 -0
  187. codex_autorunner/surfaces/web/routes/shared.py +277 -0
  188. codex_autorunner/surfaces/web/routes/system.py +196 -0
  189. codex_autorunner/surfaces/web/routes/templates.py +634 -0
  190. codex_autorunner/surfaces/web/routes/usage.py +89 -0
  191. codex_autorunner/surfaces/web/routes/voice.py +120 -0
  192. codex_autorunner/surfaces/web/routes/workspace.py +271 -0
  193. codex_autorunner/surfaces/web/runner_manager.py +25 -0
  194. codex_autorunner/surfaces/web/schemas.py +469 -0
  195. codex_autorunner/surfaces/web/static_assets.py +490 -0
  196. codex_autorunner/surfaces/web/static_refresh.py +86 -0
  197. codex_autorunner/surfaces/web/terminal_sessions.py +78 -0
  198. codex_autorunner/tickets/__init__.py +8 -1
  199. codex_autorunner/tickets/agent_pool.py +53 -4
  200. codex_autorunner/tickets/files.py +37 -16
  201. codex_autorunner/tickets/lint.py +50 -0
  202. codex_autorunner/tickets/models.py +6 -1
  203. codex_autorunner/tickets/outbox.py +50 -2
  204. codex_autorunner/tickets/runner.py +396 -57
  205. codex_autorunner/web/__init__.py +5 -1
  206. codex_autorunner/web/app.py +2 -1949
  207. codex_autorunner/web/hub_jobs.py +2 -191
  208. codex_autorunner/web/middleware.py +2 -586
  209. codex_autorunner/web/pty_session.py +2 -369
  210. codex_autorunner/web/runner_manager.py +2 -24
  211. codex_autorunner/web/schemas.py +2 -376
  212. codex_autorunner/web/static_assets.py +4 -441
  213. codex_autorunner/web/static_refresh.py +2 -85
  214. codex_autorunner/web/terminal_sessions.py +2 -77
  215. codex_autorunner/workspace/paths.py +49 -33
  216. codex_autorunner-1.2.0.dist-info/METADATA +150 -0
  217. codex_autorunner-1.2.0.dist-info/RECORD +339 -0
  218. codex_autorunner/core/adapter_utils.py +0 -21
  219. codex_autorunner/core/engine.py +0 -2653
  220. codex_autorunner/core/static_assets.py +0 -55
  221. codex_autorunner-1.0.0.dist-info/METADATA +0 -246
  222. codex_autorunner-1.0.0.dist-info/RECORD +0 -251
  223. /codex_autorunner/{routes → surfaces/web/routes}/terminal_images.py +0 -0
  224. {codex_autorunner-1.0.0.dist-info → codex_autorunner-1.2.0.dist-info}/WHEEL +0 -0
  225. {codex_autorunner-1.0.0.dist-info → codex_autorunner-1.2.0.dist-info}/entry_points.txt +0 -0
  226. {codex_autorunner-1.0.0.dist-info → codex_autorunner-1.2.0.dist-info}/licenses/LICENSE +0 -0
  227. {codex_autorunner-1.0.0.dist-info → codex_autorunner-1.2.0.dist-info}/top_level.txt +0 -0
@@ -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.forEach(tab => {
33
- if (tab.hidden) {
34
- return;
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 initialTab = tabs.some((t) => t.id === requested)
188
+ const allVisibleTabs = tabs.filter((t) => !t.hidden);
189
+ const initialTab = allVisibleTabs.some((t) => t.id === requested)
49
190
  ? requested
50
- : tabs.some((t) => t.id === defaultTab)
191
+ : allVisibleTabs.some((t) => t.id === defaultTab)
51
192
  ? defaultTab
52
- : tabs[0]?.id;
193
+ : allVisibleTabs[0]?.id;
53
194
  if (initialTab) {
54
195
  setActivePanel(initialTab);
55
196
  }
56
- else if (tabs.length > 0) {
57
- setActivePanel(tabs[0].id);
197
+ else if (allVisibleTabs.length > 0) {
198
+ setActivePanel(allVisibleTabs[0].id);
58
199
  }
59
- if (pendingActivate && tabs.some((t) => t.id === pendingActivate)) {
200
+ if (pendingActivate && allVisibleTabs.some((t) => t.id === pendingActivate)) {
60
201
  const id = pendingActivate;
61
202
  pendingActivate = null;
62
203
  setActivePanel(id);
@@ -0,0 +1,225 @@
1
+ // GENERATED FILE - do not edit directly. Source: static_src/
2
+ import { api, confirmModal, flash } from "./utils.js";
3
+ import { checkTemplatesEnabled } from "./ticketTemplates.js";
4
+ function els() {
5
+ return {
6
+ list: document.getElementById("template-repos-list"),
7
+ addBtn: document.getElementById("template-repos-add"),
8
+ form: document.getElementById("template-repo-form"),
9
+ idInput: document.getElementById("repo-id"),
10
+ urlInput: document.getElementById("repo-url"),
11
+ refInput: document.getElementById("repo-ref"),
12
+ trustedInput: document.getElementById("repo-trusted"),
13
+ saveBtn: document.getElementById("repo-save"),
14
+ cancelBtn: document.getElementById("repo-cancel"),
15
+ };
16
+ }
17
+ const state = {
18
+ mode: "create",
19
+ editId: null,
20
+ enabled: false,
21
+ repos: [],
22
+ busy: false,
23
+ };
24
+ function setBusy(busy) {
25
+ state.busy = busy;
26
+ const { saveBtn, addBtn } = els();
27
+ if (saveBtn)
28
+ saveBtn.disabled = busy;
29
+ if (addBtn)
30
+ addBtn.disabled = busy;
31
+ }
32
+ function showForm(show) {
33
+ const { form } = els();
34
+ if (!form)
35
+ return;
36
+ if (show)
37
+ form.classList.remove("hidden");
38
+ else
39
+ form.classList.add("hidden");
40
+ }
41
+ function resetForm() {
42
+ const { idInput, urlInput, refInput, trustedInput } = els();
43
+ if (idInput)
44
+ idInput.value = "";
45
+ if (urlInput)
46
+ urlInput.value = "";
47
+ if (refInput)
48
+ refInput.value = "main";
49
+ if (trustedInput)
50
+ trustedInput.checked = false;
51
+ state.mode = "create";
52
+ state.editId = null;
53
+ if (idInput)
54
+ idInput.disabled = false;
55
+ }
56
+ function openCreateForm() {
57
+ resetForm();
58
+ showForm(true);
59
+ const { idInput } = els();
60
+ idInput?.focus();
61
+ }
62
+ function openEditForm(repo) {
63
+ const { idInput, urlInput, refInput, trustedInput } = els();
64
+ state.mode = "edit";
65
+ state.editId = repo.id;
66
+ if (idInput) {
67
+ idInput.value = repo.id;
68
+ idInput.disabled = true;
69
+ }
70
+ if (urlInput)
71
+ urlInput.value = repo.url;
72
+ if (refInput)
73
+ refInput.value = repo.default_ref || "main";
74
+ if (trustedInput)
75
+ trustedInput.checked = Boolean(repo.trusted);
76
+ showForm(true);
77
+ urlInput?.focus();
78
+ }
79
+ function normalizeRequired(value, label) {
80
+ const v = (value || "").trim();
81
+ if (!v) {
82
+ flash(`${label} is required`, "error");
83
+ return null;
84
+ }
85
+ return v;
86
+ }
87
+ function renderRepos() {
88
+ const { list } = els();
89
+ if (!list)
90
+ return;
91
+ list.innerHTML = "";
92
+ if (!state.enabled) {
93
+ const hint = document.createElement("div");
94
+ hint.className = "muted small";
95
+ hint.textContent = "Templates are disabled (templates.enabled=false).";
96
+ list.appendChild(hint);
97
+ }
98
+ if (!state.repos.length) {
99
+ const empty = document.createElement("div");
100
+ empty.className = "muted small";
101
+ empty.textContent = "No template repos configured.";
102
+ list.appendChild(empty);
103
+ return;
104
+ }
105
+ for (const repo of state.repos) {
106
+ const row = document.createElement("div");
107
+ row.className = "template-repo-item";
108
+ row.innerHTML = `
109
+ <div class="template-repo-meta">
110
+ <span class="template-repo-id">${repo.id}</span>
111
+ <span class="template-repo-url">${repo.url}</span>
112
+ <span class="muted small">ref: ${repo.default_ref}${repo.trusted ? " · trusted" : ""}</span>
113
+ </div>
114
+ <div class="template-repo-actions">
115
+ <button class="ghost sm" data-action="edit" data-id="${repo.id}">Edit</button>
116
+ <button class="danger sm" data-action="delete" data-id="${repo.id}">Delete</button>
117
+ </div>
118
+ `;
119
+ list.appendChild(row);
120
+ }
121
+ list.querySelectorAll("button[data-action]").forEach((btn) => {
122
+ btn.addEventListener("click", async () => {
123
+ const action = btn.dataset.action;
124
+ const id = btn.dataset.id;
125
+ if (!action || !id)
126
+ return;
127
+ const repo = state.repos.find((r) => r.id === id);
128
+ if (!repo)
129
+ return;
130
+ if (action === "edit") {
131
+ openEditForm(repo);
132
+ return;
133
+ }
134
+ if (action === "delete") {
135
+ await deleteRepo(id);
136
+ }
137
+ });
138
+ });
139
+ }
140
+ export async function loadTemplateRepos() {
141
+ const { list } = els();
142
+ if (!list)
143
+ return;
144
+ try {
145
+ const data = (await api("/api/templates/repos"));
146
+ state.enabled = Boolean(data.enabled);
147
+ state.repos = Array.isArray(data.repos) ? data.repos : [];
148
+ renderRepos();
149
+ }
150
+ catch (err) {
151
+ state.enabled = false;
152
+ state.repos = [];
153
+ renderRepos();
154
+ flash(err.message || "Failed to load template repos", "error");
155
+ }
156
+ }
157
+ async function saveRepo() {
158
+ const { idInput, urlInput, refInput, trustedInput } = els();
159
+ if (!idInput || !urlInput || !refInput || !trustedInput)
160
+ return;
161
+ const id = normalizeRequired(idInput.value, "ID");
162
+ const url = normalizeRequired(urlInput.value, "Git URL");
163
+ const ref = normalizeRequired(refInput.value, "Default ref");
164
+ if (!id || !url || !ref)
165
+ return;
166
+ setBusy(true);
167
+ try {
168
+ if (state.mode === "create") {
169
+ await api("/api/templates/repos", {
170
+ method: "POST",
171
+ body: { id, url, trusted: Boolean(trustedInput.checked), default_ref: ref },
172
+ });
173
+ flash("Template repo added", "success");
174
+ }
175
+ else if (state.mode === "edit" && state.editId) {
176
+ await api(`/api/templates/repos/${encodeURIComponent(state.editId)}`, {
177
+ method: "PUT",
178
+ body: { url, trusted: Boolean(trustedInput.checked), default_ref: ref },
179
+ });
180
+ flash("Template repo updated", "success");
181
+ }
182
+ await loadTemplateRepos();
183
+ await checkTemplatesEnabled();
184
+ showForm(false);
185
+ resetForm();
186
+ }
187
+ catch (err) {
188
+ flash(err.message || "Failed to save template repo", "error");
189
+ }
190
+ finally {
191
+ setBusy(false);
192
+ }
193
+ }
194
+ async function deleteRepo(id) {
195
+ const confirmed = await confirmModal(`Delete template repo "${id}"?`, {
196
+ confirmText: "Delete",
197
+ danger: true,
198
+ });
199
+ if (!confirmed)
200
+ return;
201
+ setBusy(true);
202
+ try {
203
+ await api(`/api/templates/repos/${encodeURIComponent(id)}`, { method: "DELETE" });
204
+ flash("Template repo deleted", "success");
205
+ await loadTemplateRepos();
206
+ await checkTemplatesEnabled();
207
+ }
208
+ catch (err) {
209
+ flash(err.message || "Failed to delete template repo", "error");
210
+ }
211
+ finally {
212
+ setBusy(false);
213
+ }
214
+ }
215
+ export function initTemplateReposSettings() {
216
+ const { list, addBtn, saveBtn, cancelBtn } = els();
217
+ if (!list || !addBtn || !saveBtn || !cancelBtn)
218
+ return;
219
+ addBtn.addEventListener("click", () => openCreateForm());
220
+ saveBtn.addEventListener("click", () => void saveRepo());
221
+ cancelBtn.addEventListener("click", () => {
222
+ showForm(false);
223
+ resetForm();
224
+ });
225
+ }
@@ -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
+ }