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:
committed by
GitHub
parent
26ed08c23e
commit
1f92ab1ddf
@@ -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.`
|
||||
}
|
||||
|
||||
@@ -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>
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
|
||||
@@ -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)
|
||||
})
|
||||
|
||||
})
|
||||
|
||||
@@ -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()
|
||||
})
|
||||
})
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
|
||||
@@ -90,4 +90,4 @@ describe('requestScrollToBottom', () => {
|
||||
expect(second).toHaveBeenCalledOnce()
|
||||
stopSecond()
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
@@ -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).
|
||||
|
||||
Reference in New Issue
Block a user