Skip to content

fix: ship polished stable Drops Studio release - #18

Merged
svg8bit merged 2 commits into
mainfrom
codex/drops-studio-v0-parity-hotfix
Jul 31, 2026
Merged

fix: ship polished stable Drops Studio release#18
svg8bit merged 2 commits into
mainfrom
codex/drops-studio-v0-parity-hotfix

Conversation

@svg8bit

@svg8bit svg8bit commented Jul 31, 2026

Copy link
Copy Markdown
Owner

Ships the final Drops Studio production hotfix: unified branding, responsive workspace, v0-like canvas controls, official integration marks, stable Connections navigation, honest capability states, Project V2 persistence repair, legacy adapter synchronization, and retained design QA evidence.

Verified: UI guardrails, ESLint, TypeScript, 760 unit passes with 2 opt-in skips, Next/Vercel build, Vinext/Cloudflare build, Storybook build plus 47 tests, focused Playwright at 1440/1024/390, three Lighthouse runs, npm audit, and CodeRabbit review. Visual baselines were not rewritten.

Summary by CodeRabbit

  • New Features

    • Project Studio now supports verified build evidence, secure HTTPS previews, project refreshes, zoom controls, and improved save/load behavior.
    • Added a richer radio-style starter experience with station selection, playback, editable rundowns, and archive content.
    • Integration cards now display recognizable provider logos.
    • Added responsive navigation footers across Studio and platform views.
  • Bug Fixes

    • Improved access verification protects billing, team, webhook, and collaboration actions.
    • Enhanced responsive layouts, scrolling, accessibility labels, and preview behavior.
  • Style

    • Refreshed Drops branding, typography, hero presentation, dashboard colors, and footer layouts.

@vercel

vercel Bot commented Jul 31, 2026

Copy link
Copy Markdown

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated (UTC)
drops-studio Ready Ready Preview Jul 31, 2026 2:38pm

Request Review

@coderabbitai

coderabbitai Bot commented Jul 31, 2026

Copy link
Copy Markdown

Review Change Stack

📝 Walkthrough

Walkthrough

The PR updates Drops Studio branding, responsive layouts, platform evidence access, Project V2 refresh and preview behavior, persistence, integration icons, and related tests and QA records.

Changes

Studio presentation and platform surfaces

Layer / File(s) Summary
Branding, navigation, and responsive layout
app/layout.tsx, app/page.tsx, app/styles/*, components/drops-brand.tsx, components/drops-studio.tsx, components/platform/*
Brand marks, footer layouts, integration logos, hero sizing, scroll behavior, and responsive Studio controls are updated.
Preview accessibility and contracts
components/preview-canvas.tsx, e2e/contracts/home-builder-p1.spec.ts, tests/product-reality.test.mjs
Preview labels now describe the visible product and data mode. Related assertions use the accessible labels.
QA and generated starter content
design-qa.md, lib/project-template-ui.ts
The design QA report is condensed. The generated radio starter gains playback, evidence, editing, archive, responsive layout, and theme styling.

Access verification and evidence views

Layer / File(s) Summary
Agent evidence initialization
app/internal/agent-evals/page.tsx, components/agent-eval-dashboard.tsx, components/agent-eval-dashboard.module.css
The page fetches initial platform evidence server-side. The dashboard restores that state on load failure and updates candidate-evidence terminology.
Protected webhook actions
components/dropsbot-webhook-connection.tsx, tests/dropsbot-webhook.test.mjs
Webhook data and mutations require verified access. Stale responses are ignored, and unauthorized responses clear protected state.
Account and team access gates
components/studio-account-team-panel.tsx, tests/studio-account-team-panel.test.mjs, tests/collaboration-transport-client.test.mjs
Billing, team, invite, collaboration, clipboard, and project actions require verified access. Unavailable loading is represented separately from signed-out state.

Project V2 refresh and generated templates

Layer / File(s) Summary
Project refresh and persistence flow
components/project-studio.tsx, lib/project-template-materializer.ts, lib/project-v2-migration.ts
Project V2 saves and loads refresh generated or legacy content, preserve non-generated state, update revisions and hashes, and validate refreshed projects.
Builder evidence and runtime preview
components/project-studio.tsx, app/styles/project-studio.*.css, tests/runtime-preview-security.test.mjs
Builder checks and validated HTTPS previews drive release status and iframe rendering. Runtime previews gain zoom, scrolling, and conditional sandbox behavior.
Generated radio project
lib/project-template-ui.ts, tests/project-template-materializer.test.mjs
The generated radio project uses configurable theme CSS and provides stations, speech playback, market evidence, editable rundowns, and archive content.
Director and E2E contracts
components/project-studio.tsx, e2e/proofs/director-flow.spec.ts, e2e/contracts/project-v2-studio.spec.ts
Free Director proposals become deterministic. Project persistence checks use per-project localStorage records.

Persistence and validation contracts

Layer / File(s) Summary
Compatibility index storage
lib/project-store.ts, tests/project-store.test.mjs
Legacy index entries omit embedded projectV2 data. Save ordering and rollback handling are updated, with quota coverage for large records.
Security and secret fixtures
lib/enterprise-platform/oidc-provider-route.ts, tests/*security*.test.mjs, e2e/contracts/release-boundaries.spec.ts
Demo-flow signatures require canonical encoding. Secret-like test values are assembled from fragments while preserving test behavior.

Estimated code review effort: 5 (Critical) | ~120 minutes

Possibly related PRs

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 5.13% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title accurately summarizes the PR as a stable Drops Studio release hotfix.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
✨ Finishing Touches 💡 1
📝 Generate docstrings 💡
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch codex/drops-studio-v0-parity-hotfix

Comment @coderabbitai help to get the list of available commands.

@svg8bit
svg8bit merged commit e534add into main Jul 31, 2026
2 of 4 checks passed

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 14

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (2)
components/dropsbot-webhook-connection.tsx (1)

69-138: 🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

Prevent stale refresh results during callback writes.

refreshVersion only orders concurrent refresh() calls. A manual refresh can start while createCallback() or mutateCallback() is pending. Its old 404 or event response can then apply after the write completes. This can re-enable callback creation after creation or restore evidence after revocation.

Use one operation-generation or cancellation mechanism for refreshes and writes. Disable refresh while creating or mutating is set. Add deferred-response tests for create, rotate, and revoke.

Also applies to: 151-152, 192-193

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@components/dropsbot-webhook-connection.tsx` around lines 69 - 138, Unify
refresh and callback-write concurrency in the component by using a shared
operation generation or cancellation mechanism across refresh, createCallback,
and mutateCallback, so responses started before a write cannot update state
afterward. Disable the manual refresh control while creating or mutating, and
ensure create, rotate, and revoke flows invalidate or supersede pending refresh
responses while preserving the existing loading and protected-state behavior.
app/styles/drops-studio.previews.css (1)

152-156: 📐 Maintainability & Code Quality | 🟡 Minor | ⚡ Quick win

Set .catcher-result button font size to at least 14px. The rule at app/styles/drops-studio.previews.css:155 sets control text to 12px. Later size rules do not change its font size.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@app/styles/drops-studio.previews.css` around lines 152 - 156, Update the
`.catcher-result button` rule in the catcher result styling to use a font size
of at least 14px, replacing the current 12px value while preserving its existing
layout and visual properties.
🧹 Nitpick comments (4)
components/project-studio.tsx (1)

528-559: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Derive total from the task list plus the preview check.

total is hardcoded to 5 in two places while the check set is PROJECT_V2_BUILD_TASKS.length + 1. If the task list changes, the reported counts become wrong.

♻️ Proposed refactor
+const PROJECT_V2_BUILD_CHECK_TOTAL = PROJECT_V2_BUILD_TASKS.length + 1;
+
 function projectV2BuildEvidence(projectV2?: ProjectV2): {
   passed: number;
   total: number;
   verified: boolean;
 } {
-  if (!projectV2) return { passed: 0, total: 5, verified: false };
+  if (!projectV2) {
+    return { passed: 0, total: PROJECT_V2_BUILD_CHECK_TOTAL, verified: false };
+  }
@@
-  return { passed, total: 5, verified: passed === 5 };
+  return {
+    passed,
+    total: PROJECT_V2_BUILD_CHECK_TOTAL,
+    verified: passed === PROJECT_V2_BUILD_CHECK_TOTAL,
+  };
 }
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@components/project-studio.tsx` around lines 528 - 559, Update
projectV2BuildEvidence to derive the total count from
PROJECT_V2_BUILD_TASKS.length plus one preview check, replacing both hardcoded 5
values while preserving the existing passed and verified behavior.
tests/runtime-preview-security.test.mjs (1)

99-104: 🔒 Security & Privacy | 🔵 Trivial | ⚡ Quick win

Extend the assertions with an origin check for the remote preview.

These assertions lock in allow-same-origin for URL-based previews and verify only the https: protocol check. They do not verify that the preview URL is rejected when it matches the Studio origin. That gap is the reason the risk raised on components/project-studio.tsx lines 4784-4801 is not caught here. After the origin guard is added, assert it in this test.

💚 Proposed assertion
   assert.match(studio, /function currentProjectV2PreviewUrl/);
   assert.match(studio, /url\.protocol === "https:"/);
+  assert.match(studio, /url\.origin === window\.location\.origin/);
   assert.doesNotMatch(studio, /allow-popups/);
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@tests/runtime-preview-security.test.mjs` around lines 99 - 104, Extend the
assertions around currentProjectV2PreviewUrl to verify the remote preview origin
guard, not only the existing https: protocol check. Assert that the preview URL
is rejected when its origin matches the Studio origin, while preserving the
existing sandbox attribute and protocol assertions.
lib/project-template-ui.ts (1)

463-468: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Make the theme injection fail loudly.

projectTemplateGlobalCss replaces one exact :root { … } string. If PROJECT_TEMPLATE_GLOBAL_CSS changes even by one character, String.prototype.replace returns the stylesheet unchanged and every generated project silently falls back to the default accent, surface, radius, and font. Extract the default block into a named constant and assert that the replacement occurred.

♻️ Proposed refactor
+const PROJECT_TEMPLATE_ROOT_DEFAULTS =
+  ':root { color-scheme: dark; --project-accent: `#67e8f9`; --project-surface: `#070a12`; --project-radius: 20px; --project-font: Inter, ui-sans-serif, system-ui, sans-serif; }';
+
 export function projectTemplateGlobalCss(spec: GeneratedProjectSpec): string {
-  return PROJECT_TEMPLATE_GLOBAL_CSS.replace(
-    ':root { color-scheme: dark; --project-accent: `#67e8f9`; --project-surface: `#070a12`; --project-radius: 20px; --project-font: Inter, ui-sans-serif, system-ui, sans-serif; }',
-    `:root { color-scheme: dark; --project-accent: ${spec.theme.accent}; --project-surface: ${spec.theme.surface}; --project-radius: ${spec.design.radius}px; --project-font: ${projectFontStack(spec.design.font)}; }`,
-  );
+  if (!PROJECT_TEMPLATE_GLOBAL_CSS.includes(PROJECT_TEMPLATE_ROOT_DEFAULTS)) {
+    throw new Error("The generated stylesheet no longer exposes the themable :root block.");
+  }
+  return PROJECT_TEMPLATE_GLOBAL_CSS.replace(
+    PROJECT_TEMPLATE_ROOT_DEFAULTS,
+    `:root { color-scheme: dark; --project-accent: ${spec.theme.accent}; --project-surface: ${spec.theme.surface}; --project-radius: ${spec.design.radius}px; --project-font: ${projectFontStack(spec.design.font)}; }`,
+  );
 }
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@lib/project-template-ui.ts` around lines 463 - 468, Update
projectTemplateGlobalCss and PROJECT_TEMPLATE_GLOBAL_CSS handling to extract the
expected default :root block into a named constant, reuse it for replacement,
and verify that the stylesheet changed; throw an explicit error when the
replacement target is missing so theme injection cannot silently fall back to
defaults.
tests/project-v2-migration.test.mjs (1)

68-124: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Add coverage for preserved manual files and for run history.

The test asserts metadata preservation only. The adapter's stated purpose is to keep manual and AI-authored files while regenerating generated files, and to keep runs, logs, and checkpoints. Two gaps remain:

  • No file with provenance other than generated exists in configured, so the preservation branch at lib/project-v2-migration.ts lines 356-375 is never exercised.
  • configured.runs stays empty, so the case where a preserved run references a task id that the refreshed baseline does not define is untested. That case is the failure raised on lib/project-v2-migration.ts lines 378-399.

Add one manual file and one run plus matching task to the fixture.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@tests/project-v2-migration.test.mjs` around lines 68 - 124, Add coverage for
two untested preservation branches in the refreshLegacyProjectV2Migration
function by extending the configured fixture used in this test. First, add one
file to the configured.files object with a provenance property set to a value
other than "generated" (such as "manual") to exercise the file preservation code
path at lib/project-v2-migration.ts lines 356-375. Second, add at least one run
object to configured.runs with a taskId that matches an existing task in
configured.tasks, then verify in the test assertions that this run and its
associated task are preserved in the refreshed result to cover the runs
preservation validation at lib/project-v2-migration.ts lines 378-399.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@app/styles/project-studio.runtime.css`:
- Around line 6-14: The stage toolbar and zoom controls do not meet
accessibility standards for text size and interactive target dimensions. In
app/styles/project-studio.runtime.css lines 6-14, increase the .stage-toolbar
button font-size from 12px to 14px, increase the .stage-toolbar button
min-height from 29px to 44px to meet the minimum interactive target size, and
increase the .canvas-zoom-controls button min-width from 38px to 44px. In
app/styles/project-studio.workspace.css line 29, remove the 12px font-size
override or increase it to 14px to align with the minimum text size requirement.

In `@app/styles/project-studio.workspace.css`:
- Line 29: Update the .stage-toolbar button rule to use a font size of at least
14px instead of 12px, while preserving the existing toolbar sizing and spacing.

In `@components/platform/integration-catalog.tsx`:
- Around line 12-19: Replace the provider icon imports in the integration
catalog with project-local image assets rendered through next/image for official
provider marks, and use lucide-react for any generic symbols. Remove the
SiAnthropic, SiGithub, SiMoonshotai, SiOpenrouter, SiVercel, and TbBrandOpenai
react-icons imports while preserving the existing provider display behavior.

In `@components/platform/platform-shell.tsx`:
- Around line 67-71: Update the footer description paragraph that begins with
"Real crypto products powered by DropsTab..." to use body-size text styling.
Replace the `text-sm` class in the `<p>` element's className with `text-base` to
achieve the intended 16-18 px font size for the description text.

In `@components/preview-canvas.tsx`:
- Around line 290-294: Add visible text indicators to the eight preview variants
that currently lack data-mode labels: EnginePreview, PredictionPreview,
CopyPreview, AggregatorPreview, GamePreview, CompanionPreview,
TamagotchiPreview, and HuntPreview. For each variant, render an in-frame label
(such as "Sample data" or "Live data") conditionally based on the dataMode state
variable, similar to the existing implementation in ChannelPreview and
RadioPreview. This ensures sighted users can see whether the preview is
displaying live or sample data, matching the information already provided in the
aria-label attribute on the section wrapper.

In `@components/project-studio.tsx`:
- Around line 1332-1339: Update the browserTelemetryReady calculation in the
project studio status logic so it is true only when the browser smoke result is
executed in browser mode with runtime evidence; do not include
runtimePreviewUrl. Keep preview readiness separate and use a preview-specific
status label when only the URL is available, without treating either state as
provider evidence.
- Around line 4784-4801: Update currentProjectV2PreviewUrl validation to reject
HTTPS URLs whose origin matches window.location.origin, allowing only
foreign-origin preview URLs to reach the iframe. Preserve valid external preview
handling, and ensure the iframe sandbox in the runtimePreviewUrl path never
grants allow-same-origin to a same-origin URL.

In `@design-qa.md`:
- Around line 3-16: Update the visual evidence section in design-qa.md to link
matched reference-and-actual capture pairs for both 1024 px and 390 px
viewports, using browser zoom 100% and device scale factor 1. Retain the
existing desktop evidence, and only keep “final result: passed” after both
required viewport pairs are documented.

In `@lib/project-store.ts`:
- Around line 156-171: Update the save flow around the compact index and
canonical item writes so each entry in evictedItems is removed immediately after
writing compatibilityIndex and before writing the new canonical item, recording
removedItems for rollback. Adjust the catch rollback order to restore the
canonical item first, then removed items, and finally the original
PROJECTS_STORAGE_KEY index. Add a quota test covering a new project save that
requires eviction.

In `@lib/project-template-materializer.ts`:
- Around line 397-407: The metadata merge around next.manifest must preserve
consistency with a retained manually edited package.json. After applying the
fresh metadata in this refresh flow, re-synchronize manifest from the resulting
package.json using the existing syncManifestFromPackage behavior, or avoid
overwriting package.json-derived manifest fields while merging fresh.manifest.
- Around line 339-359: The refresh adapters build operation arrays without
enforcing FILE_OPERATION_LIMIT before calling applyProjectV2FileOperations. In
lib/project-template-materializer.ts lines 339-359, update
refreshGeneratedProjectV2Template to validate or batch operations before the
applyProjectV2FileOperations call; in lib/project-v2-migration.ts lines 368-375,
apply the same limit handling to preservedOperations in
refreshLegacyProjectV2Migration. Ensure neither adapter passes more than
FILE_OPERATION_LIMIT operations in a single call.

In `@lib/project-template-ui.ts`:
- Line 396: Update the generated body-copy styles in the radio player and
archive sections: change the `.radio-player > p:not(.radio-track-label,
.radio-playback)` and `.radio-archive p` font sizes to comply with the required
16–18px body-text range, while leaving helper and metadata styles unchanged.
- Around line 181-202: Import useEffect in the generated component template and
add an unmount cleanup effect that calls window.speechSynthesis.cancel() when
browser speech is available. Place the cleanup alongside togglePlayback so any
queued or active utterance stops when the component unmounts, while preserving
the existing playback behavior.

In `@lib/project-v2-migration.ts`:
- Around line 378-399: Update the ProjectV2 construction in the migration
refresh flow to keep task references consistent with preserved runs, logs, and
checkpoints. Preserve current.tasks alongside the existing current state fields,
or filter out persisted runs and related logs whose task IDs are absent from
refreshed.tasks before validation; ensure validateProjectV2 cannot encounter
unknown task references.

---

Outside diff comments:
In `@app/styles/drops-studio.previews.css`:
- Around line 152-156: Update the `.catcher-result button` rule in the catcher
result styling to use a font size of at least 14px, replacing the current 12px
value while preserving its existing layout and visual properties.

In `@components/dropsbot-webhook-connection.tsx`:
- Around line 69-138: Unify refresh and callback-write concurrency in the
component by using a shared operation generation or cancellation mechanism
across refresh, createCallback, and mutateCallback, so responses started before
a write cannot update state afterward. Disable the manual refresh control while
creating or mutating, and ensure create, rotate, and revoke flows invalidate or
supersede pending refresh responses while preserving the existing loading and
protected-state behavior.

---

Nitpick comments:
In `@components/project-studio.tsx`:
- Around line 528-559: Update projectV2BuildEvidence to derive the total count
from PROJECT_V2_BUILD_TASKS.length plus one preview check, replacing both
hardcoded 5 values while preserving the existing passed and verified behavior.

In `@lib/project-template-ui.ts`:
- Around line 463-468: Update projectTemplateGlobalCss and
PROJECT_TEMPLATE_GLOBAL_CSS handling to extract the expected default :root block
into a named constant, reuse it for replacement, and verify that the stylesheet
changed; throw an explicit error when the replacement target is missing so theme
injection cannot silently fall back to defaults.

In `@tests/project-v2-migration.test.mjs`:
- Around line 68-124: Add coverage for two untested preservation branches in the
refreshLegacyProjectV2Migration function by extending the configured fixture
used in this test. First, add one file to the configured.files object with a
provenance property set to a value other than "generated" (such as "manual") to
exercise the file preservation code path at lib/project-v2-migration.ts lines
356-375. Second, add at least one run object to configured.runs with a taskId
that matches an existing task in configured.tasks, then verify in the test
assertions that this run and its associated task are preserved in the refreshed
result to cover the runs preservation validation at lib/project-v2-migration.ts
lines 378-399.

In `@tests/runtime-preview-security.test.mjs`:
- Around line 99-104: Extend the assertions around currentProjectV2PreviewUrl to
verify the remote preview origin guard, not only the existing https: protocol
check. Assert that the preview URL is rejected when its origin matches the
Studio origin, while preserving the existing sandbox attribute and protocol
assertions.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: 16bfa465-0d9e-4ffe-8ea4-5746d7276960

📥 Commits

Reviewing files that changed from the base of the PR and between 05108df and 34b37ce.

⛔ Files ignored due to path filters (5)
  • docs/design/current-home-actual.png is excluded by !**/*.png
  • docs/design/current-integrations-actual.png is excluded by !**/*.png
  • docs/design/current-integrations-reference-vs-actual.png is excluded by !**/*.png
  • docs/design/current-studio-actual.png is excluded by !**/*.png
  • package-lock.json is excluded by !**/package-lock.json
📒 Files selected for processing (42)
  • app/internal/agent-evals/page.tsx
  • app/layout.tsx
  • app/page.tsx
  • app/styles/drops-brand.css
  • app/styles/drops-studio.builder.css
  • app/styles/drops-studio.previews.css
  • app/styles/drops-studio.responsive.css
  • app/styles/project-studio.inspector.css
  • app/styles/project-studio.runtime.css
  • app/styles/project-studio.workspace.css
  • components/agent-eval-dashboard.module.css
  • components/agent-eval-dashboard.tsx
  • components/drops-brand.tsx
  • components/drops-studio.tsx
  • components/dropsbot-webhook-connection.tsx
  • components/platform/integration-catalog.tsx
  • components/platform/platform-shell.tsx
  • components/preview-canvas.tsx
  • components/project-studio.tsx
  • components/studio-account-team-panel.tsx
  • design-qa.md
  • e2e/contracts/home-builder-p1.spec.ts
  • e2e/contracts/project-v2-studio.spec.ts
  • e2e/contracts/release-boundaries.spec.ts
  • e2e/proofs/director-flow.spec.ts
  • lib/enterprise-platform/oidc-provider-route.ts
  • lib/project-store.ts
  • lib/project-template-materializer.ts
  • lib/project-template-ui.ts
  • lib/project-v2-migration.ts
  • package.json
  • tests/agent-context-rag.test.mjs
  • tests/artifact-security.test.mjs
  • tests/collaboration-transport-client.test.mjs
  • tests/dropsbot-webhook.test.mjs
  • tests/product-reality.test.mjs
  • tests/project-export-portability.test.mjs
  • tests/project-store.test.mjs
  • tests/project-template-materializer.test.mjs
  • tests/project-v2-migration.test.mjs
  • tests/runtime-preview-security.test.mjs
  • tests/studio-account-team-panel.test.mjs
💤 Files with no reviewable changes (1)
  • app/page.tsx

Comment on lines 6 to +14
.stage-toolbar button { align-items: center; background: transparent; border: 1px solid transparent; border-radius: 7px; color: #718199; display: flex; font-size: 12px; font-weight: 710; gap: 5px; min-height: 29px; padding: 0 8px; }
.stage-toolbar button:hover,.stage-toolbar button.active { background: white; border-color: #d4deeb; color: #2f63c8; }
.stage-toolbar button.quality-ready { background: #e8f8f1; border-color: #bfe4d4; color: #16875b; }
.stage-toolbar svg { height: 12px; width: 12px; }
.device-switch { background: #e3e9f2; border-radius: 8px; padding: 2px; }
.device-switch button.active { box-shadow: 0 2px 5px rgba(28,46,75,.1); }
.runtime-browser { background: white; border: 1px solid #d4dfec; border-radius: 14px; box-shadow: 0 14px 36px rgba(28,48,80,.1); height: calc(100% - 46px); margin: auto; min-height: 590px; overflow: auto; transition: .2s ease; width: 100%; }
.canvas-zoom-controls { align-items: center; background: #e3e9f2; border-radius: 8px; display: inline-flex; flex: 0 0 auto; padding: 2px; }
.canvas-zoom-controls button { justify-content: center; min-width: 38px; padding-inline: 7px; }
.canvas-zoom-controls button:nth-child(2) { min-width: 58px; }

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🎯 Functional Correctness | 🟠 Major | ⚡ Quick win

Make the shared stage toolbar accessible.

Both style layers set stage-toolbar text below 14px. The runtime layer also permits targets smaller than 44px by 44px.

  • app/styles/project-studio.runtime.css#L6-L14: set toolbar text to at least 14px and set every toolbar and zoom button to at least 44px by 44px.
  • app/styles/project-studio.workspace.css#L29-L29: remove the 12px override or raise it to at least 14px.

As per coding guidelines, “control text [must be] at least 14 px” and “Every visible interactive target must be at least 44 by 44 CSS pixels.”

📍 Affects 2 files
  • app/styles/project-studio.runtime.css#L6-L14 (this comment)
  • app/styles/project-studio.workspace.css#L29-L29
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@app/styles/project-studio.runtime.css` around lines 6 - 14, The stage toolbar
and zoom controls do not meet accessibility standards for text size and
interactive target dimensions. In app/styles/project-studio.runtime.css lines
6-14, increase the .stage-toolbar button font-size from 12px to 14px, increase
the .stage-toolbar button min-height from 29px to 44px to meet the minimum
interactive target size, and increase the .canvas-zoom-controls button min-width
from 38px to 44px. In app/styles/project-studio.workspace.css line 29, remove
the 12px font-size override or increase it to 14px to align with the minimum
text size requirement.

Source: Coding guidelines

.checkpoint-list { gap: 9px; }.checkpoint-list > button { padding: 12px; }.checkpoint-list strong { font-size: 12px; }.checkpoint-list small { font-size: 12px; }.checkpoint-list > button > b { font-size: 12px; }
.runtime-stage { background: #eef2f8; overflow: auto; padding: 13px 16px 16px; }.stage-toolbar { height: 44px; margin-bottom: 10px; }.stage-toolbar button { font-size: 12px; min-height: 44px; padding: 0 10px; }.stage-toolbar svg { height: 14px; width: 14px; }
.runtime-browser { height: calc(100% - 54px); min-height: 600px; }.browser-bar { height: 38px; }.browser-bar > strong { font-size: 12px; min-width: 180px; }.browser-bar > b { font-size: 12px; }.runtime-browser iframe { height: calc(100% - 38px); }
.runtime-stage { background: #eef2f8; overflow: hidden; padding: 10px 12px 12px; }.stage-toolbar { height: 44px; margin-bottom: 10px; }.stage-toolbar button { font-size: 12px; min-height: 44px; padding: 0 10px; }.stage-toolbar svg { height: 14px; width: 14px; }

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

Use at least 14px toolbar text.

Line 29 overrides toolbar button text to 12px. Increase it to at least 14px.

As per coding guidelines, “control text [must be] at least 14 px.”

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@app/styles/project-studio.workspace.css` at line 29, Update the
.stage-toolbar button rule to use a font size of at least 14px instead of 12px,
while preserving the existing toolbar sizing and spacing.

Source: Coding guidelines

Comment on lines +12 to +19
import {
SiAnthropic,
SiGithub,
SiMoonshotai,
SiOpenrouter,
SiVercel,
} from "react-icons/si";
import { TbBrandOpenai } from "react-icons/tb";

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

📐 Maintainability & Code Quality | 🟡 Minor | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

printf '%s\n' '--- status ---'
git status -sb
printf '%s\n' '--- target file ---'
cat -n components/platform/integration-catalog.tsx | sed -n '1,120p'
printf '%s\n' '--- icon dependencies and usages ---'
rg -n '"(lucide-react|react-icons)"|from "react-icons|from '\''react-icons|SiAnthropic|SiGithub|SiMoonshotai|SiOpenrouter|SiVercel|TbBrandOpenai' --glob '!node_modules/**' --glob '!dist/**' .
printf '%s\n' '--- project-local image/icon conventions ---'
rg -n 'next/image|<Image|lucide-react|Icon' components app src 2>/dev/null | head -200

Repository: svg8bit/drops-studio

Length of output: 25537


🏁 Script executed:

printf '%s\n' '--- provider assets ---'
fd -HI -t f . public 2>/dev/null | rg -i '(openai|anthropic|openrouter|moonshot|kimi|github|vercel|provider|brand|logo)'
printf '%s\n' '--- deterministic import/use check ---'
python3 - <<'PY'
from pathlib import Path
p = Path("components/platform/integration-catalog.tsx")
text = p.read_text()
react_icons = [line.strip() for line in text.splitlines() if "react-icons" in line]
lucide = [line.strip() for line in text.splitlines() if 'from "lucide-react"' in line]
print({"react_icons_imports": react_icons, "lucide_imports": lucide})
print("react_icons_used_for_catalog_entries:",
      all(name in text for name in ("TbBrandOpenai", "SiAnthropic", "SiGithub",
                                    "SiMoonshotai", "SiOpenrouter", "SiVercel")))
PY

Repository: svg8bit/drops-studio

Length of output: 514


Replace the react-icons provider marks.

Use project-local image assets with next/image for official marks. Use lucide-react for generic symbols. Remove the react-icons imports.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@components/platform/integration-catalog.tsx` around lines 12 - 19, Replace
the provider icon imports in the integration catalog with project-local image
assets rendered through next/image for official provider marks, and use
lucide-react for any generic symbols. Remove the SiAnthropic, SiGithub,
SiMoonshotai, SiOpenrouter, SiVercel, and TbBrandOpenai react-icons imports
while preserving the existing provider display behavior.

Source: Coding guidelines

Comment on lines +67 to +71
<div className="mx-auto grid w-full max-w-[1500px] gap-8 px-4 py-10 sm:px-6 md:grid-cols-[minmax(220px,.8fr)_minmax(0,1.2fr)] lg:px-8">
<div className="flex min-w-0 flex-col items-start gap-4">
<DropsBrand compact />
<p className="max-w-sm text-sm leading-6 text-[#52617a]">Real crypto products powered by DropsTab intelligence and approved Drops Bot delivery.</p>
<span className="inline-flex items-start gap-2 rounded-xl bg-[#eef4ff] p-3 text-xs leading-5 text-[#52617a]"><ShieldCheck className="mt-0.5 size-4 shrink-0 text-[#245fe5]" aria-hidden="true" />Provider and deployment states appear only with matching evidence.</span>

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
set -eu
printf '%s\n' '--- target lines ---'
sed -n '60,75p' components/platform/platform-shell.tsx
printf '%s\n' '--- Tailwind/config files ---'
git ls-files | grep -E '(^|/)(tailwind|postcss|components.json|package.json)' | head -80
printf '%s\n' '--- text-sm definitions/usages ---'
rg -n --glob '!node_modules' --glob '!dist' --glob '!build' 'text-sm|font-size' .

Repository: svg8bit/drops-studio

Length of output: 50377


🏁 Script executed:

#!/bin/bash
set -eu
printf '%s\n' '--- package and Tailwind entry ---'
cat package.json
cat app/styles/tailwind.css
printf '%s\n' '--- global typography tokens and overrides ---'
cat app/styles/drops-studio.guardrails.css
printf '%s\n' '--- relevant selectors for the footer description ---'
rg -n -C 3 'footer|font-size|--font-body|text-sm' components/platform/platform-shell.tsx app/styles

Repository: svg8bit/drops-studio

Length of output: 50377


Use body-size text for the footer description.

Line 70 renders at 14 px because text-sm is not overridden outside .studio-shell. Use text-base or an equivalent 16–18 px token.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@components/platform/platform-shell.tsx` around lines 67 - 71, Update the
footer description paragraph that begins with "Real crypto products powered by
DropsTab..." to use body-size text styling. Replace the `text-sm` class in the
`<p>` element's className with `text-base` to achieve the intended 16-18 px font
size for the description text.

Source: Coding guidelines

Comment on lines +290 to +294
<section
className="preview-column"
aria-label={`${visibleName} ${dataMode === "live" ? "with live DropsTab data" : "with labelled sample data"}`}
aria-live="polite"
>

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🎯 Functional Correctness | 🟠 Major | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
set -euo pipefail

rg -n -C 4 'dataMode|sample data|live DropsTab|labelled sample' \
  components/preview-canvas.tsx \
  components/preview-canvas-variants.tsx \
  components/brief-preview.tsx 2>/dev/null || true

Repository: svg8bit/drops-studio

Length of output: 13529


🏁 Script executed:

#!/bin/bash
set -euo pipefail

# Find all preview variant types by looking at the switch statement routing
echo "=== All preview variant cases ==="
rg -A 1 'case "' components/preview-canvas-variants.tsx | grep -E '(case|return)' | head -20

echo ""
echo "=== Checking for visible data-mode text in all variants ==="
# Search for all render/return statements with dataMode in variants file
rg -B 5 'dataMode.*live|dataMode.*sample' components/preview-canvas-variants.tsx | grep -E '(^[0-9]+-.*function|dataMode)' | head -30

Repository: svg8bit/drops-studio

Length of output: 1233


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "=== Variants that DON'T receive dataMode ==="
rg -A 8 'function EnginePreview|function CopyPreview|function CompanionPreview|function TamagotchiPreview|function HuntPreview' components/preview-canvas-variants.tsx | grep -E '(function|dataMode|{$)' 

echo ""
echo "=== AggregatorPreview signature and render ==="
rg -A 15 'function AggregatorPreview' components/preview-canvas-variants.tsx | head -20

echo ""
echo "=== GamePreview signature and render ==="
rg -A 15 'function GamePreview' components/preview-canvas-variants.tsx | head -20

echo ""
echo "=== PredictionPreview signature and visible dataMode text ==="
rg -A 25 'function PredictionPreview' components/preview-canvas-variants.tsx | grep -E '(function|dataMode|return|"SAMPLE|live DropsTab)'

Repository: svg8bit/drops-studio

Length of output: 1461


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "=== PredictionPreview full signature and body ==="
rg -A 40 'function PredictionPreview' components/preview-canvas-variants.tsx

echo ""
echo "=== Routes that pass dataMode to variants ==="
rg -B 2 'dataMode=' components/preview-canvas-variants.tsx | grep -E '(case|dataMode)'

Repository: svg8bit/drops-studio

Length of output: 1574


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "=== All variant functions and their parameters ==="
rg 'function \w+Preview\(' components/preview-canvas-variants.tsx -A 8 | grep -E '(function|dataMode|{$)' 

echo ""
echo "=== Check which variants are routed and if they get dataMode passed ==="
rg -B 1 'return \(<EnginePreview|return \(<CopyPreview|return \(<CompanionPreview|return \(<AggregatorPreview|return \(<GamePreview|return \(<PredictionPreview' components/preview-canvas-variants.tsx | head -40

Repository: svg8bit/drops-studio

Length of output: 673


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "=== PreviewCanvasVariants routing - which variants are called and with what params ==="
rg -n 'case "' components/preview-canvas-variants.tsx | head -15

echo ""
echo "=== Detailed check: case statements with their returns ==="
sed -n '1215,1290p' components/preview-canvas-variants.tsx

Repository: svg8bit/drops-studio

Length of output: 2363


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "=== Check if EnginePreview, CopyPreview, etc. render any visible data-mode indicator ==="
rg -A 30 'function EnginePreview' components/preview-canvas-variants.tsx | head -40

echo ""
echo "=== Check SiriPreview for visible data mode ==="
rg -A 30 'function SiriPreview' components/preview-canvas-variants.tsx | head -40

Repository: svg8bit/drops-studio

Length of output: 2491


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "=== Check CopyPreview, AggregatorPreview, GamePreview render logic ==="
rg -A 25 'function CopyPreview|function AggregatorPreview|function GamePreview' components/preview-canvas-variants.tsx | grep -E '(function|return \(|className=|<span|<div|SAMPLE|live|data)'

echo ""
echo "=== Verify: does any non-dataMode variant render text about data source/mode? ==="
rg 'function \w+Preview' components/preview-canvas-variants.tsx -A 50 | grep -E '(function (Engine|Copy|Aggregator|Game|Companion|Tamagotchi|Hunt|Siri|Prediction)Preview|sample|live|DropsTab)' | head -20

Repository: svg8bit/drops-studio

Length of output: 1468


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "=== Final verification: which variants receive dataMode in the routing switch ==="
grep -n 'dataMode=' components/preview-canvas-variants.tsx | grep -E 'ChannelPreview|RadioPreview|PredictionPreview|EnginePreview|CopyPreview'

echo ""
echo "=== Count: how many case statements total? ==="
rg 'case "' components/preview-canvas-variants.tsx | wc -l

echo ""
echo "=== Confirm: EnginePreview, CopyPreview, Prediction, Game, Aggregator, Companion, Tamagotchi, Hunt, Siri do NOT receive dataMode ==="
rg -c 'dataMode' components/preview-canvas-variants.tsx

Repository: svg8bit/drops-studio

Length of output: 240


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "=== Summary: routing table with dataMode parameter ==="
sed -n '1229,1290p' components/preview-canvas-variants.tsx | grep -E '(case|<\w+Preview|dataMode)'

Repository: svg8bit/drops-studio

Length of output: 974


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "=== Confirm: EnginePreview through SiriPreview return structures (no dataMode checks) ==="
for variant in EnginePreview PredictionPreview CopyPreview AggregatorPreview GamePreview CompanionPreview TamagotchiPreview HuntPreview SiriPreview; do
  echo "--- $variant ---"
  rg -A 1 "function $variant" components/preview-canvas-variants.tsx | head -2
done

echo ""
echo "=== Double-check: does any of these 9 variants render visible 'sample' or 'live' text? ==="
rg 'function (Engine|Prediction|Copy|Aggregator|Game|Companion|Tamagotchi|Hunt|Siri)Preview' components/preview-canvas-variants.tsx -A 80 | grep -i 'sample\|live\|dropsTab' | head -20

Repository: svg8bit/drops-studio

Length of output: 1471


Add visible data-mode labels to eight preview variants that lack them.

The section wrapper (lines 290–294) exposes the live versus sample status only through the aria-label attribute, which is not visible to sighted users. Of the nine preview variants, only ChannelPreview and RadioPreview render visible text indicating data mode. Eight variants lack any visible label: EnginePreview, PredictionPreview, CopyPreview, AggregatorPreview, GamePreview, CompanionPreview, TamagotchiPreview, and HuntPreview. Each of these variants must include an in-frame indicator such as "Sample data" or "Live data" to comply with the guideline "do not misrepresent mockups, previews, static screens, or handoffs as completed products or integrations."

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@components/preview-canvas.tsx` around lines 290 - 294, Add visible text
indicators to the eight preview variants that currently lack data-mode labels:
EnginePreview, PredictionPreview, CopyPreview, AggregatorPreview, GamePreview,
CompanionPreview, TamagotchiPreview, and HuntPreview. For each variant, render
an in-frame label (such as "Sample data" or "Live data") conditionally based on
the dataMode state variable, similar to the existing implementation in
ChannelPreview and RadioPreview. This ensures sighted users can see whether the
preview is displaying live or sample data, matching the information already
provided in the aria-label attribute on the section wrapper.

Source: Coding guidelines

Comment on lines +339 to +359
const operations: Parameters<typeof applyProjectV2FileOperations>[2][number][] = [];

for (const [path, file] of Object.entries(project.files)) {
if (file.provenance === "generated" && !fresh.files[path]) {
operations.push({ type: "delete", path });
}
}
for (const [path, file] of Object.entries(fresh.files)) {
const current = project.files[path];
if (current && current.provenance !== "generated") continue;
if (current?.hash === file.hash) continue;
operations.push({
type: "write",
path,
content: file.content,
language: file.language,
role: file.role,
provenance: "generated",
editable: file.editable,
});
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🩺 Stability & Availability | 🟠 Major | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Description: Read FILE_OPERATION_LIMIT and count generated template file definitions.
rg -n 'FILE_OPERATION_LIMIT\s*=' lib/project-v2-files.ts
rg -n -c '\{ path: "' lib/project-template-materializer.ts
rg -n -C3 'assertProjectV2FileSetLimits' lib/project-v2-files.ts

Repository: svg8bit/drops-studio

Length of output: 984


🏁 Script executed:

# Check the materializer code at the specified lines
sed -n '339,359p' lib/project-template-materializer.ts

# Check the migration code at the specified lines  
sed -n '368,375p' lib/project-v2-migration.ts

# Find and inspect the applyProjectV2FileOperations function
rg -n 'export.*function applyProjectV2FileOperations' lib/

Repository: svg8bit/drops-studio

Length of output: 1191


🏁 Script executed:

# Check what assertProjectV2FileSetLimits validates
rg -n -A10 'export.*assertProjectV2FileSetLimits' lib/

# Look for validation of operation lists before calling applyProjectV2FileOperations
rg -n -B5 'applyProjectV2FileOperations' lib/project-template-materializer.ts
rg -n -B5 'applyProjectV2FileOperations' lib/project-v2-migration.ts

Repository: svg8bit/drops-studio

Length of output: 1766


🏁 Script executed:

# Count actual template files more accurately
rg -n 'path: "' lib/project-template-materializer.ts | wc -l

# Also check the actual template structure 
ast-grep outline lib/project-template-materializer.ts

Repository: svg8bit/drops-studio

Length of output: 785


🏁 Script executed:

# Get the complete applyProjectV2FileOperations function
sed -n '139,250p' lib/project-v2-files.ts

Repository: svg8bit/drops-studio

Length of output: 4179


🏁 Script executed:

# Check what both limits are and their relationship
rg -n 'PROJECT_V2_FILE_LIMIT\s*=' lib/
rg -n 'FILE_OPERATION_LIMIT\s*=' lib/

# Check if operations are validated against the limit
rg -n -A5 'operations.*length' lib/project-v2-files.ts | head -30

Repository: svg8bit/drops-studio

Length of output: 639


🏁 Script executed:

# Check how many files a project can realistically have
rg -n 'PROJECT_V2_FILE_LIMIT' lib/ -A2 -B2

# Check if there's any batching logic around applyProjectV2FileOperations calls
rg -n -B10 -A5 'applyProjectV2FileOperations' lib/project-template-materializer.ts | grep -A15 'const operations'

# Check the preserved operations building in migration
sed -n '340,380p' lib/project-v2-migration.ts

Repository: svg8bit/drops-studio

Length of output: 3914


🏁 Script executed:

# Verify the actual structure of what gets passed - is it really unbounded?
ast-grep outline lib/project-v2-migration.ts

Repository: svg8bit/drops-studio

Length of output: 1239


🏁 Script executed:

# Check the actual generated files from the template
sed -n '43,180p' lib/project-template-materializer.ts | head -60

Repository: svg8bit/drops-studio

Length of output: 3371


🏁 Script executed:

# Count all the files that the template generates across all categories
rg -n '"' lib/project-template-materializer.ts | grep -E 'path: |tasks\[' | wc -l

# Check what files are actually in the categories object
ast-grep run --pattern 'categories = {
  $$$
}' --lang typescript lib/project-template-materializer.ts

Repository: svg8bit/drops-studio

Length of output: 161


🏁 Script executed:

# Check the sourceFiles function to see what files are actually generated
sed -n '195,240p' lib/project-template-materializer.ts

Repository: svg8bit/drops-studio

Length of output: 7631


🏁 Script executed:

# Check materializeProjectV2Template to see what files are created
sed -n '243,330p' lib/project-template-materializer.ts

Repository: svg8bit/drops-studio

Length of output: 3313


🏁 Script executed:

# Check how many files can be in a project and verify the actual maximum operations needed
python3 - <<'PY'
# Template generates 23 files
template_files = 23

# Maximum project files per limit
max_project_files = 64

# FILE_OPERATION_LIMIT
file_op_limit = 64

# Scenario 1: refreshGeneratedProjectV2Template
# If project has all 64 files (64 max), with 50 generated and 14 manual
# Template has 23 files. Worst case: delete all 50 old generated, write 23 new = 73 operations (EXCEEDS!)
scenario1_deletes = 50
scenario1_writes = 23
scenario1_ops = scenario1_deletes + scenario1_writes
print(f"Scenario 1 (refresh template): {scenario1_ops} operations (limit: {file_op_limit})")
print(f"  - Exceeds limit: {scenario1_ops > file_op_limit}")

# Scenario 2: refreshLegacyProjectV2Migration  
# If project has 64 files total, with 60 non-generated (preserved)
scenario2_preserved = 60
print(f"\nScenario 2 (legacy migration): {scenario2_preserved} preserved operations (limit: {file_op_limit})")
print(f"  - Exceeds limit: {scenario2_preserved > file_op_limit}")

# Scenario 3: refreshLegacyProjectV2Migration
# If project has 64 files total, with 64 non-generated (all preserved)
scenario3_preserved = 64
print(f"\nScenario 3 (all preserved): {scenario3_preserved} preserved operations (limit: {file_op_limit})")
print(f"  - Exceeds limit: {scenario3_preserved > file_op_limit}")
print(f"  - At boundary: {scenario3_preserved == file_op_limit}")
PY

Repository: svg8bit/drops-studio

Length of output: 444


🏁 Script executed:

# Verify the validation is in place in applyProjectV2FileOperations
grep -n "operations.length > FILE_OPERATION_LIMIT" lib/project-v2-files.ts

# Check if there's any pre-validation before calling applyProjectV2FileOperations in either adapter
sed -n '330,385p' lib/project-template-materializer.ts | grep -E 'operations\.length|assert'
sed -n '340,380p' lib/project-v2-migration.ts | grep -E 'preservedOperations\.length|assert'

Repository: svg8bit/drops-studio

Length of output: 374


🏁 Script executed:

# Verify that scenario 1 is realistic - check if there can actually be 50+ generated files before refresh
# Look at what a worst-case spec change might delete/regenerate
rg -n 'managed\.' lib/project-template-materializer.ts | head -20

Repository: svg8bit/drops-studio

Length of output: 1187


🏁 Script executed:

# Check if there's any batching or chunking logic elsewhere
rg -n 'batch\|chunk' lib/ | grep -i 'operation\|file'

Repository: svg8bit/drops-studio

Length of output: 158


Both refresh adapters can exceed FILE_OPERATION_LIMIT when building unbounded operation lists. refreshGeneratedProjectV2Template constructs delete and write operations for the full file set without pre-validation; a project with 50+ existing generated files requires 50 deletes plus 23 template writes, totaling 73 operations and exceeding the 64-operation limit. refreshLegacyProjectV2Migration preserves all non-generated files as write operations and reaches the limit at exactly 64 preserved files. Both adapters pass unbounded lists to applyProjectV2FileOperations, which validates and rejects them at runtime.

Batch or bound the operation lists before passing them to applyProjectV2FileOperations:

  • lib/project-template-materializer.ts#L339-L359: Pre-validate operations.length against FILE_OPERATION_LIMIT before line 379, or split operations into batches if necessary.
  • lib/project-v2-migration.ts#L356-L375: Pre-validate preservedOperations.length against FILE_OPERATION_LIMIT before line 368, or batch the preserved operations.
📍 Affects 2 files
  • lib/project-template-materializer.ts#L339-L359 (this comment)
  • lib/project-v2-migration.ts#L368-L375
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@lib/project-template-materializer.ts` around lines 339 - 359, The refresh
adapters build operation arrays without enforcing FILE_OPERATION_LIMIT before
calling applyProjectV2FileOperations. In lib/project-template-materializer.ts
lines 339-359, update refreshGeneratedProjectV2Template to validate or batch
operations before the applyProjectV2FileOperations call; in
lib/project-v2-migration.ts lines 368-375, apply the same limit handling to
preservedOperations in refreshLegacyProjectV2Migration. Ensure neither adapter
passes more than FILE_OPERATION_LIMIT operations in a single call.

Comment on lines +397 to +407
next = {
...next,
manifest: fresh.manifest,
productSpec: fresh.productSpec,
integrations: fresh.integrations,
environment: fresh.environment,
permissions: fresh.permissions,
tasks: fresh.tasks,
updatedAt: now,
contentHash: "",
};

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🗄️ Data Integrity & Integration | 🟠 Major | 🏗️ Heavy lift

Manifest can desync from a preserved manual package.json.

The loop at lines 346-359 skips paths whose provenance is not generated, so a manually edited package.json stays in files. Line 399 then replaces manifest with fresh.manifest, which is derived from the deterministic template package manifest. After the refresh, manifest.dependencies and manifest.scripts can disagree with the retained package.json content. applyProjectV2FileOperations normally keeps these in sync through syncManifestFromPackage. Re-sync the manifest from the resulting package.json after the metadata merge, or exclude package.json fields from the fresh manifest overwrite.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@lib/project-template-materializer.ts` around lines 397 - 407, The metadata
merge around next.manifest must preserve consistency with a retained manually
edited package.json. After applying the fresh metadata in this refresh flow,
re-synchronize manifest from the resulting package.json using the existing
syncManifestFromPackage behavior, or avoid overwriting package.json-derived
manifest fields while merging fresh.manifest.

Comment on lines +181 to +202
const togglePlayback = () => {
if (typeof window === "undefined" || !("speechSynthesis" in window)) {
setPlayback("Browser speech is unavailable");
return;
}
if (playing) {
window.speechSynthesis.cancel();
setPlaying(false);
setPlayback("Playback stopped");
return;
}
const utterance = new SpeechSynthesisUtterance(script);
utterance.rate = 0.96;
utterance.pitch = 0.92;
utterance.volume = volume / 100;
utterance.onend = () => { setPlaying(false); setPlayback("Segment complete"); };
utterance.onerror = () => { setPlaying(false); setPlayback("Playback could not start"); };
window.speechSynthesis.cancel();
window.speechSynthesis.speak(utterance);
setPlaying(true);
setPlayback("Browser speech active");
};

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🩺 Stability & Availability | 🟡 Minor | ⚡ Quick win

Stop browser speech when the generated component unmounts.

togglePlayback starts a SpeechSynthesisUtterance through the global speechSynthesis queue. The generated component never cancels it on unmount, so the briefing keeps playing after the user navigates away inside the generated app. Add a cleanup effect. The template import list needs useEffect.

🔧 Proposed cleanup
-import { createContext, useContext, useState, type ReactNode } from "react";
+import { createContext, useContext, useEffect, useState, type ReactNode } from "react";
   const [volume, setVolume] = useState(72);
+  useEffect(() => () => { window.speechSynthesis?.cancel(); }, []);
   const currentSegment = queue[segmentIndex] ?? queue[0];
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
const togglePlayback = () => {
if (typeof window === "undefined" || !("speechSynthesis" in window)) {
setPlayback("Browser speech is unavailable");
return;
}
if (playing) {
window.speechSynthesis.cancel();
setPlaying(false);
setPlayback("Playback stopped");
return;
}
const utterance = new SpeechSynthesisUtterance(script);
utterance.rate = 0.96;
utterance.pitch = 0.92;
utterance.volume = volume / 100;
utterance.onend = () => { setPlaying(false); setPlayback("Segment complete"); };
utterance.onerror = () => { setPlaying(false); setPlayback("Playback could not start"); };
window.speechSynthesis.cancel();
window.speechSynthesis.speak(utterance);
setPlaying(true);
setPlayback("Browser speech active");
};
import { createContext, useContext, useEffect, useState, type ReactNode } from "react";
const [volume, setVolume] = useState(72);
useEffect(() => () => { window.speechSynthesis?.cancel(); }, []);
const currentSegment = queue[segmentIndex] ?? queue[0];
const togglePlayback = () => {
if (typeof window === "undefined" || !("speechSynthesis" in window)) {
setPlayback("Browser speech is unavailable");
return;
}
if (playing) {
window.speechSynthesis.cancel();
setPlaying(false);
setPlayback("Playback stopped");
return;
}
const utterance = new SpeechSynthesisUtterance(script);
utterance.rate = 0.96;
utterance.pitch = 0.92;
utterance.volume = volume / 100;
utterance.onend = () => { setPlaying(false); setPlayback("Segment complete"); };
utterance.onerror = () => { setPlaying(false); setPlayback("Playback could not start"); };
window.speechSynthesis.cancel();
window.speechSynthesis.speak(utterance);
setPlaying(true);
setPlayback("Browser speech active");
};
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@lib/project-template-ui.ts` around lines 181 - 202, Import useEffect in the
generated component template and add an unmount cleanup effect that calls
window.speechSynthesis.cancel() when browser speech is available. Place the
cleanup alongside togglePlayback so any queued or active utterance stops when
the component unmounts, while preserving the existing playback behavior.

.radio-player-top strong { color: var(--project-accent); font-size: 12px; letter-spacing: .12em; }
.radio-track-label { margin: 34px 0 12px; color: #6f7d82; font-size: 12px; font-weight: 800; letter-spacing: .12em; }
.radio-player h2 { margin: 0; font-size: clamp(2.3rem, 4.8vw, 4.6rem); line-height: .92; letter-spacing: -.055em; }
.radio-player > p:not(.radio-track-label, .radio-playback) { min-height: 74px; color: #a9b4b8; font-size: 14px; line-height: 1.65; }

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

📐 Maintainability & Code Quality | 🟡 Minor | ⚡ Quick win

Raise generated body copy to the required size.

.radio-player > p:not(.radio-track-label, .radio-playback) sets the segment script text to 14 px. .radio-archive p sets the archive description text to 13 px. Both are body copy in the generated product. The design rules require 16–18 px for body text; 12 px is reserved for helper and metadata text.

🎨 Proposed size fix
-.radio-player > p:not(.radio-track-label, .radio-playback) { min-height: 74px; color: `#a9b4b8`; font-size: 14px; line-height: 1.65; }
+.radio-player > p:not(.radio-track-label, .radio-playback) { min-height: 74px; color: `#a9b4b8`; font-size: 16px; line-height: 1.65; }
@@
-.radio-archive p { margin: 0; color: `#7d8a8f`; font-size: 13px; line-height: 1.55; }
+.radio-archive p { margin: 0; color: `#7d8a8f`; font-size: 16px; line-height: 1.55; }

As per coding guidelines: "Body text must be 16–18 px, control text at least 14 px, helper and metadata text at least 12 px".

Also applies to: 443-443

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@lib/project-template-ui.ts` at line 396, Update the generated body-copy
styles in the radio player and archive sections: change the `.radio-player >
p:not(.radio-track-label, .radio-playback)` and `.radio-archive p` font sizes to
comply with the required 16–18px body-text range, while leaving helper and
metadata styles unchanged.

Source: Coding guidelines

Comment on lines +378 to +399
const next: ProjectV2 = {
...refreshed,
revision,
integrations: current.integrations,
environment: current.environment,
permissions: current.permissions,
deployment: current.deployment,
migration: current.migration,
runs: current.runs,
logs: current.logs,
checkpoints: current.checkpoints,
preview: current.preview
? {
status: "stopped",
projectRevision: revision,
stoppedAt: updatedAt,
}
: undefined,
createdAt: current.createdAt,
updatedAt,
contentHash: "",
};

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🗄️ Data Integrity & Integration | 🟠 Major | ⚡ Quick win

Preserved runs can reference tasks that the refreshed baseline no longer defines.

next keeps current.runs, current.logs, and current.checkpoints, but tasks come from refreshed, which is derived from migrateGeneratedProjectToV2. That function selects the task set from the workspace state (workspaceTasks(workspace)) or from the HTML-only default list. If the incoming generatedProject no longer produces a task id that an existing run references, validateProjectV2 throws Project run <id> references an unknown task. and the refresh fails. The caller in components/project-studio.tsx then blocks the save and shows the refresh error. Preserve current.tasks, or drop runs and logs whose task no longer exists.

🐛 Proposed fix
   const next: ProjectV2 = {
     ...refreshed,
     revision,
+    tasks: current.tasks,
     integrations: current.integrations,
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
const next: ProjectV2 = {
...refreshed,
revision,
integrations: current.integrations,
environment: current.environment,
permissions: current.permissions,
deployment: current.deployment,
migration: current.migration,
runs: current.runs,
logs: current.logs,
checkpoints: current.checkpoints,
preview: current.preview
? {
status: "stopped",
projectRevision: revision,
stoppedAt: updatedAt,
}
: undefined,
createdAt: current.createdAt,
updatedAt,
contentHash: "",
};
const next: ProjectV2 = {
...refreshed,
revision,
tasks: current.tasks,
integrations: current.integrations,
environment: current.environment,
permissions: current.permissions,
deployment: current.deployment,
migration: current.migration,
runs: current.runs,
logs: current.logs,
checkpoints: current.checkpoints,
preview: current.preview
? {
status: "stopped",
projectRevision: revision,
stoppedAt: updatedAt,
}
: undefined,
createdAt: current.createdAt,
updatedAt,
contentHash: "",
};
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@lib/project-v2-migration.ts` around lines 378 - 399, Update the ProjectV2
construction in the migration refresh flow to keep task references consistent
with preserved runs, logs, and checkpoints. Preserve current.tasks alongside the
existing current state fields, or filter out persisted runs and related logs
whose task IDs are absent from refreshed.tasks before validation; ensure
validateProjectV2 cannot encounter unknown task references.

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 34b37ced7a

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

spec: GeneratedProjectSpec;
};

const PROJECT_V2_BUILD_TASKS = ["typecheck", "lint", "test", "build"] as const;

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge Verify the tasks declared by migrated projects

For every migrated legacy-html project, this hard-coded list requires successful typecheck and lint runs, but migrateGeneratedProjectToV2() declares only check/test/build/start for workspace projects (and test/build/dev for HTML-only projects). Even after every declared task and the preview succeed, these preserved V1 projects can reach at most 3/5, so Studio permanently reports “Build pending” instead of a verified release; derive required checks from the project's task kinds or handle the legacy adapter separately.

AGENTS.md reference: AGENTS.md:L76-L76

Useful? React with 👍 / 👎.

Comment on lines 1332 to +1334
const browserTelemetryReady = Boolean(
runtimeSmoke?.mode === "browser"
runtimePreviewUrl
|| (

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge Wait for actual browser telemetry before claiming it

When a current-revision Sandbox preview merely has a ready HTTPS URL, this branch immediately sets data-runtime-ready="true" and, on the mobile layout, displays “Browser telemetry” before the iframe has loaded or emitted any runtime evidence. An expired, blocked, or failing preview therefore presents a verified telemetry state based only on stored preview metadata; keep URL availability separate from the existing executed browser-smoke condition.

AGENTS.md reference: AGENTS.md:L111-L111

Useful? React with 👍 / 👎.

.device-switch button.active { box-shadow: 0 2px 5px rgba(28,46,75,.1); }
.runtime-browser { background: white; border: 1px solid #d4dfec; border-radius: 14px; box-shadow: 0 14px 36px rgba(28,48,80,.1); height: calc(100% - 46px); margin: auto; min-height: 590px; overflow: auto; transition: .2s ease; width: 100%; }
.canvas-zoom-controls { align-items: center; background: #e3e9f2; border-radius: 8px; display: inline-flex; flex: 0 0 auto; padding: 2px; }
.canvas-zoom-controls button { justify-content: center; min-width: 38px; padding-inline: 7px; }

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge Make the zoom controls at least 44 pixels wide

The newly visible zoom-out and zoom-in buttons compute to only 38 CSS pixels wide (their SVG and padding do not exceed this minimum), so their pointer targets are 38×44 even though the later workspace rule supplies a 44px height. Increase the minimum width to 44px so these frequently used canvas controls meet the repository's interaction-target requirement.

AGENTS.md reference: AGENTS.md:L28-L28

Useful? React with 👍 / 👎.


next = {
...next,
manifest: fresh.manifest,

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge Preserve an edited package manifest during spec refresh

When Builder has changed package.json—for example through the supported install_package tool—the file has AI provenance and is deliberately preserved earlier in this function, but this assignment replaces the synchronized manifest with the fresh template's dependency and script records. The final validateProjectV2() then rejects the revision because the manifest no longer matches package.json, so any later Director/spec edit cannot be saved; retain the manifest derived from the preserved package file while updating only spec-owned fields.

AGENTS.md reference: AGENTS.md:L105-L105

Useful? React with 👍 / 👎.

Comment on lines +4405 to +4408
window.open(
"/?connections=1",
"_blank",
"noopener,noreferrer",

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge Keep connection setup in the originating tab

Both connection shortcuts now open a new tab, but model and DropsTab credentials are written exclusively to sessionStorage. The child tab receives an initial copy of the opener's storage, yet subsequent connection writes do not propagate back, and noopener prevents any explicit handoff, so returning to the original project leaves its provider/key state unchanged and the new connection unusable there; retain same-tab navigation or implement a secure result handoff.

AGENTS.md reference: AGENTS.md:L76-L76

Useful? React with 👍 / 👎.

.radio-player-top strong { color: var(--project-accent); font-size: 12px; letter-spacing: .12em; }
.radio-track-label { margin: 34px 0 12px; color: #6f7d82; font-size: 12px; font-weight: 800; letter-spacing: .12em; }
.radio-player h2 { margin: 0; font-size: clamp(2.3rem, 4.8vw, 4.6rem); line-height: .92; letter-spacing: -.055em; }
.radio-player > p:not(.radio-track-label, .radio-playback) { min-height: 74px; color: #a9b4b8; font-size: 14px; line-height: 1.65; }

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge Raise radio script copy to the body-text minimum

The generated Crypto Radio player's segment script is normal paragraph body copy, but this new rule renders it at 14px across every viewport. That makes the primary content users read and edit smaller than the repository's mandatory 16–18px body scale; set this paragraph to at least 16px while leaving true metadata at 12px.

AGENTS.md reference: AGENTS.md:L24-L24

Useful? React with 👍 / 👎.

Comment thread lib/project-store.ts
.slice(0, PROJECT_STORE_LIMIT - 1);
const merged = [project, ...retainedExisting]
.sort((left, right) => timestamp(right) - timestamp(left));
const compatibilityIndex = merged.map(compactProjectForCompatibilityIndex);

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge Preserve V2 snapshots for every retained project

When the legacy array contains multiple local projects with projectV2 snapshots, saving any one project compacts every retained entry here, but the subsequent per-project write creates an item record only for the project being saved. Retained projects that do not already have item records therefore lose their canonical V2 files, runs, and checkpoints on the next read or reload; migrate all retained full snapshots to item records before stripping them from the compatibility index, or fail without discarding them.

AGENTS.md reference: AGENTS.md:L105-L105

Useful? React with 👍 / 👎.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant