diff --git a/packages/app/src/components/dialog-edit-project.tsx b/packages/app/src/components/dialog-edit-project.tsx index 091f00702f3..7acb766f808 100644 --- a/packages/app/src/components/dialog-edit-project.tsx +++ b/packages/app/src/components/dialog-edit-project.tsx @@ -22,7 +22,7 @@ export function DialogEditProject(props: { project: LocalProject }) { const [store, setStore] = createStore({ name: defaultName(), color: props.project.icon?.color || "pink", - iconUrl: props.project.icon?.url || "", + iconUrl: props.project.icon?.override || "", saving: false, }) @@ -74,7 +74,7 @@ export function DialogEditProject(props: { project: LocalProject }) { await globalSDK.client.project.update({ projectID: props.project.id, name, - icon: { color: store.color, url: store.iconUrl }, + icon: { color: store.color, override: store.iconUrl }, }) setStore("saving", false) dialog.close() diff --git a/packages/app/src/context/layout.tsx b/packages/app/src/context/layout.tsx index a8da156092b..d7d09aa3999 100644 --- a/packages/app/src/context/layout.tsx +++ b/packages/app/src/context/layout.tsx @@ -33,8 +33,6 @@ type SessionTabs = { type SessionView = { scroll: Record reviewOpen?: string[] - terminalOpened?: boolean - reviewPanelOpened?: boolean } export type LocalProject = Partial & { worktree: string; expanded: boolean } @@ -78,9 +76,11 @@ export const { use: useLayout, provider: LayoutProvider } = createSimpleContext( }, terminal: { height: 280, + opened: false, }, review: { diffStyle: "split" as ReviewDiffStyle, + panelOpened: true, }, session: { width: 600, @@ -172,7 +172,7 @@ export const { use: useLayout, provider: LayoutProvider } = createSimpleContext( const current = store.sessionView[sessionKey] const keep = meta.active ?? sessionKey if (!current) { - setStore("sessionView", sessionKey, { scroll: next, terminalOpened: false, reviewPanelOpened: true }) + setStore("sessionView", sessionKey, { scroll: next }) prune(keep) return } @@ -208,10 +208,10 @@ export const { use: useLayout, provider: LayoutProvider } = createSimpleContext( }) }) - const usedColors = new Set() + const [colors, setColors] = createStore>({}) - function pickAvailableColor(): AvatarColorKey { - const available = AVATAR_COLOR_KEYS.filter((c) => !usedColors.has(c)) + function pickAvailableColor(used: Set): AvatarColorKey { + const available = AVATAR_COLOR_KEYS.filter((c) => !used.has(c)) if (available.length === 0) return AVATAR_COLOR_KEYS[Math.floor(Math.random() * AVATAR_COLOR_KEYS.length)] return available[Math.floor(Math.random() * available.length)] } @@ -222,24 +222,15 @@ export const { use: useLayout, provider: LayoutProvider } = createSimpleContext( const metadata = projectID ? globalSync.data.project.find((x) => x.id === projectID) : globalSync.data.project.find((x) => x.worktree === project.worktree) - return [ - { - ...(metadata ?? {}), - ...project, - icon: { url: metadata?.icon?.url, color: metadata?.icon?.color }, + return { + ...(metadata ?? {}), + ...project, + icon: { + url: metadata?.icon?.url, + override: metadata?.icon?.override, + color: metadata?.icon?.color, }, - ] - } - - function colorize(project: LocalProject) { - if (project.icon?.color) return project - const color = pickAvailableColor() - usedColors.add(color) - project.icon = { ...project.icon, color } - if (project.id) { - globalSdk.client.project.update({ projectID: project.id, icon: { color } }) } - return project } const roots = createMemo(() => { @@ -277,8 +268,37 @@ export const { use: useLayout, provider: LayoutProvider } = createSimpleContext( }) }) - const enriched = createMemo(() => server.projects.list().flatMap(enrich)) - const list = createMemo(() => enriched().flatMap(colorize)) + const enriched = createMemo(() => server.projects.list().map(enrich)) + const list = createMemo(() => { + const projects = enriched() + return projects.map((project) => { + const color = project.icon?.color ?? colors[project.worktree] + if (!color) return project + const icon = project.icon ? { ...project.icon, color } : { color } + return { ...project, icon } + }) + }) + + createEffect(() => { + const projects = enriched() + if (projects.length === 0) return + + const used = new Set() + for (const project of projects) { + const color = project.icon?.color ?? colors[project.worktree] + if (color) used.add(color) + } + + for (const project of projects) { + if (project.icon?.color) continue + if (colors[project.worktree]) continue + const color = pickAvailableColor(used) + used.add(color) + setColors(project.worktree, color) + if (!project.id) continue + void globalSdk.client.project.update({ projectID: project.id, icon: { color } }) + } + }) onMount(() => { Promise.all( @@ -379,31 +399,31 @@ export const { use: useLayout, provider: LayoutProvider } = createSimpleContext( touch(sessionKey) scroll.seed(sessionKey) const s = createMemo(() => store.sessionView[sessionKey] ?? { scroll: {} }) - const terminalOpened = createMemo(() => s().terminalOpened ?? false) - const reviewPanelOpened = createMemo(() => s().reviewPanelOpened ?? true) + const terminalOpened = createMemo(() => store.terminal?.opened ?? false) + const reviewPanelOpened = createMemo(() => store.review?.panelOpened ?? true) function setTerminalOpened(next: boolean) { - const current = store.sessionView[sessionKey] + const current = store.terminal if (!current) { - setStore("sessionView", sessionKey, { scroll: {}, terminalOpened: next, reviewPanelOpened: true }) + setStore("terminal", { height: 280, opened: next }) return } - const value = current.terminalOpened ?? false + const value = current.opened ?? false if (value === next) return - setStore("sessionView", sessionKey, "terminalOpened", next) + setStore("terminal", "opened", next) } function setReviewPanelOpened(next: boolean) { - const current = store.sessionView[sessionKey] + const current = store.review if (!current) { - setStore("sessionView", sessionKey, { scroll: {}, terminalOpened: false, reviewPanelOpened: next }) + setStore("review", { diffStyle: "split" as ReviewDiffStyle, panelOpened: next }) return } - const value = current.reviewPanelOpened ?? true + const value = current.panelOpened ?? true if (value === next) return - setStore("sessionView", sessionKey, "reviewPanelOpened", next) + setStore("review", "panelOpened", next) } return { @@ -444,8 +464,6 @@ export const { use: useLayout, provider: LayoutProvider } = createSimpleContext( if (!current) { setStore("sessionView", sessionKey, { scroll: {}, - terminalOpened: false, - reviewPanelOpened: true, reviewOpen: open, }) return diff --git a/packages/app/src/pages/layout.tsx b/packages/app/src/pages/layout.tsx index 2f3b39d8628..9daac949e43 100644 --- a/packages/app/src/pages/layout.tsx +++ b/packages/app/src/pages/layout.tsx @@ -1284,7 +1284,7 @@ export default function Layout(props: ParentProps) {
{ if (!isActive()) { - navigate(`${props.slug}/session/${props.session.id}#message-${message.id}`) + sessionStorage.setItem("opencode.pendingMessage", `${props.session.id}|${message.id}`) + navigate(`${props.slug}/session/${props.session.id}`) return } - window.location.hash = `message-${message.id}` + window.history.replaceState(null, "", `#message-${message.id}`) window.dispatchEvent(new HashChangeEvent("hashchange")) }} size="normal" diff --git a/packages/app/src/pages/session.tsx b/packages/app/src/pages/session.tsx index 31f9eac9c27..3b405ef0773 100644 --- a/packages/app/src/pages/session.tsx +++ b/packages/app/src/pages/session.tsx @@ -1,4 +1,4 @@ -import { For, onCleanup, onMount, Show, Match, Switch, createMemo, createEffect, on } from "solid-js" +import { For, onCleanup, onMount, Show, Match, Switch, createMemo, createEffect, on, createSignal } from "solid-js" import { createMediaQuery } from "@solid-primitives/media" import { createResizeObserver } from "@solid-primitives/resize-observer" import { Dynamic } from "solid-js/web" @@ -167,6 +167,7 @@ export default function Page() { const sdk = useSDK() const prompt = usePrompt() const permission = usePermission() + const [pendingMessage, setPendingMessage] = createSignal(undefined) const sessionKey = createMemo(() => `${params.dir}${params.id ? "/" + params.id : ""}`) const tabs = createMemo(() => layout.tabs(sessionKey())) const view = createMemo(() => layout.view(sessionKey())) @@ -943,17 +944,30 @@ export default function Page() { window.history.replaceState(null, "", `#${anchor(id)}`) } + createEffect(() => { + const sessionID = params.id + if (!sessionID) return + const raw = sessionStorage.getItem("opencode.pendingMessage") + if (!raw) return + const parts = raw.split("|") + const pendingSessionID = parts[0] + const messageID = parts[1] + if (!pendingSessionID || !messageID) return + if (pendingSessionID !== sessionID) return + + sessionStorage.removeItem("opencode.pendingMessage") + setPendingMessage(messageID) + }) + const scrollToElement = (el: HTMLElement, behavior: ScrollBehavior) => { const root = scroller - if (!root) { - el.scrollIntoView({ behavior, block: "start" }) - return - } + if (!root) return false const a = el.getBoundingClientRect() const b = root.getBoundingClientRect() const top = a.top - b.top + root.scrollTop root.scrollTo({ top, behavior }) + return true } const scrollToMessage = (message: UserMessage, behavior: ScrollBehavior = "smooth") => { @@ -967,7 +981,15 @@ export default function Page() { requestAnimationFrame(() => { const el = document.getElementById(anchor(message.id)) - if (el) scrollToElement(el, behavior) + if (!el) { + requestAnimationFrame(() => { + const next = document.getElementById(anchor(message.id)) + if (!next) return + scrollToElement(next, behavior) + }) + return + } + scrollToElement(el, behavior) }) updateHash(message.id) @@ -975,10 +997,57 @@ export default function Page() { } const el = document.getElementById(anchor(message.id)) - if (el) scrollToElement(el, behavior) + if (!el) { + updateHash(message.id) + requestAnimationFrame(() => { + const next = document.getElementById(anchor(message.id)) + if (!next) return + if (!scrollToElement(next, behavior)) return + }) + return + } + if (scrollToElement(el, behavior)) { + updateHash(message.id) + return + } + + requestAnimationFrame(() => { + const next = document.getElementById(anchor(message.id)) + if (!next) return + if (!scrollToElement(next, behavior)) return + }) updateHash(message.id) } + const applyHash = (behavior: ScrollBehavior) => { + const hash = window.location.hash.slice(1) + if (!hash) { + autoScroll.forceScrollToBottom() + return + } + + const match = hash.match(/^message-(.+)$/) + if (match) { + const msg = visibleUserMessages().find((m) => m.id === match[1]) + if (msg) { + scrollToMessage(msg, behavior) + return + } + + // If we have a message hash but the message isn't loaded/rendered yet, + // don't fall back to "bottom". We'll retry once messages arrive. + return + } + + const target = document.getElementById(hash) + if (target) { + scrollToElement(target, behavior) + return + } + + autoScroll.forceScrollToBottom() + } + const getActiveMessageId = (container: HTMLDivElement) => { const cutoff = container.scrollTop + 100 const nodes = container.querySelectorAll("[data-message-id]") @@ -1019,29 +1088,45 @@ export default function Page() { if (!sessionID || !ready) return requestAnimationFrame(() => { - const hash = window.location.hash.slice(1) - if (!hash) { - autoScroll.forceScrollToBottom() - return - } + applyHash("auto") + }) + }) - const hashTarget = document.getElementById(hash) - if (hashTarget) { - scrollToElement(hashTarget, "auto") - return - } + // Retry message navigation once the target message is actually loaded. + createEffect(() => { + const sessionID = params.id + const ready = messagesReady() + if (!sessionID || !ready) return - const match = hash.match(/^message-(.+)$/) - if (match) { - const msg = visibleUserMessages().find((m) => m.id === match[1]) - if (msg) { - scrollToMessage(msg, "auto") - return - } - } + // dependencies + visibleUserMessages().length + store.turnStart + + const targetId = + pendingMessage() ?? + (() => { + const hash = window.location.hash.slice(1) + const match = hash.match(/^message-(.+)$/) + if (!match) return undefined + return match[1] + })() + if (!targetId) return + if (store.messageId === targetId) return + + const msg = visibleUserMessages().find((m) => m.id === targetId) + if (!msg) return + if (pendingMessage() === targetId) setPendingMessage(undefined) + requestAnimationFrame(() => scrollToMessage(msg, "auto")) + }) - autoScroll.forceScrollToBottom() - }) + createEffect(() => { + const sessionID = params.id + const ready = messagesReady() + if (!sessionID || !ready) return + + const handler = () => requestAnimationFrame(() => applyHash("auto")) + window.addEventListener("hashchange", handler) + onCleanup(() => window.removeEventListener("hashchange", handler)) }) createEffect(() => { diff --git a/packages/opencode/src/cli/cmd/tui/context/theme.tsx b/packages/opencode/src/cli/cmd/tui/context/theme.tsx index 7cde1b9648e..68cf25b6a8b 100644 --- a/packages/opencode/src/cli/cmd/tui/context/theme.tsx +++ b/packages/opencode/src/cli/cmd/tui/context/theme.tsx @@ -41,6 +41,7 @@ import { useRenderer } from "@opentui/solid" import { createStore, produce } from "solid-js/store" import { Global } from "@/global" import { Filesystem } from "@/util/filesystem" +import { Config } from "../../../../config/config" import { useSDK } from "./sdk" type ThemeColors = { @@ -128,7 +129,7 @@ type Variant = { light: HexColor | RefName } type ColorValue = HexColor | RefName | Variant | RGBA -type ThemeJson = { +export type ThemeJson = { $schema?: string defs?: Record theme: Omit, "selectedListItemText" | "backgroundMenu"> & { @@ -392,7 +393,7 @@ export const { use: useTheme, provider: ThemeProvider } = createSimpleContext({ }, }) -const CUSTOM_THEME_GLOB = new Bun.Glob("themes/*.json") +const CUSTOM_THEME_GLOB = new Bun.Glob("themes/*.{json,jsonc}") async function getCustomThemes() { const directories = [ Global.Path.config, @@ -412,8 +413,11 @@ async function getCustomThemes() { dot: true, cwd: dir, })) { - const name = path.basename(item, ".json") - result[name] = await Bun.file(item).json() + const ext = path.extname(item) + const name = path.basename(item, ext) + + // Use JSONC parser for all theme files regardless of extension + result[name] = await Config.loadThemeFile(item) } } return result diff --git a/packages/opencode/src/config/config.ts b/packages/opencode/src/config/config.ts index ddb3af4b0a8..8a0709d7efd 100644 --- a/packages/opencode/src/config/config.ts +++ b/packages/opencode/src/config/config.ts @@ -13,6 +13,7 @@ import { NamedError } from "@opencode-ai/util/error" import { Flag } from "../flag/flag" import { Auth } from "../auth" import { type ParseError as JsoncParseError, parse as parseJsonc, printParseErrorCode } from "jsonc-parser" +import type { ThemeJson } from "../cli/cmd/tui/context/theme" import { Instance } from "../project/instance" import { LSPServer } from "../lsp/server" import { BunProc } from "@/bun" @@ -1120,11 +1121,13 @@ export namespace Config { return load(text, filepath) } - async function load(text: string, configFilepath: string) { + async function load(text: string, configFilepath: string, enableConfigSubstitutions: boolean = true) { const original = text - text = text.replace(/\{env:([^}]+)\}/g, (_, varName) => { - return process.env[varName] || "" - }) + if (enableConfigSubstitutions) { + text = text.replace(/\{env:([^}]+)\}/g, (_, varName) => { + return process.env[varName] || "" + }) + } const fileMatches = text.match(/\{file:[^}]+\}/g) if (fileMatches) { @@ -1134,7 +1137,7 @@ export namespace Config { for (const match of fileMatches) { const lineIndex = lines.findIndex((line) => line.includes(match)) if (lineIndex !== -1 && lines[lineIndex].trim().startsWith("//")) { - continue // Skip if line is commented + continue } let filePath = match.replace(/^\{file:/, "").replace(/\}$/, "") if (filePath.startsWith("~/")) { @@ -1158,7 +1161,6 @@ export namespace Config { throw new InvalidError({ path: configFilepath, message: errMsg }, { cause: error }) }) ).trim() - // escape newlines/quotes, strip outer quotes text = text.replace(match, JSON.stringify(fileContent).slice(1, -1)) } } @@ -1242,6 +1244,48 @@ export namespace Config { return state().then((x) => x.config) } + export async function loadThemeFile(filepath: string): Promise { + log.info("loading theme", { path: filepath }) + let text = await Bun.file(filepath) + .text() + .catch((err) => { + if (err.code === "ENOENT") return + throw new JsonError({ path: filepath }, { cause: err }) + }) + if (!text) { + throw new Error("Empty theme file") + } + + // Parse JSONC directly without special features for themes + const errors: JsoncParseError[] = [] + const data = parseJsonc(text, errors, { allowTrailingComma: true }) + + if (errors.length) { + const lines = text.split("\n") + const errorDetails = errors + .map((e) => { + const beforeOffset = text.substring(0, e.offset).split("\n") + const line = beforeOffset.length + const column = beforeOffset[beforeOffset.length - 1].length + 1 + const problemLine = lines[line - 1] + + const error = `${printParseErrorCode(e.error)} at line ${line}, column ${column}` + if (!problemLine) return error + + return `${error}\n Line ${line}: ${problemLine}\n${"".padStart(column + 9)}^` + }) + .join("\n") + + throw new JsonError({ + path: filepath, + message: `\n--- JSONC Input ---\n${text}\n--- Errors ---\n${errorDetails}\n--- End ---`, + }) + } + + // Return data as ThemeJson (basic validation) + return data as ThemeJson + } + export async function update(config: Info) { const filepath = path.join(Instance.directory, "config.json") const existing = await loadFile(filepath) diff --git a/packages/opencode/src/project/project.ts b/packages/opencode/src/project/project.ts index 2cec78623d1..40ebb21ea5a 100644 --- a/packages/opencode/src/project/project.ts +++ b/packages/opencode/src/project/project.ts @@ -25,6 +25,7 @@ export namespace Project { icon: z .object({ url: z.string().optional(), + override: z.string().optional(), color: z.string().optional(), }) .optional(), @@ -190,6 +191,7 @@ export namespace Project { if (!existing.sandboxes) existing.sandboxes = [] if (Flag.OPENCODE_EXPERIMENTAL_ICON_DISCOVERY) discover(existing) + const result: Info = { ...existing, worktree, @@ -213,6 +215,7 @@ export namespace Project { export async function discover(input: Info) { if (input.vcs !== "git") return + if (input.icon?.override) return if (input.icon?.url) return const glob = new Bun.Glob("**/{favicon}.{ico,png,svg,jpg,jpeg,webp}") const matches = await Array.fromAsync( @@ -293,6 +296,7 @@ export namespace Project { ...draft.icon, } if (input.icon.url !== undefined) draft.icon.url = input.icon.url + if (input.icon.override !== undefined) draft.icon.override = input.icon.override || undefined if (input.icon.color !== undefined) draft.icon.color = input.icon.color } draft.time.updated = Date.now() diff --git a/packages/opencode/test/config/theme.test.ts b/packages/opencode/test/config/theme.test.ts new file mode 100644 index 00000000000..09d16fc7758 --- /dev/null +++ b/packages/opencode/test/config/theme.test.ts @@ -0,0 +1,115 @@ +import { describe, test, expect, beforeAll, afterAll } from "bun:test" +import { tmpdir } from "os" +import { join } from "path" +import { Config } from "../../src/config/config" +import { writeFileSync, mkdirSync, rmSync } from "fs" + +describe("Theme Loading", () => { + let tempDir: string + + beforeAll(() => { + tempDir = join(tmpdir(), "opencode-theme-test") + mkdirSync(tempDir, { recursive: true }) + }) + + afterAll(() => { + rmSync(tempDir, { recursive: true, force: true }) + }) + + test("should load JSONC theme file with comments", async () => { + const themeContent = `{ + // This is a comment + "$schema": "https://opencode.ai/theme.json", + "theme": { + "primary": "#ff0000", + "secondary": "#00ff00" + } +}` + + const themeFile = join(tempDir, "test-theme.jsonc") + writeFileSync(themeFile, themeContent) + + const theme = await Config.loadThemeFile(themeFile) + + expect(theme.theme.primary).toBe("#ff0000") + expect(theme.theme.secondary).toBe("#00ff00") + }) + + test("should NOT process environment variables in themes", async () => { + process.env.TEST_COLOR = "#00ff00" + const themeContent = `{ + "theme": { + "primary": "{env:TEST_COLOR}", + "secondary": "#ff0000" + } +}` + + const themeFile = join(tempDir, "test-theme.jsonc") + writeFileSync(themeFile, themeContent) + + const theme = await Config.loadThemeFile(themeFile) + + // Environment variable should NOT be processed in themes + expect(theme.theme.primary).toBe("{env:TEST_COLOR}") + expect(theme.theme.secondary).toBe("#ff0000") + }) + + test("should NOT process file inclusion in themes", async () => { + const colorFile = join(tempDir, "color.txt") + writeFileSync(colorFile, "#00ff00") + + const themeContent = `{ + "theme": { + "primary": "{file:color.txt}", + "secondary": "#ff0000" + } +}` + + const themeFile = join(tempDir, "test-theme.jsonc") + writeFileSync(themeFile, themeContent) + + const theme = await Config.loadThemeFile(themeFile) + + // File inclusion should NOT be processed in themes + expect(theme.theme.primary).toBe("{file:color.txt}") + expect(theme.theme.secondary).toBe("#ff0000") + }) + + test("should handle trailing commas in JSONC themes", async () => { + const themeContent = `{ + "theme": { + "primary": "#ff0000", + "secondary": "#00ff00", // Trailing comma + } +}` + + const themeFile = join(tempDir, "test-theme.jsonc") + writeFileSync(themeFile, themeContent) + + const theme = await Config.loadThemeFile(themeFile) + + expect(theme.theme.primary).toBe("#ff0000") + expect(theme.theme.secondary).toBe("#00ff00") + }) + + test("should throw error for invalid JSONC theme", async () => { + const themeContent = `{ + "theme": { + "primary": "#ff0000", + // Missing closing brace + "secondary": "#00ff00", + }` + + const themeFile = join(tempDir, "test-theme.jsonc") + writeFileSync(themeFile, themeContent) + + expect(Config.loadThemeFile(themeFile)).rejects.toThrow() + }) + + test("should throw error for empty theme file", async () => { + const themeFile = join(tempDir, "empty-theme.jsonc") + writeFileSync(themeFile, "") + + expect(Config.loadThemeFile(themeFile)).rejects.toThrow("Empty theme file") + }) +}) diff --git a/packages/sdk/js/src/v2/gen/sdk.gen.ts b/packages/sdk/js/src/v2/gen/sdk.gen.ts index 59b7f06963b..706d0f9c227 100644 --- a/packages/sdk/js/src/v2/gen/sdk.gen.ts +++ b/packages/sdk/js/src/v2/gen/sdk.gen.ts @@ -302,6 +302,7 @@ export class Project extends HeyApiClient { name?: string icon?: { url?: string + override?: string color?: string } }, diff --git a/packages/sdk/js/src/v2/gen/types.gen.ts b/packages/sdk/js/src/v2/gen/types.gen.ts index 75540f90724..b7e72fbad8f 100644 --- a/packages/sdk/js/src/v2/gen/types.gen.ts +++ b/packages/sdk/js/src/v2/gen/types.gen.ts @@ -25,6 +25,7 @@ export type Project = { name?: string icon?: { url?: string + override?: string color?: string } time: { @@ -2229,6 +2230,7 @@ export type ProjectUpdateData = { name?: string icon?: { url?: string + override?: string color?: string } } diff --git a/packages/sdk/openapi.json b/packages/sdk/openapi.json index 08dd98fd9bc..c1be820f262 100644 --- a/packages/sdk/openapi.json +++ b/packages/sdk/openapi.json @@ -231,6 +231,9 @@ "url": { "type": "string" }, + "override": { + "type": "string" + }, "color": { "type": "string" } @@ -5796,6 +5799,9 @@ "url": { "type": "string" }, + "override": { + "type": "string" + }, "color": { "type": "string" } diff --git a/packages/ui/src/components/session-turn.css b/packages/ui/src/components/session-turn.css index 5f8c0a16f6a..034d3024707 100644 --- a/packages/ui/src/components/session-turn.css +++ b/packages/ui/src/components/session-turn.css @@ -75,6 +75,17 @@ background-color: var(--background-stronger); z-index: -1; } + + &::after { + content: ""; + position: absolute; + top: 100%; + left: 0; + right: 0; + height: 32px; + background: linear-gradient(to bottom, var(--background-stronger), transparent); + pointer-events: none; + } } [data-slot="session-turn-response-trigger"] {