diff --git a/.nvmrc b/.nvmrc new file mode 100644 index 00000000..2bd5a0a9 --- /dev/null +++ b/.nvmrc @@ -0,0 +1 @@ +22 diff --git a/package.json b/package.json index cee90287..65d928b8 100644 --- a/package.json +++ b/package.json @@ -7,6 +7,9 @@ "freshell": "dist/server/cli/index.js" }, "main": "dist/electron/electron/entry.js", + "engines": { + "node": ">=22.5.0" + }, "scripts": { "predev": "cross-env PORT=3002 tsx scripts/precheck.ts", "preserve": "tsx scripts/precheck.ts", diff --git a/server/coding-cli/providers/codex.ts b/server/coding-cli/providers/codex.ts index 2d6c1f3d..6ff001a5 100644 --- a/server/coding-cli/providers/codex.ts +++ b/server/coding-cli/providers/codex.ts @@ -328,7 +328,11 @@ export function parseCodexSessionContent(content: string): ParsedSessionMeta { if (ideRequest) { title = extractTitleFromMessage(ideRequest, 200) } else if (!isSystemContext(text)) { - title = extractTitleFromMessage(text, 200) + // Strip image markup tags so titles show the actual user request + const cleaned = text.replace(/<\/?image[^>]*>/g, '').trim() + if (cleaned) { + title = extractTitleFromMessage(cleaned, 200) + } } } } diff --git a/server/coding-cli/providers/opencode.ts b/server/coding-cli/providers/opencode.ts index 99873c2e..51aabfe1 100644 --- a/server/coding-cli/providers/opencode.ts +++ b/server/coding-cli/providers/opencode.ts @@ -52,7 +52,7 @@ export class OpencodeProvider implements CodingCliProvider { try { sqlite = await import('node:sqlite') } catch { - logger.debug({ provider: this.name }, 'node:sqlite unavailable (requires Node 22.5+); skipping OpenCode sessions') + logger.warn({ provider: this.name, nodeVersion: process.version }, 'node:sqlite unavailable — OpenCode sessions will not appear. Upgrade to Node 22.5+ to enable.') return [] } diff --git a/server/coding-cli/session-indexer.ts b/server/coding-cli/session-indexer.ts index 66e96cfc..00a5a727 100644 --- a/server/coding-cli/session-indexer.ts +++ b/server/coding-cli/session-indexer.ts @@ -940,14 +940,28 @@ export class CodingCliSessionIndexer { enabledSet: Set, seenCacheKeys: Set, ): Promise { - // Collect all file-based entries for enrichment. + // Collect all file-based entries for enrichment. Files the lightweight scan couldn't + // parse (e.g. Codex with 14KB first lines) use file mtime as the recency estimate. + const statCache = new Map() const candidates: Array<{ provider: CodingCliProvider; filePath: string; cacheKey: string; lastActivityAt: number; isSubagent: boolean }> = [] for (const [provider, files] of filesByProvider) { if (!enabledSet.has(provider.name)) continue for (const filePath of files) { const cacheKey = normalizeFilePath(filePath) const cached = this.fileCache.get(cacheKey) - const lastActivityAt = cached?.baseSession?.lastActivityAt ?? 0 + let lastActivityAt = cached?.baseSession?.lastActivityAt ?? cached?.mtimeMs ?? 0 + if (lastActivityAt === 0) { + // No cache entry — file wasn't parseable from 4KB. Use mtime for sorting. + try { + let mtime = statCache.get(cacheKey) + if (mtime === undefined) { + const stat = await fsp.stat(filePath) + mtime = stat.mtimeMs || stat.mtime.getTime() + statCache.set(cacheKey, mtime) + } + lastActivityAt = mtime + } catch { /* file may have been deleted */ } + } const isSubagent = cached?.baseSession?.isSubagent ?? isSubagentSession(filePath) candidates.push({ provider, filePath, cacheKey, lastActivityAt, isSubagent }) } diff --git a/server/coding-cli/utils.ts b/server/coding-cli/utils.ts index 20716544..5d16d32f 100644 --- a/server/coding-cli/utils.ts +++ b/server/coding-cli/utils.ts @@ -308,8 +308,9 @@ async function resolveCommonDirFromGitFile(gitdir: string): Promise { export function isSystemContext(text: string): boolean { const trimmed = text.trim() if (!trimmed) return false - // XML-wrapped system context: , , , etc. - if (/^<[a-zA-Z_][\w_-]*[>\s]/.test(trimmed)) return true + // XML-wrapped system context injected by coding CLIs. Uses an allowlist of known system + // context tag names to avoid false-positives on user content like , , . + if (/^<(environment_context|system_context|system|context|INSTRUCTIONS|user_instructions|permissions|collaboration_mode|skills_instructions)[>\s]/i.test(trimmed)) return true // Instruction file headers: "# AGENTS.md instructions for...", "# System", "# Instructions" if (/^#\s*(AGENTS|Instructions?|System)/i.test(trimmed)) return true // Bracketed agent mode instructions: [SUGGESTION MODE: ...], [REVIEW MODE: ...]