@loicngr/kobo 1.11.36 → 1.11.37
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 +15 -0
- package/CHANGELOG.md +4 -0
- package/dist/server/index.js +11 -24
- package/dist/server/routes/dev-server.js +2 -1
- package/dist/server/routes/pull-requests.js +5 -4
- package/dist/server/routes/workspaces.js +122 -91
- package/dist/server/services/agent/engines/codex/client.js +1 -0
- package/dist/server/services/agent/engines/codex/engine.js +47 -10
- package/dist/server/services/agent/engines/codex/event-mapper.js +5 -1
- package/dist/server/services/agent/engines/codex/jsonrpc/peer.js +21 -8
- package/dist/server/services/agent/engines/codex/jsonrpc/transport.js +30 -3
- package/dist/server/services/agent/orchestrator.js +146 -68
- package/dist/server/services/agent/session-controller.js +12 -2
- package/dist/server/services/auto-loop-service.js +57 -3
- package/dist/server/services/change-source-branch-service.js +36 -4
- package/dist/server/services/cron-service.js +19 -0
- package/dist/server/services/db-backup-service.js +30 -0
- package/dist/server/services/dev-server-service.js +167 -38
- package/dist/server/services/image-service.js +25 -28
- package/dist/server/services/pr-checkout-service.js +34 -5
- package/dist/server/services/quota-backoff-service.js +25 -8
- package/dist/server/services/wakeup-service.js +23 -3
- package/dist/server/services/worktree-purge-service.js +3 -7
- package/dist/server/utils/agent-stop-result.js +12 -0
- package/dist/server/utils/git-ops.js +8 -1
- package/dist/server/utils/git-worktree-fingerprint.js +86 -0
- package/dist/server/utils/safe-path.js +21 -0
- package/dist/server/utils/script-runner.js +32 -9
- package/dist/server/utils/workspace-lifecycle-guard.js +27 -0
- package/package.json +1 -1
- package/src/client/dist/pwa/assets/{ActivityFeed-Bakgvay6.css → ActivityFeed-BDyFBoxd.css} +1 -1
- package/src/client/dist/pwa/assets/ActivityFeed-C_-hxIo8.js +8 -0
- package/src/client/dist/pwa/assets/{ChangelogPage-BFGYQ6eY.js → ChangelogPage-CBlGJ4zT.js} +1 -1
- package/src/client/dist/pwa/assets/{ComparisonPanel-C6DfjpZZ.js → ComparisonPanel-DZAE2X9j.js} +1 -1
- package/src/client/dist/pwa/assets/CreatePage-A3pVradx.css +1 -0
- package/src/client/dist/pwa/assets/CreatePage-ArmtcF4d.js +2 -0
- package/src/client/dist/pwa/assets/{DashboardPage-DsVRjzzy.js → DashboardPage-eQTCXcdz.js} +1 -1
- package/src/client/dist/pwa/assets/{DiffViewer-DSlSAIiW.js → DiffViewer-CaT7vuSr.js} +3 -3
- package/src/client/dist/pwa/assets/{DocumentsPanel-C1eXTw_Q.js → DocumentsPanel-A0isCTaI.js} +1 -1
- package/src/client/dist/pwa/assets/{DrawerToggleButton-BxYpUb4y.js → DrawerToggleButton-T70fh8dp.js} +1 -1
- package/src/client/dist/pwa/assets/{GitPanel-DnBWSdVp.js → GitPanel-CAvk0G7h.js} +3 -3
- package/src/client/dist/pwa/assets/{HealthPage-rX1TiaP9.js → HealthPage-D8Mr0V6w.js} +1 -1
- package/src/client/dist/pwa/assets/{MainLayout-Ceuh5uJj.js → MainLayout-x6Es4Da-.js} +2 -2
- package/src/client/dist/pwa/assets/{QBtnDropdown-K7syiyyH.js → QBtnDropdown-CQqzzlFI.js} +1 -1
- package/src/client/dist/pwa/assets/{QDrawer-DapzE8DH.js → QDrawer-Cwj2qh0r.js} +1 -1
- package/src/client/dist/pwa/assets/{QSelect-Bodml89k.js → QSelect-BSPtuXJg.js} +1 -1
- package/src/client/dist/pwa/assets/{QTooltip-K-9OdDfq.js → QTooltip-DC_kDKku.js} +1 -1
- package/src/client/dist/pwa/assets/{SchedulePanel-CfumTzAa.js → SchedulePanel-9aJSoz6Y.js} +1 -1
- package/src/client/dist/pwa/assets/{SearchPage-AhZYS-dS.js → SearchPage-BjNvtE_U.js} +1 -1
- package/src/client/dist/pwa/assets/{SettingsPage-BoHXWvKn.js → SettingsPage-CvLnT73V.js} +1 -1
- package/src/client/dist/pwa/assets/{SplitWorkspacePage-3wK11mcu.js → SplitWorkspacePage-DTao0DHr.js} +1 -1
- package/src/client/dist/pwa/assets/{TerminalPanel-CKg0BeNK.js → TerminalPanel-BrKIW-bJ.js} +1 -1
- package/src/client/dist/pwa/assets/{TouchPan-D6ctyPYf.js → TouchPan-_n1AQgoo.js} +1 -1
- package/src/client/dist/pwa/assets/{TourReplayButton-Dn4Dy_kH.js → TourReplayButton-CJF3nVvn.js} +1 -1
- package/src/client/dist/pwa/assets/{WorkspaceAttentionLabels-DnoabsfK.js → WorkspaceAttentionLabels-CxesUiIm.js} +1 -1
- package/src/client/dist/pwa/assets/{WorkspacePage-DYQrfeYw.css → WorkspacePage-DoXtwpjV.css} +1 -1
- package/src/client/dist/pwa/assets/WorkspacePage-MxBE0mr3.js +4 -0
- package/src/client/dist/pwa/assets/{browser-DHpSUK8-.js → browser-DGWpkvN3.js} +1 -1
- package/src/client/dist/pwa/assets/{build-path-tree-DtEjsZlh.js → build-path-tree-Ba1dSLgi.js} +1 -1
- package/src/client/dist/pwa/assets/{de-Dt_m-H83.js → de-DVK21RB6.js} +1 -1
- package/src/client/dist/pwa/assets/{documents-B2dg1wRf.js → documents-BMHzjW_g.js} +1 -1
- package/src/client/dist/pwa/assets/{editor.api-Cd1D35oj.js → editor.api-CYTuNwqu.js} +1 -1
- package/src/client/dist/pwa/assets/{efforts-IB1vPjJQ.js → efforts-BPlRSHXp.js} +1 -1
- package/src/client/dist/pwa/assets/{engineFeatures-DYSyDEJe.js → engineFeatures-DVB2rklO.js} +1 -1
- package/src/client/dist/pwa/assets/{es-DFg25DCS.js → es-C8cQwDZM.js} +1 -1
- package/src/client/dist/pwa/assets/{expand-template-CNAHiesL.js → expand-template-Cj82YE-O.js} +1 -1
- package/src/client/dist/pwa/assets/{format-DidwgbbX.js → format-Dk7O3LhA.js} +1 -1
- package/src/client/dist/pwa/assets/{formatters-BjzyUHlu.js → formatters-TGjvs7Ca.js} +1 -1
- package/src/client/dist/pwa/assets/{fr-CQ1slpol.js → fr-BVOMZ2Bl.js} +1 -1
- package/src/client/dist/pwa/assets/{freemarker2-jNGWawVw.js → freemarker2-39lO_dWH.js} +1 -1
- package/src/client/dist/pwa/assets/{handlebars-DfSqClrW.js → handlebars-M_0V82cD.js} +1 -1
- package/src/client/dist/pwa/assets/{html-ByLX-n8T.js → html-yILqYDsl.js} +1 -1
- package/src/client/dist/pwa/assets/i18n-2UeMfd5m.js +1 -0
- package/src/client/dist/pwa/assets/{index-DcwcFDrZ.js → index-BJU5dnkF.js} +2 -2
- package/src/client/dist/pwa/assets/{it-w09rfaP7.js → it-CxkAoOj4.js} +1 -1
- package/src/client/dist/pwa/assets/{javascript-Dja1fHfv.js → javascript-DrW-amDc.js} +1 -1
- package/src/client/dist/pwa/assets/{liquid-D315hW6C.js → liquid-D_mXmUoL.js} +1 -1
- package/src/client/dist/pwa/assets/{mdx-B5yUwpzv.js → mdx-E8O3lvR5.js} +1 -1
- package/src/client/dist/pwa/assets/{monaco-contributions-NfxrU0r3.js → monaco-contributions-C2K3sl_V.js} +1 -1
- package/src/client/dist/pwa/assets/{monaco.contribution-D1hXPkDZ.js → monaco.contribution-DLhHQIJn.js} +2 -2
- package/src/client/dist/pwa/assets/{network-auth-CARVtbNe.js → network-auth-BUwWtnui.js} +1 -1
- package/src/client/dist/pwa/assets/{notify-theme-CKC3-EAZ.js → notify-theme-1mOpY2vN.js} +1 -1
- package/src/client/dist/pwa/assets/{pending-diff-open-BlLrQaW7.js → pending-diff-open-BL7dKRqF.js} +1 -1
- package/src/client/dist/pwa/assets/{permissionModes-DMzFEQhr.js → permissionModes-BtC3_P-P.js} +1 -1
- package/src/client/dist/pwa/assets/{project-color-CFjdIj6W.js → project-color-BFBFZWeG.js} +1 -1
- package/src/client/dist/pwa/assets/{python-Dc6mnEip.js → python-DoZAYim6.js} +1 -1
- package/src/client/dist/pwa/assets/{razor-DArVFBGO.js → razor-9bZ30g2y.js} +1 -1
- package/src/client/dist/pwa/assets/{render-chat-markdown-BTPAjU2g.js → render-chat-markdown-7EtWNjG0.js} +1 -1
- package/src/client/dist/pwa/assets/{typescript-COE1vIyB.js → typescript-C1CBfL3L.js} +1 -1
- package/src/client/dist/pwa/assets/{use-tours-C8x2V1w5.js → use-tours-BJ81clv6.js} +1 -1
- package/src/client/dist/pwa/assets/{workspace-templates-DHyA_GEW.js → workspace-templates-r1-bipjM.js} +1 -1
- package/src/client/dist/pwa/assets/{xml-eYjr4cGR.js → xml-COuu5isK.js} +1 -1
- package/src/client/dist/pwa/assets/{yaml-UooOcssF.js → yaml-ChZ7WqaJ.js} +1 -1
- package/src/client/dist/pwa/index.html +1 -1
- package/src/client/dist/pwa/sw.js +1 -1
- package/src/client/dist/pwa/assets/ActivityFeed-BPx-xhFY.js +0 -8
- package/src/client/dist/pwa/assets/CreatePage-DxOyY65u.css +0 -1
- package/src/client/dist/pwa/assets/CreatePage-ypEzkAKv.js +0 -2
- package/src/client/dist/pwa/assets/WorkspacePage-BI7CW2yT.js +0 -4
- package/src/client/dist/pwa/assets/i18n-CnjXQjxy.js +0 -1
|
@@ -5,7 +5,10 @@ import path from 'node:path';
|
|
|
5
5
|
import * as gitOps from '../utils/git-ops.js';
|
|
6
6
|
import { GitConflictError } from '../utils/git-ops.js';
|
|
7
7
|
import { withGitRepoLock } from '../utils/git-repo-lock.js';
|
|
8
|
+
import { fingerprintWorktree } from '../utils/git-worktree-fingerprint.js';
|
|
9
|
+
import { withWorkspaceLifecycleGuard } from '../utils/workspace-lifecycle-guard.js';
|
|
8
10
|
import { resolveWorkspaceWorktreePath } from '../utils/worktree-paths.js';
|
|
11
|
+
import { hasController } from './agent/orchestrator.js';
|
|
9
12
|
import { createWorktree, listWorktrees, removeWorktreeUnlocked } from './worktree-service.js';
|
|
10
13
|
/**
|
|
11
14
|
* A `pathCollision.worktreePath` decision is untrusted input reaching
|
|
@@ -114,8 +117,10 @@ function diagnoseChanges(worktreePath) {
|
|
|
114
117
|
/** Classify the workspace, if any, that already tracks this branch.
|
|
115
118
|
* A purged worktree outranks the archived flag: purging always archives, and
|
|
116
119
|
* "recreate the worktree" is the useful offer, not "unarchive". */
|
|
117
|
-
export function resolveWorkspaceState(workspaces, branch) {
|
|
118
|
-
const match = workspaces.find((w) => w.workingBranch === branch
|
|
120
|
+
export function resolveWorkspaceState(workspaces, branch, projectPath) {
|
|
121
|
+
const match = workspaces.find((w) => w.workingBranch === branch &&
|
|
122
|
+
typeof w.projectPath === 'string' &&
|
|
123
|
+
path.resolve(w.projectPath) === path.resolve(projectPath));
|
|
119
124
|
if (!match)
|
|
120
125
|
return { state: 'none' };
|
|
121
126
|
if (match.worktreePurgedAt)
|
|
@@ -134,10 +139,13 @@ function safeRevParse(repoPath, ref) {
|
|
|
134
139
|
}
|
|
135
140
|
}
|
|
136
141
|
/** Hash the observed state so `/resolve` can refuse a plan built on stale facts. */
|
|
137
|
-
export function computeFingerprint(report) {
|
|
142
|
+
export async function computeFingerprint(report) {
|
|
138
143
|
const localHead = safeRevParse(report.projectPath, report.headBranch);
|
|
139
144
|
const remoteHead = safeRevParse(report.projectPath, `origin/${report.headBranch}`);
|
|
140
145
|
const payload = JSON.stringify({
|
|
146
|
+
contents: report.worktree.state === 'orphan' || report.worktree.state === 'attached'
|
|
147
|
+
? await fingerprintWorktree(report.worktree.path)
|
|
148
|
+
: null,
|
|
141
149
|
localHead,
|
|
142
150
|
remoteHead,
|
|
143
151
|
worktree: report.worktree,
|
|
@@ -174,7 +182,7 @@ export function diagnoseLocalState(projectPath, headBranch, worktreesPath, attac
|
|
|
174
182
|
headBranch,
|
|
175
183
|
targetWorktreePath,
|
|
176
184
|
blockers,
|
|
177
|
-
workspace: resolveWorkspaceState(workspaces, headBranch),
|
|
185
|
+
workspace: resolveWorkspaceState(workspaces, headBranch, projectPath),
|
|
178
186
|
worktree,
|
|
179
187
|
localChanges: inspectPath ? diagnoseChanges(inspectPath) : EMPTY_CHANGES,
|
|
180
188
|
ongoingOperation: inspectPath ? gitOps.getOngoingGitOperation(inspectPath) : null,
|
|
@@ -249,14 +257,35 @@ export class StaleDiagnosisError extends Error {
|
|
|
249
257
|
* or a source-branch change.
|
|
250
258
|
*/
|
|
251
259
|
export function resolvePrCheckout(input) {
|
|
260
|
+
const initial = diagnoseLocalState(input.projectPath, input.headBranch, input.worktreesPath, input.attachedPaths, input.workspaces);
|
|
261
|
+
const owner = initial.worktree.state === 'attached'
|
|
262
|
+
? initial.worktree.workspaceId
|
|
263
|
+
: initial.workspace.state !== 'none'
|
|
264
|
+
? initial.workspace.id
|
|
265
|
+
: null;
|
|
266
|
+
const resolve = () => resolvePrCheckoutLocked(input, owner);
|
|
267
|
+
return owner ? withWorkspaceLifecycleGuard(owner, resolve) : resolve();
|
|
268
|
+
}
|
|
269
|
+
function resolvePrCheckoutLocked(input, owner) {
|
|
252
270
|
return withGitRepoLock(input.projectPath, async () => {
|
|
271
|
+
if (owner && hasController(owner))
|
|
272
|
+
throw new Error('Stop the workspace agent before changing its checkout');
|
|
253
273
|
const applied = [];
|
|
254
274
|
// 1. Refresh, then refuse a plan built on facts that no longer hold.
|
|
255
275
|
gitOps.fetchSourceBranch(input.projectPath, input.headBranch);
|
|
256
276
|
applied.push({ kind: 'fetch', detail: `origin/${input.headBranch}` });
|
|
257
277
|
const fresh = diagnoseLocalState(input.projectPath, input.headBranch, input.worktreesPath, input.attachedPaths ?? new Map(), input.workspaces ?? []);
|
|
258
|
-
if (computeFingerprint(fresh) !== input.fingerprint)
|
|
278
|
+
if ((await computeFingerprint(fresh)) !== input.fingerprint)
|
|
259
279
|
throw new StaleDiagnosisError(fresh);
|
|
280
|
+
if ((fresh.worktree.state === 'orphan' || fresh.worktree.state === 'attached') &&
|
|
281
|
+
input.decisions.orphanWorktree === 'create-elsewhere') {
|
|
282
|
+
throw new Error('Branch is already checked out. Attach its existing worktree instead.');
|
|
283
|
+
}
|
|
284
|
+
if (fresh.localChanges.present &&
|
|
285
|
+
(input.decisions.localChanges ?? 'keep') === 'keep' &&
|
|
286
|
+
input.decisions.divergence === 'reset-hard') {
|
|
287
|
+
throw new Error('Cannot preserve local changes during a hard reset. Stash or commit them first, or keep the branch.');
|
|
288
|
+
}
|
|
260
289
|
// 2. Clear stale worktree metadata before anything needs the path.
|
|
261
290
|
if (fresh.worktree.state === 'stale-metadata') {
|
|
262
291
|
git(input.projectPath, ['worktree', 'prune']);
|
|
@@ -2,7 +2,15 @@ import { getDb } from '../db/index.js';
|
|
|
2
2
|
import { emitEphemeral } from './websocket-service.js';
|
|
3
3
|
import { getWorkspace } from './workspace-service.js';
|
|
4
4
|
const timers = new Map();
|
|
5
|
+
let suspended = false;
|
|
5
6
|
let onFireCallback = null;
|
|
7
|
+
/** Stop in-memory delivery without consuming the schedules needed by the next boot. */
|
|
8
|
+
export function suspendForShutdown() {
|
|
9
|
+
suspended = true;
|
|
10
|
+
for (const timer of timers.values())
|
|
11
|
+
clearTimeout(timer);
|
|
12
|
+
timers.clear();
|
|
13
|
+
}
|
|
6
14
|
function rowToPending(row) {
|
|
7
15
|
return {
|
|
8
16
|
workspaceId: row.workspace_id,
|
|
@@ -26,10 +34,9 @@ export function arm(workspaceId, delayMs, meta) {
|
|
|
26
34
|
const db = getDb();
|
|
27
35
|
const now = new Date();
|
|
28
36
|
const targetAt = new Date(now.getTime() + delayMs).toISOString();
|
|
29
|
-
const
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
const retryCount = (existing?.retry_count ?? 0) + 1;
|
|
37
|
+
const retryCount = meta.retryCount;
|
|
38
|
+
if (!Number.isSafeInteger(retryCount) || retryCount < 1)
|
|
39
|
+
throw new Error('retryCount must be a positive integer');
|
|
33
40
|
db.prepare(`INSERT INTO pending_quota_backoffs (workspace_id, target_at, resets_at, source, reason, retry_count, created_at)
|
|
34
41
|
VALUES (?, ?, ?, ?, ?, ?, ?)
|
|
35
42
|
ON CONFLICT(workspace_id) DO UPDATE SET
|
|
@@ -42,9 +49,12 @@ export function arm(workspaceId, delayMs, meta) {
|
|
|
42
49
|
const previous = timers.get(workspaceId);
|
|
43
50
|
if (previous)
|
|
44
51
|
clearTimeout(previous);
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
52
|
+
timers.delete(workspaceId);
|
|
53
|
+
if (!suspended) {
|
|
54
|
+
const timer = setTimeout(() => fireOrSkip(workspaceId), Math.max(0, delayMs));
|
|
55
|
+
timer.unref?.();
|
|
56
|
+
timers.set(workspaceId, timer);
|
|
57
|
+
}
|
|
48
58
|
emitEphemeral(workspaceId, 'agent:quota-backoff', {
|
|
49
59
|
targetAt,
|
|
50
60
|
resetsAt: meta.resetsAt,
|
|
@@ -90,6 +100,8 @@ export function setOnFireCallback(fn) {
|
|
|
90
100
|
* archived or missing workspaces are deleted without firing.
|
|
91
101
|
*/
|
|
92
102
|
export function restoreOnBoot(onFire) {
|
|
103
|
+
suspendForShutdown();
|
|
104
|
+
suspended = false;
|
|
93
105
|
setOnFireCallback(onFire);
|
|
94
106
|
const db = getDb();
|
|
95
107
|
const rows = db.prepare('SELECT * FROM pending_quota_backoffs').all();
|
|
@@ -107,7 +119,12 @@ export function restoreOnBoot(onFire) {
|
|
|
107
119
|
}
|
|
108
120
|
/** Internal — invoked when a timer fires. */
|
|
109
121
|
function fireOrSkip(workspaceId) {
|
|
122
|
+
if (suspended)
|
|
123
|
+
return;
|
|
110
124
|
timers.delete(workspaceId);
|
|
125
|
+
const pending = getPending(workspaceId);
|
|
126
|
+
if (!pending)
|
|
127
|
+
return;
|
|
111
128
|
// Final archive check before firing — workspace might have been archived
|
|
112
129
|
// between the timer being armed and now.
|
|
113
130
|
const ws = getWorkspace(workspaceId);
|
|
@@ -124,7 +141,7 @@ function fireOrSkip(workspaceId) {
|
|
|
124
141
|
const cb = onFireCallback;
|
|
125
142
|
if (!cb)
|
|
126
143
|
return;
|
|
127
|
-
cb(workspaceId);
|
|
144
|
+
cb(workspaceId, pending);
|
|
128
145
|
}
|
|
129
146
|
/** @internal test-only */
|
|
130
147
|
export const _timers = timers;
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { getDb } from '../db/index.js';
|
|
2
2
|
import { slugifyProjectName } from '../utils/project-slug.js';
|
|
3
|
+
import { isWorkspaceLifecycleBusy } from '../utils/workspace-lifecycle-guard.js';
|
|
3
4
|
import { resolveWorkspaceWorktreePath } from '../utils/worktree-paths.js';
|
|
4
5
|
import * as orchestrator from './agent/orchestrator.js';
|
|
5
6
|
import * as settingsService from './settings-service.js';
|
|
@@ -12,6 +13,14 @@ const AUTONOMOUS_LOOP_SENTINEL = '<<autonomous-loop-dynamic>>';
|
|
|
12
13
|
const AUTONOMOUS_LOOP_FALLBACK_PROMPT = 'Continue where you left off.';
|
|
13
14
|
/** In-memory timers — cleared on cancel/fire; rebuilt on boot via rehydrate. */
|
|
14
15
|
const timers = new Map();
|
|
16
|
+
let suspended = false;
|
|
17
|
+
/** Preserve persisted wakeups while preventing delivery during teardown. */
|
|
18
|
+
export function suspendForShutdown() {
|
|
19
|
+
suspended = true;
|
|
20
|
+
for (const timer of timers.values())
|
|
21
|
+
clearTimeout(timer);
|
|
22
|
+
timers.clear();
|
|
23
|
+
}
|
|
15
24
|
function clamp(n, lo, hi) {
|
|
16
25
|
return Math.max(lo, Math.min(hi, n));
|
|
17
26
|
}
|
|
@@ -54,9 +63,12 @@ export function schedule(workspaceId, delaySeconds, prompt, reason, agentSession
|
|
|
54
63
|
db.prepare(`INSERT OR REPLACE INTO pending_wakeups
|
|
55
64
|
(workspace_id, target_at, prompt, reason, created_at, agent_session_id)
|
|
56
65
|
VALUES (?, ?, ?, ?, ?, ?)`).run(workspaceId, targetAtIso, effectivePrompt, reason ?? null, new Date().toISOString(), agentSessionId ?? null);
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
66
|
+
timers.delete(workspaceId);
|
|
67
|
+
if (!suspended) {
|
|
68
|
+
const timeout = setTimeout(() => fire(workspaceId), clampedSeconds * 1000);
|
|
69
|
+
timeout.unref?.();
|
|
70
|
+
timers.set(workspaceId, timeout);
|
|
71
|
+
}
|
|
60
72
|
emitEphemeral(workspaceId, 'wakeup:scheduled', { targetAt: targetAtIso, reason });
|
|
61
73
|
}
|
|
62
74
|
catch (err) {
|
|
@@ -105,6 +117,8 @@ export function isWakeupScheduled(workspaceId) {
|
|
|
105
117
|
}
|
|
106
118
|
/** Re-register timers for rows persisted across restart. Skips stale entries. */
|
|
107
119
|
export function rehydrate() {
|
|
120
|
+
suspendForShutdown();
|
|
121
|
+
suspended = false;
|
|
108
122
|
try {
|
|
109
123
|
const db = getDb();
|
|
110
124
|
const rows = db.prepare('SELECT * FROM pending_wakeups').all();
|
|
@@ -139,12 +153,18 @@ export function rehydrate() {
|
|
|
139
153
|
}
|
|
140
154
|
/** Internal — invoked by setTimeout. */
|
|
141
155
|
function fire(workspaceId) {
|
|
156
|
+
if (suspended)
|
|
157
|
+
return;
|
|
142
158
|
try {
|
|
143
159
|
const db = getDb();
|
|
144
160
|
const row = db.prepare('SELECT * FROM pending_wakeups WHERE workspace_id = ?').get(workspaceId);
|
|
145
161
|
timers.delete(workspaceId);
|
|
146
162
|
if (!row)
|
|
147
163
|
return;
|
|
164
|
+
if (isWorkspaceLifecycleBusy(workspaceId)) {
|
|
165
|
+
defer(workspaceId, row);
|
|
166
|
+
return;
|
|
167
|
+
}
|
|
148
168
|
if (orchestrator.hasController(workspaceId)) {
|
|
149
169
|
// Claude can keep the stream open after a result while a background
|
|
150
170
|
// task runs. Waiting for that controller to disappear deadlocks a
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { execFile } from 'node:child_process';
|
|
2
2
|
import fs from 'node:fs';
|
|
3
3
|
import { promisify } from 'node:util';
|
|
4
|
+
import { assertAgentStopped } from '../utils/agent-stop-result.js';
|
|
4
5
|
import { logError, logWarn } from '../utils/logger.js';
|
|
5
6
|
import { withWorkspaceLifecycleGuard } from '../utils/workspace-lifecycle-guard.js';
|
|
6
7
|
import * as agentManager from './agent/orchestrator.js';
|
|
@@ -29,19 +30,14 @@ async function purgeWorktreeGuarded(workspaceId, expectedArchivedAt) {
|
|
|
29
30
|
const warnings = [];
|
|
30
31
|
// `removeWorktree` runs later in this same function — the agent must be dead
|
|
31
32
|
// by then, not merely asked to stop.
|
|
32
|
-
|
|
33
|
-
await agentManager.stopAgentAndWait(workspaceId, undefined, 'purge');
|
|
34
|
-
}
|
|
35
|
-
catch (err) {
|
|
36
|
-
const msg = err instanceof Error ? err.message : String(err);
|
|
37
|
-
logError('purge', `stopAgent failed for '${workspace.name}'`, { workspaceId: workspace.id, error: msg });
|
|
38
|
-
}
|
|
33
|
+
assertAgentStopped(await agentManager.stopAgentAndWait(workspaceId, undefined, 'purge'));
|
|
39
34
|
try {
|
|
40
35
|
await devServerService.stopDevServer(workspaceId);
|
|
41
36
|
}
|
|
42
37
|
catch (err) {
|
|
43
38
|
const msg = err instanceof Error ? err.message : String(err);
|
|
44
39
|
logError('purge', `stopDevServer failed for '${workspace.name}'`, { workspaceId: workspace.id, error: msg });
|
|
40
|
+
throw err;
|
|
45
41
|
}
|
|
46
42
|
try {
|
|
47
43
|
destroyTerminal(workspaceId);
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
export class AgentStopError extends Error {
|
|
2
|
+
outcome;
|
|
3
|
+
code = 'agent-stop-incomplete';
|
|
4
|
+
constructor(outcome) {
|
|
5
|
+
super(`Agent stop is not confirmed (${outcome}). Wait for it to stop and retry.`);
|
|
6
|
+
this.outcome = outcome;
|
|
7
|
+
}
|
|
8
|
+
}
|
|
9
|
+
export function assertAgentStopped(outcome) {
|
|
10
|
+
if (outcome !== 'stopped' && outcome !== 'not-running')
|
|
11
|
+
throw new AgentStopError(outcome);
|
|
12
|
+
}
|
|
@@ -669,7 +669,13 @@ export function listProperCommits(repoPath, workingBranch, newBase, oldBase) {
|
|
|
669
669
|
* `repoPath`. The caller (and the Kōbō worktree orchestrator) must ensure
|
|
670
670
|
* `workingBranch` is the active branch — do NOT add a `git checkout` here.
|
|
671
671
|
*/
|
|
672
|
+
export function assertCurrentBranch(repoPath, expected) {
|
|
673
|
+
const actual = getCurrentBranch(repoPath);
|
|
674
|
+
if (actual !== expected)
|
|
675
|
+
throw new Error(`Expected checkout '${expected}', found '${actual || 'detached HEAD'}'`);
|
|
676
|
+
}
|
|
672
677
|
export function reconstructBranchOnto(repoPath, workingBranch, newBase, commits) {
|
|
678
|
+
assertCurrentBranch(repoPath, workingBranch);
|
|
673
679
|
const baseRef = resolveBase(repoPath, newBase);
|
|
674
680
|
const backupBranch = `kobo-backup/${workingBranch}-${Date.now()}`;
|
|
675
681
|
git(repoPath, ['branch', backupBranch, workingBranch]);
|
|
@@ -749,8 +755,9 @@ export function pruneBackupBranches(repoPath, workingBranch, keep = 3) {
|
|
|
749
755
|
}
|
|
750
756
|
/** Abort any in-progress operation, then hard-reset `workingBranch` to a backup branch. */
|
|
751
757
|
export function restoreBranchFromBackup(repoPath, workingBranch, backupBranch) {
|
|
758
|
+
assertCurrentBranch(repoPath, workingBranch);
|
|
752
759
|
abortOngoingGitOperation(repoPath);
|
|
753
|
-
|
|
760
|
+
assertCurrentBranch(repoPath, workingBranch);
|
|
754
761
|
git(repoPath, ['reset', '--hard', backupBranch]);
|
|
755
762
|
}
|
|
756
763
|
/** Return structured diff shortstat between two refs (three-dot merge base). */
|
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
import { execFileSync, spawn } from 'node:child_process';
|
|
2
|
+
import { createHash } from 'node:crypto';
|
|
3
|
+
import { constants } from 'node:fs';
|
|
4
|
+
import { lstat, open, readlink, realpath } from 'node:fs/promises';
|
|
5
|
+
import path from 'node:path';
|
|
6
|
+
/** Hash diffs and untracked contents without loading large files into memory. */
|
|
7
|
+
export async function fingerprintWorktree(cwd) {
|
|
8
|
+
return fingerprintRepository(await realpath(cwd), new Set());
|
|
9
|
+
}
|
|
10
|
+
/** Nested repositories contribute HEAD and their own Git-filtered working state, never a .git traversal. */
|
|
11
|
+
async function fingerprintRepository(cwd, visited) {
|
|
12
|
+
if (visited.has(cwd))
|
|
13
|
+
throw new Error('Cannot fingerprint a recursive repository path');
|
|
14
|
+
visited.add(cwd);
|
|
15
|
+
const gitOptions = { cwd, encoding: 'utf8', timeout: 30_000, maxBuffer: 16 * 1024 * 1024 };
|
|
16
|
+
const topLevel = execFileSync('git', ['rev-parse', '--show-toplevel'], gitOptions).trim();
|
|
17
|
+
if ((await realpath(topLevel)) !== cwd)
|
|
18
|
+
throw new Error('Working tree changed during fingerprinting; retry diagnosis');
|
|
19
|
+
const hash = createHash('sha256');
|
|
20
|
+
let head;
|
|
21
|
+
try {
|
|
22
|
+
head = execFileSync('git', ['rev-parse', '--verify', '--quiet', 'HEAD'], gitOptions).trim();
|
|
23
|
+
}
|
|
24
|
+
catch (err) {
|
|
25
|
+
// A freshly initialized nested repository has no commit yet. Other Git failures remain fatal.
|
|
26
|
+
if (err.status !== 1)
|
|
27
|
+
throw err;
|
|
28
|
+
head = 'unborn';
|
|
29
|
+
}
|
|
30
|
+
hash.update(`HEAD\0${head}\0`);
|
|
31
|
+
for (const staged of [false, true]) {
|
|
32
|
+
hash.update(staged ? 'index\0' : 'working-tree\0');
|
|
33
|
+
await new Promise((resolve, reject) => {
|
|
34
|
+
const child = spawn('git', ['diff', '--no-ext-diff', '--no-textconv', '--binary', '--no-color', ...(staged ? ['--cached'] : []), '--'], { cwd, stdio: ['ignore', 'pipe', 'pipe'], timeout: 30_000 });
|
|
35
|
+
child.stdout.on('data', (chunk) => hash.update(chunk));
|
|
36
|
+
child.stderr.resume();
|
|
37
|
+
child.on('error', reject);
|
|
38
|
+
child.on('close', (code) => (code === 0 ? resolve() : reject(new Error('Cannot fingerprint working tree diff'))));
|
|
39
|
+
});
|
|
40
|
+
}
|
|
41
|
+
const names = execFileSync('git', ['ls-files', '--others', '--exclude-standard', '-z'], {
|
|
42
|
+
cwd,
|
|
43
|
+
encoding: 'utf8',
|
|
44
|
+
timeout: 30_000,
|
|
45
|
+
maxBuffer: 16 * 1024 * 1024,
|
|
46
|
+
})
|
|
47
|
+
.split('\0')
|
|
48
|
+
.filter(Boolean)
|
|
49
|
+
.sort();
|
|
50
|
+
for (const name of names) {
|
|
51
|
+
hash.update(`\0untracked\0${name}\0`);
|
|
52
|
+
// Remove Git's trailing slash for nested repos before lstat, so a link is never dereferenced.
|
|
53
|
+
const filePath = path.resolve(cwd, name);
|
|
54
|
+
const stat = await lstat(filePath);
|
|
55
|
+
hash.update(`${stat.mode}\0`);
|
|
56
|
+
if (stat.isSymbolicLink()) {
|
|
57
|
+
hash.update(await readlink(filePath));
|
|
58
|
+
}
|
|
59
|
+
else if (stat.isFile()) {
|
|
60
|
+
const file = await open(filePath, constants.O_RDONLY | constants.O_NOFOLLOW);
|
|
61
|
+
try {
|
|
62
|
+
const before = await file.stat();
|
|
63
|
+
for await (const chunk of file.createReadStream({ autoClose: false }))
|
|
64
|
+
hash.update(chunk);
|
|
65
|
+
const after = await file.stat();
|
|
66
|
+
if (before.size !== after.size || before.mtimeMs !== after.mtimeMs || before.ctimeMs !== after.ctimeMs)
|
|
67
|
+
throw new Error('Working tree changed during fingerprinting; retry diagnosis');
|
|
68
|
+
}
|
|
69
|
+
finally {
|
|
70
|
+
await file.close();
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
else if (stat.isDirectory()) {
|
|
74
|
+
// `ls-files --others` reports an embedded repository as a directory instead of its files.
|
|
75
|
+
const nestedPath = await realpath(filePath);
|
|
76
|
+
const relative = path.relative(cwd, nestedPath);
|
|
77
|
+
if (!relative || relative === '..' || relative.startsWith(`..${path.sep}`) || path.isAbsolute(relative))
|
|
78
|
+
throw new Error('Nested repository escapes the working tree');
|
|
79
|
+
hash.update(`\0repository\0${await fingerprintRepository(nestedPath, visited)}\0`);
|
|
80
|
+
}
|
|
81
|
+
else {
|
|
82
|
+
throw new Error('Cannot fingerprint a non-regular untracked file');
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
return hash.digest('hex');
|
|
86
|
+
}
|
|
@@ -66,3 +66,24 @@ function assertLeafIsNotSymlink(candidate) {
|
|
|
66
66
|
throw err;
|
|
67
67
|
}
|
|
68
68
|
}
|
|
69
|
+
/** Create storage directories without accepting symlinked path components. */
|
|
70
|
+
export function ensureDirectoryInside(rootPath, relativePath) {
|
|
71
|
+
assertPathInside(rootPath, relativePath);
|
|
72
|
+
const root = fs.realpathSync(rootPath);
|
|
73
|
+
let current = root;
|
|
74
|
+
for (const segment of relativePath.split(path.sep).filter(Boolean)) {
|
|
75
|
+
current = path.join(current, segment);
|
|
76
|
+
try {
|
|
77
|
+
fs.mkdirSync(current);
|
|
78
|
+
}
|
|
79
|
+
catch (err) {
|
|
80
|
+
if (err.code !== 'EEXIST')
|
|
81
|
+
throw err;
|
|
82
|
+
}
|
|
83
|
+
const stat = fs.lstatSync(current);
|
|
84
|
+
if (stat.isSymbolicLink() || !stat.isDirectory())
|
|
85
|
+
throw new Error('Symbolic links are not allowed in storage directories');
|
|
86
|
+
current = resolveExistingPathInside(root, current);
|
|
87
|
+
}
|
|
88
|
+
return current;
|
|
89
|
+
}
|
|
@@ -3,6 +3,7 @@ import fs from 'node:fs';
|
|
|
3
3
|
import path from 'node:path';
|
|
4
4
|
import * as wsService from '../services/websocket-service.js';
|
|
5
5
|
import { getIndexLockPath } from './git-ops.js';
|
|
6
|
+
import { ensureDirectoryInside } from './safe-path.js';
|
|
6
7
|
/** Default wall-clock budget for a user script before it is force-killed. */
|
|
7
8
|
export const SCRIPT_TIMEOUT_MS = 5 * 60 * 1000; // 5 minutes
|
|
8
9
|
/**
|
|
@@ -13,10 +14,31 @@ export const SCRIPT_TIMEOUT_MS = 5 * 60 * 1000; // 5 minutes
|
|
|
13
14
|
export function runScript(opts) {
|
|
14
15
|
const { workspaceId, worktreePath, script, eventPrefix, tmpFileName, env, extraEnv } = opts;
|
|
15
16
|
const timeoutMs = opts.timeoutMs ?? SCRIPT_TIMEOUT_MS;
|
|
17
|
+
let scriptPath;
|
|
18
|
+
let scriptDirectory;
|
|
19
|
+
const cleanup = () => {
|
|
20
|
+
if (scriptPath) {
|
|
21
|
+
try {
|
|
22
|
+
fs.unlinkSync(scriptPath);
|
|
23
|
+
}
|
|
24
|
+
catch {
|
|
25
|
+
/* best effort */
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
if (scriptDirectory) {
|
|
29
|
+
try {
|
|
30
|
+
fs.rmdirSync(scriptDirectory);
|
|
31
|
+
}
|
|
32
|
+
catch {
|
|
33
|
+
/* best effort */
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
};
|
|
16
37
|
return new Promise((resolve) => {
|
|
17
|
-
const
|
|
18
|
-
fs.
|
|
19
|
-
|
|
38
|
+
const storage = ensureDirectoryInside(worktreePath, '.ai');
|
|
39
|
+
scriptDirectory = fs.mkdtempSync(path.join(storage, '.script-'));
|
|
40
|
+
scriptPath = path.join(scriptDirectory, path.basename(tmpFileName));
|
|
41
|
+
fs.writeFileSync(scriptPath, script, { mode: 0o700, flag: 'wx' });
|
|
20
42
|
const proc = spawn('bash', [scriptPath], {
|
|
21
43
|
cwd: worktreePath,
|
|
22
44
|
env: {
|
|
@@ -93,12 +115,7 @@ export function runScript(opts) {
|
|
|
93
115
|
return;
|
|
94
116
|
settled = true;
|
|
95
117
|
clearTimeout(timeout);
|
|
96
|
-
|
|
97
|
-
fs.unlinkSync(scriptPath);
|
|
98
|
-
}
|
|
99
|
-
catch {
|
|
100
|
-
/* best-effort */
|
|
101
|
-
}
|
|
118
|
+
cleanup();
|
|
102
119
|
if (exitCode === 0) {
|
|
103
120
|
wsService.emitEphemeral(workspaceId, `${eventPrefix}:complete`, { hadOutput: outputEmitted });
|
|
104
121
|
}
|
|
@@ -119,5 +136,11 @@ export function runScript(opts) {
|
|
|
119
136
|
proc.on('close', (code) => {
|
|
120
137
|
finish(code ?? 1);
|
|
121
138
|
});
|
|
139
|
+
}).catch((err) => {
|
|
140
|
+
cleanup();
|
|
141
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
142
|
+
wsService.emit(workspaceId, `${eventPrefix}:output`, { text: `[kobo] Script failed to start: ${message}` });
|
|
143
|
+
wsService.emitEphemeral(workspaceId, `${eventPrefix}:error`, { exitCode: 1, message });
|
|
144
|
+
return { exitCode: 1 };
|
|
122
145
|
});
|
|
123
146
|
}
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
const busyWorkspaces = new Set();
|
|
2
|
+
const deferred = new Map();
|
|
2
3
|
export class WorkspaceLifecycleBusyError extends Error {
|
|
3
4
|
code = 'workspace-busy';
|
|
4
5
|
constructor() {
|
|
@@ -8,6 +9,19 @@ export class WorkspaceLifecycleBusyError extends Error {
|
|
|
8
9
|
export function isWorkspaceLifecycleBusy(id) {
|
|
9
10
|
return busyWorkspaces.has(id);
|
|
10
11
|
}
|
|
12
|
+
export function assertWorkspaceLifecycleAvailable(id) {
|
|
13
|
+
if (busyWorkspaces.has(id))
|
|
14
|
+
throw new WorkspaceLifecycleBusyError();
|
|
15
|
+
}
|
|
16
|
+
/** Keep automatic work pending without polling or acquiring the guard recursively. */
|
|
17
|
+
export function deferUntilWorkspaceAvailable(id, retry) {
|
|
18
|
+
if (!busyWorkspaces.has(id))
|
|
19
|
+
return false;
|
|
20
|
+
const callbacks = deferred.get(id) ?? new Set();
|
|
21
|
+
callbacks.add(retry);
|
|
22
|
+
deferred.set(id, callbacks);
|
|
23
|
+
return true;
|
|
24
|
+
}
|
|
11
25
|
/** Exclude competing restore/purge/delete operations, including their awaits. */
|
|
12
26
|
export async function withWorkspaceLifecycleGuard(id, action) {
|
|
13
27
|
if (busyWorkspaces.has(id))
|
|
@@ -18,5 +32,18 @@ export async function withWorkspaceLifecycleGuard(id, action) {
|
|
|
18
32
|
}
|
|
19
33
|
finally {
|
|
20
34
|
busyWorkspaces.delete(id);
|
|
35
|
+
const callbacks = deferred.get(id);
|
|
36
|
+
deferred.delete(id);
|
|
37
|
+
if (callbacks)
|
|
38
|
+
queueMicrotask(() => {
|
|
39
|
+
for (const retry of callbacks) {
|
|
40
|
+
try {
|
|
41
|
+
retry();
|
|
42
|
+
}
|
|
43
|
+
catch (err) {
|
|
44
|
+
console.error('[workspace-lifecycle] deferred operation failed:', err);
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
});
|
|
21
48
|
}
|
|
22
49
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@loicngr/kobo",
|
|
3
|
-
"version": "1.11.
|
|
3
|
+
"version": "1.11.37",
|
|
4
4
|
"description": "Kōbō — multi-workspace agent manager for Claude Code. Orchestrates isolated git worktrees with dev servers, Notion integration, and MCP tools.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"license": "GPL-3.0-or-later",
|
|
@@ -1 +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-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-01cd9f2a]{width:100%;height:100%;position:relative}.activity-feed-scroll[data-v-01cd9f2a]{width:100%;height:100%}.activity-feed-nav-cluster[data-v-01cd9f2a]{z-index:2;align-items:center;gap:8px;display:flex;position:absolute;bottom:14px;right:14px}.activity-feed-compacting[data-v-01cd9f2a]{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-01cd9f2a],.fade-leave-active[data-v-01cd9f2a]{transition:opacity .16s}.fade-enter-from[data-v-01cd9f2a],.fade-leave-to[data-v-01cd9f2a]{opacity:0}.activity-feed-nav-btn[data-v-01cd9f2a]{opacity:.8;transition:opacity .12s}.activity-feed-nav-btn[data-v-01cd9f2a]:hover{opacity:1}.activity-feed-scroll[data-v-01cd9f2a] .q-scrollarea__content{max-width:100%;overflow-x:hidden}.activity-feed-switching[data-v-01cd9f2a]{justify-content:center;align-items:center;width:100%;height:100%;display:flex}.activity-feed-empty[data-v-01cd9f2a]{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-01cd9f2a]{font-family:var(--kobo-font-sans);color:var(--kobo-text-2);font-size:14px;font-weight:500}.activity-feed-empty__hint[data-v-01cd9f2a]{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-d83f0940]{width:100%;height:100%;position:relative}.activity-feed-scroll[data-v-d83f0940]{width:100%;height:100%}.activity-feed-nav-cluster[data-v-d83f0940]{z-index:2;align-items:center;gap:8px;display:flex;position:absolute;bottom:14px;right:14px}.activity-feed-compacting[data-v-d83f0940]{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-d83f0940],.fade-leave-active[data-v-d83f0940]{transition:opacity .16s}.fade-enter-from[data-v-d83f0940],.fade-leave-to[data-v-d83f0940]{opacity:0}.activity-feed-nav-btn[data-v-d83f0940]{opacity:.8;transition:opacity .12s}.activity-feed-nav-btn[data-v-d83f0940]:hover{opacity:1}.activity-feed-scroll[data-v-d83f0940] .q-scrollarea__content{max-width:100%;overflow-x:hidden}.activity-feed-switching[data-v-d83f0940]{justify-content:center;align-items:center;width:100%;height:100%;display:flex}.activity-feed-empty[data-v-d83f0940]{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-d83f0940]{font-family:var(--kobo-font-sans);color:var(--kobo-text-2);font-size:14px;font-weight:500}.activity-feed-empty__hint[data-v-d83f0940]{font-family:var(--kobo-font-sans);color:var(--kobo-text-3);max-width:380px;font-size:13px}
|