Skip to content

Commit 970cb7d

Browse files
committed
fix: skip conversational headings in content fallback and add missing preamble patterns
1 parent 63516ff commit 970cb7d

File tree

5 files changed

+111
-6
lines changed

5 files changed

+111
-6
lines changed

apps/desktop/src/features/dashboard/lib/clean-artifact-title.test.ts

Lines changed: 58 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -312,3 +312,61 @@ void test("cleanArtifactTitle: extracts heading for 'Based on' title with conten
312312
"Top Competitors Analysis",
313313
);
314314
});
315+
316+
// ---------------------------------------------------------------------------
317+
// "Saved " and "Short answer" preamble detection (task 0021)
318+
// ---------------------------------------------------------------------------
319+
320+
void test("isConversationalTitle: detects 'Saved ' preamble", () => {
321+
assert.ok(isConversationalTitle("Saved the audit here:"));
322+
});
323+
324+
void test("isConversationalTitle: detects 'Short answer' preamble", () => {
325+
assert.ok(isConversationalTitle("Short answer: the real competitive set"));
326+
});
327+
328+
void test("cleanArtifactTitle: falls back to type label for 'Saved ' preamble", () => {
329+
assert.equal(
330+
cleanArtifactTitle({
331+
title: "Saved the audit here:",
332+
type: "strategy_note",
333+
}),
334+
"Strategy Note",
335+
);
336+
});
337+
338+
void test("cleanArtifactTitle: falls back to type label for 'Short answer' preamble", () => {
339+
assert.equal(
340+
cleanArtifactTitle({
341+
title: "Short answer: the real competitive set",
342+
type: "matrix",
343+
}),
344+
"Matrix",
345+
);
346+
});
347+
348+
// ---------------------------------------------------------------------------
349+
// Content heading fallback skips conversational headings (task 0021)
350+
// ---------------------------------------------------------------------------
351+
352+
void test("cleanArtifactTitle: skips conversational content heading and uses next non-conversational heading", () => {
353+
assert.equal(
354+
cleanArtifactTitle({
355+
title: "I still don't have the actual positioning thread 130",
356+
type: "page_outline",
357+
content: "## I'll start with the overview\n\nSome text\n\n## Positioning Page Outline\n\nDetails...",
358+
}),
359+
"Positioning Page Outline",
360+
);
361+
});
362+
363+
void test("cleanArtifactTitle: falls back to type label when all content headings are conversational", () => {
364+
assert.equal(
365+
cleanArtifactTitle({
366+
title: "I still don't have the actual positioning thread",
367+
type: "strategy_note",
368+
content: "## I'll start with the overview\n\nSome text\n\n## Here is what I found\n\nMore text",
369+
}),
370+
"Strategy Note",
371+
);
372+
});

apps/desktop/src/features/dashboard/lib/clean-artifact-title.ts

Lines changed: 9 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@ import { normalizeQuotes } from "./normalize-quotes";
77
* appear as an artifact title. Case-insensitive.
88
*/
99
export const CONVERSATIONAL_PATTERN =
10-
/^(I |I'm |I'll |I've |I don't|I can't|I can |I still|I checked|Got it|Let me|Here |Here'|Sure|OK |Okay|Well |So |Hmm|Based on |According to |After reviewing |After analyzing |Looking at |From the |From my |Given |Pulling |Checking |Reviewing |Analyzing |To help |In order to |For this |For your |As requested|Absolutely|Assuming )/i;
10+
/^(I |I'm |I'll |I've |I don't|I can't|I can |I still|I checked|Got it|Let me|Here |Here'|Sure|OK |Okay|Well |So |Hmm|Based on |According to |After reviewing |After analyzing |Looking at |From the |From my |Given |Pulling |Checking |Reviewing |Analyzing |To help |In order to |For this |For your |As requested|Absolutely|Assuming |Saved |Short answer)/i;
1111

1212
/**
1313
* Returns true if the given title looks like AI conversational preamble
@@ -31,11 +31,15 @@ export function cleanArtifactTitle(
3131
return stripped;
3232
}
3333

34-
// Try to extract the first markdown heading from content
34+
// Try to extract the first non-conversational markdown heading from content
3535
if (artifact.content) {
36-
const headingMatch = artifact.content.match(/^#{1,6}\s+(.+)$/m);
37-
if (headingMatch) {
38-
return stripMarkdown(headingMatch[1].trim());
36+
const headingRegex = /^#{1,6}\s+(.+)$/gm;
37+
let headingMatch: RegExpExecArray | null;
38+
while ((headingMatch = headingRegex.exec(artifact.content)) !== null) {
39+
const candidate = stripMarkdown(headingMatch[1].trim());
40+
if (!isConversationalTitle(candidate)) {
41+
return candidate;
42+
}
3943
}
4044
}
4145

packages/sidecar/src/artifact-extractor/title-cleaner.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@
33
* appear as artifact titles. Case-insensitive, anchored to start.
44
*/
55
const CONVERSATIONAL_PATTERN =
6-
/^(I |I'm |I'll |I've |I don't|I can't|I can |I still|I checked|Got it|Let me|Here |Here'|Sure|OK |Okay|Well |So |Hmm|Based on |According to |After reviewing |After analyzing |Looking at |From the |From my |Given |Pulling |Checking |Reviewing |Analyzing |To help |In order to |For this |For your |As requested|Absolutely|Assuming )/i;
6+
/^(I |I'm |I'll |I've |I don't|I can't|I can |I still|I checked|Got it|Let me|Here |Here'|Sure|OK |Okay|Well |So |Hmm|Based on |According to |After reviewing |After analyzing |Looking at |From the |From my |Given |Pulling |Checking |Reviewing |Analyzing |To help |In order to |For this |For your |As requested|Absolutely|Assuming |Saved |Short answer)/i;
77

88
/**
99
* Normalizes Unicode smart/curly quotes to ASCII equivalents

test/sidecar/title-cleaner.test.ts

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -161,3 +161,30 @@ More analysis below...`;
161161
).toBe("Competitor Positioning Matrix");
162162
});
163163
});
164+
165+
// ---------------------------------------------------------------------------
166+
// "Saved " and "Short answer" preamble detection (task 0021)
167+
// ---------------------------------------------------------------------------
168+
describe("isConversationalTitle — Saved / Short answer patterns", () => {
169+
it("detects 'Saved ' preamble", () => {
170+
expect(isConversationalTitle("Saved the audit here:")).toBe(true);
171+
});
172+
173+
it("detects 'Short answer' preamble", () => {
174+
expect(isConversationalTitle("Short answer: the real competitive set")).toBe(true);
175+
});
176+
});
177+
178+
describe("cleanSectionTitle — Saved / Short answer fallback", () => {
179+
it("falls back to type label for 'Saved ' preamble without content headings", () => {
180+
expect(
181+
cleanSectionTitle("Saved the audit here:", "Just plain text.", "strategy_note"),
182+
).toBe("Strategy Note");
183+
});
184+
185+
it("falls back to type label for 'Short answer' preamble without content headings", () => {
186+
expect(
187+
cleanSectionTitle("Short answer: the real competitive set", "Plain text.", "matrix"),
188+
).toBe("Matrix");
189+
});
190+
});

test/ui/clean-artifact-title.test.ts

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -79,6 +79,22 @@ describe("clean-artifact-title utility", () => {
7979
expect(src).toMatch(/I can /);
8080
});
8181

82+
it("detects 'Saved ' preamble pattern", () => {
83+
const src = readFileSync(utilPath, "utf-8");
84+
expect(src).toMatch(/Saved /);
85+
});
86+
87+
it("detects 'Short answer' preamble pattern", () => {
88+
const src = readFileSync(utilPath, "utf-8");
89+
expect(src).toMatch(/Short answer/);
90+
});
91+
92+
it("content heading fallback loops and skips conversational headings", () => {
93+
const src = readFileSync(utilPath, "utf-8");
94+
// Should use a while loop or similar iteration over headings, not just the first match
95+
expect(src).toMatch(/while/);
96+
});
97+
8298
it("desktop CONVERSATIONAL_PATTERN matches sidecar patterns", () => {
8399
const src = readFileSync(utilPath, "utf-8");
84100
const sidecarSrc = readFileSync(

0 commit comments

Comments
 (0)