@loicngr/kobo 1.7.22 → 1.7.24
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/AGENTS.md +11 -1
- package/CHANGELOG.md +14 -0
- package/README.md +31 -0
- package/dist/server/db/migrations.js +16 -0
- package/dist/server/db/schema.js +2 -0
- package/dist/server/routes/workspaces.js +40 -3
- package/dist/server/services/pr-watcher-service.js +40 -1
- package/dist/server/services/settings-service.js +11 -0
- package/dist/server/services/templates-service.js +32 -0
- package/dist/server/services/workspace-service.js +22 -0
- package/dist/server/services/worktree-purge-service.js +116 -0
- package/package.json +1 -1
- package/src/client/dist/spa/assets/{ActivityFeed-B73SoxPh.js → ActivityFeed-Dhj2Y5CQ.js} +1 -1
- package/src/client/dist/spa/assets/{ChangelogPage-DUU0PIjz.js → ChangelogPage-BqCf7Tdk.js} +1 -1
- package/src/client/dist/spa/assets/{ClosePopup-BG4U_b73.js → ClosePopup-0MWohgml.js} +1 -1
- package/src/client/dist/spa/assets/{CreatePage-CEDQHhlt.js → CreatePage-DhhwW2iK.js} +1 -1
- package/src/client/dist/spa/assets/{DiffViewer-DzUan5hw.js → DiffViewer-mgpaPemQ.js} +3 -3
- package/src/client/dist/spa/assets/{HealthPage-CywUBkCD.js → HealthPage-Dl-UGLDD.js} +1 -1
- package/src/client/dist/spa/assets/{MainLayout-BNlzhFX_.js → MainLayout-BiDZ_U51.js} +3 -3
- package/src/client/dist/spa/assets/{MainLayout-BxbC5qfx.css → MainLayout-D2fdfOct.css} +1 -1
- package/src/client/dist/spa/assets/{QExpansionItem-C3koMJhv.js → QExpansionItem-CiBP4NiY.js} +1 -1
- package/src/client/dist/spa/assets/{QMenu-tPOTRRE_.js → QMenu-Yx1QEIHC.js} +1 -1
- package/src/client/dist/spa/assets/{QScrollArea-HmkWoNcD.js → QScrollArea-CZVgBUBp.js} +1 -1
- package/src/client/dist/spa/assets/{QScrollObserver-mISyeTiK.js → QScrollObserver-CAlbLisQ.js} +1 -1
- package/src/client/dist/spa/assets/{QTooltip-CxkTFl9W.js → QTooltip-CwBZU_bs.js} +1 -1
- package/src/client/dist/spa/assets/{SearchPage-DaLCy-Tl.js → SearchPage-DFyeKlPZ.js} +1 -1
- package/src/client/dist/spa/assets/SettingsPage-C64_E1oJ.css +1 -0
- package/src/client/dist/spa/assets/SettingsPage-CWzjJmRK.js +9 -0
- package/src/client/dist/spa/assets/{TouchPan-DWCVQZB9.js → TouchPan-DXGxNMLq.js} +1 -1
- package/src/client/dist/spa/assets/{WorkspacePage-iHGFGSo5.css → WorkspacePage-36QGRRCt.css} +1 -1
- package/src/client/dist/spa/assets/{WorkspacePage-DV1_gaE1.js → WorkspacePage-Do8yOxLh.js} +3 -3
- package/src/client/dist/spa/assets/{build-path-tree-CWS7OK1y.js → build-path-tree-BKx2q92A.js} +1 -1
- package/src/client/dist/spa/assets/{cssMode-gWXl0t1v.js → cssMode-C_8uTD04.js} +1 -1
- package/src/client/dist/spa/assets/{editor.api-DpdJ2VyL.js → editor.api-BLN2wAE3.js} +1 -1
- package/src/client/dist/spa/assets/{editor.main-BbBPm6hz.js → editor.main-DJVMM2i4.js} +3 -3
- package/src/client/dist/spa/assets/{engineFeatures-CrrLAzmw.js → engineFeatures-DcjAE8bG.js} +1 -1
- package/src/client/dist/spa/assets/{expand-template-C-j25C1U.js → expand-template-DijLIEI5.js} +1 -1
- package/src/client/dist/spa/assets/{freemarker2-c2I2xP4t.js → freemarker2-g6EYTs3n.js} +1 -1
- package/src/client/dist/spa/assets/{handlebars-DdGipD5f.js → handlebars-Cv6y1teP.js} +1 -1
- package/src/client/dist/spa/assets/{html-C44mULGc.js → html-Bc4U-abP.js} +1 -1
- package/src/client/dist/spa/assets/{htmlMode-DCg3M8QF.js → htmlMode-BwFjeo24.js} +1 -1
- package/src/client/dist/spa/assets/i18n-PViX03Cu.js +1 -0
- package/src/client/dist/spa/assets/index-DnKXe7D-.js +82 -0
- package/src/client/dist/spa/assets/{javascript-CE0OdK4f.js → javascript-Eczqe8bD.js} +1 -1
- package/src/client/dist/spa/assets/{jsonMode-D4h7-bRc.js → jsonMode-BnyadzU0.js} +1 -1
- package/src/client/dist/spa/assets/{kobo-commands-CvhobBu_.js → kobo-commands-DRDkhOO8.js} +1 -1
- package/src/client/dist/spa/assets/{liquid-PjQMywNm.js → liquid-DwRAzAaj.js} +1 -1
- package/src/client/dist/spa/assets/{mdx-DFGLZOLU.js → mdx-CFl7wmEL.js} +1 -1
- package/src/client/dist/spa/assets/{monaco.contribution-D-j1tvz2.js → monaco.contribution--Qz8ObQD.js} +2 -2
- package/src/client/dist/spa/assets/{notifications-Dq_CVZlr.js → notifications-l1Pxijve.js} +1 -1
- package/src/client/dist/spa/assets/{permissionModes-Bt3HMECq.js → permissionModes-BT5h2RfC.js} +1 -1
- package/src/client/dist/spa/assets/{python-DYXZ9PBg.js → python-Ddo5HdBd.js} +1 -1
- package/src/client/dist/spa/assets/{razor-CuwVAV-Y.js → razor-CvJeuccY.js} +1 -1
- package/src/client/dist/spa/assets/{render-chat-markdown-D5id7lkF.js → render-chat-markdown-CODpqdnw.js} +1 -1
- package/src/client/dist/spa/assets/{tsMode-CUhQNBlR.js → tsMode-fdyRUXnV.js} +1 -1
- package/src/client/dist/spa/assets/{typescript-IVMaFcQM.js → typescript-BAEZzsZ6.js} +1 -1
- package/src/client/dist/spa/assets/{use-onboarding-dapbWk0g.js → use-onboarding-CiVkuuHv.js} +2 -2
- package/src/client/dist/spa/assets/{xml-BQ1-pFw9.js → xml-DCrp93GN.js} +1 -1
- package/src/client/dist/spa/assets/{yaml-CylVyFXK.js → yaml-tdhnQ3xr.js} +1 -1
- package/src/client/dist/spa/index.html +2 -2
- package/src/client/dist/spa/assets/SettingsPage-D6AWEFEO.css +0 -1
- package/src/client/dist/spa/assets/SettingsPage-DlNl0TLR.js +0 -9
- package/src/client/dist/spa/assets/i18n-C1Tdl-zt.js +0 -1
- package/src/client/dist/spa/assets/index-djIkvHo7.js +0 -52
package/AGENTS.md
CHANGED
|
@@ -100,7 +100,7 @@ src/
|
|
|
100
100
|
|
|
101
101
|
| Table | Purpose |
|
|
102
102
|
|---|---|
|
|
103
|
-
| `workspaces` | the unit of work — id, name, project_path, source_branch, working_branch, status, notion_url, model, dev_server_status, `archived_at`, `auto_loop`, `auto_loop_ready`, `no_progress_streak`, timestamps |
|
|
103
|
+
| `workspaces` | the unit of work — id, name, project_path, source_branch, working_branch, status, notion_url, model, dev_server_status, `archived_at`, `worktree_purged_at`, `worktree_purge_restore_data` (JSON), `auto_loop`, `auto_loop_ready`, `no_progress_streak`, timestamps |
|
|
104
104
|
| `tasks` | workspace sub-items — title, status, `is_acceptance_criterion`, sort_order; CASCADE DELETE on workspace |
|
|
105
105
|
| `agent_sessions` | Claude Code CLI invocations — pid, `claude_session_id`, status, started_at, ended_at, `name` |
|
|
106
106
|
| `ws_events` | persisted WebSocket events for replay on reconnect — type, payload, session_id, created_at |
|
|
@@ -111,6 +111,8 @@ src/
|
|
|
111
111
|
|
|
112
112
|
`archived_at` is **orthogonal** to `status` — archiving is a visibility flag, not a lifecycle state. Unarchive restores the exact pre-archive `status`.
|
|
113
113
|
|
|
114
|
+
`worktree_purged_at` + `worktree_purge_restore_data` drive the disk-space purge feature (see [Worktree purge](#worktree-purge) below): when set, the workspace's worktree folder has been removed from disk but the chat history is preserved. `worktree_purge_restore_data` is a JSON blob (`{ prNumber, prUrl, forge, mergeCommitSha, originalWorktreePath, originalSourceBranch, originalWorkingBranch }`) captured at purge time for future "Restore" UX. Both fields are cleared automatically by the pr-watcher when the worktree folder reappears on disk.
|
|
115
|
+
|
|
114
116
|
`auto_loop` (bool, default 0), `auto_loop_ready` (bool, default 0) and `no_progress_streak` (int, default 0) drive the auto-loop feature: when `auto_loop=1`, `session:ended` triggers `auto-loop-service.onSessionEnded` which either spawns the next iteration via a fresh `startAgent(resume=false)`, disables with `reason='completed'` (no pending tasks), or disables with `reason='stall'` (3 consecutive sessions without a task completed). Archive + delete both auto-disable.
|
|
115
117
|
|
|
116
118
|
## Database migrations
|
|
@@ -219,6 +221,14 @@ Background: the engine was migrated from `@openai/codex-sdk` (one-shot `codex ex
|
|
|
219
221
|
- **Custom bash override** — if `effective.changeSourceBranchScript` is non-empty (per-project override or global default), the script **replaces** the built-in flow. Spawned with `bash -c`, cwd = worktree, 5 min timeout, stderr captured (last 8 KB). Exit 0 → Kōbō updates the source-branch metadata; any non-zero exit → the stderr tail is propagated as a clean error. The user-facing menu item only shows when the resolved script is non-empty — empty = feature disabled (opt-in).
|
|
220
222
|
- **Custom-script env vars** — `KOBO_NEW_BASE`, `KOBO_OLD_BASE`, `KOBO_WORKING_BRANCH`, `KOBO_WORKTREE_PATH`, `KOBO_PROJECT_PATH`, `KOBO_PROJECT_NAME`, `KOBO_WORKSPACE_ID`, `KOBO_WORKSPACE_NAME`, `KOBO_FORGE`, `KOBO_PR_NUMBER` (empty when no PR/MR is open). The default script lives in `settings-defaults.ts` and is seeded into `global.changeSourceBranchScript` by settings migration v33; the client reads it through `GET /api/settings/defaults` for the "Reset to Kōbō default" button. See [CONFIGURATION.md → Custom change-source-branch script](CONFIGURATION.md#custom-change-source-branch-script).
|
|
221
223
|
|
|
224
|
+
### Worktree purge
|
|
225
|
+
|
|
226
|
+
`src/server/services/worktree-purge-service.ts` removes a workspace's worktree from disk while preserving the chat history and PR metadata. Triggered manually via `POST /api/workspaces/:id/purge-worktree` from the workspace context menu, or automatically by the pr-watcher when a PR transitions to MERGED **and** `global.autoPurgeOnPrMerged` is enabled (Settings → Worktrees toggle, settings migration v36).
|
|
227
|
+
|
|
228
|
+
Sequence: `captureRestoreData` (best-effort forge lookup for PR number / URL / merge SHA) → stop agent + dev server + terminal → `archiveWorkspace` → `removeWorktree` → `markWorktreePurged(restoreData)` → emit `workspace:worktree-purged`. Permission errors on removal (EACCES / EPERM — typically Docker-owned files in `node_modules` / `vendor`) are detected via regex on the error message and the warning toast carries a copy-pasteable `sudo rm -rf` + `git worktree prune` recovery command plus prevention tips (Docker `USER` directive, `setfacl` default ACL). See [CONFIGURATION.md → Auto-purge worktree on PR merged](CONFIGURATION.md#auto-purge-worktree-on-pr-merged).
|
|
229
|
+
|
|
230
|
+
**Auto-restore on manual recreation.** When the user manually recreates the worktree folder (`gh pr checkout <pr-number>` or `git worktree add <path> <branch>`), the pr-watcher detects the folder reappearing on its next 30 s tick via `autoRestoreManuallyRecreatedWorktrees()`: it iterates archived workspaces with `worktreePurgedAt`, checks `fs.existsSync(worktreePath)`, and on a hit calls `restoreWorktreeFromDisk(id)` which clears `worktree_purged_at` + `worktree_purge_restore_data` + `archived_at` in one transaction, then emits `workspace:worktree-restored`. The client websocket store reuses the same handler as `workspace:archived`/`unarchived` to refresh both the active and archived workspace lists. No UI action needed.
|
|
231
|
+
|
|
222
232
|
### Workspace attention indicators
|
|
223
233
|
|
|
224
234
|
`src/client/src/utils/workspace-attention.ts` derives a small set of badges (CI failure, changes-requested) from the PR snapshot + git stats stored on each workspace. `WorkspaceAttentionLabels.vue` renders them inline on the workspace cards in the left drawer. The derivation is a pure function — easy to unit-test, no IO. Drawer cards therefore stay reactive to whatever the pr-watcher / bulk-info refresh writes back into the store.
|
package/CHANGELOG.md
CHANGED
|
@@ -4,6 +4,20 @@ 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.24
|
|
8
|
+
|
|
9
|
+
- refactor: prune redundant comments from worktree-purge work
|
|
10
|
+
- docs: replace stray French UI labels with their English equivalents
|
|
11
|
+
- docs(changelog): drop stale Unreleased section duplicated by v1.7.23
|
|
12
|
+
|
|
13
|
+
## 1.7.23
|
|
14
|
+
|
|
15
|
+
- docs: document worktree purge, auto-restore, and permission recovery
|
|
16
|
+
- feat(client): onboarding highlights changelog and auto-purge toggle
|
|
17
|
+
- feat(pr-watcher): auto-restore manually-recreated worktrees
|
|
18
|
+
- feat(workspace): worktree purge with auto-archive and restore metadata
|
|
19
|
+
- feat(templates): add /kobo-context slash command (you need to re-import default templates)
|
|
20
|
+
|
|
7
21
|
## 1.7.22
|
|
8
22
|
|
|
9
23
|
- feat(client): accept the new app.notion.com URL format
|
package/README.md
CHANGED
|
@@ -24,6 +24,7 @@ Kōbō runs multiple coding agents in parallel, each isolated in its own git wor
|
|
|
24
24
|
- **Scheduled wakeups** — the agent schedules a one-shot wake-up via the `ScheduleWakeup` tool; Kōbō persists it across restarts, shows a live countdown, and re-invokes the agent with the stored prompt at the chosen time.
|
|
25
25
|
- **Cron schedules** — recurring per-workspace triggers the agent registers through MCP tools (`cron_create` / `cron_delete` / `cron_list`); each tick resumes the workspace session (skipped if already active), and schedules are re-armed at boot with skip-missed semantics.
|
|
26
26
|
- **Lifecycle scripts** — shell scripts run automatically at key moments: **setup** (worktree created), **cleanup** (session ended), **archive** (workspace archived). Configured globally or per project, with their output streamed into the chat.
|
|
27
|
+
- **Disk-space purge** — free a merged workspace's disk space without losing its chat history: the worktree folder is removed (PR metadata is captured for later restore), the workspace is archived, and the chat log stays queryable. Trigger manually from the workspace menu or enable **auto-purge on PR merged** in Settings → Worktrees. Recreate the worktree later with `gh pr checkout <pr-number>` and Kōbō auto-detects the folder reappearing within 30 seconds — workspace is unarchived and reactivated, no UI action needed.
|
|
27
28
|
- **Optional integrations** — Notion (import missions), Sentry (fix from issue URL), local voice transcription (whisper.cpp).
|
|
28
29
|
|
|
29
30
|
## Quick start
|
|
@@ -77,6 +78,36 @@ The full reference — every env var, every setting key, MCP server registration
|
|
|
77
78
|
|
|
78
79
|
Engine selection happens at workspace creation. Both share the same task tracking, permission modes, sub-agent panel, and quota footer. The mapping of Kōbō's four permission modes (`plan` / `bypass` / `strict` / `interactive`) to each engine's native sandbox + approval semantics is in [`CONFIGURATION.md`](./CONFIGURATION.md#permission-modes).
|
|
79
80
|
|
|
81
|
+
## Disk-space purge
|
|
82
|
+
|
|
83
|
+
A merged workspace is automatically archived but its worktree folder usually carries a lot of weight (`node_modules`, `vendor`, build artefacts…). Kōbō can free that space without losing anything queryable:
|
|
84
|
+
|
|
85
|
+
- **Manual** — workspace context menu → *Free disk space (delete worktree)*. The worktree is removed, the chat history and PR metadata stay in the database.
|
|
86
|
+
- **Automatic** — **Settings → Worktrees → Auto-purge worktree on PR merged**. When the pr-watcher sees the OPEN → MERGED transition, it archives **and** purges.
|
|
87
|
+
- **Restore** — recreate the folder yourself (`gh pr checkout <pr>` or `git worktree add <path> <branch>`). The pr-watcher detects the directory reappearing within 30 seconds and re-activates the workspace automatically (clears purge flag + unarchives). No UI action needed.
|
|
88
|
+
|
|
89
|
+
### Avoiding permission errors during purge
|
|
90
|
+
|
|
91
|
+
Docker containers usually write as `root`, so files in `node_modules` / `vendor` end up root-owned on the host. Plain `rm -rf` (which Kōbō uses under the hood) then fails with `EACCES` / `EPERM`. Pick one of these strategies depending on your setup:
|
|
92
|
+
|
|
93
|
+
1. **Best — run your container as the host user.** Add a `USER` directive in your `Dockerfile`, or set `user: "${UID}:${GID}"` in `docker-compose.yml` with `UID`/`GID` exported in your shell. No more root-owned files; nothing extra to do.
|
|
94
|
+
2. **Preventive ACL on the worktrees root.** On ext4 / btrfs / xfs with a regular Docker bind mount, a default ACL grants your user access to every file created later:
|
|
95
|
+
```bash
|
|
96
|
+
setfacl -d -m u:$(whoami):rwX <worktrees-root> # e.g. ~/.worktrees
|
|
97
|
+
```
|
|
98
|
+
Caveats: does **not** work on named Docker volumes (use a bind mount), filesystems without ACL support (NTFS, exFAT, tmpfs), strict SELinux with `:Z`, or with Docker `userns-remap`.
|
|
99
|
+
3. **Unblock an already-broken worktree** (existing root-owned files):
|
|
100
|
+
```bash
|
|
101
|
+
# Option A — recursive ACL (keeps ownership intact, just adds your user)
|
|
102
|
+
sudo setfacl -Rd -m u:$(whoami):rwX . && sudo setfacl -R -m u:$(whoami):rwX .
|
|
103
|
+
|
|
104
|
+
# Option B — take ownership outright (simpler, loses the "root-from-container" trace)
|
|
105
|
+
sudo chown -R $(whoami):$(whoami) .
|
|
106
|
+
```
|
|
107
|
+
Run from inside the worktree folder, or directly on your worktrees root (e.g. `~/.worktrees/`) to cover all existing and future workspaces at once.
|
|
108
|
+
|
|
109
|
+
When a purge does fail, Kōbō surfaces a toast with a copy-pasteable recovery command and a `git worktree prune` follow-up. The same guide is wired into **Settings → Worktrees → How purge works** for in-app reference.
|
|
110
|
+
|
|
80
111
|
## Optional integrations
|
|
81
112
|
|
|
82
113
|
Kōbō ships first-class support for three external systems. All are opt-in and reuse credentials you may already have configured for Claude Code.
|
|
@@ -339,6 +339,22 @@ export const migrations = [
|
|
|
339
339
|
}
|
|
340
340
|
},
|
|
341
341
|
},
|
|
342
|
+
{
|
|
343
|
+
version: 27,
|
|
344
|
+
name: 'add-workspace-worktree-purge',
|
|
345
|
+
migrate: (db) => {
|
|
346
|
+
const table = db.prepare("SELECT name FROM sqlite_master WHERE type='table' AND name='workspaces'").get();
|
|
347
|
+
if (!table)
|
|
348
|
+
return;
|
|
349
|
+
const cols = db.prepare('PRAGMA table_info(workspaces)').all();
|
|
350
|
+
if (!cols.some((c) => c.name === 'worktree_purged_at')) {
|
|
351
|
+
db.prepare('ALTER TABLE workspaces ADD COLUMN worktree_purged_at TEXT').run();
|
|
352
|
+
}
|
|
353
|
+
if (!cols.some((c) => c.name === 'worktree_purge_restore_data')) {
|
|
354
|
+
db.prepare('ALTER TABLE workspaces ADD COLUMN worktree_purge_restore_data TEXT').run();
|
|
355
|
+
}
|
|
356
|
+
},
|
|
357
|
+
},
|
|
342
358
|
];
|
|
343
359
|
/** Current schema version — always equals the highest migration version. */
|
|
344
360
|
export const SCHEMA_VERSION = migrations.length > 0 ? migrations[migrations.length - 1].version : 1;
|
package/dist/server/db/schema.js
CHANGED
|
@@ -35,6 +35,7 @@ import * as terminalService from '../services/terminal-service.js';
|
|
|
35
35
|
import * as wakeupService from '../services/wakeup-service.js';
|
|
36
36
|
import * as wsService from '../services/websocket-service.js';
|
|
37
37
|
import * as workspaceService from '../services/workspace-service.js';
|
|
38
|
+
import * as purgeWorktreeService from '../services/worktree-purge-service.js';
|
|
38
39
|
import * as worktreeService from '../services/worktree-service.js';
|
|
39
40
|
import { resolveUniqueBranchAndPath } from '../utils/branch-resolver.js';
|
|
40
41
|
import * as gitOps from '../utils/git-ops.js';
|
|
@@ -1922,6 +1923,27 @@ app.post('/:id/archive', migrationGuard, (c) => {
|
|
|
1922
1923
|
return c.json({ error: message }, 500);
|
|
1923
1924
|
}
|
|
1924
1925
|
});
|
|
1926
|
+
// POST /api/workspaces/:id/purge-worktree
|
|
1927
|
+
app.post('/:id/purge-worktree', migrationGuard, async (c) => {
|
|
1928
|
+
try {
|
|
1929
|
+
const id = c.req.param('id');
|
|
1930
|
+
const result = await purgeWorktreeService.purgeWorktree(id);
|
|
1931
|
+
if (result.outcome === 'not-found') {
|
|
1932
|
+
return c.json({ error: `Workspace '${id}' not found` }, 404);
|
|
1933
|
+
}
|
|
1934
|
+
if (result.outcome === 'worktree-not-owned') {
|
|
1935
|
+
return c.json({
|
|
1936
|
+
error: "This workspace attached to an external worktree (you own it). Kōbō refuses to delete files it didn't create.",
|
|
1937
|
+
}, 400);
|
|
1938
|
+
}
|
|
1939
|
+
const workspace = workspaceService.getWorkspace(id);
|
|
1940
|
+
return c.json({ workspace, warnings: result.warnings, outcome: result.outcome });
|
|
1941
|
+
}
|
|
1942
|
+
catch (err) {
|
|
1943
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
1944
|
+
return c.json({ error: message }, 500);
|
|
1945
|
+
}
|
|
1946
|
+
});
|
|
1925
1947
|
// POST /api/workspaces/:id/unarchive — restore an archived workspace
|
|
1926
1948
|
app.post('/:id/unarchive', migrationGuard, (c) => {
|
|
1927
1949
|
try {
|
|
@@ -1933,6 +1955,11 @@ app.post('/:id/unarchive', migrationGuard, (c) => {
|
|
|
1933
1955
|
if (!workspace.archivedAt) {
|
|
1934
1956
|
return c.json({ error: 'Not archived' }, 400);
|
|
1935
1957
|
}
|
|
1958
|
+
// Refuse unarchive while the worktree is missing — the pr-watcher
|
|
1959
|
+
// auto-restores once the user recreates the folder.
|
|
1960
|
+
if (workspace.worktreePurgedAt) {
|
|
1961
|
+
return c.json({ error: 'worktree-purged' }, 409);
|
|
1962
|
+
}
|
|
1936
1963
|
const updated = workspaceService.unarchiveWorkspace(id);
|
|
1937
1964
|
wsService.emitEphemeral(id, 'workspace:unarchived', { workspace: updated });
|
|
1938
1965
|
return c.json(updated);
|
|
@@ -1956,6 +1983,14 @@ function deleteWorkspaceWithSideEffects(workspace, opts) {
|
|
|
1956
1983
|
catch {
|
|
1957
1984
|
// Agent may not be running — ignore
|
|
1958
1985
|
}
|
|
1986
|
+
// Stop dev server if it was running. The processSpawn would otherwise
|
|
1987
|
+
// outlive the workspace (and keep its port + docker containers alive).
|
|
1988
|
+
try {
|
|
1989
|
+
devServerService.stopDevServer(workspace.id);
|
|
1990
|
+
}
|
|
1991
|
+
catch (err) {
|
|
1992
|
+
console.error(`[workspaces] stopDevServer during delete failed for '${workspace.name}':`, err);
|
|
1993
|
+
}
|
|
1959
1994
|
try {
|
|
1960
1995
|
terminalService.destroyTerminal(workspace.id);
|
|
1961
1996
|
}
|
|
@@ -1969,10 +2004,12 @@ function deleteWorkspaceWithSideEffects(workspace, opts) {
|
|
|
1969
2004
|
// Docker leaves root-owned files inside the worktree, git worktree
|
|
1970
2005
|
// remove fails with EACCES.
|
|
1971
2006
|
const warnings = [];
|
|
1972
|
-
//
|
|
1973
|
-
// never created the dir, so we must not delete it on the user's behalf).
|
|
2007
|
+
// Owned worktrees only — attached external worktrees aren't ours to remove.
|
|
1974
2008
|
const worktreePath = workspace.worktreePath;
|
|
1975
|
-
if (workspace.
|
|
2009
|
+
if (workspace.worktreePurgedAt) {
|
|
2010
|
+
console.log(`[workspaces] skipping worktree removal on delete (already purged): ${worktreePath}`);
|
|
2011
|
+
}
|
|
2012
|
+
else if (workspace.worktreeOwned) {
|
|
1976
2013
|
try {
|
|
1977
2014
|
worktreeService.removeWorktree(workspace.projectPath, worktreePath);
|
|
1978
2015
|
}
|
|
@@ -1,11 +1,14 @@
|
|
|
1
|
+
import fs from 'node:fs';
|
|
1
2
|
import { fetchSourceBranchAsync } from '../utils/git-ops.js';
|
|
2
3
|
import { stopDevServer } from './dev-server-service.js';
|
|
3
4
|
import { getForgeProvider } from './forge/registry.js';
|
|
4
5
|
import { resolveForge } from './forge/resolve.js';
|
|
5
6
|
import { computeGitStats } from './git-stats-service.js';
|
|
7
|
+
import { getGlobalSettings } from './settings-service.js';
|
|
6
8
|
import { destroyTerminal } from './terminal-service.js';
|
|
7
9
|
import { emitEphemeral } from './websocket-service.js';
|
|
8
|
-
import { archiveWorkspace, getWorkspace, listWorkspaces, markWorkspaceUnread, updateWorkspaceSourceBranch, } from './workspace-service.js';
|
|
10
|
+
import { archiveWorkspace, getWorkspace, listArchivedWorkspaces, listWorkspaces, markWorkspaceUnread, restoreWorktreeFromDisk, updateWorkspaceSourceBranch, } from './workspace-service.js';
|
|
11
|
+
import { purgeWorktree } from './worktree-purge-service.js';
|
|
9
12
|
// ── PR Watcher ────────────────────────────────────────────────────────────────
|
|
10
13
|
// Polls GitHub every POLL_INTERVAL_MS to detect merged/closed PRs and
|
|
11
14
|
// automatically archive the corresponding workspace.
|
|
@@ -63,7 +66,24 @@ function markUnread(workspaceId) {
|
|
|
63
66
|
console.error('[pr-watcher] markUnread failed:', err instanceof Error ? err.message : err);
|
|
64
67
|
}
|
|
65
68
|
}
|
|
69
|
+
function autoRestoreManuallyRecreatedWorktrees() {
|
|
70
|
+
for (const ws of listArchivedWorkspaces()) {
|
|
71
|
+
if (!ws.worktreePurgedAt)
|
|
72
|
+
continue;
|
|
73
|
+
if (!fs.existsSync(ws.worktreePath))
|
|
74
|
+
continue;
|
|
75
|
+
try {
|
|
76
|
+
const restored = restoreWorktreeFromDisk(ws.id);
|
|
77
|
+
emitEphemeral(ws.id, 'workspace:worktree-restored', { workspace: restored });
|
|
78
|
+
console.log(`[pr-watcher] auto-restored worktree for workspace '${ws.name}' (manual restore detected)`);
|
|
79
|
+
}
|
|
80
|
+
catch (err) {
|
|
81
|
+
console.error(`[pr-watcher] auto-restore failed for '${ws.name}':`, err instanceof Error ? err.message : err);
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
}
|
|
66
85
|
export async function checkPrStatuses() {
|
|
86
|
+
autoRestoreManuallyRecreatedWorktrees();
|
|
67
87
|
const workspaces = listWorkspaces(false); // non-archived only
|
|
68
88
|
// Clean up entries for workspaces that no longer exist
|
|
69
89
|
for (const id of lastKnownPr.keys()) {
|
|
@@ -77,6 +97,10 @@ export async function checkPrStatuses() {
|
|
|
77
97
|
}
|
|
78
98
|
}
|
|
79
99
|
for (const ws of workspaces) {
|
|
100
|
+
// Without this guard, every git/forge spawn below fails with ENOENT and
|
|
101
|
+
// floods the logs when a worktree was deleted externally.
|
|
102
|
+
if (!fs.existsSync(ws.worktreePath))
|
|
103
|
+
continue;
|
|
80
104
|
try {
|
|
81
105
|
const pr = await getForgeProvider(resolveForge(ws.projectPath)).getPrStatus(ws.worktreePath, ws.workingBranch);
|
|
82
106
|
// Detect a PR base change BEFORE computing git stats so the new base
|
|
@@ -151,6 +175,21 @@ export async function checkPrStatuses() {
|
|
|
151
175
|
reason: `PR ${pr.state.toLowerCase()}`,
|
|
152
176
|
prUrl: pr.url,
|
|
153
177
|
});
|
|
178
|
+
// Only MERGED — closed-without-merge keeps the worktree so the user
|
|
179
|
+
// can inspect / push fixes.
|
|
180
|
+
if (pr.state === 'MERGED') {
|
|
181
|
+
try {
|
|
182
|
+
const { autoPurgeOnPrMerged } = getGlobalSettings();
|
|
183
|
+
if (autoPurgeOnPrMerged) {
|
|
184
|
+
void purgeWorktree(ws.id).catch((err) => {
|
|
185
|
+
console.error(`[pr-watcher] auto-purge failed for '${ws.name}':`, err instanceof Error ? err.message : err);
|
|
186
|
+
});
|
|
187
|
+
}
|
|
188
|
+
}
|
|
189
|
+
catch (err) {
|
|
190
|
+
console.error(`[pr-watcher] auto-purge guard failed for '${ws.name}':`, err instanceof Error ? err.message : err);
|
|
191
|
+
}
|
|
192
|
+
}
|
|
154
193
|
continue; // do not run base-change detection on a workspace we just archived
|
|
155
194
|
}
|
|
156
195
|
// Review-decision and CI transitions (only on OPEN PRs; first-sight is
|
|
@@ -598,6 +598,15 @@ const settingsMigrations = [
|
|
|
598
598
|
}
|
|
599
599
|
},
|
|
600
600
|
},
|
|
601
|
+
{
|
|
602
|
+
version: 36,
|
|
603
|
+
name: 'add-auto-purge-on-pr-merged',
|
|
604
|
+
migrate: ({ global }) => {
|
|
605
|
+
if (typeof global.autoPurgeOnPrMerged !== 'boolean') {
|
|
606
|
+
global.autoPurgeOnPrMerged = false;
|
|
607
|
+
}
|
|
608
|
+
},
|
|
609
|
+
},
|
|
601
610
|
];
|
|
602
611
|
/** Current settings schema version — always equals the highest migration version. */
|
|
603
612
|
export const SETTINGS_SCHEMA_VERSION = settingsMigrations.length > 0 ? settingsMigrations[settingsMigrations.length - 1].version : 0;
|
|
@@ -641,6 +650,7 @@ function defaultSettings() {
|
|
|
641
650
|
changeSourceBranchScript: DEFAULT_CHANGE_SOURCE_BRANCH_SCRIPT,
|
|
642
651
|
editorCommand: '',
|
|
643
652
|
fileManagerCommand: '',
|
|
653
|
+
autoPurgeOnPrMerged: false,
|
|
644
654
|
browserNotifications: true,
|
|
645
655
|
audioNotifications: true,
|
|
646
656
|
audioNotificationSound: 'hey.mp3',
|
|
@@ -979,6 +989,7 @@ export function updateGlobalSettings(data) {
|
|
|
979
989
|
'changeSourceBranchScript',
|
|
980
990
|
'editorCommand',
|
|
981
991
|
'fileManagerCommand',
|
|
992
|
+
'autoPurgeOnPrMerged',
|
|
982
993
|
'browserNotifications',
|
|
983
994
|
'audioNotifications',
|
|
984
995
|
'audioNotificationSound',
|
|
@@ -127,6 +127,38 @@ export function replaceAllTemplates(templates) {
|
|
|
127
127
|
writeTemplates(validated);
|
|
128
128
|
}
|
|
129
129
|
export const DEFAULT_TEMPLATES = [
|
|
130
|
+
{
|
|
131
|
+
slug: 'kobo-context',
|
|
132
|
+
description: "Onboard the agent on Kōbō's core concepts and tools",
|
|
133
|
+
content: `You are working inside a Kōbō workspace (workspace "{workspace_name}", branch \`{working_branch}\`).\n\n` +
|
|
134
|
+
`# What Kōbō is\n` +
|
|
135
|
+
`Kōbō orchestrates multiple coding agents in parallel. Each "workspace" is a self-contained mission with:\n` +
|
|
136
|
+
`- An isolated git worktree (your current working directory)\n` +
|
|
137
|
+
`- A dedicated branch (\`{working_branch}\`), targeting a source branch\n` +
|
|
138
|
+
`- Its own session history and task list, persisted in Kōbō's SQLite DB\n` +
|
|
139
|
+
`- A dedicated MCP server (\`kobo-tasks\`) exposing tools to read/write workspace state\n\n` +
|
|
140
|
+
`# Lifecycle\n` +
|
|
141
|
+
`1. **Brainstorming** — you scope the work, output a plan, end with the literal marker \`[BRAINSTORM_COMPLETE]\`\n` +
|
|
142
|
+
`2. **Executing** — you implement the plan, commit, push\n` +
|
|
143
|
+
`3. **Auto-loop (opt-in)** — Kōbō re-spawns a fresh session per task; each iteration sees a clean context\n` +
|
|
144
|
+
`4. **Completed / Archived** — the workspace freezes; the worktree stays available read-only\n\n` +
|
|
145
|
+
`# Kōbō MCP tools (always namespaced \`kobo__…\`)\n` +
|
|
146
|
+
`- \`kobo__list_tasks\` / \`create_task\` / \`update_task\` / \`mark_task_done\` / \`delete_task\` — manage the visible task list\n` +
|
|
147
|
+
`- \`kobo__set_workspace_agent_description\` — short one-line summary shown in the sidebar; keep it current\n` +
|
|
148
|
+
`- \`kobo__get_workspace_info\` / \`kobo__get_git_info\` — read workspace metadata + git state\n` +
|
|
149
|
+
`- \`kobo__cron_create\` / \`cron_delete\` / \`cron_list\` — schedule recurring or one-shot triggers on THIS workspace\n` +
|
|
150
|
+
`- \`kobo__mark_auto_loop_ready\` — flip the loop into auto-execution after grooming\n\n` +
|
|
151
|
+
`# Conventions\n` +
|
|
152
|
+
`- \`CLAUDE.md\` / \`AGENTS.md\` at the project root override default behavior — read them first\n` +
|
|
153
|
+
`- \`.ai/.git-conventions.md\` (when present) defines per-project commit / branch rules — apply them on every git op\n` +
|
|
154
|
+
`- \`.ai/thoughts/\` is your persistent scratch (Notion imports, Sentry context, planning notes) — write freely\n` +
|
|
155
|
+
`- Never use \`--no-verify\` or skip CI hooks unless explicitly asked\n` +
|
|
156
|
+
`- Always target \`origin/<source_branch>\` for diffs and PRs, not the local branch\n\n` +
|
|
157
|
+
`# Boundaries\n` +
|
|
158
|
+
`- The user owns the \`description\` field of the workspace — never write it; you only own \`agent_description\`\n` +
|
|
159
|
+
`- The user can interrupt you at any time via the chat; treat their messages as authoritative redirections\n` +
|
|
160
|
+
`- Auto-loop is automatically disabled if the user sends a chat message during a loop — they'll re-enable it manually after\n`,
|
|
161
|
+
},
|
|
130
162
|
{
|
|
131
163
|
slug: 'review-quality',
|
|
132
164
|
description: 'Code quality review',
|
|
@@ -59,6 +59,8 @@ function mapWorkspace(row) {
|
|
|
59
59
|
initialPrompt: row.initial_prompt,
|
|
60
60
|
prChangesDismissedAt: row.pr_changes_dismissed_at,
|
|
61
61
|
prCiFailureDismissedAt: row.pr_ci_failure_dismissed_at,
|
|
62
|
+
worktreePurgedAt: row.worktree_purged_at,
|
|
63
|
+
worktreePurgeRestoreData: row.worktree_purge_restore_data,
|
|
62
64
|
engine: row.engine ?? 'claude-code',
|
|
63
65
|
autoLoop: row.auto_loop === 1,
|
|
64
66
|
autoLoopReady: row.auto_loop_ready === 1,
|
|
@@ -365,6 +367,26 @@ export function dismissPrAttention(id, kind, prUpdatedAt) {
|
|
|
365
367
|
throw new Error(`Workspace '${id}' not found`);
|
|
366
368
|
}
|
|
367
369
|
}
|
|
370
|
+
export function markWorktreePurged(id, restoreData) {
|
|
371
|
+
const db = getDb();
|
|
372
|
+
const now = new Date().toISOString();
|
|
373
|
+
const result = db
|
|
374
|
+
.prepare('UPDATE workspaces SET worktree_purged_at = ?, worktree_purge_restore_data = ?, updated_at = ? WHERE id = ?')
|
|
375
|
+
.run(now, JSON.stringify(restoreData), now, id);
|
|
376
|
+
if (result.changes === 0) {
|
|
377
|
+
throw new Error(`Workspace '${id}' not found`);
|
|
378
|
+
}
|
|
379
|
+
}
|
|
380
|
+
export function restoreWorktreeFromDisk(id) {
|
|
381
|
+
const db = getDb();
|
|
382
|
+
const workspace = getWorkspace(id);
|
|
383
|
+
if (!workspace) {
|
|
384
|
+
throw new Error(`Workspace '${id}' not found`);
|
|
385
|
+
}
|
|
386
|
+
const now = new Date().toISOString();
|
|
387
|
+
db.prepare('UPDATE workspaces SET worktree_purged_at = NULL, worktree_purge_restore_data = NULL, archived_at = NULL, updated_at = ? WHERE id = ?').run(now, id);
|
|
388
|
+
return getWorkspace(id);
|
|
389
|
+
}
|
|
368
390
|
/** Update the dev-server status column for a workspace. */
|
|
369
391
|
export function updateDevServerStatus(id, status) {
|
|
370
392
|
const db = getDb();
|
|
@@ -0,0 +1,116 @@
|
|
|
1
|
+
import fs from 'node:fs';
|
|
2
|
+
import * as agentManager from './agent/orchestrator.js';
|
|
3
|
+
import * as devServerService from './dev-server-service.js';
|
|
4
|
+
import { getForgeProvider } from './forge/registry.js';
|
|
5
|
+
import { resolveForge } from './forge/resolve.js';
|
|
6
|
+
import { destroyTerminal } from './terminal-service.js';
|
|
7
|
+
import { emitEphemeral } from './websocket-service.js';
|
|
8
|
+
import { archiveWorkspace, getWorkspace, markWorktreePurged, } from './workspace-service.js';
|
|
9
|
+
import { removeWorktree } from './worktree-service.js';
|
|
10
|
+
export async function purgeWorktree(workspaceId) {
|
|
11
|
+
const workspace = getWorkspace(workspaceId);
|
|
12
|
+
if (!workspace)
|
|
13
|
+
return { outcome: 'not-found', warnings: [] };
|
|
14
|
+
if (workspace.worktreePurgedAt)
|
|
15
|
+
return { outcome: 'already-purged', warnings: [] };
|
|
16
|
+
if (!workspace.worktreeOwned)
|
|
17
|
+
return { outcome: 'worktree-not-owned', warnings: [] };
|
|
18
|
+
const warnings = [];
|
|
19
|
+
try {
|
|
20
|
+
agentManager.stopAgent(workspaceId);
|
|
21
|
+
}
|
|
22
|
+
catch (err) {
|
|
23
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
24
|
+
console.error(`[purge] stopAgent failed for '${workspace.name}':`, msg);
|
|
25
|
+
}
|
|
26
|
+
try {
|
|
27
|
+
devServerService.stopDevServer(workspaceId);
|
|
28
|
+
}
|
|
29
|
+
catch (err) {
|
|
30
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
31
|
+
console.error(`[purge] stopDevServer failed for '${workspace.name}':`, msg);
|
|
32
|
+
}
|
|
33
|
+
try {
|
|
34
|
+
destroyTerminal(workspaceId);
|
|
35
|
+
}
|
|
36
|
+
catch { }
|
|
37
|
+
// Snapshot the forge BEFORE removing the worktree — the PR lookup uses
|
|
38
|
+
// `worktreePath` as cwd.
|
|
39
|
+
const restoreData = await captureRestoreData(workspace);
|
|
40
|
+
if (!workspace.archivedAt) {
|
|
41
|
+
try {
|
|
42
|
+
const archived = archiveWorkspace(workspaceId);
|
|
43
|
+
emitEphemeral(workspaceId, 'workspace:archived', { workspace: archived });
|
|
44
|
+
}
|
|
45
|
+
catch (err) {
|
|
46
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
47
|
+
warnings.push(`Archive failed: ${msg}`);
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
if (fs.existsSync(workspace.worktreePath)) {
|
|
51
|
+
try {
|
|
52
|
+
removeWorktree(workspace.projectPath, workspace.worktreePath);
|
|
53
|
+
}
|
|
54
|
+
catch (err) {
|
|
55
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
56
|
+
warnings.push(buildRemovalFailureMessage(workspace.worktreePath, workspace.projectPath, msg));
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
try {
|
|
60
|
+
markWorktreePurged(workspaceId, restoreData);
|
|
61
|
+
}
|
|
62
|
+
catch (err) {
|
|
63
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
64
|
+
warnings.push(`DB metadata update failed: ${msg}`);
|
|
65
|
+
}
|
|
66
|
+
emitEphemeral(workspaceId, 'workspace:worktree-purged', {
|
|
67
|
+
workspaceId,
|
|
68
|
+
purgedAt: new Date().toISOString(),
|
|
69
|
+
restoreData,
|
|
70
|
+
});
|
|
71
|
+
return { outcome: 'purged', warnings };
|
|
72
|
+
}
|
|
73
|
+
function buildRemovalFailureMessage(worktreePath, projectPath, errMsg) {
|
|
74
|
+
const isPermission = /EACCES|EPERM|permission denied|operation not permitted/i.test(errMsg);
|
|
75
|
+
const baseLine = `Failed to remove worktree '${worktreePath}'.`;
|
|
76
|
+
const recovery = ['Recovery:', ` sudo rm -rf '${worktreePath}'`, ` cd '${projectPath}' && git worktree prune`].join('\n');
|
|
77
|
+
if (isPermission) {
|
|
78
|
+
return [
|
|
79
|
+
`${baseLine} Permission denied — typically caused by Docker leaving root-owned files inside node_modules / vendor.`,
|
|
80
|
+
recovery,
|
|
81
|
+
'Prevention: configure your container to run as your host user (USER directive in Dockerfile, or `user: "$(id -u):$(id -g)"` in docker-compose), OR pre-seed the worktrees root with a default ACL:',
|
|
82
|
+
` setfacl -d -m u:$(whoami):rwx '${projectPath}/..'/worktrees`,
|
|
83
|
+
`Reason: ${errMsg}`,
|
|
84
|
+
].join('\n');
|
|
85
|
+
}
|
|
86
|
+
return [baseLine, recovery, `Reason: ${errMsg}`].join('\n');
|
|
87
|
+
}
|
|
88
|
+
async function captureRestoreData(workspace) {
|
|
89
|
+
const forge = resolveForge(workspace.projectPath);
|
|
90
|
+
let prNumber = null;
|
|
91
|
+
let prUrl = null;
|
|
92
|
+
// Direct provider call (not the pr-watcher cache) avoids an import cycle
|
|
93
|
+
// between the watcher and this service.
|
|
94
|
+
if (forge !== 'none') {
|
|
95
|
+
try {
|
|
96
|
+
const provider = getForgeProvider(forge);
|
|
97
|
+
const fresh = await provider.getPrStatus(workspace.worktreePath, workspace.workingBranch);
|
|
98
|
+
if (fresh) {
|
|
99
|
+
prNumber = fresh.number ?? null;
|
|
100
|
+
prUrl = fresh.url ?? null;
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
catch (err) {
|
|
104
|
+
console.warn(`[purge] PR lookup for restore data failed:`, err instanceof Error ? err.message : err);
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
return {
|
|
108
|
+
prNumber,
|
|
109
|
+
prUrl,
|
|
110
|
+
forge: forge,
|
|
111
|
+
mergeCommitSha: null,
|
|
112
|
+
originalWorktreePath: workspace.worktreePath,
|
|
113
|
+
originalSourceBranch: workspace.sourceBranch,
|
|
114
|
+
originalWorkingBranch: workspace.workingBranch,
|
|
115
|
+
};
|
|
116
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@loicngr/kobo",
|
|
3
|
-
"version": "1.7.
|
|
3
|
+
"version": "1.7.24",
|
|
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-
|
|
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-DnKXe7D-.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-CODpqdnw.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-DnKXe7D-.js";import{t as v}from"./render-chat-markdown-CODpqdnw.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 +1 @@
|
|
|
1
|
-
import{F as e,b as t}from"./QIcon-BmEX2rXO.js";import{T as n,w as r}from"./notifications-
|
|
1
|
+
import{F as e,b as t}from"./QIcon-BmEX2rXO.js";import{T as n,w as r}from"./notifications-l1Pxijve.js";function i(e){if(e===!1)return 0;if(e===!0||e===void 0)return 1;let t=parseInt(e,10);return isNaN(t)?0:t}var a=e({name:`close-popup`,beforeMount(e,{value:a}){let o={depth:i(a),handler(t){o.depth!==0&&setTimeout(()=>{let i=n(e);i!==void 0&&r(i,t,o.depth)})},handlerKey(e){t(e,13)===!0&&o.handler(e)}};e.__qclosepopup=o,e.addEventListener(`click`,o.handler),e.addEventListener(`keyup`,o.handlerKey)},updated(e,{value:t,oldValue:n}){t!==n&&(e.__qclosepopup.depth=i(t))},beforeUnmount(e){let t=e.__qclosepopup;e.removeEventListener(`click`,t.handler),e.removeEventListener(`keyup`,t.handlerKey),delete e.__qclosepopup}});export{a as t};
|