Skip to content

feat(effects): add fx.focusView so a pure update can move view focus - #218

Open
sonhyrd wants to merge 1 commit into
vercel-labs:mainfrom
sonhyrd:fx-focus-view
Open

feat(effects): add fx.focusView so a pure update can move view focus#218
sonhyrd wants to merge 1 commit into
vercel-labs:mainfrom
sonhyrd:fx-focus-view

Conversation

@sonhyrd

@sonhyrd sonhyrd commented Jul 27, 2026

Copy link
Copy Markdown

Summary

Adds fx.focusView(window_label, view_label) — the effects-channel route to Runtime.focusView, which a pure update cannot reach.

What was already there

Everything except the effect. The runtime method is complete and the bridge already exposes it; only the Zig app-facing verb was missing.

Layer Existing location
Runtime.focusView(window_id, label) src/runtime/window_view_runtime.zig:161
native-sdk.view.focus bridge command src/runtime/builtin_bridge.zig:322 (focusViewFromJson)
PlatformServices.focus_view_fn src/platform/types.zig:2360, implemented on the null platform and the hosts
WindowActionBinding deferral pattern src/runtime/effects.zigcloseWindow (:7757), minimizeWindow, showWindow, quitApp

So a page can move focus through the bridge, and a Zig update cannot move it at all.

Why this matters

Consider the layout the SDK now supports directly: a canvas, one or more <terminal> elements, and a WebView in one window. Keyboard focus does not reach across those panes any other way:

  • There is no key-view-loop wiring in appkit_host.m — no nextKeyView, no recalculateKeyViewLoop, no initialFirstResponder — and doCommandBySelector's insertTab: (:7032) synthesizes a tab key into the canvas widget ring instead of calling selectNextKeyView:. Measured on that layout: eight Tab presses from a canvas text field walk six canvas widgets and produce zero page events.
  • Independently, a focused <terminal> is a Tab trap — correctly, since Tab belongs to the shell running in the pty.

So an app with this layout has to own pane cycling on its own chord. Runtime.focusView is exactly the primitive for it; it just was not spendable from update.

Shape

Follows the four window verbs exactly — fire-and-forget, deferred through WindowActionBinding, mirrored for hermetic tests:

.cycle_pane => fx.focusView("main", model.nextPaneLabel()),
  • focus_view_fn on WindowActionBinding; effectsFocusViewByLabel in ui_app.zig resolves the window label and calls Runtime.focusView.
  • Refusals stay where they already live: Runtime.focusView reports unknown-view and unfocusable-view as errors, and the fire-and-forget contract turns them into no-ops — same contract closeWindow documents for an unknown label.
  • WindowActionState grows focus_view_count and lastViewLabel(). The window label rides lastLabel() with every other verb's, so the two together are the whole request.

Scope

  • One verb. focusNextView / focusPreviousView would follow the same shape if you want the trio; the by-label one is what the pane-cycling case needs, and the rest seemed better left to you.
  • The signature is not the point — fx.focusView(label) resolving against the primary window would serve a single-window app fine.
  • Transpiled cores are untouched; a Cmd counterpart is a separate decision.

Verification

  • zig build test: no new failures (this checkout has 6 pre-existing failed command lanes locally — codesign identity, packaging fixtures — identical before and after).
  • The desktop-ui-shell-tests lane run directly: 151/151 pass (150 before, plus the new one).
  • zig fmt --check clean.

Tests

fx.focusView moves keyboard focus between views from a pure update (src/runtime/ui_app_window_tests.zig) — mounts a second gpu_surface view beside the app canvas, dispatches a Msg whose update calls fx.focusView, and asserts the view took focus, the canvas lost it, and the mirror carries both labels. Then dispatches against an unknown view label and asserts the focus did not move and nothing crashed, while the mirror still counts the request.

@vercel

vercel Bot commented Jul 27, 2026

Copy link
Copy Markdown

@sondh0127 is attempting to deploy a commit to the Vercel Labs Team on Vercel.

A member of the Team first needs to authorize it.

`Runtime.focusView` and the `native-sdk.view.focus` bridge command both
ship, but the effects channel had no focus verb — so a native-rendered
app's `update`, which never sees the Runtime, could not move focus
between its own views at all.

That is load-bearing for a shell whose window holds a canvas beside a
`<terminal>` and a WebView: AppKit's key-view loop does not span them
(nothing in `appkit_host.m` wires `nextKeyView`, and `insertTab:`
synthesizes a tab KEY into the canvas ring instead of calling
`selectNextKeyView:`), and a focused terminal correctly keeps Tab for
its child. Such an app has to own pane cycling on its own chord, and
`Runtime.focusView` is exactly the primitive for it.

Adds `focus_view_fn` to `WindowActionBinding`, `focusView` to
`Effects(Msg)` beside the other window verbs, and `focus_view_count` /
`lastViewLabel()` to the `WindowActionState` mirror — the window label
rides `lastLabel()` with every other verb's, so the two together are the
whole request. Refusals stay where they already live: `Runtime.focusView`
reports unknown-view and unfocusable-view as errors, and the
fire-and-forget contract turns them into no-ops.
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