aicodeman 1.2.1 → 1.3.0

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 (232) hide show
  1. package/README.md +264 -90
  2. package/dist/attachment-magic.d.ts +13 -0
  3. package/dist/attachment-magic.d.ts.map +1 -1
  4. package/dist/attachment-magic.js +47 -0
  5. package/dist/attachment-magic.js.map +1 -1
  6. package/dist/attachment-registry.d.ts.map +1 -1
  7. package/dist/attachment-registry.js +13 -2
  8. package/dist/attachment-registry.js.map +1 -1
  9. package/dist/config/buffer-limits.d.ts +2 -3
  10. package/dist/config/buffer-limits.d.ts.map +1 -1
  11. package/dist/config/buffer-limits.js +5 -5
  12. package/dist/config/buffer-limits.js.map +1 -1
  13. package/dist/config/map-limits.d.ts +11 -0
  14. package/dist/config/map-limits.d.ts.map +1 -1
  15. package/dist/config/map-limits.js +11 -0
  16. package/dist/config/map-limits.js.map +1 -1
  17. package/dist/config/server-timing.d.ts +6 -0
  18. package/dist/config/server-timing.d.ts.map +1 -1
  19. package/dist/config/server-timing.js +9 -0
  20. package/dist/config/server-timing.js.map +1 -1
  21. package/dist/config/terminal-history.d.ts +30 -0
  22. package/dist/config/terminal-history.d.ts.map +1 -0
  23. package/dist/config/terminal-history.js +43 -0
  24. package/dist/config/terminal-history.js.map +1 -0
  25. package/dist/cron/cron-input.d.ts +30 -0
  26. package/dist/cron/cron-input.d.ts.map +1 -0
  27. package/dist/cron/cron-input.js +7 -0
  28. package/dist/cron/cron-input.js.map +1 -0
  29. package/dist/cron/cron-service.d.ts +92 -0
  30. package/dist/cron/cron-service.d.ts.map +1 -0
  31. package/dist/cron/cron-service.js +606 -0
  32. package/dist/cron/cron-service.js.map +1 -0
  33. package/dist/cron/cron-time.d.ts +29 -0
  34. package/dist/cron/cron-time.d.ts.map +1 -0
  35. package/dist/cron/cron-time.js +87 -0
  36. package/dist/cron/cron-time.js.map +1 -0
  37. package/dist/document-thumbnailer.d.ts +1 -1
  38. package/dist/document-thumbnailer.d.ts.map +1 -1
  39. package/dist/document-thumbnailer.js +11 -2
  40. package/dist/document-thumbnailer.js.map +1 -1
  41. package/dist/generated-artifact-attachments.d.ts +26 -0
  42. package/dist/generated-artifact-attachments.d.ts.map +1 -0
  43. package/dist/generated-artifact-attachments.js +61 -0
  44. package/dist/generated-artifact-attachments.js.map +1 -0
  45. package/dist/index.js +11 -0
  46. package/dist/index.js.map +1 -1
  47. package/dist/mux-interface.d.ts +37 -5
  48. package/dist/mux-interface.d.ts.map +1 -1
  49. package/dist/remote-hosts.d.ts +66 -0
  50. package/dist/remote-hosts.d.ts.map +1 -0
  51. package/dist/remote-hosts.js +198 -0
  52. package/dist/remote-hosts.js.map +1 -0
  53. package/dist/services/unified-session-service.d.ts +115 -0
  54. package/dist/services/unified-session-service.d.ts.map +1 -0
  55. package/dist/services/unified-session-service.js +173 -0
  56. package/dist/services/unified-session-service.js.map +1 -0
  57. package/dist/session-cli-builder.d.ts +3 -1
  58. package/dist/session-cli-builder.d.ts.map +1 -1
  59. package/dist/session-cli-builder.js +26 -7
  60. package/dist/session-cli-builder.js.map +1 -1
  61. package/dist/session-pty-exit-breaker.d.ts +66 -0
  62. package/dist/session-pty-exit-breaker.d.ts.map +1 -0
  63. package/dist/session-pty-exit-breaker.js +77 -0
  64. package/dist/session-pty-exit-breaker.js.map +1 -0
  65. package/dist/session.d.ts +27 -1
  66. package/dist/session.d.ts.map +1 -1
  67. package/dist/session.js +102 -11
  68. package/dist/session.js.map +1 -1
  69. package/dist/state-store.d.ts +14 -0
  70. package/dist/state-store.d.ts.map +1 -1
  71. package/dist/state-store.js +49 -0
  72. package/dist/state-store.js.map +1 -1
  73. package/dist/tmux-manager.d.ts +100 -8
  74. package/dist/tmux-manager.d.ts.map +1 -1
  75. package/dist/tmux-manager.js +249 -26
  76. package/dist/tmux-manager.js.map +1 -1
  77. package/dist/types/api.d.ts +11 -0
  78. package/dist/types/api.d.ts.map +1 -1
  79. package/dist/types/api.js.map +1 -1
  80. package/dist/types/app-state.d.ts +5 -0
  81. package/dist/types/app-state.d.ts.map +1 -1
  82. package/dist/types/app-state.js.map +1 -1
  83. package/dist/types/cron.d.ts +88 -0
  84. package/dist/types/cron.d.ts.map +1 -0
  85. package/dist/types/cron.js +13 -0
  86. package/dist/types/cron.js.map +1 -0
  87. package/dist/types/session.d.ts +57 -0
  88. package/dist/types/session.d.ts.map +1 -1
  89. package/dist/types/session.js.map +1 -1
  90. package/dist/web/heic-jpeg-converter.d.ts +24 -0
  91. package/dist/web/heic-jpeg-converter.d.ts.map +1 -0
  92. package/dist/web/heic-jpeg-converter.js +76 -0
  93. package/dist/web/heic-jpeg-converter.js.map +1 -0
  94. package/dist/web/heic-jpeg-worker.d.ts +44 -0
  95. package/dist/web/heic-jpeg-worker.d.ts.map +1 -0
  96. package/dist/web/heic-jpeg-worker.js +78 -0
  97. package/dist/web/heic-jpeg-worker.js.map +1 -0
  98. package/dist/web/ports/config-port.d.ts +2 -0
  99. package/dist/web/ports/config-port.d.ts.map +1 -1
  100. package/dist/web/ports/cron-port.d.ts +9 -0
  101. package/dist/web/ports/cron-port.d.ts.map +1 -0
  102. package/dist/web/ports/cron-port.js +6 -0
  103. package/dist/web/ports/cron-port.js.map +1 -0
  104. package/dist/web/ports/index.d.ts +1 -0
  105. package/dist/web/ports/index.d.ts.map +1 -1
  106. package/dist/web/public/api-client.c9b1cddc.js.gz +0 -0
  107. package/dist/web/public/app.b0534652.js +38 -0
  108. package/dist/web/public/app.b0534652.js.br +0 -0
  109. package/dist/web/public/app.b0534652.js.gz +0 -0
  110. package/dist/web/public/{constants.1c779517.js → constants.5b18ed64.js} +65 -0
  111. package/dist/web/public/constants.5b18ed64.js.br +0 -0
  112. package/dist/web/public/constants.5b18ed64.js.gz +0 -0
  113. package/dist/web/public/cron-ui.js +328 -0
  114. package/dist/web/public/cron-ui.js.br +0 -0
  115. package/dist/web/public/cron-ui.js.gz +0 -0
  116. package/dist/web/public/image-input.ee16ad88.js.gz +0 -0
  117. package/dist/web/public/index.html +296 -25
  118. package/dist/web/public/index.html.br +0 -0
  119. package/dist/web/public/index.html.gz +0 -0
  120. package/dist/web/public/input-cjk.63794d0b.js +1 -0
  121. package/dist/web/public/input-cjk.63794d0b.js.br +0 -0
  122. package/dist/web/public/input-cjk.63794d0b.js.gz +0 -0
  123. package/dist/web/public/keyboard-accessory.3f91acef.js.gz +0 -0
  124. package/dist/web/public/mobile-handlers.b5a667ef.js.gz +0 -0
  125. package/dist/web/public/mobile.0644e3ac.css +1 -0
  126. package/dist/web/public/mobile.0644e3ac.css.br +0 -0
  127. package/dist/web/public/mobile.0644e3ac.css.gz +0 -0
  128. package/dist/web/public/notification-manager.ca9dc40d.js.gz +0 -0
  129. package/dist/web/public/orchestrator-panel.js.gz +0 -0
  130. package/dist/web/public/{panels-ui.772e92c9.js → panels-ui.ca03853f.js} +25 -17
  131. package/dist/web/public/panels-ui.ca03853f.js.br +0 -0
  132. package/dist/web/public/panels-ui.ca03853f.js.gz +0 -0
  133. package/dist/web/public/ralph-panel.6de2d0f8.js.gz +0 -0
  134. package/dist/web/public/ralph-wizard.13a1831e.js.gz +0 -0
  135. package/dist/web/public/respawn-ui.2d249da9.js.gz +0 -0
  136. package/dist/web/public/sanitize-html.bc7078d6.js.gz +0 -0
  137. package/dist/web/public/session-ui.b51814ef.js +49 -0
  138. package/dist/web/public/session-ui.b51814ef.js.br +0 -0
  139. package/dist/web/public/session-ui.b51814ef.js.gz +0 -0
  140. package/dist/web/public/settings-ui.6c67c125.js +67 -0
  141. package/dist/web/public/settings-ui.6c67c125.js.br +0 -0
  142. package/dist/web/public/settings-ui.6c67c125.js.gz +0 -0
  143. package/dist/web/public/styles.e1ba7849.css +1 -0
  144. package/dist/web/public/styles.e1ba7849.css.br +0 -0
  145. package/dist/web/public/styles.e1ba7849.css.gz +0 -0
  146. package/dist/web/public/subagent-windows.69b60c35.js.gz +0 -0
  147. package/dist/web/public/sw.js.gz +0 -0
  148. package/dist/web/public/terminal-ui.dbf141fa.js +3 -0
  149. package/dist/web/public/terminal-ui.dbf141fa.js.br +0 -0
  150. package/dist/web/public/terminal-ui.dbf141fa.js.gz +0 -0
  151. package/dist/web/public/ultracode-panel.js.gz +0 -0
  152. package/dist/web/public/ultracode-windows.js.gz +0 -0
  153. package/dist/web/public/upload.html.gz +0 -0
  154. package/dist/web/public/vendor/dompurify.min.js.gz +0 -0
  155. package/dist/web/public/vendor/marked.min.js.gz +0 -0
  156. package/dist/web/public/vendor/xterm-addon-fit.min.js.gz +0 -0
  157. package/dist/web/public/vendor/xterm-addon-serialize.min.js.gz +0 -0
  158. package/dist/web/public/vendor/xterm-addon-unicode11.min.js.gz +0 -0
  159. package/dist/web/public/vendor/xterm-addon-webgl.min.js.gz +0 -0
  160. package/dist/web/public/vendor/xterm-zerolag-input.137ad9f0.js.gz +0 -0
  161. package/dist/web/public/vendor/xterm.css.gz +0 -0
  162. package/dist/web/public/vendor/xterm.min.js.gz +0 -0
  163. package/dist/web/public/voice-input.085e9e73.js.gz +0 -0
  164. package/dist/web/routes/case-routes.d.ts.map +1 -1
  165. package/dist/web/routes/case-routes.js +115 -1
  166. package/dist/web/routes/case-routes.js.map +1 -1
  167. package/dist/web/routes/cron-routes.d.ts +11 -0
  168. package/dist/web/routes/cron-routes.d.ts.map +1 -0
  169. package/dist/web/routes/cron-routes.js +70 -0
  170. package/dist/web/routes/cron-routes.js.map +1 -0
  171. package/dist/web/routes/index.d.ts +1 -0
  172. package/dist/web/routes/index.d.ts.map +1 -1
  173. package/dist/web/routes/index.js +1 -0
  174. package/dist/web/routes/index.js.map +1 -1
  175. package/dist/web/routes/respawn-routes.d.ts.map +1 -1
  176. package/dist/web/routes/respawn-routes.js +3 -0
  177. package/dist/web/routes/respawn-routes.js.map +1 -1
  178. package/dist/web/routes/session-routes.d.ts.map +1 -1
  179. package/dist/web/routes/session-routes.js +614 -68
  180. package/dist/web/routes/session-routes.js.map +1 -1
  181. package/dist/web/routes/system-routes.d.ts.map +1 -1
  182. package/dist/web/routes/system-routes.js +5 -0
  183. package/dist/web/routes/system-routes.js.map +1 -1
  184. package/dist/web/routes/ws-routes.d.ts.map +1 -1
  185. package/dist/web/routes/ws-routes.js +56 -17
  186. package/dist/web/routes/ws-routes.js.map +1 -1
  187. package/dist/web/schemas.d.ts +117 -0
  188. package/dist/web/schemas.d.ts.map +1 -1
  189. package/dist/web/schemas.js +168 -1
  190. package/dist/web/schemas.js.map +1 -1
  191. package/dist/web/server.d.ts +9 -2
  192. package/dist/web/server.d.ts.map +1 -1
  193. package/dist/web/server.js +82 -13
  194. package/dist/web/server.js.map +1 -1
  195. package/dist/web/session-listener-wiring.d.ts +5 -1
  196. package/dist/web/session-listener-wiring.d.ts.map +1 -1
  197. package/dist/web/session-listener-wiring.js +23 -1
  198. package/dist/web/session-listener-wiring.js.map +1 -1
  199. package/dist/web/sse-events.d.ts +15 -0
  200. package/dist/web/sse-events.d.ts.map +1 -1
  201. package/dist/web/sse-events.js +17 -0
  202. package/dist/web/sse-events.js.map +1 -1
  203. package/dist/web/ws-connection-registry.d.ts +77 -0
  204. package/dist/web/ws-connection-registry.d.ts.map +1 -0
  205. package/dist/web/ws-connection-registry.js +99 -0
  206. package/dist/web/ws-connection-registry.js.map +1 -0
  207. package/package.json +3 -1
  208. package/dist/web/public/app.a8d73c32.js +0 -38
  209. package/dist/web/public/app.a8d73c32.js.br +0 -0
  210. package/dist/web/public/app.a8d73c32.js.gz +0 -0
  211. package/dist/web/public/constants.1c779517.js.br +0 -0
  212. package/dist/web/public/constants.1c779517.js.gz +0 -0
  213. package/dist/web/public/input-cjk.4e950e0a.js +0 -1
  214. package/dist/web/public/input-cjk.4e950e0a.js.br +0 -0
  215. package/dist/web/public/input-cjk.4e950e0a.js.gz +0 -0
  216. package/dist/web/public/mobile.8a9aff7a.css +0 -1
  217. package/dist/web/public/mobile.8a9aff7a.css.br +0 -0
  218. package/dist/web/public/mobile.8a9aff7a.css.gz +0 -0
  219. package/dist/web/public/panels-ui.772e92c9.js.br +0 -0
  220. package/dist/web/public/panels-ui.772e92c9.js.gz +0 -0
  221. package/dist/web/public/session-ui.da3e53f2.js +0 -36
  222. package/dist/web/public/session-ui.da3e53f2.js.br +0 -0
  223. package/dist/web/public/session-ui.da3e53f2.js.gz +0 -0
  224. package/dist/web/public/settings-ui.38feb956.js +0 -61
  225. package/dist/web/public/settings-ui.38feb956.js.br +0 -0
  226. package/dist/web/public/settings-ui.38feb956.js.gz +0 -0
  227. package/dist/web/public/styles.1b3e6147.css +0 -1
  228. package/dist/web/public/styles.1b3e6147.css.br +0 -0
  229. package/dist/web/public/styles.1b3e6147.css.gz +0 -0
  230. package/dist/web/public/terminal-ui.865f21f6.js +0 -3
  231. package/dist/web/public/terminal-ui.865f21f6.js.br +0 -0
  232. package/dist/web/public/terminal-ui.865f21f6.js.gz +0 -0
@@ -111,6 +111,36 @@ function evaluateWebGLLongTaskTrip(recent, entries, now, config = WEBGL_FALLBACK
111
111
  return recent.length >= config.LONGTASK_COUNT;
112
112
  }
113
113
 
114
+ /**
115
+ * Pure decision for whether to skip the WebGL renderer at terminal init, and
116
+ * whether to clear the auto-fallback sticky marker. Keeps the interaction
117
+ * between device type, URL params, the sticky marker, and the user's settings
118
+ * toggle in one testable place (terminal-ui.js calls this).
119
+ *
120
+ * Precedence (desktop only — mobile always skips):
121
+ * 1. user toggle OFF -> skip (one-shot opt-out, sticky untouched)
122
+ * 2. ?nowebgl -> skip (one-shot opt-out, sticky untouched)
123
+ * 3. ?webgl=force -> enable + clear stale sticky marker
124
+ * 4. toggle ON / untouched -> respect the auto-fallback sticky marker
125
+ *
126
+ * A stored `true` is treated like the untouched default here: the checkbox
127
+ * ships checked on desktop, so any unrelated settings save stores `true` —
128
+ * letting it clear the marker would permanently defeat the GPU-stall
129
+ * auto-fallback safety net. The marker is only retired by ?webgl=force or by
130
+ * a real OFF->ON toggle flip, which saveAppSettings() detects at save time.
131
+ *
132
+ * @param {{deviceType?: string, noWebglParam?: boolean, forceParam?: boolean,
133
+ * stickyDisabled?: boolean, userPrefEnabled?: (boolean|undefined)}} [input]
134
+ * @returns {{skip: boolean, clearSticky: boolean}}
135
+ */
136
+ function shouldSkipWebGL(input = {}) {
137
+ if (input.deviceType !== 'desktop') return { skip: true, clearSticky: false };
138
+ if (input.userPrefEnabled === false) return { skip: true, clearSticky: false };
139
+ if (input.noWebglParam) return { skip: true, clearSticky: false };
140
+ if (input.forceParam) return { skip: false, clearSticky: true };
141
+ return { skip: !!input.stickyDisabled, clearSticky: false };
142
+ }
143
+
114
144
  // Expose for tests. `const` declarations at the top of a non-module script
115
145
  // are global lexical bindings but not `window` properties, so explicit
116
146
  // assignment is the test-visible API surface.
@@ -126,12 +156,40 @@ function shouldAutoWrapTabs(input) {
126
156
  return scrollWidth > clientWidth + 1;
127
157
  }
128
158
 
159
+ // COD-134 — Terminal WebSocket reconnect policy.
160
+ //
161
+ // Decide what to do after a terminal WebSocket closes, given the close `code`
162
+ // and `attempt` (0-based count of consecutive reconnects already made):
163
+ // - transient closes (code < 4004: 1000/1001/1005/1006/etc.) → 'reconnect'
164
+ // with exponential backoff (0 on the first attempt; the caller adds jitter),
165
+ // 250ms → 500 → 1000 → ... capped at 10s.
166
+ // - 4004 (session not found) / 4009 (session terminated) → 'give-up': the
167
+ // session is gone, retrying only wastes connections.
168
+ // - 4008 (too many connections) and any other code >= 4004 → 'retry-fallback':
169
+ // show the HTTP fallback but keep retrying on a bounded 5s timer so the
170
+ // transport returns to WS once the transient condition clears (un-stick).
171
+ // Pure: no DOM, no side effects.
172
+ function planWsReconnect(code, attempt) {
173
+ if (code === 4004 || code === 4009) {
174
+ return { action: 'give-up', delayMs: 0 };
175
+ }
176
+ if (code >= 4004) {
177
+ return { action: 'retry-fallback', delayMs: 5000 };
178
+ }
179
+ const delayMs = attempt <= 0 ? 0 : Math.min(250 * Math.pow(2, attempt - 1), 10000);
180
+ return { action: 'reconnect', delayMs };
181
+ }
182
+
129
183
  if (typeof window !== 'undefined') {
130
184
  window.WEBGL_FALLBACK = WEBGL_FALLBACK;
131
185
  window.evaluateWebGLLongTaskTrip = evaluateWebGLLongTaskTrip;
186
+ window.shouldSkipWebGL = shouldSkipWebGL;
132
187
  window.CodemanTabOverflow = {
133
188
  shouldAutoWrapTabs,
134
189
  };
190
+ window.CodemanWsReconnect = {
191
+ plan: planWsReconnect,
192
+ };
135
193
  }
136
194
 
137
195
  // Scheduler API — prioritize terminal writes over background UI updates.
@@ -262,6 +320,7 @@ const SSE_EVENTS = {
262
320
  SESSION_LIMIT_PAUSE_SCHEDULED: 'session:limitPauseScheduled',
263
321
  SESSION_LIMIT_RESUME: 'session:limitResume',
264
322
  SESSION_LIMIT_RESUME_CANCELLED: 'session:limitResumeCancelled',
323
+ SESSION_RESPAWN_BREAKER_TRIPPED: 'session:respawnBreakerTripped',
265
324
  SESSION_CLI_INFO: 'session:cliInfo',
266
325
  SESSION_MESSAGE: 'session:message',
267
326
  SESSION_INTERACTIVE: 'session:interactive',
@@ -276,6 +335,12 @@ const SSE_EVENTS = {
276
335
  SCHEDULED_LOG: 'scheduled:log',
277
336
  SCHEDULED_DELETED: 'scheduled:deleted',
278
337
 
338
+ // Cron jobs
339
+ CRON_JOBS_CHANGED: 'cron:jobsChanged',
340
+ CRON_JOB_DELETED: 'cron:jobDeleted',
341
+ CRON_RUN_CREATED: 'cron:runCreated',
342
+ CRON_RUN_UPDATED: 'cron:runUpdated',
343
+
279
344
  // Respawn
280
345
  RESPAWN_STARTED: 'respawn:started',
281
346
  RESPAWN_STOPPED: 'respawn:stopped',
@@ -0,0 +1,328 @@
1
+ /**
2
+ * @fileoverview Cron Jobs UI mixed into
3
+ * CodemanApp.prototype. Renders the job list + create/edit form in the
4
+ * #cronModal, and reacts to cron:* SSE events.
5
+ *
6
+ * @mixin Extends CodemanApp.prototype via Object.assign
7
+ * @dependency app.js, api-client.js, constants.js (escapeHtml)
8
+ */
9
+
10
+ Object.assign(CodemanApp.prototype, {
11
+ // ── SSE handlers ──────────────────────────────────────────────────────────
12
+
13
+ _onCronJobsChanged(data) {
14
+ if (data && Array.isArray(data.jobs)) {
15
+ this._cronJobs = data.jobs;
16
+ if (this._isCronOpen()) this.renderCronJobs();
17
+ } else if (this._isCronOpen()) {
18
+ this.refreshCron();
19
+ }
20
+ },
21
+
22
+ _onCronRunChanged() {
23
+ // A run's status changed — refresh the list so lastStatus stays current.
24
+ if (this._isCronOpen()) this.refreshCron();
25
+ },
26
+
27
+ // ── Modal open/close ──────────────────────────────────────────────────────
28
+
29
+ _isCronOpen() {
30
+ const el = document.getElementById('cronModal');
31
+ return !!el && el.classList.contains('active');
32
+ },
33
+
34
+ openCron() {
35
+ const el = document.getElementById('cronModal');
36
+ if (!el) return;
37
+ el.classList.add('active');
38
+ this.cancelCronJobForm();
39
+ this.refreshCron();
40
+ },
41
+
42
+ closeCron() {
43
+ const el = document.getElementById('cronModal');
44
+ if (el) el.classList.remove('active');
45
+ },
46
+
47
+ async refreshCron() {
48
+ const jobs = await this._apiJson('/api/cron/jobs');
49
+ this._cronJobs = Array.isArray(jobs) ? jobs : [];
50
+ this.renderCronJobs();
51
+ },
52
+
53
+ // ── List rendering ────────────────────────────────────────────────────────
54
+
55
+ renderCronJobs() {
56
+ const list = document.getElementById('cronJobList');
57
+ if (!list) return;
58
+ const jobs = this._cronJobs || [];
59
+ if (jobs.length === 0) {
60
+ list.innerHTML = '<div class="form-hint">No cron jobs yet. Click “+ New Job”.</div>';
61
+ return;
62
+ }
63
+ const rows = jobs.map((j) => {
64
+ const next = j.enabled ? this._fmtTime(j.nextRunAt) : '—';
65
+ const last = this._fmtTime(j.lastRunAt);
66
+ const status = j.lastStatus ? escapeHtml(j.lastStatus) : '—';
67
+ return `
68
+ <div class="cron-job-row">
69
+ <div class="cron-job-main">
70
+ <div class="cron-job-name">${escapeHtml(j.name || '(unnamed)')}
71
+ <span class="cron-badge">${escapeHtml(j.agentType)}</span>
72
+ <span class="cron-badge">${escapeHtml(this._fmtSchedule(j))}</span>
73
+ ${j.enabled ? '' : '<span class="cron-badge cron-badge-off">disabled</span>'}
74
+ </div>
75
+ <div class="cron-job-meta">
76
+ <span title="${escapeHtml(j.workingDir || '')}">${escapeHtml(j.workingDir || '')}</span>
77
+ · next: ${escapeHtml(next)} · last: ${escapeHtml(last)} · status: ${status}
78
+ </div>
79
+ </div>
80
+ <div class="cron-job-actions">
81
+ <button class="btn-toolbar btn-sm btn-primary" onclick="app.runCronJob('${j.id}')">Run Now</button>
82
+ <button class="btn-toolbar btn-sm" onclick="app.toggleCronJob('${j.id}', ${j.enabled ? 'false' : 'true'})">${j.enabled ? 'Disable' : 'Enable'}</button>
83
+ <button class="btn-toolbar btn-sm" onclick="app.editCronJob('${j.id}')">Edit</button>
84
+ <button class="btn-toolbar btn-sm btn-danger" onclick="app.deleteCronJob('${j.id}')">Delete</button>
85
+ </div>
86
+ </div>`;
87
+ });
88
+ list.innerHTML = rows.join('');
89
+ },
90
+
91
+ _fmtTime(ts) {
92
+ if (!ts) return '—';
93
+ try {
94
+ return new Date(ts).toLocaleString();
95
+ } catch {
96
+ return '—';
97
+ }
98
+ },
99
+
100
+ _fmtSchedule(j) {
101
+ switch (j.scheduleType) {
102
+ case 'once':
103
+ return 'once';
104
+ case 'interval':
105
+ return `every ${j.intervalMinutes}m`;
106
+ case 'daily':
107
+ return `daily ${j.dailyTime || ''}`;
108
+ case 'weekly': {
109
+ const names = ['Sun', 'Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat'];
110
+ const days = (j.weeklyDays || []).map((d) => names[d] || d).join(',');
111
+ return `weekly ${days} ${j.weeklyTime || ''}`;
112
+ }
113
+ default:
114
+ return j.scheduleType || '';
115
+ }
116
+ },
117
+
118
+ // ── Create / edit form ────────────────────────────────────────────────────
119
+
120
+ openCronJobForm(job) {
121
+ const form = document.getElementById('cronJobForm');
122
+ if (!form) return;
123
+ document.getElementById('cronFormError').textContent = '';
124
+ document.getElementById('cronFormTitle').textContent = job ? 'Edit Cron Job' : 'New Cron Job';
125
+ document.getElementById('schJobId').value = job ? job.id : '';
126
+ document.getElementById('schName').value = job ? job.name || '' : '';
127
+ document.getElementById('schAgentType').value = job ? job.agentType || 'claude' : 'claude';
128
+ document.getElementById('schWorkingDir').value = job ? job.workingDir || '' : '';
129
+ document.getElementById('schLaunchCommand').value = job ? job.launchCommand || '' : '';
130
+ document.getElementById('schPromptMode').value = job ? job.promptMode || 'inline_text' : 'inline_text';
131
+ document.getElementById('schPromptText').value = job ? job.promptText || '' : '';
132
+ document.getElementById('schPromptFilePath').value = job ? job.promptFilePath || '' : '';
133
+ document.getElementById('schInputMode').value = job ? job.inputMode || 'typed' : 'typed';
134
+ document.getElementById('schScheduleType').value = job ? job.scheduleType || 'once' : 'once';
135
+ document.getElementById('schRunAt').value = job && job.runAt ? this._toLocalInput(job.runAt) : '';
136
+ document.getElementById('schIntervalMinutes').value = job && job.intervalMinutes ? job.intervalMinutes : 60;
137
+ document.getElementById('schDailyTime').value = job ? job.dailyTime || '' : '';
138
+ document.getElementById('schWeeklyTime').value = job ? job.weeklyTime || '' : '';
139
+ const weekly = (job && job.weeklyDays) || [];
140
+ document.querySelectorAll('#schWeeklyDays input[type=checkbox]').forEach((cb) => {
141
+ cb.checked = weekly.includes(Number(cb.value));
142
+ });
143
+ document.getElementById('schConcurrencyPolicy').value = job ? job.concurrencyPolicy || 'warn_only' : 'warn_only';
144
+ document.getElementById('schAutoClosePrev').checked = job ? job.autoClosePreviousSession !== false : true;
145
+ document.getElementById('schEnabled').checked = job ? !!job.enabled : true;
146
+ document.getElementById('schNotes').value = job ? job.notes || '' : '';
147
+
148
+ this.onCronAgentTypeChange();
149
+ this.onCronPromptModeChange();
150
+ this.onCronScheduleTypeChange();
151
+ form.classList.remove('hidden');
152
+ },
153
+
154
+ editCronJob(id) {
155
+ const job = (this._cronJobs || []).find((j) => j.id === id);
156
+ if (job) this.openCronJobForm(job);
157
+ },
158
+
159
+ cancelCronJobForm() {
160
+ const form = document.getElementById('cronJobForm');
161
+ if (form) form.classList.add('hidden');
162
+ },
163
+
164
+ onCronAgentTypeChange() {
165
+ // Launch command is only meaningful for shell mode (first input line).
166
+ const isShell = document.getElementById('schAgentType').value === 'shell';
167
+ document.getElementById('schLaunchCommandRow').classList.toggle('hidden', !isShell);
168
+ },
169
+
170
+ onCronPromptModeChange() {
171
+ const mode = document.getElementById('schPromptMode').value;
172
+ document.getElementById('schPromptTextRow').classList.toggle('hidden', mode !== 'inline_text');
173
+ document.getElementById('schPromptFileRow').classList.toggle('hidden', mode !== 'prompt_file_path');
174
+ },
175
+
176
+ onCronScheduleTypeChange() {
177
+ const t = document.getElementById('schScheduleType').value;
178
+ document.getElementById('schRunAtRow').classList.toggle('hidden', t !== 'once');
179
+ document.getElementById('schIntervalRow').classList.toggle('hidden', t !== 'interval');
180
+ document.getElementById('schDailyRow').classList.toggle('hidden', t !== 'daily');
181
+ document.getElementById('schWeeklyDaysRow').classList.toggle('hidden', t !== 'weekly');
182
+ document.getElementById('schWeeklyTimeRow').classList.toggle('hidden', t !== 'weekly');
183
+ },
184
+
185
+ _toLocalInput(ts) {
186
+ // epoch-ms → 'YYYY-MM-DDTHH:MM' in local time for <input datetime-local>.
187
+ const d = new Date(ts);
188
+ const pad = (n) => String(n).padStart(2, '0');
189
+ return `${d.getFullYear()}-${pad(d.getMonth() + 1)}-${pad(d.getDate())}T${pad(d.getHours())}:${pad(d.getMinutes())}`;
190
+ },
191
+
192
+ _collectCronForm() {
193
+ const t = document.getElementById('schScheduleType').value;
194
+ const promptMode = document.getElementById('schPromptMode').value;
195
+ const body = {
196
+ name: document.getElementById('schName').value.trim(),
197
+ agentType: document.getElementById('schAgentType').value,
198
+ workingDir: document.getElementById('schWorkingDir').value.trim(),
199
+ promptMode,
200
+ inputMode: document.getElementById('schInputMode').value,
201
+ scheduleType: t,
202
+ concurrencyPolicy: document.getElementById('schConcurrencyPolicy').value,
203
+ autoClosePreviousSession: document.getElementById('schAutoClosePrev').checked,
204
+ enabled: document.getElementById('schEnabled').checked,
205
+ notes: document.getElementById('schNotes').value.trim() || undefined,
206
+ };
207
+ // Always sent for shell (an emptied field must clear a saved command on edit).
208
+ if (body.agentType === 'shell') body.launchCommand = document.getElementById('schLaunchCommand').value.trim();
209
+ if (promptMode === 'inline_text') {
210
+ // Prompt delivery is single-line only; trailing newlines are harmless, strip them.
211
+ body.promptText = document.getElementById('schPromptText').value.replace(/[\r\n]+$/, '');
212
+ } else {
213
+ body.promptFilePath = document.getElementById('schPromptFilePath').value.trim();
214
+ }
215
+
216
+ if (t === 'once') {
217
+ const v = document.getElementById('schRunAt').value;
218
+ body.runAt = v ? new Date(v).getTime() : undefined;
219
+ } else if (t === 'interval') {
220
+ body.intervalMinutes = Number(document.getElementById('schIntervalMinutes').value);
221
+ } else if (t === 'daily') {
222
+ body.dailyTime = document.getElementById('schDailyTime').value;
223
+ } else if (t === 'weekly') {
224
+ body.weeklyTime = document.getElementById('schWeeklyTime').value;
225
+ body.weeklyDays = Array.from(document.querySelectorAll('#schWeeklyDays input:checked')).map((cb) =>
226
+ Number(cb.value)
227
+ );
228
+ }
229
+ return body;
230
+ },
231
+
232
+ async saveCronJob() {
233
+ const errEl = document.getElementById('cronFormError');
234
+ errEl.textContent = '';
235
+ const body = this._collectCronForm();
236
+ if (!body.name) {
237
+ errEl.textContent = 'Name is required.';
238
+ return;
239
+ }
240
+ if (!body.workingDir) {
241
+ errEl.textContent = 'Working directory is required.';
242
+ return;
243
+ }
244
+ if (body.promptText !== undefined && /[\r\n]/.test(body.promptText)) {
245
+ errEl.textContent = 'Prompt must be a single line — multi-line prompts are not supported.';
246
+ return;
247
+ }
248
+ const id = document.getElementById('schJobId').value;
249
+ const res = id ? await this._apiPut(`/api/cron/jobs/${id}`, body) : await this._apiPost('/api/cron/jobs', body);
250
+ if (!res || !res.ok) {
251
+ let msg = 'Failed to save job.';
252
+ try {
253
+ const j = await res.json();
254
+ if (j && j.error) msg = j.error;
255
+ } catch {
256
+ /* ignore */
257
+ }
258
+ errEl.textContent = msg;
259
+ return;
260
+ }
261
+ this.showToast?.(id ? 'Cron job updated' : 'Cron job created', 'success');
262
+ this.cancelCronJobForm();
263
+ this.refreshCron();
264
+ },
265
+
266
+ // ── Actions ───────────────────────────────────────────────────────────────
267
+
268
+ async runCronJob(id) {
269
+ const job = (this._cronJobs || []).find((j) => j.id === id);
270
+ if (job) {
271
+ const active = this._countActiveAgents(job.agentType);
272
+ if (active > 0 && !confirm(`${active} ${job.agentType} session(s) already active. Run this job anyway?`)) {
273
+ return;
274
+ }
275
+ }
276
+ const res = await this._apiPost(`/api/cron/jobs/${id}/run`, {});
277
+ if (res && res.ok) {
278
+ this.showToast?.('Run started — opening session', 'success');
279
+ let data = null;
280
+ try {
281
+ data = await res.json();
282
+ } catch {
283
+ /* ignore */
284
+ }
285
+ const run = data && (data.data ? data.data.run : data.run);
286
+ if (run && run.sessionId) this._focusCronSession(run.sessionId);
287
+ this.refreshCron();
288
+ } else {
289
+ this.showToast?.('Failed to run job', 'error');
290
+ }
291
+ },
292
+
293
+ _focusCronSession(sessionId) {
294
+ // Best-effort: switch to the created session tab if it exists.
295
+ if (this.sessions && this.sessions.has(sessionId) && typeof this.switchSession === 'function') {
296
+ this.closeCron();
297
+ this.switchSession(sessionId);
298
+ }
299
+ },
300
+
301
+ _countActiveAgents(agentType) {
302
+ // Mirrors the server's countActiveAgents: only LIVE sessions count — a
303
+ // tab whose CLI already exited (stopped/error) doesn't block anything.
304
+ if (!this.sessions) return 0;
305
+ let n = 0;
306
+ for (const s of this.sessions.values()) {
307
+ if (s && s.mode === agentType && s.status !== 'stopped' && s.status !== 'error') n++;
308
+ }
309
+ return n;
310
+ },
311
+
312
+ async toggleCronJob(id, enabled) {
313
+ const res = await this._apiPut(`/api/cron/jobs/${id}/enabled`, { enabled });
314
+ if (res && res.ok) this.refreshCron();
315
+ else this.showToast?.('Failed to update job', 'error');
316
+ },
317
+
318
+ async deleteCronJob(id) {
319
+ if (!confirm('Delete this cron job and its run history?')) return;
320
+ const res = await this._apiDelete(`/api/cron/jobs/${id}`);
321
+ if (res && res.ok) {
322
+ this.showToast?.('Cron job deleted', 'success');
323
+ this.refreshCron();
324
+ } else {
325
+ this.showToast?.('Failed to delete job', 'error');
326
+ }
327
+ },
328
+ });
Binary file
Binary file