circuschief 0.6.0 → 0.7.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 (129) hide show
  1. package/package.json +1 -1
  2. package/packages/server/src/api/projects-session-helpers.js +25 -0
  3. package/packages/server/src/api/projects.js +1 -0
  4. package/packages/server/src/api/sessions-draft.js +1 -0
  5. package/packages/server/src/api/settings.js +52 -4
  6. package/packages/server/src/db/ConversationRepository.js +16 -3
  7. package/packages/server/src/db/ProjectDefaultsRepository.js +47 -37
  8. package/packages/server/src/db/SessionRepository.js +12 -8
  9. package/packages/server/src/db/SettingsRepository.js +44 -16
  10. package/packages/server/src/db/conversation-helpers.js +1 -0
  11. package/packages/server/src/db/migrations/conversationsMigrations.js +4 -0
  12. package/packages/server/src/db/migrations/index.js +2 -0
  13. package/packages/server/src/db/migrations/sessionsMigrations.js +6 -1
  14. package/packages/server/src/db/session-helpers.js +3 -0
  15. package/packages/server/src/schema.sql +8 -0
  16. package/packages/server/src/services/agentCallLogger.js +1 -1
  17. package/packages/server/src/services/commandButtonPrompts.js +48 -0
  18. package/packages/server/src/services/draftSessionService.js +2 -0
  19. package/packages/server/src/services/sessionExecution.js +3 -1
  20. package/packages/server/src/services/sessionPrompts.js +4 -0
  21. package/packages/server/src/services/sessionProvider.js +6 -8
  22. package/packages/server/src/services/streamEventCallbacks.js +72 -40
  23. package/packages/server/src/services/streamEventHandler.js +13 -2
  24. package/packages/server/src/services/streamUsageHandler.js +6 -0
  25. package/packages/server/src/services/summaryClaudeClient.js +37 -12
  26. package/packages/server/src/services/summaryModelClient.js +154 -0
  27. package/packages/server/src/services/summaryModelResolver.js +148 -0
  28. package/packages/server/src/services/summaryService.js +6 -4
  29. package/packages/server/src/services/usageTracker.js +5 -1
  30. package/packages/server/src/services/visibleFinalErrorMessage.js +123 -0
  31. package/packages/shared/src/constants.js +1 -2
  32. package/packages/shared/src/contracts/projects.js +2 -0
  33. package/packages/shared/src/utils.js +9 -17
  34. package/packages/web/dist/assets/ActiveSessionsView-UJsCILDL.js +1 -0
  35. package/packages/web/dist/assets/{AgentLogsView-Cdw4nmvd.js → AgentLogsView-BGFPLjLa.js} +1 -1
  36. package/packages/web/dist/assets/ApiClient-B4YTtyY4.js +1 -0
  37. package/packages/web/dist/assets/{ArchiveConfirmModal-J48eh3zw.js → ArchiveConfirmModal-OFaj_uX5.js} +1 -1
  38. package/packages/web/dist/assets/{CommandButtonDetailView-DnFhJY5A.js → CommandButtonDetailView-D8S258uP.js} +1 -1
  39. package/packages/web/dist/assets/EffortLevelSelector-C2378L8e.js +1 -0
  40. package/packages/web/dist/assets/{GeneralSettingsView-CQkmdczf.js → GeneralSettingsView-DsHChEhv.js} +1 -1
  41. package/packages/web/dist/assets/{InputWithButton-XyM3k6lN.js → InputWithButton-Ci15ox0a.js} +1 -1
  42. package/packages/web/dist/assets/{InterpolationHelp-PfYR3KJo.js → InterpolationHelp-CIkOSkWX.js} +1 -1
  43. package/packages/web/dist/assets/MarkdownEditor-5-bexzUT.js +2 -0
  44. package/packages/web/dist/assets/ModelSelector-BMpR0DPr.js +1 -0
  45. package/packages/web/dist/assets/{ModelSelector-BZOT1Jc6.css → ModelSelector-D8hbTRIt.css} +1 -1
  46. package/packages/web/dist/assets/{NewSessionView-DkjFLvHU.js → NewSessionView-BCqtIgWH.js} +2 -2
  47. package/packages/web/dist/assets/{NewSessionView-D_Hi7M9g.css → NewSessionView-CUUdHkfv.css} +1 -1
  48. package/packages/web/dist/assets/ProjectEditView-D9sK0fdH.css +1 -0
  49. package/packages/web/dist/assets/ProjectEditView-RFaxHhAX.js +1 -0
  50. package/packages/web/dist/assets/{ProjectListView-CuYMmd3O.js → ProjectListView-B9FuWESY.js} +1 -1
  51. package/packages/web/dist/assets/{ProjectNewView-CNaA4Maf.js → ProjectNewView-D62jYlBL.js} +1 -1
  52. package/packages/web/dist/assets/ProvidersView-DDKMIQWZ.js +1 -0
  53. package/packages/web/dist/assets/QuickResponseSettings-CDm5vwP7.js +1 -0
  54. package/packages/web/dist/assets/{QuickResponsesPanel-DIBQFj0W.css → QuickResponsesPanel-BlFDvnZ2.css} +1 -1
  55. package/packages/web/dist/assets/{QuickResponsesPanel-BqMYSHb0.js → QuickResponsesPanel-DZ_Lre_l.js} +1 -1
  56. package/packages/web/dist/assets/{ResizableTextarea-wYF3K2RO.js → ResizableTextarea-DiIOEGjN.js} +1 -1
  57. package/packages/web/dist/assets/{SessionCard-bLaQEWWX.js → SessionCard-DmjnVYWn.js} +1 -1
  58. package/packages/web/dist/assets/SessionDetailView-CL7nmfiB.js +36 -0
  59. package/packages/web/dist/assets/SessionDetailView-CupIkI7u.css +1 -0
  60. package/packages/web/dist/assets/SessionFormOptions-BpUALRKn.css +1 -0
  61. package/packages/web/dist/assets/SessionFormOptions-DYUISplS.js +1 -0
  62. package/packages/web/dist/assets/SessionListView-BcxGz4aC.js +1 -0
  63. package/packages/web/dist/assets/{SessionLogStream-DTnDAF95.js → SessionLogStream-DpUE6Xsh.js} +1 -1
  64. package/packages/web/dist/assets/{SettingsView-DNLUSsHV.js → SettingsView-BC055tIA.js} +1 -1
  65. package/packages/web/dist/assets/{SlashCommandWizard-CRGFaO8t.js → SlashCommandWizard-DmTyNG9O.js} +1 -1
  66. package/packages/web/dist/assets/SummarySettingsView-BgnRCwlq.js +1 -0
  67. package/packages/web/dist/assets/SummarySettingsView-l2bxHmZZ.css +1 -0
  68. package/packages/web/dist/assets/TemplateDetailView-BlhOmLUX.js +1 -0
  69. package/packages/web/dist/assets/{commandButtons-Bbjf3fCt.js → commandButtons-D4RPpLiu.js} +1 -1
  70. package/packages/web/dist/assets/index-4rhEeO0B.js +1 -0
  71. package/packages/web/dist/assets/index-9vb2KaAd.js +1 -0
  72. package/packages/web/dist/assets/index-B0CvZXuN.js +7 -0
  73. package/packages/web/dist/assets/{index-BQL_L4gL.js → index-B6G18FqB.js} +14 -14
  74. package/packages/web/dist/assets/{index-Cf6vdW-B.js → index-BGwH4Cfn.js} +3 -3
  75. package/packages/web/dist/assets/index-BUhvkAdF.js +1 -0
  76. package/packages/web/dist/assets/index-BcnkUk2o.js +1 -0
  77. package/packages/web/dist/assets/{index-Bs7Qf5D6.js → index-Bn5xdGFM.js} +2 -2
  78. package/packages/web/dist/assets/index-CNwkdB0T.js +1 -0
  79. package/packages/web/dist/assets/index-CfL84oGW.js +1 -0
  80. package/packages/web/dist/assets/index-CkmxO8Mm.js +1 -0
  81. package/packages/web/dist/assets/index-Cpy4-yv3.js +1 -0
  82. package/packages/web/dist/assets/index-CrAQJmoZ.js +1 -0
  83. package/packages/web/dist/assets/{index-gmCCsCQ1.css → index-Cs2nxhrT.css} +1 -1
  84. package/packages/web/dist/assets/index-D6Ky9vJe.js +3 -0
  85. package/packages/web/dist/assets/index-DfrE0gAC.js +1 -0
  86. package/packages/web/dist/assets/index-KwEyz0F3.js +1 -0
  87. package/packages/web/dist/assets/index-OfCywayk.js +1 -0
  88. package/packages/web/dist/assets/index-PDesaJc6.js +1 -0
  89. package/packages/web/dist/assets/index-uB6nhSvz.js +1 -0
  90. package/packages/web/dist/assets/{projects-CPt3AB7U.js → projects-BUiOGmmb.js} +1 -1
  91. package/packages/web/dist/assets/{providers-ChfeMvUq.js → providers-Bh1ZiiJi.js} +1 -1
  92. package/packages/web/dist/assets/sessions-DH1R-NhV.js +1 -0
  93. package/packages/web/dist/assets/settings-Z4AVVmkJ.js +1 -0
  94. package/packages/web/dist/index.html +2 -2
  95. package/packages/web/dist/assets/ActiveSessionsView-UCbQrF1b.js +0 -1
  96. package/packages/web/dist/assets/ApiClient-CWbXWDUY.js +0 -1
  97. package/packages/web/dist/assets/EffortLevelSelector-bXbPo4Zw.js +0 -1
  98. package/packages/web/dist/assets/MarkdownEditor-P8F5kO-o.js +0 -2
  99. package/packages/web/dist/assets/ModelSelector-CowKfGMP.js +0 -1
  100. package/packages/web/dist/assets/ProjectEditView-CpeKj-_w.css +0 -1
  101. package/packages/web/dist/assets/ProjectEditView-embVT7NC.js +0 -1
  102. package/packages/web/dist/assets/ProvidersView-C7rydtOd.js +0 -1
  103. package/packages/web/dist/assets/QuickResponseSettings-BTQEKhwJ.js +0 -1
  104. package/packages/web/dist/assets/SessionDetailView-Cv-xMzXp.css +0 -1
  105. package/packages/web/dist/assets/SessionDetailView-CvQOUsW2.js +0 -36
  106. package/packages/web/dist/assets/SessionFormOptions-3pzbgI2Q.js +0 -1
  107. package/packages/web/dist/assets/SessionFormOptions-DhhIkjIS.css +0 -1
  108. package/packages/web/dist/assets/SessionListView-Dranfb72.js +0 -1
  109. package/packages/web/dist/assets/SummarySettingsView-C7G_suHp.js +0 -1
  110. package/packages/web/dist/assets/SummarySettingsView-DcsmSVJI.css +0 -1
  111. package/packages/web/dist/assets/TemplateDetailView-B78_DLMR.js +0 -1
  112. package/packages/web/dist/assets/index--V7c-VZf.js +0 -1
  113. package/packages/web/dist/assets/index-8Q04yd7H.js +0 -1
  114. package/packages/web/dist/assets/index-B47XRBDH.js +0 -1
  115. package/packages/web/dist/assets/index-BXbgZrhS.js +0 -1
  116. package/packages/web/dist/assets/index-CGhDVPen.js +0 -1
  117. package/packages/web/dist/assets/index-CKcRO1A6.js +0 -1
  118. package/packages/web/dist/assets/index-CTq-SLIW.js +0 -1
  119. package/packages/web/dist/assets/index-CYyos3iC.js +0 -1
  120. package/packages/web/dist/assets/index-CsCREAxF.js +0 -1
  121. package/packages/web/dist/assets/index-DJTTk_8T.js +0 -3
  122. package/packages/web/dist/assets/index-DPqUJ5JK.js +0 -1
  123. package/packages/web/dist/assets/index-EwAe1dKg.js +0 -1
  124. package/packages/web/dist/assets/index-JBA8axyA.js +0 -1
  125. package/packages/web/dist/assets/index-JkVHFtK5.js +0 -7
  126. package/packages/web/dist/assets/index-gMPUwT55.js +0 -1
  127. package/packages/web/dist/assets/index-wadc_0zT.js +0 -1
  128. package/packages/web/dist/assets/sessions-CwPsJOb1.js +0 -1
  129. package/packages/web/dist/assets/settings-BOj6wq6t.js +0 -1
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "circuschief",
3
- "version": "0.6.0",
3
+ "version": "0.7.0",
4
4
  "description": "Local-first web UI for managing Claude Code sessions",
5
5
  "type": "module",
6
6
  "bin": {
@@ -44,6 +44,26 @@ export function resolveDefault(explicit, projectDefault, systemDefault) {
44
44
  return systemDefault;
45
45
  }
46
46
 
47
+ /**
48
+ * Normalize provider IDs from JSON or multipart request fields.
49
+ * @param {*} value - Raw providerId value
50
+ * @returns {string|null|undefined}
51
+ */
52
+ export function normalizeProviderId(value) {
53
+ if (value === undefined) return undefined;
54
+ if (value === null || value === '') return null;
55
+ if (typeof value !== 'string') {
56
+ throw new TypeError('providerId must be a string or null');
57
+ }
58
+ return value;
59
+ }
60
+
61
+ function resolveProviderDefault(explicit, projectDefault, systemDefault) {
62
+ if (explicit !== undefined) return explicit;
63
+ if (projectDefault !== undefined && projectDefault !== null) return projectDefault;
64
+ return systemDefault ?? null;
65
+ }
66
+
47
67
  /**
48
68
  * Resolve thinkingEnabled with special boolean handling.
49
69
  * @param {object} body - Request body
@@ -114,11 +134,16 @@ export function prepareSessionConfig(body, projectDefs, systemDefaults) {
114
134
  effortLevel = null;
115
135
  }
116
136
 
137
+ const explicitProviderId = normalizeProviderId(body.providerId);
138
+ const projectProviderId = normalizeProviderId(projectDefs?.providerId);
139
+ const systemProviderId = normalizeProviderId(systemDefaults.providerId);
140
+
117
141
  return {
118
142
  prompt: body.prompt,
119
143
  name: body.name,
120
144
  mode: resolveDefault(body.mode, projectDefs?.mode, systemDefaults.mode),
121
145
  model: resolveDefault(body.model, projectDefs?.model, systemDefaults.model || null),
146
+ providerId: resolveProviderDefault(explicitProviderId, projectProviderId, systemProviderId),
122
147
  effortLevel,
123
148
  gitBranch: resolveDefault(body.gitBranch, projectDefs?.gitBranch, null),
124
149
  gitMode: resolveDefault(body.gitMode, projectDefs?.gitMode, null),
@@ -243,6 +243,7 @@ function createSessionRow(projectId, config, nextTemplateId, initialStatus) {
243
243
  parentSessionId: config.parentSessionId,
244
244
  status: initialStatus,
245
245
  model: config.model,
246
+ providerId: config.providerId,
246
247
  effortLevel: config.effortLevel,
247
248
  agentType: config.agentType,
248
249
  });
@@ -57,6 +57,7 @@ router.post('/:id/start', requireSession, async (req, res) => {
57
57
  const updatedSession = await startDraft(req.session_, {
58
58
  prompt: req.body.prompt,
59
59
  model: req.body.model,
60
+ providerId: req.body.providerId,
60
61
  });
61
62
 
62
63
  res.json({ success: true, session: updatedSession });
@@ -1,8 +1,9 @@
1
1
  import { Router } from 'express';
2
- import { settings } from '../db/index.js';
2
+ import { modelProviders, settings } from '../db/index.js';
3
3
  import { DEFAULT_SESSION_TITLE_PROMPT } from '../services/summaryService.js';
4
4
 
5
5
  const router = Router();
6
+ const SUPPORTED_SUMMARY_PROVIDER_KINDS = new Set(['anthropic', 'openai']);
6
7
 
7
8
  /**
8
9
  * GET /api/settings/token-weights
@@ -93,19 +94,42 @@ router.get('/summary', (req, res) => {
93
94
  */
94
95
  router.put('/summary', (req, res) => {
95
96
  try {
96
- const { disableSessionSummaries, sessionTitlePrompt } = req.body;
97
+ const body = req.body || {};
98
+ const {
99
+ disableSessionSummaries,
100
+ sessionTitlePrompt,
101
+ summaryModel,
102
+ summaryProviderId,
103
+ } = body;
97
104
 
98
105
  // Validate that all required fields are present
99
106
  if (typeof disableSessionSummaries !== 'boolean' ||
100
- typeof sessionTitlePrompt !== 'string') {
107
+ typeof sessionTitlePrompt !== 'string' ||
108
+ !Object.prototype.hasOwnProperty.call(body, 'summaryModel') ||
109
+ !Object.prototype.hasOwnProperty.call(body, 'summaryProviderId')) {
101
110
  return res.status(400).json({
102
- error: 'Invalid summary settings. disableSessionSummaries must be a boolean, sessionTitlePrompt must be a string'
111
+ error: 'Invalid summary settings. disableSessionSummaries must be a boolean, sessionTitlePrompt must be a string, summaryModel must be present, and summaryProviderId must be present'
103
112
  });
104
113
  }
105
114
 
115
+ if (typeof summaryModel !== 'string') {
116
+ return res.status(400).json({ error: 'summaryModel must be a string' });
117
+ }
118
+
119
+ if (summaryProviderId !== null && typeof summaryProviderId !== 'string') {
120
+ return res.status(400).json({ error: 'summaryProviderId must be a string or null' });
121
+ }
122
+
123
+ const validationError = validateSummaryModelSelection(summaryModel, summaryProviderId);
124
+ if (validationError) {
125
+ return res.status(400).json({ error: validationError });
126
+ }
127
+
106
128
  const updatedSettings = settings.setSummarySettings({
107
129
  disableSessionSummaries,
108
130
  sessionTitlePrompt,
131
+ summaryModel,
132
+ summaryProviderId,
109
133
  });
110
134
 
111
135
  // Include the default prompt for UI display/editing
@@ -119,6 +143,30 @@ router.put('/summary', (req, res) => {
119
143
  }
120
144
  });
121
145
 
146
+ function validateSummaryModelSelection(summaryModel, summaryProviderId) {
147
+ if (!summaryModel) {
148
+ if (summaryProviderId !== null) {
149
+ return 'summaryProviderId must be null when summaryModel is empty';
150
+ }
151
+ return null;
152
+ }
153
+
154
+ if (!summaryProviderId) {
155
+ return 'summaryProviderId is required when summaryModel is set';
156
+ }
157
+
158
+ const provider = modelProviders.getById(summaryProviderId);
159
+ if (!provider) return `Unknown summary provider: ${summaryProviderId}`;
160
+ if (!SUPPORTED_SUMMARY_PROVIDER_KINDS.has(provider.kind || 'anthropic')) {
161
+ return `Unsupported summary provider kind: ${provider.kind}`;
162
+ }
163
+ const ownsModel = provider.models?.some((model) => model.modelId === summaryModel);
164
+ if (!ownsModel) {
165
+ return `Provider ${summaryProviderId} does not own summary model ${summaryModel}`;
166
+ }
167
+ return null;
168
+ }
169
+
122
170
  /**
123
171
  * DELETE /api/settings/summary
124
172
  * Reset summary settings to defaults
@@ -237,6 +237,7 @@ export class ConversationRepository extends BaseRepository {
237
237
  * @param {Object} usage - Usage data
238
238
  * @param {number} usage.inputTokens
239
239
  * @param {number} usage.outputTokens
240
+ * @param {number} usage.thinkingTokens
240
241
  * @param {number} usage.cacheReadInputTokens
241
242
  * @param {number} usage.cacheCreationInputTokens
242
243
  * @param {number} usage.webSearchRequests
@@ -255,6 +256,7 @@ export class ConversationRepository extends BaseRepository {
255
256
  `UPDATE conversations SET
256
257
  input_tokens = ?,
257
258
  output_tokens = ?,
259
+ thinking_tokens = ?,
258
260
  cache_read_input_tokens = ?,
259
261
  cache_creation_input_tokens = ?,
260
262
  web_search_requests = ?,
@@ -265,6 +267,7 @@ export class ConversationRepository extends BaseRepository {
265
267
  .run(
266
268
  usage.inputTokens,
267
269
  usage.outputTokens,
270
+ usage.thinkingTokens || 0,
268
271
  usage.cacheReadInputTokens,
269
272
  usage.cacheCreationInputTokens,
270
273
  usage.webSearchRequests,
@@ -290,9 +293,12 @@ export class ConversationRepository extends BaseRepository {
290
293
  const now = Date.now();
291
294
 
292
295
  this.db
293
- .prepare(
294
- `INSERT INTO conversations (id, session_id, name, summary, is_active, created_at, updated_at)
295
- VALUES (?, ?, ?, ?, ?, ?, ?)`
296
+ .prepare(
297
+ `INSERT INTO conversations (id, session_id, name, summary, is_active,
298
+ input_tokens, output_tokens, thinking_tokens,
299
+ cache_read_input_tokens, cache_creation_input_tokens,
300
+ web_search_requests, context_window, created_at, updated_at)
301
+ VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`
296
302
  )
297
303
  .run(
298
304
  id,
@@ -300,6 +306,13 @@ export class ConversationRepository extends BaseRepository {
300
306
  conv.name,
301
307
  conv.summary,
302
308
  conv.isActive ? 1 : 0,
309
+ conv.inputTokens,
310
+ conv.outputTokens,
311
+ conv.thinkingTokens,
312
+ conv.cacheReadInputTokens,
313
+ conv.cacheCreationInputTokens,
314
+ conv.webSearchRequests,
315
+ conv.contextWindow,
303
316
  now,
304
317
  now
305
318
  );
@@ -12,6 +12,7 @@ const UPSERT_FIELD_MAP = [
12
12
  { key: 'gitMode', column: 'git_mode' },
13
13
  { key: 'gitBranch', column: 'git_branch' },
14
14
  { key: 'model', column: 'model' },
15
+ { key: 'providerId', column: 'provider_id' },
15
16
  { key: 'effortLevel', column: 'effort_level' },
16
17
  ];
17
18
 
@@ -47,6 +48,7 @@ export class ProjectDefaultsRepository extends BaseRepository {
47
48
  gitMode: row.git_mode || null,
48
49
  gitBranch: row.git_branch || null,
49
50
  model: row.model || null,
51
+ providerId: row.provider_id || null,
50
52
  effortLevel: row.effort_level || null,
51
53
  createdAt: row.created_at,
52
54
  updatedAt: row.updated_at,
@@ -72,47 +74,53 @@ export class ProjectDefaultsRepository extends BaseRepository {
72
74
  * @param {Object} data - Defaults data (all fields optional)
73
75
  * @returns {Object} Updated defaults object
74
76
  */
77
+ #insertDefaults(projectId, data) {
78
+ const id = databaseManager.generateId();
79
+ const now = Date.now();
80
+
81
+ this.db
82
+ .prepare(
83
+ `INSERT INTO project_session_defaults
84
+ (id, project_id, mode, thinking_enabled, start_immediately, git_mode, git_branch, model, provider_id, effort_level, created_at, updated_at)
85
+ VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`
86
+ )
87
+ .run(
88
+ id,
89
+ projectId,
90
+ data.mode || null,
91
+ data.thinkingEnabled !== undefined ? (data.thinkingEnabled ? 1 : 0) : null,
92
+ data.startImmediately !== undefined ? (data.startImmediately ? 1 : 0) : null,
93
+ data.gitMode || null,
94
+ data.gitBranch || null,
95
+ data.model || null,
96
+ data.providerId || null,
97
+ data.effortLevel || null,
98
+ now,
99
+ now
100
+ );
101
+ }
102
+
103
+ #updateDefaults(projectId, data) {
104
+ const { updates, values } = buildUpdateFields(data);
105
+
106
+ if (updates.length > 0) {
107
+ updates.push('updated_at = ?');
108
+ values.push(Date.now());
109
+ values.push(projectId);
110
+
111
+ this.db
112
+ .prepare(`UPDATE project_session_defaults SET ${updates.join(', ')} WHERE project_id = ?`)
113
+ .run(...values);
114
+ }
115
+ }
116
+
75
117
  upsert(projectId, data) {
76
- // Get existing defaults if any
77
118
  const existing = this.getByProjectId(projectId);
78
119
 
79
120
  if (!existing) {
80
- // Create new defaults record
81
- const id = databaseManager.generateId();
82
- const now = Date.now();
83
-
84
- this.db
85
- .prepare(
86
- `INSERT INTO project_session_defaults
87
- (id, project_id, mode, thinking_enabled, start_immediately, git_mode, git_branch, model, effort_level, created_at, updated_at)
88
- VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`
89
- )
90
- .run(
91
- id,
92
- projectId,
93
- data.mode || null,
94
- data.thinkingEnabled !== undefined ? (data.thinkingEnabled ? 1 : 0) : null,
95
- data.startImmediately !== undefined ? (data.startImmediately ? 1 : 0) : null,
96
- data.gitMode || null,
97
- data.gitBranch || null,
98
- data.model || null,
99
- data.effortLevel || null,
100
- now,
101
- now
102
- );
121
+ this.#insertDefaults(projectId, data);
103
122
  } else {
104
- // Update existing defaults
105
- const { updates, values } = buildUpdateFields(data);
106
-
107
- if (updates.length > 0) {
108
- updates.push('updated_at = ?');
109
- values.push(Date.now());
110
- values.push(projectId);
111
-
112
- this.db
113
- .prepare(`UPDATE project_session_defaults SET ${updates.join(', ')} WHERE project_id = ?`)
114
- .run(...values);
115
- }
123
+ this.#updateDefaults(projectId, data);
116
124
  }
117
125
 
118
126
  return this.getByProjectId(projectId);
@@ -137,7 +145,8 @@ export class ProjectDefaultsRepository extends BaseRepository {
137
145
  .prepare(
138
146
  `UPDATE project_session_defaults
139
147
  SET mode = NULL, thinking_enabled = NULL, start_immediately = NULL,
140
- git_mode = NULL, git_branch = NULL, model = NULL, effort_level = NULL, updated_at = ?
148
+ git_mode = NULL, git_branch = NULL, model = NULL, provider_id = NULL,
149
+ effort_level = NULL, updated_at = ?
141
150
  WHERE project_id = ?`
142
151
  )
143
152
  .run(Date.now(), projectId);
@@ -167,6 +176,7 @@ export class ProjectDefaultsRepository extends BaseRepository {
167
176
  gitMode: null,
168
177
  gitBranch: null,
169
178
  model: null,
179
+ providerId: null,
170
180
  effortLevel: null,
171
181
  };
172
182
  }
@@ -45,6 +45,7 @@ export class SessionRepository extends BaseRepository {
45
45
  status: row.status,
46
46
  mode: row.mode,
47
47
  model: row.model,
48
+ providerId: row.provider_id || null,
48
49
  thinkingEnabled: Boolean(row.thinking_enabled),
49
50
  archived: Boolean(row.archived),
50
51
  starred: Boolean(row.starred),
@@ -84,7 +85,7 @@ export class SessionRepository extends BaseRepository {
84
85
  return this.map(row);
85
86
  }
86
87
 
87
- /** Create a new session with optional config (mode, thinkingEnabled, gitBranch, parentSessionId, status, model, effortLevel, agentType) */
88
+ /** Create a new session with optional config (mode, thinkingEnabled, gitBranch, parentSessionId, status, model, providerId, effortLevel, agentType) */
88
89
  create(projectId, name, prompt, options = {}) {
89
90
  const config = parseCreateConfig(options, Array.prototype.slice.call(arguments, 4));
90
91
 
@@ -98,8 +99,8 @@ export class SessionRepository extends BaseRepository {
98
99
  const now = Date.now();
99
100
  this.db
100
101
  .prepare(
101
- `INSERT INTO sessions (id, project_id, name, status, mode, thinking_enabled, git_branch, parent_session_id, model, effort_level, agent_type, created_at, updated_at)
102
- VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`
102
+ `INSERT INTO sessions (id, project_id, name, status, mode, thinking_enabled, git_branch, parent_session_id, model, provider_id, effort_level, agent_type, created_at, updated_at)
103
+ VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`
103
104
  )
104
105
  .run(
105
106
  id,
@@ -111,6 +112,7 @@ export class SessionRepository extends BaseRepository {
111
112
  config.gitBranch,
112
113
  config.parentSessionId,
113
114
  config.model,
115
+ config.providerId,
114
116
  config.effortLevel,
115
117
  agentType,
116
118
  now,
@@ -282,10 +284,10 @@ export class SessionRepository extends BaseRepository {
282
284
  // Insert new session with same settings but new ID and status
283
285
  this.db
284
286
  .prepare(
285
- `INSERT INTO sessions (id, project_id, name, status, mode, thinking_enabled, git_branch, model, effort_level, agent_type, context_window,
286
- input_tokens, output_tokens, cache_read_input_tokens, cache_creation_input_tokens,
287
+ `INSERT INTO sessions (id, project_id, name, status, mode, thinking_enabled, git_branch, model, provider_id, effort_level, agent_type, context_window,
288
+ input_tokens, output_tokens, thinking_tokens, cache_read_input_tokens, cache_creation_input_tokens,
287
289
  web_search_requests, cost_usd, created_at, updated_at)
288
- VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`
290
+ VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`
289
291
  )
290
292
  .run(
291
293
  id,
@@ -296,11 +298,13 @@ export class SessionRepository extends BaseRepository {
296
298
  source.thinkingEnabled ? 1 : 0,
297
299
  source.gitBranch, // Copy branch name (NOT worktree path)
298
300
  source.model,
301
+ source.providerId,
299
302
  source.effortLevel,
300
303
  source.agentType || DEFAULT_AGENT_TYPE,
301
304
  source.contextWindow,
302
305
  source.inputTokens,
303
306
  source.outputTokens,
307
+ source.thinkingTokens,
304
308
  source.cacheReadInputTokens,
305
309
  source.cacheCreationInputTokens,
306
310
  source.webSearchRequests,
@@ -326,11 +330,11 @@ export class SessionRepository extends BaseRepository {
326
330
  updateUsage(id, usage) {
327
331
  this.db
328
332
  .prepare(
329
- `UPDATE sessions SET input_tokens = ?, output_tokens = ?, cache_read_input_tokens = ?,
333
+ `UPDATE sessions SET input_tokens = ?, output_tokens = ?, thinking_tokens = ?, cache_read_input_tokens = ?,
330
334
  cache_creation_input_tokens = ?, web_search_requests = ?, context_window = ?, updated_at = ?
331
335
  WHERE id = ?`
332
336
  )
333
- .run(usage.inputTokens, usage.outputTokens, usage.cacheReadInputTokens,
337
+ .run(usage.inputTokens, usage.outputTokens, usage.thinkingTokens || 0, usage.cacheReadInputTokens,
334
338
  usage.cacheCreationInputTokens, usage.webSearchRequests, usage.contextWindow, Date.now(), id);
335
339
  return this.getById(id);
336
340
  }
@@ -5,6 +5,13 @@ const TOKEN_WEIGHTS_KEY = 'token_cost_weights';
5
5
  const SUMMARY_SETTINGS_KEY = 'summary_settings';
6
6
  const GENERAL_SETTINGS_KEY = 'general_settings';
7
7
 
8
+ const DEFAULT_SUMMARY_SETTINGS = Object.freeze({
9
+ disableSessionSummaries: false,
10
+ sessionTitlePrompt: '',
11
+ summaryModel: '',
12
+ summaryProviderId: null,
13
+ });
14
+
8
15
  /**
9
16
  * Settings repository for managing application-wide settings
10
17
  */
@@ -114,22 +121,13 @@ export class SettingsRepository {
114
121
  getSummarySettings() {
115
122
  const value = this.get(SUMMARY_SETTINGS_KEY);
116
123
  if (!value) {
117
- return {
118
- disableSessionSummaries: false,
119
- sessionTitlePrompt: '',
120
- };
124
+ return { ...DEFAULT_SUMMARY_SETTINGS };
121
125
  }
122
126
  try {
123
127
  const parsed = JSON.parse(value);
124
- return {
125
- disableSessionSummaries: parsed.disableSessionSummaries || false,
126
- sessionTitlePrompt: parsed.sessionTitlePrompt || '',
127
- };
128
+ return normalizeStoredSummarySettings(parsed);
128
129
  } catch {
129
- return {
130
- disableSessionSummaries: false,
131
- sessionTitlePrompt: '',
132
- };
130
+ return { ...DEFAULT_SUMMARY_SETTINGS };
133
131
  }
134
132
  }
135
133
 
@@ -138,11 +136,22 @@ export class SettingsRepository {
138
136
  * @param {Object} settings - Summary settings
139
137
  * @param {boolean} settings.disableSessionSummaries - Disable session summaries
140
138
  * @param {string} settings.sessionTitlePrompt - Custom session title prompt
139
+ * @param {string} [settings.summaryModel] - Summary model id; empty string means auto
140
+ * @param {string|null} [settings.summaryProviderId] - Provider id owning summaryModel
141
141
  */
142
142
  setSummarySettings(settings) {
143
+ const summaryModel = String(settings.summaryModel || '');
144
+ const summaryProviderId = typeof settings.summaryProviderId === 'string'
145
+ ? settings.summaryProviderId
146
+ : null;
147
+ if (summaryModel && !summaryProviderId) {
148
+ throw new Error('summaryProviderId is required when summaryModel is set');
149
+ }
143
150
  const validated = {
144
151
  disableSessionSummaries: Boolean(settings.disableSessionSummaries),
145
152
  sessionTitlePrompt: String(settings.sessionTitlePrompt || ''),
153
+ summaryModel,
154
+ summaryProviderId: summaryModel ? summaryProviderId : null,
146
155
  };
147
156
  this.set(SUMMARY_SETTINGS_KEY, JSON.stringify(validated));
148
157
  return validated;
@@ -154,10 +163,7 @@ export class SettingsRepository {
154
163
  */
155
164
  resetSummarySettings() {
156
165
  this.delete(SUMMARY_SETTINGS_KEY);
157
- return {
158
- disableSessionSummaries: false,
159
- sessionTitlePrompt: '',
160
- };
166
+ return { ...DEFAULT_SUMMARY_SETTINGS };
161
167
  }
162
168
 
163
169
  // General Settings
@@ -209,3 +215,25 @@ export class SettingsRepository {
209
215
  };
210
216
  }
211
217
  }
218
+
219
+ function normalizeStoredSummarySettings(parsed) {
220
+ if (!parsed || typeof parsed !== 'object' || Array.isArray(parsed)) {
221
+ return { ...DEFAULT_SUMMARY_SETTINGS };
222
+ }
223
+
224
+ const summaryModel = typeof parsed.summaryModel === 'string' ? parsed.summaryModel : '';
225
+ const summaryProviderId = typeof parsed.summaryProviderId === 'string' && parsed.summaryProviderId
226
+ ? parsed.summaryProviderId
227
+ : null;
228
+
229
+ if (summaryModel && !summaryProviderId) {
230
+ return { ...DEFAULT_SUMMARY_SETTINGS };
231
+ }
232
+
233
+ return {
234
+ disableSessionSummaries: Boolean(parsed.disableSessionSummaries),
235
+ sessionTitlePrompt: typeof parsed.sessionTitlePrompt === 'string' ? parsed.sessionTitlePrompt : '',
236
+ summaryModel,
237
+ summaryProviderId: summaryModel ? summaryProviderId : null,
238
+ };
239
+ }
@@ -22,6 +22,7 @@ export function mapConversationRow(row) {
22
22
  // Token usage fields
23
23
  inputTokens: row.input_tokens || 0,
24
24
  outputTokens: row.output_tokens || 0,
25
+ thinkingTokens: row.thinking_tokens || 0,
25
26
  cacheReadInputTokens: row.cache_read_input_tokens || 0,
26
27
  cacheCreationInputTokens: row.cache_creation_input_tokens || 0,
27
28
  webSearchRequests: row.web_search_requests || 0,
@@ -113,6 +113,10 @@ export const conversationsMigrations = [
113
113
  name: 'conversations-add-output_tokens',
114
114
  up(db) { addColumnIfMissing(db, TABLE_CONVERSATIONS, 'output_tokens', COL_INTEGER_DEFAULT_0); },
115
115
  },
116
+ {
117
+ name: 'conversations-add-thinking_tokens',
118
+ up(db) { addColumnIfMissing(db, TABLE_CONVERSATIONS, 'thinking_tokens', COL_INTEGER_DEFAULT_0); },
119
+ },
116
120
  {
117
121
  name: 'conversations-add-cache_read_input_tokens',
118
122
  up(db) { addColumnIfMissing(db, TABLE_CONVERSATIONS, 'cache_read_input_tokens', COL_INTEGER_DEFAULT_0); },
@@ -114,6 +114,7 @@ export const allMigrations = validateMigrations([
114
114
  c.get('conversations-add-claude_session_id'),
115
115
  c.get('conversations-add-input_tokens'),
116
116
  c.get('conversations-add-output_tokens'),
117
+ c.get('conversations-add-thinking_tokens'),
117
118
  c.get('conversations-add-cache_read_input_tokens'),
118
119
  c.get('conversations-add-cache_creation_input_tokens'),
119
120
  c.get('conversations-add-web_search_requests'),
@@ -123,6 +124,7 @@ export const allMigrations = validateMigrations([
123
124
  // --- Sessions token usage ---
124
125
  s.get('sessions-add-input_tokens'),
125
126
  s.get('sessions-add-output_tokens'),
127
+ s.get('sessions-add-thinking_tokens'),
126
128
  s.get('sessions-add-cache_read_input_tokens'),
127
129
  s.get('sessions-add-cache_creation_input_tokens'),
128
130
  s.get('sessions-add-web_search_requests'),
@@ -32,6 +32,7 @@ const SESSIONS_BASE_COLUMNS = `
32
32
  parent_session_id TEXT REFERENCES sessions(id) ON DELETE SET NULL,
33
33
  input_tokens INTEGER DEFAULT 0,
34
34
  output_tokens INTEGER DEFAULT 0,
35
+ thinking_tokens INTEGER DEFAULT 0,
35
36
  cache_read_input_tokens INTEGER DEFAULT 0,
36
37
  cache_creation_input_tokens INTEGER DEFAULT 0,
37
38
  web_search_requests INTEGER DEFAULT 0,
@@ -59,7 +60,7 @@ const SESSIONS_ALL_COLUMNS = [
59
60
  'id', 'project_id', 'name', 'status', 'mode', 'thinking_enabled',
60
61
  'git_branch', 'git_worktree', 'pr_url', 'error', 'effort_level',
61
62
  'cost_usd', 'claude_session_id', 'model', 'next_template_id',
62
- 'parent_session_id', 'input_tokens', 'output_tokens',
63
+ 'parent_session_id', 'input_tokens', 'output_tokens', 'thinking_tokens',
63
64
  'cache_read_input_tokens', 'cache_creation_input_tokens',
64
65
  'web_search_requests', 'context_window', 'archived', 'starred',
65
66
  'manually_named', 'scheduled_at', 'reschedule_delay_minutes',
@@ -211,6 +212,10 @@ export const sessionsMigrations = [
211
212
  name: 'sessions-add-output_tokens',
212
213
  up(db) { addColumnIfMissing(db, TABLE_SESSIONS, 'output_tokens', COL_INTEGER_DEFAULT_0); },
213
214
  },
215
+ {
216
+ name: 'sessions-add-thinking_tokens',
217
+ up(db) { addColumnIfMissing(db, TABLE_SESSIONS, 'thinking_tokens', COL_INTEGER_DEFAULT_0); },
218
+ },
214
219
  {
215
220
  name: 'sessions-add-cache_read_input_tokens',
216
221
  up(db) { addColumnIfMissing(db, TABLE_SESSIONS, 'cache_read_input_tokens', COL_INTEGER_DEFAULT_0); },
@@ -24,6 +24,7 @@ export function mapTokenUsage(row) {
24
24
  return {
25
25
  inputTokens: row.input_tokens || 0,
26
26
  outputTokens: row.output_tokens || 0,
27
+ thinkingTokens: row.thinking_tokens || 0,
27
28
  cacheReadInputTokens: row.cache_read_input_tokens || 0,
28
29
  cacheCreationInputTokens: row.cache_creation_input_tokens || 0,
29
30
  webSearchRequests: row.web_search_requests || 0,
@@ -58,6 +59,7 @@ const CONFIG_DEFAULTS = {
58
59
  parentSessionId: null,
59
60
  status: 'starting',
60
61
  model: null,
62
+ providerId: null,
61
63
  effortLevel: null,
62
64
  // Agent runtime for the session: 'claude-code' (default) or 'codex'.
63
65
  // Defaults to null so SessionRepository.create() can resolve it from the model.
@@ -100,6 +102,7 @@ export const DIRECT_FIELD_MAP = {
100
102
  costUsd: 'cost_usd',
101
103
  claudeSessionId: 'claude_session_id',
102
104
  model: 'model',
105
+ providerId: 'provider_id',
103
106
  nextTemplateId: 'next_template_id',
104
107
  parentSessionId: 'parent_session_id',
105
108
  scheduledAt: 'scheduled_at',
@@ -48,6 +48,13 @@ CREATE TABLE IF NOT EXISTS sessions (
48
48
  claude_session_id TEXT,
49
49
  next_template_id TEXT REFERENCES session_templates(id) ON DELETE SET NULL,
50
50
  parent_session_id TEXT REFERENCES sessions(id) ON DELETE SET NULL,
51
+ input_tokens INTEGER DEFAULT 0,
52
+ output_tokens INTEGER DEFAULT 0,
53
+ thinking_tokens INTEGER DEFAULT 0,
54
+ cache_read_input_tokens INTEGER DEFAULT 0,
55
+ cache_creation_input_tokens INTEGER DEFAULT 0,
56
+ web_search_requests INTEGER DEFAULT 0,
57
+ context_window INTEGER DEFAULT 200000,
51
58
  created_at INTEGER NOT NULL DEFAULT (unixepoch() * 1000),
52
59
  updated_at INTEGER NOT NULL DEFAULT (unixepoch() * 1000)
53
60
  );
@@ -64,6 +71,7 @@ CREATE TABLE IF NOT EXISTS conversations (
64
71
  -- Token usage fields (per-conversation tracking)
65
72
  input_tokens INTEGER DEFAULT 0,
66
73
  output_tokens INTEGER DEFAULT 0,
74
+ thinking_tokens INTEGER DEFAULT 0,
67
75
  cache_read_input_tokens INTEGER DEFAULT 0,
68
76
  cache_creation_input_tokens INTEGER DEFAULT 0,
69
77
  web_search_requests INTEGER DEFAULT 0,
@@ -20,7 +20,7 @@ export class AgentCallLogger {
20
20
  const callId = nanoid();
21
21
 
22
22
  // Build metadata object - only include keys with defined values
23
- const metadata = {};
23
+ const metadata = { ...(meta.metadata || {}) };
24
24
  if (meta.effortLevel !== undefined && meta.effortLevel !== null) {
25
25
  metadata.effortLevel = meta.effortLevel;
26
26
  }