dsh-web-icon-indicator 0.2.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/README.md +10 -6
- package/README.zh.md +8 -5
- package/lib/client.js +147 -78
- package/lib/index.js +21 -2
- package/lib/types/index.d.ts +6 -0
- package/package.json +3 -2
package/README.md
CHANGED
|
@@ -102,15 +102,19 @@ service under the `web-icon-indicator` namespace (a schemastery schema in
|
|
|
102
102
|
|
|
103
103
|
- **Web GUI:** open **设置 → 插件 → 插件配置** — a *Favicon indicator* card
|
|
104
104
|
edits the same keys (asking/done hold, and per-state effect / colors /
|
|
105
|
-
cycle), staged and saved through the settings transport.
|
|
105
|
+
cycle), staged and saved through the settings transport. Each state is a
|
|
106
|
+
collapsible row showing a color dot and a one-line summary (`blink ·
|
|
107
|
+
#E5484D ⇄ #FACC15 · 400ms`); expanding a row reveals its three fields, and
|
|
108
|
+
the colors field previews parsed swatches live.
|
|
106
109
|
- **Persistence:** values land in the profile's `settings.yaml` (default
|
|
107
110
|
`~/.dsh/settings.yaml`) as a `web-icon-indicator:` section. The composition
|
|
108
111
|
entry stays the `base` layer; resolution order is schema defaults →
|
|
109
112
|
composition entry → settings document user layer.
|
|
110
|
-
- **No server restart** for
|
|
111
|
-
`doneHoldMs` apply live
|
|
112
|
-
|
|
113
|
-
|
|
113
|
+
- **No server restart, no tab reload** for settings-card saves: `askingHoldMs` /
|
|
114
|
+
`doneHoldMs` apply live host-side, and per-state visual config (effect /
|
|
115
|
+
colors / cycle) is synced into the running tab through the status poll within
|
|
116
|
+
~1 s. Only code-level default changes in `lib/index.js` need a tab reload (or
|
|
117
|
+
a DSH web rebuild).
|
|
114
118
|
- The browser half is a hand-written `lib/client.js` (ModuleLoader factory
|
|
115
119
|
format — no build step, no runtime deps beyond the shell's `react`). The DSH
|
|
116
120
|
client scanner picks a new `dsh.client` declaration up on the next profile
|
|
@@ -124,7 +128,7 @@ service under the `web-icon-indicator` namespace (a schemastery schema in
|
|
|
124
128
|
- Status is aggregated across live `agents.list()` with priority `asking > running > done > idle`. The aggregation runs a `reconcile()` step on every request to detect running → idle transitions, because `agent/status`'s idle delivery is not guaranteed at turn end.
|
|
125
129
|
- `ask_user_question` tool calls (via `tools/pre-execute` / `tools/result`) flip the session into `asking` with a configurable minimum-hold so the icon stays visible even when the user answers immediately.
|
|
126
130
|
- Permission / **sandbox-interception** waits are also surfaced as `asking`: when the agent hits a sandbox denial and escalates (`sandbox_permissions` + `justification`), or any other tool asks for approval, the approval service appends an `approval/asked` session event and blocks the agent until you decide. The plugin watches `session/event` (with an authoritative fold over the live session log as a fallback) and pins the session into the `asking` state for that whole wait, clearing it on `approval/decided`.
|
|
127
|
-
- The browser script polls `/dsh-web-icon-status.json` once a second, fetches `base.svg` once, and then on every `requestAnimationFrame` tick rebuilds the favicon as a `data:image/svg+xml,…` URI — replacing the `__COLOR__` placeholder with the state's configured color and applying the state's configured effect. Browsers don't play favicon SVG CSS animations, so all motion is JS-driven. Because browsers pause `requestAnimationFrame` in hidden tabs, the poll also repaints a wall-clock frame for animated states, so background tabs keep animating (coarsely) instead of freezing; full-speed animation resumes when the tab is visible again. The poll also survives host restarts: a transient fetch failure restores the original icon and retries on the next tick (the SPA reconnects in place, so the icon comes back without a manual refresh).
|
|
131
|
+
- The browser script polls `/dsh-web-icon-status.json` once a second, fetches `base.svg` once, and then on every `requestAnimationFrame` tick rebuilds the favicon as a `data:image/svg+xml,…` URI — replacing the `__COLOR__` placeholder with the state's configured color and applying the state's configured effect. The status response also echoes the current per-state visual config, so a settings save reaches the running tab on the next poll (~1 s) without a reload. Browsers don't play favicon SVG CSS animations, so all motion is JS-driven. Because browsers pause `requestAnimationFrame` in hidden tabs, the poll also repaints a wall-clock frame for animated states, so background tabs keep animating (coarsely) instead of freezing; full-speed animation resumes when the tab is visible again. The poll also survives host restarts: a transient fetch failure restores the original icon and retries on the next tick (the SPA reconnects in place, so the icon comes back without a manual refresh).
|
|
128
132
|
|
|
129
133
|
## Caveats
|
|
130
134
|
|
package/README.zh.md
CHANGED
|
@@ -101,13 +101,16 @@ config:
|
|
|
101
101
|
|
|
102
102
|
- **Web GUI:** 打开 **设置 → 插件 → 插件配置**,会出现 *标签页图标指示器*
|
|
103
103
|
卡片,可编辑同样的键(提问/完成驻留,以及每个状态的特效 / 颜色 / 周期),
|
|
104
|
-
通过 settings
|
|
104
|
+
通过 settings 传输层暂存并保存。每个状态是一行可折叠条目,带主色圆点和一行
|
|
105
|
+
摘要(如 `blink · #E5484D ⇄ #FACC15 · 400ms`);展开该行才显示它的三个字段,
|
|
106
|
+
颜色输入框旁会实时预览解析出的色块。
|
|
105
107
|
- **持久化:** 值写入 profile 的 `settings.yaml`(默认 `~/.dsh/settings.yaml`)
|
|
106
108
|
的 `web-icon-indicator:` 段。合成条目仍是 `base` 层;解析顺序为 schema 默认值
|
|
107
109
|
→ 合成条目 → 设置文档用户层。
|
|
108
|
-
-
|
|
109
|
-
|
|
110
|
-
|
|
110
|
+
- **无需重启服务器、无需刷新标签页**即可让设置卡片的修改生效:`askingHoldMs` /
|
|
111
|
+
`doneHoldMs` 在主机侧即时生效;各状态的视觉配置(特效 / 颜色 / 周期)会随状态
|
|
112
|
+
轮询同步进正在运行的标签页,约 1 秒内生效。只有改 `lib/index.js` 里的代码级
|
|
113
|
+
默认值才需要重载标签页(或重新构建 DSH Web)。
|
|
111
114
|
- 浏览器半区是手写的 `lib/client.js`(ModuleLoader factory 格式——无构建步骤、
|
|
112
115
|
无额外运行期依赖,仅用 shell 自带的 `react`)。DSH 客户端扫描器会在下次启动
|
|
113
116
|
profile 时识别新的 `dsh.client` 声明。
|
|
@@ -119,7 +122,7 @@ config:
|
|
|
119
122
|
- 状态按 `agents.list()` 聚合,优先级 `asking > running > done > idle`。每次请求都会执行一次 `reconcile()` 检测 running → idle 的转换,因为 `agent/status` 的 idle 事件在回合结束时并不保证送达。
|
|
120
123
|
- `ask_user_question` 工具调用(通过 `tools/pre-execute` / `tools/result`)把会话置为 `asking`,带可配置的最小保持时长,即使你立刻回答,图标也会保持可见。
|
|
121
124
|
- 权限 / **沙箱拦截**等待同样会显示为 `asking`:当 agent 命中沙箱拒绝并请求提权(`sandbox_permissions` + `justification`),或其他工具需要征得同意时,审批服务会先写入一条 `approval/asked` 会话事件并阻塞 agent,直到你做出决定。插件监听 `session/event`(并以实时会话日志的权威折叠作为兜底)在整个等待期间将会话置为 `asking` 状态,收到 `approval/decided` 后清除。
|
|
122
|
-
- 浏览器脚本每秒轮询 `/dsh-web-icon-status.json`,首次获取 `base.svg`,然后每个 `requestAnimationFrame` 周期把 favicon 重建为 `data:image/svg+xml,…` URI——把 `__COLOR__`
|
|
125
|
+
- 浏览器脚本每秒轮询 `/dsh-web-icon-status.json`,首次获取 `base.svg`,然后每个 `requestAnimationFrame` 周期把 favicon 重建为 `data:image/svg+xml,…` URI——把 `__COLOR__` 占位符替换为状态配置的颜色,并应用该状态配置的特效。状态响应还会携带当前的每状态视觉配置,因此设置保存后约 1 秒内(下一个轮询 tick)即同步到已打开的标签页,无需刷新。浏览器不会播放 SVG favicon 的 CSS 动画,所以一切动画都由 JS 驱动。由于浏览器在**隐藏(后台)标签页会暂停 `requestAnimationFrame`**,轮询还会为动画态补绘一帧按墙钟时间计算的画面——后台标签页保持粗粒度动画(约每 1 秒)而不会冻结,切回前台后恢复满速动画。轮询还能**扛住 host 重启**:瞬时请求失败时先还原原始图标,并在下一个 tick 重试(SPA 原地重连,无需手动刷新图标即可恢复)。
|
|
123
126
|
|
|
124
127
|
## 已知限制
|
|
125
128
|
|
package/lib/client.js
CHANGED
|
@@ -49,7 +49,10 @@ window.__ModuleLoader__.load({
|
|
|
49
49
|
askingHoldMsHint: "Minimum visibility of the asking icon before it settles.",
|
|
50
50
|
doneHoldMs: "Done hold (ms)",
|
|
51
51
|
doneHoldMsHint: "How long the done icon stays before returning to idle.",
|
|
52
|
-
|
|
52
|
+
stateIdle: "Idle",
|
|
53
|
+
stateRunning: "Running",
|
|
54
|
+
stateAsking: "Asking",
|
|
55
|
+
stateDone: "Done",
|
|
53
56
|
effect: "Effect",
|
|
54
57
|
effectHint: "Animation for this state.",
|
|
55
58
|
colors: "Colors",
|
|
@@ -75,7 +78,10 @@ window.__ModuleLoader__.load({
|
|
|
75
78
|
askingHoldMsHint: "提问图标的最短可见时长。",
|
|
76
79
|
doneHoldMs: "完成驻留(毫秒)",
|
|
77
80
|
doneHoldMsHint: "完成图标停留多久后回到待机。",
|
|
78
|
-
|
|
81
|
+
stateIdle: "待机",
|
|
82
|
+
stateRunning: "运行中",
|
|
83
|
+
stateAsking: "提问",
|
|
84
|
+
stateDone: "完成",
|
|
79
85
|
effect: "特效",
|
|
80
86
|
effectHint: "该状态的动画效果。",
|
|
81
87
|
colors: "颜色",
|
|
@@ -111,8 +117,13 @@ window.__ModuleLoader__.load({
|
|
|
111
117
|
hint: { color: "var(--dsw-alias-label-tertiary)", fontSize: 12, lineHeight: "1.5", margin: 0 },
|
|
112
118
|
invalidText: { color: "var(--dsw-alias-label-error)", fontSize: 12, lineHeight: "1.5", margin: 0 },
|
|
113
119
|
select: { border: "1px solid var(--dsw-alias-border-l2)", background: "var(--dsw-alias-bg-layer-3)", height: 34, borderRadius: 8, padding: "0 8px", fontSize: 13, fontFamily: "inherit", color: "var(--dsw-alias-label-primary)" },
|
|
114
|
-
|
|
115
|
-
|
|
120
|
+
statesBlock: { borderTop: "1px solid var(--dsw-alias-border-l2)", marginTop: 4, paddingTop: 4 },
|
|
121
|
+
dot: { display: "inline-block", width: 10, height: 10, borderRadius: 999, border: "1px solid var(--dsw-alias-border-l2)" },
|
|
122
|
+
summary: { flex: 1, minWidth: 0, marginLeft: 8, overflow: "hidden", whiteSpace: "nowrap", textOverflow: "ellipsis", color: "var(--dsw-alias-label-tertiary)", fontSize: 12, lineHeight: "24px" },
|
|
123
|
+
statePanel: { padding: "2px 0 12px 22px" },
|
|
124
|
+
inputRow: { display: "flex", alignItems: "center", gap: 8 },
|
|
125
|
+
swatches: { display: "inline-flex", alignItems: "center", gap: 4, flex: "none" },
|
|
126
|
+
swatch: { display: "inline-block", width: 14, height: 14, borderRadius: 4, border: "1px solid var(--dsw-alias-border-l2)" },
|
|
116
127
|
badges: { display: "inline-flex", alignItems: "center", gap: 8 },
|
|
117
128
|
badge: { whiteSpace: "nowrap", background: "var(--dsw-alias-bg-module-platform)", color: "var(--dsw-alias-label-secondary)", borderRadius: 999, padding: "1px 8px", fontSize: 11, fontWeight: 500, lineHeight: "17px" },
|
|
118
129
|
footer: { borderTop: "1px solid var(--dsw-alias-border-l2)", display: "flex", justifyContent: "flex-end", alignItems: "center", gap: 8, padding: "12px 0 4px" },
|
|
@@ -123,12 +134,12 @@ window.__ModuleLoader__.load({
|
|
|
123
134
|
// the field row and the inner <input> flex. Primitives components carry no
|
|
124
135
|
// CSS-module hooks for us (the bundle has no build step), so one guarded
|
|
125
136
|
// style tag mirrors what the official cards' stylesheets do.
|
|
126
|
-
var FORM_CSS_TAG = "dsh-web-icon-indicator/form.css";
|
|
137
|
+
var FORM_CSS_TAG = "dsh-web-icon-indicator/form.v2.css";
|
|
127
138
|
if (typeof document !== "undefined" && document.querySelector("style[data-plugin-css=" + JSON.stringify(FORM_CSS_TAG) + "]") === null) {
|
|
128
139
|
var cssTag = document.createElement("style");
|
|
129
140
|
cssTag.dataset.plugin = "dsh-web-icon-indicator";
|
|
130
141
|
cssTag.dataset.pluginCss = FORM_CSS_TAG;
|
|
131
|
-
cssTag.textContent = ".dsh-wii-input{display:flex;align-items:center;width:100%}.dsh-wii-input>input{flex:1;min-width:0}";
|
|
142
|
+
cssTag.textContent = ".dsh-wii-input{display:flex;align-items:center;width:100%}.dsh-wii-input>input{flex:1;min-width:0}.dsh-wii-state+.dsh-wii-state{border-top:1px solid var(--dsw-alias-border-l2)}";
|
|
132
143
|
document.head.appendChild(cssTag);
|
|
133
144
|
}
|
|
134
145
|
|
|
@@ -152,38 +163,24 @@ window.__ModuleLoader__.load({
|
|
|
152
163
|
);
|
|
153
164
|
}
|
|
154
165
|
|
|
155
|
-
function
|
|
156
|
-
// props: { id,
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
})
|
|
170
|
-
);
|
|
171
|
-
}
|
|
172
|
-
|
|
173
|
-
function TextInput(props) {
|
|
166
|
+
function ValueInput(props) {
|
|
167
|
+
// props: { id, label, hint, invalidLabel, invalid, separated, disabled, value,
|
|
168
|
+
// placeholder, numeric, trailing, onChange }
|
|
169
|
+
var input = createElement(primitives.Input, {
|
|
170
|
+
id: props.id,
|
|
171
|
+
type: "text",
|
|
172
|
+
inputMode: props.numeric ? "numeric" : void 0,
|
|
173
|
+
className: "dsh-wii-input",
|
|
174
|
+
style: inputStyle(props.invalid),
|
|
175
|
+
disabled: props.disabled,
|
|
176
|
+
value: props.value,
|
|
177
|
+
placeholder: props.placeholder || "",
|
|
178
|
+
onChange: function (event) { props.onChange(event.target.value); },
|
|
179
|
+
});
|
|
174
180
|
return createElement(
|
|
175
181
|
LabeledField,
|
|
176
182
|
{ label: props.label, hint: props.hint, invalid: props.invalid, invalidLabel: props.invalidLabel, separated: props.separated },
|
|
177
|
-
createElement(
|
|
178
|
-
id: props.id,
|
|
179
|
-
type: "text",
|
|
180
|
-
className: "dsh-wii-input",
|
|
181
|
-
style: inputStyle(props.invalid),
|
|
182
|
-
disabled: props.disabled,
|
|
183
|
-
value: props.value,
|
|
184
|
-
placeholder: props.placeholder || "",
|
|
185
|
-
onChange: function (event) { props.onChange(event.target.value); },
|
|
186
|
-
})
|
|
183
|
+
props.trailing ? createElement("div", { style: styles.inputRow }, input, props.trailing) : input
|
|
187
184
|
);
|
|
188
185
|
}
|
|
189
186
|
|
|
@@ -204,6 +201,24 @@ window.__ModuleLoader__.load({
|
|
|
204
201
|
);
|
|
205
202
|
}
|
|
206
203
|
|
|
204
|
+
/** Filled dot previewing one state's primary color (colors[0]). */
|
|
205
|
+
function ColorDot(props) {
|
|
206
|
+
return createElement("span", { style: Object.assign({}, styles.dot, { background: props.color }) });
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
/** One swatch per parsed color; renders nothing while the text is unparseable. */
|
|
210
|
+
function ColorSwatches(props) {
|
|
211
|
+
var colors = parseColors(props.text);
|
|
212
|
+
if (colors === null) return null;
|
|
213
|
+
return createElement(
|
|
214
|
+
"span",
|
|
215
|
+
{ style: styles.swatches, "aria-hidden": "true" },
|
|
216
|
+
colors.map(function (color) {
|
|
217
|
+
return createElement("span", { key: color, style: Object.assign({}, styles.swatch, { background: color }) });
|
|
218
|
+
})
|
|
219
|
+
);
|
|
220
|
+
}
|
|
221
|
+
|
|
207
222
|
// ---------------------------------------------------------------------------
|
|
208
223
|
// Form model
|
|
209
224
|
// ---------------------------------------------------------------------------
|
|
@@ -270,6 +285,10 @@ window.__ModuleLoader__.load({
|
|
|
270
285
|
var openState = useState(false);
|
|
271
286
|
var open = openState[0];
|
|
272
287
|
var setOpen = openState[1];
|
|
288
|
+
// Accordion: which per-state row is expanded (null = all collapsed).
|
|
289
|
+
var openNameState = useState(null);
|
|
290
|
+
var openName = openNameState[0];
|
|
291
|
+
var setOpenName = openNameState[1];
|
|
273
292
|
|
|
274
293
|
if (!ready || value === null) return null;
|
|
275
294
|
|
|
@@ -300,6 +319,27 @@ window.__ModuleLoader__.load({
|
|
|
300
319
|
return formatNumber(state.speed);
|
|
301
320
|
};
|
|
302
321
|
|
|
322
|
+
var stateTitleKey = function (name) {
|
|
323
|
+
return "state" + name.charAt(0).toUpperCase() + name.slice(1);
|
|
324
|
+
};
|
|
325
|
+
/** One-line collapsed summary: "blink · #E5484D ⇄ #FACC15 · 400ms" (draft-aware). */
|
|
326
|
+
var stateSummary = function (name) {
|
|
327
|
+
var parts = [stateFieldValue(name, "effect")];
|
|
328
|
+
var colorsText = stateFieldValue(name, "colors").trim();
|
|
329
|
+
if (colorsText !== "") {
|
|
330
|
+
var parsed = parseColors(colorsText);
|
|
331
|
+
parts.push(parsed === null ? colorsText : parsed.join(" ⇄ "));
|
|
332
|
+
}
|
|
333
|
+
var speedText = stateFieldValue(name, "speed").trim();
|
|
334
|
+
if (speedText !== "") parts.push(speedText + "ms");
|
|
335
|
+
return parts.join(" · ");
|
|
336
|
+
};
|
|
337
|
+
/** Primary color for the row's dot; transparent while the draft is unparseable. */
|
|
338
|
+
var statePrimaryColor = function (name) {
|
|
339
|
+
var parsed = parseColors(stateFieldValue(name, "colors"));
|
|
340
|
+
return parsed === null ? "transparent" : parsed[0];
|
|
341
|
+
};
|
|
342
|
+
|
|
303
343
|
var dirty = Object.keys(drafts).length > 0;
|
|
304
344
|
|
|
305
345
|
var save = function () {
|
|
@@ -316,10 +356,16 @@ window.__ModuleLoader__.load({
|
|
|
316
356
|
if (parsed.kind === "clear") { if (userHas(key)) ops.push({ op: "unset", field: key }); }
|
|
317
357
|
else if (parsed.value !== value[key]) ops.push({ op: "set", field: key, value: parsed.value });
|
|
318
358
|
}
|
|
319
|
-
// Per-state visuals: rebuild
|
|
320
|
-
//
|
|
359
|
+
// Per-state visuals: rebuild `states` as the user layer's current
|
|
360
|
+
// entries with the drafts applied. `scope.set` REPLACES the whole
|
|
361
|
+
// field (no deep merge), so untouched entries — overrides saved
|
|
362
|
+
// earlier for other states, plus any key not in STATE_NAMES — are
|
|
363
|
+
// carried through instead of being dropped.
|
|
321
364
|
var statesDirty = false;
|
|
322
365
|
var nextStates = {};
|
|
366
|
+
if (userLayer !== void 0 && userLayer !== null && typeof userLayer.states === "object" && userLayer.states !== null) {
|
|
367
|
+
nextStates = Object.assign({}, userLayer.states);
|
|
368
|
+
}
|
|
323
369
|
for (i = 0; i < STATE_NAMES.length; i += 1) {
|
|
324
370
|
var name = STATE_NAMES[i];
|
|
325
371
|
var effKey = "states." + name + ".effect";
|
|
@@ -419,67 +465,90 @@ window.__ModuleLoader__.load({
|
|
|
419
465
|
"div",
|
|
420
466
|
{ style: styles.body },
|
|
421
467
|
!writable ? createElement("p", { style: styles.hint }, t("readOnly")) : null,
|
|
422
|
-
createElement(
|
|
468
|
+
createElement(ValueInput, {
|
|
423
469
|
id: "plugin-config-icon-asking-hold",
|
|
424
470
|
label: t("askingHoldMs"),
|
|
425
471
|
hint: t("askingHoldMsHint"),
|
|
426
472
|
invalidLabel: t("invalidNumber"),
|
|
427
473
|
invalid: failed === "invalidNumber",
|
|
428
474
|
disabled: disabled,
|
|
475
|
+
numeric: true,
|
|
429
476
|
separated: true,
|
|
430
477
|
value: fieldValue("askingHoldMs"),
|
|
431
478
|
onChange: function (text) { edit("askingHoldMs", text); },
|
|
432
479
|
}),
|
|
433
|
-
createElement(
|
|
480
|
+
createElement(ValueInput, {
|
|
434
481
|
id: "plugin-config-icon-done-hold",
|
|
435
482
|
label: t("doneHoldMs"),
|
|
436
483
|
hint: t("doneHoldMsHint"),
|
|
437
484
|
invalidLabel: t("invalidNumber"),
|
|
438
485
|
invalid: failed === "invalidNumber",
|
|
439
486
|
disabled: disabled,
|
|
487
|
+
numeric: true,
|
|
440
488
|
separated: true,
|
|
441
489
|
value: fieldValue("doneHoldMs"),
|
|
442
490
|
onChange: function (text) { edit("doneHoldMs", text); },
|
|
443
491
|
}),
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
createElement(
|
|
450
|
-
|
|
451
|
-
{
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
|
|
492
|
+
createElement(
|
|
493
|
+
"div",
|
|
494
|
+
{ style: styles.statesBlock },
|
|
495
|
+
STATE_NAMES.map(function (name) {
|
|
496
|
+
var isOpen = openName === name;
|
|
497
|
+
return createElement(
|
|
498
|
+
primitives.DisclosureRow,
|
|
499
|
+
{
|
|
500
|
+
key: name,
|
|
501
|
+
className: "dsh-wii-state",
|
|
502
|
+
icon: createElement(ColorDot, { color: statePrimaryColor(name) }),
|
|
503
|
+
title: t(stateTitleKey(name)),
|
|
504
|
+
open: isOpen,
|
|
505
|
+
expandable: true,
|
|
506
|
+
expandOnRowClick: true,
|
|
507
|
+
onToggle: function () { setOpenName(isOpen ? null : name); },
|
|
508
|
+
collapsedContent: createElement("span", { style: styles.summary }, stateSummary(name)),
|
|
509
|
+
},
|
|
510
|
+
createElement(
|
|
511
|
+
"div",
|
|
512
|
+
{ style: styles.statePanel },
|
|
513
|
+
createElement(
|
|
514
|
+
LabeledField,
|
|
515
|
+
{ label: t("effect"), hint: t("effectHint"), invalid: false, invalidLabel: "", separated: false },
|
|
516
|
+
createElement(SelectInput, {
|
|
517
|
+
id: "plugin-config-icon-" + name + "-effect",
|
|
518
|
+
options: EFFECT_NAMES,
|
|
519
|
+
disabled: disabled,
|
|
520
|
+
value: stateFieldValue(name, "effect"),
|
|
521
|
+
onChange: function (text) { edit("states." + name + ".effect", text); },
|
|
522
|
+
})
|
|
523
|
+
),
|
|
524
|
+
createElement(ValueInput, {
|
|
525
|
+
id: "plugin-config-icon-" + name + "-colors",
|
|
526
|
+
label: t("colors"),
|
|
527
|
+
hint: t("colorsHint"),
|
|
528
|
+
invalidLabel: t("invalidColors"),
|
|
529
|
+
invalid: failed === "invalidColors",
|
|
530
|
+
disabled: disabled,
|
|
531
|
+
separated: true,
|
|
532
|
+
value: stateFieldValue(name, "colors"),
|
|
533
|
+
trailing: createElement(ColorSwatches, { text: stateFieldValue(name, "colors") }),
|
|
534
|
+
onChange: function (text) { edit("states." + name + ".colors", text); },
|
|
535
|
+
}),
|
|
536
|
+
createElement(ValueInput, {
|
|
537
|
+
id: "plugin-config-icon-" + name + "-speed",
|
|
538
|
+
label: t("speed"),
|
|
539
|
+
hint: t("speedHint"),
|
|
540
|
+
invalidLabel: t("invalidNumber"),
|
|
541
|
+
invalid: failed === "invalidNumber",
|
|
542
|
+
disabled: disabled,
|
|
543
|
+
numeric: true,
|
|
544
|
+
separated: true,
|
|
545
|
+
value: stateFieldValue(name, "speed"),
|
|
546
|
+
onChange: function (text) { edit("states." + name + ".speed", text); },
|
|
547
|
+
})
|
|
548
|
+
)
|
|
549
|
+
);
|
|
550
|
+
})
|
|
551
|
+
),
|
|
483
552
|
createElement(
|
|
484
553
|
"div",
|
|
485
554
|
{ style: styles.footer },
|
package/lib/index.js
CHANGED
|
@@ -131,6 +131,7 @@ const INJECTED_SCRIPT = `
|
|
|
131
131
|
var STATUS_PATH = "__STATUS_PATH__";
|
|
132
132
|
var BASE_PATH = "__BASE_PATH__";
|
|
133
133
|
var CFG = __CFG__; // { states: { idle:{effect,colors,speed}, running:…, asking:…, done:… } }
|
|
134
|
+
var CFG_JSON = null; // serialized states last seen; null = not synced yet
|
|
134
135
|
var BASE = null; // base.svg text; __COLOR__ replaced per frame
|
|
135
136
|
var RE = /__COLOR__/g;
|
|
136
137
|
var DEF_SPEED = 1200; // per-state cycle fallback when speed is omitted
|
|
@@ -291,10 +292,24 @@ const INJECTED_SCRIPT = `
|
|
|
291
292
|
};
|
|
292
293
|
RAF = requestAnimationFrame(step);
|
|
293
294
|
}
|
|
295
|
+
// Live config sync: the status response carries the current per-state
|
|
296
|
+
// visual config (the "states" field). When it differs from what this tab
|
|
297
|
+
// renders, swap it in and reset the animation phase so the next apply()
|
|
298
|
+
// repaints from scratch — a settings-card save takes effect within one
|
|
299
|
+
// poll tick, no tab reload needed. A response without "states" (older
|
|
300
|
+
// host) leaves the baked __CFG__ in place.
|
|
301
|
+
function syncCfg(j) {
|
|
302
|
+
if (!j || typeof j.states !== "object" || j.states === null) return;
|
|
303
|
+
var next = JSON.stringify(j.states);
|
|
304
|
+
if (next === CFG_JSON) return;
|
|
305
|
+
CFG_JSON = next;
|
|
306
|
+
CFG = { states: j.states };
|
|
307
|
+
PREV_STATE = null; // force a full repaint on the next apply
|
|
308
|
+
}
|
|
294
309
|
function poll() {
|
|
295
310
|
fetch(location.origin + STATUS_PATH, { cache: "no-store" })
|
|
296
311
|
.then(function (r) { if (!r.ok) throw new Error("bad"); return r.json(); })
|
|
297
|
-
.then(function (j) { apply(j.state || "idle"); })
|
|
312
|
+
.then(function (j) { syncCfg(j); apply(j.state || "idle"); })
|
|
298
313
|
// Transient failures (host restarting, network blip) must NOT kill the
|
|
299
314
|
// poll: restore the original icon and retry on the next tick. The SPA
|
|
300
315
|
// reconnects in place, so the tab survives a host restart — the icon
|
|
@@ -572,6 +587,10 @@ export default {
|
|
|
572
587
|
};
|
|
573
588
|
|
|
574
589
|
// -- Routes --------------------------------------------------------------
|
|
590
|
+
// The status endpoint also echoes the current per-state visual config
|
|
591
|
+
// (`states`): the injected script syncs it on every poll, so a settings
|
|
592
|
+
// save changes the favicon within ~1 s without a tab reload. `no-store`
|
|
593
|
+
// keeps every poll reading the freshest config.
|
|
575
594
|
ctx.effect(() => webServer.register({
|
|
576
595
|
kind: "exact",
|
|
577
596
|
path: cfg.statusPath,
|
|
@@ -579,7 +598,7 @@ export default {
|
|
|
579
598
|
res.statusCode = 200;
|
|
580
599
|
res.setHeader("Content-Type", "application/json; charset=utf-8");
|
|
581
600
|
res.setHeader("Cache-Control", "no-store");
|
|
582
|
-
res.end(JSON.stringify(aggregate()));
|
|
601
|
+
res.end(JSON.stringify({ ...aggregate(), states: cfg.states }));
|
|
583
602
|
},
|
|
584
603
|
}));
|
|
585
604
|
|
package/lib/types/index.d.ts
CHANGED
|
@@ -77,6 +77,12 @@ export interface DshWebIconIndicatorAggregate {
|
|
|
77
77
|
state: DshWebIconStateName;
|
|
78
78
|
/** Epoch millis when that aggregate state was first entered. */
|
|
79
79
|
since: number;
|
|
80
|
+
/**
|
|
81
|
+
* Current per-state visual config, echoed by the status endpoint so the
|
|
82
|
+
* injected browser script can apply a settings save within ~1 s (no tab
|
|
83
|
+
* reload). Added in 0.2.x; older browser bundles ignore it.
|
|
84
|
+
*/
|
|
85
|
+
states: Partial<Record<DshWebIconStateName, DshWebIconStateConfig>>;
|
|
80
86
|
}
|
|
81
87
|
|
|
82
88
|
/**
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "dsh-web-icon-indicator",
|
|
3
3
|
"description": "Browser tab favicon reflects the DSH session state: idle / running / asking / done, recolored and animated from one base SVG in the browser (fully configurable colors + effects). · 浏览器标签页 favicon 实时反映 DSH 会话状态:待机 / 运行中 / 提问 / 完成,由单个 base.svg 在浏览器中动态上色与动画(颜色与特效完全可配置)。",
|
|
4
|
-
"version": "0.2.
|
|
4
|
+
"version": "0.2.1",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "lib/index.js",
|
|
7
7
|
"types": "lib/types/index.d.ts",
|
|
@@ -24,7 +24,8 @@
|
|
|
24
24
|
"@deepseek-ai/dsh-client-runtime",
|
|
25
25
|
"@deepseek-ai/dsh-client-locale",
|
|
26
26
|
"@deepseek-ai/dsh-client-ui-slots",
|
|
27
|
-
"@deepseek-ai/dsh-client-ui-settings"
|
|
27
|
+
"@deepseek-ai/dsh-client-ui-settings",
|
|
28
|
+
"@deepseek-ai/dsh-client-ui-settings-plugins"
|
|
28
29
|
]
|
|
29
30
|
}
|
|
30
31
|
},
|