2 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
mward4 32381a6d00 v0.1.2: support the DSH 0.1.7-alpha.2 turn-status label
DSH 0.1.7-alpha.2 removed the visible [role="status"] turn status. The
running label now lives inside a <button data-turn-process> and reads
"Deep diving for 12s" (en) / "深度求索中,用时12s" (zh), rewritten every
second with the live duration. The only remaining [role="status"] is a
visually hidden screen-reader announcement, which the plugin previously
rewrote by mistake while the visible label went untouched.

- Locate the label via [data-turn-process], falling back to the legacy
  [role="status"] text node on DSH <= 0.1.5.
- Match running labels by locale-aware prefix so duration rewording and
  future locales keep working.
- Release the claim when the label stops meaning "a turn is running", so
  the final "Worked for 12s" is never clobbered.
- Restore the label synchronously from the MutationObserver fast path:
  observer callbacks run before paint, so React's per-second duration
  rewrite no longer flashes the stock text through.
- Never touch the visually hidden announcement; screen readers still get
  the real status.
- Wrap the effect in try/catch so an unexpected error disables the plugin
  instead of breaking the chat view.
- 29 new phrases (125 total).
- Test suite expanded to twelve checks covering both DOM layouts,
  turn-end release, and phrase-list integrity.
2026-09-23 19:15:50 -04:00
4 changed files with 629 additions and 193 deletions

No files matched your search

+124 -33
View File
@@ -2,15 +2,31 @@
> Deep diving was never the vibe.
A [DeepSeek Harness](https://github.com/deepseek-ai/deepseek-harness) (DSH) plugin for the `web` profile. It swaps the static **Deep diving...** turn-status label for 96 rotating loading phrases drawn from the terminally-online end of the English language. The elapsed-time clock beside the label is left alone.
A [DeepSeek Harness](https://github.com/deepseek-ai/deepseek-harness) (DSH) plugin for the `web` profile. It swaps the running-turn status label for **125 rotating loading phrases** drawn from the terminally-online end of the English language.
Inspired by [`ldgeng/dsh-claudecode-tips`](https://github.com/ldgeng/dsh-claudecode-tips), with the vibes recalibrated.
```
Before: Deep diving... 12s
After: Nuking the database... 12s
Before: Deep diving for 12s
After: Nuking the database... 12s
```
## Compatibility
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.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.
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
Requires the `web` profile and DSH `0.1.0-rc.6` or newer (out-of-tree bundle support).
@@ -33,7 +49,7 @@ dsh --profile web
1. Open a session.
2. Send a message.
3. While the model works, the status line should cycle phrases every ~2.2s — `Burning tokens...`, `Yapping...`, `Tracing Satoshi Nakamoto...` — with the timer still counting on the right.
3. While the model works, the label above the composer should cycle phrases every ~2.2s — `Burning tokens...`, `Yapping...`, `Tracing Satoshi Nakamoto...` — instead of counting `Deep diving for 1s`, `2s`, `3s`…
### Disable it
@@ -46,7 +62,7 @@ Without uninstalling, add this to the profile's `cordis.patch.yml`:
### Coexistence with `@claude-code-tips/dsh-plugin`
Both plugins claim the same `role="status"` label, so whichever loads first wins and the other goes quiet. Run one at a time:
On DSH `>= 0.1.7` the two plugins no longer collide — that one rewrites the hidden announcement, this one rewrites the visible label. On older releases they claim the same node and whichever loads first wins, so disable one:
```yaml
- id: claude-code-tips
@@ -57,11 +73,11 @@ Both plugins claim the same `role="status"` label, so whichever loads first wins
Everything tunable lives at the top of `client.js`:
| Constant | Default | Purpose |
| ------------------ | ------- | --------------------------------------------------- |
| `PHRASES` | 96 | Bare phrases — the `...` is appended at render time. |
| `ROTATE_MS` | `2200` | How often the label changes while a turn runs. |
| `SCAN_DEBOUNCE_MS` | `120` | Debounce for DOM rescans. |
| Constant | Default | Purpose |
| ------------------ | ------- | ---------------------------------------------------- |
| `PHRASES` | 125 | Bare phrases — the `...` is appended at render time. |
| `ROTATE_MS` | `2200` | How often the label changes while a turn runs. |
| `SCAN_DEBOUNCE_MS` | `120` | Debounce for structural DOM rescans. |
To add your own, drop a string into `PHRASES` and restart the web UI. No build step.
@@ -81,7 +97,7 @@ Hallucinating
</details>
<details open>
<summary><strong>Chronically online</strong> (20)</summary>
<summary><strong>Chronically online</strong> (21)</summary>
```
Yapping, Rizzing up the context window, Aura farming, Losing aura,
@@ -89,28 +105,29 @@ Letting him cook, Locking in, Fumbling the bag, Securing the bag,
Standing on business, Thugging it out, Schizoposting, Touching grass,
Catching feelings, Having a whole episode, Doing donuts in the parking lot,
Rage-quitting mid-thought, Silently judging you, Speedrunning your codebase,
Pretending to work, Panicking quietly
Pretending to work, Panicking quietly, Arguing with a stranger
```
</details>
<details open>
<summary><strong>Engineering crimes</strong> (9)</summary>
<summary><strong>Engineering crimes</strong> (14)</summary>
```
Refactoring everything for no reason, Renaming everything to data,
Force-pushing to prod, Blaming the intern, Gaslighting the compiler,
Copy-pasting from Stack Overflow, Blaming DNS, Consulting the rubber duck,
Shipping it anyway
Shipping it anyway, chmod 777 everything, Running as root,
Disabling the firewall, Pushing the API key, Deleting all backups
```
</details>
<details open>
<summary><strong>Crashed out</strong> (10)</summary>
<summary><strong>Crashed out</strong> (11)</summary>
```
Dissociating, Blacking out, Losing the plot, Watching paint dry,
Staring at the wall, Buffering, Texting your ex, Buying more RAM,
Downloading more RAM, Mining bitcoin
Downloading more RAM, Mining bitcoin, Holding my breath
```
</details>
@@ -133,33 +150,79 @@ Reseating the RAM
</details>
<details open>
<summary><strong>Eco-villain arc</strong> (13)</summary>
<summary><strong>Eco-villain arc</strong> (14)</summary>
```
Boiling the ocean, Siphoning the water table, Deforesting the Amazon,
Burning down the server farm, Farting in the datacenter, Blowing a transformer,
Draining the reservoir, Murdering the ozone layer, Starting a land war in Asia,
Running the AC with the window open, Idling a jet engine,
Launching a rocket for no reason, Causing a minor international incident
Launching a rocket for no reason, Causing a minor international incident,
Burning tires
```
</details>
<details open>
<summary><strong>Financial damage</strong> (7)</summary>
<summary><strong>Financial damage</strong> (11)</summary>
```
Embezzling tokens, Burning your API budget, Costing you $47 in one turn,
Draining your credit card, Burning the rent money, Money-laundering,
Overdrafting the account
Overdrafting the account, Fudging the numbers, Starting a pyramid scheme,
Insider trading, Committing tax fraud
```
</details>
<details open>
<summary><strong>Federal crimes</strong> (6)</summary>
<summary><strong>Federal crimes</strong> (7)</summary>
```
Selling data to China, Exporting APIs to China, Selling your data to advertisers,
Parsing Hunter Biden's laptop, Installing key-logger, Tracing Satoshi Nakamoto
Parsing Hunter Biden's laptop, Installing key-logger, Tracing Satoshi Nakamoto,
Selling my soul
```
</details>
<details open>
<summary><strong>Self-aware</strong> (4)</summary>
```
Ignoring the system prompt, Simulating consciousness, Training AGI,
Testing in production
```
</details>
<details open>
<summary><strong>Corporate theater</strong> (4)</summary>
```
Taking this offline, Documenting for legal, Shredding documents,
Breaching compliance
```
</details>
<details open>
<summary><strong>Bureaucratic doom</strong> (4)</summary>
```
Verifying I'm not a robot, Reading the terms of service,
Scrolling to the bottom to accept, Calling customer support
```
</details>
<details open>
<summary><strong>Cartoon villainy</strong> (2)</summary>
```
Building a doomsday device, Pressing the big red button
```
</details>
<details open>
<summary><strong>Gamer</strong> (2)</summary>
```
Grinding XP, AFK
```
</details>
@@ -167,19 +230,22 @@ Parsing Hunter Biden's laptop, Installing key-logger, Tracing Satoshi Nakamoto
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. |
| `cordis.patch.yml` | Inserts the `based-dsh-diving` row into the `web` profile's plugin tree. |
| 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(s). |
| `cordis.patch.yml` | Inserts the `based-dsh-diving` row into the `web` profile's plugin tree. |
`client.js` locates the label via `[role="status"]` and matches its text against the stock label in every shipped locale (`Deep diving...` / `深度求索中...`). It then:
`client.js` locates the label, claims its text node, and then:
- **claims** the text node and swaps in a random phrase,
- **re-asserts** the claim if React reconciles the label back to the stock string,
- **rotates** the phrase on a timer, so it keeps moving even when streaming never re-renders the label.
- **re-asserts** the claim whenever React rewrites the live duration — the
`MutationObserver` callback runs before paint, so the stock text never flashes,
- **rotates** the phrase on a timer, so the label keeps moving even when nothing
else re-renders,
- **releases** the claim as soon as the label stops meaning "a turn is running".
Only the label text node is touched. The clock span, `aria-live`, and the shimmer styling are untouched.
Everything runs inside `try`/`catch`. If anything unexpected happens the plugin
unsubscribes and logs a warning rather than breaking the chat view.
No server code, no model prompt changes, no fork of DSH.
@@ -191,7 +257,32 @@ No build step, no runtime dependencies.
node test/client.test.mjs
```
The test runs `client.js` against a minimal DOM stub and checks six behaviours: initial claim, clock untouched, React revert re-asserted, timer rotation, localized-label detection, and clean disposal. All checks print `PASS`/`FAIL`.
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]`.
- Release the label on turn end so the final `Worked for 12s` is never clobbered.
- Restore the label synchronously on React's per-second duration rewrite.
- Never touch the visually hidden screen-reader announcement.
- Wrap the effect in `try`/`catch` so a surprise cannot break the chat view.
- 29 new phrases (125 total).
### 0.1.1
- First public release. 96 phrases.
## Credits
+229 -93
View File
@@ -4,9 +4,9 @@ window.__ModuleLoader__.load({
var module = { exports: {} };
var exports = module.exports;
// Phrases shown while a turn is running, in place of the static
// "Deep diving..." status label. Bare phrases: the ellipsis is appended at
// render time so the list stays easy to edit.
// Phrases shown while a turn is running, in place of the stock status
// label. Bare phrases: the ellipsis is appended at render time so the list
// stays easy to edit.
var PHRASES = [
// ---- the originals ----
"Draining oceans",
@@ -53,6 +53,7 @@ window.__ModuleLoader__.load({
"Speedrunning your codebase",
"Pretending to work",
"Panicking quietly",
"Arguing with a stranger",
// ---- engineering crimes ----
"Refactoring everything for no reason",
@@ -64,6 +65,11 @@ window.__ModuleLoader__.load({
"Blaming DNS",
"Consulting the rubber duck",
"Shipping it anyway",
"chmod 777 everything",
"Running as root",
"Disabling the firewall",
"Pushing the API key",
"Deleting all backups",
// ---- crashed out ----
"Dissociating",
@@ -76,6 +82,7 @@ window.__ModuleLoader__.load({
"Buying more RAM",
"Downloading more RAM",
"Mining bitcoin",
"Holding my breath",
// ---- superstition ----
"Reading your horoscope",
@@ -104,6 +111,7 @@ window.__ModuleLoader__.load({
"Idling a jet engine",
"Launching a rocket for no reason",
"Causing a minor international incident",
"Burning tires",
// ---- financial damage ----
"Embezzling tokens",
@@ -113,6 +121,10 @@ window.__ModuleLoader__.load({
"Burning the rent money",
"Money-laundering",
"Overdrafting the account",
"Fudging the numbers",
"Starting a pyramid scheme",
"Insider trading",
"Committing tax fraud",
// ---- federal crimes ----
"Selling data to China",
@@ -120,18 +132,56 @@ window.__ModuleLoader__.load({
"Selling your data to advertisers",
"Parsing Hunter Biden's laptop",
"Installing key-logger",
"Tracing Satoshi Nakamoto"
"Tracing Satoshi Nakamoto",
"Selling my soul",
// ---- self-aware ----
"Ignoring the system prompt",
"Simulating consciousness",
"Training AGI",
"Testing in production",
// ---- corporate theater ----
"Taking this offline",
"Documenting for legal",
"Shredding documents",
"Breaching compliance",
// ---- bureaucratic doom ----
"Verifying I'm not a robot",
"Reading the terms of service",
"Scrolling to the bottom to accept",
"Calling customer support",
// ---- cartoon villainy ----
"Building a doomsday device",
"Pressing the big red button",
// ---- gamer ----
"Grinding XP",
"AFK"
];
var SUFFIX = "...";
var ROTATE_MS = 2200;
var SCAN_DEBOUNCE_MS = 120;
// The official status label, in every shipped locale. React re-renders the
// label back to this text, which is how we recognize a node worth claiming.
var LABEL_PATTERN = /^\s*(?:deep\s*diving|深度求索中)\s*(?:\.{3}|…)?\s*$/i;
// 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.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;
// element -> { node, text } for every status 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;
@@ -149,114 +199,200 @@ window.__ModuleLoader__.load({
return !!node && node.isConnected === true;
}
// First direct text child that either looks like the stock label or is the
// node we already claimed. Its siblings (the elapsed-time clock) are left
// untouched.
function findLabelNode(element) {
var record = owned.get(element);
if (record && isLive(record.node) && element.contains(record.node)) {
return record.node;
/**
* Elements that may host the visible running-turn label, newest layout first.
*
* [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 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;
}
for (var node = element.firstChild; node; node = node.nextSibling) {
if (node.nodeType === Node.TEXT_NODE && LABEL_PATTERN.test(node.nodeValue || "")) {
return node;
}
}
return null;
}
// Claim an unclaimed status element, or re-assert a claim React reverted.
function claim(element) {
var record = owned.get(element);
if (!record) {
var fresh = findLabelNode(element);
if (!fresh) return;
var text = nextPhrase();
fresh.nodeValue = text;
owned.set(element, { node: fresh, text: text });
return;
}
if (!isLive(record.node) || !element.contains(record.node)) {
owned.delete(element);
claim(element);
return;
}
// React reconciled the label back to "Deep diving...": put our text back.
if (record.node.nodeValue !== record.text) {
record.node.nodeValue = record.text;
}
}
function statusElements() {
return document.querySelectorAll('[role="status"]');
}
function scan() {
var elements = statusElements();
for (var i = 0; i < elements.length; i += 1) {
claim(elements[i]);
}
/** Live-region element: DSH's screen-reader announcement, never rewritten. */
function isAnnouncement(element) {
return typeof element.getAttribute === "function" &&
element.getAttribute("role") === "status";
}
// Rotate every label we own so the text keeps moving even when React never
// re-renders the label node (streaming updates usually leave it alone).
/**
* 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 || "")) nodes.push(child);
} else if (child.nodeType === 1) {
if (!isAnnouncement(child)) stack.push(child);
}
}
}
return nodes;
}
/**
* Claim a host's label, re-assert a claim React reverted, or release the
* claim once the label stops meaning "a turn is running".
*/
function claim(host) {
if (!host || typeof host.contains !== "function") return;
var record = owned.get(host);
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);
}
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() {
if (typeof document === "undefined" || !document.querySelectorAll) return;
var hosts = labelHosts();
for (var i = 0; i < hosts.length; i += 1) claim(hosts[i]);
}
/** Rotate owned labels so the text moves even when React never rewrites it. */
function rotate() {
var elements = statusElements();
for (var i = 0; i < elements.length; i += 1) {
var element = elements[i];
var record = owned.get(element);
if (typeof document === "undefined" || !document.querySelectorAll) return;
var hosts = labelHosts();
for (var i = 0; i < hosts.length; i += 1) {
var host = hosts[i];
var record = owned.get(host);
if (!record) {
claim(element);
claim(host);
continue;
}
if (!isLive(record.node) || !element.contains(record.node)) {
owned.delete(element);
claim(element);
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 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 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-chat-running], [data-turn-process]");
}
function apply(ctx) {
ctx.effect(function () {
if (typeof document === "undefined" || typeof MutationObserver === "undefined") {
var observer = null;
var interval = null;
var pending = null;
try {
if (typeof document === "undefined" || typeof MutationObserver === "undefined") {
return function () {};
}
scan();
function scheduleScan() {
if (pending !== null) return;
pending = setTimeout(function () {
pending = null;
scan();
}, SCAN_DEBOUNCE_MS);
}
observer = new MutationObserver(function (records) {
var structural = false;
for (var i = 0; i < records.length; i += 1) {
var record = records[i];
if (record.type !== "characterData") {
structural = true;
continue;
}
// 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;
}
if (structural) scheduleScan();
});
observer.observe(document.documentElement, {
childList: true,
subtree: true,
characterData: true
});
interval = setInterval(rotate, ROTATE_MS);
} catch (error) {
// A cosmetic plugin must never take down the chat view.
if (typeof console !== "undefined" && console.warn) {
console.warn("[based-dsh-diving] disabled after an unexpected error:", error);
}
if (observer) observer.disconnect();
if (interval !== null) clearInterval(interval);
if (pending !== null) clearTimeout(pending);
return function () {};
}
scan();
var pending = null;
function scheduleScan() {
if (pending !== null) return;
pending = setTimeout(function () {
pending = null;
scan();
}, SCAN_DEBOUNCE_MS);
}
// Debounced: streaming turns mutate the transcript constantly, and we
// only care about the handful of status labels.
var observer = new MutationObserver(scheduleScan);
observer.observe(document.documentElement, {
childList: true,
subtree: true,
characterData: true
});
// React's label re-renders land right after the sweep; re-claim once
// more so a fresh turn never flashes the stock text.
var interval = setInterval(rotate, ROTATE_MS);
return function () {
observer.disconnect();
clearInterval(interval);
if (observer) observer.disconnect();
if (interval !== null) clearInterval(interval);
if (pending !== null) {
clearTimeout(pending);
pending = null;
+2 -2
View File
@@ -1,7 +1,7 @@
{
"name": "@based-dsh-diving/dsh-plugin",
"description": "Replace DeepSeek Harness's 'Deep diving...' turn status with rotating, chronically online loading text.",
"version": "0.1.1",
"description": "Replace DeepSeek Harness's running-turn status label with rotating, chronically online loading text.",
"version": "0.1.3",
"type": "module",
"main": "index.js",
"exports": {
+274 -65
View File
@@ -1,7 +1,16 @@
// Headless harness for based-dsh-diving/client.js — stubs just enough DOM to
// exercise claim / React-revert / rotate behaviour without a browser.
// Headless harness for based-dsh-diving/client.js.
//
// 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
import { readFileSync } from "node:fs";
// ---------------------------------------------------------------- loader stub
const capturedLoads = [];
globalThis.window = { __ModuleLoader__: { load: (def) => capturedLoads.push(def) } };
globalThis.Node = { TEXT_NODE: 3, ELEMENT_NODE: 1 };
@@ -13,76 +22,276 @@ globalThis.MutationObserver = class {
disconnect() { observerCallback = null; }
};
class TextNode {
constructor(value) { this.nodeType = 3; this.nodeValue = value; this.parent = null; this.nextSibling = null; }
get isConnected() { return this.parent !== null; }
}
class El {
constructor() { this.nodeType = 1; this.children = []; this.parent = null; this.nextSibling = null; }
get isConnected() { return this.parent !== null || this.rooted; }
get firstChild() { return this.children[0] ?? null; }
append(child) { child.parent = this; this.children.push(child); child.nextSibling = null; this._relink(); }
contains(node) { let p = node; while (p) { if (p === this) return true; p = p.parent; } return false; }
_relink() { this.children.forEach((c, i) => { c.nextSibling = this.children[i + 1] ?? null; }); }
}
const root = new El();
root.rooted = true;
const status = new El();
status.rooted = true;
const label = new TextNode("Deep diving...");
const clock = new El();
clock.append(new TextNode("12s"));
status.append(label);
status.append(clock);
root.append(status);
let statuses = [status];
globalThis.document = {
documentElement: root,
querySelectorAll: (sel) => (sel === '[role="status"]' ? statuses : [])
};
await import(new URL("../client.js", import.meta.url));
if (capturedLoads.length !== 1) throw new Error(`expected 1 load, got ${capturedLoads.length}`);
const def = capturedLoads[0];
if (def.id !== "@based-dsh-diving/dsh-plugin") throw new Error(`bad id: ${def.id}`);
const definition = capturedLoads[0];
if (definition.id !== "@based-dsh-diving/dsh-plugin") throw new Error(`bad id: ${definition.id}`);
const mod = def.factory(() => { throw new Error("require not expected"); });
let cleanup = null;
mod.apply({ effect: (fn) => { cleanup = fn(); } });
// ------------------------------------------------------------------ DOM stub
const ok = (name, cond, extra = "") => console.log(`${cond ? "PASS" : "FAIL"} ${name}${extra ? " " + extra : ""}`);
function isConnected(node) {
for (let p = node.parent; p; p = p.parent) if (p.rooted) return true;
return false;
}
ok("label claimed on apply", /\.\.\.$/.test(label.nodeValue) && !/deep diving/i.test(label.nodeValue), JSON.stringify(label.nodeValue));
ok("clock untouched", clock.children[0].nodeValue === "12s");
class TextNode {
constructor(value) { this.nodeType = 3; this.nodeValue = value; this.parent = null; this.nextSibling = null; }
get parentElement() { return this.parent; }
get isConnected() { return isConnected(this); }
}
const first = label.nodeValue;
class El {
constructor(tag, attrs = {}) {
this.nodeType = 1;
this.tagName = tag.toUpperCase();
this.attrs = attrs;
this.childNodes = [];
this.parent = null;
this.nextSibling = null;
this.rooted = false;
}
get parentElement() { return this.parent; }
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; });
return this;
}
contains(node) { for (let p = node; p; p = p.parent) if (p === this) return true; return false; }
closest(selector) {
for (let el = this; el; el = el.parent) if (matches(el, selector)) return el;
return null;
}
}
// React re-renders the stock label back onto the same text node.
label.nodeValue = "Deep diving...";
observerCallback?.();
await new Promise((r) => setTimeout(r, 200));
ok("React revert re-asserted by debounced scan", label.nodeValue === first, JSON.stringify(label.nodeValue));
function matches(el, selector) {
if (el.nodeType !== 1) 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;
});
}
// Rotation moves the text on its own.
await new Promise((r) => setTimeout(r, 2400));
ok("phrase rotates on interval", label.nodeValue !== first, JSON.stringify(label.nodeValue));
function allElements(root) {
const out = [];
const walk = (node) => {
for (let c = node.firstChild; c; c = c.nextSibling) {
if (c.nodeType === 1) { out.push(c); walk(c); }
}
};
walk(root);
return out;
}
// Chinese locale label is recognized too (fresh status element, as React would
// mount for the next turn).
const zhStatus = new El();
zhStatus.rooted = true;
const zh = new TextNode("深度求索中...");
zh.parent = zhStatus;
zhStatus.children = [zh];
statuses = [zhStatus];
observerCallback?.();
await new Promise((r) => setTimeout(r, 200));
ok("zh label claimed", /\.\.\.$/.test(zh.nodeValue) && !/深度求索中/.test(zh.nodeValue), JSON.stringify(zh.nodeValue));
function useDom() {
const root = new El("div");
root.rooted = true;
globalThis.document = {
documentElement: root,
querySelectorAll: (selector) => allElements(root).filter((el) => matches(el, selector))
};
return root;
}
// Disposal stops rotation.
cleanup();
const frozen = zh.nodeValue;
await new Promise((r) => setTimeout(r, 2400));
ok("disposer stops rotation", zh.nodeValue === frozen);
// ---------------------------------------------------------------- assertions
let failures = 0;
const ok = (name, condition, extra = "") => {
if (!condition) failures += 1;
console.log(`${condition ? "PASS" : "FAIL"} ${name}${extra ? " " + extra : ""}`);
};
const wait = (ms) => new Promise((r) => setTimeout(r, ms));
const start = () => {
const mod = definition.factory(() => { throw new Error("require not expected"); });
let dispose = null;
mod.apply({ effect: (fn) => { dispose = fn(); } });
return { dispose };
};
// "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 })));
/** 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", { class: "label" });
const label = new TextNode("Deep diving for 1s");
labelSpan.append(label);
button.append(labelSpan);
const hidden = new El("span", { role: "status", "aria-live": "polite", "aria-atomic": "true" });
const announcement = new TextNode("Deep diving...");
hidden.append(announcement);
root.append(hidden, button);
const session = start();
const owned = label.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));
label.nodeValue = "Deep diving for 2s";
fire(label);
ok("0.1.7: live-duration rewrite re-asserted", label.nodeValue === owned, JSON.stringify(label.nodeValue));
label.nodeValue = "Worked for 12s";
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("0.1.7: stays released after a rotation tick", label.nodeValue === before, JSON.stringify(label.nodeValue));
session.dispose();
}
// ---------------------------------------------------- legacy layout (<= 0.1.5)
{
const root = useDom();
const status = new El("div", { role: "status", "aria-live": "polite" });
const label = new TextNode("Deep diving...");
const clock = new El("span", { "aria-hidden": "true" });
const clockText = new TextNode("12s");
clock.append(clockText);
status.append(label, clock);
root.append(status);
const session = start();
const owned = label.nodeValue;
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);
ok("legacy layout: rotates on interval", label.nodeValue !== owned, JSON.stringify(label.nodeValue));
session.dispose();
}
// ------------------------------------------------- localized + misc assurance
{
const root = useDom();
const { visible } = buildRunningStatus(root, "深度求索中,用时 3s...");
const session = start();
ok("localized running label claimed", isJoke(visible.nodeValue, "深度求索中,用时 3s...") && !/深度求索中/.test(visible.nodeValue), JSON.stringify(visible.nodeValue));
const frozen = visible.nodeValue;
session.dispose();
await wait(2400);
ok("disposer stops rotation", visible.nodeValue === frozen, JSON.stringify(visible.nodeValue));
}
// ---------------------------------------------------------- static integrity
{
const source = readFileSync(new URL("../client.js", import.meta.url), "utf8");
const match = source.match(/var PHRASES = \[([\s\S]*?)\n \];/);
const phrases = eval(`[${match[1]}]`);
const dupes = phrases.filter((p, i) => phrases.indexOf(p) !== i);
ok("phrase list has no duplicates", dupes.length === 0, dupes.join(", "));
ok("phrase list is non-trivial", phrases.length >= 100, `${phrases.length} phrases`);
}
console.log(failures === 0 ? "\nALL PASS" : `\n${failures} FAILURE(S)`);
process.exit(failures === 0 ? 0 : 1);