dsh-tray-flash: Let DSH flash to remind you of unfinished tasks on Windows.

Introduction

Everyone running DSH encounters a situation: the Agent finishes a turn or stops waiting for your approval or answer, but the window isn’t in the foreground. You switch over to check the sidebar, and the yellow and green dots are still there—the matter hasn’t actually been handled. The system’s notification bar won’t watch these dots for you.

dsh-tray-flash does exactly this: map the status of the small dots on the sidebar to the appearance and disappearance of the Windows notification area tray icon, while also flashing the taskbar button. It’s just like when QQ or WeChat receives a message—the icon disappears and reappears until you’ve actually looked at it and clicked to dismiss it.

The plugin introduced below is maintained by xymy666, belongs to the “Client” category in the DeepSeek Harness community plugin directory, and the repository address can be found at the end of the article.

Plugin Positioning

One sentence: Add a persistent visual reminder of “unread/waiting” to the DSH desktop session.

It consists of two halves, running in different locations:

Half Location Function
Host lib/index.js Blinking, changing tray icon, receiving dot status
Client lib/client.js Read dots in the browser, report to Host

This division determines that it relies on DSH’s desktop profile (Electron app) to function; on other platforms, the plugin will act silently.

When it flashes

There are three types of trigger conditions:

Trigger Description
Session ends Any session’s turn/end, skipping rounds that were interrupted or cancelled
Waiting for approval Tool requests human approval (approval/asked)
Waiting for answer Model calls ask_user_question, including plan approval

There are three non-trigger conditions: sub-agent sessions (ignoreSubagent, default on), background tasks, and scheduled tasks.

One pitfall regarding the main session needs to be mentioned. In the DSH source code, the main session never produces green dots. This means that after the main session completes a turn, no dots appear at all. Following the logic of “stop only when dots disappear,” the blinking would never stop. The plugin’s solution is to record a virtual unread (virtualMainDot) for the main session, which is cleared when the window regains focus—this is the closest equivalent to “you have seen this turn.”

When it stops

When all dots disappear. The dot is the small circle preceding the sidebar session row:

Dot Meaning Who is responsible
Yellow dot Has approval / plan approval / question waiting for you Client’s pendingInteraction
Green dot Background session completed a round, and you haven’t looked yet Client’s completionUnread

When there are real dots, focus, blur, switching sessions, or mouse movement cannot end the blinking prematurely; these are just “bumping into the window.” The only exception is the virtual main dot mentioned in the previous section, which is designed to be cleared by focus.

There are two other situations:

  1. Window is closed or destroyed → stop;
  2. Client disconnects (page not loaded, or no report for more than dotsStaleSeconds) → fallback to “stop when window gains focus.”

No time limit. The blink continues as long as dots are pending. The default maxFlashSeconds: 0 means no timeout; to set a safe value, enter a positive number, e.g., maxFlashSeconds: 600.

A lesson in parameter tuning worth remembering: Early versions had a default timeout of 120 seconds, resulting in permanent stopping of flashing if a green dot hung for over two minutes. The reason was that “dot count unchanged” would not re-trigger the blink, and the 30-second heartbeat reported the same number, causing the notification to be lost. Therefore, the default was changed to unlimited.

An edge case not covered by the original rules needs to be mentioned: if the window is already in the foreground and the just-completed session is the one you are currently watching, there are no dots to wait for. “Window in foreground” was already established before the turn ended, so it cannot be counted as you having already seen it—the result is a single flash then stop, leaving the person staring at the screen without feedback.

flashUntilMouseMoves (default on) changes the confirmation signal from “window gains focus” to “mouse moves.” It only takes effect when there are no real yellow/green dots:

Situation Stop condition
Starts blinking when window is in background, and no real dots Stop when switching back to window
Window in foreground, current session completed, and no real dots Stop when mouse moves, switches sessions, or window loses focus
There are still yellow/green dots in sidebar Focus, blur, session switching, and mouse movement don’t count; keep blinking until dots reach zero

The last line is a hard rule. Version 0.5.1 applied the mouse rule to all foreground-triggered blinks, resulting in users clicking the main window and stopping the blink even though dots were still hanging in the sidebar. Version 0.5.2 changed it to prioritize real dots. Mouse detection uses the existing 1-second polling, does not create new timers, and counts any 1-pixel movement.

How the tray icon flashes

DSH’s tray icon is held by the Electron main process. The plugin uses Shell_NotifyIconW(NIM_MODIFY) to swap the icon in place on the original, without creating a new icon, touching the tooltip, or the right-click menu, only setting NIF_ICON.

The blinking is an alternation of the original icon ↔ fully transparent. The transparent one isn’t a separately drawn image, but an icon with all alpha channels set to 0:

node scripts/build-icon.mjs        # Output: assets/blank.ico

The reason swapping the default icon for fully transparent works lies in the fact that the shell recognizes the alpha channel, not the AND mask. The author tested this locally on Windows 11 26H2:

Syntax Result
alpha all 0 + AND mask all 1 Becomes completely blank
alpha 255 + AND mask all 1 Renders as a black square
1bpp icon Also becomes blank, screenshot hash matches the first one

Another property must be confirmed: whether it is still usable during the transparent period. Testing shows Shell_NotifyIconGetRect still returns the same rectangle during the transparent frame period; the icon is just invisible, and the slot and click are unaffected.

Three safety properties:

  • DesktopTray only calls setToolTip and setContextMenu during runtime, never changing the icon, so switching languages won’t brush the transparency back;
  • The icon ID is probed at runtime (trying each one using Shell_NotifyIconGetRect), not hardcoded;
  • Application restart definitely rebuilds the tray icon, and crashes won’t leave a permanent blank icon.

When the tray is unavailable (host window not found, icon load failed, shell rejection), it automatically falls back to only flashing the taskbar, with a single warning in the logs.

Installation and Enabling

A note before installation: the desktop profile is exclusive to the Electron application, so dsh plugin --profile desktop will error directly, and the plugin manager’s install_bundle may not be available. Therefore, installation is equivalent to pnpm’s link:, requiring three manual steps.

  1. Backup existing configuration:
Copy-Item "$env:USERPROFILE\.dsh\profiles\desktop\package.json" `
          "$env:USERPROFILE\.dsh\profiles\desktop\package.json.bak-tray-flash"
  1. Add a line in dependencies and append the name to the end of dsh.profile.bundles:
"dsh-tray-flash": "link:E:/0_opencode/.00工作空间/dsh-tray-flash"
  1. Create a directory junction, equivalent to the output of pnpm link::
New-Item -ItemType Junction `
  -Path "$env:USERPROFILE\.dsh\profiles\desktop\node_modules\dsh-tray-flash" `
  -Target "E:\0_opencode\.00工作空间\插件制作\dsh-tray-flash"

Replace the paths according to where you actually store the plugin.

No need to modify ~/.dsh/profiles/desktop/cordis.patch.yml; the cordis.patch.yml bundled with this package will insert its own lines.

After modifying, restart DeepSeek Harness. The desktop profile does not have patchReload enabled, so the Host code must be restarted to take effect.

Rollback:

Copy-Item "$env:USERPROFILE\.dsh\profiles\desktop\package.json.bak-tray-flash" `
          "$env:USERPROFILE\.dsh\profiles\desktop\package.json" -Force
Remove-Item "$env:USERPROFILE\.dsh\profiles\desktop\node_modules\dsh-tray-flash" -Force

Configuration

Override the lines with the same name in cordis.patch.yml (at any level):

- insert:
    - id: tray-flash
      name: dsh-tray-flash
      config:
        stopWhenDotsClear: true   # Stop when all dots disappear (core behavior)
        virtualMainDot: true      # Create virtual unread for main session (clears on focus)
        dotsStaleSeconds: 60      # Consider Client disconnected if no report for this long
        trayBlink: true           # Tray icon blink (fade in/out) (false only flashes taskbar)
        trayBlinkIntervalMs: 600  # Tray blink half-cycle (100–5000)
        flashCaption: true        # Flash title bar too (false only flashes taskbar button)
        maxFlashSeconds: 0        # 0 = follow dots, never timeout (default); positive = safe upper limit
        onlyWhenUnfocused: true   # Don't flash if in foreground (only effective when dot status is unknown)
        flashUntilMouseMoves: true # In foreground + current session done, flash until you move mouse
        ignoreInterrupted: true   # Don't flash interrupted rounds
        ignoreSubagent: true      # Don't flash sub-agents
        flashTimeout: 0           # FlashWindowEx dwTimeout(ms)
        flashCount: 0             # FlashWindowEx uCount
        pollIntervalMs: 1000      # Polling interval (200–5000)
        allowTestFlash: true      # Register self-test route
        enabled: true             # Master switch

Illegal values will not cause the plugin to fail activation; they will be replaced with default values and logged once.

Self-Check

By default, the plugin registers three routes that only allow local same-origin access:

Route Method Function
/__tray-flash/self-test POST Flash once immediately, return hit window handle and config
/__tray-flash/state GET / POST Read state / Stop flashing
/__tray-flash/dots POST / DELETE Client reports dot count / Zero out dots

To see real-time dot status:

Invoke-RestMethod http://127.0.0.1:20120/__tray-flash/state | Select-Object -ExpandProperty state |
  Select-Object flashing, lastStop, dots

dots shows counts (how many yellow/green dots), virtualMainUnread (if the main session virtual unread is pending), and status (live / stale / absent).

Combined with scripts/selftest.mjs, you can perform end-to-end verification:

node scripts/selftest.mjs            # Default http://127.0.0.1:36001
node scripts/selftest.mjs 36001

The script triggers flashing, takes 12 frames of the taskbar, compares pixel-by-pixel, stops, and prints the judgment. This script requires DSH to be running and will briefly alter the real tray icon; it is not in unit tests.

Testing

node --test test/*.test.mjs

The repository has written 175 test cases covering dot tracking (counting, virtual unread, stale disconnect fallback, client restart), dot-triggered flashing (green dots appearing after turn/end, starting only when count increases, not restarting when already flashing, continuing to flash when dots stay unchanged for a long time), state machine (stop when dots disappear, real yellow/green dots prioritized over focus/mouse, focus fallback, timeout only effective when paired, deduplication, sub-agent and interruption filtering, tray linkage), Client half (dot count, throttling, not resending if unchanged, 30-second heartbeat, cleanup on unload, silent failure), tray flashing (ID probing, transparent frames, icon lifecycle, recovery, fallback), Win32 binding, config normalization, koffi fallback chain, local fence and routes, and Host entry activation and release.

There are three more verification scripts in the real environment (probe-tray.mjs, probe-tray-visual.mjs, probe-tray-encoding.mjs), see README for usage.

Applicable Scenarios and Notes

Suitable for people who: mainly use the DSH desktop client on Windows, usually let Agents run in the background, or encounter approval and question interactions, and hope to know “there are unfinished matters” without having to stare at the sidebar.

A few limitations to know:

  • Only effective on Windows; on other platforms the plugin acts silently;
  • Tray icon blinking relies on icon ID probing; if there are multiple tray icons in the same process, it will only take the first one probed;
  • Flashing uses FlashWindowEx, whose return value is unreliable; in a single successful call, you might read back 32768 or 0; truth is determined solely by whether the screen is actually flashing;
  • Host and Window are currently in the same process tree, relying on process.ppid to find the process holding the main window; this assumption needs to be re-evaluated after architectural changes;
  • The plugin runs with the current DSH process permissions, can access the desktop and notification area; you should check the source code and license yourself before installing (the repo is declared as MIT).

Regarding the ecosystem background: DSH’s philosophy is “everything is a plugin”; the community directory is an independent site with no official affiliation to DeepSeek / Hypersphere; it is not an official app store.

Summary

dsh-tray-flash does a very narrow thing: translate the yellow and green dots on the sidebar into the blinking (fade in/out) of the tray icon and the taskbar button, stopping only when all dots disappear. For people who often let Agents run in the background, this reminder is more reliable than watching window focus. Directory page and repository address:

  • Directory page: https://www.skillhub.cn/plugins/xymy666/dsh-tray-flash
  • Code repository: https://github.com/xymy666/dsh-tray-flash
羽毛球分组比赛记分
小程序二维码

欢迎使用《羽毛球分组比赛记分》微信小程序

Xiaoye