@loicngr/kobo 1.7.26 → 1.7.28

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 (61) hide show
  1. package/CHANGELOG.md +8 -0
  2. package/dist/mcp-server/kobo-tasks-server.js +27 -0
  3. package/dist/server/routes/workspaces.js +94 -4
  4. package/dist/server/services/file-editor-service.js +11 -1
  5. package/dist/server/services/templates-service.js +7 -0
  6. package/dist/server/services/usage/providers/claude-code.js +33 -2
  7. package/dist/server/utils/git-ops.js +105 -6
  8. package/package.json +1 -1
  9. package/src/client/dist/spa/assets/{ActivityFeed-kV5CrtOV.js → ActivityFeed-DW7Bv1Cx.js} +1 -1
  10. package/src/client/dist/spa/assets/{ChangelogPage-xdtvQVeE.js → ChangelogPage-vEYi6Byk.js} +1 -1
  11. package/src/client/dist/spa/assets/{CreatePage-DFtuX7-R.js → CreatePage-CSbZYNPW.js} +2 -2
  12. package/src/client/dist/spa/assets/{DiffViewer-C4L5y8Ho.css → DiffViewer-BuYD1EDP.css} +1 -1
  13. package/src/client/dist/spa/assets/DiffViewer-CkZgt7Fp.js +8 -0
  14. package/src/client/dist/spa/assets/{HealthPage-Dkrr4w8H.js → HealthPage-BLcP13YD.js} +1 -1
  15. package/src/client/dist/spa/assets/{MainLayout-D2fdfOct.css → MainLayout-CILSd-EM.css} +1 -1
  16. package/src/client/dist/spa/assets/{MainLayout-Bpqn8uw3.js → MainLayout-j-gqN_I5.js} +17 -17
  17. package/src/client/dist/spa/assets/QSelect-U9wJ7Z2A.js +36 -0
  18. package/src/client/dist/spa/assets/{SearchPage-BI4zhWxn.js → SearchPage-BPknEVtZ.js} +1 -1
  19. package/src/client/dist/spa/assets/SettingsPage-HhPqZAtG.js +9 -0
  20. package/src/client/dist/spa/assets/WorkspacePage-Co34Wjna.js +4 -0
  21. package/src/client/dist/spa/assets/{cssMode-Cj7dq4tA.js → cssMode-D_Z8-Q5h.js} +1 -1
  22. package/src/client/dist/spa/assets/{editor.api-DmE4Vf3n.js → editor.api-DgduwnV5.js} +1 -1
  23. package/src/client/dist/spa/assets/{editor.main-BtZbr82f.js → editor.main-C4_EYinG.js} +3 -3
  24. package/src/client/dist/spa/assets/{engineFeatures-3w5M3c1y.js → engineFeatures-B3_uxW5Z.js} +1 -1
  25. package/src/client/dist/spa/assets/{expand-template-Cvhhn0Oi.js → expand-template-D7jp6KZJ.js} +1 -1
  26. package/src/client/dist/spa/assets/{freemarker2-D9gcKCMr.js → freemarker2-6aUXBO6n.js} +1 -1
  27. package/src/client/dist/spa/assets/{handlebars-BrcqC-zE.js → handlebars-D2zveC0w.js} +1 -1
  28. package/src/client/dist/spa/assets/{html-DpYYEfpW.js → html-C-zqkrrG.js} +1 -1
  29. package/src/client/dist/spa/assets/{htmlMode-B8fY-cpH.js → htmlMode-DE3gz2Wd.js} +1 -1
  30. package/src/client/dist/spa/assets/i18n-BmFuyRbK.js +1 -0
  31. package/src/client/dist/spa/assets/{index-D4b3k9DV.js → index-TewFyoT4.js} +7 -7
  32. package/src/client/dist/spa/assets/{javascript-Cty6LK6F.js → javascript-xiWyVH4o.js} +1 -1
  33. package/src/client/dist/spa/assets/{jsonMode-Dp8DQFxa.js → jsonMode-JTmi1bQa.js} +1 -1
  34. package/src/client/dist/spa/assets/kobo-commands-C0Y02P5A.js +9 -0
  35. package/src/client/dist/spa/assets/{liquid-BBh8fxMj.js → liquid-CT9b60IZ.js} +1 -1
  36. package/src/client/dist/spa/assets/{mdx-4Sg0uY19.js → mdx-QIdOXPHb.js} +1 -1
  37. package/src/client/dist/spa/assets/{monaco.contribution-BqWsUYYb.js → monaco.contribution-uzj4TvhF.js} +2 -2
  38. package/src/client/dist/spa/assets/permissionModes-BUhr5Wdx.js +1 -0
  39. package/src/client/dist/spa/assets/{python-CNVrhWpS.js → python-7sJU2SF0.js} +1 -1
  40. package/src/client/dist/spa/assets/{razor-CF0pOS5A.js → razor-kkOOm93b.js} +1 -1
  41. package/src/client/dist/spa/assets/{render-chat-markdown-C9SvAdWB.js → render-chat-markdown-G7XwL3vR.js} +1 -1
  42. package/src/client/dist/spa/assets/{tsMode-BtgiAz6G.js → tsMode-BY1pp4Yd.js} +1 -1
  43. package/src/client/dist/spa/assets/{typescript-C0__juCT.js → typescript-D3HSMsZe.js} +1 -1
  44. package/src/client/dist/spa/assets/{use-onboarding-D4TTIa1e.js → use-onboarding-CcwSDBsS.js} +1 -1
  45. package/src/client/dist/spa/assets/{xml-D0l5CGKt.js → xml-BC8zXouV.js} +1 -1
  46. package/src/client/dist/spa/assets/{yaml-CroybGxn.js → yaml-CeoIec1I.js} +1 -1
  47. package/src/client/dist/spa/index.html +1 -1
  48. package/src/mcp-server/kobo-tasks-server.ts +29 -0
  49. package/src/client/dist/spa/assets/DiffViewer-DQ5PexFb.js +0 -8
  50. package/src/client/dist/spa/assets/SettingsPage-BEMFsN0N.js +0 -9
  51. package/src/client/dist/spa/assets/WorkspacePage-CRs9kUM0.js +0 -4
  52. package/src/client/dist/spa/assets/i18n-D3e67G-j.js +0 -1
  53. package/src/client/dist/spa/assets/kobo-commands-DRDkhOO8.js +0 -9
  54. package/src/client/dist/spa/assets/permissionModes-cbEFVOC0.js +0 -1
  55. package/src/client/dist/spa/assets/skill-suite-prompts-8f_JW79j.js +0 -36
  56. /package/src/client/dist/spa/assets/{ClosePopup-0MWohgml.js → ClosePopup-BWYh08p9.js} +0 -0
  57. /package/src/client/dist/spa/assets/{QChip-D2TVel5I.js → QChip-5bnjPnRz.js} +0 -0
  58. /package/src/client/dist/spa/assets/{QList-CRYZxnPD.js → QList-Ch5K5W7r.js} +0 -0
  59. /package/src/client/dist/spa/assets/{QMenu-Yx1QEIHC.js → QMenu-CoPEAblj.js} +0 -0
  60. /package/src/client/dist/spa/assets/{build-path-tree-BKx2q92A.js → build-path-tree-B9aeh1tv.js} +0 -0
  61. /package/src/client/dist/spa/assets/{use-quasar-q6dh7QVJ.js → use-quasar-sypIWZgU.js} +0 -0
package/CHANGELOG.md CHANGED
@@ -4,6 +4,14 @@ 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.7.28
8
+
9
+ - feat: commit diff review, workspace rename tool, macOS usage keychain
10
+
11
+ ## 1.7.27
12
+
13
+ - feat(git): add dirty-worktree recovery for rebase/merge
14
+
7
15
  ## 1.7.26
8
16
 
9
17
  - fix(claude-code-engine): migrate compaction reminder to SessionStart hook
@@ -208,6 +208,21 @@ server.setRequestHandler(ListToolsRequestSchema, async () => ({
208
208
  },
209
209
  annotations: { destructiveHint: false, openWorldHint: false },
210
210
  },
211
+ {
212
+ name: 'set_workspace_name',
213
+ description: 'Rename THIS workspace — sets the `name` shown in the sidebar and window title. Call this ONLY when the user explicitly asks you to rename the workspace — never rename it on your own initiative. Whitespace is collapsed and trimmed; the name cannot be empty and is capped at the configured max length. Distinct from `agent_description` (the one-line status summary) and from the user-controlled `description`. The current value is in get_workspace_info as `name`.',
214
+ inputSchema: {
215
+ type: 'object',
216
+ properties: {
217
+ name: {
218
+ type: 'string',
219
+ description: 'New workspace name. Non-empty after trimming; control characters are stripped.',
220
+ },
221
+ },
222
+ required: ['name'],
223
+ },
224
+ annotations: { destructiveHint: false, openWorldHint: false },
225
+ },
211
226
  {
212
227
  name: 'cron_create',
213
228
  description: 'Schedule a recurring trigger on THIS workspace. At each fire, Kōbō waits for the workspace to be idle (no active session) and then resumes the same conversation by injecting `prompt` as the next user message — same UX as `schedule_wakeup` but recurring. Skip-if-active: if a session is already running when the timer fires, that occurrence is skipped, the next occurrence is computed, and the cron continues. The cron persists across server restarts (skip-missed semantics on boot — no catchup spam). Delete with `cron_delete(id)`. Multiple crons per workspace are allowed.',
@@ -520,6 +535,18 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
520
535
  }
521
536
  return ok(result);
522
537
  }
538
+ if (name === 'set_workspace_name') {
539
+ const newName = a.name;
540
+ if (typeof newName !== 'string' || !newName.trim())
541
+ return fail('name parameter is required (non-empty)');
542
+ try {
543
+ const updated = (await backendRequest('PATCH', `/api/workspaces/${workspaceId}`, { name: newName }));
544
+ return ok({ ok: true, name: updated.name ?? newName.trim() });
545
+ }
546
+ catch (err) {
547
+ return fail(err instanceof Error ? err.message : String(err));
548
+ }
549
+ }
523
550
  if (name === 'cron_create') {
524
551
  const expression = a.expression;
525
552
  const prompt = a.prompt;
@@ -624,7 +624,8 @@ app.post('/', migrationGuard, async (c) => {
624
624
  if (criteria.length > 0) {
625
625
  brainstormPrompt += `\nAcceptance criteria:\n${criteria.map((t) => `- [${t.status === 'done' ? 'x' : ' '}] ${t.title}`).join('\n')}\n`;
626
626
  }
627
- brainstormPrompt += `\nYou have access to MCP tools via the 'kobo-tasks' server:\n`;
627
+ brainstormPrompt += `\nYou have access to MCP tools via the 'kobo-tasks' server. The bullets below are the main ones — your full kobo__ toolset is larger and is listed in your available tools; consult that list for the rest (dev-server control, search_codebase, documents, settings, session usage, …):\n`;
628
+ brainstormPrompt += `- kobo__set_workspace_name(name) — rename THIS workspace (the title shown in the sidebar). Call this ONLY when the user explicitly asks you to rename the workspace — never on your own initiative. Whitespace-trimmed, non-empty; distinct from agent_description.\n`;
628
629
  if (criteria.length > 0 || todos.length > 0) {
629
630
  brainstormPrompt += `- list_tasks() — list all tasks and criteria with their IDs and current status\n`;
630
631
  brainstormPrompt += `- mark_task_done(task_id) — mark a task or criterion as done\n`;
@@ -637,6 +638,9 @@ app.post('/', migrationGuard, async (c) => {
637
638
  brainstormPrompt += `- kobo__cron_create(expression, prompt, label?, mode?, oneShot?) — schedule a (recurring or one-shot) trigger on THIS workspace. At each fire Kōbō waits for the workspace to be idle and then injects \`prompt\` as the next user message. \`expression\` is a standard 5-field cron (\`min hour dom month dow\`) or a helper (\`@hourly\`, \`@daily\`, \`@weekly\`, \`@monthly\`, \`@yearly\`). Examples: \`*/30 * * * *\` = every 30 min; \`0 9 * * 1\` = every Monday at 9am; \`0 14 7 6 *\` = 7 June at 14:00. \`mode\` is \`'resume'\` (default — every fire continues the SAME conversation that scheduled the cron, so you can chain follow-ups) or \`'fresh'\` (every fire starts a brand-new session with a clean context, ideal for periodic checks like CI watch). \`oneShot\` (default false): when true, the cron cancels itself after the first real fire — use this to trigger once at a specific time without recurring. Skip-if-active: occurrences fired while a session is running are skipped, the next is computed, and the cron continues. Persists across restarts. Returns a cron \`id\`.\n`;
638
639
  brainstormPrompt += `- kobo__cron_delete(id) — cancel a previously-armed cron by id (idempotent).\n`;
639
640
  brainstormPrompt += `- kobo__cron_list() — list every cron currently armed on THIS workspace, with their next/last fire times.\n`;
641
+ brainstormPrompt += `- kobo__schedule_wakeup(delaySeconds, prompt, label?) — schedule a one-off follow-up turn on THIS workspace after a delay. End your turn normally; once the workspace is idle, Kōbō waits delaySeconds (clamped 60..21600 = 1min..6h) then resumes this same conversation by injecting prompt as your next message. Replaces any previously pending wakeup. Prefer this over the built-in ScheduleWakeup tool.\n`;
642
+ brainstormPrompt += `- kobo__cancel_wakeup() — cancel the pending wakeup on this workspace (idempotent).\n`;
643
+ brainstormPrompt += `\nForeground & waking yourself: you run in the FOREGROUND of an interactive session — do your work within the current turn. When a turn ends the workspace goes idle and NOTHING re-invokes you on its own: a background task or detached process finishing does not wake you. To wait for something (CI, a long build/install, a scheduled re-check) and continue later, schedule your own wake-up with kobo__schedule_wakeup (one-off delay) or kobo__cron_create (recurring/scheduled), then end the turn — Kōbō re-invokes you with your prompt when it fires.\n`;
640
644
  if (effectiveSettings.gitConventions) {
641
645
  brainstormPrompt += `\n# Git conventions\nIMPORTANT: Before any git operation (commit, branch, rebase, merge, push), read and apply the conventions defined in \`.ai/.git-conventions.md\`. They are project-specific and override any default behavior. Re-read this file if you're unsure or if context was compacted.\n`;
642
646
  }
@@ -2218,7 +2222,8 @@ app.get('/:id/git-stats', async (c) => {
2218
2222
  app.get('/:id/diff', async (c) => {
2219
2223
  try {
2220
2224
  const id = c.req.param('id');
2221
- const mode = c.req.query('mode') === 'unpushed' ? 'unpushed' : 'branch';
2225
+ const rawMode = c.req.query('mode');
2226
+ const mode = rawMode === 'unpushed' ? 'unpushed' : rawMode === 'commits' ? 'commits' : 'branch';
2222
2227
  // Opt-in flag from the diff viewer toggle. Only meaningful in `branch`
2223
2228
  // mode — `unpushed` is committed-only by definition.
2224
2229
  const includeUntracked = c.req.query('includeUntracked') === '1';
@@ -2227,6 +2232,25 @@ app.get('/:id/diff', async (c) => {
2227
2232
  return c.json({ error: `Workspace '${id}' not found` }, 404);
2228
2233
  }
2229
2234
  const worktreePath = workspace.worktreePath;
2235
+ if (mode === 'commits') {
2236
+ const from = c.req.query('from');
2237
+ const to = c.req.query('to');
2238
+ if (!from || !to) {
2239
+ return c.json({ error: 'mode=commits requires from and to query params' }, 400);
2240
+ }
2241
+ if (!gitOps.commitExists(worktreePath, to)) {
2242
+ return c.json({ error: `Invalid commit ref '${to}'` }, 400);
2243
+ }
2244
+ // `to` is strict (400 above). `from` is intentionally lenient: any ref it
2245
+ // can't resolve falls back to the empty tree (renders as all-added). The
2246
+ // only unresolved `from` the UI ever sends is a root commit's `<sha>^`
2247
+ // (single-commit diff of the first commit); the compare dialog otherwise
2248
+ // only offers refs that resolve.
2249
+ const fromRef = gitOps.commitExists(worktreePath, from) ? from : gitOps.EMPTY_TREE_SHA;
2250
+ const files = gitOps.getChangedFilesBetween(worktreePath, fromRef, to);
2251
+ c.header('Cache-Control', 'no-store');
2252
+ return c.json({ files, mode: 'commits', from: fromRef, to });
2253
+ }
2230
2254
  // Sync fetch in `branch` mode so the diff reflects upstream's HEAD, not a
2231
2255
  // stale local copy of the source branch. Best-effort: a failed fetch
2232
2256
  // (offline, no remote configured) still returns the diff against whatever
@@ -2268,6 +2292,30 @@ app.get('/:id/diff-file', (c) => {
2268
2292
  return c.json({ error: `Workspace '${id}' not found` }, 404);
2269
2293
  }
2270
2294
  const worktreePath = workspace.worktreePath;
2295
+ if (c.req.query('mode') === 'commits') {
2296
+ const from = c.req.query('from');
2297
+ const to = c.req.query('to');
2298
+ if (!from || !to) {
2299
+ return c.json({ error: 'mode=commits requires from and to query params' }, 400);
2300
+ }
2301
+ if (!gitOps.commitExists(worktreePath, to)) {
2302
+ return c.json({ error: `Invalid commit ref '${to}'` }, 400);
2303
+ }
2304
+ // `from` is lenient (empty-tree fallback) for a root commit's `<sha>^` —
2305
+ // see the matching note in the `/diff` commits branch above.
2306
+ const fromRef = gitOps.commitExists(worktreePath, from) ? from : gitOps.EMPTY_TREE_SHA;
2307
+ const original = gitOps.getFileAtRef(worktreePath, fromRef, filePath);
2308
+ const modified = gitOps.getFileAtRef(worktreePath, to, filePath);
2309
+ c.header('Cache-Control', 'no-store');
2310
+ return c.json({
2311
+ original: original ?? '',
2312
+ modified: modified ?? '',
2313
+ filePath,
2314
+ mode: 'commits',
2315
+ from: fromRef,
2316
+ to,
2317
+ });
2318
+ }
2271
2319
  const baseRef = mode === 'unpushed' ? `origin/${workspace.workingBranch}` : workspace.sourceBranch;
2272
2320
  const original = gitOps.getFileAtRef(worktreePath, baseRef, filePath);
2273
2321
  const modified = gitOps.getFileContent(worktreePath, filePath);
@@ -2568,14 +2616,18 @@ app.post('/:id/rebase', (c) => {
2568
2616
  const workspace = workspaceService.getWorkspace(id);
2569
2617
  if (!workspace)
2570
2618
  return c.json({ error: `Workspace '${id}' not found` }, 404);
2619
+ const autostash = c.req.query('autostash') === '1';
2571
2620
  const worktreePath = workspace.worktreePath;
2572
- gitOps.rebaseBranch(worktreePath, workspace.sourceBranch);
2621
+ gitOps.rebaseBranch(worktreePath, workspace.sourceBranch, { autostash });
2573
2622
  return c.json({ success: true });
2574
2623
  }
2575
2624
  catch (err) {
2576
2625
  if (err instanceof gitOps.GitConflictError) {
2577
2626
  return c.json({ error: err.message, conflict: true, operation: err.operation, files: err.files }, 409);
2578
2627
  }
2628
+ if (err instanceof gitOps.DirtyWorktreeError) {
2629
+ return c.json({ error: err.message, code: 'dirty_worktree', operation: err.operation, status: err.status }, 409);
2630
+ }
2579
2631
  const message = err instanceof Error ? err.message : String(err);
2580
2632
  return c.json({ error: message }, 500);
2581
2633
  }
@@ -2587,14 +2639,18 @@ app.post('/:id/merge', (c) => {
2587
2639
  const workspace = workspaceService.getWorkspace(id);
2588
2640
  if (!workspace)
2589
2641
  return c.json({ error: `Workspace '${id}' not found` }, 404);
2642
+ const autostash = c.req.query('autostash') === '1';
2590
2643
  const worktreePath = workspace.worktreePath;
2591
- gitOps.mergeBranch(worktreePath, workspace.sourceBranch);
2644
+ gitOps.mergeBranch(worktreePath, workspace.sourceBranch, { autostash });
2592
2645
  return c.json({ success: true });
2593
2646
  }
2594
2647
  catch (err) {
2595
2648
  if (err instanceof gitOps.GitConflictError) {
2596
2649
  return c.json({ error: err.message, conflict: true, operation: err.operation, files: err.files }, 409);
2597
2650
  }
2651
+ if (err instanceof gitOps.DirtyWorktreeError) {
2652
+ return c.json({ error: err.message, code: 'dirty_worktree', operation: err.operation, status: err.status }, 409);
2653
+ }
2598
2654
  const message = err instanceof Error ? err.message : String(err);
2599
2655
  return c.json({ error: message }, 500);
2600
2656
  }
@@ -2615,6 +2671,40 @@ app.post('/:id/git/abort', (c) => {
2615
2671
  return c.json({ error: message }, 500);
2616
2672
  }
2617
2673
  });
2674
+ /** Stage + commit all working-tree changes (recovery action for a dirty rebase/merge). */
2675
+ app.post('/:id/git/commit-all', async (c) => {
2676
+ try {
2677
+ const id = c.req.param('id');
2678
+ const workspace = workspaceService.getWorkspace(id);
2679
+ if (!workspace)
2680
+ return c.json({ error: `Workspace '${id}' not found` }, 404);
2681
+ const body = (await c.req.json().catch(() => ({})));
2682
+ const message = typeof body.message === 'string' ? body.message.trim() : '';
2683
+ if (!message)
2684
+ return c.json({ error: 'Commit message is required' }, 400);
2685
+ gitOps.commitAllChanges(workspace.worktreePath, message);
2686
+ return c.json({ success: true });
2687
+ }
2688
+ catch (err) {
2689
+ const message = err instanceof Error ? err.message : String(err);
2690
+ return c.json({ error: message }, 500);
2691
+ }
2692
+ });
2693
+ /** Discard working-tree changes (destructive recovery action for a dirty rebase/merge). */
2694
+ app.post('/:id/git/discard', (c) => {
2695
+ try {
2696
+ const id = c.req.param('id');
2697
+ const workspace = workspaceService.getWorkspace(id);
2698
+ if (!workspace)
2699
+ return c.json({ error: `Workspace '${id}' not found` }, 404);
2700
+ gitOps.discardWorkingTreeChanges(workspace.worktreePath);
2701
+ return c.json({ success: true });
2702
+ }
2703
+ catch (err) {
2704
+ const message = err instanceof Error ? err.message : String(err);
2705
+ return c.json({ error: message }, 500);
2706
+ }
2707
+ });
2618
2708
  /** Hand off merge/rebase conflicts to the workspace agent with an intelligent-resolution prompt. */
2619
2709
  app.post('/:id/git/resolve-with-agent', migrationGuard, async (c) => {
2620
2710
  try {
@@ -26,7 +26,17 @@ export function saveWorkspaceFile(worktreePath, relativePath, content, baseSha)
26
26
  }
27
27
  function resolveSafe(worktreePath, relativePath) {
28
28
  const abs = path.resolve(worktreePath, relativePath);
29
- const root = realpathSync(path.resolve(worktreePath));
29
+ const lexicalRoot = path.resolve(worktreePath);
30
+ // Lexical containment check FIRST: a path that escapes the worktree via `..`
31
+ // (or an absolute path) must be reported as an escape deterministically —
32
+ // independent of whether the traversal happens to land on a directory that
33
+ // exists on disk. Without this, `../../etc/passwd` surfaces as "parent
34
+ // directory does not exist" instead of "escapes the worktree" whenever the
35
+ // worktree sits deep enough that the `..` chain lands on a missing dir.
36
+ if (abs !== lexicalRoot && !abs.startsWith(lexicalRoot + path.sep)) {
37
+ throw new Error(`Path '${relativePath}' escapes the worktree`);
38
+ }
39
+ const root = realpathSync(lexicalRoot);
30
40
  const rootWithSep = root + path.sep;
31
41
  // Resolve the parent's realpath (parent must exist; if it doesn't, the path
32
42
  // is invalid anyway and we surface that). Then join the lexical basename so
@@ -143,11 +143,18 @@ export const DEFAULT_TEMPLATES = [
143
143
  `3. **Auto-loop (opt-in)** — Kōbō re-spawns a fresh session per task; each iteration sees a clean context\n` +
144
144
  `4. **Completed / Archived** — the workspace freezes; the worktree stays available read-only\n\n` +
145
145
  `# Kōbō MCP tools (always namespaced \`kobo__…\`)\n` +
146
+ `These are the main tools — the full \`kobo__\` set is larger and is listed in your available tools; consult that list for the rest (dev-server, search_codebase, documents, settings, session usage…).\n` +
146
147
  `- \`kobo__list_tasks\` / \`create_task\` / \`update_task\` / \`mark_task_done\` / \`delete_task\` — manage the visible task list\n` +
147
148
  `- \`kobo__set_workspace_agent_description\` — short one-line summary shown in the sidebar; keep it current\n` +
149
+ `- \`kobo__set_workspace_name\` — rename this workspace (the sidebar title); ONLY when the user explicitly asks for a rename, never on your own\n` +
148
150
  `- \`kobo__get_workspace_info\` / \`kobo__get_git_info\` — read workspace metadata + git state\n` +
149
151
  `- \`kobo__cron_create\` / \`cron_delete\` / \`cron_list\` — schedule recurring or one-shot triggers on THIS workspace\n` +
152
+ `- \`kobo__schedule_wakeup\` / \`cancel_wakeup\` — pause now and resume this same session after a one-off delay\n` +
150
153
  `- \`kobo__mark_auto_loop_ready\` — flip the loop into auto-execution after grooming\n\n` +
154
+ `# Foreground & waking yourself\n` +
155
+ `- You run in the FOREGROUND of an interactive session — do your work within the current turn\n` +
156
+ `- When a turn ends the workspace goes idle; nothing re-invokes you automatically. A background task or detached process finishing does NOT wake you\n` +
157
+ `- To wait and continue later (CI, long build, scheduled check), schedule your own wake-up: \`kobo__schedule_wakeup\` (one-off delay) or \`kobo__cron_create\` (recurring), then end the turn\n\n` +
151
158
  `# Conventions\n` +
152
159
  `- \`CLAUDE.md\` / \`AGENTS.md\` at the project root override default behavior — read them first\n` +
153
160
  `- \`.ai/.git-conventions.md\` (when present) defines per-project commit / branch rules — apply them on every git op\n` +
@@ -1,16 +1,20 @@
1
+ import { execFile } from 'node:child_process';
1
2
  import { promises as fs } from 'node:fs';
2
3
  import os from 'node:os';
3
4
  import path from 'node:path';
4
5
  const API_URL = 'https://api.anthropic.com/api/oauth/usage';
5
6
  const BETA_HEADER = 'oauth-2025-04-20';
6
7
  const FETCH_TIMEOUT_MS = 10_000;
8
+ // macOS stores the Claude Code OAuth credentials in the login Keychain under
9
+ // this service name, as the JSON we'd otherwise read from .credentials.json.
10
+ const KEYCHAIN_SERVICE = 'Claude Code-credentials';
11
+ const KEYCHAIN_TIMEOUT_MS = 5_000;
7
12
  function credentialsFilePath() {
8
13
  const dir = process.env.CLAUDE_CONFIG_DIR ?? path.join(os.homedir(), '.claude');
9
14
  return path.join(dir, '.credentials.json');
10
15
  }
11
- async function readAccessToken() {
16
+ function parseAccessToken(raw) {
12
17
  try {
13
- const raw = await fs.readFile(credentialsFilePath(), 'utf8');
14
18
  const parsed = JSON.parse(raw);
15
19
  const token = parsed?.claudeAiOauth?.accessToken;
16
20
  return typeof token === 'string' && token.length > 0 ? token : null;
@@ -19,6 +23,33 @@ async function readAccessToken() {
19
23
  return null;
20
24
  }
21
25
  }
26
+ async function readTokenFromFile() {
27
+ try {
28
+ return parseAccessToken(await fs.readFile(credentialsFilePath(), 'utf8'));
29
+ }
30
+ catch {
31
+ return null;
32
+ }
33
+ }
34
+ /**
35
+ * macOS keeps the Claude Code OAuth credentials in the login Keychain instead of
36
+ * `~/.claude/.credentials.json`. Read them via the `security` CLI. Best-effort:
37
+ * any failure — item absent, Keychain locked (e.g. a headless SSH session with
38
+ * no GUI to unlock it), or timeout — resolves to null so the caller falls back
39
+ * to the unauthenticated state and never hangs the usage poller.
40
+ */
41
+ async function readTokenFromKeychain() {
42
+ if (process.platform !== 'darwin')
43
+ return null;
44
+ return new Promise((resolve) => {
45
+ execFile('security', ['find-generic-password', '-s', KEYCHAIN_SERVICE, '-w'], { timeout: KEYCHAIN_TIMEOUT_MS }, (err, stdout) => {
46
+ resolve(err ? null : parseAccessToken(stdout));
47
+ });
48
+ });
49
+ }
50
+ async function readAccessToken() {
51
+ return (await readTokenFromFile()) ?? (await readTokenFromKeychain());
52
+ }
22
53
  function mapBucket(id, raw) {
23
54
  const utilization = typeof raw?.utilization === 'number' ? raw.utilization : 0;
24
55
  const resetsAt = typeof raw?.resets_at === 'string' ? raw.resets_at : undefined;
@@ -175,6 +175,20 @@ export class GitConflictError extends Error {
175
175
  this.files = files;
176
176
  }
177
177
  }
178
+ /** Thrown when a rebase or merge is refused because the working tree has
179
+ * uncommitted changes (staged or modified tracked files). Detected
180
+ * locale-independently from the working-tree status, never from git's
181
+ * localized error text. */
182
+ export class DirtyWorktreeError extends Error {
183
+ operation;
184
+ status;
185
+ constructor(operation, status) {
186
+ super(`${operation} blocked by uncommitted changes`);
187
+ this.name = 'DirtyWorktreeError';
188
+ this.operation = operation;
189
+ this.status = status;
190
+ }
191
+ }
178
192
  /** List files currently in a conflicted state (unmerged paths). */
179
193
  export function getConflictedFiles(repoPath) {
180
194
  try {
@@ -205,8 +219,10 @@ export function getOngoingGitOperation(repoPath) {
205
219
  return null;
206
220
  }
207
221
  }
208
- /** Rebase the current branch onto the given base branch. Fetches origin first. Leaves conflicts in place. */
209
- export function rebaseBranch(repoPath, baseBranch) {
222
+ /** Rebase the current branch onto the given base branch. Fetches origin first.
223
+ * With `opts.autostash`, dirty changes are stashed/re-applied automatically.
224
+ * Leaves conflicts in place. */
225
+ export function rebaseBranch(repoPath, baseBranch, opts) {
210
226
  try {
211
227
  git(repoPath, ['fetch', 'origin', baseBranch]);
212
228
  }
@@ -214,7 +230,11 @@ export function rebaseBranch(repoPath, baseBranch) {
214
230
  // fetch may fail if offline — continue with local ref
215
231
  }
216
232
  try {
217
- git(repoPath, ['rebase', `origin/${baseBranch}`]);
233
+ const args = ['rebase'];
234
+ if (opts?.autostash)
235
+ args.push('--autostash');
236
+ args.push(`origin/${baseBranch}`);
237
+ git(repoPath, args);
218
238
  }
219
239
  catch (err) {
220
240
  const conflicted = getConflictedFiles(repoPath);
@@ -222,12 +242,19 @@ export function rebaseBranch(repoPath, baseBranch) {
222
242
  // Leave the rebase in progress so the caller can abort or request agent-assisted resolution.
223
243
  throw new GitConflictError('rebase', conflicted);
224
244
  }
245
+ const status = getWorkingTreeStatus(repoPath);
246
+ if (status.staged > 0 || status.modified > 0) {
247
+ // git refused before touching anything because the tree is dirty.
248
+ throw new DirtyWorktreeError('rebase', status);
249
+ }
225
250
  const message = err instanceof Error ? err.message : String(err);
226
251
  throw new Error(`Rebase onto '${baseBranch}' failed: ${message}`);
227
252
  }
228
253
  }
229
- /** Merge `origin/<baseBranch>` into the current branch. Fetches first. Leaves conflicts in place. */
230
- export function mergeBranch(repoPath, baseBranch) {
254
+ /** Merge `origin/<baseBranch>` into the current branch. Fetches first.
255
+ * With `opts.autostash`, dirty changes are stashed/re-applied automatically.
256
+ * Leaves conflicts in place. */
257
+ export function mergeBranch(repoPath, baseBranch, opts) {
231
258
  try {
232
259
  git(repoPath, ['fetch', 'origin', baseBranch]);
233
260
  }
@@ -235,13 +262,21 @@ export function mergeBranch(repoPath, baseBranch) {
235
262
  // offline — continue with local ref
236
263
  }
237
264
  try {
238
- git(repoPath, ['merge', '--no-ff', '--no-edit', `origin/${baseBranch}`]);
265
+ const args = ['merge', '--no-ff', '--no-edit'];
266
+ if (opts?.autostash)
267
+ args.push('--autostash');
268
+ args.push(`origin/${baseBranch}`);
269
+ git(repoPath, args);
239
270
  }
240
271
  catch (err) {
241
272
  const conflicted = getConflictedFiles(repoPath);
242
273
  if (conflicted.length > 0 || getOngoingGitOperation(repoPath) === 'merge') {
243
274
  throw new GitConflictError('merge', conflicted);
244
275
  }
276
+ const status = getWorkingTreeStatus(repoPath);
277
+ if (status.staged > 0 || status.modified > 0) {
278
+ throw new DirtyWorktreeError('merge', status);
279
+ }
245
280
  const message = err instanceof Error ? err.message : String(err);
246
281
  throw new Error(`Merge of 'origin/${baseBranch}' failed: ${message}`);
247
282
  }
@@ -697,6 +732,58 @@ export function getFileAtRef(repoPath, ref, filePath) {
697
732
  return null;
698
733
  }
699
734
  }
735
+ /** Git's canonical empty-tree object. Used as the diff base for a root commit
736
+ * (no parent), so it renders as all-added rather than erroring. */
737
+ export const EMPTY_TREE_SHA = '4b825dc642cb6eb9a060e54bf8d69288fbee4904';
738
+ /** True if `ref` resolves to a commit in the repo (SHA, `<sha>^`, `origin/<branch>`…). */
739
+ export function commitExists(repoPath, ref) {
740
+ try {
741
+ execFileSync('git', ['rev-parse', '--verify', '--quiet', `${ref}^{commit}`], {
742
+ cwd: repoPath,
743
+ stdio: ['pipe', 'pipe', 'pipe'],
744
+ });
745
+ return true;
746
+ }
747
+ catch {
748
+ return false;
749
+ }
750
+ }
751
+ /**
752
+ * List files changed between two commits, two-dot `fromRef..toRef` (the patch
753
+ * that turns `fromRef` into `toRef`). Committed history only — no working-tree
754
+ * or untracked entries (this is a historical diff). Same `DiffFile` shape as
755
+ * `getChangedFiles`. Refs are used verbatim (caller resolves/validates them).
756
+ */
757
+ export function getChangedFilesBetween(repoPath, fromRef, toRef) {
758
+ const files = [];
759
+ try {
760
+ const output = git(repoPath, ['diff', '--name-status', `${fromRef}..${toRef}`]);
761
+ for (const line of output.split('\n')) {
762
+ if (!line)
763
+ continue;
764
+ const [statusCode, ...pathParts] = line.split('\t');
765
+ if (!statusCode || pathParts.length === 0)
766
+ continue;
767
+ const filePath = (statusCode.startsWith('R') || statusCode.startsWith('C')
768
+ ? pathParts[pathParts.length - 1]
769
+ : pathParts[0])?.replace(/\/$/, '') ?? '';
770
+ if (!filePath)
771
+ continue;
772
+ let status = 'modified';
773
+ if (statusCode.startsWith('A'))
774
+ status = 'added';
775
+ else if (statusCode.startsWith('D'))
776
+ status = 'deleted';
777
+ else if (statusCode.startsWith('R'))
778
+ status = 'renamed';
779
+ files.push({ path: filePath, status });
780
+ }
781
+ }
782
+ catch {
783
+ // invalid refs / no diff → empty list
784
+ }
785
+ return files;
786
+ }
700
787
  /**
701
788
  * Reset a single file in the worktree to a sensible baseline. Cascade:
702
789
  * 1. `origin/<branchName>` if the remote ref AND the file exist there
@@ -904,3 +991,15 @@ export function stashPush(repoPath, label) {
904
991
  export function stashPop(repoPath) {
905
992
  git(repoPath, ['stash', 'pop']);
906
993
  }
994
+ /** Stage every change (tracked + untracked) and commit it. Hooks run normally
995
+ * (no --no-verify), per the project's commit conventions. */
996
+ export function commitAllChanges(repoPath, message) {
997
+ git(repoPath, ['add', '-A']);
998
+ git(repoPath, ['commit', '-m', message]);
999
+ }
1000
+ /** Discard staged + modified TRACKED changes (`git reset --hard HEAD`).
1001
+ * Untracked files are intentionally preserved — they don't block a
1002
+ * rebase/merge, and cleaning them would risk nuking .env / build artefacts. */
1003
+ export function discardWorkingTreeChanges(repoPath) {
1004
+ git(repoPath, ['reset', '--hard', 'HEAD']);
1005
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@loicngr/kobo",
3
- "version": "1.7.26",
3
+ "version": "1.7.28",
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{D as e,G as t,I as n,N as r,P as i,R as a,St as o,W as s,_ as c,at as l,d as u,et as d,f,g as p,l as m,p as h,r as g,u as _,xt as v,y,yt as b}from"./runtime-core.esm-bundler-D_RRiKBh.js";import{U as x,l as S,t as C}from"./QIcon-BmEX2rXO.js";import{c as w,s as T}from"./notifications-l1Pxijve.js";import{t as E}from"./QBtn-CoU-UC_j.js";import{n as D}from"./vue-i18n-Cq-KgjJC.js";import{_ as O,c as k,m as A,u as j}from"./index-D4b3k9DV.js";import{t as M}from"./QSpinnerDots-DspFKwCZ.js";import{t as N}from"./QTooltip-CwBZU_bs.js";import{t as ee}from"./QExpansionItem-CiBP4NiY.js";import{t as te}from"./QScrollArea-CZVgBUBp.js";import{i as ne,n as re,t as P}from"./render-chat-markdown-C9SvAdWB.js";import{t as F}from"./documents-CX2-4fhr.js";import{t as I}from"./_plugin-vue_export-helper-r4mAJOHR.js";function ie(e,t,n=!0){let r=[],i=new Map,a=new Map;for(let n=0;n<e.length;n++){let o=e[n],s=t?.[n];switch(o.kind){case`message:text`:{let e=i.get(o.messageId);if(e)e.text+=o.text,e.streaming=o.streaming;else{let e={type:`text`,messageId:o.messageId,text:o.text,streaming:o.streaming,ts:s};i.set(o.messageId,e),r.push(e)}break}case`message:end`:{let e=i.get(o.messageId);e&&(e.streaming=!1);break}case`message:thinking`:r.push({type:`thinking`,messageId:o.messageId,text:o.text,ts:s});break;case`tool:call`:{let e={type:`tool`,toolCallId:o.toolCallId,name:o.name,input:o.input,ts:s};a.set(o.toolCallId,e),r.push(e);break}case`tool:result`:{let e=a.get(o.toolCallId);e&&(e.result={output:o.output,isError:o.isError});break}case`session:started`:r.push({type:`session`,kind:`started`,detail:{engineSessionId:o.engineSessionId,model:o.model},ts:s});break;case`session:ended`:r.push({type:`session`,kind:`ended`,detail:{reason:o.reason,exitCode:o.exitCode},ts:s});break;case`session:compacted`:r.push({type:`session`,kind:`compacted`,ts:s});break;case`session:brainstorm-complete`:case`session:user-input-requested`:case`message:raw`:case`skills:discovered`:case`usage`:case`rate_limit`:case`subagent:progress`:case`error`:break;default:}}let o=null;for(let e of r)e.type===`text`&&e.streaming&&(o&&(o.streaming=!1),o=e);return o&&!n&&(o.streaming=!1),r}function ae(e,t){if(t.length===0)return e;let n=t.map(e=>({type:`user`,content:e.content,sender:e.sender,ts:e.ts})),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 L=new Set([`setup`,`cleanup`,`archive`]);function R(e){switch(e.type){case`user`:return e.sender===`system-prompt`?`system-prompt`:L.has(e.sender)?`script`:`user`;case`session`:return`session`;default:return`agent`}}function oe(e){let t=[],n=null,r=null;for(let i of e){let e=R(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 z={class:`text-caption text-grey-6`},B=y({__name:`SessionEventItem`,props:{item:{}},setup(e){let t=e,r=m(()=>{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)=>(n(),h(`span`,z,o(e.$t(r.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){V(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 V(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=H(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 H(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 U=[`innerHTML`],ce=I(y({__name:`TextMessageItem`,props:{item:{}},setup(e){let t=e,r=F(),i=k(),a=m(()=>{let e=i.selectedWorkspaceId;return e?r.documentsFor(e).map(e=>e.path):[]}),o=m(()=>re(se(ne.parse(t.item.text,{async:!1,breaks:!0,gfm:!0}),a.value),{addAttr:[`data-document-path`]}));function s(e){let t=e.target?.closest(`.document-link`);if(!t)return;e.preventDefault();let n=t.getAttribute(`data-document-path`),a=i.selectedWorkspaceId;!n||!a||r.openDocumentByPath(a,n)}return(t,r)=>(n(),h(`div`,{class:`markdown-message`,onClick:s},[_(`div`,{innerHTML:o.value},null,8,U),e.item.streaming?(n(),u(S,{key:0,size:`xs`,class:`q-ml-xs`})):f(``,!0)]))}}),[[`__scopeId`,`data-v-1b7bd8ca`]]),W={key:0,class:`text-caption text-grey-5`,style:{"font-style":`italic`}},G=[`innerHTML`],K={key:1,style:{"white-space":`pre-wrap`}},le=I(y({__name:`ThinkingItem`,props:{item:{}},setup(e){let r=e,i=m(()=>r.item.text.trim().slice(0,100)),a=m(()=>r.item.text.trim().length>0),s=m(()=>r.item.text.trim().length>100),c=m(()=>P(r.item.text));return(r,l)=>a.value?(n(),h(`div`,W,[s.value?(n(),u(ee,{key:0,dense:``,"dense-toggle":``,label:i.value,"header-class":`text-grey-5 text-caption`,style:{"font-style":`italic`}},{default:t(()=>[_(`div`,{class:`q-py-xs markdown-thinking`,innerHTML:c.value},null,8,G)]),_:1},8,[`label`])):(n(),h(`span`,K,o(e.item.text),1))])):f(``,!0)}}),[[`__scopeId`,`data-v-7f45ed94`]]);function ue(e,t){let n=e.split(`
1
+ import{D as e,G as t,I as n,N as r,P as i,R as a,St as o,W as s,_ as c,at as l,d as u,et as d,f,g as p,l as m,p as h,r as g,u as _,xt as v,y,yt as b}from"./runtime-core.esm-bundler-D_RRiKBh.js";import{U as x,l as S,t as C}from"./QIcon-BmEX2rXO.js";import{c as w,s as T}from"./notifications-l1Pxijve.js";import{t as E}from"./QBtn-CoU-UC_j.js";import{n as D}from"./vue-i18n-Cq-KgjJC.js";import{_ as O,c as k,m as A,u as j}from"./index-TewFyoT4.js";import{t as M}from"./QSpinnerDots-DspFKwCZ.js";import{t as N}from"./QTooltip-CwBZU_bs.js";import{t as ee}from"./QExpansionItem-CiBP4NiY.js";import{t as te}from"./QScrollArea-CZVgBUBp.js";import{i as ne,n as re,t as P}from"./render-chat-markdown-G7XwL3vR.js";import{t as F}from"./documents-CX2-4fhr.js";import{t as I}from"./_plugin-vue_export-helper-r4mAJOHR.js";function ie(e,t,n=!0){let r=[],i=new Map,a=new Map;for(let n=0;n<e.length;n++){let o=e[n],s=t?.[n];switch(o.kind){case`message:text`:{let e=i.get(o.messageId);if(e)e.text+=o.text,e.streaming=o.streaming;else{let e={type:`text`,messageId:o.messageId,text:o.text,streaming:o.streaming,ts:s};i.set(o.messageId,e),r.push(e)}break}case`message:end`:{let e=i.get(o.messageId);e&&(e.streaming=!1);break}case`message:thinking`:r.push({type:`thinking`,messageId:o.messageId,text:o.text,ts:s});break;case`tool:call`:{let e={type:`tool`,toolCallId:o.toolCallId,name:o.name,input:o.input,ts:s};a.set(o.toolCallId,e),r.push(e);break}case`tool:result`:{let e=a.get(o.toolCallId);e&&(e.result={output:o.output,isError:o.isError});break}case`session:started`:r.push({type:`session`,kind:`started`,detail:{engineSessionId:o.engineSessionId,model:o.model},ts:s});break;case`session:ended`:r.push({type:`session`,kind:`ended`,detail:{reason:o.reason,exitCode:o.exitCode},ts:s});break;case`session:compacted`:r.push({type:`session`,kind:`compacted`,ts:s});break;case`session:brainstorm-complete`:case`session:user-input-requested`:case`message:raw`:case`skills:discovered`:case`usage`:case`rate_limit`:case`subagent:progress`:case`error`:break;default:}}let o=null;for(let e of r)e.type===`text`&&e.streaming&&(o&&(o.streaming=!1),o=e);return o&&!n&&(o.streaming=!1),r}function ae(e,t){if(t.length===0)return e;let n=t.map(e=>({type:`user`,content:e.content,sender:e.sender,ts:e.ts})),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 L=new Set([`setup`,`cleanup`,`archive`]);function R(e){switch(e.type){case`user`:return e.sender===`system-prompt`?`system-prompt`:L.has(e.sender)?`script`:`user`;case`session`:return`session`;default:return`agent`}}function oe(e){let t=[],n=null,r=null;for(let i of e){let e=R(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 z={class:`text-caption text-grey-6`},B=y({__name:`SessionEventItem`,props:{item:{}},setup(e){let t=e,r=m(()=>{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)=>(n(),h(`span`,z,o(e.$t(r.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){V(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 V(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=H(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 H(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 U=[`innerHTML`],ce=I(y({__name:`TextMessageItem`,props:{item:{}},setup(e){let t=e,r=F(),i=k(),a=m(()=>{let e=i.selectedWorkspaceId;return e?r.documentsFor(e).map(e=>e.path):[]}),o=m(()=>re(se(ne.parse(t.item.text,{async:!1,breaks:!0,gfm:!0}),a.value),{addAttr:[`data-document-path`]}));function s(e){let t=e.target?.closest(`.document-link`);if(!t)return;e.preventDefault();let n=t.getAttribute(`data-document-path`),a=i.selectedWorkspaceId;!n||!a||r.openDocumentByPath(a,n)}return(t,r)=>(n(),h(`div`,{class:`markdown-message`,onClick:s},[_(`div`,{innerHTML:o.value},null,8,U),e.item.streaming?(n(),u(S,{key:0,size:`xs`,class:`q-ml-xs`})):f(``,!0)]))}}),[[`__scopeId`,`data-v-1b7bd8ca`]]),W={key:0,class:`text-caption text-grey-5`,style:{"font-style":`italic`}},G=[`innerHTML`],K={key:1,style:{"white-space":`pre-wrap`}},le=I(y({__name:`ThinkingItem`,props:{item:{}},setup(e){let r=e,i=m(()=>r.item.text.trim().slice(0,100)),a=m(()=>r.item.text.trim().length>0),s=m(()=>r.item.text.trim().length>100),c=m(()=>P(r.item.text));return(r,l)=>a.value?(n(),h(`div`,W,[s.value?(n(),u(ee,{key:0,dense:``,"dense-toggle":``,label:i.value,"header-class":`text-grey-5 text-caption`,style:{"font-style":`italic`}},{default:t(()=>[_(`div`,{class:`q-py-xs markdown-thinking`,innerHTML:c.value},null,8,G)]),_:1},8,[`label`])):(n(),h(`span`,K,o(e.item.text),1))])):f(``,!0)}}),[[`__scopeId`,`data-v-7f45ed94`]]);function ue(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 de(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 fe(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=de(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{G as e,I as t,N as n,R as r,St as i,_ as a,at as o,d as s,et as c,f as l,p as u,r as d,u as f,y as p}from"./runtime-core.esm-bundler-D_RRiKBh.js";import{t as m}from"./QBtn-CoU-UC_j.js";import{b as h,i as g,x as _}from"./index-D4b3k9DV.js";import{t as v}from"./render-chat-markdown-C9SvAdWB.js";import{t as y}from"./_plugin-vue_export-helper-r4mAJOHR.js";import{t as b}from"./QSpace-Crcx82On.js";import{t as x}from"./QChip-D2TVel5I.js";import{t as S}from"./QPage-3-ah4oor.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(p({__name:`ChangelogPage`,setup(p){let y=g(),N=c([]),P=c(``),F=c(!1),I=c(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 n(R),(n,c)=>(t(),s(S,{class:`q-pa-md`,style:{"max-width":`900px`,margin:`0 auto`}},{default:e(()=>[f(`div`,C,[a(m,{flat:``,dense:``,round:``,icon:`arrow_back`,onClick:c[0]||=e=>o(y).back()}),f(`div`,w,i(n.$t(`changelog.title`)),1),a(b),a(m,{flat:``,dense:``,icon:`refresh`,loading:F.value,label:n.$t(`common.refresh`),onClick:R},null,8,[`loading`,`label`])]),F.value&&N.value.length===0?(t(),u(`div`,T,i(n.$t(`common.loading`)),1)):I.value?(t(),u(`div`,E,i(I.value),1)):N.value.length===0?(t(),u(`div`,D,i(n.$t(`changelog.empty`)),1)):(t(),u(`div`,O,[P.value?(t(),u(`div`,k,i(n.$t(`changelog.currentVersion`,{version:P.value})),1)):l(``,!0),(t(!0),u(d,null,r(N.value,r=>(t(),s(h,{key:r.version,dark:``,flat:``,bordered:``},{default:e(()=>[a(_,null,{default:e(()=>[f(`div`,A,[f(`div`,j,` v`+i(r.version),1),r.version===P.value?(t(),s(x,{key:0,dense:``,size:`sm`,color:`indigo-7`,"text-color":`grey-2`,label:n.$t(`changelog.current`),class:`q-ml-sm`},null,8,[`label`])):l(``,!0)]),f(`div`,{class:`changelog-notes`,innerHTML:L(r.notes)},null,8,M)]),_:2},1024)]),_:2},1024))),128))]))]),_:1}))}}),[[`__scopeId`,`data-v-ed73d661`]]);export{N as default};
1
+ import{G as e,I as t,N as n,R as r,St as i,_ as a,at as o,d as s,et as c,f as l,p as u,r as d,u as f,y as p}from"./runtime-core.esm-bundler-D_RRiKBh.js";import{t as m}from"./QBtn-CoU-UC_j.js";import{b as h,i as g,x as _}from"./index-TewFyoT4.js";import{t as v}from"./render-chat-markdown-G7XwL3vR.js";import{t as y}from"./_plugin-vue_export-helper-r4mAJOHR.js";import{t as b}from"./QSpace-Crcx82On.js";import{t as x}from"./QChip-5bnjPnRz.js";import{t as S}from"./QPage-3-ah4oor.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(p({__name:`ChangelogPage`,setup(p){let y=g(),N=c([]),P=c(``),F=c(!1),I=c(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 n(R),(n,c)=>(t(),s(S,{class:`q-pa-md`,style:{"max-width":`900px`,margin:`0 auto`}},{default:e(()=>[f(`div`,C,[a(m,{flat:``,dense:``,round:``,icon:`arrow_back`,onClick:c[0]||=e=>o(y).back()}),f(`div`,w,i(n.$t(`changelog.title`)),1),a(b),a(m,{flat:``,dense:``,icon:`refresh`,loading:F.value,label:n.$t(`common.refresh`),onClick:R},null,8,[`loading`,`label`])]),F.value&&N.value.length===0?(t(),u(`div`,T,i(n.$t(`common.loading`)),1)):I.value?(t(),u(`div`,E,i(I.value),1)):N.value.length===0?(t(),u(`div`,D,i(n.$t(`changelog.empty`)),1)):(t(),u(`div`,O,[P.value?(t(),u(`div`,k,i(n.$t(`changelog.currentVersion`,{version:P.value})),1)):l(``,!0),(t(!0),u(d,null,r(N.value,r=>(t(),s(h,{key:r.version,dark:``,flat:``,bordered:``},{default:e(()=>[a(_,null,{default:e(()=>[f(`div`,A,[f(`div`,j,` v`+i(r.version),1),r.version===P.value?(t(),s(x,{key:0,dense:``,size:`sm`,color:`indigo-7`,"text-color":`grey-2`,label:n.$t(`changelog.current`),class:`q-ml-sm`},null,8,[`label`])):l(``,!0)]),f(`div`,{class:`changelog-notes`,innerHTML:L(r.notes)},null,8,M)]),_:2},1024)]),_:2},1024))),128))]))]),_:1}))}}),[[`__scopeId`,`data-v-ed73d661`]]);export{N as default};