Skip to content

fix(browser): fall back to Hyprland grim capture for headed viewport screenshots#67149

Closed
Angfr95 wants to merge 983 commits into
openclaw:mainfrom
Angfr95:fix/hyprland-headed-viewport-screenshot-timeout
Closed

fix(browser): fall back to Hyprland grim capture for headed viewport screenshots#67149
Angfr95 wants to merge 983 commits into
openclaw:mainfrom
Angfr95:fix/hyprland-headed-viewport-screenshot-timeout

Conversation

@Angfr95

@Angfr95 Angfr95 commented Apr 15, 2026

Copy link
Copy Markdown
Contributor

Problem

On Omarchy/Hyprland/Wayland, headed Chromium viewport screenshots time out (~13s) with Page.captureScreenshot timed out after 10000ms on certain pages (GitHub Trending, Yahoo Finance), even though the page loaded successfully. Headless mode and the control UI work fine.

Root Cause

CDP Page.captureScreenshot relies on Chromium's internal raster path, which can stall on headed Wayland sessions for compositor-dependent reasons. The page is loaded and interactive, but the screenshot command hangs.

Fix

Add a Hyprland-native capture fallback in the /screenshot route:

  1. New file hyprland-capture.ts: detects active Hyprland session, creates a dedicated headless output via hyprctl, moves the browser window onto it, and captures with grim (~20-170ms vs 10s+ timeout).
  2. Modified agent.snapshot.ts: for normal viewport screenshots on headed non-headless Linux, tries grim capture first. Falls back to CDP transparently on failure or when not on Hyprland.

Scope: only normal viewport screenshots. Full-page and element captures still use CDP/Playwright.

Tests

  • agent.snapshot.test.ts: 8/8 pass
  • screenshot.test.ts: 2/2 pass
  • cdp.test.ts: 31/31 pass

Fixes #64317

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 0c13d0ddb6

ℹ️ About Codex in GitHub

Codex has been enabled to automatically review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

When you sign up for Codex through ChatGPT, Codex can also answer questions or update the PR, like "@codex address that feedback".

Comment on lines +375 to +377
const hyprlandPng =
browserPid && browserPid > 0
? await tryHyprlandViewportCapture({ browserPid, timeoutMs: 3000 })

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

P1 Badge Keep Hyprland fallback bound to the requested target tab

In the headed Hyprland path, /screenshot now calls tryHyprlandViewportCapture with only browserPid, which captures compositor output for the browser window rather than the selected CDP target. Because ensureTabAvailable(targetId) does not activate/focus that tab, requests for a background targetId can return an image of a different tab/window, so automation can read or act on the wrong page state.

Useful? React with 👍 / 👎.

Comment on lines +379 to +380
if (hyprlandPng) {
buffer = hyprlandPng;

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

P2 Badge Preserve requested image format when Hyprland capture succeeds

When type: "jpeg" is requested, the Hyprland branch assigns the raw grim output directly to buffer, but grim emits PNG bytes. saveNormalizedScreenshotResponse only converts on resize/compression pressure; otherwise it keeps the original buffer and labels it from the requested type, which can produce .jpeg media IDs backed by PNG content and break consumers that rely on consistent output format.

Useful? React with 👍 / 👎.

@greptile-apps

greptile-apps Bot commented Apr 15, 2026

Copy link
Copy Markdown
Contributor

Greptile Summary

This PR adds a Hyprland-native screenshot fallback (grim via hyprctl headless output) to fix CDP Page.captureScreenshot timeouts on headed Wayland/Omarchy sessions. The detection and binary resolution logic is well-guarded and the fallback to CDP on non-Hyprland environments is transparent.

Two P1 defects need addressing before merge:

  • PNG/JPEG mismatch: grim always outputs PNG, but when type: "jpeg" is requested and the buffer fits within normalizeBrowserScreenshot's 5 MB / 2000 px limits (the typical case for a 1920×1080 viewport), the saved file gets contentType: "image/jpeg" despite containing PNG bytes, breaking downstream image consumers.
  • Concurrent setup race: The module-level singleton is not guarded against concurrent async callers — two simultaneous requests both enter if (!cachedCapture) and independently call setupHeadedBrowserViewportCapture, dispatching duplicate movetoworkspacesilent commands and orphaning the first capture reference.

Confidence Score: 4/5

Not safe to merge as-is; two P1 defects (format mismatch and concurrent setup race) need to be fixed first.

The Hyprland detection and grim capture mechanics are sound, and the non-Hyprland fallback is transparent. However, the PNG-labeled-as-JPEG bug will silently corrupt screenshot responses whenever JPEG is requested on Hyprland, and the concurrent-setup race can cause double workspace dispatches under load. Both are straightforward fixes but represent present defects in the changed code.

Both changed files: hyprland-capture.ts for the singleton race and missing exit cleanup; agent.snapshot.ts for the PNG/JPEG format override in the Hyprland branch.

Prompt To Fix All With AI
This is a comment left during a code review.
Path: extensions/browser/src/browser/hyprland-capture.ts
Line: 421-449

Comment:
**Race condition in singleton setup**

Because `detectHyprlandSession()` and `setupHeadedBrowserViewportCapture()` both contain `await` points, two concurrent screenshot requests that both enter the `if (!cachedCapture)` branch will each proceed through setup independently. Both dispatch `movetoworkspacesilent` for the same PID; the second write to `cachedCapture` silently overwrites the first, leaving the first capture object abandoned without teardown.

Fix: deduplicate concurrent callers with a shared in-flight promise:

```ts
let setupInFlight: Promise<HeadedBrowserViewportCapture | null> | null = null;

if (!cachedCapture) {
  if (!setupInFlight) {
    setupInFlight = (async () => {
      const session = await detectHyprlandSession();
      if (!session) return null;
      return await setupHeadedBrowserViewportCapture({ browserPid: params.browserPid, session });
    })().finally(() => { setupInFlight = null; });
  }
  const capture = await setupInFlight;
  if (!capture) return null;
  cachedCapture = capture;
  cachedPid = params.browserPid;
}
```

How can I resolve this? If you propose a fix, please make it concise.

---

This is a comment left during a code review.
Path: extensions/browser/src/browser/routes/agent.snapshot.ts
Line: 378-389

Comment:
**PNG/JPEG format mismatch when `type === "jpeg"`**

`grim` always outputs PNG. When `type === "jpeg"` and the screenshot fits within `normalizeBrowserScreenshot`'s limits (< 5 MB, dimensions ≤ 2000 px — the typical case for a 1920×1080 viewport), `normalizeBrowserScreenshot` returns `{ buffer }` with no `contentType`. The fallback `image/${params.type}` evaluates to `"image/jpeg"`, so the saved file is labeled as JPEG despite containing raw PNG bytes.

Fix: override `type` to `"png"` for the Hyprland branch:

```ts
if (hyprlandPng) {
  await saveNormalizedScreenshotResponse({
    res,
    buffer: hyprlandPng,
    type: "png", // grim always produces PNG
    targetId: tab.targetId,
    url: tab.url,
  });
  return;
}
buffer = await captureScreenshot({
  wsUrl: tab.wsUrl ?? "",
  fullPage,
  format: type,
  quality: type === "jpeg" ? 85 : undefined,
});
```

How can I resolve this? If you propose a fix, please make it concise.

---

This is a comment left during a code review.
Path: extensions/browser/src/browser/hyprland-capture.ts
Line: 417-419

Comment:
**No process-exit cleanup for headless compositor output**

The `browser-capture` headless output is never removed on process exit (`SIGTERM`, crash, normal exit). On next run `ensureCaptureOutput` will find and reuse it, so it is not permanently leaked, but the dangling monitor is visible to users in their Hyprland session.

Consider registering exit handlers that call `teardownHeadedBrowserViewportCapture(cachedCapture)` once, lazily, when the first capture is set up.

How can I resolve this? If you propose a fix, please make it concise.

Reviews (1): Last reviewed commit: "fix(browser): fall back to Hyprland grim..." | Re-trigger Greptile

Comment on lines +421 to +449
export async function tryHyprlandViewportCapture(params: {
browserPid: number;
timeoutMs?: number;
}): Promise<Buffer | null> {
if (process.platform !== "linux") return null;
try {
if (cachedCapture && cachedPid !== params.browserPid) {
await teardownHeadedBrowserViewportCapture(cachedCapture).catch(() => {});
cachedCapture = null;
cachedPid = null;
}
if (!cachedCapture) {
const session = await detectHyprlandSession();
if (!session) return null;
cachedCapture = await setupHeadedBrowserViewportCapture({
browserPid: params.browserPid,
session,
});
cachedPid = params.browserPid;
}
return await captureViewportPng({
capture: cachedCapture,
timeoutMs: params.timeoutMs ?? 2000,
});
} catch {
cachedCapture = null;
cachedPid = null;
return null;
}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

P1 Race condition in singleton setup

Because detectHyprlandSession() and setupHeadedBrowserViewportCapture() both contain await points, two concurrent screenshot requests that both enter the if (!cachedCapture) branch will each proceed through setup independently. Both dispatch movetoworkspacesilent for the same PID; the second write to cachedCapture silently overwrites the first, leaving the first capture object abandoned without teardown.

Fix: deduplicate concurrent callers with a shared in-flight promise:

let setupInFlight: Promise<HeadedBrowserViewportCapture | null> | null = null;

if (!cachedCapture) {
  if (!setupInFlight) {
    setupInFlight = (async () => {
      const session = await detectHyprlandSession();
      if (!session) return null;
      return await setupHeadedBrowserViewportCapture({ browserPid: params.browserPid, session });
    })().finally(() => { setupInFlight = null; });
  }
  const capture = await setupInFlight;
  if (!capture) return null;
  cachedCapture = capture;
  cachedPid = params.browserPid;
}
Prompt To Fix With AI
This is a comment left during a code review.
Path: extensions/browser/src/browser/hyprland-capture.ts
Line: 421-449

Comment:
**Race condition in singleton setup**

Because `detectHyprlandSession()` and `setupHeadedBrowserViewportCapture()` both contain `await` points, two concurrent screenshot requests that both enter the `if (!cachedCapture)` branch will each proceed through setup independently. Both dispatch `movetoworkspacesilent` for the same PID; the second write to `cachedCapture` silently overwrites the first, leaving the first capture object abandoned without teardown.

Fix: deduplicate concurrent callers with a shared in-flight promise:

```ts
let setupInFlight: Promise<HeadedBrowserViewportCapture | null> | null = null;

if (!cachedCapture) {
  if (!setupInFlight) {
    setupInFlight = (async () => {
      const session = await detectHyprlandSession();
      if (!session) return null;
      return await setupHeadedBrowserViewportCapture({ browserPid: params.browserPid, session });
    })().finally(() => { setupInFlight = null; });
  }
  const capture = await setupInFlight;
  if (!capture) return null;
  cachedCapture = capture;
  cachedPid = params.browserPid;
}
```

How can I resolve this? If you propose a fix, please make it concise.

Comment on lines 378 to 389
: null;
if (hyprlandPng) {
buffer = hyprlandPng;
} else {
buffer = await captureScreenshot({
wsUrl: tab.wsUrl ?? "",
fullPage,
format: type,
quality: type === "jpeg" ? 85 : undefined,
});
}
}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

P1 PNG/JPEG format mismatch when type === "jpeg"

grim always outputs PNG. When type === "jpeg" and the screenshot fits within normalizeBrowserScreenshot's limits (< 5 MB, dimensions ≤ 2000 px — the typical case for a 1920×1080 viewport), normalizeBrowserScreenshot returns { buffer } with no contentType. The fallback image/${params.type} evaluates to "image/jpeg", so the saved file is labeled as JPEG despite containing raw PNG bytes.

Fix: override type to "png" for the Hyprland branch:

if (hyprlandPng) {
  await saveNormalizedScreenshotResponse({
    res,
    buffer: hyprlandPng,
    type: "png", // grim always produces PNG
    targetId: tab.targetId,
    url: tab.url,
  });
  return;
}
buffer = await captureScreenshot({
  wsUrl: tab.wsUrl ?? "",
  fullPage,
  format: type,
  quality: type === "jpeg" ? 85 : undefined,
});
Prompt To Fix With AI
This is a comment left during a code review.
Path: extensions/browser/src/browser/routes/agent.snapshot.ts
Line: 378-389

Comment:
**PNG/JPEG format mismatch when `type === "jpeg"`**

`grim` always outputs PNG. When `type === "jpeg"` and the screenshot fits within `normalizeBrowserScreenshot`'s limits (< 5 MB, dimensions ≤ 2000 px — the typical case for a 1920×1080 viewport), `normalizeBrowserScreenshot` returns `{ buffer }` with no `contentType`. The fallback `image/${params.type}` evaluates to `"image/jpeg"`, so the saved file is labeled as JPEG despite containing raw PNG bytes.

Fix: override `type` to `"png"` for the Hyprland branch:

```ts
if (hyprlandPng) {
  await saveNormalizedScreenshotResponse({
    res,
    buffer: hyprlandPng,
    type: "png", // grim always produces PNG
    targetId: tab.targetId,
    url: tab.url,
  });
  return;
}
buffer = await captureScreenshot({
  wsUrl: tab.wsUrl ?? "",
  fullPage,
  format: type,
  quality: type === "jpeg" ? 85 : undefined,
});
```

How can I resolve this? If you propose a fix, please make it concise.

Comment on lines +417 to +419
// ── Singleton session cache ─────────────────────────────────────────────
let cachedCapture: HeadedBrowserViewportCapture | null = null;
let cachedPid: number | null = null;

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

P2 No process-exit cleanup for headless compositor output

The browser-capture headless output is never removed on process exit (SIGTERM, crash, normal exit). On next run ensureCaptureOutput will find and reuse it, so it is not permanently leaked, but the dangling monitor is visible to users in their Hyprland session.

Consider registering exit handlers that call teardownHeadedBrowserViewportCapture(cachedCapture) once, lazily, when the first capture is set up.

Prompt To Fix With AI
This is a comment left during a code review.
Path: extensions/browser/src/browser/hyprland-capture.ts
Line: 417-419

Comment:
**No process-exit cleanup for headless compositor output**

The `browser-capture` headless output is never removed on process exit (`SIGTERM`, crash, normal exit). On next run `ensureCaptureOutput` will find and reuse it, so it is not permanently leaked, but the dangling monitor is visible to users in their Hyprland session.

Consider registering exit handlers that call `teardownHeadedBrowserViewportCapture(cachedCapture)` once, lazily, when the first capture is set up.

How can I resolve this? If you propose a fix, please make it concise.

@Angfr95
Angfr95 force-pushed the fix/hyprland-headed-viewport-screenshot-timeout branch from 0c13d0d to 0b586d1 Compare April 15, 2026 11:50

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 0b586d1134

ℹ️ About Codex in GitHub

Codex has been enabled to automatically review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

When you sign up for Codex through ChatGPT, Codex can also answer questions or update the PR, like "@codex address that feedback".

Comment on lines +433 to +435
if (cachedCapture && cachedPid !== params.browserPid) {
await teardownHeadedBrowserViewportCapture(cachedCapture).catch(() => {});
cachedCapture = null;

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

P1 Badge Serialize teardown with capture for mixed-profile screenshots

Guarding only setup with setupLock leaves capture and teardown unsynchronized: if a screenshot for PID A is already in captureViewportPng, a concurrent request for PID B will execute teardownHeadedBrowserViewportCapture on the shared output immediately. That can invalidate the in-flight grim capture for PID A, causing an avoidable fallback to the original CDP timeout path and flaky failures whenever two profiles are snapshotted in parallel.

Useful? React with 👍 / 👎.

Comment on lines +458 to +460
} catch {
cachedCapture = null;
cachedPid = null;

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

P2 Badge Tear down Hyprland output on capture fallback errors

The error path clears cachedCapture/cachedPid and returns null without cleaning up a headless output that may already have been created (and possibly had the browser moved onto it). After transient setup/capture failures, this can leave browser-capture state behind until a later successful PID-switch teardown happens, so a one-off failure can persistently alter compositor state.

Useful? React with 👍 / 👎.

@Angfr95
Angfr95 force-pushed the fix/hyprland-headed-viewport-screenshot-timeout branch from 0b586d1 to ab78955 Compare April 15, 2026 12:01

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: d5d3cd3363

ℹ️ About Codex in GitHub

Codex has been enabled to automatically review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

When you sign up for Codex through ChatGPT, Codex can also answer questions or update the PR, like "@codex address that feedback".

Comment on lines +433 to +437
if (cachedCapture && cachedPid !== params.browserPid) {
await teardownHeadedBrowserViewportCapture(cachedCapture).catch(() => {});
cachedCapture = null;
cachedPid = null;
}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

P2 Badge Clean up cached Hyprland output when capture session ends

browser-capture teardown currently runs only when another screenshot arrives with a different PID (or when a capture error is thrown), so a successful screenshot followed by browser stop/restart can leave the headless Hyprland output and moved window state behind indefinitely. This makes /screenshot introduce persistent compositor side effects across runs instead of being self-contained; adding lifecycle cleanup outside the PID-mismatch path (for example on profile stop/shutdown) would prevent orphaned outputs.

Useful? React with 👍 / 👎.

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: dcfabcdae5

ℹ️ About Codex in GitHub

Codex has been enabled to automatically review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

When you sign up for Codex through ChatGPT, Codex can also answer questions or update the PR, like "@codex address that feedback".

? Math.max(250, Math.min(10000, Math.floor(params.timeoutMs)))
: DEFAULT_CAPTURE_TIMEOUT_MS;

const png = await execBuffer(grim, ["-o", params.capture.outputName, "-l", "0", "-"], {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

P1 Badge Capture only tab viewport in Hyprland fallback

captureViewportPng runs grim -o <output> and the route returns that buffer as the /screenshot result, which captures the full compositor output rather than the page viewport that Page.captureScreenshot normally returns. On headed Linux this can include browser chrome and non-content pixels (for example when the window is not exactly full-output), so downstream consumers that rely on viewport-faithful screenshots for visual reasoning or click targeting get incorrect imagery whenever the Hyprland fallback path is taken.

Useful? React with 👍 / 👎.

@Angfr95
Angfr95 force-pushed the fix/hyprland-headed-viewport-screenshot-timeout branch from dcfabcd to 2f41e4d Compare April 24, 2026 04:23

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 2f41e4d6c2

ℹ️ About Codex in GitHub

Codex has been enabled to automatically review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

When you sign up for Codex through ChatGPT, Codex can also answer questions or update the PR, like "@codex address that feedback".

: null;
const hyprlandPng =
browserPid && browserPid > 0
? await tryHyprlandViewportCapture({ browserPid, timeoutMs: 3000 })

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

P1 Badge Bind Hyprland screenshot to the requested target tab

The Hyprland branch calls tryHyprlandViewportCapture with only browserPid, so this path has no tab-level selector and can only capture whatever window/tab is currently visible for that PID. Because /screenshot accepts targetId and ensureTabAvailable(targetId) does not activate/focus that tab, background-tab requests can return an image from a different tab/window, which can mislead later automation steps.

Useful? React with 👍 / 👎.

Comment on lines +301 to +304
let monitor = findMonitorByName(
await runHyprctlJson<HyprlandMonitor[]>(session, ["monitors", "all"]),
outputName,
);

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

P2 Badge Avoid reusing arbitrary Hyprland outputs by fixed name

ensureCaptureOutput reuses any existing monitor named outputName instead of tracking whether this code created it. With the default fixed name (browser-capture), a screenshot request can attach to an output created by another OpenClaw process or user workflow, then reconfigure it and later remove it during teardown, causing cross-session compositor disruption. Use a unique per-process output name and ownership tracking before mutating/removing outputs.

Useful? React with 👍 / 👎.

@steipete

Copy link
Copy Markdown
Contributor

Codex deep review: real bug, current-path PR, but I would not merge this as-is.

What it solves: #64317 reports headed managed Chromium on Omarchy/Hyprland/Wayland loading pages successfully while normal viewport Page.captureScreenshot times out. A Hyprland fake-output + grim capture is a plausible workaround for the local-managed headed profile path, and this PR correctly keeps full-page/element/label paths on the existing screenshot implementations.

Main blockers before merge:

  1. No tests. This changes screenshot routing and adds a new process/desktop integration module, but there is no coverage for:

    • non-Linux/non-Hyprland falls back to CDP;
    • headed local-managed viewport screenshot uses Hyprland capture when available;
    • requested type: "jpeg" returns PNG metadata when the Hyprland branch captures PNG bytes;
    • concurrent screenshot calls only perform one setup;
    • setup failure clears cached state and falls back to CDP.
  2. Use the existing binary resolver instead of a new local trusted-dir list. Core already has src/infra/resolve-system-bin.ts with strict/standard trust semantics and tests. Duplicating that logic in hyprland-capture.ts will drift, and this is exactly the kind of execFile path resolution that helper exists for. If the browser plugin cannot import that directly across boundaries, expose/reuse an existing plugin-safe runtime helper rather than inventing a parallel list.

  3. Make the Hyprland mutation explicit/controllable. The implementation creates/reuses a headless output and moves the managed browser window with hyprctl dispatch movetoworkspacesilent. That is a real desktop-state mutation, not just a capture backend. For a managed OpenClaw profile it may be acceptable, but it should be guarded by a small browser config flag or at least documented in browser troubleshooting before becoming automatic for every headed local-managed Hyprland user.

  4. Add cleanup/lifecycle coverage. The cached output is removed only after capture errors or PID changes. A normal gateway exit can leave browser-capture behind. Register one-shot process-exit cleanup for the cached capture, and add a direct unit test around teardown-on-failure/cache reset.

A couple of notes from dependency/source reading: grim defaults to PNG when no -t is passed, so the current branch’s type = "png" on the Hyprland path is the right direction. Long-term, if we want to honor requested JPEG here, pass -t jpeg to grim and test the returned content type. The current PNG-only fallback is simpler and fine, but it must be covered.

Best shape: keep the feature narrow in extensions/browser/src/browser/routes/agent.snapshot.ts, move process/binary handling behind tested helpers, add route-level tests in extensions/browser/src/browser/routes/agent.snapshot.test.ts, add unit tests for hyprland-capture.ts, and add one troubleshooting note for Hyprland/Omarchy explaining the fake-output behavior and required hyprctl/grim tools.

@Angfr95
Angfr95 force-pushed the fix/hyprland-headed-viewport-screenshot-timeout branch 2 times, most recently from bf673f2 to 1c56966 Compare April 25, 2026 16:52
@openclaw-barnacle openclaw-barnacle Bot added docs Improvements or additions to documentation size: XL and removed size: L labels Apr 25, 2026
@Angfr95
Angfr95 force-pushed the fix/hyprland-headed-viewport-screenshot-timeout branch from 305dfa1 to 1c56966 Compare April 25, 2026 18:10
@openclaw-barnacle openclaw-barnacle Bot added size: L docs Improvements or additions to documentation size: XL and removed docs Improvements or additions to documentation size: XL size: L labels Apr 25, 2026
@Angfr95
Angfr95 force-pushed the fix/hyprland-headed-viewport-screenshot-timeout branch 2 times, most recently from 305dfa1 to bf673f2 Compare April 25, 2026 18:33
@openclaw-barnacle openclaw-barnacle Bot removed docs Improvements or additions to documentation size: XL labels Apr 25, 2026
@openclaw-barnacle openclaw-barnacle Bot added channel: voice-call Channel integration: voice-call channel: whatsapp-web Channel integration: whatsapp-web channel: zalo Channel integration: zalo channel: zalouser Channel integration: zalouser app: android App: android app: ios App: ios app: macos App: macos app: web-ui App: web-ui gateway Gateway runtime extensions: copilot-proxy Extension: copilot-proxy extensions: diagnostics-otel Extension: diagnostics-otel extensions: llm-task Extension: llm-task extensions: lobster Extension: lobster extensions: memory-core Extension: memory-core extensions: memory-lancedb Extension: memory-lancedb extensions: open-prose Extension: open-prose cli CLI command changes scripts Repository scripts commands Command implementations docker Docker and sandbox tooling agents Agent runtime and tooling channel: feishu Channel integration: feishu channel: twitch Channel integration: twitch channel: irc extensions: acpx extensions: anthropic extensions: openai labels Apr 25, 2026
@openclaw-barnacle

Copy link
Copy Markdown

Closing this PR because it looks dirty (too many unrelated or unexpected changes). This usually happens when a branch picks up unrelated commits or a merge went sideways. Please recreate the PR from a clean branch.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

agents Agent runtime and tooling app: android App: android app: ios App: ios app: macos App: macos app: web-ui App: web-ui channel: bluebubbles Channel integration: bluebubbles channel: discord Channel integration: discord channel: feishu Channel integration: feishu channel: googlechat Channel integration: googlechat channel: imessage Channel integration: imessage channel: irc channel: line Channel integration: line channel: matrix Channel integration: matrix channel: mattermost Channel integration: mattermost channel: msteams Channel integration: msteams channel: nextcloud-talk Channel integration: nextcloud-talk channel: nostr Channel integration: nostr channel: qa-channel Channel integration: qa-channel channel: qqbot channel: signal Channel integration: signal channel: slack Channel integration: slack channel: telegram Channel integration: telegram channel: tlon Channel integration: tlon channel: twitch Channel integration: twitch channel: voice-call Channel integration: voice-call channel: whatsapp-web Channel integration: whatsapp-web channel: zalo Channel integration: zalo channel: zalouser Channel integration: zalouser cli CLI command changes commands Command implementations docker Docker and sandbox tooling docs Improvements or additions to documentation extensions: acpx extensions: anthropic extensions: arcee extensions: byteplus extensions: cloudflare-ai-gateway extensions: codex extensions: copilot-proxy Extension: copilot-proxy extensions: deepseek extensions: diagnostics-otel Extension: diagnostics-otel extensions: duckduckgo extensions: fal extensions: gradium extensions: huggingface extensions: kilocode extensions: kimi-coding extensions: litellm extensions: llm-task Extension: llm-task extensions: lmstudio extensions: lobster Extension: lobster extensions: memory-core Extension: memory-core extensions: memory-lancedb Extension: memory-lancedb extensions: memory-wiki extensions: minimax extensions: moonshot extensions: nvidia extensions: open-prose Extension: open-prose extensions: openai extensions: qa-lab extensions: qianfan extensions: senseaudio extensions: stepfun extensions: synthetic extensions: tavily extensions: tencent extensions: together extensions: tokenjuice Changes to the bundled tokenjuice extension extensions: tts-local-cli extensions: venice extensions: vercel-ai-gateway extensions: volcengine extensions: webhooks extensions: xiaomi gateway Gateway runtime plugin: bonjour Plugin integration: bonjour plugin: google-meet scripts Repository scripts size: XL

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[Bug]: Headed Chromium viewport screenshots time out on Omarchy/Hyprland/Wayland after successful page load