Deduplicate chat controls by composer owner

Derive conversation roots from composer seats instead of every nested phase element, remove invalid and duplicate control groups, and anchor the sole surviving group directly before Git Working Dirs. Preserve Zen relocation and measurement-safe composer collapse.
This commit is contained in:
Шурупов Илья Викторович 2026-09-14 12:07:48 +03:00
parent 3eccc0411d
commit ed00c7b83a
3 changed files with 72 additions and 16 deletions

View file

@ -292,14 +292,46 @@ window.__ModuleLoader__.load({
updateChatControls();
}
function placeChatControls() {
document.querySelectorAll("[data-phase]").forEach((conversation) => {
const controls = conversation.querySelector(".dsh-chat-controls");
function conversationRoots() {
const roots = [];
document.querySelectorAll("[data-composer-seat]").forEach((seat) => {
const conversation = seat.closest("[data-phase]");
if (conversation && !roots.includes(conversation)) roots.push(conversation);
});
return roots;
}
function normalizeChatControls() {
const roots = conversationRoots();
const validRoots = new Set(roots);
const owners = new Map();
document.querySelectorAll(".dsh-chat-controls").forEach((controls) => {
const owner = controls.closest("[data-phase]");
if (!validRoots.has(owner) || owners.has(owner)) {
controls.remove();
return;
}
owners.set(owner, controls);
});
roots.forEach((conversation) => {
if (owners.has(conversation)) return;
const controls = makeChatControls();
conversation.appendChild(controls);
owners.set(conversation, controls);
});
return roots;
}
function placeChatControls(roots = conversationRoots()) {
roots.forEach((conversation) => {
const controls = Array.from(conversation.querySelectorAll(".dsh-chat-controls")).find((candidate) => candidate.closest("[data-phase]") === conversation);
if (!controls) return;
const git = conversation.querySelector(".dsh-git-working-dirs");
const utilities = conversation.querySelector('header [class*="_headerUtilities"]');
if (!zenActive && utilities?.parentElement) {
const anchor = git ?? utilities;
if (!zenActive && anchor?.parentElement) {
controls.removeAttribute("data-floating");
if (controls.parentElement !== utilities.parentElement) utilities.parentElement.insertBefore(controls, utilities);
if (controls.parentElement !== anchor.parentElement) anchor.parentElement.insertBefore(controls, anchor);
return;
}
controls.setAttribute("data-floating", "1");
@ -311,7 +343,7 @@ window.__ModuleLoader__.load({
zenActive = active;
if (active) document.documentElement.setAttribute("data-dsh-zen", "1");
else document.documentElement.removeAttribute("data-dsh-zen");
placeChatControls();
placeChatControls(normalizeChatControls());
updateChatControls();
}
@ -336,10 +368,8 @@ window.__ModuleLoader__.load({
}
function enhanceChatControls() {
document.querySelectorAll("[data-phase]").forEach((conversation) => {
if (!conversation.querySelector(".dsh-chat-controls")) conversation.appendChild(makeChatControls());
});
placeChatControls();
const roots = normalizeChatControls();
placeChatControls(roots);
updateChatControls();
}

View file

@ -1,7 +1,7 @@
{
"name": "dsh-ui-tweaks",
"description": "dsh web plugin: cohesive sidebar, header, composer-collapse, and Zen-mode controls for the DSH Web chat interface. Client-only.",
"version": "0.2.2",
"version": "0.2.3",
"private": true,
"type": "module",
"engines": {

View file

@ -62,6 +62,20 @@ class Element {
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
current = current.parentElement
}
return null
}
hasClass(name) {
return this.className.split(/\s+/).includes(name)
}
@ -84,13 +98,22 @@ 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'
titleRow.appendChild(utilities)
headerActions.appendChild(git)
titleRow.append(headerActions, utilities)
header.appendChild(titleRow)
const seat = new Element('div')
seat.setAttribute('data-composer-seat', '')
conversation.append(header, seat)
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()
@ -144,8 +167,10 @@ 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))
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 collapse = controls.querySelector('.dsh-composer-collapse-toggle')
const zen = controls.querySelector('.dsh-zen-toggle')
assert.equal(collapse.textContent, '⌄ Chat')
@ -166,7 +191,8 @@ 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.parentElement, headerActions)
assert(controls.parentElement.children.indexOf(controls) < controls.parentElement.children.indexOf(git))
assert.equal(controls.getAttribute('data-floating'), null)
cleanup()