@loicngr/kobo 1.11.13 → 1.11.14

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 (64) hide show
  1. package/CHANGELOG.md +4 -0
  2. package/dist/mcp-server/kobo-tasks-handlers.js +26 -3
  3. package/dist/server/index.js +5 -7
  4. package/dist/server/routes/export.js +2 -1
  5. package/dist/server/routes/workspaces.js +15 -0
  6. package/dist/server/services/agent/engines/claude-code/engine.js +9 -7
  7. package/dist/server/services/agent/engines/claude-code/event-mapper.js +3 -1
  8. package/dist/server/services/agent/orchestrator.js +34 -8
  9. package/dist/server/services/cron-service.js +2 -1
  10. package/dist/server/services/dev-server-service.js +13 -1
  11. package/dist/server/services/wakeup-service.js +14 -12
  12. package/dist/server/utils/git-ops.js +3 -2
  13. package/dist/server/utils/safe-path.js +34 -0
  14. package/package.json +3 -3
  15. package/src/client/dist/pwa/assets/{ActivityFeed-Ds8cAMNc.js → ActivityFeed-DqDJDX2v.js} +1 -1
  16. package/src/client/dist/pwa/assets/{ChangelogPage-DO6DNO4Q.js → ChangelogPage-Dw2LbIK2.js} +1 -1
  17. package/src/client/dist/pwa/assets/{ClosePopup-DuAqn8Pb.js → ClosePopup-eKuUr7CY.js} +1 -1
  18. package/src/client/dist/pwa/assets/{CreatePage-BnMSo_79.js → CreatePage--4KDhiYt.js} +1 -1
  19. package/src/client/dist/pwa/assets/{DiffViewer-CMnFGReI.js → DiffViewer-Bl9D1N1M.js} +3 -3
  20. package/src/client/dist/pwa/assets/{HealthPage-BRAxQKSP.js → HealthPage-CaOdjuVx.js} +1 -1
  21. package/src/client/dist/pwa/assets/{MainLayout-BN1ug95K.js → MainLayout-DP9shoO1.js} +2 -2
  22. package/src/client/dist/pwa/assets/{QBtnToggle-DsCDNQzX.js → QBtnToggle-BwPZ0Smh.js} +1 -1
  23. package/src/client/dist/pwa/assets/{QChip-DWGDTZnX.js → QChip-CY0_8v_e.js} +1 -1
  24. package/src/client/dist/pwa/assets/{QSelect-8Ip46vhk.js → QSelect-xHphd7hW.js} +1 -1
  25. package/src/client/dist/pwa/assets/{QTooltip-Bz4g3bmr.js → QTooltip-DyS20jDT.js} +1 -1
  26. package/src/client/dist/pwa/assets/{SearchPage-BD53VJEr.js → SearchPage-CggXs3dH.js} +1 -1
  27. package/src/client/dist/pwa/assets/{SettingsPage-3rYdJZga.js → SettingsPage-8AWL_4DM.js} +1 -1
  28. package/src/client/dist/pwa/assets/{TouchPan-BKu8V4eN.js → TouchPan-Cd9KTb6-.js} +1 -1
  29. package/src/client/dist/pwa/assets/{WorkspacePage-CbC_t5Rx.js → WorkspacePage-CJKSrSEC.js} +3 -3
  30. package/src/client/dist/pwa/assets/{cssMode-vaMVESGy.js → cssMode-BsN8TWME.js} +1 -1
  31. package/src/client/dist/pwa/assets/{documents-CyKQ9lT6.js → documents-DJy1tgPd.js} +1 -1
  32. package/src/client/dist/pwa/assets/{editor.api-CH3dhpKq.js → editor.api-Dj6TIm_z.js} +1 -1
  33. package/src/client/dist/pwa/assets/{expand-template-DQQQftsk.js → expand-template-By-8Db7_.js} +1 -1
  34. package/src/client/dist/pwa/assets/{freemarker2-deLpERsj.js → freemarker2-DC0xjWx5.js} +1 -1
  35. package/src/client/dist/pwa/assets/{handlebars-DdjS05Tc.js → handlebars-0Uz9nw5d.js} +1 -1
  36. package/src/client/dist/pwa/assets/{html-pK1ZEFvP.js → html-BYPeU1Vb.js} +1 -1
  37. package/src/client/dist/pwa/assets/{htmlMode-DJFDI9Ru.js → htmlMode-B0UND_vz.js} +1 -1
  38. package/src/client/dist/pwa/assets/i18n-DPDR4mQI.js +1 -0
  39. package/src/client/dist/pwa/assets/{index-B_OmZ65F.js → index-CtK8ORpZ.js} +3 -3
  40. package/src/client/dist/pwa/assets/{javascript-DbuFfxco.js → javascript-B8PZW8pe.js} +1 -1
  41. package/src/client/dist/pwa/assets/{jsonMode-B0Fu8Dcc.js → jsonMode-BixnmFse.js} +1 -1
  42. package/src/client/dist/pwa/assets/{kobo-commands-rb43ZeWG.js → kobo-commands-CX8pforo.js} +1 -1
  43. package/src/client/dist/pwa/assets/{layout-tAFrvQ5V.js → layout-BAE6NR2A.js} +1 -1
  44. package/src/client/dist/pwa/assets/{liquid-DqDIKOCy.js → liquid-BH16_Zn1.js} +1 -1
  45. package/src/client/dist/pwa/assets/{lspLanguageFeatures-CVcEibfJ.js → lspLanguageFeatures-YMzTIuK0.js} +1 -1
  46. package/src/client/dist/pwa/assets/{mdx-Bs4CovhI.js → mdx-CrrWJ7fg.js} +1 -1
  47. package/src/client/dist/pwa/assets/{network-auth-DYrrh7Zr.js → network-auth-NMnCVoaZ.js} +1 -1
  48. package/src/client/dist/pwa/assets/{pending-diff-open-D1IYH8Pd.js → pending-diff-open-DHlXLorX.js} +1 -1
  49. package/src/client/dist/pwa/assets/{project-color-DO6uj29M.js → project-color-BJrCEQ0A.js} +1 -1
  50. package/src/client/dist/pwa/assets/{python-BJoP0uN-.js → python-fk3K3s3o.js} +1 -1
  51. package/src/client/dist/pwa/assets/{razor-cwD-YBcr.js → razor-Dg1yHOal.js} +1 -1
  52. package/src/client/dist/pwa/assets/{render-chat-markdown-DZnIueUV.js → render-chat-markdown-C00A6CfY.js} +1 -1
  53. package/src/client/dist/pwa/assets/{tsMode-of3oS9sU.js → tsMode-qJ7nTArc.js} +1 -1
  54. package/src/client/dist/pwa/assets/{typescript-BTWMoUxn.js → typescript-DayfKox1.js} +1 -1
  55. package/src/client/dist/pwa/assets/{use-onboarding-BTFkRBjE.js → use-onboarding-CF_D2gbm.js} +1 -1
  56. package/src/client/dist/pwa/assets/{use-quasar-C1RCDSb6.js → use-quasar-C3oo6nVn.js} +1 -1
  57. package/src/client/dist/pwa/assets/{vs-DDeaNKsH.js → vs-qnWnWXG9.js} +3 -3
  58. package/src/client/dist/pwa/assets/{workers-DQnKuGcq.js → workers-CvvtZekC.js} +1 -1
  59. package/src/client/dist/pwa/assets/{xml-RglGWD_3.js → xml-CJ1QFxhD.js} +1 -1
  60. package/src/client/dist/pwa/assets/{yaml-CSqGQpai.js → yaml-C3N49N5E.js} +1 -1
  61. package/src/client/dist/pwa/index.html +1 -1
  62. package/src/client/dist/pwa/sw.js +1 -1
  63. package/src/mcp-server/kobo-tasks-handlers.ts +24 -4
  64. package/src/client/dist/pwa/assets/i18n-CK38o-IF.js +0 -1
package/CHANGELOG.md CHANGED
@@ -4,6 +4,10 @@ All notable changes to Kōbō are documented here. The format is based on
4
4
  [Keep a Changelog](https://keepachangelog.com/). Each release is an `## <version>`
5
5
  section — the in-app "What's new" dialog reads this file.
6
6
 
7
+ ## 1.11.14
8
+
9
+ - ⬆️ develop - refactor(deps): Bump SDKs + refactor shutdown + hardening sécurité
10
+
7
11
  ## 1.11.13
8
12
 
9
13
  - fix(engines): unstick agent sessions that never emit session:ended
@@ -4,6 +4,7 @@ import { nanoid } from 'nanoid';
4
4
  import * as cronService from '../server/services/cron-service.js';
5
5
  import * as settingsService from '../server/services/settings-service.js';
6
6
  import { slugifyProjectName } from '../server/utils/project-slug.js';
7
+ import { resolveExistingPathInside } from '../server/utils/safe-path.js';
7
8
  import { resolveWorkspaceWorktreePath } from '../server/utils/worktree-paths.js';
8
9
  /** Allowed task status values. */
9
10
  export const VALID_TASK_STATUSES = ['pending', 'in_progress', 'done'];
@@ -296,6 +297,7 @@ export function getTicketSourcesHandler(worktreePath) {
296
297
  export const DOCUMENT_DIRS = ['docs/plans', 'docs/superpowers', '.ai/thoughts'];
297
298
  /** Depth cap to keep recursion bounded even on pathological symlink loops. */
298
299
  const DOC_MAX_DEPTH = 8;
300
+ const DOC_MAX_FILE_BYTES = 1024 * 1024;
299
301
  function walkMarkdownFiles(rootAbs, rootRel, out, depth = 0) {
300
302
  if (depth > DOC_MAX_DEPTH)
301
303
  return;
@@ -313,11 +315,13 @@ function walkMarkdownFiles(rootAbs, rootRel, out, depth = 0) {
313
315
  const relEntry = `${rootRel}/${entry}`;
314
316
  let stat;
315
317
  try {
316
- stat = fs.statSync(absEntry);
318
+ stat = fs.lstatSync(absEntry);
317
319
  }
318
320
  catch {
319
321
  continue;
320
322
  }
323
+ if (stat.isSymbolicLink())
324
+ continue;
321
325
  if (stat.isDirectory()) {
322
326
  walkMarkdownFiles(absEntry, relEntry, out, depth + 1);
323
327
  }
@@ -336,7 +340,14 @@ export function listDocumentsHandler(worktreePath) {
336
340
  const absDir = path.join(worktreePath, dir);
337
341
  if (!fs.existsSync(absDir))
338
342
  continue;
339
- walkMarkdownFiles(absDir, dir, documents);
343
+ try {
344
+ if (fs.lstatSync(absDir).isSymbolicLink())
345
+ continue;
346
+ walkMarkdownFiles(resolveExistingPathInside(worktreePath, absDir), dir, documents);
347
+ }
348
+ catch {
349
+ // A document directory outside the worktree is never eligible.
350
+ }
340
351
  }
341
352
  documents.sort((a, b) => new Date(b.modifiedAt).getTime() - new Date(a.modifiedAt).getTime());
342
353
  return documents;
@@ -361,7 +372,19 @@ export function readDocumentHandler(worktreePath, relPath) {
361
372
  if (!fs.existsSync(abs)) {
362
373
  throw new Error(`Document not found: ${normalized}`);
363
374
  }
364
- return { path: normalized, content: fs.readFileSync(abs, 'utf-8') };
375
+ const lstat = fs.lstatSync(abs);
376
+ if (lstat.isSymbolicLink()) {
377
+ // Resolve for a consistent error if the link escapes; internal links are
378
+ // rejected too so list/read share the same no-symlink contract.
379
+ resolveExistingPathInside(worktreePath, abs);
380
+ throw new Error('Symbolic links are not allowed in documents');
381
+ }
382
+ const safeAbs = resolveExistingPathInside(worktreePath, abs);
383
+ const stat = fs.statSync(safeAbs);
384
+ if (stat.size > DOC_MAX_FILE_BYTES) {
385
+ throw new Error(`Document too large (max ${DOC_MAX_FILE_BYTES / 1024 / 1024} MB)`);
386
+ }
387
+ return { path: normalized, content: fs.readFileSync(safeAbs, 'utf-8') };
365
388
  }
366
389
  /**
367
390
  * Append a thought / decision / note to
@@ -25,12 +25,12 @@ import templatesRouter from './routes/templates.js';
25
25
  import usageRoutes from './routes/usage.js';
26
26
  import voiceRouter from './routes/voice.js';
27
27
  import workspacesRouter from './routes/workspaces.js';
28
- import { getAvailableSkills, reconcileOrphanSessions, restoreRetryCountsFromDb, sendMessage, setBackendPort, startAgent, startWatchdog, stopAgent, stopWatchdog, } from './services/agent/orchestrator.js';
28
+ import { getAvailableSkills, reconcileOrphanSessions, restoreRetryCountsFromDb, sendMessage, setBackendPort, startAgent, startWatchdog, stopAgent, stopAllAgents, stopWatchdog, } from './services/agent/orchestrator.js';
29
29
  import * as autoLoopService from './services/auto-loop-service.js';
30
30
  import { runContentMigrationIfNeeded } from './services/content-migration-service.js';
31
31
  import * as cronService from './services/cron-service.js';
32
32
  import { createDailyDbBackupIfNeeded, createPreMigrationBackup } from './services/db-backup-service.js';
33
- import { startDevServer, stopDevServer } from './services/dev-server-service.js';
33
+ import { startDevServer, stopAllDevServers, stopDevServer } from './services/dev-server-service.js';
34
34
  import { authorizeWsUpgrade, generateToken, getLanUrls, resolveBindHost, resolveNetworkAccessEnvOverrides, } from './services/network-access-service.js';
35
35
  import { startPrWatcher, stopPrWatcher } from './services/pr-watcher-service.js';
36
36
  import * as quotaBackoffService from './services/quota-backoff-service.js';
@@ -42,7 +42,6 @@ import * as wakeupService from './services/wakeup-service.js';
42
42
  import { emit, emitEphemeral, handleConnection, setMessageHandler } from './services/websocket-service.js';
43
43
  import { getActiveSession, getWorkspace, updateWorkspaceStatus } from './services/workspace-service.js';
44
44
  import { getClientSpaPath, getDbPath, getKoboHome, getPackageVersion } from './utils/paths.js';
45
- import { initProcessCleanup, killAll as killAllTrackedProcesses } from './utils/process-tracker.js';
46
45
  console.log(`[kobo] Kōbō home: ${getKoboHome()}`);
47
46
  // Initialize DB + run migrations
48
47
  const db = getDb();
@@ -72,7 +71,6 @@ void createDailyDbBackupIfNeeded(db, getDbPath()).then((r) => {
72
71
  }
73
72
  });
74
73
  // Initialize process cleanup, agent watchdog, PR watcher, and wakeup rehydration
75
- initProcessCleanup();
76
74
  reconcileOrphanSessions();
77
75
  startWatchdog();
78
76
  wakeupService.rehydrate();
@@ -490,10 +488,10 @@ async function gracefulShutdown(signal) {
490
488
  catch {
491
489
  // Best-effort
492
490
  }
493
- // Kill all tracked child processes (agents, dev servers)
491
+ // Stop managed agents and both direct/Docker dev servers before closing DB.
494
492
  try {
495
- killAllTrackedProcesses();
496
- console.log('[kobo] Tracked processes killed');
493
+ await Promise.all([stopAllAgents(), stopAllDevServers()]);
494
+ console.log('[kobo] Agents and dev servers stopped');
497
495
  }
498
496
  catch {
499
497
  // Best-effort
@@ -5,7 +5,8 @@ import * as workspaceService from '../services/workspace-service.js';
5
5
  const app = new Hono();
6
6
  /** Quote a CSV cell when it contains a delimiter, quote, or newline (RFC 4180). */
7
7
  function csvCell(value) {
8
- return /[",\r\n]/.test(value) ? `"${value.replace(/"/g, '""')}"` : value;
8
+ const safeValue = /^[\s]*[=+\-@]/.test(value) ? `'${value}` : value;
9
+ return /[",\r\n]/.test(safeValue) ? `"${safeValue.replace(/"/g, '""')}"` : safeValue;
9
10
  }
10
11
  function csvRow(values) {
11
12
  return values.map(csvCell).join(',');
@@ -42,6 +42,7 @@ import * as worktreeService from '../services/worktree-service.js';
42
42
  import { resolveUniqueBranchAndPath } from '../utils/branch-resolver.js';
43
43
  import * as gitOps from '../utils/git-ops.js';
44
44
  import { slugifyProjectName } from '../utils/project-slug.js';
45
+ import * as safePath from '../utils/safe-path.js';
45
46
  import { resolveSiblingWorkspaceWorktreePath } from '../utils/worktree-paths.js';
46
47
  /** Hono sub-router for workspace CRUD, tasks, agent lifecycle, git operations, and PR creation. */
47
48
  const app = new Hono();
@@ -2692,6 +2693,13 @@ app.get('/:id/diff-file', (c) => {
2692
2693
  return c.json({ error: `Workspace '${id}' not found` }, 404);
2693
2694
  }
2694
2695
  const worktreePath = workspace.worktreePath;
2696
+ try {
2697
+ safePath.assertPathInside(worktreePath, filePath);
2698
+ }
2699
+ catch (err) {
2700
+ const message = err instanceof Error ? err.message : String(err);
2701
+ return c.json({ error: message }, 400);
2702
+ }
2695
2703
  if (c.req.query('mode') === 'commits') {
2696
2704
  const from = c.req.query('from');
2697
2705
  const to = c.req.query('to');
@@ -2750,6 +2758,13 @@ app.post('/:id/rollback-file', async (c) => {
2750
2758
  if (!filePath) {
2751
2759
  return c.json({ error: 'Missing or invalid `path` field' }, 400);
2752
2760
  }
2761
+ try {
2762
+ safePath.assertPathInside(workspace.worktreePath, filePath);
2763
+ }
2764
+ catch (err) {
2765
+ const message = err instanceof Error ? err.message : String(err);
2766
+ return c.json({ error: message }, 400);
2767
+ }
2753
2768
  let target;
2754
2769
  try {
2755
2770
  target = gitOps.rollbackFile(workspace.worktreePath, workspace.workingBranch, filePath);
@@ -97,7 +97,9 @@ export function createClaudeCodeEngine() {
97
97
  async start(options, onEvent) {
98
98
  const abortController = new AbortController();
99
99
  const mapperState = createMapperState();
100
- const activeSubagentToolCallIds = new Set();
100
+ // UI/tool ids can differ from the SDK task id. Key by the stable UI id
101
+ // so a terminal notification that omits task_id still clears the task.
102
+ const activeSubagentTaskIds = new Map();
101
103
  // Pending canUseTool callbacks, keyed by SDK ctx.toolUseID.
102
104
  const pendingResolvers = new Map();
103
105
  const isInteractive = options.agentPermissionMode === 'interactive';
@@ -309,7 +311,7 @@ export function createClaudeCodeEngine() {
309
311
  if (inputStream.hasUnansweredInput(completedResponses))
310
312
  return;
311
313
  console.warn(`[claude-engine] Subagent(s) still tracked active ${SUBAGENT_STALL_TIMEOUT_MS}ms after the last turn ended — forcing session drain.`);
312
- activeSubagentToolCallIds.clear();
314
+ activeSubagentTaskIds.clear();
313
315
  // This is a forced, unclean termination (the subagent may still be
314
316
  // alive server-side) — never report it as a normal completion, so
315
317
  // auto-loop doesn't treat an orphaned run as forward progress.
@@ -328,16 +330,16 @@ export function createClaudeCodeEngine() {
328
330
  if (ev.kind !== 'subagent:progress')
329
331
  continue;
330
332
  if (ev.status === 'running')
331
- activeSubagentToolCallIds.add(ev.toolCallId);
333
+ activeSubagentTaskIds.set(ev.toolCallId, ev.taskId ?? ev.toolCallId);
332
334
  else
333
- activeSubagentToolCallIds.delete(ev.toolCallId);
335
+ activeSubagentTaskIds.delete(ev.toolCallId);
334
336
  }
335
337
  // The stall watchdog is only armed while we're waiting purely on
336
338
  // background subagents (no further SDK turn expected unless one
337
339
  // reports done). React the moment the last one clears instead of
338
340
  // waiting for a 'result' message that may never come.
339
341
  if (subagentStallTimer &&
340
- activeSubagentToolCallIds.size === 0 &&
342
+ activeSubagentTaskIds.size === 0 &&
341
343
  !inputStream.hasUnansweredInput(completedResponses)) {
342
344
  clearSubagentStallWatchdog();
343
345
  inputStream.close();
@@ -360,7 +362,7 @@ export function createClaudeCodeEngine() {
360
362
  completedResponses++;
361
363
  // A queued forced message starts the next response on this same SDK stream.
362
364
  if (!inputStream.hasUnansweredInput(completedResponses)) {
363
- if (activeSubagentToolCallIds.size === 0) {
365
+ if (activeSubagentTaskIds.size === 0) {
364
366
  clearSubagentStallWatchdog();
365
367
  inputStream.close();
366
368
  armResultDrainWatchdog();
@@ -454,7 +456,7 @@ export function createClaudeCodeEngine() {
454
456
  // background subagent task keeps running unless told to stop via
455
457
  // its own API, which would otherwise leave the Stop button
456
458
  // appearing to do nothing for up to SUBAGENT_STALL_TIMEOUT_MS.
457
- for (const taskId of activeSubagentToolCallIds) {
459
+ for (const taskId of activeSubagentTaskIds.values()) {
458
460
  try {
459
461
  void q.stopTask(taskId).catch(() => {
460
462
  /* best-effort */
@@ -192,7 +192,8 @@ export function mapSdkMessage(msg, state) {
192
192
  return events;
193
193
  }
194
194
  if (subtype === 'task_started' || subtype === 'task_progress' || subtype === 'task_notification') {
195
- const toolCallId = typeof parsed.tool_use_id === 'string' ? parsed.tool_use_id : undefined;
195
+ const taskId = typeof parsed.task_id === 'string' ? parsed.task_id : undefined;
196
+ const toolCallId = typeof parsed.tool_use_id === 'string' ? parsed.tool_use_id : taskId;
196
197
  if (toolCallId) {
197
198
  const usage = parsed.usage;
198
199
  // Unlike task_started/task_progress (in-flight signals), task_notification
@@ -204,6 +205,7 @@ export function mapSdkMessage(msg, state) {
204
205
  events.push({
205
206
  kind: 'subagent:progress',
206
207
  toolCallId,
208
+ ...(taskId ? { taskId } : {}),
207
209
  status: isDone ? 'done' : 'running',
208
210
  description: typeof parsed.description === 'string' ? parsed.description : undefined,
209
211
  taskType: typeof parsed.task_type === 'string' ? parsed.task_type : undefined,
@@ -987,12 +987,7 @@ export function interruptAgent(workspaceId, options = {}) {
987
987
  autoLoopService.disable(workspaceId, 'user-action');
988
988
  }
989
989
  }
990
- /** Gracefully stop an agent (the engine handles SIGTERM + SIGKILL). */
991
- export function stopAgent(workspaceId) {
992
- const ctrl = controllers.get(workspaceId);
993
- if (!ctrl) {
994
- throw new Error(`No agent running for workspace '${workspaceId}'`);
995
- }
990
+ async function stopController(workspaceId, ctrl) {
996
991
  wakeupService.cancel(workspaceId, 'stopped');
997
992
  // If the session was waiting on a question/permission, normalize the state
998
993
  // synchronously so callers (archive, delete, manual stop) see a clean
@@ -1020,11 +1015,42 @@ export function stopAgent(workspaceId) {
1020
1015
  controllers.delete(workspaceId);
1021
1016
  // Manual stop should also drop any pending quota auto-resume.
1022
1017
  quotaBackoffService.cancel(workspaceId, 'user');
1023
- // Fire-and-forget: controller.stop is async but we don't block callers.
1024
- void ctrl.stop().catch((err) => {
1018
+ await ctrl.stop();
1019
+ }
1020
+ /** Gracefully stop an agent (the engine handles SIGTERM + SIGKILL). */
1021
+ export function stopAgent(workspaceId) {
1022
+ const ctrl = controllers.get(workspaceId);
1023
+ if (!ctrl) {
1024
+ throw new Error(`No agent running for workspace '${workspaceId}'`);
1025
+ }
1026
+ // Fire-and-forget: callers such as archive/delete must remain synchronous.
1027
+ void stopController(workspaceId, ctrl).catch((err) => {
1025
1028
  console.error('[orchestrator] controller.stop failed:', err);
1026
1029
  });
1027
1030
  }
1031
+ /** Stop every live engine before process shutdown, with a bounded wait per engine. */
1032
+ export async function stopAllAgents(timeoutMs = 3_000) {
1033
+ const stops = [...controllers.entries()].map(async ([workspaceId, ctrl]) => {
1034
+ let timeout;
1035
+ try {
1036
+ await Promise.race([
1037
+ stopController(workspaceId, ctrl),
1038
+ new Promise((resolve) => {
1039
+ timeout = setTimeout(resolve, timeoutMs);
1040
+ timeout.unref?.();
1041
+ }),
1042
+ ]);
1043
+ }
1044
+ catch (err) {
1045
+ console.error(`[orchestrator] Failed to stop '${workspaceId}' during shutdown:`, err);
1046
+ }
1047
+ finally {
1048
+ if (timeout)
1049
+ clearTimeout(timeout);
1050
+ }
1051
+ });
1052
+ await Promise.all(stops);
1053
+ }
1028
1054
  /** Write a user message to the running agent. */
1029
1055
  export async function sendMessage(workspaceId, content, expectedSessionId) {
1030
1056
  const ctrl = controllers.get(workspaceId);
@@ -170,7 +170,6 @@ function fireOrSkip(id) {
170
170
  }
171
171
  let status = 'skipped-active';
172
172
  if (!orchestrator.hasController(row.workspace_id)) {
173
- status = 'fired';
174
173
  try {
175
174
  const globalSettings = settingsService.getGlobalSettings();
176
175
  const projectSettings = settingsService.getProjectSettings(wsRow.project_path);
@@ -186,9 +185,11 @@ function fireOrSkip(id) {
186
185
  // every fire" (clean context, no conversation continuity).
187
186
  const resumeMode = row.agent_session_id !== null;
188
187
  orchestrator.startAgent(row.workspace_id, worktreePath, row.prompt, wsRow.model, resumeMode, agentPermissionMode, row.agent_session_id ?? undefined, wsRow.reasoning_effort);
188
+ status = 'fired';
189
189
  }
190
190
  catch (err) {
191
191
  console.error(`[cron-service] startAgent at fire time failed for cron '${id}':`, err);
192
+ status = 'start-failed';
192
193
  }
193
194
  }
194
195
  const now = new Date();
@@ -3,7 +3,7 @@ import { existsSync, readdirSync, readFileSync } from 'node:fs';
3
3
  import path from 'node:path';
4
4
  import { getProjectSettings } from './settings-service.js';
5
5
  import { emitEphemeral } from './websocket-service.js';
6
- import { getWorkspace, updateDevServerStatus } from './workspace-service.js';
6
+ import { getWorkspace, listWorkspaces, updateDevServerStatus } from './workspace-service.js';
7
7
  // ── Helpers ───────────────────────────────────────────────────────────────────
8
8
  /** Build a clean env for child processes, stripping Kobo-specific variables. */
9
9
  function cleanEnv() {
@@ -312,6 +312,18 @@ export async function stopDevServer(workspaceId) {
312
312
  emitEphemeral(workspaceId, 'devserver:status', status);
313
313
  return status;
314
314
  }
315
+ /** Stop direct and Docker dev servers for every persisted workspace. */
316
+ export async function stopAllDevServers() {
317
+ const workspaceIds = new Set([...listWorkspaces(true).map((workspace) => workspace.id), ...trackedProcesses.keys()]);
318
+ await Promise.all([...workspaceIds].map(async (workspaceId) => {
319
+ try {
320
+ await stopDevServer(workspaceId);
321
+ }
322
+ catch (err) {
323
+ console.error(`[dev-server] Failed to stop '${workspaceId}' during shutdown:`, err);
324
+ }
325
+ }));
326
+ }
315
327
  // ── Logs ───────────────────────────────────────────────────────────────────────
316
328
  /**
317
329
  * Get logs from running dev-server containers for a workspace.
@@ -6,6 +6,7 @@ import * as settingsService from './settings-service.js';
6
6
  import { emitEphemeral } from './websocket-service.js';
7
7
  const MIN_DELAY_SECONDS = 60;
8
8
  const MAX_DELAY_SECONDS = 21600;
9
+ const ACTIVE_SESSION_RETRY_MS = 15_000;
9
10
  const STALE_WAKEUP_GRACE_MS = 5 * 60 * 1000;
10
11
  const AUTONOMOUS_LOOP_SENTINEL = '<<autonomous-loop-dynamic>>';
11
12
  const AUTONOMOUS_LOOP_FALLBACK_PROMPT = 'Continue where you left off.';
@@ -19,6 +20,15 @@ function rowToPending(row) {
19
20
  return null;
20
21
  return { targetAt: row.target_at, reason: row.reason ?? undefined };
21
22
  }
23
+ /** Keep a claimed wakeup durable while the session is still unavailable. */
24
+ function defer(workspaceId, row) {
25
+ const targetAt = new Date(Date.now() + ACTIVE_SESSION_RETRY_MS).toISOString();
26
+ getDb().prepare('UPDATE pending_wakeups SET target_at = ? WHERE workspace_id = ?').run(targetAt, workspaceId);
27
+ const timeout = setTimeout(() => fire(workspaceId), ACTIVE_SESSION_RETRY_MS);
28
+ timeout.unref?.();
29
+ timers.set(workspaceId, timeout);
30
+ emitEphemeral(workspaceId, 'wakeup:scheduled', { targetAt, reason: row.reason ?? undefined });
31
+ }
22
32
  /** Schedule a wakeup for the given workspace. Replaces any existing pending wakeup. */
23
33
  export function schedule(workspaceId, delaySeconds, prompt, reason, agentSessionId) {
24
34
  try {
@@ -118,21 +128,12 @@ export function rehydrate() {
118
128
  function fire(workspaceId) {
119
129
  try {
120
130
  const db = getDb();
121
- // Atomic claim: SELECT + DELETE in a single transaction so a concurrent
122
- // cancel() can't race us between the read and the act. If we come out
123
- // with a row, it's ours exclusively; no other caller will ever see it.
124
- const row = db.transaction(() => {
125
- const r = db.prepare('SELECT * FROM pending_wakeups WHERE workspace_id = ?').get(workspaceId);
126
- if (r) {
127
- db.prepare('DELETE FROM pending_wakeups WHERE workspace_id = ?').run(workspaceId);
128
- }
129
- return r;
130
- })();
131
+ const row = db.prepare('SELECT * FROM pending_wakeups WHERE workspace_id = ?').get(workspaceId);
131
132
  timers.delete(workspaceId);
132
133
  if (!row)
133
134
  return;
134
135
  if (orchestrator.hasController(workspaceId)) {
135
- emitEphemeral(workspaceId, 'wakeup:skipped', { reason: 'session-active' });
136
+ defer(workspaceId, row);
136
137
  return;
137
138
  }
138
139
  const wsRow = db
@@ -155,12 +156,13 @@ function fire(workspaceId) {
155
156
  const agentPermissionMode = stored === 'plan' || stored === 'strict' || stored === 'interactive' ? stored : 'bypass';
156
157
  try {
157
158
  orchestrator.startAgent(workspaceId, worktreePath, row.prompt, wsRow.model, true, agentPermissionMode, row.agent_session_id ?? undefined, wsRow.reasoning_effort);
159
+ db.prepare('DELETE FROM pending_wakeups WHERE workspace_id = ?').run(workspaceId);
158
160
  emitEphemeral(workspaceId, 'wakeup:fired', {});
159
161
  }
160
162
  catch (err) {
161
163
  const message = err instanceof Error ? err.message : String(err);
162
164
  console.error(`[wakeup-service] startAgent at fire time failed for '${workspaceId}':`, message);
163
- emitEphemeral(workspaceId, 'wakeup:skipped', { reason: 'fire-failed' });
165
+ defer(workspaceId, row);
164
166
  }
165
167
  }
166
168
  catch (err) {
@@ -2,6 +2,7 @@ import { execFile as execFileCb, execFileSync } from 'node:child_process';
2
2
  import { existsSync, readFileSync, rmSync, writeFileSync } from 'node:fs';
3
3
  import { join } from 'node:path';
4
4
  import { promisify } from 'node:util';
5
+ import { resolvePathInside } from './safe-path.js';
5
6
  const execFileAsync = promisify(execFileCb);
6
7
  const READ_ONLY_GIT_TIMEOUT_MS = 15_000;
7
8
  function git(repoPath, args) {
@@ -854,6 +855,7 @@ export function getChangedFilesBetween(repoPath, fromRef, toRef) {
854
855
  * feedback in the UI.
855
856
  */
856
857
  export function rollbackFile(repoPath, branchName, filePath, remote = 'origin') {
858
+ const absPath = resolvePathInside(repoPath, filePath);
857
859
  const remoteRef = `${remote}/${branchName}`;
858
860
  let remoteRefExists = false;
859
861
  try {
@@ -884,7 +886,6 @@ export function rollbackFile(repoPath, branchName, filePath, remote = 'origin')
884
886
  // with a previous rollback, stale UI list, manual rm), we still return
885
887
  // 'deleted' since the end state matches the user's intent. `rmSync`
886
888
  // over `git clean -f` keeps the action narrow to one file.
887
- const absPath = join(repoPath, filePath);
888
889
  if (existsSync(absPath)) {
889
890
  rmSync(absPath, { force: true });
890
891
  }
@@ -896,7 +897,7 @@ export const rollbackFileToRemote = rollbackFile;
896
897
  /** Get the current content of a file in the worktree. Returns null if the file doesn't exist. */
897
898
  export function getFileContent(repoPath, filePath) {
898
899
  try {
899
- return readFileSync(join(repoPath, filePath), 'utf-8');
900
+ return readFileSync(resolvePathInside(repoPath, filePath), 'utf-8');
900
901
  }
901
902
  catch {
902
903
  return null;
@@ -4,6 +4,13 @@ export function isPathInside(rootPath, candidatePath) {
4
4
  const relative = path.relative(rootPath, candidatePath);
5
5
  return relative === '' || (!relative.startsWith(`..${path.sep}`) && relative !== '..' && !path.isAbsolute(relative));
6
6
  }
7
+ /** Reject an absolute or traversal path without requiring it to exist on disk. */
8
+ export function assertPathInside(rootPath, relativePath) {
9
+ const root = path.resolve(rootPath);
10
+ const candidate = path.resolve(root, relativePath);
11
+ if (!isPathInside(root, candidate))
12
+ throw new Error(`Path '${relativePath}' escapes allowed root`);
13
+ }
7
14
  /** Resolve two existing paths and reject candidates that escape through a symlinked parent. */
8
15
  export function resolveExistingPathInside(rootPath, candidatePath) {
9
16
  const realRoot = fs.realpathSync(rootPath);
@@ -12,3 +19,30 @@ export function resolveExistingPathInside(rootPath, candidatePath) {
12
19
  throw new Error('Path escapes allowed root');
13
20
  return realCandidate;
14
21
  }
22
+ /**
23
+ * Resolve a worktree path, including its parent symlinks, while allowing a
24
+ * missing leaf (needed when removing an untracked file).
25
+ */
26
+ export function resolvePathInside(rootPath, relativePath) {
27
+ assertPathInside(rootPath, relativePath);
28
+ const root = fs.realpathSync(rootPath);
29
+ const candidate = path.resolve(rootPath, relativePath);
30
+ let parent;
31
+ try {
32
+ parent = fs.realpathSync(path.dirname(candidate));
33
+ }
34
+ catch {
35
+ throw new Error(`Path '${relativePath}' is invalid (parent directory does not exist)`);
36
+ }
37
+ if (!isPathInside(root, parent))
38
+ throw new Error('Path escapes allowed root');
39
+ const resolved = path.join(parent, path.basename(candidate));
40
+ try {
41
+ return resolveExistingPathInside(root, resolved);
42
+ }
43
+ catch (err) {
44
+ if (err.code === 'ENOENT')
45
+ return resolved;
46
+ throw err;
47
+ }
48
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@loicngr/kobo",
3
- "version": "1.11.13",
3
+ "version": "1.11.14",
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",
@@ -67,12 +67,12 @@
67
67
  "prepublishOnly": "npm run build"
68
68
  },
69
69
  "dependencies": {
70
- "@anthropic-ai/claude-agent-sdk": "^0.3.226",
70
+ "@anthropic-ai/claude-agent-sdk": "^0.3.246",
71
71
  "@emnapi/core": "^1.11.3",
72
72
  "@emnapi/runtime": "^1.11.3",
73
73
  "@hono/node-server": "^2.1.0",
74
74
  "@modelcontextprotocol/sdk": "^1.30.0",
75
- "@openai/codex": "^0.147.0",
75
+ "@openai/codex": "^0.149.1",
76
76
  "better-sqlite3": "^13.0.3",
77
77
  "cron-parser": "^5.7.0",
78
78
  "hono": "^4.13.1",
@@ -1,4 +1,4 @@
1
- import{n as e}from"./vue-i18n-C3W5i7h6.js";import{B as t,C as n,St as r,wt as i,yt as a}from"./reactivity.esm-bundler-DtSZKRQB.js";import{$ as o,A as s,B as c,H as l,It as u,Mt as d,Rt as f,Tt as p,U as m,V as h,W as g,Wt as _,X as v,Z as y,_n as b,fn as x,t as S,w as C}from"./runtime-dom.esm-bundler-DgevF6KV.js";import{l as w,t as T}from"./QIcon--SvgE-nS.js";import{G as E,P as D,Q as O,d as k,f as A,v as j,y as M}from"./index-B_OmZ65F.js";import{t as N}from"./QSpinnerDots-B4RmFJUj.js";import{u as ee}from"./QSelect-8Ip46vhk.js";import{t as P}from"./QTooltip-Bz4g3bmr.js";import{r as F,t as te}from"./pending-diff-open-D1IYH8Pd.js";import{t as I}from"./documents-CyKQ9lT6.js";import{i as L,n as ne,t as R}from"./render-chat-markdown-DZnIueUV.js";import{t as z}from"./_plugin-vue_export-helper-BDNMzG2s.js";import{n as B}from"./WorkspacePage-CbC_t5Rx.js";function re(e,t,n=!0,r){let i=[],a=new Map,o=new Map;for(let n=0;n<e.length;n++){let s=e[n],c=t?.[n],l=r?.[n]??void 0;switch(s.kind){case`message:text`:{let e=a.get(s.messageId);if(e)e.text+=s.text,e.streaming=s.streaming,l&&e.eventIds?.push(l);else{let e={type:`text`,messageId:s.messageId,text:s.text,streaming:s.streaming,ts:c,eventIds:l?[l]:void 0};a.set(s.messageId,e),i.push(e)}break}case`message:end`:{let e=a.get(s.messageId);e&&(e.streaming=!1);break}case`message:thinking`:i.push({type:`thinking`,messageId:s.messageId,text:s.text,ts:c,eventIds:l?[l]:void 0});break;case`tool:call`:{let e={type:`tool`,toolCallId:s.toolCallId,name:s.name,input:s.input,ts:c,eventIds:l?[l]:void 0};o.set(s.toolCallId,e),i.push(e);break}case`tool:result`:{let e=o.get(s.toolCallId);e&&(e.result={output:s.output,isError:s.isError},l&&e.eventIds?.push(l));break}case`session:started`:i.push({type:`session`,kind:`started`,detail:{engineSessionId:s.engineSessionId,model:s.model},ts:c,eventIds:l?[l]:void 0});break;case`session:ended`:i.push({type:`session`,kind:`ended`,detail:{reason:s.reason,exitCode:s.exitCode},ts:c,eventIds:l?[l]:void 0});break;case`session:compacted`:i.push({type:`session`,kind:`compacted`,ts:c,eventIds:l?[l]:void 0})}}let s=null;for(let e of i)e.type===`text`&&e.streaming&&(s&&(s.streaming=!1),s=e);return s&&!n&&(s.streaming=!1),i}function ie(e,t){if(t.length===0)return e;let n=t.map(e=>({type:`user`,content:e.content,sender:e.sender,ts:e.ts,eventIds:e.eventIds})),r=[...e,...n];r.sort((e,t)=>{let n=e.ts??``,r=t.ts??``;return n===r?0:n?r?n<r?-1:1:-1:1});let i;for(let e of r)e.type===`user`&&e.sender!==`system-prompt`&&e.ts&&(!i||e.ts>i)&&(i=e.ts);if(i)for(let e of r)e.type===`text`&&e.streaming&&(!e.ts||e.ts<i)&&(e.streaming=!1);return r}var V=new Set([`setup`,`cleanup`,`archive`]);function H(e){switch(e.type){case`user`:return e.sender===`system-prompt`?`system-prompt`:V.has(e.sender)?`script`:`user`;case`session`:return`session`;default:return`agent`}}function ae(e){let t=[],n=null,r=null;for(let i of e){let e=H(i),a=e===`session`||e===`system-prompt`,o=e===`script`&&i.type===`user`?`script:${i.sender}`: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}var U={class:`text-caption text-grey-6`},oe=o({__name:`SessionEventItem`,props:{item:{}},setup(e){let t=e,n=c(()=>{switch(t.item.kind){case`started`:return`session.started`;case`ended`:return`session.ended`;case`compacted`:return`session.compacted`;default:return`session.started`}});return(e,t)=>(f(),g(`span`,U,i(e.$t(n.value)),1))}});function se(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){ce(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 ce(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=le(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 le(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 W=[`innerHTML`],ue=z(o({__name:`TextMessageItem`,props:{item:{}},setup(e){let t=e,n=I(),r=k(),i=c(()=>{let e=r.selectedWorkspaceId;return e?n.documentsFor(e).map(e=>e.path):[]}),a=c(()=>{let e=se(L.parse(t.item.text,{async:!1,breaks:!0,gfm:!0}),i.value);return ne(e,{addAttr:[`data-document-path`]})});function o(e){let t=e.target?.closest(`.document-link`);if(!t)return;e.preventDefault();let i=t.getAttribute(`data-document-path`),a=r.selectedWorkspaceId;!i||!a||n.openDocumentByPath(a,i)}return(t,n)=>(f(),g(`div`,{class:`markdown-message`,onClick:o},[h(`div`,{innerHTML:a.value},null,8,W),e.item.streaming?(f(),l(w,{key:0,size:`xs`,class:`q-ml-xs`})):m(``,!0)]))}}),[[`__scopeId`,`data-v-1b7bd8ca`]]);function de(e,t){let n=e.split(`
1
+ import{n as e}from"./vue-i18n-C3W5i7h6.js";import{B as t,C as n,St as r,wt as i,yt as a}from"./reactivity.esm-bundler-DtSZKRQB.js";import{$ as o,A as s,B as c,H as l,It as u,Mt as d,Rt as f,Tt as p,U as m,V as h,W as g,Wt as _,X as v,Z as y,_n as b,fn as x,t as S,w as C}from"./runtime-dom.esm-bundler-DgevF6KV.js";import{l as w,t as T}from"./QIcon--SvgE-nS.js";import{G as E,P as D,Q as O,d as k,f as A,v as j,y as M}from"./index-CtK8ORpZ.js";import{t as N}from"./QSpinnerDots-B4RmFJUj.js";import{u as ee}from"./QSelect-xHphd7hW.js";import{t as P}from"./QTooltip-DyS20jDT.js";import{r as F,t as te}from"./pending-diff-open-DHlXLorX.js";import{t as I}from"./documents-DJy1tgPd.js";import{i as L,n as ne,t as R}from"./render-chat-markdown-C00A6CfY.js";import{t as z}from"./_plugin-vue_export-helper-BDNMzG2s.js";import{n as B}from"./WorkspacePage-CJKSrSEC.js";function re(e,t,n=!0,r){let i=[],a=new Map,o=new Map;for(let n=0;n<e.length;n++){let s=e[n],c=t?.[n],l=r?.[n]??void 0;switch(s.kind){case`message:text`:{let e=a.get(s.messageId);if(e)e.text+=s.text,e.streaming=s.streaming,l&&e.eventIds?.push(l);else{let e={type:`text`,messageId:s.messageId,text:s.text,streaming:s.streaming,ts:c,eventIds:l?[l]:void 0};a.set(s.messageId,e),i.push(e)}break}case`message:end`:{let e=a.get(s.messageId);e&&(e.streaming=!1);break}case`message:thinking`:i.push({type:`thinking`,messageId:s.messageId,text:s.text,ts:c,eventIds:l?[l]:void 0});break;case`tool:call`:{let e={type:`tool`,toolCallId:s.toolCallId,name:s.name,input:s.input,ts:c,eventIds:l?[l]:void 0};o.set(s.toolCallId,e),i.push(e);break}case`tool:result`:{let e=o.get(s.toolCallId);e&&(e.result={output:s.output,isError:s.isError},l&&e.eventIds?.push(l));break}case`session:started`:i.push({type:`session`,kind:`started`,detail:{engineSessionId:s.engineSessionId,model:s.model},ts:c,eventIds:l?[l]:void 0});break;case`session:ended`:i.push({type:`session`,kind:`ended`,detail:{reason:s.reason,exitCode:s.exitCode},ts:c,eventIds:l?[l]:void 0});break;case`session:compacted`:i.push({type:`session`,kind:`compacted`,ts:c,eventIds:l?[l]:void 0})}}let s=null;for(let e of i)e.type===`text`&&e.streaming&&(s&&(s.streaming=!1),s=e);return s&&!n&&(s.streaming=!1),i}function ie(e,t){if(t.length===0)return e;let n=t.map(e=>({type:`user`,content:e.content,sender:e.sender,ts:e.ts,eventIds:e.eventIds})),r=[...e,...n];r.sort((e,t)=>{let n=e.ts??``,r=t.ts??``;return n===r?0:n?r?n<r?-1:1:-1:1});let i;for(let e of r)e.type===`user`&&e.sender!==`system-prompt`&&e.ts&&(!i||e.ts>i)&&(i=e.ts);if(i)for(let e of r)e.type===`text`&&e.streaming&&(!e.ts||e.ts<i)&&(e.streaming=!1);return r}var V=new Set([`setup`,`cleanup`,`archive`]);function H(e){switch(e.type){case`user`:return e.sender===`system-prompt`?`system-prompt`:V.has(e.sender)?`script`:`user`;case`session`:return`session`;default:return`agent`}}function ae(e){let t=[],n=null,r=null;for(let i of e){let e=H(i),a=e===`session`||e===`system-prompt`,o=e===`script`&&i.type===`user`?`script:${i.sender}`: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}var U={class:`text-caption text-grey-6`},oe=o({__name:`SessionEventItem`,props:{item:{}},setup(e){let t=e,n=c(()=>{switch(t.item.kind){case`started`:return`session.started`;case`ended`:return`session.ended`;case`compacted`:return`session.compacted`;default:return`session.started`}});return(e,t)=>(f(),g(`span`,U,i(e.$t(n.value)),1))}});function se(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){ce(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 ce(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=le(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 le(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 W=[`innerHTML`],ue=z(o({__name:`TextMessageItem`,props:{item:{}},setup(e){let t=e,n=I(),r=k(),i=c(()=>{let e=r.selectedWorkspaceId;return e?n.documentsFor(e).map(e=>e.path):[]}),a=c(()=>{let e=se(L.parse(t.item.text,{async:!1,breaks:!0,gfm:!0}),i.value);return ne(e,{addAttr:[`data-document-path`]})});function o(e){let t=e.target?.closest(`.document-link`);if(!t)return;e.preventDefault();let i=t.getAttribute(`data-document-path`),a=r.selectedWorkspaceId;!i||!a||n.openDocumentByPath(a,i)}return(t,n)=>(f(),g(`div`,{class:`markdown-message`,onClick:o},[h(`div`,{innerHTML:a.value},null,8,W),e.item.streaming?(f(),l(w,{key:0,size:`xs`,class:`q-ml-xs`})):m(``,!0)]))}}),[[`__scopeId`,`data-v-1b7bd8ca`]]);function de(e,t){let n=e.split(`
2
2
  `),r=t.split(`
3
3
  `),i=n.length,a=r.length,o=Array.from({length:i+1},()=>Array(a+1).fill(0));for(let e=i-1;e>=0;e--)for(let t=a-1;t>=0;t--)n[e]===r[t]?o[e][t]=o[e+1][t+1]+1:o[e][t]=Math.max(o[e+1][t],o[e][t+1]);let s=[],c=0,l=0;for(;c<i&&l<a;)n[c]===r[l]?(s.push({type:`context`,content:n[c]}),c++,l++):o[c+1][l]>=o[c][l+1]?(s.push({type:`del`,content:n[c]}),c++):(s.push({type:`add`,content:r[l]}),l++);for(;c<i;)s.push({type:`del`,content:n[c++]});for(;l<a;)s.push({type:`add`,content:r[l++]});return s}function G(e){let t=e.split(`
4
4
  `),n=[];for(let e of t)e.startsWith(`@@`)||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 K(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=G(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{B as e,C as t,wt as n}from"./reactivity.esm-bundler-DtSZKRQB.js";import{$ as r,A as i,H as a,Mt as o,Rt as s,U as c,V as l,W as u,Wt as d,Z as f,_n as p}from"./runtime-dom.esm-bundler-DgevF6KV.js";import{Dt as m,G as h,kt as g,s as _}from"./index-B_OmZ65F.js";import{t as v}from"./render-chat-markdown-DZnIueUV.js";import{t as y}from"./_plugin-vue_export-helper-BDNMzG2s.js";import{t as b}from"./QSpace-D9UEvM99.js";import{t as x}from"./QChip-DWGDTZnX.js";import{t as S}from"./QPage-DuIsRmYu.js";var C={class:`row items-center q-mb-md`},w={class:`text-h6 q-ml-sm`},T={key:0,class:`text-grey-6 text-center q-pa-lg`},E={key:1,class:`text-negative text-center q-pa-lg`},D={key:2,class:`text-grey-6 text-center q-pa-lg`},O={key:3,class:`column q-gutter-md`},k={key:0,class:`text-caption text-grey-6`},A={class:`row items-center q-mb-sm`},j={class:`text-subtitle1 text-indigo-3`,style:{"font-family":`var(--kobo-font-mono, monospace)`}},M=[`innerHTML`],N=y(r({__name:`ChangelogPage`,setup(r){let y=_(),N=t([]),P=t(``),F=t(!1),I=t(null);function L(e){return v(e)}async function R(){F.value=!0,I.value=null;try{let e=await fetch(`/api/changelog`);if(!e.ok)throw Error(`HTTP ${e.status}`);let t=await e.json();P.value=t.currentVersion??``,N.value=t.versions??[]}catch(e){I.value=e instanceof Error?e.message:String(e)}finally{F.value=!1}}return o(R),(t,r)=>(s(),a(S,{class:`q-pa-md`,style:{"max-width":`900px`,margin:`0 auto`}},{default:p(()=>[l(`div`,C,[f(h,{flat:``,dense:``,round:``,icon:`arrow_back`,onClick:r[0]||=t=>e(y).back()}),l(`div`,w,n(t.$t(`changelog.title`)),1),f(b),f(h,{flat:``,dense:``,icon:`refresh`,loading:F.value,label:t.$t(`common.refresh`),onClick:R},null,8,[`loading`,`label`])]),F.value&&N.value.length===0?(s(),u(`div`,T,n(t.$t(`common.loading`)),1)):I.value?(s(),u(`div`,E,n(I.value),1)):N.value.length===0?(s(),u(`div`,D,n(t.$t(`changelog.empty`)),1)):(s(),u(`div`,O,[P.value?(s(),u(`div`,k,n(t.$t(`changelog.currentVersion`,{version:P.value})),1)):c(``,!0),(s(!0),u(i,null,d(N.value,e=>(s(),a(m,{key:e.version,dark:``,flat:``,bordered:``},{default:p(()=>[f(g,null,{default:p(()=>[l(`div`,A,[l(`div`,j,` v`+n(e.version),1),e.version===P.value?(s(),a(x,{key:0,dense:``,size:`sm`,color:`indigo-7`,"text-color":`grey-2`,label:t.$t(`changelog.current`),class:`q-ml-sm`},null,8,[`label`])):c(``,!0)]),l(`div`,{class:`changelog-notes`,innerHTML:L(e.notes)},null,8,M)]),_:2},1024)]),_:2},1024))),128))]))]),_:1}))}}),[[`__scopeId`,`data-v-ed73d661`]]);export{N as default};
1
+ import{B as e,C as t,wt as n}from"./reactivity.esm-bundler-DtSZKRQB.js";import{$ as r,A as i,H as a,Mt as o,Rt as s,U as c,V as l,W as u,Wt as d,Z as f,_n as p}from"./runtime-dom.esm-bundler-DgevF6KV.js";import{Dt as m,G as h,kt as g,s as _}from"./index-CtK8ORpZ.js";import{t as v}from"./render-chat-markdown-C00A6CfY.js";import{t as y}from"./_plugin-vue_export-helper-BDNMzG2s.js";import{t as b}from"./QSpace-D9UEvM99.js";import{t as x}from"./QChip-CY0_8v_e.js";import{t as S}from"./QPage-DuIsRmYu.js";var C={class:`row items-center q-mb-md`},w={class:`text-h6 q-ml-sm`},T={key:0,class:`text-grey-6 text-center q-pa-lg`},E={key:1,class:`text-negative text-center q-pa-lg`},D={key:2,class:`text-grey-6 text-center q-pa-lg`},O={key:3,class:`column q-gutter-md`},k={key:0,class:`text-caption text-grey-6`},A={class:`row items-center q-mb-sm`},j={class:`text-subtitle1 text-indigo-3`,style:{"font-family":`var(--kobo-font-mono, monospace)`}},M=[`innerHTML`],N=y(r({__name:`ChangelogPage`,setup(r){let y=_(),N=t([]),P=t(``),F=t(!1),I=t(null);function L(e){return v(e)}async function R(){F.value=!0,I.value=null;try{let e=await fetch(`/api/changelog`);if(!e.ok)throw Error(`HTTP ${e.status}`);let t=await e.json();P.value=t.currentVersion??``,N.value=t.versions??[]}catch(e){I.value=e instanceof Error?e.message:String(e)}finally{F.value=!1}}return o(R),(t,r)=>(s(),a(S,{class:`q-pa-md`,style:{"max-width":`900px`,margin:`0 auto`}},{default:p(()=>[l(`div`,C,[f(h,{flat:``,dense:``,round:``,icon:`arrow_back`,onClick:r[0]||=t=>e(y).back()}),l(`div`,w,n(t.$t(`changelog.title`)),1),f(b),f(h,{flat:``,dense:``,icon:`refresh`,loading:F.value,label:t.$t(`common.refresh`),onClick:R},null,8,[`loading`,`label`])]),F.value&&N.value.length===0?(s(),u(`div`,T,n(t.$t(`common.loading`)),1)):I.value?(s(),u(`div`,E,n(I.value),1)):N.value.length===0?(s(),u(`div`,D,n(t.$t(`changelog.empty`)),1)):(s(),u(`div`,O,[P.value?(s(),u(`div`,k,n(t.$t(`changelog.currentVersion`,{version:P.value})),1)):c(``,!0),(s(!0),u(i,null,d(N.value,e=>(s(),a(m,{key:e.version,dark:``,flat:``,bordered:``},{default:p(()=>[f(g,null,{default:p(()=>[l(`div`,A,[l(`div`,j,` v`+n(e.version),1),e.version===P.value?(s(),a(x,{key:0,dense:``,size:`sm`,color:`indigo-7`,"text-color":`grey-2`,label:t.$t(`changelog.current`),class:`q-ml-sm`},null,8,[`label`])):c(``,!0)]),l(`div`,{class:`changelog-notes`,innerHTML:L(e.notes)},null,8,M)]),_:2},1024)]),_:2},1024))),128))]))]),_:1}))}}),[[`__scopeId`,`data-v-ed73d661`]]);export{N as default};
@@ -1 +1 @@
1
- import{P as e,b as t}from"./QIcon--SvgE-nS.js";import{gt as n,ht as r}from"./index-B_OmZ65F.js";var i={left:!0,right:!0,up:!0,down:!0,horizontal:!0,vertical:!0},a=Object.keys(i);i.all=!0;function o(e){let t={};for(let n of a)e[n]&&(t[n]=!0);return Object.keys(t).length===0?i:(t.horizontal?t.left=t.right=!0:t.left&&t.right&&(t.horizontal=!0),t.vertical?t.up=t.down=!0:t.up&&t.down&&(t.vertical=!0),t.horizontal&&t.vertical&&(t.all=!0),t)}var s=[`INPUT`,`TEXTAREA`];function c(e,t){return t.event===void 0&&e.target!==void 0&&!e.target.draggable&&typeof t.handler==`function`&&!s.includes(e.target.nodeName.toUpperCase())&&(e.qClonedBy===void 0||!e.qClonedBy.includes(t.uid))}function l(e){return e===!1?0:e===!0||e===void 0?1:Number.parseInt(e,10)||0}var u=e({name:`close-popup`,beforeMount(e,{value:i}){let a={depth:l(i),handler(t){a.depth!==0&&setTimeout(()=>{let i=n(e);i!==void 0&&r(i,t,a.depth)},0)},handlerKey(e){t(e,13)&&a.handler(e)}};e.__qclosepopup=a,e.addEventListener(`click`,a.handler),e.addEventListener(`keyup`,a.handlerKey)},updated(e,{value:t,oldValue:n}){t!==n&&(e.__qclosepopup.depth=l(t))},beforeUnmount(e){let t=e.__qclosepopup;e.removeEventListener(`click`,t.handler),e.removeEventListener(`keyup`,t.handlerKey),delete e.__qclosepopup}});export{o as n,c as r,u as t};
1
+ import{P as e,b as t}from"./QIcon--SvgE-nS.js";import{gt as n,ht as r}from"./index-CtK8ORpZ.js";var i={left:!0,right:!0,up:!0,down:!0,horizontal:!0,vertical:!0},a=Object.keys(i);i.all=!0;function o(e){let t={};for(let n of a)e[n]&&(t[n]=!0);return Object.keys(t).length===0?i:(t.horizontal?t.left=t.right=!0:t.left&&t.right&&(t.horizontal=!0),t.vertical?t.up=t.down=!0:t.up&&t.down&&(t.vertical=!0),t.horizontal&&t.vertical&&(t.all=!0),t)}var s=[`INPUT`,`TEXTAREA`];function c(e,t){return t.event===void 0&&e.target!==void 0&&!e.target.draggable&&typeof t.handler==`function`&&!s.includes(e.target.nodeName.toUpperCase())&&(e.qClonedBy===void 0||!e.qClonedBy.includes(t.uid))}function l(e){return e===!1?0:e===!0||e===void 0?1:Number.parseInt(e,10)||0}var u=e({name:`close-popup`,beforeMount(e,{value:i}){let a={depth:l(i),handler(t){a.depth!==0&&setTimeout(()=>{let i=n(e);i!==void 0&&r(i,t,a.depth)},0)},handlerKey(e){t(e,13)&&a.handler(e)}};e.__qclosepopup=a,e.addEventListener(`click`,a.handler),e.addEventListener(`keyup`,a.handlerKey)},updated(e,{value:t,oldValue:n}){t!==n&&(e.__qclosepopup.depth=l(t))},beforeUnmount(e){let t=e.__qclosepopup;e.removeEventListener(`click`,t.handler),e.removeEventListener(`keyup`,t.handlerKey),delete e.__qclosepopup}});export{o as n,c as r,u as t};