dsh-web-icon-indicator

August 31, 2026 ยท View on GitHub

๐Ÿ“– ไธญๆ–‡ๆ–‡ๆกฃ ยท English ยท ๐Ÿ“ Changelog ยท Releases

awesome ยท DSH plugin npm version npm downloads License: MIT

Browser tab favicon reflects the current DSH session state โ€” idle / running / asking / done โ€” so you can see at a glance whether a session needs your attention, even when the tab is in the background.

โœจ What it does

  • Live session state on the tab favicon โ€” the browser-tab icon mirrors idle / running / asking / done (aggregate priority: asking > running > done > idle), so background tabs tell you at a glance what your agents are doing โ€” including ask_user_question prompts and approval / sandbox-escalation waits, which pin the icon to asking.
  • One SVG, recolored & animated in the browser โ€” ships a single whale template (icons/base.svg); every state, color and frame is rendered client-side as a data:image/svg+xml URI. No per-color icon files.
  • Six built-in effects โ€” static, blink, breath, rainbow, heartbeat, bounce โ€” all driven by JavaScript, since favicons don't play SVG CSS animations.
  • Fully configurable, applied live โ€” every state's color, effect and cycle speed, plus the asking/done hold timings, apply to the running tab within ~1 s โ€” no reload, no restart.
  • Built-in settings UI, zero YAML โ€” a Favicon indicator card in the DSH settings page edits the whole config with live color-swatch previews and persists it to settings.yaml for you (path below).
  • Background-tab & restart-proof โ€” animated states keep a wall-clock fallback while requestAnimationFrame is paused in hidden tabs, and the status poll self-heals across host restarts.
  • Active-agent count at a glance โ€” while more than one agent is active (non-idle: asking / running / done), the favicon switches from the whale to a full-frame number block showing the live count (up to 99+), colored and animated exactly like the whale would be in that state; back to the whale when 0โ€“1 agents are active. (Same visual language as the ๆปกๅน…ๆ•ฐๅญ— channel in demo/badge.html.)

๐Ÿ›  Configuration UI โ€” how to get there

#Step
1Open the DSH Web GUI and go to Settings / ่ฎพ็ฝฎ.
2In the Plugins / ๆ’ไปถ tab, open Plugin config / ๆ’ไปถ้…็ฝฎ.
3Find the Favicon indicator / ๆ ‡็ญพ้กตๅ›พๆ ‡ๆŒ‡็คบๅ™จ card.
4Expand a state row (idle / running / asking / done) to edit Effect / ็‰นๆ•ˆ, Colors / ้ขœ่‰ฒ (each swatch is a native color picker) and Cycle (ms) / ๅ‘จๆœŸ๏ผˆๆฏซ็ง’๏ผ‰ (shown only for animated states โ€” static states have no cycle); use Asking hold / ๆ้—ฎ้ฉป็•™ and Done hold / ๅฎŒๆˆ้ฉป็•™ for the two timings.

Changes are saved through the settings transport into the profile's settings.yaml and applied to the running tab within ~1 s โ€” no reload, no restart. See Configure for the full key reference.

๐ŸŽฌ Default configuration, visualized

The four default states, exactly as they appear in the browser tab (the asking whale really blinks):

Default states: idle dark whale, running yellow, asking red/yellow blinking, done green

StateDefault colorDefault effect
idle#1a1a1a โ€” deep whalestatic
running#FACC15 โ€” yellowstatic
asking#E5484D โ‡„ #FACC15 โ€” red/yellowblink (400 ms)
done#22A06B โ€” greenstatic, stays doneHoldMs, then back to idle

โœจ All effects, animated

Every preview below is the real whale path, animated the same way the plugin renders it (the previews are self-contained animated SVGs โ€” they play right in your browser):

EffectWhat it doesPreview
staticA single colored frame, no motion โ€” uses colors[0]static effect preview
blinkToggles colors[0] โ‡„ colors[1] (a darker second color is derived if missing) over speedblink effect preview
breathPulsates smoothly between colors[0] and colors[1] (derived if missing) over speedbreath effect preview
rainbowUses colors[0] as the starting hue, then cycles the color wheel over speedrainbow effect preview
heartbeatScale pulses with a sharp lub-dub beat over speed โ€” color is colors[0]heartbeat effect preview
bounceThe whale hops up and down over speed โ€” color is colors[0]bounce effect preview

Want to tweak colors and watch the tab favicon change live? Open the self-contained demo (demo/dynamic-color.html) โ€” pick a state + effect, edit colors, and the favicon updates in real time (no build, no dependencies).

Install

This is a standard DSH bundle plugin. Install it into the web profile (the GUI/TUI profiles pick it up automatically through the cordis patch layer).

From npm (recommended):

dsh plugin --profile web add dsh-web-icon-indicator@latest

From the Git source:

dsh plugin --profile web add github:waknow/dsh-web-icon-indicator

Or from a local directory / tarball:

dsh plugin --profile web add <path-or-tarball>

Or drop the directory into ~/.dsh/profiles/web/node_modules/<name>/ and ship a cordis.patch.yml that matches the one shipped here.

Configure

All keys are optional; defaults shown.

KeyDefaultMeaning
iconsDir<package>/icons/Directory holding the single base.svg
statusPath/dsh-web-icon-status.jsonJSON status endpoint
iconPathPrefix/dsh-web-icon-indicatorURL prefix base.svg is served under
askingHoldMs3500Minimum visibility of the asking state
doneHoldMs5000Time the done state stays before falling back to idle
statessee belowPer-state visual config

Each entry in states is one object per state: { effect, colors[], speed? }:

config:
  states:
    idle:    { effect: static,    colors: ['#1a1a1a'] }
    running: { effect: static,    colors: ['#FACC15'] }
    asking:  { effect: blink,     colors: ['#E5484D', '#FACC15'], speed: 400 }
    done:    { effect: static,    colors: ['#22A06B'] }
  • effect โ€” one of static | blink | breath | rainbow | heartbeat | bounce.
  • colors โ€” an array of hex colors. colors[0] is the primary. Multi-color effects read more entries: blink uses colors[0]โ‡„colors[1], breath breathes colors[0]โ‡„colors[1] (each derives a darker second color if omitted), rainbow uses only colors[0] as the starting hue.
  • speed โ€” optional per-state cycle length in ms (also the blink toggle interval). Default 1200.

Entries are shallow-merged over the defaults, so you can override only a few states. Example:

- id: dsh-web-icon-indicator
  name: 'dsh-web-icon-indicator'
  config:
    states:
      running: { effect: breath,    colors: ['#FF9900', '#FFD9A0'], speed: 900 }
      asking:  { effect: rainbow,   colors: ['#FF0000'] }
      done:    { effect: heartbeat, colors: ['#2ECC71'] }

Settings page & settings.yaml (DSH โ‰ฅ rc7)

The plugin registers the whole config surface above with the DSH settings service under the web-icon-indicator namespace (a schemastery schema in lib/index.js):

  • Web GUI: open ่ฎพ็ฝฎ โ†’ ๆ’ไปถ โ†’ ๆ’ไปถ้…็ฝฎ โ€” a Favicon indicator card edits the same keys (asking/done hold, and per-state effect / colors / cycle), staged and saved through the settings transport. Each state is a collapsible row showing a color dot and a one-line summary (blink ยท #E5484D โ‡„ #FACC15 ยท 400ms); expanding a row reveals its three fields, and the colors field previews parsed swatches live.
  • Persistence: values land in the profile's settings.yaml (default ~/.dsh/settings.yaml) as a web-icon-indicator: section. The composition entry stays the base layer; resolution order is schema defaults โ†’ composition entry โ†’ settings document user layer.
  • No server restart, no tab reload for settings-card saves: askingHoldMs / doneHoldMs apply live host-side, and per-state visual config (effect / colors / cycle) is synced into the running tab through the status poll within ~1 s. Only code-level default changes in lib/index.js need a tab reload (or a DSH web rebuild).
  • The browser half is a hand-written lib/client.js (ModuleLoader factory format โ€” no build step, no runtime deps beyond the shell's react). The DSH client scanner picks a new dsh.client declaration up on the next profile start.
  • Deployments without a settings service are unaffected: the plugin falls back to reading the composition entry exactly as before.

How it works

  • Host plugin with a small browser half: registers routes on the existing webServer โ€” the status JSON endpoint, a static /dsh-web-icon-indicator/base.svg (the whale template), and one tapIndex that injects a small browser script into every served index.html. The config surface is registered with the DSH settings service (web-icon-indicator namespace) for validation, persistence, and the settings-page card (see above).
  • Status is aggregated across live agents.list() with priority asking > running > done > idle. The aggregation runs a reconcile() step on every request to detect running โ†’ idle transitions, because agent/status's idle delivery is not guaranteed at turn end. The status endpoint also reports active โ€” the number of non-idle agents โ€” and while that count is > 1 the injected script renders a full-frame count block (the ๆปกๅน…ๆ•ฐๅญ— channel of demo/badge.html: a rounded block filled with the same per-frame state color/effect as the whale, bold white count sized 31%โ€“52% of the icon, capped at 99+) instead of the whale, so the tab shows how many agents are busy at once even in a pinned 16px tab.
  • ask_user_question tool calls (via tools/pre-execute / tools/result) flip the session into asking with a configurable minimum-hold so the icon stays visible even when the user answers immediately.
  • Permission / sandbox-interception waits are also surfaced as asking: when the agent hits a sandbox denial and escalates (sandbox_permissions + justification), or any other tool asks for approval, the approval service appends an approval/asked session event and blocks the agent until you decide. The plugin watches session/event (with an authoritative fold over the live session log as a fallback) and pins the session into the asking state for that whole wait, clearing it on approval/decided.
  • The browser script polls /dsh-web-icon-status.json once a second, fetches base.svg once, and then on every requestAnimationFrame tick rebuilds the favicon as a data:image/svg+xml,โ€ฆ URI โ€” replacing the __COLOR__ placeholder with the state's configured color and applying the state's configured effect. The status response also echoes the current per-state visual config, so a settings save reaches the running tab on the next poll (~1 s) without a reload. Browsers don't play favicon SVG CSS animations, so all motion is JS-driven. Because browsers pause requestAnimationFrame in hidden tabs, the poll also repaints a wall-clock frame for animated states, so background tabs keep animating (coarsely) instead of freezing; full-speed animation resumes when the tab is visible again. The poll also survives host restarts: a transient fetch failure restores the original icon and retries on the next tick (the SPA reconnects in place, so the icon comes back without a manual refresh).

Browser support & known limitations

The favicon is a plain image, so browsers never run the SVG's own CSS/JS animation inside the tab UI โ€” every frame is rendered here in JavaScript. How well a changing favicon is displayed differs by browser:

BrowserSVG faviconLive per-state color/effectWhy
Chrome / Edgeโœ…โœ… smoothRe-reads <link rel=icon> live; data:-URI SVGs are fine.
Firefoxโœ…โœ… smoothRenders SVG favicons well (and honors their prefers-color-scheme, unused here).
Safari (macOS)โœ… rendered staticโš ๏ธ best-effortIgnores in-SVG CSS; aggressive icon caching.
Safari (iOS)โœ… rendered staticโš ๏ธ rarelyUnlikely to refresh without revisiting the tab.

Known limitations (current as of Safari 26.3):

  • Favicons have their own cache. Chrome keeps a favicon database, Firefox a favicons.sqlite, and Safari a system-level icon cache โ€” none of which a normal clear cache touches, and WebKit even caches the "no icon" case. That is why a changed icon can linger for an existing tab. The plugin already mitigates this: it serves base.svg and the status endpoint with Cache-Control: no-store, bundles a freshness query (?t=Date.now()) on its fetches, and replaces the <link rel=icon> node on each state change.
  • Safari renders SVG favicons but ignores their internal CSS โ€” no @media, no prefers-color-scheme, no CSS animation. So all recoloring must be baked into each frame's markup (which the plugin does) rather than driven by CSS variables.
  • data:-URI SVG favicons are unreliable in Safari (WebKit bug 236616, still open; reproduced on Safari 17.6). The plugin currently builds each frame as a data:image/svg+xml URI, so on Safari the tab icon may not render at all โ€” the biggest known gap.
  • Dynamic JS updates in Safari are hit-or-miss; they may require a reload, and Safari "locks onto" the first icon it sees. There is no guaranteed, spec-supported way to swap a favicon live in Safari today.
  • Pinned-tab icon (<link rel="mask-icon">) uses its own cache, separate from the regular favicon, and is a single-colour silhouette tinted by the color attribute โ€” macOS + pinned-tab only, read at page load, not live.

Full mechanics with sources (WebKit bugs, Stack Overflow, browser-engineering blogs) and a recommended path toward smoother Safari colour changes live in docs/safari-favicon-research.md.

Caveats

  • Favicon SVG CSS animations do not run inside the browser's tab UI โ€” all effects are produced in JavaScript by rebuilding the data-URI each frame. This is a deliberate, zero-dependency design. (The animated previews in this README are demo assets for illustration only โ€” the favicon itself is JS-animated.)
  • Favicon behavior differs by browser, and Safari is the most limited โ€” see Browser support & known limitations.
  • The base template must keep its __COLOR__ placeholder in the #p { fill: โ€ฆ } rule; the browser replaces that token to color each frame.
  • The plugin runs in the host plane; it must be mounted into a profile's composition, not a session-scoped agent preset.
  • File reads go through the fs service with the configured iconsDir as cwd. Make sure that path is readable under your deployment's sandbox policy.

License

MIT