diff --git a/web/src/components/file-proposal-editor.tsx b/web/src/components/file-proposal-editor.tsx index 0df659be5..4992e0fd7 100644 --- a/web/src/components/file-proposal-editor.tsx +++ b/web/src/components/file-proposal-editor.tsx @@ -78,7 +78,7 @@ function FileProposalSession({ client, runID, source, onClose }: Parameters 256 * 1024 || draft === activeSource.content || expired || recovery.changedBase || recovery.conflict; const language = languageForPath(activeSource.path); - const monacoTheme = readPrayuTheme() === "light" ? "vs" : "vs-dark"; + const monacoTheme = readPrayuTheme() === "dark" ? "vs-dark" : "vs"; useFileEditLeaveEntry({ path: activeSource.path, dirty: Boolean(recovery.draft), busy: mutation.isPending, canSave: !invalid, save: () => mutation.mutateAsync(recovery.captureSubmission(draft)), keep: recovery.keep, diff --git a/web/src/components/file-proposal-recovery.tsx b/web/src/components/file-proposal-recovery.tsx index 448de3ec5..12a223ca0 100644 --- a/web/src/components/file-proposal-recovery.tsx +++ b/web/src/components/file-proposal-recovery.tsx @@ -51,7 +51,7 @@ export function FileProposalRecovery({ client, runID, editID, onClose }: { modified={recovery.proposed_content} original={recovery.original_content} options={{ automaticLayout: true, fontFamily: 'var(--prayu-font-mono, "JetBrains Mono", Consolas, monospace)', fontSize: 13, letterSpacing: 0, minimap: { enabled: false }, readOnly: true, renderSideBySide: true, - scrollBeyondLastLine: false }} theme={readPrayuTheme() === "light" ? "vs" : "vs-dark"} /> : + scrollBeyondLastLine: false }} theme={readPrayuTheme() === "dark" ? "vs-dark" : "vs"} /> :
{editorState === "failed" ? t("无法加载本地编辑器资源", "Local editor assets could not be loaded") : t("正在加载本地编辑器", "Loading local editor")} diff --git a/web/src/lib/appearance.test.ts b/web/src/lib/appearance.test.ts index 1d2800860..7e37248a5 100644 --- a/web/src/lib/appearance.test.ts +++ b/web/src/lib/appearance.test.ts @@ -1,23 +1,74 @@ -import { applyPrayuTheme, readPrayuTheme } from "./appearance"; +import { + applyPrayuGlassSettings, + applyPrayuTheme, + defaultPrayuGlassSettings, + prayuGlassPresets, + readPrayuGlassSettings, + readPrayuTheme, + resetPrayuGlassSettings, +} from "./appearance"; describe("Prayu appearance", () => { beforeEach(() => { window.localStorage.clear(); delete document.documentElement.dataset.prayuTheme; + delete document.documentElement.dataset.prayuGlassMode; document.documentElement.style.colorScheme = ""; + document.documentElement.style.removeProperty("--prayu-glass-blur"); + document.documentElement.style.removeProperty("--prayu-glass-frost"); + document.documentElement.style.removeProperty("--prayu-glass-brightness-white"); + document.documentElement.style.removeProperty("--prayu-glass-glow"); }); afterEach(() => { vi.restoreAllMocks(); }); - it("persists and restores the transparent glass mode", () => { + it("persists and restores the Catppuccin Latte glass mode with default Mica parameters", () => { applyPrayuTheme("glass"); expect(document.documentElement.dataset.prayuTheme).toBe("glass"); - expect(document.documentElement.style.colorScheme).toBe("dark"); + expect(document.documentElement.dataset.prayuGlassMode).toBe("mica"); + expect(document.documentElement.style.colorScheme).toBe("light"); + expect(document.documentElement.style.getPropertyValue("--prayu-glass-blur")).toBe("5px"); + expect(document.documentElement.style.getPropertyValue("--prayu-glass-frost")).toBe("0.4"); + expect(document.documentElement.style.getPropertyValue("--prayu-glass-brightness-white")).toBe("0"); + expect(document.documentElement.style.getPropertyValue("--prayu-glass-glow")).toBe("0"); expect(window.localStorage.getItem("prayu.theme")).toBe("glass"); expect(readPrayuTheme()).toBe("glass"); + expect(readPrayuGlassSettings()).toEqual(defaultPrayuGlassSettings); + }); + + it("clamps, persists, and resets glass material parameters", () => { + const updated = applyPrayuGlassSettings({ + mode: "compat", + blur: 48, + frost: 45, + brightness: 75, + glow: true, + }); + + expect(updated).toEqual({ + mode: "compat", + blur: 40, + frost: 45, + brightness: 75, + glow: true, + }); + expect(document.documentElement.dataset.prayuGlassMode).toBe("compat"); + expect(document.documentElement.style.getPropertyValue("--prayu-glass-blur")).toBe("40px"); + expect(document.documentElement.style.getPropertyValue("--prayu-glass-frost")).toBe("0.9"); + expect(document.documentElement.style.getPropertyValue("--prayu-glass-brightness-white")).toBe("0.5"); + expect(document.documentElement.style.getPropertyValue("--prayu-glass-glow")).toBe("1"); + expect(readPrayuGlassSettings()).toEqual(updated); + + applyPrayuGlassSettings(prayuGlassPresets.clear); + expect(readPrayuGlassSettings()).toMatchObject({ blur: 0, frost: 8, brightness: 50 }); + + const reset = resetPrayuGlassSettings(); + expect(reset).toEqual(defaultPrayuGlassSettings); + expect(document.documentElement.dataset.prayuGlassMode).toBe("mica"); + expect(document.documentElement.style.getPropertyValue("--prayu-glass-blur")).toBe("5px"); }); it("keeps appearance changes non-blocking when storage is unavailable", () => { @@ -26,6 +77,9 @@ describe("Prayu appearance", () => { }); expect(() => applyPrayuTheme("glass")).not.toThrow(); + expect(() => applyPrayuGlassSettings({ blur: 12, frost: 45 })).not.toThrow(); expect(document.documentElement.dataset.prayuTheme).toBe("glass"); + expect(document.documentElement.style.getPropertyValue("--prayu-glass-blur")).toBe("12px"); }); }); + diff --git a/web/src/lib/appearance.ts b/web/src/lib/appearance.ts index f8ec4f3a6..cf21b97ff 100644 --- a/web/src/lib/appearance.ts +++ b/web/src/lib/appearance.ts @@ -1,8 +1,115 @@ import { setDesktopWindowTheme } from "./desktop-window"; export type PrayuTheme = "light" | "dark" | "glass"; +export type PrayuGlassMode = "mica" | "compat"; +export type PrayuGlassPreset = "clear" | "standard" | "frosted"; + +export interface PrayuGlassSettings { + mode: PrayuGlassMode; + blur: number; + frost: number; + brightness: number; + glow: boolean; +} const themeStorageKey = "prayu.theme"; +const glassStorageKey = "prayu.glass"; + +export const defaultPrayuGlassSettings: Readonly = Object.freeze({ + mode: "mica", + blur: 5, + frost: 20, + brightness: 50, + glow: false, +}); + +export const prayuGlassPresets: Readonly< + Record>> +> = Object.freeze({ + clear: Object.freeze({ blur: 0, frost: 8, brightness: 50 }), + standard: Object.freeze({ blur: 5, frost: 20, brightness: 50 }), + frosted: Object.freeze({ blur: 12, frost: 45, brightness: 50 }), +}); + +function clampStep(value: unknown, min: number, max: number, step: number, fallback: number): number { + const num = typeof value === "number" ? value : Number(value); + if (!Number.isFinite(num)) return fallback; + const clamped = Math.min(max, Math.max(min, num)); + const stepped = Math.round(clamped / step) * step; + return Number(stepped.toFixed(2)); +} + +export function normalizePrayuGlassSettings( + input?: Partial | null, +): PrayuGlassSettings { + const mode: PrayuGlassMode = input?.mode === "compat" ? "compat" : "mica"; + const blur = clampStep(input?.blur, 0, 40, 0.5, defaultPrayuGlassSettings.blur); + const frost = clampStep(input?.frost, 0, 100, 1, defaultPrayuGlassSettings.frost); + const brightness = clampStep( + input?.brightness, + 50, + 100, + 1, + defaultPrayuGlassSettings.brightness, + ); + const glow = typeof input?.glow === "boolean" ? input.glow : defaultPrayuGlassSettings.glow; + return { mode, blur, frost, brightness, glow }; +} + +export function readPrayuGlassSettings(): PrayuGlassSettings { + if (typeof window === "undefined") return { ...defaultPrayuGlassSettings }; + try { + const raw = window.localStorage.getItem(glassStorageKey); + if (!raw) return { ...defaultPrayuGlassSettings }; + const parsed = JSON.parse(raw) as Partial; + return normalizePrayuGlassSettings(parsed); + } catch { + return { ...defaultPrayuGlassSettings }; + } +} + +function syncGlassVariables(settings: PrayuGlassSettings): void { + const root = document.documentElement; + root.dataset.prayuGlassMode = settings.mode; + const frostMultiplier = Number(Math.min(settings.frost / 50, 1.4).toFixed(3)); + const brightnessWhite = Number( + Math.max(0, Math.min(1, (settings.brightness - 50) / 50)).toFixed(3), + ); + root.style.setProperty("--prayu-glass-blur", `${settings.blur}px`); + root.style.setProperty("--prayu-glass-frost", String(frostMultiplier)); + root.style.setProperty("--prayu-glass-brightness-white", String(brightnessWhite)); + root.style.setProperty("--prayu-glass-glow", settings.glow ? "1" : "0"); +} + +export function applyPrayuGlassSettings( + next: Partial, + persist = true, +): PrayuGlassSettings { + const current = readPrayuGlassSettings(); + const normalized = normalizePrayuGlassSettings({ ...current, ...next }); + syncGlassVariables(normalized); + if (persist) { + try { + window.localStorage.setItem(glassStorageKey, JSON.stringify(normalized)); + } catch { + // Display preferences must never block the workbench. + } + } + return normalized; +} + +export function resetPrayuGlassSettings(persist = true): PrayuGlassSettings { + const normalized = { ...defaultPrayuGlassSettings }; + syncGlassVariables(normalized); + if (persist) { + try { + window.localStorage.setItem(glassStorageKey, JSON.stringify(normalized)); + } catch { + // Display preferences must never block the workbench. + } + } + return normalized; +} export function readPrayuTheme(): PrayuTheme { if (typeof window === "undefined") return "dark"; @@ -17,7 +124,8 @@ export function readPrayuTheme(): PrayuTheme { export function applyPrayuTheme(theme: PrayuTheme, persist = true): void { document.documentElement.dataset.prayuTheme = theme; - document.documentElement.style.colorScheme = theme === "light" ? "light" : "dark"; + document.documentElement.style.colorScheme = theme === "dark" ? "dark" : "light"; + syncGlassVariables(readPrayuGlassSettings()); setDesktopWindowTheme(theme); if (!persist) return; try { @@ -30,3 +138,4 @@ export function applyPrayuTheme(theme: PrayuTheme, persist = true): void { export function initializePrayuAppearance(): void { applyPrayuTheme(readPrayuTheme(), false); } + diff --git a/web/src/lib/desktop-window.test.ts b/web/src/lib/desktop-window.test.ts index 21e2a55e6..6ead4b923 100644 --- a/web/src/lib/desktop-window.test.ts +++ b/web/src/lib/desktop-window.test.ts @@ -25,8 +25,8 @@ describe("desktop window controls", () => { expect(runtime.WindowMinimise).toHaveBeenCalledTimes(1); expect(runtime.WindowToggleMaximise).toHaveBeenCalledTimes(1); - expect(runtime.WindowSetLightTheme).toHaveBeenCalledTimes(1); - expect(runtime.WindowSetDarkTheme).toHaveBeenCalledTimes(2); + expect(runtime.WindowSetLightTheme).toHaveBeenCalledTimes(2); + expect(runtime.WindowSetDarkTheme).toHaveBeenCalledTimes(1); expect(runtime.Quit).toHaveBeenCalledTimes(1); }); diff --git a/web/src/lib/desktop-window.ts b/web/src/lib/desktop-window.ts index b5513263b..4df11c507 100644 --- a/web/src/lib/desktop-window.ts +++ b/web/src/lib/desktop-window.ts @@ -25,9 +25,10 @@ export function closeDesktopWindow(): void { } export function setDesktopWindowTheme(theme: "light" | "dark" | "glass"): void { - if (theme === "light") { - window.runtime?.WindowSetLightTheme?.(); - } else { + if (theme === "dark") { window.runtime?.WindowSetDarkTheme?.(); + } else { + window.runtime?.WindowSetLightTheme?.(); } } + diff --git a/web/src/styles.css b/web/src/styles.css index ad5cdb383..caafe4e88 100644 --- a/web/src/styles.css +++ b/web/src/styles.css @@ -2614,32 +2614,55 @@ body { background: transparent; } } [data-prayu-theme="glass"] { - --prayu-window: rgb(8 12 18 / 14%); - --prayu-titlebar: rgb(20 25 33 / 24%); - --prayu-sidebar: color-mix(in srgb, rgb(18 24 32 / 22%) 82%, #dff3f4 18%); - --prayu-panel: rgb(18 24 32 / 30%); + --dsh-glass-ground: color-mix(in srgb, #eff1f5, #ffffff calc(var(--prayu-glass-brightness-white, 0) * 100%)); + --dsh-glass-ambience: calc(1 - var(--prayu-glass-brightness-white, 0)); + --dsh-glass-card: color-mix(in srgb, #eff1f5 calc(52% * var(--prayu-glass-frost, 0.4)), transparent); + --dsh-glass-card-raised: color-mix(in srgb, #eff1f5 calc(64% * var(--prayu-glass-frost, 0.4)), transparent); + --dsh-glass-card-hover: color-mix(in srgb, #eff1f5 calc(76% * var(--prayu-glass-frost, 0.4)), transparent); + --dsh-glass-bubble: color-mix(in srgb, #eff1f5 calc(46% * var(--prayu-glass-frost, 0.4)), transparent); + --dsh-glass-popover: color-mix(in srgb, #eff1f5 calc(72% * var(--prayu-glass-frost, 0.4)), rgb(239 241 245 / 82%)); + --dsh-glass-rim: color-mix(in srgb, #4c4f69 18%, transparent); + --dsh-glass-rim-soft: color-mix(in srgb, #4c4f69 12%, transparent); + --dsh-glass-rim-strong: color-mix(in srgb, #4c4f69 30%, transparent); + --dsh-glass-edge: inset 0 1px 0 rgb(255 255 255 / 42%); + --dsh-glass-drop: 0 10px 32px color-mix(in srgb, rgb(0 0 0 / 12%) 38%, transparent); + --dsh-glass-input-shadow: 0 1px 2px rgb(76 79 105 / 4%), 0 4px 14px rgb(76 79 105 / 6%); + --prayu-window: var(--dsh-glass-ground); + --prayu-titlebar: color-mix(in srgb, #e6e9ef calc(52% * var(--prayu-glass-frost, 0.4)), transparent); + --prayu-sidebar: var(--dsh-glass-card); + --prayu-panel: var(--dsh-glass-card); /* Floating menus need their own readable surface above the translucent workspace. */ - --prayu-panel-strong: rgb(18 24 32 / 90%); - --prayu-control: rgb(255 255 255 / 10%); - --prayu-control-hover: rgb(255 255 255 / 18%); + --prayu-panel-strong: var(--dsh-glass-popover); + --prayu-control: rgb(38 49 72 / 6%); + --prayu-control-hover: rgb(38 49 72 / 10%); + --prayu-primary-bg: #566570; + --prayu-primary-fg: #ffffff; + --prayu-primary-hover: #45535e; + --prayu-primary-border: color-mix(in srgb, #566570 42%, #4c4f69 18%); + --prayu-primary-shadow: rgb(76 79 105 / 12%); --prayu-selected: var(--prayu-primary-bg); --prayu-selected-ink: var(--prayu-primary-fg); - --prayu-surface-solid: #202832; - --prayu-text: #f8f9fb; - --prayu-muted: #d5dae2; - --prayu-border: rgb(255 255 255 / 17%); - --prayu-border-strong: rgb(255 255 255 / 30%); - --prayu-shadow: rgb(0 0 0 / 28%); - --prayu-focus: #f4f4f4; - --prayu-accent: #f4f4f4; - --prayu-danger: #ff9189; - --prayu-success: var(--prayu-text); - --prayu-warning: var(--prayu-text); - --prayu-diff-add: #9cd4ae; - --prayu-glass-fill: rgb(20 27 36 / 24%); - --prayu-glass-fill-strong: rgb(20 27 36 / 44%); - --prayu-glass-highlight: rgb(255 255 255 / 30%); - --prayu-glass-shadow: rgb(0 0 0 / 30%); + --prayu-surface-solid: #eff1f5; + --prayu-text: #4c4f69; + --prayu-muted: #5c5f77; + --prayu-border: var(--dsh-glass-rim-soft); + --prayu-border-strong: var(--dsh-glass-rim); + --prayu-shadow: rgb(0 0 0 / 12%); + --prayu-focus: #657682; + --prayu-accent: #566570; + --prayu-danger: #d20f39; + --prayu-danger-fill: #d20f39; + --prayu-on-danger: #ffffff; + --prayu-danger-on-dark: #f38ba8; + --prayu-success: #40a02b; + --prayu-warning: #fe640b; + --prayu-diff-add: #40a02b; + --prayu-glass-fill: var(--dsh-glass-card); + --prayu-glass-fill-strong: var(--dsh-glass-popover); + --prayu-glass-highlight: rgb(255 255 255 / 42%); + --prayu-glass-shadow: rgb(0 0 0 / 12%); + --prayu-elevation-panel: var(--dsh-glass-edge), var(--dsh-glass-drop); + --prayu-elevation-floating: var(--dsh-glass-edge), var(--dsh-glass-input-shadow); --prayu-danger-soft: color-mix(in srgb, var(--prayu-danger) 10%, var(--prayu-surface-solid)); --prayu-diff-delete: var(--prayu-danger); --prayu-diff-add-soft: color-mix(in srgb, var(--prayu-diff-add) 12%, var(--prayu-surface-solid)); @@ -2656,6 +2679,11 @@ body { background: transparent; } --prayu-panel-strong: var(--prayu-surface-solid); --prayu-glass-fill: var(--prayu-surface-solid); --prayu-glass-fill-strong: var(--prayu-surface-solid); + --dsh-glass-card: var(--prayu-surface-solid); + --dsh-glass-card-raised: var(--prayu-surface-solid); + --dsh-glass-card-hover: var(--prayu-control-hover); + --dsh-glass-bubble: var(--prayu-control-hover); + --dsh-glass-popover: var(--prayu-surface-solid); } } @@ -2665,6 +2693,9 @@ body { background: transparent; } --prayu-border: var(--prayu-text); --prayu-border-strong: var(--prayu-text); --prayu-primary-border: var(--prayu-primary-fg); + --dsh-glass-rim: var(--prayu-text); + --dsh-glass-rim-soft: var(--prayu-text); + --dsh-glass-rim-strong: var(--prayu-text); } } @@ -2708,6 +2739,17 @@ body { background: transparent; } --prayu-glass-highlight: transparent; --prayu-shadow: transparent; --prayu-glass-shadow: transparent; + --dsh-glass-card: Canvas; + --dsh-glass-card-raised: Canvas; + --dsh-glass-card-hover: Canvas; + --dsh-glass-bubble: Canvas; + --dsh-glass-popover: Canvas; + --dsh-glass-rim: CanvasText; + --dsh-glass-rim-soft: CanvasText; + --dsh-glass-rim-strong: CanvasText; + --dsh-glass-edge: none; + --dsh-glass-drop: none; + --dsh-glass-input-shadow: none; --prayu-terminal-bg: Canvas; --prayu-terminal-control: Canvas; --prayu-terminal-control-hover: Canvas; @@ -2774,10 +2816,13 @@ summary:focus-visible { } [data-prayu-theme="dark"] .prayu-shell .main-workspace, -[data-prayu-theme="dark"] .prayu-shell .main-workspace.prayu-conversation-panel, +[data-prayu-theme="dark"] .prayu-shell .main-workspace.prayu-conversation-panel { + backdrop-filter: blur(46px); +} + [data-prayu-theme="glass"] .prayu-shell .main-workspace, [data-prayu-theme="glass"] .prayu-shell .main-workspace.prayu-conversation-panel { - backdrop-filter: blur(46px); + backdrop-filter: blur(var(--prayu-glass-blur, 5px)); } .prayu-shell .main-workspace, diff --git a/web/src/v2/components/advanced-settings.tsx b/web/src/v2/components/advanced-settings.tsx index 40ac2762d..90eda04f5 100644 --- a/web/src/v2/components/advanced-settings.tsx +++ b/web/src/v2/components/advanced-settings.tsx @@ -67,15 +67,16 @@ export function V2SkillSettings({ client, desktop }: { client: APIClient; deskto

{t("先预览 ZIP 包的结构、来源与文件,再确认安装到 Code 或 Cyber 工作面。", "Preview the ZIP package's structure, source, and files before confirming installation to Code or Cyber.")}

{!client.hasSkillInstallation &&

{t("当前连接可预览包结构。具备安装权限的连接可继续确认安装。", "This connection can preview package structure. Installation requires a connection with installation permission.")}

} - setPreviewOpen(false)} open={previewOpen} /> : client.hasSkillInstallation ? <>

{t("网页端可上传 ZIP 包或原生 SKILL.md 目录并登记到 Code。结构校验和内容解析由服务端执行。", "Upload a ZIP package or a SKILL.md directory to register it with Code. The server validates the structure and parses the content.")}

:

{t("连接具备 Skill 安装权限的服务后,可选择 ZIP 包加入技能库。", "Connect with skill installation permission to add a ZIP package to the library.")}

} -
; + + {desktop && setPreviewOpen(false)} open={previewOpen} />} + ; } export function V2InspectorPreferences({ onOpenInspector }: { diff --git a/web/src/v2/components/agent-browser.css b/web/src/v2/components/agent-browser.css index b37ae878a..a5763fb36 100644 --- a/web/src/v2/components/agent-browser.css +++ b/web/src/v2/components/agent-browser.css @@ -1,7 +1,10 @@ -.v2-agent-browser { box-sizing:border-box; width:100%; min-width:0; display:grid; gap:12px; margin:16px 0; padding:16px; border:1px solid var(--v2-border); border-radius:12px; background:var(--v2-main-surface); color:var(--v2-ink); font-size:13px; line-height:1.6; } -.v2-agent-browser header { min-width:0; display:flex; align-items:flex-start; justify-content:space-between; gap:10px; flex-wrap:wrap; } -.v2-agent-browser header > div { min-width:0; display:flex; align-items:center; gap:8px; flex-wrap:wrap; } -.v2-agent-browser button { display:inline-flex; align-items:center; gap:6px; min-height:36px; padding:6px 12px; border:1px solid var(--v2-border-strong); border-radius:8px; background:var(--v2-control); color:inherit; font:inherit; cursor:pointer; } +.v2-agent-browser { min-width:0; } +.v2-agent-browser header { min-width:0; display:flex; flex-shrink:0; align-items:flex-start; justify-content:space-between; gap:10px; padding:8px 16px 2px; } +.v2-agent-browser-heading { min-width:0; min-height:30px; display:flex; align-items:center; gap:4px 8px; flex-wrap:wrap; } +.v2-agent-browser-actions { display:flex; flex-shrink:0; align-items:center; gap:8px; } +.v2-agent-browser-body { min-height:0; min-width:0; display:grid; gap:8px; padding:2px 16px 10px; overflow:auto; overscroll-behavior:contain; font-size:13px; } +.v2-agent-browser button { display:inline-flex; align-items:center; gap:6px; min-height:30px; padding:4px 10px; border:1px solid var(--v2-border); border-radius:8px; background:transparent; color:inherit; font:inherit; font-size:13px; cursor:pointer; } +.v2-agent-browser .v2-agent-browser-dismiss { flex-shrink:0; justify-content:center; width:30px; height:30px; padding:0; border:0; border-radius:50%; color:var(--v2-muted); } .v2-agent-browser button:hover { background:var(--v2-control-hover); } .v2-agent-browser button:disabled { opacity:.6; cursor:wait; } .v2-agent-browser button:focus-visible { outline:2px solid var(--v2-accent); outline-offset:2px; } @@ -15,9 +18,3 @@ .v2-agent-browser details { min-width:0; } .v2-agent-browser summary { cursor:pointer; color:var(--v2-muted); } .v2-agent-browser details p { margin-top:7px; } -@media (max-width:390px) { - .v2-agent-browser { padding:12px; gap:10px; } - .v2-agent-browser header, .v2-agent-browser header > div { align-items:stretch; } - .v2-agent-browser header > div, .v2-agent-browser header button { width:100%; } - .v2-agent-browser header button { justify-content:center; } -} diff --git a/web/src/v2/components/agent-browser.test.tsx b/web/src/v2/components/agent-browser.test.tsx index 7c7083f3a..5d914bcb0 100644 --- a/web/src/v2/components/agent-browser.test.tsx +++ b/web/src/v2/components/agent-browser.test.tsx @@ -1,6 +1,5 @@ import { act, cleanup, fireEvent, render, screen, waitFor } from "@testing-library/react"; import { QueryClient, QueryClientProvider } from "@tanstack/react-query"; -import { readFileSync } from "node:fs"; import { APIRequestError, type APIClient } from "../../api/client"; import { V2AgentBrowser } from "./agent-browser"; import { agentBrowserQueryKey } from "../../api/agent-browser"; @@ -14,9 +13,9 @@ const status = (runID = "run-browser", sessionID = "agent-browser-one") => ({ cleanup_pending: false, tree_reaped: false, profile_removed: false, }); -function draw(client: APIClient, runID = "run-browser") { +function draw(client: APIClient, runID = "run-browser", onDismiss?: () => void) { const queries = new QueryClient({ defaultOptions: { queries: { retry: false, gcTime: 0 }, mutations: { retry: false } } }); - const view = render(); + const view = render(); return { ...view, queries }; } @@ -125,14 +124,58 @@ it.each([ expect(client.postControl).not.toHaveBeenCalled(); }); -it("hides an older server without the status route and has a narrow wrapping layout", async () => { +it("keeps a dismissed notice hidden across refreshes and shows a new browser session", async () => { + const unavailable = { ...status(), available: false, can_start: false, session_id: undefined, + screenshot: undefined, state: "unavailable", unavailable_code: "full_access_required" }; + const get = vi.fn(async () => unavailable as unknown); + const client = { baseURL: "/api/v1", get, postControl: vi.fn(), downloadVerifiedImage: vi.fn() } as unknown as APIClient; + const onDismiss = vi.fn(); + const view = draw(client, "run-browser", onDismiss); + fireEvent.click(await screen.findByRole("button", { name: "关闭浏览器提示" })); + expect(screen.queryByRole("region", { name: "Agent 浏览器" })).not.toBeInTheDocument(); + expect(onDismiss).toHaveBeenCalledTimes(1); + get.mockResolvedValue({ ...unavailable, updated_at: "2026-10-19T10:00:00Z" }); + await act(async () => { await view.queries.refetchQueries({ queryKey: agentBrowserQueryKey(client.baseURL, "run-browser") }); }); + expect(screen.queryByRole("region", { name: "Agent 浏览器" })).not.toBeInTheDocument(); + get.mockResolvedValue({ ...status(), screenshot: undefined }); + await act(async () => { await view.queries.refetchQueries({ queryKey: agentBrowserQueryKey(client.baseURL, "run-browser") }); }); + expect(await screen.findByText("浏览器正在运行")).toBeInTheDocument(); + expect(client.postControl).not.toHaveBeenCalled(); +}); + +it.each([ + [{ state: "waiting_user" }, "网页需要人工处理"], + [{ state: "ready", failure_code: "action_failed" }, "最近一次页面操作失败,请让 Agent 重新读取页面后再试。"], +])("dismisses routine activity without stopping it and resurfaces for %o", async (attention, text) => { + const client = { baseURL: "/api/v1", get: vi.fn(async () => ({ ...status(), screenshot: undefined })), + postControl: vi.fn(), downloadVerifiedImage: vi.fn() } as unknown as APIClient; + const view = draw(client); + fireEvent.click(await screen.findByRole("button", { name: "关闭浏览器提示" })); + await act(async () => { view.queries.setQueryData(agentBrowserQueryKey(client.baseURL, "run-browser"), + { ...status(), state: "loading", screenshot: undefined }); }); + expect(screen.queryByRole("region", { name: "Agent 浏览器" })).not.toBeInTheDocument(); + await act(async () => { view.queries.setQueryData(agentBrowserQueryKey(client.baseURL, "run-browser"), + { ...status(), ...attention, screenshot: undefined }); }); + expect(await screen.findByText(text)).toBeInTheDocument(); + expect(screen.getByRole("button", { name: "停止浏览器" })).toBeEnabled(); + expect(client.postControl).not.toHaveBeenCalled(); +}); + +it("allows dismissing a status error and resets dismissal when changing Runs", async () => { + const client = { baseURL: "/api/v1", get: vi.fn(async () => { throw new Error("offline"); }), + postControl: vi.fn(), downloadVerifiedImage: vi.fn() } as unknown as APIClient; + const view = draw(client); + fireEvent.click(await screen.findByRole("button", { name: "关闭浏览器提示" })); + expect(screen.queryByRole("alert")).not.toBeInTheDocument(); + view.rerender(); + expect(await screen.findByRole("alert")).toHaveTextContent("无法读取浏览器状态"); + expect(screen.getByRole("button", { name: "重新读取" })).toBeEnabled(); +}); + +it("hides an older server without the status route", async () => { const client = { baseURL: "/api/v1", get: vi.fn(async () => { throw new APIRequestError("missing", "NOT_FOUND", 404); }), postControl: vi.fn(), downloadVerifiedImage: vi.fn() } as unknown as APIClient; const page = draw(client); await waitFor(() => expect(client.get).toHaveBeenCalled()); expect(page.container).toBeEmptyDOMElement(); - const styles = readFileSync("src/v2/components/agent-browser.css", "utf8"); - expect(styles).toContain("@media (max-width:390px)"); - expect(styles).toContain("flex-wrap:wrap"); - expect(styles).toContain("min-width:0"); }); diff --git a/web/src/v2/components/agent-browser.tsx b/web/src/v2/components/agent-browser.tsx index 748ba5b2c..3119b8c0d 100644 --- a/web/src/v2/components/agent-browser.tsx +++ b/web/src/v2/components/agent-browser.tsx @@ -1,11 +1,11 @@ import { useEffect, useState } from "react"; import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query"; -import { ExternalLink, RefreshCw } from "lucide-react"; +import { ExternalLink, RefreshCw, X } from "lucide-react"; import type { APIClient } from "../../api/client"; import { agentBrowserAPIAbsent, agentBrowserQueryKey, closeAgentBrowser, readAgentBrowser, type AgentBrowserStatus } from "../../api/agent-browser"; import "./agent-browser.css"; -export function V2AgentBrowser(props: { client: APIClient; runID: string; running: boolean }) { +export function V2AgentBrowser(props: { client: APIClient; runID: string; running: boolean; onDismiss?: () => void }) { return ; } @@ -49,7 +49,7 @@ function screenshotPath(runID: string, status: AgentBrowserStatus): string { })}`; } -function AgentBrowserCard({ client, runID, running }: { client: APIClient; runID: string; running: boolean }) { +function AgentBrowserCard({ client, runID, running, onDismiss }: { client: APIClient; runID: string; running: boolean; onDismiss?: () => void }) { const queries = useQueryClient(); const key = agentBrowserQueryKey(client.baseURL, runID); const [message, setMessage] = useState(""); @@ -57,6 +57,10 @@ function AgentBrowserCard({ client, runID, running }: { client: APIClient; runID const status = useQuery({ queryKey: key, queryFn: ({ signal }) => readAgentBrowser(client, runID, signal), retry: false, refetchOnMount: "always", refetchInterval: (query) => running && !query.state.error ? 3000 : false }); const current = status.data; + const noticeState = current && ["starting", "ready", "loading", "busy"].includes(current.state) ? "active" : current?.state; + const noticeKey = JSON.stringify([status.isError, current?.session_id, noticeState, current?.unavailable_code, current?.failure_code]); + const [dismissed, setDismissed] = useState(false); + useEffect(() => { setDismissed(false); }, [noticeKey]); const close = useMutation({ retry: false, mutationFn: (sessionID: string) => closeAgentBrowser(client, runID, sessionID), onSuccess: (next, sessionID) => { queries.setQueryData(key, (observed) => @@ -80,15 +84,19 @@ function AgentBrowserCard({ client, runID, running }: { client: APIClient; runID if (status.isError && agentBrowserAPIAbsent(status.error)) return null; if (status.isPending) return null; - if (status.isError) return
Agent 浏览器 -
-

无法读取浏览器状态。自动读取已暂停。

; + if (dismissed) return null; + const dismissButton = ; + if (status.isError) return
Agent 浏览器 +
{dismissButton}
+

无法读取浏览器状态。自动读取已暂停。

; if (!current) return null; const canClose = Boolean(current.session_id) && ["starting", "ready", "loading", "busy", "waiting_user", "failed"].includes(current.state); const active = canClose || current.state === "closing" || current.state === "cleanup_pending"; - return
-
Agent 浏览器{stateText(current)}
- {canClose && }
+ return
+
Agent 浏览器{stateText(current)}
+
{canClose && }{dismissButton}
+
{!current.available &&

{unavailableText(current.unavailable_code)}

} {current.available && !active &&

{current.headless ? "页面将在独立的后台浏览器中打开,通过截图查看。" : "页面将在独立浏览器窗口中打开。"}

} {current.headless && active &&

浏览器在后台运行,当前页面以已验证的截图展示。需要结束网页操作时可停止浏览器;整个任务可在对话中停止。

} @@ -105,5 +113,6 @@ function AgentBrowserCard({ client, runID, running }: { client: APIClient; runID {current.cleanup_pending &&

浏览器进程或临时资料仍在清理。

} } {message &&

{message}

} +
; } diff --git a/web/src/v2/components/approval-cards.test.tsx b/web/src/v2/components/approval-cards.test.tsx index c52eb1cda..3e81ac95d 100644 --- a/web/src/v2/components/approval-cards.test.tsx +++ b/web/src/v2/components/approval-cards.test.tsx @@ -87,10 +87,10 @@ function renderReadOnlyCards(item: ApprovalQueueItemView | null = pending(), vi.stubGlobal("fetch", fetchMock); const client = new APIClient("test-read", "/api/v1"); const decideApproval = vi.spyOn(client, "decideApproval"); - render( + const view = render( ); - return { fetchMock, decideApproval, restoreReads: () => { failure = null; } }; + return { ...view, fetchMock, decideApproval, restoreReads: () => { failure = null; } }; } describe("V2ApprovalCards", () => { @@ -116,18 +116,16 @@ describe("V2ApprovalCards", () => { expect(decideApproval).not.toHaveBeenCalled(); }); - it("distinguishes a successful empty queue from an unread queue", async () => { - const { fetchMock } = renderReadOnlyCards(null); - expect(await screen.findByText("需要你确认的操作会显示在这里。")).toBeVisible(); - expect(screen.queryByRole("region", { name: "待处理审批" })).not.toBeInTheDocument(); - expect(screen.queryByRole("alert")).not.toBeInTheDocument(); + it("leaves no placeholder after successfully reading an empty queue", async () => { + const { container, fetchMock } = renderReadOnlyCards(null); + expect(screen.getByText("正在读取待审批操作…")).toBeVisible(); + await waitFor(() => expect(container).toBeEmptyDOMElement()); expect(fetchMock).toHaveBeenCalledTimes(1); }); it("reports a failed queue read and retries with the read token", async () => { const { fetchMock, decideApproval, restoreReads } = renderReadOnlyCards(pending(), {}, "queue"); expect(await screen.findByRole("alert")).toHaveTextContent("无法读取待审批操作"); - expect(screen.queryByText("需要你确认的操作会显示在这里。")).not.toBeInTheDocument(); restoreReads(); await userEvent.click(screen.getByRole("button", { name: "重试审批队列" })); expect(await screen.findByText("Read-only exact proposal [REDACTED]")).toBeVisible(); diff --git a/web/src/v2/components/approval-cards.tsx b/web/src/v2/components/approval-cards.tsx index 2fde3fa6e..480e1a048 100644 --- a/web/src/v2/components/approval-cards.tsx +++ b/web/src/v2/components/approval-cards.tsx @@ -51,7 +51,6 @@ export function V2ApprovalCards({ client, runID, threadID, onReviewFile }: { 无法读取待审批操作,请重试后再作决定。 } - {query.isSuccess && query.data.items.length === 0 &&

需要你确认的操作会显示在这里。

} {Boolean(query.data?.items.length) &&
{!client.hasApprovalControl &&

当前连接可以查看审批。需要批准、拒绝或恢复时,请使用具有审批控制权限的连接。 diff --git a/web/src/v2/components/conversation.tsx b/web/src/v2/components/conversation.tsx index d3cca26f8..c534238e5 100644 --- a/web/src/v2/components/conversation.tsx +++ b/web/src/v2/components/conversation.tsx @@ -714,7 +714,10 @@ export function V2Conversation({ client, threadID, workspaces, onArchive, onMana const frame = requestAnimationFrame(() => { const container = composerContainerRef.current; if (container?.dataset.threadId === composerFocusRequest.threadID && !container.hidden) { - container.querySelector('textarea, [contenteditable="true"][role="textbox"]')?.focus(); + const compact = container.querySelector(".v2-composer-compact-trigger"); + const target = compact?.getClientRects().length ? compact + : container.querySelector('textarea, [contenteditable="true"][role="textbox"]'); + target?.focus(); } setComposerFocusRequest((current) => current === composerFocusRequest ? null : current); }); @@ -1042,7 +1045,6 @@ export function V2Conversation({ client, threadID, workspaces, onArchive, onMana

{detail.thread.status === "archived" ? t("此归档对话没有公开进展记录。", "This archived conversation has no public progress history.") : t("任务已经创建。Agent 的公开进展会出现在这里。", "Task created. Agent progress will appear here.")}

} {view === "conversation" && } - {view === "conversation" && } {submissionNotices.map(({ input, error }) =>

{v2TurnWasNotQueued(error) ? t("这条消息未入队,草稿与引用已保留。请重新选择、移除引用或处理当前限制后重试。", "This message was not queued. Your draft and references are kept. Update the references or resolve the current restrictions, then retry.") @@ -1120,6 +1122,8 @@ export function V2Conversation({ client, threadID, workspaces, onArchive, onMana

}
+ {view === "conversation" && setComposerFocusRequest({ threadID })} />} {client.hasControl && setComposerFocusRequest({ threadID })} />} diff --git a/web/src/v2/components/settings.css b/web/src/v2/components/settings.css index 6415d4c7d..62c0eb47c 100644 --- a/web/src/v2/components/settings.css +++ b/web/src/v2/components/settings.css @@ -325,8 +325,59 @@ .v2-theme-preview { background: var(--prayu-window); border: 1px solid var(--prayu-border-strong); border-radius: 8px; display: grid; grid-template-columns: 32% 1fr; height: 72px; overflow: hidden; width: 100%; } .v2-theme-preview i:first-child { background: var(--prayu-sidebar); } .v2-theme-preview i:last-child { background: var(--prayu-panel); box-shadow: inset 0 1px 0 var(--prayu-glass-highlight); } -.v2-theme-preview.theme-glass { background: linear-gradient(130deg, #a4b7bd, #74807c 48%, #595f63); } +.v2-theme-preview.theme-glass { + background: + radial-gradient(65% 55% at 18% 12%, rgb(101 125 141 / 10%), transparent 70%), + radial-gradient(55% 50% at 85% 18%, rgb(139 153 159 / 8%), transparent 65%), + linear-gradient(165deg, #f6f8fb 0%, #eff1f5 55%, #e3e6ec 100%); + border-color: rgb(76 79 105 / 22%); + gap: 5px; + padding: 5px; +} +.v2-theme-preview.theme-glass i:first-child, +.v2-theme-preview.theme-glass i:last-child { + background: rgb(255 255 255 / 62%); + border: 1px solid rgb(76 79 105 / 18%); + border-radius: 5px; + box-shadow: inset 0 1px 0 rgb(255 255 255 / 72%), 0 2px 6px rgb(76 79 105 / 8%); +} .v2-appearance-card > p { border-top: 1px solid var(--v2-border); color: var(--v2-muted); margin: 0; padding: 13px 15px; } +:where([data-prayu-theme="glass"]) .v2-settings-card, +:where([data-prayu-theme="glass"]) .v2-provider-list, +:where([data-prayu-theme="glass"]) .v2-archive-list { + background: var(--dsh-glass-card); + border-color: var(--dsh-glass-rim); + box-shadow: var(--dsh-glass-edge), var(--dsh-glass-drop); + -webkit-backdrop-filter: blur(var(--prayu-glass-blur, 5px)); + backdrop-filter: blur(var(--prayu-glass-blur, 5px)); +} +.v2-glass-slider-control { + align-items: center; + display: flex; + gap: 10px; + min-width: 190px; +} +.v2-glass-slider-control input[type="range"] { + accent-color: var(--v2-accent); + cursor: pointer; + flex: 1 1 auto; + margin: 0; + min-width: 110px; +} +.v2-glass-slider-control output { + color: var(--v2-ink); + font-family: var(--prayu-font-mono); + font-size: 12px; + font-variant-numeric: tabular-nums; + min-width: 48px; + text-align: right; +} +.v2-glass-settings-card .v2-setting-segmented button[aria-pressed="true"] { + background: color-mix(in srgb, var(--v2-accent) 12%, var(--v2-control)); + border-color: color-mix(in srgb, var(--v2-accent) 42%, var(--v2-border)); + color: var(--v2-accent); + font-weight: 600; +} .v2-settings-placeholder { align-items: center; display: flex; flex-direction: column; padding: 42px 24px; text-align: center; } .v2-settings-placeholder > svg { color: var(--v2-muted); margin-bottom: 12px; } @@ -385,8 +436,10 @@ } @media (prefers-reduced-transparency: reduce) { - .v2-provider-list, -.v2-provider-fields { backdrop-filter: none; background: var(--prayu-panel-strong); } + .v2-settings-card, +.v2-archive-list, +.v2-provider-list, +.v2-provider-fields { -webkit-backdrop-filter: none; backdrop-filter: none; background: var(--prayu-panel-strong); } .v2-provider-harness-result { background: var(--v2-control); } } diff --git a/web/src/v2/components/settings.test.tsx b/web/src/v2/components/settings.test.tsx index 670d08f77..bfc4668fa 100644 --- a/web/src/v2/components/settings.test.tsx +++ b/web/src/v2/components/settings.test.tsx @@ -338,3 +338,59 @@ describe("V2 permission settings hierarchy", () => { expect(postControl).not.toHaveBeenCalled(); }); }); + +describe("V2 appearance and mist glass controls", () => { + afterEach(() => { + window.localStorage.removeItem("prayu.theme"); + window.localStorage.removeItem("prayu.glass"); + }); + + it("reveals glass tuning controls when glass theme is selected and syncs CSS variables", async () => { + window.localStorage.setItem("prayu.theme", "dark"); + const user = userEvent.setup(); + render( + + ); + + expect(screen.queryByRole("region", { name: "雾透玻璃参数" })).not.toBeInTheDocument(); + + const themeGroup = screen.getByRole("group", { name: "应用主题" }); + await user.click(within(themeGroup).getByRole("button", { name: "雾透" })); + + const glassRegion = screen.getByRole("region", { name: "雾透玻璃参数" }); + expect(glassRegion).toBeVisible(); + expect(document.documentElement.dataset.prayuTheme).toBe("glass"); + expect(document.documentElement.dataset.prayuGlassMode).toBe("mica"); + + const resetButton = within(glassRegion).getByRole("button", { name: "还原默认值" }); + expect(resetButton).toBeDisabled(); + + await user.click(within(glassRegion).getByRole("button", { name: "兼容模式" })); + expect(document.documentElement.dataset.prayuGlassMode).toBe("compat"); + expect(resetButton).toBeEnabled(); + + await user.click(within(glassRegion).getByRole("button", { name: "磨砂" })); + expect(document.documentElement.style.getPropertyValue("--prayu-glass-blur")).toBe("12px"); + expect(document.documentElement.style.getPropertyValue("--prayu-glass-frost")).toBe("0.9"); + + fireEvent.change(within(glassRegion).getByRole("slider", { name: "背景亮度" }), { + target: { value: "80" }, + }); + expect(document.documentElement.style.getPropertyValue("--prayu-glass-brightness-white")).toBe("0.6"); + + const glowSwitch = within(glassRegion).getByRole("switch", { name: "氛围光斑" }); + await user.click(glowSwitch); + expect(glowSwitch).toHaveAttribute("aria-checked", "true"); + expect(document.documentElement.style.getPropertyValue("--prayu-glass-glow")).toBe("1"); + + await user.click(resetButton); + expect(document.documentElement.dataset.prayuGlassMode).toBe("mica"); + expect(document.documentElement.style.getPropertyValue("--prayu-glass-blur")).toBe("5px"); + expect(document.documentElement.style.getPropertyValue("--prayu-glass-frost")).toBe("0.4"); + expect(document.documentElement.style.getPropertyValue("--prayu-glass-brightness-white")).toBe("0"); + expect(document.documentElement.style.getPropertyValue("--prayu-glass-glow")).toBe("0"); + expect(resetButton).toBeDisabled(); + }); +}); + diff --git a/web/src/v2/components/settings.tsx b/web/src/v2/components/settings.tsx index 148d88f50..7956890ea 100644 --- a/web/src/v2/components/settings.tsx +++ b/web/src/v2/components/settings.tsx @@ -4,7 +4,18 @@ import { useInfiniteQuery, useMutation, useQuery, useQueryClient } from "@tansta import { ArchiveRestore, ArrowRight, BookOpen, Monitor, Search, Trash2, X } from "lucide-react"; import type { APIClient } from "../../api/client"; import type { ProviderDefinitionView, TaskBudgetSettings, ThreadDetailView, ThreadView, WebAccessMode, WorkspaceView } from "../../api/types"; -import { applyPrayuTheme, readPrayuTheme, type PrayuTheme } from "../../lib/appearance"; +import { + applyPrayuGlassSettings, + applyPrayuTheme, + defaultPrayuGlassSettings, + prayuGlassPresets, + readPrayuGlassSettings, + readPrayuTheme, + resetPrayuGlassSettings, + type PrayuGlassPreset, + type PrayuGlassSettings, + type PrayuTheme, +} from "../../lib/appearance"; import { useModalFocusTrap } from "../../hooks/use-modal-focus-trap"; import { v2QueryKeys } from "../query-keys"; import type { V2SettingsSection } from "./sidebar"; @@ -170,7 +181,33 @@ function GeneralSettings({ threadID, workspaces, onPermissions }: { function AppearanceSettings() { const { t } = useLocale(); const [theme, setTheme] = useState(() => readPrayuTheme()); - const select = (next: PrayuTheme) => { setTheme(next); applyPrayuTheme(next); }; + const [glass, setGlass] = useState(() => readPrayuGlassSettings()); + const select = (next: PrayuTheme) => { + setTheme(next); + applyPrayuTheme(next); + }; + const updateGlass = (patch: Partial) => { + setGlass(applyPrayuGlassSettings(patch)); + }; + const applyPreset = (preset: PrayuGlassPreset) => { + updateGlass(prayuGlassPresets[preset]); + }; + const resetGlass = () => { + setGlass(resetPrayuGlassSettings()); + }; + const activePreset: PrayuGlassPreset | null = + (["clear", "standard", "frosted"] as const).find( + (key) => + glass.blur === prayuGlassPresets[key].blur && + glass.frost === prayuGlassPresets[key].frost && + glass.brightness === prayuGlassPresets[key].brightness, + ) ?? null; + const isDefaultGlass = + glass.mode === defaultPrayuGlassSettings.mode && + glass.blur === defaultPrayuGlassSettings.blur && + glass.frost === defaultPrayuGlassSettings.frost && + glass.brightness === defaultPrayuGlassSettings.brightness && + glass.glow === defaultPrayuGlassSettings.glow; return <>

{t("外观", "Appearance")}

{t("主题与材质", "Theme & materials")}

@@ -179,8 +216,57 @@ function AppearanceSettings() { {option === "light" ? t("浅色", "Light") : option === "dark" ? t("深色", "Dark") : t("雾透", "Mist")} )}
-

{t("雾透让窗口背景透出柔和的灰蓝层次;深色使用深灰玻璃材质。开启系统的「降低透明度」后,界面会使用清晰的实色背景。", "Mist adds soft blue-gray translucency; Dark uses dark gray glass. Enable Reduce transparency in your system settings to use solid backgrounds.")}

-
; +

{t("雾透采用灰蓝透色与轻盈的磨砂质感;深色使用深灰玻璃材质。开启系统的「降低透明度」后,界面会使用清晰的实色背景。", "Mist combines a muted blue-gray tint with light frosted glass; Dark uses dark gray glass. Enable Reduce transparency in your system settings to use solid backgrounds.")}

+
+ {theme === "glass" &&
+

{t("雾透材质", "Mist material")}

+
+ +
+ + +
+
+ +
+ + + +
+
+ +
+ updateGlass({ blur: Number(event.currentTarget.value) })} + step={0.5} type="range" value={glass.blur} /> + {glass.blur} px +
+
+ +
+ updateGlass({ frost: Number(event.currentTarget.value) })} + step={1} type="range" value={glass.frost} /> + {glass.frost}% +
+
+ +
+ updateGlass({ brightness: Number(event.currentTarget.value) })} + step={1} type="range" value={glass.brightness} /> + {glass.brightness}% +
+
+ + + + + + +
+
}; } function ModelSettingsPage({ client, initialAdvancedOpen = false, prepareForDraft = false, diff --git a/web/src/v2/components/thread-context-controls.css b/web/src/v2/components/thread-context-controls.css index 5440af00c..c23c06ccd 100644 --- a/web/src/v2/components/thread-context-controls.css +++ b/web/src/v2/components/thread-context-controls.css @@ -20,9 +20,8 @@ .v2-context-compaction-error { white-space:pre-wrap; } .v2-context-compact-control :is(input, button, summary):focus-visible { outline:2px solid var(--v2-accent); outline-offset:3px; } .v2-context-resolved-reply { padding:10px 0; border-bottom:1px solid var(--v2-border); white-space:pre-wrap; } -.v2-composer-dock:has(.v2-context-pin-card) { display:flex; flex-direction:column; box-sizing:border-box; max-height:calc(100% - 24px); overflow-y:auto; overscroll-behavior-y:contain; } -.v2-composer-dock:has(.v2-context-pin-card) > :not(.v2-context-pin-card) { flex-shrink:0; } .v2-composer-dock:has(.v2-context-pin-card) .v2-composer-surface { min-height:0; } +.v2-composer-dock:has(.v2-context-pin-card):has(.v2-agent-browser) .v2-markdown-input { max-height:176px; } .v2-composer-dock:has(.v2-context-pin-card) .v2-markdown-input:has(> p:only-child > br:only-child) { min-height:0; padding-block:8px 2px; } .v2-composer-dock:has(.v2-context-pin-card) .v2-composer textarea:placeholder-shown { min-height:32px; padding-block:8px 2px; } .v2-composer-dock:has(.v2-context-pin-card) .v2-composer-footer { min-height:40px; padding-block:4px 8px; } @@ -36,7 +35,6 @@ .v2-composer-add { order:1; } .v2-composer-actions { order:3; flex-shrink:0; max-width:60%; } } -.v2-context-pin-card { display:flex; flex-direction:column; flex-shrink:1; box-sizing:border-box; margin:0 auto 8px; width:100%; max-width:var(--v2-composer-width); min-height:0; max-height:min(320px, 42dvh); overflow:hidden; overflow-wrap:anywhere; border:1px solid var(--v2-border); border-radius:20px; background:var(--prayu-surface-solid); color:var(--v2-ink); font-size:14px; line-height:1.5; } .v2-context-pin-card > header { display:flex; align-items:center; gap:9px; flex-shrink:0; padding:8px 16px 2px; color:var(--v2-muted); } .v2-context-pin-card button { font:inherit; color:inherit; cursor:pointer; } .v2-context-pin-card > header button { display:inline-flex; align-items:center; justify-content:center; margin-left:auto; width:30px; height:30px; padding:0; border:0; border-radius:50%; background:transparent; } diff --git a/web/src/v2/components/thread-context-controls.tsx b/web/src/v2/components/thread-context-controls.tsx index 46cd3bb65..e6dae7382 100644 --- a/web/src/v2/components/thread-context-controls.tsx +++ b/web/src/v2/components/thread-context-controls.tsx @@ -268,7 +268,7 @@ function PinPromptCard({ client, threadID, state, proposal, onReply, onInteracti }; const close = () => { if (resolved) onClose(); else void resolve("dismiss"); }; const choose = (answer: ThreadContextPinChoice) => { onInteraction(); setChoice(answer); }; - return
+ return
diff --git a/web/src/v2/styles.css b/web/src/v2/styles.css index 58529e36d..067ed9cdd 100644 --- a/web/src/v2/styles.css +++ b/web/src/v2/styles.css @@ -1111,6 +1111,9 @@ position: relative; z-index: 30; } +.v2-composer-dock:has(.v2-composer-card) { display: flex; flex-direction: column; box-sizing: border-box; max-height: calc(100% - 24px); } +.v2-composer-dock:has(.v2-composer-card) > :not(.v2-composer-card) { flex-shrink: 0; } +.v2-composer-card { display: flex; flex-direction: column; flex-shrink: 1; box-sizing: border-box; margin: 0 auto 8px; width: 100%; max-width: var(--v2-composer-width); min-height: 0; max-height: min(320px, 42dvh); overflow: hidden; overflow-wrap: anywhere; border: 1px solid var(--v2-border); border-radius: 20px; background: var(--prayu-surface-solid); color: var(--v2-ink); font-size: 14px; line-height: 1.5; } .v2-run-recovery { align-items: center; backdrop-filter: blur(30px) saturate(100%); @@ -1994,20 +1997,219 @@ .v2-approval-card { corner-shape: superellipse(1.5); } } -:where([data-prayu-theme="dark"], [data-prayu-theme="glass"]) .v2-sidebar, -:where([data-prayu-theme="dark"], [data-prayu-theme="glass"]) .v2-product-surface { backdrop-filter: blur(46px) saturate(100%); } +:where([data-prayu-theme="dark"]) .v2-sidebar, +:where([data-prayu-theme="dark"]) .v2-product-surface { backdrop-filter: blur(46px) saturate(100%); } + +/* Mist glass: floating and framed layouts share the same material controls. */ +:where([data-prayu-theme="glass"]) .v2-shell { + background: + radial-gradient(58% 46% at 18% 6%, + color-mix(in srgb, #657d8d calc(10% * var(--dsh-glass-ambience, 1) * var(--prayu-glass-glow, 0)), transparent), + transparent 68%), + radial-gradient(48% 42% at 86% 14%, + color-mix(in srgb, #8b999f calc(8% * var(--dsh-glass-ambience, 1) * var(--prayu-glass-glow, 0)), transparent), + transparent 62%), + linear-gradient(165deg, + color-mix(in srgb, var(--dsh-glass-ground), #ffffff calc(22% * var(--dsh-glass-ambience, 1))) 0%, + var(--dsh-glass-ground) 55%, + color-mix(in srgb, var(--dsh-glass-ground), #000000 calc(5% * var(--dsh-glass-ambience, 1))) 100%); +} + +:where([data-prayu-theme="glass"]) .v2-sidebar::before { content: none; } + +:where([data-prayu-theme="glass"]) .v2-sidebar { + -webkit-backdrop-filter: blur(var(--prayu-glass-blur, 5px)); + backdrop-filter: blur(var(--prayu-glass-blur, 5px)); +} + +:where([data-prayu-theme="glass"]:not([data-prayu-glass-mode="compat"])) .v2-titlebar { + background: transparent; + border-bottom-color: transparent; + -webkit-backdrop-filter: none; + backdrop-filter: none; +} + +:where([data-prayu-theme="glass"]:not([data-prayu-glass-mode="compat"])) .v2-product-surface { + background: transparent; + border-left: 0; + border-radius: 0; + box-shadow: none; + -webkit-backdrop-filter: none; + backdrop-filter: none; +} + +@media (min-width: 761px) { + :where([data-prayu-theme="glass"]:not([data-prayu-glass-mode="compat"])) .v2-sidebar { + margin: 6px 8px 12px 12px; + border-radius: 20px; + border: 1px solid var(--dsh-glass-rim); + background: var(--dsh-glass-card); + box-shadow: var(--dsh-glass-edge), var(--dsh-glass-drop); + overflow: hidden; + } +} + +:where([data-prayu-theme="glass"]:not([data-prayu-glass-mode="compat"])) .v2-new-conversation { + grid-template-rows: auto minmax(0, 1fr) auto; +} + +:where([data-prayu-theme="glass"]:not([data-prayu-glass-mode="compat"])) .v2-conversation { + grid-template-rows: auto auto minmax(0, 1fr); +} + +:where([data-prayu-theme="glass"]:not([data-prayu-glass-mode="compat"])) :is(.v2-conversation-header, .v2-new-conversation > header) { + margin: 6px 16px 0; + min-height: 44px; + padding: 6px 16px; + border-radius: 20px; + border: 1px solid var(--dsh-glass-rim); + background: var(--dsh-glass-card); + box-shadow: var(--dsh-glass-edge), var(--dsh-glass-drop); + -webkit-backdrop-filter: blur(var(--prayu-glass-blur, 5px)); + backdrop-filter: blur(var(--prayu-glass-blur, 5px)); + position: relative; + z-index: 8; +} + +:where([data-prayu-theme="glass"]:not([data-prayu-glass-mode="compat"])) .v2-task-navigation { + margin: 8px 16px 0; + border-radius: 14px; + border: 1px solid var(--dsh-glass-rim-soft); + background: var(--dsh-glass-card); + box-shadow: var(--dsh-glass-edge); + -webkit-backdrop-filter: blur(var(--prayu-glass-blur, 5px)); + backdrop-filter: blur(var(--prayu-glass-blur, 5px)); +} + +:where([data-prayu-theme="glass"]:not([data-prayu-glass-mode="compat"])) .v2-settings-toolbar { + background: transparent; + border-bottom-color: transparent; +} + +:where([data-prayu-theme="glass"][data-prayu-glass-mode="compat"]) .v2-titlebar { + background: var(--prayu-titlebar); + border-bottom-color: var(--dsh-glass-rim-soft); + -webkit-backdrop-filter: blur(var(--prayu-glass-blur, 5px)); + backdrop-filter: blur(var(--prayu-glass-blur, 5px)); +} + +:where([data-prayu-theme="glass"][data-prayu-glass-mode="compat"]) .v2-sidebar { + background: var(--dsh-glass-card); + border-right: 1px solid var(--dsh-glass-rim-soft); +} + +:where([data-prayu-theme="glass"][data-prayu-glass-mode="compat"]) .v2-product-surface { + background: transparent; + border-left: 0; + border-radius: 0; + box-shadow: none; + -webkit-backdrop-filter: none; + backdrop-filter: none; +} + +:where([data-prayu-theme="glass"][data-prayu-glass-mode="compat"]) :is(.v2-conversation-header, .v2-new-conversation > header, .v2-settings-toolbar) { + background: var(--dsh-glass-card); + border-bottom: 1px solid var(--dsh-glass-rim-soft); + -webkit-backdrop-filter: blur(var(--prayu-glass-blur, 5px)); + backdrop-filter: blur(var(--prayu-glass-blur, 5px)); +} + +:where([data-prayu-theme="glass"]) .v2-sidebar-actions > button:first-child { + background: var(--dsh-glass-card-raised); + border: 1px solid var(--dsh-glass-rim); + box-shadow: var(--dsh-glass-edge); + -webkit-backdrop-filter: blur(var(--prayu-glass-blur, 5px)); + backdrop-filter: blur(var(--prayu-glass-blur, 5px)); + font-weight: 500; +} + +:where([data-prayu-theme="glass"]) .v2-sidebar-actions > button:first-child:hover { + background: var(--dsh-glass-card-hover); + border-color: var(--dsh-glass-rim-strong); +} + +:where([data-prayu-theme="glass"]) :is(.v2-sidebar-search, .v2-settings-search) { + background: var(--dsh-glass-card-raised); + border-color: var(--dsh-glass-rim-soft); + box-shadow: var(--dsh-glass-edge); +} -/* Mist keeps the workspace backdrop visible without the composer's beveled sheen. */ :where([data-prayu-theme="glass"]) .v2-composer-dock { background: transparent; } :where([data-prayu-theme="glass"]) .v2-composer-surface { - background: var(--prayu-glass-fill); - border-color: var(--v2-border); - box-shadow: 0 4px 14px color-mix(in srgb, var(--prayu-shadow) 35%, transparent), - inset 0 1px 0 color-mix(in srgb, var(--prayu-glass-highlight) 50%, transparent); + background: var(--dsh-glass-card); + border: 1px solid var(--dsh-glass-rim-strong); + border-radius: 24px; + box-shadow: var(--dsh-glass-edge), var(--dsh-glass-input-shadow); + -webkit-backdrop-filter: blur(var(--prayu-glass-blur, 5px)); + backdrop-filter: blur(var(--prayu-glass-blur, 5px)); +} +:where([data-prayu-theme="glass"]) .v2-composer-surface:focus-within { + border-color: var(--prayu-focus); +} +:where([data-prayu-theme="glass"]) .v2-composer-surface:has(textarea:focus-visible) { + outline-offset: -2px; } -:where([data-prayu-theme="glass"]) .v2-composer-surface:focus-within { border-color: var(--v2-border-strong); } :where([data-prayu-theme="glass"]) .v2-composer-surface::after { content: none; } +:where([data-prayu-theme="glass"]) .v2-composer-add-trigger { + background: var(--dsh-glass-card-raised); + border: 1px solid var(--dsh-glass-rim); + box-shadow: var(--dsh-glass-edge); + color: var(--v2-ink); +} + +:where([data-prayu-theme="glass"]) .v2-composer-add-trigger:hover:not(:disabled), +:where([data-prayu-theme="glass"]) .v2-composer-add-trigger[aria-expanded="true"] { + background: var(--dsh-glass-card-hover); + border-color: var(--dsh-glass-rim-strong); +} + +:where([data-prayu-theme="glass"]) .v2-user-turn > div { + border-radius: 14px; + border: 1px solid var(--dsh-glass-rim-soft); + background: var(--dsh-glass-bubble); + box-shadow: var(--dsh-glass-edge); +} + +:where([data-prayu-theme="glass"]) :is(.v2-markdown-code-block, .v2-unified-diff, .v2-command-detail) { + background: var(--dsh-glass-card-raised); + border: 1px solid var(--dsh-glass-rim-soft); + box-shadow: var(--dsh-glass-edge); +} + +:where([data-prayu-theme="glass"]) .v2-orb i { + background: radial-gradient(circle at 28% 27%, #ffffff 0 22%, #e0e7eb 56%, #82949f 100%); + border-color: rgb(255 255 255 / 68%); +} + +:where([data-prayu-theme="glass"]) :is( + .v2-thread-menu, + .v2-composer-add-menu, + .v2-permission-popover, + .v2-network-popover, + .v2-run-network-popover, + .v2-model-route-popover, + .v2-dialog, + .v2-high-risk-dialog, + .v2-inspector-drawer, + .v2-file-drawer, + .v2-terminal-drawer +) { + background: var(--dsh-glass-popover); + border-color: var(--dsh-glass-rim); + box-shadow: var(--dsh-glass-edge), var(--dsh-glass-drop); + -webkit-backdrop-filter: blur(max(var(--prayu-glass-blur, 5px), 16px)) saturate(120%); + backdrop-filter: blur(max(var(--prayu-glass-blur, 5px), 16px)) saturate(120%); +} + +:where([data-prayu-theme="glass"]) .v2-inspector-drawer.is-docked { + background: var(--dsh-glass-card); + border-left: 1px solid var(--dsh-glass-rim); + box-shadow: var(--dsh-glass-edge); + -webkit-backdrop-filter: blur(var(--prayu-glass-blur, 5px)); + backdrop-filter: blur(var(--prayu-glass-blur, 5px)); +} + .v2-sidebar-backdrop { display: none; } .v2-workspace-start { margin-left: auto; position: relative; } .v2-project-import { max-height: calc(100dvh - 24px); overflow: auto; } @@ -2071,7 +2273,11 @@ @media (prefers-reduced-transparency: reduce) { .v2-titlebar, .v2-sidebar, +.v2-sidebar-actions > button:first-child, .v2-product-surface, +.v2-conversation-header, +.v2-new-conversation > header, +.v2-task-navigation, .v2-composer-surface, .v2-run-recovery, .v2-permission-popover, @@ -2079,7 +2285,7 @@ .v2-run-network-popover, .v2-dialog, .v2-inspector-drawer, -.v2-high-risk-dialog { backdrop-filter: none; background: var(--prayu-surface-solid); } +.v2-high-risk-dialog { -webkit-backdrop-filter: none; backdrop-filter: none; background: var(--prayu-surface-solid); } .v2-high-risk-overlay { backdrop-filter: none; background: rgb(24 24 24 / 42%); } .v2-sidebar { background: var(--prayu-surface-solid); } }