Skip to content

Commit 8a21ef1

Browse files
authored
Add named Playwright executor support to browsers playwright commands (#281)
## Summary Adds named Playwright executor support to `kernel browsers playwright`. - `kernel browsers playwright execute --executor <name>`: runs the call in a named executor (sent only when set). Help text explains concurrency across executors, serialization within one, the always-present `default` executor bound to the active tab, named executors owning a background tab that `page` is bound to, and the limit of 8 named executors. - The execute result now shows the `tab` the call was bound to (`Tab Target ID`, `Tab Created`) in the table output; JSON output passes it through from the API response. - A 409 from execute (executor limit reached) is rendered as the API message followed by the current executors (name, busy marker, tab URL) and the delete command to run, instead of the generic error formatting. - New `kernel browsers playwright executors list <id-or-name>`: table of name, busy, created at, last used at, target ID, URL (default first); supports `-o json`. - New `kernel browsers playwright executors delete <id-or-name> <executor> [--close-tab]`: `--close-tab` defaults to true and is only sent when set explicitly. Deleting `default` reports a restart rather than a deletion, matching API semantics. - The playwright command group is now built by `newBrowsersPlaywrightCommand()` so it can be exercised in tests the same way the webmcp group is. - README command reference and examples updated. ## Dependencies Bumps `github.com/kernel/kernel-go-sdk` to v0.120.0 (released), which adds the executors surface: `BrowserPlaywrightExecuteParams.Executor`, `Tab` on the execute response, `BrowserPlaywrightExecutorService.List/Delete`, `Executor`, and `ExecutorList`. ## Tests `cmd/browsers_playwright_executors_test.go` covers command wiring and flag defaults, the `executor` request param (absent vs set), tab rendering in table and JSON output, the 409 limit error, list table/JSON rendering, delete path and `close_tab` query handling, API error cleanup, and argument validation, using an `httptest` server like the webmcp tests. `go build ./...`, `go vet ./...`, `go test ./...`, and `golangci-lint run` all pass locally. --------- Co-authored-by: rgarcia <72655+rgarcia@users.noreply.github.com>
1 parent 38a711c commit 8a21ef1

4 files changed

Lines changed: 607 additions & 107 deletions

File tree

‎README.md‎

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1009,8 +1009,12 @@ Destinations are the OTLP/HTTP endpoints sessions export to. They belong to the
10091009
### Browser Playwright
10101010

10111011
- `kernel browsers playwright execute <id> [code]` - Execute Playwright/TypeScript code against the browser
1012+
- `--executor <name>` - Executor to run the call in. Calls on different executors run concurrently in separate tabs of the same browser; calls on one executor run one at a time. Omit to use the always-present `default` executor bound to the active tab. Any other name creates a named executor on first use that owns a background tab `page` is bound to. At most 8 named executors per browser (409 when exceeded)
10121013
- `--timeout <seconds>` - Maximum execution time in seconds (defaults server-side)
10131014
- If `[code]` is omitted, code is read from stdin
1015+
- `kernel browsers playwright executors list <id>` - List the browser's Playwright executors (default first) with busy state, timestamps, and the tab each named executor owns
1016+
- `kernel browsers playwright executors delete <id> <executor>` - Delete a Playwright executor. Deleting `default` restarts it instead of removing it
1017+
- `--close-tab` - Close the tab owned by the executor (default: true)
10141018

10151019
### Browser REPL
10161020

@@ -1524,6 +1528,15 @@ TS
15241528
# With a timeout in seconds
15251529
kernel browsers playwright execute my-browser --timeout 30 'await (await context.newPage()).goto("https://example.com")'
15261530

1531+
# Drive two tabs in parallel with named executors (each owns its own tab)
1532+
kernel browsers playwright execute my-browser --executor docs 'await page.goto("https://example.com/docs"); return await page.title();' &
1533+
kernel browsers playwright execute my-browser --executor pricing 'await page.goto("https://example.com/pricing"); return await page.title();' &
1534+
wait
1535+
1536+
# List executors and delete one (closes its tab unless --close-tab=false)
1537+
kernel browsers playwright executors list my-browser
1538+
kernel browsers playwright executors delete my-browser docs
1539+
15271540
# Mini CDP connection load test (10s)
15281541
cat <<'TS' | kernel browsers playwright execute my-browser
15291542
const start = Date.now();

‎cmd/browsers.go‎

Lines changed: 12 additions & 107 deletions
Original file line numberDiff line numberDiff line change
@@ -90,11 +90,6 @@ type BrowserLogService interface {
9090
StreamStreaming(ctx context.Context, idOrName string, query kernel.BrowserLogStreamParams, opts ...option.RequestOption) (stream *ssestream.Stream[shared.LogEvent])
9191
}
9292

93-
// BrowserPlaywrightService defines the subset we use for Playwright execution.
94-
type BrowserPlaywrightService interface {
95-
Execute(ctx context.Context, idOrName string, body kernel.BrowserPlaywrightExecuteParams, opts ...option.RequestOption) (res *kernel.BrowserPlaywrightExecuteResponse, err error)
96-
}
97-
9893
// BrowserComputerService defines the subset we use for OS-level mouse & screen.
9994
type BrowserComputerService interface {
10095
Batch(ctx context.Context, idOrName string, body kernel.BrowserComputerBatchParams, opts ...option.RequestOption) (err error)
@@ -517,16 +512,17 @@ type BrowsersUpdateInput struct {
517512

518513
// BrowsersCmd is a cobra-independent command handler for browsers operations.
519514
type BrowsersCmd struct {
520-
browsers BrowsersService
521-
replays BrowserReplaysService
522-
fs BrowserFSService
523-
fsWatch BrowserFWatchService
524-
process BrowserProcessService
525-
logs BrowserLogService
526-
computer BrowserComputerService
527-
playwright BrowserPlaywrightService
528-
telemetry BrowserTelemetryService
529-
webmcp BrowserWebMCPService
515+
browsers BrowsersService
516+
replays BrowserReplaysService
517+
fs BrowserFSService
518+
fsWatch BrowserFWatchService
519+
process BrowserProcessService
520+
logs BrowserLogService
521+
computer BrowserComputerService
522+
playwright BrowserPlaywrightService
523+
playwrightExecutors BrowserPlaywrightExecutorService
524+
telemetry BrowserTelemetryService
525+
webmcp BrowserWebMCPService
530526
}
531527

532528
type BrowsersListInput struct {
@@ -1836,64 +1832,6 @@ type BrowsersFSWatchEventsInput struct {
18361832
WatchID string
18371833
}
18381834

1839-
// Playwright
1840-
type BrowsersPlaywrightExecuteInput struct {
1841-
Identifier string
1842-
Code string
1843-
Timeout int64
1844-
Output string
1845-
}
1846-
1847-
func (b BrowsersCmd) PlaywrightExecute(ctx context.Context, in BrowsersPlaywrightExecuteInput) error {
1848-
if err := validateJSONOutput(in.Output); err != nil {
1849-
return err
1850-
}
1851-
1852-
if b.playwright == nil {
1853-
pterm.Error.Println("playwright service not available")
1854-
return nil
1855-
}
1856-
br, err := b.browsers.Get(ctx, in.Identifier, kernel.BrowserGetParams{})
1857-
if err != nil {
1858-
return util.CleanedUpSdkError{Err: err}
1859-
}
1860-
params := kernel.BrowserPlaywrightExecuteParams{Code: in.Code}
1861-
if in.Timeout > 0 {
1862-
params.TimeoutSec = kernel.Opt(in.Timeout)
1863-
}
1864-
res, err := b.playwright.Execute(ctx, br.SessionID, params)
1865-
if err != nil {
1866-
return util.CleanedUpSdkError{Err: err}
1867-
}
1868-
1869-
if in.Output == "json" {
1870-
return util.PrintPrettyJSON(res)
1871-
}
1872-
1873-
rows := pterm.TableData{{"Property", "Value"}, {"Success", fmt.Sprintf("%t", res.Success)}}
1874-
PrintTableNoPad(rows, true)
1875-
1876-
if res.Stdout != "" {
1877-
pterm.Info.Println("stdout:")
1878-
fmt.Println(res.Stdout)
1879-
}
1880-
if res.Stderr != "" {
1881-
pterm.Info.Println("stderr:")
1882-
fmt.Fprintln(os.Stderr, res.Stderr)
1883-
}
1884-
if res.Result != nil {
1885-
bs, err := json.MarshalIndent(res.Result, "", " ")
1886-
if err == nil {
1887-
pterm.Info.Println("result:")
1888-
fmt.Println(string(bs))
1889-
}
1890-
}
1891-
if !res.Success && res.Error != "" {
1892-
pterm.Error.Printf("error: %s\n", res.Error)
1893-
}
1894-
return nil
1895-
}
1896-
18971835
// REPL
18981836
type BrowsersReplInput struct {
18991837
Identifier string
@@ -3201,13 +3139,7 @@ func init() {
32013139
computerRoot.AddCommand(computerClick, computerMove, computerScreenshot, computerType, computerPressKey, computerScroll, computerDrag, computerSetCursor, computerGetMousePosition, computerBatch, computerReadClipboard, computerWriteClipboard)
32023140
browsersCmd.AddCommand(computerRoot)
32033141

3204-
// playwright
3205-
playwrightRoot := &cobra.Command{Use: "playwright", Short: "Playwright operations"}
3206-
playwrightExecute := &cobra.Command{Use: "execute <id> [code]", Short: "Execute Playwright/TypeScript code against the browser", Args: cobra.MinimumNArgs(1), RunE: runBrowsersPlaywrightExecute}
3207-
playwrightExecute.Flags().Int64("timeout", 0, "Maximum execution time in seconds (default per server)")
3208-
addJSONOutputFlag(playwrightExecute)
3209-
playwrightRoot.AddCommand(playwrightExecute)
3210-
browsersCmd.AddCommand(playwrightRoot)
3142+
browsersCmd.AddCommand(newBrowsersPlaywrightCommand())
32113143

32123144
// repl
32133145
replCmd := &cobra.Command{
@@ -3828,33 +3760,6 @@ func runBrowsersFSWatchEvents(cmd *cobra.Command, args []string) error {
38283760
return b.FSWatchEvents(cmd.Context(), BrowsersFSWatchEventsInput{Identifier: args[0], WatchID: args[1]})
38293761
}
38303762

3831-
func runBrowsersPlaywrightExecute(cmd *cobra.Command, args []string) error {
3832-
client := getKernelClient(cmd)
3833-
svc := client.Browsers
3834-
3835-
var code string
3836-
if len(args) >= 2 {
3837-
code = strings.Join(args[1:], " ")
3838-
} else {
3839-
// Read code from stdin
3840-
stat, _ := os.Stdin.Stat()
3841-
if (stat.Mode() & os.ModeCharDevice) != 0 {
3842-
pterm.Error.Println("no code provided. Provide code as an argument or pipe via stdin")
3843-
return nil
3844-
}
3845-
data, err := io.ReadAll(os.Stdin)
3846-
if err != nil {
3847-
pterm.Error.Printf("failed to read stdin: %v\n", err)
3848-
return nil
3849-
}
3850-
code = string(data)
3851-
}
3852-
timeout, _ := cmd.Flags().GetInt64("timeout")
3853-
output, _ := cmd.Flags().GetString("output")
3854-
b := BrowsersCmd{browsers: &svc, playwright: &svc.Playwright}
3855-
return b.PlaywrightExecute(cmd.Context(), BrowsersPlaywrightExecuteInput{Identifier: args[0], Code: strings.TrimSpace(code), Timeout: timeout, Output: output})
3856-
}
3857-
38583763
func runBrowsersRepl(cmd *cobra.Command, args []string) error {
38593764
client := getKernelClient(cmd)
38603765
svc := client.Browsers

0 commit comments

Comments
 (0)