Ship reliable v0-like Studio flow - #28
Conversation
|
The latest updates on your projects. Learn more about Vercel for GitHub.
|
|
Important Review skippedReview was skipped due to path filters ⛔ Files ignored due to path filters (2)
CodeRabbit blocks several paths by default. You can override this behavior by explicitly including those paths in the path filters. For example, including ⚙️ Run configurationConfiguration used: defaults Review profile: CHILL Plan: Pro Plus Run ID: You can disable this status message by setting the Use the checkbox below for a quick retry:
📝 WalkthroughWalkthroughThe 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. ChangesProject V2 platform and workspace
Account connections and Telegram persistence
Live test infrastructure
Estimated code review effort: 5 (Critical) | ~120 minutes Possibly related PRs
🚥 Pre-merge checks | ✅ 4 | ❌ 1❌ Failed checks (1 warning)
✅ Passed checks (4 passed)
✨ Finishing Touches 💡 1📝 Generate docstrings 💡
🧪 Generate unit tests (beta)
Comment |
a2d83d0 to
59b69f5
Compare
There was a problem hiding this comment.
💡 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".
| autoStarted.current === `${project.id}:${project.revision}` || | ||
| storageMode !== "cloud" || | ||
| project.manifest.framework.name !== "nextjs" || | ||
| project.preview?.status === "ready" |
There was a problem hiding this comment.
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 👍 / 👎.
| {directing && usesNativeProjectV2 ? ( | ||
| <button | ||
| type="button" | ||
| className="chat-stop-button" | ||
| aria-label="Stop current build" | ||
| onClick={() => void projectV2SurfaceRef.current?.stop()} |
There was a problem hiding this comment.
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 👍 / 👎.
| statusTimer = setInterval(() => { | ||
| void refreshSandboxStatus(snapshot.project.id).catch(() => undefined); | ||
| }, 4_000); |
There was a problem hiding this comment.
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 👍 / 👎.
| response.cookies.set(PROJECT_STORE_SCOPE_COOKIE, "", { | ||
| httpOnly: false, | ||
| sameSite: "lax", | ||
| secure: process.env.NODE_ENV === "production", | ||
| expires: new Date(0), | ||
| path: "/", | ||
| }); |
There was a problem hiding this comment.
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 👍 / 👎.
There was a problem hiding this comment.
Actionable comments posted: 11
🧹 Nitpick comments (18)
app/api/telegram/account/create-channel/route.ts (1)
57-58: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low valueRename the local flag to avoid shadowing
remembered.Line 35 already binds
rememberedto the stored connection secret. Line 57 declares a secondrememberedboolean in the inner block. Both names describe different data in one function. Rename the flag, for examplepersisted, 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 winAdd 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 winAdd the successful deletion path.
The suite covers only the retryable failure. The wizard treats
deleted === trueas 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 assertsretryable === 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 winReuse one provider allow-list.
The inline array repeats the
StudioAccountConnectionProviderunion at Lines 1-9. A new provider must then be added in two places. Declare oneconsttuple 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 liftExtract 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 winThe builder stubs do not match the real API contract.
Two mismatches reduce the value of this boundary:
- The
/api/builder/agentstub returns HTTP 200 with onlycodeanderrorand noresult. The real route returns a non-2xx status for gate failures, and a 200 response withoutresultis not a shape the server produces. A client that starts treating 200 as success would still pass this test.- The
/api/builder/runtimestub always answers withaction: "status". The test path can also requeststop,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 winAdd 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 atapp/api/agent/chat/route.tsLines 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 winThe abort test only covers the already-aborted path and does not assert the persisted stopped state.
controller.abort()runs before the handler reachesrequest.signal.addEventListener, so this test exercises only the explicitif (request.signal.aborted)carry-over atapp/api/builder/agent/route.tsLine 263. The listener path stays uncovered. Add a second case that aborts after the fallback starts, and assert thatstopInterruptedSessionpersistedpreview.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 valueDuplicate
.project-v2-studio-hostdefinition 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 thegrid-template-areasthat define thecanvasarea.app/styles/project-studio.accessibility.css#L615-L620: remove the duplicated block and keep only thetab-codevisibility 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 valueMerge the duplicated
.studio-splitter svgrule.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 winEnforce
expectedStorageRevisionin the PUT mock.The handler types
expectedStorageRevisionbut never compares it tostorageRevision. 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 winInsert the marker instead of retyping the whole document.
textContent()on.cm-contentreturns 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 winReject unsupported runtime actions in the mock.
Return
409unlessactionis"status".activeDurationMs: nullis 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 winAssert the ordering the index variables imply.
accessIndexandscopedReadIndexare computed but only checked for presence. Twoassert.matchcalls would prove the same thing. The invariant this PR depends on is ordering: the/api/accessrequest must run inside thereadProjectsAfterScopeBootstrapcallback, 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 winAssert the scope cookie, not only the response body.
The browser project store reads the scope from the
drops_project_scopecookie, not from the JSON body. This test verifiespayload.projectStoreScopebut never checks the cookie thatsetProjectStoreScopewrites. Add an assertion on the cookie value so a regression in the cookie name, value format, orpathfails 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 winExtract the repeated scope and lock resolution.
The same scope-resolution block appears in
readProjectsFromStore,claimLegacyProjectsSafely,saveProjectSafely, anddeleteProjectSafely. 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 winThrottle the streamed conversation updates.
updateStreamcallssetProjectfor every decoded chunk. Each call re-rendersProjectStudio, 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 winUse canonical scope parsing and key helpers, then fail closed without a valid scope.
Use
parseProjectStoreScopeCookieValue,projectStoreIndexKey, andprojectStoreItemPrefixfromlib/project-store.tsinstead of duplicating the cookie and storage formats. When the browser has no valid scope cookie, return[]or throw. Do not read the unscopedPROJECTS_STORAGE_KEY; browser-defaultreadProjectsFromStorereturns[]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
⛔ Files ignored due to path filters (10)
docs/design/current-home-actual.pngis excluded by!**/*.pngdocs/design/current-studio-actual.pngis excluded by!**/*.pnge2e/visual/home.spec.ts-snapshots/home-builder-chromium-1024-linux-system.pngis excluded by!**/*.pnge2e/visual/home.spec.ts-snapshots/home-builder-chromium-1024-linux.pngis excluded by!**/*.pnge2e/visual/home.spec.ts-snapshots/home-builder-chromium-1440-linux-system.pngis excluded by!**/*.pnge2e/visual/home.spec.ts-snapshots/home-builder-chromium-1440-linux.pngis excluded by!**/*.pnge2e/visual/home.spec.ts-snapshots/home-builder-chromium-390-linux-system.pngis excluded by!**/*.pnge2e/visual/home.spec.ts-snapshots/home-builder-chromium-390-linux.pngis excluded by!**/*.pnge2e/visual/home.spec.ts-snapshots/studio-crypto-game-chromium-1024-linux-system.pngis excluded by!**/*.pnge2e/visual/home.spec.ts-snapshots/studio-crypto-game-chromium-1440-linux-system.pngis excluded by!**/*.png
📒 Files selected for processing (52)
.env.exampleapp/api/access/route.tsapp/api/agent/chat/route.tsapp/api/auth/google/callback/route.tsapp/api/auth/openrouter/exchange/route.tsapp/api/auth/session/route.tsapp/api/builder/agent/route.tsapp/api/telegram/account/create-channel/route.tsapp/api/telegram/account/status/route.tsapp/styles/project-studio.accessibility.cssapp/styles/project-studio.responsive.csscomponents/drops-studio.tsxcomponents/project-studio.tsxcomponents/project-v2-studio-surface.tsxcomponents/telegram-channel-wizard.tsxdb/studio-account-state.tsdocs/SANDBOX_OPERATIONS.mde2e/accessibility/home.spec.tse2e/contracts/home-builder-p1.spec.tse2e/contracts/managed-platform-v4.spec.tse2e/contracts/member-project-cloud.spec.tse2e/contracts/project-v2-studio.spec.tse2e/contracts/release-boundaries.spec.tse2e/contracts/studio-responsive-regressions.spec.tse2e/contracts/ui-standards.spec.tse2e/contracts/v0-studio-flow.spec.tse2e/fixtures/project-v2-ui-test.tse2e/fixtures/ui-test.tse2e/interactions/editor-commit.spec.tse2e/interactions/project-dialog-keyboard.spec.tse2e/products/free-prompt-game.spec.tse2e/proofs/all-presets-completion.spec.tse2e/proofs/design-evidence.spec.tse2e/proofs/director-flow.spec.tse2e/proofs/published-products.spec.tslib/access-tier.tslib/builder-agent/orchestrator.tslib/project-store.tslib/studio-account-connections-client.tslib/vercel-sandbox-runtime-adapter.tspackage.jsonplaywright.config.tstests/access-tier.test.mjstests/agent-chat-route.test.mjstests/builder-agent-route.test.mjstests/builder-live-flow.live.mjstests/coderabbit-findings.test.mjstests/project-store.test.mjstests/studio-account-connections-client.test.mjstests/studio-account-state.test.mjstests/vercel-sandbox-runtime.live.mjstests/vercel-sandbox-runtime.test.mjs
💤 Files with no reviewable changes (1)
- tests/vercel-sandbox-runtime.test.mjs
| && 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 } : {}), |
There was a problem hiding this comment.
🗄️ 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.tsRepository: 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)
PYRepository: 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)
PYRepository: 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
| 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, |
There was a problem hiding this comment.
🔒 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 -80Repository: 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.tsxRepository: 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 -240Repository: 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 -320Repository: 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))
PYRepository: 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.
| .chat-model-select:focus-within { | ||
| border-radius: 9px; | ||
| box-shadow: 0 0 0 3px rgba(49, 108, 255, 0.14); | ||
| } |
There was a problem hiding this comment.
📐 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.
| .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.
| } 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); | ||
| } |
There was a problem hiding this comment.
🎯 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.
| } 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.
| 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.", | ||
| ); | ||
| } | ||
| } |
There was a problem hiding this comment.
🗄️ 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 -nRepository: 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 dbRepository: 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 dbRepository: 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 dbRepository: 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
PYRepository: 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> |
There was a problem hiding this comment.
📐 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 -240Repository: 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.tsxRepository: 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 -160Repository: 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
| || !/^@[A-Za-z0-9_]{3,32}$/.test(botUsername) | ||
| || (username && !/^@[A-Za-z][A-Za-z0-9_]{4,31}$/.test(username)) | ||
| ) return undefined; |
There was a problem hiding this comment.
🗄️ 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: normalizeusernameandbotUsernameto the@prefix before the regex test, instead of rejecting unprefixed values.app/api/telegram/account/create-channel/route.ts#L60-L79: confirm theaccountIdtype returned byinspectTelegramAccountToken, and retry the save withouttelegramReceiptwhen 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.
| const handle = await adapter.writeProject(context); | ||
|
|
||
| try { | ||
| const result = await adapter.runTests(context, handle); | ||
| assert.equal(result.exitCode, 0); | ||
| } finally { | ||
| await adapter.destroy(handle); | ||
| } |
There was a problem hiding this comment.
🩺 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
There was a problem hiding this comment.
Actionable comments posted: 2
🧹 Nitpick comments (3)
lib/project-store.ts (1)
390-402: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick winDuplicated scope and lock resolution in
saveProjectSafelyanddeleteProjectSafely. Both functions repeat the samebrowserDefaultcheck, scope ternary, signed-scope guard, andnavigator.locksdetection.readProjectsFromStoreandclaimLegacyProjectsSafelycontain 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 sharedresolveStoreAccess(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 sameresolveStoreAccess(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 valueDocument that this reader returns the compatibility index only.
writeProjectinlib/project-store.tsstores a compacted index. It removesprojectV2throughcompactProjectForCompatibilityIndex. This fixture reads only the index, so any assertion onprojectV2throughstoredProjectsForCurrentActorwill fail even when the canonical item record contains the data.storedProjectForCurrentActorreads the item record first and does not have this limitation.Add a short comment, or merge the scoped item records like
readProjectsdoes.Note also the inconsistency: this function resolves the cookie inside
page.evaluate, whilestoredProjectForCurrentActorresolves it throughpage.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 winUse the canonical project-store key helpers.
Import and use
parseProjectStoreScopeCookieValue,projectStoreIndexKey, andprojectStoreItemPrefix. UsePROJECT_STORE_ITEM_PREFIXfor the unscopeditemKey; 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
⛔ Files ignored due to path filters (10)
docs/design/current-home-actual.pngis excluded by!**/*.pngdocs/design/current-studio-actual.pngis excluded by!**/*.pnge2e/visual/home.spec.ts-snapshots/home-builder-chromium-1024-linux-system.pngis excluded by!**/*.pnge2e/visual/home.spec.ts-snapshots/home-builder-chromium-1024-linux.pngis excluded by!**/*.pnge2e/visual/home.spec.ts-snapshots/home-builder-chromium-1440-linux-system.pngis excluded by!**/*.pnge2e/visual/home.spec.ts-snapshots/home-builder-chromium-1440-linux.pngis excluded by!**/*.pnge2e/visual/home.spec.ts-snapshots/home-builder-chromium-390-linux-system.pngis excluded by!**/*.pnge2e/visual/home.spec.ts-snapshots/home-builder-chromium-390-linux.pngis excluded by!**/*.pnge2e/visual/home.spec.ts-snapshots/studio-crypto-game-chromium-1024-linux-system.pngis excluded by!**/*.pnge2e/visual/home.spec.ts-snapshots/studio-crypto-game-chromium-1440-linux-system.pngis excluded by!**/*.png
📒 Files selected for processing (53)
.env.exampleapp/api/access/route.tsapp/api/agent/chat/route.tsapp/api/auth/google/callback/route.tsapp/api/auth/openrouter/exchange/route.tsapp/api/auth/session/route.tsapp/api/builder/agent/route.tsapp/api/telegram/account/create-channel/route.tsapp/api/telegram/account/status/route.tsapp/styles/project-studio.accessibility.cssapp/styles/project-studio.responsive.csscomponents/drops-studio.tsxcomponents/project-studio.tsxcomponents/project-v2-studio-surface.tsxcomponents/telegram-channel-wizard.tsxdb/studio-account-state.tsdocs/SANDBOX_OPERATIONS.mde2e/accessibility/home.spec.tse2e/contracts/home-builder-p1.spec.tse2e/contracts/managed-platform-v4.spec.tse2e/contracts/member-project-cloud.spec.tse2e/contracts/project-v2-studio.spec.tse2e/contracts/release-boundaries.spec.tse2e/contracts/studio-responsive-regressions.spec.tse2e/contracts/ui-standards.spec.tse2e/contracts/v0-studio-flow.spec.tse2e/fixtures/project-v2-ui-test.tse2e/fixtures/ui-test.tse2e/interactions/editor-commit.spec.tse2e/interactions/project-dialog-keyboard.spec.tse2e/products/free-prompt-game.spec.tse2e/proofs/all-presets-completion.spec.tse2e/proofs/design-evidence.spec.tse2e/proofs/director-flow.spec.tse2e/proofs/published-products.spec.tslib/access-tier.tslib/builder-agent/orchestrator.tslib/project-store.tslib/studio-account-connections-client.tslib/vercel-sandbox-runtime-adapter.tspackage.jsonplaywright.config.tstests/access-tier.test.mjstests/agent-chat-route.test.mjstests/builder-agent-route.test.mjstests/builder-live-flow.live.mjstests/coderabbit-findings.test.mjstests/project-store.test.mjstests/rendered-html.test.mjstests/studio-account-connections-client.test.mjstests/studio-account-state.test.mjstests/vercel-sandbox-runtime.live.mjstests/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
| 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); | ||
| } |
There was a problem hiding this comment.
🩺 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=tsxRepository: 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 -300Repository: 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)
PYRepository: 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.
| 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); | ||
| } |
There was a problem hiding this comment.
🗄️ 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:
- A guest builds projects. The guest scope claims the marker as
guest.<identity>. - The user signs in.
app/api/auth/google/callback/route.tsreplaces the cookie withmember.<identity>. migrateLegacyProjectsToScope(memberScope)readscurrentOwner === "guest.<id>"and returnsfalse.
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
Outcome
Verification
Safety
Summary by CodeRabbit