circuschief 0.7.0 → 1.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (185) hide show
  1. package/package.json +1 -1
  2. package/packages/server/src/agents/adapters/CodexAdapter.js +5 -4
  3. package/packages/server/src/api/canvas.js +22 -57
  4. package/packages/server/src/api/commandButtons.js +16 -15
  5. package/packages/server/src/api/index.js +2 -0
  6. package/packages/server/src/api/kanban.js +4 -2
  7. package/packages/server/src/api/projects-commandButtons.js +6 -6
  8. package/packages/server/src/api/projects-helpers.js +20 -3
  9. package/packages/server/src/api/projects-session-create.js +109 -0
  10. package/packages/server/src/api/projects-session-defaults.js +51 -0
  11. package/packages/server/src/api/projects-session-helpers.js +57 -5
  12. package/packages/server/src/api/projects-templates.js +38 -0
  13. package/packages/server/src/api/projects.js +28 -170
  14. package/packages/server/src/api/providers.js +11 -1
  15. package/packages/server/src/api/sessions-commands.js +46 -25
  16. package/packages/server/src/api/sessions-lifecycle.js +10 -10
  17. package/packages/server/src/api/sessions-patch.js +45 -1
  18. package/packages/server/src/api/sessions.js +6 -5
  19. package/packages/server/src/database.js +0 -2
  20. package/packages/server/src/db/DatabaseManager.js +5 -1
  21. package/packages/server/src/db/ProjectDefaultsRepository.js +3 -3
  22. package/packages/server/src/db/ProviderRepository.js +87 -32
  23. package/packages/server/src/db/SessionRepository.js +2 -1
  24. package/packages/server/src/db/SessionTemplateRepository.js +23 -2
  25. package/packages/server/src/db/index.js +0 -3
  26. package/packages/server/src/db/migrations/index.js +60 -7
  27. package/packages/server/src/db/migrations/kanbanMigrations.js +1 -1
  28. package/packages/server/src/db/migrations/miscMigrations.js +59 -184
  29. package/packages/server/src/db/migrations/projectsMigrations.js +31 -0
  30. package/packages/server/src/db/migrations/providerCommitAttributionMigrations.js +30 -0
  31. package/packages/server/src/db/migrations/providerMigrations.js +165 -0
  32. package/packages/server/src/db/migrations/sessionTableRecreate.js +136 -0
  33. package/packages/server/src/db/migrations/sessionsMigrations.js +18 -5
  34. package/packages/server/src/db/seedBaselineData.js +137 -0
  35. package/packages/server/src/db/session-helpers.js +32 -4
  36. package/packages/server/src/middleware/sessionLookup.js +81 -8
  37. package/packages/server/src/schema.sql +153 -132
  38. package/packages/server/src/scripts/backupDatabase.js +21 -0
  39. package/packages/server/src/scripts/dbUtils.js +81 -0
  40. package/packages/server/src/scripts/inspectDatabaseSchema.js +81 -0
  41. package/packages/server/src/scripts/validateDatabaseBaseline.js +212 -0
  42. package/packages/server/src/services/codexSpawnHelper.js +9 -0
  43. package/packages/server/src/services/commandButtonPrompts.js +14 -12
  44. package/packages/server/src/services/commandRunner.js +7 -1
  45. package/packages/server/src/services/e2eSpawnCapture.js +147 -0
  46. package/packages/server/src/services/gitCommitAttribution.js +150 -0
  47. package/packages/server/src/services/gitDiff.js +132 -0
  48. package/packages/server/src/services/gitRepoUrl.js +174 -0
  49. package/packages/server/src/services/gitService.js +48 -311
  50. package/packages/server/src/services/gitSessionSetup.js +11 -1
  51. package/packages/server/src/services/gitWorktree.js +127 -0
  52. package/packages/server/src/services/kanbanTriggers.js +6 -3
  53. package/packages/server/src/services/nodeSpawnHelper.js +9 -0
  54. package/packages/server/src/services/prUrlService.js +3 -3
  55. package/packages/server/src/services/queryParamBuilder.js +90 -0
  56. package/packages/server/src/services/sessionDuplicator.js +1 -5
  57. package/packages/server/src/services/sessionExecution.js +56 -108
  58. package/packages/server/src/services/sessionPrompts.js +12 -47
  59. package/packages/server/src/services/sessionProvider.js +10 -0
  60. package/packages/server/src/services/summaryService.js +5 -3
  61. package/packages/server/src/services/summaryStaleCheck.js +23 -4
  62. package/packages/server/src/services/templateTriggerService.js +3 -1
  63. package/packages/shared/src/constants.js +3 -0
  64. package/packages/shared/src/contracts/commandButtons.js +16 -2
  65. package/packages/shared/src/contracts/projects.js +2 -2
  66. package/packages/shared/src/contracts/providers.js +60 -0
  67. package/packages/shared/src/contracts/sessions.js +29 -2
  68. package/packages/shared/src/contracts/templates.js +12 -2
  69. package/packages/shared/src/types.js +1 -9
  70. package/packages/shared/src/utils.js +2 -2
  71. package/packages/web/dist/assets/{ActiveSessionsView-UJsCILDL.js → ActiveSessionsView-Cxh8mHmB.js} +1 -1
  72. package/packages/web/dist/assets/{AgentLogsView-BGFPLjLa.js → AgentLogsView-xdfI2bR6.js} +2 -2
  73. package/packages/web/dist/assets/ApiClient-DfbJwzpz.js +1 -0
  74. package/packages/web/dist/assets/ArchiveConfirmModal-DXZYdzHR.js +1 -0
  75. package/packages/web/dist/assets/ArchiveConfirmModal-DeoCVGXt.css +1 -0
  76. package/packages/web/dist/assets/CommandButtonDetailView-D8xfqLAp.js +1 -0
  77. package/packages/web/dist/assets/CommandButtonDetailView-D9zjx9ME.css +1 -0
  78. package/packages/web/dist/assets/EffortLevelSelector-D2Hdzc_8.js +1 -0
  79. package/packages/web/dist/assets/{GeneralSettingsView-DsHChEhv.js → GeneralSettingsView-sPXkLlLy.js} +1 -1
  80. package/packages/web/dist/assets/InputWithButton-B-o0DgMH.js +1 -0
  81. package/packages/web/dist/assets/{InputWithButton-cYdrEmTs.css → InputWithButton-D9HMvfR5.css} +1 -1
  82. package/packages/web/dist/assets/{InterpolationHelp-CIkOSkWX.js → InterpolationHelp-Dxn1li4l.js} +1 -1
  83. package/packages/web/dist/assets/MarkdownEditor-D4Kbb-9l.js +2 -0
  84. package/packages/web/dist/assets/ModelSelector-72C7MUH4.js +1 -0
  85. package/packages/web/dist/assets/{ModelSelector-D8hbTRIt.css → ModelSelector-BNYKujL-.css} +1 -1
  86. package/packages/web/dist/assets/NewSessionView-BR_COfgW.js +3 -0
  87. package/packages/web/dist/assets/NewSessionView-DBl7T2Xp.css +1 -0
  88. package/packages/web/dist/assets/ProjectEditView-DbqTbA0q.css +1 -0
  89. package/packages/web/dist/assets/ProjectEditView-WImU7sNd.js +1 -0
  90. package/packages/web/dist/assets/{ProjectListView-B9FuWESY.js → ProjectListView-CYmmAcBD.js} +1 -1
  91. package/packages/web/dist/assets/{ProjectNewView-D62jYlBL.js → ProjectNewView-DEhqw3Jv.js} +1 -1
  92. package/packages/web/dist/assets/ProvidersView-XZh3jkmH.js +1 -0
  93. package/packages/web/dist/assets/ProvidersView-bZemq_Rv.css +1 -0
  94. package/packages/web/dist/assets/QuickResponsesPanel-BqmnTd-D.js +1 -0
  95. package/packages/web/dist/assets/QuickResponsesPanel-dk-Rj8xx.css +1 -0
  96. package/packages/web/dist/assets/ResizableTextarea-BQNw5e0C.css +1 -0
  97. package/packages/web/dist/assets/ResizableTextarea-DpWdIAP6.js +1 -0
  98. package/packages/web/dist/assets/SessionCard-Bw77-KwD.js +1 -0
  99. package/packages/web/dist/assets/SessionDetailView-B59TEkr-.js +36 -0
  100. package/packages/web/dist/assets/SessionDetailView-CKVBnR4T.css +1 -0
  101. package/packages/web/dist/assets/{SessionFormOptions-DYUISplS.js → SessionFormOptions-hqijxc0S.js} +1 -1
  102. package/packages/web/dist/assets/SessionListView-3-xx6EVs.css +1 -0
  103. package/packages/web/dist/assets/SessionListView-DYXHM9I-.js +1 -0
  104. package/packages/web/dist/assets/{SessionLogStream-DpUE6Xsh.js → SessionLogStream-5NfVr9pF.js} +6 -6
  105. package/packages/web/dist/assets/{SettingsView-BC055tIA.js → SettingsView-DI8ncOAV.js} +1 -1
  106. package/packages/web/dist/assets/{SlashCommandWizard-DmTyNG9O.js → SlashCommandWizard-BQ_rMzn-.js} +1 -1
  107. package/packages/web/dist/assets/{SlashCommandWizard-Dn7sNaBd.css → SlashCommandWizard-DJzw3LP5.css} +1 -1
  108. package/packages/web/dist/assets/{SummarySettingsView-BgnRCwlq.js → SummarySettingsView-C2Qs35mm.js} +1 -1
  109. package/packages/web/dist/assets/TemplateDetailView-B5NI2oTR.css +1 -0
  110. package/packages/web/dist/assets/TemplateDetailView-zVkIvgtu.js +1 -0
  111. package/packages/web/dist/assets/{commandButtons-D4RPpLiu.js → commandButtons-CoU3G4zK.js} +1 -1
  112. package/packages/web/dist/assets/index-9yF1uCCA.js +1 -0
  113. package/packages/web/dist/assets/index-BKstCaYU.js +1 -0
  114. package/packages/web/dist/assets/index-BhbH7eOk.js +1 -0
  115. package/packages/web/dist/assets/{index-BGwH4Cfn.js → index-BjuRttEY.js} +3 -3
  116. package/packages/web/dist/assets/index-Bo7PdwM5.js +1 -0
  117. package/packages/web/dist/assets/index-C2QFVD7d.js +83 -0
  118. package/packages/web/dist/assets/index-C7Ww2auW.js +1 -0
  119. package/packages/web/dist/assets/index-CAGdsDh7.js +1 -0
  120. package/packages/web/dist/assets/index-CLRsVASf.js +3 -0
  121. package/packages/web/dist/assets/{index-Bn5xdGFM.js → index-CP-SxOlV.js} +1 -1
  122. package/packages/web/dist/assets/index-CslU0psO.js +1 -0
  123. package/packages/web/dist/assets/index-DI4NxaWD.js +1 -0
  124. package/packages/web/dist/assets/index-DOzONENy.js +1 -0
  125. package/packages/web/dist/assets/index-DUa7adFh.js +1 -0
  126. package/packages/web/dist/assets/index-DZBpETI5.js +1 -0
  127. package/packages/web/dist/assets/index-DsjWqc6R.js +7 -0
  128. package/packages/web/dist/assets/index-c99Bo3JV.js +1 -0
  129. package/packages/web/dist/assets/index-mT1JpxDc.js +1 -0
  130. package/packages/web/dist/assets/index-rkQx2tso.js +1 -0
  131. package/packages/web/dist/assets/{index-Cs2nxhrT.css → index-uySCcnA_.css} +1 -1
  132. package/packages/web/dist/assets/projectDefaults-B8esIcYq.js +1 -0
  133. package/packages/web/dist/assets/{projects-BUiOGmmb.js → projects-C-8PSxKi.js} +1 -1
  134. package/packages/web/dist/assets/{providers-Bh1ZiiJi.js → providers-oXifvvqN.js} +1 -1
  135. package/packages/web/dist/assets/sessions-Nq5VafSf.js +1 -0
  136. package/packages/web/dist/assets/{settings-Z4AVVmkJ.js → settings-DtpuiyT6.js} +1 -1
  137. package/packages/web/dist/index.html +2 -2
  138. package/packages/server/src/api/sessions-notes.js +0 -51
  139. package/packages/server/src/db/SessionNoteRepository.js +0 -60
  140. package/packages/web/dist/assets/ApiClient-B4YTtyY4.js +0 -1
  141. package/packages/web/dist/assets/ArchiveConfirmModal-BQ-4gI0R.css +0 -1
  142. package/packages/web/dist/assets/ArchiveConfirmModal-OFaj_uX5.js +0 -1
  143. package/packages/web/dist/assets/CommandButtonDetailView-D8S258uP.js +0 -1
  144. package/packages/web/dist/assets/CommandButtonDetailView-DBm3rzhw.css +0 -1
  145. package/packages/web/dist/assets/EffortLevelSelector-C2378L8e.js +0 -1
  146. package/packages/web/dist/assets/InputWithButton-Ci15ox0a.js +0 -1
  147. package/packages/web/dist/assets/MarkdownEditor-5-bexzUT.js +0 -2
  148. package/packages/web/dist/assets/ModelSelector-BMpR0DPr.js +0 -1
  149. package/packages/web/dist/assets/NewSessionView-BCqtIgWH.js +0 -3
  150. package/packages/web/dist/assets/NewSessionView-CUUdHkfv.css +0 -1
  151. package/packages/web/dist/assets/ProjectEditView-D9sK0fdH.css +0 -1
  152. package/packages/web/dist/assets/ProjectEditView-RFaxHhAX.js +0 -1
  153. package/packages/web/dist/assets/ProvidersView-DDKMIQWZ.js +0 -1
  154. package/packages/web/dist/assets/ProvidersView-DE82G_5W.css +0 -1
  155. package/packages/web/dist/assets/QuickResponseSettings-B8188A1D.css +0 -1
  156. package/packages/web/dist/assets/QuickResponseSettings-CDm5vwP7.js +0 -1
  157. package/packages/web/dist/assets/QuickResponsesPanel-BlFDvnZ2.css +0 -1
  158. package/packages/web/dist/assets/QuickResponsesPanel-DZ_Lre_l.js +0 -1
  159. package/packages/web/dist/assets/ResizableTextarea-DiIOEGjN.js +0 -1
  160. package/packages/web/dist/assets/ResizableTextarea-DsU3TVwF.css +0 -1
  161. package/packages/web/dist/assets/SessionCard-DmjnVYWn.js +0 -1
  162. package/packages/web/dist/assets/SessionDetailView-CL7nmfiB.js +0 -36
  163. package/packages/web/dist/assets/SessionDetailView-CupIkI7u.css +0 -1
  164. package/packages/web/dist/assets/SessionListView-BcxGz4aC.js +0 -1
  165. package/packages/web/dist/assets/SessionListView-fHlQyecX.css +0 -1
  166. package/packages/web/dist/assets/TemplateDetailView-BlhOmLUX.js +0 -1
  167. package/packages/web/dist/assets/TemplateDetailView-DT2m06W7.css +0 -1
  168. package/packages/web/dist/assets/index-4rhEeO0B.js +0 -1
  169. package/packages/web/dist/assets/index-9vb2KaAd.js +0 -1
  170. package/packages/web/dist/assets/index-B0CvZXuN.js +0 -7
  171. package/packages/web/dist/assets/index-B6G18FqB.js +0 -82
  172. package/packages/web/dist/assets/index-BUhvkAdF.js +0 -1
  173. package/packages/web/dist/assets/index-BcnkUk2o.js +0 -1
  174. package/packages/web/dist/assets/index-CNwkdB0T.js +0 -1
  175. package/packages/web/dist/assets/index-CfL84oGW.js +0 -1
  176. package/packages/web/dist/assets/index-CkmxO8Mm.js +0 -1
  177. package/packages/web/dist/assets/index-Cpy4-yv3.js +0 -1
  178. package/packages/web/dist/assets/index-CrAQJmoZ.js +0 -1
  179. package/packages/web/dist/assets/index-D6Ky9vJe.js +0 -3
  180. package/packages/web/dist/assets/index-DfrE0gAC.js +0 -1
  181. package/packages/web/dist/assets/index-KwEyz0F3.js +0 -1
  182. package/packages/web/dist/assets/index-OfCywayk.js +0 -1
  183. package/packages/web/dist/assets/index-PDesaJc6.js +0 -1
  184. package/packages/web/dist/assets/index-uB6nhSvz.js +0 -1
  185. package/packages/web/dist/assets/sessions-DH1R-NhV.js +0 -1
@@ -1,9 +1,11 @@
1
1
  import { Router } from 'express';
2
- import { sessions, sessionTemplates, modelProviders } from '../database.js';
2
+ import { sessions, sessionTemplates, modelProviders, sessionSummaries } from '../database.js';
3
3
  import { broadcastToSession, broadcastToProject } from '../websocket.js';
4
4
  import { WS_MESSAGE_TYPES } from '../../../shared/src/index.js';
5
5
  import * as summaryService from '../services/summaryService.js';
6
6
  import { setSessionNameFromPr } from '../services/prUrlService.js';
7
+ import { checkSessionCiStatusNow } from '../services/prStatusService.js';
8
+ import { broadcastSummaryUpdate } from '../services/summaryBroadcast.js';
7
9
  import { requireSession } from '../middleware/sessionLookup.js';
8
10
 
9
11
  const router = Router();
@@ -158,6 +160,10 @@ function buildUpdateData(body) {
158
160
  updateData.manuallyNamed = true;
159
161
  }
160
162
 
163
+ if (body.prUrl !== undefined) {
164
+ updateData.prUrlAutoLinkDisabled = updateData.prUrl === null;
165
+ }
166
+
161
167
  return { updateData };
162
168
  }
163
169
 
@@ -191,6 +197,30 @@ function broadcastSessionUpdate(sessionId, projectId, updated, updateData) {
191
197
  });
192
198
  }
193
199
 
200
+ /**
201
+ * Reset all PR state fields in the session summary when the PR URL changes or is cleared.
202
+ * This ensures stale PR state (e.g., "merged") doesn't persist for a different PR
203
+ * and doesn't block summary regeneration.
204
+ * @param {string} sessionId
205
+ * @param {string|null} projectId - For broadcasting to project subscribers
206
+ */
207
+ function resetPrStateForSession(sessionId, projectId) {
208
+ const existingSummary = sessionSummaries.getBySessionId(sessionId);
209
+ if (!existingSummary) return;
210
+
211
+ sessionSummaries.upsert(sessionId, {
212
+ prState: null,
213
+ prMerged: false,
214
+ hasMergeConflicts: false,
215
+ ciStatus: null,
216
+ ciFailures: [],
217
+ });
218
+
219
+ // Broadcast the reset to both session and project subscribers
220
+ const updatedSummary = sessionSummaries.getBySessionId(sessionId);
221
+ broadcastSummaryUpdate(sessionId, projectId, updatedSummary);
222
+ }
223
+
194
224
  // PATCH /api/sessions/:id - Update session settings
195
225
  router.patch('/:id', requireSession, (req, res) => {
196
226
  const { updateData, error } = buildUpdateData(req.body);
@@ -205,6 +235,15 @@ router.patch('/:id', requireSession, (req, res) => {
205
235
 
206
236
  const updated = sessions.update(req.params.id, updateData);
207
237
 
238
+ // Reset PR state when URL changes to a different PR or is cleared
239
+ const previousPrUrl = req.session_.prUrl;
240
+ const prUrlProvided = Object.prototype.hasOwnProperty.call(updateData, 'prUrl');
241
+ const prUrlChanged = prUrlProvided && previousPrUrl && previousPrUrl !== updateData.prUrl;
242
+
243
+ if (prUrlChanged) {
244
+ resetPrStateForSession(req.params.id, req.session_.projectId);
245
+ }
246
+
208
247
  // Propagate PR URL to parent session if set (not when clearing)
209
248
  if (updateData.prUrl) {
210
249
  summaryService.propagatePrUrlToParent(req.params.id, updateData.prUrl);
@@ -214,6 +253,11 @@ router.patch('/:id', requireSession, (req, res) => {
214
253
  setSessionNameFromPr(req.params.id, updateData.prUrl).catch(err => {
215
254
  console.error(`[Sessions API] Failed to set session name from PR:`, err);
216
255
  });
256
+
257
+ // Trigger immediate PR status check for the new/changed URL
258
+ checkSessionCiStatusNow(req.params.id).catch(err => {
259
+ console.error(`[Sessions API] Failed to check PR status after URL change:`, err);
260
+ });
217
261
  }
218
262
 
219
263
  broadcastSessionUpdate(req.params.id, req.session_.projectId, updated, updateData);
@@ -8,7 +8,6 @@ import { requireSession, requireSessionAndProject } from '../middleware/sessionL
8
8
  import { commandRunner } from '../services/commandRunner.js';
9
9
 
10
10
  // Import sub-routers
11
- import notesRouter from './sessions-notes.js';
12
11
  import conversationsRouter from './sessions-conversations.js';
13
12
  import commandsRouter from './sessions-commands.js';
14
13
  import patchRouter from './sessions-patch.js';
@@ -21,7 +20,6 @@ import draftRouter from './sessions-draft.js';
21
20
  const router = Router();
22
21
 
23
22
  // Mount sub-routers
24
- router.use('/', notesRouter);
25
23
  router.use('/', conversationsRouter);
26
24
  router.use('/', commandsRouter);
27
25
  router.use('/', patchRouter);
@@ -108,11 +106,14 @@ router.get('/:id', requireSession, (req, res) => {
108
106
  const allMessages = messages.getBySessionId(req.params.id);
109
107
  const hasResponses = allMessages.some(msg => msg.role === 'assistant');
110
108
 
111
- // Get command run statuses (latest run per button for this session)
109
+ // Session details are for the requested session; command runs are workflow-root scoped.
110
+ const commandRunSessionId = sessions.getRootSessionId(req.params.id) || req.params.id;
111
+
112
+ // Get command run statuses (latest run per button for this workflow)
112
113
  // Completed runs from DB
113
- const dbRuns = commandRuns.getLatestRunsForSession(req.params.id);
114
+ const dbRuns = commandRuns.getLatestRunsForSession(commandRunSessionId);
114
115
  // Currently running commands from memory - filter all runs for this session
115
- const allRunning = commandRunner.getRunsBySession(req.params.id);
116
+ const allRunning = commandRunner.getRunsBySession(commandRunSessionId);
116
117
  const runningRuns = allRunning.filter(run => run.status === 'running');
117
118
 
118
119
  // Build map of buttonId -> run data
@@ -10,7 +10,6 @@ export {
10
10
  MessageRepository,
11
11
  ConversationRepository,
12
12
  CanvasItemRepository,
13
- SessionNoteRepository,
14
13
  TodoRepository,
15
14
  WorkLogRepository,
16
15
  SessionSummaryRepository,
@@ -35,7 +34,6 @@ export {
35
34
  messages,
36
35
  conversations,
37
36
  canvasItems,
38
- sessionNotes,
39
37
  todos,
40
38
  workLogs,
41
39
  sessionSummaries,
@@ -4,6 +4,7 @@ import { fileURLToPath } from 'url';
4
4
  import { dirname, join } from 'path';
5
5
  import crypto from 'crypto';
6
6
  import { allMigrations } from './migrations/index.js';
7
+ import { seedBaselineData } from './seedBaselineData.js';
7
8
 
8
9
  const __filename = fileURLToPath(import.meta.url);
9
10
  const __dirname = dirname(__filename);
@@ -32,7 +33,10 @@ export class DatabaseManager {
32
33
  const schema = readFileSync(join(__dirname, '..', 'schema.sql'), 'utf-8');
33
34
  this.#db.exec(schema);
34
35
 
35
- // Run migrations for existing databases
36
+ // Seed required baseline data before future migrations run.
37
+ seedBaselineData(this.#db);
38
+
39
+ // Run post-baseline migrations for existing databases.
36
40
  this.#runMigrations();
37
41
 
38
42
  return this.#db;
@@ -170,10 +170,10 @@ export class ProjectDefaultsRepository extends BaseRepository {
170
170
  */
171
171
  static getSystemDefaults() {
172
172
  return {
173
- mode: 'standard',
174
- thinkingEnabled: false,
173
+ mode: 'yolo',
174
+ thinkingEnabled: true,
175
175
  startImmediately: true,
176
- gitMode: null,
176
+ gitMode: 'worktree',
177
177
  gitBranch: null,
178
178
  model: null,
179
179
  providerId: null,
@@ -1,6 +1,7 @@
1
1
  import { BaseRepository } from './BaseRepository.js';
2
2
  import { databaseManager } from './DatabaseManager.js';
3
3
  import { encrypt, decrypt } from '../services/encryption.js';
4
+ import { normalizeCommitAttributionOverride } from '../../../shared/src/contracts/providers.js';
4
5
 
5
6
  /**
6
7
  * Valid values for `providers.kind`. Maps 1:1 to an agent adapter:
@@ -18,6 +19,55 @@ export const AGENT_TYPE_BY_KIND = Object.freeze({
18
19
  openai: 'codex',
19
20
  });
20
21
 
22
+ const BUILT_IN_MUTABLE_FIELDS = Object.freeze(['commitAttributionOverride']);
23
+
24
+ const UPDATE_COLUMN_BUILDERS = Object.freeze({
25
+ name: (value) => ['name = ?', value],
26
+ baseUrl: (value) => ['base_url = ?', value],
27
+ authToken: (value) => ['auth_token = ?', encrypt(value)],
28
+ apiTimeoutMs: (value) => ['api_timeout_ms = ?', value],
29
+ additionalEnvVars: (value) => [
30
+ 'additional_env_vars = ?',
31
+ value ? JSON.stringify(value) : null,
32
+ ],
33
+ commitAttributionOverride: (value) => [
34
+ 'commit_attribution_override = ?',
35
+ normalizeCommitAttributionOverride(value),
36
+ ],
37
+ });
38
+
39
+ function validateBuiltInUpdate(provider, data) {
40
+ if (!provider.isBuiltIn) return;
41
+
42
+ const unsupportedFields = Object.keys(data || {}).filter(
43
+ (key) => !BUILT_IN_MUTABLE_FIELDS.includes(key)
44
+ );
45
+ if (unsupportedFields.length > 0) {
46
+ throw new Error(
47
+ `Built-in providers can only update commitAttributionOverride. Rejected fields: ${unsupportedFields.join(', ')}.`
48
+ );
49
+ }
50
+ }
51
+
52
+ function validateKindImmutable(data) {
53
+ if (!data || !Object.prototype.hasOwnProperty.call(data, 'kind')) return;
54
+
55
+ throw new Error(
56
+ "Provider kind is immutable after create. Delete and recreate the provider to change kind."
57
+ );
58
+ }
59
+
60
+ function buildUpdateColumns(data = {}) {
61
+ return Object.entries(UPDATE_COLUMN_BUILDERS).reduce((result, [field, buildColumn]) => {
62
+ if (data[field] === undefined) return result;
63
+
64
+ const [update, value] = buildColumn(data[field]);
65
+ result.updates.push(update);
66
+ result.values.push(value);
67
+ return result;
68
+ }, { updates: [], values: [] });
69
+ }
70
+
21
71
  /**
22
72
  * Provider repository class (replaces ModelProviderRepository).
23
73
  *
@@ -44,6 +94,7 @@ export class ProviderRepository extends BaseRepository {
44
94
  authToken: decrypt(row.auth_token),
45
95
  apiTimeoutMs: row.api_timeout_ms,
46
96
  additionalEnvVars: row.additional_env_vars ? JSON.parse(row.additional_env_vars) : null,
97
+ commitAttributionOverride: row.commit_attribution_override ?? null,
47
98
  isBuiltIn: row.is_built_in === 1,
48
99
  kind: row.kind || 'anthropic',
49
100
  createdAt: row.created_at,
@@ -83,6 +134,7 @@ export class ProviderRepository extends BaseRepository {
83
134
  authToken = null,
84
135
  apiTimeoutMs = null,
85
136
  additionalEnvVars = null,
137
+ commitAttributionOverride = null,
86
138
  kind = 'anthropic',
87
139
  } = data;
88
140
 
@@ -95,8 +147,8 @@ export class ProviderRepository extends BaseRepository {
95
147
 
96
148
  this.db
97
149
  .prepare(
98
- `INSERT INTO providers (id, name, base_url, auth_token, api_timeout_ms, additional_env_vars, kind, created_at, updated_at)
99
- VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)`
150
+ `INSERT INTO providers (id, name, base_url, auth_token, api_timeout_ms, additional_env_vars, commit_attribution_override, kind, created_at, updated_at)
151
+ VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`
100
152
  )
101
153
  .run(
102
154
  id,
@@ -105,6 +157,7 @@ export class ProviderRepository extends BaseRepository {
105
157
  encrypt(authToken),
106
158
  apiTimeoutMs,
107
159
  additionalEnvVars ? JSON.stringify(additionalEnvVars) : null,
160
+ normalizeCommitAttributionOverride(commitAttributionOverride),
108
161
  kind,
109
162
  now,
110
163
  now
@@ -143,38 +196,13 @@ export class ProviderRepository extends BaseRepository {
143
196
  * @returns {Object} Updated provider (with models array)
144
197
  */
145
198
  update(id, data) {
146
- // `kind` is immutable after create. Existing models + env wiring depend on it,
147
- // so changing it in place would silently corrupt sessions already attached to
148
- // this provider.
149
- if (data && Object.prototype.hasOwnProperty.call(data, 'kind')) {
150
- throw new Error(
151
- "Provider kind is immutable after create. Delete and recreate the provider to change kind."
152
- );
153
- }
199
+ const provider = this.getById(id);
200
+ if (!provider) return null;
154
201
 
155
- const updates = [];
156
- const values = [];
202
+ validateBuiltInUpdate(provider, data);
203
+ validateKindImmutable(data);
157
204
 
158
- if (data.name !== undefined) {
159
- updates.push('name = ?');
160
- values.push(data.name);
161
- }
162
- if (data.baseUrl !== undefined) {
163
- updates.push('base_url = ?');
164
- values.push(data.baseUrl);
165
- }
166
- if (data.authToken !== undefined) {
167
- updates.push('auth_token = ?');
168
- values.push(encrypt(data.authToken));
169
- }
170
- if (data.apiTimeoutMs !== undefined) {
171
- updates.push('api_timeout_ms = ?');
172
- values.push(data.apiTimeoutMs);
173
- }
174
- if (data.additionalEnvVars !== undefined) {
175
- updates.push('additional_env_vars = ?');
176
- values.push(data.additionalEnvVars ? JSON.stringify(data.additionalEnvVars) : null);
177
- }
205
+ const { updates, values } = buildUpdateColumns(data);
178
206
 
179
207
  if (updates.length > 0) {
180
208
  updates.push('updated_at = ?');
@@ -348,6 +376,33 @@ export class ProviderRepository extends BaseRepository {
348
376
  return provider;
349
377
  }
350
378
 
379
+ /**
380
+ * Look up provider metadata for a model without applying runtime env fallback
381
+ * rules. Unlike getProviderByModelId, this returns built-in Anthropic too.
382
+ *
383
+ * @param {string|null|undefined} modelId
384
+ * @returns {Object|null}
385
+ */
386
+ getProviderMetadataByModelId(modelId) {
387
+ if (!modelId) return null;
388
+
389
+ const tierNames = ['sonnet', 'opus', 'haiku'];
390
+ if (tierNames.includes(modelId.toLowerCase())) {
391
+ return this.getById('anthropic-default');
392
+ }
393
+
394
+ const row = this.db
395
+ .prepare(
396
+ `SELECT p.id FROM providers p
397
+ JOIN provider_models pm ON p.id = pm.provider_id
398
+ WHERE pm.model_id = ?
399
+ ORDER BY p.is_built_in ASC, p.name ASC`
400
+ )
401
+ .get(modelId);
402
+
403
+ return row ? this.getById(row.id) : null;
404
+ }
405
+
351
406
  /**
352
407
  * Resolve a provider's agent type from its id.
353
408
  * @param {string|null|undefined} providerId
@@ -53,6 +53,7 @@ export class SessionRepository extends BaseRepository {
53
53
  gitBranch: row.git_branch,
54
54
  gitWorktree: row.git_worktree,
55
55
  prUrl: row.pr_url,
56
+ prUrlAutoLinkDisabled: Boolean(row.pr_url_auto_link_disabled),
56
57
  error: row.error,
57
58
  costUsd: row.cost_usd,
58
59
  claudeSessionId: row.claude_session_id,
@@ -73,6 +74,7 @@ export class SessionRepository extends BaseRepository {
73
74
  createdAt: row.created_at,
74
75
  updatedAt: row.updated_at,
75
76
  lastActivityAt: row.last_activity_at ?? null,
77
+ lastMessageAt: row.last_message_at ?? null,
76
78
  activeTimeMs: row.active_time_ms || 0,
77
79
  };
78
80
  }
@@ -146,7 +148,6 @@ export class SessionRepository extends BaseRepository {
146
148
  }
147
149
 
148
150
  sql += ` ORDER BY
149
- starred DESC,
150
151
  COALESCE(last_activity_at, updated_at, created_at) DESC,
151
152
  updated_at DESC,
152
153
  created_at DESC,
@@ -23,6 +23,10 @@ export class SessionTemplateRepository extends BaseRepository {
23
23
  mode: row.mode || null,
24
24
  effortLevel: row.effort_level ?? null,
25
25
  targetLaneId: row.target_lane_id || null,
26
+ showInQuickResponses: Boolean(row.show_in_quick_responses),
27
+ quickResponseAutoSubmit: Boolean(row.quick_response_auto_submit),
28
+ quickResponseSortOrder: row.quick_response_sort_order ?? 0,
29
+ legacyQuickResponseId: row.legacy_quick_response_id || null,
26
30
  createdAt: row.created_at,
27
31
  updatedAt: row.updated_at,
28
32
  };
@@ -51,13 +55,22 @@ export class SessionTemplateRepository extends BaseRepository {
51
55
  return value ? 1 : 0;
52
56
  }
53
57
 
58
+ static #normalizeBoolean(value) {
59
+ return value ? 1 : 0;
60
+ }
61
+
54
62
  create(data) {
55
63
  const id = databaseManager.generateId();
56
64
  const now = Date.now();
57
65
  this.db
58
66
  .prepare(
59
- `INSERT INTO session_templates (id, project_id, name, prompt, next_template_id, thinking_enabled, git_branch, git_mode, model, mode, effort_level, target_lane_id, created_at, updated_at)
60
- VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`
67
+ `INSERT INTO session_templates (
68
+ id, project_id, name, prompt, next_template_id, thinking_enabled,
69
+ git_branch, git_mode, model, mode, effort_level, target_lane_id,
70
+ show_in_quick_responses, quick_response_auto_submit,
71
+ quick_response_sort_order, legacy_quick_response_id,
72
+ created_at, updated_at
73
+ ) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`
61
74
  )
62
75
  .run(
63
76
  id,
@@ -72,6 +85,10 @@ export class SessionTemplateRepository extends BaseRepository {
72
85
  data.mode !== undefined && data.mode !== null ? data.mode : null,
73
86
  data.effortLevel ?? null,
74
87
  data.targetLaneId || null,
88
+ SessionTemplateRepository.#normalizeBoolean(data.showInQuickResponses),
89
+ SessionTemplateRepository.#normalizeBoolean(data.quickResponseAutoSubmit),
90
+ data.quickResponseSortOrder ?? 0,
91
+ data.legacyQuickResponseId || null,
75
92
  now,
76
93
  now
77
94
  );
@@ -93,6 +110,10 @@ export class SessionTemplateRepository extends BaseRepository {
93
110
  mode: { column: 'mode', transform: (v) => v },
94
111
  effortLevel: { column: 'effort_level', transform: (v) => v },
95
112
  targetLaneId: { column: 'target_lane_id', transform: (v) => v },
113
+ showInQuickResponses: { column: 'show_in_quick_responses', transform: (v) => v ? 1 : 0 },
114
+ quickResponseAutoSubmit: { column: 'quick_response_auto_submit', transform: (v) => v ? 1 : 0 },
115
+ quickResponseSortOrder: { column: 'quick_response_sort_order', transform: (v) => v },
116
+ legacyQuickResponseId: { column: 'legacy_quick_response_id', transform: (v) => v || null },
96
117
  };
97
118
 
98
119
  /**
@@ -12,7 +12,6 @@ export { SessionTemplateRepository } from './SessionTemplateRepository.js';
12
12
  export { MessageRepository } from './MessageRepository.js';
13
13
  export { ConversationRepository } from './ConversationRepository.js';
14
14
  export { CanvasItemRepository } from './CanvasItemRepository.js';
15
- export { SessionNoteRepository } from './SessionNoteRepository.js';
16
15
  export { TodoRepository } from './TodoRepository.js';
17
16
  export { WorkLogRepository } from './WorkLogRepository.js';
18
17
  export { SessionSummaryRepository } from './SessionSummaryRepository.js';
@@ -35,7 +34,6 @@ import { ProjectDefaultsRepository } from './ProjectDefaultsRepository.js';
35
34
  import { MessageRepository } from './MessageRepository.js';
36
35
  import { ConversationRepository } from './ConversationRepository.js';
37
36
  import { CanvasItemRepository } from './CanvasItemRepository.js';
38
- import { SessionNoteRepository } from './SessionNoteRepository.js';
39
37
  import { TodoRepository } from './TodoRepository.js';
40
38
  import { WorkLogRepository } from './WorkLogRepository.js';
41
39
  import { SessionSummaryRepository } from './SessionSummaryRepository.js';
@@ -56,7 +54,6 @@ export const projectDefaults = new ProjectDefaultsRepository();
56
54
  export const messages = new MessageRepository();
57
55
  export const conversations = new ConversationRepository();
58
56
  export const canvasItems = new CanvasItemRepository();
59
- export const sessionNotes = new SessionNoteRepository();
60
57
  export const todos = new TodoRepository();
61
58
  export const workLogs = new WorkLogRepository();
62
59
  export const sessionSummaries = new SessionSummaryRepository();
@@ -14,6 +14,8 @@ import { conversationsMigrations } from './conversationsMigrations.js';
14
14
  import { canvasItemsMigrations } from './canvasItemsMigrations.js';
15
15
  import { miscMigrations } from './miscMigrations.js';
16
16
  import { kanbanMigrations } from './kanbanMigrations.js';
17
+ import { providerMigrations } from './providerMigrations.js';
18
+ import { providerCommitAttributionMigrations } from './providerCommitAttributionMigrations.js';
17
19
 
18
20
  /**
19
21
  * Build a lookup map from a migrations array keyed by migration name.
@@ -34,6 +36,41 @@ const c = toLookup(conversationsMigrations);
34
36
  const ci = toLookup(canvasItemsMigrations);
35
37
  const m = toLookup(miscMigrations);
36
38
  const k = toLookup(kanbanMigrations);
39
+ const pr = toLookup(providerMigrations);
40
+ const pca = toLookup(providerCommitAttributionMigrations);
41
+
42
+ /**
43
+ * Repair sessions whose parent link was lost during schema consolidation.
44
+ *
45
+ * Child sessions inherit their root session's worktree. When parent_session_id
46
+ * is missing, the final UUID segment of git_worktree can identify the owning
47
+ * root session. Only repair rows where that UUID belongs to another session in
48
+ * the same project.
49
+ */
50
+ export const repairMissingSessionParentsFromWorktree = {
51
+ name: 'repair-missing-session-parents-from-worktree',
52
+ up(db) {
53
+ db.prepare(`
54
+ UPDATE sessions
55
+ SET parent_session_id = (
56
+ SELECT parent.id
57
+ FROM sessions AS parent
58
+ WHERE parent.id = substr(sessions.git_worktree, length(sessions.git_worktree) - 35, 36)
59
+ AND parent.project_id = sessions.project_id
60
+ AND parent.id <> sessions.id
61
+ )
62
+ WHERE parent_session_id IS NULL
63
+ AND git_worktree IS NOT NULL
64
+ AND EXISTS (
65
+ SELECT 1
66
+ FROM sessions AS parent
67
+ WHERE parent.id = substr(sessions.git_worktree, length(sessions.git_worktree) - 35, 36)
68
+ AND parent.project_id = sessions.project_id
69
+ AND parent.id <> sessions.id
70
+ )
71
+ `).run();
72
+ },
73
+ };
37
74
 
38
75
  /**
39
76
  * Flat, ordered list of every migration, matching the original execution order
@@ -134,6 +171,7 @@ export const allMigrations = validateMigrations([
134
171
  s.get('sessions-add-archived'),
135
172
  s.get('sessions-add-starred'),
136
173
  s.get('sessions-add-manually_named'),
174
+ s.get('sessions-add-pr_url_auto_link_disabled'),
137
175
 
138
176
  // --- Project session defaults table ---
139
177
  p.get('project_session_defaults-create-table'),
@@ -158,6 +196,7 @@ export const allMigrations = validateMigrations([
158
196
  m.get('session_templates-add-model'),
159
197
  m.get('session_templates-add-mode'),
160
198
  m.get('session_templates-add-effort_level'),
199
+ m.get('session_templates-add-quick-response-fields'),
161
200
 
162
201
  // --- Conversation messages model ---
163
202
  c.get('conversation_messages-add-model'),
@@ -166,13 +205,15 @@ export const allMigrations = validateMigrations([
166
205
  m.get('app_settings-create-table'),
167
206
 
168
207
  // --- Legacy model_providers cleanup ---
169
- m.get('model_providers-cleanup-legacy'),
208
+ pr.get('model_providers-cleanup-legacy'),
170
209
 
171
210
  // --- Providers + provider_models tables + seed ---
172
- m.get('providers-create-tables'),
173
- m.get('providers-add-kind'),
174
- m.get('providers-seed-built-in'),
175
- m.get('providers-seed-built-in-openai'),
211
+ pr.get('providers-create-tables'),
212
+ pr.get('providers-add-kind'),
213
+ pr.get('providers-add-commit_attribution_override'),
214
+ pca.get('providers-normalize-commit_attribution_override'),
215
+ pr.get('providers-seed-built-in'),
216
+ pr.get('providers-seed-built-in-openai'),
176
217
 
177
218
  // --- Sessions provider_id (from providers FK) ---
178
219
  s.get('sessions-add-provider_id-from-providers'),
@@ -182,7 +223,7 @@ export const allMigrations = validateMigrations([
182
223
  p.get('project_session_defaults-add-effort_level'),
183
224
 
184
225
  // --- Update built-in models ---
185
- m.get('providers-update-built-in-models'),
226
+ pr.get('providers-update-built-in-models'),
186
227
 
187
228
  // --- Sessions agent_type ---
188
229
  s.get('sessions-add-agent_type'),
@@ -199,12 +240,24 @@ export const allMigrations = validateMigrations([
199
240
  k.get('kanban_lanes-add-on_enter_prompt'),
200
241
  k.get('kanban_lanes-add-agent-settings'),
201
242
 
243
+ // --- Sessions default mode / thinking defaults (table recreation) ---
244
+ s.get('sessions-migrate-default-mode-thinking'),
245
+
202
246
  // --- Seed default global quick responses ---
203
247
  m.get('quick_responses-seed-defaults'),
204
248
 
249
+ // --- Convert legacy quick responses into template-backed quick responses ---
250
+ m.get('session_templates-convert-quick-responses'),
251
+
205
252
  // --- Seed default global session templates ---
206
253
  m.get('session_templates-seed-defaults'),
207
254
 
208
255
  // --- Update built-in Opus model to 4.7 ---
209
- m.get('providers-update-built-in-opus-4-7'),
256
+ pr.get('providers-update-built-in-opus-4-7'),
257
+
258
+ // --- Project session defaults: add 'current' git mode ---
259
+ p.get('project_session_defaults-git_mode-add-current'),
260
+
261
+ // --- Repair missing session parent links from worktree paths ---
262
+ repairMissingSessionParentsFromWorktree,
210
263
  ]);
@@ -88,7 +88,7 @@ export const kanbanMigrations = [
88
88
  addColumnIfMissing(db, 'kanban_lanes', 'on_enter_effort_level', 'TEXT');
89
89
  addColumnIfMissing(db, 'kanban_lanes', 'on_enter_thinking_enabled', 'INTEGER');
90
90
  addColumnIfMissing(db, 'kanban_lanes', 'on_enter_auto_reschedule_enabled', 'INTEGER DEFAULT 0');
91
- addColumnIfMissing(db, 'kanban_lanes', 'on_enter_reschedule_delay_minutes', 'INTEGER DEFAULT 15');
91
+ addColumnIfMissing(db, 'kanban_lanes', 'on_enter_reschedule_delay_minutes', 'INTEGER DEFAULT 60'); // keep in sync with DEFAULT_RESCHEDULE_DELAY_MINUTES
92
92
  addColumnIfMissing(db, 'kanban_lanes', 'on_enter_reschedule_on_token_limit', 'INTEGER DEFAULT 1');
93
93
  addColumnIfMissing(db, 'kanban_lanes', 'on_enter_reschedule_on_service_error', 'INTEGER DEFAULT 1');
94
94
  addColumnIfMissing(db, 'kanban_lanes', 'on_enter_max_reschedule_count', 'INTEGER');