-
-
Notifications
You must be signed in to change notification settings - Fork 8
Vercel Sandbox Integration #640
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 2 commits
58859ab
d41ece3
ecbd4c3
8fc5dda
dccf04b
a1cb143
8ecbab0
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,52 @@ | ||
| 'use client' | ||
|
|
||
| import { useEffect, useRef } from 'react' | ||
| import { cn } from '@/lib/utils' | ||
|
|
||
| interface LogEntry { | ||
| type: 'stdout' | 'stderr' | ||
| content: string | ||
| } | ||
|
|
||
| interface SandboxLogsProps { | ||
| logs: LogEntry[] | ||
| isExecuting?: boolean | ||
| } | ||
|
|
||
| export function SandboxLogs({ logs, isExecuting }: SandboxLogsProps) { | ||
| const scrollRef = useRef<HTMLDivElement>(null) | ||
|
|
||
| useEffect(() => { | ||
| if (scrollRef.current) { | ||
| scrollRef.current.scrollTop = scrollRef.current.scrollHeight | ||
| } | ||
| }, [logs]) | ||
|
|
||
| return ( | ||
| <div | ||
| ref={scrollRef} | ||
| className="bg-zinc-950 text-zinc-50 font-mono text-sm p-4 rounded-md overflow-auto max-h-[300px] border border-zinc-800" | ||
| > | ||
| {logs.map((log, index) => ( | ||
| <div | ||
| key={index} | ||
| className={cn( | ||
| "whitespace-pre-wrap mb-1", | ||
| log.type === 'stderr' ? "text-red-400" : "text-zinc-300" | ||
| )} | ||
| > | ||
| {log.content} | ||
| </div> | ||
| ))} | ||
| {isExecuting && ( | ||
| <div className="flex items-center gap-2 text-zinc-500 mt-2"> | ||
| <span className="w-1.5 h-1.5 bg-zinc-500 rounded-full animate-pulse" /> | ||
| <span>Executing...</span> | ||
| </div> | ||
| )} | ||
| {logs.length === 0 && !isExecuting && ( | ||
| <div className="text-zinc-500 italic">No output</div> | ||
| )} | ||
| </div> | ||
| ) | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,51 @@ | ||
| 'use client' | ||
|
|
||
| import { useState } from 'react' | ||
| import { ExternalLink, Loader2, RefreshCw } from 'lucide-react' | ||
| import { Button } from '@/components/ui/button' | ||
|
|
||
| interface SandboxPreviewProps { | ||
| url: string | ||
| } | ||
|
|
||
| export function SandboxPreview({ url }: SandboxPreviewProps) { | ||
| const [isLoading, setIsLoading] = useState(true) | ||
| const [key, setKey] = useState(0) | ||
|
|
||
| const reload = () => { | ||
| setIsLoading(true) | ||
| setKey(prev => prev + 1) | ||
| } | ||
|
|
||
| return ( | ||
| <div className="flex flex-col border rounded-md overflow-hidden bg-background"> | ||
| <div className="flex items-center justify-between px-4 py-2 border-b bg-muted/50"> | ||
| <div className="text-xs font-mono truncate mr-4">{url}</div> | ||
| <div className="flex items-center gap-1"> | ||
| <Button variant="ghost" size="icon" className="h-7 w-7" onClick={reload}> | ||
| <RefreshCw className="h-3.5 w-3.5" /> | ||
| </Button> | ||
| <Button variant="ghost" size="icon" className="h-7 w-7" asChild> | ||
| <a href={url} target="_blank" rel="noopener noreferrer"> | ||
| <ExternalLink className="h-3.5 w-3.5" /> | ||
| </a> | ||
|
Comment on lines
+25
to
+31
Contributor
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. Add accessible labels to preview controls and iframe. Line 25 and Line 28 render icon-only actions without accessible names, and Line 41 renders an unlabeled iframe. This makes the preview controls/frame hard to use with assistive tech. Suggested fix- <Button variant="ghost" size="icon" className="h-7 w-7" onClick={reload}>
+ <Button
+ variant="ghost"
+ size="icon"
+ className="h-7 w-7"
+ onClick={reload}
+ aria-label="Reload sandbox preview"
+ >
<RefreshCw className="h-3.5 w-3.5" />
</Button>
<Button variant="ghost" size="icon" className="h-7 w-7" asChild>
- <a href={url} target="_blank" rel="noopener noreferrer">
+ <a
+ href={url}
+ target="_blank"
+ rel="noopener noreferrer"
+ aria-label="Open sandbox preview in a new tab"
+ >
<ExternalLink className="h-3.5 w-3.5" />
</a>
</Button>
@@
<iframe
key={key}
src={url}
+ title="Sandbox live preview"
className="w-full h-full border-none"
sandbox="allow-scripts allow-same-origin allow-forms"
onLoad={() => setIsLoading(false)}
/>Also applies to: 41-47 🤖 Prompt for AI Agents |
||
| </Button> | ||
| </div> | ||
| </div> | ||
| <div className="relative w-full aspect-video bg-white"> | ||
| {isLoading && ( | ||
| <div className="absolute inset-0 flex items-center justify-center bg-muted/20"> | ||
| <Loader2 className="h-6 w-6 animate-spin text-muted-foreground" /> | ||
| </div> | ||
| )} | ||
| <iframe | ||
| key={key} | ||
| src={url} | ||
| className="w-full h-full border-none" | ||
| sandbox="allow-scripts allow-same-origin allow-forms" | ||
| onLoad={() => setIsLoading(false)} | ||
| /> | ||
| </div> | ||
| </div> | ||
| ) | ||
| } | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,51 @@ | ||
| 'use client' | ||
|
|
||
| import { useStreamableValue, StreamableValue } from 'ai/rsc' | ||
| import { Section } from '@/components/section' | ||
| import { SandboxLogs } from './sandbox-logs' | ||
| import { SandboxPreview } from './sandbox-preview' | ||
| import { Terminal } from 'lucide-react' | ||
|
|
||
| interface LogEntry { | ||
| type: 'stdout' | 'stderr' | ||
| content: string | ||
| } | ||
|
|
||
| interface SandboxSectionProps { | ||
| logs: StreamableValue<LogEntry[]> | ||
| previewUrl?: string | ||
| exitCode?: number | ||
| error?: string | ||
| } | ||
|
|
||
| export function SandboxSection({ logs, previewUrl, exitCode, error }: SandboxSectionProps) { | ||
| const [data, errorFromStream] = useStreamableValue(logs) | ||
| const isExecuting = data === undefined | ||
|
|
||
| return ( | ||
| <Section title="Sandbox" isCollapsed={false}> | ||
| <div className="space-y-4"> | ||
| <SandboxLogs logs={data || []} isExecuting={isExecuting} /> | ||
|
|
||
| {previewUrl && ( | ||
| <div className="mt-4"> | ||
| <h4 className="text-xs font-semibold uppercase text-muted-foreground mb-2 px-1">Live Preview</h4> | ||
| <SandboxPreview url={previewUrl} /> | ||
| </div> | ||
| )} | ||
|
|
||
| {(error || !!errorFromStream) && ( | ||
| <div className="p-3 text-xs bg-red-50 text-red-600 rounded border border-red-100 font-mono"> | ||
| <strong>Error:</strong> {error || (errorFromStream as any)?.message || 'Execution failed'} | ||
| </div> | ||
| )} | ||
|
|
||
| {exitCode !== undefined && !previewUrl && ( | ||
| <div className="text-[10px] text-muted-foreground font-mono px-1"> | ||
| Process exited with code {exitCode} | ||
| </div> | ||
| )} | ||
| </div> | ||
| </Section> | ||
| ) | ||
| } |
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.
🧹 Nitpick | 🔵 Trivial | 💤 Low value
Reorder environment variables to alphabetical order.
The static analysis tool (dotenv-linter) reports that
VERCEL_TEAM_ID(line 38) should appear beforeVERCEL_TOKEN(line 36) to maintain alphabetical/conventional ordering.🔧 Proposed reordering
📝 Committable suggestion
🧰 Tools
🪛 dotenv-linter (4.0.0)
[warning] 38-38: [UnorderedKey] The VERCEL_TEAM_ID key should go before the VERCEL_TOKEN key
(UnorderedKey)
🤖 Prompt for AI Agents
Source: Linters/SAST tools