rivet-design 0.10.2 → 0.10.4

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 (52) hide show
  1. package/dist/mcp/agent-variants/SessionStore.d.ts +23 -0
  2. package/dist/mcp/agent-variants/SessionStore.d.ts.map +1 -1
  3. package/dist/mcp/agent-variants/SessionStore.js +71 -2
  4. package/dist/mcp/agent-variants/SessionStore.js.map +1 -1
  5. package/dist/mcp/agent-variants/WorktreeOrchestrator.d.ts +26 -1
  6. package/dist/mcp/agent-variants/WorktreeOrchestrator.d.ts.map +1 -1
  7. package/dist/mcp/agent-variants/WorktreeOrchestrator.js +297 -139
  8. package/dist/mcp/agent-variants/WorktreeOrchestrator.js.map +1 -1
  9. package/dist/mcp/agent-variants/WorktreeOrchestrator.testHelpers.d.ts +5 -0
  10. package/dist/mcp/agent-variants/WorktreeOrchestrator.testHelpers.d.ts.map +1 -1
  11. package/dist/mcp/agent-variants/WorktreeOrchestrator.testHelpers.js +8 -0
  12. package/dist/mcp/agent-variants/WorktreeOrchestrator.testHelpers.js.map +1 -1
  13. package/dist/mcp/agent-variants/contracts.d.ts +26 -2
  14. package/dist/mcp/agent-variants/contracts.d.ts.map +1 -1
  15. package/dist/mcp/agent-variants/contracts.js +16 -4
  16. package/dist/mcp/agent-variants/contracts.js.map +1 -1
  17. package/dist/mcp/agent-variants/runLabel.d.ts +18 -0
  18. package/dist/mcp/agent-variants/runLabel.d.ts.map +1 -0
  19. package/dist/mcp/agent-variants/runLabel.js +146 -0
  20. package/dist/mcp/agent-variants/runLabel.js.map +1 -0
  21. package/dist/mcp/agent-variants/tools.d.ts.map +1 -1
  22. package/dist/mcp/agent-variants/tools.js +14 -5
  23. package/dist/mcp/agent-variants/tools.js.map +1 -1
  24. package/dist/mcp/server.d.ts.map +1 -1
  25. package/dist/mcp/server.js +0 -4
  26. package/dist/mcp/server.js.map +1 -1
  27. package/dist/routes/agentVariants.d.ts.map +1 -1
  28. package/dist/routes/agentVariants.js +27 -2
  29. package/dist/routes/agentVariants.js.map +1 -1
  30. package/dist/server.d.ts.map +1 -1
  31. package/dist/server.js +12 -13
  32. package/dist/server.js.map +1 -1
  33. package/dist/services/AuthService.d.ts.map +1 -1
  34. package/dist/services/AuthService.js +7 -0
  35. package/dist/services/AuthService.js.map +1 -1
  36. package/dist/services/VariantHistoryService.d.ts +5 -1
  37. package/dist/services/VariantHistoryService.d.ts.map +1 -1
  38. package/dist/services/VariantHistoryService.js +4 -0
  39. package/dist/services/VariantHistoryService.js.map +1 -1
  40. package/dist/services/WorktreeManager.d.ts.map +1 -1
  41. package/dist/services/WorktreeManager.js +30 -9
  42. package/dist/services/WorktreeManager.js.map +1 -1
  43. package/dist/utils/logger.d.ts +5 -5
  44. package/dist/utils/logger.d.ts.map +1 -1
  45. package/dist/utils/logger.js +27 -23
  46. package/dist/utils/logger.js.map +1 -1
  47. package/package.json +1 -1
  48. package/src/ui/dist/assets/main-COggU55U.js +645 -0
  49. package/src/ui/dist/assets/main-frxzIoK6.css +1 -0
  50. package/src/ui/dist/index.html +2 -2
  51. package/src/ui/dist/assets/main-C8QYh4jE.css +0 -1
  52. package/src/ui/dist/assets/main-CMk2ORlB.js +0 -645
@@ -25,6 +25,47 @@ const log = (0, logger_1.createLogger)('AgentVariantsOrchestrator');
25
25
  const FRESH_DEV_SERVER_HOST = '127.0.0.1';
26
26
  const DESIGN_CONTEXT_ROUTE_SEGMENT = 'design-md';
27
27
  const DESIGN_CONTEXT_VIEW_SEGMENT = 'view';
28
+ // Hard ceiling on worktree provisioning so a slow/large host project can never
29
+ // stall the `start_variants` / `approve_variant_briefs` tool call indefinitely.
30
+ // Existing-project provisioning clones the host working tree once per variant
31
+ // (`git worktree add` + untracked-file copy), so its cost scales with repo
32
+ // size — a big monorepo (or a React Native tree with native build dirs) used to
33
+ // hang the call for 15+ minutes with no signal. With this bound the call fails
34
+ // fast and legibly instead. Per-git-op hangs are already caught by simple-git's
35
+ // block timeout in WorktreeManager; this is the cross-op backstop. Override via
36
+ // env for unusually large repos.
37
+ const DEFAULT_PROVISION_TIMEOUT_MS = 180_000;
38
+ const PROVISION_TIMEOUT_MS = (() => {
39
+ const raw = process.env.RIVET_VARIANTS_PROVISION_TIMEOUT_MS;
40
+ const parsed = raw ? Number.parseInt(raw, 10) : NaN;
41
+ return Number.isFinite(parsed) && parsed > 0
42
+ ? parsed
43
+ : DEFAULT_PROVISION_TIMEOUT_MS;
44
+ })();
45
+ /** Sentinel so callers can distinguish a provisioning timeout from other errors. */
46
+ class ProvisionTimeoutError extends Error {
47
+ timeoutMs;
48
+ constructor(timeoutMs) {
49
+ super(`Worktree provisioning timed out after ${timeoutMs}ms`);
50
+ this.timeoutMs = timeoutMs;
51
+ this.name = 'ProvisionTimeoutError';
52
+ }
53
+ }
54
+ /**
55
+ * Reject with `ProvisionTimeoutError` if `work` hasn't settled within
56
+ * `timeoutMs`. The underlying work is NOT cancellable (git/fs I/O), so it may
57
+ * keep running after we reject — callers should best-effort clean up partial
58
+ * state. The timer is unref'd so it never keeps the process alive on its own.
59
+ */
60
+ function withProvisionTimeout(work, timeoutMs) {
61
+ let timer;
62
+ const timeout = new Promise((_, reject) => {
63
+ timer = setTimeout(() => reject(new ProvisionTimeoutError(timeoutMs)), timeoutMs);
64
+ if (typeof timer.unref === 'function')
65
+ timer.unref();
66
+ });
67
+ return Promise.race([work, timeout]).finally(() => clearTimeout(timer));
68
+ }
28
69
  /**
29
70
  * Allowlist of asset file extensions an agent-planned source may have.
30
71
  * `assetPlan` is sized for large local *assets* (3D models, images,
@@ -238,6 +279,7 @@ class AgentVariantsOrchestrator {
238
279
  setCommittedDevServerHealth;
239
280
  variantHistory;
240
281
  startStaticPreviewServerImpl;
282
+ provisionTimeoutMs;
241
283
  resources = new Map();
242
284
  /**
243
285
  * Committed dev servers from prior sessions that survived teardown. The
@@ -281,6 +323,8 @@ class AgentVariantsOrchestrator {
281
323
  this.variantHistory = deps.variantHistory ?? new VariantHistoryService_1.VariantHistoryService();
282
324
  this.startStaticPreviewServerImpl =
283
325
  deps.startStaticPreviewServer ?? StaticPreviewServer_1.startStaticPreviewServer;
326
+ this.provisionTimeoutMs =
327
+ deps.provisionTimeoutMs ?? PROVISION_TIMEOUT_MS;
284
328
  }
285
329
  // --- Pure delegations (no side effects) ---------------------------------
286
330
  propose(args) {
@@ -647,7 +691,7 @@ class AgentVariantsOrchestrator {
647
691
  const artifact = findDesignContextArtifact(this.store.getProjectContext(sessionId), artifactId);
648
692
  return artifact?.content;
649
693
  }
650
- /** Build the raw-plus-rendered DesignMD document for an artifact link. */
694
+ /** Build the rendered DesignMD document for an artifact link. */
651
695
  getDesignContextViewerHtml(sessionId, artifactId) {
652
696
  const artifact = findDesignContextArtifact(this.store.getProjectContext(sessionId), artifactId);
653
697
  return artifact ? buildDesignContextViewerDocument(artifact) : undefined;
@@ -802,6 +846,7 @@ class AgentVariantsOrchestrator {
802
846
  count,
803
847
  target: args.target,
804
848
  projectContext,
849
+ runLabel: args.runLabel,
805
850
  });
806
851
  if (proposeResult.stage !== 'awaiting_briefs' ||
807
852
  !proposeResult.briefWorkItem) {
@@ -1071,6 +1116,39 @@ class AgentVariantsOrchestrator {
1071
1116
  alreadyTerminal: result.alreadyTerminal,
1072
1117
  };
1073
1118
  }
1119
+ /**
1120
+ * Remove a variant from the UI. Hides it from the active snapshot for good
1121
+ * (it's filtered out of progress/summary/terminal accounting too) while
1122
+ * retaining it in `.rivet/variants/` with status `removed` so the work is
1123
+ * kept, never rendered again. Mirrors `cancelVariant`'s shape; idempotent.
1124
+ */
1125
+ async removeVariant(args) {
1126
+ const result = this.store.removeWorkItem({
1127
+ sessionId: args.sessionId,
1128
+ workItemId: args.variantId,
1129
+ });
1130
+ this.telemetry.track('agent_variants.variant_removed', {
1131
+ source: 'mcp',
1132
+ sessionId: args.sessionId,
1133
+ variantId: args.variantId,
1134
+ finalStatus: result.finalStatus,
1135
+ sessionStage: result.sessionStage,
1136
+ alreadyRemoved: result.alreadyRemoved,
1137
+ });
1138
+ if (!result.alreadyRemoved) {
1139
+ void this.markPersistedVariantRemoved(args.sessionId, args.variantId).catch((err) => {
1140
+ log.warn(`markPersistedVariantRemoved failed for ${args.sessionId}/${args.variantId}`, err);
1141
+ });
1142
+ }
1143
+ this.emitChange();
1144
+ return {
1145
+ sessionId: args.sessionId,
1146
+ variantId: args.variantId,
1147
+ finalStatus: result.finalStatus,
1148
+ sessionStage: result.sessionStage,
1149
+ alreadyRemoved: result.alreadyRemoved,
1150
+ };
1151
+ }
1074
1152
  /**
1075
1153
  * User has reviewed the rendered variants in chat and picked one. Look up
1076
1154
  * the captured diff, build a VariantPickEnvelope, and enqueue to the
@@ -1619,17 +1697,76 @@ class AgentVariantsOrchestrator {
1619
1697
  // --- Side-effect implementations ----------------------------------------
1620
1698
  async provisionWorktrees(sessionId, approveResult) {
1621
1699
  const projectContext = this.store.getProjectContext(sessionId);
1622
- if (projectContext.kind === 'fresh') {
1623
- // Fresh + static_preview: no worktrees, no scaffold, HTML is the
1624
- // deliverable. Fresh + vite_app: provision a Vite skeleton per variant
1625
- // and copy the agent-planned assetPlan files before the agent leases
1626
- // its code_gen items.
1627
- if (approveResult.scaffoldBaseWorkItemId) {
1700
+ const variantCount = approveResult.codeGenWorkItemIds.length +
1701
+ (approveResult.scaffoldBaseWorkItemId ? 1 : 0);
1702
+ // Fresh + static_preview provisions nothing (HTML is the deliverable) —
1703
+ // skip the timing/telemetry wrapper so the fast path stays untouched.
1704
+ if (projectContext.kind === 'fresh' && !approveResult.scaffoldBaseWorkItemId) {
1705
+ return;
1706
+ }
1707
+ const startedAt = Date.now();
1708
+ this.telemetry.track('agent_variants.provisioning_started', {
1709
+ source: 'mcp',
1710
+ sessionId,
1711
+ kind: projectContext.kind,
1712
+ variantCount,
1713
+ });
1714
+ const run = async () => {
1715
+ if (projectContext.kind === 'fresh') {
1716
+ // Fresh + vite_app: provision a Vite skeleton per variant and copy the
1717
+ // agent-planned assetPlan files before the agent leases its code_gen
1718
+ // items. (Background `npm install` is dispatched inside and is NOT
1719
+ // awaited here — the timeout bounds worktree creation, not install.)
1628
1720
  await this.provisionFreshWorktrees(sessionId, approveResult, projectContext);
1721
+ return;
1629
1722
  }
1630
- return;
1723
+ await this.provisionExistingWorktrees(sessionId, approveResult);
1724
+ };
1725
+ try {
1726
+ await withProvisionTimeout(run(), this.provisionTimeoutMs);
1727
+ this.telemetry.track('agent_variants.provisioning_completed', {
1728
+ source: 'mcp',
1729
+ sessionId,
1730
+ kind: projectContext.kind,
1731
+ variantCount,
1732
+ durationMs: Date.now() - startedAt,
1733
+ });
1734
+ }
1735
+ catch (err) {
1736
+ const timedOut = err instanceof ProvisionTimeoutError;
1737
+ this.telemetry.track('agent_variants.provisioning_failed', {
1738
+ source: 'mcp',
1739
+ sessionId,
1740
+ kind: projectContext.kind,
1741
+ variantCount,
1742
+ durationMs: Date.now() - startedAt,
1743
+ timedOut,
1744
+ errorCode: timedOut
1745
+ ? 'PROVISION_TIMEOUT'
1746
+ : err instanceof errors_1.AgentVariantsError
1747
+ ? err.code
1748
+ : 'PROVISION_FAILED',
1749
+ });
1750
+ // Best-effort cleanup of any worktrees created before the failure so a
1751
+ // timed-out clone doesn't leak half-provisioned dirs. Fire-and-forget so
1752
+ // the provisioning error returns to the caller immediately — never block
1753
+ // the error on teardown, which itself shells out to git (a wedged repo
1754
+ // could otherwise re-hang the tool on cleanup). The cleanup git calls are
1755
+ // bounded by GIT_OPTS in WorktreeManager, and the next-startup orphan
1756
+ // sweep catches anything created after this point.
1757
+ void this.teardownSession(sessionId, 'cancel').catch((teardownErr) => {
1758
+ log.warn(`cleanup after provisioning failure for ${sessionId} failed`, teardownErr);
1759
+ });
1760
+ if (timedOut) {
1761
+ throw new errors_1.AgentVariantsError('RUNTIME_VALIDATION_FAILED', `Provisioning ${variantCount} variant worktree(s) timed out after ` +
1762
+ `${Math.round(this.provisionTimeoutMs / 1000)}s. This usually means the ` +
1763
+ `project is very large or slow to clone (e.g. a big monorepo, or a ` +
1764
+ `React Native tree with native build dirs that aren't git-ignored). ` +
1765
+ `Try git-ignoring build artifacts, or generate standalone variants ` +
1766
+ `with create_zero_to_one_project instead of cloning this project.`);
1767
+ }
1768
+ throw err;
1631
1769
  }
1632
- await this.provisionExistingWorktrees(sessionId, approveResult);
1633
1770
  }
1634
1771
  /**
1635
1772
  * Existing-project flow: clone the user's repo N times via git worktree.
@@ -2120,6 +2257,7 @@ class AgentVariantsOrchestrator {
2120
2257
  label: input.briefLabel,
2121
2258
  brief: input.briefBody,
2122
2259
  sessionPrompt,
2260
+ runLabel: this.store.getRunLabel(args.sessionId),
2123
2261
  kind: 'diff',
2124
2262
  diff: args.diff,
2125
2263
  sourceDir,
@@ -2201,6 +2339,7 @@ class AgentVariantsOrchestrator {
2201
2339
  label: input.briefLabel,
2202
2340
  brief: input.briefBody,
2203
2341
  sessionPrompt,
2342
+ runLabel: this.store.getRunLabel(args.sessionId),
2204
2343
  kind: 'project-created',
2205
2344
  sourceDir,
2206
2345
  preview,
@@ -2285,6 +2424,22 @@ class AgentVariantsOrchestrator {
2285
2424
  status: 'cancelled',
2286
2425
  });
2287
2426
  }
2427
+ async markPersistedVariantRemoved(sessionId, variantId) {
2428
+ if (!this.store.hasSession(sessionId))
2429
+ return;
2430
+ const projectPath = await this.resolveHistoryProjectPath(sessionId);
2431
+ if (!projectPath)
2432
+ return;
2433
+ // No-ops (ENOENT) when the variant was never persisted — e.g. removing a
2434
+ // still-pending variant that produced no artifact. Completed variants have
2435
+ // a manifest, which flips to `removed` and is retained on disk.
2436
+ await this.variantHistory.markStatus({
2437
+ projectPath,
2438
+ sessionId,
2439
+ variantId,
2440
+ status: 'removed',
2441
+ });
2442
+ }
2288
2443
  /**
2289
2444
  * Resolve the project path that owns `.rivet/variants/` for a session.
2290
2445
  * Existing sessions: the user's project (via `resolveEnv`). Fresh sessions:
@@ -3102,8 +3257,7 @@ const buildDesignContextViewUrl = (sessionId, artifactId) => {
3102
3257
  };
3103
3258
  const buildDesignContextViewerDocument = (artifact) => {
3104
3259
  const title = `${artifact.label} DESIGN.md`;
3105
- const visualHtml = renderDesignMarkdown(artifact.content);
3106
- const rawMarkdown = escapeHtml(artifact.content);
3260
+ const visualHtml = renderDesignMarkdown(artifact.content, artifact.label);
3107
3261
  return `<!doctype html>
3108
3262
  <html lang="en">
3109
3263
  <head>
@@ -3112,131 +3266,131 @@ const buildDesignContextViewerDocument = (artifact) => {
3112
3266
  <title>${escapeHtml(title)}</title>
3113
3267
  <style>
3114
3268
  :root {
3115
- color-scheme: light;
3116
- --ink: #201b16;
3117
- --muted: #75695e;
3118
- --paper: #fbf7ef;
3119
- --panel: #fffdf8;
3120
- --rule: #eadfcf;
3121
- --accent: #e45d2f;
3122
- --accent-soft: #ffe0d3;
3123
- --code: #2a2521;
3269
+ color-scheme: dark;
3270
+ --font-main: 'Satoshi', 'Inter', system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif;
3271
+ --font-mono: ui-monospace, "SFMono-Regular", Menlo, Consolas, monospace;
3272
+ --main: #1C1C20;
3273
+ --main-light: #1E1E22;
3274
+ --main-input: #2E2E2E;
3275
+ --main-border: #232328;
3276
+ --main-hover: #404040;
3277
+ --content: #ffffff;
3278
+ --content-muted: #d1d5db;
3279
+ --content-subtle: #9ca3af;
3280
+ --divider: #4b5563;
3281
+ --primary: #FF3300;
3282
+ --primary-border: #ff6b35;
3124
3283
  }
3125
3284
  * { box-sizing: border-box; }
3126
3285
  body {
3127
3286
  margin: 0;
3128
3287
  min-height: 100vh;
3129
- background:
3130
- radial-gradient(circle at top left, rgba(228, 93, 47, 0.18), transparent 36rem),
3131
- linear-gradient(135deg, #fbf7ef 0%, #f2eadc 100%);
3132
- color: var(--ink);
3133
- font-family: ui-serif, Georgia, Cambria, "Times New Roman", serif;
3288
+ background: var(--main);
3289
+ color: var(--content);
3290
+ font-family: var(--font-main);
3134
3291
  }
3135
- main {
3136
- width: min(1440px, calc(100vw - 40px));
3137
- margin: 0 auto;
3138
- padding: 40px 0;
3292
+ .page-shell {
3293
+ min-height: 100vh;
3294
+ padding: 22px 28px 56px;
3139
3295
  }
3140
- header {
3296
+ .topbar {
3141
3297
  display: flex;
3142
3298
  align-items: center;
3143
3299
  justify-content: space-between;
3144
- gap: 24px;
3145
- margin-bottom: 24px;
3146
- border-bottom: 1px solid var(--rule);
3147
- padding-bottom: 18px;
3148
- }
3149
- h1 {
3150
- margin: 0;
3151
- max-width: 780px;
3152
- font-size: clamp(2rem, 5vw, 4.5rem);
3153
- letter-spacing: -0.06em;
3154
- line-height: 0.92;
3155
- }
3156
- .mode-input {
3157
- position: absolute;
3158
- opacity: 0;
3159
- pointer-events: none;
3300
+ margin-bottom: 28px;
3160
3301
  }
3161
- .mode-toggle {
3302
+ .brand {
3162
3303
  display: inline-flex;
3163
- flex-shrink: 0;
3164
- gap: 4px;
3165
- border: 1px solid var(--rule);
3166
- border-radius: 999px;
3167
- background: rgba(255, 253, 248, 0.72);
3168
- padding: 4px;
3169
- box-shadow: 0 12px 30px rgba(61, 44, 26, 0.08);
3170
- }
3171
- .mode-toggle label {
3172
- position: relative;
3173
- cursor: pointer;
3174
- border-radius: 999px;
3175
- padding: 8px 14px;
3176
- color: var(--muted);
3177
- font: 600 0.74rem/1.2 ui-sans-serif, system-ui, sans-serif;
3178
- letter-spacing: 0.12em;
3179
- text-transform: uppercase;
3180
- transition:
3181
- background 160ms ease,
3182
- color 160ms ease;
3183
- }
3184
- .mode-toggle label:has(input:checked) {
3185
- background: var(--ink);
3186
- color: var(--paper);
3304
+ align-items: center;
3305
+ min-height: 28px;
3187
3306
  }
3188
- .viewer {
3307
+ .brand-logo {
3189
3308
  display: block;
3309
+ width: 64px;
3310
+ height: auto;
3190
3311
  }
3191
- .viewer-panel {
3192
- display: none;
3312
+ .avatar {
3313
+ width: 28px;
3314
+ height: 28px;
3315
+ border: 1px solid var(--main-border);
3316
+ border-radius: 8px;
3317
+ background: var(--main-light);
3193
3318
  }
3194
- main:has(#designmd-visual-mode:checked) .visual-panel,
3195
- main:has(#designmd-raw-mode:checked) .raw-panel {
3196
- display: block;
3319
+ main {
3320
+ width: min(1120px, calc(100vw - 56px));
3321
+ margin: 0 auto;
3197
3322
  }
3198
- .panel {
3323
+ .document {
3199
3324
  overflow: hidden;
3200
- border: 1px solid var(--rule);
3201
- border-radius: 18px;
3202
- background: color-mix(in srgb, var(--panel) 92%, white);
3203
- box-shadow: 0 20px 60px rgba(61, 44, 26, 0.12);
3204
- }
3205
- .panel-title {
3206
- display: flex;
3207
- align-items: center;
3208
- justify-content: space-between;
3209
- border-bottom: 1px solid var(--rule);
3210
- padding: 12px 16px;
3211
- color: var(--muted);
3212
- font: 700 0.72rem/1 ui-sans-serif, system-ui, sans-serif;
3213
- letter-spacing: 0.12em;
3325
+ border: 1px solid var(--main-border);
3326
+ border-radius: 22px;
3327
+ background: var(--main-light);
3328
+ box-shadow: 0 18px 60px rgb(0 0 0 / 18%);
3329
+ }
3330
+ .document-title {
3331
+ padding: 42px 46px 32px;
3332
+ }
3333
+ .eyebrow {
3334
+ margin: 0 0 14px;
3335
+ color: var(--content-subtle);
3336
+ font-size: 0.68rem;
3337
+ font-weight: 700;
3338
+ letter-spacing: 0.16em;
3214
3339
  text-transform: uppercase;
3215
3340
  }
3341
+ .file-title {
3342
+ margin: 0;
3343
+ color: var(--content);
3344
+ font-size: clamp(2.35rem, 6vw, 4.9rem);
3345
+ font-weight: 600;
3346
+ letter-spacing: -0.07em;
3347
+ line-height: 0.94;
3348
+ }
3216
3349
  .visual {
3217
- padding: 24px;
3350
+ border-top: 1px solid var(--main-border);
3351
+ max-width: none;
3352
+ margin: 0 auto;
3353
+ padding: 36px 46px 48px;
3354
+ }
3355
+ .visual-inner {
3356
+ max-width: 720px;
3357
+ margin: 0 auto;
3218
3358
  }
3219
3359
  .visual h1,
3220
3360
  .visual h2,
3221
3361
  .visual h3 {
3222
- margin: 1.3em 0 0.45em;
3223
- letter-spacing: -0.04em;
3224
- line-height: 1;
3362
+ margin: 1.2em 0 0.4em;
3363
+ color: var(--content);
3364
+ font-weight: 600;
3365
+ letter-spacing: -0.02em;
3366
+ line-height: 1.15;
3225
3367
  }
3226
3368
  .visual h1:first-child,
3227
3369
  .visual h2:first-child,
3228
3370
  .visual h3:first-child {
3229
3371
  margin-top: 0;
3230
3372
  }
3231
- .visual h1 { font-size: 2.25rem; }
3232
- .visual h2 { font-size: 1.6rem; }
3233
- .visual h3 { font-size: 1.18rem; }
3373
+ .visual h1 { font-size: 1.25rem; }
3374
+ .visual h2 { font-size: 1.1rem; }
3375
+ .visual h3 { font-size: 1rem; }
3376
+ .visual h2 + p,
3377
+ .visual h2 + ul {
3378
+ border-left: 1px solid var(--main-border);
3379
+ padding-left: 14px;
3380
+ }
3234
3381
  .visual p,
3235
3382
  .visual li,
3236
3383
  .visual blockquote {
3237
- color: #3d342c;
3238
- font-size: 1rem;
3239
- line-height: 1.62;
3384
+ color: var(--content-muted);
3385
+ font-size: 0.88rem;
3386
+ line-height: 1.6;
3387
+ }
3388
+ .visual strong {
3389
+ color: var(--content);
3390
+ font-weight: 650;
3391
+ }
3392
+ .visual a {
3393
+ color: var(--primary-border);
3240
3394
  }
3241
3395
  .visual ul {
3242
3396
  display: grid;
@@ -3245,71 +3399,67 @@ const buildDesignContextViewerDocument = (artifact) => {
3245
3399
  }
3246
3400
  .visual blockquote {
3247
3401
  margin: 18px 0;
3248
- border-left: 4px solid var(--accent);
3402
+ border-left: 3px solid var(--primary);
3249
3403
  padding-left: 14px;
3250
- color: var(--muted);
3404
+ color: var(--content-subtle);
3251
3405
  }
3252
3406
  .visual code {
3253
3407
  border-radius: 6px;
3254
- background: var(--accent-soft);
3408
+ border: 1px solid var(--main-border);
3409
+ background: var(--main-input);
3410
+ color: var(--content);
3255
3411
  padding: 0.12rem 0.34rem;
3256
- font-family: ui-monospace, "SFMono-Regular", Menlo, Consolas, monospace;
3412
+ font-family: var(--font-mono);
3257
3413
  font-size: 0.92em;
3258
3414
  }
3259
3415
  pre {
3260
3416
  margin: 0;
3261
- max-height: calc(100vh - 190px);
3262
3417
  overflow: auto;
3263
- background: #181512;
3264
- color: #f9ead7;
3265
- padding: 20px;
3266
- font: 0.78rem/1.55 ui-monospace, "SFMono-Regular", Menlo, Consolas, monospace;
3418
+ background: var(--main);
3419
+ color: var(--content-muted);
3420
+ padding: 16px;
3421
+ font: 0.78rem/1.55 var(--font-mono);
3267
3422
  white-space: pre-wrap;
3268
3423
  word-break: break-word;
3269
3424
  }
3270
3425
  @media (max-width: 900px) {
3271
- main { width: min(100vw - 24px, 760px); padding: 24px 0; }
3272
- header { align-items: start; flex-direction: column; }
3273
- pre { max-height: 520px; }
3426
+ .page-shell { padding: 18px 12px 34px; }
3427
+ .topbar { margin-bottom: 18px; padding: 0 4px; }
3428
+ main { width: min(100vw - 24px, 760px); }
3429
+ .document { border-radius: 18px; }
3430
+ .document-title { padding: 30px 24px 24px; }
3431
+ .file-title { font-size: clamp(2rem, 12vw, 3.6rem); }
3432
+ .visual { padding: 26px 24px 34px; }
3274
3433
  }
3275
3434
  </style>
3276
3435
  </head>
3277
3436
  <body>
3278
- <main>
3279
- <header>
3280
- <h1>${escapeHtml(title)}</h1>
3281
- <div class="mode-toggle" aria-label="DesignMD view mode">
3282
- <label>
3283
- <input class="mode-input" type="radio" name="designmd-view-mode" id="designmd-visual-mode" checked />
3284
- <span>Visual</span>
3285
- </label>
3286
- <label>
3287
- <input class="mode-input" type="radio" name="designmd-view-mode" id="designmd-raw-mode" />
3288
- <span>Raw</span>
3289
- </label>
3290
- </div>
3437
+ <div class="page-shell">
3438
+ <header class="topbar" aria-label="Rivet">
3439
+ <div class="brand"><img class="brand-logo" src="/assets/logo.png" alt="Rivet" /></div>
3440
+ <div class="avatar" aria-hidden="true"></div>
3291
3441
  </header>
3292
- <section class="viewer" aria-label="DesignMD artifact">
3293
- <article class="panel viewer-panel visual-panel" id="designmd-visual">
3294
- <div class="panel-title"><span>Visual</span><span>Rendered DESIGN.md</span></div>
3295
- <div class="visual">${visualHtml}</div>
3296
- </article>
3297
- <article class="panel viewer-panel raw-panel" id="designmd-raw">
3298
- <div class="panel-title"><span>Raw</span><span>Markdown source</span></div>
3299
- <pre>${rawMarkdown}</pre>
3442
+ <main>
3443
+ <article class="document" id="designmd-visual" aria-label="DesignMD artifact">
3444
+ <div class="document-title">
3445
+ <p class="eyebrow">Design Context</p>
3446
+ <h1 class="file-title">${escapeHtml(title)}</h1>
3447
+ </div>
3448
+ <div class="visual"><div class="visual-inner">${visualHtml}</div></div>
3300
3449
  </article>
3301
- </section>
3302
- </main>
3450
+ </main>
3451
+ </div>
3303
3452
  </body>
3304
3453
  </html>`;
3305
3454
  };
3306
- const renderDesignMarkdown = (markdown) => {
3455
+ const renderDesignMarkdown = (markdown, artifactLabel) => {
3307
3456
  const lines = markdown.replace(/\r\n/g, '\n').split('\n');
3308
3457
  const output = [];
3309
3458
  const paragraph = [];
3310
3459
  const listItems = [];
3311
3460
  const codeLines = [];
3312
3461
  let isCodeBlock = false;
3462
+ let skippedFirstH1 = false;
3313
3463
  const flushParagraph = () => {
3314
3464
  if (paragraph.length === 0)
3315
3465
  return;
@@ -3361,7 +3511,15 @@ const renderDesignMarkdown = (markdown) => {
3361
3511
  flushParagraph();
3362
3512
  flushList();
3363
3513
  const level = Math.min(headingMatch[1].length, 3);
3364
- output.push(`<h${level}>${renderInlineMarkdown(headingMatch[2])}</h${level}>`);
3514
+ const headingText = headingMatch[2].trim();
3515
+ const isDuplicateTitle = headingText === artifactLabel ||
3516
+ headingText === `${artifactLabel} DESIGN.md`;
3517
+ // The viewer header already renders the artifact title.
3518
+ if (level === 1 && !skippedFirstH1 && isDuplicateTitle) {
3519
+ skippedFirstH1 = true;
3520
+ return;
3521
+ }
3522
+ output.push(`<h${level}>${renderInlineMarkdown(headingText)}</h${level}>`);
3365
3523
  return;
3366
3524
  }
3367
3525
  const listMatch = trimmed.match(/^[-*]\s+(.+)$/);