- 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>
116 lines
3.1 KiB
TypeScript
116 lines
3.1 KiB
TypeScript
/**
|
|
* `ProjectContextPanel` — edits the shared project context stored in
|
|
* `.ideai/CONTEXT.md`.
|
|
*
|
|
* This context is injected into every agent launch before the agent persona,
|
|
* regardless of the selected AI profile/model.
|
|
*/
|
|
|
|
import { useEffect, useState } from "react";
|
|
|
|
import type { GatewayError } from "@/domain";
|
|
import { Button, Panel, Spinner, cn } from "@/shared";
|
|
import { useGateways } from "@/app/di";
|
|
|
|
export interface ProjectContextPanelProps {
|
|
/** Project whose `.ideai/CONTEXT.md` should be edited. */
|
|
projectId: string;
|
|
}
|
|
|
|
function describe(e: unknown): string {
|
|
if (e && typeof e === "object" && "message" in e) {
|
|
return String((e as GatewayError).message);
|
|
}
|
|
return String(e);
|
|
}
|
|
|
|
export function ProjectContextPanel({ projectId }: ProjectContextPanelProps) {
|
|
const { project } = useGateways();
|
|
const [content, setContent] = useState("");
|
|
const [savedContent, setSavedContent] = useState("");
|
|
const [busy, setBusy] = useState(false);
|
|
const [error, setError] = useState<string | null>(null);
|
|
|
|
useEffect(() => {
|
|
let cancelled = false;
|
|
setBusy(true);
|
|
setError(null);
|
|
project
|
|
.readProjectContext(projectId)
|
|
.then((text) => {
|
|
if (!cancelled) {
|
|
setContent(text);
|
|
setSavedContent(text);
|
|
}
|
|
})
|
|
.catch((e) => {
|
|
if (!cancelled) setError(describe(e));
|
|
})
|
|
.finally(() => {
|
|
if (!cancelled) setBusy(false);
|
|
});
|
|
return () => {
|
|
cancelled = true;
|
|
};
|
|
}, [project, projectId]);
|
|
|
|
async function save() {
|
|
setBusy(true);
|
|
setError(null);
|
|
try {
|
|
await project.updateProjectContext(projectId, content);
|
|
setSavedContent(content);
|
|
} catch (e) {
|
|
setError(describe(e));
|
|
} finally {
|
|
setBusy(false);
|
|
}
|
|
}
|
|
|
|
const dirty = content !== savedContent;
|
|
|
|
return (
|
|
<Panel title="Project Context" className="flex flex-col gap-0">
|
|
{error && (
|
|
<p
|
|
role="alert"
|
|
className="mx-4 mt-3 rounded-md border border-danger/40 bg-danger/10 px-3 py-2 text-sm text-danger"
|
|
>
|
|
{error}
|
|
</p>
|
|
)}
|
|
|
|
<div className="border-b border-border px-4 py-2">
|
|
<code className="text-xs text-muted">.ideai/CONTEXT.md</code>
|
|
</div>
|
|
|
|
<div className="flex flex-col gap-3 p-4">
|
|
<textarea
|
|
aria-label="project context"
|
|
value={content}
|
|
onChange={(e) => setContent(e.target.value)}
|
|
rows={18}
|
|
className={cn(
|
|
"w-full rounded-md bg-raised px-3 py-2 text-sm text-content",
|
|
"border border-border outline-none transition-colors",
|
|
"focus:border-primary placeholder:text-faint",
|
|
"disabled:cursor-not-allowed disabled:opacity-50 resize-y font-mono",
|
|
)}
|
|
disabled={busy}
|
|
/>
|
|
|
|
<div className="flex items-center justify-end gap-2">
|
|
{busy && <Spinner size={14} />}
|
|
<Button
|
|
variant="primary"
|
|
disabled={busy || !dirty}
|
|
onClick={() => void save()}
|
|
>
|
|
Save
|
|
</Button>
|
|
</div>
|
|
</div>
|
|
</Panel>
|
|
);
|
|
}
|