diff --git a/apps/electron/src/renderer/components/app-shell/ChatDisplay.tsx b/apps/electron/src/renderer/components/app-shell/ChatDisplay.tsx index f0c1a54a2..d917a768b 100644 --- a/apps/electron/src/renderer/components/app-shell/ChatDisplay.tsx +++ b/apps/electron/src/renderer/components/app-shell/ChatDisplay.tsx @@ -1699,8 +1699,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 bb13817e4..3df5e1eca 100644 --- a/apps/electron/src/renderer/lib/local-storage.ts +++ b/apps/electron/src/renderer/lib/local-storage.ts @@ -56,6 +56,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 98e4a3edb..2b91297c9 100644 --- a/docs/loop/feature-ledger.md +++ b/docs/loop/feature-ledger.md @@ -32,7 +32,12 @@ log, not the system of record. | slug | title | source | feasibility | status | issue | pr | branch | updated | notes | | --- | --- | --- | --- | --- | --- | --- | --- | --- | --- | -| copy-message | "Copy" (copy-as-Markdown) action on assistant/agent responses | Codex desktop "Copy as Markdown" (openai/codex #2880, #17241) + Claude/ChatGPT desktop per-message copy | frontend-only | pr-open | [#70](https://github.com/modelstudioai/openwork/issues/70) | [#71](https://github.com/modelstudioai/openwork/pull/71) | loop/copy-message | 2026-07-08 | User messages + code blocks were already copyable, but full assistant responses had only a pop-out button — no copy. Extracted `AssistantMessage` component in `ChatDisplay.tsx` (owns `copied` state, mirrors the existing `ErrorMessage` extraction) with a hover copy button next to pop-out; copies raw `message.content` via `navigator.clipboard.writeText`, 2s "copied" check state, `toast.copyFailed` on error. **Zero new i18n keys** (reuses `common.copy`/`common.copied`/`toast.copyFailed`). Added a general `seed` hook to the e2e harness (`app.ts`/`runner.ts`) so assertions can pre-seed an on-disk session the embedded `SessionManager` loads on boot (no backend). typecheck:all zero-delta; `bun test` 56-failure set byte-identical to main; renderer build ✅; i18n parity ✅ (1544 keys); eslint 0 errors. CDP assertion (`copy-message.assert.ts`) seeds a user+assistant session, opens it, clicks the copy button, and asserts the exact response markdown reaches the (stubbed) clipboard + the button enters its copied state; **could not run locally** (org egress policy 403s the Electron binary download — same block as prior rounds). | +| 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-count | Live word / character count indicator in the chat composer | Codex desktop / VS Code / Google Docs status bars | frontend-only | pr-open | [#60](https://github.com/modelstudioai/openwork/issues/60) | [#61](https://github.com/modelstudioai/openwork/pull/61) | loop/composer-count | 2026-07-05 | Opened by a prior run. Draft word count + tooltip (words/chars/lines) in composer toolbar. Awaiting review. | +| shortcuts-search | Search box on the Settings → Keyboard Shortcuts page | Claude Code Desktop / VS Code keybinding 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-04 | Opened by a prior run. Awaiting review. | +| recent-commands | Surface recently-used commands in the Command Palette (⌘K) | Claude Code Desktop / VS Code recently-used | frontend-only | pr-open | [#56](https://github.com/modelstudioai/openwork/issues/56) | [#57](https://github.com/modelstudioai/openwork/pull/57) | loop/recent-commands | 2026-07-04 | Opened by a prior run. Awaiting review. | +| thinking-shortcut | Keyboard shortcut (⌘⇧E) to open the composer's thinking menu | Claude Code Desktop effort menu ⌘⇧E | frontend-only | pr-open | [#54](https://github.com/modelstudioai/openwork/issues/54) | [#55](https://github.com/modelstudioai/openwork/pull/55) | loop/thinking-shortcut | 2026-07-03 | Opened by a prior run. Awaiting review. | +| prompt-history | Recall previously-sent prompts in the composer with Up / Down arrows | Claude Code / shell / ChatGPT prompt history | frontend-only | pr-open | [#52](https://github.com/modelstudioai/openwork/issues/52) | [#53](https://github.com/modelstudioai/openwork/pull/53) | loop/prompt-history | 2026-07-03 | Opened by a prior run. 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-08 | **Merged** into `main` (2026-07-06). Renderer-only pref (localStorage) applied app-wide via `` + `data-reduce-motion` on `` + global CSS guard. Off ⇒ `reducedMotion="user"` (still honors OS). New `ReduceMotionProvider` in `main.tsx`; toggle in Appearance→Interface; 2 new i18n keys ×7 locales. | | 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. | 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 5f194ecd1..98b08b3ab 100644 --- a/packages/shared/src/i18n/locales/de.json +++ b/packages/shared/src/i18n/locales/de.json @@ -779,6 +779,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 a3da8409b..f680fa79d 100644 --- a/packages/shared/src/i18n/locales/en.json +++ b/packages/shared/src/i18n/locales/en.json @@ -779,6 +779,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 1b41a0984..4d58f2b56 100644 --- a/packages/shared/src/i18n/locales/es.json +++ b/packages/shared/src/i18n/locales/es.json @@ -779,6 +779,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 6ff355e60..fc9d3fb06 100644 --- a/packages/shared/src/i18n/locales/hu.json +++ b/packages/shared/src/i18n/locales/hu.json @@ -779,6 +779,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 5b9e95dff..25881b065 100644 --- a/packages/shared/src/i18n/locales/ja.json +++ b/packages/shared/src/i18n/locales/ja.json @@ -779,6 +779,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 aa6435f76..2e305c35f 100644 --- a/packages/shared/src/i18n/locales/pl.json +++ b/packages/shared/src/i18n/locales/pl.json @@ -779,6 +779,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 b7e77ee3c..db20070f2 100644 --- a/packages/shared/src/i18n/locales/zh-Hans.json +++ b/packages/shared/src/i18n/locales/zh-Hans.json @@ -779,6 +779,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,放好后刷新。",