mirror of
https://github.com/moltbot/moltbot.git
synced 2026-03-08 06:54:24 +00:00
TUI: preserve RTL text order in terminal output
This commit is contained in:
@@ -32,6 +32,7 @@ Docs: https://docs.openclaw.ai
|
||||
- Cron/Scheduling: validate runtime cron expressions before schedule/stagger evaluation so malformed persisted jobs report a clear `invalid cron schedule: expr is required` error instead of crashing with `undefined.trim` failures and auto-disable churn. (#23223) Thanks @asimons81.
|
||||
- Memory/QMD: migrate legacy unscoped collection bindings (for example `memory-root`) to per-agent scoped names (for example `memory-root-main`) during startup when safe, so QMD-backed `memory_search` no longer fails with `Collection not found` after upgrades. (#23228, #20727) Thanks @JLDynamics and @AaronFaby.
|
||||
- TUI/Input: enable multiline-paste burst coalescing on macOS Terminal.app and iTerm so pasted blocks no longer submit line-by-line as separate messages. (#18809) Thanks @fwends.
|
||||
- TUI/RTL: isolate right-to-left script lines (Arabic/Hebrew ranges) with Unicode bidi isolation marks in TUI text sanitization so RTL assistant output no longer renders in reversed visual order in terminal chat panes. (#21936) Thanks @Asm3r96.
|
||||
- TUI/Status: request immediate renders after setting `sending`/`waiting` activity states so in-flight runs always show visible progress indicators instead of appearing idle until completion. (#21549) Thanks @13Guinness.
|
||||
- Agents/Fallbacks: treat JSON payloads with `type: "api_error"` + `"Internal server error"` as transient failover errors so Anthropic 500-style failures trigger model fallback. (#23193) Thanks @jarvis-lane.
|
||||
- Agents/Transcripts: validate assistant tool-call names (syntax/length + registered tool allowlist) before transcript persistence and during replay sanitization so malformed failover tool names no longer poison sessions with repeated provider HTTP 400 errors. (#23324) Thanks @johnsantry.
|
||||
|
||||
@@ -251,4 +251,25 @@ describe("sanitizeRenderableText", () => {
|
||||
|
||||
expect(sanitized).toBe(input);
|
||||
});
|
||||
|
||||
it("wraps rtl lines with directional isolation marks", () => {
|
||||
const input = "مرحبا بالعالم";
|
||||
const sanitized = sanitizeRenderableText(input);
|
||||
|
||||
expect(sanitized).toBe("\u2067مرحبا بالعالم\u2069");
|
||||
});
|
||||
|
||||
it("only wraps lines that contain rtl script", () => {
|
||||
const input = "hello\nمرحبا";
|
||||
const sanitized = sanitizeRenderableText(input);
|
||||
|
||||
expect(sanitized).toBe("hello\n\u2067مرحبا\u2069");
|
||||
});
|
||||
|
||||
it("does not double-wrap lines that already include bidi controls", () => {
|
||||
const input = "\u2067مرحبا\u2069";
|
||||
const sanitized = sanitizeRenderableText(input);
|
||||
|
||||
expect(sanitized).toBe(input);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -11,6 +11,10 @@ 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 RTL_SCRIPT_RE = /[\u0590-\u08ff\ufb1d-\ufdff\ufe70-\ufefc]/;
|
||||
const BIDI_CONTROL_RE = /[\u202a-\u202e\u2066-\u2069]/;
|
||||
const RTL_ISOLATE_START = "\u2067";
|
||||
const RTL_ISOLATE_END = "\u2069";
|
||||
|
||||
function hasControlChars(text: string): boolean {
|
||||
for (const char of text) {
|
||||
@@ -91,6 +95,23 @@ function redactBinaryLikeLine(line: string): string {
|
||||
return line;
|
||||
}
|
||||
|
||||
function isolateRtlLine(line: string): string {
|
||||
if (!RTL_SCRIPT_RE.test(line) || BIDI_CONTROL_RE.test(line)) {
|
||||
return line;
|
||||
}
|
||||
return `${RTL_ISOLATE_START}${line}${RTL_ISOLATE_END}`;
|
||||
}
|
||||
|
||||
function applyRtlIsolation(text: string): string {
|
||||
if (!RTL_SCRIPT_RE.test(text)) {
|
||||
return text;
|
||||
}
|
||||
return text
|
||||
.split("\n")
|
||||
.map((line) => isolateRtlLine(line))
|
||||
.join("\n");
|
||||
}
|
||||
|
||||
export function sanitizeRenderableText(text: string): string {
|
||||
if (!text) {
|
||||
return text;
|
||||
@@ -101,7 +122,7 @@ export function sanitizeRenderableText(text: string): string {
|
||||
const hasLongTokens = LONG_TOKEN_TEST_RE.test(text);
|
||||
const hasControls = hasControlChars(text);
|
||||
if (!hasAnsi && !hasReplacementChars && !hasLongTokens && !hasControls) {
|
||||
return text;
|
||||
return applyRtlIsolation(text);
|
||||
}
|
||||
|
||||
const withoutAnsi = hasAnsi ? stripAnsi(text) : text;
|
||||
@@ -112,9 +133,10 @@ export function sanitizeRenderableText(text: string): string {
|
||||
.map((line) => redactBinaryLikeLine(line))
|
||||
.join("\n")
|
||||
: withoutControlChars;
|
||||
return LONG_TOKEN_TEST_RE.test(redacted)
|
||||
const tokenSafe = LONG_TOKEN_TEST_RE.test(redacted)
|
||||
? redacted.replace(LONG_TOKEN_RE, normalizeLongTokenForDisplay)
|
||||
: redacted;
|
||||
return applyRtlIsolation(tokenSafe);
|
||||
}
|
||||
|
||||
export function resolveFinalAssistantText(params: {
|
||||
|
||||
Reference in New Issue
Block a user