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:
2026-06-09 09:24:51 +02:00
parent 32398827fb
commit 785e9935fd
118 changed files with 5793 additions and 866 deletions

View File

@ -7,45 +7,47 @@
use tauri::ipc::Channel;
use tauri::State;
use crate::dto::DismissEmbedderSuggestionRequestDto;
use application::{
AppError, CloseProjectInput, CreateAgentInput, CreateLayoutInput, DeleteAgentInput,
DeleteLayoutInput, DeleteTemplateInput, DetectAgentDriftInput, GitBranchesInput,
GitCheckoutInput, GitCommitInput, GitGraphInput, GitInitInput, GitLogInput, GitStagePathInput,
GitStatusInput, InspectConversationInput, LaunchAgentInput, ListAgentsInput, ListLayoutsInput, LoadLayoutInput,
MutateLayoutInput, OpenProjectInput, ReadAgentContextInput, RenameLayoutInput,
SetActiveLayoutInput, SnapshotRunningAgentsInput, SyncAgentWithTemplateInput,
UpdateAgentContextInput,
AssignSkillToAgentInput, CreateSkillInput, DeleteSkillInput, ListSkillsInput,
UnassignSkillFromAgentInput, UpdateSkillInput,
CreateMemoryInput, DeleteMemoryInput, GetMemoryInput, ListMemoriesInput, ReadMemoryIndexInput,
RecallMemoryInput, ResolveMemoryLinksInput, UpdateMemoryInput,
AppError, AssignSkillToAgentInput, CloseProjectInput, CreateAgentInput, CreateLayoutInput,
CreateMemoryInput, CreateSkillInput, DeleteAgentInput, DeleteEmbedderProfileInput,
DeleteLayoutInput, DeleteMemoryInput, DeleteSkillInput, DeleteTemplateInput,
DetectAgentDriftInput, GetMemoryInput, GitBranchesInput, GitCheckoutInput, GitCommitInput,
GitGraphInput, GitInitInput, GitLogInput, GitStagePathInput, GitStatusInput,
InspectConversationInput, LaunchAgentInput, ListAgentsInput, ListLayoutsInput,
ListMemoriesInput, ListSkillsInput, LoadLayoutInput, MutateLayoutInput, OpenProjectInput,
ReadAgentContextInput, ReadMemoryIndexInput, ReadProjectContextInput, RecallMemoryInput,
RenameLayoutInput, ResolveMemoryLinksInput, SetActiveLayoutInput, SnapshotRunningAgentsInput,
SyncAgentWithTemplateInput, UnassignSkillFromAgentInput, UpdateAgentContextInput,
UpdateMemoryInput, UpdateProjectContextInput, UpdateSkillInput,
};
use domain::ports::PtyHandle;
use crate::dto::{
parse_agent_id, parse_close_terminal, parse_delete_profile, parse_layout_id, parse_node_id,
parse_profile_id, LiveAgentListDto,
parse_project_id, parse_session_id, parse_template_id, AgentDriftListDto, AgentDto,
AgentListDto, ConfigureProfilesRequestDto, CreateAgentFromTemplateRequestDto,
CreateAgentRequestDto, CreateLayoutRequestDto, CreateLayoutResultDto, CreateProjectRequestDto,
parse_agent_id, parse_close_terminal, parse_delete_profile, parse_layout_id, parse_memory_slug,
parse_node_id, parse_profile_id, parse_project_id, parse_session_id, parse_skill_id,
parse_template_id, AgentDriftListDto, AgentDto, AgentListDto, AssignSkillRequestDto,
AttachLiveAgentRequestDto, ConfigureProfilesRequestDto, ConversationDetailsDto,
CreateAgentFromTemplateRequestDto, CreateAgentRequestDto, CreateLayoutRequestDto,
CreateLayoutResultDto, CreateMemoryRequestDto, CreateProjectRequestDto, CreateSkillRequestDto,
CreateTemplateRequestDto, DeleteLayoutRequestDto, DeleteLayoutResultDto,
DetectProfilesRequestDto, DetectProfilesResponseDto, ErrorDto, FirstRunStateDto,
GitBranchesDto, GitCheckoutRequestDto, GitCommitDto, GitCommitListDto, GitCommitRequestDto,
GitStageRequestDto, GitStatusListDto, GraphCommitListDto, HealthRequestDto, HealthResponseDto,
ConversationDetailsDto, InspectConversationRequestDto,
LaunchAgentRequestDto, LayoutDto, LayoutOperationDto, ListLayoutsDto, OpenTerminalRequestDto,
ProfileDto, ProfileListDto, ProjectDto, ProjectListDto, ReadAgentContextResponseDto,
ReattachResultDto, RenameLayoutRequestDto, ResizeTerminalRequestDto, SaveProfileRequestDto,
SetActiveLayoutRequestDto, SyncAgentWithTemplateRequestDto, SyncResultDto, TemplateDto,
TemplateListDto, TerminalClosedDto, TerminalSessionDto, UpdateAgentContextRequestDto,
UpdateTemplateRequestDto, WriteTerminalRequestDto, parse_skill_id, AssignSkillRequestDto,
CreateSkillRequestDto, SkillDto, SkillListDto, UnassignSkillRequestDto, UpdateSkillRequestDto,
parse_memory_slug, CreateMemoryRequestDto, MemoryDto, MemoryIndexDto, MemoryLinksDto,
MemoryListDto, RecallMemoryRequestDto, UpdateMemoryRequestDto,
DetectProfilesRequestDto, DetectProfilesResponseDto, EmbedderEnginesDto, EmbedderProfileDto,
EmbedderProfileListDto, ErrorDto, FirstRunStateDto, GitBranchesDto, GitCheckoutRequestDto,
GitCommitDto, GitCommitListDto, GitCommitRequestDto, GitStageRequestDto, GitStatusListDto,
GraphCommitListDto, HealthRequestDto, HealthResponseDto, InspectConversationRequestDto,
LaunchAgentRequestDto, LayoutDto, LayoutOperationDto, ListLayoutsDto, LiveAgentListDto,
MemoryDto, MemoryIndexDto, MemoryLinksDto, MemoryListDto, OpenTerminalRequestDto, ProfileDto,
ProfileListDto, ProjectDto, ProjectListDto, ReadAgentContextResponseDto, ReattachResultDto,
RecallMemoryRequestDto, RenameLayoutRequestDto, ResizeTerminalRequestDto,
SaveEmbedderProfileRequestDto, SaveProfileRequestDto, SetActiveLayoutRequestDto, SkillDto,
SkillListDto, SyncAgentWithTemplateRequestDto, SyncResultDto, TemplateDto, TemplateListDto,
TerminalClosedDto, TerminalSessionDto, UnassignSkillRequestDto, UpdateAgentContextRequestDto,
UpdateMemoryRequestDto, UpdateProjectContextRequestDto, UpdateSkillRequestDto,
UpdateTemplateRequestDto, WriteTerminalRequestDto,
};
use domain::{SkillRef, SkillScope};
use crate::pty::{PtyBridge, PtyChunk};
use crate::state::AppState;
use domain::{SkillRef, SkillScope};
/// `health` — trivial command validating the full IPC pipeline
/// (frontend gateway → invoke → command → use case → ports → event relay).
@ -112,10 +114,7 @@ pub async fn open_project(
/// # Errors
/// Returns an [`ErrorDto`] (`INVALID` for a malformed id, `STORE` on failure).
#[tauri::command]
pub async fn close_project(
project_id: String,
state: State<'_, AppState>,
) -> Result<(), ErrorDto> {
pub async fn close_project(project_id: String, state: State<'_, AppState>) -> Result<(), ErrorDto> {
let id = parse_project_id(&project_id)?;
// T5: freeze `agent_was_running` on every agent leaf BEFORE any PTY release,
// reading the live registry as it stands now. Best-effort: a snapshot failure
@ -152,6 +151,49 @@ pub async fn list_projects(state: State<'_, AppState>) -> Result<ProjectListDto,
.map_err(ErrorDto::from)
}
/// `read_project_context` — read `.ideai/CONTEXT.md` for a project.
///
/// Missing context is returned as an empty string so a project whose `.ideai/`
/// was deleted can still open cleanly.
///
/// # Errors
/// Returns an [`ErrorDto`] (`INVALID` for a malformed id, `NOT_FOUND` if the
/// project is unknown, `FILESYSTEM`/`STORE` on read or UTF-8 failure).
#[tauri::command]
pub async fn read_project_context(
project_id: String,
state: State<'_, AppState>,
) -> Result<String, ErrorDto> {
let project = resolve_project(&project_id, &state).await?;
state
.read_project_context
.execute(ReadProjectContextInput { project })
.await
.map(|out| out.content)
.map_err(ErrorDto::from)
}
/// `update_project_context` — overwrite `.ideai/CONTEXT.md` for a project.
///
/// # Errors
/// Returns an [`ErrorDto`] (`INVALID` for a malformed id, `NOT_FOUND` if the
/// project is unknown, `FILESYSTEM` on write failure).
#[tauri::command]
pub async fn update_project_context(
request: UpdateProjectContextRequestDto,
state: State<'_, AppState>,
) -> Result<(), ErrorDto> {
let project = resolve_project(&request.project_id, &state).await?;
state
.update_project_context
.execute(UpdateProjectContextInput {
project,
content: request.content,
})
.await
.map_err(ErrorDto::from)
}
// ---------------------------------------------------------------------------
// Terminals (L3)
// ---------------------------------------------------------------------------
@ -224,10 +266,7 @@ pub fn write_terminal(
state: State<'_, AppState>,
) -> Result<(), ErrorDto> {
let input = request.into_input()?;
state
.write_terminal
.execute(input)
.map_err(ErrorDto::from)
state.write_terminal.execute(input).map_err(ErrorDto::from)
}
/// `resize_terminal` — resize a live PTY.
@ -241,10 +280,7 @@ pub fn resize_terminal(
state: State<'_, AppState>,
) -> Result<(), ErrorDto> {
let input = request.into_input()?;
state
.resize_terminal
.execute(input)
.map_err(ErrorDto::from)
state.resize_terminal.execute(input).map_err(ErrorDto::from)
}
/// `close_terminal` — kill a live PTY and tear down its channel.
@ -629,6 +665,99 @@ pub async fn configure_profiles(
.map_err(ErrorDto::from)
}
// ---------------------------------------------------------------------------
// Embedder profiles & engines (LOT C2 — §14.5.3)
// ---------------------------------------------------------------------------
/// `list_embedder_profiles` — list the configured embedder profiles (empty when
/// none configured ⇒ the default `none` posture).
///
/// # Errors
/// Returns an [`ErrorDto`] (`STORE` on `embedder.json` I/O failure).
#[tauri::command]
pub async fn list_embedder_profiles(
state: State<'_, AppState>,
) -> Result<EmbedderProfileListDto, ErrorDto> {
state
.list_embedder_profiles
.execute()
.await
.map(EmbedderProfileListDto::from)
.map_err(ErrorDto::from)
}
/// `save_embedder_profile` — create or replace (by id) a single embedder profile.
///
/// # Errors
/// Returns an [`ErrorDto`] (`INVALID` for empty id/name or zero dimension, `STORE`
/// on I/O failure).
#[tauri::command]
pub async fn save_embedder_profile(
request: SaveEmbedderProfileRequestDto,
state: State<'_, AppState>,
) -> Result<EmbedderProfileDto, ErrorDto> {
state
.save_embedder_profile
.execute(request.into())
.await
.map(EmbedderProfileDto::from)
.map_err(ErrorDto::from)
}
/// `delete_embedder_profile` — delete an embedder profile by id.
///
/// # Errors
/// Returns an [`ErrorDto`] (`NOT_FOUND` if absent, `STORE` on I/O failure).
#[tauri::command]
pub async fn delete_embedder_profile(
embedder_id: String,
state: State<'_, AppState>,
) -> Result<(), ErrorDto> {
state
.delete_embedder_profile
.execute(DeleteEmbedderProfileInput { id: embedder_id })
.await
.map_err(ErrorDto::from)
}
/// `describe_embedder_engines` — describe the engines available to the
/// "configure an embedder?" UI: the recommended ONNX catalogue, a best-effort
/// snapshot of the local environment, and which strategies are compiled in.
///
/// # Errors
/// Returns an [`ErrorDto`] — in practice never (the environment probe is best-effort).
#[tauri::command]
pub async fn describe_embedder_engines(
state: State<'_, AppState>,
) -> Result<EmbedderEnginesDto, ErrorDto> {
state
.describe_embedder_engines
.execute()
.await
.map(EmbedderEnginesDto::from)
.map_err(ErrorDto::from)
}
/// `dismiss_embedder_suggestion` — persist the user's response to the one-time
/// embedder suggestion (LOT C3 — §14.5.5): `later` (re-proposable next session) or
/// `never` (silenced for good). Resolves the project root from `project_id`.
///
/// # Errors
/// Returns an [`ErrorDto`] (`INVALID` for a malformed id, `NOT_FOUND` if the project
/// is unknown, `STORE` on `.embedder-prompt.json` I/O failure).
#[tauri::command]
pub async fn dismiss_embedder_suggestion(
request: DismissEmbedderSuggestionRequestDto,
state: State<'_, AppState>,
) -> Result<(), ErrorDto> {
let project = resolve_project(&request.project_id, &state).await?;
state
.dismiss_embedder_suggestion
.execute(request.into_input(project.root))
.await
.map_err(ErrorDto::from)
}
// ---------------------------------------------------------------------------
// Agents (L6)
// ---------------------------------------------------------------------------
@ -714,6 +843,36 @@ pub fn list_live_agents(
))
}
/// `attach_live_agent` — rebind an already-running agent session to a visible
/// layout cell without respawning the CLI process.
///
/// This is the backend side of "a cell is a view": a closed cell can leave an
/// agent running in the background, and opening the agent in a new cell updates
/// the session's host node while preserving the PTY/session/scrollback.
///
/// # Errors
/// Returns an [`ErrorDto`] (`INVALID` for malformed ids, `NOT_FOUND` if the
/// project/agent/live session is unknown).
#[tauri::command]
pub async fn attach_live_agent(
request: AttachLiveAgentRequestDto,
state: State<'_, AppState>,
) -> Result<LiveAgentListDto, ErrorDto> {
let _project = resolve_project(&request.project_id, &state).await?;
let agent_id = parse_agent_id(&request.agent_id)?;
let node_id = parse_node_id(&request.node_id)?;
let session = state
.terminal_sessions
.rebind_agent_node(&agent_id, node_id)
.ok_or_else(|| AppError::NotFound(format!("running session for agent {agent_id}")))?;
Ok(LiveAgentListDto::from_pairs(vec![(
agent_id,
session.node_id,
session.id,
)]))
}
/// `read_agent_context` — read an agent's Markdown context.
///
/// # Errors
@ -1190,10 +1349,7 @@ pub async fn git_log(
/// Returns an [`ErrorDto`] (`INVALID` for a bad project id or root, `GIT` on
/// failure).
#[tauri::command]
pub async fn git_init(
project_id: String,
state: State<'_, AppState>,
) -> Result<(), ErrorDto> {
pub async fn git_init(project_id: String, state: State<'_, AppState>) -> Result<(), ErrorDto> {
let project = resolve_project(&project_id, &state).await?;
let root = project.root.as_str().to_owned();
state
@ -1231,8 +1387,8 @@ pub async fn git_graph(
// Windows (L10)
// ---------------------------------------------------------------------------
use application::MoveTabToNewWindowInput;
use crate::dto::{parse_tab_id, MoveTabResultDto};
use application::MoveTabToNewWindowInput;
/// `move_tab_to_new_window` — detach a tab into a brand-new OS window.
///