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);