Insert Chat and Zen immediately before the Git utility group in normal mode, then relocate them to the conversation root while Zen hides the header. Collapse the composer with measurement-safe clipping instead of display none to prevent incorrect empty textarea height after restoring it.
185 lines
6.8 KiB
JavaScript
185 lines
6.8 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) {
|
|
if (child.parentElement) child.parentElement.children = child.parentElement.children.filter((candidate) => candidate !== child)
|
|
child.parentElement = this
|
|
this.children.push(child)
|
|
return child
|
|
}
|
|
|
|
insertBefore(child, reference) {
|
|
if (child.parentElement) child.parentElement.children = child.parentElement.children.filter((candidate) => candidate !== child)
|
|
const index = this.children.indexOf(reference)
|
|
child.parentElement = this
|
|
this.children.splice(index < 0 ? this.children.length : index, 0, 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 === 'header [class*="_headerUtilities"]') return this.descendants().find((child) => child.tagName === 'div' && child.className.includes('_headerUtilities')) ?? 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 conversation = new Element('div')
|
|
conversation.setAttribute('data-phase', 'active')
|
|
const header = new Element('header')
|
|
const titleRow = new Element('div')
|
|
const utilities = new Element('div')
|
|
utilities.className = 'fixture_headerUtilities'
|
|
titleRow.appendChild(utilities)
|
|
header.appendChild(titleRow)
|
|
const seat = new Element('div')
|
|
seat.setAttribute('data-composer-seat', '')
|
|
conversation.append(header, seat)
|
|
body.appendChild(conversation)
|
|
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-phase]') return allElements().filter((element) => element.attributes.has('data-phase'))
|
|
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 = conversation.querySelector('.dsh-chat-controls')
|
|
assert(controls)
|
|
assert.equal(controls.parentElement, titleRow)
|
|
assert(controls.parentElement.children.indexOf(controls) < controls.parentElement.children.indexOf(utilities))
|
|
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')
|
|
assert.equal(controls.parentElement, conversation)
|
|
assert.equal(controls.getAttribute('data-floating'), '1')
|
|
|
|
documentListeners.get('keydown')?.({ key: 'Escape' })
|
|
assert.equal(documentElement.getAttribute('data-dsh-zen'), null)
|
|
assert.equal(zen.textContent, 'Zen')
|
|
assert.equal(controls.parentElement, titleRow)
|
|
assert.equal(controls.getAttribute('data-floating'), null)
|
|
|
|
cleanup()
|
|
assert.equal(conversation.querySelector('.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"]'))
|
|
assert(source.includes('[class*="_detailsCol"]{visibility:hidden !important;pointer-events:none !important;border:0 !important}'))
|
|
assert(!source.includes('[class*="_detailsCol"],html[data-dsh-zen="1"] [class*="_handle"]{display:none'))
|
|
assert(source.includes('.dsh-chat-controls{z-index:12;display:flex;flex:none'))
|
|
assert(source.includes('.dsh-chat-controls[data-floating="1"]{position:absolute;right:24px;top:12px'))
|
|
assert(source.includes('[data-composer-seat][data-dsh-composer-collapsed="1"]{max-height:0 !important'))
|
|
assert(!source.includes('[data-composer-seat][data-dsh-composer-collapsed="1"]{display:none'))
|
|
assert(source.includes('[data-phase] header[class*="_header"]{display:none !important}'))
|
|
|
|
console.log('UI controls test passed')
|