@kidli1412/dsh-token-heatmap 0.4.1 → 0.5.0
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/README.md +18 -10
- package/docs//345/215/241/347/211/207/350/256/276/347/275/256/351/235/242/346/235/277.jpg +0 -0
- package/docs//345/271/264/350/247/206/345/233/276.jpg +0 -0
- package/docs//346/234/210/350/247/206/345/233/276.jpg +0 -0
- package/docs//351/242/204/350/247/210-/346/226/260/347/211/210/344/274/232/350/257/235/351/241/265.jpg +0 -0
- package/lib/client.js +201 -83
- package/lib/index.js +168 -39
- package/package.json +24 -15
package/README.md
CHANGED
|
@@ -31,16 +31,16 @@ GitHub 风格自然年热力图:覆盖所选自然年 1月–12月(`‹ 2026
|
|
|
31
31
|
|
|
32
32
|
### 卡片设置 / In-card settings
|
|
33
33
|
|
|
34
|
-

|
|
35
35
|
|
|
36
|
-
- ⚙️ **设置就在卡片上**:点标题行最右端的 **⚙**(`刷新 [⚙]
|
|
37
|
-
- **配色方案**:六个色板按钮,**点击即时生效**(不需要"保存");**默认视图**:年 /
|
|
36
|
+
- ⚙️ **设置就在卡片上**:点标题行最右端的 **⚙**(`刷新 [⚙]`)弹出**悬浮设置面板** —— 位置在 ⚙ 正上方 8px、水平居中对齐、贴边留 12px,超出视口会自动钳制;点面板外的任意位置、按 **Esc**、或再点一次 ⚙ 都会收起。**插件不再往 DSH 设置(设置 → 插件 → 插件配置)里注册任何卡片**,所以那里看不到本插件。
|
|
37
|
+
- **配色方案**:六个色板按钮,**点击即时生效**(不需要"保存");**默认视图**:年 / 月,决定新会话页面首次打开时显示哪个视图(当次会话手动切换只影响当前页面)。面板底部是阈值图例(悬停看各档范围)与一行说明。
|
|
38
38
|
- 写入失败时面板底部会红字提示"保存失败,已回到服务端的值"(settings scope 复核后回滚乐观值)。
|
|
39
|
-
-
|
|
39
|
+
- 配置作为**本插件的 profile 配置**持久化:DSH 0.2.0 用插件导出的 `Config` schema 生成设置表单,表单以 **profile entry id**(`token-heatmap`)为键,值写进 profile patch(旧的 `<DSH_HOME>/settings.yaml` 段与 0.1.1 及更早的 `<DSH_HOME>/storages/token-heatmap-config.json` 都会在启动时**一次性迁移/抢救**回配置)。
|
|
40
40
|
|
|
41
41
|
## 安装 / Install
|
|
42
42
|
|
|
43
|
-
需要 `web` profile 与 `pnpm`。DSH 兼容版本见下方「兼容性 / Compatibility」;运行于 `@deepseek-ai/dsh
|
|
43
|
+
需要 `web` profile 与 `pnpm`。DSH 兼容版本见下方「兼容性 / Compatibility」;运行于 `@deepseek-ai/dsh ^0.2.0-rc.2`(0.2.0 版本线;0.1.2 线请用 0.4.2)。
|
|
44
44
|
|
|
45
45
|
从 npm 安装:
|
|
46
46
|
|
|
@@ -68,8 +68,8 @@ dsh plugin --profile web remove @kidli1412/dsh-token-heatmap
|
|
|
68
68
|
|
|
69
69
|
## 工作原理 / How it works
|
|
70
70
|
|
|
71
|
-
- **服务端**(`lib/index.js` + `lib/usage.js` + `lib/config.js`):作为 profile bundle 挂载,**实时折叠会话事件**(监听官方 `session/event`,每个 `assistant/chunk`/`assistant/message` 的 `usage` 事件即时写入缓存,不依赖 hero 屏挂载);启动时一次性补折叠已存在的 live 会话(如 resumed 会话);请求时 `collectUsage` 再做一次增量同步兜底,并枚举 **已归档(stored)会话**补齐历史——两种 `sessionPersistence` 接口都支持:0.1.2 线的 `listSnapshots()` + `readFrom()`,以及 0.1.3 起取代它们的 `list()` + `open()`/`handle.read()`。同 `(turn, step)` 的重复样本按"替换"语义处理,归属后一天;**fork(`isSeeded`)会话从它的继承切点开始折叠**——日志开头那批属于父会话的事件在父会话侧已折叠,跳过它们才不会把同一批 token 计两次;按天、按模型聚合,缓存到 `<DSH_HOME>/storages/token-heatmap-cache.json`。通过回环受限端点 `GET /api/token-heatmap/usage` 提供;显示配置(配色 +
|
|
72
|
-
- **客户端**(`lib/client.js`):手写 `__ModuleLoader__` bundle,注册进会话 `conversation.input.dock` 列表插槽,仅在 `session.blank`(新会话 hero 屏;旧宿主回退 `composerPhase === "blank"`)时渲染——卡片没有显示开关,hero 屏上始终显示。框架真正的"卡片下方"插槽 `conversation.composer.dock` 在 hero 屏被 `!hero` 门控禁用,因此本插件利用 `input.dock` 容器(flex 列)的 CSS `order` 把自己排到输入卡片**之后**。同一份数据由 `buildGrid()`(年,53 列 × 7 行)与 `buildMonthGrid()`(月,7 列 × 5–6 行,带日号)两个纯函数分别铺格,共用 `levelOf()` 的绝对阈值分档与 `palette` 配色;`‹ ›` 按钮按当前视图步进年或月,边界取"当前年/月"与"数据里最早的月",年/月分段按钮紧跟在步进器后面,**⚙ 设置面板**在卡片底部展开(内嵌面板,动作:⚙ 切换 / × / Esc / 焦点移出)。**不注册 `settings.plugin.item`**(官方"插件配置"页签只渲染"Host 实际 serve 的 namespace ∩ 客户端已注册 key"
|
|
71
|
+
- **服务端**(`lib/index.js` + `lib/usage.js` + `lib/config.js`):作为 profile bundle 挂载,**实时折叠会话事件**(监听官方 `session/event`,每个 `assistant/chunk`/`assistant/message` 的 `usage` 事件即时写入缓存,不依赖 hero 屏挂载);启动时一次性补折叠已存在的 live 会话(如 resumed 会话);请求时 `collectUsage` 再做一次增量同步兜底,并枚举 **已归档(stored)会话**补齐历史——两种 `sessionPersistence` 接口都支持:0.1.2 线的 `listSnapshots()` + `readFrom()`,以及 0.1.3 起取代它们的 `list()` + `open()`/`handle.read()`。同 `(turn, step)` 的重复样本按"替换"语义处理,归属后一天;**fork(`isSeeded`)会话从它的继承切点开始折叠**——日志开头那批属于父会话的事件在父会话侧已折叠,跳过它们才不会把同一批 token 计两次;按天、按模型聚合,缓存到 `<DSH_HOME>/storages/token-heatmap-cache.json`。通过回环受限端点 `GET /api/token-heatmap/usage` 提供;显示配置(配色 + 默认视图)由**导出的 `Config` schema** 持有(0.2.0 的设置模型:以 entry id `token-heatmap` 为键的表单,值写进 profile patch),`GET/POST /api/token-heatmap/config` 作为回环兼容 API 读写同一份配置(读经 `settings.describe()`,写经 `settings.update()`;0.1.x 的 `enabled` 开关已废弃,该字段只作为常量 `true` 回给旧客户端),0.1.1 及更早的 `token-heatmap-config.json` 文档在启动时一次性迁移。
|
|
72
|
+
- **客户端**(`lib/client.js`):手写 `__ModuleLoader__` bundle,注册进会话 `conversation.input.dock` 列表插槽,仅在 `session.blank`(新会话 hero 屏;旧宿主回退 `composerPhase === "blank"`)时渲染——卡片没有显示开关,hero 屏上始终显示。框架真正的"卡片下方"插槽 `conversation.composer.dock` 在 hero 屏被 `!hero` 门控禁用,因此本插件利用 `input.dock` 容器(flex 列)的 CSS `order` 把自己排到输入卡片**之后**。同一份数据由 `buildGrid()`(年,53 列 × 7 行)与 `buildMonthGrid()`(月,7 列 × 5–6 行,带日号)两个纯函数分别铺格,共用 `levelOf()` 的绝对阈值分档与 `palette` 配色;`‹ ›` 按钮按当前视图步进年或月,边界取"当前年/月"与"数据里最早的月",年/月分段按钮紧跟在步进器后面,**⚙ 设置面板**在卡片底部展开(内嵌面板,动作:⚙ 切换 / × / Esc / 焦点移出)。**不注册 `settings.plugin.item`**(官方"插件配置"页签只渲染"Host 实际 serve 的 namespace ∩ 客户端已注册 key"的卡片,本插件不再占这个位置),配置经 `ctx.configForms.get("token-heatmap")` 读写本插件配置(0.2.0 的设置模型,取代 0.1.x 的 settings scope)。
|
|
73
73
|
- 语义与 `dsh-token-meter` 的 `tokenUsage` 投影一致(参考插件 [dsh-usage-stats](https://github.com/Ychris12138/dsh-usage-stats),MIT)。
|
|
74
74
|
|
|
75
75
|
## 说明 / Notes
|
|
@@ -80,10 +80,17 @@ dsh plugin --profile web remove @kidli1412/dsh-token-heatmap
|
|
|
80
80
|
|
|
81
81
|
## 兼容性 / Compatibility
|
|
82
82
|
|
|
83
|
-
- **DSH**:manifest 通过 `dsh.compatibility.dshReleases`
|
|
83
|
+
- **DSH**:manifest 通过 `dsh.compatibility.dshReleases` 将当前版本线 `0.2.0-rc.2` 声明为 `compatible`(DSH STORE 的精确逐版本兼容证据;仅范围声明不会恢复上架)。插件使用的客户端注入(`dsh-api-remotes` / `dsh-client-connection` / `dsh-client-locale` / `dsh-client-ui-conversation` / `dsh-client-ui-settings`)、客户端服务(`slots` / `locale` / `configForms`)与 Host 服务(`webServer` 精确路由、`settings.describe` / `settings.update`、`sessions`、`sessionPersistence`)在 0.2.0 版本线上均已在真实安装中核对。
|
|
84
84
|
- **Node**:`^22.19.0 || >=24.0.0`(与 DSH 一致)。
|
|
85
|
-
- **宿主要求(dsh-market 显示)**:`engines.dsh: ^0.
|
|
86
|
-
- **依赖**:`@deepseek-ai/
|
|
85
|
+
- **宿主要求(dsh-market 显示)**:`engines.dsh: ^0.2.0-rc.2`,并将运行时依赖的 lockstep 宿主包声明为 `peerDependencies`(`dsh-host-webserver` / `dsh-session` / `dsh-session-persistence` / `dsh-settings` 与客户端模块 `dsh-api-remotes` / `dsh-client-connection` / `dsh-client-locale` / `dsh-client-ui-conversation` / `dsh-client-ui-settings`,均为 `^0.2.0-rc.2`);插件市场会据此显示"宿主要求"并判断与当前 DSH 是否匹配。这些包由 DSH 运行时提供、本插件**从不 import**,所以同时标为 `peerDependenciesMeta.optional`——npm/pnpm 不再把它们装进依赖树(0.2.0 起它们彼此还有 peer 关系,沿用旧的自动安装会直接解析冲突),而"宿主要求"的语义不受影响。
|
|
86
|
+
- **依赖**:`@deepseek-ai/schemastery ^3.18.2`(唯一的真实依赖;`dsh-settings` 自 0.2.0 起不再需要作为 peer 之外的运行时依赖)。
|
|
87
|
+
- **0.5.0(DSH 0.2.0 适配)**:DSH 0.2.0 换了三套契约,宿主兼容闸门会**直接拒绝挂载**声明不符的 bundle(现象:插件在市场里显示"不兼容"、`rows: []`、hero 屏上什么都没有)——
|
|
88
|
+
1. **宿主要求必须落在 0.2.0 线上**:闸门逐条比对 `peerDependencies` 里的 `@deepseek-ai/dsh*` 与运行时版本(`semver.satisfies(..., { includePrerelease: true })`),`^0.1.2-rc.1` 不满足 `0.2.0-rc.2`,因此本版把 `engines.dsh` 与 9 个宿主 peer 一起提到 `^0.2.0-rc.2`(并把宿主包标为 optional peer,见上)。
|
|
89
|
+
2. **设置模型重做**:0.1.x 的"插件自建 settings namespace + `settings.register(ns, schema)`"没了,`settings.get(ns)` 也没了。现在**插件导出的 `Config`(schemastery)就是设置表单**,以 **profile entry id**(bundle patch 里的 `id: token-heatmap`)为键;Host 侧读别人用 `settings.describe()`(`serveConfig()` 已改,否则配置端点会静默回默认值)、写自己用 `settings.update(ns, patch)`,浏览器端用 `ctx.configForms.get("token-heatmap")`(`getSnapshot` / `subscribe` / `set`),旧的 `ctx.settingsScope.bind({namespace})` 已从客户端消失。卡片 ⚙ 面板的 UI 与"点击即时生效"完全不变。
|
|
90
|
+
3. **弹层皮肤同步**:0.2.0 的原生 stat dialog 改用 `border-radius:var(--dsw-radius-lg)` 并加 `backdrop-filter:var(--dsw-menu-backdrop-filter)`,本插件的 ⚙ 悬浮面板跟着改(带 fallback,旧宿主仍渲染 12px 圆角)。
|
|
91
|
+
> 升级时会**自动抢救旧配置**:DSH 自己的 `settings.yaml` 导入按 entry id 进行,而当时本插件正因上面的 peer 不符被拒挂载,导入失败后值只留在 `~/.dsh/settings.yaml.imported` 里。本版 Host 在启动后(等 Loader settle)读一次该文档,若条目还没有用户值就把非默认的 `colorScheme` / `defaultView` 写进配置——一次性、只读、失败即跳过。(`enabled` 字段自 0.3.0 起已废弃,不参与抢救。)
|
|
92
|
+
> 另外提醒:0.2.0 给 `dsh web` 的每个请求加了**签名 cookie 鉴权**,插件的两个端点因此只有带 cookie 的同源页面能到达;用 node/curl 直接探端点拿到 401 是 DSH 的鉴权层,**不代表插件没挂载**。Host 侧的 loopback 精确路由 fence 保留为第二道防线。
|
|
93
|
+
- **0.4.0(设置搬进卡片)**:不再注册官方 `settings.plugin.item` 插槽——设置页(设置 → 插件 → 插件配置)里不再有本插件的卡片,配色与默认视图改在卡片自己的 ⚙ 面板里改,**点击即时生效**(去掉了草稿/保存/放弃那套)。当时 Host 侧的 `settings.register("token-heatmap", schema)` 保留下来当 settings.yaml 的校验与持久化管道;**该注册在 0.5.0 已随 0.2.0 的设置模型一起移除**(导出的 `Config` 就是管道)。升级只影响设置入口位置,已有配置不动。
|
|
87
94
|
- **0.1.4(DSH 0.1.2 适配)**:rc.1 起 live session 不再携带 `.events` 数组(改用 `session.seq` + `session.eventAt(seq)`,与官方 `dsh-token-meter` 相同),新会话判断从 `composerPhase === "blank"` 改为布尔 `session.blank`;`sessionPersistence` 的 stored 会话枚举在 0.1.3-alpha.2 被替换(`listSnapshots`/`readFrom` → `list()` + `open()`/`handle.read()`),两条接口见 0.1.6 条目。客户端注入模块列表同步为新架构模块(见上)。
|
|
88
95
|
- **0.1.6(session/event 实时折叠 + stored 会话枚举修复)**:`apply()` 注册官方 `session/event` 监听器,每个 usage 事件即时折叠进缓存,解决 live 会话仅在 hero 屏挂载时才折叠而漏计同一日其他会话用量的问题(表现为当日总量偏小、历史天数丢失);启动时一次性补折叠已存在的 live 会话(如 resumed 会话)。**stored 会话枚举修复**:0.1.3-alpha.2 起 `sessionPersistence` 移除了 `readFrom()` 与 `listSnapshots()`,只保留 `list()` + `open()`/`handle.read()`;旧实现只探测 `list`/`listSnapshots` 却无条件调用 `readFrom`,导致每个 stored 会话抛错并被吞成一条 warn —— 表现为热力图只剩进程内 live 的几天。现在两条接口都支持(`list()` 的 `revision` 同样用于跳过未变更的日志,增量仍按 `seq` 去重与连续性校验),stored 会话可完整补齐历史;两者都不可用时不再误判为"日志被截断",而是保留已折叠天数并告警。token 口径与 `dsh-token-meter` 一致(input + output + cacheRead + cacheWrite,不含 reasoningTokens)。
|
|
89
96
|
- **0.2.0(月视图 + 默认视图设置)**:新增 `buildMonthGrid()` 月视图(周一起、5–6 行、日号 + 当日 token 数)与 年/月 分段切换,`‹ ›` 按当前视图步进年或月;settings namespace 新增 `defaultView`(`"year" | "month"`)字段——与 `colorScheme` 的"只约束 shape"不同,**`defaultView` 是枚举校验**(未知视图没有可回退的渲染器),旧 Host 上该字段会被 schema 丢弃、旧客户端读到未知值时回退为"年"。0.1.x 的 `settings.yaml` 无需迁移(缺字段即取默认 `year`)。
|
|
@@ -91,6 +98,7 @@ dsh plugin --profile web remove @kidli1412/dsh-token-heatmap
|
|
|
91
98
|
- **0.4.0(设置搬进卡片)**:不再注册官方 `settings.plugin.item` 插槽——设置页(设置 → 插件 → 插件配置)里不再有本插件的卡片,配色与默认视图改在卡片自己的 ⚙ 面板里改,**点击即时生效**(去掉了草稿/保存/放弃那套)。Host 侧 `settings.register("token-heatmap", schema)` 保留:它是 settings.yaml 的校验与持久化管道,与 UI 卡片无关(官方 `settings` 服务的 `get`/`update` 只对已注册 namespace 生效)。升级只影响设置入口位置,已有配置不动。
|
|
92
99
|
|
|
93
100
|
- **0.4.1(fork 会话不再重复计入父会话用量)**:DSH 的 fork 子会话(header `isSeeded=true`)日志以父会话事件的完整复制开头,其前 `inheritedEventCount` 个事件是**父会话**的 usage(父会话折叠时已计入)。此前折叠从 seq 0 读整份日志,同一批 token 被计两次——实测 2026-09-14 由 8.34 亿虚增到 13.15 亿。现在三条折叠路径(`collectUsage` 的 live 折叠、`session/event` 实时监听、stored 日志读取)都从 fork 切点开始:live 会话用官方 `session.inheritedEventCount`;stored 日志用最后一个带 `data.inherited === true` 的 `session/end-seed` 的 `seq + 1`(未打标记的 `session/end-seed` 是 compaction 边界,不算切点,与 `dsh-session-format-v2-to-v3` 的切点推导一致)。**resume 不是 fork**:`isSeeded=false` 的会话种子是它自己的历史,仍整份折叠。缓存格式版本提升到 2,旧缓存(可能含重复计入的天数)会被丢弃重建;父会话日志不可得的极端情况下会少计而非多计。
|
|
101
|
+
- **0.4.2(设置改成悬浮面板)**:⚙ 面板从"卡片底部的内嵌条"改为**悬浮面板**——portal 到 `document.body`、`position:fixed`,锚在 ⚙ 上方 8px 且水平居中对齐,按视口钳制(12px 边距),关闭方式为点外部/Esc/再点 ⚙;表面沿用 DSH 原生弹层 token(`--dsw-specific-menu` + `--dsw-elevation-prominent`,配合 `--dsw-elevation-stroke-color` 的发丝边),与底部统计 pill 的弹层一致。为此客户端 bundle 新增 `require("react-dom")`(原生的 `createPortal`),DSH 的模块图里 react-dom 一直存在,旧宿主不受影响。
|
|
94
102
|
|
|
95
103
|
## License
|
|
96
104
|
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
package/lib/client.js
CHANGED
|
@@ -7,9 +7,10 @@
|
|
|
7
7
|
* columns = weeks, rows = Mon..Sun, all seven weekday labels on the left) or a
|
|
8
8
|
* single-month calendar, switchable from the header, plus today / this-month /
|
|
9
9
|
* all-time totals and a ‹period› stepper. The card configures ITSELF through a
|
|
10
|
-
* ⚙ panel (palette + default view) persisted in
|
|
11
|
-
*
|
|
12
|
-
*
|
|
10
|
+
* ⚙ panel (palette + default view) persisted in this plugin's profile CONFIG —
|
|
11
|
+
* DSH 0.2.0 derives the form from the Host's exported `Config` and the card
|
|
12
|
+
* reads/writes it through `ctx.configForms.get("token-heatmap")`; there is no
|
|
13
|
+
* `settings.plugin.item` card and no settings scope any more, so all of its
|
|
13
14
|
* settings live on the card.
|
|
14
15
|
*
|
|
15
16
|
* The card is shown ONLY on the new-session (hero) screen — gated on the
|
|
@@ -37,6 +38,9 @@ window.__ModuleLoader__.load({
|
|
|
37
38
|
|
|
38
39
|
let react = require("react");
|
|
39
40
|
let react_jsx_runtime = require("react/jsx-runtime");
|
|
41
|
+
// Only for the settings panel, which is PORTALED to document.body so the
|
|
42
|
+
// card's own box can never clip it (the pattern the stat pills use).
|
|
43
|
+
let reactDom = require("react-dom");
|
|
40
44
|
|
|
41
45
|
//#region css
|
|
42
46
|
const css = [
|
|
@@ -88,23 +92,25 @@ window.__ModuleLoader__.load({
|
|
|
88
92
|
".thm_error{color:var(--dsw-alias-state-error-primary);background:var(--dsw-alias-interactive-bg-hover-danger);border-radius:8px;padding:7px 8px;font-size:11px;line-height:16px;display:flex;justify-content:space-between;align-items:center;gap:8px}",
|
|
89
93
|
".thm_retry{color:inherit;font:inherit;cursor:pointer;background:0 0;border:none;flex:none;padding:0}",
|
|
90
94
|
".thm_loading{color:var(--dsw-alias-label-tertiary);font-size:11px;line-height:16px;padding:6px 2px}",
|
|
91
|
-
//
|
|
92
|
-
// configures itself; nothing registers into 设置 → 插件 → 插件配置.
|
|
95
|
+
// The ⚙ at the row's right end opens a floating pill panel — the
|
|
96
|
+
// card configures itself; nothing registers into 设置 → 插件 → 插件配置.
|
|
93
97
|
".thm_gear{cursor:pointer;color:var(--dsw-alias-label-tertiary);background:0 0;border:none;border-radius:6px;align-items:center;padding:2px 4px;display:inline-flex}",
|
|
94
98
|
".thm_gear:hover{color:var(--dsw-alias-label-secondary);background:var(--dsw-alias-interactive-bg-hover)}",
|
|
95
99
|
".thm_gear[data-active=true]{color:var(--dsw-alias-label-primary);background:var(--dsw-alias-interactive-bg-hover)}",
|
|
96
|
-
// The panel
|
|
97
|
-
//
|
|
98
|
-
//
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
".
|
|
103
|
-
".
|
|
100
|
+
// The settings panel mirrors DSH's own pill dialogs: portaled to
|
|
101
|
+
// document.body, position:fixed (so the card can never clip it),
|
|
102
|
+
// measure-then-place above the ⚙ and clamped to the viewport, with
|
|
103
|
+
// the shipped dialog surface + elevation tokens. DSH 0.2.0's stat
|
|
104
|
+
// dialog moved to `--dsw-radius-lg` and a backdrop filter, so the
|
|
105
|
+
// surface follows (the fallbacks keep it rendering on an older host).
|
|
106
|
+
".thm_panel{position:fixed;z-index:1100;box-sizing:border-box;background:var(--dsw-specific-menu,#fff);--dsw-elevation-stroke-color:var(--dsw-alias-border-l1);width:400px;max-width:calc(100vw - 24px);backdrop-filter:var(--dsw-menu-backdrop-filter);box-shadow:var(--dsw-elevation-prominent,0 8px 32px rgba(0,0,0,.16));color:var(--dsw-alias-label-secondary);cursor:default;border:0;border-radius:var(--dsw-radius-lg,12px);padding:16px;font-size:12px;line-height:18px;text-align:left;font-weight:400}",
|
|
107
|
+
".thm_panelHead{color:var(--dsw-alias-label-primary);justify-content:space-between;align-items:center;gap:16px;margin-bottom:8px;font-weight:500;display:flex}",
|
|
108
|
+
".thm_panelTitleLabel{align-items:center;gap:6px;display:inline-flex}",
|
|
109
|
+
".thm_panelTitleLabel svg{flex:none;width:14px;height:14px}",
|
|
110
|
+
".thm_panelRule{border-top:.5px solid var(--dsw-alias-border-l2);margin-bottom:12px}",
|
|
104
111
|
".thm_group{flex-direction:column;gap:5px;display:flex}",
|
|
112
|
+
".thm_group+.thm_group{margin-top:14px}",
|
|
105
113
|
".thm_groupLabel{color:var(--dsw-alias-label-tertiary);font-size:11px;line-height:16px}",
|
|
106
|
-
".thm_hint{color:var(--dsw-alias-label-caption);font-size:10px;line-height:14px}",
|
|
107
|
-
".thm_hintError{color:var(--dsw-alias-state-error-primary)}",
|
|
108
114
|
".thm_swatches{flex-wrap:wrap;gap:6px;display:flex}",
|
|
109
115
|
".thm_swatch{cursor:pointer;align-items:center;gap:6px;border:1px solid var(--dsw-alias-border-l2);background:0 0;border-radius:6px;padding:3px 8px;font:inherit;display:flex}",
|
|
110
116
|
".thm_swatch:hover{border-color:var(--dsw-alias-label-dimmed)}",
|
|
@@ -115,7 +121,11 @@ window.__ModuleLoader__.load({
|
|
|
115
121
|
".thm_seg{border:1px solid var(--dsw-alias-border-l2);border-radius:8px;align-items:center;display:inline-flex;overflow:hidden;width:max-content}",
|
|
116
122
|
".thm_segBtn{cursor:pointer;color:var(--dsw-alias-label-tertiary);background:0 0;border:none;padding:1px 10px;font:inherit;font-size:11px;line-height:18px}",
|
|
117
123
|
".thm_segBtn:hover{color:var(--dsw-alias-label-secondary);background:var(--dsw-alias-interactive-bg-hover)}",
|
|
118
|
-
".thm_segBtn[data-active=true]{color:var(--dsw-alias-label-primary);background:var(--dsw-alias-interactive-bg-hover);font-weight:600}"
|
|
124
|
+
".thm_segBtn[data-active=true]{color:var(--dsw-alias-label-primary);background:var(--dsw-alias-interactive-bg-hover);font-weight:600}",
|
|
125
|
+
".thm_panelFoot{border-top:.5px solid var(--dsw-alias-border-l2);margin-top:14px;padding-top:10px}",
|
|
126
|
+
".thm_footLegend{align-items:center;gap:3px;font-size:10px;line-height:14px;display:flex;color:var(--dsw-alias-label-tertiary)}",
|
|
127
|
+
".thm_footRule{color:var(--dsw-alias-label-caption);margin-top:6px;font-size:10px;line-height:14px}",
|
|
128
|
+
".thm_footError{color:var(--dsw-alias-state-error-primary);margin-top:6px;font-size:10px;line-height:14px}"
|
|
119
129
|
].join("");
|
|
120
130
|
const tagId = "@kidli1412/dsh-token-heatmap/TokenHeatmap.module.css";
|
|
121
131
|
if (typeof document !== "undefined" && document.querySelector("style[data-plugin-css=" + JSON.stringify(tagId) + "]") === null) {
|
|
@@ -162,12 +172,14 @@ window.__ModuleLoader__.load({
|
|
|
162
172
|
gear: "thm_gear",
|
|
163
173
|
settingsPanel: "thm_panel",
|
|
164
174
|
panelHead: "thm_panelHead",
|
|
165
|
-
|
|
166
|
-
|
|
175
|
+
panelTitleLabel: "thm_panelTitleLabel",
|
|
176
|
+
panelRule: "thm_panelRule",
|
|
177
|
+
panelFoot: "thm_panelFoot",
|
|
178
|
+
footLegend: "thm_footLegend",
|
|
179
|
+
footRule: "thm_footRule",
|
|
180
|
+
footError: "thm_footError",
|
|
167
181
|
settingsGroup: "thm_group",
|
|
168
182
|
settingsGroupLabel: "thm_groupLabel",
|
|
169
|
-
settingsHint: "thm_hint",
|
|
170
|
-
settingsHintError: "thm_hintError",
|
|
171
183
|
settingsSwatches: "thm_swatches",
|
|
172
184
|
settingsSwatch: "thm_swatch",
|
|
173
185
|
settingsSwatchCells: "thm_swatchCells",
|
|
@@ -217,7 +229,7 @@ window.__ModuleLoader__.load({
|
|
|
217
229
|
settingsRed: "红色",
|
|
218
230
|
settingsPurple: "紫色",
|
|
219
231
|
settingsTeal: "青色",
|
|
220
|
-
|
|
232
|
+
settingsThresholdNote: "颜色按每天 token 数的绝对阈值分档(悬停图例看各档范围)。",
|
|
221
233
|
settingsSaveFailed: "保存失败,已回到服务端的值,请重试。",
|
|
222
234
|
settingsExpand: "展开设置",
|
|
223
235
|
settingsCollapse: "收起设置"
|
|
@@ -258,7 +270,7 @@ window.__ModuleLoader__.load({
|
|
|
258
270
|
settingsRed: "Red",
|
|
259
271
|
settingsPurple: "Purple",
|
|
260
272
|
settingsTeal: "Teal",
|
|
261
|
-
|
|
273
|
+
settingsThresholdNote: "Colors follow absolute per-day token thresholds (hover the legend for each range).",
|
|
262
274
|
settingsSaveFailed: "Save failed; the server value was restored.",
|
|
263
275
|
settingsExpand: "Show settings",
|
|
264
276
|
settingsCollapse: "Hide settings"
|
|
@@ -397,13 +409,14 @@ window.__ModuleLoader__.load({
|
|
|
397
409
|
return VIEW_MODES.includes(view) ? view : CONFIG_DEFAULTS.defaultView;
|
|
398
410
|
}
|
|
399
411
|
/**
|
|
400
|
-
* Reactive adapter over one
|
|
401
|
-
* useSyncExternalStore-compatible store whose snapshot is
|
|
402
|
-
* `{ status, colorScheme, defaultView }` derived from the
|
|
403
|
-
* `set(patch)` writes the touched fields through
|
|
404
|
-
*
|
|
405
|
-
* only changes after the Host confirms. `
|
|
406
|
-
*
|
|
412
|
+
* Reactive adapter over one config form (`ctx.configForms.get(entryId)`):
|
|
413
|
+
* a useSyncExternalStore-compatible store whose snapshot is
|
|
414
|
+
* `{ status, colorScheme, defaultView }` derived from the form.
|
|
415
|
+
* `set(patch)` writes the touched fields through `form.set(field, value)`
|
|
416
|
+
* (async; the form fences revisions and reloads on failure), and the
|
|
417
|
+
* snapshot only changes after the Host confirms. `dispose()` removes the
|
|
418
|
+
* subscription. (The pre-0.2.0 `reload()` wrapper is gone: the shared
|
|
419
|
+
* form re-reads on its own, and its API has no `load()`.)
|
|
407
420
|
*/
|
|
408
421
|
function createConfigStore(scope) {
|
|
409
422
|
const listeners = [];
|
|
@@ -445,7 +458,6 @@ window.__ModuleLoader__.load({
|
|
|
445
458
|
await scope.set("defaultView", sanitizeView(patch.defaultView));
|
|
446
459
|
}
|
|
447
460
|
},
|
|
448
|
-
reload: () => scope.load(),
|
|
449
461
|
dispose: () => unsubscribe()
|
|
450
462
|
};
|
|
451
463
|
}
|
|
@@ -606,16 +618,30 @@ window.__ModuleLoader__.load({
|
|
|
606
618
|
// Month-view cursor (`YYYY-MM`); initialised from the current month
|
|
607
619
|
// and re-anchored when it would fall outside the navigable bounds.
|
|
608
620
|
const [monthCursor, setMonthCursor] = react.useState(() => monthKey(Date.now()));
|
|
609
|
-
//
|
|
610
|
-
// configures itself here instead of living in
|
|
621
|
+
// The ⚙ at the row's right end opens the floating settings panel;
|
|
622
|
+
// the card configures itself here instead of living in
|
|
623
|
+
// 设置 → 插件 → 插件配置. The panel is PORTALED to document.body and
|
|
624
|
+
// anchored to this button, so the open state lives here with it.
|
|
625
|
+
const gearRef = react.useRef(null);
|
|
611
626
|
const [settingsOpen, setSettingsOpen] = react.useState(false);
|
|
612
627
|
react.useEffect(() => {
|
|
613
628
|
if (!settingsOpen) return;
|
|
614
629
|
const onKeyDown = (event) => {
|
|
615
630
|
if (event.key === "Escape") setSettingsOpen(false);
|
|
616
631
|
};
|
|
632
|
+
const onPointerDown = (event) => {
|
|
633
|
+
const gear = gearRef.current;
|
|
634
|
+
if (gear !== null && gear.contains(event.target)) return;
|
|
635
|
+
const panel = document.querySelector(`.${S.settingsPanel}`);
|
|
636
|
+
if (panel !== null && panel.contains(event.target)) return;
|
|
637
|
+
setSettingsOpen(false);
|
|
638
|
+
};
|
|
617
639
|
document.addEventListener("keydown", onKeyDown);
|
|
618
|
-
|
|
640
|
+
document.addEventListener("pointerdown", onPointerDown, true);
|
|
641
|
+
return () => {
|
|
642
|
+
document.removeEventListener("keydown", onKeyDown);
|
|
643
|
+
document.removeEventListener("pointerdown", onPointerDown, true);
|
|
644
|
+
};
|
|
619
645
|
}, [settingsOpen]);
|
|
620
646
|
const loaderRef = react.useRef(null);
|
|
621
647
|
if (loaderRef.current === null) loaderRef.current = createLoader();
|
|
@@ -805,16 +831,19 @@ window.__ModuleLoader__.load({
|
|
|
805
831
|
onClick: load,
|
|
806
832
|
children: dict("refresh")
|
|
807
833
|
}),
|
|
808
|
-
// The
|
|
809
|
-
//
|
|
810
|
-
//
|
|
834
|
+
// The ⚙ owns the row's right end; the ‹ › stepper
|
|
835
|
+
// and the view switch sit left of it, the stepper
|
|
836
|
+
// next to the stats it moves through. This button is
|
|
837
|
+
// also the settings panel's anchor.
|
|
811
838
|
react_jsx_runtime.jsx("button", {
|
|
812
839
|
type: "button",
|
|
840
|
+
ref: gearRef,
|
|
813
841
|
className: S.gear,
|
|
814
842
|
"data-active": settingsOpen,
|
|
843
|
+
"aria-haspopup": "dialog",
|
|
815
844
|
"aria-expanded": settingsOpen,
|
|
816
|
-
"aria-label": dict("
|
|
817
|
-
title: dict("
|
|
845
|
+
"aria-label": dict(settingsOpen ? "settingsCollapse" : "settingsExpand"),
|
|
846
|
+
title: dict(settingsOpen ? "settingsCollapse" : "settingsExpand"),
|
|
818
847
|
onClick: () => setSettingsOpen(!settingsOpen),
|
|
819
848
|
children: react_jsx_runtime.jsx("svg", {
|
|
820
849
|
width: 14,
|
|
@@ -933,6 +962,8 @@ window.__ModuleLoader__.load({
|
|
|
933
962
|
store: configStore,
|
|
934
963
|
palette,
|
|
935
964
|
translate,
|
|
965
|
+
legend: levelLegend,
|
|
966
|
+
anchor: gearRef.current,
|
|
936
967
|
onClose: () => setSettingsOpen(false)
|
|
937
968
|
}) : null
|
|
938
969
|
]
|
|
@@ -948,10 +979,45 @@ window.__ModuleLoader__.load({
|
|
|
948
979
|
//#endregion
|
|
949
980
|
|
|
950
981
|
//#region TokenHeatmapInlineSettings
|
|
982
|
+
/** Distance between the ⚙ and the panel's bottom edge (native dialog spec). */
|
|
983
|
+
const PANEL_GAP = 8;
|
|
984
|
+
/** Viewport margin the placement clamp keeps (native dialog spec). */
|
|
985
|
+
const PANEL_MARGIN = 12;
|
|
986
|
+
|
|
987
|
+
/**
|
|
988
|
+
* Place the floating panel above its ⚙ trigger, clamped to the viewport —
|
|
989
|
+
* the same measure-then-place flow (and the same 8px gap / 12px margin)
|
|
990
|
+
* the built-in stat dialogs use. The panel is `position:fixed` and lives
|
|
991
|
+
* in `document.body`, so the card can never clip it.
|
|
992
|
+
* @param trigger - the ⚙ button the panel is anchored to.
|
|
993
|
+
* @param panel - the `position:fixed` panel element.
|
|
994
|
+
*/
|
|
995
|
+
function placePanel(trigger, panel) {
|
|
996
|
+
const anchor = trigger.getBoundingClientRect();
|
|
997
|
+
const width = panel.offsetWidth;
|
|
998
|
+
const height = panel.offsetHeight;
|
|
999
|
+
if (width === 0 || height === 0) return;
|
|
1000
|
+
const viewportWidth = window.innerWidth;
|
|
1001
|
+
const viewportHeight = window.innerHeight;
|
|
1002
|
+
const left = Math.min(
|
|
1003
|
+
Math.max(PANEL_MARGIN, anchor.left + anchor.width / 2 - width / 2),
|
|
1004
|
+
Math.max(PANEL_MARGIN, viewportWidth - width - PANEL_MARGIN)
|
|
1005
|
+
);
|
|
1006
|
+
let top = anchor.top - height - PANEL_GAP;
|
|
1007
|
+
if (top < PANEL_MARGIN) top = Math.min(anchor.bottom + PANEL_GAP, viewportHeight - height - PANEL_MARGIN);
|
|
1008
|
+
panel.style.left = `${Math.round(left)}px`;
|
|
1009
|
+
panel.style.top = `${Math.round(Math.max(PANEL_MARGIN, top))}px`;
|
|
1010
|
+
panel.style.visibility = "visible";
|
|
1011
|
+
}
|
|
1012
|
+
|
|
951
1013
|
/**
|
|
952
|
-
* The card's
|
|
953
|
-
*
|
|
954
|
-
*
|
|
1014
|
+
* The card's settings panel: 配色方案 and 默认视图, edited from the card
|
|
1015
|
+
* itself instead of inside 设置 → 插件 → 插件配置 (this plugin no longer
|
|
1016
|
+
* registers into the `settings.plugin.item` seat).
|
|
1017
|
+
*
|
|
1018
|
+
* It is PORTALED to `document.body` (the card's own box can never clip
|
|
1019
|
+
* it) and anchored to the ⚙ through `anchor`; the owner closes it on
|
|
1020
|
+
* outside pointerdown and Escape, so this component only draws.
|
|
955
1021
|
*
|
|
956
1022
|
* Every control applies immediately through the bound settings scope —
|
|
957
1023
|
* there is no draft/save step, so the component owns only a local save
|
|
@@ -964,9 +1030,12 @@ window.__ModuleLoader__.load({
|
|
|
964
1030
|
* @param props.store - config store (set writes through the scope).
|
|
965
1031
|
* @param props.palette - active palette (level 0..4 → css color).
|
|
966
1032
|
* @param props.translate - interpolation helper for translated labels.
|
|
967
|
-
* @param props.
|
|
1033
|
+
* @param props.legend - level labels for the palette footnote.
|
|
1034
|
+
* @param props.anchor - the ⚙ button the panel is placed against.
|
|
1035
|
+
* @param props.onClose - collapse the panel.
|
|
968
1036
|
*/
|
|
969
|
-
function TokenHeatmapInlineSettings({ dict, config, store, palette, translate, onClose }) {
|
|
1037
|
+
function TokenHeatmapInlineSettings({ dict, config, store, palette, translate, legend, anchor, onClose }) {
|
|
1038
|
+
const panelRef = react.useRef(null);
|
|
970
1039
|
const [saveFailed, setSaveFailed] = react.useState(false);
|
|
971
1040
|
const schemes = Object.keys(COLOR_SCHEMES);
|
|
972
1041
|
// Optimistic local echo: the settings scope only reports the new value
|
|
@@ -974,6 +1043,26 @@ window.__ModuleLoader__.load({
|
|
|
974
1043
|
const [pending, setPending] = react.useState({});
|
|
975
1044
|
const scheme = pending.colorScheme ?? config.colorScheme;
|
|
976
1045
|
const defaultView = pending.defaultView ?? config.defaultView;
|
|
1046
|
+
// Place before paint (and again when the palette reflows the panel):
|
|
1047
|
+
// the trigger's box is only meaningful once the card is laid out.
|
|
1048
|
+
react.useLayoutEffect(() => {
|
|
1049
|
+
const panel = panelRef.current;
|
|
1050
|
+
if (panel === null || anchor === null || anchor === void 0) return;
|
|
1051
|
+
placePanel(anchor, panel);
|
|
1052
|
+
});
|
|
1053
|
+
react.useEffect(() => {
|
|
1054
|
+
const replace = () => {
|
|
1055
|
+
const panel = panelRef.current;
|
|
1056
|
+
if (panel === null || anchor === null || anchor === void 0) return;
|
|
1057
|
+
placePanel(anchor, panel);
|
|
1058
|
+
};
|
|
1059
|
+
window.addEventListener("resize", replace);
|
|
1060
|
+
window.addEventListener("scroll", replace, true);
|
|
1061
|
+
return () => {
|
|
1062
|
+
window.removeEventListener("resize", replace);
|
|
1063
|
+
window.removeEventListener("scroll", replace, true);
|
|
1064
|
+
};
|
|
1065
|
+
}, [anchor]);
|
|
977
1066
|
const commit = async (patch) => {
|
|
978
1067
|
setPending((current) => ({ ...current, ...patch }));
|
|
979
1068
|
setSaveFailed(false);
|
|
@@ -990,28 +1079,43 @@ window.__ModuleLoader__.load({
|
|
|
990
1079
|
if (value !== defaultView) commit({ defaultView: value });
|
|
991
1080
|
};
|
|
992
1081
|
const labelOf = (name) => dict(`settings${name[0].toUpperCase()}${name.slice(1)}`);
|
|
993
|
-
|
|
1082
|
+
const title = dict("settingsTitle");
|
|
1083
|
+
const panel = react_jsx_runtime.jsxs("div", {
|
|
1084
|
+
ref: panelRef,
|
|
994
1085
|
className: S.settingsPanel,
|
|
995
|
-
|
|
996
|
-
|
|
997
|
-
|
|
998
|
-
|
|
999
|
-
},
|
|
1086
|
+
role: "dialog",
|
|
1087
|
+
"aria-label": title,
|
|
1088
|
+
// Nothing may paint at the viewport origin before placement runs.
|
|
1089
|
+
style: { visibility: "hidden" },
|
|
1000
1090
|
children: [
|
|
1001
1091
|
react_jsx_runtime.jsxs("div", {
|
|
1002
1092
|
className: S.panelHead,
|
|
1003
1093
|
children: [
|
|
1004
|
-
react_jsx_runtime.
|
|
1005
|
-
|
|
1006
|
-
|
|
1007
|
-
|
|
1008
|
-
|
|
1009
|
-
|
|
1010
|
-
|
|
1011
|
-
|
|
1094
|
+
react_jsx_runtime.jsxs("span", {
|
|
1095
|
+
className: S.panelTitleLabel,
|
|
1096
|
+
children: [
|
|
1097
|
+
react_jsx_runtime.jsxs("svg", {
|
|
1098
|
+
width: 14,
|
|
1099
|
+
height: 14,
|
|
1100
|
+
viewBox: "0 0 24 24",
|
|
1101
|
+
fill: "none",
|
|
1102
|
+
stroke: "currentColor",
|
|
1103
|
+
"stroke-width": 2,
|
|
1104
|
+
"stroke-linecap": "round",
|
|
1105
|
+
"stroke-linejoin": "round",
|
|
1106
|
+
"aria-hidden": "true",
|
|
1107
|
+
children: [
|
|
1108
|
+
react_jsx_runtime.jsx("circle", { cx: 12, cy: 12, r: 9 }),
|
|
1109
|
+
react_jsx_runtime.jsx("path", { d: "M12 11v5" }),
|
|
1110
|
+
react_jsx_runtime.jsx("path", { d: "M12 8h.01" })
|
|
1111
|
+
]
|
|
1112
|
+
}),
|
|
1113
|
+
react_jsx_runtime.jsx("span", { children: title })
|
|
1114
|
+
]
|
|
1012
1115
|
})
|
|
1013
1116
|
]
|
|
1014
1117
|
}),
|
|
1118
|
+
react_jsx_runtime.jsx("div", { className: S.panelRule, "aria-hidden": "true" }),
|
|
1015
1119
|
react_jsx_runtime.jsxs("div", {
|
|
1016
1120
|
className: S.settingsGroup,
|
|
1017
1121
|
children: [
|
|
@@ -1058,54 +1162,68 @@ window.__ModuleLoader__.load({
|
|
|
1058
1162
|
onClick: () => chooseView(mode),
|
|
1059
1163
|
children: dict(mode === "month" ? "viewMonth" : "viewYear")
|
|
1060
1164
|
}, mode))
|
|
1061
|
-
}),
|
|
1062
|
-
react_jsx_runtime.jsx("span", {
|
|
1063
|
-
className: S.settingsHint,
|
|
1064
|
-
children: dict("settingsDefaultViewHint")
|
|
1065
1165
|
})
|
|
1066
1166
|
]
|
|
1067
1167
|
}),
|
|
1068
|
-
react_jsx_runtime.
|
|
1069
|
-
className:
|
|
1070
|
-
children:
|
|
1071
|
-
|
|
1072
|
-
|
|
1168
|
+
react_jsx_runtime.jsxs("div", {
|
|
1169
|
+
className: S.panelFoot,
|
|
1170
|
+
children: [
|
|
1171
|
+
react_jsx_runtime.jsxs("div", {
|
|
1172
|
+
className: S.footLegend,
|
|
1173
|
+
children: [
|
|
1174
|
+
dict("less"),
|
|
1175
|
+
...palette.map((color, level) => react_jsx_runtime.jsx("span", {
|
|
1176
|
+
className: S.legendCell,
|
|
1177
|
+
style: { background: color },
|
|
1178
|
+
title: legend[level] ?? ""
|
|
1179
|
+
}, `foot-${level}`)),
|
|
1180
|
+
dict("more")
|
|
1181
|
+
]
|
|
1182
|
+
}),
|
|
1183
|
+
saveFailed ? react_jsx_runtime.jsx("div", {
|
|
1184
|
+
className: S.footError,
|
|
1185
|
+
children: dict("settingsSaveFailed")
|
|
1186
|
+
}) : react_jsx_runtime.jsx("div", {
|
|
1187
|
+
className: S.footRule,
|
|
1188
|
+
children: dict("settingsThresholdNote")
|
|
1189
|
+
})
|
|
1190
|
+
]
|
|
1073
1191
|
})
|
|
1074
1192
|
]
|
|
1075
1193
|
});
|
|
1194
|
+
return reactDom.createPortal(panel, document.body);
|
|
1076
1195
|
}
|
|
1077
1196
|
//#endregion
|
|
1078
1197
|
|
|
1079
1198
|
//#region plugin body
|
|
1080
1199
|
/**
|
|
1081
|
-
*
|
|
1082
|
-
* the
|
|
1083
|
-
*
|
|
1200
|
+
* Profile entry id this plugin's config form is keyed by. DSH 0.2.0
|
|
1201
|
+
* replaced the plugin-owned settings namespace with schema-derived plugin
|
|
1202
|
+
* CONFIG: the Host exports `Config`, the form is keyed by the profile
|
|
1203
|
+
* entry id — the `id` of the row our bundle patch inserts, which is what
|
|
1204
|
+
* the browser asks for here. Spelled as a literal rather than imported: a
|
|
1205
|
+
* client package must not depend on a Host package.
|
|
1084
1206
|
*/
|
|
1085
1207
|
const SETTINGS_NS = "token-heatmap";
|
|
1086
1208
|
/** Services required by the client plugin body. */
|
|
1087
|
-
const inject = ["slots", "locale", "
|
|
1209
|
+
const inject = ["slots", "locale", "configForms"];
|
|
1088
1210
|
|
|
1089
1211
|
/**
|
|
1090
1212
|
* Client plugin body: register the dictionaries and the input-dock entry
|
|
1091
|
-
* (the heatmap below the composer card on the new-session screen)
|
|
1092
|
-
*
|
|
1093
|
-
*
|
|
1094
|
-
*
|
|
1095
|
-
* 设置 → 插件 → 插件配置 card seat) any more — the Host still serves the
|
|
1096
|
-
* namespace, it just has no configuration card to dispatch.
|
|
1213
|
+
* (the heatmap below the composer card on the new-session screen). The
|
|
1214
|
+
* card configures itself through its own ⚙ panel, which reads and writes
|
|
1215
|
+
* the plugin's config form (`ctx.configForms.get("token-heatmap")`) —
|
|
1216
|
+
* the shared form owns transport, revisions, and recovery.
|
|
1097
1217
|
*
|
|
1098
1218
|
* Slot-kind contract: `conversation.input.dock` is a LIST slot, so its
|
|
1099
|
-
* registration is keyed by `id
|
|
1100
|
-
* would instead need `key`, the settings namespace the card edits).
|
|
1219
|
+
* registration is keyed by `id`.
|
|
1101
1220
|
* @param ctx - client root context.
|
|
1102
1221
|
*/
|
|
1103
1222
|
function apply(ctx) {
|
|
1104
|
-
//
|
|
1105
|
-
//
|
|
1106
|
-
//
|
|
1107
|
-
|
|
1108
|
-
const scope = ctx.settingsScope.bind({ namespace: SETTINGS_NS });
|
|
1223
|
+
// The shared form for our entry: `getSnapshot()`/`subscribe()` for
|
|
1224
|
+
// reads, `set(field, value)` for writes. `createConfigStore` is
|
|
1225
|
+
// duck-typed over exactly that surface, so the ⚙ panel is unchanged.
|
|
1226
|
+
const scope = ctx.configForms.get(SETTINGS_NS);
|
|
1109
1227
|
configStore = createConfigStore(scope);
|
|
1110
1228
|
ctx.effect(() => ctx.locale.register(NS, { zh, en }), "token-heatmap: dictionaries");
|
|
1111
1229
|
ctx.slots.inject("conversation.input.dock", () => ctx.slots.register({
|
package/lib/index.js
CHANGED
|
@@ -11,11 +11,13 @@
|
|
|
11
11
|
* its own peer-socket loopback fence (the exact route bypasses the RPC trust
|
|
12
12
|
* fence); Host is checked only as an additional defense.
|
|
13
13
|
*
|
|
14
|
-
* Display settings (
|
|
15
|
-
*
|
|
16
|
-
*
|
|
17
|
-
*
|
|
18
|
-
*
|
|
14
|
+
* Display settings (colorScheme + defaultView) are owned by this plugin's
|
|
15
|
+
* profile CONFIG — DSH 0.2.0 derives the settings form from the exported
|
|
16
|
+
* `Config` schema, keys it by the profile entry id (`token-heatmap`, the id of
|
|
17
|
+
* the row our bundle patch inserts), and the browser edits it through
|
|
18
|
+
* `ctx.configForms.get("token-heatmap")`. The plugin's own ⚙ panel writes the
|
|
19
|
+
* same values; the legacy `<DSH_HOME>/storages/token-heatmap-config.json`
|
|
20
|
+
* document is migrated into it once at startup.
|
|
19
21
|
*
|
|
20
22
|
* Usage aggregation is INCREMENTAL: per-session fold state (day/model
|
|
21
23
|
* buckets plus the last usage sample) is cached in memory and persisted to
|
|
@@ -33,6 +35,7 @@
|
|
|
33
35
|
*/
|
|
34
36
|
|
|
35
37
|
import { homedir } from "node:os";
|
|
38
|
+
import { readFileSync } from "node:fs";
|
|
36
39
|
import { join, dirname } from "node:path";
|
|
37
40
|
import { mkdir, readFile, rename, rm, writeFile } from "node:fs/promises";
|
|
38
41
|
import { applyUsageDelta, createUsageState, mergeInto, renderUsage, zeroBuckets } from "./usage.js";
|
|
@@ -45,24 +48,17 @@ const name = "token-heatmap";
|
|
|
45
48
|
/** Services required before this plugin activates. */
|
|
46
49
|
const inject = ["webServer", "sessions", "sessionPersistence", "settings"];
|
|
47
50
|
|
|
48
|
-
//#region
|
|
51
|
+
//#region plugin config
|
|
49
52
|
/**
|
|
50
|
-
*
|
|
51
|
-
*
|
|
52
|
-
*
|
|
53
|
-
*
|
|
54
|
-
* card registered into `settings.plugin.item` whose `key` matches a served
|
|
55
|
-
* namespace. The card edits `enabled` + `colorScheme` through the settings
|
|
56
|
-
* scope; the legacy `<DSH_HOME>/storages/token-heatmap-config.json` document
|
|
57
|
-
* is migrated once at startup (see migrateLegacyConfig).
|
|
58
|
-
* DSH 0.1.2 起 `@deepseek-ai/dsh-settings` 不再导出 settingsNamespace 帮助函数:
|
|
59
|
-
* namespace 直接以字面量传入(register/get/update 内部仍按
|
|
60
|
-
* /^[a-z][a-z0-9-]*$/ 校验)。
|
|
53
|
+
* Profile entry id this plugin's config form is keyed by: the `id` of the row
|
|
54
|
+
* the bundle patch inserts, which is also what the browser asks
|
|
55
|
+
* `ctx.configForms.get(...)` for. DSH 0.2.0 derives the form from the exported
|
|
56
|
+
* `Config` schema, so no registration call exists (nor does `settings.get`).
|
|
61
57
|
*/
|
|
62
58
|
const SETTINGS_NAMESPACE = "token-heatmap";
|
|
63
59
|
|
|
64
60
|
/**
|
|
65
|
-
* Durable display preferences; also the wire envelope the browser
|
|
61
|
+
* Durable display preferences; also the wire envelope the browser form
|
|
66
62
|
* validates against. Scheme membership is deliberately NOT enforced (a newer
|
|
67
63
|
* client may know a palette the server does not — the client falls back to
|
|
68
64
|
* green); only the same shape bounds parseConfig applies: a short, non-blank
|
|
@@ -72,10 +68,12 @@ const SETTINGS_NAMESPACE = "token-heatmap";
|
|
|
72
68
|
* the hero screen, so the field is neither accepted nor served (an old
|
|
73
69
|
* settings.yaml keeps its dead `enabled` key, and nothing reads it).
|
|
74
70
|
*/
|
|
75
|
-
const
|
|
71
|
+
const Config = z.object({
|
|
76
72
|
colorScheme: z.string().min(1).max(32).default("green"),
|
|
77
73
|
defaultView: z.union(VIEW_MODES.map((mode) => z.const(mode))).default("year")
|
|
78
74
|
});
|
|
75
|
+
/** @deprecated 0.1.x name; the schema IS the config now. Kept for old importers. */
|
|
76
|
+
const TokenHeatmapSettingsSchema = Config;
|
|
79
77
|
//#endregion
|
|
80
78
|
|
|
81
79
|
const USAGE_PATH = "/api/token-heatmap/usage";
|
|
@@ -329,14 +327,127 @@ function configPath() {
|
|
|
329
327
|
return join(home, "storages", "token-heatmap-config.json");
|
|
330
328
|
}
|
|
331
329
|
|
|
330
|
+
/** DSH home resolution, mirroring `@deepseek-ai/dsh-home-paths` (no extra peer). */
|
|
331
|
+
function dshHomeDirectory(env = process.env) {
|
|
332
|
+
const configured = env.DSH_HOME;
|
|
333
|
+
if (typeof configured === "string" && configured.trim() !== "") return configured.trim();
|
|
334
|
+
return join(homedir(), ".dsh");
|
|
335
|
+
}
|
|
336
|
+
|
|
337
|
+
/**
|
|
338
|
+
* One profile entry's descriptor from the settings service, or undefined when
|
|
339
|
+
* the service is absent or the entry is not live. DSH 0.2.0 dropped the old
|
|
340
|
+
* `settings.get(ns)` accessor — a plugin's settings ARE its profile entry
|
|
341
|
+
* config now — so reads go through the same `describe()` view the settings UI
|
|
342
|
+
* renders (its `value` is the resolved section, its `user` the raw user layer).
|
|
343
|
+
* @param ctx - plugin context.
|
|
344
|
+
* @param entryId - profile entry id (`token-heatmap`).
|
|
345
|
+
* @returns the descriptor, or undefined.
|
|
346
|
+
*/
|
|
347
|
+
function entryDescriptor(ctx, entryId) {
|
|
348
|
+
const settings = typeof ctx.get === "function" ? ctx.get("settings") : ctx.settings;
|
|
349
|
+
if (settings === null || settings === void 0 || typeof settings.describe !== "function") return void 0;
|
|
350
|
+
try {
|
|
351
|
+
return settings.describe().find((row) => row.ns === entryId);
|
|
352
|
+
} catch {
|
|
353
|
+
return void 0;
|
|
354
|
+
}
|
|
355
|
+
}
|
|
356
|
+
|
|
357
|
+
/**
|
|
358
|
+
* One profile entry's resolved config section, read through the settings
|
|
359
|
+
* service.
|
|
360
|
+
* @param ctx - plugin context.
|
|
361
|
+
* @param entryId - profile entry id (`token-heatmap`).
|
|
362
|
+
* @returns the resolved section, or undefined when the entry is absent.
|
|
363
|
+
*/
|
|
364
|
+
function entryConfig(ctx, entryId) {
|
|
365
|
+
const value = entryDescriptor(ctx, entryId)?.value;
|
|
366
|
+
return value !== null && typeof value === "object" ? value : void 0;
|
|
367
|
+
}
|
|
368
|
+
|
|
369
|
+
/**
|
|
370
|
+
* Best-effort read of one scalar out of the pre-0.2.0 `settings.yaml` section
|
|
371
|
+
* for this entry. DSH's own importer moves those sections into the profile
|
|
372
|
+
* once, keyed by entry id — and skipped ours while the bundle was refused for
|
|
373
|
+
* incompatible peers, leaving the value only in the renamed document. This is
|
|
374
|
+
* a deliberately narrow scan (one top-level section, one scalar) rather than a
|
|
375
|
+
* YAML dependency: anything it cannot read is simply left for the user.
|
|
376
|
+
* @param text - contents of `settings.yaml` or `settings.yaml.imported`.
|
|
377
|
+
* @param section - top-level section name (`token-heatmap`).
|
|
378
|
+
* @param field - scalar key inside it.
|
|
379
|
+
* @returns the raw trimmed string value, or null.
|
|
380
|
+
*/
|
|
381
|
+
function legacySettingFromDocument(text, section, field) {
|
|
382
|
+
if (typeof text !== "string") return null;
|
|
383
|
+
const lines = text.split(/\r?\n/);
|
|
384
|
+
const inSection = /^[^\s#][^:]*:/;
|
|
385
|
+
let inside = false;
|
|
386
|
+
for (const line of lines) {
|
|
387
|
+
if (inSection.test(line)) {
|
|
388
|
+
inside = line.trim() === `${section}:` && !line.startsWith(" ");
|
|
389
|
+
continue;
|
|
390
|
+
}
|
|
391
|
+
if (!inside) continue;
|
|
392
|
+
const match = /^\s+([A-Za-z0-9_-]+):\s*(.*?)\s*$/.exec(line);
|
|
393
|
+
if (match === null || match[1] !== field) continue;
|
|
394
|
+
return match[2].replace(/\s+#.*$/, "").replace(/^["']|["']$/g, "");
|
|
395
|
+
}
|
|
396
|
+
return null;
|
|
397
|
+
}
|
|
398
|
+
|
|
399
|
+
/** Files the legacy value may still live in, newest name first. */
|
|
400
|
+
const LEGACY_SETTINGS_FILES = ["settings.yaml.imported", "settings.yaml"];
|
|
401
|
+
|
|
402
|
+
/**
|
|
403
|
+
* One-time rescue of display preferences from the removed settings document.
|
|
404
|
+
*
|
|
405
|
+
* DSH 0.2.0 imported the old `settings.yaml` sections per entry id, but this
|
|
406
|
+
* plugin was refused (incompatible peers) at that moment, so its section —
|
|
407
|
+
* `colorScheme` / `defaultView`, plus the dead `enabled` — only survives in
|
|
408
|
+
* the renamed document. Runs only when the entry has no user value yet, only
|
|
409
|
+
* for non-default fields, best effort, never blocking.
|
|
410
|
+
* @param ctx - plugin context carrying the settings service.
|
|
411
|
+
*/
|
|
412
|
+
async function rescueRemovedSettingsDocument(ctx) {
|
|
413
|
+
try {
|
|
414
|
+
const descriptor = entryDescriptor(ctx, SETTINGS_NAMESPACE);
|
|
415
|
+
if (descriptor === void 0) return;
|
|
416
|
+
const settings = typeof ctx.get === "function" ? ctx.get("settings") : ctx.settings;
|
|
417
|
+
const user = descriptor.user;
|
|
418
|
+
if (user !== null && typeof user === "object" && (Object.hasOwn(user, "colorScheme") || Object.hasOwn(user, "defaultView"))) return;
|
|
419
|
+
const home = dshHomeDirectory();
|
|
420
|
+
let text = null;
|
|
421
|
+
for (const file of LEGACY_SETTINGS_FILES) {
|
|
422
|
+
try {
|
|
423
|
+
text = readFileSync(join(home, file), "utf8");
|
|
424
|
+
} catch {
|
|
425
|
+
continue;
|
|
426
|
+
}
|
|
427
|
+
if (text !== null) break;
|
|
428
|
+
}
|
|
429
|
+
if (text === null) return;
|
|
430
|
+
const patch = {};
|
|
431
|
+
const scheme = legacySettingFromDocument(text, SETTINGS_NAMESPACE, "colorScheme");
|
|
432
|
+
if (scheme !== null && scheme !== DEFAULT_CONFIG.colorScheme) patch.colorScheme = scheme;
|
|
433
|
+
const view = legacySettingFromDocument(text, SETTINGS_NAMESPACE, "defaultView");
|
|
434
|
+
if (view !== null && VIEW_MODES.includes(view) && view !== DEFAULT_CONFIG.defaultView) patch.defaultView = view;
|
|
435
|
+
if (Object.keys(patch).length === 0) return;
|
|
436
|
+
await settings.update(SETTINGS_NAMESPACE, patch);
|
|
437
|
+
ctx.logger.info(`token-heatmap: carried ${JSON.stringify(patch)} over from the removed settings.yaml`);
|
|
438
|
+
} catch (error) {
|
|
439
|
+
ctx.logger.warn(`token-heatmap: legacy settings rescue skipped: ${String(error)}`);
|
|
440
|
+
}
|
|
441
|
+
}
|
|
442
|
+
|
|
332
443
|
/**
|
|
333
444
|
* One-time migration from the legacy config document
|
|
334
|
-
* (`<DSH_HOME>/storages/token-heatmap-config.json`) into the
|
|
335
|
-
*
|
|
336
|
-
*
|
|
337
|
-
*
|
|
338
|
-
*
|
|
339
|
-
*
|
|
445
|
+
* (`<DSH_HOME>/storages/token-heatmap-config.json`) into the plugin config.
|
|
446
|
+
* Runs once per process, best-effort: when the user has no config value yet,
|
|
447
|
+
* non-default values are imported through the settings write path; the legacy
|
|
448
|
+
* file is removed either way (the config entry becomes the single source of
|
|
449
|
+
* truth). A corrupt legacy document is dropped, never imported. Failures are
|
|
450
|
+
* logged and never fatal.
|
|
340
451
|
* @param ctx - plugin context carrying the settings service.
|
|
341
452
|
*/
|
|
342
453
|
async function migrateLegacyConfig(ctx) {
|
|
@@ -355,15 +466,16 @@ async function migrateLegacyConfig(ctx) {
|
|
|
355
466
|
await rm(path, { force: true });
|
|
356
467
|
return;
|
|
357
468
|
}
|
|
358
|
-
const descriptor = ctx
|
|
469
|
+
const descriptor = entryDescriptor(ctx, SETTINGS_NAMESPACE);
|
|
470
|
+
const settings = typeof ctx.get === "function" ? ctx.get("settings") : ctx.settings;
|
|
359
471
|
const userExists = descriptor !== void 0 && descriptor.user !== void 0;
|
|
360
|
-
if (!userExists) {
|
|
472
|
+
if (!userExists && settings !== void 0 && typeof settings.update === "function") {
|
|
361
473
|
const patch = {};
|
|
362
474
|
// The legacy document's `enabled` switch died with 0.2.0: import only
|
|
363
475
|
// what the card still owns.
|
|
364
476
|
if (legacy.colorScheme !== DEFAULT_CONFIG.colorScheme) patch.colorScheme = legacy.colorScheme;
|
|
365
477
|
if (legacy.defaultView !== DEFAULT_CONFIG.defaultView) patch.defaultView = legacy.defaultView;
|
|
366
|
-
if (Object.keys(patch).length > 0) await
|
|
478
|
+
if (Object.keys(patch).length > 0) await settings.update(SETTINGS_NAMESPACE, patch);
|
|
367
479
|
}
|
|
368
480
|
await rm(path, { force: true });
|
|
369
481
|
} catch (error) {
|
|
@@ -372,13 +484,13 @@ async function migrateLegacyConfig(ctx) {
|
|
|
372
484
|
}
|
|
373
485
|
|
|
374
486
|
/**
|
|
375
|
-
* Serve the resolved
|
|
487
|
+
* Serve the resolved config section (schema defaults + user layer).
|
|
376
488
|
* `enabled` stays in the payload as a constant true: the legacy loopback
|
|
377
489
|
* endpoint is a back-compat API, and a pre-0.2.0 client that reads it must not
|
|
378
490
|
* lose the card over a switch this version no longer has.
|
|
379
491
|
*/
|
|
380
492
|
function serveConfig(ctx) {
|
|
381
|
-
const section = ctx
|
|
493
|
+
const section = entryConfig(ctx, SETTINGS_NAMESPACE);
|
|
382
494
|
const scheme = typeof section?.colorScheme === "string" && section.colorScheme.length > 0 ? section.colorScheme : DEFAULT_CONFIG.colorScheme;
|
|
383
495
|
const view = typeof section?.defaultView === "string" && VIEW_MODES.includes(section.defaultView) ? section.defaultView : DEFAULT_CONFIG.defaultView;
|
|
384
496
|
return { enabled: true, colorScheme: scheme, defaultView: view };
|
|
@@ -687,19 +799,22 @@ async function handleUsage(ctx, req, res) {
|
|
|
687
799
|
}
|
|
688
800
|
|
|
689
801
|
/**
|
|
690
|
-
* Plugin body: register the usage and config routes
|
|
691
|
-
*
|
|
692
|
-
*
|
|
802
|
+
* Plugin body: register the usage and config routes and run the two one-time
|
|
803
|
+
* legacy migrations. DSH 0.2.0 needs no namespace registration — the exported
|
|
804
|
+
* `Config` is the form — so nothing here touches the settings document beyond
|
|
805
|
+
* the rescues.
|
|
693
806
|
* @param ctx - plugin context carrying webServer, sessions, sessionPersistence, and settings.
|
|
694
807
|
*/
|
|
695
808
|
function apply(ctx) {
|
|
696
|
-
// The registration is fiber-bound: disposing this plugin removes the
|
|
697
|
-
// namespace and its observers. `settings` is a hard dependency (inject),
|
|
698
|
-
// so ctx.settings is available here unconditionally.
|
|
699
|
-
ctx.settings.register(SETTINGS_NAMESPACE, TokenHeatmapSettingsSchema);
|
|
700
809
|
// Best-effort, fire-and-forget: import the pre-0.1.2 config document into
|
|
701
|
-
// the
|
|
810
|
+
// the config entry and drop the file (see migrateLegacyConfig), then rescue
|
|
811
|
+
// whatever the removed settings.yaml still holds. The rescue waits for the
|
|
812
|
+
// Loader to settle, exactly like DSH's own importer did, so the entry it
|
|
813
|
+
// inspects is the live one.
|
|
702
814
|
migrateLegacyConfig(ctx);
|
|
815
|
+
const loader = ctx.root?.loader;
|
|
816
|
+
const settled = typeof loader?.await === "function" ? Promise.resolve(loader.await()).catch(() => void 0) : Promise.resolve();
|
|
817
|
+
settled.then(() => rescueRemovedSettingsDocument(ctx)).catch(() => void 0);
|
|
703
818
|
// Real-time fold: listen to session/event and fold each usage event into
|
|
704
819
|
// the cache immediately, so live session usage is captured regardless of
|
|
705
820
|
// whether the hero screen is mounted (the client polls the usage endpoint
|
|
@@ -779,4 +894,18 @@ function apply(ctx) {
|
|
|
779
894
|
}), "token-heatmap: config route");
|
|
780
895
|
}
|
|
781
896
|
|
|
782
|
-
export {
|
|
897
|
+
export {
|
|
898
|
+
apply,
|
|
899
|
+
inject,
|
|
900
|
+
name,
|
|
901
|
+
Config,
|
|
902
|
+
CONFIG_PATH,
|
|
903
|
+
USAGE_PATH,
|
|
904
|
+
SETTINGS_NAMESPACE,
|
|
905
|
+
TokenHeatmapSettingsSchema,
|
|
906
|
+
migrateLegacyConfig,
|
|
907
|
+
entryConfig,
|
|
908
|
+
dshHomeDirectory,
|
|
909
|
+
legacySettingFromDocument,
|
|
910
|
+
rescueRemovedSettingsDocument
|
|
911
|
+
};
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@kidli1412/dsh-token-heatmap",
|
|
3
|
-
"version": "0.
|
|
4
|
-
"description": "DSH web plugin: GitHub-style daily token-usage heatmap on the new-session screen with switchable year/month views and six color schemes. The card configures itself
|
|
3
|
+
"version": "0.5.0",
|
|
4
|
+
"description": "DSH web plugin: GitHub-style daily token-usage heatmap on the new-session screen with switchable year/month views and six color schemes. The card configures itself through a floating ⚙ panel (palette + default view) — no DSH settings card — and shows today / this-month / all-time totals.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "lib/index.js",
|
|
7
7
|
"exports": {
|
|
@@ -20,15 +20,26 @@
|
|
|
20
20
|
"@deepseek-ai/schemastery": "^3.18.2"
|
|
21
21
|
},
|
|
22
22
|
"peerDependencies": {
|
|
23
|
-
"@deepseek-ai/dsh-api-remotes": "^0.
|
|
24
|
-
"@deepseek-ai/dsh-client-connection": "^0.
|
|
25
|
-
"@deepseek-ai/dsh-client-locale": "^0.
|
|
26
|
-
"@deepseek-ai/dsh-client-ui-conversation": "^0.
|
|
27
|
-
"@deepseek-ai/dsh-client-ui-settings": "^0.
|
|
28
|
-
"@deepseek-ai/dsh-host-webserver": "^0.
|
|
29
|
-
"@deepseek-ai/dsh-session": "^0.
|
|
30
|
-
"@deepseek-ai/dsh-session-persistence": "^0.
|
|
31
|
-
"@deepseek-ai/dsh-settings": "^0.
|
|
23
|
+
"@deepseek-ai/dsh-api-remotes": "^0.2.0-rc.2",
|
|
24
|
+
"@deepseek-ai/dsh-client-connection": "^0.2.0-rc.2",
|
|
25
|
+
"@deepseek-ai/dsh-client-locale": "^0.2.0-rc.2",
|
|
26
|
+
"@deepseek-ai/dsh-client-ui-conversation": "^0.2.0-rc.2",
|
|
27
|
+
"@deepseek-ai/dsh-client-ui-settings": "^0.2.0-rc.2",
|
|
28
|
+
"@deepseek-ai/dsh-host-webserver": "^0.2.0-rc.2",
|
|
29
|
+
"@deepseek-ai/dsh-session": "^0.2.0-rc.2",
|
|
30
|
+
"@deepseek-ai/dsh-session-persistence": "^0.2.0-rc.2",
|
|
31
|
+
"@deepseek-ai/dsh-settings": "^0.2.0-rc.2"
|
|
32
|
+
},
|
|
33
|
+
"peerDependenciesMeta": {
|
|
34
|
+
"@deepseek-ai/dsh-api-remotes": { "optional": true },
|
|
35
|
+
"@deepseek-ai/dsh-client-connection": { "optional": true },
|
|
36
|
+
"@deepseek-ai/dsh-client-locale": { "optional": true },
|
|
37
|
+
"@deepseek-ai/dsh-client-ui-conversation": { "optional": true },
|
|
38
|
+
"@deepseek-ai/dsh-client-ui-settings": { "optional": true },
|
|
39
|
+
"@deepseek-ai/dsh-host-webserver": { "optional": true },
|
|
40
|
+
"@deepseek-ai/dsh-session": { "optional": true },
|
|
41
|
+
"@deepseek-ai/dsh-session-persistence": { "optional": true },
|
|
42
|
+
"@deepseek-ai/dsh-settings": { "optional": true }
|
|
32
43
|
},
|
|
33
44
|
"dsh": {
|
|
34
45
|
"bundle": {
|
|
@@ -46,9 +57,7 @@
|
|
|
46
57
|
},
|
|
47
58
|
"compatibility": {
|
|
48
59
|
"dshReleases": {
|
|
49
|
-
"0.
|
|
50
|
-
"0.1.2-alpha.5": "compatible",
|
|
51
|
-
"0.1.2-rc.1": "compatible"
|
|
60
|
+
"0.2.0-rc.2": "compatible"
|
|
52
61
|
}
|
|
53
62
|
}
|
|
54
63
|
},
|
|
@@ -60,7 +69,7 @@
|
|
|
60
69
|
"license": "MIT",
|
|
61
70
|
"engines": {
|
|
62
71
|
"node": "^22.19.0 || >=24.0.0",
|
|
63
|
-
"dsh": "^0.
|
|
72
|
+
"dsh": "^0.2.0-rc.2"
|
|
64
73
|
},
|
|
65
74
|
"repository": {
|
|
66
75
|
"type": "git",
|