From 8d645c0aca295dc7d4b34163744ae76f172e3bd3 Mon Sep 17 00:00:00 2001 From: Twest2 Date: Wed, 24 Jun 2026 09:44:36 -0500 Subject: [PATCH] fix smart title generation from browser window context MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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 --- core/text-intel.js | 92 ++++++++++++++++++++++++++++------- tests/unit/text-intel.test.js | 54 ++++++++++++++++++++ 2 files changed, 129 insertions(+), 17 deletions(-) diff --git a/core/text-intel.js b/core/text-intel.js index 0f0f698..c235c94 100644 --- a/core/text-intel.js +++ b/core/text-intel.js @@ -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'; } diff --git a/tests/unit/text-intel.test.js b/tests/unit/text-intel.test.js index 16bf024..9471f1e 100644 --- a/tests/unit/text-intel.test.js +++ b/tests/unit/text-intel.test.js @@ -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: {