diff --git a/apps/mobile/src/features/settings/SettingsFollowUpRouteScreen.tsx b/apps/mobile/src/features/settings/SettingsFollowUpRouteScreen.tsx
index a7cd04f5f5c1f..80ce0812305bc 100644
--- a/apps/mobile/src/features/settings/SettingsFollowUpRouteScreen.tsx
+++ b/apps/mobile/src/features/settings/SettingsFollowUpRouteScreen.tsx
@@ -1,5 +1,10 @@
import { ScreenScrollView as ScrollView } from "../../components/ui/ScreenScrollView";
import { useAtomSet, useAtomValue } from "@effect/atom-react";
+import {
+ DEFAULT_PASTED_TEXT_ATTACHMENT_THRESHOLD,
+ type PastedTextAttachmentThreshold,
+} from "@t3tools/contracts";
+import { pastedTextThresholdLabel } from "@t3tools/client-runtime/text-paste";
import { AsyncResult } from "effect/reactivity";
import { Platform } from "react-native";
import { useSafeAreaInsets } from "react-native-safe-area-context";
@@ -28,6 +33,17 @@ const FOLLOW_UP_OPTIONS: ReadonlyArray<{
},
];
+const THRESHOLD_OPTIONS: ReadonlyArray<{
+ readonly threshold: PastedTextAttachmentThreshold;
+ readonly description: string;
+}> = [
+ { threshold: 1_000, description: "Most logs, stack traces, and code snippets become files." },
+ { threshold: 5_000, description: "Long logs and documents become files; snippets stay as text." },
+ { threshold: 32_768, description: "Default. Only very large pastes become files." },
+];
+
+const PASTE_AS_TEXT_SHORTCUT = Platform.OS === "ios" ? "Command + Shift + V" : "Ctrl + Shift + V";
+
export function SettingsFollowUpRouteScreen() {
const insets = useSafeAreaInsets();
const preferencesResult = useAtomValue(mobilePreferencesAtom);
@@ -36,6 +52,10 @@ export function SettingsFollowUpRouteScreen() {
const selectedBehavior = AsyncResult.isSuccess(preferencesResult)
? (preferencesResult.value.followUpBehavior ?? DEFAULT_FOLLOW_UP_BEHAVIOR)
: null;
+ const selectedThreshold = AsyncResult.isSuccess(preferencesResult)
+ ? (preferencesResult.value.pastedTextAttachmentThreshold ??
+ DEFAULT_PASTED_TEXT_ATTACHMENT_THRESHOLD)
+ : null;
const content = (
+
+ {THRESHOLD_OPTIONS.map(({ threshold, description }, index) => (
+ 0}
+ disabled={!preferencesReady}
+ onPress={() => savePreferences({ pastedTextAttachmentThreshold: threshold })}
+ />
+ ))}
+
+
+ Shorter pastes go into the message as text. Non-Latin text and emoji use more bytes per
+ character, so they reach the limit sooner. With a hardware keyboard, press{" "}
+ {PASTE_AS_TEXT_SHORTCUT} to paste a long block as text instead.
+
);
diff --git a/apps/mobile/src/features/settings/settings-search.ts b/apps/mobile/src/features/settings/settings-search.ts
index 49e1a8dbe082e..0b821e75aae5e 100644
--- a/apps/mobile/src/features/settings/settings-search.ts
+++ b/apps/mobile/src/features/settings/settings-search.ts
@@ -76,7 +76,15 @@ const SETTINGS_SEARCH_TERMS = {
"auto-resume",
"thread behavior",
],
- SettingsFollowUp: ["follow-ups", "queue", "steer", "while the agent is running"],
+ SettingsFollowUp: [
+ "follow-ups",
+ "queue",
+ "steer",
+ "while the agent is running",
+ "large pastes",
+ "attachment",
+ "text file",
+ ],
SettingsArchive: ["archive", "archived", "restore", "unarchive"],
SettingsProviderAccounts: [
"provider accounts",
diff --git a/apps/mobile/src/features/threads/ThreadComposer.tsx b/apps/mobile/src/features/threads/ThreadComposer.tsx
index 9c2eb6d4c38df..76e0b16c19ad9 100644
--- a/apps/mobile/src/features/threads/ThreadComposer.tsx
+++ b/apps/mobile/src/features/threads/ThreadComposer.tsx
@@ -52,6 +52,7 @@ import Animated, {
withTiming,
} from "react-native-reanimated";
import { useUniwindTheme } from "../../lib/useUniwindTheme";
+import { usePastedTextAttachmentThreshold } from "../../lib/usePastedTextAttachmentThreshold";
import { themeColorWithAlpha } from "../../lib/mobileTheme";
import { armAgentAwarenessLiveActivityForLocalWork } from "../agent-awareness/remoteRegistration";
import { scopedThreadKey } from "../../lib/scopedEntities";
@@ -422,6 +423,7 @@ export const ThreadComposer = function ThreadComposer(props: ThreadComposerProps
const navigation = useNavigation();
const foregroundColor = useUniwindTheme()["--color-foreground"];
const bodyText = useScaledTextRole("body");
+ const pastedTextAttachmentThreshold = usePastedTextAttachmentThreshold();
const fallbackInputRef = useRef(null);
const inputRef = props.editorRef ?? fallbackInputRef;
const [isFocused, setIsFocused] = useState(false);
@@ -1013,6 +1015,7 @@ export const ThreadComposer = function ThreadComposer(props: ThreadComposerProps
text: paste.text,
wouldExceedInputLimit,
canAttach: true,
+ thresholdBytes: pastedTextAttachmentThreshold,
}) === "attachment"
) {
if (canAttach) {
diff --git a/apps/mobile/src/lib/usePastedTextAttachmentThreshold.ts b/apps/mobile/src/lib/usePastedTextAttachmentThreshold.ts
new file mode 100644
index 0000000000000..a798ea756b301
--- /dev/null
+++ b/apps/mobile/src/lib/usePastedTextAttachmentThreshold.ts
@@ -0,0 +1,19 @@
+import { useAtomValue } from "@effect/atom-react";
+import { DEFAULT_PASTED_TEXT_ATTACHMENT_THRESHOLD } from "@t3tools/contracts";
+import { AsyncResult } from "effect/reactivity";
+
+import { mobilePreferencesAtom } from "../state/preferences";
+
+/**
+ * Paste size at which the composer folds clipboard text into a text-file
+ * attachment. Selects only the threshold so unrelated preference writes do not
+ * re-render the composer editors.
+ */
+export function usePastedTextAttachmentThreshold() {
+ return useAtomValue(mobilePreferencesAtom, (preferences) =>
+ AsyncResult.isSuccess(preferences)
+ ? (preferences.value.pastedTextAttachmentThreshold ??
+ DEFAULT_PASTED_TEXT_ATTACHMENT_THRESHOLD)
+ : DEFAULT_PASTED_TEXT_ATTACHMENT_THRESHOLD,
+ );
+}
diff --git a/apps/mobile/src/native/T3ComposerEditor.ios.tsx b/apps/mobile/src/native/T3ComposerEditor.ios.tsx
index 437bcda4d012d..f32b4caf49e08 100644
--- a/apps/mobile/src/native/T3ComposerEditor.ios.tsx
+++ b/apps/mobile/src/native/T3ComposerEditor.ios.tsx
@@ -1,4 +1,3 @@
-import { PASTED_TEXT_ATTACHMENT_THRESHOLD_BYTES } from "@t3tools/client-runtime/text-paste";
import { PROVIDER_SEND_TURN_MAX_INPUT_CHARS } from "@t3tools/contracts";
import { collectComposerInlineTokens } from "@t3tools/shared/composerInlineTokens";
import { composerContextEditorTokens, composerSkillLabels } from "../lib/composerContext";
@@ -27,6 +26,7 @@ import { contextChipIconUri, modelChipIcon, modelChipIconUri } from "../lib/cont
import { useUniwindTheme } from "../lib/useUniwindTheme";
import { createNativeComposerTheme } from "../lib/nativeComposerTheme";
import { FONT_FAMILIES } from "../lib/fontFamilies";
+import { usePastedTextAttachmentThreshold } from "../lib/usePastedTextAttachmentThreshold";
import { useScaledTextRole } from "../features/settings/appearance/useScaledTextRole";
import {
acknowledgeComposerNativeEvent,
@@ -179,6 +179,7 @@ export function ComposerEditor({
const bodyText = useScaledTextRole("body");
const theme = useUniwindTheme();
const fontFamily = FONT_FAMILIES.regular;
+ const pastedTextAttachmentThreshold = usePastedTextAttachmentThreshold();
const skillLabels = useMemo(() => composerSkillLabels(skills), [skills]);
const { modelsBySource, modelTokens } = useComposerModelTokens(props.value, models);
@@ -372,7 +373,7 @@ export function ComposerEditor({
autoFocus={props.autoFocus ?? false}
autoCorrect={props.autoCorrect ?? true}
spellCheck={props.spellCheck ?? true}
- textPasteThresholdBytes={onPasteText ? PASTED_TEXT_ATTACHMENT_THRESHOLD_BYTES : 0}
+ textPasteThresholdBytes={onPasteText ? pastedTextAttachmentThreshold : 0}
maxInputChars={PROVIDER_SEND_TURN_MAX_INPUT_CHARS}
style={style as StyleProp}
onComposerChange={(event) => applyNativeChange(event.nativeEvent)}
diff --git a/apps/mobile/src/native/T3ComposerEditor.native.tsx b/apps/mobile/src/native/T3ComposerEditor.native.tsx
index 5aabd8e0d6071..b957a4d1e054c 100644
--- a/apps/mobile/src/native/T3ComposerEditor.native.tsx
+++ b/apps/mobile/src/native/T3ComposerEditor.native.tsx
@@ -1,4 +1,3 @@
-import { PASTED_TEXT_ATTACHMENT_THRESHOLD_BYTES } from "@t3tools/client-runtime/text-paste";
import { PROVIDER_SEND_TURN_MAX_INPUT_CHARS } from "@t3tools/contracts";
import { collectComposerInlineTokens } from "@t3tools/shared/composerInlineTokens";
import { composerContextEditorTokens, composerSkillLabels } from "../lib/composerContext";
@@ -29,6 +28,7 @@ import { contextChipIconUri, modelChipIcon, modelChipIconUri } from "../lib/cont
import { MOBILE_TYPOGRAPHY } from "../lib/typography";
import { useNativePaste } from "../lib/useNativePaste";
import { FONT_FAMILIES } from "../lib/fontFamilies";
+import { usePastedTextAttachmentThreshold } from "../lib/usePastedTextAttachmentThreshold";
import { useAppearancePreferences } from "../features/settings/appearance/AppearancePreferencesProvider";
import { useUniwindTheme } from "../lib/useUniwindTheme";
import { createNativeComposerTheme } from "../lib/nativeComposerTheme";
@@ -313,6 +313,7 @@ export function ComposerEditor({
});
const resolvedTextStyle = StyleSheet.flatten(textStyle) ?? {};
const regularFontFamily = FONT_FAMILIES.regular;
+ const pastedTextAttachmentThreshold = usePastedTextAttachmentThreshold();
return (
{
diff --git a/apps/mobile/src/persistence/mobile-preferences.ts b/apps/mobile/src/persistence/mobile-preferences.ts
index 852d96c608520..d4f8fb3024351 100644
--- a/apps/mobile/src/persistence/mobile-preferences.ts
+++ b/apps/mobile/src/persistence/mobile-preferences.ts
@@ -10,6 +10,7 @@ import {
PullRequestInvolvement,
PullRequestListFilters,
PullRequestListState,
+ PastedTextAttachmentThreshold,
type ProviderInstanceId,
type PullRequestMergeMethod,
type SidebarProjectGroupingMode,
@@ -27,6 +28,7 @@ import { MobileStorageDecodeError, MobileStorageEncodeError } from "./mobile-sto
const PREFERENCES_KEY = "t3code.preferences";
const PREFERENCES_FALLBACK_KEY = "t3code.preferences.fallback";
const encodeJson = Schema.encodeUnknownEffect(Schema.fromJsonString(Schema.Unknown));
+const isPastedTextAttachmentThreshold = Schema.is(PastedTextAttachmentThreshold);
export const PullRequestListControls = Schema.Struct({
state: PullRequestListState,
@@ -113,6 +115,7 @@ export interface Preferences {
/** Hardware-keyboard Return behavior in the composer. */
readonly composerEnterBehavior?: ComposerEnterBehavior;
readonly followUpBehavior?: FollowUpBehavior;
+ readonly pastedTextAttachmentThreshold?: PastedTextAttachmentThreshold;
/** The client `language` setting: a locale code or "system". Unset means English. */
readonly language?: string;
readonly microphones?: ReadonlyArray;
@@ -199,6 +202,7 @@ function sanitizePreferences(parsed: Preferences): Preferences {
dismissedDatabaseStorageNotices?: readonly string[];
composerEnterBehavior?: ComposerEnterBehavior;
followUpBehavior?: FollowUpBehavior;
+ pastedTextAttachmentThreshold?: PastedTextAttachmentThreshold;
language?: string;
microphones?: ReadonlyArray;
projectGroupingEnabled?: boolean;
@@ -308,6 +312,9 @@ function sanitizePreferences(parsed: Preferences): Preferences {
if (parsed.followUpBehavior === "queue" || parsed.followUpBehavior === "steer") {
preferences.followUpBehavior = parsed.followUpBehavior;
}
+ if (isPastedTextAttachmentThreshold(parsed.pastedTextAttachmentThreshold)) {
+ preferences.pastedTextAttachmentThreshold = parsed.pastedTextAttachmentThreshold;
+ }
if (typeof parsed.language === "string") preferences.language = parsed.language;
if (Array.isArray(parsed.microphones)) {
preferences.microphones = parsed.microphones.flatMap((entry): MicrophoneEntry[] => {
diff --git a/apps/web/src/components/chat/ChatComposer.tsx b/apps/web/src/components/chat/ChatComposer.tsx
index 4ec1bde76e6a9..256ea9f936aa6 100644
--- a/apps/web/src/components/chat/ChatComposer.tsx
+++ b/apps/web/src/components/chat/ChatComposer.tsx
@@ -2611,6 +2611,7 @@ export const ChatComposer = memo(function ChatComposer(props: ChatComposerProps)
questionAttachmentTarget,
attachmentTargetKey,
fileStagingLimit,
+ pastedTextAttachmentThreshold: settings.pastedTextAttachmentThreshold,
promptRef,
composerImagesRef,
composerFilesRef,
diff --git a/apps/web/src/components/chat/useComposerAttachmentIntake.ts b/apps/web/src/components/chat/useComposerAttachmentIntake.ts
index fe3a56a392203..f1aeb262456cd 100644
--- a/apps/web/src/components/chat/useComposerAttachmentIntake.ts
+++ b/apps/web/src/components/chat/useComposerAttachmentIntake.ts
@@ -67,6 +67,7 @@ export function useComposerAttachmentIntake(input: {
readonly questionAttachmentTarget: DraftId | null;
readonly attachmentTargetKey: string;
readonly fileStagingLimit: number | null;
+ readonly pastedTextAttachmentThreshold: number;
readonly promptRef: ChatComposerProps["promptRef"];
readonly composerImagesRef: ChatComposerProps["composerImagesRef"];
readonly composerFilesRef: ChatComposerProps["composerFilesRef"];
@@ -94,6 +95,7 @@ export function useComposerAttachmentIntake(input: {
questionAttachmentTarget,
attachmentTargetKey,
fileStagingLimit,
+ pastedTextAttachmentThreshold,
promptRef,
composerImagesRef,
composerFilesRef,
@@ -381,6 +383,7 @@ export function useComposerAttachmentIntake(input: {
bypassAutoAttachment,
wouldExceedInputLimit,
canAttach: true,
+ thresholdBytes: pastedTextAttachmentThreshold,
}) === "attachment";
if (!shouldFold) {
return false;
diff --git a/apps/web/src/components/settings/SettingsPanels.tsx b/apps/web/src/components/settings/SettingsPanels.tsx
index 95acb62765c98..8ab1aabdabb33 100644
--- a/apps/web/src/components/settings/SettingsPanels.tsx
+++ b/apps/web/src/components/settings/SettingsPanels.tsx
@@ -15,11 +15,13 @@ import {
type ProviderInstanceId,
type SidebarProjectGroupingMode,
} from "@t3tools/contracts";
+import { pastedTextThresholdLabel } from "@t3tools/client-runtime/text-paste";
import {
DEFAULT_UNIFIED_SETTINGS,
type DiffLayout,
MAX_SIDEBAR_AUTO_SETTLE_AFTER_DAYS,
MIN_SIDEBAR_AUTO_SETTLE_AFTER_DAYS,
+ PASTED_TEXT_ATTACHMENT_THRESHOLDS,
type ResponseStreamingMode,
type QuitConfirmationMode,
SidebarProjectSortOrder,
@@ -532,6 +534,10 @@ export function useSettingsRestore(onRestored?: () => void) {
? ["Rich text composer"]
: []),
...(settings.sendShortcut !== DEFAULT_UNIFIED_SETTINGS.sendShortcut ? ["Send shortcut"] : []),
+ ...(settings.pastedTextAttachmentThreshold !==
+ DEFAULT_UNIFIED_SETTINGS.pastedTextAttachmentThreshold
+ ? ["Large paste threshold"]
+ : []),
...(settings.followUpBehavior !== DEFAULT_UNIFIED_SETTINGS.followUpBehavior
? ["Follow-up behavior"]
: []),
@@ -655,6 +661,7 @@ export function useSettingsRestore(onRestored?: () => void) {
showSkillsInSlashMenu: DEFAULT_UNIFIED_SETTINGS.showSkillsInSlashMenu,
composerRichTextEnabled: DEFAULT_UNIFIED_SETTINGS.composerRichTextEnabled,
sendShortcut: DEFAULT_UNIFIED_SETTINGS.sendShortcut,
+ pastedTextAttachmentThreshold: DEFAULT_UNIFIED_SETTINGS.pastedTextAttachmentThreshold,
followUpBehavior: DEFAULT_UNIFIED_SETTINGS.followUpBehavior,
toolCallVisibility: DEFAULT_UNIFIED_SETTINGS.toolCallVisibility,
screenReaderAnnouncements: DEFAULT_UNIFIED_SETTINGS.screenReaderAnnouncements,
@@ -1214,6 +1221,57 @@ export function ChatSettingsPanel() {
}
/>
+
+ updateSettings({
+ pastedTextAttachmentThreshold:
+ DEFAULT_UNIFIED_SETTINGS.pastedTextAttachmentThreshold,
+ })
+ }
+ />
+ ) : null
+ }
+ control={
+
+ }
+ />
{
it("accepts the V key on non-Latin layouts but follows the letter on Latin ones", () => {
@@ -10,3 +11,32 @@ describe("isPasteAsTextShortcut", () => {
expect(isPasteAsTextShortcut({ ...chord, key: "V", code: "Period" }, false)).toBe(true);
});
});
+
+describe("pastedTextDisposition", () => {
+ it("folds at each selectable threshold", () => {
+ for (const thresholdBytes of PASTED_TEXT_ATTACHMENT_THRESHOLDS) {
+ expect(
+ pastedTextDisposition({
+ text: "x".repeat(thresholdBytes - 1),
+ canAttach: true,
+ thresholdBytes,
+ }),
+ ).toBe("inline");
+ expect(
+ pastedTextDisposition({
+ text: "x".repeat(thresholdBytes),
+ canAttach: true,
+ thresholdBytes,
+ }),
+ ).toBe("attachment");
+ }
+ });
+
+ it("counts UTF-8 bytes, so non-Latin text folds with fewer characters", () => {
+ const text = "日本語".repeat(120);
+ expect(text.length).toBeLessThan(1_000);
+ expect(pastedTextDisposition({ text, canAttach: true, thresholdBytes: 1_000 })).toBe(
+ "attachment",
+ );
+ });
+});
diff --git a/packages/client-runtime/src/textPaste.ts b/packages/client-runtime/src/textPaste.ts
index 59a6182034572..ea6e677d288b8 100644
--- a/packages/client-runtime/src/textPaste.ts
+++ b/packages/client-runtime/src/textPaste.ts
@@ -1,5 +1,3 @@
-export const PASTED_TEXT_ATTACHMENT_THRESHOLD_BYTES = 32 * 1024;
-
const textEncoder = new TextEncoder();
export type PastedTextDisposition = "attachment" | "inline";
@@ -25,12 +23,14 @@ export function isPasteAsTextShortcut(
}
/**
- * The threshold is byte-based: character counts substantially understate the
- * context cost of some Unicode-heavy clipboard contents.
+ * `thresholdBytes` is the `pastedTextAttachmentThreshold` setting. It is compared
+ * with both the length and the UTF-8 byte count: character counts substantially
+ * understate the context cost of some Unicode-heavy clipboard contents.
*/
export function pastedTextDisposition(input: {
readonly text: string;
readonly canAttach: boolean;
+ readonly thresholdBytes: number;
readonly bypassAutoAttachment?: boolean;
readonly wouldExceedInputLimit?: boolean;
}): PastedTextDisposition {
@@ -38,12 +38,22 @@ export function pastedTextDisposition(input: {
return "inline";
}
return input.wouldExceedInputLimit ||
- input.text.length >= PASTED_TEXT_ATTACHMENT_THRESHOLD_BYTES ||
- textEncoder.encode(input.text).byteLength >= PASTED_TEXT_ATTACHMENT_THRESHOLD_BYTES
+ input.text.length >= input.thresholdBytes ||
+ textEncoder.encode(input.text).byteLength >= input.thresholdBytes
? "attachment"
: "inline";
}
+/**
+ * Settings label for a `pastedTextAttachmentThreshold` preset, such as
+ * "5 KB (~5,000 chars)". Sizes come first because the threshold also counts
+ * UTF-8 bytes, so non-Latin text and emoji fold with fewer characters.
+ */
+export function pastedTextThresholdLabel(thresholdBytes: number) {
+ const kilobytes = Math.floor(thresholdBytes / 1000);
+ return `${kilobytes} KB (~${(kilobytes * 1000).toLocaleString("en-US")} chars)`;
+}
+
export function nextPastedTextFileName(existingNames: ReadonlyArray): string {
const names = new Set(existingNames.map((name) => name.toLowerCase()));
if (!names.has("pasted-text.txt")) {
diff --git a/packages/contracts/src/settings.test.ts b/packages/contracts/src/settings.test.ts
index 4c194f473c7b0..f9c89f994ed4c 100644
--- a/packages/contracts/src/settings.test.ts
+++ b/packages/contracts/src/settings.test.ts
@@ -303,6 +303,17 @@ describe("resolveIdleCompaction", () => {
});
});
+describe("ClientSettings large paste threshold", () => {
+ it("falls back to the default for a stored preset this build no longer offers", () => {
+ const settings = decodeClientSettings({
+ pastedTextAttachmentThreshold: 4_000,
+ sendShortcut: "mod-enter",
+ });
+ expect(settings.pastedTextAttachmentThreshold).toBe(32_768);
+ expect(settings.sendShortcut).toBe("mod-enter");
+ });
+});
+
describe("ClientSettings notifications", () => {
it("requires opt-in when existing settings omit notification preferences", () => {
expect(decodeClientSettings({}).notificationMode).toBe("off");
diff --git a/packages/contracts/src/settings.ts b/packages/contracts/src/settings.ts
index 475515092fa7c..75069df25f025 100644
--- a/packages/contracts/src/settings.ts
+++ b/packages/contracts/src/settings.ts
@@ -336,6 +336,16 @@ export const LoadBalancingWeights = Schema.Record(
export const DiffColorScheme = Schema.Literals(["red-green", "blue-orange"]);
+/**
+ * Paste sizes at which the composer folds clipboard text into a text-file
+ * attachment. A paste folds when either its length or its UTF-8 byte count
+ * reaches the threshold.
+ */
+export const PASTED_TEXT_ATTACHMENT_THRESHOLDS = [1_000, 5_000, 32_768] as const;
+export const PastedTextAttachmentThreshold = Schema.Literals(PASTED_TEXT_ATTACHMENT_THRESHOLDS);
+export type PastedTextAttachmentThreshold = typeof PastedTextAttachmentThreshold.Type;
+export const DEFAULT_PASTED_TEXT_ATTACHMENT_THRESHOLD: PastedTextAttachmentThreshold = 32_768;
+
export const ChatWidth = Schema.Literals(["comfortable", "wide", "full"]);
export type ChatWidth = typeof ChatWidth.Type;
@@ -512,6 +522,12 @@ export const ClientSettingsSchema = Schema.Struct({
pullRequestLinksOpenInApp: Schema.Boolean.pipe(Schema.withDecodingDefault(Effect.succeed(true))),
composerRichTextEnabled: Schema.Boolean.pipe(Schema.withDecodingDefault(Effect.succeed(true))),
+ // A stored preset this build no longer offers falls back to the default
+ // instead of failing the whole settings decode.
+ pastedTextAttachmentThreshold: PastedTextAttachmentThreshold.pipe(
+ Schema.catchDecoding(() => Effect.succeedSome(DEFAULT_PASTED_TEXT_ATTACHMENT_THRESHOLD)),
+ Schema.withDecodingDefault(Effect.succeed(DEFAULT_PASTED_TEXT_ATTACHMENT_THRESHOLD)),
+ ),
sendShortcut: Schema.Literals(["enter", "mod-enter-multiline", "mod-enter"]).pipe(
Schema.withDecodingDefault(Effect.succeed("enter")),
),
@@ -2041,6 +2057,7 @@ export const ClientSettingsPatch = Schema.Struct({
),
pullRequestLinksOpenInApp: Schema.optionalKey(Schema.Boolean),
composerRichTextEnabled: Schema.optionalKey(Schema.Boolean),
+ pastedTextAttachmentThreshold: Schema.optionalKey(PastedTextAttachmentThreshold),
sendShortcut: Schema.optionalKey(Schema.Literals(["enter", "mod-enter-multiline", "mod-enter"])),
followUpBehavior: Schema.optionalKey(Schema.Literals(["queue", "steer"])),
proactivePanelsEnabled: Schema.optionalKey(Schema.Boolean),