fmt(js): npm run fix on merge (#95299)
Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
This commit is contained in:
committed by
GitHub
parent
71d804d005
commit
bc943d5672
@@ -61,10 +61,4 @@ async function resolveTerminalConnectionForSender(webContentsId, getTarget, ensu
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
export {
|
||||
applyConnectionChange,
|
||||
commitConnectionFailure,
|
||||
resolveTerminalConnection,
|
||||
resolveTerminalConnectionForSender
|
||||
}
|
||||
export { applyConnectionChange, commitConnectionFailure, resolveTerminalConnection, resolveTerminalConnectionForSender }
|
||||
|
||||
@@ -232,7 +232,6 @@ test('URL route fails closed for different token, headers, kind, or Cloud org',
|
||||
}
|
||||
})
|
||||
|
||||
|
||||
test('profile remote wins over a registry-backed global SSH route', () => {
|
||||
const route = resolveDesktopRemoteRoute({
|
||||
config: {
|
||||
|
||||
@@ -356,10 +356,7 @@ import {
|
||||
import { fetchMarketplaceThemes, searchMarketplaceThemes } from './vscode-marketplace'
|
||||
import { createWakeIndicatorWindowController } from './wake-indicator-window'
|
||||
import { enumerateWindowsFrontToBack, enumerationFailed, readWindowBelow } from './window-below'
|
||||
import {
|
||||
registrySshScopeForWindowRoute,
|
||||
WindowConnectionRouteRegistry
|
||||
} from './window-connection-route'
|
||||
import { registrySshScopeForWindowRoute, WindowConnectionRouteRegistry } from './window-connection-route'
|
||||
import { installWindowRendererLifecycle } from './window-renderer-lifecycle'
|
||||
import { createWindowRevealController } from './window-reveal'
|
||||
import {
|
||||
@@ -9522,14 +9519,10 @@ async function teardownSshConnection(profile) {
|
||||
// SSH connection — so if the active profile resolves to a NON-SSH backend, the
|
||||
// terminal must NOT fall through to a global SSH host.
|
||||
function activeSshTerminalTarget(webContentsId?: number) {
|
||||
const windowRoute =
|
||||
typeof webContentsId === 'number' ? windowConnectionRoutes.get(webContentsId) : null
|
||||
const windowRoute = typeof webContentsId === 'number' ? windowConnectionRoutes.get(webContentsId) : null
|
||||
|
||||
if (windowRoute?.registryScoped && windowRoute.connectionId) {
|
||||
const scope = registrySshScopeForWindowRoute(
|
||||
windowRoute,
|
||||
readDesktopConnectionsRegistry()
|
||||
)
|
||||
const scope = registrySshScopeForWindowRoute(windowRoute, readDesktopConnectionsRegistry())
|
||||
|
||||
if (!scope) {
|
||||
return null
|
||||
@@ -9579,10 +9572,7 @@ async function ensureTerminalBackend(webContentsId: number) {
|
||||
// Loopback reach for the browser pane. Scoped to the SSH connection that
|
||||
// authorized it: a different host (or none) must never inherit live forwards
|
||||
// into somebody else's machine.
|
||||
const previewReachByWebContents = new Map<
|
||||
number,
|
||||
{ registry: PreviewReachRegistry; scope: string }
|
||||
>()
|
||||
const previewReachByWebContents = new Map<number, { registry: PreviewReachRegistry; scope: string }>()
|
||||
|
||||
async function resetPreviewReach(webContentsId?: number) {
|
||||
if (typeof webContentsId === 'number') {
|
||||
@@ -15300,9 +15290,7 @@ ipcMain.handle('hermes:stop-find-in-page', event => {
|
||||
|
||||
// The renderer can't know whether a loopback URL is reachable — only main
|
||||
// knows which transport backs this gateway. Ask before loading one.
|
||||
ipcMain.handle('hermes:preview:reach', async (event, url) =>
|
||||
reachablePreviewUrl(event.sender.id, String(url || ''))
|
||||
)
|
||||
ipcMain.handle('hermes:preview:reach', async (event, url) => reachablePreviewUrl(event.sender.id, String(url || '')))
|
||||
|
||||
ipcMain.handle('hermes:openPreviewInBrowser', async (_event, url) => {
|
||||
if (!(await openPreviewInBrowser(url))) {
|
||||
|
||||
@@ -2,10 +2,7 @@ import assert from 'node:assert/strict'
|
||||
|
||||
import { test } from 'vitest'
|
||||
|
||||
import {
|
||||
resolveTerminalConnection,
|
||||
resolveTerminalConnectionForSender
|
||||
} from './connection-apply'
|
||||
import { resolveTerminalConnection, resolveTerminalConnectionForSender } from './connection-apply'
|
||||
|
||||
const ssh = {
|
||||
host: 'registry-box.test',
|
||||
@@ -49,6 +46,7 @@ test('terminal start re-reads the SSH target after backend startup', async () =>
|
||||
ssh,
|
||||
scope: 'connection:registry-ssh'
|
||||
}
|
||||
|
||||
let ready = false
|
||||
|
||||
const resolved = await resolveTerminalConnection(
|
||||
|
||||
@@ -292,11 +292,8 @@ export function registerTerminalIpc({
|
||||
const cols = Math.max(2, Number.parseInt(String(payload?.cols || 80), 10) || 80)
|
||||
const rows = Math.max(2, Number.parseInt(String(payload?.rows || 24), 10) || 24)
|
||||
|
||||
const sshTarget = await resolveTerminalConnectionForSender(
|
||||
event.sender.id,
|
||||
activeSshTerminalTarget,
|
||||
ensureBackend
|
||||
)
|
||||
const sshTarget = await resolveTerminalConnectionForSender(event.sender.id, activeSshTerminalTarget, ensureBackend)
|
||||
|
||||
const remote = Boolean(sshTarget)
|
||||
const remoteState = remote ? getSshConnectionState(sshTarget.scope) : null
|
||||
|
||||
|
||||
@@ -95,10 +95,7 @@ test('routes a non-primary SSH connection independently from another window', ()
|
||||
registryScoped: true
|
||||
})
|
||||
|
||||
assert.equal(
|
||||
registrySshScopeForWindowRoute(routes.get(11), registry),
|
||||
'conn:source-b::worker'
|
||||
)
|
||||
assert.equal(registrySshScopeForWindowRoute(routes.get(11), registry), 'conn:source-b::worker')
|
||||
assert.equal(registrySshScopeForWindowRoute(routes.get(22), registry), null)
|
||||
})
|
||||
|
||||
@@ -123,4 +120,3 @@ test('uses the canonical default profile scope when a registry SSH route has no
|
||||
'conn:source-b::default'
|
||||
)
|
||||
})
|
||||
|
||||
|
||||
@@ -13,10 +13,8 @@ export function normalizeWindowConnectionRoute(value: unknown): WindowConnection
|
||||
|
||||
const input = value as Record<string, unknown>
|
||||
const connectionId = typeof input.connectionId === 'string' ? input.connectionId.trim() : ''
|
||||
const profile =
|
||||
typeof input.profile === 'string' && input.profile.trim()
|
||||
? input.profile.trim()
|
||||
: undefined
|
||||
|
||||
const profile = typeof input.profile === 'string' && input.profile.trim() ? input.profile.trim() : undefined
|
||||
|
||||
return {
|
||||
connectionId: connectionId || null,
|
||||
@@ -50,10 +48,12 @@ export class WindowConnectionRouteRegistry {
|
||||
|
||||
if (!route) {
|
||||
this.routes.delete(webContentsId)
|
||||
|
||||
return null
|
||||
}
|
||||
|
||||
this.routes.set(webContentsId, route)
|
||||
|
||||
return route
|
||||
}
|
||||
|
||||
|
||||
@@ -580,8 +580,7 @@ export function useGatewayBoot({
|
||||
bootCompleted = true
|
||||
} catch (err) {
|
||||
const mayPublishFailure =
|
||||
!cancelled &&
|
||||
(switchToken === null ? !$gatewaySwitching.get() : isCurrentGatewaySwitch(switchToken))
|
||||
!cancelled && (switchToken === null ? !$gatewaySwitching.get() : isCurrentGatewaySwitch(switchToken))
|
||||
|
||||
if (mayPublishFailure) {
|
||||
const message = err instanceof Error ? err.message : String(err)
|
||||
|
||||
@@ -390,13 +390,13 @@ describe('useRouteResume', () => {
|
||||
expect(resumeSession).toHaveBeenCalledWith('session-2', true)
|
||||
})
|
||||
|
||||
it("does not re-resume the old session when the new profile gateway opens before /new commits (#68594)", () => {
|
||||
it('does not re-resume the old session when the new profile gateway opens before /new commits (#68594)', () => {
|
||||
const resumeSession = vi.fn(async () => undefined)
|
||||
const startFreshSessionDraft = vi.fn()
|
||||
const activeSessionIdRef: MutableRefObject<null | string> = { current: "runtime-a" }
|
||||
const activeSessionIdRef: MutableRefObject<null | string> = { current: 'runtime-a' }
|
||||
const creatingSessionRef = { current: false }
|
||||
const runtimeIdByStoredSessionIdRef = { current: new Map([["session-a", "runtime-a"]]) }
|
||||
const selectedStoredSessionIdRef: MutableRefObject<null | string> = { current: "session-a" }
|
||||
const runtimeIdByStoredSessionIdRef = { current: new Map([['session-a', 'runtime-a']]) }
|
||||
const selectedStoredSessionIdRef: MutableRefObject<null | string> = { current: 'session-a' }
|
||||
|
||||
const { rerender } = render(
|
||||
<RouteResumeHarness
|
||||
@@ -674,4 +674,3 @@ describe('useRouteResume bounded auto-retry after a failed resume', () => {
|
||||
expect($resumeExhaustedSessionId.get()).toBeNull()
|
||||
})
|
||||
})
|
||||
|
||||
|
||||
@@ -23,7 +23,13 @@ import { clearSessionDraft, stashSessionDraft, takeSessionDraft } from '@/store/
|
||||
import { requestGatewayForAgent, requestGatewayForProfile } from '@/store/gateway'
|
||||
import { $pinnedSessionIds } from '@/store/layout'
|
||||
import { $activeGatewayProfile, $newChatProfile, $newChatRoute, $profiles, ensureGatewayProfile } from '@/store/profile'
|
||||
import { $projectScope, $projectTree, $removedSessionIds, $sessionMutationsInFlight, ALL_PROJECTS } from '@/store/projects'
|
||||
import {
|
||||
$projectScope,
|
||||
$projectTree,
|
||||
$removedSessionIds,
|
||||
$sessionMutationsInFlight,
|
||||
ALL_PROJECTS
|
||||
} from '@/store/projects'
|
||||
import {
|
||||
$activeSessionId,
|
||||
$activeSessionStoredIdRotation,
|
||||
|
||||
@@ -817,7 +817,8 @@ export function useSessionActions({
|
||||
// gateway can be untagged, so retain the captured ambient connection too.
|
||||
// Either way, route by the composite (connection, profile), never by a
|
||||
// same-named profile alone.
|
||||
const sessionOwner: SessionOwnerScope = ownerRoute ||
|
||||
const sessionOwner: SessionOwnerScope =
|
||||
ownerRoute ||
|
||||
(resolvedConnectionId
|
||||
? {
|
||||
connectionId: resolvedConnectionId,
|
||||
@@ -2094,6 +2095,7 @@ export function useSessionActions({
|
||||
const archived = listed?.session
|
||||
const stampedProfile = archived?.profile?.trim()
|
||||
const profile = stampedProfile || (await resolveSessionProfile(storedSessionId))
|
||||
|
||||
if (
|
||||
listed &&
|
||||
!stampedProfile &&
|
||||
@@ -2101,8 +2103,10 @@ export function useSessionActions({
|
||||
$profiles.get().filter(item => item.name.trim()).length > 1
|
||||
) {
|
||||
notifyError(new Error('Session ownership could not be resolved'), copy.archiveFailed)
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
const wasSelected = selectedStoredSessionId === storedSessionId
|
||||
const previousPinned = $pinnedSessionIds.get()
|
||||
// Pins are keyed on the durable lineage-root id; the stored id may be the
|
||||
|
||||
@@ -224,128 +224,131 @@ export function useSessionListActions({ profileScope }: UseSessionListActionsArg
|
||||
}, [profileScope])
|
||||
|
||||
/** Refresh every sidebar session slice without committing an obsolete profile response. */
|
||||
const refreshSessions = useCallback(async (shouldPublish: () => boolean = () => true) => {
|
||||
const sessionProfile = sidebarProfileForScope(profileScope)
|
||||
const activationEpoch = gatewayActivationEpoch()
|
||||
const refreshSessions = useCallback(
|
||||
async (shouldPublish: () => boolean = () => true) => {
|
||||
const sessionProfile = sidebarProfileForScope(profileScope)
|
||||
const activationEpoch = gatewayActivationEpoch()
|
||||
|
||||
if (!shouldPublish() || sidebarProfileForScope(profileScopeRef.current) !== sessionProfile) {
|
||||
return
|
||||
}
|
||||
|
||||
const requestId = refreshSessionsRequestRef.current + 1
|
||||
refreshSessionsRequestRef.current = requestId
|
||||
// The loading flag exists to drive the initial skeletons (they only render
|
||||
// while the list is empty). Turn-complete / reconnect refreshes over a
|
||||
// populated list used to flip it true→false anyway, churning every
|
||||
// $sessionsLoading subscriber twice per turn for no visible change.
|
||||
const showLoading = $sessions.get().length === 0
|
||||
|
||||
if (showLoading && shouldPublish()) {
|
||||
setSessionsLoading(true)
|
||||
}
|
||||
|
||||
try {
|
||||
const limit = $sessionsLimit.get()
|
||||
|
||||
// Require at least one message so abandoned/empty "Untitled" drafts (one
|
||||
// was created per TUI/desktop launch before the lazy-create fix) don't
|
||||
// clutter the sidebar.
|
||||
// Unified cross-profile list (served read-only off each profile's
|
||||
// state.db; no per-profile backend is spawned). Single-profile users get
|
||||
// the same rows tagged profile="default".
|
||||
// Scope every sidebar slice to the active profile (not always 'all') so a profile
|
||||
// with few recent sessions isn't windowed out of the cross-profile
|
||||
// recency page and never inherits another profile's cron or messaging
|
||||
// sections. ALL_PROFILES remains the explicit unified view.
|
||||
// Batched: one request opens each profile DB once and returns all three
|
||||
// source-scoped slices, instead of three separate listAllProfileSessions
|
||||
// calls that each reopened + re-counted every profile DB per refresh.
|
||||
const result = await listSidebarSessions({
|
||||
recentsProfile: sessionProfile,
|
||||
recentsLimit: limit,
|
||||
recentsExclude: SIDEBAR_EXCLUDED_SOURCES,
|
||||
cronLimit: CRON_SECTION_LIMIT,
|
||||
messagingLimit: MESSAGING_SECTION_LIMIT,
|
||||
messagingExclude: MESSAGING_EXCLUDED_SOURCES
|
||||
})
|
||||
|
||||
if (
|
||||
shouldPublish() &&
|
||||
refreshSessionsRequestRef.current === requestId &&
|
||||
sidebarProfileForScope(profileScopeRef.current) === sessionProfile &&
|
||||
gatewayActivationEpoch() === activationEpoch
|
||||
) {
|
||||
const recents = result.recents
|
||||
|
||||
// Drop rows the user just deleted/archived: a refresh can race an
|
||||
// in-flight mutation and the backend page still carries the doomed row.
|
||||
// Honoring the optimistic tombstone keeps the removal from flashing back
|
||||
// (the tombstone self-clears once projects.tree confirms the delete).
|
||||
const incoming = dropTombstoned(recents.sessions)
|
||||
|
||||
// Signature-gate the swap (same pattern as cron/messaging): a refresh
|
||||
// that returns content-identical rows must keep the previous array
|
||||
// identity, or every sidebar memo keyed on $sessions recomputes and the
|
||||
// whole list re-renders once per turn/broadcast for nothing.
|
||||
setSessions(prev => {
|
||||
const next = mergeSessionPage(prev, incoming, sessionsToKeep())
|
||||
|
||||
return sameCronSignature(prev, next) ? prev : next
|
||||
})
|
||||
// "Is there another page?" instead of an exact total: the backend
|
||||
// reports which profiles filled their window, which costs nothing on
|
||||
// top of the rows it already read (the old exact totals ran a COUNT(*)
|
||||
// per profile DB on every refresh). Reference-stable when unchanged so
|
||||
// the sidebar's group memos don't recompute per refresh.
|
||||
setSessionProfilesTruncated(prev => {
|
||||
const next = recents.profiles_truncated ?? {}
|
||||
const prevKeys = Object.keys(prev)
|
||||
|
||||
return prevKeys.length === Object.keys(next).length && prevKeys.every(key => prev[key] === next[key])
|
||||
? prev
|
||||
: next
|
||||
})
|
||||
// Same identity gate: these totals only move when a session bills, and
|
||||
// a fresh object every refresh would repaint every profile header.
|
||||
setSessionProfilesUsage(prev => {
|
||||
const next = recents.profiles_usage ?? {}
|
||||
const prevKeys = Object.keys(prev)
|
||||
|
||||
return prevKeys.length === Object.keys(next).length &&
|
||||
prevKeys.every(
|
||||
key => prev[key]?.tokens === next[key]?.tokens && prev[key]?.cost_usd === next[key]?.cost_usd
|
||||
)
|
||||
? prev
|
||||
: next
|
||||
})
|
||||
|
||||
// Cron section: latest N cron sessions (kept so a pinned cron run still
|
||||
// resolves via sessionByAnyId), signature-gated like above.
|
||||
setCronSessions(prev => (sameCronSignature(prev, result.cron.sessions) ? prev : result.cron.sessions))
|
||||
|
||||
// Messaging sections: drop any non-messaging source the broad exclude
|
||||
// didn't catch (custom sources stay in local recents), then split per
|
||||
// platform in the UI.
|
||||
const messagingRows = dropTombstoned(result.messaging.sessions.filter(s => isMessagingSource(s.source)))
|
||||
|
||||
setMessagingSessions(prev => (sameCronSignature(prev, messagingRows) ? prev : messagingRows))
|
||||
// Hit the cap → at least one platform may have more on disk than loaded.
|
||||
setMessagingTruncated(result.messaging.sessions.length >= MESSAGING_SECTION_LIMIT)
|
||||
if (!shouldPublish() || sidebarProfileForScope(profileScopeRef.current) !== sessionProfile) {
|
||||
return
|
||||
}
|
||||
} finally {
|
||||
// Request identity preserves the zero-argument refresh contract across a
|
||||
// failed activation epoch; an explicit owner predicate is stronger and
|
||||
// must never release a newer switch's loading barrier.
|
||||
if (showLoading && shouldPublish() && refreshSessionsRequestRef.current === requestId) {
|
||||
setSessionsLoading(false)
|
||||
}
|
||||
}
|
||||
|
||||
// Cron *jobs* are a distinct API (getCronJobs), not a session slice.
|
||||
if (shouldPublish() && sidebarProfileForScope(profileScopeRef.current) === sessionProfile) {
|
||||
void refreshCronJobs()
|
||||
}
|
||||
}, [profileScope, refreshCronJobs])
|
||||
const requestId = refreshSessionsRequestRef.current + 1
|
||||
refreshSessionsRequestRef.current = requestId
|
||||
// The loading flag exists to drive the initial skeletons (they only render
|
||||
// while the list is empty). Turn-complete / reconnect refreshes over a
|
||||
// populated list used to flip it true→false anyway, churning every
|
||||
// $sessionsLoading subscriber twice per turn for no visible change.
|
||||
const showLoading = $sessions.get().length === 0
|
||||
|
||||
if (showLoading && shouldPublish()) {
|
||||
setSessionsLoading(true)
|
||||
}
|
||||
|
||||
try {
|
||||
const limit = $sessionsLimit.get()
|
||||
|
||||
// Require at least one message so abandoned/empty "Untitled" drafts (one
|
||||
// was created per TUI/desktop launch before the lazy-create fix) don't
|
||||
// clutter the sidebar.
|
||||
// Unified cross-profile list (served read-only off each profile's
|
||||
// state.db; no per-profile backend is spawned). Single-profile users get
|
||||
// the same rows tagged profile="default".
|
||||
// Scope every sidebar slice to the active profile (not always 'all') so a profile
|
||||
// with few recent sessions isn't windowed out of the cross-profile
|
||||
// recency page and never inherits another profile's cron or messaging
|
||||
// sections. ALL_PROFILES remains the explicit unified view.
|
||||
// Batched: one request opens each profile DB once and returns all three
|
||||
// source-scoped slices, instead of three separate listAllProfileSessions
|
||||
// calls that each reopened + re-counted every profile DB per refresh.
|
||||
const result = await listSidebarSessions({
|
||||
recentsProfile: sessionProfile,
|
||||
recentsLimit: limit,
|
||||
recentsExclude: SIDEBAR_EXCLUDED_SOURCES,
|
||||
cronLimit: CRON_SECTION_LIMIT,
|
||||
messagingLimit: MESSAGING_SECTION_LIMIT,
|
||||
messagingExclude: MESSAGING_EXCLUDED_SOURCES
|
||||
})
|
||||
|
||||
if (
|
||||
shouldPublish() &&
|
||||
refreshSessionsRequestRef.current === requestId &&
|
||||
sidebarProfileForScope(profileScopeRef.current) === sessionProfile &&
|
||||
gatewayActivationEpoch() === activationEpoch
|
||||
) {
|
||||
const recents = result.recents
|
||||
|
||||
// Drop rows the user just deleted/archived: a refresh can race an
|
||||
// in-flight mutation and the backend page still carries the doomed row.
|
||||
// Honoring the optimistic tombstone keeps the removal from flashing back
|
||||
// (the tombstone self-clears once projects.tree confirms the delete).
|
||||
const incoming = dropTombstoned(recents.sessions)
|
||||
|
||||
// Signature-gate the swap (same pattern as cron/messaging): a refresh
|
||||
// that returns content-identical rows must keep the previous array
|
||||
// identity, or every sidebar memo keyed on $sessions recomputes and the
|
||||
// whole list re-renders once per turn/broadcast for nothing.
|
||||
setSessions(prev => {
|
||||
const next = mergeSessionPage(prev, incoming, sessionsToKeep())
|
||||
|
||||
return sameCronSignature(prev, next) ? prev : next
|
||||
})
|
||||
// "Is there another page?" instead of an exact total: the backend
|
||||
// reports which profiles filled their window, which costs nothing on
|
||||
// top of the rows it already read (the old exact totals ran a COUNT(*)
|
||||
// per profile DB on every refresh). Reference-stable when unchanged so
|
||||
// the sidebar's group memos don't recompute per refresh.
|
||||
setSessionProfilesTruncated(prev => {
|
||||
const next = recents.profiles_truncated ?? {}
|
||||
const prevKeys = Object.keys(prev)
|
||||
|
||||
return prevKeys.length === Object.keys(next).length && prevKeys.every(key => prev[key] === next[key])
|
||||
? prev
|
||||
: next
|
||||
})
|
||||
// Same identity gate: these totals only move when a session bills, and
|
||||
// a fresh object every refresh would repaint every profile header.
|
||||
setSessionProfilesUsage(prev => {
|
||||
const next = recents.profiles_usage ?? {}
|
||||
const prevKeys = Object.keys(prev)
|
||||
|
||||
return prevKeys.length === Object.keys(next).length &&
|
||||
prevKeys.every(
|
||||
key => prev[key]?.tokens === next[key]?.tokens && prev[key]?.cost_usd === next[key]?.cost_usd
|
||||
)
|
||||
? prev
|
||||
: next
|
||||
})
|
||||
|
||||
// Cron section: latest N cron sessions (kept so a pinned cron run still
|
||||
// resolves via sessionByAnyId), signature-gated like above.
|
||||
setCronSessions(prev => (sameCronSignature(prev, result.cron.sessions) ? prev : result.cron.sessions))
|
||||
|
||||
// Messaging sections: drop any non-messaging source the broad exclude
|
||||
// didn't catch (custom sources stay in local recents), then split per
|
||||
// platform in the UI.
|
||||
const messagingRows = dropTombstoned(result.messaging.sessions.filter(s => isMessagingSource(s.source)))
|
||||
|
||||
setMessagingSessions(prev => (sameCronSignature(prev, messagingRows) ? prev : messagingRows))
|
||||
// Hit the cap → at least one platform may have more on disk than loaded.
|
||||
setMessagingTruncated(result.messaging.sessions.length >= MESSAGING_SECTION_LIMIT)
|
||||
}
|
||||
} finally {
|
||||
// Request identity preserves the zero-argument refresh contract across a
|
||||
// failed activation epoch; an explicit owner predicate is stronger and
|
||||
// must never release a newer switch's loading barrier.
|
||||
if (showLoading && shouldPublish() && refreshSessionsRequestRef.current === requestId) {
|
||||
setSessionsLoading(false)
|
||||
}
|
||||
}
|
||||
|
||||
// Cron *jobs* are a distinct API (getCronJobs), not a session slice.
|
||||
if (shouldPublish() && sidebarProfileForScope(profileScopeRef.current) === sessionProfile) {
|
||||
void refreshCronJobs()
|
||||
}
|
||||
},
|
||||
[profileScope, refreshCronJobs]
|
||||
)
|
||||
|
||||
const loadMoreSessions = useCallback(async () => {
|
||||
bumpSessionsLimit()
|
||||
|
||||
Vendored
+7
-5
@@ -415,11 +415,13 @@ declare global {
|
||||
write: (id: string, data: string) => Promise<boolean>
|
||||
}
|
||||
reachPreviewUrl?: (url: string) => Promise<string>
|
||||
setActiveConnectionRoute?: (route: {
|
||||
connectionId?: null | string
|
||||
profile?: string
|
||||
registryScoped?: boolean
|
||||
} | null) => void
|
||||
setActiveConnectionRoute?: (
|
||||
route: {
|
||||
connectionId?: null | string
|
||||
profile?: string
|
||||
registryScoped?: boolean
|
||||
} | null
|
||||
) => void
|
||||
onClosePreviewRequested?: (callback: () => void) => () => void
|
||||
onPreviewNav?: (callback: (command: 'back' | 'forward' | 'reload') => void) => () => void
|
||||
onOpenFolderRequested?: (callback: () => void) => () => void
|
||||
|
||||
@@ -625,9 +625,8 @@ export const host = {
|
||||
// the deleted profile.
|
||||
const ambientConnectionId = route ? null : String(activeGatewayConnectionId() || '').trim()
|
||||
|
||||
const ambientRemoteConnectionId = ambientConnectionId && ambientConnectionId !== 'local'
|
||||
? ambientConnectionId
|
||||
: null
|
||||
const ambientRemoteConnectionId =
|
||||
ambientConnectionId && ambientConnectionId !== 'local' ? ambientConnectionId : null
|
||||
|
||||
if (!name) {
|
||||
throw new Error('deleteProfile: profile name required')
|
||||
|
||||
@@ -224,7 +224,12 @@ export function setPrimaryGatewayConnection(connection: Pick<HermesConnection, '
|
||||
function isPrimaryRegistryRoute(connectionId: null | string, profile: string): boolean {
|
||||
const id = String(connectionId ?? '').trim()
|
||||
|
||||
return normKey(profile) === g.primaryProfile && Boolean(id) && Boolean(g.primaryConnectionId) && id === g.primaryConnectionId
|
||||
return (
|
||||
normKey(profile) === g.primaryProfile &&
|
||||
Boolean(id) &&
|
||||
Boolean(g.primaryConnectionId) &&
|
||||
id === g.primaryConnectionId
|
||||
)
|
||||
}
|
||||
|
||||
export function isActivePrimary(): boolean {
|
||||
|
||||
@@ -23,7 +23,8 @@ let promptAckStatus: null | string = null
|
||||
vi.mock('@/hermes', () => ({
|
||||
HermesGateway: class {
|
||||
connectionState = 'closed'
|
||||
eventHandler: ((event: { payload?: Record<string, unknown>; session_id?: string; type: string }) => void) | null = null
|
||||
eventHandler: ((event: { payload?: Record<string, unknown>; session_id?: string; type: string }) => void) | null =
|
||||
null
|
||||
stateHandler: ((state: string) => void) | null = null
|
||||
connect = vi.fn(async () => {
|
||||
this.connectionState = 'open'
|
||||
@@ -36,15 +37,18 @@ vi.mock('@/hermes', () => ({
|
||||
return method === 'prompt.submit' && promptAckStatus ? { status: promptAckStatus } : { method, params }
|
||||
})
|
||||
close = vi.fn()
|
||||
emit = (event: { payload?: Record<string, unknown>; session_id?: string; type: string }) => this.eventHandler?.(event)
|
||||
emit = (event: { payload?: Record<string, unknown>; session_id?: string; type: string }) =>
|
||||
this.eventHandler?.(event)
|
||||
emitState = (state: string) => this.stateHandler?.(state)
|
||||
onEvent = vi.fn((handler: (event: { payload?: Record<string, unknown>; session_id?: string; type: string }) => void) => {
|
||||
this.eventHandler = handler
|
||||
onEvent = vi.fn(
|
||||
(handler: (event: { payload?: Record<string, unknown>; session_id?: string; type: string }) => void) => {
|
||||
this.eventHandler = handler
|
||||
|
||||
return () => {
|
||||
this.eventHandler = null
|
||||
return () => {
|
||||
this.eventHandler = null
|
||||
}
|
||||
}
|
||||
})
|
||||
)
|
||||
onState = vi.fn((handler: (state: string) => void) => {
|
||||
this.stateHandler = handler
|
||||
|
||||
@@ -338,12 +342,10 @@ describe('requestForSessionProfile', () => {
|
||||
installDesktop()
|
||||
const ambient = vi.fn(async () => ({ ambient: true }))
|
||||
|
||||
await requestForSessionProfile(
|
||||
{ connectionId: 'local', profile: 'default' },
|
||||
ambient as never,
|
||||
'prompt.submit',
|
||||
{ session_id: 'rt-bot-chat', text: 'research this' }
|
||||
)
|
||||
await requestForSessionProfile({ connectionId: 'local', profile: 'default' }, ambient as never, 'prompt.submit', {
|
||||
session_id: 'rt-bot-chat',
|
||||
text: 'research this'
|
||||
})
|
||||
|
||||
// prompt.submit ACKs immediately while the model keeps running. Releasing
|
||||
// the request-scoped socket here detaches the runtime session; the backend
|
||||
@@ -375,23 +377,24 @@ describe('requestForSessionProfile', () => {
|
||||
vi.useRealTimers()
|
||||
})
|
||||
|
||||
it.each(['queued', 'redirected', 'future-nonterminal'])('retains a routed socket for non-terminal ACK status %s', async status => {
|
||||
const primary = makePrimary()
|
||||
setPrimaryGateway(primary as never, 'default')
|
||||
installDesktop()
|
||||
const ambient = vi.fn(async () => ({ ambient: true }))
|
||||
it.each(['queued', 'redirected', 'future-nonterminal'])(
|
||||
'retains a routed socket for non-terminal ACK status %s',
|
||||
async status => {
|
||||
const primary = makePrimary()
|
||||
setPrimaryGateway(primary as never, 'default')
|
||||
installDesktop()
|
||||
const ambient = vi.fn(async () => ({ ambient: true }))
|
||||
|
||||
promptAckStatus = status
|
||||
promptAckStatus = status
|
||||
|
||||
await requestForSessionProfile(
|
||||
'loki',
|
||||
ambient as never,
|
||||
'prompt.submit',
|
||||
{ session_id: `rt-${status}`, text: 'continue' }
|
||||
)
|
||||
await requestForSessionProfile('loki', ambient as never, 'prompt.submit', {
|
||||
session_id: `rt-${status}`,
|
||||
text: 'continue'
|
||||
})
|
||||
|
||||
expect(secondaryGateways[0].close).not.toHaveBeenCalled()
|
||||
})
|
||||
expect(secondaryGateways[0].close).not.toHaveBeenCalled()
|
||||
}
|
||||
)
|
||||
|
||||
it.each(['complete', 'completed', 'error'])('releases a routed socket for terminal ACK status %s', async status => {
|
||||
const primary = makePrimary()
|
||||
@@ -401,12 +404,10 @@ describe('requestForSessionProfile', () => {
|
||||
|
||||
promptAckStatus = status
|
||||
|
||||
await requestForSessionProfile(
|
||||
'loki',
|
||||
ambient as never,
|
||||
'prompt.submit',
|
||||
{ session_id: `rt-${status}`, text: 'finish' }
|
||||
)
|
||||
await requestForSessionProfile('loki', ambient as never, 'prompt.submit', {
|
||||
session_id: `rt-${status}`,
|
||||
text: 'finish'
|
||||
})
|
||||
|
||||
expect(secondaryGateways[0].close).toHaveBeenCalledOnce()
|
||||
})
|
||||
|
||||
@@ -228,9 +228,7 @@ export type NewChatWorkspaceTarget = null | string | undefined
|
||||
|
||||
export const getConfiguredDefaultProjectDir = (): string => configuredDefaultProjectDir
|
||||
|
||||
export async function syncConfiguredDefaultProjectDir(
|
||||
shouldPublish: () => boolean = () => true
|
||||
): Promise<string> {
|
||||
export async function syncConfiguredDefaultProjectDir(shouldPublish: () => boolean = () => true): Promise<string> {
|
||||
const settings = window.hermesDesktop?.settings?.getDefaultProjectDir
|
||||
|
||||
if (!settings) {
|
||||
|
||||
Reference in New Issue
Block a user