@loicngr/kobo 1.11.26 → 1.11.28
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/AGENTS.md +19 -1
- package/CHANGELOG.md +10 -0
- package/dist/mcp-server/kobo-tasks-handlers.js +17 -1
- package/dist/server/db/migrations.js +34 -0
- package/dist/server/db/schema.js +3 -1
- package/dist/server/index.js +80 -23
- package/dist/server/middleware/host-check-middleware.js +80 -0
- package/dist/server/routes/changelog.js +51 -3
- package/dist/server/routes/notion.js +1 -1
- package/dist/server/routes/pull-requests.js +6 -2
- package/dist/server/routes/sentry.js +1 -1
- package/dist/server/routes/settings.js +23 -8
- package/dist/server/routes/usage.js +12 -0
- package/dist/server/routes/workspaces.js +240 -91
- package/dist/server/services/agent/engines/claude-code/engine.js +112 -15
- package/dist/server/services/agent/engines/codex/engine.js +18 -0
- package/dist/server/services/agent/engines/codex/server-requests.js +5 -20
- package/dist/server/services/agent/orchestrator.js +143 -34
- package/dist/server/services/agent/session-controller.js +28 -0
- package/dist/server/services/agent/streaming-batcher.js +1 -0
- package/dist/server/services/agent/turn-liveness.js +12 -1
- package/dist/server/services/auto-loop-service.js +65 -0
- package/dist/server/services/awaiting-user-reminder-service.js +115 -0
- package/dist/server/services/change-source-branch-service.js +1 -1
- package/dist/server/services/file-editor-service.js +2 -46
- package/dist/server/services/forge/bitbucket/provider.js +9 -8
- package/dist/server/services/forge/github/provider.js +4 -3
- package/dist/server/services/forge/gitlab/provider.js +4 -3
- package/dist/server/services/forge/parse-cli-json.js +14 -0
- package/dist/server/services/forge/resolve.js +29 -1
- package/dist/server/services/lifecycle-hook-service.js +114 -0
- package/dist/server/services/network-access-service.js +187 -6
- package/dist/server/services/pr-watcher-service.js +27 -1
- package/dist/server/services/settings-service.js +147 -4
- package/dist/server/services/usage/reliability.js +81 -0
- package/dist/server/services/wakeup-service.js +23 -0
- package/dist/server/services/websocket-service.js +31 -3
- package/dist/server/services/workspace-service.js +36 -4
- package/dist/server/services/worktree-purge-service.js +1 -1
- package/dist/server/utils/git-ops.js +159 -1
- package/dist/server/utils/paths.js +19 -0
- package/dist/server/utils/safe-path.js +21 -1
- package/dist/server/utils/script-runner.js +2 -1
- package/dist/server/utils/workspace-name.js +37 -0
- package/dist/shared/consts.js +22 -0
- package/dist/shared/models.js +4 -4
- package/package.json +3 -2
- package/src/client/dist/pwa/assets/{ActivityFeed-DUjY6lCm.css → ActivityFeed-C6XSxdJ0.css} +1 -1
- package/src/client/dist/pwa/assets/ActivityFeed-LDzg5XPx.js +8 -0
- package/src/client/dist/pwa/assets/ChangelogPage-CjiWI9Qa.js +1 -0
- package/src/client/dist/pwa/assets/ClosePopup-CK6wvsMy.js +1 -0
- package/src/client/dist/pwa/assets/ComparisonPanel-C0hI2D1O.css +1 -0
- package/src/client/dist/pwa/assets/ComparisonPanel-D6eEciwQ.js +1 -0
- package/src/client/dist/pwa/assets/CreatePage-DHBa7ZL5.js +2 -0
- package/src/client/dist/pwa/assets/CreatePage-DeKYK6uC.css +1 -0
- package/src/client/dist/pwa/assets/DashboardPage-BqgK4lAf.css +1 -0
- package/src/client/dist/pwa/assets/DashboardPage-Xev4suQ7.js +1 -0
- package/src/client/dist/pwa/assets/DiffViewer-D0BQ5Dip.js +8 -0
- package/src/client/dist/pwa/assets/DiffViewer-DnR79ycb.css +1 -0
- package/src/client/dist/pwa/assets/DocumentsPanel-7hMCYUTd.js +1 -0
- package/src/client/dist/pwa/assets/DrawerToggleButton-BASeszPg.js +1 -0
- package/src/client/dist/pwa/assets/GitPanel-D-rwHnFn.js +3 -0
- package/src/client/dist/pwa/assets/{GitPanel-BKrtkPrV.css → GitPanel-DVs5BCU-.css} +1 -1
- package/src/client/dist/pwa/assets/HealthPage-BKXqP1li.js +1 -0
- package/src/client/dist/pwa/assets/MainLayout-DnPtpWgP.js +2 -0
- package/src/client/dist/pwa/assets/MainLayout-ExB0xYHB.css +1 -0
- package/src/client/dist/pwa/assets/QBanner-CDiXUya7.js +1 -0
- package/src/client/dist/pwa/assets/QBtnDropdown-52YHuBbx.js +1 -0
- package/src/client/dist/pwa/assets/QChip-DpUYTCOJ.js +1 -0
- package/src/client/dist/pwa/assets/QItemSection-CEUi4Tz0.js +1 -0
- package/src/client/dist/pwa/assets/QMarkupTable-DcoQpjPu.js +1 -0
- package/src/client/dist/pwa/assets/SchedulePanel-C7NK1hIK.js +1 -0
- package/src/client/dist/pwa/assets/SearchPage-Cm8chltA.js +1 -0
- package/src/client/dist/pwa/assets/{SettingsPage-CDZhPV6A.css → SettingsPage-2vdOZu1C.css} +1 -1
- package/src/client/dist/pwa/assets/SettingsPage-B4xs1RUl.js +10 -0
- package/src/client/dist/pwa/assets/TerminalPanel-CSPk8QtK.js +18 -0
- package/src/client/dist/pwa/assets/{TerminalPanel-B6xJJs8p.css → TerminalPanel-DbVZk0vD.css} +1 -1
- package/src/client/dist/pwa/assets/TouchPan-CpO61wPy.js +1 -0
- package/src/client/dist/pwa/assets/WorkspaceAttentionLabels-CrLP_I5S.css +1 -0
- package/src/client/dist/pwa/assets/WorkspaceAttentionLabels-DZrbJ0vD.js +1 -0
- package/src/client/dist/pwa/assets/WorkspacePage-D0-FkRVJ.js +4 -0
- package/src/client/dist/pwa/assets/browser-Bg25_sDh.js +8 -0
- package/src/client/dist/pwa/assets/build-path-tree-DH4SSsEz.js +1 -0
- package/src/client/dist/pwa/assets/de-Bf_OsgDw.js +17 -0
- package/src/client/dist/pwa/assets/documents-CDZqd_g5.js +1 -0
- package/src/client/dist/pwa/assets/editor-BJU3Xb3h.css +1 -0
- package/src/client/dist/pwa/assets/editor.api-CMsYdZTX.js +413 -0
- package/src/client/dist/pwa/assets/{efforts-BPZzMyrc.js → efforts-Db4w0alg.js} +1 -1
- package/src/client/dist/pwa/assets/engineFeatures-BjtQR0vB.js +1 -0
- package/src/client/dist/pwa/assets/es-1ib0e9jm.js +17 -0
- package/src/client/dist/pwa/assets/expand-template-CTUNyJrv.css +1 -0
- package/src/client/dist/pwa/assets/expand-template-DY8ntqUb.js +1 -0
- package/src/client/dist/pwa/assets/format-C-9jMgWb.js +435 -0
- package/src/client/dist/pwa/assets/format-CX6Cv1cD.css +1 -0
- package/src/client/dist/pwa/assets/{formatters-D0dU6VcA.js → formatters-CQ90Icr4.js} +1 -1
- package/src/client/dist/pwa/assets/fr-nj0BrM3X.js +17 -0
- package/src/client/dist/pwa/assets/{freemarker2-BqVyQSGe.js → freemarker2-KxB1BtrL.js} +1 -1
- package/src/client/dist/pwa/assets/{handlebars-9K762gzW.js → handlebars-7TByd_Zc.js} +1 -1
- package/src/client/dist/pwa/assets/{html-CRmp4AC_.js → html-BoVZ_gre.js} +1 -1
- package/src/client/dist/pwa/assets/i18n-BubYp85n.js +1 -0
- package/src/client/dist/pwa/assets/{index-BpmxQlZi.css → index-6H3m6vH7.css} +1 -1
- package/src/client/dist/pwa/assets/index-Ck1_qrff.js +18 -0
- package/src/client/dist/pwa/assets/it-B9MEgvg1.js +17 -0
- package/src/client/dist/pwa/assets/{javascript-DB9a_ZGm.js → javascript-BLL8vPQy.js} +1 -1
- package/src/client/dist/pwa/assets/layout-CFuk4mdG.js +1 -0
- package/src/client/dist/pwa/assets/{liquid-C67ZTBj7.js → liquid-DzcT-R6k.js} +1 -1
- package/src/client/dist/pwa/assets/{mdx-C_K9rc9_.js → mdx-Bguo999w.js} +1 -1
- package/src/client/dist/pwa/assets/monaco-contributions-CM7FH-iZ.js +51 -0
- package/src/client/dist/pwa/assets/monaco-contributions-Cb0afGfW.css +1 -0
- package/src/client/dist/pwa/assets/monaco.contribution-Dkq0qtWA.js +2 -0
- package/src/client/dist/pwa/assets/network-auth-C0moem8_.js +1 -0
- package/src/client/dist/pwa/assets/{notify-theme-thUiJn9n.js → notify-theme-C7mNmzKx.js} +1 -1
- package/src/client/dist/pwa/assets/pending-diff-open-BaQ_SNEF.js +1 -0
- package/src/client/dist/pwa/assets/permissionModes-CJZsT9Fl.js +36 -0
- package/src/client/dist/pwa/assets/pinia-ClsyLh3i.js +1 -0
- package/src/client/dist/pwa/assets/{project-color-hgNWqp0x.js → project-color-54JZHVy2.js} +1 -1
- package/src/client/dist/pwa/assets/{python-D1CBO9Uf.js → python-D5RQNiPF.js} +1 -1
- package/src/client/dist/pwa/assets/{razor-BCiKSj6_.js → razor-CVwoDJol.js} +1 -1
- package/src/client/dist/pwa/assets/{render-chat-markdown-Cw1_NLqs.js → render-chat-markdown-NAmPVIQp.js} +1 -1
- package/src/client/dist/pwa/assets/rolldown-runtime-hePW80VL.js +1 -0
- package/src/client/dist/pwa/assets/{typescript-D-8UENST.js → typescript-qdgwClKe.js} +1 -1
- package/src/client/dist/pwa/assets/use-checkbox-3io6bTPe.js +1 -0
- package/src/client/dist/pwa/assets/use-onboarding-DbYF8ZX_.js +2 -0
- package/src/client/dist/pwa/assets/use-quasar-B4eIgSfZ.js +1 -0
- package/src/client/dist/pwa/assets/vue-i18n-DtajJy_8.js +3 -0
- package/src/client/dist/pwa/assets/{xml-BBPvJ9QP.js → xml-DNSfQSlk.js} +1 -1
- package/src/client/dist/pwa/assets/{yaml-D4q1p7HX.js → yaml-6gnX2NnP.js} +1 -1
- package/src/client/dist/pwa/index.html +6 -5
- package/src/client/dist/pwa/sw.js +1 -1
- package/src/mcp-server/kobo-tasks-handlers.ts +17 -1
- package/src/client/dist/pwa/assets/ActivityFeed-CGMEOMn5.js +0 -8
- package/src/client/dist/pwa/assets/ChangelogPage-rvISZfJb.js +0 -1
- package/src/client/dist/pwa/assets/ClosePopup-BK6mNWsy.js +0 -1
- package/src/client/dist/pwa/assets/CreatePage-BCuDTB_0.css +0 -1
- package/src/client/dist/pwa/assets/CreatePage-BptAgVvu.js +0 -2
- package/src/client/dist/pwa/assets/DiffViewer-ZrvQ0YWN.js +0 -8
- package/src/client/dist/pwa/assets/DiffViewer-ahWtFrRy.css +0 -1
- package/src/client/dist/pwa/assets/DocumentsPanel-DuWbZiID.js +0 -1
- package/src/client/dist/pwa/assets/DrawerToggleButton-CBdOJzVZ.js +0 -1
- package/src/client/dist/pwa/assets/GitPanel-BiIXQPfL.js +0 -3
- package/src/client/dist/pwa/assets/HealthPage-DTrmhh3X.js +0 -1
- package/src/client/dist/pwa/assets/MainLayout-PSmlvfJd.js +0 -2
- package/src/client/dist/pwa/assets/MainLayout-Vq1iUPjx.css +0 -1
- package/src/client/dist/pwa/assets/QBanner-BoAbhM1O.js +0 -1
- package/src/client/dist/pwa/assets/QBtnDropdown-Ckvd3IVR.js +0 -1
- package/src/client/dist/pwa/assets/QChip-BzB_GF4Z.js +0 -1
- package/src/client/dist/pwa/assets/QItemSection-5GBV9MR2.js +0 -1
- package/src/client/dist/pwa/assets/SchedulePanel-DJWMsAoh.js +0 -1
- package/src/client/dist/pwa/assets/SearchPage-BI8w9utc.js +0 -1
- package/src/client/dist/pwa/assets/SettingsPage-Dr8oSevA.js +0 -16
- package/src/client/dist/pwa/assets/TerminalPanel-DVC58DJb.js +0 -18
- package/src/client/dist/pwa/assets/TouchPan-pqksSni0.js +0 -1
- package/src/client/dist/pwa/assets/WorkspacePage-DRP9Ypmz.js +0 -4
- package/src/client/dist/pwa/assets/build-path-tree-CQ4zZqo0.js +0 -1
- package/src/client/dist/pwa/assets/css.worker-JKZQyld4.js +0 -93
- package/src/client/dist/pwa/assets/cssMode-MGWKgqj7.js +0 -1
- package/src/client/dist/pwa/assets/documents-B6eOZeFK.js +0 -1
- package/src/client/dist/pwa/assets/editor-vZnmUXVO.css +0 -1
- package/src/client/dist/pwa/assets/editor.api-Dq41Mm99.js +0 -847
- package/src/client/dist/pwa/assets/engineFeatures-Dd_dxoOS.js +0 -1
- package/src/client/dist/pwa/assets/expand-template-B4LAsXAF.css +0 -1
- package/src/client/dist/pwa/assets/expand-template-BfZqON3q.js +0 -1
- package/src/client/dist/pwa/assets/html.worker-BUr90sI4.js +0 -506
- package/src/client/dist/pwa/assets/htmlMode-CFxFF9iH.js +0 -1
- package/src/client/dist/pwa/assets/i18n-BsWCIFmf.js +0 -1
- package/src/client/dist/pwa/assets/index-xTf0VfFG.js +0 -82
- package/src/client/dist/pwa/assets/json.worker-BOODjG9U.js +0 -62
- package/src/client/dist/pwa/assets/jsonMode-JhLTE1-C.js +0 -7
- package/src/client/dist/pwa/assets/layout-nWIyHeJf.js +0 -1
- package/src/client/dist/pwa/assets/lspLanguageFeatures-BIDd3ml0.js +0 -4
- package/src/client/dist/pwa/assets/network-auth-BPxfeLcl.js +0 -1
- package/src/client/dist/pwa/assets/pending-diff-open-BdA8vrDx.js +0 -1
- package/src/client/dist/pwa/assets/permissionModes-D09rnNgv.js +0 -36
- package/src/client/dist/pwa/assets/ts.worker-C-oGa8JT.js +0 -67738
- package/src/client/dist/pwa/assets/tsMode-Db77HzVu.js +0 -11
- package/src/client/dist/pwa/assets/use-checkbox-z-Qs4ZHY.js +0 -1
- package/src/client/dist/pwa/assets/use-id-DlouqdB8.js +0 -1
- package/src/client/dist/pwa/assets/use-onboarding-BafrD9nc.js +0 -2
- package/src/client/dist/pwa/assets/vs-Bb18eQC8.js +0 -56
- package/src/client/dist/pwa/assets/vs-JJSFJJVJ.css +0 -1
- package/src/client/dist/pwa/assets/vue-i18n-C3W5i7h6.js +0 -3
- package/src/client/dist/pwa/assets/workers-CImko7IX.js +0 -1
|
@@ -44,6 +44,7 @@ import * as gitOps from '../utils/git-ops.js';
|
|
|
44
44
|
import { logError } from '../utils/logger.js';
|
|
45
45
|
import { slugifyProjectName } from '../utils/project-slug.js';
|
|
46
46
|
import * as safePath from '../utils/safe-path.js';
|
|
47
|
+
import { resolveExtractedName } from '../utils/workspace-name.js';
|
|
47
48
|
import { resolveSiblingWorkspaceWorktreePath } from '../utils/worktree-paths.js';
|
|
48
49
|
/** Hono sub-router for workspace CRUD, tasks, agent lifecycle, git operations, and PR creation. */
|
|
49
50
|
const app = new Hono();
|
|
@@ -231,26 +232,6 @@ function parsePrNumberFromUrl(prUrl) {
|
|
|
231
232
|
}
|
|
232
233
|
return number;
|
|
233
234
|
}
|
|
234
|
-
// Mirrors the private `WORKSPACE_NAME_MAX_LENGTH` in workspace-service.ts —
|
|
235
|
-
// duplicated rather than exported to avoid coupling this route to that
|
|
236
|
-
// module's internals (and the many `vi.mock(workspace-service.js, factory)`
|
|
237
|
-
// test files that would otherwise need updating for an export nothing else
|
|
238
|
-
// consumes).
|
|
239
|
-
const WORKSPACE_NAME_MAX_LENGTH = 200;
|
|
240
|
-
/**
|
|
241
|
-
* Cap an auto-derived title (Notion page title, Sentry issue title, PR/MR
|
|
242
|
-
* title) before handing it to `updateWorkspaceName`. That function throws on
|
|
243
|
-
* anything longer — the right behaviour for a name the user typed
|
|
244
|
-
* themselves, but titles pulled from an external source (a long exception
|
|
245
|
-
* message, a verbose PR title) are not user input: truncating them is the
|
|
246
|
-
* sane default, matching the existing `slugifyBranchSegment` precedent for
|
|
247
|
-
* auto-derived branch names.
|
|
248
|
-
*/
|
|
249
|
-
function truncateWorkspaceName(name) {
|
|
250
|
-
if (name.length <= WORKSPACE_NAME_MAX_LENGTH)
|
|
251
|
-
return name;
|
|
252
|
-
return `${name.slice(0, WORKSPACE_NAME_MAX_LENGTH - 1)}…`;
|
|
253
|
-
}
|
|
254
235
|
app.get('/', (c) => {
|
|
255
236
|
try {
|
|
256
237
|
const workspaces = workspaceService.listWorkspaces();
|
|
@@ -265,7 +246,7 @@ app.get('/', (c) => {
|
|
|
265
246
|
app.post('/:id/engine-handoff-preview', migrationGuard, async (c) => {
|
|
266
247
|
try {
|
|
267
248
|
const id = c.req.param('id');
|
|
268
|
-
const body = await c.req.json();
|
|
249
|
+
const body = await c.req.json().catch(() => ({}));
|
|
269
250
|
const workspace = workspaceService.getWorkspaceWithTasks(id);
|
|
270
251
|
if (!workspace)
|
|
271
252
|
return c.json({ error: `Workspace '${id}' not found` }, 404);
|
|
@@ -282,7 +263,9 @@ app.post('/:id/engine-handoff-preview', migrationGuard, async (c) => {
|
|
|
282
263
|
app.post('/:id/switch-engine', migrationGuard, async (c) => {
|
|
283
264
|
try {
|
|
284
265
|
const id = c.req.param('id');
|
|
285
|
-
const body = await c.req
|
|
266
|
+
const body = await c.req
|
|
267
|
+
.json()
|
|
268
|
+
.catch(() => ({}));
|
|
286
269
|
const workspace = workspaceService.getWorkspaceWithTasks(id);
|
|
287
270
|
if (!workspace)
|
|
288
271
|
return c.json({ error: `Workspace '${id}' not found` }, 404);
|
|
@@ -340,7 +323,9 @@ app.post('/', migrationGuard, async (c) => {
|
|
|
340
323
|
// can read it to decide whether the setup-script caveat applies.
|
|
341
324
|
let setupScriptConfigured = false;
|
|
342
325
|
try {
|
|
343
|
-
const body = await c.req
|
|
326
|
+
const body = await c.req
|
|
327
|
+
.json()
|
|
328
|
+
.catch(() => ({}));
|
|
344
329
|
// workingBranch is derived from git when worktreePath is provided, so
|
|
345
330
|
// it's not required in that flow. The other 3 fields stay mandatory.
|
|
346
331
|
if (!body.name || !body.projectPath || !body.sourceBranch) {
|
|
@@ -357,6 +342,14 @@ app.post('/', migrationGuard, async (c) => {
|
|
|
357
342
|
if (body.workingBranch && !gitOps.isValidBranchName(body.workingBranch)) {
|
|
358
343
|
return c.json({ error: `Invalid working branch name: ${body.workingBranch}` }, 400);
|
|
359
344
|
}
|
|
345
|
+
// Same reasoning for the source branch, which is always caller-supplied and
|
|
346
|
+
// reaches `git fetch <remote> <sourceBranch>` as a bare argument.
|
|
347
|
+
if (!gitOps.isValidBranchName(body.sourceBranch)) {
|
|
348
|
+
return c.json({ error: `Invalid source branch name: ${body.sourceBranch}` }, 400);
|
|
349
|
+
}
|
|
350
|
+
if (body.comparisonId !== undefined && typeof body.comparisonId !== 'string') {
|
|
351
|
+
return c.json({ error: 'comparisonId must be a string' }, 400);
|
|
352
|
+
}
|
|
360
353
|
creationId = typeof body.creationId === 'string' && body.creationId.length > 0 ? body.creationId : undefined;
|
|
361
354
|
currentStep = 'validate';
|
|
362
355
|
emitCreateProgress(creationId, currentStep);
|
|
@@ -397,7 +390,7 @@ app.post('/', migrationGuard, async (c) => {
|
|
|
397
390
|
let baseRef = `origin/${body.sourceBranch}`;
|
|
398
391
|
let usedLocalFallback = false;
|
|
399
392
|
try {
|
|
400
|
-
gitOps.
|
|
393
|
+
await gitOps.fetchSourceBranchOrThrowAsync(body.projectPath, body.sourceBranch);
|
|
401
394
|
}
|
|
402
395
|
catch (err) {
|
|
403
396
|
const message = err instanceof Error ? err.message : String(err);
|
|
@@ -603,7 +596,13 @@ app.post('/', migrationGuard, async (c) => {
|
|
|
603
596
|
.replace(/^-|-$/g, '')
|
|
604
597
|
.substring(0, 50);
|
|
605
598
|
const safeSlug = titleSlug || 'task';
|
|
606
|
-
|
|
599
|
+
// An engine comparison posts here once per engine with the same
|
|
600
|
+
// ticket: without the suffix both halves collapse onto one branch and
|
|
601
|
+
// the second gets a random hash instead of its engine.
|
|
602
|
+
const engineSuffix = body.comparisonId && body.engine ? `-${body.engine}` : '';
|
|
603
|
+
workingBranch = ticketPrefix
|
|
604
|
+
? `${typePrefix}${ticketPrefix}--${safeSlug}${engineSuffix}`
|
|
605
|
+
: `${typePrefix}${safeSlug}${engineSuffix}`;
|
|
607
606
|
}
|
|
608
607
|
}
|
|
609
608
|
// Compute the project slug once and reuse it for both the prospective
|
|
@@ -661,6 +660,9 @@ app.post('/', migrationGuard, async (c) => {
|
|
|
661
660
|
reasoningEffort: body.reasoningEffort,
|
|
662
661
|
agentPermissionMode: resolveCreateAgentPermissionMode(body.agentPermissionMode, body.projectPath, globalSettings, body.engine ?? 'claude-code'),
|
|
663
662
|
engine: body.engine,
|
|
663
|
+
// Set by the engine-comparison flow, which posts here once per engine
|
|
664
|
+
// with the same id so the resulting workspaces can find each other.
|
|
665
|
+
comparisonId: body.comparisonId,
|
|
664
666
|
...(useReusedWorktree ? {} : { worktreesPath: globalSettings.worktreesPath }),
|
|
665
667
|
});
|
|
666
668
|
createdWorkspace = workspace;
|
|
@@ -706,9 +708,11 @@ app.post('/', migrationGuard, async (c) => {
|
|
|
706
708
|
// a custom name and Notion hasn't already filled it. Prefix with the Sentry
|
|
707
709
|
// short-id (e.g. "SEKUR-IOS-9 | TypeError: …") so the workspace stays
|
|
708
710
|
// identifiable in the sidebar without opening the panel.
|
|
709
|
-
if (sentryContent?.title && !notionContent?.title
|
|
711
|
+
if (sentryContent?.title && !notionContent?.title) {
|
|
710
712
|
const prefix = sentryContent.issueId ? `${sentryContent.issueId} | ` : '';
|
|
711
|
-
|
|
713
|
+
const renamed = resolveExtractedName(workspace.name, `${prefix}${sentryContent.title}`);
|
|
714
|
+
if (renamed)
|
|
715
|
+
workspace = workspaceService.updateWorkspaceName(workspace.id, renamed);
|
|
712
716
|
}
|
|
713
717
|
currentStep = 'create-tasks';
|
|
714
718
|
emitCreateProgress(creationId, currentStep);
|
|
@@ -732,16 +736,20 @@ app.post('/', migrationGuard, async (c) => {
|
|
|
732
736
|
// Update workspace name with Notion page title only if user didn't
|
|
733
737
|
// provide a custom name. Prefix with the Notion unique-id (e.g.
|
|
734
738
|
// Use the Notion page title when the user left the generic placeholder.
|
|
735
|
-
if (notionContent.title
|
|
736
|
-
|
|
739
|
+
if (notionContent.title) {
|
|
740
|
+
const renamed = resolveExtractedName(workspace.name, notionContent.title);
|
|
741
|
+
if (renamed)
|
|
742
|
+
workspace = workspaceService.updateWorkspaceName(workspace.id, renamed);
|
|
737
743
|
}
|
|
738
744
|
}
|
|
739
745
|
// Update workspace name with the PR/MR title if the user did not provide
|
|
740
746
|
// a custom name and neither Notion nor Sentry already claimed it. This
|
|
741
747
|
// makes the PR a THIRD context source, lowest priority of the three
|
|
742
748
|
// (Notion > Sentry > PR), consistent with the checks above.
|
|
743
|
-
if (prContent && !notionContent?.title && !sentryContent?.title
|
|
744
|
-
|
|
749
|
+
if (prContent && !notionContent?.title && !sentryContent?.title) {
|
|
750
|
+
const renamed = resolveExtractedName(workspace.name, prContent.title);
|
|
751
|
+
if (renamed)
|
|
752
|
+
workspace = workspaceService.updateWorkspaceName(workspace.id, renamed);
|
|
745
753
|
}
|
|
746
754
|
// Create manual tasks/criteria if no Notion content was extracted
|
|
747
755
|
if (!notionContent && (Array.isArray(body.tasks) || Array.isArray(body.acceptanceCriteria))) {
|
|
@@ -814,6 +822,11 @@ app.post('/', migrationGuard, async (c) => {
|
|
|
814
822
|
['.ai/thoughts/', 'thoughts/'],
|
|
815
823
|
['.ai/images/', 'images/'],
|
|
816
824
|
['.ai/.setup-script.tmp', '.setup-script.tmp'],
|
|
825
|
+
['.ai/.cleanup-script.tmp', '.cleanup-script.tmp'],
|
|
826
|
+
['.ai/.archive-script.tmp', '.archive-script.tmp'],
|
|
827
|
+
// A crash mid-hook leaves the temp script behind; the next agent
|
|
828
|
+
// would otherwise happily commit it.
|
|
829
|
+
['.ai/.hook-*.tmp', '.hook-*.tmp'],
|
|
817
830
|
['.mcp.json', ''],
|
|
818
831
|
];
|
|
819
832
|
const toAdd = [];
|
|
@@ -1401,6 +1414,31 @@ app.get('/info', (c) => {
|
|
|
1401
1414
|
return c.json({ error: message }, 500);
|
|
1402
1415
|
}
|
|
1403
1416
|
});
|
|
1417
|
+
// GET /api/workspaces/:id/comparison — the workspaces running the same task on
|
|
1418
|
+
// another engine, with the diff each one produced. Reads the git-stats cache the
|
|
1419
|
+
// pr-watcher already maintains rather than shelling out to git twice per request.
|
|
1420
|
+
app.get('/:id/comparison', (c) => {
|
|
1421
|
+
const id = c.req.param('id');
|
|
1422
|
+
try {
|
|
1423
|
+
const workspace = workspaceService.getWorkspace(id);
|
|
1424
|
+
if (!workspace)
|
|
1425
|
+
return c.json({ error: `Workspace '${id}' not found` }, 404);
|
|
1426
|
+
if (!workspace.comparisonId)
|
|
1427
|
+
return c.json({ comparisonId: null, members: [] });
|
|
1428
|
+
const stats = getAllGitStats();
|
|
1429
|
+
const members = workspaceService.listComparisonMembers(workspace.comparisonId).map((member) => ({
|
|
1430
|
+
workspace: member,
|
|
1431
|
+
// Null, not zeros: "not measured yet" and "changed nothing" are
|
|
1432
|
+
// different answers, and only one of them is true here.
|
|
1433
|
+
gitStats: stats[member.id] ?? null,
|
|
1434
|
+
}));
|
|
1435
|
+
return c.json({ comparisonId: workspace.comparisonId, members });
|
|
1436
|
+
}
|
|
1437
|
+
catch (err) {
|
|
1438
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
1439
|
+
return c.json({ error: message }, 500);
|
|
1440
|
+
}
|
|
1441
|
+
});
|
|
1404
1442
|
// POST /api/workspaces/pr-snapshot/refresh/:id — on-demand refresh of a single
|
|
1405
1443
|
// workspace's PR snapshot, driven by the Git tab refresh button. Static prefix
|
|
1406
1444
|
// keeps it ahead of `/:id` in the Hono matcher.
|
|
@@ -1458,7 +1496,13 @@ app.get('/auto-loop-states', (c) => {
|
|
|
1458
1496
|
// GET /api/workspaces/:id/auto-loop — current auto-loop status for one workspace.
|
|
1459
1497
|
app.get('/:id/auto-loop', (c) => {
|
|
1460
1498
|
try {
|
|
1461
|
-
|
|
1499
|
+
const id = c.req.param('id');
|
|
1500
|
+
// getStatus returns a default row for an unknown id, so without this the
|
|
1501
|
+
// caller gets a cheerful `auto_loop: false` for a workspace that does not
|
|
1502
|
+
// exist — the neighbouring handlers all 404.
|
|
1503
|
+
if (!workspaceService.getWorkspace(id))
|
|
1504
|
+
return c.json({ error: `Workspace '${id}' not found` }, 404);
|
|
1505
|
+
return c.json(autoLoopService.getStatus(id));
|
|
1462
1506
|
}
|
|
1463
1507
|
catch (err) {
|
|
1464
1508
|
const message = err instanceof Error ? err.message : String(err);
|
|
@@ -1821,13 +1865,18 @@ app.delete('/:id/events/:eventId', (c) => {
|
|
|
1821
1865
|
const db = getDb();
|
|
1822
1866
|
// Capture the session BEFORE deleting: since v36 there is no AFTER DELETE
|
|
1823
1867
|
// trigger, so the metrics of that session must be recomputed once, here.
|
|
1824
|
-
|
|
1825
|
-
|
|
1826
|
-
|
|
1827
|
-
|
|
1828
|
-
|
|
1829
|
-
|
|
1830
|
-
|
|
1868
|
+
// Transactional for the same reason deleteSession is: a crash between the
|
|
1869
|
+
// delete and the recompute must not leave stale metrics behind.
|
|
1870
|
+
const transaction = db.transaction(() => {
|
|
1871
|
+
const row = db
|
|
1872
|
+
.prepare('SELECT session_id FROM ws_events WHERE id = ? AND workspace_id = ?')
|
|
1873
|
+
.get(eventId, workspaceId);
|
|
1874
|
+
db.prepare('DELETE FROM ws_events WHERE id = ? AND workspace_id = ?').run(eventId, workspaceId);
|
|
1875
|
+
if (row?.session_id) {
|
|
1876
|
+
workspaceService.recomputeSessionMetrics(workspaceId, row.session_id);
|
|
1877
|
+
}
|
|
1878
|
+
});
|
|
1879
|
+
transaction();
|
|
1831
1880
|
return c.json({ ok: true });
|
|
1832
1881
|
}
|
|
1833
1882
|
catch (err) {
|
|
@@ -1843,7 +1892,9 @@ app.post('/:id/tasks', async (c) => {
|
|
|
1843
1892
|
if (!workspace) {
|
|
1844
1893
|
return c.json({ error: `Workspace '${id}' not found` }, 404);
|
|
1845
1894
|
}
|
|
1846
|
-
const body = await c.req
|
|
1895
|
+
const body = await c.req
|
|
1896
|
+
.json()
|
|
1897
|
+
.catch(() => ({}));
|
|
1847
1898
|
if (!body.title?.trim()) {
|
|
1848
1899
|
return c.json({ error: 'Title is required' }, 400);
|
|
1849
1900
|
}
|
|
@@ -1874,7 +1925,9 @@ app.patch('/:id/tasks/:taskId', async (c) => {
|
|
|
1874
1925
|
if (!task) {
|
|
1875
1926
|
return c.json({ error: `Task '${taskId}' not found in workspace '${id}'` }, 404);
|
|
1876
1927
|
}
|
|
1877
|
-
const body = await c.req
|
|
1928
|
+
const body = await c.req
|
|
1929
|
+
.json()
|
|
1930
|
+
.catch(() => ({}));
|
|
1878
1931
|
if (body.status === undefined && body.title === undefined) {
|
|
1879
1932
|
return c.json({ error: 'At least one of status or title is required' }, 400);
|
|
1880
1933
|
}
|
|
@@ -1992,6 +2045,10 @@ app.post('/:id/tasks/:taskId/notify-done', (c) => {
|
|
|
1992
2045
|
return c.json({ error: message }, 500);
|
|
1993
2046
|
}
|
|
1994
2047
|
});
|
|
2048
|
+
// Cap how many recent rows one search may scan: the LIKE below cannot use an
|
|
2049
|
+
// index past the workspace_id prefix, and better-sqlite3 is synchronous — an
|
|
2050
|
+
// unbounded scan of a months-old workspace blocks every request and WS emit.
|
|
2051
|
+
const HISTORY_SEARCH_SCAN_CAP = 20_000;
|
|
1995
2052
|
// GET /api/workspaces/:id/history-search — full-workspace persisted conversation search.
|
|
1996
2053
|
app.get('/:id/history-search', (c) => {
|
|
1997
2054
|
try {
|
|
@@ -2004,10 +2061,13 @@ app.get('/:id/history-search', (c) => {
|
|
|
2004
2061
|
const limit = Math.min(parseInt(c.req.query('limit') ?? '30', 10) || 30, 100);
|
|
2005
2062
|
const needle = `%${query.replace(/[%_]/g, '\\$&')}%`;
|
|
2006
2063
|
const rows = getDb()
|
|
2007
|
-
.prepare(`SELECT id, session_id, type, payload, created_at FROM
|
|
2008
|
-
|
|
2009
|
-
|
|
2010
|
-
|
|
2064
|
+
.prepare(`SELECT id, session_id, type, payload, created_at FROM (
|
|
2065
|
+
SELECT rowid AS rid, id, session_id, type, payload, created_at FROM ws_events
|
|
2066
|
+
WHERE workspace_id = ? ORDER BY rowid DESC LIMIT ?
|
|
2067
|
+
)
|
|
2068
|
+
WHERE payload LIKE ? ESCAPE '\\'
|
|
2069
|
+
ORDER BY rid DESC LIMIT ?`)
|
|
2070
|
+
.all(id, HISTORY_SEARCH_SCAN_CAP, needle, limit * 4);
|
|
2011
2071
|
const normalized = rows.flatMap((row) => {
|
|
2012
2072
|
const result = readableHistorySearchResult(row.type, row.payload, query);
|
|
2013
2073
|
if (!result)
|
|
@@ -2063,6 +2123,59 @@ function readableHistorySearchResult(type, rawPayload, query) {
|
|
|
2063
2123
|
snippet: `${start > 0 ? '…' : ''}${text.slice(start, end).replace(/\s+/g, ' ').trim()}${end < text.length ? '…' : ''}`,
|
|
2064
2124
|
};
|
|
2065
2125
|
}
|
|
2126
|
+
/**
|
|
2127
|
+
* GET /api/workspaces/:id/sessions/:sessionId/summary — why a session ended.
|
|
2128
|
+
*
|
|
2129
|
+
* A session can stop for many reasons (quota, transient failure, drain
|
|
2130
|
+
* watchdog, auto-loop stall, failed resume, manual stop, natural end) and none
|
|
2131
|
+
* of them were legible after the fact: you had to read the event feed to work
|
|
2132
|
+
* out what happened overnight. Everything here is already collected —
|
|
2133
|
+
* `agent_sessions` and `session_event_metrics` — it was just never put in one
|
|
2134
|
+
* place. Note that `end_reason` itself only records four values (completed /
|
|
2135
|
+
* error / killed / watchdog); quota, stall and failed resume are visible
|
|
2136
|
+
* through the workspace status and the auto-loop events, not through it.
|
|
2137
|
+
*/
|
|
2138
|
+
app.get('/:id/sessions/:sessionId/summary', (c) => {
|
|
2139
|
+
try {
|
|
2140
|
+
const id = c.req.param('id');
|
|
2141
|
+
const sessionId = c.req.param('sessionId');
|
|
2142
|
+
if (!workspaceService.getWorkspace(id))
|
|
2143
|
+
return c.json({ error: `Workspace '${id}' not found` }, 404);
|
|
2144
|
+
const db = getDb();
|
|
2145
|
+
const session = db
|
|
2146
|
+
.prepare(`SELECT id, engine, model, status, end_reason, started_at, ended_at
|
|
2147
|
+
FROM agent_sessions WHERE id = ? AND workspace_id = ?`)
|
|
2148
|
+
.get(sessionId, id);
|
|
2149
|
+
if (!session)
|
|
2150
|
+
return c.json({ error: `Session '${sessionId}' not found` }, 404);
|
|
2151
|
+
const metrics = db
|
|
2152
|
+
.prepare(`SELECT tool_calls, errors, input_tokens, output_tokens
|
|
2153
|
+
FROM session_event_metrics WHERE workspace_id = ? AND session_id = ?`)
|
|
2154
|
+
.get(id, sessionId);
|
|
2155
|
+
const startedAt = Date.parse(session.started_at);
|
|
2156
|
+
const endedAt = session.ended_at ? Date.parse(session.ended_at) : null;
|
|
2157
|
+
return c.json({
|
|
2158
|
+
sessionId: session.id,
|
|
2159
|
+
engine: session.engine,
|
|
2160
|
+
model: session.model,
|
|
2161
|
+
status: session.status,
|
|
2162
|
+
// Null for sessions that ended before this was recorded, and for one
|
|
2163
|
+
// still running. The client says "unknown" rather than inventing a cause.
|
|
2164
|
+
endReason: session.end_reason,
|
|
2165
|
+
startedAt: session.started_at,
|
|
2166
|
+
endedAt: session.ended_at,
|
|
2167
|
+
durationMs: endedAt !== null && !Number.isNaN(startedAt) ? endedAt - startedAt : null,
|
|
2168
|
+
toolCalls: metrics?.tool_calls ?? 0,
|
|
2169
|
+
errors: metrics?.errors ?? 0,
|
|
2170
|
+
inputTokens: metrics?.input_tokens ?? 0,
|
|
2171
|
+
outputTokens: metrics?.output_tokens ?? 0,
|
|
2172
|
+
});
|
|
2173
|
+
}
|
|
2174
|
+
catch (err) {
|
|
2175
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
2176
|
+
return c.json({ error: message }, 500);
|
|
2177
|
+
}
|
|
2178
|
+
});
|
|
2066
2179
|
// GET /api/workspaces/:id/session-metrics — compact observability data for the timeline.
|
|
2067
2180
|
app.get('/:id/session-metrics', (c) => {
|
|
2068
2181
|
try {
|
|
@@ -2106,15 +2219,22 @@ app.get('/:id/diagnostic.json', (c) => {
|
|
|
2106
2219
|
const sessions = db
|
|
2107
2220
|
.prepare('SELECT id, engine_session_id, status, started_at, ended_at, model, name FROM agent_sessions WHERE workspace_id = ? ORDER BY started_at ASC')
|
|
2108
2221
|
.all(id);
|
|
2222
|
+
// Bounded on purpose: better-sqlite3 is synchronous, so an unbounded read
|
|
2223
|
+
// materialises every event the workspace ever produced and blocks the
|
|
2224
|
+
// event loop while `c.json` serialises them. A diagnostic bundle wants a
|
|
2225
|
+
// representative trace, not the entire history.
|
|
2226
|
+
const DIAGNOSTIC_EVENT_LIMIT = 50_000;
|
|
2109
2227
|
const events = db
|
|
2110
|
-
.prepare('SELECT id, session_id, type, created_at FROM ws_events WHERE workspace_id = ? ORDER BY rowid ASC')
|
|
2111
|
-
.all(id);
|
|
2228
|
+
.prepare('SELECT id, session_id, type, created_at FROM ws_events WHERE workspace_id = ? ORDER BY rowid ASC LIMIT ?')
|
|
2229
|
+
.all(id, DIAGNOSTIC_EVENT_LIMIT);
|
|
2230
|
+
const eventsTruncated = events.length === DIAGNOSTIC_EVENT_LIMIT;
|
|
2112
2231
|
c.header('Content-Disposition', `attachment; filename="${workspace.name.replace(/[^a-z0-9]+/gi, '-').toLowerCase() || id}-diagnostic.json"`);
|
|
2113
2232
|
return c.json({
|
|
2114
2233
|
generatedAt: new Date().toISOString(),
|
|
2115
2234
|
workspace: { id, name: workspace.name, engine: workspace.engine },
|
|
2116
2235
|
sessions,
|
|
2117
2236
|
events,
|
|
2237
|
+
eventsTruncated,
|
|
2118
2238
|
});
|
|
2119
2239
|
}
|
|
2120
2240
|
catch (err) {
|
|
@@ -2206,21 +2326,24 @@ app.get('/:id/events', (c) => {
|
|
|
2206
2326
|
if (firstRow) {
|
|
2207
2327
|
const older = session
|
|
2208
2328
|
? db
|
|
2209
|
-
.prepare('SELECT
|
|
2329
|
+
.prepare('SELECT 1 as c FROM ws_events WHERE workspace_id = ? AND (session_id = ? OR session_id IS NULL) AND rowid < ? LIMIT 1')
|
|
2210
2330
|
.get(id, session, firstRow.rowid)
|
|
2211
2331
|
: db
|
|
2212
|
-
.prepare('SELECT
|
|
2332
|
+
.prepare('SELECT 1 as c FROM ws_events WHERE workspace_id = ? AND rowid < ? LIMIT 1')
|
|
2213
2333
|
.get(id, firstRow.rowid);
|
|
2214
|
-
hasMore = older
|
|
2334
|
+
hasMore = older !== undefined;
|
|
2215
2335
|
}
|
|
2216
2336
|
}
|
|
2217
2337
|
else {
|
|
2218
|
-
|
|
2338
|
+
// `LIMIT 1 OFFSET <page size>` answers "is there anything past this
|
|
2339
|
+
// page?" by reading one row, where COUNT(*) walked every event the
|
|
2340
|
+
// workspace ever produced — on each page, synchronously.
|
|
2341
|
+
const beyond = session
|
|
2219
2342
|
? db
|
|
2220
|
-
.prepare('SELECT
|
|
2221
|
-
.get(id, session)
|
|
2222
|
-
: db.prepare('SELECT
|
|
2223
|
-
hasMore =
|
|
2343
|
+
.prepare('SELECT 1 as c FROM ws_events WHERE workspace_id = ? AND (session_id = ? OR session_id IS NULL) LIMIT 1 OFFSET ?')
|
|
2344
|
+
.get(id, session, rows.length)
|
|
2345
|
+
: db.prepare('SELECT 1 as c FROM ws_events WHERE workspace_id = ? LIMIT 1 OFFSET ?').get(id, rows.length);
|
|
2346
|
+
hasMore = beyond !== undefined;
|
|
2224
2347
|
}
|
|
2225
2348
|
}
|
|
2226
2349
|
return c.json({ events, hasMore });
|
|
@@ -2415,7 +2538,7 @@ app.delete('/:id/pr-watch-disabled', async (c) => {
|
|
|
2415
2538
|
app.put('/:id/tags', async (c) => {
|
|
2416
2539
|
const { id } = c.req.param();
|
|
2417
2540
|
try {
|
|
2418
|
-
const body = await c.req.json();
|
|
2541
|
+
const body = await c.req.json().catch(() => ({}));
|
|
2419
2542
|
if (!Array.isArray(body.tags)) {
|
|
2420
2543
|
return c.json({ error: 'tags must be an array of strings' }, 400);
|
|
2421
2544
|
}
|
|
@@ -2652,26 +2775,31 @@ app.post('/:id/run-setup-script', async (c) => {
|
|
|
2652
2775
|
if (setupScriptRunning.has(id)) {
|
|
2653
2776
|
return c.json({ error: 'Setup script is already running for this workspace' }, 409);
|
|
2654
2777
|
}
|
|
2655
|
-
//
|
|
2656
|
-
//
|
|
2657
|
-
|
|
2658
|
-
if (agentManager.getAgentStatus(id)) {
|
|
2659
|
-
await agentManager.stopAgentAndWait(id);
|
|
2660
|
-
}
|
|
2661
|
-
}
|
|
2662
|
-
catch (err) {
|
|
2663
|
-
console.error(`[workspaces] stopAgentAndWait before setup script failed for '${id}':`, err);
|
|
2664
|
-
}
|
|
2665
|
-
const effectiveSettings = settingsService.getEffectiveSettings(workspace.projectPath);
|
|
2666
|
-
if (!effectiveSettings.setupScript) {
|
|
2667
|
-
return c.json({ error: 'No setup script configured' }, 400);
|
|
2668
|
-
}
|
|
2669
|
-
const worktreePath = workspace.worktreePath;
|
|
2670
|
-
if (!fs.existsSync(worktreePath)) {
|
|
2671
|
-
return c.json({ error: `Worktree path does not exist: ${worktreePath}` }, 400);
|
|
2672
|
-
}
|
|
2778
|
+
// Claim before the first await. Checking here and claiming after the agent
|
|
2779
|
+
// stop below let two concurrent calls both pass the check and then run the
|
|
2780
|
+
// user's script twice, concurrently, in the same worktree.
|
|
2673
2781
|
setupScriptRunning.add(id);
|
|
2782
|
+
// Everything below releases the claim, including the validation failures:
|
|
2783
|
+
// a workspace left marked as running would refuse every later attempt.
|
|
2674
2784
|
try {
|
|
2785
|
+
// Stop the running agent before re-running the setup script — the script
|
|
2786
|
+
// rewrites the worktree the agent may still be writing to.
|
|
2787
|
+
try {
|
|
2788
|
+
if (agentManager.getAgentStatus(id)) {
|
|
2789
|
+
await agentManager.stopAgentAndWait(id);
|
|
2790
|
+
}
|
|
2791
|
+
}
|
|
2792
|
+
catch (err) {
|
|
2793
|
+
console.error(`[workspaces] stopAgentAndWait before setup script failed for '${id}':`, err);
|
|
2794
|
+
}
|
|
2795
|
+
const effectiveSettings = settingsService.getEffectiveSettings(workspace.projectPath);
|
|
2796
|
+
if (!effectiveSettings.setupScript) {
|
|
2797
|
+
return c.json({ error: 'No setup script configured' }, 400);
|
|
2798
|
+
}
|
|
2799
|
+
const worktreePath = workspace.worktreePath;
|
|
2800
|
+
if (!fs.existsSync(worktreePath)) {
|
|
2801
|
+
return c.json({ error: `Worktree path does not exist: ${worktreePath}` }, 400);
|
|
2802
|
+
}
|
|
2675
2803
|
const result = await runSetupScript(workspace.id, worktreePath, effectiveSettings.setupScript, {
|
|
2676
2804
|
workspaceName: workspace.name,
|
|
2677
2805
|
branchName: workspace.workingBranch,
|
|
@@ -2790,7 +2918,7 @@ async function deleteWorkspaceWithSideEffects(workspace, opts) {
|
|
|
2790
2918
|
// a few lines below: firing and forgetting here pulled the directory from
|
|
2791
2919
|
// under an agent still writing to it, destroying uncommitted work.
|
|
2792
2920
|
try {
|
|
2793
|
-
await agentManager.stopAgentAndWait(workspace.id);
|
|
2921
|
+
await agentManager.stopAgentAndWait(workspace.id, undefined, 'delete');
|
|
2794
2922
|
}
|
|
2795
2923
|
catch (err) {
|
|
2796
2924
|
console.error(`[workspaces] stopAgentAndWait during delete failed for '${workspace.name}':`, err);
|
|
@@ -2866,6 +2994,11 @@ async function deleteWorkspaceWithSideEffects(workspace, opts) {
|
|
|
2866
2994
|
}
|
|
2867
2995
|
// Delete workspace from DB (cascades to tasks, sessions, events)
|
|
2868
2996
|
workspaceService.deleteWorkspace(workspace.id);
|
|
2997
|
+
// Announce the deletion so any other tab with this workspace open (or a
|
|
2998
|
+
// live terminal attached to it) can react — mirrors workspace:archived's
|
|
2999
|
+
// cross-tab-sync purpose. Ephemeral: a deleted workspace has no future
|
|
3000
|
+
// ws_events history to replay, so persisting this would be pointless.
|
|
3001
|
+
wsService.emitEphemeral(workspace.id, 'workspace:deleted', { workspaceId: workspace.id });
|
|
2869
3002
|
return warnings;
|
|
2870
3003
|
}
|
|
2871
3004
|
// DELETE /api/workspaces/archived — bulk-delete every archived workspace.
|
|
@@ -3168,7 +3301,7 @@ app.post('/:id/rollback-file', async (c) => {
|
|
|
3168
3301
|
if (!workspace) {
|
|
3169
3302
|
return c.json({ error: `Workspace '${id}' not found` }, 404);
|
|
3170
3303
|
}
|
|
3171
|
-
const body = await c.req.json();
|
|
3304
|
+
const body = await c.req.json().catch(() => ({}));
|
|
3172
3305
|
const filePath = typeof body?.path === 'string' ? body.path.trim() : '';
|
|
3173
3306
|
if (!filePath) {
|
|
3174
3307
|
return c.json({ error: 'Missing or invalid `path` field' }, 400);
|
|
@@ -3410,10 +3543,10 @@ app.post('/:id/push', async (c) => {
|
|
|
3410
3543
|
// no-options call shape identical to before for callers/tests that
|
|
3411
3544
|
// assert on argument count.
|
|
3412
3545
|
if (force) {
|
|
3413
|
-
gitOps.
|
|
3546
|
+
await gitOps.pushBranchAsync(worktreePath, workspace.workingBranch, { force: true });
|
|
3414
3547
|
}
|
|
3415
3548
|
else {
|
|
3416
|
-
gitOps.
|
|
3549
|
+
await gitOps.pushBranchAsync(worktreePath, workspace.workingBranch);
|
|
3417
3550
|
}
|
|
3418
3551
|
}
|
|
3419
3552
|
catch (err) {
|
|
@@ -3432,14 +3565,14 @@ app.post('/:id/push', async (c) => {
|
|
|
3432
3565
|
});
|
|
3433
3566
|
// POST /api/workspaces/:id/fetch — git fetch the workspace repo (all branches of origin).
|
|
3434
3567
|
// Read-only: updates remote-tracking refs, never touches the working tree.
|
|
3435
|
-
app.post('/:id/fetch', (c) => {
|
|
3568
|
+
app.post('/:id/fetch', async (c) => {
|
|
3436
3569
|
try {
|
|
3437
3570
|
const id = c.req.param('id');
|
|
3438
3571
|
const workspace = workspaceService.getWorkspace(id);
|
|
3439
3572
|
if (!workspace) {
|
|
3440
3573
|
return c.json({ error: `Workspace '${id}' not found` }, 404);
|
|
3441
3574
|
}
|
|
3442
|
-
gitOps.
|
|
3575
|
+
await gitOps.fetchAllBranchesAsync(workspace.worktreePath);
|
|
3443
3576
|
return c.json({ ok: true });
|
|
3444
3577
|
}
|
|
3445
3578
|
catch (err) {
|
|
@@ -3448,7 +3581,7 @@ app.post('/:id/fetch', (c) => {
|
|
|
3448
3581
|
}
|
|
3449
3582
|
});
|
|
3450
3583
|
// POST /api/workspaces/:id/pull — pull working branch from origin (fast-forward only)
|
|
3451
|
-
app.post('/:id/pull', (c) => {
|
|
3584
|
+
app.post('/:id/pull', async (c) => {
|
|
3452
3585
|
try {
|
|
3453
3586
|
const id = c.req.param('id');
|
|
3454
3587
|
const workspace = workspaceService.getWorkspace(id);
|
|
@@ -3458,7 +3591,7 @@ app.post('/:id/pull', (c) => {
|
|
|
3458
3591
|
const worktreePath = workspace.worktreePath;
|
|
3459
3592
|
const autostash = c.req.query('autostash') === '1';
|
|
3460
3593
|
try {
|
|
3461
|
-
gitOps.
|
|
3594
|
+
await gitOps.pullBranchAsync(worktreePath, workspace.workingBranch, 'origin', { autostash });
|
|
3462
3595
|
}
|
|
3463
3596
|
catch (err) {
|
|
3464
3597
|
if (err instanceof gitOps.DirtyWorktreeError) {
|
|
@@ -3478,7 +3611,7 @@ app.post('/:id/pull', (c) => {
|
|
|
3478
3611
|
}
|
|
3479
3612
|
});
|
|
3480
3613
|
/** Rebase the workspace branch onto its source branch. */
|
|
3481
|
-
app.post('/:id/rebase', (c) => {
|
|
3614
|
+
app.post('/:id/rebase', async (c) => {
|
|
3482
3615
|
try {
|
|
3483
3616
|
const id = c.req.param('id');
|
|
3484
3617
|
const workspace = workspaceService.getWorkspace(id);
|
|
@@ -3486,7 +3619,7 @@ app.post('/:id/rebase', (c) => {
|
|
|
3486
3619
|
return c.json({ error: `Workspace '${id}' not found` }, 404);
|
|
3487
3620
|
const autostash = c.req.query('autostash') === '1';
|
|
3488
3621
|
const worktreePath = workspace.worktreePath;
|
|
3489
|
-
gitOps.
|
|
3622
|
+
await gitOps.rebaseBranchAsync(worktreePath, workspace.sourceBranch, { autostash });
|
|
3490
3623
|
return c.json({ success: true });
|
|
3491
3624
|
}
|
|
3492
3625
|
catch (err) {
|
|
@@ -3501,7 +3634,7 @@ app.post('/:id/rebase', (c) => {
|
|
|
3501
3634
|
}
|
|
3502
3635
|
});
|
|
3503
3636
|
/** Merge the source branch into the workspace branch (non-fast-forward). */
|
|
3504
|
-
app.post('/:id/merge', (c) => {
|
|
3637
|
+
app.post('/:id/merge', async (c) => {
|
|
3505
3638
|
try {
|
|
3506
3639
|
const id = c.req.param('id');
|
|
3507
3640
|
const workspace = workspaceService.getWorkspace(id);
|
|
@@ -3509,7 +3642,7 @@ app.post('/:id/merge', (c) => {
|
|
|
3509
3642
|
return c.json({ error: `Workspace '${id}' not found` }, 404);
|
|
3510
3643
|
const autostash = c.req.query('autostash') === '1';
|
|
3511
3644
|
const worktreePath = workspace.worktreePath;
|
|
3512
|
-
gitOps.
|
|
3645
|
+
await gitOps.mergeBranchAsync(worktreePath, workspace.sourceBranch, { autostash });
|
|
3513
3646
|
return c.json({ success: true });
|
|
3514
3647
|
}
|
|
3515
3648
|
catch (err) {
|
|
@@ -3702,9 +3835,14 @@ Start now.`;
|
|
|
3702
3835
|
app.post('/:id/change-pr-base', async (c) => {
|
|
3703
3836
|
try {
|
|
3704
3837
|
const id = c.req.param('id');
|
|
3705
|
-
const body = await c.req.json();
|
|
3838
|
+
const body = await c.req.json().catch(() => ({}));
|
|
3706
3839
|
if (!body.base)
|
|
3707
3840
|
return c.json({ error: 'Missing base parameter' }, 400);
|
|
3841
|
+
// Forwarded to `gh pr edit --base <base>` / the GitLab equivalent, so a
|
|
3842
|
+
// name starting with `-` would read as a CLI option rather than a branch.
|
|
3843
|
+
if (!gitOps.isValidBranchName(body.base)) {
|
|
3844
|
+
return c.json({ error: `Invalid base branch name: ${body.base}` }, 400);
|
|
3845
|
+
}
|
|
3708
3846
|
const workspace = workspaceService.getWorkspace(id);
|
|
3709
3847
|
if (!workspace)
|
|
3710
3848
|
return c.json({ error: `Workspace '${id}' not found` }, 404);
|
|
@@ -3797,9 +3935,14 @@ app.post('/:id/change-source-branch', async (c) => {
|
|
|
3797
3935
|
const id = c.req.param('id');
|
|
3798
3936
|
const workspace = workspaceService.getWorkspace(id);
|
|
3799
3937
|
try {
|
|
3800
|
-
const body = await c.req.json();
|
|
3938
|
+
const body = await c.req.json().catch(() => ({}));
|
|
3801
3939
|
if (!body.newBase)
|
|
3802
3940
|
return c.json({ error: 'Missing newBase parameter' }, 400);
|
|
3941
|
+
// Reaches git (fetch/reset/cherry-pick) and the forge CLI as a bare
|
|
3942
|
+
// argument, and is exported to the custom script as KOBO_NEW_BASE.
|
|
3943
|
+
if (!gitOps.isValidBranchName(body.newBase)) {
|
|
3944
|
+
return c.json({ error: `Invalid source branch name: ${body.newBase}` }, 400);
|
|
3945
|
+
}
|
|
3803
3946
|
if (!workspace)
|
|
3804
3947
|
return c.json({ error: `Workspace '${id}' not found` }, 404);
|
|
3805
3948
|
const result = await changeSourceBranch(id, body.newBase);
|
|
@@ -3830,9 +3973,15 @@ app.post('/:id/change-source-branch', async (c) => {
|
|
|
3830
3973
|
app.post('/:id/cancel-source-change', async (c) => {
|
|
3831
3974
|
try {
|
|
3832
3975
|
const id = c.req.param('id');
|
|
3833
|
-
const body = await c.req.json();
|
|
3976
|
+
const body = await c.req.json().catch(() => ({}));
|
|
3834
3977
|
if (!body.previousBase)
|
|
3835
3978
|
return c.json({ error: 'Missing previousBase parameter' }, 400);
|
|
3979
|
+
// Persisted as the workspace source branch, and from there it reaches
|
|
3980
|
+
// `git fetch origin <sourceBranch>` as a bare argument — including from a
|
|
3981
|
+
// plain visit to the Diff tab, with no further user action.
|
|
3982
|
+
if (!gitOps.isValidBranchName(body.previousBase)) {
|
|
3983
|
+
return c.json({ error: `Invalid source branch name: ${body.previousBase}` }, 400);
|
|
3984
|
+
}
|
|
3836
3985
|
const workspace = workspaceService.getWorkspace(id);
|
|
3837
3986
|
if (!workspace)
|
|
3838
3987
|
return c.json({ error: `Workspace '${id}' not found` }, 404);
|
|
@@ -3860,13 +4009,13 @@ app.post('/:id/cancel-source-change', async (c) => {
|
|
|
3860
4009
|
}
|
|
3861
4010
|
});
|
|
3862
4011
|
/** Force-push the working branch with --force-with-lease (after a history rewrite). */
|
|
3863
|
-
app.post('/:id/force-push', (c) => {
|
|
4012
|
+
app.post('/:id/force-push', async (c) => {
|
|
3864
4013
|
try {
|
|
3865
4014
|
const id = c.req.param('id');
|
|
3866
4015
|
const workspace = workspaceService.getWorkspace(id);
|
|
3867
4016
|
if (!workspace)
|
|
3868
4017
|
return c.json({ error: `Workspace '${id}' not found` }, 404);
|
|
3869
|
-
gitOps.
|
|
4018
|
+
await gitOps.pushBranchAsync(workspace.worktreePath, workspace.workingBranch, { force: true });
|
|
3870
4019
|
return c.json({ success: true });
|
|
3871
4020
|
}
|
|
3872
4021
|
catch (err) {
|