@loicngr/kobo 1.11.39 → 1.11.40
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 +12 -0
- package/CHANGELOG.md +4 -0
- 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/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 +225 -241
- 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/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/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/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-MqsC-95i.js +8 -0
- package/src/client/dist/pwa/assets/{ChangelogPage-D8563008.js → ChangelogPage-BxURWYxa.js} +1 -1
- package/src/client/dist/pwa/assets/{ComparisonPanel-Bd0X08sg.js → ComparisonPanel-DjiMDAUi.js} +2 -2
- package/src/client/dist/pwa/assets/CreatePage-B-rhBmqv.js +2 -0
- package/src/client/dist/pwa/assets/DashboardPage-mA5RZf1t.js +1 -0
- package/src/client/dist/pwa/assets/DiffViewer-Ci7ucmy3.js +8 -0
- package/src/client/dist/pwa/assets/{DiffViewer-DnR79ycb.css → DiffViewer-DEPOa3D7.css} +1 -1
- package/src/client/dist/pwa/assets/{DocumentsPanel-BJsJ0fNs.js → DocumentsPanel-RZTvcJkT.js} +1 -1
- package/src/client/dist/pwa/assets/{DrawerToggleButton-BVLO5K5d.js → DrawerToggleButton-CTOc3yA0.js} +1 -1
- package/src/client/dist/pwa/assets/GitPanel-_kL2gMoy.js +3 -0
- package/src/client/dist/pwa/assets/{HealthPage-RFZS7Lao.js → HealthPage-D3__hKJ5.js} +1 -1
- package/src/client/dist/pwa/assets/MainLayout-B3JKUm3n.js +2 -0
- package/src/client/dist/pwa/assets/{QBtnDropdown-X8oNU5Mb.js → QBtnDropdown-td0RAVPx.js} +1 -1
- package/src/client/dist/pwa/assets/{QDrawer-Cij17skX.js → QDrawer-BkyFPV7a.js} +1 -1
- package/src/client/dist/pwa/assets/QSelect-WspK3KC7.js +1 -0
- package/src/client/dist/pwa/assets/{QTooltip-CwcCGSWq.js → QTooltip-7pw7iuV0.js} +1 -1
- package/src/client/dist/pwa/assets/SchedulePanel-BAA4Aajn.css +1 -0
- package/src/client/dist/pwa/assets/SchedulePanel-CLNM13oo.js +1 -0
- package/src/client/dist/pwa/assets/SearchPage-BGc1oQSM.css +1 -0
- package/src/client/dist/pwa/assets/SearchPage-CmRgIvu4.js +1 -0
- package/src/client/dist/pwa/assets/SettingsPage-Cj6Y_TlN.js +9 -0
- package/src/client/dist/pwa/assets/SettingsPage-sVlsJQ0b.css +1 -0
- package/src/client/dist/pwa/assets/{SplitWorkspacePage-D7lGFRKs.js → SplitWorkspacePage-CCHGEsKf.js} +1 -1
- package/src/client/dist/pwa/assets/{TerminalPanel-DI8DHb9e.js → TerminalPanel-ijmsYk-2.js} +9 -9
- package/src/client/dist/pwa/assets/{TouchPan-B3nrkCY8.js → TouchPan-DuQFjlQF.js} +1 -1
- package/src/client/dist/pwa/assets/{TourReplayButton-DU1UShMY.js → TourReplayButton-C5RF0lOK.js} +1 -1
- package/src/client/dist/pwa/assets/{WorkspaceAttentionLabels-C-yN5IX8.js → WorkspaceAttentionLabels-BR_XElog.js} +1 -1
- package/src/client/dist/pwa/assets/WorkspacePage-BGCildyP.js +4 -0
- package/src/client/dist/pwa/assets/{WorkspacePage-BAC6u9OP.css → WorkspacePage-CqcMweNL.css} +1 -1
- package/src/client/dist/pwa/assets/{browser-f9MHQjfi.js → browser-DUqzdkbg.js} +1 -1
- package/src/client/dist/pwa/assets/{build-path-tree-BWBzP8os.js → build-path-tree-ImiSpzfI.js} +1 -1
- package/src/client/dist/pwa/assets/de-CaHID9mW.js +17 -0
- package/src/client/dist/pwa/assets/{documents-C2cLnEBQ.js → documents-2C1hn1pw.js} +1 -1
- package/src/client/dist/pwa/assets/{editor.api-sw146MWN.js → editor.api-Bm1vVaBr.js} +1 -1
- package/src/client/dist/pwa/assets/{efforts-C5ZZgCH4.js → efforts-_D8DFYXo.js} +1 -1
- package/src/client/dist/pwa/assets/engineFeatures-CZ9qfBYq.js +1 -0
- package/src/client/dist/pwa/assets/es-ZhzlCmer.js +17 -0
- package/src/client/dist/pwa/assets/{expand-template-B6xAlAaM.js → expand-template-CbDNnTPP.js} +1 -1
- package/src/client/dist/pwa/assets/{format-BwWP6Aay.js → format-pPImdFUy.js} +1 -1
- package/src/client/dist/pwa/assets/{formatters-DyWtO-nR.js → formatters-dTX38ZsV.js} +1 -1
- package/src/client/dist/pwa/assets/fr-BfXw7L7J.js +17 -0
- package/src/client/dist/pwa/assets/{freemarker2-DZ5Hm8UH.js → freemarker2-DGluDnAm.js} +1 -1
- package/src/client/dist/pwa/assets/{handlebars-BRUGpTA7.js → handlebars-BgviTnpD.js} +1 -1
- package/src/client/dist/pwa/assets/{html-DNOayNNs.js → html-CV-MrunL.js} +1 -1
- package/src/client/dist/pwa/assets/i18n-CmXBenUp.js +1 -0
- package/src/client/dist/pwa/assets/index-9tiw3NjK.js +20 -0
- package/src/client/dist/pwa/assets/it-CHAurcTF.js +17 -0
- package/src/client/dist/pwa/assets/{javascript-C0tDQU3z.js → javascript-C2VFcHf2.js} +1 -1
- package/src/client/dist/pwa/assets/{liquid-CN46O6OF.js → liquid-Dg9ZhbP5.js} +1 -1
- package/src/client/dist/pwa/assets/{mdx-DM-lrr8Q.js → mdx-ImgrPYNl.js} +1 -1
- package/src/client/dist/pwa/assets/{monaco-contributions-CkN7y-8c.js → monaco-contributions-waWZXS1T.js} +1 -1
- package/src/client/dist/pwa/assets/{monaco.contribution-2BcAOLjc.js → monaco.contribution-BB2rtCHM.js} +2 -2
- package/src/client/dist/pwa/assets/network-auth-h6O0qCMA.js +1 -0
- package/src/client/dist/pwa/assets/{notify-theme-a91Tcr6o.js → notify-theme-Dyg-FGI6.js} +1 -1
- package/src/client/dist/pwa/assets/{pending-diff-open-DOtELZfB.js → pending-diff-open-Dfpg92li.js} +1 -1
- package/src/client/dist/pwa/assets/{permissionModes-C-9Crj-c.js → permissionModes-DDWmNBW1.js} +1 -1
- package/src/client/dist/pwa/assets/{project-color-BRe4oyNx.js → project-color-XUfY_zbw.js} +1 -1
- package/src/client/dist/pwa/assets/{python-BRpJG6sZ.js → python-DVLs-DBA.js} +1 -1
- package/src/client/dist/pwa/assets/{razor-CehCmvHl.js → razor-D34BqMwB.js} +1 -1
- package/src/client/dist/pwa/assets/{render-chat-markdown-DlbQLZ02.js → render-chat-markdown-DwUkPy-O.js} +1 -1
- package/src/client/dist/pwa/assets/{typescript-Dt_8mVuj.js → typescript-C7-UpNXx.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-DfE-kwrX.js +2 -0
- package/src/client/dist/pwa/assets/{workspace-templates-GCObcjO6.js → workspace-templates-DwwdOEAq.js} +1 -1
- package/src/client/dist/pwa/assets/{xml-_gOwdzB4.js → xml-Cx1oQhEr.js} +1 -1
- package/src/client/dist/pwa/assets/{yaml-BxeBqpGf.js → yaml-BQOiJsdx.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 +118 -2
- 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-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-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/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
|
@@ -3,6 +3,7 @@ import { isValidSkillSuite } from '../../shared/skill-suite-prompts.js';
|
|
|
3
3
|
import { getDb } from '../db/index.js';
|
|
4
4
|
import { getBackendPort } from '../services/agent/orchestrator.js';
|
|
5
5
|
import { DEFAULT_NOTION_INITIAL_PROMPT, DEFAULT_SENTRY_INITIAL_PROMPT, } from '../services/initial-prompt-template-service.js';
|
|
6
|
+
import { getMcpConnectionInfo } from '../services/mcp-connection-info-service.js';
|
|
6
7
|
import { generateToken, getLanUrls } from '../services/network-access-service.js';
|
|
7
8
|
import { DEFAULT_REVIEW_PROMPT_TEMPLATE } from '../services/review-template-service.js';
|
|
8
9
|
import { DEFAULT_CHANGE_SOURCE_BRANCH_SCRIPT } from '../services/settings-defaults.js';
|
|
@@ -82,6 +83,10 @@ app.get('/ws-events-retention-preview', (c) => {
|
|
|
82
83
|
return c.json({ error: message }, 500);
|
|
83
84
|
}
|
|
84
85
|
});
|
|
86
|
+
// GET /api/settings/mcp — protected connection metadata, without credentials
|
|
87
|
+
app.get('/mcp', (c) => {
|
|
88
|
+
return c.json(getMcpConnectionInfo(getBackendPort(), settingsService.getGlobalSettings(), getDb().name));
|
|
89
|
+
});
|
|
85
90
|
// GET /api/settings/network — network access state + LAN URLs
|
|
86
91
|
app.get('/network', (c) => {
|
|
87
92
|
try {
|
|
@@ -1,5 +1,6 @@
|
|
|
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);
|
|
@@ -59,7 +60,7 @@ function workspaceErrorStatus(err) {
|
|
|
59
60
|
/** Hono sub-router for workspace CRUD, tasks, agents, Git and PR creation. */
|
|
60
61
|
const app = new Hono();
|
|
61
62
|
// 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']) {
|
|
63
|
+
for (const route of ['/:id/archive', '/:id/run-setup-script', '/:id/cancel-source-change', '/:id/rollback-file']) {
|
|
63
64
|
app.use(route, async (c, next) => {
|
|
64
65
|
try {
|
|
65
66
|
await withWorkspaceLifecycleGuard(c.req.param('id'), next);
|
|
@@ -184,6 +185,7 @@ async function rollbackFailedCreation(workspace, opts) {
|
|
|
184
185
|
try {
|
|
185
186
|
return await deleteWorkspaceWithSideEffects(workspace, {
|
|
186
187
|
deleteLocalBranch: opts.deleteLocalBranch,
|
|
188
|
+
removeWorktree: opts.removeWorktree,
|
|
187
189
|
// Nothing was ever pushed at this point in the creation flow.
|
|
188
190
|
deleteRemoteBranch: false,
|
|
189
191
|
});
|
|
@@ -368,6 +370,7 @@ app.post('/', migrationGuard, async (c) => {
|
|
|
368
370
|
// Set only once WE created the worktree, which is also what created the
|
|
369
371
|
// branch. A failure before that point must not delete a branch we never made.
|
|
370
372
|
let createdBranch = false;
|
|
373
|
+
let createdWorktree = false;
|
|
371
374
|
// Hoisted out of the `build-prompt` block (declared with `const` there) so
|
|
372
375
|
// both rollback paths — the `start-agent` branch and the outermost catch —
|
|
373
376
|
// can read it to decide whether the setup-script caveat applies.
|
|
@@ -671,190 +674,206 @@ app.post('/', migrationGuard, async (c) => {
|
|
|
671
674
|
// worktree path so they stay aligned.
|
|
672
675
|
currentStep = 'create-record';
|
|
673
676
|
emitCreateProgress(creationId, currentStep);
|
|
674
|
-
let prospectiveWorktreePath;
|
|
675
677
|
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);
|
|
678
|
+
// Allocate the unique branch/path and create its checkout under one lock.
|
|
679
|
+
// Cleanup is deliberately outside: it acquires the same repository lock.
|
|
680
|
+
const creation = await withGitRepoLock(body.projectPath, async () => {
|
|
681
|
+
let prospectiveWorktreePath;
|
|
682
|
+
if (useReusedWorktree) {
|
|
683
|
+
// Another attachment may have completed while this request waited for the lock.
|
|
684
|
+
const existing = getDb().prepare('SELECT id FROM workspaces WHERE worktree_path = ?').get(body.worktreePath);
|
|
685
|
+
if (existing) {
|
|
686
|
+
const failure = 'This worktree is already attached to another Kōbō workspace';
|
|
687
|
+
emitCreateFailed(creationId, 'inspect-worktree', failure);
|
|
688
|
+
return c.json({ error: failure, step: 'inspect-worktree' }, 422);
|
|
689
|
+
}
|
|
690
|
+
prospectiveWorktreePath = body.worktreePath;
|
|
695
691
|
}
|
|
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', {});
|
|
692
|
+
else {
|
|
693
|
+
try {
|
|
694
|
+
const resolved = resolveUniqueBranchAndPath({
|
|
695
|
+
projectPath: body.projectPath,
|
|
696
|
+
baseBranch: workingBranch,
|
|
697
|
+
worktreesPath: globalSettings.worktreesPath,
|
|
698
|
+
projectSlug,
|
|
699
|
+
});
|
|
700
|
+
workingBranch = resolved.workingBranch;
|
|
701
|
+
prospectiveWorktreePath = resolved.worktreePath;
|
|
702
|
+
workingBranchAdjusted = resolved.adjusted;
|
|
703
|
+
}
|
|
704
|
+
catch (err) {
|
|
705
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
706
|
+
emitCreateFailed(creationId, 'create-record', message);
|
|
707
|
+
return c.json({ error: message, step: 'create-record' }, 409);
|
|
708
|
+
}
|
|
734
709
|
}
|
|
735
|
-
|
|
736
|
-
|
|
737
|
-
|
|
738
|
-
|
|
739
|
-
|
|
740
|
-
|
|
741
|
-
|
|
742
|
-
|
|
743
|
-
|
|
744
|
-
|
|
745
|
-
|
|
746
|
-
|
|
747
|
-
|
|
748
|
-
|
|
749
|
-
|
|
750
|
-
|
|
751
|
-
|
|
710
|
+
let workspace = workspaceService.createWorkspace({
|
|
711
|
+
name: body.name,
|
|
712
|
+
projectPath: body.projectPath,
|
|
713
|
+
sourceBranch: body.sourceBranch,
|
|
714
|
+
workingBranch,
|
|
715
|
+
notionUrl: body.notionUrl,
|
|
716
|
+
notionPageId: body.notionPageId,
|
|
717
|
+
sentryUrl: body.sentryUrl,
|
|
718
|
+
prUrl: body.prUrl,
|
|
719
|
+
worktreePath: prospectiveWorktreePath,
|
|
720
|
+
worktreeOwned: !useReusedWorktree,
|
|
721
|
+
model: body.model,
|
|
722
|
+
brainstormModel: body.brainstormModel,
|
|
723
|
+
reasoningEffort: body.reasoningEffort,
|
|
724
|
+
agentPermissionMode: resolveCreateAgentPermissionMode(body.agentPermissionMode, body.projectPath, globalSettings, body.engine ?? 'claude-code'),
|
|
725
|
+
engine: body.engine,
|
|
726
|
+
// Set by the engine-comparison flow, which posts here once per engine
|
|
727
|
+
// with the same id so the resulting workspaces can find each other.
|
|
728
|
+
comparisonId: body.comparisonId,
|
|
729
|
+
...(useReusedWorktree ? {} : { worktreesPath: globalSettings.worktreesPath }),
|
|
730
|
+
});
|
|
731
|
+
createdWorkspace = workspace;
|
|
732
|
+
// Enable auto-loop before starting the initial brainstorming session so
|
|
733
|
+
// its model override is available when selecting that session's model.
|
|
734
|
+
if (body.autoLoop === true) {
|
|
735
|
+
const notionProducedTasks = body.notionUrl !== undefined &&
|
|
736
|
+
notionContent != null &&
|
|
737
|
+
notionContent.todos.length > 0 &&
|
|
738
|
+
notionContent.gherkinFeatures.length > 0;
|
|
739
|
+
const sessionMode = body.autoLoopSessionMode === 'continuous' ? 'continuous' : 'per_task';
|
|
740
|
+
const db = getDb();
|
|
741
|
+
db.prepare('UPDATE workspaces SET auto_loop = 1, auto_loop_ready = ?, auto_loop_session_mode = ? WHERE id = ?').run(notionProducedTasks ? 1 : 0, sessionMode, workspace.id);
|
|
742
|
+
workspace = workspaceService.getWorkspace(workspace.id) ?? workspace;
|
|
743
|
+
// Emit events so the frontend refreshes autoLoopStates without F5.
|
|
744
|
+
wsService.emitEphemeral(workspace.id, 'autoloop:enabled', {});
|
|
745
|
+
if (notionProducedTasks) {
|
|
746
|
+
wsService.emitEphemeral(workspace.id, 'autoloop:ready-flipped', {});
|
|
747
|
+
}
|
|
752
748
|
}
|
|
753
|
-
|
|
754
|
-
|
|
749
|
+
// Auto-tag the workspace based on its creation source — `notion` when
|
|
750
|
+
// imported from a Notion page, `sentry` when bootstrapped from a Sentry
|
|
751
|
+
// issue URL. Pre-seeded in the global tag catalogue via migration v9.
|
|
752
|
+
// Skip any tag the user has removed from the catalogue so we respect
|
|
753
|
+
// their choice (they may have pruned "notion"/"sentry" on purpose).
|
|
754
|
+
const catalogTags = new Set(globalSettings.tags ?? []);
|
|
755
|
+
const autoTags = [];
|
|
756
|
+
if (body.notionUrl && catalogTags.has('notion'))
|
|
757
|
+
autoTags.push('notion');
|
|
758
|
+
if (body.sentryUrl && catalogTags.has('sentry'))
|
|
759
|
+
autoTags.push('sentry');
|
|
760
|
+
if (autoTags.length > 0) {
|
|
761
|
+
try {
|
|
762
|
+
const tagged = workspaceService.setWorkspaceTags(workspace.id, autoTags);
|
|
763
|
+
if (tagged)
|
|
764
|
+
workspace = tagged;
|
|
765
|
+
}
|
|
766
|
+
catch (err) {
|
|
767
|
+
console.error('[workspaces] Failed to apply auto tags:', err);
|
|
768
|
+
}
|
|
755
769
|
}
|
|
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
|
-
});
|
|
770
|
+
// Update workspace name with Sentry issue title if the user did not provide
|
|
771
|
+
// a custom name and Notion hasn't already filled it. Prefix with the Sentry
|
|
772
|
+
// short-id (e.g. "SEKUR-IOS-9 | TypeError: …") so the workspace stays
|
|
773
|
+
// identifiable in the sidebar without opening the panel.
|
|
774
|
+
if (sentryContent?.title && !notionContent?.title) {
|
|
775
|
+
const prefix = sentryContent.issueId ? `${sentryContent.issueId} | ` : '';
|
|
776
|
+
const renamed = resolveExtractedName(workspace.name, `${prefix}${sentryContent.title}`);
|
|
777
|
+
if (renamed)
|
|
778
|
+
workspace = workspaceService.updateWorkspaceName(workspace.id, renamed);
|
|
778
779
|
}
|
|
779
|
-
|
|
780
|
-
|
|
781
|
-
|
|
782
|
-
|
|
783
|
-
|
|
784
|
-
|
|
780
|
+
currentStep = 'create-tasks';
|
|
781
|
+
emitCreateProgress(creationId, currentStep);
|
|
782
|
+
// Create tasks from extracted Notion data
|
|
783
|
+
if (notionContent) {
|
|
784
|
+
let sortOrder = 0;
|
|
785
|
+
for (const todo of notionContent.todos) {
|
|
786
|
+
workspaceService.createTask(workspace.id, {
|
|
787
|
+
title: todo.title,
|
|
788
|
+
isAcceptanceCriterion: false,
|
|
789
|
+
sortOrder: sortOrder++,
|
|
790
|
+
});
|
|
791
|
+
}
|
|
792
|
+
for (const feature of notionContent.gherkinFeatures) {
|
|
793
|
+
workspaceService.createTask(workspace.id, {
|
|
794
|
+
title: feature,
|
|
795
|
+
isAcceptanceCriterion: true,
|
|
796
|
+
sortOrder: sortOrder++,
|
|
797
|
+
});
|
|
798
|
+
}
|
|
799
|
+
// Update workspace name with Notion page title only if user didn't
|
|
800
|
+
// provide a custom name. Prefix with the Notion unique-id (e.g.
|
|
801
|
+
// Use the Notion page title when the user left the generic placeholder.
|
|
802
|
+
if (notionContent.title) {
|
|
803
|
+
const renamed = resolveExtractedName(workspace.name, notionContent.title);
|
|
804
|
+
if (renamed)
|
|
805
|
+
workspace = workspaceService.updateWorkspaceName(workspace.id, renamed);
|
|
806
|
+
}
|
|
785
807
|
}
|
|
786
|
-
// Update workspace name with
|
|
787
|
-
//
|
|
788
|
-
//
|
|
789
|
-
|
|
790
|
-
|
|
808
|
+
// Update workspace name with the PR/MR title if the user did not provide
|
|
809
|
+
// a custom name and neither Notion nor Sentry already claimed it. This
|
|
810
|
+
// makes the PR a THIRD context source, lowest priority of the three
|
|
811
|
+
// (Notion > Sentry > PR), consistent with the checks above.
|
|
812
|
+
if (prContent && !notionContent?.title && !sentryContent?.title) {
|
|
813
|
+
const renamed = resolveExtractedName(workspace.name, prContent.title);
|
|
791
814
|
if (renamed)
|
|
792
815
|
workspace = workspaceService.updateWorkspaceName(workspace.id, renamed);
|
|
793
816
|
}
|
|
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
|
-
});
|
|
817
|
+
// Create manual tasks/criteria if no Notion content was extracted
|
|
818
|
+
if (!notionContent && (Array.isArray(body.tasks) || Array.isArray(body.acceptanceCriteria))) {
|
|
819
|
+
let sortOrder = 0;
|
|
820
|
+
if (Array.isArray(body.tasks)) {
|
|
821
|
+
for (const title of body.tasks) {
|
|
822
|
+
if (typeof title === 'string' && title.trim()) {
|
|
823
|
+
workspaceService.createTask(workspace.id, {
|
|
824
|
+
title: title.trim(),
|
|
825
|
+
isAcceptanceCriterion: false,
|
|
826
|
+
sortOrder: sortOrder++,
|
|
827
|
+
});
|
|
828
|
+
}
|
|
815
829
|
}
|
|
816
830
|
}
|
|
817
|
-
|
|
818
|
-
|
|
819
|
-
|
|
820
|
-
|
|
821
|
-
|
|
822
|
-
|
|
823
|
-
|
|
824
|
-
|
|
825
|
-
}
|
|
831
|
+
if (Array.isArray(body.acceptanceCriteria)) {
|
|
832
|
+
for (const title of body.acceptanceCriteria) {
|
|
833
|
+
if (typeof title === 'string' && title.trim()) {
|
|
834
|
+
workspaceService.createTask(workspace.id, {
|
|
835
|
+
title: title.trim(),
|
|
836
|
+
isAcceptanceCriterion: true,
|
|
837
|
+
sortOrder: sortOrder++,
|
|
838
|
+
});
|
|
839
|
+
}
|
|
826
840
|
}
|
|
827
841
|
}
|
|
828
842
|
}
|
|
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;
|
|
843
|
+
currentStep = 'create-worktree';
|
|
844
|
+
emitCreateProgress(creationId, currentStep);
|
|
845
|
+
// Create git worktree for the working branch — unless we're reusing an
|
|
846
|
+
// existing one, in which case the path is taken straight from the body.
|
|
847
|
+
let worktreePath;
|
|
848
|
+
if (useReusedWorktree) {
|
|
849
|
+
worktreePath = body.worktreePath;
|
|
846
850
|
}
|
|
847
|
-
|
|
848
|
-
|
|
849
|
-
|
|
850
|
-
|
|
851
|
-
|
|
852
|
-
|
|
853
|
-
|
|
854
|
-
|
|
855
|
-
|
|
851
|
+
else {
|
|
852
|
+
try {
|
|
853
|
+
const created = await worktreeService.createWorktreeUnlocked(body.projectPath, workingBranch, baseRef, globalSettings.worktreesPath, projectSlug);
|
|
854
|
+
worktreePath = created.worktreePath;
|
|
855
|
+
// Not an assumption: `createWorktree` reports which git command it ran.
|
|
856
|
+
// It falls back to attaching an existing branch, and deleting that one
|
|
857
|
+
// on rollback would destroy commits the user made before Kobo existed.
|
|
858
|
+
createdBranch = created.branchCreated;
|
|
859
|
+
createdWorktree = true;
|
|
860
|
+
}
|
|
861
|
+
catch (err) {
|
|
862
|
+
if (err instanceof worktreeService.WorktreeCreationError) {
|
|
863
|
+
createdWorktree = true;
|
|
864
|
+
createdBranch = err.branchCreated;
|
|
865
|
+
}
|
|
866
|
+
throw new Error(`Failed to create worktree: ${err instanceof Error ? err.message : String(err)}`, {
|
|
867
|
+
cause: err,
|
|
868
|
+
});
|
|
869
|
+
}
|
|
856
870
|
}
|
|
857
|
-
|
|
871
|
+
return { workspace, worktreePath };
|
|
872
|
+
});
|
|
873
|
+
if (creation instanceof Response)
|
|
874
|
+
return creation;
|
|
875
|
+
const { workspace } = creation;
|
|
876
|
+
const { worktreePath } = creation;
|
|
858
877
|
currentStep = 'write-conventions';
|
|
859
878
|
emitCreateProgress(creationId, currentStep);
|
|
860
879
|
// Ensure Kobo-generated files are gitignored. Check both the root
|
|
@@ -1322,12 +1341,18 @@ Once the brainstorming + planning steps above are complete and you have a saved
|
|
|
1322
1341
|
// already guards the worktree via `worktreeOwned`; the branch flag is
|
|
1323
1342
|
// ours to set.
|
|
1324
1343
|
const rollbackWarnings = await rollbackFailedCreation(workspace, {
|
|
1325
|
-
deleteLocalBranch:
|
|
1344
|
+
deleteLocalBranch: createdBranch,
|
|
1345
|
+
removeWorktree: createdWorktree,
|
|
1326
1346
|
});
|
|
1327
1347
|
// The setup script has already run by this point — see the caveat's own
|
|
1328
1348
|
// comment for what that rollback cannot reach.
|
|
1329
1349
|
const setupCaveat = setupScriptConfigured ? SETUP_SCRIPT_ROLLBACK_CAVEAT : '';
|
|
1330
|
-
const
|
|
1350
|
+
const removed = createdBranch
|
|
1351
|
+
? 'The workspace, its worktree and its branch were removed.'
|
|
1352
|
+
: createdWorktree
|
|
1353
|
+
? 'The workspace and its worktree were removed.'
|
|
1354
|
+
: 'The half-created workspace was removed.';
|
|
1355
|
+
const failure = `Failed to start the agent: ${agentStartError}. ${removed}${setupCaveat}`;
|
|
1331
1356
|
emitCreateFailed(creationId, 'start-agent', failure);
|
|
1332
1357
|
return c.json({ error: failure, step: 'start-agent', rollback: { done: true, warnings: rollbackWarnings } }, 500);
|
|
1333
1358
|
}
|
|
@@ -1372,7 +1397,10 @@ Once the brainstorming + planning steps above are complete and you have a saved
|
|
|
1372
1397
|
// reported error.
|
|
1373
1398
|
if (createdWorkspace) {
|
|
1374
1399
|
emitCreateProgress(creationId, 'rollback');
|
|
1375
|
-
const rollbackWarnings = await rollbackFailedCreation(createdWorkspace, {
|
|
1400
|
+
const rollbackWarnings = await rollbackFailedCreation(createdWorkspace, {
|
|
1401
|
+
deleteLocalBranch: createdBranch,
|
|
1402
|
+
removeWorktree: createdWorktree,
|
|
1403
|
+
});
|
|
1376
1404
|
logError('workspaces', 'Rolled back a creation that failed outside any per-step handler', {
|
|
1377
1405
|
workspaceId: createdWorkspace.id,
|
|
1378
1406
|
step: currentStep,
|
|
@@ -1450,8 +1478,10 @@ app.get('/pr-states', (c) => {
|
|
|
1450
1478
|
// stay before GET /:id.
|
|
1451
1479
|
app.get('/info', (c) => {
|
|
1452
1480
|
try {
|
|
1481
|
+
const workspaces = workspaceService.listWorkspaces(false);
|
|
1453
1482
|
return c.json({
|
|
1454
|
-
workspaces
|
|
1483
|
+
workspaces,
|
|
1484
|
+
pendingInputs: Object.fromEntries(workspaces.map((workspace) => [workspace.id, agentManager.getPendingInputs(workspace.id)])),
|
|
1455
1485
|
prSnapshots: getAllPrSnapshots(),
|
|
1456
1486
|
gitStats: getAllGitStats(),
|
|
1457
1487
|
// In-memory truth, not the `status` column: a workspace missing from
|
|
@@ -2119,12 +2149,8 @@ app.post('/:id/tasks/:taskId/notify-done', (c) => {
|
|
|
2119
2149
|
return c.json({ error: message }, workspaceErrorStatus(err));
|
|
2120
2150
|
}
|
|
2121
2151
|
});
|
|
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) => {
|
|
2152
|
+
// GET /api/workspaces/:id/history-search — the same logical-message index as global search.
|
|
2153
|
+
app.get('/:id/history-search', async (c) => {
|
|
2128
2154
|
try {
|
|
2129
2155
|
const id = c.req.param('id');
|
|
2130
2156
|
if (!workspaceService.getWorkspace(id))
|
|
@@ -2132,71 +2158,23 @@ app.get('/:id/history-search', (c) => {
|
|
|
2132
2158
|
const query = (c.req.query('q') ?? '').trim();
|
|
2133
2159
|
if (query.length < 2)
|
|
2134
2160
|
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 }];
|
|
2161
|
+
const limit = Math.max(1, Math.min(parseInt(c.req.query('limit') ?? '30', 10) || 30, 100));
|
|
2162
|
+
const results = await searchEvents(query, { workspaceId: id, includeArchived: true, limit }, c.req.raw.signal);
|
|
2163
|
+
return c.json({
|
|
2164
|
+
results: results.map((result) => ({
|
|
2165
|
+
eventId: result.eventId,
|
|
2166
|
+
sessionId: result.sessionId,
|
|
2167
|
+
createdAt: result.timestamp,
|
|
2168
|
+
kind: result.type === 'user:message' ? 'user' : 'agent',
|
|
2169
|
+
snippet: result.snippet,
|
|
2170
|
+
})),
|
|
2171
|
+
partial: getSearchIndexStatus().state !== 'ready',
|
|
2150
2172
|
});
|
|
2151
|
-
return c.json({ results: normalized.slice(0, limit) });
|
|
2152
2173
|
}
|
|
2153
2174
|
catch (err) {
|
|
2154
|
-
return c.json({ error: err instanceof Error ? err.message : String(err) },
|
|
2175
|
+
return c.json({ error: err instanceof Error ? err.message : String(err) }, 503);
|
|
2155
2176
|
}
|
|
2156
2177
|
});
|
|
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
2178
|
/**
|
|
2201
2179
|
* GET /api/workspaces/:id/sessions/:sessionId/summary — why a session ended.
|
|
2202
2180
|
*
|
|
@@ -3061,7 +3039,7 @@ async function deleteWorkspaceWithSideEffectsUnlocked(workspace, opts) {
|
|
|
3061
3039
|
if (workspace.worktreePurgedAt) {
|
|
3062
3040
|
console.log(`[workspaces] skipping worktree removal on delete (already purged): ${worktreePath}`);
|
|
3063
3041
|
}
|
|
3064
|
-
else if (workspace.worktreeOwned) {
|
|
3042
|
+
else if (workspace.worktreeOwned && opts.removeWorktree !== false) {
|
|
3065
3043
|
try {
|
|
3066
3044
|
await worktreeService.removeWorktree(workspace.projectPath, worktreePath);
|
|
3067
3045
|
}
|
|
@@ -3423,7 +3401,7 @@ app.post('/:id/rollback-file', async (c) => {
|
|
|
3423
3401
|
return c.json({ error: `Workspace '${id}' not found` }, 404);
|
|
3424
3402
|
}
|
|
3425
3403
|
const body = await c.req.json().catch(() => ({}));
|
|
3426
|
-
const filePath = typeof body?.path === 'string' ? body.path
|
|
3404
|
+
const filePath = typeof body?.path === 'string' ? body.path : '';
|
|
3427
3405
|
if (!filePath) {
|
|
3428
3406
|
return c.json({ error: 'Missing or invalid `path` field' }, 400);
|
|
3429
3407
|
}
|
|
@@ -3436,7 +3414,13 @@ app.post('/:id/rollback-file', async (c) => {
|
|
|
3436
3414
|
}
|
|
3437
3415
|
let target;
|
|
3438
3416
|
try {
|
|
3439
|
-
target =
|
|
3417
|
+
target = await withGitRepoLock(workspace.worktreePath, async () => {
|
|
3418
|
+
if (agentManager.getAgentStatus(id) !== null)
|
|
3419
|
+
throw new Error('Stop the agent before rolling back a file');
|
|
3420
|
+
gitOps.assertCurrentBranch(workspace.worktreePath, workspace.workingBranch);
|
|
3421
|
+
gitOps.assertNoIndexLock(workspace.worktreePath);
|
|
3422
|
+
return gitOps.rollbackFile(workspace.worktreePath, workspace.workingBranch, filePath);
|
|
3423
|
+
});
|
|
3440
3424
|
}
|
|
3441
3425
|
catch (err) {
|
|
3442
3426
|
const message = err instanceof Error ? err.message : String(err);
|