@loicngr/kobo 1.11.26 → 1.11.28
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/AGENTS.md +19 -1
- package/CHANGELOG.md +10 -0
- package/dist/mcp-server/kobo-tasks-handlers.js +17 -1
- package/dist/server/db/migrations.js +34 -0
- package/dist/server/db/schema.js +3 -1
- package/dist/server/index.js +80 -23
- package/dist/server/middleware/host-check-middleware.js +80 -0
- package/dist/server/routes/changelog.js +51 -3
- package/dist/server/routes/notion.js +1 -1
- package/dist/server/routes/pull-requests.js +6 -2
- package/dist/server/routes/sentry.js +1 -1
- package/dist/server/routes/settings.js +23 -8
- package/dist/server/routes/usage.js +12 -0
- package/dist/server/routes/workspaces.js +240 -91
- package/dist/server/services/agent/engines/claude-code/engine.js +112 -15
- package/dist/server/services/agent/engines/codex/engine.js +18 -0
- package/dist/server/services/agent/engines/codex/server-requests.js +5 -20
- package/dist/server/services/agent/orchestrator.js +143 -34
- package/dist/server/services/agent/session-controller.js +28 -0
- package/dist/server/services/agent/streaming-batcher.js +1 -0
- package/dist/server/services/agent/turn-liveness.js +12 -1
- package/dist/server/services/auto-loop-service.js +65 -0
- package/dist/server/services/awaiting-user-reminder-service.js +115 -0
- package/dist/server/services/change-source-branch-service.js +1 -1
- package/dist/server/services/file-editor-service.js +2 -46
- package/dist/server/services/forge/bitbucket/provider.js +9 -8
- package/dist/server/services/forge/github/provider.js +4 -3
- package/dist/server/services/forge/gitlab/provider.js +4 -3
- package/dist/server/services/forge/parse-cli-json.js +14 -0
- package/dist/server/services/forge/resolve.js +29 -1
- package/dist/server/services/lifecycle-hook-service.js +114 -0
- package/dist/server/services/network-access-service.js +187 -6
- package/dist/server/services/pr-watcher-service.js +27 -1
- package/dist/server/services/settings-service.js +147 -4
- package/dist/server/services/usage/reliability.js +81 -0
- package/dist/server/services/wakeup-service.js +23 -0
- package/dist/server/services/websocket-service.js +31 -3
- package/dist/server/services/workspace-service.js +36 -4
- package/dist/server/services/worktree-purge-service.js +1 -1
- package/dist/server/utils/git-ops.js +159 -1
- package/dist/server/utils/paths.js +19 -0
- package/dist/server/utils/safe-path.js +21 -1
- package/dist/server/utils/script-runner.js +2 -1
- package/dist/server/utils/workspace-name.js +37 -0
- package/dist/shared/consts.js +22 -0
- package/dist/shared/models.js +4 -4
- package/package.json +3 -2
- package/src/client/dist/pwa/assets/{ActivityFeed-DUjY6lCm.css → ActivityFeed-C6XSxdJ0.css} +1 -1
- package/src/client/dist/pwa/assets/ActivityFeed-LDzg5XPx.js +8 -0
- package/src/client/dist/pwa/assets/ChangelogPage-CjiWI9Qa.js +1 -0
- package/src/client/dist/pwa/assets/ClosePopup-CK6wvsMy.js +1 -0
- package/src/client/dist/pwa/assets/ComparisonPanel-C0hI2D1O.css +1 -0
- package/src/client/dist/pwa/assets/ComparisonPanel-D6eEciwQ.js +1 -0
- package/src/client/dist/pwa/assets/CreatePage-DHBa7ZL5.js +2 -0
- package/src/client/dist/pwa/assets/CreatePage-DeKYK6uC.css +1 -0
- package/src/client/dist/pwa/assets/DashboardPage-BqgK4lAf.css +1 -0
- package/src/client/dist/pwa/assets/DashboardPage-Xev4suQ7.js +1 -0
- package/src/client/dist/pwa/assets/DiffViewer-D0BQ5Dip.js +8 -0
- package/src/client/dist/pwa/assets/DiffViewer-DnR79ycb.css +1 -0
- package/src/client/dist/pwa/assets/DocumentsPanel-7hMCYUTd.js +1 -0
- package/src/client/dist/pwa/assets/DrawerToggleButton-BASeszPg.js +1 -0
- package/src/client/dist/pwa/assets/GitPanel-D-rwHnFn.js +3 -0
- package/src/client/dist/pwa/assets/{GitPanel-BKrtkPrV.css → GitPanel-DVs5BCU-.css} +1 -1
- package/src/client/dist/pwa/assets/HealthPage-BKXqP1li.js +1 -0
- package/src/client/dist/pwa/assets/MainLayout-DnPtpWgP.js +2 -0
- package/src/client/dist/pwa/assets/MainLayout-ExB0xYHB.css +1 -0
- package/src/client/dist/pwa/assets/QBanner-CDiXUya7.js +1 -0
- package/src/client/dist/pwa/assets/QBtnDropdown-52YHuBbx.js +1 -0
- package/src/client/dist/pwa/assets/QChip-DpUYTCOJ.js +1 -0
- package/src/client/dist/pwa/assets/QItemSection-CEUi4Tz0.js +1 -0
- package/src/client/dist/pwa/assets/QMarkupTable-DcoQpjPu.js +1 -0
- package/src/client/dist/pwa/assets/SchedulePanel-C7NK1hIK.js +1 -0
- package/src/client/dist/pwa/assets/SearchPage-Cm8chltA.js +1 -0
- package/src/client/dist/pwa/assets/{SettingsPage-CDZhPV6A.css → SettingsPage-2vdOZu1C.css} +1 -1
- package/src/client/dist/pwa/assets/SettingsPage-B4xs1RUl.js +10 -0
- package/src/client/dist/pwa/assets/TerminalPanel-CSPk8QtK.js +18 -0
- package/src/client/dist/pwa/assets/{TerminalPanel-B6xJJs8p.css → TerminalPanel-DbVZk0vD.css} +1 -1
- package/src/client/dist/pwa/assets/TouchPan-CpO61wPy.js +1 -0
- package/src/client/dist/pwa/assets/WorkspaceAttentionLabels-CrLP_I5S.css +1 -0
- package/src/client/dist/pwa/assets/WorkspaceAttentionLabels-DZrbJ0vD.js +1 -0
- package/src/client/dist/pwa/assets/WorkspacePage-D0-FkRVJ.js +4 -0
- package/src/client/dist/pwa/assets/browser-Bg25_sDh.js +8 -0
- package/src/client/dist/pwa/assets/build-path-tree-DH4SSsEz.js +1 -0
- package/src/client/dist/pwa/assets/de-Bf_OsgDw.js +17 -0
- package/src/client/dist/pwa/assets/documents-CDZqd_g5.js +1 -0
- package/src/client/dist/pwa/assets/editor-BJU3Xb3h.css +1 -0
- package/src/client/dist/pwa/assets/editor.api-CMsYdZTX.js +413 -0
- package/src/client/dist/pwa/assets/{efforts-BPZzMyrc.js → efforts-Db4w0alg.js} +1 -1
- package/src/client/dist/pwa/assets/engineFeatures-BjtQR0vB.js +1 -0
- package/src/client/dist/pwa/assets/es-1ib0e9jm.js +17 -0
- package/src/client/dist/pwa/assets/expand-template-CTUNyJrv.css +1 -0
- package/src/client/dist/pwa/assets/expand-template-DY8ntqUb.js +1 -0
- package/src/client/dist/pwa/assets/format-C-9jMgWb.js +435 -0
- package/src/client/dist/pwa/assets/format-CX6Cv1cD.css +1 -0
- package/src/client/dist/pwa/assets/{formatters-D0dU6VcA.js → formatters-CQ90Icr4.js} +1 -1
- package/src/client/dist/pwa/assets/fr-nj0BrM3X.js +17 -0
- package/src/client/dist/pwa/assets/{freemarker2-BqVyQSGe.js → freemarker2-KxB1BtrL.js} +1 -1
- package/src/client/dist/pwa/assets/{handlebars-9K762gzW.js → handlebars-7TByd_Zc.js} +1 -1
- package/src/client/dist/pwa/assets/{html-CRmp4AC_.js → html-BoVZ_gre.js} +1 -1
- package/src/client/dist/pwa/assets/i18n-BubYp85n.js +1 -0
- package/src/client/dist/pwa/assets/{index-BpmxQlZi.css → index-6H3m6vH7.css} +1 -1
- package/src/client/dist/pwa/assets/index-Ck1_qrff.js +18 -0
- package/src/client/dist/pwa/assets/it-B9MEgvg1.js +17 -0
- package/src/client/dist/pwa/assets/{javascript-DB9a_ZGm.js → javascript-BLL8vPQy.js} +1 -1
- package/src/client/dist/pwa/assets/layout-CFuk4mdG.js +1 -0
- package/src/client/dist/pwa/assets/{liquid-C67ZTBj7.js → liquid-DzcT-R6k.js} +1 -1
- package/src/client/dist/pwa/assets/{mdx-C_K9rc9_.js → mdx-Bguo999w.js} +1 -1
- package/src/client/dist/pwa/assets/monaco-contributions-CM7FH-iZ.js +51 -0
- package/src/client/dist/pwa/assets/monaco-contributions-Cb0afGfW.css +1 -0
- package/src/client/dist/pwa/assets/monaco.contribution-Dkq0qtWA.js +2 -0
- package/src/client/dist/pwa/assets/network-auth-C0moem8_.js +1 -0
- package/src/client/dist/pwa/assets/{notify-theme-thUiJn9n.js → notify-theme-C7mNmzKx.js} +1 -1
- package/src/client/dist/pwa/assets/pending-diff-open-BaQ_SNEF.js +1 -0
- package/src/client/dist/pwa/assets/permissionModes-CJZsT9Fl.js +36 -0
- package/src/client/dist/pwa/assets/pinia-ClsyLh3i.js +1 -0
- package/src/client/dist/pwa/assets/{project-color-hgNWqp0x.js → project-color-54JZHVy2.js} +1 -1
- package/src/client/dist/pwa/assets/{python-D1CBO9Uf.js → python-D5RQNiPF.js} +1 -1
- package/src/client/dist/pwa/assets/{razor-BCiKSj6_.js → razor-CVwoDJol.js} +1 -1
- package/src/client/dist/pwa/assets/{render-chat-markdown-Cw1_NLqs.js → render-chat-markdown-NAmPVIQp.js} +1 -1
- package/src/client/dist/pwa/assets/rolldown-runtime-hePW80VL.js +1 -0
- package/src/client/dist/pwa/assets/{typescript-D-8UENST.js → typescript-qdgwClKe.js} +1 -1
- package/src/client/dist/pwa/assets/use-checkbox-3io6bTPe.js +1 -0
- package/src/client/dist/pwa/assets/use-onboarding-DbYF8ZX_.js +2 -0
- package/src/client/dist/pwa/assets/use-quasar-B4eIgSfZ.js +1 -0
- package/src/client/dist/pwa/assets/vue-i18n-DtajJy_8.js +3 -0
- package/src/client/dist/pwa/assets/{xml-BBPvJ9QP.js → xml-DNSfQSlk.js} +1 -1
- package/src/client/dist/pwa/assets/{yaml-D4q1p7HX.js → yaml-6gnX2NnP.js} +1 -1
- package/src/client/dist/pwa/index.html +6 -5
- package/src/client/dist/pwa/sw.js +1 -1
- package/src/mcp-server/kobo-tasks-handlers.ts +17 -1
- package/src/client/dist/pwa/assets/ActivityFeed-CGMEOMn5.js +0 -8
- package/src/client/dist/pwa/assets/ChangelogPage-rvISZfJb.js +0 -1
- package/src/client/dist/pwa/assets/ClosePopup-BK6mNWsy.js +0 -1
- package/src/client/dist/pwa/assets/CreatePage-BCuDTB_0.css +0 -1
- package/src/client/dist/pwa/assets/CreatePage-BptAgVvu.js +0 -2
- package/src/client/dist/pwa/assets/DiffViewer-ZrvQ0YWN.js +0 -8
- package/src/client/dist/pwa/assets/DiffViewer-ahWtFrRy.css +0 -1
- package/src/client/dist/pwa/assets/DocumentsPanel-DuWbZiID.js +0 -1
- package/src/client/dist/pwa/assets/DrawerToggleButton-CBdOJzVZ.js +0 -1
- package/src/client/dist/pwa/assets/GitPanel-BiIXQPfL.js +0 -3
- package/src/client/dist/pwa/assets/HealthPage-DTrmhh3X.js +0 -1
- package/src/client/dist/pwa/assets/MainLayout-PSmlvfJd.js +0 -2
- package/src/client/dist/pwa/assets/MainLayout-Vq1iUPjx.css +0 -1
- package/src/client/dist/pwa/assets/QBanner-BoAbhM1O.js +0 -1
- package/src/client/dist/pwa/assets/QBtnDropdown-Ckvd3IVR.js +0 -1
- package/src/client/dist/pwa/assets/QChip-BzB_GF4Z.js +0 -1
- package/src/client/dist/pwa/assets/QItemSection-5GBV9MR2.js +0 -1
- package/src/client/dist/pwa/assets/SchedulePanel-DJWMsAoh.js +0 -1
- package/src/client/dist/pwa/assets/SearchPage-BI8w9utc.js +0 -1
- package/src/client/dist/pwa/assets/SettingsPage-Dr8oSevA.js +0 -16
- package/src/client/dist/pwa/assets/TerminalPanel-DVC58DJb.js +0 -18
- package/src/client/dist/pwa/assets/TouchPan-pqksSni0.js +0 -1
- package/src/client/dist/pwa/assets/WorkspacePage-DRP9Ypmz.js +0 -4
- package/src/client/dist/pwa/assets/build-path-tree-CQ4zZqo0.js +0 -1
- package/src/client/dist/pwa/assets/css.worker-JKZQyld4.js +0 -93
- package/src/client/dist/pwa/assets/cssMode-MGWKgqj7.js +0 -1
- package/src/client/dist/pwa/assets/documents-B6eOZeFK.js +0 -1
- package/src/client/dist/pwa/assets/editor-vZnmUXVO.css +0 -1
- package/src/client/dist/pwa/assets/editor.api-Dq41Mm99.js +0 -847
- package/src/client/dist/pwa/assets/engineFeatures-Dd_dxoOS.js +0 -1
- package/src/client/dist/pwa/assets/expand-template-B4LAsXAF.css +0 -1
- package/src/client/dist/pwa/assets/expand-template-BfZqON3q.js +0 -1
- package/src/client/dist/pwa/assets/html.worker-BUr90sI4.js +0 -506
- package/src/client/dist/pwa/assets/htmlMode-CFxFF9iH.js +0 -1
- package/src/client/dist/pwa/assets/i18n-BsWCIFmf.js +0 -1
- package/src/client/dist/pwa/assets/index-xTf0VfFG.js +0 -82
- package/src/client/dist/pwa/assets/json.worker-BOODjG9U.js +0 -62
- package/src/client/dist/pwa/assets/jsonMode-JhLTE1-C.js +0 -7
- package/src/client/dist/pwa/assets/layout-nWIyHeJf.js +0 -1
- package/src/client/dist/pwa/assets/lspLanguageFeatures-BIDd3ml0.js +0 -4
- package/src/client/dist/pwa/assets/network-auth-BPxfeLcl.js +0 -1
- package/src/client/dist/pwa/assets/pending-diff-open-BdA8vrDx.js +0 -1
- package/src/client/dist/pwa/assets/permissionModes-D09rnNgv.js +0 -36
- package/src/client/dist/pwa/assets/ts.worker-C-oGa8JT.js +0 -67738
- package/src/client/dist/pwa/assets/tsMode-Db77HzVu.js +0 -11
- package/src/client/dist/pwa/assets/use-checkbox-z-Qs4ZHY.js +0 -1
- package/src/client/dist/pwa/assets/use-id-DlouqdB8.js +0 -1
- package/src/client/dist/pwa/assets/use-onboarding-BafrD9nc.js +0 -2
- package/src/client/dist/pwa/assets/vs-Bb18eQC8.js +0 -56
- package/src/client/dist/pwa/assets/vs-JJSFJJVJ.css +0 -1
- package/src/client/dist/pwa/assets/vue-i18n-C3W5i7h6.js +0 -3
- package/src/client/dist/pwa/assets/workers-CImko7IX.js +0 -1
|
@@ -20,6 +20,18 @@ function rowToPending(row) {
|
|
|
20
20
|
return null;
|
|
21
21
|
return { targetAt: row.target_at, reason: row.reason ?? undefined };
|
|
22
22
|
}
|
|
23
|
+
/**
|
|
24
|
+
* How many times a wakeup may be re-armed after `startAgent` failed before we
|
|
25
|
+
* conclude the failure is permanent. `startAgent` throws for good on an
|
|
26
|
+
* archived or purged workspace, and on a session id that no longer resolves;
|
|
27
|
+
* without a bound that is a 15 s loop writing to SQLite forever. Deferring
|
|
28
|
+
* because a session is *running* is a different case and stays unbounded — the
|
|
29
|
+
* wakeup is meant to land once that session ends, however long it takes.
|
|
30
|
+
*/
|
|
31
|
+
const MAX_FAILED_RETRIES = 5;
|
|
32
|
+
/** Consecutive `startAgent` failures per workspace. In memory on purpose: a
|
|
33
|
+
* restart is a legitimate fresh attempt. */
|
|
34
|
+
const failedRetries = new Map();
|
|
23
35
|
/** Keep a claimed wakeup durable while the session is still unavailable. */
|
|
24
36
|
function defer(workspaceId, row) {
|
|
25
37
|
const targetAt = new Date(Date.now() + ACTIVE_SESSION_RETRY_MS).toISOString();
|
|
@@ -59,6 +71,7 @@ export function cancel(workspaceId, reason) {
|
|
|
59
71
|
clearTimeout(existing);
|
|
60
72
|
timers.delete(workspaceId);
|
|
61
73
|
}
|
|
74
|
+
failedRetries.delete(workspaceId);
|
|
62
75
|
const db = getDb();
|
|
63
76
|
const result = db.prepare('DELETE FROM pending_wakeups WHERE workspace_id = ?').run(workspaceId);
|
|
64
77
|
if (result.changes > 0) {
|
|
@@ -156,12 +169,22 @@ function fire(workspaceId) {
|
|
|
156
169
|
const agentPermissionMode = stored === 'plan' || stored === 'strict' || stored === 'interactive' ? stored : 'bypass';
|
|
157
170
|
try {
|
|
158
171
|
orchestrator.startAgent(workspaceId, worktreePath, row.prompt, wsRow.model, true, agentPermissionMode, row.agent_session_id ?? undefined, wsRow.reasoning_effort);
|
|
172
|
+
failedRetries.delete(workspaceId);
|
|
159
173
|
db.prepare('DELETE FROM pending_wakeups WHERE workspace_id = ?').run(workspaceId);
|
|
160
174
|
emitEphemeral(workspaceId, 'wakeup:fired', {});
|
|
161
175
|
}
|
|
162
176
|
catch (err) {
|
|
163
177
|
const message = err instanceof Error ? err.message : String(err);
|
|
164
178
|
console.error(`[wakeup-service] startAgent at fire time failed for '${workspaceId}':`, message);
|
|
179
|
+
const attempts = (failedRetries.get(workspaceId) ?? 0) + 1;
|
|
180
|
+
if (attempts > MAX_FAILED_RETRIES) {
|
|
181
|
+
failedRetries.delete(workspaceId);
|
|
182
|
+
db.prepare('DELETE FROM pending_wakeups WHERE workspace_id = ?').run(workspaceId);
|
|
183
|
+
console.error(`[wakeup-service] giving up on the wakeup for '${workspaceId}' after ${MAX_FAILED_RETRIES} failed attempts`);
|
|
184
|
+
emitEphemeral(workspaceId, 'wakeup:skipped', { reason: message });
|
|
185
|
+
return;
|
|
186
|
+
}
|
|
187
|
+
failedRetries.set(workspaceId, attempts);
|
|
165
188
|
defer(workspaceId, row);
|
|
166
189
|
}
|
|
167
190
|
}
|
|
@@ -78,7 +78,18 @@ export function handleConnection(ws) {
|
|
|
78
78
|
case 'devserver:start':
|
|
79
79
|
case 'devserver:stop': {
|
|
80
80
|
if (messageHandler) {
|
|
81
|
-
|
|
81
|
+
// The handler does DB work before its own try/catch, so it can fail
|
|
82
|
+
// both ways: a rejection if it is async, a plain throw if it is not.
|
|
83
|
+
// The process treats either as fatal, which would turn one bad frame
|
|
84
|
+
// into "Kōbō died and took every running agent with it". Calling it
|
|
85
|
+
// inside the async wrapper funnels both into the same catch.
|
|
86
|
+
void (async () => messageHandler?.(type, payload))().catch((err) => {
|
|
87
|
+
console.error(`[ws] Message handler failed for '${type}':`, err);
|
|
88
|
+
// Every other branch of this switch answers on failure; a routed
|
|
89
|
+
// message that vanishes silently leaves the user waiting forever.
|
|
90
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
91
|
+
ws.send(JSON.stringify({ type: 'error', payload: { message: `Failed to handle '${type}': ${message}` } }));
|
|
92
|
+
});
|
|
82
93
|
}
|
|
83
94
|
break;
|
|
84
95
|
}
|
|
@@ -124,14 +135,31 @@ export function handleConnection(ws) {
|
|
|
124
135
|
* Persists the event to the ws_events table.
|
|
125
136
|
* Returns the event id.
|
|
126
137
|
*/
|
|
138
|
+
/**
|
|
139
|
+
* The insert is on the busiest write path in the process: one row per agent
|
|
140
|
+
* event, hundreds per turn once token deltas are batched. Compiling the
|
|
141
|
+
* statement once and reusing it is better-sqlite3's documented fast path.
|
|
142
|
+
* Keyed on the database handle so a `resetDb()` between tests cannot hand back
|
|
143
|
+
* a statement bound to a closed connection.
|
|
144
|
+
*/
|
|
145
|
+
let cachedInsert = null;
|
|
146
|
+
function insertEventStatement() {
|
|
147
|
+
const db = getDb();
|
|
148
|
+
if (!cachedInsert || cachedInsert.db !== db) {
|
|
149
|
+
cachedInsert = {
|
|
150
|
+
db,
|
|
151
|
+
statement: db.prepare('INSERT INTO ws_events (id, workspace_id, type, payload, session_id, created_at) VALUES (?, ?, ?, ?, ?, ?)'),
|
|
152
|
+
};
|
|
153
|
+
}
|
|
154
|
+
return cachedInsert.statement;
|
|
155
|
+
}
|
|
127
156
|
export function emit(workspaceId, type, payload, sessionId) {
|
|
128
157
|
const id = nanoid();
|
|
129
158
|
const createdAt = new Date().toISOString();
|
|
130
159
|
let replayable = false;
|
|
131
160
|
// Best-effort persist — don't let FK violation (deleted workspace) break the broadcast
|
|
132
161
|
try {
|
|
133
|
-
|
|
134
|
-
db.prepare('INSERT INTO ws_events (id, workspace_id, type, payload, session_id, created_at) VALUES (?, ?, ?, ?, ?, ?)').run(id, workspaceId, type, JSON.stringify(payload), sessionId ?? null, createdAt);
|
|
162
|
+
insertEventStatement().run(id, workspaceId, type, JSON.stringify(payload), sessionId ?? null, createdAt);
|
|
135
163
|
replayable = true;
|
|
136
164
|
}
|
|
137
165
|
catch (err) {
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { nanoid } from 'nanoid';
|
|
2
2
|
import { getDb } from '../db/index.js';
|
|
3
|
+
import { isValidBranchName } from '../utils/git-ops.js';
|
|
3
4
|
import { resolveWorkspaceWorktreePath } from '../utils/worktree-paths.js';
|
|
4
5
|
import * as orchestrator from './agent/orchestrator.js';
|
|
5
6
|
import * as autoLoopService from './auto-loop-service.js';
|
|
@@ -26,7 +27,11 @@ const VALID_TRANSITIONS = {
|
|
|
26
27
|
// session can hit an interactive tool-approval/question before its status
|
|
27
28
|
// flips to `executing` — the same reasoning as the entering-quota comment
|
|
28
29
|
// above, just for the other direction.
|
|
29
|
-
|
|
30
|
+
// `error` is the escape hatch when the retry machinery itself fails: the
|
|
31
|
+
// status is moved to `quota` before the backoff is armed, so a failure in
|
|
32
|
+
// between would otherwise strand the workspace there — no timer, and no
|
|
33
|
+
// banner either, since QuotaBackoffBanner needs a backoff row to render.
|
|
34
|
+
quota: ['idle', 'executing', 'awaiting-user', 'error'],
|
|
30
35
|
};
|
|
31
36
|
// NOTE: `engine` is stored as a plain TEXT column and returned as a `string` on
|
|
32
37
|
// the `Workspace` interface rather than the stricter `EngineId` union. The DB
|
|
@@ -73,6 +78,7 @@ function mapWorkspace(row) {
|
|
|
73
78
|
prCiFailureDismissedAt: row.pr_ci_failure_dismissed_at,
|
|
74
79
|
worktreePurgedAt: row.worktree_purged_at,
|
|
75
80
|
worktreePurgeRestoreData: row.worktree_purge_restore_data,
|
|
81
|
+
comparisonId: row.comparison_id ?? null,
|
|
76
82
|
engine: row.engine ?? 'claude-code',
|
|
77
83
|
autoLoop: row.auto_loop === 1,
|
|
78
84
|
autoLoopReady: row.auto_loop_ready === 1,
|
|
@@ -125,9 +131,12 @@ export function createWorkspace(data) {
|
|
|
125
131
|
INSERT INTO workspaces (
|
|
126
132
|
id, name, project_path, source_branch, working_branch, status,
|
|
127
133
|
notion_url, notion_page_id, sentry_url, pr_url, worktree_path, worktree_owned,
|
|
128
|
-
model, brainstorm_model, reasoning_effort, permission_mode, permission_profile, agent_permission_mode, engine, created_at, updated_at
|
|
129
|
-
) VALUES (?, ?, ?, ?, ?, 'created', ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
|
130
|
-
`).run(id, data.name, data.projectPath, data.sourceBranch, data.workingBranch, data.notionUrl ?? null, data.notionPageId ?? null, data.sentryUrl ?? null, data.prUrl ?? null, computedWorktreePath, owned ? 1 : 0, data.model ?? 'claude-opus-4-8', data.brainstormModel ?? null, data.reasoningEffort ?? 'auto', legacyMode, legacyProfile, unifiedMode, data.engine ?? 'claude-code',
|
|
134
|
+
model, brainstorm_model, reasoning_effort, permission_mode, permission_profile, agent_permission_mode, engine, comparison_id, created_at, updated_at
|
|
135
|
+
) VALUES (?, ?, ?, ?, ?, 'created', ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
|
136
|
+
`).run(id, data.name, data.projectPath, data.sourceBranch, data.workingBranch, data.notionUrl ?? null, data.notionPageId ?? null, data.sentryUrl ?? null, data.prUrl ?? null, computedWorktreePath, owned ? 1 : 0, data.model ?? 'claude-opus-4-8', data.brainstormModel ?? null, data.reasoningEffort ?? 'auto', legacyMode, legacyProfile, unifiedMode, data.engine ?? 'claude-code',
|
|
137
|
+
// Empty string is not a group: it would silently gather every workspace
|
|
138
|
+
// created with a blank id into one comparison.
|
|
139
|
+
data.comparisonId?.trim() ? data.comparisonId : null, now, now);
|
|
131
140
|
return getWorkspace(id);
|
|
132
141
|
}
|
|
133
142
|
/** Fetch a single workspace by ID, or null if not found. */
|
|
@@ -136,6 +145,21 @@ export function getWorkspace(id) {
|
|
|
136
145
|
const row = db.prepare('SELECT * FROM workspaces WHERE id = ?').get(id);
|
|
137
146
|
return row ? mapWorkspace(row) : null;
|
|
138
147
|
}
|
|
148
|
+
/**
|
|
149
|
+
* Every workspace sharing a comparison id, oldest first — the order they were
|
|
150
|
+
* created in, which is the order the engines were picked.
|
|
151
|
+
*
|
|
152
|
+
* An empty or missing id matches nothing: `comparison_id` is NULL for the vast
|
|
153
|
+
* majority of workspaces, and returning them all here would be a footgun.
|
|
154
|
+
*/
|
|
155
|
+
export function listComparisonMembers(comparisonId) {
|
|
156
|
+
if (!comparisonId.trim())
|
|
157
|
+
return [];
|
|
158
|
+
const rows = getDb()
|
|
159
|
+
.prepare('SELECT * FROM workspaces WHERE comparison_id = ? ORDER BY created_at, id')
|
|
160
|
+
.all(comparisonId);
|
|
161
|
+
return rows.map(mapWorkspace);
|
|
162
|
+
}
|
|
139
163
|
/** List all workspaces, optionally including archived ones. Ordered by most recently updated. */
|
|
140
164
|
export function listWorkspaces(includeArchived = false) {
|
|
141
165
|
const db = getDb();
|
|
@@ -275,6 +299,14 @@ export function updateWorkspaceSourceBranch(id, sourceBranch) {
|
|
|
275
299
|
if (!sanitized) {
|
|
276
300
|
throw new Error('Source branch cannot be empty');
|
|
277
301
|
}
|
|
302
|
+
// The stored value reaches `git fetch origin <sourceBranch>` as a bare
|
|
303
|
+
// argument, from paths as ordinary as opening the Diff tab, so a name git
|
|
304
|
+
// would read as an option must never land here. Guarding the writer rather
|
|
305
|
+
// than each route covers the pr-watcher too, which takes the base the forge
|
|
306
|
+
// reports — and a leading dash is a perfectly valid ref name upstream.
|
|
307
|
+
if (!isValidBranchName(sanitized)) {
|
|
308
|
+
throw new Error(`Invalid source branch name: ${sanitized}`);
|
|
309
|
+
}
|
|
278
310
|
const db = getDb();
|
|
279
311
|
const now = new Date().toISOString();
|
|
280
312
|
const result = db
|
|
@@ -20,7 +20,7 @@ export async function purgeWorktree(workspaceId) {
|
|
|
20
20
|
// `removeWorktree` runs later in this same function — the agent must be dead
|
|
21
21
|
// by then, not merely asked to stop.
|
|
22
22
|
try {
|
|
23
|
-
await agentManager.stopAgentAndWait(workspaceId);
|
|
23
|
+
await agentManager.stopAgentAndWait(workspaceId, undefined, 'purge');
|
|
24
24
|
}
|
|
25
25
|
catch (err) {
|
|
26
26
|
const msg = err instanceof Error ? err.message : String(err);
|
|
@@ -67,6 +67,28 @@ async function gitAsync(repoPath, args, timeout = READ_ONLY_GIT_TIMEOUT_MS) {
|
|
|
67
67
|
});
|
|
68
68
|
return stdout.trimEnd();
|
|
69
69
|
}
|
|
70
|
+
/**
|
|
71
|
+
* Async counterpart of `git()` for the operations that talk to a remote.
|
|
72
|
+
*
|
|
73
|
+
* Same 60 s budget and, crucially, the same non-interactive environment: a
|
|
74
|
+
* credential or passphrase prompt on a background process would hang for the
|
|
75
|
+
* whole timeout with nobody to answer it. `gitAsync` above omits that env
|
|
76
|
+
* because it was written for local read-only commands.
|
|
77
|
+
*
|
|
78
|
+
* These run on HTTP handlers, and `execFileSync` there blocks the single
|
|
79
|
+
* thread that also serves the WebSocket streams and the agent events: one push
|
|
80
|
+
* over a slow link froze the entire app for up to a minute, heartbeat included.
|
|
81
|
+
*/
|
|
82
|
+
async function gitNetworkAsync(repoPath, args) {
|
|
83
|
+
const { stdout } = await execFileAsync('git', args, {
|
|
84
|
+
cwd: repoPath,
|
|
85
|
+
encoding: 'utf-8',
|
|
86
|
+
timeout: SYNC_GIT_TIMEOUT_MS,
|
|
87
|
+
maxBuffer: GIT_MAX_BUFFER_BYTES,
|
|
88
|
+
env: NON_INTERACTIVE_GIT_ENV,
|
|
89
|
+
});
|
|
90
|
+
return stdout.trimEnd();
|
|
91
|
+
}
|
|
70
92
|
/** Raised when a stale `index.lock` blocks every write on a worktree. */
|
|
71
93
|
export class GitIndexLockError extends Error {
|
|
72
94
|
lockPath;
|
|
@@ -385,6 +407,114 @@ export function mergeBranch(repoPath, baseBranch, opts) {
|
|
|
385
407
|
throw new Error(`Merge of 'origin/${baseBranch}' failed: ${message}`);
|
|
386
408
|
}
|
|
387
409
|
}
|
|
410
|
+
/**
|
|
411
|
+
* Async variants of the network operations, for the HTTP handlers.
|
|
412
|
+
*
|
|
413
|
+
* They mirror their synchronous twins one for one — same arguments, same
|
|
414
|
+
* errors, same conflict and dirty-tree detection — so a caller only swaps the
|
|
415
|
+
* name and adds `await`. Only the remote round-trip is async: the error paths
|
|
416
|
+
* below read local state (conflicted files, ongoing operation, working tree),
|
|
417
|
+
* which is fast and stays synchronous.
|
|
418
|
+
*/
|
|
419
|
+
export async function pushBranchAsync(repoPath, branchName, options = {}) {
|
|
420
|
+
const remote = options.remote ?? 'origin';
|
|
421
|
+
const args = ['push', '-u'];
|
|
422
|
+
if (options.force)
|
|
423
|
+
args.push('--force-with-lease');
|
|
424
|
+
args.push(remote, branchName);
|
|
425
|
+
try {
|
|
426
|
+
await gitNetworkAsync(repoPath, args);
|
|
427
|
+
}
|
|
428
|
+
catch (err) {
|
|
429
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
430
|
+
throw new Error(`Failed to push branch '${branchName}' to '${remote}': ${message}`);
|
|
431
|
+
}
|
|
432
|
+
}
|
|
433
|
+
export async function fetchAllBranchesAsync(repoPath, remote = 'origin') {
|
|
434
|
+
try {
|
|
435
|
+
await gitNetworkAsync(repoPath, ['fetch', remote]);
|
|
436
|
+
}
|
|
437
|
+
catch (err) {
|
|
438
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
439
|
+
throw new Error(`Failed to fetch from '${remote}': ${message}`);
|
|
440
|
+
}
|
|
441
|
+
}
|
|
442
|
+
export async function pullBranchAsync(repoPath, branchName, remote = 'origin', opts) {
|
|
443
|
+
if (!opts?.autostash) {
|
|
444
|
+
const status = getWorkingTreeStatus(repoPath);
|
|
445
|
+
if (status.staged > 0 || status.modified > 0) {
|
|
446
|
+
throw new DirtyWorktreeError('pull', status);
|
|
447
|
+
}
|
|
448
|
+
}
|
|
449
|
+
try {
|
|
450
|
+
const args = ['pull', '--ff-only'];
|
|
451
|
+
if (opts?.autostash)
|
|
452
|
+
args.push('--autostash');
|
|
453
|
+
args.push(remote, branchName);
|
|
454
|
+
await gitNetworkAsync(repoPath, args);
|
|
455
|
+
}
|
|
456
|
+
catch (err) {
|
|
457
|
+
if (err instanceof DirtyWorktreeError)
|
|
458
|
+
throw err;
|
|
459
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
460
|
+
throw new Error(`Failed to pull branch '${branchName}' from '${remote}': ${message}`);
|
|
461
|
+
}
|
|
462
|
+
}
|
|
463
|
+
export async function rebaseBranchAsync(repoPath, baseBranch, opts) {
|
|
464
|
+
try {
|
|
465
|
+
await gitNetworkAsync(repoPath, ['fetch', 'origin', baseBranch]);
|
|
466
|
+
}
|
|
467
|
+
catch {
|
|
468
|
+
// fetch may fail if offline — continue with local ref
|
|
469
|
+
}
|
|
470
|
+
try {
|
|
471
|
+
const args = ['rebase'];
|
|
472
|
+
if (opts?.autostash)
|
|
473
|
+
args.push('--autostash');
|
|
474
|
+
args.push(`origin/${baseBranch}`);
|
|
475
|
+
await gitNetworkAsync(repoPath, args);
|
|
476
|
+
}
|
|
477
|
+
catch (err) {
|
|
478
|
+
const conflicted = getConflictedFiles(repoPath);
|
|
479
|
+
if (conflicted.length > 0 || getOngoingGitOperation(repoPath) === 'rebase') {
|
|
480
|
+
// Leave the rebase in progress so the caller can abort or request agent-assisted resolution.
|
|
481
|
+
throw new GitConflictError('rebase', conflicted);
|
|
482
|
+
}
|
|
483
|
+
const status = getWorkingTreeStatus(repoPath);
|
|
484
|
+
if (status.staged > 0 || status.modified > 0) {
|
|
485
|
+
throw new DirtyWorktreeError('rebase', status);
|
|
486
|
+
}
|
|
487
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
488
|
+
throw new Error(`Rebase onto '${baseBranch}' failed: ${message}`);
|
|
489
|
+
}
|
|
490
|
+
}
|
|
491
|
+
export async function mergeBranchAsync(repoPath, baseBranch, opts) {
|
|
492
|
+
try {
|
|
493
|
+
await gitNetworkAsync(repoPath, ['fetch', 'origin', baseBranch]);
|
|
494
|
+
}
|
|
495
|
+
catch {
|
|
496
|
+
// offline — continue with local ref
|
|
497
|
+
}
|
|
498
|
+
try {
|
|
499
|
+
const args = ['merge', '--no-ff', '--no-edit'];
|
|
500
|
+
if (opts?.autostash)
|
|
501
|
+
args.push('--autostash');
|
|
502
|
+
args.push(`origin/${baseBranch}`);
|
|
503
|
+
await gitNetworkAsync(repoPath, args);
|
|
504
|
+
}
|
|
505
|
+
catch (err) {
|
|
506
|
+
const conflicted = getConflictedFiles(repoPath);
|
|
507
|
+
if (conflicted.length > 0 || getOngoingGitOperation(repoPath) === 'merge') {
|
|
508
|
+
throw new GitConflictError('merge', conflicted);
|
|
509
|
+
}
|
|
510
|
+
const status = getWorkingTreeStatus(repoPath);
|
|
511
|
+
if (status.staged > 0 || status.modified > 0) {
|
|
512
|
+
throw new DirtyWorktreeError('merge', status);
|
|
513
|
+
}
|
|
514
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
515
|
+
throw new Error(`Merge of 'origin/${baseBranch}' failed: ${message}`);
|
|
516
|
+
}
|
|
517
|
+
}
|
|
388
518
|
/** Abort an in-progress merge, rebase or cherry-pick. No-op if nothing is in progress. */
|
|
389
519
|
export function abortOngoingGitOperation(repoPath) {
|
|
390
520
|
const op = getOngoingGitOperation(repoPath);
|
|
@@ -1130,6 +1260,10 @@ export function isGitWorktree(repoPath) {
|
|
|
1130
1260
|
* The remaining rules mirror `git check-ref-format`.
|
|
1131
1261
|
*/
|
|
1132
1262
|
export function isValidBranchName(name) {
|
|
1263
|
+
// Guard the type too: a non-string would otherwise be coerced, and the
|
|
1264
|
+
// string "undefined" happens to satisfy the pattern below.
|
|
1265
|
+
if (typeof name !== 'string')
|
|
1266
|
+
return false;
|
|
1133
1267
|
if (!/^[A-Za-z0-9][A-Za-z0-9/_.-]*$/.test(name))
|
|
1134
1268
|
return false;
|
|
1135
1269
|
if (name.includes('..') || name.includes('//'))
|
|
@@ -1303,9 +1437,33 @@ export async function getUnpushedCountAsync(repoPath, workingBranch) {
|
|
|
1303
1437
|
* Mirrors the sync `fetchSourceBranch` sibling, including the optional `remote`
|
|
1304
1438
|
* parameter (defaults to `'origin'`).
|
|
1305
1439
|
*/
|
|
1440
|
+
/**
|
|
1441
|
+
* Async twin of `fetchSourceBranch`: it PROPAGATES the failure.
|
|
1442
|
+
*
|
|
1443
|
+
* Distinct from `fetchSourceBranchAsync` right below, which swallows it — that
|
|
1444
|
+
* one is the pr-watcher's best-effort refresh, where a missing remote is not
|
|
1445
|
+
* worth interrupting a background sweep. Workspace creation needs the opposite:
|
|
1446
|
+
* a source branch that cannot be fetched is a 422, not a workspace built on a
|
|
1447
|
+
* stale ref.
|
|
1448
|
+
*/
|
|
1449
|
+
export async function fetchSourceBranchOrThrowAsync(repoPath, sourceBranch, remote = 'origin') {
|
|
1450
|
+
try {
|
|
1451
|
+
await gitNetworkAsync(repoPath, ['fetch', remote, sourceBranch]);
|
|
1452
|
+
}
|
|
1453
|
+
catch (err) {
|
|
1454
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
1455
|
+
throw new Error(`Failed to fetch '${sourceBranch}' from '${remote}': ${message}`);
|
|
1456
|
+
}
|
|
1457
|
+
}
|
|
1306
1458
|
export async function fetchSourceBranchAsync(repoPath, branch, remote = 'origin') {
|
|
1307
1459
|
try {
|
|
1308
|
-
|
|
1460
|
+
// Non-interactive env like every other network call: without it a
|
|
1461
|
+
// credential or passphrase prompt hangs this fetch for its whole timeout.
|
|
1462
|
+
await execFileAsync('git', ['fetch', remote, branch], {
|
|
1463
|
+
cwd: repoPath,
|
|
1464
|
+
timeout: 30_000,
|
|
1465
|
+
env: NON_INTERACTIVE_GIT_ENV,
|
|
1466
|
+
});
|
|
1309
1467
|
}
|
|
1310
1468
|
catch (err) {
|
|
1311
1469
|
const msg = err instanceof Error ? err.message : String(err);
|
|
@@ -2,6 +2,7 @@ import fs from 'node:fs';
|
|
|
2
2
|
import os from 'node:os';
|
|
3
3
|
import path from 'node:path';
|
|
4
4
|
import { fileURLToPath } from 'node:url';
|
|
5
|
+
import { isPathInside } from './safe-path.js';
|
|
5
6
|
// Package root resolved from this file's location via pure path arithmetic
|
|
6
7
|
// (no filesystem calls, so this stays robust when tests mock `node:fs`).
|
|
7
8
|
// This file lives at `<root>/src/server/utils/paths.ts` in dev (tsx) and at
|
|
@@ -144,3 +145,21 @@ export function getClientSpaPath() {
|
|
|
144
145
|
const pwa = getPackageAssetPath('src', 'client', 'dist', 'pwa');
|
|
145
146
|
return fs.existsSync(pwa) ? pwa : null;
|
|
146
147
|
}
|
|
148
|
+
/**
|
|
149
|
+
* The file to serve for a SPA request, or null for "404".
|
|
150
|
+
*
|
|
151
|
+
* Kept pure (dist path in, file path out) because the traversal guard that
|
|
152
|
+
* used to live inline in index.ts once rejected `/` itself: the root resolves
|
|
153
|
+
* to the dist directory exactly, which a `startsWith(dist + sep)` check does
|
|
154
|
+
* not accept. `isPathInside` treats the root as inside, and this function has
|
|
155
|
+
* a test for the one URL every install opens first.
|
|
156
|
+
*/
|
|
157
|
+
export function resolveSpaFile(clientDistPath, pathname) {
|
|
158
|
+
const root = path.resolve(clientDistPath);
|
|
159
|
+
const candidate = path.resolve(root, `.${pathname}`);
|
|
160
|
+
if (!isPathInside(root, candidate))
|
|
161
|
+
return null;
|
|
162
|
+
// Client-side routes and directories fall back to the shell.
|
|
163
|
+
const file = fs.existsSync(candidate) && !fs.statSync(candidate).isDirectory() ? candidate : path.join(root, 'index.html');
|
|
164
|
+
return fs.existsSync(file) ? file : null;
|
|
165
|
+
}
|
|
@@ -41,8 +41,28 @@ export function resolvePathInside(rootPath, relativePath) {
|
|
|
41
41
|
return resolveExistingPathInside(root, resolved);
|
|
42
42
|
}
|
|
43
43
|
catch (err) {
|
|
44
|
+
if (err.code !== 'ENOENT')
|
|
45
|
+
throw err;
|
|
46
|
+
// The leaf does not exist. That is legitimate — this is how a new file gets
|
|
47
|
+
// created — unless the leaf is itself a symlink whose target is missing:
|
|
48
|
+
// realpath gives up with ENOENT on those too, and returning the unresolved
|
|
49
|
+
// path lets `writeFileSync` follow the link on O_CREAT and write outside the
|
|
50
|
+
// root. A repository that ships `notes.md -> ~/.bashrc.d/x.sh` would get a
|
|
51
|
+
// write there the next time the user saves.
|
|
52
|
+
assertLeafIsNotSymlink(resolved);
|
|
53
|
+
return resolved;
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
/** Reject a leaf that is a symlink, dangling or not. */
|
|
57
|
+
function assertLeafIsNotSymlink(candidate) {
|
|
58
|
+
try {
|
|
59
|
+
if (fs.lstatSync(candidate).isSymbolicLink())
|
|
60
|
+
throw new Error('Path escapes allowed root');
|
|
61
|
+
}
|
|
62
|
+
catch (err) {
|
|
63
|
+
// Genuinely absent: nothing to follow, nothing to reject.
|
|
44
64
|
if (err.code === 'ENOENT')
|
|
45
|
-
return
|
|
65
|
+
return;
|
|
46
66
|
throw err;
|
|
47
67
|
}
|
|
48
68
|
}
|
|
@@ -11,7 +11,7 @@ export const SCRIPT_TIMEOUT_MS = 5 * 60 * 1000; // 5 minutes
|
|
|
11
11
|
* Shared mechanism behind the setup and cleanup script services.
|
|
12
12
|
*/
|
|
13
13
|
export function runScript(opts) {
|
|
14
|
-
const { workspaceId, worktreePath, script, eventPrefix, tmpFileName, env } = opts;
|
|
14
|
+
const { workspaceId, worktreePath, script, eventPrefix, tmpFileName, env, extraEnv } = opts;
|
|
15
15
|
const timeoutMs = opts.timeoutMs ?? SCRIPT_TIMEOUT_MS;
|
|
16
16
|
return new Promise((resolve) => {
|
|
17
17
|
const scriptPath = path.join(worktreePath, '.ai', tmpFileName);
|
|
@@ -21,6 +21,7 @@ export function runScript(opts) {
|
|
|
21
21
|
cwd: worktreePath,
|
|
22
22
|
env: {
|
|
23
23
|
...process.env,
|
|
24
|
+
...Object.fromEntries(Object.entries(extraEnv ?? {}).filter(([key]) => key.startsWith('KOBO_'))),
|
|
24
25
|
WORKSPACE_ID: workspaceId,
|
|
25
26
|
WORKSPACE_NAME: env?.workspaceName ?? '',
|
|
26
27
|
BRANCH_NAME: env?.branchName ?? '',
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Mirrors the private `WORKSPACE_NAME_MAX_LENGTH` in workspace-service.ts.
|
|
3
|
+
* Duplicated rather than exported to avoid coupling this helper to that
|
|
4
|
+
* module's internals and to the many `vi.mock(workspace-service.js, factory)`
|
|
5
|
+
* test files that would otherwise need updating.
|
|
6
|
+
*/
|
|
7
|
+
const WORKSPACE_NAME_MAX_LENGTH = 200;
|
|
8
|
+
/**
|
|
9
|
+
* Cap an auto-derived title (Notion page title, Sentry issue title, PR/MR
|
|
10
|
+
* title) before handing it to `updateWorkspaceName`, which throws on anything
|
|
11
|
+
* longer. Right for a name the user typed; a long exception message pulled
|
|
12
|
+
* from Sentry is not user input, and truncating it is the sane default.
|
|
13
|
+
*/
|
|
14
|
+
export function truncateWorkspaceName(name) {
|
|
15
|
+
if (name.length <= WORKSPACE_NAME_MAX_LENGTH)
|
|
16
|
+
return name;
|
|
17
|
+
return `${name.slice(0, WORKSPACE_NAME_MAX_LENGTH - 1)}…`;
|
|
18
|
+
}
|
|
19
|
+
/**
|
|
20
|
+
* The create page sends `workspace` when the user typed no name, and an engine
|
|
21
|
+
* comparison sends `workspace (Claude Code)` / `workspace (OpenAI Codex)`. Both
|
|
22
|
+
* are placeholders the extracted title should replace; the suffix is what
|
|
23
|
+
* tells the two halves apart and must survive the rename.
|
|
24
|
+
*/
|
|
25
|
+
const PLACEHOLDER = /^workspace( \([^()]+\))?$/;
|
|
26
|
+
/**
|
|
27
|
+
* The name a workspace should take once a title was extracted for it, or
|
|
28
|
+
* `null` when the current name is the user's own and must be left alone.
|
|
29
|
+
*/
|
|
30
|
+
export function resolveExtractedName(currentName, extractedTitle) {
|
|
31
|
+
const match = PLACEHOLDER.exec(currentName);
|
|
32
|
+
if (!match)
|
|
33
|
+
return null;
|
|
34
|
+
const suffix = match[1] ?? '';
|
|
35
|
+
// Truncate the title alone so the engine suffix is never what gets cut.
|
|
36
|
+
return `${truncateWorkspaceName(extractedTitle)}${suffix}`;
|
|
37
|
+
}
|
package/dist/shared/consts.js
CHANGED
|
@@ -1 +1,23 @@
|
|
|
1
1
|
export const WORKTREES_PATH = '.worktrees';
|
|
2
|
+
/**
|
|
3
|
+
* Global settings holding a live credential. They must never leave the server
|
|
4
|
+
* in a payload a third party can read: they are blanked out of the config
|
|
5
|
+
* bundle, of the settings the HTTP API returns, and of what the MCP tools hand
|
|
6
|
+
* to the agent. Add any new credential here rather than filtering it at one
|
|
7
|
+
* call site.
|
|
8
|
+
*
|
|
9
|
+
* Lives in `shared` on purpose: the MCP server and the HTTP layer both need it,
|
|
10
|
+
* and a test that mocks `settings-service` must not be able to mock the list of
|
|
11
|
+
* secrets away.
|
|
12
|
+
*/
|
|
13
|
+
export const SECRET_GLOBAL_KEYS = ['notionMcpKey', 'sentryMcpKey', 'networkAccessToken', 'bitbucketToken'];
|
|
14
|
+
/**
|
|
15
|
+
* Stand-in the API sends instead of a stored credential. A client never
|
|
16
|
+
* receives the real value, so the settings form round-trips this mask on save
|
|
17
|
+
* and the write path reads it as "leave the stored one alone". Typing a new
|
|
18
|
+
* value still replaces the secret, and clearing the field still erases it —
|
|
19
|
+
* only an untouched field is preserved.
|
|
20
|
+
*/
|
|
21
|
+
export const MASKED_SECRET = '••••••••';
|
|
22
|
+
/** The single character `MASKED_SECRET` is made of. */
|
|
23
|
+
export const MASK_CHARACTER = '•';
|
package/dist/shared/models.js
CHANGED
|
@@ -6,10 +6,10 @@ export const CLAUDE_MODELS = [
|
|
|
6
6
|
i18nDescriptionKey: 'model.autoDescription',
|
|
7
7
|
},
|
|
8
8
|
{
|
|
9
|
-
id: 'claude-fable-5',
|
|
10
|
-
label: 'Fable 5',
|
|
11
|
-
i18nLabelKey: 'model.
|
|
12
|
-
i18nDescriptionKey: 'model.
|
|
9
|
+
id: 'claude-fable-5-1',
|
|
10
|
+
label: 'Fable 5.1',
|
|
11
|
+
i18nLabelKey: 'model.fable51',
|
|
12
|
+
i18nDescriptionKey: 'model.fable51Description',
|
|
13
13
|
},
|
|
14
14
|
{
|
|
15
15
|
id: 'claude-mythos-5',
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@loicngr/kobo",
|
|
3
|
-
"version": "1.11.
|
|
3
|
+
"version": "1.11.28",
|
|
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",
|
|
@@ -48,7 +48,7 @@
|
|
|
48
48
|
"node": ">=24.15.0"
|
|
49
49
|
},
|
|
50
50
|
"scripts": {
|
|
51
|
-
"dev": "KOBO_ENFORCE_LOCAL_HOME=1 KOBO_HOME=./data PORT=3300 tsx watch src/server/index.ts",
|
|
51
|
+
"dev": "KOBO_ENFORCE_LOCAL_HOME=1 KOBO_HOME=./data PORT=3300 KOBO_DEV_CLIENT_ORIGIN=http://localhost:8080 tsx watch src/server/index.ts",
|
|
52
52
|
"dev:client": "cd src/client && KOBO_BACKEND_PORT=3300 npx quasar dev",
|
|
53
53
|
"dev:all": "concurrently \"npm run dev\" \"npm run dev:client\"",
|
|
54
54
|
"build:client": "cd src/client && npx quasar build -m pwa",
|
|
@@ -60,6 +60,7 @@
|
|
|
60
60
|
"test:watch": "vitest --config vitest.config.ts --root .",
|
|
61
61
|
"test:client": "cd src/client && npm test",
|
|
62
62
|
"test:all": "npm test && npm run test:client",
|
|
63
|
+
"typecheck:tests": "tsc -p tsconfig.test.json",
|
|
63
64
|
"lint": "biome check .",
|
|
64
65
|
"lint:fix": "biome check --write .",
|
|
65
66
|
"format": "biome format --write .",
|
|
@@ -1 +1 @@
|
|
|
1
|
-
.agent-error-item[data-v-e98c3c02]{gap:var(--kobo-space-sm);padding:var(--kobo-space-sm) var(--kobo-space-md);border-left:2px solid var(--kobo-danger);background:var(--kobo-surface-2);border-radius:var(--kobo-radius-sm);align-items:flex-start;display:flex}.agent-error-item__icon[data-v-e98c3c02]{color:var(--kobo-danger);flex:none;margin-top:2px}.agent-error-item__body[data-v-e98c3c02]{min-width:0}.agent-error-item__title[data-v-e98c3c02]{color:var(--kobo-text);font-weight:600}.agent-error-item__message[data-v-e98c3c02]{color:var(--kobo-text-2);font-family:var(--kobo-font-mono);white-space:pre-wrap;word-break:break-word;font-size:12px}.markdown-message[data-v-539815f6]{color:var(--kobo-text-2);word-break:break-word;overflow-wrap:anywhere;min-width:0;max-width:100%;font-size:13px;line-height:1.55}.markdown-message[data-v-539815f6] *{max-width:100%}.markdown-message[data-v-539815f6] p{margin:0 0 .5em}.markdown-message[data-v-539815f6] p:last-child{margin-bottom:0}.markdown-message[data-v-539815f6] pre{background:#00000059;border-radius:4px;margin:.5em 0;padding:.5em .75em;overflow-x:auto}.markdown-message[data-v-539815f6] code{word-break:break-all;background:#0000004d;border-radius:3px;padding:.1em .3em;font-size:.9em}.markdown-message[data-v-539815f6] pre code{background:0 0;padding:0}.markdown-message[data-v-539815f6] ul,.markdown-message[data-v-539815f6] ol{margin:.25em 0 .5em;padding-left:1.5em}.markdown-message[data-v-539815f6] li{margin:.15em 0}.markdown-message[data-v-539815f6] a{color:var(--kobo-text-2);text-decoration:underline}.markdown-message[data-v-539815f6] .document-link{color:var(--kobo-text-2);cursor:pointer;-webkit-text-decoration:underline dotted;text-decoration:underline dotted}.markdown-message[data-v-539815f6] .document-link:hover{color:var(--kobo-text);-webkit-text-decoration:underline;text-decoration:underline}.markdown-message[data-v-539815f6] h1,.markdown-message[data-v-539815f6] h2,.markdown-message[data-v-539815f6] h3,.markdown-message[data-v-539815f6] h4,.markdown-message[data-v-539815f6] h5,.markdown-message[data-v-539815f6] h6{margin:.5em 0 .3em;font-weight:600;line-height:1.3}.markdown-message[data-v-539815f6] h1{font-size:1.25em}.markdown-message[data-v-539815f6] h2{font-size:1.15em}.markdown-message[data-v-539815f6] h3{font-size:1.08em}.markdown-message[data-v-539815f6] h4,.markdown-message[data-v-539815f6] h5,.markdown-message[data-v-539815f6] h6{font-size:1em}.markdown-message[data-v-539815f6] blockquote{color:#ffffffb3;border-left:3px solid #fff3;margin:.5em 0;padding-left:.75em}.markdown-message[data-v-539815f6] table{border-collapse:collapse;margin:.5em 0}.markdown-message[data-v-539815f6] th,.markdown-message[data-v-539815f6] td{border:1px solid #ffffff26;padding:.25em .5em}.tool-row[data-v-2482adfe]{border-radius:4px;margin:0;font-size:12px}.tool-header[data-v-2482adfe]{color:var(--kobo-text-2);cursor:default;align-items:center;gap:10px;min-width:0;padding:5px 10px;display:flex}.tool-row:not(.tool-row-generic) .tool-header[data-v-2482adfe],.tool-row--toggleable .tool-header[data-v-2482adfe]{cursor:pointer}.tool-row:has(.tool-diff) .tool-header[data-v-2482adfe]{cursor:pointer}.tool-row:not(.tool-row-generic) .tool-header[data-v-2482adfe]:hover,.tool-row--toggleable .tool-header[data-v-2482adfe]:hover{background:#ffffff08}.tool-icon[data-v-2482adfe]{color:var(--kobo-text-2);flex-shrink:0}.tool-name[data-v-2482adfe]{color:var(--kobo-text-2);flex-shrink:0;font-weight:600}.tool-arg[data-v-2482adfe],.tool-path[data-v-2482adfe]{color:var(--kobo-text-3);text-overflow:ellipsis;white-space:nowrap;min-width:0;max-width:100%;font-family:var(--kobo-font-mono);font-size:11.5px;overflow:hidden}.tool-open-diff[data-v-2482adfe]{flex-shrink:0}.tool-path[data-v-2482adfe],.tool-arg[data-v-2482adfe]{flex:1}.tool-stat-add[data-v-2482adfe]{color:var(--kobo-success);flex-shrink:0;font-size:11px;font-weight:600}.tool-stat-del[data-v-2482adfe]{color:var(--kobo-danger);flex-shrink:0;font-size:11px;font-weight:600}.tool-diff[data-v-2482adfe]{max-height:400px;font-family:var(--kobo-font-mono);background:#0003;border-radius:4px;margin-top:4px;padding:8px 0;font-size:11px;line-height:1.5;overflow:auto}.diff-line[data-v-2482adfe]{white-space:pre;color:var(--kobo-text-2);padding:0 12px}.diff-sign[data-v-2482adfe]{width:14px;color:var(--kobo-text-disabled);-webkit-user-select:none;user-select:none;display:inline-block}.diff-add[data-v-2482adfe]{color:var(--kobo-success);background:#66bb6a1a}.diff-add .diff-sign[data-v-2482adfe]{color:var(--kobo-success)}.diff-del[data-v-2482adfe]{color:var(--kobo-danger);background:#ef53501a}.diff-del .diff-sign[data-v-2482adfe]{color:var(--kobo-danger)}.tool-output[data-v-2482adfe]{color:var(--kobo-text-2);font-family:var(--kobo-font-mono);white-space:pre-wrap;background:#00000026;border-radius:4px;max-height:8em;margin-top:4px;padding:6px 10px;font-size:11px;overflow:auto}.markdown-message[data-v-7687eb7e]{color:var(--kobo-text-2);word-break:break-word;overflow-wrap:anywhere;min-width:0;max-width:100%;font-size:13px;line-height:1.55}.markdown-message[data-v-7687eb7e] *{max-width:100%}.markdown-message[data-v-7687eb7e] img{-o-object-fit:contain;object-fit:contain;cursor:zoom-in;background:#0003;border-radius:4px;max-width:180px;max-height:100px;margin:.3em 0;display:block}.markdown-message[data-v-7687eb7e] code{word-break:break-all}.markdown-message[data-v-7687eb7e] p{margin:0 0 .4em}.markdown-message[data-v-7687eb7e] p:last-child{margin-bottom:0}.markdown-message[data-v-7687eb7e] code{background:#00000040;border-radius:3px;padding:.1em .3em}.markdown-message[data-v-7687eb7e] h1,.markdown-message[data-v-7687eb7e] h2,.markdown-message[data-v-7687eb7e] h3,.markdown-message[data-v-7687eb7e] h4,.markdown-message[data-v-7687eb7e] h5,.markdown-message[data-v-7687eb7e] h6{margin:.4em 0 .25em;font-weight:600;line-height:1.3}.markdown-message[data-v-7687eb7e] h1{font-size:1.25em}.markdown-message[data-v-7687eb7e] h2{font-size:1.15em}.markdown-message[data-v-7687eb7e] h3{font-size:1.08em}.markdown-message[data-v-7687eb7e] h4,.markdown-message[data-v-7687eb7e] h5,.markdown-message[data-v-7687eb7e] h6{font-size:1em}.markdown-user-prompt[data-v-7687eb7e]{color:var(--kobo-text-2);font-size:12px;font-style:italic}.markdown-user-prompt[data-v-7687eb7e] p{margin:0 0 .4em}.markdown-user-prompt[data-v-7687eb7e] code{background:#ffffff14;border-radius:3px;padding:.1em .3em;font-style:normal}.image-lightbox-img{-o-object-fit:contain;object-fit:contain;cursor:zoom-out;background:#0000004d;border-radius:4px;max-width:92vw;max-height:92vh;display:block}.turn-card[data-v-59df7154]{border:1px solid #ffffff14;border-left:3px solid var(--turn-accent);background:#ffffff05;border-radius:6px;min-width:0;max-width:100%;margin:14px 0;overflow:hidden}.turn-card--highlighted[data-v-59df7154]{animation:1.8s ease-out search-highlight-59df7154}@keyframes search-highlight-59df7154{0%,45%{box-shadow:0 0 0 2px #818cf8f2,0 0 22px #818cf873}to{box-shadow:none}}.turn-header[data-v-59df7154]{color:var(--kobo-text-3);background:#ffffff08;border-bottom:1px solid #ffffff0d;align-items:center;gap:8px;padding:8px 14px;font-size:11px;display:flex}.turn-badge[data-v-59df7154]{letter-spacing:.3px;border-radius:3px;padding:2px 8px;font-size:11px;font-weight:700}.turn-badge-user[data-v-59df7154]{color:var(--kobo-turn-user);background:#ce93d826}.turn-badge-agent[data-v-59df7154]{color:var(--kobo-turn-agent);background:#7986cb26}.turn-badge-system[data-v-59df7154]{color:var(--kobo-text-2);background:#75757533;font-style:italic}.turn-badge-session[data-v-59df7154]{color:var(--kobo-text-3);background:#61616133}.turn-badge-script[data-v-59df7154]{color:var(--kobo-success);background:#4db6ac26}.turn-time[data-v-59df7154]{color:var(--kobo-text-disabled);font-family:var(--kobo-font-mono);font-size:11px}.turn-time-arrow[data-v-59df7154]{opacity:.7;margin:0 -2px}.turn-time-updated[data-v-59df7154]{color:var(--kobo-text-3)}.turn-actions[data-v-59df7154]{color:var(--kobo-text-3);font-size:11px}.turn-body[data-v-59df7154]{flex-direction:column;gap:12px;min-width:0;padding:14px 18px;display:flex}.turn-body[data-v-59df7154]>*{min-width:0;max-width:100%}.turn-body[data-v-59df7154] .tool-row+.tool-row{margin-top:-8px}.turn-scroll-top[data-v-59df7154]{justify-content:flex-start;padding:0 8px 6px;display:flex}.turn-scroll-top-btn[data-v-59df7154]{opacity:.5;transition:opacity .15s}.turn-scroll-top-btn[data-v-59df7154]:hover{opacity:1}.activity-feed-wrap[data-v-03fed8f2]{width:100%;height:100%;position:relative}.activity-feed-scroll[data-v-03fed8f2]{width:100%;height:100%}.activity-feed-nav-cluster[data-v-03fed8f2]{z-index:2;align-items:center;gap:8px;display:flex;position:absolute;bottom:14px;right:14px}.activity-feed-compacting[data-v-03fed8f2]{z-index:3;background:var(--kobo-surface);border:1px solid var(--kobo-border);border-radius:6px;align-items:center;gap:8px;padding:6px 14px;display:flex;position:absolute;top:12px;left:50%;transform:translate(-50%);box-shadow:0 4px 16px #00000059}.fade-enter-active[data-v-03fed8f2],.fade-leave-active[data-v-03fed8f2]{transition:opacity .16s}.fade-enter-from[data-v-03fed8f2],.fade-leave-to[data-v-03fed8f2]{opacity:0}.activity-feed-nav-btn[data-v-03fed8f2]{opacity:.8;transition:opacity .12s}.activity-feed-nav-btn[data-v-03fed8f2]:hover{opacity:1}.activity-feed-scroll[data-v-03fed8f2] .q-scrollarea__content{max-width:100%;overflow-x:hidden}.activity-feed-switching[data-v-03fed8f2]{justify-content:center;align-items:center;width:100%;height:100%;display:flex}.activity-feed-empty[data-v-03fed8f2]{align-items:center;gap:var(--kobo-space-sm);padding:var(--kobo-space-4xl) var(--kobo-space-xl);color:var(--kobo-text-3);text-align:center;flex-direction:column;display:flex}.activity-feed-empty__title[data-v-03fed8f2]{font-family:var(--kobo-font-sans);color:var(--kobo-text-2);font-size:14px;font-weight:500}.activity-feed-empty__hint[data-v-03fed8f2]{font-family:var(--kobo-font-sans);color:var(--kobo-text-3);max-width:380px;font-size:13px}
|
|
1
|
+
.agent-error-item[data-v-e98c3c02]{gap:var(--kobo-space-sm);padding:var(--kobo-space-sm) var(--kobo-space-md);border-left:2px solid var(--kobo-danger);background:var(--kobo-surface-2);border-radius:var(--kobo-radius-sm);align-items:flex-start;display:flex}.agent-error-item__icon[data-v-e98c3c02]{color:var(--kobo-danger);flex:none;margin-top:2px}.agent-error-item__body[data-v-e98c3c02]{min-width:0}.agent-error-item__title[data-v-e98c3c02]{color:var(--kobo-text);font-weight:600}.agent-error-item__message[data-v-e98c3c02]{color:var(--kobo-text-2);font-family:var(--kobo-font-mono);white-space:pre-wrap;word-break:break-word;font-size:12px}.markdown-message[data-v-99371035]{color:var(--kobo-text-2);word-break:break-word;overflow-wrap:anywhere;min-width:0;max-width:100%;font-size:13px;line-height:1.55}.markdown-message[data-v-99371035] *{max-width:100%}.markdown-message[data-v-99371035] p{margin:0 0 .5em}.markdown-message[data-v-99371035] p:last-child{margin-bottom:0}.markdown-message[data-v-99371035] pre{background:#00000059;border-radius:4px;margin:.5em 0;padding:.5em .75em;overflow-x:auto}.markdown-message[data-v-99371035] code{word-break:break-all;background:#0000004d;border-radius:3px;padding:.1em .3em;font-size:.9em}.markdown-message[data-v-99371035] pre code{background:0 0;padding:0}.markdown-message[data-v-99371035] ul,.markdown-message[data-v-99371035] ol{margin:.25em 0 .5em;padding-left:1.5em}.markdown-message[data-v-99371035] li{margin:.15em 0}.markdown-message[data-v-99371035] a{color:var(--kobo-text-2);text-decoration:underline}.markdown-message[data-v-99371035] .document-link{color:var(--kobo-text-2);cursor:pointer;-webkit-text-decoration:underline dotted;text-decoration:underline dotted}.markdown-message[data-v-99371035] .document-link:hover{color:var(--kobo-text);-webkit-text-decoration:underline;text-decoration:underline}.markdown-message[data-v-99371035] h1,.markdown-message[data-v-99371035] h2,.markdown-message[data-v-99371035] h3,.markdown-message[data-v-99371035] h4,.markdown-message[data-v-99371035] h5,.markdown-message[data-v-99371035] h6{margin:.5em 0 .3em;font-weight:600;line-height:1.3}.markdown-message[data-v-99371035] h1{font-size:1.25em}.markdown-message[data-v-99371035] h2{font-size:1.15em}.markdown-message[data-v-99371035] h3{font-size:1.08em}.markdown-message[data-v-99371035] h4,.markdown-message[data-v-99371035] h5,.markdown-message[data-v-99371035] h6{font-size:1em}.markdown-message[data-v-99371035] blockquote{color:#ffffffb3;border-left:3px solid #fff3;margin:.5em 0;padding-left:.75em}.markdown-message[data-v-99371035] table{border-collapse:collapse;margin:.5em 0}.markdown-message[data-v-99371035] th,.markdown-message[data-v-99371035] td{border:1px solid #ffffff26;padding:.25em .5em}.tool-row[data-v-2482adfe]{border-radius:4px;margin:0;font-size:12px}.tool-header[data-v-2482adfe]{color:var(--kobo-text-2);cursor:default;align-items:center;gap:10px;min-width:0;padding:5px 10px;display:flex}.tool-row:not(.tool-row-generic) .tool-header[data-v-2482adfe],.tool-row--toggleable .tool-header[data-v-2482adfe]{cursor:pointer}.tool-row:has(.tool-diff) .tool-header[data-v-2482adfe]{cursor:pointer}.tool-row:not(.tool-row-generic) .tool-header[data-v-2482adfe]:hover,.tool-row--toggleable .tool-header[data-v-2482adfe]:hover{background:#ffffff08}.tool-icon[data-v-2482adfe]{color:var(--kobo-text-2);flex-shrink:0}.tool-name[data-v-2482adfe]{color:var(--kobo-text-2);flex-shrink:0;font-weight:600}.tool-arg[data-v-2482adfe],.tool-path[data-v-2482adfe]{color:var(--kobo-text-3);text-overflow:ellipsis;white-space:nowrap;min-width:0;max-width:100%;font-family:var(--kobo-font-mono);font-size:11.5px;overflow:hidden}.tool-open-diff[data-v-2482adfe]{flex-shrink:0}.tool-path[data-v-2482adfe],.tool-arg[data-v-2482adfe]{flex:1}.tool-stat-add[data-v-2482adfe]{color:var(--kobo-success);flex-shrink:0;font-size:11px;font-weight:600}.tool-stat-del[data-v-2482adfe]{color:var(--kobo-danger);flex-shrink:0;font-size:11px;font-weight:600}.tool-diff[data-v-2482adfe]{max-height:400px;font-family:var(--kobo-font-mono);background:#0003;border-radius:4px;margin-top:4px;padding:8px 0;font-size:11px;line-height:1.5;overflow:auto}.diff-line[data-v-2482adfe]{white-space:pre;color:var(--kobo-text-2);padding:0 12px}.diff-sign[data-v-2482adfe]{width:14px;color:var(--kobo-text-disabled);-webkit-user-select:none;user-select:none;display:inline-block}.diff-add[data-v-2482adfe]{color:var(--kobo-success);background:#66bb6a1a}.diff-add .diff-sign[data-v-2482adfe]{color:var(--kobo-success)}.diff-del[data-v-2482adfe]{color:var(--kobo-danger);background:#ef53501a}.diff-del .diff-sign[data-v-2482adfe]{color:var(--kobo-danger)}.tool-output[data-v-2482adfe]{color:var(--kobo-text-2);font-family:var(--kobo-font-mono);white-space:pre-wrap;background:#00000026;border-radius:4px;max-height:8em;margin-top:4px;padding:6px 10px;font-size:11px;overflow:auto}.markdown-message[data-v-7687eb7e]{color:var(--kobo-text-2);word-break:break-word;overflow-wrap:anywhere;min-width:0;max-width:100%;font-size:13px;line-height:1.55}.markdown-message[data-v-7687eb7e] *{max-width:100%}.markdown-message[data-v-7687eb7e] img{-o-object-fit:contain;object-fit:contain;cursor:zoom-in;background:#0003;border-radius:4px;max-width:180px;max-height:100px;margin:.3em 0;display:block}.markdown-message[data-v-7687eb7e] code{word-break:break-all}.markdown-message[data-v-7687eb7e] p{margin:0 0 .4em}.markdown-message[data-v-7687eb7e] p:last-child{margin-bottom:0}.markdown-message[data-v-7687eb7e] code{background:#00000040;border-radius:3px;padding:.1em .3em}.markdown-message[data-v-7687eb7e] h1,.markdown-message[data-v-7687eb7e] h2,.markdown-message[data-v-7687eb7e] h3,.markdown-message[data-v-7687eb7e] h4,.markdown-message[data-v-7687eb7e] h5,.markdown-message[data-v-7687eb7e] h6{margin:.4em 0 .25em;font-weight:600;line-height:1.3}.markdown-message[data-v-7687eb7e] h1{font-size:1.25em}.markdown-message[data-v-7687eb7e] h2{font-size:1.15em}.markdown-message[data-v-7687eb7e] h3{font-size:1.08em}.markdown-message[data-v-7687eb7e] h4,.markdown-message[data-v-7687eb7e] h5,.markdown-message[data-v-7687eb7e] h6{font-size:1em}.markdown-user-prompt[data-v-7687eb7e]{color:var(--kobo-text-2);font-size:12px;font-style:italic}.markdown-user-prompt[data-v-7687eb7e] p{margin:0 0 .4em}.markdown-user-prompt[data-v-7687eb7e] code{background:#ffffff14;border-radius:3px;padding:.1em .3em;font-style:normal}.image-lightbox-img{-o-object-fit:contain;object-fit:contain;cursor:zoom-out;background:#0000004d;border-radius:4px;max-width:92vw;max-height:92vh;display:block}.turn-card[data-v-6e61835e]{border:1px solid #ffffff14;border-left:3px solid var(--turn-accent);background:#ffffff05;border-radius:6px;min-width:0;max-width:100%;margin:14px 0;overflow:hidden}.turn-card--highlighted[data-v-6e61835e]{animation:1.8s ease-out search-highlight-6e61835e}@keyframes search-highlight-6e61835e{0%,45%{box-shadow:0 0 0 2px #818cf8f2,0 0 22px #818cf873}to{box-shadow:none}}.turn-header[data-v-6e61835e]{color:var(--kobo-text-3);background:#ffffff08;border-bottom:1px solid #ffffff0d;align-items:center;gap:8px;padding:8px 14px;font-size:11px;display:flex}.turn-badge[data-v-6e61835e]{letter-spacing:.3px;border-radius:3px;padding:2px 8px;font-size:11px;font-weight:700}.turn-badge-user[data-v-6e61835e]{color:var(--kobo-turn-user);background:#ce93d826}.turn-badge-agent[data-v-6e61835e]{color:var(--kobo-turn-agent);background:#7986cb26}.turn-badge-system[data-v-6e61835e]{color:var(--kobo-text-2);background:#75757533;font-style:italic}.turn-badge-session[data-v-6e61835e]{color:var(--kobo-text-3);background:#61616133}.turn-badge-script[data-v-6e61835e]{color:var(--kobo-success);background:#4db6ac26}.turn-time[data-v-6e61835e]{color:var(--kobo-text-disabled);font-family:var(--kobo-font-mono);font-size:11px}.turn-time-arrow[data-v-6e61835e]{opacity:.7;margin:0 -2px}.turn-time-updated[data-v-6e61835e]{color:var(--kobo-text-3)}.turn-actions[data-v-6e61835e]{color:var(--kobo-text-3);font-size:11px}.turn-body[data-v-6e61835e]{flex-direction:column;gap:12px;min-width:0;padding:14px 18px;display:flex}.turn-body[data-v-6e61835e]>*{min-width:0;max-width:100%}.turn-body[data-v-6e61835e] .tool-row+.tool-row{margin-top:-8px}.turn-scroll-top[data-v-6e61835e]{justify-content:flex-start;padding:0 8px 6px;display:flex}.turn-scroll-top-btn[data-v-6e61835e]{opacity:.5;transition:opacity .15s}.turn-scroll-top-btn[data-v-6e61835e]:hover{opacity:1}.activity-feed-wrap[data-v-03fed8f2]{width:100%;height:100%;position:relative}.activity-feed-scroll[data-v-03fed8f2]{width:100%;height:100%}.activity-feed-nav-cluster[data-v-03fed8f2]{z-index:2;align-items:center;gap:8px;display:flex;position:absolute;bottom:14px;right:14px}.activity-feed-compacting[data-v-03fed8f2]{z-index:3;background:var(--kobo-surface);border:1px solid var(--kobo-border);border-radius:6px;align-items:center;gap:8px;padding:6px 14px;display:flex;position:absolute;top:12px;left:50%;transform:translate(-50%);box-shadow:0 4px 16px #00000059}.fade-enter-active[data-v-03fed8f2],.fade-leave-active[data-v-03fed8f2]{transition:opacity .16s}.fade-enter-from[data-v-03fed8f2],.fade-leave-to[data-v-03fed8f2]{opacity:0}.activity-feed-nav-btn[data-v-03fed8f2]{opacity:.8;transition:opacity .12s}.activity-feed-nav-btn[data-v-03fed8f2]:hover{opacity:1}.activity-feed-scroll[data-v-03fed8f2] .q-scrollarea__content{max-width:100%;overflow-x:hidden}.activity-feed-switching[data-v-03fed8f2]{justify-content:center;align-items:center;width:100%;height:100%;display:flex}.activity-feed-empty[data-v-03fed8f2]{align-items:center;gap:var(--kobo-space-sm);padding:var(--kobo-space-4xl) var(--kobo-space-xl);color:var(--kobo-text-3);text-align:center;flex-direction:column;display:flex}.activity-feed-empty__title[data-v-03fed8f2]{font-family:var(--kobo-font-sans);color:var(--kobo-text-2);font-size:14px;font-weight:500}.activity-feed-empty__hint[data-v-03fed8f2]{font-family:var(--kobo-font-sans);color:var(--kobo-text-3);max-width:380px;font-size:13px}
|