From 50654f96ce59bec2ca3200cd54806704472ff21f Mon Sep 17 00:00:00 2001 From: Eugene Blikh Date: Mon, 27 Apr 2026 10:33:25 +0300 Subject: [PATCH] web: wire display settings UI --- web/src/features/session/Transcript.tsx | 1 + web/src/features/settings/DisplaySection.tsx | 128 +++++++++++++++++++ web/src/main.tsx | 7 + web/src/routes/__root.tsx | 4 - web/src/routes/settings.tsx | 6 +- web/src/styles/home.css | 2 +- web/src/styles/projects.css | 2 +- web/src/styles/session.css | 2 + web/src/styles/settings.css | 47 ++++++- web/src/styles/tokens.css | 3 + 10 files changed, 192 insertions(+), 10 deletions(-) create mode 100644 web/src/features/settings/DisplaySection.tsx diff --git a/web/src/features/session/Transcript.tsx b/web/src/features/session/Transcript.tsx index 72ca157f6d7500bc85ac4cafa9aee2c4c4fd11be..987ef94215fd8397c039fde037e61eb7394823ad 100644 --- a/web/src/features/session/Transcript.tsx +++ b/web/src/features/session/Transcript.tsx @@ -22,6 +22,7 @@ export function Transcript({ key={t.i} id={`turn-${t.i}`} className={`turn ${t.role}${isSel ? ' selected' : ''}`} + {...(t.role === 'tool' || t.toolKind != null ? { 'data-tool': '1' } : {})} onClick={() => onSelect(t.i)} >
diff --git a/web/src/features/settings/DisplaySection.tsx b/web/src/features/settings/DisplaySection.tsx new file mode 100644 index 0000000000000000000000000000000000000000..237006d069c20044b6365fc64b0752ef984ed205 --- /dev/null +++ b/web/src/features/settings/DisplaySection.tsx @@ -0,0 +1,128 @@ +import React from 'react' +import { getThemePreference, setTheme } from '../../lib/theme' +import type { ThemePreference } from '../../lib/theme' +import { getDensityPreference, setDensity } from '../../lib/density' +import type { DensityPreference } from '../../lib/density' +import { getToolCallsPreference, setToolCalls } from '../../lib/toolCalls' +import type { ToolCallsPreference } from '../../lib/toolCalls' + +export function DisplaySection(): React.JSX.Element { + const [theme, setThemeState] = React.useState(getThemePreference) + const [density, setDensityState] = React.useState(getDensityPreference) + const [toolCalls, setToolCallsState] = React.useState( + getToolCallsPreference, + ) + + function handleThemeChange(value: ThemePreference) { + setThemeState(value) + setTheme(value === 'system' ? null : value) + } + + function handleDensityChange(value: DensityPreference | null) { + const next = value ?? 'cozy' + setDensityState(next) + setDensity(value) + } + + function handleToolCallsChange(value: ToolCallsPreference | null) { + const next = value ?? 'yes' + setToolCallsState(next) + setToolCalls(value) + } + + return ( +
+
+ Theme + + + +
+ Follows your OS appearance setting when System is selected. +
+
+ +
+ Row density + + +
+ Controls vertical spacing in session lists and project tables. +
+
+ +
+ Tool calls + + +
+ Tool-call turns are hidden with CSS; they remain in the DOM for + anchor-target and selection stability. +
+
+
+ ) +} diff --git a/web/src/main.tsx b/web/src/main.tsx index 63883dce1115cf45c2a022e012a1a736f546b9e3..7807e48423aa23703544f2a405bf11695a5b5094 100644 --- a/web/src/main.tsx +++ b/web/src/main.tsx @@ -3,6 +3,9 @@ import { createRoot } from 'react-dom/client' import { QueryClient, QueryClientProvider } from '@tanstack/react-query' import { RouterProvider, createRouter } from '@tanstack/react-router' import { routeTree } from './routeTree.gen' +import { bootstrapTheme } from './lib/theme' +import { bootstrapDensity } from './lib/density' +import { bootstrapToolCalls } from './lib/toolCalls' const queryClient = new QueryClient() @@ -17,6 +20,10 @@ declare module '@tanstack/react-router' { const rootEl = document.getElementById('root') if (!rootEl) throw new Error('Root element #root not found') +bootstrapTheme() +bootstrapDensity() +bootstrapToolCalls() + createRoot(rootEl).render( diff --git a/web/src/routes/__root.tsx b/web/src/routes/__root.tsx index 6554cf0636dbf5b30cfbcc67d236b04a1ca9c626..de8ea436f093dc7ffc6ab345d2d701a9f8af6432 100644 --- a/web/src/routes/__root.tsx +++ b/web/src/routes/__root.tsx @@ -1,6 +1,5 @@ import React, { useState, useEffect, useRef, createContext, useContext } from 'react' import { createRootRoute, Outlet, useNavigate } from '@tanstack/react-router' -import { bootstrapTheme } from '../lib/theme' import { createKeyboardController } from '../lib/keyboard' import type { RouteName } from '../lib/keyboard' import { TopBar } from '../shell/TopBar' @@ -48,9 +47,6 @@ function RootComponent(): React.JSX.Element { const cursorRef = useRef(noopCursor) useEffect(() => { - // Bootstrap theme once on mount - bootstrapTheme() - const controller = createKeyboardController({ go: (route: RouteName) => { const paths: Record = { diff --git a/web/src/routes/settings.tsx b/web/src/routes/settings.tsx index 4836631fbc8bead8ff20380cc51f3b856a66da11..dbdc04b269081015ef43de7c88b585420907754b 100644 --- a/web/src/routes/settings.tsx +++ b/web/src/routes/settings.tsx @@ -4,13 +4,14 @@ import { Tag } from '../primitives' import { SubBar } from '../shell/SubBar' import { SectionRail } from '../features/settings/SectionRail' import { SavedSearchesSection } from '../features/settings/SavedSearchesSection' +import { DisplaySection } from '../features/settings/DisplaySection' import '../styles/settings.css' type SectionKey = 'saved-searches' | 'display' -const SECTIONS: { key: SectionKey; label: string; disabled?: boolean; tag?: string }[] = [ +const SECTIONS: { key: SectionKey; label: string }[] = [ { key: 'saved-searches', label: 'Saved searches' }, - { key: 'display', label: 'Display', disabled: true, tag: 'in #8' }, + { key: 'display', label: 'Display' }, ] export const Route = createFileRoute('/settings')({ @@ -29,6 +30,7 @@ function SettingsRoute(): React.JSX.Element {
{active === 'saved-searches' && } + {active === 'display' && }
diff --git a/web/src/styles/home.css b/web/src/styles/home.css index b9814f43faf322a9be905878389e8fb3a2414ba1..cb6588b2048194fd3385f3c208f455d1f3d2f248 100644 --- a/web/src/styles/home.css +++ b/web/src/styles/home.css @@ -23,7 +23,7 @@ .home-row { display: grid; - padding: 4px 14px; + padding: var(--row-pad) 14px; border-bottom: 1px solid var(--rule-2); align-items: center; gap: 10px; diff --git a/web/src/styles/projects.css b/web/src/styles/projects.css index 5bd0fab4fa102e4476feca7f1e81509131c317af..308acfe3851b765ae96d689f99496b2701e7e2a5 100644 --- a/web/src/styles/projects.css +++ b/web/src/styles/projects.css @@ -23,7 +23,7 @@ .projects-row { display: grid; - padding: 4px 14px; + padding: var(--row-pad) 14px; border-bottom: 1px solid var(--rule-2); align-items: center; gap: 10px; diff --git a/web/src/styles/session.css b/web/src/styles/session.css index 6a031ad1501a93788b0a7375d7c3f6ea03d1c14a..71a8ddd7feea3e4f48d45a2b7f4e5645a1a88552 100644 --- a/web/src/styles/session.css +++ b/web/src/styles/session.css @@ -46,6 +46,8 @@ cursor: pointer; } +[data-show-toolcalls="false"] .turn[data-tool="1"] { display: none; } + .turn.user { background: var(--turn-user); border-left-color: var(--ink-4); } .turn.assistant { background: var(--turn-asst); border-left-color: var(--accent); } .turn.tool { background: var(--turn-tool); } diff --git a/web/src/styles/settings.css b/web/src/styles/settings.css index b89c17fb0f186060f7b15f0b5d8677b94bda717a..31d146de5f09844a7af8ca0527ccd2e5a67f9895 100644 --- a/web/src/styles/settings.css +++ b/web/src/styles/settings.css @@ -173,7 +173,7 @@ .saved-searches-row { display: grid; grid-template-columns: 180px 1fr 90px 120px; - padding: 5px 10px; + padding: var(--row-pad) 10px; border-bottom: 1px solid var(--rule-2); align-items: center; gap: 10px; @@ -191,7 +191,7 @@ .saved-searches-row-editing { display: block; - padding: 6px 10px; + padding: var(--row-pad) 10px; } .saved-searches-cell { @@ -223,3 +223,46 @@ .muted { color: var(--ink-3); } + +/* ── Display section ──────────────────────────────────────────────────────────── */ + +.display-section { + display: flex; + flex-direction: column; + gap: 16px; +} + +.display-section fieldset { + border: 1px solid var(--rule-2); + border-radius: 4px; + padding: 12px 14px; + margin: 0; +} + +.display-section legend { + font-size: 13px; + font-weight: 600; + color: var(--ink); + padding: 0 4px; +} + +.display-section label { + display: flex; + align-items: center; + gap: 6px; + padding: 4px 0; + font-size: 12px; + color: var(--ink-2); + cursor: pointer; +} + +.display-section input[type="radio"] { + accent-color: var(--accent); +} + +.display-help { + font-size: 11px; + color: var(--ink-3); + margin-top: 4px; + padding-left: 18px; +} diff --git a/web/src/styles/tokens.css b/web/src/styles/tokens.css index 3bbdb3e0ab022d57f106f919c3bd12a05b70f7db..e0a282fb9455c9d0e108a7a50282c293f592ee77 100644 --- a/web/src/styles/tokens.css +++ b/web/src/styles/tokens.css @@ -53,6 +53,9 @@ --sans: 'Inter', system-ui, sans-serif; } +[data-density="cozy"] { --row-pad: 4px; } +[data-density="compact"] { --row-pad: 2px; } + [data-theme="dark"] { /* Same accent hue but tuned for dark surface contrast */ --accent: oklch(0.78 0.16 120);