@loicngr/kobo 2.1.28 → 2.1.30
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 +60 -11
- package/CHANGELOG.md +20 -1
- package/CONFIGURATION.md +140 -35
- package/CONTRIBUTING.md +2 -2
- package/DESIGN.md +1 -1
- package/README.md +12 -9
- package/SECURITY.md +7 -2
- package/dist/server/db/memory-schema.js +5 -5
- package/dist/server/db/migrations.js +2 -0
- package/dist/server/db/mistral-memory-migration.js +40 -0
- package/dist/server/routes/engines.js +14 -0
- package/dist/server/routes/environment.js +1 -1
- package/dist/server/routes/health.js +14 -3
- package/dist/server/routes/settings.js +7 -1
- package/dist/server/routes/workspaces.js +116 -9
- package/dist/server/services/agent/engines/claude-code/capabilities.js +2 -0
- package/dist/server/services/agent/engines/claude-code/engine.js +3 -0
- package/dist/server/services/agent/engines/claude-code/resolve-binary.js +1 -1
- package/dist/server/services/agent/engines/codex/capabilities.js +2 -0
- package/dist/server/services/agent/engines/codex/engine.js +3 -0
- package/dist/server/services/agent/engines/configuration.js +14 -0
- package/dist/server/services/agent/engines/mistral-vibe/capabilities.js +13 -0
- package/dist/server/services/agent/engines/mistral-vibe/client.js +64 -0
- package/dist/server/services/agent/engines/mistral-vibe/configuration.js +76 -0
- package/dist/server/services/agent/engines/mistral-vibe/engine.js +361 -0
- package/dist/server/services/agent/engines/mistral-vibe/event-mapper.js +297 -0
- package/dist/server/services/agent/engines/mistral-vibe/options-builder.js +35 -0
- package/dist/server/services/agent/engines/mistral-vibe/server-requests.js +278 -0
- package/dist/server/services/agent/engines/mistral-vibe/spawn.js +176 -0
- package/dist/server/services/agent/engines/registry.js +2 -0
- package/dist/server/services/agent/engines/types.js +6 -0
- package/dist/server/services/agent/orchestrator.js +88 -25
- package/dist/server/services/agent/session-controller.js +5 -0
- package/dist/server/services/auto-loop-final-review-service.js +10 -3
- package/dist/server/services/dev-server-service.js +68 -4
- package/dist/server/services/environment-check-service.js +9 -3
- package/dist/server/services/memory-service.js +1 -1
- package/dist/server/services/mistral-options-service.js +145 -0
- package/dist/server/services/pr-watcher-service.js +40 -11
- package/dist/server/services/review-service.js +33 -9
- package/dist/server/services/session-handoff-service.js +28 -2
- package/dist/server/services/settings-defaults.js +1 -0
- package/dist/server/services/settings-service.js +19 -1
- package/dist/server/services/workspace-creation-mcp-service.js +50 -8
- package/dist/server/services/workspace-dialogue-service.js +2 -0
- package/dist/server/services/workspace-message-service.js +19 -1
- package/dist/server/services/workspace-permission-policy-service.js +24 -0
- package/dist/server/utils/bounded-process.js +1 -1
- package/dist/shared/launch-instructions.js +18 -7
- package/dist/shared/memory.js +1 -1
- package/dist/shared/mistral-models.js +9 -0
- package/dist/shared/mistral-options.js +1 -0
- package/dist/shared/question-selections.js +16 -0
- package/dist/shared/workspace-creation-tools.js +8 -2
- package/dist/shared/workspace-dialogue-tools.js +15 -4
- package/docs/deployment-vps.md +10 -3
- package/docs/getting-started.md +4 -3
- package/docs/troubleshooting.md +2 -2
- package/package.json +20 -14
- package/src/client/dist/pwa/assets/ActivityFeed-C5stmHXP.js +2 -0
- package/src/client/dist/pwa/assets/AutoLoopFinalReviewSettings-CIDSX32d.css +1 -0
- package/src/client/dist/pwa/assets/AutoLoopFinalReviewSettings-DG32W_0G.js +1 -0
- package/src/client/dist/pwa/assets/{ChangelogPage-CAAUEn5X.js → ChangelogPage-_vItY8vA.js} +1 -1
- package/src/client/dist/pwa/assets/{ClosePopup-CRV2hxHS.js → ClosePopup-rU8Bm-mC.js} +1 -1
- package/src/client/dist/pwa/assets/{ComparisonPanel-BCb7vB5E.js → ComparisonPanel-B3BMzQH0.js} +2 -2
- package/src/client/dist/pwa/assets/CreatePage-FlGrfmjX.js +2 -0
- package/src/client/dist/pwa/assets/CreatePage-oh8ihVC4.css +1 -0
- package/src/client/dist/pwa/assets/{DashboardPage-XE52EABP.js → DashboardPage-lJGwWQYQ.js} +1 -1
- package/src/client/dist/pwa/assets/DiffViewer-DIaQH3lA.js +8 -0
- package/src/client/dist/pwa/assets/{DocumentsPanel-L4pbQfz1.js → DocumentsPanel-Ct-nm4fX.js} +1 -1
- package/src/client/dist/pwa/assets/{DrawerToggleButton-DiOJm5he.js → DrawerToggleButton-M5qv4Io-.js} +1 -1
- package/src/client/dist/pwa/assets/GitPanel-BaHCgb3p.js +3 -0
- package/src/client/dist/pwa/assets/GitPanel-C8vw2qvW.css +1 -0
- package/src/client/dist/pwa/assets/HealthPage-DR3wa6iD.js +1 -0
- package/src/client/dist/pwa/assets/HealthPage-Dbn4J_DT.css +1 -0
- package/src/client/dist/pwa/assets/{MainLayout-DoAEgANU.css → MainLayout-CHi8qP1D.css} +1 -1
- package/src/client/dist/pwa/assets/MainLayout-WJabICsA.js +2 -0
- package/src/client/dist/pwa/assets/{MemoryPanel-DtEvh_b7.js → MemoryPanel-CRpWdfc0.js} +1 -1
- package/src/client/dist/pwa/assets/MemoryProposalList-DGz3lDbL.js +1 -0
- package/src/client/dist/pwa/assets/{QBadge-CW3Lw-kL.js → QBadge-DiwbRpte.js} +1 -1
- package/src/client/dist/pwa/assets/{QBanner-BT2ZemYu.js → QBanner-Cw89HSjk.js} +1 -1
- package/src/client/dist/pwa/assets/QBtn-kfhHFy7Q.js +1 -0
- package/src/client/dist/pwa/assets/{QBtnDropdown-CWPIugs8.js → QBtnDropdown-tt8rUNNR.js} +1 -1
- package/src/client/dist/pwa/assets/{QBtnGroup-CnM4vSJA.js → QBtnGroup-Bioxjp9q.js} +1 -1
- package/src/client/dist/pwa/assets/{QBtnToggle-DccOOtbh.js → QBtnToggle-Bod-x9im.js} +1 -1
- package/src/client/dist/pwa/assets/{QChip-Be16M4Ig.js → QChip-DvhtPv_L.js} +1 -1
- package/src/client/dist/pwa/assets/{QExpansionItem-D-EGL2b3.js → QExpansionItem-BkrI3UPL.js} +1 -1
- package/src/client/dist/pwa/assets/{QItemSection-MdJ1_ZK7.js → QItemSection-ahqRK4JF.js} +1 -1
- package/src/client/dist/pwa/assets/{QList-Cx1UfaUm.js → QList-CAUl0v66.js} +1 -1
- package/src/client/dist/pwa/assets/{QMarkupTable-UhP7fVUp.js → QMarkupTable-B4LLAly-.js} +1 -1
- package/src/client/dist/pwa/assets/{QPage-CqkGHokx.js → QPage-DIy_mRoT.js} +1 -1
- package/src/client/dist/pwa/assets/{QScrollArea-BMrqK-xd.js → QScrollArea-BvgnY-oA.js} +1 -1
- package/src/client/dist/pwa/assets/QSelect-BtZB1wrB.js +1 -0
- package/src/client/dist/pwa/assets/{QSpinnerDots-xdWyaE1i.js → QSpinnerDots-Bea6hAuJ.js} +1 -1
- package/src/client/dist/pwa/assets/{QTooltip-Bk8dLUtU.js → QTooltip-CDH6p745.js} +1 -1
- package/src/client/dist/pwa/assets/{SchedulePanel-CuxmxCKi.js → SchedulePanel-C2rWlh7l.js} +1 -1
- package/src/client/dist/pwa/assets/{SearchPage-DKnQPUqY.js → SearchPage-BFd4NQwJ.js} +1 -1
- package/src/client/dist/pwa/assets/{SettingsPage-B4rmd2WX.css → SettingsPage-Bm0TcarK.css} +1 -1
- package/src/client/dist/pwa/assets/SettingsPage-DDQCgnjL.js +9 -0
- package/src/client/dist/pwa/assets/{SplitWorkspacePage-WhQyX8fn.js → SplitWorkspacePage-DKCtOZO4.js} +1 -1
- package/src/client/dist/pwa/assets/TerminalPanel-BE_MSZF0.js +16 -0
- package/src/client/dist/pwa/assets/{ToolCallItem-DYubi-J0.js → ToolCallItem-BEYaOtgh.js} +4 -4
- package/src/client/dist/pwa/assets/{TouchPan-DDotyYLN.js → TouchPan-DeYoOjX5.js} +1 -1
- package/src/client/dist/pwa/assets/{TourReplayButton-kAnEMcy2.js → TourReplayButton-D5lMwIKt.js} +1 -1
- package/src/client/dist/pwa/assets/WorkspaceAttentionLabels-BHEAsIoo.js +1 -0
- package/src/client/dist/pwa/assets/WorkspaceAttentionLabels-v7ein__H.css +1 -0
- package/src/client/dist/pwa/assets/WorkspacePage-5_wsJPaJ.js +4 -0
- package/src/client/dist/pwa/assets/WorkspacePage-CtUXz0Cq.css +1 -0
- package/src/client/dist/pwa/assets/{browser-DVYLH8vl.js → browser-ogy_9LVv.js} +2 -2
- package/src/client/dist/pwa/assets/build-path-tree-CRXxaYkA.js +1 -0
- package/src/client/dist/pwa/assets/custom-sounds-iLTkCd_i.js +1 -0
- package/src/client/dist/pwa/assets/de-BqDU7iZH.js +17 -0
- package/src/client/dist/pwa/assets/detached-fullscreen-CHqMxB-8.js +1 -0
- package/src/client/dist/pwa/assets/dom-CRPVbmiy.js +2 -0
- package/src/client/dist/pwa/assets/ecl-94qehhXe.js +1 -0
- package/src/client/dist/pwa/assets/{editor.api-BC38eXAS.js → editor.api-Dx6TbfZG.js} +9 -9
- package/src/client/dist/pwa/assets/editor.worker-xrHNhPSq.js +33 -0
- package/src/client/dist/pwa/assets/efforts-DnJh2fW_.js +9 -0
- package/src/client/dist/pwa/assets/es-D4VMXQRk.js +17 -0
- package/src/client/dist/pwa/assets/expand-template-PUEVWny3.js +1 -0
- package/src/client/dist/pwa/assets/{format-OwxXBnDJ.js → format-yf9XM3dV.js} +48 -48
- package/src/client/dist/pwa/assets/{formatters-YgaCZcwJ.js → formatters-D0sXu9Oe.js} +1 -1
- package/src/client/dist/pwa/assets/fr-DIyaCFB3.js +17 -0
- package/src/client/dist/pwa/assets/{freemarker2-l7R42fI2.js → freemarker2-l_xIHxRf.js} +1 -1
- package/src/client/dist/pwa/assets/{handlebars-DFmPrgjH.js → handlebars-CYgq99yQ.js} +1 -1
- package/src/client/dist/pwa/assets/{html-CERIAlPM.js → html-C28GatN_.js} +1 -1
- package/src/client/dist/pwa/assets/i18n-2AmHIUy0.js +1 -0
- package/src/client/dist/pwa/assets/index-C0MApoXN.css +1 -0
- package/src/client/dist/pwa/assets/index-MMYMztJ_.js +20 -0
- package/src/client/dist/pwa/assets/it-CF6_4OS0.js +17 -0
- package/src/client/dist/pwa/assets/{javascript-BBYncplF.js → javascript-YCSueyyk.js} +1 -1
- package/src/client/dist/pwa/assets/{liquid-C1HqYyYU.js → liquid-CUBn90Gs.js} +1 -1
- package/src/client/dist/pwa/assets/{mdx-E74dsP22.js → mdx-C5WOHShJ.js} +1 -1
- package/src/client/dist/pwa/assets/monaco-contributions-DYwqbh7m.js +53 -0
- package/src/client/dist/pwa/assets/{monaco.contribution-DYdn9CwR.js → monaco.contribution-CoFzrPQl.js} +2 -2
- package/src/client/dist/pwa/assets/network-auth-BpJ6Noax.js +1 -0
- package/src/client/dist/pwa/assets/notify-theme-NPq4-Tup.js +1 -0
- package/src/client/dist/pwa/assets/permissionModes-Bbl901Hx.js +36 -0
- package/src/client/dist/pwa/assets/{private.use-form-DY1BkePL.js → private.use-form-BcspuzM9.js} +1 -1
- package/src/client/dist/pwa/assets/{project-color-_snXWYUp.js → project-color-gNyhKJlV.js} +1 -1
- package/src/client/dist/pwa/assets/{python-DNAAGBcX.js → python-lhKUZdPj.js} +1 -1
- package/src/client/dist/pwa/assets/{razor-CB3g5p_T.js → razor-Chi44rEL.js} +1 -1
- package/src/client/dist/pwa/assets/reactivity.esm-bundler-Dplx1ILy.js +1 -0
- package/src/client/dist/pwa/assets/render-chat-markdown-C9ihAYBt.js +67 -0
- package/src/client/dist/pwa/assets/runtime-core.esm-bundler-BAUgNzMK.js +1 -0
- package/src/client/dist/pwa/assets/{typescript-0kpz8bqP.js → typescript-CU2TXVm6.js} +1 -1
- package/src/client/dist/pwa/assets/{use-checkbox-v4aAbE6K.js → use-checkbox-DUxgXR4e.js} +1 -1
- package/src/client/dist/pwa/assets/{use-id-BrdYXce0.js → use-id-B7ieAx2m.js} +1 -1
- package/src/client/dist/pwa/assets/use-is-mobile-CHmaewI1.js +1 -0
- package/src/client/dist/pwa/assets/{use-search-index-status-D38yFOD_.js → use-search-index-status-CQ9S05G7.js} +1 -1
- package/src/client/dist/pwa/assets/use-slide-transition-BJh9xGMa.js +1 -0
- package/src/client/dist/pwa/assets/use-tours-BmrZbHjz.js +2 -0
- package/src/client/dist/pwa/assets/use-tours-CU5oA7cz.css +1 -0
- package/src/client/dist/pwa/assets/use-worktree-restore-DcucRlLh.js +1 -0
- package/src/client/dist/pwa/assets/{workspace-templates-D-lPJQ4G.js → workspace-templates-DaG42EEt.js} +1 -1
- package/src/client/dist/pwa/assets/{xml-BbOuqdUe.js → xml-CPO5LI6F.js} +1 -1
- package/src/client/dist/pwa/assets/{yaml-gasFQtfk.js → yaml-CoYs82jI.js} +1 -1
- package/src/client/dist/pwa/index.html +10 -10
- package/src/client/dist/pwa/sw.js +1 -1
- package/src/mcp-server/README.md +18 -6
- package/src/client/dist/pwa/assets/ActivityFeed-D58VS9o2.js +0 -2
- package/src/client/dist/pwa/assets/AutoLoopFinalReviewSettings-CK3r_fVB.js +0 -1
- package/src/client/dist/pwa/assets/AutoLoopFinalReviewSettings-Q-OlSQ1c.css +0 -1
- package/src/client/dist/pwa/assets/CreatePage-DasH1Uk4.js +0 -2
- package/src/client/dist/pwa/assets/CreatePage-DxOes_cF.css +0 -1
- package/src/client/dist/pwa/assets/DiffViewer-CncKlXne.js +0 -8
- package/src/client/dist/pwa/assets/GitPanel-WJlzy2VJ.js +0 -3
- package/src/client/dist/pwa/assets/GitPanel-yHvU38dp.css +0 -1
- package/src/client/dist/pwa/assets/HealthPage-Cnlhrn--.css +0 -1
- package/src/client/dist/pwa/assets/HealthPage-hJY5MurK.js +0 -1
- package/src/client/dist/pwa/assets/MainLayout-DgWElK7w.js +0 -2
- package/src/client/dist/pwa/assets/MemoryProposalList-kgFCm9zz.js +0 -1
- package/src/client/dist/pwa/assets/QBtn-BUSdPaf4.js +0 -1
- package/src/client/dist/pwa/assets/QSelect-DFWyDWCE.js +0 -1
- package/src/client/dist/pwa/assets/SettingsPage-CO3vIpPd.js +0 -9
- package/src/client/dist/pwa/assets/TerminalPanel-BY0lPq6k.js +0 -16
- package/src/client/dist/pwa/assets/WorkspaceAttentionLabels-B9LE8HtU.css +0 -1
- package/src/client/dist/pwa/assets/WorkspaceAttentionLabels-DiYt3rP3.js +0 -1
- package/src/client/dist/pwa/assets/WorkspacePage-BEBwwr81.css +0 -1
- package/src/client/dist/pwa/assets/WorkspacePage-K-1ce50d.js +0 -4
- package/src/client/dist/pwa/assets/build-path-tree-DEuOdIIk.js +0 -1
- package/src/client/dist/pwa/assets/custom-sounds-Ggw6V9iF.js +0 -1
- package/src/client/dist/pwa/assets/de-Bk3zQrin.js +0 -17
- package/src/client/dist/pwa/assets/detached-fullscreen-Du3TcYdX.js +0 -1
- package/src/client/dist/pwa/assets/dom-BtYTvLeN.js +0 -2
- package/src/client/dist/pwa/assets/ecl-D05T4iGw.js +0 -1
- package/src/client/dist/pwa/assets/editor.worker-BLJF_oQx.js +0 -33
- package/src/client/dist/pwa/assets/efforts-TGacUPcJ.js +0 -9
- package/src/client/dist/pwa/assets/es-f3xuAWA9.js +0 -17
- package/src/client/dist/pwa/assets/expand-template-DM8gq3Mo.js +0 -1
- package/src/client/dist/pwa/assets/fr-B6c21SG3.js +0 -17
- package/src/client/dist/pwa/assets/i18n-WaE4cZK9.js +0 -1
- package/src/client/dist/pwa/assets/index-C8zAZ-k-.css +0 -1
- package/src/client/dist/pwa/assets/index-CpWBGKdM.js +0 -20
- package/src/client/dist/pwa/assets/it-DUstDpC2.js +0 -17
- package/src/client/dist/pwa/assets/monaco-contributions-DHvm-kp5.js +0 -53
- package/src/client/dist/pwa/assets/network-auth-DTZUKbny.js +0 -1
- package/src/client/dist/pwa/assets/notify-theme-FuTL7HuS.js +0 -1
- package/src/client/dist/pwa/assets/permissionModes-D-Nexnoo.js +0 -36
- package/src/client/dist/pwa/assets/reactivity.esm-bundler-DY1JkAEA.js +0 -1
- package/src/client/dist/pwa/assets/render-chat-markdown-CafzCcmu.js +0 -69
- package/src/client/dist/pwa/assets/runtime-core.esm-bundler-Bqg71Vll.js +0 -1
- package/src/client/dist/pwa/assets/use-is-mobile-CDB_rzM9.js +0 -1
- package/src/client/dist/pwa/assets/use-slide-transition-B_Nu6u-t.js +0 -1
- package/src/client/dist/pwa/assets/use-tours-BWkHp34X.js +0 -2
- package/src/client/dist/pwa/assets/use-tours-CTZuBZOi.css +0 -1
- package/src/client/dist/pwa/assets/use-worktree-restore-DUjZr44N.js +0 -1
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
import { execFile, spawn } from 'node:child_process';
|
|
2
2
|
import { existsSync } from 'node:fs';
|
|
3
|
+
import { StringDecoder } from 'node:string_decoder';
|
|
4
|
+
import { stripVTControlCharacters } from 'node:util';
|
|
3
5
|
import { resolveInstance, sanitizeBranchName } from '../utils/dev-server-instance.js';
|
|
4
6
|
import { assertWorkspaceLifecycleAvailable } from '../utils/workspace-lifecycle-guard.js';
|
|
5
7
|
import { getProjectSettings } from './settings-service.js';
|
|
@@ -25,6 +27,7 @@ function runCommand(command, args, options = {}) {
|
|
|
25
27
|
// ── State ──────────────────────────────────────────────────────────────────────
|
|
26
28
|
/** workspaceId -> spawned dev-server process */
|
|
27
29
|
const trackedProcesses = new Map();
|
|
30
|
+
const directServerStatuses = new Map();
|
|
28
31
|
const stoppingProcesses = new Map();
|
|
29
32
|
const generations = new Map();
|
|
30
33
|
let shuttingDown = false;
|
|
@@ -34,6 +37,7 @@ export function beginDevServerShutdown() {
|
|
|
34
37
|
/** Test-only: clear the tracked-processes map between tests. */
|
|
35
38
|
export function _resetTrackedProcessesForTests() {
|
|
36
39
|
trackedProcesses.clear();
|
|
40
|
+
directServerStatuses.clear();
|
|
37
41
|
stoppingProcesses.clear();
|
|
38
42
|
generations.clear();
|
|
39
43
|
shuttingDown = false;
|
|
@@ -74,14 +78,18 @@ export async function listRunningContainers(projectName) {
|
|
|
74
78
|
* still running (e.g. `docker compose up -d` is pulling/building images), the
|
|
75
79
|
* status is reported as `'starting'` even if no matching container is visible
|
|
76
80
|
* in `docker ps` yet. This prevents the UI from flashing to `'stopped'` during
|
|
77
|
-
* long build phases.
|
|
81
|
+
* long build phases. Direct processes report running and their URL after a
|
|
82
|
+
* recognized local readiness announcement on stdout or stderr.
|
|
78
83
|
*/
|
|
79
84
|
export async function getStatus(projectPath, workingBranch, workspaceId) {
|
|
80
85
|
const config = resolveInstance(projectPath, workingBranch);
|
|
86
|
+
const directStatus = workspaceId ? directServerStatuses.get(workspaceId) : undefined;
|
|
87
|
+
if (workspaceId && directStatus && trackedProcesses.has(workspaceId))
|
|
88
|
+
return { ...directStatus };
|
|
81
89
|
if (!config) {
|
|
82
90
|
return {
|
|
83
|
-
status: 'unknown',
|
|
84
|
-
instanceName: '',
|
|
91
|
+
status: workspaceId && trackedProcesses.has(workspaceId) ? 'starting' : 'unknown',
|
|
92
|
+
instanceName: workspaceId && trackedProcesses.has(workspaceId) ? sanitizeBranchName(workingBranch) : '',
|
|
85
93
|
projectName: '',
|
|
86
94
|
httpPort: '',
|
|
87
95
|
url: '',
|
|
@@ -163,12 +171,64 @@ export function startDevServer(workspaceId) {
|
|
|
163
171
|
const generation = (generations.get(workspaceId) ?? 0) + 1;
|
|
164
172
|
generations.set(workspaceId, generation);
|
|
165
173
|
trackedProcesses.set(workspaceId, proc);
|
|
166
|
-
//
|
|
174
|
+
// Parse complete lines separately per stream: CLI output and ANSI sequences
|
|
175
|
+
// can span arbitrary pipe chunks. Bound the unfinished line retained in memory.
|
|
176
|
+
const observeOutput = () => {
|
|
177
|
+
const decoder = new StringDecoder('utf8');
|
|
178
|
+
let pending = '';
|
|
179
|
+
return (data) => {
|
|
180
|
+
if (shuttingDown ||
|
|
181
|
+
trackedProcesses.get(workspaceId) !== proc ||
|
|
182
|
+
generations.get(workspaceId) !== generation ||
|
|
183
|
+
stoppingProcesses.has(workspaceId) ||
|
|
184
|
+
directServerStatuses.has(workspaceId))
|
|
185
|
+
return;
|
|
186
|
+
pending += decoder.write(data);
|
|
187
|
+
const lines = pending.split(/[\r\n]/);
|
|
188
|
+
pending = lines.pop().slice(-8192);
|
|
189
|
+
for (const line of lines) {
|
|
190
|
+
// Quasar's "App URL....." and Vite/Next's "Local:" are explicit
|
|
191
|
+
// readiness announcements; unrelated links in build output are not.
|
|
192
|
+
const match = stripVTControlCharacters(line).match(/^\s*(?:[»➜-]\s*)?(?:App URL[.\s]*|Local:\s*)(https?:\/\/\S+)\s*$/i);
|
|
193
|
+
if (!match)
|
|
194
|
+
continue;
|
|
195
|
+
let url;
|
|
196
|
+
try {
|
|
197
|
+
url = new URL(match[1]);
|
|
198
|
+
}
|
|
199
|
+
catch {
|
|
200
|
+
continue;
|
|
201
|
+
}
|
|
202
|
+
if (url.username ||
|
|
203
|
+
url.password ||
|
|
204
|
+
!['localhost', '127.0.0.1', '[::1]', '0.0.0.0', '[::]'].includes(url.hostname))
|
|
205
|
+
continue;
|
|
206
|
+
if (url.hostname === '0.0.0.0' || url.hostname === '[::]')
|
|
207
|
+
url.hostname = 'localhost';
|
|
208
|
+
const status = {
|
|
209
|
+
status: 'running',
|
|
210
|
+
instanceName,
|
|
211
|
+
projectName: '',
|
|
212
|
+
httpPort: url.port || (url.protocol === 'https:' ? '443' : '80'),
|
|
213
|
+
url: url.href,
|
|
214
|
+
containers: [],
|
|
215
|
+
};
|
|
216
|
+
directServerStatuses.set(workspaceId, status);
|
|
217
|
+
updateDevServerStatus(workspaceId, 'running');
|
|
218
|
+
emitEphemeral(workspaceId, 'devserver:status', status);
|
|
219
|
+
break;
|
|
220
|
+
}
|
|
221
|
+
};
|
|
222
|
+
};
|
|
223
|
+
const observeStdout = observeOutput();
|
|
224
|
+
const observeStderr = observeOutput();
|
|
167
225
|
proc.stdout?.on('data', (data) => {
|
|
168
226
|
console.log(`[dev-server:${instanceName}] ${data.toString().trim()}`);
|
|
227
|
+
observeStdout(data);
|
|
169
228
|
});
|
|
170
229
|
proc.stderr?.on('data', (data) => {
|
|
171
230
|
console.error(`[dev-server:${instanceName}] ${data.toString().trim()}`);
|
|
231
|
+
observeStderr(data);
|
|
172
232
|
});
|
|
173
233
|
proc.on('exit', (code) => {
|
|
174
234
|
// Shell exit does not imply that its background children have stopped.
|
|
@@ -181,6 +241,7 @@ export function startDevServer(workspaceId) {
|
|
|
181
241
|
if (trackedProcesses.get(workspaceId) !== proc || stoppingProcesses.has(workspaceId))
|
|
182
242
|
return;
|
|
183
243
|
trackedProcesses.delete(workspaceId);
|
|
244
|
+
directServerStatuses.delete(workspaceId);
|
|
184
245
|
const currentStatus = await getStatus(workspace.projectPath, workspace.workingBranch);
|
|
185
246
|
if (shuttingDown || generations.get(workspaceId) !== generation || stoppingProcesses.has(workspaceId))
|
|
186
247
|
return;
|
|
@@ -200,6 +261,7 @@ export function startDevServer(workspaceId) {
|
|
|
200
261
|
// A signal failure is also an 'error' event; it does not prove process exit.
|
|
201
262
|
if (!proc.pid)
|
|
202
263
|
trackedProcesses.delete(workspaceId);
|
|
264
|
+
directServerStatuses.delete(workspaceId);
|
|
203
265
|
updateDevServerStatus(workspaceId, 'error');
|
|
204
266
|
console.error(`[dev-server] Process error for workspace ${workspaceId}:`, err);
|
|
205
267
|
emitEphemeral(workspaceId, 'devserver:status', {
|
|
@@ -233,6 +295,7 @@ export async function stopDevServer(workspaceId) {
|
|
|
233
295
|
if (pending)
|
|
234
296
|
return pending;
|
|
235
297
|
generations.set(workspaceId, (generations.get(workspaceId) ?? 0) + 1);
|
|
298
|
+
directServerStatuses.delete(workspaceId);
|
|
236
299
|
const stopping = stopDevServerOwned(workspaceId);
|
|
237
300
|
stoppingProcesses.set(workspaceId, stopping);
|
|
238
301
|
try {
|
|
@@ -333,6 +396,7 @@ async function stopTrackedProcess(workspaceId) {
|
|
|
333
396
|
throw new Error('Dev server stop is not confirmed; retry after the process exits');
|
|
334
397
|
if (trackedProcesses.get(workspaceId) === tracked)
|
|
335
398
|
trackedProcesses.delete(workspaceId);
|
|
399
|
+
directServerStatuses.delete(workspaceId);
|
|
336
400
|
}
|
|
337
401
|
async function stopDevServerOwned(workspaceId) {
|
|
338
402
|
const workspace = getWorkspace(workspaceId);
|
|
@@ -66,13 +66,15 @@ function liveProbe() {
|
|
|
66
66
|
},
|
|
67
67
|
worktreesDirectory: (project) => resolveWorktreesRoot(project, getGlobalSettings().worktreesPath),
|
|
68
68
|
codexBinary: resolveCodexBinary,
|
|
69
|
+
mistralBinary: () => 'vibe-acp',
|
|
69
70
|
};
|
|
70
71
|
}
|
|
71
72
|
/** No credential files, provider calls, generated prompts, or raw process errors in this report. */
|
|
72
73
|
export async function checkEnvironment(request, probe = liveProbe()) {
|
|
73
|
-
const [major, minor] = probe.nodeVersion.split('.').map(Number);
|
|
74
|
+
const [major, minor, patch] = probe.nodeVersion.split('.').map(Number);
|
|
75
|
+
const supportedNode = major > 26 || (major === 26 && (minor > 12 || (minor === 12 && patch >= 1)));
|
|
74
76
|
const checks = [
|
|
75
|
-
{ code: 'node', status:
|
|
77
|
+
{ code: 'node', status: supportedNode ? 'ok' : 'error' },
|
|
76
78
|
{ code: 'platform', status: ['linux', 'darwin'].includes(probe.platform) ? 'ok' : 'error' },
|
|
77
79
|
];
|
|
78
80
|
const commandAvailable = async (command, args) => {
|
|
@@ -84,7 +86,11 @@ export async function checkEnvironment(request, probe = liveProbe()) {
|
|
|
84
86
|
return error.code === 'ENOENT' ? 'missing' : 'error';
|
|
85
87
|
}
|
|
86
88
|
};
|
|
87
|
-
const binary = request.engine === '
|
|
89
|
+
const binary = request.engine === 'mistral-vibe'
|
|
90
|
+
? probe.mistralBinary()
|
|
91
|
+
: request.engine === 'codex'
|
|
92
|
+
? probe.codexBinary()
|
|
93
|
+
: probe.claudeBinary();
|
|
88
94
|
const [git, shell, storage, runtime] = await Promise.all([
|
|
89
95
|
commandAvailable('git', ['--version']),
|
|
90
96
|
commandAvailable('bash', ['--version']),
|
|
@@ -154,7 +154,7 @@ function assertInternalActorCanAccessScope(actor, scope) {
|
|
|
154
154
|
if (!source || source.workspace_id !== actor.workspaceId) {
|
|
155
155
|
throw new MemoryNotFoundError('Agent session is not bound to the stated workspace');
|
|
156
156
|
}
|
|
157
|
-
if (source.engine !== 'claude-code' && source.engine !== 'codex') {
|
|
157
|
+
if (source.engine !== 'claude-code' && source.engine !== 'codex' && source.engine !== 'mistral-vibe') {
|
|
158
158
|
throw new MemoryNotFoundError('Bound agent session has no valid engine');
|
|
159
159
|
}
|
|
160
160
|
if (source.engine !== actor.engine)
|
|
@@ -0,0 +1,145 @@
|
|
|
1
|
+
import { stat } from 'node:fs/promises';
|
|
2
|
+
import { homedir } from 'node:os';
|
|
3
|
+
import { resolve } from 'node:path';
|
|
4
|
+
import * as acp from '@agentclientprotocol/sdk';
|
|
5
|
+
import { closeVibeAcpSession, connectVibeAcp, requestVibeAcp, } from './agent/engines/mistral-vibe/client.js';
|
|
6
|
+
import { assertMistralSelection, MISTRAL_MODES, readMistralSelect, requireMistralOption, } from './agent/engines/mistral-vibe/configuration.js';
|
|
7
|
+
import { spawnVibeAcp } from './agent/engines/mistral-vibe/spawn.js';
|
|
8
|
+
import { getWorkspace } from './workspace-service.js';
|
|
9
|
+
const pending = new Map();
|
|
10
|
+
const cache = new Map();
|
|
11
|
+
let owned = 0;
|
|
12
|
+
export async function getMistralOptions(input) {
|
|
13
|
+
for (const [key, value] of Object.entries(input)) {
|
|
14
|
+
if (value !== undefined &&
|
|
15
|
+
(typeof value !== 'string' ||
|
|
16
|
+
!value.trim() ||
|
|
17
|
+
value.length > (key === 'projectPath' ? 4096 : 256) ||
|
|
18
|
+
value.includes('\0')))
|
|
19
|
+
throw new Error(`Invalid Mistral Vibe ${key}`);
|
|
20
|
+
}
|
|
21
|
+
if (input.workspaceId && input.projectPath)
|
|
22
|
+
throw new Error('Choose workspaceId or projectPath, not both');
|
|
23
|
+
const permission = input.agentPermissionMode ?? 'interactive';
|
|
24
|
+
if (!Object.hasOwn(MISTRAL_MODES, permission))
|
|
25
|
+
throw new Error('Mistral Vibe requires a supported native permission mode');
|
|
26
|
+
assertMistralSelection(input.model, 'model');
|
|
27
|
+
const workspace = input.workspaceId ? getWorkspace(input.workspaceId) : undefined;
|
|
28
|
+
if (input.workspaceId && !workspace)
|
|
29
|
+
throw new Error('Workspace not found');
|
|
30
|
+
if (workspace &&
|
|
31
|
+
(typeof workspace.worktreePath !== 'string' || !workspace.worktreePath || workspace.worktreePurgedAt))
|
|
32
|
+
throw new Error('Restore the workspace context before discovering Vibe options');
|
|
33
|
+
const cwd = resolve(workspace?.worktreePath ?? input.projectPath ?? homedir());
|
|
34
|
+
if (!(await stat(cwd).catch(() => undefined))?.isDirectory())
|
|
35
|
+
throw new Error('Mistral Vibe context directory does not exist');
|
|
36
|
+
const mode = MISTRAL_MODES[permission];
|
|
37
|
+
const model = input.model && input.model !== 'auto' ? input.model : undefined;
|
|
38
|
+
const key = JSON.stringify([cwd, mode, model]);
|
|
39
|
+
const cached = cache.get(key);
|
|
40
|
+
if (cached && cached.expires > Date.now())
|
|
41
|
+
return cached.options;
|
|
42
|
+
const existing = pending.get(key);
|
|
43
|
+
if (existing)
|
|
44
|
+
return existing;
|
|
45
|
+
if (owned >= 4)
|
|
46
|
+
throw new Error('Mistral Vibe discovery is busy or awaiting native closure; try again after closure');
|
|
47
|
+
owned++;
|
|
48
|
+
const probe = (async () => {
|
|
49
|
+
let runtime;
|
|
50
|
+
try {
|
|
51
|
+
const env = Object.fromEntries(Object.entries(process.env).filter(([name]) => !/^KOBO_.*TOKEN$/.test(name)));
|
|
52
|
+
runtime = spawnVibeAcp({
|
|
53
|
+
cwd,
|
|
54
|
+
env: { ...env, VIBE_DEFAULT_AGENT: mode, ...(model ? { VIBE_ACTIVE_MODEL: model } : {}) },
|
|
55
|
+
});
|
|
56
|
+
}
|
|
57
|
+
catch {
|
|
58
|
+
owned--;
|
|
59
|
+
throw new Error('Cannot start native Vibe discovery; check vibe-acp on PATH');
|
|
60
|
+
}
|
|
61
|
+
let connection;
|
|
62
|
+
let sessionId;
|
|
63
|
+
let canClose = false;
|
|
64
|
+
// Failed shutdown never releases a slot while native ownership remains unresolved.
|
|
65
|
+
void runtime.closed.then(() => {
|
|
66
|
+
owned--;
|
|
67
|
+
connection?.close();
|
|
68
|
+
});
|
|
69
|
+
try {
|
|
70
|
+
connection = connectVibeAcp(runtime.child, {
|
|
71
|
+
requestPermission: () => ({ outcome: { outcome: 'cancelled' } }),
|
|
72
|
+
createElicitation: () => ({ action: 'cancel' }),
|
|
73
|
+
});
|
|
74
|
+
const initialized = await requestVibeAcp(connection, acp.methods.agent.initialize, {
|
|
75
|
+
protocolVersion: acp.PROTOCOL_VERSION,
|
|
76
|
+
clientCapabilities: {},
|
|
77
|
+
});
|
|
78
|
+
const capabilities = initialized.agentCapabilities;
|
|
79
|
+
if (!capabilities || typeof capabilities !== 'object' || Array.isArray(capabilities))
|
|
80
|
+
throw new Error('Invalid Vibe ACP capabilities');
|
|
81
|
+
if (capabilities.sessionCapabilities != null &&
|
|
82
|
+
(typeof capabilities.sessionCapabilities !== 'object' || Array.isArray(capabilities.sessionCapabilities)))
|
|
83
|
+
throw new Error('Invalid Vibe ACP session capabilities');
|
|
84
|
+
const close = capabilities.sessionCapabilities?.close;
|
|
85
|
+
if (close != null && (typeof close !== 'object' || Array.isArray(close)))
|
|
86
|
+
throw new Error('Invalid Vibe ACP close capability');
|
|
87
|
+
canClose = close != null;
|
|
88
|
+
const session = await requestVibeAcp(connection, acp.methods.agent.session.new, { cwd, mcpServers: [] });
|
|
89
|
+
sessionId = session.sessionId;
|
|
90
|
+
if (sessionId.length > 256 || sessionId.includes('\0'))
|
|
91
|
+
throw new Error('Invalid Vibe ACP identity');
|
|
92
|
+
requireMistralOption(session, 'mode', mode, true);
|
|
93
|
+
if (model)
|
|
94
|
+
requireMistralOption(session, 'model', model, true);
|
|
95
|
+
const options = { model: readMistralSelect(session, 'model'), thinking: readMistralSelect(session, 'thinking') };
|
|
96
|
+
await runtime.stop(() => closeVibeAcpSession(connection, { sessionId: sessionId, active: false, canClose }));
|
|
97
|
+
await runtime.closed;
|
|
98
|
+
if (cache.size >= 32)
|
|
99
|
+
cache.delete(cache.keys().next().value);
|
|
100
|
+
cache.set(key, { options, expires: Date.now() + 30_000 });
|
|
101
|
+
return options;
|
|
102
|
+
}
|
|
103
|
+
catch {
|
|
104
|
+
throw new Error('Native Vibe options unavailable; check the executable, authentication, project/profile and selected model, then retry');
|
|
105
|
+
}
|
|
106
|
+
finally {
|
|
107
|
+
await runtime
|
|
108
|
+
.stop(connection && sessionId
|
|
109
|
+
? () => closeVibeAcpSession(connection, { sessionId: sessionId, active: false, canClose })
|
|
110
|
+
: undefined)
|
|
111
|
+
.catch(() => {
|
|
112
|
+
throw new Error('Native Vibe discovery shutdown unconfirmed; ownership retained until confirmed closure or server restart');
|
|
113
|
+
});
|
|
114
|
+
}
|
|
115
|
+
})();
|
|
116
|
+
pending.set(key, probe);
|
|
117
|
+
try {
|
|
118
|
+
return await probe;
|
|
119
|
+
}
|
|
120
|
+
finally {
|
|
121
|
+
pending.delete(key);
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
/** External mutation preflight; the actual new/load session is still authoritative. */
|
|
125
|
+
export async function preflightMistralConfiguration(engine, context, selection, resume = false) {
|
|
126
|
+
if (engine !== 'mistral-vibe')
|
|
127
|
+
return;
|
|
128
|
+
assertMistralSelection(selection.model, 'model');
|
|
129
|
+
assertMistralSelection(selection.reasoningEffort, 'thinking');
|
|
130
|
+
// New-session defaults cannot validate thinking for an Auto model pinned in a saved session.
|
|
131
|
+
if (resume && (selection.model === undefined || selection.model === 'auto'))
|
|
132
|
+
return;
|
|
133
|
+
if ((selection.model === undefined || selection.model === 'auto') &&
|
|
134
|
+
(selection.reasoningEffort === undefined || selection.reasoningEffort === 'auto'))
|
|
135
|
+
return;
|
|
136
|
+
const options = await getMistralOptions({
|
|
137
|
+
...context,
|
|
138
|
+
model: selection.model,
|
|
139
|
+
agentPermissionMode: selection.agentPermissionMode,
|
|
140
|
+
});
|
|
141
|
+
if (selection.reasoningEffort !== undefined &&
|
|
142
|
+
selection.reasoningEffort !== 'auto' &&
|
|
143
|
+
!options.thinking.options.some((option) => option.value === selection.reasoningEffort))
|
|
144
|
+
throw new Error('Mistral Vibe thinking selection is unavailable for this model; refresh native options');
|
|
145
|
+
}
|
|
@@ -6,6 +6,7 @@ import { hasController } from './agent/orchestrator.js';
|
|
|
6
6
|
import { stopDevServer } from './dev-server-service.js';
|
|
7
7
|
import { getForgeProvider } from './forge/registry.js';
|
|
8
8
|
import { resolveForge } from './forge/resolve.js';
|
|
9
|
+
import { deriveReadyToMerge } from './forge/types.js';
|
|
9
10
|
import { computeGitStats } from './git-stats-service.js';
|
|
10
11
|
import * as lifecycleHookService from './lifecycle-hook-service.js';
|
|
11
12
|
import { getGlobalSettings } from './settings-service.js';
|
|
@@ -38,15 +39,31 @@ async function runBounded(items, concurrency, worker) {
|
|
|
38
39
|
});
|
|
39
40
|
await Promise.all(workers);
|
|
40
41
|
}
|
|
41
|
-
/**
|
|
42
|
-
* (state, base, reviewDecision). */
|
|
42
|
+
/** Latest snapshot for the UI, including explicit refreshes. */
|
|
43
43
|
const lastKnownPr = new Map();
|
|
44
|
+
/** Only the watcher consumes transitions; refreshing the UI must not consume a merge. */
|
|
45
|
+
const lastProcessedPr = new Map();
|
|
46
|
+
function recordProcessedPr(workspaceId, snapshot, displayedAtStart) {
|
|
47
|
+
const previous = lastProcessedPr.get(workspaceId);
|
|
48
|
+
// A refresh completed during this tick's Git work may already have newer data.
|
|
49
|
+
if (lastKnownPr.get(workspaceId) === displayedAtStart)
|
|
50
|
+
lastKnownPr.set(workspaceId, snapshot);
|
|
51
|
+
// Keep notification history through a recalculation, while the public snapshot
|
|
52
|
+
// still blocks merging until the forge has actually confirmed mergeability.
|
|
53
|
+
const keepReadiness = previous?.number === snapshot.number &&
|
|
54
|
+
previous.url === snapshot.url &&
|
|
55
|
+
previous.readyToMerge &&
|
|
56
|
+
snapshot.mergeable === 'UNKNOWN' &&
|
|
57
|
+
deriveReadyToMerge({ ...snapshot, mergeable: 'MERGEABLE' });
|
|
58
|
+
lastProcessedPr.set(workspaceId, keepReadiness ? { ...snapshot, readyToMerge: true } : snapshot);
|
|
59
|
+
}
|
|
44
60
|
/** Latest git-stats snapshot per workspace, refreshed each watcher tick. */
|
|
45
61
|
const lastKnownGitStats = new Map();
|
|
46
62
|
const activeChecks = new Map();
|
|
47
63
|
/** Also cancels stale results from checks that started before restoration. */
|
|
48
64
|
export function invalidateWorkspacePrCaches(workspaceId) {
|
|
49
65
|
lastKnownPr.delete(workspaceId);
|
|
66
|
+
lastProcessedPr.delete(workspaceId);
|
|
50
67
|
lastKnownGitStats.delete(workspaceId);
|
|
51
68
|
for (const check of activeChecks.get(workspaceId) ?? [])
|
|
52
69
|
check.invalidated = true;
|
|
@@ -77,6 +94,9 @@ export function getAllGitStats() {
|
|
|
77
94
|
* workspace's entry, so this must be done explicitly by the caller). */
|
|
78
95
|
export function clearPrSnapshotCache(workspaceId) {
|
|
79
96
|
lastKnownPr.delete(workspaceId);
|
|
97
|
+
lastProcessedPr.delete(workspaceId);
|
|
98
|
+
for (const check of activeChecks.get(workspaceId) ?? [])
|
|
99
|
+
check.invalidated = true;
|
|
80
100
|
}
|
|
81
101
|
/**
|
|
82
102
|
* Test-only escape hatch — drops the in-memory cache so each test starts
|
|
@@ -84,6 +104,7 @@ export function clearPrSnapshotCache(workspaceId) {
|
|
|
84
104
|
*/
|
|
85
105
|
export function _resetForTest() {
|
|
86
106
|
lastKnownPr.clear();
|
|
107
|
+
lastProcessedPr.clear();
|
|
87
108
|
lastKnownGitStats.clear();
|
|
88
109
|
}
|
|
89
110
|
/**
|
|
@@ -127,9 +148,10 @@ export async function checkPrStatuses() {
|
|
|
127
148
|
await autoRestoreManuallyRecreatedWorktrees();
|
|
128
149
|
const workspaces = listWorkspaces(false); // non-archived only
|
|
129
150
|
// Clean up entries for workspaces that no longer exist
|
|
130
|
-
for (const id of lastKnownPr.keys()) {
|
|
151
|
+
for (const id of new Set([...lastKnownPr.keys(), ...lastProcessedPr.keys()])) {
|
|
131
152
|
if (!workspaces.some((ws) => ws.id === id)) {
|
|
132
153
|
lastKnownPr.delete(id);
|
|
154
|
+
lastProcessedPr.delete(id);
|
|
133
155
|
}
|
|
134
156
|
}
|
|
135
157
|
for (const id of lastKnownGitStats.keys()) {
|
|
@@ -142,6 +164,7 @@ export async function checkPrStatuses() {
|
|
|
142
164
|
// floods the logs when a worktree was deleted externally.
|
|
143
165
|
if (isWorkspaceLifecycleBusy(ws.id) || !fs.existsSync(ws.worktreePath))
|
|
144
166
|
return;
|
|
167
|
+
const displayedAtStart = lastKnownPr.get(ws.id);
|
|
145
168
|
const check = { invalidated: false };
|
|
146
169
|
const checks = activeChecks.get(ws.id) ?? new Set();
|
|
147
170
|
checks.add(check);
|
|
@@ -172,7 +195,7 @@ export async function checkPrStatuses() {
|
|
|
172
195
|
// `gh pr edit --base …`, the lag was painful and confusing.
|
|
173
196
|
let baseTransitionedFrom = null;
|
|
174
197
|
if (pr?.state === 'OPEN' && pr.base) {
|
|
175
|
-
const prevBase =
|
|
198
|
+
const prevBase = ws.sourceBranch;
|
|
176
199
|
if (prevBase !== pr.base) {
|
|
177
200
|
try {
|
|
178
201
|
updateWorkspaceSourceBranch(ws.id, pr.base);
|
|
@@ -211,7 +234,8 @@ export async function checkPrStatuses() {
|
|
|
211
234
|
}
|
|
212
235
|
if (stale() || !pr)
|
|
213
236
|
return;
|
|
214
|
-
const
|
|
237
|
+
const processed = lastProcessedPr.get(ws.id);
|
|
238
|
+
const prev = processed?.number === pr.number && processed.url === pr.url ? processed : undefined;
|
|
215
239
|
// We delay updating `lastKnownPr` until after the actions succeed.
|
|
216
240
|
// Setting it eagerly would poison the cache: if updateWorkspaceSourceBranch
|
|
217
241
|
// throws (transient DB issue, race with workspace deletion), the cache
|
|
@@ -242,7 +266,7 @@ export async function checkPrStatuses() {
|
|
|
242
266
|
// manual archive route and purge-worktree, this path never stops the
|
|
243
267
|
// agent, so archiving under one orphans its pending question and
|
|
244
268
|
// leaves it writing into a workspace the UI now shows as archived.
|
|
245
|
-
|
|
269
|
+
recordProcessedPr(ws.id, pr, displayedAtStart);
|
|
246
270
|
return;
|
|
247
271
|
}
|
|
248
272
|
console.log(`[pr-watcher] PR ${pr.state.toLowerCase()} for workspace '${ws.name}' — archiving`);
|
|
@@ -275,6 +299,7 @@ export async function checkPrStatuses() {
|
|
|
275
299
|
if (!archived)
|
|
276
300
|
return;
|
|
277
301
|
lastKnownPr.delete(ws.id);
|
|
302
|
+
lastProcessedPr.delete(ws.id);
|
|
278
303
|
emitEphemeral(ws.id, 'workspace:archived', {
|
|
279
304
|
reason: `PR ${pr.state.toLowerCase()}`,
|
|
280
305
|
prUrl: pr.url,
|
|
@@ -344,10 +369,10 @@ export async function checkPrStatuses() {
|
|
|
344
369
|
// missing one — keeps the OPEN→CLOSED/MERGED archiving logic stable.
|
|
345
370
|
if (pr.state !== 'OPEN' || !pr.base) {
|
|
346
371
|
const next = pr.base ? pr : { ...pr, base: prev?.base ?? pr.base };
|
|
347
|
-
|
|
372
|
+
recordProcessedPr(ws.id, next, displayedAtStart);
|
|
348
373
|
return;
|
|
349
374
|
}
|
|
350
|
-
|
|
375
|
+
recordProcessedPr(ws.id, pr, displayedAtStart);
|
|
351
376
|
// Emit the base-change event AFTER the snapshot is committed so a
|
|
352
377
|
// sync:response replay on reconnect sees a consistent state. The
|
|
353
378
|
// DB update + ws.sourceBranch mutation already happened above
|
|
@@ -372,9 +397,8 @@ export async function checkPrStatuses() {
|
|
|
372
397
|
}
|
|
373
398
|
/**
|
|
374
399
|
* On-demand refresh of a single workspace's PR snapshot. Bypasses the 30s tick.
|
|
375
|
-
*
|
|
376
|
-
*
|
|
377
|
-
* looking at.
|
|
400
|
+
* Publishes the snapshot and synchronizes the PR base. Lifecycle transitions
|
|
401
|
+
* remain pending for the watcher, including OPEN → MERGED after a UI merge.
|
|
378
402
|
*
|
|
379
403
|
* Returns the fresh snapshot, or null if the workspace has no PR (cache entry
|
|
380
404
|
* cleared in that case). Throws if the workspace doesn't exist.
|
|
@@ -421,6 +445,11 @@ export async function refreshPrSnapshot(workspaceId) {
|
|
|
421
445
|
}
|
|
422
446
|
}
|
|
423
447
|
lastKnownPr.set(workspaceId, snap);
|
|
448
|
+
const processed = lastProcessedPr.get(workspaceId);
|
|
449
|
+
if (!ws.prWatchDisabledAt &&
|
|
450
|
+
!current.prWatchDisabledAt &&
|
|
451
|
+
(processed?.number !== snap.number || processed.url !== snap.url))
|
|
452
|
+
lastProcessedPr.set(workspaceId, snap);
|
|
424
453
|
return snap;
|
|
425
454
|
}
|
|
426
455
|
finally {
|
|
@@ -9,6 +9,7 @@ import { listEngines } from './agent/engines/registry.js';
|
|
|
9
9
|
import * as agentManager from './agent/orchestrator.js';
|
|
10
10
|
import { bindReviewSession, getFinalReviewSourceSession } from './auto-loop-final-review-service.js';
|
|
11
11
|
import { canStartAutomatically } from './auto-loop-service.js';
|
|
12
|
+
import { preflightMistralConfiguration } from './mistral-options-service.js';
|
|
12
13
|
import { beginReviewLaunch, finishReviewLaunch } from './review-launch-runtime.js';
|
|
13
14
|
import { getReviewReturn, registerReviewReturn, restoreReviewConfiguration } from './review-return-service.js';
|
|
14
15
|
import { getActiveReviewTemplate, renderReviewTemplate } from './review-template-service.js';
|
|
@@ -47,20 +48,28 @@ function resolveConfiguration(workspace, body, allowCustomModel = false) {
|
|
|
47
48
|
const engine = listEngines().find((e) => e.id === engineId);
|
|
48
49
|
if (!engine)
|
|
49
50
|
throw new ReviewRequestError(`Unknown engine '${engineId}'`, 400);
|
|
51
|
+
if (!engine.capabilities.supportsReadOnly && engineId !== 'mistral-vibe')
|
|
52
|
+
throw new ReviewRequestError(`Engine '${engineId}' does not support read-only reviews`, 400);
|
|
50
53
|
const changedEngine = engineId !== workspace.engine;
|
|
51
54
|
const config = {
|
|
52
55
|
engine: engineId,
|
|
53
56
|
model: body.model ?? (changedEngine ? 'auto' : workspace.model),
|
|
54
57
|
reasoningEffort: body.reasoningEffort ?? (changedEngine ? 'auto' : workspace.reasoningEffort),
|
|
55
|
-
//
|
|
56
|
-
agentPermissionMode:
|
|
58
|
+
// Native reviews have independent permissions; other returning reviewers use Plan.
|
|
59
|
+
agentPermissionMode: engineId === 'mistral-vibe'
|
|
60
|
+
? (body.agentPermissionMode ?? 'interactive')
|
|
61
|
+
: body.returnToSession
|
|
62
|
+
? 'plan'
|
|
63
|
+
: (body.agentPermissionMode ?? workspace.agentPermissionMode),
|
|
57
64
|
};
|
|
58
65
|
// Preserve an existing custom model when unchanged; reject incompatible selections.
|
|
59
|
-
if (
|
|
66
|
+
if (engineId !== 'mistral-vibe' &&
|
|
67
|
+
!allowCustomModel &&
|
|
60
68
|
(changedEngine || config.model !== workspace.model) &&
|
|
61
69
|
!engine.capabilities.models.some((m) => m.id === config.model))
|
|
62
70
|
throw new ReviewRequestError(`Model '${config.model}' is not supported by '${engineId}'`, 400);
|
|
63
|
-
if (
|
|
71
|
+
if (engineId !== 'mistral-vibe' &&
|
|
72
|
+
(changedEngine || config.reasoningEffort !== workspace.reasoningEffort) &&
|
|
64
73
|
!engine.capabilities.effortLevels?.some((e) => e.id === config.reasoningEffort))
|
|
65
74
|
throw new ReviewRequestError(`Reasoning effort '${config.reasoningEffort}' is not supported by '${engineId}'`, 400);
|
|
66
75
|
if (!engine.capabilities.permissionModes.includes(config.agentPermissionMode))
|
|
@@ -81,6 +90,15 @@ export async function startWorkspaceReview(id, input, internal) {
|
|
|
81
90
|
throw new ReviewRequestError(`Workspace '${id}' not found`, 404);
|
|
82
91
|
const body = parseReviewRequest(input);
|
|
83
92
|
const configuration = resolveConfiguration(workspace, body, internal?.autoLoopFinalReview);
|
|
93
|
+
const nativeReview = configuration.engine === 'mistral-vibe';
|
|
94
|
+
if (nativeReview && internal?.autoLoopFinalReview)
|
|
95
|
+
configuration.agentPermissionMode = 'interactive';
|
|
96
|
+
try {
|
|
97
|
+
await preflightMistralConfiguration(configuration.engine, { workspaceId: id }, configuration);
|
|
98
|
+
}
|
|
99
|
+
catch (error) {
|
|
100
|
+
throw new ReviewRequestError(error instanceof Error ? error.message : 'Invalid native review configuration', 400);
|
|
101
|
+
}
|
|
84
102
|
assertWorkspaceLifecycleAvailable(id);
|
|
85
103
|
if (workspace.archivedAt || workspace.worktreePurgedAt)
|
|
86
104
|
throw new ReviewRequestError('Restore the workspace before starting a review', 409);
|
|
@@ -94,7 +112,7 @@ export async function startWorkspaceReview(id, input, internal) {
|
|
|
94
112
|
const differsFromSession = !!originalSession &&
|
|
95
113
|
((!!originalSession.engine && originalSession.engine !== configuration.engine) ||
|
|
96
114
|
(configuration.model !== 'auto' && !!originalSession.model && originalSession.model !== configuration.model));
|
|
97
|
-
const newSession = body.newSession === true || changed || differsFromSession || body.returnToSession === true;
|
|
115
|
+
const newSession = nativeReview || body.newSession === true || changed || differsFromSession || body.returnToSession === true;
|
|
98
116
|
if (body.returnToSession &&
|
|
99
117
|
(!originalSession?.engineSessionId || (originalSession.engine && originalSession.engine !== workspace.engine)))
|
|
100
118
|
throw new ReviewRequestError('The current session cannot be resumed; start it before requesting an automatic return', 400);
|
|
@@ -134,7 +152,10 @@ export async function startWorkspaceReview(id, input, internal) {
|
|
|
134
152
|
});
|
|
135
153
|
// The return fires only when the review session ends by itself: a reviewer
|
|
136
154
|
// that starts fixing, or waits on a question, never hands back.
|
|
137
|
-
if (
|
|
155
|
+
if (nativeReview)
|
|
156
|
+
rendered +=
|
|
157
|
+
'\n\nThis is a report-only review under native Vibe permissions, without guaranteed filesystem read-only enforcement. These rules override any template or skill suggesting fixes: do not modify, create or delete files, commit, push, or mutate tasks, workspace state or memory. Report findings and recommended fixes only. Respect native approval requests; never bypass or auto-approve them. If approval is required, wait for the user. Finish with a standalone report including severity, file/line references and remaining uncertainty.';
|
|
158
|
+
else if (body.returnToSession)
|
|
138
159
|
rendered +=
|
|
139
160
|
'\n\nThis is a read-only review session; these rules override any template instruction or skill that suggests fixing issues. Do not modify, create or delete files, and do not commit or push: the original agent session applies the fixes. Running read-only checks such as tests or linters is fine. Do not ask the user questions or wait for input: note any decision that needs the user in your summary instead.' +
|
|
140
161
|
'\n\nFinish with a standalone summary of your review findings, including severity, file/line references, recommended fixes and any remaining uncertainty. This final message will be handed back to the original agent session. Do not rely on earlier messages to explain your findings.';
|
|
@@ -177,7 +198,10 @@ export async function startWorkspaceReview(id, input, internal) {
|
|
|
177
198
|
original: {
|
|
178
199
|
engine: workspace.engine,
|
|
179
200
|
model: workspace.model,
|
|
180
|
-
...(
|
|
201
|
+
...((workspace.engine !== 'mistral-vibe' || workspace.model !== 'auto') && originalSession.model
|
|
202
|
+
? { sessionModel: originalSession.model }
|
|
203
|
+
: {}),
|
|
204
|
+
// Restore iteration intent separately; native review returns retain saved session thinking.
|
|
181
205
|
reasoningEffort: workspace.reasoningEffort,
|
|
182
206
|
agentPermissionMode: workspace.agentPermissionMode,
|
|
183
207
|
},
|
|
@@ -204,9 +228,9 @@ export async function startWorkspaceReview(id, input, internal) {
|
|
|
204
228
|
pendingSessionId,
|
|
205
229
|
configuration.reasoningEffort,
|
|
206
230
|
];
|
|
207
|
-
if (reviewLaunch) {
|
|
231
|
+
if (nativeReview || reviewLaunch) {
|
|
208
232
|
startArgs[8] = undefined;
|
|
209
|
-
startArgs[9] = { mcpEnv: reviewLaunch
|
|
233
|
+
startArgs[9] = { mcpEnv: { ...reviewLaunch?.mcpEnv, ...(nativeReview ? { KOBO_REVIEW_ONLY: '1' } : {}) } };
|
|
210
234
|
}
|
|
211
235
|
const agent = agentManager.startAgent(...startArgs);
|
|
212
236
|
workspaceService.updateWorkspaceStatus(id, 'executing');
|
|
@@ -6,9 +6,11 @@ import { getDb } from '../db/index.js';
|
|
|
6
6
|
import { assertAgentStopped } from '../utils/agent-stop-result.js';
|
|
7
7
|
import { ensureDirectoryInside } from '../utils/safe-path.js';
|
|
8
8
|
import { reserveWorkspaceLifecycle } from '../utils/workspace-lifecycle-guard.js';
|
|
9
|
+
import { assertEngineConfigurationSupported } from './agent/engines/configuration.js';
|
|
9
10
|
import { listEngines } from './agent/engines/registry.js';
|
|
10
11
|
import * as agents from './agent/orchestrator.js';
|
|
11
12
|
import { buildEngineHandoff } from './engine-handoff-service.js';
|
|
13
|
+
import { preflightMistralConfiguration } from './mistral-options-service.js';
|
|
12
14
|
import { getReviewReturn } from './review-return-service.js';
|
|
13
15
|
import { activateSession, deactivateSession } from './session-activity-service.js';
|
|
14
16
|
import { registerHandoffStopHandler } from './session-handoff-runtime.js';
|
|
@@ -180,6 +182,15 @@ function parseRequest(input) {
|
|
|
180
182
|
},
|
|
181
183
|
};
|
|
182
184
|
}
|
|
185
|
+
export async function preflightSessionHandoff(workspaceId, input) {
|
|
186
|
+
const body = parseRequest(input);
|
|
187
|
+
// Existing requests are replayed by createSessionHandoff with fingerprint validation.
|
|
188
|
+
if (getDb()
|
|
189
|
+
.prepare('SELECT 1 FROM session_handoffs WHERE workspace_id = ? AND request_id = ?')
|
|
190
|
+
.get(workspaceId, body.requestId))
|
|
191
|
+
return;
|
|
192
|
+
await preflightMistralConfiguration(body.target.engine, { workspaceId }, body.target);
|
|
193
|
+
}
|
|
183
194
|
export function createSessionHandoff(workspaceId, input, suppliedReport) {
|
|
184
195
|
if (agents.isShuttingDown())
|
|
185
196
|
throw new SessionHandoffError('Kōbō is shutting down');
|
|
@@ -207,11 +218,22 @@ export function createSessionHandoff(workspaceId, input, suppliedReport) {
|
|
|
207
218
|
throw new SessionHandoffError('The current session changed. Reopen the transfer dialog.');
|
|
208
219
|
const engine = listEngines().find((item) => item.id === body.target.engine);
|
|
209
220
|
if (!engine?.capabilities.permissionModes.includes(body.target.agentPermissionMode) ||
|
|
210
|
-
(
|
|
221
|
+
(engine.id !== 'mistral-vibe' &&
|
|
222
|
+
!engine.capabilities.models.some((model) => model.id === body.target.model) &&
|
|
211
223
|
!(workspace.engine === body.target.engine && workspace.model === body.target.model)) ||
|
|
212
|
-
(
|
|
224
|
+
(engine.id !== 'mistral-vibe' &&
|
|
225
|
+
body.target.reasoningEffort !== 'auto' &&
|
|
213
226
|
!engine.capabilities.effortLevels?.some((effort) => effort.id === body.target.reasoningEffort)))
|
|
214
227
|
throw new SessionHandoffError('The target engine configuration is not supported', 400);
|
|
228
|
+
try {
|
|
229
|
+
assertEngineConfigurationSupported(engine, body.target);
|
|
230
|
+
}
|
|
231
|
+
catch (error) {
|
|
232
|
+
throw new SessionHandoffError(error instanceof Error ? error.message : 'Invalid engine configuration', 400);
|
|
233
|
+
}
|
|
234
|
+
if (body.generateSummary &&
|
|
235
|
+
!listEngines().find((item) => item.id === workspace.engine)?.capabilities.supportsReadOnly)
|
|
236
|
+
throw new SessionHandoffError('The source engine does not support read-only handoff summaries', 400);
|
|
215
237
|
const id = nanoid();
|
|
216
238
|
const runtime = reserve(workspaceId, id);
|
|
217
239
|
try {
|
|
@@ -289,6 +311,10 @@ async function run(runtime) {
|
|
|
289
311
|
runtime.running = true;
|
|
290
312
|
const row = read(runtime.id);
|
|
291
313
|
try {
|
|
314
|
+
const target = map(row).target;
|
|
315
|
+
await preflightMistralConfiguration(target.engine, { workspaceId: row.workspace_id }, target);
|
|
316
|
+
if (runtime.cancelled)
|
|
317
|
+
return;
|
|
292
318
|
state(row.id, 'stopping');
|
|
293
319
|
assertAgentStopped(await agents.stopAgentAndWait(row.workspace_id, undefined, 'handoff'));
|
|
294
320
|
if (runtime.cancelled)
|