merge #357: read betterSidebar via ctx.get() in all consumers (LuckVd)

This commit is contained in:
HuanLinOTO
2026-08-24 01:35:00 +08:00
10 changed files with 44 additions and 34 deletions
+4 -4
View File
@@ -83,7 +83,7 @@ function treeWidthOf(tab: SidebarTab): number {
/** Merge a patch into the tab's persisted meta (rides the layout). */
function patchMeta(ctx: Context, tab: SidebarTab, patch: Record<string, unknown>): void {
ctx.betterSidebar?.updateTab(tab.id, { meta: { ...metaOf(tab), ...patch } })
ctx.get('betterSidebar')?.updateTab(tab.id, { meta: { ...metaOf(tab), ...patch } })
}
/** Clamp one dock width into the contract range. */
@@ -137,7 +137,7 @@ export function EditorHost(props: {
*/
const openFile = (absolute: string): void => {
if (inPlace) {
ctx.betterSidebar?.updateTab(tab.id, { path: absolute, title: baseName(absolute) })
ctx.get('betterSidebar')?.updateTab(tab.id, { path: absolute, title: baseName(absolute) })
} else {
openSidebarFile(ctx, store, scope.sessionId, absolute)
}
@@ -303,7 +303,7 @@ export function EditorHost(props: {
content: result.kind === 'text' ? result.content : '',
truncated: result.truncated,
head: result.kind === 'binary' ? result.head : undefined,
}, (head) => ctx.betterSidebar?.matchFileViewer(path, head), mediaUrlOf)
}, (head) => ctx.get('betterSidebar')?.matchFileViewer(path, head), mediaUrlOf)
apply(outcome)
}).catch((error: unknown) => {
if (cancelled) return
@@ -312,7 +312,7 @@ export function EditorHost(props: {
return
}
}
apply(planFirstMatch(ctx.betterSidebar?.matchFileViewer(path), mediaUrlOf))
apply(planFirstMatch(ctx.get('betterSidebar')?.matchFileViewer(path), mediaUrlOf))
return () => { cancelled = true; controller.abort() }
}, [scope.sessionId, scope.cwd, path, ctx, showEmpty, reloadSeq])
+4 -4
View File
@@ -295,7 +295,7 @@ export function SideChatView(props: {
setError(null)
try {
const { childId } = await api.sidechatStart(scope.sessionId)
ctx.betterSidebar?.updateTab(tab.id, { meta: { threadId: childId } })
ctx.get('betterSidebar')?.updateTab(tab.id, { meta: { threadId: childId } })
} catch (cause) {
setError(cause instanceof Error ? cause.message : String(cause))
} finally {
@@ -319,7 +319,7 @@ export function SideChatView(props: {
const title = threadDisplayTitle(display)
if (title !== '' && title !== tab.title) {
try {
ctx.betterSidebar?.updateTab(tab.id, { title })
ctx.get('betterSidebar')?.updateTab(tab.id, { title })
} catch {
// A stale title is cosmetic; the thread keeps working.
}
@@ -422,7 +422,7 @@ export function SideChatView(props: {
* creates the thread on mount). */
const openNewThread = (): void => {
setMenuOpen(false)
ctx.betterSidebar?.openTab({ type: 'sidechat' }, scope)
ctx.get('betterSidebar')?.openTab({ type: 'sidechat' }, scope)
}
/** Switch to an existing thread: parked for createTab, deduped to the
@@ -431,7 +431,7 @@ export function SideChatView(props: {
setMenuOpen(false)
if (id === threadId) return
parkSidechatReopen(id)
ctx.betterSidebar?.openTab({ type: 'sidechat' }, scope)
ctx.get('betterSidebar')?.openTab({ type: 'sidechat' }, scope)
}
const menuItems = useMemo<MenuEntry[]>(() => {
+17 -17
View File
@@ -137,7 +137,7 @@ interface TabContentProps extends TabContentMemoKey {
const TabContent = memo(function TabContent(props: TabContentProps) {
const { tab, sessionId, cwd, expanded, onToggleDir, onReferenceFile, ctx, store, visible, onSubagentJump, onOpenDiff } = props
const scope = { sessionId, cwd }
const descriptor = ctx.betterSidebar?.getTab(tab.type)
const descriptor = ctx.get('betterSidebar')?.getTab(tab.type)
if (descriptor === undefined) {
return <OrphanedTab ctx={ctx} store={store} scope={scope} tab={tab} visible={visible} />
}
@@ -165,7 +165,7 @@ const TabContent = memo(function TabContent(props: TabContentProps) {
* Tabs the user disabled in the side card settings are filtered out
* entirely — re-enabling them is the settings page's job. */
function buildNewTabOptions(state: SidebarState, ctx: Context, scope: SessionScope): NewTabOption[] {
const service = ctx.betterSidebar
const service = ctx.get('betterSidebar')
if (service === undefined) return []
return service.getTabs()
.filter(d => !d.hidden && service.isTabEnabled(d.id))
@@ -231,7 +231,7 @@ export function Sidebar(props: { ctx: Context; store: SidebarStore }) {
// from going stale, mirroring the localeRevision mechanism above.
const [tabsVersion, setTabsVersion] = useState(0)
useEffect(() => {
const service = ctx.betterSidebar
const service = ctx.get('betterSidebar')
if (service === undefined) return
return service.subscribe(() => setTabsVersion(version => version + 1))
}, [ctx])
@@ -437,7 +437,7 @@ export function Sidebar(props: { ctx: Context; store: SidebarStore }) {
try {
const list = JSON.parse(event.data) as Array<{ uuid: string; title: string; command: string; exited: boolean }>
if (!Array.isArray(list)) return
store.reduce(s => ctx.betterSidebar?.isTabEnabled('terminal') === false
store.reduce(s => ctx.get('betterSidebar')?.isTabEnabled('terminal') === false
? s
: reconcileAgentTerminals(s, list))
} catch {
@@ -493,13 +493,13 @@ export function Sidebar(props: { ctx: Context; store: SidebarStore }) {
autoOpenPendingRef.current = null
if (!detectNewDirectSubagent(baseline, ctx.sessions.list.getSnapshot(), sessionId)) return
if (!store.getPrefs().autoOpenSubagent) return
if (ctx.betterSidebar?.isTabEnabled('subagent') === false) return
if (ctx.get('betterSidebar')?.isTabEnabled('subagent') === false) return
store.reduce(s => s.panelOpen ? s : togglePanel(s))
// Pin the landing to the right panel: the auto-opened Subagent page must
// appear where the panel just expanded, not in a bottom-panel pane the
// user last touched.
store.reduce(s => ({ ...s, activePane: firstLeaf(s.splits).id }))
ctx.betterSidebar?.openTab({ type: 'subagent', title: t('subagent') })
ctx.get('betterSidebar')?.openTab({ type: 'subagent', title: t('subagent') })
}, AUTO_OPEN_DEBOUNCE_MS)
autoOpenPendingRef.current = { baseline, timer }
}, [sessionList, sessionId, store, ctx])
@@ -527,10 +527,10 @@ export function Sidebar(props: { ctx: Context; store: SidebarStore }) {
if (sessionId === undefined || prev === undefined) return
if (!detectNewJob(prev, sessionList, sessionId)) return
if (!store.getPrefs().autoOpenJobs) return
if (ctx.betterSidebar?.isTabEnabled('subagent') === false) return
if (ctx.get('betterSidebar')?.isTabEnabled('subagent') === false) return
store.reduce(s => s.panelOpen ? s : togglePanel(s))
store.reduce(s => ({ ...s, activePane: firstLeaf(s.splits).id }))
ctx.betterSidebar?.openTab({ type: 'subagent', title: t('subagent') })
ctx.get('betterSidebar')?.openTab({ type: 'subagent', title: t('subagent') })
}, [sessionList, sessionId, store, ctx])
/**
@@ -551,7 +551,7 @@ export function Sidebar(props: { ctx: Context; store: SidebarStore }) {
subagentJumpRef.current = undefined
store.reduce(s => s.panelOpen ? s : togglePanel(s))
store.reduce(s => ({ ...s, activePane: firstLeaf(s.splits).id }))
ctx.betterSidebar?.openTab({ type: 'subagent', title: t('subagent') })
ctx.get('betterSidebar')?.openTab({ type: 'subagent', title: t('subagent') })
}, [sessionId, store, ctx])
// The app shell's center column: the bottom panel spans ONLY that column
@@ -802,11 +802,11 @@ export function Sidebar(props: { ctx: Context; store: SidebarStore }) {
if (wasOpen === undefined || wasOpen || !state.bottomOpen) return
if (state.bottomOpenedOnce) return
if (store.getPrefs().bottomPanelAutoTerminal === false) return
if (ctx.betterSidebar?.isTabEnabled('terminal') === false) return
if (ctx.get('betterSidebar')?.isTabEnabled('terminal') === false) return
// Land the tab in the bottom panel's first pane; the once-flag is set
// atomically so later expansions never repeat the auto-open.
store.reduce(s => ({ ...s, activePane: firstLeaf(s.bottomSplits).id, bottomOpenedOnce: true }))
ctx.betterSidebar?.openTab({ type: 'terminal' })
ctx.get('betterSidebar')?.openTab({ type: 'terminal' })
}, [state, store, ctx, narrow])
// Panel drags: the right panel's width (left edge strip), the bottom
@@ -1087,7 +1087,7 @@ export function Sidebar(props: { ctx: Context; store: SidebarStore }) {
// Route through the service: the tab-bar close is the canonical close
// path (finds the pane itself, fires descriptor.onClose); the session
// scope (with its cwd) rides to the callback.
ctx.betterSidebar?.closeTab(tabId, sessionId === undefined ? undefined : { sessionId, cwd })
ctx.get('betterSidebar')?.closeTab(tabId, sessionId === undefined ? undefined : { sessionId, cwd })
if (tab?.type === 'terminal') {
if (isAgentTabId(tabId)) {
const uuid = agentUuidOf(tabId)
@@ -1101,7 +1101,7 @@ export function Sidebar(props: { ctx: Context; store: SidebarStore }) {
// Route through the service: same reducer (finds the pane in EITHER
// tree, sets the active pane) and fires descriptor.onActivate; the
// session scope (with its cwd) rides to the callback.
ctx.betterSidebar?.activateTab(tabId, sessionId === undefined ? undefined : { sessionId, cwd })
ctx.get('betterSidebar')?.activateTab(tabId, sessionId === undefined ? undefined : { sessionId, cwd })
},
focusPane: (paneId) => { store.reduce(s => ({ ...s, activePane: paneId })) },
moveTabToEdge: (payload: TabDragPayload, toPane: string, zone: DropZone) => {
@@ -1183,9 +1183,9 @@ export function Sidebar(props: { ctx: Context; store: SidebarStore }) {
}).height
const onNewTab = (optionId: string): void => {
const service = ctx.betterSidebar
const service = ctx.get('betterSidebar')
const descriptor = service?.getTab(optionId)
if (descriptor === undefined) return
if (service === undefined || descriptor === undefined) return
const title = typeof descriptor.title === 'function' ? descriptor.title() : descriptor.title
// The session scope rides along: lifecycle callbacks receive it (and
// the open stays in the current session, as before).
@@ -1200,7 +1200,7 @@ export function Sidebar(props: { ctx: Context; store: SidebarStore }) {
*/
/** The tab icon from the tab-type registry (shared by every workbench). */
const tabIconOf = (tab: SidebarTab): ReactNode => {
const descriptor = ctx.betterSidebar?.getTab(tab.type)
const descriptor = ctx.get('betterSidebar')?.getTab(tab.type)
if (descriptor === undefined) return null
return typeof descriptor.icon === 'function' ? descriptor.icon(14) : descriptor.icon
}
@@ -1211,7 +1211,7 @@ export function Sidebar(props: { ctx: Context; store: SidebarStore }) {
* strip must never break because a plugin's badge computation failed.
*/
const tabBadgeOf = (tab: SidebarTab): ReactNode => {
const descriptor = ctx.betterSidebar?.getTab(tab.type)
const descriptor = ctx.get('betterSidebar')?.getTab(tab.type)
if (descriptor?.badge === undefined) return null
let value: string | number | null | undefined
try {
+1 -1
View File
@@ -363,7 +363,7 @@ export function apply(ctx: Context): void {
let title: string | undefined
try { title = new URL(url).hostname } catch { /* keep the default title */ }
const type = urlTargetOf(new URL(url)) ?? 'browser'
ctx.betterSidebar?.openTab({ type, url, title })
ctx.get('betterSidebar')?.openTab({ type, url, title })
},
selfOrigin: window.location.origin,
})
+1 -1
View File
@@ -22,7 +22,7 @@ export function openSidebarFile(ctx: Context, store: SidebarStore, sessionId: st
const title = at === -1 ? absolute : absolute.slice(at + 1)
// Route through the sidebar service so the editor descriptor's dedupeKey
// (per-path) applies; the id is path-derived so multiple editors coexist.
ctx.betterSidebar?.openTab({ type: 'editor', title, path: absolute, id: `editor:${absolute}` })
ctx.get('betterSidebar')?.openTab({ type: 'editor', title, path: absolute, id: `editor:${absolute}` })
}
/** The intercepted produced-files row (visual twin of the deliverables chips). */
+1
View File
@@ -65,6 +65,7 @@ function mountSidebar(): MountedSidebar {
locale: { subscribe: () => () => {}, getSnapshot: () => localeSnapshot },
sessions: { list: { subscribe: () => () => {}, getSnapshot: () => sessionsSnapshot } },
betterSidebar: service,
get: (name: string) => name === 'betterSidebar' ? service : undefined,
}
const root: Root = createRoot(container)
act(() => { root.render(createElement(Sidebar, { ctx: ctx as never, store })) })
+1
View File
@@ -40,6 +40,7 @@ function setup(): {
const sessionsSnapshot = { byId: { 'editor-home-session': { cwd: '/tmp' } }, current: 'editor-home-session' }
const ctx = {
betterSidebar: service,
get: (name: string) => name === 'betterSidebar' ? service : undefined,
sessions: { list: { subscribe: () => () => {}, getSnapshot: () => sessionsSnapshot } },
} as unknown as Context
return { store, ctx, homeTab }
+3
View File
@@ -108,6 +108,9 @@ describe('open-path interception wiring', () => {
},
workspaces: funnel,
betterSidebar: { openTab: (seed: unknown) => { opened.push(seed as Record<string, unknown>) } },
get: (name: string) => name === 'betterSidebar'
? { openTab: (seed: unknown) => { opened.push(seed as Record<string, unknown>) } }
: undefined,
} as unknown as Context
const store = createSidebarStore()
const original = ctx.workspaces.openPath
+1
View File
@@ -68,6 +68,7 @@ function mountSidebar(): MountedSidebar {
locale: { subscribe: () => () => {}, getSnapshot: () => localeSnapshot },
sessions: { list: { subscribe: () => () => {}, getSnapshot: () => sessionsSnapshot } },
betterSidebar: service,
get: (name: string) => name === 'betterSidebar' ? service : undefined,
}
const root: Root = createRoot(container)
act(() => { root.render(createElement(Sidebar, { ctx: ctx as never, store })) })
+11 -7
View File
@@ -75,13 +75,17 @@ const producedOwner = (paths: string[]): unknown => ({
const emptyOwner = (): unknown => ({ nodes: [{ kind: 'assistant', seq: 1, turn: 1 }], seq: 1 })
/** The minimal client-context fake the registration (and its seats) touch. */
const clientCtx = (slots: unknown): Context => ({
slots,
sessions: {
list: { getSnapshot: () => ({ current: 's1', byId: { s1: { id: 's1', cwd: '/w', displayTitle: 's1' } } }) },
},
betterSidebar: { openTab: vi.fn() },
} as unknown as Context)
const clientCtx = (slots: unknown): Context => {
const betterSidebar = { openTab: vi.fn() }
return {
slots,
sessions: {
list: { getSnapshot: () => ({ current: 's1', byId: { s1: { id: 's1', cwd: '/w', displayTitle: 's1' } } }) },
},
betterSidebar,
get: (name: string) => name === 'betterSidebar' ? betterSidebar : undefined,
} as unknown as Context
}
describe('turn-tail interception registration (issue #15)', () => {
it('registers through slots.inject and lands once the slot is already declared', () => {