rivet-design 0.9.4 → 0.9.6
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/dist/mcp/agent-variants/SessionStore.d.ts +11 -2
- package/dist/mcp/agent-variants/SessionStore.d.ts.map +1 -1
- package/dist/mcp/agent-variants/SessionStore.js +85 -22
- package/dist/mcp/agent-variants/SessionStore.js.map +1 -1
- package/dist/mcp/agent-variants/WorktreeOrchestrator.d.ts +70 -3
- package/dist/mcp/agent-variants/WorktreeOrchestrator.d.ts.map +1 -1
- package/dist/mcp/agent-variants/WorktreeOrchestrator.js +790 -132
- package/dist/mcp/agent-variants/WorktreeOrchestrator.js.map +1 -1
- package/dist/mcp/agent-variants/contracts.d.ts +495 -129
- package/dist/mcp/agent-variants/contracts.d.ts.map +1 -1
- package/dist/mcp/agent-variants/contracts.js +120 -37
- package/dist/mcp/agent-variants/contracts.js.map +1 -1
- package/dist/mcp/agent-variants/createZeroToOneTool.d.ts +40 -15
- package/dist/mcp/agent-variants/createZeroToOneTool.d.ts.map +1 -1
- package/dist/mcp/agent-variants/createZeroToOneTool.js +24 -8
- package/dist/mcp/agent-variants/createZeroToOneTool.js.map +1 -1
- package/dist/mcp/agent-variants/tools.d.ts +17 -2
- package/dist/mcp/agent-variants/tools.d.ts.map +1 -1
- package/dist/mcp/agent-variants/tools.js +129 -15
- package/dist/mcp/agent-variants/tools.js.map +1 -1
- package/dist/mcp/server.d.ts.map +1 -1
- package/dist/mcp/server.js +13 -7
- package/dist/mcp/server.js.map +1 -1
- package/dist/prompts/agentModPrompts.d.ts.map +1 -1
- package/dist/prompts/agentModPrompts.js +9 -8
- package/dist/prompts/agentModPrompts.js.map +1 -1
- package/dist/proxy-middleware/proxy-config.d.ts +2 -2
- package/dist/proxy-middleware/proxy-config.d.ts.map +1 -1
- package/dist/proxy-middleware/proxy-config.js +66 -22
- package/dist/proxy-middleware/proxy-config.js.map +1 -1
- package/dist/routes/agentVariants.d.ts +2 -13
- package/dist/routes/agentVariants.d.ts.map +1 -1
- package/dist/routes/agentVariants.js +233 -3
- package/dist/routes/agentVariants.js.map +1 -1
- package/dist/server.d.ts.map +1 -1
- package/dist/server.js +58 -1
- package/dist/server.js.map +1 -1
- package/dist/services/ProjectDetectionService.d.ts.map +1 -1
- package/dist/services/ProjectDetectionService.js +12 -0
- package/dist/services/ProjectDetectionService.js.map +1 -1
- package/dist/services/VariantHistoryService.d.ts +180 -0
- package/dist/services/VariantHistoryService.d.ts.map +1 -0
- package/dist/services/VariantHistoryService.js +515 -0
- package/dist/services/VariantHistoryService.js.map +1 -0
- package/dist/services/agent/AgentCore.d.ts +1 -1
- package/dist/services/agent/AgentCore.d.ts.map +1 -1
- package/dist/services/agent/AgentCore.js +24 -1
- package/dist/services/agent/AgentCore.js.map +1 -1
- package/dist/services/agent/AgentModService.d.ts +1 -1
- package/dist/services/agent/AgentModService.js +1 -1
- package/dist/utils/skills/claude-skill.d.ts +1 -1
- package/dist/utils/skills/claude-skill.js +1 -1
- package/dist/utils/skills/cursor-rules.d.ts +1 -1
- package/dist/utils/skills/cursor-rules.js +1 -1
- package/dist/utils/skills/describe-motion-protocol.d.ts +11 -0
- package/dist/utils/skills/describe-motion-protocol.d.ts.map +1 -0
- package/dist/utils/skills/describe-motion-protocol.js +216 -0
- package/dist/utils/skills/describe-motion-protocol.js.map +1 -0
- package/dist/utils/skills/shared-variants-protocol.d.ts.map +1 -1
- package/dist/utils/skills/shared-variants-protocol.js +23 -17
- package/dist/utils/skills/shared-variants-protocol.js.map +1 -1
- package/package.json +2 -2
- package/src/ui/dist/assets/main-BX1XfsOq.css +1 -0
- package/src/ui/dist/assets/{main-CpX7fB64.js → main-CO7W1r28.js} +38 -38
- package/src/ui/dist/index.html +2 -2
- 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,8 +19,11 @@ 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';
|
|
25
|
+
const DESIGN_CONTEXT_ROUTE_SEGMENT = 'design-md';
|
|
26
|
+
const DESIGN_CONTEXT_VIEW_SEGMENT = 'view';
|
|
22
27
|
/**
|
|
23
28
|
* Allowlist of asset file extensions an agent-planned source may have.
|
|
24
29
|
* `assetPlan` is sized for large local *assets* (3D models, images,
|
|
@@ -36,15 +41,36 @@ const FRESH_DEV_SERVER_HOST = '127.0.0.1';
|
|
|
36
41
|
*/
|
|
37
42
|
const ALLOWED_ASSET_EXTENSIONS = new Set([
|
|
38
43
|
// 3D / models
|
|
39
|
-
'.glb',
|
|
44
|
+
'.glb',
|
|
45
|
+
'.gltf',
|
|
46
|
+
'.obj',
|
|
47
|
+
'.fbx',
|
|
48
|
+
'.usdz',
|
|
40
49
|
// images
|
|
41
|
-
'.png',
|
|
50
|
+
'.png',
|
|
51
|
+
'.jpg',
|
|
52
|
+
'.jpeg',
|
|
53
|
+
'.gif',
|
|
54
|
+
'.webp',
|
|
55
|
+
'.svg',
|
|
56
|
+
'.avif',
|
|
57
|
+
'.bmp',
|
|
58
|
+
'.ico',
|
|
42
59
|
// video
|
|
43
|
-
'.mp4',
|
|
60
|
+
'.mp4',
|
|
61
|
+
'.webm',
|
|
62
|
+
'.mov',
|
|
44
63
|
// audio
|
|
45
|
-
'.mp3',
|
|
64
|
+
'.mp3',
|
|
65
|
+
'.wav',
|
|
66
|
+
'.ogg',
|
|
67
|
+
'.m4a',
|
|
46
68
|
// fonts
|
|
47
|
-
'.woff',
|
|
69
|
+
'.woff',
|
|
70
|
+
'.woff2',
|
|
71
|
+
'.ttf',
|
|
72
|
+
'.otf',
|
|
73
|
+
'.eot',
|
|
48
74
|
// PDFs
|
|
49
75
|
'.pdf',
|
|
50
76
|
]);
|
|
@@ -177,8 +203,7 @@ function copyAssetIntoWorktree(worktreePath, entry, assetSourceRoot) {
|
|
|
177
203
|
throw new errors_1.AgentVariantsError('RUNTIME_VALIDATION_FAILED', `assetPlan.source resolved extension '${resolvedExt || '(none)'}' is not on the allowlist (resolved from '${entry.source}').`);
|
|
178
204
|
}
|
|
179
205
|
const normalizedDest = path_1.default.normalize(entry.destination);
|
|
180
|
-
if (normalizedDest.startsWith('..') ||
|
|
181
|
-
path_1.default.isAbsolute(normalizedDest)) {
|
|
206
|
+
if (normalizedDest.startsWith('..') || path_1.default.isAbsolute(normalizedDest)) {
|
|
182
207
|
throw new errors_1.AgentVariantsError('RUNTIME_VALIDATION_FAILED', `assetPlan.destination must stay inside the worktree, got '${entry.destination}'`);
|
|
183
208
|
}
|
|
184
209
|
const absDest = path_1.default.join(worktreePath, normalizedDest);
|
|
@@ -209,6 +234,7 @@ class AgentVariantsOrchestrator {
|
|
|
209
234
|
materializeProject;
|
|
210
235
|
previewQaRunner;
|
|
211
236
|
switchPreviewPort;
|
|
237
|
+
variantHistory;
|
|
212
238
|
resources = new Map();
|
|
213
239
|
/**
|
|
214
240
|
* Committed dev servers from prior sessions that survived teardown. The
|
|
@@ -248,6 +274,7 @@ class AgentVariantsOrchestrator {
|
|
|
248
274
|
deps.materializeProject ?? defaultMaterializeProject;
|
|
249
275
|
this.previewQaRunner = deps.previewQaRunner ?? defaultPreviewQaRunner;
|
|
250
276
|
this.switchPreviewPort = deps.switchPreviewPort;
|
|
277
|
+
this.variantHistory = deps.variantHistory ?? new VariantHistoryService_1.VariantHistoryService();
|
|
251
278
|
}
|
|
252
279
|
// --- Pure delegations (no side effects) ---------------------------------
|
|
253
280
|
propose(args) {
|
|
@@ -301,8 +328,10 @@ class AgentVariantsOrchestrator {
|
|
|
301
328
|
const variants = this.getVariants(sessionId);
|
|
302
329
|
const sessionProjectContext = this.store.getProjectContext(sessionId);
|
|
303
330
|
const projectContext = toActiveProjectContext(sessionProjectContext);
|
|
304
|
-
const destinationPath = projectContext.kind === 'fresh'
|
|
305
|
-
|
|
331
|
+
const destinationPath = projectContext.kind === 'fresh'
|
|
332
|
+
? projectContext.workspacePath
|
|
333
|
+
: undefined;
|
|
334
|
+
const artifacts = buildSessionArtifacts(sessionId, sessionProjectContext);
|
|
306
335
|
return {
|
|
307
336
|
active: true,
|
|
308
337
|
sessionId,
|
|
@@ -424,7 +453,9 @@ class AgentVariantsOrchestrator {
|
|
|
424
453
|
};
|
|
425
454
|
}
|
|
426
455
|
}
|
|
427
|
-
catch {
|
|
456
|
+
catch {
|
|
457
|
+
/* work item may not exist in edge cases */
|
|
458
|
+
}
|
|
428
459
|
}
|
|
429
460
|
if (!preview && port) {
|
|
430
461
|
preview = { kind: 'dev_server', port };
|
|
@@ -435,7 +466,7 @@ class AgentVariantsOrchestrator {
|
|
|
435
466
|
const canView = Boolean(preview) || (isSucceeded && Boolean(port));
|
|
436
467
|
const canCommit = isSucceeded && !qaFailed;
|
|
437
468
|
const commitDisabledReason = qaFailed
|
|
438
|
-
? qa?.summary ?? 'Variant failed QA'
|
|
469
|
+
? (qa?.summary ?? 'Variant failed QA')
|
|
439
470
|
: 'Wait for a successful variant';
|
|
440
471
|
return {
|
|
441
472
|
...variant,
|
|
@@ -481,7 +512,9 @@ class AgentVariantsOrchestrator {
|
|
|
481
512
|
}
|
|
482
513
|
getStaticPreviewHtml(sessionId, workItemId) {
|
|
483
514
|
// Primary: from the staticPreviews Map populated by handleSucceededReport.
|
|
484
|
-
const fromMap = this.resources
|
|
515
|
+
const fromMap = this.resources
|
|
516
|
+
.get(sessionId)
|
|
517
|
+
?.staticPreviews.get(workItemId)?.html;
|
|
485
518
|
if (fromMap)
|
|
486
519
|
return fromMap;
|
|
487
520
|
// Fallback: read directly from the work item's stored output — available
|
|
@@ -496,6 +529,16 @@ class AgentVariantsOrchestrator {
|
|
|
496
529
|
return undefined;
|
|
497
530
|
}
|
|
498
531
|
}
|
|
532
|
+
/** Resolve the DESIGN.md markdown behind an artifact link. */
|
|
533
|
+
getDesignContextMarkdown(sessionId, artifactId) {
|
|
534
|
+
const artifact = findDesignContextArtifact(this.store.getProjectContext(sessionId), artifactId);
|
|
535
|
+
return artifact?.content;
|
|
536
|
+
}
|
|
537
|
+
/** Build the raw-plus-rendered DesignMD document for an artifact link. */
|
|
538
|
+
getDesignContextViewerHtml(sessionId, artifactId) {
|
|
539
|
+
const artifact = findDesignContextArtifact(this.store.getProjectContext(sessionId), artifactId);
|
|
540
|
+
return artifact ? buildDesignContextViewerDocument(artifact) : undefined;
|
|
541
|
+
}
|
|
499
542
|
getStaticPreviewByBriefId(sessionId, briefId) {
|
|
500
543
|
const resources = this.resources.get(sessionId);
|
|
501
544
|
if (!resources)
|
|
@@ -554,8 +597,12 @@ class AgentVariantsOrchestrator {
|
|
|
554
597
|
*/
|
|
555
598
|
async startUnified(args) {
|
|
556
599
|
const count = args.briefs?.length ?? args.count ?? 4;
|
|
557
|
-
const projectContext = args.projectContext ?? {
|
|
558
|
-
|
|
600
|
+
const projectContext = args.projectContext ?? {
|
|
601
|
+
kind: 'existing',
|
|
602
|
+
};
|
|
603
|
+
const sourceContext = projectContext.kind === 'fresh'
|
|
604
|
+
? projectContext.sourceContext
|
|
605
|
+
: undefined;
|
|
559
606
|
const isSourceGrounded = Boolean(sourceContext?.sourceUrls?.length) ||
|
|
560
607
|
Boolean(sourceContext?.sourceArtifacts?.length) ||
|
|
561
608
|
Boolean(sourceContext?.sourceIntent) ||
|
|
@@ -736,6 +783,9 @@ class AgentVariantsOrchestrator {
|
|
|
736
783
|
fromStage: stageBefore,
|
|
737
784
|
});
|
|
738
785
|
this.emitChange();
|
|
786
|
+
void this.markPersistedVariantsCancelled(args.sessionId).catch((err) => {
|
|
787
|
+
log.warn(`markPersistedVariantsCancelled failed for ${args.sessionId}`, err);
|
|
788
|
+
});
|
|
739
789
|
void this.teardownSession(args.sessionId, 'cancel').catch((err) => {
|
|
740
790
|
log.error(`teardownSession failed for ${args.sessionId}`, err);
|
|
741
791
|
});
|
|
@@ -763,6 +813,11 @@ class AgentVariantsOrchestrator {
|
|
|
763
813
|
alreadyTerminal: result.alreadyTerminal,
|
|
764
814
|
reason: args.reason ?? null,
|
|
765
815
|
});
|
|
816
|
+
if (!result.alreadyTerminal) {
|
|
817
|
+
void this.markPersistedVariantCancelled(args.sessionId, args.variantId).catch((err) => {
|
|
818
|
+
log.warn(`markPersistedVariantCancelled failed for ${args.sessionId}/${args.variantId}`, err);
|
|
819
|
+
});
|
|
820
|
+
}
|
|
766
821
|
this.emitChange();
|
|
767
822
|
return {
|
|
768
823
|
sessionId: args.sessionId,
|
|
@@ -808,6 +863,38 @@ class AgentVariantsOrchestrator {
|
|
|
808
863
|
// without going back through resources (which may have been torn down).
|
|
809
864
|
const existingPick = this.store.getVariantPick(args.sessionId);
|
|
810
865
|
if (existingPick && existingPick.variantId === args.variantId) {
|
|
866
|
+
// Retry-safe history flip. If the first commit attempt enqueued
|
|
867
|
+
// successfully but the history-persist task crashed (it's
|
|
868
|
+
// fire-and-forget — see persistVariantHistoryAtCommit call site),
|
|
869
|
+
// the chosen variant stays at `completed` instead of `committed`
|
|
870
|
+
// on disk. Re-run the flip on every duplicate attempt; the
|
|
871
|
+
// terminal-status guard in `markStatus` makes this safely
|
|
872
|
+
// idempotent.
|
|
873
|
+
const projectContext = this.store.getProjectContext(args.sessionId);
|
|
874
|
+
let historyProjectPath;
|
|
875
|
+
if (projectContext.kind === 'fresh') {
|
|
876
|
+
historyProjectPath = projectContext.workspaceRoot;
|
|
877
|
+
}
|
|
878
|
+
else {
|
|
879
|
+
try {
|
|
880
|
+
historyProjectPath = (await this.resolveEnv(args.sessionId))
|
|
881
|
+
?.projectPath;
|
|
882
|
+
}
|
|
883
|
+
catch {
|
|
884
|
+
historyProjectPath = undefined;
|
|
885
|
+
}
|
|
886
|
+
}
|
|
887
|
+
if (historyProjectPath) {
|
|
888
|
+
void this.persistVariantHistoryAtCommit({
|
|
889
|
+
sessionId: args.sessionId,
|
|
890
|
+
chosenVariantId: args.variantId,
|
|
891
|
+
projectPath: historyProjectPath,
|
|
892
|
+
projectKind: projectContext.kind,
|
|
893
|
+
destinationPath: existingPick.destinationPath ?? historyProjectPath,
|
|
894
|
+
}).catch((err) => {
|
|
895
|
+
log.warn(`persistVariantHistoryAtCommit (duplicate retry) failed for session ${args.sessionId}`, err);
|
|
896
|
+
});
|
|
897
|
+
}
|
|
811
898
|
return {
|
|
812
899
|
enqueued: false,
|
|
813
900
|
duplicate: true,
|
|
@@ -824,23 +911,18 @@ class AgentVariantsOrchestrator {
|
|
|
824
911
|
}
|
|
825
912
|
const variantSnapshot = this.getVariants(args.sessionId).find((variant) => variant.workItemId === args.variantId);
|
|
826
913
|
if (!variantSnapshot || variantSnapshot.actions?.commit?.enabled !== true) {
|
|
827
|
-
throw new errors_1.AgentVariantsError('INVALID_STAGE_ACTION', variantSnapshot?.actions?.commit?.reason ??
|
|
914
|
+
throw new errors_1.AgentVariantsError('INVALID_STAGE_ACTION', variantSnapshot?.actions?.commit?.reason ??
|
|
915
|
+
'Variant is not committable');
|
|
828
916
|
}
|
|
829
917
|
const input = this.store.getWorkItemInput(args.sessionId, args.variantId);
|
|
830
918
|
const projectContext = this.store.getProjectContext(args.sessionId);
|
|
831
919
|
let payload;
|
|
832
920
|
let envelopeDestination;
|
|
833
921
|
let changedFilesCount;
|
|
834
|
-
let
|
|
922
|
+
let runnablePaths;
|
|
835
923
|
if (projectContext.kind === 'fresh') {
|
|
836
924
|
const destinationPath = projectContext.workspacePath;
|
|
837
925
|
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
926
|
const freshMode = projectContext.executionPlan?.mode === 'vite_app'
|
|
845
927
|
? 'vite_app'
|
|
846
928
|
: 'static_preview';
|
|
@@ -918,7 +1000,7 @@ class AgentVariantsOrchestrator {
|
|
|
918
1000
|
// chosen project. Best-effort: failures here log and continue so
|
|
919
1001
|
// a partial history never blocks the commit handoff.
|
|
920
1002
|
try {
|
|
921
|
-
this.preserveUnchosenVariants({
|
|
1003
|
+
runnablePaths = this.preserveUnchosenVariants({
|
|
922
1004
|
sessionId: args.sessionId,
|
|
923
1005
|
chosenVariantId: args.variantId,
|
|
924
1006
|
destinationPath,
|
|
@@ -950,11 +1032,16 @@ class AgentVariantsOrchestrator {
|
|
|
950
1032
|
}
|
|
951
1033
|
else {
|
|
952
1034
|
// Static_preview: HTML is the entire deliverable. Write index.html.
|
|
1035
|
+
// Prefer the in-memory record; fall back to the persisted history at
|
|
1036
|
+
// `<workspaceRoot>/.rivet/variants/<sessionId>/<variantId>/files/index.html`
|
|
1037
|
+
// so a process restart between report_variant_complete and commit
|
|
1038
|
+
// doesn't strand the variant.
|
|
953
1039
|
const staticPreview = resources.staticPreviews.get(args.variantId);
|
|
954
|
-
const
|
|
955
|
-
|
|
956
|
-
|
|
957
|
-
:
|
|
1040
|
+
const htmlFromSnapshot = await this.variantHistory.readStaticPreview({
|
|
1041
|
+
projectPath: projectContext.workspaceRoot,
|
|
1042
|
+
sessionId: args.sessionId,
|
|
1043
|
+
variantId: args.variantId,
|
|
1044
|
+
});
|
|
958
1045
|
if (!staticPreview && !htmlFromSnapshot) {
|
|
959
1046
|
throw new errors_1.AgentVariantsError('INVALID_STAGE_ACTION', `No static preview found for variant ${args.variantId} — wait for report_variant_complete(succeeded) first`);
|
|
960
1047
|
}
|
|
@@ -1009,7 +1096,7 @@ class AgentVariantsOrchestrator {
|
|
|
1009
1096
|
diff: record.diff,
|
|
1010
1097
|
target: input.target,
|
|
1011
1098
|
changedFilesCount,
|
|
1012
|
-
note:
|
|
1099
|
+
note: "Variant diff applied to the user's working tree (uncommitted).",
|
|
1013
1100
|
};
|
|
1014
1101
|
envelopeDestination = env.projectPath;
|
|
1015
1102
|
}
|
|
@@ -1028,30 +1115,37 @@ class AgentVariantsOrchestrator {
|
|
|
1028
1115
|
sessionId: args.sessionId,
|
|
1029
1116
|
envelope,
|
|
1030
1117
|
});
|
|
1031
|
-
|
|
1032
|
-
|
|
1033
|
-
|
|
1034
|
-
|
|
1035
|
-
|
|
1036
|
-
|
|
1037
|
-
|
|
1038
|
-
|
|
1039
|
-
|
|
1040
|
-
|
|
1041
|
-
|
|
1042
|
-
|
|
1043
|
-
|
|
1044
|
-
|
|
1045
|
-
|
|
1046
|
-
|
|
1047
|
-
|
|
1048
|
-
|
|
1049
|
-
|
|
1050
|
-
|
|
1051
|
-
}
|
|
1118
|
+
// History: flip every persisted variant in this session to its terminal
|
|
1119
|
+
// status. Variants were persisted at code_gen success time; this only
|
|
1120
|
+
// patches the status (+ destinationPath on the chosen one). For fresh
|
|
1121
|
+
// sessions, history lives at `<workspaceRoot>/.rivet/variants/` — the
|
|
1122
|
+
// user's working dir — so all variants from any session in the workspace
|
|
1123
|
+
// accumulate in one place, not per-subproject. Best-effort, never blocks
|
|
1124
|
+
// the commit.
|
|
1125
|
+
const historyProjectPath = projectContext.kind === 'fresh'
|
|
1126
|
+
? projectContext.workspaceRoot
|
|
1127
|
+
: envelopeDestination;
|
|
1128
|
+
void this.persistVariantHistoryAtCommit({
|
|
1129
|
+
sessionId: args.sessionId,
|
|
1130
|
+
chosenVariantId: args.variantId,
|
|
1131
|
+
projectPath: historyProjectPath,
|
|
1132
|
+
projectKind: projectContext.kind,
|
|
1133
|
+
destinationPath: envelopeDestination,
|
|
1134
|
+
runnablePaths,
|
|
1135
|
+
}).catch((err) => {
|
|
1136
|
+
log.warn(`persistVariantHistoryAtCommit failed for session ${args.sessionId}`, err);
|
|
1137
|
+
});
|
|
1052
1138
|
const enqueueResult = this.adapter.enqueue(envelope);
|
|
1053
1139
|
resources.committedVariantIds.add(args.variantId);
|
|
1054
|
-
|
|
1140
|
+
// For fresh `new-project` commits the committed variant IS the user's
|
|
1141
|
+
// final result — there's no dev server to fall back to. Keep the session
|
|
1142
|
+
// active so SSE keeps pushing snapshots with the chosen variant; the UI's
|
|
1143
|
+
// iframe stays on it instead of unmounting to about:blank (which would
|
|
1144
|
+
// surface a misleading "Preview isn't connected" overlay). For diff /
|
|
1145
|
+
// diff-applied commits the user moves on to their own dev server, so
|
|
1146
|
+
// clearing is correct — that's the original behavior preserved below.
|
|
1147
|
+
if (this.activeSessionId === args.sessionId &&
|
|
1148
|
+
payload.kind !== 'project-created') {
|
|
1055
1149
|
this.activeSessionId = null;
|
|
1056
1150
|
}
|
|
1057
1151
|
const dwellMsFromTerminal = resources.terminalAt
|
|
@@ -1090,12 +1184,17 @@ class AgentVariantsOrchestrator {
|
|
|
1090
1184
|
/**
|
|
1091
1185
|
* Ensure the user-facing destination path can receive the new project.
|
|
1092
1186
|
* Rejects when the path exists and is non-empty.
|
|
1187
|
+
*
|
|
1188
|
+
* `.rivet/` (pre-commit snapshots + history manifests) and `.gitignore`
|
|
1189
|
+
* (written by VariantHistoryService.ensureGitignore when variants persist
|
|
1190
|
+
* at success time) are both tolerated — neither is user-authored content
|
|
1191
|
+
* that would be clobbered by the materialize step.
|
|
1093
1192
|
*/
|
|
1094
1193
|
assertDestinationAvailable(destinationPath) {
|
|
1095
1194
|
if (!fs_1.default.existsSync(destinationPath))
|
|
1096
1195
|
return;
|
|
1097
1196
|
const entries = fs_1.default.readdirSync(destinationPath);
|
|
1098
|
-
const userVisibleEntries = entries.filter((entry) => entry !== '.rivet');
|
|
1197
|
+
const userVisibleEntries = entries.filter((entry) => entry !== '.rivet' && entry !== '.gitignore');
|
|
1099
1198
|
if (userVisibleEntries.length === 0)
|
|
1100
1199
|
return;
|
|
1101
1200
|
throw new errors_1.AgentVariantsError('DESTINATION_NOT_EMPTY', `Destination ${destinationPath} is not empty (${userVisibleEntries.length} entries) — refuse to materialize.`);
|
|
@@ -1187,10 +1286,16 @@ class AgentVariantsOrchestrator {
|
|
|
1187
1286
|
designContext: summarizeDesignContext(designContext),
|
|
1188
1287
|
});
|
|
1189
1288
|
log.info(`Provisioning ${codeGenIds.length} fresh worktree(s) for session ${sessionId}`);
|
|
1190
|
-
// destinationParent for fresh worktrees:
|
|
1191
|
-
//
|
|
1192
|
-
// commit into a directory rename
|
|
1193
|
-
|
|
1289
|
+
// destinationParent for fresh worktrees: the user's workspace root
|
|
1290
|
+
// (sibling to `.rivet/`). Keeping the worktree on the same volume as
|
|
1291
|
+
// the materialize destination turns commit into a directory rename
|
|
1292
|
+
// instead of a recursive copy. `path.dirname(workspacePath)` *used*
|
|
1293
|
+
// to equal `workspaceRoot`, but after nesting subprojects under
|
|
1294
|
+
// `<workspaceRoot>/.rivet/<slug>/` the dirname is now `.rivet/`,
|
|
1295
|
+
// which would stage worktrees inside `.rivet/.rivet-variants/`.
|
|
1296
|
+
// Use workspaceRoot directly so staging lives at
|
|
1297
|
+
// `<workspaceRoot>/.rivet-variants/` as originally intended.
|
|
1298
|
+
const destinationParent = projectContext.workspaceRoot;
|
|
1194
1299
|
const paths = await createFresh.call(this.worktrees, sessionId, codeGenIds.length, viteReactTs_1.VITE_REACT_TS_TEMPLATE, designContext, sourceContext, destinationParent);
|
|
1195
1300
|
resources.scaffoldBaseWorkItemId = scaffoldId;
|
|
1196
1301
|
resources.freshDestinationParent = destinationParent;
|
|
@@ -1387,10 +1492,17 @@ class AgentVariantsOrchestrator {
|
|
|
1387
1492
|
};
|
|
1388
1493
|
resources.staticPreviews.set(workItemId, record);
|
|
1389
1494
|
if (this.store.getProjectContext(sessionId).kind === 'fresh') {
|
|
1390
|
-
|
|
1495
|
+
// History at `<workspaceRoot>/.rivet/variants/<sessionId>/<variantId>/`
|
|
1496
|
+
// is the sole on-disk record. The legacy per-subproject snapshot
|
|
1497
|
+
// tree (`<slug>/.rivet/<variantName>/`) is no longer written —
|
|
1498
|
+
// it duplicated this data in a parallel layout and cluttered
|
|
1499
|
+
// `.rivet/` with slug-named directories before the user ever
|
|
1500
|
+
// committed.
|
|
1501
|
+
this.persistCompletedFreshVariant({
|
|
1391
1502
|
sessionId,
|
|
1392
1503
|
workItemId,
|
|
1393
|
-
|
|
1504
|
+
}).catch((err) => {
|
|
1505
|
+
log.warn(`persistCompletedFreshVariant failed for ${sessionId}/${workItemId}`, err);
|
|
1394
1506
|
});
|
|
1395
1507
|
}
|
|
1396
1508
|
const leasedAt = resources.leasedAt.get(workItemId);
|
|
@@ -1424,6 +1536,31 @@ class AgentVariantsOrchestrator {
|
|
|
1424
1536
|
catch (err) {
|
|
1425
1537
|
log.warn(`getDiff failed for ${record.worktreePath}`, err);
|
|
1426
1538
|
}
|
|
1539
|
+
// History: persist every completed variant immediately to
|
|
1540
|
+
// `<projectPath>/.rivet/variants/`. Existing projects pass the captured
|
|
1541
|
+
// diff (or an empty string when capture itself failed — the variant
|
|
1542
|
+
// still succeeded code-gen-wise, and the history row is the only
|
|
1543
|
+
// record the UI has). Fresh-project variants copy their worktree
|
|
1544
|
+
// (vite_app) or the staged HTML (static_preview). Best-effort — a
|
|
1545
|
+
// failure here must never block dev-server startup or the user's pick
|
|
1546
|
+
// flow.
|
|
1547
|
+
if (!isFresh) {
|
|
1548
|
+
this.persistCompletedExistingVariant({
|
|
1549
|
+
sessionId,
|
|
1550
|
+
workItemId,
|
|
1551
|
+
diff: record.diff ?? '',
|
|
1552
|
+
}).catch((err) => {
|
|
1553
|
+
log.warn(`persistCompletedExistingVariant failed for ${sessionId}/${workItemId}`, err);
|
|
1554
|
+
});
|
|
1555
|
+
}
|
|
1556
|
+
else {
|
|
1557
|
+
this.persistCompletedFreshVariant({
|
|
1558
|
+
sessionId,
|
|
1559
|
+
workItemId,
|
|
1560
|
+
}).catch((err) => {
|
|
1561
|
+
log.warn(`persistCompletedFreshVariant failed for ${sessionId}/${workItemId}`, err);
|
|
1562
|
+
});
|
|
1563
|
+
}
|
|
1427
1564
|
// Bring up a dev server in the variant's worktree so the user can cycle
|
|
1428
1565
|
// through live variants in the iframe via the chip. Failures here are
|
|
1429
1566
|
// logged but non-fatal — the user can still pick by reading the diff.
|
|
@@ -1463,80 +1600,229 @@ class AgentVariantsOrchestrator {
|
|
|
1463
1600
|
log.warn(`Failed to start dev server for variant ${workItemId}; live preview disabled for this variant`, err);
|
|
1464
1601
|
}
|
|
1465
1602
|
}
|
|
1466
|
-
|
|
1603
|
+
/**
|
|
1604
|
+
* Persist a completed existing-project code_gen variant into
|
|
1605
|
+
* `<env.projectPath>/.rivet/variants/<sessionId>/<variantId>/`. Called from
|
|
1606
|
+
* `handleSucceededReport` once the worktree diff has been captured but
|
|
1607
|
+
* before the user picks. Status is `completed` until `commitVariant` or a
|
|
1608
|
+
* cancellation transitions it.
|
|
1609
|
+
*/
|
|
1610
|
+
async persistCompletedExistingVariant(args) {
|
|
1467
1611
|
const projectContext = this.store.getProjectContext(args.sessionId);
|
|
1468
|
-
if (projectContext.kind !== '
|
|
1612
|
+
if (projectContext.kind !== 'existing')
|
|
1469
1613
|
return;
|
|
1614
|
+
let env;
|
|
1615
|
+
try {
|
|
1616
|
+
env = await this.resolveEnv(args.sessionId);
|
|
1470
1617
|
}
|
|
1471
|
-
|
|
1472
|
-
|
|
1473
|
-
|
|
1474
|
-
|
|
1475
|
-
|
|
1476
|
-
|
|
1477
|
-
|
|
1478
|
-
|
|
1479
|
-
|
|
1480
|
-
|
|
1481
|
-
|
|
1482
|
-
|
|
1483
|
-
|
|
1484
|
-
|
|
1485
|
-
|
|
1486
|
-
|
|
1487
|
-
|
|
1488
|
-
|
|
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,
|
|
1618
|
+
catch (err) {
|
|
1619
|
+
log.warn(`persistCompletedExistingVariant: resolveEnv failed for ${args.sessionId}`, err);
|
|
1620
|
+
return;
|
|
1621
|
+
}
|
|
1622
|
+
const record = this.resources.get(args.sessionId)?.worktrees.get(args.workItemId);
|
|
1623
|
+
let sourceDir;
|
|
1624
|
+
if (env.framework === 'static' && record) {
|
|
1625
|
+
try {
|
|
1626
|
+
sourceDir = await this.worktrees.getProjectCwdInWorktree(record.worktreePath);
|
|
1627
|
+
}
|
|
1628
|
+
catch (err) {
|
|
1629
|
+
log.warn(`persistCompletedExistingVariant: static snapshot path failed for ${args.sessionId}/${args.workItemId}`, err);
|
|
1630
|
+
}
|
|
1631
|
+
}
|
|
1632
|
+
const input = this.store.getWorkItemInput(args.sessionId, args.workItemId);
|
|
1633
|
+
const sessionPrompt = this.store.getPrompt(args.sessionId);
|
|
1634
|
+
await this.variantHistory.persistVariant({
|
|
1635
|
+
projectPath: env.projectPath,
|
|
1496
1636
|
sessionId: args.sessionId,
|
|
1497
1637
|
variantId: args.workItemId,
|
|
1498
|
-
|
|
1499
|
-
|
|
1500
|
-
|
|
1501
|
-
|
|
1502
|
-
|
|
1503
|
-
|
|
1504
|
-
|
|
1505
|
-
|
|
1506
|
-
|
|
1507
|
-
|
|
1508
|
-
|
|
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,
|
|
1638
|
+
label: input.briefLabel,
|
|
1639
|
+
brief: input.briefBody,
|
|
1640
|
+
sessionPrompt,
|
|
1641
|
+
kind: 'diff',
|
|
1642
|
+
diff: args.diff,
|
|
1643
|
+
sourceDir,
|
|
1644
|
+
preview: sourceDir
|
|
1645
|
+
? { kind: 'static', entryPath: 'index.html' }
|
|
1646
|
+
: { kind: 'none', reason: 'diff_only' },
|
|
1647
|
+
changedFilesCount: countDiffFiles(args.diff),
|
|
1648
|
+
projectKind: 'existing',
|
|
1526
1649
|
});
|
|
1527
1650
|
}
|
|
1528
|
-
|
|
1529
|
-
|
|
1651
|
+
/**
|
|
1652
|
+
* Persist a completed fresh-project variant into
|
|
1653
|
+
* `<projectContext.workspacePath>/.rivet/variants/<sessionId>/<variantId>/`.
|
|
1654
|
+
* Called from `handleSucceededReport` right after the per-variant snapshot
|
|
1655
|
+
* lands on disk — for static_preview that's the `.rivet/<slug>/` snapshot
|
|
1656
|
+
* dir; for vite_app fresh variants it's the worktree itself. Status starts
|
|
1657
|
+
* as 'completed' and transitions to 'committed' / 'rejected' / 'cancelled'
|
|
1658
|
+
* via `markStatus` at commit or teardown time.
|
|
1659
|
+
*
|
|
1660
|
+
* Running at success time (rather than commit time) means the history panel
|
|
1661
|
+
* populates live as variants generate — including when the user never picks
|
|
1662
|
+
* one. Mirrors the existing-project persistCompletedExistingVariant flow.
|
|
1663
|
+
*/
|
|
1664
|
+
async persistCompletedFreshVariant(args) {
|
|
1665
|
+
const projectContext = this.store.getProjectContext(args.sessionId);
|
|
1666
|
+
if (projectContext.kind !== 'fresh')
|
|
1667
|
+
return;
|
|
1668
|
+
// Variant history lives at the workspace root — the user's working
|
|
1669
|
+
// dir (e.g. `fable-eng-demo/`). All variants from any session in this
|
|
1670
|
+
// workspace accumulate at
|
|
1671
|
+
// `<workspaceRoot>/.rivet/variants/<sessionId>/<variantId>/`.
|
|
1672
|
+
const historyProjectPath = projectContext.workspaceRoot;
|
|
1673
|
+
const input = this.store.getWorkItemInput(args.sessionId, args.workItemId);
|
|
1674
|
+
const sessionPrompt = this.store.getPrompt(args.sessionId);
|
|
1675
|
+
const designArtifact = resolveDesignArtifact(input.designContextEntry);
|
|
1676
|
+
const resources = this.resources.get(args.sessionId);
|
|
1677
|
+
const worktreeRecord = resources?.worktrees.get(args.workItemId);
|
|
1678
|
+
const staticPreview = resources?.staticPreviews.get(args.workItemId);
|
|
1679
|
+
// Vite_app deliverables are full scaffolded worktrees — pass the worktree
|
|
1680
|
+
// directory as sourceDir and let copyDirFiltered handle it (excludes
|
|
1681
|
+
// node_modules, .rivet, etc). Static_preview deliverables are inline HTML
|
|
1682
|
+
// captured in `resources.staticPreviews`; stage them in a tmp dir so the
|
|
1683
|
+
// existing copy path works without poking the user's workspace.
|
|
1684
|
+
let sourceDir = null;
|
|
1685
|
+
let tmpStagingDir = null;
|
|
1686
|
+
if (worktreeRecord && fs_1.default.existsSync(worktreeRecord.worktreePath)) {
|
|
1687
|
+
sourceDir = worktreeRecord.worktreePath;
|
|
1688
|
+
}
|
|
1689
|
+
else if (staticPreview) {
|
|
1690
|
+
tmpStagingDir = fs_1.default.mkdtempSync(path_1.default.join(os_1.default.tmpdir(), `rivet-variant-${args.workItemId}-`));
|
|
1691
|
+
fs_1.default.writeFileSync(path_1.default.join(tmpStagingDir, 'index.html'), staticPreview.html, 'utf8');
|
|
1692
|
+
fs_1.default.writeFileSync(path_1.default.join(tmpStagingDir, 'brief.md'), `# ${input.briefLabel}\n\n${input.briefBody}\n`, 'utf8');
|
|
1693
|
+
sourceDir = tmpStagingDir;
|
|
1694
|
+
}
|
|
1695
|
+
if (!sourceDir) {
|
|
1696
|
+
log.warn(`persistCompletedFreshVariant: no source for ${args.workItemId} (session ${args.sessionId}); skipping`);
|
|
1697
|
+
return;
|
|
1698
|
+
}
|
|
1699
|
+
// For static_preview, only `index.html` is a real deliverable — `brief.md`
|
|
1700
|
+
// is implementation-detail staging that we copy alongside it. Hardcode
|
|
1701
|
+
// the count so the history row matches what `commit_variant` reports
|
|
1702
|
+
// (always 1) instead of double-counting the brief.
|
|
1703
|
+
const changedFilesCount = staticPreview ? 1 : countWorktreeFiles(sourceDir);
|
|
1704
|
+
const preview = staticPreview
|
|
1705
|
+
? { kind: 'static', entryPath: 'index.html' }
|
|
1706
|
+
: {
|
|
1707
|
+
kind: 'dev-server',
|
|
1708
|
+
command: {
|
|
1709
|
+
cwd: '.',
|
|
1710
|
+
packageManager: 'npm',
|
|
1711
|
+
script: 'dev',
|
|
1712
|
+
},
|
|
1713
|
+
};
|
|
1714
|
+
try {
|
|
1715
|
+
await this.variantHistory.persistVariant({
|
|
1716
|
+
projectPath: historyProjectPath,
|
|
1717
|
+
sessionId: args.sessionId,
|
|
1718
|
+
variantId: args.workItemId,
|
|
1719
|
+
label: input.briefLabel,
|
|
1720
|
+
brief: input.briefBody,
|
|
1721
|
+
sessionPrompt,
|
|
1722
|
+
kind: 'project-created',
|
|
1723
|
+
sourceDir,
|
|
1724
|
+
preview,
|
|
1725
|
+
changedFilesCount,
|
|
1726
|
+
projectKind: 'fresh',
|
|
1727
|
+
designMarkdown: designArtifact?.markdown,
|
|
1728
|
+
designSource: designArtifact?.source,
|
|
1729
|
+
});
|
|
1730
|
+
}
|
|
1731
|
+
finally {
|
|
1732
|
+
if (tmpStagingDir) {
|
|
1733
|
+
try {
|
|
1734
|
+
fs_1.default.rmSync(tmpStagingDir, { recursive: true, force: true });
|
|
1735
|
+
}
|
|
1736
|
+
catch (err) {
|
|
1737
|
+
log.warn(`Failed to clean up variant staging dir ${tmpStagingDir}: ${err instanceof Error ? err.message : String(err)}`);
|
|
1738
|
+
}
|
|
1739
|
+
}
|
|
1740
|
+
}
|
|
1741
|
+
}
|
|
1742
|
+
/**
|
|
1743
|
+
* At commit time, flip persisted variant manifests to terminal statuses
|
|
1744
|
+
* ('committed' for the chosen, 'rejected' for the rest). Both existing- and
|
|
1745
|
+
* fresh-project variants have already been persisted at code_gen success
|
|
1746
|
+
* time, so this is a pure status update — no source-dir copy required.
|
|
1747
|
+
* Also records the destination path on the chosen variant so history can
|
|
1748
|
+
* surface "this variant became <path>".
|
|
1749
|
+
*/
|
|
1750
|
+
async persistVariantHistoryAtCommit(args) {
|
|
1530
1751
|
const variants = this.store.getVariants(args.sessionId);
|
|
1531
|
-
const
|
|
1532
|
-
|
|
1533
|
-
|
|
1752
|
+
for (const variant of variants) {
|
|
1753
|
+
if (variant.status !== 'succeeded')
|
|
1754
|
+
continue;
|
|
1755
|
+
const workItemId = variant.workItemId;
|
|
1756
|
+
const isChosen = workItemId === args.chosenVariantId;
|
|
1757
|
+
const status = isChosen
|
|
1758
|
+
? 'committed'
|
|
1759
|
+
: 'rejected';
|
|
1760
|
+
await this.variantHistory.markStatus({
|
|
1761
|
+
projectPath: args.projectPath,
|
|
1762
|
+
sessionId: args.sessionId,
|
|
1763
|
+
variantId: workItemId,
|
|
1764
|
+
status,
|
|
1765
|
+
destinationPath: isChosen ? args.destinationPath : undefined,
|
|
1766
|
+
runnablePath: args.runnablePaths?.get(workItemId),
|
|
1767
|
+
});
|
|
1768
|
+
}
|
|
1769
|
+
}
|
|
1770
|
+
/**
|
|
1771
|
+
* Flip persisted variant manifests to status='cancelled' for every variant
|
|
1772
|
+
* in the session that was already snapshotted to disk. Fresh- and existing-
|
|
1773
|
+
* project variants both persist at success time now, so both need their
|
|
1774
|
+
* manifests flipped here. Best-effort — missing manifests are silently
|
|
1775
|
+
* skipped by VariantHistoryService.markStatus.
|
|
1776
|
+
*/
|
|
1777
|
+
async markPersistedVariantsCancelled(sessionId) {
|
|
1778
|
+
if (!this.store.hasSession(sessionId))
|
|
1779
|
+
return;
|
|
1780
|
+
const projectPath = await this.resolveHistoryProjectPath(sessionId);
|
|
1781
|
+
if (!projectPath)
|
|
1782
|
+
return;
|
|
1783
|
+
const variants = this.store.getVariants(sessionId);
|
|
1784
|
+
for (const variant of variants) {
|
|
1785
|
+
await this.variantHistory.markStatus({
|
|
1786
|
+
projectPath,
|
|
1787
|
+
sessionId,
|
|
1788
|
+
variantId: variant.workItemId,
|
|
1789
|
+
status: 'cancelled',
|
|
1790
|
+
});
|
|
1791
|
+
}
|
|
1792
|
+
}
|
|
1793
|
+
async markPersistedVariantCancelled(sessionId, variantId) {
|
|
1794
|
+
if (!this.store.hasSession(sessionId))
|
|
1795
|
+
return;
|
|
1796
|
+
const projectPath = await this.resolveHistoryProjectPath(sessionId);
|
|
1797
|
+
if (!projectPath)
|
|
1798
|
+
return;
|
|
1799
|
+
await this.variantHistory.markStatus({
|
|
1800
|
+
projectPath,
|
|
1801
|
+
sessionId,
|
|
1802
|
+
variantId,
|
|
1803
|
+
status: 'cancelled',
|
|
1534
1804
|
});
|
|
1535
|
-
|
|
1536
|
-
|
|
1537
|
-
|
|
1805
|
+
}
|
|
1806
|
+
/**
|
|
1807
|
+
* Resolve the project path that owns `.rivet/variants/` for a session.
|
|
1808
|
+
* Existing sessions: the user's project (via `resolveEnv`). Fresh sessions:
|
|
1809
|
+
* the *workspace root*, which is the parent of `workspacePath` — variants
|
|
1810
|
+
* accumulate there across sessions instead of being scattered under each
|
|
1811
|
+
* subproject. Must match what `persistCompletedFreshVariant` writes to.
|
|
1812
|
+
*/
|
|
1813
|
+
async resolveHistoryProjectPath(sessionId) {
|
|
1814
|
+
const projectContext = this.store.getProjectContext(sessionId);
|
|
1815
|
+
if (projectContext.kind === 'fresh') {
|
|
1816
|
+
return projectContext.workspaceRoot;
|
|
1817
|
+
}
|
|
1818
|
+
try {
|
|
1819
|
+
const env = await this.resolveEnv(sessionId);
|
|
1820
|
+
return env.projectPath;
|
|
1821
|
+
}
|
|
1822
|
+
catch (err) {
|
|
1823
|
+
log.warn(`resolveHistoryProjectPath: resolveEnv failed for ${sessionId}`, err);
|
|
1824
|
+
return null;
|
|
1538
1825
|
}
|
|
1539
|
-
return `${baseSlug}-${index + 1}`;
|
|
1540
1826
|
}
|
|
1541
1827
|
readManifest(manifestPath) {
|
|
1542
1828
|
if (!fs_1.default.existsSync(manifestPath)) {
|
|
@@ -1597,8 +1883,9 @@ class AgentVariantsOrchestrator {
|
|
|
1597
1883
|
*/
|
|
1598
1884
|
preserveUnchosenVariants(args) {
|
|
1599
1885
|
const resources = this.resources.get(args.sessionId);
|
|
1886
|
+
const runnablePaths = new Map();
|
|
1600
1887
|
if (!resources)
|
|
1601
|
-
return;
|
|
1888
|
+
return runnablePaths;
|
|
1602
1889
|
const destinationParent = path_1.default.dirname(args.destinationPath);
|
|
1603
1890
|
const projectSlug = path_1.default.basename(args.destinationPath);
|
|
1604
1891
|
const historyDir = (0, createProjectArtifacts_1.createVariantsHistoryPath)(destinationParent, projectSlug);
|
|
@@ -1617,6 +1904,7 @@ class AgentVariantsOrchestrator {
|
|
|
1617
1904
|
const folderName = `${numericPrefix}-${slug}`;
|
|
1618
1905
|
if (variant.workItemId === args.chosenVariantId) {
|
|
1619
1906
|
chosenSlug = slug;
|
|
1907
|
+
runnablePaths.set(variant.workItemId, projectSlug);
|
|
1620
1908
|
manifestEntries.push({
|
|
1621
1909
|
variantId: variant.workItemId,
|
|
1622
1910
|
label,
|
|
@@ -1682,6 +1970,7 @@ class AgentVariantsOrchestrator {
|
|
|
1682
1970
|
// Update the in-memory record so teardown doesn't try to operate on
|
|
1683
1971
|
// the stale path.
|
|
1684
1972
|
record.worktreePath = newPath;
|
|
1973
|
+
runnablePaths.set(variant.workItemId, `${path_1.default.basename(historyDir)}/${folderName}`);
|
|
1685
1974
|
manifestEntries.push({
|
|
1686
1975
|
variantId: variant.workItemId,
|
|
1687
1976
|
label,
|
|
@@ -1716,6 +2005,7 @@ class AgentVariantsOrchestrator {
|
|
|
1716
2005
|
log.warn(`Writing variants history manifest failed for ${historyDir}`, err);
|
|
1717
2006
|
}
|
|
1718
2007
|
resources.vitePreservedSiblings = true;
|
|
2008
|
+
return runnablePaths;
|
|
1719
2009
|
}
|
|
1720
2010
|
/**
|
|
1721
2011
|
* Rename `sourceWorktreePath` into `destinationPath`, then replace the
|
|
@@ -1995,11 +2285,33 @@ function parseStaticPreviewOutput(output) {
|
|
|
1995
2285
|
};
|
|
1996
2286
|
}
|
|
1997
2287
|
function buildStaticPreviewDocument(input) {
|
|
1998
|
-
if (/<!doctype html>|<html[\s>]/i.test(input.html)) {
|
|
1999
|
-
return input.html;
|
|
2000
|
-
}
|
|
2001
2288
|
const style = input.css ? `<style>\n${input.css}\n</style>` : '';
|
|
2002
2289
|
const script = input.js ? `<script>\n${input.js}\n</script>` : '';
|
|
2290
|
+
if (/<!doctype html>|<html[\s>]/i.test(input.html)) {
|
|
2291
|
+
// Full document: inject css before </head> and js before </body>. The
|
|
2292
|
+
// agent often passes a complete `<!doctype html>...` blob with css/js
|
|
2293
|
+
// alongside; without this they're silently dropped and the variant ships
|
|
2294
|
+
// unstyled and non-interactive. Falls back to appending to the end if
|
|
2295
|
+
// the closing tag isn't found.
|
|
2296
|
+
let doc = input.html;
|
|
2297
|
+
if (style) {
|
|
2298
|
+
if (/<\/head>/i.test(doc)) {
|
|
2299
|
+
doc = doc.replace(/<\/head>/i, () => `${style}\n</head>`);
|
|
2300
|
+
}
|
|
2301
|
+
else {
|
|
2302
|
+
doc += `\n${style}`;
|
|
2303
|
+
}
|
|
2304
|
+
}
|
|
2305
|
+
if (script) {
|
|
2306
|
+
if (/<\/body>/i.test(doc)) {
|
|
2307
|
+
doc = doc.replace(/<\/body>/i, () => `${script}\n</body>`);
|
|
2308
|
+
}
|
|
2309
|
+
else {
|
|
2310
|
+
doc += `\n${script}`;
|
|
2311
|
+
}
|
|
2312
|
+
}
|
|
2313
|
+
return doc;
|
|
2314
|
+
}
|
|
2003
2315
|
return `<!doctype html>
|
|
2004
2316
|
<html lang="en">
|
|
2005
2317
|
<head>
|
|
@@ -2061,6 +2373,7 @@ const toActiveProjectContext = (projectContext) => {
|
|
|
2061
2373
|
return {
|
|
2062
2374
|
kind: 'fresh',
|
|
2063
2375
|
workspacePath: projectContext.workspacePath,
|
|
2376
|
+
workspaceRoot: projectContext.workspaceRoot,
|
|
2064
2377
|
framework: projectContext.framework,
|
|
2065
2378
|
designContext: projectContext.designContext?.map((entry) => entry.kind === 'slug'
|
|
2066
2379
|
? { kind: 'slug', slug: entry.slug }
|
|
@@ -2090,15 +2403,26 @@ const toActiveProjectContext = (projectContext) => {
|
|
|
2090
2403
|
* Resolve the user-facing supporting artifacts for a session.
|
|
2091
2404
|
*
|
|
2092
2405
|
* For 0→1 (`fresh`) sessions with a populated `designContext`, each slot is
|
|
2093
|
-
* turned into a `design_context` artifact
|
|
2094
|
-
*
|
|
2406
|
+
* turned into a `design_context` artifact that links to Rivet-hosted
|
|
2407
|
+
* DESIGN.md views:
|
|
2095
2408
|
* - `slug` entries resolve bundled catalog markdown via the design catalog.
|
|
2096
2409
|
* - `markdown` entries (Agent Browser / inspiration extractor output) carry
|
|
2097
|
-
* their stored markdown
|
|
2410
|
+
* their stored markdown when the linked route is requested.
|
|
2098
2411
|
* Slots whose markdown can't be resolved are skipped so the UI never renders
|
|
2099
2412
|
* a metadata-only DESIGN.md row.
|
|
2100
2413
|
*/
|
|
2101
|
-
const buildSessionArtifacts = (projectContext) => {
|
|
2414
|
+
const buildSessionArtifacts = (sessionId, projectContext) => {
|
|
2415
|
+
const artifacts = buildResolvedDesignContextArtifacts(projectContext);
|
|
2416
|
+
return artifacts.map(({ content: _content, ...artifact }) => ({
|
|
2417
|
+
...artifact,
|
|
2418
|
+
rawUrl: buildDesignContextRawUrl(sessionId, artifact.id),
|
|
2419
|
+
viewUrl: buildDesignContextViewUrl(sessionId, artifact.id),
|
|
2420
|
+
}));
|
|
2421
|
+
};
|
|
2422
|
+
const findDesignContextArtifact = (projectContext, artifactId) => {
|
|
2423
|
+
return buildResolvedDesignContextArtifacts(projectContext).find((artifact) => artifact.id === artifactId);
|
|
2424
|
+
};
|
|
2425
|
+
const buildResolvedDesignContextArtifacts = (projectContext) => {
|
|
2102
2426
|
if (projectContext.kind !== 'fresh')
|
|
2103
2427
|
return [];
|
|
2104
2428
|
const designContext = projectContext.designContext;
|
|
@@ -2117,7 +2441,9 @@ const buildSessionArtifacts = (projectContext) => {
|
|
|
2117
2441
|
id: `design_context:${slot}:${entry.slug}`,
|
|
2118
2442
|
kind: 'design_context',
|
|
2119
2443
|
label: catalogEntry?.name ?? entry.slug,
|
|
2120
|
-
...(catalogEntry?.description
|
|
2444
|
+
...(catalogEntry?.description
|
|
2445
|
+
? { summary: catalogEntry.description }
|
|
2446
|
+
: {}),
|
|
2121
2447
|
status: 'ready',
|
|
2122
2448
|
source: 'static',
|
|
2123
2449
|
contentType: 'text/markdown',
|
|
@@ -2149,8 +2475,6 @@ const buildSessionArtifacts = (projectContext) => {
|
|
|
2149
2475
|
});
|
|
2150
2476
|
};
|
|
2151
2477
|
const addDesignContextArtifact = (artifactsByContent, artifact) => {
|
|
2152
|
-
if (!artifact.content)
|
|
2153
|
-
return;
|
|
2154
2478
|
const existing = artifactsByContent.get(artifact.content);
|
|
2155
2479
|
if (existing) {
|
|
2156
2480
|
existing.usedByVariantCount += 1;
|
|
@@ -2161,6 +2485,340 @@ const addDesignContextArtifact = (artifactsByContent, artifact) => {
|
|
|
2161
2485
|
usedByVariantCount: 1,
|
|
2162
2486
|
});
|
|
2163
2487
|
};
|
|
2488
|
+
const buildDesignContextRawUrl = (sessionId, artifactId) => {
|
|
2489
|
+
return `/api/variants/${encodeURIComponent(sessionId)}/${DESIGN_CONTEXT_ROUTE_SEGMENT}/${encodeURIComponent(artifactId)}`;
|
|
2490
|
+
};
|
|
2491
|
+
const buildDesignContextViewUrl = (sessionId, artifactId) => {
|
|
2492
|
+
return `${buildDesignContextRawUrl(sessionId, artifactId)}/${DESIGN_CONTEXT_VIEW_SEGMENT}`;
|
|
2493
|
+
};
|
|
2494
|
+
const buildDesignContextViewerDocument = (artifact) => {
|
|
2495
|
+
const title = `${artifact.label} DESIGN.md`;
|
|
2496
|
+
const visualHtml = renderDesignMarkdown(artifact.content);
|
|
2497
|
+
const rawMarkdown = escapeHtml(artifact.content);
|
|
2498
|
+
return `<!doctype html>
|
|
2499
|
+
<html lang="en">
|
|
2500
|
+
<head>
|
|
2501
|
+
<meta charset="utf-8" />
|
|
2502
|
+
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
|
2503
|
+
<title>${escapeHtml(title)}</title>
|
|
2504
|
+
<style>
|
|
2505
|
+
:root {
|
|
2506
|
+
color-scheme: light;
|
|
2507
|
+
--ink: #201b16;
|
|
2508
|
+
--muted: #75695e;
|
|
2509
|
+
--paper: #fbf7ef;
|
|
2510
|
+
--panel: #fffdf8;
|
|
2511
|
+
--rule: #eadfcf;
|
|
2512
|
+
--accent: #e45d2f;
|
|
2513
|
+
--accent-soft: #ffe0d3;
|
|
2514
|
+
--code: #2a2521;
|
|
2515
|
+
}
|
|
2516
|
+
* { box-sizing: border-box; }
|
|
2517
|
+
body {
|
|
2518
|
+
margin: 0;
|
|
2519
|
+
min-height: 100vh;
|
|
2520
|
+
background:
|
|
2521
|
+
radial-gradient(circle at top left, rgba(228, 93, 47, 0.18), transparent 36rem),
|
|
2522
|
+
linear-gradient(135deg, #fbf7ef 0%, #f2eadc 100%);
|
|
2523
|
+
color: var(--ink);
|
|
2524
|
+
font-family: ui-serif, Georgia, Cambria, "Times New Roman", serif;
|
|
2525
|
+
}
|
|
2526
|
+
main {
|
|
2527
|
+
width: min(1440px, calc(100vw - 40px));
|
|
2528
|
+
margin: 0 auto;
|
|
2529
|
+
padding: 40px 0;
|
|
2530
|
+
}
|
|
2531
|
+
header {
|
|
2532
|
+
display: flex;
|
|
2533
|
+
align-items: center;
|
|
2534
|
+
justify-content: space-between;
|
|
2535
|
+
gap: 24px;
|
|
2536
|
+
margin-bottom: 24px;
|
|
2537
|
+
border-bottom: 1px solid var(--rule);
|
|
2538
|
+
padding-bottom: 18px;
|
|
2539
|
+
}
|
|
2540
|
+
h1 {
|
|
2541
|
+
margin: 0;
|
|
2542
|
+
max-width: 780px;
|
|
2543
|
+
font-size: clamp(2rem, 5vw, 4.5rem);
|
|
2544
|
+
letter-spacing: -0.06em;
|
|
2545
|
+
line-height: 0.92;
|
|
2546
|
+
}
|
|
2547
|
+
.mode-input {
|
|
2548
|
+
position: absolute;
|
|
2549
|
+
opacity: 0;
|
|
2550
|
+
pointer-events: none;
|
|
2551
|
+
}
|
|
2552
|
+
.mode-toggle {
|
|
2553
|
+
display: inline-flex;
|
|
2554
|
+
flex-shrink: 0;
|
|
2555
|
+
gap: 4px;
|
|
2556
|
+
border: 1px solid var(--rule);
|
|
2557
|
+
border-radius: 999px;
|
|
2558
|
+
background: rgba(255, 253, 248, 0.72);
|
|
2559
|
+
padding: 4px;
|
|
2560
|
+
box-shadow: 0 12px 30px rgba(61, 44, 26, 0.08);
|
|
2561
|
+
}
|
|
2562
|
+
.mode-toggle label {
|
|
2563
|
+
position: relative;
|
|
2564
|
+
cursor: pointer;
|
|
2565
|
+
border-radius: 999px;
|
|
2566
|
+
padding: 8px 14px;
|
|
2567
|
+
color: var(--muted);
|
|
2568
|
+
font: 600 0.74rem/1.2 ui-sans-serif, system-ui, sans-serif;
|
|
2569
|
+
letter-spacing: 0.12em;
|
|
2570
|
+
text-transform: uppercase;
|
|
2571
|
+
transition:
|
|
2572
|
+
background 160ms ease,
|
|
2573
|
+
color 160ms ease;
|
|
2574
|
+
}
|
|
2575
|
+
.mode-toggle label:has(input:checked) {
|
|
2576
|
+
background: var(--ink);
|
|
2577
|
+
color: var(--paper);
|
|
2578
|
+
}
|
|
2579
|
+
.viewer {
|
|
2580
|
+
display: block;
|
|
2581
|
+
}
|
|
2582
|
+
.viewer-panel {
|
|
2583
|
+
display: none;
|
|
2584
|
+
}
|
|
2585
|
+
main:has(#designmd-visual-mode:checked) .visual-panel,
|
|
2586
|
+
main:has(#designmd-raw-mode:checked) .raw-panel {
|
|
2587
|
+
display: block;
|
|
2588
|
+
}
|
|
2589
|
+
.panel {
|
|
2590
|
+
overflow: hidden;
|
|
2591
|
+
border: 1px solid var(--rule);
|
|
2592
|
+
border-radius: 18px;
|
|
2593
|
+
background: color-mix(in srgb, var(--panel) 92%, white);
|
|
2594
|
+
box-shadow: 0 20px 60px rgba(61, 44, 26, 0.12);
|
|
2595
|
+
}
|
|
2596
|
+
.panel-title {
|
|
2597
|
+
display: flex;
|
|
2598
|
+
align-items: center;
|
|
2599
|
+
justify-content: space-between;
|
|
2600
|
+
border-bottom: 1px solid var(--rule);
|
|
2601
|
+
padding: 12px 16px;
|
|
2602
|
+
color: var(--muted);
|
|
2603
|
+
font: 700 0.72rem/1 ui-sans-serif, system-ui, sans-serif;
|
|
2604
|
+
letter-spacing: 0.12em;
|
|
2605
|
+
text-transform: uppercase;
|
|
2606
|
+
}
|
|
2607
|
+
.visual {
|
|
2608
|
+
padding: 24px;
|
|
2609
|
+
}
|
|
2610
|
+
.visual h1,
|
|
2611
|
+
.visual h2,
|
|
2612
|
+
.visual h3 {
|
|
2613
|
+
margin: 1.3em 0 0.45em;
|
|
2614
|
+
letter-spacing: -0.04em;
|
|
2615
|
+
line-height: 1;
|
|
2616
|
+
}
|
|
2617
|
+
.visual h1:first-child,
|
|
2618
|
+
.visual h2:first-child,
|
|
2619
|
+
.visual h3:first-child {
|
|
2620
|
+
margin-top: 0;
|
|
2621
|
+
}
|
|
2622
|
+
.visual h1 { font-size: 2.25rem; }
|
|
2623
|
+
.visual h2 { font-size: 1.6rem; }
|
|
2624
|
+
.visual h3 { font-size: 1.18rem; }
|
|
2625
|
+
.visual p,
|
|
2626
|
+
.visual li,
|
|
2627
|
+
.visual blockquote {
|
|
2628
|
+
color: #3d342c;
|
|
2629
|
+
font-size: 1rem;
|
|
2630
|
+
line-height: 1.62;
|
|
2631
|
+
}
|
|
2632
|
+
.visual ul {
|
|
2633
|
+
display: grid;
|
|
2634
|
+
gap: 8px;
|
|
2635
|
+
padding-left: 1.1rem;
|
|
2636
|
+
}
|
|
2637
|
+
.visual blockquote {
|
|
2638
|
+
margin: 18px 0;
|
|
2639
|
+
border-left: 4px solid var(--accent);
|
|
2640
|
+
padding-left: 14px;
|
|
2641
|
+
color: var(--muted);
|
|
2642
|
+
}
|
|
2643
|
+
.visual code {
|
|
2644
|
+
border-radius: 6px;
|
|
2645
|
+
background: var(--accent-soft);
|
|
2646
|
+
padding: 0.12rem 0.34rem;
|
|
2647
|
+
font-family: ui-monospace, "SFMono-Regular", Menlo, Consolas, monospace;
|
|
2648
|
+
font-size: 0.92em;
|
|
2649
|
+
}
|
|
2650
|
+
pre {
|
|
2651
|
+
margin: 0;
|
|
2652
|
+
max-height: calc(100vh - 190px);
|
|
2653
|
+
overflow: auto;
|
|
2654
|
+
background: #181512;
|
|
2655
|
+
color: #f9ead7;
|
|
2656
|
+
padding: 20px;
|
|
2657
|
+
font: 0.78rem/1.55 ui-monospace, "SFMono-Regular", Menlo, Consolas, monospace;
|
|
2658
|
+
white-space: pre-wrap;
|
|
2659
|
+
word-break: break-word;
|
|
2660
|
+
}
|
|
2661
|
+
@media (max-width: 900px) {
|
|
2662
|
+
main { width: min(100vw - 24px, 760px); padding: 24px 0; }
|
|
2663
|
+
header { align-items: start; flex-direction: column; }
|
|
2664
|
+
pre { max-height: 520px; }
|
|
2665
|
+
}
|
|
2666
|
+
</style>
|
|
2667
|
+
</head>
|
|
2668
|
+
<body>
|
|
2669
|
+
<main>
|
|
2670
|
+
<header>
|
|
2671
|
+
<h1>${escapeHtml(title)}</h1>
|
|
2672
|
+
<div class="mode-toggle" aria-label="DesignMD view mode">
|
|
2673
|
+
<label>
|
|
2674
|
+
<input class="mode-input" type="radio" name="designmd-view-mode" id="designmd-visual-mode" checked />
|
|
2675
|
+
<span>Visual</span>
|
|
2676
|
+
</label>
|
|
2677
|
+
<label>
|
|
2678
|
+
<input class="mode-input" type="radio" name="designmd-view-mode" id="designmd-raw-mode" />
|
|
2679
|
+
<span>Raw</span>
|
|
2680
|
+
</label>
|
|
2681
|
+
</div>
|
|
2682
|
+
</header>
|
|
2683
|
+
<section class="viewer" aria-label="DesignMD artifact">
|
|
2684
|
+
<article class="panel viewer-panel visual-panel" id="designmd-visual">
|
|
2685
|
+
<div class="panel-title"><span>Visual</span><span>Rendered DESIGN.md</span></div>
|
|
2686
|
+
<div class="visual">${visualHtml}</div>
|
|
2687
|
+
</article>
|
|
2688
|
+
<article class="panel viewer-panel raw-panel" id="designmd-raw">
|
|
2689
|
+
<div class="panel-title"><span>Raw</span><span>Markdown source</span></div>
|
|
2690
|
+
<pre>${rawMarkdown}</pre>
|
|
2691
|
+
</article>
|
|
2692
|
+
</section>
|
|
2693
|
+
</main>
|
|
2694
|
+
</body>
|
|
2695
|
+
</html>`;
|
|
2696
|
+
};
|
|
2697
|
+
const renderDesignMarkdown = (markdown) => {
|
|
2698
|
+
const lines = markdown.replace(/\r\n/g, '\n').split('\n');
|
|
2699
|
+
const output = [];
|
|
2700
|
+
const paragraph = [];
|
|
2701
|
+
const listItems = [];
|
|
2702
|
+
const codeLines = [];
|
|
2703
|
+
let isCodeBlock = false;
|
|
2704
|
+
const flushParagraph = () => {
|
|
2705
|
+
if (paragraph.length === 0)
|
|
2706
|
+
return;
|
|
2707
|
+
output.push(`<p>${renderInlineMarkdown(paragraph.join(' '))}</p>`);
|
|
2708
|
+
paragraph.length = 0;
|
|
2709
|
+
};
|
|
2710
|
+
const flushList = () => {
|
|
2711
|
+
if (listItems.length === 0)
|
|
2712
|
+
return;
|
|
2713
|
+
output.push(`<ul>${listItems.map((item) => `<li>${renderInlineMarkdown(item)}</li>`).join('')}</ul>`);
|
|
2714
|
+
listItems.length = 0;
|
|
2715
|
+
};
|
|
2716
|
+
const flushCode = () => {
|
|
2717
|
+
if (codeLines.length === 0)
|
|
2718
|
+
return;
|
|
2719
|
+
output.push(`<pre>${escapeHtml(codeLines.join('\n'))}</pre>`);
|
|
2720
|
+
codeLines.length = 0;
|
|
2721
|
+
};
|
|
2722
|
+
lines.forEach((line) => {
|
|
2723
|
+
if (line.trim().startsWith('```')) {
|
|
2724
|
+
if (isCodeBlock) {
|
|
2725
|
+
flushCode();
|
|
2726
|
+
isCodeBlock = false;
|
|
2727
|
+
return;
|
|
2728
|
+
}
|
|
2729
|
+
flushParagraph();
|
|
2730
|
+
flushList();
|
|
2731
|
+
isCodeBlock = true;
|
|
2732
|
+
return;
|
|
2733
|
+
}
|
|
2734
|
+
if (isCodeBlock) {
|
|
2735
|
+
codeLines.push(line);
|
|
2736
|
+
return;
|
|
2737
|
+
}
|
|
2738
|
+
const trimmed = line.trim();
|
|
2739
|
+
if (!trimmed) {
|
|
2740
|
+
flushParagraph();
|
|
2741
|
+
flushList();
|
|
2742
|
+
return;
|
|
2743
|
+
}
|
|
2744
|
+
if (/^---+$/.test(trimmed)) {
|
|
2745
|
+
flushParagraph();
|
|
2746
|
+
flushList();
|
|
2747
|
+
output.push('<hr />');
|
|
2748
|
+
return;
|
|
2749
|
+
}
|
|
2750
|
+
const headingMatch = trimmed.match(/^(#{1,6})\s+(.+)$/);
|
|
2751
|
+
if (headingMatch) {
|
|
2752
|
+
flushParagraph();
|
|
2753
|
+
flushList();
|
|
2754
|
+
const level = Math.min(headingMatch[1].length, 3);
|
|
2755
|
+
output.push(`<h${level}>${renderInlineMarkdown(headingMatch[2])}</h${level}>`);
|
|
2756
|
+
return;
|
|
2757
|
+
}
|
|
2758
|
+
const listMatch = trimmed.match(/^[-*]\s+(.+)$/);
|
|
2759
|
+
if (listMatch) {
|
|
2760
|
+
flushParagraph();
|
|
2761
|
+
listItems.push(listMatch[1]);
|
|
2762
|
+
return;
|
|
2763
|
+
}
|
|
2764
|
+
const quoteMatch = trimmed.match(/^>\s?(.+)$/);
|
|
2765
|
+
if (quoteMatch) {
|
|
2766
|
+
flushParagraph();
|
|
2767
|
+
flushList();
|
|
2768
|
+
output.push(`<blockquote>${renderInlineMarkdown(quoteMatch[1])}</blockquote>`);
|
|
2769
|
+
return;
|
|
2770
|
+
}
|
|
2771
|
+
paragraph.push(trimmed);
|
|
2772
|
+
});
|
|
2773
|
+
flushCode();
|
|
2774
|
+
flushParagraph();
|
|
2775
|
+
flushList();
|
|
2776
|
+
return output.join('\n');
|
|
2777
|
+
};
|
|
2778
|
+
const renderInlineMarkdown = (value) => {
|
|
2779
|
+
return escapeHtml(value)
|
|
2780
|
+
.replace(/\*\*([^*]+)\*\*/g, '<strong>$1</strong>')
|
|
2781
|
+
.replace(/`([^`]+)`/g, '<code>$1</code>')
|
|
2782
|
+
.replace(/\*([^*]+)\*/g, '<em>$1</em>');
|
|
2783
|
+
};
|
|
2784
|
+
const escapeHtml = (value) => {
|
|
2785
|
+
return value
|
|
2786
|
+
.replace(/&/g, '&')
|
|
2787
|
+
.replace(/</g, '<')
|
|
2788
|
+
.replace(/>/g, '>')
|
|
2789
|
+
.replace(/"/g, '"')
|
|
2790
|
+
.replace(/'/g, ''');
|
|
2791
|
+
};
|
|
2792
|
+
/**
|
|
2793
|
+
* Resolve a per-variant design context entry into the raw DESIGN.md markdown
|
|
2794
|
+
* the worktree scaffolder writes, plus a small `designSource` descriptor for
|
|
2795
|
+
* the variant manifest. Slug entries resolve through the bundled catalog;
|
|
2796
|
+
* markdown entries (Agent Browser / inspiration extractor output) carry their
|
|
2797
|
+
* stored markdown verbatim. Returns null when the entry is missing or the
|
|
2798
|
+
* slug doesn't resolve to bundled markdown.
|
|
2799
|
+
*/
|
|
2800
|
+
const resolveDesignArtifact = (entry) => {
|
|
2801
|
+
if (!entry)
|
|
2802
|
+
return null;
|
|
2803
|
+
if (entry.kind === 'markdown') {
|
|
2804
|
+
return {
|
|
2805
|
+
markdown: entry.content,
|
|
2806
|
+
source: { kind: 'markdown', label: entry.label },
|
|
2807
|
+
};
|
|
2808
|
+
}
|
|
2809
|
+
const markdown = (0, designCatalog_1.loadDesignSystemMarkdown)(entry.slug);
|
|
2810
|
+
if (!markdown)
|
|
2811
|
+
return null;
|
|
2812
|
+
const catalogEntry = (0, designCatalog_1.getDesignSystemBySlug)(entry.slug);
|
|
2813
|
+
return {
|
|
2814
|
+
markdown,
|
|
2815
|
+
source: {
|
|
2816
|
+
kind: 'slug',
|
|
2817
|
+
slug: entry.slug,
|
|
2818
|
+
label: catalogEntry?.name ?? entry.slug,
|
|
2819
|
+
},
|
|
2820
|
+
};
|
|
2821
|
+
};
|
|
2164
2822
|
const summarizeDesignContext = (designContext) => {
|
|
2165
2823
|
if (!designContext)
|
|
2166
2824
|
return null;
|