~bigbes/lethe

50654f96ce59bec2ca3200cd54806704472ff21f — Eugene Blikh 30 days ago 7cffe38
web: wire display settings UI
M web/src/features/session/Transcript.tsx => web/src/features/session/Transcript.tsx +1 -0
@@ 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)}
          >
            <div className="turn-meta">

A web/src/features/settings/DisplaySection.tsx => web/src/features/settings/DisplaySection.tsx +128 -0
@@ 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<ThemePreference>(getThemePreference)
  const [density, setDensityState] = React.useState<DensityPreference>(getDensityPreference)
  const [toolCalls, setToolCallsState] = React.useState<ToolCallsPreference>(
    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 (
    <div className="display-section">
      <fieldset>
        <legend>Theme</legend>
        <label>
          <input
            type="radio"
            name="theme"
            value="system"
            checked={theme === 'system'}
            onChange={() => handleThemeChange('system')}
          />
          System
        </label>
        <label>
          <input
            type="radio"
            name="theme"
            value="light"
            checked={theme === 'light'}
            onChange={() => handleThemeChange('light')}
          />
          Light
        </label>
        <label>
          <input
            type="radio"
            name="theme"
            value="dark"
            checked={theme === 'dark'}
            onChange={() => handleThemeChange('dark')}
          />
          Dark
        </label>
        <div className="display-help">
          Follows your OS appearance setting when System is selected.
        </div>
      </fieldset>

      <fieldset>
        <legend>Row density</legend>
        <label>
          <input
            type="radio"
            name="density"
            value="cozy"
            checked={density === 'cozy'}
            onChange={() => handleDensityChange('cozy')}
          />
          Cozy
        </label>
        <label>
          <input
            type="radio"
            name="density"
            value="compact"
            checked={density === 'compact'}
            onChange={() => handleDensityChange('compact')}
          />
          Compact
        </label>
        <div className="display-help">
          Controls vertical spacing in session lists and project tables.
        </div>
      </fieldset>

      <fieldset>
        <legend>Tool calls</legend>
        <label>
          <input
            type="radio"
            name="toolCalls"
            value="yes"
            checked={toolCalls === 'yes'}
            onChange={() => handleToolCallsChange('yes')}
          />
          Show
        </label>
        <label>
          <input
            type="radio"
            name="toolCalls"
            value="no"
            checked={toolCalls === 'no'}
            onChange={() => handleToolCallsChange('no')}
          />
          Hide
        </label>
        <div className="display-help">
          Tool-call turns are hidden with CSS; they remain in the DOM for
          anchor-target and selection stability.
        </div>
      </fieldset>
    </div>
  )
}

M web/src/main.tsx => web/src/main.tsx +7 -0
@@ 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(
  <React.StrictMode>
    <QueryClientProvider client={queryClient}>

M web/src/routes/__root.tsx => web/src/routes/__root.tsx +0 -4
@@ 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<CursorHandle>(noopCursor)

  useEffect(() => {
    // Bootstrap theme once on mount
    bootstrapTheme()

    const controller = createKeyboardController({
      go: (route: RouteName) => {
        const paths: Record<RouteName, string> = {

M web/src/routes/settings.tsx => web/src/routes/settings.tsx +4 -2
@@ 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 {
        <SectionRail sections={SECTIONS} active={active} onSelect={setActive} />
        <div className="settings-panel">
          {active === 'saved-searches' && <SavedSearchesSection />}
          {active === 'display' && <DisplaySection />}
        </div>
      </div>
    </>

M web/src/styles/home.css => web/src/styles/home.css +1 -1
@@ 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;

M web/src/styles/projects.css => web/src/styles/projects.css +1 -1
@@ 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;

M web/src/styles/session.css => web/src/styles/session.css +2 -0
@@ 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); }

M web/src/styles/settings.css => web/src/styles/settings.css +45 -2
@@ 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;
}

M web/src/styles/tokens.css => web/src/styles/tokens.css +3 -0
@@ 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);