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:
Шурупов Илья Викторович 2026-09-14 10:49:53 +03:00
parent cf9443753e
commit 500b5532a8
7 changed files with 267 additions and 15 deletions

View file

@ -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` |

View file

@ -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("");

View file

@ -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": {

View file

@ -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());

View file

@ -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": {

View file

@ -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
View 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')