-
Notifications
You must be signed in to change notification settings - Fork 2k
Fix bugs and add features #3103
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Changes from all commits
8f4054a
32ed7be
7e2d180
043040b
126cfd4
35e66df
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -1,7 +1,9 @@ | ||
| import { useEditorEngine } from '@/components/store/editor'; | ||
| import { DefaultSettings } from '@onlook/constants'; | ||
| import type { ImageMessageContext, MessageContext } from '@onlook/models/chat'; | ||
| import { MessageContextType } from '@onlook/models/chat'; | ||
| import { assertNever } from '@onlook/utility'; | ||
| import { toast } from '@onlook/ui/sonner'; | ||
| import { assertNever, sanitizeFilename } from '@onlook/utility'; | ||
| import { observer } from 'mobx-react-lite'; | ||
| import { AnimatePresence } from 'motion/react'; | ||
| import { useMemo } from 'react'; | ||
|
|
@@ -17,6 +19,31 @@ const typeOrder = { | |
| [MessageContextType.IMAGE]: 5, | ||
| }; | ||
|
|
||
| const mimeExtensionMap: Record<string, string> = { | ||
| 'image/jpeg': 'jpg', | ||
| 'image/png': 'png', | ||
| 'image/webp': 'webp', | ||
| 'image/gif': 'gif', | ||
| 'image/svg+xml': 'svg', | ||
| 'image/bmp': 'bmp', | ||
| 'image/x-icon': 'ico', | ||
| 'video/mp4': 'mp4', | ||
| 'video/webm': 'webm', | ||
| }; | ||
|
|
||
| const getExtensionFromMimeType = (mimeType: string) => { | ||
| return mimeExtensionMap[mimeType] ?? 'png'; | ||
| }; | ||
|
|
||
| const getSanitizedFileName = (displayName: string, mimeType: string) => { | ||
| const sanitizedName = sanitizeFilename(displayName || `chat-image-${Date.now()}`); | ||
| if (sanitizedName.includes('.')) { | ||
| return sanitizedName; | ||
| } | ||
| const extension = getExtensionFromMimeType(mimeType); | ||
| return `${sanitizedName}.${extension}`; | ||
| }; | ||
|
|
||
| const getStableKey = (context: MessageContext, index: number): string => { | ||
| switch (context.type) { | ||
| case MessageContextType.FILE: | ||
|
|
@@ -46,6 +73,55 @@ export const InputContextPills = observer(() => { | |
| editorEngine.chat.context.context = newContext; | ||
| }; | ||
|
|
||
| const handleSaveImageContext = async (imageContext: ImageMessageContext) => { | ||
| if (imageContext.source === 'local' && imageContext.path) { | ||
| toast.success('Image already exists in project assets'); | ||
| return; | ||
| } | ||
|
|
||
| try { | ||
| const destinationFolder = `${DefaultSettings.IMAGE_FOLDER}/images`; | ||
| const fileName = getSanitizedFileName(imageContext.displayName, imageContext.mimeType); | ||
|
|
||
| let targetPath = `${destinationFolder}/${fileName}`; | ||
| let counter = 1; | ||
|
|
||
| while (await editorEngine.activeSandbox.fileExists(targetPath)) { | ||
| const lastDotIndex = fileName.lastIndexOf('.'); | ||
| const baseName = | ||
| lastDotIndex > 0 ? fileName.slice(0, lastDotIndex) : fileName; | ||
| const extension = | ||
| lastDotIndex > 0 ? fileName.slice(lastDotIndex) : ''; | ||
| targetPath = `${destinationFolder}/${baseName}-${counter}${extension}`; | ||
| counter += 1; | ||
| } | ||
|
|
||
| const imageResponse = await fetch(imageContext.content); | ||
| const imageBuffer = await imageResponse.arrayBuffer(); | ||
| const imageData = new Uint8Array(imageBuffer); | ||
|
|
||
| await editorEngine.activeSandbox.writeFile(targetPath, imageData); | ||
|
|
||
| const savedImageName = targetPath.split('/').pop() ?? fileName; | ||
| const updatedImageContext: ImageMessageContext = { | ||
| ...imageContext, | ||
| source: 'local', | ||
| path: targetPath, | ||
| branchId: editorEngine.branches.activeBranch.id, | ||
| displayName: savedImageName, | ||
| }; | ||
|
|
||
| editorEngine.chat.context.context = editorEngine.chat.context.context.map((context) => | ||
| context === imageContext ? updatedImageContext : context, | ||
| ); | ||
|
|
||
| toast.success(`Saved image to ${targetPath}`); | ||
| } catch (error) { | ||
| console.error('Failed to save image to project assets:', error); | ||
| toast.error('Failed to save image to project assets'); | ||
| } | ||
|
Comment on lines
+78
to
+122
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Hardcoded user-facing toast strings. The toast messages on lines 78, 118 and 121 are hardcoded English strings shown to users. Route them through next-intl so they can be localized. As per coding guidelines: "Avoid hardcoded user-facing text; use next-intl messages/hooks instead" (applies to 🤖 Prompt for AI Agents |
||
| }; | ||
|
|
||
| const sortedContexts = useMemo(() => { | ||
| return [...editorEngine.chat.context.context] | ||
| .sort((a, b) => { | ||
|
|
@@ -64,6 +140,7 @@ export const InputContextPills = observer(() => { | |
| <ImagePill | ||
| key={key} | ||
| context={context as ImageMessageContext} | ||
| onSave={() => void handleSaveImageContext(context as ImageMessageContext)} | ||
| onRemove={() => handleRemoveContext(context)} | ||
| /> | ||
| ); | ||
|
|
||
| Original file line number | Diff line number | Diff line change | ||||||||||||||||||||||||||||||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| @@ -1,68 +1,94 @@ | ||||||||||||||||||||||||||||||||||||||||||||||
| 'use client'; | ||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||
| import { useGetBackground } from '@/hooks/use-get-background'; | ||||||||||||||||||||||||||||||||||||||||||||||
| import { Routes } from '@/utils/constants'; | ||||||||||||||||||||||||||||||||||||||||||||||
| import { Card, CardDescription, CardHeader, CardTitle } from '@onlook/ui/card'; | ||||||||||||||||||||||||||||||||||||||||||||||
| import { Icons } from '@onlook/ui/icons'; | ||||||||||||||||||||||||||||||||||||||||||||||
| import { useRouter } from 'next/navigation'; | ||||||||||||||||||||||||||||||||||||||||||||||
| import { TopBar } from '../_components/top-bar'; | ||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||
| type ImportType = 'local' | 'github' | 'figma'; | ||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||
| const importCards: { | ||||||||||||||||||||||||||||||||||||||||||||||
| type: ImportType; | ||||||||||||||||||||||||||||||||||||||||||||||
| title: string; | ||||||||||||||||||||||||||||||||||||||||||||||
| description: string; | ||||||||||||||||||||||||||||||||||||||||||||||
| icon: React.ReactNode; | ||||||||||||||||||||||||||||||||||||||||||||||
| ariaLabel: string; | ||||||||||||||||||||||||||||||||||||||||||||||
| }[] = [ | ||||||||||||||||||||||||||||||||||||||||||||||
| { | ||||||||||||||||||||||||||||||||||||||||||||||
| type: 'local', | ||||||||||||||||||||||||||||||||||||||||||||||
| title: 'Import a Local Project', | ||||||||||||||||||||||||||||||||||||||||||||||
| description: | ||||||||||||||||||||||||||||||||||||||||||||||
| 'Select a directory from your computer to start working with your project in Pixelraft.', | ||||||||||||||||||||||||||||||||||||||||||||||
| icon: <Icons.Upload className="w-6 h-6 text-primary" />, | ||||||||||||||||||||||||||||||||||||||||||||||
| ariaLabel: 'Import local project', | ||||||||||||||||||||||||||||||||||||||||||||||
| }, | ||||||||||||||||||||||||||||||||||||||||||||||
| { | ||||||||||||||||||||||||||||||||||||||||||||||
| type: 'github', | ||||||||||||||||||||||||||||||||||||||||||||||
| title: 'Import from GitHub', | ||||||||||||||||||||||||||||||||||||||||||||||
| description: | ||||||||||||||||||||||||||||||||||||||||||||||
| 'Connect your GitHub account to import repositories and create pull requests from Pixelraft.', | ||||||||||||||||||||||||||||||||||||||||||||||
| icon: <Icons.GitHubLogo className="w-6 h-6 text-primary" />, | ||||||||||||||||||||||||||||||||||||||||||||||
| ariaLabel: 'Import from GitHub', | ||||||||||||||||||||||||||||||||||||||||||||||
| }, | ||||||||||||||||||||||||||||||||||||||||||||||
| { | ||||||||||||||||||||||||||||||||||||||||||||||
| type: 'figma', | ||||||||||||||||||||||||||||||||||||||||||||||
| title: 'Import from Figma', | ||||||||||||||||||||||||||||||||||||||||||||||
| description: | ||||||||||||||||||||||||||||||||||||||||||||||
| 'Paste a Figma file URL to generate a Pixelraft-ready starter project and synced design metadata.', | ||||||||||||||||||||||||||||||||||||||||||||||
| icon: <Icons.Layout className="w-6 h-6 text-primary" />, | ||||||||||||||||||||||||||||||||||||||||||||||
| ariaLabel: 'Import from Figma', | ||||||||||||||||||||||||||||||||||||||||||||||
| }, | ||||||||||||||||||||||||||||||||||||||||||||||
| ]; | ||||||||||||||||||||||||||||||||||||||||||||||
|
Comment on lines
+19
to
+43
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. User-visible copy still says "Pixelraft" — contradicts the rebrand goal added in this same PR. This same PR adds ✏️ Proposed copy fix- description:
- 'Select a directory from your computer to start working with your project in Pixelraft.',
+ description:
+ 'Select a directory from your computer to start working with your project in Onlook.',
@@
- description:
- 'Connect your GitHub account to import repositories and create pull requests from Pixelraft.',
+ description:
+ 'Connect your GitHub account to import repositories and create pull requests from Onlook.',
@@
- description:
- 'Paste a Figma file URL to generate a Pixelraft-ready starter project and synced design metadata.',
+ description:
+ 'Paste a Figma file URL to generate an Onlook-ready starter project and synced design metadata.',🤖 Prompt for AI Agents |
||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||
| const importPathMap: Record<ImportType, string> = { | ||||||||||||||||||||||||||||||||||||||||||||||
| local: `${Routes.IMPORT_PROJECT}/local`, | ||||||||||||||||||||||||||||||||||||||||||||||
| github: Routes.IMPORT_GITHUB, | ||||||||||||||||||||||||||||||||||||||||||||||
| figma: Routes.IMPORT_FIGMA, | ||||||||||||||||||||||||||||||||||||||||||||||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
This maps the new Figma card to Useful? React with 👍 / 👎. |
||||||||||||||||||||||||||||||||||||||||||||||
| }; | ||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||
| const Page = () => { | ||||||||||||||||||||||||||||||||||||||||||||||
| const router = useRouter(); | ||||||||||||||||||||||||||||||||||||||||||||||
| const handleCardClick = (type: 'local' | 'github') => { | ||||||||||||||||||||||||||||||||||||||||||||||
| router.push(`/projects/import/${type}`); | ||||||||||||||||||||||||||||||||||||||||||||||
| }; | ||||||||||||||||||||||||||||||||||||||||||||||
| const backgroundUrl = useGetBackground('create'); | ||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||
| const handleCardClick = (type: ImportType) => { | ||||||||||||||||||||||||||||||||||||||||||||||
| router.push(importPathMap[type]); | ||||||||||||||||||||||||||||||||||||||||||||||
| }; | ||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||
| return ( | ||||||||||||||||||||||||||||||||||||||||||||||
| <div className="w-screen h-screen flex flex-col" | ||||||||||||||||||||||||||||||||||||||||||||||
| <div | ||||||||||||||||||||||||||||||||||||||||||||||
| className="w-screen h-screen flex flex-col" | ||||||||||||||||||||||||||||||||||||||||||||||
| style={{ | ||||||||||||||||||||||||||||||||||||||||||||||
| backgroundSize: 'cover', | ||||||||||||||||||||||||||||||||||||||||||||||
| backgroundPosition: 'center', | ||||||||||||||||||||||||||||||||||||||||||||||
| backgroundImage: `url(${backgroundUrl})`, | ||||||||||||||||||||||||||||||||||||||||||||||
| }} | ||||||||||||||||||||||||||||||||||||||||||||||
| > | ||||||||||||||||||||||||||||||||||||||||||||||
| <TopBar /> | ||||||||||||||||||||||||||||||||||||||||||||||
| <div className="flex items-center justify-center overflow-hidden max-w-4xl mx-auto w-full flex-1 gap-6 p-6 select-none"> | ||||||||||||||||||||||||||||||||||||||||||||||
| <Card | ||||||||||||||||||||||||||||||||||||||||||||||
| className={`w-full h-64 cursor-pointer transition-all duration-200 bg-background/80 backdrop-blur-xl hover:shadow-lg hover:scale-[1.02] border-[0.5px] border-foreground-tertiary/50`} | ||||||||||||||||||||||||||||||||||||||||||||||
| onClick={() => handleCardClick('local')} | ||||||||||||||||||||||||||||||||||||||||||||||
| tabIndex={0} | ||||||||||||||||||||||||||||||||||||||||||||||
| role="button" | ||||||||||||||||||||||||||||||||||||||||||||||
| aria-label="Import local project" | ||||||||||||||||||||||||||||||||||||||||||||||
| > | ||||||||||||||||||||||||||||||||||||||||||||||
| <CardHeader className="flex flex-col justify-between h-full"> | ||||||||||||||||||||||||||||||||||||||||||||||
| <div className="w-12 h-12 rounded-lg bg-primary/10 flex items-center justify-center select-none"> | ||||||||||||||||||||||||||||||||||||||||||||||
| <Icons.Upload className="w-6 h-6 text-primary" /> | ||||||||||||||||||||||||||||||||||||||||||||||
| </div> | ||||||||||||||||||||||||||||||||||||||||||||||
| <div className="space-y-2"> | ||||||||||||||||||||||||||||||||||||||||||||||
| <CardTitle className="text-title3">Import a Local Project</CardTitle> | ||||||||||||||||||||||||||||||||||||||||||||||
| <CardDescription className="text-sm text-balance"> | ||||||||||||||||||||||||||||||||||||||||||||||
| Select a directory from your computer to start working with your project in Onlook. | ||||||||||||||||||||||||||||||||||||||||||||||
| </CardDescription> | ||||||||||||||||||||||||||||||||||||||||||||||
| </div> | ||||||||||||||||||||||||||||||||||||||||||||||
| </CardHeader> | ||||||||||||||||||||||||||||||||||||||||||||||
| </Card> | ||||||||||||||||||||||||||||||||||||||||||||||
| {/* Temporary disabled */} | ||||||||||||||||||||||||||||||||||||||||||||||
| <Card | ||||||||||||||||||||||||||||||||||||||||||||||
| className={'w-full h-64 cursor-pointer transition-all duration-200 bg-background/80 backdrop-blur-xl hover:shadow-lg hover:scale-[1.02] border-[0.5px] border-foreground-tertiary/50 cursor-not-allowed opacity-60'} | ||||||||||||||||||||||||||||||||||||||||||||||
| onClick={() => false && handleCardClick('github')} | ||||||||||||||||||||||||||||||||||||||||||||||
| tabIndex={0} | ||||||||||||||||||||||||||||||||||||||||||||||
| role="button" | ||||||||||||||||||||||||||||||||||||||||||||||
| aria-label="Connect to GitHub" | ||||||||||||||||||||||||||||||||||||||||||||||
| > | ||||||||||||||||||||||||||||||||||||||||||||||
| <CardHeader className="flex flex-col justify-between h-full"> | ||||||||||||||||||||||||||||||||||||||||||||||
| <div className="w-12 h-12 rounded-lg bg-primary/10 flex items-center justify-center select-none"> | ||||||||||||||||||||||||||||||||||||||||||||||
| <Icons.GitHubLogo className="w-6 h-6 text-primary" /> | ||||||||||||||||||||||||||||||||||||||||||||||
| </div> | ||||||||||||||||||||||||||||||||||||||||||||||
| <div className="space-y-2"> | ||||||||||||||||||||||||||||||||||||||||||||||
| <CardTitle className="text-title3">Import from GitHub</CardTitle> | ||||||||||||||||||||||||||||||||||||||||||||||
| <CardDescription className="text-sm text-balance"> | ||||||||||||||||||||||||||||||||||||||||||||||
| Connect your GitHub account to access and work with your repositories | ||||||||||||||||||||||||||||||||||||||||||||||
| </CardDescription> | ||||||||||||||||||||||||||||||||||||||||||||||
| </div> | ||||||||||||||||||||||||||||||||||||||||||||||
| </CardHeader> | ||||||||||||||||||||||||||||||||||||||||||||||
| </Card> | ||||||||||||||||||||||||||||||||||||||||||||||
| <div className="flex items-center justify-center overflow-hidden max-w-6xl mx-auto w-full flex-1 gap-6 p-6 select-none"> | ||||||||||||||||||||||||||||||||||||||||||||||
| {importCards.map((card) => ( | ||||||||||||||||||||||||||||||||||||||||||||||
| <Card | ||||||||||||||||||||||||||||||||||||||||||||||
| key={card.type} | ||||||||||||||||||||||||||||||||||||||||||||||
| className="w-full h-64 cursor-pointer transition-all duration-200 bg-background/80 backdrop-blur-xl hover:shadow-lg hover:scale-[1.02] border-[0.5px] border-foreground-tertiary/50" | ||||||||||||||||||||||||||||||||||||||||||||||
| onClick={() => handleCardClick(card.type)} | ||||||||||||||||||||||||||||||||||||||||||||||
| tabIndex={0} | ||||||||||||||||||||||||||||||||||||||||||||||
| role="button" | ||||||||||||||||||||||||||||||||||||||||||||||
| aria-label={card.ariaLabel} | ||||||||||||||||||||||||||||||||||||||||||||||
| > | ||||||||||||||||||||||||||||||||||||||||||||||
|
Comment on lines
+71
to
+78
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Card exposed as The ♿ Proposed accessibility fix- <Card
- key={card.type}
- className="w-full h-64 cursor-pointer transition-all duration-200 bg-background/80 backdrop-blur-xl hover:shadow-lg hover:scale-[1.02] border-[0.5px] border-foreground-tertiary/50"
- onClick={() => handleCardClick(card.type)}
- tabIndex={0}
- role="button"
- aria-label={card.ariaLabel}
- >
+ <Card
+ key={card.type}
+ className="w-full h-64 cursor-pointer transition-all duration-200 bg-background/80 backdrop-blur-xl hover:shadow-lg hover:scale-[1.02] border-[0.5px] border-foreground-tertiary/50 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-primary"
+ onClick={() => handleCardClick(card.type)}
+ onKeyDown={(e) => {
+ if (e.key === 'Enter' || e.key === ' ') {
+ e.preventDefault();
+ handleCardClick(card.type);
+ }
+ }}
+ tabIndex={0}
+ role="button"
+ aria-label={card.ariaLabel}
+ >A focus-visible style is also worth adding so the focused card is discoverable. Based on learnings, this surface should ideally use a real 📝 Committable suggestion
Suggested change
🤖 Prompt for AI Agents |
||||||||||||||||||||||||||||||||||||||||||||||
| <CardHeader className="flex flex-col justify-between h-full"> | ||||||||||||||||||||||||||||||||||||||||||||||
| <div className="w-12 h-12 rounded-lg bg-primary/10 flex items-center justify-center select-none"> | ||||||||||||||||||||||||||||||||||||||||||||||
| {card.icon} | ||||||||||||||||||||||||||||||||||||||||||||||
| </div> | ||||||||||||||||||||||||||||||||||||||||||||||
| <div className="space-y-2"> | ||||||||||||||||||||||||||||||||||||||||||||||
| <CardTitle className="text-title3">{card.title}</CardTitle> | ||||||||||||||||||||||||||||||||||||||||||||||
| <CardDescription className="text-sm text-balance"> | ||||||||||||||||||||||||||||||||||||||||||||||
| {card.description} | ||||||||||||||||||||||||||||||||||||||||||||||
| </CardDescription> | ||||||||||||||||||||||||||||||||||||||||||||||
| </div> | ||||||||||||||||||||||||||||||||||||||||||||||
| </CardHeader> | ||||||||||||||||||||||||||||||||||||||||||||||
| </Card> | ||||||||||||||||||||||||||||||||||||||||||||||
| ))} | ||||||||||||||||||||||||||||||||||||||||||||||
| </div> | ||||||||||||||||||||||||||||||||||||||||||||||
| </div> | ||||||||||||||||||||||||||||||||||||||||||||||
| ); | ||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
🧩 Analysis chain
🏁 Script executed:
Repository: onlook-dev/onlook
Length of output: 50373
🏁 Script executed:
Repository: onlook-dev/onlook
Length of output: 43
🏁 Script executed:
Repository: onlook-dev/onlook
Length of output: 2001
🏁 Script executed:
Repository: onlook-dev/onlook
Length of output: 43
🏁 Script executed:
Repository: onlook-dev/onlook
Length of output: 3604
🏁 Script executed:
Repository: onlook-dev/onlook
Length of output: 43
🏁 Script executed:
Repository: onlook-dev/onlook
Length of output: 4394
🏁 Script executed:
Repository: onlook-dev/onlook
Length of output: 43
🏁 Script executed:
Repository: onlook-dev/onlook
Length of output: 523
🏁 Script executed:
Repository: onlook-dev/onlook
Length of output: 2885
🏁 Script executed:
Repository: onlook-dev/onlook
Length of output: 1336
🏁 Script executed:
Repository: onlook-dev/onlook
Length of output: 1915
🏁 Script executed:
Repository: onlook-dev/onlook
Length of output: 1183
🏁 Script executed:
Repository: onlook-dev/onlook
Length of output: 43
🏁 Script executed:
Repository: onlook-dev/onlook
Length of output: 43
🏁 Script executed:
Repository: onlook-dev/onlook
Length of output: 89
🏁 Script executed:
Repository: onlook-dev/onlook
Length of output: 785
🏁 Script executed:
Repository: onlook-dev/onlook
Length of output: 1915
🏁 Script executed:
Repository: onlook-dev/onlook
Length of output: 1996
🏁 Script executed:
Repository: onlook-dev/onlook
Length of output: 2055
🏁 Script executed:
Repository: onlook-dev/onlook
Length of output: 505
🏁 Script executed:
Repository: onlook-dev/onlook
Length of output: 43
Capture
branchIdbefore async operations to prevent mismatch.The code writes files through
editorEngine.activeSandbox, then capturesbranchIdafter multiple awaits (fileExists, fetch, writeFile). If the user switches branches during this sequence, the savedpathwill exist in one branch's sandbox but be tagged with a different branch's ID. CapturebranchIdearly before file operations, similar to the pattern inimage-tab/index.tsxline 22.🤖 Prompt for AI Agents