Files
PiAgentUI/src/main.tsx
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

165 lines
5.8 KiB
TypeScript
Raw 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.
import { StrictMode, Suspense } from 'react'
import { createRoot } from 'react-dom/client'
import 'katex/dist/katex.min.css'
import './index.css'
import './i18n'
import { initOverlayScrollbars } from './lib/overlayScrollbar'
import App from './App.tsx'
import { ErrorBoundary } from './components/ErrorBoundary.tsx'
import { DirectoryProvider, SessionProvider } from './contexts'
import { themeStore } from './store/themeStore'
import { serverStore } from './store/serverStore'
import { messageStore } from './store/messageStore'
import { childSessionStore } from './store/childSessionStore'
import { todoStore } from './store/todoStore'
import { autoApproveStore } from './store/autoApproveStore'
import { serviceStore } from './store/serviceStore'
import { reconnectSSE } from './api/events'
import { resetPathModeCache } from './utils/directoryUtils'
import { isTauri, isTauriMobile } from './utils/tauri'
import { apiErrorHandler, globalErrorHandler } from './utils/errorHandling'
// Polyfill: randomUUID 在非 HTTPS 环境可能缺失(如局域网 HTTP)
// 统一补齐,避免业务层 scattered fallback。
function ensureRandomUUID() {
const cryptoObj = globalThis.crypto as Crypto & { randomUUID?: () => string }
if (!cryptoObj || typeof cryptoObj.getRandomValues !== 'function') return
if (typeof cryptoObj.randomUUID === 'function') return
cryptoObj.randomUUID = () => {
const bytes = new Uint8Array(16)
cryptoObj.getRandomValues(bytes)
// RFC 4122 v4
bytes[6] = (bytes[6] & 0x0f) | 0x40
bytes[8] = (bytes[8] & 0x3f) | 0x80
const hex = Array.from(bytes, b => b.toString(16).padStart(2, '0'))
return `${hex.slice(0, 4).join('')}-${hex.slice(4, 6).join('')}-${hex.slice(6, 8).join('')}-${hex.slice(8, 10).join('')}-${hex.slice(10).join('')}`
}
}
ensureRandomUUID()
// 禁用浏览器的 scroll restoration(刷新时不恢复旧 scrollTop),
// 由 ChatArea 自行控制定位
if ('scrollRestoration' in history) {
history.scrollRestoration = 'manual'
}
// 初始化主题系统(在 React 渲染前注入 CSS 变量,避免闪烁)
themeStore.init()
// 全局 overlay 滚动条 — 等 DOM 就绪后启动
if (document.readyState === 'loading') {
document.addEventListener('DOMContentLoaded', initOverlayScrollbars)
} else {
// DOM 已就绪(defer script 或者 module)
requestAnimationFrame(initOverlayScrollbars)
}
// 注册 server 切换 → 清理所有 server-specific 状态 + SSE 重连
serverStore.onServerChange(() => {
// 1. 清空内存中的 session/消息数据
messageStore.clearAll()
childSessionStore.clearAll()
todoStore.clearAll()
// 2. 重置路径模式缓存(不同服务器可能是不同操作系统)
resetPathModeCache()
// 4. 重新加载 auto-approve 开关状态(从新服务器 of storage key 读取)
autoApproveStore.reloadFromStorage()
// 5. 重连 SSE(会自动连到新服务器)
reconnectSSE()
})
const isNativeTauri = isTauri()
const isNativeTauriMobile = isNativeTauri && isTauriMobile()
// Tauri 原生 app 初始化
if (isNativeTauri) {
// 添加 CSS class 用于 safe-area 适配
document.documentElement.classList.add('tauri-app')
// 确保 viewport meta 包含 viewport-fit=cover(用于状态栏沉浸式)
const viewportMeta = document.querySelector('meta[name="viewport"]')
if (viewportMeta) {
const content = viewportMeta.getAttribute('content') || ''
if (!content.includes('viewport-fit=cover')) {
viewportMeta.setAttribute('content', content + ', viewport-fit=cover')
}
}
// Auto-start pi serve(如果设置开启)
if (!isNativeTauriMobile && serviceStore.autoStart) {
const serverUrl = serverStore.getActiveBaseUrl() || 'http://127.0.0.1:4096'
const binaryPath = serviceStore.effectiveBinaryPath
import('@tauri-apps/api/core').then(({ invoke }) => {
serviceStore.setStarting(true)
invoke<boolean>('start_pi_service', { url: serverUrl, binaryPath, envVars: serviceStore.envVarsRecord })
.then(weStarted => {
serviceStore.setStartedByUs(weStarted)
serviceStore.setRunning(true)
serviceStore.setStarting(false)
if (weStarted) {
console.info('[Service] pi serve started by app')
} else {
console.info('[Service] pi serve already running')
}
})
.catch(err => {
serviceStore.setStarting(false)
apiErrorHandler('auto-start pi serve', err)
})
})
}
}
// 全局错误处理 - 防止未捕获错误导致页面刷新
window.addEventListener('error', event => {
globalErrorHandler('uncaught error', event.error)
event.preventDefault()
})
window.addEventListener('unhandledrejection', event => {
globalErrorHandler('unhandled promise rejection', event.reason)
event.preventDefault()
})
async function bootstrap() {
if (isTauri()) {
try {
const { invoke } = await import('@tauri-apps/api/core')
const conn = await invoke<{ port: number; token: string } | null>('get_pi_agent_connection')
if (conn) {
console.log('[PiAgentUi] Discovered Pi Agent connection info:', conn)
;(globalThis as any).window.PI_BOOT_DATA = {
baseUrl: `http://127.0.0.1:${conn.port}`,
token: conn.token,
}
} else {
console.log('[PiAgentUi] No active Pi Agent connection file found. App will run in standalone mode.')
}
} catch (err) {
console.error('[PiAgentUi] Failed to get Pi Agent connection:', err)
}
}
createRoot(document.getElementById('root')!).render(
<StrictMode>
<ErrorBoundary>
<Suspense fallback={null}>
<DirectoryProvider>
<SessionProvider>
<App />
</SessionProvider>
</DirectoryProvider>
</Suspense>
</ErrorBoundary>
</StrictMode>,
)
}
void bootstrap()