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:
@ -201,11 +201,13 @@ export function applyOperation(
|
||||
if (target === undefined) throw notFound(op.to);
|
||||
if (target !== null) throw invalid("target cell is occupied");
|
||||
const root = mapNode(tree.root, (n) => {
|
||||
// Preserve agent / conversationId / agentWasRunning on both ends — only
|
||||
// the ephemeral `session` moves (mirror of the Rust `move_session`).
|
||||
if (n.type === "leaf" && n.node.id === op.from) {
|
||||
return { type: "leaf", node: { id: n.node.id, session: null } };
|
||||
return { type: "leaf", node: { ...n.node, session: null } };
|
||||
}
|
||||
if (n.type === "leaf" && n.node.id === op.to) {
|
||||
return { type: "leaf", node: { id: n.node.id, session } };
|
||||
return { type: "leaf", node: { ...n.node, session } };
|
||||
}
|
||||
return n;
|
||||
});
|
||||
@ -242,6 +244,40 @@ export function applyOperation(
|
||||
if (!found) throw notFound(op.target);
|
||||
return { root };
|
||||
}
|
||||
case "setCellConversation": {
|
||||
let found = false;
|
||||
const root = mapNode(tree.root, (n) => {
|
||||
if (n.type === "leaf" && n.node.id === op.target) {
|
||||
found = true;
|
||||
const { conversationId: _c, ...rest } = n.node;
|
||||
// Omit when null (mirrors the backend `skip_serializing_if`).
|
||||
const updated = op.conversationId !== null
|
||||
? { ...rest, conversationId: op.conversationId }
|
||||
: rest;
|
||||
return { type: "leaf", node: updated };
|
||||
}
|
||||
return n;
|
||||
});
|
||||
if (!found) throw notFound(op.target);
|
||||
return { root };
|
||||
}
|
||||
case "setAgentRunning": {
|
||||
let found = false;
|
||||
const root = mapNode(tree.root, (n) => {
|
||||
if (n.type === "leaf" && n.node.id === op.target) {
|
||||
found = true;
|
||||
const { agentWasRunning: _r, ...rest } = n.node;
|
||||
// Omit when false (mirrors the backend `skip_serializing_if`).
|
||||
const updated = op.running
|
||||
? { ...rest, agentWasRunning: true }
|
||||
: rest;
|
||||
return { type: "leaf", node: updated };
|
||||
}
|
||||
return n;
|
||||
});
|
||||
if (!found) throw notFound(op.target);
|
||||
return { root };
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -255,6 +291,34 @@ export function leaves(tree: LayoutTree): LeafCell[] {
|
||||
return out;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sessions hosted by the children a `merge` would **drop** — every child of
|
||||
* `container` except the kept one, walked recursively (a dropped child may be a
|
||||
* nested split with several terminals). Used by the close button to tear down
|
||||
* the discarded PTYs instead of orphaning them, mirroring `setCellAgent`. Returns
|
||||
* `[]` if the container isn't found or holds no sessions.
|
||||
*/
|
||||
export function droppedSessions(
|
||||
tree: LayoutTree,
|
||||
container: string,
|
||||
keepIndex: number,
|
||||
): string[] {
|
||||
const out: string[] = [];
|
||||
mapNode(tree.root, (n) => {
|
||||
if (n.type === "split" && n.node.id === container) {
|
||||
n.node.children.forEach((c, i) => {
|
||||
if (i === keepIndex) return;
|
||||
mapNode(c.node, (m) => {
|
||||
if (m.type === "leaf" && m.node.session) out.push(m.node.session);
|
||||
return m;
|
||||
});
|
||||
});
|
||||
}
|
||||
return n;
|
||||
});
|
||||
return out;
|
||||
}
|
||||
|
||||
/** Convenience: builds a `split` operation splitting `target` in `direction`. */
|
||||
export function splitOp(target: string, direction: Direction): LayoutOperation {
|
||||
return {
|
||||
|
||||
Reference in New Issue
Block a user