cxs is a local-first CLI for searching Codex session logs. It is designed for progressive retrieval: find the right session first, then read
1
fork

Configure Feed

Select the types of activity you want to include in your feed.

fix(query): 补齐单字 CJK 召回

+62 -1
+7
src/query-session-fields.test.ts
··· 58 58 expect(found.results[0]?.matchSource).toBe("session"); 59 59 expect(found.results[0]?.matchSeq).toBeNull(); 60 60 expect(found.results[0]?.snippet).toContain("订阅取消提醒"); 61 + 62 + const singleCharFound = findSessions(dbPath, "设", 5); 63 + expect(singleCharFound.results).toHaveLength(1); 64 + expect(singleCharFound.results[0]?.sessionUuid).toBe("abababab-abab-4aba-8aba-abababababab"); 65 + expect(singleCharFound.results[0]?.matchSource).toBe("session"); 66 + expect(singleCharFound.results[0]?.matchSeq).toBeNull(); 67 + expect(singleCharFound.results[0]?.snippet).toContain("<mark>设</mark>"); 61 68 }); 62 69 63 70 test("session-level fields have explicit ranking weights", () => {
+55 -1
src/query/search.ts
··· 33 33 if (!normalized || !tableExists(db, "sessions_fts")) return []; 34 34 35 35 const terms = queryTerms(normalized); 36 - if (terms.length === 0) return []; 36 + if (terms.length === 0) { 37 + if (hasCjk(normalized)) return searchSessionsByLike(db, normalized, limit, selector); 38 + return []; 39 + } 37 40 38 41 return searchSessionsByFts(db, normalized, terms, limit, selector); 39 42 } ··· 129 132 return rows.map((row) => ({ 130 133 ...row, 131 134 snippet: makeRawSnippet(row.contentText, query, terms), 135 + })); 136 + } 137 + 138 + function searchSessionsByLike( 139 + db: Db, 140 + query: string, 141 + limit: number, 142 + selector: Selector | null, 143 + ): RawHitRow[] { 144 + const like = `%${escapeLike(query.toLowerCase())}%`; 145 + const conditions = [ 146 + `( 147 + lower(s.title) LIKE ? ESCAPE '\\' 148 + OR lower(s.summary_text) LIKE ? ESCAPE '\\' 149 + OR lower(s.compact_text) LIKE ? ESCAPE '\\' 150 + OR lower(s.reasoning_summary_text) LIKE ? ESCAPE '\\' 151 + )`, 152 + ]; 153 + const params: SqlParams = [like, like, like, like]; 154 + if (selector) { 155 + const selectorWhere = selectorWhereSql(selector, "s"); 156 + conditions.push(...selectorWhere.conditions); 157 + params.push(...selectorWhere.params); 158 + } 159 + params.push(limit); 160 + 161 + const rows = db 162 + .prepare<typeof params, RawHitRow & { contentText: string }>(` 163 + SELECT 164 + s.session_uuid AS sessionUuid, 165 + s.title AS title, 166 + s.summary_text AS summaryText, 167 + s.cwd AS cwd, 168 + s.started_at AS startedAt, 169 + s.ended_at AS endedAt, 170 + 'session' AS matchSource, 171 + NULL AS matchSeq, 172 + 'session' AS matchRole, 173 + NULL AS matchTimestamp, 174 + s.title || char(10) || s.summary_text || char(10) || s.compact_text || char(10) || s.reasoning_summary_text AS contentText 175 + FROM sessions s 176 + WHERE ${conditions.join(" AND ")} 177 + ORDER BY s.started_at DESC 178 + LIMIT ? 179 + `) 180 + .all(...params) as Array<RawHitRow & { contentText: string }>; 181 + 182 + return rows.map((row, index) => ({ 183 + ...row, 184 + snippet: makeRawSnippet(row.contentText, query, []), 185 + score: -(index + 1), 132 186 })); 133 187 } 134 188