Skip to content
Merged
Show file tree
Hide file tree
Changes from 6 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 3 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -60,9 +60,11 @@ It works on local files directly and does not require cloud hosting. The skills
- Unified Codex + Claude session list
- Local session pinning with persistent pinned state and pinned-first ordering
- Keyword/source/cwd filters
- Fast search UX: short-lived query result caching to avoid rescanning on each keystroke
- Usage subview with 7d / 30d session trends, message trends, source share, and top paths
- Markdown export
- Session-level and message-level delete (supports batch)
- Session-level and message-level delete (supports batch), with a local recycle bin for restore/purge
- Large-session preview optimization (fast tail preview path)

**Skills Market**
- Switch the skills install target between Codex and Claude Code
Expand Down
4 changes: 3 additions & 1 deletion README.zh.md
Original file line number Diff line number Diff line change
Expand Up @@ -61,9 +61,11 @@ Codex Mate 提供一套本地优先的 CLI + Web UI,用于统一管理:
- 同页查看 Codex 与 Claude 会话
- 支持本地会话置顶,置顶状态持久化保存并优先排序显示
- 关键词搜索、来源筛选、cwd 路径筛选
- 搜索体验优化:短周期结果缓存,避免输入时重复扫描
- Usage 子页:近 7 天 / 近 30 天会话趋势、消息趋势、来源占比、高频路径
- 会话导出 Markdown
- 会话与消息级删除(支持批量)
- 会话与消息级删除(支持批量),并提供本地回收站用于恢复/彻底删除
- 大会话预览优化(快速 tail 预览路径)

**Skills 市场**
- 在 Codex 与 Claude Code 之间切换 skills 安装目标
Expand Down
18 changes: 9 additions & 9 deletions cli.js
Original file line number Diff line number Diff line change
Expand Up @@ -4158,12 +4158,14 @@ async function listAllSessions(params = {}) {
const queryTokens = expandSessionQueryTokens(normalizeQueryTokens(params.query));
const hasQuery = queryTokens.length > 0;
const browseLightweight = params.browseLightweight === true && !hasQuery && !hasPathFilter;
const cacheKey = hasQuery ? '' : `${browseLightweight ? 'browse' : 'default'}:${source}:${limit}:${normalizedPathFilter}`;
if (!hasQuery) {
const cached = getSessionListCache(cacheKey, forceRefresh);
if (cached) {
return cached;
}
const queryKeyRaw = typeof params.query === 'string' ? params.query.trim() : '';
const queryKey = queryKeyRaw.length > 240 ? queryKeyRaw.slice(0, 240) : queryKeyRaw;
const cacheKey = hasQuery
? `query:${source}:${limit}:${normalizedPathFilter}:${params.queryMode || ''}:${params.queryScope || ''}:${params.roleFilter || ''}:${Number(params.contentScanLimit) || ''}:${Number(params.contentScanBytes) || ''}:${queryKey}`
: `${browseLightweight ? 'browse' : 'default'}:${source}:${limit}:${normalizedPathFilter}`;
Comment on lines +4161 to +4165

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

Avoid cache-key collisions for long queries.

Truncating the query text to 240 chars can make two different searches share one cache entry, so a later search may get the previous result set until the 4s TTL expires. Use a hash of the full normalized query payload instead of a prefix.

💡 Proposed fix
-    const queryKeyRaw = typeof params.query === 'string' ? params.query.trim() : '';
-    const queryKey = queryKeyRaw.length > 240 ? queryKeyRaw.slice(0, 240) : queryKeyRaw;
+    const queryKey = hasQuery
+        ? crypto.createHash('sha256').update(JSON.stringify({
+            tokens: queryTokens,
+            queryMode: normalizeQueryMode(params.queryMode),
+            queryScope: normalizeQueryScope(params.queryScope),
+            roleFilter: normalizeRoleFilter(params.roleFilter),
+            contentScanLimit: Number.isFinite(Number(params.contentScanLimit))
+                ? Math.max(1, Number(params.contentScanLimit))
+                : DEFAULT_CONTENT_SCAN_LIMIT,
+            contentScanBytes: Number.isFinite(Number(params.contentScanBytes))
+                ? Math.max(1024, Number(params.contentScanBytes))
+                : 0
+        })).digest('hex')
+        : '';
     const cacheKey = hasQuery
-        ? `query:${source}:${limit}:${normalizedPathFilter}:${params.queryMode || ''}:${params.queryScope || ''}:${params.roleFilter || ''}:${Number(params.contentScanLimit) || ''}:${Number(params.contentScanBytes) || ''}:${queryKey}`
+        ? `query:${source}:${limit}:${normalizedPathFilter}:${queryKey}`
         : `${browseLightweight ? 'browse' : 'default'}:${source}:${limit}:${normalizedPathFilter}`;
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@cli.js` around lines 4161 - 4165, The current cacheKey logic truncates
params.query into queryKey (via queryKeyRaw.slice(0,240)), which can cause
collisions; replace that truncation with a stable hash of the full normalized
query payload: compute a SHA-256 (or other strong) hex digest of the full
params.query (or the full normalized query payload including
params.queryMode/params.queryScope/roleFilter/etc.) and use that digest in the
cacheKey instead of queryKey (you can optionally keep a short human-readable
prefix of queryKeyRaw). Update the code that defines queryKeyRaw/queryKey and
the branch that composes cacheKey (the symbols to change: queryKeyRaw, queryKey,
cacheKey) to compute and insert the hash (e.g., via
crypto.createHash('sha256').update(fullPayload).digest('hex')) so long queries
do not collide.

const cached = getSessionListCache(cacheKey, forceRefresh);
if (cached) {
return cached;
}

const scanOptions = hasPathFilter
Expand Down Expand Up @@ -4204,9 +4206,7 @@ async function listAllSessions(params = {}) {
});
}
result = mergeAndLimitSessions(result, limit);
if (!hasQuery) {
setSessionListCache(cacheKey, result);
}
setSessionListCache(cacheKey, result);
return result;
}

Expand Down
4 changes: 4 additions & 0 deletions plugins/prompt-templates/methods.mjs
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import {
persistPromptTemplatesToStorage,
persistPromptTemplateSelectedIdToStorage
} from './storage.mjs';
import {
getFirstPluginId,
Expand Down Expand Up @@ -116,6 +117,7 @@ export function createPluginsMethods() {
if (!next) return;
if (next === this.promptComposerSelectedTemplateId) return;
this.promptComposerSelectedTemplateId = next;
persistPromptTemplateSelectedIdToStorage(next, localStorage);
this.promptComposerVarValuesRaw = readComposerValuesForTemplate(next);
if (typeof this.$nextTick === 'function') {
this.$nextTick(() => {
Expand Down Expand Up @@ -218,6 +220,7 @@ export function createPluginsMethods() {
const next = typeof id === 'string' ? id.trim() : '';
if (!next) return;
this.promptComposerSelectedTemplateId = next;
persistPromptTemplateSelectedIdToStorage(next, localStorage);
this.promptComposerVarValuesRaw = readComposerValuesForTemplate(next);
this.promptComposerCommand = '';
this.promptComposerPickerVisible = false;
Expand All @@ -235,6 +238,7 @@ export function createPluginsMethods() {
resetPromptComposer() {
this.promptComposerCommand = '';
this.promptComposerSelectedTemplateId = '';
persistPromptTemplateSelectedIdToStorage('', localStorage);
this.promptComposerVarValuesRaw = {};
this.promptComposerPickerVisible = false;
this.promptComposerPickerKeyword = '';
Expand Down
11 changes: 9 additions & 2 deletions plugins/prompt-templates/overview.mjs
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
import {
persistPromptTemplatesToStorage,
readPromptTemplatesFromStorage
readPromptTemplatesFromStorage,
readPromptTemplateSelectedIdFromStorage,
persistPromptTemplateSelectedIdToStorage
} from './storage.mjs';
import { buildBuiltinCommentPolishTemplate } from './comment-polish/index.mjs';
import { buildBuiltinRuleAckTemplate } from './rule-ack/index.mjs';
Expand Down Expand Up @@ -49,10 +51,15 @@ export async function loadPromptTemplatesOverview(ctx, options = {}) {

if (app.mainTab === 'plugins' && app.promptTemplatesMode === 'compose') {
const list = Array.isArray(app.promptTemplatesList) ? app.promptTemplatesList : [];
const storedSelected = readPromptTemplateSelectedIdFromStorage(localStorage);
const exists = list.some((item) => item && item.id === app.promptComposerSelectedTemplateId);
if (!app.promptComposerSelectedTemplateId || !exists) {
const storedExists = storedSelected ? list.some((item) => item && item.id === storedSelected) : false;
if (storedExists && storedSelected !== app.promptComposerSelectedTemplateId) {
app.promptComposerSelectedTemplateId = storedSelected;
} else if (!app.promptComposerSelectedTemplateId || !exists) {
app.promptComposerSelectedTemplateId = 'builtin_comment_polish';
}
persistPromptTemplateSelectedIdToStorage(app.promptComposerSelectedTemplateId, localStorage);
if (!app.promptComposerVarValuesRaw || typeof app.promptComposerVarValuesRaw !== 'object') {
app.promptComposerVarValuesRaw = {};
}
Expand Down
28 changes: 28 additions & 0 deletions plugins/prompt-templates/storage.mjs
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
const STORAGE_KEY = 'codexmate.plugins.promptTemplates.v1';
const SELECTED_TEMPLATE_STORAGE_KEY = 'codexmate.plugins.promptTemplates.selectedTemplateId.v1';

export function readPromptTemplatesFromStorage(storage = localStorage) {
if (!storage) return [];
Expand Down Expand Up @@ -34,3 +35,30 @@ export function clearPromptTemplatesStorage(storage = localStorage) {
storage.removeItem(STORAGE_KEY);
} catch (_) {}
}

export function readPromptTemplateSelectedIdFromStorage(storage = localStorage) {
if (!storage) return '';
let raw = '';
try {
raw = storage.getItem(SELECTED_TEMPLATE_STORAGE_KEY) || '';
} catch (_) {
raw = '';
}
const id = typeof raw === 'string' ? raw.trim() : '';
return id;
}

export function persistPromptTemplateSelectedIdToStorage(templateId, storage = localStorage) {
if (!storage) return false;
const id = typeof templateId === 'string' ? templateId.trim() : '';
try {
if (!id) {
storage.removeItem(SELECTED_TEMPLATE_STORAGE_KEY);
return true;
}
storage.setItem(SELECTED_TEMPLATE_STORAGE_KEY, id);
return true;
} catch (_) {
return false;
}
}
10 changes: 8 additions & 2 deletions web-ui/styles/controls-forms.css
Original file line number Diff line number Diff line change
Expand Up @@ -360,10 +360,16 @@
}

.selector-header .btn-tool-compact {
padding: 3px 7px;
display: inline-flex;
align-items: center;
justify-content: center;
width: auto;
min-width: 0;
padding: 2px 6px;
font-size: 10px;
line-height: 1;
box-shadow: none;
min-height: 24px;
height: 22px;
min-height: 22px;
white-space: nowrap;
}
4 changes: 2 additions & 2 deletions web-ui/styles/layout-shell.css
Original file line number Diff line number Diff line change
Expand Up @@ -315,14 +315,14 @@ body::after {
color: var(--color-text-muted);
letter-spacing: 0.06em;
text-transform: uppercase;
padding: 0 8px;
padding: 0 6px;
margin-bottom: 8px;
}

.side-item {
width: 100%;
text-align: left;
padding: 7px 10px;
padding: 5px;
border-radius: 8px;
border: 1px solid transparent;
background: transparent;
Expand Down
Loading