feat: 内置 HTML 预览器 + 沙箱浏览器 tab(v0.5.0)
- html viewer:.html/.htm 沙箱 iframe 预览(相对资源经 /sidebar/html 路径编码路由加载,CSP sandbox + 信任围栏 + cwd 边界),预览/编辑切换 - browser tab:+ 菜单多开(browser:<n>),地址栏后退/前进/刷新/在浏览器中打开(15px 图标),URL 与标题按会话持久化(patchTab + nextBrowser) - 安全:iframe 一律不透明源沙箱(无 allow-same-origin/top-navigation、no-referrer、权限策略全禁、禁 srcdoc);地址栏仅 http(s)、拒绝 localhost 等本机地址;界面实时沙箱状态(关闭红色警示)+ 临时解锁/恢复 - 设置:htmlViewerNoSandbox / htmlViewerDefaultUnsafe / browserNoSandbox / browserInterceptLinks 四个声明式二级设置(默认安全) - 外链拦截:聊天/界面 http(s) 外链默认在侧边栏打开(openTab seed.url),Ctrl/Cmd 点击放行 - 嵌入拒绝面板:browser.probe 路由探测 X-Frame-Options/frame-ancestors,被拒站点显示原因 + 在浏览器中打开/仍然加载 - 设置页可滚动;内置 7 tab + 9 viewer;新增设计文档;268 个测试全绿
This commit is contained in:
+32
-15
@@ -22,7 +22,7 @@
|
||||
import type { ReactNode } from 'react'
|
||||
import type { Context } from '../context-types.ts'
|
||||
import {
|
||||
activateTab, allLeaves, closeTab as closeTabReducer, openTabInActivePane,
|
||||
activateTab, allLeaves, closeTab as closeTabReducer, openTabInActivePane, patchTab,
|
||||
type SidebarState, type SidebarStore, type SidebarTab,
|
||||
} from './state.ts'
|
||||
import type { SessionScope } from './api.ts'
|
||||
@@ -194,9 +194,12 @@ export interface BetterSidebarService {
|
||||
* Open a tab (used by external tabs and the + menu). `title` overrides
|
||||
* the descriptor's title when given (the editor tab shows the file name);
|
||||
* when the descriptor provides `createTab` it mints the tab itself and
|
||||
* `title`/`path`/`id` are ignored. A disabled tab type is a no-op.
|
||||
* `title`/`path`/`id` are ignored. `url` lands the tab with its `path`
|
||||
* pre-set to the URL (the browser tab's navigation seed; the caller
|
||||
* usually pairs it with a hostname `title`). A disabled tab type is a
|
||||
* no-op.
|
||||
*/
|
||||
openTab(seed: { type: string; title?: string; path?: string; diff?: SidebarTab['diff']; id?: string }): void
|
||||
openTab(seed: { type: string; title?: string; path?: string; diff?: SidebarTab['diff']; id?: string; url?: string }): void
|
||||
/** Close a tab by id. */
|
||||
closeTab(tabId: string): void
|
||||
/** Subscribe to registry changes (register/dispose). */
|
||||
@@ -297,7 +300,7 @@ export function createBetterSidebarService(store: SidebarStore): BetterSidebarSe
|
||||
return undefined
|
||||
}
|
||||
|
||||
const openTab = (seed: { type: string; title?: string; path?: string; diff?: SidebarTab['diff']; id?: string }): void => {
|
||||
const openTab = (seed: { type: string; title?: string; path?: string; diff?: SidebarTab['diff']; id?: string; url?: string }): void => {
|
||||
// A type the user disabled in settings never opens — neither from the
|
||||
// + menu nor from derived flows (file opens, subagent auto-open,
|
||||
// external plugins). Already-open tabs keep rendering.
|
||||
@@ -309,22 +312,36 @@ export function createBetterSidebarService(store: SidebarStore): BetterSidebarSe
|
||||
const descriptor = tabs.get(seed.type)
|
||||
if (descriptor === undefined) return state
|
||||
// Let the descriptor mint the tab (terminal's nextTerminal bump, etc.).
|
||||
let tab: SidebarTab
|
||||
let next: SidebarState
|
||||
if (descriptor.createTab !== undefined) {
|
||||
const result = descriptor.createTab(state)
|
||||
if (result === null) return state
|
||||
const next = applyDedupe(state, result.tab, descriptor)
|
||||
return result.patch !== undefined ? { ...next, ...result.patch } : next
|
||||
tab = result.tab
|
||||
next = applyDedupe(state, result.tab, descriptor)
|
||||
if (result.patch !== undefined) next = { ...next, ...result.patch }
|
||||
} else {
|
||||
tab = {
|
||||
id: seed.id ?? seed.type,
|
||||
type: seed.type,
|
||||
// A caller-provided title wins (the editor shows the file name);
|
||||
// otherwise the descriptor's (possibly i18n) title is the default.
|
||||
title: seed.title ?? (typeof descriptor.title === 'function' ? descriptor.title() : descriptor.title),
|
||||
...(seed.path !== undefined ? { path: seed.path } : {}),
|
||||
...(seed.diff !== undefined ? { diff: seed.diff } : {}),
|
||||
}
|
||||
next = applyDedupe(state, tab, descriptor)
|
||||
}
|
||||
const tab: SidebarTab = {
|
||||
id: seed.id ?? seed.type,
|
||||
type: seed.type,
|
||||
// A caller-provided title wins (the editor shows the file name);
|
||||
// otherwise the descriptor's (possibly i18n) title is the default.
|
||||
title: seed.title ?? (typeof descriptor.title === 'function' ? descriptor.title() : descriptor.title),
|
||||
...(seed.path !== undefined ? { path: seed.path } : {}),
|
||||
...(seed.diff !== undefined ? { diff: seed.diff } : {}),
|
||||
// A URL seed pre-fills the tab's path (the browser tab navigates to it
|
||||
// on mount). An explicit seed.title also wins over a createTab-minted
|
||||
// default title (e.g. the sidebar-browser's hostname title).
|
||||
if (seed.url !== undefined) {
|
||||
return patchTab(next, tab.id, {
|
||||
path: seed.url,
|
||||
...(seed.title !== undefined ? { title: seed.title } : {}),
|
||||
})
|
||||
}
|
||||
return applyDedupe(state, tab, descriptor)
|
||||
return next
|
||||
})
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user