feat(memory): config embedders (LOT C2) + suggestion contextuelle (LOT C3) + contexte projet partagé
- LOT C2 (§14.5.3) : use cases de configuration des embedders déclaratifs (List/Save/Delete + DescribeEmbedderEngines : modèles ONNX recommandés, environnement local détecté, stratégies compilées). UI EmbedderSettings. - LOT C3 (§14.5.5) : suggestion contextuelle best-effort à l'activation quand la mémoire dépasse le budget de recall sans embedder configuré (event EmbedderSuggested, anti-spam 1×/session, « ne plus demander »). - Contexte projet partagé .ideai/CONTEXT.md (model-agnostic) injecté à tous les agents/profils au lancement, avant la persona. UI ProjectContextPanel. Tests : backend workspace vert (0 échec) ; frontend 306/306. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
@ -180,8 +180,14 @@ fn lid(n: u128) -> LayoutId {
|
||||
}
|
||||
|
||||
async fn register_project(store: &FakeStore, id: ProjectId) -> ProjectId {
|
||||
let project =
|
||||
Project::new(id, "Demo", ProjectPath::new(ROOT).unwrap(), RemoteRef::Local, 0).unwrap();
|
||||
let project = Project::new(
|
||||
id,
|
||||
"Demo",
|
||||
ProjectPath::new(ROOT).unwrap(),
|
||||
RemoteRef::Local,
|
||||
0,
|
||||
)
|
||||
.unwrap();
|
||||
store.save_project(&project).await.unwrap();
|
||||
id
|
||||
}
|
||||
@ -264,7 +270,10 @@ async fn load_migrates_a_legacy_layout_json() {
|
||||
let fs = FakeFs::default();
|
||||
let id = register_project(&store, pid(2)).await;
|
||||
// Only the legacy single-layout file exists.
|
||||
fs.put(LEGACY_PATH, &serde_json::to_vec(&single_leaf(nid(7))).unwrap());
|
||||
fs.put(
|
||||
LEGACY_PATH,
|
||||
&serde_json::to_vec(&single_leaf(nid(7))).unwrap(),
|
||||
);
|
||||
|
||||
let load = LoadLayout::new(Arc::new(store), Arc::new(fs.clone()));
|
||||
let out = load
|
||||
@ -300,7 +309,10 @@ async fn load_defaults_to_single_empty_leaf_when_absent() {
|
||||
_ => panic!("expected a single default leaf"),
|
||||
}
|
||||
// Default was written through; two loads are deterministic.
|
||||
assert_eq!(root_leaf_id(&read_active_tree(&fs)), root_leaf_id(&out.layout));
|
||||
assert_eq!(
|
||||
root_leaf_id(&read_active_tree(&fs)),
|
||||
root_leaf_id(&out.layout)
|
||||
);
|
||||
assert!(fs.has_dir("/home/me/proj/.ideai"));
|
||||
}
|
||||
|
||||
@ -415,7 +427,10 @@ async fn mutate_split_persists_camelcase_layout_and_announces() {
|
||||
let tree = active_tree_json(&env.fs);
|
||||
assert_eq!(tree["root"]["type"], "split", "tagged on `type`");
|
||||
assert_eq!(tree["root"]["node"]["direction"], "row");
|
||||
assert_eq!(tree["root"]["node"]["children"].as_array().unwrap().len(), 2);
|
||||
assert_eq!(
|
||||
tree["root"]["node"]["children"].as_array().unwrap().len(),
|
||||
2
|
||||
);
|
||||
|
||||
assert_eq!(
|
||||
env.bus.events(),
|
||||
@ -499,6 +514,62 @@ async fn mutate_set_session_attaches_and_clears() {
|
||||
.is_none());
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn mutate_attach_session_moves_session_between_cells() {
|
||||
let env = mut_env(pid(13)).await;
|
||||
env.mutate
|
||||
.execute(MutateLayoutInput {
|
||||
project_id: env.project_id,
|
||||
layout_id: None,
|
||||
operation: LayoutOperation::Split {
|
||||
target: nid(1),
|
||||
direction: Direction::Row,
|
||||
new_leaf: nid(2),
|
||||
container: nid(9),
|
||||
},
|
||||
})
|
||||
.await
|
||||
.expect("split");
|
||||
env.mutate
|
||||
.execute(MutateLayoutInput {
|
||||
project_id: env.project_id,
|
||||
layout_id: None,
|
||||
operation: LayoutOperation::SetSession {
|
||||
target: nid(1),
|
||||
session: Some(sid(77)),
|
||||
},
|
||||
})
|
||||
.await
|
||||
.expect("set initial session");
|
||||
|
||||
let out = env
|
||||
.mutate
|
||||
.execute(MutateLayoutInput {
|
||||
project_id: env.project_id,
|
||||
layout_id: None,
|
||||
operation: LayoutOperation::AttachSession {
|
||||
target: nid(2),
|
||||
session: sid(77),
|
||||
},
|
||||
})
|
||||
.await
|
||||
.expect("attach existing session");
|
||||
|
||||
match &out.layout.root {
|
||||
LayoutNode::Split(split) => {
|
||||
match &split.children[0].node {
|
||||
LayoutNode::Leaf(leaf) => assert!(leaf.session.is_none()),
|
||||
_ => panic!("expected first leaf"),
|
||||
}
|
||||
match &split.children[1].node {
|
||||
LayoutNode::Leaf(leaf) => assert_eq!(leaf.session, Some(sid(77))),
|
||||
_ => panic!("expected second leaf"),
|
||||
}
|
||||
}
|
||||
_ => panic!("expected split root"),
|
||||
}
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn mutate_invalid_op_errors_and_does_not_persist() {
|
||||
let env = mut_env(pid(14)).await;
|
||||
@ -744,7 +815,9 @@ async fn create_layout_appends_and_activates_it() {
|
||||
.unwrap();
|
||||
|
||||
let list = ListLayouts::new(Arc::new(store), Arc::new(fs))
|
||||
.execute(ListLayoutsInput { project_id: pid(30) })
|
||||
.execute(ListLayoutsInput {
|
||||
project_id: pid(30),
|
||||
})
|
||||
.await
|
||||
.unwrap();
|
||||
assert_eq!(list.layouts.len(), 2, "Default + Backend");
|
||||
@ -774,21 +847,19 @@ async fn create_layout_rejects_empty_name() {
|
||||
#[tokio::test]
|
||||
async fn rename_layout_changes_the_name() {
|
||||
let (store, fs, bus) = mgmt_env(pid(32)).await;
|
||||
RenameLayout::new(
|
||||
Arc::new(store.clone()),
|
||||
Arc::new(fs.clone()),
|
||||
Arc::new(bus),
|
||||
)
|
||||
.execute(RenameLayoutInput {
|
||||
project_id: pid(32),
|
||||
layout_id: lid(1),
|
||||
name: "Main".to_owned(),
|
||||
})
|
||||
.await
|
||||
.unwrap();
|
||||
RenameLayout::new(Arc::new(store.clone()), Arc::new(fs.clone()), Arc::new(bus))
|
||||
.execute(RenameLayoutInput {
|
||||
project_id: pid(32),
|
||||
layout_id: lid(1),
|
||||
name: "Main".to_owned(),
|
||||
})
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
let list = ListLayouts::new(Arc::new(store), Arc::new(fs))
|
||||
.execute(ListLayoutsInput { project_id: pid(32) })
|
||||
.execute(ListLayoutsInput {
|
||||
project_id: pid(32),
|
||||
})
|
||||
.await
|
||||
.unwrap();
|
||||
assert_eq!(list.layouts[0].name, "Main");
|
||||
@ -825,21 +896,23 @@ async fn delete_active_layout_reassigns_active() {
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
let out = DeleteLayout::new(
|
||||
Arc::new(store.clone()),
|
||||
Arc::new(fs.clone()),
|
||||
Arc::new(bus),
|
||||
)
|
||||
.execute(DeleteLayoutInput {
|
||||
project_id: pid(34),
|
||||
layout_id: created.layout_id,
|
||||
})
|
||||
.await
|
||||
.unwrap();
|
||||
assert_eq!(out.active_id, lid(1), "active fell back to the Default layout");
|
||||
let out = DeleteLayout::new(Arc::new(store.clone()), Arc::new(fs.clone()), Arc::new(bus))
|
||||
.execute(DeleteLayoutInput {
|
||||
project_id: pid(34),
|
||||
layout_id: created.layout_id,
|
||||
})
|
||||
.await
|
||||
.unwrap();
|
||||
assert_eq!(
|
||||
out.active_id,
|
||||
lid(1),
|
||||
"active fell back to the Default layout"
|
||||
);
|
||||
|
||||
let list = ListLayouts::new(Arc::new(store), Arc::new(fs))
|
||||
.execute(ListLayoutsInput { project_id: pid(34) })
|
||||
.execute(ListLayoutsInput {
|
||||
project_id: pid(34),
|
||||
})
|
||||
.await
|
||||
.unwrap();
|
||||
assert_eq!(list.layouts.len(), 1);
|
||||
@ -863,17 +936,13 @@ async fn set_active_layout_switches_and_load_follows() {
|
||||
.unwrap();
|
||||
|
||||
// Switch back to the Default layout.
|
||||
SetActiveLayout::new(
|
||||
Arc::new(store.clone()),
|
||||
Arc::new(fs.clone()),
|
||||
Arc::new(bus),
|
||||
)
|
||||
.execute(SetActiveLayoutInput {
|
||||
project_id: pid(35),
|
||||
layout_id: lid(1),
|
||||
})
|
||||
.await
|
||||
.unwrap();
|
||||
SetActiveLayout::new(Arc::new(store.clone()), Arc::new(fs.clone()), Arc::new(bus))
|
||||
.execute(SetActiveLayoutInput {
|
||||
project_id: pid(35),
|
||||
layout_id: lid(1),
|
||||
})
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
let loaded = LoadLayout::new(Arc::new(store), Arc::new(fs))
|
||||
.execute(LoadLayoutInput {
|
||||
|
||||
Reference in New Issue
Block a user