# dsh-arrowkey-nav

[English](./README.md) | [简体中文](./README.zh.md) | [日本語](./README.ja.md) | [한국어](./README.ko.md) | [Français](./README.fr.md) | [Deutsch](./README.de.md) | [Italiano](./README.it.md) | [Русский](./README.ru.md) | [Español](./README.es.md)

Навигация клавишами-стрелками для веб-GUI DSH. Переключайте сессии, не отрываясь
от клавиатуры:

| Клавиша | Действие | Область |
| --- | --- | --- |
| `↑` | предыдущая сессия | в текущем рабочем пространстве |
| `↓` | следующая сессия | в текущем рабочем пространстве |
| `←` | предыдущее рабочее пространство | все рабочие пространства |
| `→` | следующее рабочее пространство | все рабочие пространства |

`↑`/`↓` зацикливаются на границах рабочего пространства и никогда не переходят в
соседнее; переход между пространствами — задача `←`/`→`. После переключения
целевая строка прокручивается в область видимости, а фокус возвращается в поле
ввода, если нажатие было начато там, так что печатать можно продолжать сразу.

## Версия и линии DSH

Одна версия обслуживает все поддерживаемые линии хоста. Версия `0.5.0` проверена со всеми rc линий DSH 0.1.0–0.2.0 — точный белый список это `engines.dsh`, распространяемый из единого источника `scripts/hosts.mjs` скриптом `scripts/sync-hosts.mjs` (ручная правка ломает `npm run hosts:check`):

| Линия DSH | проверенные rc |
| --- | --- |
| 0.1.0 | rc.2, rc.3, rc.6, rc.7, rc.8 |
| 0.1.1 | rc.1, rc.2 |
| 0.1.2 | rc.1 |
| 0.1.5 | rc.1, rc.2, rc.3 |
| 0.1.7 | rc.1, rc.2 |
| 0.2.0 | rc.1, rc.2 |

- Сборок по линиям нет: оба поколения переключения разведены проверкой возможностей внутри одного бандла (`uiWorkspace.openSession` в поколениях 0.1.6+, `sessions.open` на более старых) — все линии исполняют один и тот же код.
- На линиях 0.1.0/0.1.1 (поколение `dsh-client-runtime`) декларации и юнит-тесты готовы, но нажатия клавиш на реальных хостах ещё не проверены; до тех пор считайте эти две ячейки best effort.

## Установка

```sh
dsh plugin --profile web add dsh-arrowkey-nav -w
```

`latest` в registry — единственная версия для всех линий. У каждой линии есть и dist-tag на тот же билд — `dsh-arrowkey-nav@dsh-0.2.0`, `dsh-arrowkey-nav@dsh-0.1.5` и так далее, — если хочется назвать линию; цель установки идентична. Релиз хоста вне таблицы будет отклонён белым списком `engines.dsh`, а не смонтируется наполовину сломанным.

После этого перезапустите профиль — работающий экземпляр не подхватывает новый
слой бандла «на горячую». Затем перезагрузите `http://127.0.0.1:3080`.

Убедитесь, что строка присутствует в собранном дереве:

```sh
dsh web --dump-config | Select-String dsh-arrowkey-nav
```

Вместо этого можно зарегистрировать локальный checkout:
`dsh plugin --profile web add <absolute path to the checkout> -w`; полное
руководство, включая обновление, проверку и удаление, см. в
[INSTALL.md](./INSTALL.md).

### Удаление

```sh
dsh plugin --profile web remove dsh-arrowkey-nav
```

После перезапуска клавиши ведут себя в точности как до установки: слушатель
принадлежит Cordis-эффекту плагина и удаляется вместе с ним.

## Как это работает

Идентичность берётся из двух клиентских контроллеров, но никогда из DOM: строки
не несут ни `data-*`, ни `id`, поэтому у строки нельзя спросить, какая она
сессия.

- Каждое переключение проходит через исполнитель, выбранный проверкой возможностей: `ctx.uiWorkspace.openSession(id)` в поколениях DSH 0.1.6+, `ctx.sessions.open(id)` на более старых линиях — один бандл, сборок по линиям нет. Панель деталей отдельной обработки не требует: штатный каркас уже закрывает её при смене текущей сессии.

**«Следующая» сессия следует порядку, который боковая панель рисует сейчас**, а
не порядку участников из контроллера. Они расходятся ровно в том случае, который
вы замечаете: боковая панель согласовывает свой сохранённый порядок с составом
участников и поднимает недавно активные сессии наверх, поэтому старая сессия в
момент открытия прыгает на первую строку. Поэтому плагин читает отрисованный
порядок строк из боковой панели, а при несмонтированной панели (свёрнутой в
узкую полосу) возвращается к порядку контроллера. Секция принимается, только
когда её строки покрывают все сессии, которые контроллер сообщает для этого
пространства; неоднозначная или несовпадающая секция возвращает управление, а не
угадывает.

Клавиши не трогаются, когда событие уже принадлежит кому-то другому: любой
модификатор, композиция IME, событие, уже обработанное выше по стеку, или фокус
внутри диалога, меню или другого редактируемого поля (поиск сессий в боковой
панели, поле переименования). Слушатель регистрируется на фазе захвата, но
вызывает `preventDefault()` только когда действительно выполняет навигацию.

Поле ввода — единственный условный случай. **Пустое поле ввода отдаёт свои
стрелки** — каретки, которую можно двигать, нет, значит клавиша свободна —
именно поэтому навигация возможна сразу после отправки сообщения, пока фокус ещё
в поле ввода. Как только в поле ввода **появляется черновик, его стрелки снова
принадлежат каретке**, так что редактирование промпта никогда не перехватывается.

Браузерная половина не содержит ни компонентов React, ни CSS, ни запросов
платформенных модулей, а у пакета нет зависимостей времени выполнения —
`lib/client.js` генерируется скриптом `scripts/build-client.mjs` в контракт
module-loader, который ожидает страница.

## Разработка

```sh
npm run typecheck   # both compiler faces
npm test            # resolver unit tests + built-bundle smoke tests
npm run build       # regenerate lib/client.js after editing src/client
```

`npm run build` требует `typescript`; если он не установлен в этом пакете,
направьте `DSH_TYPESCRIPT` на существующий каталог модуля `typescript`.

## Известные ограничения

- **Свёрнутая группа рабочих пространств не прокручивается.** Её строки не
  смонтированы, а dsh не предоставляет публичного способа развернуть группу,
  поэтому переключение происходит, но целевая строка остаётся вне вида. Рабочее
  пространство с текущей сессией dsh сам держит развёрнутым, поэтому `↑`/`↓` не
  затронуты.
- **Боковая панель, свёрнутая в узкую полосу, тоже не прокручивается** по той же
  причине: дерево не смонтировано. Переключение всё же происходит.
- **Режим «в одном списке»** не имеет секций по рабочим пространствам, но
  `←`/`→` всё равно идут по порядку пространств контроллера, поэтому список
  может казаться перескакивающим между секциями.
- **Прокрутка выполняется по мере возможностей.** Любая ошибка поиска
  проглатывается: выделение уже переместилось, а отсутствующая строка никогда не
  должна обернуться неверной.
- **Поля снимков пока не стабилизированы (pre-stable).** Формы снимков `sessions`
  и `workspaces` ещё не стабилизировались — проверенные версии хоста — в точности белый список
  `engines.dsh` (`engines.dsh: 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`). Если обновление dsh их изменит,
  пересмотреть нужно только `src/client/navigate.ts` и `src/client/apply.ts`;
  хост без обеих API переключения оставит плагин инертным, а не уронит страницу.

## Структура

```
src/index.ts                node half: loader entry, installs nothing
src/client/constants.ts     keys owned, DOM anchors read
src/client/navigate.ts      narrow waist: snapshot types + arrow resolver
src/client/apply.ts         execution: open / blank-first / connectWorkspace
src/client/dom.ts           read-only DOM: scroller, section binding, scroll, focus
src/client/session-nav.ts   key guards + one arrow press end to end
src/client/index.ts         Cordis entry: inject + one capturing listener
scripts/build-client.mjs    generates lib/client.js
tests/                      resolver unit tests + bundle smoke tests
```

## Лицензия

MIT
