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:
@ -6,7 +6,8 @@ mod helpers;
|
||||
use domain::{
|
||||
Agent, AgentManifest, AgentOrigin, AgentProfile, AgentTemplate, ContextInjection, Direction,
|
||||
LayoutNode, LayoutTree, LeafCell, ManifestEntry, MarkdownDoc, Project, ProjectPath, RemoteRef,
|
||||
Skill, SkillId, SkillRef, SkillScope, SplitContainer, SshAuth, TemplateVersion, WeightedChild,
|
||||
SessionStrategy, Skill, SkillId, SkillRef, SkillScope, SplitContainer, SshAuth, TemplateVersion,
|
||||
WeightedChild,
|
||||
};
|
||||
use helpers::{node, session};
|
||||
use uuid::Uuid;
|
||||
@ -108,6 +109,7 @@ fn profile_roundtrip_all_injection_variants() {
|
||||
ci,
|
||||
Some("claude --version".into()),
|
||||
"{projectRoot}",
|
||||
None,
|
||||
)
|
||||
.unwrap();
|
||||
assert_eq!(roundtrip(&p), p);
|
||||
@ -133,12 +135,78 @@ fn profile_cwd_template_is_camel_case() {
|
||||
ContextInjection::stdin(),
|
||||
None,
|
||||
"{projectRoot}",
|
||||
None,
|
||||
)
|
||||
.unwrap();
|
||||
let json = serde_json::to_string(&p).unwrap();
|
||||
assert!(json.contains("\"cwdTemplate\""), "json was {json}");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn profile_with_session_roundtrips_and_uses_camel_case() {
|
||||
let session = SessionStrategy::new(Some("--session-id".into()), "--resume").unwrap();
|
||||
let p = AgentProfile::new(
|
||||
profid(1),
|
||||
"Claude Code",
|
||||
"claude",
|
||||
vec![],
|
||||
ContextInjection::stdin(),
|
||||
None,
|
||||
"{agentRunDir}",
|
||||
Some(session),
|
||||
)
|
||||
.unwrap();
|
||||
let json = serde_json::to_string(&p).unwrap();
|
||||
assert!(
|
||||
json.contains("\"session\":{\"assignFlag\":\"--session-id\",\"resumeFlag\":\"--resume\"}"),
|
||||
"json was {json}"
|
||||
);
|
||||
assert_eq!(roundtrip(&p), p);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn profile_without_session_omits_the_field() {
|
||||
let p = AgentProfile::new(
|
||||
profid(1),
|
||||
"n",
|
||||
"c",
|
||||
vec![],
|
||||
ContextInjection::stdin(),
|
||||
None,
|
||||
"{projectRoot}",
|
||||
None,
|
||||
)
|
||||
.unwrap();
|
||||
let json = serde_json::to_string(&p).unwrap();
|
||||
assert!(!json.contains("session"), "json was {json}");
|
||||
assert_eq!(roundtrip(&p), p);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn legacy_profile_without_session_deserialises_to_none() {
|
||||
// A `profiles.json` produced before the `session` field existed.
|
||||
let json = r#"{
|
||||
"id": "00000000-0000-0000-0000-000000000001",
|
||||
"name": "Claude Code",
|
||||
"command": "claude",
|
||||
"args": [],
|
||||
"contextInjection": { "strategy": "stdin" },
|
||||
"detect": null,
|
||||
"cwdTemplate": "{agentRunDir}"
|
||||
}"#;
|
||||
let p: AgentProfile = serde_json::from_str(json).expect("legacy profile must deserialise");
|
||||
assert_eq!(p.session, None);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn session_assign_flag_omitted_when_none() {
|
||||
let session = SessionStrategy::new(None, "--continue").unwrap();
|
||||
let json = serde_json::to_string(&session).unwrap();
|
||||
assert!(!json.contains("assignFlag"), "json was {json}");
|
||||
assert!(json.contains("\"resumeFlag\":\"--continue\""), "json was {json}");
|
||||
assert_eq!(roundtrip(&session), session);
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// AgentTemplate
|
||||
// ---------------------------------------------------------------------------
|
||||
@ -299,6 +367,8 @@ fn layout_roundtrip() {
|
||||
id: node(1),
|
||||
session: Some(session(100)),
|
||||
agent: None,
|
||||
conversation_id: None,
|
||||
agent_was_running: false,
|
||||
}),
|
||||
weight: 1.5,
|
||||
},
|
||||
@ -307,6 +377,8 @@ fn layout_roundtrip() {
|
||||
id: node(2),
|
||||
session: None,
|
||||
agent: None,
|
||||
conversation_id: None,
|
||||
agent_was_running: false,
|
||||
}),
|
||||
weight: 2.5,
|
||||
},
|
||||
@ -330,6 +402,8 @@ fn leaf_with_agent_roundtrip_and_omits_null() {
|
||||
id: node(1),
|
||||
session: None,
|
||||
agent: Some(AgentId::from_uuid(agent_uuid)),
|
||||
conversation_id: None,
|
||||
agent_was_running: false,
|
||||
}));
|
||||
let rt = roundtrip(&tree);
|
||||
match rt.root {
|
||||
@ -344,6 +418,8 @@ fn leaf_with_agent_roundtrip_and_omits_null() {
|
||||
id: node(2),
|
||||
session: None,
|
||||
agent: None,
|
||||
conversation_id: None,
|
||||
agent_was_running: false,
|
||||
}));
|
||||
let json2 = serde_json::to_string(&tree_no_agent).unwrap();
|
||||
assert!(!json2.contains("\"agent\""), "agent field should be omitted when None; json was {json2}");
|
||||
|
||||
Reference in New Issue
Block a user