-
Notifications
You must be signed in to change notification settings - Fork 5
feat: surface session search hits in browser and preview #75
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,121 @@ | ||
| import assert from 'assert'; | ||
| import path from 'path'; | ||
| import { fileURLToPath, pathToFileURL } from 'url'; | ||
|
|
||
| const __filename = fileURLToPath(import.meta.url); | ||
| const __dirname = path.dirname(__filename); | ||
|
|
||
| const logic = await import(pathToFileURL(path.join(__dirname, '..', '..', 'web-ui', 'logic.mjs'))); | ||
| const { createSessionBrowserMethods } = await import( | ||
| pathToFileURL(path.join(__dirname, '..', '..', 'web-ui', 'modules', 'app.methods.session-browser.mjs')) | ||
| ); | ||
| const { createSessionComputed } = await import( | ||
| pathToFileURL(path.join(__dirname, '..', '..', 'web-ui', 'modules', 'app.computed.session.mjs')) | ||
| ); | ||
|
|
||
| const { normalizeSessionMatch, findSessionMessageMatchKey } = logic; | ||
|
|
||
| test('normalizeSessionMatch extracts primary snippet and count', () => { | ||
| const normalized = normalizeSessionMatch({ | ||
| match: { | ||
| hit: true, | ||
| count: 3, | ||
| snippets: [' first hit ', '', 'second hit'] | ||
| } | ||
| }); | ||
|
|
||
| assert.deepStrictEqual(normalized, { | ||
| hit: true, | ||
| count: 3, | ||
| snippets: ['first hit', 'second hit'], | ||
| primarySnippet: 'first hit', | ||
| hasSnippet: true | ||
| }); | ||
| }); | ||
|
|
||
| test('findSessionMessageMatchKey finds record key from matched snippet', () => { | ||
| const messages = [ | ||
| { recordLineIndex: 1, text: 'hello there' }, | ||
| { recordLineIndex: 4, text: 'needle appears in this message body' } | ||
| ]; | ||
|
|
||
| assert.strictEqual(findSessionMessageMatchKey(messages, ['appears in this message']), 'record-4'); | ||
| }); | ||
|
|
||
| test('sortedSessionsList keeps normalized match payload for UI display', () => { | ||
| const computed = createSessionComputed(); | ||
| const list = computed.sortedSessionsList.call({ | ||
| sessionsList: [{ sessionId: 's1', match: { hit: true, count: 2, snippets: ['matched text'] } }], | ||
| sessionPinnedMap: {}, | ||
| getSessionExportKey(session) { | ||
| return session.sessionId; | ||
| } | ||
| }); | ||
|
|
||
| assert.strictEqual(list.length, 1); | ||
| assert.strictEqual(list[0].match.primarySnippet, 'matched text'); | ||
| assert.strictEqual(list[0].match.count, 2); | ||
| }); | ||
|
|
||
| test('loadActiveSessionDetail annotates matched message and schedules jump', async () => { | ||
| const methods = createSessionBrowserMethods({ | ||
| api: async () => ({ | ||
| messages: [ | ||
| { recordLineIndex: 1, role: 'user', text: 'hello world' }, | ||
| { recordLineIndex: 2, role: 'assistant', text: 'needle is right here in the answer' } | ||
| ], | ||
| clipped: false, | ||
| messageLimit: 80, | ||
| totalMessages: 2 | ||
| }) | ||
| }); | ||
|
|
||
| const jumps = []; | ||
| const context = { | ||
| activeSession: { | ||
| source: 'codex', | ||
| sessionId: 's1', | ||
| filePath: '/tmp/demo.jsonl', | ||
| match: { | ||
| hit: true, | ||
| count: 1, | ||
| snippets: ['needle is right here'] | ||
| } | ||
| }, | ||
| mainTab: 'sessions', | ||
| sessionPreviewRenderEnabled: true, | ||
| sessionDetailRequestSeq: 0, | ||
| sessionDetailInitialMessageLimit: 80, | ||
| sessionDetailMessageLimit: 80, | ||
| sessionPreviewPendingVisibleCount: 0, | ||
| sessionPreviewVisibleCount: 0, | ||
| sessionMessageRefMap: Object.create(null), | ||
| sessionMessageRefBinderMap: Object.create(null), | ||
| resetSessionDetailPagination() {}, | ||
| resetSessionPreviewMessageRender() {}, | ||
| primeSessionPreviewMessageRender() { | ||
| this.sessionPreviewVisibleCount = 2; | ||
| }, | ||
| cancelSessionTimelineSync() {}, | ||
| clearSessionTimelineRefs() {}, | ||
| syncActiveSessionMessageCount() {}, | ||
| invalidateSessionTimelineMeasurementCache() {}, | ||
| $nextTick(fn) { fn(); }, | ||
| jumpToSessionTimelineNode(key) { jumps.push(key); }, | ||
| getRecordKey(message) { | ||
| return String(message.recordLineIndex); | ||
| }, | ||
| getRecordRenderKey(message, idx) { | ||
| return `record-${message.recordLineIndex || idx}`; | ||
| }, | ||
| normalizeSessionMessage: methods.normalizeSessionMessage, | ||
| pendingSessionMatchKey: '' | ||
| }; | ||
|
|
||
| await methods.loadActiveSessionDetail.call(context); | ||
|
|
||
| assert.strictEqual(context.pendingSessionMatchKey, 'record-2'); | ||
| assert.strictEqual(context.activeSessionMessages[1].isSearchMatch, true); | ||
| assert.strictEqual(context.activeSessionMessages[1].matchSnippet, 'needle is right here'); | ||
| assert.deepStrictEqual(jumps, ['record-2']); | ||
| }); | ||
| Original file line number | Diff line number | Diff line change | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| @@ -1,6 +1,8 @@ | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| import { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| buildSessionFilterCacheState, | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| findSessionMessageMatchKey, | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| isSessionQueryEnabled, | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| normalizeSessionMatch, | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| normalizeSessionMessageRole, | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| normalizeSessionPathFilter | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } from '../logic.mjs'; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
@@ -323,11 +325,16 @@ export function createSessionBrowserMethods(options = {}) { | |||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| const roleLabel = normalizedRole === 'user' | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| ? 'User' | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| : (normalizedRole === 'system' ? 'System' : 'Assistant'); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| const matchSnippet = typeof safeMessage.matchSnippet === 'string' | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| ? safeMessage.matchSnippet.trim() | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| : ''; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| return { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| ...safeMessage, | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| role: normalizedRole, | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| normalizedRole, | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| roleLabel | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| roleLabel, | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| matchSnippet, | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| isSearchMatch: !!matchSnippet | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| }; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| }, | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
@@ -376,6 +383,7 @@ export function createSessionBrowserMethods(options = {}) { | |||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| this.activeSessionDetailClipped = false; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| this.cancelSessionTimelineSync(); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| this.sessionTimelineActiveKey = ''; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| this.pendingSessionMatchKey = ''; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| this.clearSessionTimelineRefs(); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| await this.loadActiveSessionDetail(); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| }, | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
@@ -429,7 +437,31 @@ export function createSessionBrowserMethods(options = {}) { | |||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| }, | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| async loadActiveSessionDetail(options = {}) { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| return loadActiveSessionDetailHelper.call(this, api, options); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| const result = await loadActiveSessionDetailHelper.call(this, api, options); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| const match = normalizeSessionMatch(this.activeSession); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| const nextMatchKey = findSessionMessageMatchKey(this.activeSessionMessages, match.snippets); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| this.pendingSessionMatchKey = nextMatchKey; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| if (nextMatchKey) { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| this.activeSessionMessages = Object.freeze( | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| this.activeSessionMessages.map((message, index) => { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| const key = this.getRecordRenderKey(message, index); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| if (key !== nextMatchKey) { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| return message; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| return Object.freeze({ | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| ...message, | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| matchSnippet: match.primarySnippet | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| }); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| }) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| ); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| this.$nextTick(() => { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| if (this.pendingSessionMatchKey !== nextMatchKey) return; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| if (typeof this.jumpToSessionTimelineNode === 'function') { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| this.jumpToSessionTimelineNode(nextMatchKey); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| }); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| return result; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
Comment on lines
439
to
465
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Potential race condition between match computation and async helper result. The match is computed from Consider capturing 🛡️ Suggested guard async loadActiveSessionDetail(options = {}) {
+ const sessionSnapshot = this.activeSession;
const result = await loadActiveSessionDetailHelper.call(this, api, options);
+ // Guard against session change during async load
+ if (this.activeSession !== sessionSnapshot) {
+ return result;
+ }
const match = normalizeSessionMatch(this.activeSession);📝 Committable suggestion
Suggested change
🤖 Prompt for AI Agents |
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| }; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
🧩 Analysis chain
🏁 Script executed:
Repository: SakuraByteCore/codexmate
Length of output: 620
🏁 Script executed:
cat -n tests/unit/session-search-ui.test.mjs | head -70Repository: SakuraByteCore/codexmate
Length of output: 3112
🏁 Script executed:
Repository: SakuraByteCore/codexmate
Length of output: 95
🏁 Script executed:
Repository: SakuraByteCore/codexmate
Length of output: 95
🏁 Script executed:
Repository: SakuraByteCore/codexmate
Length of output: 116
🏁 Script executed:
Repository: SakuraByteCore/codexmate
Length of output: 2121
🏁 Script executed:
Repository: SakuraByteCore/codexmate
Length of output: 1461
🏁 Script executed:
# Read the test runner to understand how tests work cat tests/unit/run.mjsRepository: SakuraByteCore/codexmate
Length of output: 3842
🏁 Script executed:
Repository: SakuraByteCore/codexmate
Length of output: 1630
🏁 Script executed:
Repository: SakuraByteCore/codexmate
Length of output: 2512
🏁 Script executed:
Repository: SakuraByteCore/codexmate
Length of output: 50
🏁 Script executed:
Repository: SakuraByteCore/codexmate
Length of output: 1218
Test file is not registered in the test suite and will never execute.
The file
session-search-ui.test.mjsis not imported intests/unit/run.mjs, so this test is dead code. Either add it to the test runner or remove the file. Additionally, if this test were to run, it would fail becausecreateSessionBrowserMethodsdestructuresloadActiveSessionDetailHelperfrom options (line 12 ofapp.methods.session-browser.mjs), but the test only passesapion line 61. To fix: add the test totests/unit/run.mjsand provide the missing helper in the mock setup, or delete the orphaned test file.🤖 Prompt for AI Agents