🎉 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
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:
commit
b3489e1ede
983 files changed
+158475
No files matched your search
+164
@@ -0,0 +1,164 @@
|
||||
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()
|
||||
Reference in new issue
Block a user