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

@ -191,6 +191,8 @@ fn single_leaf(node_id: NodeId) -> LayoutTree {
id: node_id,
session: None,
agent: None,
conversation_id: None,
agent_was_running: false,
})
}
@ -638,6 +640,77 @@ async fn mutate_set_cell_agent_missing_leaf_is_not_found() {
assert_eq!(err.code(), "NOT_FOUND", "got {err:?}");
}
// ---------------------------------------------------------------------------
// SetCellConversation (T4b — persist the assigned CLI conversation id)
// ---------------------------------------------------------------------------
#[tokio::test]
async fn mutate_set_cell_conversation_persists_id_on_leaf() {
let env = mut_env(pid(52)).await;
// Record a conversation id on the single root leaf (nid(1)).
env.mutate
.execute(MutateLayoutInput {
project_id: env.project_id,
layout_id: None,
operation: LayoutOperation::SetCellConversation {
target: nid(1),
conversation_id: Some("conv-42".to_owned()),
},
})
.await
.expect("set_cell_conversation records the id");
// The id must survive in the persisted JSON, so the next open resumes.
let tree_json = active_tree_json(&env.fs);
assert_eq!(
tree_json["root"]["node"]["conversationId"], "conv-42",
"conversation id must be persisted on the leaf"
);
// Now clear it.
let out = env
.mutate
.execute(MutateLayoutInput {
project_id: env.project_id,
layout_id: None,
operation: LayoutOperation::SetCellConversation {
target: nid(1),
conversation_id: None,
},
})
.await
.expect("set_cell_conversation clears the id");
match &out.layout.root {
LayoutNode::Leaf(l) => assert_eq!(l.conversation_id, None, "id must be cleared"),
_ => panic!("expected leaf root"),
}
assert!(
active_tree_json(&env.fs)["root"]["node"]
.get("conversationId")
.is_none(),
"cleared id must not be serialised"
);
}
#[tokio::test]
async fn mutate_set_cell_conversation_missing_leaf_is_not_found() {
let env = mut_env(pid(53)).await;
let err = env
.mutate
.execute(MutateLayoutInput {
project_id: env.project_id,
layout_id: None,
operation: LayoutOperation::SetCellConversation {
target: nid(404),
conversation_id: Some("x".to_owned()),
},
})
.await
.expect_err("unknown node rejected");
assert_eq!(err.code(), "NOT_FOUND", "got {err:?}");
}
// ---------------------------------------------------------------------------
// Named-layout management (#4)
// ---------------------------------------------------------------------------