Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -11,21 +11,16 @@ nonisolated enum PaneActionCatalog: ActionCatalogGroup {
arguments: [CatalogArgument.cwdString.optional, CatalogArgument.keepBool.optional], targets: [.pane], cliName: "pane split-right", mainMenu: .view, startsTerminal: true
),
// A PNG of the pane as on screen, never changing focus or selection
// (ScreenshotHandlers): Copy menu and palette; automation is refused.
{
var screenshot = ActionDescriptor(
id: "pane.screenshot",
title: String(localized: "action.pane.screenshot", defaultValue: "Screenshot Pane", bundle: .module),
keywords: ["screenshot", "capture", "image", "png", "picture"], category: .pane, symbol: "camera",
surfaces: [.palette, .contextMenu], arguments: [CatalogArgument.screenshotOut], targets: [.pane],
cliName: "pane screenshot",
surfacePlan: ActionSurfacePlan(cli: .offered, contextMenus: [ActionSurfaceCatalog.p(.pane, .inspect, 510, folder: .copy)])
)
// A pane can contain browser pixels. Automation must use the browser host's
// guarded capture, which masks cross-session secrets before it returns pixels.
screenshot.isPersonOnly = true
return screenshot
}(),
// (ScreenshotHandlers): Copy menu, palette and automation. An automation run's
// image has every web page the app does not own painted over (ScreenshotPrivacyMask).
ActionDescriptor(
id: "pane.screenshot",
title: String(localized: "action.pane.screenshot", defaultValue: "Screenshot Pane", bundle: .module),
keywords: ["screenshot", "capture", "image", "png", "picture"], category: .pane, symbol: "camera",
surfaces: [.palette, .contextMenu], arguments: [CatalogArgument.screenshotOut], targets: [.pane],
cliName: "pane screenshot",
surfacePlan: ActionSurfacePlan(cli: .offered, contextMenus: [ActionSurfaceCatalog.p(.pane, .inspect, 510, folder: .copy)])
),
ActionDescriptor(
id: "newColumn",
title: String(localized: "action.newColumn", defaultValue: "New Column", bundle: .module),
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -375,21 +375,16 @@ nonisolated enum TabActionCatalog: ActionCatalogGroup {
targets: [.tab], cliName: "tab copy-identifiers"
),
// A PNG of the tab's content, never changing focus or selection
// (ScreenshotHandlers): Copy menu and palette; automation is refused.
{
var screenshot = ActionDescriptor(
id: "tab.screenshot",
title: String(localized: "action.tab.screenshot", defaultValue: "Screenshot Tab", bundle: .module),
keywords: ["screenshot", "capture", "image", "png", "picture"], category: .tab, symbol: "camera",
surfaces: [.palette, .contextMenu], arguments: [CatalogArgument.screenshotOut], targets: [.tab],
cliName: "tab screenshot",
surfacePlan: ActionSurfacePlan(cli: .offered, contextMenus: [ActionSurfaceCatalog.p(.tab, .inspect, 510, folder: .copy)])
)
// A tab can be a browser. Automation must use the browser host's guarded
// capture, which masks cross-session secrets before it returns pixels.
screenshot.isPersonOnly = true
return screenshot
}(),
// (ScreenshotHandlers): Copy menu, palette and automation. Automation gets no
// browser tab's pixels here: the browser host's guarded capture masks its secrets.
ActionDescriptor(
id: "tab.screenshot",
title: String(localized: "action.tab.screenshot", defaultValue: "Screenshot Tab", bundle: .module),
keywords: ["screenshot", "capture", "image", "png", "picture"], category: .tab, symbol: "camera",
surfaces: [.palette, .contextMenu], arguments: [CatalogArgument.screenshotOut], targets: [.tab],
cliName: "tab screenshot",
surfacePlan: ActionSurfacePlan(cli: .offered, contextMenus: [ActionSurfaceCatalog.p(.tab, .inspect, 510, folder: .copy)])
),
ActionDescriptor(
id: "palette.copyPaneID",
title: String(localized: "action.palette.copyPaneID", defaultValue: "Copy Pane ID", bundle: .module),
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -76,21 +76,18 @@ nonisolated enum WindowActionCatalog: ActionCatalogGroup {
surfaces: [.palette], cliName: "app minimize-window"
),
// A PNG of the window as on screen, never focusing or raising it
// (a person copies it without Screen Recording; ScreenshotHandlers).
{
var screenshot = ActionDescriptor(
id: "window.screenshot",
title: String(localized: "action.window.screenshot", defaultValue: "Screenshot Window", bundle: .module),
keywords: ["screenshot", "capture", "image", "png", "picture"], category: .window, symbol: "camera",
surfaces: [.palette], arguments: [CatalogArgument.screenshotOut], targets: [.window],
cliName: "window screenshot",
surfacePlan: ActionSurfacePlan(cli: .offered, contextMenuExemption: .noTargetSurface)
)
// A window can contain browser pixels. Automation must use the browser host's
// guarded capture, which masks cross-session secrets before it returns pixels.
screenshot.isPersonOnly = true
return screenshot
}(),
// (agents read the app without Screen Recording; ScreenshotHandlers). An
// automation run's image has every web page the app does not own painted
// over (ScreenshotPrivacyMask): the browser host's guarded capture is the
// only way automation sees a page's pixels.
ActionDescriptor(
id: "window.screenshot",
title: String(localized: "action.window.screenshot", defaultValue: "Screenshot Window", bundle: .module),
keywords: ["screenshot", "capture", "image", "png", "picture"], category: .window, symbol: "camera",
surfaces: [.palette], arguments: [CatalogArgument.screenshotOut], targets: [.window],
cliName: "window screenshot",
surfacePlan: ActionSurfacePlan(cli: .offered, contextMenuExemption: .noTargetSurface)
),
ActionDescriptor(
id: "toggleFullScreen",
title: String(localized: "action.toggleFullScreen", defaultValue: "Toggle Full Screen", bundle: .module),
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -12379,6 +12379,137 @@
}
}
},
"handlers.screenshot.browserNeedsREPL": {
"extractionState": "manual",
"localizations": {
"en": {
"stringUnit": {
"state": "translated",
"value": "A browser page's screenshot for automation comes from the browser REPL, which hides the secrets on the page: browser_repl_eval with page.screenshot()"
}
},
"ar": {
"stringUnit": {
"state": "translated",
"value": "لقطة شاشة صفحة المتصفح للأتمتة تأتي من REPL المتصفح، الذي يخفي الأسرار في الصفحة: browser_repl_eval مع page.screenshot()"
}
},
"bs": {
"stringUnit": {
"state": "translated",
"value": "Snimak ekrana stranice preglednika za automatizaciju dolazi iz REPL-a preglednika, koji skriva tajne na stranici: browser_repl_eval sa page.screenshot()"
}
},
"da": {
"stringUnit": {
"state": "translated",
"value": "Et skærmbillede af en browserside til automatisering kommer fra browserens REPL, som skjuler hemmelighederne på siden: browser_repl_eval med page.screenshot()"
}
},
"de": {
"stringUnit": {
"state": "translated",
"value": "Ein Bildschirmfoto einer Browserseite für die Automatisierung kommt aus der Browser-REPL, die die Geheimnisse auf der Seite verbirgt: browser_repl_eval mit page.screenshot()"
}
},
"es": {
"stringUnit": {
"state": "translated",
"value": "La captura de una página del navegador para la automatización viene del REPL del navegador, que oculta los secretos de la página: browser_repl_eval con page.screenshot()"
}
},
"fr": {
"stringUnit": {
"state": "translated",
"value": "Une capture d’une page du navigateur pour l’automatisation vient du REPL du navigateur, qui masque les secrets de la page : browser_repl_eval avec page.screenshot()"
}
},
"it": {
"stringUnit": {
"state": "translated",
"value": "Lo screenshot di una pagina del browser per l’automazione arriva dal REPL del browser, che nasconde i segreti della pagina: browser_repl_eval con page.screenshot()"
}
},
"ja": {
"stringUnit": {
"state": "translated",
"value": "自動化でのブラウザページのスクリーンショットは、ページ上の秘密情報を隠すブラウザ REPL から取得します: browser_repl_eval で page.screenshot()"
}
},
"km": {
"stringUnit": {
"state": "translated",
"value": "រូបថតអេក្រង់នៃទំព័រកម្មវិធីរុករកសម្រាប់ស្វ័យប្រវត្តិកម្ម មកពី REPL របស់កម្មវិធីរុករក ដែលលាក់អាថ៌កំបាំងនៅលើទំព័រ៖ browser_repl_eval ជាមួយ page.screenshot()"
}
},
"ko": {
"stringUnit": {
"state": "translated",
"value": "자동화용 브라우저 페이지 스크린샷은 페이지의 비밀 정보를 가리는 브라우저 REPL에서 가져옵니다: browser_repl_eval에서 page.screenshot()"
}
},
"nb": {
"stringUnit": {
"state": "translated",
"value": "Et skjermbilde av en nettleserside for automatisering kommer fra nettleserens REPL, som skjuler hemmelighetene på siden: browser_repl_eval med page.screenshot()"
}
},
"pl": {
"stringUnit": {
"state": "translated",
"value": "Zrzut ekranu strony przeglądarki dla automatyzacji pochodzi z REPL przeglądarki, który ukrywa sekrety na stronie: browser_repl_eval z page.screenshot()"
}
},
"pt-BR": {
"stringUnit": {
"state": "translated",
"value": "A captura de uma página do navegador para automação vem do REPL do navegador, que oculta os segredos da página: browser_repl_eval com page.screenshot()"
}
},
"ru": {
"stringUnit": {
"state": "translated",
"value": "Снимок экрана страницы браузера для автоматизации делает REPL браузера, который скрывает секреты на странице: browser_repl_eval с page.screenshot()"
}
},
"th": {
"stringUnit": {
"state": "translated",
"value": "ภาพหน้าจอของหน้าเบราว์เซอร์สำหรับระบบอัตโนมัติมาจาก REPL ของเบราว์เซอร์ ซึ่งซ่อนข้อมูลลับบนหน้า: browser_repl_eval กับ page.screenshot()"
}
},
"tr": {
"stringUnit": {
"state": "translated",
"value": "Otomasyon için bir tarayıcı sayfasının ekran görüntüsü, sayfadaki gizli bilgileri saklayan tarayıcı REPL'inden gelir: page.screenshot() ile browser_repl_eval"
}
},
"uk": {
"stringUnit": {
"state": "translated",
"value": "Знімок екрана сторінки браузера для автоматизації робить REPL браузера, який приховує секрети на сторінці: browser_repl_eval з page.screenshot()"
}
},
"vi": {
"stringUnit": {
"state": "translated",
"value": "Ảnh chụp màn hình trang trình duyệt cho tự động hóa lấy từ REPL của trình duyệt, nơi ẩn các bí mật trên trang: browser_repl_eval với page.screenshot()"
}
},
"zh-Hans": {
"stringUnit": {
"state": "translated",
"value": "自动化获取浏览器页面截图须通过浏览器 REPL,它会隐藏页面上的机密信息:在 browser_repl_eval 中使用 page.screenshot()"
}
},
"zh-Hant": {
"stringUnit": {
"state": "translated",
"value": "自動化取得瀏覽器頁面截圖須透過瀏覽器 REPL,它會隱藏頁面上的機密資訊:在 browser_repl_eval 中使用 page.screenshot()"
}
}
}
},
"handlers.layoutTemplate.delete": {
"extractionState": "manual",
"localizations": {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -16,11 +16,31 @@ import CmuxNextPages
struct AppWindowImage {
let services: AppServices

/// The window's image at its backing scale.
/// - Throws: ``WindowScreenshotFailure`` when the window server gives no image.
func capture(_ window: NSWindow) async throws -> CGImage {
/// The window's image at its backing scale. With `masked` (an automation
/// run) every browser page area is opaque black (``ScreenshotPrivacyMask``),
/// no Chromium browser page image is painted, and the overlay panels and
/// other windows, which can hold web content, are left out.
/// - Throws: ``WindowScreenshotFailure`` when the window server gives no image,
/// or a masked image cannot be painted.
func capture(_ window: NSWindow, masked: Bool = false) async throws -> CGImage {
// The masks are measured before the capture, which can yield to a layout pass.
let masks = masked ? ScreenshotPrivacyMask.rects(in: window, services: services).map(WindowImageLayer.mask) : []
let base = try await window.screenshotImage(includeChild: WindowOverlayHost.isPageWindow)
var layers: [WindowImageLayer] = []
if masked {
layers = masks
// The app's own React pages on Chromium (the agent page, Settings) go back over their page windows' masks.
for (page, rect) in PageRegistry.shownChromiumPages(in: window) where ScreenshotPrivacyMask.isAppOwned(page) {
guard let image = await page.chromiumImage() else { continue }
layers.append(.page(image, rect))
}
if layers.isEmpty { return base }
// A mask that cannot be painted never lets the unmasked image out.
guard let image = WindowImageLayer.composite(base: base, window: window, layers: layers) else {
throw WindowScreenshotFailure.blank
}
return image
}
for (page, rect) in DebugWindowSnapshot.chromiumPages(in: window, services: services) {
// A page that cannot give its image keeps the window server's
// pixels there (its background without the grant).
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,12 @@ import Foundation
/// PNG fits one answer), `width` and `height` (pixels), `scale` (pixels per
/// point) and `method` (`window` or `page`). A person's run (palette, menu)
/// also copies the image.
///
/// An automation run (CLI, MCP, scripts) gets the same image with every web
/// page the app does not own painted black (``ScreenshotPrivacyMask``): the
/// app cannot mask the secrets a page shows the way the browser host does, so
/// a browser tab or a pane showing one is refused and points at the browser
/// REPL, whose capture masks them.
enum ScreenshotHandlers {
/// The capture gave no image; the message says why.
static let unavailableCode = "screenshot.unavailable"
Expand All @@ -33,7 +39,7 @@ enum ScreenshotHandlers {
let output = try ScreenshotOutput(invocation)
guard let window = try context.window(invocation).window else { throw ActionFailure(message: ScreenshotStrings.notOnScreen) }
start(output: output, invocation: invocation, context: context) {
try await shown(window, crop: nil, services: context.services)
try await shown(window, crop: nil, masked: ScreenshotPrivacyMask.applies(to: invocation), services: context.services)
}
})
registry.bind("pane.screenshot", run: { invocation in
Expand All @@ -46,19 +52,23 @@ enum ScreenshotHandlers {
guard let window = pane.view.window, pane.isVisible, !pane.view.isHiddenOrHasHiddenAncestor else {
throw ActionFailure(message: ScreenshotStrings.paneNotShown)
}
let masked = ScreenshotPrivacyMask.applies(to: invocation)
if masked, case .browser? = pane.currentContent { throw ActionFailure(message: ScreenshotStrings.browserNeedsREPL) }
let view = pane.view
start(output: output, invocation: invocation, context: context) {
try await shown(window, crop: view, services: context.services)
try await shown(window, crop: view, masked: masked, services: context.services)
}
})
registry.bind("tab.screenshot", run: { invocation in
let output = try ScreenshotOutput(invocation)
let (tab, controller) = try resolveTab(invocation, services: context.services)
let masked = ScreenshotPrivacyMask.applies(to: invocation)
if masked, tab.kind == .browser { throw ActionFailure(message: ScreenshotStrings.browserNeedsREPL) }
if let controller, let window = controller.view.window, controller.isVisible, controller.currentTabKey == tab.id,
!controller.view.contentHost.isHiddenOrHasHiddenAncestor {
let view = controller.view.contentHost
start(output: output, invocation: invocation, context: context) {
try await shown(window, crop: view, services: context.services)
try await shown(window, crop: view, masked: masked, services: context.services)
}
return
}
Expand Down Expand Up @@ -96,9 +106,10 @@ enum ScreenshotHandlers {
let method: String
}

/// `window` as on screen, cropped to `crop`'s frame when given.
private static func shown(_ window: NSWindow, crop view: NSView?, services: AppServices) async throws -> Capture {
let full = try await AppWindowImage(services: services).capture(window)
/// `window` as on screen, cropped to `crop`'s frame when given; `masked`
/// for an automation run (``ScreenshotPrivacyMask``).
private static func shown(_ window: NSWindow, crop view: NSView?, masked: Bool, services: AppServices) async throws -> Capture {
let full = try await AppWindowImage(services: services).capture(window, masked: masked)
let size = window.frame.size
let scale = size.width > 0 ? Double(full.width) / size.width : Double(window.backingScaleFactor)
guard let view else { return Capture(image: full, scale: scale, method: "window") }
Expand Down
Loading