feat: 内置受管浏览器(CDP + AX 快照 ref 体系)+ 本地浏览器配置导入

This commit is contained in:
Rick Ye
2026-09-07 11:20:51 +08:00
parent 5dd645b745
commit eafbe8c15d
16 changed files with 2490 additions and 37 deletions
+60 -32
View File
@@ -1,6 +1,7 @@
import { useEffect, useRef, useState } from 'react'
import type { JSX } from 'react'
import { useAgent } from './hooks/useAgent'
import { useBrowser } from './hooks/useBrowser'
import { SidebarNav } from './components/SidebarNav'
import { TitleBar } from './components/TitleBar'
import { ChatView } from './components/ChatView'
@@ -8,6 +9,7 @@ import { BottomSlot } from './components/BottomSlot'
import { Composer } from './components/Composer'
import { SettingsModal } from './components/SettingsModal'
import { Search } from './components/Search'
import { BrowserPanel } from './components/browser/BrowserPanel'
import { applyThemeColor } from './lib/theme'
export default function App(): JSX.Element {
@@ -31,6 +33,8 @@ export default function App(): JSX.Element {
const prefillRef = useRef<string | null>(null)
const [settingsOpen, setSettingsOpen] = useState(false)
const [searchOpen, setSearchOpen] = useState(false)
const [browserOpen, setBrowserOpen] = useState(false)
const browser = useBrowser()
// 启动时应用持久化的主题色
useEffect(() => {
@@ -96,41 +100,65 @@ export default function App(): JSX.Element {
onOpenWorkspace={openWorkspace}
/>
<main className="relative flex min-w-0 flex-1 flex-col">
<TitleBar title={sessionTitle} />
<ChatView
messages={state.messages}
tools={state.tools}
streaming={state.streaming}
error={state.error}
retry={state.retry}
sessionError={state.sessionError}
pendingApprovals={state.approvals}
onDismissError={clearError}
onPickSuggestion={handlePickSuggestion}
onApplySelection={handleApplySelection}
<TitleBar
title={sessionTitle}
browserOpen={browserOpen}
onToggleBrowser={() => setBrowserOpen((open) => !open)}
/>
<BottomSlot
approvals={state.approvals}
onResolve={resolveApproval}
composer={
<Composer
<div className="flex min-h-0 flex-1">
<div className="relative flex min-w-0 flex-1 flex-col">
<ChatView
messages={state.messages}
tools={state.tools}
streaming={state.streaming}
queue={state.queue}
disabled={!state.ready}
model={state.sessionState?.model}
permissionMode={state.sessionState?.permissionMode ?? 'confirm'}
sessionKey={state.sessionState?.sessionId}
sessionMessageCount={state.sessionState?.messageCount}
onSend={send}
onStop={stop}
onNewSession={newSession}
onSetModel={setModel}
onOpenSettings={() => setSettingsOpen(true)}
onPermissionMode={setPermissionMode}
prefillRef={prefillRef}
error={state.error}
retry={state.retry}
sessionError={state.sessionError}
pendingApprovals={state.approvals}
onDismissError={clearError}
onPickSuggestion={handlePickSuggestion}
onApplySelection={handleApplySelection}
/>
}
/>
<BottomSlot
approvals={state.approvals}
onResolve={resolveApproval}
composer={
<Composer
streaming={state.streaming}
queue={state.queue}
disabled={!state.ready}
model={state.sessionState?.model}
permissionMode={state.sessionState?.permissionMode ?? 'confirm'}
sessionKey={state.sessionState?.sessionId}
sessionMessageCount={state.sessionState?.messageCount}
onSend={send}
onStop={stop}
onNewSession={newSession}
onSetModel={setModel}
onOpenSettings={() => setSettingsOpen(true)}
onPermissionMode={setPermissionMode}
prefillRef={prefillRef}
/>
}
/>
</div>
{browserOpen && (
<BrowserPanel
state={browser.state}
suspended={settingsOpen || searchOpen}
onNavigate={browser.navigate}
onGoBack={browser.goBack}
onGoForward={browser.goForward}
onReload={browser.reload}
onNewTab={browser.newTab}
onSelectTab={browser.selectTab}
onCloseTab={browser.closeTab}
onAcknowledgeRisk={browser.acknowledgeRisk}
onDetectSources={browser.detectSources}
onImportProfile={browser.importProfile}
/>
)}
</div>
</main>
<Search
open={searchOpen}
@@ -1,4 +1,5 @@
import { useEffect, useState, type JSX } from 'react'
import { LuGlobe } from 'react-icons/lu'
/**
* 自绘标题栏(配合主进程 titleBarStyle:'hidden')。
@@ -6,7 +7,15 @@ import { useEffect, useState, type JSX } from 'react'
* 右侧自绘窗口控制按钮(最小化/最大化还原/关闭),高度 40px。
* 不使用系统 WCO overlay —— 其原生背景会盖住标题栏分隔线。
*/
export function TitleBar({ title }: { title?: string }): JSX.Element {
export function TitleBar({
title,
browserOpen,
onToggleBrowser
}: {
title?: string
browserOpen: boolean
onToggleBrowser: () => void
}): JSX.Element {
const name = title?.trim()
const [maximized, setMaximized] = useState(false)
@@ -15,7 +24,20 @@ export function TitleBar({ title }: { title?: string }): JSX.Element {
return (
<div className="relative z-20 flex h-10 shrink-0 select-none items-center border-b border-zinc-200 bg-white pl-4 [-webkit-app-region:drag]">
<div className="min-w-0 truncate text-[12.5px] font-medium text-zinc-800">{name || '新会话'}</div>
<div className="ml-auto flex h-full shrink-0 [-webkit-app-region:no-drag]">
<div className="ml-auto flex h-full shrink-0 items-center gap-1 pr-1.5 [-webkit-app-region:no-drag]">
<button
type="button"
aria-label={browserOpen ? '关闭内置浏览器' : '打开内置浏览器'}
title="内置浏览器"
onClick={onToggleBrowser}
className={`flex h-7 w-[30px] items-center justify-center rounded transition-colors ${
browserOpen ? 'bg-accent-tint text-accent-ink' : 'text-zinc-500 hover:bg-zinc-100 hover:text-zinc-700'
}`}
>
<LuGlobe size={14} />
</button>
</div>
<div className="flex h-full shrink-0 [-webkit-app-region:no-drag]">
<button
type="button"
aria-label="最小化"
@@ -0,0 +1,402 @@
import { useEffect, useMemo, useRef, useState, type JSX } from 'react'
import {
LuArrowLeft,
LuArrowRight,
LuChevronDown,
LuCircleAlert,
LuDownload,
LuGlobe,
LuPlus,
LuRotateCw,
LuX
} from 'react-icons/lu'
import type { BrowserImportResult, BrowserSource, BrowserUiState } from '@shared/types'
import { BrowserSlot } from './BrowserSlot'
/**
* 内置受管浏览器右侧面板。
*
* 页面本体是主进程的原生 WebContentsView(盖在 DOM 之上),面板 DOM 只负责
* 标签栏/地址栏/工具条的「chrome」部分;BrowserSlot 量出页面区域矩形后由主进程
* 对齐原生视图。任何全屏浮层(设置/搜索/导入)出现时必须卸载 Slot 隐藏原生视图。
*/
const PANEL_DEFAULT_WIDTH = 480
const PANEL_MIN_WIDTH = 380
const PANEL_MAX_WIDTH = 760
interface BrowserPanelProps {
state: BrowserUiState | null
/** 全屏浮层(设置/搜索)打开时暂停原生视图,避免其盖住浮层 */
suspended: boolean
onNavigate: (input: string) => void
onGoBack: () => void
onGoForward: () => void
onReload: () => void
onNewTab: () => void
onSelectTab: (tabId: string) => void
onCloseTab: (tabId: string) => void
onAcknowledgeRisk: () => void
onDetectSources: () => Promise<BrowserSource[]>
onImportProfile: (sourceId: string, profileDir: string) => Promise<BrowserImportResult>
}
export function BrowserPanel(props: BrowserPanelProps): JSX.Element {
const { state, suspended } = props
const [width, setWidth] = useState(PANEL_DEFAULT_WIDTH)
const [address, setAddress] = useState('')
const [importOpen, setImportOpen] = useState(false)
const dragState = useRef<{ startX: number; startWidth: number } | null>(null)
const activeTab = useMemo(
() => state?.tabs.find((t) => t.tabId === state.activeTabId) ?? null,
[state]
)
// 地址栏跟随激活页 URL(用户正在输入时不打扰)
const addressRef = useRef<HTMLInputElement>(null)
useEffect(() => {
if (document.activeElement !== addressRef.current) {
setAddress(activeTab?.url ?? '')
}
}, [activeTab?.url])
// 面板宽度拖拽(左边缘把手)
useEffect(() => {
const onMove = (e: PointerEvent): void => {
if (!dragState.current) return
const delta = dragState.current.startX - e.clientX
setWidth(Math.min(PANEL_MAX_WIDTH, Math.max(PANEL_MIN_WIDTH, dragState.current.startWidth + delta)))
}
const onUp = (): void => {
dragState.current = null
}
window.addEventListener('pointermove', onMove)
window.addEventListener('pointerup', onUp)
return () => {
window.removeEventListener('pointermove', onMove)
window.removeEventListener('pointerup', onUp)
}
}, [])
const startResize = (e: React.PointerEvent): void => {
dragState.current = { startX: e.clientX, startWidth: width }
}
const submitAddress = (): void => {
const value = address.trim()
if (value) props.onNavigate(value)
addressRef.current?.blur()
}
const riskBanner = state && !state.riskAcknowledged
const showSlot = Boolean(activeTab) && !suspended && !importOpen
return (
<aside
className="relative flex h-full shrink-0 flex-col border-l border-zinc-200 bg-zinc-50"
style={{ width }}
aria-label="内置浏览器"
>
{/* 左边缘拖拽把手 */}
<div
className="absolute inset-y-0 left-0 z-10 w-1 cursor-col-resize hover:bg-accent/40"
onPointerDown={startResize}
role="separator"
aria-orientation="vertical"
/>
{/* 标签栏 */}
<div className="flex h-9 shrink-0 items-center gap-1 overflow-x-auto border-b border-zinc-200 bg-white px-1.5">
{(state?.tabs ?? []).map((tab) => (
<button
key={tab.tabId}
type="button"
onClick={() => props.onSelectTab(tab.tabId)}
className={`group flex h-7 max-w-44 min-w-20 shrink-0 items-center gap-1 rounded px-2 text-left text-[11px] transition-colors ${
tab.tabId === state?.activeTabId
? 'bg-accent-tint text-accent-ink'
: 'text-zinc-500 hover:bg-zinc-100 hover:text-zinc-700'
}`}
>
{tab.loading && <LuRotateCw size={10} className="shrink-0 animate-spin" />}
<span className="min-w-0 flex-1 truncate">{tab.title || tab.url || '新标签页'}</span>
<span
role="button"
tabIndex={-1}
aria-label="关闭标签页"
onClick={(e) => {
e.stopPropagation()
props.onCloseTab(tab.tabId)
}}
className="hidden shrink-0 rounded p-0.5 hover:bg-zinc-200 group-hover:block"
>
<LuX size={10} />
</span>
</button>
))}
<button
type="button"
aria-label="新建标签页"
onClick={props.onNewTab}
disabled={Boolean(riskBanner)}
className="flex size-7 shrink-0 items-center justify-center rounded text-zinc-400 transition-colors hover:bg-zinc-100 hover:text-zinc-600 disabled:opacity-40"
>
<LuPlus size={13} />
</button>
</div>
{/* 工具条 */}
<div className="flex h-10 shrink-0 items-center gap-1 border-b border-zinc-200 bg-white px-1.5">
<button
type="button"
aria-label="后退"
onClick={props.onGoBack}
className="flex size-7 items-center justify-center rounded text-zinc-500 transition-colors hover:bg-zinc-100 hover:text-zinc-700"
>
<LuArrowLeft size={14} />
</button>
<button
type="button"
aria-label="前进"
onClick={props.onGoForward}
className="flex size-7 items-center justify-center rounded text-zinc-500 transition-colors hover:bg-zinc-100 hover:text-zinc-700"
>
<LuArrowRight size={14} />
</button>
<button
type="button"
aria-label="刷新"
onClick={props.onReload}
className="flex size-7 items-center justify-center rounded text-zinc-500 transition-colors hover:bg-zinc-100 hover:text-zinc-700"
>
<LuRotateCw size={13} />
</button>
<div className="mx-1 flex h-7 min-w-0 flex-1 items-center gap-1.5 rounded border border-zinc-200 bg-zinc-50 px-2 focus-within:border-accent focus-within:bg-white">
<LuGlobe size={11} className="shrink-0 text-zinc-400" />
<input
ref={addressRef}
value={address}
onChange={(e) => setAddress(e.target.value)}
onKeyDown={(e) => {
if (e.key === 'Enter') submitAddress()
if (e.key === 'Escape') addressRef.current?.blur()
}}
placeholder="输入网址或搜索…"
spellCheck={false}
className="min-w-0 flex-1 bg-transparent text-[12px] text-zinc-700 outline-none placeholder:text-zinc-400"
/>
</div>
<button
type="button"
onClick={() => setImportOpen(true)}
title="导入本机浏览器登录态"
className="flex size-7 items-center justify-center rounded text-zinc-500 transition-colors hover:bg-zinc-100 hover:text-zinc-700"
>
<LuDownload size={13} />
</button>
</div>
{/* 风险告知(首次使用门禁,Agent 与用户操作都会被拦截直到确认) */}
{riskBanner && (
<div className="flex shrink-0 items-start gap-2 border-b border-amber-200 bg-amber-50 px-3 py-2.5 text-[11.5px] leading-relaxed text-amber-800">
<LuCircleAlert size={14} className="mt-0.5 shrink-0" />
<div className="min-w-0 flex-1">
<p>
Agent 将以你的身份操作网页。平台可能将自动化行为识别为异常,导致验证码、限流或账号风控;请勿在受管浏览器中登录支付等高敏感账号。
</p>
<button
type="button"
onClick={props.onAcknowledgeRisk}
className="mt-1.5 rounded bg-amber-600 px-2.5 py-1 text-[11px] font-medium text-white transition-colors hover:bg-amber-700"
>
我已了解,开始使用
</button>
</div>
</div>
)}
{/* 页面区域:原生 WebContentsView 对齐到这里的矩形 */}
<div className="relative min-h-0 flex-1 bg-white">
{showSlot && activeTab ? (
<BrowserSlot key={activeTab.tabId} tabId={activeTab.tabId} />
) : (
<div className="flex h-full flex-col items-center justify-center gap-3 px-8 text-center">
<LuGlobe size={28} className="text-zinc-300" />
{riskBanner ? (
<p className="text-[12px] text-zinc-400">确认上方风险告知后即可打开网页</p>
) : (
<>
<button
type="button"
onClick={props.onNewTab}
className="rounded bg-accent px-3.5 py-1.5 text-[12px] font-medium text-white transition-opacity hover:opacity-90"
>
打开浏览器
</button>
<p className="text-[11.5px] leading-relaxed text-zinc-400">
也可以直接让 Agent 帮你操作网页(browser_navigate / browser_observe 等工具)
</p>
</>
)}
</div>
)}
</div>
{/* Agent 操作轨迹 */}
{state?.trace && !importOpen && (
<div className="shrink-0 truncate border-t border-zinc-200 bg-white px-3 py-1.5 text-[10.5px] text-zinc-400">
{state.trace}
</div>
)}
{/* 配置导入浮层(覆盖面板,出现时原生视图已隐藏) */}
{importOpen && (
<ImportOverlay
state={state}
onClose={() => setImportOpen(false)}
onDetectSources={props.onDetectSources}
onImportProfile={props.onImportProfile}
/>
)}
</aside>
)
}
// ---- 配置导入浮层 ----
function ImportOverlay({
state,
onClose,
onDetectSources,
onImportProfile
}: {
state: BrowserUiState | null
onClose: () => void
onDetectSources: () => Promise<BrowserSource[]>
onImportProfile: (sourceId: string, profileDir: string) => Promise<BrowserImportResult>
}): JSX.Element {
const [sources, setSources] = useState<BrowserSource[] | null>(null)
const [selected, setSelected] = useState<{ sourceId: string; profileDir: string } | null>(null)
const [busy, setBusy] = useState(false)
const [result, setResult] = useState<BrowserImportResult | null>(null)
const [error, setError] = useState<string | null>(null)
useEffect(() => {
onDetectSources()
.then((list) => {
setSources(list)
const first = list[0]
if (first?.profiles[0]) setSelected({ sourceId: first.id, profileDir: first.profiles[0].dir })
})
.catch((err) => setError(err instanceof Error ? err.message : String(err)))
}, [onDetectSources])
const runImport = (): void => {
if (!selected) return
setBusy(true)
setError(null)
setResult(null)
onImportProfile(selected.sourceId, selected.profileDir)
.then(setResult)
.catch((err) => setError(err instanceof Error ? err.message : String(err)))
.finally(() => setBusy(false))
}
return (
<div className="absolute inset-0 z-20 flex flex-col bg-white/95 backdrop-blur-[2px]">
<div className="flex h-10 shrink-0 items-center justify-between border-b border-zinc-200 px-3">
<div className="text-[12.5px] font-medium text-zinc-700">导入本机浏览器登录态</div>
<button
type="button"
aria-label="关闭"
onClick={onClose}
className="flex size-6 items-center justify-center rounded text-zinc-400 hover:bg-zinc-100 hover:text-zinc-600"
>
<LuX size={13} />
</button>
</div>
<div className="min-h-0 flex-1 overflow-y-auto p-3 text-[12px] text-zinc-600">
<p className="mb-3 leading-relaxed text-zinc-500">
选择本机浏览器的一个配置(Profile),把其中各网站的登录态(Cookie / Local Storage)导入到内置受管浏览器。
导入前请<span className="font-medium text-zinc-700">完全退出</span>该浏览器,并关闭本面板的所有标签页。
</p>
{state?.imported && (
<div className="mb-3 rounded border border-zinc-200 bg-zinc-50 px-2.5 py-2 text-[11.5px] text-zinc-500">
当前导入来源:{state.imported.browser} · {state.imported.profile}
{state.imported.cookies ? '(含 Cookie)' : ''}
</div>
)}
{sources === null && !error && <p className="py-6 text-center text-zinc-400">正在检测本机浏览器…</p>}
{sources?.length === 0 && <p className="py-6 text-center text-zinc-400">未检测到可导入的本机浏览器(支持 Chrome / Edge / Brave / Chromium)</p>}
<div className="flex flex-col gap-2">
{sources?.map((source) => (
<div key={source.id} className="rounded border border-zinc-200">
<div className="flex items-center gap-1.5 px-2.5 py-2 text-[12px] font-medium text-zinc-700">
<LuChevronDown size={12} className="text-zinc-400" />
{source.name}
<span className="text-[10.5px] font-normal text-zinc-400">({source.profiles.length} 个配置)</span>
</div>
<div className="flex flex-col gap-1 px-2 pb-2">
{source.profiles.map((profile) => {
const active = selected?.sourceId === source.id && selected.profileDir === profile.dir
return (
<button
key={profile.dir}
type="button"
onClick={() => setSelected({ sourceId: source.id, profileDir: profile.dir })}
className={`flex items-center justify-between rounded px-2.5 py-1.5 text-left text-[11.5px] transition-colors ${
active ? 'bg-accent-tint text-accent-ink' : 'text-zinc-600 hover:bg-zinc-100'
}`}
>
<span className="min-w-0 truncate">
{profile.name}
<span className="ml-1.5 text-[10.5px] text-zinc-400">{profile.dir}</span>
</span>
{active && <span className="shrink-0 text-[10px]">已选择</span>}
</button>
)
})}
</div>
</div>
))}
</div>
{error && (
<div className="mt-3 rounded border border-red-200 bg-red-50 px-2.5 py-2 text-[11.5px] text-red-600">{error}</div>
)}
{result && (
<div
className={`mt-3 rounded border px-2.5 py-2 text-[11.5px] leading-relaxed ${
result.ok ? 'border-green-200 bg-green-50 text-green-700' : 'border-red-200 bg-red-50 text-red-600'
}`}
>
{result.message}
</div>
)}
</div>
<div className="flex shrink-0 items-center justify-end gap-2 border-t border-zinc-200 px-3 py-2.5">
<button
type="button"
onClick={onClose}
className="rounded px-3 py-1.5 text-[12px] text-zinc-500 transition-colors hover:bg-zinc-100 hover:text-zinc-700"
>
关闭
</button>
<button
type="button"
disabled={!selected || busy}
onClick={runImport}
className="rounded bg-accent px-3.5 py-1.5 text-[12px] font-medium text-white transition-opacity hover:opacity-90 disabled:opacity-40"
>
{busy ? '导入中…' : '导入登录态'}
</button>
</div>
</div>
)
}
@@ -0,0 +1,78 @@
import * as React from 'react'
/**
* 受管浏览器页面的 DOM 占位符。
*
* 原生 WebContentsView 由主进程挂载在窗口 contentView 上,天然盖在所有 DOM 之上,
* CSS z-index 无法反转。这里量出占位矩形(ResizeObserver + 窗口 resize),经 IPC
* 同步给主进程做 setBounds,让原生视图精确对齐到面板布局。
*
* revision 全局单调递增:快速切换 Tab 时旧 Slot 晚到的 IPC 不会覆盖新挂载的视图。
*/
let revisionCounter = 0
function nextRevision(): number {
revisionCounter += 1
return revisionCounter
}
export function BrowserSlot({ tabId }: { tabId: string }): React.ReactElement {
const ref = React.useRef<HTMLDivElement>(null)
React.useLayoutEffect(() => {
const element = ref.current
if (!element) return
let frame = 0
const commit = (visible: boolean): void => {
const rect = element.getBoundingClientRect()
void window.api.browserSetLayout({
tabId,
visible: visible && rect.width > 4 && rect.height > 4,
revision: nextRevision(),
bounds: {
x: Math.round(rect.x),
y: Math.round(rect.y),
width: Math.round(rect.width),
height: Math.round(rect.height)
}
})
}
const publish = (immediate = false): void => {
if (frame) cancelAnimationFrame(frame)
if (immediate) {
frame = 0
commit(true)
return
}
frame = requestAnimationFrame(() => {
frame = 0
commit(true)
})
}
const observer = new ResizeObserver(() => publish())
observer.observe(element)
window.addEventListener('resize', onWindowResize)
// 挂载立即上报(不等一帧),避免快速切换时原生视图停留在隐藏状态
publish(true)
function onWindowResize(): void {
publish()
}
return () => {
observer.disconnect()
window.removeEventListener('resize', onWindowResize)
if (frame) cancelAnimationFrame(frame)
void window.api.browserSetLayout({
tabId,
visible: false,
revision: nextRevision(),
bounds: { x: 0, y: 0, width: 0, height: 0 }
})
}
}, [tabId])
return <div ref={ref} className="h-full w-full bg-white" aria-label="受管浏览器页面" />
}
@@ -0,0 +1,57 @@
import { useCallback, useEffect, useState } from 'react'
import type { BrowserImportResult, BrowserSource, BrowserUiState } from '@shared/types'
/**
* 内置受管浏览器面板状态与操作。
* 状态由主进程 BrowserController 广播(browser:event),这里只做订阅与动作封装。
*/
export function useBrowser(): {
state: BrowserUiState | null
navigate: (input: string) => void
goBack: () => void
goForward: () => void
reload: () => void
newTab: () => void
selectTab: (tabId: string) => void
closeTab: (tabId: string) => void
closeBrowser: () => void
acknowledgeRisk: () => void
detectSources: () => Promise<BrowserSource[]>
importProfile: (sourceId: string, profileDir: string) => Promise<BrowserImportResult>
} {
const [state, setState] = useState<BrowserUiState | null>(null)
useEffect(() => {
void window.api.browserGetState().then(setState).catch(() => undefined)
return window.api.onBrowserEvent((event) => {
if (event.type === 'state') setState(event.state)
})
}, [])
const dispatchError = useCallback((err: unknown): void => {
console.error('[browser]', err instanceof Error ? err.message : String(err))
}, [])
const wrap = useCallback(
<A,>(fn: (...args: A[]) => Promise<unknown>) =>
(...args: A[]): void => {
void fn(...args).catch(dispatchError)
},
[dispatchError]
)
return {
state,
navigate: wrap((input: string) => window.api.browserNavigate(input)),
goBack: wrap(() => window.api.browserGoBack()),
goForward: wrap(() => window.api.browserGoForward()),
reload: wrap(() => window.api.browserReload()),
newTab: wrap(() => window.api.browserNewTab()),
selectTab: wrap((tabId: string) => window.api.browserSelectTab(tabId)),
closeTab: wrap((tabId: string) => window.api.browserCloseTab(tabId)),
closeBrowser: wrap(() => window.api.browserClose()),
acknowledgeRisk: wrap(() => window.api.browserAcknowledgeRisk()),
detectSources: () => window.api.browserDetectSources(),
importProfile: (sourceId: string, profileDir: string) => window.api.browserImportProfile(sourceId, profileDir)
}
}
+36 -1
View File
@@ -2,6 +2,10 @@ import type {
AgentUiEvent,
AppSettings,
AttachmentInfo,
BrowserImportResult,
BrowserSource,
BrowserUiEvent,
BrowserUiState,
ChatResources,
CustomProviderConfig,
ModelCatalog,
@@ -16,7 +20,19 @@ import type {
WorkspaceMeta
} from '@shared/types'
export type { AgentUiEvent, AppSettings, ModelCatalog, ModelInfo, SessionMeta, SessionState, SessionStatsInfo }
export type {
AgentUiEvent,
AppSettings,
ModelCatalog,
ModelInfo,
SessionMeta,
SessionState,
SessionStatsInfo,
BrowserImportResult,
BrowserSource,
BrowserUiEvent,
BrowserUiState
}
declare global {
interface Window {
@@ -59,6 +75,25 @@ declare global {
setThemeColor: (id: string) => Promise<void>
resolveApproval: (id: string, approved: boolean) => Promise<void>
onEvent: (callback: (event: AgentUiEvent) => void) => () => void
browserGetState: () => Promise<BrowserUiState>
browserNavigate: (input: string) => Promise<BrowserUiState>
browserGoBack: () => Promise<BrowserUiState>
browserGoForward: () => Promise<BrowserUiState>
browserReload: () => Promise<BrowserUiState>
browserNewTab: (url?: string) => Promise<BrowserUiState>
browserSelectTab: (tabId: string) => Promise<BrowserUiState>
browserCloseTab: (tabId?: string) => Promise<BrowserUiState>
browserClose: () => Promise<BrowserUiState>
browserAcknowledgeRisk: () => Promise<void>
browserSetLayout: (layout: {
tabId: string
visible: boolean
revision: number
bounds: { x: number; y: number; width: number; height: number }
}) => Promise<void>
browserDetectSources: () => Promise<BrowserSource[]>
browserImportProfile: (sourceId: string, profileDir: string) => Promise<BrowserImportResult>
onBrowserEvent: (callback: (event: BrowserUiEvent) => void) => () => void
}
}
}