After removing the former collapse feature, force a one-time reflow of DSH input mirrors and textareas and refresh the conversation composer-height variable. Restore every temporary inline style immediately and mark each mounted input so normal prompt behavior remains entirely native afterward.
237 lines
9.4 KiB
JavaScript
237 lines
9.4 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 = ''
|
|
this.clientHeight = 32
|
|
this.offsetHeight = 32
|
|
this.isConnected = true
|
|
const properties = new Map()
|
|
this.style = {
|
|
position: '',
|
|
height: '',
|
|
setProperty: (name, value) => properties.set(name, String(value)),
|
|
getPropertyValue: (name) => properties.get(name) ?? '',
|
|
}
|
|
}
|
|
|
|
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 === '[data-input-mirror]') return this.descendants().find((child) => child.attributes.has('data-input-mirror')) ?? null
|
|
if (selector === 'textarea') return this.descendants().find((child) => child.tagName === 'textarea') ?? null
|
|
if (selector.startsWith('.')) return this.descendants().find((child) => child.hasClass(selector.slice(1))) ?? null
|
|
return null
|
|
}
|
|
|
|
querySelectorAll(selector) {
|
|
if (selector.startsWith('.')) return this.descendants().filter((child) => child.hasClass(selector.slice(1)))
|
|
return []
|
|
}
|
|
|
|
closest(selector) {
|
|
let current = this
|
|
while (current) {
|
|
if (selector === '[data-phase]' && current.attributes.has('data-phase')) return current
|
|
if (selector === '[data-composer-seat]' && current.attributes.has('data-composer-seat')) return current
|
|
current = current.parentElement
|
|
}
|
|
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 headerActions = new Element('div')
|
|
const git = new Element('div')
|
|
git.className = 'dsh-git-working-dirs'
|
|
const utilities = new Element('div')
|
|
utilities.className = 'fixture_headerUtilities'
|
|
headerActions.appendChild(git)
|
|
titleRow.append(headerActions, utilities)
|
|
header.appendChild(titleRow)
|
|
const seat = new Element('div')
|
|
seat.setAttribute('data-composer-seat', '')
|
|
seat.setAttribute('data-dsh-composer-collapsed', '1')
|
|
const inputScroll = new Element('div')
|
|
inputScroll.setAttribute('data-input-scroll', '')
|
|
const input = new Element('textarea')
|
|
const mirror = new Element('div')
|
|
mirror.setAttribute('data-input-mirror', '')
|
|
inputScroll.append(input, mirror)
|
|
seat.appendChild(inputScroll)
|
|
const nestedPhase = new Element('div')
|
|
nestedPhase.setAttribute('data-phase', 'streaming')
|
|
const strayControls = new Element('div')
|
|
strayControls.className = 'dsh-chat-controls'
|
|
nestedPhase.appendChild(strayControls)
|
|
conversation.append(header, nestedPhase, 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 === '[data-input-scroll]') return allElements().filter((element) => element.attributes.has('data-input-scroll'))
|
|
if (selector === '[data-dsh-composer-repaired="1"]') return allElements().filter((element) => element.getAttribute('data-dsh-composer-repaired') === '1')
|
|
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([['dsh-ui-tweaks:composer-collapsed', '1']])
|
|
const window = {
|
|
document,
|
|
localStorage: {
|
|
getItem: (key) => storage.get(key) ?? null,
|
|
setItem: (key, value) => storage.set(key, String(value)),
|
|
removeItem: (key) => storage.delete(key),
|
|
},
|
|
}
|
|
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(document.querySelectorAll('.dsh-chat-controls').length, 1)
|
|
assert.equal(strayControls.parentElement, null)
|
|
assert.equal(controls.parentElement, headerActions)
|
|
assert(controls.parentElement.children.indexOf(controls) < controls.parentElement.children.indexOf(git))
|
|
const zen = controls.querySelector('.dsh-zen-toggle')
|
|
assert.equal(controls.querySelector('.dsh-composer-collapse-toggle'), null)
|
|
assert.equal(zen.textContent, 'Zen')
|
|
assert.equal(seat.getAttribute('data-dsh-composer-collapsed'), null)
|
|
assert.equal(storage.has('dsh-ui-tweaks:composer-collapsed'), false)
|
|
assert.equal(inputScroll.getAttribute('data-dsh-composer-repaired'), '1')
|
|
assert.equal(mirror.style.position, '')
|
|
assert.equal(input.style.height, '')
|
|
assert.equal(conversation.style.getPropertyValue('--dsh-composer-height'), '32px')
|
|
|
|
zen.click()
|
|
assert.equal(documentElement.getAttribute('data-dsh-zen'), '1')
|
|
assert.equal(zen.textContent, 'Exit Zen')
|
|
assert.equal(seat.getAttribute('data-dsh-composer-collapsed'), null)
|
|
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, headerActions)
|
|
assert(controls.parentElement.children.indexOf(controls) < controls.parentElement.children.indexOf(git))
|
|
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(inputScroll.getAttribute('data-dsh-composer-repaired'), 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('dsh-composer-collapse-toggle'))
|
|
assert(!source.includes('[data-composer-seat][data-dsh-composer-collapsed="1"]'))
|
|
assert(source.includes('[data-composer-seat]{position:absolute !important;left:0;right:0;bottom:0;visibility:hidden'))
|
|
assert(source.includes('[data-phase] header[class*="_header"]{display:none !important}'))
|
|
|
|
console.log('UI controls test passed')
|