feat: configurable paste collapse thresholds (TUI + CLI)

Adds two new config keys:
- paste_collapse_threshold (default: 5) — line count threshold for
  bracketed paste collapse in both TUI and CLI
- paste_collapse_threshold_fallback (default: 0, disabled) — same for
  the fallback heuristic in terminals without bracketed paste support

TUI frontend reads these from config.get full via applyDisplay/patchUiState.
CLI reads from self.config at paste-handling time.

Closes #5626
Related: #5623
This commit is contained in:
kylekahraman 2026-05-13 12:22:35 +00:00 committed by Teknium
parent 973bb124a4
commit ab42658dfc
8 changed files with 35 additions and 7 deletions

View file

@ -105,6 +105,8 @@ export interface UiState {
info: null | SessionInfo
inlineDiffs: boolean
mouseTracking: MouseTrackingMode
pasteCollapseLines: number
sections: SectionVisibility
showCost: boolean
showReasoning: boolean

View file

@ -17,6 +17,7 @@ const buildUiState = (): UiState => ({
info: null,
inlineDiffs: true,
mouseTracking: MOUSE_TRACKING,
pasteCollapseLines: 5,
sections: {},
showCost: false,
showReasoning: false,

View file

@ -8,7 +8,6 @@ import { useStore } from '@nanostores/react'
import { useCallback, useMemo, useState } from 'react'
import type { PasteEvent } from '../components/textInput.js'
import { LARGE_PASTE } from '../config/limits.js'
import type { ImageAttachResponse, InputDetectDropResponse } from '../gatewayTypes.js'
import { useCompletion } from '../hooks/useCompletion.js'
import { useInputHistory } from '../hooks/useInputHistory.js'
@ -190,8 +189,9 @@ export function useComposerState({
}
const lineCount = cleanedText.split('\n').length
const pasteCollapseLines = getUiState().pasteCollapseLines
if (cleanedText.length < LARGE_PASTE.chars && lineCount < LARGE_PASTE.lines) {
if (pasteCollapseLines === 0 || lineCount < pasteCollapseLines) {
return {
cursor: cursor + cleanedText.length,
value: value.slice(0, cursor) + cleanedText + value.slice(cursor)

View file

@ -142,6 +142,17 @@ const _voiceRecordKeyFromConfig = (cfg: ConfigFullResponse | null): ParsedVoiceR
return raw ? parseVoiceRecordKey(raw) : DEFAULT_VOICE_RECORD_KEY
}
const _pasteCollapseLinesFromConfig = (cfg: ConfigFullResponse | null): number => {
if (!cfg?.config) return 5
const raw = cfg.config.paste_collapse_threshold
if (typeof raw === 'number' && Number.isFinite(raw) && raw >= 0) return Math.round(raw)
if (typeof raw === 'string') {
const n = parseInt(raw, 10)
if (Number.isFinite(n) && n >= 0) return n
}
return 5
}
/** Fetch ``config.get full`` and fan the result through ``applyDisplay``.
*
* Extracted so the mtime-reload path can be exercised by the test
@ -188,6 +199,7 @@ export const applyDisplay = (
indicatorStyle: normalizeIndicatorStyle(d.tui_status_indicator),
inlineDiffs: d.inline_diffs !== false,
mouseTracking: normalizeMouseTracking(d),
pasteCollapseLines: _pasteCollapseLinesFromConfig(cfg),
sections: resolveSections(d.sections),
showCost: !!d.show_cost,
showReasoning: !!d.show_reasoning,

View file

@ -1,4 +1,4 @@
export const LARGE_PASTE = { chars: 8000, lines: 80 }
export const LARGE_PASTE = { lines: 5 }
export const LIVE_RENDER_MAX_CHARS = 16_000
export const LIVE_RENDER_MAX_LINES = 240

View file

@ -82,7 +82,7 @@ export interface ConfigVoiceConfig {
}
export interface ConfigFullResponse {
config?: { display?: ConfigDisplayConfig; voice?: ConfigVoiceConfig }
config?: { display?: ConfigDisplayConfig; voice?: ConfigVoiceConfig; paste_collapse_threshold?: number }
}
export interface ConfigMtimeResponse {