Merge pull request #73698 from NousResearch/bb/desktop-overlay-perf

perf(desktop): kill sidebar + overlay render churn from hot store subscriptions
This commit is contained in:
brooklyn! 2026-07-28 21:01:26 -05:00 committed by GitHub
commit 40a53ca031
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
12 changed files with 815 additions and 201 deletions

View file

@ -0,0 +1,61 @@
#!/usr/bin/env node
// Robust A/B: measure each surface N times, report median wasted renders.
// Reduces timing noise from stream ticks landing on different clicks.
import WebSocket from 'ws'
const RUNS = 3
let msgId = 1
function send(ws, method, params = {}) {
const id = msgId++
return new Promise((resolve, reject) => {
const h = (data) => { const m = JSON.parse(data); if (m.id === id) { ws.off('message', h); m.error ? reject(m.error) : resolve(m.result) } }
ws.on('message', h); ws.send(JSON.stringify({ id, method, params }))
})
}
async function ev(ws, e) { return (await send(ws, 'Runtime.evaluate', { expression: e, returnByValue: true, awaitPromise: true }))?.result?.value }
const sleep = ms => new Promise(r => setTimeout(r, ms))
const median = arr => { const s = [...arr].sort((a,b)=>a-b); return s[Math.floor(s.length/2)] }
async function wsUrl() {
const d = await (await fetch('http://127.0.0.1:9222/json/list')).json()
return d.find(t => t.type === 'page' && (t.url||'').includes('5174')).webSocketDebuggerUrl
}
async function measureOnce(ws, setup, holdMs) {
await setup(ws)
await sleep(300)
await ev(ws, `__RENDER_COUNTS__.start(); true`)
await sleep(holdMs)
const rep = JSON.parse(await ev(ws, `JSON.stringify(__RENDER_COUNTS__.report())`))
return rep.reduce((s, c) => s + c.wasted, 0)
}
async function main() {
const ws = new WebSocket(await wsUrl())
await new Promise((res, rej) => { ws.on('open', res); ws.on('error', rej) })
await send(ws, 'Runtime.enable')
const surfaces = [
['Artifacts', ws => ev(ws, `window.location.hash='#/artifacts'; true`), 2000],
['Messaging', ws => ev(ws, `window.location.hash='#/messaging'; true`), 2000],
['Cron', ws => ev(ws, `window.location.hash='#/cron'; true`), 2000],
['Profiles', ws => ev(ws, `window.location.hash='#/profiles'; true`), 2000],
['Agents', ws => ev(ws, `window.location.hash='#/agents'; true`), 2000],
['Starmap', ws => ev(ws, `window.location.hash='#/starmap'; true`), 2000],
['Webhooks', ws => ev(ws, `window.location.hash='#/webhooks'; true`), 2000],
['CommandCenter/System', async ws => { await ev(ws, `window.location.hash='#/command-center'; true`); await sleep(400); await ev(ws, `Array.from(document.querySelectorAll('button')).find(b=>b.textContent?.trim()==='System')?.click(); true`) }, 2000],
]
const label = process.argv[2] || 'RUN'
console.log(`\n=== ${label} (median of ${RUNS}, wasted renders, 2s idle) ===`)
for (const [name, setup, hold] of surfaces) {
const runs = []
for (let i = 0; i < RUNS; i++) {
runs.push(await measureOnce(ws, setup, hold))
await ev(ws, `window.location.hash='#/'; true`); await sleep(300)
}
console.log(` ${name.padEnd(24)} median ${String(median(runs)).padStart(6)} (runs: ${runs.join(', ')})`)
}
ws.close()
}
main().catch(e => { console.error(e); process.exit(1) })

View file

@ -0,0 +1,161 @@
#!/usr/bin/env node
// CDP probe: measure render churn on overlay surfaces (cmdk, settings, command-center, skills)
// against the user's live instance on :9222. Read-only — never closes or navigates away.
import WebSocket from 'ws'
const CDP_URL = 'ws://127.0.0.1:9222'
const TARGET_GLOB = '/devtools/page/'
let msgId = 1
function send(ws, method, params = {}) {
const id = msgId++
return new Promise((resolve, reject) => {
const handler = (data) => {
const msg = JSON.parse(data.toString())
if (msg.id === id) {
ws.off('message', handler)
if (msg.error) reject(new Error(JSON.stringify(msg.error)))
else resolve(msg.result)
}
}
ws.on('message', handler)
ws.send(JSON.stringify({ id, method, params }))
})
}
async function getRendererTarget() {
const resp = await fetch('http://127.0.0.1:9222/json/list')
const targets = await resp.json()
// Find the renderer target (not devtools)
return targets.find(t => t.url?.startsWith('http://127.0.0.1:5174') || t.url?.includes('5174'))
?? targets.find(t => t.type === 'page' && !t.url.startsWith('devtools'))
}
async function evalInPage(ws, expr) {
const result = await send(ws, 'Runtime.evaluate', {
expression: expr,
returnByValue: true,
awaitPromise: true,
})
return result?.result?.value
}
async function main() {
const target = await getRendererTarget()
if (!target) {
console.error('No renderer target found')
process.exit(1)
}
console.log(`Target: ${target.url}`)
const ws = new WebSocket(target.webSocketDebuggerUrl)
await new Promise((resolve, reject) => {
ws.on('open', resolve)
ws.on('error', reject)
})
await send(ws, 'Runtime.enable')
// Check if __RENDER_COUNTS__ is available
const hasCounter = await evalInPage(ws, 'typeof __RENDER_COUNTS__')
console.log(`__RENDER_COUNTS__ available: ${hasCounter}`)
if (hasCounter !== 'object') {
console.log('Render counter not loaded — checking perf-live...')
const hasPerfLive = await evalInPage(ws, 'typeof __PERF_LIVE__')
console.log(`__PERF_LIVE__ available: ${hasPerfLive}`)
}
// 1. Baseline: open cmdk, type a few chars, measure
console.log('\n=== CmdK Palette ===')
await evalInPage(ws, `
window.__renderBaseline = {}
if (typeof __RENDER_COUNTS__ === 'object' && __RENDER_COUNTS__) {
__RENDER_COUNTS__.reset()
}
// Open cmdk via keyboard shortcut
const evt = new KeyboardEvent('keydown', { key: 'k', metaKey: true, bubbles: true })
document.dispatchEvent(evt)
true
`)
await new Promise(r => setTimeout(r, 500))
// Type some characters
for (const ch of ['s', 'e', 't', 't', 'i', 'n', 'g']) {
await evalInPage(ws, `
const el = document.querySelector('[cmdk-input]') || document.querySelector('input[placeholder]')
if (el) {
el.focus()
el.value = el.value + '${ch}'
el.dispatchEvent(new Event('input', { bubbles: true }))
}
true
`)
await new Promise(r => setTimeout(r, 80))
}
await new Promise(r => setTimeout(r, 300))
const cmdkCounts = await evalInPage(ws, `
__RENDER_COUNTS__?.snapshot?.() ?? __RENDER_COUNTS__?.counts ?? 'no counter'
`)
console.log('CmdK render counts after typing "setting":', JSON.stringify(cmdkCounts, null, 2))
// Close cmdk
await evalInPage(ws, `new KeyboardEvent('keydown', { key: 'Escape', bubbles: true }); document.dispatchEvent(new KeyboardEvent('keydown', { key: 'Escape', bubbles: true })); true`)
await new Promise(r => setTimeout(r, 300))
// 2. Command Center — measure renders while on System tab
console.log('\n=== Command Center (System tab) ===')
await evalInPage(ws, `
if (typeof __RENDER_COUNTS__ === 'object' && __RENDER_COUNTS__) {
__RENDER_COUNTS__.reset()
}
// Navigate to command center system section
const link = Array.from(document.querySelectorAll('a, button')).find(el => el.textContent?.includes('System') || el.textContent?.includes('Command Center'))
if (link) link.click()
true
`)
await new Promise(r => setTimeout(r, 1000))
const ccCounts = await evalInPage(ws, `
__RENDER_COUNTS__?.snapshot?.() ?? __RENDER_COUNTS__?.counts ?? 'no counter'
`)
console.log('Command Center render counts (after 1s on System tab):', JSON.stringify(ccCounts, null, 2))
// 3. Settings page — measure renders on nav
console.log('\n=== Settings Page ===')
await evalInPage(ws, `
if (typeof __RENDER_COUNTS__ === 'object' && __RENDER_COUNTS__) {
__RENDER_COUNTS__.reset()
}
// Navigate to settings
const link = Array.from(document.querySelectorAll('a, button')).find(el => el.textContent?.includes('Settings'))
if (link) link.click()
true
`)
await new Promise(r => setTimeout(r, 1000))
// Click through a few nav items
for (const label of ['Appearance', 'Gateway', 'Keys', 'About']) {
await evalInPage(ws, `
const el = Array.from(document.querySelectorAll('button')).find(b => b.textContent?.trim() === '${label}')
if (el) el.click()
true
`)
await new Promise(r => setTimeout(r, 150))
}
const settingsCounts = await evalInPage(ws, `
__RENDER_COUNTS__?.snapshot?.() ?? __RENDER_COUNTS__?.counts ?? 'no counter'
`)
console.log('Settings render counts (after clicking 4 nav items):', JSON.stringify(settingsCounts, null, 2))
ws.close()
console.log('\nDone.')
}
main().catch(err => {
console.error('Error:', err)
process.exit(1)
})

View file

@ -0,0 +1,201 @@
#!/usr/bin/env node
// Comprehensive overlay render-churn measurement on the live instance.
// Measures: cmdk root + submenus, settings, command center, capabilities, system overlays.
// Read-only — never closes the app, only navigates and clicks.
import WebSocket from 'ws'
const WS_URL = 'ws://127.0.0.1:9222/devtools/page/6E095DBE024BD280C674D00023C01201'
let msgId = 1
function send(ws, method, params = {}) {
const id = msgId++
return new Promise((resolve, reject) => {
const handler = (data) => {
const msg = JSON.parse(data.toString())
if (msg.id === id) {
ws.off('message', handler)
msg.error ? reject(new Error(JSON.stringify(msg.error))) : resolve(msg.result)
}
}
ws.on('message', handler)
ws.send(JSON.stringify({ id, method, params }))
})
}
async function eval_(ws, expr) {
const r = await send(ws, 'Runtime.evaluate', { expression: expr, returnByValue: true, awaitPromise: true })
return r?.result?.value
}
async function sleep(ms) { return new Promise(r => setTimeout(r, ms)) }
async function measure(ws, label, fn, settleMs = 400) {
await eval_(ws, `__RENDER_COUNTS__.start(); true`)
await fn(ws)
await sleep(settleMs)
const report = await eval_(ws, `JSON.stringify(__RENDER_COUNTS__.report())`)
const parsed = JSON.parse(report)
const totalRenders = parsed.reduce((s, c) => s + c.renders, 0)
const totalWasted = parsed.reduce((s, c) => s + c.wasted, 0)
const totalMs = parsed.reduce((s, c) => s + c.totalMs, 0)
const top5 = parsed.filter(c => c.wasted > 0).sort((a,b) => b.wasted - a.wasted).slice(0, 5)
console.log(`\n${label}`)
console.log(` total: ${totalRenders} renders, ${totalWasted} wasted, ${totalMs.toFixed(1)}ms`)
if (top5.length > 0) {
for (const c of top5) {
console.log(` ${c.name}: ${c.renders} renders, ${c.wasted} wasted, ${c.totalMs.toFixed(1)}ms`)
}
} else {
console.log(` (no wasted renders)`)
}
return { label, totalRenders, totalWasted, totalMs, top: top5 }
}
async function clickByText(ws, text, tag = 'button') {
return eval_(ws, `
const el = Array.from(document.querySelectorAll('${tag}')).find(b => b.textContent?.trim() === '${text}')
if (el) { el.click(); true } else false
`)
}
async function clickByHref(ws, partial) {
return eval_(ws, `
const el = Array.from(document.querySelectorAll('a,button')).find(e => e.getAttribute('href')?.includes('${partial}'))
if (el) { el.click(); true } else false
`)
}
async function typeInInput(ws, text) {
for (const ch of text) {
await eval_(ws, `
const el = document.querySelector('[cmdk-input]') || document.querySelector('input[type="text"]') || document.querySelector('input')
if (el) { el.focus(); el.value = el.value + '${ch}'; el.dispatchEvent(new Event('input', {bubbles:true})) }
true
`)
await sleep(50)
}
}
async function pressKey(ws, key, mods = {}) {
await eval_(ws, `
document.dispatchEvent(new KeyboardEvent('keydown', { key: '${key}', ${Object.entries(mods).map(([k,v]) => `${k}:${v}`).join(', ')}, bubbles: true }))
true
`)
}
async function main() {
const ws = new WebSocket(WS_URL)
await new Promise((resolve, reject) => {
ws.on('open', resolve)
ws.on('error', reject)
})
await send(ws, 'Runtime.enable')
const avail = await eval_(ws, `typeof __RENDER_COUNTS__`)
console.log(`__RENDER_COUNTS__: ${avail}`)
if (avail !== 'object') {
console.error('Render counter not loaded. Reload the page.')
process.exit(1)
}
const results = []
// === 1. CmdK root: open, type "setting", close ===
results.push(await measure(ws, '1. CmdK root (type "setting")', async (ws) => {
await pressKey(ws, 'k', { metaKey: true })
await sleep(300)
await typeInInput(ws, 'setting')
}))
await pressKey(ws, 'Escape')
await sleep(300)
// === 2. CmdK submenu: open, navigate to theme picker, click a theme, back ===
results.push(await measure(ws, '2. CmdK theme submenu', async (ws) => {
await pressKey(ws, 'k', { metaKey: true })
await sleep(300)
await typeInInput(ws, 'theme')
await sleep(200)
// Click first theme item
await eval_(ws, `
const item = document.querySelector('[cmdk-item]')
if (item) item.click()
true
`)
await sleep(300)
}))
await pressKey(ws, 'Escape')
await sleep(300)
// === 3. CmdK color-mode submenu ===
results.push(await measure(ws, '3. CmdK color-mode submenu', async (ws) => {
await pressKey(ws, 'k', { metaKey: true })
await sleep(300)
await typeInInput(ws, 'color mode')
await sleep(200)
await eval_(ws, `const item = document.querySelector('[cmdk-item]'); if (item) item.click(); true`)
await sleep(300)
}))
await pressKey(ws, 'Escape')
await sleep(300)
// === 4. Settings: open, click through nav items ===
results.push(await measure(ws, '4. Settings (5 nav clicks)', async (ws) => {
await clickByHref(ws, 'settings')
await sleep(500)
for (const label of ['Appearance', 'Gateway', 'Keys', 'Notifications', 'About']) {
await clickByText(ws, label)
await sleep(100)
}
}))
// === 5. Command Center: open to System tab, idle 2s ===
results.push(await measure(ws, '5. Command Center (System tab, 2s idle)', async (ws) => {
await clickByHref(ws, 'command-center')
await sleep(500)
await clickByText(ws, 'System')
await sleep(2000)
}, 100))
// === 6. Command Center: Sessions tab ===
results.push(await measure(ws, '6. Command Center (Sessions tab, 1s)', async (ws) => {
await clickByText(ws, 'Sessions')
await sleep(1000)
}, 100))
// === 7. Capabilities/Skills page ===
results.push(await measure(ws, '7. Capabilities (Skills tab, 1s)', async (ws) => {
await clickByHref(ws, 'skills')
await sleep(800)
// Click Skills tab if not already
await clickByText(ws, 'Skills')
await sleep(1000)
}, 100))
// === 8. Capabilities Toolsets tab ===
results.push(await measure(ws, '8. Capabilities (Toolsets tab, 1s)', async (ws) => {
await clickByText(ws, 'Tools')
await sleep(1000)
}, 100))
// === 9. Capabilities MCP tab ===
results.push(await measure(ws, '9. Capabilities (MCP tab, 1s)', async (ws) => {
await clickByText(ws, 'MCP')
await sleep(1000)
}, 100))
// Navigate back to chat
await eval_(ws, `window.location.hash = '#/'; true`)
await sleep(300)
// Summary table
console.log('\n\n=== SUMMARY ===')
console.log('Surface'.padEnd(45) + 'Renders'.padStart(10) + 'Wasted'.padStart(10) + 'ms'.padStart(10))
console.log('-'.repeat(75))
for (const r of results) {
console.log(r.label.padEnd(45) + String(r.totalRenders).padStart(10) + String(r.totalWasted).padStart(10) + r.totalMs.toFixed(1).padStart(10))
}
ws.close()
}
main().catch(err => { console.error(err); process.exit(1) })

View file

@ -0,0 +1,110 @@
#!/usr/bin/env node
// Full surface sweep: every overlay, every settings sub-page, every system overlay.
// Read-only — navigates and clicks, never closes the app.
import WebSocket from 'ws'
const WS_URL = process.env.CDP_WS || 'ws://127.0.0.1:9222/devtools/page/6E095DBE024BD280C674D00023C01201'
let msgId = 1
function send(ws, method, params = {}) {
const id = msgId++
return new Promise((resolve, reject) => {
const handler = (data) => {
const msg = JSON.parse(data.toString())
if (msg.id === id) {
ws.off('message', handler)
msg.error ? reject(new Error(JSON.stringify(msg.error))) : resolve(msg.result)
}
}
ws.on('message', handler)
ws.send(JSON.stringify({ id, method, params }))
})
}
async function eval_(ws, expr) {
const r = await send(ws, 'Runtime.evaluate', { expression: expr, returnByValue: true, awaitPromise: true })
return r?.result?.value
}
async function sleep(ms) { return new Promise(r => setTimeout(r, ms)) }
async function measure(ws, label, fn, settleMs = 300) {
await eval_(ws, `__RENDER_COUNTS__.start(); true`)
await fn(ws)
await sleep(settleMs)
const report = JSON.parse(await eval_(ws, `JSON.stringify(__RENDER_COUNTS__.report())`))
const totalRenders = report.reduce((s, c) => s + c.renders, 0)
const totalWasted = report.reduce((s, c) => s + c.wasted, 0)
const totalMs = report.reduce((s, c) => s + c.totalMs, 0)
const top = report.filter(c => c.wasted > 0).sort((a,b) => b.wasted - a.wasted).slice(0, 6)
console.log(`\n${label}`)
console.log(` ${totalRenders} renders · ${totalWasted} wasted · ${totalMs.toFixed(1)}ms`)
for (const c of top) console.log(` ${c.name}: ${c.renders}r / ${c.wasted}w / ${c.totalMs.toFixed(1)}ms`)
if (top.length === 0) console.log(` (clean)`)
return { label, totalRenders, totalWasted, totalMs }
}
async function nav(ws, hash) {
await eval_(ws, `window.location.hash = '#${hash}'; true`)
}
async function clickText(ws, text, tag='button') {
return eval_(ws, `
const el = Array.from(document.querySelectorAll('${tag}')).find(b => b.textContent?.trim() === ${JSON.stringify(text)})
if (el) { el.click(); true } else false
`)
}
async function main() {
const ws = new WebSocket(WS_URL)
await new Promise((res, rej) => { ws.on('open', res); ws.on('error', rej) })
await send(ws, 'Runtime.enable')
if (await eval_(ws, `typeof __RENDER_COUNTS__`) !== 'object') { console.error('counter not loaded'); process.exit(1) }
const results = []
// System overlays — open each, idle 2s (streaming underneath is the churn test)
for (const [label, route] of [
['Artifacts', '/artifacts'],
['Messaging', '/messaging'],
['Cron', '/cron'],
['Profiles', '/profiles'],
['Agents', '/agents'],
['Starmap', '/starmap'],
['Webhooks', '/webhooks'],
]) {
results.push(await measure(ws, `OVERLAY: ${label} (open + 2s idle)`, async (ws) => {
await nav(ws, route)
await sleep(2200)
}, 100))
await nav(ws, '/')
await sleep(300)
}
// Settings sub-pages — open settings, click each nav item, idle 1.5s on it
await nav(ws, '/settings')
await sleep(600)
for (const label of ['Model','Session','Appearance','Notifications','Providers','Gateway','Keybinds','API Keys','Plugins','Archived Chats','About']) {
results.push(await measure(ws, `SETTINGS: ${label} (1.5s idle)`, async (ws) => {
await clickText(ws, label)
await sleep(1500)
}, 100))
}
await nav(ws, '/')
await sleep(300)
// Messaging sub-tabs (platform detail) — click through if present
await nav(ws, '/messaging')
await sleep(800)
results.push(await measure(ws, 'MESSAGING: idle 2s w/ platform list', async (ws) => {
await sleep(2000)
}, 100))
await nav(ws, '/')
await sleep(300)
console.log('\n\n=== SUMMARY (wasted renders) ===')
console.log('Surface'.padEnd(48) + 'Renders'.padStart(9) + 'Wasted'.padStart(9) + 'ms'.padStart(9))
console.log('-'.repeat(75))
for (const r of results) {
console.log(r.label.padEnd(48) + String(r.totalRenders).padStart(9) + String(r.totalWasted).padStart(9) + r.totalMs.toFixed(1).padStart(9))
}
ws.close()
}
main().catch(e => { console.error(e); process.exit(1) })

View file

@ -1,5 +1,5 @@
import type * as React from 'react'
import { useCallback, useEffect, useMemo, useState } from 'react'
import { memo, useCallback, useEffect, useMemo, useState } from 'react'
import { useNavigate } from 'react-router-dom'
import { ZoomableImage } from '@/components/chat/zoomable-image'
@ -96,7 +96,7 @@ type CellCtx = {
}
interface ArtifactColumn {
Cell: (props: { artifact: ArtifactRecord; ctx: CellCtx }) => React.ReactElement
Cell: React.ComponentType<{ artifact: ArtifactRecord; ctx: CellCtx }>
bodyClassName: string
header: (filter: ArtifactFilter, a: Translations['artifacts']) => string
id: 'location' | 'primary' | 'session'
@ -278,10 +278,12 @@ export function ArtifactsView({ setStatusbarItemGroup: _setStatusbarItemGroup, .
})
}, [])
const cellCtx: CellCtx = {
onOpen: openArtifact,
onOpenChat: sessionId => openSession(sessionId, navigate)
}
// Stable ctx: recreating it (or its onOpenChat closure) every render made
// every artifact cell re-render whenever the page did — and a link cell's
// async title fetch re-rendered the page repeatedly. openArtifact is already
// a useCallback; navigate is stable, so onOpenChat can be too.
const openChat = useCallback((sessionId: string) => openSession(sessionId, navigate), [navigate])
const cellCtx: CellCtx = useMemo(() => ({ onOpen: openArtifact, onOpenChat: openChat }), [openArtifact, openChat])
return (
<PageSearchShell
@ -549,7 +551,7 @@ function ArtifactCellAction({
)
}
function PrimaryCell({ artifact, ctx }: { artifact: ArtifactRecord; ctx: CellCtx }) {
const PrimaryCell = memo(function PrimaryCell({ artifact, ctx }: { artifact: ArtifactRecord; ctx: CellCtx }) {
const isLink = artifact.kind === 'link'
const brand = isLink ? resolveBrandIcon(shortHostLabel(artifact.href)) : null
const Icon = brand ?? (isLink ? Link2 : FileText)
@ -571,9 +573,9 @@ function PrimaryCell({ artifact, ctx }: { artifact: ArtifactRecord; ctx: CellCtx
</span>
</ArtifactCellAction>
)
}
})
function LocationCell({ artifact }: { artifact: ArtifactRecord; ctx: CellCtx }) {
const LocationCell = memo(function LocationCell({ artifact }: { artifact: ArtifactRecord; ctx: CellCtx }) {
const { t } = useI18n()
const isLink = artifact.kind === 'link'
const value = isLink ? hostPathLabel(artifact.value) : artifact.value
@ -602,9 +604,9 @@ function LocationCell({ artifact }: { artifact: ArtifactRecord; ctx: CellCtx })
/>
</div>
)
}
})
function SessionCell({ artifact, ctx }: { artifact: ArtifactRecord; ctx: CellCtx }) {
const SessionCell = memo(function SessionCell({ artifact, ctx }: { artifact: ArtifactRecord; ctx: CellCtx }) {
return (
<ArtifactCellAction onClick={() => ctx.onOpenChat(artifact.sessionId)} title={artifact.sessionTitle}>
<span className="flex min-w-0 flex-col">
@ -615,7 +617,7 @@ function SessionCell({ artifact, ctx }: { artifact: ArtifactRecord; ctx: CellCtx
</span>
</ArtifactCellAction>
)
}
})
const ARTIFACT_COLUMNS: readonly ArtifactColumn[] = [
{

View file

@ -1,6 +1,7 @@
import { useStore } from '@nanostores/react'
import { type Translations, useI18n } from '@/i18n'
import { useStoreSelector } from '@/lib/use-session-slice'
import { cn } from '@/lib/utils'
import { $backgroundRunningSessionIds } from '@/store/composer-status'
import { $unreadFinishedSessionIds } from '@/store/session'
@ -111,11 +112,15 @@ export function SessionStatusDot({ storedSessionId, session, branchStem, classNa
useStore($sessionColorById)
const color = sessionColorFor(session) ?? null
const needsInput = useStore($attentionSessionIds).includes(storedSessionId)
const isWorking = useStore($workingSessionIds).includes(storedSessionId)
const isStalled = useStore($stalledSessionIds).includes(storedSessionId)
const isUnread = useStore($unreadFinishedSessionIds).includes(storedSessionId)
const hasBackground = useStore($backgroundRunningSessionIds).includes(storedSessionId)
// Per-session membership as booleans via useStoreSelector: these arrays tick
// on every stream delta (any session working/stalled/etc changes the array
// reference), but a given dot only repaints when ITS OWN membership flips.
// A plain useStore(array).includes(id) re-rendered every dot on every tick.
const needsInput = useStoreSelector($attentionSessionIds, ids => ids.includes(storedSessionId))
const isWorking = useStoreSelector($workingSessionIds, ids => ids.includes(storedSessionId))
const isStalled = useStoreSelector($stalledSessionIds, ids => ids.includes(storedSessionId))
const isUnread = useStoreSelector($unreadFinishedSessionIds, ids => ids.includes(storedSessionId))
const hasBackground = useStoreSelector($backgroundRunningSessionIds, ids => ids.includes(storedSessionId))
const dotState = sessionDotState({ hasBackground, isStalled, isUnread, isWorking, needsInput })

View file

@ -1,4 +1,5 @@
import { useStore } from '@nanostores/react'
import { memo } from 'react'
import type * as React from 'react'
import { ProfileTag } from '@/app/chat/profile-tag'
@ -54,7 +55,7 @@ function formatAge(seconds: number, r: Translations['sidebar']['row']): string {
return unit === 'second' ? r.ageNow : `${value}${r[AGE_KEY[unit]]}`
}
export function SidebarSessionRow({
function SidebarSessionRowImpl({
session,
branchStem,
isPinned,
@ -251,3 +252,32 @@ export function SidebarSessionRow({
</SessionContextMenu>
)
}
// The sidebar re-renders on every stream tick ($sessions/$workingSessionIds
// churn), and it stays mounted beneath every overlay — so an unmemoized row
// re-rendered the whole list (and its Codicon/label/status-dot subtree) on each
// delta, bleeding churn into Settings, Cron, Profiles, Artifacts, etc.
//
// The callback props (onArchive/onResume/…) are fresh closures every render by
// design (they close over the row's session id), so a default memo never bails.
// They're pure id-forwarders, though — identical behavior for a given row — so
// the comparator deliberately ignores them and compares only the DATA that
// changes what the row paints. A row whose session/selection/working/pin state
// is unchanged now bails out, even while a sibling session streams.
function rowPropsEqual(a: SidebarSessionRowProps, b: SidebarSessionRowProps): boolean {
return (
a.session === b.session &&
a.isPinned === b.isPinned &&
a.isSelected === b.isSelected &&
a.isWorking === b.isWorking &&
a.branchStem === b.branchStem &&
a.reorderable === b.reorderable &&
a.dragging === b.dragging &&
a.showProfile === b.showProfile &&
a.dragHandleProps === b.dragHandleProps &&
a.className === b.className &&
a.style === b.style
)
}
export const SidebarSessionRow = memo(SidebarSessionRowImpl, rowPropsEqual)

View file

@ -1,4 +1,3 @@
import { useStore } from '@nanostores/react'
import { type MouseEvent, type ReactNode, useCallback, useEffect, useMemo, useRef, useState } from 'react'
import { LogTail } from '@/components/chat/log-tail'
@ -26,6 +25,7 @@ import {
} from '@/lib/icons'
import { exportSession } from '@/lib/session-export'
import { fmtDateTime } from '@/lib/time'
import { useStoreSelector } from '@/lib/use-session-slice'
import { cn } from '@/lib/utils'
import { upsertDesktopActionTask } from '@/store/activity'
import { $pinnedSessionIds, pinSession, unpinSession } from '@/store/layout'
@ -48,6 +48,12 @@ const LOG_LEVELS = ['ALL', 'INFO', 'WARNING', 'ERROR'] as const
const USAGE_PERIODS = [7, 30, 90] as const
type UsagePeriod = (typeof USAGE_PERIODS)[number]
// Stable empty arrays so the selector returns the same reference when we're
// not on the Sessions tab — useStoreSelector bails out on Object.is, so the
// component never re-renders from $sessions ticks while on System/Usage/etc.
const EMPTY_SESSIONS: readonly never[] = []
const EMPTY_PINNED: readonly string[] = []
interface CommandCenterViewProps {
initialSection?: CommandCenterSection
onClose: () => void
@ -129,10 +135,12 @@ function EmptyPanel({ action, description, title }: { action?: ReactNode; descri
export function CommandCenterView({ initialSection, onClose, onDeleteSession, onOpenSession }: CommandCenterViewProps) {
const { t } = useI18n()
const cc = t.commandCenter
const sessions = useStore($sessions)
const pinnedSessionIds = useStore($pinnedSessionIds)
// $sessions ticks on every streaming token (title updates, new sessions),
// but we only need the data on the Sessions tab. Subscribe conditionally so
// the System/Usage/Maintenance tabs don't re-render on every stream delta.
const [section, setSection] = useRouteEnumParam('section', SECTIONS, initialSection ?? 'sessions')
const sessions = useStoreSelector($sessions, s => (section === 'sessions' ? s : EMPTY_SESSIONS))
const pinnedSessionIds = useStoreSelector($pinnedSessionIds, s => (section === 'sessions' ? s : EMPTY_PINNED))
const [query, setQuery] = useState('')
const [status, setStatus] = useState<StatusResponse | null>(null)
@ -294,25 +302,29 @@ export function CommandCenterView({ initialSection, onClose, onDeleteSession, on
[cc, refreshSystem]
)
const navGroups = useMemo(
() =>
SECTIONS.map(value => ({
active: section === value,
icon:
value === 'sessions'
? MessageCircle
: value === 'system'
? Activity
: value === 'maintenance'
? Wrench
: BarChart3,
id: value,
label: cc.sections[value],
onSelect: () => setSection(value)
})),
[cc, section, setSection]
)
return (
<OverlayView closeLabel={cc.close} onClose={onClose}>
<OverlaySplitLayout>
<OverlayNav
groups={SECTIONS.map(value => ({
active: section === value,
icon:
value === 'sessions'
? MessageCircle
: value === 'system'
? Activity
: value === 'maintenance'
? Wrench
: BarChart3,
id: value,
label: cc.sections[value],
onSelect: () => setSection(value)
}))}
/>
<OverlayNav groups={navGroups} />
<OverlayMain>
<header className="mb-4 flex items-center justify-between gap-3 max-[47.5rem]:mb-2">

View file

@ -1,7 +1,7 @@
import { useStore } from '@nanostores/react'
import { useQuery } from '@tanstack/react-query'
import { Dialog as DialogPrimitive } from 'radix-ui'
import { useCallback, useEffect, useMemo, useRef, useState } from 'react'
import { memo, useCallback, useEffect, useMemo, useRef, useState } from 'react'
import { useNavigate } from 'react-router-dom'
import { HUD_HEADING, HUD_ITEM, HUD_POSITION, HUD_SURFACE, HUD_TEXT } from '@/app/floating-hud'
@ -218,6 +218,41 @@ const rankGroups = (groups: PaletteGroup[], search: string): PaletteGroup[] => {
// theme lists under both Light and Dark). The id suffix disambiguates.
const paletteValue = (item: PaletteItem): string => `${item.label}\u0001${item.id}`
const PaletteRow = memo(function PaletteRow({
bindings,
item,
onSelectMods,
onSelectItem
}: {
bindings: Record<string, string[]>
item: PaletteItem
onSelectMods: (event: { ctrlKey: boolean; metaKey: boolean; shiftKey: boolean }) => void
onSelectItem: (item: PaletteItem) => void
}) {
const Icon = item.icon
const combo = item.action ? bindings[item.action]?.[0] : undefined
return (
<CommandItem
className={cn(HUD_ITEM, HUD_TEXT)}
keywords={item.keywords}
onMouseDown={onSelectMods}
onSelect={() => onSelectItem(item)}
value={paletteValue(item)}
>
<Icon className="size-3.5 shrink-0 text-muted-foreground" />
<span className="truncate">{item.label}</span>
{combo && <KbdCombo className="ml-auto opacity-55" combo={combo} size="sm" />}
{item.to && (
<ChevronRight className={cn('size-3.5 shrink-0 text-muted-foreground/70', !combo && 'ml-auto')} />
)}
{item.active && (
<Check className={cn('size-3.5 shrink-0 text-primary', !combo && !item.to && 'ml-auto')} />
)}
</CommandItem>
)
})
// Hermes session ids: <YYYYMMDD>_<HHMMSS>_<6 hex>. Used to offer a direct
// "Go to session id" jump for ids that aren't in the recent-200 list.
const SESSION_ID_RE = /^\d{8}_\d{6}_[a-f0-9]{6}$/
@ -996,35 +1031,15 @@ export function CommandPalette() {
heading={group.heading}
key={group.heading ?? `palette-group-${index}`}
>
{group.items.map(item => {
const Icon = item.icon
const combo = item.action ? bindings[item.action]?.[0] : undefined
return (
<CommandItem
className={cn(HUD_ITEM, HUD_TEXT)}
key={item.id}
keywords={item.keywords}
onMouseDown={noteSelectMods}
onSelect={() => handleSelect(item)}
value={paletteValue(item)}
>
<Icon className="size-3.5 shrink-0 text-muted-foreground" />
<span className="truncate">{item.label}</span>
{combo && <KbdCombo className="ml-auto opacity-55" combo={combo} size="sm" />}
{item.to && (
<ChevronRight
className={cn('size-3.5 shrink-0 text-muted-foreground/70', !combo && 'ml-auto')}
/>
)}
{item.active && (
<Check
className={cn('size-3.5 shrink-0 text-primary', !combo && !item.to && 'ml-auto')}
/>
)}
</CommandItem>
)
})}
{group.items.map(item => (
<PaletteRow
bindings={bindings}
item={item}
key={item.id}
onSelectItem={handleSelect}
onSelectMods={noteSelectMods}
/>
))}
</CommandGroup>
))}
</>

View file

@ -1,4 +1,4 @@
import { Fragment, type ReactNode } from 'react'
import { Fragment, memo, type ReactNode } from 'react'
import { TabDropdown } from '@/components/ui/tab-dropdown'
import type { IconComponent } from '@/lib/icons'
@ -94,7 +94,14 @@ export function OverlayMain({ children, className }: OverlayMainProps) {
)
}
export function OverlayNavItem({ active, icon: Icon, label, nested, onClick, trailing }: OverlayNavItemProps) {
export const OverlayNavItem = memo(function OverlayNavItem({
active,
icon: Icon,
label,
nested,
onClick,
trailing
}: OverlayNavItemProps) {
return (
<button
className={cn(
@ -121,7 +128,7 @@ export function OverlayNavItem({ active, icon: Icon, label, nested, onClick, tra
{trailing}
</button>
)
}
})
export interface OverlayNavLink {
active: boolean

View file

@ -1,4 +1,4 @@
import { useEffect, useRef } from 'react'
import { useCallback, useEffect, useMemo, useRef } from 'react'
import { useLocation, useNavigate } from 'react-router-dom'
import { codiconIcon } from '@/components/ui/codicon'
@ -85,22 +85,29 @@ export function SettingsView({ onClose, onConfigSaved, onMainModelChanged }: Set
// Jump to a section + its sub-view in one navigate. Two sequential setters
// would each read the same stale `search` and the second would clobber the
// first's `tab` — so the sub-view never opened on narrow screens.
const openSubView = (tab: SettingsViewId, param: string, value: string, fallback: string) => {
const params = new URLSearchParams(search)
params.set('tab', tab)
const openSubView = useCallback(
(tab: SettingsViewId, param: string, value: string, fallback: string) => {
const params = new URLSearchParams(search)
params.set('tab', tab)
if (value === fallback) {
params.delete(param)
} else {
params.set(param, value)
}
if (value === fallback) {
params.delete(param)
} else {
params.set(param, value)
}
const qs = params.toString()
navigate({ hash, pathname, search: qs ? `?${qs}` : '' }, { replace: true })
}
const qs = params.toString()
navigate({ hash, pathname, search: qs ? `?${qs}` : '' }, { replace: true })
},
[hash, navigate, pathname, search]
)
const openProviderView = (view: ProviderView) => openSubView('providers', 'pview', view, 'accounts')
const openKeysView = (view: KeysView) => openSubView('keys', 'kview', view, 'tools')
const openProviderView = useCallback(
(view: ProviderView) => openSubView('providers', 'pview', view, 'accounts'),
[openSubView]
)
const openKeysView = useCallback((view: KeysView) => openSubView('keys', 'kview', view, 'tools'), [openSubView])
const importInputRef = useRef<HTMLInputElement | null>(null)
@ -134,123 +141,124 @@ export function SettingsView({ onClose, onConfigSaved, onMainModelChanged }: Set
}
}
const navGroups: OverlayNavGroup[] = [
const navGroups: OverlayNavGroup[] = useMemo(() => [
...SECTIONS.map(s => {
const view = `config:${s.id}` as SettingsViewId
const view = `config:${s.id}` as SettingsViewId
return {
active: activeView === view,
icon: s.icon,
id: view,
label: t.settings.sections[s.id] ?? s.label,
onSelect: () => setActiveView(view)
return {
active: activeView === view,
icon: s.icon,
id: view,
label: t.settings.sections[s.id] ?? s.label,
onSelect: () => setActiveView(view)
}
}),
{
active: activeView === 'notifications',
icon: Bell,
id: 'notifications',
label: t.settings.nav.notifications,
onSelect: () => setActiveView('notifications')
},
{
active: activeView === 'billing',
icon: BarChart3,
id: 'billing',
label: t.settings.nav.billing,
onSelect: () => setActiveView('billing')
},
{
active: activeView === 'providers',
children: [
{
active: activeView === 'providers' && providerView === 'accounts',
icon: codiconIcon('account'),
id: 'pview:accounts',
label: t.settings.nav.providerAccounts,
onSelect: () => openProviderView('accounts')
},
{
active: activeView === 'providers' && providerView === 'keys',
icon: KeyRound,
id: 'pview:keys',
label: t.settings.nav.providerApiKeys,
onSelect: () => openProviderView('keys')
},
{
active: activeView === 'providers' && providerView === 'custom-endpoints',
icon: Globe,
id: 'pview:custom-endpoints',
label: t.settings.nav.providerCustomEndpoints,
onSelect: () => openProviderView('custom-endpoints')
}
],
gapBefore: true,
icon: Zap,
id: 'providers',
label: t.settings.nav.providers,
onSelect: () => setActiveView('providers')
},
{
active: activeView === 'gateway',
icon: Globe,
id: 'gateway',
label: t.settings.nav.gateway,
onSelect: () => setActiveView('gateway')
},
{
active: activeView === 'keybinds',
icon: Keyboard,
id: 'keybinds',
label: t.settings.nav.keybinds,
onSelect: () => setActiveView('keybinds')
},
{
active: activeView === 'keys',
children: [
{
active: activeView === 'keys' && keysView === 'tools',
icon: Wrench,
id: 'kview:tools',
label: t.settings.nav.keysTools,
onSelect: () => openKeysView('tools')
},
{
active: activeView === 'keys' && keysView === 'settings',
icon: Settings2,
id: 'kview:settings',
label: t.settings.nav.keysSettings,
onSelect: () => openKeysView('settings')
}
],
icon: KeyRound,
id: 'keys',
label: t.settings.nav.apiKeys,
onSelect: () => setActiveView('keys')
},
{
active: activeView === 'plugins',
icon: Package,
id: 'plugins',
label: t.settings.nav.plugins,
onSelect: () => setActiveView('plugins')
},
{
active: activeView === 'sessions',
icon: Archive,
id: 'sessions',
label: t.settings.nav.archivedChats,
onSelect: () => setActiveView('sessions')
},
{
active: activeView === 'about',
gapBefore: true,
icon: Info,
id: 'about',
label: t.settings.nav.about,
onSelect: () => setActiveView('about')
}
}),
{
active: activeView === 'notifications',
icon: Bell,
id: 'notifications',
label: t.settings.nav.notifications,
onSelect: () => setActiveView('notifications')
},
{
active: activeView === 'billing',
icon: BarChart3,
id: 'billing',
label: t.settings.nav.billing,
onSelect: () => setActiveView('billing')
},
{
active: activeView === 'providers',
children: [
{
active: activeView === 'providers' && providerView === 'accounts',
icon: codiconIcon('account'),
id: 'pview:accounts',
label: t.settings.nav.providerAccounts,
onSelect: () => openProviderView('accounts')
},
{
active: activeView === 'providers' && providerView === 'keys',
icon: KeyRound,
id: 'pview:keys',
label: t.settings.nav.providerApiKeys,
onSelect: () => openProviderView('keys')
},
{
active: activeView === 'providers' && providerView === 'custom-endpoints',
icon: Globe,
id: 'pview:custom-endpoints',
label: t.settings.nav.providerCustomEndpoints,
onSelect: () => openProviderView('custom-endpoints')
}
],
gapBefore: true,
icon: Zap,
id: 'providers',
label: t.settings.nav.providers,
onSelect: () => setActiveView('providers')
},
{
active: activeView === 'gateway',
icon: Globe,
id: 'gateway',
label: t.settings.nav.gateway,
onSelect: () => setActiveView('gateway')
},
{
active: activeView === 'keybinds',
icon: Keyboard,
id: 'keybinds',
label: t.settings.nav.keybinds,
onSelect: () => setActiveView('keybinds')
},
{
active: activeView === 'keys',
children: [
{
active: activeView === 'keys' && keysView === 'tools',
icon: Wrench,
id: 'kview:tools',
label: t.settings.nav.keysTools,
onSelect: () => openKeysView('tools')
},
{
active: activeView === 'keys' && keysView === 'settings',
icon: Settings2,
id: 'kview:settings',
label: t.settings.nav.keysSettings,
onSelect: () => openKeysView('settings')
}
],
icon: KeyRound,
id: 'keys',
label: t.settings.nav.apiKeys,
onSelect: () => setActiveView('keys')
},
{
active: activeView === 'plugins',
icon: Package,
id: 'plugins',
label: t.settings.nav.plugins,
onSelect: () => setActiveView('plugins')
},
{
active: activeView === 'sessions',
icon: Archive,
id: 'sessions',
label: t.settings.nav.archivedChats,
onSelect: () => setActiveView('sessions')
},
{
active: activeView === 'about',
gapBefore: true,
icon: Info,
id: 'about',
label: t.settings.nav.about,
onSelect: () => setActiveView('about')
}
]
]
, [activeView, keysView, providerView, t, setActiveView, openProviderView, openKeysView])
const navFooter = (
<>

View file

@ -16,7 +16,6 @@ import {
getSkills,
getToolsets,
getUsageAnalytics,
type HermesGateway,
toggleSkill,
toggleToolset
} from '@/hermes'
@ -26,6 +25,7 @@ import { compactNumber } from '@/lib/format'
import { queryClient, writeCache } from '@/lib/query-client'
import { invalidateSlashCompletions } from '@/lib/slash-completion-cache'
import { normalize } from '@/lib/text'
import { useStoreSelector } from '@/lib/use-session-slice'
import { $gateway } from '@/store/gateway'
import { notify, notifyError } from '@/store/notifications'
import { $activeGatewayProfile, normalizeProfileKey } from '@/store/profile'
@ -184,8 +184,10 @@ interface SkillsViewProps extends React.ComponentProps<'section'> {
export function SkillsView({ setStatusbarItemGroup: _setStatusbarItemGroup, ...props }: SkillsViewProps) {
const { t } = useI18n()
const gateway = useStore($gateway) as HermesGateway | null
const [mode, setMode] = useRouteEnumParam('tab', SKILLS_MODES, 'skills')
// $gateway only feeds the MCP tab — gate the subscription so Skills/Toolsets/Hub
// tabs don't re-render on connect/disconnect/reconnect.
const gateway = useStoreSelector($gateway, g => (mode === 'mcp' ? g : null))
const [query, setQuery] = useState('')