Repository navigation
Expand file tree
/
Copy pathmain.tsx
More file actions
205 lines (171 loc) · 7.69 KB
/
Copy pathmain.tsx
File metadata and controls
205 lines (171 loc) · 7.69 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
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 { DirectoryProvider, FullscreenProvider, SessionProvider } from './contexts'
import { themeStore } from './store/themeStore'
import { affectsBoundServer } from './store/serverChangeScope'
import { serverStore } from './store/serverStore'
import { autoApproveStore } from './store/autoApproveStore'
import { serviceStore } from './store/serviceStore'
import { wslStore } from './store/wslStore'
import { reconnectSSE } from './api/events'
import { getSDKClientAsync, invalidateSDKClient } from './api/sdk'
import { resetPathModeCache } from './utils/directoryUtils'
import { isTauri, isTauriMobile } from './utils/tauri'
import { apiErrorHandler, globalErrorHandler } from './utils/errorHandling'
import { applyLocalServiceUrl } from './utils/localServiceUrl'
// 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()
// 订阅 WSL 后端状态推送(Windows 桌面端:事件驱动,非 Tauri 环境下静默)
wslStore.start()
// 全局 overlay 滚动条 — 等 DOM 就绪后启动
if (document.readyState === 'loading') {
document.addEventListener('DOMContentLoaded', initOverlayScrollbars)
} else {
// DOM 已就绪(defer script 或者 module)
requestAnimationFrame(initOverlayScrollbars)
}
// 注册 active server 入口变化 → 重建目标服务器的 SDK + 刷新 per-server 配置 + 重连 SSE
serverStore.onServerChange((serverId, reason) => {
// SDK client 按 serverId 缓存:仅重建目标服务器的 client。
// 这是「该服务器端点事实已变」的处理,与 active 无关,任何 reason 都要做
invalidateSDKClient(serverId)
if (isTauri()) {
void getSDKClientAsync(serverId).catch(err => apiErrorHandler('reinitialize sdk client after server endpoint change', err))
}
// 多服务器模式:messageStore / childSessionStore 的数据按 `serverId::sessionId`
// 分片存储,切换 active server 不应清空其他 pane 正在使用的服务器数据,因此不再 clearAll。
// 以下动作只作用于「当前 active 服务器的数据/连接」,必须按 reason 门控:
// - server-switch:active 本身变了(含删除回退)→ 缓存与 SSE 目标都换了
// - server-runtime-updated / local-runtime-url:仅当变的这台就是 active 才相关;
// 非 active 的 WSL 服务器重启不得重置 active 缓存、更不得重连 active 的 SSE
// (重连窗口会丢事件、会话列表闪烁)
if (!affectsBoundServer(undefined, serverId, reason, serverStore.getActiveServerId())) return
// 重置路径模式缓存(不同服务器可能是不同操作系统)
resetPathModeCache()
// 重新加载 auto-approve 开关状态(从新服务器的 storage key 读取)
autoApproveStore.reloadFromStorage()
// 重连 active server 的 SSE(会自动连到新的 server endpoint)
reconnectSSE()
})
const isNativeTauri = isTauri()
const isNativeTauriMobile = isNativeTauri && isTauriMobile()
interface StartOpencodeServiceResult {
started: boolean
startedByUs: boolean
/** 是否是注册表可认领的共享后台服务(serve --service;认领来的也算) */
managed?: boolean
url?: string | null
/** 连接用密码:注册表(管理模式)或服务端随机生成 / OPENCODE_PASSWORD / OPENCODE_SERVER_PASSWORD 环境变量(私有模式) */
serverPassword?: string | null
}
function configureNativeShell() {
if (!isNativeTauri) return
// 添加 CSS class 用于 safe-area 适配
document.documentElement.classList.add('tauri-app')
// 确保 viewport meta 包含 viewport-fit=cover(用于状态栏沉浸式)
const viewportMeta = document.querySelector('meta[name="viewport"]')
if (!viewportMeta) return
const content = viewportMeta.getAttribute('content') || ''
if (!content.includes('viewport-fit=cover')) {
viewportMeta.setAttribute('content', content + ', viewport-fit=cover')
}
}
async function initializeNativeDesktopService() {
if (!isNativeTauri || isNativeTauriMobile || !serviceStore.autoStart) return
const serverUrl = serverStore.getLocalServerUrl()
serviceStore.setStarting(true)
try {
const { invoke } = await import('@tauri-apps/api/core')
try {
const path = await invoke<string | null>('detect_opencode_binary', { envVars: serviceStore.envVarsRecord })
serviceStore.setDetectedBinaryPath(path)
} catch {
// Starting with PATH fallback is still useful if detection fails.
}
const result = await invoke<StartOpencodeServiceResult>('start_opencode_service', {
url: serverUrl,
binaryPath: serviceStore.effectiveBinaryPath,
envVars: serviceStore.envVarsRecord,
extraArgs: serviceStore.serveArgsList,
// 条目上已有的凭据:让「已经在跑」的探测通过 v2 的强制密码门
auth: serverStore.getLocalServer()?.auth
? ([serverStore.getLocalServer()!.auth!.username, serverStore.getLocalServer()!.auth!.password] as [string, string])
: null,
})
applyLocalServiceUrl(result.url, result.serverPassword)
serviceStore.setStartedByUs(result.startedByUs)
serviceStore.setManaged(result.managed ?? false)
serviceStore.setRunning(true)
if (result.started) {
console.info('[Service] opencode background service started by app')
} else if (result.managed) {
console.info('[Service] adopted running background service')
} else {
console.info('[Service] opencode serve already running')
}
} catch (err) {
apiErrorHandler('auto-start opencode serve', err)
} finally {
serviceStore.setStarting(false)
}
}
configureNativeShell()
// 全局错误处理 - 防止未捕获错误导致页面刷新
window.addEventListener('error', event => {
globalErrorHandler('uncaught error', event.error)
event.preventDefault()
})
window.addEventListener('unhandledrejection', event => {
globalErrorHandler('unhandled promise rejection', event.reason)
event.preventDefault()
})
function bootstrap() {
createRoot(document.getElementById('root')!).render(
<StrictMode>
<Suspense fallback={null}>
<DirectoryProvider>
<SessionProvider>
<FullscreenProvider>
<App />
</FullscreenProvider>
</SessionProvider>
</DirectoryProvider>
</Suspense>
</StrictMode>,
)
}
function startApp() {
bootstrap()
void initializeNativeDesktopService()
if (isNativeTauri) {
void getSDKClientAsync().catch(err => apiErrorHandler('initialize sdk client', err))
}
}
startApp()