Compare commits

...

7 Commits

Author SHA1 Message Date
VooDisss
c9c1cf21f0 fix(ui): stop forced auto-follow during streaming (#309)
# PR Draft: Fix sticky auto-scroll during streaming chat responses

Fixes #308

## Summary

This change makes chat auto-scroll easier to escape while assistant
output is still streaming.

The goal is to stop the viewport from repeatedly pulling the user back
toward the bottom once they begin scrolling upward to inspect earlier
content.

## Why

Before this change, streaming updates could keep reasserting
bottom-follow behavior during active rendering. That made auto-scroll
feel sticky and forced users to scroll repeatedly or forcefully just to
review earlier parts of an in-progress response.

The intended behavior is simpler: once the user scrolls upward to leave
follow mode, the UI should respect that decision instead of fighting it
during subsequent stream updates.

## What Changed

1. Removed render-time force-bottom behavior from the shared
follow-scroll helper path.
2. Updated streamed reasoning output to restore scroll without forcing
the viewport back to the bottom.
3. Updated streamed tool-call output to use the same non-forcing restore
behavior.

## Scope Boundaries

Included:

- Sticky auto-scroll behavior during streamed chat output
- Shared follow-scroll behavior used by streamed nested panes
- Reasoning and tool-call streaming paths that reused the same forced
follow behavior

Not included:

- A full rewrite of the virtualized message list follow model
- Broader scroll UX changes outside the streaming follow/escape behavior
- Unrelated UI or plugin configuration changes in the worktree

## Technical Notes

The core problem was not basic auto-scroll itself, but a render-time
path that could keep forcing bottom-follow behavior while new streamed
content was arriving.

That meant a user's attempt to scroll upward could be overridden
repeatedly by subsequent stream updates, which is why the auto-scroll
felt sticky. The fix removes that override and keeps render-time
restoration dependent on the current follow state instead.

## Files Changed

- `packages/ui/src/lib/follow-scroll.tsx`
- `packages/ui/src/components/message-block.tsx`
- `packages/ui/src/components/tool-call.tsx`

## Verification

Performed:

1. Reproduced the sticky auto-scroll behavior with a long multi-line
streaming response.
2. Verified that scrolling upward during streaming now disengages follow
more naturally in the affected streamed panes.
3. Ran `npm run typecheck --workspace @codenomad/ui`.
4. Ran `npm run build --workspace @codenomad/ui`.

Build note:

- The UI typecheck passes.
- The UI build succeeds.
- The build still emits existing third-party and chunk-size warnings
unrelated to this change.

## Risks and Follow-up

1. The broader scroll-follow model is still more heuristic-heavy than
ideal, so there may be future follow-up work to simplify it further.
2. This PR intentionally applies the smallest targeted fix to the known
snap-back path instead of rewriting the full chat scroll system.

---------

Co-authored-by: Shantur Rathore <i@shantur.com>
2026-04-10 16:26:33 +01:00
Shantur Rathore
c7d4f99e48 fix(ui): prevent settings modal overflow on phones 2026-04-09 21:00:17 +01:00
Shantur Rathore
d50c00afb4 revert: remove debouncing and transparent window from zoom fix
Reverted debouncing logic and transparent window mode that were causing issues.
Kept the zoom step reduction from 0.2 to 0.1 for finer control.
2026-04-09 16:23:45 +01:00
Shantur Rathore
0ef57df3bc fix(ui): show token stats and simplify context window calculation
- Track messageInfoVersion in cache signature to rebuild when tokens arrive via SSE
- Read tokens from step-finish part directly (embedded in SSE events)
- Simplify available tokens to show full context window when no explicit input limit
2026-04-08 22:19:10 +01:00
Shantur Rathore
0739ec857c Reapply "fix(ui): support unified diff patch format in session changes viewer"
This reverts commit af6429162f.
2026-04-08 20:57:23 +01:00
Shantur Rathore
b060ab45ff Revert "feat(tauri): add zip bundle target for macOS and Windows"
This reverts commit 197898c01c.
2026-04-08 20:57:23 +01:00
Shantur Rathore
af6429162f Revert "fix(ui): support unified diff patch format in session changes viewer"
This reverts commit 2e9ee2cde6.
2026-04-08 20:57:12 +01:00
11 changed files with 77 additions and 73 deletions

View File

@@ -378,7 +378,7 @@ jobs:
- name: Build macOS bundle (Tauri) - name: Build macOS bundle (Tauri)
working-directory: packages/tauri-app working-directory: packages/tauri-app
run: npm exec -- tauri build --bundles app,zip run: npm exec -- tauri build
- name: Package Tauri artifacts (macOS) - name: Package Tauri artifacts (macOS)
if: ${{ inputs.upload || inputs.upload_actions_artifacts }} if: ${{ inputs.upload || inputs.upload_actions_artifacts }}
@@ -388,9 +388,7 @@ jobs:
ARTIFACT_DIR="packages/tauri-app/release-tauri" ARTIFACT_DIR="packages/tauri-app/release-tauri"
rm -rf "$ARTIFACT_DIR" rm -rf "$ARTIFACT_DIR"
mkdir -p "$ARTIFACT_DIR" mkdir -p "$ARTIFACT_DIR"
if [ -f "$BUNDLE_ROOT/macos/CodeNomad.app.zip" ]; then if [ -d "$BUNDLE_ROOT/macos/CodeNomad.app" ]; then
mv "$BUNDLE_ROOT/macos/CodeNomad.app.zip" "$ARTIFACT_DIR/CodeNomad-Tauri-${VERSION}-macos-x64.zip"
elif [ -d "$BUNDLE_ROOT/macos/CodeNomad.app" ]; then
ditto -ck --sequesterRsrc --keepParent "$BUNDLE_ROOT/macos/CodeNomad.app" "$ARTIFACT_DIR/CodeNomad-Tauri-${VERSION}-macos-x64.zip" ditto -ck --sequesterRsrc --keepParent "$BUNDLE_ROOT/macos/CodeNomad.app" "$ARTIFACT_DIR/CodeNomad-Tauri-${VERSION}-macos-x64.zip"
fi fi
@@ -464,7 +462,7 @@ jobs:
- name: Build macOS bundle (Tauri, arm64) - name: Build macOS bundle (Tauri, arm64)
working-directory: packages/tauri-app working-directory: packages/tauri-app
run: npm exec -- tauri build --bundles app,zip run: npm exec -- tauri build
- name: Package Tauri artifacts (macOS arm64) - name: Package Tauri artifacts (macOS arm64)
if: ${{ inputs.upload || inputs.upload_actions_artifacts }} if: ${{ inputs.upload || inputs.upload_actions_artifacts }}
@@ -474,9 +472,7 @@ jobs:
ARTIFACT_DIR="packages/tauri-app/release-tauri" ARTIFACT_DIR="packages/tauri-app/release-tauri"
rm -rf "$ARTIFACT_DIR" rm -rf "$ARTIFACT_DIR"
mkdir -p "$ARTIFACT_DIR" mkdir -p "$ARTIFACT_DIR"
if [ -f "$BUNDLE_ROOT/macos/CodeNomad.app.zip" ]; then if [ -d "$BUNDLE_ROOT/macos/CodeNomad.app" ]; then
mv "$BUNDLE_ROOT/macos/CodeNomad.app.zip" "$ARTIFACT_DIR/CodeNomad-Tauri-${VERSION}-macos-arm64.zip"
elif [ -d "$BUNDLE_ROOT/macos/CodeNomad.app" ]; then
ditto -ck --sequesterRsrc --keepParent "$BUNDLE_ROOT/macos/CodeNomad.app" "$ARTIFACT_DIR/CodeNomad-Tauri-${VERSION}-macos-arm64.zip" ditto -ck --sequesterRsrc --keepParent "$BUNDLE_ROOT/macos/CodeNomad.app" "$ARTIFACT_DIR/CodeNomad-Tauri-${VERSION}-macos-arm64.zip"
fi fi
@@ -553,7 +549,7 @@ jobs:
- name: Build Windows bundle (Tauri) - name: Build Windows bundle (Tauri)
shell: bash shell: bash
working-directory: packages/tauri-app working-directory: packages/tauri-app
run: npm exec -- tauri build --bundles nsis,zip run: npm exec -- tauri build
- name: Package Tauri artifacts (Windows) - name: Package Tauri artifacts (Windows)
if: ${{ inputs.upload || inputs.upload_actions_artifacts }} if: ${{ inputs.upload || inputs.upload_actions_artifacts }}
@@ -563,19 +559,10 @@ jobs:
$artifactDir = "packages/tauri-app/release-tauri" $artifactDir = "packages/tauri-app/release-tauri"
if (Test-Path $artifactDir) { Remove-Item $artifactDir -Recurse -Force } if (Test-Path $artifactDir) { Remove-Item $artifactDir -Recurse -Force }
New-Item -ItemType Directory -Path $artifactDir | Out-Null New-Item -ItemType Directory -Path $artifactDir | Out-Null
$exe = Get-ChildItem -Path $bundleRoot -Recurse -File -Filter *.exe | Select-Object -First 1
# Use Tauri-generated zip if available if ($null -ne $exe) {
$tauriZip = Get-ChildItem -Path "$bundleRoot/nsis" -Filter "*.zip" -File | Select-Object -First 1
if ($null -ne $tauriZip) {
$dest = Join-Path $artifactDir ("CodeNomad-Tauri-$env:VERSION-windows-x64.zip") $dest = Join-Path $artifactDir ("CodeNomad-Tauri-$env:VERSION-windows-x64.zip")
Move-Item $tauriZip.FullName $dest -Force Compress-Archive -Path $exe.Directory.FullName -DestinationPath $dest -Force
} else {
# Fallback: manually zip the exe
$exe = Get-ChildItem -Path $bundleRoot -Recurse -File -Filter *.exe | Select-Object -First 1
if ($null -ne $exe) {
$dest = Join-Path $artifactDir ("CodeNomad-Tauri-$env:VERSION-windows-x64.zip")
Compress-Archive -Path $exe.Directory.FullName -DestinationPath $dest -Force
}
} }
- name: Upload Actions artifacts (Tauri Windows) - name: Upload Actions artifacts (Tauri Windows)
@@ -661,7 +648,7 @@ jobs:
- name: Build Linux bundle (Tauri) - name: Build Linux bundle (Tauri)
working-directory: packages/tauri-app working-directory: packages/tauri-app
run: npm exec -- tauri build --bundles appimage,deb,rpm run: npm exec -- tauri build
- name: Package Tauri artifacts (Linux) - name: Package Tauri artifacts (Linux)
if: ${{ inputs.upload || inputs.upload_actions_artifacts }} if: ${{ inputs.upload || inputs.upload_actions_artifacts }}

View File

@@ -11,14 +11,7 @@
"sync:version": "node ./scripts/sync-tauri-version.js", "sync:version": "node ./scripts/sync-tauri-version.js",
"prebuild": "node ./scripts/prebuild.js", "prebuild": "node ./scripts/prebuild.js",
"bundle:server": "npm run prebuild", "bundle:server": "npm run prebuild",
"build": "tauri build", "build": "tauri build"
"build:mac": "tauri build --target universal-apple-darwin --bundles app,zip",
"build:mac-arm": "tauri build --target aarch64-apple-darwin --bundles app,zip",
"build:mac-intel": "tauri build --target x86_64-apple-darwin --bundles app,zip",
"build:mac-zip": "tauri build --target universal-apple-darwin --bundles zip",
"build:win": "tauri build --bundles nsis,zip",
"build:win-zip": "tauri build --bundles zip",
"build:linux": "tauri build --bundles appimage,deb,rpm"
}, },
"devDependencies": { "devDependencies": {
"@tauri-apps/cli": "^2.9.4" "@tauri-apps/cli": "^2.9.4"

View File

@@ -9,7 +9,7 @@ use serde_json::json;
use std::collections::HashMap; use std::collections::HashMap;
use std::sync::atomic::{AtomicBool, Ordering}; use std::sync::atomic::{AtomicBool, Ordering};
use std::sync::Mutex; use std::sync::Mutex;
use std::time::{Instant, SystemTime, UNIX_EPOCH}; use std::time::{SystemTime, UNIX_EPOCH};
use tauri::menu::{MenuBuilder, MenuItem, SubmenuBuilder}; use tauri::menu::{MenuBuilder, MenuItem, SubmenuBuilder};
use tauri::plugin::{Builder as PluginBuilder, TauriPlugin}; use tauri::plugin::{Builder as PluginBuilder, TauriPlugin};
use tauri::webview::Webview; use tauri::webview::Webview;
@@ -32,12 +32,10 @@ use std::os::windows::ffi::OsStrExt;
use windows_sys::Win32::UI::Shell::SetCurrentProcessExplicitAppUserModelID; use windows_sys::Win32::UI::Shell::SetCurrentProcessExplicitAppUserModelID;
static QUIT_REQUESTED: AtomicBool = AtomicBool::new(false); static QUIT_REQUESTED: AtomicBool = AtomicBool::new(false);
static LAST_ZOOM_TIME: Mutex<Option<Instant>> = Mutex::new(None);
const DEFAULT_ZOOM_LEVEL: f64 = 1.0; const DEFAULT_ZOOM_LEVEL: f64 = 1.0;
const ZOOM_STEP: f64 = 0.1; const ZOOM_STEP: f64 = 0.1;
const MIN_ZOOM_LEVEL: f64 = 0.2; const MIN_ZOOM_LEVEL: f64 = 0.2;
const MAX_ZOOM_LEVEL: f64 = 5.0; const MAX_ZOOM_LEVEL: f64 = 5.0;
const ZOOM_DEBOUNCE_MS: u64 = 50;
#[cfg(windows)] #[cfg(windows)]
const WINDOWS_APP_USER_MODEL_ID: &str = "ai.neuralnomads.codenomad.client"; const WINDOWS_APP_USER_MODEL_ID: &str = "ai.neuralnomads.codenomad.client";
@@ -259,15 +257,6 @@ fn clamp_zoom_level(value: f64) -> f64 {
} }
fn set_main_window_zoom(app_handle: &AppHandle, next_zoom: f64) { fn set_main_window_zoom(app_handle: &AppHandle, next_zoom: f64) {
if let Ok(mut last_zoom_time) = LAST_ZOOM_TIME.lock() {
if let Some(last_time) = *last_zoom_time {
if last_time.elapsed().as_millis() < ZOOM_DEBOUNCE_MS as u128 {
return;
}
}
*last_zoom_time = Some(Instant::now());
}
if let Some(window) = app_handle.get_webview_window("main") { if let Some(window) = app_handle.get_webview_window("main") {
let normalized = clamp_zoom_level(next_zoom); let normalized = clamp_zoom_level(next_zoom);
if window.set_zoom(normalized).is_ok() { if window.set_zoom(normalized).is_ok() {

View File

@@ -23,7 +23,6 @@
"resizable": true, "resizable": true,
"fullscreen": false, "fullscreen": false,
"decorations": true, "decorations": true,
"transparent": true,
"theme": "Dark", "theme": "Dark",
"backgroundColor": "#1a1a1a", "backgroundColor": "#1a1a1a",
"zoomHotkeysEnabled": true "zoomHotkeysEnabled": true
@@ -53,11 +52,10 @@
], ],
"targets": [ "targets": [
"app", "app",
"zip",
"appimage", "appimage",
"deb", "deb",
"rpm", "rpm",
"nsis" "nsis"
] ]
} }
} }

View File

@@ -629,13 +629,12 @@ export default function MessageBlock(props: MessageBlockProps) {
const lastAssistantIdx = props.lastAssistantIndex() const lastAssistantIdx = props.lastAssistantIndex()
const isQueued = current.role === "user" && (lastAssistantIdx === -1 || index > lastAssistantIdx) const isQueued = current.role === "user" && (lastAssistantIdx === -1 || index > lastAssistantIdx)
// Intentionally untracked: messageInfoVersion updates should not trigger const messageInfoVersion = props.store().state.messageInfoVersion[current.id] ?? 0
// a full message block rebuild; record revision is the invalidation key.
const info = untrack(messageInfo)
const cacheSignature = [ const cacheSignature = [
current.id, current.id,
current.revision, current.revision,
messageInfoVersion,
isQueued ? 1 : 0, isQueued ? 1 : 0,
props.showThinking() ? 1 : 0, props.showThinking() ? 1 : 0,
props.thinkingDefaultExpanded() ? 1 : 0, props.thinkingDefaultExpanded() ? 1 : 0,
@@ -647,6 +646,9 @@ export default function MessageBlock(props: MessageBlockProps) {
return cachedBlock.block return cachedBlock.block
} }
// Only capture info after cache check fails - ensures fresh data on version bump
const info = untrack(messageInfo)
const { orderedParts } = buildRecordDisplayData(props.instanceId, current) const { orderedParts } = buildRecordDisplayData(props.instanceId, current)
const items: MessageBlockItem[] = [] const items: MessageBlockItem[] = []
const blockContentKeys: string[] = [] const blockContentKeys: string[] = []
@@ -1108,17 +1110,23 @@ function StepCard(props: StepCardProps) {
return null return null
} }
const info = props.messageInfo const info = props.messageInfo
if (!info || info.role !== "assistant" || !info.tokens) { const part = props.part as any
// step-finish parts have tokens embedded; also check messageInfo
const partTokens = part?.tokens
const infoTokens = info && info.role === "assistant" ? info.tokens : undefined
const tokens = partTokens ?? infoTokens
if (!tokens) {
return null return null
} }
const tokens = info.tokens
return { return {
input: tokens.input ?? 0, input: tokens.input ?? 0,
output: tokens.output ?? 0, output: tokens.output ?? 0,
reasoning: tokens.reasoning ?? 0, reasoning: tokens.reasoning ?? 0,
cacheRead: tokens.cache?.read ?? 0, cacheRead: tokens.cache?.read ?? 0,
cacheWrite: tokens.cache?.write ?? 0, cacheWrite: tokens.cache?.write ?? 0,
cost: info.cost ?? 0, cost: (part?.cost ?? (info && info.role === "assistant" ? info.cost : 0)) ?? 0,
} }
} }
@@ -1336,9 +1344,7 @@ function ReasoningStreamOutput(props: {
if (preRef && preRef.textContent !== nextText) { if (preRef && preRef.textContent !== nextText) {
preRef.textContent = nextText preRef.textContent = nextText
} }
if (followScroll.autoScroll()) { followScroll.restoreAfterRender()
followScroll.restoreAfterRender({ forceBottom: true })
}
notifyContentRendered() notifyContentRendered()
}) })

View File

@@ -334,7 +334,7 @@ const Field: Component<{
<div class="settings-toggle-title">{props.label}</div> <div class="settings-toggle-title">{props.label}</div>
<div class="settings-toggle-caption">{props.caption}</div> <div class="settings-toggle-caption">{props.caption}</div>
</div> </div>
<div class="flex items-center gap-2 min-w-[18rem] max-w-[24rem] w-full"> <div class="flex items-center gap-2 w-full min-w-0 sm:min-w-[18rem] sm:max-w-[24rem]">
{props.icon} {props.icon}
<input <input
type={props.type ?? "text"} type={props.type ?? "text"}
@@ -361,7 +361,7 @@ const SelectField: Component<{
<div class="settings-toggle-title">{props.label}</div> <div class="settings-toggle-title">{props.label}</div>
<div class="settings-toggle-caption">{props.caption}</div> <div class="settings-toggle-caption">{props.caption}</div>
</div> </div>
<div class="min-w-[18rem] max-w-[24rem] w-full"> <div class="w-full min-w-0 sm:min-w-[18rem] sm:max-w-[24rem]">
<select value={props.value} onInput={(event) => props.onInput(event.currentTarget.value)} class="selector-input w-full"> <select value={props.value} onInput={(event) => props.onInput(event.currentTarget.value)} class="selector-input w-full">
<For each={props.options}>{(option) => <option value={option.value}>{option.label}</option>}</For> <For each={props.options}>{(option) => <option value={option.value}>{option.label}</option>}</For>
</select> </select>

View File

@@ -454,7 +454,7 @@ function ToolCallDetails(props: {
createEffect(() => { createEffect(() => {
if (followScroll.autoScroll()) { if (followScroll.autoScroll()) {
scrollHelpers.restoreAfterRender({ forceBottom: true }) scrollHelpers.restoreAfterRender()
} }
}) })

View File

@@ -47,7 +47,7 @@ export interface ToolScrollHelpers {
registerContainer(element: HTMLDivElement | null, options?: { disableTracking?: boolean }): void registerContainer(element: HTMLDivElement | null, options?: { disableTracking?: boolean }): void
handleScroll(event: Event & { currentTarget: HTMLDivElement }): void handleScroll(event: Event & { currentTarget: HTMLDivElement }): void
renderSentinel(options?: { disableTracking?: boolean }): JSXElement | null renderSentinel(options?: { disableTracking?: boolean }): JSXElement | null
restoreAfterRender(options?: { forceBottom?: boolean }): void restoreAfterRender(): void
} }
export interface ToolRendererContext { export interface ToolRendererContext {

View File

@@ -16,7 +16,7 @@ export interface FollowScrollHelpers {
registerContainer: (element: HTMLDivElement | null | undefined, options?: { disableTracking?: boolean }) => void registerContainer: (element: HTMLDivElement | null | undefined, options?: { disableTracking?: boolean }) => void
handleScroll: (event: Event & { currentTarget: HTMLDivElement }) => void handleScroll: (event: Event & { currentTarget: HTMLDivElement }) => void
renderSentinel: (options?: { disableTracking?: boolean }) => JSXElement | null renderSentinel: (options?: { disableTracking?: boolean }) => JSXElement | null
restoreAfterRender: (options?: { forceBottom?: boolean }) => void restoreAfterRender: () => void
autoScroll: Accessor<boolean> autoScroll: Accessor<boolean>
} }
@@ -183,7 +183,7 @@ export function createFollowScroll(options: FollowScrollOptions): FollowScrollHe
return <div ref={setBottomSentinel} aria-hidden="true" class={options.sentinelClassName} style={{ height: "1px" }} /> return <div ref={setBottomSentinel} aria-hidden="true" class={options.sentinelClassName} style={{ height: "1px" }} />
} }
const restoreAfterRender = (config?: { forceBottom?: boolean }) => { const restoreAfterRender = () => {
const container = scrollContainerRef const container = scrollContainerRef
if (container && hasUserScrollIntent() && !isAtBottom(container)) { if (container && hasUserScrollIntent() && !isAtBottom(container)) {
if (autoScroll()) { if (autoScroll()) {
@@ -195,7 +195,10 @@ export function createFollowScroll(options: FollowScrollOptions): FollowScrollHe
return return
} }
const shouldFollow = config?.forceBottom ?? autoScroll() // Never let a render-time caller force follow mode back on after the user
// has already escaped it. Staying pinned should depend on the current
// follow state, not on a caller opting into forceBottom.
const shouldFollow = autoScroll()
requestAnimationFrame(() => { requestAnimationFrame(() => {
restoreScrollPosition(shouldFollow) restoreScrollPosition(shouldFollow)
if (shouldFollow) { if (shouldFollow) {

View File

@@ -116,18 +116,11 @@ export function updateSessionInfo(instanceId: string, sessionId: string): void {
// Prefer explicit input limits when provided by the API. // Prefer explicit input limits when provided by the API.
// This is used by the UI "Avail" chip. // This is used by the UI "Avail" chip.
contextAvailableTokens = modelInputLimit contextAvailableTokens = modelInputLimit
} } else if (contextWindow > 0) {
// When no explicit input limit, show full context window capacity.
if (!contextAvailableFromPrevious && contextAvailableTokens === null) { contextAvailableTokens = contextWindow
if (contextWindow > 0) { } else {
if (latestHasContextUsage && actualUsageTokens > 0) { contextAvailableTokens = null
contextAvailableTokens = Math.max(contextWindow - (actualUsageTokens + outputBudget), 0)
} else {
contextAvailableTokens = contextWindow
}
} else {
contextAvailableTokens = null
}
} }
setSessionInfoByInstance((prev) => { setSessionInfoByInstance((prev) => {

View File

@@ -526,14 +526,49 @@
@media (max-width: 640px) { @media (max-width: 640px) {
.settings-screen-frame { .settings-screen-frame {
padding: 0; padding: 0;
overflow: hidden;
} }
.modal-surface.settings-screen-shell { .modal-surface.settings-screen-shell {
width: 100%; width: 100%;
max-width: 100%;
height: 100%; height: 100%;
max-height: none; max-height: none;
min-height: 100%; min-height: 100%;
border-radius: 0; border-radius: 0;
overflow-x: hidden;
}
.modal-surface.settings-screen-shell .settings-screen-nav,
.modal-surface.settings-screen-shell .settings-screen-nav-list,
.modal-surface.settings-screen-shell .settings-screen-content,
.modal-surface.settings-screen-shell .settings-screen-scroll,
.modal-surface.settings-screen-shell .settings-section-stack,
.modal-surface.settings-screen-shell .settings-stack,
.modal-surface.settings-screen-shell .settings-card,
.modal-surface.settings-screen-shell .settings-card-content,
.modal-surface.settings-screen-shell .settings-toggle-row,
.modal-surface.settings-screen-shell .settings-toggle-row > * {
min-width: 0;
}
.modal-surface.settings-screen-shell .selector-trigger,
.modal-surface.settings-screen-shell .selector-input,
.modal-surface.settings-screen-shell .selector-button {
min-width: 0;
max-width: 100%;
}
.modal-surface.settings-screen-shell .settings-toggle-caption,
.modal-surface.settings-screen-shell .settings-inline-note,
.modal-surface.settings-screen-shell .remote-address-url,
.modal-surface.settings-screen-shell code {
overflow-wrap: anywhere;
word-break: break-word;
}
.modal-surface.settings-screen-shell .whitespace-nowrap {
white-space: normal;
} }
.settings-screen-content-header, .settings-screen-content-header,