Skip to content

feat(#2316): add Scripts subtab to Tasks panel with backend list/raw endpoints#3935

Open
rodboev wants to merge 4 commits into
nesquena:masterfrom
rodboev:pr/scripts-panel
Open

feat(#2316): add Scripts subtab to Tasks panel with backend list/raw endpoints#3935
rodboev wants to merge 4 commits into
nesquena:masterfrom
rodboev:pr/scripts-panel

Conversation

@rodboev

@rodboev rodboev commented Jun 10, 2026

Copy link
Copy Markdown
Contributor

Thinking Path

  • The Tasks panel already loads cron jobs on open (switchPanel → loadCrons()), but the scripts in ~/.hermes/scripts/ that those jobs reference are invisible from the UI — users must leave the browser to inspect them.
  • The panel has no subtab infrastructure yet; adding one mirrors the pattern used in Settings (switchSettingsSection + pane toggling) adapted to a simpler two-tab case.
  • Rather than inventing a new discovery mechanism, the backend resolves the same ~/.hermes/scripts/ path used by the cron hint text (cron_script_path_hint in i18n.js) and applies the same safe_resolve() guard already used by the skills content endpoint (/api/skills/content) for traversal protection.
  • The source preview uses lazy loading on expand (same UX as collapsible skill cards) so the list renders immediately without fetching every file.

What Changed

  • static/index.html: replace #panelTasks panel header with subtab buttons (#tasksSubtabJobs, #tasksSubtabScripts), wrap the cron list in #tasksJobsPane, add #tasksScriptsPane with #scriptsList
  • static/panels.js: add switchTasksSubtab(), loadScripts(), _renderScriptsList() functions; update switchPanel tasks branch to respect active subtab
  • static/i18n.js: add tab_tasks_jobs, tab_tasks_scripts, scripts_no_scripts, scripts_load_error, scripts_path_label keys
  • api/routes.py: add GET /api/scripts/list and GET /api/scripts/raw routing entries; add _hermes_scripts_dir(), _parse_script_docstring(), _handle_scripts_list(), _handle_scripts_raw() handler functions
  • static/style.css: add .tasks-subtabs, .tasks-subtab, .scripts-list, .script-item, .script-header, .script-desc, .script-source rules

Why It Matters

Users can now browse, read descriptions for, and inspect the source of any script in ~/.hermes/scripts/ directly from the Tasks panel without needing server access, making script-backed cron jobs understandable at a glance.

Verification

$env:PYTHONUTF8 = '1'
..\hermes-agent\venv\Scripts\python.exe -m pytest tests/test_2316_scripts_panel.py -v --timeout=60
..\hermes-agent\venv\Scripts\python.exe -m pytest tests/ -v --timeout=60

Manual: open Tasks panel, click Scripts subtab, verify list populates; click a script card to expand source; attempt ?path=../../etc/passwd in devtools — expect 400.

Risks / Follow-ups

  • Profile-scoped ~/.hermes/profiles/<profile>/scripts/ discovery deferred to a follow-up (issue comments call out global scope for first cut)
  • No write/edit surface in this PR; scripts remain read-only
  • Syntax highlighting requires Prism.js to be loaded; falls back gracefully to plain text if unavailable
  • Non-.py/.sh extensions (e.g. .rb) are not listed; can be extended by adding to the suffix allowlist in _handle_scripts_list

Model Used

Claude Opus 4.6 via Claude Code CLI

@greptile-apps

greptile-apps Bot commented Jun 10, 2026

Copy link
Copy Markdown

Greptile Summary

This PR adds a Scripts subtab to the Tasks panel, exposing ~/.hermes/scripts/ through two new backend endpoints (GET /api/scripts/list and GET /api/scripts/raw) with path-traversal protection, extension filtering, and lazy source loading in the frontend.

  • Backend (api/routes.py): adds _hermes_scripts_dir(), _parse_script_docstring(), _handle_scripts_list(), and _handle_scripts_raw(), all guarded by safe_resolve() and an extension allowlist.
  • Frontend (static/panels.js, index.html, style.css): introduces switchTasksSubtab(), loadScripts(), and _renderScriptsList() with inline Prism.js highlighting and on-demand source fetching per card.
  • i18n (i18n.js): adds five new keys across all thirteen supported locales.

Confidence Score: 4/5

Safe to merge with one backend fix: the shell-comment parser silently returns empty descriptions for scripts whose first comment is preceded by a blank line, which is the most common real-world layout.

The traversal guard, extension allowlist, and lazy-loading approach are all solid. The one concrete defect is in _parse_script_docstring: the elif stripped and not lines: continue condition skips non-empty non-comment preamble lines but does not skip blank lines, so any blank line before the first # comment breaks out of the loop and returns an empty string. This silently makes every normally-formatted shell script appear with no description in the UI.

api/routes.py — specifically the _parse_script_docstring function

Important Files Changed

Filename Overview
api/routes.py Adds scripts list/raw endpoints with safe_resolve guard and extension filter; _parse_script_docstring has a blank-line handling bug that silently drops descriptions for scripts with a blank line between the shebang and the first comment.
static/panels.js Adds switchTasksSubtab, loadScripts, and _renderScriptsList; subtab switching logic and lazy load are correct; duplicate click-handler pattern is unusual but works because inline onclick fires before addEventListener.
static/index.html Adds subtab buttons and pane wrappers; correctly mirrors existing cron layout with no structural issues.
static/i18n.js Adds five new i18n keys across all 13 locales; translations appear appropriate.
static/style.css Adds subtab and script card CSS rules; no conflicts with existing selectors.
tests/test_2316_scripts_panel.py Good coverage of list, raw, path traversal, and sort order; the blank-line-between-shebang-and-description case is not tested, leaving the parser bug uncaught.

Sequence Diagram

sequenceDiagram
    participant U as User
    participant UI as panels.js
    participant BE as routes.py (backend)
    participant FS as ~/.hermes/scripts/

    U->>UI: click "Scripts" subtab
    UI->>UI: switchTasksSubtab('scripts')
    UI->>BE: GET /api/scripts/list
    BE->>FS: iterdir() + filter .py/.sh/.bash/.zsh
    FS-->>BE: file paths
    BE->>BE: _parse_script_docstring(p) per file
    BE-->>UI: "{scripts: [{name, description}, ...]}"
    UI->>UI: _renderScriptsList() render cards

    U->>UI: click script card to expand
    UI->>UI: classList.toggle('expanded')
    alt source not yet loaded
        UI->>BE: "GET /api/scripts/raw?path=name"
        BE->>BE: safe_resolve(scripts_dir, name)
        BE->>BE: extension allowlist check
        BE->>FS: read_text()
        FS-->>BE: source content
        BE-->>UI: "{name, source}"
        UI->>UI: set code.textContent + Prism.highlightElement()
    end
    UI->>UI: sourceEl.style.display visible

    U->>UI: click card to collapse
    UI->>UI: sourceEl.style.display hidden
Loading

Reviews (4): Last reviewed commit: "fix(#2316): harden raw endpoint, catch V..." | Re-trigger Greptile

Comment thread api/routes.py
Comment thread static/panels.js
Comment thread static/panels.js
Comment thread api/routes.py
@rodboev rodboev force-pushed the pr/scripts-panel branch from 76fbc32 to da4cb2a Compare June 11, 2026 16:18
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant