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.
152 lines
4.6 KiB
JavaScript
152 lines
4.6 KiB
JavaScript
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')
|