fix: 回应 Codex 审查的 4 条意见(2×P1 + 2×P2)
- P1 state.ts reduceFor:恢复共享 uid 计数器到调用前水平(目标 session 的 max 可能低于激活 session 已用 id,否则下次 mint 撞 id 损坏布局); 回归测试实证:无修复时 split 产生重复 pane id - P1 service.ts 生命周期分类:dedupe 聚焦判定改为基于输入态的存在性 (dedupeKey 匹配或 id 匹配都算聚焦)——按 path 去重 + 新 id 打开时 不再对从未插入的幽灵 tab 触发 onOpen(资源泄漏源) - P2 SideCardSection:乐观 prefs ref(optimisticRef)驱动嵌套 map 合并 (tabsEnabled/viewersEnabled/pluginSettings),同 tick 多次写入不再 互相丢弃;mergePluginSetting 抽纯函数 + 单测 - P2 state.ts schedulePersist:每 session 独立防抖定时器(persistTimers Map),定向打开不再取消其他 session 的待写;单测验证双写都落盘
This commit is contained in:
@@ -128,6 +128,24 @@ function featureNameOf(feature: TabDescriptor | FileViewerDescriptor): string {
|
||||
return textOf('title' in feature ? feature.title : undefined) || feature.id
|
||||
}
|
||||
|
||||
/**
|
||||
* Merge one plugin-owned setting into a pluginSettings map (pure, v0.12.0+).
|
||||
* Sequential merges are additive: each call spreads the map it was GIVEN,
|
||||
* so building from the latest optimistic map keeps earlier keys intact
|
||||
* (two same-tick writes must not drop each other).
|
||||
*/
|
||||
export function mergePluginSetting(
|
||||
pluginSettings: Record<string, Record<string, unknown>>,
|
||||
descriptorId: string,
|
||||
key: string,
|
||||
value: unknown,
|
||||
): Record<string, Record<string, unknown>> {
|
||||
return {
|
||||
...pluginSettings,
|
||||
[descriptorId]: { ...(pluginSettings[descriptorId] ?? {}), [key]: value },
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Render a custom settings panel (`settings.render`) with error containment:
|
||||
* a throwing panel shows an inline error line instead of breaking the whole
|
||||
@@ -355,6 +373,14 @@ export function SideCardSection({ store, service }: SideCardSectionProps) {
|
||||
const [error, setError] = useState<string | null>(null)
|
||||
// Which feature's secondary settings popup is open (null = closed).
|
||||
const [settingsFor, setSettingsFor] = useState<TabDescriptor | FileViewerDescriptor | null>(null)
|
||||
// The LATEST optimistic prefs, kept in sync with the state. Nested-map
|
||||
// merges (tabsEnabled / viewersEnabled / pluginSettings) MUST build from
|
||||
// this ref, not from the render-time `prefs`: two same-tick writes (e.g.
|
||||
// a settings panel updating several plugin keys at once) would otherwise
|
||||
// both spread the stale map and the later patch would drop the earlier
|
||||
// key even though the commits are serialized.
|
||||
const optimisticRef = useRef(prefs)
|
||||
useEffect(() => { optimisticRef.current = prefs }, [prefs])
|
||||
|
||||
// The declarative inventory: the registered tab types and file viewers.
|
||||
// Local state + service.subscribe (registry changes are rare — plugin
|
||||
@@ -425,8 +451,10 @@ export function SideCardSection({ store, service }: SideCardSectionProps) {
|
||||
|
||||
/** Optimistically apply one pref patch, then commit (revert on failure). */
|
||||
const applyPref = (patch: Record<string, unknown>): void => {
|
||||
const previous = prefs
|
||||
setPrefs({ ...previous, ...patch } as SidebarPrefs)
|
||||
const previous = optimisticRef.current
|
||||
const next = { ...previous, ...patch } as SidebarPrefs
|
||||
optimisticRef.current = next
|
||||
setPrefs(next)
|
||||
setError(null)
|
||||
void commit(patch).then(outcome => applyOutcome(previous, outcome))
|
||||
}
|
||||
@@ -437,12 +465,12 @@ export function SideCardSection({ store, service }: SideCardSectionProps) {
|
||||
|
||||
/** Flip one per-tab enable switch (merge into the tabsEnabled map). */
|
||||
const onToggleTab = (id: string, next: boolean): void => {
|
||||
applyPref({ tabsEnabled: { ...prefs.tabsEnabled, [id]: next } })
|
||||
applyPref({ tabsEnabled: { ...optimisticRef.current.tabsEnabled, [id]: next } })
|
||||
}
|
||||
|
||||
/** Flip one per-viewer enable switch (merge into the viewersEnabled map). */
|
||||
const onToggleViewer = (id: string, next: boolean): void => {
|
||||
applyPref({ viewersEnabled: { ...prefs.viewersEnabled, [id]: next } })
|
||||
applyPref({ viewersEnabled: { ...optimisticRef.current.viewersEnabled, [id]: next } })
|
||||
}
|
||||
|
||||
/** Flip one declaratively-declared toggle (a SidebarPrefs boolean field). */
|
||||
@@ -474,12 +502,7 @@ export function SideCardSection({ store, service }: SideCardSectionProps) {
|
||||
|
||||
/** Persist one plugin-owned setting of one descriptor (merged into the pluginSettings blob). */
|
||||
const applyPluginSetting = (descriptorId: string, key: string, value: unknown): void => {
|
||||
applyPref({
|
||||
pluginSettings: {
|
||||
...prefs.pluginSettings,
|
||||
[descriptorId]: { ...(prefs.pluginSettings[descriptorId] ?? {}), [key]: value },
|
||||
},
|
||||
})
|
||||
applyPref({ pluginSettings: mergePluginSetting(optimisticRef.current.pluginSettings, descriptorId, key, value) })
|
||||
}
|
||||
|
||||
/** Flip one plugin-owned switch row (same row shape, plugin-scoped key). */
|
||||
|
||||
Reference in New Issue
Block a user