# dsh-widget-center
**English | [中文](./README.md)**
DSH Widget Center — native macOS desktop widgets, multi-instance

License version platform tests

Shares Widget Notes Widget Multi-instance Native windows market_quote tool Type Studio

NSPanel + WKWebView standalone processes —— settings.section slot · /widget-center/* API · Type Studio session pipeline
> **dsh-widget-center** is a DSH plugin that manages any number of native macOS > desktop widgets — **shares** quote tickers and **notes** memos — each an > independent NSPanel window with its own config and lifecycle, edited as > instance cards in the DSH settings UI. Quotes come from free public APIs via > a resident host process: zero tokens, multi-source fallback, plus a > `market_quote` model tool any session can call. UI copy is bilingual > (zh default / en, following the DSH language and the browser's > Accept-Language respectively). ## Interface ![Widget Center settings page: instance cards with runtime status and quick actions](docs/settings-list.png) ## Features ### 🧩 Widget instances (multi-instance) - Each instance is one independent desktop window: `type` + name + window geometry + type-specific config. Run multiple shares tickers and notes memos side by side. - **Shares**: watchlist (canonical symbols, groups), CN red-up/US green-up color schemes, stale ⚠ marker, sparkline, US pre/post market, trading-phase-aware throttling. - **Notes**: multi-line text (≤50 lines × 500 chars/line); the desktop window follows edits within 30s — no re-show needed. ### 🖥 Native macOS desktop windows (primary surface) - Single-file Swift app compiled lazily via `swiftc` (binary cached): borderless non-activating NSPanel + WKWebView — no dock icon, floats above normal windows, visible on every space. - **Draggable anywhere**; **type-aware right-click menu** (refresh/reload · always-on-top · reset size · quit), localized window titles (follows the macOS language). - `detached` standalone processes: already-open widgets survive DSH restarts; one pidfile per instance (`widget-.pid`), binary shared by all instances. - Fallback entry: chrome `--app` command (browser window, equivalent, labeled as a fallback). ### ⚙️ Widget Center settings page (three views) - DSH settings sidebar "Widget Center" page (`settings.section` slot, additive). - **List**: one compact card per instance — type badge / name / runtime status / one-line summary + "Show on desktop / Hide" quick actions; a single create entry, no save bar (the list always mirrors persisted state). - **Create**: the create entry opens a type picker; clicking a type creates the instance with defaults and jumps into its detail view. - **New type (Type Studio)**: the "+ New widget type" button beside "New widget instance" on the list page → a pure client pipeline (`sessions.create` attached to the current workspace with the creation-mode `cordis` preset, falling back to standard) creates a **not-started** session with a pinned title and the issue-template prompt pre-filled into the composer; complete the `{{placeholders}}` and send to begin. - **Detail**: click a card — a dedicated "‹ Back to list" nav row plus the full config editor (name / window / autostart / enable / type-specific config / market_quote switch) and delete (two-step confirm). - Saving issues one full-replace `PUT /api/instances`; create and delete persist immediately; the host validates and hot-applies, keeps state across restarts, and answers 400 with reasons on invalid input. ### 📡 Data layer & model tool - Five providers (tencent / eastmoney / sina / yahoo / binance) with priority fallback; GBK transcoding, sina Referer, proxy retry (socks5, needsProxy sources only), trading-phase TTL throttling, disk cache with stale serving. - `market_quote(symbols[])` model tool: standardized quotes for any session (always cite the returned `ts`); the switch lives in the shares instance config (`conversationTool`). ## Quick start ```bash # Install (DSH ≥ 0.1.1-rc.6) dsh plugin --profile web add github:NattoCB/dsh-widget-center # Plugin-set changes need a dsh web restart; client changes hot-reload on refresh ``` - **Native widget prerequisite**: `swiftc` (Xcode Command Line Tools). The first "Show on desktop" compiles automatically (seconds); the binary is cached at `~/.dsh/storages/widget-center/SharesWidget`. - Usage: DSH settings UI → "Widget Center" sidebar → "Show on desktop" on a card → a window appears on the desktop; drag to position; ask the agent "what's 159845 at" → the `market_quote` tool answers. ## Symbols | Form | Market | Primary source | |---|---|---| | `sh000852` `sz399006` | A-share index | tencent → eastmoney → sina | | `sz159845` `sh560010` | A-share ETF | same | | `sh600519` etc. | A-share stocks | same | | `hk00700` | HK stocks | tencent (yahoo fallback) | | `usAAPL` | US stocks | tencent (yahoo fallback) | | `fx_susdcny` | FX | sina (yahoo fallback) | | `binance:USDCUSDT` | Crypto | binance | | `nf_CU0` | Futures | **not implemented** (unsupported symbols land in `failed`) | ## Settings ($DSH_HOME/storages/widget-center/settings.json, v2 instances model) ```jsonc { "version": 2, "instances": [ { "id": "shares-1", "type": "shares", "name": "Shares", "enabled": true, "autoStart": true, "window": { "x": 120, "y": 240, "w": 380, "h": 460 }, "config": { "conversationTool": true, "watchlist": [ /* … */ ] } }, { "id": "notes-1", "type": "notes", "name": "Notes", "enabled": true, "autoStart": true, "window": { "x": 160, "y": 160, "w": 260, "h": 320 }, "config": { "lines": ["first line", "second line"] } } ] } ``` Shares `config` keys (full table in [PRD §6](./PRD.md)): `conversationTool` (instance-level market_quote switch), `watchlist`, `refreshIntervalSec` (5–600), `openTtlSec`/`closedTtlSec` (trading-phase TTL), `providerPriority`, `proxy` (default off; socks5/socks5h only), `theme`/`upsideGreen`/ `showSparkline`/`extendedHours`. Notes `config`: `lines: string[]`. The v1 single-widget settings and the v0.10 top-level `conversationTool` migrate automatically. ## HTTP API (mounted under `/widget-center/`, JSON envelope `{ok,data}`/`{ok:false,error}`) ``` GET /api/instances # {instances[+widget{running,pid}]} PUT /api/instances # full replace, 400 on invalid; hot-applied POST /api/instances/:id/show # lazy compile + detached window start POST /api/instances/:id/hide # SIGTERM + pidfile cleanup GET /api/quotes?symbols=sh000852,usAAPL # omit symbols = first enabled shares watchlist GET /api/health # {providers, cacheSize, instance summary} GET /dashboard/:id # per-instance desktop HTML (copy follows Accept-Language) ``` (Type Studio runs through the client sessions service — no HTTP route.) ``` ``` ## Development ```bash node --test # 63 unit tests (parser/phases/settings/service/routes/widget-host/dashboard) node scripts/verify-live.mjs # live six-symbol run + source-outage fallback demo (read-only) swiftc -O -swift-version 5 -o /tmp/SharesWidget src/widget/WidgetApp.swift # manual compile check ``` Local development install: clone this repo and run `dsh plugin --profile web add file:` (the file: dependency points at the directory; the source tree is the source of truth); after edits, sync the installed copy with `cp -R src client cordis.patch.yml package.json ~/.dsh/profiles/web/node_modules/dsh-widget-center/`. ## Deviations from the PRD (decision record) 1. **Proxy transport**: FR-1.6 specified undici ProxyAgent + socks-proxy-agent; undici cannot be imported from a plugin bundle, so the proxy is socks5/socks5h only. After all providers fail, the request retries through the proxy and the last working channel is remembered — this covers the measured reality that eastmoney direct connections are dropped while the proxy path works. 2. **Futures `nf_*`**: listed in FR-1.1's symbol table, no provider implemented; requests land in `failed`. 3. **Holidays**: weekend detection only (PRD R6); a misjudged open/closed trading phase on cn/hk holidays only affects refresh cadence, and a closed-day misjudgment serves the latest traded snapshot — trust the returned `ts`. 4. **No system-level desktop-layer pinning**: NSPanel .floating sits above normal windows; true desktop-icon-layer pinning (WidgetKit) needs an app bundle + sandbox signing, under evaluation. 5. **Proxy default off (v0.12)**: the shipped default must not assume a local socks service; the URL stays as an example. ## Known pitfalls - Sina requires `Referer: https://finance.sina.com.cn` or it answers 403 (baked into the provider). - eastmoney push2 502s with long field lists / high frequency; currently mitigated by "short field set + proxy retry + natural rate recovery". - The desktop pages (`src/dashboard.js`) and the settings page are two independent implementations (client bundles cannot import host modules); keep quote-row visuals in sync across both. --- - Design & implementation decisions: [PRD.md](./PRD.md) (source of truth) - License: MIT