circuschief 0.8.0 → 1.1.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 (155) hide show
  1. package/package.json +1 -1
  2. package/packages/server/src/agents/AgentGateway.js +2 -0
  3. package/packages/server/src/agents/adapters/GeminiAdapter.js +105 -0
  4. package/packages/server/src/agents/adapters/cliUtils.js +15 -0
  5. package/packages/server/src/agents/adapters/codexCliRunner.js +1 -8
  6. package/packages/server/src/agents/adapters/geminiCliRunner.js +183 -0
  7. package/packages/server/src/agents/adapters/geminiEventMapper.js +195 -0
  8. package/packages/server/src/api/commandButtons.js +16 -15
  9. package/packages/server/src/api/projects-commandButtons.js +6 -6
  10. package/packages/server/src/api/projects-session-create.js +109 -0
  11. package/packages/server/src/api/projects-session-defaults.js +51 -0
  12. package/packages/server/src/api/projects-session-helpers.js +47 -1
  13. package/packages/server/src/api/projects-templates.js +38 -0
  14. package/packages/server/src/api/projects.js +28 -180
  15. package/packages/server/src/api/sessions-commands.js +21 -18
  16. package/packages/server/src/api/sessions-patch.js +41 -1
  17. package/packages/server/src/db/ProviderRepository.js +4 -2
  18. package/packages/server/src/db/SessionRepository.js +1 -1
  19. package/packages/server/src/db/SessionTemplateRepository.js +23 -2
  20. package/packages/server/src/db/migrations/canvasItemsMigrations.js +109 -0
  21. package/packages/server/src/db/migrations/conversationsMigrations.js +187 -0
  22. package/packages/server/src/db/migrations/index.js +234 -6
  23. package/packages/server/src/db/migrations/kanbanMigrations.js +99 -0
  24. package/packages/server/src/db/migrations/miscMigrations.js +244 -0
  25. package/packages/server/src/db/migrations/projectsMigrations.js +130 -0
  26. package/packages/server/src/db/migrations/providerCommitAttributionMigrations.js +30 -0
  27. package/packages/server/src/db/migrations/providerMigrations.js +250 -0
  28. package/packages/server/src/db/migrations/sessionTableRecreate.js +136 -0
  29. package/packages/server/src/db/migrations/sessionsMigrations.js +300 -0
  30. package/packages/server/src/db/seedBaselineData.js +23 -1
  31. package/packages/server/src/db/session-helpers.js +26 -1
  32. package/packages/server/src/schema.sql +5 -1
  33. package/packages/server/src/services/commandButtonPrompts.js +9 -7
  34. package/packages/server/src/services/e2eSpawnCapture.js +47 -6
  35. package/packages/server/src/services/geminiSpawnHelper.js +47 -0
  36. package/packages/server/src/services/gitCommitAttribution.js +38 -8
  37. package/packages/server/src/services/gitDiff.js +107 -0
  38. package/packages/server/src/services/gitRepoUrl.js +174 -0
  39. package/packages/server/src/services/gitService.js +43 -311
  40. package/packages/server/src/services/gitWorktree.js +127 -0
  41. package/packages/server/src/services/providerTestService.js +59 -1
  42. package/packages/server/src/services/queryParamBuilder.js +33 -1
  43. package/packages/server/src/services/sessionExecution.js +4 -0
  44. package/packages/server/src/services/sessionPrompts.js +23 -1
  45. package/packages/server/src/services/sessionProvider.js +41 -1
  46. package/packages/shared/src/constants.js +1 -1
  47. package/packages/shared/src/contracts/providers.js +1 -1
  48. package/packages/shared/src/contracts/sessions.js +27 -1
  49. package/packages/shared/src/contracts/templates.js +10 -0
  50. package/packages/shared/src/types.js +7 -0
  51. package/packages/web/dist/assets/{ActiveSessionsView-B0XHqLmv.js → ActiveSessionsView-EdNxmPmZ.js} +1 -1
  52. package/packages/web/dist/assets/{AgentLogsView-DmsjUMlB.js → AgentLogsView-C2wX0JPP.js} +2 -2
  53. package/packages/web/dist/assets/ApiClient-DfbJwzpz.js +1 -0
  54. package/packages/web/dist/assets/ArchiveConfirmModal-DJERn5XO.js +1 -0
  55. package/packages/web/dist/assets/CommandButtonDetailView-CBPI8-US.js +1 -0
  56. package/packages/web/dist/assets/CommandButtonDetailView-D9zjx9ME.css +1 -0
  57. package/packages/web/dist/assets/EffortLevelSelector-PaBpUveC.js +1 -0
  58. package/packages/web/dist/assets/{GeneralSettingsView-D1nI8_zk.js → GeneralSettingsView-Dw-x83R0.js} +1 -1
  59. package/packages/web/dist/assets/{InputWithButton-CAkttyqx.js → InputWithButton-CHHcpF4I.js} +1 -1
  60. package/packages/web/dist/assets/{InterpolationHelp-BO1j9Z3_.js → InterpolationHelp-CLNPz8s8.js} +1 -1
  61. package/packages/web/dist/assets/MarkdownEditor-DYi1igfT.js +2 -0
  62. package/packages/web/dist/assets/ModelSelector-Cko_yTO5.js +1 -0
  63. package/packages/web/dist/assets/{ModelSelector-BSxKUSus.css → ModelSelector-Dtwe5xLH.css} +1 -1
  64. package/packages/web/dist/assets/{NewSessionView-BDPb-1qr.css → NewSessionView-DBl7T2Xp.css} +1 -1
  65. package/packages/web/dist/assets/NewSessionView-DwUfBg70.js +3 -0
  66. package/packages/web/dist/assets/ProjectEditView-CSbsea3U.js +1 -0
  67. package/packages/web/dist/assets/ProjectEditView-DbqTbA0q.css +1 -0
  68. package/packages/web/dist/assets/{ProjectListView-DcNyuINs.js → ProjectListView-CEc_LWZL.js} +1 -1
  69. package/packages/web/dist/assets/{ProjectNewView-B5YV62hv.js → ProjectNewView-D4U0uRlp.js} +1 -1
  70. package/packages/web/dist/assets/ProvidersView-2KCOiY6Q.css +1 -0
  71. package/packages/web/dist/assets/ProvidersView-CD1j8BOv.js +1 -0
  72. package/packages/web/dist/assets/QuickResponsesPanel-Dp39f12o.js +1 -0
  73. package/packages/web/dist/assets/QuickResponsesPanel-dk-Rj8xx.css +1 -0
  74. package/packages/web/dist/assets/ResizableTextarea-BWywIqOv.js +1 -0
  75. package/packages/web/dist/assets/ResizableTextarea-DERSH3Wz.css +1 -0
  76. package/packages/web/dist/assets/SessionCard-B6d5ijDW.js +1 -0
  77. package/packages/web/dist/assets/SessionDetailView-DWbXdx7A.js +36 -0
  78. package/packages/web/dist/assets/SessionDetailView-ULeIkWS0.css +1 -0
  79. package/packages/web/dist/assets/{SessionFormOptions-B6AxyREh.js → SessionFormOptions-Dz9ik4Fo.js} +1 -1
  80. package/packages/web/dist/assets/{SessionListView-B5_6gW49.css → SessionListView-3-xx6EVs.css} +1 -1
  81. package/packages/web/dist/assets/SessionListView-C129buBe.js +1 -0
  82. package/packages/web/dist/assets/{SessionLogStream-LlZ3z_Xj.js → SessionLogStream-BvXUNNBZ.js} +6 -6
  83. package/packages/web/dist/assets/{SettingsView-CTGiGvR2.js → SettingsView-DW1NvpX_.js} +1 -1
  84. package/packages/web/dist/assets/SlashCommandWizard-DleYBxrE.js +1 -0
  85. package/packages/web/dist/assets/{SummarySettingsView-BR2ZjEa3.js → SummarySettingsView-CLUfcWvf.js} +1 -1
  86. package/packages/web/dist/assets/TemplateDetailView-B5NI2oTR.css +1 -0
  87. package/packages/web/dist/assets/TemplateDetailView-Cukb205e.js +1 -0
  88. package/packages/web/dist/assets/{commandButtons-BfqR-fqq.js → commandButtons-DejH0rVN.js} +1 -1
  89. package/packages/web/dist/assets/index-BD7Y3rBE.js +3 -0
  90. package/packages/web/dist/assets/{index-BY174HVJ.css → index-Bd20AzX1.css} +1 -1
  91. package/packages/web/dist/assets/index-BgJiarKe.js +1 -0
  92. package/packages/web/dist/assets/index-Bk32fSSG.js +1 -0
  93. package/packages/web/dist/assets/index-BkA6pF2Z.js +1 -0
  94. package/packages/web/dist/assets/index-Cltr-Ldt.js +7 -0
  95. package/packages/web/dist/assets/index-Co-46Tp3.js +1 -0
  96. package/packages/web/dist/assets/index-Cpykk857.js +1 -0
  97. package/packages/web/dist/assets/index-CtABl0D1.js +1 -0
  98. package/packages/web/dist/assets/index-Cuqk5m9S.js +1 -0
  99. package/packages/web/dist/assets/{index-fK8FIZgP.js → index-CvXApbVC.js} +15 -15
  100. package/packages/web/dist/assets/index-D2gN-xEH.js +1 -0
  101. package/packages/web/dist/assets/index-Dd3WpmyQ.js +1 -0
  102. package/packages/web/dist/assets/index-Dk6--9rj.js +1 -0
  103. package/packages/web/dist/assets/{index-DgkC10TW.js → index-MZf7MlPX.js} +3 -3
  104. package/packages/web/dist/assets/{index-DtfUt785.js → index-NShCcwfj.js} +1 -1
  105. package/packages/web/dist/assets/index-hA3VEuSq.js +1 -0
  106. package/packages/web/dist/assets/index-p0mp3nca.js +1 -0
  107. package/packages/web/dist/assets/index-qntNa5r_.js +1 -0
  108. package/packages/web/dist/assets/index-qq9ceNSK.js +1 -0
  109. package/packages/web/dist/assets/projectDefaults-D9xkp2XR.js +1 -0
  110. package/packages/web/dist/assets/{projects-DXYQNJIi.js → projects-BvLADGKx.js} +1 -1
  111. package/packages/web/dist/assets/{providers-1bnH-exJ.js → providers-DZ-fOa4G.js} +1 -1
  112. package/packages/web/dist/assets/{sessions-6zGUlFrt.js → sessions-DETEyjPI.js} +1 -1
  113. package/packages/web/dist/assets/{settings-MbfRir0d.js → settings-TWfbahn5.js} +1 -1
  114. package/packages/web/dist/index.html +2 -2
  115. package/packages/web/dist/assets/ApiClient-C3ztI9s9.js +0 -1
  116. package/packages/web/dist/assets/ArchiveConfirmModal-BlCyn5Vt.js +0 -1
  117. package/packages/web/dist/assets/CommandButtonDetailView-CdSCPp78.js +0 -1
  118. package/packages/web/dist/assets/CommandButtonDetailView-DBm3rzhw.css +0 -1
  119. package/packages/web/dist/assets/EffortLevelSelector-hc2MNKg6.js +0 -1
  120. package/packages/web/dist/assets/MarkdownEditor-ucRAP_UM.js +0 -2
  121. package/packages/web/dist/assets/ModelSelector-CwTz8ZWO.js +0 -1
  122. package/packages/web/dist/assets/NewSessionView-BsDrp8mj.js +0 -3
  123. package/packages/web/dist/assets/ProjectEditView-CwTOeSun.js +0 -1
  124. package/packages/web/dist/assets/ProjectEditView-J15mcsWz.css +0 -1
  125. package/packages/web/dist/assets/ProvidersView-bZemq_Rv.css +0 -1
  126. package/packages/web/dist/assets/ProvidersView-nY9GnDdO.js +0 -1
  127. package/packages/web/dist/assets/QuickResponseSettings-B352c75l.css +0 -1
  128. package/packages/web/dist/assets/QuickResponseSettings-BQwQXuL7.js +0 -1
  129. package/packages/web/dist/assets/QuickResponsesPanel-BlFDvnZ2.css +0 -1
  130. package/packages/web/dist/assets/QuickResponsesPanel-BzSYcCSP.js +0 -1
  131. package/packages/web/dist/assets/ResizableTextarea-B3YIdIXv.js +0 -1
  132. package/packages/web/dist/assets/ResizableTextarea-DsU3TVwF.css +0 -1
  133. package/packages/web/dist/assets/SessionCard-CjE1tXiT.js +0 -1
  134. package/packages/web/dist/assets/SessionDetailView-3cPZrbS3.js +0 -36
  135. package/packages/web/dist/assets/SessionDetailView-CZRZMrfM.css +0 -1
  136. package/packages/web/dist/assets/SessionListView-CLXBfLcq.js +0 -1
  137. package/packages/web/dist/assets/SlashCommandWizard-Cy04d7-o.js +0 -1
  138. package/packages/web/dist/assets/TemplateDetailView-DH6Oswsp.js +0 -1
  139. package/packages/web/dist/assets/TemplateDetailView-DT2m06W7.css +0 -1
  140. package/packages/web/dist/assets/index-1zziPL6l.js +0 -1
  141. package/packages/web/dist/assets/index-7kzHPxSF.js +0 -1
  142. package/packages/web/dist/assets/index-B0N_obMc.js +0 -1
  143. package/packages/web/dist/assets/index-BNk_gdfI.js +0 -1
  144. package/packages/web/dist/assets/index-CSqaAH-0.js +0 -1
  145. package/packages/web/dist/assets/index-C_q4WlK8.js +0 -1
  146. package/packages/web/dist/assets/index-D1wpU4y0.js +0 -7
  147. package/packages/web/dist/assets/index-D5zCA8sD.js +0 -1
  148. package/packages/web/dist/assets/index-DGR8ELWY.js +0 -1
  149. package/packages/web/dist/assets/index-DHga8pXo.js +0 -1
  150. package/packages/web/dist/assets/index-DSby02Wl.js +0 -1
  151. package/packages/web/dist/assets/index-DqjXJTVI.js +0 -1
  152. package/packages/web/dist/assets/index-_4S2uLDI.js +0 -1
  153. package/packages/web/dist/assets/index-gmiZeFXN.js +0 -1
  154. package/packages/web/dist/assets/index-irD539ZM.js +0 -3
  155. package/packages/web/dist/assets/index-yq-E1Y00.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();
@@ -195,6 +197,30 @@ function broadcastSessionUpdate(sessionId, projectId, updated, updateData) {
195
197
  });
196
198
  }
197
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
+
198
224
  // PATCH /api/sessions/:id - Update session settings
199
225
  router.patch('/:id', requireSession, (req, res) => {
200
226
  const { updateData, error } = buildUpdateData(req.body);
@@ -209,6 +235,15 @@ router.patch('/:id', requireSession, (req, res) => {
209
235
 
210
236
  const updated = sessions.update(req.params.id, updateData);
211
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
+
212
247
  // Propagate PR URL to parent session if set (not when clearing)
213
248
  if (updateData.prUrl) {
214
249
  summaryService.propagatePrUrlToParent(req.params.id, updateData.prUrl);
@@ -218,6 +253,11 @@ router.patch('/:id', requireSession, (req, res) => {
218
253
  setSessionNameFromPr(req.params.id, updateData.prUrl).catch(err => {
219
254
  console.error(`[Sessions API] Failed to set session name from PR:`, err);
220
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
+ });
221
261
  }
222
262
 
223
263
  broadcastSessionUpdate(req.params.id, req.session_.projectId, updated, updateData);
@@ -5,10 +5,11 @@ import { normalizeCommitAttributionOverride } from '../../../shared/src/contract
5
5
 
6
6
  /**
7
7
  * Valid values for `providers.kind`. Maps 1:1 to an agent adapter:
8
- * - 'anthropic' 'claude-code'
8
+ * - 'anthropic' ��� 'claude-code'
9
9
  * - 'openai' → 'codex'
10
+ * - 'google' → 'gemini'
10
11
  */
11
- export const PROVIDER_KINDS = Object.freeze(['anthropic', 'openai']);
12
+ export const PROVIDER_KINDS = Object.freeze(['anthropic', 'openai', 'google']);
12
13
 
13
14
  /**
14
15
  * Mapping from provider kind to the agent adapter that should drive sessions
@@ -17,6 +18,7 @@ export const PROVIDER_KINDS = Object.freeze(['anthropic', 'openai']);
17
18
  export const AGENT_TYPE_BY_KIND = Object.freeze({
18
19
  anthropic: 'claude-code',
19
20
  openai: 'codex',
21
+ google: 'gemini',
20
22
  });
21
23
 
22
24
  const BUILT_IN_MUTABLE_FIELDS = Object.freeze(['commitAttributionOverride']);
@@ -74,6 +74,7 @@ export class SessionRepository extends BaseRepository {
74
74
  createdAt: row.created_at,
75
75
  updatedAt: row.updated_at,
76
76
  lastActivityAt: row.last_activity_at ?? null,
77
+ lastMessageAt: row.last_message_at ?? null,
77
78
  activeTimeMs: row.active_time_ms || 0,
78
79
  };
79
80
  }
@@ -147,7 +148,6 @@ export class SessionRepository extends BaseRepository {
147
148
  }
148
149
 
149
150
  sql += ` ORDER BY
150
- starred DESC,
151
151
  COALESCE(last_activity_at, updated_at, created_at) DESC,
152
152
  updated_at DESC,
153
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
  /**
@@ -0,0 +1,109 @@
1
+ /**
2
+ * Migrations for the canvas_items table.
3
+ * Each export is an array of { name, up(db) } migration objects.
4
+ */
5
+ import { addColumnIfMissing, getColumns, getTableSql } from './migrationUtils.js';
6
+
7
+ /**
8
+ * Migrate canvas_items table to include 'code' in type CHECK constraint.
9
+ * SQLite doesn't support ALTER TABLE to modify constraints, so we recreate the table.
10
+ */
11
+ function migrateCanvasItemsTypeConstraint(db) {
12
+ const tableSql = getTableSql(db, 'canvas_items');
13
+
14
+ // If schema already includes 'code', no migration needed
15
+ if (tableSql?.includes("'code'")) {
16
+ return;
17
+ }
18
+
19
+ db.exec(`
20
+ CREATE TABLE canvas_items_new (
21
+ id TEXT PRIMARY KEY,
22
+ session_id TEXT REFERENCES sessions(id) ON DELETE CASCADE,
23
+ type TEXT NOT NULL CHECK (type IN ('image', 'markdown', 'text', 'json', 'pdf', 'code')),
24
+ content TEXT,
25
+ data TEXT,
26
+ mime_type TEXT,
27
+ filename TEXT,
28
+ label TEXT,
29
+ width INTEGER,
30
+ height INTEGER,
31
+ created_at INTEGER NOT NULL DEFAULT (unixepoch() * 1000)
32
+ );
33
+
34
+ INSERT INTO canvas_items_new SELECT * FROM canvas_items;
35
+
36
+ DROP TABLE canvas_items;
37
+
38
+ ALTER TABLE canvas_items_new RENAME TO canvas_items;
39
+
40
+ CREATE INDEX IF NOT EXISTS idx_canvas_session ON canvas_items(session_id);
41
+ `);
42
+ }
43
+
44
+ /**
45
+ * Migrate canvas_items table to drop label column.
46
+ */
47
+ function migrateCanvasItemsDropLabel(db) {
48
+ const columns = getColumns(db, 'canvas_items');
49
+
50
+ // If label column doesn't exist, migration already done
51
+ if (!columns.includes('label')) {
52
+ return;
53
+ }
54
+
55
+ db.exec(`
56
+ CREATE TABLE canvas_items_new (
57
+ id TEXT PRIMARY KEY,
58
+ session_id TEXT REFERENCES sessions(id) ON DELETE CASCADE,
59
+ type TEXT NOT NULL CHECK (type IN ('image', 'markdown', 'text', 'json', 'pdf', 'code')),
60
+ content TEXT,
61
+ data TEXT,
62
+ mime_type TEXT,
63
+ filename TEXT,
64
+ width INTEGER,
65
+ height INTEGER,
66
+ deleted_at INTEGER,
67
+ created_at INTEGER NOT NULL DEFAULT (unixepoch() * 1000)
68
+ );
69
+
70
+ INSERT INTO canvas_items_new (id, session_id, type, content, data, mime_type, filename, width, height, deleted_at, created_at)
71
+ SELECT id, session_id, type, content, data, mime_type, filename, width, height, deleted_at, created_at FROM canvas_items;
72
+
73
+ DROP TABLE canvas_items;
74
+
75
+ ALTER TABLE canvas_items_new RENAME TO canvas_items;
76
+
77
+ CREATE INDEX IF NOT EXISTS idx_canvas_session ON canvas_items(session_id);
78
+ CREATE INDEX IF NOT EXISTS idx_canvas_deleted ON canvas_items(deleted_at);
79
+ `);
80
+ }
81
+
82
+ /** @type {Array<{name: string, up: (db: import('better-sqlite3').Database) => void}>} */
83
+ export const canvasItemsMigrations = [
84
+ {
85
+ name: 'canvas_items-migrate-type-constraint',
86
+ up(db) { migrateCanvasItemsTypeConstraint(db); },
87
+ },
88
+ {
89
+ name: 'canvas_items-add-deleted_at',
90
+ up(db) {
91
+ addColumnIfMissing(db, 'canvas_items', 'deleted_at', 'INTEGER');
92
+ db.exec('CREATE INDEX IF NOT EXISTS idx_canvas_deleted ON canvas_items(deleted_at)');
93
+ },
94
+ },
95
+ {
96
+ name: 'canvas_items-drop-label',
97
+ up(db) { migrateCanvasItemsDropLabel(db); },
98
+ },
99
+ {
100
+ name: 'canvas_items-add-updated_at',
101
+ up(db) {
102
+ const columns = getColumns(db, 'canvas_items');
103
+ if (!columns.includes('updated_at')) {
104
+ db.exec('ALTER TABLE canvas_items ADD COLUMN updated_at INTEGER');
105
+ db.exec('UPDATE canvas_items SET updated_at = created_at WHERE updated_at IS NULL');
106
+ }
107
+ },
108
+ },
109
+ ];
@@ -0,0 +1,187 @@
1
+ /**
2
+ * Migrations for the conversations, conversation_messages, and message_attachments tables.
3
+ * Each export is an array of { name, up(db) } migration objects.
4
+ */
5
+ import crypto from 'crypto';
6
+ import { addColumnIfMissing, getColumns } from './migrationUtils.js';
7
+
8
+ // Table name constants for migrations
9
+ const TABLE_CONVERSATIONS = 'conversations';
10
+
11
+ // Column type constants
12
+ const COL_INTEGER_DEFAULT_0 = 'INTEGER DEFAULT 0';
13
+
14
+ /**
15
+ * Create default conversations for existing sessions that don't have any
16
+ * and associate orphaned messages with the default conversation.
17
+ */
18
+ function migrateExistingSessionsToConversations(db) {
19
+ const sessionsWithoutConversations = db
20
+ .prepare(
21
+ `
22
+ SELECT DISTINCT s.id FROM sessions s
23
+ LEFT JOIN conversations c ON c.session_id = s.id
24
+ WHERE c.id IS NULL
25
+ AND EXISTS (SELECT 1 FROM conversation_messages m WHERE m.session_id = s.id)
26
+ `
27
+ )
28
+ .all();
29
+
30
+ for (const session of sessionsWithoutConversations) {
31
+ const convId = crypto.randomUUID();
32
+ const now = Date.now();
33
+
34
+ db.prepare(
35
+ `INSERT INTO conversations (id, session_id, name, is_active, created_at, updated_at)
36
+ VALUES (?, ?, ?, 1, ?, ?)`
37
+ ).run(convId, session.id, 'Initial', now, now);
38
+
39
+ db.prepare(
40
+ `UPDATE conversation_messages SET conversation_id = ? WHERE session_id = ? AND conversation_id IS NULL`
41
+ ).run(convId, session.id);
42
+ }
43
+ }
44
+
45
+ /** @type {Array<{name: string, up: (db: import('better-sqlite3').Database) => void}>} */
46
+ export const conversationsMigrations = [
47
+ // --- Session summaries PR columns ---
48
+ {
49
+ name: 'session_summaries-add-pr_merged',
50
+ up(db) { addColumnIfMissing(db, 'session_summaries', 'pr_merged', 'INTEGER'); },
51
+ },
52
+ {
53
+ name: 'session_summaries-add-pr_state',
54
+ up(db) { addColumnIfMissing(db, 'session_summaries', 'pr_state', 'TEXT'); },
55
+ },
56
+ {
57
+ name: 'session_summaries-add-has_merge_conflicts',
58
+ up(db) { addColumnIfMissing(db, 'session_summaries', 'has_merge_conflicts', 'INTEGER'); },
59
+ },
60
+ {
61
+ name: 'session_summaries-add-ci_status',
62
+ up(db) { addColumnIfMissing(db, 'session_summaries', 'ci_status', 'TEXT'); },
63
+ },
64
+ {
65
+ name: 'session_summaries-add-ci_failures',
66
+ up(db) { addColumnIfMissing(db, 'session_summaries', 'ci_failures', 'TEXT'); },
67
+ },
68
+ {
69
+ name: 'session_summaries-add-last_summarized_message_id',
70
+ up(db) { addColumnIfMissing(db, 'session_summaries', 'last_summarized_message_id', 'TEXT'); },
71
+ },
72
+
73
+ // --- Message attachments ---
74
+ {
75
+ name: 'message_attachments-add-file_path',
76
+ up(db) { addColumnIfMissing(db, 'message_attachments', 'file_path', 'TEXT'); },
77
+ },
78
+
79
+ // --- Conversation messages: conversation_id ---
80
+ {
81
+ name: 'conversation_messages-add-conversation_id',
82
+ up(db) {
83
+ const columns = getColumns(db, 'conversation_messages');
84
+ if (!columns.includes('conversation_id')) {
85
+ db.exec(
86
+ 'ALTER TABLE conversation_messages ADD COLUMN conversation_id TEXT REFERENCES conversations(id) ON DELETE CASCADE'
87
+ );
88
+ db.exec(
89
+ 'CREATE INDEX IF NOT EXISTS idx_messages_conversation ON conversation_messages(conversation_id)'
90
+ );
91
+ }
92
+ },
93
+ },
94
+
95
+ // --- Migrate existing sessions to conversations ---
96
+ {
97
+ name: 'conversations-migrate-existing-sessions',
98
+ up(db) { migrateExistingSessionsToConversations(db); },
99
+ },
100
+
101
+ // --- Conversations: claude_session_id ---
102
+ {
103
+ name: 'conversations-add-claude_session_id',
104
+ up(db) { addColumnIfMissing(db, TABLE_CONVERSATIONS, 'claude_session_id', 'TEXT'); },
105
+ },
106
+
107
+ // --- Conversations token usage ---
108
+ {
109
+ name: 'conversations-add-input_tokens',
110
+ up(db) { addColumnIfMissing(db, TABLE_CONVERSATIONS, 'input_tokens', COL_INTEGER_DEFAULT_0); },
111
+ },
112
+ {
113
+ name: 'conversations-add-output_tokens',
114
+ up(db) { addColumnIfMissing(db, TABLE_CONVERSATIONS, 'output_tokens', COL_INTEGER_DEFAULT_0); },
115
+ },
116
+ {
117
+ name: 'conversations-add-thinking_tokens',
118
+ up(db) { addColumnIfMissing(db, TABLE_CONVERSATIONS, 'thinking_tokens', COL_INTEGER_DEFAULT_0); },
119
+ },
120
+ {
121
+ name: 'conversations-add-cache_read_input_tokens',
122
+ up(db) { addColumnIfMissing(db, TABLE_CONVERSATIONS, 'cache_read_input_tokens', COL_INTEGER_DEFAULT_0); },
123
+ },
124
+ {
125
+ name: 'conversations-add-cache_creation_input_tokens',
126
+ up(db) { addColumnIfMissing(db, TABLE_CONVERSATIONS, 'cache_creation_input_tokens', COL_INTEGER_DEFAULT_0); },
127
+ },
128
+ {
129
+ name: 'conversations-add-web_search_requests',
130
+ up(db) { addColumnIfMissing(db, TABLE_CONVERSATIONS, 'web_search_requests', COL_INTEGER_DEFAULT_0); },
131
+ },
132
+ {
133
+ name: 'conversations-add-context_window',
134
+ up(db) { addColumnIfMissing(db, TABLE_CONVERSATIONS, 'context_window', 'INTEGER DEFAULT 200000'); },
135
+ },
136
+ {
137
+ name: 'conversations-add-model',
138
+ up(db) { addColumnIfMissing(db, TABLE_CONVERSATIONS, 'model', 'TEXT'); },
139
+ },
140
+
141
+ // --- Conversation branching ---
142
+ {
143
+ name: 'conversations-add-parent_conversation_id',
144
+ up(db) {
145
+ const columns = getColumns(db, TABLE_CONVERSATIONS);
146
+ if (!columns.includes('parent_conversation_id')) {
147
+ db.exec(
148
+ 'ALTER TABLE conversations ADD COLUMN parent_conversation_id TEXT REFERENCES conversations(id) ON DELETE SET NULL'
149
+ );
150
+ db.exec(
151
+ 'CREATE INDEX IF NOT EXISTS idx_conversations_parent ON conversations(parent_conversation_id)'
152
+ );
153
+ }
154
+ },
155
+ },
156
+ {
157
+ name: 'conversations-add-branch_from_message_id',
158
+ up(db) {
159
+ addColumnIfMissing(
160
+ db, TABLE_CONVERSATIONS, 'branch_from_message_id',
161
+ 'TEXT REFERENCES conversation_messages(id) ON DELETE SET NULL'
162
+ );
163
+ },
164
+ },
165
+
166
+ // --- Session todos: conversation_id ---
167
+ {
168
+ name: 'session_todos-add-conversation_id',
169
+ up(db) {
170
+ const columns = getColumns(db, 'session_todos');
171
+ if (!columns.includes('conversation_id')) {
172
+ db.exec(
173
+ 'ALTER TABLE session_todos ADD COLUMN conversation_id TEXT REFERENCES conversations(id) ON DELETE CASCADE'
174
+ );
175
+ db.exec(
176
+ 'CREATE INDEX IF NOT EXISTS idx_todos_conversation ON session_todos(conversation_id)'
177
+ );
178
+ }
179
+ },
180
+ },
181
+
182
+ // --- Conversation messages: model ---
183
+ {
184
+ name: 'conversation_messages-add-model',
185
+ up(db) { addColumnIfMissing(db, 'conversation_messages', 'model', 'TEXT'); },
186
+ },
187
+ ];