-
Notifications
You must be signed in to change notification settings - Fork 19
Streaming output and webhook-based completion when run in workflow #118
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 27 commits
ad0fe67
fea1505
6642456
1341002
f7a8871
bcd25eb
e6cee90
3573942
c20ae44
cb558d3
eb849f6
1868043
536b7b4
d005345
45444ec
d4a5307
5568329
c303030
5780020
a4707d1
51f187d
79d055c
256facd
2a53951
d93d588
0d286fa
36e8a9e
99190b4
4d09ce4
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,2 +1,5 @@ | ||
| /.next | ||
| /next-env.d.ts | ||
| .vercel | ||
| .env*.local | ||
| examples/workflow-code-runner/public/.well-known/ | ||
| 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 |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -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
|
||
| 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") { | ||
|
|
||
| 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 }} | ||
| /> | ||
| ); | ||
| } |
| 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> | ||
| ); | ||
| } |
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.
This
.gitignoreentry likely won’t match because it’s already insideexamples/workflow-code-runner/;examples/workflow-code-runner/public/.well-known/is an extra path prefix. Use a path relative to this directory (e.g.public/.well-known/or/public/.well-known/) so the well-known folder is actually ignored.