coding-tool-x 3.8.0 → 3.8.1
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/README.md +4 -6
- package/package.json +2 -2
- package/src/config/paths.js +1 -0
- package/src/server/api/codex-sessions.js +14 -14
- package/src/server/api/gemini-sessions.js +52 -14
- package/src/server/api/omp-projects.js +2 -2
- package/src/server/api/omp-sessions.js +61 -23
- package/src/server/api/sessions.js +44 -2
- package/src/server/services/codex-sessions.js +41 -233
- package/src/server/services/dashboard-snapshot-worker.js +5 -5
- package/src/server/services/gemini-sessions.js +67 -157
- package/src/server/services/omp-sessions.js +102 -105
- package/src/server/services/opencode-sessions.js +339 -546
- package/src/server/services/session-converter.js +4 -4
- package/src/server/services/session-history-adapters/claude.js +235 -0
- package/src/server/services/session-history-adapters/codex.js +158 -0
- package/src/server/services/session-history-adapters/gemini.js +292 -0
- package/src/server/services/session-history-adapters/index.js +20 -0
- package/src/server/services/session-history-adapters/omp.js +256 -0
- package/src/server/services/session-history-index.js +930 -0
- package/src/server/services/session-history-worker.js +98 -0
- package/src/server/services/session-snapshot-worker.js +7 -7
- package/src/server/services/sessions.js +167 -775
- package/src/server/services/sqlite-connection.js +78 -0
- package/src/server/services/workspace-service.js +3 -3
|
@@ -1,12 +1,12 @@
|
|
|
1
1
|
const fs = require('fs');
|
|
2
2
|
const path = require('path');
|
|
3
3
|
const crypto = require('crypto');
|
|
4
|
-
const {
|
|
4
|
+
const { DatabaseSync } = require('node:sqlite');
|
|
5
5
|
const { NATIVE_PATHS, PATHS } = require('../../config/paths');
|
|
6
6
|
|
|
7
7
|
/**
|
|
8
8
|
* OpenCode 会话服务
|
|
9
|
-
* 读取 OpenCode SQLite
|
|
9
|
+
* 读取 OpenCode SQLite 会话数据(node:sqlite / DatabaseSync)
|
|
10
10
|
*/
|
|
11
11
|
|
|
12
12
|
const PROJECT_ORDER_FILE = PATHS.opencodeProjectOrder;
|
|
@@ -146,79 +146,48 @@ function toIsoTime(input) {
|
|
|
146
146
|
}
|
|
147
147
|
}
|
|
148
148
|
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
}
|
|
153
|
-
if (typeof value === 'number') {
|
|
154
|
-
return Number.isFinite(value) ? String(Math.trunc(value)) : 'NULL';
|
|
155
|
-
}
|
|
156
|
-
if (typeof value === 'boolean') {
|
|
157
|
-
return value ? '1' : '0';
|
|
158
|
-
}
|
|
159
|
-
return `'${String(value).replace(/'/g, "''")}'`;
|
|
160
|
-
}
|
|
149
|
+
// ---------------------------------------------------------------------------
|
|
150
|
+
// Database helpers (node:sqlite / DatabaseSync)
|
|
151
|
+
// ---------------------------------------------------------------------------
|
|
161
152
|
|
|
162
|
-
function
|
|
163
|
-
if (!isOpenCodeInstalled())
|
|
164
|
-
|
|
165
|
-
|
|
153
|
+
function _openCodeDb() {
|
|
154
|
+
if (!isOpenCodeInstalled()) return null;
|
|
155
|
+
const db = new DatabaseSync(OPENCODE_DB_PATH, { readOnly: false, timeout: 5000 });
|
|
156
|
+
db.exec('PRAGMA foreign_keys = ON');
|
|
157
|
+
return db;
|
|
158
|
+
}
|
|
166
159
|
|
|
160
|
+
function _query(sql, ...params) {
|
|
161
|
+
const db = _openCodeDb();
|
|
162
|
+
if (!db) return [];
|
|
167
163
|
try {
|
|
168
|
-
|
|
169
|
-
encoding: 'utf8',
|
|
170
|
-
stdio: ['ignore', 'pipe', 'pipe'],
|
|
171
|
-
maxBuffer: 10 * 1024 * 1024,
|
|
172
|
-
windowsHide: true
|
|
173
|
-
}).trim();
|
|
174
|
-
|
|
175
|
-
if (!output) {
|
|
176
|
-
return [];
|
|
177
|
-
}
|
|
178
|
-
|
|
179
|
-
const parsed = JSON.parse(output);
|
|
180
|
-
return Array.isArray(parsed) ? parsed : [];
|
|
164
|
+
return db.prepare(sql).all(...params);
|
|
181
165
|
} catch (err) {
|
|
182
166
|
console.error('[OpenCode Sessions] SQLite query failed:', err.message);
|
|
183
167
|
return [];
|
|
184
168
|
}
|
|
185
169
|
}
|
|
186
170
|
|
|
187
|
-
function
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
execFileSync('sqlite3', [OPENCODE_DB_PATH, sql], {
|
|
193
|
-
encoding: 'utf8',
|
|
194
|
-
stdio: ['ignore', 'pipe', 'pipe'],
|
|
195
|
-
maxBuffer: 10 * 1024 * 1024,
|
|
196
|
-
windowsHide: true
|
|
197
|
-
});
|
|
171
|
+
function _exec(sql, ...params) {
|
|
172
|
+
const db = _openCodeDb();
|
|
173
|
+
if (!db) throw new Error('OpenCode CLI not installed');
|
|
174
|
+
db.prepare(sql).run(...params);
|
|
198
175
|
}
|
|
199
176
|
|
|
200
177
|
function buildContext(text, keyword, contextLength = 35) {
|
|
201
178
|
if (!text || !keyword) {
|
|
202
179
|
return null;
|
|
203
180
|
}
|
|
204
|
-
|
|
205
|
-
const
|
|
206
|
-
|
|
207
|
-
? parsedContextLength
|
|
208
|
-
: 35;
|
|
209
|
-
|
|
210
|
-
const lowerText = String(text).toLowerCase();
|
|
211
|
-
const lowerKeyword = String(keyword).toLowerCase();
|
|
212
|
-
const index = lowerText.indexOf(lowerKeyword);
|
|
213
|
-
if (index === -1) {
|
|
181
|
+
const lower = text.toLowerCase();
|
|
182
|
+
const idx = lower.indexOf(keyword.toLowerCase());
|
|
183
|
+
if (idx === -1) {
|
|
214
184
|
return null;
|
|
215
185
|
}
|
|
216
|
-
|
|
217
|
-
const
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
if (
|
|
221
|
-
if (end < String(text).length) context = `${context}...`;
|
|
186
|
+
const start = Math.max(0, idx - contextLength);
|
|
187
|
+
const end = Math.min(text.length, idx + keyword.length + contextLength);
|
|
188
|
+
let context = text.slice(start, end);
|
|
189
|
+
if (start > 0) context = '...' + context;
|
|
190
|
+
if (end < text.length) context = context + '...';
|
|
222
191
|
return context;
|
|
223
192
|
}
|
|
224
193
|
|
|
@@ -227,122 +196,95 @@ function isOpenCodeInstalled() {
|
|
|
227
196
|
return fs.existsSync(OPENCODE_DB_PATH);
|
|
228
197
|
}
|
|
229
198
|
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
return NATIVE_PATHS.opencode.data;
|
|
233
|
-
}
|
|
234
|
-
|
|
235
|
-
// 兼容导出:保留旧路径函数
|
|
236
|
-
function getSessionsDir() {
|
|
237
|
-
return path.join(getOpenCodeDataDir(), 'storage', 'session');
|
|
199
|
+
function invalidateProjectAndSessionCountsCache() {
|
|
200
|
+
countsCache = { expiresAt: 0, value: EMPTY_COUNTS };
|
|
238
201
|
}
|
|
239
202
|
|
|
240
|
-
//
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
}
|
|
203
|
+
// ---------------------------------------------------------------------------
|
|
204
|
+
// Project / session order (file-based)
|
|
205
|
+
// ---------------------------------------------------------------------------
|
|
244
206
|
|
|
245
207
|
function getProjectOrder() {
|
|
246
|
-
|
|
247
|
-
return Array.isArray(order) ? order : [];
|
|
208
|
+
return readJsonSafe(PROJECT_ORDER_FILE, []).order || [];
|
|
248
209
|
}
|
|
249
210
|
|
|
250
211
|
function saveProjectOrder(order) {
|
|
251
|
-
|
|
252
|
-
throw new Error('order must be an array');
|
|
253
|
-
}
|
|
254
|
-
writeJsonSafe(PROJECT_ORDER_FILE, order);
|
|
255
|
-
return { success: true };
|
|
256
|
-
}
|
|
257
|
-
|
|
258
|
-
function getSessionOrderMap() {
|
|
259
|
-
const map = readJsonSafe(SESSION_ORDER_FILE, {});
|
|
260
|
-
return map && typeof map === 'object' ? map : {};
|
|
212
|
+
writeJsonSafe(PROJECT_ORDER_FILE, { order: Array.isArray(order) ? order : [] });
|
|
261
213
|
}
|
|
262
214
|
|
|
263
|
-
function
|
|
264
|
-
|
|
215
|
+
function removeProjectFromOrder(projectId) {
|
|
216
|
+
const current = getProjectOrder();
|
|
217
|
+
saveProjectOrder(current.filter(id => id !== projectId));
|
|
265
218
|
}
|
|
266
219
|
|
|
267
220
|
function getSessionOrder(projectId) {
|
|
268
|
-
const
|
|
269
|
-
|
|
270
|
-
return Array.isArray(order) ? order : [];
|
|
221
|
+
const all = readJsonSafe(SESSION_ORDER_FILE, {}).order || {};
|
|
222
|
+
return all[projectId] || [];
|
|
271
223
|
}
|
|
272
224
|
|
|
273
225
|
function saveSessionOrder(projectId, order) {
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
}
|
|
277
|
-
if (!Array.isArray(order)) {
|
|
278
|
-
throw new Error('order must be an array');
|
|
279
|
-
}
|
|
280
|
-
|
|
281
|
-
const map = getSessionOrderMap();
|
|
282
|
-
map[projectId] = order;
|
|
283
|
-
saveSessionOrderMap(map);
|
|
284
|
-
return { success: true };
|
|
226
|
+
const all = readJsonSafe(SESSION_ORDER_FILE, {}).order || {};
|
|
227
|
+
all[projectId] = Array.isArray(order) ? order : [];
|
|
228
|
+
writeJsonSafe(SESSION_ORDER_FILE, { order: all });
|
|
285
229
|
}
|
|
286
230
|
|
|
287
231
|
function removeSessionFromOrder(projectId, sessionId) {
|
|
288
|
-
const
|
|
289
|
-
|
|
290
|
-
return;
|
|
291
|
-
}
|
|
292
|
-
map[projectId] = map[projectId].filter(id => id !== sessionId);
|
|
293
|
-
saveSessionOrderMap(map);
|
|
232
|
+
const current = getSessionOrder(projectId);
|
|
233
|
+
saveSessionOrder(projectId, current.filter(id => id !== sessionId));
|
|
294
234
|
}
|
|
295
235
|
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
236
|
+
// ---------------------------------------------------------------------------
|
|
237
|
+
// Metadata helpers
|
|
238
|
+
// ---------------------------------------------------------------------------
|
|
299
239
|
|
|
300
|
-
|
|
301
|
-
if (
|
|
302
|
-
|
|
303
|
-
|
|
240
|
+
function getProjectDisplayName(project) {
|
|
241
|
+
if (!project) return 'Unknown';
|
|
242
|
+
const worktree = project.worktree || '';
|
|
243
|
+
if (worktree) {
|
|
244
|
+
const parsed = parseJsonMaybe(project.data);
|
|
245
|
+
if (parsed && typeof parsed.name === 'string' && parsed.name.trim()) {
|
|
246
|
+
return parsed.name.trim();
|
|
247
|
+
}
|
|
248
|
+
return path.basename(worktree);
|
|
304
249
|
}
|
|
250
|
+
return project.id || 'Unknown';
|
|
305
251
|
}
|
|
306
252
|
|
|
307
|
-
function
|
|
308
|
-
|
|
253
|
+
function getSessionLocation(sessionId) {
|
|
254
|
+
const row = getSessionRowById(sessionId);
|
|
255
|
+
if (!row) return null;
|
|
256
|
+
return { projectId: row.project_id, directory: row.directory, sessionData: row };
|
|
309
257
|
}
|
|
310
258
|
|
|
259
|
+
// ---------------------------------------------------------------------------
|
|
260
|
+
// Database query functions (parameterized)
|
|
261
|
+
// ---------------------------------------------------------------------------
|
|
262
|
+
|
|
311
263
|
function queryProjectAndSessionCounts() {
|
|
312
|
-
const rows =
|
|
264
|
+
const rows = _query(`
|
|
313
265
|
SELECT
|
|
314
266
|
(SELECT COUNT(*) FROM project) AS project_count,
|
|
315
267
|
(SELECT COUNT(*) FROM session WHERE time_archived IS NULL) AS session_count
|
|
316
268
|
`);
|
|
317
|
-
|
|
318
|
-
const row = rows[0] || {};
|
|
319
|
-
return {
|
|
320
|
-
projectCount: Number(row.project_count) || 0,
|
|
321
|
-
sessionCount: Number(row.session_count) || 0
|
|
322
|
-
};
|
|
269
|
+
return rows.length > 0 ? rows[0] : { project_count: 0, session_count: 0 };
|
|
323
270
|
}
|
|
324
271
|
|
|
325
272
|
function getProjectRows() {
|
|
326
|
-
return
|
|
273
|
+
return _query(`
|
|
327
274
|
SELECT
|
|
328
275
|
p.id,
|
|
329
276
|
p.worktree,
|
|
330
|
-
p.name,
|
|
331
277
|
p.time_created,
|
|
332
278
|
p.time_updated,
|
|
333
|
-
|
|
279
|
+
p.time_archived,
|
|
280
|
+
p.data
|
|
334
281
|
FROM project p
|
|
335
|
-
|
|
336
|
-
SELECT project_id, COUNT(*) AS session_count
|
|
337
|
-
FROM session
|
|
338
|
-
WHERE time_archived IS NULL
|
|
339
|
-
GROUP BY project_id
|
|
340
|
-
) s ON s.project_id = p.id
|
|
282
|
+
ORDER BY p.time_updated DESC
|
|
341
283
|
`);
|
|
342
284
|
}
|
|
343
285
|
|
|
344
286
|
function getSessionRowsByProjectId(projectId) {
|
|
345
|
-
return
|
|
287
|
+
return _query(`
|
|
346
288
|
SELECT
|
|
347
289
|
s.id,
|
|
348
290
|
s.project_id,
|
|
@@ -361,28 +303,16 @@ function getSessionRowsByProjectId(projectId) {
|
|
|
361
303
|
s.time_created,
|
|
362
304
|
s.time_updated,
|
|
363
305
|
s.time_compacting,
|
|
364
|
-
s.time_archived
|
|
365
|
-
(
|
|
366
|
-
COALESCE((
|
|
367
|
-
SELECT SUM(length(CAST(COALESCE(m.data, '') AS BLOB)))
|
|
368
|
-
FROM message m
|
|
369
|
-
WHERE m.session_id = s.id
|
|
370
|
-
), 0) +
|
|
371
|
-
COALESCE((
|
|
372
|
-
SELECT SUM(length(CAST(COALESCE(p.data, '') AS BLOB)))
|
|
373
|
-
FROM part p
|
|
374
|
-
WHERE p.session_id = s.id
|
|
375
|
-
), 0)
|
|
376
|
-
) AS size
|
|
306
|
+
s.time_archived
|
|
377
307
|
FROM session s
|
|
378
|
-
WHERE s.project_id =
|
|
308
|
+
WHERE s.project_id = ?
|
|
379
309
|
AND s.time_archived IS NULL
|
|
380
310
|
ORDER BY s.time_updated DESC
|
|
381
|
-
|
|
311
|
+
`, projectId);
|
|
382
312
|
}
|
|
383
313
|
|
|
384
314
|
function getSessionRowById(sessionId) {
|
|
385
|
-
const rows =
|
|
315
|
+
const rows = _query(`
|
|
386
316
|
SELECT
|
|
387
317
|
s.id,
|
|
388
318
|
s.project_id,
|
|
@@ -401,29 +331,16 @@ function getSessionRowById(sessionId) {
|
|
|
401
331
|
s.time_created,
|
|
402
332
|
s.time_updated,
|
|
403
333
|
s.time_compacting,
|
|
404
|
-
s.time_archived
|
|
405
|
-
(
|
|
406
|
-
COALESCE((
|
|
407
|
-
SELECT SUM(length(CAST(COALESCE(m.data, '') AS BLOB)))
|
|
408
|
-
FROM message m
|
|
409
|
-
WHERE m.session_id = s.id
|
|
410
|
-
), 0) +
|
|
411
|
-
COALESCE((
|
|
412
|
-
SELECT SUM(length(CAST(COALESCE(p.data, '') AS BLOB)))
|
|
413
|
-
FROM part p
|
|
414
|
-
WHERE p.session_id = s.id
|
|
415
|
-
), 0)
|
|
416
|
-
) AS size
|
|
334
|
+
s.time_archived
|
|
417
335
|
FROM session s
|
|
418
|
-
WHERE s.id =
|
|
336
|
+
WHERE s.id = ?
|
|
419
337
|
LIMIT 1
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
return rows[0] || null;
|
|
338
|
+
`, sessionId);
|
|
339
|
+
return rows.length > 0 ? rows[0] : null;
|
|
423
340
|
}
|
|
424
341
|
|
|
425
342
|
function getMessageRowsBySessionId(sessionId) {
|
|
426
|
-
return
|
|
343
|
+
return _query(`
|
|
427
344
|
SELECT
|
|
428
345
|
id,
|
|
429
346
|
session_id,
|
|
@@ -431,13 +348,13 @@ function getMessageRowsBySessionId(sessionId) {
|
|
|
431
348
|
time_updated,
|
|
432
349
|
data
|
|
433
350
|
FROM message
|
|
434
|
-
WHERE session_id =
|
|
351
|
+
WHERE session_id = ?
|
|
435
352
|
ORDER BY time_created ASC
|
|
436
|
-
|
|
353
|
+
`, sessionId);
|
|
437
354
|
}
|
|
438
355
|
|
|
439
356
|
function getPartRowsBySessionId(sessionId) {
|
|
440
|
-
return
|
|
357
|
+
return _query(`
|
|
441
358
|
SELECT
|
|
442
359
|
id,
|
|
443
360
|
message_id,
|
|
@@ -446,72 +363,49 @@ function getPartRowsBySessionId(sessionId) {
|
|
|
446
363
|
time_updated,
|
|
447
364
|
data
|
|
448
365
|
FROM part
|
|
449
|
-
WHERE session_id =
|
|
366
|
+
WHERE session_id = ?
|
|
450
367
|
ORDER BY time_created ASC
|
|
451
|
-
|
|
368
|
+
`, sessionId);
|
|
452
369
|
}
|
|
453
370
|
|
|
454
|
-
function normalizeSession(session, projectId
|
|
455
|
-
const size = Number(session?.size);
|
|
371
|
+
function normalizeSession(session, projectId) {
|
|
456
372
|
return {
|
|
457
373
|
sessionId: session.id,
|
|
458
|
-
projectName: projectId || session.project_id,
|
|
459
374
|
mtime: toIsoTime(session.time_updated) || new Date().toISOString(),
|
|
460
|
-
size:
|
|
461
|
-
filePath:
|
|
375
|
+
size: 0, // SQLite doesn't track file size natively
|
|
376
|
+
filePath: `opencode://${projectId}/${session.id}`,
|
|
462
377
|
gitBranch: null,
|
|
463
|
-
firstMessage: session.title ||
|
|
464
|
-
forkedFrom: null,
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
378
|
+
firstMessage: session.title || null,
|
|
379
|
+
forkedFrom: session.parent_id || null,
|
|
380
|
+
source: 'opencode',
|
|
381
|
+
directory: session.directory || null,
|
|
382
|
+
slug: session.slug || null,
|
|
383
|
+
model: null,
|
|
384
|
+
provider: null,
|
|
385
|
+
projectName: projectId,
|
|
386
|
+
projectFullPath: null
|
|
468
387
|
};
|
|
469
388
|
}
|
|
470
389
|
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
function getProjectDisplayName(project) {
|
|
476
|
-
const rawName = typeof project?.name === 'string' ? project.name.trim() : '';
|
|
477
|
-
const rawId = typeof project?.id === 'string' ? project.id.trim() : '';
|
|
478
|
-
const worktree = typeof project?.worktree === 'string' ? project.worktree.trim() : '';
|
|
479
|
-
|
|
480
|
-
// OpenCode 的 project.name 可能为空或异常写成会话ID,优先回退为目录名。
|
|
481
|
-
if (rawName && rawName !== rawId && !isSessionLikeName(rawName)) {
|
|
482
|
-
return rawName;
|
|
483
|
-
}
|
|
484
|
-
|
|
485
|
-
if (worktree) {
|
|
486
|
-
const dirName = path.basename(worktree);
|
|
487
|
-
if (dirName && dirName !== path.sep && dirName !== '.' && dirName !== '..') {
|
|
488
|
-
return dirName;
|
|
489
|
-
}
|
|
490
|
-
}
|
|
390
|
+
// ---------------------------------------------------------------------------
|
|
391
|
+
// Public API
|
|
392
|
+
// ---------------------------------------------------------------------------
|
|
491
393
|
|
|
492
|
-
return rawName || rawId;
|
|
493
|
-
}
|
|
494
|
-
|
|
495
|
-
// 获取所有项目
|
|
496
394
|
function getProjects(_options = {}) {
|
|
497
395
|
const projects = getProjectRows().map((project) => ({
|
|
498
396
|
name: project.id,
|
|
499
397
|
displayName: getProjectDisplayName(project),
|
|
500
398
|
fullPath: project.worktree || '/',
|
|
501
399
|
path: project.worktree || '/',
|
|
502
|
-
sessionCount:
|
|
503
|
-
lastUsed:
|
|
400
|
+
sessionCount: 0,
|
|
401
|
+
lastUsed: toIsoTime(project.time_updated),
|
|
504
402
|
source: 'opencode'
|
|
505
403
|
}));
|
|
506
404
|
|
|
507
|
-
|
|
508
|
-
|
|
509
|
-
getProjectOrder(),
|
|
510
|
-
(a, b) => (b.lastUsed || 0) - (a.lastUsed || 0)
|
|
511
|
-
);
|
|
405
|
+
const order = getProjectOrder();
|
|
406
|
+
return sortByOrder(projects, order, (a, b) => (b.lastUsed || '').localeCompare(a.lastUsed || ''));
|
|
512
407
|
}
|
|
513
408
|
|
|
514
|
-
// 根据项目ID获取会话列表
|
|
515
409
|
function getSessionsByProjectId(projectId, _options = {}) {
|
|
516
410
|
const sessions = getSessionRowsByProjectId(projectId).map(session => normalizeSession(session, projectId));
|
|
517
411
|
const order = getSessionOrder(projectId);
|
|
@@ -520,156 +414,86 @@ function getSessionsByProjectId(projectId, _options = {}) {
|
|
|
520
414
|
(a, b) => new Date(b.mtime).getTime() - new Date(a.mtime).getTime()
|
|
521
415
|
);
|
|
522
416
|
|
|
523
|
-
|
|
524
|
-
|
|
525
|
-
|
|
526
|
-
|
|
527
|
-
const orderMap = new Map(order.map((id, idx) => [id, idx]));
|
|
528
|
-
return [...fallbackSorted].sort((a, b) => {
|
|
529
|
-
const aIndex = orderMap.has(a.sessionId) ? orderMap.get(a.sessionId) : Number.MAX_SAFE_INTEGER;
|
|
530
|
-
const bIndex = orderMap.has(b.sessionId) ? orderMap.get(b.sessionId) : Number.MAX_SAFE_INTEGER;
|
|
531
|
-
if (aIndex === bIndex) {
|
|
532
|
-
return new Date(b.mtime).getTime() - new Date(a.mtime).getTime();
|
|
533
|
-
}
|
|
534
|
-
return aIndex - bIndex;
|
|
535
|
-
});
|
|
536
|
-
}
|
|
537
|
-
|
|
538
|
-
// 根据项目名获取会话列表
|
|
539
|
-
function getSessionsByProject(projectName, options = {}) {
|
|
540
|
-
return getSessionsByProjectId(projectName, options);
|
|
541
|
-
}
|
|
542
|
-
|
|
543
|
-
function getSessionLocation(sessionId) {
|
|
544
|
-
const session = getSessionRowById(sessionId);
|
|
545
|
-
if (!session) {
|
|
546
|
-
return null;
|
|
547
|
-
}
|
|
548
|
-
return {
|
|
549
|
-
projectId: session.project_id,
|
|
550
|
-
sessionData: session
|
|
551
|
-
};
|
|
552
|
-
}
|
|
553
|
-
|
|
554
|
-
// 根据会话ID获取会话详情
|
|
555
|
-
function getSessionById(sessionId) {
|
|
556
|
-
const location = getSessionLocation(sessionId);
|
|
557
|
-
if (!location) {
|
|
558
|
-
return null;
|
|
559
|
-
}
|
|
560
|
-
|
|
561
|
-
return normalizeSession(location.sessionData, location.projectId);
|
|
417
|
+
return sortByOrder(fallbackSorted, order, (a, b) =>
|
|
418
|
+
new Date(b.mtime).getTime() - new Date(a.mtime).getTime()
|
|
419
|
+
);
|
|
562
420
|
}
|
|
563
421
|
|
|
564
|
-
function
|
|
565
|
-
|
|
566
|
-
const
|
|
422
|
+
function searchSessions(keyword) {
|
|
423
|
+
if (!keyword || !keyword.trim()) return [];
|
|
424
|
+
const lowerKeyword = keyword.toLowerCase();
|
|
567
425
|
|
|
568
|
-
const
|
|
569
|
-
|
|
570
|
-
|
|
571
|
-
|
|
572
|
-
|
|
573
|
-
|
|
574
|
-
|
|
426
|
+
const allSessions = [];
|
|
427
|
+
const projects = getProjectRows();
|
|
428
|
+
for (const project of projects) {
|
|
429
|
+
const sessions = getSessionRowsByProjectId(project.id);
|
|
430
|
+
for (const session of sessions) {
|
|
431
|
+
const messages = getMessageRowsBySessionId(session.id);
|
|
432
|
+
const parts = getPartRowsBySessionId(session.id);
|
|
433
|
+
|
|
434
|
+
const matchedMessages = [];
|
|
435
|
+
const partsByMessageId = new Map();
|
|
436
|
+
for (const part of parts) {
|
|
437
|
+
const existing = partsByMessageId.get(part.message_id) || [];
|
|
438
|
+
existing.push(part);
|
|
439
|
+
partsByMessageId.set(part.message_id, existing);
|
|
440
|
+
}
|
|
575
441
|
|
|
576
|
-
|
|
442
|
+
for (let i = 0; i < messages.length; i++) {
|
|
443
|
+
const message = messages[i];
|
|
444
|
+
const data = parseJsonMaybe(message.data, null);
|
|
445
|
+
let text = '';
|
|
446
|
+
|
|
447
|
+
if (data && data.role === 'user') {
|
|
448
|
+
text = extractTextFromMessageData(data);
|
|
449
|
+
} else {
|
|
450
|
+
const messageParts = partsByMessageId.get(message.id) || [];
|
|
451
|
+
for (const part of messageParts) {
|
|
452
|
+
const partData = parseJsonMaybe(part.data, null);
|
|
453
|
+
text += (text ? '\n' : '') + extractTextFromPartData(partData);
|
|
454
|
+
}
|
|
455
|
+
}
|
|
577
456
|
|
|
578
|
-
|
|
579
|
-
|
|
580
|
-
|
|
581
|
-
|
|
582
|
-
|
|
583
|
-
|
|
457
|
+
if (text.toLowerCase().includes(lowerKeyword)) {
|
|
458
|
+
matchedMessages.push({
|
|
459
|
+
messageIndex: i,
|
|
460
|
+
role: data ? (data.role || 'unknown') : 'unknown',
|
|
461
|
+
context: buildContext(text, keyword),
|
|
462
|
+
timestamp: toIsoTime(message.time_created)
|
|
463
|
+
});
|
|
464
|
+
}
|
|
465
|
+
}
|
|
584
466
|
|
|
585
|
-
|
|
586
|
-
|
|
587
|
-
|
|
588
|
-
|
|
589
|
-
|
|
590
|
-
|
|
591
|
-
|
|
467
|
+
if (matchedMessages.length > 0) {
|
|
468
|
+
allSessions.push({
|
|
469
|
+
sessionId: session.id,
|
|
470
|
+
projectName: project.id,
|
|
471
|
+
projectDisplayName: getProjectDisplayName(project),
|
|
472
|
+
firstMessage: session.title || null,
|
|
473
|
+
matches: matchedMessages,
|
|
474
|
+
matchCount: matchedMessages.length,
|
|
475
|
+
source: 'opencode'
|
|
476
|
+
});
|
|
592
477
|
}
|
|
593
478
|
}
|
|
594
|
-
|
|
595
|
-
const fallbackText = extractTextFromMessageData(messageData);
|
|
596
|
-
const content = partTexts.join('\n').trim() || fallbackText || '[空消息]';
|
|
597
|
-
|
|
598
|
-
const timestamp = toIsoTime(
|
|
599
|
-
messageData?.time?.created || row.time_created || row.time_updated
|
|
600
|
-
);
|
|
601
|
-
|
|
602
|
-
converted.push({
|
|
603
|
-
type: role,
|
|
604
|
-
role,
|
|
605
|
-
content,
|
|
606
|
-
timestamp,
|
|
607
|
-
model: role === 'assistant'
|
|
608
|
-
? (messageData?.model?.modelID || messageData?.modelID || messageData?.model || 'opencode')
|
|
609
|
-
: null
|
|
610
|
-
});
|
|
611
|
-
}
|
|
612
|
-
|
|
613
|
-
return converted;
|
|
614
|
-
}
|
|
615
|
-
|
|
616
|
-
function getSessionMessages(sessionId) {
|
|
617
|
-
const location = getSessionLocation(sessionId);
|
|
618
|
-
if (!location) {
|
|
619
|
-
throw new Error('Session not found');
|
|
620
|
-
}
|
|
621
|
-
|
|
622
|
-
return buildSessionMessages(sessionId).map(({ type, content, timestamp, model }) => ({
|
|
623
|
-
type,
|
|
624
|
-
content,
|
|
625
|
-
timestamp,
|
|
626
|
-
model
|
|
627
|
-
}));
|
|
628
|
-
}
|
|
629
|
-
|
|
630
|
-
// 获取项目和会话数量统计
|
|
631
|
-
function getProjectAndSessionCounts(options = {}) {
|
|
632
|
-
const now = Date.now();
|
|
633
|
-
if (options.force) {
|
|
634
|
-
countsCache.expiresAt = 0;
|
|
635
|
-
}
|
|
636
|
-
if (!options.force && countsCache.expiresAt > now) {
|
|
637
|
-
return countsCache.value;
|
|
638
479
|
}
|
|
639
480
|
|
|
640
|
-
|
|
641
|
-
const counts = queryProjectAndSessionCounts();
|
|
642
|
-
countsCache = {
|
|
643
|
-
value: counts,
|
|
644
|
-
expiresAt: now + COUNTS_CACHE_TTL_MS
|
|
645
|
-
};
|
|
646
|
-
return counts;
|
|
647
|
-
} catch (err) {
|
|
648
|
-
console.error('[OpenCode Sessions] Failed to get counts:', err);
|
|
649
|
-
return countsCache.value || EMPTY_COUNTS;
|
|
650
|
-
}
|
|
481
|
+
return allSessions.sort((a, b) => b.matchCount - a.matchCount);
|
|
651
482
|
}
|
|
652
483
|
|
|
653
484
|
function getRecentSessions(limit = 5) {
|
|
654
|
-
const projects = getProjects();
|
|
655
|
-
const { loadAliases } = require('./alias');
|
|
656
|
-
const { getForkRelations } = require('./sessions');
|
|
657
|
-
const aliases = loadAliases();
|
|
658
|
-
const forkRelations = getForkRelations();
|
|
659
485
|
const allSessions = [];
|
|
660
486
|
|
|
487
|
+
const projects = getProjectRows();
|
|
661
488
|
for (const project of projects) {
|
|
662
|
-
const sessions =
|
|
663
|
-
|
|
489
|
+
const sessions = getSessionRowsByProjectId(project.id);
|
|
490
|
+
for (const session of sessions) {
|
|
664
491
|
allSessions.push({
|
|
665
|
-
...session,
|
|
666
|
-
|
|
667
|
-
|
|
668
|
-
projectName: project.name,
|
|
669
|
-
projectDisplayName: project.displayName,
|
|
670
|
-
projectFullPath: project.fullPath
|
|
492
|
+
...normalizeSession(session, project.id),
|
|
493
|
+
projectDisplayName: getProjectDisplayName(project),
|
|
494
|
+
projectFullPath: project.worktree || null
|
|
671
495
|
});
|
|
672
|
-
}
|
|
496
|
+
}
|
|
673
497
|
}
|
|
674
498
|
|
|
675
499
|
return allSessions
|
|
@@ -677,6 +501,66 @@ function getRecentSessions(limit = 5) {
|
|
|
677
501
|
.slice(0, limit);
|
|
678
502
|
}
|
|
679
503
|
|
|
504
|
+
function getSessionById(sessionId) {
|
|
505
|
+
const location = getSessionLocation(sessionId);
|
|
506
|
+
if (!location) return null;
|
|
507
|
+
|
|
508
|
+
const messages = buildSessionMessages(sessionId);
|
|
509
|
+
return {
|
|
510
|
+
sessionId,
|
|
511
|
+
mtime: toIsoTime(location.sessionData.time_updated) || new Date().toISOString(),
|
|
512
|
+
size: 0,
|
|
513
|
+
filePath: `opencode://${location.projectId}/${sessionId}`,
|
|
514
|
+
source: 'opencode',
|
|
515
|
+
directory: location.directory || null,
|
|
516
|
+
projectName: location.projectId,
|
|
517
|
+
messages
|
|
518
|
+
};
|
|
519
|
+
}
|
|
520
|
+
|
|
521
|
+
function buildSessionMessages(sessionId) {
|
|
522
|
+
const messages = getMessageRowsBySessionId(sessionId);
|
|
523
|
+
const parts = getPartRowsBySessionId(sessionId);
|
|
524
|
+
|
|
525
|
+
const partsByMessageId = new Map();
|
|
526
|
+
for (const part of parts) {
|
|
527
|
+
const existing = partsByMessageId.get(part.message_id) || [];
|
|
528
|
+
existing.push(part);
|
|
529
|
+
partsByMessageId.set(part.message_id, existing);
|
|
530
|
+
}
|
|
531
|
+
|
|
532
|
+
return messages.map((message) => {
|
|
533
|
+
const data = parseJsonMaybe(message.data, null);
|
|
534
|
+
const messageParts = partsByMessageId.get(message.id) || [];
|
|
535
|
+
|
|
536
|
+
let content = '';
|
|
537
|
+
if (data && data.role === 'user') {
|
|
538
|
+
content = extractTextFromMessageData(data);
|
|
539
|
+
} else if (messageParts.length > 0) {
|
|
540
|
+
content = messageParts
|
|
541
|
+
.map(part => {
|
|
542
|
+
const partData = parseJsonMaybe(part.data, null);
|
|
543
|
+
return extractTextFromPartData(partData);
|
|
544
|
+
})
|
|
545
|
+
.filter(Boolean)
|
|
546
|
+
.join('\n');
|
|
547
|
+
}
|
|
548
|
+
|
|
549
|
+
return {
|
|
550
|
+
id: message.id,
|
|
551
|
+
role: data ? (data.role || 'unknown') : 'unknown',
|
|
552
|
+
type: data ? (data.role || 'unknown') : 'unknown',
|
|
553
|
+
content,
|
|
554
|
+
timestamp: toIsoTime(message.time_created),
|
|
555
|
+
model: data ? (data.model || null) : null,
|
|
556
|
+
parts: messageParts.map(part => ({
|
|
557
|
+
id: part.id,
|
|
558
|
+
data: parseJsonMaybe(part.data, null)
|
|
559
|
+
}))
|
|
560
|
+
};
|
|
561
|
+
});
|
|
562
|
+
}
|
|
563
|
+
|
|
680
564
|
// 删除会话
|
|
681
565
|
function deleteSession(sessionId) {
|
|
682
566
|
const location = getSessionLocation(sessionId);
|
|
@@ -684,10 +568,10 @@ function deleteSession(sessionId) {
|
|
|
684
568
|
throw new Error('Session not found');
|
|
685
569
|
}
|
|
686
570
|
|
|
687
|
-
|
|
688
|
-
|
|
689
|
-
|
|
690
|
-
|
|
571
|
+
_exec(
|
|
572
|
+
`DELETE FROM session WHERE id = ?`,
|
|
573
|
+
sessionId
|
|
574
|
+
);
|
|
691
575
|
|
|
692
576
|
try {
|
|
693
577
|
const { deleteAlias } = require('./alias');
|
|
@@ -727,93 +611,93 @@ function forkSession(sessionId) {
|
|
|
727
611
|
const parts = getPartRowsBySessionId(sessionId);
|
|
728
612
|
const now = Date.now();
|
|
729
613
|
const newSessionId = `ses_${crypto.randomUUID().replace(/-/g, '')}`;
|
|
614
|
+
const db = _openCodeDb();
|
|
615
|
+
if (!db) throw new Error('OpenCode CLI not installed');
|
|
730
616
|
|
|
731
617
|
const messageIdMap = new Map();
|
|
732
618
|
for (const message of messages) {
|
|
733
619
|
messageIdMap.set(message.id, `msg_${crypto.randomUUID().replace(/-/g, '')}`);
|
|
734
620
|
}
|
|
735
621
|
|
|
736
|
-
|
|
737
|
-
statements.push('PRAGMA foreign_keys = ON;');
|
|
738
|
-
statements.push('BEGIN IMMEDIATE;');
|
|
739
|
-
|
|
740
|
-
statements.push(`
|
|
741
|
-
INSERT INTO session (
|
|
742
|
-
id, project_id, parent_id, slug, directory, title, version, share_url,
|
|
743
|
-
summary_additions, summary_deletions, summary_files, summary_diffs,
|
|
744
|
-
revert, permission, time_created, time_updated, time_compacting, time_archived
|
|
745
|
-
) VALUES (
|
|
746
|
-
${sqlQuote(newSessionId)},
|
|
747
|
-
${sqlQuote(source.project_id)},
|
|
748
|
-
${sqlQuote(source.parent_id)},
|
|
749
|
-
${sqlQuote(source.slug)},
|
|
750
|
-
${sqlQuote(source.directory)},
|
|
751
|
-
${sqlQuote(source.title)},
|
|
752
|
-
${sqlQuote(source.version)},
|
|
753
|
-
${sqlQuote(source.share_url)},
|
|
754
|
-
${sqlQuote(source.summary_additions)},
|
|
755
|
-
${sqlQuote(source.summary_deletions)},
|
|
756
|
-
${sqlQuote(source.summary_files)},
|
|
757
|
-
${sqlQuote(source.summary_diffs)},
|
|
758
|
-
${sqlQuote(source.revert)},
|
|
759
|
-
${sqlQuote(source.permission)},
|
|
760
|
-
${sqlQuote(now)},
|
|
761
|
-
${sqlQuote(now)},
|
|
762
|
-
${sqlQuote(source.time_compacting)},
|
|
763
|
-
NULL
|
|
764
|
-
);
|
|
765
|
-
`);
|
|
622
|
+
db.exec('BEGIN IMMEDIATE');
|
|
766
623
|
|
|
767
|
-
|
|
768
|
-
|
|
769
|
-
|
|
624
|
+
try {
|
|
625
|
+
db.prepare(`
|
|
626
|
+
INSERT INTO session (
|
|
627
|
+
id, project_id, parent_id, slug, directory, title, version, share_url,
|
|
628
|
+
summary_additions, summary_deletions, summary_files, summary_diffs,
|
|
629
|
+
revert, permission, time_created, time_updated, time_compacting, time_archived
|
|
630
|
+
) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, NULL)
|
|
631
|
+
`).run(
|
|
632
|
+
newSessionId,
|
|
633
|
+
source.project_id,
|
|
634
|
+
source.parent_id,
|
|
635
|
+
source.slug,
|
|
636
|
+
source.directory,
|
|
637
|
+
source.title,
|
|
638
|
+
source.version,
|
|
639
|
+
source.share_url,
|
|
640
|
+
source.summary_additions,
|
|
641
|
+
source.summary_deletions,
|
|
642
|
+
source.summary_files,
|
|
643
|
+
source.summary_diffs,
|
|
644
|
+
source.revert,
|
|
645
|
+
source.permission,
|
|
646
|
+
now,
|
|
647
|
+
now,
|
|
648
|
+
source.time_compacting
|
|
649
|
+
);
|
|
770
650
|
|
|
771
|
-
|
|
772
|
-
|
|
773
|
-
|
|
774
|
-
|
|
775
|
-
|
|
776
|
-
|
|
777
|
-
|
|
651
|
+
const insertMessageStmt = db.prepare(
|
|
652
|
+
`INSERT INTO message (id, session_id, time_created, time_updated, data) VALUES (?, ?, ?, ?, ?)`
|
|
653
|
+
);
|
|
654
|
+
for (const message of messages) {
|
|
655
|
+
const newMessageId = messageIdMap.get(message.id);
|
|
656
|
+
const messageData = parseJsonMaybe(message.data, null);
|
|
657
|
+
|
|
658
|
+
let serializedData = message.data;
|
|
659
|
+
if (messageData && typeof messageData === 'object') {
|
|
660
|
+
if (typeof messageData.parentID === 'string' && messageIdMap.has(messageData.parentID)) {
|
|
661
|
+
messageData.parentID = messageIdMap.get(messageData.parentID);
|
|
662
|
+
}
|
|
663
|
+
if (typeof messageData.id === 'string') {
|
|
664
|
+
messageData.id = newMessageId;
|
|
665
|
+
}
|
|
666
|
+
serializedData = JSON.stringify(messageData);
|
|
778
667
|
}
|
|
779
|
-
serializedData = JSON.stringify(messageData);
|
|
780
|
-
}
|
|
781
668
|
|
|
782
|
-
|
|
783
|
-
|
|
784
|
-
|
|
785
|
-
|
|
786
|
-
|
|
787
|
-
|
|
788
|
-
${sqlQuote(message.time_updated)},
|
|
789
|
-
${sqlQuote(serializedData)}
|
|
669
|
+
insertMessageStmt.run(
|
|
670
|
+
newMessageId,
|
|
671
|
+
newSessionId,
|
|
672
|
+
message.time_created,
|
|
673
|
+
message.time_updated,
|
|
674
|
+
serializedData
|
|
790
675
|
);
|
|
791
|
-
`);
|
|
792
|
-
}
|
|
793
|
-
|
|
794
|
-
for (const part of parts) {
|
|
795
|
-
const newPartId = `prt_${crypto.randomUUID().replace(/-/g, '')}`;
|
|
796
|
-
const targetMessageId = messageIdMap.get(part.message_id);
|
|
797
|
-
if (!targetMessageId) {
|
|
798
|
-
continue;
|
|
799
676
|
}
|
|
800
677
|
|
|
801
|
-
|
|
802
|
-
INSERT INTO part (id, message_id, session_id, time_created, time_updated, data)
|
|
803
|
-
|
|
804
|
-
|
|
805
|
-
|
|
806
|
-
|
|
807
|
-
|
|
808
|
-
|
|
809
|
-
|
|
678
|
+
const insertPartStmt = db.prepare(
|
|
679
|
+
`INSERT INTO part (id, message_id, session_id, time_created, time_updated, data) VALUES (?, ?, ?, ?, ?, ?)`
|
|
680
|
+
);
|
|
681
|
+
for (const part of parts) {
|
|
682
|
+
const newPartId = `prt_${crypto.randomUUID().replace(/-/g, '')}`;
|
|
683
|
+
const targetMessageId = messageIdMap.get(part.message_id);
|
|
684
|
+
if (!targetMessageId) continue;
|
|
685
|
+
|
|
686
|
+
insertPartStmt.run(
|
|
687
|
+
newPartId,
|
|
688
|
+
targetMessageId,
|
|
689
|
+
newSessionId,
|
|
690
|
+
part.time_created,
|
|
691
|
+
part.time_updated,
|
|
692
|
+
part.data
|
|
810
693
|
);
|
|
811
|
-
|
|
812
|
-
}
|
|
813
|
-
|
|
814
|
-
statements.push('COMMIT;');
|
|
694
|
+
}
|
|
815
695
|
|
|
816
|
-
|
|
696
|
+
db.exec('COMMIT');
|
|
697
|
+
} catch (err) {
|
|
698
|
+
db.exec('ROLLBACK');
|
|
699
|
+
throw new Error('Failed to fork session: ' + err.message);
|
|
700
|
+
}
|
|
817
701
|
|
|
818
702
|
try {
|
|
819
703
|
const { getForkRelations, saveForkRelations } = require('./sessions');
|
|
@@ -821,165 +705,74 @@ function forkSession(sessionId) {
|
|
|
821
705
|
relations[newSessionId] = sessionId;
|
|
822
706
|
saveForkRelations(relations);
|
|
823
707
|
} catch (err) {
|
|
824
|
-
// ignore
|
|
708
|
+
// ignore
|
|
825
709
|
}
|
|
826
710
|
|
|
827
|
-
const existingOrder = getSessionOrder(location.projectId);
|
|
828
|
-
saveSessionOrder(location.projectId, [newSessionId, ...existingOrder.filter(id => id !== newSessionId)]);
|
|
829
|
-
|
|
830
711
|
invalidateProjectAndSessionCountsCache();
|
|
831
|
-
return {
|
|
832
|
-
success: true,
|
|
833
|
-
newSessionId,
|
|
834
|
-
forkedFrom: sessionId,
|
|
835
|
-
projectName: location.projectId,
|
|
836
|
-
newFilePath: null
|
|
837
|
-
};
|
|
712
|
+
return { success: true, sessionId: newSessionId, forkedFrom: sessionId };
|
|
838
713
|
}
|
|
839
714
|
|
|
840
715
|
function deleteProject(projectId) {
|
|
841
|
-
const projectRows =
|
|
842
|
-
SELECT id FROM project WHERE id =
|
|
843
|
-
|
|
716
|
+
const projectRows = _query(
|
|
717
|
+
`SELECT id FROM project WHERE id = ? LIMIT 1`,
|
|
718
|
+
projectId
|
|
719
|
+
);
|
|
844
720
|
|
|
845
721
|
if (projectRows.length === 0) {
|
|
846
722
|
throw new Error('Project not found');
|
|
847
723
|
}
|
|
848
724
|
|
|
849
|
-
const sessionRows =
|
|
850
|
-
SELECT id FROM session WHERE project_id =
|
|
851
|
-
|
|
725
|
+
const sessionRows = _query(
|
|
726
|
+
`SELECT id FROM session WHERE project_id = ?`,
|
|
727
|
+
projectId
|
|
728
|
+
);
|
|
852
729
|
|
|
853
730
|
const deletedSessionIds = sessionRows.map(row => row.id);
|
|
854
731
|
|
|
855
|
-
for (const sessionId of deletedSessionIds) {
|
|
856
|
-
try {
|
|
857
|
-
const { deleteAlias } = require('./alias');
|
|
858
|
-
deleteAlias(sessionId);
|
|
859
|
-
} catch (err) {
|
|
860
|
-
// ignore alias cleanup errors
|
|
861
|
-
}
|
|
862
|
-
}
|
|
863
|
-
|
|
864
|
-
runSqliteExec(`
|
|
865
|
-
PRAGMA foreign_keys = ON;
|
|
866
|
-
DELETE FROM project WHERE id = ${sqlQuote(projectId)};
|
|
867
|
-
`);
|
|
868
|
-
|
|
869
|
-
removeProjectFromOrder(projectId);
|
|
870
|
-
|
|
871
732
|
try {
|
|
872
|
-
const {
|
|
873
|
-
|
|
874
|
-
|
|
875
|
-
Object.keys(relations).forEach((key) => {
|
|
876
|
-
if (deletedSet.has(key) || deletedSet.has(relations[key])) {
|
|
877
|
-
delete relations[key];
|
|
878
|
-
}
|
|
733
|
+
const { deleteAlias } = require('./alias');
|
|
734
|
+
deletedSessionIds.forEach(id => {
|
|
735
|
+
try { deleteAlias(id); } catch (_) {}
|
|
879
736
|
});
|
|
880
|
-
|
|
881
|
-
|
|
882
|
-
|
|
883
|
-
}
|
|
737
|
+
} catch (_) {}
|
|
738
|
+
|
|
739
|
+
_exec(`DELETE FROM project WHERE id = ?`, projectId);
|
|
884
740
|
|
|
741
|
+
removeProjectFromOrder(projectId);
|
|
885
742
|
invalidateProjectAndSessionCountsCache();
|
|
886
|
-
return {
|
|
887
|
-
success: true,
|
|
888
|
-
projectName: projectId,
|
|
889
|
-
deletedCount: deletedSessionIds.length
|
|
890
|
-
};
|
|
743
|
+
return { success: true, projectId, deletedSessions: deletedSessionIds.length };
|
|
891
744
|
}
|
|
892
745
|
|
|
893
|
-
|
|
894
|
-
|
|
895
|
-
|
|
896
|
-
|
|
746
|
+
function getProjectAndSessionCounts(options = {}) {
|
|
747
|
+
if (options.force) {
|
|
748
|
+
invalidateProjectAndSessionCountsCache();
|
|
749
|
+
} else if (countsCache.expiresAt > Date.now()) {
|
|
750
|
+
return countsCache.value;
|
|
897
751
|
}
|
|
898
752
|
|
|
899
|
-
const
|
|
900
|
-
const
|
|
901
|
-
|
|
902
|
-
|
|
903
|
-
|
|
904
|
-
|
|
905
|
-
for (const project of projects) {
|
|
906
|
-
if (projectFilter && project.name !== projectFilter) {
|
|
907
|
-
continue;
|
|
908
|
-
}
|
|
909
|
-
|
|
910
|
-
const sessions = getSessionsByProjectId(project.name);
|
|
911
|
-
for (const session of sessions) {
|
|
912
|
-
const matches = [];
|
|
913
|
-
|
|
914
|
-
const quickChecks = [
|
|
915
|
-
session.sessionId,
|
|
916
|
-
session.firstMessage,
|
|
917
|
-
session.slug,
|
|
918
|
-
session.directory
|
|
919
|
-
];
|
|
920
|
-
|
|
921
|
-
for (const text of quickChecks) {
|
|
922
|
-
const context = buildContext(text, searchKeyword, contextLength);
|
|
923
|
-
if (context) {
|
|
924
|
-
matches.push({
|
|
925
|
-
role: 'assistant',
|
|
926
|
-
context,
|
|
927
|
-
timestamp: session.mtime
|
|
928
|
-
});
|
|
929
|
-
}
|
|
930
|
-
}
|
|
931
|
-
|
|
932
|
-
const sessionMessages = buildSessionMessages(session.sessionId);
|
|
933
|
-
for (const message of sessionMessages) {
|
|
934
|
-
const context = buildContext(message.content, searchKeyword, contextLength);
|
|
935
|
-
if (!context) {
|
|
936
|
-
continue;
|
|
937
|
-
}
|
|
938
|
-
|
|
939
|
-
matches.push({
|
|
940
|
-
role: message.role,
|
|
941
|
-
context,
|
|
942
|
-
timestamp: message.timestamp
|
|
943
|
-
});
|
|
944
|
-
}
|
|
945
|
-
|
|
946
|
-
if (matches.length > 0) {
|
|
947
|
-
results.push({
|
|
948
|
-
sessionId: session.sessionId,
|
|
949
|
-
projectName: project.name,
|
|
950
|
-
projectDisplayName: project.displayName,
|
|
951
|
-
projectFullPath: project.fullPath,
|
|
952
|
-
alias: aliases[session.sessionId] || null,
|
|
953
|
-
matchCount: matches.length,
|
|
954
|
-
matches: matches.slice(0, 5),
|
|
955
|
-
source: 'opencode'
|
|
956
|
-
});
|
|
957
|
-
}
|
|
958
|
-
}
|
|
959
|
-
}
|
|
753
|
+
const row = queryProjectAndSessionCounts();
|
|
754
|
+
const result = {
|
|
755
|
+
projectCount: Number(row.project_count) || 0,
|
|
756
|
+
sessionCount: Number(row.session_count) || 0
|
|
757
|
+
};
|
|
960
758
|
|
|
961
|
-
|
|
759
|
+
countsCache = { expiresAt: Date.now() + COUNTS_CACHE_TTL_MS, value: result };
|
|
760
|
+
return result;
|
|
962
761
|
}
|
|
963
762
|
|
|
964
763
|
module.exports = {
|
|
965
764
|
isOpenCodeInstalled,
|
|
966
|
-
getOpenCodeDataDir,
|
|
967
|
-
getSessionsDir,
|
|
968
|
-
getProjectsDir,
|
|
969
765
|
getProjects,
|
|
970
|
-
getProjectOrder,
|
|
971
|
-
saveProjectOrder,
|
|
972
|
-
getSessionsByProject,
|
|
973
766
|
getSessionsByProjectId,
|
|
974
767
|
getSessionById,
|
|
975
|
-
|
|
768
|
+
searchSessions,
|
|
976
769
|
getRecentSessions,
|
|
977
|
-
normalizeSession,
|
|
978
|
-
getProjectAndSessionCounts,
|
|
979
770
|
deleteSession,
|
|
980
|
-
deleteProject,
|
|
981
771
|
forkSession,
|
|
982
|
-
|
|
772
|
+
deleteProject,
|
|
773
|
+
saveProjectOrder,
|
|
983
774
|
saveSessionOrder,
|
|
984
|
-
|
|
775
|
+
getProjectAndSessionCounts,
|
|
776
|
+
_query, // exposed for testing
|
|
777
|
+
_openCodeDb // exposed for testing
|
|
985
778
|
};
|