diff --git a/CHANGELOG.md b/CHANGELOG.md index 328683ef..d68217fd 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,9 @@ ### Added +- Cron detail prompt and output panels now have expand/collapse controls that + remove the inner scroll cap and remember the expanded state per job/run. + - **PR #2099** by @dobby-d-elf — Adds an opt-in `Settings → Preferences → Fade text effect` toggle (off by default). When enabled, newly streamed output tokens are revealed through an adaptive playout buffer and animated with an opacity-only fade similar to ChatGPT and other frontier LLM apps. Implementation details: fade locked per stream to avoid mid-stream toggle rewind; reduced-motion users get non-animated text; live cursor hidden while fade is active; custom renderer on `streaming-markdown` parser wraps only newly-appended words; animated spans replace themselves with plain text on `animationend` (no long-lived wrapper buildup in long responses); unsafe streamed `href`/`src` values blocked in fade renderer `set_attr` path. Performance tuning: 200ms base fade duration scaling to 350ms for fast output, 16ms word stagger, 320ms done-drain wait cap, 160 wps visual cap, max 2-3 words/frame, brief pauses after sentence punctuation. Default-off means existing users see no change. 293-line regression test pinning the contract. - **PR #2165** by @starship-s — Pooled OpenAI Codex quota status surfaced in the Providers panel. Pre-fix, the Providers page presented Codex quota as if there were only one credential/account state, which was misleading when users authenticate through a credential pool with several usable credentials, temporarily exhausted credentials, failed probes, and different reset windows. Now the active provider quota card includes a credential-pool summary (available / exhausted / failed / checked counts), displays the best currently-available pool windows in the collapsed view as "Best of N", and exposes per-credential detail behind an expandable section. Exhausted credentials are intentionally NOT re-probed while their cooldown is active (matches credential-pool selection behavior, avoids generating failed quota calls from a status page). Manual refresh still means "probe now" but transient refresh failures preserve the last known-good snapshot. JWT decode (`_decode_jwt_claims_unverified`) is used only for token-shape classification (Codex OAuth JWT vs raw OpenAI API key), explicitly NOT for authorization — documented in the function docstring. Per-row plan labels only shown when verified account-limit data is available. Concurrent probing capped at `min(_CODEX_POOL_MAX_WORKERS=6, len(probe_items))` so page render time stays bounded on large pools. Transient `None` probe results are NOT cached (only known unavailable/exhausted states are cached); 32-test regression suite covering pool snapshot, concurrent probe, JWT detection, cache invalidation, transient-vs-known cache distinction, and i18n parity across all currently-supported locales. Scoped to OpenAI Codex (the only provider with the credential-pool/account-limit path needed to surface this accurately). diff --git a/docs/pr-media/2289/cron-output-collapsed.png b/docs/pr-media/2289/cron-output-collapsed.png new file mode 100644 index 00000000..a863cd3a Binary files /dev/null and b/docs/pr-media/2289/cron-output-collapsed.png differ diff --git a/docs/pr-media/2289/cron-output-expanded.png b/docs/pr-media/2289/cron-output-expanded.png new file mode 100644 index 00000000..ef9741d1 Binary files /dev/null and b/docs/pr-media/2289/cron-output-expanded.png differ diff --git a/static/i18n.js b/static/i18n.js index db0ca5b0..21dd9e8d 100644 --- a/static/i18n.js +++ b/static/i18n.js @@ -980,6 +980,10 @@ const LOCALES = { cron_schedule_placeholder: 'Schedule', cron_prompt_placeholder: 'Prompt', cron_last_output: 'Last output', + cron_expand_prompt: 'Expand prompt', + cron_collapse_prompt: 'Collapse prompt', + cron_expand_output: 'Expand output', + cron_collapse_output: 'Collapse output', cron_all_runs: 'All runs', cron_hide_runs: 'Hide runs', cron_no_runs_yet: '(no runs yet)', @@ -2153,6 +2157,10 @@ const LOCALES = { cron_schedule_placeholder: 'Pianificazione', cron_prompt_placeholder: 'Prompt', cron_last_output: 'Ultimo output', + cron_expand_prompt: 'Espandi prompt', + cron_collapse_prompt: 'Comprimi prompt', + cron_expand_output: 'Espandi output', + cron_collapse_output: 'Comprimi output', cron_all_runs: 'Tutte le esecuzioni', cron_hide_runs: 'Nascondi esecuzioni', cron_no_runs_yet: '(nessuna esecuzione)', @@ -3333,6 +3341,10 @@ const LOCALES = { cron_schedule_placeholder: 'スケジュール', cron_prompt_placeholder: 'プロンプト', cron_last_output: '前回の出力', + cron_expand_prompt: 'プロンプトを展開', + cron_collapse_prompt: 'プロンプトを折りたたむ', + cron_expand_output: '出力を展開', + cron_collapse_output: '出力を折りたたむ', cron_all_runs: 'すべての実行', cron_hide_runs: '実行履歴を隠す', cron_no_runs_yet: '(まだ実行されていません)', @@ -4278,6 +4290,10 @@ const LOCALES = { cron_schedule_placeholder: 'Расписание', cron_prompt_placeholder: 'Промпт', cron_last_output: 'Последний вывод', + cron_expand_prompt: 'Развернуть промпт', + cron_collapse_prompt: 'Свернуть промпт', + cron_expand_output: 'Развернуть вывод', + cron_collapse_output: 'Свернуть вывод', cron_all_runs: 'Все запуски', cron_hide_runs: 'Скрыть запуски', cron_no_runs_yet: '(пока запусков нет)', @@ -5397,6 +5413,10 @@ const LOCALES = { cron_schedule_placeholder: 'Schedule', cron_prompt_placeholder: 'Prompt', cron_last_output: 'Last output', + cron_expand_prompt: 'Expand prompt', + cron_collapse_prompt: 'Collapse prompt', + cron_expand_output: 'Expand output', + cron_collapse_output: 'Collapse output', cron_all_runs: 'All runs', cron_hide_runs: 'Hide runs', cron_no_runs_yet: '(no runs yet)', @@ -6684,6 +6704,10 @@ const LOCALES = { cron_schedule_placeholder: '0 9 * * *', cron_prompt_placeholder: 'Aufgabenbeschreibung', cron_last_output: 'Letzte Ausgabe', + cron_expand_prompt: 'Prompt erweitern', + cron_collapse_prompt: 'Prompt einklappen', + cron_expand_output: 'Ausgabe erweitern', + cron_collapse_output: 'Ausgabe einklappen', cron_all_runs: 'Alle Ausführungen', cron_hide_runs: 'Ausführungen ausblenden', cron_no_runs_yet: 'Noch keine Ausführungen.', @@ -7623,6 +7647,10 @@ const LOCALES = { cron_schedule_placeholder: '调度表达式', cron_prompt_placeholder: '提示词', cron_last_output: '最近输出', + cron_expand_prompt: '展开提示词', + cron_collapse_prompt: '收起提示词', + cron_expand_output: '展开输出', + cron_collapse_output: '收起输出', cron_all_runs: '全部运行记录', cron_hide_runs: '隐藏记录', cron_no_runs_yet: '(暂无运行记录)', @@ -8896,6 +8924,10 @@ const LOCALES = { cron_job_updated: '\u6392\u7a0b\u4efb\u52d9\u5df2\u66f4\u65b0', cron_last: '\u4e0a\u6b21', cron_last_output: '\u6700\u5f8c\u8f38\u51fa', + cron_expand_prompt: '展開提示詞', + cron_collapse_prompt: '收起提示詞', + cron_expand_output: '展開輸出', + cron_collapse_output: '收起輸出', cron_next: '\u4e0b\u6b21', cron_no_jobs: '\u627e\u4e0d\u5230\u6392\u7a0b\u4efb\u52d9\u3002', cron_no_runs_yet: '\uff08\u5c1a\u7121\u57f7\u884c\u8a18\u9304\uff09', @@ -10008,6 +10040,10 @@ const LOCALES = { cron_schedule_placeholder: 'Agendamento', cron_prompt_placeholder: 'Prompt', cron_last_output: 'Último output', + cron_expand_prompt: 'Expandir prompt', + cron_collapse_prompt: 'Contraer prompt', + cron_expand_output: 'Expandir saída', + cron_collapse_output: 'Contraer saída', cron_all_runs: 'Todas execuções', cron_hide_runs: 'Esconder execuções', cron_no_runs_yet: '(sem execuções ainda)', @@ -11085,6 +11121,10 @@ const LOCALES = { cron_schedule_placeholder: 'Schedule', cron_prompt_placeholder: 'Prompt', cron_last_output: 'Last output', + cron_expand_prompt: 'Expand prompt', + cron_collapse_prompt: 'Collapse prompt', + cron_expand_output: 'Expand output', + cron_collapse_output: 'Collapse output', cron_all_runs: 'All runs', cron_hide_runs: 'Hide runs', cron_no_runs_yet: '(no runs yet)', @@ -12183,6 +12223,10 @@ const LOCALES = { cron_schedule_placeholder: 'Calendrier', cron_prompt_placeholder: 'Rapide', cron_last_output: 'Dernière sortie', + cron_expand_prompt: 'Développer le prompt', + cron_collapse_prompt: 'Réduire le prompt', + cron_expand_output: 'Développer la sortie', + cron_collapse_output: 'Réduire la sortie', cron_all_runs: 'Toutes les courses', cron_hide_runs: 'Masquer les courses', cron_no_runs_yet: '(pas encore de courses)', diff --git a/static/panels.js b/static/panels.js index 42123ba5..441c7ede 100644 --- a/static/panels.js +++ b/static/panels.js @@ -461,6 +461,45 @@ async function loadCrons(animate) { } } +function _cronPanelExpandKey(jobId, suffix){ + return `hermes-webui-cron-${suffix}-expanded-${encodeURIComponent(String(jobId||''))}`; +} + +function _cronRunExpandKey(jobId, filename){ + return `${_cronPanelExpandKey(jobId, 'run')}-${encodeURIComponent(String(filename||''))}`; +} + +function _cronExpansionGet(key){ + try { return localStorage.getItem(key) === '1'; } catch(_) { return false; } +} + +function _cronExpansionSet(key, expanded){ + try { localStorage.setItem(key, expanded ? '1' : '0'); } catch(_) {} +} + +function toggleCronPromptExpanded(jobId){ + const key = _cronPanelExpandKey(jobId, 'prompt'); + _cronExpansionSet(key, !_cronExpansionGet(key)); + if (_currentCronDetail && String(_currentCronDetail.id) === String(jobId)) { + _renderCronDetail(_currentCronDetail); + } +} + +function toggleCronRunExpanded(jobId, filename, runId){ + const key = _cronRunExpandKey(jobId, filename); + const expanded = !_cronExpansionGet(key); + _cronExpansionSet(key, expanded); + const item = document.getElementById(runId); + const body = item ? item.querySelector('.detail-run-body') : null; + const btn = item ? item.querySelector('.detail-expand-toggle') : null; + if (body) body.classList.toggle('expanded', expanded); + if (btn) { + btn.textContent = expanded ? '▴' : '▾'; + btn.title = expanded ? (t('cron_collapse_output') || 'Collapse output') : (t('cron_expand_output') || 'Expand output'); + btn.setAttribute('aria-label', btn.title); + } +} + function _renderCronDetail(job){ _currentCronDetail = job; const title = $('taskDetailTitle'); @@ -501,6 +540,8 @@ 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'); body.innerHTML = `