Overhaul the template: KERNEL branding, current SDKs, streamed steps sidebar - #6
Conversation
…sidebar - restyle to the KERNEL design system: kernel green, beige, and charcoal tokens, Inter + IBM Plex Mono, square corners, lowercase copy, and the brand-assets wordmark and mark - upgrade ai to 7, @ai-sdk/openai to 4, @ai-sdk/react to 4, @onkernel/sdk to 0.100, next to 16.3, react to 19.3 - inline the playwright execution tool so the agent works on the current AI SDK instead of the ai-5-pinned package - stream agent steps into a sidebar next to the live view, with the generated playwright, per-step status, execution time, return values, and failures - fix the eslint setup for eslint 9 and drop unused deps and assets
|
The latest updates on your projects. Learn more about Vercel for GitHub.
|
next 16 blocks its own dev resources from hosts that are not in allowedDevOrigins, which leaves the page unhydrated when the dev server is opened at 127.0.0.1 instead of localhost.
- pass req.signal into createAgentUIStreamResponse so stopping a run ends the agent loop instead of only the client stream - render an unfinished step as cancelled once the stream is gone, rather than leaving it spinning - check the delete response before clearing the session, so a failed delete surfaces instead of dropping the session id while the browser keeps running
a stopped run leaves a tool call without a result, so the next request failed with a missing tool result error. convert the message history with ignoreIncompleteToolCalls so the model only sees finished calls, and derive the cancelled state from the run itself so an interrupted step does not turn back into a running one when a follow-up task starts.
- create browser sessions at 1024x768, so the live view is legible in the space the page gives it instead of being a scaled-down 1920x1080 - move the three-call strip into the browser column as a list, where it fits, rather than a three-up grid squeezed into a third of it - shrink the step numbers to match the labels beside them, and refresh the preview screenshot
- 1280x800 keeps the live view readable without the 1920x1080 default, and the frame goes to 16:10 to match - "browser vm session" replaces "chromium session" in the hero, and the create card says the same
with a session open the page no longer scrolls: the live view takes the height that is left, the steps pane owns the only scrollbar, and the footer is only rendered on the landing page. narrower than lg still flows normally.
- the session error renders above the split again, instead of becoming a third column in the flex row - the live view keeps its 16:10 box below lg, where the column has no fixed height, and sizes itself from the available height at lg
There was a problem hiding this comment.
Cursor Bugbot has reviewed your changes using default effort and found 1 potential issue.
❌ Bugbot Autofix is OFF. To automatically fix reported issues with cloud agents, enable autofix in the Cursor dashboard.
Reviewed by Cursor Bugbot for commit 1029684. Configure here.
the live view is a 16:10 box that grows to 900px wide and then stops, centered in its column and sized from the space around it, so it neither elongates on a tall screen nor letterboxes on a short one. the split itself is capped at 820px tall and centered in the viewport, and the call list lines up with the live view. below lg everything still sizes down and scrolls as before.
playwright gives every locator action a 30 second budget by default, which is
longer than anyone watches. the instructions now require a short one: open a
snippet that touches a selector with page.setDefaultTimeout(5000), or pass
{ timeout: 5000 } to the action, with up to 15000 allowed for a navigation.
they also point out that checking count() first beats awaiting a value that may
not exist, and that a .catch() does not shorten the wait it wraps. the tool
description repeats the rule for the model and in the code parameter's help.
kernel's timeout_seconds only counts inactivity, and a live view counts as activity, so a tab left open holds a browser forever. sessions now carry a tag and a deadline: the page closes the session when the clock runs out (a short interval, since browsers clamp long timeouts), and every create sweeps this app's sessions that are already past it, so an abandoned tab cannot leak one. the session bar shows the remaining minutes, and SESSION_TTL_MS in lib/session.ts is the only number to change.
A missing server env var is a server misconfiguration, not a bad client request. The response body already said as much; the status code now agrees with it.
The 400-char clone url was hand-typed in the route and in README with no link between the two copies. It now lives in lib/deploy-url.ts and the route imports it; README still needs its own literal for the badge markdown, so a comment there points back at the shared constant.
The README badge can't import the shared constant, so leave a pointer back to it for whoever updates one copy and forgets the other.
response.json() rejects on an HTML error page (e.g. a route that throws before returning JSON), which surfaced as an opaque parse error instead of an actionable message.
session lived only in component state, so reloading the page stranded a live browser the user could no longer see or close - it just burned until its session timeout. It's now mirrored to sessionStorage and restored on mount. Step history isn't restored (useChat owns that separately), and this deliberately doesn't try to close the session on beforeunload: that would fire on refresh too and delete the very session this is meant to recover, and the bounded session timeout already caps an abandoned tab's cost.
"1 steps" read as an oversight, and the composer hint said "cmd + enter" even though the handler already accepts ctrlKey too.
Steps and notes were tracked in two separate arrays and rendered steps-then-notes, so a note the model said before step 2 would render below step 5. They're now one ordered list of entries per run, so the rendered order matches the stream order. Also preserves case on notes: the model's closing prose was being force-lowercased along with static UI copy, the same inconsistency the task text above it (which does carry data-preserve-case) made visible.
When the stream ended in a server error, busy went false and every run's unfinished step got the same "cancelled" chip as an actual user stop - misleading, since the user didn't cancel anything. Only the newest run's own chat status can tell the two apart, so it now flows through to pick "failed" instead.
stepCountIs(24) ended a run silently: no note, no final prose, and the sidebar showed a finished-looking run with no indication it was cut off. The limit is now a shared constant, the model is told its budget in the instructions, and the sidebar flags a run that used all of it.
stderr came back from playwright.execute and went to the model, but the sidebar dropped it - the most useful thing on screen for a failed step. stdout stays unrendered and untracked in the sidebar's own Step type; the SDK response itself is unaffected.
Neither getHighlighter().then(...) nor copy() had a rejection path - a lazy shiki chunk-load failure logged an unhandled rejection, and a clipboard write that fails (denied permission, non-HTTPS origin) left the copy button silently doing nothing.
highlighter ??= kept the failed promise forever, so one transient chunk-load failure permanently disabled syntax highlighting for the rest of the page's life. It now clears the cache on rejection so the next call retries.
useEffect(..., [code]) re-ran shiki on every token appended during input-streaming, costing a full highlight pass per chunk on top of the dangerouslySetInnerHTML swap. Highlighting is now skipped until the step leaves the "writing" state and the code is final; the plain <pre> fallback already covers the streaming case.
The scale lived in @layer components as plain classes, and Tailwind only generates variants (md:, hover:, etc.) for utilities declared with @Utility - so md:text-heading-02 on the hero <h1> was dead CSS and the headline never grew past text-heading-04 at any breakpoint. Verified in the built stylesheet: md\:text-heading-02 now emits a rule.
/api/agent takes any sessionId and runs model-written playwright against it with no auth or rate limiting - worth a line under the deploy button given "Deploy with Vercel" makes a public deploy the default path. Also corrected the streaming section, which still named createAgentUIStreamResponse after the route moved to toUIMessageStreamResponse, and added the two new lib/ files to the code map.
A request /api/agent rejects before streaming starts (missing
sessionId, missing an api key) returns a plain { error } JSON body.
useChat's transport throws the raw response text as the error message
on any non-OK response, so that JSON showed up verbatim in the sidebar
instead of the clean sentence the create-browser route already shows
for the same class of error. The sidebar now unwraps a JSON { error }
body back to its message; a real in-stream error (already plain text
via onError) passes through unchanged.
masnwilliams
left a comment
There was a problem hiding this comment.
this is a strong refresh overall: the main page is substantially simpler, the streamed agent UI fits the template well, and the dependency migration is clean. requesting one small lifecycle fix before merge. the live-view sizing comment is non-blocking but worth verifying against the previous resize-loop failure mode.
masnwilliams
left a comment
There was a problem hiding this comment.
superseding my earlier changes-requested state. the close-during-execution behavior and responsive live-view sizing are worth following up on, but neither should block this template refresh without evidence of a user-visible failure.
stop() only aborts the chat stream; an in-flight playwright.execute() keeps running in the vm regardless, so closing the browser right after could race a delete against an execution still using the session. Disabling close while status is submitted/streaming removes the race instead of trying to outrun it. Addresses masnwilliams' review comment on app/page.tsx:118 (PR #6, review 5203949180). Left the live-view sizing comment on BrowserPanel.tsx alone per request - manual testing first.
The call/body column had min-w-0, letting flex shrink it to near nothing whenever the stat text next to it was long (e.g. "0 calls · 0ms in browser" on the execute row). Since kernel.browsers.playwright .execute() has no spaces to wrap on, the text just overflowed past its collapsed box and sat on top of the stat text instead of wrapping. Giving that column a real min width and letting the call text break mid-word lets flex-wrap push the stat onto its own line instead, which is what the other two rows already did by having a short or absent stat. Verified with no overlap from 320px to 1024px wide.
close-during-execution behavior is fixed + live-view sizing is not an issue on this repo (tested live). should be all set |

Summary
Brings the template onto the current KERNEL brand and current SDKs, and replaces the end-of-run steps modal with a sidebar that streams while the agent works.
What changed
Branding. Replaced the black/purple styling with the KERNEL design system: kernel green, beige, and charcoal tokens (the palette is closed — default Tailwind colors are cleared in
globals.css), Inter + IBM Plex Mono, square corners, lowercase copy, and the wordmark and mark from kernel.sh/brand-assets.Dependencies.
ai@ai-sdk/openai@ai-sdk/react@onkernel/sdknextreact/react-domtailwindcssgeist,@onkernel/ai-sdk,playwright-coreshiki@onkernel/ai-sdkpinsai: ^5and has not been published since november 2025, so it blocks the AI SDK upgrade. Its whole implementation is a 25-line wrapper aroundkernel.browsers.playwright.execute, so the tool now lives inlib/playwright-tool.tswith the same name, input schema, and behavior. That also makes the execution path readable in the template itself, which is the point of the sidebar.Streaming.
/api/agentmoves from a blockingagent.generate()to aToolLoopAgentwithtoUIMessageStreamResponse(), and the client renders thetool-playwright_executeparts fromuseChat. Steps, generated code, timing, return values, and failures appear while the run is in flight instead of after it ends, and the composer stays usable for follow-up tasks on the same session. Stopping a run stops the model from issuing further tool calls and marks the unfinished step as cancelled; a browser call already in flight runs to completion, since Kernel doesn't support cancelling one mid-execution.Layout. The browser live view and the steps sit side by side, with the session id, spin-up time, region, and stealth state in the panel header, and a strip underneath showing the three SDK calls with that session's numbers.
Housekeeping. The lint script pointed at
next lint, which no longer exists in Next 16, and.eslintrc.jsondoes not work with ESLint 9; both are replaced with a flat config and all findings are fixed.typescript.ignoreBuildErrorsis gone so the template typechecks on build.allowedDevOriginslets the dev overlay load when the server is opened at127.0.0.1rather thanlocalhost. Unused components, assets, andconsole.logs are removed.Verification
bun run typecheck,bun run lint, andbun run buildpass.Notes
@onkernel/ai-sdk; if that stays the documented path, itsaipeer range should be widened to^5 || ^6 || ^7.Note
Medium Risk
Public
/api/agentstill runs model-generated Playwright against any suppliedsessionIdon the deployer's API keys with no auth; streaming and SDK upgrades change runtime behavior for all agent runs.Overview
Rebrands the Next.js template around KERNEL (beige/charcoal/green tokens, Inter + IBM Plex Mono, lowercase UI) and upgrades the stack—AI SDK 7, gpt-5.4,
ToolLoopAgent, and a much newer@onkernel/sdk—while dropping@onkernel/ai-sdkin favor of a locallib/playwright-tool.tswrapper aroundkernel.browsers.playwright.execute./api/agentno longer blocks ongenerate()with a one-shottask; it accepts chatmessages+sessionId, streams viatoUIMessageStreamResponse(), and the home page usesuseChatsotool-playwright_executesteps (code, timing, results, errors) show inAgentStepsSidebarbesideBrowserPanel’s live view. Browser creation now sets a 1280×800 viewport, 30‑minute session timeout, and returns region/stealth; sessions can be restored fromsessionStorageafter refresh.Docs, deploy clone URL, env examples, and README are rewritten for the new flow;
.eslintrc.jsonis removed as part of the lint/tooling refresh described in the PR notes.Reviewed by Cursor Bugbot for commit 1befb96. Bugbot is set up for automated code reviews on this repo. Configure here.