assistme 0.8.8 → 0.8.10
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/index.js +86 -8
- package/package.json +1 -1
- package/src/agent/title-generator.ts +82 -0
- package/src/orchestrator.ts +28 -0
- package/tests/agent/title-generator.test.ts +196 -0
package/dist/index.js
CHANGED
|
@@ -1290,6 +1290,64 @@ var SessionHeartbeat = class {
|
|
|
1290
1290
|
}
|
|
1291
1291
|
};
|
|
1292
1292
|
|
|
1293
|
+
// src/agent/title-generator.ts
|
|
1294
|
+
import {
|
|
1295
|
+
query as query3
|
|
1296
|
+
} from "@anthropic-ai/claude-agent-sdk";
|
|
1297
|
+
var TITLE_OUTPUT_FORMAT = {
|
|
1298
|
+
type: "json_schema",
|
|
1299
|
+
schema: {
|
|
1300
|
+
type: "object",
|
|
1301
|
+
properties: {
|
|
1302
|
+
title: {
|
|
1303
|
+
type: "string",
|
|
1304
|
+
description: "A very short title (max 6 words) for the task conversation"
|
|
1305
|
+
}
|
|
1306
|
+
},
|
|
1307
|
+
required: ["title"],
|
|
1308
|
+
additionalProperties: false
|
|
1309
|
+
}
|
|
1310
|
+
};
|
|
1311
|
+
async function generateTitle(prompt) {
|
|
1312
|
+
if (!prompt.trim()) return "Untitled";
|
|
1313
|
+
const systemPrompt = "Generate a very short title (max 6 words) for a task conversation based on the user's prompt. Return ONLY the title, no quotes, no punctuation at the end. If the prompt is in Chinese, generate a Chinese title.";
|
|
1314
|
+
let title = "Untitled";
|
|
1315
|
+
for await (const message of query3({
|
|
1316
|
+
prompt,
|
|
1317
|
+
options: {
|
|
1318
|
+
model: "claude-haiku-4-5-20251001",
|
|
1319
|
+
systemPrompt,
|
|
1320
|
+
allowedTools: [],
|
|
1321
|
+
effort: "low",
|
|
1322
|
+
outputFormat: TITLE_OUTPUT_FORMAT
|
|
1323
|
+
}
|
|
1324
|
+
})) {
|
|
1325
|
+
if (message.type === "result") {
|
|
1326
|
+
const resultMsg = message;
|
|
1327
|
+
if (resultMsg.subtype === "success") {
|
|
1328
|
+
const successMsg = resultMsg;
|
|
1329
|
+
const output = successMsg.structured_output;
|
|
1330
|
+
if (output?.title) {
|
|
1331
|
+
title = String(output.title).trim().slice(0, 100);
|
|
1332
|
+
}
|
|
1333
|
+
}
|
|
1334
|
+
}
|
|
1335
|
+
}
|
|
1336
|
+
return title;
|
|
1337
|
+
}
|
|
1338
|
+
async function generateAndSaveTitle(conversationId, prompt) {
|
|
1339
|
+
try {
|
|
1340
|
+
const title = await generateTitle(prompt);
|
|
1341
|
+
await callMcpHandler("conversation.update_title", {
|
|
1342
|
+
conversation_id: conversationId,
|
|
1343
|
+
title
|
|
1344
|
+
});
|
|
1345
|
+
log.info(`Generated title: "${title}"`);
|
|
1346
|
+
} catch (err) {
|
|
1347
|
+
log.debug(`Title generation failed (non-critical): ${err}`);
|
|
1348
|
+
}
|
|
1349
|
+
}
|
|
1350
|
+
|
|
1293
1351
|
// src/orchestrator.ts
|
|
1294
1352
|
var Orchestrator = class {
|
|
1295
1353
|
session = null;
|
|
@@ -1303,10 +1361,12 @@ var Orchestrator = class {
|
|
|
1303
1361
|
workerManager = null;
|
|
1304
1362
|
userId = null;
|
|
1305
1363
|
conversationId = null;
|
|
1364
|
+
titledConversations = /* @__PURE__ */ new Set();
|
|
1306
1365
|
// ── Lifecycle ───────────────────────────────────────────────────
|
|
1307
1366
|
async start(userId) {
|
|
1308
1367
|
const config = getConfig();
|
|
1309
1368
|
this.userId = userId;
|
|
1369
|
+
this.titledConversations.clear();
|
|
1310
1370
|
this.session = await createSession(config.sessionName, config.workspacePath, "0.1.0");
|
|
1311
1371
|
this.conversationId = await getOrCreateCliConversation();
|
|
1312
1372
|
this.workerManager = new WorkerManager(userId, this.session.id);
|
|
@@ -1319,6 +1379,12 @@ var Orchestrator = class {
|
|
|
1319
1379
|
this.heartbeat.start(this.session.id);
|
|
1320
1380
|
this.taskPoller = new TaskPoller(this.session.id, {
|
|
1321
1381
|
onTask: (task) => {
|
|
1382
|
+
if (task.conversation_id && !this.titledConversations.has(task.conversation_id)) {
|
|
1383
|
+
this.titledConversations.add(task.conversation_id);
|
|
1384
|
+
generateAndSaveTitle(task.conversation_id, task.prompt).catch(
|
|
1385
|
+
(err) => log.debug(`Title generation failed: ${err}`)
|
|
1386
|
+
);
|
|
1387
|
+
}
|
|
1322
1388
|
this.workerManager.dispatchTask(task).catch((err) => {
|
|
1323
1389
|
log.error(`Task dispatch error: ${err}`);
|
|
1324
1390
|
});
|
|
@@ -1377,6 +1443,12 @@ var Orchestrator = class {
|
|
|
1377
1443
|
}
|
|
1378
1444
|
const task = await createTask(this.conversationId, this.session.id, prompt);
|
|
1379
1445
|
await claimTask(task.id);
|
|
1446
|
+
if (!this.titledConversations.has(this.conversationId)) {
|
|
1447
|
+
this.titledConversations.add(this.conversationId);
|
|
1448
|
+
generateAndSaveTitle(this.conversationId, prompt).catch(
|
|
1449
|
+
(err) => log.debug(`Title generation failed: ${err}`)
|
|
1450
|
+
);
|
|
1451
|
+
}
|
|
1380
1452
|
await this.workerManager.dispatchTask(task);
|
|
1381
1453
|
}
|
|
1382
1454
|
async dispatchAndWait(prompt) {
|
|
@@ -1385,6 +1457,12 @@ var Orchestrator = class {
|
|
|
1385
1457
|
}
|
|
1386
1458
|
const task = await createTask(this.conversationId, this.session.id, prompt);
|
|
1387
1459
|
await claimTask(task.id);
|
|
1460
|
+
if (!this.titledConversations.has(this.conversationId)) {
|
|
1461
|
+
this.titledConversations.add(this.conversationId);
|
|
1462
|
+
generateAndSaveTitle(this.conversationId, prompt).catch(
|
|
1463
|
+
(err) => log.debug(`Title generation failed: ${err}`)
|
|
1464
|
+
);
|
|
1465
|
+
}
|
|
1388
1466
|
await this.workerManager.dispatchAndWait(task);
|
|
1389
1467
|
}
|
|
1390
1468
|
/** Alias for interactive CLI. */
|
|
@@ -1979,17 +2057,17 @@ Memories (${memories.length}):`));
|
|
|
1979
2057
|
process.exit(1);
|
|
1980
2058
|
}
|
|
1981
2059
|
});
|
|
1982
|
-
memoryCmd.command("search <query>").description("Search memories").action(async (
|
|
2060
|
+
memoryCmd.command("search <query>").description("Search memories").action(async (query4) => {
|
|
1983
2061
|
try {
|
|
1984
2062
|
await getCurrentUserId();
|
|
1985
2063
|
const mm = new MemoryManager();
|
|
1986
|
-
const results = await mm.search(
|
|
2064
|
+
const results = await mm.search(query4);
|
|
1987
2065
|
if (results.length === 0) {
|
|
1988
|
-
console.log(chalk8.yellow(`No memories matching "${
|
|
2066
|
+
console.log(chalk8.yellow(`No memories matching "${query4}"`));
|
|
1989
2067
|
return;
|
|
1990
2068
|
}
|
|
1991
2069
|
console.log(chalk8.bold(`
|
|
1992
|
-
Search results for "${
|
|
2070
|
+
Search results for "${query4}":`));
|
|
1993
2071
|
for (const m of results) {
|
|
1994
2072
|
console.log(` [${m.category}] ${m.content}`);
|
|
1995
2073
|
}
|
|
@@ -2055,18 +2133,18 @@ function registerSkillCommands(program2) {
|
|
|
2055
2133
|
);
|
|
2056
2134
|
console.log();
|
|
2057
2135
|
});
|
|
2058
|
-
skillCmd.command("search <query>").description("Search skills in your collection and marketplace").action(async (
|
|
2136
|
+
skillCmd.command("search <query>").description("Search skills in your collection and marketplace").action(async (query4) => {
|
|
2059
2137
|
const spinner = ora4("Searching skills...").start();
|
|
2060
2138
|
const sm = await getAuthenticatedSkillManager();
|
|
2061
2139
|
try {
|
|
2062
|
-
const results = await sm.searchDb(
|
|
2140
|
+
const results = await sm.searchDb(query4);
|
|
2063
2141
|
spinner.stop();
|
|
2064
2142
|
if (results.length === 0) {
|
|
2065
|
-
console.log(chalk9.yellow(`No skills found for "${
|
|
2143
|
+
console.log(chalk9.yellow(`No skills found for "${query4}"`));
|
|
2066
2144
|
return;
|
|
2067
2145
|
}
|
|
2068
2146
|
console.log(chalk9.bold(`
|
|
2069
|
-
Skills matching "${
|
|
2147
|
+
Skills matching "${query4}":`));
|
|
2070
2148
|
for (const r of results) {
|
|
2071
2149
|
const emoji = r.emoji ? `${r.emoji} ` : "";
|
|
2072
2150
|
console.log(` ${emoji}${chalk9.cyan(r.name)} [${r.source}]`);
|
package/package.json
CHANGED
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
import {
|
|
2
|
+
query,
|
|
3
|
+
type SDKResultMessage,
|
|
4
|
+
type SDKResultSuccess,
|
|
5
|
+
type OutputFormat,
|
|
6
|
+
} from "@anthropic-ai/claude-agent-sdk";
|
|
7
|
+
import { callMcpHandler } from "../db/api-client.js";
|
|
8
|
+
import { log } from "../utils/logger.js";
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* Structured output schema for title generation.
|
|
12
|
+
*/
|
|
13
|
+
const TITLE_OUTPUT_FORMAT: OutputFormat = {
|
|
14
|
+
type: "json_schema",
|
|
15
|
+
schema: {
|
|
16
|
+
type: "object",
|
|
17
|
+
properties: {
|
|
18
|
+
title: {
|
|
19
|
+
type: "string",
|
|
20
|
+
description: "A very short title (max 6 words) for the task conversation",
|
|
21
|
+
},
|
|
22
|
+
},
|
|
23
|
+
required: ["title"],
|
|
24
|
+
additionalProperties: false,
|
|
25
|
+
},
|
|
26
|
+
};
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* Generate a short conversation title from the user's prompt.
|
|
30
|
+
* Uses the Claude Agent SDK query() with structured output.
|
|
31
|
+
*/
|
|
32
|
+
export async function generateTitle(prompt: string): Promise<string> {
|
|
33
|
+
if (!prompt.trim()) return "Untitled";
|
|
34
|
+
|
|
35
|
+
const systemPrompt =
|
|
36
|
+
"Generate a very short title (max 6 words) for a task conversation based on the user's prompt. " +
|
|
37
|
+
"Return ONLY the title, no quotes, no punctuation at the end. " +
|
|
38
|
+
"If the prompt is in Chinese, generate a Chinese title.";
|
|
39
|
+
|
|
40
|
+
let title = "Untitled";
|
|
41
|
+
|
|
42
|
+
for await (const message of query({
|
|
43
|
+
prompt,
|
|
44
|
+
options: {
|
|
45
|
+
model: "claude-haiku-4-5-20251001",
|
|
46
|
+
systemPrompt,
|
|
47
|
+
allowedTools: [],
|
|
48
|
+
effort: "low",
|
|
49
|
+
outputFormat: TITLE_OUTPUT_FORMAT,
|
|
50
|
+
},
|
|
51
|
+
})) {
|
|
52
|
+
if (message.type === "result") {
|
|
53
|
+
const resultMsg = message as SDKResultMessage;
|
|
54
|
+
if (resultMsg.subtype === "success") {
|
|
55
|
+
const successMsg = resultMsg as SDKResultSuccess;
|
|
56
|
+
const output = successMsg.structured_output as { title?: string } | undefined;
|
|
57
|
+
if (output?.title) {
|
|
58
|
+
title = String(output.title).trim().slice(0, 100);
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
return title;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
/**
|
|
68
|
+
* Generate and save a title for a conversation.
|
|
69
|
+
* Fails silently — title generation is best-effort.
|
|
70
|
+
*/
|
|
71
|
+
export async function generateAndSaveTitle(conversationId: string, prompt: string): Promise<void> {
|
|
72
|
+
try {
|
|
73
|
+
const title = await generateTitle(prompt);
|
|
74
|
+
await callMcpHandler("conversation.update_title", {
|
|
75
|
+
conversation_id: conversationId,
|
|
76
|
+
title,
|
|
77
|
+
});
|
|
78
|
+
log.info(`Generated title: "${title}"`);
|
|
79
|
+
} catch (err) {
|
|
80
|
+
log.debug(`Title generation failed (non-critical): ${err}`);
|
|
81
|
+
}
|
|
82
|
+
}
|
package/src/orchestrator.ts
CHANGED
|
@@ -32,6 +32,7 @@ import { WorkerManager } from "./workers/manager.js";
|
|
|
32
32
|
import { JobAnalysisPoller } from "./agent/job-analysis-poller.js";
|
|
33
33
|
import { TaskPoller } from "./agent/task-poller.js";
|
|
34
34
|
import { SessionHeartbeat } from "./agent/session-heartbeat.js";
|
|
35
|
+
import { generateAndSaveTitle } from "./agent/title-generator.js";
|
|
35
36
|
|
|
36
37
|
export class Orchestrator {
|
|
37
38
|
private session: AgentSession | null = null;
|
|
@@ -47,12 +48,14 @@ export class Orchestrator {
|
|
|
47
48
|
|
|
48
49
|
private userId: string | null = null;
|
|
49
50
|
private conversationId: string | null = null;
|
|
51
|
+
private titledConversations = new Set<string>();
|
|
50
52
|
|
|
51
53
|
// ── Lifecycle ───────────────────────────────────────────────────
|
|
52
54
|
|
|
53
55
|
async start(userId: string): Promise<AgentSession> {
|
|
54
56
|
const config = getConfig();
|
|
55
57
|
this.userId = userId;
|
|
58
|
+
this.titledConversations.clear();
|
|
56
59
|
|
|
57
60
|
this.session = await createSession(config.sessionName, config.workspacePath, "0.1.0");
|
|
58
61
|
this.conversationId = await getOrCreateCliConversation();
|
|
@@ -71,6 +74,13 @@ export class Orchestrator {
|
|
|
71
74
|
|
|
72
75
|
this.taskPoller = new TaskPoller(this.session.id, {
|
|
73
76
|
onTask: (task) => {
|
|
77
|
+
// Generate title on first task per conversation (fire-and-forget)
|
|
78
|
+
if (task.conversation_id && !this.titledConversations.has(task.conversation_id)) {
|
|
79
|
+
this.titledConversations.add(task.conversation_id);
|
|
80
|
+
generateAndSaveTitle(task.conversation_id, task.prompt).catch((err) =>
|
|
81
|
+
log.debug(`Title generation failed: ${err}`)
|
|
82
|
+
);
|
|
83
|
+
}
|
|
74
84
|
this.workerManager!.dispatchTask(task).catch((err) => {
|
|
75
85
|
log.error(`Task dispatch error: ${err}`);
|
|
76
86
|
});
|
|
@@ -141,6 +151,15 @@ export class Orchestrator {
|
|
|
141
151
|
|
|
142
152
|
const task = await createTask(this.conversationId, this.session.id, prompt);
|
|
143
153
|
await claimTask(task.id);
|
|
154
|
+
|
|
155
|
+
// Generate title on first task per conversation (fire-and-forget)
|
|
156
|
+
if (!this.titledConversations.has(this.conversationId)) {
|
|
157
|
+
this.titledConversations.add(this.conversationId);
|
|
158
|
+
generateAndSaveTitle(this.conversationId, prompt).catch((err) =>
|
|
159
|
+
log.debug(`Title generation failed: ${err}`)
|
|
160
|
+
);
|
|
161
|
+
}
|
|
162
|
+
|
|
144
163
|
await this.workerManager!.dispatchTask(task);
|
|
145
164
|
}
|
|
146
165
|
|
|
@@ -151,6 +170,15 @@ export class Orchestrator {
|
|
|
151
170
|
|
|
152
171
|
const task = await createTask(this.conversationId, this.session.id, prompt);
|
|
153
172
|
await claimTask(task.id);
|
|
173
|
+
|
|
174
|
+
// Generate title on first task per conversation (fire-and-forget)
|
|
175
|
+
if (!this.titledConversations.has(this.conversationId)) {
|
|
176
|
+
this.titledConversations.add(this.conversationId);
|
|
177
|
+
generateAndSaveTitle(this.conversationId, prompt).catch((err) =>
|
|
178
|
+
log.debug(`Title generation failed: ${err}`)
|
|
179
|
+
);
|
|
180
|
+
}
|
|
181
|
+
|
|
154
182
|
await this.workerManager!.dispatchAndWait(task);
|
|
155
183
|
}
|
|
156
184
|
|
|
@@ -0,0 +1,196 @@
|
|
|
1
|
+
import { describe, it, expect, vi, beforeEach } from "vitest";
|
|
2
|
+
|
|
3
|
+
// ── Mocks ────────────────────────────────────────────────────────
|
|
4
|
+
|
|
5
|
+
let mockQueryCalls: unknown[] = [];
|
|
6
|
+
const mockQueryMessages: unknown[] = [];
|
|
7
|
+
vi.mock("@anthropic-ai/claude-agent-sdk", () => ({
|
|
8
|
+
query: vi.fn((params: unknown) => {
|
|
9
|
+
mockQueryCalls.push(params);
|
|
10
|
+
return {
|
|
11
|
+
[Symbol.asyncIterator]: async function* () {
|
|
12
|
+
for (const msg of mockQueryMessages) {
|
|
13
|
+
yield msg;
|
|
14
|
+
}
|
|
15
|
+
},
|
|
16
|
+
};
|
|
17
|
+
}),
|
|
18
|
+
}));
|
|
19
|
+
|
|
20
|
+
const mockCallMcpHandler = vi.fn().mockResolvedValue(null);
|
|
21
|
+
vi.mock("../../src/db/api-client.js", () => ({
|
|
22
|
+
callMcpHandler: (...args: unknown[]) => mockCallMcpHandler(...args),
|
|
23
|
+
}));
|
|
24
|
+
|
|
25
|
+
vi.mock("../../src/utils/logger.js", () => ({
|
|
26
|
+
log: {
|
|
27
|
+
debug: vi.fn(),
|
|
28
|
+
info: vi.fn(),
|
|
29
|
+
warn: vi.fn(),
|
|
30
|
+
error: vi.fn(),
|
|
31
|
+
},
|
|
32
|
+
}));
|
|
33
|
+
|
|
34
|
+
// ── Import after mocks ──────────────────────────────────────────
|
|
35
|
+
|
|
36
|
+
import { generateTitle, generateAndSaveTitle } from "../../src/agent/title-generator.js";
|
|
37
|
+
import { query } from "@anthropic-ai/claude-agent-sdk";
|
|
38
|
+
|
|
39
|
+
// ── Tests ────────────────────────────────────────────────────────
|
|
40
|
+
|
|
41
|
+
describe("title-generator", () => {
|
|
42
|
+
beforeEach(() => {
|
|
43
|
+
vi.clearAllMocks();
|
|
44
|
+
mockQueryCalls = [];
|
|
45
|
+
mockQueryMessages.length = 0;
|
|
46
|
+
});
|
|
47
|
+
|
|
48
|
+
describe("generateTitle", () => {
|
|
49
|
+
it("returns Untitled for empty prompt", async () => {
|
|
50
|
+
const title = await generateTitle("");
|
|
51
|
+
expect(title).toBe("Untitled");
|
|
52
|
+
expect(query).not.toHaveBeenCalled();
|
|
53
|
+
});
|
|
54
|
+
|
|
55
|
+
it("returns Untitled for whitespace-only prompt", async () => {
|
|
56
|
+
const title = await generateTitle(" ");
|
|
57
|
+
expect(title).toBe("Untitled");
|
|
58
|
+
expect(query).not.toHaveBeenCalled();
|
|
59
|
+
});
|
|
60
|
+
|
|
61
|
+
it("passes correct SDK query options", async () => {
|
|
62
|
+
mockQueryMessages.push({
|
|
63
|
+
type: "result",
|
|
64
|
+
subtype: "success",
|
|
65
|
+
structured_output: { title: "Fix Login Bug" },
|
|
66
|
+
});
|
|
67
|
+
|
|
68
|
+
await generateTitle("The login button is broken");
|
|
69
|
+
|
|
70
|
+
expect(mockQueryCalls).toHaveLength(1);
|
|
71
|
+
const call = mockQueryCalls[0] as { prompt: string; options: Record<string, unknown> };
|
|
72
|
+
expect(call.prompt).toBe("The login button is broken");
|
|
73
|
+
expect(call.options.model).toBe("claude-haiku-4-5-20251001");
|
|
74
|
+
expect(call.options.allowedTools).toEqual([]);
|
|
75
|
+
expect(call.options.effort).toBe("low");
|
|
76
|
+
expect(call.options.outputFormat).toBeDefined();
|
|
77
|
+
});
|
|
78
|
+
|
|
79
|
+
it("parses valid structured output title", async () => {
|
|
80
|
+
mockQueryMessages.push({
|
|
81
|
+
type: "result",
|
|
82
|
+
subtype: "success",
|
|
83
|
+
structured_output: { title: "Fix Login Bug" },
|
|
84
|
+
});
|
|
85
|
+
|
|
86
|
+
const title = await generateTitle("The login button is broken");
|
|
87
|
+
expect(title).toBe("Fix Login Bug");
|
|
88
|
+
});
|
|
89
|
+
|
|
90
|
+
it("returns Untitled when structured_output is missing title field", async () => {
|
|
91
|
+
mockQueryMessages.push({
|
|
92
|
+
type: "result",
|
|
93
|
+
subtype: "success",
|
|
94
|
+
structured_output: {},
|
|
95
|
+
});
|
|
96
|
+
|
|
97
|
+
const title = await generateTitle("Some prompt");
|
|
98
|
+
expect(title).toBe("Untitled");
|
|
99
|
+
});
|
|
100
|
+
|
|
101
|
+
it("returns Untitled when structured_output is undefined", async () => {
|
|
102
|
+
mockQueryMessages.push({
|
|
103
|
+
type: "result",
|
|
104
|
+
subtype: "success",
|
|
105
|
+
structured_output: undefined,
|
|
106
|
+
});
|
|
107
|
+
|
|
108
|
+
const title = await generateTitle("Some prompt");
|
|
109
|
+
expect(title).toBe("Untitled");
|
|
110
|
+
});
|
|
111
|
+
|
|
112
|
+
it("truncates title longer than 100 characters", async () => {
|
|
113
|
+
const longTitle = "A".repeat(150);
|
|
114
|
+
mockQueryMessages.push({
|
|
115
|
+
type: "result",
|
|
116
|
+
subtype: "success",
|
|
117
|
+
structured_output: { title: longTitle },
|
|
118
|
+
});
|
|
119
|
+
|
|
120
|
+
const title = await generateTitle("Some prompt");
|
|
121
|
+
expect(title).toHaveLength(100);
|
|
122
|
+
expect(title).toBe("A".repeat(100));
|
|
123
|
+
});
|
|
124
|
+
|
|
125
|
+
it("trims whitespace from title", async () => {
|
|
126
|
+
mockQueryMessages.push({
|
|
127
|
+
type: "result",
|
|
128
|
+
subtype: "success",
|
|
129
|
+
structured_output: { title: " Fix Bug " },
|
|
130
|
+
});
|
|
131
|
+
|
|
132
|
+
const title = await generateTitle("Some prompt");
|
|
133
|
+
expect(title).toBe("Fix Bug");
|
|
134
|
+
});
|
|
135
|
+
|
|
136
|
+
it("returns Untitled on SDK error (non-success result)", async () => {
|
|
137
|
+
mockQueryMessages.push({
|
|
138
|
+
type: "result",
|
|
139
|
+
subtype: "error",
|
|
140
|
+
});
|
|
141
|
+
|
|
142
|
+
const title = await generateTitle("Some prompt");
|
|
143
|
+
expect(title).toBe("Untitled");
|
|
144
|
+
});
|
|
145
|
+
|
|
146
|
+
it("ignores non-result messages", async () => {
|
|
147
|
+
mockQueryMessages.push(
|
|
148
|
+
{ type: "assistant", content: [{ type: "text", text: "hello" }] },
|
|
149
|
+
{
|
|
150
|
+
type: "result",
|
|
151
|
+
subtype: "success",
|
|
152
|
+
structured_output: { title: "Real Title" },
|
|
153
|
+
}
|
|
154
|
+
);
|
|
155
|
+
|
|
156
|
+
const title = await generateTitle("Some prompt");
|
|
157
|
+
expect(title).toBe("Real Title");
|
|
158
|
+
});
|
|
159
|
+
});
|
|
160
|
+
|
|
161
|
+
describe("generateAndSaveTitle", () => {
|
|
162
|
+
it("generates title and saves via MCP handler", async () => {
|
|
163
|
+
mockQueryMessages.push({
|
|
164
|
+
type: "result",
|
|
165
|
+
subtype: "success",
|
|
166
|
+
structured_output: { title: "Deploy Dashboard" },
|
|
167
|
+
});
|
|
168
|
+
|
|
169
|
+
await generateAndSaveTitle("conv-123", "Deploy the dashboard");
|
|
170
|
+
|
|
171
|
+
expect(mockCallMcpHandler).toHaveBeenCalledWith("conversation.update_title", {
|
|
172
|
+
conversation_id: "conv-123",
|
|
173
|
+
title: "Deploy Dashboard",
|
|
174
|
+
});
|
|
175
|
+
});
|
|
176
|
+
|
|
177
|
+
it("does not throw on SDK failure (graceful degradation)", async () => {
|
|
178
|
+
(query as ReturnType<typeof vi.fn>).mockImplementationOnce(() => {
|
|
179
|
+
throw new Error("SDK unavailable");
|
|
180
|
+
});
|
|
181
|
+
|
|
182
|
+
await expect(generateAndSaveTitle("conv-123", "test")).resolves.toBeUndefined();
|
|
183
|
+
});
|
|
184
|
+
|
|
185
|
+
it("does not throw on MCP handler failure", async () => {
|
|
186
|
+
mockQueryMessages.push({
|
|
187
|
+
type: "result",
|
|
188
|
+
subtype: "success",
|
|
189
|
+
structured_output: { title: "Some Title" },
|
|
190
|
+
});
|
|
191
|
+
mockCallMcpHandler.mockRejectedValueOnce(new Error("MCP failure"));
|
|
192
|
+
|
|
193
|
+
await expect(generateAndSaveTitle("conv-123", "test")).resolves.toBeUndefined();
|
|
194
|
+
});
|
|
195
|
+
});
|
|
196
|
+
});
|