From c83e3025ee44477d0587fac8478665ffa8e0adcb Mon Sep 17 00:00:00 2001 From: Claude Date: Mon, 6 Jul 2026 13:37:59 +0000 Subject: [PATCH 1/2] Add a "Conversation width" setting (Comfortable / Wide / Full) to Appearance MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Adds a renderer-only "Conversation width" preference to Settings → Appearance → Interface, letting users widen the chat transcript and composer beyond the fixed 840px reading column — matching the width controls in Claude Desktop, ChatGPT desktop and Codex / VS Code. - New ConversationWidthProvider (localStorage `craft-conversation-width`, mirroring ReduceMotionContext) reflects the choice onto `` as `data-conversation-width` and drives a `--chat-content-max-width` CSS variable (840px / 1100px / none). - ChatDisplay transcript container and ChatInputZone read the variable via `max-width: var(--chat-content-max-width, 840px)`; the fallback leaves the shared web viewer unchanged and compact/popover embeddings untouched. - Segmented control in the Appearance Interface section; adds an optional `testId` to SettingsSegmentedControl for e2e targeting. - 5 new i18n keys across all 7 locales. - CDP assertion drives the real Settings UI, cycling all three options and asserting the radio state, `` attribute, computed CSS variable and persisted localStorage value. Closes #62 --- .../components/app-shell/ChatDisplay.tsx | 7 +- .../app-shell/input/ChatInputZone.tsx | 3 +- .../settings/SettingsSegmentedControl.tsx | 6 + .../context/ConversationWidthContext.tsx | 96 ++++++++++++++ .../src/renderer/lib/local-storage.ts | 1 + apps/electron/src/renderer/main.tsx | 9 +- .../pages/settings/AppearanceSettingsPage.tsx | 19 +++ docs/loop/feature-ledger.md | 1 + e2e/assertions/conversation-width.assert.ts | 123 ++++++++++++++++++ packages/shared/src/i18n/locales/de.json | 5 + packages/shared/src/i18n/locales/en.json | 5 + packages/shared/src/i18n/locales/es.json | 5 + packages/shared/src/i18n/locales/hu.json | 5 + packages/shared/src/i18n/locales/ja.json | 5 + packages/shared/src/i18n/locales/pl.json | 5 + packages/shared/src/i18n/locales/zh-Hans.json | 5 + 16 files changed, 294 insertions(+), 6 deletions(-) create mode 100644 apps/electron/src/renderer/context/ConversationWidthContext.tsx create mode 100644 e2e/assertions/conversation-width.assert.ts diff --git a/apps/electron/src/renderer/components/app-shell/ChatDisplay.tsx b/apps/electron/src/renderer/components/app-shell/ChatDisplay.tsx index 81d0f1978..9ee13a3c1 100644 --- a/apps/electron/src/renderer/components/app-shell/ChatDisplay.tsx +++ b/apps/electron/src/renderer/components/app-shell/ChatDisplay.tsx @@ -1712,8 +1712,11 @@ export const ChatDisplay = React.forwardRef }} > -
diff --git a/apps/electron/src/renderer/components/app-shell/input/ChatInputZone.tsx b/apps/electron/src/renderer/components/app-shell/input/ChatInputZone.tsx index 02a021f06..0450f74f1 100644 --- a/apps/electron/src/renderer/components/app-shell/input/ChatInputZone.tsx +++ b/apps/electron/src/renderer/components/app-shell/input/ChatInputZone.tsx @@ -83,8 +83,9 @@ export function ChatInputZone({ return (
{ size?: 'sm' | 'md' /** Additional className */ className?: string + /** Optional test id — applied to the group and, suffixed with `-`, to each option */ + testId?: string } /** @@ -50,10 +52,12 @@ export function SettingsSegmentedControl({ options, size = 'md', className, + testId, }: SettingsSegmentedControlProps) { return (
{options.map((option) => { @@ -65,6 +69,8 @@ export function SettingsSegmentedControl({ type="button" role="radio" aria-checked={isSelected} + data-testid={testId ? `${testId}-${option.value}` : undefined} + data-value={option.value} onClick={() => onValueChange(option.value)} className={cn( 'flex items-center gap-1.5 rounded-lg transition-all', diff --git a/apps/electron/src/renderer/context/ConversationWidthContext.tsx b/apps/electron/src/renderer/context/ConversationWidthContext.tsx new file mode 100644 index 000000000..61160172c --- /dev/null +++ b/apps/electron/src/renderer/context/ConversationWidthContext.tsx @@ -0,0 +1,96 @@ +/** + * ConversationWidthContext + * + * App-wide "Conversation width" preference — controls the reading-column width + * of the chat transcript and composer, mirroring the width controls in + * comparable desktop clients (Claude Desktop, ChatGPT desktop, Codex / VS Code). + * + * Three modes: + * - `comfortable` (default) — the classic ~840px reading column; + * - `wide` — a roomier 1100px column for code-heavy conversations; + * - `full` — no max-width, uses the full available panel width. + * + * When applied it: + * - sets `data-conversation-width=""` on `` (for CSS hooks + tests); + * - drives a CSS custom property `--chat-content-max-width` on `` + * (`840px` / `1100px` / `none`). The transcript container and composer read + * it via `max-width: var(--chat-content-max-width, 840px)`, so the fallback + * keeps the shared web viewer (`packages/ui`) unchanged at 840px. + * + * The preference is persisted in `localStorage` (renderer-only, no backend), + * mirroring the other lightweight UI prefs in `lib/local-storage.ts`. + */ + +import React, { + createContext, + useContext, + useState, + useEffect, + useCallback, + type ReactNode, +} from 'react' +import * as storage from '@/lib/local-storage' + +export type ConversationWidth = 'comfortable' | 'wide' | 'full' + +/** Resolved CSS `max-width` value for each mode. */ +const MAX_WIDTH_BY_MODE: Record = { + comfortable: '840px', + wide: '1100px', + full: 'none', +} + +const CONVERSATION_WIDTH_ATTR = 'data-conversation-width' +const MAX_WIDTH_VAR = '--chat-content-max-width' + +const DEFAULT_WIDTH: ConversationWidth = 'comfortable' + +function isConversationWidth(value: unknown): value is ConversationWidth { + return value === 'comfortable' || value === 'wide' || value === 'full' +} + +interface ConversationWidthContextType { + conversationWidth: ConversationWidth + setConversationWidth: (value: ConversationWidth) => void +} + +const ConversationWidthContext = createContext(null) + +/** Reflect the preference onto so CSS + the transcript/composer can react. */ +function applyConversationWidth(mode: ConversationWidth): void { + const root = document.documentElement + root.setAttribute(CONVERSATION_WIDTH_ATTR, mode) + root.style.setProperty(MAX_WIDTH_VAR, MAX_WIDTH_BY_MODE[mode]) +} + +export function ConversationWidthProvider({ children }: { children: ReactNode }) { + const [conversationWidth, setConversationWidthState] = useState(() => { + const stored = storage.get(storage.KEYS.conversationWidth, DEFAULT_WIDTH) + return isConversationWidth(stored) ? stored : DEFAULT_WIDTH + }) + + // Keep the DOM in sync (also covers the initial value on mount). + useEffect(() => { + applyConversationWidth(conversationWidth) + }, [conversationWidth]) + + const setConversationWidth = useCallback((value: ConversationWidth) => { + setConversationWidthState(value) + storage.set(storage.KEYS.conversationWidth, value) + applyConversationWidth(value) + }, []) + + return ( + + {children} + + ) +} + +export function useConversationWidth(): ConversationWidthContextType { + const ctx = useContext(ConversationWidthContext) + if (!ctx) { + throw new Error('useConversationWidth must be used within a ConversationWidthProvider') + } + return ctx +} diff --git a/apps/electron/src/renderer/lib/local-storage.ts b/apps/electron/src/renderer/lib/local-storage.ts index a4369c288..c50b2f911 100644 --- a/apps/electron/src/renderer/lib/local-storage.ts +++ b/apps/electron/src/renderer/lib/local-storage.ts @@ -62,6 +62,7 @@ export const KEYS = { // Appearance showConnectionIcons: 'show-connection-icons', reduceMotion: 'reduce-motion', // Minimize animations/transitions app-wide + conversationWidth: 'conversation-width', // Chat reading-column width: comfortable | wide | full // What's New whatsNewLastSeenVersion: 'whats-new-last-seen-version', diff --git a/apps/electron/src/renderer/main.tsx b/apps/electron/src/renderer/main.tsx index 14729366d..768032fb8 100644 --- a/apps/electron/src/renderer/main.tsx +++ b/apps/electron/src/renderer/main.tsx @@ -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 { ConversationWidthProvider } from './context/ConversationWidthContext' import { windowWorkspaceIdAtom } from './atoms/sessions' import { Toaster } from '@/components/ui/sonner' import { PetWindowController } from '@/components/pet/PetWindowController' @@ -108,9 +109,11 @@ function Root() { return ( - - - + + + + + ) diff --git a/apps/electron/src/renderer/pages/settings/AppearanceSettingsPage.tsx b/apps/electron/src/renderer/pages/settings/AppearanceSettingsPage.tsx index 4da9ba24c..cde36c00c 100644 --- a/apps/electron/src/renderer/pages/settings/AppearanceSettingsPage.tsx +++ b/apps/electron/src/renderer/pages/settings/AppearanceSettingsPage.tsx @@ -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 { useConversationWidth } from '@/context/ConversationWidthContext' import { useAppShellContext } from '@/context/AppShellContext' import { routes } from '@/lib/navigate' import { FolderOpen, Monitor, RefreshCw, Sun, Moon } from 'lucide-react' @@ -143,6 +144,9 @@ export default function AppearanceSettingsPage() { // Reduce motion toggle (renderer-only preference, persisted in localStorage) const { reduceMotion, setReduceMotion } = useReduceMotion() + // Conversation width (renderer-only preference, persisted in localStorage) + const { conversationWidth, setConversationWidth } = useConversationWidth() + // Pet companion settings + custom pets (synced via shared Jotai atoms) const { pets, @@ -387,6 +391,21 @@ export default function AppearanceSettingsPage() { onCheckedChange={setReduceMotion} testId="reduce-motion-toggle" /> + + + diff --git a/docs/loop/feature-ledger.md b/docs/loop/feature-ledger.md index 95d8cb26a..32f938318 100644 --- a/docs/loop/feature-ledger.md +++ b/docs/loop/feature-ledger.md @@ -33,6 +33,7 @@ log, not the system of record. | slug | title | source | feasibility | status | issue | pr | branch | updated | notes | | --- | --- | --- | --- | --- | --- | --- | --- | --- | --- | +| conversation-width | "Conversation width" (Comfortable / Wide / Full) setting in Appearance | Claude Desktop wider-chat / ChatGPT desktop width / Codex & VS Code content width | frontend-only | pr-open | [#62](https://github.com/modelstudioai/openwork/issues/62) | [#PR](https://github.com/modelstudioai/openwork/pull/PR) | loop/conversation-width | 2026-07-06 | Renderer-only pref (localStorage `craft-conversation-width`). New `ConversationWidthProvider` in `main.tsx` sets `data-conversation-width` on `` + drives CSS var `--chat-content-max-width` (840px/1100px/none). Transcript container (`ChatDisplay`) + composer (`ChatInputZone`) read it via `max-width: var(--chat-content-max-width, 840px)`; fallback keeps shared web viewer unchanged. Segmented control in Appearance→Interface; added `testId` to `SettingsSegmentedControl`; 5 new i18n keys ×7 locales. typecheck/`bun test` zero-delta vs main (56-failure set byte-identical); renderer build ✅; i18n parity ✅. CDP assertion `e2e/assertions/conversation-width.assert.ts` included; **could not run locally** (egress 403 blocks Electron binary + `libsignal-node`/`eslint-config` git-tarball deps — same env blocker as #51). | | composer-word-count | Live word/character/line count indicator in the chat composer | Codex/Claude desktop composer counters + editor status bars | 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-06 | Pure `computeComposerCounts` module (+unit tests) → `ComposerCountIndicator` in the composer toolbar, derived from the existing `input` string; hidden when empty / in compact mode. 3 new plural-neutral i18n keys (`chat.composer{Words,Characters,Lines}`) in all 7 locales. typecheck:all zero-delta (11 pre-existing baseline errors, none in touched files); unit tests 10/10; i18n parity OK; renderer build ✅; assertion transpiles. **CDP could not run locally**: the e2e build 403s on the Electron binary download and the `libsignal` WhatsApp-worker dep (same egress block as prior loop PRs); `composer-count.assert.ts` included for CI/reviewer. | | shortcuts-search | Search box on the Settings → Keyboard Shortcuts page | Codex "keypress search" + VS Code / Claude keybindings search; mirrors OpenWork's own settings-navigator search (#40) | 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-05 | Pure renderer view over the action registry (`actionsByCategory` + `getHotkeyDisplay`); filters rows by action label **and** rendered key tokens ("keypress search"), hides empty sections, shows empty state. Reuses `common.search`/`noResultsFound`/`clear` (**zero** new i18n keys). Added `data-testid="settings-item-"` to settings-nav items for e2e navigation. typecheck/renderer-build/i18n-parity zero-delta vs main; touched-area tests pass. CDP assertion written (app launch egress-blocked locally, same as prior rounds). | | command-palette-recents | "Recently used" section in the Command Palette (⌘K) | VS Code ⌘⇧P / Raycast / Linear ⌘K / Claude Code Desktop command menu | 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-05 | Palette (#42) had no memory of what you run. Adds a persisted recents list (`localStorage` `command-palette-recents`, dedupe/cap-6, newest-first) surfaced as a top "Recently used" group, hidden while searching. Pure `pushRecent` module + 6 unit tests (pass). One new i18n key `commands.recent` × 7 locales. typecheck/`bun test` zero-delta vs main (56 baseline fails on both, 0 new); renderer build ✅. **CDP could not run locally** (same env block as prior rounds: egress 403s Electron binary + `libsignal`/WhatsApp-worker dep) — assertion included for CI/reviewer. | diff --git a/e2e/assertions/conversation-width.assert.ts b/e2e/assertions/conversation-width.assert.ts new file mode 100644 index 000000000..2e0b8c1ce --- /dev/null +++ b/e2e/assertions/conversation-width.assert.ts @@ -0,0 +1,123 @@ +/** + * Feature assertion: the "Conversation width" segmented control in + * Settings → Appearance actually applies and persists an app-wide reading-column + * width preference. + * + * Drives the real UI over CDP entirely in the draft/no-session state (no seeded + * conversation, no backend connection): opens Settings → Appearance, selects each + * width option, and asserts the observable effects that the transcript + composer + * consume — the selected radio state, the `data-conversation-width` attribute on + * , the computed `--chat-content-max-width` CSS custom property (the value + * the chat containers read via `max-width: var(--chat-content-max-width, 840px)`), + * and the persisted localStorage value. + * + * Cycling through all three options proves it both applies and reverts, not merely + * renders. + */ + +import type { Assertion } from '../runner'; + +const SETTINGS_NAV = '[data-testid="nav:settings"]'; +const APPEARANCE_NAV = '[data-testid="settings-nav-appearance"]'; +const CONTROL = '[data-testid="conversation-width-control"]'; +const STORAGE_KEY = 'craft-conversation-width'; + +type Mode = 'comfortable' | 'wide' | 'full'; + +const EXPECTED_MAX_WIDTH: Record = { + comfortable: '840px', + wide: '1100px', + full: 'none', +}; + +/** aria-checked ("true" | "false" | null) for a given option button. */ +function optionCheckedExpr(mode: Mode): string { + return `(() => { + const el = document.querySelector('[data-testid="conversation-width-control-${mode}"]'); + return el ? el.getAttribute('aria-checked') : null; + })()`; +} + +/** The `data-conversation-width` marker value on . */ +function htmlModeExpr(): string { + return `document.documentElement.getAttribute('data-conversation-width')`; +} + +/** The computed `--chat-content-max-width` custom property on . */ +function cssVarExpr(): string { + return `getComputedStyle(document.documentElement).getPropertyValue('--chat-content-max-width').trim()`; +} + +/** The persisted localStorage value (JSON-encoded string) for the preference. */ +function storedValueExpr(): string { + return `window.localStorage.getItem(${JSON.stringify(STORAGE_KEY)})`; +} + +const assertion: Assertion = { + name: 'conversation-width control applies and persists an app-wide width preference', + async run(app) { + const { session } = app; + + // App fully mounted. + await session.waitForFunction( + '!document.getElementById("_loader") && (document.getElementById("root")?.childElementCount ?? 0) > 0', + { timeoutMs: 30000, message: 'app did not mount' }, + ); + + // Open Settings → Appearance (real user path). + await session.click(SETTINGS_NAV, { timeoutMs: 15000 }); + await session.click(APPEARANCE_NAV, { timeoutMs: 15000 }); + + // The control is the feature under test — its presence is the first signal. + await session.waitForSelector(CONTROL, { + timeoutMs: 15000, + message: 'conversation-width control did not render', + }); + + // Initial state: comfortable selected, marked comfortable, 840px column. + const initialChecked = await session.evaluate(optionCheckedExpr('comfortable')); + if (initialChecked !== 'true') { + throw new Error(`expected "comfortable" selected initially, saw aria-checked=${initialChecked}`); + } + const initialMode = await session.evaluate(htmlModeExpr()); + if (initialMode !== 'comfortable') { + throw new Error(`expected data-conversation-width="comfortable" initially, saw ${JSON.stringify(initialMode)}`); + } + const initialVar = await session.evaluate(cssVarExpr()); + if (initialVar !== EXPECTED_MAX_WIDTH.comfortable) { + throw new Error(`expected --chat-content-max-width=840px initially, saw ${JSON.stringify(initialVar)}`); + } + + // Selecting each mode applies + persists; the final "comfortable" proves revert. + const sequence: Mode[] = ['full', 'wide', 'comfortable']; + for (const mode of sequence) { + await session.click(`[data-testid="conversation-width-control-${mode}"]`); + + // Radio state flips on. + await session.waitForFunction( + `${optionCheckedExpr(mode)} === 'true'`, + { timeoutMs: 5000, message: `"${mode}" option did not become selected` }, + ); + + // marker updates. + await session.waitForFunction( + `${htmlModeExpr()} === ${JSON.stringify(mode)}`, + { timeoutMs: 5000, message: `data-conversation-width did not update to "${mode}"` }, + ); + + // CSS custom property (what the chat containers actually read) updates. + await session.waitForFunction( + `${cssVarExpr()} === ${JSON.stringify(EXPECTED_MAX_WIDTH[mode])}`, + { timeoutMs: 5000, message: `--chat-content-max-width did not become ${EXPECTED_MAX_WIDTH[mode]} for "${mode}"` }, + ); + + // Persisted (JSON-encoded string). + const stored = await session.evaluate(storedValueExpr()); + if (stored !== JSON.stringify(mode)) { + throw new Error(`expected persisted ${JSON.stringify(JSON.stringify(mode))} after selecting "${mode}", saw ${JSON.stringify(stored)}`); + } + } + }, +}; + +export default assertion; diff --git a/packages/shared/src/i18n/locales/de.json b/packages/shared/src/i18n/locales/de.json index 13e498969..60c31ee43 100644 --- a/packages/shared/src/i18n/locales/de.json +++ b/packages/shared/src/i18n/locales/de.json @@ -786,6 +786,11 @@ "settings.appearance.noToolIcons": "Keine Werkzeugsymbol-Zuordnungen gefunden", "settings.appearance.reduceMotion": "Bewegung reduzieren", "settings.appearance.reduceMotionDesc": "Animationen und Übergänge in der gesamten App minimieren.", + "settings.appearance.conversationWidth": "Unterhaltungsbreite", + "settings.appearance.conversationWidthDesc": "Legt fest, wie breit der Chatverlauf und das Eingabefeld sind.", + "settings.appearance.conversationWidthComfortable": "Komfortabel", + "settings.appearance.conversationWidthWide": "Breit", + "settings.appearance.conversationWidthFull": "Voll", "settings.appearance.pet": "Begleiter", "settings.appearance.petCustom": "Eigene Begleiter", "settings.appearance.petCustomHint": "Füge einen Ordner mit pet.json und einem 8x9-Spritesheet hinzu und aktualisiere dann.", diff --git a/packages/shared/src/i18n/locales/en.json b/packages/shared/src/i18n/locales/en.json index c12137fa1..e336760eb 100644 --- a/packages/shared/src/i18n/locales/en.json +++ b/packages/shared/src/i18n/locales/en.json @@ -786,6 +786,11 @@ "settings.appearance.noToolIcons": "No tool icon mappings found", "settings.appearance.reduceMotion": "Reduce motion", "settings.appearance.reduceMotionDesc": "Minimize animations and transitions throughout the app.", + "settings.appearance.conversationWidth": "Conversation width", + "settings.appearance.conversationWidthDesc": "Set how wide the chat transcript and composer are.", + "settings.appearance.conversationWidthComfortable": "Comfortable", + "settings.appearance.conversationWidthWide": "Wide", + "settings.appearance.conversationWidthFull": "Full", "settings.appearance.pet": "Pet", "settings.appearance.petCustom": "Custom pets", "settings.appearance.petCustomHint": "Add a pet folder with pet.json and an 8x9 spritesheet, then refresh.", diff --git a/packages/shared/src/i18n/locales/es.json b/packages/shared/src/i18n/locales/es.json index 6948b2233..e9c70c84c 100644 --- a/packages/shared/src/i18n/locales/es.json +++ b/packages/shared/src/i18n/locales/es.json @@ -786,6 +786,11 @@ "settings.appearance.noToolIcons": "No se encontraron asignaciones de iconos de herramientas", "settings.appearance.reduceMotion": "Reducir movimiento", "settings.appearance.reduceMotionDesc": "Minimiza las animaciones y transiciones en toda la aplicación.", + "settings.appearance.conversationWidth": "Ancho de la conversación", + "settings.appearance.conversationWidthDesc": "Define el ancho de la transcripción del chat y del compositor.", + "settings.appearance.conversationWidthComfortable": "Cómodo", + "settings.appearance.conversationWidthWide": "Ancho", + "settings.appearance.conversationWidthFull": "Completo", "settings.appearance.pet": "Mascota", "settings.appearance.petCustom": "Mascotas personalizadas", "settings.appearance.petCustomHint": "Añade una carpeta con pet.json y un spritesheet de 8x9, luego actualiza.", diff --git a/packages/shared/src/i18n/locales/hu.json b/packages/shared/src/i18n/locales/hu.json index ba7f4154d..e9e8f3ce3 100644 --- a/packages/shared/src/i18n/locales/hu.json +++ b/packages/shared/src/i18n/locales/hu.json @@ -786,6 +786,11 @@ "settings.appearance.noToolIcons": "Nem található eszközikon-hozzárendelés", "settings.appearance.reduceMotion": "Mozgás csökkentése", "settings.appearance.reduceMotionDesc": "Az animációk és átmenetek minimalizálása az egész alkalmazásban.", + "settings.appearance.conversationWidth": "Beszélgetés szélessége", + "settings.appearance.conversationWidthDesc": "Beállítja a csevegés és a szerkesztő szélességét.", + "settings.appearance.conversationWidthComfortable": "Kényelmes", + "settings.appearance.conversationWidthWide": "Széles", + "settings.appearance.conversationWidthFull": "Teljes", "settings.appearance.pet": "Kabala", "settings.appearance.petCustom": "Egyéni kabalák", "settings.appearance.petCustomHint": "Adj hozzá egy mappát pet.json fájllal és 8x9-es spritesheettel, majd frissíts.", diff --git a/packages/shared/src/i18n/locales/ja.json b/packages/shared/src/i18n/locales/ja.json index f95e6b40d..7d6960b6d 100644 --- a/packages/shared/src/i18n/locales/ja.json +++ b/packages/shared/src/i18n/locales/ja.json @@ -786,6 +786,11 @@ "settings.appearance.noToolIcons": "ツールアイコンのマッピングが見つかりません", "settings.appearance.reduceMotion": "モーションを減らす", "settings.appearance.reduceMotionDesc": "アプリ全体のアニメーションとトランジションを最小限にします。", + "settings.appearance.conversationWidth": "会話の幅", + "settings.appearance.conversationWidthDesc": "チャット履歴と入力欄の表示幅を設定します。", + "settings.appearance.conversationWidthComfortable": "標準", + "settings.appearance.conversationWidthWide": "ワイド", + "settings.appearance.conversationWidthFull": "全幅", "settings.appearance.pet": "ペット", "settings.appearance.petCustom": "カスタムペット", "settings.appearance.petCustomHint": "pet.json と 8x9 のスプライトシートを含むフォルダーを追加してから更新します。", diff --git a/packages/shared/src/i18n/locales/pl.json b/packages/shared/src/i18n/locales/pl.json index 10db689fb..838193359 100644 --- a/packages/shared/src/i18n/locales/pl.json +++ b/packages/shared/src/i18n/locales/pl.json @@ -786,6 +786,11 @@ "settings.appearance.noToolIcons": "Nie znaleziono mapowań ikon narzędzi", "settings.appearance.reduceMotion": "Ogranicz ruch", "settings.appearance.reduceMotionDesc": "Ogranicz animacje i przejścia w całej aplikacji.", + "settings.appearance.conversationWidth": "Szerokość rozmowy", + "settings.appearance.conversationWidthDesc": "Określa szerokość transkrypcji czatu i pola tekstowego.", + "settings.appearance.conversationWidthComfortable": "Komfortowa", + "settings.appearance.conversationWidthWide": "Szeroka", + "settings.appearance.conversationWidthFull": "Pełna", "settings.appearance.pet": "Maskotka", "settings.appearance.petCustom": "Własne maskotki", "settings.appearance.petCustomHint": "Dodaj folder z pet.json i arkuszem sprite'ów 8x9, a potem odśwież.", diff --git a/packages/shared/src/i18n/locales/zh-Hans.json b/packages/shared/src/i18n/locales/zh-Hans.json index 1fea1373c..11f469a97 100644 --- a/packages/shared/src/i18n/locales/zh-Hans.json +++ b/packages/shared/src/i18n/locales/zh-Hans.json @@ -786,6 +786,11 @@ "settings.appearance.noToolIcons": "未找到工具图标映射", "settings.appearance.reduceMotion": "减少动态效果", "settings.appearance.reduceMotionDesc": "在整个应用中尽量减少动画和过渡效果。", + "settings.appearance.conversationWidth": "对话宽度", + "settings.appearance.conversationWidthDesc": "设置聊天记录和输入框的显示宽度。", + "settings.appearance.conversationWidthComfortable": "舒适", + "settings.appearance.conversationWidthWide": "宽", + "settings.appearance.conversationWidthFull": "全宽", "settings.appearance.pet": "宠物", "settings.appearance.petCustom": "自定义宠物", "settings.appearance.petCustomHint": "每只宠物一个文件夹,包含 pet.json 和 8x9 spritesheet,放好后刷新。", From c4ad5bf0586c7353b95be5be343f02e71a5c132d Mon Sep 17 00:00:00 2001 From: Claude Date: Mon, 6 Jul 2026 13:38:43 +0000 Subject: [PATCH 2/2] docs(loop): record conversation-width PR #63 in ledger --- docs/loop/feature-ledger.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/loop/feature-ledger.md b/docs/loop/feature-ledger.md index 32f938318..2d2e73c0b 100644 --- a/docs/loop/feature-ledger.md +++ b/docs/loop/feature-ledger.md @@ -33,7 +33,7 @@ log, not the system of record. | slug | title | source | feasibility | status | issue | pr | branch | updated | notes | | --- | --- | --- | --- | --- | --- | --- | --- | --- | --- | -| conversation-width | "Conversation width" (Comfortable / Wide / Full) setting in Appearance | Claude Desktop wider-chat / ChatGPT desktop width / Codex & VS Code content width | frontend-only | pr-open | [#62](https://github.com/modelstudioai/openwork/issues/62) | [#PR](https://github.com/modelstudioai/openwork/pull/PR) | loop/conversation-width | 2026-07-06 | Renderer-only pref (localStorage `craft-conversation-width`). New `ConversationWidthProvider` in `main.tsx` sets `data-conversation-width` on `` + drives CSS var `--chat-content-max-width` (840px/1100px/none). Transcript container (`ChatDisplay`) + composer (`ChatInputZone`) read it via `max-width: var(--chat-content-max-width, 840px)`; fallback keeps shared web viewer unchanged. Segmented control in Appearance→Interface; added `testId` to `SettingsSegmentedControl`; 5 new i18n keys ×7 locales. typecheck/`bun test` zero-delta vs main (56-failure set byte-identical); renderer build ✅; i18n parity ✅. CDP assertion `e2e/assertions/conversation-width.assert.ts` included; **could not run locally** (egress 403 blocks Electron binary + `libsignal-node`/`eslint-config` git-tarball deps — same env blocker as #51). | +| conversation-width | "Conversation width" (Comfortable / Wide / Full) setting in Appearance | Claude Desktop wider-chat / ChatGPT desktop width / Codex & VS Code content width | 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-06 | Renderer-only pref (localStorage `craft-conversation-width`). New `ConversationWidthProvider` in `main.tsx` sets `data-conversation-width` on `` + drives CSS var `--chat-content-max-width` (840px/1100px/none). Transcript container (`ChatDisplay`) + composer (`ChatInputZone`) read it via `max-width: var(--chat-content-max-width, 840px)`; fallback keeps shared web viewer unchanged. Segmented control in Appearance→Interface; added `testId` to `SettingsSegmentedControl`; 5 new i18n keys ×7 locales. typecheck/`bun test` zero-delta vs main (56-failure set byte-identical); renderer build ✅; i18n parity ✅. CDP assertion `e2e/assertions/conversation-width.assert.ts` included; **could not run locally** (egress 403 blocks Electron binary + `libsignal-node`/`eslint-config` git-tarball deps — same env blocker as #51). | | composer-word-count | Live word/character/line count indicator in the chat composer | Codex/Claude desktop composer counters + editor status bars | 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-06 | Pure `computeComposerCounts` module (+unit tests) → `ComposerCountIndicator` in the composer toolbar, derived from the existing `input` string; hidden when empty / in compact mode. 3 new plural-neutral i18n keys (`chat.composer{Words,Characters,Lines}`) in all 7 locales. typecheck:all zero-delta (11 pre-existing baseline errors, none in touched files); unit tests 10/10; i18n parity OK; renderer build ✅; assertion transpiles. **CDP could not run locally**: the e2e build 403s on the Electron binary download and the `libsignal` WhatsApp-worker dep (same egress block as prior loop PRs); `composer-count.assert.ts` included for CI/reviewer. | | shortcuts-search | Search box on the Settings → Keyboard Shortcuts page | Codex "keypress search" + VS Code / Claude keybindings search; mirrors OpenWork's own settings-navigator search (#40) | 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-05 | Pure renderer view over the action registry (`actionsByCategory` + `getHotkeyDisplay`); filters rows by action label **and** rendered key tokens ("keypress search"), hides empty sections, shows empty state. Reuses `common.search`/`noResultsFound`/`clear` (**zero** new i18n keys). Added `data-testid="settings-item-"` to settings-nav items for e2e navigation. typecheck/renderer-build/i18n-parity zero-delta vs main; touched-area tests pass. CDP assertion written (app launch egress-blocked locally, same as prior rounds). | | command-palette-recents | "Recently used" section in the Command Palette (⌘K) | VS Code ⌘⇧P / Raycast / Linear ⌘K / Claude Code Desktop command menu | 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-05 | Palette (#42) had no memory of what you run. Adds a persisted recents list (`localStorage` `command-palette-recents`, dedupe/cap-6, newest-first) surfaced as a top "Recently used" group, hidden while searching. Pure `pushRecent` module + 6 unit tests (pass). One new i18n key `commands.recent` × 7 locales. typecheck/`bun test` zero-delta vs main (56 baseline fails on both, 0 new); renderer build ✅. **CDP could not run locally** (same env block as prior rounds: egress 403s Electron binary + `libsignal`/WhatsApp-worker dep) — assertion included for CI/reviewer. |