🎉 Initial commit: PiAgentUI — AI Agent Desktop & Web UI
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

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
This commit is contained in:
Evandro committed 2026-06-01 00:50:06 -03:00
commit b3489e1ede
983 files changed
+158475

No files matched your search

+299
View File
@@ -0,0 +1,299 @@
// ============================================
// Directory Path Utilities
// 统一的目录路径处理工具
// ============================================
//
// 设计原则:
// 1. 前端内部存储:统一使用正斜杠 (URL友好,跨平台)
// 2. 后端 API 请求:根据 pathMode 决定用正斜杠还是反斜杠
// 3. 路径比较:统一规范化后比较(正斜杠 + 小写 + 无末尾斜杠)
//
import { serverStorage } from './perServerStorage'
// ============================================
// Path Mode Configuration
// ============================================
/**
* 路径模式
* - auto: 自动检测(根据后端响应判断)
* - unix: 强制使用正斜杠 /
* - windows: 强制使用反斜杠 \
*/
export type PathMode = 'auto' | 'unix' | 'windows'
/**
* 检测到的实际路径风格
*/
export type DetectedPathStyle = 'unix' | 'windows'
const STORAGE_KEY_PATH_MODE = 'opencode-path-mode'
const STORAGE_KEY_DETECTED_STYLE = 'opencode-detected-path-style'
let _pathMode: PathMode | null = null
let _detectedStyle: DetectedPathStyle | null = null
/**
* 重置路径模式缓存(服务器切换时调用)
* 下次 getPathMode / getDetectedPathStyle 会从 serverStorage 重新读取
*/
export function resetPathModeCache(): void {
_pathMode = null
_detectedStyle = null
}
// Path mode change listeners
type PathModeListener = (mode: PathMode, effectiveStyle: DetectedPathStyle) => void
const _listeners: Set<PathModeListener> = new Set()
/**
* 订阅路径模式变化
*/
export function subscribePathMode(listener: PathModeListener): () => void {
_listeners.add(listener)
return () => _listeners.delete(listener)
}
/**
* 通知所有监听器
*/
function notifyListeners(): void {
const mode = getPathMode()
const style = getEffectivePathStyle()
_listeners.forEach(listener => listener(mode, style))
}
/**
* 获取当前路径模式设置
*/
export function getPathMode(): PathMode {
if (_pathMode === null) {
try {
const saved = serverStorage.get(STORAGE_KEY_PATH_MODE)
if (saved === 'unix' || saved === 'windows') {
_pathMode = saved
} else {
_pathMode = 'auto' // 默认自动检测
}
} catch {
_pathMode = 'auto'
}
}
return _pathMode
}
/**
* 设置路径模式
*/
export function setPathMode(mode: PathMode): void {
_pathMode = mode
try {
serverStorage.set(STORAGE_KEY_PATH_MODE, mode)
} catch {
// ignore
}
notifyListeners()
}
/**
* 获取检测到的路径风格
*/
export function getDetectedPathStyle(): DetectedPathStyle {
if (_detectedStyle === null) {
try {
const saved = serverStorage.get(STORAGE_KEY_DETECTED_STYLE)
_detectedStyle = saved === 'windows' ? 'windows' : 'unix'
} catch {
_detectedStyle = 'unix'
}
}
return _detectedStyle
}
/**
* 设置检测到的路径风格(由自动检测逻辑调用)
*/
export function setDetectedPathStyle(style: DetectedPathStyle): void {
const previousStyle = _detectedStyle
_detectedStyle = style
try {
serverStorage.set(STORAGE_KEY_DETECTED_STYLE, style)
} catch {
// ignore
}
// 只在 auto 模式且风格变化时通知
if (getPathMode() === 'auto' && previousStyle !== style) {
notifyListeners()
}
}
/**
* 获取实际生效的路径风格
* - 如果是 auto 模式,返回检测到的风格
* - 否则返回用户设置的模式
*/
export function getEffectivePathStyle(): DetectedPathStyle {
const mode = getPathMode()
if (mode === 'auto') {
return getDetectedPathStyle()
}
return mode
}
/**
* 是否为 Windows 路径模式(实际生效的)
*/
export function isWindowsPathMode(): boolean {
return getEffectivePathStyle() === 'windows'
}
/**
* 根据当前模式格式化路径用于 API 请求
* - unix 模式: 转正斜杠
* - windows 模式: 转反斜杠
*/
export function formatPathForApi(dir: string | undefined | null): string | undefined {
if (!dir) return undefined
let trimmed = dir.replace(/[/\\]+$/, '') // 移除末尾斜杠
// 根路径保护:/ → "",C:/ → "C:",需要恢复斜杠
if (!trimmed) {
trimmed = '/'
} else if (/^[a-zA-Z]:$/.test(trimmed)) {
trimmed = trimmed + '/'
}
if (isWindowsPathMode()) {
return trimmed.replace(/\//g, '\\')
} else {
return trimmed.replace(/\\/g, '/')
}
}
/**
* 从后端响应中检测路径风格
* 分析路径字符串,判断后端使用的是哪种斜杠风格
*/
export function detectPathStyleFromResponse(path: string | undefined | null): DetectedPathStyle | null {
if (!path) return null
const backslashCount = (path.match(/\\/g) || []).length
const forwardSlashCount = (path.match(/\//g) || []).length
// 如果有反斜杠且多于正斜杠,判定为 Windows 风格
if (backslashCount > 0 && backslashCount >= forwardSlashCount) {
return 'windows'
}
// 如果有正斜杠,判定为 Unix 风格
if (forwardSlashCount > 0) {
return 'unix'
}
return null
}
/**
* 自动检测并更新路径风格
* 调用此函数时传入后端返回的路径,会自动更新检测结果
*/
export function autoDetectPathStyle(path: string | undefined | null): void {
const detected = detectPathStyleFromResponse(path)
if (detected) {
setDetectedPathStyle(detected)
}
}
// ============================================
// Path Normalization (Internal Use)
// ============================================
/**
* 规范化目录路径为正斜杠格式
* 用于前端内部存储和 URL
*
* @example
* normalizeToForwardSlash('E:\\dev\\project') // 'E:/dev/project'
* normalizeToForwardSlash('E:/dev/project/') // 'E:/dev/project'
*/
export function normalizeToForwardSlash(dir: string | undefined | null): string {
if (!dir) return ''
return dir
.replace(/\\/g, '/') // 反斜杠 → 正斜杠
.replace(/\/+$/, '') // 移除末尾斜杠
}
/**
* 规范化目录路径用于比较
* - 统一使用正斜杠
* - 移除末尾斜杠
* - 转小写(Windows 路径不区分大小写)
*
* @example
* normalizeForComparison('E:\\Dev\\Project') // 'e:/dev/project'
*/
export function normalizeForComparison(dir: string | undefined | null): string {
if (!dir) return ''
return dir
.replace(/\\/g, '/') // 反斜杠 → 正斜杠
.replace(/\/+$/, '') // 移除末尾斜杠
.toLowerCase() // Windows 路径不区分大小写
}
/**
* 比较两个目录路径是否相同
* 处理斜杠、大小写差异
*
* @example
* isSameDirectory('E:\\dev\\project', 'E:/dev/project') // true
* isSameDirectory('E:/Dev/Project', 'e:/dev/project') // true
*/
export function isSameDirectory(dir1: string | undefined | null, dir2: string | undefined | null): boolean {
return normalizeForComparison(dir1) === normalizeForComparison(dir2)
}
/**
* 从路径中提取目录名(最后一个部分)
*
* @example
* getDirectoryName('E:/dev/my-project') // 'my-project'
* getDirectoryName('E:\\dev\\my-project\\') // 'my-project'
*/
export function getDirectoryName(dir: string | undefined | null): string {
if (!dir) return ''
const normalized = normalizeToForwardSlash(dir)
const parts = normalized.split('/').filter(Boolean)
const name = parts[parts.length - 1] || normalized
// 解码 URL 编码的中文等字符
try {
return decodeURIComponent(name)
} catch {
return name
}
}
/**
* 检查路径是否是 Windows 绝对路径
*
* @example
* isWindowsAbsolutePath('E:/dev') // true
* isWindowsAbsolutePath('/home/user') // false
*/
export function isWindowsAbsolutePath(path: string): boolean {
return /^[a-zA-Z]:/.test(path)
}
/**
* 检查路径是否是绝对路径(Windows 或 Unix)
*/
export function isAbsolutePath(path: string): boolean {
return isWindowsAbsolutePath(path) || path.startsWith('/')
}
// ============================================
// Re-export from stringUtils for compatibility
// ============================================
// 保持向后兼容,逐步废弃 stringUtils 中的目录函数
export { normalizeForComparison as normalizeDirectoryPath }