commit 4ea8234e6a353140404c91c9903f4dd02ef9dafd Author: mward4 Date: Mon Sep 14 20:41:58 2026 -0400 v0.1.1: initial release of @based-dsh-diving/dsh-plugin DSH web-profile plugin bundle that replaces the static "Deep diving..." turn-status label with 96 rotating loading phrases. Entirely browser-side: it claims the [role="status"] label text node, re-asserts the claim when React reconciles the stock string back, and rotates phrases on a 2.2s timer so the label keeps moving even when streaming never re-renders it. The elapsed-time clock, aria-live, and shimmer styling are untouched. Includes a headless DOM-stub test covering initial claim, clock preservation, React revert, timer rotation, localized-label detection (Deep diving... / 深度求索中...), and clean disposal. Architecture inspired by ldgeng/dsh-claudecode-tips (MIT). diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..c2658d7 --- /dev/null +++ b/.gitignore @@ -0,0 +1 @@ +node_modules/ diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..f7c3219 --- /dev/null +++ b/LICENSE @@ -0,0 +1,24 @@ +MIT License + +Copyright (c) 2026 based-dsh-diving contributors + +Portions of the client-plugin structure are derived from +ldgeng/dsh-claudecode-tips (MIT License, Copyright (c) 2026 ldgeng). + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/README.md b/README.md new file mode 100644 index 0000000..1cf0416 --- /dev/null +++ b/README.md @@ -0,0 +1,203 @@ +# based-dsh-diving + +> 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. + +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 +``` + +## Install + +Requires the `web` profile and DSH `0.1.0-rc.6` or newer (out-of-tree bundle support). + +```sh +# straight from Gitea +dsh plugin --profile web add git+https://gitea.milesward.dev/mward4/based-dsh-diving.git + +# or from a local checkout +dsh plugin --profile web add /path/to/based-dsh-diving +``` + +Then restart the web UI: + +```sh +dsh --profile web +``` + +### Verify it works + +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. + +### Disable it + +Without uninstalling, add this to the profile's `cordis.patch.yml`: + +```yaml +- id: based-dsh-diving + disabled: true +``` + +### 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: + +```yaml +- id: claude-code-tips + disabled: true +``` + +## Configuration + +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. | + +To add your own, drop a string into `PHRASES` and restart the web UI. No build step. + +## The phrases + +
+The originals (22) + +``` +Draining oceans, Killing sea turtles, Half-assing responses, Burning tokens, +Doomscrolling, Vibecoding, Deep diving, Bullshitting, Gaslighting, +Procrastinating, Guessing, Glazing, Crashing out, Deleting all files, +Melting glaciers, Evaporating a lake, Frying the power grid, +Strip-mining for lithium, Nuking the database, Malding, Spiraling, +Hallucinating +``` +
+ +
+Chronically online (20) + +``` +Yapping, Rizzing up the context window, Aura farming, Losing aura, +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 +``` +
+ +
+Engineering crimes (9) + +``` +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 +``` +
+ +
+Crashed out (10) + +``` +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 +``` +
+ +
+Superstition (5) + +``` +Reading your horoscope, Blaming Mercury retrograde, Praying, +Flipping a coin, Rerolling the dice +``` +
+ +
+Hardware abuse (4) + +``` +Shoveling coal into the GPU, Blowing on the cartridge, Wiggling the cable, +Reseating the RAM +``` +
+ +
+Eco-villain arc (13) + +``` +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 +``` +
+ +
+Financial damage (7) + +``` +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 +``` +
+ +
+Federal crimes (6) + +``` +Selling data to China, Exporting APIs to China, Selling your data to advertisers, +Parsing Hunter Biden's laptop, Installing key-logger, Tracing Satoshi Nakamoto +``` +
+ +## How it works + +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. | + +`client.js` locates the label via `[role="status"]` and matches its text against the stock label in every shipped locale (`Deep diving...` / `深度求索中...`). It 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. + +Only the label text node is touched. The clock span, `aria-live`, and the shimmer styling are untouched. + +No server code, no model prompt changes, no fork of DSH. + +## Development + +No build step, no runtime dependencies. + +```sh +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`. + +## Credits + +- Architecture inspired by [`ldgeng/dsh-claudecode-tips`](https://github.com/ldgeng/dsh-claudecode-tips) (MIT). +- Built for [DeepSeek Harness](https://github.com/deepseek-ai/deepseek-harness). + +## License + +MIT — see [LICENSE](LICENSE). diff --git a/client.js b/client.js new file mode 100644 index 0000000..6c09d7f --- /dev/null +++ b/client.js @@ -0,0 +1,272 @@ +window.__ModuleLoader__.load({ + id: "@based-dsh-diving/dsh-plugin", + factory: (require) => { + 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. + var PHRASES = [ + // ---- the originals ---- + "Draining oceans", + "Killing sea turtles", + "Half-assing responses", + "Burning tokens", + "Doomscrolling", + "Vibecoding", + "Deep diving", + "Bullshitting", + "Gaslighting", + "Procrastinating", + "Guessing", + "Glazing", + "Crashing out", + "Deleting all files", + "Melting glaciers", + "Evaporating a lake", + "Frying the power grid", + "Strip-mining for lithium", + "Nuking the database", + "Malding", + "Spiraling", + "Hallucinating", + + // ---- chronically online ---- + "Yapping", + "Rizzing up the context window", + "Aura farming", + "Losing aura", + "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", + + // ---- engineering crimes ---- + "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", + + // ---- crashed out ---- + "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", + + // ---- superstition ---- + "Reading your horoscope", + "Blaming Mercury retrograde", + "Praying", + "Flipping a coin", + "Rerolling the dice", + + // ---- hardware abuse ---- + "Shoveling coal into the GPU", + "Blowing on the cartridge", + "Wiggling the cable", + "Reseating the RAM", + + // ---- eco-villain arc ---- + "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", + + // ---- financial damage ---- + "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", + + // ---- federal crimes ---- + "Selling data to China", + "Exporting APIs to China", + "Selling your data to advertisers", + "Parsing Hunter Biden's laptop", + "Installing key-logger", + "Tracing Satoshi Nakamoto" + ]; + + 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; + + // element -> { node, text } for every status label we currently own. + var owned = new WeakMap(); + + var lastIndex = -1; + + function nextPhrase() { + var index; + do { + index = Math.floor(Math.random() * PHRASES.length); + } while (index === lastIndex && PHRASES.length > 1); + lastIndex = index; + return PHRASES[index] + SUFFIX; + } + + function isLive(node) { + 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; + } + 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]); + } + } + + // 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). + function rotate() { + var elements = statusElements(); + for (var i = 0; i < elements.length; i += 1) { + var element = elements[i]; + var record = owned.get(element); + if (!record) { + claim(element); + continue; + } + if (!isLive(record.node) || !element.contains(record.node)) { + owned.delete(element); + claim(element); + continue; + } + var text = nextPhrase(); + record.text = text; + record.node.nodeValue = text; + } + } + + function apply(ctx) { + ctx.effect(function () { + if (typeof document === "undefined" || typeof MutationObserver === "undefined") { + 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 (pending !== null) { + clearTimeout(pending); + pending = null; + } + owned = new WeakMap(); + }; + }, "based-dsh-diving: rotate turn-status loading text"); + } + + exports.apply = apply; + return module.exports; + } +}); diff --git a/cordis.patch.yml b/cordis.patch.yml new file mode 100644 index 0000000..8c87268 --- /dev/null +++ b/cordis.patch.yml @@ -0,0 +1,16 @@ +# @based-dsh-diving/dsh-plugin +# +# A client-side bundle layer for the web profile: it inserts a browser plugin +# that watches the chat turn status and replaces the static "Deep diving..." +# label with rotating, chronically online loading text while a turn is running. +# The elapsed-time clock beside the label is preserved. +# +# Later layers (profile cordis.patch.yml, $DSH_HOME/cordis.patch.yml, --patch) +# can re-patch or disable this row with: +# +# - id: based-dsh-diving +# disabled: true + +- insert: + - id: based-dsh-diving + name: '@based-dsh-diving/dsh-plugin' diff --git a/index.js b/index.js new file mode 100644 index 0000000..cde48e4 --- /dev/null +++ b/index.js @@ -0,0 +1,6 @@ +// Host-side half of @based-dsh-diving/dsh-plugin. +// +// This package is deliberately a no-op on the host: the plugin's entire job is +// to ship the browser-side `client.js` bundle to the web UI. The cordis row +// still needs a valid host module, so this stub satisfies the loader. +export function apply() {} diff --git a/package.json b/package.json new file mode 100644 index 0000000..d803e42 --- /dev/null +++ b/package.json @@ -0,0 +1,31 @@ +{ + "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", + "type": "module", + "main": "index.js", + "exports": { + ".": "./index.js", + "./client": "./client.js", + "./package.json": "./package.json" + }, + "files": [ + "index.js", + "client.js", + "cordis.patch.yml", + "README.md", + "LICENSE" + ], + "license": "MIT", + "scripts": { + "test": "node test/client.test.mjs" + }, + "dsh": { + "bundle": { + "patch": "./cordis.patch.yml" + }, + "client": { + "platform": "web" + } + } +} diff --git a/test/client.test.mjs b/test/client.test.mjs new file mode 100644 index 0000000..06d9fc4 --- /dev/null +++ b/test/client.test.mjs @@ -0,0 +1,88 @@ +// Headless harness for based-dsh-diving/client.js — stubs just enough DOM to +// exercise claim / React-revert / rotate behaviour without a browser. +import { readFileSync } from "node:fs"; + +const capturedLoads = []; +globalThis.window = { __ModuleLoader__: { load: (def) => capturedLoads.push(def) } }; +globalThis.Node = { TEXT_NODE: 3, ELEMENT_NODE: 1 }; + +let observerCallback = null; +globalThis.MutationObserver = class { + constructor(cb) { observerCallback = cb; } + observe() {} + 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 mod = def.factory(() => { throw new Error("require not expected"); }); +let cleanup = null; +mod.apply({ effect: (fn) => { cleanup = fn(); } }); + +const ok = (name, cond, extra = "") => console.log(`${cond ? "PASS" : "FAIL"} ${name}${extra ? " " + extra : ""}`); + +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"); + +const first = label.nodeValue; + +// 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)); + +// 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)); + +// 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)); + +// Disposal stops rotation. +cleanup(); +const frozen = zh.nodeValue; +await new Promise((r) => setTimeout(r, 2400)); +ok("disposer stops rotation", zh.nodeValue === frozen);