-
- {certificateError ? : }
-
-
- {certificateError
- ? translate('browser.loadFailure.connectionNotSecure', "Connection isn't secure")
- : guestRecoveryError
- ? translate('browser.guestRecovery.title', 'Browser page stopped')
- : meta.host
- ? translate('browser.loadFailure.cantReachHost', "Can't reach {{value0}}", {
- value0: meta.host
- })
- : translate('browser.loadFailure.cantLoadPage', "Can't load this page")}
-
-
- {formatLoadFailureDescription(presentationLoadError, meta)}
-
- {certificateError && meta.isLocalhostLike ? (
-
- {translate(
- 'browser.loadFailure.trustedCertificateGuidance',
- 'For local development, use a trusted local certificate when possible.'
- )}
+
+
+
+
+ {certificateError ? : }
+
+
+ {certificateError
+ ? translate('browser.loadFailure.connectionNotSecure', "Connection isn't secure")
+ : guestRecoveryError
+ ? translate('browser.guestRecovery.title', 'Browser page stopped')
+ : meta.host
+ ? translate('browser.loadFailure.cantReachHost', "Can't reach {{value0}}", {
+ value0: meta.host
+ })
+ : translate('browser.loadFailure.cantLoadPage', "Can't load this page")}
+
+
+ {formatLoadFailureDescription(presentationLoadError, meta)}
- ) : null}
- {recoveryHint ?
{recoveryHint}
: null}
- {sshRoutedHint ? (
-
- {translate(
- 'browser.loadFailure.sshRoutedHint',
- "This page browses through the workspace's SSH host — the host may be disconnected or unable to reach this site."
- )}
- {onRecheckSshRoute ? (
+ {certificateError && meta.isLocalhostLike ? (
+
+ {translate(
+ 'browser.loadFailure.trustedCertificateGuidance',
+ 'For local development, use a trusted local certificate when possible.'
+ )}
+
+ ) : null}
+ {recoveryHint ? (
+
{recoveryHint}
+ ) : null}
+ {sshRoutedHint ? (
+
+ {translate(
+ 'browser.loadFailure.sshRoutedHint',
+ "This page browses through the workspace's SSH host — the host may be disconnected or unable to reach this site."
+ )}
+ {onRecheckSshRoute ? (
+ <>
+ {' '}
+
+ {translate('browser.loadFailure.recheckSshRoute', 'Run connection check')}
+
+ >
+ ) : null}
+
+ ) : null}
+ {activeProceedAttempt?.state === 'failed' && activeProceedAttempt.reason ? (
+
+ {formatCertificateProceedFailure(activeProceedAttempt.reason)}
+
+ ) : null}
+
+ {certificateError ? (
<>
- {' '}
-
- {translate('browser.loadFailure.recheckSshRoute', 'Run connection check')}
-
+ {externalButton}
+ {retryButton}
+ {copyButton}
+ {matchingCertificateFailure ? (
+
+ {activeProceedAttempt?.state === 'submitting' &&
+ activeProceedAttempt.showConnecting ? (
+ <>
+
+ {translate('browser.loadFailure.connecting', 'Connecting…')}
+ >
+ ) : (
+ translate('browser.loadFailure.proceedUnsafe', 'Proceed Anyway (Unsafe)')
+ )}
+
+ ) : null}
>
- ) : null}
-
- ) : null}
- {activeProceedAttempt?.state === 'failed' && activeProceedAttempt.reason ? (
-
- {formatCertificateProceedFailure(activeProceedAttempt.reason)}
-
- ) : null}
-
- {certificateError ? (
- <>
- {externalButton}
- {retryButton}
- {copyButton}
- {matchingCertificateFailure ? (
-
- {activeProceedAttempt?.state === 'submitting' &&
- activeProceedAttempt.showConnecting ? (
- <>
-
- {translate('browser.loadFailure.connecting', 'Connecting…')}
- >
- ) : (
- translate('browser.loadFailure.proceedUnsafe', 'Proceed Anyway (Unsafe)')
- )}
-
- ) : null}
- >
- ) : (
- <>
- {httpsRecoveryUrl && !guestRecoveryError ? (
- onTryHttps(httpsRecoveryUrl)}
- >
- {translate('browser.loadFailure.tryHttps', 'Try HTTPS')}
-
- ) : null}
- {retryButton}
- {copyButton}
- {externalButton}
- >
- )}
+ ) : (
+ <>
+ {httpsRecoveryUrl && !guestRecoveryError ? (
+ onTryHttps(httpsRecoveryUrl)}
+ >
+ {translate('browser.loadFailure.tryHttps', 'Try HTTPS')}
+
+ ) : null}
+ {retryButton}
+ {copyButton}
+ {externalButton}
+ >
+ )}
+
-
+
)
}
diff --git a/src/renderer/src/components/editor/IpynbViewer.tsx b/src/renderer/src/components/editor/IpynbViewer.tsx
index 03beb68b122..e5283b7cb85 100644
--- a/src/renderer/src/components/editor/IpynbViewer.tsx
+++ b/src/renderer/src/components/editor/IpynbViewer.tsx
@@ -255,7 +255,7 @@ export default function IpynbViewer({
}
}}
>
-
+
{translate('auto.components.editor.IpynbViewer.9e06ae5d36', 'Run Notebook Code?')}
diff --git a/src/renderer/src/components/native-chat/NativeChatApprovalCard.tsx b/src/renderer/src/components/native-chat/NativeChatApprovalCard.tsx
index 2918058b790..7dd0c988b09 100644
--- a/src/renderer/src/components/native-chat/NativeChatApprovalCard.tsx
+++ b/src/renderer/src/components/native-chat/NativeChatApprovalCard.tsx
@@ -1,3 +1,4 @@
+import { SecuritySurface } from '@/components/ui/security-surface'
import { useRef } from 'react'
import { ShieldQuestion, X } from 'lucide-react'
import { cn } from '@/lib/utils'
@@ -60,133 +61,136 @@ export function NativeChatApprovalCard({
const escape = onCancel ?? (isSubmitting ? undefined : onCollapse)
return (
-
-
-
{
- if (event.key === 'Escape' && !event.nativeEvent.isComposing && escape) {
- event.preventDefault()
- event.stopPropagation()
- escape()
- }
- }}
- className="flex min-h-0 w-full flex-1 flex-col gap-2 overflow-hidden rounded-lg border border-input bg-card px-4 py-3 shadow-xs focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring"
- >
-
-
-
- {onCancel ? (
-
-
-
- ) : onCollapse ? (
-
- ) : null}
-
- {hasContext ? (
-
- {approval.description ? (
-
{approval.description}
- ) : null}
- {approval.decisionReason ? (
-
-
- {translate('components.native-chat.approval.reason', 'Reason')}:{' '}
-
- {approval.decisionReason}
+
+
+
+
{
+ if (event.key === 'Escape' && !event.nativeEvent.isComposing && escape) {
+ event.preventDefault()
+ event.stopPropagation()
+ escape()
+ }
+ }}
+ className="flex min-h-0 w-full flex-1 flex-col gap-2 overflow-hidden rounded-lg border border-input bg-card px-4 py-3 shadow-xs focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring"
+ >
+
+
+
+
+ {approval.title}
- ) : null}
- {neededPath ? (
-
-
- {translate('components.native-chat.approval.needsAccess', 'Needs access to')}
- :{' '}
-
- {neededPath}
-
- ) : null}
- {isPlanApprovalSubject(approval.subject) ? (
-
-
- {approval.subject.filePath ? (
-
-
- {translate('components.native-chat.approval.plan.file', 'Plan file')}:{' '}
-
- {approval.subject.filePath}
-
- ) : null}
-
- ) : approval.detail ? (
-
+ {onCancel ? (
+
- {approval.detail}
-
- ) : null}
- {newerSubject ? (
-
- {translate(
- 'components.native-chat.approval.needsNewerOrca',
- 'This request needs a newer version of Orca.'
- )}
-
+
+
+ ) : onCollapse ? (
+
) : null}
- ) : null}
-
- {approval.options.map((opt, i) => (
-
onChoose(opt.send)}
- className={cn(
- 'rounded-md px-4 py-1.5 text-sm font-semibold transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring disabled:pointer-events-none disabled:opacity-50',
- i === 0
- ? 'bg-primary text-primary-foreground hover:bg-primary/90'
- : 'border border-border bg-background text-foreground hover:bg-accent'
- )}
+ {hasContext ? (
+
- {opt.label}
-
- ))}
+ {approval.description ? (
+
{approval.description}
+ ) : null}
+ {approval.decisionReason ? (
+
+
+ {translate('components.native-chat.approval.reason', 'Reason')}:{' '}
+
+ {approval.decisionReason}
+
+ ) : null}
+ {neededPath ? (
+
+
+ {translate('components.native-chat.approval.needsAccess', 'Needs access to')}
+ :{' '}
+
+ {neededPath}
+
+ ) : null}
+ {isPlanApprovalSubject(approval.subject) ? (
+
+
+ {approval.subject.filePath ? (
+
+
+ {translate('components.native-chat.approval.plan.file', 'Plan file')}
+ :{' '}
+
+ {approval.subject.filePath}
+
+ ) : null}
+
+ ) : approval.detail ? (
+
+ {approval.detail}
+
+ ) : null}
+ {newerSubject ? (
+
+ {translate(
+ 'components.native-chat.approval.needsNewerOrca',
+ 'This request needs a newer version of Orca.'
+ )}
+
+ ) : null}
+
+ ) : null}
+
+ {approval.options.map((opt, i) => (
+ onChoose(opt.send)}
+ className={cn(
+ 'rounded-md px-4 py-1.5 text-sm font-semibold transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring disabled:pointer-events-none disabled:opacity-50',
+ i === 0
+ ? 'bg-primary text-primary-foreground hover:bg-primary/90'
+ : 'border border-border bg-background text-foreground hover:bg-accent'
+ )}
+ >
+ {opt.label}
+
+ ))}
+
-
+
)
}
diff --git a/src/renderer/src/components/settings/PluginConsentDialog.tsx b/src/renderer/src/components/settings/PluginConsentDialog.tsx
index f0b39d05ba1..7b0eaca54b2 100644
--- a/src/renderer/src/components/settings/PluginConsentDialog.tsx
+++ b/src/renderer/src/components/settings/PluginConsentDialog.tsx
@@ -134,7 +134,9 @@ export function PluginConsentDialog({
}}
>
{
// Why: dismissal is the safety-preserving path, so it receives initial focus.
event.preventDefault()
diff --git a/src/renderer/src/components/settings/PluginInstallDialog.tsx b/src/renderer/src/components/settings/PluginInstallDialog.tsx
index 1b13bea59e1..b9786db960a 100644
--- a/src/renderer/src/components/settings/PluginInstallDialog.tsx
+++ b/src/renderer/src/components/settings/PluginInstallDialog.tsx
@@ -79,7 +79,7 @@ export function PluginInstallDialog({
return (
!installing && onOpenChange(nextOpen)}>
-
+
{translate('auto.components.settings.PluginInstallDialog.title', 'Install plugin')}
diff --git a/src/renderer/src/components/settings/PluginMarketplacePreviewDialog.tsx b/src/renderer/src/components/settings/PluginMarketplacePreviewDialog.tsx
index 6f7e34fa657..cdc3c35bde4 100644
--- a/src/renderer/src/components/settings/PluginMarketplacePreviewDialog.tsx
+++ b/src/renderer/src/components/settings/PluginMarketplacePreviewDialog.tsx
@@ -152,7 +152,7 @@ export function PluginMarketplacePreviewDialog({
: undefined
return (
!open && !busy && onClose()}>
-
+
{preview ? (
<>
diff --git a/src/renderer/src/components/settings/PluginMarketplaceSourceDialog.tsx b/src/renderer/src/components/settings/PluginMarketplaceSourceDialog.tsx
index 89c901ff2a8..85391956270 100644
--- a/src/renderer/src/components/settings/PluginMarketplaceSourceDialog.tsx
+++ b/src/renderer/src/components/settings/PluginMarketplaceSourceDialog.tsx
@@ -121,7 +121,9 @@ export function PluginMarketplaceSourceDialog({
return (
!busyAction && onOpenChange(nextOpen)}>
{
event.preventDefault()
urlRef.current?.focus()
diff --git a/src/renderer/src/components/settings/PluginRemoveDialog.tsx b/src/renderer/src/components/settings/PluginRemoveDialog.tsx
index 3cae9dd9aaf..f9829e66470 100644
--- a/src/renderer/src/components/settings/PluginRemoveDialog.tsx
+++ b/src/renderer/src/components/settings/PluginRemoveDialog.tsx
@@ -29,6 +29,7 @@ export function PluginRemoveDialog({
return (
!open && !busy && onCancel()}>
{
event.preventDefault()
cancelRef.current?.focus()
diff --git a/src/renderer/src/components/settings/PluginRollbackDialog.tsx b/src/renderer/src/components/settings/PluginRollbackDialog.tsx
index f8693f4829e..21a03bf12ad 100644
--- a/src/renderer/src/components/settings/PluginRollbackDialog.tsx
+++ b/src/renderer/src/components/settings/PluginRollbackDialog.tsx
@@ -31,6 +31,7 @@ export function PluginRollbackDialog({
return (
!open && !busy && onCancel()}>
{
event.preventDefault()
cancelRef.current?.focus()
diff --git a/src/renderer/src/components/sidebar/OrcaYamlTrustDialog.tsx b/src/renderer/src/components/sidebar/OrcaYamlTrustDialog.tsx
index 60ea9bcf90f..a14a8990f81 100644
--- a/src/renderer/src/components/sidebar/OrcaYamlTrustDialog.tsx
+++ b/src/renderer/src/components/sidebar/OrcaYamlTrustDialog.tsx
@@ -104,7 +104,7 @@ const OrcaYamlTrustDialog = React.memo(function OrcaYamlTrustDialog() {
return (
-
+
{previouslyApproved
diff --git a/src/renderer/src/components/ui/dialog.tsx b/src/renderer/src/components/ui/dialog.tsx
index b19a1c769dc..2763406ee92 100644
--- a/src/renderer/src/components/ui/dialog.tsx
+++ b/src/renderer/src/components/ui/dialog.tsx
@@ -1,5 +1,6 @@
'use client'
+import { SECURITY_SURFACE_CLASS } from './security-surface'
import * as React from 'react'
import { cva, type VariantProps } from 'class-variance-authority'
import { Minus, XIcon } from 'lucide-react'
@@ -54,7 +55,9 @@ const dialogContentSurfaceVariants = cva('', {
'feature-tip':
'gap-0 bg-[color-mix(in_srgb,var(--foreground)_8%,var(--background))] p-0 dark:bg-[color-mix(in_srgb,var(--foreground)_16%,var(--background))]',
/** Edge to edge, for an embedded panel that brings its own header and padding. */
- panel: 'gap-0 p-0'
+ panel: 'gap-0 p-0',
+ /** Trust, consent and provenance decisions: host-owned tokens a plugin theme cannot restyle. */
+ security: SECURITY_SURFACE_CLASS
}
},
defaultVariants: { surface: 'default' }
@@ -94,6 +97,7 @@ function DialogContent({
closeButtonVariant = 'close',
surface,
size,
+ scrollable = false,
...props
}: React.ComponentProps &
VariantProps & {
@@ -102,6 +106,8 @@ function DialogContent({
closeButtonVariant?: 'close' | 'minimize'
/** `compact` is a short confirmation's width, like the close prompts. */
size?: 'default' | 'compact'
+ /** The whole dialog scrolls within the viewport instead of growing past it. */
+ scrollable?: boolean
}) {
return (
@@ -120,6 +126,7 @@ function DialogContent({
className={cn(
'fixed top-[50%] left-[50%] z-50 grid w-full max-w-[calc(100%-2rem)] grid-cols-[minmax(0,1fr)] translate-x-[-50%] translate-y-[-50%] gap-4 rounded-lg border border-black/14 bg-background/96 p-6 text-foreground shadow-[0_20px_60px_rgba(0,0,0,0.28),inset_0_1px_0_rgba(255,255,255,0.08)] backdrop-blur-2xl duration-200 outline-none dark:border-white/14 dark:bg-[rgba(23,23,23,0.96)] dark:shadow-[0_24px_72px_rgba(0,0,0,0.55),inset_0_1px_0_rgba(255,255,255,0.06)] data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=closed]:zoom-out-95 data-[state=open]:animate-in data-[state=open]:fade-in-0 data-[state=open]:zoom-in-95 sm:max-w-lg data-[size=compact]:sm:max-w-sm',
dialogContentSurfaceVariants({ surface }),
+ scrollable && 'max-h-[calc(100vh-3rem)] overflow-y-auto scrollbar-sleek',
className
)}
{...props}
diff --git a/src/renderer/src/components/ui/security-surface.test.tsx b/src/renderer/src/components/ui/security-surface.test.tsx
new file mode 100644
index 00000000000..9a6e8acc8ee
--- /dev/null
+++ b/src/renderer/src/components/ui/security-surface.test.tsx
@@ -0,0 +1,61 @@
+// @vitest-environment happy-dom
+import { readFileSync } from 'node:fs'
+import { join } from 'node:path'
+import { cleanup, render } from '@testing-library/react'
+import { afterEach, describe, expect, it } from 'vitest'
+import { Dialog, DialogContent, DialogDescription, DialogTitle } from './dialog'
+import { SecuritySurface } from './security-surface'
+
+// Every dialog that asks the user to trust, approve, run or remove code. A new one belongs here.
+const TRUST_DIALOGS = [
+ 'sidebar/OrcaYamlTrustDialog.tsx',
+ 'editor/IpynbViewer.tsx',
+ 'settings/PluginConsentDialog.tsx',
+ 'settings/PluginInstallDialog.tsx',
+ 'settings/PluginMarketplacePreviewDialog.tsx',
+ 'settings/PluginMarketplaceSourceDialog.tsx',
+ 'settings/PluginRemoveDialog.tsx',
+ 'settings/PluginRollbackDialog.tsx'
+]
+
+// Trust decisions made outside a dialog: agent command approvals and certificate overrides.
+const INLINE_TRUST_SURFACES = [
+ 'native-chat/NativeChatApprovalCard.tsx',
+ 'browser-pane/navigate/browser-load-failure-overlay.tsx'
+]
+
+afterEach(cleanup)
+
+describe('security surfaces', () => {
+ it('applies the host-owned token set', () => {
+ render(
+
+
+ Trust
+ Review
+
+
+ )
+ expect(document.querySelector('[role="dialog"]')?.classList).toContain('plugin-security-chrome')
+ })
+
+ it.each(TRUST_DIALOGS)('%s renders on the security surface', (file) => {
+ const source = readFileSync(join(import.meta.dirname, '..', file), 'utf8')
+ expect(source).toMatch(/]*surface="security"/)
+ expect(source).not.toContain('plugin-security-chrome')
+ })
+
+ it('applies the host-owned token set outside dialogs', () => {
+ const { container } = render(
+
+ Allow
+
+ )
+ expect(container.firstElementChild?.classList).toContain('plugin-security-chrome')
+ })
+
+ it.each(INLINE_TRUST_SURFACES)('%s renders inside a security surface', (file) => {
+ const source = readFileSync(join(import.meta.dirname, '..', file), 'utf8')
+ expect(source).toMatch(/]/)
+ })
+})
diff --git a/src/renderer/src/components/ui/security-surface.tsx b/src/renderer/src/components/ui/security-surface.tsx
new file mode 100644
index 00000000000..ccf2a741e09
--- /dev/null
+++ b/src/renderer/src/components/ui/security-surface.tsx
@@ -0,0 +1,15 @@
+import type React from 'react'
+import * as Slot from 'radix-ui/slot'
+
+/** Host-owned tokens a plugin theme cannot restyle; see `.plugin-security-chrome` in main.css. */
+export const SECURITY_SURFACE_CLASS = 'plugin-security-chrome'
+
+/** Puts its one child element on the host-owned token surface. For trust decisions outside a
+ * dialog (an agent approval, a certificate override); dialogs use `surface="security"`. */
+export function SecuritySurface({ children }: { children: React.ReactElement }): React.JSX.Element {
+ return (
+
+ {children}
+
+ )
+}