Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -1699,7 +1699,7 @@ export const ChatDisplay = React.forwardRef<ChatDisplayHandle, ChatDisplayProps>
<ScrollArea className="h-full min-w-0" viewportRef={scrollViewportRef}>
<div className={cn(
CHAT_LAYOUT.maxWidth,
"mx-auto min-w-0",
"chat-text-scope mx-auto min-w-0",
compactMode ? "px-3 py-4 space-y-2" : [CHAT_LAYOUT.containerPadding, CHAT_LAYOUT.messageSpacing]
)}>
{/* Session-level AnimatePresence: Prevents layout jump when switching sessions */}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,12 @@ export interface SettingsSegmentedControlProps<T extends string = string> {
size?: 'sm' | 'md'
/** Additional className */
className?: string
/**
* Optional test id. When set, `data-testid` is applied to the group and each
* option button additionally carries a `data-value` attribute, so tests can
* address a specific segment without depending on its (localized) label.
*/
testId?: string
}

/**
Expand All @@ -50,11 +56,13 @@ export function SettingsSegmentedControl<T extends string = string>({
options,
size = 'md',
className,
testId,
}: SettingsSegmentedControlProps<T>) {
return (
<div
role="radiogroup"
className={cn('inline-flex gap-1', className)}
data-testid={testId}
>
{options.map((option) => {
const isSelected = option.value === value
Expand All @@ -65,6 +73,7 @@ export function SettingsSegmentedControl<T extends string = string>({
type="button"
role="radio"
aria-checked={isSelected}
data-value={testId ? option.value : undefined}
onClick={() => onValueChange(option.value)}
className={cn(
'flex items-center gap-1.5 rounded-lg transition-all',
Expand Down
90 changes: 90 additions & 0 deletions apps/electron/src/renderer/context/ChatTextSizeContext.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,90 @@
/**
* ChatTextSizeContext
*
* App-wide "Chat text size" preference — scales the typography of the
* conversation transcript without resizing the rest of the app.
*
* Unlike the OS-level window zoom wired to the View menu
* (`webContents.setZoomFactor`), which scales the entire UI (sidebar, toolbar,
* composer, icons, spacing), this preference only affects the reading text in
* chat. It works by reflecting the choice onto `<html>` as
* `data-chat-text-size="small|medium|large"`; the global CSS in `index.css`
* maps that to a `--chat-font-scale` custom property, which the chat transcript
* container (`.chat-text-scope` in `ChatDisplay`) consumes via
* `font-size: calc(1em * var(--chat-font-scale))`. Because the scale is
* `em`-relative it is exactly neutral at `medium` (1×) and scales the inherited
* message text up/down at `small` (0.9×) / `large` (1.15×).
*
* The preference is persisted in `localStorage` (renderer-only, no backend),
* mirroring the other lightweight UI prefs in `lib/local-storage.ts` and the
* sibling `ReduceMotionContext`.
*/

import React, {
createContext,
useContext,
useState,
useEffect,
useCallback,
type ReactNode,
} from 'react'
import * as storage from '@/lib/local-storage'

export type ChatTextSize = 'small' | 'medium' | 'large'

const CHAT_TEXT_SIZES: readonly ChatTextSize[] = ['small', 'medium', 'large']

const DEFAULT_CHAT_TEXT_SIZE: ChatTextSize = 'medium'

interface ChatTextSizeContextType {
chatTextSize: ChatTextSize
setChatTextSize: (value: ChatTextSize) => void
}

const ChatTextSizeContext = createContext<ChatTextSizeContextType | null>(null)

const CHAT_TEXT_SIZE_ATTR = 'data-chat-text-size'

/** Guard against malformed persisted values. */
function normalize(value: unknown): ChatTextSize {
return CHAT_TEXT_SIZES.includes(value as ChatTextSize)
? (value as ChatTextSize)
: DEFAULT_CHAT_TEXT_SIZE
}

/** Reflect the preference onto <html> so the global CSS var can react. */
function applyChatTextSizeAttribute(size: ChatTextSize): void {
document.documentElement.setAttribute(CHAT_TEXT_SIZE_ATTR, size)
}

export function ChatTextSizeProvider({ children }: { children: ReactNode }) {
const [chatTextSize, setChatTextSizeState] = useState<ChatTextSize>(() =>
normalize(storage.get<ChatTextSize>(storage.KEYS.chatTextSize, DEFAULT_CHAT_TEXT_SIZE)),
)

// Keep the DOM attribute in sync (also covers the initial value on mount).
useEffect(() => {
applyChatTextSizeAttribute(chatTextSize)
}, [chatTextSize])

const setChatTextSize = useCallback((value: ChatTextSize) => {
const next = normalize(value)
setChatTextSizeState(next)
storage.set(storage.KEYS.chatTextSize, next)
applyChatTextSizeAttribute(next)
}, [])

return (
<ChatTextSizeContext.Provider value={{ chatTextSize, setChatTextSize }}>
{children}
</ChatTextSizeContext.Provider>
)
}

export function useChatTextSize(): ChatTextSizeContextType {
const ctx = useContext(ChatTextSizeContext)
if (!ctx) {
throw new Error('useChatTextSize must be used within a ChatTextSizeProvider')
}
return ctx
}
20 changes: 20 additions & 0 deletions apps/electron/src/renderer/index.css
Original file line number Diff line number Diff line change
Expand Up @@ -1386,3 +1386,23 @@ html.dark[data-scenic] .fullscreen-overlay-background {
transition-delay: 0ms !important;
scroll-behavior: auto !important;
}

/* Chat text size: the "Chat text size" control in Appearance settings reflects
its choice onto <html> as data-chat-text-size. That maps to the
--chat-font-scale custom property, which the chat transcript container
(.chat-text-scope) consumes as `font-size: calc(1em * var(--chat-font-scale))`.
Being em-relative keeps the default (medium) exactly neutral and scales only
the conversation's reading text — the sidebar, top bar, composer, and icons
keep their native size. */
:root {
--chat-font-scale: 1;
}
[data-chat-text-size='small'] {
--chat-font-scale: 0.9;
}
[data-chat-text-size='large'] {
--chat-font-scale: 1.15;
}
.chat-text-scope {
font-size: calc(1em * var(--chat-font-scale));
}
1 change: 1 addition & 0 deletions apps/electron/src/renderer/lib/local-storage.ts
Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,7 @@ export const KEYS = {
// Appearance
showConnectionIcons: 'show-connection-icons',
reduceMotion: 'reduce-motion', // Minimize animations/transitions app-wide
chatTextSize: 'chat-text-size', // Scale conversation text without resizing the app chrome

// What's New
whatsNewLastSeenVersion: 'whats-new-last-seen-version',
Expand Down
9 changes: 6 additions & 3 deletions apps/electron/src/renderer/main.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import { Provider as JotaiProvider, useAtomValue } from 'jotai'
import App from './App'
import { ThemeProvider } from './context/ThemeContext'
import { ReduceMotionProvider } from './context/ReduceMotionContext'
import { ChatTextSizeProvider } from './context/ChatTextSizeContext'
import { windowWorkspaceIdAtom } from './atoms/sessions'
import { Toaster } from '@/components/ui/sonner'
import { PetWindowController } from '@/components/pet/PetWindowController'
Expand Down Expand Up @@ -108,9 +109,11 @@ function Root() {
return (
<ThemeProvider activeWorkspaceId={workspaceId}>
<ReduceMotionProvider>
<App />
<Toaster />
<PetWindowController />
<ChatTextSizeProvider>
<App />
<Toaster />
<PetWindowController />
</ChatTextSizeProvider>
</ReduceMotionProvider>
</ThemeProvider>
)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ import { HeaderMenu } from '@/components/ui/HeaderMenu'
import { EditPopover, EditButton, getEditConfig } from '@/components/ui/EditPopover'
import { useTheme } from '@/context/ThemeContext'
import { useReduceMotion } from '@/context/ReduceMotionContext'
import { useChatTextSize, type ChatTextSize } from '@/context/ChatTextSizeContext'
import { useAppShellContext } from '@/context/AppShellContext'
import { routes } from '@/lib/navigate'
import { FolderOpen, Monitor, RefreshCw, Sun, Moon } from 'lucide-react'
Expand Down Expand Up @@ -143,6 +144,9 @@ export default function AppearanceSettingsPage() {
// Reduce motion toggle (renderer-only preference, persisted in localStorage)
const { reduceMotion, setReduceMotion } = useReduceMotion()

// Chat text size (renderer-only preference, persisted in localStorage)
const { chatTextSize, setChatTextSize } = useChatTextSize()

// Pet companion settings + custom pets (synced via shared Jotai atoms)
const {
pets,
Expand Down Expand Up @@ -387,6 +391,21 @@ export default function AppearanceSettingsPage() {
onCheckedChange={setReduceMotion}
testId="reduce-motion-toggle"
/>
<SettingsRow
label={t("settings.appearance.chatTextSize")}
description={t("settings.appearance.chatTextSizeDesc")}
>
<SettingsSegmentedControl
value={chatTextSize}
onValueChange={(value) => setChatTextSize(value as ChatTextSize)}
testId="chat-text-size-control"
options={[
{ value: 'small', label: t("settings.appearance.chatTextSizeSmall") },
{ value: 'medium', label: t("settings.appearance.chatTextSizeDefault") },
{ value: 'large', label: t("settings.appearance.chatTextSizeLarge") },
]}
/>
</SettingsRow>
</SettingsCard>
</SettingsSection>

Expand Down
9 changes: 8 additions & 1 deletion docs/loop/feature-ledger.md
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,14 @@ log, not the system of record.

| slug | title | source | feasibility | status | issue | pr | branch | updated | notes |
| --- | --- | --- | --- | --- | --- | --- | --- | --- | --- |
| reduce-motion | "Reduce motion" accessibility setting in Appearance | Claude desktop / macOS / Windows reduce-motion + `prefers-reduced-motion` | frontend-only | pr-open | [#50](https://github.com/modelstudioai/openwork/issues/50) | [#51](https://github.com/modelstudioai/openwork/pull/51) | loop/reduce-motion | 2026-07-03 | Renderer-only pref (localStorage) applied app-wide via `<MotionConfig reducedMotion>` + `data-reduce-motion` on `<html>` + global CSS guard. Off ⇒ `reducedMotion="user"` (still honors OS). New `ReduceMotionProvider` in `main.tsx`; toggle in Appearance→Interface; 2 new i18n keys ×7 locales. typecheck/`bun test` zero-delta vs main (56-failure set byte-identical); renderer build ✅; i18n parity ✅. CDP assertion included; **could not run locally** (egress 403s Electron binary download). |
| chat-text-size | "Chat text size" setting (Small / Default / Large) in Appearance | Claude Desktop "Chat font" + anthropics/claude-code #50543/#48887; ChatGPT desktop font-size requests | frontend-only | pr-open | [#64](https://github.com/modelstudioai/openwork/issues/64) | [#65](https://github.com/modelstudioai/openwork/pull/65) | loop/chat-text-size | 2026-07-07 | Renderer-only pref (localStorage `craft-chat-text-size`) reflected onto `<html>` as `data-chat-text-size` + `--chat-font-scale` CSS var (0.9/1/1.15). Transcript container gets `.chat-text-scope` with `font-size: calc(1em * var(--chat-font-scale))` — em-relative ⇒ neutral at Default, scales only conversation text (chrome untouched). New `ChatTextSizeProvider`; segmented control in Appearance→Interface; `SettingsSegmentedControl` gained optional `testId`/`data-value`; 5 new i18n keys ×7 locales. typecheck/`bun test` **zero-delta vs main** (11 pre-existing tsc errors + 56 pre-existing fail-lines byte-identical); renderer build ✅; i18n parity ✅. CDP assertion authored (drives control, asserts attr/CSS-var/localStorage + probe computed font-size ratio ~1.15/0.9); **could not execute** — Electron runtime binary download is org-egress-policy 403 (same block as #51). |
| conversation-width | "Conversation width" setting (Comfortable / Wide / Full) in Appearance | Claude/ChatGPT/Codex desktop content-width settings | frontend-only | pr-open | [#62](https://github.com/modelstudioai/openwork/issues/62) | [#63](https://github.com/modelstudioai/openwork/pull/63) | loop/conversation-width | 2026-07-07 | Opened by a prior run; reconciled from GitHub. `ConversationWidthProvider` + `data-conversation-width` + `--chat-content-max-width`. Awaiting review. |
| composer-word-count | Live word / character count indicator in the chat composer | Claude/ChatGPT/Codex desktop composer meta | frontend-only | pr-open | [#60](https://github.com/modelstudioai/openwork/issues/60) | [#61](https://github.com/modelstudioai/openwork/pull/61) | loop/composer-word-count | 2026-07-07 | Opened by a prior run; reconciled from GitHub. Awaiting review. |
| shortcuts-search | Search box on the Settings → Keyboard Shortcuts page | VS Code / Codex keyboard-shortcuts search | frontend-only | pr-open | [#58](https://github.com/modelstudioai/openwork/issues/58) | [#59](https://github.com/modelstudioai/openwork/pull/59) | loop/shortcuts-search | 2026-07-07 | Opened by a prior run; reconciled from GitHub. Awaiting review. |
| command-palette-recents | Surface recently-used commands in the Command Palette (⌘K) | Claude Code / VS Code / Linear recent commands | frontend-only | pr-open | [#56](https://github.com/modelstudioai/openwork/issues/56) | [#57](https://github.com/modelstudioai/openwork/pull/57) | loop/command-palette-recents | 2026-07-07 | Opened by a prior run; reconciled from GitHub. Awaiting review. |
| thinking-menu-shortcut | Keyboard shortcut (⌘⇧E) to open the composer's thinking-effort menu | Claude Code Desktop ⌘⇧E | frontend-only | pr-open | [#54](https://github.com/modelstudioai/openwork/issues/54) | [#55](https://github.com/modelstudioai/openwork/pull/55) | loop/thinking-menu-shortcut | 2026-07-07 | Opened by a prior run; reconciled from GitHub. Awaiting review. |
| composer-prompt-history | Recall previously-sent prompts in the composer with Up / Down | Claude Code / shell-style prompt history | frontend-only | pr-open | [#52](https://github.com/modelstudioai/openwork/issues/52) | [#53](https://github.com/modelstudioai/openwork/pull/53) | loop/composer-prompt-history | 2026-07-07 | Opened by a prior run; reconciled from GitHub. Awaiting review. |
| reduce-motion | "Reduce motion" accessibility setting in Appearance | Claude desktop / macOS / Windows reduce-motion + `prefers-reduced-motion` | frontend-only | merged | [#50](https://github.com/modelstudioai/openwork/issues/50) | [#51](https://github.com/modelstudioai/openwork/pull/51) | loop/reduce-motion | 2026-07-07 | **Merged** into `main`. Renderer-only pref (localStorage) applied app-wide via `<MotionConfig reducedMotion>` + `data-reduce-motion` on `<html>` + global CSS guard. New `ReduceMotionProvider` in `main.tsx`; toggle in Appearance→Interface; 2 new i18n keys ×7 locales. Established the provider+data-attr+CSS pattern reused by chat-text-size and conversation-width. |
| composer-expand | Expand / collapse (maximize) toggle for the chat composer | Claude/ChatGPT/Codex desktop composer maximize | frontend-only | pr-open | [#48](https://github.com/modelstudioai/openwork/issues/48) | [#49](https://github.com/modelstudioai/openwork/pull/49) | loop/composer-expand | 2026-07-03 | Opened by a prior run. Adds `isComposerExpanded` toggle in `FreeFormInput`; 2 new i18n keys. Awaiting review. |
| scroll-to-bottom | "Jump to latest" (scroll-to-bottom) button in the chat transcript | Claude Code / ChatGPT / Codex desktop | frontend-only | pr-open | [#46](https://github.com/modelstudioai/openwork/issues/46) | [#47](https://github.com/modelstudioai/openwork/pull/47) | loop/scroll-to-bottom | 2026-07-02 | Opened by a prior run. Floating jump button in `ChatDisplay` + `seed()` harness hook. Awaiting review. |
| thinking-level-picker | Thinking-level (reasoning effort) picker in the chat composer | Claude Code Desktop effort menu (⌘⇧E) + OpenWork's own model picker | frontend-only | merged | [#44](https://github.com/modelstudioai/openwork/issues/44) | [#45](https://github.com/modelstudioai/openwork/pull/45) | loop/thinking-level-picker | 2026-07-03 | **Merged** into `main` (2026-07-02). `thinkingLevel`/`onThinkingLevelChange` already plumbed to `FreeFormInput`; only the UI trigger was missing. Reuses `thinking.*` + `settings.ai.thinking` i18n keys (zero new keys). |
Expand Down
Loading