dsh-balance-plugin 0.1.0 → 0.2.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/PUBLISHING.md CHANGED
@@ -1,45 +1,53 @@
1
1
  # Publishing dsh-balance-plugin
2
2
 
3
- Step-by-step release manual for the two official distribution channels (both are supported and can coexist).
3
+ Release manual for the supported npm and GitHub Release tarball channels.
4
4
 
5
5
  ## Prerequisites
6
6
 
7
7
  - [npm](https://docs.npmjs.com/cli/) account, logged in: `npm whoami`
8
8
  - A GitHub account; the repository will be public.
9
9
 
10
- ## 1. Create the GitHub repository
10
+ ## 1. Build and verify
11
11
 
12
- 1. On GitHub, **New repository** → name `dsh-balance-plugin`, public.
13
- 2. **Do not** initialize with README/.gitignore (this repo already has them).
14
- 3. Add the local remote and push:
12
+ ```sh
13
+ # from plugins/dsh-balance-plugin
14
+ pnpm run build
15
+ pnpm test
16
+ pnpm pack --dry-run # must contain lib/, cordis.patch.yml, README files, and LICENSE
17
+ ```
18
+
19
+ In an isolated DSH `0.1.5-rc.1` or later profile, verify both installation and update behavior:
15
20
 
16
21
  ```sh
17
- cd ~/Documents/dsh-balance-plugin
18
- git remote add origin git@github.com:<you>/dsh-balance-plugin.git
19
- git push -u origin main
22
+ dsh plugin --profile demo add ./dsh-balance-plugin-<version>.tgz
23
+ dsh plugin --profile demo update dsh-balance-plugin
24
+ dsh --profile demo --dump-config # expect "# == dsh-balance-plugin"
20
25
  ```
21
26
 
22
- 4. On the repository page: **About → Topics → add `dsh-plugin`** (the official discovery topic from DeepSeek Harness's README). Also consider `deepseek-harness`.
27
+ Restart the Web profile and hard-refresh the browser to verify the authenticated balance readout.
23
28
 
24
29
  ## 2. Publish to npm
25
30
 
26
31
  ```sh
27
- # from ~/Documents/dsh-balance-plugin
28
32
  pnpm run build && pnpm test # gates
29
- npm version patch # bumps to 0.1.1, creates the git tag
33
+ npm version patch # creates the release version and git tag
30
34
  pnpm pack # inspect the tarball contents
31
35
  npm publish # publishes prebuilt lib/ — users install with zero friction
32
36
  git push && git push --tags
33
37
  ```
34
38
 
35
- npm is the **recommended** channel: users run `dsh plugin --profile <name> add dsh-balance-plugin` and get prebuilt code with no install-time build or permission prompt.
39
+ npm is the **recommended** channel: users run `dsh plugin --profile <name> add dsh-balance-plugin` and get prebuilt code with no install-time build or permission prompt. Existing npm users upgrade with `dsh plugin --profile <name> update dsh-balance-plugin`.
36
40
 
37
- ## 3. GitHub installs (optional, automatic once the repo exists)
41
+ ## 3. Publish a GitHub Release tarball
38
42
 
39
- Users can also install directly from GitHub. Two official caveats (from `docs/user/develop/basic/publish.md`):
43
+ The package is in this monorepo's `plugins/` subdirectory, so `github:` package specs cannot target it. Publish the prebuilt tarball instead:
44
+
45
+ ```sh
46
+ pnpm pack
47
+ gh release create v<version> dsh-balance-plugin-<version>.tgz --title "dsh-balance-plugin <version>" --notes-file <notes-file>
48
+ ```
40
49
 
41
- - Git installs fetch **source**, not build output the package ships a self-contained `prepare` script (`node scripts/build.mjs`, no project references), which pnpm runs after install;
42
- - pnpm ≥ 10 refuses to run git dependencies' `prepare` until the user opts in via `allowBuilds` in the profile's `pnpm-workspace.yaml`. Document this in your README (already present).
50
+ Users install or replace a tarball release with `dsh plugin --profile <name> add ./dsh-balance-plugin-<version>.tgz`, then run `dsh --profile <name> --dump-config` and restart DSH.
43
51
 
44
52
  ## 4. Versioning and release notes
45
53
 
@@ -47,16 +55,7 @@ Users can also install directly from GitHub. Two official caveats (from `docs/us
47
55
  - Create a GitHub Release per tag with the changelog; reference the npm version.
48
56
  - Semantic versioning: `patch` for fixes, `minor` for features, `major` for breaking changes.
49
57
 
50
- ## 5. Verification checklist (official inspection points)
51
-
52
- ```sh
53
- # fresh install from the tarball
54
- dsh plugin --profile demo add ./dsh-balance-plugin-0.1.0.tgz
55
- dsh --profile demo --dump-config # expect "# == dsh-balance-plugin" layer
56
- # then in the web profile with a real credential: the readout renders
57
- ```
58
-
59
- ## 6. Local development loop
58
+ ## 5. Local development loop
60
59
 
61
60
  ```sh
62
61
  pnpm install
package/README.md CHANGED
@@ -2,9 +2,11 @@
2
2
 
3
3
  DeepSeek wallet balance at the sidebar foot — a [DeepSeek Harness](https://github.com/deepseek-ai/deepseek-harness) plugin.
4
4
 
5
+ > **DeepSeek-only**: this plugin targets the DeepSeek platform exclusively — the official DeepSeek balance endpoint and `DEEPSEEK_API_KEY`. No other platforms or providers are supported.
6
+
5
7
  English | [中文](README.zh.md)
6
8
 
7
- Shows your DeepSeek account balance as a **card icon + amount** in the sidebar footer (beside Settings), tinted by how much is left. Refreshes when a conversation turn ends, on click, and on mount — no polling.
9
+ Shows your DeepSeek account balance as a **card icon + amount** in the sidebar footer (beside Settings), tinted by how much is left. Refreshes when a conversation turn ends, on click, and on mount — no polling. When the amount changes, a signed delta (`+¥3.14` / `-¥1.97`) floats up and fades next to it — every spend (or top-up) is visible at a glance.
8
10
 
9
11
  ![screenshot placeholder: sidebar foot showing `💳 ¥12.34` in green](docs/screenshot.png)
10
12
 
@@ -16,12 +18,13 @@ Shows your DeepSeek account balance as a **card icon + amount** in the sidebar f
16
18
  - zero or below: red
17
19
  - **Refresh on turn end**: the host pushes an SSE `refresh` event when any conversation turn ends — wallet spend settles with the turn.
18
20
  - **Click to refresh**; in-flight refreshes are deduped (a click or turn-end tick during a pending query is a no-op).
21
+ - **Float delta**: on a visible amount change, a signed delta floats up and fades out — green when the balance rises, red when it falls. Zero or sub-cent changes are silent, the first load never floats, and the animation is suppressed under `prefers-reduced-motion`.
19
22
  - **Silent failure**: unconfigured credentials or a failed fetch render nothing — the readout is advisory and never surfaces an error.
20
23
  - **Original currencies**: balances are displayed as the provider reports them; no conversion, no summing.
21
24
 
22
25
  ## Install
23
26
 
24
- Requires a DeepSeek account credential. The plugin resolves `DEEPSEEK_API_KEY` through the harness credential seam (`~/.dsh/.credentials.yaml` or the environment), exactly like the official DeepSeek adapter.
27
+ Requires DeepSeek Harness `0.1.5-rc.1` or later and a DeepSeek account credential. The plugin resolves `DEEPSEEK_API_KEY` through the optional harness credential service (`~/.dsh/.credentials.yaml`) or the environment.
25
28
 
26
29
  ### From npm (recommended)
27
30
 
@@ -29,20 +32,27 @@ Requires a DeepSeek account credential. The plugin resolves `DEEPSEEK_API_KEY` t
29
32
  dsh plugin --profile <name> add dsh-balance-plugin
30
33
  ```
31
34
 
32
- ### From GitHub
35
+ ### From the GitHub Release (no npm account needed)
36
+
37
+ Download `dsh-balance-plugin-<version>.tgz` from the [releases page](https://github.com/luokai-demo/dsh-plugins/releases), then:
33
38
 
34
39
  ```sh
35
- dsh plugin --profile <name> add github:<you>/dsh-balance-plugin#<commit-sha>
40
+ dsh plugin --profile <name> add ./dsh-balance-plugin-<version>.tgz
36
41
  ```
37
42
 
38
- Git installs fetch source, so pnpm 10 refuses the install-time build until you allow it. Copy the exact package key pnpm prints into the profile's `pnpm-workspace.yaml`:
43
+ Note: the plugin lives in the `plugins/` subdirectory of the `dsh-plugins` monorepo, so `dsh plugin add github:...` cannot target it use the tarball (or npm).
39
44
 
40
- ```yaml
41
- allowBuilds:
42
- dsh-balance-plugin: true
45
+ ### Update an existing install
46
+
47
+ For npm installs, update the package in the same profile, confirm that its bundle layer remains active, then restart the Web server:
48
+
49
+ ```sh
50
+ dsh plugin --profile <name> update dsh-balance-plugin
51
+ dsh --profile <name> --dump-config # expect a "# == dsh-balance-plugin" layer
52
+ # restart dsh web, then hard-refresh the browser
43
53
  ```
44
54
 
45
- then re-run the `add` command. Only install packages whose source you trust, and pin the commit.
55
+ `dsh plugin` forwards pnpm commands and reconciles the installed package's `dsh.bundle` declaration after a successful update. For a Release tarball, run `dsh plugin --profile <name> add ./dsh-balance-plugin-<version>.tgz` with the newer archive, then perform the same verification and restart.
46
56
 
47
57
  ## Configuration
48
58
 
@@ -63,7 +73,7 @@ The plugin ships with sensible defaults; override via the bundle's plugin row:
63
73
 
64
74
  | Half | What it does |
65
75
  |---|---|
66
- | **Host** (Node) | Registers `GET /dsh-balance` (the wallet readout as JSON) and `GET /dsh-balance/events` (an SSE stream). Listens on the harness's `session/event` broadcast and emits a `refresh` SSE event per `turn/end`. Resolves the credential **per request** through `ctx.credentials`, falling back to the environment — a changed key applies without a restart. |
76
+ | **Host** (Node) | Registers authenticated `GET /dsh-balance` (the wallet readout as JSON) and `GET /dsh-balance/events` (an SSE stream). Each route passes the Harness browser-auth and Origin fence before reading a credential. It listens on `session/event` and emits `refresh` per `turn/end`, resolving credentials **per request** through the optional service before falling back to the environment. |
67
77
  | **Client** (browser) | Registers the `sidebar.footer.action` slot entry (the shell's reserved seat beside Settings; on the collapsed rail it stacks above the settings icon). Fetches the readout on mount, on `refresh` SSE events, and on click, with an in-flight guard. The card icon is self-contained (no dependency on the shell's icon library). |
68
78
 
69
79
  Position and interaction follow the shell's own design: the `sidebar.footer.action` hole is the official extension point for "optional actions beside Settings".
@@ -96,7 +106,7 @@ tests/ # node:test unit tests for balance-core
96
106
  ```sh
97
107
  pnpm install
98
108
  pnpm run build # emits lib/index.js + lib/client.js
99
- pnpm test # balance-core unit tests
109
+ pnpm test # balance-core, float, and host-route unit tests
100
110
  ```
101
111
 
102
112
  Local install check (official verification points):
@@ -111,7 +121,7 @@ dsh --profile demo --dump-config # expect a "# == dsh-balance-plugin" layer
111
121
  Before publishing a new version:
112
122
 
113
123
  1. `pnpm run build` and `pnpm test` pass.
114
- 2. Local install check above passes (`--dump-config` shows the layer; the web profile renders the readout with a real credential).
124
+ 2. Local install and `dsh plugin --profile demo update dsh-balance-plugin` both keep the `--dump-config` layer; the web profile renders the readout with a real credential.
115
125
  3. Version bumped (`npm version patch/minor/major`); git tag matches.
116
126
  4. `pnpm pack` — inspect the tarball: it must contain `lib/`, `cordis.patch.yml`, `README.md` (and nothing else heavy).
117
127
  5. `npm publish` — publishes prebuilt code, so users install with zero friction.
package/README.zh.md CHANGED
@@ -2,9 +2,11 @@
2
2
 
3
3
  侧边栏底部的 DeepSeek 钱包余额——一个 [DeepSeek Harness](https://github.com/deepseek-ai/deepseek-harness) 插件。
4
4
 
5
+ > **纯 DeepSeek**:本插件只针对 DeepSeek 平台——官方 DeepSeek 余额端点与 `DEEPSEEK_API_KEY`,不支持任何其他平台或服务商。
6
+
5
7
  [English](README.md) | 中文
6
8
 
7
- 在侧边栏底部(设置按钮旁)以**信用卡图标 + 金额**显示你的 DeepSeek 账户余额,并按剩余额度着色。在对话结束时、点击时与挂载时刷新——无轮询。
9
+ 在侧边栏底部(设置按钮旁)以**信用卡图标 + 金额**显示你的 DeepSeek 账户余额,并按剩余额度着色。在对话结束时、点击时与挂载时刷新——无轮询。金额变动时,带符号的差额(`+¥3.14` / `-¥1.97`)会从余额旁上飘淡出——每次花费(或充值)都一目了然。
8
10
 
9
11
  ![截图占位:侧边栏底部显示绿色的 `💳 ¥12.34`](docs/screenshot.png)
10
12
 
@@ -16,12 +18,13 @@
16
18
  - 零或以下:红色
17
19
  - **对话结束自动刷新**:宿主在任何对话轮次结束时推送 SSE `refresh` 事件——消耗随轮次结算。
18
20
  - **点击刷新**;在途刷新去重(查询进行中时点击或轮次结束触发均为 no-op)。
21
+ - **差额上飘**:金额发生可见变动时,带符号差额上飘淡出——余额上升为绿色、下降为红色。零变动与不足一分钱的变化静默,首次加载不上飘,且尊重系统 `prefers-reduced-motion`(减弱动态效果时停用动画)。
19
22
  - **失败静默**:凭证未配置或抓取失败时不渲染任何内容——读数仅供参考,绝不显示错误。
20
23
  - **原始币种**:余额按提供方报告显示;不换算、不加总。
21
24
 
22
25
  ## 安装
23
26
 
24
- 需要一个 DeepSeek 账户凭证。插件通过 harness 凭证接缝(`~/.dsh/.credentials.yaml` 或环境变量)解析 `DEEPSEEK_API_KEY`,与官方 DeepSeek 适配器一致。
27
+ 需要 DeepSeek Harness `0.1.5-rc.1` 或更高版本,以及一个 DeepSeek 账户凭证。插件通过可选的 harness 凭证服务(`~/.dsh/.credentials.yaml`)或环境变量解析 `DEEPSEEK_API_KEY`。
25
28
 
26
29
  ### 从 npm 安装(推荐)
27
30
 
@@ -29,20 +32,27 @@
29
32
  dsh plugin --profile <名字> add dsh-balance-plugin
30
33
  ```
31
34
 
32
- ### 从 GitHub 安装
35
+ ### 从 GitHub Release 安装(无需 npm 账号)
36
+
37
+ 从 [releases 页面](https://github.com/luokai-demo/dsh-plugins/releases) 下载 `dsh-balance-plugin-<版本>.tgz`,然后:
33
38
 
34
39
  ```sh
35
- dsh plugin --profile <名字> add github:<你>/dsh-balance-plugin#<commit-sha>
40
+ dsh plugin --profile <名字> add ./dsh-balance-plugin-<版本>.tgz
36
41
  ```
37
42
 
38
- Git 安装取到的是源码,pnpm 10 会拒绝安装时的构建,直到你允许:把 pnpm 打印的包键复制进 profile `pnpm-workspace.yaml`:
43
+ 注意:插件位于 `dsh-plugins` monorepo `plugins/` 子目录,`dsh plugin add github:...` 无法指向子目录——请使用 tarball(或 npm)。
39
44
 
40
- ```yaml
41
- allowBuilds:
42
- dsh-balance-plugin: true
45
+ ### 升级已有安装
46
+
47
+ 通过 npm 安装的版本,在同一个 profile 中升级包,确认 bundle 层仍处于启用状态,然后重启 Web 服务:
48
+
49
+ ```sh
50
+ dsh plugin --profile <名字> update dsh-balance-plugin
51
+ dsh --profile <名字> --dump-config # 应出现 "# == dsh-balance-plugin" 层
52
+ # 重启 dsh web,然后硬刷新浏览器
43
53
  ```
44
54
 
45
- 然后重跑 `add`。只安装你信任源码的包,并钉住 commit。
55
+ `dsh plugin` 会转发 pnpm 命令,并在成功升级后重新核对已安装包的 `dsh.bundle` 声明。GitHub Release tarball 则执行 `dsh plugin --profile <名字> add ./dsh-balance-plugin-<版本>.tgz` 安装新归档,再进行相同的验证与重启。
46
56
 
47
57
  ## 配置
48
58
 
@@ -63,7 +73,7 @@ allowBuilds:
63
73
 
64
74
  | 半面 | 做什么 |
65
75
  |---|---|
66
- | **宿主**(Node) | 注册 `GET /dsh-balance`(钱包读数 JSON)与 `GET /dsh-balance/events`(SSE 流)。监听 harness `session/event` 广播,每个 `turn/end` 推送 `refresh` 事件。**每次请求**经 `ctx.credentials` 解析凭证并回退环境变量——改 key 无需重启。 |
76
+ | **宿主**(Node) | 注册经认证的 `GET /dsh-balance`(钱包读数 JSON)与 `GET /dsh-balance/events`(SSE 流)。每个路由都会先通过 Harness 的浏览器身份与 Origin 栅栏,再读取凭证。监听 `session/event`,每个 `turn/end` 推送 `refresh`;**每次请求**经可选服务解析凭证,再回退环境变量。 |
67
77
  | **客户端**(浏览器) | 注册 `sidebar.footer.action` 插槽条目(shell 预留的设置旁座位;收起 rail 时堆叠在设置图标上方)。在挂载时、`refresh` SSE 事件与点击时抓取读数,带 in-flight 守卫。信用卡图标自包含(不依赖 shell 的图标库)。 |
68
78
 
69
79
  位置与交互遵循 shell 自身设计:`sidebar.footer.action` 是"设置旁可选操作"的官方扩展点。
@@ -90,13 +100,13 @@ src/
90
100
  index.ts # 宿主插件:路由 + SSE + 轮次结束监听
91
101
  client.tsx # 浏览器插件:侧边栏底部操作
92
102
  scripts/build.mjs # esbuild:宿主 ESM + 浏览器 __ModuleLoader__ bundle
93
- tests/ # balance-core node:test 单元测试
103
+ tests/ # balance-core、float 与宿主路由的 node:test 单元测试
94
104
  ```
95
105
 
96
106
  ```sh
97
107
  pnpm install
98
108
  pnpm run build # 生成 lib/index.js + lib/client.js
99
- pnpm test # balance-core 单元测试
109
+ pnpm test # balance-core、float 与宿主路由单元测试
100
110
  ```
101
111
 
102
112
  本地安装检查(官方检验点):
@@ -111,7 +121,7 @@ dsh --profile demo --dump-config # 应出现 "# == dsh-balance-plugin" 层
111
121
  发布新版本前:
112
122
 
113
123
  1. `pnpm run build` 与 `pnpm test` 通过。
114
- 2. 上述本地安装检查通过(`--dump-config` 显示层;web profile 用真实凭证渲染读数)。
124
+ 2. 上述本地安装与 `dsh plugin --profile demo update dsh-balance-plugin` 都通过;`--dump-config` 保留该层,且 Web profile 在真实凭证下能渲染读数。
115
125
  3. 版本已提升(`npm version patch/minor/major`);git tag 对应。
116
126
  4. `pnpm pack`——检查 tarball:必须包含 `lib/`、`cordis.patch.yml`、`README.md`(别无其他大文件)。
117
127
  5. `npm publish`——发布预构建代码,用户零摩擦安装。
package/lib/client.js CHANGED
@@ -28,10 +28,31 @@ __export(client_exports, {
28
28
  });
29
29
  module.exports = __toCommonJS(client_exports);
30
30
  var import_react = require("react");
31
+
32
+ // src/float.ts
33
+ var FLOAT_EPSILON = 5e-3;
34
+ function shouldFloat(prev, next, epsilon = FLOAT_EPSILON) {
35
+ if (prev === void 0 || !Number.isFinite(next)) return false;
36
+ return Math.abs(next - prev) >= epsilon;
37
+ }
38
+ function floatDelta(prev, next) {
39
+ return next - prev;
40
+ }
41
+ function formatFloat(delta, symbol) {
42
+ const sign = delta >= 0 ? "+" : "-";
43
+ return `${sign}${symbol}${Math.abs(delta).toFixed(2)}`;
44
+ }
45
+ function symbolOf(display) {
46
+ const match = /^\D+/.exec(display);
47
+ return match === null ? "" : match[0];
48
+ }
49
+
50
+ // src/client.tsx
31
51
  var name = "dsh-balance-plugin";
32
52
  var inject = ["slots"];
33
53
  var BALANCE_ENDPOINT = "/dsh-balance";
34
54
  var EVENTS_ENDPOINT = "/dsh-balance/events";
55
+ var FLOAT_LIFETIME_MS = 1300;
35
56
  function CardIcon({ size }) {
36
57
  return (0, import_react.createElement)(
37
58
  "svg",
@@ -51,6 +72,7 @@ function CardIcon({ size }) {
51
72
  }
52
73
  var styles = {
53
74
  button: {
75
+ position: "relative",
54
76
  flex: "none",
55
77
  display: "inline-flex",
56
78
  alignItems: "center",
@@ -74,10 +96,37 @@ var styles = {
74
96
  fontVariantNumeric: "tabular-nums",
75
97
  lineHeight: 1
76
98
  },
99
+ floatLayer: {
100
+ position: "absolute",
101
+ top: -16,
102
+ left: 0,
103
+ right: 0,
104
+ display: "flex",
105
+ justifyContent: "center",
106
+ pointerEvents: "none"
107
+ },
108
+ float: {
109
+ fontSize: 13,
110
+ fontWeight: 700,
111
+ fontVariantNumeric: "tabular-nums",
112
+ lineHeight: 1,
113
+ whiteSpace: "nowrap",
114
+ animation: "dsh-balance-float-up 1.3s ease-out forwards"
115
+ },
77
116
  success: { color: "var(--dsw-alias-state-success-primary, #16a34a)" },
78
117
  warning: { color: "var(--dsw-static-amber-600, #d97706)" },
79
118
  danger: { color: "var(--dsw-alias-state-error-primary, #dc2626)" }
80
119
  };
120
+ var FLOAT_CSS = `
121
+ @keyframes dsh-balance-float-up {
122
+ from { transform: translateY(0); opacity: 1; }
123
+ 70% { opacity: 1; }
124
+ to { transform: translateY(-26px); opacity: 0; }
125
+ }
126
+ @media (prefers-reduced-motion: reduce) {
127
+ .dsh-balance-float { display: none !important; }
128
+ }
129
+ `;
81
130
  function toneOf(readout) {
82
131
  if (readout.total >= readout.threshold) return "success";
83
132
  if (readout.total > 0) return "warning";
@@ -86,14 +135,33 @@ function toneOf(readout) {
86
135
  function BalanceAction({ wide }) {
87
136
  const [readout, setReadout] = (0, import_react.useState)(void 0);
88
137
  const [turnEpoch, setTurnEpoch] = (0, import_react.useState)(0);
138
+ const [floats, setFloats] = (0, import_react.useState)([]);
89
139
  const inFlight = (0, import_react.useRef)(false);
140
+ const floatSeq = (0, import_react.useRef)(0);
141
+ const readoutRef = (0, import_react.useRef)(void 0);
142
+ (0, import_react.useEffect)(() => {
143
+ readoutRef.current = readout;
144
+ }, [readout]);
145
+ const pushFloat = (0, import_react.useCallback)((delta, symbol) => {
146
+ const id = ++floatSeq.current;
147
+ setFloats((items) => [...items, { id, text: formatFloat(delta, symbol), tone: delta > 0 ? "success" : "danger" }]);
148
+ setTimeout(() => {
149
+ setFloats((items) => items.filter((item) => item.id !== id));
150
+ }, FLOAT_LIFETIME_MS);
151
+ }, []);
90
152
  const refresh = (0, import_react.useCallback)(() => {
91
153
  if (inFlight.current) return;
92
154
  inFlight.current = true;
93
- fetch(BALANCE_ENDPOINT, { cache: "no-store" }).then((response) => response.json()).then(setReadout).catch(() => setReadout(void 0)).finally(() => {
155
+ fetch(BALANCE_ENDPOINT, { cache: "no-store" }).then((response) => response.json()).then((next) => {
156
+ const prev = readoutRef.current;
157
+ if (prev?.status === "ok" && next.status === "ok" && shouldFloat(prev.total, next.total)) {
158
+ pushFloat(floatDelta(prev.total, next.total), symbolOf(next.display));
159
+ }
160
+ setReadout(next);
161
+ }).catch(() => setReadout(void 0)).finally(() => {
94
162
  inFlight.current = false;
95
163
  });
96
- }, []);
164
+ }, [pushFloat]);
97
165
  (0, import_react.useEffect)(() => {
98
166
  refresh();
99
167
  }, [refresh, turnEpoch]);
@@ -119,14 +187,32 @@ function BalanceAction({ wide }) {
119
187
  onClick: refresh
120
188
  },
121
189
  (0, import_react.createElement)(CardIcon, { size: wide ? 16 : 18 }),
122
- wide ? (0, import_react.createElement)("span", { style: { ...styles.amount, ...styles[tone] } }, readout.display) : null
190
+ wide ? (0, import_react.createElement)("span", { style: { ...styles.amount, ...styles[tone] } }, readout.display) : null,
191
+ floats.length > 0 ? (0, import_react.createElement)(
192
+ "span",
193
+ { style: styles.floatLayer, "aria-hidden": true },
194
+ floats.map((item) => (0, import_react.createElement)(
195
+ "span",
196
+ { key: item.id, className: "dsh-balance-float", style: { ...styles.float, ...styles[item.tone] } },
197
+ item.text
198
+ ))
199
+ ) : null
123
200
  );
124
201
  }
125
202
  function apply(ctx) {
126
- ctx.slots.inject("sidebar.footer.action", () => ctx.slots.register({
127
- name: "sidebar.footer.action",
128
- id: "balance",
129
- order: 0
130
- }, BalanceAction));
203
+ ctx.slots.inject("sidebar.footer.action", () => {
204
+ const styleEl = document.createElement("style");
205
+ styleEl.textContent = FLOAT_CSS;
206
+ document.head.appendChild(styleEl);
207
+ const disposer = ctx.slots.register({
208
+ name: "sidebar.footer.action",
209
+ id: "balance",
210
+ order: 0
211
+ }, BalanceAction);
212
+ return () => {
213
+ disposer();
214
+ styleEl.remove();
215
+ };
216
+ });
131
217
  }
132
218
  return module.exports; } });
package/lib/float.js ADDED
@@ -0,0 +1,24 @@
1
+ // src/float.ts
2
+ var FLOAT_EPSILON = 5e-3;
3
+ function shouldFloat(prev, next, epsilon = FLOAT_EPSILON) {
4
+ if (prev === void 0 || !Number.isFinite(next)) return false;
5
+ return Math.abs(next - prev) >= epsilon;
6
+ }
7
+ function floatDelta(prev, next) {
8
+ return next - prev;
9
+ }
10
+ function formatFloat(delta, symbol) {
11
+ const sign = delta >= 0 ? "+" : "-";
12
+ return `${sign}${symbol}${Math.abs(delta).toFixed(2)}`;
13
+ }
14
+ function symbolOf(display) {
15
+ const match = /^\D+/.exec(display);
16
+ return match === null ? "" : match[0];
17
+ }
18
+ export {
19
+ FLOAT_EPSILON,
20
+ floatDelta,
21
+ formatFloat,
22
+ shouldFloat,
23
+ symbolOf
24
+ };
package/lib/index.js CHANGED
@@ -98,13 +98,27 @@ async function fetchBalance(credentials, env, config) {
98
98
 
99
99
  // src/index.ts
100
100
  var name = "dsh-balance-plugin";
101
- var inject = ["webServer"];
101
+ var inject = ["webServer", "connection"];
102
102
  var Config = Schema.object({
103
103
  apiKeyEnv: Schema.string().default("DEEPSEEK_API_KEY"),
104
104
  baseURL: Schema.string().default("https://api.deepseek.com"),
105
105
  timeoutMs: Schema.natural().min(1).max(12e4).default(12e3),
106
106
  lowBalanceThreshold: Schema.number().min(0).default(2)
107
107
  });
108
+ function rejectUntrustedRequest(ctx, req, res) {
109
+ const connection = Reflect.get(ctx, "connection");
110
+ const status = connection.requestRejection(req);
111
+ if (status === void 0) return false;
112
+ res.statusCode = status;
113
+ res.end();
114
+ return true;
115
+ }
116
+ function rejectNonGetRequest(req, res) {
117
+ if (req.method === "GET") return false;
118
+ res.writeHead(405, { allow: "GET" });
119
+ res.end();
120
+ return true;
121
+ }
108
122
  function apply(ctx, config) {
109
123
  const sseClients = /* @__PURE__ */ new Set();
110
124
  const broadcastRefresh = () => {
@@ -119,7 +133,8 @@ function apply(ctx, config) {
119
133
  const unregisterBalance = ctx.webServer.register({
120
134
  kind: "exact",
121
135
  path: "/dsh-balance",
122
- handler: async (_req, res) => {
136
+ handler: async (req, res) => {
137
+ if (rejectUntrustedRequest(ctx, req, res) || rejectNonGetRequest(req, res)) return;
123
138
  const credentials = ctx.get("credentials");
124
139
  const resolver = credentials === void 0 ? void 0 : { resolve: (ref) => credentials.resolve(ref) };
125
140
  const readout = await fetchBalance(resolver, process.env, config);
@@ -136,6 +151,7 @@ function apply(ctx, config) {
136
151
  kind: "exact",
137
152
  path: "/dsh-balance/events",
138
153
  handler: (req, res) => {
154
+ if (rejectUntrustedRequest(ctx, req, res) || rejectNonGetRequest(req, res)) return;
139
155
  res.writeHead(200, {
140
156
  "content-type": "text/event-stream",
141
157
  "cache-control": "no-cache",
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "dsh-balance-plugin",
3
- "version": "0.1.0",
4
- "description": "DeepSeek wallet-balance readout at the sidebar foot: card icon + amount, status-tinted, refreshed on turn end and on click",
3
+ "version": "0.2.1",
4
+ "description": "DeepSeek wallet-balance readout at the sidebar foot: card icon + amount, status-tinted, refreshed on turn end and on click, with a floating delta animation on change",
5
5
  "type": "module",
6
6
  "main": "lib/index.js",
7
7
  "exports": {
@@ -29,21 +29,14 @@
29
29
  ]
30
30
  }
31
31
  },
32
- "scripts": {
33
- "build": "node scripts/build.mjs",
34
- "prepare": "node scripts/build.mjs",
35
- "test": "node --test tests/*.test.mjs"
36
- },
37
32
  "peerDependencies": {
38
- "@deepseek-ai/cordis": "^4.0.1",
39
- "@deepseek-ai/dsh-credentials": "0.0.1-rc.5",
40
- "@deepseek-ai/schemastery": "^3.18.1",
33
+ "@deepseek-ai/cordis": "^4.0.2",
34
+ "@deepseek-ai/schemastery": "^3.18.2",
41
35
  "react": "^18.0.0"
42
36
  },
43
37
  "devDependencies": {
44
- "@deepseek-ai/cordis": "^4.0.1",
45
- "@deepseek-ai/dsh-credentials": "0.0.1-rc.5",
46
- "@deepseek-ai/schemastery": "^3.18.1",
38
+ "@deepseek-ai/cordis": "^4.0.2",
39
+ "@deepseek-ai/schemastery": "^3.18.2",
47
40
  "@types/react": "^18.0.0",
48
41
  "esbuild": "^0.25.0",
49
42
  "react": "^18.0.0",
@@ -60,5 +53,9 @@
60
53
  "dsh-plugin",
61
54
  "balance",
62
55
  "wallet"
63
- ]
64
- }
56
+ ],
57
+ "scripts": {
58
+ "build": "node scripts/build.mjs",
59
+ "test": "node --test tests/*.test.mjs"
60
+ }
61
+ }