@loicngr/kobo 1.11.8 → 1.11.10

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 (180) hide show
  1. package/AGENTS.md +2 -1
  2. package/CHANGELOG.md +9 -0
  3. package/README.md +2 -2
  4. package/dist/server/db/migrations.js +155 -19
  5. package/dist/server/db/schema.js +93 -0
  6. package/dist/server/index.js +35 -26
  7. package/dist/server/routes/dev-server.js +10 -8
  8. package/dist/server/routes/documents.js +33 -3
  9. package/dist/server/routes/health.js +16 -15
  10. package/dist/server/routes/images.js +17 -2
  11. package/dist/server/routes/workspaces.js +64 -49
  12. package/dist/server/services/agent/engines/codex/engine.js +5 -1
  13. package/dist/server/services/agent/orchestrator.js +8 -1
  14. package/dist/server/services/auto-loop-service.js +1 -1
  15. package/dist/server/services/change-source-branch-service.js +43 -3
  16. package/dist/server/services/dev-server-service.js +55 -41
  17. package/dist/server/services/forge/bitbucket/provider.js +6 -11
  18. package/dist/server/services/forge/github/provider.js +10 -9
  19. package/dist/server/services/forge/gitlab/provider.js +11 -13
  20. package/dist/server/services/git-stats-service.js +8 -6
  21. package/dist/server/services/image-service.js +34 -5
  22. package/dist/server/services/notion-service.js +45 -5
  23. package/dist/server/services/pr-watcher-service.js +20 -9
  24. package/dist/server/services/search-service.js +53 -40
  25. package/dist/server/services/websocket-service.js +8 -3
  26. package/dist/server/services/workspace-service.js +81 -10
  27. package/dist/server/services/worktree-purge-service.js +1 -1
  28. package/dist/server/utils/git-ops.js +89 -22
  29. package/dist/server/utils/mcp-client.js +4 -0
  30. package/dist/server/utils/safe-path.js +14 -0
  31. package/dist/server/utils/script-runner.js +16 -1
  32. package/package.json +22 -22
  33. package/src/client/dist/pwa/assets/{ActivityFeed-D9EfbRqv.css → ActivityFeed-BrjeLlhi.css} +1 -1
  34. package/src/client/dist/pwa/assets/ActivityFeed-UEAexGjc.js +8 -0
  35. package/src/client/dist/pwa/assets/ChangelogPage-XSYwu9XA.js +1 -0
  36. package/src/client/dist/pwa/assets/{ClosePopup-_rbZbpfb.js → ClosePopup-dpcrW9DC.js} +1 -1
  37. package/src/client/dist/pwa/assets/CreatePage-BK__rjvR.js +2 -0
  38. package/src/client/dist/pwa/assets/CreatePage-QwtfYunr.css +1 -0
  39. package/src/client/dist/pwa/assets/{DiffViewer-CRrDdYBa.css → DiffViewer-CS3P9Ngi.css} +1 -1
  40. package/src/client/dist/pwa/assets/DiffViewer-DnFN6Sc_.js +8 -0
  41. package/src/client/dist/pwa/assets/HealthPage-DVrZfMI6.js +1 -0
  42. package/src/client/dist/pwa/assets/MainLayout-D_3nFaCG.js +19 -0
  43. package/src/client/dist/pwa/assets/MainLayout-u-AM87m3.css +1 -0
  44. package/src/client/dist/pwa/assets/QBadge-sMJZ_eZm.js +1 -0
  45. package/src/client/dist/pwa/assets/QBanner-DedhGMcj.js +1 -0
  46. package/src/client/dist/pwa/assets/QBtnToggle-BgfN19l8.js +1 -0
  47. package/src/client/dist/pwa/assets/QChip-EQGYShN0.js +1 -0
  48. package/src/client/dist/pwa/assets/QIcon--SvgE-nS.js +1 -0
  49. package/src/client/dist/pwa/assets/QList-Be_FbTA3.js +1 -0
  50. package/src/client/dist/pwa/assets/QPage-DuIsRmYu.js +1 -0
  51. package/src/client/dist/pwa/assets/QSelect-CtxxliRU.js +36 -0
  52. package/src/client/dist/pwa/assets/QSeparator-BdyRE-AM.js +1 -0
  53. package/src/client/dist/pwa/assets/QSpace-D9UEvM99.js +1 -0
  54. package/src/client/dist/pwa/assets/QSpinnerDots-B4RmFJUj.js +1 -0
  55. package/src/client/dist/pwa/assets/QTooltip-HEIFh5w9.js +1 -0
  56. package/src/client/dist/pwa/assets/SearchPage-1B5HOf90.js +1 -0
  57. package/src/client/dist/pwa/assets/SettingsPage-BXiWr2ba.css +1 -0
  58. package/src/client/dist/pwa/assets/SettingsPage-v8f4rrCt.js +16 -0
  59. package/src/client/dist/pwa/assets/TouchPan-lynWXY69.js +1 -0
  60. package/src/client/dist/pwa/assets/WorkspacePage-ChnyJ3xI.css +1 -0
  61. package/src/client/dist/pwa/assets/WorkspacePage-Vjzi8YnW.js +4 -0
  62. package/src/client/dist/pwa/assets/codicon-Brq4_Ui5.ttf +0 -0
  63. package/src/client/dist/pwa/assets/{css.worker-CBlhdqTa.js → css.worker-JKZQyld4.js} +38 -34
  64. package/src/client/dist/pwa/assets/cssMode-cg8iVI-i.js +1 -0
  65. package/src/client/dist/pwa/assets/documents-Dlx8yiSO.js +1 -0
  66. package/src/client/dist/pwa/assets/editor-vZnmUXVO.css +1 -0
  67. package/src/client/dist/pwa/assets/editor.api-CfaJRkrX.js +847 -0
  68. package/src/client/dist/pwa/assets/editor.worker-D-m8IBMa.js +30 -0
  69. package/src/client/dist/pwa/assets/expand-template-D3LZldj1.js +1 -0
  70. package/src/client/dist/pwa/assets/formatters-D0dU6VcA.js +1 -0
  71. package/src/client/dist/pwa/assets/{freemarker2-DczqHd5F.js → freemarker2-B_8OIClK.js} +1 -1
  72. package/src/client/dist/pwa/assets/handlebars-CaYi_uxU.js +1 -0
  73. package/src/client/dist/pwa/assets/html-BnCUDUl1.js +1 -0
  74. package/src/client/dist/pwa/assets/html.worker-BUr90sI4.js +506 -0
  75. package/src/client/dist/pwa/assets/htmlMode-BZjKGnQF.js +1 -0
  76. package/src/client/dist/pwa/assets/i18n-B6SDs7Lm.js +1 -0
  77. package/src/client/dist/pwa/assets/index-CK1nk7qr.css +1 -0
  78. package/src/client/dist/pwa/assets/index-CoOsPyyG.js +82 -0
  79. package/src/client/dist/pwa/assets/{javascript-Bs83z_W4.js → javascript-DkSfoeHU.js} +1 -1
  80. package/src/client/dist/pwa/assets/json.worker-BOODjG9U.js +62 -0
  81. package/src/client/dist/pwa/assets/{jsonMode-Bc-8bzJi.js → jsonMode-yKXxfx1V.js} +3 -3
  82. package/src/client/dist/pwa/assets/{kobo-commands-B2kM0INN.js → kobo-commands-vn1IQxLn.js} +1 -1
  83. package/src/client/dist/pwa/assets/layout-CiQpugQB.js +1 -0
  84. package/src/client/dist/pwa/assets/liquid-Dlhysv3U.js +1 -0
  85. package/src/client/dist/pwa/assets/lspLanguageFeatures-CeYSQFUz.js +4 -0
  86. package/src/client/dist/pwa/assets/mdx-B8iME5NA.js +1 -0
  87. package/src/client/dist/pwa/assets/network-auth-j2Drut6q.js +1 -0
  88. package/src/client/dist/pwa/assets/pending-diff-open-B4RYz7G_.js +1 -0
  89. package/src/client/dist/pwa/assets/permissionModes-6jhegAm-.js +1 -0
  90. package/src/client/dist/pwa/assets/{project-color-DIFZ95_g.js → project-color-e3OdFi9X.js} +1 -1
  91. package/src/client/dist/pwa/assets/python-CbFbC0ZR.js +1 -0
  92. package/src/client/dist/pwa/assets/razor-7c2NCdFI.js +1 -0
  93. package/src/client/dist/pwa/assets/reactivity.esm-bundler-DtSZKRQB.js +1 -0
  94. package/src/client/dist/pwa/assets/render-chat-markdown-Fer8kY38.js +67 -0
  95. package/src/client/dist/pwa/assets/runtime-dom.esm-bundler-DgevF6KV.js +4 -0
  96. package/src/client/dist/pwa/assets/{ts.worker-PmaSgaZk.js → ts.worker-C-oGa8JT.js} +199 -195
  97. package/src/client/dist/pwa/assets/{tsMode-DOsxzgZi.js → tsMode-BmShKjbx.js} +1 -1
  98. package/src/client/dist/pwa/assets/typescript-Dhu7Upbs.js +1 -0
  99. package/src/client/dist/pwa/assets/use-checkbox-SrkgiJho.js +1 -0
  100. package/src/client/dist/pwa/assets/use-onboarding-CTZuBZOi.css +1 -0
  101. package/src/client/dist/pwa/assets/use-onboarding-fe4sPCNf.js +2 -0
  102. package/src/client/dist/pwa/assets/use-quasar-CeHlTrGg.js +1 -0
  103. package/src/client/dist/pwa/assets/vs-B-xsceNQ.js +56 -0
  104. package/src/client/dist/pwa/assets/vs-JJSFJJVJ.css +1 -0
  105. package/src/client/dist/pwa/assets/vue-i18n-C3W5i7h6.js +3 -0
  106. package/src/client/dist/pwa/assets/workers-zVMTXVoC.js +1 -0
  107. package/src/client/dist/pwa/assets/xml-D64wpCtR.js +1 -0
  108. package/src/client/dist/pwa/assets/yaml-CLSRKwfx.js +1 -0
  109. package/src/client/dist/pwa/index.html +8 -8
  110. package/src/client/dist/pwa/manifest.json +1 -1
  111. package/src/client/dist/pwa/sw.js +1 -1
  112. package/src/client/dist/pwa/assets/ActivityFeed-BkkyAw81.js +0 -8
  113. package/src/client/dist/pwa/assets/ChangelogPage-BlvhBx6o.js +0 -1
  114. package/src/client/dist/pwa/assets/CreatePage-B_JcmRfU.css +0 -1
  115. package/src/client/dist/pwa/assets/CreatePage-RSyoQwD7.js +0 -2
  116. package/src/client/dist/pwa/assets/DiffViewer-BSNxNex2.js +0 -8
  117. package/src/client/dist/pwa/assets/HealthPage-CUD1kipV.js +0 -1
  118. package/src/client/dist/pwa/assets/MainLayout-BCLgsn0C.js +0 -37
  119. package/src/client/dist/pwa/assets/MainLayout-DlS2wxkK.css +0 -1
  120. package/src/client/dist/pwa/assets/QBadge-DV5kn4mL.js +0 -1
  121. package/src/client/dist/pwa/assets/QBanner-CmfaeizG.js +0 -1
  122. package/src/client/dist/pwa/assets/QBtnGroup-C442-yBb.js +0 -1
  123. package/src/client/dist/pwa/assets/QBtnToggle-xg3LYW7D.js +0 -1
  124. package/src/client/dist/pwa/assets/QChip-DhUfnmbz.js +0 -1
  125. package/src/client/dist/pwa/assets/QIcon-C6E-oEjI.js +0 -1
  126. package/src/client/dist/pwa/assets/QInput-BF8x4RTv.js +0 -1
  127. package/src/client/dist/pwa/assets/QList-D5EKdFPn.js +0 -1
  128. package/src/client/dist/pwa/assets/QPage-D3rjwA1l.js +0 -1
  129. package/src/client/dist/pwa/assets/QSelect-BhyT0k2N.js +0 -36
  130. package/src/client/dist/pwa/assets/QSeparator-DWTCQxsa.js +0 -1
  131. package/src/client/dist/pwa/assets/QSpace-ezNrZkuj.js +0 -1
  132. package/src/client/dist/pwa/assets/QSpinnerDots-DIV68-cV.js +0 -1
  133. package/src/client/dist/pwa/assets/QTooltip-ybbFi3KK.js +0 -1
  134. package/src/client/dist/pwa/assets/SearchPage-Bb7TG3Vo.js +0 -1
  135. package/src/client/dist/pwa/assets/SettingsPage-C2qe4xKu.css +0 -1
  136. package/src/client/dist/pwa/assets/SettingsPage-CjPnnn60.js +0 -16
  137. package/src/client/dist/pwa/assets/ThinkingItem-B1RKyCIS.js +0 -1
  138. package/src/client/dist/pwa/assets/ThinkingItem-Ckk4K1O-.css +0 -1
  139. package/src/client/dist/pwa/assets/TouchPan-DzZPYIol.js +0 -1
  140. package/src/client/dist/pwa/assets/WorkspacePage-6Rgjarvf.js +0 -4
  141. package/src/client/dist/pwa/assets/WorkspacePage-Bgxg_Rua.css +0 -1
  142. package/src/client/dist/pwa/assets/build-path-tree-KRACVkD3.js +0 -1
  143. package/src/client/dist/pwa/assets/chunk-QTnfLwEv.js +0 -1
  144. package/src/client/dist/pwa/assets/codicon-ngg6Pgfi.ttf +0 -0
  145. package/src/client/dist/pwa/assets/cssMode-CQsB6mFO.js +0 -1
  146. package/src/client/dist/pwa/assets/documents-C7efBF5t.js +0 -1
  147. package/src/client/dist/pwa/assets/editor-BFebQ78e.css +0 -1
  148. package/src/client/dist/pwa/assets/editor.api2-C58pkxmw.js +0 -872
  149. package/src/client/dist/pwa/assets/editor.main-BhLqjdq4.js +0 -6
  150. package/src/client/dist/pwa/assets/editor.worker-Cu3tR8iJ.js +0 -26
  151. package/src/client/dist/pwa/assets/expand-template-BA-hhG5v.js +0 -1
  152. package/src/client/dist/pwa/assets/formatters-DugWNOEs.js +0 -1
  153. package/src/client/dist/pwa/assets/handlebars-DmA7twi8.js +0 -1
  154. package/src/client/dist/pwa/assets/html-DtZDrViU.js +0 -1
  155. package/src/client/dist/pwa/assets/html.worker-BKBrNna1.js +0 -502
  156. package/src/client/dist/pwa/assets/htmlMode-DO9_qUdT.js +0 -1
  157. package/src/client/dist/pwa/assets/i18n-CipNqHTm.js +0 -1
  158. package/src/client/dist/pwa/assets/index-BxspWSDs.js +0 -84
  159. package/src/client/dist/pwa/assets/index-CwYR3iHt.css +0 -1
  160. package/src/client/dist/pwa/assets/json.worker-BaLYt9Ss.js +0 -58
  161. package/src/client/dist/pwa/assets/layout-C6frIxKZ.js +0 -1
  162. package/src/client/dist/pwa/assets/liquid-Dnxe0NUB.js +0 -1
  163. package/src/client/dist/pwa/assets/lspLanguageFeatures-qDBRYovw.js +0 -4
  164. package/src/client/dist/pwa/assets/mdx-C1oke9bq.js +0 -1
  165. package/src/client/dist/pwa/assets/monaco.contribution-BbFzrpO3.js +0 -2
  166. package/src/client/dist/pwa/assets/network-auth-Bj1LS5R7.js +0 -1
  167. package/src/client/dist/pwa/assets/pending-diff-open-C3voSObg.js +0 -1
  168. package/src/client/dist/pwa/assets/permissionModes-DMuXGtZu.js +0 -1
  169. package/src/client/dist/pwa/assets/python-Dgq-bJSM.js +0 -1
  170. package/src/client/dist/pwa/assets/razor-Bms4CQGF.js +0 -1
  171. package/src/client/dist/pwa/assets/render-chat-markdown-BRSTe4je.js +0 -66
  172. package/src/client/dist/pwa/assets/typescript-CzOp_GqX.js +0 -1
  173. package/src/client/dist/pwa/assets/use-checkbox-E9SV7iH5.js +0 -1
  174. package/src/client/dist/pwa/assets/use-onboarding-B3l7mx48.css +0 -1
  175. package/src/client/dist/pwa/assets/use-onboarding-BGy3sOAN.js +0 -2
  176. package/src/client/dist/pwa/assets/use-quasar-DsmzxifG.js +0 -1
  177. package/src/client/dist/pwa/assets/vue.runtime.esm-bundler-JZnIeD9D.js +0 -3
  178. package/src/client/dist/pwa/assets/workers-0fozwhvS.js +0 -1
  179. package/src/client/dist/pwa/assets/xml-CPa01uHm.js +0 -1
  180. package/src/client/dist/pwa/assets/yaml-Cw3fzSuh.js +0 -1
@@ -1,6 +1,6 @@
1
1
  import { getDb } from '../db/index.js';
2
2
  /** Event types that carry user-authored or assistant-authored readable text. */
3
- const SEARCHABLE_TYPES = ['user:message', 'agent:output'];
3
+ const SEARCHABLE_TYPES = ['user:message', 'agent:event', 'agent:output'];
4
4
  const SNIPPET_CONTEXT = 100; // chars on each side of the match
5
5
  /**
6
6
  * Extract the readable text content of a ws_events payload, or `null` when
@@ -29,6 +29,9 @@ function extractReadableText(type, payload) {
29
29
  return parts.length > 0 ? parts.join('\n') : null;
30
30
  }
31
31
  }
32
+ if (type === 'agent:event' && p.kind === 'message:text') {
33
+ return typeof p.text === 'string' ? p.text : null;
34
+ }
32
35
  return null;
33
36
  }
34
37
  function buildSnippet(text, matchIndex, query) {
@@ -56,49 +59,59 @@ export function searchEvents(query, options = {}) {
56
59
  const db = getDb();
57
60
  const typePlaceholders = SEARCHABLE_TYPES.map(() => '?').join(', ');
58
61
  const archiveFilter = includeArchived ? '' : 'AND w.archived_at IS NULL';
59
- const likePattern = `%${trimmed}%`;
60
- // Over-fetch a bit so post-filter rejects don't shrink us below `limit` on
61
- // realistic datasets, without scanning the entire table.
62
- const dbLimit = Math.max(limit * 3, 100);
63
- const rows = db
64
- .prepare(`SELECT e.id, e.workspace_id, e.session_id, e.type, e.created_at, e.payload,
65
- w.name AS workspace_name, w.archived_at
66
- FROM ws_events e
67
- JOIN workspaces w ON e.workspace_id = w.id
68
- WHERE e.type IN (${typePlaceholders})
69
- AND e.payload LIKE ?
70
- ${archiveFilter}
71
- ORDER BY e.created_at DESC
72
- LIMIT ?`)
73
- .all(...SEARCHABLE_TYPES, likePattern, dbLimit);
62
+ const escapedQuery = trimmed.replace(/[\\%_]/g, '\\$&');
63
+ const likePattern = `%${escapedQuery}%`;
74
64
  const needle = trimmed.toLowerCase();
75
65
  const results = [];
76
- for (const row of rows) {
77
- let parsed;
78
- try {
79
- parsed = JSON.parse(row.payload);
80
- }
81
- catch {
82
- continue;
66
+ const batchSize = Math.min(Math.max(limit * 3, 100), 500);
67
+ const statement = db.prepare(`SELECT e.id, e.workspace_id, e.session_id, e.type, e.created_at, e.payload,
68
+ w.name AS workspace_name, w.archived_at
69
+ FROM ws_events e
70
+ JOIN workspaces w ON e.workspace_id = w.id
71
+ WHERE e.type IN (${typePlaceholders})
72
+ AND e.payload LIKE ? ESCAPE '\\'
73
+ ${archiveFilter}
74
+ AND (? IS NULL OR e.created_at < ? OR (e.created_at = ? AND e.id < ?))
75
+ ORDER BY e.created_at DESC, e.id DESC
76
+ LIMIT ?`);
77
+ let cursorCreatedAt = null;
78
+ let cursorId = null;
79
+ while (results.length < limit) {
80
+ const rows = statement.all(...SEARCHABLE_TYPES, likePattern, cursorCreatedAt, cursorCreatedAt, cursorCreatedAt, cursorId, batchSize);
81
+ for (const row of rows) {
82
+ let parsed;
83
+ try {
84
+ parsed = JSON.parse(row.payload);
85
+ }
86
+ catch {
87
+ continue;
88
+ }
89
+ const text = extractReadableText(row.type, parsed);
90
+ if (!text)
91
+ continue;
92
+ const idx = text.toLowerCase().indexOf(needle);
93
+ if (idx < 0)
94
+ continue;
95
+ results.push({
96
+ eventId: row.id,
97
+ sessionId: row.session_id,
98
+ workspaceId: row.workspace_id,
99
+ workspaceName: row.workspace_name,
100
+ archived: row.archived_at !== null,
101
+ type: row.type,
102
+ timestamp: row.created_at,
103
+ snippet: buildSnippet(text, idx, trimmed),
104
+ });
105
+ if (results.length >= limit)
106
+ break;
83
107
  }
84
- const text = extractReadableText(row.type, parsed);
85
- if (!text)
86
- continue;
87
- const idx = text.toLowerCase().indexOf(needle);
88
- if (idx < 0)
89
- continue;
90
- results.push({
91
- eventId: row.id,
92
- sessionId: row.session_id,
93
- workspaceId: row.workspace_id,
94
- workspaceName: row.workspace_name,
95
- archived: row.archived_at !== null,
96
- type: row.type,
97
- timestamp: row.created_at,
98
- snippet: buildSnippet(text, idx, trimmed),
99
- });
100
- if (results.length >= limit)
108
+ if (rows.length < batchSize)
109
+ break;
110
+ const last = rows.at(-1);
111
+ if (!last)
101
112
  break;
113
+ cursorCreatedAt = last.created_at;
114
+ cursorId = last.id;
102
115
  }
103
116
  return results;
104
117
  }
@@ -109,16 +109,18 @@ export function handleConnection(ws) {
109
109
  export function emit(workspaceId, type, payload, sessionId) {
110
110
  const id = nanoid();
111
111
  const createdAt = new Date().toISOString();
112
+ let replayable = false;
112
113
  // Best-effort persist — don't let FK violation (deleted workspace) break the broadcast
113
114
  try {
114
115
  const db = getDb();
115
116
  db.prepare('INSERT INTO ws_events (id, workspace_id, type, payload, session_id, created_at) VALUES (?, ?, ?, ?, ?, ?)').run(id, workspaceId, type, JSON.stringify(payload), sessionId ?? null, createdAt);
117
+ replayable = true;
116
118
  }
117
119
  catch (err) {
118
120
  console.error(`[websocket-service] Failed to persist event (workspace=${workspaceId}, type=${type}):`, err);
119
121
  }
120
122
  // Build the event object to send
121
- const event = { id, workspaceId, type, payload, sessionId, createdAt };
123
+ const event = { id, workspaceId, type, payload, sessionId, createdAt, replayable };
122
124
  const message = JSON.stringify(event);
123
125
  // Broadcast to subscribed clients. Wrap `.send` in try/catch so a dropped
124
126
  // client doesn't throw and abort delivery to the remaining subscribers.
@@ -145,7 +147,7 @@ export function emit(workspaceId, type, payload, sessionId) {
145
147
  export function emitEphemeral(workspaceId, type, payload) {
146
148
  const id = nanoid();
147
149
  const createdAt = new Date().toISOString();
148
- const event = { id, workspaceId, type, payload, createdAt };
150
+ const event = { id, workspaceId, type, payload, createdAt, replayable: false };
149
151
  const message = JSON.stringify(event);
150
152
  let sendErrorLogged = false;
151
153
  for (const [ws, subs] of clients) {
@@ -188,12 +190,14 @@ export function handleSyncRequest(ws, lastEventId, workspaceIds) {
188
190
  // up. This keeps first-paint fast on long-lived workspaces with tens of
189
191
  // thousands of events without ever deleting anything from the DB.
190
192
  const INITIAL_WINDOW = 300;
193
+ let mode = 'snapshot';
191
194
  if (lastEventId) {
192
195
  // Resume path: replay every event strictly after the cursor (delta
193
196
  // since last seen). If the cursor is stale/unknown, fall back to the
194
197
  // recent window rather than streaming the entire history.
195
198
  const lastRow = db.prepare('SELECT rowid FROM ws_events WHERE id = ?').get(lastEventId);
196
199
  if (lastRow) {
200
+ mode = 'delta';
197
201
  rows = db
198
202
  .prepare(`SELECT * FROM ws_events WHERE workspace_id IN (${placeholders}) AND rowid > ? ORDER BY rowid ASC`)
199
203
  .all(...resolvedIds, lastRow.rowid);
@@ -232,9 +236,10 @@ export function handleSyncRequest(ws, lastEventId, workspaceIds) {
232
236
  payload: parsedPayload,
233
237
  sessionId: row.session_id ?? undefined,
234
238
  createdAt: row.created_at,
239
+ replayable: true,
235
240
  };
236
241
  });
237
- ws.send(JSON.stringify({ type: 'sync:response', payload: { events } }));
242
+ ws.send(JSON.stringify({ type: 'sync:response', payload: { events, mode } }));
238
243
  }
239
244
  // ── Utilities ──────────────────────────────────────────────────────────────────
240
245
  /**
@@ -165,20 +165,29 @@ export function updateWorkspaceStatus(id, status) {
165
165
  return getWorkspace(id);
166
166
  }
167
167
  const WORKSPACE_NAME_MAX_LENGTH = 200;
168
- /** Update a workspace's display name. */
169
- export function updateWorkspaceName(id, name) {
170
- // Strip control characters (incl. newlines) and collapse whitespace
168
+ function sanitizeWorkspaceName(name) {
171
169
  const noControl = Array.from(name, (c) => {
172
170
  const code = c.charCodeAt(0);
173
171
  return code < 32 || code === 127 ? ' ' : c;
174
172
  }).join('');
175
173
  const sanitized = noControl.replace(/\s+/g, ' ').trim();
176
- if (!sanitized) {
174
+ if (!sanitized)
177
175
  throw new Error('Workspace name cannot be empty');
178
- }
179
176
  if (sanitized.length > WORKSPACE_NAME_MAX_LENGTH) {
180
177
  throw new Error(`Workspace name cannot exceed ${WORKSPACE_NAME_MAX_LENGTH} characters`);
181
178
  }
179
+ return sanitized;
180
+ }
181
+ function normalizeWorkspaceDescription(description) {
182
+ const trimmed = description == null ? null : description.trim();
183
+ if (trimmed !== null && trimmed.length > 200) {
184
+ throw new Error(`Description must be 200 characters or fewer (got ${trimmed.length})`);
185
+ }
186
+ return trimmed && trimmed.length > 0 ? trimmed : null;
187
+ }
188
+ /** Update a workspace's display name. */
189
+ export function updateWorkspaceName(id, name) {
190
+ const sanitized = sanitizeWorkspaceName(name);
182
191
  const db = getDb();
183
192
  const now = new Date().toISOString();
184
193
  const result = db.prepare('UPDATE workspaces SET name = ?, updated_at = ? WHERE id = ?').run(sanitized, now, id);
@@ -311,11 +320,7 @@ export function updateAgentPermissionMode(id, mode) {
311
320
  * does not exist.
312
321
  */
313
322
  export function updateWorkspaceDescription(id, description) {
314
- const trimmed = description == null ? null : description.trim();
315
- if (trimmed !== null && trimmed.length > 200) {
316
- throw new Error(`Description must be 200 characters or fewer (got ${trimmed.length})`);
317
- }
318
- const stored = trimmed && trimmed.length > 0 ? trimmed : null;
323
+ const stored = normalizeWorkspaceDescription(description);
319
324
  const db = getDb();
320
325
  const result = db
321
326
  .prepare('UPDATE workspaces SET description = ?, updated_at = ? WHERE id = ?')
@@ -326,6 +331,72 @@ export function updateWorkspaceDescription(id, description) {
326
331
  emitEphemeral(id, 'workspace:description-updated', { description: stored });
327
332
  return getWorkspace(id);
328
333
  }
334
+ /** Validate and persist a multi-field API update with one atomic SQL statement. */
335
+ export function updateWorkspaceFields(id, fields) {
336
+ const workspace = getWorkspace(id);
337
+ if (!workspace)
338
+ throw new Error(`Workspace '${id}' not found`);
339
+ const assignments = [];
340
+ const values = [];
341
+ if (fields.model !== undefined) {
342
+ if (typeof fields.model !== 'string' || !fields.model.trim())
343
+ throw new Error('model must be a non-empty string');
344
+ assignments.push('model = ?');
345
+ values.push(fields.model.trim());
346
+ }
347
+ if (fields.reasoningEffort !== undefined) {
348
+ if (typeof fields.reasoningEffort !== 'string' || !fields.reasoningEffort.trim()) {
349
+ throw new Error('reasoningEffort must be a non-empty string');
350
+ }
351
+ assignments.push('reasoning_effort = ?');
352
+ values.push(fields.reasoningEffort.trim());
353
+ }
354
+ if (fields.agentPermissionMode !== undefined) {
355
+ if (!['plan', 'bypass', 'strict', 'interactive'].includes(fields.agentPermissionMode)) {
356
+ throw new Error('Invalid agentPermissionMode');
357
+ }
358
+ const legacyMode = fields.agentPermissionMode === 'plan' ? 'plan' : 'auto-accept';
359
+ const legacyProfile = fields.agentPermissionMode === 'plan' ? 'bypass' : fields.agentPermissionMode;
360
+ assignments.push('agent_permission_mode = ?', 'permission_mode = ?', 'permission_profile = ?');
361
+ values.push(fields.agentPermissionMode, legacyMode, legacyProfile);
362
+ }
363
+ if (fields.status !== undefined) {
364
+ const allowed = VALID_TRANSITIONS[workspace.status];
365
+ if (!allowed || (fields.status !== workspace.status && !allowed.includes(fields.status))) {
366
+ throw new Error(`Invalid status transition from '${workspace.status}' to '${fields.status}'. Allowed: ${allowed?.join(', ') ?? ''}`);
367
+ }
368
+ assignments.push('status = ?');
369
+ values.push(fields.status);
370
+ }
371
+ if (fields.name !== undefined) {
372
+ if (typeof fields.name !== 'string')
373
+ throw new Error('name must be a string');
374
+ assignments.push('name = ?');
375
+ values.push(sanitizeWorkspaceName(fields.name));
376
+ }
377
+ const hasDescription = Object.hasOwn(fields, 'description');
378
+ let storedDescription;
379
+ if (hasDescription) {
380
+ if (fields.description !== null && typeof fields.description !== 'string') {
381
+ throw new Error('description must be a string or null');
382
+ }
383
+ storedDescription = normalizeWorkspaceDescription(fields.description ?? null);
384
+ assignments.push('description = ?');
385
+ values.push(storedDescription);
386
+ }
387
+ if (assignments.length === 0)
388
+ return workspace;
389
+ assignments.push('updated_at = ?');
390
+ values.push(new Date().toISOString(), id);
391
+ const result = getDb()
392
+ .prepare(`UPDATE workspaces SET ${assignments.join(', ')} WHERE id = ?`)
393
+ .run(...values);
394
+ if (result.changes === 0)
395
+ throw new Error(`Workspace '${id}' not found`);
396
+ if (hasDescription)
397
+ emitEphemeral(id, 'workspace:description-updated', { description: storedDescription ?? null });
398
+ return getWorkspace(id);
399
+ }
329
400
  /**
330
401
  * Update a workspace's agent-side description (≤ 200 chars after trim).
331
402
  * Empty string (after trim) or `null` clears the column.
@@ -24,7 +24,7 @@ export async function purgeWorktree(workspaceId) {
24
24
  console.error(`[purge] stopAgent failed for '${workspace.name}':`, msg);
25
25
  }
26
26
  try {
27
- devServerService.stopDevServer(workspaceId);
27
+ await devServerService.stopDevServer(workspaceId);
28
28
  }
29
29
  catch (err) {
30
30
  const msg = err instanceof Error ? err.message : String(err);
@@ -3,6 +3,7 @@ import { existsSync, readFileSync, rmSync, writeFileSync } from 'node:fs';
3
3
  import { join } from 'node:path';
4
4
  import { promisify } from 'node:util';
5
5
  const execFileAsync = promisify(execFileCb);
6
+ const READ_ONLY_GIT_TIMEOUT_MS = 15_000;
6
7
  function git(repoPath, args) {
7
8
  // `trimEnd` (not `trim`): some git outputs are column-aligned and the LEADING
8
9
  // space carries information. The classic case is `git status --porcelain`,
@@ -13,6 +14,15 @@ function git(repoPath, args) {
13
14
  // always appends) still goes — that's what every caller expects.
14
15
  return execFileSync('git', args, { cwd: repoPath, encoding: 'utf-8' }).trimEnd();
15
16
  }
17
+ async function gitAsync(repoPath, args, timeout = READ_ONLY_GIT_TIMEOUT_MS) {
18
+ const { stdout } = await execFileAsync('git', args, {
19
+ cwd: repoPath,
20
+ encoding: 'utf-8',
21
+ timeout,
22
+ maxBuffer: 10 * 1024 * 1024,
23
+ });
24
+ return stdout.trimEnd();
25
+ }
16
26
  /** Return the name of the currently checked-out branch. */
17
27
  export function getCurrentBranch(repoPath) {
18
28
  return git(repoPath, ['rev-parse', '--abbrev-ref', 'HEAD']);
@@ -899,31 +909,33 @@ export function writeFileInWorktree(absPath, content) {
899
909
  /** Parse `git status --porcelain` into counts of staged, modified, and untracked files. */
900
910
  export function getWorkingTreeStatus(repoPath) {
901
911
  try {
902
- const output = git(repoPath, ['status', '--porcelain']);
903
- let staged = 0;
904
- let modified = 0;
905
- let untracked = 0;
906
- for (const line of output.split('\n')) {
907
- if (!line)
908
- continue;
909
- const x = line[0]; // index status
910
- const y = line[1]; // worktree status
911
- if (x === '?' && y === '?') {
912
- untracked++;
913
- }
914
- else {
915
- if (x !== ' ' && x !== '?')
916
- staged++;
917
- if (y !== ' ' && y !== '?')
918
- modified++;
919
- }
920
- }
921
- return { staged, modified, untracked };
912
+ return parseWorkingTreeStatus(git(repoPath, ['status', '--porcelain']));
922
913
  }
923
914
  catch {
924
915
  return { staged: 0, modified: 0, untracked: 0 };
925
916
  }
926
917
  }
918
+ function parseWorkingTreeStatus(output) {
919
+ let staged = 0;
920
+ let modified = 0;
921
+ let untracked = 0;
922
+ for (const line of output.split('\n')) {
923
+ if (!line)
924
+ continue;
925
+ const x = line[0];
926
+ const y = line[1];
927
+ if (x === '?' && y === '?') {
928
+ untracked++;
929
+ }
930
+ else {
931
+ if (x !== ' ' && x !== '?')
932
+ staged++;
933
+ if (y !== ' ' && y !== '?')
934
+ modified++;
935
+ }
936
+ }
937
+ return { staged, modified, untracked };
938
+ }
927
939
  /**
928
940
  * List uncommitted working-tree files with their status, parsed from
929
941
  * `git status --porcelain`. Same classification rule as getWorkingTreeStatus.
@@ -1052,6 +1064,57 @@ export function getWorkingTreeDiffStats(repoPath) {
1052
1064
  }
1053
1065
  // ── Async versions ───────────────────────────────────────────────────────────
1054
1066
  // Non-blocking alternatives for hot paths (route handlers).
1067
+ async function resolveBaseAsync(repoPath, base) {
1068
+ try {
1069
+ await gitAsync(repoPath, ['rev-parse', '--verify', `origin/${base}`]);
1070
+ return `origin/${base}`;
1071
+ }
1072
+ catch {
1073
+ return base;
1074
+ }
1075
+ }
1076
+ /** Non-blocking commit count for request and polling hot paths. */
1077
+ export async function getCommitCountAsync(repoPath, base, head) {
1078
+ try {
1079
+ const ref = await resolveBaseAsync(repoPath, base);
1080
+ const output = await gitAsync(repoPath, ['rev-list', '--count', `${ref}..${head}`]);
1081
+ return parseInt(output, 10) || 0;
1082
+ }
1083
+ catch {
1084
+ return 0;
1085
+ }
1086
+ }
1087
+ /** Non-blocking behind count for request and polling hot paths. */
1088
+ export async function getCommitsBehindAsync(repoPath, base, head) {
1089
+ try {
1090
+ const ref = await resolveBaseAsync(repoPath, base);
1091
+ const output = await gitAsync(repoPath, ['rev-list', '--count', `${head}..${ref}`]);
1092
+ const count = parseInt(output, 10);
1093
+ return Number.isFinite(count) ? count : 0;
1094
+ }
1095
+ catch {
1096
+ return 0;
1097
+ }
1098
+ }
1099
+ /** Non-blocking diff summary for request and polling hot paths. */
1100
+ export async function getStructuredDiffStatsBetweenAsync(repoPath, base, head) {
1101
+ try {
1102
+ const ref = await resolveBaseAsync(repoPath, base);
1103
+ return parseDiffShortstat(await gitAsync(repoPath, ['diff', '--shortstat', `${ref}...${head}`]));
1104
+ }
1105
+ catch {
1106
+ return { filesChanged: 0, insertions: 0, deletions: 0 };
1107
+ }
1108
+ }
1109
+ /** Non-blocking working-tree summary for request and polling hot paths. */
1110
+ export async function getWorkingTreeStatusAsync(repoPath) {
1111
+ try {
1112
+ return parseWorkingTreeStatus(await gitAsync(repoPath, ['status', '--porcelain']));
1113
+ }
1114
+ catch {
1115
+ return { staged: 0, modified: 0, untracked: 0 };
1116
+ }
1117
+ }
1055
1118
  /**
1056
1119
  * Async version of `getUnpushedCount`. Same `origin/<workingBranch>` semantic:
1057
1120
  * returns `-1` when the remote ref does not exist (never pushed), `0` when
@@ -1060,7 +1123,10 @@ export function getWorkingTreeDiffStats(repoPath) {
1060
1123
  export async function getUnpushedCountAsync(repoPath, workingBranch) {
1061
1124
  const remoteRef = `origin/${workingBranch}`;
1062
1125
  try {
1063
- await execFileAsync('git', ['rev-parse', '--verify', remoteRef], { cwd: repoPath });
1126
+ await execFileAsync('git', ['rev-parse', '--verify', remoteRef], {
1127
+ cwd: repoPath,
1128
+ timeout: READ_ONLY_GIT_TIMEOUT_MS,
1129
+ });
1064
1130
  }
1065
1131
  catch {
1066
1132
  return -1; // branch never pushed (no remote ref)
@@ -1069,6 +1135,7 @@ export async function getUnpushedCountAsync(repoPath, workingBranch) {
1069
1135
  const { stdout } = await execFileAsync('git', ['rev-list', `${remoteRef}..HEAD`, '--count'], {
1070
1136
  cwd: repoPath,
1071
1137
  encoding: 'utf-8',
1138
+ timeout: READ_ONLY_GIT_TIMEOUT_MS,
1072
1139
  });
1073
1140
  return parseInt(stdout.trim(), 10) || 0;
1074
1141
  }
@@ -1086,7 +1153,7 @@ export async function getUnpushedCountAsync(repoPath, workingBranch) {
1086
1153
  */
1087
1154
  export async function fetchSourceBranchAsync(repoPath, branch, remote = 'origin') {
1088
1155
  try {
1089
- await execFileAsync('git', ['fetch', remote, branch], { cwd: repoPath });
1156
+ await execFileAsync('git', ['fetch', remote, branch], { cwd: repoPath, timeout: 30_000 });
1090
1157
  }
1091
1158
  catch (err) {
1092
1159
  const msg = err instanceof Error ? err.message : String(err);
@@ -58,6 +58,10 @@ export function spawnMcpProcess(command, args, env) {
58
58
  const mcpProcess = spawn(command, args, {
59
59
  stdio: ['pipe', 'pipe', 'pipe'],
60
60
  env,
61
+ // Detached: `command` is frequently `npx`, which spawns its own `node`
62
+ // child to run the actual MCP server. Without this, `mcpProcess.kill()`
63
+ // only reaches the `npx` wrapper and can leave the real server running.
64
+ detached: true,
61
65
  });
62
66
  // Defensive default 'error' listener: a ChildProcess that emits 'error'
63
67
  // without any listener crashes the whole Node process (Unhandled 'error').
@@ -0,0 +1,14 @@
1
+ import fs from 'node:fs';
2
+ import path from 'node:path';
3
+ export function isPathInside(rootPath, candidatePath) {
4
+ const relative = path.relative(rootPath, candidatePath);
5
+ return relative === '' || (!relative.startsWith(`..${path.sep}`) && relative !== '..' && !path.isAbsolute(relative));
6
+ }
7
+ /** Resolve two existing paths and reject candidates that escape through a symlinked parent. */
8
+ export function resolveExistingPathInside(rootPath, candidatePath) {
9
+ const realRoot = fs.realpathSync(rootPath);
10
+ const realCandidate = fs.realpathSync(candidatePath);
11
+ if (!isPathInside(realRoot, realCandidate))
12
+ throw new Error('Path escapes allowed root');
13
+ return realCandidate;
14
+ }
@@ -27,9 +27,24 @@ export function runScript(opts) {
27
27
  PROJECT_PATH: env?.projectPath ?? '',
28
28
  },
29
29
  stdio: ['ignore', 'pipe', 'pipe'],
30
+ // Detached so this is the leader of its own process group: a script
31
+ // that backgrounds a child (`npm run dev &`) would otherwise leave
32
+ // that child running after a timeout kill, since SIGKILL on `proc`
33
+ // alone only reaches the `bash` process itself.
34
+ detached: true,
30
35
  });
31
36
  const timeout = setTimeout(() => {
32
- proc.kill('SIGKILL');
37
+ // Kill the whole process group (-pid), not just `proc` — otherwise a
38
+ // backgrounded child of the script survives the timeout as an orphan.
39
+ try {
40
+ if (proc.pid)
41
+ process.kill(-proc.pid, 'SIGKILL');
42
+ else
43
+ proc.kill('SIGKILL');
44
+ }
45
+ catch {
46
+ proc.kill('SIGKILL');
47
+ }
33
48
  // Destroy pipes so the 'close' event fires immediately even if
34
49
  // child processes (e.g. sleep) inherited the file descriptors.
35
50
  proc.stdout?.destroy();
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@loicngr/kobo",
3
- "version": "1.11.8",
3
+ "version": "1.11.10",
4
4
  "description": "Kōbō — multi-workspace agent manager for Claude Code. Orchestrates isolated git worktrees with dev servers, Notion integration, and MCP tools.",
5
5
  "type": "module",
6
6
  "license": "GPL-3.0-or-later",
@@ -45,7 +45,7 @@
45
45
  "quasar"
46
46
  ],
47
47
  "engines": {
48
- "node": ">=20"
48
+ "node": ">=24.15.0"
49
49
  },
50
50
  "scripts": {
51
51
  "dev": "KOBO_ENFORCE_LOCAL_HOME=1 KOBO_HOME=./data PORT=3300 tsx watch src/server/index.ts",
@@ -54,7 +54,7 @@
54
54
  "build:client": "cd src/client && npx quasar build -m pwa",
55
55
  "build:server": "npx tsc && chmod +x dist/server/index.js",
56
56
  "build": "npm run build:client && npm run build:server",
57
- "install-all": "npm install && cd src/client && npm install",
57
+ "install-all": "npm install && cd src/client && npm install && cd src-pwa && npm install",
58
58
  "start": "node dist/server/index.js",
59
59
  "test": "vitest run --config vitest.config.ts --root .",
60
60
  "test:watch": "vitest --config vitest.config.ts --root .",
@@ -67,32 +67,32 @@
67
67
  "prepublishOnly": "npm run build"
68
68
  },
69
69
  "dependencies": {
70
- "@anthropic-ai/claude-agent-sdk": "^0.3.183",
71
- "@emnapi/core": "^1.11.1",
72
- "@emnapi/runtime": "^1.11.1",
73
- "@hono/node-server": "^2.0.11",
74
- "@modelcontextprotocol/sdk": "^1.29.0",
75
- "@openai/codex": "^0.145.0",
76
- "better-sqlite3": "^12.11.1",
77
- "cron-parser": "^5.5.0",
78
- "hono": "^4.12.26",
79
- "nanoid": "^5.1.14",
70
+ "@anthropic-ai/claude-agent-sdk": "^0.3.226",
71
+ "@emnapi/core": "^1.11.3",
72
+ "@emnapi/runtime": "^1.11.3",
73
+ "@hono/node-server": "^2.1.0",
74
+ "@modelcontextprotocol/sdk": "^1.30.0",
75
+ "@openai/codex": "^0.147.0",
76
+ "better-sqlite3": "^13.0.3",
77
+ "cron-parser": "^5.7.0",
78
+ "hono": "^4.13.1",
79
+ "nanoid": "^6.0.1",
80
80
  "node-pty": "^1.1.0",
81
- "ws": "^8.21.0"
81
+ "ws": "^8.21.3"
82
82
  },
83
83
  "devDependencies": {
84
- "@biomejs/biome": "2.4.10",
85
- "@types/better-sqlite3": "^7.6.13",
86
- "@types/node": "^26.0.0",
84
+ "@biomejs/biome": "2.5.7",
85
+ "@types/better-sqlite3": "^9.6.0",
86
+ "@types/node": "^26.2.0",
87
87
  "@types/ws": "^8.18.1",
88
- "@vitest/runner": "^4.1.9",
89
- "concurrently": "^10.0.3",
90
- "tsx": "^4.22.4",
88
+ "@vitest/runner": "^4.1.10",
89
+ "concurrently": "^10.0.4",
90
+ "tsx": "^4.23.11",
91
91
  "typescript": "^6.0.3",
92
- "vitest": "^4.1.9"
92
+ "vitest": "^4.1.10"
93
93
  },
94
94
  "overrides": {
95
- "@hono/node-server": "^2.0.11",
95
+ "@hono/node-server": "^2.1.0",
96
96
  "shell-quote": "^1.10.0"
97
97
  }
98
98
  }
@@ -1 +1 @@
1
- .markdown-message[data-v-1b7bd8ca]{color:#e0e0e0;word-break:break-word;overflow-wrap:anywhere;min-width:0;max-width:100%;font-size:13px;line-height:1.55}.markdown-message[data-v-1b7bd8ca] *{max-width:100%}.markdown-message[data-v-1b7bd8ca] p{margin:0 0 .5em}.markdown-message[data-v-1b7bd8ca] p:last-child{margin-bottom:0}.markdown-message[data-v-1b7bd8ca] pre{background:#00000059;border-radius:4px;margin:.5em 0;padding:.5em .75em;overflow-x:auto}.markdown-message[data-v-1b7bd8ca] code{word-break:break-all;background:#0000004d;border-radius:3px;padding:.1em .3em;font-size:.9em}.markdown-message[data-v-1b7bd8ca] pre code{background:0 0;padding:0}.markdown-message[data-v-1b7bd8ca] ul,.markdown-message[data-v-1b7bd8ca] ol{margin:.25em 0 .5em;padding-left:1.5em}.markdown-message[data-v-1b7bd8ca] li{margin:.15em 0}.markdown-message[data-v-1b7bd8ca] a{color:#7986cb;text-decoration:underline}.markdown-message[data-v-1b7bd8ca] .document-link{color:#9fa8da;cursor:pointer;-webkit-text-decoration:underline dotted;text-decoration:underline dotted}.markdown-message[data-v-1b7bd8ca] .document-link:hover{color:#c5cae9;-webkit-text-decoration:underline;text-decoration:underline}.markdown-message[data-v-1b7bd8ca] h1,.markdown-message[data-v-1b7bd8ca] h2,.markdown-message[data-v-1b7bd8ca] h3,.markdown-message[data-v-1b7bd8ca] h4,.markdown-message[data-v-1b7bd8ca] h5,.markdown-message[data-v-1b7bd8ca] h6{margin:.5em 0 .3em;font-weight:600;line-height:1.3}.markdown-message[data-v-1b7bd8ca] h1{font-size:1.25em}.markdown-message[data-v-1b7bd8ca] h2{font-size:1.15em}.markdown-message[data-v-1b7bd8ca] h3{font-size:1.08em}.markdown-message[data-v-1b7bd8ca] h4,.markdown-message[data-v-1b7bd8ca] h5,.markdown-message[data-v-1b7bd8ca] h6{font-size:1em}.markdown-message[data-v-1b7bd8ca] blockquote{color:#ffffffb3;border-left:3px solid #fff3;margin:.5em 0;padding-left:.75em}.markdown-message[data-v-1b7bd8ca] table{border-collapse:collapse;margin:.5em 0}.markdown-message[data-v-1b7bd8ca] th,.markdown-message[data-v-1b7bd8ca] td{border:1px solid #ffffff26;padding:.25em .5em}.tool-row[data-v-fc912366]{border-radius:4px;margin:0;font-size:12px}.tool-header[data-v-fc912366]{color:#bbb;cursor:default;align-items:center;gap:10px;min-width:0;padding:5px 10px;display:flex}.tool-row:not(.tool-row-generic) .tool-header[data-v-fc912366],.tool-row--toggleable .tool-header[data-v-fc912366]{cursor:pointer}.tool-row:has(.tool-diff) .tool-header[data-v-fc912366]{cursor:pointer}.tool-row:not(.tool-row-generic) .tool-header[data-v-fc912366]:hover,.tool-row--toggleable .tool-header[data-v-fc912366]:hover{background:#ffffff08}.tool-icon[data-v-fc912366]{color:#9fbce0;flex-shrink:0}.tool-name[data-v-fc912366]{color:#d0d0d0;flex-shrink:0;font-weight:600}.tool-arg[data-v-fc912366],.tool-path[data-v-fc912366]{color:#999;text-overflow:ellipsis;white-space:nowrap;min-width:0;max-width:100%;font-family:SF Mono,Menlo,Consolas,monospace;font-size:11.5px;overflow:hidden}.tool-open-diff[data-v-fc912366]{flex-shrink:0}.tool-path[data-v-fc912366],.tool-arg[data-v-fc912366]{flex:1}.tool-stat-add[data-v-fc912366]{color:#66bb6a;flex-shrink:0;font-size:11px;font-weight:600}.tool-stat-del[data-v-fc912366]{color:#ef5350;flex-shrink:0;font-size:11px;font-weight:600}.tool-diff[data-v-fc912366]{background:#0003;border-radius:4px;max-height:400px;margin-top:4px;padding:8px 0;font-family:SF Mono,Menlo,Consolas,monospace;font-size:11px;line-height:1.5;overflow:auto}.diff-line[data-v-fc912366]{white-space:pre;color:#bbb;padding:0 12px}.diff-sign[data-v-fc912366]{color:#555;-webkit-user-select:none;user-select:none;width:14px;display:inline-block}.diff-add[data-v-fc912366]{color:#c8e6c9;background:#66bb6a1a}.diff-add .diff-sign[data-v-fc912366]{color:#66bb6a}.diff-del[data-v-fc912366]{color:#ffcdd2;background:#ef53501a}.diff-del .diff-sign[data-v-fc912366]{color:#ef5350}.tool-output[data-v-fc912366]{color:#aaa;white-space:pre-wrap;background:#00000026;border-radius:4px;max-height:8em;margin-top:4px;padding:6px 10px;font-family:SF Mono,Menlo,Consolas,monospace;font-size:11px;overflow:auto}.markdown-message[data-v-f34be4c5]{color:#e0e0e0;word-break:break-word;overflow-wrap:anywhere;min-width:0;max-width:100%;font-size:13px;line-height:1.55}.markdown-message[data-v-f34be4c5] *{max-width:100%}.markdown-message[data-v-f34be4c5] img{-o-object-fit:contain;object-fit:contain;cursor:zoom-in;background:#0003;border-radius:4px;max-width:180px;max-height:100px;margin:.3em 0;display:block}.markdown-message[data-v-f34be4c5] code{word-break:break-all}.markdown-message[data-v-f34be4c5] p{margin:0 0 .4em}.markdown-message[data-v-f34be4c5] p:last-child{margin-bottom:0}.markdown-message[data-v-f34be4c5] code{background:#00000040;border-radius:3px;padding:.1em .3em}.markdown-message[data-v-f34be4c5] h1,.markdown-message[data-v-f34be4c5] h2,.markdown-message[data-v-f34be4c5] h3,.markdown-message[data-v-f34be4c5] h4,.markdown-message[data-v-f34be4c5] h5,.markdown-message[data-v-f34be4c5] h6{margin:.4em 0 .25em;font-weight:600;line-height:1.3}.markdown-message[data-v-f34be4c5] h1{font-size:1.25em}.markdown-message[data-v-f34be4c5] h2{font-size:1.15em}.markdown-message[data-v-f34be4c5] h3{font-size:1.08em}.markdown-message[data-v-f34be4c5] h4,.markdown-message[data-v-f34be4c5] h5,.markdown-message[data-v-f34be4c5] h6{font-size:1em}.markdown-user-prompt[data-v-f34be4c5]{color:#aaa;font-size:12px;font-style:italic}.markdown-user-prompt[data-v-f34be4c5] p{margin:0 0 .4em}.markdown-user-prompt[data-v-f34be4c5] code{background:#ffffff14;border-radius:3px;padding:.1em .3em;font-style:normal}.image-lightbox-img{-o-object-fit:contain;object-fit:contain;cursor:zoom-out;background:#0000004d;border-radius:4px;max-width:92vw;max-height:92vh;display:block}.turn-card[data-v-6b8d8e3e]{border:1px solid #ffffff14;border-left:3px solid var(--turn-accent);background:#ffffff05;border-radius:6px;min-width:0;max-width:100%;margin:14px 0;overflow:hidden}.turn-card--highlighted[data-v-6b8d8e3e]{animation:1.8s ease-out search-highlight-6b8d8e3e}@keyframes search-highlight-6b8d8e3e{0%,45%{box-shadow:0 0 0 2px #818cf8f2,0 0 22px #818cf873}to{box-shadow:none}}.turn-header[data-v-6b8d8e3e]{color:#888;background:#ffffff08;border-bottom:1px solid #ffffff0d;align-items:center;gap:8px;padding:8px 14px;font-size:11px;display:flex}.turn-badge[data-v-6b8d8e3e]{letter-spacing:.3px;border-radius:3px;padding:2px 8px;font-size:11px;font-weight:700}.turn-badge-user[data-v-6b8d8e3e]{color:#ce93d8;background:#ce93d826}.turn-badge-agent[data-v-6b8d8e3e]{color:#7986cb;background:#7986cb26}.turn-badge-system[data-v-6b8d8e3e]{color:#bdbdbd;background:#75757533;font-style:italic}.turn-badge-session[data-v-6b8d8e3e]{color:#9e9e9e;background:#61616133}.turn-badge-script[data-v-6b8d8e3e]{color:#4db6ac;background:#4db6ac26}.turn-time[data-v-6b8d8e3e]{color:#666;font-family:SF Mono,Menlo,Consolas,monospace;font-size:11px}.turn-time-arrow[data-v-6b8d8e3e]{opacity:.7;margin:0 -2px}.turn-time-updated[data-v-6b8d8e3e]{color:#8891a3}.turn-actions[data-v-6b8d8e3e]{color:#777;font-size:11px}.turn-body[data-v-6b8d8e3e]{flex-direction:column;gap:12px;min-width:0;padding:14px 18px;display:flex}.turn-body[data-v-6b8d8e3e]>*{min-width:0;max-width:100%}.turn-body[data-v-6b8d8e3e] .tool-row+.tool-row{margin-top:-8px}.turn-scroll-top[data-v-6b8d8e3e]{justify-content:flex-start;padding:0 8px 6px;display:flex}.turn-scroll-top-btn[data-v-6b8d8e3e]{opacity:.5;transition:opacity .15s}.turn-scroll-top-btn[data-v-6b8d8e3e]:hover{opacity:1}.activity-feed-wrap[data-v-0cdc51af]{width:100%;height:100%;position:relative}.activity-feed-scroll[data-v-0cdc51af]{width:100%;height:100%}.activity-feed-nav-cluster[data-v-0cdc51af]{z-index:2;align-items:center;gap:8px;display:flex;position:absolute;bottom:14px;right:14px}.activity-feed-compacting[data-v-0cdc51af]{z-index:3;background:var(--kobo-surface);border:1px solid var(--kobo-border);border-radius:6px;align-items:center;gap:8px;padding:6px 14px;display:flex;position:absolute;top:12px;left:50%;transform:translate(-50%);box-shadow:0 4px 16px #00000059}.fade-enter-active[data-v-0cdc51af],.fade-leave-active[data-v-0cdc51af]{transition:opacity .16s}.fade-enter-from[data-v-0cdc51af],.fade-leave-to[data-v-0cdc51af]{opacity:0}.activity-feed-nav-btn[data-v-0cdc51af]{opacity:.8;transition:opacity .12s}.activity-feed-nav-btn[data-v-0cdc51af]:hover{opacity:1}.content-origin-marker[data-v-0cdc51af]{pointer-events:none;width:0;height:0;margin:0;padding:0}.activity-feed-scroll[data-v-0cdc51af] .q-scrollarea__content{max-width:100%;overflow-x:hidden}.activity-feed-switching[data-v-0cdc51af]{justify-content:center;align-items:center;width:100%;height:100%;display:flex}
1
+ .markdown-message[data-v-1b7bd8ca]{color:#e0e0e0;word-break:break-word;overflow-wrap:anywhere;min-width:0;max-width:100%;font-size:13px;line-height:1.55}.markdown-message[data-v-1b7bd8ca] *{max-width:100%}.markdown-message[data-v-1b7bd8ca] p{margin:0 0 .5em}.markdown-message[data-v-1b7bd8ca] p:last-child{margin-bottom:0}.markdown-message[data-v-1b7bd8ca] pre{background:#00000059;border-radius:4px;margin:.5em 0;padding:.5em .75em;overflow-x:auto}.markdown-message[data-v-1b7bd8ca] code{word-break:break-all;background:#0000004d;border-radius:3px;padding:.1em .3em;font-size:.9em}.markdown-message[data-v-1b7bd8ca] pre code{background:0 0;padding:0}.markdown-message[data-v-1b7bd8ca] ul,.markdown-message[data-v-1b7bd8ca] ol{margin:.25em 0 .5em;padding-left:1.5em}.markdown-message[data-v-1b7bd8ca] li{margin:.15em 0}.markdown-message[data-v-1b7bd8ca] a{color:#7986cb;text-decoration:underline}.markdown-message[data-v-1b7bd8ca] .document-link{color:#9fa8da;cursor:pointer;-webkit-text-decoration:underline dotted;text-decoration:underline dotted}.markdown-message[data-v-1b7bd8ca] .document-link:hover{color:#c5cae9;-webkit-text-decoration:underline;text-decoration:underline}.markdown-message[data-v-1b7bd8ca] h1,.markdown-message[data-v-1b7bd8ca] h2,.markdown-message[data-v-1b7bd8ca] h3,.markdown-message[data-v-1b7bd8ca] h4,.markdown-message[data-v-1b7bd8ca] h5,.markdown-message[data-v-1b7bd8ca] h6{margin:.5em 0 .3em;font-weight:600;line-height:1.3}.markdown-message[data-v-1b7bd8ca] h1{font-size:1.25em}.markdown-message[data-v-1b7bd8ca] h2{font-size:1.15em}.markdown-message[data-v-1b7bd8ca] h3{font-size:1.08em}.markdown-message[data-v-1b7bd8ca] h4,.markdown-message[data-v-1b7bd8ca] h5,.markdown-message[data-v-1b7bd8ca] h6{font-size:1em}.markdown-message[data-v-1b7bd8ca] blockquote{color:#ffffffb3;border-left:3px solid #fff3;margin:.5em 0;padding-left:.75em}.markdown-message[data-v-1b7bd8ca] table{border-collapse:collapse;margin:.5em 0}.markdown-message[data-v-1b7bd8ca] th,.markdown-message[data-v-1b7bd8ca] td{border:1px solid #ffffff26;padding:.25em .5em}.tool-row[data-v-fc912366]{border-radius:4px;margin:0;font-size:12px}.tool-header[data-v-fc912366]{color:#bbb;cursor:default;align-items:center;gap:10px;min-width:0;padding:5px 10px;display:flex}.tool-row:not(.tool-row-generic) .tool-header[data-v-fc912366],.tool-row--toggleable .tool-header[data-v-fc912366]{cursor:pointer}.tool-row:has(.tool-diff) .tool-header[data-v-fc912366]{cursor:pointer}.tool-row:not(.tool-row-generic) .tool-header[data-v-fc912366]:hover,.tool-row--toggleable .tool-header[data-v-fc912366]:hover{background:#ffffff08}.tool-icon[data-v-fc912366]{color:#9fbce0;flex-shrink:0}.tool-name[data-v-fc912366]{color:#d0d0d0;flex-shrink:0;font-weight:600}.tool-arg[data-v-fc912366],.tool-path[data-v-fc912366]{color:#999;text-overflow:ellipsis;white-space:nowrap;min-width:0;max-width:100%;font-family:SF Mono,Menlo,Consolas,monospace;font-size:11.5px;overflow:hidden}.tool-open-diff[data-v-fc912366]{flex-shrink:0}.tool-path[data-v-fc912366],.tool-arg[data-v-fc912366]{flex:1}.tool-stat-add[data-v-fc912366]{color:#66bb6a;flex-shrink:0;font-size:11px;font-weight:600}.tool-stat-del[data-v-fc912366]{color:#ef5350;flex-shrink:0;font-size:11px;font-weight:600}.tool-diff[data-v-fc912366]{background:#0003;border-radius:4px;max-height:400px;margin-top:4px;padding:8px 0;font-family:SF Mono,Menlo,Consolas,monospace;font-size:11px;line-height:1.5;overflow:auto}.diff-line[data-v-fc912366]{white-space:pre;color:#bbb;padding:0 12px}.diff-sign[data-v-fc912366]{color:#555;-webkit-user-select:none;user-select:none;width:14px;display:inline-block}.diff-add[data-v-fc912366]{color:#c8e6c9;background:#66bb6a1a}.diff-add .diff-sign[data-v-fc912366]{color:#66bb6a}.diff-del[data-v-fc912366]{color:#ffcdd2;background:#ef53501a}.diff-del .diff-sign[data-v-fc912366]{color:#ef5350}.tool-output[data-v-fc912366]{color:#aaa;white-space:pre-wrap;background:#00000026;border-radius:4px;max-height:8em;margin-top:4px;padding:6px 10px;font-family:SF Mono,Menlo,Consolas,monospace;font-size:11px;overflow:auto}.markdown-message[data-v-f34be4c5]{color:#e0e0e0;word-break:break-word;overflow-wrap:anywhere;min-width:0;max-width:100%;font-size:13px;line-height:1.55}.markdown-message[data-v-f34be4c5] *{max-width:100%}.markdown-message[data-v-f34be4c5] img{-o-object-fit:contain;object-fit:contain;cursor:zoom-in;background:#0003;border-radius:4px;max-width:180px;max-height:100px;margin:.3em 0;display:block}.markdown-message[data-v-f34be4c5] code{word-break:break-all}.markdown-message[data-v-f34be4c5] p{margin:0 0 .4em}.markdown-message[data-v-f34be4c5] p:last-child{margin-bottom:0}.markdown-message[data-v-f34be4c5] code{background:#00000040;border-radius:3px;padding:.1em .3em}.markdown-message[data-v-f34be4c5] h1,.markdown-message[data-v-f34be4c5] h2,.markdown-message[data-v-f34be4c5] h3,.markdown-message[data-v-f34be4c5] h4,.markdown-message[data-v-f34be4c5] h5,.markdown-message[data-v-f34be4c5] h6{margin:.4em 0 .25em;font-weight:600;line-height:1.3}.markdown-message[data-v-f34be4c5] h1{font-size:1.25em}.markdown-message[data-v-f34be4c5] h2{font-size:1.15em}.markdown-message[data-v-f34be4c5] h3{font-size:1.08em}.markdown-message[data-v-f34be4c5] h4,.markdown-message[data-v-f34be4c5] h5,.markdown-message[data-v-f34be4c5] h6{font-size:1em}.markdown-user-prompt[data-v-f34be4c5]{color:#aaa;font-size:12px;font-style:italic}.markdown-user-prompt[data-v-f34be4c5] p{margin:0 0 .4em}.markdown-user-prompt[data-v-f34be4c5] code{background:#ffffff14;border-radius:3px;padding:.1em .3em;font-style:normal}.image-lightbox-img{-o-object-fit:contain;object-fit:contain;cursor:zoom-out;background:#0000004d;border-radius:4px;max-width:92vw;max-height:92vh;display:block}.turn-card[data-v-6b8d8e3e]{border:1px solid #ffffff14;border-left:3px solid var(--turn-accent);background:#ffffff05;border-radius:6px;min-width:0;max-width:100%;margin:14px 0;overflow:hidden}.turn-card--highlighted[data-v-6b8d8e3e]{animation:1.8s ease-out search-highlight-6b8d8e3e}@keyframes search-highlight-6b8d8e3e{0%,45%{box-shadow:0 0 0 2px #818cf8f2,0 0 22px #818cf873}to{box-shadow:none}}.turn-header[data-v-6b8d8e3e]{color:#888;background:#ffffff08;border-bottom:1px solid #ffffff0d;align-items:center;gap:8px;padding:8px 14px;font-size:11px;display:flex}.turn-badge[data-v-6b8d8e3e]{letter-spacing:.3px;border-radius:3px;padding:2px 8px;font-size:11px;font-weight:700}.turn-badge-user[data-v-6b8d8e3e]{color:#ce93d8;background:#ce93d826}.turn-badge-agent[data-v-6b8d8e3e]{color:#7986cb;background:#7986cb26}.turn-badge-system[data-v-6b8d8e3e]{color:#bdbdbd;background:#75757533;font-style:italic}.turn-badge-session[data-v-6b8d8e3e]{color:#9e9e9e;background:#61616133}.turn-badge-script[data-v-6b8d8e3e]{color:#4db6ac;background:#4db6ac26}.turn-time[data-v-6b8d8e3e]{color:#666;font-family:SF Mono,Menlo,Consolas,monospace;font-size:11px}.turn-time-arrow[data-v-6b8d8e3e]{opacity:.7;margin:0 -2px}.turn-time-updated[data-v-6b8d8e3e]{color:#8891a3}.turn-actions[data-v-6b8d8e3e]{color:#777;font-size:11px}.turn-body[data-v-6b8d8e3e]{flex-direction:column;gap:12px;min-width:0;padding:14px 18px;display:flex}.turn-body[data-v-6b8d8e3e]>*{min-width:0;max-width:100%}.turn-body[data-v-6b8d8e3e] .tool-row+.tool-row{margin-top:-8px}.turn-scroll-top[data-v-6b8d8e3e]{justify-content:flex-start;padding:0 8px 6px;display:flex}.turn-scroll-top-btn[data-v-6b8d8e3e]{opacity:.5;transition:opacity .15s}.turn-scroll-top-btn[data-v-6b8d8e3e]:hover{opacity:1}.activity-feed-wrap[data-v-0fba0dc4]{width:100%;height:100%;position:relative}.activity-feed-scroll[data-v-0fba0dc4]{width:100%;height:100%}.activity-feed-nav-cluster[data-v-0fba0dc4]{z-index:2;align-items:center;gap:8px;display:flex;position:absolute;bottom:14px;right:14px}.activity-feed-compacting[data-v-0fba0dc4]{z-index:3;background:var(--kobo-surface);border:1px solid var(--kobo-border);border-radius:6px;align-items:center;gap:8px;padding:6px 14px;display:flex;position:absolute;top:12px;left:50%;transform:translate(-50%);box-shadow:0 4px 16px #00000059}.fade-enter-active[data-v-0fba0dc4],.fade-leave-active[data-v-0fba0dc4]{transition:opacity .16s}.fade-enter-from[data-v-0fba0dc4],.fade-leave-to[data-v-0fba0dc4]{opacity:0}.activity-feed-nav-btn[data-v-0fba0dc4]{opacity:.8;transition:opacity .12s}.activity-feed-nav-btn[data-v-0fba0dc4]:hover{opacity:1}.content-origin-marker[data-v-0fba0dc4]{pointer-events:none;width:0;height:0;margin:0;padding:0}.activity-feed-scroll[data-v-0fba0dc4] .q-scrollarea__content{max-width:100%;overflow-x:hidden}.activity-feed-switching[data-v-0fba0dc4]{justify-content:center;align-items:center;width:100%;height:100%;display:flex}