mirror of
https://github.com/wu736139669/hapi.git
synced 2026-10-06 18:39:47 +00:00
Six text conflicts, all resolved:
- cli/apiMachine + runner/run: adopt upstream's MachinePathPolicy; keep the
local policy that browsing stays disabled until --workspace-root is set
(banner text + upstream's browse test adapted to assert the local gate).
- cli/codex root.test.ts: keep both the syncHistory split test and upstream's
change_title/name test; fixture return merges metadata + syncHistory.
- web/SessionList: combine the local personal/global pinned split with
upstream's search-relevance ranking (deps union).
- web/AskUserQuestionFooter: take upstream's draft-restore effect and thread
the local skippedByQuestion through the draft store; the local DSH skip test
moves to AskUserQuestionFooter.skip.test.tsx so upstream's vi.mock setup in
the original file cannot shadow it.
Also: bun install for the assistant-ui 0.15.21 bump, regenerated
shared/fixtures from the merged pipeline.
Test status vs the pre-merge baseline (d6cf54d7), identical pre-existing
failures only: cli 7, hub 2 (opencodeClear), web 35, shared 2 (DSH); relay
clean; typecheck clean across all packages.
245 lines
9.1 KiB
TypeScript
245 lines
9.1 KiB
TypeScript
import { defineConfig, type Plugin } from 'vite'
|
||
import react from '@vitejs/plugin-react'
|
||
import { VitePWA } from 'vite-plugin-pwa'
|
||
import { readFileSync, readdirSync } from 'node:fs'
|
||
import { resolve } from 'node:path'
|
||
import { shareTargetPathnameFromBase } from './src/lib/sharePath'
|
||
|
||
function spaFallback(): Plugin {
|
||
return {
|
||
name: 'spa-fallback',
|
||
configureServer(server) {
|
||
server.middlewares.use((req, _res, next) => {
|
||
const url = (req.url ?? '').split('?')[0]
|
||
if (url === '/' || url === '' || url.includes('.') || url.startsWith('/@') || url.startsWith('/api') || url.startsWith('/socket.io') || url.startsWith('/src/')) {
|
||
next()
|
||
return
|
||
}
|
||
req.url = url.startsWith('/studio/') ? '/studio.html' : '/index.html'
|
||
next()
|
||
})
|
||
}
|
||
}
|
||
}
|
||
|
||
const base = process.env.VITE_BASE_URL || '/'
|
||
const shareAction = shareTargetPathnameFromBase(base)
|
||
const hubTarget = process.env.VITE_HUB_PROXY || 'http://127.0.0.1:3006'
|
||
const appVersion = readAppVersion()
|
||
|
||
function readAppVersion(): string {
|
||
const buildInfoPath = resolve(__dirname, '../shared/src/buildInfo.ts')
|
||
const buildInfo = readFileSync(buildInfoPath, 'utf8')
|
||
const match = buildInfo.match(/export const APP_VERSION = ['"]([^'"]+)['"]/)
|
||
|
||
if (!match) {
|
||
throw new Error(`Could not read APP_VERSION from ${buildInfoPath}`)
|
||
}
|
||
|
||
return match[1]
|
||
}
|
||
|
||
function getVendorChunkName(id: string): string | undefined {
|
||
if (!id.includes('/node_modules/')) {
|
||
return undefined
|
||
}
|
||
|
||
if (id.includes('/node_modules/@xterm/')) {
|
||
return 'vendor-terminal'
|
||
}
|
||
|
||
if (
|
||
id.includes('/node_modules/@assistant-ui/')
|
||
|| id.includes('/node_modules/remark-gfm/')
|
||
|| id.includes('/node_modules/hast-util-to-jsx-runtime/')
|
||
) {
|
||
return 'vendor-assistant'
|
||
}
|
||
|
||
if (id.includes('/node_modules/@elevenlabs/react/')) {
|
||
return 'vendor-voice'
|
||
}
|
||
|
||
return undefined
|
||
}
|
||
|
||
function copyKaTeXFonts(): Plugin {
|
||
return {
|
||
name: 'copy-katex-fonts',
|
||
apply: 'build',
|
||
generateBundle() {
|
||
const fontsDir = resolve(__dirname, 'node_modules/katex/dist/fonts')
|
||
for (const fileName of readdirSync(fontsDir)) {
|
||
if (!/\.(?:ttf|woff|woff2)$/i.test(fileName)) continue
|
||
this.emitFile({
|
||
type: 'asset',
|
||
fileName: `assets/fonts/${fileName}`,
|
||
source: readFileSync(resolve(fontsDir, fileName))
|
||
})
|
||
}
|
||
}
|
||
}
|
||
}
|
||
|
||
export default defineConfig({
|
||
appType: 'spa',
|
||
define: {
|
||
__APP_VERSION__: JSON.stringify(appVersion),
|
||
},
|
||
server: {
|
||
host: true,
|
||
allowedHosts: ['hapidev.weishu.me'],
|
||
proxy: {
|
||
'/api': {
|
||
target: hubTarget,
|
||
changeOrigin: true
|
||
},
|
||
'/socket.io': {
|
||
target: hubTarget,
|
||
ws: true
|
||
}
|
||
}
|
||
},
|
||
plugins: [
|
||
react(),
|
||
spaFallback(),
|
||
copyKaTeXFonts(),
|
||
VitePWA({
|
||
// User-controlled reload avoids mid-session surprise reloads (autoUpdate reloads all tabs).
|
||
registerType: 'prompt',
|
||
includeAssets: ['favicon.ico', 'apple-touch-icon-180x180.png', 'mask-icon.svg'],
|
||
strategies: 'injectManifest',
|
||
srcDir: 'src',
|
||
filename: 'sw.ts',
|
||
manifest: {
|
||
name: 'HAPI',
|
||
short_name: 'HAPI',
|
||
description: 'AI-powered development assistant',
|
||
// An installed Android WebAPK stores theme_color once at install time and uses it
|
||
// as a fixed toolbar color, so any value here pins the status bar to one appearance.
|
||
// With none, Chrome falls back to white in light mode and black in dark mode. That
|
||
// follows the Android system uiMode, not the in-app appearance setting, so the bar
|
||
// and the app diverge while an appearance override is active. `undefined` is
|
||
// explicit because the plugin fills in its own #42b883 default otherwise, and the
|
||
// resulting "theme_color is missing" build warning is wrong: theme_color is
|
||
// optional for installability.
|
||
theme_color: undefined,
|
||
// Splash background stays light; only the status bar needed to become adaptive.
|
||
background_color: '#ffffff',
|
||
display: 'standalone',
|
||
orientation: 'portrait',
|
||
scope: base,
|
||
start_url: base,
|
||
icons: [
|
||
{
|
||
src: 'pwa-64x64.png',
|
||
sizes: '64x64',
|
||
type: 'image/png',
|
||
purpose: 'any'
|
||
},
|
||
{
|
||
src: 'pwa-192x192.png',
|
||
sizes: '192x192',
|
||
type: 'image/png',
|
||
purpose: 'any'
|
||
},
|
||
{
|
||
src: 'pwa-512x512.png',
|
||
sizes: '512x512',
|
||
type: 'image/png',
|
||
purpose: 'any'
|
||
},
|
||
{
|
||
src: 'pwa-maskable-192x192.png',
|
||
sizes: '192x192',
|
||
type: 'image/png',
|
||
purpose: 'maskable'
|
||
},
|
||
{
|
||
src: 'pwa-maskable-512x512.png',
|
||
sizes: '512x512',
|
||
type: 'image/png',
|
||
purpose: 'maskable'
|
||
}
|
||
],
|
||
// Web Share Target — Android Chrome routes POSTs to /share
|
||
// when the user picks HAPI in the system share sheet. The
|
||
// service worker (`web/src/sw.ts`) intercepts POST /share,
|
||
// stashes the multipart payload in IndexedDB, and 303-
|
||
// redirects to /share?id=<transferId> for the SPA picker.
|
||
// `*/*` is the broad fallback; explicit MIME prefixes stay
|
||
// first because some Chrome versions only honor declared
|
||
// prefixes when surfacing in the share sheet.
|
||
share_target: {
|
||
action: shareAction,
|
||
method: 'POST',
|
||
enctype: 'multipart/form-data',
|
||
params: {
|
||
title: 'title',
|
||
text: 'text',
|
||
url: 'url',
|
||
files: [
|
||
{
|
||
name: 'files',
|
||
accept: [
|
||
'image/*',
|
||
'application/pdf',
|
||
'text/*',
|
||
'application/json',
|
||
'application/zip',
|
||
'*/*'
|
||
]
|
||
}
|
||
]
|
||
}
|
||
}
|
||
},
|
||
injectManifest: {
|
||
globPatterns: ['**/*.{js,css,html,ico,png,svg,woff,woff2}'],
|
||
// The SPA entry contains the complete multi-agent control surface.
|
||
// Keep it available offline after adding Pi history import instead
|
||
// of silently dropping the entry chunk from the PWA precache.
|
||
maximumFileSizeToCacheInBytes: 3 * 1024 * 1024
|
||
},
|
||
devOptions: {
|
||
enabled: true,
|
||
type: 'module'
|
||
}
|
||
})
|
||
],
|
||
base,
|
||
optimizeDeps: {
|
||
// dev 下 VitePWA 会给每个页面注入 service worker 注册,而 sw.ts 里的
|
||
// workbox 依赖要等 SW 真正注册后才被 Vite 发现,触发一次「optimized
|
||
// dependencies changed → reloading」的整页刷新。这个刷新会打断当时
|
||
// 正在跑的 e2e(表现为元素凭空消失)。提前声明即可在启动时一次预构建。
|
||
include: [
|
||
'workbox-precaching',
|
||
'workbox-routing',
|
||
'workbox-strategies',
|
||
'workbox-expiration'
|
||
]
|
||
},
|
||
resolve: {
|
||
// 单例 React 兜底:workspace 里个别依赖(如 @radix-ui/react-popover)
|
||
// 没被链进 web/node_modules,只能从仓库根解析,于是拿到与应用代码
|
||
// 不同的那份 react 实例。两份 React 同时存在会让带 hook 的第三方
|
||
// 组件在渲染时抛 "Invalid hook call",并连累整棵 React 树卸载。
|
||
dedupe: ['react', 'react-dom'],
|
||
alias: {
|
||
'@': resolve(__dirname, 'src')
|
||
}
|
||
},
|
||
build: {
|
||
outDir: 'dist',
|
||
emptyOutDir: true,
|
||
rollupOptions: {
|
||
output: {
|
||
manualChunks(id) {
|
||
return getVendorChunkName(id)
|
||
}
|
||
}
|
||
}
|
||
}
|
||
})
|