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:
@ -116,6 +116,7 @@ export function AgentsPanel({ projectId, projectRoot = "" }: AgentsPanelProps) {
|
||||
* set by the hook once launchAgent resolves.
|
||||
*/
|
||||
const [activeAgentId, setActiveAgentId] = useState<string | null>(null);
|
||||
const [panelNodeIds, setPanelNodeIds] = useState<Record<string, string>>({});
|
||||
|
||||
/**
|
||||
* Live PTY session id of the running agent terminal, keyed by agent id. Lets
|
||||
@ -149,11 +150,15 @@ export function AgentsPanel({ projectId, projectRoot = "" }: AgentsPanelProps) {
|
||||
}
|
||||
|
||||
function handleLaunch(agentId: string) {
|
||||
setPanelNodeIds((prev) => ({
|
||||
...prev,
|
||||
[agentId]: prev[agentId] ?? crypto.randomUUID(),
|
||||
}));
|
||||
setActiveAgentId(agentId);
|
||||
}
|
||||
|
||||
function handleStop() {
|
||||
vm.stopAgent();
|
||||
function handleStop(agentId?: string) {
|
||||
void vm.stopAgent(agentId);
|
||||
setActiveAgentId(null);
|
||||
}
|
||||
|
||||
@ -288,6 +293,7 @@ export function AgentsPanel({ projectId, projectRoot = "" }: AgentsPanelProps) {
|
||||
{vm.agents.map((a) => {
|
||||
const isSelected = a.id === vm.selectedAgentId;
|
||||
const isRunning = a.id === activeAgentId;
|
||||
const live = vm.liveAgents.find((candidate) => candidate.agentId === a.id);
|
||||
const profileName =
|
||||
vm.profiles.find((p) => p.id === a.profileId)?.name ??
|
||||
a.profileId;
|
||||
@ -318,6 +324,11 @@ export function AgentsPanel({ projectId, projectRoot = "" }: AgentsPanelProps) {
|
||||
)}
|
||||
</span>
|
||||
<span className="text-xs text-muted">{profileName}</span>
|
||||
{live && (
|
||||
<span className="text-xs text-primary">
|
||||
running in IdeA · {live.sessionId ?? live.nodeId}
|
||||
</span>
|
||||
)}
|
||||
</button>
|
||||
|
||||
<div className="flex items-center gap-1.5 shrink-0">
|
||||
@ -334,12 +345,12 @@ export function AgentsPanel({ projectId, projectRoot = "" }: AgentsPanelProps) {
|
||||
Sync
|
||||
</Button>
|
||||
)}
|
||||
{isRunning ? (
|
||||
{isRunning || live ? (
|
||||
<Button
|
||||
size="sm"
|
||||
variant="ghost"
|
||||
aria-label={`stop ${a.name}`}
|
||||
onClick={handleStop}
|
||||
onClick={() => handleStop(a.id)}
|
||||
className="text-danger hover:text-danger"
|
||||
>
|
||||
Stop
|
||||
@ -381,7 +392,11 @@ export function AgentsPanel({ projectId, projectRoot = "" }: AgentsPanelProps) {
|
||||
key={activeAgentId}
|
||||
cwd={projectRoot}
|
||||
open={(opts, onData) =>
|
||||
vm.launchAgent(activeAgentId, opts, onData)
|
||||
vm.launchAgent(
|
||||
activeAgentId,
|
||||
{ ...opts, nodeId: panelNodeIds[activeAgentId] },
|
||||
onData,
|
||||
)
|
||||
}
|
||||
reattach={(sessionId, onData) =>
|
||||
gateways.agent.reattach(sessionId, onData)
|
||||
|
||||
@ -10,7 +10,7 @@
|
||||
import { useCallback, useEffect, useState } from "react";
|
||||
|
||||
import type { Agent, AgentProfile, GatewayError } from "@/domain";
|
||||
import type { OpenTerminalOptions, TerminalHandle } from "@/ports";
|
||||
import type { LiveAgent, OpenTerminalOptions, TerminalHandle } from "@/ports";
|
||||
import { useGateways } from "@/app/di";
|
||||
|
||||
/** What the agents UI needs from this hook. */
|
||||
@ -23,6 +23,8 @@ export interface AgentsViewModel {
|
||||
context: string;
|
||||
/** Profiles available for assigning to a new agent. */
|
||||
profiles: AgentProfile[];
|
||||
/** Agents that currently own a live PTY session, as tracked by IdeA. */
|
||||
liveAgents: LiveAgent[];
|
||||
/** Last error message, or `null`. */
|
||||
error: string | null;
|
||||
/** Whether a request is in flight. */
|
||||
@ -34,6 +36,8 @@ export interface AgentsViewModel {
|
||||
runningAgentId: string | null;
|
||||
/** Reloads the agent list. */
|
||||
refresh: () => Promise<void>;
|
||||
/** Reloads the currently-live agent sessions. */
|
||||
refreshLiveAgents: () => Promise<void>;
|
||||
/** Creates a new agent and refreshes the list. */
|
||||
createAgent: (name: string, profileId: string, initialContent?: string) => Promise<void>;
|
||||
/** Selects an agent and loads its context. */
|
||||
@ -52,8 +56,8 @@ export interface AgentsViewModel {
|
||||
options: OpenTerminalOptions,
|
||||
onData: (bytes: Uint8Array) => void,
|
||||
) => Promise<TerminalHandle>;
|
||||
/** Clears `runningAgentId` (called by the Stop button to unmount the terminal). */
|
||||
stopAgent: () => void;
|
||||
/** Stops an agent PTY explicitly by closing its live session id. */
|
||||
stopAgent: (agentId?: string) => Promise<void>;
|
||||
}
|
||||
|
||||
function describe(e: unknown): string {
|
||||
@ -64,12 +68,13 @@ function describe(e: unknown): string {
|
||||
}
|
||||
|
||||
export function useAgents(projectId: string): AgentsViewModel {
|
||||
const { agent, profile, system } = useGateways();
|
||||
const { agent, profile, system, terminal } = useGateways();
|
||||
|
||||
const [agents, setAgents] = useState<Agent[]>([]);
|
||||
const [selectedAgentId, setSelectedAgentId] = useState<string | null>(null);
|
||||
const [context, setContext] = useState<string>("");
|
||||
const [profiles, setProfiles] = useState<AgentProfile[]>([]);
|
||||
const [liveAgents, setLiveAgents] = useState<LiveAgent[]>([]);
|
||||
const [error, setError] = useState<string | null>(null);
|
||||
const [busy, setBusy] = useState(false);
|
||||
const [runningAgentId, setRunningAgentId] = useState<string | null>(null);
|
||||
@ -91,10 +96,22 @@ export function useAgents(projectId: string): AgentsViewModel {
|
||||
}
|
||||
}, [agent, profile, projectId]);
|
||||
|
||||
const refreshLiveAgents = useCallback(async () => {
|
||||
try {
|
||||
setLiveAgents(await agent.listLiveAgents(projectId));
|
||||
} catch {
|
||||
setLiveAgents([]);
|
||||
}
|
||||
}, [agent, projectId]);
|
||||
|
||||
useEffect(() => {
|
||||
void refresh();
|
||||
}, [refresh]);
|
||||
|
||||
useEffect(() => {
|
||||
void refreshLiveAgents();
|
||||
}, [refreshLiveAgents]);
|
||||
|
||||
// Live refresh: the agents list is otherwise only reloaded on mount or after a
|
||||
// UI-driven CRUD. An agent created/launched/stopped *out of band* — most
|
||||
// notably by the orchestrator (`spawn_agent`, ARCHITECTURE §14.3) when an AI
|
||||
@ -109,6 +126,7 @@ export function useAgents(projectId: string): AgentsViewModel {
|
||||
.onDomainEvent((event) => {
|
||||
if (event.type === "agentLaunched" || event.type === "agentExited") {
|
||||
void refresh();
|
||||
void refreshLiveAgents();
|
||||
}
|
||||
})
|
||||
.then((un) => {
|
||||
@ -119,7 +137,7 @@ export function useAgents(projectId: string): AgentsViewModel {
|
||||
cancelled = true;
|
||||
unsubscribe?.();
|
||||
};
|
||||
}, [system, refresh]);
|
||||
}, [system, refresh, refreshLiveAgents]);
|
||||
|
||||
const createAgent = useCallback(
|
||||
async (name: string, profileId: string, initialContent?: string) => {
|
||||
@ -205,28 +223,51 @@ export function useAgents(projectId: string): AgentsViewModel {
|
||||
try {
|
||||
const handle = await agent.launchAgent(projectId, agentId, options, onData);
|
||||
setRunningAgentId(agentId);
|
||||
void refreshLiveAgents();
|
||||
return handle;
|
||||
} catch (e) {
|
||||
setError(describe(e));
|
||||
throw e;
|
||||
}
|
||||
},
|
||||
[agent, projectId],
|
||||
[agent, projectId, refreshLiveAgents],
|
||||
);
|
||||
|
||||
const stopAgent = useCallback(() => {
|
||||
setRunningAgentId(null);
|
||||
}, []);
|
||||
const stopAgent = useCallback(
|
||||
async (agentId?: string) => {
|
||||
const targetAgentId = agentId ?? runningAgentId;
|
||||
const live = targetAgentId
|
||||
? liveAgents.find((candidate) => candidate.agentId === targetAgentId)
|
||||
: null;
|
||||
setRunningAgentId((current) =>
|
||||
current === targetAgentId || !targetAgentId ? null : current,
|
||||
);
|
||||
if (live?.sessionId) {
|
||||
try {
|
||||
await terminal?.closeTerminal(live.sessionId);
|
||||
setLiveAgents((current) =>
|
||||
current.filter((candidate) => candidate.agentId !== targetAgentId),
|
||||
);
|
||||
} catch (e) {
|
||||
setError(describe(e));
|
||||
await refreshLiveAgents();
|
||||
}
|
||||
}
|
||||
},
|
||||
[liveAgents, refreshLiveAgents, runningAgentId, terminal],
|
||||
);
|
||||
|
||||
return {
|
||||
agents,
|
||||
selectedAgentId,
|
||||
context,
|
||||
profiles,
|
||||
liveAgents,
|
||||
error,
|
||||
busy,
|
||||
runningAgentId,
|
||||
refresh,
|
||||
refreshLiveAgents,
|
||||
createAgent,
|
||||
selectAgent,
|
||||
saveContext,
|
||||
|
||||
Reference in New Issue
Block a user