From 8b7448bcf09fa4609bdc800f707a4cc709029e83 Mon Sep 17 00:00:00 2001 From: Bohdan Triapitsyn Date: Wed, 8 Jul 2026 14:46:44 +0300 Subject: [PATCH] feat: add code block line wrap toggle Adds a chat code block wrap toggle in markdown code block headers Persists and restores the setting across desktop/web settings Adds localized labels and OpenChamber search entry for the new option --- .../components/chat/MarkdownRendererImpl.tsx | 20 ++++++- .../src/components/chat/markdown/decorate.ts | 57 +++++++++++++++++-- .../sections/openchamber/OpenChamberPage.tsx | 2 +- .../openchamber/OpenChamberVisualSettings.tsx | 31 +++++++++- packages/ui/src/lib/api/types.ts | 1 + packages/ui/src/lib/desktop.ts | 1 + .../ui/src/lib/i18n/messages/en.settings.ts | 2 + packages/ui/src/lib/i18n/messages/en.ts | 2 + .../ui/src/lib/i18n/messages/es.settings.ts | 2 + packages/ui/src/lib/i18n/messages/es.ts | 2 + .../ui/src/lib/i18n/messages/fr.settings.ts | 2 + packages/ui/src/lib/i18n/messages/fr.ts | 2 + .../ui/src/lib/i18n/messages/ja.settings.ts | 2 + packages/ui/src/lib/i18n/messages/ja.ts | 2 + .../ui/src/lib/i18n/messages/ko.settings.ts | 2 + packages/ui/src/lib/i18n/messages/ko.ts | 2 + .../ui/src/lib/i18n/messages/pl.settings.ts | 2 + packages/ui/src/lib/i18n/messages/pl.ts | 2 + .../src/lib/i18n/messages/pt-BR.settings.ts | 2 + packages/ui/src/lib/i18n/messages/pt-BR.ts | 2 + .../ui/src/lib/i18n/messages/uk.settings.ts | 2 + packages/ui/src/lib/i18n/messages/uk.ts | 2 + .../src/lib/i18n/messages/zh-CN.settings.ts | 2 + packages/ui/src/lib/i18n/messages/zh-CN.ts | 2 + .../src/lib/i18n/messages/zh-TW.settings.ts | 2 + packages/ui/src/lib/i18n/messages/zh-TW.ts | 2 + packages/ui/src/lib/persistence.ts | 6 ++ packages/ui/src/lib/settings/search.ts | 6 ++ packages/ui/src/stores/useUIStore.ts | 7 +++ packages/vscode/CHANGELOG.md | 4 ++ 30 files changed, 166 insertions(+), 9 deletions(-) diff --git a/packages/ui/src/components/chat/MarkdownRendererImpl.tsx b/packages/ui/src/components/chat/MarkdownRendererImpl.tsx index 6e71125c..fe8f757f 100644 --- a/packages/ui/src/components/chat/MarkdownRendererImpl.tsx +++ b/packages/ui/src/components/chat/MarkdownRendererImpl.tsx @@ -23,6 +23,7 @@ import { renderMarkdownBlocks, renderMarkdownSync } from './markdown/markdownCor import { ensureMarkdownShikiTheme, getMarkdownSyntaxVars } from './markdown/markdownTheme'; import { attachMarkdownInteractions, + applyMarkdownCodeBlockWrapState, decorateMarkdown, type DecorateContext, type DecorateLabels, @@ -863,6 +864,8 @@ const useDecorateContext = ( const labels: DecorateLabels = React.useMemo(() => ({ copy: 'Copy code', copied: 'Copied', + enableCodeWrap: t('markdownRenderer.code.actions.enableWrapTitle'), + disableCodeWrap: t('markdownRenderer.code.actions.disableWrapTitle'), copyTable: t('markdownRenderer.table.actions.copyTitle'), downloadTable: t('markdownRenderer.table.actions.downloadTitle'), copyDiagram: t('markdownRenderer.mermaid.actions.copySourceTitle'), @@ -871,6 +874,12 @@ const useDecorateContext = ( previewTitle: t('terminalView.preview.openTitle'), }), [t]); + const codeBlockLineWrap = useUIStore((state) => state.codeBlockLineWrap); + const setCodeBlockLineWrap = useUIStore((state) => state.setCodeBlockLineWrap); + const toggleCodeBlockLineWrap = React.useCallback(() => { + setCodeBlockLineWrap(!useUIStore.getState().codeBlockLineWrap); + }, [setCodeBlockLineWrap]); + return React.useMemo(() => { const colors = mermaidColorsFromTheme(currentTheme); const mode = useUIStore.getState().mermaidRenderingMode; @@ -884,8 +893,8 @@ const useDecorateContext = ( return {}; } }); - return { labels, renderMermaid, onPreviewLoopback }; - }, [currentTheme, labels, onPreviewLoopback]); + return { labels, codeBlockLineWrap, onToggleCodeBlockLineWrap: toggleCodeBlockLineWrap, renderMermaid, onPreviewLoopback }; + }, [currentTheme, labels, codeBlockLineWrap, toggleCodeBlockLineWrap, onPreviewLoopback]); }; // Runs the async render pipeline into the container and keeps a stable @@ -994,6 +1003,13 @@ const useMorphdomMarkdown = ({ target.style.setProperty(key, value); } }, [containerRef, syntaxVars]); + + React.useEffect(() => { + const container = containerRef.current; + const target = container?.querySelector('[data-markdown-content]') ?? container; + if (!target) return; + applyMarkdownCodeBlockWrapState(target, ctx.codeBlockLineWrap, ctx.labels); + }, [containerRef, ctx.codeBlockLineWrap, ctx.labels]); }; const markdownContentClassName = (variant: MarkdownVariant): string => diff --git a/packages/ui/src/components/chat/markdown/decorate.ts b/packages/ui/src/components/chat/markdown/decorate.ts index f933b361..862938ef 100644 --- a/packages/ui/src/components/chat/markdown/decorate.ts +++ b/packages/ui/src/components/chat/markdown/decorate.ts @@ -11,6 +11,8 @@ export type MermaidRender = { svg?: string; ascii?: string }; export type DecorateLabels = { copy: string; copied: string; + enableCodeWrap: string; + disableCodeWrap: string; copyTable: string; downloadTable: string; copyDiagram: string; @@ -21,6 +23,8 @@ export type DecorateLabels = { export type DecorateContext = { labels: DecorateLabels; + codeBlockLineWrap: boolean; + onToggleCodeBlockLineWrap?: () => void; // Renders a mermaid block source to svg/ascii using current theme colors. renderMermaid: (source: string) => MermaidRender; onPreviewLoopback?: (url: string) => void; @@ -36,6 +40,7 @@ const ICONS = { copy: spriteIcon('file-copy'), check: spriteIcon('check'), download: spriteIcon('download'), + textWrap: spriteIcon('text-wrap'), } as const; const ICON_BTN_CLASS = @@ -56,6 +61,37 @@ const makeIconButton = (icon: keyof typeof ICONS, title: string, slot: string): return button; }; +const applyCodeBlockWrapState = (wrapper: HTMLElement, enabled: boolean, labels: DecorateLabels): void => { + const body = wrapper.querySelector('[data-md-code-body]'); + const pre = wrapper.querySelector('pre'); + const code = wrapper.querySelector('pre code'); + const wrapButton = wrapper.querySelector('[data-md-action="toggle-code-wrap"]'); + wrapper.setAttribute('data-code-wrap', enabled ? 'true' : 'false'); + body?.classList.toggle('overflow-x-auto', !enabled); + body?.classList.toggle('overflow-x-hidden', enabled); + pre?.classList.toggle('whitespace-pre-wrap', enabled); + pre?.classList.toggle('break-words', enabled); + code?.classList.toggle('whitespace-pre-wrap', enabled); + code?.classList.toggle('break-words', enabled); + if (wrapButton) { + const title = enabled ? labels.disableCodeWrap : labels.enableCodeWrap; + wrapButton.setAttribute('title', title); + wrapButton.setAttribute('aria-label', title); + wrapButton.classList.toggle('text-foreground', enabled); + wrapButton.classList.toggle('opacity-100', enabled); + wrapButton.classList.toggle('text-muted-foreground', !enabled); + wrapButton.classList.toggle('opacity-65', !enabled); + wrapButton.setAttribute('aria-pressed', enabled ? 'true' : 'false'); + } +}; + +export const applyMarkdownCodeBlockWrapState = (root: HTMLElement, enabled: boolean, labels: DecorateLabels): void => { + const wrappers = root.querySelectorAll('[data-component="markdown-code"]'); + for (const wrapper of Array.from(wrappers)) { + applyCodeBlockWrapState(wrapper, enabled, labels); + } +}; + const flashCopied = (button: HTMLButtonElement, copiedTitle: string, restore: keyof typeof ICONS, restoreTitle: string): void => { setHtml(button, ICONS.check); button.setAttribute('title', copiedTitle); @@ -78,7 +114,7 @@ const decorateInlineCode = (root: HTMLElement): void => { } }; -const decorateCodeBlocks = (root: HTMLElement, labels: DecorateLabels): void => { +const decorateCodeBlocks = (root: HTMLElement, ctx: DecorateContext): void => { const blocks = root.querySelectorAll('pre'); for (const pre of Array.from(blocks)) { // Skip mermaid placeholders (handled separately). @@ -104,11 +140,17 @@ const decorateCodeBlocks = (root: HTMLElement, labels: DecorateLabels): void => const langLabel = document.createElement('span'); langLabel.className = 'font-mono text-[13px] text-muted-foreground'; langLabel.textContent = language; - const copyBtn = makeIconButton('copy', labels.copy, 'copy-code'); + const copyBtn = makeIconButton('copy', ctx.labels.copy, 'copy-code'); + const wrapBtn = makeIconButton('textWrap', ctx.codeBlockLineWrap ? ctx.labels.disableCodeWrap : ctx.labels.enableCodeWrap, 'toggle-code-wrap'); header.appendChild(langLabel); - header.appendChild(copyBtn); + const actions = document.createElement('div'); + actions.className = 'flex items-center gap-1'; + actions.appendChild(wrapBtn); + actions.appendChild(copyBtn); + header.appendChild(actions); const body = document.createElement('div'); + body.setAttribute('data-md-code-body', ''); body.className = 'px-3 py-2.5 overflow-x-auto'; parent.replaceChild(wrapper, pre); @@ -117,6 +159,7 @@ const decorateCodeBlocks = (root: HTMLElement, labels: DecorateLabels): void => body.appendChild(pre); wrapper.appendChild(header); wrapper.appendChild(body); + applyCodeBlockWrapState(wrapper, ctx.codeBlockLineWrap, ctx.labels); } }; @@ -332,7 +375,7 @@ const decorateLinks = (root: HTMLElement, ctx: DecorateContext): void => { export const decorateMarkdown = (root: HTMLElement, ctx: DecorateContext): void => { decorateInlineCode(root); decorateMermaid(root, ctx); - decorateCodeBlocks(root, ctx.labels); + decorateCodeBlocks(root, ctx); decorateTables(root, ctx.labels); decorateLinks(root, ctx); }; @@ -387,6 +430,12 @@ export const attachMarkdownInteractions = ( return; } + if (action === 'toggle-code-wrap') { + event.preventDefault(); + ctx.onToggleCodeBlockLineWrap?.(); + return; + } + // Toggle table menus if (action === 'table-copy-toggle' || action === 'table-download-toggle') { event.preventDefault(); diff --git a/packages/ui/src/components/sections/openchamber/OpenChamberPage.tsx b/packages/ui/src/components/sections/openchamber/OpenChamberPage.tsx index 8bfee71f..37404f5c 100644 --- a/packages/ui/src/components/sections/openchamber/OpenChamberPage.tsx +++ b/packages/ui/src/components/sections/openchamber/OpenChamberPage.tsx @@ -144,7 +144,7 @@ const VisualSectionContent: React.FC = () => { // Chat section: User message rendering, Diff layout, Mobile status bar, Show reasoning traces, Follow-up behavior, Persist draft const ChatSectionContent: React.FC = () => { - return ; + return ; }; // Sessions section: Default model & agent, Session retention diff --git a/packages/ui/src/components/sections/openchamber/OpenChamberVisualSettings.tsx b/packages/ui/src/components/sections/openchamber/OpenChamberVisualSettings.tsx index e75ddeda..2b39c81c 100644 --- a/packages/ui/src/components/sections/openchamber/OpenChamberVisualSettings.tsx +++ b/packages/ui/src/components/sections/openchamber/OpenChamberVisualSettings.tsx @@ -245,7 +245,7 @@ const normalizeUserMessageRenderingMode = (mode: unknown): 'markdown' | 'plain' return mode === 'markdown' ? 'markdown' : 'plain'; }; -type VisibleSetting = 'sessionAssist' | 'theme' | 'pwaInstallName' | 'pwaOrientation' | 'mobileKeyboardMode' | 'timeFormat' | 'weekStart' | 'fontSize' | 'terminalFontSize' | 'spacing' | 'inputBarOffset' | 'mermaidRendering' | 'userMessageRendering' | 'chatRenderMode' | 'messageTransport' | 'activityRenderMode' | 'collapsibleUserMessages' | 'stickyUserHeader' | 'wideChatLayout' | 'splitAssistantMessageActions' | 'diffLayout' | 'mobileStatusBar' | 'dotfiles' | 'fileViewerPreview' | 'reasoning' | 'showToolFileIcons' | 'showTurnChangedFiles' | 'expandedTools' | 'followUpBehavior' | 'terminalQuickKeys' | 'fileEditorKeymap' | 'persistDraft' | 'inputSpellcheck' | 'reportUsage' | 'expandedEditorToolbar'; +type VisibleSetting = 'sessionAssist' | 'theme' | 'pwaInstallName' | 'pwaOrientation' | 'mobileKeyboardMode' | 'timeFormat' | 'weekStart' | 'fontSize' | 'terminalFontSize' | 'spacing' | 'inputBarOffset' | 'mermaidRendering' | 'userMessageRendering' | 'chatRenderMode' | 'messageTransport' | 'activityRenderMode' | 'collapsibleUserMessages' | 'stickyUserHeader' | 'wideChatLayout' | 'codeBlockLineWrap' | 'splitAssistantMessageActions' | 'diffLayout' | 'mobileStatusBar' | 'dotfiles' | 'fileViewerPreview' | 'reasoning' | 'showToolFileIcons' | 'showTurnChangedFiles' | 'expandedTools' | 'followUpBehavior' | 'terminalQuickKeys' | 'fileEditorKeymap' | 'persistDraft' | 'inputSpellcheck' | 'reportUsage' | 'expandedEditorToolbar'; interface OpenChamberVisualSettingsProps { /** Which settings to show. If undefined, shows all. */ @@ -279,6 +279,8 @@ export const OpenChamberVisualSettings: React.FC const setExpandedEditorToolbar = useUIStore(state => state.setExpandedEditorToolbar); const wideChatLayoutEnabled = useUIStore(state => state.wideChatLayoutEnabled); const setWideChatLayoutEnabled = useUIStore(state => state.setWideChatLayoutEnabled); + const codeBlockLineWrap = useUIStore(state => state.codeBlockLineWrap); + const setCodeBlockLineWrap = useUIStore(state => state.setCodeBlockLineWrap); const chatRenderMode = useUIStore(state => state.chatRenderMode); const setChatRenderMode = useUIStore(state => state.setChatRenderMode); const activityRenderMode = useUIStore(state => state.activityRenderMode); @@ -561,6 +563,7 @@ export const OpenChamberVisualSettings: React.FC || shouldShow('collapsibleUserMessages') || shouldShow('stickyUserHeader') || shouldShow('wideChatLayout') + || shouldShow('codeBlockLineWrap') || shouldShow('splitAssistantMessageActions') || shouldShow('diffLayout') || shouldShow('dotfiles') @@ -1779,7 +1782,7 @@ export const OpenChamberVisualSettings: React.FC )} - {(shouldShow('sessionAssist') || shouldShow('collapsibleUserMessages') || shouldShow('stickyUserHeader') || shouldShow('wideChatLayout') || shouldShow('splitAssistantMessageActions') || shouldShow('dotfiles') || shouldShow('fileViewerPreview') || shouldShow('persistDraft') || shouldShow('showToolFileIcons') || shouldShow('showTurnChangedFiles') || (!isMobile && shouldShow('inputSpellcheck')) || shouldShow('reasoning')) && ( + {(shouldShow('sessionAssist') || shouldShow('collapsibleUserMessages') || shouldShow('stickyUserHeader') || shouldShow('wideChatLayout') || shouldShow('codeBlockLineWrap') || shouldShow('splitAssistantMessageActions') || shouldShow('dotfiles') || shouldShow('fileViewerPreview') || shouldShow('persistDraft') || shouldShow('showToolFileIcons') || shouldShow('showTurnChangedFiles') || (!isMobile && shouldShow('inputSpellcheck')) || shouldShow('reasoning')) && (
{shouldShow('sessionAssist') && ( <> @@ -1980,6 +1983,30 @@ export const OpenChamberVisualSettings: React.FC )} + {shouldShow('codeBlockLineWrap') && ( +
setCodeBlockLineWrap(!codeBlockLineWrap)} + onKeyDown={(event) => { + if (event.key === ' ' || event.key === 'Enter') { + event.preventDefault(); + setCodeBlockLineWrap(!codeBlockLineWrap); + } + }} + > + + {t('settings.openchamber.visual.field.codeBlockLineWrap')} +
+ )} + {shouldShow('showToolFileIcons') && (
= { "sortableTabsStrip.aria.tabs": "Pestañas", "openChamberLogo.aria.logo": "Logo de OpenChamber", "markdownRenderer.table.actions.copyTitle": "Copiar tabla", + "markdownRenderer.code.actions.enableWrapTitle": "Activar ajuste de línea", + "markdownRenderer.code.actions.disableWrapTitle": "Desactivar ajuste de línea", "markdownRenderer.table.actions.downloadTitle": "Descargar tabla", "markdownRenderer.table.toast.downloadedAsFormat": "Tabla descargada como {format}", "markdownRenderer.mermaid.actions.copyTitle": "Copiar", diff --git a/packages/ui/src/lib/i18n/messages/fr.settings.ts b/packages/ui/src/lib/i18n/messages/fr.settings.ts index 65d29ab1..fb4d70da 100644 --- a/packages/ui/src/lib/i18n/messages/fr.settings.ts +++ b/packages/ui/src/lib/i18n/messages/fr.settings.ts @@ -1644,6 +1644,8 @@ export const settingsDict = { 'settings.openchamber.visual.field.showSplitAssistantMessageActions': 'Actions intégrées de l\'assistant', 'settings.openchamber.visual.field.showSplitAssistantMessageActionsTooltip': 'Afficher Copier la réponse, Enregistrer en image et Lire à haute voix sur les blocs de texte de l\'assistant qui apparaissent avant les appels d\'outil suivants dans la même réponse.', 'settings.openchamber.visual.field.showToolFileIconsAria': 'Afficher les icônes des fichiers d\'outils', + 'settings.openchamber.visual.field.codeBlockLineWrapAria': 'Renvoyer à la ligne les blocs de code', + 'settings.openchamber.visual.field.codeBlockLineWrap': 'Renvoyer à la ligne les blocs de code', 'settings.openchamber.visual.field.showToolFileIcons': 'Afficher les icônes des fichiers d\'outils', 'settings.openchamber.visual.field.showDotfilesAria': 'Afficher les fichiers de points', 'settings.openchamber.visual.field.showDotfiles': 'Afficher les fichiers cachés', diff --git a/packages/ui/src/lib/i18n/messages/fr.ts b/packages/ui/src/lib/i18n/messages/fr.ts index ccf31fa8..7f0f1a61 100644 --- a/packages/ui/src/lib/i18n/messages/fr.ts +++ b/packages/ui/src/lib/i18n/messages/fr.ts @@ -2440,6 +2440,8 @@ export const dict = { 'sortableTabsStrip.aria.tabs': 'Onglets', 'openChamberLogo.aria.logo': 'Logo OpenChamber', 'markdownRenderer.table.actions.copyTitle': 'Copier le tableau', + 'markdownRenderer.code.actions.enableWrapTitle': 'Activer le retour à la ligne', + 'markdownRenderer.code.actions.disableWrapTitle': 'Désactiver le retour à la ligne', 'markdownRenderer.table.actions.downloadTitle': 'Télécharger le tableau', 'markdownRenderer.table.toast.downloadedAsFormat': 'Table téléchargée sous le nom {format}', 'markdownRenderer.mermaid.actions.copyTitle': 'Copie', diff --git a/packages/ui/src/lib/i18n/messages/ja.settings.ts b/packages/ui/src/lib/i18n/messages/ja.settings.ts index 04cdefd9..1559ad4a 100644 --- a/packages/ui/src/lib/i18n/messages/ja.settings.ts +++ b/packages/ui/src/lib/i18n/messages/ja.settings.ts @@ -1741,6 +1741,8 @@ export const settingsDict = { 'settings.openchamber.visual.field.showSplitAssistantMessageActions': 'インラインアシスタントアクション', 'settings.openchamber.visual.field.showSplitAssistantMessageActionsTooltip': '同じ応答内の後のツール呼び出しの前に表示されるアシスタントテキストブロックに、回答コピー、画像保存、読み上げを表示します。', 'settings.openchamber.visual.field.showToolFileIconsAria': 'ツールファイルアイコンを表示', + 'settings.openchamber.visual.field.codeBlockLineWrapAria': 'コードブロックの行を折り返す', + 'settings.openchamber.visual.field.codeBlockLineWrap': 'コードブロックの行を折り返す', 'settings.openchamber.visual.field.showToolFileIcons': 'ツールファイルアイコンを表示', 'settings.openchamber.visual.field.showTurnChangedFilesAria': '完了したターンの変更ファイルを表示', 'settings.openchamber.visual.field.showTurnChangedFiles': '完了したターンの変更ファイルを表示', diff --git a/packages/ui/src/lib/i18n/messages/ja.ts b/packages/ui/src/lib/i18n/messages/ja.ts index dfba9e49..33e15536 100644 --- a/packages/ui/src/lib/i18n/messages/ja.ts +++ b/packages/ui/src/lib/i18n/messages/ja.ts @@ -2666,6 +2666,8 @@ export const dict: Record = { 'sortableTabsStrip.aria.tabs': 'タブ', 'openChamberLogo.aria.logo': 'OpenChamberロゴ', 'markdownRenderer.table.actions.copyTitle': 'テーブルをコピー', + 'markdownRenderer.code.actions.enableWrapTitle': '行折り返しを有効にする', + 'markdownRenderer.code.actions.disableWrapTitle': '行折り返しを無効にする', 'markdownRenderer.table.actions.downloadTitle': 'テーブルをダウンロード', 'markdownRenderer.table.toast.downloadedAsFormat': 'テーブルを{format}としてダウンロードしました', 'markdownRenderer.mermaid.actions.copyTitle': 'コピー', diff --git a/packages/ui/src/lib/i18n/messages/ko.settings.ts b/packages/ui/src/lib/i18n/messages/ko.settings.ts index 2e4b00cf..a9c1ecab 100644 --- a/packages/ui/src/lib/i18n/messages/ko.settings.ts +++ b/packages/ui/src/lib/i18n/messages/ko.settings.ts @@ -1708,6 +1708,8 @@ export const settingsDict = { 'settings.openchamber.visual.field.showSplitAssistantMessageActions': '인라인 어시스턴트 작업', 'settings.openchamber.visual.field.showSplitAssistantMessageActionsTooltip': '같은 응답에서 이후 도구 호출 앞에 나타나는 어시스턴트 텍스트 블록에 답변 복사, 이미지로 저장, 소리 내어 읽기 작업을 표시합니다.', 'settings.openchamber.visual.field.showToolFileIconsAria': '도구 파일 아이콘 표시', + 'settings.openchamber.visual.field.codeBlockLineWrapAria': '코드 블록 줄 바꿈', + 'settings.openchamber.visual.field.codeBlockLineWrap': '코드 블록 줄 바꿈', 'settings.openchamber.visual.field.showToolFileIcons': '도구 파일 아이콘 표시', 'settings.openchamber.visual.field.showTurnChangedFilesAria': '완료된 턴의 변경 파일 표시', 'settings.openchamber.visual.field.showTurnChangedFiles': '완료된 턴의 변경 파일 표시', diff --git a/packages/ui/src/lib/i18n/messages/ko.ts b/packages/ui/src/lib/i18n/messages/ko.ts index 06d84b75..9ed0362b 100644 --- a/packages/ui/src/lib/i18n/messages/ko.ts +++ b/packages/ui/src/lib/i18n/messages/ko.ts @@ -2670,6 +2670,8 @@ export const dict: Record = { 'sortableTabsStrip.aria.tabs': '탭', 'openChamberLogo.aria.logo': 'OpenChamber 로고', 'markdownRenderer.table.actions.copyTitle': '표 복사', + 'markdownRenderer.code.actions.enableWrapTitle': '줄 바꿈 켜기', + 'markdownRenderer.code.actions.disableWrapTitle': '줄 바꿈 끄기', 'markdownRenderer.table.actions.downloadTitle': '표 다운로드', 'markdownRenderer.table.toast.downloadedAsFormat': '{format} 형식으로 다운로드됨', 'markdownRenderer.mermaid.actions.copyTitle': '복사', diff --git a/packages/ui/src/lib/i18n/messages/pl.settings.ts b/packages/ui/src/lib/i18n/messages/pl.settings.ts index bf2a2145..cb8bb21c 100644 --- a/packages/ui/src/lib/i18n/messages/pl.settings.ts +++ b/packages/ui/src/lib/i18n/messages/pl.settings.ts @@ -989,6 +989,8 @@ export const settingsDict = { 'settings.openchamber.visual.field.showSplitAssistantMessageActionsTooltip': 'Pokaż Kopiuj odpowiedź, Zapisz jako obraz i Odczytaj na głos w blokach tekstu asystenta, które pojawiają się przed późniejszymi wywołaniami narzędzi w tej samej odpowiedzi.', 'settings.openchamber.visual.field.showToolFileIcons': 'Pokaż ikony plików narzędzi', 'settings.openchamber.visual.field.showToolFileIconsAria': 'Pokaż ikony plików narzędzi', + 'settings.openchamber.visual.field.codeBlockLineWrapAria': 'Zawijaj wiersze bloków kodu', + 'settings.openchamber.visual.field.codeBlockLineWrap': 'Zawijaj wiersze bloków kodu', 'settings.openchamber.visual.field.showTurnChangedFiles': 'Pokaż zmienione pliki w ukończonych turach', 'settings.openchamber.visual.field.showTurnChangedFilesAria': 'Pokaż zmienione pliki w ukończonych turach', 'settings.openchamber.visual.field.spacingDensity': 'Gęstość odstępów', diff --git a/packages/ui/src/lib/i18n/messages/pl.ts b/packages/ui/src/lib/i18n/messages/pl.ts index b1f00c03..4a9e9099 100644 --- a/packages/ui/src/lib/i18n/messages/pl.ts +++ b/packages/ui/src/lib/i18n/messages/pl.ts @@ -2161,6 +2161,8 @@ export const dict: Record = { 'markdownRenderer.mermaid.actions.downloadSvgTitle': 'Pobierz SVG', 'markdownRenderer.mermaid.toast.downloadFailed': 'Nie udało się pobrać diagramu', 'markdownRenderer.table.actions.copyTitle': 'Kopiuj tabelę', + 'markdownRenderer.code.actions.enableWrapTitle': 'Włącz zawijanie wierszy', + 'markdownRenderer.code.actions.disableWrapTitle': 'Wyłącz zawijanie wierszy', 'markdownRenderer.table.actions.downloadTitle': 'Pobierz tabelę', 'markdownRenderer.table.toast.downloadedAsFormat': 'Tabela została pobrana jako {format}', 'mcpDropdown.actions.openAria': 'Serwery MCP', diff --git a/packages/ui/src/lib/i18n/messages/pt-BR.settings.ts b/packages/ui/src/lib/i18n/messages/pt-BR.settings.ts index b696fd14..8b225d66 100644 --- a/packages/ui/src/lib/i18n/messages/pt-BR.settings.ts +++ b/packages/ui/src/lib/i18n/messages/pt-BR.settings.ts @@ -1708,6 +1708,8 @@ export const settingsDict = { "settings.openchamber.visual.field.showSplitAssistantMessageActions": "Ações inline do assistente", "settings.openchamber.visual.field.showSplitAssistantMessageActionsTooltip": "Mostra Copiar resposta, Salvar como imagem e Ler em voz alta em blocos de texto do assistente que aparecem antes de chamadas de ferramenta posteriores na mesma resposta.", "settings.openchamber.visual.field.showToolFileIconsAria": "Mostrar ícones de arquivos de ferramentas", + "settings.openchamber.visual.field.codeBlockLineWrapAria": "Quebrar linhas de blocos de código", + "settings.openchamber.visual.field.codeBlockLineWrap": "Quebrar linhas de blocos de código", "settings.openchamber.visual.field.showToolFileIcons": "Mostrar ícones de arquivos de ferramentas", "settings.openchamber.visual.field.showTurnChangedFilesAria": "Mostrar arquivos alterados em turnos concluídos", "settings.openchamber.visual.field.showTurnChangedFiles": "Mostrar arquivos alterados em turnos concluídos", diff --git a/packages/ui/src/lib/i18n/messages/pt-BR.ts b/packages/ui/src/lib/i18n/messages/pt-BR.ts index fc7dec50..43978db4 100644 --- a/packages/ui/src/lib/i18n/messages/pt-BR.ts +++ b/packages/ui/src/lib/i18n/messages/pt-BR.ts @@ -2636,6 +2636,8 @@ export const dict: Record = { "sortableTabsStrip.aria.tabs": "Abas", "openChamberLogo.aria.logo": "Logo do OpenChamber", "markdownRenderer.table.actions.copyTitle": "Copiar tabela", + "markdownRenderer.code.actions.enableWrapTitle": "Ativar quebra de linha", + "markdownRenderer.code.actions.disableWrapTitle": "Desativar quebra de linha", "markdownRenderer.table.actions.downloadTitle": "Baixar tabela", "markdownRenderer.table.toast.downloadedAsFormat": "Tabela baixada como {format}", "markdownRenderer.mermaid.actions.copyTitle": "Copiar", diff --git a/packages/ui/src/lib/i18n/messages/uk.settings.ts b/packages/ui/src/lib/i18n/messages/uk.settings.ts index f245ef5d..b8cac604 100644 --- a/packages/ui/src/lib/i18n/messages/uk.settings.ts +++ b/packages/ui/src/lib/i18n/messages/uk.settings.ts @@ -1708,6 +1708,8 @@ export const settingsDict = { "settings.openchamber.visual.field.showSplitAssistantMessageActions": "Вбудовані дії асистента", "settings.openchamber.visual.field.showSplitAssistantMessageActionsTooltip": "Показує «Скопіювати відповідь», «Зберегти як зображення» та озвучення для текстових блоків асистента, які з’являються перед пізнішими викликами інструментів у тій самій відповіді.", "settings.openchamber.visual.field.showToolFileIconsAria": "Показати значки файлів інструментів", + "settings.openchamber.visual.field.codeBlockLineWrapAria": "Переносити рядки блоків коду", + "settings.openchamber.visual.field.codeBlockLineWrap": "Переносити рядки блоків коду", "settings.openchamber.visual.field.showToolFileIcons": "Показати значки файлів інструментів", "settings.openchamber.visual.field.showTurnChangedFilesAria": "Показати змінені файли для завершених ходів", "settings.openchamber.visual.field.showTurnChangedFiles": "Показати змінені файли для завершених ходів", diff --git a/packages/ui/src/lib/i18n/messages/uk.ts b/packages/ui/src/lib/i18n/messages/uk.ts index 563ba33e..27218468 100644 --- a/packages/ui/src/lib/i18n/messages/uk.ts +++ b/packages/ui/src/lib/i18n/messages/uk.ts @@ -2636,6 +2636,8 @@ export const dict: Record = { "sortableTabsStrip.aria.tabs": "Вкладки", "openChamberLogo.aria.logo": "Логотип OpenChamber", "markdownRenderer.table.actions.copyTitle": "Скопіювати таблицю", + "markdownRenderer.code.actions.enableWrapTitle": "Увімкнути перенесення рядків", + "markdownRenderer.code.actions.disableWrapTitle": "Вимкнути перенесення рядків", "markdownRenderer.table.actions.downloadTitle": "Завантажити таблицю", "markdownRenderer.table.toast.downloadedAsFormat": "Таблицю завантажено як {format}", "markdownRenderer.mermaid.actions.copyTitle": "Копіювати", diff --git a/packages/ui/src/lib/i18n/messages/zh-CN.settings.ts b/packages/ui/src/lib/i18n/messages/zh-CN.settings.ts index 52b65166..3216f4b2 100644 --- a/packages/ui/src/lib/i18n/messages/zh-CN.settings.ts +++ b/packages/ui/src/lib/i18n/messages/zh-CN.settings.ts @@ -1708,6 +1708,8 @@ export const settingsDict = { 'settings.openchamber.visual.field.showSplitAssistantMessageActions': '内联助手操作', 'settings.openchamber.visual.field.showSplitAssistantMessageActionsTooltip': '在同一条回复中,如果助手文本块后面还有工具调用,则显示“复制回答”“保存为图片”和“朗读”操作。', 'settings.openchamber.visual.field.showToolFileIconsAria': '显示工具文件图标', + 'settings.openchamber.visual.field.codeBlockLineWrapAria': '代码块自动换行', + 'settings.openchamber.visual.field.codeBlockLineWrap': '代码块自动换行', 'settings.openchamber.visual.field.showToolFileIcons': '显示工具文件图标', 'settings.openchamber.visual.field.showTurnChangedFilesAria': '显示已完成回合的更改文件', 'settings.openchamber.visual.field.showTurnChangedFiles': '显示已完成回合的更改文件', diff --git a/packages/ui/src/lib/i18n/messages/zh-CN.ts b/packages/ui/src/lib/i18n/messages/zh-CN.ts index 04fd66c4..03c8955a 100644 --- a/packages/ui/src/lib/i18n/messages/zh-CN.ts +++ b/packages/ui/src/lib/i18n/messages/zh-CN.ts @@ -2636,6 +2636,8 @@ export const dict: Record = { 'sortableTabsStrip.aria.tabs': '标签页', 'openChamberLogo.aria.logo': 'OpenChamber 标志', 'markdownRenderer.table.actions.copyTitle': '复制表格', + 'markdownRenderer.code.actions.enableWrapTitle': '启用自动换行', + 'markdownRenderer.code.actions.disableWrapTitle': '禁用自动换行', 'markdownRenderer.table.actions.downloadTitle': '下载表格', 'markdownRenderer.table.toast.downloadedAsFormat': '表格已下载为 {format}', 'markdownRenderer.mermaid.actions.copyTitle': '复制', diff --git a/packages/ui/src/lib/i18n/messages/zh-TW.settings.ts b/packages/ui/src/lib/i18n/messages/zh-TW.settings.ts index 5a781ce3..4a4cd197 100644 --- a/packages/ui/src/lib/i18n/messages/zh-TW.settings.ts +++ b/packages/ui/src/lib/i18n/messages/zh-TW.settings.ts @@ -1624,6 +1624,8 @@ 'settings.openchamber.visual.field.showSplitAssistantMessageActions': '行內助理操作', 'settings.openchamber.visual.field.showSplitAssistantMessageActionsTooltip': '在同一則回覆中,如果助理文字區塊後方還有工具呼叫,則顯示「複製回答」「儲存為圖片」和「朗讀」操作。', 'settings.openchamber.visual.field.showToolFileIconsAria': '顯示工具檔案圖示', + 'settings.openchamber.visual.field.codeBlockLineWrapAria': '程式碼區塊自動換行', + 'settings.openchamber.visual.field.codeBlockLineWrap': '程式碼區塊自動換行', 'settings.openchamber.visual.field.showToolFileIcons': '顯示工具檔案圖示', 'settings.openchamber.visual.field.showTurnChangedFilesAria': '顯示已完成回合的變更檔案', 'settings.openchamber.visual.field.showTurnChangedFiles': '顯示已完成回合的變更檔案', diff --git a/packages/ui/src/lib/i18n/messages/zh-TW.ts b/packages/ui/src/lib/i18n/messages/zh-TW.ts index 32d761ce..067f0490 100644 --- a/packages/ui/src/lib/i18n/messages/zh-TW.ts +++ b/packages/ui/src/lib/i18n/messages/zh-TW.ts @@ -2633,6 +2633,8 @@ export const dict: Record = { 'sortableTabsStrip.aria.tabs': '分頁', 'openChamberLogo.aria.logo': 'OpenChamber 標誌', 'markdownRenderer.table.actions.copyTitle': '複製表格', + 'markdownRenderer.code.actions.enableWrapTitle': '啟用自動換行', + 'markdownRenderer.code.actions.disableWrapTitle': '停用自動換行', 'markdownRenderer.table.actions.downloadTitle': '下載表格', 'markdownRenderer.table.toast.downloadedAsFormat': '表格已下載為 {format}', 'markdownRenderer.mermaid.actions.copyTitle': '複製', diff --git a/packages/ui/src/lib/persistence.ts b/packages/ui/src/lib/persistence.ts index 4f7fec0e..1d760f08 100644 --- a/packages/ui/src/lib/persistence.ts +++ b/packages/ui/src/lib/persistence.ts @@ -507,6 +507,9 @@ const applyDesktopUiPreferences = (settings: DesktopSettings) => { if (typeof settings.showToolFileIcons === 'boolean' && settings.showToolFileIcons !== store.showToolFileIcons) { store.setShowToolFileIcons(settings.showToolFileIcons); } + if (typeof settings.codeBlockLineWrap === 'boolean' && settings.codeBlockLineWrap !== store.codeBlockLineWrap) { + store.setCodeBlockLineWrap(settings.codeBlockLineWrap); + } if (typeof settings.showTurnChangedFiles === 'boolean' && settings.showTurnChangedFiles !== store.showTurnChangedFiles) { store.setShowTurnChangedFiles(settings.showTurnChangedFiles); } @@ -1044,6 +1047,9 @@ const sanitizeWebSettings = (payload: unknown): DesktopSettings | null => { if (typeof candidate.showToolFileIcons === 'boolean') { result.showToolFileIcons = candidate.showToolFileIcons; } + if (typeof candidate.codeBlockLineWrap === 'boolean') { + result.codeBlockLineWrap = candidate.codeBlockLineWrap; + } if (typeof candidate.showTurnChangedFiles === 'boolean') { result.showTurnChangedFiles = candidate.showTurnChangedFiles; } diff --git a/packages/ui/src/lib/settings/search.ts b/packages/ui/src/lib/settings/search.ts index f6db9686..c632bb44 100644 --- a/packages/ui/src/lib/settings/search.ts +++ b/packages/ui/src/lib/settings/search.ts @@ -203,6 +203,12 @@ const SETTINGS_SEARCH_ITEMS: readonly SettingsSearchItem[] = [ titleKey: 'settings.openchamber.visual.field.wideChatLayout', keywords: ['layout', 'wide', 'messages'], }, + { + id: 'chat.code-block-line-wrap', + page: 'chat', + titleKey: 'settings.openchamber.visual.field.codeBlockLineWrap', + keywords: ['code', 'wrap', 'line wrap', 'markdown'], + }, { id: 'chat.inline-assistant-actions', page: 'chat', diff --git a/packages/ui/src/stores/useUIStore.ts b/packages/ui/src/stores/useUIStore.ts index b8f00a43..1983b7be 100644 --- a/packages/ui/src/stores/useUIStore.ts +++ b/packages/ui/src/stores/useUIStore.ts @@ -639,6 +639,7 @@ interface UIStore { showOpenCodeUpdateNotifications: boolean; inputSpellcheckEnabled: boolean; wideChatLayoutEnabled: boolean; + codeBlockLineWrap: boolean; showToolFileIcons: boolean; showTurnChangedFiles: boolean; showExpandedBashTools: boolean; @@ -787,6 +788,7 @@ interface UIStore { setShowOpenCodeUpdateNotifications: (value: boolean) => void; setInputSpellcheckEnabled: (value: boolean) => void; setWideChatLayoutEnabled: (value: boolean) => void; + setCodeBlockLineWrap: (value: boolean) => void; setShowToolFileIcons: (value: boolean) => void; setShowTurnChangedFiles: (value: boolean) => void; setShowExpandedBashTools: (value: boolean) => void; @@ -929,6 +931,7 @@ export const useUIStore = create()( showOpenCodeUpdateNotifications: true, inputSpellcheckEnabled: false, wideChatLayoutEnabled: false, + codeBlockLineWrap: true, showToolFileIcons: true, showTurnChangedFiles: false, showExpandedBashTools: false, @@ -2043,6 +2046,9 @@ export const useUIStore = create()( setWideChatLayoutEnabled: (value) => { set({ wideChatLayoutEnabled: value }); }, + setCodeBlockLineWrap: (value) => { + set({ codeBlockLineWrap: value }); + }, setShowToolFileIcons: (value) => { set({ showToolFileIcons: value }); }, @@ -2306,6 +2312,7 @@ export const useUIStore = create()( showOpenCodeUpdateNotifications: state.showOpenCodeUpdateNotifications, inputSpellcheckEnabled: state.inputSpellcheckEnabled, wideChatLayoutEnabled: state.wideChatLayoutEnabled, + codeBlockLineWrap: state.codeBlockLineWrap, showToolFileIcons: state.showToolFileIcons, showTurnChangedFiles: state.showTurnChangedFiles, showExpandedBashTools: state.showExpandedBashTools, diff --git a/packages/vscode/CHANGELOG.md b/packages/vscode/CHANGELOG.md index 514d95c2..2eba0b48 100644 --- a/packages/vscode/CHANGELOG.md +++ b/packages/vscode/CHANGELOG.md @@ -1,3 +1,7 @@ +## [Unreleased] + +- Chat: if sending a message times out or loses the connection after OpenCode accepted it, the extension now keeps the sent message instead of rolling it back as failed. + ## [1.14.1] - 2026-07-07 - Chat: the timeline dialog can now load older messages when the current session history has not all been fetched yet.