From ef11ab5b14367dc4f72795137546a873db37ff7c Mon Sep 17 00:00:00 2001 From: Cursor Agent Date: Tue, 4 Aug 2026 11:38:02 +0000 Subject: [PATCH 1/5] feat(ui): attach large text pastes as virtual files Offer to turn sufficiently large plain-text clipboard pastes into pasted-context-N.txt attachments instead of inserting them into the composer, with ask/attach/inline composer settings. Co-authored-by: Serhii Dziupin --- packages/ui/src/components/chat/ChatInput.tsx | 127 +++++++++++++++++- .../__tests__/attachmentCitations.test.ts | 7 + .../components/chat/attachmentCitations.ts | 14 ++ .../components/chat/composer/DOCUMENTATION.md | 14 +- .../composer/__tests__/largeTextPaste.test.ts | 45 +++++++ .../chat/composer/largeTextPaste.ts | 55 ++++++++ .../sections/openchamber/OpenChamberPage.tsx | 1 + .../openchamber/OpenChamberVisualSettings.tsx | 45 ++++++- .../ui/src/lib/i18n/messages/de.settings.ts | 7 + packages/ui/src/lib/i18n/messages/de.ts | 5 + .../ui/src/lib/i18n/messages/en.settings.ts | 7 + packages/ui/src/lib/i18n/messages/en.ts | 5 + .../ui/src/lib/i18n/messages/es.settings.ts | 7 + packages/ui/src/lib/i18n/messages/es.ts | 5 + .../ui/src/lib/i18n/messages/fr.settings.ts | 7 + packages/ui/src/lib/i18n/messages/fr.ts | 5 + .../ui/src/lib/i18n/messages/ja.settings.ts | 7 + packages/ui/src/lib/i18n/messages/ja.ts | 5 + .../ui/src/lib/i18n/messages/ko.settings.ts | 7 + packages/ui/src/lib/i18n/messages/ko.ts | 5 + .../ui/src/lib/i18n/messages/pl.settings.ts | 7 + packages/ui/src/lib/i18n/messages/pl.ts | 5 + .../src/lib/i18n/messages/pt-BR.settings.ts | 7 + packages/ui/src/lib/i18n/messages/pt-BR.ts | 5 + .../ui/src/lib/i18n/messages/uk.settings.ts | 7 + packages/ui/src/lib/i18n/messages/uk.ts | 5 + .../src/lib/i18n/messages/zh-CN.settings.ts | 7 + packages/ui/src/lib/i18n/messages/zh-CN.ts | 5 + .../src/lib/i18n/messages/zh-TW.settings.ts | 7 + packages/ui/src/lib/i18n/messages/zh-TW.ts | 5 + packages/ui/src/lib/settings/search.ts | 9 +- packages/ui/src/stores/useUIStore.ts | 18 +++ packages/ui/src/sync/DOCUMENTATION.md | 2 +- 33 files changed, 458 insertions(+), 11 deletions(-) create mode 100644 packages/ui/src/components/chat/composer/__tests__/largeTextPaste.test.ts create mode 100644 packages/ui/src/components/chat/composer/largeTextPaste.ts diff --git a/packages/ui/src/components/chat/ChatInput.tsx b/packages/ui/src/components/chat/ChatInput.tsx index ba493f55..cc4c6d08 100644 --- a/packages/ui/src/components/chat/ChatInput.tsx +++ b/packages/ui/src/components/chat/ChatInput.tsx @@ -82,7 +82,13 @@ import { eventMatchesShortcut, getEffectiveShortcutCombo, normalizeCombo } from import { assignImageAttachmentFilenames, buildAttachmentCitationText, + nextPastedContextFilename, } from './attachmentCitations'; +import { + createPastedContextFile, + isLargePlainTextPaste, +} from './composer/largeTextPaste'; +import type { LargeTextPasteBehavior } from '@/stores/useUIStore'; import type { FileMentionAutocompleteInputSource } from './fileMentionAutocompleteState'; import { classifyMention, @@ -288,6 +294,8 @@ const ChatInputComponent: React.FC = ({ onOpenSettings, scrollTo const messageRef = React.useRef(message); const currentChatDraftIdentityRef = React.useRef(initialDraftIdentityRef.current); const pendingPastedAttachmentFilenamesRef = React.useRef>(new Set()); + const largeTextPasteToastIdRef = React.useRef(null); + const largeTextPasteOfferIdRef = React.useRef(0); // TODO: port sendMessage to session-actions (complex — creates sessions, handles attachments, etc.) // eslint-disable-next-line @typescript-eslint/no-explicit-any @@ -358,6 +366,7 @@ const ChatInputComponent: React.FC = ({ onOpenSettings, scrollTo const inputBarOffset = useUIStore((state) => state.inputBarOffset); const persistChatDraft = useUIStore((state) => state.persistChatDraft); const inputSpellcheckEnabled = useUIStore((state) => state.inputSpellcheckEnabled); + const largeTextPasteBehavior = useUIStore((state) => state.largeTextPasteBehavior); const isExpandedInput = useUIStore((state) => state.isExpandedInput); const setExpandedInput = useUIStore((state) => state.setExpandedInput); const setTimelineDialogOpen = useUIStore((state) => state.setTimelineDialogOpen); @@ -1723,14 +1732,124 @@ const ChatInputComponent: React.FC = ({ onOpenSettings, scrollTo const imageFiles = Array.from(fileMap.values()); const pastedText = e.clipboardData.getData('text'); + const sessionReady = Boolean(currentSessionId || newSessionDraftOpen); + if (imageFiles.length === 0) { - if (pastedText.includes('@')) { - markFileMentionPasteSuppression(); + const behavior: LargeTextPasteBehavior = largeTextPasteBehavior; + const shouldOfferLargePaste = sessionReady + && inputMode === 'normal' + && behavior !== 'inline' + && isLargePlainTextPaste(pastedText); + + if (!shouldOfferLargePaste) { + if (pastedText.includes('@')) { + markFileMentionPasteSuppression(); + } + return; } + + // Must run synchronously — ComposerEditor does not consume paste. + e.preventDefault(); + + const pasteInline = () => { + if (pastedText.includes('@')) { + markFileMentionPasteSuppression(); + } + insertTextAtSelection( + pastedText, + getFileMentionInputSourceForInsertedText(pastedText), + ); + }; + + const attachAsFile = async () => { + const filename = nextPastedContextFilename([ + ...attachedFiles.map((file) => file.filename), + ...pendingPastedAttachmentFilenamesRef.current, + ]); + const citationText = buildAttachmentCitationText([filename]); + const textarea = composerRef.current; + const selectionStart = textarea?.getSelection().start ?? message.length; + const selectionEnd = textarea?.getSelection().end ?? message.length; + const insertionText = withInlineInsertionBoundaries( + citationText, + message.slice(0, selectionStart), + message.slice(selectionEnd), + ); + + insertTextAtSelection( + insertionText, + getFileMentionInputSourceForInsertedText(insertionText), + ); + + const file = createPastedContextFile(pastedText, filename); + pendingPastedAttachmentFilenamesRef.current.add(filename); + try { + await addAttachedFile(file); + } catch (error) { + console.error('Clipboard text attach failed', error); + toast.error( + error instanceof Error + ? error.message + : t('chat.chatInput.toast.clipboardTextAttachFailed'), + ); + } finally { + pendingPastedAttachmentFilenamesRef.current.delete(filename); + } + }; + + if (behavior === 'attach') { + await attachAsFile(); + return; + } + + const offerId = largeTextPasteOfferIdRef.current + 1; + largeTextPasteOfferIdRef.current = offerId; + + if (largeTextPasteToastIdRef.current !== null) { + // Invalidate first so a synchronous onDismiss from dismiss() + // cannot apply the superseded paste. + toast.dismiss(largeTextPasteToastIdRef.current); + largeTextPasteToastIdRef.current = null; + } + + const resolveLargePaste = (action: 'attach' | 'inline') => { + if (offerId !== largeTextPasteOfferIdRef.current) { + return; + } + // Invalidate this offer so a later onDismiss cannot double-apply. + largeTextPasteOfferIdRef.current += 1; + largeTextPasteToastIdRef.current = null; + if (action === 'attach') { + void attachAsFile(); + return; + } + pasteInline(); + }; + + largeTextPasteToastIdRef.current = toast.info( + t('chat.chatInput.toast.largeTextPaste.title'), + { + description: t('chat.chatInput.toast.largeTextPaste.description'), + duration: Infinity, + action: { + label: t('chat.chatInput.toast.largeTextPaste.attach'), + onClick: () => resolveLargePaste('attach'), + }, + cancel: { + label: t('chat.chatInput.toast.largeTextPaste.inline'), + onClick: () => resolveLargePaste('inline'), + }, + onDismiss: () => { + // Dismissing without a choice keeps the paste — insert inline + // so clipboard content is not lost. + resolveLargePaste('inline'); + }, + }, + ); return; } - if (!currentSessionId && !newSessionDraftOpen) { + if (!sessionReady) { if (pastedText.includes('@')) { markFileMentionPasteSuppression(); } @@ -1771,7 +1890,7 @@ const ChatInputComponent: React.FC = ({ onOpenSettings, scrollTo pendingPastedAttachmentFilenamesRef.current.delete(filename); } } - }, [addAttachedFile, attachedFiles, currentSessionId, inputMode, markFileMentionPasteSuppression, message, newSessionDraftOpen, insertTextAtSelection, setMessage, t, updateAutocompleteState]); + }, [addAttachedFile, attachedFiles, currentSessionId, inputMode, largeTextPasteBehavior, markFileMentionPasteSuppression, message, newSessionDraftOpen, insertTextAtSelection, setMessage, t, updateAutocompleteState]); const handleFileSelect = (file: { name: string; path: string; relativePath?: string }) => { diff --git a/packages/ui/src/components/chat/__tests__/attachmentCitations.test.ts b/packages/ui/src/components/chat/__tests__/attachmentCitations.test.ts index 96221b61..92d88ecd 100644 --- a/packages/ui/src/components/chat/__tests__/attachmentCitations.test.ts +++ b/packages/ui/src/components/chat/__tests__/attachmentCitations.test.ts @@ -5,6 +5,7 @@ import { buildAttachmentCitationText, findAttachmentCitationRanges, isGenericImageFilename, + nextPastedContextFilename, } from '../attachmentCitations'; describe('attachment citations', () => { @@ -53,4 +54,10 @@ describe('attachment citations', () => { ['desktop.jpg'], )).toEqual([{ start: 8, end: 21 }]); }); + + test('assigns sequential pasted-context filenames', () => { + expect(nextPastedContextFilename([])).toBe('pasted-context-1.txt'); + expect(nextPastedContextFilename(['pasted-context-1.txt', 'notes.md'])).toBe('pasted-context-2.txt'); + expect(nextPastedContextFilename(['PASTED-CONTEXT-2.TXT'])).toBe('pasted-context-1.txt'); + }); }); diff --git a/packages/ui/src/components/chat/attachmentCitations.ts b/packages/ui/src/components/chat/attachmentCitations.ts index 1faf6925..e3e380e1 100644 --- a/packages/ui/src/components/chat/attachmentCitations.ts +++ b/packages/ui/src/components/chat/attachmentCitations.ts @@ -144,6 +144,20 @@ export const assignImageAttachmentFilenames = ( }); }; +/** Next unused `pasted-context-N.txt` name for a large text paste attachment. */ +export const nextPastedContextFilename = (existingFilenames: string[]): string => { + const used = new Set(existingFilenames.map(normalizeFilenameKey)); + + for (let index = 1; index < Number.MAX_SAFE_INTEGER; index += 1) { + const candidate = `pasted-context-${index}.txt`; + if (!used.has(normalizeFilenameKey(candidate))) { + return candidate; + } + } + + return `pasted-context-${Date.now()}.txt`; +}; + export const buildAttachmentCitationText = (filenames: string[]): string => ( filenames.map((filename) => `[${filename}]`).join(' ') ); diff --git a/packages/ui/src/components/chat/composer/DOCUMENTATION.md b/packages/ui/src/components/chat/composer/DOCUMENTATION.md index 3544932a..db521edb 100644 --- a/packages/ui/src/components/chat/composer/DOCUMENTATION.md +++ b/packages/ui/src/components/chat/composer/DOCUMENTATION.md @@ -18,6 +18,16 @@ belongs to one of them. | `attachments/` | Files: paths, drop payloads | | `ui/` | Presentation | | `text.ts` | How inserted text meets the text already there | +| `largeTextPaste.ts` | Detect large plain-text pastes and build virtual `.txt` files | + +`ChatInput.handlePaste` owns paste orchestration: URL-over-selection markdown +links, clipboard images (attach + citation), and large plain-text pastes. +Large pastes (about 2,000 characters or 25 lines) follow the composer setting +`largeTextPasteBehavior` (`ask` / `attach` / `inline`). Attaching creates an +in-memory `text/plain` file named `pasted-context-N.txt`, inserts a bracket +citation, and sends it through the same attachment pipeline as a manually +picked `.txt` file. Short text, images, and URL wraps keep their existing +paths. ## The prompt language @@ -119,8 +129,8 @@ hardware. The package has no DOM test environment, so coverage stops at the state and logic layers: the language, the submit assembly, path and drop handling, text -splicing, message history, and the CodeMirror language extension at the -`EditorState` level. +splicing, large-paste detection, message history, and the CodeMirror language +extension at the `EditorState` level. Rendering, focus, keyboard behavior, IME and WKWebView are **not covered by tests** and are verified by hand. Do not report a change to them as validated diff --git a/packages/ui/src/components/chat/composer/__tests__/largeTextPaste.test.ts b/packages/ui/src/components/chat/composer/__tests__/largeTextPaste.test.ts new file mode 100644 index 00000000..b205394e --- /dev/null +++ b/packages/ui/src/components/chat/composer/__tests__/largeTextPaste.test.ts @@ -0,0 +1,45 @@ +import { describe, expect, test } from 'bun:test'; + +import { + LARGE_TEXT_PASTE_CHAR_THRESHOLD, + LARGE_TEXT_PASTE_LINE_THRESHOLD, + createPastedContextFile, + isLargePlainTextPaste, +} from '../largeTextPaste'; + +describe('large text paste helpers', () => { + test('treats short text as not large', () => { + expect(isLargePlainTextPaste('hello world')).toBe(false); + expect(isLargePlainTextPaste('line1\nline2\nline3')).toBe(false); + }); + + test('treats empty and whitespace-only pastes as not large', () => { + expect(isLargePlainTextPaste('')).toBe(false); + expect(isLargePlainTextPaste(' \n\t ')).toBe(false); + }); + + test('detects pastes at the character threshold', () => { + const text = 'a'.repeat(LARGE_TEXT_PASTE_CHAR_THRESHOLD); + expect(isLargePlainTextPaste(text)).toBe(true); + expect(isLargePlainTextPaste(text.slice(0, -1))).toBe(false); + }); + + test('detects pastes at the line threshold', () => { + const lines = Array.from({ length: LARGE_TEXT_PASTE_LINE_THRESHOLD }, (_, index) => `line ${index}`); + expect(isLargePlainTextPaste(lines.join('\n'))).toBe(true); + expect(isLargePlainTextPaste(lines.slice(0, -1).join('\n'))).toBe(false); + }); + + test('honors custom thresholds', () => { + expect(isLargePlainTextPaste('abcdef', { charThreshold: 5 })).toBe(true); + expect(isLargePlainTextPaste('a\nb\nc', { lineThreshold: 3 })).toBe(true); + expect(isLargePlainTextPaste('a\nb', { lineThreshold: 3, charThreshold: 100 })).toBe(false); + }); + + test('creates a text/plain file with the given name', async () => { + const file = createPastedContextFile('architecture notes', 'pasted-context-1.txt'); + expect(file.name).toBe('pasted-context-1.txt'); + expect(file.type.startsWith('text/plain')).toBe(true); + expect(await file.text()).toBe('architecture notes'); + }); +}); diff --git a/packages/ui/src/components/chat/composer/largeTextPaste.ts b/packages/ui/src/components/chat/composer/largeTextPaste.ts new file mode 100644 index 00000000..2b180d52 --- /dev/null +++ b/packages/ui/src/components/chat/composer/largeTextPaste.ts @@ -0,0 +1,55 @@ +/** + * Large plain-text paste → virtual file attachment helpers. + * + * Detect when clipboard text is large enough that inserting it into the + * composer would clutter the prompt, and build an in-memory text/plain File + * the attachment pipeline can send like any other .txt attachment. + */ + +export const LARGE_TEXT_PASTE_CHAR_THRESHOLD = 2000; +export const LARGE_TEXT_PASTE_LINE_THRESHOLD = 25; + +const countLines = (text: string): number => { + let lines = 1; + for (let index = 0; index < text.length; index += 1) { + if (text.charCodeAt(index) === 10) { + lines += 1; + } + } + return lines; +}; + +/** + * Whether pasted plain text should be offered (or auto-handled) as a file + * attachment instead of being inserted into the composer. + * + * Empty / whitespace-only pastes are never large. Thresholds are OR'd: + * character count or line count is enough. + */ +export const isLargePlainTextPaste = ( + text: string, + options?: { + charThreshold?: number; + lineThreshold?: number; + }, +): boolean => { + if (!text || !text.trim()) { + return false; + } + + const charThreshold = options?.charThreshold ?? LARGE_TEXT_PASTE_CHAR_THRESHOLD; + const lineThreshold = options?.lineThreshold ?? LARGE_TEXT_PASTE_LINE_THRESHOLD; + + if (text.length >= charThreshold) { + return true; + } + + return countLines(text) >= lineThreshold; +}; + +export const createPastedContextFile = (text: string, filename: string): File => ( + new File([text], filename, { + type: 'text/plain', + lastModified: Date.now(), + }) +); diff --git a/packages/ui/src/components/sections/openchamber/OpenChamberPage.tsx b/packages/ui/src/components/sections/openchamber/OpenChamberPage.tsx index 73f870ca..5e6e2071 100644 --- a/packages/ui/src/components/sections/openchamber/OpenChamberPage.tsx +++ b/packages/ui/src/components/sections/openchamber/OpenChamberPage.tsx @@ -206,6 +206,7 @@ const ChatSectionContent: React.FC = () => { 'followUpBehavior', 'persistDraft', 'inputSpellcheck', + 'largeTextPaste', ]} /> ); diff --git a/packages/ui/src/components/sections/openchamber/OpenChamberVisualSettings.tsx b/packages/ui/src/components/sections/openchamber/OpenChamberVisualSettings.tsx index 73d76470..fee6c784 100644 --- a/packages/ui/src/components/sections/openchamber/OpenChamberVisualSettings.tsx +++ b/packages/ui/src/components/sections/openchamber/OpenChamberVisualSettings.tsx @@ -3,7 +3,7 @@ import { runtimeFetch } from '@/lib/runtime-fetch'; import { useThemeSystem } from '@/contexts/useThemeSystem'; import type { ThemeMode } from '@/types/theme'; -import { useUIStore } from '@/stores/useUIStore'; +import { useUIStore, type LargeTextPasteBehavior } from '@/stores/useUIStore'; import { useMessageQueueStore, type FollowUpBehavior } from '@/stores/messageQueueStore'; import { cn } from '@/lib/utils'; import { Button } from '@/components/ui/button'; @@ -275,11 +275,26 @@ const FOLLOW_UP_BEHAVIOR_OPTIONS: Option[] = [ }, ]; +const LARGE_TEXT_PASTE_BEHAVIOR_OPTIONS: Option[] = [ + { + id: 'ask', + labelKey: 'settings.openchamber.visual.option.largeTextPaste.ask.label', + }, + { + id: 'attach', + labelKey: 'settings.openchamber.visual.option.largeTextPaste.attach.label', + }, + { + id: 'inline', + labelKey: 'settings.openchamber.visual.option.largeTextPaste.inline.label', + }, +]; + const normalizeUserMessageRenderingMode = (mode: unknown): 'markdown' | 'plain' => { return mode === 'markdown' ? 'markdown' : 'plain'; }; -type VisibleSetting = 'sessionAssist' | 'sessionGoal' | 'theme' | 'windowControlsPosition' | 'pwaInstallName' | 'pwaOrientation' | 'mobileKeyboardMode' | 'timeFormat' | 'weekStart' | 'fontSize' | 'terminalFontSize' | 'terminalShell' | 'terminalLoginShell' | 'editorFontSize' | 'spacing' | 'inputBarOffset' | 'mermaidRendering' | 'userMessageRendering' | 'chatRenderMode' | 'messageTransport' | 'activityRenderMode' | 'collapsibleUserMessages' | 'stickyUserHeader' | 'promptNavigatorEnabled' | 'wideChatLayout' | 'codeBlockLineWrap' | 'splitAssistantMessageActions' | 'subagentReadOnlyBanner' | 'diffLayout' | 'mobileStatusBar' | 'dotfiles' | 'fileViewerPreview' | 'reasoning' | 'showToolFileIcons' | 'showTurnChangedFiles' | 'expandedTools' | 'followUpBehavior' | 'terminalQuickKeys' | 'fileEditorKeymap' | 'persistDraft' | 'inputSpellcheck' | 'reportUsage' | 'expandedEditorToolbar' | 'autoSaveEnabled'; +type VisibleSetting = 'sessionAssist' | 'sessionGoal' | 'theme' | 'windowControlsPosition' | 'pwaInstallName' | 'pwaOrientation' | 'mobileKeyboardMode' | 'timeFormat' | 'weekStart' | 'fontSize' | 'terminalFontSize' | 'terminalShell' | 'terminalLoginShell' | 'editorFontSize' | 'spacing' | 'inputBarOffset' | 'mermaidRendering' | 'userMessageRendering' | 'chatRenderMode' | 'messageTransport' | 'activityRenderMode' | 'collapsibleUserMessages' | 'stickyUserHeader' | 'promptNavigatorEnabled' | 'wideChatLayout' | 'codeBlockLineWrap' | 'splitAssistantMessageActions' | 'subagentReadOnlyBanner' | 'diffLayout' | 'mobileStatusBar' | 'dotfiles' | 'fileViewerPreview' | 'reasoning' | 'showToolFileIcons' | 'showTurnChangedFiles' | 'expandedTools' | 'followUpBehavior' | 'terminalQuickKeys' | 'fileEditorKeymap' | 'persistDraft' | 'inputSpellcheck' | 'largeTextPaste' | 'reportUsage' | 'expandedEditorToolbar' | 'autoSaveEnabled'; const WINDOW_CONTROLS_POSITION_OPTIONS: Array<{ id: DesktopWindowControlsPosition; labelKey: string }> = [ { id: 'left', labelKey: 'settings.openchamber.desktopNetwork.option.windowControlsLeft' }, @@ -372,6 +387,8 @@ export const OpenChamberVisualSettings: React.FC const setPersistChatDraft = useUIStore(state => state.setPersistChatDraft); const inputSpellcheckEnabled = useUIStore(state => state.inputSpellcheckEnabled); const setInputSpellcheckEnabled = useUIStore(state => state.setInputSpellcheckEnabled); + const largeTextPasteBehavior = useUIStore(state => state.largeTextPasteBehavior); + const setLargeTextPasteBehavior = useUIStore(state => state.setLargeTextPasteBehavior); const showToolFileIcons = useUIStore(state => state.showToolFileIcons); const setShowToolFileIcons = useUIStore(state => state.setShowToolFileIcons); const showTurnChangedFiles = useUIStore(state => state.showTurnChangedFiles); @@ -665,6 +682,7 @@ export const OpenChamberVisualSettings: React.FC || shouldShow('reasoning') || shouldShow('followUpBehavior') || shouldShow('persistDraft') + || shouldShow('largeTextPaste') || shouldShow('showToolFileIcons') || shouldShow('expandedTools') || (!isMobile && shouldShow('inputSpellcheck')); @@ -687,6 +705,7 @@ export const OpenChamberVisualSettings: React.FC || shouldShow('dotfiles') || shouldShow('fileViewerPreview') || shouldShow('persistDraft') + || shouldShow('largeTextPaste') || shouldShow('showToolFileIcons') || shouldShow('showTurnChangedFiles') || (!isMobile && shouldShow('inputSpellcheck')) @@ -2036,7 +2055,7 @@ export const OpenChamberVisualSettings: React.FC )} - {(shouldShow('persistDraft') || (!isMobile && shouldShow('inputSpellcheck'))) && ( + {(shouldShow('persistDraft') || shouldShow('largeTextPaste') || (!isMobile && shouldShow('inputSpellcheck'))) && ( settingsItem="chat.spellcheck" /> )} + + {shouldShow('largeTextPaste') && ( + + + {LARGE_TEXT_PASTE_BEHAVIOR_OPTIONS.map((option) => ( + setLargeTextPasteBehavior(option.id)} + label={tUnsafe(option.labelKey)} + ariaLabel={t('settings.openchamber.visual.field.largeTextPasteOptionAria', { option: tUnsafe(option.labelKey) })} + /> + ))} + + + )} )} diff --git a/packages/ui/src/lib/i18n/messages/de.settings.ts b/packages/ui/src/lib/i18n/messages/de.settings.ts index fc3cfa86..81862cbf 100644 --- a/packages/ui/src/lib/i18n/messages/de.settings.ts +++ b/packages/ui/src/lib/i18n/messages/de.settings.ts @@ -1878,6 +1878,13 @@ export const settingsDict = { 'settings.openchamber.visual.field.persistDraftMessages': 'Entwurfsnachrichten speichern', 'settings.openchamber.visual.field.enableSpellcheckInTextInputsAria': 'Rechtschreibprüfung in Texteingaben aktivieren', 'settings.openchamber.visual.field.enableSpellcheckInTextInputs': 'Rechtschreibprüfung in Texteingaben aktivieren', + 'settings.openchamber.visual.field.largeTextPaste': 'Großes Texteinfügen', + 'settings.openchamber.visual.field.largeTextPasteHint': 'Beim Einfügen von mehr als etwa 2.000 Zeichen oder 25 Zeilen wählen, ob der Text als Datei angehängt, direkt eingefügt oder jedes Mal nachgefragt werden soll.', + 'settings.openchamber.visual.field.largeTextPasteAria': 'Verhalten bei großem Texteinfügen', + 'settings.openchamber.visual.field.largeTextPasteOptionAria': 'Großes Texteinfügen: {option}', + 'settings.openchamber.visual.option.largeTextPaste.ask.label': 'Jedes Mal fragen', + 'settings.openchamber.visual.option.largeTextPaste.attach.label': 'Als Datei anhängen', + 'settings.openchamber.visual.option.largeTextPaste.inline.label': 'Direkt einfügen', 'settings.openchamber.visual.field.sendAnonymousUsageReportsAria': 'Anonyme Nutzungsberichte senden', 'settings.openchamber.visual.field.sendAnonymousUsageReports': 'Anonyme Nutzungsberichte senden', 'settings.openchamber.visual.field.sendAnonymousUsageReportsHint': 'Hilft uns zu verstehen, welche App-Versionen aktiv genutzt werden, damit wir Verbesserungen priorisieren können. Es werden nur die App-Version, Plattform und Laufzeit gesammelt - keine persönlichen Daten oder Code.', diff --git a/packages/ui/src/lib/i18n/messages/de.ts b/packages/ui/src/lib/i18n/messages/de.ts index 0db76c86..679c1812 100644 --- a/packages/ui/src/lib/i18n/messages/de.ts +++ b/packages/ui/src/lib/i18n/messages/de.ts @@ -1963,6 +1963,11 @@ export const dict = { 'chat.chatInput.toast.sendAttachmentsFailed': 'Fehler beim Senden der Anhänge. Versuche weniger Dateien oder kleinere Bilder.', 'chat.chatInput.toast.messageSendFailed': 'Nachricht konnte nicht gesendet werden. Anhänge wurden wiederhergestellt.', 'chat.chatInput.toast.clipboardAttachFailed': 'Fehler beim Anhängen des Bildes aus der Zwischenablage', + 'chat.chatInput.toast.clipboardTextAttachFailed': 'Fehler beim Anhängen des eingefügten Texts als Datei', + 'chat.chatInput.toast.largeTextPaste.title': 'Großes Texteinfügen', + 'chat.chatInput.toast.largeTextPaste.description': 'Als Datei anhängen, um das Eingabefeld übersichtlich zu halten, oder den Text direkt einfügen.', + 'chat.chatInput.toast.largeTextPaste.attach': 'Als Datei anhängen', + 'chat.chatInput.toast.largeTextPaste.inline': 'Direkt einfügen', 'chat.chatInput.toast.addedFileMentions': '{count} Datei(er) hinzugefügt', 'chat.chatInput.toast.attachFileFailed': 'Fehler beim Anhängen der Datei', 'chat.chatInput.toast.attachNamedFailed': 'Fehler beim Anhängen von {name}', diff --git a/packages/ui/src/lib/i18n/messages/en.settings.ts b/packages/ui/src/lib/i18n/messages/en.settings.ts index 4a1deb81..cbff96df 100644 --- a/packages/ui/src/lib/i18n/messages/en.settings.ts +++ b/packages/ui/src/lib/i18n/messages/en.settings.ts @@ -1964,6 +1964,13 @@ export const settingsDict = { 'settings.openchamber.visual.field.persistDraftMessages': 'Persist Draft Messages', 'settings.openchamber.visual.field.enableSpellcheckInTextInputsAria': 'Enable spellcheck in text inputs', 'settings.openchamber.visual.field.enableSpellcheckInTextInputs': 'Enable Spellcheck in Text Inputs', + 'settings.openchamber.visual.field.largeTextPaste': 'Large text paste', + 'settings.openchamber.visual.field.largeTextPasteHint': 'When pasting more than about 2,000 characters or 25 lines, choose whether to attach the text as a file, paste it inline, or ask each time.', + 'settings.openchamber.visual.field.largeTextPasteAria': 'Large text paste behavior', + 'settings.openchamber.visual.field.largeTextPasteOptionAria': 'Large text paste: {option}', + 'settings.openchamber.visual.option.largeTextPaste.ask.label': 'Ask each time', + 'settings.openchamber.visual.option.largeTextPaste.attach.label': 'Attach as file', + 'settings.openchamber.visual.option.largeTextPaste.inline.label': 'Paste inline', 'settings.openchamber.visual.field.sendAnonymousUsageReportsAria': 'Send anonymous usage reports', 'settings.openchamber.visual.field.sendAnonymousUsageReports': 'Send anonymous usage reports', 'settings.openchamber.visual.field.sendAnonymousUsageReportsHint': 'Helps us understand which app versions are actively used so we can prioritize improvements. Only app version, platform, and runtime are collected - no personal data or code.', diff --git a/packages/ui/src/lib/i18n/messages/en.ts b/packages/ui/src/lib/i18n/messages/en.ts index 17d137e5..9953d07d 100644 --- a/packages/ui/src/lib/i18n/messages/en.ts +++ b/packages/ui/src/lib/i18n/messages/en.ts @@ -2124,6 +2124,11 @@ export const dict = { 'chat.chatInput.toast.sendAttachmentsFailed': 'Failed to send attachments. Try fewer files or smaller images.', 'chat.chatInput.toast.messageSendFailed': 'Message failed to send. Attachments restored.', 'chat.chatInput.toast.clipboardAttachFailed': 'Failed to attach image from clipboard', + 'chat.chatInput.toast.clipboardTextAttachFailed': 'Failed to attach pasted text as a file', + 'chat.chatInput.toast.largeTextPaste.title': 'Large text paste', + 'chat.chatInput.toast.largeTextPaste.description': 'Attach as a file to keep the composer clear, or paste the text inline.', + 'chat.chatInput.toast.largeTextPaste.attach': 'Attach as file', + 'chat.chatInput.toast.largeTextPaste.inline': 'Paste inline', 'chat.chatInput.toast.addedFileMentions': 'Added {count} file mention(s)', 'chat.chatInput.toast.attachFileFailed': 'Failed to attach file', 'chat.chatInput.toast.attachNamedFailed': 'Failed to attach {name}', diff --git a/packages/ui/src/lib/i18n/messages/es.settings.ts b/packages/ui/src/lib/i18n/messages/es.settings.ts index 31b07208..4cfed7ed 100644 --- a/packages/ui/src/lib/i18n/messages/es.settings.ts +++ b/packages/ui/src/lib/i18n/messages/es.settings.ts @@ -1940,6 +1940,13 @@ export const settingsDict = { "settings.openchamber.visual.field.persistDraftMessages": "Conservar borradores de mensajes", "settings.openchamber.visual.field.enableSpellcheckInTextInputsAria": "Habilitar ortografía en campos de texto", "settings.openchamber.visual.field.enableSpellcheckInTextInputs": "Habilitar ortografía en campos de texto", + "settings.openchamber.visual.field.largeTextPaste": "Pegado de texto grande", + "settings.openchamber.visual.field.largeTextPasteHint": "Al pegar más de unos 2000 caracteres o 25 líneas, elige si adjuntar el texto como archivo, pegarlo en línea o preguntar cada vez.", + "settings.openchamber.visual.field.largeTextPasteAria": "Comportamiento del pegado de texto grande", + "settings.openchamber.visual.field.largeTextPasteOptionAria": "Pegado de texto grande: {option}", + "settings.openchamber.visual.option.largeTextPaste.ask.label": "Preguntar cada vez", + "settings.openchamber.visual.option.largeTextPaste.attach.label": "Adjuntar como archivo", + "settings.openchamber.visual.option.largeTextPaste.inline.label": "Pegar en línea", "settings.openchamber.visual.field.sendAnonymousUsageReportsAria": "Enviar informes anónimos de uso", "settings.openchamber.visual.field.sendAnonymousUsageReports": "Enviar informes anónimos de uso", "settings.openchamber.visual.field.sendAnonymousUsageReportsHint": "Nos ayuda a entender qué versiones de la aplicación se usan activamente para priorizar mejoras. Solo se recopilan la versión de la aplicación, la plataforma y el entorno de ejecución ; no se recopilan datos personales ni código.", diff --git a/packages/ui/src/lib/i18n/messages/es.ts b/packages/ui/src/lib/i18n/messages/es.ts index ea5086d0..a4e2ea13 100644 --- a/packages/ui/src/lib/i18n/messages/es.ts +++ b/packages/ui/src/lib/i18n/messages/es.ts @@ -2090,6 +2090,11 @@ export const dict: Record = { "chat.chatInput.toast.sendAttachmentsFailed": "No se pudieron enviar los adjuntos. Intenta con menos archivos o imágenes más pequeñas.", "chat.chatInput.toast.messageSendFailed": "El mensaje no se pudo enviar. Los adjuntos se restauraron.", "chat.chatInput.toast.clipboardAttachFailed": "No se pudo adjuntar la imagen desde el portapapeles", + "chat.chatInput.toast.clipboardTextAttachFailed": "No se pudo adjuntar el texto pegado como archivo", + "chat.chatInput.toast.largeTextPaste.title": "Pegado de texto grande", + "chat.chatInput.toast.largeTextPaste.description": "Adjunta como archivo para mantener el compositor despejado, o pega el texto en línea.", + "chat.chatInput.toast.largeTextPaste.attach": "Adjuntar como archivo", + "chat.chatInput.toast.largeTextPaste.inline": "Pegar en línea", "chat.chatInput.toast.addedFileMentions": "Se añadieron {count} mención(es) de archivo", "chat.chatInput.toast.attachFileFailed": "No se pudo adjuntar el archivo", "chat.chatInput.toast.attachNamedFailed": "No se pudo adjuntar {name}", diff --git a/packages/ui/src/lib/i18n/messages/fr.settings.ts b/packages/ui/src/lib/i18n/messages/fr.settings.ts index f5dc26d4..2db269c5 100644 --- a/packages/ui/src/lib/i18n/messages/fr.settings.ts +++ b/packages/ui/src/lib/i18n/messages/fr.settings.ts @@ -1843,6 +1843,13 @@ export const settingsDict = { 'settings.openchamber.visual.field.persistDraftMessages': 'Conserver les brouillons de messages', 'settings.openchamber.visual.field.enableSpellcheckInTextInputsAria': 'Activer la vérification orthographique dans les saisies de texte', 'settings.openchamber.visual.field.enableSpellcheckInTextInputs': 'Activer la vérification orthographique dans les entrées de texte', + 'settings.openchamber.visual.field.largeTextPaste': 'Collage de texte volumineux', + 'settings.openchamber.visual.field.largeTextPasteHint': 'Lors d’un collage de plus d’environ 2 000 caractères ou 25 lignes, choisir de joindre le texte comme fichier, de le coller en ligne ou de demander à chaque fois.', + 'settings.openchamber.visual.field.largeTextPasteAria': 'Comportement du collage de texte volumineux', + 'settings.openchamber.visual.field.largeTextPasteOptionAria': 'Collage de texte volumineux : {option}', + 'settings.openchamber.visual.option.largeTextPaste.ask.label': 'Demander à chaque fois', + 'settings.openchamber.visual.option.largeTextPaste.attach.label': 'Joindre comme fichier', + 'settings.openchamber.visual.option.largeTextPaste.inline.label': 'Coller en ligne', 'settings.openchamber.visual.field.sendAnonymousUsageReportsAria': 'Envoyer des rapports d\'utilisation anonymes', 'settings.openchamber.visual.field.sendAnonymousUsageReports': 'Envoyer des rapports d\'utilisation anonymes', 'settings.openchamber.visual.field.sendAnonymousUsageReportsHint': 'Nous aide à comprendre quelles versions de l\'application sont activement utilisées afin que nous puissions prioriser les améliorations. Seules la version de l’application, la plate-forme et le runtime sont collectés – aucune donnée personnelle ni code.', diff --git a/packages/ui/src/lib/i18n/messages/fr.ts b/packages/ui/src/lib/i18n/messages/fr.ts index 3a625450..a56f5434 100644 --- a/packages/ui/src/lib/i18n/messages/fr.ts +++ b/packages/ui/src/lib/i18n/messages/fr.ts @@ -1893,6 +1893,11 @@ export const dict = { 'chat.chatInput.toast.sendAttachmentsFailed': 'Échec de l\'envoi des pièces jointes. Essayez moins de fichiers ou des images plus petites.', 'chat.chatInput.toast.messageSendFailed': 'Le message n\'a pas pu être envoyé. Pièces jointes restaurées.', 'chat.chatInput.toast.clipboardAttachFailed': 'Échec de la pièce jointe de l\'image du presse-papiers', + 'chat.chatInput.toast.clipboardTextAttachFailed': 'Échec de la pièce jointe du texte collé comme fichier', + 'chat.chatInput.toast.largeTextPaste.title': 'Collage de texte volumineux', + 'chat.chatInput.toast.largeTextPaste.description': 'Joindre comme fichier pour garder la zone de saisie claire, ou coller le texte en ligne.', + 'chat.chatInput.toast.largeTextPaste.attach': 'Joindre comme fichier', + 'chat.chatInput.toast.largeTextPaste.inline': 'Coller en ligne', 'chat.chatInput.toast.addedFileMentions': 'Ajout des mentions du fichier {count}', 'chat.chatInput.toast.attachFileFailed': 'Impossible de joindre le fichier', 'chat.chatInput.toast.attachNamedFailed': 'Échec de la connexion du {name}', diff --git a/packages/ui/src/lib/i18n/messages/ja.settings.ts b/packages/ui/src/lib/i18n/messages/ja.settings.ts index 3d7565a9..6a64dd40 100644 --- a/packages/ui/src/lib/i18n/messages/ja.settings.ts +++ b/packages/ui/src/lib/i18n/messages/ja.settings.ts @@ -1973,6 +1973,13 @@ export const settingsDict = { 'settings.openchamber.visual.field.persistDraftMessages': '下書きメッセージを保持', 'settings.openchamber.visual.field.enableSpellcheckInTextInputsAria': 'テキスト入力のスペルチェックを有効化', 'settings.openchamber.visual.field.enableSpellcheckInTextInputs': 'テキスト入力のスペルチェックを有効化', + 'settings.openchamber.visual.field.largeTextPaste': '大きなテキストの貼り付け', + 'settings.openchamber.visual.field.largeTextPasteHint': '約 2,000 文字または 25 行を超えるテキストを貼り付けるとき、ファイルとして添付するか、そのまま貼り付けるか、毎回確認するかを選べます。', + 'settings.openchamber.visual.field.largeTextPasteAria': '大きなテキスト貼り付けの動作', + 'settings.openchamber.visual.field.largeTextPasteOptionAria': '大きなテキストの貼り付け: {option}', + 'settings.openchamber.visual.option.largeTextPaste.ask.label': '毎回確認する', + 'settings.openchamber.visual.option.largeTextPaste.attach.label': 'ファイルとして添付', + 'settings.openchamber.visual.option.largeTextPaste.inline.label': 'そのまま貼り付け', 'settings.openchamber.visual.field.sendAnonymousUsageReportsAria': '匿名使用状況レポートを送信', 'settings.openchamber.visual.field.sendAnonymousUsageReports': '匿名使用状況レポートを送信', 'settings.openchamber.visual.field.sendAnonymousUsageReportsHint': 'どのアプリバージョンがアクティブに使用されているかを把握し、改善の優先順位を決めるのに役立ちます。収集されるのはアプリバージョン、プラットフォーム、ランタイムのみで、個人データやコードは収集されません。', diff --git a/packages/ui/src/lib/i18n/messages/ja.ts b/packages/ui/src/lib/i18n/messages/ja.ts index 181ec3e9..a8e8e4b9 100644 --- a/packages/ui/src/lib/i18n/messages/ja.ts +++ b/packages/ui/src/lib/i18n/messages/ja.ts @@ -2120,6 +2120,11 @@ export const dict: Record = { 'chat.chatInput.toast.sendAttachmentsFailed': '添付ファイルの送信に失敗しました。ファイルを減らすかサイズを小さくしてください。', 'chat.chatInput.toast.messageSendFailed': 'メッセージの送信に失敗しました。添付ファイルは復元されました。', 'chat.chatInput.toast.clipboardAttachFailed': 'クリップボードからの画像添付に失敗しました', + 'chat.chatInput.toast.clipboardTextAttachFailed': '貼り付けたテキストのファイル添付に失敗しました', + 'chat.chatInput.toast.largeTextPaste.title': '大きなテキストの貼り付け', + 'chat.chatInput.toast.largeTextPaste.description': '入力欄をすっきり保つためにファイルとして添付するか、そのまま貼り付けます。', + 'chat.chatInput.toast.largeTextPaste.attach': 'ファイルとして添付', + 'chat.chatInput.toast.largeTextPaste.inline': 'そのまま貼り付け', 'chat.chatInput.toast.addedFileMentions': '{count}件のファイルメンションを追加しました', 'chat.chatInput.toast.attachFileFailed': 'ファイルの添付に失敗しました', 'gitView.commit.aiHighlights.insertAria': '挿入のariaラベル', diff --git a/packages/ui/src/lib/i18n/messages/ko.settings.ts b/packages/ui/src/lib/i18n/messages/ko.settings.ts index df181c60..bf3b665a 100644 --- a/packages/ui/src/lib/i18n/messages/ko.settings.ts +++ b/packages/ui/src/lib/i18n/messages/ko.settings.ts @@ -1940,6 +1940,13 @@ export const settingsDict = { 'settings.openchamber.visual.field.persistDraftMessages': '초안 메시지 유지', 'settings.openchamber.visual.field.enableSpellcheckInTextInputsAria': '텍스트 입력에서 맞춤법 검사 활성화', 'settings.openchamber.visual.field.enableSpellcheckInTextInputs': '텍스트 입력에서 맞춤법 검사 활성화', + 'settings.openchamber.visual.field.largeTextPaste': '긴 텍스트 붙여넣기', + 'settings.openchamber.visual.field.largeTextPasteHint': '약 2,000자 또는 25줄을 넘는 텍스트를 붙여넣을 때 파일로 첨부할지, 본문에 붙여넣을지, 매번 물어볼지 선택합니다.', + 'settings.openchamber.visual.field.largeTextPasteAria': '긴 텍스트 붙여넣기 동작', + 'settings.openchamber.visual.field.largeTextPasteOptionAria': '긴 텍스트 붙여넣기: {option}', + 'settings.openchamber.visual.option.largeTextPaste.ask.label': '매번 묻기', + 'settings.openchamber.visual.option.largeTextPaste.attach.label': '파일로 첨부', + 'settings.openchamber.visual.option.largeTextPaste.inline.label': '본문에 붙여넣기', 'settings.openchamber.visual.field.sendAnonymousUsageReportsAria': '익명 사용량 보고서 보내기', 'settings.openchamber.visual.field.sendAnonymousUsageReports': '익명 사용량 보고서 보내기', 'settings.openchamber.visual.field.sendAnonymousUsageReportsHint': '활성 사용 앱 버전을 파악해 개선 우선순위를 정하는 데 도움이 됩니다. 앱 버전, 플랫폼, 런타임만 수집되며 개인 데이터나 코드는 수집되지 않습니다.', diff --git a/packages/ui/src/lib/i18n/messages/ko.ts b/packages/ui/src/lib/i18n/messages/ko.ts index 150a56ce..5794969b 100644 --- a/packages/ui/src/lib/i18n/messages/ko.ts +++ b/packages/ui/src/lib/i18n/messages/ko.ts @@ -2124,6 +2124,11 @@ export const dict: Record = { 'chat.chatInput.toast.sendAttachmentsFailed': '첨부 파일 전송 실패. 파일 수나 이미지 크기를 줄여 보세요.', 'chat.chatInput.toast.messageSendFailed': '메시지 전송에 실패했습니다. 첨부 파일을 복원했습니다.', 'chat.chatInput.toast.clipboardAttachFailed': '클립보드 이미지 첨부 실패', + 'chat.chatInput.toast.clipboardTextAttachFailed': '붙여넣은 텍스트를 파일로 첨부하지 못했습니다', + 'chat.chatInput.toast.largeTextPaste.title': '긴 텍스트 붙여넣기', + 'chat.chatInput.toast.largeTextPaste.description': '입력창을 깔끔하게 유지하려면 파일로 첨부하거나, 본문에 붙여넣으세요.', + 'chat.chatInput.toast.largeTextPaste.attach': '파일로 첨부', + 'chat.chatInput.toast.largeTextPaste.inline': '본문에 붙여넣기', 'chat.chatInput.toast.addedFileMentions': '파일 멘션 {count}개 추가됨', 'chat.chatInput.toast.attachFileFailed': '첨부 파일 실패', 'chat.chatInput.toast.attachNamedFailed': '첨부 {name} 실패', diff --git a/packages/ui/src/lib/i18n/messages/pl.settings.ts b/packages/ui/src/lib/i18n/messages/pl.settings.ts index dcc3d774..47249c01 100644 --- a/packages/ui/src/lib/i18n/messages/pl.settings.ts +++ b/packages/ui/src/lib/i18n/messages/pl.settings.ts @@ -1024,6 +1024,13 @@ export const settingsDict = { 'settings.openchamber.visual.field.enableSpellcheckInTextInputs': 'Włącz sprawdzanie pisowni w polach tekstowych', 'settings.openchamber.visual.field.enableSpellcheckInTextInputsAria': 'Włącz sprawdzanie pisowni w polach tekstowych', + 'settings.openchamber.visual.field.largeTextPaste': 'Wklejanie dużego tekstu', + 'settings.openchamber.visual.field.largeTextPasteHint': 'Przy wklejaniu ponad około 2000 znaków lub 25 wierszy wybierz, czy dołączyć tekst jako plik, wkleić go w treści, czy pytać za każdym razem.', + 'settings.openchamber.visual.field.largeTextPasteAria': 'Zachowanie przy wklejaniu dużego tekstu', + 'settings.openchamber.visual.field.largeTextPasteOptionAria': 'Wklejanie dużego tekstu: {option}', + 'settings.openchamber.visual.option.largeTextPaste.ask.label': 'Pytaj za każdym razem', + 'settings.openchamber.visual.option.largeTextPaste.attach.label': 'Dołącz jako plik', + 'settings.openchamber.visual.option.largeTextPaste.inline.label': 'Wklej w treści', 'settings.openchamber.visual.field.fontSizePercentageAria': 'Procentowy rozmiar czcionki', 'settings.openchamber.visual.field.inputBarOffset': 'Przesunięcie paska wpisywania', 'settings.openchamber.visual.field.inputBarOffsetTooltip': 'Podnieś pasek wpisywania, aby uniknąć zasłaniania przez systemowe elementy ekranu, takie jak pasek gestów.', diff --git a/packages/ui/src/lib/i18n/messages/pl.ts b/packages/ui/src/lib/i18n/messages/pl.ts index 8f93e1f6..fe6bad3e 100644 --- a/packages/ui/src/lib/i18n/messages/pl.ts +++ b/packages/ui/src/lib/i18n/messages/pl.ts @@ -1221,6 +1221,11 @@ export const dict: Record = { 'chat.chatInput.toast.unsupportedAttachmentModalities': 'Model {model} nie obsługuje danych wejściowych {modalities} wymaganych przez {files}. Nadal możesz wysłać wiadomość, ale te załączniki mogą zostać zignorowane.', 'chat.chatInput.toast.attachmentsTooLarge': 'Załączniki są zbyt duże, aby je wysłać. Spróbuj zmniejszyć liczbę lub rozmiar obrazów.', 'chat.chatInput.toast.clipboardAttachFailed': 'Nie udało się dołączyć obrazu ze schowka', + 'chat.chatInput.toast.clipboardTextAttachFailed': 'Nie udało się dołączyć wklejonego tekstu jako pliku', + 'chat.chatInput.toast.largeTextPaste.title': 'Wklejanie dużego tekstu', + 'chat.chatInput.toast.largeTextPaste.description': 'Dołącz jako plik, aby nie zaśmiecać pola wiadomości, albo wklej tekst w treści.', + 'chat.chatInput.toast.largeTextPaste.attach': 'Dołącz jako plik', + 'chat.chatInput.toast.largeTextPaste.inline': 'Wklej w treści', 'chat.chatInput.toast.compactFailed': 'Nie udało się skompaktować sesji', 'chat.chatInput.toast.messageSendFailed': 'Nie udało się wysłać wiadomości. Załączniki zostały przywrócone.', 'chat.chatInput.toast.openSessionFirst': 'Najpierw otwórz sesję', 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 5950ec10..e9b0b8dc 100644 --- a/packages/ui/src/lib/i18n/messages/pt-BR.settings.ts +++ b/packages/ui/src/lib/i18n/messages/pt-BR.settings.ts @@ -1940,6 +1940,13 @@ export const settingsDict = { "settings.openchamber.visual.field.persistDraftMessages": "Manter rascunhos de mensagens", "settings.openchamber.visual.field.enableSpellcheckInTextInputsAria": "Ativar ortografia em campos de texto", "settings.openchamber.visual.field.enableSpellcheckInTextInputs": "Ativar ortografia em campos de texto", + "settings.openchamber.visual.field.largeTextPaste": "Colagem de texto grande", + "settings.openchamber.visual.field.largeTextPasteHint": "Ao colar mais de cerca de 2.000 caracteres ou 25 linhas, escolha anexar o texto como arquivo, colar no corpo da mensagem ou perguntar sempre.", + "settings.openchamber.visual.field.largeTextPasteAria": "Comportamento da colagem de texto grande", + "settings.openchamber.visual.field.largeTextPasteOptionAria": "Colagem de texto grande: {option}", + "settings.openchamber.visual.option.largeTextPaste.ask.label": "Perguntar sempre", + "settings.openchamber.visual.option.largeTextPaste.attach.label": "Anexar como arquivo", + "settings.openchamber.visual.option.largeTextPaste.inline.label": "Colar no corpo", "settings.openchamber.visual.field.sendAnonymousUsageReportsAria": "Enviar relatórios anônimos de uso", "settings.openchamber.visual.field.sendAnonymousUsageReports": "Enviar relatórios anônimos de uso", "settings.openchamber.visual.field.sendAnonymousUsageReportsHint": "Ajuda-nos a entender quais versões do aplicativo são usadas ativamente para priorizar melhorias. Coletamos apenas a versão do aplicativo, a plataforma e o ambiente de execução; não coletamos dados pessoais nem código.", diff --git a/packages/ui/src/lib/i18n/messages/pt-BR.ts b/packages/ui/src/lib/i18n/messages/pt-BR.ts index 44fef67a..3ad5cb36 100644 --- a/packages/ui/src/lib/i18n/messages/pt-BR.ts +++ b/packages/ui/src/lib/i18n/messages/pt-BR.ts @@ -2090,6 +2090,11 @@ export const dict: Record = { "chat.chatInput.toast.sendAttachmentsFailed": "Não foi possível enviar os anexos. Tente com menos arquivos ou imagens menores.", "chat.chatInput.toast.messageSendFailed": "A mensagem não pôde ser enviada. Os anexos foram restaurados.", "chat.chatInput.toast.clipboardAttachFailed": "Não foi possível anexar a imagem da área de transferência", + "chat.chatInput.toast.clipboardTextAttachFailed": "Não foi possível anexar o texto colado como arquivo", + "chat.chatInput.toast.largeTextPaste.title": "Colagem de texto grande", + "chat.chatInput.toast.largeTextPaste.description": "Anexe como arquivo para manter o compositor limpo, ou cole o texto no corpo da mensagem.", + "chat.chatInput.toast.largeTextPaste.attach": "Anexar como arquivo", + "chat.chatInput.toast.largeTextPaste.inline": "Colar no corpo", "chat.chatInput.toast.addedFileMentions": "Foram adicionadas {count} menção(es) de arquivo", "chat.chatInput.toast.attachFileFailed": "Não foi possível anexar o arquivo", "chat.chatInput.toast.attachNamedFailed": "Não foi possível anexar {name}", diff --git a/packages/ui/src/lib/i18n/messages/uk.settings.ts b/packages/ui/src/lib/i18n/messages/uk.settings.ts index 306608d2..5c398ee8 100644 --- a/packages/ui/src/lib/i18n/messages/uk.settings.ts +++ b/packages/ui/src/lib/i18n/messages/uk.settings.ts @@ -1940,6 +1940,13 @@ export const settingsDict = { "settings.openchamber.visual.field.persistDraftMessages": "Зберігати чернетки повідомлень", "settings.openchamber.visual.field.enableSpellcheckInTextInputsAria": "Увімкнути перевірку орфографії під час введення тексту", "settings.openchamber.visual.field.enableSpellcheckInTextInputs": "Увімкнути перевірку орфографії в текстових полях", + "settings.openchamber.visual.field.largeTextPaste": "Вставлення великого тексту", + "settings.openchamber.visual.field.largeTextPasteHint": "Під час вставлення понад приблизно 2000 символів або 25 рядків виберіть, чи долучити текст як файл, вставити його в повідомлення чи запитувати щоразу.", + "settings.openchamber.visual.field.largeTextPasteAria": "Поведінка вставлення великого тексту", + "settings.openchamber.visual.field.largeTextPasteOptionAria": "Вставлення великого тексту: {option}", + "settings.openchamber.visual.option.largeTextPaste.ask.label": "Запитувати щоразу", + "settings.openchamber.visual.option.largeTextPaste.attach.label": "Долучити як файл", + "settings.openchamber.visual.option.largeTextPaste.inline.label": "Вставити в повідомлення", "settings.openchamber.visual.field.sendAnonymousUsageReportsAria": "Надсилати анонімні звіти про використання", "settings.openchamber.visual.field.sendAnonymousUsageReports": "Надсилати анонімні звіти про використання", "settings.openchamber.visual.field.sendAnonymousUsageReportsHint": "Допомагає нам зрозуміти, які версії застосунків активно використовуються, щоб ми могли визначити пріоритети покращень. Збираються лише версія застосунку, платформа та середовище виконання – без особистих даних чи коду.", diff --git a/packages/ui/src/lib/i18n/messages/uk.ts b/packages/ui/src/lib/i18n/messages/uk.ts index c8663cea..4d779c76 100644 --- a/packages/ui/src/lib/i18n/messages/uk.ts +++ b/packages/ui/src/lib/i18n/messages/uk.ts @@ -2090,6 +2090,11 @@ export const dict: Record = { "chat.chatInput.toast.sendAttachmentsFailed": "Не вдалося надіслати вкладення. Спробуйте зменшити кількість файлів або зображень.", "chat.chatInput.toast.messageSendFailed": "Не вдалося надіслати повідомлення. Вкладення відновлено.", "chat.chatInput.toast.clipboardAttachFailed": "Не вдалося вкласти зображення з буфера обміну", + "chat.chatInput.toast.clipboardTextAttachFailed": "Не вдалося долучити вставлений текст як файл", + "chat.chatInput.toast.largeTextPaste.title": "Вставлення великого тексту", + "chat.chatInput.toast.largeTextPaste.description": "Долучіть як файл, щоб не захаращувати поле вводу, або вставте текст у повідомлення.", + "chat.chatInput.toast.largeTextPaste.attach": "Долучити як файл", + "chat.chatInput.toast.largeTextPaste.inline": "Вставити в повідомлення", "chat.chatInput.toast.addedFileMentions": "Додано згадки файлів {count}", "chat.chatInput.toast.attachFileFailed": "Не вдалося прикріпити файл", "chat.chatInput.toast.attachNamedFailed": "Не вдалося прикріпити {name}", 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 42aa7d0a..c6a54155 100644 --- a/packages/ui/src/lib/i18n/messages/zh-CN.settings.ts +++ b/packages/ui/src/lib/i18n/messages/zh-CN.settings.ts @@ -1940,6 +1940,13 @@ export const settingsDict = { 'settings.openchamber.visual.field.persistDraftMessages': '保留草稿消息', 'settings.openchamber.visual.field.enableSpellcheckInTextInputsAria': '在文本输入框启用拼写检查', 'settings.openchamber.visual.field.enableSpellcheckInTextInputs': '在文本输入框启用拼写检查', + 'settings.openchamber.visual.field.largeTextPaste': '粘贴大段文本', + 'settings.openchamber.visual.field.largeTextPasteHint': '粘贴超过约 2000 个字符或 25 行时,可选择附加为文件、直接粘贴到输入框,或每次询问。', + 'settings.openchamber.visual.field.largeTextPasteAria': '大段文本粘贴行为', + 'settings.openchamber.visual.field.largeTextPasteOptionAria': '大段文本粘贴:{option}', + 'settings.openchamber.visual.option.largeTextPaste.ask.label': '每次询问', + 'settings.openchamber.visual.option.largeTextPaste.attach.label': '附加为文件', + 'settings.openchamber.visual.option.largeTextPaste.inline.label': '直接粘贴', 'settings.openchamber.visual.field.sendAnonymousUsageReportsAria': '发送匿名使用报告', 'settings.openchamber.visual.field.sendAnonymousUsageReports': '发送匿名使用报告', 'settings.openchamber.visual.field.sendAnonymousUsageReportsHint': '帮助我们了解哪些应用版本正在被积极使用,以便优先改进。仅收集应用版本、平台和运行时信息,不收集个人数据或代码。', diff --git a/packages/ui/src/lib/i18n/messages/zh-CN.ts b/packages/ui/src/lib/i18n/messages/zh-CN.ts index 76ffac2f..77fdb126 100644 --- a/packages/ui/src/lib/i18n/messages/zh-CN.ts +++ b/packages/ui/src/lib/i18n/messages/zh-CN.ts @@ -2090,6 +2090,11 @@ export const dict: Record = { 'chat.chatInput.toast.sendAttachmentsFailed': '发送附件失败。请尝试更少文件或更小图片。', 'chat.chatInput.toast.messageSendFailed': '消息发送失败,附件已恢复。', 'chat.chatInput.toast.clipboardAttachFailed': '从剪贴板附加图片失败', + 'chat.chatInput.toast.clipboardTextAttachFailed': '无法将粘贴的文本附加为文件', + 'chat.chatInput.toast.largeTextPaste.title': '粘贴大段文本', + 'chat.chatInput.toast.largeTextPaste.description': '附加为文件以保持输入框简洁,或直接粘贴到输入框。', + 'chat.chatInput.toast.largeTextPaste.attach': '附加为文件', + 'chat.chatInput.toast.largeTextPaste.inline': '直接粘贴', 'chat.chatInput.toast.addedFileMentions': '已添加 {count} 个文件提及', 'chat.chatInput.toast.attachFileFailed': '附加文件失败', 'chat.chatInput.toast.attachNamedFailed': '附加 {name} 失败', 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 a1d814b0..258fe44c 100644 --- a/packages/ui/src/lib/i18n/messages/zh-TW.settings.ts +++ b/packages/ui/src/lib/i18n/messages/zh-TW.settings.ts @@ -1846,6 +1846,13 @@ 'settings.openchamber.visual.field.persistDraftMessages': '保留草稿訊息', 'settings.openchamber.visual.field.enableSpellcheckInTextInputsAria': '在文字輸入方塊啟用拼寫檢查', 'settings.openchamber.visual.field.enableSpellcheckInTextInputs': '在文字輸入方塊啟用拼寫檢查', + 'settings.openchamber.visual.field.largeTextPaste': '貼上大段文字', + 'settings.openchamber.visual.field.largeTextPasteHint': '貼上超過約 2000 個字元或 25 行時,可選擇附加為檔案、直接貼到輸入框,或每次詢問。', + 'settings.openchamber.visual.field.largeTextPasteAria': '大段文字貼上行為', + 'settings.openchamber.visual.field.largeTextPasteOptionAria': '大段文字貼上:{option}', + 'settings.openchamber.visual.option.largeTextPaste.ask.label': '每次詢問', + 'settings.openchamber.visual.option.largeTextPaste.attach.label': '附加為檔案', + 'settings.openchamber.visual.option.largeTextPaste.inline.label': '直接貼上', 'settings.openchamber.visual.field.sendAnonymousUsageReportsAria': '送出匿名使用報告', 'settings.openchamber.visual.field.sendAnonymousUsageReports': '送出匿名使用報告', 'settings.openchamber.visual.field.sendAnonymousUsageReportsHint': '協助我們了解哪些應用程式版本仍在被積極使用,以便優先改進。僅收集應用程式版本、平台與執行階段資訊,不收集個人資料或程式碼。', diff --git a/packages/ui/src/lib/i18n/messages/zh-TW.ts b/packages/ui/src/lib/i18n/messages/zh-TW.ts index 3538d59c..e9a4a2cd 100644 --- a/packages/ui/src/lib/i18n/messages/zh-TW.ts +++ b/packages/ui/src/lib/i18n/messages/zh-TW.ts @@ -2094,6 +2094,11 @@ export const dict: Record = { 'chat.chatInput.toast.sendAttachmentsFailed': '傳送附件失敗。請嘗試更少檔案或更小圖片。', 'chat.chatInput.toast.messageSendFailed': '訊息傳送失敗,附件已恢復。', 'chat.chatInput.toast.clipboardAttachFailed': '從剪貼簿附加圖片失敗', + 'chat.chatInput.toast.clipboardTextAttachFailed': '無法將貼上的文字附加為檔案', + 'chat.chatInput.toast.largeTextPaste.title': '貼上大段文字', + 'chat.chatInput.toast.largeTextPaste.description': '附加為檔案以保持輸入框簡潔,或直接貼到輸入框。', + 'chat.chatInput.toast.largeTextPaste.attach': '附加為檔案', + 'chat.chatInput.toast.largeTextPaste.inline': '直接貼上', 'chat.chatInput.toast.addedFileMentions': '已加入 {count} 個檔案提及', 'chat.chatInput.toast.attachFileFailed': '附加檔案失敗', 'chat.chatInput.toast.attachNamedFailed': '附加 {name} 失敗', diff --git a/packages/ui/src/lib/settings/search.ts b/packages/ui/src/lib/settings/search.ts index eecca22c..5c8980e3 100644 --- a/packages/ui/src/lib/settings/search.ts +++ b/packages/ui/src/lib/settings/search.ts @@ -338,7 +338,7 @@ const SETTINGS_SEARCH_ITEMS: readonly SettingsSearchItem[] = [ id: 'chat.composer', page: 'chat', titleKey: 'settings.openchamber.visual.section.composer', - keywords: ['input', 'draft', 'spellcheck'], + keywords: ['input', 'draft', 'spellcheck', 'paste'], }, { id: 'chat.spellcheck', @@ -347,6 +347,13 @@ const SETTINGS_SEARCH_ITEMS: readonly SettingsSearchItem[] = [ keywords: ['spelling', 'input'], isAvailable: (ctx) => !ctx.isMobile, }, + { + id: 'chat.large-text-paste', + page: 'chat', + titleKey: 'settings.openchamber.visual.field.largeTextPaste', + descriptionKey: 'settings.openchamber.visual.field.largeTextPasteHint', + keywords: ['paste', 'clipboard', 'attachment', 'large', 'text', 'file'], + }, { id: 'sessions.default-model', page: 'sessions', diff --git a/packages/ui/src/stores/useUIStore.ts b/packages/ui/src/stores/useUIStore.ts index d00bb6ff..7a69bc77 100644 --- a/packages/ui/src/stores/useUIStore.ts +++ b/packages/ui/src/stores/useUIStore.ts @@ -25,6 +25,16 @@ export type WeekStartPreference = 'auto' | 'sunday' | 'monday'; export type DesktopWindowControlsPosition = 'left' | 'right'; export type DesktopWindowControlsStyle = 'classic' | 'traffic-lights'; export type FileEditorKeymap = 'default' | 'vim'; +export type LargeTextPasteBehavior = 'ask' | 'attach' | 'inline'; + +export const DEFAULT_LARGE_TEXT_PASTE_BEHAVIOR: LargeTextPasteBehavior = 'ask'; + +export const normalizeLargeTextPasteBehavior = (value: unknown): LargeTextPasteBehavior => { + if (value === 'attach' || value === 'inline' || value === 'ask') { + return value; + } + return DEFAULT_LARGE_TEXT_PASTE_BEHAVIOR; +}; function normalizeFileEditorKeymap(value: unknown): FileEditorKeymap { return value === 'vim' ? 'vim' : 'default'; @@ -690,6 +700,7 @@ interface UIStore { showOpenCodeUpdateNotifications: boolean; agentControlToolEnabled: boolean; inputSpellcheckEnabled: boolean; + largeTextPasteBehavior: LargeTextPasteBehavior; wideChatLayoutEnabled: boolean; codeBlockLineWrap: boolean; showToolFileIcons: boolean; @@ -851,6 +862,7 @@ interface UIStore { setShowOpenCodeUpdateNotifications: (value: boolean) => void; setAgentControlToolEnabled: (value: boolean) => void; setInputSpellcheckEnabled: (value: boolean) => void; + setLargeTextPasteBehavior: (value: LargeTextPasteBehavior) => void; setWideChatLayoutEnabled: (value: boolean) => void; setCodeBlockLineWrap: (value: boolean) => void; setShowToolFileIcons: (value: boolean) => void; @@ -1001,6 +1013,7 @@ export const useUIStore = create()( showOpenCodeUpdateNotifications: !isWindowsArm64(), agentControlToolEnabled: true, inputSpellcheckEnabled: false, + largeTextPasteBehavior: DEFAULT_LARGE_TEXT_PASTE_BEHAVIOR, wideChatLayoutEnabled: false, codeBlockLineWrap: true, showToolFileIcons: true, @@ -2152,6 +2165,9 @@ export const useUIStore = create()( setInputSpellcheckEnabled: (value) => { set({ inputSpellcheckEnabled: value }); }, + setLargeTextPasteBehavior: (value) => { + set({ largeTextPasteBehavior: normalizeLargeTextPasteBehavior(value) }); + }, setWideChatLayoutEnabled: (value) => { set({ wideChatLayoutEnabled: value }); }, @@ -2377,6 +2393,7 @@ export const useUIStore = create()( } state.fileEditorKeymap = normalizeFileEditorKeymap(state.fileEditorKeymap); + state.largeTextPasteBehavior = normalizeLargeTextPasteBehavior(state.largeTextPasteBehavior); if (typeof state.autoSaveEnabled !== 'boolean') { state.autoSaveEnabled = true; @@ -2461,6 +2478,7 @@ export const useUIStore = create()( showOpenCodeUpdateNotifications: state.showOpenCodeUpdateNotifications, agentControlToolEnabled: state.agentControlToolEnabled, inputSpellcheckEnabled: state.inputSpellcheckEnabled, + largeTextPasteBehavior: state.largeTextPasteBehavior, wideChatLayoutEnabled: state.wideChatLayoutEnabled, codeBlockLineWrap: state.codeBlockLineWrap, showToolFileIcons: state.showToolFileIcons, diff --git a/packages/ui/src/sync/DOCUMENTATION.md b/packages/ui/src/sync/DOCUMENTATION.md index cc9b4abb..a8f96b13 100644 --- a/packages/ui/src/sync/DOCUMENTATION.md +++ b/packages/ui/src/sync/DOCUMENTATION.md @@ -54,7 +54,7 @@ So: | `selection-store.ts` | Model/agent/variant selections | App UI state | | `voice-store.ts` | Voice state | App UI state | -Local chat attachments are normalized by `attachment-files.ts` before entering `input-store.ts`. PNG, JPEG, GIF, WebP, and PDF retain their media type; HEIC/HEIF is converted to JPEG; recognized text/code formats and unknown files whose first 4 KB are text are sent as `text/plain`; binary files outside the supported media types are rejected. Jupyter notebooks become readable markdown with non-text outputs omitted. HAR credentials, cookies, and sensitive URL parameters are redacted, while request/response body text is omitted. SVG and Draw.io files are attached as source text, not executable/rendered content. Browser and VS Code pickers expose the same allowlist, while drag-and-drop may still accept an unknown extension after content inspection. +Local chat attachments are normalized by `attachment-files.ts` before entering `input-store.ts`. PNG, JPEG, GIF, WebP, and PDF retain their media type; HEIC/HEIF is converted to JPEG; recognized text/code formats and unknown files whose first 4 KB are text are sent as `text/plain`; binary files outside the supported media types are rejected. Jupyter notebooks become readable markdown with non-text outputs omitted. HAR credentials, cookies, and sensitive URL parameters are redacted, while request/response body text is omitted. SVG and Draw.io files are attached as source text, not executable/rendered content. Browser and VS Code pickers expose the same allowlist, while drag-and-drop may still accept an unknown extension after content inspection. Large plain-text clipboard pastes can become in-memory `text/plain` attachments named `pasted-context-N.txt` through the composer paste path; they use the same normalization and send pipeline as manually attached `.txt` files. Office and OpenDocument packages are metadata-validated before asynchronous extraction, with limits of 20 MB compressed input, 5,000 archive entries, 25 MB per entry, 8 MB per XML part, and 100 MB total uncompressed content. Unsafe or non-canonical archive paths reject the whole attachment, and only XML, relationship, and supported image entries are decompressed and retained. Extracted text, including its explicit truncation notice, is bounded to 2,000,000 characters. At most 50 signature-validated PNG, JPEG, GIF, or WebP images and 40 MB of image bytes are retained, with a 20 MB per-image limit; unsupported, invalid, omitted, and truncated content remains explicit in the extracted text. Images whose citations fall beyond text truncation are not attached. Extracted document content remains a `text/plain` file attachment with the original document filename, rather than becoming visible user-message text. Supported embedded images become separate image file parts; the extracted text contains `[filename]` citations at the source paragraph, slide object, spreadsheet cell anchor, or OpenDocument text position. Generated image filenames are re-evaluated if the composer changes during asynchronous preparation, avoiding collisions. The store publishes all generated parts atomically only after every data URL is ready. From 471d0a8ecb005f37983ecdb979b8b33333969b8e Mon Sep 17 00:00:00 2001 From: Cursor Agent Date: Tue, 4 Aug 2026 11:57:38 +0000 Subject: [PATCH 2/5] fix(ui): neaten large text paste toast Drop the grey description and info icon, and widen the toast so the title and two actions sit on one clean row. Co-authored-by: Serhii Dziupin --- packages/ui/src/components/chat/ChatInput.tsx | 2 +- packages/ui/src/lib/i18n/messages/de.ts | 1 - packages/ui/src/lib/i18n/messages/en.ts | 1 - packages/ui/src/lib/i18n/messages/es.ts | 1 - packages/ui/src/lib/i18n/messages/fr.ts | 1 - packages/ui/src/lib/i18n/messages/ja.ts | 1 - packages/ui/src/lib/i18n/messages/ko.ts | 1 - packages/ui/src/lib/i18n/messages/pl.ts | 1 - packages/ui/src/lib/i18n/messages/pt-BR.ts | 1 - packages/ui/src/lib/i18n/messages/uk.ts | 1 - packages/ui/src/lib/i18n/messages/zh-CN.ts | 1 - packages/ui/src/lib/i18n/messages/zh-TW.ts | 1 - 12 files changed, 1 insertion(+), 12 deletions(-) diff --git a/packages/ui/src/components/chat/ChatInput.tsx b/packages/ui/src/components/chat/ChatInput.tsx index cc4c6d08..b5dee769 100644 --- a/packages/ui/src/components/chat/ChatInput.tsx +++ b/packages/ui/src/components/chat/ChatInput.tsx @@ -1829,8 +1829,8 @@ const ChatInputComponent: React.FC = ({ onOpenSettings, scrollTo largeTextPasteToastIdRef.current = toast.info( t('chat.chatInput.toast.largeTextPaste.title'), { - description: t('chat.chatInput.toast.largeTextPaste.description'), duration: Infinity, + className: '!min-w-[22rem] !w-auto [&_[data-icon]]:!hidden', action: { label: t('chat.chatInput.toast.largeTextPaste.attach'), onClick: () => resolveLargePaste('attach'), diff --git a/packages/ui/src/lib/i18n/messages/de.ts b/packages/ui/src/lib/i18n/messages/de.ts index 679c1812..9bcdd585 100644 --- a/packages/ui/src/lib/i18n/messages/de.ts +++ b/packages/ui/src/lib/i18n/messages/de.ts @@ -1965,7 +1965,6 @@ export const dict = { 'chat.chatInput.toast.clipboardAttachFailed': 'Fehler beim Anhängen des Bildes aus der Zwischenablage', 'chat.chatInput.toast.clipboardTextAttachFailed': 'Fehler beim Anhängen des eingefügten Texts als Datei', 'chat.chatInput.toast.largeTextPaste.title': 'Großes Texteinfügen', - 'chat.chatInput.toast.largeTextPaste.description': 'Als Datei anhängen, um das Eingabefeld übersichtlich zu halten, oder den Text direkt einfügen.', 'chat.chatInput.toast.largeTextPaste.attach': 'Als Datei anhängen', 'chat.chatInput.toast.largeTextPaste.inline': 'Direkt einfügen', 'chat.chatInput.toast.addedFileMentions': '{count} Datei(er) hinzugefügt', diff --git a/packages/ui/src/lib/i18n/messages/en.ts b/packages/ui/src/lib/i18n/messages/en.ts index 9953d07d..22c51d6e 100644 --- a/packages/ui/src/lib/i18n/messages/en.ts +++ b/packages/ui/src/lib/i18n/messages/en.ts @@ -2126,7 +2126,6 @@ export const dict = { 'chat.chatInput.toast.clipboardAttachFailed': 'Failed to attach image from clipboard', 'chat.chatInput.toast.clipboardTextAttachFailed': 'Failed to attach pasted text as a file', 'chat.chatInput.toast.largeTextPaste.title': 'Large text paste', - 'chat.chatInput.toast.largeTextPaste.description': 'Attach as a file to keep the composer clear, or paste the text inline.', 'chat.chatInput.toast.largeTextPaste.attach': 'Attach as file', 'chat.chatInput.toast.largeTextPaste.inline': 'Paste inline', 'chat.chatInput.toast.addedFileMentions': 'Added {count} file mention(s)', diff --git a/packages/ui/src/lib/i18n/messages/es.ts b/packages/ui/src/lib/i18n/messages/es.ts index a4e2ea13..a45604e5 100644 --- a/packages/ui/src/lib/i18n/messages/es.ts +++ b/packages/ui/src/lib/i18n/messages/es.ts @@ -2092,7 +2092,6 @@ export const dict: Record = { "chat.chatInput.toast.clipboardAttachFailed": "No se pudo adjuntar la imagen desde el portapapeles", "chat.chatInput.toast.clipboardTextAttachFailed": "No se pudo adjuntar el texto pegado como archivo", "chat.chatInput.toast.largeTextPaste.title": "Pegado de texto grande", - "chat.chatInput.toast.largeTextPaste.description": "Adjunta como archivo para mantener el compositor despejado, o pega el texto en línea.", "chat.chatInput.toast.largeTextPaste.attach": "Adjuntar como archivo", "chat.chatInput.toast.largeTextPaste.inline": "Pegar en línea", "chat.chatInput.toast.addedFileMentions": "Se añadieron {count} mención(es) de archivo", diff --git a/packages/ui/src/lib/i18n/messages/fr.ts b/packages/ui/src/lib/i18n/messages/fr.ts index a56f5434..7b5d5ed4 100644 --- a/packages/ui/src/lib/i18n/messages/fr.ts +++ b/packages/ui/src/lib/i18n/messages/fr.ts @@ -1895,7 +1895,6 @@ export const dict = { 'chat.chatInput.toast.clipboardAttachFailed': 'Échec de la pièce jointe de l\'image du presse-papiers', 'chat.chatInput.toast.clipboardTextAttachFailed': 'Échec de la pièce jointe du texte collé comme fichier', 'chat.chatInput.toast.largeTextPaste.title': 'Collage de texte volumineux', - 'chat.chatInput.toast.largeTextPaste.description': 'Joindre comme fichier pour garder la zone de saisie claire, ou coller le texte en ligne.', 'chat.chatInput.toast.largeTextPaste.attach': 'Joindre comme fichier', 'chat.chatInput.toast.largeTextPaste.inline': 'Coller en ligne', 'chat.chatInput.toast.addedFileMentions': 'Ajout des mentions du fichier {count}', diff --git a/packages/ui/src/lib/i18n/messages/ja.ts b/packages/ui/src/lib/i18n/messages/ja.ts index a8e8e4b9..b3bf1ccd 100644 --- a/packages/ui/src/lib/i18n/messages/ja.ts +++ b/packages/ui/src/lib/i18n/messages/ja.ts @@ -2122,7 +2122,6 @@ export const dict: Record = { 'chat.chatInput.toast.clipboardAttachFailed': 'クリップボードからの画像添付に失敗しました', 'chat.chatInput.toast.clipboardTextAttachFailed': '貼り付けたテキストのファイル添付に失敗しました', 'chat.chatInput.toast.largeTextPaste.title': '大きなテキストの貼り付け', - 'chat.chatInput.toast.largeTextPaste.description': '入力欄をすっきり保つためにファイルとして添付するか、そのまま貼り付けます。', 'chat.chatInput.toast.largeTextPaste.attach': 'ファイルとして添付', 'chat.chatInput.toast.largeTextPaste.inline': 'そのまま貼り付け', 'chat.chatInput.toast.addedFileMentions': '{count}件のファイルメンションを追加しました', diff --git a/packages/ui/src/lib/i18n/messages/ko.ts b/packages/ui/src/lib/i18n/messages/ko.ts index 5794969b..3fe5a80c 100644 --- a/packages/ui/src/lib/i18n/messages/ko.ts +++ b/packages/ui/src/lib/i18n/messages/ko.ts @@ -2126,7 +2126,6 @@ export const dict: Record = { 'chat.chatInput.toast.clipboardAttachFailed': '클립보드 이미지 첨부 실패', 'chat.chatInput.toast.clipboardTextAttachFailed': '붙여넣은 텍스트를 파일로 첨부하지 못했습니다', 'chat.chatInput.toast.largeTextPaste.title': '긴 텍스트 붙여넣기', - 'chat.chatInput.toast.largeTextPaste.description': '입력창을 깔끔하게 유지하려면 파일로 첨부하거나, 본문에 붙여넣으세요.', 'chat.chatInput.toast.largeTextPaste.attach': '파일로 첨부', 'chat.chatInput.toast.largeTextPaste.inline': '본문에 붙여넣기', 'chat.chatInput.toast.addedFileMentions': '파일 멘션 {count}개 추가됨', diff --git a/packages/ui/src/lib/i18n/messages/pl.ts b/packages/ui/src/lib/i18n/messages/pl.ts index fe6bad3e..057b86b7 100644 --- a/packages/ui/src/lib/i18n/messages/pl.ts +++ b/packages/ui/src/lib/i18n/messages/pl.ts @@ -1223,7 +1223,6 @@ export const dict: Record = { 'chat.chatInput.toast.clipboardAttachFailed': 'Nie udało się dołączyć obrazu ze schowka', 'chat.chatInput.toast.clipboardTextAttachFailed': 'Nie udało się dołączyć wklejonego tekstu jako pliku', 'chat.chatInput.toast.largeTextPaste.title': 'Wklejanie dużego tekstu', - 'chat.chatInput.toast.largeTextPaste.description': 'Dołącz jako plik, aby nie zaśmiecać pola wiadomości, albo wklej tekst w treści.', 'chat.chatInput.toast.largeTextPaste.attach': 'Dołącz jako plik', 'chat.chatInput.toast.largeTextPaste.inline': 'Wklej w treści', 'chat.chatInput.toast.compactFailed': 'Nie udało się skompaktować sesji', diff --git a/packages/ui/src/lib/i18n/messages/pt-BR.ts b/packages/ui/src/lib/i18n/messages/pt-BR.ts index 3ad5cb36..9116573a 100644 --- a/packages/ui/src/lib/i18n/messages/pt-BR.ts +++ b/packages/ui/src/lib/i18n/messages/pt-BR.ts @@ -2092,7 +2092,6 @@ export const dict: Record = { "chat.chatInput.toast.clipboardAttachFailed": "Não foi possível anexar a imagem da área de transferência", "chat.chatInput.toast.clipboardTextAttachFailed": "Não foi possível anexar o texto colado como arquivo", "chat.chatInput.toast.largeTextPaste.title": "Colagem de texto grande", - "chat.chatInput.toast.largeTextPaste.description": "Anexe como arquivo para manter o compositor limpo, ou cole o texto no corpo da mensagem.", "chat.chatInput.toast.largeTextPaste.attach": "Anexar como arquivo", "chat.chatInput.toast.largeTextPaste.inline": "Colar no corpo", "chat.chatInput.toast.addedFileMentions": "Foram adicionadas {count} menção(es) de arquivo", diff --git a/packages/ui/src/lib/i18n/messages/uk.ts b/packages/ui/src/lib/i18n/messages/uk.ts index 4d779c76..2b93fa06 100644 --- a/packages/ui/src/lib/i18n/messages/uk.ts +++ b/packages/ui/src/lib/i18n/messages/uk.ts @@ -2092,7 +2092,6 @@ export const dict: Record = { "chat.chatInput.toast.clipboardAttachFailed": "Не вдалося вкласти зображення з буфера обміну", "chat.chatInput.toast.clipboardTextAttachFailed": "Не вдалося долучити вставлений текст як файл", "chat.chatInput.toast.largeTextPaste.title": "Вставлення великого тексту", - "chat.chatInput.toast.largeTextPaste.description": "Долучіть як файл, щоб не захаращувати поле вводу, або вставте текст у повідомлення.", "chat.chatInput.toast.largeTextPaste.attach": "Долучити як файл", "chat.chatInput.toast.largeTextPaste.inline": "Вставити в повідомлення", "chat.chatInput.toast.addedFileMentions": "Додано згадки файлів {count}", diff --git a/packages/ui/src/lib/i18n/messages/zh-CN.ts b/packages/ui/src/lib/i18n/messages/zh-CN.ts index 77fdb126..58cc1898 100644 --- a/packages/ui/src/lib/i18n/messages/zh-CN.ts +++ b/packages/ui/src/lib/i18n/messages/zh-CN.ts @@ -2092,7 +2092,6 @@ export const dict: Record = { 'chat.chatInput.toast.clipboardAttachFailed': '从剪贴板附加图片失败', 'chat.chatInput.toast.clipboardTextAttachFailed': '无法将粘贴的文本附加为文件', 'chat.chatInput.toast.largeTextPaste.title': '粘贴大段文本', - 'chat.chatInput.toast.largeTextPaste.description': '附加为文件以保持输入框简洁,或直接粘贴到输入框。', 'chat.chatInput.toast.largeTextPaste.attach': '附加为文件', 'chat.chatInput.toast.largeTextPaste.inline': '直接粘贴', 'chat.chatInput.toast.addedFileMentions': '已添加 {count} 个文件提及', diff --git a/packages/ui/src/lib/i18n/messages/zh-TW.ts b/packages/ui/src/lib/i18n/messages/zh-TW.ts index e9a4a2cd..8753d3bc 100644 --- a/packages/ui/src/lib/i18n/messages/zh-TW.ts +++ b/packages/ui/src/lib/i18n/messages/zh-TW.ts @@ -2096,7 +2096,6 @@ export const dict: Record = { 'chat.chatInput.toast.clipboardAttachFailed': '從剪貼簿附加圖片失敗', 'chat.chatInput.toast.clipboardTextAttachFailed': '無法將貼上的文字附加為檔案', 'chat.chatInput.toast.largeTextPaste.title': '貼上大段文字', - 'chat.chatInput.toast.largeTextPaste.description': '附加為檔案以保持輸入框簡潔,或直接貼到輸入框。', 'chat.chatInput.toast.largeTextPaste.attach': '附加為檔案', 'chat.chatInput.toast.largeTextPaste.inline': '直接貼上', 'chat.chatInput.toast.addedFileMentions': '已加入 {count} 個檔案提及', From 29420a7e7e74445e165b8c739473212633655429 Mon Sep 17 00:00:00 2001 From: Cursor Agent Date: Tue, 4 Aug 2026 12:15:01 +0000 Subject: [PATCH 3/5] fix(ui): rename large paste toast to Large text detected Use clearer toast title copy across all locales. Co-authored-by: Serhii Dziupin --- packages/ui/src/lib/i18n/messages/de.ts | 2 +- packages/ui/src/lib/i18n/messages/en.ts | 2 +- packages/ui/src/lib/i18n/messages/es.ts | 2 +- packages/ui/src/lib/i18n/messages/fr.ts | 2 +- packages/ui/src/lib/i18n/messages/ja.ts | 2 +- packages/ui/src/lib/i18n/messages/ko.ts | 2 +- packages/ui/src/lib/i18n/messages/pl.ts | 2 +- packages/ui/src/lib/i18n/messages/pt-BR.ts | 2 +- packages/ui/src/lib/i18n/messages/uk.ts | 2 +- packages/ui/src/lib/i18n/messages/zh-CN.ts | 2 +- packages/ui/src/lib/i18n/messages/zh-TW.ts | 2 +- 11 files changed, 11 insertions(+), 11 deletions(-) diff --git a/packages/ui/src/lib/i18n/messages/de.ts b/packages/ui/src/lib/i18n/messages/de.ts index 9bcdd585..293e3225 100644 --- a/packages/ui/src/lib/i18n/messages/de.ts +++ b/packages/ui/src/lib/i18n/messages/de.ts @@ -1964,7 +1964,7 @@ export const dict = { 'chat.chatInput.toast.messageSendFailed': 'Nachricht konnte nicht gesendet werden. Anhänge wurden wiederhergestellt.', 'chat.chatInput.toast.clipboardAttachFailed': 'Fehler beim Anhängen des Bildes aus der Zwischenablage', 'chat.chatInput.toast.clipboardTextAttachFailed': 'Fehler beim Anhängen des eingefügten Texts als Datei', - 'chat.chatInput.toast.largeTextPaste.title': 'Großes Texteinfügen', + 'chat.chatInput.toast.largeTextPaste.title': 'Großer Text erkannt', 'chat.chatInput.toast.largeTextPaste.attach': 'Als Datei anhängen', 'chat.chatInput.toast.largeTextPaste.inline': 'Direkt einfügen', 'chat.chatInput.toast.addedFileMentions': '{count} Datei(er) hinzugefügt', diff --git a/packages/ui/src/lib/i18n/messages/en.ts b/packages/ui/src/lib/i18n/messages/en.ts index 22c51d6e..84a8f38d 100644 --- a/packages/ui/src/lib/i18n/messages/en.ts +++ b/packages/ui/src/lib/i18n/messages/en.ts @@ -2125,7 +2125,7 @@ export const dict = { 'chat.chatInput.toast.messageSendFailed': 'Message failed to send. Attachments restored.', 'chat.chatInput.toast.clipboardAttachFailed': 'Failed to attach image from clipboard', 'chat.chatInput.toast.clipboardTextAttachFailed': 'Failed to attach pasted text as a file', - 'chat.chatInput.toast.largeTextPaste.title': 'Large text paste', + 'chat.chatInput.toast.largeTextPaste.title': 'Large text detected', 'chat.chatInput.toast.largeTextPaste.attach': 'Attach as file', 'chat.chatInput.toast.largeTextPaste.inline': 'Paste inline', 'chat.chatInput.toast.addedFileMentions': 'Added {count} file mention(s)', diff --git a/packages/ui/src/lib/i18n/messages/es.ts b/packages/ui/src/lib/i18n/messages/es.ts index a45604e5..7099dd5a 100644 --- a/packages/ui/src/lib/i18n/messages/es.ts +++ b/packages/ui/src/lib/i18n/messages/es.ts @@ -2091,7 +2091,7 @@ export const dict: Record = { "chat.chatInput.toast.messageSendFailed": "El mensaje no se pudo enviar. Los adjuntos se restauraron.", "chat.chatInput.toast.clipboardAttachFailed": "No se pudo adjuntar la imagen desde el portapapeles", "chat.chatInput.toast.clipboardTextAttachFailed": "No se pudo adjuntar el texto pegado como archivo", - "chat.chatInput.toast.largeTextPaste.title": "Pegado de texto grande", + "chat.chatInput.toast.largeTextPaste.title": "Texto grande detectado", "chat.chatInput.toast.largeTextPaste.attach": "Adjuntar como archivo", "chat.chatInput.toast.largeTextPaste.inline": "Pegar en línea", "chat.chatInput.toast.addedFileMentions": "Se añadieron {count} mención(es) de archivo", diff --git a/packages/ui/src/lib/i18n/messages/fr.ts b/packages/ui/src/lib/i18n/messages/fr.ts index 7b5d5ed4..badf21ec 100644 --- a/packages/ui/src/lib/i18n/messages/fr.ts +++ b/packages/ui/src/lib/i18n/messages/fr.ts @@ -1894,7 +1894,7 @@ export const dict = { 'chat.chatInput.toast.messageSendFailed': 'Le message n\'a pas pu être envoyé. Pièces jointes restaurées.', 'chat.chatInput.toast.clipboardAttachFailed': 'Échec de la pièce jointe de l\'image du presse-papiers', 'chat.chatInput.toast.clipboardTextAttachFailed': 'Échec de la pièce jointe du texte collé comme fichier', - 'chat.chatInput.toast.largeTextPaste.title': 'Collage de texte volumineux', + 'chat.chatInput.toast.largeTextPaste.title': 'Texte volumineux détecté', 'chat.chatInput.toast.largeTextPaste.attach': 'Joindre comme fichier', 'chat.chatInput.toast.largeTextPaste.inline': 'Coller en ligne', 'chat.chatInput.toast.addedFileMentions': 'Ajout des mentions du fichier {count}', diff --git a/packages/ui/src/lib/i18n/messages/ja.ts b/packages/ui/src/lib/i18n/messages/ja.ts index b3bf1ccd..c07f604f 100644 --- a/packages/ui/src/lib/i18n/messages/ja.ts +++ b/packages/ui/src/lib/i18n/messages/ja.ts @@ -2121,7 +2121,7 @@ export const dict: Record = { 'chat.chatInput.toast.messageSendFailed': 'メッセージの送信に失敗しました。添付ファイルは復元されました。', 'chat.chatInput.toast.clipboardAttachFailed': 'クリップボードからの画像添付に失敗しました', 'chat.chatInput.toast.clipboardTextAttachFailed': '貼り付けたテキストのファイル添付に失敗しました', - 'chat.chatInput.toast.largeTextPaste.title': '大きなテキストの貼り付け', + 'chat.chatInput.toast.largeTextPaste.title': '大きなテキストを検出', 'chat.chatInput.toast.largeTextPaste.attach': 'ファイルとして添付', 'chat.chatInput.toast.largeTextPaste.inline': 'そのまま貼り付け', 'chat.chatInput.toast.addedFileMentions': '{count}件のファイルメンションを追加しました', diff --git a/packages/ui/src/lib/i18n/messages/ko.ts b/packages/ui/src/lib/i18n/messages/ko.ts index 3fe5a80c..32787b7d 100644 --- a/packages/ui/src/lib/i18n/messages/ko.ts +++ b/packages/ui/src/lib/i18n/messages/ko.ts @@ -2125,7 +2125,7 @@ export const dict: Record = { 'chat.chatInput.toast.messageSendFailed': '메시지 전송에 실패했습니다. 첨부 파일을 복원했습니다.', 'chat.chatInput.toast.clipboardAttachFailed': '클립보드 이미지 첨부 실패', 'chat.chatInput.toast.clipboardTextAttachFailed': '붙여넣은 텍스트를 파일로 첨부하지 못했습니다', - 'chat.chatInput.toast.largeTextPaste.title': '긴 텍스트 붙여넣기', + 'chat.chatInput.toast.largeTextPaste.title': '긴 텍스트 감지됨', 'chat.chatInput.toast.largeTextPaste.attach': '파일로 첨부', 'chat.chatInput.toast.largeTextPaste.inline': '본문에 붙여넣기', 'chat.chatInput.toast.addedFileMentions': '파일 멘션 {count}개 추가됨', diff --git a/packages/ui/src/lib/i18n/messages/pl.ts b/packages/ui/src/lib/i18n/messages/pl.ts index 057b86b7..a1ebaf3d 100644 --- a/packages/ui/src/lib/i18n/messages/pl.ts +++ b/packages/ui/src/lib/i18n/messages/pl.ts @@ -1222,7 +1222,7 @@ export const dict: Record = { 'chat.chatInput.toast.attachmentsTooLarge': 'Załączniki są zbyt duże, aby je wysłać. Spróbuj zmniejszyć liczbę lub rozmiar obrazów.', 'chat.chatInput.toast.clipboardAttachFailed': 'Nie udało się dołączyć obrazu ze schowka', 'chat.chatInput.toast.clipboardTextAttachFailed': 'Nie udało się dołączyć wklejonego tekstu jako pliku', - 'chat.chatInput.toast.largeTextPaste.title': 'Wklejanie dużego tekstu', + 'chat.chatInput.toast.largeTextPaste.title': 'Wykryto duży tekst', 'chat.chatInput.toast.largeTextPaste.attach': 'Dołącz jako plik', 'chat.chatInput.toast.largeTextPaste.inline': 'Wklej w treści', 'chat.chatInput.toast.compactFailed': 'Nie udało się skompaktować sesji', diff --git a/packages/ui/src/lib/i18n/messages/pt-BR.ts b/packages/ui/src/lib/i18n/messages/pt-BR.ts index 9116573a..72aa9589 100644 --- a/packages/ui/src/lib/i18n/messages/pt-BR.ts +++ b/packages/ui/src/lib/i18n/messages/pt-BR.ts @@ -2091,7 +2091,7 @@ export const dict: Record = { "chat.chatInput.toast.messageSendFailed": "A mensagem não pôde ser enviada. Os anexos foram restaurados.", "chat.chatInput.toast.clipboardAttachFailed": "Não foi possível anexar a imagem da área de transferência", "chat.chatInput.toast.clipboardTextAttachFailed": "Não foi possível anexar o texto colado como arquivo", - "chat.chatInput.toast.largeTextPaste.title": "Colagem de texto grande", + "chat.chatInput.toast.largeTextPaste.title": "Texto grande detectado", "chat.chatInput.toast.largeTextPaste.attach": "Anexar como arquivo", "chat.chatInput.toast.largeTextPaste.inline": "Colar no corpo", "chat.chatInput.toast.addedFileMentions": "Foram adicionadas {count} menção(es) de arquivo", diff --git a/packages/ui/src/lib/i18n/messages/uk.ts b/packages/ui/src/lib/i18n/messages/uk.ts index 2b93fa06..6f25914b 100644 --- a/packages/ui/src/lib/i18n/messages/uk.ts +++ b/packages/ui/src/lib/i18n/messages/uk.ts @@ -2091,7 +2091,7 @@ export const dict: Record = { "chat.chatInput.toast.messageSendFailed": "Не вдалося надіслати повідомлення. Вкладення відновлено.", "chat.chatInput.toast.clipboardAttachFailed": "Не вдалося вкласти зображення з буфера обміну", "chat.chatInput.toast.clipboardTextAttachFailed": "Не вдалося долучити вставлений текст як файл", - "chat.chatInput.toast.largeTextPaste.title": "Вставлення великого тексту", + "chat.chatInput.toast.largeTextPaste.title": "Виявлено великий текст", "chat.chatInput.toast.largeTextPaste.attach": "Долучити як файл", "chat.chatInput.toast.largeTextPaste.inline": "Вставити в повідомлення", "chat.chatInput.toast.addedFileMentions": "Додано згадки файлів {count}", diff --git a/packages/ui/src/lib/i18n/messages/zh-CN.ts b/packages/ui/src/lib/i18n/messages/zh-CN.ts index 58cc1898..bd1bcc72 100644 --- a/packages/ui/src/lib/i18n/messages/zh-CN.ts +++ b/packages/ui/src/lib/i18n/messages/zh-CN.ts @@ -2091,7 +2091,7 @@ export const dict: Record = { 'chat.chatInput.toast.messageSendFailed': '消息发送失败,附件已恢复。', 'chat.chatInput.toast.clipboardAttachFailed': '从剪贴板附加图片失败', 'chat.chatInput.toast.clipboardTextAttachFailed': '无法将粘贴的文本附加为文件', - 'chat.chatInput.toast.largeTextPaste.title': '粘贴大段文本', + 'chat.chatInput.toast.largeTextPaste.title': '检测到大段文本', 'chat.chatInput.toast.largeTextPaste.attach': '附加为文件', 'chat.chatInput.toast.largeTextPaste.inline': '直接粘贴', 'chat.chatInput.toast.addedFileMentions': '已添加 {count} 个文件提及', diff --git a/packages/ui/src/lib/i18n/messages/zh-TW.ts b/packages/ui/src/lib/i18n/messages/zh-TW.ts index 8753d3bc..ad054f0d 100644 --- a/packages/ui/src/lib/i18n/messages/zh-TW.ts +++ b/packages/ui/src/lib/i18n/messages/zh-TW.ts @@ -2095,7 +2095,7 @@ export const dict: Record = { 'chat.chatInput.toast.messageSendFailed': '訊息傳送失敗,附件已恢復。', 'chat.chatInput.toast.clipboardAttachFailed': '從剪貼簿附加圖片失敗', 'chat.chatInput.toast.clipboardTextAttachFailed': '無法將貼上的文字附加為檔案', - 'chat.chatInput.toast.largeTextPaste.title': '貼上大段文字', + 'chat.chatInput.toast.largeTextPaste.title': '偵測到大段文字', 'chat.chatInput.toast.largeTextPaste.attach': '附加為檔案', 'chat.chatInput.toast.largeTextPaste.inline': '直接貼上', 'chat.chatInput.toast.addedFileMentions': '已加入 {count} 個檔案提及', From d26ff65c5849229001d740cf031dcc82252f3292 Mon Sep 17 00:00:00 2001 From: Cursor Agent Date: Tue, 4 Aug 2026 13:18:56 +0000 Subject: [PATCH 4/5] fix(ui): harden large-paste toast for mobile and stale state Scope toast width overrides to sm+ so Sonner keeps full-width mobile toasts, resolve ask actions from live composer/attachment state, and extract offer-id invalidation into a unit-tested helper. Co-authored-by: Serhii Dziupin --- packages/ui/src/components/chat/ChatInput.tsx | 43 +++++++++----- .../components/chat/composer/DOCUMENTATION.md | 10 ++-- .../__tests__/largeTextPasteOffer.test.ts | 56 +++++++++++++++++++ .../chat/composer/largeTextPasteOffer.ts | 33 +++++++++++ 4 files changed, 124 insertions(+), 18 deletions(-) create mode 100644 packages/ui/src/components/chat/composer/__tests__/largeTextPasteOffer.test.ts create mode 100644 packages/ui/src/components/chat/composer/largeTextPasteOffer.ts diff --git a/packages/ui/src/components/chat/ChatInput.tsx b/packages/ui/src/components/chat/ChatInput.tsx index b5dee769..4f4cde5c 100644 --- a/packages/ui/src/components/chat/ChatInput.tsx +++ b/packages/ui/src/components/chat/ChatInput.tsx @@ -88,6 +88,11 @@ import { createPastedContextFile, isLargePlainTextPaste, } from './composer/largeTextPaste'; +import { + LARGE_TEXT_PASTE_TOAST_CLASSNAME, + beginLargeTextPasteOffer, + resolveLargeTextPasteOffer, +} from './composer/largeTextPasteOffer'; import type { LargeTextPasteBehavior } from '@/stores/useUIStore'; import type { FileMentionAutocompleteInputSource } from './fileMentionAutocompleteState'; import { @@ -1591,21 +1596,24 @@ const ChatInputComponent: React.FC = ({ onOpenSettings, scrollTo if (!editor) { // No mounted editor (collapsed mobile pill): append to the state // the editor will be seeded from. - const nextValue = message + text; + const nextValue = messageRef.current + text; setMessage(nextValue); updateAutocompleteState(nextValue, nextValue.length, inputSource, text); return; } const { start, end } = editor.getSelection(); - const nextValue = `${message.substring(0, start)}${text}${message.substring(end)}`; + // Read the live document — delayed toast actions must not use a + // paste-time React `message` closure. + const currentMessage = editor.getValue(); + const nextValue = `${currentMessage.substring(0, start)}${text}${currentMessage.substring(end)}`; const cursorPosition = start + text.length; // One dispatch places both the text and the caret, so there is no // frame where the caret sits at a stale offset. editor.insertText(text); updateAutocompleteState(nextValue, cursorPosition, inputSource, text); - }, [message, updateAutocompleteState]); + }, [updateAutocompleteState]); const clearDropTextSuppression = React.useCallback(() => { suppressNextFileDropTextInsertRef.current = false; @@ -1762,18 +1770,22 @@ const ChatInputComponent: React.FC = ({ onOpenSettings, scrollTo }; const attachAsFile = async () => { + // Read live attachment + composer state at action time — the ask + // toast can outlive the paste while the user types or attaches more. + const liveAttachedFiles = useInputStore.getState().attachedFiles; const filename = nextPastedContextFilename([ - ...attachedFiles.map((file) => file.filename), + ...liveAttachedFiles.map((file) => file.filename), ...pendingPastedAttachmentFilenamesRef.current, ]); const citationText = buildAttachmentCitationText([filename]); - const textarea = composerRef.current; - const selectionStart = textarea?.getSelection().start ?? message.length; - const selectionEnd = textarea?.getSelection().end ?? message.length; + const editor = composerRef.current; + const currentMessage = editor?.getValue() ?? messageRef.current; + const selectionStart = editor?.getSelection().start ?? currentMessage.length; + const selectionEnd = editor?.getSelection().end ?? currentMessage.length; const insertionText = withInlineInsertionBoundaries( citationText, - message.slice(0, selectionStart), - message.slice(selectionEnd), + currentMessage.slice(0, selectionStart), + currentMessage.slice(selectionEnd), ); insertTextAtSelection( @@ -1802,7 +1814,7 @@ const ChatInputComponent: React.FC = ({ onOpenSettings, scrollTo return; } - const offerId = largeTextPasteOfferIdRef.current + 1; + const offerId = beginLargeTextPasteOffer(largeTextPasteOfferIdRef.current); largeTextPasteOfferIdRef.current = offerId; if (largeTextPasteToastIdRef.current !== null) { @@ -1813,11 +1825,14 @@ const ChatInputComponent: React.FC = ({ onOpenSettings, scrollTo } const resolveLargePaste = (action: 'attach' | 'inline') => { - if (offerId !== largeTextPasteOfferIdRef.current) { + const resolution = resolveLargeTextPasteOffer( + largeTextPasteOfferIdRef.current, + offerId, + ); + largeTextPasteOfferIdRef.current = resolution.nextOfferId; + if (!resolution.accepted) { return; } - // Invalidate this offer so a later onDismiss cannot double-apply. - largeTextPasteOfferIdRef.current += 1; largeTextPasteToastIdRef.current = null; if (action === 'attach') { void attachAsFile(); @@ -1830,7 +1845,7 @@ const ChatInputComponent: React.FC = ({ onOpenSettings, scrollTo t('chat.chatInput.toast.largeTextPaste.title'), { duration: Infinity, - className: '!min-w-[22rem] !w-auto [&_[data-icon]]:!hidden', + className: LARGE_TEXT_PASTE_TOAST_CLASSNAME, action: { label: t('chat.chatInput.toast.largeTextPaste.attach'), onClick: () => resolveLargePaste('attach'), diff --git a/packages/ui/src/components/chat/composer/DOCUMENTATION.md b/packages/ui/src/components/chat/composer/DOCUMENTATION.md index db521edb..759067ee 100644 --- a/packages/ui/src/components/chat/composer/DOCUMENTATION.md +++ b/packages/ui/src/components/chat/composer/DOCUMENTATION.md @@ -19,6 +19,7 @@ belongs to one of them. | `ui/` | Presentation | | `text.ts` | How inserted text meets the text already there | | `largeTextPaste.ts` | Detect large plain-text pastes and build virtual `.txt` files | +| `largeTextPasteOffer.ts` | Ask-toast offer id begin/resolve (supersede + double-apply guards) | `ChatInput.handlePaste` owns paste orchestration: URL-over-selection markdown links, clipboard images (attach + citation), and large plain-text pastes. @@ -26,8 +27,9 @@ Large pastes (about 2,000 characters or 25 lines) follow the composer setting `largeTextPasteBehavior` (`ask` / `attach` / `inline`). Attaching creates an in-memory `text/plain` file named `pasted-context-N.txt`, inserts a bracket citation, and sends it through the same attachment pipeline as a manually -picked `.txt` file. Short text, images, and URL wraps keep their existing -paths. +picked `.txt` file. Ask-toast actions read live composer/attachment state so +typing or other attaches between paste and choice stay consistent. Short text, +images, and URL wraps keep their existing paths. ## The prompt language @@ -129,8 +131,8 @@ hardware. The package has no DOM test environment, so coverage stops at the state and logic layers: the language, the submit assembly, path and drop handling, text -splicing, large-paste detection, message history, and the CodeMirror language -extension at the `EditorState` level. +splicing, large-paste detection, paste-offer invalidation, message history, and +the CodeMirror language extension at the `EditorState` level. Rendering, focus, keyboard behavior, IME and WKWebView are **not covered by tests** and are verified by hand. Do not report a change to them as validated diff --git a/packages/ui/src/components/chat/composer/__tests__/largeTextPasteOffer.test.ts b/packages/ui/src/components/chat/composer/__tests__/largeTextPasteOffer.test.ts new file mode 100644 index 00000000..2b453a31 --- /dev/null +++ b/packages/ui/src/components/chat/composer/__tests__/largeTextPasteOffer.test.ts @@ -0,0 +1,56 @@ +import { describe, expect, test } from 'bun:test'; + +import { + LARGE_TEXT_PASTE_TOAST_CLASSNAME, + beginLargeTextPasteOffer, + resolveLargeTextPasteOffer, +} from '../largeTextPasteOffer'; + +describe('large text paste offer state', () => { + test('begin allocates the next offer id', () => { + expect(beginLargeTextPasteOffer(0)).toBe(1); + expect(beginLargeTextPasteOffer(3)).toBe(4); + }); + + test('resolve accepts a matching active offer and invalidates it', () => { + expect(resolveLargeTextPasteOffer(2, 2)).toEqual({ + accepted: true, + nextOfferId: 3, + }); + }); + + test('resolve rejects a superseded offer without advancing', () => { + expect(resolveLargeTextPasteOffer(5, 4)).toEqual({ + accepted: false, + nextOfferId: 5, + }); + }); + + test('second resolve after accept is rejected (double-apply guard)', () => { + const first = resolveLargeTextPasteOffer(1, 1); + expect(first.accepted).toBe(true); + expect(resolveLargeTextPasteOffer(first.nextOfferId, 1)).toEqual({ + accepted: false, + nextOfferId: first.nextOfferId, + }); + }); + + test('begin then resolve of the old id is rejected', () => { + const previous = 2; + const next = beginLargeTextPasteOffer(previous); + expect(resolveLargeTextPasteOffer(next, previous)).toEqual({ + accepted: false, + nextOfferId: next, + }); + expect(resolveLargeTextPasteOffer(next, next).accepted).toBe(true); + }); + + test('toast class widens only from the sm breakpoint', () => { + const classes = LARGE_TEXT_PASTE_TOAST_CLASSNAME.split(/\s+/); + expect(classes).toContain('sm:!min-w-[22rem]'); + expect(classes).toContain('sm:!w-auto'); + expect(classes).toContain('[&_[data-icon]]:!hidden'); + expect(classes.includes('!min-w-[22rem]')).toBe(false); + expect(classes.includes('!w-auto')).toBe(false); + }); +}); diff --git a/packages/ui/src/components/chat/composer/largeTextPasteOffer.ts b/packages/ui/src/components/chat/composer/largeTextPasteOffer.ts new file mode 100644 index 00000000..976a9d41 --- /dev/null +++ b/packages/ui/src/components/chat/composer/largeTextPasteOffer.ts @@ -0,0 +1,33 @@ +/** + * Offer-id state for the large-text paste ask toast. + * + * The toast can outlive the paste event (duration Infinity), and a second + * large paste can supersede an unanswered offer. These helpers keep that + * invalidation pure so ChatInput only wires toast UI to attach/inline actions. + */ + +export type LargeTextPasteOfferAction = 'attach' | 'inline'; + +/** Allocate a new offer id, superseding any unanswered previous offer. */ +export const beginLargeTextPasteOffer = (activeOfferId: number): number => ( + activeOfferId + 1 +); + +/** + * Attempt to resolve an offer. Returns whether this call won the race, and the + * next active id. A superseded or already-resolved offer is rejected so + * dismiss/action cannot double-apply. + */ +export const resolveLargeTextPasteOffer = ( + activeOfferId: number, + offerId: number, +): { accepted: boolean; nextOfferId: number } => { + if (offerId !== activeOfferId) { + return { accepted: false, nextOfferId: activeOfferId }; + } + return { accepted: true, nextOfferId: activeOfferId + 1 }; +}; + +/** Toast chrome: widen on desktop only; leave mobile full-width to Sonner. */ +export const LARGE_TEXT_PASTE_TOAST_CLASSNAME = + '[&_[data-icon]]:!hidden sm:!min-w-[22rem] sm:!w-auto'; From 6b17c0b4e2a0cfce1969b5335b4ddf30737ef29a Mon Sep 17 00:00:00 2001 From: Bohdan Triapitsyn Date: Sat, 29 Aug 2026 00:30:01 +0300 Subject: [PATCH 5/5] chore(ui): finish large text paste merge into main Adds the Turkish dictionary entries main introduced after this branch was opened, keeps the composer's four-space style in the new paste modules, and drops the unused offer-action type plus the widening return annotation flagged by the anti-slop lint. --- .../chat/composer/largeTextPaste.ts | 48 +++++++++---------- .../chat/composer/largeTextPasteOffer.ts | 20 ++++---- .../ui/src/lib/i18n/messages/tr.settings.ts | 7 +++ packages/ui/src/lib/i18n/messages/tr.ts | 4 ++ 4 files changed, 44 insertions(+), 35 deletions(-) diff --git a/packages/ui/src/components/chat/composer/largeTextPaste.ts b/packages/ui/src/components/chat/composer/largeTextPaste.ts index 2b180d52..b9660de7 100644 --- a/packages/ui/src/components/chat/composer/largeTextPaste.ts +++ b/packages/ui/src/components/chat/composer/largeTextPaste.ts @@ -10,13 +10,13 @@ export const LARGE_TEXT_PASTE_CHAR_THRESHOLD = 2000; export const LARGE_TEXT_PASTE_LINE_THRESHOLD = 25; const countLines = (text: string): number => { - let lines = 1; - for (let index = 0; index < text.length; index += 1) { - if (text.charCodeAt(index) === 10) { - lines += 1; + let lines = 1; + for (let index = 0; index < text.length; index += 1) { + if (text.charCodeAt(index) === 10) { + lines += 1; + } } - } - return lines; + return lines; }; /** @@ -27,29 +27,29 @@ const countLines = (text: string): number => { * character count or line count is enough. */ export const isLargePlainTextPaste = ( - text: string, - options?: { - charThreshold?: number; - lineThreshold?: number; - }, + text: string, + options?: { + charThreshold?: number; + lineThreshold?: number; + }, ): boolean => { - if (!text || !text.trim()) { - return false; - } + if (!text || !text.trim()) { + return false; + } - const charThreshold = options?.charThreshold ?? LARGE_TEXT_PASTE_CHAR_THRESHOLD; - const lineThreshold = options?.lineThreshold ?? LARGE_TEXT_PASTE_LINE_THRESHOLD; + const charThreshold = options?.charThreshold ?? LARGE_TEXT_PASTE_CHAR_THRESHOLD; + const lineThreshold = options?.lineThreshold ?? LARGE_TEXT_PASTE_LINE_THRESHOLD; - if (text.length >= charThreshold) { - return true; - } + if (text.length >= charThreshold) { + return true; + } - return countLines(text) >= lineThreshold; + return countLines(text) >= lineThreshold; }; export const createPastedContextFile = (text: string, filename: string): File => ( - new File([text], filename, { - type: 'text/plain', - lastModified: Date.now(), - }) + new File([text], filename, { + type: 'text/plain', + lastModified: Date.now(), + }) ); diff --git a/packages/ui/src/components/chat/composer/largeTextPasteOffer.ts b/packages/ui/src/components/chat/composer/largeTextPasteOffer.ts index 976a9d41..4dc53e63 100644 --- a/packages/ui/src/components/chat/composer/largeTextPasteOffer.ts +++ b/packages/ui/src/components/chat/composer/largeTextPasteOffer.ts @@ -6,11 +6,9 @@ * invalidation pure so ChatInput only wires toast UI to attach/inline actions. */ -export type LargeTextPasteOfferAction = 'attach' | 'inline'; - /** Allocate a new offer id, superseding any unanswered previous offer. */ export const beginLargeTextPasteOffer = (activeOfferId: number): number => ( - activeOfferId + 1 + activeOfferId + 1 ); /** @@ -19,15 +17,15 @@ export const beginLargeTextPasteOffer = (activeOfferId: number): number => ( * dismiss/action cannot double-apply. */ export const resolveLargeTextPasteOffer = ( - activeOfferId: number, - offerId: number, -): { accepted: boolean; nextOfferId: number } => { - if (offerId !== activeOfferId) { - return { accepted: false, nextOfferId: activeOfferId }; - } - return { accepted: true, nextOfferId: activeOfferId + 1 }; + activeOfferId: number, + offerId: number, +) => { + if (offerId !== activeOfferId) { + return { accepted: false, nextOfferId: activeOfferId }; + } + return { accepted: true, nextOfferId: activeOfferId + 1 }; }; /** Toast chrome: widen on desktop only; leave mobile full-width to Sonner. */ export const LARGE_TEXT_PASTE_TOAST_CLASSNAME = - '[&_[data-icon]]:!hidden sm:!min-w-[22rem] sm:!w-auto'; + '[&_[data-icon]]:!hidden sm:!min-w-[22rem] sm:!w-auto'; diff --git a/packages/ui/src/lib/i18n/messages/tr.settings.ts b/packages/ui/src/lib/i18n/messages/tr.settings.ts index 43f7f0e6..34125588 100644 --- a/packages/ui/src/lib/i18n/messages/tr.settings.ts +++ b/packages/ui/src/lib/i18n/messages/tr.settings.ts @@ -2000,6 +2000,13 @@ export const settingsDict = { 'settings.openchamber.visual.field.queueMessagesByDefaultTooltip': 'Etkinleştirildiğinde Enter mesajları kuyruğa ekler. Göndermek için {modifier}+Enter kullanın.', 'settings.openchamber.visual.field.persistDraftMessagesAria': 'Taslak mesajları kalıcı olarak sakla', 'settings.openchamber.visual.field.persistDraftMessages': 'Taslak mesajları kalıcı olarak sakla', + 'settings.openchamber.visual.field.largeTextPaste': 'Büyük metin yapıştırma', + 'settings.openchamber.visual.field.largeTextPasteHint': 'Yaklaşık 2.000 karakterden veya 25 satırdan fazlasını yapıştırırken metnin dosya olarak eklenmesini mi, satır içi yapıştırılmasını mı yoksa her seferinde sorulmasını mı istediğinizi seçin.', + 'settings.openchamber.visual.field.largeTextPasteAria': 'Büyük metin yapıştırma davranışı', + 'settings.openchamber.visual.field.largeTextPasteOptionAria': 'Büyük metin yapıştırma: {option}', + 'settings.openchamber.visual.option.largeTextPaste.ask.label': 'Her seferinde sor', + 'settings.openchamber.visual.option.largeTextPaste.attach.label': 'Dosya olarak ekle', + 'settings.openchamber.visual.option.largeTextPaste.inline.label': 'Satır içi yapıştır', 'settings.openchamber.visual.field.enableSpellcheckInTextInputsAria': 'Metin girişlerinde yazım denetimini etkinleştir', 'settings.openchamber.visual.field.enableSpellcheckInTextInputs': 'Metin girişlerinde yazım denetimini etkinleştir', 'settings.openchamber.visual.field.sendAnonymousUsageReportsAria': 'Anonim kullanım raporları gönder', diff --git a/packages/ui/src/lib/i18n/messages/tr.ts b/packages/ui/src/lib/i18n/messages/tr.ts index bb1955b4..6a9715d8 100644 --- a/packages/ui/src/lib/i18n/messages/tr.ts +++ b/packages/ui/src/lib/i18n/messages/tr.ts @@ -2272,6 +2272,10 @@ export const dict = { 'chat.chatInput.toast.sendAttachmentsFailed': 'Ekler gönderilemedi. Daha az dosya veya daha küçük görseller deneyin.', 'chat.chatInput.toast.messageSendFailed': 'Mesaj gönderilemedi. Ekler geri yüklendi.', 'chat.chatInput.toast.clipboardAttachFailed': 'Panodan görsel eklenemedi', + 'chat.chatInput.toast.clipboardTextAttachFailed': 'Yapıştırılan metin dosya olarak eklenemedi', + 'chat.chatInput.toast.largeTextPaste.title': 'Büyük metin algılandı', + 'chat.chatInput.toast.largeTextPaste.attach': 'Dosya olarak ekle', + 'chat.chatInput.toast.largeTextPaste.inline': 'Satır içi yapıştır', 'chat.chatInput.toast.addedFileMentions': '{count} dosya bahsi eklendi', 'chat.chatInput.toast.attachFileFailed': 'Dosya eklenemedi', 'chat.chatInput.toast.attachNamedFailed': '{name} eklenemedi',