rivet-design 0.11.16 → 0.12.0

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.
Files changed (84) hide show
  1. package/dist/mcp/agent-variants/SessionStore.d.ts.map +1 -1
  2. package/dist/mcp/agent-variants/SessionStore.js +5 -3
  3. package/dist/mcp/agent-variants/SessionStore.js.map +1 -1
  4. package/dist/mcp/agent-variants/WorktreeOrchestrator.d.ts +29 -0
  5. package/dist/mcp/agent-variants/WorktreeOrchestrator.d.ts.map +1 -1
  6. package/dist/mcp/agent-variants/WorktreeOrchestrator.js +185 -14
  7. package/dist/mcp/agent-variants/WorktreeOrchestrator.js.map +1 -1
  8. package/dist/mcp/agent-variants/WorktreeOrchestrator.testHelpers.d.ts.map +1 -1
  9. package/dist/mcp/agent-variants/WorktreeOrchestrator.testHelpers.js +13 -0
  10. package/dist/mcp/agent-variants/WorktreeOrchestrator.testHelpers.js.map +1 -1
  11. package/dist/mcp/agent-variants/briefContent.d.ts +16 -0
  12. package/dist/mcp/agent-variants/briefContent.d.ts.map +1 -0
  13. package/dist/mcp/agent-variants/briefContent.js +42 -0
  14. package/dist/mcp/agent-variants/briefContent.js.map +1 -0
  15. package/dist/mcp/agent-variants/contracts.d.ts +1 -0
  16. package/dist/mcp/agent-variants/contracts.d.ts.map +1 -1
  17. package/dist/mcp/agent-variants/contracts.js +1 -1
  18. package/dist/mcp/agent-variants/contracts.js.map +1 -1
  19. package/dist/mcp/agent-variants/tools.d.ts +10 -0
  20. package/dist/mcp/agent-variants/tools.d.ts.map +1 -1
  21. package/dist/mcp/agent-variants/tools.js +11 -4
  22. package/dist/mcp/agent-variants/tools.js.map +1 -1
  23. package/dist/mcp/server.d.ts +57 -3
  24. package/dist/mcp/server.d.ts.map +1 -1
  25. package/dist/mcp/server.js +457 -48
  26. package/dist/mcp/server.js.map +1 -1
  27. package/dist/prototype-host.js +1 -1
  28. package/dist/routes/agentVariants.d.ts +15 -1
  29. package/dist/routes/agentVariants.d.ts.map +1 -1
  30. package/dist/routes/agentVariants.js +173 -7
  31. package/dist/routes/agentVariants.js.map +1 -1
  32. package/dist/routes/comments.d.ts +3 -0
  33. package/dist/routes/comments.d.ts.map +1 -0
  34. package/dist/routes/comments.js +113 -0
  35. package/dist/routes/comments.js.map +1 -0
  36. package/dist/routes/prototypeHost.d.ts +1 -1
  37. package/dist/routes/prototypeHost.d.ts.map +1 -1
  38. package/dist/routes/prototypeHost.js +164 -12
  39. package/dist/routes/prototypeHost.js.map +1 -1
  40. package/dist/server.d.ts.map +1 -1
  41. package/dist/server.js +2 -0
  42. package/dist/server.js.map +1 -1
  43. package/dist/services/BundleStore.d.ts +14 -0
  44. package/dist/services/BundleStore.d.ts.map +1 -1
  45. package/dist/services/BundleStore.js +77 -13
  46. package/dist/services/BundleStore.js.map +1 -1
  47. package/dist/services/HistoryReplayService.d.ts +78 -0
  48. package/dist/services/HistoryReplayService.d.ts.map +1 -0
  49. package/dist/services/HistoryReplayService.js +265 -0
  50. package/dist/services/HistoryReplayService.js.map +1 -0
  51. package/dist/services/ProjectDetectionService.d.ts.map +1 -1
  52. package/dist/services/ProjectDetectionService.js +14 -5
  53. package/dist/services/ProjectDetectionService.js.map +1 -1
  54. package/dist/services/PrototypeBuildService.d.ts +2 -2
  55. package/dist/services/PrototypeBuildService.js +2 -2
  56. package/dist/services/PrototypeDeployService.d.ts +11 -3
  57. package/dist/services/PrototypeDeployService.d.ts.map +1 -1
  58. package/dist/services/PrototypeDeployService.js +22 -9
  59. package/dist/services/PrototypeDeployService.js.map +1 -1
  60. package/dist/services/TreeStore.d.ts +118 -0
  61. package/dist/services/TreeStore.d.ts.map +1 -0
  62. package/dist/services/TreeStore.js +386 -0
  63. package/dist/services/TreeStore.js.map +1 -0
  64. package/dist/services/VariantHistoryService.d.ts +83 -0
  65. package/dist/services/VariantHistoryService.d.ts.map +1 -1
  66. package/dist/services/VariantHistoryService.js +169 -30
  67. package/dist/services/VariantHistoryService.js.map +1 -1
  68. package/dist/utils/skills/claude-skill.d.ts +1 -1
  69. package/dist/utils/skills/claude-skill.js +1 -1
  70. package/dist/utils/skills/cursor-rules.d.ts +1 -1
  71. package/dist/utils/skills/cursor-rules.js +1 -1
  72. package/dist/utils/skills/shared-variants-protocol.d.ts +2 -2
  73. package/dist/utils/skills/shared-variants-protocol.d.ts.map +1 -1
  74. package/dist/utils/skills/shared-variants-protocol.js +8 -6
  75. package/dist/utils/skills/shared-variants-protocol.js.map +1 -1
  76. package/package.json +1 -1
  77. package/src/ui/dist/assets/main-D5T0vjqq.js +655 -0
  78. package/src/ui/dist/assets/main-D_9NxkWD.css +1 -0
  79. package/src/ui/dist/favicon-16.png +0 -0
  80. package/src/ui/dist/favicon-32.png +0 -0
  81. package/src/ui/dist/favicon.ico +0 -0
  82. package/src/ui/dist/index.html +4 -2
  83. package/src/ui/dist/assets/main-RWAqAMXv.css +0 -1
  84. package/src/ui/dist/assets/main-X83y2cfF.js +0 -655
@@ -3,7 +3,7 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
3
3
  return (mod && mod.__esModule) ? mod : { "default": mod };
4
4
  };
5
5
  Object.defineProperty(exports, "__esModule", { value: true });
6
- exports.startMCPServer = exports.createRivetMcp = exports.ackVariantBridgeLease = exports.ackApplyBridgeLease = exports.createVariantRequestLeaseTracker = exports.buildLeasedVariantResponse = exports.buildStaticPreviewRefineResponse = exports.collectStaticPreviewRefineTargetInstructions = exports.buildApplyChangesResponse = exports.buildNoPendingChangesResponse = exports.leaseAvailableBatchForPoll = exports.resolveLeasePickupRequestId = exports.buildVariantRequestBatchResponse = exports.buildMixedVariantBatchResponse = exports.startParentProcessMonitor = exports.buildVisualEditorWatchAction = exports.configureMcpStdioLogging = void 0;
6
+ exports.startMCPServer = exports.createRivetMcp = exports.canReuseExistingProjectVisualEditor = exports.closeDetachedVisualEditorServer = exports.getActiveVisualEditorProjectAction = exports.buildVariantAckFailedResponse = exports.buildVariantManualAckBlockedResponse = exports.ackVariantBridgeLease = exports.ackApplyBridgeLease = exports.createVariantRequestLeaseTracker = exports.buildLeasedVariantResponse = exports.buildStaticPreviewRefineResponse = exports.collectStaticPreviewRefineTargetInstructions = exports.buildApplyChangesResponse = exports.buildNoPendingChangesResponse = exports.leaseAvailableBatchForPoll = exports.resolveLeasePickupRequestId = exports.buildVariantRequestBatchResponse = exports.buildMixedVariantBatchResponse = exports.startParentProcessMonitor = exports.buildVisualEditorWatchAction = exports.configureMcpStdioLogging = void 0;
7
7
  const mcp_js_1 = require("@modelcontextprotocol/sdk/server/mcp.js");
8
8
  const stdio_js_1 = require("@modelcontextprotocol/sdk/server/stdio.js");
9
9
  const v3_1 = require("zod/v3");
@@ -720,6 +720,39 @@ const variantRequestWorkKey = (sessionId, workItemId) => `${sessionId}:${workIte
720
720
  const createVariantRequestLeaseTracker = (params) => {
721
721
  const trackedByWorkItem = new Map();
722
722
  const trackedByRequest = new Map();
723
+ const finishedVariantWorkByRequest = new Map();
724
+ const markGeneratedWorkFinished = (lease) => {
725
+ finishedVariantWorkByRequest.set(lease.requestId, lease.leaseId);
726
+ };
727
+ const hasFinishedGeneratedWork = (lease) => finishedVariantWorkByRequest.get(lease.requestId) === lease.leaseId;
728
+ const findTrackedLease = (args) => {
729
+ if (args.requestId) {
730
+ const tracked = trackedByRequest.get(args.requestId);
731
+ if (tracked) {
732
+ return tracked;
733
+ }
734
+ }
735
+ if (args.leaseId) {
736
+ for (const tracked of trackedByRequest.values()) {
737
+ if (tracked.leaseId === args.leaseId) {
738
+ return tracked;
739
+ }
740
+ }
741
+ }
742
+ return null;
743
+ };
744
+ const findActiveVariantLease = (args) => {
745
+ const activeLeases = params.activeVariantLeases?.() ?? [];
746
+ return (activeLeases.find((lease) => {
747
+ if (lease.kind !== 'variant') {
748
+ return false;
749
+ }
750
+ if (args.leaseId && lease.leaseId === args.leaseId) {
751
+ return true;
752
+ }
753
+ return Boolean(args.requestId && lease.requestId === args.requestId);
754
+ }) ?? null);
755
+ };
723
756
  return {
724
757
  claim(args) {
725
758
  if (!args.requestId) {
@@ -733,12 +766,14 @@ const createVariantRequestLeaseTracker = (params) => {
733
766
  if (trackedByRequest.has(lease.requestId)) {
734
767
  return;
735
768
  }
769
+ markGeneratedWorkFinished(lease);
736
770
  params.ackRequest({
737
771
  leaseId: lease.leaseId,
738
772
  requestId: lease.requestId,
739
773
  });
740
774
  return;
741
775
  }
776
+ finishedVariantWorkByRequest.delete(lease.requestId);
742
777
  const tracked = trackedByRequest.get(lease.requestId) ??
743
778
  {
744
779
  leaseId: lease.leaseId,
@@ -767,11 +802,36 @@ const createVariantRequestLeaseTracker = (params) => {
767
802
  return;
768
803
  }
769
804
  trackedByRequest.delete(tracked.requestId);
805
+ markGeneratedWorkFinished(tracked);
770
806
  params.ackRequest({
771
807
  leaseId: tracked.leaseId,
772
808
  requestId: tracked.requestId,
773
809
  });
774
810
  },
811
+ getManualAckBlock(args) {
812
+ const tracked = findTrackedLease(args);
813
+ if (!tracked || tracked.remainingWorkItemIds.size === 0) {
814
+ const activeLease = findActiveVariantLease(args);
815
+ if (!activeLease) {
816
+ return null;
817
+ }
818
+ if (hasFinishedGeneratedWork(activeLease)) {
819
+ return null;
820
+ }
821
+ return {
822
+ reason: 'variant_work_unclaimed',
823
+ leaseId: activeLease.leaseId,
824
+ requestId: activeLease.requestId,
825
+ remainingWorkItemIds: [],
826
+ };
827
+ }
828
+ return {
829
+ reason: 'variant_work_unfinished',
830
+ leaseId: tracked.leaseId,
831
+ requestId: tracked.requestId,
832
+ remainingWorkItemIds: [...tracked.remainingWorkItemIds],
833
+ };
834
+ },
775
835
  };
776
836
  };
777
837
  exports.createVariantRequestLeaseTracker = createVariantRequestLeaseTracker;
@@ -801,8 +861,13 @@ const ackApplyBridgeLease = (args) => {
801
861
  };
802
862
  exports.ackApplyBridgeLease = ackApplyBridgeLease;
803
863
  const ackVariantBridgeLease = (args) => {
804
- if (!args.leaseId)
805
- return;
864
+ if (!args.leaseId) {
865
+ return {
866
+ ok: false,
867
+ reason: 'missing_lease_id',
868
+ requestId: args.requestId,
869
+ };
870
+ }
806
871
  const ackResult = args.bridge.ackBatch({
807
872
  leaseId: args.leaseId,
808
873
  requestId: args.requestId,
@@ -816,7 +881,12 @@ const ackVariantBridgeLease = (args) => {
816
881
  changeCount: ackResult.mergedRequestIds.length,
817
882
  sessionId,
818
883
  });
819
- return;
884
+ return {
885
+ ok: true,
886
+ leaseId: args.leaseId,
887
+ requestId: args.requestId ?? ackResult.mergedRequestIds[0],
888
+ changeCount: ackResult.mergedRequestIds.length,
889
+ };
820
890
  }
821
891
  args.telemetry.trackMCPChangeAckFailed({
822
892
  channel: 'variant',
@@ -826,8 +896,82 @@ const ackVariantBridgeLease = (args) => {
826
896
  sessionId,
827
897
  failureReason: 'ack_failed',
828
898
  });
899
+ return {
900
+ ok: false,
901
+ reason: 'ack_failed',
902
+ leaseId: args.leaseId,
903
+ requestId: args.requestId,
904
+ };
829
905
  };
830
906
  exports.ackVariantBridgeLease = ackVariantBridgeLease;
907
+ /** Builds the MCP error returned when generated variant work still owns a lease. */
908
+ const buildVariantManualAckBlockedResponse = (block) => ({
909
+ content: [
910
+ {
911
+ type: 'text',
912
+ text: JSON.stringify({
913
+ error: 'variant_ack_blocked',
914
+ reason: block.reason,
915
+ message: block.reason === 'variant_work_unfinished'
916
+ ? 'This variant request still has unfinished generated work. Finish it with report_variant_complete; do not use ack_batch to skip or finish generated variant work.'
917
+ : 'This UI variant request has an active lease, but generated work has not claimed it yet. Process it through start_variants or scoped continue_variants({ action: "request_work", workItemIds }) and finish with report_variant_complete; do not use ack_batch to skip variant work.',
918
+ leaseId: block.leaseId,
919
+ requestId: block.requestId,
920
+ remainingWorkItemIds: block.remainingWorkItemIds,
921
+ }),
922
+ },
923
+ ],
924
+ isError: true,
925
+ });
926
+ exports.buildVariantManualAckBlockedResponse = buildVariantManualAckBlockedResponse;
927
+ /** Builds the MCP error returned when ack_batch does not match an active variant lease. */
928
+ const buildVariantAckFailedResponse = (result) => ({
929
+ content: [
930
+ {
931
+ type: 'text',
932
+ text: JSON.stringify({
933
+ error: 'variant_ack_failed',
934
+ reason: result.reason,
935
+ message: result.reason === 'missing_lease_id'
936
+ ? 'ack_batch requires the leaseId from the variant batch.'
937
+ : 'No active variant lease matched this ack_batch request.',
938
+ ...(result.leaseId ? { leaseId: result.leaseId } : {}),
939
+ ...(result.requestId ? { requestId: result.requestId } : {}),
940
+ }),
941
+ },
942
+ ],
943
+ isError: true,
944
+ });
945
+ exports.buildVariantAckFailedResponse = buildVariantAckFailedResponse;
946
+ /**
947
+ * Chooses how an active visual editor session should handle a project change.
948
+ */
949
+ const getActiveVisualEditorProjectAction = (args) => {
950
+ if (args.activeProjectPath === args.requestedProjectPath) {
951
+ return 'attach';
952
+ }
953
+ if (args.devServerOwnership === 'none') {
954
+ return 'rebind';
955
+ }
956
+ return 'restart';
957
+ };
958
+ exports.getActiveVisualEditorProjectAction = getActiveVisualEditorProjectAction;
959
+ /**
960
+ * Closes a Rivet server handle that is no longer attached to a bridge session.
961
+ */
962
+ const closeDetachedVisualEditorServer = async (args) => {
963
+ if (!args.serverHandle || args.isBridgeActive) {
964
+ return false;
965
+ }
966
+ await args.cleanup();
967
+ return true;
968
+ };
969
+ exports.closeDetachedVisualEditorServer = closeDetachedVisualEditorServer;
970
+ /**
971
+ * Existing-project variants need a mounted upstream for the Original preview.
972
+ */
973
+ const canReuseExistingProjectVisualEditor = (args) => Boolean(args.attachment && args.devServerOwnership !== 'none');
974
+ exports.canReuseExistingProjectVisualEditor = canReuseExistingProjectVisualEditor;
831
975
  /**
832
976
  * Builds the fully configured Rivet MCP server (tools, auth gating, session
833
977
  * bridge, telemetry) without binding it to a transport. Shared by the stdio
@@ -916,6 +1060,13 @@ const createRivetMcp = (options) => {
916
1060
  }
917
1061
  return (0, exports.buildStaticPreviewRefineResponse)(consumedRequest, droppedRefinements, createdWorkItemIds, lease);
918
1062
  };
1063
+ /** Release a variant bridge lease after tracked generated work has finished. */
1064
+ const ackGeneratedVariantBridgeLease = (args) => {
1065
+ const result = (0, exports.ackVariantBridgeLease)({ bridge, telemetry, ...args });
1066
+ if (!result.ok) {
1067
+ log.warn(`Failed to release generated variant bridge lease ${args.leaseId}: ${result.reason}`);
1068
+ }
1069
+ };
919
1070
  /** Complete an apply lease and refresh git status in the Rivet UI. */
920
1071
  const handleApplyAck = (sessionId, args) => {
921
1072
  const leaseId = args.leaseId ?? bridge.findActiveApplyLease()?.leaseId ?? undefined;
@@ -933,11 +1084,20 @@ const createRivetMcp = (options) => {
933
1084
  };
934
1085
  /** Release a variant lease after the agent finishes the batch. */
935
1086
  const handleVariantAck = (args) => {
936
- (0, exports.ackVariantBridgeLease)({ bridge, telemetry, ...args });
1087
+ const manualAckBlock = variantRequestLeaseTracker.getManualAckBlock(args);
1088
+ if (manualAckBlock) {
1089
+ return (0, exports.buildVariantManualAckBlockedResponse)(manualAckBlock);
1090
+ }
1091
+ const result = (0, exports.ackVariantBridgeLease)({ bridge, telemetry, ...args });
1092
+ if (!result.ok) {
1093
+ return (0, exports.buildVariantAckFailedResponse)(result);
1094
+ }
1095
+ return null;
937
1096
  };
938
1097
  const variantRequestLeaseTracker = (0, exports.createVariantRequestLeaseTracker)({
939
1098
  leaseRequest: (requestId) => bridge.leaseBatch(requestId),
940
- ackRequest: handleVariantAck,
1099
+ ackRequest: ackGeneratedVariantBridgeLease,
1100
+ activeVariantLeases: () => getActiveLeaseMetadata(bridge),
941
1101
  });
942
1102
  /** Complete the active run correlation and refresh git status in the Rivet UI. */
943
1103
  const handleRefreshGit = (sessionId) => {
@@ -1178,6 +1338,71 @@ const createRivetMcp = (options) => {
1178
1338
  await shutdown();
1179
1339
  });
1180
1340
  }
1341
+ /** Returns an attachment for the current visual editor session, if one exists. */
1342
+ const attachActiveVisualEditor = async (projectPath) => {
1343
+ if (!bridge.isActive()) {
1344
+ return null;
1345
+ }
1346
+ if (!rivetServerUrl || rivetServerPort === null) {
1347
+ throw new Error('Rivet visual editor session is active but URL metadata is unavailable');
1348
+ }
1349
+ const activeEditorSessionId = bridge.getSessionId();
1350
+ if (!activeEditorSessionId) {
1351
+ throw new Error('Rivet visual editor session is active but has no session ID');
1352
+ }
1353
+ const projectAction = (0, exports.getActiveVisualEditorProjectAction)({
1354
+ activeProjectPath: bridge.getProjectPath(),
1355
+ requestedProjectPath: projectPath,
1356
+ devServerOwnership,
1357
+ });
1358
+ if (projectAction === 'restart') {
1359
+ await cleanupSessionResources({ trackSessionEnd: false });
1360
+ return null;
1361
+ }
1362
+ if (projectAction === 'rebind') {
1363
+ bridge.rebindProjectPath(projectPath);
1364
+ }
1365
+ // Don't spawn a duplicate tab when a Rivet page is already open. The
1366
+ // variants flow re-enters this path on every propose/start call; calling
1367
+ // `open()` again opens a fresh tab in most browsers rather than focusing
1368
+ // the existing one. Only (re)open when no UI client is connected — i.e.
1369
+ // the user closed the page and needs it back.
1370
+ if (!agentVariantsOrchestrator.hasUiClients()) {
1371
+ try {
1372
+ await (0, open_1.default)(rivetServerUrl);
1373
+ }
1374
+ catch {
1375
+ log.warn('Failed to focus existing Rivet browser automatically');
1376
+ }
1377
+ }
1378
+ return {
1379
+ sessionId: activeEditorSessionId,
1380
+ url: rivetServerUrl,
1381
+ rivetPort: rivetServerPort,
1382
+ opened: false,
1383
+ attached: true,
1384
+ };
1385
+ };
1386
+ /** Reads feature flags for a visual editor session without blocking startup on flag failures. */
1387
+ const resolveSessionGitEnabled = async () => {
1388
+ try {
1389
+ const featureFlags = await (0, evaluateFlags_1.getFeatureFlags)();
1390
+ return featureFlags.gitUi.isEnabled;
1391
+ }
1392
+ catch (error) {
1393
+ log.warn('Failed to evaluate feature flags for MCP session startup; defaulting git to enabled:', error);
1394
+ return true;
1395
+ }
1396
+ };
1397
+ /** Writes editor-specific guidance once a visual editor session is ready. */
1398
+ const writeEditorGuidance = (projectPath) => {
1399
+ if (mcpEditor === 'cursor') {
1400
+ (0, skillWriter_1.writeCursorRulesIfNeeded)(projectPath);
1401
+ }
1402
+ else if (mcpEditor === 'claude' || mcpEditor === undefined) {
1403
+ (0, skillWriter_1.writeSkillFileIfNeeded)();
1404
+ }
1405
+ };
1181
1406
  /**
1182
1407
  * Ensure zero-to-one variant sessions have a Rivet UI shell subscribed to
1183
1408
  * the shared variants orchestrator. Fresh projects do not have a base app
@@ -1188,38 +1413,17 @@ const createRivetMcp = (options) => {
1188
1413
  // The bridge tracks the generated project folder. That keeps /api/config,
1189
1414
  // history reads, and the committed preview focused on the same folder.
1190
1415
  const { projectPath } = input;
1191
- if (bridge.isActive()) {
1192
- if (!rivetServerUrl || rivetServerPort === null) {
1193
- throw new Error('Rivet visual editor session is active but URL metadata is unavailable');
1194
- }
1195
- const activeEditorSessionId = bridge.getSessionId();
1196
- if (!activeEditorSessionId) {
1197
- throw new Error('Rivet visual editor session is active but has no session ID');
1198
- }
1199
- if (bridge.getProjectPath() !== projectPath) {
1200
- bridge.rebindProjectPath(projectPath);
1201
- }
1202
- // Don't spawn a duplicate tab when a Rivet page is already open. The
1203
- // variants flow re-enters this path on every propose/start call; calling
1204
- // `open()` again opens a fresh tab in most browsers rather than focusing
1205
- // the existing one. Only (re)open when no UI client is connected — i.e.
1206
- // the user closed the page and needs it back.
1207
- if (!agentVariantsOrchestrator.hasUiClients()) {
1208
- try {
1209
- await (0, open_1.default)(rivetServerUrl);
1210
- }
1211
- catch {
1212
- log.warn('Failed to focus existing Rivet browser automatically');
1213
- }
1214
- }
1215
- return {
1216
- sessionId: activeEditorSessionId,
1217
- url: rivetServerUrl,
1218
- rivetPort: rivetServerPort,
1219
- opened: false,
1220
- attached: true,
1221
- };
1222
- }
1416
+ const activeAttachment = await attachActiveVisualEditor(projectPath);
1417
+ if (activeAttachment) {
1418
+ return activeAttachment;
1419
+ }
1420
+ await (0, exports.closeDetachedVisualEditorServer)({
1421
+ isBridgeActive: bridge.isActive(),
1422
+ serverHandle,
1423
+ cleanup: async () => {
1424
+ await cleanupSessionResources({ trackSessionEnd: false });
1425
+ },
1426
+ });
1223
1427
  let nextRivetPort;
1224
1428
  try {
1225
1429
  nextRivetPort = await (0, portUtils_1.findAvailablePort)(index_1.DEFAULT_PORT);
@@ -1282,12 +1486,210 @@ const createRivetMcp = (options) => {
1282
1486
  // of through the open_visual_editor tool, so without this the
1283
1487
  // SKILL.md / rivet.mdc file would never be rewritten when only
1284
1488
  // start_variants is used.
1285
- if (mcpEditor === 'cursor') {
1286
- (0, skillWriter_1.writeCursorRulesIfNeeded)(input.projectPath);
1489
+ writeEditorGuidance(input.projectPath);
1490
+ const url = `http://localhost:${nextRivetPort}/rivet`;
1491
+ rivetServerPort = nextRivetPort;
1492
+ rivetServerUrl = url;
1493
+ try {
1494
+ await (0, open_1.default)(url);
1287
1495
  }
1288
- else if (mcpEditor === 'claude' || mcpEditor === undefined) {
1289
- (0, skillWriter_1.writeSkillFileIfNeeded)();
1496
+ catch {
1497
+ log.warn('Failed to open browser automatically');
1290
1498
  }
1499
+ return {
1500
+ sessionId: editorSessionId,
1501
+ url,
1502
+ rivetPort: nextRivetPort,
1503
+ opened: true,
1504
+ attached: false,
1505
+ };
1506
+ };
1507
+ /**
1508
+ * Ensure existing-project variant sessions have the base app mounted as the
1509
+ * default preview upstream before variant preview targets are selected.
1510
+ */
1511
+ const ensureExistingVisualEditorOpen = async (input) => {
1512
+ const { projectPath } = input;
1513
+ const activeAttachment = await attachActiveVisualEditor(projectPath);
1514
+ if (activeAttachment &&
1515
+ (0, exports.canReuseExistingProjectVisualEditor)({
1516
+ attachment: activeAttachment,
1517
+ devServerOwnership,
1518
+ })) {
1519
+ return activeAttachment;
1520
+ }
1521
+ if (activeAttachment) {
1522
+ await cleanupSessionResources({ trackSessionEnd: false });
1523
+ }
1524
+ await (0, exports.closeDetachedVisualEditorServer)({
1525
+ isBridgeActive: bridge.isActive(),
1526
+ serverHandle,
1527
+ cleanup: async () => {
1528
+ await cleanupSessionResources({ trackSessionEnd: false });
1529
+ },
1530
+ });
1531
+ const detection = new index_core_1.ProjectDetectionService();
1532
+ let nextDevServerPort = null;
1533
+ let nextDevServerHost = 'localhost';
1534
+ let nextDevServerOwnership = 'none';
1535
+ let staticTarget = null;
1536
+ if (input.framework === 'static') {
1537
+ staticTarget = (0, staticProjectTarget_1.resolveStaticProjectTarget)({ targetPath: projectPath });
1538
+ if ((0, staticProjectTarget_1.needsStaticEntryPath)(staticTarget)) {
1539
+ throw new Error('Static folder has multiple HTML files and no index.html. Open the visual editor with a specific entry path before starting variants.');
1540
+ }
1541
+ const entryFullPath = path_1.default.join(projectPath, staticTarget.entryPath);
1542
+ const didWriteStarter = staticTarget.htmlEntryFiles.length === 0
1543
+ ? (0, staticStarter_1.ensureStaticStarterCanvas)(projectPath)
1544
+ : false;
1545
+ const hasStaticEntry = fs_1.default.existsSync(entryFullPath) ||
1546
+ (didWriteStarter && staticTarget.entryPath === 'index.html');
1547
+ if (hasStaticEntry) {
1548
+ nextDevServerHost = '127.0.0.1';
1549
+ nextDevServerPort = await (0, portUtils_1.findAvailablePort)(index_1.DEFAULT_USER_PORT);
1550
+ devServerProcess = await (0, StaticPreviewServer_1.startStaticPreviewServer)({
1551
+ rootPath: projectPath,
1552
+ port: nextDevServerPort,
1553
+ host: nextDevServerHost,
1554
+ });
1555
+ nextDevServerOwnership = 'rivet_owned';
1556
+ devServerProcess.on('exit', (code) => {
1557
+ log.info(`Static preview server exited with code ${code}`);
1558
+ devServerProcess = null;
1559
+ if (bridge.isActive()) {
1560
+ updateBridgeHealth('upstream_unreachable', false);
1561
+ }
1562
+ });
1563
+ updateBridgeHealth('healthy', true);
1564
+ log.info(`Static preview server ready on ${nextDevServerHost}:${nextDevServerPort}`);
1565
+ }
1566
+ }
1567
+ else {
1568
+ const frameworkConfig = devServerCommand_1.FRAMEWORK_DEV_CONFIG[input.framework];
1569
+ const packageManager = await detection.detectPackageManager(projectPath);
1570
+ const preferredPort = detection.readConfiguredPort(projectPath) ?? frameworkConfig.defaultPort;
1571
+ nextDevServerPort = await (0, portUtils_1.findAvailablePort)(preferredPort);
1572
+ const { cmd, args: cmdArgs, env, } = (0, devServerCommand_1.buildDevServerCommand)(input.framework, packageManager, frameworkConfig.devCommand, nextDevServerPort);
1573
+ log.info(`Auto-starting dev server: ${cmd} ${cmdArgs.join(' ')} in ${projectPath}`);
1574
+ devServerProcess = (0, child_process_1.spawn)(cmd, cmdArgs, {
1575
+ cwd: projectPath,
1576
+ env: { ...process.env, ...env },
1577
+ stdio: 'pipe',
1578
+ detached: false,
1579
+ });
1580
+ nextDevServerOwnership = 'rivet_owned';
1581
+ devServerProcess.on('error', (err) => {
1582
+ log.warn('Dev server process error:', err);
1583
+ });
1584
+ devServerProcess.on('exit', (code) => {
1585
+ log.info(`Dev server exited with code ${code}`);
1586
+ devServerProcess = null;
1587
+ if (bridge.isActive()) {
1588
+ updateBridgeHealth('upstream_unreachable', false);
1589
+ }
1590
+ });
1591
+ devServerProcess.stdout?.on('data', (data) => {
1592
+ log.debug(`[dev-server] ${data.toString().trim()}`);
1593
+ });
1594
+ devServerProcess.stderr?.on('data', (data) => {
1595
+ log.debug(`[dev-server] ${data.toString().trim()}`);
1596
+ });
1597
+ const ready = await waitForPort(nextDevServerPort, nextDevServerHost, 30_000);
1598
+ if (!ready) {
1599
+ if (devServerProcess) {
1600
+ await stopChildProcess(devServerProcess);
1601
+ }
1602
+ devServerProcess = null;
1603
+ throw new Error(`Dev server did not become ready on port ${nextDevServerPort} within 30s.`);
1604
+ }
1605
+ const startupValidation = await validateProjectServer({
1606
+ host: nextDevServerHost,
1607
+ port: nextDevServerPort,
1608
+ framework: input.framework,
1609
+ projectPath,
1610
+ });
1611
+ if (!startupValidation.valid) {
1612
+ if (devServerProcess) {
1613
+ await stopChildProcess(devServerProcess);
1614
+ }
1615
+ devServerProcess = null;
1616
+ throw new Error(startupValidation.reason);
1617
+ }
1618
+ updateBridgeHealth('healthy', true);
1619
+ log.info(`Dev server ready on ${nextDevServerHost}:${nextDevServerPort}`);
1620
+ }
1621
+ let nextRivetPort;
1622
+ try {
1623
+ nextRivetPort = await (0, portUtils_1.findAvailablePort)(index_1.DEFAULT_PORT);
1624
+ }
1625
+ catch (error) {
1626
+ log.warn(`Port discovery failed for existing-project editor, falling back to ${index_1.DEFAULT_PORT}:`, error);
1627
+ nextRivetPort = index_1.DEFAULT_PORT;
1628
+ }
1629
+ let retries = 0;
1630
+ const maxRetries = 5;
1631
+ const isGitEnabledForSession = await resolveSessionGitEnabled();
1632
+ while (!serverHandle && retries < maxRetries) {
1633
+ try {
1634
+ serverHandle = await (0, server_1.startServer)({
1635
+ framework: input.framework,
1636
+ projectPath,
1637
+ staticEntry: staticTarget?.entryPath,
1638
+ userPort: nextDevServerPort ?? 0,
1639
+ userHost: nextDevServerHost,
1640
+ sessionBridge: bridge,
1641
+ agentVariantsOrchestrator,
1642
+ telemetry,
1643
+ mcpEditor,
1644
+ skipProcessHandlers: true,
1645
+ rivetPort: nextRivetPort,
1646
+ isGitEnabled: isGitEnabledForSession,
1647
+ });
1648
+ }
1649
+ catch (error) {
1650
+ if (error?.code !== 'EADDRINUSE') {
1651
+ if (devServerProcess) {
1652
+ await stopChildProcess(devServerProcess);
1653
+ }
1654
+ devServerProcess = null;
1655
+ throw error;
1656
+ }
1657
+ retries++;
1658
+ nextRivetPort = await (0, portUtils_1.findAvailablePort)(nextRivetPort + 1);
1659
+ log.debug(`Existing-project editor port in use, retrying with port ${nextRivetPort} (attempt ${retries}/${maxRetries})`);
1660
+ }
1661
+ }
1662
+ if (!serverHandle) {
1663
+ if (devServerProcess) {
1664
+ await stopChildProcess(devServerProcess);
1665
+ }
1666
+ devServerProcess = null;
1667
+ throw new Error(`Could not start Rivet server after ${maxRetries} attempts.`);
1668
+ }
1669
+ const editorSessionId = bridge.startSession(projectPath);
1670
+ sessionStartedAt = Date.now();
1671
+ devServerOwnership = nextDevServerOwnership;
1672
+ devServerHost = nextDevServerHost;
1673
+ devServerPort = nextDevServerPort;
1674
+ bridge.setDevServerContext({
1675
+ ownership: devServerOwnership,
1676
+ host: devServerHost,
1677
+ port: devServerPort,
1678
+ startedByRivet: devServerOwnership === 'rivet_owned',
1679
+ });
1680
+ if (devServerOwnership === 'none') {
1681
+ updateBridgeHealth('unknown', null);
1682
+ }
1683
+ else {
1684
+ updateBridgeHealth('healthy', true);
1685
+ }
1686
+ telemetry.trackMCPSessionStart({
1687
+ framework: input.framework,
1688
+ devServerStarted: devServerOwnership === 'rivet_owned',
1689
+ sessionId: editorSessionId,
1690
+ mcpEditor,
1691
+ });
1692
+ writeEditorGuidance(projectPath);
1291
1693
  const url = `http://localhost:${nextRivetPort}/rivet`;
1292
1694
  rivetServerPort = nextRivetPort;
1293
1695
  rivetServerUrl = url;
@@ -1854,7 +2256,7 @@ const createRivetMcp = (options) => {
1854
2256
  ],
1855
2257
  };
1856
2258
  });
1857
- guardedMcp.tool('watch_for_changes', `Required visual-editor watch loop. Call immediately after a successful open_visual_editor response, or after a start_variants response that includes editorNextAction/visualEditor. It blocks until the user sends a change batch, then returns a leased payload with leaseId and kind. After editing files (apply) call watch_for_changes again with refresh_git: true and the leaseId to release the lease and refresh git. After variant batches call watch_for_changes with ack_batch: true and the leaseId. If it times out (hasChanges: false), call it again to keep waiting.`, {
2259
+ guardedMcp.tool('watch_for_changes', `Required visual-editor watch loop. Call immediately after a successful open_visual_editor response, or after a start_variants response that includes editorNextAction/visualEditor. It blocks until the user sends a change batch, then returns a leased payload with leaseId and kind. After editing files (apply), call watch_for_changes again with refresh_git: true and the leaseId to release the apply lease and refresh git. For UI-originated variant_request/static_preview_refine batches, lease the exact workItemIds and finish generated work with report_variant_complete; do not use ack_batch to skip or finish generated variant work. If it times out (hasChanges: false), call it again to keep waiting.`, {
1858
2260
  sessionId: v3_1.z
1859
2261
  .string()
1860
2262
  .describe('The session ID returned by open_visual_editor'),
@@ -1875,7 +2277,7 @@ const createRivetMcp = (options) => {
1875
2277
  ack_batch: v3_1.z
1876
2278
  .boolean()
1877
2279
  .optional()
1878
- .describe('After completing a variant lease: set true with leaseId to release the leased batch'),
2280
+ .describe('Only for variant leases that are not tied to generated work items. UI-originated variant_request/static_preview_refine work is released by report_variant_complete; unclaimed or unfinished generated work rejects ack_batch.'),
1879
2281
  leaseId: v3_1.z
1880
2282
  .string()
1881
2283
  .optional()
@@ -1907,10 +2309,13 @@ const createRivetMcp = (options) => {
1907
2309
  });
1908
2310
  }
1909
2311
  if (args.ack_batch) {
1910
- handleVariantAck({
2312
+ const ackError = handleVariantAck({
1911
2313
  leaseId: args.leaseId,
1912
2314
  requestId: args.requestId,
1913
2315
  });
2316
+ if (ackError) {
2317
+ return ackError;
2318
+ }
1914
2319
  }
1915
2320
  // `handleApplyAck` sets READY so the UI can clear comment markers. Do not
1916
2321
  // overwrite it synchronously — the UI polls `/api/mcp/status` and needs
@@ -1942,7 +2347,7 @@ const createRivetMcp = (options) => {
1942
2347
  activeLeases: getActiveLeaseMetadata(bridge),
1943
2348
  });
1944
2349
  });
1945
- guardedMcp.tool('get_pending_changes', `Pick up pending design changes from the Rivet visual editor. Returns a leased batch with leaseId and kind when work is queued. After editing files (apply), call again with refresh_git: true and leaseId to ack and refresh git. After variant batches, call with ack_batch: true and leaseId. Returns hasChanges: false if no changes are queued yet.`, {
2350
+ guardedMcp.tool('get_pending_changes', `Pick up pending design changes from the Rivet visual editor. Returns a leased batch with leaseId and kind when work is queued. After editing files (apply), call again with refresh_git: true and leaseId to ack and refresh git. For UI-originated variant_request/static_preview_refine batches, lease the exact workItemIds and finish generated work with report_variant_complete; do not use ack_batch to skip or finish generated variant work. Returns hasChanges: false if no changes are queued yet.`, {
1946
2351
  sessionId: v3_1.z
1947
2352
  .string()
1948
2353
  .describe('The session ID returned by open_visual_editor'),
@@ -1957,7 +2362,7 @@ const createRivetMcp = (options) => {
1957
2362
  ack_batch: v3_1.z
1958
2363
  .boolean()
1959
2364
  .optional()
1960
- .describe('After completing a variant lease: set true with leaseId to release the leased batch'),
2365
+ .describe('Only for variant leases that are not tied to generated work items. UI-originated variant_request/static_preview_refine work is released by report_variant_complete; unclaimed or unfinished generated work rejects ack_batch.'),
1961
2366
  leaseId: v3_1.z
1962
2367
  .string()
1963
2368
  .optional()
@@ -1988,10 +2393,13 @@ const createRivetMcp = (options) => {
1988
2393
  });
1989
2394
  }
1990
2395
  if (args.ack_batch) {
1991
- handleVariantAck({
2396
+ const ackError = handleVariantAck({
1992
2397
  leaseId: args.leaseId,
1993
2398
  requestId: args.requestId,
1994
2399
  });
2400
+ if (ackError) {
2401
+ return ackError;
2402
+ }
1995
2403
  }
1996
2404
  const routed = leaseAndRouteBatch(args.sessionId, (0, exports.resolveLeasePickupRequestId)(args));
1997
2405
  if (routed) {
@@ -2035,6 +2443,7 @@ const createRivetMcp = (options) => {
2035
2443
  orchestrator: agentVariantsOrchestrator,
2036
2444
  leaseOwner: () => 'mcp-server',
2037
2445
  ensureVisualEditorOpen: ensureZeroToOneVisualEditorOpen,
2446
+ ensureExistingVisualEditorOpen,
2038
2447
  getProjectPath: () => bridge.getProjectPath(),
2039
2448
  resolveVariantRequestProjectPath: (requestId) => bridge.resolveVariantRequestProjectPath(requestId),
2040
2449
  pendingVariantRequestCount: () => bridge.pendingVariantRequestCount(),