@loicngr/kobo 1.7.27 → 1.7.29

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 (63) hide show
  1. package/CHANGELOG.md +8 -0
  2. package/dist/mcp-server/kobo-tasks-server.js +27 -0
  3. package/dist/server/routes/templates.js +19 -1
  4. package/dist/server/routes/workspaces.js +70 -3
  5. package/dist/server/services/templates-service.js +35 -0
  6. package/dist/server/services/usage/providers/claude-code.js +33 -2
  7. package/dist/server/utils/git-ops.js +101 -3
  8. package/package.json +1 -1
  9. package/src/client/dist/spa/assets/{ActivityFeed-CY7iSUwF.js → ActivityFeed-rio_ru8s.js} +1 -1
  10. package/src/client/dist/spa/assets/{ChangelogPage-DWAmbFWX.js → ChangelogPage-Dk8nKtM0.js} +1 -1
  11. package/src/client/dist/spa/assets/{CreatePage-YT54FfA1.js → CreatePage-BmGIFI8W.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-deM4uqPU.js +8 -0
  14. package/src/client/dist/spa/assets/{HealthPage-DGh8w9iT.js → HealthPage-DAA51STa.js} +1 -1
  15. package/src/client/dist/spa/assets/{MainLayout-DKUsEDQ4.css → MainLayout-4vhZRrU2.css} +1 -1
  16. package/src/client/dist/spa/assets/{MainLayout-C0mTMDDv.js → MainLayout-DrY2_qyP.js} +17 -17
  17. package/src/client/dist/spa/assets/QSelect-Dm7qaLfk.js +36 -0
  18. package/src/client/dist/spa/assets/{SearchPage-pIX1EzKl.js → SearchPage-CMH0AoRw.js} +1 -1
  19. package/src/client/dist/spa/assets/SettingsPage-B7dDhXCS.css +1 -0
  20. package/src/client/dist/spa/assets/SettingsPage-DH-KTis9.js +9 -0
  21. package/src/client/dist/spa/assets/WorkspacePage-CH3k_9_s.js +4 -0
  22. package/src/client/dist/spa/assets/{cssMode-2ylLbTu-.js → cssMode-DmArionU.js} +1 -1
  23. package/src/client/dist/spa/assets/{editor.api-CadEvLPy.js → editor.api-CseR9xvC.js} +1 -1
  24. package/src/client/dist/spa/assets/{editor.main-C71mfCQt.js → editor.main-Yih57bDS.js} +3 -3
  25. package/src/client/dist/spa/assets/{engineFeatures-DM5FzkGJ.js → engineFeatures-d1kZ8NXB.js} +1 -1
  26. package/src/client/dist/spa/assets/{expand-template-B5Xg2o0V.js → expand-template-D_3H5Och.js} +1 -1
  27. package/src/client/dist/spa/assets/{freemarker2-BhNdcQrl.js → freemarker2-Cd59aTHU.js} +1 -1
  28. package/src/client/dist/spa/assets/{handlebars-e8nGt_F5.js → handlebars-CeswicPh.js} +1 -1
  29. package/src/client/dist/spa/assets/{html-3AMWBd0B.js → html-C3-kLMMe.js} +1 -1
  30. package/src/client/dist/spa/assets/{htmlMode-C_TeBjBg.js → htmlMode-DQxZ0n2e.js} +1 -1
  31. package/src/client/dist/spa/assets/i18n-IlYZHxVd.js +1 -0
  32. package/src/client/dist/spa/assets/{index-BHT72IUw.js → index-C0H1sHuD.js} +8 -8
  33. package/src/client/dist/spa/assets/{javascript-DNXV1m0n.js → javascript-0NpnFHx9.js} +1 -1
  34. package/src/client/dist/spa/assets/{jsonMode-CoKVM_Q0.js → jsonMode-Byj6sx5w.js} +1 -1
  35. package/src/client/dist/spa/assets/kobo-commands-B_DBKQxq.js +9 -0
  36. package/src/client/dist/spa/assets/{liquid-Bor4GGNF.js → liquid-CzcaXBnO.js} +1 -1
  37. package/src/client/dist/spa/assets/{mdx-ybAriXVK.js → mdx-Dl6UUR_U.js} +1 -1
  38. package/src/client/dist/spa/assets/{monaco.contribution-BDBos_h5.js → monaco.contribution-mzaMwnEg.js} +2 -2
  39. package/src/client/dist/spa/assets/permissionModes-BUhr5Wdx.js +1 -0
  40. package/src/client/dist/spa/assets/{python-B8yv8Fk-.js → python-BFAu7VF2.js} +1 -1
  41. package/src/client/dist/spa/assets/{razor-BCfu0JXU.js → razor-BET2CB_3.js} +1 -1
  42. package/src/client/dist/spa/assets/{render-chat-markdown-sjnPe3Vu.js → render-chat-markdown-DsbgZODM.js} +1 -1
  43. package/src/client/dist/spa/assets/{tsMode-Cha-D9yo.js → tsMode-ORpv-QPD.js} +1 -1
  44. package/src/client/dist/spa/assets/{typescript-DPQ0P6qa.js → typescript-DGcONjjW.js} +1 -1
  45. package/src/client/dist/spa/assets/{use-onboarding-B6FZK2Yn.js → use-onboarding-B9TeTfkj.js} +1 -1
  46. package/src/client/dist/spa/assets/{xml-pWJPZqZ8.js → xml-Cs4DTg3L.js} +1 -1
  47. package/src/client/dist/spa/assets/{yaml-YsHRVX_S.js → yaml-HxNN05si.js} +1 -1
  48. package/src/client/dist/spa/index.html +1 -1
  49. package/src/mcp-server/kobo-tasks-server.ts +29 -0
  50. package/src/client/dist/spa/assets/DiffViewer-DTthEOys.js +0 -8
  51. package/src/client/dist/spa/assets/SettingsPage-C64_E1oJ.css +0 -1
  52. package/src/client/dist/spa/assets/SettingsPage-COBWjs_d.js +0 -9
  53. package/src/client/dist/spa/assets/WorkspacePage-CTaTo1Wh.js +0 -4
  54. package/src/client/dist/spa/assets/i18n-BPs_6dhe.js +0 -1
  55. package/src/client/dist/spa/assets/kobo-commands-DRDkhOO8.js +0 -9
  56. package/src/client/dist/spa/assets/permissionModes-Cebx7fq6.js +0 -1
  57. package/src/client/dist/spa/assets/skill-suite-prompts-8f_JW79j.js +0 -36
  58. /package/src/client/dist/spa/assets/{ClosePopup-0MWohgml.js → ClosePopup-BWYh08p9.js} +0 -0
  59. /package/src/client/dist/spa/assets/{QChip-D2TVel5I.js → QChip-5bnjPnRz.js} +0 -0
  60. /package/src/client/dist/spa/assets/{QList-CRYZxnPD.js → QList-Ch5K5W7r.js} +0 -0
  61. /package/src/client/dist/spa/assets/{QMenu-Yx1QEIHC.js → QMenu-CoPEAblj.js} +0 -0
  62. /package/src/client/dist/spa/assets/{build-path-tree-BKx2q92A.js → build-path-tree-B9aeh1tv.js} +0 -0
  63. /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.29
8
+
9
+ - feat: git working-tree tooling, template reset, diff label
10
+
11
+ ## 1.7.28
12
+
13
+ - feat: commit diff review, workspace rename tool, macOS usage keychain
14
+
7
15
  ## 1.7.27
8
16
 
9
17
  - feat(git): add dirty-worktree recovery for rebase/merge
@@ -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;
@@ -5,7 +5,10 @@ const app = new Hono();
5
5
  // GET /api/templates — list all templates
6
6
  app.get('/', (c) => {
7
7
  try {
8
- return c.json({ templates: templatesService.listTemplates() });
8
+ return c.json({
9
+ templates: templatesService.listTemplates(),
10
+ defaultSlugs: templatesService.getDefaultTemplateSlugs(),
11
+ });
9
12
  }
10
13
  catch (err) {
11
14
  const message = err instanceof Error ? err.message : String(err);
@@ -75,6 +78,21 @@ app.post('/reload-defaults', (c) => {
75
78
  return c.json({ error: message }, 500);
76
79
  }
77
80
  });
81
+ // POST /api/templates/:slug/reset-default — restore a default template to its built-in content
82
+ app.post('/:slug/reset-default', (c) => {
83
+ try {
84
+ const slug = c.req.param('slug');
85
+ const template = templatesService.resetTemplateToDefault(slug);
86
+ if (!template) {
87
+ return c.json({ error: `Template '${slug}' is not a default template` }, 404);
88
+ }
89
+ return c.json({ template });
90
+ }
91
+ catch (err) {
92
+ const message = err instanceof Error ? err.message : String(err);
93
+ return c.json({ error: message }, 500);
94
+ }
95
+ });
78
96
  // DELETE /api/templates/:slug — delete a template
79
97
  app.delete('/:slug', (c) => {
80
98
  try {
@@ -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);
@@ -2373,6 +2421,21 @@ app.get('/:id/commits', async (c) => {
2373
2421
  return c.json({ error: message }, 500);
2374
2422
  }
2375
2423
  });
2424
+ // GET /:id/working-tree-files — list uncommitted working-tree files (read-only)
2425
+ app.get('/:id/working-tree-files', (c) => {
2426
+ try {
2427
+ const id = c.req.param('id');
2428
+ const workspace = workspaceService.getWorkspace(id);
2429
+ if (!workspace)
2430
+ return c.json({ error: `Workspace '${id}' not found` }, 404);
2431
+ const files = gitOps.getWorkingTreeFiles(workspace.worktreePath);
2432
+ return c.json({ files });
2433
+ }
2434
+ catch (err) {
2435
+ const message = err instanceof Error ? err.message : String(err);
2436
+ return c.json({ error: message }, 500);
2437
+ }
2438
+ });
2376
2439
  // POST /api/workspaces/:id/rename-branch { newName }
2377
2440
  // Rename the working branch in git, move the worktree dir to match, and
2378
2441
  // update the DB. Run as one atomic operation from the UI "Rename branch"
@@ -2544,10 +2607,14 @@ app.post('/:id/pull', (c) => {
2544
2607
  return c.json({ error: `Workspace '${id}' not found` }, 404);
2545
2608
  }
2546
2609
  const worktreePath = workspace.worktreePath;
2610
+ const autostash = c.req.query('autostash') === '1';
2547
2611
  try {
2548
- gitOps.pullBranch(worktreePath, workspace.workingBranch);
2612
+ gitOps.pullBranch(worktreePath, workspace.workingBranch, 'origin', { autostash });
2549
2613
  }
2550
2614
  catch (err) {
2615
+ if (err instanceof gitOps.DirtyWorktreeError) {
2616
+ return c.json({ error: err.message, code: 'dirty_worktree', operation: err.operation, status: err.status }, 409);
2617
+ }
2551
2618
  const message = err instanceof Error ? err.message : String(err);
2552
2619
  return c.json({ error: message }, 500);
2553
2620
  }
@@ -72,6 +72,34 @@ export function deleteTemplate(slug) {
72
72
  writeTemplates(next);
73
73
  return true;
74
74
  }
75
+ /** Slugs of every built-in default template (used to show the reset action). */
76
+ export function getDefaultTemplateSlugs() {
77
+ return DEFAULT_TEMPLATES.map((t) => t.slug);
78
+ }
79
+ /**
80
+ * Reset a default template back to its built-in content/description. Returns the
81
+ * updated template, or null if `slug` is not a default template. Recreates the
82
+ * row if it had been deleted.
83
+ */
84
+ export function resetTemplateToDefault(slug) {
85
+ const def = DEFAULT_TEMPLATES.find((d) => d.slug === slug);
86
+ if (!def)
87
+ return null;
88
+ const templates = listTemplates();
89
+ const now = new Date().toISOString();
90
+ const idx = templates.findIndex((t) => t.slug === slug);
91
+ let result;
92
+ if (idx >= 0) {
93
+ result = { ...templates[idx], description: def.description, content: def.content, updatedAt: now };
94
+ templates[idx] = result;
95
+ }
96
+ else {
97
+ result = { ...def, createdAt: now, updatedAt: now };
98
+ templates.push(result);
99
+ }
100
+ writeTemplates(templates);
101
+ return result;
102
+ }
75
103
  // ── Internals ──────────────────────────────────────────────────────────────
76
104
  function validateTemplateInput(input) {
77
105
  if (!SLUG_PATTERN.test(input.slug)) {
@@ -143,11 +171,18 @@ export const DEFAULT_TEMPLATES = [
143
171
  `3. **Auto-loop (opt-in)** — Kōbō re-spawns a fresh session per task; each iteration sees a clean context\n` +
144
172
  `4. **Completed / Archived** — the workspace freezes; the worktree stays available read-only\n\n` +
145
173
  `# Kōbō MCP tools (always namespaced \`kobo__…\`)\n` +
174
+ `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
175
  `- \`kobo__list_tasks\` / \`create_task\` / \`update_task\` / \`mark_task_done\` / \`delete_task\` — manage the visible task list\n` +
147
176
  `- \`kobo__set_workspace_agent_description\` — short one-line summary shown in the sidebar; keep it current\n` +
177
+ `- \`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
178
  `- \`kobo__get_workspace_info\` / \`kobo__get_git_info\` — read workspace metadata + git state\n` +
149
179
  `- \`kobo__cron_create\` / \`cron_delete\` / \`cron_list\` — schedule recurring or one-shot triggers on THIS workspace\n` +
180
+ `- \`kobo__schedule_wakeup\` / \`cancel_wakeup\` — pause now and resume this same session after a one-off delay\n` +
150
181
  `- \`kobo__mark_auto_loop_ready\` — flip the loop into auto-execution after grooming\n\n` +
182
+ `# Foreground & waking yourself\n` +
183
+ `- You run in the FOREGROUND of an interactive session — do your work within the current turn\n` +
184
+ `- 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` +
185
+ `- 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
186
  `# Conventions\n` +
152
187
  `- \`CLAUDE.md\` / \`AGENTS.md\` at the project root override default behavior — read them first\n` +
153
188
  `- \`.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;
@@ -153,10 +153,24 @@ export function fetchAllBranches(repoPath, remote = 'origin') {
153
153
  throw new Error(`Failed to fetch from '${remote}': ${message}`);
154
154
  }
155
155
  }
156
- /** Pull the current branch from the remote using fast-forward only. */
157
- export function pullBranch(repoPath, branchName, remote = 'origin') {
156
+ /** Pull the current branch from the remote using fast-forward only.
157
+ * With `opts.autostash`, dirty changes are stashed/re-applied automatically.
158
+ * Without it, a dirty tree (staged or modified tracked files) is refused up-front
159
+ * with a `DirtyWorktreeError` — same recovery path rebase/merge offer — instead of
160
+ * letting git fail with a localized message. Detected locale-independently. */
161
+ export function pullBranch(repoPath, branchName, remote = 'origin', opts) {
162
+ if (!opts?.autostash) {
163
+ const status = getWorkingTreeStatus(repoPath);
164
+ if (status.staged > 0 || status.modified > 0) {
165
+ throw new DirtyWorktreeError('pull', status);
166
+ }
167
+ }
158
168
  try {
159
- git(repoPath, ['pull', '--ff-only', remote, branchName]);
169
+ const args = ['pull', '--ff-only'];
170
+ if (opts?.autostash)
171
+ args.push('--autostash');
172
+ args.push(remote, branchName);
173
+ git(repoPath, args);
160
174
  }
161
175
  catch (err) {
162
176
  const message = err instanceof Error ? err.message : String(err);
@@ -732,6 +746,58 @@ export function getFileAtRef(repoPath, ref, filePath) {
732
746
  return null;
733
747
  }
734
748
  }
749
+ /** Git's canonical empty-tree object. Used as the diff base for a root commit
750
+ * (no parent), so it renders as all-added rather than erroring. */
751
+ export const EMPTY_TREE_SHA = '4b825dc642cb6eb9a060e54bf8d69288fbee4904';
752
+ /** True if `ref` resolves to a commit in the repo (SHA, `<sha>^`, `origin/<branch>`…). */
753
+ export function commitExists(repoPath, ref) {
754
+ try {
755
+ execFileSync('git', ['rev-parse', '--verify', '--quiet', `${ref}^{commit}`], {
756
+ cwd: repoPath,
757
+ stdio: ['pipe', 'pipe', 'pipe'],
758
+ });
759
+ return true;
760
+ }
761
+ catch {
762
+ return false;
763
+ }
764
+ }
765
+ /**
766
+ * List files changed between two commits, two-dot `fromRef..toRef` (the patch
767
+ * that turns `fromRef` into `toRef`). Committed history only — no working-tree
768
+ * or untracked entries (this is a historical diff). Same `DiffFile` shape as
769
+ * `getChangedFiles`. Refs are used verbatim (caller resolves/validates them).
770
+ */
771
+ export function getChangedFilesBetween(repoPath, fromRef, toRef) {
772
+ const files = [];
773
+ try {
774
+ const output = git(repoPath, ['diff', '--name-status', `${fromRef}..${toRef}`]);
775
+ for (const line of output.split('\n')) {
776
+ if (!line)
777
+ continue;
778
+ const [statusCode, ...pathParts] = line.split('\t');
779
+ if (!statusCode || pathParts.length === 0)
780
+ continue;
781
+ const filePath = (statusCode.startsWith('R') || statusCode.startsWith('C')
782
+ ? pathParts[pathParts.length - 1]
783
+ : pathParts[0])?.replace(/\/$/, '') ?? '';
784
+ if (!filePath)
785
+ continue;
786
+ let status = 'modified';
787
+ if (statusCode.startsWith('A'))
788
+ status = 'added';
789
+ else if (statusCode.startsWith('D'))
790
+ status = 'deleted';
791
+ else if (statusCode.startsWith('R'))
792
+ status = 'renamed';
793
+ files.push({ path: filePath, status });
794
+ }
795
+ }
796
+ catch {
797
+ // invalid refs / no diff → empty list
798
+ }
799
+ return files;
800
+ }
735
801
  /**
736
802
  * Reset a single file in the worktree to a sensible baseline. Cascade:
737
803
  * 1. `origin/<branchName>` if the remote ref AND the file exist there
@@ -830,6 +896,38 @@ export function getWorkingTreeStatus(repoPath) {
830
896
  return { staged: 0, modified: 0, untracked: 0 };
831
897
  }
832
898
  }
899
+ /**
900
+ * List uncommitted working-tree files with their status, parsed from
901
+ * `git status --porcelain`. Same classification rule as getWorkingTreeStatus.
902
+ * For renames (porcelain `old -> new`) the NEW path is kept. Best-effort: [] on error.
903
+ */
904
+ export function getWorkingTreeFiles(repoPath) {
905
+ try {
906
+ const output = git(repoPath, ['status', '--porcelain']);
907
+ const files = [];
908
+ for (const line of output.split('\n')) {
909
+ if (!line)
910
+ continue;
911
+ const x = line[0];
912
+ const y = line[1];
913
+ let filePath = line.slice(3);
914
+ const arrowIdx = filePath.indexOf(' -> ');
915
+ if (arrowIdx !== -1)
916
+ filePath = filePath.slice(arrowIdx + 4);
917
+ const untracked = x === '?' && y === '?';
918
+ files.push({
919
+ path: filePath,
920
+ staged: !untracked && x !== ' ' && x !== '?',
921
+ modified: !untracked && y !== ' ' && y !== '?',
922
+ untracked,
923
+ });
924
+ }
925
+ return files;
926
+ }
927
+ catch {
928
+ return [];
929
+ }
930
+ }
833
931
  /**
834
932
  * Count commits ahead of `origin/<workingBranch>`. Returns `-1` when the remote
835
933
  * ref does not exist (i.e. the branch has never been pushed).
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@loicngr/kobo",
3
- "version": "1.7.27",
3
+ "version": "1.7.29",
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-BHT72IUw.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-sjnPe3Vu.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-C0H1sHuD.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-DsbgZODM.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-BHT72IUw.js";import{t as v}from"./render-chat-markdown-sjnPe3Vu.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-C0H1sHuD.js";import{t as v}from"./render-chat-markdown-DsbgZODM.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};