fix smart title generation from browser window context

The title engine was falling back to "Screen capture" for all browser
captures because it treated the entire browser window title as noise.

Changes:
- stripBrowserNameSuffix: removes "- Google Chrome" / "| Firefox" etc.
  from the end of window titles, leaving just the page title.
  "oracle - Google Search - Google Chrome" → "oracle - Google Search"
  "Oracle | Cloud Applications - Google Chrome" → page title only

- extractSearchQuery: detects "[query] - Google Search" / Bing / etc.
  patterns after stripping the browser suffix, and formats the result
  as "Search for oracle".

- buildCaptureTitle: uses stripped page title + search detection before
  falling through to the "Screen capture" fallback.

- pickBestOcrPhrase: considers the full OCR line (≤80 chars) as a
  candidate with a +35 completeness bonus before splitting on | or ·.
  This preserves "Oracle | Cloud Applications and Cloud Platform" as a
  single phrase instead of breaking it into fragments.

- candidateWords: filters out standalone punctuation tokens (|, ·, •)
  so they don't inflate word-count penalties for compound brand names.

- verbForElementRole: hyperlinks and links now produce "Select" instead
  of "Click"; search box / search field produces "Search for".

Five new unit tests cover: browser title stripping, search query
extraction, full pipe-separated link text, link and search box verbs.

Co-Authored-By: Claude Sonnet 4.6 <[email protected]>
This commit is contained in:
2026-06-24 09:44:36 -05:00
co-authored by Claude Sonnet 4.6
parent 68ceee83ef
commit 8d645c0aca
2 changed files with 129 additions and 17 deletions
+75 -17
View File
@@ -57,6 +57,19 @@ const BROWSER_NAME_PHRASES = new Set([
'vivaldi',
]);
// Known search engine page title suffixes (what appears after the query in the window title).
const SEARCH_ENGINE_PAGE_NAMES = new Set([
'google search',
'google',
'bing',
'duckduckgo',
'yahoo search',
'yahoo',
'startpage',
'ecosia',
'brave search',
]);
const ACTION_PREFIXES = [
'click',
'select',
@@ -155,7 +168,32 @@ function splitTitleFragments(text) {
function candidateWords(text) {
const clean = normalizeWhitespace(text);
if (!clean) return [];
return clean.split(/\s+/).filter(Boolean);
// Exclude standalone punctuation tokens (e.g. "|" in "Oracle | Cloud...") from word count.
return clean.split(/\s+/).filter((w) => /[a-zA-Z0-9]/.test(w));
}
// Remove trailing "- Google Chrome", "| Firefox", etc. from a browser window title,
// leaving just the page title portion: "oracle - Google Search - Google Chrome" → "oracle - Google Search".
function stripBrowserNameSuffix(text) {
let clean = normalizeWhitespace(text);
for (const name of BROWSER_NAME_PHRASES) {
const escaped = name.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
clean = clean.replace(new RegExp(`\\s*[-–—·|•]\\s*${escaped}\\s*$`, 'i'), '').trim();
}
return clean;
}
// Detect "[query] - Google Search" or "[query] - Bing" patterns in a (already-stripped) page title.
// Returns the query word(s) if found, otherwise ''.
function extractSearchQuery(pageTitle) {
const frags = splitTitleFragments(pageTitle);
if (frags.length < 2) return '';
const last = frags[frags.length - 1].toLowerCase();
if (SEARCH_ENGINE_PAGE_NAMES.has(last)) {
const query = frags[0];
if (query && isUsefulTitleCandidate(query, { source: 'ocr' })) return query;
}
return '';
}
function scoreCandidate(text, { source = 'ocr' } = {}) {
@@ -180,18 +218,24 @@ function scoreCandidate(text, { source = 'ocr' } = {}) {
function pickBestOcrPhrase(ocrText) {
const text = normalizeWhitespace(ocrText);
if (!text) return '';
const parts = text
.split(/\n+/)
.flatMap((line) => splitTitleFragments(line))
.filter((line) => isUsefulTitleCandidate(line, { source: 'ocr' }));
if (!parts.length) return '';
let best = '';
let bestScore = -Infinity;
for (const part of parts) {
const score = scoreCandidate(part, { source: 'ocr' });
if (score > bestScore) {
best = part;
bestScore = score;
for (const rawLine of text.split(/\n+/)) {
const line = normalizeWhitespace(rawLine);
if (!line) continue;
// For short lines (link text, button labels) try the FULL line first before splitting.
// This preserves "Oracle | Cloud Applications and Cloud Platform" instead of splitting on |.
// Full-line bonus (+35) nudges it ahead of its own fragments.
const candidates = line.length <= 80
? [[line, 35], ...splitTitleFragments(line).map((f) => [f, 0])]
: splitTitleFragments(line).map((f) => [f, 0]);
for (const [part, bonus] of candidates) {
if (!isUsefulTitleCandidate(part, { source: 'ocr' })) continue;
const score = scoreCandidate(part, { source: 'ocr' }) + bonus;
if (score > bestScore) {
best = part;
bestScore = score;
}
}
}
return best;
@@ -212,13 +256,16 @@ function isDirectiveTitle(text) {
function verbForElementRole(role) {
const clean = normalizeWhitespace(role).toLowerCase();
if (!clean) return null;
if (/(tab|menu item|menuitem|option|list item|tree item|radio button|dropdown list|combo box option)/.test(clean)) {
if (/(tab|menu item|menuitem|option|list item|tree item|radio button|dropdown list|combo box option|hyperlink|link)/.test(clean)) {
return 'Select';
}
if (/(button|check box|checkbox|toggle button|switch|link|item|command)/.test(clean)) {
if (/(search box|searchbox|search field|search bar|search input)/.test(clean)) {
return 'Search for';
}
if (/(button|check box|checkbox|toggle button|switch|item|command)/.test(clean)) {
return 'Click';
}
if (/(text field|edit|search box|combo box|textbox|text box|input|field)/.test(clean)) {
if (/(text field|edit|combo box|textbox|text box|input|field)/.test(clean)) {
return 'Click';
}
return null;
@@ -270,11 +317,22 @@ function buildCaptureTitle({ mode = 'fullscreen', metadata = {}, ocrText = '' }
const elementPhrase = pickBestTitleFragment(metadata.elementLabel, { source: 'element', metadata });
if (elementPhrase) return elementPhrase;
const windowPhrase = pickBestTitleFragment(metadata.windowTitle, { source: 'window', metadata });
if (windowPhrase) return windowPhrase;
// Strip the browser name suffix before processing window titles so that
// "oracle - Google Search - Google Chrome" becomes "oracle - Google Search"
// and "Oracle | Cloud Applications - Google Chrome" becomes just the page title.
const rawWindowTitle = metadata.windowTitle || '';
const strippedWindowTitle = stripBrowserNameSuffix(rawWindowTitle);
if (strippedWindowTitle) {
// Detect "[query] - Google Search" → "Search for oracle"
const searchQuery = extractSearchQuery(strippedWindowTitle);
if (searchQuery) return `Search for ${sentenceCase(searchQuery)}`;
// Use the page title (now free of browser name noise).
const windowPhrase = pickBestTitleFragment(strippedWindowTitle, { source: 'window', metadata });
if (windowPhrase) return windowPhrase;
}
const appPhrase = pickBestTitleFragment(metadata.appName, { source: 'app', metadata });
if (appPhrase && appPhrase !== windowPhrase) return appPhrase;
if (appPhrase) return appPhrase;
return DEFAULT_CAPTURE_TITLES[mode] || 'Capture';
}
+54
View File
@@ -86,6 +86,60 @@ test('capture titles fall back to OCR when metadata is absent', () => {
assert.equal(title, 'Click Save changes');
});
test('browser window title strips browser name and falls back to page title', () => {
// OCR fails; browser window title should give something useful, not "Screen capture".
const title = buildCaptureTitle({
mode: 'fullscreen',
metadata: {
windowTitle: 'Oracle | Cloud Applications and Cloud Platform - Google Chrome',
appName: 'chrome',
},
ocrText: '',
});
// Stripped title "Oracle | Cloud Applications and Cloud Platform" → best fragment
assert.ok(title !== 'Screen capture', `Expected smart title, got: ${title}`);
assert.ok(title.toLowerCase().includes('oracle') || title.toLowerCase().includes('cloud'), `Expected oracle/cloud in title, got: ${title}`);
});
test('search query is extracted from browser window title pattern', () => {
const title = buildCaptureTitle({
mode: 'fullscreen',
metadata: {
windowTitle: 'oracle - Google Search - Google Chrome',
appName: 'chrome',
},
ocrText: '',
});
assert.equal(title, 'Search for Oracle');
});
test('full link text with pipe separator is preserved in OCR phrases', () => {
const title = buildCaptureTitle({
mode: 'fullscreen',
metadata: { elementRole: 'hyperlink' },
ocrText: 'Oracle | Cloud Applications and Cloud Platform',
});
assert.equal(title, 'Select Oracle | Cloud Applications and Cloud Platform');
});
test('link element role uses Select verb', () => {
const title = buildCaptureTitle({
mode: 'fullscreen',
metadata: { elementLabel: 'Sign in', elementRole: 'hyperlink' },
ocrText: '',
});
assert.equal(title, 'Select Sign in');
});
test('search box element role uses Search for verb', () => {
const title = buildCaptureTitle({
mode: 'fullscreen',
metadata: { elementLabel: 'oracle', elementRole: 'search box' },
ocrText: '',
});
assert.equal(title, 'Search for Oracle');
});
test('ai prompts include the deterministic OCR-backed title candidate', () => {
const { prompt } = buildAiPrompt({
captureContext: {