# dsh-widget-center
**English | [中文](./README.md)**
> **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

## 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