Files
PiAgentUI/src/utils/directoryUtils.ts
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

300 lines
8.0 KiB
TypeScript
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
// ============================================
// 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 }