@loicngr/kobo 1.11.0 → 1.11.2

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 (87) hide show
  1. package/CHANGELOG.md +38 -0
  2. package/README.md +6 -6
  3. package/dist/mcp-server/kobo-tasks-server.js +1 -1
  4. package/dist/server/routes/dev-server.js +14 -0
  5. package/dist/server/routes/workspaces.js +149 -0
  6. package/dist/server/services/agent/engines/claude-code/event-mapper.js +1 -1
  7. package/dist/server/services/agent/engines/codex/event-mapper.js +1 -1
  8. package/dist/server/services/agent/orchestrator.js +56 -2
  9. package/dist/server/services/forge/github/provider.js +13 -1
  10. package/dist/server/services/forge/gitlab/provider.js +13 -1
  11. package/dist/server/services/forge/none.js +13 -1
  12. package/dist/server/services/settings-service.js +53 -11
  13. package/dist/shared/project-colors.js +7 -0
  14. package/package.json +1 -1
  15. package/src/client/dist/pwa/assets/ActivityFeed-BNhy8oDx.js +8 -0
  16. package/src/client/dist/pwa/assets/{ChangelogPage-DLmP9KSb.js → ChangelogPage-eRWLuQLR.js} +1 -1
  17. package/src/client/dist/pwa/assets/{ClosePopup-Cc7aUs96.js → ClosePopup-Cc2YwliF.js} +1 -1
  18. package/src/client/dist/pwa/assets/CreatePage-B9Mxi7zD.js +2 -0
  19. package/src/client/dist/pwa/assets/CreatePage-B_JcmRfU.css +1 -0
  20. package/src/client/dist/pwa/assets/{DiffViewer-B-TSPjtj.js → DiffViewer-DglPAzaZ.js} +3 -3
  21. package/src/client/dist/pwa/assets/{HealthPage-13Ugeexp.js → HealthPage-sS-N-_9T.js} +1 -1
  22. package/src/client/dist/pwa/assets/{MainLayout-BzOcdtNd.js → MainLayout-BelFb6SW.js} +17 -17
  23. package/src/client/dist/pwa/assets/{MainLayout-CgHzEM37.css → MainLayout-BoZsUoGd.css} +1 -1
  24. package/src/client/dist/pwa/assets/{QBtnToggle-BUJ1g7BR.js → QBtnToggle-eC2_l1xJ.js} +1 -1
  25. package/src/client/dist/pwa/assets/{QChip-CPiNmK-x.js → QChip-BT8qyt_p.js} +1 -1
  26. package/src/client/dist/pwa/assets/QSelect-Bj-wqU82.js +36 -0
  27. package/src/client/dist/pwa/assets/QTooltip-CCNfQ3sa.js +1 -0
  28. package/src/client/dist/pwa/assets/{SearchPage-C7ulmpmI.js → SearchPage-6CSIDDcM.js} +1 -1
  29. package/src/client/dist/pwa/assets/{SettingsPage-DJEH2ahl.css → SettingsPage-CENUP7ua.css} +1 -1
  30. package/src/client/dist/pwa/assets/SettingsPage-jGIZfB7N.js +16 -0
  31. package/src/client/dist/pwa/assets/ThinkingItem-Ckk4K1O-.css +1 -0
  32. package/src/client/dist/pwa/assets/ThinkingItem-kP-lU251.js +1 -0
  33. package/src/client/dist/pwa/assets/{TouchPan-g-ZGD0pd.js → TouchPan-DVggdTB9.js} +1 -1
  34. package/src/client/dist/pwa/assets/WorkspacePage-BWRpD_9V.js +4 -0
  35. package/src/client/dist/pwa/assets/{WorkspacePage-CnRbNBAh.css → WorkspacePage-D-1HoeQg.css} +1 -1
  36. package/src/client/dist/pwa/assets/{build-path-tree-B3R74R5X.js → build-path-tree-FNTYv-td.js} +1 -1
  37. package/src/client/dist/pwa/assets/{cssMode-Ca0QWQGQ.js → cssMode-DHVn7kI3.js} +1 -1
  38. package/src/client/dist/pwa/assets/documents-DoEDCa8X.js +1 -0
  39. package/src/client/dist/pwa/assets/{editor.api2-CQQ3TFHp.js → editor.api2-DO15WOFV.js} +1 -1
  40. package/src/client/dist/pwa/assets/{editor.main-BmtlNqLW.js → editor.main-Ce_TPxw1.js} +2 -2
  41. package/src/client/dist/pwa/assets/{expand-template-DkLDjInO.js → expand-template-DnNNlpvQ.js} +1 -1
  42. package/src/client/dist/pwa/assets/{formatters-COWwqgNl.js → formatters-BBT0Ye5P.js} +1 -1
  43. package/src/client/dist/pwa/assets/{freemarker2-Be_VI-2P.js → freemarker2-B5mFQ9MW.js} +1 -1
  44. package/src/client/dist/pwa/assets/{handlebars-C6MV-4UG.js → handlebars-TGXWWwrU.js} +1 -1
  45. package/src/client/dist/pwa/assets/{html-H42ZDxQV.js → html-BcoyXTgh.js} +1 -1
  46. package/src/client/dist/pwa/assets/{htmlMode-COCELcr-.js → htmlMode-Dhuwig6w.js} +1 -1
  47. package/src/client/dist/pwa/assets/i18n-BHXcyfLT.js +1 -0
  48. package/src/client/dist/pwa/assets/index-CGnQPjwo.js +84 -0
  49. package/src/client/dist/pwa/assets/{javascript-CiFkTKAE.js → javascript-v_uXBoLN.js} +1 -1
  50. package/src/client/dist/pwa/assets/{jsonMode--0Mpy1sQ.js → jsonMode-CErvRpqG.js} +1 -1
  51. package/src/client/dist/pwa/assets/{kobo-commands-BzxwZ3vQ.js → kobo-commands-DXJ85QAa.js} +1 -1
  52. package/src/client/dist/pwa/assets/{layout-BcUfiaFQ.js → layout-Di93C8KP.js} +1 -1
  53. package/src/client/dist/pwa/assets/{liquid-BlkSsRl8.js → liquid-ZappZiY7.js} +1 -1
  54. package/src/client/dist/pwa/assets/{lspLanguageFeatures-DXelvPSE.js → lspLanguageFeatures-ur5VVJZY.js} +1 -1
  55. package/src/client/dist/pwa/assets/{mdx-BoNBdGqW.js → mdx-BKL2jNxT.js} +1 -1
  56. package/src/client/dist/pwa/assets/{monaco.contribution-D8uREgcb.js → monaco.contribution-WfNiFCn_.js} +2 -2
  57. package/src/client/dist/pwa/assets/{network-auth-wzwG23Rr.js → network-auth-O2hz04zH.js} +1 -1
  58. package/src/client/dist/pwa/assets/{pending-diff-open-DyoLDUlV.js → pending-diff-open-CysqNMDL.js} +1 -1
  59. package/src/client/dist/pwa/assets/{permissionModes-D7Iv04mQ.js → permissionModes-Crg3_WdF.js} +1 -1
  60. package/src/client/dist/pwa/assets/project-color-DVafgHFe.js +1 -0
  61. package/src/client/dist/pwa/assets/{python-DZCdvSIy.js → python-DXpnDgWC.js} +1 -1
  62. package/src/client/dist/pwa/assets/{razor-CFwK0FwM.js → razor-2pTtVGVn.js} +1 -1
  63. package/src/client/dist/pwa/assets/{render-chat-markdown-DCydC_jc.js → render-chat-markdown-BbmsqP4r.js} +1 -1
  64. package/src/client/dist/pwa/assets/{tsMode-D8ZbVZB6.js → tsMode-t5L5YTTB.js} +1 -1
  65. package/src/client/dist/pwa/assets/{typescript-CjxR71vN.js → typescript-C2FUrxc2.js} +1 -1
  66. package/src/client/dist/pwa/assets/{use-onboarding-DA5ka-XJ.js → use-onboarding-CxQRTetV.js} +1 -1
  67. package/src/client/dist/pwa/assets/{use-quasar-5A_Wy1iR.js → use-quasar-CUBX9V4F.js} +1 -1
  68. package/src/client/dist/pwa/assets/{workers-Bv1gqcaP.js → workers-ri7So4GA.js} +1 -1
  69. package/src/client/dist/pwa/assets/{xml-vP8L_5ev.js → xml-CvWFj3be.js} +1 -1
  70. package/src/client/dist/pwa/assets/{yaml-YN_D0NXJ.js → yaml-DsSH3Hc4.js} +1 -1
  71. package/src/client/dist/pwa/index.html +1 -1
  72. package/src/client/dist/pwa/sounds/basic-notification.mp3 +0 -0
  73. package/src/client/dist/pwa/sw.js +1 -1
  74. package/src/mcp-server/kobo-tasks-server.ts +1 -1
  75. package/src/client/dist/pwa/assets/ActivityFeed-ujsfk0sG.js +0 -8
  76. package/src/client/dist/pwa/assets/CreatePage-DfMhzgbv.js +0 -2
  77. package/src/client/dist/pwa/assets/CreatePage-qMgdzdb-.css +0 -1
  78. package/src/client/dist/pwa/assets/QSelect-D63npmWX.js +0 -36
  79. package/src/client/dist/pwa/assets/QTooltip-DOQ5d9M2.js +0 -1
  80. package/src/client/dist/pwa/assets/SettingsPage-DY1g-LTs.js +0 -16
  81. package/src/client/dist/pwa/assets/ThinkingItem-BVoamjnK.css +0 -1
  82. package/src/client/dist/pwa/assets/ThinkingItem-M7hXIRih.js +0 -1
  83. package/src/client/dist/pwa/assets/WorkspacePage-BU_uwlKN.js +0 -4
  84. package/src/client/dist/pwa/assets/documents-a-MDhFWk.js +0 -1
  85. package/src/client/dist/pwa/assets/i18n-BgrVh81z.js +0 -1
  86. package/src/client/dist/pwa/assets/index-Cs6I8vi_.js +0 -84
  87. package/src/client/dist/pwa/assets/project-color-C3soNzD0.js +0 -1
package/CHANGELOG.md CHANGED
@@ -4,6 +4,44 @@ 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.2
8
+
9
+ - feat(integrations): let Notion and Sentry be enabled independently, keep
10
+ their settings isolated, hide inactive project overrides, and add detailed
11
+ **Test connection** feedback.
12
+ - feat(chat): stream Claude output token-by-token like Codex, consolidate agent
13
+ reasoning into one live panel above the composer, and add a visibility
14
+ preference for reasoning and verbose system messages.
15
+ - feat(chat): add `Ctrl+J` for a new line, `Ctrl+Enter` to force a queued
16
+ message while an agent is busy, and keep the shortcuts visible below chat.
17
+ - feat(chat): add session-aware `Ctrl+F` workspace-history search, scrolling to
18
+ the selected result; global search now opens the same exact message.
19
+ - feat(chat): add a `Ctrl+K` workspace command palette for common actions.
20
+ - feat(chat): make worktree-relative paths and Edit/Write cards open the
21
+ corresponding file in the Git diff viewer.
22
+ - feat(git): add workspace commits with either a user-written message or an
23
+ agent request; add confirmed PR/MR merging and an optional remote-branch
24
+ deletion after merge.
25
+ - feat(auto-loop): retry quota, rate-limit, and temporary upstream failures;
26
+ add a configurable maximum retry count.
27
+ - feat(notifications): add `basic-notification.mp3`, event-level sound
28
+ inheritance, an agent-error sound, browser-permission diagnostics, and more
29
+ reliable playback while Kobo is in a background tab.
30
+ - feat(settings): extend the project color palette and improve save-state
31
+ handling for activity-feed preferences.
32
+ - feat(observability): enrich the session timeline with tools, tokens, and
33
+ errors; keep it fresh during an active run and retain diagnostic export.
34
+ - feat(pwa): show install, offline, update, and WebSocket-reconnection states;
35
+ preserve the visible chat area when the status banner is displayed.
36
+ - fix(websocket): avoid reconnecting after an explicit disconnect and resume
37
+ safely after network connectivity returns.
38
+
39
+ ## 1.11.1
40
+
41
+ - feat(workspace): improve workspace actions
42
+ - feat(workspace): Forge merge button
43
+ - feat(workspace): Commit button
44
+
7
45
  ## 1.11.0
8
46
 
9
47
  - feat(pwa): ship the production client as an installable Progressive Web App.
package/README.md CHANGED
@@ -17,27 +17,27 @@ Kōbō runs multiple coding agents in parallel, each isolated in its own git wor
17
17
 
18
18
  - **Isolated worktrees**: each workspace is a dedicated git worktree on its own branch, so parallel sessions never collide.
19
19
  - **Two agent engines**: Claude Code (via `@anthropic-ai/claude-agent-sdk`) and OpenAI Codex (via `codex app-server`), chosen per workspace.
20
- - **Live chat**: streaming text, inline Edit/Write diffs, per-turn cards, a compaction-in-progress indicator, infinite scrollback, and the latest Claude or Codex reasoning in a panel immediately above the message input. `/` 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. The global search page deep-links to the same location. 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.
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. 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
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
22
 
23
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), inline conflict resolution, and `Sync` / `Push` / `Open PR` / `Change PR base` / `Change source branch`. Edit/Write cards in chat open their exact changed file in the diff viewer. Multi-forge: GitHub (`gh`), GitLab (`glab`), or no forge, auto-detected from the remote and overridable per project.
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), 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`), or no forge, auto-detected from the remote and overridable per project.
25
25
 
26
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.
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
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
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.
30
30
 
31
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. 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.
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
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
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
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
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
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 session duration and status; download a redacted workspace diagnostic JSON for troubleshooting.
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
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 and offers an explicit reload when an update is ready. Live agent activity still requires a connection to the local Kōbō server.
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.
41
41
 
42
42
  ## Quick start
43
43
 
@@ -298,7 +298,7 @@ const WORKSPACE_SCOPED_TOOLS = [
298
298
  },
299
299
  {
300
300
  name: 'get_dev_server_status',
301
- description: 'CALL BEFORE asking the user whether the app is running, or when your change is dev-server-sensitive. Returns running/stopped/starting/error + URL, port, container names.',
301
+ description: 'CALL BEFORE asking the user whether the app is running, or when your change is dev-server-sensitive. Returns `not_configured` when this project has no dev server: do not start one manually in that case. Otherwise returns running/stopped/starting/error + URL, port, container names.',
302
302
  inputSchema: { type: 'object', properties: {}, required: [] },
303
303
  annotations: { readOnlyHint: true, openWorldHint: false },
304
304
  },
@@ -1,6 +1,7 @@
1
1
  import { Hono } from 'hono';
2
2
  import { migrationGuard } from '../middleware/migration-guard.js';
3
3
  import { getDevServerLogs, getStatus, startDevServer, stopDevServer } from '../services/dev-server-service.js';
4
+ import { getProjectSettings } from '../services/settings-service.js';
4
5
  import { getWorkspace } from '../services/workspace-service.js';
5
6
  /** Hono sub-router for per-workspace dev server lifecycle (start, stop, status, logs). */
6
7
  const app = new Hono();
@@ -12,6 +13,19 @@ app.get('/:workspaceId/status', (c) => {
12
13
  if (!workspace) {
13
14
  return c.json({ error: `Workspace '${workspaceId}' not found` }, 404);
14
15
  }
16
+ const devServer = getProjectSettings(workspace.projectPath)?.devServer;
17
+ if (!devServer?.startCommand?.trim()) {
18
+ return c.json({
19
+ status: 'not_configured',
20
+ configured: false,
21
+ message: 'No dev server is configured for this project',
22
+ instanceName: '',
23
+ projectName: '',
24
+ httpPort: '',
25
+ url: '',
26
+ containers: [],
27
+ });
28
+ }
15
29
  const status = getStatus(workspace.projectPath, workspace.workingBranch, workspaceId);
16
30
  // If runtime detection returns unknown, use persisted status from DB
17
31
  if (status.status === 'unknown' && workspace.devServerStatus && workspace.devServerStatus !== 'stopped') {
@@ -1559,6 +1559,48 @@ function readableHistorySearchResult(type, rawPayload, query) {
1559
1559
  snippet: `${start > 0 ? '…' : ''}${text.slice(start, end).replace(/\s+/g, ' ').trim()}${end < text.length ? '…' : ''}`,
1560
1560
  };
1561
1561
  }
1562
+ // GET /api/workspaces/:id/session-metrics — compact observability data for the timeline.
1563
+ app.get('/:id/session-metrics', (c) => {
1564
+ try {
1565
+ const id = c.req.param('id');
1566
+ if (!workspaceService.getWorkspace(id))
1567
+ return c.json({ error: `Workspace '${id}' not found` }, 404);
1568
+ const db = getDb();
1569
+ const sessions = workspaceService.listSessions(id);
1570
+ const metrics = new Map(sessions.map((session) => [
1571
+ session.id,
1572
+ { sessionId: session.id, toolCalls: 0, errors: 0, inputTokens: 0, outputTokens: 0 },
1573
+ ]));
1574
+ const events = db
1575
+ .prepare("SELECT session_id, payload FROM ws_events WHERE workspace_id = ? AND type = 'agent:event' AND session_id IS NOT NULL")
1576
+ .all(id);
1577
+ for (const row of events) {
1578
+ const metric = metrics.get(row.session_id);
1579
+ if (!metric)
1580
+ continue;
1581
+ try {
1582
+ const event = JSON.parse(row.payload);
1583
+ if (event.kind === 'tool:call')
1584
+ metric.toolCalls++;
1585
+ if (event.kind === 'error' || (event.kind === 'tool:result' && event.isError === true))
1586
+ metric.errors++;
1587
+ if (event.kind === 'usage') {
1588
+ if (typeof event.inputTokens === 'number')
1589
+ metric.inputTokens = Math.max(metric.inputTokens, event.inputTokens);
1590
+ if (typeof event.outputTokens === 'number')
1591
+ metric.outputTokens = Math.max(metric.outputTokens, event.outputTokens);
1592
+ }
1593
+ }
1594
+ catch {
1595
+ // Corrupt historical events are intentionally ignored in diagnostics.
1596
+ }
1597
+ }
1598
+ return c.json({ sessions, metrics: [...metrics.values()] });
1599
+ }
1600
+ catch (err) {
1601
+ return c.json({ error: err instanceof Error ? err.message : String(err) }, 500);
1602
+ }
1603
+ });
1562
1604
  // GET /api/workspaces/:id/diagnostic.json — portable, redacted-by-design event diagnostic.
1563
1605
  app.get('/:id/diagnostic.json', (c) => {
1564
1606
  try {
@@ -2988,6 +3030,51 @@ app.post('/:id/git/commit-all', async (c) => {
2988
3030
  return c.json({ error: message }, 500);
2989
3031
  }
2990
3032
  });
3033
+ /** Ask the workspace agent to inspect and commit the current working tree. */
3034
+ app.post('/:id/git/commit-with-agent', migrationGuard, async (c) => {
3035
+ try {
3036
+ const id = c.req.param('id');
3037
+ const workspace = workspaceService.getWorkspace(id);
3038
+ if (!workspace)
3039
+ return c.json({ error: `Workspace '${id}' not found` }, 404);
3040
+ const workingTree = gitOps.getWorkingTreeStatus(workspace.worktreePath);
3041
+ if (workingTree.staged + workingTree.modified + workingTree.untracked === 0) {
3042
+ return c.json({ error: 'No uncommitted changes to commit' }, 400);
3043
+ }
3044
+ const prompt = `Please prepare a clean commit for the uncommitted changes in this workspace.
3045
+
3046
+ 1. Read .ai/.git-conventions.md if it exists and follow it exactly.
3047
+ 2. Inspect git status and the staged and unstaged diffs. Do not include unrelated files.
3048
+ 3. Run the relevant focused checks when practical.
3049
+ 4. Stage the intended changes and create one conventional, descriptive commit.
3050
+ 5. Do NOT push the branch.
3051
+
3052
+ When finished, report the commit SHA, its message, the files included, and the checks you ran.`;
3053
+ const session = workspaceService.getActiveSession(workspace.id);
3054
+ wsService.emit(workspace.id, 'user:message', { content: prompt, sender: 'user' }, session?.id ?? undefined);
3055
+ wakeupService.cancel(workspace.id, 'user-message');
3056
+ let messageSent = false;
3057
+ try {
3058
+ agentManager.sendMessage(workspace.id, prompt);
3059
+ messageSent = true;
3060
+ }
3061
+ catch {
3062
+ try {
3063
+ agentManager.startAgent(workspace.id, workspace.worktreePath, prompt, workspace.model, true, workspace.agentPermissionMode, undefined, workspace.reasoningEffort);
3064
+ workspaceService.updateWorkspaceStatus(workspace.id, 'executing');
3065
+ messageSent = true;
3066
+ }
3067
+ catch (resumeErr) {
3068
+ const message = resumeErr instanceof Error ? resumeErr.message : String(resumeErr);
3069
+ return c.json({ error: `Unable to ask the agent to commit: ${message}` }, 409);
3070
+ }
3071
+ }
3072
+ return c.json({ ok: true, messageSent });
3073
+ }
3074
+ catch (err) {
3075
+ return c.json({ error: err instanceof Error ? err.message : String(err) }, 500);
3076
+ }
3077
+ });
2991
3078
  /** Discard working-tree changes (destructive recovery action for a dirty rebase/merge). */
2992
3079
  app.post('/:id/git/discard', (c) => {
2993
3080
  try {
@@ -3115,6 +3202,68 @@ app.post('/:id/change-pr-base', async (c) => {
3115
3202
  return c.json({ error: message }, 500);
3116
3203
  }
3117
3204
  });
3205
+ /** Merge a ready, open PR/MR through its resolved forge provider. */
3206
+ app.post('/:id/merge-pr', async (c) => {
3207
+ try {
3208
+ const id = c.req.param('id');
3209
+ const workspace = workspaceService.getWorkspace(id);
3210
+ if (!workspace)
3211
+ return c.json({ error: `Workspace '${id}' not found` }, 404);
3212
+ const provider = getForgeProvider(resolveForge(workspace.projectPath));
3213
+ if (!provider.capabilities.canMergeRequest) {
3214
+ return c.json({ error: 'This project has no forge that can merge the PR', code: 'forge_unsupported' }, 409);
3215
+ }
3216
+ const availability = await provider.isAvailable(workspace.worktreePath);
3217
+ if (!availability.available) {
3218
+ return c.json({
3219
+ error: `Forge CLI unavailable (${availability.reason ?? 'unknown'})`,
3220
+ code: `forge_${availability.reason ?? 'unavailable'}`,
3221
+ }, 409);
3222
+ }
3223
+ const snapshot = await provider.getPrStatus(workspace.worktreePath, workspace.workingBranch);
3224
+ if (!snapshot || snapshot.state !== 'OPEN') {
3225
+ return c.json({ error: 'No open PR found for this workspace', code: 'pr_not_open' }, 409);
3226
+ }
3227
+ if (!snapshot.readyToMerge) {
3228
+ return c.json({ error: 'This PR is not ready to merge', code: 'pr_not_ready' }, 409);
3229
+ }
3230
+ await provider.mergeRequest(workspace.worktreePath, snapshot.number);
3231
+ const merged = await provider.getPrStatus(workspace.worktreePath, workspace.workingBranch);
3232
+ return c.json({ success: true, merged: merged?.state === 'MERGED', branch: workspace.workingBranch });
3233
+ }
3234
+ catch (err) {
3235
+ if (err instanceof ForgeUnavailableError) {
3236
+ return c.json({ error: err.message, code: 'forge_unavailable' }, 409);
3237
+ }
3238
+ const message = err instanceof Error ? err.message : String(err);
3239
+ return c.json({ error: message }, 500);
3240
+ }
3241
+ });
3242
+ /** Delete the workspace branch from the remote after its PR/MR was merged. */
3243
+ app.post('/:id/delete-remote-branch', async (c) => {
3244
+ try {
3245
+ const id = c.req.param('id');
3246
+ const workspace = workspaceService.getWorkspace(id);
3247
+ if (!workspace)
3248
+ return c.json({ error: `Workspace '${id}' not found` }, 404);
3249
+ const provider = getForgeProvider(resolveForge(workspace.projectPath));
3250
+ if (!provider.capabilities.canDeleteRemoteBranch) {
3251
+ return c.json({ error: 'This project cannot delete remote branches', code: 'forge_unsupported' }, 409);
3252
+ }
3253
+ const availability = await provider.isAvailable(workspace.worktreePath);
3254
+ if (!availability.available)
3255
+ return c.json({ error: 'Forge CLI unavailable', code: 'forge_unavailable' }, 409);
3256
+ const snapshot = await provider.getPrStatus(workspace.worktreePath, workspace.workingBranch);
3257
+ if (snapshot?.state !== 'MERGED')
3258
+ return c.json({ error: 'The PR is not merged yet', code: 'pr_not_merged' }, 409);
3259
+ await provider.deleteRemoteBranch(workspace.worktreePath, workspace.workingBranch);
3260
+ return c.json({ success: true });
3261
+ }
3262
+ catch (err) {
3263
+ const message = err instanceof Error ? err.message : String(err);
3264
+ return c.json({ error: message }, 500);
3265
+ }
3266
+ });
3118
3267
  /** Re-target a workspace onto a new source branch (metadata + worktree + PR base). */
3119
3268
  app.post('/:id/change-source-branch', async (c) => {
3120
3269
  const id = c.req.param('id');
@@ -76,7 +76,7 @@ function normalizeRateLimitInfo(info) {
76
76
  * Anthropic's surfaces (`rate_limit_exceeded`, `Claude AI usage limit
77
77
  * reached`, `You're out of extra usage`, `quota exceeded`).
78
78
  */
79
- export const QUOTA_PATTERN = /out of extra usage|rate[_ ]limit|usage limit|quota exceeded/i;
79
+ export const QUOTA_PATTERN = /\b429\b|out of extra usage|rate[_ ]limit|usage limit|quota exceeded/i;
80
80
  export function createMapperState() {
81
81
  return {
82
82
  sessionStartedEmitted: false,
@@ -7,7 +7,7 @@ import { nanoid } from 'nanoid';
7
7
  // - "quota exceeded" / "insufficient_quota" / "insufficient quota"
8
8
  // - "out of extra usage" / "out of usage"
9
9
  // - "usage limit exceeded"
10
- export const QUOTA_PATTERN = /rate[_ ]limit|quota|usage limit|insufficient[_ ]quota|out of (extra )?usage/i;
10
+ export const QUOTA_PATTERN = /\b429\b|rate[_ ]limit|quota|usage limit|insufficient[_ ]quota|out of (extra )?usage/i;
11
11
  export function createMapperState(opts) {
12
12
  return {
13
13
  sessionStartedEmitted: false,
@@ -7,7 +7,7 @@ import * as autoLoopService from '../auto-loop-service.js';
7
7
  import * as cleanupScriptService from '../cleanup-script-service.js';
8
8
  import * as cronService from '../cron-service.js';
9
9
  import * as quotaBackoffService from '../quota-backoff-service.js';
10
- import { getEffectiveSettings } from '../settings-service.js';
10
+ import { getEffectiveSettings, getGlobalSettings } from '../settings-service.js';
11
11
  import { refreshNow } from '../usage/poller.js';
12
12
  import * as wakeupService from '../wakeup-service.js';
13
13
  import { emit, emitEphemeral } from '../websocket-service.js';
@@ -540,6 +540,12 @@ function handleEvent(workspaceId, agentSessionId, ev) {
540
540
  if (ev.kind === 'error' && ev.category === 'quota') {
541
541
  void handleQuota(workspaceId, agentSessionId);
542
542
  }
543
+ if (ev.kind === 'error' &&
544
+ ev.category === 'other' &&
545
+ TRANSIENT_SERVER_ERROR_PATTERN.test(ev.message) &&
546
+ getWs(workspaceId)?.autoLoop) {
547
+ void handleTransientAutoLoopFailure(workspaceId);
548
+ }
543
549
  if (ev.kind === 'error' && ev.category === 'resume_failed') {
544
550
  resumeFailedSet.add(workspaceId);
545
551
  clearStaleEngineSessionId(workspaceId);
@@ -1195,6 +1201,8 @@ function clearStaleEngineSessionId(workspaceId) {
1195
1201
  console.error('[orchestrator] Failed to clear stale engine session ID:', err);
1196
1202
  }
1197
1203
  }
1204
+ /** Retryable upstream failures for an auto-loop iteration (not coding errors). */
1205
+ export const TRANSIENT_SERVER_ERROR_PATTERN = /\b(?:http\s*)?500\b|internal server error|service unavailable|temporarily unavailable|overloaded/i;
1198
1206
  const QUOTA_SAFETY_MARGIN_MS = 30_000;
1199
1207
  const QUOTA_MAX_BACKOFF_MS = 24 * 60 * 60 * 1000;
1200
1208
  const QUOTA_SATURATION_THRESHOLD_PCT = 95;
@@ -1214,7 +1222,11 @@ const QUOTA_FALLBACK_LADDER_MINUTES = [15, 30, 60, 180, 300];
1214
1222
  * finally (2) a fixed ladder (15 → 30 → 60 → 180 → 300 min) whenever neither
1215
1223
  * source is usable.
1216
1224
  */
1217
- export async function computeQuotaBackoffMs(workspaceId, retryCount) {
1225
+ export async function computeQuotaBackoffMs(workspaceId, retryCount, preferQuotaReset = true) {
1226
+ if (!preferQuotaReset) {
1227
+ const idx = Math.min(Math.max(0, retryCount), QUOTA_FALLBACK_LADDER_MINUTES.length - 1);
1228
+ return { delayMs: QUOTA_FALLBACK_LADDER_MINUTES[idx] * 60 * 1000, source: 'fallback_ladder' };
1229
+ }
1218
1230
  // 1. Prefer the rate_limit event from the agent stream — most recent + most precise.
1219
1231
  const info = latestRateLimitInfo.get(workspaceId);
1220
1232
  if (info?.buckets?.length) {
@@ -1270,14 +1282,56 @@ async function handleQuota(workspaceId, _agentSessionId) {
1270
1282
  // May fail if transition is not valid
1271
1283
  }
1272
1284
  const retryCount = retryCounts.get(workspaceId) ?? 0;
1285
+ const autoLoopEnabled = getWs(workspaceId)?.autoLoop === true;
1286
+ const maxRetries = autoLoopEnabled ? (getGlobalSettings().autoLoopMaxRetries ?? 5) : 5;
1287
+ if (autoLoopEnabled && retryCount >= maxRetries) {
1288
+ autoLoopService.disable(workspaceId, 'error');
1289
+ try {
1290
+ updateWorkspaceStatus(workspaceId, 'error');
1291
+ }
1292
+ catch {
1293
+ // The loop is disabled even if an already-terminal status rejects this transition.
1294
+ }
1295
+ return;
1296
+ }
1273
1297
  const { delayMs, resetsAt, source } = await computeQuotaBackoffMs(workspaceId, retryCount);
1274
1298
  retryCounts.set(workspaceId, retryCount + 1);
1275
1299
  // The quotaBackoffService owns the timer + the persistent row + the
1276
1300
  // 'agent:quota-backoff' WS emit. Hand off everything to it.
1277
1301
  quotaBackoffService.arm(workspaceId, delayMs, { resetsAt: resetsAt ?? null, source });
1278
1302
  }
1303
+ /**
1304
+ * Use the same persisted retry path for temporary upstream failures (HTTP 500
1305
+ * and overloads) while deliberately ignoring quota-reset data: a server error
1306
+ * is unrelated to the user's quota window and follows the fallback ladder.
1307
+ */
1308
+ async function handleTransientAutoLoopFailure(workspaceId) {
1309
+ try {
1310
+ updateWorkspaceStatus(workspaceId, 'quota');
1311
+ }
1312
+ catch {
1313
+ // May fail if transition is not valid.
1314
+ }
1315
+ const retryCount = retryCounts.get(workspaceId) ?? 0;
1316
+ const maxRetries = getGlobalSettings().autoLoopMaxRetries ?? 5;
1317
+ if (retryCount >= maxRetries) {
1318
+ autoLoopService.disable(workspaceId, 'error');
1319
+ try {
1320
+ updateWorkspaceStatus(workspaceId, 'error');
1321
+ }
1322
+ catch {
1323
+ // The loop is disabled even if an already-terminal status rejects this transition.
1324
+ }
1325
+ return;
1326
+ }
1327
+ const { delayMs, resetsAt, source } = await computeQuotaBackoffMs(workspaceId, retryCount, false);
1328
+ retryCounts.set(workspaceId, retryCount + 1);
1329
+ quotaBackoffService.arm(workspaceId, delayMs, { resetsAt: resetsAt ?? null, source });
1330
+ }
1279
1331
  /** @internal test-only — re-export of `handleQuota` for direct testing. */
1280
1332
  export const _handleQuota = handleQuota;
1333
+ /** @internal test-only — re-export of the transient auto-loop retry handler. */
1334
+ export const _handleTransientAutoLoopFailure = handleTransientAutoLoopFailure;
1281
1335
  /**
1282
1336
  * Rebuild the in-memory `retryCounts` map from the persisted `pending_quota_backoffs`
1283
1337
  * rows. Called from `index.ts` at boot, before `quotaBackoffService.restoreOnBoot`.
@@ -92,7 +92,13 @@ function availabilityFromError(err) {
92
92
  }
93
93
  export const githubProvider = {
94
94
  id: 'github',
95
- capabilities: { canCreatePr: true, canChangePrBase: true, requestTermShort: 'PR' },
95
+ capabilities: {
96
+ canCreatePr: true,
97
+ canChangePrBase: true,
98
+ canMergeRequest: true,
99
+ canDeleteRemoteBranch: true,
100
+ requestTermShort: 'PR',
101
+ },
96
102
  async isAvailable(repoPath) {
97
103
  try {
98
104
  await execFileAsync('gh', ['auth', 'status'], { cwd: repoPath, encoding: 'utf-8' });
@@ -127,4 +133,10 @@ export const githubProvider = {
127
133
  async changePrBase(repoPath, base) {
128
134
  await execFileAsync('gh', ['pr', 'edit', '--base', base], { cwd: repoPath, encoding: 'utf-8' });
129
135
  },
136
+ async mergeRequest(repoPath, number) {
137
+ await execFileAsync('gh', ['pr', 'merge', String(number), '--merge'], { cwd: repoPath, encoding: 'utf-8' });
138
+ },
139
+ async deleteRemoteBranch(repoPath, branch) {
140
+ await execFileAsync('git', ['push', 'origin', '--delete', branch], { cwd: repoPath, encoding: 'utf-8' });
141
+ },
130
142
  };
@@ -129,7 +129,13 @@ function availabilityFromError(err) {
129
129
  }
130
130
  export const gitlabProvider = {
131
131
  id: 'gitlab',
132
- capabilities: { canCreatePr: true, canChangePrBase: true, requestTermShort: 'MR' },
132
+ capabilities: {
133
+ canCreatePr: true,
134
+ canChangePrBase: true,
135
+ canMergeRequest: true,
136
+ canDeleteRemoteBranch: true,
137
+ requestTermShort: 'MR',
138
+ },
133
139
  async isAvailable(repoPath) {
134
140
  try {
135
141
  await execFileAsync('glab', ['auth', 'status'], { cwd: repoPath, encoding: 'utf-8' });
@@ -179,4 +185,10 @@ export const gitlabProvider = {
179
185
  async changePrBase(repoPath, base) {
180
186
  await execFileAsync('glab', ['mr', 'update', '--target-branch', base], { cwd: repoPath, encoding: 'utf-8' });
181
187
  },
188
+ async mergeRequest(repoPath, number) {
189
+ await execFileAsync('glab', ['mr', 'merge', String(number), '--yes'], { cwd: repoPath, encoding: 'utf-8' });
190
+ },
191
+ async deleteRemoteBranch(repoPath, branch) {
192
+ await execFileAsync('git', ['push', 'origin', '--delete', branch], { cwd: repoPath, encoding: 'utf-8' });
193
+ },
182
194
  };
@@ -7,7 +7,13 @@ import { ForgeUnavailableError } from './types.js';
7
7
  */
8
8
  export const noneProvider = {
9
9
  id: 'none',
10
- capabilities: { canCreatePr: false, canChangePrBase: false, requestTermShort: 'PR' },
10
+ capabilities: {
11
+ canCreatePr: false,
12
+ canChangePrBase: false,
13
+ canMergeRequest: false,
14
+ canDeleteRemoteBranch: false,
15
+ requestTermShort: 'PR',
16
+ },
11
17
  async isAvailable() {
12
18
  return { available: false };
13
19
  },
@@ -20,4 +26,10 @@ export const noneProvider = {
20
26
  async changePrBase() {
21
27
  throw new ForgeUnavailableError('This project has no supported forge configured');
22
28
  },
29
+ async mergeRequest() {
30
+ throw new ForgeUnavailableError('This project has no supported forge configured');
31
+ },
32
+ async deleteRemoteBranch() {
33
+ throw new ForgeUnavailableError('This project has no supported forge configured');
34
+ },
23
35
  };
@@ -768,6 +768,31 @@ const settingsMigrations = [
768
768
  global.showThinkingBlocks = true;
769
769
  },
770
770
  },
771
+ {
772
+ version: 48,
773
+ name: 'add-agent-error-notification-sound',
774
+ migrate: ({ global }) => {
775
+ if (typeof global.audioAgentErrorNotifications !== 'boolean')
776
+ global.audioAgentErrorNotifications = false;
777
+ if (typeof global.audioAgentErrorSound !== 'string' || global.audioAgentErrorSound.length === 0) {
778
+ global.audioAgentErrorSound = 'inherit';
779
+ }
780
+ if (typeof global.audioAgentErrorVolume !== 'number' || !Number.isFinite(global.audioAgentErrorVolume)) {
781
+ global.audioAgentErrorVolume = 1;
782
+ }
783
+ },
784
+ },
785
+ {
786
+ version: 49,
787
+ name: 'add-auto-loop-max-retries',
788
+ migrate: ({ global }) => {
789
+ if (typeof global.autoLoopMaxRetries !== 'number' ||
790
+ !Number.isInteger(global.autoLoopMaxRetries) ||
791
+ global.autoLoopMaxRetries < 1) {
792
+ global.autoLoopMaxRetries = 5;
793
+ }
794
+ },
795
+ },
771
796
  ];
772
797
  /** Current settings schema version — always equals the highest migration version. */
773
798
  export const SETTINGS_SCHEMA_VERSION = settingsMigrations.length > 0 ? settingsMigrations[settingsMigrations.length - 1].version : 0;
@@ -814,40 +839,44 @@ function defaultSettings() {
814
839
  fileManagerCommand: '',
815
840
  terminalCommand: '',
816
841
  autoPurgeOnPrMerged: false,
842
+ autoLoopMaxRetries: 5,
817
843
  networkAccessEnabled: false,
818
844
  networkAccessToken: '',
819
845
  networkAccessBehindProxy: false,
820
846
  browserNotifications: true,
821
847
  audioNotifications: true,
822
- audioQuestionNotifications: true,
823
- audioWorkspaceCreatedNotifications: true,
848
+ audioQuestionNotifications: false,
849
+ audioWorkspaceCreatedNotifications: false,
850
+ audioAgentErrorNotifications: false,
824
851
  audioNotificationSound: 'hey.mp3',
825
- audioQuestionSound: 'hey.mp3',
826
- audioWorkspaceCreatedSound: 'warcraft-3-humain-travail.mp3',
852
+ audioQuestionSound: 'inherit',
853
+ audioWorkspaceCreatedSound: 'inherit',
854
+ audioAgentErrorSound: 'inherit',
827
855
  audioPrCiFailedSound: 'inherit',
828
- audioPrCiFailedEnabled: true,
856
+ audioPrCiFailedEnabled: false,
829
857
  audioPrCiFailedVolume: 1,
830
858
  audioPrCiRecoveredSound: 'inherit',
831
- audioPrCiRecoveredEnabled: true,
859
+ audioPrCiRecoveredEnabled: false,
832
860
  audioPrCiRecoveredVolume: 1,
833
861
  audioPrChangesRequestedSound: 'inherit',
834
- audioPrChangesRequestedEnabled: true,
862
+ audioPrChangesRequestedEnabled: false,
835
863
  audioPrChangesRequestedVolume: 1,
836
864
  audioPrApprovedSound: 'inherit',
837
- audioPrApprovedEnabled: true,
865
+ audioPrApprovedEnabled: false,
838
866
  audioPrApprovedVolume: 1,
839
867
  audioPrMergeConflictSound: 'inherit',
840
- audioPrMergeConflictEnabled: true,
868
+ audioPrMergeConflictEnabled: false,
841
869
  audioPrMergeConflictVolume: 1,
842
870
  audioPrReadyToMergeSound: 'inherit',
843
- audioPrReadyToMergeEnabled: true,
871
+ audioPrReadyToMergeEnabled: false,
844
872
  audioPrReadyToMergeVolume: 1,
845
873
  audioPrMergedSound: 'inherit',
846
- audioPrMergedEnabled: true,
874
+ audioPrMergedEnabled: false,
847
875
  audioPrMergedVolume: 1,
848
876
  audioNotificationVolume: 1,
849
877
  audioQuestionVolume: 1,
850
878
  audioWorkspaceCreatedVolume: 1,
879
+ audioAgentErrorVolume: 1,
851
880
  notionStatusProperty: '',
852
881
  notionInProgressStatus: '',
853
882
  notionAssigneeProperty: '',
@@ -1182,6 +1211,15 @@ export function updateGlobalSettings(data) {
1182
1211
  delete data.cleanupScriptMode;
1183
1212
  }
1184
1213
  }
1214
+ if ('autoLoopMaxRetries' in data) {
1215
+ if (typeof data.autoLoopMaxRetries !== 'number' ||
1216
+ !Number.isInteger(data.autoLoopMaxRetries) ||
1217
+ data.autoLoopMaxRetries < 1 ||
1218
+ data.autoLoopMaxRetries > 20) {
1219
+ console.warn(`[settings] Invalid autoLoopMaxRetries value rejected: ${data.autoLoopMaxRetries}`);
1220
+ delete data.autoLoopMaxRetries;
1221
+ }
1222
+ }
1185
1223
  const allowedGlobalKeys = [
1186
1224
  'defaultModelByEngine',
1187
1225
  'dangerouslySkipPermissions',
@@ -1202,13 +1240,16 @@ export function updateGlobalSettings(data) {
1202
1240
  'fileManagerCommand',
1203
1241
  'terminalCommand',
1204
1242
  'autoPurgeOnPrMerged',
1243
+ 'autoLoopMaxRetries',
1205
1244
  'browserNotifications',
1206
1245
  'audioNotifications',
1207
1246
  'audioQuestionNotifications',
1208
1247
  'audioWorkspaceCreatedNotifications',
1248
+ 'audioAgentErrorNotifications',
1209
1249
  'audioNotificationSound',
1210
1250
  'audioQuestionSound',
1211
1251
  'audioWorkspaceCreatedSound',
1252
+ 'audioAgentErrorSound',
1212
1253
  'audioPrCiFailedSound',
1213
1254
  'audioPrCiFailedEnabled',
1214
1255
  'audioPrCiFailedVolume',
@@ -1233,6 +1274,7 @@ export function updateGlobalSettings(data) {
1233
1274
  'audioNotificationVolume',
1234
1275
  'audioQuestionVolume',
1235
1276
  'audioWorkspaceCreatedVolume',
1277
+ 'audioAgentErrorVolume',
1236
1278
  'notionStatusProperty',
1237
1279
  'notionInProgressStatus',
1238
1280
  'notionAssigneeProperty',
@@ -17,6 +17,13 @@ export const PROJECT_COLOR_PALETTE = [
17
17
  'amber-5',
18
18
  'orange-5',
19
19
  'brown-5',
20
+ 'light-blue-5',
21
+ 'light-green-5',
22
+ 'lime-5',
23
+ 'yellow-6',
24
+ 'deep-orange-5',
25
+ 'blue-grey-5',
26
+ 'grey-6',
20
27
  ];
21
28
  export function isValidProjectColor(value) {
22
29
  return typeof value === 'string' && PROJECT_COLOR_PALETTE.includes(value);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@loicngr/kobo",
3
- "version": "1.11.0",
3
+ "version": "1.11.2",
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",