Волна твик (dsh-tweaks)
Твики DeepSeek Harness одним пакетом. Каждый включается своей галочкой на странице пакета (Settings → Plugins → «Волна твик») и выключается там же — без перезапуска.
| Твик | Что делает |
|---|---|
fileDownload |
Скачивание файлов: долгое нажатие на файловой карточке в чате и пункт «Скачать» в меню вкладки файла правой панели |
queueEditor |
Единая форма редактирования очереди: текст и вложения, добавление/удаление, сохранение на прежнем месте; host-роут в tweaks/queue-edit-host.js |
mobileComposer |
Мобильный ввод: Enter/paste без отправки, «+» только File/Goal/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.
Кнопка «отправить с 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=<путь к папке плагина>
Вручную: добавить "dsh-tweaks": "link:<путь>" в зависимости профиля, вписать
dsh-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-<id> */. Соседей это
не задевает.
Хост-часть твика
Хост-часть твика (если нужна) — отдельный файл tweaks/<id>-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 ч)» — в пятницу
вечером следующее дорогое окно действительно в понедельник, потому что выходные
целиком дешёвые. Наведение на шкалу подсвечивает час под курсором и его статус: зона
наведения — вся полоска, поэтому на переходах между кружочками шкала не мигает.
Настройки твика — на странице пакета рядом с галочкой, свои у каждого браузера
(лежат в localStorage, ключ dsh.ui-light.settings.v1, хост-половине они не нужны):
| Настройка | Значения |
|---|---|
| Нижний индикатор | только индикатор · индикатор и время, без «ещё» и адреса (по умолчанию) · индикатор и оставшееся время (с адресом) · индикатор, время и тариф |
Умолчания одинаковы для всех: твик включён, нижний индикатор — «Индикатор и время», предупреждение — за 10 минут до пика. Личные правки в браузере перекрывают умолчания только у того, кто их сделал. | Предупреждение | за 10 (по умолчанию), 20 или 30 минут до пика |
Пауза на красном
На красном свете твик приостанавливает автоматическую работу и удерживает расписания, а на зелёном возвращает их сам:
- сообщение расписания снимается из входящих агента и возвращается на зелёном
(
sessionController.resolveAgent+agent.followup); ручные сообщения разработчика проходят всегда — на красном можно работать; - автоматические шаги (и субагенты автоматического родителя) ждут зелёного на
ближайшем модельном вызове (
llm/stream); шаг классифицируется по сообщениям-триггерам, а служебные вставки (runtime-context,time-context,skill-catalog,repeat-tool-reminderи другие) игнорируются — иначе каждый шаг выглядел бы ручным; - пустой ход, открытый после изъятого расписания, отклоняется: модельный вызов в красное время не выполняется и токены не тратятся;
- удержанное переживает перезапуск DSH: памятка лежит в
$DSH_HOME/dsh-tweaks/held.json, доставка повторяется до 8 раз, пока свет зелёный и сессию можно поднять; - диагностика: файл
$DSH_HOME/dsh-tweaks/traffic-light.log— одна строка на событие (свет, удержание, доставка, паузы), 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; без разрешения на буфер пункт остаётся, но копирование может не сработать.
Проверка
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
test/check-client.cjs гоняет настоящий client.js в песочнице с заглушками DOM,
React, ctx и configForms: 75 проверок; мобильные регионы — ещё 36 + 13 + 8 + 7 + 5 + 4 + 2 контрактов — флаги включают и выключают твики,
твик без клиентского кода (whisper) включается и выключается галочкой, повторное
включение не копит обработчики, дефолты без службы конфигурации, форма пишет
значения (операция set + ревизия), шаг и границы масштаба, хранилище, команды и их
привязки, плашка, Ctrl + колесо, попапы (перехват координат, calc(), сигнал
resize, отсутствие дрейфа), полоски, рамка, меню по правой кнопке и режим
выдвижного мобильного меню (узкая/широкая рамка, тап по чату, «три точки»).
test/check-host.cjs проверяет хост-половину без установки DSH: index.js грузится
с заглушкой @deepseek-ai/schemastery, флаг поднимает настоящий
tweaks/whisper-host.js, смена модели перезапускает его, выгрузка снимает, а
пропажа файла хост-части не роняет плагин. Там же — разовая настройка
распознавания: первое включение зовёт configure ровно один раз и сохраняет
признак в своё поле схемы, повторный запуск с готовым признаком выбор не трогает,
падение configure и отсутствие службы распознавания твик не роняют.
test/check-whisper.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 скрыт только визуально внутри редактора; тот же режим показан
снимаемым чипом в conversation.input.left (компактная подпись «Plan» в строке под полем). Persist/submit/undo остаются у HARNESS:
его отправка сама применяет режим к полному телу, у твика нет своего send handler.
Это работает на desktop и телефоне; desktop Enter/Shift+Enter не перехватываются.
Ведущий /plan при вставке превращается в один claim, полный хвост с пустыми
абзацами остаётся в теле. Пустой уже выбранный режим не дублирует вставленный token.
Полный текст пишется атомарно через ревизионно защищённый insertText; поздний ответ
каталога не затирает новые правки. Неизвестная slash-команда остаётся целым текстом.
Android-ветки beforeinput insertText/insertCompositionText/insertFromComposition
также принимают многострочный payload без clipboardData; noncancelable input
согласуется с моделью на следующем input. Для Notes поддержан HTML-only буфер,
а также доказуемо обрезанный plain (первый абзац — точный префикс полного HTML).
HTML разбирается вне документа, script/style игнорируются.
«+» сохраняет выделение и показывает только File/Goal/Plan:
Feedback и секция Commands уже есть в других панелях. Набранный / меню не
открывает (тир claimed у детектора триггеров), поэтому slash-команды доступны
через «+», а не через ввод. Клавиатура при «+» и @ остаётся на месте: меню
открываются над ней, а вне меню тап/Escape его закрывают. Меню с поиском и не
более чем шестью пунктами поиск теряет (фокус уходит на первый пункт).
Вызов нативного меню использует только начальный token-span, а не выделение/конец
длинного текста: иначе штатный beginCommand заменяет диапазон [0, span.end) и
может удалить всё тело. На touch-платформе текстовый scrollport ограничен 20%
visualViewport.height (вдвое меньше прежнего), панель кнопок не сжимается,
меню привязано к «+» и ограничено видимой областью. Enter в любом textarea на touch — перенос строки, а не submit/next: это
касается и своего варианта ответа в вопроснике, и очереди, и goal-редактора.
Отправка/сохранение остаются кнопками.
У поля на touch выключены
spellcheck, autocorrect и рамка IME-композиции: последнее слово больше не
подчёркивается до пробела/Enter. Если клавиатура всё же держит слово в
композиции, первый Enter фиксирует его и сразу добавляет перенос: твик ждёт
compositionend и вставляет \n, только если клавиатура не сделала это сама.
Поэтому список без знаков препинания пишется одним Enter на строку. После программной вставки (Enter, paste) твик сам
фокусирует редактор и прокручивает [data-input-scroll] к каретке. Учитываются
экранная клавиатура и существующий твик масштаба (включая 150%). Ограничения
геометрии не применяются к desktop. При выключении всё снимается.
Очередь отложенных сообщений
Окно «редактировать отложенное сообщение» (очередь под чатом) — штатный
InlineEditor: <textarea>, на котором Enter без Shift сохраняет сообщение.
На touch-устройстве твик ставит capture-подписку на keydown документа и для
Enter внутри [data-queue-dock] останавливает только всплытие, не preventDefault:
браузер сам вставляет перенос строки, React-обработчик сохранить не успевает.
Сохранение остаётся кнопке-галочке. Desktop не затронут, IME не перехватывается;
обычные textarea (например, инструкция задачи расписания) не трогаются.
Редактор goal на touch растянут на всю ширину экрана: textarea выше (120 px, до 36vh), кнопки справа стоят вертикально — крестик сверху, галочка снизу. Enter в этом textarea вставляет перенос строки, сохранение — только галочкой.
Застрявший pending-echo очереди (Sending... с выключенными действиями) твик
лечит как recovery: через 2.5 с карандаш становится доступен, по клику строка
снимается из локальной очереди, а её текст возвращается в поле ввода — можно
отредактировать и отправить заново. Это срабатывает только для локального echo
без вложений; обычная строка durable-очереди по-прежнему редактируется штатным
инлайн-редактором.
Проверка
node test/check-mobile-composer.cjs
# Зависимости тестового DOM можно поставить отдельно, не меняя production profile:
npm install --prefix /tmp/dsh-composer-test happy-dom@21.0.4 react@19.3.1 react-dom@19.3.1 zod@4.6.7
MOBILE_TEST_RUNTIME=/tmp/dsh-composer-test \
HARNESS_COMPOSER_CLIENT=/path/to/installed/dsh-client-ui-conversation/client.js \
node test/check-composer-mode.cjs
Интеграционный тест исполняет установленную модель composer/Lexical с нативными
claim и отправкой в локальный stub: тестовый текст 3084 символа с ведущим /plan,
пустыми абзацами, Android beforeinput/composition, Notes HTML, отсутствие дублей,
сохранение тела при выборе/снятии режима, 412×915, экранная клавиатура, 150% zoom,
desktop-клавиши, асинхронная гонка и снятие обработчиков.
Это DOM-симуляция, не реальный браузер/телефон: точный trace Яндекс-клавиатуры
не получен, обычный ClipboardEvent upstream сам по себе сохраняет все абзацы.
Реальное Android/iOS поведение требует проверки человеком после обновления страницы.
Мобильное меню (mobileSidebar)
Входит в тот же пакет и включается галочкой; хост-части нет. На узкой рамке
(< 1024 логических px — порог SIDEBAR_AUTO_COLLAPSE) первый трек сетки DSH
становится нулевым, а колонка панели уезжает за левый край (position: fixed,
transform: translateX(-102%)). Чат и карточка композера получают
--dsh-chat-content-width: 100%, поэтому центральная панель занимает всю ширину.
Центральная панель на телефоне чуть компактнее: боковые поля ленты 8 px, пузырь сообщения 8×12 px, пользовательские сообщения до 94% ширины, композер шире (side clearance 4 px). Вкладки Chat/Trajectory и Team/Subagents/Job стоят плотнее, чтобы в шапку входило больше.
Шапка на телефоне собирается общим слоем conversation.header.leading:
[меню] [название чата], справа — верхнее «…» и кнопка правой панели (светофор
в шапке не рисуется: он живёт под полем ввода).
Меню — 28×28 с иконкой 15 px, как у правой панели; штатные крошки скрыты, название
рисует сам твик. Иконка меню не скрывается на время открытия/закрытия ящика: она
всегда стоит в левом верхнем углу центральной панели и проявляется, как только
колонка уезжает. Верхнее «…» прижимается к видимой области: примитив иногда ставил
его ниже innerHeight, теперь позиция фиксируется по кнопке и клавиатуре. Второстепенные действия/утилиты и меню видов скрыты, их кнопки
остаются невидимыми анкорами у правого верхнего края.
Верхнее «…» становится единственным меню шапки: в него добавляются «Copy session
ID» (id берётся из data-conversation-session), прокси к скрытым действиям
(Agent Team, фоновые задачи и т.п.) и отдельные пункты скрытого меню видов
(Browse other plugins, Create your plugin). Прокси нажимают исходные кнопки, поэтому
логика приложения не дублируется.
Открытие и закрытие — штатное layout.toggleSidebar(); вызов при открытом ящике
превращается в анимированное закрытие: ящик уезжает за 0.3 s (как правая панель),
и только затем приложение меняет содержимое — рельса не мелькает. Тап мимо ящика
(вне портальных меню) закрывает его тем же путём; если pointerdown не дошёл,
страховка на pointerup всё равно закрывает. Запоздавший toggle во время
анимации игнорируется, а завершение закрытия проверяет состояние панели: если
приложение уже свернуло её само (навигация по чату), повторного toggle нет —
ящик не «прыгает» и не открывается сам. По тапу на строку чата твик:
- снимает фокус с поля ввода, если он там был;
- взводит подавление программного фокуса:
HTMLElement.prototype.focusна время окна не даёт приложению поставить каретку и поднять клавиатуру. Любой следующий жест снимает подавление, нативный тап по полю снова фокусирует; при выключении оригинал возвращается; - запускает анимированное закрытие ящика.
Сетку твик не пишет в inline-стиль: он читает gridTemplateColumns приложения
(включая записи freePanels) и подставляет трек правой панели в своё правило
!important. Поэтому свободный размер панелей не конфликтует, а трек правой
панели не подменяется нашим правилом. Режим включается атрибутом
html[data-dsh-tweaks-mobile-drawer="on"] только если найдены и рамка, и колонка
сайдбара: при смене вёрстки DSH панель остаётся штатной, а не исчезает.
Глобальные панели (Plugins, Automation tasks, Статус Volna, More) из тела панели
убраны: _panelList скрыт, а в sidebar.footer.action рядом с «Signed in…» стоит
нижняя иконка. Она открывает тот же список поповером над подвалом; строки и More
остаются штатными. Поповер закрывается повторным кликом, кликом мимо, Escape,
выбором панели и закрытием ящика.
Вкладки правой панели на телефоне: выбор больше не запускает плавную прокрутку
полосы (scroll-behavior:auto), поэтому нет «уезжающей не в ту сторону» анимации.
Короткий горизонтальный свайп по полосе вкладок или по телу панели листает вкладки
(влево — следующая, вправо — предыдущая). Чтобы перенести вкладку, её надо зажать
на секунду: до этого движения гасятся и штатный drag не стартует; после удержания
dockkit получает движение и переносит чип как обычно. Иконки разделения экрана и
разворота во весь экран в мобильной правой панели скрыты.
Вопросник ask_user_question на телефоне растянут на всю ширину: рамка без
боковых отступов, карточка [data-question-key] > section шириной 100%.
Вопрос, пояснение и варианты прокручиваются одним полотном (карточка — скроллер,
[data-question-scroll] без собственного overflow), а футер Skip/Next/Submit
закреплён снизу. Поэтому длинный вопрос больше не съедает площадь вариантов.
В строке чата на мобильном остаётся только меню «три точки»: вложенная кнопка архива/удаления скрыта (она есть в меню), точки прижаты к правому краю, а время и закрепление уступают им место. Вложенные чаты сдвинуты на 4 px левее, чтобы названиям оставалось больше места.
«Три точки» строки чата и результата поиска на мобильном видны без наведения
(_rowActions); в строке чата время и метка закрепления уступают им место. Клик
внутри _rowActions твик не считает выбором чата: меню открывается штатно, панель
не закрывается. Навигация по панелям и «новая сессия» панель закрывают.
Проверка
node test/check-mobile-sidebar.cjs
node test/check-mobile-tabs.cjs
node test/check-queue-edit.cjs
node test/check-queue-editor.cjs
node test/check-download.cjs
node test/check-file-download.cjs
check-mobile-sidebar.cjs — контракт региона без браузера: треки сетки, выбор
чата против «трёх точек», подавление фокуса и его откат, атрибут режима,
поповер панелей, полная ширина, шапка и якоря CSS. Реальное поведение телефона
(жесты, клавиатура, iOS/Android) проверяется человеком после обновления страницы.
No comments yet. Be the first to write one.