Add in-chat collapse and Zen controls
Hide workspace and agent content when the native sidebar rail is collapsed. Add persistent whole-composer collapse controls and a temporary Zen mode that removes side panels, chat header, and composer while keeping an in-chat exit control available. Cover state transitions, Escape handling, React rerender idempotence, and cleanup with a dependency-free DOM regression test.
This commit is contained in:
parent
cf9443753e
commit
500b5532a8
7 changed files with 267 additions and 15 deletions
|
|
@ -3,7 +3,7 @@
|
|||
| Component | Source |
|
||||
|---|---|
|
||||
| `dsh-touched-git` | Local repository `main` at `c33e67bdb43620b9a65fd5684971ade1caa14153`; one test-only absolute-home fallback was made portable with `homedir()` |
|
||||
| Other local plugins | Imported from non-Git directories under `~/.dsh/plugins` |
|
||||
| Other local plugins | Initially imported from non-Git directories under `~/.dsh/plugins`; maintained in this repository and deployed back to the live plugin directories |
|
||||
| DSH CLI | npm `@deepseek-ai/dsh@0.1.1-rc.2` |
|
||||
| OpenVSCode Server | Exact 1.109.5 runtime bundled and SHA-256 pinned by `manifests/editor-runtime.tsv`; upstream release metadata retained in `plugins/dsh-touched-git/editor-release.json` |
|
||||
| cloudflared | Exact 2026.8.3 Linux x86-64 binary bundled and SHA-256 pinned by `manifests/cloudflared.tsv` |
|
||||
|
|
|
|||
|
|
@ -42,6 +42,7 @@ window.__ModuleLoader__.load({
|
|||
if (HIDE_NEW_SESSION) {
|
||||
rules.push('[class*="_root"] [class*="_newSession"]{display:none !important;}');
|
||||
}
|
||||
rules.push('[data-sidebar-collapsed] [class*="_sidebarCol"] [class*="_regionArea"]{display:none !important;}');
|
||||
|
||||
const STYLE_ID = "dsh-compact-sidebar-style";
|
||||
const cssText = rules.join("");
|
||||
|
|
|
|||
|
|
@ -1,7 +1,7 @@
|
|||
{
|
||||
"name": "dsh-compact-sidebar",
|
||||
"description": "dsh web plugin: remove the dead vertical space between the logo row and the New Session button by tightening the sidebar logo row. Client-only.",
|
||||
"version": "0.1.0",
|
||||
"description": "dsh web plugin: hide optional sidebar chrome and suppress the workspace or agent tree while the native sidebar rail is collapsed. Client-only.",
|
||||
"version": "0.1.1",
|
||||
"private": true,
|
||||
"type": "module",
|
||||
"engines": {
|
||||
|
|
|
|||
|
|
@ -89,6 +89,9 @@ window.__ModuleLoader__.load({
|
|||
// -----------------------------------------------------------------------
|
||||
|
||||
const STYLE_ID = "dsh-ui-tweaks-style";
|
||||
const COMPOSER_STORAGE_KEY = "dsh-ui-tweaks:composer-collapsed";
|
||||
let composerCollapsed = readStoredComposerState();
|
||||
let zenActive = false;
|
||||
|
||||
/** Assemble the combined static CSS for whichever features are enabled. */
|
||||
function buildCss() {
|
||||
|
|
@ -213,6 +216,24 @@ window.__ModuleLoader__.load({
|
|||
);
|
||||
}
|
||||
|
||||
rules.push(
|
||||
'[data-sidebar-collapsed] [class*="_sidebarCol"] [class*="_regionArea"]{display:none !important;}',
|
||||
'[data-composer-seat]{position:relative}',
|
||||
'.dsh-chat-controls{position:absolute;right:24px;bottom:calc(100% + 6px);z-index:12;display:flex;align-items:center;gap:6px}',
|
||||
'.dsh-chat-control{height:28px;min-width:28px;padding:0 9px;border:1px solid var(--dsw-alias-border-l2,rgba(255,255,255,.12));border-radius:14px;background:var(--dsw-specific-input-major);color:var(--dsw-alias-label-secondary);font:var(--dsw-font-xs-13);cursor:pointer;box-shadow:var(--dsw-shadow-lv1)}',
|
||||
'.dsh-chat-control:hover{color:var(--dsw-alias-label-primary);background:color-mix(in srgb,var(--dsw-specific-input-major),#fff 6%)}',
|
||||
'[data-composer-seat][data-dsh-composer-collapsed="1"]{min-height:38px;justify-content:center}',
|
||||
'[data-composer-seat][data-dsh-composer-collapsed="1"]>:not(.dsh-chat-controls){display:none !important}',
|
||||
'[data-composer-seat][data-dsh-composer-collapsed="1"]>.dsh-chat-controls{position:static;align-self:flex-end;margin:4px 24px 6px 0}',
|
||||
'html[data-dsh-zen="1"] [class*="_frame"][style*="grid-template-columns"]{grid-template-columns:0 minmax(0,1fr) 0 !important}',
|
||||
'html[data-dsh-zen="1"] [class*="_sidebarCol"],html[data-dsh-zen="1"] [class*="_detailsCol"],html[data-dsh-zen="1"] [class*="_handle"]{display:none !important}',
|
||||
'html[data-dsh-zen="1"] [data-phase]>[class*="_header"]{display:none !important}',
|
||||
'html[data-dsh-zen="1"] [data-composer-seat]{min-height:38px;justify-content:center}',
|
||||
'html[data-dsh-zen="1"] [data-composer-seat]>:not(.dsh-chat-controls){display:none !important}',
|
||||
'html[data-dsh-zen="1"] [data-composer-seat]>.dsh-chat-controls{position:static;align-self:flex-end;margin:4px 24px 6px 0}',
|
||||
'html[data-dsh-zen="1"] .dsh-composer-collapse-toggle{display:none !important}'
|
||||
);
|
||||
|
||||
return rules.join("");
|
||||
}
|
||||
|
||||
|
|
@ -230,6 +251,87 @@ window.__ModuleLoader__.load({
|
|||
|
||||
// ---- (3) header-menu dynamic behavior ---------------------------------
|
||||
|
||||
function readStoredComposerState() {
|
||||
try {
|
||||
return window.localStorage.getItem(COMPOSER_STORAGE_KEY) === "1";
|
||||
} catch {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
function storeComposerState() {
|
||||
try {
|
||||
window.localStorage.setItem(COMPOSER_STORAGE_KEY, composerCollapsed ? "1" : "0");
|
||||
} catch {}
|
||||
}
|
||||
|
||||
function updateChatControls() {
|
||||
document.querySelectorAll("[data-composer-seat]").forEach((seat) => {
|
||||
if (composerCollapsed) seat.setAttribute("data-dsh-composer-collapsed", "1");
|
||||
else seat.removeAttribute("data-dsh-composer-collapsed");
|
||||
const collapse = seat.querySelector(".dsh-composer-collapse-toggle");
|
||||
if (collapse) {
|
||||
const collapseLabel = composerCollapsed ? "⌃ Chat" : "⌄ Chat";
|
||||
if (collapse.textContent !== collapseLabel) collapse.textContent = collapseLabel;
|
||||
collapse.title = composerCollapsed ? "Expand bottom chat bar" : "Collapse bottom chat bar";
|
||||
collapse.setAttribute("aria-expanded", composerCollapsed ? "false" : "true");
|
||||
}
|
||||
const zen = seat.querySelector(".dsh-zen-toggle");
|
||||
if (zen) {
|
||||
const zenLabel = zenActive ? "Exit Zen" : "Zen";
|
||||
if (zen.textContent !== zenLabel) zen.textContent = zenLabel;
|
||||
zen.title = zenActive ? "Exit Zen mode" : "Enter Zen mode";
|
||||
zen.setAttribute("aria-pressed", zenActive ? "true" : "false");
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
function setComposerCollapsed(collapsed) {
|
||||
composerCollapsed = collapsed;
|
||||
storeComposerState();
|
||||
updateChatControls();
|
||||
}
|
||||
|
||||
function setZenActive(active) {
|
||||
zenActive = active;
|
||||
if (active) document.documentElement.setAttribute("data-dsh-zen", "1");
|
||||
else document.documentElement.removeAttribute("data-dsh-zen");
|
||||
updateChatControls();
|
||||
}
|
||||
|
||||
function makeChatControls() {
|
||||
const controls = document.createElement("div");
|
||||
controls.className = "dsh-chat-controls";
|
||||
|
||||
const collapse = document.createElement("button");
|
||||
collapse.type = "button";
|
||||
collapse.className = "dsh-chat-control dsh-composer-collapse-toggle";
|
||||
collapse.setAttribute("aria-label", "Toggle bottom chat bar");
|
||||
collapse.addEventListener("click", () => setComposerCollapsed(!composerCollapsed));
|
||||
|
||||
const zen = document.createElement("button");
|
||||
zen.type = "button";
|
||||
zen.className = "dsh-chat-control dsh-zen-toggle";
|
||||
zen.setAttribute("aria-label", "Toggle Zen mode");
|
||||
zen.addEventListener("click", () => setZenActive(!zenActive));
|
||||
|
||||
controls.append(collapse, zen);
|
||||
return controls;
|
||||
}
|
||||
|
||||
function enhanceChatControls() {
|
||||
document.querySelectorAll("[data-composer-seat]").forEach((seat) => {
|
||||
if (!seat.querySelector(":scope > .dsh-chat-controls")) seat.appendChild(makeChatControls());
|
||||
});
|
||||
updateChatControls();
|
||||
}
|
||||
|
||||
function removeChatControls() {
|
||||
setZenActive(false);
|
||||
document.querySelectorAll(".dsh-chat-controls").forEach((controls) => controls.remove());
|
||||
document.querySelectorAll("[data-composer-seat]").forEach((seat) => seat.removeAttribute("data-dsh-composer-collapsed"));
|
||||
}
|
||||
|
||||
/** Close every open header menu (optionally sparing one host). */
|
||||
function closeAll(except) {
|
||||
document.querySelectorAll('[data-dsh-hdr-menu="host"][data-open="1"]').forEach((host) => {
|
||||
|
|
@ -267,6 +369,8 @@ window.__ModuleLoader__.load({
|
|||
|
||||
/** Enhance every header-utilities region currently in the DOM (idempotent). */
|
||||
function enhance() {
|
||||
enhanceChatControls();
|
||||
if (!COLLAPSE_HEADER) return;
|
||||
const panels = document.querySelectorAll('[class*="_headerUtilities"]');
|
||||
panels.forEach((panel) => {
|
||||
const host = panel.parentElement;
|
||||
|
|
@ -310,15 +414,6 @@ window.__ModuleLoader__.load({
|
|||
const start = () => {
|
||||
installStyle();
|
||||
|
||||
// Features 1 & 2 are pure CSS — nothing more to wire.
|
||||
if (!COLLAPSE_HEADER) {
|
||||
return () => {
|
||||
const style = document.getElementById(STYLE_ID);
|
||||
if (style) style.remove();
|
||||
};
|
||||
}
|
||||
|
||||
// Feature 3 — dynamic header dropdown.
|
||||
enhance();
|
||||
|
||||
const observer = new MutationObserver(scheduleEnhance);
|
||||
|
|
@ -329,7 +424,9 @@ window.__ModuleLoader__.load({
|
|||
if (!host) closeAll();
|
||||
};
|
||||
const onKey = (e) => {
|
||||
if (e.key === "Escape") closeAll();
|
||||
if (e.key !== "Escape") return;
|
||||
closeAll();
|
||||
if (zenActive) setZenActive(false);
|
||||
};
|
||||
document.addEventListener("click", onDocClick, true);
|
||||
document.addEventListener("keydown", onKey, true);
|
||||
|
|
@ -339,6 +436,7 @@ window.__ModuleLoader__.load({
|
|||
document.removeEventListener("click", onDocClick, true);
|
||||
document.removeEventListener("keydown", onKey, true);
|
||||
closeAll();
|
||||
removeChatControls();
|
||||
const style = document.getElementById(STYLE_ID);
|
||||
if (style) style.remove();
|
||||
document.querySelectorAll(".dsh-hdr-menu-trigger").forEach((n) => n.remove());
|
||||
|
|
|
|||
|
|
@ -1,7 +1,7 @@
|
|||
{
|
||||
"name": "dsh-ui-tweaks",
|
||||
"description": "dsh web plugin: a bundle of toggleable UI tweaks — match the sidebar background to the main content, replace the sidebar brand logo with plain text while keeping the native collapse/expand toggle, hide the New Session button, and collapse the conversation-header utility buttons into a single dropdown menu. Client-only.",
|
||||
"version": "0.1.0",
|
||||
"description": "dsh web plugin: cohesive sidebar, header, composer-collapse, and Zen-mode controls for the DSH Web chat interface. Client-only.",
|
||||
"version": "0.2.0",
|
||||
"private": true,
|
||||
"type": "module",
|
||||
"engines": {
|
||||
|
|
|
|||
|
|
@ -7,6 +7,7 @@ cd "$repository_directory"
|
|||
bash -n install.sh uninstall.sh bin/dsh-app bin/free-port-3080.sh test/*.sh
|
||||
node --check scripts/apply-compatibility-patches.mjs
|
||||
node test/profile-manifest.test.mjs
|
||||
node test/ui-controls.test.mjs
|
||||
python3 test/editor-artifacts.test.py
|
||||
python3 test/editor-runtime.test.py
|
||||
python3 test/cloudflared-artifact.test.py
|
||||
|
|
|
|||
152
test/ui-controls.test.mjs
Normal file
152
test/ui-controls.test.mjs
Normal file
|
|
@ -0,0 +1,152 @@
|
|||
import assert from 'node:assert/strict'
|
||||
import { readFile } from 'node:fs/promises'
|
||||
import vm from 'node:vm'
|
||||
|
||||
class Element {
|
||||
constructor(tagName) {
|
||||
this.tagName = tagName
|
||||
this.attributes = new Map()
|
||||
this.children = []
|
||||
this.listeners = new Map()
|
||||
this.dataset = {}
|
||||
this.parentElement = null
|
||||
this.className = ''
|
||||
this.textContent = ''
|
||||
this.title = ''
|
||||
this.type = ''
|
||||
}
|
||||
|
||||
append(...children) {
|
||||
for (const child of children) this.appendChild(child)
|
||||
}
|
||||
|
||||
appendChild(child) {
|
||||
child.parentElement = this
|
||||
this.children.push(child)
|
||||
return child
|
||||
}
|
||||
|
||||
addEventListener(name, listener) {
|
||||
this.listeners.set(name, listener)
|
||||
}
|
||||
|
||||
click() {
|
||||
this.listeners.get('click')?.({ preventDefault() {}, stopPropagation() {}, target: this })
|
||||
}
|
||||
|
||||
setAttribute(name, value) {
|
||||
this.attributes.set(name, String(value))
|
||||
}
|
||||
|
||||
getAttribute(name) {
|
||||
return this.attributes.get(name) ?? null
|
||||
}
|
||||
|
||||
removeAttribute(name) {
|
||||
this.attributes.delete(name)
|
||||
}
|
||||
|
||||
querySelector(selector) {
|
||||
if (selector === ':scope > .dsh-chat-controls') return this.children.find((child) => child.hasClass('dsh-chat-controls')) ?? null
|
||||
if (selector.startsWith('.')) return this.descendants().find((child) => child.hasClass(selector.slice(1))) ?? null
|
||||
return null
|
||||
}
|
||||
|
||||
hasClass(name) {
|
||||
return this.className.split(/\s+/).includes(name)
|
||||
}
|
||||
|
||||
descendants() {
|
||||
return this.children.flatMap((child) => [child, ...child.descendants()])
|
||||
}
|
||||
|
||||
remove() {
|
||||
if (!this.parentElement) return
|
||||
this.parentElement.children = this.parentElement.children.filter((child) => child !== this)
|
||||
this.parentElement = null
|
||||
}
|
||||
}
|
||||
|
||||
const documentElement = new Element('html')
|
||||
const head = new Element('head')
|
||||
const body = new Element('body')
|
||||
const seat = new Element('div')
|
||||
seat.setAttribute('data-composer-seat', '')
|
||||
body.appendChild(seat)
|
||||
const documentListeners = new Map()
|
||||
|
||||
const allElements = () => [documentElement, head, body, ...head.descendants(), ...body.descendants()]
|
||||
const document = {
|
||||
documentElement,
|
||||
head,
|
||||
body,
|
||||
createElement: (tagName) => new Element(tagName),
|
||||
getElementById: (id) => allElements().find((element) => element.id === id) ?? null,
|
||||
querySelectorAll(selector) {
|
||||
if (selector === '[data-composer-seat]') return allElements().filter((element) => element.attributes.has('data-composer-seat'))
|
||||
if (selector === '.dsh-chat-controls') return allElements().filter((element) => element.hasClass('dsh-chat-controls'))
|
||||
if (selector === '.dsh-hdr-menu-trigger' || selector === '[data-dsh-hdr-menu]' || selector === '[class*="_headerUtilities"]') return []
|
||||
return []
|
||||
},
|
||||
addEventListener: (name, listener) => documentListeners.set(name, listener),
|
||||
removeEventListener: (name) => documentListeners.delete(name),
|
||||
}
|
||||
|
||||
const storage = new Map()
|
||||
const window = {
|
||||
document,
|
||||
localStorage: {
|
||||
getItem: (key) => storage.get(key) ?? null,
|
||||
setItem: (key, value) => storage.set(key, String(value)),
|
||||
},
|
||||
}
|
||||
window.window = window
|
||||
let definition
|
||||
window.__ModuleLoader__ = { load: (candidate) => { definition = candidate } }
|
||||
|
||||
class MutationObserver {
|
||||
observe() {}
|
||||
disconnect() {}
|
||||
}
|
||||
|
||||
const source = await readFile(new URL('../plugins/dsh-ui-tweaks/lib/client.js', import.meta.url), 'utf8')
|
||||
vm.runInNewContext(source, {
|
||||
window,
|
||||
document,
|
||||
MutationObserver,
|
||||
requestAnimationFrame: (callback) => callback(),
|
||||
setTimeout,
|
||||
})
|
||||
|
||||
const plugin = definition.factory(() => {})
|
||||
let cleanup
|
||||
plugin.apply({ effect: (start) => { cleanup = start() } })
|
||||
|
||||
const controls = seat.querySelector(':scope > .dsh-chat-controls')
|
||||
assert(controls)
|
||||
const collapse = controls.querySelector('.dsh-composer-collapse-toggle')
|
||||
const zen = controls.querySelector('.dsh-zen-toggle')
|
||||
assert.equal(collapse.textContent, '⌄ Chat')
|
||||
assert.equal(zen.textContent, 'Zen')
|
||||
|
||||
collapse.click()
|
||||
assert.equal(seat.getAttribute('data-dsh-composer-collapsed'), '1')
|
||||
assert.equal(storage.get('dsh-ui-tweaks:composer-collapsed'), '1')
|
||||
assert.equal(collapse.textContent, '⌃ Chat')
|
||||
|
||||
zen.click()
|
||||
assert.equal(documentElement.getAttribute('data-dsh-zen'), '1')
|
||||
assert.equal(zen.textContent, 'Exit Zen')
|
||||
assert.equal(seat.getAttribute('data-dsh-composer-collapsed'), '1')
|
||||
|
||||
documentListeners.get('keydown')?.({ key: 'Escape' })
|
||||
assert.equal(documentElement.getAttribute('data-dsh-zen'), null)
|
||||
assert.equal(zen.textContent, 'Zen')
|
||||
|
||||
cleanup()
|
||||
assert.equal(seat.querySelector(':scope > .dsh-chat-controls'), null)
|
||||
assert.equal(seat.getAttribute('data-dsh-composer-collapsed'), null)
|
||||
assert.equal(documentElement.getAttribute('data-dsh-zen'), null)
|
||||
assert(source.includes('[data-sidebar-collapsed] [class*="_sidebarCol"] [class*="_regionArea"]'))
|
||||
|
||||
console.log('UI controls test passed')
|
||||
Loading…
Add table
Add a link
Reference in a new issue