From 07d389993ea0940631f3f03d44c74610fa959636 Mon Sep 17 00:00:00 2001 From: bowenliang123 Date: Thu, 24 Sep 2026 16:47:43 +0800 Subject: [PATCH] feat(client): let the session search match the last message MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The placeholder promised less than the user wanted and the matcher delivered less than the placeholder should: the search box only substring-matched title and directory. It now also matches the session's newest own message (timeline.lastUser, the card preview text), case-insensitively, and a non-string fold shape safely never matches. The placeholder copy follows in both locales ("Search title / path / last message..." / "搜索标题 / 目录 / 最后消息..."). --- src/client/i18n.ts | 4 ++-- src/client/overview.ts | 8 +++++--- tests/client/overview.spec.ts | 9 ++++++++- 3 files changed, 15 insertions(+), 6 deletions(-) diff --git a/src/client/i18n.ts b/src/client/i18n.ts index 59515b1..307ae29 100644 --- a/src/client/i18n.ts +++ b/src/client/i18n.ts @@ -273,7 +273,7 @@ export const DICT_ZH: Record = { 'ov.heat.mon.07': '7月', 'ov.heat.mon.08': '8月', 'ov.heat.mon.09': '9月', 'ov.heat.mon.10': '10月', 'ov.heat.mon.11': '11月', 'ov.heat.mon.12': '12月', 'ov.list.title': '会话', - 'ov.list.search': '搜索标题或目录…', + 'ov.list.search': '搜索标题 / 目录 / 最后消息…', 'ov.list.sortLabel': '排序', 'ov.list.sort.recent': '最近更新', 'ov.list.sort.tokens': 'Token 总用量', 'ov.list.sort.context': '当前 Context', 'ov.list.empty': '还没有会话 — 发起一轮对话后,这里会列出每个会话的上下文画像', @@ -563,7 +563,7 @@ export const DICT_EN: Record = { 'ov.heat.mon.07': 'Jul', 'ov.heat.mon.08': 'Aug', 'ov.heat.mon.09': 'Sep', 'ov.heat.mon.10': 'Oct', 'ov.heat.mon.11': 'Nov', 'ov.heat.mon.12': 'Dec', 'ov.list.title': 'Sessions', - 'ov.list.search': 'Search title or path…', + 'ov.list.search': 'Search title / path / last message…', 'ov.list.sortLabel': 'Sort', 'ov.list.sort.recent': 'Last updated', 'ov.list.sort.tokens': 'Total Tokens', 'ov.list.sort.context': 'Current Context', 'ov.list.empty': 'No sessions yet — each session’s context profile appears here once you start one', diff --git a/src/client/overview.ts b/src/client/overview.ts index 45f58fc..6210c9c 100644 --- a/src/client/overview.ts +++ b/src/client/overview.ts @@ -274,8 +274,8 @@ export function createdDayOf(activity: ContextActivity | null): string | undefin /** * The panel's row pipeline: range (by last-activity), then the heatmap's * picked day (sessions contributing to that day's merged ledger), then the - * search box (title or directory substring). Each stage keeps the rows it - * cannot prove out of the result — never an exception. + * search box (title, directory, or last-message substring). Each stage keeps + * the rows it cannot prove out of the result — never an exception. */ export function filterRows( rows: readonly OverviewRow[], @@ -293,7 +293,9 @@ export function filterRows( if (query !== '') { const inTitle = row.title.toLowerCase().includes(query) const inCwd = row.cwd !== undefined && row.cwd.toLowerCase().includes(query) - if (!inTitle && !inCwd) return false + const lastUser = row.timeline?.lastUser + const inLastUser = typeof lastUser === 'string' && lastUser.toLowerCase().includes(query) + if (!inTitle && !inCwd && !inLastUser) return false } return true }) diff --git a/tests/client/overview.spec.ts b/tests/client/overview.spec.ts index d229b17..f7a5d4a 100644 --- a/tests/client/overview.spec.ts +++ b/tests/client/overview.spec.ts @@ -228,11 +228,18 @@ describe('filterRows', () => { assert.deepEqual(filterRows(rows, { range: 'all', day: '2026-09-11', query: '' }, now), [], 'no ledger entry') }) - test('the query matches title or directory, case-insensitively', () => { + test('the query matches title, directory, or last message, case-insensitively', () => { assert.deepEqual(filterRows(rows, { range: 'all', day: null, query: 'FRESH' }, now).map(r => r.id), ['new']) assert.deepEqual(filterRows(rows, { range: 'all', day: null, query: '/repo' }, now).map(r => r.id), ['new']) assert.deepEqual(filterRows(rows, { range: 'all', day: null, query: ' ' }, now).length, 3, 'a blank query matches all') assert.deepEqual(filterRows(rows, { range: 'all', day: null, query: 'zzz' }, now), []) + // The session's newest own message joins the haystack. + const talked = rowOf({ id: 'talk', title: 'quiet title', timeline: { lastUser: 'ship the QUARTERLY report' } as unknown as ContextTimeline }) + assert.deepEqual(filterRows([talked], { range: 'all', day: null, query: 'quarterly' }, now).map(r => r.id), ['talk']) + assert.deepEqual(filterRows([talked], { range: 'all', day: null, query: 'quiet' }, now).map(r => r.id), ['talk'], 'the title still matches') + // A non-string last message (a hostile fold shape) never matches. + const odd = rowOf({ id: 'odd', title: 'odd one', timeline: { lastUser: 42 } as unknown as ContextTimeline }) + assert.deepEqual(filterRows([odd], { range: 'all', day: null, query: '42' }, now), []) }) })