diff --git a/cli.py b/cli.py index 1c527e08312..93050a070d8 100644 --- a/cli.py +++ b/cli.py @@ -13351,7 +13351,8 @@ class HermesCLI: pasted_text = _sanitize_surrogates(pasted_text) line_count = pasted_text.count('\n') buf = event.current_buffer - if line_count >= 5 and not buf.text.strip().startswith('/'): + threshold = self.config.get("paste_collapse_threshold", 5) + if threshold > 0 and line_count >= threshold and not buf.text.strip().startswith('/'): _paste_counter[0] += 1 paste_dir = _hermes_home / "pastes" paste_dir.mkdir(parents=True, exist_ok=True) @@ -13520,7 +13521,8 @@ class HermesCLI: newlines_added = line_count - _prev_newline_count[0] _prev_newline_count[0] = line_count is_paste = chars_added > 1 or newlines_added >= 4 - if line_count >= 5 and is_paste and not text.startswith('/'): + threshold = self.config.get("paste_collapse_threshold_fallback", 0) + if threshold > 0 and line_count >= threshold and is_paste and not text.startswith('/'): _paste_counter[0] += 1 paste_dir = _hermes_home / "pastes" paste_dir.mkdir(parents=True, exist_ok=True) diff --git a/hermes_cli/config.py b/hermes_cli/config.py index 07dfe23ba8d..4fca9906d17 100644 --- a/hermes_cli/config.py +++ b/hermes_cli/config.py @@ -1771,6 +1771,7 @@ DEFAULT_CONFIG = { "servers": {}, }, + # X (Twitter) Search via xAI's built-in x_search Responses tool. # The tool registers when xAI credentials are available (SuperGrok # OAuth or XAI_API_KEY) AND the x_search toolset is enabled in @@ -1827,8 +1828,18 @@ DEFAULT_CONFIG = { }, }, + # Paste collapse thresholds (TUI + CLI). + # collapse_threshold: paste collapses to a file reference when line count + # exceeds this value (bracketed paste, safe: appends to existing text). + # collapse_threshold_fallback: same but for the fallback heuristic used + # by terminals without bracketed paste support (destructive: replaces + # entire buffer). 0 = disabled. + "paste_collapse_threshold": 5, + "paste_collapse_threshold_fallback": 0, + + # Config schema version - bump this when adding new required fields - "_config_version": 23, + "_config_version": 24, } # ============================================================================= diff --git a/ui-tui/src/app/interfaces.ts b/ui-tui/src/app/interfaces.ts index cb2788bbf4f..2a39ffeb52e 100644 --- a/ui-tui/src/app/interfaces.ts +++ b/ui-tui/src/app/interfaces.ts @@ -105,6 +105,8 @@ export interface UiState { info: null | SessionInfo inlineDiffs: boolean mouseTracking: MouseTrackingMode + pasteCollapseLines: number + sections: SectionVisibility showCost: boolean showReasoning: boolean diff --git a/ui-tui/src/app/uiStore.ts b/ui-tui/src/app/uiStore.ts index ea592700b77..b449736d3d0 100644 --- a/ui-tui/src/app/uiStore.ts +++ b/ui-tui/src/app/uiStore.ts @@ -17,6 +17,7 @@ const buildUiState = (): UiState => ({ info: null, inlineDiffs: true, mouseTracking: MOUSE_TRACKING, + pasteCollapseLines: 5, sections: {}, showCost: false, showReasoning: false, diff --git a/ui-tui/src/app/useComposerState.ts b/ui-tui/src/app/useComposerState.ts index 859506db94e..583f812f424 100644 --- a/ui-tui/src/app/useComposerState.ts +++ b/ui-tui/src/app/useComposerState.ts @@ -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) diff --git a/ui-tui/src/app/useConfigSync.ts b/ui-tui/src/app/useConfigSync.ts index 35694dbec6a..57a3d153d65 100644 --- a/ui-tui/src/app/useConfigSync.ts +++ b/ui-tui/src/app/useConfigSync.ts @@ -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, diff --git a/ui-tui/src/config/limits.ts b/ui-tui/src/config/limits.ts index 9043297d549..31b062b9cdc 100644 --- a/ui-tui/src/config/limits.ts +++ b/ui-tui/src/config/limits.ts @@ -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 diff --git a/ui-tui/src/gatewayTypes.ts b/ui-tui/src/gatewayTypes.ts index 9de1c85112d..356b23dd0d6 100644 --- a/ui-tui/src/gatewayTypes.ts +++ b/ui-tui/src/gatewayTypes.ts @@ -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 {