1 Commits
Author SHA1 Message Date
mward4 1500f44bed v0.1.3: support the DSH 0.2.0 running indicator, keep 0.1.7/legacy working
DSH 0.2.0-rc.1 moved the live running label out of button[data-turn-process]
(which now carries only completed text) and into a new div[data-chat-running]
indicator rendered through TextShimmer.

- Discover the label host newest-layout-first: [data-chat-running], then
  [data-turn-process], then [role="status"] as a legacy-only fallback.
- Rewrite every matching text node in a host, not just the first: TextShimmer
  renders the label twice (visible layer + aria-hidden decorative overlay), and
  leaving one behind paints the stock wording back on top of the phrase.
- Skip nested live regions while scanning, so the screen-reader announcement
  survives now that it sits inside the label host.
- Extend hostOf() to the new hook so the per-second rewrite is still restored
  synchronously, before paint.

Tests cover all three layouts, including 0.2.0 precedence over a finished
process row and both shimmer layers staying in sync.
2026-09-29 00:30:22 -04:00
4 changed files with 226 additions and 98 deletions

No files matched your search

+23 -13
View File
@@ -13,16 +13,19 @@ After: Nuking the database... 12s
## Compatibility
The live duration DSH appends to the label changes every second, and DSH moved the label between releases. Both layouts are handled:
The live duration DSH appends to the label changes every second, and DSH has moved the label twice. All three layouts are handled, newest hook first:
| DSH version | Where the visible label lives | Path |
| ----------------- | ------------------------------------------------------ | --------- |
| `>= 0.1.7-alpha.2` | `button[data-turn-process]` label span | primary |
| `<= 0.1.5-rc.2` | the visible `[role="status"]` text node | legacy |
| DSH version | Where the visible label lives | Path |
| ------------------ | ---------------------------------------------- | --------- |
| `>= 0.2.0-rc.1` | `div[data-chat-running]` → `TextShimmer` label | primary |
| `>= 0.1.7-alpha.2` | `button[data-turn-process]` label span | fallback |
| `<= 0.1.5-rc.2` | the visible `[role="status"]` text node | legacy |
Labels are matched by prefix in both shipped locales — `Deep diving…` / `Deep diving for 12s` and `深度求索中…` / `深度求索中,用时12s` — so future rewording of the duration text does not break it. Completed labels (`Worked`, `Took 12s`, `Stopped`, `Failed`) are never touched: the plugin releases the label the moment the turn ends.
Labels are matched by prefix in both shipped locales — `Deep diving…` / `Deep diving for 12s…` and `深度求索中…` / `深度求索中,用时 12s…` — so future rewording of the duration text does not break it. Completed labels (`Worked`, `Took 12s`, `Stopped`, `Failed`) are never touched: the plugin releases the label the moment the turn ends.
On `>= 0.1.7` the only remaining `[role="status"]` is a **visually hidden screen-reader announcement**. It is deliberately left intact, so assistive tech still hears the real status while sighted users get the joke.
On `>= 0.1.7` the only remaining `[role="status"]` is a **visually hidden screen-reader announcement**. It is deliberately left intact, so assistive tech still hears the real status while sighted users get the joke. On `>= 0.2.0` that announcement sits *inside* the running indicator, so the plugin skips nested live regions rather than assuming the host element is the label.
On `>= 0.2.0` the label is rendered through `TextShimmer`, which duplicates the text into an `aria-hidden` decorative overlay. Both copies are rewritten together — otherwise the overlay paints the stock wording back on top of the rotating phrase. The collapsible process row now carries only completed text, so a finished row from an earlier turn is left alone while the current turn runs.
## Install
@@ -230,7 +233,7 @@ A DSH plugin bundle is three small pieces:
| File | Role |
| ------------------ | ----------------------------------------------------------------------- |
| `index.js` | Host-side half. Deliberately a no-op — the work happens in the browser. |
| `client.js` | Browser-half bundle: finds the status label and rewrites its text node. |
| `client.js` | Browser-half bundle: finds the status label and rewrites its text node(s). |
| `cordis.patch.yml` | Inserts the `based-dsh-diving` row into the `web` profile's plugin tree. |
`client.js` locates the label, claims its text node, and then:
@@ -254,14 +257,21 @@ No build step, no runtime dependencies.
node test/client.test.mjs
```
The suite runs `client.js` against a minimal DOM stub and covers twelve checks:
new-layout claim, screen-reader announcement preserved, live-duration rewrite
re-asserted, release on turn end, staying released, legacy-layout claim, clock
preserved, timer rotation, localized labels, clean disposal, and phrase-list
integrity.
The suite runs `client.js` against a minimal DOM stub and covers 22 checks across all three layouts: `0.2.0` claim with both shimmer layers kept in sync, screen-reader announcement preserved, live-duration rewrite re-asserted, precedence over a finished process row, release on turn end, staying released, `0.1.7` claim, legacy-layout claim, clock preserved, timer rotation, localized labels, clean disposal, and phrase-list integrity.
## Changelog
### 0.1.3
- Support DSH `>= 0.2.0-rc.1`, which moved the running label out of
`button[data-turn-process]` and into a new `div[data-chat-running]` indicator.
- Backward compatible: the `0.1.7` and `<= 0.1.5` layouts still work, tried in
that order after the new hook.
- Keep `TextShimmer`'s visible and `aria-hidden` label layers in sync, so the
decorative overlay cannot paint the stock wording back on top.
- Skip nested live regions while scanning, keeping the screen-reader
announcement intact even though it now sits inside the label host.
- Leave finished `[data-turn-process]` rows alone while a turn is running.
### 0.1.2
- Support the DSH `>= 0.1.7-alpha.2` label, which moved out of `[role="status"]`
and into `button[data-turn-process]`.
+82 -57
View File
@@ -166,17 +166,22 @@ window.__ModuleLoader__.load({
var ROTATE_MS = 2200;
var SCAN_DEBOUNCE_MS = 120;
// The stock running-turn label, across locales and both DOM layouts:
// The stock running-turn label, across locales and every DOM layout shipped
// so far:
//
// DSH <= 0.1.5 "Deep diving..." / "深度求索中..."
// DSH >= 0.1.7 "Deep diving for 12s" / "深度求索中,用时12s"
// DSH <= 0.1.5 "Deep diving..." / "深度求索中..."
// DSH 0.1.7 "Deep diving for 12s" / "深度求索中,用时12s"
// DSH >= 0.2.0 "Deep diving for 12s..." / "深度求索中,用时 12s..."
//
// Completed labels ("Worked", "Took 12s", "Stopped", "Failed",
// "已完成工作", "用时 12s", "处理失败") never begin with these prefixes, so a
// prefix match is enough and survives future rewording.
var RUNNING_LABEL = /^\s*(?:deep\s*diving|深度求索中)/i;
// host element -> { node, text } for every label we currently own.
// host element -> { nodes, text } for every label we currently own. A host
// can carry more than one copy of the label: TextShimmer renders the text in
// a visible layer plus an aria-hidden overlay, and both must move together or
// the overlay paints the stock wording back on top of the rotating phrase.
var owned = new WeakMap();
var lastIndex = -1;
@@ -195,42 +200,54 @@ window.__ModuleLoader__.load({
}
/**
* Elements that may host the visible running-turn label.
* Elements that may host the visible running-turn label, newest layout first.
*
* DSH >= 0.1.7 renders the label inside a <button data-turn-process>; its
* only remaining [role="status"] is a visually hidden screen-reader
* announcement, which we deliberately leave alone. Older releases rendered
* the label as the visible [role="status"] text itself.
* [data-chat-running] DSH >= 0.2.0: the blue "running" indicator that
* carries the live "Deep diving for 12s..." label.
* [data-turn-process] DSH 0.1.7: the collapsible process row, which
* then carried the running label.
* [role="status"] DSH <= 0.1.5: the label *was* the status text.
*
* Later releases leave [role="status"] to a visually hidden screen-reader
* announcement, so it is only ever used as a last-resort fallback, and never
* when a newer hook is present.
*/
function labelHosts() {
var hosts = [];
var buttons = document.querySelectorAll("[data-turn-process]");
if (buttons.length > 0) {
for (var i = 0; i < buttons.length; i += 1) hosts.push(buttons[i]);
return hosts;
var layouts = ["[data-chat-running]", "[data-turn-process]"];
for (var i = 0; i < layouts.length; i += 1) {
var found = document.querySelectorAll(layouts[i]);
if (found.length > 0) return found;
}
var statuses = document.querySelectorAll('[role="status"]');
for (var j = 0; j < statuses.length; j += 1) hosts.push(statuses[j]);
return hosts;
return document.querySelectorAll('[role="status"]');
}
/** First text descendant still carrying the stock running label. */
function findLabelNode(host) {
var record = owned.get(host);
if (record && isLive(record.node) && host.contains(record.node)) return record.node;
/** Live-region element: DSH's screen-reader announcement, never rewritten. */
function isAnnouncement(element) {
return typeof element.getAttribute === "function" &&
element.getAttribute("role") === "status";
}
/**
* Every text descendant still carrying the stock running label.
*
* Returns a list rather than one node because TextShimmer duplicates the
* label into a decorative overlay; skipping nested live regions keeps the
* visually hidden announcement out of it.
*/
function findLabelNodes(host) {
var nodes = [];
var stack = [host];
while (stack.length > 0) {
var node = stack.pop();
for (var child = node.firstChild; child; child = child.nextSibling) {
if (child.nodeType === 3) {
if (RUNNING_LABEL.test(child.nodeValue || "")) return child;
if (RUNNING_LABEL.test(child.nodeValue || "")) nodes.push(child);
} else if (child.nodeType === 1) {
stack.push(child);
if (!isAnnouncement(child)) stack.push(child);
}
}
}
return null;
return nodes;
}
/**
@@ -242,30 +259,35 @@ window.__ModuleLoader__.load({
var record = owned.get(host);
if (!record) {
var fresh = findLabelNode(host);
if (!fresh) return;
var text = nextPhrase();
fresh.nodeValue = text;
owned.set(host, { node: fresh, text: text });
return;
}
if (!isLive(record.node) || !host.contains(record.node)) {
if (record) {
var alive = [];
for (var i = 0; i < record.nodes.length; i += 1) {
if (isLive(record.nodes[i]) && host.contains(record.nodes[i])) alive.push(record.nodes[i]);
}
if (alive.length > 0) {
record.nodes = alive;
for (var j = 0; j < alive.length; j += 1) {
var current = alive[j].nodeValue || "";
if (current === record.text) continue;
if (RUNNING_LABEL.test(current)) {
alive[j].nodeValue = record.text;
continue;
}
// The label changed meaning (the turn finished): hands off for good.
// Staying claimed would fight the final "Worked for 12s" copy.
owned.delete(host);
return;
}
return;
}
owned.delete(host);
claim(host);
return;
}
var current = record.node.nodeValue || "";
if (current === record.text) return;
if (RUNNING_LABEL.test(current)) {
record.node.nodeValue = record.text;
return;
}
// The turn finished (or the label changed meaning): hands off for good.
// Leaving this claimed would fight the final "Worked for 12s" text.
owned.delete(host);
var fresh = findLabelNodes(host);
if (fresh.length === 0) return;
var text = nextPhrase();
for (var k = 0; k < fresh.length; k += 1) fresh[k].nodeValue = text;
owned.set(host, { nodes: fresh, text: text });
}
function scan() {
@@ -285,27 +307,29 @@ window.__ModuleLoader__.load({
claim(host);
continue;
}
if (!isLive(record.node) || !host.contains(record.node)) {
var stale = false;
for (var j = 0; j < record.nodes.length; j += 1) {
var node = record.nodes[j];
if (!isLive(node) || !host.contains(node)) { stale = true; break; }
var current = node.nodeValue || "";
if (current !== record.text && !RUNNING_LABEL.test(current)) { stale = true; break; }
}
if (stale) {
owned.delete(host);
claim(host);
continue;
}
var current = record.node.nodeValue || "";
if (current !== record.text && !RUNNING_LABEL.test(current)) {
owned.delete(host);
continue;
}
var text = nextPhrase();
record.text = text;
record.node.nodeValue = text;
for (var k = 0; k < record.nodes.length; k += 1) record.nodes[k].nodeValue = text;
}
}
/** Nearest turn-process button above a mutated text node, if any. */
/** Nearest running-status host above a mutated text node, if any. */
function hostOf(node) {
var element = node && node.parentElement;
if (!element || typeof element.closest !== "function") return null;
return element.closest("[data-turn-process]");
return element.closest("[data-chat-running], [data-turn-process]");
}
function apply(ctx) {
@@ -337,9 +361,10 @@ window.__ModuleLoader__.load({
structural = true;
continue;
}
// Fast path: DSH >= 0.1.7 rewrites the live duration every
// second. Observer callbacks run before paint, so restoring here
// keeps the stock label from flashing through.
// Fast path: DSH rewrites the live duration every second (in
// 0.2.0 that is two shimmer layers at once). Observer callbacks
// run before paint, so restoring here keeps the stock label from
// flashing through.
var host = hostOf(record.target);
if (host) claim(host);
else structural = true;
+1 -1
View File
@@ -1,7 +1,7 @@
{
"name": "@based-dsh-diving/dsh-plugin",
"description": "Replace DeepSeek Harness's running-turn status label with rotating, chronically online loading text.",
"version": "0.1.2",
"version": "0.1.3",
"type": "module",
"main": "index.js",
"exports": {
+120 -27
View File
@@ -1,6 +1,9 @@
// Headless harness for based-dsh-diving/client.js.
//
// Stubs just enough DOM to exercise both supported layouts:
// Stubs just enough DOM to exercise all three supported layouts:
// - DSH >= 0.2.0: <div data-chat-running> holding a hidden [role="status"]
// announcement plus a TextShimmer whose label is rendered
// twice (visible layer + aria-hidden decorative overlay)
// - DSH >= 0.1.7: <button data-turn-process> > <span>label</span>
// plus a visually hidden [role="status"] announcement
// - DSH <= 0.1.5: the visible label is the [role="status"] text itself
@@ -51,6 +54,7 @@ class El {
get isConnected() { return this.rooted || isConnected(this); }
get firstChild() { return this.childNodes[0] ?? null; }
get firstElementChild() { return this.childNodes.find((n) => n.nodeType === 1) ?? null; }
getAttribute(name) { return this.attrs[name] === undefined ? null : String(this.attrs[name]); }
append(...nodes) {
for (const node of nodes) { node.parent = this; this.childNodes.push(node); }
this.childNodes.forEach((n, i) => { n.nextSibling = this.childNodes[i + 1] ?? null; });
@@ -65,9 +69,13 @@ class El {
function matches(el, selector) {
if (el.nodeType !== 1) return false;
if (selector === "[data-turn-process]") return el.attrs["data-turn-process"] !== undefined;
if (selector === '[role="status"]') return el.attrs.role === "status";
return false;
return selector.split(",").some((raw) => {
const part = raw.trim();
if (part === "[data-turn-process]") return el.attrs["data-turn-process"] !== undefined;
if (part === "[data-chat-running]") return el.attrs["data-chat-running"] !== undefined;
if (part === '[role="status"]') return el.attrs.role === "status";
return false;
});
}
function allElements(root) {
@@ -105,14 +113,107 @@ const start = () => {
mod.apply({ effect: (fn) => { dispose = fn(); } });
return { dispose };
};
const isJoke = (value) => /\.\.\.$/.test(value);
// "Deep diving" is itself one of the phrases, so compare against the stock text
// this layout shipped rather than pattern-matching the joke wording.
const isJoke = (value, stock) => /\.\.\.$/.test(value) && value !== stock;
const fire = (...nodes) => observerCallback?.(nodes.map((target) => ({ type: "characterData", target })));
// ------------------------------------------------------- new layout (>= 0.1.7)
/** Build DSH >= 0.2.0's RunningStatus subtree and return its label text nodes. */
function buildRunningStatus(root, labelText) {
const running = new El("div", { "data-chat-running": "true", class: "running" });
const hidden = new El("span", { role: "status", "aria-live": "polite", "aria-atomic": "true" });
const announcement = new TextNode("Deep diving...");
hidden.append(announcement);
const divider = new El("span", { "aria-hidden": "true" });
const content = new El("span", { class: "runningContent" });
const shimmer = new El("span", { "data-shimmer": "true" });
const visibleLayer = new El("span", { class: "content" });
const visible = new TextNode(labelText);
visibleLayer.append(visible);
const decoration = new El("span", { "aria-hidden": "true" });
const sweep = new El("span", { class: "sweep" });
const highlightLayer = new El("span", { class: "content highlight" });
const highlight = new TextNode(labelText);
highlightLayer.append(highlight);
sweep.append(highlightLayer);
decoration.append(sweep);
shimmer.append(visibleLayer, decoration);
content.append(shimmer);
running.append(hidden, divider, content);
root.append(running);
return { running, visible, highlight, announcement };
}
// -------------------------------------------- current layout (DSH >= 0.2.0)
{
const root = useDom();
const { visible, highlight, announcement } = buildRunningStatus(root, "Deep diving for 1s...");
const session = start();
const owned = visible.nodeValue;
ok("0.2.0: visible shimmer layer claimed", isJoke(owned, "Deep diving for 1s..."), JSON.stringify(owned));
ok("0.2.0: decorative overlay stays in sync", highlight.nodeValue === owned, JSON.stringify(highlight.nodeValue));
ok("0.2.0: screen-reader announcement untouched", announcement.nodeValue === "Deep diving...", JSON.stringify(announcement.nodeValue));
// React rewrites the live duration every second, in both layers at once.
visible.nodeValue = "Deep diving for 2s...";
highlight.nodeValue = "Deep diving for 2s...";
fire(visible, highlight);
ok("0.2.0: live-duration rewrite re-asserted (visible)", visible.nodeValue === owned, JSON.stringify(visible.nodeValue));
ok("0.2.0: live-duration rewrite re-asserted (overlay)", highlight.nodeValue === owned, JSON.stringify(highlight.nodeValue));
// Turn ends: the plugin must release, not fight the final text.
visible.nodeValue = "Worked for 12s";
highlight.nodeValue = "Worked for 12s";
fire(visible, highlight);
ok("0.2.0: released on turn end", visible.nodeValue === "Worked for 12s", JSON.stringify(visible.nodeValue));
const before = visible.nodeValue;
await wait(2400);
ok("0.2.0: stays released after a rotation tick", visible.nodeValue === before, JSON.stringify(visible.nodeValue));
session.dispose();
}
// ------------------------------- precedence: a stale completed process row
// On 0.2.0 the collapsible process row still exists for finished turns, so the
// running indicator must win and the finished row must be left alone.
{
const root = useDom();
const done = new El("button", { "data-turn-process": "t0" });
const doneLabel = new TextNode("Worked for 40s");
const doneSpan = new El("span", { class: "label" });
doneSpan.append(doneLabel);
done.append(doneSpan);
root.append(done);
const { visible } = buildRunningStatus(root, "Deep diving for 1s...");
const session = start();
ok("0.2.0: running indicator wins over finished row", isJoke(visible.nodeValue, "Deep diving for 1s..."), JSON.stringify(visible.nodeValue));
ok("0.2.0: finished process row untouched", doneLabel.nodeValue === "Worked for 40s", JSON.stringify(doneLabel.nodeValue));
// A finished row that React later rewrites must not be claimed either.
doneLabel.nodeValue = "Took 41s";
fire(doneLabel);
ok("0.2.0: finished row never claimed", doneLabel.nodeValue === "Took 41s", JSON.stringify(doneLabel.nodeValue));
session.dispose();
}
// ----------------------------------------------------------------- 0.1.7 row
{
const root = useDom();
const button = new El("button", { "data-turn-process": "t1" });
const labelSpan = new El("span");
const labelSpan = new El("span", { class: "label" });
const label = new TextNode("Deep diving for 1s");
labelSpan.append(label);
button.append(labelSpan);
@@ -124,23 +225,20 @@ const isJoke = (value) => /\.\.\.$/.test(value);
const session = start();
const owned = label.nodeValue;
ok("new layout: label claimed", isJoke(owned), JSON.stringify(owned));
ok("new layout: screen-reader announcement untouched", announcement.nodeValue === "Deep diving...", JSON.stringify(announcement.nodeValue));
ok("0.1.7: label claimed", isJoke(owned, "Deep diving for 1s"), JSON.stringify(owned));
ok("0.1.7: screen-reader announcement untouched", announcement.nodeValue === "Deep diving...", JSON.stringify(announcement.nodeValue));
// React rewrites the live duration every second; the observer must restore
// synchronously, before paint.
label.nodeValue = "Deep diving for 2s";
observerCallback?.([{ type: "characterData", target: label }]);
ok("new layout: live-duration rewrite re-asserted", label.nodeValue === owned, JSON.stringify(label.nodeValue));
fire(label);
ok("0.1.7: live-duration rewrite re-asserted", label.nodeValue === owned, JSON.stringify(label.nodeValue));
// Turn ends: the plugin must release the label, not fight the final text.
label.nodeValue = "Worked for 12s";
observerCallback?.([{ type: "characterData", target: label }]);
ok("new layout: released on turn end", label.nodeValue === "Worked for 12s", JSON.stringify(label.nodeValue));
fire(label);
ok("0.1.7: released on turn end", label.nodeValue === "Worked for 12s", JSON.stringify(label.nodeValue));
const before = label.nodeValue;
await wait(2400);
ok("new layout: stays released after a rotation tick", label.nodeValue === before, JSON.stringify(label.nodeValue));
ok("0.1.7: stays released after a rotation tick", label.nodeValue === before, JSON.stringify(label.nodeValue));
session.dispose();
}
@@ -160,7 +258,7 @@ const isJoke = (value) => /\.\.\.$/.test(value);
const session = start();
const owned = label.nodeValue;
ok("legacy layout: label claimed", isJoke(owned), JSON.stringify(owned));
ok("legacy layout: label claimed", isJoke(owned, "Deep diving..."), JSON.stringify(owned));
ok("legacy layout: clock untouched", clockText.nodeValue === "12s", JSON.stringify(clockText.nodeValue));
await wait(2400);
@@ -173,20 +271,15 @@ const isJoke = (value) => /\.\.\.$/.test(value);
{
const root = useDom();
const button = new El("button", { "data-turn-process": "t2" });
const span = new El("span");
const label = new TextNode("深度求索中,用时3s");
span.append(label);
button.append(span);
root.append(button);
const { visible } = buildRunningStatus(root, "深度求索中,用时 3s...");
const session = start();
ok("localized running label claimed", isJoke(label.nodeValue) && !/深度求索中/.test(label.nodeValue), JSON.stringify(label.nodeValue));
ok("localized running label claimed", isJoke(visible.nodeValue, "深度求索中,用时 3s...") && !/深度求索中/.test(visible.nodeValue), JSON.stringify(visible.nodeValue));
const frozen = label.nodeValue;
const frozen = visible.nodeValue;
session.dispose();
await wait(2400);
ok("disposer stops rotation", label.nodeValue === frozen, JSON.stringify(label.nodeValue));
ok("disposer stops rotation", visible.nodeValue === frozen, JSON.stringify(visible.nodeValue));
}
// ---------------------------------------------------------- static integrity