rivet-design 0.9.4 → 0.9.5

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 (55) hide show
  1. package/dist/mcp/agent-variants/SessionStore.d.ts +1 -0
  2. package/dist/mcp/agent-variants/SessionStore.d.ts.map +1 -1
  3. package/dist/mcp/agent-variants/SessionStore.js +3 -0
  4. package/dist/mcp/agent-variants/SessionStore.js.map +1 -1
  5. package/dist/mcp/agent-variants/WorktreeOrchestrator.d.ts +66 -3
  6. package/dist/mcp/agent-variants/WorktreeOrchestrator.d.ts.map +1 -1
  7. package/dist/mcp/agent-variants/WorktreeOrchestrator.js +369 -106
  8. package/dist/mcp/agent-variants/WorktreeOrchestrator.js.map +1 -1
  9. package/dist/mcp/agent-variants/contracts.d.ts +106 -0
  10. package/dist/mcp/agent-variants/contracts.d.ts.map +1 -1
  11. package/dist/mcp/agent-variants/contracts.js +30 -1
  12. package/dist/mcp/agent-variants/contracts.js.map +1 -1
  13. package/dist/mcp/agent-variants/createZeroToOneTool.d.ts +8 -2
  14. package/dist/mcp/agent-variants/createZeroToOneTool.d.ts.map +1 -1
  15. package/dist/mcp/agent-variants/createZeroToOneTool.js +24 -8
  16. package/dist/mcp/agent-variants/createZeroToOneTool.js.map +1 -1
  17. package/dist/mcp/agent-variants/tools.d.ts +17 -2
  18. package/dist/mcp/agent-variants/tools.d.ts.map +1 -1
  19. package/dist/mcp/agent-variants/tools.js +122 -15
  20. package/dist/mcp/agent-variants/tools.js.map +1 -1
  21. package/dist/mcp/server.d.ts.map +1 -1
  22. package/dist/mcp/server.js +13 -7
  23. package/dist/mcp/server.js.map +1 -1
  24. package/dist/prompts/agentModPrompts.d.ts.map +1 -1
  25. package/dist/prompts/agentModPrompts.js +9 -8
  26. package/dist/prompts/agentModPrompts.js.map +1 -1
  27. package/dist/proxy-middleware/proxy-config.d.ts +2 -2
  28. package/dist/proxy-middleware/proxy-config.d.ts.map +1 -1
  29. package/dist/proxy-middleware/proxy-config.js +66 -22
  30. package/dist/proxy-middleware/proxy-config.js.map +1 -1
  31. package/dist/routes/agentVariants.d.ts +2 -13
  32. package/dist/routes/agentVariants.d.ts.map +1 -1
  33. package/dist/routes/agentVariants.js +148 -1
  34. package/dist/routes/agentVariants.js.map +1 -1
  35. package/dist/server.d.ts.map +1 -1
  36. package/dist/server.js +58 -1
  37. package/dist/server.js.map +1 -1
  38. package/dist/services/ProjectDetectionService.d.ts.map +1 -1
  39. package/dist/services/ProjectDetectionService.js +12 -0
  40. package/dist/services/ProjectDetectionService.js.map +1 -1
  41. package/dist/services/VariantHistoryService.d.ts +117 -0
  42. package/dist/services/VariantHistoryService.d.ts.map +1 -0
  43. package/dist/services/VariantHistoryService.js +385 -0
  44. package/dist/services/VariantHistoryService.js.map +1 -0
  45. package/dist/services/agent/AgentCore.d.ts +1 -1
  46. package/dist/services/agent/AgentCore.d.ts.map +1 -1
  47. package/dist/services/agent/AgentCore.js +24 -1
  48. package/dist/services/agent/AgentCore.js.map +1 -1
  49. package/dist/services/agent/AgentModService.d.ts +1 -1
  50. package/dist/services/agent/AgentModService.js +1 -1
  51. package/package.json +2 -2
  52. package/src/ui/dist/assets/main-OdmwI8Od.css +1 -0
  53. package/src/ui/dist/assets/{main-CpX7fB64.js → main-SuZlKEi0.js} +41 -41
  54. package/src/ui/dist/index.html +2 -2
  55. package/src/ui/dist/assets/main-Qqe2_oMT.css +0 -1
@@ -4,9 +4,11 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
4
4
  };
5
5
  Object.defineProperty(exports, "__esModule", { value: true });
6
6
  exports.AgentVariantsOrchestrator = void 0;
7
+ exports.buildStaticPreviewDocument = buildStaticPreviewDocument;
7
8
  const crypto_1 = require("crypto");
8
9
  const events_1 = require("events");
9
10
  const fs_1 = __importDefault(require("fs"));
11
+ const os_1 = __importDefault(require("os"));
10
12
  const path_1 = __importDefault(require("path"));
11
13
  const child_process_1 = require("child_process");
12
14
  const simple_git_1 = require("simple-git");
@@ -17,6 +19,7 @@ const contracts_1 = require("./contracts");
17
19
  const viteReactTs_1 = require("../../services/templates/viteReactTs");
18
20
  const designCatalog_1 = require("../../services/templates/designCatalog");
19
21
  const previewQa_1 = require("./previewQa");
22
+ const VariantHistoryService_1 = require("../../services/VariantHistoryService");
20
23
  const log = (0, logger_1.createLogger)('AgentVariantsOrchestrator');
21
24
  const FRESH_DEV_SERVER_HOST = '127.0.0.1';
22
25
  /**
@@ -209,6 +212,7 @@ class AgentVariantsOrchestrator {
209
212
  materializeProject;
210
213
  previewQaRunner;
211
214
  switchPreviewPort;
215
+ variantHistory;
212
216
  resources = new Map();
213
217
  /**
214
218
  * Committed dev servers from prior sessions that survived teardown. The
@@ -248,6 +252,7 @@ class AgentVariantsOrchestrator {
248
252
  deps.materializeProject ?? defaultMaterializeProject;
249
253
  this.previewQaRunner = deps.previewQaRunner ?? defaultPreviewQaRunner;
250
254
  this.switchPreviewPort = deps.switchPreviewPort;
255
+ this.variantHistory = deps.variantHistory ?? new VariantHistoryService_1.VariantHistoryService();
251
256
  }
252
257
  // --- Pure delegations (no side effects) ---------------------------------
253
258
  propose(args) {
@@ -736,6 +741,9 @@ class AgentVariantsOrchestrator {
736
741
  fromStage: stageBefore,
737
742
  });
738
743
  this.emitChange();
744
+ void this.markPersistedVariantsCancelled(args.sessionId).catch((err) => {
745
+ log.warn(`markPersistedVariantsCancelled failed for ${args.sessionId}`, err);
746
+ });
739
747
  void this.teardownSession(args.sessionId, 'cancel').catch((err) => {
740
748
  log.error(`teardownSession failed for ${args.sessionId}`, err);
741
749
  });
@@ -763,6 +771,11 @@ class AgentVariantsOrchestrator {
763
771
  alreadyTerminal: result.alreadyTerminal,
764
772
  reason: args.reason ?? null,
765
773
  });
774
+ if (!result.alreadyTerminal) {
775
+ void this.markPersistedVariantCancelled(args.sessionId, args.variantId).catch((err) => {
776
+ log.warn(`markPersistedVariantCancelled failed for ${args.sessionId}/${args.variantId}`, err);
777
+ });
778
+ }
766
779
  this.emitChange();
767
780
  return {
768
781
  sessionId: args.sessionId,
@@ -808,6 +821,38 @@ class AgentVariantsOrchestrator {
808
821
  // without going back through resources (which may have been torn down).
809
822
  const existingPick = this.store.getVariantPick(args.sessionId);
810
823
  if (existingPick && existingPick.variantId === args.variantId) {
824
+ // Retry-safe history flip. If the first commit attempt enqueued
825
+ // successfully but the history-persist task crashed (it's
826
+ // fire-and-forget — see persistVariantHistoryAtCommit call site),
827
+ // the chosen variant stays at `completed` instead of `committed`
828
+ // on disk. Re-run the flip on every duplicate attempt; the
829
+ // terminal-status guard in `markStatus` makes this safely
830
+ // idempotent.
831
+ const projectContext = this.store.getProjectContext(args.sessionId);
832
+ let historyProjectPath;
833
+ if (projectContext.kind === 'fresh') {
834
+ historyProjectPath = projectContext.workspaceRoot;
835
+ }
836
+ else {
837
+ try {
838
+ historyProjectPath =
839
+ (await this.resolveEnv(args.sessionId))?.projectPath;
840
+ }
841
+ catch {
842
+ historyProjectPath = undefined;
843
+ }
844
+ }
845
+ if (historyProjectPath) {
846
+ void this.persistVariantHistoryAtCommit({
847
+ sessionId: args.sessionId,
848
+ chosenVariantId: args.variantId,
849
+ projectPath: historyProjectPath,
850
+ projectKind: projectContext.kind,
851
+ destinationPath: existingPick.destinationPath ?? historyProjectPath,
852
+ }).catch((err) => {
853
+ log.warn(`persistVariantHistoryAtCommit (duplicate retry) failed for session ${args.sessionId}`, err);
854
+ });
855
+ }
811
856
  return {
812
857
  enqueued: false,
813
858
  duplicate: true,
@@ -831,16 +876,9 @@ class AgentVariantsOrchestrator {
831
876
  let payload;
832
877
  let envelopeDestination;
833
878
  let changedFilesCount;
834
- let freshVariantFolderName;
835
879
  if (projectContext.kind === 'fresh') {
836
880
  const destinationPath = projectContext.workspacePath;
837
881
  this.assertDestinationAvailable(destinationPath);
838
- const variantFolderName = this.getFreshVariantFolderName({
839
- sessionId: args.sessionId,
840
- variantId: args.variantId,
841
- variantName: input.briefLabel,
842
- });
843
- freshVariantFolderName = variantFolderName;
844
882
  const freshMode = projectContext.executionPlan?.mode === 'vite_app'
845
883
  ? 'vite_app'
846
884
  : 'static_preview';
@@ -950,11 +988,16 @@ class AgentVariantsOrchestrator {
950
988
  }
951
989
  else {
952
990
  // Static_preview: HTML is the entire deliverable. Write index.html.
991
+ // Prefer the in-memory record; fall back to the persisted history at
992
+ // `<workspaceRoot>/.rivet/variants/<sessionId>/<variantId>/files/index.html`
993
+ // so a process restart between report_variant_complete and commit
994
+ // doesn't strand the variant.
953
995
  const staticPreview = resources.staticPreviews.get(args.variantId);
954
- const persistedSnapshotIndexPath = (0, createProjectArtifacts_1.createProjectVariantIndexPath)(destinationPath, variantFolderName);
955
- const htmlFromSnapshot = fs_1.default.existsSync(persistedSnapshotIndexPath)
956
- ? fs_1.default.readFileSync(persistedSnapshotIndexPath, 'utf8')
957
- : null;
996
+ const htmlFromSnapshot = await this.variantHistory.readStaticPreview({
997
+ projectPath: projectContext.workspaceRoot,
998
+ sessionId: args.sessionId,
999
+ variantId: args.variantId,
1000
+ });
958
1001
  if (!staticPreview && !htmlFromSnapshot) {
959
1002
  throw new errors_1.AgentVariantsError('INVALID_STAGE_ACTION', `No static preview found for variant ${args.variantId} — wait for report_variant_complete(succeeded) first`);
960
1003
  }
@@ -1028,27 +1071,25 @@ class AgentVariantsOrchestrator {
1028
1071
  sessionId: args.sessionId,
1029
1072
  envelope,
1030
1073
  });
1031
- if (projectContext.kind === 'fresh') {
1032
- // Manifest persistence is bookkeeping for the .rivet/ artifacts and must
1033
- // never strand the session `recordVariantPick` above has already marked
1034
- // this variant as the selection, so a retry would short-circuit via the
1035
- // `duplicate: true` path and skip the enqueue below. Catch and log
1036
- // instead of letting a filesystem hiccup block the handoff to pending-
1037
- // changes.
1038
- try {
1039
- this.persistFreshSelectionMetadata({
1040
- sessionId: args.sessionId,
1041
- variantId: args.variantId,
1042
- variantName: input.briefLabel,
1043
- projectPath: projectContext.workspacePath,
1044
- variantFolderName: freshVariantFolderName ?? (0, createProjectArtifacts_1.createProjectVariantSlug)(input.briefLabel),
1045
- changedFilesCount,
1046
- });
1047
- }
1048
- catch (err) {
1049
- log.warn(`persistFreshSelectionMetadata failed for session ${args.sessionId} variant ${args.variantId} — continuing with enqueue`, err);
1050
- }
1051
- }
1074
+ // History: flip every persisted variant in this session to its terminal
1075
+ // status. Variants were persisted at code_gen success time; this only
1076
+ // patches the status (+ destinationPath on the chosen one). For fresh
1077
+ // sessions, history lives at `<workspaceRoot>/.rivet/variants/` the
1078
+ // user's working dir so all variants from any session in the workspace
1079
+ // accumulate in one place, not per-subproject. Best-effort, never blocks
1080
+ // the commit.
1081
+ const historyProjectPath = projectContext.kind === 'fresh'
1082
+ ? projectContext.workspaceRoot
1083
+ : envelopeDestination;
1084
+ void this.persistVariantHistoryAtCommit({
1085
+ sessionId: args.sessionId,
1086
+ chosenVariantId: args.variantId,
1087
+ projectPath: historyProjectPath,
1088
+ projectKind: projectContext.kind,
1089
+ destinationPath: envelopeDestination,
1090
+ }).catch((err) => {
1091
+ log.warn(`persistVariantHistoryAtCommit failed for session ${args.sessionId}`, err);
1092
+ });
1052
1093
  const enqueueResult = this.adapter.enqueue(envelope);
1053
1094
  resources.committedVariantIds.add(args.variantId);
1054
1095
  if (this.activeSessionId === args.sessionId) {
@@ -1090,12 +1131,17 @@ class AgentVariantsOrchestrator {
1090
1131
  /**
1091
1132
  * Ensure the user-facing destination path can receive the new project.
1092
1133
  * Rejects when the path exists and is non-empty.
1134
+ *
1135
+ * `.rivet/` (pre-commit snapshots + history manifests) and `.gitignore`
1136
+ * (written by VariantHistoryService.ensureGitignore when variants persist
1137
+ * at success time) are both tolerated — neither is user-authored content
1138
+ * that would be clobbered by the materialize step.
1093
1139
  */
1094
1140
  assertDestinationAvailable(destinationPath) {
1095
1141
  if (!fs_1.default.existsSync(destinationPath))
1096
1142
  return;
1097
1143
  const entries = fs_1.default.readdirSync(destinationPath);
1098
- const userVisibleEntries = entries.filter((entry) => entry !== '.rivet');
1144
+ const userVisibleEntries = entries.filter((entry) => entry !== '.rivet' && entry !== '.gitignore');
1099
1145
  if (userVisibleEntries.length === 0)
1100
1146
  return;
1101
1147
  throw new errors_1.AgentVariantsError('DESTINATION_NOT_EMPTY', `Destination ${destinationPath} is not empty (${userVisibleEntries.length} entries) — refuse to materialize.`);
@@ -1187,10 +1233,16 @@ class AgentVariantsOrchestrator {
1187
1233
  designContext: summarizeDesignContext(designContext),
1188
1234
  });
1189
1235
  log.info(`Provisioning ${codeGenIds.length} fresh worktree(s) for session ${sessionId}`);
1190
- // destinationParent for fresh worktrees: same parent the materialized
1191
- // project will land in. Keeping the worktree on the same volume turns
1192
- // commit into a directory rename instead of a recursive copy.
1193
- const destinationParent = path_1.default.dirname(projectContext.workspacePath);
1236
+ // destinationParent for fresh worktrees: the user's workspace root
1237
+ // (sibling to `.rivet/`). Keeping the worktree on the same volume as
1238
+ // the materialize destination turns commit into a directory rename
1239
+ // instead of a recursive copy. `path.dirname(workspacePath)` *used*
1240
+ // to equal `workspaceRoot`, but after nesting subprojects under
1241
+ // `<workspaceRoot>/.rivet/<slug>/` the dirname is now `.rivet/`,
1242
+ // which would stage worktrees inside `.rivet/.rivet-variants/`.
1243
+ // Use workspaceRoot directly so staging lives at
1244
+ // `<workspaceRoot>/.rivet-variants/` as originally intended.
1245
+ const destinationParent = projectContext.workspaceRoot;
1194
1246
  const paths = await createFresh.call(this.worktrees, sessionId, codeGenIds.length, viteReactTs_1.VITE_REACT_TS_TEMPLATE, designContext, sourceContext, destinationParent);
1195
1247
  resources.scaffoldBaseWorkItemId = scaffoldId;
1196
1248
  resources.freshDestinationParent = destinationParent;
@@ -1387,10 +1439,17 @@ class AgentVariantsOrchestrator {
1387
1439
  };
1388
1440
  resources.staticPreviews.set(workItemId, record);
1389
1441
  if (this.store.getProjectContext(sessionId).kind === 'fresh') {
1390
- this.persistFreshVariantArtifacts({
1442
+ // History at `<workspaceRoot>/.rivet/variants/<sessionId>/<variantId>/`
1443
+ // is the sole on-disk record. The legacy per-subproject snapshot
1444
+ // tree (`<slug>/.rivet/<variantName>/`) is no longer written —
1445
+ // it duplicated this data in a parallel layout and cluttered
1446
+ // `.rivet/` with slug-named directories before the user ever
1447
+ // committed.
1448
+ this.persistCompletedFreshVariant({
1391
1449
  sessionId,
1392
1450
  workItemId,
1393
- html: staticPreview.html,
1451
+ }).catch((err) => {
1452
+ log.warn(`persistCompletedFreshVariant failed for ${sessionId}/${workItemId}`, err);
1394
1453
  });
1395
1454
  }
1396
1455
  const leasedAt = resources.leasedAt.get(workItemId);
@@ -1424,6 +1483,31 @@ class AgentVariantsOrchestrator {
1424
1483
  catch (err) {
1425
1484
  log.warn(`getDiff failed for ${record.worktreePath}`, err);
1426
1485
  }
1486
+ // History: persist every completed variant immediately to
1487
+ // `<projectPath>/.rivet/variants/`. Existing projects pass the captured
1488
+ // diff (or an empty string when capture itself failed — the variant
1489
+ // still succeeded code-gen-wise, and the history row is the only
1490
+ // record the UI has). Fresh-project variants copy their worktree
1491
+ // (vite_app) or the staged HTML (static_preview). Best-effort — a
1492
+ // failure here must never block dev-server startup or the user's pick
1493
+ // flow.
1494
+ if (!isFresh) {
1495
+ this.persistCompletedExistingVariant({
1496
+ sessionId,
1497
+ workItemId,
1498
+ diff: record.diff ?? '',
1499
+ }).catch((err) => {
1500
+ log.warn(`persistCompletedExistingVariant failed for ${sessionId}/${workItemId}`, err);
1501
+ });
1502
+ }
1503
+ else {
1504
+ this.persistCompletedFreshVariant({
1505
+ sessionId,
1506
+ workItemId,
1507
+ }).catch((err) => {
1508
+ log.warn(`persistCompletedFreshVariant failed for ${sessionId}/${workItemId}`, err);
1509
+ });
1510
+ }
1427
1511
  // Bring up a dev server in the variant's worktree so the user can cycle
1428
1512
  // through live variants in the iframe via the chip. Failures here are
1429
1513
  // logged but non-fatal — the user can still pick by reading the diff.
@@ -1463,80 +1547,206 @@ class AgentVariantsOrchestrator {
1463
1547
  log.warn(`Failed to start dev server for variant ${workItemId}; live preview disabled for this variant`, err);
1464
1548
  }
1465
1549
  }
1466
- persistFreshVariantArtifacts(args) {
1550
+ /**
1551
+ * Persist a completed existing-project code_gen variant into
1552
+ * `<env.projectPath>/.rivet/variants/<sessionId>/<variantId>/`. Called from
1553
+ * `handleSucceededReport` once the worktree diff has been captured but
1554
+ * before the user picks. Status is `completed` until `commitVariant` or a
1555
+ * cancellation transitions it.
1556
+ */
1557
+ async persistCompletedExistingVariant(args) {
1467
1558
  const projectContext = this.store.getProjectContext(args.sessionId);
1468
- if (projectContext.kind !== 'fresh') {
1559
+ if (projectContext.kind !== 'existing')
1469
1560
  return;
1561
+ let projectPath;
1562
+ try {
1563
+ const env = await this.resolveEnv(args.sessionId);
1564
+ projectPath = env.projectPath;
1470
1565
  }
1471
- const projectPath = projectContext.workspacePath;
1472
- const now = new Date().toISOString();
1473
- const projectManifestPath = (0, createProjectArtifacts_1.createProjectManifestPath)(projectPath);
1474
- const existingProjectManifest = this.readManifest(projectManifestPath);
1475
- (0, createProjectArtifacts_1.writeCreateProjectManifestFile)(projectManifestPath, (0, createProjectArtifacts_1.createProjectManifest)({
1476
- createdAt: existingProjectManifest?.createdAt ?? now,
1477
- selectedDesignSlug: existingProjectManifest?.selectedDesignSlug,
1478
- latestVariantSessionId: args.sessionId,
1479
- selectedVariantId: existingProjectManifest?.selectedVariantId,
1480
- }));
1481
- const briefInput = this.store.getWorkItemInput(args.sessionId, args.workItemId);
1482
- const variantFolderName = this.getFreshVariantFolderName({
1483
- sessionId: args.sessionId,
1484
- variantId: args.workItemId,
1485
- variantName: briefInput.briefLabel,
1486
- });
1487
- const snapshotPath = (0, createProjectArtifacts_1.createProjectVariantSnapshotPath)(projectPath, variantFolderName);
1488
- fs_1.default.rmSync(snapshotPath, { recursive: true, force: true });
1489
- fs_1.default.mkdirSync(snapshotPath, { recursive: true });
1490
- const briefPath = (0, createProjectArtifacts_1.createProjectVariantBriefPath)(projectPath, variantFolderName);
1491
- fs_1.default.writeFileSync(briefPath, `# ${briefInput.briefLabel}\n\n${briefInput.briefBody}\n`, 'utf8');
1492
- const variantManifestPath = (0, createProjectArtifacts_1.createProjectVariantManifestPath)(projectPath, variantFolderName);
1493
- (0, createProjectArtifacts_1.writeCreateProjectManifestFile)(variantManifestPath, {
1494
- schemaVersion: createProjectArtifacts_1.CREATE_PROJECT_MANIFEST_SCHEMA_VERSION,
1495
- createdAt: now,
1566
+ catch (err) {
1567
+ log.warn(`persistCompletedExistingVariant: resolveEnv failed for ${args.sessionId}`, err);
1568
+ return;
1569
+ }
1570
+ const input = this.store.getWorkItemInput(args.sessionId, args.workItemId);
1571
+ const sessionPrompt = this.store.getPrompt(args.sessionId);
1572
+ await this.variantHistory.persistVariant({
1573
+ projectPath,
1496
1574
  sessionId: args.sessionId,
1497
1575
  variantId: args.workItemId,
1498
- variantName: briefInput.briefLabel,
1499
- variantFolderName,
1500
- changedFilesCount: 1,
1501
- });
1502
- fs_1.default.writeFileSync((0, createProjectArtifacts_1.createProjectVariantIndexPath)(projectPath, variantFolderName), args.html, 'utf8');
1503
- }
1504
- persistFreshSelectionMetadata(args) {
1505
- const now = new Date().toISOString();
1506
- const projectManifestPath = (0, createProjectArtifacts_1.createProjectManifestPath)(args.projectPath);
1507
- const existingProjectManifest = this.readManifest(projectManifestPath);
1508
- (0, createProjectArtifacts_1.writeCreateProjectManifestFile)(projectManifestPath, (0, createProjectArtifacts_1.createProjectManifest)({
1509
- createdAt: existingProjectManifest?.createdAt ?? now,
1510
- selectedDesignSlug: existingProjectManifest?.selectedDesignSlug,
1511
- latestVariantSessionId: args.sessionId,
1512
- selectedVariantId: args.variantId,
1513
- }));
1514
- const variantManifestPath = (0, createProjectArtifacts_1.createProjectVariantManifestPath)(args.projectPath, args.variantFolderName);
1515
- const existingVariantManifest = this.readManifest(variantManifestPath);
1516
- (0, createProjectArtifacts_1.writeCreateProjectManifestFile)(variantManifestPath, {
1517
- ...existingVariantManifest,
1518
- schemaVersion: createProjectArtifacts_1.CREATE_PROJECT_MANIFEST_SCHEMA_VERSION,
1519
- createdAt: existingVariantManifest?.createdAt ?? now,
1520
- sessionId: args.sessionId,
1521
- variantId: args.variantId,
1522
- variantName: args.variantName,
1523
- variantFolderName: args.variantFolderName,
1524
- changedFilesCount: args.changedFilesCount,
1525
- selectedVariantId: args.variantId,
1576
+ label: input.briefLabel,
1577
+ brief: input.briefBody,
1578
+ sessionPrompt,
1579
+ kind: 'diff',
1580
+ diff: args.diff,
1581
+ changedFilesCount: countDiffFiles(args.diff),
1582
+ projectKind: 'existing',
1526
1583
  });
1527
1584
  }
1528
- getFreshVariantFolderName(args) {
1529
- const baseSlug = (0, createProjectArtifacts_1.createProjectVariantSlug)(args.variantName);
1585
+ /**
1586
+ * Persist a completed fresh-project variant into
1587
+ * `<projectContext.workspacePath>/.rivet/variants/<sessionId>/<variantId>/`.
1588
+ * Called from `handleSucceededReport` right after the per-variant snapshot
1589
+ * lands on disk — for static_preview that's the `.rivet/<slug>/` snapshot
1590
+ * dir; for vite_app fresh variants it's the worktree itself. Status starts
1591
+ * as 'completed' and transitions to 'committed' / 'rejected' / 'cancelled'
1592
+ * via `markStatus` at commit or teardown time.
1593
+ *
1594
+ * Running at success time (rather than commit time) means the history panel
1595
+ * populates live as variants generate — including when the user never picks
1596
+ * one. Mirrors the existing-project persistCompletedExistingVariant flow.
1597
+ */
1598
+ async persistCompletedFreshVariant(args) {
1599
+ const projectContext = this.store.getProjectContext(args.sessionId);
1600
+ if (projectContext.kind !== 'fresh')
1601
+ return;
1602
+ // Variant history lives at the workspace root — the user's working
1603
+ // dir (e.g. `fable-eng-demo/`). All variants from any session in this
1604
+ // workspace accumulate at
1605
+ // `<workspaceRoot>/.rivet/variants/<sessionId>/<variantId>/`.
1606
+ const historyProjectPath = projectContext.workspaceRoot;
1607
+ const input = this.store.getWorkItemInput(args.sessionId, args.workItemId);
1608
+ const sessionPrompt = this.store.getPrompt(args.sessionId);
1609
+ const designArtifact = resolveDesignArtifact(input.designContextEntry);
1610
+ const resources = this.resources.get(args.sessionId);
1611
+ const worktreeRecord = resources?.worktrees.get(args.workItemId);
1612
+ const staticPreview = resources?.staticPreviews.get(args.workItemId);
1613
+ // Vite_app deliverables are full scaffolded worktrees — pass the worktree
1614
+ // directory as sourceDir and let copyDirFiltered handle it (excludes
1615
+ // node_modules, .rivet, etc). Static_preview deliverables are inline HTML
1616
+ // captured in `resources.staticPreviews`; stage them in a tmp dir so the
1617
+ // existing copy path works without poking the user's workspace.
1618
+ let sourceDir = null;
1619
+ let tmpStagingDir = null;
1620
+ if (worktreeRecord && fs_1.default.existsSync(worktreeRecord.worktreePath)) {
1621
+ sourceDir = worktreeRecord.worktreePath;
1622
+ }
1623
+ else if (staticPreview) {
1624
+ tmpStagingDir = fs_1.default.mkdtempSync(path_1.default.join(os_1.default.tmpdir(), `rivet-variant-${args.workItemId}-`));
1625
+ fs_1.default.writeFileSync(path_1.default.join(tmpStagingDir, 'index.html'), staticPreview.html, 'utf8');
1626
+ fs_1.default.writeFileSync(path_1.default.join(tmpStagingDir, 'brief.md'), `# ${input.briefLabel}\n\n${input.briefBody}\n`, 'utf8');
1627
+ sourceDir = tmpStagingDir;
1628
+ }
1629
+ if (!sourceDir) {
1630
+ log.warn(`persistCompletedFreshVariant: no source for ${args.workItemId} (session ${args.sessionId}); skipping`);
1631
+ return;
1632
+ }
1633
+ // For static_preview, only `index.html` is a real deliverable — `brief.md`
1634
+ // is implementation-detail staging that we copy alongside it. Hardcode
1635
+ // the count so the history row matches what `commit_variant` reports
1636
+ // (always 1) instead of double-counting the brief.
1637
+ const changedFilesCount = staticPreview
1638
+ ? 1
1639
+ : countWorktreeFiles(sourceDir);
1640
+ try {
1641
+ await this.variantHistory.persistVariant({
1642
+ projectPath: historyProjectPath,
1643
+ sessionId: args.sessionId,
1644
+ variantId: args.workItemId,
1645
+ label: input.briefLabel,
1646
+ brief: input.briefBody,
1647
+ sessionPrompt,
1648
+ kind: 'project-created',
1649
+ sourceDir,
1650
+ changedFilesCount,
1651
+ projectKind: 'fresh',
1652
+ designMarkdown: designArtifact?.markdown,
1653
+ designSource: designArtifact?.source,
1654
+ });
1655
+ }
1656
+ finally {
1657
+ if (tmpStagingDir) {
1658
+ try {
1659
+ fs_1.default.rmSync(tmpStagingDir, { recursive: true, force: true });
1660
+ }
1661
+ catch (err) {
1662
+ log.warn(`Failed to clean up variant staging dir ${tmpStagingDir}: ${err instanceof Error ? err.message : String(err)}`);
1663
+ }
1664
+ }
1665
+ }
1666
+ }
1667
+ /**
1668
+ * At commit time, flip persisted variant manifests to terminal statuses
1669
+ * ('committed' for the chosen, 'rejected' for the rest). Both existing- and
1670
+ * fresh-project variants have already been persisted at code_gen success
1671
+ * time, so this is a pure status update — no source-dir copy required.
1672
+ * Also records the destination path on the chosen variant so history can
1673
+ * surface "this variant became <path>".
1674
+ */
1675
+ async persistVariantHistoryAtCommit(args) {
1530
1676
  const variants = this.store.getVariants(args.sessionId);
1531
- const matchingVariants = variants.filter((variant) => {
1532
- const input = this.store.getWorkItemInput(args.sessionId, variant.workItemId);
1533
- return (0, createProjectArtifacts_1.createProjectVariantSlug)(input.briefLabel ?? '') === baseSlug;
1677
+ for (const variant of variants) {
1678
+ if (variant.status !== 'succeeded')
1679
+ continue;
1680
+ const workItemId = variant.workItemId;
1681
+ const isChosen = workItemId === args.chosenVariantId;
1682
+ const status = isChosen
1683
+ ? 'committed'
1684
+ : 'rejected';
1685
+ await this.variantHistory.markStatus({
1686
+ projectPath: args.projectPath,
1687
+ sessionId: args.sessionId,
1688
+ variantId: workItemId,
1689
+ status,
1690
+ destinationPath: isChosen ? args.destinationPath : undefined,
1691
+ });
1692
+ }
1693
+ }
1694
+ /**
1695
+ * Flip persisted variant manifests to status='cancelled' for every variant
1696
+ * in the session that was already snapshotted to disk. Fresh- and existing-
1697
+ * project variants both persist at success time now, so both need their
1698
+ * manifests flipped here. Best-effort — missing manifests are silently
1699
+ * skipped by VariantHistoryService.markStatus.
1700
+ */
1701
+ async markPersistedVariantsCancelled(sessionId) {
1702
+ if (!this.store.hasSession(sessionId))
1703
+ return;
1704
+ const projectPath = await this.resolveHistoryProjectPath(sessionId);
1705
+ if (!projectPath)
1706
+ return;
1707
+ const variants = this.store.getVariants(sessionId);
1708
+ for (const variant of variants) {
1709
+ await this.variantHistory.markStatus({
1710
+ projectPath,
1711
+ sessionId,
1712
+ variantId: variant.workItemId,
1713
+ status: 'cancelled',
1714
+ });
1715
+ }
1716
+ }
1717
+ async markPersistedVariantCancelled(sessionId, variantId) {
1718
+ if (!this.store.hasSession(sessionId))
1719
+ return;
1720
+ const projectPath = await this.resolveHistoryProjectPath(sessionId);
1721
+ if (!projectPath)
1722
+ return;
1723
+ await this.variantHistory.markStatus({
1724
+ projectPath,
1725
+ sessionId,
1726
+ variantId,
1727
+ status: 'cancelled',
1534
1728
  });
1535
- const index = matchingVariants.findIndex((variant) => variant.workItemId === args.variantId);
1536
- if (index <= 0) {
1537
- return baseSlug;
1729
+ }
1730
+ /**
1731
+ * Resolve the project path that owns `.rivet/variants/` for a session.
1732
+ * Existing sessions: the user's project (via `resolveEnv`). Fresh sessions:
1733
+ * the *workspace root*, which is the parent of `workspacePath` — variants
1734
+ * accumulate there across sessions instead of being scattered under each
1735
+ * subproject. Must match what `persistCompletedFreshVariant` writes to.
1736
+ */
1737
+ async resolveHistoryProjectPath(sessionId) {
1738
+ const projectContext = this.store.getProjectContext(sessionId);
1739
+ if (projectContext.kind === 'fresh') {
1740
+ return projectContext.workspaceRoot;
1741
+ }
1742
+ try {
1743
+ const env = await this.resolveEnv(sessionId);
1744
+ return env.projectPath;
1745
+ }
1746
+ catch (err) {
1747
+ log.warn(`resolveHistoryProjectPath: resolveEnv failed for ${sessionId}`, err);
1748
+ return null;
1538
1749
  }
1539
- return `${baseSlug}-${index + 1}`;
1540
1750
  }
1541
1751
  readManifest(manifestPath) {
1542
1752
  if (!fs_1.default.existsSync(manifestPath)) {
@@ -1995,11 +2205,33 @@ function parseStaticPreviewOutput(output) {
1995
2205
  };
1996
2206
  }
1997
2207
  function buildStaticPreviewDocument(input) {
1998
- if (/<!doctype html>|<html[\s>]/i.test(input.html)) {
1999
- return input.html;
2000
- }
2001
2208
  const style = input.css ? `<style>\n${input.css}\n</style>` : '';
2002
2209
  const script = input.js ? `<script>\n${input.js}\n</script>` : '';
2210
+ if (/<!doctype html>|<html[\s>]/i.test(input.html)) {
2211
+ // Full document: inject css before </head> and js before </body>. The
2212
+ // agent often passes a complete `<!doctype html>...` blob with css/js
2213
+ // alongside; without this they're silently dropped and the variant ships
2214
+ // unstyled and non-interactive. Falls back to appending to the end if
2215
+ // the closing tag isn't found.
2216
+ let doc = input.html;
2217
+ if (style) {
2218
+ if (/<\/head>/i.test(doc)) {
2219
+ doc = doc.replace(/<\/head>/i, () => `${style}\n</head>`);
2220
+ }
2221
+ else {
2222
+ doc += `\n${style}`;
2223
+ }
2224
+ }
2225
+ if (script) {
2226
+ if (/<\/body>/i.test(doc)) {
2227
+ doc = doc.replace(/<\/body>/i, () => `${script}\n</body>`);
2228
+ }
2229
+ else {
2230
+ doc += `\n${script}`;
2231
+ }
2232
+ }
2233
+ return doc;
2234
+ }
2003
2235
  return `<!doctype html>
2004
2236
  <html lang="en">
2005
2237
  <head>
@@ -2061,6 +2293,7 @@ const toActiveProjectContext = (projectContext) => {
2061
2293
  return {
2062
2294
  kind: 'fresh',
2063
2295
  workspacePath: projectContext.workspacePath,
2296
+ workspaceRoot: projectContext.workspaceRoot,
2064
2297
  framework: projectContext.framework,
2065
2298
  designContext: projectContext.designContext?.map((entry) => entry.kind === 'slug'
2066
2299
  ? { kind: 'slug', slug: entry.slug }
@@ -2161,6 +2394,36 @@ const addDesignContextArtifact = (artifactsByContent, artifact) => {
2161
2394
  usedByVariantCount: 1,
2162
2395
  });
2163
2396
  };
2397
+ /**
2398
+ * Resolve a per-variant design context entry into the raw DESIGN.md markdown
2399
+ * the worktree scaffolder writes, plus a small `designSource` descriptor for
2400
+ * the variant manifest. Slug entries resolve through the bundled catalog;
2401
+ * markdown entries (Agent Browser / inspiration extractor output) carry their
2402
+ * stored markdown verbatim. Returns null when the entry is missing or the
2403
+ * slug doesn't resolve to bundled markdown.
2404
+ */
2405
+ const resolveDesignArtifact = (entry) => {
2406
+ if (!entry)
2407
+ return null;
2408
+ if (entry.kind === 'markdown') {
2409
+ return {
2410
+ markdown: entry.content,
2411
+ source: { kind: 'markdown', label: entry.label },
2412
+ };
2413
+ }
2414
+ const markdown = (0, designCatalog_1.loadDesignSystemMarkdown)(entry.slug);
2415
+ if (!markdown)
2416
+ return null;
2417
+ const catalogEntry = (0, designCatalog_1.getDesignSystemBySlug)(entry.slug);
2418
+ return {
2419
+ markdown,
2420
+ source: {
2421
+ kind: 'slug',
2422
+ slug: entry.slug,
2423
+ label: catalogEntry?.name ?? entry.slug,
2424
+ },
2425
+ };
2426
+ };
2164
2427
  const summarizeDesignContext = (designContext) => {
2165
2428
  if (!designContext)
2166
2429
  return null;