@loicngr/kobo 1.7.6 → 1.7.7

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 (85) hide show
  1. package/README.md +84 -0
  2. package/dist/server/index.js +2 -0
  3. package/dist/server/routes/voice.js +149 -0
  4. package/dist/server/services/settings-service.js +57 -0
  5. package/dist/server/services/transcription-service.js +206 -0
  6. package/package.json +12 -10
  7. package/src/client/dist/spa/assets/{ActivityFeed-BboSPm4b.js → ActivityFeed-DlPVoOGb.js} +1 -1
  8. package/src/client/dist/spa/assets/{AutoLoopChip-w8D77bI5.js → AutoLoopChip-CkSzkC0C.js} +1 -1
  9. package/src/client/dist/spa/assets/{ClosePopup-D_UAdwkA.js → ClosePopup-DTcbxsC0.js} +1 -1
  10. package/src/client/dist/spa/assets/CreatePage-BoRappO3.css +1 -0
  11. package/src/client/dist/spa/assets/CreatePage-DpCVNwYk.js +2 -0
  12. package/src/client/dist/spa/assets/{DiffViewer-CblFgn8w.js → DiffViewer-D-uNbBq0.js} +3 -3
  13. package/src/client/dist/spa/assets/{HealthPage-CBSw7e5q.js → HealthPage-xZ0PP4F-.js} +1 -1
  14. package/src/client/dist/spa/assets/{MainLayout-DhaYycak.js → MainLayout-DdkKM2ba.js} +2 -2
  15. package/src/client/dist/spa/assets/{QBadge-DWH42dbo.js → QBadge-C7r6oPSi.js} +1 -1
  16. package/src/client/dist/spa/assets/{QBtn-a6jxWjmW.js → QBtn-DEuWKHbR.js} +1 -1
  17. package/src/client/dist/spa/assets/{QCheckbox-D5jfsxLV.js → QCheckbox-BvHfXBFY.js} +1 -1
  18. package/src/client/dist/spa/assets/{QChip-ByxK0Tuf.js → QChip-erWIZgxW.js} +1 -1
  19. package/src/client/dist/spa/assets/{QExpansionItem-CH1ipL9n.js → QExpansionItem-BGg74no1.js} +1 -1
  20. package/src/client/dist/spa/assets/QIcon-qfJNZLIW.js +1 -0
  21. package/src/client/dist/spa/assets/{QInput-Cm5-AGQ4.js → QInput-DCJEwE8V.js} +1 -1
  22. package/src/client/dist/spa/assets/{QItemLabel-DrTxqTqV.js → QItemLabel-CHkgkZVj.js} +1 -1
  23. package/src/client/dist/spa/assets/{QItemSection-5YpFpPDm.js → QItemSection-CQUDd0Vg.js} +1 -1
  24. package/src/client/dist/spa/assets/{QList-D0FtnQJI.js → QList-BbnN_oNX.js} +1 -1
  25. package/src/client/dist/spa/assets/{QMenu-B4xMxMGd.js → QMenu-D6uqosRg.js} +1 -1
  26. package/src/client/dist/spa/assets/{QPage-DFi3K093.js → QPage-Co2h9wd_.js} +1 -1
  27. package/src/client/dist/spa/assets/{QRadio-B3aKjCVu.js → QRadio-DJxOyOA3.js} +1 -1
  28. package/src/client/dist/spa/assets/QSpace-DKIph84L.js +1 -0
  29. package/src/client/dist/spa/assets/{QSpinnerDots-CszPQQ9J.js → QSpinnerDots-Bfl2RMy4.js} +1 -1
  30. package/src/client/dist/spa/assets/{QTabPanels-D2ks0UIA.js → QTabPanels-ClPY9y4T.js} +1 -1
  31. package/src/client/dist/spa/assets/{QToggle-1-N9qWq4.js → QToggle-DNOTC_3a.js} +1 -1
  32. package/src/client/dist/spa/assets/{QTooltip-fDNzBEfN.js → QTooltip-DUGPNNeQ.js} +1 -1
  33. package/src/client/dist/spa/assets/{SearchPage-cZTwP4Lf.js → SearchPage-C07dgzT9.js} +1 -1
  34. package/src/client/dist/spa/assets/SettingsPage-CLNmI0Rr.css +1 -0
  35. package/src/client/dist/spa/assets/SettingsPage-D0CZNqkA.js +9 -0
  36. package/src/client/dist/spa/assets/{TouchPan-DoE24Io3.js → TouchPan-DvVlszwO.js} +1 -1
  37. package/src/client/dist/spa/assets/WorkspacePage-CKeCLPi0.js +4 -0
  38. package/src/client/dist/spa/assets/WorkspacePage-CRIcsASQ.css +1 -0
  39. package/src/client/dist/spa/assets/{build-path-tree-B1Lvvqto.js → build-path-tree-CCMckvpr.js} +1 -1
  40. package/src/client/dist/spa/assets/{cssMode-BFLYiiEw.js → cssMode-D6XTTdwy.js} +1 -1
  41. package/src/client/dist/spa/assets/{documents-kx0vLfSG.js → documents-soWtna0O.js} +1 -1
  42. package/src/client/dist/spa/assets/{editor.api-2asmmhth.js → editor.api-6hDVHddO.js} +1 -1
  43. package/src/client/dist/spa/assets/{editor.main-ChCYZyez.js → editor.main-DsLU1RWu.js} +3 -3
  44. package/src/client/dist/spa/assets/{expand-template-CXQFkQOJ.js → expand-template-Crz1uiBt.js} +1 -1
  45. package/src/client/dist/spa/assets/{formatters-DCAQ6ANJ.js → formatters-guwb-rzl.js} +1 -1
  46. package/src/client/dist/spa/assets/{freemarker2-BaBL9E9G.js → freemarker2-Bn1f0t2U.js} +1 -1
  47. package/src/client/dist/spa/assets/{handlebars-BxDour4L.js → handlebars-O92Cbq66.js} +1 -1
  48. package/src/client/dist/spa/assets/{html-C6hnkfIL.js → html-Ck95BMBU.js} +1 -1
  49. package/src/client/dist/spa/assets/{htmlMode-9zT3-dmz.js → htmlMode-DDYhH2FJ.js} +1 -1
  50. package/src/client/dist/spa/assets/i18n-BLgknHpf.js +1 -0
  51. package/src/client/dist/spa/assets/index-CdHDdk1y.js +2 -0
  52. package/src/client/dist/spa/assets/{javascript-C3YjvKbE.js → javascript-Cy2ddqHg.js} +1 -1
  53. package/src/client/dist/spa/assets/{jsonMode-DcJDgMzf.js → jsonMode-BIfVcp5z.js} +1 -1
  54. package/src/client/dist/spa/assets/{liquid-CsT8SjJM.js → liquid-B287eegh.js} +1 -1
  55. package/src/client/dist/spa/assets/{mdx-CT3yVSyc.js → mdx-B8HSzGai.js} +1 -1
  56. package/src/client/dist/spa/assets/{models-BsjWUKqM.js → models-Bd_v3W7Q.js} +1 -1
  57. package/src/client/dist/spa/assets/{monaco.contribution-DKGNz1oQ.js → monaco.contribution-CofcHzEf.js} +2 -2
  58. package/src/client/dist/spa/assets/{notifications-OnPq4FrH.js → notifications-BPnKFW60.js} +1 -1
  59. package/src/client/dist/spa/assets/{purify.es-CPieV82n.js → purify.es-BCEwTYRx.js} +1 -1
  60. package/src/client/dist/spa/assets/{python-Ca5miKgj.js → python-csaKR6_U.js} +1 -1
  61. package/src/client/dist/spa/assets/{razor-7qzusGRc.js → razor-C2wEv-nX.js} +1 -1
  62. package/src/client/dist/spa/assets/{render-chat-markdown-Bqq2G-yI.js → render-chat-markdown-Bjcei0vn.js} +1 -1
  63. package/src/client/dist/spa/assets/runtime-core.esm-bundler-9Z0QAO_7.js +1 -0
  64. package/src/client/dist/spa/assets/{tsMode-BdvO8jZ2.js → tsMode-DGLVs57K.js} +1 -1
  65. package/src/client/dist/spa/assets/{typescript-BfVNzhgs.js → typescript-w0GWHzZ3.js} +1 -1
  66. package/src/client/dist/spa/assets/{use-checkbox-D7zmRxGI.js → use-checkbox-y_fOkYZN.js} +1 -1
  67. package/src/client/dist/spa/assets/{use-id-CuaR1RiE.js → use-id-_7wiRcgb.js} +1 -1
  68. package/src/client/dist/spa/assets/{use-panel-D-8nAQns.js → use-panel-CbJ44rqY.js} +1 -1
  69. package/src/client/dist/spa/assets/use-quasar-DQYS47mh.js +1 -0
  70. package/src/client/dist/spa/assets/{vue-i18n-BcfTCFFS.js → vue-i18n-DI-gS-CC.js} +1 -1
  71. package/src/client/dist/spa/assets/{xml-DGNXGqXL.js → xml-CTn-vnEd.js} +1 -1
  72. package/src/client/dist/spa/assets/{yaml-CtAtOyt5.js → yaml-CTyUSvLZ.js} +1 -1
  73. package/src/client/dist/spa/index.html +12 -12
  74. package/src/client/dist/spa/assets/CreatePage-BDObLDJc.js +0 -2
  75. package/src/client/dist/spa/assets/CreatePage-DssmsAsV.css +0 -1
  76. package/src/client/dist/spa/assets/QIcon-BJuyqdsT.js +0 -1
  77. package/src/client/dist/spa/assets/QSpace-CLtL3aPy.js +0 -1
  78. package/src/client/dist/spa/assets/SettingsPage-C1efO0VM.js +0 -1
  79. package/src/client/dist/spa/assets/SettingsPage-CMyeQ9_u.css +0 -1
  80. package/src/client/dist/spa/assets/WorkspacePage-3jcof896.js +0 -4
  81. package/src/client/dist/spa/assets/WorkspacePage-CCtIrBiR.css +0 -1
  82. package/src/client/dist/spa/assets/i18n-CLY0XI9-.js +0 -1
  83. package/src/client/dist/spa/assets/index-D6wj_wQ9.js +0 -2
  84. package/src/client/dist/spa/assets/runtime-core.esm-bundler-C3IgBgY5.js +0 -1
  85. package/src/client/dist/spa/assets/use-quasar-Sdcq6zzV.js +0 -1
package/README.md CHANGED
@@ -155,6 +155,90 @@ If you need to pin a specific version of the Notion MCP server, use a fork, or a
155
155
 
156
156
  Without a valid token configured, the Notion import field in the workspace creation form will return an error when you click **Refresh** or submit a Notion URL — the rest of Kōbō (workspaces, agents, tasks, Git integration) keeps working independently.
157
157
 
158
+ ## Voice transcription (local Whisper)
159
+
160
+ Kōbō supports local voice transcription with push-to-talk in both:
161
+
162
+ - `WorkspacePage` (chat input)
163
+ - `CreatePage` (workspace instructions textarea)
164
+
165
+ ### Requirements
166
+
167
+ - `whisper-cli` from [`whisper.cpp`](https://github.com/ggml-org/whisper.cpp)
168
+ - `ffmpeg`
169
+ - `cmake` (required to build `whisper.cpp` from source)
170
+ - At least one Whisper model downloaded from **Settings → Voice**
171
+
172
+ ### Install `whisper.cpp` (Linux/macOS)
173
+
174
+ ```bash
175
+ git clone https://github.com/ggml-org/whisper.cpp.git
176
+ cd whisper.cpp
177
+ cmake -B build
178
+ cmake --build build -j
179
+ ```
180
+
181
+ This usually produces `build/bin/whisper-cli`.
182
+
183
+ You can also download a prebuilt archive from the `whisper.cpp` releases page (for example: <https://github.com/ggml-org/whisper.cpp/releases/tag/v1.8.4>) and point Kōbō to the extracted `whisper-cli` binary path.
184
+
185
+ ### Install `ffmpeg`
186
+
187
+ Ubuntu / Debian:
188
+
189
+ ```bash
190
+ sudo apt update
191
+ sudo apt install -y cmake build-essential ffmpeg
192
+ ```
193
+
194
+ Windows:
195
+
196
+ - Install `ffmpeg` (for example via Chocolatey: `choco install ffmpeg`, or via Scoop: `scoop install ffmpeg`)
197
+ - Verify in PowerShell:
198
+
199
+ ```powershell
200
+ where ffmpeg
201
+ ffmpeg -version
202
+ ```
203
+
204
+ ### Windows notes for `whisper.cpp`
205
+
206
+ Install CMake and Visual Studio Build Tools (C/C++), then build `whisper.cpp` (or use a prebuilt `whisper-cli`), then verify:
207
+
208
+ ```powershell
209
+ where whisper-cli
210
+ whisper-cli -h
211
+ ```
212
+
213
+ ### Configure in Kōbō
214
+
215
+ Open **Settings → Voice**:
216
+
217
+ - Enable voice transcription
218
+ - Optionally set:
219
+ - **Whisper binary path (optional)**
220
+ - **ffmpeg binary path (optional)**
221
+ - If left empty, Kōbō falls back to:
222
+ - `whisper-cli` from `PATH` (or `WHISPER_CPP_COMMAND` if set)
223
+ - `ffmpeg` from `PATH`
224
+ - Download a model (e.g. `base`) and select it as active
225
+
226
+ The Voice panel shows runtime status (`ready/missing`) for both Whisper and ffmpeg so setup issues are visible immediately.
227
+
228
+ ### Advanced voice parameters
229
+
230
+ Kōbō exposes additional transcription settings in **Settings → Voice**:
231
+
232
+ - **Temperature** (`0..1`) — decoding stability vs flexibility
233
+ - **Initial prompt** — optional context/jargon for better recognition
234
+ - **Translate to English** — translate non-English speech to English
235
+ - **Suppress non-speech tokens** — reduce non-speech artifacts in output
236
+
237
+ Recommended defaults by model:
238
+
239
+ - `tiny` / `base` → `0.1`
240
+ - `small` / `medium` / `large-v3` → `0.2`
241
+
158
242
  ## Sentry integration
159
243
 
160
244
  Kōbō can turn a Sentry issue into a dedicated "fix workspace" — you paste the issue URL at workspace creation and Kōbō extracts the stacktrace, culprit, tags, offending spans and extra context, writes them as a local markdown file inside the worktree (`.ai/thoughts/SENTRY-<id>.md`), and primes the Claude agent with a TDD fix workflow that points at that file. The agent also keeps access to the Sentry MCP tools (`search_issue_events`, `get_issue_tag_values`, `get_sentry_resource`) so it can dig deeper on its own. **This feature is opt-in and reuses the Sentry MCP configuration you already have for Claude Code** — Kōbō does not manage a Sentry token separately.
@@ -19,6 +19,7 @@ import sentryRouter from './routes/sentry.js';
19
19
  import settingsRouter from './routes/settings.js';
20
20
  import templatesRouter from './routes/templates.js';
21
21
  import usageRoutes from './routes/usage.js';
22
+ import voiceRouter from './routes/voice.js';
22
23
  import workspacesRouter from './routes/workspaces.js';
23
24
  import { getAvailableSkills, reconcileOrphanSessions, restoreRetryCountsFromDb, sendMessage, setBackendPort, startAgent, startWatchdog, stopAgent, stopWatchdog, } from './services/agent/orchestrator.js';
24
25
  import * as autoLoopService from './services/auto-loop-service.js';
@@ -83,6 +84,7 @@ app.route('/api/search', searchRouter);
83
84
  app.route('/api/health', healthRouter);
84
85
  app.route('/api/engines', enginesRouter);
85
86
  app.route('/api/migration', migrationRouter);
87
+ app.route('/api/voice', voiceRouter);
86
88
  // Skills endpoint
87
89
  app.get('/api/skills', (c) => c.json(getAvailableSkills()));
88
90
  const PORT = parseInt(process.env.SERVER_PORT || process.env.PORT || '3000', 10);
@@ -0,0 +1,149 @@
1
+ import { Hono } from 'hono';
2
+ import * as settingsService from '../services/settings-service.js';
3
+ import * as transcriptionService from '../services/transcription-service.js';
4
+ import * as workspaceService from '../services/workspace-service.js';
5
+ const app = new Hono();
6
+ const MAX_AUDIO_SIZE = 10 * 1024 * 1024;
7
+ const ALLOWED_AUDIO_MIME = new Set(['audio/webm', 'audio/ogg', 'audio/wav', 'audio/mpeg', 'audio/mp4']);
8
+ const LANGUAGE_RE = /^[a-z-]+$/i;
9
+ function isVoiceLikeError(err) {
10
+ if (!err || typeof err !== 'object')
11
+ return false;
12
+ const e = err;
13
+ return typeof e.message === 'string' && typeof e.code === 'string' && typeof e.status === 'number';
14
+ }
15
+ function toVoiceHttpStatus(status) {
16
+ return status === 400 ? 400 : 500;
17
+ }
18
+ async function parseAndTranscribeFromBody(c, config) {
19
+ const body = await c.req.parseBody();
20
+ const audio = body.audio;
21
+ const languageRaw = body.language;
22
+ const language = typeof languageRaw === 'string' && languageRaw.trim().length > 0 ? languageRaw.trim() : 'auto';
23
+ if (language !== 'auto' && (!LANGUAGE_RE.test(language) || language.length > 16)) {
24
+ return c.json({ error: `Invalid language '${language}'`, code: 'LANGUAGE_INVALID' }, 400);
25
+ }
26
+ if (!audio || !(audio instanceof File)) {
27
+ return c.json({ error: 'Missing audio field in multipart body', code: 'MIC_AUDIO_INVALID' }, 400);
28
+ }
29
+ if (!ALLOWED_AUDIO_MIME.has(audio.type)) {
30
+ return c.json({ error: `Unsupported audio type '${audio.type}'`, code: 'MIC_AUDIO_INVALID' }, 400);
31
+ }
32
+ const buffer = Buffer.from(await audio.arrayBuffer());
33
+ if (buffer.length === 0 || buffer.length > MAX_AUDIO_SIZE) {
34
+ return c.json({ error: 'Invalid audio size', code: 'MIC_AUDIO_INVALID' }, 400);
35
+ }
36
+ const result = await transcriptionService.transcribeAudio({
37
+ audioBuffer: buffer,
38
+ modelName: config.modelName,
39
+ language,
40
+ temperature: config.temperature,
41
+ prompt: config.prompt,
42
+ translateToEnglish: config.translateToEnglish,
43
+ suppressNonSpeechTokens: config.suppressNonSpeechTokens,
44
+ });
45
+ return c.json(result);
46
+ }
47
+ app.get('/models', (c) => {
48
+ try {
49
+ return c.json(transcriptionService.listVoiceModels());
50
+ }
51
+ catch (err) {
52
+ const message = err instanceof Error ? err.message : String(err);
53
+ return c.json({ error: message }, 500);
54
+ }
55
+ });
56
+ app.get('/runtime', async (c) => {
57
+ try {
58
+ const status = await transcriptionService.getVoiceRuntimeStatus();
59
+ return c.json(status);
60
+ }
61
+ catch (err) {
62
+ const message = err instanceof Error ? err.message : String(err);
63
+ return c.json({ error: message, code: 'VOICE_RUNTIME_CHECK_FAILED' }, 500);
64
+ }
65
+ });
66
+ app.post('/models/:name/download', async (c) => {
67
+ try {
68
+ const name = c.req.param('name');
69
+ const result = await transcriptionService.downloadVoiceModel(name);
70
+ return c.json(result, 201);
71
+ }
72
+ catch (err) {
73
+ if (err instanceof transcriptionService.VoiceError || isVoiceLikeError(err)) {
74
+ return c.json({ error: err.message, code: err.code }, toVoiceHttpStatus(err.status));
75
+ }
76
+ const message = err instanceof Error ? err.message : String(err);
77
+ return c.json({ error: message, code: 'MODEL_DOWNLOAD_FAILED' }, 500);
78
+ }
79
+ });
80
+ app.delete('/models/:name', (c) => {
81
+ try {
82
+ const name = c.req.param('name');
83
+ transcriptionService.deleteVoiceModel(name);
84
+ return c.body(null, 204);
85
+ }
86
+ catch (err) {
87
+ if (err instanceof transcriptionService.VoiceError || isVoiceLikeError(err)) {
88
+ return c.json({ error: err.message, code: err.code }, toVoiceHttpStatus(err.status));
89
+ }
90
+ const message = err instanceof Error ? err.message : String(err);
91
+ return c.json({ error: message, code: 'MODEL_DELETE_FAILED' }, 500);
92
+ }
93
+ });
94
+ app.post('/workspaces/:id/transcribe', async (c) => {
95
+ try {
96
+ const id = c.req.param('id');
97
+ const workspace = workspaceService.getWorkspace(id);
98
+ if (!workspace)
99
+ return c.json({ error: `Workspace '${id}' not found` }, 404);
100
+ const global = settingsService.getGlobalSettings();
101
+ if (!global.voiceEnabled) {
102
+ return c.json({ error: 'Voice transcription is disabled', code: 'VOICE_DISABLED' }, 400);
103
+ }
104
+ if (!global.voiceModel) {
105
+ return c.json({ error: 'No voice model configured', code: 'MODEL_NOT_CONFIGURED' }, 400);
106
+ }
107
+ return await parseAndTranscribeFromBody(c, {
108
+ modelName: global.voiceModel,
109
+ temperature: global.voiceTemperature,
110
+ prompt: global.voicePrompt,
111
+ translateToEnglish: global.voiceTranslateToEnglish,
112
+ suppressNonSpeechTokens: global.voiceSuppressNonSpeechTokens,
113
+ });
114
+ }
115
+ catch (err) {
116
+ if (err instanceof transcriptionService.VoiceError || isVoiceLikeError(err)) {
117
+ return c.json({ error: err.message, code: err.code }, toVoiceHttpStatus(err.status));
118
+ }
119
+ const message = err instanceof Error ? err.message : String(err);
120
+ return c.json({ error: message, code: 'TRANSCRIPTION_FAILED' }, 500);
121
+ }
122
+ });
123
+ // Draft transcription endpoint used before a workspace exists (Create page).
124
+ app.post('/transcribe', async (c) => {
125
+ try {
126
+ const global = settingsService.getGlobalSettings();
127
+ if (!global.voiceEnabled) {
128
+ return c.json({ error: 'Voice transcription is disabled', code: 'VOICE_DISABLED' }, 400);
129
+ }
130
+ if (!global.voiceModel) {
131
+ return c.json({ error: 'No voice model configured', code: 'MODEL_NOT_CONFIGURED' }, 400);
132
+ }
133
+ return await parseAndTranscribeFromBody(c, {
134
+ modelName: global.voiceModel,
135
+ temperature: global.voiceTemperature,
136
+ prompt: global.voicePrompt,
137
+ translateToEnglish: global.voiceTranslateToEnglish,
138
+ suppressNonSpeechTokens: global.voiceSuppressNonSpeechTokens,
139
+ });
140
+ }
141
+ catch (err) {
142
+ if (err instanceof transcriptionService.VoiceError) {
143
+ return c.json({ error: err.message, code: err.code }, toVoiceHttpStatus(err.status));
144
+ }
145
+ const message = err instanceof Error ? err.message : String(err);
146
+ return c.json({ error: message, code: 'TRANSCRIPTION_FAILED' }, 500);
147
+ }
148
+ });
149
+ export default app;
@@ -266,6 +266,39 @@ const settingsMigrations = [
266
266
  }
267
267
  },
268
268
  },
269
+ {
270
+ version: 17,
271
+ name: 'add-voice-transcription-settings',
272
+ migrate({ global }) {
273
+ if (typeof global.voiceEnabled !== 'boolean')
274
+ global.voiceEnabled = false;
275
+ if (global.voicePttKey !== 'alt' && global.voicePttKey !== 'ctrl+space')
276
+ global.voicePttKey = 'alt';
277
+ if (typeof global.voiceLanguage !== 'string' || global.voiceLanguage.length === 0)
278
+ global.voiceLanguage = 'auto';
279
+ if (typeof global.voiceModel !== 'string' && global.voiceModel !== null)
280
+ global.voiceModel = null;
281
+ if (typeof global.voiceCommandPath !== 'string')
282
+ global.voiceCommandPath = '';
283
+ if (typeof global.voiceFfmpegPath !== 'string')
284
+ global.voiceFfmpegPath = '';
285
+ },
286
+ },
287
+ {
288
+ version: 18,
289
+ name: 'add-voice-advanced-settings',
290
+ migrate({ global }) {
291
+ const t = Number(global.voiceTemperature);
292
+ if (!Number.isFinite(t) || t < 0 || t > 1)
293
+ global.voiceTemperature = 0;
294
+ if (typeof global.voicePrompt !== 'string')
295
+ global.voicePrompt = '';
296
+ if (typeof global.voiceTranslateToEnglish !== 'boolean')
297
+ global.voiceTranslateToEnglish = false;
298
+ if (typeof global.voiceSuppressNonSpeechTokens !== 'boolean')
299
+ global.voiceSuppressNonSpeechTokens = true;
300
+ },
301
+ },
269
302
  ];
270
303
  /** Current settings schema version — always equals the highest migration version. */
271
304
  export const SETTINGS_SCHEMA_VERSION = settingsMigrations.length > 0 ? settingsMigrations[settingsMigrations.length - 1].version : 0;
@@ -313,6 +346,16 @@ function defaultSettings() {
313
346
  tags: [...DEFAULT_WORKSPACE_TAGS],
314
347
  worktreesPath: WORKTREES_PATH,
315
348
  worktreesPrefixByProject: false,
349
+ voiceEnabled: false,
350
+ voicePttKey: 'alt',
351
+ voiceLanguage: 'auto',
352
+ voiceModel: null,
353
+ voiceCommandPath: '',
354
+ voiceFfmpegPath: '',
355
+ voiceTemperature: 0,
356
+ voicePrompt: '',
357
+ voiceTranslateToEnglish: false,
358
+ voiceSuppressNonSpeechTokens: true,
316
359
  },
317
360
  projects: [],
318
361
  };
@@ -551,6 +594,16 @@ export function updateGlobalSettings(data) {
551
594
  'tags',
552
595
  'worktreesPath',
553
596
  'worktreesPrefixByProject',
597
+ 'voiceEnabled',
598
+ 'voicePttKey',
599
+ 'voiceLanguage',
600
+ 'voiceModel',
601
+ 'voiceCommandPath',
602
+ 'voiceFfmpegPath',
603
+ 'voiceTemperature',
604
+ 'voicePrompt',
605
+ 'voiceTranslateToEnglish',
606
+ 'voiceSuppressNonSpeechTokens',
554
607
  ];
555
608
  const filtered = pickKnownKeys(data, allowedGlobalKeys);
556
609
  if (filtered.tags !== undefined) {
@@ -564,6 +617,10 @@ export function updateGlobalSettings(data) {
564
617
  const v = Number(filtered.audioNotificationVolume);
565
618
  filtered.audioNotificationVolume = Number.isFinite(v) ? Math.max(0, Math.min(1, v)) : 1;
566
619
  }
620
+ if (filtered.voiceTemperature !== undefined) {
621
+ const t = Number(filtered.voiceTemperature);
622
+ filtered.voiceTemperature = Number.isFinite(t) ? Math.max(0, Math.min(1, t)) : settings.global.voiceTemperature;
623
+ }
567
624
  if (filtered.worktreesPath !== undefined) {
568
625
  filtered.worktreesPath = validateWorktreesPath(filtered.worktreesPath, { allowEmpty: false });
569
626
  ensureGlobalWorktreesRootExists(filtered.worktreesPath);
@@ -0,0 +1,206 @@
1
+ import { execFile } from 'node:child_process';
2
+ import fs from 'node:fs';
3
+ import os from 'node:os';
4
+ import path from 'node:path';
5
+ import { promisify } from 'node:util';
6
+ import { getKoboHome } from '../utils/paths.js';
7
+ import { getGlobalSettings } from './settings-service.js';
8
+ const execFileAsync = promisify(execFile);
9
+ const MAX_LANG_LENGTH = 16;
10
+ export class VoiceError extends Error {
11
+ code;
12
+ status;
13
+ constructor(message, code, status = 400) {
14
+ super(message);
15
+ this.code = code;
16
+ this.status = status;
17
+ this.name = 'VoiceError';
18
+ }
19
+ }
20
+ export const VOICE_MODELS = [
21
+ {
22
+ name: 'tiny',
23
+ fileName: 'ggml-tiny.bin',
24
+ url: 'https://huggingface.co/ggerganov/whisper.cpp/resolve/main/ggml-tiny.bin?download=true',
25
+ },
26
+ {
27
+ name: 'base',
28
+ fileName: 'ggml-base.bin',
29
+ url: 'https://huggingface.co/ggerganov/whisper.cpp/resolve/main/ggml-base.bin?download=true',
30
+ },
31
+ {
32
+ name: 'small',
33
+ fileName: 'ggml-small.bin',
34
+ url: 'https://huggingface.co/ggerganov/whisper.cpp/resolve/main/ggml-small.bin?download=true',
35
+ },
36
+ {
37
+ name: 'medium',
38
+ fileName: 'ggml-medium.bin',
39
+ url: 'https://huggingface.co/ggerganov/whisper.cpp/resolve/main/ggml-medium.bin?download=true',
40
+ },
41
+ {
42
+ name: 'large-v3',
43
+ fileName: 'ggml-large-v3.bin',
44
+ url: 'https://huggingface.co/ggerganov/whisper.cpp/resolve/main/ggml-large-v3.bin?download=true',
45
+ },
46
+ ];
47
+ function voiceHome() {
48
+ return path.join(getKoboHome(), 'voice');
49
+ }
50
+ function modelsDir() {
51
+ return path.join(voiceHome(), 'models', 'whisper');
52
+ }
53
+ function resolveWhisperCommand() {
54
+ const global = getGlobalSettings();
55
+ const fromSettings = (global.voiceCommandPath ?? '').trim();
56
+ if (fromSettings.length > 0)
57
+ return fromSettings;
58
+ return process.env.WHISPER_CPP_COMMAND || 'whisper-cli';
59
+ }
60
+ function resolveFfmpegCommand() {
61
+ const global = getGlobalSettings();
62
+ const fromSettings = (global.voiceFfmpegPath ?? '').trim();
63
+ if (fromSettings.length > 0)
64
+ return fromSettings;
65
+ return 'ffmpeg';
66
+ }
67
+ function ensureVoiceDirs() {
68
+ fs.mkdirSync(modelsDir(), { recursive: true });
69
+ }
70
+ function resolveModel(name) {
71
+ const model = VOICE_MODELS.find((m) => m.name === name);
72
+ if (!model)
73
+ throw new VoiceError(`Unknown voice model '${name}'`, 'MODEL_UNKNOWN', 400);
74
+ return model;
75
+ }
76
+ export function listVoiceModels() {
77
+ ensureVoiceDirs();
78
+ const settings = getGlobalSettings();
79
+ const available = VOICE_MODELS.map((m) => ({
80
+ name: m.name,
81
+ fileName: m.fileName,
82
+ installed: fs.existsSync(path.join(modelsDir(), m.fileName)),
83
+ }));
84
+ return { available, activeModel: settings.voiceModel };
85
+ }
86
+ export async function getVoiceRuntimeStatus() {
87
+ const command = resolveWhisperCommand();
88
+ let ffmpegAvailable = true;
89
+ let ffmpegError;
90
+ try {
91
+ await execFileAsync(resolveFfmpegCommand(), ['-version'], { timeout: 5000 });
92
+ }
93
+ catch (err) {
94
+ ffmpegAvailable = false;
95
+ ffmpegError = err instanceof Error ? err.message : String(err);
96
+ }
97
+ try {
98
+ await execFileAsync(command, ['-h'], { timeout: 5000 });
99
+ return { available: ffmpegAvailable, command, ffmpegAvailable, ffmpegError };
100
+ }
101
+ catch (err) {
102
+ const message = err instanceof Error ? err.message : String(err);
103
+ return { available: false, command, error: message, ffmpegAvailable, ffmpegError };
104
+ }
105
+ }
106
+ export async function downloadVoiceModel(name) {
107
+ ensureVoiceDirs();
108
+ const model = resolveModel(name);
109
+ const res = await fetch(model.url);
110
+ if (!res.ok) {
111
+ throw new VoiceError(`Failed to download model '${name}' (HTTP ${res.status})`, 'MODEL_DOWNLOAD_FAILED', 500);
112
+ }
113
+ const filePath = path.join(modelsDir(), model.fileName);
114
+ const tmpPath = `${filePath}.tmp`;
115
+ try {
116
+ const bytes = Buffer.from(await res.arrayBuffer());
117
+ fs.writeFileSync(tmpPath, bytes);
118
+ fs.renameSync(tmpPath, filePath);
119
+ }
120
+ finally {
121
+ if (fs.existsSync(tmpPath))
122
+ fs.rmSync(tmpPath, { force: true });
123
+ }
124
+ return { name, filePath };
125
+ }
126
+ export function deleteVoiceModel(name) {
127
+ const model = resolveModel(name);
128
+ const filePath = path.join(modelsDir(), model.fileName);
129
+ if (fs.existsSync(filePath))
130
+ fs.unlinkSync(filePath);
131
+ }
132
+ function getInstalledModelPath(name) {
133
+ const model = resolveModel(name);
134
+ const fullPath = path.join(modelsDir(), model.fileName);
135
+ if (!fs.existsSync(fullPath)) {
136
+ throw new VoiceError(`Model '${name}' is not installed`, 'MODEL_NOT_INSTALLED', 400);
137
+ }
138
+ return fullPath;
139
+ }
140
+ export async function transcribeAudio(params) {
141
+ const { audioBuffer, modelName } = params;
142
+ const language = params.language && params.language.trim().length > 0 ? params.language : 'auto';
143
+ const temperature = Number.isFinite(Number(params.temperature))
144
+ ? Math.max(0, Math.min(1, Number(params.temperature)))
145
+ : 0;
146
+ const prompt = (params.prompt ?? '').trim();
147
+ const translateToEnglish = params.translateToEnglish === true;
148
+ const suppressNst = params.suppressNonSpeechTokens !== false;
149
+ if (language.length > MAX_LANG_LENGTH || !/^[a-z-]+$/i.test(language)) {
150
+ throw new VoiceError(`Invalid language '${language}'`, 'LANGUAGE_INVALID', 400);
151
+ }
152
+ const modelPath = getInstalledModelPath(modelName);
153
+ const tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), 'kobo-voice-'));
154
+ const audioPath = path.join(tmpDir, 'input.webm');
155
+ const wavPath = path.join(tmpDir, 'input.wav');
156
+ try {
157
+ fs.writeFileSync(audioPath, audioBuffer);
158
+ // Normalize browser-recorded audio (webm/ogg/...) to a mono WAV file that
159
+ // whisper-cli can decode reliably across platforms.
160
+ await execFileAsync(resolveFfmpegCommand(), ['-y', '-i', audioPath, '-ar', '16000', '-ac', '1', wavPath], {
161
+ timeout: 60000,
162
+ });
163
+ const cmd = resolveWhisperCommand();
164
+ const args = [
165
+ '-m',
166
+ modelPath,
167
+ '-f',
168
+ wavPath,
169
+ '-otxt',
170
+ '-of',
171
+ path.join(tmpDir, 'out'),
172
+ '--temperature',
173
+ String(temperature),
174
+ ];
175
+ if (language !== 'auto')
176
+ args.push('-l', language);
177
+ if (translateToEnglish)
178
+ args.push('--translate');
179
+ if (suppressNst)
180
+ args.push('--suppress-nst');
181
+ if (prompt.length > 0)
182
+ args.push('--prompt', prompt);
183
+ const start = Date.now();
184
+ const { stderr } = await execFileAsync(cmd, args, { timeout: 120000 });
185
+ const durationMs = Date.now() - start;
186
+ const outTxt = path.join(tmpDir, 'out.txt');
187
+ if (!fs.existsSync(outTxt)) {
188
+ throw new VoiceError(`Transcription output missing (${stderr || 'no stderr'})`, 'TRANSCRIPTION_FAILED', 500);
189
+ }
190
+ const text = fs.readFileSync(outTxt, 'utf-8').trim();
191
+ return { text, durationMs, model: modelName, language };
192
+ }
193
+ catch (err) {
194
+ const message = err instanceof Error ? err.message : String(err);
195
+ if (message.includes('ENOENT')) {
196
+ throw new VoiceError('Voice runtime missing (whisper-cli or ffmpeg)', 'VOICE_RUNTIME_MISSING', 500);
197
+ }
198
+ if (message.includes('timed out')) {
199
+ throw new VoiceError('Whisper transcription timed out', 'TRANSCRIPTION_TIMEOUT', 500);
200
+ }
201
+ throw err;
202
+ }
203
+ finally {
204
+ fs.rmSync(tmpDir, { recursive: true, force: true });
205
+ }
206
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@loicngr/kobo",
3
- "version": "1.7.6",
3
+ "version": "1.7.7",
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",
@@ -66,25 +66,27 @@
66
66
  "prepublishOnly": "npm run build"
67
67
  },
68
68
  "dependencies": {
69
- "@anthropic-ai/claude-agent-sdk": "^0.2.126",
70
- "@hono/node-server": "^1.19.13",
69
+ "@anthropic-ai/claude-agent-sdk": "^0.2.90",
70
+ "@emnapi/core": "^1.10.0",
71
+ "@emnapi/runtime": "^1.10.0",
72
+ "@hono/node-server": "^2.0.2",
71
73
  "@modelcontextprotocol/sdk": "^1.29.0",
72
- "better-sqlite3": "^12.8.0",
74
+ "better-sqlite3": "^12.9.0",
73
75
  "cron-parser": "^5.5.0",
74
- "hono": "^4.12.12",
75
- "nanoid": "^5.1.7",
76
+ "hono": "^4.12.18",
77
+ "nanoid": "^5.1.11",
76
78
  "node-pty": "^1.1.0",
77
79
  "ws": "^8.20.0"
78
80
  },
79
81
  "devDependencies": {
80
82
  "@biomejs/biome": "2.4.10",
81
83
  "@types/better-sqlite3": "^7.6.13",
82
- "@types/node": "^25.5.2",
84
+ "@types/node": "^25.6.2",
83
85
  "@types/ws": "^8.18.1",
84
- "@vitest/runner": "^4.1.2",
86
+ "@vitest/runner": "^4.1.5",
85
87
  "concurrently": "^9.2.1",
86
88
  "tsx": "^4.21.0",
87
- "typescript": "^6.0.2",
88
- "vitest": "^3.2.4"
89
+ "typescript": "^6.0.3",
90
+ "vitest": "^4.1.5"
89
91
  }
90
92
  }
@@ -1,4 +1,4 @@
1
- import{E as e,F as t,H as n,L as r,M as i,Q as a,U as o,_t as s,bt as c,d as l,f as u,g as d,h as f,l as p,p as m,r as h,rt as g,u as _,v,yt as y}from"./runtime-core.esm-bundler-C3IgBgY5.js";import{U as b,l as x,t as S}from"./QIcon-BJuyqdsT.js";import{c as C,s as w}from"./notifications-OnPq4FrH.js";import{t as T}from"./QBtn-a6jxWjmW.js";import{n as E}from"./vue-i18n-BcfTCFFS.js";import{d as D,g as O,l as k,p as A}from"./index-D6wj_wQ9.js";import{t as j}from"./QSpinnerDots-CszPQQ9J.js";import{t as M}from"./QTooltip-fDNzBEfN.js";import{t as N}from"./QExpansionItem-CH1ipL9n.js";import{n as ee,r as te,t as P}from"./render-chat-markdown-Bqq2G-yI.js";import{n as F}from"./purify.es-CPieV82n.js";import{t as I}from"./documents-kx0vLfSG.js";import{t as L}from"./_plugin-vue_export-helper-Cj6tcsj6.js";function ne(e,t,n=!0){let r=[],i=new Map,a=new Map;for(let n=0;n<e.length;n++){let o=e[n],s=t?.[n];switch(o.kind){case`message:text`:{let e=i.get(o.messageId);if(e)e.text+=o.text,e.streaming=o.streaming;else{let e={type:`text`,messageId:o.messageId,text:o.text,streaming:o.streaming,ts:s};i.set(o.messageId,e),r.push(e)}break}case`message:end`:{let e=i.get(o.messageId);e&&(e.streaming=!1);break}case`message:thinking`:r.push({type:`thinking`,messageId:o.messageId,text:o.text,ts:s});break;case`tool:call`:{let e={type:`tool`,toolCallId:o.toolCallId,name:o.name,input:o.input,ts:s};a.set(o.toolCallId,e),r.push(e);break}case`tool:result`:{let e=a.get(o.toolCallId);e&&(e.result={output:o.output,isError:o.isError});break}case`session:started`:r.push({type:`session`,kind:`started`,detail:{engineSessionId:o.engineSessionId,model:o.model},ts:s});break;case`session:ended`:r.push({type:`session`,kind:`ended`,detail:{reason:o.reason,exitCode:o.exitCode},ts:s});break;case`session:compacted`:r.push({type:`session`,kind:`compacted`,ts:s});break;case`session:brainstorm-complete`:case`session:user-input-requested`:case`message:raw`:case`skills:discovered`:case`usage`:case`rate_limit`:case`subagent:progress`:case`error`:break;default:}}let o=null;for(let e of r)e.type===`text`&&e.streaming&&(o&&(o.streaming=!1),o=e);return o&&!n&&(o.streaming=!1),r}function re(e,t){if(t.length===0)return e;let n=t.map(e=>({type:`user`,content:e.content,sender:e.sender,ts:e.ts})),r=[...e,...n];r.sort((e,t)=>{let n=e.ts??``,r=t.ts??``;return n===r?0:n?r?n<r?-1:1:-1:1});let i;for(let e of r)e.type===`user`&&e.sender!==`system-prompt`&&e.ts&&(!i||e.ts>i)&&(i=e.ts);if(i)for(let e of r)e.type===`text`&&e.streaming&&(!e.ts||e.ts<i)&&(e.streaming=!1);return r}function R(e){switch(e.type){case`user`:return e.sender===`system-prompt`?`system-prompt`:`user`;case`session`:return`session`;default:return`agent`}}function ie(e){let t=[],n=null;for(let r of e){let e=R(r),i=e===`session`||e===`system-prompt`;!n||n.speaker!==e||i?(n={speaker:e,ts:r.ts,items:[r]},t.push(n),i&&(n=null)):n.items.push(r)}return t}var z={class:`text-caption text-grey-6`},B=v({__name:`SessionEventItem`,props:{item:{}},setup(e){let n=e,r=p(()=>{switch(n.item.kind){case`started`:return`session.started`;case`ended`:return`session.ended`;case`compacted`:return`session.compacted`;default:return`session.started`}});return(e,n)=>(t(),m(`span`,z,c(e.$t(r.value)),1))}});function V(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){ae(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 ae(e,t,n){let r=e.textContent??``;if(!t.some(e=>r.includes(e)))return;let i=n.createDocumentFragment(),a=0;for(;a<r.length;){let e=H(r,a,t);if(!e){i.appendChild(n.createTextNode(r.slice(a)));break}e.index>a&&i.appendChild(n.createTextNode(r.slice(a,e.index)));let o=n.createElement(`a`);o.className=`document-link`,o.setAttribute(`data-document-path`,e.path),o.setAttribute(`href`,`#`),o.textContent=e.path,i.appendChild(o),a=e.index+e.path.length}e.parentNode?.replaceChild(i,e)}function H(e,t,n){let r=null;for(let i of n){let n=e.indexOf(i,t);n<0||(!r||n<r.index||n===r.index&&i.length>r.path.length)&&(r={index:n,path:i})}return r}var U=[`innerHTML`],W=L(v({__name:`TextMessageItem`,props:{item:{}},setup(e){let n=e,r=I(),i=k(),a=p(()=>{let e=i.selectedWorkspaceId;return e?r.documentsFor(e).map(e=>e.path):[]}),o=p(()=>ee(V(F.parse(n.item.text,{async:!1,breaks:!0,gfm:!0}),a.value),{addAttr:[`data-document-path`]}));function s(e){let t=e.target?.closest(`.document-link`);if(!t)return;e.preventDefault();let n=t.getAttribute(`data-document-path`),a=i.selectedWorkspaceId;!n||!a||r.openDocumentByPath(a,n)}return(n,r)=>(t(),m(`div`,{class:`markdown-message`,onClick:s},[_(`div`,{innerHTML:o.value},null,8,U),e.item.streaming?(t(),l(x,{key:0,size:`xs`,class:`q-ml-xs`})):u(``,!0)]))}}),[[`__scopeId`,`data-v-1b7bd8ca`]]),oe={key:0,class:`text-caption text-grey-5`,style:{"font-style":`italic`}},G=[`innerHTML`],K={key:1,style:{"white-space":`pre-wrap`}},se=L(v({__name:`ThinkingItem`,props:{item:{}},setup(e){let n=e,r=p(()=>n.item.text.trim().slice(0,100)),i=p(()=>n.item.text.trim().length>0),a=p(()=>n.item.text.trim().length>100),s=p(()=>P(n.item.text));return(n,d)=>i.value?(t(),m(`div`,oe,[a.value?(t(),l(N,{key:0,dense:``,"dense-toggle":``,label:r.value,"header-class":`text-grey-5 text-caption`,style:{"font-style":`italic`}},{default:o(()=>[_(`div`,{class:`q-py-xs markdown-thinking`,innerHTML:s.value},null,8,G)]),_:1},8,[`label`])):(t(),m(`span`,K,c(e.item.text),1))])):u(``,!0)}}),[[`__scopeId`,`data-v-7f45ed94`]]);function ce(e,t){let n=e.split(`
1
+ import{E as e,F as t,H as n,L as r,M as i,Q as a,U as o,_t as s,bt as c,d as l,f as u,g as d,h as f,l as p,p as m,r as h,rt as g,u as _,v,yt as y}from"./runtime-core.esm-bundler-9Z0QAO_7.js";import{U as b,l as x,t as S}from"./QIcon-qfJNZLIW.js";import{c as C,s as w}from"./notifications-BPnKFW60.js";import{t as T}from"./QBtn-DEuWKHbR.js";import{n as E}from"./vue-i18n-DI-gS-CC.js";import{d as D,g as O,l as k,p as A}from"./index-CdHDdk1y.js";import{t as j}from"./QSpinnerDots-Bfl2RMy4.js";import{t as M}from"./QTooltip-DUGPNNeQ.js";import{t as N}from"./QExpansionItem-BGg74no1.js";import{n as ee,r as te,t as P}from"./render-chat-markdown-Bjcei0vn.js";import{n as F}from"./purify.es-BCEwTYRx.js";import{t as I}from"./documents-soWtna0O.js";import{t as L}from"./_plugin-vue_export-helper-Cj6tcsj6.js";function ne(e,t,n=!0){let r=[],i=new Map,a=new Map;for(let n=0;n<e.length;n++){let o=e[n],s=t?.[n];switch(o.kind){case`message:text`:{let e=i.get(o.messageId);if(e)e.text+=o.text,e.streaming=o.streaming;else{let e={type:`text`,messageId:o.messageId,text:o.text,streaming:o.streaming,ts:s};i.set(o.messageId,e),r.push(e)}break}case`message:end`:{let e=i.get(o.messageId);e&&(e.streaming=!1);break}case`message:thinking`:r.push({type:`thinking`,messageId:o.messageId,text:o.text,ts:s});break;case`tool:call`:{let e={type:`tool`,toolCallId:o.toolCallId,name:o.name,input:o.input,ts:s};a.set(o.toolCallId,e),r.push(e);break}case`tool:result`:{let e=a.get(o.toolCallId);e&&(e.result={output:o.output,isError:o.isError});break}case`session:started`:r.push({type:`session`,kind:`started`,detail:{engineSessionId:o.engineSessionId,model:o.model},ts:s});break;case`session:ended`:r.push({type:`session`,kind:`ended`,detail:{reason:o.reason,exitCode:o.exitCode},ts:s});break;case`session:compacted`:r.push({type:`session`,kind:`compacted`,ts:s});break;case`session:brainstorm-complete`:case`session:user-input-requested`:case`message:raw`:case`skills:discovered`:case`usage`:case`rate_limit`:case`subagent:progress`:case`error`:break;default:}}let o=null;for(let e of r)e.type===`text`&&e.streaming&&(o&&(o.streaming=!1),o=e);return o&&!n&&(o.streaming=!1),r}function re(e,t){if(t.length===0)return e;let n=t.map(e=>({type:`user`,content:e.content,sender:e.sender,ts:e.ts})),r=[...e,...n];r.sort((e,t)=>{let n=e.ts??``,r=t.ts??``;return n===r?0:n?r?n<r?-1:1:-1:1});let i;for(let e of r)e.type===`user`&&e.sender!==`system-prompt`&&e.ts&&(!i||e.ts>i)&&(i=e.ts);if(i)for(let e of r)e.type===`text`&&e.streaming&&(!e.ts||e.ts<i)&&(e.streaming=!1);return r}function R(e){switch(e.type){case`user`:return e.sender===`system-prompt`?`system-prompt`:`user`;case`session`:return`session`;default:return`agent`}}function ie(e){let t=[],n=null;for(let r of e){let e=R(r),i=e===`session`||e===`system-prompt`;!n||n.speaker!==e||i?(n={speaker:e,ts:r.ts,items:[r]},t.push(n),i&&(n=null)):n.items.push(r)}return t}var z={class:`text-caption text-grey-6`},B=v({__name:`SessionEventItem`,props:{item:{}},setup(e){let n=e,r=p(()=>{switch(n.item.kind){case`started`:return`session.started`;case`ended`:return`session.ended`;case`compacted`:return`session.compacted`;default:return`session.started`}});return(e,n)=>(t(),m(`span`,z,c(e.$t(r.value)),1))}});function V(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){ae(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 ae(e,t,n){let r=e.textContent??``;if(!t.some(e=>r.includes(e)))return;let i=n.createDocumentFragment(),a=0;for(;a<r.length;){let e=H(r,a,t);if(!e){i.appendChild(n.createTextNode(r.slice(a)));break}e.index>a&&i.appendChild(n.createTextNode(r.slice(a,e.index)));let o=n.createElement(`a`);o.className=`document-link`,o.setAttribute(`data-document-path`,e.path),o.setAttribute(`href`,`#`),o.textContent=e.path,i.appendChild(o),a=e.index+e.path.length}e.parentNode?.replaceChild(i,e)}function H(e,t,n){let r=null;for(let i of n){let n=e.indexOf(i,t);n<0||(!r||n<r.index||n===r.index&&i.length>r.path.length)&&(r={index:n,path:i})}return r}var U=[`innerHTML`],W=L(v({__name:`TextMessageItem`,props:{item:{}},setup(e){let n=e,r=I(),i=k(),a=p(()=>{let e=i.selectedWorkspaceId;return e?r.documentsFor(e).map(e=>e.path):[]}),o=p(()=>ee(V(F.parse(n.item.text,{async:!1,breaks:!0,gfm:!0}),a.value),{addAttr:[`data-document-path`]}));function s(e){let t=e.target?.closest(`.document-link`);if(!t)return;e.preventDefault();let n=t.getAttribute(`data-document-path`),a=i.selectedWorkspaceId;!n||!a||r.openDocumentByPath(a,n)}return(n,r)=>(t(),m(`div`,{class:`markdown-message`,onClick:s},[_(`div`,{innerHTML:o.value},null,8,U),e.item.streaming?(t(),l(x,{key:0,size:`xs`,class:`q-ml-xs`})):u(``,!0)]))}}),[[`__scopeId`,`data-v-1b7bd8ca`]]),oe={key:0,class:`text-caption text-grey-5`,style:{"font-style":`italic`}},G=[`innerHTML`],K={key:1,style:{"white-space":`pre-wrap`}},se=L(v({__name:`ThinkingItem`,props:{item:{}},setup(e){let n=e,r=p(()=>n.item.text.trim().slice(0,100)),i=p(()=>n.item.text.trim().length>0),a=p(()=>n.item.text.trim().length>100),s=p(()=>P(n.item.text));return(n,d)=>i.value?(t(),m(`div`,oe,[a.value?(t(),l(N,{key:0,dense:``,"dense-toggle":``,label:r.value,"header-class":`text-grey-5 text-caption`,style:{"font-style":`italic`}},{default:o(()=>[_(`div`,{class:`q-py-xs markdown-thinking`,innerHTML:s.value},null,8,G)]),_:1},8,[`label`])):(t(),m(`span`,K,c(e.item.text),1))])):u(``,!0)}}),[[`__scopeId`,`data-v-7f45ed94`]]);function ce(e,t){let n=e.split(`
2
2
  `),r=t.split(`
3
3
  `),i=n.length,a=r.length,o=Array.from({length:i+1},()=>Array(a+1).fill(0));for(let e=i-1;e>=0;e--)for(let t=a-1;t>=0;t--)n[e]===r[t]?o[e][t]=o[e+1][t+1]+1:o[e][t]=Math.max(o[e+1][t],o[e][t+1]);let s=[],c=0,l=0;for(;c<i&&l<a;)n[c]===r[l]?(s.push({type:`context`,content:n[c]}),c++,l++):o[c+1][l]>=o[c][l+1]?(s.push({type:`del`,content:n[c]}),c++):(s.push({type:`add`,content:r[l]}),l++);for(;c<i;)s.push({type:`del`,content:n[c++]});for(;l<a;)s.push({type:`add`,content:r[l++]});return s}function q(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??``;return{toolName:`Edit`,filePath:e,oldString:t,newString:r,replaceAll:n.replace_all??!1,additions:r?r.split(`
4
4
  `).length:0,deletions:t?t.split(`
@@ -1 +1 @@
1
- import{F as e,U as t,bt as n,d as r,f as i,g as a,h as o,l as s,rt as c,v as l}from"./runtime-core.esm-bundler-C3IgBgY5.js";import{n as u}from"./vue-i18n-BcfTCFFS.js";import{l as d}from"./index-D6wj_wQ9.js";import{t as f}from"./QTooltip-fDNzBEfN.js";import{t as p}from"./QChip-ByxK0Tuf.js";var m=l({__name:`AutoLoopChip`,props:{workspace:{default:null}},setup(l){let m=l,{t:h}=u(),g=d(),_=s(()=>m.workspace??g.selectedWorkspace),v=s(()=>_.value?.id??null),y=s(()=>v.value?g.autoLoopStates[v.value]??null:null),b=s(()=>!!y.value?.auto_loop),x=s(()=>!!y.value?.auto_loop_ready),S=s(()=>v.value!==null&&g.selectedWorkspaceId===v.value),C=s(()=>S.value?g.tasks.filter(e=>e.status===`done`).length:y.value?.tasks_done??0),w=s(()=>S.value?g.tasks.length:y.value?.tasks_total??0);return(s,l)=>b.value&&!x.value?(e(),r(p,{key:0,dense:``,square:``,size:`sm`,color:`indigo-4`,"text-color":`white`,icon:`hourglass_top`,class:`q-ml-sm`},{default:t(()=>[o(n(c(h)(`autoLoop.preparing`))+` `,1),a(f,null,{default:t(()=>[o(n(c(h)(`autoLoop.preparingTooltip`)),1)]),_:1})]),_:1})):b.value?(e(),r(p,{key:1,dense:``,square:``,size:`sm`,color:`amber-9`,"text-color":`white`,icon:`autorenew`,class:`q-ml-sm`},{default:t(()=>[o(n(c(h)(`autoLoop.progress`,{done:C.value,total:w.value})),1)]),_:1})):i(``,!0)}});export{m as t};
1
+ import{F as e,U as t,bt as n,d as r,f as i,g as a,h as o,l as s,rt as c,v as l}from"./runtime-core.esm-bundler-9Z0QAO_7.js";import{n as u}from"./vue-i18n-DI-gS-CC.js";import{l as d}from"./index-CdHDdk1y.js";import{t as f}from"./QTooltip-DUGPNNeQ.js";import{t as p}from"./QChip-erWIZgxW.js";var m=l({__name:`AutoLoopChip`,props:{workspace:{default:null}},setup(l){let m=l,{t:h}=u(),g=d(),_=s(()=>m.workspace??g.selectedWorkspace),v=s(()=>_.value?.id??null),y=s(()=>v.value?g.autoLoopStates[v.value]??null:null),b=s(()=>!!y.value?.auto_loop),x=s(()=>!!y.value?.auto_loop_ready),S=s(()=>v.value!==null&&g.selectedWorkspaceId===v.value),C=s(()=>S.value?g.tasks.filter(e=>e.status===`done`).length:y.value?.tasks_done??0),w=s(()=>S.value?g.tasks.length:y.value?.tasks_total??0);return(s,l)=>b.value&&!x.value?(e(),r(p,{key:0,dense:``,square:``,size:`sm`,color:`indigo-4`,"text-color":`white`,icon:`hourglass_top`,class:`q-ml-sm`},{default:t(()=>[o(n(c(h)(`autoLoop.preparing`))+` `,1),a(f,null,{default:t(()=>[o(n(c(h)(`autoLoop.preparingTooltip`)),1)]),_:1})]),_:1})):b.value?(e(),r(p,{key:1,dense:``,square:``,size:`sm`,color:`amber-9`,"text-color":`white`,icon:`autorenew`,class:`q-ml-sm`},{default:t(()=>[o(n(c(h)(`autoLoop.progress`,{done:C.value,total:w.value})),1)]),_:1})):i(``,!0)}});export{m as t};
@@ -1 +1 @@
1
- import{F as e,b as t}from"./QIcon-BJuyqdsT.js";import{C as n,w as r}from"./notifications-OnPq4FrH.js";function i(e){if(e===!1)return 0;if(e===!0||e===void 0)return 1;let t=parseInt(e,10);return isNaN(t)?0:t}var a=e({name:`close-popup`,beforeMount(e,{value:a}){let o={depth:i(a),handler(t){o.depth!==0&&setTimeout(()=>{let i=r(e);i!==void 0&&n(i,t,o.depth)})},handlerKey(e){t(e,13)===!0&&o.handler(e)}};e.__qclosepopup=o,e.addEventListener(`click`,o.handler),e.addEventListener(`keyup`,o.handlerKey)},updated(e,{value:t,oldValue:n}){t!==n&&(e.__qclosepopup.depth=i(t))},beforeUnmount(e){let t=e.__qclosepopup;e.removeEventListener(`click`,t.handler),e.removeEventListener(`keyup`,t.handlerKey),delete e.__qclosepopup}});export{a as t};
1
+ import{F as e,b as t}from"./QIcon-qfJNZLIW.js";import{C as n,w as r}from"./notifications-BPnKFW60.js";function i(e){if(e===!1)return 0;if(e===!0||e===void 0)return 1;let t=parseInt(e,10);return isNaN(t)?0:t}var a=e({name:`close-popup`,beforeMount(e,{value:a}){let o={depth:i(a),handler(t){o.depth!==0&&setTimeout(()=>{let i=r(e);i!==void 0&&n(i,t,o.depth)})},handlerKey(e){t(e,13)===!0&&o.handler(e)}};e.__qclosepopup=o,e.addEventListener(`click`,o.handler),e.addEventListener(`keyup`,o.handlerKey)},updated(e,{value:t,oldValue:n}){t!==n&&(e.__qclosepopup.depth=i(t))},beforeUnmount(e){let t=e.__qclosepopup;e.removeEventListener(`click`,t.handler),e.removeEventListener(`keyup`,t.handlerKey),delete e.__qclosepopup}});export{a as t};
@@ -0,0 +1 @@
1
+ .create-page[data-v-f1afc3a6]{background-color:#1a1a2e;min-height:100%;padding:48px 24px}.create-inner[data-v-f1afc3a6]{width:100%;max-width:700px}.create-title[data-v-f1afc3a6]{font-size:24px;line-height:1.3}.create-card[data-v-f1afc3a6]{background:#224;border:1px solid #444;overflow:hidden}.card-top-bar[data-v-f1afc3a6]{background:#1e1e3a;min-height:36px}.card-name-wrap[data-v-f1afc3a6]{background:#224;padding:8px 16px 4px}.card-name-wrap[data-v-f1afc3a6] .q-field__control{height:32px;min-height:32px;padding:0}.card-name-wrap[data-v-f1afc3a6] input{color:#e0e0e0;font-size:15px;font-weight:500}.card-name-wrap[data-v-f1afc3a6] input::placeholder{color:#555}.card-textarea-wrap[data-v-f1afc3a6]{background:#224;position:relative}.create-slash-popup[data-v-f1afc3a6]{z-index:9999;position:absolute;top:calc(100% + 4px);left:12px;right:12px}.repo-select[data-v-f1afc3a6]{min-width:160px;max-width:260px}.repo-select[data-v-f1afc3a6] .q-field__prepend{align-items:center;height:auto;padding-top:0}.create-textarea[data-v-f1afc3a6]{color:#d0d0d0;width:100%;padding:12px 16px 4px}.create-textarea[data-v-f1afc3a6] .q-field__control{padding:0}.create-textarea[data-v-f1afc3a6] textarea{color:#d0d0d0;resize:none;min-height:100px;font-size:14px;line-height:1.6}.create-textarea[data-v-f1afc3a6] textarea::placeholder{color:#666}.voice-btn--recording[data-v-f1afc3a6]{animation:1.1s ease-in-out infinite voice-pulse-f1afc3a6}@keyframes voice-pulse-f1afc3a6{0%{opacity:1;transform:scale(1)}50%{opacity:.86;transform:scale(1.06)}to{opacity:1;transform:scale(1)}}.notion-toggle-btn[data-v-f1afc3a6]{background:#333;padding:2px 10px}.notion-url-wrap[data-v-f1afc3a6]{background:#1e1e3a;padding:8px 0 0}.notion-url-input[data-v-f1afc3a6]{padding:0 12px}.notion-url-input[data-v-f1afc3a6] .q-field__control{height:36px;min-height:36px;padding:0}.notion-url-input[data-v-f1afc3a6] input{color:#d0d0d0;font-size:13px}.notion-url-input[data-v-f1afc3a6] input::placeholder{color:#555;font-size:12px}.notion-error[data-v-f1afc3a6],.notion-valid[data-v-f1afc3a6]{padding-bottom:6px}.notion-peek-choice[data-v-f1afc3a6]{padding-top:4px}.peek-card[data-v-f1afc3a6]{cursor:pointer;text-align:left;color:#e0e0e0;font-family:inherit;font-size:inherit;background:#ffffff08;border:1px solid #ffffff14;border-radius:8px;align-items:center;gap:10px;padding:10px 12px;transition:background .15s,border-color .15s,transform .1s;display:flex;position:relative}.peek-card[data-v-f1afc3a6]:hover{background:#ffffff0f;border-color:#6c63ff66}.peek-card[data-v-f1afc3a6]:active{transform:scale(.99)}.peek-card--active[data-v-f1afc3a6]{background:#6c63ff1f;border-color:#6c63ffd9;box-shadow:0 0 0 1px #6c63ff66}.peek-card--active .peek-card-icon[data-v-f1afc3a6]{color:#8a82ff}.peek-card--active .peek-card-title[data-v-f1afc3a6]{color:#fff}.peek-card-icon[data-v-f1afc3a6]{color:#999;flex-shrink:0}.peek-card-text[data-v-f1afc3a6]{flex:1;min-width:0;line-height:1.25}.peek-card-title[data-v-f1afc3a6]{color:#d0d0d0;font-size:12px;font-weight:600}.peek-card-desc[data-v-f1afc3a6]{color:#888;margin-top:2px;font-size:10.5px}.peek-card-check[data-v-f1afc3a6]{flex-shrink:0}.sentry-toggle-btn[data-v-f1afc3a6]{background:#333;padding:2px 10px}.sentry-url-wrap[data-v-f1afc3a6]{background:#1e1e3a;padding:8px 0 0}.sentry-url-input[data-v-f1afc3a6]{padding:0 12px}.sentry-url-input[data-v-f1afc3a6] .q-field__control{height:36px;min-height:36px;padding:0}.sentry-url-input[data-v-f1afc3a6] input{color:#d0d0d0;font-size:13px}.sentry-url-input[data-v-f1afc3a6] input::placeholder{color:#555;font-size:12px}.sentry-error[data-v-f1afc3a6],.sentry-valid[data-v-f1afc3a6]{padding-bottom:6px}.slide-enter-active[data-v-f1afc3a6],.slide-leave-active[data-v-f1afc3a6]{transition:all .2s;overflow:hidden}.slide-enter-from[data-v-f1afc3a6],.slide-leave-to[data-v-f1afc3a6]{opacity:0;max-height:0}.slide-enter-to[data-v-f1afc3a6],.slide-leave-from[data-v-f1afc3a6]{opacity:1;max-height:120px}.card-bottom-bar[data-v-f1afc3a6]{background:#1e1e3a}.skip-setup-btn[data-v-f1afc3a6]{min-height:28px;padding:2px 10px;font-size:11px}.skip-setup-btn[data-v-f1afc3a6] .q-btn__content{gap:4px}.skip-setup-btn[data-v-f1afc3a6] .q-icon{font-size:14px}.bottom-row-git .bottom-select-label[data-v-f1afc3a6]{text-overflow:ellipsis;white-space:nowrap;overflow:hidden}.bottom-row-git .bottom-select.repo-select[data-v-f1afc3a6] input{color:#bbb;padding:0 4px;font-size:11px}.bottom-row-git .bottom-select.repo-select[data-v-f1afc3a6] input::placeholder{color:#666;font-style:italic}.bottom-select[data-v-f1afc3a6]{background:#333;height:28px;padding:0 6px}.bottom-select[data-v-f1afc3a6] .q-field__control{height:28px;min-height:28px;padding:0}.bottom-select[data-v-f1afc3a6] .q-field__native{min-height:unset;padding:0}.bottom-select-label[data-v-f1afc3a6]{color:#bbb;gap:2px;font-size:11px}.bottom-sep[data-v-f1afc3a6]{color:#555;padding:0 2px;font-size:12px;line-height:1}.repo-path-wrap[data-v-f1afc3a6]{background:#333;border-radius:6px;height:28px;padding:0 8px}.repo-input[data-v-f1afc3a6]{min-width:140px}.repo-input[data-v-f1afc3a6] .q-field__control{height:28px;min-height:28px;padding:0}.repo-input[data-v-f1afc3a6] input{color:#bbb;font-size:11px}.repo-input[data-v-f1afc3a6] input::placeholder{color:#666;font-size:11px}.branch-select[data-v-f1afc3a6]{min-width:80px}.create-btn[data-v-f1afc3a6]{color:#fff;background:#4f46e5;min-width:220px;height:32px;padding:0 32px;font-size:13px}.create-btn[data-v-f1afc3a6] .q-btn__content{height:32px}.create-hint[data-v-f1afc3a6]{line-height:1.5}.fade-enter-active[data-v-f1afc3a6],.fade-leave-active[data-v-f1afc3a6]{transition:opacity .2s}.fade-enter-from[data-v-f1afc3a6],.fade-leave-to[data-v-f1afc3a6]{opacity:0}.manual-hint[data-v-f1afc3a6]{background:#1e1e3a;line-height:1.4}.manual-expansion[data-v-f1afc3a6]{background:#1e1e3a;border:1px solid #333;border-radius:4px;margin-top:6px;overflow:hidden}.manual-expansion[data-v-f1afc3a6] .manual-expansion-header{min-height:32px;padding:4px 10px;font-size:12px}.manual-expansion[data-v-f1afc3a6] .q-expansion-item__content,.manual-section-body[data-v-f1afc3a6]{background:#1a1a2e}.manual-input[data-v-f1afc3a6] .q-field__control{height:26px;min-height:26px;padding:0}.manual-input[data-v-f1afc3a6] input{color:#e0e0e0;font-size:12px}.manual-input[data-v-f1afc3a6] input::placeholder{color:#555}.manual-item[data-v-f1afc3a6]{border-top:1px solid #ffffff0a}.manual-item[data-v-f1afc3a6]:first-child{border-top:none}