Волна твик (volna-tweaks)
Твики DeepSeek Harness одним пакетом. Каждый включается своей галочкой на странице
пакета (Settings → Plugins → «Волна твик») и выключается там же — без перезапуска.
| Твик | Что делает |
|---|---|
fileDownload |
Скачивание файлов: долгое нажатие на файловой карточке в чате и пункт «Скачать» в меню вкладки файла правой панели |
queueEditor |
Единая форма редактирования очереди: текст и вложения, добавление/удаление, сохранение на прежнем месте; host-роут в tweaks/queue-edit-host.js |
mobileComposer |
Мобильный ввод: Enter/paste без отправки, «+» только File/Goal/Plan, / без меню, системный чип Plan (вход через «+» → Plan), прокрутка к каретке, высота черновика вдвое меньше, без подчёркивания ввода |
mobileSidebar |
Мобильный shell: ящик слева, иконка и название в шапке, чат на всю ширину, панели в нижней иконке, все меню в верхнем «…» |
zoom |
Масштаб всего интерфейса: пара клавиш меняет размер интерфейса, а не только шрифт переписки |
contextMenu |
Своё меню по правой кнопке: системное меню Electron не масштабируется вместе с интерфейсом |
whisper |
Распознавание русской речи локальной моделью Whisper (tweaks/whisper-host.js, Python-воркер): выключено по умолчанию |
trafficLight |
Светофор дорогих часов DeepSeek: лампа и переключатель «Спецсигнал» под полем ввода; на пике встают все ходы (цель, итоги субагентов, сообщения команды, расписания, внешние сообщения), на зелёном продолжаются сами; спецсигнал пускает чат и всё его дерево работать на красном |
imageModelFallback |
Кнопка «Отправить с Flash» над полем ввода, когда выбранная модель не видит изображения: меняет модель и повторяет отправку |
freePanels |
Свободный размер панелей: штатный драг не упирается в лимиты DSH (сайдбар 264–420, правая панель 300 – 0.7 окна, центр не уже 400) |
Свободный размер панелей (твик freePanels)
Штатный драг панелей DSH клампит ширины: сайдбар 264–420 px, правая панель
300 px – 70% окна (min(окно − сайдбар − 400, 0.7·окно)), центральная колонка не уже
400 px. Из-за этого панель упирается, а ручка продолжает «уезжать» от курсора.
Твик перехватывает нажатие на ручку рамки в capture-фазе — приложение свой
DragHandle не получает — и пишет ширину прямо в сетку рамки. Правила:
- тянуть можно до 120 px с каждой стороны и так, чтобы центру осталось не меньше
120 px (граница — 120 px, чтобы было чем взяться за разделитель); - ширины запоминаются (
localStorage, ключdsh.ui-panels.v1) и применяются на
следующей загрузке; пока панели не тянули, используется ширина приложения; - свернутый сайдбар и закрытая/полноэкранная правая панель не трогаются — там
шириной распоряжается само приложение; - в настольном приложении вместе с сеткой обновляется
--dsh-windows-sidebar-width, чтобы шапка окна оставалась выровненной; - в масштабе сдвиг курсора уже в логических пикселях: координату на ручках
[data-side]делит твик «масштаб», второй раз делить нельзя.
Разметка DSH берётся только по data-*-атрибутам и суффиксам классов
([class*="_frame"], [class*="_sidebarCol"], [data-rightbar-col],
[data-side="..."]), поэтому привязки к хешам сборки нет и твик работает и в
браузере, и в настольном приложении.
Редактор очереди с вложениями (queueEditor)
Durable-строки очереди (не Sending...) редактируются одной формой в стиле
основного композера: textarea с переносом Enter, вложения с превью и ×,
кнопка «+ Вложение» (картинки и файлы), Save/Cancel.
Клик по штатному карандашу перехватывается и открывает форму вместо нативного
inline-редактора. Хостовый роут POST /dsh-tweaks/queue/edit сохраняет новые
вложения через attachments.saveImages/saveFile, проверяет, что существующие
ссылки принадлежат этому элементу очереди, и заменяет сообщение через
agent.inbox.replace — позиция в очереди не меняется, остальные строки не
трогаются. Форма поднимается в conversation.input.dock над очередью; на телефоне
она на всю ширину. При исчезновении элемента/ошибке форма остаётся открытой с
текстом ошибки, очередь не меняется. Хост-роут требует перезапуска dsh.
Скачивание файлов (fileDownload)
Долгое нажатие (≈0.5 с) на файловой карточке в чате открывает меню со строкой
«Скачать» и «Копировать путь». В правой панели у вкладки файла меню
(долгое нажатие / контекстное меню) получает «Скачать» и «Копировать путь»
рядом со штатным Close. Путь для durable-вложения разрешает host через
attachments.fileHostPath, для workspace-файла — через workspaceFiles.stat.
Байты отдаёт хост-роут POST /dsh-tweaks/download: durable-вложение читается
attachments.readFileStream, файл workspace — workspaceFiles.readBytes.
Браузер получает Blob и скачивает обычной ссылкой. Роут закрыт штатным
connection-фильтром; клиентская галочка fileDownload только управляет UI.
Хост-роут требует перезапуска dsh.
Меню «+»: только строки
Позицию, ширину, анимацию и закрытие меню «+» задаёт ядро. Твик больше не
перехватывает клик по «+» и не пишет position/left/top меню: он только
скрывает лишние строки, оставляя File/Goal/Plan (Файл/Цель/План).
Меню «+» на любом языке
Фильтр оставляет в меню только File/Goal/Plan по служебным alias (file,
goal, plan), а не по видимой подписи. Поэтому русские «Файл/Цель/План» и
другие локали не превращают меню в пустое.
Твик «цель живёт» (host)
goalKeepalive — host-твик: после restart/fork/resume возвращает активацию
цели active+disarmed, не трогая ручную паузу, complete и blocked.
Журнал — $DSH_HOME/dsh-tweaks/goal-keepalive.log, ротация 1 МБ.
Страховка агентов (agentGuards, host)
Механическая страховка к правилам агентов HARNESS (аудит промахов 13.11.2026).
Хук tools/pre-execute отказывает в вызове до исполнения тела инструмента:
| Правило | Флаг | Что ловит | Промахи |
|---|---|---|---|
secrets |
agentGuardsSecrets |
в bash: PGPASSWORD=<литерал>, DSN postgres://u:<пароль>@, --password=<литерал>, Bearer <токен>, токены ghp_/gho_/github_pat_/re_/sk-/xox?-. $VAR, ${VAR}, $(…) разрешены |
18, 21 |
wip |
agentGuardsWip |
+wip в команде выката; dev-stand.sh deploy/resume с --allow-shared-tree или из грязного worktree (git status --porcelain); --ref пропускается |
1, 2, 8, 13 |
rootWrite |
agentGuardsRootWrite |
write/edit/apply_patch в основной checkout на main/master внутри agentGuardsRootWriteRoots (по умолчанию /home/goxa/project); linked worktree и agentGuardsRootWriteAllow (docs/plans/, .cursor/plans/, reports/, .local/, .wt/, INFRA-CHANGES.md) пропускаются |
1, 2, 20, 26 |
loop |
agentGuardsLoop |
agentGuardsLoopLimit (3) одинаковых вызовов подряд у одного агента (имя + аргументы без description) |
16, 22 |
agentGuards: false снимает хук целиком. Флаги volatile: правка в
cordis.patch.yml профиля применяется без перезапуска плагина. Галочек в
форме нет — это политика инстанса, а не вкус пользователя.
Журнал отказов — $DSH_HOME/dsh-tweaks/agent-guards.jsonl (JSON на строку,
ротация 1 МБ): ts, rule, tool, session, sub, reason, misses.
Значение секрета в журнал не пишется — только вид совпадения. Журнал читает
еженедельный разбор реестра инцидентов.
Ошибка самой проверки = пропуск вызова (fail-open) и строка guard-error;
git вызывается с таймаутом 3 с. Проверка: node test/check-agent-guards.cjs.
Диагностический выключатель
?tweaks=off выключает твики только для этой ссылки: параметр остаётся в
адресе, но в localStorage ничего не записывается. Обычная ссылка без параметра
всегда включает твики; старый сохранённый флаг снимается автоматически.
?tweaks=on тоже снимает legacy-флаг. Перезапуск DSH не нужен.
Мобильная панель твиков и версии
В подвале мобильной левой панели есть кнопка «Твики»: она открывает ту же форму
флагов, что и страница пакета, в собственной шторке — без перехода
Plugins → Волна твик. Под каждым переключателем указаны полные версии:
0.2.1-rc.5 и 0.2.2-rc.1. Твики, которые ядро 0.2.2-rc.1 уже делает само
(drawer, автоскрытие, клавиатура, центр, меню «…»), на нём не применяются.
Светофор регистрируется в conversation.input.dock на 0.2.1-rc.5 и
0.2.2-rc.1; старый conversation.composer.dock остаётся fallback для более
ранних сборок. На 0.2.2-rc.1 лампа стоит в conversation.composer.dock —
под полем ввода, первой перед статистикой ходов/токенов/контекста. По умолчанию
она показывает не текущие часы, а оставшееся время до смены тарифа
(2ч 15м, 17м); на красном — до зелёного. Панель «Дешёвые часы» открывается
у этой лампы. Вторым элементом в строке дока стоит переключатель «Спецсигнал»,
а подпись лампы (aria-label/title) — это статус: «ждёт зелёного до 13:00»,
«спецсигнал: работа на красном (вдвое дороже) до 13:00» или «вне пика — до пн
04:00».
Совместимость версий ядра
Твики определяют возможности DOM, а не номер версии. Если у ядра есть нативный
drawer (data-sidebar-drawer / drawerMask), мобильная левая панель остаётся
за ядром: наш слой не пишет fixed-правила и не накладывает вторую подложку.
freePanels в этом режиме держит первую колонку сетки нулевой, а zoom не
раздувает узкую рамку до 1024 px. На ядре без drawer работает наш выдвижной
слой и прежняя сетка.
Меню верхнего «…»: быстрый показ
Пункты (Copy session ID, Feedback, Download session log, виды) собираются в меню
до его появления; затем меню один раз проявляется на финальной позиции с
коротким сдвигом сверху вниз (130ms ease-out). Пока идёт сборка, меню скрыто,
поэтому оно не растёт и не дёргается после открытия.
Панель светофора: над лампой при любом масштабе
Панель «Дешёвые часы» рендерится порталом в body, вне зум-контекста #root.
Позиция считается в визуальных пикселях окна, а собственный transform:
scale() панели компенсирует масштаб интерфейса. Панель всегда садится низом
над лампой, прокручивается при нехватке места и пересчитывается при смене
масштаба/размера окна. Это исправляет уезжание за окно в Windows-приложении,
браузере и на телефоне.
Кнопка «отправить с Flash» (твик imageModelFallback)
Когда сервер отвергает отправку с причиной «модель не поддерживает изображения»
(например, в чате выбран DeepSeek-V4-Pro), обычный текст ошибки ничего не предлагает
сделать. Твик добавляет над полем ввода плашку с кнопкой Отправить с Flash:
- неудавшаяся отправка к этому моменту уже восстановлена в поле ввода (текст и
картинки), поэтому кнопка не теряет вложение; - клик переключает модель через штатный
modelDirectories.directoryFor(sessionId).select
(провайдер, модель и
уровень рассуждений берутся из настроек и текущего выбора сессии) и повторяет
ту же отправку публичнымinputActions.submit(); - причину отказа сервер присылает кодом
MODEL_DOES_NOT_SUPPORT_IMAGES— по нему
плашка и появляется, поэтому чужие ошибки она не перехватывает; - крестик скрывает плашку до следующего отказа.
Провайдер и модель-замена — служебные поля схемы Config (index.js), а не галочки:
imageFallbackProvider (по умолчанию deepseek-account), imageFallbackModel
(deepseek-flash) и imageFallbackLabel (Flash) — подпись на кнопке. Правятся
через патч профиля, если раскладка моделей на сервере другая.
Клавиши (твик zoom)
| Действие | Основной блок | Нумпад | Мышь |
|---|---|---|---|
| Крупнее | Ctrl + = |
Ctrl + + |
Ctrl + колесо вверх |
| Мельче | Ctrl + - |
Ctrl + − |
Ctrl + колесо вниз |
| Сброс 100% | Ctrl + 0 |
Ctrl + 0 |
— |
Shift не нужен: клавиша читается по позиции, поэтому Ctrl + Shift + = тоже
работает, и раскладка не важна. Шаг — 5%, диапазон — 50…200%. Размер показывается
плашкой в правом нижнем углу 1,2 с и хранится в localStorage (ключ dsh.ui-zoom.v1).
В браузере Ctrl + = / - / 0 забирает страница, поэтому там те же действия
доступны с Alt. Переназначить любую комбинацию: Settings → «Горячие клавиши».
Установка
plugin_manager action=install_bundle target=<путь к папке плагина>
Репозиторий: https://github.com/pridurok-goxa/volna-tweaks.
Windows:
git clone https://github.com/pridurok-goxa/volna-tweaks.git C:\volna-tweaks
Затем в DSH: plugin_manager action=install_bundle target=C:\volna-tweaks.
Вручную: добавить "volna-tweaks": "link:<путь>" в зависимости профиля, вписать
volna-tweaks в dsh.profile.bundles, затем pnpm install в папке профиля.
Как устроено
client.js— браузерная половина: реестрTWEAKS, чтение флагов, форма, код твиков.index.js— хост-половина: схемаConfigс флагами твиков и подключение
хост-частей (tweaks/whisper-host.js,tweaks/traffic-light-host.js).cordis.patch.yml— одна запись пакета (id: zoom); строка на странице Plugins
выключает пакет целиком, поэтому отдельные твики живут флагами, а не записями.locale/en.json,locale/ru.json— подписи карточки пакета (meta.title,
meta.description); блокmeta:вcordis.patch.ymlстраница Plugins не читает.
Включение и выключение твиков
Флаг твика — поле схемы Config в index.js, помеченное .volatile(): только такие
поля DSH показывает в настройках и принимает на запись, а их смена доходит до живой
записи без перезапуска. Имя поля = id твика в реестре TWEAKS в client.js.
Форму DSH не рисует — её рисует плагин. Галочки живут в слоте
plugins.bundle.config с ключом dsh-tweaks, то есть на странице самого пакета.
Клик по галочке сразу пишет значение (set с прочитанной ревизией) в клиентский
сервис configForms — тот же, откуда плагин читает флаги: namespace формы — id
записи в cordis.patch.yml, то есть zoom.
client.js держит один набор твиков на пакет: apply(ctx) читает флаги,
включает включённые, выключает выключенные и подписывается на изменения. Ресурсы
твика регистрируются через api.own и снимаются при выключении, поэтому повторное
включение не оставляет двойных обработчиков. Пока конфигурация не готова
(configForms нет или status !== 'ready'), работают дефолты реестра: zoom,
contextMenu и trafficLight включены, whisper — нет. В готовом снимке
отсутствие поля — это тоже дефолт реестра: пропавшее поле не включает твик,
выключенный по умолчанию.
Выключение zoom возвращает интерфейс к 100% (значение в localStorage остаётся,
включение вернёт прежний масштаб); попапы при этом возвращаются к исходным
координатам. Выключение contextMenu возвращает системное меню.
Новый твик: поле в Config (index.js), запись в TWEAKS (client.js) и, если у
твика есть код, свой регион в client.js — /* #region tweak- */. Соседей это
не задевает.
Хост-часть твика
Хост-часть твика (если нужна) — отдельный файл tweaks/-host.js, который
index.js подключает по флагу. Клиентская половина у пакета одна, отдельного
модуля на твик не бывает; клиентской части может не быть вовсе — у whisper её
нет, галочка включает хост-часть.
Плагин читает флаги через событие loader/volatile-update: смена одних только
volatile-полей не перезапускает плагин, а DSH компилирует новые значения в уже
выданную конфигурацию и уведомляет владельца записи. Значения volatile-полей
приходят ссылками Volatile (.get()), поэтому хост-часть получает развёрнутую
копию конфигурации обычными значениями. Жизненный цикл твика whisper: флаг
включён — activate(ctx, config); выключен — deactivate(); сменился
whisperModel у включённого — deactivate() и следом activate() с новыми
значениями (размер модели провайдер читает один раз при регистрации).
Переключения идут очередью, а выгрузка плагина ждёт deactivate() — Python-воркер
не переживает твик.
Жизненный цикл твика trafficLight проще: activate(ctx, config) ставит
слушателей и таймер, а смена значений политики приходит в update(config) без
перезапуска — удержанные на красном напоминания переживают правку окон.
deactivate() снимает слушателей, будит ожидающие вызовы и возвращает удержанное
в работу.
Модуль твика импортируется динамически и в try/catch: файла может не быть
(другая ветка, неполная поставка), и тогда причина уходит в лог, а схема Config
и остальные твики продолжают работать. Поле files в package.json включает
tweaks/**, поэтому Python-воркер (tweaks/worker.py) попадает в поставку.
При первом включении твика «речь» хост-половина один раз выставляет настройки
распознавания: после успешного activate (провайдер whisper-local уже
зарегистрирован) идёт ctx.speechToText.configure({ providerId: 'whisper-local',
language: 'ru' }), а затем в своё служебное volatile-поле whisperSelectionApplied
записывается признак «выставлено». Галочки у этого поля нет: форма строится из
реестра TWEAKS, а не из схемы. Дальше выбор не перезаписывается — сменил
пользователь провайдера или язык руками, configure больше не зовётся. Ошибка
(службы распознавания нет, язык не поддержан, служба настроек недоступна) уходит
в лог и твик не роняет; признак при этом не ставится, поэтому при следующем
включении попытка повторяется.
Светофор дорогих часов
Правило тарифов DeepSeek (сверено с официальной страницей цен,
ред. 2026-09-10): пиковая цена действует по будням 09:00–12:00 и 14:00–18:00
по Пекину (для Москвы это 04:00–07:00 и 09:00–13:00), кроме официальных
праздников КНР; всё остальное время — полцены. Полуинтервалы: граница 12:00 уже
вне пика, 14:00 — снова пик.
Индикатор — круглая лампа под полем ввода, самой первой в строке дока
(перед системными показателями): зелёный — полцены, жёлтый мигает
за выбранное время до пика, красный — пик. В шапке индикатора нет: она остаётся
за системными кнопками и заголовком чата. На красном при работающих сессиях
лампа дышит (плавно растёт от меньшего размера до нормального) и на каждом
максимуме роста от её края уходит мягкая волна — радиус ограничен 2.6 диаметра
лампы, поэтому волна не выходит за пределы индикатора. Панель открывается только по нажатию на лампу, раскрывается вверх от неё и никогда не уходит за край окна: если места над лампой мало, панель ограничивается по высоте и прокручивается внутри. Оформление повторяет
системный попап (беседа «Token usage»/контекст): матовый полупрозрачный фон
--dsw-specific-menu, блюр подложки --dsw-menu-backdrop-filter, системные тень и
скругление. Внутри — горизонтальная шкала суток: 24 кружочка от 0 до 24 часов в
местном времени разработчика, красные — дорогие часы, зелёные — дешёвые (в
выходные и праздники вся полоска зелёная), текущий час крупнее и под указателем
«сейчас». Жёлтых кружочков на шкале нет: жёлтой мигает только лампа, предупреждая о
приближении дорогого времени. Под каждым кружочком постоянно видна подпись его часа,
а часы, открывающие зелёный диапазон (после дорогого окна или с начала суток),
выделены жирнее — по ним читается структура дня: для московских окон это 00, 07 и 13.
Подпись под шкалой в покое показывает текущий час, при наведении — час под курсором,
формат один и тот же, поэтому ничего не появляется и не пропадает. Ниже — отсчёт до смены тарифа,
правило тарифов отдельными строками (будни и часы, праздники, текущий часовой пояс)
и строка «Смена тарифа» с адресом: например «пн 09:00 (через 60 ч)» — в пятницу
вечером следующее дорогое окно действительно в понедельник, потому что выходные
целиком дешёвые. Наведение на шкалу подсвечивает час под курсором и его статус: зона
наведения — вся полоска, поэтому на переходах между кружочками шкала не мигает.
Рядом с лампой — маленький переключатель «Спецсигнал»: в покое тускло-серая
точка, включённый — синяя с двойными вспышками, как проблесковый маячок
(при prefers-reduced-motion: reduce — статичный синий без анимации). Он
пускает этот чат и всё его дерево субагентов работать на красном (вдвое дороже)
и включается-выключается только человеком. Состояние живёт не в localStorage,
а в конфиге пакета (поле trafficLightSpecial), поэтому действует на всех
устройствах и переживает рестарт DSH. Начальное значение — чат
session-e4698bf8-717f-4e1a-9390-a2b40c03d985: включено по решению Игоря
(t1447u), он работает на красном вместе со своим деревом. В панели та же кнопка
стоит строкой
«Спецсигнал: вкл/выкл» с пояснением, что чат и команда работают на красном, а
строка статуса отвечает на вопрос «чего и до каких пор ждём» — см. «Пауза на
красном». Если запись не сохранилась (настройки успели поменять в другой
вкладке), кнопка один раз перечитывает снимок и повторяет запись, а на 5 секунд
показывает подсказку «Спецсигнал не сохранился…».
Настройки твика — на странице пакета рядом с галочкой, свои у каждого браузера
(лежат в localStorage, ключ dsh.ui-light.settings.v1, хост-половине они не нужны):
| Настройка | Значения |
|---|---|
Нижний индикатор (dockMode) |
«Только индикатор» · «Индикатор и время» (по умолчанию) · «Индикатор и оставшееся время» · «Индикатор, время и тариф» |
Предупреждение (warningMinutes) |
за 10 (по умолчанию), 20 или 30 минут до пика |
Умолчания одинаковы для всех: твик включён, нижний индикатор — «Индикатор и
время», предупреждение — за 10 минут до пика. Личные правки в браузере
перекрывают умолчания только у того, кто их сделал: это настройки показа, паузы
и спецсигнал от них не зависят.
Пауза на красном
На красном встают все ходы без спецсигнала, а на зелёном они продолжаются
сами — без нажатий и повторов:
- модельный вызов (
llm/stream) любой сессии ждёт зелёного: цель, итоги
субагентов (subagent-settled), сообщения команды (agent-messageи
team-message), расписания, внешние сообщения. Входящие не изымаются — их ход
просто ждёт, поэтому ничего не теряется; - сообщение расписания (
schedule) на красном снимается из входящих агента и
возвращается на зелёном (sessionController.resolveAgent+agent.followup):
памятка лежит в$DSH_HOME/dsh-tweaks/held.json, доставка повторяется до 8 раз
и переживает рестарт DSH; - пустой ход (доставку изъяли, а ход всё равно открылся) на красном отклоняется:
модельного вызова и трат нет. У сессии со спецсигналом такой ход не
отклоняется; - служебные вызовы с
purpose(заголовок чата, компакция) работой хода не
считаются и проходят сразу; - пропуск даёт только переключатель «Спецсигнал»: он пускает сессию и всё её
дерево субагентов и субсубагентов, действует через любое число красных окон,
пока человек его не выключит. Набор берётся из
$DSH_HOME/dsh-tweaks/special.json(запись атомарная: tmp + rename), затем из
конфига, затем из seed — поэтому выключение тоже переживает рестарт: пустой
список пишется как «выключено», а не как «файла нет». На старте файл
авторитетнее конфига: если они разошлись, факт расхождения пишется в лог
строкойstate-conflict, а гейт идёт по файлу. В работе набор меняет
переключатель — его запись ложится и в конфиг, и в леджер; - сообщения пропуск не дают — ни человеческий промпт, ни промпт
continuable-субагента, ни стиринг родителя:harness-relayи
deputy-wake-prompt.pyшлют тот жеclientTimeZone(константу
Europe/Moscow), ту же owner-cookie и байт-в-байт такой жеsource,
principalв этой установке не выставляется никому, а IP/UA в сессию не
пишутся. Поэтому ночью человек встаёт вместе со всеми, а если работает —
включает спецсигнал. Отдельно:gate2-verifyразличает человека сильнее —
он смотрит access-лог nginx (браузерный POST с внешнего адреса иMozilla),
а relay и watchdog ходят на127.0.0.1мимо nginx. Следующий шаг усиления —
principalдля сервисных учёток хаба или транспортный хук
connection/request; сейчас это не делается; - ночной сценарий МСК: 04:00 — пауза, 07:00 — работа продолжается сама, 09:00 —
снова пауза, после 13:00 — дешёвое время. Окна, праздники КНР и жёлтое
предупреждение берутся из политики выше; - статус виден в панели (
ждёт зелёного до 13:00,спецсигнал: работа на красном (вдвое дороже) до 13:00,вне пика — до пн 04:00) и в подписи лампы;
его считает браузер по политике и конфигу — своего RPC у твика нет; - диагностика:
$DSH_HOME/dsh-tweaks/traffic-light.log. Прежние строки
trafficLight: …(свет, удержание, доставка) не изменились; рядом появились
структурированные события:pause | session=… | until=…,
resume | session=… | after=green,
signal | session=… | state=on|off | actor=ui-config|seed|config|file | rev=N,
state-conflict | file=N | config=N | authority=file,save-error | reason=…,
signal-less | reason=no-initiator. Journal для этого не нужен.
Масштаб
zoom ставится корню приложения (#root). Размеры корня не подменяются: движок сам
отдаёт содержимому логическую ширину «ширина / масштаб», и интерфейс заполняет окно.
transform: scale не подходит — с ним расходится попадание курсора в элементы.
Настройку «Font size» в Settings плагин не трогает: она меняет только текст переписки.
Попапы
Меню и подсказки приложение монтирует порталами в body, мимо корня. Общий zoom до них
не достаёт, поэтому каждый слой получает свой, а координаты плагин заворачивает в формулу:
/* приложение пишет --dsh-ui-zoom-src-left: 270px */
left: calc(var(--dsh-ui-zoom-src-left) / var(--dsh-ui-zoom));
Делит браузер, а не JS. --dsh-ui-zoom лежит на корне документа, поэтому смена масштаба
пересчитывает все попапы разом. JS-деление давало дрейф: на нецелом масштабе частное —
периодическая дробь, браузер округляет inline-длину, и меню уезжало в разные места экрана.
offsetWidth и offsetHeight попапов плагин отдаёт приложению в вьюпортных пикселях:
по ним оно выравнивает меню по кнопке и подгоняет его под экран. При смене масштаба
плагин шлёт resize — якорь переехал.
Меню по правой кнопке
Системное меню рисует Electron, и zoom до него не достаёт. Плагин перехватывает правый
клик и показывает своё: в поле ввода — Undo, Redo, Cut, Copy, Paste, Select All,
при выделении вне поля — только Copy. Вставка идёт через Clipboard API.
Своё меню — обычный слой в body, поэтому масштаб ему отдаёт твик zoom через мост
(пока твик выключен, мост молчит: масштабировать нечего). Фон и текст — токены темы.
Подписи берутся через translate(): штатный переводчик отдаёт сам ключ, если словарь
не успел подключиться, и в меню появлялось menu.copy.
Полоски изменения ширины
Приложение прибавляет к ширине панели сдвиг e.clientX - origin, а ширина — в логических
пикселях. Курсор визуальный, поэтому плагин отдаёт событиям на полоске логический
clientX: иначе на 150% панель проходит в полтора раза больше курсора.
Позицию полоска получает как left: viewport - rightbar. Под zoom эта арифметика
расходится с сеткой — при 125% полоска стояла на 1078 px, а граница панели на 1202 px.
Поэтому плагин ставит полоску по границе колонки: offsetLeft и offsetWidth.
Рамка интерфейса
AppFrame измеряет себя через getBoundingClientRect() и из этой ширины считает колонки.
Под zoom движок отдаёт визуальную ширину, поэтому плагин возвращает логическую. Ниже
1024 логических пикселей не опускает: по этому порогу приложение сворачивает сайдбар
(SIDEBAR_AUTO_COLLAPSE). Подмена живёт на уровне пакета и идемпотентна, поэтому
повторное включение твика не делит замер дважды.
Ограничения
- Меню
ApplicationиEditрисует Windows — плагин до них не достаёт. - Чем крупнее масштаб, тем раньше сворачивается сайдбар:
zoomуменьшает логическую
ширину, а порог у приложения — 1024 логических пикселя. При окне 1180 px панель
сворачивается примерно со 120%. - Ширина правой панели хранится в пикселях, поэтому с ростом масштаба занимает большую
долю окна. Меняется перетаскиванием полоски. - Вставка в своём меню зависит от доступа к буферу обмена.
- Длинное меню ограничивается по высоте в
vh, аvhзум не масштабирует. - На не-loopback Web служба конфигурации недоступна (
status: 'unavailable'): там
галочки заблокированы, а твики работают на дефолтах. В Desktop и на loopback всё в порядке. @deepseek-ai/schemasteryобъявлен вpeerDependencies: пакет берётся из установки
DSH, отдельная установка не нужна. Правкаindex.jsподхватывается после перезапуска
приложения — HMR по умолчанию следит только за файлами профиля (root: []).- Мобильное меню держится за те же хрупкие привязки, что и свободные панели:
суффиксы хешированных классов (_frame,_sidebarCol,_rowActions) и
data-атрибуты (data-sidebar-collapsed,data-row-key,data-queue-dock). При
крупной смене вёрстки DSH режим не включается (нет рамки или колонки) и интерфейс
остаётся штатным; проверить это можно по атрибуту
html[data-dsh-tweaks-mobile-drawer]. - Подавление фокуса после тапа по чату — на время твика подменённый
HTMLElement.prototype.focus: программный фокус поля ввода глотается, нативный
тап по полю работает. Desktop-поведение не меняется. - Enter в редакторе очереди остаётся переносом строки только на touch-устройстве;
на desktop Enter по-прежнему сохраняет сообщение. - Клавиатура при «+» и
@не скрывается (по просьбе): меню открываются поверх
неё. Тап вне меню закрывает его, чтобы оно не залипало. - Подчёркивание последнего слова снимается через
spellcheck=falseи прозрачную
рамку композиции; на части Android-клавиатур системное подчёркивание IME может
остаться — это поведение клавиатуры, не приложения. - Реальную экранную клавиатуру iOS/Android DOM-тест не воспроизводит: после
обновления страницы нужна проверка человеком на телефоне. - На планшете с открытой правой панелью раскрытый ящик заставляет приложение
пересчитать трек панели по развёрнутому сайдбару; твик берёт трек из сетки
приложения как есть и не восстанавливает прежнюю ширину. На телефоне правая
панель в узкой раскладке места не получает, поэтому это не проявляется. - Чистка шапки — DOM-надстройка: второстепенные кнопки скрыты невидимыми
анкорами, а в меню верхнего «…» добавляются пункты-прокси и «Copy session ID».
Подписи прокси берутся изaria-label/title исходных кнопок; при смене вёрстки
DSH прокси могут не появиться, а сами кнопки останутся скрытыми до правки
селекторов. Меню видов (Browse other plugins / Create your plugin) вставляется
после чтения скрытого меню, поэтому его подписи зависят от локали приложения. - Поиск в маленьких меню убирается эвристикой: поле поиска скрывается, если в
меню не больше шести пунктов. Если в новой вёрстке поиск нужен маленькому меню,
правило придётся сузить. - Фильтр «+» держит подписи пунктов en/zh (
File/文件,Goal/目标,
Plan/计划) — при смене набора это единственная точка правки. Чип режима
подписывается по имени claim (plan→ «Plan»). - «Copy session ID» пишет в буфер обмена через примитив оболочки с fallback на
navigator.clipboard; без разрешения на буфер пункт остаётся, но копирование
может не сработать. - Светофор на красном останавливает все ходы, включая ручные: сообщение
человека, агента, сервиса, субагента и relay пропуска не дают — отличить
человека от сервиса по сообщению нельзя (см. «Пауза на красном»). Ночью
работают через спецсигнал. - Спецсигнал включается и выключается только кнопкой, разового пропуска нет.
Кнопку технически может нажать любой клиент с owner-cookie: служба
конфигурации не передаёт принципал (configForms), поэтому в лог пишется
actor=ui-config, а не имя человека. Усиление — свой typert-неймспейс либо
principalдля сервисных учёток хаба; сейчас это не делается. - Дочерний чат показывает базовый статус: наследование спецсигнала от родителя
клиенту не видно, поэтому в нём будет «ждёт зелёного», хотя работа идёт. - Родословная читается только у живых агентов: если промежуточная сессия
выгружена, цепочка обрывается и потомок ждёт зелёного (fail-closed). - Спецсигнал не имеет срока: он действует через любое число красных окон, пока
его не выключат, и всё это время работа идёт по двойной цене. - Расписания удерживаются и возвращаются на зелёном; остальные сообщения не
изымаются — их ход просто ждёт. - Служебные вызовы с
purpose(заголовок чата, компакция) на красном не ждут. - Леджер
special.jsonможет не записаться (save-error, повтор на каждом
пересчёте): в памяти и в интерфейсе остаётся значение конфига, а после
перезапуска авторитетным снова становится файл — расхождение при старте видно
строкойstate-conflict. Направление «файл ON, конфиг OFF» означает работу по
двойной цене при статусе «ждёт зелёного». - Единственный обход гейта без кнопки — вызов, у которого не определяется сессия
(id === ''): он проходит и пишетsignal-less. Промпты человека и сервисов
всегда несут сессию, так что путь остаётся страховкой от ложной блокировки. - Скриншоты светофора снимает руководитель на своём стенде: headless-браузера на
хосте нет.
Проверка
node --check client.js
node --check index.js
node test\check-client.cjs
node test\check-whisper.cjs
node test\check-host.cjs
node test\check-traffic-light.cjs
node test\check-mobile-composer.cjs
node test\check-mobile-sidebar.cjs
node test\check-agent-guards.cjs
test/check-client.cjs гоняет настоящий client.js в песочнице с заглушками DOM,
React, ctx и configForms: 95 проверок; мобильные регионы — ещё 36 + 13 + 8 + 7 + 5 + 4 + 2 контрактов — флаги включают и выключают твики,
твик без клиентского кода (whisper) включается и выключается галочкой, повторное
включение не копит обработчики, дефолты без службы конфигурации, форма пишет
значения (операция set + ревизия), шаг и границы масштаба, хранилище, команды и их
привязки, плашка, Ctrl + колесо, попапы (перехват координат, calc(), сигнал
resize, отсутствие дрейфа), полоски, рамка, меню по правой кнопке, режим
выдвижного мобильного меню (узкая/широкая рамка, тап по чату, «три точки») и
спецсигнал светофора: различимые маркеры у лампы и в панели, подпись статуса в
aria-label лампы, блокировка кнопки без сессии и без права записи, повтор после
устаревшей ревизии, reduced-motion, дочерний чат с базовым статусом и паритет
ключей в обоих словарях.
test/check-host.cjs проверяет хост-половину без установки DSH: index.js грузится
с заглушкой @deepseek-ai/schemastery, флаг поднимает настоящий
tweaks/whisper-host.js, смена модели перезапускает его, выгрузка снимает, а
пропажа файла хост-части не роняет плагин. Там же — разовая настройка
распознавания: первое включение зовёт configure ровно один раз и сохраняет
признак в своё поле схемы, повторный запуск с готовым признаком выбор не трогает,
падение configure и отсутствие службы распознавания твик не роняют.
test/check-whisper.cjs — контракт самой хост-части.
test/check-traffic-light.cjs проверяет светофор v2 на моке контекста: политику
окон и её копию в client.js (дрейфа нет), гейт на красном (ждёт любой ход,
включая человеческий промпт и итоги субагентов), наследование спецсигнала по
дереву и пределы подъёма, удержание и возврат расписаний, пустой ход, purpose,
id === '' (signal-less), персистентность леджера (файл → конфиг → seed,
пустой список = выключено, state-conflict, save-error с повтором), строки
лога и seed session-e4698bf8-… в схеме. Отдельно — ассерты автономного стенда.
test/traffic-light-stand.html — стенд светофора v2: открывается локально
(file://), без сети и сборки, показывает все состояния сразу — зелёный, жёлтый,
красный с ожиданием, красный со спецсигналом, отказ записи, reduced-motion — и
таблицу «состояние → ожидание». Скриншоты по нему снимает руководитель;
машинные ассерты стенда живут в test/check-traffic-light.cjs.
test/zoom-stand.html прогоняется в headless Edge в реальном времени: --dump-dom
отдаёт страницу до конца её асинхронной работы, а --virtual-time-budget ломает
порядок ResizeObserver и даёт ложный сбой на перетаскивании полоски. Стенд прогоняет
связку «якорь — портал» на всех масштабах 0.5…2.0 и четырёх раскладках, отдельно
проверяет полоску панели, перетаскивание и меню под курсором.
Мобильный composer (mobileComposer)
Входит в существующий пакет dsh-tweaks (та же галочка Settings → Plugins).
Включён по умолчанию; desktop Enter/Shift+Enter не меняются. Мобильное устройство
определяется через (pointer: coarse) или userAgentData.mobile, а не ширину окна.
На таком устройстве и физическая клавиатура работает в безопасном мобильном режиме.
Слот conversation.input.overlay привязывает capture-обработчики только к своей
карточке [data-composer-card]. Публичные InputActions.captureInsertion/insertText
сохраняют выделение, ссылки, защиту ревизии, санитаризацию и undo. Enter и
beforeinput-only insertParagraph/insertLineBreak не попадают в upstream submit.
IME-подтверждение не превращается в перенос строки. Текстовый paste и
beforeinput-only insertFromPaste сохраняют весь текст, пустые строки и tabs;
CRLF/CR нормализуются в LF. Картинки и смешанный paste с файлами остаются штатными.
Никаких изменений минифицированного HARNESS, приватного Lexical API или отправки
сообщения из твика нет. При отключении/размонтаже обработчики снимаются.
Штатный режим и многоабзацная вставка
Используется штатный mode/claim API HARNESS 0.2.1-rc.5:
inputTriggers.sessionOf(actx).resolveClaim,
slash/input-begin-command, slash/input-consume-token, InputActions.
Выбор через «+» создаёт штатный claim, не вставляет буквальный /plan в тело.
Штатный atomic node скрыт только визуально внутри редактора; тот же режим показан