From e464f52e489d5935331a4b59f95e3e31a78d6141 Mon Sep 17 00:00:00 2001 From: benclawbot Date: Mon, 13 Jul 2026 00:46:57 +0200 Subject: [PATCH] fix(chat): condense objectives and align prompts --- src/App.tsx | 8 ++++---- src/providers/planner.test.ts | 18 ++++++++++++++++-- src/providers/planner.ts | 18 +++++++++++++++++- src/views/HomeView.module.css | 4 ++-- 4 files changed, 39 insertions(+), 9 deletions(-) diff --git a/src/App.tsx b/src/App.tsx index 74fe95a..db84336 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -22,7 +22,7 @@ import { isTauriRuntime } from "./providers/minimax"; import { listSkills, loadSkill, type SkillDetail, type SkillSummary } from "./providers/skills"; import { useSlashMenu, type SlashItem } from "./providers/slash"; import { buildContextMessages, type UiChatBubble } from "./providers/context"; -import { generatePlanSteps, isSubstantiveObjective, summarizeSessionTitle } from "./providers/planner"; +import { generatePlanSteps, isSubstantiveObjective, summarizeObjectiveLine, summarizeSessionTitle } from "./providers/planner"; import { normalizeTokenUsage } from "./providers/tokenUsage"; import type { RuntimePlan, RuntimePlanStep } from "./agentRuntimeDeepLoop"; import { deleteSession, listSessions, newSessionId, saveSession, type PersistedSession } from "./providers/sessions"; @@ -1344,8 +1344,8 @@ const livePromptTokens = useMemo(() => { // for this objective. Result lands in `runtimePlan`, which // PlanProgressPanel reads to surface task-specific steps instead of // the generic 5-step boilerplate. Fire-and-forget — the agent loop - // doesn't wait for the plan; the panel shows the heuristic fallback - // until the LLM response arrives. + // doesn't wait for the plan; the panel shows a planning state until + // the objective-specific response arrives. setRuntimePlan(null); planObjectiveRef.current = prompt.trim(); if (!isSubstantiveObjective(prompt)) { @@ -1363,7 +1363,7 @@ const livePromptTokens = useMemo(() => { if (planObjectiveRef.current !== prompt.trim()) return; if (!steps) { setPlanPhase("unavailable"); return; } setRuntimePlan({ - objective: prompt.trim(), + objective: summarizeObjectiveLine(prompt), status: "in_progress", steps: steps.map((label, index) => ({ id: `plan-${index}`, diff --git a/src/providers/planner.test.ts b/src/providers/planner.test.ts index ed1a51c..46497ce 100644 --- a/src/providers/planner.test.ts +++ b/src/providers/planner.test.ts @@ -1,5 +1,5 @@ import { describe, expect, it, vi, beforeEach } from "vitest"; -import { isSubstantiveObjective, generatePlanSteps, summarizeSessionTitle } from "./planner"; +import { isSubstantiveObjective, generatePlanSteps, summarizeObjectiveLine, summarizeSessionTitle } from "./planner"; import * as registry from "./registry"; vi.mock("./registry", async () => { @@ -43,6 +43,20 @@ describe("planner.isSubstantiveObjective", () => { }); }); +describe("planner.summarizeObjectiveLine", () => { + it("reduces a long specification to a single concise line", () => { + const prompt = "Build an app according to this presentation of the app, make it beautiful and designed according to the context: Version courte — le pitch en 3 phrases\nCahier de dictées est une application web de suivi orthographique."; + const result = summarizeObjectiveLine(prompt); + expect(result).toBe("Build an app according to this presentation of the app, make it beautiful…"); + expect(result).not.toContain("\n"); + expect(result.length).toBeLessThanOrEqual(80); + }); + + it("leaves short objectives intact", () => { + expect(summarizeObjectiveLine("Fix the chat alignment" )).toBe("Fix the chat alignment"); + }); +}); + describe("planner.generatePlanSteps", () => { beforeEach(() => { vi.mocked(registry.dispatchChat).mockReset(); @@ -179,4 +193,4 @@ describe("summarizeSessionTitle", () => { expect(await summarizeSessionTitle("hi", { provider: "minimax" })).toBeNull(); expect(await summarizeSessionTitle("", { provider: "minimax" })).toBeNull(); }); -}); \ No newline at end of file +}); diff --git a/src/providers/planner.ts b/src/providers/planner.ts index 52e70e7..f9b5ceb 100644 --- a/src/providers/planner.ts +++ b/src/providers/planner.ts @@ -28,6 +28,22 @@ Objective: `; const SUBSTANTIVE_MIN_LEN = 24; +/** + * Produce a compact display label for the Plan panel without changing the + * full objective sent to the agent. Long specifications are reduced to their + * opening clause and capped at 13 words / 80 characters. + */ +export function summarizeObjectiveLine(objective: string): string { + const normalized = objective.replace(/\s+/g, " ").trim(); + if (normalized.length <= 80 && normalized.split(" ").length <= 13) return normalized; + const openingClause = normalized.split(/[:\n]|\s[—–]\s/)[0]?.trim() || normalized; + const words = openingClause.split(" ").filter(Boolean); + let summary = words.slice(0, 13).join(" "); + if (summary.length > 79) summary = summary.slice(0, 79).replace(/\s+\S*$/, ""); + const wasShortened = summary.length < normalized.length; + return `${summary.replace(/[.,;:!?]+$/, "")}${wasShortened ? "…" : ""}`; +} + /** * Clamp a single step label to ≤5 words and ≤40 chars so the plan * panel never ships prose-length bullets. Strips trailing punctuation, @@ -185,4 +201,4 @@ export async function summarizeSessionTitle( } catch { return fallback; } -} \ No newline at end of file +} diff --git a/src/views/HomeView.module.css b/src/views/HomeView.module.css index c489463..843d89a 100644 --- a/src/views/HomeView.module.css +++ b/src/views/HomeView.module.css @@ -55,7 +55,7 @@ /* User-side chat layout. Descends into the shared chat-bubble/avatar/body classes used by the shared chat surface; those rules stay global. Avatar is pinned to the right edge; the body fills the remaining row - width with right-aligned text, so long user prompts use the full + width with left-aligned text, so long user prompts use the full column instead of floating in a narrow right-side strip. Selector pairs `.chat-bubble` with the hashed local class so the rule wins the cascade against the global `.chat-bubble` block in @@ -74,7 +74,7 @@ grid-column: 1; grid-row: 1; justify-self: stretch; - text-align: right; + text-align: left; align-items: stretch; max-width: 100%; }