feat(terminals): reprise de conversation par cellule + fix ordre d'écriture

Permet de recharger la conversation CLI précédente de chaque cellule à la
réouverture du projet, de façon universelle (indépendant du modèle/CLI).

- profil AgentRuntime: bloc déclaratif optionnel `session { assignFlag, resumeFlag }`
- LeafCell: `conversationId` (persistant, distinct du SessionId PTY) + `agentWasRunning`
- runtime: SessionPlan (None/Assign/Resume) + composition pure des args
- LaunchAgent: décide Assign vs Resume, génère l'UUID, remonte l'id assigné
  (persistance par l'appelant via setCellConversation — découplage SRP)
- close: SnapshotRunningAgents fige `agentWasRunning` avant le kill-all
  (statut clot/en cours universel, sans parsing CLI)
- SessionInspector: port optionnel best-effort + adapter ClaudeTranscriptInspector
- popup de reprise par cellule (statut + sujet/tokens si dispo), intercalée
  avant le Resume auto, jamais sur le chemin reattach

fix(terminals): sérialise les écritures PTY (file FIFO par handle) — corrige
les caractères mélangés/accents dus au réordonnancement des invoke Tauri concurrents

fix(layout): l'opération `move` préservait mal les champs du leaf (perdait `agent`)

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
2026-06-07 22:27:08 +02:00
parent d11eaaa8c0
commit 3ed0f6b45f
61 changed files with 5098 additions and 98 deletions

View File

@ -48,12 +48,32 @@ export function makeTerminalHandle(
sessionId: string,
channel: Channel<number[]>,
): TerminalHandle {
// Serialise writes per handle. Each `write` chains its `invoke` after the
// previous one resolves, so the order in which `write` is *called* is the
// order the bytes reach the backend stdin — regardless of how Tauri's IPC
// schedules concurrent `invoke`s. Without this, typing/pasting fast puts
// several `invoke`s in flight at once and they can land out of order, garbling
// the CLI input (e.g. "le même" → "le mO é J é IDEIDE…").
//
// The chain only sequences *ordering*; a failed write is swallowed for the
// purpose of the chain (logged, then the chain continues) so one rejected
// promise never blocks every subsequent keystroke. The error is still
// surfaced to the caller of that specific `write` via its own promise.
let chain: Promise<void> = Promise.resolve();
return {
sessionId,
async write(data: Uint8Array): Promise<void> {
await invoke("write_terminal", {
request: { sessionId, data: Array.from(data) },
});
write(data: Uint8Array): Promise<void> {
const run = chain.then(() =>
invoke<void>("write_terminal", {
request: { sessionId, data: Array.from(data) },
}),
);
// Keep the chain alive even if this write rejects: the next write must
// still run. Swallow the error on the *chain* copy only — `run` keeps the
// rejection so the caller can observe it.
chain = run.catch(() => {});
return run;
},
async resize(rows: number, cols: number): Promise<void> {
await invoke("resize_terminal", {
@ -105,4 +125,11 @@ export class TauriTerminalGateway implements TerminalGateway {
scrollback: Uint8Array.from(res.scrollback),
};
}
async closeTerminal(sessionId: string): Promise<void> {
// Kills the PTY by id (the backend `close_terminal` command). Used when a
// cell's agent changes and the old PTY must be torn down even though its
// owning view only ever detaches.
await invoke("close_terminal", { sessionId });
}
}