diff --git a/packages/ui/src/components/instance/instance-shell2.tsx b/packages/ui/src/components/instance/instance-shell2.tsx index 0579e149..2f4e1929 100644 --- a/packages/ui/src/components/instance/instance-shell2.tsx +++ b/packages/ui/src/components/instance/instance-shell2.tsx @@ -1222,6 +1222,10 @@ const InstanceShell2: Component = (props) => {
+ setPermissionModalOpen(true)} + />
= (props) => {
+ setPermissionModalOpen(true)} + />
diff --git a/packages/ui/src/components/permission-approval-modal.tsx b/packages/ui/src/components/permission-approval-modal.tsx index d514cfca..3f899322 100644 --- a/packages/ui/src/components/permission-approval-modal.tsx +++ b/packages/ui/src/components/permission-approval-modal.tsx @@ -1,15 +1,10 @@ -import { Show, createSignal, createMemo, createEffect, onCleanup, type Component } from "solid-js" +import { For, Show, createMemo, createSignal, createEffect, onCleanup, type Component } from "solid-js" import type { PermissionRequestLike } from "../types/permission" -import { getPermissionSessionId, getPermissionKind, getPermissionDisplayTitle, getPermissionMessageId, getPermissionCallId } from "../types/permission" -import { getPermissionQueue, activePermissionId, sendPermissionResponse, setActivePermissionIdForInstance } from "../stores/instances" -import { setActiveSession } from "../stores/session-state" +import { getPermissionCallId, getPermissionDisplayTitle, getPermissionKind, getPermissionMessageId, getPermissionSessionId } from "../types/permission" +import { activePermissionId, getPermissionQueue } from "../stores/instances" +import { loadMessages, setActiveSession } from "../stores/sessions" import { messageStoreBus } from "../stores/message-v2/bus" -import { ToolCallDiffViewer } from "./diff-viewer" -import { useTheme } from "../lib/theme" -import { getRelativePath, getToolIcon, getToolName } from "./tool-call/utils" -import { getLogger } from "../lib/logger" - -const log = getLogger("session") +import ToolCall from "./tool-call" interface PermissionApprovalModalProps { instanceId: string @@ -17,201 +12,113 @@ interface PermissionApprovalModalProps { onClose: () => void } +type ResolvedToolCall = { + messageId: string + sessionId: string + toolPart: Extract + messageVersion: number + partVersion: number +} + +function resolveToolCallFromPermission( + instanceId: string, + permission: PermissionRequestLike, +): ResolvedToolCall | null { + const sessionId = getPermissionSessionId(permission) + const messageId = getPermissionMessageId(permission) + if (!sessionId || !messageId) return null + + const store = messageStoreBus.getInstance(instanceId) + if (!store) return null + + const record = store.getMessage(messageId) + if (!record) return null + + const metadata = ((permission as any).metadata || {}) as Record + const directPartId = + (permission as any).partID ?? + (permission as any).partId ?? + (metadata as any).partID ?? + (metadata as any).partId ?? + undefined + + const callId = getPermissionCallId(permission) + + const findToolPart = (partId: string) => { + const partRecord = record.parts?.[partId] + const part = partRecord?.data + if (!part || part.type !== "tool") return null + return { + toolPart: part as ResolvedToolCall["toolPart"], + partVersion: partRecord.revision ?? 0, + } + } + + if (typeof directPartId === "string" && directPartId.length > 0) { + const resolved = findToolPart(directPartId) + if (resolved) { + return { + messageId, + sessionId, + toolPart: resolved.toolPart, + messageVersion: record.revision, + partVersion: resolved.partVersion, + } + } + } + + if (callId) { + for (const partId of record.partIds) { + const partRecord = record.parts?.[partId] + const part = partRecord?.data as any + if (!part || part.type !== "tool") continue + const partCallId = part.callID ?? part.callId ?? part.toolCallID ?? part.toolCallId ?? undefined + if (partCallId === callId && typeof part.id === "string" && part.id.length > 0) { + return { + messageId, + sessionId, + toolPart: part as ResolvedToolCall["toolPart"], + messageVersion: record.revision, + partVersion: partRecord.revision ?? 0, + } + } + } + } + + return null +} + const PermissionApprovalModal: Component = (props) => { - const { isDark } = useTheme() - const [submitting, setSubmitting] = createSignal(false) - const [error, setError] = createSignal(null) + const [loadingSession, setLoadingSession] = createSignal(null) const queue = createMemo(() => getPermissionQueue(props.instanceId)) const activePermId = createMemo(() => activePermissionId().get(props.instanceId) ?? null) - const activePermission = createMemo((): PermissionRequestLike | null => { - const id = activePermId() - if (!id) return null - return queue().find((p) => p.id === id) ?? null + const orderedQueue = createMemo(() => { + const current = queue() + const activeId = activePermId() + if (!activeId) return current + const index = current.findIndex((entry) => entry.id === activeId) + if (index <= 0) return current + const active = current[index] + if (!active) return current + return [active, ...current.slice(0, index), ...current.slice(index + 1)] }) - const hasActivePermission = createMemo(() => activePermission() !== null) + const hasPermissions = createMemo(() => queue().length > 0) - // Current position in queue - const currentIndex = createMemo(() => { - const perm = activePermission() - if (!perm) return -1 - return queue().findIndex((p) => p.id === perm.id) - }) - - const hasPrev = createMemo(() => currentIndex() > 0) - const hasNext = createMemo(() => currentIndex() < queue().length - 1) - - // Extract tool details - try to get actual tool name from message store first - const toolInfo = createMemo(() => { - const permission = activePermission() - if (!permission) return null - - const metadata = ((permission as any).metadata || {}) as Record - let toolName = "unknown" - - // BEST METHOD: Try to get the actual tool from the linked message part - // This is how the inline chat gets it (via toolPart.tool) - const messageId = getPermissionMessageId(permission) - const callId = getPermissionCallId(permission) - - if (messageId) { - const store = messageStoreBus.getInstance(props.instanceId) - if (store) { - const record = store.getMessage(messageId) - if (record) { - // Search through parts for the tool call matching this permission - for (const partId of record.partIds) { - const partRecord = record.parts[partId] - if (!partRecord?.data || partRecord.data.type !== "tool") continue - - const part = partRecord.data as any - // Match by callId if available - const partCallId = part.callID ?? part.callId ?? part.toolCallID ?? part.toolCallId - if (callId && partCallId === callId && part.tool) { - toolName = part.tool - break - } - // If no callId match, just use the first tool part's name - if (!callId && part.tool) { - toolName = part.tool - break - } - } - } - } - } - - // Fallback: Check metadata fields - if (toolName === "unknown") { - const metaToolName = (metadata.toolName as string) || (metadata.tool as string) || (metadata.action as string) - if (metaToolName) { - toolName = metaToolName.replace(/^opencode_/, "").toLowerCase() - } - } - - // Fallback: Check permission kind for embedded action words - if (toolName === "unknown") { - const kind = getPermissionKind(permission).toLowerCase() - if (kind.includes("read")) toolName = "read" - else if (kind.includes("write")) toolName = "write" - else if (kind.includes("edit")) toolName = "edit" - else if (kind.includes("shell") || kind.includes("bash") || kind.includes("command")) toolName = "bash" - else if (kind.includes("patch")) toolName = "patch" - } - - const command = metadata.command as string | undefined - const filePath = (metadata.filePath as string) || (metadata.path as string) || undefined - const input = metadata.input as Record | undefined - - return { - toolName, - icon: getToolIcon(toolName), - displayName: getToolName(toolName), - command, - filePath, - input - } - }) - - // Check if we can navigate to session - const sessionId = createMemo(() => getPermissionSessionId(activePermission())) - const canGoToSession = createMemo(() => !!sessionId()) - - createEffect(() => { - const permission = activePermission() - if (!permission) { - setSubmitting(false) - setError(null) - } - }) - - // Keyboard shortcuts - createEffect(() => { - if (!props.isOpen || !hasActivePermission()) return - - const handler = (event: KeyboardEvent) => { - if (submitting()) return - - if (event.key === "Enter") { - event.preventDefault() - handleResponse("once") - } else if (event.key === "a" || event.key === "A") { - event.preventDefault() - handleResponse("always") - } else if (event.key === "d" || event.key === "D") { - event.preventDefault() - handleResponse("reject") - } else if (event.key === "Escape") { - event.preventDefault() - props.onClose() - } else if (event.key === "ArrowLeft" && hasPrev()) { - event.preventDefault() - navigatePrev() - } else if (event.key === "ArrowRight" && hasNext()) { - event.preventDefault() - navigateNext() - } - } - - document.addEventListener("keydown", handler) - onCleanup(() => document.removeEventListener("keydown", handler)) - }) - - function navigatePrev() { - const idx = currentIndex() - if (idx > 0) { - const prevPerm = queue()[idx - 1] - if (prevPerm) { - setActivePermissionIdForInstance(props.instanceId, prevPerm.id) - } - } - } - - function navigateNext() { - const idx = currentIndex() - if (idx < queue().length - 1) { - const nextPerm = queue()[idx + 1] - if (nextPerm) { - setActivePermissionIdForInstance(props.instanceId, nextPerm.id) - } - } - } - - function handleGoToSession() { - const sid = sessionId() - if (sid) { - setActiveSession(props.instanceId, sid) + const closeOnEscape = (event: KeyboardEvent) => { + if (event.key === "Escape") { + event.preventDefault() props.onClose() } } - async function handleResponse(response: "once" | "always" | "reject") { - const permission = activePermission() - if (!permission) return - - setSubmitting(true) - setError(null) - - try { - const sid = getPermissionSessionId(permission) || "" - await sendPermissionResponse(props.instanceId, sid, permission.id, response) - - // Wait a moment for queue to update before closing - setTimeout(() => { - const remaining = getPermissionQueue(props.instanceId) - if (remaining.length === 0) { - props.onClose() - } - }, 100) - } catch (err) { - log.error("Failed to send permission response", err) - setError(err instanceof Error ? err.message : "Failed to send response") - } finally { - setSubmitting(false) - } - } + createEffect(() => { + if (!props.isOpen) return + document.addEventListener("keydown", closeOnEscape) + onCleanup(() => document.removeEventListener("keydown", closeOnEscape)) + }) function handleBackdropClick(event: MouseEvent) { if (event.target === event.currentTarget) { @@ -219,196 +126,115 @@ const PermissionApprovalModal: Component = (props) } } - const diffPayload = createMemo(() => { - const permission = activePermission() - if (!permission) return null + async function handleLoadSession(sessionId: string) { + if (!sessionId) return + setLoadingSession(sessionId) + try { + await loadMessages(props.instanceId, sessionId) + } finally { + setLoadingSession((current) => (current === sessionId ? null : current)) + } + } - const metadata = ((permission as any).metadata || {}) as Record - const diffValue = typeof metadata.diff === "string" ? metadata.diff : null - if (!diffValue || diffValue.trim().length === 0) return null - - const diffPath = - typeof metadata.filePath === "string" ? metadata.filePath : - typeof metadata.path === "string" ? metadata.path : - undefined - - return { diffText: diffValue, filePath: diffPath } - }) + function handleGoToSession(sessionId: string) { + if (!sessionId) return + setActiveSession(props.instanceId, sessionId) + props.onClose() + } return ( -
- - }> - {/* Header */} -
-
-

- Permission Required -

- 1}> - - {currentIndex() + 1} of {queue().length} - - -
-
- - - - -
+
+
diff --git a/packages/ui/src/components/permission-notification-banner.tsx b/packages/ui/src/components/permission-notification-banner.tsx index 412de5f9..17e04907 100644 --- a/packages/ui/src/components/permission-notification-banner.tsx +++ b/packages/ui/src/components/permission-notification-banner.tsx @@ -1,6 +1,6 @@ import { Show, createMemo, type Component } from "solid-js" +import { ShieldAlert } from "lucide-solid" import { getPermissionQueueLength } from "../stores/instances" -import { isElectronHost } from "../lib/runtime-env" interface PermissionNotificationBannerProps { instanceId: string @@ -10,46 +10,25 @@ interface PermissionNotificationBannerProps { const PermissionNotificationBanner: Component = (props) => { const queueLength = createMemo(() => getPermissionQueueLength(props.instanceId)) const hasPermissions = createMemo(() => queueLength() > 0) - const isElectron = isElectronHost() + const label = createMemo(() => { + const count = queueLength() + return `${count} permission${count === 1 ? "" : "s"} pending approval` + }) return ( - {/* Electron: Full banner with text */} - - - - - {/* Web: Compact indicator button */} - - - + ) } diff --git a/packages/ui/src/components/prompt-input.tsx b/packages/ui/src/components/prompt-input.tsx index e214e9ae..07cd5ad8 100644 --- a/packages/ui/src/components/prompt-input.tsx +++ b/packages/ui/src/components/prompt-input.tsx @@ -7,6 +7,7 @@ import { resolvePastedPlaceholders } from "../lib/prompt-placeholders" import { createFileAttachment, createTextAttachment, createAgentAttachment } from "../types/attachment" import type { Attachment } from "../types/attachment" import type { Agent } from "../types/session" +import type { Command as SDKCommand } from "@opencode-ai/sdk/v2" import Kbd from "./kbd" import { getActiveInstance } from "../stores/instances" import { agents, getSessionDraftPrompt, setSessionDraftPrompt, clearSessionDraftPrompt, executeCustomCommand } from "../stores/sessions" @@ -767,7 +768,7 @@ export default function PromptInput(props: PromptInputProps) { type: "file" file: { path: string; relativePath?: string; isGitFile: boolean; isDirectory?: boolean } } - | { type: "command"; command: { name: string; description?: string } }, + | { type: "command"; command: SDKCommand }, ) { if (item.type === "command") { const name = item.command.name diff --git a/packages/ui/src/styles/components/permission-notification.css b/packages/ui/src/styles/components/permission-notification.css index 44da6a5b..170afd74 100644 --- a/packages/ui/src/styles/components/permission-notification.css +++ b/packages/ui/src/styles/components/permission-notification.css @@ -1,699 +1,231 @@ -/* Permission Notification Banner */ -.permission-notification-banner { - display: flex; +/* Central permission UI (toolbar + modal). + Kept intentionally small; reuse existing tokens. */ + +.permission-center-trigger { + display: inline-flex; align-items: center; - gap: var(--space-sm); - padding: 0.375rem 0.75rem; - background: linear-gradient(135deg, var(--status-warning) 0%, color-mix(in srgb, var(--status-warning) 85%, #000) 100%); - color: var(--text-inverted); - border: 1px solid color-mix(in srgb, var(--status-warning) 70%, #000); - border-radius: var(--radius-sm); - font-size: var(--font-size-sm); + gap: 0.25rem; + padding: 0.25rem 0.5rem; + border-radius: 9999px; + border: 1px solid var(--session-status-permission-fg); + background-color: var(--session-status-permission-bg); + color: var(--session-status-permission-fg); + font-size: 0.75rem; font-weight: var(--font-weight-semibold); cursor: pointer; - transition: all 0.2s ease; - box-shadow: 0 2px 4px color-mix(in srgb, var(--status-warning) 30%, transparent); - animation: permission-pulse 2s ease-in-out infinite; + transition: background-color 0.15s ease, border-color 0.15s ease, transform 0.15s ease; } -.permission-notification-banner:hover { - background: linear-gradient(135deg, color-mix(in srgb, var(--status-warning) 85%, #000) 0%, color-mix(in srgb, var(--status-warning) 70%, #000) 100%); - box-shadow: 0 4px 8px color-mix(in srgb, var(--status-warning) 40%, transparent); +.permission-center-trigger:hover, +.permission-center-trigger:focus-visible { + outline: none; + background-color: color-mix(in srgb, var(--session-status-permission-bg) 70%, var(--surface-hover)); transform: translateY(-1px); } -.permission-notification-banner:active { - transform: translateY(0); - box-shadow: 0 2px 4px color-mix(in srgb, var(--status-warning) 30%, transparent); +.permission-center-icon { + width: 1rem; + height: 1rem; } -.permission-notification-icon { - font-size: 1rem; +.permission-center-count { line-height: 1; - flex-shrink: 0; } -.permission-notification-text { - white-space: nowrap; - color: inherit; - font-size: inherit; -} - -.permission-notification-count { - display: inline-flex; - align-items: center; - justify-content: center; - min-width: 1.25rem; - height: 1.25rem; - padding: 0 0.25rem; - background: color-mix(in srgb, #000 30%, transparent); - border-radius: var(--radius-full); - font-size: var(--font-size-xs); - font-weight: var(--font-weight-bold); - color: inherit; - flex-shrink: 0; -} - -@keyframes permission-pulse { - - 0%, - 100% { - box-shadow: 0 2px 4px color-mix(in srgb, var(--status-warning) 30%, transparent); - } - - 50% { - box-shadow: 0 2px 8px color-mix(in srgb, var(--status-warning) 60%, transparent), 0 0 16px color-mix(in srgb, var(--status-warning) 30%, transparent); - } -} - -/* Web Permission Indicator Button (for browser-based streaming) */ -.permission-indicator-button { - position: relative; - display: inline-flex; - align-items: center; - justify-content: center; - width: 2rem; - height: 2rem; - padding: 0; - background: color-mix(in srgb, var(--status-warning) 15%, transparent); - color: var(--status-warning); - border: 2px solid var(--status-warning); - border-radius: var(--radius-full); - font-size: var(--font-size-sm); - font-weight: var(--font-weight-bold); - cursor: pointer; - transition: all 0.2s ease; - animation: web-permission-pulse 2s ease-in-out infinite; - flex-shrink: 0; - z-index: 10; - min-width: 2rem; - min-height: 2rem; - max-width: 2rem; - max-height: 2rem; -} - -.permission-indicator-button:hover { - background: color-mix(in srgb, var(--status-warning) 25%, transparent); - box-shadow: 0 0 8px color-mix(in srgb, var(--status-warning) 50%, transparent); - transform: scale(1.05); -} - -.permission-indicator-button:active { - transform: scale(0.98); -} - -.permission-indicator-button:focus-visible { - outline: 2px solid var(--focus-ring-color); - outline-offset: 2px; -} - -.permission-indicator-badge { - display: inline-flex; - align-items: center; - justify-content: center; - font-size: var(--font-size-xs); - color: inherit; -} - -@keyframes web-permission-pulse { - - 0%, - 100% { - box-shadow: 0 0 0 0 color-mix(in srgb, var(--status-warning) 40%, transparent); - } - - 50% { - box-shadow: 0 0 0 4px color-mix(in srgb, var(--status-warning) 20%, transparent); - } -} - -/* Ensure button visibility in web toolbars */ -@supports (display: flex) { - .permission-indicator-button { - visibility: visible; - opacity: 1; - pointer-events: auto; - } -} - -/* Permission Approval Modal */ -.permission-approval-modal-backdrop { +.permission-center-modal-backdrop { position: fixed; - top: 0; - left: 0; - right: 0; - bottom: 0; - background: color-mix(in srgb, var(--text-inverted) 60%, transparent); + inset: 0; + background: color-mix(in srgb, var(--text-inverted) 55%, transparent); backdrop-filter: blur(4px); display: flex; align-items: center; justify-content: center; z-index: 9999; padding: var(--space-lg); - animation: modal-backdrop-fadein 0.2s ease; } -@keyframes modal-backdrop-fadein { - from { - opacity: 0; - } - - to { - opacity: 1; - } -} - -.permission-approval-modal { - background: var(--surface-base); - border: 1px solid var(--border-base); - border-radius: var(--radius-xl); - box-shadow: 0 20px 25px -5px color-mix(in srgb, #000 10%, transparent), 0 10px 10px -5px color-mix(in srgb, #000 4%, transparent); - max-width: 48rem; - width: 100%; +.permission-center-modal { + width: min(900px, 100%); max-height: 90vh; display: flex; flex-direction: column; - animation: modal-slidein 0.3s ease; + border-radius: var(--radius-xl); + border: 1px solid var(--border-base); + background: var(--surface-base); + box-shadow: var(--panel-shadow, 0 12px 32px rgba(0, 0, 0, 0.25)); + overflow: hidden; } -@keyframes modal-slidein { - from { - opacity: 0; - transform: translateY(-20px) scale(0.95); - } - - to { - opacity: 1; - transform: translateY(0) scale(1); - } -} - -.permission-modal-empty { - padding: var(--space-xl); - color: var(--text-secondary); - text-align: center; -} - -.permission-modal-header { +.permission-center-modal-header { display: flex; align-items: center; justify-content: space-between; - padding: var(--space-lg); + gap: var(--space-md); + padding: var(--space-md); border-bottom: 1px solid var(--border-base); - gap: var(--space-lg); } -.permission-modal-title { - font-size: var(--font-size-xl); +.permission-center-modal-title-row { + display: flex; + align-items: center; + gap: var(--space-sm); + min-width: 0; +} + +.permission-center-modal-title { + font-size: var(--font-size-lg); font-weight: var(--font-weight-semibold); color: var(--text-primary); margin: 0; - flex: 1; } -.permission-modal-count { - display: inline-flex; - align-items: center; - justify-content: center; - padding: var(--space-xs) var(--space-sm); - background: var(--surface-secondary); - border-radius: var(--radius-sm); - border: 1px solid var(--border-base); - font-size: var(--font-size-sm); - font-weight: var(--font-weight-medium); - color: var(--text-secondary); - white-space: nowrap; - flex-shrink: 0; -} - -.permission-modal-body { - flex: 1; - overflow-y: auto; - padding: var(--space-lg); - display: flex; - flex-direction: column; - gap: var(--space-lg); -} - -.permission-modal-type { - display: inline-flex; - align-items: center; - padding: var(--space-xs) var(--space-sm); - background: color-mix(in srgb, var(--status-warning) 20%, transparent); - color: var(--status-warning); - border: 1px solid color-mix(in srgb, var(--status-warning) 50%, transparent); - border-radius: var(--radius-sm); - font-size: var(--font-size-xs); - font-weight: var(--font-weight-semibold); - text-transform: uppercase; - letter-spacing: 0.05em; - align-self: flex-start; -} - -.permission-modal-message { - padding: var(--space-lg); - background: var(--surface-secondary); - border: 1px solid var(--border-base); - border-radius: var(--radius-md); - font-family: var(--font-family-mono); - word-break: break-word; -} - -.permission-modal-message code { - font-family: var(--font-family-mono); - font-size: var(--font-size-sm); - color: var(--text-primary); -} - -.permission-modal-diff { - display: flex; - flex-direction: column; - gap: var(--space-sm); -} - -.permission-modal-diff-label { - font-size: var(--font-size-xs); - font-weight: var(--font-weight-semibold); - color: var(--text-secondary); - text-transform: uppercase; - letter-spacing: 0.05em; -} - -.permission-modal-diff-viewer { - border: 1px solid var(--border-base); - border-radius: var(--radius-md); - overflow: hidden; - max-height: 24rem; - background: var(--surface-secondary); -} - -.permission-modal-error { - padding: var(--space-sm) var(--space-lg); - background: color-mix(in srgb, var(--status-error) 20%, transparent); - color: var(--status-error); - border: 1px solid color-mix(in srgb, var(--status-error) 50%, transparent); - border-radius: var(--radius-md); - font-size: var(--font-size-sm); -} - -.permission-modal-footer { - display: flex; - flex-direction: column; - gap: var(--space-lg); - padding: var(--space-lg); - border-top: 1px solid var(--border-base); - background: var(--surface-secondary); -} - -.permission-modal-buttons { - display: flex; - gap: var(--space-sm); - justify-content: stretch; -} - -.permission-modal-button { - flex: 1; - padding: var(--button-padding-y) var(--button-padding-x); - border: 1px solid transparent; - border-radius: var(--button-radius); - font-size: var(--font-size-sm); - font-weight: var(--font-weight-semibold); - cursor: pointer; - transition: all 0.15s ease; - font-family: var(--font-family-sans); -} - -.permission-modal-button:disabled { - opacity: 0.5; - cursor: not-allowed; -} - -.permission-modal-button:focus-visible { - outline: 2px solid var(--focus-ring-color); - outline-offset: 2px; -} - -.permission-modal-button-once { - background: var(--accent-primary); - color: var(--text-inverted); - border-color: var(--accent-primary); -} - -.permission-modal-button-once:hover:not(:disabled) { - background: var(--accent-hover); - border-color: var(--accent-hover); -} - -.permission-modal-button-always { - background: var(--status-success); - color: var(--text-inverted); - border-color: var(--status-success); -} - -.permission-modal-button-always:hover:not(:disabled) { - background: color-mix(in srgb, var(--status-success) 85%, #000); - border-color: color-mix(in srgb, var(--status-success) 85%, #000); -} - -.permission-modal-button-deny { - background: var(--status-error); - color: var(--text-inverted); - border-color: var(--status-error); -} - -.permission-modal-button-deny:hover:not(:disabled) { - background: color-mix(in srgb, var(--status-error) 85%, #000); - border-color: color-mix(in srgb, var(--status-error) 85%, #000); -} - -.permission-modal-shortcuts { - display: flex; - gap: var(--space-lg); - justify-content: center; - flex-wrap: wrap; -} - -.permission-modal-shortcut { - display: flex; - align-items: center; - gap: var(--space-sm); - font-size: var(--font-size-xs); - color: var(--text-secondary); -} - -.permission-modal-shortcut kbd { +.permission-center-modal-count { display: inline-flex; align-items: center; justify-content: center; min-width: 1.5rem; height: 1.5rem; - padding: 0 0.375rem; - background: var(--kbd-bg); - border: 1px solid var(--kbd-border); - color: var(--kbd-text); - border-radius: var(--radius-xs); - font-family: var(--font-family-mono); + padding: 0 0.4rem; + border-radius: 9999px; + background: var(--session-status-permission-bg); + color: var(--session-status-permission-fg); + border: 1px solid var(--session-status-permission-fg); font-size: var(--font-size-xs); font-weight: var(--font-weight-semibold); - box-shadow: inset 0 -2px 0 color-mix(in srgb, #000 10%, transparent); } -/* Enhanced Header Layout */ -.permission-modal-header { +.permission-center-modal-close { + display: inline-flex; + align-items: center; + justify-content: center; + width: 2rem; + height: 2rem; + border-radius: var(--radius-sm); + border: 1px solid var(--border-base); + background: var(--surface-secondary); + color: var(--text-primary); + cursor: pointer; +} + +.permission-center-modal-close:hover { + background: var(--surface-hover); +} + +.permission-center-modal-body { + flex: 1; + min-height: 0; + overflow: auto; + padding: var(--space-md); +} + +.permission-center-empty { + color: var(--text-secondary); + padding: var(--space-lg); + text-align: center; +} + +.permission-center-list { + display: flex; + flex-direction: column; + gap: var(--space-md); +} + +.permission-center-item { + border: 1px solid var(--border-base); + border-radius: var(--radius-lg); + background: var(--surface-secondary); + overflow: hidden; +} + +.permission-center-item-active { + border-color: var(--session-status-permission-fg); + box-shadow: 0 0 0 1px color-mix(in srgb, var(--session-status-permission-fg) 35%, transparent); +} + +.permission-center-item-header { display: flex; align-items: center; justify-content: space-between; - padding: var(--space-lg); + gap: var(--space-md); + padding: var(--space-sm) var(--space-md); border-bottom: 1px solid var(--border-base); - gap: var(--space-md); - flex-wrap: wrap; -} - -.permission-modal-header-left { - display: flex; - align-items: center; - gap: var(--space-md); - flex: 1; - min-width: 0; -} - -.permission-modal-header-actions { - display: flex; - align-items: center; - gap: var(--space-sm); - flex-shrink: 0; -} - -.permission-modal-go-to-session { - display: inline-flex; - align-items: center; - gap: var(--space-xs); - padding: var(--space-xs) var(--space-sm); - background: var(--surface-secondary); - color: var(--text-secondary); - border: 1px solid var(--border-base); - border-radius: var(--radius-sm); - font-size: var(--font-size-xs); - font-weight: var(--font-weight-medium); - cursor: pointer; - transition: all 0.15s ease; - white-space: nowrap; -} - -.permission-modal-go-to-session:hover { background: var(--surface-base); - color: var(--text-primary); - border-color: var(--accent-primary); } -.permission-modal-close { - display: inline-flex; +.permission-center-item-heading { + display: flex; align-items: center; - justify-content: center; - width: 1.75rem; - height: 1.75rem; - padding: 0; - background: transparent; + gap: var(--space-sm); +} + +.permission-center-item-kind { + font-size: var(--font-size-xs); + letter-spacing: 0.05em; + text-transform: uppercase; color: var(--text-secondary); - border: none; - border-radius: var(--radius-sm); - font-size: var(--font-size-md); - cursor: pointer; - transition: all 0.15s ease; -} - -.permission-modal-close:hover { - background: var(--surface-secondary); - color: var(--text-primary); -} - -/* Tool Details Section */ -.permission-modal-tool-details { - display: flex; - flex-direction: column; - gap: var(--space-sm); - padding: var(--space-md); - background: var(--surface-secondary); - border: 1px solid var(--border-base); - border-radius: var(--radius-md); -} - -.permission-modal-tool-header { - display: flex; - align-items: center; - gap: var(--space-sm); - flex-wrap: wrap; -} - -.permission-modal-tool-icon { - font-size: 1.25rem; - line-height: 1; -} - -.permission-modal-tool-name { - font-size: var(--font-size-md); font-weight: var(--font-weight-semibold); - color: var(--text-primary); } -.permission-modal-tool-path { - font-family: var(--font-family-mono); +.permission-center-item-chip { font-size: var(--font-size-xs); - color: var(--text-secondary); - background: var(--surface-base); - padding: var(--space-xs) var(--space-sm); - border-radius: var(--radius-xs); + font-weight: var(--font-weight-semibold); + padding: 0.1rem 0.4rem; + border-radius: 9999px; + border: 1px solid var(--session-status-permission-fg); + background: var(--session-status-permission-bg); + color: var(--session-status-permission-fg); } -.permission-modal-tool-command { - padding: var(--space-sm); - background: var(--surface-base); - border: 1px solid var(--border-base); +.permission-center-item-actions { + display: inline-flex; + align-items: center; + gap: var(--space-sm); +} + +.permission-center-item-action { + padding: 0.25rem 0.5rem; border-radius: var(--radius-sm); - overflow-x: auto; -} - -.permission-modal-tool-command code { - font-family: var(--font-family-mono); - font-size: var(--font-size-sm); - color: var(--text-primary); - white-space: pre-wrap; - word-break: break-all; -} - -/* Queue Navigation */ -.permission-modal-nav { - display: flex; - justify-content: center; - gap: var(--space-md); - padding-bottom: var(--space-md); -} - -.permission-modal-nav-button { - padding: var(--space-xs) var(--space-md); + border: 1px solid var(--border-base); background: var(--surface-secondary); - color: var(--text-secondary); - border: 1px solid var(--border-base); - border-radius: var(--radius-sm); - font-size: var(--font-size-sm); - font-weight: var(--font-weight-medium); - cursor: pointer; - transition: all 0.15s ease; -} - -.permission-modal-nav-button:hover:not(:disabled) { - background: var(--surface-base); color: var(--text-primary); - border-color: var(--accent-primary); + font-size: var(--font-size-xs); + cursor: pointer; } -.permission-modal-nav-button:disabled { - opacity: 0.4; +.permission-center-item-action:hover { + background: var(--surface-hover); +} + +.permission-center-item-action:disabled { + opacity: 0.6; cursor: not-allowed; } -.permission-modal-tool-badge { - display: inline-flex; - align-items: center; - padding: var(--space-xs) var(--space-sm); - background: var(--surface-base); - border: 1px solid var(--border-base); - border-radius: var(--radius-sm); +.permission-center-fallback { + padding: var(--space-md); +} + +.permission-center-fallback-title code { font-family: var(--font-family-mono); font-size: var(--font-size-sm); - font-weight: var(--font-weight-medium); color: var(--text-primary); } -/* ===== RESPONSIVE STYLES ===== */ - -/* Portrait phone - near full screen with safe margins */ -@media (max-width: 480px) and (orientation: portrait) { - .permission-approval-modal { - width: 95vw; - max-height: 90vh; - max-height: calc(90vh - env(safe-area-inset-top) - env(safe-area-inset-bottom)); - border-radius: var(--radius-md); - margin: 5vh auto; - } - - .permission-approval-modal-backdrop { - padding: var(--space-md); - } - - .permission-modal-body { - flex: 1; - overflow-y: auto; - -webkit-overflow-scrolling: touch; - max-height: 50vh; - } - - .permission-modal-buttons { - flex-direction: column; - gap: var(--space-sm); - } - - .permission-modal-button { - min-height: 48px; - font-size: var(--font-size-md); - } - - .permission-modal-shortcuts { - display: none; - } - - .permission-modal-footer { - flex-shrink: 0; - position: sticky; - bottom: 0; - background: var(--surface-secondary); - border-top: 1px solid var(--border-base); - } - - .permission-modal-go-to-session { - font-size: var(--font-size-xs); - padding: var(--space-sm) var(--space-md); - } - - .permission-modal-diff-viewer { - max-height: 40vh; - } +.permission-center-fallback-hint { + margin-top: var(--space-sm); + color: var(--text-secondary); + font-size: var(--font-size-sm); } -/* Landscape phone */ -@media (max-width: 812px) and (orientation: landscape) { - .permission-approval-modal { - width: 95vw; - max-height: 95vh; - margin: auto; +@media (max-width: 720px) { + .permission-center-modal-backdrop { + padding: 0; } - .permission-modal-body { - overflow-y: auto; - max-height: 50vh; - -webkit-overflow-scrolling: touch; - } - - .permission-modal-buttons { - flex-direction: row; - flex-wrap: nowrap; - } - - .permission-modal-button { - min-height: 44px; - padding: var(--space-sm) var(--space-md); - font-size: var(--font-size-sm); - } - - .permission-modal-shortcuts { - display: none; - } - - .permission-modal-diff-viewer { - max-height: 30vh; - } - - .permission-modal-footer { - flex-shrink: 0; + .permission-center-modal { + width: 100vw; + height: 100vh; + max-height: none; + border-radius: 0; } } - -/* Small tablets portrait */ -@media (min-width: 481px) and (max-width: 768px) and (orientation: portrait) { - .permission-approval-modal { - width: 95vw; - max-height: 90vh; - } - - .permission-modal-body { - overflow-y: auto; - max-height: 60vh; - } - - .permission-modal-button { - min-height: 44px; - } -} - -/* Touch device optimizations */ -@media (hover: none) and (pointer: coarse) { - .permission-modal-button { - min-height: 44px; - } - - .permission-modal-nav-button { - min-height: 40px; - padding: var(--space-sm) var(--space-lg); - } - - .permission-modal-go-to-session { - min-height: 36px; - padding: var(--space-sm) var(--space-md); - } - - .permission-modal-close { - width: 2.5rem; - height: 2.5rem; - font-size: var(--font-size-lg); - } -} \ No newline at end of file