@loicngr/kobo 1.11.40 → 1.11.42

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 (90) hide show
  1. package/AGENTS.md +110 -32
  2. package/CHANGELOG.md +8 -0
  3. package/README.md +6 -4
  4. package/dist/server/routes/images.js +65 -5
  5. package/dist/server/routes/workspaces.js +29 -10
  6. package/dist/server/services/activity-service.js +3 -0
  7. package/dist/server/services/agent/engines/claude-code/engine.js +1 -0
  8. package/dist/server/services/attachment-service.js +122 -0
  9. package/dist/server/services/image-service.js +16 -7
  10. package/dist/server/services/settings-service.js +13 -0
  11. package/dist/shared/attachments.js +58 -0
  12. package/package.json +1 -1
  13. package/src/client/dist/pwa/assets/{ActivityFeed-MqsC-95i.js → ActivityFeed-CyCGH0Zr.js} +1 -1
  14. package/src/client/dist/pwa/assets/{ChangelogPage-BxURWYxa.js → ChangelogPage-B6xjI-I4.js} +1 -1
  15. package/src/client/dist/pwa/assets/{ComparisonPanel-DjiMDAUi.js → ComparisonPanel-BmRzCu1c.js} +1 -1
  16. package/src/client/dist/pwa/assets/CreatePage-DHe0x63K.js +2 -0
  17. package/src/client/dist/pwa/assets/CreatePage-TY9g3hZb.css +1 -0
  18. package/src/client/dist/pwa/assets/{DashboardPage-mA5RZf1t.js → DashboardPage-CRTkx9P-.js} +1 -1
  19. package/src/client/dist/pwa/assets/{DiffViewer-Ci7ucmy3.js → DiffViewer-BJfLiYGs.js} +3 -3
  20. package/src/client/dist/pwa/assets/{DocumentsPanel-RZTvcJkT.js → DocumentsPanel-B9jjzoqY.js} +1 -1
  21. package/src/client/dist/pwa/assets/{DrawerToggleButton-CTOc3yA0.js → DrawerToggleButton-CBZpPa30.js} +1 -1
  22. package/src/client/dist/pwa/assets/GitPanel-gqySqt7G.js +3 -0
  23. package/src/client/dist/pwa/assets/{HealthPage-D3__hKJ5.js → HealthPage-9VEvoR_4.js} +1 -1
  24. package/src/client/dist/pwa/assets/MainLayout-BM14JTBB.js +2 -0
  25. package/src/client/dist/pwa/assets/{MainLayout-DxMVVA6f.css → MainLayout-Dhj28mOe.css} +1 -1
  26. package/src/client/dist/pwa/assets/{QBtnDropdown-td0RAVPx.js → QBtnDropdown-Cnkq_5eu.js} +1 -1
  27. package/src/client/dist/pwa/assets/{QDrawer-BkyFPV7a.js → QDrawer-C3b-lbmv.js} +1 -1
  28. package/src/client/dist/pwa/assets/{QSelect-WspK3KC7.js → QSelect-DJL6yy1E.js} +1 -1
  29. package/src/client/dist/pwa/assets/{QTooltip-7pw7iuV0.js → QTooltip-WIoDeLoZ.js} +1 -1
  30. package/src/client/dist/pwa/assets/{SchedulePanel-CLNM13oo.js → SchedulePanel-tVggDx-C.js} +1 -1
  31. package/src/client/dist/pwa/assets/{SearchPage-CmRgIvu4.js → SearchPage-l4m1qxKs.js} +1 -1
  32. package/src/client/dist/pwa/assets/SettingsPage-W6N01tGz.css +1 -0
  33. package/src/client/dist/pwa/assets/SettingsPage-msHMBS7q.js +9 -0
  34. package/src/client/dist/pwa/assets/{SplitWorkspacePage-CCHGEsKf.js → SplitWorkspacePage-AXMYd-9b.js} +1 -1
  35. package/src/client/dist/pwa/assets/{TerminalPanel-ijmsYk-2.js → TerminalPanel-CENXl8po.js} +1 -1
  36. package/src/client/dist/pwa/assets/{TouchPan-DuQFjlQF.js → TouchPan-DaEpS0Vf.js} +1 -1
  37. package/src/client/dist/pwa/assets/{TourReplayButton-C5RF0lOK.js → TourReplayButton-U3mwT0fV.js} +1 -1
  38. package/src/client/dist/pwa/assets/{WorkspaceAttentionLabels-BR_XElog.js → WorkspaceAttentionLabels-CpTA6VGb.js} +1 -1
  39. package/src/client/dist/pwa/assets/WorkspacePage-DCg9zSxz.css +1 -0
  40. package/src/client/dist/pwa/assets/WorkspacePage-DE6-wlf7.js +4 -0
  41. package/src/client/dist/pwa/assets/{browser-DUqzdkbg.js → browser-B4pgwnXH.js} +1 -1
  42. package/src/client/dist/pwa/assets/{build-path-tree-ImiSpzfI.js → build-path-tree-6ENXqh7O.js} +1 -1
  43. package/src/client/dist/pwa/assets/{de-CaHID9mW.js → de-D9gPXN_a.js} +2 -2
  44. package/src/client/dist/pwa/assets/{documents-2C1hn1pw.js → documents-D88lOIxR.js} +1 -1
  45. package/src/client/dist/pwa/assets/{editor.api-Bm1vVaBr.js → editor.api-qQNqM6Y6.js} +1 -1
  46. package/src/client/dist/pwa/assets/{efforts-_D8DFYXo.js → efforts-CMQi0Rp2.js} +1 -1
  47. package/src/client/dist/pwa/assets/{engineFeatures-CZ9qfBYq.js → engineFeatures-BPcXoXFd.js} +1 -1
  48. package/src/client/dist/pwa/assets/{es-ZhzlCmer.js → es-B4Q1ncyP.js} +2 -2
  49. package/src/client/dist/pwa/assets/expand-template-DuMdsD-k.js +1 -0
  50. package/src/client/dist/pwa/assets/{format-pPImdFUy.js → format-BVgznNN8.js} +1 -1
  51. package/src/client/dist/pwa/assets/{formatters-dTX38ZsV.js → formatters-DV1VAk5B.js} +1 -1
  52. package/src/client/dist/pwa/assets/{fr-BfXw7L7J.js → fr-Qv-edVJn.js} +2 -2
  53. package/src/client/dist/pwa/assets/{freemarker2-DGluDnAm.js → freemarker2-Nt_Z9A9p.js} +1 -1
  54. package/src/client/dist/pwa/assets/{handlebars-BgviTnpD.js → handlebars-sJjXCA98.js} +1 -1
  55. package/src/client/dist/pwa/assets/{html-CV-MrunL.js → html-B2wF0NKO.js} +1 -1
  56. package/src/client/dist/pwa/assets/i18n-eiyc92m_.js +1 -0
  57. package/src/client/dist/pwa/assets/{index-9tiw3NjK.js → index-DtsrXdzx.js} +3 -3
  58. package/src/client/dist/pwa/assets/{it-CHAurcTF.js → it-D-qzfo9T.js} +2 -2
  59. package/src/client/dist/pwa/assets/{javascript-C2VFcHf2.js → javascript-BPbCoYVZ.js} +1 -1
  60. package/src/client/dist/pwa/assets/{liquid-Dg9ZhbP5.js → liquid-DBwLDEnw.js} +1 -1
  61. package/src/client/dist/pwa/assets/{mdx-ImgrPYNl.js → mdx-D_Sphj-P.js} +1 -1
  62. package/src/client/dist/pwa/assets/{monaco-contributions-waWZXS1T.js → monaco-contributions-BBjimLYs.js} +1 -1
  63. package/src/client/dist/pwa/assets/monaco.contribution-DNP7HNKq.js +2 -0
  64. package/src/client/dist/pwa/assets/{network-auth-h6O0qCMA.js → network-auth-BLGlHVxR.js} +1 -1
  65. package/src/client/dist/pwa/assets/{notify-theme-Dyg-FGI6.js → notify-theme-tvaTlaFw.js} +1 -1
  66. package/src/client/dist/pwa/assets/{pending-diff-open-Dfpg92li.js → pending-diff-open-CxtcrwVN.js} +1 -1
  67. package/src/client/dist/pwa/assets/{permissionModes-DDWmNBW1.js → permissionModes-CESk18Vk.js} +1 -1
  68. package/src/client/dist/pwa/assets/{project-color-XUfY_zbw.js → project-color-D75XmBNA.js} +1 -1
  69. package/src/client/dist/pwa/assets/{python-DVLs-DBA.js → python-BdBTal4f.js} +1 -1
  70. package/src/client/dist/pwa/assets/{razor-D34BqMwB.js → razor-gJNlRRte.js} +1 -1
  71. package/src/client/dist/pwa/assets/{render-chat-markdown-DwUkPy-O.js → render-chat-markdown-K3tUW_1p.js} +1 -1
  72. package/src/client/dist/pwa/assets/{typescript-C7-UpNXx.js → typescript-DAGixRVW.js} +1 -1
  73. package/src/client/dist/pwa/assets/{use-tours-DfE-kwrX.js → use-tours-CS5oBFk6.js} +1 -1
  74. package/src/client/dist/pwa/assets/{workspace-templates-DwwdOEAq.js → workspace-templates-BoGUaV1C.js} +1 -1
  75. package/src/client/dist/pwa/assets/{xml-Cx1oQhEr.js → xml-BC9pt2ZZ.js} +1 -1
  76. package/src/client/dist/pwa/assets/{yaml-BQOiJsdx.js → yaml-CQpOdW-l.js} +1 -1
  77. package/src/client/dist/pwa/index.html +1 -1
  78. package/src/client/dist/pwa/sw.js +1 -1
  79. package/src/mcp-server/README.md +51 -7
  80. package/src/client/dist/pwa/assets/CreatePage-A3pVradx.css +0 -1
  81. package/src/client/dist/pwa/assets/CreatePage-B-rhBmqv.js +0 -2
  82. package/src/client/dist/pwa/assets/GitPanel-_kL2gMoy.js +0 -3
  83. package/src/client/dist/pwa/assets/MainLayout-B3JKUm3n.js +0 -2
  84. package/src/client/dist/pwa/assets/SettingsPage-Cj6Y_TlN.js +0 -9
  85. package/src/client/dist/pwa/assets/SettingsPage-sVlsJQ0b.css +0 -1
  86. package/src/client/dist/pwa/assets/WorkspacePage-BGCildyP.js +0 -4
  87. package/src/client/dist/pwa/assets/WorkspacePage-CqcMweNL.css +0 -1
  88. package/src/client/dist/pwa/assets/expand-template-CbDNnTPP.js +0 -1
  89. package/src/client/dist/pwa/assets/i18n-CmXBenUp.js +0 -1
  90. package/src/client/dist/pwa/assets/monaco.contribution-BB2rtCHM.js +0 -2
@@ -7,12 +7,15 @@ const execFileAsync = promisify(execFileCb);
7
7
  import fs from 'node:fs';
8
8
  import path from 'node:path';
9
9
  import { Hono } from 'hono';
10
+ import { bodyLimit } from 'hono/body-limit';
11
+ import { MAX_ATTACHMENT_REQUEST_BYTES } from '../../shared/attachments.js';
10
12
  import { AUTO_LOOP_HARD_RULES, buildAutoLoopGroomingSteps, buildGroomingIntro } from '../../shared/auto-loop-prompts.js';
11
13
  import { getDb } from '../db/index.js';
12
14
  import { migrationGuard } from '../middleware/migration-guard.js';
13
15
  import { listEngines } from '../services/agent/engines/registry.js';
14
16
  import * as agentManager from '../services/agent/orchestrator.js';
15
17
  import * as archiveScriptService from '../services/archive-script-service.js';
18
+ import { AttachmentRequestError, attachmentReference, readWorkspaceCreationRequest, removeAttachments, saveAttachments, } from '../services/attachment-service.js';
16
19
  import * as autoLoopService from '../services/auto-loop-service.js';
17
20
  import { changeSourceBranch } from '../services/change-source-branch-service.js';
18
21
  import { listChatHistory, pushChatHistory } from '../services/chat-history-service.js';
@@ -183,12 +186,16 @@ function emitCreateFailed(creationId, step, message) {
183
186
  */
184
187
  async function rollbackFailedCreation(workspace, opts) {
185
188
  try {
186
- return await deleteWorkspaceWithSideEffects(workspace, {
189
+ const warnings = await deleteWorkspaceWithSideEffects(workspace, {
187
190
  deleteLocalBranch: opts.deleteLocalBranch,
188
191
  removeWorktree: opts.removeWorktree,
189
192
  // Nothing was ever pushed at this point in the creation flow.
190
193
  deleteRemoteBranch: false,
191
194
  });
195
+ if (!opts.removeWorktree && opts.attachments?.length && fs.existsSync(workspace.worktreePath)) {
196
+ warnings.push(...(await removeAttachments(workspace.worktreePath, opts.attachments)));
197
+ }
198
+ return warnings;
192
199
  }
193
200
  catch (err) {
194
201
  const message = err instanceof Error ? err.message : String(err);
@@ -353,8 +360,11 @@ app.post('/:id/switch-engine', migrationGuard, async (c) => {
353
360
  return c.json({ error: err instanceof Error ? err.message : 'Failed to switch engine' }, 500);
354
361
  }
355
362
  });
356
- // POST /api/workspaces — create workspace
357
- app.post('/', migrationGuard, async (c) => {
363
+ const creationBodyLimit = bodyLimit({
364
+ maxSize: MAX_ATTACHMENT_REQUEST_BYTES,
365
+ onError: (c) => c.json({ error: 'Workspace creation request is too large', step: 'validate' }, 413),
366
+ });
367
+ app.post('/', migrationGuard, creationBodyLimit, async (c) => {
358
368
  // Declared outside the try so the outermost catch (any unforeseen throw,
359
369
  // including a malformed request body) can still name a step instead of
360
370
  // leaving the create-progress channel silent. Updated in lockstep with
@@ -371,14 +381,13 @@ app.post('/', migrationGuard, async (c) => {
371
381
  // branch. A failure before that point must not delete a branch we never made.
372
382
  let createdBranch = false;
373
383
  let createdWorktree = false;
384
+ let creationAttachments = [];
374
385
  // Hoisted out of the `build-prompt` block (declared with `const` there) so
375
386
  // both rollback paths — the `start-agent` branch and the outermost catch —
376
387
  // can read it to decide whether the setup-script caveat applies.
377
388
  let setupScriptConfigured = false;
378
389
  try {
379
- const body = await c.req
380
- .json()
381
- .catch(() => ({}));
390
+ const { body, attachments } = await readWorkspaceCreationRequest(c.req.raw);
382
391
  // workingBranch is derived from git when worktreePath is provided, so
383
392
  // it's not required in that flow. The other 3 fields stay mandatory.
384
393
  if (!body.name || !body.projectPath || !body.sourceBranch) {
@@ -890,6 +899,7 @@ app.post('/', migrationGuard, async (c) => {
890
899
  ['.ai/.git-conventions.md', '.git-conventions.md'],
891
900
  ['.ai/thoughts/', 'thoughts/'],
892
901
  ['.ai/images/', 'images/'],
902
+ ['.ai/attachments/', 'attachments/'],
893
903
  ['.ai/.setup-script.tmp', '.setup-script.tmp'],
894
904
  ['.ai/.cleanup-script.tmp', '.cleanup-script.tmp'],
895
905
  ['.ai/.archive-script.tmp', '.archive-script.tmp'],
@@ -943,6 +953,12 @@ app.post('/', migrationGuard, async (c) => {
943
953
  let agentStartError = null;
944
954
  currentStep = 'write-context-files';
945
955
  emitCreateProgress(creationId, currentStep);
956
+ creationAttachments = await saveAttachments(worktreePath, attachments);
957
+ if (creationAttachments.length > 0) {
958
+ body.description = [body.description?.trim(), ...creationAttachments.map(attachmentReference)]
959
+ .filter(Boolean)
960
+ .join('\n\n');
961
+ }
946
962
  // Save Notion content as markdown in worktree
947
963
  let notionFilePath = null;
948
964
  if (notionContent && body.notionUrl) {
@@ -1249,9 +1265,10 @@ Once the brainstorming + planning steps above are complete and you have a saved
1249
1265
  }
1250
1266
  // Setup script — runs AFTER the prompt is persisted so a crash here
1251
1267
  // leaves the workspace in `error` state with `initial_prompt` ready for
1252
- // a retry via POST /:id/start. Skipped when reusing an existing worktree
1253
- // (rerunning could be destructive — drop a curated node_modules, etc.).
1254
- setupScriptConfigured = Boolean(effectiveSettings.setupScript) && !body.skipSetupScript && !useReusedWorktree;
1268
+ // a retry via POST /:id/start. PR imports require an explicit false to
1269
+ // run setup; ordinary worktree reuse continues to skip it unconditionally.
1270
+ const allowSetupScript = prContent ? body.skipSetupScript === false : !useReusedWorktree && !body.skipSetupScript;
1271
+ setupScriptConfigured = Boolean(effectiveSettings.setupScript) && allowSetupScript;
1255
1272
  if (setupScriptConfigured) {
1256
1273
  currentStep = 'setup-script';
1257
1274
  emitCreateProgress(creationId, currentStep);
@@ -1343,6 +1360,7 @@ Once the brainstorming + planning steps above are complete and you have a saved
1343
1360
  const rollbackWarnings = await rollbackFailedCreation(workspace, {
1344
1361
  deleteLocalBranch: createdBranch,
1345
1362
  removeWorktree: createdWorktree,
1363
+ attachments: creationAttachments,
1346
1364
  });
1347
1365
  // The setup script has already run by this point — see the caveat's own
1348
1366
  // comment for what that rollback cannot reach.
@@ -1400,6 +1418,7 @@ Once the brainstorming + planning steps above are complete and you have a saved
1400
1418
  const rollbackWarnings = await rollbackFailedCreation(createdWorkspace, {
1401
1419
  deleteLocalBranch: createdBranch,
1402
1420
  removeWorktree: createdWorktree,
1421
+ attachments: creationAttachments,
1403
1422
  });
1404
1423
  logError('workspaces', 'Rolled back a creation that failed outside any per-step handler', {
1405
1424
  workspaceId: createdWorkspace.id,
@@ -1418,7 +1437,7 @@ Once the brainstorming + planning steps above are complete and you have a saved
1418
1437
  return c.json({ error: failure, step: currentStep, rollback: { done: true, warnings: rollbackWarnings } }, 500);
1419
1438
  }
1420
1439
  emitCreateFailed(creationId, currentStep, message);
1421
- return c.json({ error: message, step: currentStep }, 500);
1440
+ return c.json({ error: message, step: currentStep }, err instanceof AttachmentRequestError ? 400 : 500);
1422
1441
  }
1423
1442
  });
1424
1443
  // POST /api/workspaces/:id/sessions — create a new idle agent session
@@ -1,4 +1,5 @@
1
1
  import { getDb } from '../db/index.js';
2
+ import { getGlobalSettings } from './settings-service.js';
2
3
  export function classifyActivity(type, payload) {
3
4
  const event = payload && typeof payload === 'object' ? payload : {};
4
5
  if (type === 'agent:event') {
@@ -33,6 +34,8 @@ export function recordActivity(workspaceId, type, payload, sessionId, db) {
33
34
  if (!kind)
34
35
  return;
35
36
  try {
37
+ if (getGlobalSettings().activityDigestEnabled === false)
38
+ return;
36
39
  const database = db ?? getDb();
37
40
  database
38
41
  .prepare('INSERT INTO workspace_activity (workspace_id, kind, session_id, created_at) VALUES (?, ?, ?, ?)')
@@ -377,6 +377,7 @@ export function createClaudeCodeEngine() {
377
377
  kind: 'error',
378
378
  category: 'other',
379
379
  message: 'Session force-ended: the SDK generator stayed open after its final result (drain watchdog).',
380
+ code: 'result_drain_timeout',
380
381
  });
381
382
  emitSessionEnded('watchdog', null);
382
383
  }
@@ -0,0 +1,122 @@
1
+ import fs from 'node:fs';
2
+ import path from 'node:path';
3
+ import { nanoid } from 'nanoid';
4
+ import { attachmentFormat, validateAttachments } from '../../shared/attachments.js';
5
+ import { ensureDirectoryInside, resolvePathInside } from '../utils/safe-path.js';
6
+ import { deleteImage, saveImage } from './image-service.js';
7
+ export class AttachmentRequestError extends Error {
8
+ }
9
+ /** Existing JSON clients stay supported; browser attachments travel with the creation request. */
10
+ export async function readWorkspaceCreationRequest(request) {
11
+ let body;
12
+ let attachments = [];
13
+ try {
14
+ if (request.headers.get('content-type')?.toLowerCase().startsWith('multipart/form-data')) {
15
+ const form = await request.formData();
16
+ const metadata = form.get('workspace');
17
+ if (typeof metadata !== 'string' || form.getAll('workspace').length !== 1) {
18
+ throw new AttachmentRequestError('Expected one JSON workspace field');
19
+ }
20
+ body = JSON.parse(metadata);
21
+ const files = [...form.getAll('attachments'), ...form.getAll('images')];
22
+ if (files.some((file) => !(file instanceof File))) {
23
+ throw new AttachmentRequestError('Each attachment must be a file');
24
+ }
25
+ attachments = files;
26
+ }
27
+ else {
28
+ body = await request.json();
29
+ }
30
+ }
31
+ catch (error) {
32
+ if (error instanceof AttachmentRequestError)
33
+ throw error;
34
+ throw new AttachmentRequestError('Invalid workspace creation body');
35
+ }
36
+ if (!body || typeof body !== 'object' || Array.isArray(body)) {
37
+ throw new AttachmentRequestError('Workspace must be a JSON object');
38
+ }
39
+ if ('description' in body && body.description !== undefined && typeof body.description !== 'string') {
40
+ throw new AttachmentRequestError('description must be a string');
41
+ }
42
+ const error = validateAttachments(attachments);
43
+ if (error) {
44
+ const messages = {
45
+ type: 'Unsupported attachment type. Use PNG, JPEG, GIF, WebP, PDF, Markdown or text documents',
46
+ size: 'Each attachment must be non-empty and no larger than 50 MB',
47
+ count: 'A workspace can be created with at most 10 attachments',
48
+ total: 'Creation attachments must total no more than 50 MB',
49
+ };
50
+ throw new AttachmentRequestError(messages[error]);
51
+ }
52
+ return { body: body, attachments };
53
+ }
54
+ /** Remove only this creation's files, preserving existing worktree content. */
55
+ export async function removeAttachments(worktreePath, attachments) {
56
+ const warnings = [];
57
+ for (const attachment of attachments) {
58
+ try {
59
+ if (attachment.kind === 'image') {
60
+ await deleteImage(worktreePath, attachment.uid);
61
+ }
62
+ else {
63
+ const directory = ensureDirectoryInside(worktreePath, '.ai/attachments');
64
+ // Paths come exclusively from saveAttachments; never use the original upload name.
65
+ fs.unlinkSync(path.join(directory, path.basename(attachment.relativePath)));
66
+ }
67
+ }
68
+ catch (error) {
69
+ warnings.push(`Could not remove creation attachment '${attachment.uid}': ${error instanceof Error ? error.message : error}`);
70
+ }
71
+ }
72
+ return warnings;
73
+ }
74
+ export async function saveAttachments(worktreePath, files) {
75
+ const saved = [];
76
+ try {
77
+ for (const file of files) {
78
+ const format = attachmentFormat(file);
79
+ if (!format)
80
+ throw new AttachmentRequestError('Unsupported attachment type');
81
+ const buffer = Buffer.from(await file.arrayBuffer());
82
+ if (format.kind === 'image') {
83
+ const name = path.extname(file.name).toLowerCase() === format.extension
84
+ ? file.name
85
+ : `${file.name || 'image'}${format.extension}`;
86
+ saved.push({ ...(await saveImage(worktreePath, buffer, name)), kind: 'image', originalName: file.name });
87
+ }
88
+ else {
89
+ const directory = ensureDirectoryInside(worktreePath, '.ai/attachments');
90
+ const ignorePath = resolvePathInside(worktreePath, '.gitignore');
91
+ const ignore = fs.existsSync(ignorePath) ? fs.readFileSync(ignorePath, 'utf8') : '';
92
+ if (!ignore.split('\n').some((line) => line.trim() === '.ai/attachments/')) {
93
+ fs.appendFileSync(ignorePath, `${ignore && !ignore.endsWith('\n') ? '\n' : ''}.ai/attachments/\n`);
94
+ }
95
+ const uid = nanoid(10);
96
+ const filename = `${uid}${format.extension}`;
97
+ const descriptor = fs.openSync(path.join(directory, filename), 'wx', 0o600);
98
+ // Track ownership as soon as exclusive creation succeeds: a failed
99
+ // write can leave bytes on disk and must participate in rollback.
100
+ saved.push({ uid, relativePath: `.ai/attachments/${filename}`, kind: 'file', originalName: file.name });
101
+ try {
102
+ fs.writeFileSync(descriptor, buffer);
103
+ }
104
+ finally {
105
+ fs.closeSync(descriptor);
106
+ }
107
+ }
108
+ }
109
+ return saved;
110
+ }
111
+ catch (error) {
112
+ const warnings = await removeAttachments(worktreePath, saved);
113
+ for (const warning of warnings)
114
+ console.error(`[creation-attachments] ${warning}`);
115
+ throw error;
116
+ }
117
+ }
118
+ export function attachmentReference(attachment) {
119
+ return attachment.kind === 'image'
120
+ ? `[image: ${attachment.relativePath}]`
121
+ : `Attached document ${JSON.stringify(attachment.originalName)}: [file: ${attachment.relativePath}]`;
122
+ }
@@ -39,14 +39,18 @@ function writeIndex(imagesDir, entries) {
39
39
  throw new Error('Symbolic links are not allowed for image index');
40
40
  }
41
41
  const tempPath = path.join(imagesDir, `.index-${randomUUID()}.tmp`);
42
- let created = false;
42
+ const descriptor = fs.openSync(tempPath, 'wx', 0o600);
43
43
  try {
44
- fs.writeFileSync(tempPath, JSON.stringify(entries, null, 2), { flag: 'wx', mode: 0o600 });
45
- created = true;
44
+ try {
45
+ fs.writeFileSync(descriptor, JSON.stringify(entries, null, 2));
46
+ }
47
+ finally {
48
+ fs.closeSync(descriptor);
49
+ }
46
50
  fs.renameSync(tempPath, indexPath);
47
51
  }
48
52
  finally {
49
- if (created && fs.existsSync(tempPath))
53
+ if (fs.existsSync(tempPath))
50
54
  fs.unlinkSync(tempPath);
51
55
  }
52
56
  }
@@ -63,11 +67,16 @@ export async function saveImage(worktreePath, fileBuffer, originalName) {
63
67
  const imagesDir = ensureRealImagesDirectory(worktreePath);
64
68
  const filename = `${uid}.${ext}`;
65
69
  await withLock(worktreePath, () => {
66
- // Write the image file inside the lock so both the file write and index
67
- // update happen atomically — avoids orphan files on crash between the two.
70
+ // Serialize image/index writes and clean up incomplete writes on failure.
68
71
  const imagePath = path.join(imagesDir, filename);
69
- fs.writeFileSync(imagePath, fileBuffer, { flag: 'wx', mode: 0o600 });
72
+ const descriptor = fs.openSync(imagePath, 'wx', 0o600);
70
73
  try {
74
+ try {
75
+ fs.writeFileSync(descriptor, fileBuffer);
76
+ }
77
+ finally {
78
+ fs.closeSync(descriptor);
79
+ }
71
80
  const entries = readIndex(imagesDir);
72
81
  entries.push({ uid, originalName, createdAt: new Date().toISOString() });
73
82
  writeIndex(imagesDir, entries);
@@ -901,6 +901,14 @@ const settingsMigrations = [
901
901
  global.defaultModelByEngine = map;
902
902
  },
903
903
  },
904
+ {
905
+ version: 58,
906
+ name: 'add-activity-digest-toggle',
907
+ migrate: ({ global }) => {
908
+ if (typeof global.activityDigestEnabled !== 'boolean')
909
+ global.activityDigestEnabled = true;
910
+ },
911
+ },
904
912
  ];
905
913
  /** Current settings schema version — always equals the highest migration version. */
906
914
  export const SETTINGS_SCHEMA_VERSION = settingsMigrations.length > 0 ? settingsMigrations[settingsMigrations.length - 1].version : 0;
@@ -954,6 +962,7 @@ function defaultSettings() {
954
962
  autoPurgeOnPrMerged: false,
955
963
  autoLoopMaxRetries: 5,
956
964
  awaitingUserReminderMinutes: 0,
965
+ activityDigestEnabled: true,
957
966
  maxConcurrentAgents: 0,
958
967
  wsEventsRetentionDays: 0,
959
968
  wsEventsKeepPerWorkspace: 0,
@@ -1460,6 +1469,9 @@ export function updateGlobalSettings(input) {
1460
1469
  }
1461
1470
  }
1462
1471
  }
1472
+ if ('activityDigestEnabled' in data && typeof data.activityDigestEnabled !== 'boolean') {
1473
+ delete data.activityDigestEnabled;
1474
+ }
1463
1475
  const allowedGlobalKeys = [
1464
1476
  'defaultModelByEngine',
1465
1477
  'dangerouslySkipPermissions',
@@ -1485,6 +1497,7 @@ export function updateGlobalSettings(input) {
1485
1497
  'autoPurgeOnPrMerged',
1486
1498
  'autoLoopMaxRetries',
1487
1499
  'awaitingUserReminderMinutes',
1500
+ 'activityDigestEnabled',
1488
1501
  'maxConcurrentAgents',
1489
1502
  'wsEventsRetentionDays',
1490
1503
  'wsEventsKeepPerWorkspace',
@@ -0,0 +1,58 @@
1
+ /** Bounds and formats shared by the create form and multipart endpoint. */
2
+ const IMAGE_EXTENSIONS = {
3
+ 'image/png': ['.png'],
4
+ 'image/jpeg': ['.jpg', '.jpeg'],
5
+ 'image/gif': ['.gif'],
6
+ 'image/webp': ['.webp'],
7
+ };
8
+ export const DOCUMENT_EXTENSIONS = [
9
+ '.md',
10
+ '.markdown',
11
+ '.txt',
12
+ '.text',
13
+ '.pdf',
14
+ '.csv',
15
+ '.tsv',
16
+ '.json',
17
+ '.jsonl',
18
+ '.yaml',
19
+ '.yml',
20
+ '.xml',
21
+ '.log',
22
+ '.rst',
23
+ '.toml',
24
+ ];
25
+ export const ATTACHMENT_ACCEPT = [
26
+ ...Object.keys(IMAGE_EXTENSIONS),
27
+ ...Object.values(IMAGE_EXTENSIONS).flat(),
28
+ ...DOCUMENT_EXTENSIONS,
29
+ ].join(',');
30
+ export const MAX_ATTACHMENTS = 10;
31
+ export const MAX_ATTACHMENT_BYTES = 50 * 1024 * 1024;
32
+ export const MAX_ATTACHMENTS_BYTES = 50 * 1024 * 1024;
33
+ export const MAX_ATTACHMENT_REQUEST_BYTES = MAX_ATTACHMENTS_BYTES + 1024 * 1024;
34
+ export function attachmentFormat(file) {
35
+ const extension = /\.[^.\\/]+$/.exec(file.name ?? '')?.[0]?.toLowerCase() ?? '';
36
+ const imageExtensions = IMAGE_EXTENSIONS[file.type];
37
+ if (imageExtensions)
38
+ return { kind: 'image', extension: imageExtensions.includes(extension) ? extension : imageExtensions[0] };
39
+ // Browsers vary in their MIME labels for text documents, especially Markdown.
40
+ if (DOCUMENT_EXTENSIONS.includes(extension))
41
+ return { kind: 'file', extension };
42
+ if ((!file.type || file.type === 'application/octet-stream') &&
43
+ Object.values(IMAGE_EXTENSIONS).some((values) => values.includes(extension))) {
44
+ return { kind: 'image', extension };
45
+ }
46
+ return null;
47
+ }
48
+ export function validateAttachments(files) {
49
+ if (files.length > MAX_ATTACHMENTS)
50
+ return 'count';
51
+ if (files.some((file) => !attachmentFormat(file)))
52
+ return 'type';
53
+ if (files.some((file) => file.size === 0 || file.size > MAX_ATTACHMENT_BYTES))
54
+ return 'size';
55
+ if (files.reduce((sum, file) => sum + file.size, 0) > MAX_ATTACHMENTS_BYTES)
56
+ return 'total';
57
+ return null;
58
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@loicngr/kobo",
3
- "version": "1.11.40",
3
+ "version": "1.11.42",
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",
@@ -1,4 +1,4 @@
1
- import{S as e,ct as t,j as n,ot as r,p as i,ut as a,v as o}from"./reactivity.esm-bundler-DEJj9cFv.js";import{C as s,F as c,J as l,L as u,N as d,O as f,P as p,Y as m,_ as h,d as g,f as _,g as v,j as y,k as b,l as x,p as S,r as C,u as w,y as T,z as ee}from"./runtime-core.esm-bundler-CDnVUfeZ.js";import{J as E,b as D,ct as te,l as O,pt as k,t as ne,tt as A,w as re,y as ie}from"./QBtn-CZaBclNw.js";import{A as ae,Dt as oe,Lt as j,N as se,P as M,Pt as ce,Tt as N,Z as le,d as ue,m as de,p as fe,x as P}from"./index-9tiw3NjK.js";import{t as pe}from"./QSpinnerDots-DnCV3VzI.js";import{t as me}from"./QList-qupzuEqO.js";import{t as F}from"./QMarkupTable-B9QuKtK_.js";import{c as I,l as L}from"./QSelect-WspK3KC7.js";import{d as he}from"./permissionModes-DDWmNBW1.js";import{r as ge,t as R}from"./pending-diff-open-Dfpg92li.js";import{t as z}from"./documents-2C1hn1pw.js";import{a as B}from"./use-tours-DfE-kwrX.js";import{t as V}from"./QTooltip-7pw7iuV0.js";import{i as _e,n as ve,o as H,r as ye,t as U}from"./render-chat-markdown-DwUkPy-O.js";import{a as be,i as xe,n as Se,o as Ce,r as we,s as W}from"./WorkspacePage-BGCildyP.js";function Te(e,t){return s(`div`,e,[s(`table`,{class:`q-table`},t)])}var Ee={list:me,table:F},G=[`list`,`table`,`__qtable`],De=A({name:`QVirtualScroll`,props:{...L,type:{type:String,default:`list`,validator:e=>G.includes(e)},items:{type:Array,default:()=>[]},itemsFn:Function,itemsSize:Number,scrollTarget:j},setup(t,{slots:n,attrs:r}){let i,a=e(null),o=x(()=>t.itemsSize>=0&&t.itemsFn!==void 0?Number.parseInt(t.itemsSize,10):Array.isArray(t.items)?t.items.length:0),{virtualScrollSliceRange:c,localResetVirtualScroll:u,padVirtualScroll:f,onVirtualScrollEvt:m}=I({virtualScrollLength:o,getVirtualScrollTarget:S,getVirtualScrollEl:v}),h=x(()=>{if(o.value===0)return[];let e=(e,t)=>({index:c.value.from+t,item:e});return t.itemsFn===void 0?t.items.slice(c.value.from,c.value.to).map(e):t.itemsFn(c.value.from,c.value.to-c.value.from).map(e)}),g=x(()=>`q-virtual-scroll q-virtual-scroll`+(t.virtualScrollHorizontal?`--horizontal`:`--vertical`)+(t.scrollTarget===void 0?` scroll`:``)),_=x(()=>t.scrollTarget===void 0?{tabindex:0}:{});l(o,()=>{u()}),l(()=>t.scrollTarget,()=>{w(),C()});function v(){let e=a.value;return e===null?null:e.$el||e}function S(){return i}function C(){let e=v();e!==null&&(i=ce(e,t.scrollTarget),i.addEventListener(`scroll`,m,E.passive))}function w(){i!==void 0&&(i.removeEventListener(`scroll`,m,E.passive),i=void 0)}function T(){let e=f(t.type===`list`?`div`:`tbody`,h.value.map(n.default));return n.before!==void 0&&(e=n.before().concat(e)),D(n.after,e)}return u(),p(()=>{C()}),b(()=>{C()}),d(()=>{w()}),y(()=>{w()}),()=>{if(n.default===void 0){console.error(`QVirtualScroll: default scoped slot is required for rendering`);return}return t.type===`__qtable`?Te({ref:a,class:`q-table__middle `+g.value,tabindex:0},T()):s(Ee[t.type],{...r,ref:a,class:[r.class,g.value],..._.value},T)}}}),K=new Set([`setup`,`cleanup`,`archive`]);function Oe(e){switch(e.type){case`user`:return e.sender===`system-prompt`?`system-prompt`:K.has(e.sender)||ae(e.sender)?`script`:`user`;case`session`:return`session`;default:return`agent`}}function ke(e){let t=[],n=null,r=null;for(let i of e){let e=Oe(i),a=e===`session`||e===`system-prompt`,o=e===`script`&&i.type===`user`?`script:${i.sender}`:e===`user`&&i.type===`user`&&i.source?JSON.stringify([`mcp`,i.source.transport,i.source.clientName]):e;!n||r!==o||a?(n={speaker:e,ts:i.ts,items:[i]},r=o,t.push(n),a&&(n=null)):n.items.push(i)}return t}function Ae(e){switch(e.type){case`text`:return`text:${e.messageId}`;case`thinking`:return`thinking:${e.messageId}:${e.eventIds?.[0]??e.ts??``}`;case`tool`:return`tool:${e.toolCallId}`;case`session`:return`session:${e.kind}:${e.eventIds?.[0]??e.ts??``}`;case`user`:return`user:${e.eventIds?.[0]??`${e.sender}:${e.ts??``}`}`;case`error`:return`error:${e.eventIds?.[0]??`${e.category}:${e.ts??``}`}`}}function je(e){let t=e.items[0];return`${e.speaker}:${t?Ae(t):`empty`}`}function Me(e,t){let n=Math.min(t,e.length)-1;for(let t=n;t>=0;t--)if(e[t]?.speaker===`user`)return t;return-1}function Ne(e,t,n){return new Promise(r=>{let i=!1,a=null,o=null,s=e=>{i||(i=!0,a!==null&&clearTimeout(a),a=null,o?.(),r(e))};if(o=l(e,e=>{t(e)&&s(!0)},{immediate:!0}),i){o();return}a=setTimeout(()=>s(!1),n)})}var Pe={class:`agent-error-item`},Fe={class:`agent-error-item__body`},Ie={class:`agent-error-item__title`},Le={class:`agent-error-item__message`},Re=B(T({__name:`AgentErrorItem`,props:{item:{}},setup(e){let t=e,{te:n}=N(),r=x(()=>{let e=`agent.error.${t.item.category}`;return n(e)?e:`agent.error.other`});return(t,n)=>(u(),S(`div`,Pe,[h(O,{name:`error_outline`,size:`16px`,class:`agent-error-item__icon`}),w(`div`,Fe,[w(`div`,Ie,a(t.$t(r.value)),1),w(`div`,Le,a(e.item.message),1)])]))}}),[[`__scopeId`,`data-v-e98c3c02`]]),q={class:`text-caption text-kobo-3`},ze=T({__name:`SessionEventItem`,props:{item:{}},setup(e){let t=e,n=x(()=>{switch(t.item.kind){case`started`:return`session.started`;case`ended`:return W(t.item.detail);case`compacted`:return`session.compacted`;default:return`session.started`}});return(e,t)=>(u(),S(`span`,q,a(e.$t(n.value)),1))}});function J(e,t){if(t.length===0||e.length===0)return e;let n=[...t].sort((e,t)=>t.length-e.length),r=new DOMParser().parseFromString(`<div>${e}</div>`,`text/html`),i=r.body.firstChild;if(!i)return e;function a(e){if(e.nodeType===Node.TEXT_NODE){Be(e,n,r);return}if(e.nodeName===`A`)return;let t=Array.from(e.childNodes);for(let e of t)a(e)}return a(i),i.innerHTML}function Be(e,t,n){let r=e.textContent??``;if(!t.some(e=>r.includes(e)))return;let i=n.createDocumentFragment(),a=0;for(;a<r.length;){let e=Y(r,a,t);if(!e){i.appendChild(n.createTextNode(r.slice(a)));break}e.index>a&&i.appendChild(n.createTextNode(r.slice(a,e.index)));let o=n.createElement(`a`);o.className=`document-link`,o.setAttribute(`data-document-path`,e.path),o.setAttribute(`href`,`#`),o.textContent=e.path,i.appendChild(o),a=e.index+e.path.length}e.parentNode?.replaceChild(i,e)}function Y(e,t,n){let r=null;for(let i of n){let n=e.indexOf(i,t);n<0||(!r||n<r.index||n===r.index&&i.length>r.path.length)&&(r={index:n,path:i})}return r}var X=[`innerHTML`],Ve=B(T({__name:`TextMessageItem`,props:{item:{}},setup(e){let t=e,n=z(),r=fe(),i=ie(),{t:a}=N(),o=x(()=>{let e=r.selectedWorkspaceId;return e?n.documentsFor(e).map(e=>e.path):[]}),s=x(()=>{let e=t.item;if(e.streaming)return U(e.text);let n=o.value;return ye(`${e.messageId}|${n.join(`,`)}`,e.text,e=>{let t=J(H.parse(e,{async:!1,breaks:!0,gfm:!0}),n);return _e(t,{addAttr:[`data-document-path`]})})});function c(e){let t=e.target?.closest(`.document-link`);if(!t)return;e.preventDefault();let o=t.getAttribute(`data-document-path`),s=r.selectedWorkspaceId;o&&s&&n.openDocumentByPath(s,o).catch(e=>{console.error(`[TextMessageItem] openDocumentByPath failed:`,e),i.notify({type:`negative`,message:a(`documents.loadFailed`),position:`top`})})}return(t,n)=>(u(),S(`div`,{class:`markdown-message`,onClick:c},[w(`div`,{innerHTML:s.value},null,8,X),e.item.streaming?(u(),g(re,{key:0,size:`xs`,class:`q-ml-xs`})):_(``,!0)]))}}),[[`__scopeId`,`data-v-99371035`]]);function He(e,t){let n=e.split(`
1
+ import{S as e,ct as t,j as n,ot as r,p as i,ut as a,v as o}from"./reactivity.esm-bundler-DEJj9cFv.js";import{C as s,F as c,J as l,L as u,N as d,O as f,P as p,Y as m,_ as h,d as g,f as _,g as v,j as y,k as b,l as x,p as S,r as C,u as w,y as T,z as ee}from"./runtime-core.esm-bundler-CDnVUfeZ.js";import{J as E,b as D,ct as te,l as O,pt as k,t as ne,tt as A,w as re,y as ie}from"./QBtn-CZaBclNw.js";import{A as ae,Dt as oe,Lt as j,N as se,P as M,Pt as ce,Tt as N,Z as le,d as ue,m as de,p as fe,x as P}from"./index-DtsrXdzx.js";import{t as pe}from"./QSpinnerDots-DnCV3VzI.js";import{t as me}from"./QList-qupzuEqO.js";import{t as F}from"./QMarkupTable-B9QuKtK_.js";import{c as I,l as L}from"./QSelect-DJL6yy1E.js";import{d as he}from"./permissionModes-CESk18Vk.js";import{r as ge,t as R}from"./pending-diff-open-CxtcrwVN.js";import{t as z}from"./documents-D88lOIxR.js";import{a as B}from"./use-tours-CS5oBFk6.js";import{t as V}from"./QTooltip-WIoDeLoZ.js";import{i as _e,n as ve,o as H,r as ye,t as U}from"./render-chat-markdown-K3tUW_1p.js";import{a as be,i as xe,n as Se,o as Ce,r as we,s as W}from"./WorkspacePage-DE6-wlf7.js";function Te(e,t){return s(`div`,e,[s(`table`,{class:`q-table`},t)])}var Ee={list:me,table:F},G=[`list`,`table`,`__qtable`],De=A({name:`QVirtualScroll`,props:{...L,type:{type:String,default:`list`,validator:e=>G.includes(e)},items:{type:Array,default:()=>[]},itemsFn:Function,itemsSize:Number,scrollTarget:j},setup(t,{slots:n,attrs:r}){let i,a=e(null),o=x(()=>t.itemsSize>=0&&t.itemsFn!==void 0?Number.parseInt(t.itemsSize,10):Array.isArray(t.items)?t.items.length:0),{virtualScrollSliceRange:c,localResetVirtualScroll:u,padVirtualScroll:f,onVirtualScrollEvt:m}=I({virtualScrollLength:o,getVirtualScrollTarget:S,getVirtualScrollEl:v}),h=x(()=>{if(o.value===0)return[];let e=(e,t)=>({index:c.value.from+t,item:e});return t.itemsFn===void 0?t.items.slice(c.value.from,c.value.to).map(e):t.itemsFn(c.value.from,c.value.to-c.value.from).map(e)}),g=x(()=>`q-virtual-scroll q-virtual-scroll`+(t.virtualScrollHorizontal?`--horizontal`:`--vertical`)+(t.scrollTarget===void 0?` scroll`:``)),_=x(()=>t.scrollTarget===void 0?{tabindex:0}:{});l(o,()=>{u()}),l(()=>t.scrollTarget,()=>{w(),C()});function v(){let e=a.value;return e===null?null:e.$el||e}function S(){return i}function C(){let e=v();e!==null&&(i=ce(e,t.scrollTarget),i.addEventListener(`scroll`,m,E.passive))}function w(){i!==void 0&&(i.removeEventListener(`scroll`,m,E.passive),i=void 0)}function T(){let e=f(t.type===`list`?`div`:`tbody`,h.value.map(n.default));return n.before!==void 0&&(e=n.before().concat(e)),D(n.after,e)}return u(),p(()=>{C()}),b(()=>{C()}),d(()=>{w()}),y(()=>{w()}),()=>{if(n.default===void 0){console.error(`QVirtualScroll: default scoped slot is required for rendering`);return}return t.type===`__qtable`?Te({ref:a,class:`q-table__middle `+g.value,tabindex:0},T()):s(Ee[t.type],{...r,ref:a,class:[r.class,g.value],..._.value},T)}}}),K=new Set([`setup`,`cleanup`,`archive`]);function Oe(e){switch(e.type){case`user`:return e.sender===`system-prompt`?`system-prompt`:K.has(e.sender)||ae(e.sender)?`script`:`user`;case`session`:return`session`;default:return`agent`}}function ke(e){let t=[],n=null,r=null;for(let i of e){let e=Oe(i),a=e===`session`||e===`system-prompt`,o=e===`script`&&i.type===`user`?`script:${i.sender}`:e===`user`&&i.type===`user`&&i.source?JSON.stringify([`mcp`,i.source.transport,i.source.clientName]):e;!n||r!==o||a?(n={speaker:e,ts:i.ts,items:[i]},r=o,t.push(n),a&&(n=null)):n.items.push(i)}return t}function Ae(e){switch(e.type){case`text`:return`text:${e.messageId}`;case`thinking`:return`thinking:${e.messageId}:${e.eventIds?.[0]??e.ts??``}`;case`tool`:return`tool:${e.toolCallId}`;case`session`:return`session:${e.kind}:${e.eventIds?.[0]??e.ts??``}`;case`user`:return`user:${e.eventIds?.[0]??`${e.sender}:${e.ts??``}`}`;case`error`:return`error:${e.eventIds?.[0]??`${e.category}:${e.ts??``}`}`}}function je(e){let t=e.items[0];return`${e.speaker}:${t?Ae(t):`empty`}`}function Me(e,t){let n=Math.min(t,e.length)-1;for(let t=n;t>=0;t--)if(e[t]?.speaker===`user`)return t;return-1}function Ne(e,t,n){return new Promise(r=>{let i=!1,a=null,o=null,s=e=>{i||(i=!0,a!==null&&clearTimeout(a),a=null,o?.(),r(e))};if(o=l(e,e=>{t(e)&&s(!0)},{immediate:!0}),i){o();return}a=setTimeout(()=>s(!1),n)})}var Pe={class:`agent-error-item`},Fe={class:`agent-error-item__body`},Ie={class:`agent-error-item__title`},Le={class:`agent-error-item__message`},Re=B(T({__name:`AgentErrorItem`,props:{item:{}},setup(e){let t=e,{te:n}=N(),r=x(()=>{let e=`agent.error.${t.item.category}`;return n(e)?e:`agent.error.other`});return(t,n)=>(u(),S(`div`,Pe,[h(O,{name:`error_outline`,size:`16px`,class:`agent-error-item__icon`}),w(`div`,Fe,[w(`div`,Ie,a(t.$t(r.value)),1),w(`div`,Le,a(e.item.message),1)])]))}}),[[`__scopeId`,`data-v-e98c3c02`]]),q={class:`text-caption text-kobo-3`},ze=T({__name:`SessionEventItem`,props:{item:{}},setup(e){let t=e,n=x(()=>{switch(t.item.kind){case`started`:return`session.started`;case`ended`:return W(t.item.detail);case`compacted`:return`session.compacted`;default:return`session.started`}});return(e,t)=>(u(),S(`span`,q,a(e.$t(n.value)),1))}});function J(e,t){if(t.length===0||e.length===0)return e;let n=[...t].sort((e,t)=>t.length-e.length),r=new DOMParser().parseFromString(`<div>${e}</div>`,`text/html`),i=r.body.firstChild;if(!i)return e;function a(e){if(e.nodeType===Node.TEXT_NODE){Be(e,n,r);return}if(e.nodeName===`A`)return;let t=Array.from(e.childNodes);for(let e of t)a(e)}return a(i),i.innerHTML}function Be(e,t,n){let r=e.textContent??``;if(!t.some(e=>r.includes(e)))return;let i=n.createDocumentFragment(),a=0;for(;a<r.length;){let e=Y(r,a,t);if(!e){i.appendChild(n.createTextNode(r.slice(a)));break}e.index>a&&i.appendChild(n.createTextNode(r.slice(a,e.index)));let o=n.createElement(`a`);o.className=`document-link`,o.setAttribute(`data-document-path`,e.path),o.setAttribute(`href`,`#`),o.textContent=e.path,i.appendChild(o),a=e.index+e.path.length}e.parentNode?.replaceChild(i,e)}function Y(e,t,n){let r=null;for(let i of n){let n=e.indexOf(i,t);n<0||(!r||n<r.index||n===r.index&&i.length>r.path.length)&&(r={index:n,path:i})}return r}var X=[`innerHTML`],Ve=B(T({__name:`TextMessageItem`,props:{item:{}},setup(e){let t=e,n=z(),r=fe(),i=ie(),{t:a}=N(),o=x(()=>{let e=r.selectedWorkspaceId;return e?n.documentsFor(e).map(e=>e.path):[]}),s=x(()=>{let e=t.item;if(e.streaming)return U(e.text);let n=o.value;return ye(`${e.messageId}|${n.join(`,`)}`,e.text,e=>{let t=J(H.parse(e,{async:!1,breaks:!0,gfm:!0}),n);return _e(t,{addAttr:[`data-document-path`]})})});function c(e){let t=e.target?.closest(`.document-link`);if(!t)return;e.preventDefault();let o=t.getAttribute(`data-document-path`),s=r.selectedWorkspaceId;o&&s&&n.openDocumentByPath(s,o).catch(e=>{console.error(`[TextMessageItem] openDocumentByPath failed:`,e),i.notify({type:`negative`,message:a(`documents.loadFailed`),position:`top`})})}return(t,n)=>(u(),S(`div`,{class:`markdown-message`,onClick:c},[w(`div`,{innerHTML:s.value},null,8,X),e.item.streaming?(u(),g(re,{key:0,size:`xs`,class:`q-ml-xs`})):_(``,!0)]))}}),[[`__scopeId`,`data-v-99371035`]]);function He(e,t){let n=e.split(`
2
2
  `),r=t.split(`
3
3
  `),i=0;for(;i<n.length&&i<r.length&&n[i]===r[i];)i++;let a=n.length,o=r.length;for(;a>i&&o>i&&n[a-1]===r[o-1];)a--,o--;let s=n.slice(0,i).map(e=>({type:`context`,content:e})),c=n.slice(a).map(e=>({type:`context`,content:e})),l=n.slice(i,a),u=r.slice(i,o);return l.length>800||u.length>800?[...s,...l.map(e=>({type:`del`,content:e})),...u.map(e=>({type:`add`,content:e})),...c]:[...s,...Z(l,u),...c]}function Z(e,t){let n=e.length,r=t.length;if(n===0&&r===0)return[];if(n===0)return t.map(e=>({type:`add`,content:e}));if(r===0)return e.map(e=>({type:`del`,content:e}));let i=r+1,a=new Int32Array((n+1)*i);for(let o=n-1;o>=0;o--)for(let n=r-1;n>=0;n--)a[o*i+n]=e[o]===t[n]?a[(o+1)*i+(n+1)]+1:Math.max(a[(o+1)*i+n],a[o*i+(n+1)]);let o=[],s=0,c=0;for(;s<n&&c<r;)e[s]===t[c]?(o.push({type:`context`,content:e[s]}),s++,c++):a[(s+1)*i+c]>=a[s*i+(c+1)]?(o.push({type:`del`,content:e[s]}),s++):(o.push({type:`add`,content:t[c]}),c++);for(;s<n;)o.push({type:`del`,content:e[s++]});for(;c<r;)o.push({type:`add`,content:t[c++]});return o}function Q(e){let t=e.split(`
4
4
  `),n=[],r=!1;for(let e of t){if(e.startsWith(`@@`)){r=!0;continue}(r||!e.startsWith(`+++`)&&!e.startsWith(`---`))&&(e.startsWith(`+`)?n.push({type:`add`,content:e.slice(1)}):e.startsWith(`-`)?n.push({type:`del`,content:e.slice(1)}):e.startsWith(` `)?n.push({type:`context`,content:e.slice(1)}):e.length>0&&n.push({type:`context`,content:e}))}return n}function Ue(e,t){if(!t||typeof t!=`object`)return null;let n=t;if(e===`Edit`){let e=n.file_path;if(!e)return null;let t=n.old_string??``,r=n.new_string??``,i=typeof n.diff==`string`?n.diff:``;if(!t&&!r&&i.length>0){let t=Q(i);return{toolName:`Edit`,filePath:e,additions:t.filter(e=>e.type===`add`).length,deletions:t.filter(e=>e.type===`del`).length,diffLines:t}}return{toolName:`Edit`,filePath:e,oldString:t,newString:r,replaceAll:n.replace_all??!1,additions:r?r.split(`
@@ -1 +1 @@
1
- import{j as e,ut as t,v as n}from"./reactivity.esm-bundler-DEJj9cFv.js";import{L as r,P as i,Y as a,_ as o,d as s,f as c,p as l,r as u,u as d,y as f,z as p}from"./runtime-core.esm-bundler-CDnVUfeZ.js";import{t as m}from"./QBtn-CZaBclNw.js";import{Ut as h,Vt as g,n as _}from"./index-9tiw3NjK.js";import{a as v,i as y,n as b}from"./use-tours-DfE-kwrX.js";import{n as x}from"./render-chat-markdown-DwUkPy-O.js";import{t as S}from"./QChip-owciRuZu.js";import{t as C}from"./QPage-DaX9sMQC.js";import{t as w}from"./DrawerToggleButton-CTOc3yA0.js";import{t as T}from"./TourReplayButton-C5RF0lOK.js";var E={class:`row items-center q-mb-md`},D={class:`text-h6 q-ml-sm`},O={key:0,class:`text-kobo-3 text-center q-pa-lg`},k={key:1,class:`text-negative text-center q-pa-lg`},A={key:2,class:`text-kobo-3 text-center q-pa-lg`},j={key:3,class:`column q-gutter-md`,"data-tour":`changelog-list`},M={key:0,class:`text-caption text-kobo-3`},N={class:`row items-center q-mb-sm`},P={class:`text-subtitle1 text-kobo-2`,style:{"font-family":`var(--kobo-font-mono, monospace)`}},F=[`innerHTML`],I=v(f({__name:`ChangelogPage`,setup(f){let v=_(),{scheduleAutoRun:I}=b(),L=n([]),R=n(``),z=n(!1),B=n(null);function V(e){return x(e)}async function H(){z.value=!0,B.value=null;try{let e=await fetch(`/api/changelog`);if(!e.ok)throw Error(`HTTP ${e.status}`);let t=await e.json();R.value=t.currentVersion??``,L.value=t.versions??[]}catch(e){B.value=e instanceof Error?e.message:String(e)}finally{z.value=!1}}return i(()=>{H(),I(`changelog`)}),(n,i)=>(r(),s(C,{class:`q-pa-md`,style:{"max-width":`900px`,margin:`0 auto`}},{default:a(()=>[d(`div`,E,[o(w,{class:`q-mr-xs`}),o(m,{flat:``,dense:``,round:``,icon:`arrow_back`,onClick:i[0]||=t=>e(v).back()}),d(`div`,D,t(n.$t(`changelog.title`)),1),o(y),o(m,{flat:``,dense:``,icon:`refresh`,loading:z.value,label:n.$t(`common.refresh`),onClick:H},null,8,[`loading`,`label`]),o(T,{"tour-id":`changelog`,class:`q-ml-xs`})]),z.value&&L.value.length===0?(r(),l(`div`,O,t(n.$t(`common.loading`)),1)):B.value?(r(),l(`div`,k,t(B.value),1)):L.value.length===0?(r(),l(`div`,A,t(n.$t(`changelog.empty`)),1)):(r(),l(`div`,j,[R.value?(r(),l(`div`,M,t(n.$t(`changelog.currentVersion`,{version:R.value})),1)):c(``,!0),(r(!0),l(u,null,p(L.value,e=>(r(),s(g,{key:e.version,dark:``,flat:``,bordered:``},{default:a(()=>[o(h,null,{default:a(()=>[d(`div`,N,[d(`div`,P,` v`+t(e.version),1),e.version===R.value?(r(),s(S,{key:0,dense:``,size:`sm`,color:`primary`,"text-color":`kobo-1`,label:n.$t(`changelog.current`),class:`q-ml-sm`},null,8,[`label`])):c(``,!0)]),d(`div`,{class:`changelog-notes`,innerHTML:V(e.notes)},null,8,F)]),_:2},1024)]),_:2},1024))),128))]))]),_:1}))}}),[[`__scopeId`,`data-v-6137cb7c`]]);export{I as default};
1
+ import{j as e,ut as t,v as n}from"./reactivity.esm-bundler-DEJj9cFv.js";import{L as r,P as i,Y as a,_ as o,d as s,f as c,p as l,r as u,u as d,y as f,z as p}from"./runtime-core.esm-bundler-CDnVUfeZ.js";import{t as m}from"./QBtn-CZaBclNw.js";import{Ut as h,Vt as g,n as _}from"./index-DtsrXdzx.js";import{a as v,i as y,n as b}from"./use-tours-CS5oBFk6.js";import{n as x}from"./render-chat-markdown-K3tUW_1p.js";import{t as S}from"./QChip-owciRuZu.js";import{t as C}from"./QPage-DaX9sMQC.js";import{t as w}from"./DrawerToggleButton-CBZpPa30.js";import{t as T}from"./TourReplayButton-U3mwT0fV.js";var E={class:`row items-center q-mb-md`},D={class:`text-h6 q-ml-sm`},O={key:0,class:`text-kobo-3 text-center q-pa-lg`},k={key:1,class:`text-negative text-center q-pa-lg`},A={key:2,class:`text-kobo-3 text-center q-pa-lg`},j={key:3,class:`column q-gutter-md`,"data-tour":`changelog-list`},M={key:0,class:`text-caption text-kobo-3`},N={class:`row items-center q-mb-sm`},P={class:`text-subtitle1 text-kobo-2`,style:{"font-family":`var(--kobo-font-mono, monospace)`}},F=[`innerHTML`],I=v(f({__name:`ChangelogPage`,setup(f){let v=_(),{scheduleAutoRun:I}=b(),L=n([]),R=n(``),z=n(!1),B=n(null);function V(e){return x(e)}async function H(){z.value=!0,B.value=null;try{let e=await fetch(`/api/changelog`);if(!e.ok)throw Error(`HTTP ${e.status}`);let t=await e.json();R.value=t.currentVersion??``,L.value=t.versions??[]}catch(e){B.value=e instanceof Error?e.message:String(e)}finally{z.value=!1}}return i(()=>{H(),I(`changelog`)}),(n,i)=>(r(),s(C,{class:`q-pa-md`,style:{"max-width":`900px`,margin:`0 auto`}},{default:a(()=>[d(`div`,E,[o(w,{class:`q-mr-xs`}),o(m,{flat:``,dense:``,round:``,icon:`arrow_back`,onClick:i[0]||=t=>e(v).back()}),d(`div`,D,t(n.$t(`changelog.title`)),1),o(y),o(m,{flat:``,dense:``,icon:`refresh`,loading:z.value,label:n.$t(`common.refresh`),onClick:H},null,8,[`loading`,`label`]),o(T,{"tour-id":`changelog`,class:`q-ml-xs`})]),z.value&&L.value.length===0?(r(),l(`div`,O,t(n.$t(`common.loading`)),1)):B.value?(r(),l(`div`,k,t(B.value),1)):L.value.length===0?(r(),l(`div`,A,t(n.$t(`changelog.empty`)),1)):(r(),l(`div`,j,[R.value?(r(),l(`div`,M,t(n.$t(`changelog.currentVersion`,{version:R.value})),1)):c(``,!0),(r(!0),l(u,null,p(L.value,e=>(r(),s(g,{key:e.version,dark:``,flat:``,bordered:``},{default:a(()=>[o(h,null,{default:a(()=>[d(`div`,N,[d(`div`,P,` v`+t(e.version),1),e.version===R.value?(r(),s(S,{key:0,dense:``,size:`sm`,color:`primary`,"text-color":`kobo-1`,label:n.$t(`changelog.current`),class:`q-ml-sm`},null,8,[`label`])):c(``,!0)]),d(`div`,{class:`changelog-notes`,innerHTML:V(e.notes)},null,8,F)]),_:2},1024)]),_:2},1024))),128))]))]),_:1}))}}),[[`__scopeId`,`data-v-6137cb7c`]]);export{I as default};
@@ -1,2 +1,2 @@
1
- import{ot as e,ut as t,v as n}from"./reactivity.esm-bundler-DEJj9cFv.js";import{J as r,L as i,P as a,Y as ee,_ as o,d as te,f as s,g as ne,l as c,p as l,r as u,u as d,y as f,z as re}from"./runtime-core.esm-bundler-CDnVUfeZ.js";import{t as p}from"./QBtn-CZaBclNw.js";import{Tt as ie,n as ae,p as oe}from"./index-9tiw3NjK.js";import{t as se}from"./QMarkupTable-B9QuKtK_.js";import{a as m,i as h}from"./use-tours-DfE-kwrX.js";var g=[`workspace`,`engine`,`model`,`status`,`branch`,`tasks_done`,`tasks_total`,`commits`,`files_changed`,`insertions`,`deletions`,`agent_messages`,`user_messages`,`injected_prompts`,`questions`,`tool_calls`,`errors`,`input_tokens`,`output_tokens`,`sessions`,`duration_seconds`];function _(e){if(e==null)return``;let t=String(e);return/[",\r\n]/.test(t)?`"${t.replace(/"/g,`""`)}"`:t}function v(e){let t=e.map(e=>{let t=e.gitStats,n=e.activity;return[e.workspace.name,e.workspace.engine,e.workspace.model,e.workspace.status,e.workspace.workingBranch,e.tasks.done,e.tasks.total,t?.commitCount??null,t?.filesChanged??null,t?.insertions??null,t?.deletions??null,n.agentMessages,n.userMessages,n.injectedPrompts,n.questions,n.toolCalls,n.errors,n.inputTokens,n.outputTokens,n.sessions,Math.round(n.durationMs/1e3)].map(_).join(`,`)});return`${[g.join(`,`),...t].join(`\r
1
+ import{ot as e,ut as t,v as n}from"./reactivity.esm-bundler-DEJj9cFv.js";import{J as r,L as i,P as a,Y as ee,_ as o,d as te,f as s,g as ne,l as c,p as l,r as u,u as d,y as f,z as re}from"./runtime-core.esm-bundler-CDnVUfeZ.js";import{t as p}from"./QBtn-CZaBclNw.js";import{Tt as ie,n as ae,p as oe}from"./index-DtsrXdzx.js";import{t as se}from"./QMarkupTable-B9QuKtK_.js";import{a as m,i as h}from"./use-tours-CS5oBFk6.js";var g=[`workspace`,`engine`,`model`,`status`,`branch`,`tasks_done`,`tasks_total`,`commits`,`files_changed`,`insertions`,`deletions`,`agent_messages`,`user_messages`,`injected_prompts`,`questions`,`tool_calls`,`errors`,`input_tokens`,`output_tokens`,`sessions`,`duration_seconds`];function _(e){if(e==null)return``;let t=String(e);return/[",\r\n]/.test(t)?`"${t.replace(/"/g,`""`)}"`:t}function v(e){let t=e.map(e=>{let t=e.gitStats,n=e.activity;return[e.workspace.name,e.workspace.engine,e.workspace.model,e.workspace.status,e.workspace.workingBranch,e.tasks.done,e.tasks.total,t?.commitCount??null,t?.filesChanged??null,t?.insertions??null,t?.deletions??null,n.agentMessages,n.userMessages,n.injectedPrompts,n.questions,n.toolCalls,n.errors,n.inputTokens,n.outputTokens,n.sessions,Math.round(n.durationMs/1e3)].map(_).join(`,`)});return`${[g.join(`,`),...t].join(`\r
2
2
  `)}`}var y={key:0,class:`comparison-panel q-pa-md`},b={class:`row items-center q-mb-sm`},x={class:`text-subtitle2`},S={class:`text-caption text-kobo-3 q-mb-sm`},C={class:`text-left`},w={class:`text-left`},T={class:`text-right`},E={class:`text-right`},D={class:`text-right`},O={class:`text-right`},k={class:`text-right`},A={class:`text-right`},j={class:`text-right`},M={class:`text-right`},N={class:`text-right`},P={class:`text-right`},F={class:`text-right`},I={class:`text-caption text-kobo-3 comparison-mono`},L={key:0,class:`text-caption text-kobo-3`},R={class:`text-right comparison-mono`},z={class:`text-right`},B={class:`text-right`},V={class:`text-right`},ce={key:0,class:`comparison-mono`},H={class:`text-kobo-success`},U={class:`text-kobo-danger q-ml-xs`},W={key:1,class:`text-kobo-3`},G={class:`text-right comparison-mono`},le=[`title`],ue={class:`text-right`},de={class:`text-right`},fe={class:`text-right comparison-mono`},pe=[`title`],me={class:`text-right`},he={class:`text-right comparison-mono`},ge={class:`text-right`},_e={key:1,class:`text-caption text-kobo-3`},ve={key:0,class:`text-caption text-kobo-warning q-mt-sm`},K=m(f({__name:`ComparisonPanel`,props:{workspaceId:{}},setup(f){let m=f;function g(e){return e<1e3?String(e):e<1e6?`${(e/1e3).toFixed(+(e<1e4))}k`:`${(e/1e6).toFixed(1)}M`}function _(e){if(e<=0)return`-`;let t=Math.round(e/1e3);if(t<60)return q(`reliability.durationSeconds`,{s:t});let n=Math.floor(t/60);return n<60?q(`reliability.durationMinutes`,{m:n,s:t%60}):q(`reliability.durationHours`,{h:Math.floor(n/60),m:n%60})}let K=ae(),ye=oe(),{t:q}=ie(),J=n([]),Y=n(!1),be={created:`workspaceStatus.created`,extracting:`workspaceStatus.extracting`,brainstorming:`workspaceStatus.brainstorming`,executing:`workspaceStatus.executing`,compacting:`workspaceStatus.compacting`,"awaiting-user":`workspaceStatus.awaitingUser`,completed:`workspaceStatus.completed`,idle:`workspaceStatus.idle`,error:`workspaceStatus.error`,quota:`workspaceStatus.quota`,retrying:`workspaceStatus.retrying`},X=n({});function xe(e){return X.value[e]??e}async function Se(){try{let e=await fetch(`/api/engines`);if(!e.ok)return;let t=await e.json();X.value=Object.fromEntries(t.map(e=>[e.id,e.displayName]))}catch{}}function Z(e){let t=be[e];return t?q(t):e}let Ce=c(()=>m.workspaceId);function Q(e){return e.workspace.id===Ce.value}function we(e){ye.selectWorkspace(e),K.push({name:`workspace`,params:{id:e}})}function Te(){let e=v(J.value),t=new Blob([e],{type:`text/csv;charset=utf-8`}),n=URL.createObjectURL(t),r=document.createElement(`a`);r.href=n,r.download=`kobo-comparison-${J.value[0]?.workspace.comparisonId??`comparison`}.csv`,r.click(),URL.revokeObjectURL(n)}async function $(){Y.value=!0;try{let e=await fetch(`/api/workspaces/${m.workspaceId}/comparison`);if(!e.ok){J.value=[];return}let t=await e.json();J.value=t.members??[]}catch{J.value=[]}finally{Y.value=!1}}return a(()=>{Se(),$()}),r(()=>m.workspaceId,$),(n,r)=>J.value.length>0?(i(),l(`div`,y,[d(`div`,b,[d(`div`,x,t(n.$t(`comparison.title`)),1),o(h),o(p,{flat:``,dense:``,round:``,size:`sm`,icon:`download`,title:n.$t(`comparison.exportCsv`),onClick:Te},null,8,[`title`]),o(p,{flat:``,dense:``,round:``,size:`sm`,icon:`refresh`,loading:Y.value,onClick:$},null,8,[`loading`])]),d(`div`,S,t(n.$t(`comparison.hint`)),1),o(se,{flat:``,dense:``,dark:``,class:`comparison-table`},{default:ee(()=>[d(`thead`,null,[d(`tr`,null,[d(`th`,C,t(n.$t(`comparison.engine`)),1),d(`th`,w,t(n.$t(`comparison.status`)),1),d(`th`,T,t(n.$t(`comparison.tasks`)),1),d(`th`,E,t(n.$t(`comparison.commits`)),1),d(`th`,D,t(n.$t(`comparison.files`)),1),d(`th`,O,t(n.$t(`comparison.diff`)),1),d(`th`,k,t(n.$t(`comparison.messages`)),1),d(`th`,A,t(n.$t(`comparison.questions`)),1),d(`th`,j,t(n.$t(`comparison.tools`)),1),d(`th`,M,t(n.$t(`comparison.errors`)),1),d(`th`,N,t(n.$t(`comparison.tokens`)),1),d(`th`,P,t(n.$t(`comparison.sessions`)),1),d(`th`,F,t(n.$t(`comparison.duration`)),1),r[0]||=d(`th`,null,null,-1)])]),d(`tbody`,null,[(i(!0),l(u,null,re(J.value,r=>(i(),l(`tr`,{key:r.workspace.id,class:e({"comparison-current":Q(r)})},[d(`td`,null,[d(`div`,null,t(xe(r.workspace.engine)),1),d(`div`,I,t(r.workspace.model),1)]),d(`td`,null,[ne(t(Z(r.workspace.status))+` `,1),r.workspace.archivedAt?(i(),l(`span`,L,` · `+t(n.$t(`comparison.archived`)),1)):s(``,!0)]),d(`td`,R,t(r.tasks.done)+`/`+t(r.tasks.total),1),d(`td`,z,t(r.gitStats?.commitCount??`-`),1),d(`td`,B,t(r.gitStats?.filesChanged??`-`),1),d(`td`,V,[r.gitStats?(i(),l(`span`,ce,[d(`span`,H,`+`+t(r.gitStats.insertions),1),d(`span`,U,`-`+t(r.gitStats.deletions),1)])):(i(),l(`span`,W,`-`))]),d(`td`,G,[d(`span`,{title:n.$t(`comparison.messagesTitle`)},t(r.activity.agentMessages)+` / `+t(r.activity.userMessages),9,le)]),d(`td`,ue,t(r.activity.questions),1),d(`td`,de,t(r.activity.toolCalls),1),d(`td`,{class:e([`text-right`,r.activity.errors>0?`text-kobo-danger`:``])},t(r.activity.errors),3),d(`td`,fe,[d(`span`,{title:n.$t(`comparison.tokensTitle`)},t(g(r.activity.inputTokens))+` / `+t(g(r.activity.outputTokens)),9,pe)]),d(`td`,me,t(r.activity.sessions),1),d(`td`,he,t(_(r.activity.durationMs)),1),d(`td`,ge,[Q(r)?(i(),l(`span`,_e,t(n.$t(`comparison.current`)),1)):(i(),te(p,{key:0,flat:``,dense:``,"no-caps":``,size:`sm`,label:n.$t(`comparison.open`),onClick:e=>we(r.workspace.id)},null,8,[`label`,`onClick`]))])],2))),128))])]),_:1}),J.value.length<2?(i(),l(`div`,ve,t(n.$t(`comparison.incomplete`)),1)):s(``,!0)])):s(``,!0)}}),[[`__scopeId`,`data-v-e1a1dbcc`]]);export{K as default};