diff --git a/images/chromium-headful/demos/demo_smooth_typing.py b/images/chromium-headful/demos/demo_smooth_typing.py
new file mode 100644
index 00000000..14502073
--- /dev/null
+++ b/images/chromium-headful/demos/demo_smooth_typing.py
@@ -0,0 +1,137 @@
+#!/usr/bin/env python3
+"""
+Demo script: smooth typing vs instant typing.
+
+Drives the typing_demo.html page through the kernel-images API to produce
+a side-by-side comparison suitable for recording as a GIF/MP4.
+
+Usage:
+ # 1. Start a kernel-images container
+ # 2. Upload typing_demo.html to the container
+ # 3. Run this script:
+ python demo_smooth_typing.py --base-url http://localhost:8000
+
+Requirements:
+ pip install requests
+"""
+
+import argparse
+import base64
+import json
+import time
+from pathlib import Path
+
+import requests
+
+DEMO_TEXT = "The quick brown fox jumps over the lazy dog. Hello world!"
+
+
+def api(base: str, method: str, path: str, **kwargs):
+ url = f"{base}{path}"
+ resp = getattr(requests, method)(url, **kwargs)
+ resp.raise_for_status()
+ return resp
+
+
+def upload_demo_page(base: str):
+ html_path = Path(__file__).parent / "typing_demo.html"
+ html_bytes = html_path.read_bytes()
+ api(base, "put", "/fs/write_file", params={"path": "/tmp/typing_demo.html"},
+ data=html_bytes, headers={"Content-Type": "application/octet-stream"})
+ print("Uploaded typing_demo.html")
+
+
+def execute_js(base: str, code: str):
+ api(base, "post", "/playwright/execute", json={"code": code, "timeout_sec": 10})
+
+
+def navigate(base: str):
+ execute_js(base, "await page.goto('file:///tmp/typing_demo.html');")
+ time.sleep(1)
+
+
+def click_input(base: str):
+ execute_js(base, "await page.click('#input');")
+ time.sleep(0.3)
+
+
+def clear_input(base: str):
+ execute_js(base, "window.demoApi.clear();")
+ time.sleep(0.3)
+
+
+def set_mode(base: str, label: str, cls: str):
+ execute_js(base, f"window.demoApi.setMode('{label}', '{cls}');")
+
+
+def type_text(base: str, text: str, smooth: bool = False, typo_chance: float = 0):
+ body = {"text": text, "smooth": smooth}
+ if typo_chance > 0:
+ body["typo_chance"] = typo_chance
+ if not smooth:
+ body["delay"] = 0
+ api(base, "post", "/computer/type", json=body)
+
+
+def start_recording(base: str):
+ api(base, "post", "/recording/start", json={"framerate": 15, "id": "typing-demo"})
+ print("Recording started")
+ time.sleep(0.5)
+
+
+def stop_recording(base: str):
+ api(base, "post", "/recording/stop", json={"id": "typing-demo"})
+ time.sleep(1)
+ print("Recording stopped")
+
+
+def download_recording(base: str, output: str):
+ resp = api(base, "get", "/recording/download", params={"id": "typing-demo"})
+ Path(output).write_bytes(resp.content)
+ print(f"Saved recording to {output}")
+
+
+def run_demo(base: str, output: str):
+ upload_demo_page(base)
+ navigate(base)
+ start_recording(base)
+
+ # --- Phase 1: Instant typing (no delay) ---
+ set_mode(base, "INSTANT TYPING — delay: 0", "instant")
+ time.sleep(1)
+ click_input(base)
+ type_text(base, DEMO_TEXT, smooth=False)
+ time.sleep(2)
+
+ # --- Phase 2: Smooth typing (no typos) ---
+ clear_input(base)
+ set_mode(base, "SMOOTH TYPING — HUMAN-LIKE", "smooth")
+ time.sleep(1)
+ click_input(base)
+ type_text(base, DEMO_TEXT, smooth=True)
+ time.sleep(2)
+
+ # --- Phase 3: Smooth typing with typos ---
+ clear_input(base)
+ set_mode(base, "SMOOTH TYPING — WITH TYPOS", "typos")
+ time.sleep(1)
+ click_input(base)
+ type_text(base, DEMO_TEXT, smooth=True, typo_chance=0.04)
+ time.sleep(2)
+
+ stop_recording(base)
+ download_recording(base, output)
+
+
+def main():
+ parser = argparse.ArgumentParser(description="Smooth typing demo recorder")
+ parser.add_argument("--base-url", default="http://localhost:8000",
+ help="Base URL of the kernel-images API")
+ parser.add_argument("--output", default="smooth_typing_demo.mp4",
+ help="Output video file path")
+ args = parser.parse_args()
+ run_demo(args.base_url, args.output)
+
+
+if __name__ == "__main__":
+ main()
diff --git a/images/chromium-headful/demos/typing_demo.html b/images/chromium-headful/demos/typing_demo.html
new file mode 100644
index 00000000..3d4cf4be
--- /dev/null
+++ b/images/chromium-headful/demos/typing_demo.html
@@ -0,0 +1,170 @@
+
+
+
+
+
+ Typing Demo
+
+
+
+ Typing Demo
+
+
+
+
+
+
+
+ Keystroke timing visualization
+
+
+
+
diff --git a/server/cmd/api/api/computer.go b/server/cmd/api/api/computer.go
index 2367cd68..8ed937cd 100644
--- a/server/cmd/api/api/computer.go
+++ b/server/cmd/api/api/computer.go
@@ -18,6 +18,7 @@ import (
"github.com/onkernel/kernel-images/server/lib/logger"
"github.com/onkernel/kernel-images/server/lib/mousetrajectory"
+ "github.com/onkernel/kernel-images/server/lib/typinghumanizer"
oapi "github.com/onkernel/kernel-images/server/lib/oapi"
)
@@ -450,19 +451,20 @@ func (s *ApiService) TakeScreenshot(ctx context.Context, request oapi.TakeScreen
}
func (s *ApiService) doTypeText(ctx context.Context, body oapi.TypeTextRequest) error {
+ if body.Smooth != nil && *body.Smooth {
+ return s.doTypeTextSmooth(ctx, body)
+ }
+
log := logger.FromContext(ctx)
- // Validate delay if provided
if body.Delay != nil && *body.Delay < 0 {
return &validationError{msg: "delay must be >= 0 milliseconds"}
}
- // Build xdotool arguments
args := []string{"type"}
if body.Delay != nil {
args = append(args, "--delay", strconv.Itoa(*body.Delay))
}
- // Use "--" to terminate options and pass raw text
args = append(args, "--", body.Text)
output, err := defaultXdoTool.Run(ctx, args...)
@@ -474,6 +476,170 @@ func (s *ApiService) doTypeText(ctx context.Context, body oapi.TypeTextRequest)
return nil
}
+func (s *ApiService) doTypeTextSmooth(ctx context.Context, body oapi.TypeTextRequest) error {
+ log := logger.FromContext(ctx)
+
+ if body.TypoChance != nil && (*body.TypoChance < 0 || *body.TypoChance > 0.10) {
+ return &validationError{msg: "typo_chance must be between 0.0 and 0.10"}
+ }
+
+ rng := rand.New(rand.NewSource(rand.Int63()))
+ runes := []rune(body.Text)
+
+ var typoRate float64
+ if body.TypoChance != nil {
+ typoRate = float64(*body.TypoChance)
+ }
+ typos := typinghumanizer.GenerateTypoPositions(rng, len(runes), typoRate)
+
+ // Build a typo lookup set for O(1) access during chunk iteration
+ typoByPos := map[int]typinghumanizer.Typo{}
+ for _, typo := range typos {
+ typoByPos[typo.Pos] = typo
+ }
+
+ chunks := typinghumanizer.SplitWordChunks(body.Text)
+ if len(chunks) == 0 {
+ return nil
+ }
+
+ globalPos := 0
+ for chunkIdx, chunk := range chunks {
+ select {
+ case <-ctx.Done():
+ return &executionError{msg: "typing cancelled"}
+ default:
+ }
+
+ chunkRunes := []rune(chunk)
+ chunkStart := globalPos
+ chunkEnd := chunkStart + len(chunkRunes)
+
+ // Find typos within this chunk
+ var chunkTypo *typinghumanizer.Typo
+ for pos := chunkStart; pos < chunkEnd; pos++ {
+ if t, ok := typoByPos[pos]; ok {
+ chunkTypo = &t
+ break
+ }
+ }
+
+ intraDelayMs := rng.Intn(70) + 50
+
+ if chunkTypo == nil {
+ if err := s.xdotoolTypeChunk(ctx, chunk, intraDelayMs); err != nil {
+ log.Error("xdotool type chunk failed", "err", err, "chunk", chunkIdx)
+ return &executionError{msg: "failed during smooth typing"}
+ }
+ } else {
+ localPos := chunkTypo.Pos - chunkStart
+ if err := s.typeChunkWithTypo(ctx, log, rng, chunkRunes, localPos, *chunkTypo, intraDelayMs); err != nil {
+ return err
+ }
+ }
+
+ globalPos = chunkEnd
+
+ if chunkIdx < len(chunks)-1 {
+ pause := typinghumanizer.UniformJitter(rng, 140, 60, 60)
+ if typinghumanizer.IsSentenceEnd(chunk) {
+ pause = pause * 3 / 2
+ }
+ if err := sleepWithContext(ctx, pause); err != nil {
+ return &executionError{msg: "typing cancelled"}
+ }
+ }
+ }
+
+ log.Info("executed smooth typing", "chunks", len(chunks), "typos", len(typos), "textLen", len(body.Text))
+ return nil
+}
+
+func (s *ApiService) xdotoolTypeChunk(ctx context.Context, text string, delayMs int) error {
+ args := []string{"type", "--delay", strconv.Itoa(delayMs), "--", text}
+ output, err := defaultXdoTool.Run(ctx, args...)
+ if err != nil {
+ return fmt.Errorf("xdotool type failed: %s (output: %s)", err, string(output))
+ }
+ return nil
+}
+
+func (s *ApiService) typeChunkWithTypo(
+ ctx context.Context,
+ log *slog.Logger,
+ rng *rand.Rand,
+ chunkRunes []rune,
+ typoLocalPos int,
+ typo typinghumanizer.Typo,
+ delayMs int,
+) error {
+ // Type text before the typo
+ if typoLocalPos > 0 {
+ before := string(chunkRunes[:typoLocalPos])
+ if err := s.xdotoolTypeChunk(ctx, before, delayMs); err != nil {
+ return &executionError{msg: "failed during smooth typing"}
+ }
+ }
+
+ correctChar := chunkRunes[typoLocalPos]
+ var wrongText string
+ var backspaces int
+
+ switch typo.Kind {
+ case typinghumanizer.TypoAdjacentKey:
+ wrongText = string(typinghumanizer.AdjacentKey(rng, correctChar))
+ backspaces = 1
+ case typinghumanizer.TypoDoubling:
+ wrongText = string([]rune{correctChar, correctChar})
+ backspaces = 2
+ case typinghumanizer.TypoTranspose:
+ if typoLocalPos+1 < len(chunkRunes) {
+ wrongText = string([]rune{chunkRunes[typoLocalPos+1], correctChar})
+ backspaces = 2
+ } else {
+ wrongText = string(typinghumanizer.AdjacentKey(rng, correctChar))
+ backspaces = 1
+ }
+ case typinghumanizer.TypoExtraChar:
+ wrongText = string([]rune{typinghumanizer.AdjacentKey(rng, correctChar), correctChar})
+ backspaces = 2
+ }
+
+ // Type the wrong text
+ if err := s.xdotoolTypeChunk(ctx, wrongText, delayMs); err != nil {
+ return &executionError{msg: "failed during smooth typing"}
+ }
+
+ // "Oh no" realization pause
+ realizationPause := typinghumanizer.UniformJitter(rng, 350, 150, 150)
+ if err := sleepWithContext(ctx, realizationPause); err != nil {
+ return &executionError{msg: "typing cancelled"}
+ }
+
+ // Backspace to correct
+ bsArgs := make([]string, 0, backspaces*2)
+ for i := 0; i < backspaces; i++ {
+ bsArgs = append(bsArgs, "key", "BackSpace")
+ }
+ if output, err := defaultXdoTool.Run(ctx, bsArgs...); err != nil {
+ log.Error("xdotool backspace failed", "err", err, "output", string(output))
+ return &executionError{msg: "failed during typo correction"}
+ }
+
+ // Brief recovery pause
+ recoveryPause := typinghumanizer.UniformJitter(rng, 80, 30, 40)
+ if err := sleepWithContext(ctx, recoveryPause); err != nil {
+ return &executionError{msg: "typing cancelled"}
+ }
+
+ // Type the correct remainder of the chunk from the typo position onward
+ if err := s.xdotoolTypeChunk(ctx, string(chunkRunes[typoLocalPos:]), delayMs); err != nil {
+ return &executionError{msg: "failed during smooth typing"}
+ }
+
+ return nil
+}
+
func (s *ApiService) TypeText(ctx context.Context, request oapi.TypeTextRequestObject) (oapi.TypeTextResponseObject, error) {
s.inputMu.Lock()
defer s.inputMu.Unlock()
diff --git a/server/go.sum b/server/go.sum
index baa1fd2c..d13d59b4 100644
--- a/server/go.sum
+++ b/server/go.sum
@@ -127,8 +127,6 @@ github.com/morikuni/aec v1.0.0 h1:nP9CBfwrvYnBRgY6qfDQkygYDmYwOilePFkwzv4dU8A=
github.com/morikuni/aec v1.0.0/go.mod h1:BbKIizmSmc5MMPqRYbxO4ZU0S0+P200+tUnFx7PXmsc=
github.com/nrednav/cuid2 v1.1.0 h1:Y2P9Fo1Iz7lKuwcn+fS0mbxkNvEqoNLUtm0+moHCnYc=
github.com/nrednav/cuid2 v1.1.0/go.mod h1:jBjkJAI+QLM4EUGvtwGDHC1cP1QQrRNfLo/A7qJFDhA=
-github.com/oapi-codegen/runtime v1.1.2 h1:P2+CubHq8fO4Q6fV1tqDBZHCwpVpvPg7oKiYzQgXIyI=
-github.com/oapi-codegen/runtime v1.1.2/go.mod h1:SK9X900oXmPWilYR5/WKPzt3Kqxn/uS/+lbpREv+eCg=
github.com/oapi-codegen/runtime v1.2.0 h1:RvKc1CVS1QeKSNzO97FBQbSMZyQ8s6rZd+LpmzwHMP4=
github.com/oapi-codegen/runtime v1.2.0/go.mod h1:Y7ZhmmlE8ikZOmuHRRndiIm7nf3xcVv+YMweKgG1DT0=
github.com/oasdiff/yaml v0.0.0-20250309154309-f31be36b4037 h1:G7ERwszslrBzRxj//JalHPu/3yz+De2J+4aLtSRlHiY=
@@ -175,9 +173,8 @@ github.com/tklauser/go-sysconf v0.3.12 h1:0QaGUFOdQaIVdPgfITYzaTegZvdCjmYO52cSFA
github.com/tklauser/go-sysconf v0.3.12/go.mod h1:Ho14jnntGE1fpdOqQEEaiKRpvIavV0hSfmBq8nJbHYI=
github.com/tklauser/numcpus v0.6.1 h1:ng9scYS7az0Bk4OZLvrNXNSAO2Pxr1XXRAPyjhIx+Fk=
github.com/tklauser/numcpus v0.6.1/go.mod h1:1XfjsgE2zo8GVw7POkMbHENHzVg3GzmoZ9fESEdAacY=
-github.com/ugorji/go/codec v1.2.11 h1:BMaWp1Bb6fHwEtbplGBGJ498wD+LKlNSl25MjdZY4dU=
-github.com/ugorji/go/codec v1.2.11/go.mod h1:UNopzCgEMSXjBc6AOMqYvWC1ktqTAfzJZUZgYf6w6lg=
github.com/ugorji/go/codec v1.2.12 h1:9LC83zGrHhuUA9l16C9AHXAqEV/2wBQ4nkvumAE65EE=
+github.com/ugorji/go/codec v1.2.12/go.mod h1:UNopzCgEMSXjBc6AOMqYvWC1ktqTAfzJZUZgYf6w6lg=
github.com/yusufpapurcu/wmi v1.2.4 h1:zFUKzehAFReQwLys1b/iSMl+JQGSCSjtVqQn9bBrPo0=
github.com/yusufpapurcu/wmi v1.2.4/go.mod h1:SBZ9tNy3G9/m5Oi98Zks0QjeHVDvuK0qfxQmPyzfmi0=
go.opentelemetry.io/auto/sdk v1.2.1 h1:jXsnJ4Lmnqd11kwkBV2LgLoFMZKizbCi5fNZ/ipaZ64=
diff --git a/server/lib/oapi/oapi.go b/server/lib/oapi/oapi.go
index 4854c577..127d1ea5 100644
--- a/server/lib/oapi/oapi.go
+++ b/server/lib/oapi/oapi.go
@@ -1,6 +1,6 @@
// Package oapi provides primitives to interact with the openapi HTTP API.
//
-// Code generated by github.com/oapi-codegen/oapi-codegen/v2 version v2.6.0 DO NOT EDIT.
+// Code generated by github.com/oapi-codegen/oapi-codegen/v2 version v2.5.1 DO NOT EDIT.
package oapi
import (
@@ -9,7 +9,6 @@ import (
"context"
"encoding/base64"
"encoding/json"
- "errors"
"fmt"
"io"
"mime/multipart"
@@ -35,24 +34,6 @@ const (
ClickMouseRequestButtonRight ClickMouseRequestButton = "right"
)
-// Valid indicates whether the value is a known member of the ClickMouseRequestButton enum.
-func (e ClickMouseRequestButton) Valid() bool {
- switch e {
- case ClickMouseRequestButtonBack:
- return true
- case ClickMouseRequestButtonForward:
- return true
- case ClickMouseRequestButtonLeft:
- return true
- case ClickMouseRequestButtonMiddle:
- return true
- case ClickMouseRequestButtonRight:
- return true
- default:
- return false
- }
-}
-
// Defines values for ClickMouseRequestClickType.
const (
Click ClickMouseRequestClickType = "click"
@@ -60,20 +41,6 @@ const (
Up ClickMouseRequestClickType = "up"
)
-// Valid indicates whether the value is a known member of the ClickMouseRequestClickType enum.
-func (e ClickMouseRequestClickType) Valid() bool {
- switch e {
- case Click:
- return true
- case Down:
- return true
- case Up:
- return true
- default:
- return false
- }
-}
-
// Defines values for ComputerActionType.
const (
ClickMouse ComputerActionType = "click_mouse"
@@ -86,30 +53,6 @@ const (
TypeText ComputerActionType = "type_text"
)
-// Valid indicates whether the value is a known member of the ComputerActionType enum.
-func (e ComputerActionType) Valid() bool {
- switch e {
- case ClickMouse:
- return true
- case DragMouse:
- return true
- case MoveMouse:
- return true
- case PressKey:
- return true
- case Scroll:
- return true
- case SetCursor:
- return true
- case Sleep:
- return true
- case TypeText:
- return true
- default:
- return false
- }
-}
-
// Defines values for DragMouseRequestButton.
const (
DragMouseRequestButtonLeft DragMouseRequestButton = "left"
@@ -117,20 +60,6 @@ const (
DragMouseRequestButtonRight DragMouseRequestButton = "right"
)
-// Valid indicates whether the value is a known member of the DragMouseRequestButton enum.
-func (e DragMouseRequestButton) Valid() bool {
- switch e {
- case DragMouseRequestButtonLeft:
- return true
- case DragMouseRequestButtonMiddle:
- return true
- case DragMouseRequestButtonRight:
- return true
- default:
- return false
- }
-}
-
// Defines values for FileSystemEventType.
const (
CREATE FileSystemEventType = "CREATE"
@@ -139,22 +68,6 @@ const (
WRITE FileSystemEventType = "WRITE"
)
-// Valid indicates whether the value is a known member of the FileSystemEventType enum.
-func (e FileSystemEventType) Valid() bool {
- switch e {
- case CREATE:
- return true
- case DELETE:
- return true
- case RENAME:
- return true
- case WRITE:
- return true
- default:
- return false
- }
-}
-
// Defines values for PatchDisplayRequestRefreshRate.
const (
N10 PatchDisplayRequestRefreshRate = 10
@@ -163,22 +76,6 @@ const (
N60 PatchDisplayRequestRefreshRate = 60
)
-// Valid indicates whether the value is a known member of the PatchDisplayRequestRefreshRate enum.
-func (e PatchDisplayRequestRefreshRate) Valid() bool {
- switch e {
- case N10:
- return true
- case N25:
- return true
- case N30:
- return true
- case N60:
- return true
- default:
- return false
- }
-}
-
// Defines values for ProcessKillRequestSignal.
const (
HUP ProcessKillRequestSignal = "HUP"
@@ -187,73 +84,23 @@ const (
TERM ProcessKillRequestSignal = "TERM"
)
-// Valid indicates whether the value is a known member of the ProcessKillRequestSignal enum.
-func (e ProcessKillRequestSignal) Valid() bool {
- switch e {
- case HUP:
- return true
- case INT:
- return true
- case KILL:
- return true
- case TERM:
- return true
- default:
- return false
- }
-}
-
// Defines values for ProcessStatusState.
const (
Exited ProcessStatusState = "exited"
Running ProcessStatusState = "running"
)
-// Valid indicates whether the value is a known member of the ProcessStatusState enum.
-func (e ProcessStatusState) Valid() bool {
- switch e {
- case Exited:
- return true
- case Running:
- return true
- default:
- return false
- }
-}
-
// Defines values for ProcessStreamEventEvent.
const (
Exit ProcessStreamEventEvent = "exit"
)
-// Valid indicates whether the value is a known member of the ProcessStreamEventEvent enum.
-func (e ProcessStreamEventEvent) Valid() bool {
- switch e {
- case Exit:
- return true
- default:
- return false
- }
-}
-
// Defines values for ProcessStreamEventStream.
const (
Stderr ProcessStreamEventStream = "stderr"
Stdout ProcessStreamEventStream = "stdout"
)
-// Valid indicates whether the value is a known member of the ProcessStreamEventStream enum.
-func (e ProcessStreamEventStream) Valid() bool {
- switch e {
- case Stderr:
- return true
- case Stdout:
- return true
- default:
- return false
- }
-}
-
// Defines values for DownloadDirZstdParamsCompressionLevel.
const (
Best DownloadDirZstdParamsCompressionLevel = "best"
@@ -262,40 +109,12 @@ const (
Fastest DownloadDirZstdParamsCompressionLevel = "fastest"
)
-// Valid indicates whether the value is a known member of the DownloadDirZstdParamsCompressionLevel enum.
-func (e DownloadDirZstdParamsCompressionLevel) Valid() bool {
- switch e {
- case Best:
- return true
- case Better:
- return true
- case Default:
- return true
- case Fastest:
- return true
- default:
- return false
- }
-}
-
// Defines values for LogsStreamParamsSource.
const (
Path LogsStreamParamsSource = "path"
Supervisor LogsStreamParamsSource = "supervisor"
)
-// Valid indicates whether the value is a known member of the LogsStreamParamsSource enum.
-func (e LogsStreamParamsSource) Valid() bool {
- switch e {
- case Path:
- return true
- case Supervisor:
- return true
- default:
- return false
- }
-}
-
// BatchComputerActionRequest A batch of computer actions to execute sequentially.
type BatchComputerActionRequest struct {
// Actions Ordered list of actions to execute. Execution stops on the first error.
@@ -590,19 +409,19 @@ type ProcessExecRequest struct {
AsRoot *bool `json:"as_root,omitempty"`
// AsUser Run the process as this user.
- AsUser *string `json:"as_user,omitempty"`
+ AsUser *string `json:"as_user"`
// Command Executable or shell command to run.
Command string `json:"command"`
// Cwd Working directory (absolute path) to run the command in.
- Cwd *string `json:"cwd,omitempty"`
+ Cwd *string `json:"cwd"`
// Env Environment variables to set for the process.
Env *map[string]string `json:"env,omitempty"`
// TimeoutSec Maximum execution time in seconds.
- TimeoutSec *int `json:"timeout_sec,omitempty"`
+ TimeoutSec *int `json:"timeout_sec"`
}
// ProcessExecResult Result of a synchronous command execution.
@@ -650,7 +469,7 @@ type ProcessSpawnRequest struct {
AsRoot *bool `json:"as_root,omitempty"`
// AsUser Run the process as this user.
- AsUser *string `json:"as_user,omitempty"`
+ AsUser *string `json:"as_user"`
// Cols Initial terminal columns when allocate_tty is true.
Cols *int `json:"cols,omitempty"`
@@ -659,7 +478,7 @@ type ProcessSpawnRequest struct {
Command string `json:"command"`
// Cwd Working directory (absolute path) to run the command in.
- Cwd *string `json:"cwd,omitempty"`
+ Cwd *string `json:"cwd"`
// Env Environment variables to set for the process.
Env *map[string]string `json:"env,omitempty"`
@@ -668,7 +487,7 @@ type ProcessSpawnRequest struct {
Rows *int `json:"rows,omitempty"`
// TimeoutSec Maximum execution time in seconds.
- TimeoutSec *int `json:"timeout_sec,omitempty"`
+ TimeoutSec *int `json:"timeout_sec"`
}
// ProcessSpawnResult Information about a spawned process.
@@ -689,7 +508,7 @@ type ProcessStatus struct {
CpuPct *float32 `json:"cpu_pct,omitempty"`
// ExitCode Exit code if the process has exited.
- ExitCode *int `json:"exit_code,omitempty"`
+ ExitCode *int `json:"exit_code"`
// MemBytes Estimated resident memory usage in bytes.
MemBytes *int `json:"mem_bytes,omitempty"`
@@ -737,12 +556,12 @@ type ProcessStreamEventStream string
// RecorderInfo defines model for RecorderInfo.
type RecorderInfo struct {
// FinishedAt Timestamp when recording finished
- FinishedAt *time.Time `json:"finished_at,omitempty"`
+ FinishedAt *time.Time `json:"finished_at"`
Id string `json:"id"`
IsRecording bool `json:"isRecording"`
// StartedAt Timestamp when recording started
- StartedAt *time.Time `json:"started_at,omitempty"`
+ StartedAt *time.Time `json:"started_at"`
}
// ScreenshotRegion defines model for ScreenshotRegion.
@@ -845,11 +664,22 @@ type StopRecordingRequest struct {
// TypeTextRequest defines model for TypeTextRequest.
type TypeTextRequest struct {
- // Delay Delay in milliseconds between keystrokes
+ // Delay Delay in milliseconds between keystrokes. Ignored when smooth is true.
Delay *int `json:"delay,omitempty"`
+ // Smooth Use human-like variable keystroke timing instead of a fixed delay.
+ // When enabled, text is typed in word-sized chunks with variable
+ // intra-word delays and natural inter-word pauses. The delay field
+ // is ignored when smooth is true.
+ Smooth *bool `json:"smooth,omitempty"`
+
// Text Text to type on the host computer
Text string `json:"text"`
+
+ // TypoChance Probability (0.0-0.10) of a typo per character, which is then
+ // corrected with backspace. Requires smooth to be true. Set to 0
+ // to disable. Typical human range is 0.02-0.05.
+ TypoChance *float32 `json:"typo_chance,omitempty"`
}
// WriteClipboardRequest defines model for WriteClipboardRequest.
@@ -3001,7 +2831,7 @@ func NewDownloadDirZipRequest(server string, params *DownloadDirZipParams) (*htt
if params != nil {
queryValues := queryURL.Query()
- if queryFrag, err := runtime.StyleParamWithOptions("form", true, "path", params.Path, runtime.StyleParamOptions{ParamLocation: runtime.ParamLocationQuery, Type: "string", Format: ""}); err != nil {
+ if queryFrag, err := runtime.StyleParamWithLocation("form", true, "path", runtime.ParamLocationQuery, params.Path); err != nil {
return nil, err
} else if parsed, err := url.ParseQuery(queryFrag); err != nil {
return nil, err
@@ -3046,7 +2876,7 @@ func NewDownloadDirZstdRequest(server string, params *DownloadDirZstdParams) (*h
if params != nil {
queryValues := queryURL.Query()
- if queryFrag, err := runtime.StyleParamWithOptions("form", true, "path", params.Path, runtime.StyleParamOptions{ParamLocation: runtime.ParamLocationQuery, Type: "string", Format: ""}); err != nil {
+ if queryFrag, err := runtime.StyleParamWithLocation("form", true, "path", runtime.ParamLocationQuery, params.Path); err != nil {
return nil, err
} else if parsed, err := url.ParseQuery(queryFrag); err != nil {
return nil, err
@@ -3060,7 +2890,7 @@ func NewDownloadDirZstdRequest(server string, params *DownloadDirZstdParams) (*h
if params.CompressionLevel != nil {
- if queryFrag, err := runtime.StyleParamWithOptions("form", true, "compression_level", *params.CompressionLevel, runtime.StyleParamOptions{ParamLocation: runtime.ParamLocationQuery, Type: "string", Format: ""}); err != nil {
+ if queryFrag, err := runtime.StyleParamWithLocation("form", true, "compression_level", runtime.ParamLocationQuery, *params.CompressionLevel); err != nil {
return nil, err
} else if parsed, err := url.ParseQuery(queryFrag); err != nil {
return nil, err
@@ -3107,7 +2937,7 @@ func NewFileInfoRequest(server string, params *FileInfoParams) (*http.Request, e
if params != nil {
queryValues := queryURL.Query()
- if queryFrag, err := runtime.StyleParamWithOptions("form", true, "path", params.Path, runtime.StyleParamOptions{ParamLocation: runtime.ParamLocationQuery, Type: "string", Format: ""}); err != nil {
+ if queryFrag, err := runtime.StyleParamWithLocation("form", true, "path", runtime.ParamLocationQuery, params.Path); err != nil {
return nil, err
} else if parsed, err := url.ParseQuery(queryFrag); err != nil {
return nil, err
@@ -3152,7 +2982,7 @@ func NewListFilesRequest(server string, params *ListFilesParams) (*http.Request,
if params != nil {
queryValues := queryURL.Query()
- if queryFrag, err := runtime.StyleParamWithOptions("form", true, "path", params.Path, runtime.StyleParamOptions{ParamLocation: runtime.ParamLocationQuery, Type: "string", Format: ""}); err != nil {
+ if queryFrag, err := runtime.StyleParamWithLocation("form", true, "path", runtime.ParamLocationQuery, params.Path); err != nil {
return nil, err
} else if parsed, err := url.ParseQuery(queryFrag); err != nil {
return nil, err
@@ -3237,7 +3067,7 @@ func NewReadFileRequest(server string, params *ReadFileParams) (*http.Request, e
if params != nil {
queryValues := queryURL.Query()
- if queryFrag, err := runtime.StyleParamWithOptions("form", true, "path", params.Path, runtime.StyleParamOptions{ParamLocation: runtime.ParamLocationQuery, Type: "string", Format: ""}); err != nil {
+ if queryFrag, err := runtime.StyleParamWithLocation("form", true, "path", runtime.ParamLocationQuery, params.Path); err != nil {
return nil, err
} else if parsed, err := url.ParseQuery(queryFrag); err != nil {
return nil, err
@@ -3433,7 +3263,7 @@ func NewStopFsWatchRequest(server string, watchId string) (*http.Request, error)
var pathParam0 string
- pathParam0, err = runtime.StyleParamWithOptions("simple", false, "watch_id", watchId, runtime.StyleParamOptions{ParamLocation: runtime.ParamLocationPath, Type: "string", Format: ""})
+ pathParam0, err = runtime.StyleParamWithLocation("simple", false, "watch_id", runtime.ParamLocationPath, watchId)
if err != nil {
return nil, err
}
@@ -3467,7 +3297,7 @@ func NewStreamFsEventsRequest(server string, watchId string) (*http.Request, err
var pathParam0 string
- pathParam0, err = runtime.StyleParamWithOptions("simple", false, "watch_id", watchId, runtime.StyleParamOptions{ParamLocation: runtime.ParamLocationPath, Type: "string", Format: ""})
+ pathParam0, err = runtime.StyleParamWithLocation("simple", false, "watch_id", runtime.ParamLocationPath, watchId)
if err != nil {
return nil, err
}
@@ -3517,7 +3347,7 @@ func NewWriteFileRequestWithBody(server string, params *WriteFileParams, content
if params != nil {
queryValues := queryURL.Query()
- if queryFrag, err := runtime.StyleParamWithOptions("form", true, "path", params.Path, runtime.StyleParamOptions{ParamLocation: runtime.ParamLocationQuery, Type: "string", Format: ""}); err != nil {
+ if queryFrag, err := runtime.StyleParamWithLocation("form", true, "path", runtime.ParamLocationQuery, params.Path); err != nil {
return nil, err
} else if parsed, err := url.ParseQuery(queryFrag); err != nil {
return nil, err
@@ -3531,7 +3361,7 @@ func NewWriteFileRequestWithBody(server string, params *WriteFileParams, content
if params.Mode != nil {
- if queryFrag, err := runtime.StyleParamWithOptions("form", true, "mode", *params.Mode, runtime.StyleParamOptions{ParamLocation: runtime.ParamLocationQuery, Type: "string", Format: ""}); err != nil {
+ if queryFrag, err := runtime.StyleParamWithLocation("form", true, "mode", runtime.ParamLocationQuery, *params.Mode); err != nil {
return nil, err
} else if parsed, err := url.ParseQuery(queryFrag); err != nil {
return nil, err
@@ -3580,7 +3410,7 @@ func NewLogsStreamRequest(server string, params *LogsStreamParams) (*http.Reques
if params != nil {
queryValues := queryURL.Query()
- if queryFrag, err := runtime.StyleParamWithOptions("form", true, "source", params.Source, runtime.StyleParamOptions{ParamLocation: runtime.ParamLocationQuery, Type: "string", Format: ""}); err != nil {
+ if queryFrag, err := runtime.StyleParamWithLocation("form", true, "source", runtime.ParamLocationQuery, params.Source); err != nil {
return nil, err
} else if parsed, err := url.ParseQuery(queryFrag); err != nil {
return nil, err
@@ -3594,7 +3424,7 @@ func NewLogsStreamRequest(server string, params *LogsStreamParams) (*http.Reques
if params.Follow != nil {
- if queryFrag, err := runtime.StyleParamWithOptions("form", true, "follow", *params.Follow, runtime.StyleParamOptions{ParamLocation: runtime.ParamLocationQuery, Type: "boolean", Format: ""}); err != nil {
+ if queryFrag, err := runtime.StyleParamWithLocation("form", true, "follow", runtime.ParamLocationQuery, *params.Follow); err != nil {
return nil, err
} else if parsed, err := url.ParseQuery(queryFrag); err != nil {
return nil, err
@@ -3610,7 +3440,7 @@ func NewLogsStreamRequest(server string, params *LogsStreamParams) (*http.Reques
if params.Path != nil {
- if queryFrag, err := runtime.StyleParamWithOptions("form", true, "path", *params.Path, runtime.StyleParamOptions{ParamLocation: runtime.ParamLocationQuery, Type: "string", Format: ""}); err != nil {
+ if queryFrag, err := runtime.StyleParamWithLocation("form", true, "path", runtime.ParamLocationQuery, *params.Path); err != nil {
return nil, err
} else if parsed, err := url.ParseQuery(queryFrag); err != nil {
return nil, err
@@ -3626,7 +3456,7 @@ func NewLogsStreamRequest(server string, params *LogsStreamParams) (*http.Reques
if params.SupervisorProcess != nil {
- if queryFrag, err := runtime.StyleParamWithOptions("form", true, "supervisor_process", *params.SupervisorProcess, runtime.StyleParamOptions{ParamLocation: runtime.ParamLocationQuery, Type: "string", Format: ""}); err != nil {
+ if queryFrag, err := runtime.StyleParamWithLocation("form", true, "supervisor_process", runtime.ParamLocationQuery, *params.SupervisorProcess); err != nil {
return nil, err
} else if parsed, err := url.ParseQuery(queryFrag); err != nil {
return nil, err
@@ -3788,7 +3618,7 @@ func NewProcessKillRequestWithBody(server string, processId openapi_types.UUID,
var pathParam0 string
- pathParam0, err = runtime.StyleParamWithOptions("simple", false, "process_id", processId, runtime.StyleParamOptions{ParamLocation: runtime.ParamLocationPath, Type: "string", Format: "uuid"})
+ pathParam0, err = runtime.StyleParamWithLocation("simple", false, "process_id", runtime.ParamLocationPath, processId)
if err != nil {
return nil, err
}
@@ -3835,7 +3665,7 @@ func NewProcessResizeRequestWithBody(server string, processId openapi_types.UUID
var pathParam0 string
- pathParam0, err = runtime.StyleParamWithOptions("simple", false, "process_id", processId, runtime.StyleParamOptions{ParamLocation: runtime.ParamLocationPath, Type: "string", Format: "uuid"})
+ pathParam0, err = runtime.StyleParamWithLocation("simple", false, "process_id", runtime.ParamLocationPath, processId)
if err != nil {
return nil, err
}
@@ -3871,7 +3701,7 @@ func NewProcessStatusRequest(server string, processId openapi_types.UUID) (*http
var pathParam0 string
- pathParam0, err = runtime.StyleParamWithOptions("simple", false, "process_id", processId, runtime.StyleParamOptions{ParamLocation: runtime.ParamLocationPath, Type: "string", Format: "uuid"})
+ pathParam0, err = runtime.StyleParamWithLocation("simple", false, "process_id", runtime.ParamLocationPath, processId)
if err != nil {
return nil, err
}
@@ -3916,7 +3746,7 @@ func NewProcessStdinRequestWithBody(server string, processId openapi_types.UUID,
var pathParam0 string
- pathParam0, err = runtime.StyleParamWithOptions("simple", false, "process_id", processId, runtime.StyleParamOptions{ParamLocation: runtime.ParamLocationPath, Type: "string", Format: "uuid"})
+ pathParam0, err = runtime.StyleParamWithLocation("simple", false, "process_id", runtime.ParamLocationPath, processId)
if err != nil {
return nil, err
}
@@ -3952,7 +3782,7 @@ func NewProcessStdoutStreamRequest(server string, processId openapi_types.UUID)
var pathParam0 string
- pathParam0, err = runtime.StyleParamWithOptions("simple", false, "process_id", processId, runtime.StyleParamOptions{ParamLocation: runtime.ParamLocationPath, Type: "string", Format: "uuid"})
+ pathParam0, err = runtime.StyleParamWithLocation("simple", false, "process_id", runtime.ParamLocationPath, processId)
if err != nil {
return nil, err
}
@@ -4044,7 +3874,7 @@ func NewDownloadRecordingRequest(server string, params *DownloadRecordingParams)
if params.Id != nil {
- if queryFrag, err := runtime.StyleParamWithOptions("form", true, "id", *params.Id, runtime.StyleParamOptions{ParamLocation: runtime.ParamLocationQuery, Type: "string", Format: ""}); err != nil {
+ if queryFrag, err := runtime.StyleParamWithLocation("form", true, "id", runtime.ParamLocationQuery, *params.Id); err != nil {
return nil, err
} else if parsed, err := url.ParseQuery(queryFrag); err != nil {
return nil, err
@@ -8710,7 +8540,7 @@ func (siw *ServerInterfaceWrapper) DownloadDirZip(w http.ResponseWriter, r *http
return
}
- err = runtime.BindQueryParameterWithOptions("form", true, true, "path", r.URL.Query(), ¶ms.Path, runtime.BindQueryParameterOptions{Type: "string", Format: ""})
+ err = runtime.BindQueryParameter("form", true, true, "path", r.URL.Query(), ¶ms.Path)
if err != nil {
siw.ErrorHandlerFunc(w, r, &InvalidParamFormatError{ParamName: "path", Err: err})
return
@@ -8744,7 +8574,7 @@ func (siw *ServerInterfaceWrapper) DownloadDirZstd(w http.ResponseWriter, r *htt
return
}
- err = runtime.BindQueryParameterWithOptions("form", true, true, "path", r.URL.Query(), ¶ms.Path, runtime.BindQueryParameterOptions{Type: "string", Format: ""})
+ err = runtime.BindQueryParameter("form", true, true, "path", r.URL.Query(), ¶ms.Path)
if err != nil {
siw.ErrorHandlerFunc(w, r, &InvalidParamFormatError{ParamName: "path", Err: err})
return
@@ -8752,7 +8582,7 @@ func (siw *ServerInterfaceWrapper) DownloadDirZstd(w http.ResponseWriter, r *htt
// ------------- Optional query parameter "compression_level" -------------
- err = runtime.BindQueryParameterWithOptions("form", true, false, "compression_level", r.URL.Query(), ¶ms.CompressionLevel, runtime.BindQueryParameterOptions{Type: "string", Format: ""})
+ err = runtime.BindQueryParameter("form", true, false, "compression_level", r.URL.Query(), ¶ms.CompressionLevel)
if err != nil {
siw.ErrorHandlerFunc(w, r, &InvalidParamFormatError{ParamName: "compression_level", Err: err})
return
@@ -8786,7 +8616,7 @@ func (siw *ServerInterfaceWrapper) FileInfo(w http.ResponseWriter, r *http.Reque
return
}
- err = runtime.BindQueryParameterWithOptions("form", true, true, "path", r.URL.Query(), ¶ms.Path, runtime.BindQueryParameterOptions{Type: "string", Format: ""})
+ err = runtime.BindQueryParameter("form", true, true, "path", r.URL.Query(), ¶ms.Path)
if err != nil {
siw.ErrorHandlerFunc(w, r, &InvalidParamFormatError{ParamName: "path", Err: err})
return
@@ -8820,7 +8650,7 @@ func (siw *ServerInterfaceWrapper) ListFiles(w http.ResponseWriter, r *http.Requ
return
}
- err = runtime.BindQueryParameterWithOptions("form", true, true, "path", r.URL.Query(), ¶ms.Path, runtime.BindQueryParameterOptions{Type: "string", Format: ""})
+ err = runtime.BindQueryParameter("form", true, true, "path", r.URL.Query(), ¶ms.Path)
if err != nil {
siw.ErrorHandlerFunc(w, r, &InvalidParamFormatError{ParamName: "path", Err: err})
return
@@ -8868,7 +8698,7 @@ func (siw *ServerInterfaceWrapper) ReadFile(w http.ResponseWriter, r *http.Reque
return
}
- err = runtime.BindQueryParameterWithOptions("form", true, true, "path", r.URL.Query(), ¶ms.Path, runtime.BindQueryParameterOptions{Type: "string", Format: ""})
+ err = runtime.BindQueryParameter("form", true, true, "path", r.URL.Query(), ¶ms.Path)
if err != nil {
siw.ErrorHandlerFunc(w, r, &InvalidParamFormatError{ParamName: "path", Err: err})
return
@@ -8963,7 +8793,7 @@ func (siw *ServerInterfaceWrapper) StopFsWatch(w http.ResponseWriter, r *http.Re
// ------------- Path parameter "watch_id" -------------
var watchId string
- err = runtime.BindStyledParameterWithOptions("simple", "watch_id", chi.URLParam(r, "watch_id"), &watchId, runtime.BindStyledParameterOptions{ParamLocation: runtime.ParamLocationPath, Explode: false, Required: true, Type: "string", Format: ""})
+ err = runtime.BindStyledParameterWithOptions("simple", "watch_id", chi.URLParam(r, "watch_id"), &watchId, runtime.BindStyledParameterOptions{ParamLocation: runtime.ParamLocationPath, Explode: false, Required: true})
if err != nil {
siw.ErrorHandlerFunc(w, r, &InvalidParamFormatError{ParamName: "watch_id", Err: err})
return
@@ -8988,7 +8818,7 @@ func (siw *ServerInterfaceWrapper) StreamFsEvents(w http.ResponseWriter, r *http
// ------------- Path parameter "watch_id" -------------
var watchId string
- err = runtime.BindStyledParameterWithOptions("simple", "watch_id", chi.URLParam(r, "watch_id"), &watchId, runtime.BindStyledParameterOptions{ParamLocation: runtime.ParamLocationPath, Explode: false, Required: true, Type: "string", Format: ""})
+ err = runtime.BindStyledParameterWithOptions("simple", "watch_id", chi.URLParam(r, "watch_id"), &watchId, runtime.BindStyledParameterOptions{ParamLocation: runtime.ParamLocationPath, Explode: false, Required: true})
if err != nil {
siw.ErrorHandlerFunc(w, r, &InvalidParamFormatError{ParamName: "watch_id", Err: err})
return
@@ -9022,7 +8852,7 @@ func (siw *ServerInterfaceWrapper) WriteFile(w http.ResponseWriter, r *http.Requ
return
}
- err = runtime.BindQueryParameterWithOptions("form", true, true, "path", r.URL.Query(), ¶ms.Path, runtime.BindQueryParameterOptions{Type: "string", Format: ""})
+ err = runtime.BindQueryParameter("form", true, true, "path", r.URL.Query(), ¶ms.Path)
if err != nil {
siw.ErrorHandlerFunc(w, r, &InvalidParamFormatError{ParamName: "path", Err: err})
return
@@ -9030,7 +8860,7 @@ func (siw *ServerInterfaceWrapper) WriteFile(w http.ResponseWriter, r *http.Requ
// ------------- Optional query parameter "mode" -------------
- err = runtime.BindQueryParameterWithOptions("form", true, false, "mode", r.URL.Query(), ¶ms.Mode, runtime.BindQueryParameterOptions{Type: "string", Format: ""})
+ err = runtime.BindQueryParameter("form", true, false, "mode", r.URL.Query(), ¶ms.Mode)
if err != nil {
siw.ErrorHandlerFunc(w, r, &InvalidParamFormatError{ParamName: "mode", Err: err})
return
@@ -9064,7 +8894,7 @@ func (siw *ServerInterfaceWrapper) LogsStream(w http.ResponseWriter, r *http.Req
return
}
- err = runtime.BindQueryParameterWithOptions("form", true, true, "source", r.URL.Query(), ¶ms.Source, runtime.BindQueryParameterOptions{Type: "string", Format: ""})
+ err = runtime.BindQueryParameter("form", true, true, "source", r.URL.Query(), ¶ms.Source)
if err != nil {
siw.ErrorHandlerFunc(w, r, &InvalidParamFormatError{ParamName: "source", Err: err})
return
@@ -9072,7 +8902,7 @@ func (siw *ServerInterfaceWrapper) LogsStream(w http.ResponseWriter, r *http.Req
// ------------- Optional query parameter "follow" -------------
- err = runtime.BindQueryParameterWithOptions("form", true, false, "follow", r.URL.Query(), ¶ms.Follow, runtime.BindQueryParameterOptions{Type: "boolean", Format: ""})
+ err = runtime.BindQueryParameter("form", true, false, "follow", r.URL.Query(), ¶ms.Follow)
if err != nil {
siw.ErrorHandlerFunc(w, r, &InvalidParamFormatError{ParamName: "follow", Err: err})
return
@@ -9080,7 +8910,7 @@ func (siw *ServerInterfaceWrapper) LogsStream(w http.ResponseWriter, r *http.Req
// ------------- Optional query parameter "path" -------------
- err = runtime.BindQueryParameterWithOptions("form", true, false, "path", r.URL.Query(), ¶ms.Path, runtime.BindQueryParameterOptions{Type: "string", Format: ""})
+ err = runtime.BindQueryParameter("form", true, false, "path", r.URL.Query(), ¶ms.Path)
if err != nil {
siw.ErrorHandlerFunc(w, r, &InvalidParamFormatError{ParamName: "path", Err: err})
return
@@ -9088,7 +8918,7 @@ func (siw *ServerInterfaceWrapper) LogsStream(w http.ResponseWriter, r *http.Req
// ------------- Optional query parameter "supervisor_process" -------------
- err = runtime.BindQueryParameterWithOptions("form", true, false, "supervisor_process", r.URL.Query(), ¶ms.SupervisorProcess, runtime.BindQueryParameterOptions{Type: "string", Format: ""})
+ err = runtime.BindQueryParameter("form", true, false, "supervisor_process", r.URL.Query(), ¶ms.SupervisorProcess)
if err != nil {
siw.ErrorHandlerFunc(w, r, &InvalidParamFormatError{ParamName: "supervisor_process", Err: err})
return
@@ -9155,7 +8985,7 @@ func (siw *ServerInterfaceWrapper) ProcessKill(w http.ResponseWriter, r *http.Re
// ------------- Path parameter "process_id" -------------
var processId openapi_types.UUID
- err = runtime.BindStyledParameterWithOptions("simple", "process_id", chi.URLParam(r, "process_id"), &processId, runtime.BindStyledParameterOptions{ParamLocation: runtime.ParamLocationPath, Explode: false, Required: true, Type: "string", Format: "uuid"})
+ err = runtime.BindStyledParameterWithOptions("simple", "process_id", chi.URLParam(r, "process_id"), &processId, runtime.BindStyledParameterOptions{ParamLocation: runtime.ParamLocationPath, Explode: false, Required: true})
if err != nil {
siw.ErrorHandlerFunc(w, r, &InvalidParamFormatError{ParamName: "process_id", Err: err})
return
@@ -9180,7 +9010,7 @@ func (siw *ServerInterfaceWrapper) ProcessResize(w http.ResponseWriter, r *http.
// ------------- Path parameter "process_id" -------------
var processId openapi_types.UUID
- err = runtime.BindStyledParameterWithOptions("simple", "process_id", chi.URLParam(r, "process_id"), &processId, runtime.BindStyledParameterOptions{ParamLocation: runtime.ParamLocationPath, Explode: false, Required: true, Type: "string", Format: "uuid"})
+ err = runtime.BindStyledParameterWithOptions("simple", "process_id", chi.URLParam(r, "process_id"), &processId, runtime.BindStyledParameterOptions{ParamLocation: runtime.ParamLocationPath, Explode: false, Required: true})
if err != nil {
siw.ErrorHandlerFunc(w, r, &InvalidParamFormatError{ParamName: "process_id", Err: err})
return
@@ -9205,7 +9035,7 @@ func (siw *ServerInterfaceWrapper) ProcessStatus(w http.ResponseWriter, r *http.
// ------------- Path parameter "process_id" -------------
var processId openapi_types.UUID
- err = runtime.BindStyledParameterWithOptions("simple", "process_id", chi.URLParam(r, "process_id"), &processId, runtime.BindStyledParameterOptions{ParamLocation: runtime.ParamLocationPath, Explode: false, Required: true, Type: "string", Format: "uuid"})
+ err = runtime.BindStyledParameterWithOptions("simple", "process_id", chi.URLParam(r, "process_id"), &processId, runtime.BindStyledParameterOptions{ParamLocation: runtime.ParamLocationPath, Explode: false, Required: true})
if err != nil {
siw.ErrorHandlerFunc(w, r, &InvalidParamFormatError{ParamName: "process_id", Err: err})
return
@@ -9230,7 +9060,7 @@ func (siw *ServerInterfaceWrapper) ProcessStdin(w http.ResponseWriter, r *http.R
// ------------- Path parameter "process_id" -------------
var processId openapi_types.UUID
- err = runtime.BindStyledParameterWithOptions("simple", "process_id", chi.URLParam(r, "process_id"), &processId, runtime.BindStyledParameterOptions{ParamLocation: runtime.ParamLocationPath, Explode: false, Required: true, Type: "string", Format: "uuid"})
+ err = runtime.BindStyledParameterWithOptions("simple", "process_id", chi.URLParam(r, "process_id"), &processId, runtime.BindStyledParameterOptions{ParamLocation: runtime.ParamLocationPath, Explode: false, Required: true})
if err != nil {
siw.ErrorHandlerFunc(w, r, &InvalidParamFormatError{ParamName: "process_id", Err: err})
return
@@ -9255,7 +9085,7 @@ func (siw *ServerInterfaceWrapper) ProcessStdoutStream(w http.ResponseWriter, r
// ------------- Path parameter "process_id" -------------
var processId openapi_types.UUID
- err = runtime.BindStyledParameterWithOptions("simple", "process_id", chi.URLParam(r, "process_id"), &processId, runtime.BindStyledParameterOptions{ParamLocation: runtime.ParamLocationPath, Explode: false, Required: true, Type: "string", Format: "uuid"})
+ err = runtime.BindStyledParameterWithOptions("simple", "process_id", chi.URLParam(r, "process_id"), &processId, runtime.BindStyledParameterOptions{ParamLocation: runtime.ParamLocationPath, Explode: false, Required: true})
if err != nil {
siw.ErrorHandlerFunc(w, r, &InvalidParamFormatError{ParamName: "process_id", Err: err})
return
@@ -9296,7 +9126,7 @@ func (siw *ServerInterfaceWrapper) DownloadRecording(w http.ResponseWriter, r *h
// ------------- Optional query parameter "id" -------------
- err = runtime.BindQueryParameterWithOptions("form", true, false, "id", r.URL.Query(), ¶ms.Id, runtime.BindQueryParameterOptions{Type: "string", Format: ""})
+ err = runtime.BindQueryParameter("form", true, false, "id", r.URL.Query(), ¶ms.Id)
if err != nil {
siw.ErrorHandlerFunc(w, r, &InvalidParamFormatError{ParamName: "id", Err: err})
return
@@ -12159,13 +11989,10 @@ func (sh *strictHandler) TakeScreenshot(w http.ResponseWriter, r *http.Request)
var body TakeScreenshotJSONRequestBody
if err := json.NewDecoder(r.Body).Decode(&body); err != nil {
- if !errors.Is(err, io.EOF) {
- sh.options.RequestErrorHandlerFunc(w, r, fmt.Errorf("can't decode JSON body: %w", err))
- return
- }
- } else {
- request.Body = &body
+ sh.options.RequestErrorHandlerFunc(w, r, fmt.Errorf("can't decode JSON body: %w", err))
+ return
}
+ request.Body = &body
handler := func(ctx context.Context, w http.ResponseWriter, r *http.Request, request interface{}) (interface{}, error) {
return sh.ssi.TakeScreenshot(ctx, request.(TakeScreenshotRequestObject))
@@ -13045,13 +12872,10 @@ func (sh *strictHandler) DeleteRecording(w http.ResponseWriter, r *http.Request)
var body DeleteRecordingJSONRequestBody
if err := json.NewDecoder(r.Body).Decode(&body); err != nil {
- if !errors.Is(err, io.EOF) {
- sh.options.RequestErrorHandlerFunc(w, r, fmt.Errorf("can't decode JSON body: %w", err))
- return
- }
- } else {
- request.Body = &body
+ sh.options.RequestErrorHandlerFunc(w, r, fmt.Errorf("can't decode JSON body: %w", err))
+ return
}
+ request.Body = &body
handler := func(ctx context.Context, w http.ResponseWriter, r *http.Request, request interface{}) (interface{}, error) {
return sh.ssi.DeleteRecording(ctx, request.(DeleteRecordingRequestObject))
@@ -13129,13 +12953,10 @@ func (sh *strictHandler) StartRecording(w http.ResponseWriter, r *http.Request)
var body StartRecordingJSONRequestBody
if err := json.NewDecoder(r.Body).Decode(&body); err != nil {
- if !errors.Is(err, io.EOF) {
- sh.options.RequestErrorHandlerFunc(w, r, fmt.Errorf("can't decode JSON body: %w", err))
- return
- }
- } else {
- request.Body = &body
+ sh.options.RequestErrorHandlerFunc(w, r, fmt.Errorf("can't decode JSON body: %w", err))
+ return
}
+ request.Body = &body
handler := func(ctx context.Context, w http.ResponseWriter, r *http.Request, request interface{}) (interface{}, error) {
return sh.ssi.StartRecording(ctx, request.(StartRecordingRequestObject))
@@ -13163,13 +12984,10 @@ func (sh *strictHandler) StopRecording(w http.ResponseWriter, r *http.Request) {
var body StopRecordingJSONRequestBody
if err := json.NewDecoder(r.Body).Decode(&body); err != nil {
- if !errors.Is(err, io.EOF) {
- sh.options.RequestErrorHandlerFunc(w, r, fmt.Errorf("can't decode JSON body: %w", err))
- return
- }
- } else {
- request.Body = &body
+ sh.options.RequestErrorHandlerFunc(w, r, fmt.Errorf("can't decode JSON body: %w", err))
+ return
}
+ request.Body = &body
handler := func(ctx context.Context, w http.ResponseWriter, r *http.Request, request interface{}) (interface{}, error) {
return sh.ssi.StopRecording(ctx, request.(StopRecordingRequestObject))
@@ -13194,154 +13012,158 @@ func (sh *strictHandler) StopRecording(w http.ResponseWriter, r *http.Request) {
// Base64 encoded, gzipped, json marshaled Swagger object
var swaggerSpec = []string{
- "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",
+ "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",
}
// GetSwagger returns the content of the embedded swagger specification file
diff --git a/server/lib/typinghumanizer/typinghumanizer.go b/server/lib/typinghumanizer/typinghumanizer.go
new file mode 100644
index 00000000..66b00790
--- /dev/null
+++ b/server/lib/typinghumanizer/typinghumanizer.go
@@ -0,0 +1,167 @@
+package typinghumanizer
+
+import (
+ "math/rand"
+ "strings"
+ "time"
+ "unicode"
+)
+
+// UniformJitter returns a random duration in [baseMs-jitterMs, baseMs+jitterMs],
+// clamped to a minimum of minMs.
+func UniformJitter(rng *rand.Rand, baseMs, jitterMs, minMs int) time.Duration {
+ ms := baseMs - jitterMs + rng.Intn(2*jitterMs+1)
+ if ms < minMs {
+ ms = minMs
+ }
+ return time.Duration(ms) * time.Millisecond
+}
+
+// SplitWordChunks splits text into word-sized chunks, keeping trailing
+// whitespace and punctuation attached to the preceding word.
+// For example, "Hello world. How are you?" becomes:
+//
+// ["Hello ", "world. ", "How ", "are ", "you?"]
+func SplitWordChunks(text string) []string {
+ if len(text) == 0 {
+ return nil
+ }
+
+ var chunks []string
+ var current strings.Builder
+
+ runes := []rune(text)
+ i := 0
+ for i < len(runes) {
+ r := runes[i]
+ current.WriteRune(r)
+ i++
+
+ if unicode.IsSpace(r) {
+ for i < len(runes) && unicode.IsSpace(runes[i]) {
+ current.WriteRune(runes[i])
+ i++
+ }
+ chunks = append(chunks, current.String())
+ current.Reset()
+ }
+ }
+
+ if current.Len() > 0 {
+ chunks = append(chunks, current.String())
+ }
+
+ return chunks
+}
+
+// IsSentenceEnd returns true if the chunk ends with sentence-ending punctuation
+// (before any trailing whitespace).
+func IsSentenceEnd(chunk string) bool {
+ trimmed := strings.TrimRightFunc(chunk, unicode.IsSpace)
+ if len(trimmed) == 0 {
+ return false
+ }
+ last := trimmed[len(trimmed)-1]
+ return last == '.' || last == '!' || last == '?'
+}
+
+// TypoKind identifies the type of typo to inject.
+type TypoKind int
+
+const (
+ TypoAdjacentKey TypoKind = iota // Hit a neighboring key
+ TypoDoubling // Type the character twice
+ TypoTranspose // Swap current and next character
+ TypoExtraChar // Insert a random adjacent key before the correct one
+)
+
+// Typo describes a single typo at a position in the text.
+type Typo struct {
+ Pos int // Character index in the rune slice
+ Kind TypoKind // What kind of typo
+}
+
+// qwertyAdj maps each lowercase letter to its adjacent keys on a QWERTY layout.
+var qwertyAdj = [26][]byte{
+ {'q', 'w', 's', 'z'}, // a
+ {'v', 'g', 'h', 'n'}, // b
+ {'x', 'd', 'f', 'v'}, // c
+ {'s', 'e', 'r', 'f', 'c', 'x'}, // d
+ {'w', 's', 'd', 'r'}, // e
+ {'d', 'r', 't', 'g', 'v', 'c'}, // f
+ {'f', 't', 'y', 'h', 'b', 'v'}, // g
+ {'g', 'y', 'u', 'j', 'n', 'b'}, // h
+ {'u', 'j', 'k', 'o'}, // i
+ {'h', 'u', 'i', 'k', 'n', 'm'}, // j
+ {'j', 'i', 'o', 'l', 'm'}, // k
+ {'k', 'o', 'p'}, // l
+ {'n', 'j', 'k'}, // m
+ {'b', 'h', 'j', 'm'}, // n
+ {'i', 'k', 'l', 'p'}, // o
+ {'o', 'l'}, // p
+ {'w', 'a'}, // q
+ {'e', 'd', 'f', 't'}, // r
+ {'a', 'w', 'e', 'd', 'x', 'z'}, // s
+ {'r', 'f', 'g', 'y'}, // t
+ {'y', 'h', 'j', 'i'}, // u
+ {'c', 'f', 'g', 'b'}, // v
+ {'q', 'a', 's', 'e'}, // w
+ {'z', 's', 'd', 'c'}, // x
+ {'t', 'g', 'h', 'u'}, // y
+ {'a', 's', 'x'}, // z
+}
+
+// AdjacentKey returns a random QWERTY neighbor of the given character.
+// If the character has no known neighbors (non-letter), it returns the
+// character itself unchanged.
+func AdjacentKey(rng *rand.Rand, ch rune) rune {
+ lower := unicode.ToLower(ch)
+ if lower < 'a' || lower > 'z' {
+ return ch
+ }
+ neighbors := qwertyAdj[lower-'a']
+ if len(neighbors) == 0 {
+ return ch
+ }
+ adj := rune(neighbors[rng.Intn(len(neighbors))])
+ if unicode.IsUpper(ch) {
+ adj = unicode.ToUpper(adj)
+ }
+ return adj
+}
+
+// GenerateTypoPositions computes typo positions using geometric gap sampling.
+// O(typos) random calls, not O(chars). Returns a sorted slice of Typo structs.
+func GenerateTypoPositions(rng *rand.Rand, textLen int, typoRate float64) []Typo {
+ if typoRate <= 0 || textLen <= 1 {
+ return nil
+ }
+ avgGap := int(1.0 / typoRate)
+ if avgGap < 2 {
+ avgGap = 2
+ }
+
+ var typos []Typo
+ halfGap := avgGap / 2
+ if halfGap < 1 {
+ halfGap = 1
+ }
+ pos := halfGap + rng.Intn(avgGap)
+ for pos < textLen {
+ roll := rng.Intn(100)
+ var kind TypoKind
+ switch {
+ case roll < 60:
+ kind = TypoAdjacentKey
+ case roll < 80:
+ kind = TypoDoubling
+ case roll < 95:
+ kind = TypoTranspose
+ default:
+ kind = TypoExtraChar
+ }
+ typos = append(typos, Typo{Pos: pos, Kind: kind})
+ pos += halfGap + rng.Intn(avgGap)
+ }
+ return typos
+}
diff --git a/server/lib/typinghumanizer/typinghumanizer_test.go b/server/lib/typinghumanizer/typinghumanizer_test.go
new file mode 100644
index 00000000..5271aa80
--- /dev/null
+++ b/server/lib/typinghumanizer/typinghumanizer_test.go
@@ -0,0 +1,192 @@
+package typinghumanizer
+
+import (
+ "math/rand"
+ "testing"
+ "time"
+
+ "github.com/stretchr/testify/assert"
+ "github.com/stretchr/testify/require"
+)
+
+func TestUniformJitter(t *testing.T) {
+ rng := rand.New(rand.NewSource(42))
+
+ t.Run("stays within range", func(t *testing.T) {
+ for i := 0; i < 1000; i++ {
+ d := UniformJitter(rng, 100, 30, 50)
+ ms := d.Milliseconds()
+ assert.GreaterOrEqual(t, ms, int64(50), "should be >= minMs")
+ assert.LessOrEqual(t, ms, int64(130), "should be <= baseMs+jitterMs")
+ }
+ })
+
+ t.Run("clamps to minimum", func(t *testing.T) {
+ for i := 0; i < 100; i++ {
+ d := UniformJitter(rng, 10, 20, 5)
+ assert.GreaterOrEqual(t, d.Milliseconds(), int64(5))
+ }
+ })
+
+ t.Run("zero jitter returns base", func(t *testing.T) {
+ d := UniformJitter(rng, 100, 0, 0)
+ assert.Equal(t, 100*time.Millisecond, d)
+ })
+}
+
+func TestSplitWordChunks(t *testing.T) {
+ tests := []struct {
+ name string
+ input string
+ expected []string
+ }{
+ {
+ name: "simple sentence",
+ input: "Hello world",
+ expected: []string{"Hello ", "world"},
+ },
+ {
+ name: "with punctuation",
+ input: "Hello world. How are you?",
+ expected: []string{"Hello ", "world. ", "How ", "are ", "you?"},
+ },
+ {
+ name: "single word",
+ input: "Hello",
+ expected: []string{"Hello"},
+ },
+ {
+ name: "empty string",
+ input: "",
+ expected: nil,
+ },
+ {
+ name: "only spaces",
+ input: " ",
+ expected: []string{" "},
+ },
+ {
+ name: "multiple spaces between words",
+ input: "Hello world",
+ expected: []string{"Hello ", "world"},
+ },
+ {
+ name: "trailing space",
+ input: "Hello ",
+ expected: []string{"Hello "},
+ },
+ {
+ name: "leading space",
+ input: " Hello",
+ expected: []string{" ", "Hello"},
+ },
+ }
+
+ for _, tt := range tests {
+ t.Run(tt.name, func(t *testing.T) {
+ result := SplitWordChunks(tt.input)
+ require.Equal(t, tt.expected, result)
+ })
+ }
+}
+
+func TestIsSentenceEnd(t *testing.T) {
+ tests := []struct {
+ chunk string
+ expected bool
+ }{
+ {"world. ", true},
+ {"you?", true},
+ {"wow! ", true},
+ {"Hello ", false},
+ {"word", false},
+ {"", false},
+ {" ", false},
+ {"end.", true},
+ }
+
+ for _, tt := range tests {
+ t.Run(tt.chunk, func(t *testing.T) {
+ assert.Equal(t, tt.expected, IsSentenceEnd(tt.chunk))
+ })
+ }
+}
+
+func TestAdjacentKey(t *testing.T) {
+ rng := rand.New(rand.NewSource(42))
+
+ t.Run("returns a neighbor for lowercase letters", func(t *testing.T) {
+ for i := 0; i < 100; i++ {
+ adj := AdjacentKey(rng, 'a')
+ assert.Contains(t, []rune{'q', 'w', 's', 'z'}, adj)
+ }
+ })
+
+ t.Run("preserves uppercase", func(t *testing.T) {
+ for i := 0; i < 100; i++ {
+ adj := AdjacentKey(rng, 'A')
+ assert.Contains(t, []rune{'Q', 'W', 'S', 'Z'}, adj)
+ }
+ })
+
+ t.Run("returns same char for non-letters", func(t *testing.T) {
+ assert.Equal(t, '5', AdjacentKey(rng, '5'))
+ assert.Equal(t, '.', AdjacentKey(rng, '.'))
+ assert.Equal(t, ' ', AdjacentKey(rng, ' '))
+ })
+}
+
+func TestGenerateTypoPositions(t *testing.T) {
+ rng := rand.New(rand.NewSource(42))
+
+ t.Run("zero rate returns nil", func(t *testing.T) {
+ assert.Nil(t, GenerateTypoPositions(rng, 100, 0))
+ })
+
+ t.Run("short text returns nil or few typos", func(t *testing.T) {
+ typos := GenerateTypoPositions(rng, 1, 0.05)
+ assert.Nil(t, typos)
+ })
+
+ t.Run("positions are within bounds and sorted", func(t *testing.T) {
+ textLen := 200
+ typos := GenerateTypoPositions(rng, textLen, 0.03)
+ for i, typo := range typos {
+ assert.GreaterOrEqual(t, typo.Pos, 0)
+ assert.Less(t, typo.Pos, textLen)
+ if i > 0 {
+ assert.Greater(t, typo.Pos, typos[i-1].Pos, "positions must be strictly increasing")
+ }
+ }
+ })
+
+ t.Run("roughly matches expected count", func(t *testing.T) {
+ textLen := 1000
+ rate := 0.03
+ totalTypos := 0
+ runs := 200
+ for i := 0; i < runs; i++ {
+ localRng := rand.New(rand.NewSource(int64(i)))
+ typos := GenerateTypoPositions(localRng, textLen, rate)
+ totalTypos += len(typos)
+ }
+ avgTypos := float64(totalTypos) / float64(runs)
+ expected := float64(textLen) * rate
+ assert.InDelta(t, expected, avgTypos, expected*0.3, "average typo count should be near expected")
+ })
+
+ t.Run("kind distribution is weighted", func(t *testing.T) {
+ counts := map[TypoKind]int{}
+ for i := 0; i < 500; i++ {
+ localRng := rand.New(rand.NewSource(int64(i)))
+ typos := GenerateTypoPositions(localRng, 500, 0.05)
+ for _, typo := range typos {
+ counts[typo.Kind]++
+ }
+ }
+ total := counts[TypoAdjacentKey] + counts[TypoDoubling] + counts[TypoTranspose] + counts[TypoExtraChar]
+ require.Greater(t, total, 0)
+ adjPct := float64(counts[TypoAdjacentKey]) / float64(total)
+ assert.InDelta(t, 0.60, adjPct, 0.10, "adjacent key should be ~60%%")
+ })
+}
diff --git a/server/openapi.yaml b/server/openapi.yaml
index 79396d80..737e1db6 100644
--- a/server/openapi.yaml
+++ b/server/openapi.yaml
@@ -1367,9 +1367,26 @@ components:
description: Text to type on the host computer
delay:
type: integer
- description: Delay in milliseconds between keystrokes
+ description: Delay in milliseconds between keystrokes. Ignored when smooth is true.
minimum: 0
default: 0
+ smooth:
+ type: boolean
+ description: |
+ Use human-like variable keystroke timing instead of a fixed delay.
+ When enabled, text is typed in word-sized chunks with variable
+ intra-word delays and natural inter-word pauses. The delay field
+ is ignored when smooth is true.
+ default: false
+ typo_chance:
+ type: number
+ description: |
+ Probability (0.0-0.10) of a typo per character, which is then
+ corrected with backspace. Requires smooth to be true. Set to 0
+ to disable. Typical human range is 0.02-0.05.
+ minimum: 0
+ maximum: 0.10
+ default: 0
additionalProperties: false
ClipboardContent:
type: object