P3-B: claude-adapter — états fins busy/waiting/idle
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.
This commit is contained in:
@@ -8,6 +8,7 @@ import { FLOW, REPLAY_TAIL_BYTES, type SessionSummary } from '@arboretum/shared'
|
||||
import { RingBuffer } from './ring-buffer.js';
|
||||
import { buildSpawnSpec } from './claude-launcher.js';
|
||||
import { findByPid } from './session-registry.js';
|
||||
import { SessionActivityTracker } from './claude-adapter.js';
|
||||
import type { Db } from '../db/index.js';
|
||||
|
||||
const RING_CAPACITY = 2 * 1024 * 1024;
|
||||
@@ -44,6 +45,8 @@ interface ManagedSession {
|
||||
killTimer: NodeJS.Timeout | null;
|
||||
/** ID interne du CLI claude, résolu via le registre après spawn (null pour bash / pas encore prêt). */
|
||||
claudeSessionId: string | null;
|
||||
/** détection d'état fin (busy/waiting/idle + dialogue) ; null pour bash (P3-B). */
|
||||
tracker: SessionActivityTracker | null;
|
||||
}
|
||||
|
||||
export interface PtyManagerEvents {
|
||||
@@ -90,7 +93,14 @@ export class PtyManager extends EventEmitter<PtyManagerEvents> {
|
||||
exited: null,
|
||||
killTimer: null,
|
||||
claudeSessionId: null,
|
||||
tracker: null,
|
||||
};
|
||||
// Détection d'état fin (P3-B) : uniquement pour claude (bash n'a pas de registre).
|
||||
if (command === 'claude') {
|
||||
session.tracker = new SessionActivityTracker(proc.pid, this.sessionsDir, () => {
|
||||
if (!session.exited) this.emit('session_update', this.summarize(session));
|
||||
});
|
||||
}
|
||||
this.live.set(id, session);
|
||||
this.db
|
||||
.prepare('INSERT INTO sessions (id, cwd, command, created_at, resumed_from) VALUES (?, ?, ?, ?, ?)')
|
||||
@@ -212,7 +222,10 @@ export class PtyManager extends EventEmitter<PtyManagerEvents> {
|
||||
const hasController = [...s.clients].some((c) => c.controlling);
|
||||
binding.controlling = binding.mode === 'interactive' && !hasController;
|
||||
s.clients.add(binding);
|
||||
if (binding.controlling) s.proc.resize(cols, rows);
|
||||
if (binding.controlling) {
|
||||
s.proc.resize(cols, rows);
|
||||
s.tracker?.resize(cols, rows);
|
||||
}
|
||||
// Replay : reset terminal + queue du ring (l'écran TUI courant se reconstitue)
|
||||
binding.sendResync(s.ring.tail(REPLAY_TAIL_BYTES));
|
||||
binding.sentBytes = 0;
|
||||
@@ -247,6 +260,7 @@ export class PtyManager extends EventEmitter<PtyManagerEvents> {
|
||||
const s = this.live.get(sessionId);
|
||||
if (!s || s.exited || !binding.controlling) return;
|
||||
s.proc.resize(cols, rows);
|
||||
s.tracker?.resize(cols, rows);
|
||||
}
|
||||
|
||||
ack(sessionId: string, binding: ClientBinding, bytes: number): void {
|
||||
@@ -266,6 +280,7 @@ export class PtyManager extends EventEmitter<PtyManagerEvents> {
|
||||
|
||||
private handleOutput(s: ManagedSession, chunk: Buffer): void {
|
||||
s.ring.write(chunk);
|
||||
s.tracker?.feed(chunk);
|
||||
for (const c of s.clients) {
|
||||
if (c.lagging) continue;
|
||||
c.sendOutput(chunk);
|
||||
@@ -302,6 +317,8 @@ export class PtyManager extends EventEmitter<PtyManagerEvents> {
|
||||
|
||||
private handleExit(s: ManagedSession, exitCode: number | null, signal: number | null): void {
|
||||
s.exited = { exitCode, signal };
|
||||
s.tracker?.dispose();
|
||||
s.tracker = null;
|
||||
if (s.killTimer) clearTimeout(s.killTimer);
|
||||
const endedAt = new Date().toISOString();
|
||||
this.db.prepare('UPDATE sessions SET ended_at = ?, exit_code = ? WHERE id = ?').run(endedAt, exitCode, s.id);
|
||||
@@ -313,6 +330,7 @@ export class PtyManager extends EventEmitter<PtyManagerEvents> {
|
||||
}
|
||||
|
||||
private summarize(s: ManagedSession): SessionSummary {
|
||||
const act = s.tracker?.snapshot();
|
||||
return {
|
||||
id: s.id,
|
||||
cwd: s.cwd,
|
||||
@@ -330,7 +348,11 @@ export class PtyManager extends EventEmitter<PtyManagerEvents> {
|
||||
// une managée vivante ne se resume pas (corruption) ; une managée claude morte oui.
|
||||
resumable: !!s.exited && s.command === 'claude' && s.claudeSessionId != null,
|
||||
attachable: !s.exited,
|
||||
registryStatus: null, // P2 : statut fin des managées via claude-adapter (P3-B)
|
||||
// statut brut du registre dérivé de l'activité fine (P3-B) ; null pour bash.
|
||||
registryStatus: act?.activity ?? null,
|
||||
activity: act?.activity ?? null,
|
||||
waitingFor: act?.waitingFor ?? null,
|
||||
dialog: act?.dialog ?? null,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user