feat(frontend): terminal pilotable au doigt sur téléphone (#69)
Lots 3 et 4 du ticket #69. Sans ça un agent CLI est indriveable depuis un téléphone : le clavier virtuel n'a ni Esc, ni Tab, ni Ctrl, ni flèches, donc on peut taper un prompt mais pas l'interrompre, compléter un chemin, sortir d'un éditeur ou rappeler l'historique. - `TerminalView` expose un `onReady(api)` optionnel (donc desktop inchangé, et inerte quand xterm ne monte pas). `api.send` passe par `term.input()` : le *même* chemin qu'une frappe réelle, donc le relais PTY, le comptage de lignes et la suspension du write-portal s'appliquent à l'identique. Écrire sur le handle aurait court-circuité le portal. - `TerminalKeyBar` (web-only) : Esc/Tab/Ctrl-C/Ctrl-D/flèches en chips tactiles 44px, scroll horizontal. Chaque tap annule le déplacement de focus et refocalise xterm — sinon le clavier virtuel se referme à chaque touche. - La cellule agent passe de `h-64` fixe (une letterbox d'~20 lignes) à `h-[60dvh]` sur téléphone, `sm:h-80` au-delà. - Tests : séquences d'octets émises, préservation du focus, état désactivé avant montage de xterm, et garde de non-régression sur la frontière — le client web ne monte aucun layout-grid/split/dock desktop. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
@ -95,6 +95,32 @@ interface TerminalViewProps {
|
||||
* cells.
|
||||
*/
|
||||
portal?: WritePortal;
|
||||
/**
|
||||
* Called once, when xterm has mounted, with an imperative
|
||||
* {@link TerminalInputApi} for this cell (#69). Optional and inert when
|
||||
* absent, so the desktop path is unchanged; the web client uses it to drive
|
||||
* the mobile key toolbar. Not called when xterm fails to mount (headless).
|
||||
*/
|
||||
onReady?: (api: TerminalInputApi) => void;
|
||||
}
|
||||
|
||||
/**
|
||||
* Imperative handle over a mounted terminal (#69), handed to the caller by
|
||||
* {@link TerminalViewProps.onReady}.
|
||||
*
|
||||
* It exists because a phone's virtual keyboard has no Esc, Tab, Ctrl or arrow
|
||||
* keys, so the web client renders a key toolbar that needs to inject them. Both
|
||||
* methods deliberately go through xterm rather than the PTY handle:
|
||||
* `send` routes through `term.input()` — the *same* path a real keystroke takes
|
||||
* — so the PTY relay, the write-portal's line counting and its suspension gate
|
||||
* all keep applying. Writing to the handle directly would bypass the portal and
|
||||
* let an injected key race a delegation.
|
||||
*/
|
||||
export interface TerminalInputApi {
|
||||
/** Inject `data` exactly as if the user had typed it. */
|
||||
send: (data: string) => void;
|
||||
/** Focus the terminal — on a phone this summons the virtual keyboard. */
|
||||
focus: () => void;
|
||||
}
|
||||
|
||||
export function TerminalView({
|
||||
@ -105,6 +131,7 @@ export function TerminalView({
|
||||
onSessionId,
|
||||
agentMode = false,
|
||||
portal,
|
||||
onReady,
|
||||
}: TerminalViewProps) {
|
||||
const { terminal } = useGateways();
|
||||
const containerRef = useRef<HTMLDivElement | null>(null);
|
||||
@ -136,6 +163,8 @@ export function TerminalView({
|
||||
agentModeRef.current = agentMode;
|
||||
const portalRef = useRef(portal);
|
||||
portalRef.current = portal;
|
||||
const onReadyRef = useRef(onReady);
|
||||
onReadyRef.current = onReady;
|
||||
|
||||
useEffect(() => {
|
||||
const container = containerRef.current;
|
||||
@ -194,6 +223,19 @@ export function TerminalView({
|
||||
else pending += data;
|
||||
});
|
||||
|
||||
// Publish the input API now that the keystroke relay above is live, so an
|
||||
// injected key is handled exactly like a typed one. Guarded on `disposed`:
|
||||
// the caller may still hold this object after the cell unmounts, and driving
|
||||
// a disposed xterm throws.
|
||||
onReadyRef.current?.({
|
||||
send: (data) => {
|
||||
if (!disposed) term.input(data);
|
||||
},
|
||||
focus: () => {
|
||||
if (!disposed) term.focus();
|
||||
},
|
||||
});
|
||||
|
||||
const onData = (bytes: Uint8Array) => {
|
||||
if (!disposed) term.write(bytes);
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user