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

@ -17,6 +17,7 @@ import { Channel, invoke } from "@tauri-apps/api/core";
import type { Agent } from "@/domain";
import type {
AgentGateway,
ConversationDetails,
CreateAgentInput,
OpenTerminalOptions,
ReattachResult,
@ -30,6 +31,8 @@ interface LaunchAgentResponse {
cwd: string;
rows: number;
cols: number;
/** Conversation id minted by this launch (omitted when nothing was assigned). */
assignedConversationId?: string;
}
export class TauriAgentGateway implements AgentGateway {
@ -85,11 +88,19 @@ export class TauriAgentGateway implements AgentGateway {
agentId,
rows: options.rows,
cols: options.cols,
// Resume id: the leaf's persisted conversation id, when any (T4b). The
// backend resumes it; absent ⇒ a fresh cell that may get a new id.
conversationId: options.conversationId ?? null,
},
onOutput: channel,
});
return makeTerminalHandle(res.sessionId, channel);
const handle = makeTerminalHandle(res.sessionId, channel);
// Surface the id assigned by this launch so the caller persists it on the
// leaf (`setCellConversation`) and resumes next time.
return res.assignedConversationId
? { ...handle, assignedConversationId: res.assignedConversationId }
: handle;
}
async reattach(
@ -111,4 +122,16 @@ export class TauriAgentGateway implements AgentGateway {
scrollback: Uint8Array.from(res.scrollback),
};
}
inspectConversation(
projectId: string,
agentId: string,
conversationId: string,
): Promise<ConversationDetails> {
// `inspect_conversation` takes a single `request` DTO; absent fields are
// omitted from the response (best-effort), so an empty object is valid.
return invoke<ConversationDetails>("inspect_conversation", {
request: { projectId, agentId, conversationId },
});
}
}