tensorgrid-ui
v2.65.0
Published
TENSOR GRID — фирменный интерфейс поверх DeepSeek Harness: своя палитра и айдентика, полная русская локализация, независимый обзор работы несколькими моделями, живой каталог моделей, установка и откат одной командой
Readme
TENSOR GRID
Фирменный продукт поверх DeepSeek Harness: своя палитра и айдентика, полная русская локализация, независимый обзор работы несколькими моделями, живой каталог моделей, установка и откат одной командой.
Ставится плагином, а не форком: ни один поставочный компонент не замещается, поэтому обновления dsh продолжают доезжать до интерфейса.
Инструкция для того, кто ставит впервые, — docs/установка.html.
Как выпускать — docs/ВЫПУСК.md. Как переходить на новую dsh —
docs/ОБНОВЛЕНИЕ-DSH.md. План работ — docs/ПЛАН-РАБОТ.md.
Про имя. Репозиторий и папка до сих пор называются
dsh-ui-obsidian-ion— так проект звался, когда был одной лишь палитрой. Переименование тянет за собой адреса и историю, поэтому отложено. Сам продукт, пакетtensorgrid-uiи всё, что видит человек, называются TENSOR GRID.
Из чего состоит
| Файл | Роль |
|---|---|
| package.json | секция dsh.client — по ней рантайм понимает, что у пакета есть браузерная половина |
| lib/index.js | стабильная host-оболочка: маршруты, иконки, манифест и получение Cordis-служб |
| lib/http-runtime.js | перечитываемые HTTP-обработчики входа и обновлений |
| lib/auth-bridge.js | состояние разговора входа |
| lib/update-check.js | проверка версий и установка из реестра |
| lib/client.js | браузерная половина: палитра, стили, компонент слоя |
lib/client.js написан руками в том же формате, что и поставочные
@deepseek-ai/dsh-client-ui-*: регистрация в window.__ModuleLoader__,
зависимости через require, экспорт apply и inject. Сборщик не нужен.
Что пакет занимает
Только аддитивные слоты — те, у которых replaceRisk: none. Ни один
поставочный компонент не замещается, поэтому улучшения dsh продолжают
доезжать до интерфейса. Это условие закреплено в тесте белым списком.
| Слот | Вид | Что даёт |
|---|---|---|
| shell.overlay | list | ambient-слой: сферы, проход, кромка, зерно, виньетка, отклик |
| settings.general.item | list | строка «Атмосфера» — регулятор интенсивности |
| settings.general.item | list | строка «Акцент» — выбор ведущего тона |
| conversation.input.dock | list | невидимый драйвер отклика на работу агента |
| conversation.hero.brand.mark | single | знак на экране пустой сессии |
Плюс слой переопределения токенов через theme.overrideTokens — публичный
метод сервиса темы.
Две CSS-переменные вместо связи компонентов
--dsx-intensity— пользовательский регулятор (0 / 0.5 / 1 / 1.7), умножает непрозрачность всех слоёв. Хранится вlocalStorage.--dsx-activity— 0 в покое, 1 пока идёт ход агента.--dsx-accent-rgbи--dsx-accent2-rgb— ведущий тон и контрапункт выбранного акцента, тройками каналов дляrgba(var(…), α).
Смена акцента бьёт по двум сторонам сразу: свечение ведут переменные, а
--dsw-alias-brand-primary переписывается повторным вызовом
overrideTokens с тем же источником — служба документирует это как замену
всего слоя целиком.
Переменная на documentElement — единственный мост между двумя поддеревьями:
ambient-слой живёт в корневом shell.overlay, у которого нет сессии, а хук
useSession раздаётся только сессионным слотам. Драйвер сидит в сессионном
слоте, ничего не рисует и лишь ведёт переменную.
Почему нет своих карточек инструментов
tool.call.toolview ключуется именем инструмента, и механизм сохранения
дочерних слотов существует: в опциях регистрации есть children, а
компонент получает renderSlot — так поставочная строка read_image
объявляет себе tool.call.images. То есть технически заменить карточку,
ничего не потеряв, можно.
Отказ по другой причине. Владельческие пропсы — { callId, toolName, block,
openFile, cwd, home, loadImage, inspect }, и вся суть карточки в разборе
block: варианты, заголовок, вывод, ошибки, diff, терминал, поиск, файл.
Своя карточка обязана воспроизвести этот разбор и застывает на сегодняшней
форме внутренней структуры. Это ровно тот случай замороженной копии, ради
исключения которого пакет и держится аддитивных слотов.
Почему тема не регистрируется через theme.register
ThemeDefinition пиннит одну colorScheme, поэтому выбор такой темы ломает
режим «следовать системе». Слой переопределения несёт пару значений на
каждый токен и переключение схемы переживает сам. Вдобавок поставочная
строка «Оформление» знает ровно три значения — appearance.light,
appearance.dark, appearance.system — и отрисовать стороннюю тему не
может. Регистрация темы была бы регрессом, а не улучшением.
Установка одной командой
git clone https://github.com/tayyarg07/tensorgrid-ui.git
cd tensorgrid-ui
.\install.ps1Установщик проверит требования, поставит dsh (если его нет), создаст профиль, разложит пакет, патч-слой и агент-пресет, заведёт автозапуск и проверит, что всё на месте. Тем же файлом обновляют: он повторно запускаемый и чужого не трогает — настройки, ключи и историю сессий не переписывает.
| Ключ | Что делает |
|---|---|
| -Preflight | только проверить требования и выйти |
| -DryRun | показать план, ничего не меняя |
| -NoAutostart | не заводить автозапуск при входе в Windows |
| -ProfileId <имя> | другой идентификатор профиля и пресета |
Что должно быть на машине
| Нужно | Зачем | Если нет |
|---|---|---|
| Node.js 22.12 или новее | CLI самого dsh зависит от commander@15, а он требует эту версию | https://nodejs.org |
| npm | ставит dsh | приходит вместе с Node.js |
| pnpm | dsh plugin add — это переходник к pnpm; без него dsh отказывается | npm install -g pnpm или corepack enable |
Проверяется всё сразу, до первого изменения, и о недостающем говорится за один заход:
.\install.ps1 -Preflight
npx --yes tensorgrid-ui -Preflight # то же самое без gitРаньше этого не было, и аудит показал цену: pnpm не проверялся вовсе, установка доходила до середины, ответ dsh «pnpm not found on PATH» выбрасывался, а человек читал «Проверьте доступ в интернет» и шёл проверять роутер. Теперь чужой ответ при отказе показывается целиком, а самая частая причина названа прямо.
Ниже — то же самое вручную, если нужно понимать каждый шаг.
Что у меня стоит и не отстало ли
.\status.ps1
.\status.ps1 -Port 3081 # если приложение на другом порту
npx --yes tensorgrid-ui status # то же самое без gitНичего не меняет, только смотрит. Отвечает на четыре вопроса, которые иначе выясняются перепиской: версия dsh и есть ли новее, версия продукта и не отстала ли от репозитория, все ли части на месте, работает ли приложение прямо сейчас и на том ли профиле.
Ключ -Offline пропускает обращение к npm.
Про метку latest в npm
Она указывает на 0.1.5-rc.1, тогда как 0.1.5-rc.2 опубликован позже, —
обычное дело для предрелизов. Поэтому установщик ставит закреплённую
версию из env/snapshot.json, а не latest: иначе сотрудник получил бы
версию старше той, на которой продукт проверялся.
status.ps1 показывает обе и подсвечивает расхождение.
Обновления приходят с двух сторон
| Откуда | Что | Как применяется |
|---|---|---|
| от вас | новый коммит в репозитории | git pull + install.ps1 |
| от DeepSeek | новая версия dsh в npm | npm install -g "@deepseek-ai/dsh@X" |
Каналы независимы, но не безобидны по отдельности. Опасен второй без первого: пакет опирается на контракты dsh — имена слотов, методы сервисов, пространства имён перевода. Если обновить dsh, не переснимая снимок и не прогоняя тест, поломка обнаружится у сотрудника.
Поэтому обновление dsh должно приходить через вас: вы проверяете новую
версию и выпускаете версию продукта с отметкой «проверено на такой-то».
Установщик ставит закреплённую версию из env/snapshot.json, а не
latest, — сотрудник не получит непроверенный dsh случайно.
Порядок выпуска
Когда вышла новая версия dsh:
npm install -g "@deepseek-ai/dsh@<новая>" # 1. поставить у себя
.\verify.ps1 # 2. прогнать всю цепочку
git diff env/snapshot.json # 3. посмотреть, что изменилось
# 4. починить, если что-то отвалилось
git commit -am "проверено на dsh <новая>" # 5. зафиксировать
git push # 6. выпуститьverify.ps1 — единственная проверка, которую нужно помнить перед
выпуском. Она сверяет версию dsh со снимком, переснимает окружение,
обновляет эталон строк, проверяет связность перевода, пересобирает
словари, гоняет все проверки контракта и смотрит, всё ли зафиксировано.
Две честные оговорки, найденные аудитом.
Она не останавливается на первом провале — копит замечания и завершается неуспехом в конце. Здесь раньше было обещано обратное.
Число проверок тут больше не называется. Оно меняется каждый выпуск, и в трёх местах этого файла стояло «172» при фактических 232. Устаревшее число врёт тише всего и потому опаснее всего.
Порядок шагов не случаен: снимок снимается до проверки контракта, потому что тест сверяется именно с ним.
Обновление без новой версии dsh
Продукт сам сверяет установленную версию с меткой latest в реестре и
показывает результат в настройках. Кнопка «Обновить» ставит новую версию,
после чего просит либо обновить страницу, либо перезапустить приложение —
решение принимается по отпечатку host-половины, а не наугад.
То же самое из консоли:
dsh plugin --profile tensorgrid add tensorgrid-ui@latestУ самого dsh автообновления нет, и никто о новых версиях не уведомляет — это проверено, а не предположено. Обновлять его нужно вручную:
npm install -g "@deepseek-ai/dsh@<версия>"После него стоит переснять снимок окружения и посмотреть, что изменилось (см. раздел «После обновления dsh»).
Установка вручную
Единица приложения — профиль, а не пакет. Профиль задаёт, какие бандлы
составляют сборку, и добавляет поверх них свои строки. Поставочный профиль
web при этом не трогается и остаётся запасным вариантом.
Всё лежит в ~/.dsh/, который не перезаписывается обновлением dsh —
в отличие от каталога установки node_modules/@deepseek-ai/dsh.
Создать профиль из поставочного шаблона. Флаг
--dump-configзаставляет команду создать профиль и выйти, не поднимая сервер:dsh --profile tensorgrid --from-default-profile web --dump-configПоставить пакет из реестра штатной командой dsh — она пробрасывает установку в pnpm внутри профиля:
dsh plugin --profile tensorgrid add tensorgrid-ui@<версия>Версию задаёт манифест репозитория, а не метка
latest: у всех должно встать ровно то, что проверено, а не то, что оказалось свежим в момент установки.Предупреждение «declares no
dsh.bundle» здесь нормально: пакет не бандл, а один плагин, который монтируется своей строкой ниже.Положить патч-слой профиля — готовый файл лежит в репозитории:
profile/cordis.patch.yml → ~/.dsh/profiles/tensorgrid/cordis.patch.ymlЕго содержимое:
- insert: - id: authorization name: '@deepseek-ai/dsh-authorization' - id: ui-tensorgrid name: 'tensorgrid-ui'Пакет монтируется по имени: спецификатор разрешается штатным ESM-резолвом из
node_modulesпрофиля, куда его кладётdsh plugin add, а манифестdsh.clientнаходится подъёмом до ближайшегоpackage.json. Регистрировать пакет где-то ещё не нужно.Строка
authorization— стойка входа. Ни один поставочный профиль её не монтирует, а без неёllm-pi-aiне регистрирует ни одного потока входа, и в настройках остаются только ключи API. Подробнее — в разделе «Вход по подписке».Положить агент-пресет:
preset/ → ~/.dsh/.agent-presets/tensorgrid/Профиль задаёт приложение — какие бандлы его составляют. Пресет задаёт агента — его персону, набор инструментов и делегирование. Это две разные плоскости, и живут они в разных местах.
Запустить:
dsh --profile tensorgridПресет выбирается в интерфейсе, в настройках агента: в списке появится «TENSOR GRID» рядом с поставочными.
Вход по подписке
Провайдеры моделей приходят из библиотеки pi-ai, и у части из них есть
вход по подписке, а не только ключ API. Потоки входа регистрирует
llm-pi-ai, но только если смонтирована стойка dsh-authorization —
ни один поставочный профиль её не монтирует, поэтому в стоковом интерфейсе
видны одни ключи. Одна строка в патч-слое включает все 39 потоков, шесть из
которых с подпиской:
| Провайдер | Ключ | Способы |
|---|---|---|
| Anthropic | llm-pi-ai/anthropic | Claude Pro/Max, ключ API |
| OpenAI Codex | llm-pi-ai/openai-codex | ChatGPT Plus/Pro |
| GitHub Copilot | llm-pi-ai/github-copilot | вход, токен |
| Kimi For Coding | llm-pi-ai/kimi-coding | вход, ключ API |
| OpenRouter | llm-pi-ai/openrouter | вход, ключ API |
| xAI | llm-pi-ai/xai | SuperGrok или X Premium, ключ API |
Учётные данные ложатся в ~/.dsh/.credentials.yaml — вне профилей,
поэтому вход, сделанный однажды, работает во всех, а подписка у каждого
сотрудника своя.
Строка настроек ведёт разговор входа целиком: показывает ссылку и код, задаёт вопросы с выбором и вводом, позволяет отменить попытку. Отмена не украшение — стойка допускает одну попытку на ключ, и осиротевшая заявка иначе держит ключ занятым до перезапуска.
Три ошибки, на которых это строилось
Чтение службы из Cordis оказалось не тем, чем выглядит, и стоило трёх выпусков подряд:
ctx.authorizationкак свойство — запрещено без объявления вinject;ctx.get('authorization')возвращает службу, но её методы всё равно охраняются контекстом, который службу не объявил;- объявить стойку в
injectвсего ряда нельзя — она необязательна, и профиль без неё остался бы без палитры, айдентики и перевода.
Работает только четвёртый вариант: вложенный ctx.inject, который ждёт
службы, не задерживая остальное, и отдаёт ссылки, связанные с контекстом,
их объявившим. Правило закреплено проверками в дымовом тесте.
Проверка установки
Профиль можно поднять на свободном порту, не мешая уже запущенному:
dsh --profile tensorgrid --port 3081 --no-openКоманда напечатает адрес с токеном. Две проверки по нему:
GET /favicon.svgобязан вернуть SVG сaria-label="TENSOR GRID"— значит host-половина заняла маршрут;- в HTML страницы (с токеном) обязано встречаться
tensorgrid-ui— значит пакет попал в загрузочный граф__DSH_BOOT__и клиентская половина будет загружена. Имя берётся изpackage.json, а не из имени папки: проверка по старомуdsh-ui-obsidian-ionдала бы ложный отказ.
Откат
dsh web поднимает стоковый интерфейс. Удаление продукта — удалить каталог
~/.dsh/profiles/tensorgrid.
Что проверено, а что нет
Проверено по живому рантайму:
контракт
window.__ModuleLoader__.load({ id, factory })и экспортapply/inject— снято с@deepseek-ai/dsh-client-ui-brand-official;способ вставки CSS тегом
<style data-plugin-css>— снят с@deepseek-ai/dsh-client-ui-layout;имена и требования всех 13 токенов — через
Theme.listTokens;контракт слота
shell.overlay(list,replaceRisk: none) — черезSlots.listSubTree;сам дизайн работает: он уже запущен как динамический Package
obsid-1/pkg-2.обнаружение пакета — прослежено по коду
@deepseek-ai/dsh-client-modules(lib/index.js,locatePkgJson/nearestPackage/resolveMeta). Цепочка для нашей строки:- спецификатор начинается с
.→ считается путём; - резолвится через ту же ESM-резолюцию, что импортировала host-половину;
nearestPackage()идёт вверх от файла до ближайшегоpackage.json. Имя заранее не ожидается, поэтому подходит любое — пакету не нужно быть разрешимым вnode_modules, workspace иdependenciesне нужны;dsh.client.platformдолжен быть ровно"web";exports["./client"]принимается и строкой, и объектом сdefault;- путь бандла =
dirname(package.json) + exports["./client"].
- спецификатор начинается с
исполнение бандла —
node test/smoke.mjs. Секция про манифест проверена негативным контролем: приplatform: "node"и снятомexports["./client"]тест валится на четырёх проверках.
Не проверено запуском:
- фактическая отрисовка в браузере.
Почему загрузку нельзя проверить со стороны Host
Хотелось бы спросить рантайм «применились ли мои токены», но такого ответа нет:
- маршрут
/plugins/<id>/client.jsтребует токен авторизации, снаружи отдаёт 401/404; theme.getTheme()возвращает{ preference, fontSize, active, themes, revision }, гдеactive.tokens— карта токенов определения темы (у встроенных тем пустая). Переопределения изoverrideTokensлежат отдельным слоем, который снимок не публикует;- динамический клиентский плагин по устройству песочницы не имеет ни
document, ниwindow, поэтому прочитать вычисленную CSS-переменную или найти вставленный тег стилей нельзя.
Что проверяется со стороны Host — это регистрация: сервис clientModules,
метод clientPath(id). Если он возвращает путь к lib/client.js внутри
профиля, значит манифест найден и принят. Остальное показывает только
перезагрузка страницы.
Агент-пресет
preset/ — копия поставочного standard, сделанная штатной службой
agentPresets. Поставочные пресеты править нельзя: их затирает обновление
dsh, а порча cordis отключила бы саму возможность авторства пресетов.
Отличий от поставки немного — 47 добавленных строк и 3 убранных:
- persona брендирована и задаёт язык ответа;
- строки делегирования в чужие продукты убраны.
Всё остальное дословно как в поставке — чтобы при обновлении dsh diff показал ровно то, что изменили авторы, и это было легко перенести.
Почему чужих продуктов здесь больше нет
Раньше задачу можно было отдать наружу — в Codex или Claude Code, — и ревизорами могли быть они же. И то и другое убрано целиком, а не спрятано.
Довод не «они плохи», а «они ничего не добавляли». Чистый опыт: одна и та же модель в чужой обвязке и у нас дала совпадение тринадцати находок из четырнадцати. Разное находят разные МОДЕЛИ, а не разные обвязки. Обе подписки — Anthropic и OpenAI — работают прямо здесь, плюс любая модель по ключу, включая сотни через OpenRouter.
А стоили они дорого: два пакета, которых нет в поставке dsh, две лишних установки сотруднику и почти все дефекты, которые пришлось чинить, — сборка аргументов, поток ввода, забор ответа отдельным файлом, обёртки Windows, ключи авторизации в окружении ребёнка, отмена, тайм-аут, обрезанный вывод. Вместе с ними ушёл целый класс поломок: запуск чужого процесса, временные каталоги и разбор чужого формата. Файл запуска ревизоров похудел с 546 строк до 267.
Осталась одна дорога исполнения, и её видно целиком.
Порядок не случаен: наличие провайдера на Host само по себе прав не даёт, а
снятый disabled без установленного бандла уронит ряд.
Проверка пресета
Композиция проверяется до запуска сессии — через agentPresets.resolve() и
standingKeyFor(). Если пресет сломан, они скажут об этом сразу, а не
молчаливым отсутствием инструментов в сессии.
Эксплуатация
Запуск и перезапуск
dsh webПерезапуск — закрыть окно (или Ctrl+C) и запустить снова. Автозапуск при
входе в Windows настроен файлом DeepSeek Harness.cmd в папке
shell:startup; чтобы отключить — удалить его.
| Команда | Что делает |
|---|---|
| dsh --profile web --dump-config | показать собранное дерево плагинов |
| dsh plugin --profile web add <пакет> | поставить плагин в профиль |
| dsh --version | версия |
Добавить модель, которой нет в списке
Список моделей OpenRouter в dsh — это вшитый каталог пакета pi-ai,
снятый на день его сборки. Модель, появившаяся в OpenRouter позже, в списке
не окажется, сколько ни жми «Обновить»: обновление перечитывает провайдеров,
а не каталог.
Насколько велик разрыв — измерено, а не предположено: OpenRouter отдаёт
444 модели, вшитый каталог маршрута openrouter знает 366, живых
моделей не хватает 89; ещё одиннадцать, наоборот, у OpenRouter уже
исчезли, а в каталоге остались.
Прямой путь — дописать модель в маршрут — не годится: поле models
заменяет каталог маршрута целиком, то есть добавление одной модели
стирает остальные триста с лишним. Поэтому для ручных моделей заводится
ОТДЕЛЬНЫЙ маршрут, а основной openrouter остаётся нетронутым.
Кнопкой TENSOR GRID — обычный путь
Настройки → Общие → строка «Список моделей OpenRouter» → Сверить.
Она спрашивает живой список у OpenRouter, сравнивает со вшитым каталогом и
показывает три числа: сколько у провайдера, сколько знает dsh и сколько
годных новинок нашлось. Вторая кнопка — Добавить новые — записывает их
в маршрут openrouter-extra.
Два отсева, и оба не вкусовые.
Уже известные каталогу пропускаются. Иначе рядом с каждой моделью встал бы её двойник, причём наш — беднее: цен и поправок совместимости в ответе OpenRouter нет.
Те, что здесь не заработают, отсеиваются с названной причиной. Агенту
нужен вызов инструментов — без него он не прочитает ни файла; пакетные
модели (:batch) отвечают часами и в разговоре просто повиснут. Отбор
совпадает с политикой самого dsh: моделей с tools и tool_choice у
OpenRouter 367, а во вшитом каталоге — 366.
Измерено в день выпуска: из 89 отсутствующих 74 отсеиваются, остаётся 15 годных.
Сверка не запускается сама — только нажатием. Она ходит в сеть и меняет настройки, а настройки открывают и ради размера шрифта. Запись точечная: у существующего маршрута меняется ТОЛЬКО список моделей, ключ и заголовок остаются как были. Когда каталог dsh догоняет всё, что мы добавляли, маршрут убирается целиком — чтобы двойников не осталось.
Через поставочный интерфейс — тот же механизм, но вручную
Настройки → Модели → строка OpenRouter (вручную) с тегом «Custom» →
кнопка получения моделей. Она спрашивает живой список у самого OpenRouter,
показывает поиск по нему и добавляет отмеченные галочками; уже добавленные
не трогаются. YAML руками при этом не нужен.
Там же кнопкой «добавить своего провайдера» создаётся и сам такой маршрут — это тот же механизм, которым описывают любой сторонний шлюз.
Чего живой список НЕ приносит: цену, модальности, уровни рассуждения и
поправки совместимости. В ответе OpenRouter на /models для каждой модели
есть только id, имя и две ёмкости — остальное во вшитом каталоге. Поэтому
подтянутая вручную запись беднее каталожной, и подменять каталог целиком
живым списком было бы ухудшением, а не улучшением.
Через файл — когда нужно указать больше, чем даёт список
В ~/.dsh/settings.yaml:
llm-pi-ai:
providers:
openrouter:
apiKeyEnv: OPENROUTER_API_KEY
openrouter-extra:
displayName: OpenRouter (вручную)
apiKeyEnv: OPENROUTER_API_KEY # тот же ключ, второй записи не нужно
api: openai-completions
baseURL: https://openrouter.ai/api/v1
models:
- id: stealth/union-alpha
name: Union Alpha
contextWindow: 262144
maxTokens: 131072
input: [text, image]
reasoningEfforts: false # модель без уровней рассужденияЧисла не выдумываются, а берутся из самого OpenRouter:
$r = Invoke-RestMethod https://openrouter.ai/api/v1/models
$r.data | Where-Object id -eq 'stealth/union-alpha' | ConvertTo-Json -Depth 6context_length → contextWindow, top_provider.max_completion_tokens →
maxTokens, architecture.input_modalities → input. Если в
supported_parameters нет reasoning, ставится reasoningEfforts: false;
иначе перечисляются уровни.
Перезапуск не нужен: настройки перечитываются перед следующим запросом. Странице нужно обновление, чтобы забрать новый список.
Проверено на живом приложении: маршрут поднялся без перезапуска, модель
разобралась (262144 / 131072, вход text,image) и ответила на пробный
запрос — finish: stop, 6 входных и 5 выходных токенов. Опрос живого списка
для этого маршрута вернул все 444 модели — то есть кнопка в интерфейсе
опирается на рабочий механизм, а не на обещание.
Отдельно про «скрытые» модели вроде stealth/*: они бесплатны, живут на
общем пуле и отвечают 429 ... temporarily rate-limited upstream, когда пул
занят. Это ответ провайдера, а не поломка настройки. Для работы такие модели
не годятся — только для проб.
После обновления dsh
Переустанавливать ничего не нужно: npm update -g @deepseek-ai/dsh меняет
только каталог установки, а пакет и строка композиции лежат в ~/.dsh/.
При первом же запуске всё смонтируется само.
Но контракты, на которые опирается пакет, могли сдвинуться. Проверка:
cd C:\тест\dsh-ui-obsidian-ion
node tools/extract-locales.mjs # обновить эталон, увидеть новые ключи
node tools/audit-ru.mjs # связность перевода
node tools/build-ru.mjs # пересобрать словари
node test/smoke.mjs # проверки контрактаНовые строки интерфейса, которых нет в русском словаре, покажутся по-английски — это штатный фолбэк, а не поломка.
Доставка изменений и перезапуск
Пакет приезжает из реестра; копирования файлов больше нет нигде.
Что чем подхватывается:
| Изменилось | Достаточно |
|---|---|
| lib/client.js | обновить страницу |
| lib/http-runtime.js, update-check.js | ничего — следующий запрос возьмёт новую версию |
| lib/model-sync.js | ничего — следующее нажатие возьмёт новую версию |
| lib/review-log.js, lib/auth-bridge.js | перезапустить приложение |
| lib/index.js, состав маршрутов, зависимости | перезапустить приложение |
Два файла в этом списке — исключение, и не по недосмотру. Они хранят состояние: журнал держит очередь записи, стойка входа — незавершённую попытку авторизации. Горячая загрузка приписывает к адресу время правки и создаёт НОВЫЙ экземпляр модуля, а значит вторую очередь и второго владельца попытки.
Чем это кончалось: журнал пишут две стороны — панель по нажатию и инструмент обзора в конце прогона, — и вторая импортировала модуль без версии. Экземпляра было два, очереди две; одновременная запись читала одно состояние и сохраняла каждая своё, так что чужая правка исчезала бесследно. Атомарная замена файла от этого не спасает — она защищает от половины файла. Воспроизведено испытанием: восемь одновременных записей через два экземпляра сохраняются не все, через один — все восемь.
Поэтому эти два модуля грузятся один раз за процесс, а их правка требует перезапуска. Плата честная: она дешевле потерянного вердикта и зависшего входа.
Так стало с 1.1.4. До неё обработчики жили в index.js, а он читается
только при старте — поэтому каждый выпуск требовал перезапуска, и
пользователь трижды подряд на это натыкался.
Переход на 1.1.4 сам требует одного перезапуска: старая оболочка про новый модуль ещё не знает.
Две честные границы. Это не горячая перезагрузка всего приложения — всё, что регистрируется в Cordis, по-прежнему живёт до перезапуска. И обновление во время идущего входа обрывает разговор: состояние попытки привязано к экземпляру модуля, а замена файла создаёт новый.
Проверено запуском, а не рассуждением: правка ответа обработчика доехала до следующего запроса, возврат исходного текста — тоже, PID процесса не менялся.
Цикл правок
Источник истины — копия в рабочем каталоге; в профиле лежит рабочая копия.
- Править
lib/client.jsздесь. - Прогнать
node test/smoke.mjs. Файл уезжает в браузер как есть — ни сборщик, ни тайпчекер его не смотрят, поэтому опечатка ломает загрузку молча. Тест исполняет бандл в Node с подставнымиwindow,documentиrequireи проверяет: регистрацию в__ModuleLoader__, экспортapply/inject, вставку тега стилей, все 13 токенов со светлой и тёмной парой, занятие слотаshell.overlay, гашениеpointer-eventsна слоях, совпадение имён классов в разметке и CSS, определённость всех@keyframesи отсутствие неиспользуемых. - Скопировать каталог в профиль (каталог
test/в рантайме не нужен). - Обновить страницу. Путь к бандлу кэшируется, содержимое — нет: граф пересобирается на загрузке страницы и отдаёт свежий файл.
Перезапуск профиля не нужен даже при правке cordis.patch.yml: у профиля
web выставлен patchReload: live.
Откат
Вернуть предыдущую установку
Два пути, и выбор между ними — по обстоятельствам.
.\rollback.ps1 -List # что есть: версии в реестре и местные копии
.\rollback.ps1 # предыдущая версия из реестра (нужна сеть)
.\rollback.ps1 -To 2.4.1 # конкретная версия из реестра
.\rollback.ps1 -FromBackup # из местной копии, БЕЗ СЕТИ
.\rollback.ps1 -FromBackup -Backup 2026-09-15_12-41-55Без git: npx --yes tensorgrid-ui rollback -List и так далее.
Из реестра возвращается только пакет интерфейса — реестр npm хранит опубликованные версии навсегда.
Из местной копии возвращаются пакет, патч-слой и пресет сразу.
install.ps1 перед каждой перезаписью складывает их в
~/.dsh/profiles/<id>/.backup/<дата>/; хранятся три последние копии, и
убираются старые только после того, как новая установка прошла проверку.
Второй путь появился по итогам аудита: первый бесполезен ровно тогда, когда нужен больше всего — сеть недоступна, реестр молчит, сломался не пакет, а патч-слой. Копии при этом лежали рядом и не читались ничем.
Оговорка про местный откат: файлы кладутся обычным каталогом, и pnpm временно перестаёт считать пакет своим. Следующая штатная установка возвращает его под управление pnpm. Для аварийного восстановления это допустимо, для повседневной работы — нет.
Откат возвращает ровно три вещи: пакет, патч-слой и пресет. Настройки, ключи и история сессий не трогаются — они не входят в установку и от неё сломаться не могли.
Версию dsh откат не меняет: она глобальная и общая для всех профилей,
включая стоковый web. Скрипт лишь сообщит, если сохранённая копия
ставилась на другой версии, и подскажет команду.
Выключить продукт целиком
Убрать строку из cordis.patch.yml — перезапуск не нужен. Оба эффекта
(токены и слой) сняты диспозерами, следов не остаётся.
Стоковый интерфейс всегда доступен командой dsh web.
Удалить полностью
Удалить каталог профиля ~/.dsh/profiles/<id> и каталог пресета
~/.dsh/.agent-presets/<id>.
Почему здесь нет mix-blend-mode
Первая версия полагалась на mix-blend-mode: screen, рассчитывая, что
свечение сложится с фоном приложения и само погаснет на светлой теме. Это
не работает: контейнер слота shell.overlay объявлен с z-index: 20,
то есть создаёт изолированный контекст наложения, и смешивание происходит
не с приложением, а с прозрачным фоном самого контейнера.
Поэтому режимы наложения убраны как мёртвый код, а различие тем берётся из
штатного контракта самой темы — атрибута data-ds-dark-theme на body.
Его выставляет bootstrap-скрипт ui-theme ещё до монтирования плагинов и
дальше поддерживает ThemePresenter из ui-layout. Базовые правила в
client.js рассчитаны на светлую тему и сдержанны; селекторы
body[data-ds-dark-theme] … поднимают интенсивность для тёмной.
Это единственная опора на DOM приложения, и она сознательная: атрибут — объявленный интерфейс темы, а не внутреннее имя класса, которое сменится при следующей сборке.
