fix: 回应独立 CR 的 P2/P3 意见(0×P1)
- P2-1 onOpen 收到真正落地的 tab:url 新建场景从 landed 态按 id 反查
(补丁后对象),回调不再拿到未打 url 的 tab
- P2-2 插件设置行显式取值源:FeatureSettingsRows 新增 valueSource prop,
插件行读自己的 pluginSettings blob——插件键与宿主 pref 同名时不再
静默读宿主值(原投影合并有读宿主写插件的读写不对称);prop 命名避开
Object.prototype.valueOf 冲突
- P2-3 consumer 类型检查接入工程:package.json 新增 check:consumer-types
脚本 + CI 在 build 后执行;脚本补 lib/types 存在性前置检查(防假绿)
- P3-1 url seed 只在新建时应用:dedupe 聚焦的 tab 不再被覆写 path
- P3-2 closeTab/activateTab 未知 id 严格 no-op:store.reduce 对同引用
结果短路(不 notify、不重写 localStorage),根因修复 + 回归测试
- P3-3 closeTab/activateTab 增加可选 scope(cwd 随回调传递),Sidebar
双树激活/关闭路径传入 { sessionId, cwd }
- P3-4 定向打开不再自动展开不可见 session 的面板
- P3-5 Sidebar 终端 pty 释放查找覆盖双树(底部面板终端不再漏掉 HTTP
兜底)
- P3-6/P3-7 与 Nits:AGENTS.md/指南文档化(dedupeKey 纯函数、meta 用
null 清除、回调 scope 的 cwd 语义);consumer fixture 补常量导出覆盖
测试 +6(url 落地保真 / url 不覆写聚焦 / 未知 id 无 notify / 定向打开
不展开 / scope 随回调 / valueSource 键隔离),464 全绿;typecheck、
build、consumer 校验全部通过。
This commit is contained in:
@@ -11,6 +11,10 @@
|
||||
* `dsh-better-sidebar/client/service` in the built package.
|
||||
*/
|
||||
import type {} from '../src/client/service.ts'
|
||||
import {
|
||||
SIDEBAR_FEATURES,
|
||||
SIDEBAR_SERVICE_VERSION,
|
||||
} from '../src/client/service.ts'
|
||||
import type {
|
||||
BetterSidebarService,
|
||||
FileFetchStrategy,
|
||||
@@ -129,5 +133,8 @@ const toggleType: SidebarSettingToggleType = 'number'
|
||||
const toggle: SidebarSettingToggle = { key: 'k', title: 'K', type: toggleType }
|
||||
const declaration: SidebarSettingsDeclaration = { toggles: [toggle], pluginToggles: [toggle] }
|
||||
const typeName: TabType = 'my-plugin:db'
|
||||
const version: string = SIDEBAR_SERVICE_VERSION
|
||||
const features: readonly string[] = SIDEBAR_FEATURES
|
||||
void version; void features
|
||||
const strategy: FileFetchStrategy = 'mediaUrl'
|
||||
void diff; void prefs; void store; void declaration; void typeName; void strategy
|
||||
|
||||
@@ -897,3 +897,89 @@ describe('lifecycle classification vs dedupe (codex review fixes)', () => {
|
||||
expect(events).toEqual(['open', 'activate'])
|
||||
})
|
||||
})
|
||||
|
||||
describe('independent CR follow-up fixes', () => {
|
||||
it('onOpen for a url-created tab receives the LANDED tab (path = url)', () => {
|
||||
const store = createSidebarStore()
|
||||
const service = createBetterSidebarService(store)
|
||||
let seenPath: string | undefined
|
||||
service.registerTab({
|
||||
id: 'web',
|
||||
title: 'Web',
|
||||
onOpen: (tab) => { seenPath = tab.path },
|
||||
component: () => null,
|
||||
})
|
||||
store.setSession('s1')
|
||||
service.openTab({ type: 'web', title: 'example.com', url: 'https://example.com/x' })
|
||||
expect(seenPath).toBe('https://example.com/x')
|
||||
})
|
||||
|
||||
it('a url seed NEVER overwrites the path of a dedupe-focused tab', () => {
|
||||
const store = createSidebarStore()
|
||||
const service = createBetterSidebarService(store)
|
||||
service.registerTab({
|
||||
id: 'web',
|
||||
title: 'Web',
|
||||
dedupeKey: () => 'web',
|
||||
component: () => null,
|
||||
})
|
||||
store.setSession('s1')
|
||||
service.openTab({ type: 'web', title: 'first', url: 'https://a.example' })
|
||||
service.openTab({ type: 'web', title: 'second', url: 'https://b.example' })
|
||||
const tabs = allLeaves(store.getSnapshot().state!.splits).flatMap(l => l.tabs).filter(t => t.type === 'web')
|
||||
expect(tabs).toHaveLength(1)
|
||||
// The focused tab keeps its ORIGINAL url — the second open must not
|
||||
// repoint it.
|
||||
expect(tabs[0]!.path).toBe('https://a.example')
|
||||
})
|
||||
|
||||
it('closing / activating an unknown tab id is a strict no-op (no notify)', () => {
|
||||
const store = createSidebarStore()
|
||||
const service = createBetterSidebarService(store)
|
||||
service.registerTab({ id: 'x', title: 'X', component: () => null })
|
||||
store.setSession('s1')
|
||||
let calls = 0
|
||||
store.subscribe(() => { calls++ })
|
||||
service.closeTab('does-not-exist')
|
||||
service.activateTab('does-not-exist')
|
||||
expect(calls).toBe(0)
|
||||
})
|
||||
|
||||
it('a targeted open into an INACTIVE session never auto-expands its panels', () => {
|
||||
const store = createSidebarStore()
|
||||
const service = createBetterSidebarService(store)
|
||||
service.registerTab({ id: 'editor', title: 'Editor', component: () => null })
|
||||
store.setSession('s1')
|
||||
// The target session starts collapsed.
|
||||
store.reduceFor('s2', s => ({ ...s, panelOpen: false, bottomOpen: false }))
|
||||
service.openTab({ type: 'editor', title: 'main.ts', path: '/p/main.ts' }, { sessionId: 's2' })
|
||||
// Nothing is in sight for the user — the open must not expand s2.
|
||||
store.setSession('s2')
|
||||
expect(store.getSnapshot().state?.panelOpen).toBe(false)
|
||||
expect(store.getSnapshot().state?.bottomOpen).toBe(false)
|
||||
expect(allLeaves(store.getSnapshot().state!.splits).flatMap(l => l.tabs).some(t => t.type === 'editor')).toBe(true)
|
||||
})
|
||||
|
||||
it('closeTab/activateTab accept an optional scope that rides to the callback', () => {
|
||||
const store = createSidebarStore()
|
||||
const service = createBetterSidebarService(store)
|
||||
const seen: Array<{ kind: string; cwd?: string }> = []
|
||||
service.registerTab({
|
||||
id: 'life',
|
||||
title: 'Life',
|
||||
single: true,
|
||||
onActivate: (_tab, scope) => { seen.push({ kind: 'activate', cwd: scope.cwd }) },
|
||||
onClose: (_tab, scope) => { seen.push({ kind: 'close', cwd: scope.cwd }) },
|
||||
component: () => null,
|
||||
})
|
||||
store.setSession('s1')
|
||||
service.openTab({ type: 'life', title: 'Life' })
|
||||
const tab = allLeaves(store.getSnapshot().state!.splits).flatMap(l => l.tabs).find(t => t.type === 'life')!
|
||||
service.activateTab(tab.id, { sessionId: 's1', cwd: '/work' })
|
||||
service.closeTab(tab.id, { sessionId: 's1', cwd: '/work' })
|
||||
expect(seen).toEqual([
|
||||
{ kind: 'activate', cwd: '/work' },
|
||||
{ kind: 'close', cwd: '/work' },
|
||||
])
|
||||
})
|
||||
})
|
||||
|
||||
@@ -236,3 +236,34 @@ describe('mergePluginSetting (v0.12.0, codex review fix)', () => {
|
||||
expect(map['my-plugin:db']).toEqual({ pageSize: 50, theme: 'dark' })
|
||||
})
|
||||
})
|
||||
|
||||
describe('FeatureSettingsRows valueSource (v0.12.0, independent CR fix)', () => {
|
||||
it('plugin rows read from their OWN value source — a plugin key colliding with a host pref never reads the host value', () => {
|
||||
const prefs = { ...SIDEBAR_PREFS_DEFAULTS, openByDefault: true }
|
||||
const toggle = { key: 'openByDefault', title: 'My flag' }
|
||||
// valueOf returns undefined (the plugin never wrote this key): the row
|
||||
// must render UNCHECKED even though the host pref openByDefault is true.
|
||||
let html = renderToString(createElement(FeatureSettingsRows, {
|
||||
toggles: [toggle],
|
||||
prefs,
|
||||
onToggle: () => {},
|
||||
valueSource: () => undefined,
|
||||
}))
|
||||
expect(html).not.toContain('checked=""')
|
||||
// The plugin wrote `true` into its own blob: the row is checked.
|
||||
html = renderToString(createElement(FeatureSettingsRows, {
|
||||
toggles: [toggle],
|
||||
prefs,
|
||||
onToggle: () => {},
|
||||
valueSource: () => true,
|
||||
}))
|
||||
expect(html).toContain('checked=""')
|
||||
// Without valueOf the row falls back to the prefs face (host semantics).
|
||||
html = renderToString(createElement(FeatureSettingsRows, {
|
||||
toggles: [toggle],
|
||||
prefs,
|
||||
onToggle: () => {},
|
||||
}))
|
||||
expect(html).toContain('checked=""')
|
||||
})
|
||||
})
|
||||
|
||||
Reference in New Issue
Block a user