fmt(js): npm run fix on merge (#105106)

Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
This commit is contained in:
hermes-seaeye[bot]
2026-09-07 13:53:40 +00:00
committed by GitHub
parent 26ed08c23e
commit 1f92ab1ddf
19 changed files with 209 additions and 80 deletions
+6 -2
View File
@@ -10,10 +10,14 @@ export function windowsUpdatePrerequisiteError(updateRoot: string): string | nul
const required = [path.join(updateRoot, 'venv', 'Scripts', 'python.exe')]
// Pre-reorg flat scripts remain supported; damaged modern trees do not.
if (existsSync(maintainedDir)) {required.push(path.join(maintainedDir, 'windows.ps1'))}
if (existsSync(maintainedDir)) {
required.push(path.join(maintainedDir, 'windows.ps1'))
}
for (const candidate of required) {
if (stagedFileExists(candidate)) {continue}
if (stagedFileExists(candidate)) {
continue
}
return `Update aborted: ${candidate} is missing or unreadable. Repair the installation and review antivirus quarantine before retrying.`
}
+10 -2
View File
@@ -205,7 +205,10 @@ export function listOAuthProviders(profile?: null | string): Promise<OAuthProvid
})
}
export function disconnectOAuthProvider(providerId: string, profile?: null | string): Promise<{ ok: boolean; provider: string }> {
export function disconnectOAuthProvider(
providerId: string,
profile?: null | string
): Promise<{ ok: boolean; provider: string }> {
return hermesApi<{ ok: boolean; provider: string }>({
...profileScoped(profile),
path: `/api/providers/oauth/${encodeURIComponent(providerId)}`,
@@ -222,7 +225,12 @@ export function startOAuthLogin(providerId: string, profile?: ProfileScope): Pro
})
}
export function submitOAuthCode(providerId: string, sessionId: string, code: string, profile?: null | string): Promise<OAuthSubmitResponse> {
export function submitOAuthCode(
providerId: string,
sessionId: string,
code: string,
profile?: null | string
): Promise<OAuthSubmitResponse> {
return hermesApi<OAuthSubmitResponse>({
...profileScoped(profile),
path: `/api/providers/oauth/${encodeURIComponent(providerId)}/submit`,
@@ -13,7 +13,14 @@ it('reveals the complete slash description on hover without intercepting selecti
const onPick = vi.fn()
render(
<I18nProvider configClient={null} initialLocale="en">
<ComposerTriggerPopover activeIndex={0} items={[item]} kind="/" loading={false} onHover={vi.fn()} onPick={onPick} />
<ComposerTriggerPopover
activeIndex={0}
items={[item]}
kind="/"
loading={false}
onHover={vi.fn()}
onPick={onPick}
/>
</I18nProvider>
)
const row = screen.getByRole('button')
@@ -199,4 +199,4 @@ describe('ComposerTriggerPopover keyboard scrolling', () => {
expect(drawer.scrollTop).toBe(30)
})
})
})
@@ -230,7 +230,9 @@ export function ComposerTriggerPopover({
</span>
<span className="min-w-0 shrink truncate font-medium leading-5 text-foreground">{display}</span>
{description && (
<span className="min-w-0 flex-1 truncate leading-5 text-(--ui-text-tertiary)">{description}</span>
<span className="min-w-0 flex-1 truncate leading-5 text-(--ui-text-tertiary)">
{description}
</span>
)}
</>
)}
+12 -3
View File
@@ -175,7 +175,12 @@ import {
useRepoWorktreeMap
} from './projects'
import { WorktreeDialog } from './projects/worktree-dialog'
import { SidebarBlankState, SidebarLoadErrorState, SidebarPinnedEmptyState, SidebarSessionSkeletons } from './section-states'
import {
SidebarBlankState,
SidebarLoadErrorState,
SidebarPinnedEmptyState,
SidebarSessionSkeletons
} from './section-states'
import { buildSessionByAnyId, resolvePinnedSessions } from './session-index'
import { SidebarSessionsSection, VIRTUALIZE_THRESHOLD } from './sessions-section'
import { CONTEXT_SPLIT_KIT, SplitSubmenu } from './split-submenu'
@@ -987,8 +992,12 @@ export function ChatSidebar({
// Entering a project lazily hydrates its full lanes (repo -> lane -> sessions)
// from the backend — same grouping/ids as the overview, just with rows.
const { project: enteredProjectTree, failed: projectLoadFailed, loading: projectLoading, retry: retryProject } =
useEnteredProjectSessions(enteredProjectId, gatewayReady, projectTree, `${activeConnectionId}:${profileScope}`)
const {
project: enteredProjectTree,
failed: projectLoadFailed,
loading: projectLoading,
retry: retryProject
} = useEnteredProjectSessions(enteredProjectId, gatewayReady, projectTree, `${activeConnectionId}:${profileScope}`)
// Prefer the hydrated tree; fall back to the overview node (empty lanes) while
// the drill-in fetch is in flight, so the header/structure render immediately.
@@ -11,16 +11,20 @@ afterEach(cleanup)
it('ignores departed drill-ins and clears failure on retry', async () => {
let failOld!: (error: Error) => void
vi.mocked(fetchProjectSessions)
.mockImplementationOnce(() => new Promise((_, reject) => { failOld = reject }))
.mockImplementationOnce(
() =>
new Promise((_, reject) => {
failOld = reject
})
)
.mockResolvedValueOnce(null)
.mockRejectedValueOnce(new Error('failed'))
.mockResolvedValueOnce(null)
const tree: never[] = []
const { result, rerender } = renderHook(
({ id }) => useEnteredProjectSessions(id, true, tree, 'default'),
{ initialProps: { id: 'old' } }
)
const { result, rerender } = renderHook(({ id }) => useEnteredProjectSessions(id, true, tree, 'default'), {
initialProps: { id: 'old' }
})
rerender({ id: 'current' })
await waitFor(() => expect(result.current.loading).toBe(false))
@@ -17,7 +17,9 @@ export function useEnteredProjectSessions(
const [loading, setLoading] = useState(false)
const [retryToken, setRetryToken] = useState(0)
useEffect(() => { setProject(null) }, [projectId, scope])
useEffect(() => {
setProject(null)
}, [projectId, scope])
useEffect(() => {
let cancelled = false
@@ -32,11 +34,25 @@ export function useEnteredProjectSessions(
setLoading(true)
void fetchProjectSessions(projectId)
.then(next => { if (!cancelled) { setProject(next) } })
.catch(() => { if (!cancelled) { setFailed(true) } })
.finally(() => { if (!cancelled) { setLoading(false) } })
.then(next => {
if (!cancelled) {
setProject(next)
}
})
.catch(() => {
if (!cancelled) {
setFailed(true)
}
})
.finally(() => {
if (!cancelled) {
setLoading(false)
}
})
return () => { cancelled = true }
return () => {
cancelled = true
}
}, [projectId, ready, treeRevision, scope, retryToken])
return { project, failed, loading, retry: () => setRetryToken(token => token + 1) }
@@ -61,12 +61,14 @@ function renderMenu() {
)
}
it('matches and highlights separator-equivalent queries without revealing unrelated models', async () => {
renderMenu()
await screen.findByText(/Qwen3\.8 Flash/i)
for (const [query, marked] of [['qwen3.8-flash', 'Qwen3.8 Flash'], ['qwen3 8', 'Qwen3.8']]) {
for (const [query, marked] of [
['qwen3.8-flash', 'Qwen3.8 Flash'],
['qwen3 8', 'Qwen3.8']
]) {
fireEvent.change(screen.getByRole('textbox', { name: 'Search models' }), { target: { value: query } })
await vi.waitFor(() => {
expect(screen.getByText(marked, { selector: 'mark' })).not.toBeNull()
@@ -169,5 +169,4 @@ describe('list session-scroll restore', () => {
// resulting reader position. Here the abandoned target must not return.
expect(vp.scrollTop).not.toBe(2000 - CLIENT_H - 800)
})
})
+1 -4
View File
@@ -265,10 +265,7 @@ function ModelResults({
const q = normalize(search)
const matches = (provider: ModelOptionProvider, model: string) =>
!q ||
foldIncludes(modelSearchText(model), q) ||
foldIncludes(provider.name, q) ||
foldIncludes(provider.slug, q)
!q || foldIncludes(modelSearchText(model), q) || foldIncludes(provider.name, q) || foldIncludes(provider.slug, q)
// Only configured providers (those with curated models) are selectable
// here. Switching to a NOT-yet-configured provider goes through the
@@ -198,13 +198,16 @@ export function DesktopOnboardingOverlay({
const targetProfile = onboarding.targetProfile ?? profile
// Async flows retain the initiating route even after the overlay closes.
const ctx = useMemo<OnboardingContext>(() => ({
profile: targetProfile,
requestGateway: onboarding.targetProfile
? (method, params) => requestGatewayForProfile(targetProfile, method, params)
: requestGateway,
onCompleted: () => onCompletedRef.current?.()
}), [onboarding.targetProfile, targetProfile, requestGateway])
const ctx = useMemo<OnboardingContext>(
() => ({
profile: targetProfile,
requestGateway: onboarding.targetProfile
? (method, params) => requestGatewayForProfile(targetProfile, method, params)
: requestGateway,
onCompleted: () => onCompletedRef.current?.()
}),
[onboarding.targetProfile, targetProfile, requestGateway]
)
// Cinematic exit on "Begin": dissolve the panel + overlay (revealing the chat
// behind), THEN finalize so the unmount lands after the fade — mirrors the
@@ -42,4 +42,4 @@ describe('HighlightMatches', () => {
expect(marksOf(container)).toEqual(['Grok'])
})
})
})
@@ -15,7 +15,9 @@ vi.mock('@/hermes', async importActual => ({
setApiRequestConnection: vi.fn(),
HermesGateway: class {
connectionState = 'closed'
private eventHandlers = new Set<(event: { payload?: Record<string, unknown>; session_id?: string; type: string }) => void>()
private eventHandlers = new Set<
(event: { payload?: Record<string, unknown>; session_id?: string; type: string }) => void
>()
request = vi.fn(async (method: string, params: Record<string, unknown>) => ({ method, params }))
constructor() {
@@ -58,7 +60,8 @@ const {
const { $profiles } = await import('./profile')
const { $activeSessionId, _resetSessionOwnerHintsForTests, setSessionOwnerHint, setSessions } = await import('./session')
const { $activeSessionId, _resetSessionOwnerHintsForTests, setSessionOwnerHint, setSessions } =
await import('./session')
const { $gateway } = await import('./gateway')
const { clearAllPrompts } = await import('./prompts')
@@ -160,8 +163,9 @@ describe('profile-only secondary approval ownership', () => {
it('rejects unproven profiles and retires only transient local ownership, never durable exact routes', async () => {
recordSessionEventScope({ profile: 'research', session_id: 'rt-unproven' })
expect(knownOwnerForSession('rt-unproven')).toBeUndefined()
await expect(requestForOwnedSession('rt-unproven', vi.fn() as never, 'approval.respond', {}))
.rejects.toSatisfy(isSessionOwnerResolutionError)
await expect(requestForOwnedSession('rt-unproven', vi.fn() as never, 'approval.respond', {})).rejects.toSatisfy(
isSessionOwnerResolutionError
)
await ensureGatewayForProfile('research')
const secondary = gatewayMocks.instances[0]
@@ -178,8 +182,9 @@ describe('profile-only secondary approval ownership', () => {
expect(knownOwnerForSession('rt-remote')).toEqual(exact)
const getConnection = window.hermesDesktop!.getConnection
vi.mocked(getConnection).mockClear()
await expect(requestForOwnedSession('rt-retired', vi.fn() as never, 'approval.respond', {}))
.rejects.toSatisfy(isSessionOwnerResolutionError)
await expect(requestForOwnedSession('rt-retired', vi.fn() as never, 'approval.respond', {})).rejects.toSatisfy(
isSessionOwnerResolutionError
)
expect(getConnection).not.toHaveBeenCalled()
})
})
+30 -10
View File
@@ -81,28 +81,45 @@ function fallbackTimeoutGateway(): OnboardingContext['requestGateway'] {
describe('refreshOnboarding', () => {
it('keeps onboarding work in its initiating lifetime and profile', async () => {
const { startManualOnboarding, startProviderOAuth, saveOnboardingApiKey, closeManualOnboarding } = await import('./onboarding')
const { startManualOnboarding, startProviderOAuth, saveOnboardingApiKey, closeManualOnboarding } =
await import('./onboarding')
const requests: { path: string; profile?: string }[] = []
let release!: () => void
let delayKey = true
installApiMock(async request => {
requests.push(request)
if (request.path === '/api/providers/oauth') { return { providers: [] } }
if (request.path.endsWith('/start')) {
return { flow: 'device_code', session_id: 'local-fixture', user_code: 'FAKE', verification_url: 'http://localhost/fixture', expires_in: 600 }
if (request.path === '/api/providers/oauth') {
return { providers: [] }
}
if (request.path.includes('/poll/')) { return { status: 'approved' } }
if (request.path.endsWith('/start')) {
return {
flow: 'device_code',
session_id: 'local-fixture',
user_code: 'FAKE',
verification_url: 'http://localhost/fixture',
expires_in: 600
}
}
if (request.path === '/api/env' && delayKey) { await new Promise<void>(resolve => { release = resolve }) }
if (request.path.includes('/poll/')) {
return { status: 'approved' }
}
if (request.path === '/api/env' && delayKey) {
await new Promise<void>(resolve => {
release = resolve
})
}
if (request.path.startsWith('/api/model/options')) {
return { providers: [{ slug: 'fixture', name: 'Fixture', models: ['fixture-model'] }] }
}
if (request.path.startsWith('/api/model/recommended-default')) { return { model: 'fixture-model' } }
if (request.path.startsWith('/api/model/recommended-default')) {
return { model: 'fixture-model' }
}
return { ok: true }
})
@@ -110,8 +127,11 @@ describe('refreshOnboarding', () => {
let profile = 'beta'
const ctx: OnboardingContext = {
get profile() { return profile },
requestGateway: async method => (method === 'setup.status' ? { provider_configured: true } : { ok: true }) as never
get profile() {
return profile
},
requestGateway: async method =>
(method === 'setup.status' ? { provider_configured: true } : { ok: true }) as never
}
try {
+67 -24
View File
@@ -341,10 +341,14 @@ async function completeWithModelConfirm(
const generation = flowGeneration
await ctx.requestGateway('reload.env').catch(() => undefined)
if (generation !== flowGeneration) { return }
if (generation !== flowGeneration) {
return
}
const defaults = await fetchProviderDefaultModel(preferredSlugs, ctx.profile)
if (generation !== flowGeneration) { return }
if (generation !== flowGeneration) {
return
}
if (defaults) {
// Persist the chosen provider/model before the runtime gate so a stale
@@ -362,10 +366,14 @@ async function completeWithModelConfirm(
{ skipConfirmPrompt: true }
)
if (generation !== flowGeneration) { return }
if (generation !== flowGeneration) {
return
}
notifyGatewayTools(res.gateway_tools)
} catch (error) {
if (generation !== flowGeneration) { return }
if (generation !== flowGeneration) {
return
}
onFail(error instanceof Error ? error.message : 'Hermes could not save the selected model.')
return
@@ -374,7 +382,9 @@ async function completeWithModelConfirm(
const runtime = await checkRuntime(ctx, preferredSlugs[0])
if (generation !== flowGeneration) { return }
if (generation !== flowGeneration) {
return
}
if (!runtime.ready && !ignoreRuntimeGate) {
onFail(runtime.reason)
@@ -420,13 +430,19 @@ async function refreshProviders() {
try {
const { providers } = await listOAuthProviders($desktopOnboarding.get().targetProfile)
if (generation !== flowGeneration) { return }
if (generation !== flowGeneration) {
return
}
patch({ mode: providers.length > 0 ? 'oauth' : 'apikey', providers })
} catch {
if (generation !== flowGeneration) { return }
if (generation !== flowGeneration) {
return
}
patch({ mode: 'apikey', providers: [] })
} finally {
if (generation === flowGeneration) { providersRefreshPromise = null }
if (generation === flowGeneration) {
providersRefreshPromise = null
}
}
})()
@@ -706,7 +722,9 @@ export async function startProviderOAuth(provider: OAuthProvider, ctx: Onboardin
)
pollTimer = window.setInterval(() => void pollSession(provider, start, ctx, generation), POLL_MS)
} catch (error) {
if (generation !== flowGeneration) { return }
if (generation !== flowGeneration) {
return
}
setFlow({ status: 'error', provider, message: `Could not start sign-in: ${errMessage(error)}` })
}
}
@@ -716,7 +734,9 @@ async function pollSession(provider: OAuthProvider, start: DeviceStart, ctx: Onb
try {
const { error_message, status } = await pollOAuthSession(provider.id, start.session_id, ctx.profile)
if (generation !== flowGeneration) { return }
if (generation !== flowGeneration) {
return
}
if (status === 'approved') {
clearPoll()
@@ -733,7 +753,9 @@ async function pollSession(provider: OAuthProvider, start: DeviceStart, ctx: Onb
setFlow({ status: 'error', provider, start, message: error_message || `Sign-in ${status}.` })
}
} catch (error) {
if (generation !== flowGeneration) { return }
if (generation !== flowGeneration) {
return
}
clearPoll()
setFlow({ status: 'error', provider, start, message: `Polling failed: ${errMessage(error)}` })
}
@@ -763,7 +785,9 @@ export async function submitOnboardingCode(ctx: OnboardingContext) {
try {
const resp = await submitOAuthCode(provider.id, start.session_id, code.trim(), ctx.profile)
if (generation !== flowGeneration) { return }
if (generation !== flowGeneration) {
return
}
if (resp.ok && resp.status === 'approved') {
setFlow({ status: 'success', provider })
@@ -778,7 +802,9 @@ export async function submitOnboardingCode(ctx: OnboardingContext) {
setFlow({ status: 'error', provider, start, message: resp.message || 'Token exchange failed.' })
}
} catch (error) {
if (generation !== flowGeneration) { return }
if (generation !== flowGeneration) {
return
}
setFlow({ status: 'error', provider, start, message: errMessage(error) })
}
}
@@ -896,7 +922,9 @@ export async function saveOnboardingApiKey(
try {
await setEnvVar(envKey, trimmed, ctx.profile)
if (generation !== flowGeneration) { return { ok: false } }
if (generation !== flowGeneration) {
return { ok: false }
}
// For API-key flows we don't have a definitive provider id (the
// user picked which API key they're entering, but the corresponding
// backend slug — e.g. OPENROUTER_API_KEY → "openrouter" — is the
@@ -950,7 +978,9 @@ export async function saveOnboardingLocalEndpoint(baseUrl: string, apiKey: strin
try {
const probe = await validateProviderCredential('OPENAI_BASE_URL', url, key)
if (generation !== flowGeneration) { return { ok: false } }
if (generation !== flowGeneration) {
return { ok: false }
}
if (!probe.ok && probe.reachable) {
return { ok: false, message: probe.message || 'Could not reach that endpoint.' }
@@ -975,14 +1005,20 @@ export async function saveOnboardingLocalEndpoint(baseUrl: string, apiKey: strin
try {
await setMainModelAssignment({ provider: 'custom', model, base_url: url, api_key: key }, ctx.profile)
if (generation !== flowGeneration) { return { ok: false } }
if (generation !== flowGeneration) {
return { ok: false }
}
await ctx.requestGateway('reload.env').catch(() => undefined)
if (generation !== flowGeneration) { return { ok: false } }
if (generation !== flowGeneration) {
return { ok: false }
}
const runtime = await checkRuntime(ctx)
if (generation !== flowGeneration) { return { ok: false } }
if (generation !== flowGeneration) {
return { ok: false }
}
if (!runtime.ready) {
const detail = (runtime.reason ?? '').trim()
@@ -1017,19 +1053,26 @@ export async function setOnboardingModel(model: string) {
setFlow({ ...flow, currentModel: model, saving: true })
try {
await setMainModelAssignment({
provider: flow.providerSlug,
model
}, flowProfile ?? $desktopOnboarding.get().targetProfile)
await setMainModelAssignment(
{
provider: flow.providerSlug,
model
},
flowProfile ?? $desktopOnboarding.get().targetProfile
)
if (generation !== flowGeneration) { return }
if (generation !== flowGeneration) {
return
}
const current = $desktopOnboarding.get().flow
if (current.status === 'confirming_model') {
setFlow({ ...current, currentModel: model, saving: false })
}
} catch (error) {
if (generation !== flowGeneration) { return }
if (generation !== flowGeneration) {
return
}
notifyError(error, 'Could not change model')
const current = $desktopOnboarding.get().flow
+5 -2
View File
@@ -536,8 +536,11 @@ export async function fetchProjectSessions(projectId: string): Promise<SidebarPr
return res.project ?? null
} catch (error) {
if (generation !== projectSessionsRefreshGeneration || profile !== projectProfile() ||
(context && !stillOnProjectsContext(context))) {
if (
generation !== projectSessionsRefreshGeneration ||
profile !== projectProfile() ||
(context && !stillOnProjectsContext(context))
) {
return null
}
+1 -1
View File
@@ -90,4 +90,4 @@ describe('requestScrollToBottom', () => {
expect(second).toHaveBeenCalledOnce()
stopSecond()
})
})
})
+9 -2
View File
@@ -216,11 +216,18 @@ function persistPositions(storageKey: string, positions: Record<string, ThreadSc
writeKey(storageKey, keys.length === 0 ? null : JSON.stringify(positions))
}
export function getThreadScrollPosition(sessionKey: string, storageKey = threadScrollStorageKey()): ThreadScrollState | undefined {
export function getThreadScrollPosition(
sessionKey: string,
storageKey = threadScrollStorageKey()
): ThreadScrollState | undefined {
return loadPositions(storageKey)[sessionKey]
}
export function saveThreadScrollPosition(sessionKey: string, state: ThreadScrollState, storageKey = threadScrollStorageKey()) {
export function saveThreadScrollPosition(
sessionKey: string,
state: ThreadScrollState,
storageKey = threadScrollStorageKey()
) {
const positions = loadPositions(storageKey)
// Delete then re-add to track recency (insertion order = LRU anchor).