Files
PiAgentUI/src/utils/ansiUtils.ts
T
Evandro b3489e1ede
Build & Push Frontend Docker Image / build-and-push (push) Canceled after 0s
Build Validation / validate (push) Canceled after 6s
Build & Push Backend Docker Image / build-and-push (push) Failing after 10m10s
Build & Push Gateway Docker Image / build-and-push (push) Failing after 10m9s
🎉 Initial commit: PiAgentUI — AI Agent Desktop & Web UI
A professional workspace for AI coding agents with:
- Real-time streaming chat with SSE
- Session management (fork, clone, tree navigation)
- MCP tools browser & metadata panel
- Skills browser (global, project, package sources)
- Voice input & STT provider configuration
- Integrated terminal & file explorer
- Model selector with capability metadata
- Keyboard-first UI with command palette
- 8 languages (en, pt-BR, es, ja, zh-CN, ar, bn, hi)
- Tauri 2 desktop app
- React 19 + TypeScript + Vite 8 + Tailwind CSS 4
- Pi Agent extension runtime

Originally forked from OpenCodeUI, now an independent project.
License: GPL-3.0-only
2026-06-01 00:50:06 -03:00

141 lines
3.5 KiB
TypeScript

/**
* ANSI escape code 处理工具
*
* 支持两种模式:
* - stripAnsi: 纯净文本,去掉所有控制符
* - parseAnsi: 解析为带颜色信息的 segments,用于渲染彩色终端输出
*/
// ANSI escape sequence 正则
// 匹配 CSI (Control Sequence Introducer) 序列:ESC[ ... 终止符
// eslint-disable-next-line no-control-regex
const ANSI_RE = /\x1b\[[0-9;]*m/g
/**
* 去掉所有 ANSI escape codes
*/
export function stripAnsi(text: string): string {
return text.replace(ANSI_RE, '')
}
// ============================================
// ANSI → colored segments
// ============================================
export interface AnsiSegment {
text: string
fg?: string
bold?: boolean
dim?: boolean
italic?: boolean
}
// 标准 ANSI 3/4-bit 颜色映射
const ANSI_COLORS: Record<number, string> = {
30: 'var(--ansi-black, #545454)',
31: 'var(--ansi-red, #cf4647)',
32: 'var(--ansi-green, #4ea24c)',
33: 'var(--ansi-yellow, #c4a500)',
34: 'var(--ansi-blue, #3d7ec7)',
35: 'var(--ansi-magenta, #b44e91)',
36: 'var(--ansi-cyan, #21a8a5)',
37: 'var(--ansi-white, #cccccc)',
// 高亮/明亮色
90: 'var(--ansi-bright-black, #767676)',
91: 'var(--ansi-bright-red, #f0706f)',
92: 'var(--ansi-bright-green, #7bc96f)',
93: 'var(--ansi-bright-yellow, #e3d94e)',
94: 'var(--ansi-bright-blue, #6cb5ed)',
95: 'var(--ansi-bright-magenta, #d07fd0)',
96: 'var(--ansi-bright-cyan, #5fd7d7)',
97: 'var(--ansi-bright-white, #eeeeee)',
}
interface AnsiState {
fg?: string
bold: boolean
dim: boolean
italic: boolean
}
/**
* 解析含 ANSI 控制符的文本为 segments
*/
export function parseAnsi(text: string): AnsiSegment[] {
const segments: AnsiSegment[] = []
const state: AnsiState = { bold: false, dim: false, italic: false }
let lastIndex = 0
ANSI_RE.lastIndex = 0
let match: RegExpExecArray | null
while ((match = ANSI_RE.exec(text)) !== null) {
// 控制符前面的文本
if (match.index > lastIndex) {
const chunk = text.slice(lastIndex, match.index)
if (chunk) {
segments.push({
text: chunk,
fg: state.fg,
bold: state.bold || undefined,
dim: state.dim || undefined,
italic: state.italic || undefined,
})
}
}
lastIndex = ANSI_RE.lastIndex
// 解析 SGR 参数
const params = match[0].slice(2, -1) // 去掉 ESC[ 和 m
const codes = params === '' ? [0] : params.split(';').map(Number)
for (const code of codes) {
if (code === 0) {
// Reset
state.fg = undefined
state.bold = false
state.dim = false
state.italic = false
} else if (code === 1) {
state.bold = true
} else if (code === 2) {
state.dim = true
} else if (code === 3) {
state.italic = true
} else if (code === 22) {
state.bold = false
state.dim = false
} else if (code === 23) {
state.italic = false
} else if (code === 39) {
state.fg = undefined
} else if (ANSI_COLORS[code]) {
state.fg = ANSI_COLORS[code]
}
}
}
// 剩余文本
if (lastIndex < text.length) {
const chunk = text.slice(lastIndex)
if (chunk) {
segments.push({
text: chunk,
fg: state.fg,
bold: state.bold || undefined,
dim: state.dim || undefined,
italic: state.italic || undefined,
})
}
}
return segments
}
/**
* 检测文本是否包含 ANSI escape codes
*/
export function hasAnsi(text: string): boolean {
return ANSI_RE.test(text)
}