circuschief 0.2.1 → 0.4.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 (121) hide show
  1. package/package.json +1 -1
  2. package/packages/server/src/api/git.js +15 -0
  3. package/packages/server/src/api/index.js +13 -2
  4. package/packages/server/src/api/projects-commandButtons.js +96 -0
  5. package/packages/server/src/api/projects-session-helpers.js +51 -33
  6. package/packages/server/src/api/projects.js +109 -113
  7. package/packages/server/src/api/sessions-archive.js +2 -1
  8. package/packages/server/src/api/sessions-lifecycle.js +2 -1
  9. package/packages/server/src/api/sessions-patch.js +2 -0
  10. package/packages/server/src/config.js +6 -0
  11. package/packages/server/src/database.js +1 -0
  12. package/packages/server/src/db/DatabaseManager.js +12 -1
  13. package/packages/server/src/db/ProjectRepository.js +22 -5
  14. package/packages/server/src/db/index.js +4 -0
  15. package/packages/server/src/db/migrations/index.js +7 -0
  16. package/packages/server/src/db/migrations/miscMigrations.js +78 -1
  17. package/packages/server/src/db/migrations/projectsMigrations.js +4 -0
  18. package/packages/server/src/index.js +10 -4
  19. package/packages/server/src/services/gitService.js +42 -6
  20. package/packages/server/src/services/gitSessionSetup.js +4 -3
  21. package/packages/server/src/services/kanbanTriggers.js +1 -0
  22. package/packages/server/src/services/schedulerService.js +32 -0
  23. package/packages/server/src/services/sessionExecution.js +2 -2
  24. package/packages/server/src/services/templateTriggerService.js +1 -0
  25. package/packages/shared/src/contracts/projects.js +4 -1
  26. package/packages/shared/src/types.js +4 -3
  27. package/packages/web/dist/assets/ActiveSessionsView-BVco8bPU.css +1 -0
  28. package/packages/web/dist/assets/ActiveSessionsView-D1daFFvI.js +1 -0
  29. package/packages/web/dist/assets/{AgentLogsView-D4l0N9ZA.js → AgentLogsView-B_NDIx2_.js} +1 -1
  30. package/packages/web/dist/assets/{ApiClient-Dbs1H78V.js → ApiClient-CcqJ-GAv.js} +1 -1
  31. package/packages/web/dist/assets/ArchiveConfirmModal-BHqbCCX2.js +1 -0
  32. package/packages/web/dist/assets/{ArchiveConfirmModal-CQZeuYBz.css → ArchiveConfirmModal-BQ-4gI0R.css} +1 -1
  33. package/packages/web/dist/assets/CommandButtonDetailView-B9crZey8.js +1 -0
  34. package/packages/web/dist/assets/EffortLevelSelector-HuOPegKI.js +1 -0
  35. package/packages/web/dist/assets/{GeneralSettingsView-BqCzCX-z.js → GeneralSettingsView-CzBagrDs.js} +1 -1
  36. package/packages/web/dist/assets/{PathChooser-CXFxb8Oj.js → InputWithButton-Cplkm8Ze.js} +1 -1
  37. package/packages/web/dist/assets/InputWithButton-cYdrEmTs.css +1 -0
  38. package/packages/web/dist/assets/InterpolationHelp-bG_y10VY.js +1 -0
  39. package/packages/web/dist/assets/MarkdownEditor-CXDVTLvp.js +2 -0
  40. package/packages/web/dist/assets/{ModelSelector-DSxaZWBL.js → ModelSelector-CgpqdZtV.js} +1 -1
  41. package/packages/web/dist/assets/{NewSessionView-BsI7JtO9.js → NewSessionView-91V-4qLi.js} +2 -2
  42. package/packages/web/dist/assets/{NewSessionView-Byoi1XdQ.css → NewSessionView-D_Hi7M9g.css} +1 -1
  43. package/packages/web/dist/assets/ProjectEditView-C3bOYnD6.js +1 -0
  44. package/packages/web/dist/assets/ProjectEditView-CpeKj-_w.css +1 -0
  45. package/packages/web/dist/assets/ProjectListView-BEo1p4dO.js +1 -0
  46. package/packages/web/dist/assets/ProjectListView-CumumZN-.css +1 -0
  47. package/packages/web/dist/assets/ProjectNewView-CXGjy3OL.js +1 -0
  48. package/packages/web/dist/assets/ProjectNewView-CaLXfFzd.css +1 -0
  49. package/packages/web/dist/assets/{ProvidersView-CgAr0qms.js → ProvidersView-CMAnPTEX.js} +1 -1
  50. package/packages/web/dist/assets/{ProvidersView-B_QQF3RM.css → ProvidersView-uD8SKWpA.css} +1 -1
  51. package/packages/web/dist/assets/{QuickResponseSettings-uDDpwaza.js → QuickResponseSettings-Ds4X-J-t.js} +1 -1
  52. package/packages/web/dist/assets/{QuickResponsesPanel-D0qs0Fm_.js → QuickResponsesPanel-BY2v23c5.js} +1 -1
  53. package/packages/web/dist/assets/{ResizableTextarea-_kHi1Mg3.js → ResizableTextarea-C2s6_7X9.js} +1 -1
  54. package/packages/web/dist/assets/{SessionCard-Be1-bK0C.js → SessionCard-C7vFzR16.js} +1 -1
  55. package/packages/web/dist/assets/{SessionDetailView-mnGRMaLY.css → SessionDetailView-BL83oPiI.css} +1 -1
  56. package/packages/web/dist/assets/SessionDetailView-D3f0FEV1.js +36 -0
  57. package/packages/web/dist/assets/{SessionFormOptions-DvhOyP6z.js → SessionFormOptions-3LfqiLiR.js} +1 -1
  58. package/packages/web/dist/assets/{SessionListView-CuHsWj85.js → SessionListView-NGW-u434.js} +1 -1
  59. package/packages/web/dist/assets/{SessionLogStream-Da_GniUZ.js → SessionLogStream-NR-AS676.js} +6 -6
  60. package/packages/web/dist/assets/SettingsView-DKINDb2z.js +1 -0
  61. package/packages/web/dist/assets/{SlashCommandWizard-B_8ifpxN.js → SlashCommandWizard-PXipO1yA.js} +1 -1
  62. package/packages/web/dist/assets/{SummarySettingsView-KvgSGHdd.js → SummarySettingsView-D_2bSsYD.js} +1 -1
  63. package/packages/web/dist/assets/{TemplateDetailView-BhOjYIvS.js → TemplateDetailView-C5rbgXwU.js} +1 -1
  64. package/packages/web/dist/assets/{commandButtons-B4OYZP0J.js → commandButtons-D_-wR8zJ.js} +1 -1
  65. package/packages/web/dist/assets/index-BCazaXF8.js +1 -0
  66. package/packages/web/dist/assets/index-BmQRt229.js +3 -0
  67. package/packages/web/dist/assets/index-CCQGqJXX.js +1 -0
  68. package/packages/web/dist/assets/index-CcCiJkwz.js +1 -0
  69. package/packages/web/dist/assets/index-Cs-001Bx.js +1 -0
  70. package/packages/web/dist/assets/index-CxiSnR0R.js +1 -0
  71. package/packages/web/dist/assets/index-D-lQSDZh.js +1 -0
  72. package/packages/web/dist/assets/{index-CSOPrlmq.js → index-D1Lg5reX.js} +3 -3
  73. package/packages/web/dist/assets/index-D9VgH58U.js +1 -0
  74. package/packages/web/dist/assets/index-D9Z6zsGS.js +1 -0
  75. package/packages/web/dist/assets/index-DP2i58hO.js +1 -0
  76. package/packages/web/dist/assets/index-DaL3nu0U.js +1 -0
  77. package/packages/web/dist/assets/index-Dfy1JGZs.js +1 -0
  78. package/packages/web/dist/assets/index-DgIOe3cM.js +1 -0
  79. package/packages/web/dist/assets/index-Dp3Eg3c0.js +1 -0
  80. package/packages/web/dist/assets/{index-BHVnr8MO.js → index-DveLfEiG.js} +2 -2
  81. package/packages/web/dist/assets/index-DyQ22-ut.js +1 -0
  82. package/packages/web/dist/assets/{index-BqVgX_Jy.js → index-Krfrs3sc.js} +3 -3
  83. package/packages/web/dist/assets/index-gylMFbgn.js +7 -0
  84. package/packages/web/dist/assets/{projects-B2du-GX8.js → projects-CMJJca64.js} +1 -1
  85. package/packages/web/dist/assets/{providers-B__J6FX0.js → providers-BCtNZWYw.js} +1 -1
  86. package/packages/web/dist/assets/{sessions-VDrd87yA.js → sessions-CoRS-wuR.js} +1 -1
  87. package/packages/web/dist/assets/{settings-CZ7Pc-Pt.js → settings-BN_W4nwV.js} +1 -1
  88. package/packages/web/dist/assets/useSummaryHelpers-GVg7sMWF.js +1 -0
  89. package/packages/web/dist/index.html +1 -1
  90. package/packages/web/dist/assets/ActiveSessionsView-3697sD8N.js +0 -1
  91. package/packages/web/dist/assets/ActiveSessionsView-DfYXc6dz.css +0 -1
  92. package/packages/web/dist/assets/ArchiveConfirmModal-Bv3vGOMM.js +0 -1
  93. package/packages/web/dist/assets/CommandButtonDetailView-Bk_SHxpu.js +0 -1
  94. package/packages/web/dist/assets/EffortLevelSelector-VfBEelvO.js +0 -1
  95. package/packages/web/dist/assets/InterpolationHelp-Dc1Y0T6v.js +0 -1
  96. package/packages/web/dist/assets/MarkdownEditor-DwBQkZbs.js +0 -2
  97. package/packages/web/dist/assets/PathChooser-BoMGzeg2.css +0 -1
  98. package/packages/web/dist/assets/ProjectEditView-Bes4Mib4.js +0 -1
  99. package/packages/web/dist/assets/ProjectEditView-DNwBUNRk.css +0 -1
  100. package/packages/web/dist/assets/ProjectListView-C55H1JHQ.css +0 -1
  101. package/packages/web/dist/assets/ProjectListView-DzEu-C36.js +0 -1
  102. package/packages/web/dist/assets/ProjectNewView-CpgE4R-l.css +0 -1
  103. package/packages/web/dist/assets/ProjectNewView-Cv-iEAgl.js +0 -1
  104. package/packages/web/dist/assets/SessionDetailView-DUYb7qTA.js +0 -36
  105. package/packages/web/dist/assets/SettingsView-5RDCXNUa.js +0 -1
  106. package/packages/web/dist/assets/index-80Qu7W6P.js +0 -1
  107. package/packages/web/dist/assets/index-B8_Iqwcq.js +0 -1
  108. package/packages/web/dist/assets/index-B9JErft2.js +0 -1
  109. package/packages/web/dist/assets/index-BarVnQIj.js +0 -3
  110. package/packages/web/dist/assets/index-BsvRdU0B.js +0 -1
  111. package/packages/web/dist/assets/index-Bugg2M-E.js +0 -1
  112. package/packages/web/dist/assets/index-C2Pjy-M8.js +0 -1
  113. package/packages/web/dist/assets/index-CS_wb_Vj.js +0 -1
  114. package/packages/web/dist/assets/index-ClzNIdCp.js +0 -1
  115. package/packages/web/dist/assets/index-Cn9Ajkye.js +0 -1
  116. package/packages/web/dist/assets/index-CucpVX4L.js +0 -1
  117. package/packages/web/dist/assets/index-D9hZYvW3.js +0 -1
  118. package/packages/web/dist/assets/index-DA0dK_PG.js +0 -7
  119. package/packages/web/dist/assets/index-DgpSn-jR.js +0 -1
  120. package/packages/web/dist/assets/index-HZwIyC9t.js +0 -1
  121. package/packages/web/dist/assets/index-SS3wA2sI.js +0 -1
@@ -19,7 +19,13 @@ export class ProjectRepository extends BaseRepository {
19
19
  onSessionDeleted: row.on_session_deleted,
20
20
  prPollInterval: row.pr_poll_interval,
21
21
  repoUrl: row.repo_url,
22
- kanbanEnabled: row.kanban_enabled === undefined ? true : Boolean(row.kanban_enabled),
22
+ worktreePath: row.worktree_path,
23
+ // Kanban is an experimental feature, disabled by default for new projects.
24
+ // Fallback to `false` when the column is absent so missing data is
25
+ // treated as opt-out (consistent with create()).
26
+ kanbanEnabled: row.kanban_enabled === undefined ? false : Boolean(row.kanban_enabled),
27
+ sessionCount: row.session_count ?? 0,
28
+ lastActivityAt: row.last_activity_at ?? null,
23
29
  createdAt: row.created_at,
24
30
  updatedAt: row.updated_at,
25
31
  };
@@ -33,12 +39,13 @@ export class ProjectRepository extends BaseRepository {
33
39
  onSessionDeleted = null,
34
40
  prPollInterval = 60000,
35
41
  repoUrl = null,
36
- kanbanEnabled = true,
42
+ kanbanEnabled = false,
43
+ worktreePath = null,
37
44
  } = options;
38
45
  this.db
39
46
  .prepare(
40
- `INSERT INTO projects (id, name, working_directory, system_prompt, on_session_created, on_session_deleted, pr_poll_interval, repo_url, kanban_enabled, created_at, updated_at)
41
- VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`
47
+ `INSERT INTO projects (id, name, working_directory, system_prompt, on_session_created, on_session_deleted, pr_poll_interval, repo_url, kanban_enabled, worktree_path, created_at, updated_at)
48
+ VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`
42
49
  )
43
50
  .run(
44
51
  id,
@@ -50,6 +57,7 @@ export class ProjectRepository extends BaseRepository {
50
57
  prPollInterval,
51
58
  repoUrl,
52
59
  kanbanEnabled ? 1 : 0,
60
+ worktreePath,
53
61
  now,
54
62
  now
55
63
  );
@@ -57,7 +65,15 @@ export class ProjectRepository extends BaseRepository {
57
65
  }
58
66
 
59
67
  getAll() {
60
- const rows = this.db.prepare('SELECT * FROM projects ORDER BY updated_at DESC').all();
68
+ const rows = this.db.prepare(`
69
+ SELECT p.*,
70
+ COUNT(CASE WHEN s.archived = 0 THEN s.id END) as session_count,
71
+ MAX(s.updated_at) as last_activity_at
72
+ FROM projects p
73
+ LEFT JOIN sessions s ON s.project_id = p.id
74
+ GROUP BY p.id
75
+ ORDER BY p.updated_at DESC
76
+ `).all();
61
77
  return this.mapAll(rows);
62
78
  }
63
79
 
@@ -73,6 +89,7 @@ export class ProjectRepository extends BaseRepository {
73
89
  onSessionDeleted: { column: 'on_session_deleted' },
74
90
  prPollInterval: { column: 'pr_poll_interval' },
75
91
  repoUrl: { column: 'repo_url' },
92
+ worktreePath: { column: 'worktree_path' },
76
93
  kanbanEnabled: { column: 'kanban_enabled', transform: (v) => v ? 1 : 0 },
77
94
  };
78
95
 
@@ -91,6 +91,10 @@ export function closeDatabase() {
91
91
  return databaseManager.close();
92
92
  }
93
93
 
94
+ export function getDbPath() {
95
+ return databaseManager.getPath();
96
+ }
97
+
94
98
  export function generateId() {
95
99
  return databaseManager.generateId();
96
100
  }
@@ -65,6 +65,7 @@ export const allMigrations = validateMigrations([
65
65
  p.get('projects-add-on_session_created'),
66
66
  p.get('projects-add-on_session_deleted'),
67
67
  p.get('projects-add-repo_url'),
68
+ p.get('projects-add-worktree_path'),
68
69
  p.get('projects-drop-summary-columns'),
69
70
 
70
71
  // --- Sessions scheduling columns ---
@@ -196,4 +197,10 @@ export const allMigrations = validateMigrations([
196
197
 
197
198
  // --- Seed default global quick responses ---
198
199
  m.get('quick_responses-seed-defaults'),
200
+
201
+ // --- Seed default global session templates ---
202
+ m.get('session_templates-seed-defaults'),
203
+
204
+ // --- Update built-in Opus model to 4.7 ---
205
+ m.get('providers-update-built-in-opus-4-7'),
199
206
  ]);
@@ -28,7 +28,8 @@ function seedBuiltInProvider(db) {
28
28
  const defaultModels = [
29
29
  { id: 'anthropic-haiku', modelId: 'claude-haiku-4-5-20251001', displayName: 'Haiku 4.5', description: 'Fast & lightweight', tier: 'haiku' },
30
30
  { id: 'anthropic-sonnet', modelId: 'claude-sonnet-4-6', displayName: 'Sonnet 4.6', description: 'Balanced', tier: 'sonnet' },
31
- { id: 'anthropic-opus', modelId: 'claude-opus-4-6', displayName: 'Opus 4.6', description: 'Most capable (default)', tier: 'opus' },
31
+ { id: 'anthropic-opus', modelId: 'claude-opus-4-6', displayName: 'Opus 4.6', description: 'Previous generation', tier: 'opus' },
32
+ { id: 'anthropic-opus-4-7', modelId: 'claude-opus-4-7', displayName: 'Opus 4.7', description: 'Most capable (default)', tier: 'opus' },
32
33
  ];
33
34
 
34
35
  const insertModel = db.prepare(
@@ -61,6 +62,55 @@ function updateBuiltInModels(db) {
61
62
  ).run('claude-opus-4-6', 'Opus 4.6', providerId, 'anthropic-opus');
62
63
  }
63
64
 
65
+ /**
66
+ * Prompt strings for the default global session templates.
67
+ * Exported so tests can assert verbatim equality.
68
+ */
69
+ export const DEFAULT_SESSION_TEMPLATE_PROMPTS = {
70
+ REVIEW: `Review the plan on the canvas. If there's more than one plan then review the most recently updated plan. if there are no plans on the canvas then look for the most recently updated plan on the root session canvas.
71
+
72
+ Make sure there are tests explicitly called out for all changes. Make sure that all context necessary to hand off the task is included in the plan.
73
+
74
+ Are there any gaps in the plan? Is test coverage spelled out explicitly? Does the code match the assumptions in the plan?
75
+
76
+ Update the plan according to your review recommendations.`,
77
+ IMPLEMENT: `Implement the plan on the canvas. If there's more than one plan on the canvas then use the most recently updated plan. If you don't see a plan on the canvas then look at the parent session's canvas.`,
78
+ PR: `Ensure all relevant changes are committed and pushed. Then determine the session's goals. You can typically find details about the goals of the session by looking at the most recently modified markdown documents on the root session's canvas - these are typically plans that were implemented during the session. You can also look at the root session's summary, but don't trigger a new summary to be created if the summary is missing.
79
+
80
+ Create a draft pr and ensure all changes are committed and pushed.`,
81
+ };
82
+
83
+ /**
84
+ * Seed default global session templates when no global template exists.
85
+ * Idempotent: if any global session template already exists, does nothing.
86
+ */
87
+ function seedDefaultSessionTemplates(db) {
88
+ const count = db.prepare(
89
+ 'SELECT COUNT(*) AS cnt FROM session_templates WHERE project_id IS NULL'
90
+ ).get().cnt;
91
+ if (count > 0) return;
92
+
93
+ const defaults = [
94
+ { name: 'Review the plan', prompt: DEFAULT_SESSION_TEMPLATE_PROMPTS.REVIEW },
95
+ { name: 'Implement the plan on the canvas', prompt: DEFAULT_SESSION_TEMPLATE_PROMPTS.IMPLEMENT },
96
+ { name: 'Create/update PR', prompt: DEFAULT_SESSION_TEMPLATE_PROMPTS.PR },
97
+ ];
98
+
99
+ const stmt = db.prepare(`
100
+ INSERT INTO session_templates (
101
+ id, project_id, name, prompt,
102
+ next_template_id, thinking_enabled,
103
+ git_branch, git_mode, model, mode, effort_level, target_lane_id,
104
+ created_at, updated_at
105
+ ) VALUES (?, NULL, ?, ?, NULL, 1, NULL, NULL, NULL, 'yolo', NULL, NULL, ?, ?)
106
+ `);
107
+
108
+ const now = Date.now();
109
+ for (const item of defaults) {
110
+ stmt.run(randomUUID(), item.name, item.prompt, now, now);
111
+ }
112
+ }
113
+
64
114
  /**
65
115
  * Seed default global quick responses when the table is empty.
66
116
  */
@@ -239,4 +289,31 @@ export const miscMigrations = [
239
289
  name: 'quick_responses-seed-defaults',
240
290
  up(db) { seedDefaultQuickResponses(db); },
241
291
  },
292
+
293
+ // --- Seed default global session templates ---
294
+ {
295
+ name: 'session_templates-seed-defaults',
296
+ up(db) { seedDefaultSessionTemplates(db); },
297
+ },
298
+
299
+ // --- Add Opus 4.7 as a new built-in model (keep Opus 4.6 for existing sessions) ---
300
+ {
301
+ name: 'providers-update-built-in-opus-4-7',
302
+ up(db) {
303
+ const providerId = 'anthropic-default';
304
+
305
+ // Mark existing Opus 4.6 row as previous generation
306
+ db.prepare(
307
+ `UPDATE provider_models
308
+ SET description = ?
309
+ WHERE provider_id = ? AND id = ?`
310
+ ).run('Previous generation', providerId, 'anthropic-opus');
311
+
312
+ // Insert new Opus 4.7 row (OR IGNORE in case seed already created it)
313
+ db.prepare(
314
+ `INSERT OR IGNORE INTO provider_models (id, provider_id, model_id, display_name, description, tier, created_at)
315
+ VALUES (?, ?, ?, ?, ?, ?, ?)`
316
+ ).run('anthropic-opus-4-7', providerId, 'claude-opus-4-7', 'Opus 4.7', 'Most capable (default)', 'opus', Date.now());
317
+ },
318
+ },
242
319
  ];
@@ -43,6 +43,10 @@ export const projectsMigrations = [
43
43
  name: 'projects-add-repo_url',
44
44
  up(db) { addColumnIfMissing(db, 'projects', 'repo_url', 'TEXT'); },
45
45
  },
46
+ {
47
+ name: 'projects-add-worktree_path',
48
+ up(db) { addColumnIfMissing(db, 'projects', 'worktree_path', 'TEXT'); },
49
+ },
46
50
  {
47
51
  name: 'projects-drop-summary-columns',
48
52
  up(db) { migrateProjectsDropSummaryColumns(db); },
@@ -1,5 +1,7 @@
1
1
  import { createServer } from 'http';
2
2
  import { execSync } from 'child_process';
3
+ import { mkdirSync } from 'fs';
4
+ import { dirname } from 'path';
3
5
  import { createApp } from './app.js';
4
6
  import { initDatabase } from './database.js';
5
7
  import { initWebSocket, webSocketManager } from './websocket.js';
@@ -11,6 +13,7 @@ import { schedulerService } from './services/schedulerService.js';
11
13
  import * as sessionManager from './services/sessionManager.js';
12
14
  import { clearScheduledTimers } from './services/summaryService.js';
13
15
  import { commandRunner } from './services/commandRunner.js';
16
+ import { getDefaultDbPath } from './config.js';
14
17
 
15
18
  /**
16
19
  * Validate Node.js environment at startup.
@@ -32,7 +35,7 @@ function validateNodeEnvironment() {
32
35
  const { port, disableAnalytics } = parseCliOptions();
33
36
  process.env.PORT = String(port);
34
37
  const production = process.env.NODE_ENV === 'production';
35
- const dbPath = process.env.DB_PATH || 'circuschief.db';
38
+ const dbPath = process.env.DB_PATH || getDefaultDbPath();
36
39
 
37
40
  // Catch uncaught exceptions
38
41
  process.on('uncaughtException', (err) => {
@@ -45,9 +48,13 @@ process.on('unhandledRejection', (reason, promise) => {
45
48
  // Validate Node.js environment
46
49
  validateNodeEnvironment();
47
50
 
51
+ // Ensure the database directory exists
52
+ mkdirSync(dirname(dbPath), { recursive: true });
53
+
48
54
  // Initialize database
49
55
  initDatabase(dbPath);
50
56
  console.log(`Database initialized: ${dbPath}`);
57
+ console.log(`VCR_MODE: ${process.env.VCR_MODE || '(unset)'}`);
51
58
 
52
59
  // Apply --no-analytics flag to persisted settings
53
60
  if (disableAnalytics) {
@@ -64,9 +71,8 @@ const server = createServer(app);
64
71
  // Initialize WebSocket for app
65
72
  initWebSocket(server);
66
73
 
67
- // Initialize and start scheduler service
68
- schedulerService.initialize(sessionManager);
69
- schedulerService.start();
74
+ // Initialize and start scheduler service (gated off under VCR_MODE)
75
+ schedulerService.startIfEnabled(sessionManager);
70
76
 
71
77
  // Start PR status polling service
72
78
  prStatusService.start();
@@ -1,5 +1,7 @@
1
1
  import { exec } from 'child_process';
2
2
  import { promisify } from 'util';
3
+ import path from 'path';
4
+ import { realpath } from 'fs/promises';
3
5
 
4
6
  const execAsync = promisify(exec);
5
7
 
@@ -239,12 +241,12 @@ export async function getCurrentBranch(directory) {
239
241
  * Create a new worktree
240
242
  * @param {string} directory
241
243
  * @param {string} branch
242
- * @param {string} path
244
+ * @param {string} worktreePath
243
245
  * @param {Object} options
244
246
  * @param {boolean} options.skipFetch - Skip fetching from origin (default: false)
245
247
  * @returns {Promise<{path: string, branch: string}>}
246
248
  */
247
- export async function createWorktree(directory, branch, path, options = {}) {
249
+ export async function createWorktree(directory, branch, worktreePath, options = {}) {
248
250
  const { skipFetch = false } = options;
249
251
 
250
252
  // Fetch latest from origin to ensure we have up-to-date default branch
@@ -256,8 +258,8 @@ export async function createWorktree(directory, branch, path, options = {}) {
256
258
  const defaultBranch = await getOriginDefaultBranch(directory);
257
259
  // Base new branch on origin's default branch to avoid including unrelated commits from HEAD
258
260
  // Use --no-track to prevent the new branch from tracking the start-point (main/master)
259
- await git(directory, `worktree add --no-track "${path}" -b "${branch}" ${defaultBranch}`);
260
- return { path, branch };
261
+ await git(directory, `worktree add --no-track "${worktreePath}" -b "${branch}" ${defaultBranch}`);
262
+ return { path: worktreePath, branch };
261
263
  }
262
264
 
263
265
  /**
@@ -266,9 +268,9 @@ export async function createWorktree(directory, branch, path, options = {}) {
266
268
  * @param {string} path
267
269
  * @param {boolean} force - Force removal even if worktree has uncommitted changes
268
270
  */
269
- export async function removeWorktree(directory, path, force = false) {
271
+ export async function removeWorktree(directory, worktreePath, force = false) {
270
272
  const forceFlag = force ? '--force' : '';
271
- await git(directory, `worktree remove ${forceFlag} "${path}"`);
273
+ await git(directory, `worktree remove ${forceFlag} "${worktreePath}"`);
272
274
  }
273
275
 
274
276
  /**
@@ -518,3 +520,37 @@ export async function pinAuthorInWorktree(worktreePath, projectDir, { env } = {}
518
520
 
519
521
  return true;
520
522
  }
523
+
524
+ /**
525
+ * Detect the worktree path for a directory by inspecting existing worktrees.
526
+ * If external worktrees exist, uses the parent directory of the first one.
527
+ * Otherwise, falls back to {directory}/.worktrees.
528
+ * @param {string} directory - The git repository directory
529
+ * @returns {Promise<{worktreePath: string, source: 'detected' | 'default'}>}
530
+ */
531
+ export async function detectWorktreePath(directory) {
532
+ const isRepo = await isGitRepo(directory);
533
+ if (!isRepo) {
534
+ return { worktreePath: path.join(directory, '.worktrees'), source: 'default' };
535
+ }
536
+
537
+ // Resolve symlinks for consistent path comparison (e.g., /var -> /private/var on macOS)
538
+ let resolvedDir;
539
+ try {
540
+ resolvedDir = await realpath(directory);
541
+ } catch {
542
+ resolvedDir = path.resolve(directory);
543
+ }
544
+
545
+ const worktrees = await getWorktrees(directory);
546
+ // Filter out the main worktree (its path === directory or resolves to it)
547
+ const externalWorktrees = worktrees.filter(wt => path.resolve(wt.path) !== resolvedDir);
548
+
549
+ if (externalWorktrees.length > 0) {
550
+ // Use the parent directory of the first external worktree
551
+ const parentDir = path.dirname(path.resolve(externalWorktrees[0].path));
552
+ return { worktreePath: parentDir, source: 'detected' };
553
+ }
554
+
555
+ return { worktreePath: path.join(resolvedDir, '.worktrees'), source: 'default' };
556
+ }
@@ -8,9 +8,10 @@ import * as gitService from './gitService.js';
8
8
  * @param {string|null} options.gitMode - 'branch', 'worktree', or null
9
9
  * @param {string|null} options.gitBranch - Branch name
10
10
  * @param {string} options.sessionId - Session ID
11
+ * @param {string|null} [options.worktreeBasePath] - Custom base path for worktrees (overrides default .worktrees)
11
12
  * @returns {Promise<{workingDirectory: string, gitWorktree: string|null}>}
12
13
  */
13
- export async function setupGitForSession({ projectDir, gitMode, gitBranch, sessionId }) {
14
+ export async function setupGitForSession({ projectDir, gitMode, gitBranch, sessionId, worktreeBasePath }) {
14
15
  // No git operations if gitMode is not specified
15
16
  if (!gitMode || !gitBranch) {
16
17
  return {
@@ -29,8 +30,8 @@ export async function setupGitForSession({ projectDir, gitMode, gitBranch, sessi
29
30
  }
30
31
 
31
32
  if (gitMode === 'worktree') {
32
- // Create a worktree in .worktrees/{sessionId}
33
- const worktreePath = join(projectDir, '.worktrees', sessionId);
33
+ // Create a worktree in the configured base path or .worktrees/{sessionId}
34
+ const worktreePath = join(worktreeBasePath || join(projectDir, '.worktrees'), sessionId);
34
35
  await gitService.createWorktreeForBranch(projectDir, gitBranch, worktreePath);
35
36
  // Pin the human developer's git identity so they are the commit Author
36
37
  await gitService.pinAuthorInWorktree(worktreePath, projectDir);
@@ -54,6 +54,7 @@ export async function determineWorkingDirectory(parentSession, project, gitOptio
54
54
  gitMode: gitOptions.gitMode || null,
55
55
  gitBranch: gitOptions.gitBranch || null,
56
56
  sessionId: gitOptions.sessionId,
57
+ worktreeBasePath: project.worktreePath || null,
57
58
  });
58
59
  return { workingDirectory: gitSetup.workingDirectory, gitWorktree: gitSetup.gitWorktree };
59
60
  }
@@ -49,6 +49,38 @@ class SchedulerService {
49
49
  }
50
50
  }
51
51
 
52
+ /**
53
+ * Check whether the scheduler is currently polling.
54
+ * @returns {boolean}
55
+ */
56
+ isRunning() {
57
+ return this.intervalId !== null;
58
+ }
59
+
60
+ /**
61
+ * Start the scheduler unless the environment opts out (e.g. VCR_MODE).
62
+ *
63
+ * This gate lives on the service so unit tests can exercise the decision
64
+ * without booting the whole server. The env parameter defaults to
65
+ * process.env but is injectable for tests.
66
+ *
67
+ * Note: an empty VCR_MODE string is treated the same as unset, matching
68
+ * the /api/server-info contract.
69
+ *
70
+ * @param {object} sessionManager - Session manager instance
71
+ * @param {object} [env=process.env] - Env-like object (for tests)
72
+ * @returns {boolean} true if started, false if gated off
73
+ */
74
+ startIfEnabled(sessionManager, env = process.env) {
75
+ if (env.VCR_MODE && env.VCR_MODE.length > 0) {
76
+ console.log('[SchedulerService] VCR_MODE set, scheduler disabled');
77
+ return false;
78
+ }
79
+ this.initialize(sessionManager);
80
+ this.start();
81
+ return true;
82
+ }
83
+
52
84
  /**
53
85
  * Check for scheduled sessions that are due to start
54
86
  */
@@ -21,6 +21,8 @@ import {
21
21
  import { shouldRescheduleOnError, _checkProactiveReschedule } from './sessionErrors.js';
22
22
  import { schedulerService } from './schedulerService.js';
23
23
  import { buildConversationContextForModelSwitch } from './conversationContext.js';
24
+ import { broadcastToSession } from '../websocket.js';
25
+ import { WS_MESSAGE_TYPES } from '../../../shared/src/index.js';
24
26
 
25
27
  /**
26
28
  * Create the agent for a session, using gateway + logging + VCR.
@@ -243,8 +245,6 @@ async function setupConversationAndMessage(sessionId, content, fileAttachments)
243
245
  const activeConversation = conversations.ensureActiveConversation(sessionId);
244
246
  activeConversationIds.set(sessionId, activeConversation.id);
245
247
 
246
- const { broadcastToSession } = await import('../websocket.js');
247
- const { WS_MESSAGE_TYPES } = await import('@circuschief/shared');
248
248
  const message = messages.create(sessionId, 'user', content, { toolUse: null, conversationId: activeConversation.id });
249
249
  broadcastToSession(sessionId, WS_MESSAGE_TYPES.SESSION_MESSAGE, {
250
250
  message,
@@ -98,6 +98,7 @@ async function resolveWorkingDirectory(parentSession, project, settings, newSess
98
98
  gitMode: settings.gitMode,
99
99
  gitBranch: settings.gitBranch,
100
100
  sessionId: newSessionId,
101
+ worktreeBasePath: project.worktreePath || null,
101
102
  });
102
103
  return { workingDirectory: gitSetup.workingDirectory, gitWorktree: gitSetup.gitWorktree };
103
104
  }
@@ -8,7 +8,8 @@ export const CreateProjectRequest = z.object({
8
8
  onSessionDeleted: z.string().nullable().optional(),
9
9
  prPollInterval: z.number().int().min(10000).optional(), // Min 10 seconds
10
10
  repoUrl: z.string().url().nullable().optional(),
11
- kanbanEnabled: z.boolean().optional(), // Default true
11
+ kanbanEnabled: z.boolean().optional(), // Default false (experimental feature)
12
+ worktreePath: z.string().nullable().optional(),
12
13
  });
13
14
 
14
15
  export const UpdateProjectRequest = z.object({
@@ -20,6 +21,7 @@ export const UpdateProjectRequest = z.object({
20
21
  prPollInterval: z.number().int().min(10000).optional(), // Min 10 seconds
21
22
  repoUrl: z.string().url().nullable().optional(),
22
23
  kanbanEnabled: z.boolean().optional(),
24
+ worktreePath: z.string().nullable().optional(),
23
25
  });
24
26
 
25
27
  export const ProjectResponse = z.object({
@@ -32,6 +34,7 @@ export const ProjectResponse = z.object({
32
34
  prPollInterval: z.number().int(),
33
35
  repoUrl: z.string().url().nullable().optional(),
34
36
  kanbanEnabled: z.boolean(),
37
+ worktreePath: z.string().nullable(),
35
38
  createdAt: z.number(),
36
39
  updatedAt: z.number(),
37
40
  });
@@ -7,7 +7,7 @@
7
7
  */
8
8
 
9
9
  /**
10
- * @typedef {'claude-sonnet-4-6' | 'claude-opus-4-6' | 'claude-haiku-4-5-20251001'} ClaudeModel
10
+ * @typedef {'claude-sonnet-4-6' | 'claude-opus-4-6' | 'claude-opus-4-7' | 'claude-haiku-4-5-20251001'} ClaudeModel
11
11
  */
12
12
 
13
13
  /**
@@ -117,9 +117,10 @@ export const TOOL_TEMPLATE_PAYLOAD_TYPES = ['command', 'prompt'];
117
117
  export const CLAUDE_MODELS = [
118
118
  { id: 'claude-haiku-4-5-20251001', name: 'Haiku 4.5', description: 'Fast & lightweight' },
119
119
  { id: 'claude-sonnet-4-6', name: 'Sonnet 4.6', description: 'Balanced' },
120
- { id: 'claude-opus-4-6', name: 'Opus 4.6', description: 'Most capable (default)' },
120
+ { id: 'claude-opus-4-6', name: 'Opus 4.6', description: 'Previous generation' },
121
+ { id: 'claude-opus-4-7', name: 'Opus 4.7', description: 'Most capable (default)' },
121
122
  ];
122
- export const DEFAULT_MODEL = 'claude-opus-4-6';
123
+ export const DEFAULT_MODEL = 'claude-opus-4-7';
123
124
 
124
125
  /**
125
126
  * @typedef {Object} KanbanBoard
@@ -0,0 +1 @@
1
+ .page-header[data-v-9a53578e]{display:flex;justify-content:space-between;align-items:flex-start;margin-bottom:2rem}.page-header h1[data-v-9a53578e]{margin:0}.page-description[data-v-9a53578e]{margin:.25rem 0 0;font-size:.875rem;color:var(--color-text-soft)}.skeleton-list[data-v-9a53578e]{display:flex;flex-direction:column;gap:1rem}.error-message[data-v-9a53578e]{color:var(--color-error);padding:1rem;background-color:#f851491a;border-radius:var(--border-radius)}.empty-state[data-v-9a53578e]{text-align:center;padding:3rem;color:var(--color-text-soft)}.empty-state p[data-v-9a53578e]{margin-bottom:1rem}.session-list[data-v-9a53578e]{display:flex;flex-direction:column;gap:1rem}.status-filters[data-v-9a53578e]{display:flex;align-items:center;gap:.5rem;margin-bottom:1rem}.filter-label[data-v-9a53578e]{font-size:.875rem;color:var(--color-text-soft)}.filter-btn[data-v-9a53578e]{background:none;border:1px solid var(--color-border);padding:.375rem .75rem;font-size:.8rem;color:var(--color-text-soft);cursor:pointer;border-radius:var(--border-radius);transition:all .15s;text-transform:capitalize}.filter-btn[data-v-9a53578e]:hover{border-color:var(--color-primary);color:var(--color-text)}.star-icon[data-v-9a53578e]{position:relative;display:inline-block}.filter-btn.star-filter-all[data-v-9a53578e]{background:transparent;border-color:var(--color-border);color:var(--color-text-soft)}.filter-btn.star-filter-all[data-v-9a53578e]:hover{border-color:var(--color-primary);color:var(--color-text)}.filter-btn.star-filter-active[data-v-9a53578e],.filter-btn.active[data-v-9a53578e]{background:var(--color-primary);border-color:var(--color-primary);color:#fff}.filter-btn.star-filter-unstarred[data-v-9a53578e]{background:transparent;border-color:#f97316;color:#f97316}.star-crossed[data-v-9a53578e]:after{content:"";position:absolute;top:50%;left:-10%;right:-10%;height:2px;background:currentColor;transform:translateY(-50%) rotate(-45deg);pointer-events:none}
@@ -0,0 +1 @@
1
+ import{u as K}from"./sessions-CoRS-wuR.js";import{u as Q}from"./commandButtons-D_-wR8zJ.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-D1Lg5reX.js";import{a as x}from"./ApiClient-CcqJ-GAv.js";import{S as le}from"./SessionCard-C7vFzR16.js";import"./settings-BN_W4nwV.js";import"./SessionLogStream-NR-AS676.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)}}},ge=X(we,[["__scopeId","data-v-9a53578e"]]);export{ge as default};
@@ -1,2 +1,2 @@
1
- import{L as q,_ as O,a as r,c as i,b as t,F as k,r as $,t as f,k as _,E as A,x as p,f as W,e as F,G as z,w as G,o as H,A as J,d as w,u as m,l as B}from"./index-CSOPrlmq.js";import{a as L}from"./ApiClient-Dbs1H78V.js";const U=q("agentLogs",{state:()=>({logs:[],pagination:{total:0,limit:25,offset:0,hasMore:!1},filters:{agentType:null,callType:null,status:null,model:null,startDate:null,endDate:null,sessionId:null},filterOptions:{agentTypes:[],callTypes:[],statuses:[],models:[]},perPage:25,currentPage:1,sortBy:"started_at",sortOrder:"DESC",loading:!1,error:null}),getters:{totalPages:s=>Math.ceil(s.pagination.total/s.perPage)||0,activeFilters:s=>{const e={};for(const[y,h]of Object.entries(s.filters))h!=null&&(e[y]=h);return e}},actions:{async fetchLogs(){this.loading=!0,this.error=null;try{const s=(this.currentPage-1)*this.perPage,e=await L.getAgentCallLogs({limit:this.perPage,offset:s,...this.activeFilters,sortBy:this.sortBy,sortOrder:this.sortOrder});this.logs=e.logs,this.pagination=e.pagination}catch(s){this.error=s.message}finally{this.loading=!1}},async fetchFilterOptions(){try{const s=await L.getAgentCallFilterOptions();this.filterOptions=s}catch(s){console.error("Failed to fetch filter options:",s)}},setFilter(s,e){this.filters[s]=e||null,this.currentPage=1,this.fetchLogs()},clearFilters(){Object.keys(this.filters).forEach(s=>this.filters[s]=null),this.currentPage=1,this.fetchLogs()},setPage(s){this.currentPage=s,this.fetchLogs()},setPerPage(s){this.perPage=s,this.currentPage=1,this.fetchLogs()},setSort(s,e){this.sortBy=s,this.sortOrder=e,this.currentPage=1,this.fetchLogs()},async clearAllLogs(){this.loading=!0,this.error=null;try{await L.deleteAllAgentCallLogs(),this.logs=[],this.pagination={total:0,limit:this.perPage,offset:0,hasMore:!1},this.currentPage=1,await this.fetchFilterOptions()}catch(s){this.error=s.message}finally{this.loading=!1}}}}),Z={class:"filter-bar"},Q={class:"filter-row"},X={class:"filter-group"},Y=["value"],K={class:"filter-group"},tt=["value"],et={class:"filter-group"},st=["value"],lt=["value"],at={class:"filter-group"},nt=["value"],ot=["value"],rt={class:"filter-group"},it=["value"],ut=["value"],dt={class:"filter-group"},ct=["value"],ft=["value"],gt={__name:"AgentLogFilters",props:{filters:{type:Object,required:!0},filterOptions:{type:Object,required:!0},hasActiveFilters:{type:Boolean,default:!1},confirming:{type:Boolean,default:!1}},emits:["filter-change","date-change","clear-filters","clear-all"],setup(s){const e=s;function y(v){return new Date(v).toISOString().slice(0,10)}const h=p(()=>e.filters.startDate?y(e.filters.startDate):""),b=p(()=>e.filters.endDate?y(e.filters.endDate):"");return(v,a)=>(r(),i("div",Z,[t("div",Q,[t("div",X,[a[8]||(a[8]=t("label",{class:"filter-label"},"From",-1)),t("input",{type:"date",class:"filter-input",value:h.value,onChange:a[0]||(a[0]=n=>v.$emit("date-change","startDate",n))},null,40,Y)]),t("div",K,[a[9]||(a[9]=t("label",{class:"filter-label"},"To",-1)),t("input",{type:"date",class:"filter-input",value:b.value,onChange:a[1]||(a[1]=n=>v.$emit("date-change","endDate",n))},null,40,tt)]),t("div",et,[a[11]||(a[11]=t("label",{class:"filter-label"},"Agent Type",-1)),t("select",{class:"filter-select",value:s.filters.agentType||"",onChange:a[2]||(a[2]=n=>v.$emit("filter-change","agentType",n.target.value))},[a[10]||(a[10]=t("option",{value:""}," All ",-1)),(r(!0),i(k,null,$(s.filterOptions.agentTypes,n=>(r(),i("option",{key:n,value:n},f(n),9,lt))),128))],40,st)]),t("div",at,[a[13]||(a[13]=t("label",{class:"filter-label"},"Call Type",-1)),t("select",{class:"filter-select",value:s.filters.callType||"",onChange:a[3]||(a[3]=n=>v.$emit("filter-change","callType",n.target.value))},[a[12]||(a[12]=t("option",{value:""}," All ",-1)),(r(!0),i(k,null,$(s.filterOptions.callTypes,n=>(r(),i("option",{key:n,value:n},f(n),9,ot))),128))],40,nt)]),t("div",rt,[a[15]||(a[15]=t("label",{class:"filter-label"},"Status",-1)),t("select",{class:"filter-select",value:s.filters.status||"",onChange:a[4]||(a[4]=n=>v.$emit("filter-change","status",n.target.value))},[a[14]||(a[14]=t("option",{value:""}," All ",-1)),(r(!0),i(k,null,$(s.filterOptions.statuses,n=>(r(),i("option",{key:n,value:n},f(n),9,ut))),128))],40,it)]),t("div",dt,[a[17]||(a[17]=t("label",{class:"filter-label"},"Model",-1)),t("select",{class:"filter-select",value:s.filters.model||"",onChange:a[5]||(a[5]=n=>v.$emit("filter-change","model",n.target.value))},[a[16]||(a[16]=t("option",{value:""}," All ",-1)),(r(!0),i(k,null,$(s.filterOptions.models,n=>(r(),i("option",{key:n,value:n},f(n),9,ft))),128))],40,ct)]),s.hasActiveFilters?(r(),i("button",{key:0,class:"btn-clear",onClick:a[6]||(a[6]=n=>v.$emit("clear-filters"))}," Clear Filters ")):_("",!0),t("button",{class:A(["btn-clear-all",{confirming:s.confirming}]),onClick:a[7]||(a[7]=n=>v.$emit("clear-all"))},f(s.confirming?"Confirm Clear All?":"Clear All Logs"),3)])]))}},vt=O(gt,[["__scopeId","data-v-780329d3"]]),pt={class:"table-wrapper"},ht={key:0,class:"loading-overlay"},yt={class:"log-table"},bt=["onClick"],mt={key:0,class:"sort-indicator"},kt={key:0},$t=["colspan"],Ct={class:"td"},_t={class:"status-text"},Tt={class:"td font-mono text-sm"},At={class:"td font-mono text-sm"},Pt={class:"td model-cell"},Lt={class:"td"},Ot={key:1},St={class:"td"},Dt={key:1},Ft=["title"],wt={class:"td text-right"},Bt=["title"],Mt={__name:"AgentLogTable",props:{logs:{type:Array,required:!0},loading:{type:Boolean,default:!1},sortBy:{type:String,default:"started_at"},sortOrder:{type:String,default:"DESC"}},emits:["sort"],setup(s){const e=[{key:"status",label:"Status",sortable:!0},{key:"agent_type",label:"Agent Type",sortable:!0},{key:"call_type",label:"Call Type",sortable:!0},{key:"model",label:"Model",sortable:!0},{key:"session",label:"Session",sortable:!1},{key:"effort",label:"Effort",sortable:!1},{key:"total_tokens",label:"Tokens",sortable:!0},{key:"duration_ms",label:"Duration",sortable:!0},{key:"started_at",label:"Started",sortable:!0}];function y(l){switch(l){case"completed":return"dot-completed";case"error":return"dot-error";case"streaming":case"pending":return"dot-pending";default:return"dot-default"}}function h(l){const d=[];return l.inputTokens&&d.push(`Input: ${b(l.inputTokens)}`),l.outputTokens&&d.push(`Output: ${b(l.outputTokens)}`),l.thinkingTokens&&d.push(`Thinking: ${b(l.thinkingTokens)}`),l.cacheReadTokens&&d.push(`Cache Read: ${b(l.cacheReadTokens)}`),l.cacheWriteTokens&&d.push(`Cache Write: ${b(l.cacheWriteTokens)}`),d.join(`
1
+ import{L as q,_ as O,a as r,c as i,b as t,F as k,r as $,t as f,i as _,E as A,x as p,g as W,f as F,G as z,w as G,o as H,A as J,d as w,u as m,n as B}from"./index-D1Lg5reX.js";import{a as L}from"./ApiClient-CcqJ-GAv.js";const U=q("agentLogs",{state:()=>({logs:[],pagination:{total:0,limit:25,offset:0,hasMore:!1},filters:{agentType:null,callType:null,status:null,model:null,startDate:null,endDate:null,sessionId:null},filterOptions:{agentTypes:[],callTypes:[],statuses:[],models:[]},perPage:25,currentPage:1,sortBy:"started_at",sortOrder:"DESC",loading:!1,error:null}),getters:{totalPages:s=>Math.ceil(s.pagination.total/s.perPage)||0,activeFilters:s=>{const e={};for(const[y,h]of Object.entries(s.filters))h!=null&&(e[y]=h);return e}},actions:{async fetchLogs(){this.loading=!0,this.error=null;try{const s=(this.currentPage-1)*this.perPage,e=await L.getAgentCallLogs({limit:this.perPage,offset:s,...this.activeFilters,sortBy:this.sortBy,sortOrder:this.sortOrder});this.logs=e.logs,this.pagination=e.pagination}catch(s){this.error=s.message}finally{this.loading=!1}},async fetchFilterOptions(){try{const s=await L.getAgentCallFilterOptions();this.filterOptions=s}catch(s){console.error("Failed to fetch filter options:",s)}},setFilter(s,e){this.filters[s]=e||null,this.currentPage=1,this.fetchLogs()},clearFilters(){Object.keys(this.filters).forEach(s=>this.filters[s]=null),this.currentPage=1,this.fetchLogs()},setPage(s){this.currentPage=s,this.fetchLogs()},setPerPage(s){this.perPage=s,this.currentPage=1,this.fetchLogs()},setSort(s,e){this.sortBy=s,this.sortOrder=e,this.currentPage=1,this.fetchLogs()},async clearAllLogs(){this.loading=!0,this.error=null;try{await L.deleteAllAgentCallLogs(),this.logs=[],this.pagination={total:0,limit:this.perPage,offset:0,hasMore:!1},this.currentPage=1,await this.fetchFilterOptions()}catch(s){this.error=s.message}finally{this.loading=!1}}}}),Z={class:"filter-bar"},Q={class:"filter-row"},X={class:"filter-group"},Y=["value"],K={class:"filter-group"},tt=["value"],et={class:"filter-group"},st=["value"],lt=["value"],at={class:"filter-group"},nt=["value"],ot=["value"],rt={class:"filter-group"},it=["value"],ut=["value"],dt={class:"filter-group"},ct=["value"],ft=["value"],gt={__name:"AgentLogFilters",props:{filters:{type:Object,required:!0},filterOptions:{type:Object,required:!0},hasActiveFilters:{type:Boolean,default:!1},confirming:{type:Boolean,default:!1}},emits:["filter-change","date-change","clear-filters","clear-all"],setup(s){const e=s;function y(v){return new Date(v).toISOString().slice(0,10)}const h=p(()=>e.filters.startDate?y(e.filters.startDate):""),b=p(()=>e.filters.endDate?y(e.filters.endDate):"");return(v,a)=>(r(),i("div",Z,[t("div",Q,[t("div",X,[a[8]||(a[8]=t("label",{class:"filter-label"},"From",-1)),t("input",{type:"date",class:"filter-input",value:h.value,onChange:a[0]||(a[0]=n=>v.$emit("date-change","startDate",n))},null,40,Y)]),t("div",K,[a[9]||(a[9]=t("label",{class:"filter-label"},"To",-1)),t("input",{type:"date",class:"filter-input",value:b.value,onChange:a[1]||(a[1]=n=>v.$emit("date-change","endDate",n))},null,40,tt)]),t("div",et,[a[11]||(a[11]=t("label",{class:"filter-label"},"Agent Type",-1)),t("select",{class:"filter-select",value:s.filters.agentType||"",onChange:a[2]||(a[2]=n=>v.$emit("filter-change","agentType",n.target.value))},[a[10]||(a[10]=t("option",{value:""}," All ",-1)),(r(!0),i(k,null,$(s.filterOptions.agentTypes,n=>(r(),i("option",{key:n,value:n},f(n),9,lt))),128))],40,st)]),t("div",at,[a[13]||(a[13]=t("label",{class:"filter-label"},"Call Type",-1)),t("select",{class:"filter-select",value:s.filters.callType||"",onChange:a[3]||(a[3]=n=>v.$emit("filter-change","callType",n.target.value))},[a[12]||(a[12]=t("option",{value:""}," All ",-1)),(r(!0),i(k,null,$(s.filterOptions.callTypes,n=>(r(),i("option",{key:n,value:n},f(n),9,ot))),128))],40,nt)]),t("div",rt,[a[15]||(a[15]=t("label",{class:"filter-label"},"Status",-1)),t("select",{class:"filter-select",value:s.filters.status||"",onChange:a[4]||(a[4]=n=>v.$emit("filter-change","status",n.target.value))},[a[14]||(a[14]=t("option",{value:""}," All ",-1)),(r(!0),i(k,null,$(s.filterOptions.statuses,n=>(r(),i("option",{key:n,value:n},f(n),9,ut))),128))],40,it)]),t("div",dt,[a[17]||(a[17]=t("label",{class:"filter-label"},"Model",-1)),t("select",{class:"filter-select",value:s.filters.model||"",onChange:a[5]||(a[5]=n=>v.$emit("filter-change","model",n.target.value))},[a[16]||(a[16]=t("option",{value:""}," All ",-1)),(r(!0),i(k,null,$(s.filterOptions.models,n=>(r(),i("option",{key:n,value:n},f(n),9,ft))),128))],40,ct)]),s.hasActiveFilters?(r(),i("button",{key:0,class:"btn-clear",onClick:a[6]||(a[6]=n=>v.$emit("clear-filters"))}," Clear Filters ")):_("",!0),t("button",{class:A(["btn-clear-all",{confirming:s.confirming}]),onClick:a[7]||(a[7]=n=>v.$emit("clear-all"))},f(s.confirming?"Confirm Clear All?":"Clear All Logs"),3)])]))}},vt=O(gt,[["__scopeId","data-v-780329d3"]]),pt={class:"table-wrapper"},ht={key:0,class:"loading-overlay"},yt={class:"log-table"},bt=["onClick"],mt={key:0,class:"sort-indicator"},kt={key:0},$t=["colspan"],Ct={class:"td"},_t={class:"status-text"},Tt={class:"td font-mono text-sm"},At={class:"td font-mono text-sm"},Pt={class:"td model-cell"},Lt={class:"td"},Ot={key:1},St={class:"td"},Dt={key:1},Ft=["title"],wt={class:"td text-right"},Bt=["title"],Mt={__name:"AgentLogTable",props:{logs:{type:Array,required:!0},loading:{type:Boolean,default:!1},sortBy:{type:String,default:"started_at"},sortOrder:{type:String,default:"DESC"}},emits:["sort"],setup(s){const e=[{key:"status",label:"Status",sortable:!0},{key:"agent_type",label:"Agent Type",sortable:!0},{key:"call_type",label:"Call Type",sortable:!0},{key:"model",label:"Model",sortable:!0},{key:"session",label:"Session",sortable:!1},{key:"effort",label:"Effort",sortable:!1},{key:"total_tokens",label:"Tokens",sortable:!0},{key:"duration_ms",label:"Duration",sortable:!0},{key:"started_at",label:"Started",sortable:!0}];function y(l){switch(l){case"completed":return"dot-completed";case"error":return"dot-error";case"streaming":case"pending":return"dot-pending";default:return"dot-default"}}function h(l){const d=[];return l.inputTokens&&d.push(`Input: ${b(l.inputTokens)}`),l.outputTokens&&d.push(`Output: ${b(l.outputTokens)}`),l.thinkingTokens&&d.push(`Thinking: ${b(l.thinkingTokens)}`),l.cacheReadTokens&&d.push(`Cache Read: ${b(l.cacheReadTokens)}`),l.cacheWriteTokens&&d.push(`Cache Write: ${b(l.cacheWriteTokens)}`),d.join(`
2
2
  `)||"No token data"}function b(l){return l==null||l===0?"0":l.toLocaleString()}function v(l){if(l==null)return"—";if(l<1e3)return`${l}ms`;if(l<6e4)return`${(l/1e3).toFixed(1)}s`;const d=Math.floor(l/6e4),C=Math.floor(l%6e4/1e3);return`${d}m ${C}s`}function a(l){const d=Date.now()-l;return d<6e4?"just now":d<36e5?`${Math.floor(d/6e4)}m ago`:d<864e5?`${Math.floor(d/36e5)}h ago`:`${Math.floor(d/864e5)}d ago`}function n(l){try{if(l.metadata)return(typeof l.metadata=="string"?JSON.parse(l.metadata):l.metadata).effortLevel||null}catch{}return null}function P(l){return{auto:"Auto",low:"Low",medium:"Med",high:"High",max:"Max"}[l]||l}return(l,d)=>{const C=W("router-link");return r(),i("div",pt,[s.loading?(r(),i("div",ht,[...d[0]||(d[0]=[t("div",{class:"spinner"},null,-1)])])):_("",!0),t("table",yt,[t("thead",null,[t("tr",null,[(r(),i(k,null,$(e,o=>t("th",{key:o.key,class:A(["th",o.sortable?"sortable":""]),onClick:S=>o.sortable?l.$emit("sort",o.key):null},[F(f(o.label)+" ",1),o.sortable&&s.sortBy===o.key?(r(),i("span",mt,f(s.sortOrder==="ASC"?"↑":"↓"),1)):_("",!0)],10,bt)),64))])]),t("tbody",null,[!s.loading&&s.logs.length===0?(r(),i("tr",kt,[t("td",{colspan:e.length,class:"empty-cell"}," No agent call logs found. ",8,$t)])):_("",!0),(r(!0),i(k,null,$(s.logs,o=>(r(),i("tr",{key:o.id,class:"log-row"},[t("td",Ct,[t("span",{class:A(["status-dot",y(o.status)])},null,2),t("span",_t,f(o.status),1)]),t("td",Tt,f(o.agentType||"—"),1),t("td",At,f(o.callType||"—"),1),t("td",Pt,f(o.model||"—"),1),t("td",Lt,[o.sessionId?(r(),z(C,{key:0,to:`/sessions/${o.sessionId}`,class:"session-link"},{default:G(()=>[F(f(o.sessionName||o.sessionId),1)]),_:2},1032,["to"])):(r(),i("span",Ot,"—"))]),t("td",St,[n(o)?(r(),i("span",{key:0,class:A(["effort-badge",`effort-${n(o)}`])},f(P(n(o))),3)):(r(),i("span",Dt,"—"))]),t("td",{class:"td text-right",title:h(o)},f(b(o.totalTokens)),9,Ft),t("td",wt,f(v(o.durationMs)),1),t("td",{class:"td text-right",title:o.startedAt?new Date(o.startedAt).toLocaleString():""},f(o.startedAt?a(o.startedAt):"—"),9,Bt)]))),128))])])])}}},It=O(Mt,[["__scopeId","data-v-20d5856a"]]),Et={class:"agent-logs"},Nt={key:0,class:"error-banner"},Vt={key:1,class:"pagination-bar"},xt={class:"per-page-control"},jt=["value"],Rt=["value"],qt={class:"page-info"},Wt={class:"page-buttons"},zt=["disabled"],Gt=["disabled"],Ht={key:0,class:"page-ellipsis"},Jt=["onClick"],Ut=["disabled"],Zt=["disabled"],Qt={__name:"AgentLogsView",setup(s){const e=U(),y=B(!1),h=B(null),b=p(()=>e.logs),v=p(()=>e.pagination),a=p(()=>e.filters),n=p(()=>e.filterOptions),P=p(()=>e.perPage),l=p(()=>e.currentPage),d=p(()=>e.totalPages),C=p(()=>e.sortBy),o=p(()=>e.sortOrder),S=p(()=>e.loading),D=p(()=>e.error),M=p(()=>Object.values(e.filters).some(g=>g!=null)),I=p(()=>v.value.total===0?0:v.value.offset+1),E=p(()=>Math.min(v.value.offset+P.value,v.value.total)),N=p(()=>{const g=d.value,c=l.value;if(g<=7)return Array.from({length:g},(T,R)=>R+1);const u=[];return c<=4?u.push(1,2,3,4,5,"...",g):c>=g-3?u.push(1,"...",g-4,g-3,g-2,g-1,g):u.push(1,"...",c-1,c,c+1,"...",g),u});function V(g){C.value===g?e.setSort(g,o.value==="ASC"?"DESC":"ASC"):e.setSort(g,"DESC")}function x(g,c){const u=c.target.value;if(!u){e.setFilter(g,null);return}const T=new Date(`${u}T00:00:00Z`).getTime();e.filters[g]=T,e.currentPage=1,e.fetchLogs()}function j(){y.value?(h.value&&(clearTimeout(h.value),h.value=null),y.value=!1,e.clearAllLogs()):(y.value=!0,h.value=setTimeout(()=>{y.value=!1,h.value=null},3e3))}return H(()=>{e.fetchLogs(),e.fetchFilterOptions()}),J(()=>{h.value&&clearTimeout(h.value)}),(g,c)=>(r(),i("div",Et,[w(vt,{filters:a.value,"filter-options":n.value,"has-active-filters":M.value,confirming:y.value,onFilterChange:c[0]||(c[0]=(u,T)=>m(e).setFilter(u,T)),onDateChange:x,onClearFilters:c[1]||(c[1]=u=>m(e).clearFilters()),onClearAll:j},null,8,["filters","filter-options","has-active-filters","confirming"]),D.value?(r(),i("div",Nt,[t("span",null,f(D.value),1),t("button",{class:"btn-retry",onClick:c[2]||(c[2]=u=>m(e).fetchLogs())}," Retry ")])):_("",!0),w(It,{logs:b.value,loading:S.value,"sort-by":C.value,"sort-order":o.value,onSort:V},null,8,["logs","loading","sort-by","sort-order"]),v.value.total>0?(r(),i("div",Vt,[t("div",xt,[c[8]||(c[8]=t("label",{class:"filter-label"},"Per page",-1)),t("select",{class:"filter-select per-page-select",value:P.value,onChange:c[3]||(c[3]=u=>m(e).setPerPage(parseInt(u.target.value)))},[(r(),i(k,null,$([10,25,50,100],u=>t("option",{key:u,value:u},f(u),9,Rt)),64))],40,jt)]),t("div",qt," Showing "+f(I.value)+"–"+f(E.value)+" of "+f(v.value.total)+" logs ",1),t("div",Wt,[t("button",{class:"page-btn",disabled:l.value===1,onClick:c[4]||(c[4]=u=>m(e).setPage(1))}," « ",8,zt),t("button",{class:"page-btn",disabled:l.value===1,onClick:c[5]||(c[5]=u=>m(e).setPage(l.value-1))}," ‹ ",8,Gt),(r(!0),i(k,null,$(N.value,u=>(r(),i(k,{key:u},[u==="..."?(r(),i("span",Ht,"…")):(r(),i("button",{key:1,class:A(["page-btn",{"page-btn-active":u===l.value}]),onClick:T=>m(e).setPage(u)},f(u),11,Jt))],64))),128)),t("button",{class:"page-btn",disabled:l.value===d.value,onClick:c[6]||(c[6]=u=>m(e).setPage(l.value+1))}," › ",8,Ut),t("button",{class:"page-btn",disabled:l.value===d.value,onClick:c[7]||(c[7]=u=>m(e).setPage(d.value))}," » ",8,Zt)])])):_("",!0)]))}},Kt=O(Qt,[["__scopeId","data-v-e1df2011"]]);export{Kt as default};