@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.
- package/README.md +84 -0
- package/dist/server/index.js +2 -0
- package/dist/server/routes/voice.js +149 -0
- package/dist/server/services/settings-service.js +57 -0
- package/dist/server/services/transcription-service.js +206 -0
- package/package.json +12 -10
- package/src/client/dist/spa/assets/{ActivityFeed-BboSPm4b.js → ActivityFeed-DlPVoOGb.js} +1 -1
- package/src/client/dist/spa/assets/{AutoLoopChip-w8D77bI5.js → AutoLoopChip-CkSzkC0C.js} +1 -1
- package/src/client/dist/spa/assets/{ClosePopup-D_UAdwkA.js → ClosePopup-DTcbxsC0.js} +1 -1
- package/src/client/dist/spa/assets/CreatePage-BoRappO3.css +1 -0
- package/src/client/dist/spa/assets/CreatePage-DpCVNwYk.js +2 -0
- package/src/client/dist/spa/assets/{DiffViewer-CblFgn8w.js → DiffViewer-D-uNbBq0.js} +3 -3
- package/src/client/dist/spa/assets/{HealthPage-CBSw7e5q.js → HealthPage-xZ0PP4F-.js} +1 -1
- package/src/client/dist/spa/assets/{MainLayout-DhaYycak.js → MainLayout-DdkKM2ba.js} +2 -2
- package/src/client/dist/spa/assets/{QBadge-DWH42dbo.js → QBadge-C7r6oPSi.js} +1 -1
- package/src/client/dist/spa/assets/{QBtn-a6jxWjmW.js → QBtn-DEuWKHbR.js} +1 -1
- package/src/client/dist/spa/assets/{QCheckbox-D5jfsxLV.js → QCheckbox-BvHfXBFY.js} +1 -1
- package/src/client/dist/spa/assets/{QChip-ByxK0Tuf.js → QChip-erWIZgxW.js} +1 -1
- package/src/client/dist/spa/assets/{QExpansionItem-CH1ipL9n.js → QExpansionItem-BGg74no1.js} +1 -1
- package/src/client/dist/spa/assets/QIcon-qfJNZLIW.js +1 -0
- package/src/client/dist/spa/assets/{QInput-Cm5-AGQ4.js → QInput-DCJEwE8V.js} +1 -1
- package/src/client/dist/spa/assets/{QItemLabel-DrTxqTqV.js → QItemLabel-CHkgkZVj.js} +1 -1
- package/src/client/dist/spa/assets/{QItemSection-5YpFpPDm.js → QItemSection-CQUDd0Vg.js} +1 -1
- package/src/client/dist/spa/assets/{QList-D0FtnQJI.js → QList-BbnN_oNX.js} +1 -1
- package/src/client/dist/spa/assets/{QMenu-B4xMxMGd.js → QMenu-D6uqosRg.js} +1 -1
- package/src/client/dist/spa/assets/{QPage-DFi3K093.js → QPage-Co2h9wd_.js} +1 -1
- package/src/client/dist/spa/assets/{QRadio-B3aKjCVu.js → QRadio-DJxOyOA3.js} +1 -1
- package/src/client/dist/spa/assets/QSpace-DKIph84L.js +1 -0
- package/src/client/dist/spa/assets/{QSpinnerDots-CszPQQ9J.js → QSpinnerDots-Bfl2RMy4.js} +1 -1
- package/src/client/dist/spa/assets/{QTabPanels-D2ks0UIA.js → QTabPanels-ClPY9y4T.js} +1 -1
- package/src/client/dist/spa/assets/{QToggle-1-N9qWq4.js → QToggle-DNOTC_3a.js} +1 -1
- package/src/client/dist/spa/assets/{QTooltip-fDNzBEfN.js → QTooltip-DUGPNNeQ.js} +1 -1
- package/src/client/dist/spa/assets/{SearchPage-cZTwP4Lf.js → SearchPage-C07dgzT9.js} +1 -1
- package/src/client/dist/spa/assets/SettingsPage-CLNmI0Rr.css +1 -0
- package/src/client/dist/spa/assets/SettingsPage-D0CZNqkA.js +9 -0
- package/src/client/dist/spa/assets/{TouchPan-DoE24Io3.js → TouchPan-DvVlszwO.js} +1 -1
- package/src/client/dist/spa/assets/WorkspacePage-CKeCLPi0.js +4 -0
- package/src/client/dist/spa/assets/WorkspacePage-CRIcsASQ.css +1 -0
- package/src/client/dist/spa/assets/{build-path-tree-B1Lvvqto.js → build-path-tree-CCMckvpr.js} +1 -1
- package/src/client/dist/spa/assets/{cssMode-BFLYiiEw.js → cssMode-D6XTTdwy.js} +1 -1
- package/src/client/dist/spa/assets/{documents-kx0vLfSG.js → documents-soWtna0O.js} +1 -1
- package/src/client/dist/spa/assets/{editor.api-2asmmhth.js → editor.api-6hDVHddO.js} +1 -1
- package/src/client/dist/spa/assets/{editor.main-ChCYZyez.js → editor.main-DsLU1RWu.js} +3 -3
- package/src/client/dist/spa/assets/{expand-template-CXQFkQOJ.js → expand-template-Crz1uiBt.js} +1 -1
- package/src/client/dist/spa/assets/{formatters-DCAQ6ANJ.js → formatters-guwb-rzl.js} +1 -1
- package/src/client/dist/spa/assets/{freemarker2-BaBL9E9G.js → freemarker2-Bn1f0t2U.js} +1 -1
- package/src/client/dist/spa/assets/{handlebars-BxDour4L.js → handlebars-O92Cbq66.js} +1 -1
- package/src/client/dist/spa/assets/{html-C6hnkfIL.js → html-Ck95BMBU.js} +1 -1
- package/src/client/dist/spa/assets/{htmlMode-9zT3-dmz.js → htmlMode-DDYhH2FJ.js} +1 -1
- package/src/client/dist/spa/assets/i18n-BLgknHpf.js +1 -0
- package/src/client/dist/spa/assets/index-CdHDdk1y.js +2 -0
- package/src/client/dist/spa/assets/{javascript-C3YjvKbE.js → javascript-Cy2ddqHg.js} +1 -1
- package/src/client/dist/spa/assets/{jsonMode-DcJDgMzf.js → jsonMode-BIfVcp5z.js} +1 -1
- package/src/client/dist/spa/assets/{liquid-CsT8SjJM.js → liquid-B287eegh.js} +1 -1
- package/src/client/dist/spa/assets/{mdx-CT3yVSyc.js → mdx-B8HSzGai.js} +1 -1
- package/src/client/dist/spa/assets/{models-BsjWUKqM.js → models-Bd_v3W7Q.js} +1 -1
- package/src/client/dist/spa/assets/{monaco.contribution-DKGNz1oQ.js → monaco.contribution-CofcHzEf.js} +2 -2
- package/src/client/dist/spa/assets/{notifications-OnPq4FrH.js → notifications-BPnKFW60.js} +1 -1
- package/src/client/dist/spa/assets/{purify.es-CPieV82n.js → purify.es-BCEwTYRx.js} +1 -1
- package/src/client/dist/spa/assets/{python-Ca5miKgj.js → python-csaKR6_U.js} +1 -1
- package/src/client/dist/spa/assets/{razor-7qzusGRc.js → razor-C2wEv-nX.js} +1 -1
- package/src/client/dist/spa/assets/{render-chat-markdown-Bqq2G-yI.js → render-chat-markdown-Bjcei0vn.js} +1 -1
- package/src/client/dist/spa/assets/runtime-core.esm-bundler-9Z0QAO_7.js +1 -0
- package/src/client/dist/spa/assets/{tsMode-BdvO8jZ2.js → tsMode-DGLVs57K.js} +1 -1
- package/src/client/dist/spa/assets/{typescript-BfVNzhgs.js → typescript-w0GWHzZ3.js} +1 -1
- package/src/client/dist/spa/assets/{use-checkbox-D7zmRxGI.js → use-checkbox-y_fOkYZN.js} +1 -1
- package/src/client/dist/spa/assets/{use-id-CuaR1RiE.js → use-id-_7wiRcgb.js} +1 -1
- package/src/client/dist/spa/assets/{use-panel-D-8nAQns.js → use-panel-CbJ44rqY.js} +1 -1
- package/src/client/dist/spa/assets/use-quasar-DQYS47mh.js +1 -0
- package/src/client/dist/spa/assets/{vue-i18n-BcfTCFFS.js → vue-i18n-DI-gS-CC.js} +1 -1
- package/src/client/dist/spa/assets/{xml-DGNXGqXL.js → xml-CTn-vnEd.js} +1 -1
- package/src/client/dist/spa/assets/{yaml-CtAtOyt5.js → yaml-CTyUSvLZ.js} +1 -1
- package/src/client/dist/spa/index.html +12 -12
- package/src/client/dist/spa/assets/CreatePage-BDObLDJc.js +0 -2
- package/src/client/dist/spa/assets/CreatePage-DssmsAsV.css +0 -1
- package/src/client/dist/spa/assets/QIcon-BJuyqdsT.js +0 -1
- package/src/client/dist/spa/assets/QSpace-CLtL3aPy.js +0 -1
- package/src/client/dist/spa/assets/SettingsPage-C1efO0VM.js +0 -1
- package/src/client/dist/spa/assets/SettingsPage-CMyeQ9_u.css +0 -1
- package/src/client/dist/spa/assets/WorkspacePage-3jcof896.js +0 -4
- package/src/client/dist/spa/assets/WorkspacePage-CCtIrBiR.css +0 -1
- package/src/client/dist/spa/assets/i18n-CLY0XI9-.js +0 -1
- package/src/client/dist/spa/assets/index-D6wj_wQ9.js +0 -2
- package/src/client/dist/spa/assets/runtime-core.esm-bundler-C3IgBgY5.js +0 -1
- 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.
|
package/dist/server/index.js
CHANGED
|
@@ -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.
|
|
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.
|
|
70
|
-
"@
|
|
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.
|
|
74
|
+
"better-sqlite3": "^12.9.0",
|
|
73
75
|
"cron-parser": "^5.5.0",
|
|
74
|
-
"hono": "^4.12.
|
|
75
|
-
"nanoid": "^5.1.
|
|
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.
|
|
84
|
+
"@types/node": "^25.6.2",
|
|
83
85
|
"@types/ws": "^8.18.1",
|
|
84
|
-
"@vitest/runner": "^4.1.
|
|
86
|
+
"@vitest/runner": "^4.1.5",
|
|
85
87
|
"concurrently": "^9.2.1",
|
|
86
88
|
"tsx": "^4.21.0",
|
|
87
|
-
"typescript": "^6.0.
|
|
88
|
-
"vitest": "^
|
|
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-
|
|
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-
|
|
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-
|
|
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}
|