Skip to content

Overhaul the template: KERNEL branding, current SDKs, streamed steps sidebar - #6

Open
dprevoznik wants to merge 32 commits into
mainfrom
hypeship/3.0-refresh
Open

Overhaul the template: KERNEL branding, current SDKs, streamed steps sidebar#6
dprevoznik wants to merge 32 commits into
mainfrom
hypeship/3.0-refresh

Conversation

@dprevoznik

@dprevoznik dprevoznik commented Sep 11, 2026

Copy link
Copy Markdown
Contributor

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.

package before after
ai 5.0.89 7.0.97
@ai-sdk/openai ^2.0.69 ^4.0.65
@ai-sdk/react ^4.0.100
@onkernel/sdk 0.22.0 0.100.0
next 16.0.7 16.3.4
react / react-dom 19.2.1 19.3.0
tailwindcss ^4.1.14 ^4.3.3
geist, @onkernel/ai-sdk, playwright-core removed
shiki ^4.4.3

@onkernel/ai-sdk pins ai: ^5 and has not been published since november 2025, so it blocks the AI SDK upgrade. Its whole implementation is a 25-line wrapper around kernel.browsers.playwright.execute, so the tool now lives in lib/playwright-tool.ts with 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/agent moves from a blocking agent.generate() to a ToolLoopAgent with createAgentUIStreamResponse, and the client renders the tool-playwright_execute parts from useChat. 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 aborts the agent loop (not just the client stream) and marks the unfinished step as cancelled.

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.json does not work with ESLint 9; both are replaced with a flat config and all findings are fixed. typescript.ignoreBuildErrors is gone so the template typechecks on build. allowedDevOrigins lets the dev overlay load when the server is opened at 127.0.0.1 rather than localhost. Unused components, assets, and console.logs are removed.

Verification

  • bun run typecheck, bun run lint, and bun run build pass.
  • Ran the built app end to end against a real KERNEL session: created a browser, ran a task where one step failed with a playwright strict-mode error and the agent recovered on the next step, ran a second task on the same session, stopped a run mid-step, and closed the browser. The live view rendered throughout.
  • Same flow checked in dev mode.

Notes

  • The sidebar numbers steps per task, so a second task on the same session starts again at 01.
  • The brand palette has no error color, so failed steps use charcoal on beige-muted with the raw error in mono.
  • The docs page for the vercel ai sdk integration still describes @onkernel/ai-sdk; if that stays the documented path, its ai peer range should be widened to ^5 || ^6 || ^7.

Note

Medium Risk
Large dependency and agent-path refactor plus unauthenticated /api/agent that runs model-generated Playwright on any supplied sessionId using deployer API keys.

Overview
Rebrands the Next.js template to KERNEL (beige/green tokens, Inter + Plex Mono, new hero and split layout) and upgrades the stack to AI SDK 7, gpt-5.4, and a current @onkernel/sdk, dropping @onkernel/ai-sdk in favor of lib/playwright-tool.ts.

Agent flow moves from a blocking POST with { sessionId, task } to ToolLoopAgent streaming via toUIMessageStreamResponse(); the UI uses useChat and AgentStepsSidebar so playwright steps, code, timing, results, and errors appear live. Follow-up tasks reuse the same browser session; sessionStorage restores session metadata on refresh.

Browser API tweaks: smaller viewport, 30-minute session timeout, and region/stealth on create. README and deploy URLs point at kernel/kernel-nextjs-template; docs call out that /api/agent is unauthenticated on public deploys.

Reviewed by Cursor Bugbot for commit 3ddfa6b. Bugbot is set up for automated code reviews on this repo. Configure here.

…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
@vercel

vercel Bot commented Sep 11, 2026

Copy link
Copy Markdown

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated
kernel-nextjs-template Ready Ready Preview Sep 11, 2026 8:57pm UTC

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.

@cursor cursor Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Stale Bugbot comment from a previous run.

Comment thread app/api/agent/route.ts
Comment thread app/page.tsx
- 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

@cursor cursor Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Stale Bugbot comment from a previous run.

Comment thread components/AgentStepsSidebar.tsx Outdated
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.

@cursor cursor Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Stale Bugbot comment from a previous run.

Comment thread app/page.tsx
Comment thread components/BrowserPanel.tsx Outdated
- 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

@cursor cursor Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Cursor Bugbot has reviewed your changes using default effort and found 1 potential issue.

Fix All in Cursor

❌ 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.

Comment thread components/BrowserPanel.tsx Outdated
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.
Comment thread app/api/create-browser/route.ts
this was not the behaviour we wanted: sessions keep the inactivity timeout and
the explicit close, and no longer carry a deadline, a tag, or the sweep.
req.json() threw on an empty or malformed body before either route's
own validation ran, so Next returned an opaque 500 instead of the
intended 400 for a missing sessionId.
KERNEL_API_KEY was checked but OPENAI_API_KEY, the other variable
.env.example asks for, was not - a deploy with only the former set
first fails deep inside the agent stream. toUIMessageStreamResponse
also defaulted onError to a fixed "An error occurred." string, so that
failure (and any other) reached the sidebar with no detail. This is a
template running on the deployer's own keys, so showing the real
message is the right tradeoff.
timeout_seconds matched maxDuration exactly, so a task started any
time after the browser was created could hit a session that expires
mid-run - surfacing as a raw SDK error on a playwright_execute step
instead of a normal timeout.
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.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant