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

@ -52,6 +52,18 @@ export interface CreateAgentInput {
initialContent?: string;
}
/**
* Best-effort enriched details about a CLI conversation (T7), used to enrich the
* resume popup. Both fields are optional: a missing inspector or a missing
* transcript yields an empty object — the popup degrades to the status alone.
*/
export interface ConversationDetails {
/** A short, best-effort label for the conversation (last topic). */
lastTopic?: string;
/** A best-effort cumulative token count. */
tokenCount?: number;
}
/** Agents: create, list, read/update context, delete, launch (L6). */
export interface AgentGateway {
/** Lists all agents belonging to the given project. */
@ -67,6 +79,13 @@ export interface AgentGateway {
/**
* Launches the agent: opens a PTY, spawns the CLI, and wires the output stream.
* Mirrors {@link TerminalGateway.openTerminal} but invokes `launch_agent`.
*
* The returned handle carries an optional {@link TerminalHandle.assignedConversationId}:
* when the agent's profile assigns a fresh CLI conversation id on this first
* launch (the cell had none yet), it is surfaced here so the caller can persist
* it on the hosting leaf (`setCellConversation`) and resume on the next open.
* Pass the leaf's current `conversationId` via {@link OpenTerminalOptions} to
* resume an existing conversation instead.
*/
launchAgent(
projectId: string,
@ -84,6 +103,17 @@ export interface AgentGateway {
sessionId: string,
onData: (bytes: Uint8Array) => void,
): Promise<ReattachResult>;
/**
* Reads best-effort {@link ConversationDetails} for an agent's conversation
* (T7), to enrich the resume popup with the last topic + a token indicator.
* Best-effort by contract: a missing/unsupported inspector or a missing
* transcript resolves to an empty object (never rejects for "no details").
*/
inspectConversation(
projectId: string,
agentId: string,
conversationId: string,
): Promise<ConversationDetails>;
}
/** Options for opening a terminal. */
@ -94,6 +124,13 @@ export interface OpenTerminalOptions {
rows: number;
/** Initial terminal width in columns. */
cols: number;
/**
* Persistent CLI conversation id recorded on the hosting cell, if any. When
* present, an agent launch **resumes** that conversation; when absent the
* launch may *assign* a fresh id (surfaced on the returned handle). Only
* meaningful for {@link AgentGateway.launchAgent}; ignored for plain terminals.
*/
conversationId?: string;
}
/**
@ -106,6 +143,14 @@ export interface OpenTerminalOptions {
export interface TerminalHandle {
/** Stable session id (UUID) used by the backend for this PTY. */
readonly sessionId: string;
/**
* Conversation id **assigned** by an agent launch when the profile minted a
* fresh one (the cell had none yet). Present only on handles returned by
* {@link AgentGateway.launchAgent}; `undefined` for plain terminals, resumes,
* or profiles without a session block. The caller persists it on the hosting
* leaf so the next open resumes instead of re-assigning.
*/
readonly assignedConversationId?: string;
/** Sends bytes (xterm keystrokes) to the PTY. */
write(data: Uint8Array): Promise<void>;
/** Resizes the PTY. */
@ -152,6 +197,17 @@ export interface TerminalGateway {
sessionId: string,
onData: (bytes: Uint8Array) => void,
): Promise<ReattachResult>;
/**
* Kills a live PTY by its session id, independently of any view-held handle.
* Used when a cell's agent changes: the old PTY must be torn down even though
* the owning {@link TerminalHandle} is private to its (unmounting) view, whose
* cleanup only ever {@link TerminalHandle.detach}es. Best-effort: resolves even
* if the session is already gone.
*
* This is the only sanctioned way to kill a PTY outside
* {@link TerminalHandle.close}.
*/
closeTerminal(sessionId: string): Promise<void>;
}
/** The outcome of {@link TerminalGateway.reattach}. */