Skip to content

Ship reliable v0-like Studio flow - #28

Merged
svg8bit merged 2 commits into
mainfrom
codex/drops-studio-v0-flow-quality
Aug 1, 2026
Merged

Ship reliable v0-like Studio flow#28
svg8bit merged 2 commits into
mainfrom
codex/drops-studio-v0-flow-quality

Conversation

@svg8bit

@svg8bit svg8bit commented Aug 1, 2026

Copy link
Copy Markdown
Owner

Outcome

  • makes the Studio chat use the selected connected model with bounded secret-safe streaming
  • persists account-scoped connections and projects with safe actor migration and conflict handling
  • makes build/Sandbox state immediate and recoverable, with verified preview-host isolation
  • simplifies and resizes the v0-like Studio workspace while preserving the real Project V2 surfaces
  • separates 42 credentialed/billable live cases from the deterministic release suite instead of green-skipping them

Verification

  • full repository gate passed before final review: 792 unit, 162 regular E2E, 47 Storybook, 42 visual
  • final focused gate: typecheck, ESLint, 45/45 critical tests, skipped 0
  • CodeRabbit findings addressed
  • visual baselines intentionally updated with owner approval

Safety

  • no credentials in source or diff
  • V1, 12 recipes, /p/{slug}, ZIP, session-only BYOK and approval gates preserved
  • live provider matrix remains explicit opt-in and fails closed without configuration

Summary by CodeRabbit

  • New Features
    • Added a unified Code workspace with native previews, model selection, streamed AI responses, and Stop controls.
    • Added actor-scoped project storage with improved restoration and synchronization.
    • Added Telegram channel recovery across sessions and clearer connection status.
  • Bug Fixes
    • Improved handling of interrupted builds, timeouts, unavailable services, and corrupt cloud projects.
    • Improved responsive layouts, mobile behavior, preview loading, and accessibility.
  • Improvements
    • Added more reliable account connection retries and session migration.
    • Expanded live validation and release testing coverage.

@vercel

vercel Bot commented Aug 1, 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 Aug 1, 2026 2:51pm

Request Review

@coderabbitai

coderabbitai Bot commented Aug 1, 2026

Copy link
Copy Markdown

Review Change Stack

Important

Review skipped

Review was skipped due to path filters

⛔ Files ignored due to path filters (2)
  • e2e/visual/home.spec.ts-snapshots/studio-crypto-game-chromium-1024-linux.png is excluded by !**/*.png
  • e2e/visual/home.spec.ts-snapshots/studio-crypto-game-chromium-1440-linux.png is excluded by !**/*.png

CodeRabbit blocks several paths by default. You can override this behavior by explicitly including those paths in the path filters. For example, including **/dist/** will override the default block on the dist directory, by removing the pattern from both the lists.

⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: 70217346-6b57-41cd-9c4d-01d811fbf05c

You can disable this status message by setting the reviews.review_status to false in the CodeRabbit configuration file.

Use the checkbox below for a quick retry:

  • 🔍 Trigger review
📝 Walkthrough

Walkthrough

The PR adds actor-scoped Project V2 storage, native Code workspace execution, streamed agent chat, cancellation handling, resilient account connections, Telegram receipt persistence, and updated E2E and live-test infrastructure.

Changes

Project V2 platform and workspace

Layer / File(s) Summary
Scoped storage and access bootstrap
lib/project-store.ts, lib/access-tier.ts, app/api/access/route.ts, components/drops-studio.tsx, components/project-studio.tsx
Project data now uses guest/member scopes, cookie-based discovery, legacy migration locks, and Project V2 readiness metadata.
Native Code workspace and builder
components/project-studio.tsx, components/project-v2-studio-surface.tsx, app/api/builder/agent/route.ts, lib/builder-agent/orchestrator.ts, app/styles/*
The Code workspace runs Project V2 builds through an imperative surface, supports stop handling, serialized snapshots, preview loading, provider selection, and responsive layouts.
Streaming chat and cancellation
app/api/agent/chat/route.ts, components/project-studio.tsx, tests/agent-chat-route.test.mjs, tests/builder-agent-route.test.mjs
Chat supports negotiated text streaming, bounded output, secret filtering, and abort propagation. Builder cancellation returns distinct cancellation and timeout responses.
Scoped storage and workspace validation
e2e/fixtures/*, e2e/contracts/*, e2e/interactions/*, tests/project-store.test.mjs
Fixtures and tests use actor-aware storage helpers, scoped cookies, Project V2 API state, and viewport tags.

Account connections and Telegram persistence

Layer / File(s) Summary
Connection reliability and Telegram receipts
lib/studio-account-connections-client.ts, db/studio-account-state.ts, app/api/telegram/*, components/telegram-channel-wizard.tsx
Account requests now retry and report structured failures. Telegram receipts are validated, encrypted, restored, and safely forgotten.

Live test infrastructure

Layer / File(s) Summary
Live commands and runtime controls
package.json, .env.example, docs/SANDBOX_OPERATIONS.md, playwright.config.ts, lib/vercel-sandbox-runtime-adapter.ts, tests/*live.mjs
Dedicated live commands, environment guards, Sandbox gating, cleanup, and viewport-specific test discovery are documented and implemented.

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

Possibly related PRs

  • svg8bit/drops-studio#23: Related account-aware Studio, project-store, builder, authentication, and Telegram connection changes.
  • svg8bit/drops-studio#25: Related Project Studio UI, builder cancellation, Sandbox lifecycle, and persistent account connection changes.
  • svg8bit/drops-studio#27: Related chat, provider persistence, migration, and Project Studio integration changes.
🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 0.00% 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 clearly summarizes the primary objective: shipping a reliable v0-like Studio flow.
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-flow-quality

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

@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: a2d83d018c

ℹ️ 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".

Comment on lines +761 to 764
autoStarted.current === `${project.id}:${project.revision}` ||
storageMode !== "cloud" ||
project.manifest.framework.name !== "nextjs" ||
project.preview?.status === "ready"

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 Limit automatic builds to the initial revision

When a user manually saves a file or imports GitHub source, the operation increments project.revision and changes an existing preview to stopped; keying autoStarted by revision therefore passes this guard again and calls runBuilder after 50 ms. Because the route now honors the selected provider, every manual edit can start a billable Sandbox and paid model request without the user choosing Build, despite the UI saying they must rebuild explicitly. Keep auto-build restricted to the initial handoff or require an explicit build action for subsequent revisions.

AGENTS.md reference: AGENTS.md:L7-L10

Useful? React with 👍 / 👎.

Comment on lines +5406 to +5411
{directing && usesNativeProjectV2 ? (
<button
type="button"
className="chat-stop-button"
aria-label="Stop current build"
onClick={() => void projectV2SurfaceRef.current?.stop()}

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Badge Show the stop control only for an active build

For a native Project V2, directing is also true while a read-only /api/agent/chat response is streaming, so this button appears even though the child has no active builder controller. Clicking it calls stopActiveRun, which unconditionally sends the runtime stop action: the live Sandbox preview is terminated while the chat request continues. Gate this control on an actual builder run, or give chat streaming its own abort controller instead of routing the action to the runtime.

AGENTS.md reference: AGENTS.md:L103-L109

Useful? React with 👍 / 👎.

Comment on lines +645 to +647
statusTimer = setInterval(() => {
void refreshSandboxStatus(snapshot.project.id).catch(() => undefined);
}, 4_000);

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Badge Keep status polling within the runtime-action quota

Polling every four seconds consumes the same builder-runtime-action limit of 120 requests per hour enforced by app/api/builder/runtime/route.ts. A build near the 270-second server deadline uses about 67 status requests, so two slow build or repair attempts in one hour exhaust the quota and make subsequent status, stop, preview, and other runtime actions return 429; slow status calls can also overlap because the interval does not await the prior request. Use a slower or non-overlapping poll and a read-specific quota boundary.

AGENTS.md reference: AGENTS.md:L103-L109

Useful? React with 👍 / 👎.

Comment on lines +21 to +27
response.cookies.set(PROJECT_STORE_SCOPE_COOKIE, "", {
httpOnly: false,
sameSite: "lax",
secure: process.env.NODE_ENV === "production",
expires: new Date(0),
path: "/",
});

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 Re-establish a guest project scope after sign-out

A successful sign-out now deletes the browser project-scope cookie, but the client remains on the page, keeps the current project list, and never calls /api/access to establish the still-valid guest identity as the new scope. Subsequent Build, edit, and delete operations call saveProjectSafely or deleteProjectSafely, which reject browser-default access when that cookie is absent, so the builder is unusable until the user reloads; the success toast simultaneously claims browser projects remain available. Re-bootstrap the guest scope before returning success or force a reload after sign-out.

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

Useful? React with 👍 / 👎.

@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: 11

🧹 Nitpick comments (18)
app/api/telegram/account/create-channel/route.ts (1)

57-58: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

Rename the local flag to avoid shadowing remembered.

Line 35 already binds remembered to the stored connection secret. Line 57 declares a second remembered boolean in the inner block. Both names describe different data in one function. Rename the flag, for example persisted, and use it in the response body at Line 92.

♻️ Proposed rename
-    let remembered = false;
+    let persisted = false;
     if (account) {
-        remembered = true;
+        persisted = true;
       accountPersistence: {
         available: Boolean(account),
-        remembered,
+        remembered: persisted,
       },
🤖 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/api/telegram/account/create-channel/route.ts` around lines 57 - 58,
Rename the inner boolean declaration in the account-handling block from
remembered to a distinct name such as persisted, and update its assignment and
response-body usage near the create-channel handler’s return path. Keep the
outer remembered value for the stored connection secret unchanged.
tests/studio-account-state.test.mjs (1)

216-259: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Add a case for a username without the @ prefix.

The fixture always uses "@fixture_alpha". The parser rejects "fixture_alpha", which is the form Telegram normally returns. Add that case to pin the intended behavior, whichever behavior you choose after the parser review in db/studio-account-state.ts Lines 164-166.

💚 Proposed test case
     await assert.rejects(
       storage.saveStudioConnection(identity, {
         provider: "telegram",
         credential: "telegram-account-session-fixture",
         telegramReceipt: { ...receipt, accountId: "not-an-account" },
       }, undefined, sql),
       /receipt is invalid/i,
     );
+    // Pin the expected handling of an unprefixed channel username.
+    await assert.rejects(
+      storage.saveStudioConnection(identity, {
+        provider: "telegram",
+        credential: "telegram-account-session-fixture",
+        telegramReceipt: { ...receipt, username: "fixture_alpha" },
+      }, undefined, sql),
+      /receipt is invalid/i,
+    );
🤖 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/studio-account-state.test.mjs` around lines 216 - 259, Add a Telegram
receipt validation case in the test covering the existing receipt fixture, using
a username without the “@” prefix and asserting the intended parser behavior
established in saveStudioConnection. Keep the current prefixed-username fixture
and other invalid-field checks unchanged.
tests/studio-account-connections-client.test.mjs (1)

149-169: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Add the successful deletion path.

The suite covers only the retryable failure. The wizard treats deleted === true as the condition for clearing local state (components/telegram-channel-wizard.tsx Lines 263-271). Add one case that returns { deleted: true, connections: [] } and asserts a single call, plus one non-retryable 4xx case that asserts retryable === false.

🤖 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/studio-account-connections-client.test.mjs` around lines 149 - 169, Add
coverage alongside the existing forgetStudioConnection failure test: add a
successful response returning deleted true with an empty connections list and
assert one fetch call, then add a non-retryable 4xx response and assert
retryable is false. Reuse the existing fetch stubbing and cleanup pattern.
lib/studio-account-connections-client.ts (2)

96-108: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Reuse one provider allow-list.

The inline array repeats the StudioAccountConnectionProvider union at Lines 1-9. A new provider must then be added in two places. Declare one const tuple and derive the union from it.

♻️ Proposed refactor
+const CONNECTION_PROVIDERS = [
+  "dropstab",
+  "dropsbot",
+  "openai",
+  "anthropic",
+  "openrouter",
+  "kimi",
+  "custom",
+  "telegram",
+] as const;
+
-export type StudioAccountConnectionProvider =
-  | "dropstab"
-  | "dropsbot"
-  | "openai"
-  | "anthropic"
-  | "openrouter"
-  | "kimi"
-  | "custom"
-  | "telegram";
+export type StudioAccountConnectionProvider = (typeof CONNECTION_PROVIDERS)[number];
-      || ![
-        "dropstab",
-        ...
-      ].includes(provider)
+      || !(CONNECTION_PROVIDERS as readonly string[]).includes(provider)
🤖 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/studio-account-connections-client.ts` around lines 96 - 108, Define a
single provider allow-list tuple near the existing
StudioAccountConnectionProvider declaration, derive the union type from that
tuple, and update the validation in the candidate/provider path to reuse the
tuple instead of its inline array. Ensure adding a provider requires changing
only the shared tuple.

178-226: 📐 Maintainability & Code Quality | 🔵 Trivial | 🏗️ Heavy lift

Extract the shared retry loop.

The three request functions repeat the same structure: attempt count, timeout signal, retryable status check, retry pause, and terminal fallback result. Extract one helper that performs the fetch with retries and returns the response or null. Each function then only maps its payload. This reduces the risk of the three copies drifting apart.

Also applies to: 228-283, 285-337

🤖 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/studio-account-connections-client.ts` around lines 178 - 226, The request
functions, including readStudioAccountSnapshot, duplicate retry and timeout
handling. Extract a shared helper that performs the configured fetch attempts,
applies the timeout signal and retryable status logic, waits via retryPause, and
returns the successful Response or null after exhaustion; then update
readStudioAccountSnapshot and the other two request functions to delegate to it
and only map their payloads, preserving each function’s existing
response-specific behavior.
e2e/products/free-prompt-game.spec.ts (1)

103-136: 🗄️ Data Integrity & Integration | 🔵 Trivial | ⚡ Quick win

The builder stubs do not match the real API contract.

Two mismatches reduce the value of this boundary:

  1. The /api/builder/agent stub returns HTTP 200 with only code and error and no result. The real route returns a non-2xx status for gate failures, and a 200 response without result is not a shape the server produces. A client that starts treating 200 as success would still pass this test.
  2. The /api/builder/runtime stub always answers with action: "status". The test path can also request stop, preview, or task actions. Echo the requested action from the post body so the stub stays faithful.
♻️ Proposed refactor
   await page.route("**/api/builder/agent", async (route) => {
     await route.fulfill({
-      status: 200,
+      status: 503,
       contentType: "application/json",
       body: JSON.stringify({
         code: "E2E_SANDBOX_SEPARATE_GATE",
         error: "Live Sandbox verification runs in the explicit credentialed gate.",
       }),
     })
   })
   await page.route("**/api/builder/runtime", async (route) => {
+    const requested = (route.request().postDataJSON() as { action?: string })?.action ?? "status"
     await route.fulfill({
       status: 200,
       contentType: "application/json",
       body: JSON.stringify({
-        action: "status",
+        action: requested,
         result: {
🤖 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 `@e2e/products/free-prompt-game.spec.ts` around lines 103 - 136, Update the
builder route stubs in the test: make the `/api/builder/agent` gate-failure
response use the real non-2xx status and failure shape without a fabricated
successful 200 response, and change the `/api/builder/runtime` handler to parse
the POST body and echo its requested action instead of always returning
`"status"`. Preserve the existing runtime result fields and unavailable-state
behavior.
tests/agent-chat-route.test.mjs (1)

213-251: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Add coverage for the oversized single-line rejection.

This test proves that many bounded lines pass. No test drives a single partial line past MAX_STREAM_LINE_CHARACTERS, which is the fail-closed branch at app/api/agent/chat/route.ts Lines 121-125. Add a case that streams more than 16,384 characters without a newline and assert the reader rejects with the oversized message.

🤖 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/agent-chat-route.test.mjs` around lines 213 - 251, Add a separate test
near the existing large-chunk coverage that streams a single line exceeding
MAX_STREAM_LINE_CHARACTERS without a newline, then assert reading the response
rejects with the expected oversized-line error message. Keep the existing
bounded-lines test unchanged.
tests/builder-agent-route.test.mjs (1)

347-369: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

The abort test only covers the already-aborted path and does not assert the persisted stopped state.

controller.abort() runs before the handler reaches request.signal.addEventListener, so this test exercises only the explicit if (request.signal.aborted) carry-over at app/api/builder/agent/route.ts Line 263. The listener path stays uncovered. Add a second case that aborts after the fallback starts, and assert that stopInterruptedSession persisted preview.status === "stopped".

♻️ Proposed additions
   assert.equal(payload.code, "BUILDER_EXECUTION_CANCELLED");
   assert.match(payload.error, /saved project files.*preserved/i);
   assert.equal(deps.calls.stop, 1);
+  assert.equal(deps.getStored().preview?.status, "stopped");
 });
+
+test("an abort during an active build cancels through the request listener", async () => {
+  const deps = dependencies();
+  const controller = new AbortController();
+  let started;
+  const running = new Promise((resolve) => { started = resolve; });
+  deps.deterministicFallback = {
+    async run() {
+      started();
+      await new Promise(() => {});
+    },
+  };
+  const pending = handleBuilderAgentRequest(request("/api/builder/agent", {
+    projectId: "builder-route-project",
+    prompt: "Stop this build after it started.",
+    mode: "build",
+    provider: { provider: "free" },
+  }, {}, controller.signal), deps);
+  await running;
+  controller.abort();
+  const response = await pending;
+  assert.equal(response.status, 499);
+});
🤖 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/builder-agent-route.test.mjs` around lines 347 - 369, Add a separate
test near the existing disconnected-client test that waits until the
deterministic fallback begins before calling controller.abort(), thereby
exercising the request.signal abort listener rather than the already-aborted
branch. Await the handler response and assert cancellation as appropriate, then
verify the persisted project state through stopInterruptedSession’s result or
the established persistence fixture, specifically confirming preview.status is
"stopped".
app/styles/project-studio.responsive.css (1)

345-350: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

Duplicate .project-v2-studio-host definition in two stylesheets. Both files declare the same selector with the same four properties. The two rules must stay in sync manually, and a future change to one file silently loses to load order.

  • app/styles/project-studio.responsive.css#L345-L350: keep this definition as the single source, because the file also owns the grid-template-areas that define the canvas area.
  • app/styles/project-studio.accessibility.css#L615-L620: remove the duplicated block and keep only the tab-code visibility rules that follow it.
🤖 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.responsive.css` around lines 345 - 350, Keep the
.project-v2-studio-host definition in
app/styles/project-studio.responsive.css#L345-L350 as the single source of
truth. Remove the duplicate four-property block from
app/styles/project-studio.accessibility.css#L615-L620, leaving only the
following tab-code visibility rules unchanged.
app/styles/project-studio.accessibility.css (1)

160-176: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

Merge the duplicated .studio-splitter svg rule.

Lines 160-166 and 168-175 declare the same selector with the same specificity and no conflicting properties. One block is enough and prevents future divergence.

♻️ Proposed consolidation
 .studio-splitter svg {
+  background: `#ffffff`;
+  border: 1px solid `#d8e2ef`;
+  border-radius: 999px;
+  box-shadow: 0 4px 14px rgba(30, 61, 110, 0.12);
+  box-sizing: content-box;
   height: 24px;
   opacity: 0.72;
+  padding: 9px 2px;
   position: relative;
   transition: opacity 160ms ease;
   width: 16px;
 }
-
-.studio-splitter svg {
-  background: `#ffffff`;
-  border: 1px solid `#d8e2ef`;
-  border-radius: 999px;
-  box-shadow: 0 4px 14px rgba(30, 61, 110, 0.12);
-  box-sizing: content-box;
-  padding: 9px 2px;
-}
🤖 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.accessibility.css` around lines 160 - 176, Merge
the two adjacent `.studio-splitter svg` CSS rules into a single rule containing
all existing declarations, preserving the current styles and selector
specificity.
e2e/fixtures/project-v2-ui-test.ts (1)

109-132: 🗄️ Data Integrity & Integration | 🔵 Trivial | ⚡ Quick win

Enforce expectedStorageRevision in the PUT mock.

The handler types expectedStorageRevision but never compares it to storageRevision. Every write succeeds. The optimistic-concurrency path that this PR adds is therefore not exercised by any spec that uses this fixture, and a client that sends a stale revision still passes. Return a conflict response when the value does not match.

♻️ Proposed change
       if (!input.project) {
         await route.fulfill({
           status: 400,
           contentType: "application/json",
           body: JSON.stringify({
             code: "PROJECT_V2_INVALID_FIXTURE_REQUEST",
             error: "A Project V2 snapshot is required.",
           }),
         });
         return;
       }
+      if (
+        typeof input.expectedStorageRevision === "number" &&
+        input.expectedStorageRevision !== storageRevision
+      ) {
+        await route.fulfill({
+          status: 409,
+          contentType: "application/json",
+          body: JSON.stringify({
+            code: "PROJECT_V2_STORAGE_CONFLICT",
+            storageRevision,
+            project: remoteProject,
+          }),
+        });
+        return;
+      }
       remoteProject = input.project;
🤖 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 `@e2e/fixtures/project-v2-ui-test.ts` around lines 109 - 132, Update the PUT
handler to compare input.expectedStorageRevision with storageRevision before
mutating remoteProject or incrementing the revision; return the fixture’s
conflict response when the supplied revision is stale or mismatched, and
continue the existing successful write flow only for matching revisions.
e2e/interactions/editor-commit.spec.ts (1)

155-158: 🎯 Functional Correctness | 🔵 Trivial | ⚡ Quick win

Insert the marker instead of retyping the whole document.

textContent() on .cm-content returns only the rendered lines. CodeMirror virtualizes the document, so the value can be truncated. Lines 157-158 then select all and retype that truncated text, which can drop source lines and can be reshaped by auto-indent and bracket closing. The assertion at line 165 still passes because it checks the marker only, so the loss stays hidden.

Place the cursor at the document start and type the marker only.

♻️ Proposed change
   const editor = page.locator(".cm-content")
   const marker = "// SOURCE-E2E-PERSISTED"
-  const originalSource = await editor.textContent()
   await editor.click()
-  await page.keyboard.press("ControlOrMeta+A")
-  await page.keyboard.type(`${marker}\n${originalSource ?? ""}`, { delay: 1 })
+  await page.keyboard.press("ControlOrMeta+Home")
+  await page.keyboard.type(`${marker}\n`)
🤖 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 `@e2e/interactions/editor-commit.spec.ts` around lines 155 - 158, Update the
editor interaction around originalSource so it no longer reads, selects, or
retypes the document. After focusing the CodeMirror editor, place the cursor at
the document start and type only marker, preserving the existing assertion and
avoiding any transformation of the original content.
e2e/contracts/v0-studio-flow.spec.ts (1)

40-62: 🗄️ Data Integrity & Integration | 🔵 Trivial | ⚡ Quick win

Reject unsupported runtime actions in the mock.

Return 409 unless action is "status". activeDurationMs: null is valid for an unavailable runtime.

🤖 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 `@e2e/contracts/v0-studio-flow.spec.ts` around lines 40 - 62, Update the
runtime mock route in the test setup to inspect the requested action and fulfill
with status 409 for any action other than "status"; retain the existing 200
response payload for the supported status action, including activeDurationMs:
null.
tests/coderabbit-findings.test.mjs (1)

48-58: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Assert the ordering the index variables imply.

accessIndex and scopedReadIndex are computed but only checked for presence. Two assert.match calls would prove the same thing. The invariant this PR depends on is ordering: the /api/access request must run inside the readProjectsAfterScopeBootstrap callback, so the scope cookie exists before the scoped read. Assert that relationship.

💚 Suggested assertion
   assert.notEqual(accessIndex, -1, "actor bootstrap request must remain present");
   assert.notEqual(scopedReadIndex, -1, "scoped project bootstrap helper must remain present");
+  assert.ok(
+    scopedReadIndex < accessIndex,
+    "the /api/access request must run inside the scoped project bootstrap callback",
+  );
🤖 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/coderabbit-findings.test.mjs` around lines 48 - 58, Update the
assertions around accessIndex and scopedReadIndex in the test to verify
ordering, not just presence: assert that the `/api/access` request occurs after
the readProjectsAfterScopeBootstrap callback begins. Keep the existing presence
checks and other builder assertions unchanged.
tests/access-tier.test.mjs (1)

455-465: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Assert the scope cookie, not only the response body.

The browser project store reads the scope from the drops_project_scope cookie, not from the JSON body. This test verifies payload.projectStoreScope but never checks the cookie that setProjectStoreScope writes. Add an assertion on the cookie value so a regression in the cookie name, value format, or path fails here.

💚 Suggested assertion
+    const { PROJECT_STORE_SCOPE_COOKIE } = await import("../lib/project-store.ts");
+    const scopeCookie = response.cookies.get(PROJECT_STORE_SCOPE_COOKIE);
+    assert.equal(
+      scopeCookie?.value,
+      `member.${readStudioAccountCookie(accountCookie, secret).identity}`,
+    );
+    assert.equal(scopeCookie?.path, "/");
🤖 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/access-tier.test.mjs` around lines 455 - 465, Extend the access
response test around GET and setProjectStoreScope to assert the
drops_project_scope cookie directly, including its serialized scope value and
path attribute. Keep the existing payload.projectStoreScope assertions, and read
the cookie from the response rather than inferring it from the JSON body.
lib/project-store.ts (1)

387-402: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Extract the repeated scope and lock resolution.

The same scope-resolution block appears in readProjectsFromStore, claimLegacyProjectsSafely, saveProjectSafely, and deleteProjectSafely. The lock-detection block repeats three times. Extract two small helpers so a future change to the scope contract updates one place.

♻️ Suggested helpers
+function resolveAccess(options: ProjectStoreAccessOptions): {
+  browserDefault: boolean;
+  storage: StorageLike;
+  scope: ProjectStoreScope | null;
+  locks: LockManagerLike | null;
+} {
+  const browserDefault = options.storage === undefined;
+  const storage = options.storage ?? window.localStorage;
+  const scope = options.scope === undefined
+    ? browserDefault ? browserProjectStoreScope() : null
+    : options.scope ? normalizeScope(options.scope) : null;
+  const locks = options.locks === undefined
+    ? (browserDefault && typeof navigator !== "undefined" && "locks" in navigator
+        ? navigator.locks as unknown as LockManagerLike
+        : null)
+    : options.locks;
+  return { browserDefault, storage, scope, locks };
+}
🤖 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-store.ts` around lines 387 - 402, Extract the duplicated
scope-resolution logic into a shared helper and reuse it from
readProjectsFromStore, claimLegacyProjectsSafely, saveProjectSafely, and
deleteProjectSafely. Extract the repeated navigator lock-detection logic into a
second helper and reuse it wherever those functions currently resolve locks,
preserving existing defaults, normalization, and signed-scope validation
behavior.
components/project-studio.tsx (1)

2096-2139: 🚀 Performance & Scalability | 🔵 Trivial | ⚡ Quick win

Throttle the streamed conversation updates.

updateStream calls setProject for every decoded chunk. Each call re-renders ProjectStudio, which is a very large component, and rebuilds the conversation list. A fast token stream produces one full re-render per chunk.

Accumulate the text and flush on an animation frame, then flush once more after the loop completes.

♻️ Suggested throttling
       let reply = "";
+      let frame: number | null = null;
+      const flush = () => {
+        frame = null;
+        updateStream(reply);
+      };
+      const scheduleFlush = () => {
+        if (frame === null) frame = window.requestAnimationFrame(flush);
+      };
       const updateStream = (content: string) => {
@@
         while (true) {
           const chunk = await reader.read();
           if (chunk.done) break;
           reply += decoder.decode(chunk.value, { stream: true });
-          updateStream(reply);
+          scheduleFlush();
         }
         reply += decoder.decode();
+        if (frame !== null) window.cancelAnimationFrame(frame);
         if (!reply.trim()) throw new Error("The selected model returned an empty response.");
🤖 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 2096 - 2139, Throttle
updateStream in the streaming flow so setProject is not called for every decoded
chunk: accumulate the latest reply and schedule conversation updates through
requestAnimationFrame, retaining only the newest pending content per frame.
Flush any pending update once the read loop completes, then preserve the
existing final trim, empty-response validation, and persistence behavior.
e2e/fixtures/ui-test.ts (1)

21-85: 🎯 Functional Correctness | 🔵 Trivial | ⚡ Quick win

Use canonical scope parsing and key helpers, then fail closed without a valid scope.

Use parseProjectStoreScopeCookieValue, projectStoreIndexKey, and projectStoreItemPrefix from lib/project-store.ts instead of duplicating the cookie and storage formats. When the browser has no valid scope cookie, return [] or throw. Do not read the unscoped PROJECTS_STORAGE_KEY; browser-default readProjectsFromStore returns [] in this case.

🤖 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 `@e2e/fixtures/ui-test.ts` around lines 21 - 85, Update scopedProjectStoreKeys,
storedProjectsForCurrentActor, and storedProjectForCurrentActor to use
parseProjectStoreScopeCookieValue, projectStoreIndexKey, and
projectStoreItemPrefix from lib/project-store.ts instead of duplicating scope
parsing and key construction. When no valid scope cookie exists,
storedProjectsForCurrentActor must return [] and storedProjectForCurrentActor
must throw; never read the unscoped PROJECTS_STORAGE_KEY.
🤖 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/api/access/route.ts`:
- Around line 101-117: Configure DROPS_GUEST_COOKIE_SECRET in every production
deployment so resolveStudioProjectActor recognizes signed guests and /api/access
includes projectStoreScope for guest requests. If unsigned guests remain
supported, update the actor/projectStoreScope handling to emit an explicit
compatibility scope instead of omitting it, preserving guest project migration
and saveProjectSafely behavior.

In `@app/api/builder/agent/route.ts`:
- Around line 287-299: Update the auto-start flow around the agent dependency
setup and restored provider selection to prevent paid builds from starting
without explicit consent: force auto-builds to use the free provider, or
validate server-side consent before loading remembered credentials for a paid
provider. Ensure the enforcement occurs in the `/api/builder/agent` request path
rather than relying only on client behavior.

In `@app/styles/project-studio.accessibility.css`:
- Around line 496-499: Increase the contrast of the replacement focus indicator
in .chat-model-select:focus-within by raising the box-shadow color alpha or
using a solid ring color, ensuring it remains clearly visible against the white
composer footer after the native select outline is removed.

In `@components/project-v2-studio-surface.tsx`:
- Around line 739-747: Update the cleanup in the build-run flow’s finally block
so the AUTO_BUILD_KEY lease is removed only for non-cancelled outcomes. Preserve
the lease when the run was cancelled, while retaining the existing timer,
abort-ref, active-run, and busy-state cleanup.
- Around line 1139-1140: Update the Stop button handler in the component around
stopActiveRun and stopSandbox so it calls stopActiveRun only when the active
operation is the builder action, not merely whenever busy is true. For task
runs, checkpoint restores, deployments, and other busy states, call stopSandbox
directly while preserving the existing disabled state and error notification
behavior.
- Line 597: Update the polling flow around the callback containing
result.releaseGate and setActiveView so it does not invoke the rate-limited
builder-runtime-action endpoint every 4 seconds; use a separate status-read
request for build polling, or explicitly handle 429 responses by surfacing the
error and stopping or backing off polling instead of discarding it.

In `@components/telegram-channel-wizard.tsx`:
- Line 348: Update the Change button in the telegram account row, identified by
the button rendered with disconnecting/disconnect, to use at least 44px width
and height and a minimum 14px font size. Add sufficient sizing or padding while
preserving its existing disabled state and click behavior.
- Around line 259-271: The disconnect flow in disconnect should call
forgetStudioConnection("telegram") for every signed-in user, regardless of
accountRemembered, while keeping guest disconnects local. Remove
accountRemembered from the deletion decision, preserve the existing
removal-error handling, and rely on the endpoint’s successful behavior when no
Telegram vault entry exists.

In `@db/studio-account-state.ts`:
- Around line 164-166: Normalize username and botUsername to include the @
prefix before validation in parseTelegramChannelReceipt, accepting unprefixed
Telegram API values. In app/api/telegram/account/create-channel/route.ts lines
60-79, validate the accountId type returned by inspectTelegramAccountToken and
retry saveStudioConnection without telegramReceipt when receipt validation
fails, preserving the rotated credential.

In `@e2e/fixtures/project-v2-ui-test.ts`:
- Around line 229-245: Update the initialization script around the autoBuildKey
and seedKey values so the auto-build suppression marker matches the persisted
project revision after saves. Preserve the existing storage seeding behavior,
but derive suppression from the saved revision or use a stable project-scoped
marker rather than only the original projectV2.revision key.

In `@tests/vercel-sandbox-runtime.live.mjs`:
- Around line 106-113: Move Sandbox handle creation before the try block in the
live test, then pass that precreated handle to adapter.writeProject and retain
it for cleanup. Ensure the finally block always destroys the original handle,
including when writeProject throws before returning.

---

Nitpick comments:
In `@app/api/telegram/account/create-channel/route.ts`:
- Around line 57-58: Rename the inner boolean declaration in the
account-handling block from remembered to a distinct name such as persisted, and
update its assignment and response-body usage near the create-channel handler’s
return path. Keep the outer remembered value for the stored connection secret
unchanged.

In `@app/styles/project-studio.accessibility.css`:
- Around line 160-176: Merge the two adjacent `.studio-splitter svg` CSS rules
into a single rule containing all existing declarations, preserving the current
styles and selector specificity.

In `@app/styles/project-studio.responsive.css`:
- Around line 345-350: Keep the .project-v2-studio-host definition in
app/styles/project-studio.responsive.css#L345-L350 as the single source of
truth. Remove the duplicate four-property block from
app/styles/project-studio.accessibility.css#L615-L620, leaving only the
following tab-code visibility rules unchanged.

In `@components/project-studio.tsx`:
- Around line 2096-2139: Throttle updateStream in the streaming flow so
setProject is not called for every decoded chunk: accumulate the latest reply
and schedule conversation updates through requestAnimationFrame, retaining only
the newest pending content per frame. Flush any pending update once the read
loop completes, then preserve the existing final trim, empty-response
validation, and persistence behavior.

In `@e2e/contracts/v0-studio-flow.spec.ts`:
- Around line 40-62: Update the runtime mock route in the test setup to inspect
the requested action and fulfill with status 409 for any action other than
"status"; retain the existing 200 response payload for the supported status
action, including activeDurationMs: null.

In `@e2e/fixtures/project-v2-ui-test.ts`:
- Around line 109-132: Update the PUT handler to compare
input.expectedStorageRevision with storageRevision before mutating remoteProject
or incrementing the revision; return the fixture’s conflict response when the
supplied revision is stale or mismatched, and continue the existing successful
write flow only for matching revisions.

In `@e2e/fixtures/ui-test.ts`:
- Around line 21-85: Update scopedProjectStoreKeys,
storedProjectsForCurrentActor, and storedProjectForCurrentActor to use
parseProjectStoreScopeCookieValue, projectStoreIndexKey, and
projectStoreItemPrefix from lib/project-store.ts instead of duplicating scope
parsing and key construction. When no valid scope cookie exists,
storedProjectsForCurrentActor must return [] and storedProjectForCurrentActor
must throw; never read the unscoped PROJECTS_STORAGE_KEY.

In `@e2e/interactions/editor-commit.spec.ts`:
- Around line 155-158: Update the editor interaction around originalSource so it
no longer reads, selects, or retypes the document. After focusing the CodeMirror
editor, place the cursor at the document start and type only marker, preserving
the existing assertion and avoiding any transformation of the original content.

In `@e2e/products/free-prompt-game.spec.ts`:
- Around line 103-136: Update the builder route stubs in the test: make the
`/api/builder/agent` gate-failure response use the real non-2xx status and
failure shape without a fabricated successful 200 response, and change the
`/api/builder/runtime` handler to parse the POST body and echo its requested
action instead of always returning `"status"`. Preserve the existing runtime
result fields and unavailable-state behavior.

In `@lib/project-store.ts`:
- Around line 387-402: Extract the duplicated scope-resolution logic into a
shared helper and reuse it from readProjectsFromStore,
claimLegacyProjectsSafely, saveProjectSafely, and deleteProjectSafely. Extract
the repeated navigator lock-detection logic into a second helper and reuse it
wherever those functions currently resolve locks, preserving existing defaults,
normalization, and signed-scope validation behavior.

In `@lib/studio-account-connections-client.ts`:
- Around line 96-108: Define a single provider allow-list tuple near the
existing StudioAccountConnectionProvider declaration, derive the union type from
that tuple, and update the validation in the candidate/provider path to reuse
the tuple instead of its inline array. Ensure adding a provider requires
changing only the shared tuple.
- Around line 178-226: The request functions, including
readStudioAccountSnapshot, duplicate retry and timeout handling. Extract a
shared helper that performs the configured fetch attempts, applies the timeout
signal and retryable status logic, waits via retryPause, and returns the
successful Response or null after exhaustion; then update
readStudioAccountSnapshot and the other two request functions to delegate to it
and only map their payloads, preserving each function’s existing
response-specific behavior.

In `@tests/access-tier.test.mjs`:
- Around line 455-465: Extend the access response test around GET and
setProjectStoreScope to assert the drops_project_scope cookie directly,
including its serialized scope value and path attribute. Keep the existing
payload.projectStoreScope assertions, and read the cookie from the response
rather than inferring it from the JSON body.

In `@tests/agent-chat-route.test.mjs`:
- Around line 213-251: Add a separate test near the existing large-chunk
coverage that streams a single line exceeding MAX_STREAM_LINE_CHARACTERS without
a newline, then assert reading the response rejects with the expected
oversized-line error message. Keep the existing bounded-lines test unchanged.

In `@tests/builder-agent-route.test.mjs`:
- Around line 347-369: Add a separate test near the existing disconnected-client
test that waits until the deterministic fallback begins before calling
controller.abort(), thereby exercising the request.signal abort listener rather
than the already-aborted branch. Await the handler response and assert
cancellation as appropriate, then verify the persisted project state through
stopInterruptedSession’s result or the established persistence fixture,
specifically confirming preview.status is "stopped".

In `@tests/coderabbit-findings.test.mjs`:
- Around line 48-58: Update the assertions around accessIndex and
scopedReadIndex in the test to verify ordering, not just presence: assert that
the `/api/access` request occurs after the readProjectsAfterScopeBootstrap
callback begins. Keep the existing presence checks and other builder assertions
unchanged.

In `@tests/studio-account-connections-client.test.mjs`:
- Around line 149-169: Add coverage alongside the existing
forgetStudioConnection failure test: add a successful response returning deleted
true with an empty connections list and assert one fetch call, then add a
non-retryable 4xx response and assert retryable is false. Reuse the existing
fetch stubbing and cleanup pattern.

In `@tests/studio-account-state.test.mjs`:
- Around line 216-259: Add a Telegram receipt validation case in the test
covering the existing receipt fixture, using a username without the “@” prefix
and asserting the intended parser behavior established in saveStudioConnection.
Keep the current prefixed-username fixture and other invalid-field checks
unchanged.
🪄 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: 282a27c5-7af2-4951-a65d-f91a80e5aa90

📥 Commits

Reviewing files that changed from the base of the PR and between c1f803e and a2d83d0.

⛔ Files ignored due to path filters (10)
  • docs/design/current-home-actual.png is excluded by !**/*.png
  • docs/design/current-studio-actual.png is excluded by !**/*.png
  • e2e/visual/home.spec.ts-snapshots/home-builder-chromium-1024-linux-system.png is excluded by !**/*.png
  • e2e/visual/home.spec.ts-snapshots/home-builder-chromium-1024-linux.png is excluded by !**/*.png
  • e2e/visual/home.spec.ts-snapshots/home-builder-chromium-1440-linux-system.png is excluded by !**/*.png
  • e2e/visual/home.spec.ts-snapshots/home-builder-chromium-1440-linux.png is excluded by !**/*.png
  • e2e/visual/home.spec.ts-snapshots/home-builder-chromium-390-linux-system.png is excluded by !**/*.png
  • e2e/visual/home.spec.ts-snapshots/home-builder-chromium-390-linux.png is excluded by !**/*.png
  • e2e/visual/home.spec.ts-snapshots/studio-crypto-game-chromium-1024-linux-system.png is excluded by !**/*.png
  • e2e/visual/home.spec.ts-snapshots/studio-crypto-game-chromium-1440-linux-system.png is excluded by !**/*.png
📒 Files selected for processing (52)
  • .env.example
  • app/api/access/route.ts
  • app/api/agent/chat/route.ts
  • app/api/auth/google/callback/route.ts
  • app/api/auth/openrouter/exchange/route.ts
  • app/api/auth/session/route.ts
  • app/api/builder/agent/route.ts
  • app/api/telegram/account/create-channel/route.ts
  • app/api/telegram/account/status/route.ts
  • app/styles/project-studio.accessibility.css
  • app/styles/project-studio.responsive.css
  • components/drops-studio.tsx
  • components/project-studio.tsx
  • components/project-v2-studio-surface.tsx
  • components/telegram-channel-wizard.tsx
  • db/studio-account-state.ts
  • docs/SANDBOX_OPERATIONS.md
  • e2e/accessibility/home.spec.ts
  • e2e/contracts/home-builder-p1.spec.ts
  • e2e/contracts/managed-platform-v4.spec.ts
  • e2e/contracts/member-project-cloud.spec.ts
  • e2e/contracts/project-v2-studio.spec.ts
  • e2e/contracts/release-boundaries.spec.ts
  • e2e/contracts/studio-responsive-regressions.spec.ts
  • e2e/contracts/ui-standards.spec.ts
  • e2e/contracts/v0-studio-flow.spec.ts
  • e2e/fixtures/project-v2-ui-test.ts
  • e2e/fixtures/ui-test.ts
  • e2e/interactions/editor-commit.spec.ts
  • e2e/interactions/project-dialog-keyboard.spec.ts
  • e2e/products/free-prompt-game.spec.ts
  • e2e/proofs/all-presets-completion.spec.ts
  • e2e/proofs/design-evidence.spec.ts
  • e2e/proofs/director-flow.spec.ts
  • e2e/proofs/published-products.spec.ts
  • lib/access-tier.ts
  • lib/builder-agent/orchestrator.ts
  • lib/project-store.ts
  • lib/studio-account-connections-client.ts
  • lib/vercel-sandbox-runtime-adapter.ts
  • package.json
  • playwright.config.ts
  • tests/access-tier.test.mjs
  • tests/agent-chat-route.test.mjs
  • tests/builder-agent-route.test.mjs
  • tests/builder-live-flow.live.mjs
  • tests/coderabbit-findings.test.mjs
  • tests/project-store.test.mjs
  • tests/studio-account-connections-client.test.mjs
  • tests/studio-account-state.test.mjs
  • tests/vercel-sandbox-runtime.live.mjs
  • tests/vercel-sandbox-runtime.test.mjs
💤 Files with no reviewable changes (1)
  • tests/vercel-sandbox-runtime.test.mjs

Comment thread app/api/access/route.ts
Comment on lines +101 to +117
&& projectV2SyncReadiness(readinessEnvironment),
});
const signedGuestCookie = context.identityCookie
?? request.cookies.get(GUEST_IDENTITY_COOKIE)?.value;
const actor = signedGuestCookie
? resolveStudioProjectActor(
{ guestCookie: signedGuestCookie },
readinessEnvironment,
)
: null;
const projectStoreScope = actor?.kind === "guest"
? { kind: "guest" as const, identity: actor.identity }
: null;
const response = NextResponse.json(
{
access,
...(projectStoreScope ? { projectStoreScope } : {}),

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

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Description: Check whether guest cookie signing is documented as required and whether a fallback exists.
set -euo pipefail

rg -n 'DROPS_GUEST_COOKIE_SECRET' --glob '!node_modules' -C3
rg -n -C4 'resolveGuestCookieSecret' lib/access-tier.ts

Repository: svg8bit/drops-studio

Length of output: 158


🏁 Script executed:

#!/bin/bash
set -euo pipefail

printf '%s\n' '--- candidate files ---'
git ls-files | rg '(^|/)(app/api/access/route\.ts|lib/access-tier\.ts|.*project.*|.*store.*|.*studio.*)$' | head -200

printf '%s\n' '--- relevant symbols and configuration references ---'
rg -n -C4 \
  'resolveStudioProjectActor|resolveGuestCookieSecret|DROPS_GUEST_COOKIE_SECRET|drops_project_scope|readProjectsFromStore|saveProjectSafely|GUEST_IDENTITY_COOKIE|legacy namespace|Project storage is waiting for a signed actor scope' \
  --glob '!node_modules' --glob '!dist' --glob '!build' .

printf '%s\n' '--- access route ---'
cat -n app/api/access/route.ts | sed -n '70,145p'

Repository: svg8bit/drops-studio

Length of output: 50378


🏁 Script executed:

#!/bin/bash
set -euo pipefail

printf '%s\n' '--- access route ---'
cat -n app/api/access/route.ts | sed -n '1,155p'

printf '%s\n' '--- access-tier actor and guest access logic ---'
cat -n lib/access-tier.ts | sed -n '80,130p'
cat -n lib/access-tier.ts | sed -n '250,365p'

printf '%s\n' '--- project-store scope and migration logic ---'
cat -n lib/project-store.ts | sed -n '1,90p'
cat -n lib/project-store.ts | sed -n '175,310p'
cat -n lib/project-store.ts | sed -n '381,410p'

printf '%s\n' '--- access and deployment documentation ---'
cat -n docs/ACCESS_TIERS.md | sed -n '1,45p'
cat -n docs/V2_SECURITY_MODEL.md | sed -n '30,55p'
cat -n .env.example | sed -n '1,15p'

Repository: svg8bit/drops-studio

Length of output: 29632


🏁 Script executed:

#!/bin/bash
set -euo pipefail

printf '%s\n' '--- scope bootstrap implementation and callers ---'
rg -n -C8 \
  'browserProjectStoreScope|establishActorScope|readProjectsAfterScopeBootstrap|PROJECT_STORE_SCOPE_COOKIE|parseProjectStoreScopeCookieValue|/api/access' \
  lib components app tests --glob '!node_modules'

printf '%s\n' '--- focused project-store tests ---'
rg -n -C8 \
  'legacy|scope|signed actor|Project storage is waiting|DROPS_GUEST_COOKIE_SECRET|no secret|production' \
  tests/project-store.test.mjs tests/*access* tests/*security* 2>/dev/null || true

printf '%s\n' '--- project-store implementation around browser scope ---'
cat -n lib/project-store.ts | sed -n '85,175p'

printf '%s\n' '--- access bootstrap in the main Studio callers ---'
cat -n components/drops-studio.tsx | sed -n '600,735p'
cat -n components/project-studio.tsx | sed -n '900,1060p'

Repository: svg8bit/drops-studio

Length of output: 50377


🏁 Script executed:

#!/bin/bash
set -euo pipefail

printf '%s\n' '--- drops-studio bootstrap and save handling ---'
cat -n components/drops-studio.tsx | sed -n '634,690p'
cat -n components/drops-studio.tsx | sed -n '1868,1935p'

printf '%s\n' '--- project-studio bootstrap and save handling ---'
cat -n components/project-studio.tsx | sed -n '944,1015p'
cat -n components/project-studio.tsx | sed -n '1628,1665p'

printf '%s\n' '--- focused migration tests ---'
cat -n tests/project-store.test.mjs | sed -n '395,445p'
cat -n tests/project-store.test.mjs | sed -n '130,172p'

printf '%s\n' '--- deterministic source-level verifier ---'
python3 - <<'PY'
from pathlib import Path
import re

access = Path("lib/access-tier.ts").read_text()
route = Path("app/api/access/route.ts").read_text()
store = Path("lib/project-store.ts").read_text()
builder = Path("components/drops-studio.tsx").read_text()
studio = Path("components/project-studio.tsx").read_text()

checks = {
    "production_without_guest_secret_returns_empty_secret":
        'return env.NODE_ENV === "production" ? ""' in access,
    "project_actor_requires_guest_secret":
        'const guest = secret\n    ? readGuestIdentityCookie' in access
        and 'if (!guest) return null;' in access,
    "guest_scope_only_when_guest_actor_exists":
        'const projectStoreScope = actor?.kind === "guest"' in route,
    "browser_read_without_scope_returns_empty":
        'if (browserDefault && !scope) return [];' in store,
    "browser_save_without_scope_throws":
        'if (browserDefault && !scope) {\n    throw new Error("Project storage is waiting for a signed actor scope.");' in store,
    "legacy_migration_requires_scope":
        'if (browserDefault && !scope) {\n    throw new Error("Project storage is waiting for a signed actor scope.");' in store
        and 'await claimLegacyProjectsSafely(options);' in store,
    "builder_bootstraps_before_project_read":
        'readProjectsAfterScopeBootstrap(async () =>' in builder
        and 'fetch("/api/access"' in builder,
    "studio_bootstraps_before_project_read":
        'readProjectsAfterScopeBootstrap(async () =>' in studio
        and 'fetch("/api/access"' in studio,
}

for name, result in checks.items():
    print(f"{name}={result}")
if not all(checks.values()):
    raise SystemExit(1)
PY

Repository: svg8bit/drops-studio

Length of output: 15145


🏁 Script executed:

#!/bin/bash
set -euo pipefail

printf '%s\n' '--- drops-studio bootstrap and save handling ---'
cat -n components/drops-studio.tsx | sed -n '634,690p'
cat -n components/drops-studio.tsx | sed -n '1868,1935p'

printf '%s\n' '--- project-studio bootstrap and save handling ---'
cat -n components/project-studio.tsx | sed -n '944,1015p'
cat -n components/project-studio.tsx | sed -n '1628,1665p'

printf '%s\n' '--- focused migration tests ---'
cat -n tests/project-store.test.mjs | sed -n '395,445p'
cat -n tests/project-store.test.mjs | sed -n '130,172p'

printf '%s\n' '--- deterministic source-level verifier ---'
python3 - <<'PY'
from pathlib import Path

access = Path("lib/access-tier.ts").read_text()
route = Path("app/api/access/route.ts").read_text()
store = Path("lib/project-store.ts").read_text()
builder = Path("components/drops-studio.tsx").read_text()
studio = Path("components/project-studio.tsx").read_text()

checks = {
    "production_without_guest_secret_returns_empty_secret":
        'return env.NODE_ENV === "production" ? ""' in access,
    "project_actor_requires_guest_secret":
        'const guest = secret\n    ? readGuestIdentityCookie' in access
        and 'if (!guest) return null;' in access,
    "guest_scope_only_when_guest_actor_exists":
        'const projectStoreScope = actor?.kind === "guest"' in route,
    "browser_read_without_scope_returns_empty":
        'if (browserDefault && !scope) return [];' in store,
    "browser_save_without_scope_throws":
        'if (browserDefault && !scope) {\n    throw new Error("Project storage is waiting for a signed actor scope.");' in store,
    "legacy_migration_requires_scope":
        'if (browserDefault && !scope) {\n    throw new Error("Project storage is waiting for a signed actor scope.");' in store
        and 'await claimLegacyProjectsSafely(options);' in store,
    "builder_bootstraps_before_project_read":
        'readProjectsAfterScopeBootstrap(async () =>' in builder
        and 'fetch("/api/access"' in builder,
    "studio_bootstraps_before_project_read":
        'readProjectsAfterScopeBootstrap(async () =>' in studio
        and 'fetch("/api/access"' in studio,
}

for name, result in checks.items():
    print(f"{name}={result}")
if not all(checks.values()):
    raise SystemExit(1)
PY

Repository: svg8bit/drops-studio

Length of output: 15145


Configure DROPS_GUEST_COOKIE_SECRET in every production deployment. Without it, /api/access omits projectStoreScope; guest browser reads cannot migrate legacy projects, and saveProjectSafely fails, breaking guest project creation and editing. If unsigned guests are supported, add an explicit compatibility scope.

🤖 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/api/access/route.ts` around lines 101 - 117, Configure
DROPS_GUEST_COOKIE_SECRET in every production deployment so
resolveStudioProjectActor recognizes signed guests and /api/access includes
projectStoreScope for guest requests. If unsigned guests remain supported,
update the actor/projectStoreScope handling to emit an explicit compatibility
scope instead of omitting it, preserving guest project migration and
saveProjectSafely behavior.

Source: Coding guidelines

Comment on lines +287 to +299
provider: remembered.selection,
approvedTools: [...approvedTools],
};
const agentDependencies = {
services: session,
audit,
credentials:
parsed.data.mode === "build" ? undefined : remembered.credentials,
credentials: remembered.credentials,
deterministicFallback:
dependencies.deterministicFallback ??
materializedProjectDeterministicFallback,
modelResolver: dependencies.modelResolver,
runnerFactory: dependencies.runnerFactory,
signal: executionController.signal,

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🔒 Security & Privacy | 🟠 Major | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Check quota/consent gating for builder provider selection.
rg -n -C 5 'rememberedBuilderConnection' app/api/builder/shared.ts
rg -n -C 4 'resolveFundedBuildQuota|consumeBuilderLimit|credentialOwner' --type=ts lib app | head -80

Repository: svg8bit/drops-studio

Length of output: 5426


🏁 Script executed:

#!/bin/bash
set -e
printf '%s\n' '--- agent route ---'
sed -n '1,380p' app/api/builder/agent/route.ts
printf '%s\n' '--- shared remembered connection and request policy ---'
sed -n '200,360p' app/api/builder/shared.ts
printf '%s\n' '--- quota call sites in builder routes ---'
rg -n -C 8 'consumeFundedBuildQuota|resolveFundedBuildQuota|rememberedBuilderConnection|credentialOwner|approvedTools|provider:' app/api/builder app/api/workspace lib --type ts
printf '%s\n' '--- studio auto-start references ---'
rg -n -C 12 'auto.?start|autoStart|build' components/project-v2-studio-surface.tsx

Repository: svg8bit/drops-studio

Length of output: 50377


🏁 Script executed:

#!/bin/bash
set -e
printf '%s\n' '--- builder limit implementation ---'
rg -n -C 12 'export async function consumeBuilderLimit|function consumeBuilderLimit|consumeBuilderLimit' app/api/builder/shared.ts
printf '%s\n' '--- agent execution/provider resolution ---'
rg -n -C 10 'type BuilderAgentRequest|interface BuilderAgentRequest|provider\.provider|credentials|modelResolver|runBuilderAgent' lib/builder-agent app/api/builder --glob '*.ts' --glob '!app/api/builder/agent/route.ts'
printf '%s\n' '--- auto-start and builder request construction ---'
rg -n -C 16 'auto.?start|autoStart|handleBuilder|/api/builder/agent|provider:|mode: "build"|mode: .build.' components/project-v2-studio-surface.tsx app --glob '*.tsx' --glob '*.ts'
printf '%s\n' '--- consent/approval state related to provider or build ---'
rg -n -C 10 'consent|approval|approvedTools|paid|provider.*(free|gateway|openai|anthropic|openrouter|kimi|custom)' components app lib --glob '*.tsx' --glob '*.ts' | head -240

Repository: svg8bit/drops-studio

Length of output: 50377


🏁 Script executed:

#!/bin/bash
set -e
printf '%s\n' '--- quota implementation ---'
sed -n '120,180p' app/api/builder/shared.ts
printf '%s\n' '--- builder provider execution ---'
sed -n '164,260p' lib/builder-agent/orchestrator.ts
printf '%s\n' '--- studio provider type and component initialization ---'
sed -n '1,180p' components/project-v2-studio-surface.tsx
printf '%s\n' '--- provider prop call sites ---'
rg -n -C 20 '<ProjectV2StudioSurface|ProjectV2StudioSurface\(' components app --glob '*.tsx' --glob '*.ts'
printf '%s\n' '--- provider state and consent controls ---'
rg -n -C 12 'ProjectProvider|setProvider|provider\s*=|sessionStorage.*provider|connection|Connect|consent' components app --glob '*.tsx' --glob '*.ts' | head -320

Repository: svg8bit/drops-studio

Length of output: 42837


🏁 Script executed:

#!/bin/bash
set -e
printf '%s\n' '--- active provider state and derivation ---'
rg -n -C 18 'activeProvider|connectedBrains|accountBrain|setAccountBrain|setConnectedBrains' components/project-studio.tsx
printf '%s\n' '--- provider connection handlers ---'
rg -n -C 20 'connect.*provider|provider.*connect|save.*provider|sessionStorage\.setItem.*drops-studio|onChooseProvider|activeBrain' components/project-studio.tsx components/drops-studio-setup.tsx
printf '%s\n' '--- static verifier for auto-build provider and gating ---'
python3 - <<'PY'
from pathlib import Path
surface = Path("components/project-v2-studio-surface.tsx").read_text()
route = Path("app/api/builder/agent/route.ts").read_text()
shared = Path("app/api/builder/shared.ts").read_text()
orchestrator = Path("lib/builder-agent/orchestrator.ts").read_text()

checks = {
    "auto_build_passes_build_mode": 'runBuilder(\n        "build",' in surface,
    "auto_build_uses_current_provider": 'provider: providerSelection(provider)' in surface,
    "route_replaces_request_provider_with_remembered_selection": 'provider: remembered.selection' in route,
    "route_passes_remembered_credentials": 'credentials: remembered.credentials' in route,
    "route_has_only_generic_session_limit": 'consumeBuilderLimit(actorId, "builder-agent-session"' in route,
    "route_has_only_generic_minute_limit": 'consumeBuilderLimit(actorId, "builder-agent-minute"' in route,
    "no_paid_cost_gate_symbol_in_route": all(
        token not in route for token in ("cost", "paid", "consent", "providerQuota")
    ),
    "paid_provider_resolves_model": 'const resolved = await (dependencies.modelResolver ?? resolveBuilderModel)' in orchestrator,
}
for name, value in checks.items():
    print(f"{name}={value}")
print("remembered_connection_reads_account_secret=" +
      str("readStudioConnectionSecret(" in shared))
PY

Repository: svg8bit/drops-studio

Length of output: 43209


Require consent before auto-starting paid builds.

The generic session and minute limits do not gate provider cost. The auto-start path passes the restored activeProvider to /api/builder/agent without a per-build consent check. Keep auto-build on free, or require and enforce explicit paid-build consent server-side before loading remembered credentials.

🤖 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/api/builder/agent/route.ts` around lines 287 - 299, Update the auto-start
flow around the agent dependency setup and restored provider selection to
prevent paid builds from starting without explicit consent: force auto-builds to
use the free provider, or validate server-side consent before loading remembered
credentials for a paid provider. Ensure the enforcement occurs in the
`/api/builder/agent` request path rather than relying only on client behavior.

Comment on lines +496 to +499
.chat-model-select:focus-within {
border-radius: 9px;
box-shadow: 0 0 0 3px rgba(49, 108, 255, 0.14);
}

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

Increase the focus-ring contrast for the model selector.

Line 492 removes the native outline of the select. The replacement ring uses rgba(49, 108, 255, 0.14), which is nearly invisible on the white composer footer at line 463. Keyboard users then lose the focus indicator. Raise the alpha or add a solid ring color.

🛡️ Proposed fix
 .chat-model-select:focus-within {
   border-radius: 9px;
-  box-shadow: 0 0 0 3px rgba(49, 108, 255, 0.14);
+  box-shadow: 0 0 0 2px `#ffffff`, 0 0 0 4px rgba(49, 108, 255, 0.85);
 }
📝 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
.chat-model-select:focus-within {
border-radius: 9px;
box-shadow: 0 0 0 3px rgba(49, 108, 255, 0.14);
}
.chat-model-select:focus-within {
border-radius: 9px;
box-shadow: 0 0 0 2px `#ffffff`, 0 0 0 4px rgba(49, 108, 255, 0.85);
}
🤖 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.accessibility.css` around lines 496 - 499, Increase
the contrast of the replacement focus indicator in
.chat-model-select:focus-within by raising the box-shadow color alpha or using a
solid ring color, ensuring it remains clearly visible against the white composer
footer after the native select outline is removed.

Comment thread components/project-v2-studio-surface.tsx
Comment on lines 739 to 747
} finally {
if (statusTimer) clearInterval(statusTimer);
if (builderAbort.current === controller) builderAbort.current = null;
activeRunRef.current = false;
window.sessionStorage.removeItem(
`${AUTO_BUILD_KEY}:${project.id}:${project.revision}`,
);
if (mounted.current) setBusy(null);
}

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

A cancelled run clears the auto-build lease, so the next mount can auto-start the same build again.

The finally block removes ${AUTO_BUILD_KEY}:${project.id}:${project.revision} for every outcome, including user cancellation. autoStarted is a ref, so it does not survive a remount. After the user stops a build, the auto-build effect at Lines 759-780 still matches (storageMode === "cloud", framework nextjs, preview?.status !== "ready"), and the removed lease no longer suppresses it. The stopped build then restarts without user intent.

Keep the lease when the run was cancelled.

🛠️ Proposed fix
     } finally {
       if (statusTimer) clearInterval(statusTimer);
       if (builderAbort.current === controller) builderAbort.current = null;
       activeRunRef.current = false;
-      window.sessionStorage.removeItem(
-        `${AUTO_BUILD_KEY}:${project.id}:${project.revision}`,
-      );
+      if (!controller.signal.aborted) {
+        window.sessionStorage.removeItem(
+          `${AUTO_BUILD_KEY}:${project.id}:${project.revision}`,
+        );
+      }
       if (mounted.current) setBusy(null);
     }
📝 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
} finally {
if (statusTimer) clearInterval(statusTimer);
if (builderAbort.current === controller) builderAbort.current = null;
activeRunRef.current = false;
window.sessionStorage.removeItem(
`${AUTO_BUILD_KEY}:${project.id}:${project.revision}`,
);
if (mounted.current) setBusy(null);
}
} finally {
if (statusTimer) clearInterval(statusTimer);
if (builderAbort.current === controller) builderAbort.current = null;
activeRunRef.current = false;
if (!controller.signal.aborted) {
window.sessionStorage.removeItem(
`${AUTO_BUILD_KEY}:${project.id}:${project.revision}`,
);
}
if (mounted.current) setBusy(null);
}
🤖 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-v2-studio-surface.tsx` around lines 739 - 747, Update the
cleanup in the build-run flow’s finally block so the AUTO_BUILD_KEY lease is
removed only for non-cancelled outcomes. Preserve the lease when the run was
cancelled, while retaining the existing timer, abort-ref, active-run, and
busy-state cleanup.

Comment on lines +259 to +271
async function disconnect() {
setError("");
setDisconnecting(true);
try {
if (accountRemembered) {
const removal = await forgetStudioConnection("telegram");
if (!removal.deleted) {
throw new Error(
removal.error
?? "Telegram could not be removed from your encrypted account vault.",
);
}
}

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

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Check the DELETE semantics for a provider that is not stored.
fd -t f 'route.ts' app/api/account/connections --exec cat -n

Repository: svg8bit/drops-studio

Length of output: 9321


🏁 Script executed:

#!/bin/bash
set -eu

printf '%s\n' '--- candidate files ---'
fd -t f \( 'telegram-channel-wizard.tsx' -o 'route.ts' -o '*studio-account-state*' \)

printf '%s\n' '--- wizard disconnect and state handling ---'
sed -n '1,340p' components/telegram-channel-wizard.tsx

printf '%s\n' '--- Telegram status route ---'
status_route="$(fd -t f 'route.ts' app/api/telegram/account | head -n 1)"
[ -n "$status_route" ] && cat -n "$status_route"

printf '%s\n' '--- account-state implementations and usages ---'
rg -n -C 5 'readStudioConnectionSecret|forgetStudioConnection|deleteStudioConnection|accountRemembered|remembered|clear.*session|disconnect' \
  components app lib db

Repository: svg8bit/drops-studio

Length of output: 320


🏁 Script executed:

#!/bin/bash
set -eu

printf '%s\n' '--- wizard disconnect and state handling ---'
cat -n components/telegram-channel-wizard.tsx | sed -n '1,340p'

printf '%s\n' '--- Telegram status route ---'
cat -n app/api/telegram/account/status/route.ts

printf '%s\n' '--- account-state implementations and usages ---'
rg -n -C 6 'readStudioConnectionSecret|forgetStudioConnection|deleteStudioConnection|accountRemembered|remembered|clear.*session|disconnect' \
  components app lib db

Repository: svg8bit/drops-studio

Length of output: 50377


🏁 Script executed:

#!/bin/bash
set -eu

printf '%s\n' '--- disconnect UI ---'
rg -n -C 12 'disconnect\(|Change|Disconnect|disconnecting' components/telegram-channel-wizard.tsx

printf '%s\n' '--- client DELETE helper ---'
cat -n lib/studio-account-connections-client.ts

printf '%s\n' '--- delete and read implementations ---'
cat -n db/studio-account-state.ts | sed -n '540,660p'

printf '%s\n' '--- signed-in/account resolution and connection status consumers ---'
rg -n -C 8 'function forgetStudioConnection|forgetStudioConnection|memberConnected|resolveStudioAccount|publicConnectionStatuses' \
  lib components app db

Repository: svg8bit/drops-studio

Length of output: 50377


🏁 Script executed:

#!/bin/bash
set -eu

python3 - <<'PY'
from pathlib import Path

status = Path("app/api/telegram/account/status/route.ts").read_text()
wizard = Path("components/telegram-channel-wizard.tsx").read_text()
connections = Path("app/api/account/connections/route.ts").read_text()
state = Path("db/studio-account-state.ts").read_text()

assert 'await readStudioConnectionSecret(account.identity, "telegram").catch(() => null)' in status
assert 'remembered: Boolean(remembered)' in status
assert 'if (!payload.connected || !payload.account)' in wizard
assert 'if (accountRemembered)' in wizard
assert 'window.sessionStorage.removeItem(ACCOUNT_STORAGE_KEY)' in wizard
assert 'if (!actor) return response({ error: "Sign in to change remembered connections." }, 401);' in connections
assert 'delete connections[provider];' in state
assert 'return response({ deleted: true' in connections

def status_result(*, signed_in, vault_read_ok, browser_token, token_valid):
    remembered = signed_in and vault_read_ok
    token = browser_token or (remembered and "vault-token") or ""
    if not token:
        return {"connected": False, "remembered": False, "change_visible": False}
    if not token_valid:
        return {"connected": False, "remembered": False, "change_visible": False}
    return {"connected": True, "remembered": bool(remembered), "change_visible": True}

def disconnect_effect(*, account_remembered, signed_in):
    delete_called = bool(account_remembered)
    delete_result = "deleted" if delete_called and signed_in else ("401" if delete_called else "skipped")
    return delete_called, delete_result

cases = [
    ("signed-in, vault read fails, browser token valid",
     status_result(signed_in=True, vault_read_ok=False, browser_token=True, token_valid=True),
     disconnect_effect(account_remembered=False, signed_in=True)),
    ("signed-in, vault read succeeds, browser token valid",
     status_result(signed_in=True, vault_read_ok=True, browser_token=True, token_valid=True),
     disconnect_effect(account_remembered=True, signed_in=True)),
    ("guest, browser token valid",
     status_result(signed_in=False, vault_read_ok=True, browser_token=True, token_valid=True),
     disconnect_effect(account_remembered=False, signed_in=False)),
    ("signed-in, token inspection fails",
     status_result(signed_in=True, vault_read_ok=True, browser_token=True, token_valid=False),
     disconnect_effect(account_remembered=False, signed_in=True)),
]

for name, status_value, disconnect_value in cases:
    print(name)
    print("  status:", status_value)
    print("  current disconnect:", {
        "delete_called": disconnect_value[0],
        "delete_result": disconnect_value[1],
    })

assert cases[0][1] == {"connected": True, "remembered": False, "change_visible": True}
assert cases[0][2] == (False, "skipped")
assert cases[1][2] == (True, "deleted")
assert cases[2][2] == (False, "skipped")
assert cases[3][1]["change_visible"] is False
PY

Repository: svg8bit/drops-studio

Length of output: 920


Delete the Telegram vault entry on every signed-in disconnect.

When vault reads fail, accountRemembered is false even with a valid browser token. “Change” then clears only session storage, and a later visit restores the vault connection. Do not use accountRemembered to decide whether to call forgetStudioConnection; keep guest disconnects local. The DELETE endpoint already treats an absent provider as success.

🤖 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/telegram-channel-wizard.tsx` around lines 259 - 271, The
disconnect flow in disconnect should call forgetStudioConnection("telegram") for
every signed-in user, regardless of accountRemembered, while keeping guest
disconnects local. Remove accountRemembered from the deletion decision, preserve
the existing removal-error handling, and rely on the endpoint’s successful
behavior when no Telegram vault entry exists.

{(["connected", "creating"].includes(phase)) && account && (
<div className="telegram-channel-builder">
<div className="telegram-account-row"><span><UserRoundCheck /></span><div><strong>{account.displayName}</strong><small>{accountRemembered ? "Encrypted in your Studio account" : account.username || "Connected for this browser tab"}</small></div><b>CONNECTED</b><button type="button" onClick={disconnect}>Change</button></div>
<div className="telegram-account-row"><span><UserRoundCheck /></span><div><strong>{account.displayName}</strong><small>{accountRemembered ? "Encrypted in your Studio account" : account.username || "Connected for this browser tab"}</small></div><b>CONNECTED</b><button type="button" disabled={disconnecting} onClick={() => void disconnect()}>{disconnecting ? "Disconnecting…" : "Change"}</button></div>

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:

#!/bin/bash
# Inspect the Telegram account row button styles.
rg -nP -C6 'telegram-account-row' --glob '*.css'

Repository: svg8bit/drops-studio

Length of output: 158


🏁 Script executed:

#!/bin/bash
set -e
printf '%s\n' '--- matching files ---'
fd -i 'telegram|global|css' . -t f | head -80
printf '%s\n' '--- class references ---'
rg -n -C8 'telegram-account-row|accountRemembered|Change' . --glob '*.{css,tsx,ts,jsx,js}' | head -240

Repository: svg8bit/drops-studio

Length of output: 17795


🏁 Script executed:

#!/bin/bash
set -e
printf '%s\n' '--- Telegram stylesheet matches ---'
rg -n -C12 'telegram-account-row|account-row' app/styles/drops-studio.telegram.css app/styles/*.css
printf '%s\n' '--- component context ---'
sed -n '320,365p' components/telegram-channel-wizard.tsx

Repository: svg8bit/drops-studio

Length of output: 24949


🏁 Script executed:

#!/bin/bash
set -e
printf '%s\n' '--- all account-row button rules ---'
rg -n -C4 'telegram-account-row[^}]*button|button[^}]*telegram-account-row|(^|[,{[:space:]])button([[:space:]{:.]|$)' app/globals.css app/styles --glob '*.css' | head -260
printf '%s\n' '--- stylesheet imports/order ---'
rg -n 'drops-studio.telegram.css|globals.css|`@import`' app components --glob '*.{css,tsx,ts,jsx,js}' | head -160

Repository: svg8bit/drops-studio

Length of output: 30726


Set the Change control to at least 44 × 44 CSS pixels and 14 px text.

.telegram-account-row > button sets font-size: 12px and no size or padding.

🤖 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/telegram-channel-wizard.tsx` at line 348, Update the Change button
in the telegram account row, identified by the button rendered with
disconnecting/disconnect, to use at least 44px width and height and a minimum
14px font size. Add sufficient sizing or padding while preserving its existing
disabled state and click behavior.

Source: Coding guidelines

Comment on lines +164 to +166
|| !/^@[A-Za-z0-9_]{3,32}$/.test(botUsername)
|| (username && !/^@[A-Za-z][A-Za-z0-9_]{4,31}$/.test(username))
) return undefined;

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

Receipt validation is strict and the producer does not normalize, so a valid channel can lose its persisted session. parseTelegramChannelReceipt returns undefined for values that the Telegram API commonly returns, and saveStudioConnection then throws for the whole write, including the rotated credential. The route catches that error and only logs it.

  • db/studio-account-state.ts#L164-L166: normalize username and botUsername to the @ prefix before the regex test, instead of rejecting unprefixed values.
  • app/api/telegram/account/create-channel/route.ts#L60-L79: confirm the accountId type returned by inspectTelegramAccountToken, and retry the save without telegramReceipt when the receipt is rejected, so the rotated credential is still persisted.
📍 Affects 2 files
  • db/studio-account-state.ts#L164-L166 (this comment)
  • app/api/telegram/account/create-channel/route.ts#L60-L79
🤖 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 `@db/studio-account-state.ts` around lines 164 - 166, Normalize username and
botUsername to include the @ prefix before validation in
parseTelegramChannelReceipt, accepting unprefixed Telegram API values. In
app/api/telegram/account/create-channel/route.ts lines 60-79, validate the
accountId type returned by inspectTelegramAccountToken and retry
saveStudioConnection without telegramReceipt when receipt validation fails,
preserving the rotated credential.

Comment thread e2e/fixtures/project-v2-ui-test.ts
Comment on lines +106 to +113
const handle = await adapter.writeProject(context);

try {
const result = await adapter.runTests(context, handle);
assert.equal(result.exitCode, 0);
} finally {
await adapter.destroy(handle);
}

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

Clean up the Sandbox when writeProject fails.

Line 106 can provision a persistent Sandbox. If writeProject throws before it returns, line 112 does not run. The failed live test can leave a billable Sandbox active.

Create the Sandbox handle before the try block. Pass that handle to writeProject. Destroy the original handle in finally.

Proposed fix
-  const handle = await adapter.writeProject(context);
+  const handle = await adapter.ensure(context);
 
   try {
-    const result = await adapter.runTests(context, handle);
+    const writtenHandle = await adapter.writeProject(context, handle);
+    const result = await adapter.runTests(context, writtenHandle);
     assert.equal(result.exitCode, 0);
   } finally {
     await adapter.destroy(handle);
   }

As per coding guidelines, “Every external or destructive tool must have explicit approval, timeout, quota, audit record, bounded output, and idempotency behavior.”

🤖 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/vercel-sandbox-runtime.live.mjs` around lines 106 - 113, Move Sandbox
handle creation before the try block in the live test, then pass that precreated
handle to adapter.writeProject and retain it for cleanup. Ensure the finally
block always destroys the original handle, including when writeProject throws
before returning.

Source: Coding guidelines

@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: 2

🧹 Nitpick comments (3)
lib/project-store.ts (1)

390-402: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Duplicated scope and lock resolution in saveProjectSafely and deleteProjectSafely. Both functions repeat the same browserDefault check, scope ternary, signed-scope guard, and navigator.locks detection. readProjectsFromStore and claimLegacyProjectsSafely contain the same pattern. The shared root cause is the absence of one resolver helper.

  • lib/project-store.ts#L390-L402: replace the inline block with a call to a shared resolveStoreAccess(options) helper that returns { storage, scope, locks } and applies the signed-scope guard.
  • lib/project-store.ts#L467-L479: replace the identical inline block with the same resolveStoreAccess(options) call.
♻️ Proposed helper
function resolveStoreAccess(
  options: ProjectStoreAccessOptions,
  requireScope: boolean,
): { storage: StorageLike; scope: ProjectStoreScope | null; locks: LockManagerLike | null } {
  const browserDefault = options.storage === undefined;
  const storage = options.storage ?? window.localStorage;
  const scope = options.scope === undefined
    ? browserDefault ? browserProjectStoreScope() : null
    : options.scope ? normalizeScope(options.scope) : null;
  if (requireScope && browserDefault && !scope) {
    throw new Error("Project storage is waiting for a signed actor scope.");
  }
  const locks = options.locks === undefined
    ? (browserDefault && typeof navigator !== "undefined" && "locks" in navigator
        ? navigator.locks as unknown as LockManagerLike
        : null)
    : options.locks;
  return { storage, scope, locks };
}
🤖 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-store.ts` around lines 390 - 402, Introduce a shared
resolveStoreAccess helper in lib/project-store.ts that resolves storage, scope,
and locks and applies the signed-scope guard when required. Replace the
duplicated inline resolution blocks at lib/project-store.ts:390-402 and
lib/project-store.ts:467-479 with calls to this helper, preserving each caller’s
scope requirement and returned access values.
e2e/fixtures/ui-test.ts (2)

38-57: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

Document that this reader returns the compatibility index only.

writeProject in lib/project-store.ts stores a compacted index. It removes projectV2 through compactProjectForCompatibilityIndex. This fixture reads only the index, so any assertion on projectV2 through storedProjectsForCurrentActor will fail even when the canonical item record contains the data. storedProjectForCurrentActor reads the item record first and does not have this limitation.

Add a short comment, or merge the scoped item records like readProjects does.

Note also the inconsistency: this function resolves the cookie inside page.evaluate, while storedProjectForCurrentActor resolves it through page.context().cookies(). Use one approach.

🤖 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 `@e2e/fixtures/ui-test.ts` around lines 38 - 57, Document in
storedProjectsForCurrentActor that it reads only the compacted compatibility
index and cannot provide projectV2 data; alternatively, merge scoped item
records as readProjects does. Also align its scope-cookie resolution with
storedProjectForCurrentActor by using the same established approach rather than
resolving the cookie differently inside page.evaluate.

21-36: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Use the canonical project-store key helpers.

Import and use parseProjectStoreScopeCookieValue, projectStoreIndexKey, and projectStoreItemPrefix. Use PROJECT_STORE_ITEM_PREFIX for the unscoped itemKey; it currently matches the hard-coded prefix but must not be duplicated.

🤖 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 `@e2e/fixtures/ui-test.ts` around lines 21 - 36, Update scopedProjectStoreKeys
to use parseProjectStoreScopeCookieValue for cookie parsing,
projectStoreIndexKey for the scoped index key, and projectStoreItemPrefix for
scoped item keys. For unscoped keys, use PROJECT_STORE_ITEM_PREFIX when
constructing itemKey instead of duplicating the hard-coded prefix, while
preserving the existing projectId-null behavior.
🤖 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 `@lib/project-store.ts`:
- Around line 217-231: Update migrateLegacyProjectsToScope to track all
migration owners rather than treating PROJECT_STORE_LEGACY_MIGRATION_KEY as a
single owner string. Preserve existing claims, allow a member scope to adopt
data from a previously claimed guest scope on the same device, merge both the
legacy global keys and the prior guest namespace, and append the current owner
after successful migration while retaining existing project merge precedence.
- Around line 175-186: Update readProjectsFromStore so a browser scope bootstrap
failure is represented separately from an empty project list instead of
returning []; preserve [] for a successfully resolved scope with no projects.
Adjust the project-loading fallback in project-studio.tsx to handle the
unavailable-scope result without rendering “Project not found” for an existing
project, while keeping saveProjectSafely’s absent-scope rejection behavior
unchanged.

---

Nitpick comments:
In `@e2e/fixtures/ui-test.ts`:
- Around line 38-57: Document in storedProjectsForCurrentActor that it reads
only the compacted compatibility index and cannot provide projectV2 data;
alternatively, merge scoped item records as readProjects does. Also align its
scope-cookie resolution with storedProjectForCurrentActor by using the same
established approach rather than resolving the cookie differently inside
page.evaluate.
- Around line 21-36: Update scopedProjectStoreKeys to use
parseProjectStoreScopeCookieValue for cookie parsing, projectStoreIndexKey for
the scoped index key, and projectStoreItemPrefix for scoped item keys. For
unscoped keys, use PROJECT_STORE_ITEM_PREFIX when constructing itemKey instead
of duplicating the hard-coded prefix, while preserving the existing
projectId-null behavior.

In `@lib/project-store.ts`:
- Around line 390-402: Introduce a shared resolveStoreAccess helper in
lib/project-store.ts that resolves storage, scope, and locks and applies the
signed-scope guard when required. Replace the duplicated inline resolution
blocks at lib/project-store.ts:390-402 and lib/project-store.ts:467-479 with
calls to this helper, preserving each caller’s scope requirement and returned
access values.
🪄 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: 597254ee-2525-496d-bf91-0bc49304b3f4

📥 Commits

Reviewing files that changed from the base of the PR and between a2d83d0 and 59b69f5.

⛔ Files ignored due to path filters (10)
  • docs/design/current-home-actual.png is excluded by !**/*.png
  • docs/design/current-studio-actual.png is excluded by !**/*.png
  • e2e/visual/home.spec.ts-snapshots/home-builder-chromium-1024-linux-system.png is excluded by !**/*.png
  • e2e/visual/home.spec.ts-snapshots/home-builder-chromium-1024-linux.png is excluded by !**/*.png
  • e2e/visual/home.spec.ts-snapshots/home-builder-chromium-1440-linux-system.png is excluded by !**/*.png
  • e2e/visual/home.spec.ts-snapshots/home-builder-chromium-1440-linux.png is excluded by !**/*.png
  • e2e/visual/home.spec.ts-snapshots/home-builder-chromium-390-linux-system.png is excluded by !**/*.png
  • e2e/visual/home.spec.ts-snapshots/home-builder-chromium-390-linux.png is excluded by !**/*.png
  • e2e/visual/home.spec.ts-snapshots/studio-crypto-game-chromium-1024-linux-system.png is excluded by !**/*.png
  • e2e/visual/home.spec.ts-snapshots/studio-crypto-game-chromium-1440-linux-system.png is excluded by !**/*.png
📒 Files selected for processing (53)
  • .env.example
  • app/api/access/route.ts
  • app/api/agent/chat/route.ts
  • app/api/auth/google/callback/route.ts
  • app/api/auth/openrouter/exchange/route.ts
  • app/api/auth/session/route.ts
  • app/api/builder/agent/route.ts
  • app/api/telegram/account/create-channel/route.ts
  • app/api/telegram/account/status/route.ts
  • app/styles/project-studio.accessibility.css
  • app/styles/project-studio.responsive.css
  • components/drops-studio.tsx
  • components/project-studio.tsx
  • components/project-v2-studio-surface.tsx
  • components/telegram-channel-wizard.tsx
  • db/studio-account-state.ts
  • docs/SANDBOX_OPERATIONS.md
  • e2e/accessibility/home.spec.ts
  • e2e/contracts/home-builder-p1.spec.ts
  • e2e/contracts/managed-platform-v4.spec.ts
  • e2e/contracts/member-project-cloud.spec.ts
  • e2e/contracts/project-v2-studio.spec.ts
  • e2e/contracts/release-boundaries.spec.ts
  • e2e/contracts/studio-responsive-regressions.spec.ts
  • e2e/contracts/ui-standards.spec.ts
  • e2e/contracts/v0-studio-flow.spec.ts
  • e2e/fixtures/project-v2-ui-test.ts
  • e2e/fixtures/ui-test.ts
  • e2e/interactions/editor-commit.spec.ts
  • e2e/interactions/project-dialog-keyboard.spec.ts
  • e2e/products/free-prompt-game.spec.ts
  • e2e/proofs/all-presets-completion.spec.ts
  • e2e/proofs/design-evidence.spec.ts
  • e2e/proofs/director-flow.spec.ts
  • e2e/proofs/published-products.spec.ts
  • lib/access-tier.ts
  • lib/builder-agent/orchestrator.ts
  • lib/project-store.ts
  • lib/studio-account-connections-client.ts
  • lib/vercel-sandbox-runtime-adapter.ts
  • package.json
  • playwright.config.ts
  • tests/access-tier.test.mjs
  • tests/agent-chat-route.test.mjs
  • tests/builder-agent-route.test.mjs
  • tests/builder-live-flow.live.mjs
  • tests/coderabbit-findings.test.mjs
  • tests/project-store.test.mjs
  • tests/rendered-html.test.mjs
  • tests/studio-account-connections-client.test.mjs
  • tests/studio-account-state.test.mjs
  • tests/vercel-sandbox-runtime.live.mjs
  • tests/vercel-sandbox-runtime.test.mjs
💤 Files with no reviewable changes (1)
  • tests/vercel-sandbox-runtime.test.mjs
🚧 Files skipped from review as they are similar to previous changes (48)
  • e2e/contracts/home-builder-p1.spec.ts
  • e2e/proofs/all-presets-completion.spec.ts
  • tests/studio-account-state.test.mjs
  • e2e/proofs/published-products.spec.ts
  • e2e/contracts/managed-platform-v4.spec.ts
  • lib/builder-agent/orchestrator.ts
  • app/api/auth/google/callback/route.ts
  • lib/vercel-sandbox-runtime-adapter.ts
  • tests/builder-live-flow.live.mjs
  • e2e/proofs/design-evidence.spec.ts
  • app/api/auth/openrouter/exchange/route.ts
  • tests/access-tier.test.mjs
  • e2e/accessibility/home.spec.ts
  • tests/project-store.test.mjs
  • docs/SANDBOX_OPERATIONS.md
  • app/api/telegram/account/status/route.ts
  • tests/coderabbit-findings.test.mjs
  • playwright.config.ts
  • app/api/access/route.ts
  • app/api/telegram/account/create-channel/route.ts
  • tests/builder-agent-route.test.mjs
  • e2e/products/free-prompt-game.spec.ts
  • tests/agent-chat-route.test.mjs
  • e2e/contracts/v0-studio-flow.spec.ts
  • e2e/contracts/project-v2-studio.spec.ts
  • e2e/interactions/editor-commit.spec.ts
  • tests/studio-account-connections-client.test.mjs
  • package.json
  • e2e/contracts/member-project-cloud.spec.ts
  • components/drops-studio.tsx
  • e2e/fixtures/project-v2-ui-test.ts
  • app/api/builder/agent/route.ts
  • e2e/proofs/director-flow.spec.ts
  • lib/access-tier.ts
  • e2e/interactions/project-dialog-keyboard.spec.ts
  • e2e/contracts/ui-standards.spec.ts
  • app/api/agent/chat/route.ts
  • e2e/contracts/studio-responsive-regressions.spec.ts
  • app/styles/project-studio.accessibility.css
  • app/styles/project-studio.responsive.css
  • e2e/contracts/release-boundaries.spec.ts
  • app/api/auth/session/route.ts
  • components/telegram-channel-wizard.tsx
  • lib/studio-account-connections-client.ts
  • components/project-v2-studio-surface.tsx
  • db/studio-account-state.ts
  • tests/vercel-sandbox-runtime.live.mjs
  • components/project-studio.tsx

Comment thread lib/project-store.ts
Comment on lines 175 to +186
export function readProjectsFromStore(
storage: StorageLike = window.localStorage,
storage?: StorageLike,
options: ProjectStoreReadOptions = {},
): GeneratedProject[] {
const browserDefault = storage === undefined;
const resolvedStorage = storage ?? window.localStorage;
const scope = options.scope === undefined
? browserDefault ? browserProjectStoreScope() : null
: options.scope ? normalizeScope(options.scope) : null;
if (browserDefault && !scope) return [];
return readProjects(resolvedStorage, scope);
}

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

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Find zero-argument readProjectsFromStore call sites and their surrounding logic.
rg -nP -C 12 '\breadProjectsFromStore\s*\(\s*\)' --type=ts --type=tsx
rg -nP -C 6 '\breadProjectsAfterScopeBootstrap\s*\(' --type=ts --type=tsx

Repository: svg8bit/drops-studio

Length of output: 222


🏁 Script executed:

#!/bin/bash
set -eu

printf '%s\n' 'Zero-argument call sites:'
rg -n -P -C 16 '\breadProjectsFromStore\s*\(\s*\)' -g '*.ts' -g '*.tsx' -g '*.js' -g '*.jsx' . || true

printf '%s\n' 'Bootstrap call sites:'
rg -n -P -C 10 '\breadProjectsAfterScopeBootstrap\s*\(' -g '*.ts' -g '*.tsx' -g '*.js' -g '*.jsx' . || true

printf '%s\n' 'Store implementation:'
cat -n lib/project-store.ts | sed -n '130,285p'

Repository: svg8bit/drops-studio

Length of output: 16022


🏁 Script executed:

#!/bin/bash
set -eu

printf '%s\n' 'project-studio load effect:'
cat -n components/project-studio.tsx | sed -n '880,1085p'

printf '%s\n' 'project-studio team-project path:'
cat -n components/project-studio.tsx | sed -n '1640,1770p'

printf '%s\n' 'drops-studio bootstrap continuation:'
cat -n components/drops-studio.tsx | sed -n '600,730p'

printf '%s\n' 'scope and store helper definitions/usages:'
rg -n -C 8 'browserProjectStoreScope|ProjectStoreReadOptions|readProjectsFromStore|claimLegacyProjectsSafely' lib components -g '*.ts' -g '*.tsx'

Repository: svg8bit/drops-studio

Length of output: 34362


🏁 Script executed:

#!/bin/bash
set -eu

printf '%s\n' 'Project load completion and missing-project handling:'
cat -n components/project-studio.tsx | sed -n '1040,1165p'
rg -n -C 12 'drops_project_scope|PROJECT_STORE_SCOPE_COOKIE|set.*scope|scope.*cookie' app pages lib components -g '*.ts' -g '*.tsx' 2>/dev/null || true

printf '%s\n' 'Access endpoint files:'
rg -l 'api/access|function.*access|export.*GET|export.*POST' app pages -g '*.ts' -g '*.tsx' 2>/dev/null | sort

printf '%s\n' 'Project-store write and save paths:'
cat -n lib/project-store.ts | sed -n '313,420p'

Repository: svg8bit/drops-studio

Length of output: 50377


🏁 Script executed:

#!/bin/bash
set -eu

printf '%s\n' 'Access route candidates:'
fd -t f -i 'access|route' app pages 2>/dev/null | head -80

printf '%s\n' 'Exact scope-cookie references:'
rg -n -C 10 'drops_project_scope|PROJECT_STORE_SCOPE_COOKIE' app pages lib components -g '*.ts' -g '*.tsx' 2>/dev/null | head -240

printf '%s\n' 'Missing-project rendering:'
rg -n -C 10 'project.*not found|not found|params\.id|setProject\(null|project === null|!project' components/project-studio.tsx -g '*.tsx' | head -260

printf '%s\n' 'Save implementation:'
cat -n lib/project-store.ts | sed -n '313,415p'

Repository: svg8bit/drops-studio

Length of output: 29631


🏁 Script executed:

#!/bin/bash
set -eu

printf '%s\n' 'Access route scope establishment:'
cat -n app/api/access/route.ts | sed -n '25,190p'

printf '%s\n' 'Project-studio loaded and missing-project branches:'
rg -n -C 14 'loaded|Project not found|project not found|No project|runtimeProject|return \(' components/project-studio.tsx -g '*.tsx' | tail -320

printf '%s\n' 'Team project callback references:'
rg -n -C 12 'applyTeamProject|onApply|sharedProject' components/project-studio.tsx components -g '*.tsx' | head -300

Repository: svg8bit/drops-studio

Length of output: 38830


🏁 Script executed:

#!/bin/bash
set -eu

python3 - <<'PY'
from pathlib import Path
import re

store = Path("lib/project-store.ts").read_text()
studio = Path("components/project-studio.tsx").read_text()

calls = list(re.finditer(r"\breadProjectsFromStore\s*\(\s*\)", studio))
print(f"zero_argument_calls={len(calls)}")
for match in calls:
    line = studio.count("\n", 0, match.start()) + 1
    context = studio[match.start():].splitlines()[0].strip()
    print(f"line={line} call={context}")

print("empty_without_browser_scope=", "if (browserDefault && !scope) return [];" in store)
print("write_rejects_without_browser_scope=", 'if (browserDefault && !scope) {\n    throw new Error("Project storage is waiting for a signed actor scope.");' in store)
print("load_fallback_reads_zero_argument_store=", "readProjectsFromStore().find" in studio)
print("load_marks_loaded_after_missing_found=", "if (!project || !preset)" in studio and "setLoaded(true)" in studio)
PY

Repository: svg8bit/drops-studio

Length of output: 477


Do not render an existing project as “Project not found” when scope bootstrap fails.

At components/project-studio.tsx:1038, the fallback can return [] without drops_project_scope; the load then renders the missing-project state. Keep scope-unavailable separate from an empty project list. The call at line 1725 is safe because saveProjectSafely rejects an absent scope.

🤖 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-store.ts` around lines 175 - 186, Update readProjectsFromStore so
a browser scope bootstrap failure is represented separately from an empty
project list instead of returning []; preserve [] for a successfully resolved
scope with no projects. Adjust the project-loading fallback in
project-studio.tsx to handle the unavailable-scope result without rendering
“Project not found” for an existing project, while keeping saveProjectSafely’s
absent-scope rejection behavior unchanged.

Comment thread lib/project-store.ts
Comment on lines +217 to +231
function migrateLegacyProjectsToScope(
scopeInput: ProjectStoreScope,
storage: StorageLike = window.localStorage,
): boolean {
const scope = normalizeScope(scopeInput);
const owner = projectStoreScopeCookieValue(scope);
const currentOwner = storage.getItem(PROJECT_STORE_LEGACY_MIGRATION_KEY);
if (currentOwner) return currentOwner === owner;

const merged = new Map<string, GeneratedProject>();
for (const project of readProjects(storage, null)) merged.set(project.id, project);
for (const project of readProjects(storage, scope)) {
const current = merged.get(project.id);
if (!current || timestamp(project) >= timestamp(current)) merged.set(project.id, project);
}

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

Guest-owned legacy data is never adopted by the member scope.

PROJECT_STORE_LEGACY_MIGRATION_KEY holds a single owner string. Line 224 returns early when any owner exists. Consider the normal upgrade path:

  1. A guest builds projects. The guest scope claims the marker as guest.<identity>.
  2. The user signs in. app/api/auth/google/callback/route.ts replaces the cookie with member.<identity>.
  3. migrateLegacyProjectsToScope(memberScope) reads currentOwner === "guest.<id>" and returns false.

The member scope index stays empty. The legacy keys remain in localStorage, but the signed-in user no longer sees the projects that the same browser created as a guest. That breaks the "Preserve V1 projects" product outcome for the most common sign-in flow.

Track claims per scope, and let a member scope adopt data that a guest scope on the same device already claimed.

🐛 Proposed direction
-  const currentOwner = storage.getItem(PROJECT_STORE_LEGACY_MIGRATION_KEY);
-  if (currentOwner) return currentOwner === owner;
+  // Record one claim per scope so a member scope can adopt guest-claimed
+  // legacy data on the same device.
+  const claimedOwners = parseClaimedOwners(
+    storage.getItem(PROJECT_STORE_LEGACY_MIGRATION_KEY),
+  );
+  if (claimedOwners.includes(owner)) return true;

Then merge from the legacy global keys and from the previously claimed guest namespace, and append owner to the claim list instead of overwriting it.

As per coding guidelines: "Preserve V1 projects, all 12 recipes, /p/{slug}, ZIP export, current connections, checkpoints, and session-only BYOK behavior as product outcomes; verify compatibility before release."

🤖 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-store.ts` around lines 217 - 231, Update
migrateLegacyProjectsToScope to track all migration owners rather than treating
PROJECT_STORE_LEGACY_MIGRATION_KEY as a single owner string. Preserve existing
claims, allow a member scope to adopt data from a previously claimed guest scope
on the same device, merge both the legacy global keys and the prior guest
namespace, and append the current owner after successful migration while
retaining existing project merge precedence.

Source: Coding guidelines

@svg8bit
svg8bit merged commit e9a9c7a into main Aug 1, 2026
4 checks passed
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