diff --git a/images/chromium-headful/client/src/components/video.vue b/images/chromium-headful/client/src/components/video.vue index fdb1d375..a747d6ac 100644 --- a/images/chromium-headful/client/src/components/video.vue +++ b/images/chromium-headful/client/src/components/video.vue @@ -552,6 +552,10 @@ } beforeDestroy() { + if (this._scrollFlushTimeout != null) { + clearTimeout(this._scrollFlushTimeout) + this._scrollFlushTimeout = null + } this.observer.disconnect() this.$accessor.video.setPlayable(false) /* Guacamole Keyboard does not provide destroy functions */ @@ -708,7 +712,53 @@ }) } - wheelThrottle = false + _scrollAccX = 0 + _scrollAccY = 0 + _scrollLastSendTime = 0 + _scrollFlushTimeout: ReturnType | null = null + _scrollLastClientX = 0 + _scrollLastClientY = 0 + _scrollApiUrl: string | null = null + + _getScrollApiUrl(): string { + if (this._scrollApiUrl) return this._scrollApiUrl + // The kernel-images API is exposed on port 444 (maps to 10001 inside the + // container) in both Docker and unikernel deployments. + this._scrollApiUrl = `${location.protocol}//${location.hostname}:444/live-view/scroll` + return this._scrollApiUrl + } + + _clearScrollFlushTimeout() { + if (this._scrollFlushTimeout != null) { + clearTimeout(this._scrollFlushTimeout) + this._scrollFlushTimeout = null + } + } + + _sendScrollAccumulated(clientX: number, clientY: number) { + if (this._scrollAccX === 0 && this._scrollAccY === 0) { + return + } + const { w, h } = this.$accessor.video.resolution + const rect = this._overlay.getBoundingClientRect() + const sx = Math.round((w / rect.width) * (clientX - rect.left)) + const sy = Math.round((h / rect.height) * (clientY - rect.top)) + + const dx = this._scrollAccX + const dy = this._scrollAccY + this._scrollAccX = 0 + this._scrollAccY = 0 + this._scrollLastSendTime = Date.now() + + const url = this._getScrollApiUrl() + fetch(url, { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ x: sx, y: sy, delta_x: -dx, delta_y: -dy }), + keepalive: true, + }).catch(() => {}) + } + onWheel(e: WheelEvent) { if (!this.hosting || this.locked) { return @@ -717,8 +767,6 @@ let x = e.deltaX let y = e.deltaY - // Normalize to pixel units. deltaMode 1 = lines, 2 = pages; convert - // both to approximate pixel values so the divisor below works uniformly. if (e.deltaMode !== 0) { x *= WHEEL_LINE_HEIGHT y *= WHEEL_LINE_HEIGHT @@ -729,26 +777,29 @@ y = y * -1 } - // The server sends one XTestFakeButtonEvent per unit we pass here, - // and each event scrolls Chromium by ~120 px. Raw pixel deltas from - // trackpads are already in pixels (~120 per notch), so dividing by - // PIXELS_PER_TICK converts them to discrete scroll "ticks". The - // result is clamped to [-scroll, scroll] (the user-facing sensitivity - // setting) so fast swipes don't over-scroll. - const PIXELS_PER_TICK = 120 - x = x === 0 ? 0 : Math.min(Math.max(Math.round(x / PIXELS_PER_TICK) || Math.sign(x), -this.scroll), this.scroll) - y = y === 0 ? 0 : Math.min(Math.max(Math.round(y / PIXELS_PER_TICK) || Math.sign(y), -this.scroll), this.scroll) + this._scrollAccX += x + this._scrollAccY += y - this.sendMousePos(e) + if (this._scrollAccX === 0 && this._scrollAccY === 0) { + return + } - if (!this.wheelThrottle) { - this.wheelThrottle = true - this.$client.sendData('wheel', { x, y }) + this._scrollLastClientX = e.clientX + this._scrollLastClientY = e.clientY - window.setTimeout(() => { - this.wheelThrottle = false - }, 100) + const now = Date.now() + if (now - this._scrollLastSendTime < 50) { + if (this._scrollFlushTimeout == null) { + this._scrollFlushTimeout = setTimeout(() => { + this._scrollFlushTimeout = null + this._sendScrollAccumulated(this._scrollLastClientX, this._scrollLastClientY) + }, 50) + } + return } + + this._clearScrollFlushTimeout() + this._sendScrollAccumulated(e.clientX, e.clientY) } onTouchHandler(e: TouchEvent) { diff --git a/server/api b/server/api index 2c657a10..b30376fe 100755 Binary files a/server/api and b/server/api differ diff --git a/server/cmd/api/api/computer.go b/server/cmd/api/api/computer.go index 2367cd68..62edc4e5 100644 --- a/server/cmd/api/api/computer.go +++ b/server/cmd/api/api/computer.go @@ -16,6 +16,7 @@ import ( "syscall" "time" + "github.com/onkernel/kernel-images/server/lib/cdpclient" "github.com/onkernel/kernel-images/server/lib/logger" "github.com/onkernel/kernel-images/server/lib/mousetrajectory" oapi "github.com/onkernel/kernel-images/server/lib/oapi" @@ -813,6 +814,44 @@ func (s *ApiService) doScroll(ctx context.Context, body oapi.ScrollRequest) erro return nil } +func (s *ApiService) LiveViewScroll(ctx context.Context, request oapi.LiveViewScrollRequestObject) (oapi.LiveViewScrollResponseObject, error) { + if request.Body == nil { + return oapi.LiveViewScroll400JSONResponse{BadRequestErrorJSONResponse: oapi.BadRequestErrorJSONResponse{Message: "missing request body"}}, nil + } + + var deltaX, deltaY float64 + if request.Body.DeltaX != nil { + deltaX = *request.Body.DeltaX + } + if request.Body.DeltaY != nil { + deltaY = *request.Body.DeltaY + } + + if deltaX == 0 && deltaY == 0 { + return oapi.LiveViewScroll200Response{}, nil + } + + upstreamURL := s.upstreamMgr.Current() + if upstreamURL == "" { + return oapi.LiveViewScroll503JSONResponse{Message: "devtools upstream not available"}, nil + } + + cdpCtx, cancel := context.WithTimeout(ctx, 3*time.Second) + defer cancel() + + client, err := cdpclient.Dial(cdpCtx, upstreamURL) + if err != nil { + return oapi.LiveViewScroll500JSONResponse{InternalErrorJSONResponse: oapi.InternalErrorJSONResponse{Message: fmt.Sprintf("cdp dial failed: %s", err)}}, nil + } + defer client.Close() + + if err := client.DispatchMouseWheelEvent(cdpCtx, request.Body.X, request.Body.Y, deltaX, deltaY); err != nil { + return oapi.LiveViewScroll500JSONResponse{InternalErrorJSONResponse: oapi.InternalErrorJSONResponse{Message: fmt.Sprintf("cdp scroll failed: %s", err)}}, nil + } + + return oapi.LiveViewScroll200Response{}, nil +} + func (s *ApiService) Scroll(ctx context.Context, request oapi.ScrollRequestObject) (oapi.ScrollResponseObject, error) { s.inputMu.Lock() defer s.inputMu.Unlock() diff --git a/server/cmd/api/main.go b/server/cmd/api/main.go index c80ddd27..a71dd569 100644 --- a/server/cmd/api/main.go +++ b/server/cmd/api/main.go @@ -54,6 +54,20 @@ func main() { r.Use( chiMiddleware.Logger, chiMiddleware.Recoverer, + func(next http.Handler) http.Handler { + return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + if strings.HasPrefix(r.URL.Path, "/live-view/") { + w.Header().Set("Access-Control-Allow-Origin", "*") + w.Header().Set("Access-Control-Allow-Methods", "GET, POST, PUT, PATCH, DELETE, OPTIONS") + w.Header().Set("Access-Control-Allow-Headers", "Content-Type, Authorization") + if r.Method == http.MethodOptions { + w.WriteHeader(http.StatusNoContent) + return + } + } + next.ServeHTTP(w, r) + }) + }, func(next http.Handler) http.Handler { return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { ctxWithLogger := logger.AddToContext(r.Context(), slogger) 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/cdpclient/cdpclient.go b/server/lib/cdpclient/cdpclient.go index eff95a00..0c3b502b 100644 --- a/server/lib/cdpclient/cdpclient.go +++ b/server/lib/cdpclient/cdpclient.go @@ -159,3 +159,66 @@ func (c *Client) SetDeviceMetricsOverride(ctx context.Context, width, height int return nil } + +// DispatchMouseWheelEvent sends a mouseWheel event to the first page target +// via CDP Input.dispatchMouseEvent. deltaX/deltaY are in CSS pixels, allowing +// sub-notch precision that X11 button events cannot express. +func (c *Client) DispatchMouseWheelEvent(ctx context.Context, x, y int, deltaX, deltaY float64) error { + targetsResult, err := c.send(ctx, "Target.getTargets", nil, "") + if err != nil { + return fmt.Errorf("Target.getTargets: %w", err) + } + + var targets struct { + TargetInfos []struct { + TargetID string `json:"targetId"` + Type string `json:"type"` + } `json:"targetInfos"` + } + if err := json.Unmarshal(targetsResult, &targets); err != nil { + return fmt.Errorf("unmarshal targets: %w", err) + } + + var pageTargetID string + for _, t := range targets.TargetInfos { + if t.Type == "page" { + pageTargetID = t.TargetID + break + } + } + if pageTargetID == "" { + return fmt.Errorf("no page target found") + } + + attachResult, err := c.send(ctx, "Target.attachToTarget", map[string]any{ + "targetId": pageTargetID, + "flatten": true, + }, "") + if err != nil { + return fmt.Errorf("Target.attachToTarget: %w", err) + } + + var attach struct { + SessionID string `json:"sessionId"` + } + if err := json.Unmarshal(attachResult, &attach); err != nil { + return fmt.Errorf("unmarshal attach: %w", err) + } + + _, err = c.send(ctx, "Input.dispatchMouseEvent", map[string]any{ + "type": "mouseWheel", + "x": x, + "y": y, + "deltaX": deltaX, + "deltaY": deltaY, + }, attach.SessionID) + if err != nil { + return fmt.Errorf("Input.dispatchMouseEvent mouseWheel: %w", err) + } + + _, _ = c.send(ctx, "Target.detachFromTarget", map[string]any{ + "sessionId": attach.SessionID, + }, "") + + return nil +} diff --git a/server/lib/oapi/oapi.go b/server/lib/oapi/oapi.go index 4854c577..7aeb6d32 100644 --- a/server/lib/oapi/oapi.go +++ b/server/lib/oapi/oapi.go @@ -495,6 +495,21 @@ type FileSystemEventType string // ListFiles Array of file or directory information entries. type ListFiles = []FileInfo +// LiveViewScrollRequest defines model for LiveViewScrollRequest. +type LiveViewScrollRequest struct { + // DeltaX Horizontal scroll delta in CSS pixels, forwarded directly to CDP. Positive scrolls left, negative scrolls right. + DeltaX *float64 `json:"delta_x,omitempty"` + + // DeltaY Vertical scroll delta in CSS pixels, forwarded directly to CDP. Positive scrolls up, negative scrolls down. + DeltaY *float64 `json:"delta_y,omitempty"` + + // X X coordinate (CSS pixels) at which to dispatch the scroll + X int `json:"x"` + + // Y Y coordinate (CSS pixels) at which to dispatch the scroll + Y int `json:"y"` +} + // LogEvent A log entry from the application. type LogEvent struct { // Message Log message text. @@ -1058,6 +1073,9 @@ type UploadZstdMultipartRequestBody UploadZstdMultipartBody // StartFsWatchJSONRequestBody defines body for StartFsWatch for application/json ContentType. type StartFsWatchJSONRequestBody = StartFsWatchRequest +// LiveViewScrollJSONRequestBody defines body for LiveViewScroll for application/json ContentType. +type LiveViewScrollJSONRequestBody = LiveViewScrollRequest + // ExecutePlaywrightCodeJSONRequestBody defines body for ExecutePlaywrightCode for application/json ContentType. type ExecutePlaywrightCodeJSONRequestBody = ExecutePlaywrightRequest @@ -1295,6 +1313,11 @@ type ClientInterface interface { // WriteFileWithBody request with any body WriteFileWithBody(ctx context.Context, params *WriteFileParams, contentType string, body io.Reader, reqEditors ...RequestEditorFn) (*http.Response, error) + // LiveViewScrollWithBody request with any body + LiveViewScrollWithBody(ctx context.Context, contentType string, body io.Reader, reqEditors ...RequestEditorFn) (*http.Response, error) + + LiveViewScroll(ctx context.Context, body LiveViewScrollJSONRequestBody, reqEditors ...RequestEditorFn) (*http.Response, error) + // LogsStream request LogsStream(ctx context.Context, params *LogsStreamParams, reqEditors ...RequestEditorFn) (*http.Response, error) @@ -1980,6 +2003,30 @@ func (c *Client) WriteFileWithBody(ctx context.Context, params *WriteFileParams, return c.Client.Do(req) } +func (c *Client) LiveViewScrollWithBody(ctx context.Context, contentType string, body io.Reader, reqEditors ...RequestEditorFn) (*http.Response, error) { + req, err := NewLiveViewScrollRequestWithBody(c.Server, contentType, body) + if err != nil { + return nil, err + } + req = req.WithContext(ctx) + if err := c.applyEditors(ctx, req, reqEditors); err != nil { + return nil, err + } + return c.Client.Do(req) +} + +func (c *Client) LiveViewScroll(ctx context.Context, body LiveViewScrollJSONRequestBody, reqEditors ...RequestEditorFn) (*http.Response, error) { + req, err := NewLiveViewScrollRequest(c.Server, body) + if err != nil { + return nil, err + } + req = req.WithContext(ctx) + if err := c.applyEditors(ctx, req, reqEditors); err != nil { + return nil, err + } + return c.Client.Do(req) +} + func (c *Client) LogsStream(ctx context.Context, params *LogsStreamParams, reqEditors ...RequestEditorFn) (*http.Response, error) { req, err := NewLogsStreamRequest(c.Server, params) if err != nil { @@ -3558,6 +3605,46 @@ func NewWriteFileRequestWithBody(server string, params *WriteFileParams, content return req, nil } +// NewLiveViewScrollRequest calls the generic LiveViewScroll builder with application/json body +func NewLiveViewScrollRequest(server string, body LiveViewScrollJSONRequestBody) (*http.Request, error) { + var bodyReader io.Reader + buf, err := json.Marshal(body) + if err != nil { + return nil, err + } + bodyReader = bytes.NewReader(buf) + return NewLiveViewScrollRequestWithBody(server, "application/json", bodyReader) +} + +// NewLiveViewScrollRequestWithBody generates requests for LiveViewScroll with any type of body +func NewLiveViewScrollRequestWithBody(server string, contentType string, body io.Reader) (*http.Request, error) { + var err error + + serverURL, err := url.Parse(server) + if err != nil { + return nil, err + } + + operationPath := fmt.Sprintf("/live-view/scroll") + if operationPath[0] == '/' { + operationPath = "." + operationPath + } + + queryURL, err := serverURL.Parse(operationPath) + if err != nil { + return nil, err + } + + req, err := http.NewRequest("POST", queryURL.String(), body) + if err != nil { + return nil, err + } + + req.Header.Add("Content-Type", contentType) + + return req, nil +} + // NewLogsStreamRequest generates requests for LogsStream func NewLogsStreamRequest(server string, params *LogsStreamParams) (*http.Request, error) { var err error @@ -4356,6 +4443,11 @@ type ClientWithResponsesInterface interface { // WriteFileWithBodyWithResponse request with any body WriteFileWithBodyWithResponse(ctx context.Context, params *WriteFileParams, contentType string, body io.Reader, reqEditors ...RequestEditorFn) (*WriteFileResponse, error) + // LiveViewScrollWithBodyWithResponse request with any body + LiveViewScrollWithBodyWithResponse(ctx context.Context, contentType string, body io.Reader, reqEditors ...RequestEditorFn) (*LiveViewScrollResponse, error) + + LiveViewScrollWithResponse(ctx context.Context, body LiveViewScrollJSONRequestBody, reqEditors ...RequestEditorFn) (*LiveViewScrollResponse, error) + // LogsStreamWithResponse request LogsStreamWithResponse(ctx context.Context, params *LogsStreamParams, reqEditors ...RequestEditorFn) (*LogsStreamResponse, error) @@ -5201,6 +5293,30 @@ func (r WriteFileResponse) StatusCode() int { return 0 } +type LiveViewScrollResponse struct { + Body []byte + HTTPResponse *http.Response + JSON400 *BadRequestError + JSON500 *InternalError + JSON503 *Error +} + +// Status returns HTTPResponse.Status +func (r LiveViewScrollResponse) Status() string { + if r.HTTPResponse != nil { + return r.HTTPResponse.Status + } + return http.StatusText(0) +} + +// StatusCode returns HTTPResponse.StatusCode +func (r LiveViewScrollResponse) StatusCode() int { + if r.HTTPResponse != nil { + return r.HTTPResponse.StatusCode + } + return 0 +} + type LogsStreamResponse struct { Body []byte HTTPResponse *http.Response @@ -5986,6 +6102,23 @@ func (c *ClientWithResponses) WriteFileWithBodyWithResponse(ctx context.Context, return ParseWriteFileResponse(rsp) } +// LiveViewScrollWithBodyWithResponse request with arbitrary body returning *LiveViewScrollResponse +func (c *ClientWithResponses) LiveViewScrollWithBodyWithResponse(ctx context.Context, contentType string, body io.Reader, reqEditors ...RequestEditorFn) (*LiveViewScrollResponse, error) { + rsp, err := c.LiveViewScrollWithBody(ctx, contentType, body, reqEditors...) + if err != nil { + return nil, err + } + return ParseLiveViewScrollResponse(rsp) +} + +func (c *ClientWithResponses) LiveViewScrollWithResponse(ctx context.Context, body LiveViewScrollJSONRequestBody, reqEditors ...RequestEditorFn) (*LiveViewScrollResponse, error) { + rsp, err := c.LiveViewScroll(ctx, body, reqEditors...) + if err != nil { + return nil, err + } + return ParseLiveViewScrollResponse(rsp) +} + // LogsStreamWithResponse request returning *LogsStreamResponse func (c *ClientWithResponses) LogsStreamWithResponse(ctx context.Context, params *LogsStreamParams, reqEditors ...RequestEditorFn) (*LogsStreamResponse, error) { rsp, err := c.LogsStream(ctx, params, reqEditors...) @@ -7430,6 +7563,46 @@ func ParseWriteFileResponse(rsp *http.Response) (*WriteFileResponse, error) { return response, nil } +// ParseLiveViewScrollResponse parses an HTTP response from a LiveViewScrollWithResponse call +func ParseLiveViewScrollResponse(rsp *http.Response) (*LiveViewScrollResponse, error) { + bodyBytes, err := io.ReadAll(rsp.Body) + defer func() { _ = rsp.Body.Close() }() + if err != nil { + return nil, err + } + + response := &LiveViewScrollResponse{ + Body: bodyBytes, + HTTPResponse: rsp, + } + + switch { + case strings.Contains(rsp.Header.Get("Content-Type"), "json") && rsp.StatusCode == 400: + var dest BadRequestError + if err := json.Unmarshal(bodyBytes, &dest); err != nil { + return nil, err + } + response.JSON400 = &dest + + case strings.Contains(rsp.Header.Get("Content-Type"), "json") && rsp.StatusCode == 500: + var dest InternalError + if err := json.Unmarshal(bodyBytes, &dest); err != nil { + return nil, err + } + response.JSON500 = &dest + + case strings.Contains(rsp.Header.Get("Content-Type"), "json") && rsp.StatusCode == 503: + var dest Error + if err := json.Unmarshal(bodyBytes, &dest); err != nil { + return nil, err + } + response.JSON503 = &dest + + } + + return response, nil +} + // ParseLogsStreamResponse parses an HTTP response from a LogsStreamWithResponse call func ParseLogsStreamResponse(rsp *http.Response) (*LogsStreamResponse, error) { bodyBytes, err := io.ReadAll(rsp.Body) @@ -8088,6 +8261,9 @@ type ServerInterface interface { // Write or create a file // (PUT /fs/write_file) WriteFile(w http.ResponseWriter, r *http.Request, params WriteFileParams) + // Pixel-precise scroll for the live view client + // (POST /live-view/scroll) + LiveViewScroll(w http.ResponseWriter, r *http.Request) // Stream logs over SSE // (GET /logs/stream) LogsStream(w http.ResponseWriter, r *http.Request, params LogsStreamParams) @@ -8334,6 +8510,12 @@ func (_ Unimplemented) WriteFile(w http.ResponseWriter, r *http.Request, params w.WriteHeader(http.StatusNotImplemented) } +// Pixel-precise scroll for the live view client +// (POST /live-view/scroll) +func (_ Unimplemented) LiveViewScroll(w http.ResponseWriter, r *http.Request) { + w.WriteHeader(http.StatusNotImplemented) +} + // Stream logs over SSE // (GET /logs/stream) func (_ Unimplemented) LogsStream(w http.ResponseWriter, r *http.Request, params LogsStreamParams) { @@ -9047,6 +9229,20 @@ func (siw *ServerInterfaceWrapper) WriteFile(w http.ResponseWriter, r *http.Requ handler.ServeHTTP(w, r) } +// LiveViewScroll operation middleware +func (siw *ServerInterfaceWrapper) LiveViewScroll(w http.ResponseWriter, r *http.Request) { + + handler := http.Handler(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + siw.Handler.LiveViewScroll(w, r) + })) + + for _, middleware := range siw.HandlerMiddlewares { + handler = middleware(handler) + } + + handler.ServeHTTP(w, r) +} + // LogsStream operation middleware func (siw *ServerInterfaceWrapper) LogsStream(w http.ResponseWriter, r *http.Request) { @@ -9567,6 +9763,9 @@ func HandlerWithOptions(si ServerInterface, options ChiServerOptions) http.Handl r.Group(func(r chi.Router) { r.Put(options.BaseURL+"/fs/write_file", wrapper.WriteFile) }) + r.Group(func(r chi.Router) { + r.Post(options.BaseURL+"/live-view/scroll", wrapper.LiveViewScroll) + }) r.Group(func(r chi.Router) { r.Get(options.BaseURL+"/logs/stream", wrapper.LogsStream) }) @@ -10970,6 +11169,49 @@ func (response WriteFile500JSONResponse) VisitWriteFileResponse(w http.ResponseW return json.NewEncoder(w).Encode(response) } +type LiveViewScrollRequestObject struct { + Body *LiveViewScrollJSONRequestBody +} + +type LiveViewScrollResponseObject interface { + VisitLiveViewScrollResponse(w http.ResponseWriter) error +} + +type LiveViewScroll200Response struct { +} + +func (response LiveViewScroll200Response) VisitLiveViewScrollResponse(w http.ResponseWriter) error { + w.WriteHeader(200) + return nil +} + +type LiveViewScroll400JSONResponse struct{ BadRequestErrorJSONResponse } + +func (response LiveViewScroll400JSONResponse) VisitLiveViewScrollResponse(w http.ResponseWriter) error { + w.Header().Set("Content-Type", "application/json") + w.WriteHeader(400) + + return json.NewEncoder(w).Encode(response) +} + +type LiveViewScroll500JSONResponse struct{ InternalErrorJSONResponse } + +func (response LiveViewScroll500JSONResponse) VisitLiveViewScrollResponse(w http.ResponseWriter) error { + w.Header().Set("Content-Type", "application/json") + w.WriteHeader(500) + + return json.NewEncoder(w).Encode(response) +} + +type LiveViewScroll503JSONResponse Error + +func (response LiveViewScroll503JSONResponse) VisitLiveViewScrollResponse(w http.ResponseWriter) error { + w.Header().Set("Content-Type", "application/json") + w.WriteHeader(503) + + return json.NewEncoder(w).Encode(response) +} + type LogsStreamRequestObject struct { Params LogsStreamParams } @@ -11722,6 +11964,9 @@ type StrictServerInterface interface { // Write or create a file // (PUT /fs/write_file) WriteFile(ctx context.Context, request WriteFileRequestObject) (WriteFileResponseObject, error) + // Pixel-precise scroll for the live view client + // (POST /live-view/scroll) + LiveViewScroll(ctx context.Context, request LiveViewScrollRequestObject) (LiveViewScrollResponseObject, error) // Stream logs over SSE // (GET /logs/stream) LogsStream(ctx context.Context, request LogsStreamRequestObject) (LogsStreamResponseObject, error) @@ -12769,6 +13014,37 @@ func (sh *strictHandler) WriteFile(w http.ResponseWriter, r *http.Request, param } } +// LiveViewScroll operation middleware +func (sh *strictHandler) LiveViewScroll(w http.ResponseWriter, r *http.Request) { + var request LiveViewScrollRequestObject + + var body LiveViewScrollJSONRequestBody + if err := json.NewDecoder(r.Body).Decode(&body); err != nil { + 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.LiveViewScroll(ctx, request.(LiveViewScrollRequestObject)) + } + for _, middleware := range sh.middlewares { + handler = middleware(handler, "LiveViewScroll") + } + + response, err := handler(r.Context(), w, r, request) + + if err != nil { + sh.options.ResponseErrorHandlerFunc(w, r, err) + } else if validResponse, ok := response.(LiveViewScrollResponseObject); ok { + if err := validResponse.VisitLiveViewScrollResponse(w); err != nil { + sh.options.ResponseErrorHandlerFunc(w, r, err) + } + } else if response != nil { + sh.options.ResponseErrorHandlerFunc(w, r, fmt.Errorf("unexpected response type: %T", response)) + } +} + // LogsStream operation middleware func (sh *strictHandler) LogsStream(w http.ResponseWriter, r *http.Request, params LogsStreamParams) { var request LogsStreamRequestObject @@ -13194,154 +13470,160 @@ func (sh *strictHandler) StopRecording(w http.ResponseWriter, r *http.Request) { // Base64 encoded, gzipped, json marshaled Swagger object var swaggerSpec = []string{ - "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", } // GetSwagger returns the content of the embedded swagger specification file diff --git a/server/openapi.yaml b/server/openapi.yaml index 79396d80..739fd773 100644 --- a/server/openapi.yaml +++ b/server/openapi.yaml @@ -1169,6 +1169,35 @@ paths: $ref: "#/components/responses/BadRequestError" "500": $ref: "#/components/responses/InternalError" + /live-view/scroll: + post: + summary: Pixel-precise scroll for the live view client + description: | + Accepts pixel-precise scroll deltas and forwards them directly to + Chromium via CDP Input.dispatchMouseEvent (type mouseWheel), bypassing + X11 entirely. Unlike /computer/scroll which accepts discrete tick + counts, this endpoint accepts floating-point CSS pixel deltas for + smooth trackpad scrolling in the live view. + operationId: liveViewScroll + requestBody: + required: true + content: + application/json: + schema: + $ref: "#/components/schemas/LiveViewScrollRequest" + responses: + "200": + description: Scroll dispatched + "400": + $ref: "#/components/responses/BadRequestError" + "500": + $ref: "#/components/responses/InternalError" + "503": + description: DevTools upstream not available + content: + application/json: + schema: + $ref: "#/components/schemas/Error" /playwright/execute: post: summary: Execute Playwright/TypeScript code against the browser @@ -1439,6 +1468,29 @@ components: items: type: string additionalProperties: false + LiveViewScrollRequest: + type: object + required: + - x + - y + properties: + x: + type: integer + description: X coordinate (CSS pixels) at which to dispatch the scroll + y: + type: integer + description: Y coordinate (CSS pixels) at which to dispatch the scroll + delta_x: + type: number + format: double + description: Horizontal scroll delta in CSS pixels, forwarded directly to CDP. Positive scrolls left, negative scrolls right. + default: 0 + delta_y: + type: number + format: double + description: Vertical scroll delta in CSS pixels, forwarded directly to CDP. Positive scrolls up, negative scrolls down. + default: 0 + additionalProperties: false DragMouseRequest: type: object required: