@loicngr/kobo 1.11.39 → 1.11.41
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 +101 -32
- package/CHANGELOG.md +8 -0
- package/README.md +6 -4
- package/dist/mcp-server/kobo-tasks-handlers.js +16 -9
- package/dist/mcp-server/kobo-tasks-server.js +13 -2
- package/dist/mcp-server/workspace-dialogue-client.js +28 -0
- package/dist/server/db/mcp-message-schema.js +16 -0
- package/dist/server/db/migrations.js +4 -0
- package/dist/server/db/schema.js +4 -0
- package/dist/server/db/search-schema.js +53 -0
- package/dist/server/index.js +17 -86
- package/dist/server/middleware/network-auth-middleware.js +5 -1
- package/dist/server/routes/changelog.js +4 -49
- package/dist/server/routes/git.js +2 -2
- package/dist/server/routes/images.js +65 -5
- package/dist/server/routes/mcp.js +47 -0
- package/dist/server/routes/pull-requests.js +1 -1
- package/dist/server/routes/search.js +13 -4
- package/dist/server/routes/settings.js +5 -0
- package/dist/server/routes/workspaces.js +250 -248
- package/dist/server/services/agent/engines/claude-code/engine.js +31 -33
- package/dist/server/services/agent/engines/codex/engine.js +37 -8
- package/dist/server/services/agent/engines/codex/event-mapper.js +3 -1
- package/dist/server/services/agent/engines/codex/options-builder.js +4 -3
- package/dist/server/services/agent/engines/codex/server-requests.js +8 -1
- package/dist/server/services/agent/orchestrator.js +20 -3
- package/dist/server/services/attachment-service.js +122 -0
- package/dist/server/services/change-source-branch-service.js +10 -42
- package/dist/server/services/chat-receipt-service.js +33 -0
- package/dist/server/services/cron-service.js +27 -1
- package/dist/server/services/image-service.js +16 -7
- package/dist/server/services/mcp-client-context.js +12 -0
- package/dist/server/services/mcp-connection-info-service.js +63 -0
- package/dist/server/services/mcp-message-delivery-service.js +59 -0
- package/dist/server/services/mcp-message-request-service.js +64 -0
- package/dist/server/services/pr-checkout-service.js +10 -10
- package/dist/server/services/pr-watcher-service.js +14 -9
- package/dist/server/services/search/indexer.js +232 -0
- package/dist/server/services/search/worker.js +76 -0
- package/dist/server/services/search-service.js +104 -110
- package/dist/server/services/update-check-service.js +119 -0
- package/dist/server/services/websocket-service.js +121 -35
- package/dist/server/services/workspace-dialogue-service.js +128 -0
- package/dist/server/services/workspace-message-service.js +58 -0
- package/dist/server/services/workspace-permission-policy-service.js +33 -3
- package/dist/server/services/worktree-restore-service.js +2 -2
- package/dist/server/services/worktree-service.js +197 -124
- package/dist/server/utils/bounded-process.js +137 -0
- package/dist/server/utils/git-ops.js +76 -128
- package/dist/server/utils/git-repo-lock.js +27 -13
- package/dist/shared/attachments.js +58 -0
- package/dist/shared/workspace-dialogue-tools.js +89 -0
- package/dist/shared/workspace-message-types.js +19 -0
- package/package.json +2 -1
- package/src/client/dist/pwa/assets/{ActivityFeed-Chsw1Mh7.css → ActivityFeed-BYR5x7aa.css} +1 -1
- package/src/client/dist/pwa/assets/ActivityFeed-DTIe6O9o.js +8 -0
- package/src/client/dist/pwa/assets/{ChangelogPage-D8563008.js → ChangelogPage-BWeu0YeI.js} +1 -1
- package/src/client/dist/pwa/assets/{ComparisonPanel-Bd0X08sg.js → ComparisonPanel-o_EHlTwV.js} +2 -2
- package/src/client/dist/pwa/assets/CreatePage-CGi88k6z.css +1 -0
- package/src/client/dist/pwa/assets/CreatePage-DbY5gBv8.js +2 -0
- package/src/client/dist/pwa/assets/DashboardPage-CIo_gesD.js +1 -0
- package/src/client/dist/pwa/assets/{DiffViewer-DnR79ycb.css → DiffViewer-DEPOa3D7.css} +1 -1
- package/src/client/dist/pwa/assets/DiffViewer-DEcf48KI.js +8 -0
- package/src/client/dist/pwa/assets/{DocumentsPanel-BJsJ0fNs.js → DocumentsPanel-D9vcB91W.js} +1 -1
- package/src/client/dist/pwa/assets/{DrawerToggleButton-BVLO5K5d.js → DrawerToggleButton-Ca0_CxSl.js} +1 -1
- package/src/client/dist/pwa/assets/GitPanel-DCmB2R5i.js +3 -0
- package/src/client/dist/pwa/assets/{HealthPage-RFZS7Lao.js → HealthPage-_q3vy_xn.js} +1 -1
- package/src/client/dist/pwa/assets/MainLayout-Duwnq3cc.js +2 -0
- package/src/client/dist/pwa/assets/{QBtnDropdown-X8oNU5Mb.js → QBtnDropdown-BeiZIHVA.js} +1 -1
- package/src/client/dist/pwa/assets/{QDrawer-Cij17skX.js → QDrawer-Fj3M9QGD.js} +1 -1
- package/src/client/dist/pwa/assets/QSelect-DtSUuK8t.js +1 -0
- package/src/client/dist/pwa/assets/{QTooltip-CwcCGSWq.js → QTooltip-DwkX8eQ6.js} +1 -1
- package/src/client/dist/pwa/assets/SchedulePanel-BAA4Aajn.css +1 -0
- package/src/client/dist/pwa/assets/SchedulePanel-BlZDd7TK.js +1 -0
- package/src/client/dist/pwa/assets/SearchPage-BGc1oQSM.css +1 -0
- package/src/client/dist/pwa/assets/SearchPage-BGi1yRxU.js +1 -0
- package/src/client/dist/pwa/assets/SettingsPage-DnO-ImWE.js +9 -0
- package/src/client/dist/pwa/assets/SettingsPage-sVlsJQ0b.css +1 -0
- package/src/client/dist/pwa/assets/{SplitWorkspacePage-D7lGFRKs.js → SplitWorkspacePage-vz6jN1DM.js} +1 -1
- package/src/client/dist/pwa/assets/{TerminalPanel-DI8DHb9e.js → TerminalPanel-IdPIGp2_.js} +9 -9
- package/src/client/dist/pwa/assets/{TouchPan-B3nrkCY8.js → TouchPan-DTJqxVp9.js} +1 -1
- package/src/client/dist/pwa/assets/{TourReplayButton-DU1UShMY.js → TourReplayButton-Bj2GGyhU.js} +1 -1
- package/src/client/dist/pwa/assets/{WorkspaceAttentionLabels-C-yN5IX8.js → WorkspaceAttentionLabels-BBaGZlwv.js} +1 -1
- package/src/client/dist/pwa/assets/WorkspacePage-DCg9zSxz.css +1 -0
- package/src/client/dist/pwa/assets/WorkspacePage-Dy0spsfi.js +4 -0
- package/src/client/dist/pwa/assets/{browser-f9MHQjfi.js → browser-BFj5wwbS.js} +1 -1
- package/src/client/dist/pwa/assets/{build-path-tree-BWBzP8os.js → build-path-tree-C-HSiySk.js} +1 -1
- package/src/client/dist/pwa/assets/de-BdLe7apJ.js +17 -0
- package/src/client/dist/pwa/assets/{documents-C2cLnEBQ.js → documents-C2Xnjh9Z.js} +1 -1
- package/src/client/dist/pwa/assets/{editor.api-sw146MWN.js → editor.api-BbKfnNmv.js} +1 -1
- package/src/client/dist/pwa/assets/{efforts-C5ZZgCH4.js → efforts-BM7fdaaL.js} +1 -1
- package/src/client/dist/pwa/assets/engineFeatures-b74YRODT.js +1 -0
- package/src/client/dist/pwa/assets/es-C1LM-3aq.js +17 -0
- package/src/client/dist/pwa/assets/expand-template-DiDWiIkx.js +1 -0
- package/src/client/dist/pwa/assets/{format-BwWP6Aay.js → format-CnbUxAmw.js} +1 -1
- package/src/client/dist/pwa/assets/{formatters-DyWtO-nR.js → formatters-B2uUIbAY.js} +1 -1
- package/src/client/dist/pwa/assets/fr-KCK6r5ga.js +17 -0
- package/src/client/dist/pwa/assets/{freemarker2-DZ5Hm8UH.js → freemarker2-Cu1vGY0x.js} +1 -1
- package/src/client/dist/pwa/assets/{handlebars-BRUGpTA7.js → handlebars-CETuqLyz.js} +1 -1
- package/src/client/dist/pwa/assets/{html-DNOayNNs.js → html-BTNpr39P.js} +1 -1
- package/src/client/dist/pwa/assets/i18n-BkUirOzl.js +1 -0
- package/src/client/dist/pwa/assets/index-mvh5w1cM.js +20 -0
- package/src/client/dist/pwa/assets/it-BM6t0IGU.js +17 -0
- package/src/client/dist/pwa/assets/{javascript-C0tDQU3z.js → javascript-C7Ncb6V7.js} +1 -1
- package/src/client/dist/pwa/assets/{liquid-CN46O6OF.js → liquid-D0hUdg4x.js} +1 -1
- package/src/client/dist/pwa/assets/{mdx-DM-lrr8Q.js → mdx-BK5H1Xst.js} +1 -1
- package/src/client/dist/pwa/assets/{monaco-contributions-CkN7y-8c.js → monaco-contributions-BBW6JNgz.js} +1 -1
- package/src/client/dist/pwa/assets/{monaco.contribution-2BcAOLjc.js → monaco.contribution-1LZrAlY4.js} +2 -2
- package/src/client/dist/pwa/assets/network-auth-CVGS__6a.js +1 -0
- package/src/client/dist/pwa/assets/{notify-theme-a91Tcr6o.js → notify-theme-DIKLxr8F.js} +1 -1
- package/src/client/dist/pwa/assets/{pending-diff-open-DOtELZfB.js → pending-diff-open-B02zuH1j.js} +1 -1
- package/src/client/dist/pwa/assets/{permissionModes-C-9Crj-c.js → permissionModes-C3otN1Vk.js} +1 -1
- package/src/client/dist/pwa/assets/{project-color-BRe4oyNx.js → project-color-BUcW_eZA.js} +1 -1
- package/src/client/dist/pwa/assets/{python-BRpJG6sZ.js → python-DEeUVvnp.js} +1 -1
- package/src/client/dist/pwa/assets/{razor-CehCmvHl.js → razor-CG6cL8ya.js} +1 -1
- package/src/client/dist/pwa/assets/{render-chat-markdown-DlbQLZ02.js → render-chat-markdown-Ct_T5TPN.js} +1 -1
- package/src/client/dist/pwa/assets/{typescript-Dt_8mVuj.js → typescript-CSQAg5UC.js} +1 -1
- package/src/client/dist/pwa/assets/use-search-index-status-DtLzygcy.js +1 -0
- package/src/client/dist/pwa/assets/use-tours-_6HwJ_Rp.js +2 -0
- package/src/client/dist/pwa/assets/{workspace-templates-GCObcjO6.js → workspace-templates-B8WWsWEI.js} +1 -1
- package/src/client/dist/pwa/assets/{xml-_gOwdzB4.js → xml-BSB4-DXR.js} +1 -1
- package/src/client/dist/pwa/assets/{yaml-BxeBqpGf.js → yaml-CSGKph85.js} +1 -1
- package/src/client/dist/pwa/index.html +1 -1
- package/src/client/dist/pwa/sw.js +1 -1
- package/src/mcp-server/README.md +169 -9
- package/src/mcp-server/kobo-tasks-handlers.ts +14 -11
- package/src/mcp-server/kobo-tasks-server.ts +19 -2
- package/src/mcp-server/workspace-dialogue-client.ts +34 -0
- package/src/client/dist/pwa/assets/ActivityFeed-BzzvcOmq.js +0 -8
- package/src/client/dist/pwa/assets/CreatePage-A3pVradx.css +0 -1
- package/src/client/dist/pwa/assets/CreatePage-Bon8qIka.js +0 -2
- package/src/client/dist/pwa/assets/DashboardPage-DYxdxEul.js +0 -1
- package/src/client/dist/pwa/assets/DiffViewer-BpsLNxnj.js +0 -8
- package/src/client/dist/pwa/assets/GitPanel-Cal_wzC0.js +0 -3
- package/src/client/dist/pwa/assets/MainLayout-vvyiRcql.js +0 -2
- package/src/client/dist/pwa/assets/QSelect-xd-AlGo_.js +0 -1
- package/src/client/dist/pwa/assets/SchedulePanel-BunlzsOY.css +0 -1
- package/src/client/dist/pwa/assets/SchedulePanel-CXRUsz6G.js +0 -1
- package/src/client/dist/pwa/assets/SearchPage-CVgRa025.css +0 -1
- package/src/client/dist/pwa/assets/SearchPage-DopbgLxh.js +0 -1
- package/src/client/dist/pwa/assets/SettingsPage-Do9d0Pov.css +0 -1
- package/src/client/dist/pwa/assets/SettingsPage-Jx6TnOzR.js +0 -9
- package/src/client/dist/pwa/assets/WorkspacePage-BAC6u9OP.css +0 -1
- package/src/client/dist/pwa/assets/WorkspacePage-DKRpdpPg.js +0 -4
- package/src/client/dist/pwa/assets/de-QkDLrcGq.js +0 -17
- package/src/client/dist/pwa/assets/engineFeatures-CAVMDawq.js +0 -1
- package/src/client/dist/pwa/assets/es-BgQFKlo8.js +0 -17
- package/src/client/dist/pwa/assets/expand-template-B6xAlAaM.js +0 -1
- package/src/client/dist/pwa/assets/fr-DAdLWjNM.js +0 -17
- package/src/client/dist/pwa/assets/i18n-CBez_vip.js +0 -1
- package/src/client/dist/pwa/assets/index-VL3fI9a3.js +0 -20
- package/src/client/dist/pwa/assets/it-Bumaewp1.js +0 -17
- package/src/client/dist/pwa/assets/network-auth-KYS4snQH.js +0 -1
- package/src/client/dist/pwa/assets/use-tours-CSVM_54K.js +0 -2
|
@@ -1,17 +1,21 @@
|
|
|
1
1
|
import { execFile as execFileCb, execFileSync, spawn } from 'node:child_process';
|
|
2
2
|
import { promisify } from 'node:util';
|
|
3
|
+
import { getSearchIndexStatus, searchEvents } from '../services/search-service.js';
|
|
3
4
|
import { AgentStopError, assertAgentStopped } from '../utils/agent-stop-result.js';
|
|
4
5
|
import { withGitRepoLock } from '../utils/git-repo-lock.js';
|
|
5
6
|
const execFileAsync = promisify(execFileCb);
|
|
6
7
|
import fs from 'node:fs';
|
|
7
8
|
import path from 'node:path';
|
|
8
9
|
import { Hono } from 'hono';
|
|
10
|
+
import { bodyLimit } from 'hono/body-limit';
|
|
11
|
+
import { MAX_ATTACHMENT_REQUEST_BYTES } from '../../shared/attachments.js';
|
|
9
12
|
import { AUTO_LOOP_HARD_RULES, buildAutoLoopGroomingSteps, buildGroomingIntro } from '../../shared/auto-loop-prompts.js';
|
|
10
13
|
import { getDb } from '../db/index.js';
|
|
11
14
|
import { migrationGuard } from '../middleware/migration-guard.js';
|
|
12
15
|
import { listEngines } from '../services/agent/engines/registry.js';
|
|
13
16
|
import * as agentManager from '../services/agent/orchestrator.js';
|
|
14
17
|
import * as archiveScriptService from '../services/archive-script-service.js';
|
|
18
|
+
import { AttachmentRequestError, attachmentReference, readWorkspaceCreationRequest, removeAttachments, saveAttachments, } from '../services/attachment-service.js';
|
|
15
19
|
import * as autoLoopService from '../services/auto-loop-service.js';
|
|
16
20
|
import { changeSourceBranch } from '../services/change-source-branch-service.js';
|
|
17
21
|
import { listChatHistory, pushChatHistory } from '../services/chat-history-service.js';
|
|
@@ -59,7 +63,7 @@ function workspaceErrorStatus(err) {
|
|
|
59
63
|
/** Hono sub-router for workspace CRUD, tasks, agents, Git and PR creation. */
|
|
60
64
|
const app = new Hono();
|
|
61
65
|
// These handlers own teardown across awaits; purge/delete/restore own their guard in the service.
|
|
62
|
-
for (const route of ['/:id/archive', '/:id/run-setup-script', '/:id/cancel-source-change']) {
|
|
66
|
+
for (const route of ['/:id/archive', '/:id/run-setup-script', '/:id/cancel-source-change', '/:id/rollback-file']) {
|
|
63
67
|
app.use(route, async (c, next) => {
|
|
64
68
|
try {
|
|
65
69
|
await withWorkspaceLifecycleGuard(c.req.param('id'), next);
|
|
@@ -182,11 +186,16 @@ function emitCreateFailed(creationId, step, message) {
|
|
|
182
186
|
*/
|
|
183
187
|
async function rollbackFailedCreation(workspace, opts) {
|
|
184
188
|
try {
|
|
185
|
-
|
|
189
|
+
const warnings = await deleteWorkspaceWithSideEffects(workspace, {
|
|
186
190
|
deleteLocalBranch: opts.deleteLocalBranch,
|
|
191
|
+
removeWorktree: opts.removeWorktree,
|
|
187
192
|
// Nothing was ever pushed at this point in the creation flow.
|
|
188
193
|
deleteRemoteBranch: false,
|
|
189
194
|
});
|
|
195
|
+
if (!opts.removeWorktree && opts.attachments?.length && fs.existsSync(workspace.worktreePath)) {
|
|
196
|
+
warnings.push(...(await removeAttachments(workspace.worktreePath, opts.attachments)));
|
|
197
|
+
}
|
|
198
|
+
return warnings;
|
|
190
199
|
}
|
|
191
200
|
catch (err) {
|
|
192
201
|
const message = err instanceof Error ? err.message : String(err);
|
|
@@ -351,8 +360,11 @@ app.post('/:id/switch-engine', migrationGuard, async (c) => {
|
|
|
351
360
|
return c.json({ error: err instanceof Error ? err.message : 'Failed to switch engine' }, 500);
|
|
352
361
|
}
|
|
353
362
|
});
|
|
354
|
-
|
|
355
|
-
|
|
363
|
+
const creationBodyLimit = bodyLimit({
|
|
364
|
+
maxSize: MAX_ATTACHMENT_REQUEST_BYTES,
|
|
365
|
+
onError: (c) => c.json({ error: 'Workspace creation request is too large', step: 'validate' }, 413),
|
|
366
|
+
});
|
|
367
|
+
app.post('/', migrationGuard, creationBodyLimit, async (c) => {
|
|
356
368
|
// Declared outside the try so the outermost catch (any unforeseen throw,
|
|
357
369
|
// including a malformed request body) can still name a step instead of
|
|
358
370
|
// leaving the create-progress channel silent. Updated in lockstep with
|
|
@@ -368,14 +380,14 @@ app.post('/', migrationGuard, async (c) => {
|
|
|
368
380
|
// Set only once WE created the worktree, which is also what created the
|
|
369
381
|
// branch. A failure before that point must not delete a branch we never made.
|
|
370
382
|
let createdBranch = false;
|
|
383
|
+
let createdWorktree = false;
|
|
384
|
+
let creationAttachments = [];
|
|
371
385
|
// Hoisted out of the `build-prompt` block (declared with `const` there) so
|
|
372
386
|
// both rollback paths — the `start-agent` branch and the outermost catch —
|
|
373
387
|
// can read it to decide whether the setup-script caveat applies.
|
|
374
388
|
let setupScriptConfigured = false;
|
|
375
389
|
try {
|
|
376
|
-
const body = await c.req
|
|
377
|
-
.json()
|
|
378
|
-
.catch(() => ({}));
|
|
390
|
+
const { body, attachments } = await readWorkspaceCreationRequest(c.req.raw);
|
|
379
391
|
// workingBranch is derived from git when worktreePath is provided, so
|
|
380
392
|
// it's not required in that flow. The other 3 fields stay mandatory.
|
|
381
393
|
if (!body.name || !body.projectPath || !body.sourceBranch) {
|
|
@@ -671,190 +683,206 @@ app.post('/', migrationGuard, async (c) => {
|
|
|
671
683
|
// worktree path so they stay aligned.
|
|
672
684
|
currentStep = 'create-record';
|
|
673
685
|
emitCreateProgress(creationId, currentStep);
|
|
674
|
-
let prospectiveWorktreePath;
|
|
675
686
|
let workingBranchAdjusted = false;
|
|
676
|
-
|
|
677
|
-
|
|
678
|
-
|
|
679
|
-
|
|
680
|
-
|
|
681
|
-
|
|
682
|
-
|
|
683
|
-
|
|
684
|
-
|
|
685
|
-
|
|
686
|
-
|
|
687
|
-
|
|
688
|
-
prospectiveWorktreePath =
|
|
689
|
-
workingBranchAdjusted = resolved.adjusted;
|
|
690
|
-
}
|
|
691
|
-
catch (err) {
|
|
692
|
-
const message = err instanceof Error ? err.message : String(err);
|
|
693
|
-
emitCreateFailed(creationId, 'create-record', message);
|
|
694
|
-
return c.json({ error: message, step: 'create-record' }, 409);
|
|
687
|
+
// Allocate the unique branch/path and create its checkout under one lock.
|
|
688
|
+
// Cleanup is deliberately outside: it acquires the same repository lock.
|
|
689
|
+
const creation = await withGitRepoLock(body.projectPath, async () => {
|
|
690
|
+
let prospectiveWorktreePath;
|
|
691
|
+
if (useReusedWorktree) {
|
|
692
|
+
// Another attachment may have completed while this request waited for the lock.
|
|
693
|
+
const existing = getDb().prepare('SELECT id FROM workspaces WHERE worktree_path = ?').get(body.worktreePath);
|
|
694
|
+
if (existing) {
|
|
695
|
+
const failure = 'This worktree is already attached to another Kōbō workspace';
|
|
696
|
+
emitCreateFailed(creationId, 'inspect-worktree', failure);
|
|
697
|
+
return c.json({ error: failure, step: 'inspect-worktree' }, 422);
|
|
698
|
+
}
|
|
699
|
+
prospectiveWorktreePath = body.worktreePath;
|
|
695
700
|
}
|
|
696
|
-
|
|
697
|
-
|
|
698
|
-
|
|
699
|
-
|
|
700
|
-
|
|
701
|
-
|
|
702
|
-
|
|
703
|
-
|
|
704
|
-
|
|
705
|
-
|
|
706
|
-
|
|
707
|
-
|
|
708
|
-
|
|
709
|
-
|
|
710
|
-
|
|
711
|
-
|
|
712
|
-
|
|
713
|
-
// Set by the engine-comparison flow, which posts here once per engine
|
|
714
|
-
// with the same id so the resulting workspaces can find each other.
|
|
715
|
-
comparisonId: body.comparisonId,
|
|
716
|
-
...(useReusedWorktree ? {} : { worktreesPath: globalSettings.worktreesPath }),
|
|
717
|
-
});
|
|
718
|
-
createdWorkspace = workspace;
|
|
719
|
-
// Enable auto-loop before starting the initial brainstorming session so
|
|
720
|
-
// its model override is available when selecting that session's model.
|
|
721
|
-
if (body.autoLoop === true) {
|
|
722
|
-
const notionProducedTasks = body.notionUrl !== undefined &&
|
|
723
|
-
notionContent != null &&
|
|
724
|
-
notionContent.todos.length > 0 &&
|
|
725
|
-
notionContent.gherkinFeatures.length > 0;
|
|
726
|
-
const sessionMode = body.autoLoopSessionMode === 'continuous' ? 'continuous' : 'per_task';
|
|
727
|
-
const db = getDb();
|
|
728
|
-
db.prepare('UPDATE workspaces SET auto_loop = 1, auto_loop_ready = ?, auto_loop_session_mode = ? WHERE id = ?').run(notionProducedTasks ? 1 : 0, sessionMode, workspace.id);
|
|
729
|
-
workspace = workspaceService.getWorkspace(workspace.id) ?? workspace;
|
|
730
|
-
// Emit events so the frontend refreshes autoLoopStates without F5.
|
|
731
|
-
wsService.emitEphemeral(workspace.id, 'autoloop:enabled', {});
|
|
732
|
-
if (notionProducedTasks) {
|
|
733
|
-
wsService.emitEphemeral(workspace.id, 'autoloop:ready-flipped', {});
|
|
701
|
+
else {
|
|
702
|
+
try {
|
|
703
|
+
const resolved = resolveUniqueBranchAndPath({
|
|
704
|
+
projectPath: body.projectPath,
|
|
705
|
+
baseBranch: workingBranch,
|
|
706
|
+
worktreesPath: globalSettings.worktreesPath,
|
|
707
|
+
projectSlug,
|
|
708
|
+
});
|
|
709
|
+
workingBranch = resolved.workingBranch;
|
|
710
|
+
prospectiveWorktreePath = resolved.worktreePath;
|
|
711
|
+
workingBranchAdjusted = resolved.adjusted;
|
|
712
|
+
}
|
|
713
|
+
catch (err) {
|
|
714
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
715
|
+
emitCreateFailed(creationId, 'create-record', message);
|
|
716
|
+
return c.json({ error: message, step: 'create-record' }, 409);
|
|
717
|
+
}
|
|
734
718
|
}
|
|
735
|
-
|
|
736
|
-
|
|
737
|
-
|
|
738
|
-
|
|
739
|
-
|
|
740
|
-
|
|
741
|
-
|
|
742
|
-
|
|
743
|
-
|
|
744
|
-
|
|
745
|
-
|
|
746
|
-
|
|
747
|
-
|
|
748
|
-
|
|
749
|
-
|
|
750
|
-
|
|
751
|
-
|
|
719
|
+
let workspace = workspaceService.createWorkspace({
|
|
720
|
+
name: body.name,
|
|
721
|
+
projectPath: body.projectPath,
|
|
722
|
+
sourceBranch: body.sourceBranch,
|
|
723
|
+
workingBranch,
|
|
724
|
+
notionUrl: body.notionUrl,
|
|
725
|
+
notionPageId: body.notionPageId,
|
|
726
|
+
sentryUrl: body.sentryUrl,
|
|
727
|
+
prUrl: body.prUrl,
|
|
728
|
+
worktreePath: prospectiveWorktreePath,
|
|
729
|
+
worktreeOwned: !useReusedWorktree,
|
|
730
|
+
model: body.model,
|
|
731
|
+
brainstormModel: body.brainstormModel,
|
|
732
|
+
reasoningEffort: body.reasoningEffort,
|
|
733
|
+
agentPermissionMode: resolveCreateAgentPermissionMode(body.agentPermissionMode, body.projectPath, globalSettings, body.engine ?? 'claude-code'),
|
|
734
|
+
engine: body.engine,
|
|
735
|
+
// Set by the engine-comparison flow, which posts here once per engine
|
|
736
|
+
// with the same id so the resulting workspaces can find each other.
|
|
737
|
+
comparisonId: body.comparisonId,
|
|
738
|
+
...(useReusedWorktree ? {} : { worktreesPath: globalSettings.worktreesPath }),
|
|
739
|
+
});
|
|
740
|
+
createdWorkspace = workspace;
|
|
741
|
+
// Enable auto-loop before starting the initial brainstorming session so
|
|
742
|
+
// its model override is available when selecting that session's model.
|
|
743
|
+
if (body.autoLoop === true) {
|
|
744
|
+
const notionProducedTasks = body.notionUrl !== undefined &&
|
|
745
|
+
notionContent != null &&
|
|
746
|
+
notionContent.todos.length > 0 &&
|
|
747
|
+
notionContent.gherkinFeatures.length > 0;
|
|
748
|
+
const sessionMode = body.autoLoopSessionMode === 'continuous' ? 'continuous' : 'per_task';
|
|
749
|
+
const db = getDb();
|
|
750
|
+
db.prepare('UPDATE workspaces SET auto_loop = 1, auto_loop_ready = ?, auto_loop_session_mode = ? WHERE id = ?').run(notionProducedTasks ? 1 : 0, sessionMode, workspace.id);
|
|
751
|
+
workspace = workspaceService.getWorkspace(workspace.id) ?? workspace;
|
|
752
|
+
// Emit events so the frontend refreshes autoLoopStates without F5.
|
|
753
|
+
wsService.emitEphemeral(workspace.id, 'autoloop:enabled', {});
|
|
754
|
+
if (notionProducedTasks) {
|
|
755
|
+
wsService.emitEphemeral(workspace.id, 'autoloop:ready-flipped', {});
|
|
756
|
+
}
|
|
752
757
|
}
|
|
753
|
-
|
|
754
|
-
|
|
758
|
+
// Auto-tag the workspace based on its creation source — `notion` when
|
|
759
|
+
// imported from a Notion page, `sentry` when bootstrapped from a Sentry
|
|
760
|
+
// issue URL. Pre-seeded in the global tag catalogue via migration v9.
|
|
761
|
+
// Skip any tag the user has removed from the catalogue so we respect
|
|
762
|
+
// their choice (they may have pruned "notion"/"sentry" on purpose).
|
|
763
|
+
const catalogTags = new Set(globalSettings.tags ?? []);
|
|
764
|
+
const autoTags = [];
|
|
765
|
+
if (body.notionUrl && catalogTags.has('notion'))
|
|
766
|
+
autoTags.push('notion');
|
|
767
|
+
if (body.sentryUrl && catalogTags.has('sentry'))
|
|
768
|
+
autoTags.push('sentry');
|
|
769
|
+
if (autoTags.length > 0) {
|
|
770
|
+
try {
|
|
771
|
+
const tagged = workspaceService.setWorkspaceTags(workspace.id, autoTags);
|
|
772
|
+
if (tagged)
|
|
773
|
+
workspace = tagged;
|
|
774
|
+
}
|
|
775
|
+
catch (err) {
|
|
776
|
+
console.error('[workspaces] Failed to apply auto tags:', err);
|
|
777
|
+
}
|
|
755
778
|
}
|
|
756
|
-
|
|
757
|
-
|
|
758
|
-
|
|
759
|
-
|
|
760
|
-
|
|
761
|
-
|
|
762
|
-
|
|
763
|
-
|
|
764
|
-
|
|
765
|
-
workspace = workspaceService.updateWorkspaceName(workspace.id, renamed);
|
|
766
|
-
}
|
|
767
|
-
currentStep = 'create-tasks';
|
|
768
|
-
emitCreateProgress(creationId, currentStep);
|
|
769
|
-
// Create tasks from extracted Notion data
|
|
770
|
-
if (notionContent) {
|
|
771
|
-
let sortOrder = 0;
|
|
772
|
-
for (const todo of notionContent.todos) {
|
|
773
|
-
workspaceService.createTask(workspace.id, {
|
|
774
|
-
title: todo.title,
|
|
775
|
-
isAcceptanceCriterion: false,
|
|
776
|
-
sortOrder: sortOrder++,
|
|
777
|
-
});
|
|
779
|
+
// Update workspace name with Sentry issue title if the user did not provide
|
|
780
|
+
// a custom name and Notion hasn't already filled it. Prefix with the Sentry
|
|
781
|
+
// short-id (e.g. "SEKUR-IOS-9 | TypeError: …") so the workspace stays
|
|
782
|
+
// identifiable in the sidebar without opening the panel.
|
|
783
|
+
if (sentryContent?.title && !notionContent?.title) {
|
|
784
|
+
const prefix = sentryContent.issueId ? `${sentryContent.issueId} | ` : '';
|
|
785
|
+
const renamed = resolveExtractedName(workspace.name, `${prefix}${sentryContent.title}`);
|
|
786
|
+
if (renamed)
|
|
787
|
+
workspace = workspaceService.updateWorkspaceName(workspace.id, renamed);
|
|
778
788
|
}
|
|
779
|
-
|
|
780
|
-
|
|
781
|
-
|
|
782
|
-
|
|
783
|
-
|
|
784
|
-
|
|
789
|
+
currentStep = 'create-tasks';
|
|
790
|
+
emitCreateProgress(creationId, currentStep);
|
|
791
|
+
// Create tasks from extracted Notion data
|
|
792
|
+
if (notionContent) {
|
|
793
|
+
let sortOrder = 0;
|
|
794
|
+
for (const todo of notionContent.todos) {
|
|
795
|
+
workspaceService.createTask(workspace.id, {
|
|
796
|
+
title: todo.title,
|
|
797
|
+
isAcceptanceCriterion: false,
|
|
798
|
+
sortOrder: sortOrder++,
|
|
799
|
+
});
|
|
800
|
+
}
|
|
801
|
+
for (const feature of notionContent.gherkinFeatures) {
|
|
802
|
+
workspaceService.createTask(workspace.id, {
|
|
803
|
+
title: feature,
|
|
804
|
+
isAcceptanceCriterion: true,
|
|
805
|
+
sortOrder: sortOrder++,
|
|
806
|
+
});
|
|
807
|
+
}
|
|
808
|
+
// Update workspace name with Notion page title only if user didn't
|
|
809
|
+
// provide a custom name. Prefix with the Notion unique-id (e.g.
|
|
810
|
+
// Use the Notion page title when the user left the generic placeholder.
|
|
811
|
+
if (notionContent.title) {
|
|
812
|
+
const renamed = resolveExtractedName(workspace.name, notionContent.title);
|
|
813
|
+
if (renamed)
|
|
814
|
+
workspace = workspaceService.updateWorkspaceName(workspace.id, renamed);
|
|
815
|
+
}
|
|
785
816
|
}
|
|
786
|
-
// Update workspace name with
|
|
787
|
-
//
|
|
788
|
-
//
|
|
789
|
-
|
|
790
|
-
|
|
817
|
+
// Update workspace name with the PR/MR title if the user did not provide
|
|
818
|
+
// a custom name and neither Notion nor Sentry already claimed it. This
|
|
819
|
+
// makes the PR a THIRD context source, lowest priority of the three
|
|
820
|
+
// (Notion > Sentry > PR), consistent with the checks above.
|
|
821
|
+
if (prContent && !notionContent?.title && !sentryContent?.title) {
|
|
822
|
+
const renamed = resolveExtractedName(workspace.name, prContent.title);
|
|
791
823
|
if (renamed)
|
|
792
824
|
workspace = workspaceService.updateWorkspaceName(workspace.id, renamed);
|
|
793
825
|
}
|
|
794
|
-
|
|
795
|
-
|
|
796
|
-
|
|
797
|
-
|
|
798
|
-
|
|
799
|
-
|
|
800
|
-
|
|
801
|
-
|
|
802
|
-
|
|
803
|
-
|
|
804
|
-
|
|
805
|
-
|
|
806
|
-
let sortOrder = 0;
|
|
807
|
-
if (Array.isArray(body.tasks)) {
|
|
808
|
-
for (const title of body.tasks) {
|
|
809
|
-
if (typeof title === 'string' && title.trim()) {
|
|
810
|
-
workspaceService.createTask(workspace.id, {
|
|
811
|
-
title: title.trim(),
|
|
812
|
-
isAcceptanceCriterion: false,
|
|
813
|
-
sortOrder: sortOrder++,
|
|
814
|
-
});
|
|
826
|
+
// Create manual tasks/criteria if no Notion content was extracted
|
|
827
|
+
if (!notionContent && (Array.isArray(body.tasks) || Array.isArray(body.acceptanceCriteria))) {
|
|
828
|
+
let sortOrder = 0;
|
|
829
|
+
if (Array.isArray(body.tasks)) {
|
|
830
|
+
for (const title of body.tasks) {
|
|
831
|
+
if (typeof title === 'string' && title.trim()) {
|
|
832
|
+
workspaceService.createTask(workspace.id, {
|
|
833
|
+
title: title.trim(),
|
|
834
|
+
isAcceptanceCriterion: false,
|
|
835
|
+
sortOrder: sortOrder++,
|
|
836
|
+
});
|
|
837
|
+
}
|
|
815
838
|
}
|
|
816
839
|
}
|
|
817
|
-
|
|
818
|
-
|
|
819
|
-
|
|
820
|
-
|
|
821
|
-
|
|
822
|
-
|
|
823
|
-
|
|
824
|
-
|
|
825
|
-
}
|
|
840
|
+
if (Array.isArray(body.acceptanceCriteria)) {
|
|
841
|
+
for (const title of body.acceptanceCriteria) {
|
|
842
|
+
if (typeof title === 'string' && title.trim()) {
|
|
843
|
+
workspaceService.createTask(workspace.id, {
|
|
844
|
+
title: title.trim(),
|
|
845
|
+
isAcceptanceCriterion: true,
|
|
846
|
+
sortOrder: sortOrder++,
|
|
847
|
+
});
|
|
848
|
+
}
|
|
826
849
|
}
|
|
827
850
|
}
|
|
828
851
|
}
|
|
829
|
-
|
|
830
|
-
|
|
831
|
-
|
|
832
|
-
|
|
833
|
-
|
|
834
|
-
|
|
835
|
-
|
|
836
|
-
worktreePath = body.worktreePath;
|
|
837
|
-
}
|
|
838
|
-
else {
|
|
839
|
-
try {
|
|
840
|
-
const created = worktreeService.createWorktree(body.projectPath, workingBranch, baseRef, globalSettings.worktreesPath, projectSlug);
|
|
841
|
-
worktreePath = created.worktreePath;
|
|
842
|
-
// Not an assumption: `createWorktree` reports which git command it ran.
|
|
843
|
-
// It falls back to attaching an existing branch, and deleting that one
|
|
844
|
-
// on rollback would destroy commits the user made before Kobo existed.
|
|
845
|
-
createdBranch = created.branchCreated;
|
|
852
|
+
currentStep = 'create-worktree';
|
|
853
|
+
emitCreateProgress(creationId, currentStep);
|
|
854
|
+
// Create git worktree for the working branch — unless we're reusing an
|
|
855
|
+
// existing one, in which case the path is taken straight from the body.
|
|
856
|
+
let worktreePath;
|
|
857
|
+
if (useReusedWorktree) {
|
|
858
|
+
worktreePath = body.worktreePath;
|
|
846
859
|
}
|
|
847
|
-
|
|
848
|
-
|
|
849
|
-
|
|
850
|
-
|
|
851
|
-
|
|
852
|
-
|
|
853
|
-
|
|
854
|
-
|
|
855
|
-
|
|
860
|
+
else {
|
|
861
|
+
try {
|
|
862
|
+
const created = await worktreeService.createWorktreeUnlocked(body.projectPath, workingBranch, baseRef, globalSettings.worktreesPath, projectSlug);
|
|
863
|
+
worktreePath = created.worktreePath;
|
|
864
|
+
// Not an assumption: `createWorktree` reports which git command it ran.
|
|
865
|
+
// It falls back to attaching an existing branch, and deleting that one
|
|
866
|
+
// on rollback would destroy commits the user made before Kobo existed.
|
|
867
|
+
createdBranch = created.branchCreated;
|
|
868
|
+
createdWorktree = true;
|
|
869
|
+
}
|
|
870
|
+
catch (err) {
|
|
871
|
+
if (err instanceof worktreeService.WorktreeCreationError) {
|
|
872
|
+
createdWorktree = true;
|
|
873
|
+
createdBranch = err.branchCreated;
|
|
874
|
+
}
|
|
875
|
+
throw new Error(`Failed to create worktree: ${err instanceof Error ? err.message : String(err)}`, {
|
|
876
|
+
cause: err,
|
|
877
|
+
});
|
|
878
|
+
}
|
|
856
879
|
}
|
|
857
|
-
|
|
880
|
+
return { workspace, worktreePath };
|
|
881
|
+
});
|
|
882
|
+
if (creation instanceof Response)
|
|
883
|
+
return creation;
|
|
884
|
+
const { workspace } = creation;
|
|
885
|
+
const { worktreePath } = creation;
|
|
858
886
|
currentStep = 'write-conventions';
|
|
859
887
|
emitCreateProgress(creationId, currentStep);
|
|
860
888
|
// Ensure Kobo-generated files are gitignored. Check both the root
|
|
@@ -871,6 +899,7 @@ app.post('/', migrationGuard, async (c) => {
|
|
|
871
899
|
['.ai/.git-conventions.md', '.git-conventions.md'],
|
|
872
900
|
['.ai/thoughts/', 'thoughts/'],
|
|
873
901
|
['.ai/images/', 'images/'],
|
|
902
|
+
['.ai/attachments/', 'attachments/'],
|
|
874
903
|
['.ai/.setup-script.tmp', '.setup-script.tmp'],
|
|
875
904
|
['.ai/.cleanup-script.tmp', '.cleanup-script.tmp'],
|
|
876
905
|
['.ai/.archive-script.tmp', '.archive-script.tmp'],
|
|
@@ -924,6 +953,12 @@ app.post('/', migrationGuard, async (c) => {
|
|
|
924
953
|
let agentStartError = null;
|
|
925
954
|
currentStep = 'write-context-files';
|
|
926
955
|
emitCreateProgress(creationId, currentStep);
|
|
956
|
+
creationAttachments = await saveAttachments(worktreePath, attachments);
|
|
957
|
+
if (creationAttachments.length > 0) {
|
|
958
|
+
body.description = [body.description?.trim(), ...creationAttachments.map(attachmentReference)]
|
|
959
|
+
.filter(Boolean)
|
|
960
|
+
.join('\n\n');
|
|
961
|
+
}
|
|
927
962
|
// Save Notion content as markdown in worktree
|
|
928
963
|
let notionFilePath = null;
|
|
929
964
|
if (notionContent && body.notionUrl) {
|
|
@@ -1322,12 +1357,19 @@ Once the brainstorming + planning steps above are complete and you have a saved
|
|
|
1322
1357
|
// already guards the worktree via `worktreeOwned`; the branch flag is
|
|
1323
1358
|
// ours to set.
|
|
1324
1359
|
const rollbackWarnings = await rollbackFailedCreation(workspace, {
|
|
1325
|
-
deleteLocalBranch:
|
|
1360
|
+
deleteLocalBranch: createdBranch,
|
|
1361
|
+
removeWorktree: createdWorktree,
|
|
1362
|
+
attachments: creationAttachments,
|
|
1326
1363
|
});
|
|
1327
1364
|
// The setup script has already run by this point — see the caveat's own
|
|
1328
1365
|
// comment for what that rollback cannot reach.
|
|
1329
1366
|
const setupCaveat = setupScriptConfigured ? SETUP_SCRIPT_ROLLBACK_CAVEAT : '';
|
|
1330
|
-
const
|
|
1367
|
+
const removed = createdBranch
|
|
1368
|
+
? 'The workspace, its worktree and its branch were removed.'
|
|
1369
|
+
: createdWorktree
|
|
1370
|
+
? 'The workspace and its worktree were removed.'
|
|
1371
|
+
: 'The half-created workspace was removed.';
|
|
1372
|
+
const failure = `Failed to start the agent: ${agentStartError}. ${removed}${setupCaveat}`;
|
|
1331
1373
|
emitCreateFailed(creationId, 'start-agent', failure);
|
|
1332
1374
|
return c.json({ error: failure, step: 'start-agent', rollback: { done: true, warnings: rollbackWarnings } }, 500);
|
|
1333
1375
|
}
|
|
@@ -1372,7 +1414,11 @@ Once the brainstorming + planning steps above are complete and you have a saved
|
|
|
1372
1414
|
// reported error.
|
|
1373
1415
|
if (createdWorkspace) {
|
|
1374
1416
|
emitCreateProgress(creationId, 'rollback');
|
|
1375
|
-
const rollbackWarnings = await rollbackFailedCreation(createdWorkspace, {
|
|
1417
|
+
const rollbackWarnings = await rollbackFailedCreation(createdWorkspace, {
|
|
1418
|
+
deleteLocalBranch: createdBranch,
|
|
1419
|
+
removeWorktree: createdWorktree,
|
|
1420
|
+
attachments: creationAttachments,
|
|
1421
|
+
});
|
|
1376
1422
|
logError('workspaces', 'Rolled back a creation that failed outside any per-step handler', {
|
|
1377
1423
|
workspaceId: createdWorkspace.id,
|
|
1378
1424
|
step: currentStep,
|
|
@@ -1390,7 +1436,7 @@ Once the brainstorming + planning steps above are complete and you have a saved
|
|
|
1390
1436
|
return c.json({ error: failure, step: currentStep, rollback: { done: true, warnings: rollbackWarnings } }, 500);
|
|
1391
1437
|
}
|
|
1392
1438
|
emitCreateFailed(creationId, currentStep, message);
|
|
1393
|
-
return c.json({ error: message, step: currentStep }, 500);
|
|
1439
|
+
return c.json({ error: message, step: currentStep }, err instanceof AttachmentRequestError ? 400 : 500);
|
|
1394
1440
|
}
|
|
1395
1441
|
});
|
|
1396
1442
|
// POST /api/workspaces/:id/sessions — create a new idle agent session
|
|
@@ -1450,8 +1496,10 @@ app.get('/pr-states', (c) => {
|
|
|
1450
1496
|
// stay before GET /:id.
|
|
1451
1497
|
app.get('/info', (c) => {
|
|
1452
1498
|
try {
|
|
1499
|
+
const workspaces = workspaceService.listWorkspaces(false);
|
|
1453
1500
|
return c.json({
|
|
1454
|
-
workspaces
|
|
1501
|
+
workspaces,
|
|
1502
|
+
pendingInputs: Object.fromEntries(workspaces.map((workspace) => [workspace.id, agentManager.getPendingInputs(workspace.id)])),
|
|
1455
1503
|
prSnapshots: getAllPrSnapshots(),
|
|
1456
1504
|
gitStats: getAllGitStats(),
|
|
1457
1505
|
// In-memory truth, not the `status` column: a workspace missing from
|
|
@@ -2119,12 +2167,8 @@ app.post('/:id/tasks/:taskId/notify-done', (c) => {
|
|
|
2119
2167
|
return c.json({ error: message }, workspaceErrorStatus(err));
|
|
2120
2168
|
}
|
|
2121
2169
|
});
|
|
2122
|
-
//
|
|
2123
|
-
|
|
2124
|
-
// unbounded scan of a months-old workspace blocks every request and WS emit.
|
|
2125
|
-
const HISTORY_SEARCH_SCAN_CAP = 20_000;
|
|
2126
|
-
// GET /api/workspaces/:id/history-search — full-workspace persisted conversation search.
|
|
2127
|
-
app.get('/:id/history-search', (c) => {
|
|
2170
|
+
// GET /api/workspaces/:id/history-search — the same logical-message index as global search.
|
|
2171
|
+
app.get('/:id/history-search', async (c) => {
|
|
2128
2172
|
try {
|
|
2129
2173
|
const id = c.req.param('id');
|
|
2130
2174
|
if (!workspaceService.getWorkspace(id))
|
|
@@ -2132,71 +2176,23 @@ app.get('/:id/history-search', (c) => {
|
|
|
2132
2176
|
const query = (c.req.query('q') ?? '').trim();
|
|
2133
2177
|
if (query.length < 2)
|
|
2134
2178
|
return c.json({ results: [] });
|
|
2135
|
-
const limit = Math.min(parseInt(c.req.query('limit') ?? '30', 10) || 30, 100);
|
|
2136
|
-
const
|
|
2137
|
-
|
|
2138
|
-
.
|
|
2139
|
-
|
|
2140
|
-
|
|
2141
|
-
|
|
2142
|
-
|
|
2143
|
-
|
|
2144
|
-
|
|
2145
|
-
|
|
2146
|
-
const result = readableHistorySearchResult(row.type, row.payload, query);
|
|
2147
|
-
if (!result)
|
|
2148
|
-
return [];
|
|
2149
|
-
return [{ eventId: row.id, sessionId: row.session_id, createdAt: row.created_at, ...result }];
|
|
2179
|
+
const limit = Math.max(1, Math.min(parseInt(c.req.query('limit') ?? '30', 10) || 30, 100));
|
|
2180
|
+
const results = await searchEvents(query, { workspaceId: id, includeArchived: true, limit }, c.req.raw.signal);
|
|
2181
|
+
return c.json({
|
|
2182
|
+
results: results.map((result) => ({
|
|
2183
|
+
eventId: result.eventId,
|
|
2184
|
+
sessionId: result.sessionId,
|
|
2185
|
+
createdAt: result.timestamp,
|
|
2186
|
+
kind: result.type === 'user:message' ? 'user' : 'agent',
|
|
2187
|
+
snippet: result.snippet,
|
|
2188
|
+
})),
|
|
2189
|
+
partial: getSearchIndexStatus().state !== 'ready',
|
|
2150
2190
|
});
|
|
2151
|
-
return c.json({ results: normalized.slice(0, limit) });
|
|
2152
2191
|
}
|
|
2153
2192
|
catch (err) {
|
|
2154
|
-
return c.json({ error: err instanceof Error ? err.message : String(err) },
|
|
2193
|
+
return c.json({ error: err instanceof Error ? err.message : String(err) }, 503);
|
|
2155
2194
|
}
|
|
2156
2195
|
});
|
|
2157
|
-
function readableHistorySearchResult(type, rawPayload, query) {
|
|
2158
|
-
let payload;
|
|
2159
|
-
try {
|
|
2160
|
-
payload = JSON.parse(rawPayload);
|
|
2161
|
-
}
|
|
2162
|
-
catch {
|
|
2163
|
-
return null;
|
|
2164
|
-
}
|
|
2165
|
-
let text = null;
|
|
2166
|
-
let kind = 'agent';
|
|
2167
|
-
if (type === 'user:message') {
|
|
2168
|
-
text = typeof payload.content === 'string' ? payload.content : null;
|
|
2169
|
-
kind = 'user';
|
|
2170
|
-
}
|
|
2171
|
-
else if (type === 'agent:event') {
|
|
2172
|
-
if (payload.kind === 'message:text' && typeof payload.text === 'string')
|
|
2173
|
-
text = payload.text;
|
|
2174
|
-
}
|
|
2175
|
-
else if (type === 'agent:output') {
|
|
2176
|
-
const message = payload.message;
|
|
2177
|
-
if (Array.isArray(message?.content)) {
|
|
2178
|
-
text = message.content
|
|
2179
|
-
.flatMap((block) => {
|
|
2180
|
-
if (!block || typeof block !== 'object')
|
|
2181
|
-
return [];
|
|
2182
|
-
const candidate = block;
|
|
2183
|
-
return candidate.type === 'text' && typeof candidate.text === 'string' ? [candidate.text] : [];
|
|
2184
|
-
})
|
|
2185
|
-
.join('\n');
|
|
2186
|
-
}
|
|
2187
|
-
}
|
|
2188
|
-
if (!text)
|
|
2189
|
-
return null;
|
|
2190
|
-
const matchIndex = text.toLowerCase().indexOf(query.toLowerCase());
|
|
2191
|
-
if (matchIndex < 0)
|
|
2192
|
-
return null;
|
|
2193
|
-
const start = Math.max(0, matchIndex - 90);
|
|
2194
|
-
const end = Math.min(text.length, matchIndex + query.length + 180);
|
|
2195
|
-
return {
|
|
2196
|
-
kind,
|
|
2197
|
-
snippet: `${start > 0 ? '…' : ''}${text.slice(start, end).replace(/\s+/g, ' ').trim()}${end < text.length ? '…' : ''}`,
|
|
2198
|
-
};
|
|
2199
|
-
}
|
|
2200
2196
|
/**
|
|
2201
2197
|
* GET /api/workspaces/:id/sessions/:sessionId/summary — why a session ended.
|
|
2202
2198
|
*
|
|
@@ -3061,7 +3057,7 @@ async function deleteWorkspaceWithSideEffectsUnlocked(workspace, opts) {
|
|
|
3061
3057
|
if (workspace.worktreePurgedAt) {
|
|
3062
3058
|
console.log(`[workspaces] skipping worktree removal on delete (already purged): ${worktreePath}`);
|
|
3063
3059
|
}
|
|
3064
|
-
else if (workspace.worktreeOwned) {
|
|
3060
|
+
else if (workspace.worktreeOwned && opts.removeWorktree !== false) {
|
|
3065
3061
|
try {
|
|
3066
3062
|
await worktreeService.removeWorktree(workspace.projectPath, worktreePath);
|
|
3067
3063
|
}
|
|
@@ -3423,7 +3419,7 @@ app.post('/:id/rollback-file', async (c) => {
|
|
|
3423
3419
|
return c.json({ error: `Workspace '${id}' not found` }, 404);
|
|
3424
3420
|
}
|
|
3425
3421
|
const body = await c.req.json().catch(() => ({}));
|
|
3426
|
-
const filePath = typeof body?.path === 'string' ? body.path
|
|
3422
|
+
const filePath = typeof body?.path === 'string' ? body.path : '';
|
|
3427
3423
|
if (!filePath) {
|
|
3428
3424
|
return c.json({ error: 'Missing or invalid `path` field' }, 400);
|
|
3429
3425
|
}
|
|
@@ -3436,7 +3432,13 @@ app.post('/:id/rollback-file', async (c) => {
|
|
|
3436
3432
|
}
|
|
3437
3433
|
let target;
|
|
3438
3434
|
try {
|
|
3439
|
-
target =
|
|
3435
|
+
target = await withGitRepoLock(workspace.worktreePath, async () => {
|
|
3436
|
+
if (agentManager.getAgentStatus(id) !== null)
|
|
3437
|
+
throw new Error('Stop the agent before rolling back a file');
|
|
3438
|
+
gitOps.assertCurrentBranch(workspace.worktreePath, workspace.workingBranch);
|
|
3439
|
+
gitOps.assertNoIndexLock(workspace.worktreePath);
|
|
3440
|
+
return gitOps.rollbackFile(workspace.worktreePath, workspace.workingBranch, filePath);
|
|
3441
|
+
});
|
|
3440
3442
|
}
|
|
3441
3443
|
catch (err) {
|
|
3442
3444
|
const message = err instanceof Error ? err.message : String(err);
|