Skip to content
Open
Show file tree
Hide file tree
Changes from 26 commits
Commits
Show all changes
29 commits
Select commit Hold shift + click to select a range
ad0fe67
simplify example: use Sandbox methods directly in workflow
pranaygp Mar 27, 2026
fea1505
stream sandbox output to UI via workflow named streams
pranaygp Mar 27, 2026
6642456
accept Web WritableStream in runCommand stdout/stderr
pranaygp Mar 27, 2026
1341002
fix typecheck: guard .emit() call for WritableStream compatibility
pranaygp Mar 27, 2026
f7a8871
fix: ensure client before calling logs() in getCachedOutput
pranaygp Mar 27, 2026
bcd25eb
remove manual stream close — workflow runtime handles it
pranaygp Mar 27, 2026
e6cee90
return result instead of throwing on code failure
pranaygp Mar 27, 2026
3573942
show live status with current phase and attempt number
pranaygp Mar 27, 2026
c20ae44
fix: write to status stream inside a step function
pranaygp Mar 27, 2026
cb558d3
accept string in writeFiles content, avoid Buffer in workflow context
pranaygp Mar 27, 2026
eb849f6
stream generated code to UI via status updates
pranaygp Mar 28, 2026
1868043
add onCompleteUrl to runCommand for webhook-based completion
pranaygp Mar 28, 2026
536b7b4
auto-create workflow webhook for detached runCommand
pranaygp Mar 28, 2026
d005345
revert auto-webhook: use explicit createWebhook at workflow level
pranaygp Mar 28, 2026
45444ec
transparent webhook integration for detached runCommand
pranaygp Mar 28, 2026
d4a5307
fix: use regular import("workflow") so bundler can resolve it
pranaygp Mar 28, 2026
5568329
use workflow tarball for example app
pranaygp Mar 28, 2026
c303030
update workflow tarball for example app
pranaygp Mar 28, 2026
5780020
fix: include status stream in SSE endpoint
pranaygp Mar 28, 2026
a4707d1
add cmd+enter shortcut to submit
pranaygp Mar 28, 2026
51f187d
add sandbox lifecycle status updates
pranaygp Mar 28, 2026
79d055c
add syntax highlighting and terminal UI components
pranaygp Mar 28, 2026
256facd
split-pane layout: code left, terminal output right
pranaygp Mar 28, 2026
2a53951
vercel/black theme with line numbers
pranaygp Mar 28, 2026
d93d588
add runtime selector (Node.js 24/22, Python 3.13)
pranaygp Mar 28, 2026
0d286fa
add Bash runtime option
pranaygp Mar 28, 2026
36e8a9e
remove generated workflow manifest and gitignore it
pranaygp Mar 28, 2026
99190b4
Merge branch 'main' into pranay/serde-review
VaguelySerious Apr 1, 2026
4d09ce4
Merge branch 'main' into pranay/serde-review
VaguelySerious Apr 2, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions examples/workflow-code-runner/.gitignore
Original file line number Diff line number Diff line change
@@ -1,2 +1,4 @@
/.next
/next-env.d.ts
.vercel
.env*.local
4 changes: 4 additions & 0 deletions examples/workflow-code-runner/NOTES.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
# Problems with current implementation

* webhook resumption doesn't work when running this locally because sandbox can't resume a localhost webhook
* deployment protection needs to be disabled for webhook resumption to happen in a preview branch
40 changes: 38 additions & 2 deletions examples/workflow-code-runner/app/api/run/route.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,22 +3,58 @@ import { runCode } from "@/workflows/code-runner";
import { NextResponse } from "next/server";

export async function POST(request: Request) {
const { prompt } = await request.json();
const { prompt, runtime } = await request.json();

const run = await start(runCode, [prompt]);
const run = await start(runCode, [prompt, runtime]);

return NextResponse.json({ runId: run.runId });
}

export async function GET(request: Request) {
const { searchParams } = new URL(request.url);
const runId = searchParams.get("runId");
const stream = searchParams.get("stream");

if (!runId) {
return NextResponse.json({ error: "Missing runId" }, { status: 400 });
}

const run = getRun(runId);

// Stream stdout, stderr, or status as SSE
if (stream === "stdout" || stream === "stderr" || stream === "status") {
const readable = run.getReadable<string>({ namespace: stream });
const encoder = new TextEncoder();

const sseStream = new ReadableStream({
async start(controller) {
try {
const reader = readable.getReader();
while (true) {
Comment on lines +29 to +33
Copy link

Copilot AI Mar 28, 2026

Choose a reason for hiding this comment

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

The SSE ReadableStream doesn’t implement cancel(). If the client disconnects, the server-side loop may keep reading from the workflow stream and attempting to enqueue, wasting resources. Consider adding a cancel() handler that cancels/releases the reader and stops the loop when the response is aborted.

Copilot uses AI. Check for mistakes.
const { done, value } = await reader.read();
if (done) break;
controller.enqueue(
encoder.encode(`data: ${JSON.stringify(value)}\n\n`),
);
}
controller.enqueue(encoder.encode("event: done\ndata: {}\n\n"));
controller.close();
} catch {
controller.close();
}
},
});

return new Response(sseStream, {
headers: {
"Content-Type": "text/event-stream",
"Cache-Control": "no-cache",
Connection: "keep-alive",
},
});
}

// Poll for status
const status = await run.status;

if (status === "completed") {
Expand Down
49 changes: 49 additions & 0 deletions examples/workflow-code-runner/app/components/code-block.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
"use client";

import { useEffect, useState } from "react";
import { codeToHtml } from "shiki";

export function CodeBlock({
code,
lang = "javascript",
}: {
code: string;
lang?: string;
}) {
const [html, setHtml] = useState<string>("");

useEffect(() => {
let cancelled = false;
codeToHtml(code, {
lang,
theme: "github-dark-default",
}).then((result) => {
if (!cancelled) setHtml(result);
});
return () => {
cancelled = true;
};
}, [code, lang]);

if (!html) {
return (
<pre className="line-numbers bg-black p-4 font-mono text-sm leading-relaxed text-[#e6edf3]">
{code.split("\n").map((line, i) => (
<div key={i} className="table-row">
<span className="table-cell w-8 select-none pr-4 text-right tabular-nums text-[#484f58]">
{i + 1}
</span>
<span className="table-cell">{line}</span>
</div>
))}
</pre>
);
}

return (
<div
className="code-block [&_pre]:!bg-black [&_pre]:p-4 [&_pre]:leading-relaxed [&_pre]:text-sm [&_code]:text-sm [&_code]:leading-relaxed"
dangerouslySetInnerHTML={{ __html: html }}
/>
);
}
43 changes: 43 additions & 0 deletions examples/workflow-code-runner/app/components/terminal.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
"use client";

import Ansi from "ansi-to-react";
import { useEffect, useRef } from "react";

export function Terminal({
title,
children,
variant = "default",
}: {
title: string;
children: string;
variant?: "default" | "error";
}) {
const scrollRef = useRef<HTMLPreElement>(null);

useEffect(() => {
if (scrollRef.current) {
scrollRef.current.scrollTop = scrollRef.current.scrollHeight;
}
}, [children]);

return (
<div className="flex h-full flex-col">
<div className="flex items-center gap-2 border-b border-[rgba(255,255,255,0.1)] bg-black px-4 py-2">
<div className="flex gap-1.5">
<div className="h-2.5 w-2.5 rounded-full bg-[#484f58]" />
<div className="h-2.5 w-2.5 rounded-full bg-[#484f58]" />
<div className="h-2.5 w-2.5 rounded-full bg-[#484f58]" />
</div>
<span className="ml-2 text-xs text-[#484f58]">{title}</span>
</div>
<pre
ref={scrollRef}
className={`flex-1 overflow-auto bg-black p-4 font-mono text-sm leading-relaxed ${
variant === "error" ? "text-[#f85149]" : "text-[#e6edf3]"
}`}
>
<Ansi>{children}</Ansi>
</pre>
</div>
);
}
22 changes: 21 additions & 1 deletion examples/workflow-code-runner/app/globals.css
Original file line number Diff line number Diff line change
@@ -1,13 +1,17 @@
@import "tailwindcss";

:root {
--background: #0a0a0a;
--background: #000000;
--foreground: #ededed;
--border: rgba(255, 255, 255, 0.1);
--muted: #484f58;
}

@theme inline {
--color-background: var(--background);
--color-foreground: var(--foreground);
--color-border: var(--border);
--color-muted: var(--muted);
--font-sans: var(--font-geist-sans);
--font-mono: var(--font-geist-mono);
}
Expand All @@ -16,3 +20,19 @@ body {
background: var(--background);
color: var(--foreground);
}

/* Shiki line numbers */
.code-block code {
counter-reset: step;
}

.code-block code .line::before {
content: counter(step);
counter-increment: step;
display: inline-block;
width: 2rem;
margin-right: 1.5rem;
text-align: right;
color: var(--muted);
font-variant-numeric: tabular-nums;
}
Loading
Loading