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.
409 lines
13 KiB
JavaScript
409 lines
13 KiB
JavaScript
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 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",
|
|
"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",
|
|
"Arguing with a stranger",
|
|
|
|
// ---- 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",
|
|
"chmod 777 everything",
|
|
"Running as root",
|
|
"Disabling the firewall",
|
|
"Pushing the API key",
|
|
"Deleting all backups",
|
|
|
|
// ---- 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",
|
|
"Holding my breath",
|
|
|
|
// ---- 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",
|
|
"Burning tires",
|
|
|
|
// ---- 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",
|
|
"Fudging the numbers",
|
|
"Starting a pyramid scheme",
|
|
"Insider trading",
|
|
"Committing tax fraud",
|
|
|
|
// ---- 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",
|
|
"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 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;
|
|
|
|
// 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;
|
|
|
|
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;
|
|
}
|
|
|
|
/**
|
|
* 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;
|
|
}
|
|
return document.querySelectorAll('[role="status"]');
|
|
}
|
|
|
|
/** 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 || "")) 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() {
|
|
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(host);
|
|
continue;
|
|
}
|
|
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;
|
|
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 () {
|
|
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 () {};
|
|
}
|
|
|
|
return function () {
|
|
if (observer) observer.disconnect();
|
|
if (interval !== null) 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;
|
|
}
|
|
});
|