fix: 处理 Copilot 二轮 review 的 10 条评论
- BrowserView: 即使 host 在白名单,GUI 自身 origin 永不给 allow-same-origin(裸 host 条目覆盖所有端口时可提权) - intercept: revealInExplorer 相对路径经 resolveSidebarPath 解析后再 revealPaths(交付物路径是相对的) - intercept: ctx.betterSidebar 改为 ctx.get(#357 纤维链读取约定) - FileTree/EditorHost/TreePanel/tabs: 把 revealed 贯通到树行渲染 + 高亮类 + scrollIntoView(此前高亮永不渲染) - state.revealPaths: 重建祖先目录时保留前导分隔符(POSIX/UNC 此前目录永远折叠) - state.schedulePersist: 仅活动会话写入全局宽度(targeted open 不再用非活动会话的过期宽度覆盖拖拽值) - browser.probe: GET 回退显式取消 body(否则流式/大响应挂住 socket) - terminal-font: leading generic 前只放 symbols-only 字体;fully-patched 放 generic 后(避免 hijack 度量字体) - sidebar.module.css: 恢复拖拽反馈(#130 的 silent resize 与 PR 描述冲突,取 HEAD) - Sidebar: 剩余 4 处 ctx.betterSidebar 改 ctx.get - tests: 补 revealPaths/iframeSandboxFor/terminal-font 回归测试 + EditorHost/FileTree 测试补 revealed prop
This commit is contained in:
@@ -62,6 +62,7 @@ function mountHost(ctx: Context, store: ReturnType<typeof createSidebarStore>, t
|
||||
scope: { sessionId: 'editor-home-session' },
|
||||
tab: tab(),
|
||||
expanded: [],
|
||||
revealed: [],
|
||||
onToggleDir: () => {},
|
||||
onReferenceFile: () => {},
|
||||
}))
|
||||
|
||||
@@ -96,7 +96,7 @@ function mount(ctx: Context, tab: () => SidebarTab): { container: HTMLDivElement
|
||||
act(() => {
|
||||
root.render(createElement(EditorHost, {
|
||||
ctx, store: ctx.betterSidebar as never, scope: { sessionId: 'editor-home-session' },
|
||||
tab: tab(), expanded: [], onToggleDir: () => {}, onReferenceFile: () => {},
|
||||
tab: tab(), expanded: [], revealed: [], onToggleDir: () => {}, onReferenceFile: () => {},
|
||||
}))
|
||||
})
|
||||
return {
|
||||
|
||||
@@ -56,6 +56,7 @@ async function mountTree(busy = false): Promise<Harness> {
|
||||
sessionId: 's1',
|
||||
cwd: '/tmp',
|
||||
expanded: [],
|
||||
revealed: [],
|
||||
onToggle: () => {},
|
||||
onOpenFile: () => {},
|
||||
onReferenceFile: () => {},
|
||||
|
||||
@@ -65,6 +65,7 @@ async function mountTree(overrides: {
|
||||
sessionId: 's1',
|
||||
cwd: '/tmp',
|
||||
expanded: [],
|
||||
revealed: [],
|
||||
onToggle: () => {},
|
||||
onOpenFile: () => {},
|
||||
onOpenFileNewTab: () => {},
|
||||
|
||||
@@ -78,6 +78,7 @@ function mount(ctx: Context, store: ReturnType<typeof createSidebarStore>, tab:
|
||||
scope: { sessionId: 'markdown-manual-refresh-session', cwd: '/tmp' },
|
||||
tab,
|
||||
expanded: [],
|
||||
revealed: [],
|
||||
onToggleDir: () => {},
|
||||
onReferenceFile: () => {},
|
||||
}))
|
||||
|
||||
@@ -13,7 +13,7 @@ import { createElement } from 'react'
|
||||
import './browser-globals.ts'
|
||||
import type { Context } from '../src/context-types.ts'
|
||||
import { TextEditor, HTML_IFRAME_SANDBOX } from '../src/client/TextEditor.tsx'
|
||||
import { BrowserView, BrowserEmbedBlocked, BROWSER_IFRAME_SANDBOX } from '../src/client/BrowserView.tsx'
|
||||
import { BrowserView, BrowserEmbedBlocked, BROWSER_IFRAME_SANDBOX, iframeSandboxFor } from '../src/client/BrowserView.tsx'
|
||||
import { createSidebarStore } from '../src/client/state.ts'
|
||||
import type { FileViewerProps } from '../src/client/service.ts'
|
||||
|
||||
@@ -140,6 +140,23 @@ describe('browser tab iframe sandbox', () => {
|
||||
expect(iframe).toContain('allow=""')
|
||||
})
|
||||
|
||||
it('never grants allow-same-origin to the GUI\'s own origin, even when allowlisted', () => {
|
||||
// A bare-host allowlist entry covers every port, so an allowlisted GUI
|
||||
// host also matches the GUI's exact origin — but a page at the GUI's
|
||||
// origin must keep the opaque-origin sandbox: allow-same-origin there
|
||||
// would make it same-origin with its parent and hand it GUI storage/API.
|
||||
const allowlist = 'localhost'
|
||||
const guiOrigin = 'http://localhost:5173'
|
||||
expect(iframeSandboxFor('http://localhost:5173/', allowlist, guiOrigin)).toBe(BROWSER_IFRAME_SANDBOX)
|
||||
expect(iframeSandboxFor('http://localhost:5173/', allowlist, guiOrigin)).not.toContain('allow-same-origin')
|
||||
// Other allowlisted loopback origins still get the same-origin token.
|
||||
const sandbox = iframeSandboxFor('http://localhost:4000/', allowlist, guiOrigin)
|
||||
expect(sandbox).not.toBe(BROWSER_IFRAME_SANDBOX)
|
||||
expect(sandbox).toContain('allow-same-origin')
|
||||
// A different (non-GUI) origin with no allowlist stays opaque.
|
||||
expect(iframeSandboxFor('https://example.com/', '', guiOrigin)).toBe(BROWSER_IFRAME_SANDBOX)
|
||||
})
|
||||
|
||||
it('renders the live sandbox status row with the temporary unlock action', () => {
|
||||
const store = createSidebarStore()
|
||||
const html = renderToString(createElement(BrowserView, tabProps(store, 'https://example.com/')))
|
||||
|
||||
+28
-1
@@ -4,7 +4,7 @@ import {
|
||||
dockFloat, FLOAT_MIN_H, FLOAT_MIN_W, floatTab, floatWithTab, insertLeafAt, makeDefaultState,
|
||||
migrateBottomTabs, moveFloat, moveTab, moveTabToEdge, openDiffTab,
|
||||
openTabInActivePane, patchTab, raiseFloat, reconcileAgentTerminals, resizeFloat, resizeSplit,
|
||||
resizeSplitIn, sanitizeState, setBottomHeight,
|
||||
resizeSplitIn, revealPaths, sanitizeState, setBottomHeight,
|
||||
splitPane, tabOpenIn, toggleBottomPanel, toggleExpanded, togglePanel,
|
||||
type SidebarLeaf, type SidebarState, type SidebarTab, type SplitNode,
|
||||
} from '../src/client/state.ts'
|
||||
@@ -1084,6 +1084,33 @@ describe('free windows (v0.16.0)', () => {
|
||||
expect(allLeaves(after.splits).flatMap(leaf => leaf.tabs).some(tab => tab.id === 'agent:u2')).toBe(true)
|
||||
})
|
||||
|
||||
describe('revealPaths (show in folder)', () => {
|
||||
it('expands ancestors with their ABSOLUTE path (leading separator preserved)', () => {
|
||||
// POSIX: the root (/w/src) is not itself expanded, but the subdirs
|
||||
// below it must be recorded as ABSOLUTE paths so FileTree's
|
||||
// `expanded.includes(entry.path)` matches.
|
||||
const base = makeDefaultState()
|
||||
const next = revealPaths(base, '/w/src', ['/w/src/sub/deep/a.ts'])
|
||||
expect(next.revealed).toEqual(['/w/src/sub/deep/a.ts'])
|
||||
expect(next.expanded).toContain('/w/src/sub')
|
||||
expect(next.expanded).toContain('/w/src/sub/deep')
|
||||
expect(next.expanded).not.toContain('w/src/sub')
|
||||
expect(next.expanded).not.toContain('/w/src')
|
||||
})
|
||||
|
||||
it('keeps a Windows drive-letter root and a UNC prefix', () => {
|
||||
const drive = revealPaths(makeDefaultState(), 'C:\\work', ['C:\\work\\src\\a.ts'])
|
||||
expect(drive.expanded).toContain('C:\\work\\src')
|
||||
const unc = revealPaths(makeDefaultState(), '\\\\server\\share', ['\\\\server\\share\\sub\\a.ts'])
|
||||
expect(unc.expanded).toContain('\\\\server\\share\\sub')
|
||||
})
|
||||
|
||||
it('resolves nothing to the same reference (no churn)', () => {
|
||||
const base = makeDefaultState()
|
||||
expect(revealPaths(base, '/w', [])).toBe(base)
|
||||
})
|
||||
})
|
||||
|
||||
describe('sanitizeState (floats)', () => {
|
||||
const g = globalThis as Record<string, unknown>
|
||||
beforeEach(() => {
|
||||
|
||||
@@ -54,6 +54,17 @@ describe('withIconFontFallbacks', () => {
|
||||
.toBeLessThan(indexOfFamily(stack, 'ui-monospace'))
|
||||
})
|
||||
|
||||
it('keeps fully-patched fonts BEHIND a leading generic (they carry Latin and would become the base)', () => {
|
||||
// A leading generic resolves, so prepending a fully-patched Nerd Font
|
||||
// (which ships Latin glyphs) would make it xterm's measuring/base font
|
||||
// and override the user/theme family precedence. Only the symbols-only
|
||||
// patches (no Latin) may precede it.
|
||||
const stack = withIconFontFallbacks('monospace')
|
||||
expect(indexOfFamily(stack, '"Symbols Nerd Font Mono"')).toBeLessThan(indexOfFamily(stack, 'monospace'))
|
||||
expect(indexOfFamily(stack, '"Hack Nerd Font Mono"')).toBeGreaterThan(indexOfFamily(stack, 'monospace'))
|
||||
expect(indexOfFamily(stack, '"JetBrainsMono Nerd Font Mono"')).toBeGreaterThan(indexOfFamily(stack, 'monospace'))
|
||||
})
|
||||
|
||||
it('splices ahead of the FIRST generic when several are present', () => {
|
||||
const stack = withIconFontFallbacks('Menlo, monospace, sans-serif')
|
||||
const nerd = indexOfFamily(stack, '"Symbols Nerd Font Mono"')
|
||||
|
||||
Reference in New Issue
Block a user