circuschief 0.8.0 → 1.0.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 (136) hide show
  1. package/package.json +1 -1
  2. package/packages/server/src/api/commandButtons.js +16 -15
  3. package/packages/server/src/api/projects-commandButtons.js +6 -6
  4. package/packages/server/src/api/projects-session-create.js +109 -0
  5. package/packages/server/src/api/projects-session-defaults.js +51 -0
  6. package/packages/server/src/api/projects-session-helpers.js +47 -1
  7. package/packages/server/src/api/projects-templates.js +38 -0
  8. package/packages/server/src/api/projects.js +28 -180
  9. package/packages/server/src/api/sessions-commands.js +21 -18
  10. package/packages/server/src/api/sessions-patch.js +41 -1
  11. package/packages/server/src/db/SessionRepository.js +1 -1
  12. package/packages/server/src/db/SessionTemplateRepository.js +23 -2
  13. package/packages/server/src/db/migrations/canvasItemsMigrations.js +109 -0
  14. package/packages/server/src/db/migrations/conversationsMigrations.js +187 -0
  15. package/packages/server/src/db/migrations/index.js +225 -6
  16. package/packages/server/src/db/migrations/kanbanMigrations.js +99 -0
  17. package/packages/server/src/db/migrations/miscMigrations.js +244 -0
  18. package/packages/server/src/db/migrations/projectsMigrations.js +130 -0
  19. package/packages/server/src/db/migrations/providerCommitAttributionMigrations.js +30 -0
  20. package/packages/server/src/db/migrations/providerMigrations.js +165 -0
  21. package/packages/server/src/db/migrations/sessionTableRecreate.js +136 -0
  22. package/packages/server/src/db/migrations/sessionsMigrations.js +300 -0
  23. package/packages/server/src/db/session-helpers.js +26 -1
  24. package/packages/server/src/schema.sql +4 -0
  25. package/packages/server/src/services/commandButtonPrompts.js +9 -7
  26. package/packages/server/src/services/gitCommitAttribution.js +38 -8
  27. package/packages/server/src/services/gitDiff.js +132 -0
  28. package/packages/server/src/services/gitRepoUrl.js +174 -0
  29. package/packages/server/src/services/gitService.js +37 -309
  30. package/packages/server/src/services/gitWorktree.js +127 -0
  31. package/packages/server/src/services/sessionPrompts.js +1 -1
  32. package/packages/shared/src/contracts/sessions.js +27 -1
  33. package/packages/shared/src/contracts/templates.js +10 -0
  34. package/packages/web/dist/assets/{ActiveSessionsView-B0XHqLmv.js → ActiveSessionsView-Cxh8mHmB.js} +1 -1
  35. package/packages/web/dist/assets/{AgentLogsView-DmsjUMlB.js → AgentLogsView-xdfI2bR6.js} +2 -2
  36. package/packages/web/dist/assets/ApiClient-DfbJwzpz.js +1 -0
  37. package/packages/web/dist/assets/ArchiveConfirmModal-DXZYdzHR.js +1 -0
  38. package/packages/web/dist/assets/CommandButtonDetailView-D8xfqLAp.js +1 -0
  39. package/packages/web/dist/assets/CommandButtonDetailView-D9zjx9ME.css +1 -0
  40. package/packages/web/dist/assets/EffortLevelSelector-D2Hdzc_8.js +1 -0
  41. package/packages/web/dist/assets/{GeneralSettingsView-D1nI8_zk.js → GeneralSettingsView-sPXkLlLy.js} +1 -1
  42. package/packages/web/dist/assets/{InputWithButton-CAkttyqx.js → InputWithButton-B-o0DgMH.js} +1 -1
  43. package/packages/web/dist/assets/{InterpolationHelp-BO1j9Z3_.js → InterpolationHelp-Dxn1li4l.js} +1 -1
  44. package/packages/web/dist/assets/MarkdownEditor-D4Kbb-9l.js +2 -0
  45. package/packages/web/dist/assets/ModelSelector-72C7MUH4.js +1 -0
  46. package/packages/web/dist/assets/{ModelSelector-BSxKUSus.css → ModelSelector-BNYKujL-.css} +1 -1
  47. package/packages/web/dist/assets/NewSessionView-BR_COfgW.js +3 -0
  48. package/packages/web/dist/assets/{NewSessionView-BDPb-1qr.css → NewSessionView-DBl7T2Xp.css} +1 -1
  49. package/packages/web/dist/assets/ProjectEditView-DbqTbA0q.css +1 -0
  50. package/packages/web/dist/assets/ProjectEditView-WImU7sNd.js +1 -0
  51. package/packages/web/dist/assets/{ProjectListView-DcNyuINs.js → ProjectListView-CYmmAcBD.js} +1 -1
  52. package/packages/web/dist/assets/{ProjectNewView-B5YV62hv.js → ProjectNewView-DEhqw3Jv.js} +1 -1
  53. package/packages/web/dist/assets/ProvidersView-XZh3jkmH.js +1 -0
  54. package/packages/web/dist/assets/QuickResponsesPanel-BqmnTd-D.js +1 -0
  55. package/packages/web/dist/assets/QuickResponsesPanel-dk-Rj8xx.css +1 -0
  56. package/packages/web/dist/assets/ResizableTextarea-BQNw5e0C.css +1 -0
  57. package/packages/web/dist/assets/ResizableTextarea-DpWdIAP6.js +1 -0
  58. package/packages/web/dist/assets/SessionCard-Bw77-KwD.js +1 -0
  59. package/packages/web/dist/assets/SessionDetailView-B59TEkr-.js +36 -0
  60. package/packages/web/dist/assets/SessionDetailView-CKVBnR4T.css +1 -0
  61. package/packages/web/dist/assets/{SessionFormOptions-B6AxyREh.js → SessionFormOptions-hqijxc0S.js} +1 -1
  62. package/packages/web/dist/assets/{SessionListView-B5_6gW49.css → SessionListView-3-xx6EVs.css} +1 -1
  63. package/packages/web/dist/assets/SessionListView-DYXHM9I-.js +1 -0
  64. package/packages/web/dist/assets/{SessionLogStream-LlZ3z_Xj.js → SessionLogStream-5NfVr9pF.js} +6 -6
  65. package/packages/web/dist/assets/{SettingsView-CTGiGvR2.js → SettingsView-DI8ncOAV.js} +1 -1
  66. package/packages/web/dist/assets/{SlashCommandWizard-Cy04d7-o.js → SlashCommandWizard-BQ_rMzn-.js} +1 -1
  67. package/packages/web/dist/assets/{SummarySettingsView-BR2ZjEa3.js → SummarySettingsView-C2Qs35mm.js} +1 -1
  68. package/packages/web/dist/assets/TemplateDetailView-B5NI2oTR.css +1 -0
  69. package/packages/web/dist/assets/TemplateDetailView-zVkIvgtu.js +1 -0
  70. package/packages/web/dist/assets/{commandButtons-BfqR-fqq.js → commandButtons-CoU3G4zK.js} +1 -1
  71. package/packages/web/dist/assets/index-9yF1uCCA.js +1 -0
  72. package/packages/web/dist/assets/index-BKstCaYU.js +1 -0
  73. package/packages/web/dist/assets/index-BhbH7eOk.js +1 -0
  74. package/packages/web/dist/assets/{index-DgkC10TW.js → index-BjuRttEY.js} +3 -3
  75. package/packages/web/dist/assets/index-Bo7PdwM5.js +1 -0
  76. package/packages/web/dist/assets/index-C2QFVD7d.js +83 -0
  77. package/packages/web/dist/assets/index-C7Ww2auW.js +1 -0
  78. package/packages/web/dist/assets/index-CAGdsDh7.js +1 -0
  79. package/packages/web/dist/assets/index-CLRsVASf.js +3 -0
  80. package/packages/web/dist/assets/{index-DtfUt785.js → index-CP-SxOlV.js} +1 -1
  81. package/packages/web/dist/assets/index-CslU0psO.js +1 -0
  82. package/packages/web/dist/assets/index-DI4NxaWD.js +1 -0
  83. package/packages/web/dist/assets/index-DOzONENy.js +1 -0
  84. package/packages/web/dist/assets/index-DUa7adFh.js +1 -0
  85. package/packages/web/dist/assets/index-DZBpETI5.js +1 -0
  86. package/packages/web/dist/assets/index-DsjWqc6R.js +7 -0
  87. package/packages/web/dist/assets/index-c99Bo3JV.js +1 -0
  88. package/packages/web/dist/assets/index-mT1JpxDc.js +1 -0
  89. package/packages/web/dist/assets/index-rkQx2tso.js +1 -0
  90. package/packages/web/dist/assets/{index-BY174HVJ.css → index-uySCcnA_.css} +1 -1
  91. package/packages/web/dist/assets/projectDefaults-B8esIcYq.js +1 -0
  92. package/packages/web/dist/assets/{projects-DXYQNJIi.js → projects-C-8PSxKi.js} +1 -1
  93. package/packages/web/dist/assets/{providers-1bnH-exJ.js → providers-oXifvvqN.js} +1 -1
  94. package/packages/web/dist/assets/{sessions-6zGUlFrt.js → sessions-Nq5VafSf.js} +1 -1
  95. package/packages/web/dist/assets/{settings-MbfRir0d.js → settings-DtpuiyT6.js} +1 -1
  96. package/packages/web/dist/index.html +2 -2
  97. package/packages/web/dist/assets/ApiClient-C3ztI9s9.js +0 -1
  98. package/packages/web/dist/assets/ArchiveConfirmModal-BlCyn5Vt.js +0 -1
  99. package/packages/web/dist/assets/CommandButtonDetailView-CdSCPp78.js +0 -1
  100. package/packages/web/dist/assets/CommandButtonDetailView-DBm3rzhw.css +0 -1
  101. package/packages/web/dist/assets/EffortLevelSelector-hc2MNKg6.js +0 -1
  102. package/packages/web/dist/assets/MarkdownEditor-ucRAP_UM.js +0 -2
  103. package/packages/web/dist/assets/ModelSelector-CwTz8ZWO.js +0 -1
  104. package/packages/web/dist/assets/NewSessionView-BsDrp8mj.js +0 -3
  105. package/packages/web/dist/assets/ProjectEditView-CwTOeSun.js +0 -1
  106. package/packages/web/dist/assets/ProjectEditView-J15mcsWz.css +0 -1
  107. package/packages/web/dist/assets/ProvidersView-nY9GnDdO.js +0 -1
  108. package/packages/web/dist/assets/QuickResponseSettings-B352c75l.css +0 -1
  109. package/packages/web/dist/assets/QuickResponseSettings-BQwQXuL7.js +0 -1
  110. package/packages/web/dist/assets/QuickResponsesPanel-BlFDvnZ2.css +0 -1
  111. package/packages/web/dist/assets/QuickResponsesPanel-BzSYcCSP.js +0 -1
  112. package/packages/web/dist/assets/ResizableTextarea-B3YIdIXv.js +0 -1
  113. package/packages/web/dist/assets/ResizableTextarea-DsU3TVwF.css +0 -1
  114. package/packages/web/dist/assets/SessionCard-CjE1tXiT.js +0 -1
  115. package/packages/web/dist/assets/SessionDetailView-3cPZrbS3.js +0 -36
  116. package/packages/web/dist/assets/SessionDetailView-CZRZMrfM.css +0 -1
  117. package/packages/web/dist/assets/SessionListView-CLXBfLcq.js +0 -1
  118. package/packages/web/dist/assets/TemplateDetailView-DH6Oswsp.js +0 -1
  119. package/packages/web/dist/assets/TemplateDetailView-DT2m06W7.css +0 -1
  120. package/packages/web/dist/assets/index-1zziPL6l.js +0 -1
  121. package/packages/web/dist/assets/index-7kzHPxSF.js +0 -1
  122. package/packages/web/dist/assets/index-B0N_obMc.js +0 -1
  123. package/packages/web/dist/assets/index-BNk_gdfI.js +0 -1
  124. package/packages/web/dist/assets/index-CSqaAH-0.js +0 -1
  125. package/packages/web/dist/assets/index-C_q4WlK8.js +0 -1
  126. package/packages/web/dist/assets/index-D1wpU4y0.js +0 -7
  127. package/packages/web/dist/assets/index-D5zCA8sD.js +0 -1
  128. package/packages/web/dist/assets/index-DGR8ELWY.js +0 -1
  129. package/packages/web/dist/assets/index-DHga8pXo.js +0 -1
  130. package/packages/web/dist/assets/index-DSby02Wl.js +0 -1
  131. package/packages/web/dist/assets/index-DqjXJTVI.js +0 -1
  132. package/packages/web/dist/assets/index-_4S2uLDI.js +0 -1
  133. package/packages/web/dist/assets/index-fK8FIZgP.js +0 -83
  134. package/packages/web/dist/assets/index-gmiZeFXN.js +0 -1
  135. package/packages/web/dist/assets/index-irD539ZM.js +0 -3
  136. package/packages/web/dist/assets/index-yq-E1Y00.js +0 -1
@@ -1,26 +1,42 @@
1
1
  import { exec } from 'child_process';
2
2
  import { promisify } from 'util';
3
- import path from 'path';
4
- import { realpath } from 'fs/promises';
5
3
  export {
4
+ _setManagedHooksPath,
6
5
  clearWorktreeCommitAttribution,
7
6
  configureWorktreeCommitAttribution,
8
7
  ensureWorktreeCommitAttributionHook,
8
+ getManagedHooksPath,
9
9
  } from './gitCommitAttribution.js';
10
+ export {
11
+ normalizeGitRemoteUrl,
12
+ getRepositoryUrl,
13
+ detectWorktreePath,
14
+ } from './gitRepoUrl.js';
15
+ export {
16
+ getDiff,
17
+ getStagedDiff,
18
+ getUntrackedFiles,
19
+ getDiffAgainstBranch,
20
+ getStagedDiffAgainstBranch,
21
+ getDiffBetweenRefs,
22
+ getModifiedFilesCount,
23
+ } from './gitDiff.js';
24
+ export {
25
+ branchExists,
26
+ checkoutBranch,
27
+ createWorktree,
28
+ removeWorktree,
29
+ createWorktreeForBranch,
30
+ } from './gitWorktree.js';
10
31
 
11
32
  const execAsync = promisify(exec);
12
33
 
13
- // Cache for default branch detection per repository
14
- // Key: directory path, Value: { branch: string, timestamp: number }
34
+ // Cache for default branch detection: directory -> { branch, timestamp }
15
35
  const defaultBranchCache = new Map();
16
36
  const CACHE_TTL_MS = 5 * 60 * 1000; // 5 minutes
17
37
  const MAX_CACHE_SIZE = 100; // Maximum number of repositories to cache
18
38
 
19
- // Configurable logger for warning messages
20
- // Can be overridden via setLogger() for custom logging behavior
21
- let logger = {
22
- warn: (...args) => console.warn(...args),
23
- };
39
+ import { _setWorktreeLogger } from './gitWorktree.js';
24
40
 
25
41
  /**
26
42
  * Set a custom logger for git service warnings.
@@ -29,19 +45,15 @@ let logger = {
29
45
  * @param {Function} customLogger.warn - Function to handle warning messages
30
46
  */
31
47
  export function setLogger(customLogger) {
32
- logger = customLogger;
48
+ _setWorktreeLogger(customLogger);
33
49
  }
34
50
 
35
- /**
36
- * Evict oldest entries from cache if it exceeds MAX_CACHE_SIZE.
37
- * Uses LRU-like eviction based on timestamp.
38
- */
51
+ /** Evict oldest cache entries if size exceeds MAX_CACHE_SIZE (LRU-like). */
39
52
  function evictOldestCacheEntries() {
40
53
  if (defaultBranchCache.size <= MAX_CACHE_SIZE) {
41
54
  return;
42
55
  }
43
56
 
44
- // Sort entries by timestamp and remove oldest ones
45
57
  const entries = [...defaultBranchCache.entries()];
46
58
  entries.sort((a, b) => a[1].timestamp - b[1].timestamp);
47
59
 
@@ -51,32 +63,19 @@ function evictOldestCacheEntries() {
51
63
  }
52
64
  }
53
65
 
54
- /**
55
- * Safely fetch from origin remote.
56
- * Logs a warning if fetch fails but does not throw.
57
- * @param {string} directory - The git repository directory
58
- * @returns {Promise<boolean>} - True if fetch succeeded, false otherwise
59
- */
60
- async function safeFetchOrigin(directory) {
61
- try {
62
- await git(directory, 'fetch origin');
63
- return true;
64
- } catch (err) {
65
- // No origin or network unavailable, proceed without fetch
66
- logger.warn('Could not fetch from origin, proceeding with local refs:', err.message);
67
- return false;
68
- }
69
- }
70
-
71
66
  /**
72
67
  * Execute a git command in a directory
73
68
  * @param {string} directory
74
69
  * @param {string} command
70
+ * @param {Object} [opts]
71
+ * @param {Object} [opts.env]
72
+ * @param {number} [opts.timeout]
75
73
  * @returns {Promise<string>}
76
74
  */
77
- async function git(directory, command, opts = {}) {
75
+ export async function git(directory, command, opts = {}) {
78
76
  const execOpts = { cwd: directory };
79
77
  if (opts.env) execOpts.env = opts.env;
78
+ if (opts.timeout) execOpts.timeout = opts.timeout;
80
79
  const { stdout } = await execAsync(`git ${command}`, execOpts);
81
80
  return stdout.trim();
82
81
  }
@@ -246,242 +245,9 @@ export async function getCurrentBranch(directory) {
246
245
  }
247
246
  }
248
247
 
249
- /**
250
- * Create a new worktree
251
- * @param {string} directory
252
- * @param {string} branch
253
- * @param {string} worktreePath
254
- * @param {Object} options
255
- * @param {boolean} options.skipFetch - Skip fetching from origin (default: false)
256
- * @returns {Promise<{path: string, branch: string}>}
257
- */
258
- export async function createWorktree(directory, branch, worktreePath, options = {}) {
259
- const { skipFetch = false } = options;
260
-
261
- // Fetch latest from origin to ensure we have up-to-date default branch
262
- if (!skipFetch) {
263
- await safeFetchOrigin(directory);
264
- }
265
-
266
- // Get the default branch from origin (main or master)
267
- const defaultBranch = await getOriginDefaultBranch(directory);
268
- // Base new branch on origin's default branch to avoid including unrelated commits from HEAD
269
- // Use --no-track to prevent the new branch from tracking the start-point (main/master)
270
- await git(directory, `worktree add --no-track "${worktreePath}" -b "${branch}" ${defaultBranch}`);
271
- return { path: worktreePath, branch };
272
- }
273
-
274
- /**
275
- * Remove a worktree
276
- * @param {string} directory
277
- * @param {string} path
278
- * @param {boolean} force - Force removal even if worktree has uncommitted changes
279
- */
280
- export async function removeWorktree(directory, worktreePath, force = false) {
281
- const forceFlag = force ? '--force' : '';
282
- await git(directory, `worktree remove ${forceFlag} "${worktreePath}"`);
283
- }
284
-
285
- /**
286
- * Get diff for a directory
287
- * @param {string} directory
288
- * @returns {Promise<string>}
289
- */
290
- export async function getDiff(directory) {
291
- try {
292
- return await git(directory, 'diff');
293
- } catch {
294
- return '';
295
- }
296
- }
297
-
298
- /**
299
- * Get staged diff for a directory
300
- * @param {string} directory
301
- * @returns {Promise<string>}
302
- */
303
- export async function getStagedDiff(directory) {
304
- try {
305
- return await git(directory, 'diff --cached');
306
- } catch {
307
- return '';
308
- }
309
- }
310
-
311
- /**
312
- * Check if a branch exists
313
- * @param {string} directory
314
- * @param {string} branch
315
- * @returns {Promise<boolean>}
316
- */
317
- export async function branchExists(directory, branch) {
318
- try {
319
- await git(directory, `rev-parse --verify refs/heads/${branch}`);
320
- return true;
321
- } catch {
322
- return false;
323
- }
324
- }
325
-
326
- /**
327
- * Checkout a branch, creating it if it doesn't exist
328
- * @param {string} directory
329
- * @param {string} branch
330
- * @returns {Promise<void>}
331
- */
332
- export async function checkoutBranch(directory, branch) {
333
- const exists = await branchExists(directory, branch);
334
- if (exists) {
335
- await git(directory, `checkout "${branch}"`);
336
- } else {
337
- await git(directory, `checkout -b "${branch}"`);
338
- }
339
- }
340
-
341
- /**
342
- * Create a worktree for a branch (creates branch if it doesn't exist)
343
- * @param {string} directory - Main repo directory
344
- * @param {string} branch - Branch name
345
- * @param {string} worktreePath - Path for the new worktree
346
- * @param {Object} options
347
- * @param {boolean} options.skipFetch - Skip fetching from origin (default: false)
348
- * @returns {Promise<{path: string, branch: string}>}
349
- */
350
- export async function createWorktreeForBranch(directory, branch, worktreePath, options = {}) {
351
- const { skipFetch = false } = options;
352
-
353
- // Fetch latest from origin to ensure we have up-to-date default branch
354
- if (!skipFetch) {
355
- await safeFetchOrigin(directory);
356
- }
357
-
358
- const exists = await branchExists(directory, branch);
359
- if (exists) {
360
- await git(directory, `worktree add "${worktreePath}" "${branch}"`);
361
- } else {
362
- // Get the default branch from origin (main or master)
363
- const defaultBranch = await getOriginDefaultBranch(directory);
364
- // Base new branch on origin's default branch to avoid including unrelated commits from HEAD
365
- // Use --no-track to prevent the new branch from tracking the start-point (main/master)
366
- await git(directory, `worktree add --no-track -b "${branch}" "${worktreePath}" ${defaultBranch}`);
367
- }
368
- return { path: worktreePath, branch };
369
- }
370
-
371
- /**
372
- * Get list of untracked files
373
- * @param {string} directory
374
- * @returns {Promise<string[]>}
375
- */
376
- export async function getUntrackedFiles(directory) {
377
- try {
378
- const output = await git(directory, 'ls-files --others --exclude-standard');
379
- if (!output) return [];
380
- return output.split('\n').filter((line) => line.trim());
381
- } catch {
382
- return [];
383
- }
384
- }
385
-
386
- /**
387
- * Get diff for a directory compared to a specific branch
388
- * @param {string} directory
389
- * @param {string} branch - Branch to compare against (e.g., 'origin/main')
390
- * @returns {Promise<string>}
391
- */
392
- export async function getDiffAgainstBranch(directory, branch) {
393
- try {
394
- return await git(directory, `diff ${branch}`);
395
- } catch {
396
- return '';
397
- }
398
- }
399
-
400
- /**
401
- * Get staged diff for a directory compared to a specific branch
402
- * @param {string} directory
403
- * @param {string} branch - Branch to compare against (e.g., 'origin/main')
404
- * @returns {Promise<string>}
405
- */
406
- export async function getStagedDiffAgainstBranch(directory, branch) {
407
- try {
408
- return await git(directory, `diff --cached ${branch}`);
409
- } catch {
410
- return '';
411
- }
412
- }
413
-
414
- /**
415
- * Get diff between two git refs (e.g., comparing HEAD to origin/main)
416
- * This shows the committed changes between two refs, ignoring working tree state
417
- * @param {string} directory
418
- * @param {string} fromRef - Base ref (e.g., 'origin/main')
419
- * @param {string} toRef - Target ref (e.g., 'HEAD')
420
- * @returns {Promise<string>}
421
- */
422
- export async function getDiffBetweenRefs(directory, fromRef, toRef) {
423
- try {
424
- return await git(directory, `diff ${fromRef} ${toRef}`);
425
- } catch {
426
- return '';
427
- }
428
- }
429
-
430
- /**
431
- * Get count of files modified/added compared to a branch
432
- * Includes committed changes + staged + unstaged + untracked files
433
- * @param {string} directory - The git repository directory
434
- * @param {string} branch - Branch to compare against (e.g., 'origin/main')
435
- * @returns {Promise<number>} - Total count of unique files modified/added
436
- */
437
- export async function getModifiedFilesCount(directory, branch) {
438
- try {
439
- // Get all modified files in one command using --name-only
440
- // This includes: committed changes vs branch + staged
441
- const committedAndStaged = await git(
442
- directory,
443
- `diff --name-only ${branch}...HEAD`
444
- );
445
-
446
- // Get unstaged changes (working tree vs index)
447
- const unstaged = await git(directory, 'diff --name-only');
448
-
449
- // Get untracked files
450
- const untracked = await getUntrackedFiles(directory);
451
-
452
- // Combine all files into a Set to get unique count
453
- const allFiles = new Set();
454
-
455
- // Parse committed+staged files
456
- if (committedAndStaged) {
457
- committedAndStaged.split('\n').forEach(f => {
458
- if (f.trim()) allFiles.add(f.trim());
459
- });
460
- }
461
-
462
- // Parse unstaged files
463
- if (unstaged) {
464
- unstaged.split('\n').forEach(f => {
465
- if (f.trim()) allFiles.add(f.trim());
466
- });
467
- }
468
-
469
- // Add untracked files
470
- untracked.forEach(f => allFiles.add(f));
471
-
472
- return allFiles.size;
473
- } catch (error) {
474
- logger.warn(`Failed to get modified files count for ${directory}:`, error.message);
475
- return 0;
476
- }
477
- }
478
-
479
248
  /**
480
249
  * Get the git author info from the global config (~/.gitconfig).
481
- *
482
- * Uses `--global` so that a contaminated local config (e.g. one that
483
- * already has Claude Code's identity) is bypassed.
484
- *
250
+ * Uses `--global` so that a contaminated local config is bypassed.
485
251
  * @param {string} directory
486
252
  * @param {Object} [options]
487
253
  * @param {Object} [options.env] - Custom environment variables (useful for tests)
@@ -502,17 +268,12 @@ export async function getGitAuthor(directory, { env } = {}) {
502
268
 
503
269
  /**
504
270
  * Pin the human developer's git identity in a worktree's config.
505
- *
506
- * Reads user.name/user.email from the main project directory and writes
507
- * them into the worktree-specific config (--worktree). This ensures the
508
- * human is always the commit Author, even if the session's environment
509
- * tries to override it. Claude Code already adds its own Co-Authored-By
510
- * trailer via its system prompt, so no hook is needed.
511
- *
512
- * Only call this for worktree directories, not the main repo.
513
- *
271
+ * Reads user.name/user.email from the main project directory and writes them
272
+ * into the worktree-specific config (--worktree). Only call for worktree dirs.
514
273
  * @param {string} worktreePath - The worktree directory
515
274
  * @param {string} projectDir - The main project directory (to read author from)
275
+ * @param {Object} [options]
276
+ * @param {Object} [options.env] - Custom environment variables (useful for tests)
516
277
  * @returns {Promise<boolean>} - True if author was pinned
517
278
  */
518
279
  export async function pinAuthorInWorktree(worktreePath, projectDir, { env } = {}) {
@@ -530,36 +291,3 @@ export async function pinAuthorInWorktree(worktreePath, projectDir, { env } = {}
530
291
  return true;
531
292
  }
532
293
 
533
- /**
534
- * Detect the worktree path for a directory by inspecting existing worktrees.
535
- * If external worktrees exist, uses the parent directory of the first one.
536
- * Otherwise, falls back to {directory}/.worktrees.
537
- * @param {string} directory - The git repository directory
538
- * @returns {Promise<{worktreePath: string, source: 'detected' | 'default'}>}
539
- */
540
- export async function detectWorktreePath(directory) {
541
- const isRepo = await isGitRepo(directory);
542
- if (!isRepo) {
543
- return { worktreePath: path.join(directory, '.worktrees'), source: 'default' };
544
- }
545
-
546
- // Resolve symlinks for consistent path comparison (e.g., /var -> /private/var on macOS)
547
- let resolvedDir;
548
- try {
549
- resolvedDir = await realpath(directory);
550
- } catch {
551
- resolvedDir = path.resolve(directory);
552
- }
553
-
554
- const worktrees = await getWorktrees(directory);
555
- // Filter out the main worktree (its path === directory or resolves to it)
556
- const externalWorktrees = worktrees.filter(wt => path.resolve(wt.path) !== resolvedDir);
557
-
558
- if (externalWorktrees.length > 0) {
559
- // Use the parent directory of the first external worktree
560
- const parentDir = path.dirname(path.resolve(externalWorktrees[0].path));
561
- return { worktreePath: parentDir, source: 'detected' };
562
- }
563
-
564
- return { worktreePath: path.join(resolvedDir, '.worktrees'), source: 'default' };
565
- }
@@ -0,0 +1,127 @@
1
+ import { git, getOriginDefaultBranch } from './gitService.js';
2
+
3
+ // Configurable logger for warning messages
4
+ let logger = {
5
+ warn: (...args) => console.warn(...args),
6
+ };
7
+
8
+ /**
9
+ * Set a custom logger for git worktree warnings.
10
+ * @param {Object} customLogger - Logger object with a warn method
11
+ */
12
+ export function _setWorktreeLogger(customLogger) {
13
+ logger = customLogger;
14
+ }
15
+
16
+ /**
17
+ * Safely fetch from origin remote.
18
+ * Logs a warning if fetch fails but does not throw.
19
+ * @param {string} directory - The git repository directory
20
+ * @returns {Promise<boolean>} - True if fetch succeeded, false otherwise
21
+ */
22
+ async function safeFetchOrigin(directory) {
23
+ try {
24
+ await git(directory, 'fetch origin');
25
+ return true;
26
+ } catch (err) {
27
+ // No origin or network unavailable, proceed without fetch
28
+ logger.warn('Could not fetch from origin, proceeding with local refs:', err.message);
29
+ return false;
30
+ }
31
+ }
32
+
33
+ /**
34
+ * Check if a branch exists
35
+ * @param {string} directory
36
+ * @param {string} branch
37
+ * @returns {Promise<boolean>}
38
+ */
39
+ export async function branchExists(directory, branch) {
40
+ try {
41
+ await git(directory, `rev-parse --verify refs/heads/${branch}`);
42
+ return true;
43
+ } catch {
44
+ return false;
45
+ }
46
+ }
47
+
48
+ /**
49
+ * Checkout a branch, creating it if it doesn't exist
50
+ * @param {string} directory
51
+ * @param {string} branch
52
+ * @returns {Promise<void>}
53
+ */
54
+ export async function checkoutBranch(directory, branch) {
55
+ const exists = await branchExists(directory, branch);
56
+ if (exists) {
57
+ await git(directory, `checkout "${branch}"`);
58
+ } else {
59
+ await git(directory, `checkout -b "${branch}"`);
60
+ }
61
+ }
62
+
63
+ /**
64
+ * Create a new worktree
65
+ * @param {string} directory
66
+ * @param {string} branch
67
+ * @param {string} worktreePath
68
+ * @param {Object} options
69
+ * @param {boolean} options.skipFetch - Skip fetching from origin (default: false)
70
+ * @returns {Promise<{path: string, branch: string}>}
71
+ */
72
+ export async function createWorktree(directory, branch, worktreePath, options = {}) {
73
+ const { skipFetch = false } = options;
74
+
75
+ // Fetch latest from origin to ensure we have up-to-date default branch
76
+ if (!skipFetch) {
77
+ await safeFetchOrigin(directory);
78
+ }
79
+
80
+ // Get the default branch from origin (main or master)
81
+ const defaultBranch = await getOriginDefaultBranch(directory);
82
+ // Base new branch on origin's default branch to avoid including unrelated commits from HEAD
83
+ // Use --no-track to prevent the new branch from tracking the start-point (main/master)
84
+ await git(directory, `worktree add --no-track "${worktreePath}" -b "${branch}" ${defaultBranch}`);
85
+ return { path: worktreePath, branch };
86
+ }
87
+
88
+ /**
89
+ * Remove a worktree
90
+ * @param {string} directory
91
+ * @param {string} path
92
+ * @param {boolean} force - Force removal even if worktree has uncommitted changes
93
+ */
94
+ export async function removeWorktree(directory, worktreePath, force = false) {
95
+ const forceFlag = force ? '--force' : '';
96
+ await git(directory, `worktree remove ${forceFlag} "${worktreePath}"`);
97
+ }
98
+
99
+ /**
100
+ * Create a worktree for a branch (creates branch if it doesn't exist)
101
+ * @param {string} directory - Main repo directory
102
+ * @param {string} branch - Branch name
103
+ * @param {string} worktreePath - Path for the new worktree
104
+ * @param {Object} options
105
+ * @param {boolean} options.skipFetch - Skip fetching from origin (default: false)
106
+ * @returns {Promise<{path: string, branch: string}>}
107
+ */
108
+ export async function createWorktreeForBranch(directory, branch, worktreePath, options = {}) {
109
+ const { skipFetch = false } = options;
110
+
111
+ // Fetch latest from origin to ensure we have up-to-date default branch
112
+ if (!skipFetch) {
113
+ await safeFetchOrigin(directory);
114
+ }
115
+
116
+ const exists = await branchExists(directory, branch);
117
+ if (exists) {
118
+ await git(directory, `worktree add "${worktreePath}" "${branch}"`);
119
+ } else {
120
+ // Get the default branch from origin (main or master)
121
+ const defaultBranch = await getOriginDefaultBranch(directory);
122
+ // Base new branch on origin's default branch to avoid including unrelated commits from HEAD
123
+ // Use --no-track to prevent the new branch from tracking the start-point (main/master)
124
+ await git(directory, `worktree add --no-track -b "${branch}" "${worktreePath}" ${defaultBranch}`);
125
+ }
126
+ return { path: worktreePath, branch };
127
+ }
@@ -222,7 +222,7 @@ curl -X POST ${apiUrl}/api/projects/${projectId}/sessions \\
222
222
  -d '{"prompt": "Your task description here"}'
223
223
  \`\`\`
224
224
  Only \`prompt\` is required. Omitted settings are automatically derived from the project's session defaults, then system defaults, matching UI behavior.
225
- Optional override fields: \`name\`, \`mode\`, \`thinkingEnabled\` (boolean), \`effortLevel\` (low/medium/high/max/auto), \`model\`, \`providerId\`, \`gitBranch\`, \`gitMode\`, \`templateId\`, \`nextTemplateId\`, \`parentSessionId\` (to create a related follow-up session from the current session), \`startImmediately\`, \`scheduledAt\`, \`autoRescheduleEnabled\`, \`rescheduleDelayMinutes\`, \`rescheduleOnTokenLimit\`, \`rescheduleOnServiceError\`, \`maxRescheduleCount\`, \`maxTotalTokens\`, and \`rescheduleAtTokenCount\`.
225
+ Optional override fields: \`name\`, \`mode\`, \`thinkingEnabled\` (boolean), \`effortLevel\` (low/medium/high/max/auto), \`model\`, \`providerId\`, \`gitBranch\`, \`gitMode\`, \`templateId\`, \`nextTemplateId\`, \`parentSessionId\` (to create a related follow-up session from the current session), \`startImmediately\`, \`scheduledAt\` (ISO 8601 date-time string with timezone, e.g. \`"2026-06-12T14:00:00Z"\`), \`autoRescheduleEnabled\`, \`rescheduleDelayMinutes\`, \`rescheduleOnTokenLimit\`, \`rescheduleOnServiceError\`, \`maxRescheduleCount\`, \`maxTotalTokens\`, and \`rescheduleAtTokenCount\`.
226
226
 
227
227
  ### Send a Follow-up Message
228
228
  \`\`\`bash
@@ -1,5 +1,31 @@
1
1
  import { z } from 'zod';
2
2
 
3
+ const SCHEDULED_AT_FORMAT_MESSAGE = 'scheduledAt must be a valid ISO 8601 date-time string with a timezone';
4
+ const ISO_8601_DATE_TIME_WITH_TIMEZONE = /^(\d{4})-(\d{2})-(\d{2})T([01]\d|2[0-3]):([0-5]\d):([0-5]\d)(?:\.\d+)?(Z|[+-](?:[01]\d|2[0-3]):[0-5]\d)$/;
5
+
6
+ function hasValidDateParts(year, month, day) {
7
+ const parsed = new Date(Date.UTC(year, month - 1, day));
8
+ return parsed.getUTCFullYear() === year
9
+ && parsed.getUTCMonth() === month - 1
10
+ && parsed.getUTCDate() === day;
11
+ }
12
+
13
+ function isScheduledAtIsoString(value) {
14
+ const match = ISO_8601_DATE_TIME_WITH_TIMEZONE.exec(value);
15
+ if (!match) return false;
16
+
17
+ const year = Number(match[1]);
18
+ const month = Number(match[2]);
19
+ const day = Number(match[3]);
20
+ if (!hasValidDateParts(year, month, day)) return false;
21
+
22
+ return Number.isFinite(Date.parse(value));
23
+ }
24
+
25
+ const ScheduledAtIsoString = z.string().refine(isScheduledAtIsoString, {
26
+ message: SCHEDULED_AT_FORMAT_MESSAGE,
27
+ });
28
+
3
29
  export const CreateSessionRequest = z.object({
4
30
  prompt: z.string().min(1),
5
31
  name: z.string().optional(),
@@ -11,7 +37,7 @@ export const CreateSessionRequest = z.object({
11
37
  templateId: z.string().uuid().optional(), // Template to apply on session creation
12
38
  nextTemplateId: z.string().uuid().nullable().optional(),
13
39
  // Scheduling fields
14
- scheduledAt: z.number().optional(), // Unix timestamp in ms
40
+ scheduledAt: ScheduledAtIsoString.optional(),
15
41
  autoRescheduleEnabled: z.boolean().optional(),
16
42
  rescheduleDelayMinutes: z.number().min(5).max(1440).optional(), // 5 min to 24 hours
17
43
  rescheduleOnTokenLimit: z.boolean().optional(),
@@ -11,6 +11,9 @@ export const CreateSessionTemplateRequest = z.object({
11
11
  mode: z.enum(['plan', 'standard', 'yolo']).nullable().optional(),
12
12
  effortLevel: z.enum(['low', 'medium', 'high', 'max', 'auto']).nullable().optional(),
13
13
  targetLaneId: z.string().uuid().nullable().optional(), // Lane to place session in when created from this template
14
+ showInQuickResponses: z.boolean().optional(),
15
+ quickResponseAutoSubmit: z.boolean().optional(),
16
+ quickResponseSortOrder: z.number().int().optional(),
14
17
  });
15
18
 
16
19
  export const UpdateSessionTemplateRequest = z.object({
@@ -24,6 +27,9 @@ export const UpdateSessionTemplateRequest = z.object({
24
27
  mode: z.enum(['plan', 'standard', 'yolo']).nullable().optional(),
25
28
  effortLevel: z.enum(['low', 'medium', 'high', 'max', 'auto']).nullable().optional(),
26
29
  targetLaneId: z.string().uuid().nullable().optional(),
30
+ showInQuickResponses: z.boolean().optional(),
31
+ quickResponseAutoSubmit: z.boolean().optional(),
32
+ quickResponseSortOrder: z.number().int().optional(),
27
33
  });
28
34
 
29
35
  export const SessionTemplateResponse = z.object({
@@ -39,6 +45,10 @@ export const SessionTemplateResponse = z.object({
39
45
  mode: z.string().nullable(),
40
46
  effortLevel: z.enum(['low', 'medium', 'high', 'max', 'auto']).nullable(),
41
47
  targetLaneId: z.string().uuid().nullable(),
48
+ showInQuickResponses: z.boolean(),
49
+ quickResponseAutoSubmit: z.boolean(),
50
+ quickResponseSortOrder: z.number(),
51
+ legacyQuickResponseId: z.string().nullable(),
42
52
  createdAt: z.number(),
43
53
  updatedAt: z.number(),
44
54
  });
@@ -1 +1 @@
1
- import{u as K}from"./sessions-6zGUlFrt.js";import{u as Q}from"./commandButtons-BfqR-fqq.js";import{z as j,W as c,_ as X,o as Z,A as ee,l as te,a,c as l,b as f,F as A,r as C,E as O,u as S,t as b,d as se,w as re,f as ne,x as D,g as oe,n as ie,y as R,G as ae}from"./index-fK8FIZgP.js";import{a as x}from"./ApiClient-C3ztI9s9.js";import{S as le}from"./SessionCard-CjE1tXiT.js";import"./SessionLogStream-LlZ3z_Xj.js";function ce(){const{on:E,off:s}=j(),d=h=>p=>{const i=F=>{F.session&&p(F.session,F.projectId)};return E(h,i),()=>s(h,i)},I=d(c.SESSION_CREATED),w=d(c.SESSION_UPDATED);return{onSessionCreated:I,onSessionUpdated:w,onSessionDeleted:h=>{const p=i=>{i.sessionId&&h(i.sessionId,i.projectId)};return E(c.SESSION_DELETED,p),()=>s(c.SESSION_DELETED,p)},onSessionSummaryUpdated:h=>{const p=i=>{i.sessionId&&i.summary&&h(i.sessionId,i.summary,i.projectId)};return E(c.SESSION_SUMMARY_UPDATED,p),()=>s(c.SESSION_SUMMARY_UPDATED,p)}}}const de=["data-state"],ue={class:"filters-container"},fe={class:"status-filters"},Se=["onClick"],pe=["title"],me={key:0,class:"star-icon"},he={key:1,class:"star-icon star-crossed"},_e={key:2,class:"star-icon"},ye={key:0,class:"skeleton-list"},ve={key:1,class:"error-message"},Fe={key:2,class:"empty-state","data-testid":"active-sessions-empty"},Ee={key:3,class:"empty-state","data-testid":"active-sessions-empty"},Ie={key:4,class:"session-list"},we={__name:"ActiveSessionsView",setup(E){const s=K(),d=Q(),I=["waiting","stopped","error"],w=["running","starting"],v=ie(new Set),g=e=>{s.statusFilter===e?s.setStatusFilter(null):s.setStatusFilter(e)},h=()=>{s.starredFilter===null?s.setStarredFilter("starred"):s.starredFilter==="starred"?s.setStarredFilter("unstarred"):s.setStarredFilter(null)},p=D(()=>s.starredFilter==="starred"?"Showing starred sessions only. Click to filter unstarred.":s.starredFilter==="unstarred"?"Showing unstarred sessions only. Click to show all.":"Showing all sessions. Click to filter by starred."),i=D(()=>{let e=s.activeSessions;return s.statusFilter&&(e=e.filter(t=>{const r=t.status;return!!(s.statusFilter==="idle"&&I.includes(r)||s.statusFilter==="running"&&w.includes(r))})),s.starredFilter==="starred"?e=e.filter(t=>t.starred):s.starredFilter==="unstarred"&&(e=e.filter(t=>!t.starred)),e}),F=D(()=>s.loading?"loading":s.error?"error":s.activeSessions.length===0?"empty-all":i.value.length===0?"empty-filtered":"results"),_=R({}),u=R({}),m=R({});let N=null;const y=[],{onSessionCreated:P,onSessionUpdated:B,onSessionDeleted:L,onSessionSummaryUpdated:V}=ce();async function M(){const e=new Set(s.activeSessions.map(t=>t.projectId).filter(t=>t&&!v.value.has(t)));for(const t of e)try{await d.fetchButtons(t),await d.fetchLatestRunsForProject(t),v.value.add(t)}catch(r){console.error(`Failed to fetch buttons for project ${t}:`,r)}}function U(e,t,r){d.runs[e]||(d.runs[e]={runId:e,buttonId:t,sessionId:r,status:"running",output:"",exitCode:null,startedAt:Date.now(),outputTruncated:!1})}function H(e){["running","waiting","starting"].includes(e.status)&&(s.activeSessions.some(r=>r.id===e.id)||(s.activeSessions.unshift(e),e.projectId&&!v.value.has(e.projectId)&&(d.fetchButtons(e.projectId),v.value.add(e.projectId))))}function G(e){const t=["running","waiting","starting"].includes(e.status),r=s.activeSessions.findIndex(n=>n.id===e.id);t?r>=0?s.activeSessions[r]=e:s.activeSessions.unshift(e):r>=0&&(s.activeSessions.splice(r,1),delete _[e.id],delete u[e.id],delete m[e.id])}function W(e){const t=s.activeSessions.findIndex(r=>r.id===e);t>=0&&s.activeSessions.splice(t,1),delete _[e],delete u[e],delete m[e]}function Y(e,t){_[e]=t,u[e]=!1,m[e]=!1}function $(e,t,r){const n=o=>{U(o.runId,o.buttonId,o.sessionId),d.appendOutput(o.runId,o.output)};e(c.COMMAND_RUN_OUTPUT,n),r.push(()=>t(c.COMMAND_RUN_OUTPUT,n));const k=o=>{U(o.runId,o.buttonId,o.sessionId),d.completeRun(o.runId,o.exitCode,o.output)};e(c.COMMAND_RUN_COMPLETE,k),r.push(()=>t(c.COMMAND_RUN_COMPLETE,k));const T=o=>{U(o.runId,o.buttonId,o.sessionId),d.errorRun(o.runId,o.error)};e(c.COMMAND_RUN_ERROR,T),r.push(()=>t(c.COMMAND_RUN_ERROR,T))}Z(async()=>{s.restoreStatusFilter(),s.restoreStarredFilter(),await s.fetchActiveSessions(),await M(),y.push(P(H)),y.push(B(G)),y.push(L(W)),y.push(V(Y));const{on:e,off:t}=j();$(e,t,y),N=setInterval(()=>{s.fetchActiveSessions(!1)},3e4)}),ee(()=>{y.forEach(e=>e()),N&&clearInterval(N)}),te(()=>s.activeSessions,()=>{z(),M()},{immediate:!0});async function z(){const t=s.activeSessions.filter(r=>!_[r.id]&&!u[r.id]).map(r=>r.id);if(t.length!==0){for(const r of t)u[r]=!0,m[r]=!1;try{const r=await x.getSessionSummariesBatch(t);for(const n of t)r[n]&&(_[n]=r[n]),u[n]=!1}catch(r){console.warn("Failed to fetch summaries batch:",r.message);for(const n of t)m[n]=!0,u[n]=!1}}}async function q(e){var t;u[e]=!0,m[e]=!1;try{const r=await x.getSessionSummary(e);r&&(_[e]=r)}catch(r){((t=r.response)==null?void 0:t.status)!==404&&(console.warn(`Failed to fetch summary for session ${e}:`,r.message),m[e]=!0)}finally{u[e]=!1}}async function J(e){m[e]=!1,await q(e)}return(e,t)=>{const r=oe("router-link");return a(),l("div",{class:"container","data-testid":"active-sessions-view","data-state":F.value},[t[3]||(t[3]=f("div",{class:"page-header"},[f("div",null,[f("p",{class:"page-description"}," All active sessions across your projects ")])],-1)),f("div",ue,[f("div",fe,[(a(),l(A,null,C(["running","idle"],n=>f("button",{key:n,class:O(["filter-btn",{active:S(s).statusFilter===n}]),onClick:k=>g(n)},b(n),11,Se)),64)),f("button",{class:O(["filter-btn star-btn",{"star-filter-active":S(s).starredFilter==="starred","star-filter-unstarred":S(s).starredFilter==="unstarred","star-filter-all":S(s).starredFilter===null}]),title:p.value,onClick:h},[S(s).starredFilter==="starred"?(a(),l("span",me,"⭐")):S(s).starredFilter==="unstarred"?(a(),l("span",he,"⭐")):(a(),l("span",_e,"☆"))],10,pe)])]),S(s).loading?(a(),l("div",ye,[(a(),l(A,null,C(3,n=>f("div",{key:n,class:"skeleton card",style:{height:"120px"}})),64))])):S(s).error?(a(),l("div",ve,b(S(s).error),1)):S(s).activeSessions.length===0?(a(),l("div",Fe,[t[1]||(t[1]=f("p",null,"No active sessions. All sessions are completed or there are no sessions yet.",-1)),se(r,{to:"/",class:"btn btn-primary"},{default:re(()=>[...t[0]||(t[0]=[ne(" View Projects ",-1)])]),_:1})])):i.value.length===0?(a(),l("div",Ee,[...t[2]||(t[2]=[f("p",null,"No sessions match the current filter.",-1)])])):(a(),l("div",Ie,[(a(!0),l(A,null,C(i.value,n=>(a(),ae(le,{key:n.id,session:n,"show-project":!0,"show-summary":!0,summary:_[n.id],"summary-loading":u[n.id],"summary-error":m[n.id],onRetrySummary:J},null,8,["session","summary","summary-loading","summary-error"]))),128))]))],8,de)}}},Re=X(we,[["__scopeId","data-v-9a53578e"]]);export{Re as default};
1
+ import{u as K}from"./sessions-Nq5VafSf.js";import{u as Q}from"./commandButtons-CoU3G4zK.js";import{E as j,W as c,_ as X,o as Z,G as ee,l as te,a,c as l,b as f,F as C,r as D,J as O,u as S,t as b,d as se,w as re,f as ne,z as A,g as oe,n as ie,C as R,x as ae}from"./index-C2QFVD7d.js";import{a as x}from"./ApiClient-DfbJwzpz.js";import{S as le}from"./SessionCard-Bw77-KwD.js";import"./SessionLogStream-5NfVr9pF.js";function ce(){const{on:E,off:s}=j(),d=h=>p=>{const i=F=>{F.session&&p(F.session,F.projectId)};return E(h,i),()=>s(h,i)},I=d(c.SESSION_CREATED),w=d(c.SESSION_UPDATED);return{onSessionCreated:I,onSessionUpdated:w,onSessionDeleted:h=>{const p=i=>{i.sessionId&&h(i.sessionId,i.projectId)};return E(c.SESSION_DELETED,p),()=>s(c.SESSION_DELETED,p)},onSessionSummaryUpdated:h=>{const p=i=>{i.sessionId&&i.summary&&h(i.sessionId,i.summary,i.projectId)};return E(c.SESSION_SUMMARY_UPDATED,p),()=>s(c.SESSION_SUMMARY_UPDATED,p)}}}const de=["data-state"],ue={class:"filters-container"},fe={class:"status-filters"},Se=["onClick"],pe=["title"],me={key:0,class:"star-icon"},he={key:1,class:"star-icon star-crossed"},_e={key:2,class:"star-icon"},ve={key:0,class:"skeleton-list"},ye={key:1,class:"error-message"},Fe={key:2,class:"empty-state","data-testid":"active-sessions-empty"},Ee={key:3,class:"empty-state","data-testid":"active-sessions-empty"},Ie={key:4,class:"session-list"},we={__name:"ActiveSessionsView",setup(E){const s=K(),d=Q(),I=["waiting","stopped","error"],w=["running","starting"],y=ie(new Set),g=e=>{s.statusFilter===e?s.setStatusFilter(null):s.setStatusFilter(e)},h=()=>{s.starredFilter===null?s.setStarredFilter("starred"):s.starredFilter==="starred"?s.setStarredFilter("unstarred"):s.setStarredFilter(null)},p=A(()=>s.starredFilter==="starred"?"Showing starred sessions only. Click to filter unstarred.":s.starredFilter==="unstarred"?"Showing unstarred sessions only. Click to show all.":"Showing all sessions. Click to filter by starred."),i=A(()=>{let e=s.activeSessions;return s.statusFilter&&(e=e.filter(t=>{const r=t.status;return!!(s.statusFilter==="idle"&&I.includes(r)||s.statusFilter==="running"&&w.includes(r))})),s.starredFilter==="starred"?e=e.filter(t=>t.starred):s.starredFilter==="unstarred"&&(e=e.filter(t=>!t.starred)),e}),F=A(()=>s.loading?"loading":s.error?"error":s.activeSessions.length===0?"empty-all":i.value.length===0?"empty-filtered":"results"),_=R({}),u=R({}),m=R({});let N=null;const v=[],{onSessionCreated:P,onSessionUpdated:B,onSessionDeleted:L,onSessionSummaryUpdated:V}=ce();async function M(){const e=new Set(s.activeSessions.map(t=>t.projectId).filter(t=>t&&!y.value.has(t)));for(const t of e)try{await d.fetchButtons(t),await d.fetchLatestRunsForProject(t),y.value.add(t)}catch(r){console.error(`Failed to fetch buttons for project ${t}:`,r)}}function U(e,t,r){d.runs[e]||(d.runs[e]={runId:e,buttonId:t,sessionId:r,status:"running",output:"",exitCode:null,startedAt:Date.now(),outputTruncated:!1})}function H(e){["running","waiting","starting"].includes(e.status)&&(s.activeSessions.some(r=>r.id===e.id)||(s.activeSessions.unshift(e),e.projectId&&!y.value.has(e.projectId)&&(d.fetchButtons(e.projectId),y.value.add(e.projectId))))}function G(e){const t=["running","waiting","starting"].includes(e.status),r=s.activeSessions.findIndex(n=>n.id===e.id);t?r>=0?s.activeSessions[r]=e:s.activeSessions.unshift(e):r>=0&&(s.activeSessions.splice(r,1),delete _[e.id],delete u[e.id],delete m[e.id])}function W(e){const t=s.activeSessions.findIndex(r=>r.id===e);t>=0&&s.activeSessions.splice(t,1),delete _[e],delete u[e],delete m[e]}function Y(e,t){_[e]=t,u[e]=!1,m[e]=!1}function $(e,t,r){const n=o=>{U(o.runId,o.buttonId,o.sessionId),d.appendOutput(o.runId,o.output)};e(c.COMMAND_RUN_OUTPUT,n),r.push(()=>t(c.COMMAND_RUN_OUTPUT,n));const k=o=>{U(o.runId,o.buttonId,o.sessionId),d.completeRun(o.runId,o.exitCode,o.output)};e(c.COMMAND_RUN_COMPLETE,k),r.push(()=>t(c.COMMAND_RUN_COMPLETE,k));const T=o=>{U(o.runId,o.buttonId,o.sessionId),d.errorRun(o.runId,o.error)};e(c.COMMAND_RUN_ERROR,T),r.push(()=>t(c.COMMAND_RUN_ERROR,T))}Z(async()=>{s.restoreStatusFilter(),s.restoreStarredFilter(),await s.fetchActiveSessions(),await M(),v.push(P(H)),v.push(B(G)),v.push(L(W)),v.push(V(Y));const{on:e,off:t}=j();$(e,t,v),N=setInterval(()=>{s.fetchActiveSessions(!1)},3e4)}),ee(()=>{v.forEach(e=>e()),N&&clearInterval(N)}),te(()=>s.activeSessions,()=>{z(),M()},{immediate:!0});async function z(){const t=s.activeSessions.filter(r=>!_[r.id]&&!u[r.id]).map(r=>r.id);if(t.length!==0){for(const r of t)u[r]=!0,m[r]=!1;try{const r=await x.getSessionSummariesBatch(t);for(const n of t)r[n]&&(_[n]=r[n]),u[n]=!1}catch(r){console.warn("Failed to fetch summaries batch:",r.message);for(const n of t)m[n]=!0,u[n]=!1}}}async function q(e){var t;u[e]=!0,m[e]=!1;try{const r=await x.getSessionSummary(e);r&&(_[e]=r)}catch(r){((t=r.response)==null?void 0:t.status)!==404&&(console.warn(`Failed to fetch summary for session ${e}:`,r.message),m[e]=!0)}finally{u[e]=!1}}async function J(e){m[e]=!1,await q(e)}return(e,t)=>{const r=oe("router-link");return a(),l("div",{class:"container","data-testid":"active-sessions-view","data-state":F.value},[t[3]||(t[3]=f("div",{class:"page-header"},[f("div",null,[f("p",{class:"page-description"}," All active sessions across your projects ")])],-1)),f("div",ue,[f("div",fe,[(a(),l(C,null,D(["running","idle"],n=>f("button",{key:n,class:O(["filter-btn",{active:S(s).statusFilter===n}]),onClick:k=>g(n)},b(n),11,Se)),64)),f("button",{class:O(["filter-btn star-btn",{"star-filter-active":S(s).starredFilter==="starred","star-filter-unstarred":S(s).starredFilter==="unstarred","star-filter-all":S(s).starredFilter===null}]),title:p.value,onClick:h},[S(s).starredFilter==="starred"?(a(),l("span",me,"⭐")):S(s).starredFilter==="unstarred"?(a(),l("span",he,"⭐")):(a(),l("span",_e,"☆"))],10,pe)])]),S(s).loading?(a(),l("div",ve,[(a(),l(C,null,D(3,n=>f("div",{key:n,class:"skeleton card",style:{height:"120px"}})),64))])):S(s).error?(a(),l("div",ye,b(S(s).error),1)):S(s).activeSessions.length===0?(a(),l("div",Fe,[t[1]||(t[1]=f("p",null,"No active sessions. All sessions are completed or there are no sessions yet.",-1)),se(r,{to:"/",class:"btn btn-primary"},{default:re(()=>[...t[0]||(t[0]=[ne(" View Projects ",-1)])]),_:1})])):i.value.length===0?(a(),l("div",Ee,[...t[2]||(t[2]=[f("p",null,"No sessions match the current filter.",-1)])])):(a(),l("div",Ie,[(a(!0),l(C,null,D(i.value,n=>(a(),ae(le,{key:n.id,session:n,"show-project":!0,"show-summary":!0,summary:_[n.id],"summary-loading":u[n.id],"summary-error":m[n.id],onRetrySummary:J},null,8,["session","summary","summary-loading","summary-error"]))),128))]))],8,de)}}},Re=X(we,[["__scopeId","data-v-9a53578e"]]);export{Re as default};