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.
- package/README.md +264 -90
- package/dist/attachment-magic.d.ts +13 -0
- package/dist/attachment-magic.d.ts.map +1 -1
- package/dist/attachment-magic.js +47 -0
- package/dist/attachment-magic.js.map +1 -1
- package/dist/attachment-registry.d.ts.map +1 -1
- package/dist/attachment-registry.js +13 -2
- package/dist/attachment-registry.js.map +1 -1
- package/dist/config/buffer-limits.d.ts +2 -3
- package/dist/config/buffer-limits.d.ts.map +1 -1
- package/dist/config/buffer-limits.js +5 -5
- package/dist/config/buffer-limits.js.map +1 -1
- package/dist/config/map-limits.d.ts +11 -0
- package/dist/config/map-limits.d.ts.map +1 -1
- package/dist/config/map-limits.js +11 -0
- package/dist/config/map-limits.js.map +1 -1
- package/dist/config/server-timing.d.ts +6 -0
- package/dist/config/server-timing.d.ts.map +1 -1
- package/dist/config/server-timing.js +9 -0
- package/dist/config/server-timing.js.map +1 -1
- package/dist/config/terminal-history.d.ts +30 -0
- package/dist/config/terminal-history.d.ts.map +1 -0
- package/dist/config/terminal-history.js +43 -0
- package/dist/config/terminal-history.js.map +1 -0
- package/dist/cron/cron-input.d.ts +30 -0
- package/dist/cron/cron-input.d.ts.map +1 -0
- package/dist/cron/cron-input.js +7 -0
- package/dist/cron/cron-input.js.map +1 -0
- package/dist/cron/cron-service.d.ts +92 -0
- package/dist/cron/cron-service.d.ts.map +1 -0
- package/dist/cron/cron-service.js +606 -0
- package/dist/cron/cron-service.js.map +1 -0
- package/dist/cron/cron-time.d.ts +29 -0
- package/dist/cron/cron-time.d.ts.map +1 -0
- package/dist/cron/cron-time.js +87 -0
- package/dist/cron/cron-time.js.map +1 -0
- package/dist/document-thumbnailer.d.ts +1 -1
- package/dist/document-thumbnailer.d.ts.map +1 -1
- package/dist/document-thumbnailer.js +11 -2
- package/dist/document-thumbnailer.js.map +1 -1
- package/dist/generated-artifact-attachments.d.ts +26 -0
- package/dist/generated-artifact-attachments.d.ts.map +1 -0
- package/dist/generated-artifact-attachments.js +61 -0
- package/dist/generated-artifact-attachments.js.map +1 -0
- package/dist/index.js +11 -0
- package/dist/index.js.map +1 -1
- package/dist/mux-interface.d.ts +37 -5
- package/dist/mux-interface.d.ts.map +1 -1
- package/dist/remote-hosts.d.ts +66 -0
- package/dist/remote-hosts.d.ts.map +1 -0
- package/dist/remote-hosts.js +198 -0
- package/dist/remote-hosts.js.map +1 -0
- package/dist/services/unified-session-service.d.ts +115 -0
- package/dist/services/unified-session-service.d.ts.map +1 -0
- package/dist/services/unified-session-service.js +173 -0
- package/dist/services/unified-session-service.js.map +1 -0
- package/dist/session-cli-builder.d.ts +3 -1
- package/dist/session-cli-builder.d.ts.map +1 -1
- package/dist/session-cli-builder.js +26 -7
- package/dist/session-cli-builder.js.map +1 -1
- package/dist/session-pty-exit-breaker.d.ts +66 -0
- package/dist/session-pty-exit-breaker.d.ts.map +1 -0
- package/dist/session-pty-exit-breaker.js +77 -0
- package/dist/session-pty-exit-breaker.js.map +1 -0
- package/dist/session.d.ts +27 -1
- package/dist/session.d.ts.map +1 -1
- package/dist/session.js +102 -11
- package/dist/session.js.map +1 -1
- package/dist/state-store.d.ts +14 -0
- package/dist/state-store.d.ts.map +1 -1
- package/dist/state-store.js +49 -0
- package/dist/state-store.js.map +1 -1
- package/dist/tmux-manager.d.ts +100 -8
- package/dist/tmux-manager.d.ts.map +1 -1
- package/dist/tmux-manager.js +249 -26
- package/dist/tmux-manager.js.map +1 -1
- package/dist/types/api.d.ts +11 -0
- package/dist/types/api.d.ts.map +1 -1
- package/dist/types/api.js.map +1 -1
- package/dist/types/app-state.d.ts +5 -0
- package/dist/types/app-state.d.ts.map +1 -1
- package/dist/types/app-state.js.map +1 -1
- package/dist/types/cron.d.ts +88 -0
- package/dist/types/cron.d.ts.map +1 -0
- package/dist/types/cron.js +13 -0
- package/dist/types/cron.js.map +1 -0
- package/dist/types/session.d.ts +57 -0
- package/dist/types/session.d.ts.map +1 -1
- package/dist/types/session.js.map +1 -1
- package/dist/web/heic-jpeg-converter.d.ts +24 -0
- package/dist/web/heic-jpeg-converter.d.ts.map +1 -0
- package/dist/web/heic-jpeg-converter.js +76 -0
- package/dist/web/heic-jpeg-converter.js.map +1 -0
- package/dist/web/heic-jpeg-worker.d.ts +44 -0
- package/dist/web/heic-jpeg-worker.d.ts.map +1 -0
- package/dist/web/heic-jpeg-worker.js +78 -0
- package/dist/web/heic-jpeg-worker.js.map +1 -0
- package/dist/web/ports/config-port.d.ts +2 -0
- package/dist/web/ports/config-port.d.ts.map +1 -1
- package/dist/web/ports/cron-port.d.ts +9 -0
- package/dist/web/ports/cron-port.d.ts.map +1 -0
- package/dist/web/ports/cron-port.js +6 -0
- package/dist/web/ports/cron-port.js.map +1 -0
- package/dist/web/ports/index.d.ts +1 -0
- package/dist/web/ports/index.d.ts.map +1 -1
- package/dist/web/public/api-client.c9b1cddc.js.gz +0 -0
- package/dist/web/public/app.b0534652.js +38 -0
- package/dist/web/public/app.b0534652.js.br +0 -0
- package/dist/web/public/app.b0534652.js.gz +0 -0
- package/dist/web/public/{constants.1c779517.js → constants.5b18ed64.js} +65 -0
- package/dist/web/public/constants.5b18ed64.js.br +0 -0
- package/dist/web/public/constants.5b18ed64.js.gz +0 -0
- package/dist/web/public/cron-ui.js +328 -0
- package/dist/web/public/cron-ui.js.br +0 -0
- package/dist/web/public/cron-ui.js.gz +0 -0
- package/dist/web/public/image-input.ee16ad88.js.gz +0 -0
- package/dist/web/public/index.html +296 -25
- package/dist/web/public/index.html.br +0 -0
- package/dist/web/public/index.html.gz +0 -0
- package/dist/web/public/input-cjk.63794d0b.js +1 -0
- package/dist/web/public/input-cjk.63794d0b.js.br +0 -0
- package/dist/web/public/input-cjk.63794d0b.js.gz +0 -0
- package/dist/web/public/keyboard-accessory.3f91acef.js.gz +0 -0
- package/dist/web/public/mobile-handlers.b5a667ef.js.gz +0 -0
- package/dist/web/public/mobile.0644e3ac.css +1 -0
- package/dist/web/public/mobile.0644e3ac.css.br +0 -0
- package/dist/web/public/mobile.0644e3ac.css.gz +0 -0
- package/dist/web/public/notification-manager.ca9dc40d.js.gz +0 -0
- package/dist/web/public/orchestrator-panel.js.gz +0 -0
- package/dist/web/public/{panels-ui.772e92c9.js → panels-ui.ca03853f.js} +25 -17
- package/dist/web/public/panels-ui.ca03853f.js.br +0 -0
- package/dist/web/public/panels-ui.ca03853f.js.gz +0 -0
- package/dist/web/public/ralph-panel.6de2d0f8.js.gz +0 -0
- package/dist/web/public/ralph-wizard.13a1831e.js.gz +0 -0
- package/dist/web/public/respawn-ui.2d249da9.js.gz +0 -0
- package/dist/web/public/sanitize-html.bc7078d6.js.gz +0 -0
- package/dist/web/public/session-ui.b51814ef.js +49 -0
- package/dist/web/public/session-ui.b51814ef.js.br +0 -0
- package/dist/web/public/session-ui.b51814ef.js.gz +0 -0
- package/dist/web/public/settings-ui.6c67c125.js +67 -0
- package/dist/web/public/settings-ui.6c67c125.js.br +0 -0
- package/dist/web/public/settings-ui.6c67c125.js.gz +0 -0
- package/dist/web/public/styles.e1ba7849.css +1 -0
- package/dist/web/public/styles.e1ba7849.css.br +0 -0
- package/dist/web/public/styles.e1ba7849.css.gz +0 -0
- package/dist/web/public/subagent-windows.69b60c35.js.gz +0 -0
- package/dist/web/public/sw.js.gz +0 -0
- package/dist/web/public/terminal-ui.dbf141fa.js +3 -0
- package/dist/web/public/terminal-ui.dbf141fa.js.br +0 -0
- package/dist/web/public/terminal-ui.dbf141fa.js.gz +0 -0
- package/dist/web/public/ultracode-panel.js.gz +0 -0
- package/dist/web/public/ultracode-windows.js.gz +0 -0
- package/dist/web/public/upload.html.gz +0 -0
- package/dist/web/public/vendor/dompurify.min.js.gz +0 -0
- package/dist/web/public/vendor/marked.min.js.gz +0 -0
- package/dist/web/public/vendor/xterm-addon-fit.min.js.gz +0 -0
- package/dist/web/public/vendor/xterm-addon-serialize.min.js.gz +0 -0
- package/dist/web/public/vendor/xterm-addon-unicode11.min.js.gz +0 -0
- package/dist/web/public/vendor/xterm-addon-webgl.min.js.gz +0 -0
- package/dist/web/public/vendor/xterm-zerolag-input.137ad9f0.js.gz +0 -0
- package/dist/web/public/vendor/xterm.css.gz +0 -0
- package/dist/web/public/vendor/xterm.min.js.gz +0 -0
- package/dist/web/public/voice-input.085e9e73.js.gz +0 -0
- package/dist/web/routes/case-routes.d.ts.map +1 -1
- package/dist/web/routes/case-routes.js +115 -1
- package/dist/web/routes/case-routes.js.map +1 -1
- package/dist/web/routes/cron-routes.d.ts +11 -0
- package/dist/web/routes/cron-routes.d.ts.map +1 -0
- package/dist/web/routes/cron-routes.js +70 -0
- package/dist/web/routes/cron-routes.js.map +1 -0
- package/dist/web/routes/index.d.ts +1 -0
- package/dist/web/routes/index.d.ts.map +1 -1
- package/dist/web/routes/index.js +1 -0
- package/dist/web/routes/index.js.map +1 -1
- package/dist/web/routes/respawn-routes.d.ts.map +1 -1
- package/dist/web/routes/respawn-routes.js +3 -0
- package/dist/web/routes/respawn-routes.js.map +1 -1
- package/dist/web/routes/session-routes.d.ts.map +1 -1
- package/dist/web/routes/session-routes.js +614 -68
- package/dist/web/routes/session-routes.js.map +1 -1
- package/dist/web/routes/system-routes.d.ts.map +1 -1
- package/dist/web/routes/system-routes.js +5 -0
- package/dist/web/routes/system-routes.js.map +1 -1
- package/dist/web/routes/ws-routes.d.ts.map +1 -1
- package/dist/web/routes/ws-routes.js +56 -17
- package/dist/web/routes/ws-routes.js.map +1 -1
- package/dist/web/schemas.d.ts +117 -0
- package/dist/web/schemas.d.ts.map +1 -1
- package/dist/web/schemas.js +168 -1
- package/dist/web/schemas.js.map +1 -1
- package/dist/web/server.d.ts +9 -2
- package/dist/web/server.d.ts.map +1 -1
- package/dist/web/server.js +82 -13
- package/dist/web/server.js.map +1 -1
- package/dist/web/session-listener-wiring.d.ts +5 -1
- package/dist/web/session-listener-wiring.d.ts.map +1 -1
- package/dist/web/session-listener-wiring.js +23 -1
- package/dist/web/session-listener-wiring.js.map +1 -1
- package/dist/web/sse-events.d.ts +15 -0
- package/dist/web/sse-events.d.ts.map +1 -1
- package/dist/web/sse-events.js +17 -0
- package/dist/web/sse-events.js.map +1 -1
- package/dist/web/ws-connection-registry.d.ts +77 -0
- package/dist/web/ws-connection-registry.d.ts.map +1 -0
- package/dist/web/ws-connection-registry.js +99 -0
- package/dist/web/ws-connection-registry.js.map +1 -0
- package/package.json +3 -1
- package/dist/web/public/app.a8d73c32.js +0 -38
- package/dist/web/public/app.a8d73c32.js.br +0 -0
- package/dist/web/public/app.a8d73c32.js.gz +0 -0
- package/dist/web/public/constants.1c779517.js.br +0 -0
- package/dist/web/public/constants.1c779517.js.gz +0 -0
- package/dist/web/public/input-cjk.4e950e0a.js +0 -1
- package/dist/web/public/input-cjk.4e950e0a.js.br +0 -0
- package/dist/web/public/input-cjk.4e950e0a.js.gz +0 -0
- package/dist/web/public/mobile.8a9aff7a.css +0 -1
- package/dist/web/public/mobile.8a9aff7a.css.br +0 -0
- package/dist/web/public/mobile.8a9aff7a.css.gz +0 -0
- package/dist/web/public/panels-ui.772e92c9.js.br +0 -0
- package/dist/web/public/panels-ui.772e92c9.js.gz +0 -0
- package/dist/web/public/session-ui.da3e53f2.js +0 -36
- package/dist/web/public/session-ui.da3e53f2.js.br +0 -0
- package/dist/web/public/session-ui.da3e53f2.js.gz +0 -0
- package/dist/web/public/settings-ui.38feb956.js +0 -61
- package/dist/web/public/settings-ui.38feb956.js.br +0 -0
- package/dist/web/public/settings-ui.38feb956.js.gz +0 -0
- package/dist/web/public/styles.1b3e6147.css +0 -1
- package/dist/web/public/styles.1b3e6147.css.br +0 -0
- package/dist/web/public/styles.1b3e6147.css.gz +0 -0
- package/dist/web/public/terminal-ui.865f21f6.js +0 -3
- package/dist/web/public/terminal-ui.865f21f6.js.br +0 -0
- 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',
|
|
Binary file
|
|
Binary file
|
|
@@ -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
|
|
Binary file
|