Skip to content

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

Merged
dprevoznik merged 35 commits into
mainfrom
hypeship/3.0-refresh
Sep 15, 2026
Merged

dprevoznik merged 35 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 toUIMessageStreamResponse(), 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 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.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
Public /api/agent still runs model-generated Playwright against any supplied sessionId on 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-sdk in favor of a local lib/playwright-tool.ts wrapper around kernel.browsers.playwright.execute.

/api/agent no longer blocks on generate() with a one-shot task; it accepts chat messages + sessionId, streams via toUIMessageStreamResponse(), and the home page uses useChat so tool-playwright_execute steps (code, timing, results, errors) show in AgentStepsSidebar beside BrowserPanel’s live view. Browser creation now sets a 1280×800 viewport, 30‑minute session timeout, and returns region/stealth; sessions can be restored from sessionStorage after refresh.

Docs, deploy clone URL, env examples, and README are rewritten for the new flow; .eslintrc.json is 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.

…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 15, 2026 2:03am 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.
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 masnwilliams left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

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.

Comment thread app/page.tsx Outdated
Comment thread components/BrowserPanel.tsx
masnwilliams
masnwilliams previously approved these changes Sep 14, 2026

@masnwilliams masnwilliams left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

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.
@dprevoznik

Copy link
Copy Markdown
Contributor Author

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.

close-during-execution behavior is fixed + live-view sizing is not an issue on this repo (tested live). should be all set

@dprevoznik
dprevoznik merged commit ef6c6a9 into main Sep 15, 2026
4 checks passed
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.

2 participants