@siteboon/claude-code-ui 1.23.2 → 1.24.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/assets/{index-C6ZomNnQ.js → index-Cyy9g2W2.js} +181 -181
- package/dist/assets/index-Dlc1jmTz.css +32 -0
- package/dist/index.html +2 -2
- package/package.json +1 -1
- package/server/index.js +46 -1
- package/server/projects.js +684 -5
- package/server/routes/gemini.js +7 -1
- package/server/routes/git.js +127 -71
- package/server/routes/projects.js +4 -6
- package/server/routes/user.js +22 -5
- package/server/utils/gitConfig.js +15 -5
- package/dist/assets/index-BFyod1Qa.css +0 -32
package/server/projects.js
CHANGED
|
@@ -481,9 +481,13 @@ async function getProjects(progressCallback = null) {
|
|
|
481
481
|
}
|
|
482
482
|
applyCustomSessionNames(project.codexSessions, 'codex');
|
|
483
483
|
|
|
484
|
-
// Also fetch Gemini sessions for this project
|
|
484
|
+
// Also fetch Gemini sessions for this project (UI + CLI)
|
|
485
485
|
try {
|
|
486
|
-
|
|
486
|
+
const uiSessions = sessionManager.getProjectSessions(actualProjectDir) || [];
|
|
487
|
+
const cliSessions = await getGeminiCliSessions(actualProjectDir);
|
|
488
|
+
const uiIds = new Set(uiSessions.map(s => s.id));
|
|
489
|
+
const mergedGemini = [...uiSessions, ...cliSessions.filter(s => !uiIds.has(s.id))];
|
|
490
|
+
project.geminiSessions = mergedGemini;
|
|
487
491
|
} catch (e) {
|
|
488
492
|
console.warn(`Could not load Gemini sessions for project ${entry.name}:`, e.message);
|
|
489
493
|
project.geminiSessions = [];
|
|
@@ -584,9 +588,12 @@ async function getProjects(progressCallback = null) {
|
|
|
584
588
|
}
|
|
585
589
|
applyCustomSessionNames(project.codexSessions, 'codex');
|
|
586
590
|
|
|
587
|
-
// Try to fetch Gemini sessions for manual projects too
|
|
591
|
+
// Try to fetch Gemini sessions for manual projects too (UI + CLI)
|
|
588
592
|
try {
|
|
589
|
-
|
|
593
|
+
const uiSessions = sessionManager.getProjectSessions(actualProjectDir) || [];
|
|
594
|
+
const cliSessions = await getGeminiCliSessions(actualProjectDir);
|
|
595
|
+
const uiIds = new Set(uiSessions.map(s => s.id));
|
|
596
|
+
project.geminiSessions = [...uiSessions, ...cliSessions.filter(s => !uiIds.has(s.id))];
|
|
590
597
|
} catch (e) {
|
|
591
598
|
console.warn(`Could not load Gemini sessions for manual project ${projectName}:`, e.message);
|
|
592
599
|
}
|
|
@@ -1862,6 +1869,675 @@ async function deleteCodexSession(sessionId) {
|
|
|
1862
1869
|
}
|
|
1863
1870
|
}
|
|
1864
1871
|
|
|
1872
|
+
async function searchConversations(query, limit = 50, onProjectResult = null, signal = null) {
|
|
1873
|
+
const safeQuery = typeof query === 'string' ? query.trim() : '';
|
|
1874
|
+
const safeLimit = Math.max(1, Math.min(Number.isFinite(limit) ? limit : 50, 200));
|
|
1875
|
+
const claudeDir = path.join(os.homedir(), '.claude', 'projects');
|
|
1876
|
+
const config = await loadProjectConfig();
|
|
1877
|
+
const results = [];
|
|
1878
|
+
let totalMatches = 0;
|
|
1879
|
+
const words = safeQuery.toLowerCase().split(/\s+/).filter(w => w.length > 0);
|
|
1880
|
+
if (words.length === 0) return { results: [], totalMatches: 0, query: safeQuery };
|
|
1881
|
+
|
|
1882
|
+
const isAborted = () => signal?.aborted === true;
|
|
1883
|
+
|
|
1884
|
+
const isSystemMessage = (textContent) => {
|
|
1885
|
+
return typeof textContent === 'string' && (
|
|
1886
|
+
textContent.startsWith('<command-name>') ||
|
|
1887
|
+
textContent.startsWith('<command-message>') ||
|
|
1888
|
+
textContent.startsWith('<command-args>') ||
|
|
1889
|
+
textContent.startsWith('<local-command-stdout>') ||
|
|
1890
|
+
textContent.startsWith('<system-reminder>') ||
|
|
1891
|
+
textContent.startsWith('Caveat:') ||
|
|
1892
|
+
textContent.startsWith('This session is being continued from a previous') ||
|
|
1893
|
+
textContent.startsWith('Invalid API key') ||
|
|
1894
|
+
textContent.includes('{"subtasks":') ||
|
|
1895
|
+
textContent.includes('CRITICAL: You MUST respond with ONLY a JSON') ||
|
|
1896
|
+
textContent === 'Warmup'
|
|
1897
|
+
);
|
|
1898
|
+
};
|
|
1899
|
+
|
|
1900
|
+
const extractText = (content) => {
|
|
1901
|
+
if (typeof content === 'string') return content;
|
|
1902
|
+
if (Array.isArray(content)) {
|
|
1903
|
+
return content
|
|
1904
|
+
.filter(part => part.type === 'text' && part.text)
|
|
1905
|
+
.map(part => part.text)
|
|
1906
|
+
.join(' ');
|
|
1907
|
+
}
|
|
1908
|
+
return '';
|
|
1909
|
+
};
|
|
1910
|
+
|
|
1911
|
+
const escapeRegex = (s) => s.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
|
|
1912
|
+
const wordPatterns = words.map(w => new RegExp(`(?<!\\p{L})${escapeRegex(w)}(?!\\p{L})`, 'u'));
|
|
1913
|
+
const allWordsMatch = (textLower) => {
|
|
1914
|
+
return wordPatterns.every(p => p.test(textLower));
|
|
1915
|
+
};
|
|
1916
|
+
|
|
1917
|
+
const buildSnippet = (text, textLower, snippetLen = 150) => {
|
|
1918
|
+
let firstIndex = -1;
|
|
1919
|
+
let firstWordLen = 0;
|
|
1920
|
+
for (const w of words) {
|
|
1921
|
+
const re = new RegExp(`(?<!\\p{L})${escapeRegex(w)}(?!\\p{L})`, 'u');
|
|
1922
|
+
const m = re.exec(textLower);
|
|
1923
|
+
if (m && (firstIndex === -1 || m.index < firstIndex)) {
|
|
1924
|
+
firstIndex = m.index;
|
|
1925
|
+
firstWordLen = w.length;
|
|
1926
|
+
}
|
|
1927
|
+
}
|
|
1928
|
+
if (firstIndex === -1) firstIndex = 0;
|
|
1929
|
+
const halfLen = Math.floor(snippetLen / 2);
|
|
1930
|
+
let start = Math.max(0, firstIndex - halfLen);
|
|
1931
|
+
let end = Math.min(text.length, firstIndex + halfLen + firstWordLen);
|
|
1932
|
+
let snippet = text.slice(start, end).replace(/\n/g, ' ');
|
|
1933
|
+
const prefix = start > 0 ? '...' : '';
|
|
1934
|
+
const suffix = end < text.length ? '...' : '';
|
|
1935
|
+
snippet = prefix + snippet + suffix;
|
|
1936
|
+
const snippetLower = snippet.toLowerCase();
|
|
1937
|
+
const highlights = [];
|
|
1938
|
+
for (const word of words) {
|
|
1939
|
+
const re = new RegExp(`(?<!\\p{L})${escapeRegex(word)}(?!\\p{L})`, 'gu');
|
|
1940
|
+
let match;
|
|
1941
|
+
while ((match = re.exec(snippetLower)) !== null) {
|
|
1942
|
+
highlights.push({ start: match.index, end: match.index + word.length });
|
|
1943
|
+
}
|
|
1944
|
+
}
|
|
1945
|
+
highlights.sort((a, b) => a.start - b.start);
|
|
1946
|
+
const merged = [];
|
|
1947
|
+
for (const h of highlights) {
|
|
1948
|
+
const last = merged[merged.length - 1];
|
|
1949
|
+
if (last && h.start <= last.end) {
|
|
1950
|
+
last.end = Math.max(last.end, h.end);
|
|
1951
|
+
} else {
|
|
1952
|
+
merged.push({ ...h });
|
|
1953
|
+
}
|
|
1954
|
+
}
|
|
1955
|
+
return { snippet, highlights: merged };
|
|
1956
|
+
};
|
|
1957
|
+
|
|
1958
|
+
try {
|
|
1959
|
+
await fs.access(claudeDir);
|
|
1960
|
+
const entries = await fs.readdir(claudeDir, { withFileTypes: true });
|
|
1961
|
+
const projectDirs = entries.filter(e => e.isDirectory());
|
|
1962
|
+
let scannedProjects = 0;
|
|
1963
|
+
const totalProjects = projectDirs.length;
|
|
1964
|
+
|
|
1965
|
+
for (const projectEntry of projectDirs) {
|
|
1966
|
+
if (totalMatches >= safeLimit || isAborted()) break;
|
|
1967
|
+
|
|
1968
|
+
const projectName = projectEntry.name;
|
|
1969
|
+
const projectDir = path.join(claudeDir, projectName);
|
|
1970
|
+
const displayName = config[projectName]?.displayName
|
|
1971
|
+
|| await generateDisplayName(projectName);
|
|
1972
|
+
|
|
1973
|
+
let files;
|
|
1974
|
+
try {
|
|
1975
|
+
files = await fs.readdir(projectDir);
|
|
1976
|
+
} catch {
|
|
1977
|
+
continue;
|
|
1978
|
+
}
|
|
1979
|
+
|
|
1980
|
+
const jsonlFiles = files.filter(
|
|
1981
|
+
file => file.endsWith('.jsonl') && !file.startsWith('agent-')
|
|
1982
|
+
);
|
|
1983
|
+
|
|
1984
|
+
const projectResult = {
|
|
1985
|
+
projectName,
|
|
1986
|
+
projectDisplayName: displayName,
|
|
1987
|
+
sessions: []
|
|
1988
|
+
};
|
|
1989
|
+
|
|
1990
|
+
for (const file of jsonlFiles) {
|
|
1991
|
+
if (totalMatches >= safeLimit || isAborted()) break;
|
|
1992
|
+
|
|
1993
|
+
const filePath = path.join(projectDir, file);
|
|
1994
|
+
const sessionMatches = new Map();
|
|
1995
|
+
const sessionSummaries = new Map();
|
|
1996
|
+
const pendingSummaries = new Map();
|
|
1997
|
+
const sessionLastMessages = new Map();
|
|
1998
|
+
let currentSessionId = null;
|
|
1999
|
+
|
|
2000
|
+
try {
|
|
2001
|
+
const fileStream = fsSync.createReadStream(filePath);
|
|
2002
|
+
const rl = readline.createInterface({
|
|
2003
|
+
input: fileStream,
|
|
2004
|
+
crlfDelay: Infinity
|
|
2005
|
+
});
|
|
2006
|
+
|
|
2007
|
+
for await (const line of rl) {
|
|
2008
|
+
if (totalMatches >= safeLimit || isAborted()) break;
|
|
2009
|
+
if (!line.trim()) continue;
|
|
2010
|
+
|
|
2011
|
+
let entry;
|
|
2012
|
+
try {
|
|
2013
|
+
entry = JSON.parse(line);
|
|
2014
|
+
} catch {
|
|
2015
|
+
continue;
|
|
2016
|
+
}
|
|
2017
|
+
|
|
2018
|
+
if (entry.sessionId) {
|
|
2019
|
+
currentSessionId = entry.sessionId;
|
|
2020
|
+
}
|
|
2021
|
+
if (entry.type === 'summary' && entry.summary) {
|
|
2022
|
+
const sid = entry.sessionId || currentSessionId;
|
|
2023
|
+
if (sid) {
|
|
2024
|
+
sessionSummaries.set(sid, entry.summary);
|
|
2025
|
+
} else if (entry.leafUuid) {
|
|
2026
|
+
pendingSummaries.set(entry.leafUuid, entry.summary);
|
|
2027
|
+
}
|
|
2028
|
+
}
|
|
2029
|
+
|
|
2030
|
+
// Apply pending summary via parentUuid
|
|
2031
|
+
if (entry.parentUuid && currentSessionId && !sessionSummaries.has(currentSessionId)) {
|
|
2032
|
+
const pending = pendingSummaries.get(entry.parentUuid);
|
|
2033
|
+
if (pending) sessionSummaries.set(currentSessionId, pending);
|
|
2034
|
+
}
|
|
2035
|
+
|
|
2036
|
+
// Track last user/assistant message for fallback title
|
|
2037
|
+
if (entry.message?.content && currentSessionId && !entry.isApiErrorMessage) {
|
|
2038
|
+
const role = entry.message.role;
|
|
2039
|
+
if (role === 'user' || role === 'assistant') {
|
|
2040
|
+
const text = extractText(entry.message.content);
|
|
2041
|
+
if (text && !isSystemMessage(text)) {
|
|
2042
|
+
if (!sessionLastMessages.has(currentSessionId)) {
|
|
2043
|
+
sessionLastMessages.set(currentSessionId, {});
|
|
2044
|
+
}
|
|
2045
|
+
const msgs = sessionLastMessages.get(currentSessionId);
|
|
2046
|
+
if (role === 'user') msgs.user = text;
|
|
2047
|
+
else msgs.assistant = text;
|
|
2048
|
+
}
|
|
2049
|
+
}
|
|
2050
|
+
}
|
|
2051
|
+
|
|
2052
|
+
if (!entry.message?.content) continue;
|
|
2053
|
+
if (entry.message.role !== 'user' && entry.message.role !== 'assistant') continue;
|
|
2054
|
+
if (entry.isApiErrorMessage) continue;
|
|
2055
|
+
|
|
2056
|
+
const text = extractText(entry.message.content);
|
|
2057
|
+
if (!text || isSystemMessage(text)) continue;
|
|
2058
|
+
|
|
2059
|
+
const textLower = text.toLowerCase();
|
|
2060
|
+
if (!allWordsMatch(textLower)) continue;
|
|
2061
|
+
|
|
2062
|
+
const sessionId = entry.sessionId || currentSessionId || file.replace('.jsonl', '');
|
|
2063
|
+
if (!sessionMatches.has(sessionId)) {
|
|
2064
|
+
sessionMatches.set(sessionId, []);
|
|
2065
|
+
}
|
|
2066
|
+
|
|
2067
|
+
const matches = sessionMatches.get(sessionId);
|
|
2068
|
+
if (matches.length < 2) {
|
|
2069
|
+
const { snippet, highlights } = buildSnippet(text, textLower);
|
|
2070
|
+
matches.push({
|
|
2071
|
+
role: entry.message.role,
|
|
2072
|
+
snippet,
|
|
2073
|
+
highlights,
|
|
2074
|
+
timestamp: entry.timestamp || null,
|
|
2075
|
+
provider: 'claude',
|
|
2076
|
+
messageUuid: entry.uuid || null
|
|
2077
|
+
});
|
|
2078
|
+
totalMatches++;
|
|
2079
|
+
}
|
|
2080
|
+
}
|
|
2081
|
+
} catch {
|
|
2082
|
+
continue;
|
|
2083
|
+
}
|
|
2084
|
+
|
|
2085
|
+
for (const [sessionId, matches] of sessionMatches) {
|
|
2086
|
+
projectResult.sessions.push({
|
|
2087
|
+
sessionId,
|
|
2088
|
+
provider: 'claude',
|
|
2089
|
+
sessionSummary: sessionSummaries.get(sessionId) || (() => {
|
|
2090
|
+
const msgs = sessionLastMessages.get(sessionId);
|
|
2091
|
+
const lastMsg = msgs?.user || msgs?.assistant;
|
|
2092
|
+
return lastMsg ? (lastMsg.length > 50 ? lastMsg.substring(0, 50) + '...' : lastMsg) : 'New Session';
|
|
2093
|
+
})(),
|
|
2094
|
+
matches
|
|
2095
|
+
});
|
|
2096
|
+
}
|
|
2097
|
+
}
|
|
2098
|
+
|
|
2099
|
+
// Search Codex sessions for this project
|
|
2100
|
+
try {
|
|
2101
|
+
const actualProjectDir = await extractProjectDirectory(projectName);
|
|
2102
|
+
if (actualProjectDir && !isAborted() && totalMatches < safeLimit) {
|
|
2103
|
+
await searchCodexSessionsForProject(
|
|
2104
|
+
actualProjectDir, projectResult, words, allWordsMatch, extractText, isSystemMessage,
|
|
2105
|
+
buildSnippet, safeLimit, () => totalMatches, (n) => { totalMatches += n; }, isAborted
|
|
2106
|
+
);
|
|
2107
|
+
}
|
|
2108
|
+
} catch {
|
|
2109
|
+
// Skip codex search errors
|
|
2110
|
+
}
|
|
2111
|
+
|
|
2112
|
+
// Search Gemini sessions for this project
|
|
2113
|
+
try {
|
|
2114
|
+
const actualProjectDir = await extractProjectDirectory(projectName);
|
|
2115
|
+
if (actualProjectDir && !isAborted() && totalMatches < safeLimit) {
|
|
2116
|
+
await searchGeminiSessionsForProject(
|
|
2117
|
+
actualProjectDir, projectResult, words, allWordsMatch,
|
|
2118
|
+
buildSnippet, safeLimit, () => totalMatches, (n) => { totalMatches += n; }
|
|
2119
|
+
);
|
|
2120
|
+
}
|
|
2121
|
+
} catch {
|
|
2122
|
+
// Skip gemini search errors
|
|
2123
|
+
}
|
|
2124
|
+
|
|
2125
|
+
scannedProjects++;
|
|
2126
|
+
if (projectResult.sessions.length > 0) {
|
|
2127
|
+
results.push(projectResult);
|
|
2128
|
+
if (onProjectResult) {
|
|
2129
|
+
onProjectResult({ projectResult, totalMatches, scannedProjects, totalProjects });
|
|
2130
|
+
}
|
|
2131
|
+
} else if (onProjectResult && scannedProjects % 10 === 0) {
|
|
2132
|
+
onProjectResult({ projectResult: null, totalMatches, scannedProjects, totalProjects });
|
|
2133
|
+
}
|
|
2134
|
+
}
|
|
2135
|
+
} catch {
|
|
2136
|
+
// claudeDir doesn't exist
|
|
2137
|
+
}
|
|
2138
|
+
|
|
2139
|
+
return { results, totalMatches, query: safeQuery };
|
|
2140
|
+
}
|
|
2141
|
+
|
|
2142
|
+
async function searchCodexSessionsForProject(
|
|
2143
|
+
projectPath, projectResult, words, allWordsMatch, extractText, isSystemMessage,
|
|
2144
|
+
buildSnippet, limit, getTotalMatches, addMatches, isAborted
|
|
2145
|
+
) {
|
|
2146
|
+
const normalizedProjectPath = normalizeComparablePath(projectPath);
|
|
2147
|
+
if (!normalizedProjectPath) return;
|
|
2148
|
+
const codexSessionsDir = path.join(os.homedir(), '.codex', 'sessions');
|
|
2149
|
+
try {
|
|
2150
|
+
await fs.access(codexSessionsDir);
|
|
2151
|
+
} catch {
|
|
2152
|
+
return;
|
|
2153
|
+
}
|
|
2154
|
+
|
|
2155
|
+
const jsonlFiles = await findCodexJsonlFiles(codexSessionsDir);
|
|
2156
|
+
|
|
2157
|
+
for (const filePath of jsonlFiles) {
|
|
2158
|
+
if (getTotalMatches() >= limit || isAborted()) break;
|
|
2159
|
+
|
|
2160
|
+
try {
|
|
2161
|
+
const fileStream = fsSync.createReadStream(filePath);
|
|
2162
|
+
const rl = readline.createInterface({ input: fileStream, crlfDelay: Infinity });
|
|
2163
|
+
|
|
2164
|
+
// First pass: read session_meta to check project path match
|
|
2165
|
+
let sessionMeta = null;
|
|
2166
|
+
for await (const line of rl) {
|
|
2167
|
+
if (!line.trim()) continue;
|
|
2168
|
+
try {
|
|
2169
|
+
const entry = JSON.parse(line);
|
|
2170
|
+
if (entry.type === 'session_meta' && entry.payload) {
|
|
2171
|
+
sessionMeta = entry.payload;
|
|
2172
|
+
break;
|
|
2173
|
+
}
|
|
2174
|
+
} catch { continue; }
|
|
2175
|
+
}
|
|
2176
|
+
|
|
2177
|
+
// Skip sessions that don't belong to this project
|
|
2178
|
+
if (!sessionMeta) continue;
|
|
2179
|
+
const sessionProjectPath = normalizeComparablePath(sessionMeta.cwd);
|
|
2180
|
+
if (sessionProjectPath !== normalizedProjectPath) continue;
|
|
2181
|
+
|
|
2182
|
+
// Second pass: re-read file to find matching messages
|
|
2183
|
+
const fileStream2 = fsSync.createReadStream(filePath);
|
|
2184
|
+
const rl2 = readline.createInterface({ input: fileStream2, crlfDelay: Infinity });
|
|
2185
|
+
let lastUserMessage = null;
|
|
2186
|
+
const matches = [];
|
|
2187
|
+
|
|
2188
|
+
for await (const line of rl2) {
|
|
2189
|
+
if (getTotalMatches() >= limit || isAborted()) break;
|
|
2190
|
+
if (!line.trim()) continue;
|
|
2191
|
+
|
|
2192
|
+
let entry;
|
|
2193
|
+
try { entry = JSON.parse(line); } catch { continue; }
|
|
2194
|
+
|
|
2195
|
+
let text = null;
|
|
2196
|
+
let role = null;
|
|
2197
|
+
|
|
2198
|
+
if (entry.type === 'event_msg' && entry.payload?.type === 'user_message' && entry.payload.message) {
|
|
2199
|
+
text = entry.payload.message;
|
|
2200
|
+
role = 'user';
|
|
2201
|
+
lastUserMessage = text;
|
|
2202
|
+
} else if (entry.type === 'response_item' && entry.payload?.type === 'message') {
|
|
2203
|
+
const contentParts = entry.payload.content || [];
|
|
2204
|
+
if (entry.payload.role === 'user') {
|
|
2205
|
+
text = contentParts
|
|
2206
|
+
.filter(p => p.type === 'input_text' && p.text)
|
|
2207
|
+
.map(p => p.text)
|
|
2208
|
+
.join(' ');
|
|
2209
|
+
role = 'user';
|
|
2210
|
+
if (text) lastUserMessage = text;
|
|
2211
|
+
} else if (entry.payload.role === 'assistant') {
|
|
2212
|
+
text = contentParts
|
|
2213
|
+
.filter(p => p.type === 'output_text' && p.text)
|
|
2214
|
+
.map(p => p.text)
|
|
2215
|
+
.join(' ');
|
|
2216
|
+
role = 'assistant';
|
|
2217
|
+
}
|
|
2218
|
+
}
|
|
2219
|
+
|
|
2220
|
+
if (!text || !role) continue;
|
|
2221
|
+
const textLower = text.toLowerCase();
|
|
2222
|
+
if (!allWordsMatch(textLower)) continue;
|
|
2223
|
+
|
|
2224
|
+
if (matches.length < 2) {
|
|
2225
|
+
const { snippet, highlights } = buildSnippet(text, textLower);
|
|
2226
|
+
matches.push({ role, snippet, highlights, timestamp: entry.timestamp || null, provider: 'codex' });
|
|
2227
|
+
addMatches(1);
|
|
2228
|
+
}
|
|
2229
|
+
}
|
|
2230
|
+
|
|
2231
|
+
if (matches.length > 0) {
|
|
2232
|
+
projectResult.sessions.push({
|
|
2233
|
+
sessionId: sessionMeta.id,
|
|
2234
|
+
provider: 'codex',
|
|
2235
|
+
sessionSummary: lastUserMessage
|
|
2236
|
+
? (lastUserMessage.length > 50 ? lastUserMessage.substring(0, 50) + '...' : lastUserMessage)
|
|
2237
|
+
: 'Codex Session',
|
|
2238
|
+
matches
|
|
2239
|
+
});
|
|
2240
|
+
}
|
|
2241
|
+
} catch {
|
|
2242
|
+
continue;
|
|
2243
|
+
}
|
|
2244
|
+
}
|
|
2245
|
+
}
|
|
2246
|
+
|
|
2247
|
+
async function searchGeminiSessionsForProject(
|
|
2248
|
+
projectPath, projectResult, words, allWordsMatch,
|
|
2249
|
+
buildSnippet, limit, getTotalMatches, addMatches
|
|
2250
|
+
) {
|
|
2251
|
+
// 1) Search in-memory sessions (created via UI)
|
|
2252
|
+
for (const [sessionId, session] of sessionManager.sessions) {
|
|
2253
|
+
if (getTotalMatches() >= limit) break;
|
|
2254
|
+
if (session.projectPath !== projectPath) continue;
|
|
2255
|
+
|
|
2256
|
+
const matches = [];
|
|
2257
|
+
for (const msg of session.messages) {
|
|
2258
|
+
if (getTotalMatches() >= limit) break;
|
|
2259
|
+
if (msg.role !== 'user' && msg.role !== 'assistant') continue;
|
|
2260
|
+
|
|
2261
|
+
const text = typeof msg.content === 'string' ? msg.content
|
|
2262
|
+
: Array.isArray(msg.content) ? msg.content.filter(p => p.type === 'text').map(p => p.text).join(' ')
|
|
2263
|
+
: '';
|
|
2264
|
+
if (!text) continue;
|
|
2265
|
+
|
|
2266
|
+
const textLower = text.toLowerCase();
|
|
2267
|
+
if (!allWordsMatch(textLower)) continue;
|
|
2268
|
+
|
|
2269
|
+
if (matches.length < 2) {
|
|
2270
|
+
const { snippet, highlights } = buildSnippet(text, textLower);
|
|
2271
|
+
matches.push({
|
|
2272
|
+
role: msg.role, snippet, highlights,
|
|
2273
|
+
timestamp: msg.timestamp ? msg.timestamp.toISOString() : null,
|
|
2274
|
+
provider: 'gemini'
|
|
2275
|
+
});
|
|
2276
|
+
addMatches(1);
|
|
2277
|
+
}
|
|
2278
|
+
}
|
|
2279
|
+
|
|
2280
|
+
if (matches.length > 0) {
|
|
2281
|
+
const firstUserMsg = session.messages.find(m => m.role === 'user');
|
|
2282
|
+
const summary = firstUserMsg?.content
|
|
2283
|
+
? (typeof firstUserMsg.content === 'string'
|
|
2284
|
+
? (firstUserMsg.content.length > 50 ? firstUserMsg.content.substring(0, 50) + '...' : firstUserMsg.content)
|
|
2285
|
+
: 'Gemini Session')
|
|
2286
|
+
: 'Gemini Session';
|
|
2287
|
+
|
|
2288
|
+
projectResult.sessions.push({
|
|
2289
|
+
sessionId,
|
|
2290
|
+
provider: 'gemini',
|
|
2291
|
+
sessionSummary: summary,
|
|
2292
|
+
matches
|
|
2293
|
+
});
|
|
2294
|
+
}
|
|
2295
|
+
}
|
|
2296
|
+
|
|
2297
|
+
// 2) Search Gemini CLI sessions on disk (~/.gemini/tmp/<project>/chats/*.json)
|
|
2298
|
+
const normalizedProjectPath = normalizeComparablePath(projectPath);
|
|
2299
|
+
if (!normalizedProjectPath) return;
|
|
2300
|
+
|
|
2301
|
+
const geminiTmpDir = path.join(os.homedir(), '.gemini', 'tmp');
|
|
2302
|
+
try {
|
|
2303
|
+
await fs.access(geminiTmpDir);
|
|
2304
|
+
} catch {
|
|
2305
|
+
return;
|
|
2306
|
+
}
|
|
2307
|
+
|
|
2308
|
+
const trackedSessionIds = new Set();
|
|
2309
|
+
for (const [sid] of sessionManager.sessions) {
|
|
2310
|
+
trackedSessionIds.add(sid);
|
|
2311
|
+
}
|
|
2312
|
+
|
|
2313
|
+
let projectDirs;
|
|
2314
|
+
try {
|
|
2315
|
+
projectDirs = await fs.readdir(geminiTmpDir);
|
|
2316
|
+
} catch {
|
|
2317
|
+
return;
|
|
2318
|
+
}
|
|
2319
|
+
|
|
2320
|
+
for (const projectDir of projectDirs) {
|
|
2321
|
+
if (getTotalMatches() >= limit) break;
|
|
2322
|
+
|
|
2323
|
+
const projectRootFile = path.join(geminiTmpDir, projectDir, '.project_root');
|
|
2324
|
+
let projectRoot;
|
|
2325
|
+
try {
|
|
2326
|
+
projectRoot = (await fs.readFile(projectRootFile, 'utf8')).trim();
|
|
2327
|
+
} catch {
|
|
2328
|
+
continue;
|
|
2329
|
+
}
|
|
2330
|
+
|
|
2331
|
+
if (normalizeComparablePath(projectRoot) !== normalizedProjectPath) continue;
|
|
2332
|
+
|
|
2333
|
+
const chatsDir = path.join(geminiTmpDir, projectDir, 'chats');
|
|
2334
|
+
let chatFiles;
|
|
2335
|
+
try {
|
|
2336
|
+
chatFiles = await fs.readdir(chatsDir);
|
|
2337
|
+
} catch {
|
|
2338
|
+
continue;
|
|
2339
|
+
}
|
|
2340
|
+
|
|
2341
|
+
for (const chatFile of chatFiles) {
|
|
2342
|
+
if (getTotalMatches() >= limit) break;
|
|
2343
|
+
if (!chatFile.endsWith('.json')) continue;
|
|
2344
|
+
|
|
2345
|
+
try {
|
|
2346
|
+
const filePath = path.join(chatsDir, chatFile);
|
|
2347
|
+
const data = await fs.readFile(filePath, 'utf8');
|
|
2348
|
+
const session = JSON.parse(data);
|
|
2349
|
+
if (!session.messages || !Array.isArray(session.messages)) continue;
|
|
2350
|
+
|
|
2351
|
+
const cliSessionId = session.sessionId || chatFile.replace('.json', '');
|
|
2352
|
+
if (trackedSessionIds.has(cliSessionId)) continue;
|
|
2353
|
+
|
|
2354
|
+
const matches = [];
|
|
2355
|
+
let firstUserText = null;
|
|
2356
|
+
|
|
2357
|
+
for (const msg of session.messages) {
|
|
2358
|
+
if (getTotalMatches() >= limit) break;
|
|
2359
|
+
|
|
2360
|
+
const role = msg.type === 'user' ? 'user'
|
|
2361
|
+
: (msg.type === 'gemini' || msg.type === 'assistant') ? 'assistant'
|
|
2362
|
+
: null;
|
|
2363
|
+
if (!role) continue;
|
|
2364
|
+
|
|
2365
|
+
let text = '';
|
|
2366
|
+
if (typeof msg.content === 'string') {
|
|
2367
|
+
text = msg.content;
|
|
2368
|
+
} else if (Array.isArray(msg.content)) {
|
|
2369
|
+
text = msg.content
|
|
2370
|
+
.filter(p => p.text)
|
|
2371
|
+
.map(p => p.text)
|
|
2372
|
+
.join(' ');
|
|
2373
|
+
}
|
|
2374
|
+
if (!text) continue;
|
|
2375
|
+
|
|
2376
|
+
if (role === 'user' && !firstUserText) firstUserText = text;
|
|
2377
|
+
|
|
2378
|
+
const textLower = text.toLowerCase();
|
|
2379
|
+
if (!allWordsMatch(textLower)) continue;
|
|
2380
|
+
|
|
2381
|
+
if (matches.length < 2) {
|
|
2382
|
+
const { snippet, highlights } = buildSnippet(text, textLower);
|
|
2383
|
+
matches.push({
|
|
2384
|
+
role, snippet, highlights,
|
|
2385
|
+
timestamp: msg.timestamp || null,
|
|
2386
|
+
provider: 'gemini'
|
|
2387
|
+
});
|
|
2388
|
+
addMatches(1);
|
|
2389
|
+
}
|
|
2390
|
+
}
|
|
2391
|
+
|
|
2392
|
+
if (matches.length > 0) {
|
|
2393
|
+
const summary = firstUserText
|
|
2394
|
+
? (firstUserText.length > 50 ? firstUserText.substring(0, 50) + '...' : firstUserText)
|
|
2395
|
+
: 'Gemini CLI Session';
|
|
2396
|
+
|
|
2397
|
+
projectResult.sessions.push({
|
|
2398
|
+
sessionId: cliSessionId,
|
|
2399
|
+
provider: 'gemini',
|
|
2400
|
+
sessionSummary: summary,
|
|
2401
|
+
matches
|
|
2402
|
+
});
|
|
2403
|
+
}
|
|
2404
|
+
} catch {
|
|
2405
|
+
continue;
|
|
2406
|
+
}
|
|
2407
|
+
}
|
|
2408
|
+
}
|
|
2409
|
+
}
|
|
2410
|
+
|
|
2411
|
+
async function getGeminiCliSessions(projectPath) {
|
|
2412
|
+
const normalizedProjectPath = normalizeComparablePath(projectPath);
|
|
2413
|
+
if (!normalizedProjectPath) return [];
|
|
2414
|
+
|
|
2415
|
+
const geminiTmpDir = path.join(os.homedir(), '.gemini', 'tmp');
|
|
2416
|
+
try {
|
|
2417
|
+
await fs.access(geminiTmpDir);
|
|
2418
|
+
} catch {
|
|
2419
|
+
return [];
|
|
2420
|
+
}
|
|
2421
|
+
|
|
2422
|
+
const sessions = [];
|
|
2423
|
+
let projectDirs;
|
|
2424
|
+
try {
|
|
2425
|
+
projectDirs = await fs.readdir(geminiTmpDir);
|
|
2426
|
+
} catch {
|
|
2427
|
+
return [];
|
|
2428
|
+
}
|
|
2429
|
+
|
|
2430
|
+
for (const projectDir of projectDirs) {
|
|
2431
|
+
const projectRootFile = path.join(geminiTmpDir, projectDir, '.project_root');
|
|
2432
|
+
let projectRoot;
|
|
2433
|
+
try {
|
|
2434
|
+
projectRoot = (await fs.readFile(projectRootFile, 'utf8')).trim();
|
|
2435
|
+
} catch {
|
|
2436
|
+
continue;
|
|
2437
|
+
}
|
|
2438
|
+
|
|
2439
|
+
if (normalizeComparablePath(projectRoot) !== normalizedProjectPath) continue;
|
|
2440
|
+
|
|
2441
|
+
const chatsDir = path.join(geminiTmpDir, projectDir, 'chats');
|
|
2442
|
+
let chatFiles;
|
|
2443
|
+
try {
|
|
2444
|
+
chatFiles = await fs.readdir(chatsDir);
|
|
2445
|
+
} catch {
|
|
2446
|
+
continue;
|
|
2447
|
+
}
|
|
2448
|
+
|
|
2449
|
+
for (const chatFile of chatFiles) {
|
|
2450
|
+
if (!chatFile.endsWith('.json')) continue;
|
|
2451
|
+
try {
|
|
2452
|
+
const filePath = path.join(chatsDir, chatFile);
|
|
2453
|
+
const data = await fs.readFile(filePath, 'utf8');
|
|
2454
|
+
const session = JSON.parse(data);
|
|
2455
|
+
if (!session.messages || !Array.isArray(session.messages)) continue;
|
|
2456
|
+
|
|
2457
|
+
const sessionId = session.sessionId || chatFile.replace('.json', '');
|
|
2458
|
+
const firstUserMsg = session.messages.find(m => m.type === 'user');
|
|
2459
|
+
let summary = 'Gemini CLI Session';
|
|
2460
|
+
if (firstUserMsg) {
|
|
2461
|
+
const text = Array.isArray(firstUserMsg.content)
|
|
2462
|
+
? firstUserMsg.content.filter(p => p.text).map(p => p.text).join(' ')
|
|
2463
|
+
: (typeof firstUserMsg.content === 'string' ? firstUserMsg.content : '');
|
|
2464
|
+
if (text) {
|
|
2465
|
+
summary = text.length > 50 ? text.substring(0, 50) + '...' : text;
|
|
2466
|
+
}
|
|
2467
|
+
}
|
|
2468
|
+
|
|
2469
|
+
sessions.push({
|
|
2470
|
+
id: sessionId,
|
|
2471
|
+
summary,
|
|
2472
|
+
messageCount: session.messages.length,
|
|
2473
|
+
lastActivity: session.lastUpdated || session.startTime || null,
|
|
2474
|
+
provider: 'gemini'
|
|
2475
|
+
});
|
|
2476
|
+
} catch {
|
|
2477
|
+
continue;
|
|
2478
|
+
}
|
|
2479
|
+
}
|
|
2480
|
+
}
|
|
2481
|
+
|
|
2482
|
+
return sessions.sort((a, b) =>
|
|
2483
|
+
new Date(b.lastActivity || 0) - new Date(a.lastActivity || 0)
|
|
2484
|
+
);
|
|
2485
|
+
}
|
|
2486
|
+
|
|
2487
|
+
async function getGeminiCliSessionMessages(sessionId) {
|
|
2488
|
+
const geminiTmpDir = path.join(os.homedir(), '.gemini', 'tmp');
|
|
2489
|
+
let projectDirs;
|
|
2490
|
+
try {
|
|
2491
|
+
projectDirs = await fs.readdir(geminiTmpDir);
|
|
2492
|
+
} catch {
|
|
2493
|
+
return [];
|
|
2494
|
+
}
|
|
2495
|
+
|
|
2496
|
+
for (const projectDir of projectDirs) {
|
|
2497
|
+
const chatsDir = path.join(geminiTmpDir, projectDir, 'chats');
|
|
2498
|
+
let chatFiles;
|
|
2499
|
+
try {
|
|
2500
|
+
chatFiles = await fs.readdir(chatsDir);
|
|
2501
|
+
} catch {
|
|
2502
|
+
continue;
|
|
2503
|
+
}
|
|
2504
|
+
|
|
2505
|
+
for (const chatFile of chatFiles) {
|
|
2506
|
+
if (!chatFile.endsWith('.json')) continue;
|
|
2507
|
+
try {
|
|
2508
|
+
const filePath = path.join(chatsDir, chatFile);
|
|
2509
|
+
const data = await fs.readFile(filePath, 'utf8');
|
|
2510
|
+
const session = JSON.parse(data);
|
|
2511
|
+
const fileSessionId = session.sessionId || chatFile.replace('.json', '');
|
|
2512
|
+
if (fileSessionId !== sessionId) continue;
|
|
2513
|
+
|
|
2514
|
+
return (session.messages || []).map(msg => {
|
|
2515
|
+
const role = msg.type === 'user' ? 'user'
|
|
2516
|
+
: (msg.type === 'gemini' || msg.type === 'assistant') ? 'assistant'
|
|
2517
|
+
: msg.type;
|
|
2518
|
+
|
|
2519
|
+
let content = '';
|
|
2520
|
+
if (typeof msg.content === 'string') {
|
|
2521
|
+
content = msg.content;
|
|
2522
|
+
} else if (Array.isArray(msg.content)) {
|
|
2523
|
+
content = msg.content.filter(p => p.text).map(p => p.text).join('\n');
|
|
2524
|
+
}
|
|
2525
|
+
|
|
2526
|
+
return {
|
|
2527
|
+
type: 'message',
|
|
2528
|
+
message: { role, content },
|
|
2529
|
+
timestamp: msg.timestamp || null
|
|
2530
|
+
};
|
|
2531
|
+
});
|
|
2532
|
+
} catch {
|
|
2533
|
+
continue;
|
|
2534
|
+
}
|
|
2535
|
+
}
|
|
2536
|
+
}
|
|
2537
|
+
|
|
2538
|
+
return [];
|
|
2539
|
+
}
|
|
2540
|
+
|
|
1865
2541
|
export {
|
|
1866
2542
|
getProjects,
|
|
1867
2543
|
getSessions,
|
|
@@ -1878,5 +2554,8 @@ export {
|
|
|
1878
2554
|
clearProjectDirectoryCache,
|
|
1879
2555
|
getCodexSessions,
|
|
1880
2556
|
getCodexSessionMessages,
|
|
1881
|
-
deleteCodexSession
|
|
2557
|
+
deleteCodexSession,
|
|
2558
|
+
getGeminiCliSessions,
|
|
2559
|
+
getGeminiCliSessionMessages,
|
|
2560
|
+
searchConversations
|
|
1882
2561
|
};
|