mirror of
https://github.com/R0m1k3/Loki.git
synced 2026-10-11 17:26:57 +02:00
Plan : validation des étapes en direct (point par point)
Le plan s'affichait mais restait figé — on ne voyait pas le modèle avancer. Désormais : - Le modèle annonce chaque étape accomplie (« ✅ Étape N terminée »), demandé dans la consigne de plan. - L'agent détecte ces annonces dans le flux et émet des événements plan_step, sans jamais recompter deux fois la même étape. - Le panneau PLAN coche les étapes en temps réel : ✓ vert + barré pour les faites, sablier sur l'étape en cours, compteur « n/N validées ». Chemin agent uniquement (le moteur code tourne de bout en bout). Les messages passés restent affichés tels quels. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01SVay7z3y7q2gEe54ByAE6N
This commit is contained in:
5 files changed
+112
-15
No files matched your search
@@ -575,6 +575,7 @@ export async function streamChat(
|
||||
onStatus: (msg: string) => void;
|
||||
onNotice: (msg: string) => void;
|
||||
onPlan?: (steps: string[]) => void;
|
||||
onPlanStep?: (index: number) => void;
|
||||
onRevision?: (content: string) => void;
|
||||
onDone: (full: string) => void;
|
||||
onError: (msg: string) => void;
|
||||
@@ -638,6 +639,7 @@ export async function streamChat(
|
||||
const payload = JSON.parse(dataLines.join("\n"));
|
||||
if (event === "token") handlers.onToken(payload.content);
|
||||
else if (event === "plan") handlers.onPlan?.(payload.steps);
|
||||
else if (event === "plan_step") handlers.onPlanStep?.(payload.index);
|
||||
else if (event === "revision") handlers.onRevision?.(payload.content);
|
||||
else if (event === "thinking") handlers.onThinking(payload.content);
|
||||
else if (event === "status") handlers.onStatus(payload.message);
|
||||
|
||||
@@ -20,6 +20,7 @@ export function ChatPanel() {
|
||||
streamNotice,
|
||||
streamTools,
|
||||
streamPlan,
|
||||
streamPlanDone,
|
||||
sendMessage,
|
||||
currentSessionId,
|
||||
config,
|
||||
@@ -40,7 +41,7 @@ export function ChatPanel() {
|
||||
// Auto-scroll vers le bas à chaque token / message.
|
||||
useEffect(() => {
|
||||
scrollRef.current?.scrollTo({ top: scrollRef.current.scrollHeight });
|
||||
}, [messages, streamContent, streamThinking, streamTools, streamPlan]);
|
||||
}, [messages, streamContent, streamThinking, streamTools, streamPlan, streamPlanDone]);
|
||||
|
||||
const submit = () => {
|
||||
if (!draft.trim() || streaming) return;
|
||||
@@ -111,6 +112,7 @@ export function ChatPanel() {
|
||||
pendingStatus={streamStatus}
|
||||
notice={streamNotice}
|
||||
thinking={streamThinking}
|
||||
planDone={streamPlanDone}
|
||||
/>
|
||||
)}
|
||||
{showingStreaming && pendingShell && (
|
||||
@@ -234,7 +236,22 @@ function ModeSelector() {
|
||||
);
|
||||
}
|
||||
|
||||
function PlanCard({ steps }: { steps: string[] }) {
|
||||
function PlanCard({
|
||||
steps,
|
||||
done,
|
||||
live,
|
||||
}: {
|
||||
steps: string[];
|
||||
done?: number[];
|
||||
live?: boolean;
|
||||
}) {
|
||||
const doneSet = new Set(done ?? []);
|
||||
// Étape active = la première non validée, uniquement pendant le stream.
|
||||
const activeIdx = live
|
||||
? steps.findIndex((_, i) => !doneSet.has(i))
|
||||
: -1;
|
||||
const doneCount = doneSet.size;
|
||||
|
||||
return (
|
||||
<div
|
||||
className="mb-[11px] overflow-hidden border-[3px] border-line bg-card shadow-hard-sm"
|
||||
@@ -243,18 +260,39 @@ function PlanCard({ steps }: { steps: string[] }) {
|
||||
<div className="flex items-center gap-2 border-b-2 border-line-soft px-3 py-2">
|
||||
<span className="font-pixel text-[9px] text-accent">PLAN</span>
|
||||
<span className="text-[12px] text-muted-2">
|
||||
{steps.length} étape{steps.length > 1 ? "s" : ""}
|
||||
{doneCount > 0
|
||||
? `${doneCount}/${steps.length} validée${doneCount > 1 ? "s" : ""}`
|
||||
: `${steps.length} étape${steps.length > 1 ? "s" : ""}`}
|
||||
</span>
|
||||
</div>
|
||||
<ol className="m-0 list-none px-3 py-2">
|
||||
{steps.map((s, i) => (
|
||||
<li key={i} className="flex gap-2 py-[3px] text-[13px] text-ink-2">
|
||||
<span className="flex h-[18px] w-[18px] flex-none items-center justify-center border-2 border-line bg-base text-[11px] text-ink">
|
||||
{i + 1}
|
||||
</span>
|
||||
<span className="min-w-0">{s}</span>
|
||||
</li>
|
||||
))}
|
||||
{steps.map((s, i) => {
|
||||
const isDone = doneSet.has(i);
|
||||
const isActive = i === activeIdx;
|
||||
return (
|
||||
<li
|
||||
key={i}
|
||||
className={`flex gap-2 py-[3px] text-[13px] ${
|
||||
isDone ? "text-muted-2" : "text-ink-2"
|
||||
}`}
|
||||
>
|
||||
<span
|
||||
className={`flex h-[18px] w-[18px] flex-none items-center justify-center border-2 text-[11px] ${
|
||||
isDone
|
||||
? "border-ok bg-ok text-white"
|
||||
: isActive
|
||||
? "border-accent bg-base text-accent"
|
||||
: "border-line bg-base text-ink"
|
||||
}`}
|
||||
>
|
||||
{isDone ? "✓" : isActive ? "…" : i + 1}
|
||||
</span>
|
||||
<span className={`min-w-0 ${isDone ? "line-through" : ""}`}>
|
||||
{s}
|
||||
</span>
|
||||
</li>
|
||||
);
|
||||
})}
|
||||
</ol>
|
||||
</div>
|
||||
);
|
||||
@@ -266,12 +304,14 @@ function Bubble({
|
||||
pendingStatus,
|
||||
notice,
|
||||
thinking,
|
||||
planDone,
|
||||
}: {
|
||||
msg: Message;
|
||||
pending?: boolean;
|
||||
pendingStatus?: string;
|
||||
notice?: string | null;
|
||||
thinking?: string;
|
||||
planDone?: number[];
|
||||
}) {
|
||||
const time = new Date(msg.created_at * 1000).toLocaleTimeString("fr-FR", {
|
||||
hour: "2-digit",
|
||||
@@ -315,7 +355,7 @@ function Bubble({
|
||||
live={!!pending}
|
||||
/>
|
||||
{(msg.meta?.plan?.length ?? 0) > 0 && (
|
||||
<PlanCard steps={msg.meta!.plan!} />
|
||||
<PlanCard steps={msg.meta!.plan!} done={planDone} live={!!pending} />
|
||||
)}
|
||||
{(msg.meta?.tools ?? []).map((t: ToolCall, i: number) => (
|
||||
<ToolCard key={i} call={t} />
|
||||
|
||||
@@ -51,6 +51,7 @@ interface LokiState {
|
||||
streamNotice: string | null;
|
||||
streamTools: ToolCall[]; // appels d'outils de la réponse en cours
|
||||
streamPlan: string[]; // plan de la réponse en cours
|
||||
streamPlanDone: number[]; // index des étapes validées en direct
|
||||
|
||||
fileTree: FileNode[];
|
||||
previewPath: string | null;
|
||||
@@ -114,6 +115,7 @@ export const useStore = create<LokiState>((set, get) => ({
|
||||
streamNotice: null,
|
||||
streamTools: [],
|
||||
streamPlan: [],
|
||||
streamPlanDone: [],
|
||||
fileTree: [],
|
||||
previewPath: null,
|
||||
previewContent: "",
|
||||
@@ -410,6 +412,8 @@ export const useStore = create<LokiState>((set, get) => ({
|
||||
streamStatus: "Connexion à Ollama…",
|
||||
streamNotice: null,
|
||||
streamTools: [],
|
||||
streamPlan: [],
|
||||
streamPlanDone: [],
|
||||
pendingShell: null,
|
||||
});
|
||||
|
||||
@@ -427,7 +431,13 @@ export const useStore = create<LokiState>((set, get) => ({
|
||||
onToken: (t) => set({ streamContent: get().streamContent + t }),
|
||||
onThinking: (t) => set({ streamThinking: get().streamThinking + t }),
|
||||
onStatus: (message) => set({ streamStatus: message }),
|
||||
onPlan: (steps) => set({ streamPlan: steps }),
|
||||
onPlan: (steps) => set({ streamPlan: steps, streamPlanDone: [] }),
|
||||
onPlanStep: (index) =>
|
||||
set({
|
||||
streamPlanDone: get().streamPlanDone.includes(index)
|
||||
? get().streamPlanDone
|
||||
: [...get().streamPlanDone, index],
|
||||
}),
|
||||
onRevision: (content) => set({ streamContent: content }),
|
||||
onNotice: (message) => set({ streamNotice: message }),
|
||||
onToolCall: (call) =>
|
||||
@@ -463,6 +473,8 @@ export const useStore = create<LokiState>((set, get) => ({
|
||||
streamStatus: "",
|
||||
streamNotice: null,
|
||||
streamTools: [],
|
||||
streamPlan: [],
|
||||
streamPlanDone: [],
|
||||
});
|
||||
// Recharge depuis la base + l'arborescence (fichiers créés par
|
||||
// l'agent) — trois requêtes indépendantes, en parallèle.
|
||||
|
||||
Reference in new issue
Block a user