Script cron jobs (no_agent) in the Tasks panel now show a script badge, banner, script path + working directory, and 'Script output' run labels instead of an empty Prompt card (#3589, @pamnard). Self-rebased onto v0.51.331 (was CONFLICTING, CHANGELOG-only stale base). Maintainer fix: added the 10 new cron i18n keys to Polish (pl), which the PR had missed (12/13 locales) — all keys now 13/13, locale tests pass. Full suite 8285 passed, ESLint/scope-undef/ruff CLEAN, Opus SHIP-safe. Co-authored-by: pamnard <pamnard@users.noreply.github.com>
This commit is contained in:
parent
d560a312f2
commit
2fd039cce4
6 changed files with 321 additions and 46 deletions
|
|
@ -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
|
||||
|
|
|
|||
130
static/i18n.js
130
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',
|
||||
|
|
|
|||
133
static/panels.js
133
static/panels.js
|
|
@ -516,7 +516,7 @@ async function loadCrons(animate) {
|
|||
item.innerHTML = `
|
||||
<div class="cron-header">
|
||||
${isNewRun ? '<span class="cron-new-dot" title="New run"></span>' : ''}
|
||||
${isAgentMode ? '<span class="cron-agent-badge" title="Agent mode">🤖</span>' : ''}
|
||||
${isAgentMode ? '<span class="cron-agent-badge" title="Agent mode">🤖</span>' : `<span class="cron-script-badge" title="${esc(t('cron_script_badge_title') || 'Script job (no agent)')}">📜</span>`}
|
||||
<span class="cron-name" title="${esc(job.name)}">${esc(job.name)}</span>
|
||||
<span class="cron-profile-badge" title="${esc(profileTitle)}">${esc(profileLabel)}</span>
|
||||
<span class="cron-status ${status.listClass}">${esc(status.label)}</span>
|
||||
|
|
@ -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 `<div class="detail-alert cron-script-job-banner">
|
||||
<div class="detail-alert-title">${esc(t('cron_mode_script') || 'Script')}</div>
|
||||
<p>${esc(t('cron_script_job_banner') || 'Runs a script on schedule — stdout is delivered to the target. No agent, prompt, or skills.')}</p>
|
||||
</div>`;
|
||||
}
|
||||
|
||||
function _cronScriptCardHtml(job){
|
||||
const script = String(job && job.script || '').trim() || '—';
|
||||
const workdir = String(job && job.workdir || '').trim();
|
||||
const workdirRow = workdir
|
||||
? `<div class="detail-row"><div class="detail-row-label">${esc(t('cron_workdir_label') || 'Working directory')}</div><div class="detail-row-value"><code>${esc(workdir)}</code></div></div>`
|
||||
: '';
|
||||
return `<div class="detail-card cron-script-card">
|
||||
<div class="detail-card-title">${esc(t('cron_script_card_title') || 'Script')}</div>
|
||||
<div class="detail-script">${esc(script)}</div>
|
||||
${workdirRow}
|
||||
<div class="detail-hint cron-script-card-hint">${esc(t('cron_script_path_hint') || 'Resolved under ~/.hermes/scripts/ unless an absolute path. Edit the script file on the server to change behavior.')}</div>
|
||||
</div>`;
|
||||
}
|
||||
|
||||
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 `<div class="detail-card">
|
||||
<div class="detail-card-title detail-card-title-row">
|
||||
<span>${esc(t('cron_prompt_label') || 'Prompt')}</span>
|
||||
<button type="button" class="detail-expand-toggle" onclick="toggleCronPromptExpanded('${esc(job.id)}')" title="${esc(promptToggleLabel)}" aria-label="${esc(promptToggleLabel)}">${esc(promptExpanded ? '▴' : '▾')}</button>
|
||||
</div>
|
||||
<div class="detail-prompt ${promptExpanded ? 'expanded' : ''}">${esc(job.prompt || '')}</div>
|
||||
</div>`;
|
||||
}
|
||||
|
||||
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 ? `<div class="detail-row"><div class="detail-row-label">${esc(t('error_prefix').replace(/:\s*$/,''))}</div><div class="detail-row-value" style="color:var(--accent-text)">${esc(job.last_error)}</div></div>` : '';
|
||||
|
|
@ -619,11 +667,13 @@ function _renderCronDetail(job){
|
|||
</div>
|
||||
</div>` : '';
|
||||
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 ? '' : `<div class="detail-row"><div class="detail-row-label">${esc(t('cron_skills_label') || 'Skills')}</div><div class="detail-row-value">${esc(skills)}</div></div>`;
|
||||
const instructionCard = isNoAgent ? _cronScriptCardHtml(job) : _cronAgentPromptCardHtml(job);
|
||||
body.innerHTML = `
|
||||
<div class="main-view-content">
|
||||
${attentionBanner}
|
||||
${isNoAgent ? _cronScriptJobBannerHtml() : ''}
|
||||
<div class="detail-card">
|
||||
<div class="detail-card-title">${esc(t('cron_status_active').replace(/./,c=>c.toUpperCase()))}</div>
|
||||
<div class="detail-row"><div class="detail-row-label">Status</div><div class="detail-row-value"><span class="detail-badge ${status.detailClass}">${esc(status.label)}</span></div></div>
|
||||
|
|
@ -631,22 +681,15 @@ function _renderCronDetail(job){
|
|||
<div class="detail-row"><div class="detail-row-label">${esc(t('cron_next'))}</div><div class="detail-row-value">${esc(nextRun)}</div></div>
|
||||
<div class="detail-row"><div class="detail-row-label">${esc(t('cron_last'))}</div><div class="detail-row-value">${esc(lastRun)}</div></div>
|
||||
<div class="detail-row"><div class="detail-row-label">Deliver</div><div class="detail-row-value">${esc(deliver)}</div></div>
|
||||
<div class="detail-row"><div class="detail-row-label">Mode</div><div class="detail-row-value"><span class="detail-badge" id="cronJobMode">${esc(cronJobMode)}</span>${modelProvider ? ` <code>${modelProvider}</code>` : ''}</div></div>
|
||||
${isNoAgent ? `<div class="detail-row"><div class="detail-row-label">No-agent script</div><div class="detail-row-value"><code>${esc(script || '—')}</code></div></div>` : ''}
|
||||
<div class="detail-row"><div class="detail-row-label">${esc(t('cron_mode_label') || 'Mode')}</div><div class="detail-row-value"><span class="detail-badge cron-mode-badge ${isNoAgent ? 'script' : 'agent'}" id="cronJobMode">${esc(cronJobMode)}</span>${modelProvider ? ` <code>${modelProvider}</code>` : ''}</div></div>
|
||||
<div class="detail-row"><div class="detail-row-label">${esc(t('cron_profile_label') || 'Profile')}</div><div class="detail-row-value"><span class="detail-badge active" title="${esc(profileTitle)}">${esc(profileLabel)}</span></div></div>
|
||||
<div class="detail-row"><div class="detail-row-label">${esc(t('cron_toast_notifications_label') || 'Completion toasts')}</div><div class="detail-row-value"><span class="detail-badge ${toastNotifications ? 'active' : ''}">${esc(toastNotifications ? (t('cron_toast_notifications_enabled') || 'Enabled') : (t('cron_toast_notifications_disabled') || 'Disabled'))}</span></div></div>
|
||||
<div class="detail-row"><div class="detail-row-label">Skills</div><div class="detail-row-value">${esc(skills)}</div></div>
|
||||
${skillsRow}
|
||||
${lastError}
|
||||
</div>
|
||||
<div class="detail-card">
|
||||
<div class="detail-card-title detail-card-title-row">
|
||||
<span>Prompt</span>
|
||||
<button type="button" class="detail-expand-toggle" onclick="toggleCronPromptExpanded('${esc(job.id)}')" title="${esc(promptToggleLabel)}" aria-label="${esc(promptToggleLabel)}">${esc(promptExpanded ? '▴' : '▾')}</button>
|
||||
</div>
|
||||
<div class="detail-prompt ${promptExpanded ? 'expanded' : ''}">${esc(job.prompt || '')}</div>
|
||||
</div>
|
||||
${instructionCard}
|
||||
<div class="detail-card ${_cronNewJobIds.has(String(job.id)) ? 'has-new-run' : ''}" id="cronDetailRuns">
|
||||
<div class="detail-card-title">${esc(t('cron_last_output'))}</div>
|
||||
<div class="detail-card-title">${esc(outputTitle)}</div>
|
||||
<div style="color:var(--muted);font-size:12px">${esc(t('loading'))}</div>
|
||||
</div>
|
||||
</div>`;
|
||||
|
|
@ -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 = `<div class="detail-card-title">${esc(t('cron_last_output'))}</div><div style="color:var(--muted);font-size:12px">${esc(t('cron_no_runs_yet'))}</div>`;
|
||||
card.innerHTML = `<div class="detail-card-title">${esc(outputTitle)}</div><div style="color:var(--muted);font-size:12px">${esc(t('cron_no_runs_yet'))}</div>`;
|
||||
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 `<div class="detail-run-item" id="${rid}">
|
||||
|
|
@ -717,7 +762,7 @@ async function _loadCronDetailRuns(jobId){
|
|||
</div>`;
|
||||
}).join('');
|
||||
const countLabel = data.total > 50 ? ` (${data.total} runs, showing latest 50)` : ` (${data.total} runs)`;
|
||||
card.innerHTML = `<div class="detail-card-title">${esc(t('cron_last_output'))}${countLabel}</div>${rows}`;
|
||||
card.innerHTML = `<div class="detail-card-title">${esc(outputTitle)}${countLabel}</div>${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 ? '' : `
|
||||
<div class="detail-form-row">
|
||||
<label for="cronFormPrompt">${esc(t('cron_prompt_label') || 'Prompt')}</label>
|
||||
<textarea id="cronFormPrompt" rows="6" placeholder="${esc(t('cron_prompt_placeholder') || 'Must be self-contained')}" required>${esc(prompt || '')}</textarea>
|
||||
</div>`;
|
||||
const scriptBlock = isNoAgent ? `
|
||||
<div class="detail-form-row">
|
||||
<label for="cronFormScript">${esc(t('cron_script_path_label') || 'Script path')}</label>
|
||||
<input type="text" id="cronFormScript" value="${esc(script || '')}" readonly autocomplete="off">
|
||||
<div class="detail-form-hint">${esc(t('cron_script_path_hint') || 'Resolved under ~/.hermes/scripts/ unless an absolute path. Edit the script file on the server to change behavior.')}</div>
|
||||
</div>` : '';
|
||||
const skillsBlock = isNoAgent ? '' : `
|
||||
<div class="detail-form-row">
|
||||
<label for="cronFormSkillSearch">${esc(t('cron_skills_label') || 'Skills')}</label>
|
||||
<div class="skill-picker-wrap">
|
||||
<input type="text" id="cronFormSkillSearch" placeholder="${esc(t('cron_skills_placeholder') || 'Add skills (optional)...')}" autocomplete="off" ${isEdit ? 'disabled' : ''}>
|
||||
<div id="cronFormSkillDropdown" class="skill-picker-dropdown" style="display:none"></div>
|
||||
<div id="cronFormSkillTags" class="skill-picker-tags"></div>
|
||||
</div>
|
||||
${isEdit ? `<div class="detail-form-hint">${esc(t('cron_skills_edit_hint') || 'Skill list is not editable after creation.')}</div>` : ''}
|
||||
</div>`;
|
||||
body.innerHTML = `
|
||||
<div class="main-view-content">
|
||||
${isNoAgent ? _cronScriptJobBannerHtml() : ''}
|
||||
<form class="detail-form" onsubmit="event.preventDefault(); saveCronForm();">
|
||||
<div class="detail-form-row">
|
||||
<label for="cronFormName">${esc(t('cron_name_label') || 'Name')}</label>
|
||||
|
|
@ -938,11 +1005,8 @@ function _renderCronForm({ name, schedule, prompt, deliver, profile, toast_notif
|
|||
<div class="detail-form-hint">${esc(t('cron_schedule_hint') || "Cron expression or shorthand like 'every 1h'.")}</div>
|
||||
<div id="cronFormScheduleOnceWarning" class="detail-form-warning cron-once-warning" style="display:none">${esc(t('cron_schedule_once_warning') || "Duration forms like '30m' run once and are removed after running. Use 'every 30m' to keep a recurring job.")}</div>
|
||||
</div>
|
||||
<div class="detail-form-row ${isNoAgent ? 'cron-no-agent-prompt-row' : ''}">
|
||||
<label for="cronFormPrompt">${esc(t('cron_prompt_label') || 'Prompt')}</label>
|
||||
<textarea id="cronFormPrompt" rows="6" placeholder="${esc(t('cron_prompt_placeholder') || 'Must be self-contained')}"${isNoAgent ? ' disabled' : ' required'}>${esc(prompt || '')}</textarea>
|
||||
${isNoAgent ? `<div class="detail-form-hint cron-no-agent-hint">No-agent mode runs the configured script directly; Prompt is unused. No-agent script: <code>${esc(script || '—')}</code></div>` : ''}
|
||||
</div>
|
||||
${scriptBlock}
|
||||
${promptBlock}
|
||||
<div class="detail-form-row">
|
||||
<label for="cronFormDeliver">${esc(t('cron_deliver_label') || 'Deliver output to')}</label>
|
||||
<select id="cronFormDeliver">
|
||||
|
|
@ -963,15 +1027,7 @@ function _renderCronForm({ name, schedule, prompt, deliver, profile, toast_notif
|
|||
<span>${esc(t('cron_toast_notifications_hint') || 'Show a toast when this cron finishes.')}</span>
|
||||
</label>
|
||||
</div>
|
||||
<div class="detail-form-row">
|
||||
<label for="cronFormSkillSearch">${esc(t('cron_skills_label') || 'Skills')}</label>
|
||||
<div class="skill-picker-wrap">
|
||||
<input type="text" id="cronFormSkillSearch" placeholder="${esc(t('cron_skills_placeholder') || 'Add skills (optional)...')}" autocomplete="off" ${isEdit ? 'disabled' : ''}>
|
||||
<div id="cronFormSkillDropdown" class="skill-picker-dropdown" style="display:none"></div>
|
||||
<div id="cronFormSkillTags" class="skill-picker-tags"></div>
|
||||
</div>
|
||||
${isEdit ? `<div class="detail-form-hint">${esc(t('cron_skills_edit_hint') || 'Skill list is not editable after creation.')}</div>` : ''}
|
||||
</div>
|
||||
${skillsBlock}
|
||||
<div id="cronFormError" class="detail-form-error" style="display:none"></div>
|
||||
</form>
|
||||
</div>`;
|
||||
|
|
@ -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;}
|
||||
|
|
|
|||
|
|
@ -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);}
|
||||
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
72
tests/test_cron_script_job_ui.py
Normal file
72
tests/test_cron_script_job_ui.py
Normal file
|
|
@ -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
|
||||
Loading…
Reference in a new issue