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
12 changes: 12 additions & 0 deletions components/AppShell.auto-name.test.mjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
import assert from "node:assert/strict";
import fs from "node:fs";
import test from "node:test";

const source = fs.readFileSync(new URL("./AppShell.tsx", import.meta.url), "utf8");

test("压缩后的会话仍可根据持久化消息数生成标题", () => {
assert.match(
source,
/\(sessionStats\?\.userMessages \?\? 0\) > 0 \|\| selectedSession\.messageCount > 0/,
);
});
3 changes: 2 additions & 1 deletion components/AppShell.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -1023,9 +1023,10 @@ export function AppShell() {
{!isMobile && <span>{translate("history.label")}</span>}
</button>
{(() => {
// 上下文压缩后当前消息可能不再包含 user 消息,需同时参考会话文件的消息总数。
const hasMessages = Boolean(
selectedSession
&& (sessionStats?.userMessages ?? selectedSession.messageCount) > 0,
&& ((sessionStats?.userMessages ?? 0) > 0 || selectedSession.messageCount > 0),
);
const disabled = !selectedSession || !hasMessages || autoNameStatus.kind === "naming";
const isSuccess = autoNameStatus.kind === "success";
Expand Down
43 changes: 43 additions & 0 deletions lib/session-title.test.mjs
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import assert from "node:assert/strict";
import test from "node:test";
import { createAssistantMessageEventStream } from "@earendil-works/pi-ai";
import { convertToLlm } from "@earendil-works/pi-coding-agent";
import { createJiti } from "jiti";

const jiti = createJiti(import.meta.url);
Expand Down Expand Up @@ -103,6 +104,48 @@ test("waits for the source reply before sending the title prompt", async () => {
assert.deepEqual(providerRoles, ["user", "assistant", "user"]);
});

test("generates a title when compaction removed all literal user messages", async () => {
let providerMessages;
const sourceAgent = {
state: {
systemPrompt: "system",
model: { provider: "test", id: "test-model" },
thinkingLevel: "off",
tools: [],
messages: [
{
role: "compactionSummary",
summary: "The user asked to fix title generation after compaction.",
tokensBefore: 100_000,
timestamp: 1,
},
assistantMessage("The implementation is complete"),
],
},
waitForIdle: async () => {},
convertToLlm,
streamFunction: (_model, context) => {
providerMessages = context.messages;
const stream = createAssistantMessageEventStream();
queueMicrotask(() => {
stream.push({
type: "done",
reason: "stop",
message: assistantMessage("Title Generation After Compaction"),
});
});
return stream;
},
sessionId: "source-session-id",
};

const result = await generateSessionTitle({ agent: sourceAgent });

assert.equal(result.title, "Title Generation After Compaction");
assert.deepEqual(providerMessages.map((message) => message.role), ["user", "assistant", "user"]);
assert.match(providerMessages[0].content[0].text, /fix title generation after compaction/);
});

test("temporary title agent preserves the provider-facing prefix", async () => {
const model = { provider: "test", id: "cached-model" };
const messages = [{ role: "user", content: [{ type: "text", text: "Fix it" }] }];
Expand Down
4 changes: 3 additions & 1 deletion lib/session-title.ts
Original file line number Diff line number Diff line change
Expand Up @@ -214,7 +214,9 @@ export async function generateSessionTitle(source: AgentSession): Promise<Genera

const sanitizedMessages = sanitizeTitleMessages(sourceAgent.state.messages);
const historyLength = sanitizedMessages.length;
if (!sanitizedMessages.some((message) => message.role === "user")) {
if (!sanitizedMessages.some(
(message) => message.role === "user" || message.role === "compactionSummary",
)) {
throw new Error("The session has no user messages to name");
}

Expand Down