Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 4 additions & 4 deletions src/App.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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";
Expand Down Expand Up @@ -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)) {
Expand All @@ -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<RuntimePlanStep>((label, index) => ({
id: `plan-${index}`,
Expand Down
18 changes: 16 additions & 2 deletions src/providers/planner.test.ts
Original file line number Diff line number Diff line change
@@ -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 () => {
Expand Down Expand Up @@ -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();
Expand Down Expand Up @@ -179,4 +193,4 @@ describe("summarizeSessionTitle", () => {
expect(await summarizeSessionTitle("hi", { provider: "minimax" })).toBeNull();
expect(await summarizeSessionTitle("", { provider: "minimax" })).toBeNull();
});
});
});
18 changes: 17 additions & 1 deletion src/providers/planner.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down Expand Up @@ -185,4 +201,4 @@ export async function summarizeSessionTitle(
} catch {
return fallback;
}
}
}
4 changes: 2 additions & 2 deletions src/views/HomeView.module.css
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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%;
}
Expand Down
Loading