# 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)

Navigation par touches fléchées pour l'interface web de DSH. Changez de session
sans quitter le clavier :

| Touche | Action | Portée |
| --- | --- | --- |
| `↑` | session précédente | dans l'espace de travail courant |
| `↓` | session suivante | dans l'espace de travail courant |
| `←` | espace de travail précédent | tous les espaces de travail |
| `→` | espace de travail suivant | tous les espaces de travail |

`↑`/`↓` rebouclent aux extrémités de l'espace de travail et ne franchissent jamais
la frontière vers l'espace voisin ; ce franchissement est le rôle de `←`/`→`. Après
un changement, la ligne cible est amenée dans le champ de vision, et le focus revient
à la zone de saisie si c'est de là que venait l'appui, si bien que la frappe peut
continuer immédiatement.

## Version et lignes DSH

Une seule version sert toutes les lignes d'hôte prises en charge. La version `0.5.0` est testée avec tous les rc des lignes DSH 0.1.0–0.2.0 — la liste blanche exacte est `engines.dsh`, distribuée depuis la source unique `scripts/hosts.mjs` par `scripts/sync-hosts.mjs` (toute édition à la main fait échouer `npm run hosts:check`) :

| Ligne DSH | rc testés |
| --- | --- |
| 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 |

- Pas de build par ligne : les deux générations de commutation sont réparties par sonde de capacités dans un seul bundle (`uiWorkspace.openSession` à partir de la génération 0.1.6, `sessions.open` avant) — chaque ligne exécute le même code.
- Sur les lignes 0.1.0/0.1.1 (génération `dsh-client-runtime`), la déclaration et les tests unitaires sont complets, mais la vérification des touches sur des hôtes réels reste à faire ; d'ici là, considérez ces deux cellules comme best effort.

## Installation

```sh
dsh plugin --profile web add dsh-arrowkey-nav -w
```

Le `latest` du registry est l'unique version pour toutes les lignes. Chaque ligne a aussi un dist-tag pointant vers le même build — `dsh-arrowkey-nav@dsh-0.2.0`, `dsh-arrowkey-nav@dsh-0.1.5`, etc. — si vous préférez nommer la ligne ; la cible d'installation est identique. Une version d'hôte hors du tableau est rejetée par la liste blanche `engines.dsh` au lieu de se monter à moitié cassée.

Redémarrez ensuite le profil — une instance en cours d'exécution ne charge pas à
chaud une nouvelle couche de bundle. Rechargez ensuite `http://127.0.0.1:3080`.

Confirmez que la ligne figure dans l'arbre composé :

```sh
dsh web --dump-config | Select-String dsh-arrowkey-nav
```

Un checkout local peut être enregistré à la place avec
`dsh plugin --profile web add <absolute path to the checkout> -w` ; voir
[INSTALL.md](./INSTALL.md) pour le guide complet, comprenant mise à niveau,
vérification et suppression.

### Suppression

```sh
dsh plugin --profile web remove dsh-arrowkey-nav
```

Après redémarrage, les touches se comportent exactement comme avant
l'installation : l'écouteur appartient à l'effet Cordis du plugin et est supprimé
avec lui.

## Fonctionnement

L'identité provient des deux contrôleurs clients, jamais du DOM : les lignes ne
portent ni `data-*` ni `id`, on ne peut donc pas demander à une ligne de quelle
session il s'agit.

- Chaque changement passe par l'exécuteur choisi par sonde de capacités : `ctx.uiWorkspace.openSession(id)` sur les générations DSH 0.1.6+, `ctx.sessions.open(id)` sur les lignes plus anciennes — un seul bundle, aucun build par ligne. Le panneau de détails ne demande aucun traitement : le cadre fourni le referme déjà quand la session courante change.

**La session « suivante » suit l'ordre que la barre latérale est en train de
dessiner**, et non l'ordre des membres du contrôleur. Les deux divergent
précisément dans le cas que vous remarquez : la barre latérale réconcilie son
propre ordre persisté avec les adhésions et promeut en tête les sessions
récemment actives, de sorte qu'une ancienne session bondit à la première ligne dès
que vous l'ouvrez. Le plugin lit donc l'ordre des lignes rendu par la barre
latérale, et retombe sur l'ordre du contrôleur quand la barre latérale n'est pas
montée (rail réduit). Une section n'est acceptée que si ses lignes rendent compte
de toutes les sessions que le contrôleur signale pour cet espace de travail ; une
section ambiguë ou incohérente entraîne un repli plutôt qu'une supposition.

Les touches sont laissées tranquilles quand l'événement appartient déjà à
quelqu'un d'autre : un modificateur quelconque, une composition IME, un événement
déjà traité en amont, ou un focus dans une boîte de dialogue, un menu ou un autre
champ éditable (la recherche de session de la barre latérale, une boîte de
renommage). L'écouteur s'enregistre en phase de capture mais n'appelle
`preventDefault()` que lorsqu'il navigue réellement.

La zone de saisie est le seul cas conditionnel. **Une zone de saisie vide cède ses
touches fléchées** — il n'y a pas de curseur à déplacer, la touche est donc libre —
et c'est ce qui permet de naviguer juste après l'envoi d'un message, alors que la
zone de saisie détient encore le focus. Dès que la zone de saisie **contient un
brouillon, ses touches fléchées redeviennent la propriété du curseur**, si bien
que l'édition d'un prompt n'est jamais détournée.

La moitié navigateur ne contient ni composant React, ni CSS, ni requête de module
de plateforme, et le paquet n'a aucune dépendance à l'exécution — `lib/client.js`
est généré par `scripts/build-client.mjs` selon le contrat de chargeur de modules
attendu par la page.

## Développement

```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` requiert `typescript` ; s'il n'est pas installé dans ce paquet,
faites pointer `DSH_TYPESCRIPT` vers un répertoire de module `typescript`
existant.

## Limitations connues

- **Un groupe d'espaces de travail replié ne défile pas.** Ses lignes ne sont pas
  montées et dsh n'expose aucun moyen public de déplier un groupe : le changement
  a lieu, mais la ligne cible reste hors de vue. L'espace de travail contenant la
  session courante est maintenu déplié par dsh lui-même, `↑`/`↓` ne sont donc pas
  concernés.
- **Une barre latérale réduite en rail ne défile pas non plus**, pour la même
  raison : l'arbre n'est pas monté. Le changement a quand même lieu.
- **Le mode « dans une seule liste »** n'a pas de sections par espace de travail,
  mais `←`/`→` parcourent toujours l'ordre des espaces de travail du contrôleur ;
  la liste peut donc sembler sauter d'une section à l'autre.
- **Le défilement relève du meilleur effort.** Tout échec de recherche est avalé :
  la sélection a déjà bougé, et une ligne manquante ne doit jamais se transformer
  en une mauvaise.
- **Les champs d'instantané sont pré-stables.** Les formes des instantanés
  `sessions` et `workspaces` ne sont pas encore stabilisées — les versions d'hôte testées sont exactement la
  liste blanche `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`). Si une mise à niveau de
  dsh les modifie, `src/client/navigate.ts` et `src/client/apply.ts` sont les
  seuls fichiers à revoir ; un hôte sans aucune API de commutation laisse le plugin inerte plutôt
  que de faire échouer la page.

## Structure

```
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
```

## Licence

MIT
