ocwatch 0.4.0 → 0.6.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.
Files changed (41) hide show
  1. package/README.md +22 -3
  2. package/package.json +4 -4
  3. package/src/client/dist/assets/GraphView-BZV40eAE.css +1 -0
  4. package/src/client/dist/assets/GraphView-KWCCGYb2.js +9 -0
  5. package/src/client/dist/assets/graph-Cw_XSlvx.js +7 -0
  6. package/src/client/dist/assets/index-CbgYG3pJ.js +23 -0
  7. package/src/client/dist/assets/index-CgDCc8Mm.css +1 -0
  8. package/src/client/dist/assets/motion-CGUGF2CN.js +9 -0
  9. package/src/client/dist/index.html +4 -2
  10. package/src/server/__tests__/helpers/testDb.ts +220 -0
  11. package/src/server/index.ts +27 -27
  12. package/src/server/logic/activityLogic.ts +260 -0
  13. package/src/server/logic/index.ts +2 -0
  14. package/src/server/logic/sessionLogic.ts +107 -0
  15. package/src/server/routes/parts.ts +9 -7
  16. package/src/server/routes/poll.ts +32 -46
  17. package/src/server/routes/projects.ts +10 -27
  18. package/src/server/routes/sessions.ts +159 -68
  19. package/src/server/routes/sse.ts +10 -4
  20. package/src/server/services/parsing.ts +211 -0
  21. package/src/server/services/pollService.ts +400 -116
  22. package/src/server/services/recentSessions.ts +14 -0
  23. package/src/server/services/sessionContext.ts +97 -0
  24. package/src/server/services/sessionService.ts +97 -193
  25. package/src/server/services/sessionTree.ts +92 -0
  26. package/src/server/storage/db.ts +63 -0
  27. package/src/server/storage/index.ts +28 -0
  28. package/src/server/storage/queries.ts +528 -0
  29. package/src/server/utils/projectResolver.ts +9 -3
  30. package/src/server/utils/sessionStatus.ts +5 -89
  31. package/src/server/validation.ts +2 -4
  32. package/src/server/watcher.ts +225 -82
  33. package/src/shared/constants.ts +8 -3
  34. package/src/shared/index.ts +3 -0
  35. package/src/shared/types/index.ts +48 -53
  36. package/src/shared/utils/activityUtils.ts +3 -2
  37. package/src/client/dist/assets/index-BIu7r5_5.css +0 -1
  38. package/src/client/dist/assets/index-BYMVif3u.js +0 -50
  39. package/src/server/storage/messageParser.ts +0 -169
  40. package/src/server/storage/partParser.ts +0 -532
  41. package/src/server/storage/sessionParser.ts +0 -180
@@ -0,0 +1,63 @@
1
+ import { Database } from "bun:sqlite";
2
+ import { existsSync } from "node:fs";
3
+ import { homedir } from "node:os";
4
+ import { join } from "node:path";
5
+
6
+ const SQLITE_BUSY_TIMEOUT_MS = 5000;
7
+ const SQLITE_CACHE_SIZE = -20000;
8
+
9
+ let dbSingleton: Database | null | undefined;
10
+
11
+ function getStorageRootPath(): string {
12
+ const xdgDataHome = process.env.XDG_DATA_HOME;
13
+ if (xdgDataHome) {
14
+ return xdgDataHome;
15
+ }
16
+
17
+ return join(homedir(), ".local", "share");
18
+ }
19
+
20
+ function getDbPath(): string {
21
+ return join(getStorageRootPath(), "opencode", "opencode.db");
22
+ }
23
+
24
+ function configureConnectionPragmas(db: Database): void {
25
+ db.query(`PRAGMA busy_timeout = ${SQLITE_BUSY_TIMEOUT_MS};`).run();
26
+ db.query(`PRAGMA cache_size = ${SQLITE_CACHE_SIZE};`).run();
27
+ }
28
+
29
+ export function checkDbExists(): boolean {
30
+ return existsSync(getDbPath());
31
+ }
32
+
33
+ export function getDb(): Database | null {
34
+ if (dbSingleton !== undefined) {
35
+ return dbSingleton;
36
+ }
37
+
38
+ if (!checkDbExists()) {
39
+ dbSingleton = null;
40
+ return dbSingleton;
41
+ }
42
+
43
+ try {
44
+ const db = new Database(getDbPath(), { readonly: true });
45
+ configureConnectionPragmas(db);
46
+ dbSingleton = db;
47
+ return dbSingleton;
48
+ } catch (error) {
49
+ const reason = error instanceof Error ? error.message : String(error);
50
+ console.warn(`[storage/db] Failed to open SQLite database: ${reason}`);
51
+ dbSingleton = null;
52
+ return dbSingleton;
53
+ }
54
+ }
55
+
56
+ export function closeDb(): void {
57
+ if (!dbSingleton) {
58
+ return;
59
+ }
60
+
61
+ dbSingleton.close();
62
+ dbSingleton = undefined;
63
+ }
@@ -0,0 +1,28 @@
1
+ export { checkDbExists, closeDb, getDb } from "./db";
2
+ export {
3
+ queryMaxTimestamp,
4
+ queryMessages,
5
+ queryMessagesForSessions,
6
+ queryPart,
7
+ queryParts,
8
+ queryPartsForSessions,
9
+ queryProjects,
10
+ queryProjectByWorktree,
11
+ queryProjectSummaries,
12
+ querySession,
13
+ querySessionChildren,
14
+ querySessionSubtree,
15
+ querySessionSubtreeRevision,
16
+ querySessions,
17
+ queryTodos,
18
+ listProjects,
19
+ } from "./queries";
20
+ export type {
21
+ DbMessageRow,
22
+ DbPartRow,
23
+ DbProjectRow,
24
+ DbProjectSummaryRow,
25
+ DbSessionRow,
26
+ DbTodoRow,
27
+ } from "./queries";
28
+ export { parseBoulder, calculatePlanProgress } from "./boulderParser";
@@ -0,0 +1,528 @@
1
+ import type { Database, Statement } from "bun:sqlite";
2
+ import { getDb } from "./db";
3
+
4
+ export interface DbProjectRow {
5
+ id: string;
6
+ name: string | null;
7
+ worktree: string;
8
+ vcs: string | null;
9
+ commands: string | null;
10
+ sandboxes: string | null;
11
+ timeCreated: number;
12
+ timeUpdated: number;
13
+ }
14
+
15
+ export interface DbSessionRow {
16
+ id: string;
17
+ projectID: string;
18
+ parentID: string | null;
19
+ slug: string | null;
20
+ directory: string;
21
+ title: string;
22
+ version: string | null;
23
+ timeCreated: number;
24
+ timeUpdated: number;
25
+ }
26
+
27
+ export interface DbMessageRow {
28
+ id: string;
29
+ sessionID: string;
30
+ timeCreated: number;
31
+ timeUpdated: number;
32
+ role: string | null;
33
+ agent: string | null;
34
+ data: string;
35
+ }
36
+
37
+ export interface DbPartRow {
38
+ id: string;
39
+ messageID: string;
40
+ sessionID: string;
41
+ timeCreated: number;
42
+ timeUpdated: number;
43
+ type: string | null;
44
+ tool: string | null;
45
+ state: string | null;
46
+ data: string;
47
+ }
48
+
49
+ export interface DbTodoRow {
50
+ sessionID: string;
51
+ content: string;
52
+ status: string;
53
+ priority: string;
54
+ position: number;
55
+ timeCreated: number;
56
+ timeUpdated: number;
57
+ }
58
+
59
+ export interface DbProjectSummaryRow {
60
+ id: string;
61
+ worktree: string;
62
+ sessionCount: number;
63
+ lastActivityAt: number;
64
+ }
65
+
66
+ let cachedDb: Database | null | undefined;
67
+
68
+ let queryProjectsStmt: Statement<DbProjectRow, []> | null = null;
69
+ let querySessionsStmt: Statement<DbSessionRow, [string | null, number | null, number]> | null = null;
70
+ let querySessionStmt: Statement<DbSessionRow, [string]> | null = null;
71
+ let querySessionChildrenStmt: Statement<DbSessionRow, [string]> | null = null;
72
+ let querySessionSubtreeStmt: Statement<DbSessionRow, [string]> | null = null;
73
+ let queryMessagesStmt: Statement<DbMessageRow, [string, number]> | null = null;
74
+ let queryPartsStmt: Statement<DbPartRow, [string]> | null = null;
75
+ let queryPartStmt: Statement<DbPartRow, [string]> | null = null;
76
+ let queryTodosStmt: Statement<DbTodoRow, [string]> | null = null;
77
+ let queryMaxTimestampStmt: Statement<{ maxTimestamp: number | null }, []> | null = null;
78
+ let querySessionSubtreeRevisionStmt: Statement<{ maxTimestamp: number | null }, [string]> | null = null;
79
+ let queryProjectByWorktreeStmt: Statement<DbProjectRow, [string]> | null = null;
80
+ let queryProjectSummariesStmt: Statement<DbProjectSummaryRow, []> | null = null;
81
+
82
+ function getReadyDb(): Database | null {
83
+ const db = getDb();
84
+ if (!db) {
85
+ return null;
86
+ }
87
+
88
+ if (cachedDb === db) {
89
+ return db;
90
+ }
91
+
92
+ cachedDb = db;
93
+ queryProjectsStmt = db.query<DbProjectRow, []>(`
94
+ SELECT
95
+ id,
96
+ name,
97
+ worktree,
98
+ vcs,
99
+ commands,
100
+ sandboxes,
101
+ time_created AS timeCreated,
102
+ time_updated AS timeUpdated
103
+ FROM project
104
+ ORDER BY time_updated DESC
105
+ `);
106
+
107
+ querySessionsStmt = db.query<DbSessionRow, [string | null, number | null, number]>(`
108
+ SELECT
109
+ id,
110
+ project_id AS projectID,
111
+ parent_id AS parentID,
112
+ slug,
113
+ directory,
114
+ title,
115
+ version,
116
+ time_created AS timeCreated,
117
+ time_updated AS timeUpdated
118
+ FROM session
119
+ WHERE (?1 IS NULL OR project_id = ?1)
120
+ AND (?2 IS NULL OR time_updated > ?2)
121
+ ORDER BY time_updated DESC
122
+ LIMIT ?3
123
+ `);
124
+
125
+ querySessionStmt = db.query<DbSessionRow, [string]>(`
126
+ SELECT
127
+ id,
128
+ project_id AS projectID,
129
+ parent_id AS parentID,
130
+ slug,
131
+ directory,
132
+ title,
133
+ version,
134
+ time_created AS timeCreated,
135
+ time_updated AS timeUpdated
136
+ FROM session
137
+ WHERE id = ?1
138
+ LIMIT 1
139
+ `);
140
+
141
+ querySessionChildrenStmt = db.query<DbSessionRow, [string]>(`
142
+ SELECT
143
+ id,
144
+ project_id AS projectID,
145
+ parent_id AS parentID,
146
+ slug,
147
+ directory,
148
+ title,
149
+ version,
150
+ time_created AS timeCreated,
151
+ time_updated AS timeUpdated
152
+ FROM session
153
+ WHERE parent_id = ?1
154
+ ORDER BY time_created ASC
155
+ `);
156
+
157
+ querySessionSubtreeStmt = db.query<DbSessionRow, [string]>(`
158
+ WITH RECURSIVE subtree AS (
159
+ SELECT
160
+ id,
161
+ project_id AS projectID,
162
+ parent_id AS parentID,
163
+ slug,
164
+ directory,
165
+ title,
166
+ version,
167
+ time_created AS timeCreated,
168
+ time_updated AS timeUpdated
169
+ FROM session
170
+ WHERE id = ?1
171
+
172
+ UNION ALL
173
+
174
+ SELECT
175
+ s.id,
176
+ s.project_id AS projectID,
177
+ s.parent_id AS parentID,
178
+ s.slug,
179
+ s.directory,
180
+ s.title,
181
+ s.version,
182
+ s.time_created AS timeCreated,
183
+ s.time_updated AS timeUpdated
184
+ FROM session s
185
+ INNER JOIN subtree st ON s.parent_id = st.id
186
+ )
187
+ SELECT
188
+ id,
189
+ projectID,
190
+ parentID,
191
+ slug,
192
+ directory,
193
+ title,
194
+ version,
195
+ timeCreated,
196
+ timeUpdated
197
+ FROM subtree
198
+ ORDER BY timeCreated ASC, id ASC
199
+ `);
200
+
201
+ queryMessagesStmt = db.query<DbMessageRow, [string, number]>(`
202
+ SELECT
203
+ id,
204
+ session_id AS sessionID,
205
+ time_created AS timeCreated,
206
+ time_updated AS timeUpdated,
207
+ json_extract(data, '$.role') AS role,
208
+ json_extract(data, '$.agent') AS agent,
209
+ data
210
+ FROM message
211
+ WHERE session_id = ?1
212
+ ORDER BY time_created DESC
213
+ LIMIT ?2
214
+ `);
215
+
216
+ queryPartsStmt = db.query<DbPartRow, [string]>(`
217
+ SELECT
218
+ id,
219
+ message_id AS messageID,
220
+ session_id AS sessionID,
221
+ time_created AS timeCreated,
222
+ time_updated AS timeUpdated,
223
+ json_extract(data, '$.type') AS type,
224
+ json_extract(data, '$.tool') AS tool,
225
+ CASE
226
+ WHEN json_type(data, '$.state') = 'text' THEN json_extract(data, '$.state')
227
+ WHEN json_type(data, '$.state.type') = 'text' THEN json_extract(data, '$.state.type')
228
+ ELSE NULL
229
+ END AS state,
230
+ data
231
+ FROM part
232
+ WHERE session_id = ?1
233
+ ORDER BY time_created DESC
234
+ `);
235
+
236
+ queryPartStmt = db.query<DbPartRow, [string]>(`
237
+ SELECT
238
+ id,
239
+ message_id AS messageID,
240
+ session_id AS sessionID,
241
+ time_created AS timeCreated,
242
+ time_updated AS timeUpdated,
243
+ json_extract(data, '$.type') AS type,
244
+ json_extract(data, '$.tool') AS tool,
245
+ CASE
246
+ WHEN json_type(data, '$.state') = 'text' THEN json_extract(data, '$.state')
247
+ WHEN json_type(data, '$.state.type') = 'text' THEN json_extract(data, '$.state.type')
248
+ ELSE NULL
249
+ END AS state,
250
+ data
251
+ FROM part
252
+ WHERE id = ?1
253
+ LIMIT 1
254
+ `);
255
+
256
+ queryTodosStmt = db.query<DbTodoRow, [string]>(`
257
+ SELECT
258
+ session_id AS sessionID,
259
+ content,
260
+ status,
261
+ priority,
262
+ position,
263
+ time_created AS timeCreated,
264
+ time_updated AS timeUpdated
265
+ FROM todo
266
+ WHERE session_id = ?1
267
+ ORDER BY position ASC, time_created ASC
268
+ `);
269
+
270
+ queryMaxTimestampStmt = db.query<{ maxTimestamp: number | null }, []>(`
271
+ SELECT MAX(ts) AS maxTimestamp
272
+ FROM (
273
+ SELECT MAX(time_updated) AS ts FROM session
274
+ UNION ALL
275
+ SELECT MAX(time_updated) AS ts FROM message
276
+ UNION ALL
277
+ SELECT MAX(time_updated) AS ts FROM part
278
+ )
279
+ `);
280
+
281
+ querySessionSubtreeRevisionStmt = db.query<{ maxTimestamp: number | null }, [string]>(`
282
+ WITH RECURSIVE subtree AS (
283
+ SELECT id
284
+ FROM session
285
+ WHERE id = ?1
286
+
287
+ UNION ALL
288
+
289
+ SELECT s.id
290
+ FROM session s
291
+ INNER JOIN subtree st ON s.parent_id = st.id
292
+ )
293
+ SELECT MAX(ts) AS maxTimestamp
294
+ FROM (
295
+ SELECT MAX(time_updated) AS ts FROM session WHERE id IN (SELECT id FROM subtree)
296
+ UNION ALL
297
+ SELECT MAX(time_updated) AS ts FROM message WHERE session_id IN (SELECT id FROM subtree)
298
+ UNION ALL
299
+ SELECT MAX(time_updated) AS ts FROM part WHERE session_id IN (SELECT id FROM subtree)
300
+ )
301
+ `);
302
+
303
+ queryProjectByWorktreeStmt = db.query<DbProjectRow, [string]>(`
304
+ SELECT
305
+ id,
306
+ name,
307
+ worktree,
308
+ vcs,
309
+ commands,
310
+ sandboxes,
311
+ time_created AS timeCreated,
312
+ time_updated AS timeUpdated
313
+ FROM project
314
+ WHERE worktree = ?1
315
+ LIMIT 1
316
+ `);
317
+
318
+ queryProjectSummariesStmt = db.query<DbProjectSummaryRow, []>(`
319
+ SELECT
320
+ p.id,
321
+ p.worktree,
322
+ COUNT(s.id) AS sessionCount,
323
+ COALESCE(MAX(s.time_updated), p.time_updated) AS lastActivityAt
324
+ FROM project p
325
+ LEFT JOIN session s ON s.project_id = p.id
326
+ GROUP BY p.id
327
+ ORDER BY lastActivityAt DESC
328
+ `);
329
+
330
+ return db;
331
+ }
332
+
333
+ export function queryProjects(): DbProjectRow[] {
334
+ const db = getReadyDb();
335
+ if (!db || !queryProjectsStmt) {
336
+ return [];
337
+ }
338
+
339
+ return queryProjectsStmt.all();
340
+ }
341
+
342
+ export function querySessions(
343
+ projectId?: string,
344
+ since?: number,
345
+ limit = 20,
346
+ ): DbSessionRow[] {
347
+ const db = getReadyDb();
348
+ if (!db || !querySessionsStmt) {
349
+ return [];
350
+ }
351
+
352
+ return querySessionsStmt.all(projectId ?? null, since ?? null, limit);
353
+ }
354
+
355
+ export function querySession(sessionId: string): DbSessionRow | null {
356
+ const db = getReadyDb();
357
+ if (!db || !querySessionStmt) {
358
+ return null;
359
+ }
360
+
361
+ return querySessionStmt.get(sessionId);
362
+ }
363
+
364
+ export function querySessionChildren(sessionId: string): DbSessionRow[] {
365
+ const db = getReadyDb();
366
+ if (!db || !querySessionChildrenStmt) {
367
+ return [];
368
+ }
369
+
370
+ return querySessionChildrenStmt.all(sessionId);
371
+ }
372
+
373
+ export function querySessionSubtree(sessionId: string): DbSessionRow[] {
374
+ const db = getReadyDb();
375
+ if (!db || !querySessionSubtreeStmt) {
376
+ return [];
377
+ }
378
+
379
+ return querySessionSubtreeStmt.all(sessionId);
380
+ }
381
+
382
+ export function queryMessages(sessionId: string, limit = 100): DbMessageRow[] {
383
+ const db = getReadyDb();
384
+ if (!db || !queryMessagesStmt) {
385
+ return [];
386
+ }
387
+
388
+ return queryMessagesStmt.all(sessionId, limit);
389
+ }
390
+
391
+ export function queryParts(sessionId: string): DbPartRow[] {
392
+ const db = getReadyDb();
393
+ if (!db || !queryPartsStmt) {
394
+ return [];
395
+ }
396
+
397
+ return queryPartsStmt.all(sessionId);
398
+ }
399
+
400
+ export function queryMessagesForSessions(sessionIds: string[], limitPerSession = 100): DbMessageRow[] {
401
+ const db = getReadyDb();
402
+ if (!db || sessionIds.length === 0) {
403
+ return [];
404
+ }
405
+
406
+ const placeholders = sessionIds.map((_, index) => `?${index + 1}`).join(", ");
407
+ const limitParam = `?${sessionIds.length + 1}`;
408
+ const statement = db.query<DbMessageRow, (string | number)[]>(`
409
+ SELECT
410
+ id,
411
+ sessionID,
412
+ timeCreated,
413
+ timeUpdated,
414
+ role,
415
+ agent,
416
+ data
417
+ FROM (
418
+ SELECT
419
+ id,
420
+ session_id AS sessionID,
421
+ time_created AS timeCreated,
422
+ time_updated AS timeUpdated,
423
+ json_extract(data, '$.role') AS role,
424
+ json_extract(data, '$.agent') AS agent,
425
+ data,
426
+ ROW_NUMBER() OVER (
427
+ PARTITION BY session_id
428
+ ORDER BY time_created DESC, id DESC
429
+ ) AS rowNumber
430
+ FROM message
431
+ WHERE session_id IN (${placeholders})
432
+ )
433
+ WHERE rowNumber <= ${limitParam}
434
+ ORDER BY sessionID ASC, timeCreated DESC, id DESC
435
+ `);
436
+
437
+ return statement.all(...sessionIds, limitPerSession);
438
+ }
439
+
440
+ export function queryPartsForSessions(sessionIds: string[]): DbPartRow[] {
441
+ const db = getReadyDb();
442
+ if (!db || sessionIds.length === 0) {
443
+ return [];
444
+ }
445
+
446
+ const placeholders = sessionIds.map((_, index) => `?${index + 1}`).join(", ");
447
+ const statement = db.query<DbPartRow, string[]>(`
448
+ SELECT
449
+ id,
450
+ message_id AS messageID,
451
+ session_id AS sessionID,
452
+ time_created AS timeCreated,
453
+ time_updated AS timeUpdated,
454
+ json_extract(data, '$.type') AS type,
455
+ json_extract(data, '$.tool') AS tool,
456
+ CASE
457
+ WHEN json_type(data, '$.state') = 'text' THEN json_extract(data, '$.state')
458
+ WHEN json_type(data, '$.state.type') = 'text' THEN json_extract(data, '$.state.type')
459
+ ELSE NULL
460
+ END AS state,
461
+ data
462
+ FROM part
463
+ WHERE session_id IN (${placeholders})
464
+ ORDER BY sessionID ASC, timeCreated DESC, id DESC
465
+ `);
466
+
467
+ return statement.all(...sessionIds);
468
+ }
469
+
470
+ export function queryPart(partId: string): DbPartRow | null {
471
+ const db = getReadyDb();
472
+ if (!db || !queryPartStmt) {
473
+ return null;
474
+ }
475
+
476
+ return queryPartStmt.get(partId);
477
+ }
478
+
479
+ export function queryTodos(sessionId: string): DbTodoRow[] {
480
+ const db = getReadyDb();
481
+ if (!db || !queryTodosStmt) {
482
+ return [];
483
+ }
484
+
485
+ return queryTodosStmt.all(sessionId);
486
+ }
487
+
488
+ export function queryMaxTimestamp(): number {
489
+ const db = getReadyDb();
490
+ if (!db || !queryMaxTimestampStmt) {
491
+ return 0;
492
+ }
493
+
494
+ return Number(queryMaxTimestampStmt.get()?.maxTimestamp ?? 0);
495
+ }
496
+
497
+ export function querySessionSubtreeRevision(sessionId: string): number {
498
+ const db = getReadyDb();
499
+ if (!db || !querySessionSubtreeRevisionStmt) {
500
+ return 0;
501
+ }
502
+
503
+ return Number(querySessionSubtreeRevisionStmt.get(sessionId)?.maxTimestamp ?? 0);
504
+ }
505
+
506
+
507
+ export function listProjects(): string[] {
508
+ const projects = queryProjects();
509
+ return projects.map((p) => p.id);
510
+ }
511
+
512
+ export function queryProjectByWorktree(directory: string): DbProjectRow | null {
513
+ const db = getReadyDb();
514
+ if (!db || !queryProjectByWorktreeStmt) {
515
+ return null;
516
+ }
517
+
518
+ return queryProjectByWorktreeStmt.get(directory);
519
+ }
520
+
521
+ export function queryProjectSummaries(): DbProjectSummaryRow[] {
522
+ const db = getReadyDb();
523
+ if (!db || !queryProjectSummariesStmt) {
524
+ return [];
525
+ }
526
+
527
+ return queryProjectSummariesStmt.all();
528
+ }
@@ -1,5 +1,5 @@
1
1
  import { stat } from "node:fs/promises";
2
- import { listAllSessions } from "../storage/sessionParser";
2
+ import { queryProjects } from "../storage";
3
3
  import type { SessionMetadata } from "../../shared/types";
4
4
 
5
5
  async function directoryExists(directory: string): Promise<boolean> {
@@ -15,8 +15,14 @@ export async function resolveProjectDirectory(
15
15
  projectId: string,
16
16
  preloadedSessions?: SessionMetadata[],
17
17
  ): Promise<string | null> {
18
- const allSessions = preloadedSessions ?? await listAllSessions();
19
- const directory = allSessions.find((session) => session.projectID === projectId)?.directory;
18
+ let directory: string | undefined;
19
+
20
+ if (preloadedSessions) {
21
+ directory = preloadedSessions.find((session) => session.projectID === projectId)?.directory;
22
+ } else {
23
+ const projects = queryProjects();
24
+ directory = projects.find((p) => p.id === projectId)?.worktree;
25
+ }
20
26
 
21
27
  if (!directory) {
22
28
  return null;