diff --git a/CHANGELOG.md b/CHANGELOG.md index 990c44f8a7b..d9e0983f0b4 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -30,6 +30,7 @@ Docs: https://docs.openclaw.ai ### Fixes +- TUI/token copy-safety rendering: treat long credential-like mixed alphanumeric tokens (including quoted forms) as copy-sensitive in render sanitization so formatter hard-wrap guards no longer inject visible spaces into auth-style values before display. (#26710) Thanks @jasonthane. - WhatsApp/self-chat response prefix fallback: stop forcing `"[openclaw]"` as the implicit outbound response prefix when no identity name or response prefix is configured, so blank/default prefix settings no longer inject branding text unexpectedly in self-chat flows. (#27962) Thanks @ecanmor. - Memory/QMD search result decoding: accept `qmd search` hits that only include `file` URIs (for example `qmd://collection/path.md`) without `docid`, resolve them through managed collection roots, and keep multi-collection results keyed by file fallback so valid QMD hits no longer collapse to empty `memory_search` output. (#28181) Thanks @0x76696265. - Memory/QMD collection-name conflict recovery: when `qmd collection add` fails because another collection already occupies the same `path + pattern`, detect the conflicting collection from `collection list`, remove it, and retry add so agent-scoped managed collections are created deterministically instead of being silently skipped; also add warning-only fallback when qmd metadata is unavailable to avoid destructive guesses. (#25496) Thanks @Ramsbaby. diff --git a/src/tui/tui-formatters.test.ts b/src/tui/tui-formatters.test.ts index 3d903ec7514..c4dfa26bb14 100644 --- a/src/tui/tui-formatters.test.ts +++ b/src/tui/tui-formatters.test.ts @@ -249,6 +249,20 @@ describe("sanitizeRenderableText", () => { expect(sanitized).toBe(input); }); + it("preserves long credential-like mixed alnum tokens for copy safety", () => { + const input = "e3b19c3b87bcf364b23eebb2c276e96ec478956ba1d84c93"; + const sanitized = sanitizeRenderableText(input); + + expect(sanitized).toBe(input); + }); + + it("preserves quoted credential-like mixed alnum tokens for copy safety", () => { + const input = "'e3b19c3b87bcf364b23eebb2c276e96ec478956ba1d84c93'"; + const sanitized = sanitizeRenderableText(input); + + expect(sanitized).toBe(input); + }); + it("wraps rtl lines with directional isolation marks", () => { const input = "مرحبا بالعالم"; const sanitized = sanitizeRenderableText(input); diff --git a/src/tui/tui-formatters.ts b/src/tui/tui-formatters.ts index 7d23a4c47f6..566839c7cf1 100644 --- a/src/tui/tui-formatters.ts +++ b/src/tui/tui-formatters.ts @@ -11,6 +11,8 @@ const BINARY_LINE_REPLACEMENT_THRESHOLD = 12; const URL_PREFIX_RE = /^(https?:\/\/|file:\/\/)/i; const WINDOWS_DRIVE_RE = /^[a-zA-Z]:[\\/]/; const FILE_LIKE_RE = /^[a-zA-Z0-9._-]+$/; +const EDGE_PUNCTUATION_RE = /^[`"'([{<]+|[`"')\]}>.,:;!?]+$/g; +const TOKENISH_MIN_LENGTH = 24; const RTL_SCRIPT_RE = /[\u0590-\u08ff\ufb1d-\ufdff\ufe70-\ufefc]/; const BIDI_CONTROL_RE = /[\u202a-\u202e\u2066-\u2069]/; const RTL_ISOLATE_START = "\u2067"; @@ -56,6 +58,9 @@ function chunkToken(token: string, maxChars: number): string[] { } function isCopySensitiveToken(token: string): boolean { + const coreToken = token.replace(EDGE_PUNCTUATION_RE, ""); + const candidate = coreToken || token; + if (URL_PREFIX_RE.test(token)) { return true; } @@ -73,7 +78,16 @@ function isCopySensitiveToken(token: string): boolean { if (token.includes("/") || token.includes("\\")) { return true; } - return token.includes("_") && FILE_LIKE_RE.test(token); + if (token.includes("_") && FILE_LIKE_RE.test(token)) { + return true; + } + + // Preserve long credential-like tokens (hex/base62/etc.) to avoid introducing + // visible spaces that users may copy back into secrets. + if (candidate.length >= TOKENISH_MIN_LENGTH && /[a-z]/i.test(candidate) && /\d/.test(candidate)) { + return true; + } + return false; } function normalizeLongTokenForDisplay(token: string): string {