# dsh-canvas-tsx-sidebar

[简体中文](README.md) | [Français](README.fr.md) | [Deutsch](README.de.md) | [Italiano](README.it.md) | [Русский](README.ru.md) | [Español](README.es.md)

Веб-плагин DSH (DeepSeek Harness) — **плагин-потребитель dsh-better-sidebar**.

Статически разбирает файлы `*.canvas.tsx` из Qoder Canvas в рабочей области в структурированные страницы и отображает их в правой боковой панели DSH.

> **Полностью статический конвейер**: исходники canvas не выполняются. Без `eval`, без `new Function`, без бандлера, без sandbox-iframe.
> Разбор выполняется на стороне браузера собственным легковесным рекурсивным парсером (ноль зависимостей парсера).

В репозитории также есть **Skill**: `skills/writing-qoder-canvas/`, который учит LLM писать этот формат. См. [Интеграция Skill](#интеграция-skill-научить-llm-писать-canvastsx).

![canvas-tsx-sidebar](assets/canvas.png)

**Диапазон совместимости**: **полное перечисление единой линией** — `engines.dsh` — точное перечисление из 16 позиций (та же строка в package.json `engines`, peer `@deepseek-ai/dsh-client-locale` и `dsh.plugin.json`; без интервалов, без caret):

```text
0.1.0-rc.2 || 0.1.0-rc.3 || 0.1.0-rc.6 || 0.1.0-rc.7 || 0.1.0-rc.8 || 0.1.1-rc.1 || 0.1.1-rc.2 || 0.1.2-rc.1 || 0.1.5-rc.1 || 0.1.5-rc.2 || 0.1.5-rc.3 || 0.1.7-rc.1 || 0.1.7-rc.2 || 0.2.0-rc.1 || 0.2.0-rc.2 || 0.2.1-rc.1 || 0.2.1-rc.2 || 0.2.1-rc.3 || 0.2.1-rc.4 || 0.2.1-rc.5
```

**0.7.2 (latest / `dsh-0.2.1-rc.2`) и выше** устанавливается на любой из перечисленных хостов; неперечисленные версии хоста (в т.ч. 0.2.1 stable и линия 0.2.2) вне заявленной области — при выпуске stable-версии перечисление нужно расширить и перевыпустить; не используйте `latest` вслепую на необъявленных хостах (`engines` не удовлетворены → тихая блокировка стартовой проверкой). Исторические dist-tag старых линий (все = 0.6.0, 15 позиций до 0.2.0-rc.2):

| DSH host | plugin version | install dist-tag |
|---|---|---|
| any in the 20-term enum (incl. 0.2.1-rc.1 and rc.2) | **0.7.2** (latest) | `latest` or `dsh-0.2.1-rc.1` / `dsh-0.2.1-rc.2` |
| 0.2.0-rc.1/rc.2 | 0.6.0 | `dsh-0.2.0` |
| 0.1.7-rc.1/rc.2 | 0.6.0 | `dsh-0.1.7` |
| 0.1.5 line | 0.6.0 | `dsh-0.1.5` |
| 0.1.2-rc.1 | 0.6.0 | `dsh-0.1.2` |

(по состоянию на 2026-10-17; старые линии обслуживаются ветками `compat/0.1.7` / замороженной `compat/0.1.5` и `archive/release/0.1.2`.)

---

## Предварительные требования

- DSH `dsh web` нормально запускается
- Установлен [dsh-better-sidebar](https://www.npmjs.com/package/dsh-better-sidebar) (`>=0.19.1`)

Без better-sidebar плагин **полностью пассивен**: обе регистрации молча пропускаются и не затрагивают остальные функции DSH.

## Установка

### Способ A (рекомендуется, официальный CLI)

```sh
# hosts inside the 20-term enum (incl. 0.2.1-rc.1 and rc.2): latest works
dsh plugin --profile <profile> add dsh-canvas-tsx-sidebar@latest          # 0.7.2, 17-term full enumeration
dsh plugin --profile <profile> add dsh-canvas-tsx-sidebar@dsh-0.2.1-rc.1  # same, explicit line tag
# alt: local tarball (this line: dsh-canvas-tsx-sidebar-0.7.2.tgz)
dsh plugin --profile <profile> add <dsh-canvas-tsx-sidebar-0.7.2.tgz>
```
```

`dsh` добавляет этот пакет в `dsh.profile.bundles`; при запуске входящий в пакет `cordis.patch.yml` вставляет запись загрузчика, и клиентский бандл регистрируется и доставляется согласно `entry.name`.

### Способ B (вручную, альтернативный)

1. Скопировать пакет в `~/.dsh/profiles/<profile>/node_modules/dsh-canvas-tsx-sidebar`
   (либо добавить `"dsh-canvas-tsx-sidebar": "link:<путь-к-плагину>"` в `dependencies` файла `package.json` профиля);
2. дописать следующую строку в `~/.dsh/profiles/<profile>/cordis.patch.yml`:

   ```yaml
   - insert:
       - id: dsh-canvas-tsx-sidebar
         name: dsh-canvas-tsx-sidebar
   ```

3. выполнить `pnpm install` в каталоге профиля.

> ⚠️ **Выберите способ A или способ B — не регистрируйте дважды.**

### Активация

4. **Перезапустите `dsh web`** — добавление бандла требует перезагрузки на стороне хоста (hot reload работает только для изменений клиента **уже подключённого** плагина);
5. жёсткое обновление страницы в браузере (Ctrl+Shift+R).

Состояние подключения можно перепроверить через `pwsh -NoProfile -File ./scripts/mount-check.ps1`.

## Использование

Две точки входа:

| Точка входа | Как вызывается | Поведение |
|---|---|---|
| **Просмотрщик файлов** (перехват) | Открыть любой `.tsx` в дереве файлов | `.canvas.tsx` → структурированная страница + переключатель `Просмотр/Код`; остальные `.tsx` → просмотр исходника |
| **Вкладка** | меню `+` правой панели → **Отчёт Canvas** | ввести путь вручную и посмотреть, не открывая файл в редакторе |

Просмотрщик файлов можно отключить в настройках карточки Side; после отключения такие файлы возвращаются к встроенному просмотрщику кода.

Открытие **одного** файла рендерит **именно этот** файл — плагин не сканирует рабочую область.

### Почему просмотрщик файлов обязан перехватывать все .tsx

`exts: ['tsx']` — единственный работоспособный способ перехвата; цена в том, что перехватываются и `.tsx` вне canvas. Все доказательства взяты из исходников better-sidebar и практических проверок в экосистеме:

| # | Ограничение | Доказательство |
|---|---|---|
| L1 | `extOf()` берёт только последний сегмент расширения → `extOf('a.canvas.tsx') === 'tsx'` | `src/client/paths.ts:80-85` |
| L2 | `exts: ['tsx']` перехватывает **все** `.tsx` рабочей области, а priority 0 перекрывает CodeMirror встроенного `code` (-100) | `service.ts:847-875` |
| L3 | `detect` вызывается только при доступных байтах `head`, а `head` приходит только из бинарного `fs.read` — текстовый `.tsx` до этой ветки никогда не доходит | `service.ts:860` |
| L4 | после перехвата дескриптором `component` обязан отрисоваться, **API делегирования/отката нет** | `EditorHost.tsx:349,486` |
| L5 | экосистемный плагин `dsh-code-nav` уже содержит `tsx` в своём `LANG_EXT` и перехватывает с `priority: 10` | `dsh-code-nav/src/lang-registry.js` |

→ Определение `.canvas.tsx` возможно только внутри собственного компонента. Поэтому: **просмотрщик перехватывает все `.tsx`** (canvas идёт на страницу, остальное — на исходник), а **вкладка** сохранена как второй вход, минующий перехват файлов.

## Интеграция Skill: научить LLM писать .canvas.tsx

`skills/writing-qoder-canvas/` — **самодостаточный, полностью переносимый** Skill:

```
skills/writing-qoder-canvas/
  SKILL.md                      # условия срабатывания, скелет файла, пять железных правил, список красных флагов
  references/components.md      # 38 реально отрисовываемых тегов + prop каждого (машинная проверка)
  references/expressions.md     # замкнутые правила статического парсера: какие значения выживают
  references/layout.md          # писать под боковую панель ~400px (а не под превью 960px)
  examples/status-report.canvas.tsx   # рендеримый образец, тесты гарантируют ноль деградаций
```

### Почему он не лжёт

Документация дрейфует, поэтому каждое утверждение здесь привязано к исходникам (`tests/skill.spec.ts`):

- список поддерживаемого в `components.md` должен **поэлементно совпадать** с тегами `case` в `render.tsx` (38 штук, включая порядок);
- ни одно имя из списка «не поддерживается» не должно встречаться в рендерере;
- `examples/status-report.canvas.tsx` должен отрисовываться **без единой деградации** — без пометок `unsupported`, без пунктирных рамок неизвестных компонентов.

Если документация расходится с реальными возможностями, `pnpm test` падает.

### Как установить его в DSH

DSH обнаруживает Skill в фиксированном наборе корней, причём Skill должен лежать **ровно на один уровень в глубину**: `<root>/<name>/SKILL.md`.
Вложенные `**/SKILL.md` не находятся (провайдер следит за каждым корнем через chokidar, `depth: 1`).

| rank | Источник | Путь |
|---|---|---|
| 100 | `project-dsh` | `<projectRoot>/.dsh/skills` |
| 200 | `project-agents` | `<projectRoot>/.agents/skills` |
| 300 | `custom` | `customSkillDirs` из конфигурации DSH |
| 400 | `user-dsh` | `~/.dsh/skills` |
| 500 | `user-agents` | `~/.agents/skills` |
| 600 | `bundled` | `$DSH_BUNDLED_SKILL_DIR` |

```powershell
# по умолчанию: установка в ~/.agents/skills через junction (без копий, без дрейфа)
pwsh -NoProfile -File ./scripts/install-skill.ps1

# установка в другое место; -Copy делает настоящую независимую копию (дрейфует, после правок запускать заново)
pwsh -NoProfile -File ./scripts/install-skill.ps1 -Target UserDsh
pwsh -NoProfile -File ./scripts/install-skill.ps1 -Path D:\some\skills -Copy

# удаление
pwsh -NoProfile -File ./scripts/install-skill.ps1 -Uninstall
```

Провайдер следит за корнями, **перезапускать `dsh web` не нужно**: после установки Skill появится в каталоге skill'ов следующей сессии.

**По умолчанию junction, а не копия**, потому что копии дрейфуют — эта рабочая область уже обжигалась на `memport` с «тремя копиями, ни одна не синхронна». Junction делает копию в репозитории единственным авторитетным источником.

> `skills/` **не входит в `files[]` в `package.json`** — это актив репозитория, а не публикуемый npm-артефакт.

## Ограничения версий

На `dsh-better-sidebar@0.19.0 / 0.19.1` path seed у `openTab({ path })` перенаправляется в редактор файлов, и **вкладка-компонент не монтируется** (upstream #632, исправлено с 0.19.2). Поэтому плагин **не полагается на path seed**, а сам разрешает целевой файл через `/sidebar/api` (`session.cwd` → `fs.tree` → `fs.read`) — этот подход одинаково работает и на 0.19.2+.

## Скины и темы

**Оболочка плагина** (вкладки, карточка настроек, кнопки) потребляет только токены `--dsw-alias-*` от DSH и автоматически следует всем скинам и светлой/тёмной теме.

**Поддерево документов canvas — исключение, и намеренное**: `.canvas.tsx` описывает лист с фиксированной вёрсткой, цвета выбирает автор; перекраска по скину изменила бы сам вид отчёта. Поэтому `styles.ts` использует буквальные значения цветов, и **каждый селектор ограничен `.dsh-canvas-doc`**, без протечек в UI хоста (`tests/render.spec.tsx` сторожит область действия, `tests/purity.spec.ts` — поверхность регистрации).

## Известные ограничения

- Разбор ведёт **легковесный собственный парсер**, а не компиляторная точность. Дженерики, декораторы и произвольные вызовы не моделируются и всегда деградируют, но **ошибка никогда не бросается**.
- Разбор значений — **замкнутый набор правил** (подробности в `skills/writing-qoder-canvas/references/expressions.md`).
  Поддерживается: литералы, утверждения `as const` / `as T`, литеральные `const` на уровне модуля и в начале тела функции,
  проекции `ARR.map(x => литерал)` по статическим массивам, `canvasImage('литерал')`.
  Не поддерживается (**не вычисляется**): условные выражения, вызовы функций, цепочки членов, шаблонная интерполяция, арифметика, `new`, `await`,
  `const` внутри вложенных блоков, данные из межмодульных `import`.
  - не-литеральный **дочерний узел** → отрисовывается как встроенная полоса деградации, исходный фрагмент остаётся видимым;
  - не-литеральное **свойство** → свойство отбрасывается, имя попадает в `unresolved` IR, остальные свойства отрисовываются как обычно.
- Теги в нижнем регистре всегда прозрачно проходят как нативный HTML; только **заглавные и не отображённые** компоненты рисуются пунктирной рамкой с именем.
- Вкладки отрисовываются **только для чтения**.
- **Парсер терпимее компилятора**: Qoder SDK требует, чтобы авторы правили диагностику через «Canvas TypeScript check» в IDE, но реальные файлы не всегда чисты. На нелегальном TSX этот плагин **не падает и не прерывается**, деградирует только проблемный узел.
  Авторитетную диагностику можно посмотреть через `node scripts/syntax-oracle.cjs <file.canvas.tsx>`.

  > Проверено на практике: строка 46 входящего в репозиторий образца `cmp-cloud-sdk-report.canvas.tsx`,
  > `{'created': ...}`, — нелегальный TSX (TypeScript выдаёт **TS1005 + TS1381** — «голый» оператор разворота).
  > Плагин деградирует это место во встроенную пометку, остальные 147 строк рендерит как обычно.

## Семантика пробелов в JSX

Текстовые дочерние узлы сворачиваются строго по семантике `cleanJSXElementLiteralChild` из React:

- **ведущий отступ на всех строках, кроме первой, безусловно срезается**;
- **концевые пробелы последней строки сохраняются** — этот пробел несущий: он отделяет текст от идущего следом контейнера выражения
  (пример: пробел в `…сложение (0 + {'created': ...})…`; уберите его — и два куска слипнутся).

## Разработка

```sh
pnpm install
pnpm typecheck     # tsc (включая проверку «ноль зависимостей от Node» в клиентской части)
pnpm test          # vitest
pnpm build         # tsc dts + tsdown (хостовая половина + клиентский бандл)
pnpm run audit:bundle   # аудит реальных артефактов сборки
```

## Скрипты разработки

| Скрипт | Назначение |
|---|---|
| `scripts/dump-ir.ts` | Экспорт IR-структуры / JSON / эталонных фикстур файла (`--write`) |
| `scripts/syntax-oracle.cjs` | Определяет **настоящим компилятором TypeScript**, является ли исходник корректным TSX (`typescript` остаётся devDependency и в бандл не попадает) |
| `scripts/audit-bundle.cjs` | Аудит реальных артефактов сборки: утечки встроенных модулей, повторные парсеры, `eval`, регистрации вне зоны ответственности |
| `scripts/mount-check.ps1` | Проверка/исправление подключения профиля (чистый ASCII, совместим с Windows PowerShell 5.1) |
| `scripts/install-skill.ps1` | Устанавливает `skills/writing-qoder-canvas` в корень skill'ов DSH (по умолчанию junction) |
| `scripts/component-census.cjs` | Автономная перепись компонентов (без жёстко зашитой таблицы компонентов, с учётом многострочности) |
| `scripts/corpus-stats.ts` / `format-economics.ts` / `audit-corpus.ts` / `corpus-gaps.ts` | Статистика корпуса и практические замеры экономики формата (источник цифр для `docs/canvas-format-rules.md`) |
