import { stripLeadingInboundMetadata } from "../auto-reply/reply/strip-inbound-meta.js" ;
import { formatRawAssistantErrorForUi } from "../shared/assistant-error-format.js" ;
import { extractAssistantVisibleText } from "../shared/chat-message-content.js" ;
import { stripAnsi } from "../terminal/ansi.js" ;
import { formatTokenCount } from "../utils/usage-format.js" ;
const REPLACEMENT_CHAR_RE = /\uFFFD/g;
const MAX_TOKEN_CHARS = 32 ;
const LONG_TOKEN_RE = /\S{33 ,}/g;
const LONG_TOKEN_TEST_RE = /\S{33 ,}/;
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" ;
const RTL_ISOLATE_END = "\u2069" ;
function hasControlChars(text: string): boolean {
for (const char of text) {
const code = char .charCodeAt(0 );
const isAsciiControl = code <= 0 x1f && code !== 0 x09 && code !== 0 x0a && code !== 0 x0d;
const isC1Control = code >= 0 x7f && code <= 0 x9f;
if (isAsciiControl || isC1Control) {
return true ;
}
}
return false ;
}
function stripControlChars(text: string): string {
if (!hasControlChars(text)) {
return text;
}
let sanitized = "" ;
for (const char of text) {
const code = char .charCodeAt(0 );
const isAsciiControl = code <= 0 x1f && code !== 0 x09 && code !== 0 x0a && code !== 0 x0d;
const isC1Control = code >= 0 x7f && code <= 0 x9f;
if (!isAsciiControl && !isC1Control) {
sanitized += char ;
}
}
return sanitized;
}
function chunkToken(token: string, maxChars: number): string[] {
if (token.length <= maxChars) {
return [token];
}
const chunks: string[] = [];
for (let i = 0 ; i < token.length; i += maxChars) {
chunks.push(token.slice(i, i + maxChars));
}
return chunks;
}
function isCopySensitiveToken(token: string): boolean {
const coreToken = token.replace(EDGE_PUNCTUATION_RE, "" );
const candidate = coreToken || token;
if (URL_PREFIX_RE.test(token)) {
return true ;
}
if (
token.startsWith("/" ) ||
token.startsWith("~/" ) ||
token.startsWith("./" ) ||
token.startsWith("../" )
) {
return true ;
}
if (WINDOWS_DRIVE_RE.test(token) || token.startsWith("\\\\" )) {
return true ;
}
if (token.includes("/" ) || token.includes("\\" )) {
return true ;
}
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 {
// Preserve copy-sensitive tokens exactly (paths/urls/file-like names).
if (isCopySensitiveToken(token)) {
return token;
}
return chunkToken(token, MAX_TOKEN_CHARS).join(" " );
}
function redactBinaryLikeLine(line: string): string {
const replacementCount = (line.match(REPLACEMENT_CHAR_RE) || []).length;
if (
replacementCount >= BINARY_LINE_REPLACEMENT_THRESHOLD &&
replacementCount * 2 >= line.length
) {
return "[binary data omitted]" ;
}
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;
}
const hasAnsi = text.includes("\u001b" );
const hasReplacementChars = text.includes("\uFFFD" );
const hasLongTokens = LONG_TOKEN_TEST_RE.test(text);
const hasControls = hasControlChars(text);
if (!hasAnsi && !hasReplacementChars && !hasLongTokens && !hasControls) {
return applyRtlIsolation(text);
}
const withoutAnsi = hasAnsi ? stripAnsi(text) : text;
const withoutControlChars = hasControls ? stripControlChars(withoutAnsi) : withoutAnsi;
const redacted = hasReplacementChars
? withoutControlChars
.split("\n" )
.map((line) => redactBinaryLikeLine(line))
.join("\n" )
: withoutControlChars;
const tokenSafe = LONG_TOKEN_TEST_RE.test(redacted)
? redacted.replace(LONG_TOKEN_RE, normalizeLongTokenForDisplay)
: redacted;
return applyRtlIsolation(tokenSafe);
}
export function resolveFinalAssistantText(params: {
finalText?: string | null ;
streamedText?: string | null ;
errorMessage?: string | null ;
}) {
const finalText = params.finalText ?? "" ;
if (finalText.trim()) {
return finalText;
}
const streamedText = params.streamedText ?? "" ;
if (streamedText.trim()) {
return streamedText;
}
const errorMessage = params.errorMessage ?? "" ;
if (errorMessage.trim()) {
return formatRawAssistantErrorForUi(errorMessage);
}
return "(no output)" ;
}
export function composeThinkingAndContent(params: {
thinkingText?: string;
contentText?: string;
showThinking?: boolean ;
}) {
const thinkingText = params.thinkingText?.trim() ?? "" ;
const contentText = params.contentText?.trim() ?? "" ;
const parts: string[] = [];
if (params.showThinking && thinkingText) {
parts.push(`[thinking]\n${thinkingText}`);
}
if (contentText) {
parts.push(contentText);
}
return parts.join("\n\n" ).trim();
}
function asMessageRecord(message: unknown): Record<string, unknown> | undefined {
if (!message || typeof message !== "object" ) {
return undefined;
}
return message as Record<string, unknown>;
}
function resolveMessageRecord(
message: unknown,
): { record: Record<string, unknown>; content: unknown } | undefined {
const record = asMessageRecord(message);
if (!record) {
return undefined;
}
return { record, content: record.content };
}
function formatAssistantErrorFromRecord(record: Record<string, unknown>): string {
const stopReason = typeof record.stopReason === "string" ? record.stopReason : "" ;
if (stopReason !== "error" ) {
return "" ;
}
const errorMessage = typeof record.errorMessage === "string" ? record.errorMessage : "" ;
return formatRawAssistantErrorForUi(errorMessage);
}
function collectSanitizedBlockStrings(params: {
content: unknown;
blockType: "text" | "thinking" ;
valueKey: "text" | "thinking" ;
}): string[] {
if (!Array.isArray(params.content)) {
return [];
}
const parts: string[] = [];
for (const block of params.content) {
if (!block || typeof block !== "object" ) {
continue ;
}
const rec = block as Record<string, unknown>;
if (rec.type === params.blockType && typeof rec[params.valueKey] === "string" ) {
parts.push(sanitizeRenderableText(rec[params.valueKey] as string));
}
}
return parts;
}
/**
* Extract ONLY thinking blocks from message content.
* Model-agnostic: returns empty string if no thinking blocks exist.
*/
export function extractThinkingFromMessage(message: unknown): string {
const resolved = resolveMessageRecord(message);
if (!resolved) {
return "" ;
}
const { content } = resolved;
if (typeof content === "string" ) {
return "" ;
}
const parts = collectSanitizedBlockStrings({
content,
blockType: "thinking" ,
valueKey: "thinking" ,
});
return parts.join("\n" ).trim();
}
/**
* Extract ONLY text content blocks from message (excludes thinking).
* Model-agnostic: works for any model with text content blocks.
*/
export function extractContentFromMessage(message: unknown): string {
const resolved = resolveMessageRecord(message);
if (!resolved) {
return "" ;
}
const { record, content } = resolved;
if (record.role === "assistant" ) {
if (typeof content === "string" ) {
return sanitizeRenderableText(content).trim();
}
if (Array.isArray(content)) {
return extractAssistantRenderableContent(record);
}
}
if (typeof content === "string" ) {
return sanitizeRenderableText(content).trim();
}
const parts = collectSanitizedBlockStrings({
content,
blockType: "text" ,
valueKey: "text" ,
});
if (parts.length > 0 ) {
return parts.join("\n" ).trim();
}
return formatAssistantErrorFromRecord(record);
}
function extractAssistantRenderableContent(record: Record<string, unknown>): string {
const visible = sanitizeRenderableText(extractAssistantVisibleText(record) ?? "" ).trim();
if (visible) {
return visible;
}
return formatAssistantErrorFromRecord(record);
}
function extractTextBlocks(content: unknown, opts?: { includeThinking?: boolean }): string {
if (typeof content === "string" ) {
return sanitizeRenderableText(content).trim();
}
if (!Array.isArray(content)) {
return "" ;
}
const textParts = collectSanitizedBlockStrings({
content,
blockType: "text" ,
valueKey: "text" ,
});
const thinkingParts =
opts?.includeThinking === true
? collectSanitizedBlockStrings({
content,
blockType: "thinking" ,
valueKey: "thinking" ,
})
: [];
return composeThinkingAndContent({
thinkingText: thinkingParts.join("\n" ).trim(),
contentText: textParts.join("\n" ).trim(),
showThinking: opts?.includeThinking ?? false ,
});
}
export function extractTextFromMessage(
message: unknown,
opts?: { includeThinking?: boolean },
): string {
const record = asMessageRecord(message);
if (!record) {
return "" ;
}
if (record.role === "assistant" ) {
return composeThinkingAndContent({
thinkingText: extractThinkingFromMessage(record),
contentText: extractAssistantRenderableContent(record),
showThinking: opts?.includeThinking ?? false ,
});
}
const text = extractTextBlocks(record.content, opts);
if (text) {
if (record.role === "user" || record.command === true ) {
return stripLeadingInboundMetadata(text);
}
return text;
}
const errorText = formatAssistantErrorFromRecord(record);
if (!errorText) {
return "" ;
}
return errorText;
}
export function isCommandMessage(message: unknown): boolean {
if (!message || typeof message !== "object" ) {
return false ;
}
return (message as Record<string, unknown>).command === true ;
}
export function formatTokens(total?: number | null , context?: number | null ) {
if (total == null && context == null ) {
return "tokens ?" ;
}
const totalLabel = total == null ? "?" : formatTokenCount(total);
if (context == null ) {
return `tokens ${totalLabel}`;
}
const pct =
typeof total === "number" && context > 0
? Math.min(999 , Math.round((total / context) * 100 ))
: null ;
return `tokens ${totalLabel}/${formatTokenCount(context)}${pct !== null ? ` (${pct}%)` : "" }`;
}
export function formatContextUsageLine(params: {
total?: number | null ;
context?: number | null ;
remaining?: number | null ;
percent?: number | null ;
}) {
const totalLabel = typeof params.total === "number" ? formatTokenCount(params.total) : "?" ;
const ctxLabel = typeof params.context === "number" ? formatTokenCount(params.context) : "?" ;
const pct = typeof params.percent === "number" ? Math.min(999 , Math.round(params.percent)) : null ;
const remainingLabel =
typeof params.remaining === "number" ? `${formatTokenCount(params.remaining)} left` : null ;
const pctLabel = pct !== null ? `${pct}%` : null ;
const extra = [remainingLabel, pctLabel].filter(Boolean ).join(", " );
return `tokens ${totalLabel}/${ctxLabel}${extra ? ` (${extra})` : "" }`;
}
export function asString(value: unknown, fallback = "" ): string {
if (typeof value === "string" ) {
return value;
}
if (typeof value === "number" || typeof value === "boolean" ) {
return String(value);
}
return fallback;
}
Messung V0.5 in Prozent C=99 H=98 G=98
¤ Dauer der Verarbeitung: 0.11 Sekunden
(vorverarbeitet am 2026-06-09)
¤
*© Formatika GbR, Deutschland