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:
@ -30,6 +30,7 @@ import type {
|
||||
} from "@/domain";
|
||||
import type {
|
||||
AgentGateway,
|
||||
ConversationDetails,
|
||||
CreateAgentInput,
|
||||
CreateSkillInput,
|
||||
CreateTemplateInput,
|
||||
@ -323,7 +324,17 @@ export class MockAgentGateway implements AgentGateway {
|
||||
queueMicrotask(() =>
|
||||
session.emit(enc.encode(`agent ${agentId} @ ${cwd}\r\n`)),
|
||||
);
|
||||
return makeMockHandle(session, () => this.sessions.delete(sessionId));
|
||||
const handle = makeMockHandle(session, () => this.sessions.delete(sessionId));
|
||||
// Simulate session assignment (T4b): a fresh cell (no conversation id) gets a
|
||||
// newly-minted id surfaced on the handle so the caller persists it; a cell
|
||||
// that already carries an id resumes it and nothing new is assigned.
|
||||
if (!options.conversationId) {
|
||||
return {
|
||||
...handle,
|
||||
assignedConversationId: `mock-conversation-${sessionId}`,
|
||||
};
|
||||
}
|
||||
return handle;
|
||||
}
|
||||
|
||||
async reattach(
|
||||
@ -344,6 +355,31 @@ export class MockAgentGateway implements AgentGateway {
|
||||
scrollback,
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Best-effort conversation details, keyed by conversation id (T7). Empty by
|
||||
* default (degraded mode); a test seeds enriched details via
|
||||
* {@link _setConversationDetails}.
|
||||
*/
|
||||
private conversationDetails = new Map<string, ConversationDetails>();
|
||||
|
||||
/** Seeds the (best-effort) details returned for a given conversation id. */
|
||||
_setConversationDetails(
|
||||
conversationId: string,
|
||||
details: ConversationDetails,
|
||||
): void {
|
||||
this.conversationDetails.set(conversationId, details);
|
||||
}
|
||||
|
||||
async inspectConversation(
|
||||
_projectId: string,
|
||||
_agentId: string,
|
||||
conversationId: string,
|
||||
): Promise<ConversationDetails> {
|
||||
// Best-effort: an unknown conversation yields empty details (degraded mode),
|
||||
// never an error — mirroring the backend contract.
|
||||
return structuredClone(this.conversationDetails.get(conversationId) ?? {});
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
@ -421,6 +457,16 @@ export class MockTerminalGateway implements TerminalGateway {
|
||||
scrollback,
|
||||
};
|
||||
}
|
||||
|
||||
async closeTerminal(sessionId: string): Promise<void> {
|
||||
// Best-effort / idempotent: kill the PTY by id (mirrors the handle's close)
|
||||
// so a later reattach to it fails. No-op if the session is already gone.
|
||||
const session = this.sessions.get(sessionId);
|
||||
if (session) {
|
||||
session.closed = true;
|
||||
this.sessions.delete(sessionId);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export class MockProjectGateway implements ProjectGateway {
|
||||
|
||||
Reference in New Issue
Block a user