Files
PiAgentUI/src/main.tsx
T
Evandro 98b5db029f wip: snapshot das alterações não commitadas (2026-10-20)
Origem: D:/Documents/Github/PiAgentUI/PiAgentUi
Arquivos omitidos (segredos, bancos de dados, logs, >50 MB): 4
2026-10-20 00:48:28 -03:00

169 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 { createRoot } from 'react-dom/client'
import { createElement } from 'react'
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 { 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()
type PiAgentConnection = { port: number; token: string }
type PiAgentServiceStart = {
started: boolean
connection?: PiAgentConnection | null
agent_dir?: string
session_dir?: string
}
function installPiAgentConnection(conn: PiAgentConnection) {
;(globalThis as any).window.PI_BOOT_DATA = {
baseUrl: `http://127.0.0.1:${conn.port}`,
token: conn.token,
}
}
// 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')
}
}
// The isolated Pi backend is discovered/started inside bootstrap() before React renders.
}
// 全局错误处理 - 防止未捕获错误导致页面刷新
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')
let conn = await invoke<PiAgentConnection | null>('get_pi_agent_connection')
const shouldAutoStartPi = serviceStore.autoStart || navigator.webdriver === true
if (!conn && !isNativeTauriMobile && shouldAutoStartPi) {
serviceStore.setStarting(true)
const result = await invoke<PiAgentServiceStart>('start_pi_service', {
url: serverStore.getActiveBaseUrl() || 'http://127.0.0.1:4096',
binaryPath: serviceStore.effectiveBinaryPath,
envVars: serviceStore.envVarsRecord,
})
serviceStore.setStartedByUs(result.started)
serviceStore.setRunning(Boolean(result.connection))
conn = result.connection ?? null
}
if (conn) {
console.log('[PiAgentUi] Discovered isolated Pi Agent connection info:', conn)
installPiAgentConnection(conn)
} else {
console.log('[PiAgentUi] No isolated Pi Agent backend found. App will run in standalone mode.')
}
} catch (err) {
console.error('[PiAgentUi] Failed to initialize Pi Agent backend:', err)
serviceStore.setRunning(false)
} finally {
serviceStore.setStarting(false)
}
}
createRoot(document.getElementById('root')!).render(
createElement(
ErrorBoundary,
null,
createElement(
DirectoryProvider,
null,
createElement(SessionProvider, null, createElement(App)),
),
),
)
}
void bootstrap()