@loicngr/kobo 1.11.23 → 1.11.25

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (61) hide show
  1. package/CHANGELOG.md +8 -4
  2. package/README.md +40 -96
  3. package/dist/server/db/migrations.js +18 -0
  4. package/dist/server/db/schema.js +1 -0
  5. package/dist/server/routes/workspaces.js +23 -3
  6. package/dist/server/services/agent/orchestrator.js +2 -2
  7. package/dist/server/services/quota-backoff-service.js +6 -3
  8. package/dist/server/services/workspace-service.js +5 -1
  9. package/package.json +1 -1
  10. package/src/client/dist/pwa/assets/{ActivityFeed-BNyFzRkx.js → ActivityFeed-DePXS5oh.js} +1 -1
  11. package/src/client/dist/pwa/assets/{ChangelogPage-7pgilQJn.js → ChangelogPage-DEF1geul.js} +1 -1
  12. package/src/client/dist/pwa/assets/{CreatePage-P4N9bro4.js → CreatePage-CEjwXKQF.js} +1 -1
  13. package/src/client/dist/pwa/assets/{DiffViewer-Cl2w9liE.js → DiffViewer-FrpLqWlv.js} +3 -3
  14. package/src/client/dist/pwa/assets/{DocumentsPanel-CARiAfZs.js → DocumentsPanel-4j-UIHic.js} +1 -1
  15. package/src/client/dist/pwa/assets/{GitPanel-txKWaxSr.js → GitPanel-oLfllkFW.js} +3 -3
  16. package/src/client/dist/pwa/assets/{HealthPage-B0_v81_H.js → HealthPage-DS46JzBg.js} +1 -1
  17. package/src/client/dist/pwa/assets/{MainLayout-BCIURScy.css → MainLayout-Vq1iUPjx.css} +1 -1
  18. package/src/client/dist/pwa/assets/{MainLayout-Cl26t1Yw.js → MainLayout-WBL2b8tD.js} +2 -2
  19. package/src/client/dist/pwa/assets/{QBtnDropdown-9dpk4IiI.js → QBtnDropdown-CrhiOT1R.js} +1 -1
  20. package/src/client/dist/pwa/assets/{SchedulePanel-CSJbQpWB.js → SchedulePanel-Cfeyie6U.js} +1 -1
  21. package/src/client/dist/pwa/assets/{SearchPage-B5Wndh73.js → SearchPage-DSj7yg7w.js} +1 -1
  22. package/src/client/dist/pwa/assets/{SettingsPage-BmSaJrE5.js → SettingsPage-DzF8gAj4.js} +1 -1
  23. package/src/client/dist/pwa/assets/{TerminalPanel-CuKWlENm.js → TerminalPanel-e4Mmlz6H.js} +1 -1
  24. package/src/client/dist/pwa/assets/{TouchPan-CflYDvzb.js → TouchPan-D8ih3XMN.js} +1 -1
  25. package/src/client/dist/pwa/assets/{WorkspacePage-xKaXyg_O.css → WorkspacePage-3rwxkeew.css} +1 -1
  26. package/src/client/dist/pwa/assets/{WorkspacePage-C8SAYVkt.js → WorkspacePage-D-YQa2pY.js} +3 -3
  27. package/src/client/dist/pwa/assets/{build-path-tree-nS5a0hfa.js → build-path-tree-BW6daoqZ.js} +1 -1
  28. package/src/client/dist/pwa/assets/{cssMode-BWoHpG_3.js → cssMode-CpDMzly7.js} +1 -1
  29. package/src/client/dist/pwa/assets/{editor.api-CHHKMKIl.js → editor.api-DSpAvlaE.js} +1 -1
  30. package/src/client/dist/pwa/assets/{efforts-C0gGtu_O.js → efforts-D0XglPsK.js} +1 -1
  31. package/src/client/dist/pwa/assets/{engineFeatures-Cm3wk7lo.js → engineFeatures-Df8DgaJk.js} +1 -1
  32. package/src/client/dist/pwa/assets/{expand-template-CRv7R1En.js → expand-template-CqiZhw0u.js} +1 -1
  33. package/src/client/dist/pwa/assets/{freemarker2-ChmoNkJH.js → freemarker2-BzDrgBen.js} +1 -1
  34. package/src/client/dist/pwa/assets/{handlebars-DjdwH2Vr.js → handlebars-P-1hO0gb.js} +1 -1
  35. package/src/client/dist/pwa/assets/{html-DJszTgCl.js → html-BhvJWTDE.js} +1 -1
  36. package/src/client/dist/pwa/assets/{htmlMode-D0QTtBWO.js → htmlMode-CNuL7DKZ.js} +1 -1
  37. package/src/client/dist/pwa/assets/i18n-B0-TME1H.js +1 -0
  38. package/src/client/dist/pwa/assets/{index-okqx5CVc.js → index-DMyroqeJ.js} +2 -2
  39. package/src/client/dist/pwa/assets/{javascript-ByhJGDgS.js → javascript-DOLOmOsk.js} +1 -1
  40. package/src/client/dist/pwa/assets/{jsonMode-Db1oPA-y.js → jsonMode-CNjA0mBf.js} +1 -1
  41. package/src/client/dist/pwa/assets/{liquid-CGMily4q.js → liquid-b-gv156K.js} +1 -1
  42. package/src/client/dist/pwa/assets/{lspLanguageFeatures-B85CeJTe.js → lspLanguageFeatures-Cu5au25t.js} +1 -1
  43. package/src/client/dist/pwa/assets/{mdx-B5_UPmNo.js → mdx-CthI5Pfx.js} +1 -1
  44. package/src/client/dist/pwa/assets/{network-auth-D9vq0xDI.js → network-auth-jClRVM5o.js} +1 -1
  45. package/src/client/dist/pwa/assets/{notify-theme-kRr6YTnZ.js → notify-theme-D7WNwTAk.js} +1 -1
  46. package/src/client/dist/pwa/assets/{pending-diff-open-nLdNq-y-.js → pending-diff-open-BjmrYqfo.js} +1 -1
  47. package/src/client/dist/pwa/assets/{permissionModes-Du6lXebH.js → permissionModes-BIjHkGNz.js} +1 -1
  48. package/src/client/dist/pwa/assets/{project-color-tT7gsmB5.js → project-color-DOOxOoXY.js} +1 -1
  49. package/src/client/dist/pwa/assets/{python-FzBHx7LG.js → python-BBwkm7ly.js} +1 -1
  50. package/src/client/dist/pwa/assets/{razor-BU_JvIXc.js → razor-LwTiJwRy.js} +1 -1
  51. package/src/client/dist/pwa/assets/{render-chat-markdown-DrGdVCku.js → render-chat-markdown-Bl9MpfC9.js} +1 -1
  52. package/src/client/dist/pwa/assets/{tsMode-BDW6Cb6M.js → tsMode-BZWTBrEG.js} +1 -1
  53. package/src/client/dist/pwa/assets/{typescript-21K3hWN-.js → typescript-CBEi6s0G.js} +1 -1
  54. package/src/client/dist/pwa/assets/{use-onboarding-BCM8AGRg.js → use-onboarding-DJxL4chd.js} +1 -1
  55. package/src/client/dist/pwa/assets/{vs-Ch3CDiTS.js → vs-BBBJO4M1.js} +3 -3
  56. package/src/client/dist/pwa/assets/{workers-CbIvH33O.js → workers-DxK2n_dl.js} +1 -1
  57. package/src/client/dist/pwa/assets/{xml-d-kArTuN.js → xml-DafnVQfx.js} +1 -1
  58. package/src/client/dist/pwa/assets/{yaml-tzAPZ-1Q.js → yaml-CbIbKyyP.js} +1 -1
  59. package/src/client/dist/pwa/index.html +1 -1
  60. package/src/client/dist/pwa/sw.js +1 -1
  61. package/src/client/dist/pwa/assets/i18n-BQFPc-Bc.js +0 -1
package/CHANGELOG.md CHANGED
@@ -4,13 +4,17 @@ All notable changes to Kōbō are documented here. The format is based on
4
4
  [Keep a Changelog](https://keepachangelog.com/). Each release is an `## <version>`
5
5
  section — the in-app "What's new" dialog reads this file.
6
6
 
7
- ## 1.11.23
7
+ ## 1.11.25
8
8
 
9
- - fix(test): configure git identity on the pr-checkout temp repo
10
- - feat: create workspaces from an existing PR/MR
9
+ - fix: quota-status transitions, name overflow, and quota mislabeling
10
+
11
+ ## 1.11.24
11
12
 
12
- ## 1.11.22
13
+ - docs: redesign README as a landing page, document PR/MR import
13
14
 
15
+ ## 1.11.23
16
+
17
+ - fix(test): configure git identity on the pr-checkout temp repo
14
18
  - feat: create workspaces from an existing PR/MR
15
19
 
16
20
  ## 1.11.21
package/README.md CHANGED
@@ -1,43 +1,26 @@
1
1
  # Kōbō
2
2
 
3
- > Multi-workspace orchestrator for [Claude Code](https://claude.com/claude-code) and [OpenAI Codex](https://developers.openai.com/codex/) agents.
3
+ **Run a fleet of Claude Code and Codex agents in parallel — each in its own git worktree, all from one dashboard.**
4
4
 
5
5
  [![npm](https://img.shields.io/npm/v/@loicngr/kobo.svg)](https://www.npmjs.com/package/@loicngr/kobo)
6
6
  [![license](https://img.shields.io/npm/l/@loicngr/kobo.svg)](./LICENSE)
7
7
  [![node](https://img.shields.io/node/v/@loicngr/kobo.svg)](https://nodejs.org/)
8
8
 
9
- Kōbō runs multiple coding agents in parallel, each isolated in its own git worktree, branch, and dev server. Its installable Vue/Quasar dashboard streams output, tasks, git state, and quota usage across every workspace.
10
-
11
- ![Kōbō workspace view: live chat and git panel](docs/assets/images/workspace-chat.png)
9
+ Kōbō (工房, "workshop") turns *one agent, one terminal* into a real workflow. Spin up as many isolated workspaces as you need, watch every agent work live, review and merge their diffs from a Monaco-based Git panel, and let auto-loop or scheduled wakeups keep them going while you're away.
12
10
 
13
11
  > [!NOTE]
14
12
  > Active development on `develop`. Forward-only migrations and timestamped pre-migration backups keep upgrades safe.
15
13
 
16
- ## Features
17
-
18
- - **Isolated worktrees**: each workspace is a dedicated git worktree on its own branch, so parallel sessions never collide.
19
- - **Two agent engines**: Claude Code (via `@anthropic-ai/claude-agent-sdk`) and OpenAI Codex (via `codex app-server`). Switch between them in the same workspace without creating another worktree: Kōbō starts a fresh session for the target engine and provides an editable handoff with tasks, Git state, diff summary, and recent context.
20
- - **Live chat**: streaming text, inline Edit/Write diffs, per-turn cards, a compaction-in-progress indicator, infinite scrollback, and a reasoning panel immediately above the message input while Claude or Codex is actively thinking. `/` autocompletes skills and commands, `@` fuzzy-autocompletes worktree file paths, and `Ctrl+F` searches readable messages in the current workspace: selecting a result loads its session and scrolls to it. `Ctrl+K` opens a command palette for common workspace actions. The global search page deep-links to the same location. Sessions retain their engine and can be renamed or permanently deleted with only their linked history. Messages queued while an agent is busy stay scoped to their workspace and session. **Send now** delivers a queued message to an active Claude Code stream or steers the active Codex turn; it remains queued until the engine confirms acceptance. Use `Enter` to send, `Ctrl+Enter` to force delivery while an agent is busy, and `Shift+Enter` or `Ctrl+J` for a new line.
21
- - **Full MCP toolset (`kobo-tasks`)**: a per-workspace MCP server the agent uses for far more than tasks — task/acceptance-criteria CRUD, starting/stopping the dev server and reading its logs, a unified `get_ticket` (Notion or Sentry), searching past conversations across every workspace, per-session token/cost usage, and a `.ai/thoughts` decision log. Native Claude Code Task tools complement it for lightweight sub-agent coordination. See [`AGENTS.md`](./AGENTS.md) for the full tool list.
22
-
23
- ![Sub-agents panel showing parallel tool calls](docs/assets/images/sub-agents-panel.png)
24
- - **Git panel**: a Monaco-based diff viewer with **inline file editing** (edit the right-hand panel directly, save with `Ctrl/Cmd+S`, conflict-guarded via sha precondition) and syntax highlighting for PHP, Vue SFCs, JavaScript, TypeScript, and the existing web/config languages. It also provides inline conflict resolution and `Sync` / `Push` / `Commit` / `Open PR` / `Merge ready PR` / `Change PR base` / `Change source branch`. You can write the commit message yourself or ask the active engine to prepare the commit; PR/MR merging always requires a confirmation, then Kōbō can offer to delete the merged remote branch. Edit/Write cards in chat open their exact changed file in the diff viewer. Multi-forge: GitHub (`gh`), GitLab (`glab`), Bitbucket Community (`bkt`), or no forge, auto-detected from the remote and overridable per project.
14
+ ![Kōbō workspace view: live chat and git panel](docs/assets/images/workspace-chat.png)
25
15
 
26
- ![Diff viewer with side-by-side changes](docs/assets/images/diff-viewer.png)
27
- - **Dev server panel**: start, stop, and tail logs for a workspace's dev server (Docker or npm) straight from the Tools panel — no need to leave the UI. When a project has no configured server, the panel and MCP tool report that explicitly instead of implying one is running.
28
- - **Attention indicators**: workspace cards surface CI failures, review-requested changes, and a conflict-aware **ready-to-merge** badge, plus a one-click **Fix CI** button that spawns a fix workspace automatically. Seven PR/MR and CI transitions each have their own audio toggle, sound, and volume; simultaneous transitions play sequentially.
29
- - **Interactive Q&A**: an agent can pause mid-session to ask a clarifying question through the UI; the workspace surfaces under "Needs Attention" until you answer. Choosing **Other** opens an inline free-form field, and option previews are available on hover. Codex native interactive questions require its `plan` permission mode.
16
+ ## Why Kōbō
30
17
 
31
- ![Agent asking a clarifying question, awaiting the user's answer](docs/assets/images/agent-question.png)
32
- - **Auto-loop**: an opt-in mode that walks the task list, spawning a fresh session per task and stopping once there's nothing left to do, progress stalls, an error occurs, or the agent needs input from you. Rate-limit and temporary upstream failures retry on the quota backoff ladder; Settings controls the maximum number of automatic retries. Optionally run the initial brainstorming session on a stronger model and every task after that on a cheaper one, without leaving the engine you picked.
33
- - **Quota-aware**: 5-hour / 7-day Claude usage and Codex rate-limit buckets sit in the footer, and sessions auto-resume after a rate-limit reset.
34
- - **Scheduled wakeups**: the agent schedules a one-shot wake-up via the `schedule_wakeup` MCP tool. Kōbō persists it across restarts, shows a live countdown, and re-invokes the agent with the stored prompt at the chosen time.
35
- - **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.
36
- - **Lifecycle scripts**: shell scripts run automatically at key moments — **setup** (worktree created), **cleanup** (session ended), **archive** (workspace archived). Configure them globally or per project, with their output streamed into the chat.
37
- - **Disk-space purge**: free a merged workspace's disk space without losing its chat history — see [below](#disk-space-purge).
38
- - **Observability**: a compact session timeline in the right panel shows each session’s duration, tools, tokens, and errors; download a redacted workspace diagnostic JSON for troubleshooting.
39
- - **Optional integrations**: independently enable or disable Notion (import missions) and Sentry (fix from issue URL) in Settings, with a **Test connection** action for each; local voice transcription uses whisper.cpp.
40
- - **Installable PWA**: production builds work as a Progressive Web App. Install Kōbō from your browser on desktop or mobile for an app-like window; it signals offline state, WebSocket reconnection, and offers an explicit reload when an update is ready. Live agent activity still requires a connection to the local Kōbō server.
18
+ | | |
19
+ |---|---|
20
+ | **🗂️ Isolated worktrees, two engines** | Every workspace is its own git worktree and branch — parallel sessions never collide. Pick Claude Code or OpenAI Codex per workspace, and switch engines later without losing the worktree: Kōbō hands off tasks, git state, and recent context to a fresh session. |
21
+ | **💬 Live chat, real diff review** | Streaming responses, inline Edit/Write diffs, a reasoning panel, and a Monaco diff viewer with **inline file editing**, conflict resolution, and one-click `Sync` / `Push` / `Open PR` / `Merge`. |
22
+ | **🔁 Auto-loop, cron, wakeups** | Turn an agent loose on the task list: it works through tasks, retries on rate limits, and stops itself when there's nothing left, progress stalls, or it needs you. Cron schedules and one-shot wakeups keep workspaces moving on their own timeline. |
23
+ | **🔀 Create from a PR/MR, or from a ticket** | Pick an open pull/merge request from GitHub, GitLab, or Bitbucket and Kōbō resolves every local conflict for you before spinning up the workspace. Or start straight from a Notion page or a Sentry issue URL. |
41
24
 
42
25
  ## Quick start
43
26
 
@@ -47,29 +30,35 @@ Requires Node.js ≥ 24.15 and a logged-in Claude Code **or** Codex CLI.
47
30
  npx @loicngr/kobo@latest
48
31
  ```
49
32
 
50
- Default port is `3000`. If you already run something on that port (or another Kōbō instance), pick your own. `SERVER_PORT` is read first, `PORT` is the fallback:
33
+ Open <http://localhost:3000>. Data is persisted under `~/.config/kobo/` (override via `KOBO_HOME`).
34
+
35
+ Default port is `3000`; if it's taken, `SERVER_PORT` (checked first) or `PORT` picks another:
51
36
 
52
37
  ```bash
53
38
  SERVER_PORT=9997 PORT=9998 npx @loicngr/kobo@latest
54
39
  ```
55
40
 
56
- Open <http://localhost:3000> (or whichever port you picked). Data is persisted under `~/.config/kobo/` (override via `KOBO_HOME`).
41
+ Kōbō's production build is an installable PWA — use your browser's **Install app** action. Want to run from source or contribute instead? See [`CONTRIBUTING.md`](./CONTRIBUTING.md). Prefer Docker? Jump to [Docker deployment](#docker-deployment).
57
42
 
58
- ### Install as an app
43
+ ![Diff viewer with side-by-side changes](docs/assets/images/diff-viewer.png)
59
44
 
60
- Kōbō's production build is a PWA. When opening a production instance, use your browser's **Install app** / **Add to Home Screen** action to install it. It shows an offline notice and lets you explicitly reload when an update is ready, so an active session is never interrupted automatically. The development client (`npm run dev:client`) intentionally runs as a normal Vite application, so it does not expose an install prompt.
45
+ ## Everything else Kōbō does
61
46
 
62
- Want to run from source or contribute? See [`CONTRIBUTING.md`](./CONTRIBUTING.md).
47
+ - **Command palette & search**: `/` autocompletes skills and commands, `@` fuzzy-completes worktree file paths, `Ctrl+F` searches readable messages, `Ctrl+K` opens a command palette. The global search page deep-links to the same spot.
48
+ - **Full MCP toolset (`kobo-tasks`)**: task/acceptance-criteria CRUD, dev server control, a unified `get_ticket` (Notion or Sentry), cross-workspace conversation search, per-session usage, and a `.ai/thoughts` decision log — see [`AGENTS.md`](./AGENTS.md) for the full tool list.
63
49
 
64
- ### Docker
50
+ ![Sub-agents panel showing parallel tool calls](docs/assets/images/sub-agents-panel.png)
51
+ - **Multi-forge Git panel**: GitHub (`gh`), GitLab (`glab`), or Bitbucket Community (`bkt`), auto-detected from the remote — `Open PR`, `Merge ready PR`, `Change PR base`, `Change source branch`, all from the UI.
52
+ - **Dev server panel**: start, stop, and tail a workspace's dev server (Docker or npm) from the Tools panel.
53
+ - **Attention indicators**: CI failures, review-requested changes, and a conflict-aware **ready-to-merge** badge on every workspace card, plus a one-click **Fix CI** button.
54
+ - **Interactive Q&A**: an agent can pause mid-session to ask you a question through the UI instead of guessing.
65
55
 
66
- An official `Dockerfile` and three ready-to-use Compose files ship in this repository: a quick local
67
- test stack, a Traefik-fronted local rehearsal (`kobo.localhost`, no domain needed), and a full VPS
68
- reference (Traefik + Let's Encrypt, SSH access, optional Docker-socket passthrough for the
69
- dev-server panel). See [`CONFIGURATION.md`](./CONFIGURATION.md#docker-deployment) for the complete
70
- setup — every compose file, every env var, every volume mount, and the `KOBO_NETWORK_ACCESS_ENABLED`
71
- / `KOBO_NETWORK_ACCESS_BEHIND_PROXY` env vars that bootstrap network access at container boot with
72
- no manual step.
56
+ ![Agent asking a clarifying question, awaiting the user's answer](docs/assets/images/agent-question.png)
57
+ - **Quota-aware**: 5-hour / 7-day Claude usage and Codex rate-limit buckets live in the footer; sessions auto-resume after a reset.
58
+ - **Disk-space purge**: reclaim a merged workspace's `node_modules`/`vendor` weight without losing its chat history — see [`CONFIGURATION.md`](./CONFIGURATION.md#auto-purge-worktree-on-pr-merged).
59
+ - **Lifecycle scripts**: shell scripts run automatically on setup, cleanup, and archive, with output streamed into the chat.
60
+ - **Observability**: a per-session timeline (duration, tools, tokens, errors) and a downloadable redacted diagnostic JSON.
61
+ - **Optional integrations**: Notion (import missions) and Sentry (fix from issue URL), each independently toggled with a **Test connection** action; local voice transcription via `whisper.cpp`.
73
62
 
74
63
  ## Configuration
75
64
 
@@ -77,77 +66,32 @@ The most common knobs:
77
66
 
78
67
  | Env var | Default | Purpose |
79
68
  |---|---|---|
80
- | `PORT` | `3000` | HTTP / WebSocket server port (overridden by `SERVER_PORT` if set) |
81
- | `SERVER_PORT` | none | Preferred override for the server port; takes precedence over `PORT` |
69
+ | `PORT` / `SERVER_PORT` | `3000` | HTTP / WebSocket server port (`SERVER_PORT` takes precedence) |
82
70
  | `KOBO_HOME` | `~/.config/kobo` | Data directory (SQLite, settings, voice models) |
83
71
  | `NOTION_API_TOKEN` | none | Notion integration token |
84
72
  | `ANTHROPIC_API_KEY` | none | Claude Code engine credential (alternative to `claude /login`) |
85
73
  | `OPENAI_API_KEY` | none | Codex engine credential (alternative to `codex login`) |
86
74
 
87
- Global and per-project settings (worktree path, dev server commands, E2E framework, prompt templates, git conventions, branch prefixes, lifecycle scripts, task prompt) are edited in **Settings** at runtime. Notion and Sentry are enabled independently in their respective tabs; disabling one keeps its configuration and existing workspace links, but removes its import and automation UI. **Settings → Forge** holds local Bitbucket Community credentials for `bkt`; Kōbō uses them for PR actions and provides them only to agents working in Bitbucket projects. In **Settings → General → Activity feed**, you can hide agent reasoning panels. Per-project values inherit from the global ones when left empty.
88
-
89
- The full reference (every env var, every setting key, MCP server registration, Notion / Sentry / Voice setup) is in [`CONFIGURATION.md`](./CONFIGURATION.md).
90
-
91
- ## Agent runtimes
92
-
93
- - **Claude Code.** Authenticate once with `claude /login`. Kōbō calls the embedded SDK directly, so no `claude` binary is required at runtime.
94
- - **OpenAI Codex.** Run `codex login` or export `OPENAI_API_KEY`. Kōbō spawns a long-lived `codex app-server` subprocess per workspace and bridges its JSON-RPC stream to the same UI.
95
-
96
- You pick the engine at workspace creation and can switch it later from the workspace header. A switch never attempts to resume a Claude conversation in Codex (or the reverse): it stops the active agent, starts a fresh target-engine session in the same worktree, and lets you review the handoff before launch. The new agent can retrieve further context from prior sessions through the read-only `kobo__read_workspace_events_csv` and `kobo__search_codebase` MCP tools. Both engines 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).
97
-
98
- ## Disk-space purge
99
-
100
- A merged workspace is automatically archived, but its worktree folder usually carries a lot of weight (`node_modules`, `vendor`, build artefacts…). Kōbō frees that space without losing anything queryable:
101
-
102
- - **Manual**: workspace context menu → *Free disk space (delete worktree)*. The worktree is removed; the chat history and PR metadata stay in the database.
103
- - **Automatic**: **Settings → Worktrees → Auto-purge worktree on PR merged**. When the pr-watcher sees the OPEN → MERGED transition, it archives **and** purges.
104
- - **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. No UI action needed.
105
- - **Permission errors**: if removal hits `EACCES`/`EPERM` (common with Docker containers writing as `root`), Kōbō first tries to auto-recover by `chown`-ing the worktree from a throwaway Docker container. If that isn't possible, it shows a toast with a copy-pasteable manual recovery command. Full troubleshooting (ACL setup, Docker `USER` directive, manual `chown`) is in [`CONFIGURATION.md`](./CONFIGURATION.md#permission-errors-during-purge).
106
-
107
- ## Optional integrations
75
+ Everything else — worktree paths, dev server commands, prompt templates, git conventions, lifecycle scripts, forge selection, permission modes — lives in **Settings**, with per-project values inheriting from global ones. The full reference (every env var, every setting key, MCP server registration, forge/Notion/Sentry/voice setup) is in [`CONFIGURATION.md`](./CONFIGURATION.md).
108
76
 
109
- 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.
110
-
111
- - **Notion**: import missions, tasks, and acceptance criteria from a Notion page. Enable it in **Settings → Notion** when you want its creation/import flow, then use **Test connection** to validate its MCP credentials.
112
- - **Sentry**: paste an issue URL to spawn a fix workspace with the stacktrace, tags, and a TDD workflow. Enable it independently in **Settings → Sentry**; **Test connection** validates its MCP credentials and authenticated identity.
113
- - **Voice transcription**: local push-to-talk via [`whisper.cpp`](https://github.com/ggml-org/whisper.cpp).
77
+ ### Docker
114
78
 
115
- See [`CONFIGURATION.md`](./CONFIGURATION.md) for the setup of each.
79
+ An official `Dockerfile` and three ready-to-use Compose files ship in this repository: a quick local test stack, a Traefik-fronted local rehearsal (no domain needed), and a full VPS reference (Traefik + Let's Encrypt, SSH access, optional Docker-socket passthrough). See [`CONFIGURATION.md`](./CONFIGURATION.md#docker-deployment) for every compose file, env var, and volume mount.
116
80
 
117
- ## Network access
81
+ ### Network access
118
82
 
119
- By default, Kōbō binds to `127.0.0.1` only (localhost). To control Kōbō from
120
- another device on the same Wi-Fi or LAN:
83
+ Kōbō binds to `127.0.0.1` only by default. Enabling **Settings → Global → Network access** re-binds to the LAN behind a shared token (a QR code makes pairing a phone easy). Plain HTTP — keep it to trusted networks, or front it with HTTPS/a VPN for anything further. Details in [`CONFIGURATION.md`](./CONFIGURATION.md#network-access).
121
84
 
122
- 1. Open **Settings → Global → Network access** and enable it.
123
- 2. Restart Kōbō when prompted, since the server must re-bind to apply the change.
124
- 3. Scan the QR code shown in the Settings panel from your phone, or copy a LAN
125
- URL and paste the token in the login dialog on the remote device.
85
+ ## Agent runtimes
126
86
 
127
- > **Trusted networks only.** Kōbō uses plain HTTP, so the token travels in cleartext.
128
- > Do not expose the port to the internet. For remote access over the internet,
129
- > put a terminating HTTPS proxy or a VPN (e.g. Tailscale) in front of Kōbō.
130
- >
131
- > **Production only.** This protection applies when running a built Kōbō
132
- > (`npm start` / `npx @loicngr/kobo`). In development (`npm run dev:all`) the Vite
133
- > dev server is always exposed on the LAN and bypasses the token. See
134
- > [`CONFIGURATION.md`](./CONFIGURATION.md#production-vs-development-mode-important).
87
+ - **Claude Code**: authenticate once with `claude /login`. Kōbō calls the embedded SDK directly — no `claude` binary needed at runtime.
88
+ - **OpenAI Codex**: run `codex login` or export `OPENAI_API_KEY`. Kōbō spawns a long-lived `codex app-server` subprocess per workspace.
135
89
 
136
- See [`CONFIGURATION.md`](./CONFIGURATION.md#network-access) for token management,
137
- QR code details, and all security caveats.
90
+ Both engines share task tracking, permission modes, the sub-agent panel, and the quota footer. The mapping of Kōbō's four permission modes (`plan` / `bypass` / `strict` / `interactive`) to each engine's native sandbox semantics is in [`CONFIGURATION.md`](./CONFIGURATION.md#permission-modes).
138
91
 
139
92
  ## Skill suites
140
93
 
141
- Kōbō's auto-generated prompts (review, auto-loop grooming, QA, brainstorming) can target four different skill ecosystems, selectable in **Settings → Skills**:
142
-
143
- - **[superpowers](https://github.com/obra/superpowers)** (default): a plugin for Claude Code with the brainstorm → spec → plan → execute discipline, TDD, debugging, code review.
144
- - **[gstack](https://github.com/garrytan/gstack)**: CLI slash commands for navigation, QA, design review, ship pipeline, second-opinion via Codex.
145
- - **superpowers + gstack**: both, with each used for what it does best.
146
- - **custom**: write your own prompts.
147
-
148
- Optionally pair with **[gbrain](https://github.com/garrytan/gbrain)**, a per-project knowledge graph + semantic search exposed as an MCP server. It is inherited automatically from your `~/.claude.json` config.
149
-
150
- Full install instructions and the prompt-suite differences are in [`CONFIGURATION.md`](./CONFIGURATION.md#skill-suites).
94
+ Kōbō's auto-generated prompts (review, auto-loop grooming, QA, brainstorming) can target **[superpowers](https://github.com/obra/superpowers)** (brainstorm → spec → plan → execute, TDD, debugging), **[gstack](https://github.com/garrytan/gstack)** (slash-command workflows for QA, design review, ship pipelines), both together, or your own custom prompts — selectable in **Settings → Skills**. Pairs optionally with **[gbrain](https://github.com/garrytan/gbrain)** for per-project semantic search. Full setup in [`CONFIGURATION.md`](./CONFIGURATION.md#skill-suites).
151
95
 
152
96
  ## Architecture
153
97
 
@@ -674,6 +674,24 @@ export const migrations = [
674
674
  }
675
675
  },
676
676
  },
677
+ {
678
+ version: 38,
679
+ name: 'add-quota-backoff-reason',
680
+ migrate: (db) => {
681
+ const table = db
682
+ .prepare("SELECT name FROM sqlite_master WHERE type = 'table' AND name = 'pending_quota_backoffs'")
683
+ .get();
684
+ if (!table)
685
+ return;
686
+ const columns = db.prepare('PRAGMA table_info(pending_quota_backoffs)').all().map((column) => column.name);
687
+ if (!columns.includes('reason')) {
688
+ // Existing rows predate the quota/transient split — every one of them
689
+ // was armed by the old code path that only ever meant "real quota",
690
+ // so 'quota' is the correct backfill, not a placeholder guess.
691
+ db.exec("ALTER TABLE pending_quota_backoffs ADD COLUMN reason TEXT NOT NULL DEFAULT 'quota'");
692
+ }
693
+ },
694
+ },
677
695
  ];
678
696
  /** Current schema version — always equals the highest migration version. */
679
697
  export const SCHEMA_VERSION = migrations.length > 0 ? migrations[migrations.length - 1].version : 1;
@@ -154,6 +154,7 @@ export function initSchema(db) {
154
154
  target_at TEXT NOT NULL,
155
155
  resets_at TEXT,
156
156
  source TEXT NOT NULL CHECK (source IN ('rate_limit_info', 'usage_api', 'fallback_ladder')),
157
+ reason TEXT NOT NULL DEFAULT 'quota',
157
158
  retry_count INTEGER NOT NULL DEFAULT 0,
158
159
  created_at TEXT NOT NULL
159
160
  );
@@ -231,6 +231,26 @@ function parsePrNumberFromUrl(prUrl) {
231
231
  }
232
232
  return number;
233
233
  }
234
+ // Mirrors the private `WORKSPACE_NAME_MAX_LENGTH` in workspace-service.ts —
235
+ // duplicated rather than exported to avoid coupling this route to that
236
+ // module's internals (and the many `vi.mock(workspace-service.js, factory)`
237
+ // test files that would otherwise need updating for an export nothing else
238
+ // consumes).
239
+ const WORKSPACE_NAME_MAX_LENGTH = 200;
240
+ /**
241
+ * Cap an auto-derived title (Notion page title, Sentry issue title, PR/MR
242
+ * title) before handing it to `updateWorkspaceName`. That function throws on
243
+ * anything longer — the right behaviour for a name the user typed
244
+ * themselves, but titles pulled from an external source (a long exception
245
+ * message, a verbose PR title) are not user input: truncating them is the
246
+ * sane default, matching the existing `slugifyBranchSegment` precedent for
247
+ * auto-derived branch names.
248
+ */
249
+ function truncateWorkspaceName(name) {
250
+ if (name.length <= WORKSPACE_NAME_MAX_LENGTH)
251
+ return name;
252
+ return `${name.slice(0, WORKSPACE_NAME_MAX_LENGTH - 1)}…`;
253
+ }
234
254
  app.get('/', (c) => {
235
255
  try {
236
256
  const workspaces = workspaceService.listWorkspaces();
@@ -688,7 +708,7 @@ app.post('/', migrationGuard, async (c) => {
688
708
  // identifiable in the sidebar without opening the panel.
689
709
  if (sentryContent?.title && !notionContent?.title && workspace.name === 'workspace') {
690
710
  const prefix = sentryContent.issueId ? `${sentryContent.issueId} | ` : '';
691
- workspace = workspaceService.updateWorkspaceName(workspace.id, `${prefix}${sentryContent.title}`);
711
+ workspace = workspaceService.updateWorkspaceName(workspace.id, truncateWorkspaceName(`${prefix}${sentryContent.title}`));
692
712
  }
693
713
  currentStep = 'create-tasks';
694
714
  emitCreateProgress(creationId, currentStep);
@@ -713,7 +733,7 @@ app.post('/', migrationGuard, async (c) => {
713
733
  // provide a custom name. Prefix with the Notion unique-id (e.g.
714
734
  // Use the Notion page title when the user left the generic placeholder.
715
735
  if (notionContent.title && workspace.name === 'workspace') {
716
- workspace = workspaceService.updateWorkspaceName(workspace.id, notionContent.title);
736
+ workspace = workspaceService.updateWorkspaceName(workspace.id, truncateWorkspaceName(notionContent.title));
717
737
  }
718
738
  }
719
739
  // Update workspace name with the PR/MR title if the user did not provide
@@ -721,7 +741,7 @@ app.post('/', migrationGuard, async (c) => {
721
741
  // makes the PR a THIRD context source, lowest priority of the three
722
742
  // (Notion > Sentry > PR), consistent with the checks above.
723
743
  if (prContent && !notionContent?.title && !sentryContent?.title && workspace.name === 'workspace') {
724
- workspace = workspaceService.updateWorkspaceName(workspace.id, prContent.title);
744
+ workspace = workspaceService.updateWorkspaceName(workspace.id, truncateWorkspaceName(prContent.title));
725
745
  }
726
746
  // Create manual tasks/criteria if no Notion content was extracted
727
747
  if (!notionContent && (Array.isArray(body.tasks) || Array.isArray(body.acceptanceCriteria))) {
@@ -1799,7 +1799,7 @@ async function handleQuota(workspaceId, _agentSessionId) {
1799
1799
  retryCounts.set(workspaceId, retryCount + 1);
1800
1800
  // The quotaBackoffService owns the timer + the persistent row + the
1801
1801
  // 'agent:quota-backoff' WS emit. Hand off everything to it.
1802
- quotaBackoffService.arm(workspaceId, delayMs, { resetsAt: resetsAt ?? null, source });
1802
+ quotaBackoffService.arm(workspaceId, delayMs, { resetsAt: resetsAt ?? null, source, reason: 'quota' });
1803
1803
  }
1804
1804
  /**
1805
1805
  * Use the same persisted retry path for temporary upstream failures (HTTP 500
@@ -1827,7 +1827,7 @@ async function handleTransientAutoLoopFailure(workspaceId) {
1827
1827
  }
1828
1828
  const { delayMs, resetsAt, source } = await computeQuotaBackoffMs(workspaceId, retryCount, false);
1829
1829
  retryCounts.set(workspaceId, retryCount + 1);
1830
- quotaBackoffService.arm(workspaceId, delayMs, { resetsAt: resetsAt ?? null, source });
1830
+ quotaBackoffService.arm(workspaceId, delayMs, { resetsAt: resetsAt ?? null, source, reason: 'transient' });
1831
1831
  }
1832
1832
  /** @internal test-only — re-export of `handleQuota` for direct testing. */
1833
1833
  export const _handleQuota = handleQuota;
@@ -9,6 +9,7 @@ function rowToPending(row) {
9
9
  targetAt: row.target_at,
10
10
  resetsAt: row.resets_at,
11
11
  source: row.source,
12
+ reason: row.reason,
12
13
  retryCount: row.retry_count,
13
14
  createdAt: row.created_at,
14
15
  };
@@ -29,14 +30,15 @@ export function arm(workspaceId, delayMs, meta) {
29
30
  .prepare('SELECT retry_count FROM pending_quota_backoffs WHERE workspace_id = ?')
30
31
  .get(workspaceId);
31
32
  const retryCount = (existing?.retry_count ?? 0) + 1;
32
- db.prepare(`INSERT INTO pending_quota_backoffs (workspace_id, target_at, resets_at, source, retry_count, created_at)
33
- VALUES (?, ?, ?, ?, ?, ?)
33
+ db.prepare(`INSERT INTO pending_quota_backoffs (workspace_id, target_at, resets_at, source, reason, retry_count, created_at)
34
+ VALUES (?, ?, ?, ?, ?, ?, ?)
34
35
  ON CONFLICT(workspace_id) DO UPDATE SET
35
36
  target_at = excluded.target_at,
36
37
  resets_at = excluded.resets_at,
37
38
  source = excluded.source,
39
+ reason = excluded.reason,
38
40
  retry_count = excluded.retry_count,
39
- created_at = excluded.created_at`).run(workspaceId, targetAt, meta.resetsAt, meta.source, retryCount, now.toISOString());
41
+ created_at = excluded.created_at`).run(workspaceId, targetAt, meta.resetsAt, meta.source, meta.reason, retryCount, now.toISOString());
40
42
  const previous = timers.get(workspaceId);
41
43
  if (previous)
42
44
  clearTimeout(previous);
@@ -47,6 +49,7 @@ export function arm(workspaceId, delayMs, meta) {
47
49
  targetAt,
48
50
  resetsAt: meta.resetsAt,
49
51
  source: meta.source,
52
+ reason: meta.reason,
50
53
  retryCount,
51
54
  });
52
55
  }
@@ -22,7 +22,11 @@ const VALID_TRANSITIONS = {
22
22
  completed: ['idle', 'executing'],
23
23
  idle: ['executing', 'brainstorming', 'extracting', 'error'],
24
24
  error: ['idle', 'executing', 'brainstorming', 'extracting'],
25
- quota: ['idle', 'executing'],
25
+ // `awaiting-user` is reachable from `quota` too: a resumed quota-backoff
26
+ // session can hit an interactive tool-approval/question before its status
27
+ // flips to `executing` — the same reasoning as the entering-quota comment
28
+ // above, just for the other direction.
29
+ quota: ['idle', 'executing', 'awaiting-user'],
26
30
  };
27
31
  // NOTE: `engine` is stored as a plain TEXT column and returned as a `string` on
28
32
  // the `Workspace` interface rather than the stricter `EngineId` union. The DB
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@loicngr/kobo",
3
- "version": "1.11.23",
3
+ "version": "1.11.25",
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{n as e}from"./vue-i18n-C3W5i7h6.js";import{B as t,C as n,St as r,wt as i,yt as a}from"./reactivity.esm-bundler-DtSZKRQB.js";import{$ as o,A as s,At as c,B as l,Dt as u,Et as d,H as f,It as p,Mt as m,Ot as h,Rt as g,Tt as _,U as v,V as y,W as b,Wt as x,X as S,Z as C,_n as w,fn as T,st as E,t as D,ut as O,w as k}from"./runtime-dom.esm-bundler-DgevF6KV.js";import{S as A,W as j,Z as M,b as N,l as P,t as F,w as ee}from"./QBtn-B7WFdfmy.js";import{O as te,c as I,d as L,k as ne}from"./use-id-DlouqdB8.js";import{H as re,T as ie,h as ae,lt as R,m as oe,w as se}from"./index-okqx5CVc.js";import{t as ce}from"./QSpinnerDots-DWOFEezR.js";import{r as z}from"./QItemSection-5GBV9MR2.js";import{_ as le,b as B,x as V}from"./permissionModes-Du6lXebH.js";import{r as ue,t as H}from"./pending-diff-open-nLdNq-y-.js";import{t as U}from"./documents-B6eOZeFK.js";import{a as W,i as G}from"./layout-nWIyHeJf.js";import{i as de,n as fe,o as pe,r as me,t as K}from"./render-chat-markdown-DrGdVCku.js";import{a as he,i as ge,n as _e,o as ve,r as ye,s as be}from"./WorkspacePage-C8SAYVkt.js";var q=[`horizontal`,`vertical`,`cell`,`none`],J=M({name:`QMarkupTable`,props:{...ne,dense:Boolean,flat:Boolean,bordered:Boolean,square:Boolean,wrapCells:Boolean,separator:{type:String,default:`horizontal`,validator:e=>q.includes(e)}},setup(e,{slots:t}){let n=E(),r=te(e,n.proxy.$q),i=l(()=>`q-markup-table q-table__container q-table__card q-table--${e.separator}-separator`+(r.value?` q-table--dark q-table__card--dark q-dark`:``)+(e.dense?` q-table--dense`:``)+(e.flat?` q-table--flat`:``)+(e.bordered?` q-table--bordered`:``)+(e.square?` q-table--square`:``)+(e.wrapCells?``:` q-table--no-wrap`));return()=>O(`div`,{class:i.value},[O(`table`,{class:`q-table`},A(t.default))])}});function xe(e,t){return O(`div`,e,[O(`table`,{class:`q-table`},t)])}var Se={list:z,table:J},Ce=[`list`,`table`,`__qtable`],we=M({name:`QVirtualScroll`,props:{...V,type:{type:String,default:`list`,validator:e=>Ce.includes(e)},items:{type:Array,default:()=>[]},itemsFn:Function,itemsSize:Number,scrollTarget:L},setup(e,{slots:t,attrs:r}){let i,a=n(null),o=l(()=>e.itemsSize>=0&&e.itemsFn!==void 0?Number.parseInt(e.itemsSize,10):Array.isArray(e.items)?e.items.length:0),{virtualScrollSliceRange:s,localResetVirtualScroll:f,padVirtualScroll:p,onVirtualScrollEvt:g}=B({virtualScrollLength:o,getVirtualScrollTarget:x,getVirtualScrollEl:b}),_=l(()=>{if(o.value===0)return[];let t=(e,t)=>({index:s.value.from+t,item:e});return e.itemsFn===void 0?e.items.slice(s.value.from,s.value.to).map(t):e.itemsFn(s.value.from,s.value.to-s.value.from).map(t)}),v=l(()=>`q-virtual-scroll q-virtual-scroll`+(e.virtualScrollHorizontal?`--horizontal`:`--vertical`)+(e.scrollTarget===void 0?` scroll`:``)),y=l(()=>e.scrollTarget===void 0?{tabindex:0}:{});T(o,()=>{f()}),T(()=>e.scrollTarget,()=>{C(),S()});function b(){let e=a.value;return e===null?null:e.$el||e}function x(){return i}function S(){let t=b();t!==null&&(i=I(t,e.scrollTarget),i.addEventListener(`scroll`,g,j.passive))}function C(){i!==void 0&&(i.removeEventListener(`scroll`,g,j.passive),i=void 0)}function w(){let n=p(e.type===`list`?`div`:`tbody`,_.value.map(t.default));return t.before!==void 0&&(n=t.before().concat(n)),N(t.after,n)}return u(()=>{f()}),m(()=>{S()}),d(()=>{S()}),c(()=>{C()}),h(()=>{C()}),()=>{if(t.default===void 0){console.error(`QVirtualScroll: default scoped slot is required for rendering`);return}return e.type===`__qtable`?xe({ref:a,class:`q-table__middle `+v.value},w()):O(Se[e.type],{...r,ref:a,class:[r.class,v.value],...y.value},w)}}}),Te=new Set([`setup`,`cleanup`,`archive`]);function Ee(e){switch(e.type){case`user`:return e.sender===`system-prompt`?`system-prompt`:Te.has(e.sender)?`script`:`user`;case`session`:return`session`;default:return`agent`}}function De(e){let t=[],n=null,r=null;for(let i of e){let e=Ee(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}function Y(e){switch(e.type){case`text`:return`text:${e.messageId}`;case`thinking`:return`thinking:${e.messageId}:${e.eventIds?.[0]??e.ts??``}`;case`tool`:return`tool:${e.toolCallId}`;case`session`:return`session:${e.kind}:${e.eventIds?.[0]??e.ts??``}`;case`user`:return`user:${e.eventIds?.[0]??`${e.sender}:${e.ts??``}`}`;case`error`:return`error:${e.eventIds?.[0]??`${e.category}:${e.ts??``}`}`}}function Oe(e){let t=e.items[0];return`${e.speaker}:${t?Y(t):`empty`}`}function ke(e,t){let n=Math.min(t,e.length)-1;for(let t=n;t>=0;t--)if(e[t]?.speaker===`user`)return t;return-1}function Ae(e,t,n){return new Promise(r=>{let i=!1,a=null,o=null,s=e=>{i||(i=!0,a!==null&&clearTimeout(a),a=null,o?.(),r(e))};if(o=T(e,e=>{t(e)&&s(!0)},{immediate:!0}),i){o();return}a=setTimeout(()=>s(!1),n)})}var je={class:`agent-error-item`},X={class:`agent-error-item__body`},Me={class:`agent-error-item__title`},Z={class:`agent-error-item__message`},Ne=G(o({__name:`AgentErrorItem`,props:{item:{}},setup(t){let n=t,{te:r}=e(),a=l(()=>{let e=`agent.error.${n.item.category}`;return r(e)?e:`agent.error.other`});return(e,n)=>(g(),b(`div`,je,[C(P,{name:`error_outline`,size:`16px`,class:`agent-error-item__icon`}),y(`div`,X,[y(`div`,Me,i(e.$t(a.value)),1),y(`div`,Z,i(t.item.message),1)])]))}}),[[`__scopeId`,`data-v-e98c3c02`]]),Q={class:`text-caption text-kobo-3`},$=o({__name:`SessionEventItem`,props:{item:{}},setup(e){let t=e,n=l(()=>{switch(t.item.kind){case`started`:return`session.started`;case`ended`:return be(t.item.detail);case`compacted`:return`session.compacted`;default:return`session.started`}});return(e,t)=>(g(),b(`span`,Q,i(e.$t(n.value)),1))}});function Pe(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){Fe(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 Fe(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=Ie(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 Ie(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 Le=[`innerHTML`],Re=G(o({__name:`TextMessageItem`,props:{item:{}},setup(e){let t=e,n=U(),r=oe(),i=l(()=>{let e=r.selectedWorkspaceId;return e?n.documentsFor(e).map(e=>e.path):[]}),a=l(()=>{let e=t.item;if(e.streaming)return K(e.text);let n=i.value;return me(`${e.messageId}|${n.join(`,`)}`,e.text,e=>{let t=Pe(pe.parse(e,{async:!1,breaks:!0,gfm:!0}),n);return de(t,{addAttr:[`data-document-path`]})})});function o(e){let t=e.target?.closest(`.document-link`);if(!t)return;e.preventDefault();let i=t.getAttribute(`data-document-path`),a=r.selectedWorkspaceId;!i||!a||n.openDocumentByPath(a,i)}return(t,n)=>(g(),b(`div`,{class:`markdown-message`,onClick:o},[y(`div`,{innerHTML:a.value},null,8,Le),e.item.streaming?(g(),f(ee,{key:0,size:`xs`,class:`q-ml-xs`})):v(``,!0)]))}}),[[`__scopeId`,`data-v-539815f6`]]);function ze(e,t){let n=e.split(`
1
+ import{n as e}from"./vue-i18n-C3W5i7h6.js";import{B as t,C as n,St as r,wt as i,yt as a}from"./reactivity.esm-bundler-DtSZKRQB.js";import{$ as o,A as s,At as c,B as l,Dt as u,Et as d,H as f,It as p,Mt as m,Ot as h,Rt as g,Tt as _,U as v,V as y,W as b,Wt as x,X as S,Z as C,_n as w,fn as T,st as E,t as D,ut as O,w as k}from"./runtime-dom.esm-bundler-DgevF6KV.js";import{S as A,W as j,Z as M,b as N,l as P,t as F,w as ee}from"./QBtn-B7WFdfmy.js";import{O as te,c as I,d as L,k as ne}from"./use-id-DlouqdB8.js";import{H as re,T as ie,h as ae,lt as R,m as oe,w as se}from"./index-DMyroqeJ.js";import{t as ce}from"./QSpinnerDots-DWOFEezR.js";import{r as z}from"./QItemSection-5GBV9MR2.js";import{_ as le,b as B,x as V}from"./permissionModes-BIjHkGNz.js";import{r as ue,t as H}from"./pending-diff-open-BjmrYqfo.js";import{t as U}from"./documents-B6eOZeFK.js";import{a as W,i as G}from"./layout-nWIyHeJf.js";import{i as de,n as fe,o as pe,r as me,t as K}from"./render-chat-markdown-Bl9MpfC9.js";import{a as he,i as ge,n as _e,o as ve,r as ye,s as be}from"./WorkspacePage-D-YQa2pY.js";var q=[`horizontal`,`vertical`,`cell`,`none`],J=M({name:`QMarkupTable`,props:{...ne,dense:Boolean,flat:Boolean,bordered:Boolean,square:Boolean,wrapCells:Boolean,separator:{type:String,default:`horizontal`,validator:e=>q.includes(e)}},setup(e,{slots:t}){let n=E(),r=te(e,n.proxy.$q),i=l(()=>`q-markup-table q-table__container q-table__card q-table--${e.separator}-separator`+(r.value?` q-table--dark q-table__card--dark q-dark`:``)+(e.dense?` q-table--dense`:``)+(e.flat?` q-table--flat`:``)+(e.bordered?` q-table--bordered`:``)+(e.square?` q-table--square`:``)+(e.wrapCells?``:` q-table--no-wrap`));return()=>O(`div`,{class:i.value},[O(`table`,{class:`q-table`},A(t.default))])}});function xe(e,t){return O(`div`,e,[O(`table`,{class:`q-table`},t)])}var Se={list:z,table:J},Ce=[`list`,`table`,`__qtable`],we=M({name:`QVirtualScroll`,props:{...V,type:{type:String,default:`list`,validator:e=>Ce.includes(e)},items:{type:Array,default:()=>[]},itemsFn:Function,itemsSize:Number,scrollTarget:L},setup(e,{slots:t,attrs:r}){let i,a=n(null),o=l(()=>e.itemsSize>=0&&e.itemsFn!==void 0?Number.parseInt(e.itemsSize,10):Array.isArray(e.items)?e.items.length:0),{virtualScrollSliceRange:s,localResetVirtualScroll:f,padVirtualScroll:p,onVirtualScrollEvt:g}=B({virtualScrollLength:o,getVirtualScrollTarget:x,getVirtualScrollEl:b}),_=l(()=>{if(o.value===0)return[];let t=(e,t)=>({index:s.value.from+t,item:e});return e.itemsFn===void 0?e.items.slice(s.value.from,s.value.to).map(t):e.itemsFn(s.value.from,s.value.to-s.value.from).map(t)}),v=l(()=>`q-virtual-scroll q-virtual-scroll`+(e.virtualScrollHorizontal?`--horizontal`:`--vertical`)+(e.scrollTarget===void 0?` scroll`:``)),y=l(()=>e.scrollTarget===void 0?{tabindex:0}:{});T(o,()=>{f()}),T(()=>e.scrollTarget,()=>{C(),S()});function b(){let e=a.value;return e===null?null:e.$el||e}function x(){return i}function S(){let t=b();t!==null&&(i=I(t,e.scrollTarget),i.addEventListener(`scroll`,g,j.passive))}function C(){i!==void 0&&(i.removeEventListener(`scroll`,g,j.passive),i=void 0)}function w(){let n=p(e.type===`list`?`div`:`tbody`,_.value.map(t.default));return t.before!==void 0&&(n=t.before().concat(n)),N(t.after,n)}return u(()=>{f()}),m(()=>{S()}),d(()=>{S()}),c(()=>{C()}),h(()=>{C()}),()=>{if(t.default===void 0){console.error(`QVirtualScroll: default scoped slot is required for rendering`);return}return e.type===`__qtable`?xe({ref:a,class:`q-table__middle `+v.value},w()):O(Se[e.type],{...r,ref:a,class:[r.class,v.value],...y.value},w)}}}),Te=new Set([`setup`,`cleanup`,`archive`]);function Ee(e){switch(e.type){case`user`:return e.sender===`system-prompt`?`system-prompt`:Te.has(e.sender)?`script`:`user`;case`session`:return`session`;default:return`agent`}}function De(e){let t=[],n=null,r=null;for(let i of e){let e=Ee(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}function Y(e){switch(e.type){case`text`:return`text:${e.messageId}`;case`thinking`:return`thinking:${e.messageId}:${e.eventIds?.[0]??e.ts??``}`;case`tool`:return`tool:${e.toolCallId}`;case`session`:return`session:${e.kind}:${e.eventIds?.[0]??e.ts??``}`;case`user`:return`user:${e.eventIds?.[0]??`${e.sender}:${e.ts??``}`}`;case`error`:return`error:${e.eventIds?.[0]??`${e.category}:${e.ts??``}`}`}}function Oe(e){let t=e.items[0];return`${e.speaker}:${t?Y(t):`empty`}`}function ke(e,t){let n=Math.min(t,e.length)-1;for(let t=n;t>=0;t--)if(e[t]?.speaker===`user`)return t;return-1}function Ae(e,t,n){return new Promise(r=>{let i=!1,a=null,o=null,s=e=>{i||(i=!0,a!==null&&clearTimeout(a),a=null,o?.(),r(e))};if(o=T(e,e=>{t(e)&&s(!0)},{immediate:!0}),i){o();return}a=setTimeout(()=>s(!1),n)})}var je={class:`agent-error-item`},X={class:`agent-error-item__body`},Me={class:`agent-error-item__title`},Z={class:`agent-error-item__message`},Ne=G(o({__name:`AgentErrorItem`,props:{item:{}},setup(t){let n=t,{te:r}=e(),a=l(()=>{let e=`agent.error.${n.item.category}`;return r(e)?e:`agent.error.other`});return(e,n)=>(g(),b(`div`,je,[C(P,{name:`error_outline`,size:`16px`,class:`agent-error-item__icon`}),y(`div`,X,[y(`div`,Me,i(e.$t(a.value)),1),y(`div`,Z,i(t.item.message),1)])]))}}),[[`__scopeId`,`data-v-e98c3c02`]]),Q={class:`text-caption text-kobo-3`},$=o({__name:`SessionEventItem`,props:{item:{}},setup(e){let t=e,n=l(()=>{switch(t.item.kind){case`started`:return`session.started`;case`ended`:return be(t.item.detail);case`compacted`:return`session.compacted`;default:return`session.started`}});return(e,t)=>(g(),b(`span`,Q,i(e.$t(n.value)),1))}});function Pe(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){Fe(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 Fe(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=Ie(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 Ie(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 Le=[`innerHTML`],Re=G(o({__name:`TextMessageItem`,props:{item:{}},setup(e){let t=e,n=U(),r=oe(),i=l(()=>{let e=r.selectedWorkspaceId;return e?n.documentsFor(e).map(e=>e.path):[]}),a=l(()=>{let e=t.item;if(e.streaming)return K(e.text);let n=i.value;return me(`${e.messageId}|${n.join(`,`)}`,e.text,e=>{let t=Pe(pe.parse(e,{async:!1,breaks:!0,gfm:!0}),n);return de(t,{addAttr:[`data-document-path`]})})});function o(e){let t=e.target?.closest(`.document-link`);if(!t)return;e.preventDefault();let i=t.getAttribute(`data-document-path`),a=r.selectedWorkspaceId;!i||!a||n.openDocumentByPath(a,i)}return(t,n)=>(g(),b(`div`,{class:`markdown-message`,onClick:o},[y(`div`,{innerHTML:a.value},null,8,Le),e.item.streaming?(g(),f(ee,{key:0,size:`xs`,class:`q-ml-xs`})):v(``,!0)]))}}),[[`__scopeId`,`data-v-539815f6`]]);function ze(e,t){let n=e.split(`
2
2
  `),r=t.split(`
3
3
  `),i=0;for(;i<n.length&&i<r.length&&n[i]===r[i];)i++;let a=n.length,o=r.length;for(;a>i&&o>i&&n[a-1]===r[o-1];)a--,o--;let s=n.slice(0,i).map(e=>({type:`context`,content:e})),c=n.slice(a).map(e=>({type:`context`,content:e})),l=n.slice(i,a),u=r.slice(i,o);return l.length>800||u.length>800?[...s,...l.map(e=>({type:`del`,content:e})),...u.map(e=>({type:`add`,content:e})),...c]:[...s,...Be(l,u),...c]}function Be(e,t){let n=e.length,r=t.length;if(n===0&&r===0)return[];if(n===0)return t.map(e=>({type:`add`,content:e}));if(r===0)return e.map(e=>({type:`del`,content:e}));let i=r+1,a=new Int32Array((n+1)*i);for(let o=n-1;o>=0;o--)for(let n=r-1;n>=0;n--)a[o*i+n]=e[o]===t[n]?a[(o+1)*i+(n+1)]+1:Math.max(a[(o+1)*i+n],a[o*i+(n+1)]);let o=[],s=0,c=0;for(;s<n&&c<r;)e[s]===t[c]?(o.push({type:`context`,content:e[s]}),s++,c++):a[(s+1)*i+c]>=a[s*i+(c+1)]?(o.push({type:`del`,content:e[s]}),s++):(o.push({type:`add`,content:t[c]}),c++);for(;s<n;)o.push({type:`del`,content:e[s++]});for(;c<r;)o.push({type:`add`,content:t[c++]});return o}function Ve(e){let t=e.split(`
4
4
  `),n=[],r=!1;for(let e of t){if(e.startsWith(`@@`)){r=!0;continue}!r&&(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 He(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=Ve(i);return{toolName:`Edit`,filePath:e,additions:t.filter(e=>e.type===`add`).length,deletions:t.filter(e=>e.type===`del`).length,diffLines:t}}return{toolName:`Edit`,filePath:e,oldString:t,newString:r,replaceAll:n.replace_all??!1,additions:r?r.split(`
@@ -1 +1 @@
1
- import{B as e,C as t,wt as n}from"./reactivity.esm-bundler-DtSZKRQB.js";import{$ as r,A as i,H as a,Mt as o,Rt as s,U as c,V as l,W as u,Wt as d,Z as f,_n as p}from"./runtime-dom.esm-bundler-DgevF6KV.js";import{t as m}from"./QBtn-B7WFdfmy.js";import{_t as h,ht as g,r as _}from"./index-okqx5CVc.js";import{i as v}from"./layout-nWIyHeJf.js";import{n as y}from"./render-chat-markdown-DrGdVCku.js";import{t as b}from"./QSpace-D3PNLwa4.js";import{t as x}from"./QChip-BzB_GF4Z.js";import{t as S}from"./QPage-RpDxx5ah.js";import{t as C}from"./DrawerToggleButton-CBdOJzVZ.js";var w={class:`row items-center q-mb-md`},T={class:`text-h6 q-ml-sm`},E={key:0,class:`text-kobo-3 text-center q-pa-lg`},D={key:1,class:`text-negative text-center q-pa-lg`},O={key:2,class:`text-kobo-3 text-center q-pa-lg`},k={key:3,class:`column q-gutter-md`},A={key:0,class:`text-caption text-kobo-3`},j={class:`row items-center q-mb-sm`},M={class:`text-subtitle1 text-kobo-2`,style:{"font-family":`var(--kobo-font-mono, monospace)`}},N=[`innerHTML`],P=v(r({__name:`ChangelogPage`,setup(r){let v=_(),P=t([]),F=t(``),I=t(!1),L=t(null);function R(e){return y(e)}async function z(){I.value=!0,L.value=null;try{let e=await fetch(`/api/changelog`);if(!e.ok)throw Error(`HTTP ${e.status}`);let t=await e.json();F.value=t.currentVersion??``,P.value=t.versions??[]}catch(e){L.value=e instanceof Error?e.message:String(e)}finally{I.value=!1}}return o(z),(t,r)=>(s(),a(S,{class:`q-pa-md`,style:{"max-width":`900px`,margin:`0 auto`}},{default:p(()=>[l(`div`,w,[f(C,{class:`q-mr-xs`}),f(m,{flat:``,dense:``,round:``,icon:`arrow_back`,onClick:r[0]||=t=>e(v).back()}),l(`div`,T,n(t.$t(`changelog.title`)),1),f(b),f(m,{flat:``,dense:``,icon:`refresh`,loading:I.value,label:t.$t(`common.refresh`),onClick:z},null,8,[`loading`,`label`])]),I.value&&P.value.length===0?(s(),u(`div`,E,n(t.$t(`common.loading`)),1)):L.value?(s(),u(`div`,D,n(L.value),1)):P.value.length===0?(s(),u(`div`,O,n(t.$t(`changelog.empty`)),1)):(s(),u(`div`,k,[F.value?(s(),u(`div`,A,n(t.$t(`changelog.currentVersion`,{version:F.value})),1)):c(``,!0),(s(!0),u(i,null,d(P.value,e=>(s(),a(g,{key:e.version,dark:``,flat:``,bordered:``},{default:p(()=>[f(h,null,{default:p(()=>[l(`div`,j,[l(`div`,M,` v`+n(e.version),1),e.version===F.value?(s(),a(x,{key:0,dense:``,size:`sm`,color:`primary`,"text-color":`kobo-1`,label:t.$t(`changelog.current`),class:`q-ml-sm`},null,8,[`label`])):c(``,!0)]),l(`div`,{class:`changelog-notes`,innerHTML:R(e.notes)},null,8,N)]),_:2},1024)]),_:2},1024))),128))]))]),_:1}))}}),[[`__scopeId`,`data-v-303054e6`]]);export{P as default};
1
+ import{B as e,C as t,wt as n}from"./reactivity.esm-bundler-DtSZKRQB.js";import{$ as r,A as i,H as a,Mt as o,Rt as s,U as c,V as l,W as u,Wt as d,Z as f,_n as p}from"./runtime-dom.esm-bundler-DgevF6KV.js";import{t as m}from"./QBtn-B7WFdfmy.js";import{_t as h,ht as g,r as _}from"./index-DMyroqeJ.js";import{i as v}from"./layout-nWIyHeJf.js";import{n as y}from"./render-chat-markdown-Bl9MpfC9.js";import{t as b}from"./QSpace-D3PNLwa4.js";import{t as x}from"./QChip-BzB_GF4Z.js";import{t as S}from"./QPage-RpDxx5ah.js";import{t as C}from"./DrawerToggleButton-CBdOJzVZ.js";var w={class:`row items-center q-mb-md`},T={class:`text-h6 q-ml-sm`},E={key:0,class:`text-kobo-3 text-center q-pa-lg`},D={key:1,class:`text-negative text-center q-pa-lg`},O={key:2,class:`text-kobo-3 text-center q-pa-lg`},k={key:3,class:`column q-gutter-md`},A={key:0,class:`text-caption text-kobo-3`},j={class:`row items-center q-mb-sm`},M={class:`text-subtitle1 text-kobo-2`,style:{"font-family":`var(--kobo-font-mono, monospace)`}},N=[`innerHTML`],P=v(r({__name:`ChangelogPage`,setup(r){let v=_(),P=t([]),F=t(``),I=t(!1),L=t(null);function R(e){return y(e)}async function z(){I.value=!0,L.value=null;try{let e=await fetch(`/api/changelog`);if(!e.ok)throw Error(`HTTP ${e.status}`);let t=await e.json();F.value=t.currentVersion??``,P.value=t.versions??[]}catch(e){L.value=e instanceof Error?e.message:String(e)}finally{I.value=!1}}return o(z),(t,r)=>(s(),a(S,{class:`q-pa-md`,style:{"max-width":`900px`,margin:`0 auto`}},{default:p(()=>[l(`div`,w,[f(C,{class:`q-mr-xs`}),f(m,{flat:``,dense:``,round:``,icon:`arrow_back`,onClick:r[0]||=t=>e(v).back()}),l(`div`,T,n(t.$t(`changelog.title`)),1),f(b),f(m,{flat:``,dense:``,icon:`refresh`,loading:I.value,label:t.$t(`common.refresh`),onClick:z},null,8,[`loading`,`label`])]),I.value&&P.value.length===0?(s(),u(`div`,E,n(t.$t(`common.loading`)),1)):L.value?(s(),u(`div`,D,n(L.value),1)):P.value.length===0?(s(),u(`div`,O,n(t.$t(`changelog.empty`)),1)):(s(),u(`div`,k,[F.value?(s(),u(`div`,A,n(t.$t(`changelog.currentVersion`,{version:F.value})),1)):c(``,!0),(s(!0),u(i,null,d(P.value,e=>(s(),a(g,{key:e.version,dark:``,flat:``,bordered:``},{default:p(()=>[f(h,null,{default:p(()=>[l(`div`,j,[l(`div`,M,` v`+n(e.version),1),e.version===F.value?(s(),a(x,{key:0,dense:``,size:`sm`,color:`primary`,"text-color":`kobo-1`,label:t.$t(`changelog.current`),class:`q-ml-sm`},null,8,[`label`])):c(``,!0)]),l(`div`,{class:`changelog-notes`,innerHTML:R(e.notes)},null,8,N)]),_:2},1024)]),_:2},1024))),128))]))]),_:1}))}}),[[`__scopeId`,`data-v-303054e6`]]);export{P as default};