diff --git a/CHANGELOG.md b/CHANGELOG.md index dfd5d62c..2afea7cb 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -3,6 +3,11 @@ ## [Unreleased] +## [v0.51.332] — 2026-06-08 — Release KV (distinguish script cron jobs in Tasks) + +### Fixed +- **Script cron jobs (`no_agent`) in the Tasks panel no longer show an empty Prompt card that looked like missing configuration.** Script jobs now display a 📜 script badge (next to the agent 🤖 badge), a "Script" banner, the script path + working directory, and "Script output" run-history labels distinct from agent jobs. (#3589, @pamnard) + ## [v0.51.331] — 2026-06-08 — Release KU (dismissible error toasts) ### Fixed diff --git a/static/i18n.js b/static/i18n.js index c0778181..9ffb9830 100644 --- a/static/i18n.js +++ b/static/i18n.js @@ -1148,6 +1148,16 @@ const LOCALES = { cron_collapse_prompt: 'Collapse prompt', cron_expand_output: 'Expand output', cron_collapse_output: 'Collapse output', + cron_mode_agent: 'Agent', + cron_mode_script: 'Script', + cron_mode_label: 'Mode', + cron_script_job_banner: 'Runs a script on schedule — stdout is delivered to the target. No agent, prompt, or skills.', + cron_script_card_title: 'Script', + cron_script_output: 'Script output', + cron_script_path_label: 'Script path', + cron_script_path_hint: 'Resolved under ~/.hermes/scripts/ unless an absolute path. Edit the script file on the server to change behavior.', + cron_script_badge_title: 'Script job (no agent)', + cron_workdir_label: 'Working directory', cron_view_full_output: 'View full output', cron_all_runs: 'All runs', cron_hide_runs: 'Hide runs', @@ -2513,6 +2523,16 @@ const LOCALES = { cron_collapse_prompt: 'Comprimi prompt', cron_expand_output: 'Espandi output', cron_collapse_output: 'Comprimi output', + cron_mode_agent: 'Agente', + cron_mode_script: 'Script', + cron_mode_label: 'Modalità', + cron_script_job_banner: 'Esegue uno script a intervalli — stdout viene consegnato alla destinazione. Nessun agente, prompt o skill.', + cron_script_card_title: 'Script', + cron_script_output: 'Output script', + cron_script_path_label: 'Percorso script', + cron_script_path_hint: 'Risolto sotto ~/.hermes/scripts/ salvo percorso assoluto. Modifica lo script sul server per cambiare il comportamento.', + cron_script_badge_title: 'Job script (senza agente)', + cron_workdir_label: 'Directory di lavoro', cron_view_full_output: 'View full output', cron_all_runs: 'Tutte le esecuzioni', cron_hide_runs: 'Nascondi esecuzioni', @@ -3883,6 +3903,16 @@ const LOCALES = { cron_collapse_prompt: 'プロンプトを折りたたむ', cron_expand_output: '出力を展開', cron_collapse_output: '出力を折りたたむ', + cron_mode_agent: 'エージェント', + cron_mode_script: 'スクリプト', + cron_mode_label: 'モード', + cron_script_job_banner: 'スケジュールでスクリプトを実行し、stdout を配信先へ送ります。エージェント・プロンプト・スキルは使いません。', + cron_script_card_title: 'スクリプト', + cron_script_output: 'スクリプト出力', + cron_script_path_label: 'スクリプトパス', + cron_script_path_hint: '絶対パスでない限り ~/.hermes/scripts/ から解決されます。動作を変えるにはサーバー上のスクリプトを編集してください。', + cron_script_badge_title: 'スクリプトジョブ(エージェントなし)', + cron_workdir_label: '作業ディレクトリ', cron_view_full_output: '全出力を表示', cron_all_runs: 'すべての実行', cron_hide_runs: '実行履歴を隠す', @@ -4967,6 +4997,16 @@ const LOCALES = { cron_collapse_prompt: 'Свернуть промпт', cron_expand_output: 'Развернуть вывод', cron_collapse_output: 'Свернуть вывод', + cron_mode_agent: 'Агент', + cron_mode_script: 'Скрипт', + cron_mode_label: 'Режим', + cron_script_job_banner: 'По расписанию запускается скрипт — stdout доставляется в канал. Агент, промпт и skills не используются.', + cron_script_card_title: 'Скрипт', + cron_script_output: 'Вывод скрипта', + cron_script_path_label: 'Путь к скрипту', + cron_script_path_hint: 'Ищется в ~/.hermes/scripts/, если не указан абсолютный путь. Логику меняй в файле скрипта на сервере.', + cron_script_badge_title: 'Скриптовая задача (без агента)', + cron_workdir_label: 'Рабочая директория', cron_view_full_output: 'View full output', cron_all_runs: 'Все запуски', cron_hide_runs: 'Скрыть запуски', @@ -6274,6 +6314,16 @@ const LOCALES = { cron_collapse_prompt: 'Collapse prompt', cron_expand_output: 'Expand output', cron_collapse_output: 'Collapse output', + cron_mode_agent: 'Agent', + cron_mode_script: 'Script', + cron_mode_label: 'Mode', + cron_script_job_banner: 'Runs a script on schedule — stdout is delivered to the target. No agent, prompt, or skills.', + cron_script_card_title: 'Script', + cron_script_output: 'Script output', + cron_script_path_label: 'Script path', + cron_script_path_hint: 'Resolved under ~/.hermes/scripts/ unless an absolute path. Edit the script file on the server to change behavior.', + cron_script_badge_title: 'Script job (no agent)', + cron_workdir_label: 'Working directory', cron_view_full_output: 'View full output', cron_all_runs: 'All runs', cron_hide_runs: 'Hide runs', @@ -7784,6 +7834,16 @@ const LOCALES = { cron_collapse_prompt: 'Prompt einklappen', cron_expand_output: 'Ausgabe erweitern', cron_collapse_output: 'Ausgabe einklappen', + cron_mode_agent: 'Agent', + cron_mode_script: 'Skript', + cron_mode_label: 'Modus', + cron_script_job_banner: 'Führt ein Skript nach Plan aus — stdout wird zugestellt. Kein Agent, Prompt oder Skills.', + cron_script_card_title: 'Skript', + cron_script_output: 'Skriptausgabe', + cron_script_path_label: 'Skriptpfad', + cron_script_path_hint: 'Wird unter ~/.hermes/scripts/ aufgelöst, sofern kein absoluter Pfad. Verhalten über die Skriptdatei auf dem Server ändern.', + cron_script_badge_title: 'Skript-Job (ohne Agent)', + cron_workdir_label: 'Arbeitsverzeichnis', cron_view_full_output: 'View full output', cron_all_runs: 'Alle Ausführungen', cron_hide_runs: 'Ausführungen ausblenden', @@ -8890,6 +8950,16 @@ const LOCALES = { cron_collapse_prompt: '收起提示词', cron_expand_output: '展开输出', cron_collapse_output: '收起输出', + cron_mode_agent: '代理', + cron_mode_script: '脚本', + cron_mode_label: '模式', + cron_script_job_banner: '按计划运行脚本,stdout 会投递到目标。不使用代理、提示词或技能。', + cron_script_card_title: '脚本', + cron_script_output: '脚本输出', + cron_script_path_label: '脚本路径', + cron_script_path_hint: '除非是绝对路径,否则在 ~/.hermes/scripts/ 下解析。请在服务器上编辑脚本文件以更改行为。', + cron_script_badge_title: '脚本任务(无代理)', + cron_workdir_label: '工作目录', cron_view_full_output: 'View full output', cron_all_runs: '全部运行记录', cron_hide_runs: '隐藏记录', @@ -10460,6 +10530,16 @@ const LOCALES = { cron_collapse_prompt: '收起提示詞', cron_expand_output: '展開輸出', cron_collapse_output: '收起輸出', + cron_mode_agent: '代理', + cron_mode_script: '腳本', + cron_mode_label: '模式', + cron_script_job_banner: '依排程執行腳本,stdout 會送到交付目標。不使用代理、提示詞或技能。', + cron_script_card_title: '腳本', + cron_script_output: '腳本輸出', + cron_script_path_label: '腳本路徑', + cron_script_path_hint: '除非為絕對路徑,否則會在 ~/.hermes/scripts/ 下解析。請在伺服器上編輯腳本檔以變更行為。', + cron_script_badge_title: '腳本任務(無代理)', + cron_workdir_label: '工作目錄', cron_view_full_output: '檢視完整輸出', cron_all_runs: '所有執行', cron_hide_runs: '隱藏執行記錄', @@ -11647,6 +11727,16 @@ const LOCALES = { cron_collapse_prompt: 'Contraer prompt', cron_expand_output: 'Expandir saída', cron_collapse_output: 'Contraer saída', + cron_mode_agent: 'Agente', + cron_mode_script: 'Script', + cron_mode_label: 'Modo', + cron_script_job_banner: 'Executa um script no agendamento — stdout é entregue ao destino. Sem agente, prompt ou skills.', + cron_script_card_title: 'Script', + cron_script_output: 'Saída do script', + cron_script_path_label: 'Caminho do script', + cron_script_path_hint: 'Resolvido em ~/.hermes/scripts/ salvo caminho absoluto. Edite o script no servidor para alterar o comportamento.', + cron_script_badge_title: 'Tarefa script (sem agente)', + cron_workdir_label: 'Diretório de trabalho', cron_view_full_output: 'View full output', cron_all_runs: 'Todas execuções', cron_hide_runs: 'Esconder execuções', @@ -12912,6 +13002,16 @@ const LOCALES = { cron_collapse_prompt: 'Collapse prompt', cron_expand_output: 'Expand output', cron_collapse_output: 'Collapse output', + cron_mode_agent: 'Agent', + cron_mode_script: 'Script', + cron_mode_label: 'Mode', + cron_script_job_banner: 'Runs a script on schedule — stdout is delivered to the target. No agent, prompt, or skills.', + cron_script_card_title: 'Script', + cron_script_output: 'Script output', + cron_script_path_label: 'Script path', + cron_script_path_hint: 'Resolved under ~/.hermes/scripts/ unless an absolute path. Edit the script file on the server to change behavior.', + cron_script_badge_title: 'Script job (no agent)', + cron_workdir_label: 'Working directory', cron_view_full_output: 'View full output', cron_all_runs: 'All runs', cron_hide_runs: 'Hide runs', @@ -14218,6 +14318,16 @@ const LOCALES = { cron_collapse_prompt: 'Réduire le prompt', cron_expand_output: 'Développer la sortie', cron_collapse_output: 'Réduire la sortie', + cron_mode_agent: 'Agent', + cron_mode_script: 'Script', + cron_mode_label: 'Mode', + cron_script_job_banner: 'Exécute un script selon l\'horaire — stdout est livré à la cible. Pas d\'agent, prompt ni skills.', + cron_script_card_title: 'Script', + cron_script_output: 'Sortie du script', + cron_script_path_label: 'Chemin du script', + cron_script_path_hint: 'Résolu sous ~/.hermes/scripts/ sauf chemin absolu. Modifiez le script sur le serveur pour changer le comportement.', + cron_script_badge_title: 'Tâche script (sans agent)', + cron_workdir_label: 'Répertoire de travail', cron_view_full_output: 'View full output', cron_all_runs: 'Toutes les courses', cron_hide_runs: 'Masquer les courses', @@ -15577,6 +15687,16 @@ const LOCALES = { cron_collapse_prompt: 'İstemi daralt', cron_expand_output: 'Çıktıyı genişlet', cron_collapse_output: 'Çıktıyı daralt', + cron_mode_agent: 'Ajan', + cron_mode_script: 'Betik', + cron_mode_label: 'Mod', + cron_script_job_banner: 'Zamanlanmış betiği çalıştırır — stdout hedefe iletilir. Ajan, istem veya beceri yok.', + cron_script_card_title: 'Betik', + cron_script_output: 'Betik çıktısı', + cron_script_path_label: 'Betik yolu', + cron_script_path_hint: 'Mutlak yol değilse ~/.hermes/scripts/ altında çözülür. Davranışı değiştirmek için sunucudaki betiği düzenleyin.', + cron_script_badge_title: 'Betik işi (ajan yok)', + cron_workdir_label: 'Çalışma dizini', cron_view_full_output: 'View full output', cron_all_runs: 'Tüm koşular', cron_hide_runs: 'Çalıştırmaları gizle', @@ -17013,6 +17133,16 @@ const LOCALES = { cron_collapse_prompt: 'Zwiń zapytanie', cron_expand_output: 'Rozwiń wyjście', cron_collapse_output: 'Zwiń wyjście', + cron_mode_agent: 'Agent', + cron_mode_script: 'Skrypt', + cron_mode_label: 'Tryb', + cron_script_job_banner: 'Uruchamia skrypt według harmonogramu — standardowe wyjście jest dostarczane do celu. Bez agenta, promptu ani umiejętności.', + cron_script_card_title: 'Skrypt', + cron_script_output: 'Wyjście skryptu', + cron_script_path_label: 'Ścieżka skryptu', + cron_script_path_hint: 'Rozwiązywana względem ~/.hermes/scripts/, chyba że podano ścieżkę bezwzględną. Edytuj plik skryptu na serwerze, aby zmienić działanie.', + cron_script_badge_title: 'Zadanie skryptowe (bez agenta)', + cron_workdir_label: 'Katalog roboczy', cron_view_full_output: 'Wyświetl pełne wyjście', cron_all_runs: 'Wszystkie uruchomienia', cron_hide_runs: 'Ukryj uruchomienia', diff --git a/static/panels.js b/static/panels.js index bc1d2ad0..9fa938ea 100644 --- a/static/panels.js +++ b/static/panels.js @@ -516,7 +516,7 @@ async function loadCrons(animate) { item.innerHTML = `
${isNewRun ? '' : ''} - ${isAgentMode ? '🤖' : ''} + ${isAgentMode ? '🤖' : `📜`} ${esc(job.name)} ${esc(profileLabel)} ${esc(status.label)} @@ -579,6 +579,55 @@ function toggleCronRunExpanded(jobId, filename, runId){ } } +function _isCronScriptJob(job){ + return !!(job && job.no_agent); +} + +function _cronModeLabel(job){ + return _isCronScriptJob(job) + ? (t('cron_mode_script') || 'Script') + : (t('cron_mode_agent') || 'Agent'); +} + +function _cronOutputTitle(job){ + return _isCronScriptJob(job) + ? (t('cron_script_output') || 'Script output') + : (t('cron_last_output') || 'Last output'); +} + +function _cronScriptJobBannerHtml(){ + return `
+
${esc(t('cron_mode_script') || 'Script')}
+

${esc(t('cron_script_job_banner') || 'Runs a script on schedule — stdout is delivered to the target. No agent, prompt, or skills.')}

+
`; +} + +function _cronScriptCardHtml(job){ + const script = String(job && job.script || '').trim() || '—'; + const workdir = String(job && job.workdir || '').trim(); + const workdirRow = workdir + ? `
${esc(t('cron_workdir_label') || 'Working directory')}
${esc(workdir)}
` + : ''; + return `
+
${esc(t('cron_script_card_title') || 'Script')}
+
${esc(script)}
+ ${workdirRow} +
${esc(t('cron_script_path_hint') || 'Resolved under ~/.hermes/scripts/ unless an absolute path. Edit the script file on the server to change behavior.')}
+
`; +} + +function _cronAgentPromptCardHtml(job){ + const promptExpanded = _cronExpansionGet(_cronPanelExpandKey(job.id, 'prompt')); + const promptToggleLabel = promptExpanded ? (t('cron_collapse_prompt') || 'Collapse prompt') : (t('cron_expand_prompt') || 'Expand prompt'); + return `
+
+ ${esc(t('cron_prompt_label') || 'Prompt')} + +
+
${esc(job.prompt || '')}
+
`; +} + function _renderCronDetail(job){ _currentCronDetail = job; const title = $('taskDetailTitle'); @@ -592,14 +641,13 @@ function _renderCronDetail(job){ const schedule = job.schedule_display || (job.schedule && job.schedule.expression) || ''; const skills = Array.isArray(job.skills) && job.skills.length ? job.skills.join(', ') : '—'; const deliver = job.deliver || 'local'; - const isNoAgent = !!job.no_agent; - const cronJobMode = isNoAgent ? 'no-agent' : 'agent'; + const isNoAgent = _isCronScriptJob(job); + const cronJobMode = _cronModeLabel(job); const modelProvider = job.provider && job.model ? `${esc(job.provider)}/${esc(job.model)}` : job.model ? esc(job.model) : job.provider ? esc(job.provider) : isNoAgent ? '' : 'default'; - const script = job.script || ''; const profileLabel = _cronProfileLabel(job.profile); const profileTitle = _cronProfileTitle(job.profile); const lastError = job.last_error ? `
${esc(t('error_prefix').replace(/:\s*$/,''))}
${esc(job.last_error)}
` : ''; @@ -619,11 +667,13 @@ function _renderCronDetail(job){
` : ''; const toastNotifications = job.toast_notifications !== false; - const promptExpanded = _cronExpansionGet(_cronPanelExpandKey(job.id, 'prompt')); - const promptToggleLabel = promptExpanded ? (t('cron_collapse_prompt') || 'Collapse prompt') : (t('cron_expand_prompt') || 'Expand prompt'); + const outputTitle = _cronOutputTitle(job); + const skillsRow = isNoAgent ? '' : `
${esc(t('cron_skills_label') || 'Skills')}
${esc(skills)}
`; + const instructionCard = isNoAgent ? _cronScriptCardHtml(job) : _cronAgentPromptCardHtml(job); body.innerHTML = `
${attentionBanner} + ${isNoAgent ? _cronScriptJobBannerHtml() : ''}
${esc(t('cron_status_active').replace(/./,c=>c.toUpperCase()))}
Status
${esc(status.label)}
@@ -631,22 +681,15 @@ function _renderCronDetail(job){
${esc(t('cron_next'))}
${esc(nextRun)}
${esc(t('cron_last'))}
${esc(lastRun)}
Deliver
${esc(deliver)}
-
Mode
${esc(cronJobMode)}${modelProvider ? ` ${modelProvider}` : ''}
- ${isNoAgent ? `
No-agent script
${esc(script || '—')}
` : ''} +
${esc(t('cron_mode_label') || 'Mode')}
${esc(cronJobMode)}${modelProvider ? ` ${modelProvider}` : ''}
${esc(t('cron_profile_label') || 'Profile')}
${esc(profileLabel)}
${esc(t('cron_toast_notifications_label') || 'Completion toasts')}
${esc(toastNotifications ? (t('cron_toast_notifications_enabled') || 'Enabled') : (t('cron_toast_notifications_disabled') || 'Disabled'))}
-
Skills
${esc(skills)}
+ ${skillsRow} ${lastError}
-
-
- Prompt - -
-
${esc(job.prompt || '')}
-
+ ${instructionCard}
-
${esc(t('cron_last_output'))}
+
${esc(outputTitle)}
${esc(t('loading'))}
`; @@ -693,8 +736,10 @@ async function _loadCronDetailRuns(jobId){ if (!_currentCronDetail || _currentCronDetail.id !== jobId) return; const card = $('cronDetailRuns'); if (!card) return; + const outputTitle = _cronOutputTitle(_currentCronDetail); + const isScriptJob = _isCronScriptJob(_currentCronDetail); if (!data.runs || !data.runs.length) { - card.innerHTML = `
${esc(t('cron_last_output'))}
${esc(t('cron_no_runs_yet'))}
`; + card.innerHTML = `
${esc(outputTitle)}
${esc(t('cron_no_runs_yet'))}
`; return; } const rows = data.runs.map((run, i) => { @@ -702,7 +747,7 @@ async function _loadCronDetailRuns(jobId){ const sizeStr = run.size > 1024 ? (run.size/1024).toFixed(1)+' KB' : run.size+' B'; const dateStr = new Date(run.modified * 1000).toLocaleString(); const rid = `cron-det-run-${jobId}-${i}`; - const usageStrip = _formatCronRunUsageStrip(run.usage); + const usageStrip = isScriptJob ? '' : _formatCronRunUsageStrip(run.usage); const runExpanded = _cronExpansionGet(_cronRunExpandKey(jobId, run.filename)); const runToggleLabel = runExpanded ? (t('cron_collapse_output') || 'Collapse output') : (t('cron_expand_output') || 'Expand output'); return `
@@ -717,7 +762,7 @@ async function _loadCronDetailRuns(jobId){
`; }).join(''); const countLabel = data.total > 50 ? ` (${data.total} runs, showing latest 50)` : ` (${data.total} runs)`; - card.innerHTML = `
${esc(t('cron_last_output'))}${countLabel}
${rows}`; + card.innerHTML = `
${esc(outputTitle)}${countLabel}
${rows}`; } catch(e) { /* ignore */ } } @@ -925,8 +970,30 @@ function _renderCronForm({ name, schedule, prompt, deliver, profile, toast_notif const isNoAgent = !!no_agent; const toastNotifications = toast_notifications !== false; title.textContent = isEdit ? (t('edit') + ' · ' + (name || schedule || t('scheduled_jobs'))) : t('new_job'); + const promptBlock = isNoAgent ? '' : ` +
+ + +
`; + const scriptBlock = isNoAgent ? ` +
+ + +
${esc(t('cron_script_path_hint') || 'Resolved under ~/.hermes/scripts/ unless an absolute path. Edit the script file on the server to change behavior.')}
+
` : ''; + const skillsBlock = isNoAgent ? '' : ` +
+ +
+ + +
+
+ ${isEdit ? `
${esc(t('cron_skills_edit_hint') || 'Skill list is not editable after creation.')}
` : ''} +
`; body.innerHTML = `
+ ${isNoAgent ? _cronScriptJobBannerHtml() : ''}
@@ -938,11 +1005,8 @@ function _renderCronForm({ name, schedule, prompt, deliver, profile, toast_notif
${esc(t('cron_schedule_hint') || "Cron expression or shorthand like 'every 1h'.")}
-
- - - ${isNoAgent ? `
No-agent mode runs the configured script directly; Prompt is unused. No-agent script: ${esc(script || '—')}
` : ''} -
+ ${scriptBlock} + ${promptBlock}
- -
-
- ${isEdit ? `
${esc(t('cron_skills_edit_hint') || 'Skill list is not editable after creation.')}
` : ''} -
+ ${skillsBlock} `; @@ -979,7 +1035,7 @@ function _renderCronForm({ name, schedule, prompt, deliver, profile, toast_notif if (empty) empty.style.display = 'none'; _setCronHeaderButtons(isEdit ? 'edit' : 'create'); _populateCronDeliverOptions(deliver, isEdit); - _renderCronSkillTags(); + if (!isNoAgent) _renderCronSkillTags(); const scheduleEl = $('cronFormSchedule'); if (scheduleEl) { scheduleEl.addEventListener('input', _syncCronScheduleWarning); @@ -1088,14 +1144,15 @@ async function saveCronForm(){ const profileEl=$('cronFormProfile'); const toastEl=$('cronFormToastNotifications'); const errEl=$('cronFormError'); - if(!schEl||!promptEl||!errEl) return; + if(!schEl||!errEl) return; + const isNoAgent = !!(_cronPreFormDetail && _cronPreFormDetail.no_agent); + if(!isNoAgent && !promptEl) return; const name=(nameEl?nameEl.value:'').trim(); const schedule=schEl.value.trim(); - const prompt=promptEl.value.trim(); + const prompt=promptEl ? promptEl.value.trim() : ''; const deliver=delivEl?delivEl.value:'local'; const profile=profileEl?profileEl.value:''; const toastNotifications=toastEl?!!toastEl.checked:true; - const isNoAgent = !!(_cronPreFormDetail && _cronPreFormDetail.no_agent); errEl.style.display='none'; if(!schedule){errEl.textContent=t('cron_schedule_required_example');errEl.style.display='';return;} if(!isNoAgent && !prompt){errEl.textContent=t('cron_prompt_required');errEl.style.display='';return;} diff --git a/static/style.css b/static/style.css index 0fa3360f..5729406f 100644 --- a/static/style.css +++ b/static/style.css @@ -4077,6 +4077,14 @@ main.main > #mainPlugin{display:none;} .cron-badge{position:absolute;top:2px;right:2px;background:#e53e3e;color:#fff;font-size:9px;font-weight:700;min-width:14px;height:14px;line-height:14px;text-align:center;border-radius:7px;padding:0 3px;} .cron-new-dot{width:7px;height:7px;border-radius:50%;background:var(--success,#22c55e);flex-shrink:0;animation:cron-dot-pulse 2s ease-in-out infinite;} .cron-agent-badge{flex-shrink:0;font-size:12px;line-height:1;} +.cron-script-badge{flex-shrink:0;font-size:12px;line-height:1;opacity:.92;} +.cron-script-job-banner{margin-bottom:12px;} +.cron-script-job-banner .detail-alert-title{font-weight:600;} +.detail-script{background:var(--sidebar);border:1px solid var(--border);border-radius:8px;padding:10px 12px;font-size:12px;white-space:pre-wrap;line-height:1.55;color:var(--text);font-family:'SF Mono',ui-monospace,monospace;word-break:break-word;} +.detail-hint{font-size:11px;color:var(--muted);line-height:1.5;} +.cron-script-card .detail-hint,.cron-script-card-hint{margin-top:8px;} +.detail-badge.cron-mode-badge.script{background:rgba(148,163,184,.14);color:var(--text-secondary);} +.detail-badge.cron-mode-badge.agent{background:rgba(99,179,237,.14);color:#90cdf4;} @keyframes cron-dot-pulse{0%,100%{opacity:1;}50%{opacity:.4;}} .has-new-run{border-color:var(--success,#22c55e)!important;box-shadow:0 0 0 1px var(--success,#22c55e);} diff --git a/tests/test_cron_no_agent_edit.py b/tests/test_cron_no_agent_edit.py index 79075f22..00ac092e 100644 --- a/tests/test_cron_no_agent_edit.py +++ b/tests/test_cron_no_agent_edit.py @@ -2,7 +2,6 @@ from __future__ import annotations -import re from pathlib import Path ROOT = Path(__file__).resolve().parents[1] @@ -46,13 +45,16 @@ def test_open_cron_edit_plumbs_no_agent_and_script_to_form(): assert "script: job.script || ''" in body -def test_no_agent_form_drops_prompt_required_attribute_and_shows_script_context(): +def test_no_agent_form_hides_prompt_and_shows_readonly_script_path(): body = _function_body("_renderCronForm") assert "no_agent" in body and "script" in body assert "const isNoAgent = !!no_agent;" in body - assert "cron-no-agent-hint" in body - assert "No-agent script" in body - assert "${isNoAgent ? ' disabled' : ' required'}" in body + assert "const promptBlock = isNoAgent ? '' :" in body + assert 'id="cronFormScript"' in body + assert "readonly autocomplete=\"off\"" in body + assert "cron_script_path_hint" in body + assert "const skillsBlock = isNoAgent ? '' :" in body + assert "if (!isNoAgent) _renderCronSkillTags();" in body def test_save_cron_form_keeps_agent_prompt_required_but_skips_no_agent_edits(): @@ -65,7 +67,8 @@ def test_save_cron_form_keeps_agent_prompt_required_but_skips_no_agent_edits(): def test_no_agent_detail_displays_mode_and_script(): body = _function_body("_renderCronDetail") - assert "const isNoAgent = !!job.no_agent;" in body - assert "No-agent script" in body + assert "const isNoAgent = _isCronScriptJob(job);" in body + assert "isNoAgent ? _cronScriptCardHtml(job) : _cronAgentPromptCardHtml(job)" in body + assert "isNoAgent ? _cronScriptJobBannerHtml() : ''" in body assert "cronJobMode" in body - assert "job.script" in body + assert "cron-mode-badge ${isNoAgent ? 'script' : 'agent'}" in body diff --git a/tests/test_cron_script_job_ui.py b/tests/test_cron_script_job_ui.py new file mode 100644 index 00000000..6f013a06 --- /dev/null +++ b/tests/test_cron_script_job_ui.py @@ -0,0 +1,72 @@ +"""Static coverage for script (no_agent) cron jobs in Tasks UI.""" + +from __future__ import annotations + +from pathlib import Path + +ROOT = Path(__file__).resolve().parents[1] +PANELS_JS = (ROOT / "static" / "panels.js").read_text(encoding="utf-8") +STYLE_CSS = (ROOT / "static" / "style.css").read_text(encoding="utf-8") +I18N_JS = (ROOT / "static" / "i18n.js").read_text(encoding="utf-8") + + +def test_cron_script_job_helpers_exist(): + assert "function _isCronScriptJob(job)" in PANELS_JS + assert "function _cronModeLabel(job)" in PANELS_JS + assert "function _cronOutputTitle(job)" in PANELS_JS + assert "function _cronScriptJobBannerHtml()" in PANELS_JS + assert "function _cronScriptCardHtml(job)" in PANELS_JS + assert "function _cronAgentPromptCardHtml(job)" in PANELS_JS + + +def test_cron_detail_branches_on_no_agent(): + assert "const isNoAgent = _isCronScriptJob(job)" in PANELS_JS + assert "isNoAgent ? _cronScriptCardHtml(job) : _cronAgentPromptCardHtml(job)" in PANELS_JS + assert "isNoAgent ? _cronScriptJobBannerHtml() : ''" in PANELS_JS + assert 'class="detail-script"' in PANELS_JS + assert 'class="detail-alert cron-script-job-banner"' in PANELS_JS + assert "cron-mode-badge ${isNoAgent ? 'script' : 'agent'}" in PANELS_JS + + +def test_cron_list_shows_script_badge(): + assert "cron-script-badge" in PANELS_JS + assert "cron_script_badge_title" in PANELS_JS + + +def test_cron_form_hides_prompt_for_script_jobs(): + assert "const isNoAgent = !!no_agent" in PANELS_JS + assert "const promptBlock = isNoAgent ? '' :" in PANELS_JS + assert "const scriptBlock = isNoAgent ?" in PANELS_JS + assert 'id="cronFormScript"' in PANELS_JS + assert "if(!isNoAgent && !prompt)" in PANELS_JS + + +def test_cron_runs_skip_usage_strip_for_script_jobs(): + assert "const isScriptJob = _isCronScriptJob(_currentCronDetail)" in PANELS_JS + assert "const usageStrip = isScriptJob ? '' : _formatCronRunUsageStrip(run.usage)" in PANELS_JS + + +def test_cron_script_job_styles_exist(): + assert ".cron-script-badge" in STYLE_CSS + assert ".cron-script-job-banner" in STYLE_CSS + assert ".detail-script{" in STYLE_CSS + assert ".detail-badge.cron-mode-badge.script" in STYLE_CSS + assert ".detail-badge.cron-mode-badge.agent" in STYLE_CSS + + +def test_cron_script_job_i18n_keys_exist_in_every_locale(): + locale_count = I18N_JS.count("cron_last_output:") + assert locale_count >= 9 + for key in ( + "cron_mode_agent", + "cron_mode_script", + "cron_mode_label", + "cron_script_job_banner", + "cron_script_card_title", + "cron_script_output", + "cron_script_path_label", + "cron_script_path_hint", + "cron_script_badge_title", + "cron_workdir_label", + ): + assert I18N_JS.count(f"{key}:") >= locale_count, key