Détection de l'état fin d'une session claude et du dialogue en cours, pour savoir « quelle session attend une réponse ». Source primaire = registre ~/.claude/sessions (stable) ; l'écran reconstruit via @xterm/headless TYPE le dialogue (trust/permission/question/plan) et couvre le cas Trust (avant registre). - dép: @xterm/headless ^6.0.0 (aligné sur @xterm/xterm du front). - core/screen-reader.ts: terminal headless persistant par session, alimenté par le flux PTY ; snapshotLines() préserve les espaces (bat le strip ANSI naïf). - core/dialog-classifier.ts: typage pur (texte aplati + regex tolérante inter- versions) + extraction des options numérotées (❯ = sélection). - core/claude-adapter.ts: SessionActivityTracker par session (registre + écran → activity + dialog), évaluation débouncée sur output + poll registre léger, émission sur changement effectif uniquement. - shared: SessionSummary += activity?/waitingFor?/dialog? (optionnels, additif). - pty-manager: tracker instancié pour claude, feed dans handleOutput, summarize enrichi, resize propagé, dispose à l'exit. - web: composant SessionStateBadge (busy/waiting/idle colorés) réutilisable. - tests: screen-reader + classifier sur fixtures réelles S3 (perm-write/ask/ trust), claude-adapter (machine à états). 159 verts.
47 lines
2.1 KiB
TypeScript
47 lines
2.1 KiB
TypeScript
// Typage de dialogue à partir de l'écran reconstruit (ScreenReader) — fonctions PURES.
|
||
// L'écran sert à TYPER le dialogue et extraire ses options ; l'état (waiting vrai/faux) vient du
|
||
// registre (source primaire). Tolérant aux variations de rendu inter-versions (texte aplati + regex).
|
||
import type { DialogKind, DialogOption } from '@arboretum/shared';
|
||
|
||
export interface ClassifiedDialog {
|
||
kind: DialogKind;
|
||
options: DialogOption[];
|
||
}
|
||
|
||
// Option numérotée, éventuellement préfixée du curseur ❯ : « ❯ 1. Yes », « 2) No »…
|
||
const OPTION_RE = /^\s*(❯)?\s*(\d+)[.)]\s+(.*\S)\s*$/;
|
||
|
||
export function parseOptions(lines: string[]): DialogOption[] {
|
||
const out: DialogOption[] = [];
|
||
for (const line of lines) {
|
||
const m = OPTION_RE.exec(line);
|
||
if (m && m[2] && m[3]) out.push({ n: Number(m[2]), label: m[3].trim(), selected: Boolean(m[1]) });
|
||
}
|
||
return out;
|
||
}
|
||
|
||
/**
|
||
* Retourne le dialogue typé visible à l'écran, ou null si aucun. `trust` et `question` priment sur
|
||
* `permission` (un écran AskUserQuestion contient aussi « Esc to cancel »). Un écran numéroté non
|
||
* typé est traité en `permission` générique (best-effort — le fallback reste le terminal web).
|
||
*/
|
||
export function classifyDialog(lines: string[]): ClassifiedDialog | null {
|
||
const text = lines.join('\n');
|
||
const flat = text.replace(/\s+/g, '').toLowerCase();
|
||
const options = parseOptions(lines);
|
||
|
||
const isTrust = flat.includes('trust') && (text.includes('❯') || flat.includes('entertoconfirm') || flat.includes('trustthisfolder'));
|
||
const isQuestion = flat.includes('entertoselect') || flat.includes('tonavigate') || text.includes('↑/↓');
|
||
const isPermission = flat.includes('doyouwant') || flat.includes('esctocancel') || flat.includes('tabtoamend');
|
||
const isPlan = flat.includes('readytocode') || flat.includes('wouldyouliketoproceed');
|
||
|
||
let kind: DialogKind | null = null;
|
||
if (isTrust) kind = 'trust';
|
||
else if (isQuestion) kind = 'question';
|
||
else if (isPlan) kind = 'plan';
|
||
else if (isPermission) kind = 'permission';
|
||
else if (options.length > 0) kind = 'permission';
|
||
|
||
return kind ? { kind, options } : null;
|
||
}
|