@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.
- package/CHANGELOG.md +8 -0
- package/dist/mcp-server/kobo-tasks-server.js +27 -0
- package/dist/server/routes/workspaces.js +94 -4
- package/dist/server/services/file-editor-service.js +11 -1
- package/dist/server/services/templates-service.js +7 -0
- package/dist/server/services/usage/providers/claude-code.js +33 -2
- package/dist/server/utils/git-ops.js +105 -6
- package/package.json +1 -1
- package/src/client/dist/spa/assets/{ActivityFeed-kV5CrtOV.js → ActivityFeed-DW7Bv1Cx.js} +1 -1
- package/src/client/dist/spa/assets/{ChangelogPage-xdtvQVeE.js → ChangelogPage-vEYi6Byk.js} +1 -1
- package/src/client/dist/spa/assets/{CreatePage-DFtuX7-R.js → CreatePage-CSbZYNPW.js} +2 -2
- package/src/client/dist/spa/assets/{DiffViewer-C4L5y8Ho.css → DiffViewer-BuYD1EDP.css} +1 -1
- package/src/client/dist/spa/assets/DiffViewer-CkZgt7Fp.js +8 -0
- package/src/client/dist/spa/assets/{HealthPage-Dkrr4w8H.js → HealthPage-BLcP13YD.js} +1 -1
- package/src/client/dist/spa/assets/{MainLayout-D2fdfOct.css → MainLayout-CILSd-EM.css} +1 -1
- package/src/client/dist/spa/assets/{MainLayout-Bpqn8uw3.js → MainLayout-j-gqN_I5.js} +17 -17
- package/src/client/dist/spa/assets/QSelect-U9wJ7Z2A.js +36 -0
- package/src/client/dist/spa/assets/{SearchPage-BI4zhWxn.js → SearchPage-BPknEVtZ.js} +1 -1
- package/src/client/dist/spa/assets/SettingsPage-HhPqZAtG.js +9 -0
- package/src/client/dist/spa/assets/WorkspacePage-Co34Wjna.js +4 -0
- package/src/client/dist/spa/assets/{cssMode-Cj7dq4tA.js → cssMode-D_Z8-Q5h.js} +1 -1
- package/src/client/dist/spa/assets/{editor.api-DmE4Vf3n.js → editor.api-DgduwnV5.js} +1 -1
- package/src/client/dist/spa/assets/{editor.main-BtZbr82f.js → editor.main-C4_EYinG.js} +3 -3
- package/src/client/dist/spa/assets/{engineFeatures-3w5M3c1y.js → engineFeatures-B3_uxW5Z.js} +1 -1
- package/src/client/dist/spa/assets/{expand-template-Cvhhn0Oi.js → expand-template-D7jp6KZJ.js} +1 -1
- package/src/client/dist/spa/assets/{freemarker2-D9gcKCMr.js → freemarker2-6aUXBO6n.js} +1 -1
- package/src/client/dist/spa/assets/{handlebars-BrcqC-zE.js → handlebars-D2zveC0w.js} +1 -1
- package/src/client/dist/spa/assets/{html-DpYYEfpW.js → html-C-zqkrrG.js} +1 -1
- package/src/client/dist/spa/assets/{htmlMode-B8fY-cpH.js → htmlMode-DE3gz2Wd.js} +1 -1
- package/src/client/dist/spa/assets/i18n-BmFuyRbK.js +1 -0
- package/src/client/dist/spa/assets/{index-D4b3k9DV.js → index-TewFyoT4.js} +7 -7
- package/src/client/dist/spa/assets/{javascript-Cty6LK6F.js → javascript-xiWyVH4o.js} +1 -1
- package/src/client/dist/spa/assets/{jsonMode-Dp8DQFxa.js → jsonMode-JTmi1bQa.js} +1 -1
- package/src/client/dist/spa/assets/kobo-commands-C0Y02P5A.js +9 -0
- package/src/client/dist/spa/assets/{liquid-BBh8fxMj.js → liquid-CT9b60IZ.js} +1 -1
- package/src/client/dist/spa/assets/{mdx-4Sg0uY19.js → mdx-QIdOXPHb.js} +1 -1
- package/src/client/dist/spa/assets/{monaco.contribution-BqWsUYYb.js → monaco.contribution-uzj4TvhF.js} +2 -2
- package/src/client/dist/spa/assets/permissionModes-BUhr5Wdx.js +1 -0
- package/src/client/dist/spa/assets/{python-CNVrhWpS.js → python-7sJU2SF0.js} +1 -1
- package/src/client/dist/spa/assets/{razor-CF0pOS5A.js → razor-kkOOm93b.js} +1 -1
- package/src/client/dist/spa/assets/{render-chat-markdown-C9SvAdWB.js → render-chat-markdown-G7XwL3vR.js} +1 -1
- package/src/client/dist/spa/assets/{tsMode-BtgiAz6G.js → tsMode-BY1pp4Yd.js} +1 -1
- package/src/client/dist/spa/assets/{typescript-C0__juCT.js → typescript-D3HSMsZe.js} +1 -1
- package/src/client/dist/spa/assets/{use-onboarding-D4TTIa1e.js → use-onboarding-CcwSDBsS.js} +1 -1
- package/src/client/dist/spa/assets/{xml-D0l5CGKt.js → xml-BC8zXouV.js} +1 -1
- package/src/client/dist/spa/assets/{yaml-CroybGxn.js → yaml-CeoIec1I.js} +1 -1
- package/src/client/dist/spa/index.html +1 -1
- package/src/mcp-server/kobo-tasks-server.ts +29 -0
- package/src/client/dist/spa/assets/DiffViewer-DQ5PexFb.js +0 -8
- package/src/client/dist/spa/assets/SettingsPage-BEMFsN0N.js +0 -9
- package/src/client/dist/spa/assets/WorkspacePage-CRs9kUM0.js +0 -4
- package/src/client/dist/spa/assets/i18n-D3e67G-j.js +0 -1
- package/src/client/dist/spa/assets/kobo-commands-DRDkhOO8.js +0 -9
- package/src/client/dist/spa/assets/permissionModes-cbEFVOC0.js +0 -1
- package/src/client/dist/spa/assets/skill-suite-prompts-8f_JW79j.js +0 -36
- /package/src/client/dist/spa/assets/{ClosePopup-0MWohgml.js → ClosePopup-BWYh08p9.js} +0 -0
- /package/src/client/dist/spa/assets/{QChip-D2TVel5I.js → QChip-5bnjPnRz.js} +0 -0
- /package/src/client/dist/spa/assets/{QList-CRYZxnPD.js → QList-Ch5K5W7r.js} +0 -0
- /package/src/client/dist/spa/assets/{QMenu-Yx1QEIHC.js → QMenu-CoPEAblj.js} +0 -0
- /package/src/client/dist/spa/assets/{build-path-tree-BKx2q92A.js → build-path-tree-B9aeh1tv.js} +0 -0
- /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
|
|
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
|
|
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
|
-
|
|
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.
|
|
209
|
-
|
|
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
|
-
|
|
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.
|
|
230
|
-
|
|
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
|
-
|
|
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.
|
|
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-
|
|
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-
|
|
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};
|