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:
@@ -61,6 +61,8 @@ export interface SidebarState {
|
||||
activePane: string | null
|
||||
/** Monotonic terminal tab counter (ids survive reloads). */
|
||||
nextTerminal: number
|
||||
/** Monotonic browser tab counter (ids survive reloads; mirrors nextTerminal). */
|
||||
nextBrowser: number
|
||||
/** Explorer expansion set (absolute directory paths). */
|
||||
expanded: string[]
|
||||
splits: SplitNode
|
||||
@@ -129,6 +131,7 @@ export function makeDefaultState(width = PANEL_DEFAULT, panelOpen = true, seedEx
|
||||
width,
|
||||
activePane: leaf.id,
|
||||
nextTerminal: 1,
|
||||
nextBrowser: 1,
|
||||
expanded: [],
|
||||
splits: leaf,
|
||||
}
|
||||
@@ -308,6 +311,36 @@ export function activateTab(state: SidebarState, paneId: string, tabId: string):
|
||||
}
|
||||
}
|
||||
|
||||
/** Update the display fields of one open tab (title / path) without
|
||||
* re-opening it. The browser tab persists its current URL and hostname
|
||||
* title through this reducer so a reload restores the visited page. A
|
||||
* missing tab id is a no-op. */
|
||||
export function patchTab(
|
||||
state: SidebarState,
|
||||
tabId: string,
|
||||
patch: { title?: string; path?: string },
|
||||
): SidebarState {
|
||||
let changed = false
|
||||
const walk = (node: SplitNode): SplitNode => {
|
||||
if (node.kind === 'leaf') {
|
||||
const tabs = node.tabs.map(tab => {
|
||||
if (tab.id !== tabId) return tab
|
||||
changed = true
|
||||
return {
|
||||
...tab,
|
||||
...(patch.title !== undefined ? { title: patch.title } : {}),
|
||||
...(patch.path !== undefined ? { path: patch.path } : {}),
|
||||
}
|
||||
})
|
||||
return tabs === node.tabs ? node : { ...node, tabs }
|
||||
}
|
||||
const children = node.children.map(walk)
|
||||
return children === node.children ? node : { ...node, children }
|
||||
}
|
||||
const splits = walk(state.splits)
|
||||
return changed ? { ...state, splits } : state
|
||||
}
|
||||
|
||||
/**
|
||||
* Land a tab in the active pane (or focus its existing instance by id).
|
||||
* Dedup strategies (single-instance, per-path, per-change) are owned by the
|
||||
@@ -572,6 +605,12 @@ export function sanitizeState(parsed: unknown): SidebarState | undefined {
|
||||
if (typeof record.nextTerminal !== 'number' || !Number.isInteger(record.nextTerminal) || record.nextTerminal < 1) {
|
||||
return undefined
|
||||
}
|
||||
// nextBrowser arrived in a later build; a missing or malformed value on an
|
||||
// OLDER persisted state defaults to 1 so existing layouts keep loading
|
||||
// (unlike nextTerminal, which is strict — it predates the v1 shape).
|
||||
const nextBrowser = typeof record.nextBrowser === 'number' && Number.isInteger(record.nextBrowser) && record.nextBrowser >= 1
|
||||
? record.nextBrowser
|
||||
: 1
|
||||
if (typeof record.activePane !== 'string' && record.activePane !== null) return undefined
|
||||
if (!Array.isArray(record.expanded) || record.expanded.some(item => typeof item !== 'string')) return undefined
|
||||
const reid = new Map<string, string>()
|
||||
@@ -585,6 +624,7 @@ export function sanitizeState(parsed: unknown): SidebarState | undefined {
|
||||
// new tabs still land in the pane the user was using.
|
||||
activePane: typeof record.activePane === 'string' ? (reid.get(record.activePane) ?? record.activePane) : null,
|
||||
nextTerminal: record.nextTerminal,
|
||||
nextBrowser,
|
||||
expanded: record.expanded as string[],
|
||||
splits,
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user