diff --git a/src/renderer/src/components/settings/SessionHistoryServerRow.tsx b/src/renderer/src/components/settings/SessionHistoryServerRow.tsx index daf93e720d2..9cd1c791b5d 100644 --- a/src/renderer/src/components/settings/SessionHistoryServerRow.tsx +++ b/src/renderer/src/components/settings/SessionHistoryServerRow.tsx @@ -25,8 +25,7 @@ export function SessionHistoryServerRow({ details, refresh = 0, onError, - onStateChange, - onUserToggle + onStateChange }: { environment: PublicKnownRuntimeEnvironment details: RuntimeHostDetails | undefined @@ -35,7 +34,6 @@ export function SessionHistoryServerRow({ onError: (message: string | null) => void /** Lets the pane count and order computers it does not itself poll. */ onStateChange?: (environmentId: string, state: SessionSearchComputerState) => void - onUserToggle?: (environmentId: string, enabled: boolean) => void }): React.JSX.Element { const hostId = toRuntimeExecutionHostId(environment.id) const mounted = useMountedRef() @@ -93,7 +91,6 @@ export function SessionHistoryServerRow({ } function toggle(): Promise { - onUserToggle?.(environment.id, !enabled) return setEnabled(!enabled) } diff --git a/src/renderer/src/components/settings/SessionHistorySettingsPane.test.tsx b/src/renderer/src/components/settings/SessionHistorySettingsPane.test.tsx index 270b651e24d..909b2839b11 100644 --- a/src/renderer/src/components/settings/SessionHistorySettingsPane.test.tsx +++ b/src/renderer/src/components/settings/SessionHistorySettingsPane.test.tsx @@ -59,16 +59,14 @@ function pane( enabled = false, confirm = vi.fn().mockResolvedValue(true), save = vi.fn().mockResolvedValue(undefined), - historyDays: number | null = null, - autoEnableNewComputers = false + historyDays: number | null = null ) { return render( @@ -110,7 +108,7 @@ function statusByHost(): void { return answer }) } -const summaryLine = (): string => screen.getByText(/computers/).textContent ?? '' +const enableAllButton = () => screen.queryByRole('button', { name: 'Enable on all computers' }) async function openAdvanced(): Promise { await act(async () => { fireEvent.click(screen.getByRole('button', { name: /Advanced/ })) @@ -359,7 +357,7 @@ it('offers only this computer to a paired client, with no server rows', async () expect(screen.getAllByRole('switch')).toHaveLength(1) expect(screen.getByRole('switch')).toBeDisabled() expect(screen.queryByRole('status')).not.toBeInTheDocument() - expect(screen.queryByRole('button', { name: 'Turn on all' })).not.toBeInTheDocument() + expect(enableAllButton()).not.toBeInTheDocument() expect(screen.queryByRole('button', { name: 'Open' })).not.toBeInTheDocument() expect(mocks.status).not.toHaveBeenCalled() }) @@ -391,57 +389,65 @@ it('leaves a lone computer to its own switch, with no roll-up above it', async ( pane(true) await act(async () => {}) expect(screen.getAllByRole('switch')).toHaveLength(1) - expect(screen.queryByText(/of 1 computers/)).not.toBeInTheDocument() - expect(screen.queryByRole('button', { name: 'Turn on all' })).not.toBeInTheDocument() + expect(enableAllButton()).not.toBeInTheDocument() expect(screen.queryByText('This computer')).not.toBeInTheDocument() expect(screen.queryByText('Orca remote servers')).not.toBeInTheDocument() }) -it('counts every computer in one line, leaving out the segments worth zero', async () => { +it('offers the button only while a paired server is reachable and off', async () => { mixedFleet() pane(true) await act(async () => {}) - expect(summaryLine()).toBe('On 2 of 5 computers · 1 offline · 1 need an update') - mocks.environments = [{ id: 'off', name: 'gpu-a' }] - mocks.details = { off: CONNECTED_DETAILS } - mocks.statusByHost = { local: current, 'runtime:off': off } + expect(enableAllButton()).toBeInTheDocument() + + // gpu-a was the only eligible one; with it on, the offline and too-old rows leave nothing to do. + mocks.statusByHost = { ...mocks.statusByHost, 'runtime:off': current } statusByHost() cleanup() pane(true) await act(async () => {}) - expect(summaryLine()).toBe('On 1 of 2 computers') + expect(enableAllButton()).not.toBeInTheDocument() }) -it('turns on every reachable computer and skips the ones it cannot', async () => { +it('does not offer the button for a server whose state is still unknown', async () => { + mocks.environments = [{ id: 'a', name: 'gpu-a' }] + mocks.details = {} + mocks.statusByHost = { local: current } + statusByHost() + pane(true) + await act(async () => {}) + expect(enableAllButton()).not.toBeInTheDocument() +}) + +it('enables every reachable server and skips the ones it cannot', async () => { mixedFleet() const confirm = vi.fn().mockResolvedValue(true) - const save = vi.fn().mockResolvedValue(undefined) - pane(true, confirm, save) + pane(true, confirm) await act(async () => {}) await act(async () => { - fireEvent.click(screen.getByRole('button', { name: 'Turn on all' })) + fireEvent.click(screen.getByRole('button', { name: 'Enable on all computers' })) }) expect(confirm).not.toHaveBeenCalled() + // linux 1 is offline and m4 air is too old, so neither is asked; build-01 is already on. expect(mocks.setEnabled.mock.calls.map((call) => call[0])).toEqual(['runtime:off']) - expect(save).toHaveBeenCalledWith({ aiVaultSearchAutoEnableNewComputers: true }) }) -it('turns this computer on as part of turning them all on', async () => { +it('enables this computer as part of enabling them all', async () => { mocks.environments = [{ id: 'off', name: 'gpu-a' }] mocks.details = { off: CONNECTED_DETAILS } mocks.statusByHost = { local: off, 'runtime:off': off } statusByHost() const save = vi.fn().mockResolvedValue(undefined) - pane(false, vi.fn().mockResolvedValue(true), save) + pane(false, undefined, save) await act(async () => {}) await act(async () => { - fireEvent.click(screen.getByRole('button', { name: 'Turn on all' })) + fireEvent.click(screen.getByRole('button', { name: 'Enable on all computers' })) }) expect(save).toHaveBeenCalledWith({ aiVaultSearch: { enabled: true, historyDays: null } }) expect(mocks.setEnabled).toHaveBeenCalledWith('runtime:off', true) }) -it('keeps going after a host refuses, and withholds the standing consent', async () => { +it('keeps going after a host refuses, and names the one that did', async () => { mocks.environments = [ { id: 'a', name: 'gpu-a' }, { id: 'b', name: 'gpu-b' } @@ -450,53 +456,31 @@ it('keeps going after a host refuses, and withholds the standing consent', async mocks.statusByHost = { local: current, 'runtime:a': off, 'runtime:b': off } statusByHost() mocks.setEnabled.mockRejectedValueOnce(new Error('relay down')).mockResolvedValue(current) - const save = vi.fn().mockResolvedValue(undefined) - pane(true, vi.fn().mockResolvedValue(true), save) + pane(true) await act(async () => {}) await act(async () => { - fireEvent.click(screen.getByRole('button', { name: 'Turn on all' })) + fireEvent.click(screen.getByRole('button', { name: 'Enable on all computers' })) }) expect(mocks.setEnabled.mock.calls.map((call) => call[0])).toEqual(['runtime:a', 'runtime:b']) expect(screen.getByRole('alert')).toHaveTextContent('Could not change session search on gpu-a') - expect(save).not.toHaveBeenCalledWith({ aiVaultSearchAutoEnableNewComputers: true }) }) -it('hides the button and says so once nothing is left to turn on', async () => { - mocks.environments = [ - { id: 'a', name: 'gpu-a' }, - { id: 'gone', name: 'linux 1' } - ] - mocks.details = { a: CONNECTED_DETAILS, gone: OFFLINE_DETAILS } - mocks.statusByHost = { local: current, 'runtime:a': current } - statusByHost() - pane(true, undefined, undefined, null, true) - await act(async () => {}) - expect(screen.queryByRole('button', { name: 'Turn on all' })).not.toBeInTheDocument() - expect(summaryLine()).toBe('On 2 of 3 computers · 1 offline New computers turn on when they can.') -}) - -it('turns on a newly reachable server while the standing consent holds', async () => { - mocks.environments = [{ id: 'a', name: 'gpu-a' }] - mocks.details = { a: CONNECTED_DETAILS } - mocks.statusByHost = { local: current, 'runtime:a': off } - statusByHost() - pane(true, undefined, undefined, null, true) - await act(async () => {}) - expect(mocks.setEnabled).toHaveBeenCalledWith('runtime:a', true) -}) - -it('drops the standing consent when a server is turned off by hand', async () => { +it('remembers nothing after a server is turned back off by hand', async () => { mocks.environments = [{ id: 'a', name: 'gpu-a' }] mocks.details = { a: CONNECTED_DETAILS } mocks.statusByHost = { local: current, 'runtime:a': current } statusByHost() const save = vi.fn().mockResolvedValue(undefined) - pane(true, vi.fn().mockResolvedValue(true), save, null, true) + pane(true, undefined, save) await act(async () => {}) + expect(enableAllButton()).not.toBeInTheDocument() + mocks.setEnabled.mockResolvedValue(off) await act(async () => { fireEvent.click(screen.getByRole('switch', { name: 'Search sessions on gpu-a' })) }) - expect(save).toHaveBeenCalledWith({ aiVaultSearchAutoEnableNewComputers: false }) + // The row went off, so the offer comes straight back; no preference was written either way. + expect(enableAllButton()).toBeInTheDocument() + expect(save).not.toHaveBeenCalled() }) it('folds the list past six computers and orders it by what the user can act on', async () => { diff --git a/src/renderer/src/components/settings/SessionHistorySettingsPane.tsx b/src/renderer/src/components/settings/SessionHistorySettingsPane.tsx index b2ec893f0a0..cee4abc22c0 100644 --- a/src/renderer/src/components/settings/SessionHistorySettingsPane.tsx +++ b/src/renderer/src/components/settings/SessionHistorySettingsPane.tsx @@ -22,8 +22,6 @@ import { SessionSearchComputerList } from './SessionSearchComputerList' import { isTurnOnableSessionSearchState, orderSessionSearchServers, - sessionSearchSummarySentence, - summarizeSessionSearchComputers, type SessionSearchComputerEntry, type SessionSearchComputerState } from './session-search-computer-rollup' @@ -33,7 +31,6 @@ import { sessionSearchStatusDetails, sessionSearchStatusMessage } from './session-history-status-copy' -import { useSessionSearchAutoEnable } from './use-session-search-auto-enable' import { useSessionSearchStatus } from './use-session-search-status' import { useRuntimeEnvironmentCatalog } from './use-runtime-environment-catalog' @@ -45,7 +42,6 @@ export function SessionHistorySettingsPane({ updateSettings: (updates: Partial) => Promise }): React.JSX.Element { const policy = resolveAiVaultSearchSettings(settings) - const autoEnableNewComputers = settings.aiVaultSearchAutoEnableNewComputers === true const isWebClient = isWebClientLocation() const closeSettingsPage = useAppStore((state) => state.closeSettingsPage) const showAiVaultSearch = useAppStore((state) => state.showAiVaultSearch) @@ -53,9 +49,6 @@ export function SessionHistorySettingsPane({ const [error, setError] = useState(null) const [refresh, setRefresh] = useState(0) const [serverStates, setServerStates] = useState>({}) - const [userToggledServers, setUserToggledServers] = useState>( - () => new Set() - ) const { environments, detailsByEnvironmentId } = useRuntimeEnvironmentCatalog() const localRead = useSessionSearchStatus({ executionHostId: LOCAL_EXECUTION_HOST_ID, @@ -82,18 +75,11 @@ export function SessionHistorySettingsPane({ state: serverStates[environment.id] ?? 'checking', environment })) - const summary = summarizeSessionSearchComputers([localEntry, ...serverEntries]) const orderedServers = orderSessionSearchServers(serverEntries) - // Rebuilt each render on purpose: the hook keys off the host ids, not this array. - const autoEnableTargets = serverEntries - .filter( - (entry) => isTurnOnableSessionSearchState(entry.state) && !userToggledServers.has(entry.id) - ) - .map((entry) => ({ - id: entry.id, - hostId: toRuntimeExecutionHostId(entry.id), - name: entry.name - })) + // The button's whole reason to exist: a paired server this client could switch on right now. + const enableableServers = serverEntries.filter((entry) => + isTurnOnableSessionSearchState(entry.state) + ) const handleServerState = useCallback( (environmentId: string, state: SessionSearchComputerState) => { @@ -130,49 +116,26 @@ export function SessionHistorySettingsPane({ return save({ enabled: !policy.enabled }) } - /** A hand-off the user made themselves overrides the standing "turn on new computers" consent. */ - function noteServerToggledByHand(environmentId: string, enabled: boolean): void { - setUserToggledServers((current) => { - if (current.has(environmentId)) { - return current - } - const next = new Set(current) - next.add(environmentId) - return next - }) - if (!enabled && autoEnableNewComputers) { - void updateSettings({ aiVaultSearchAutoEnableNewComputers: false }) - } - } - - async function turnOnEveryComputer(): Promise { + /** Remembers nothing: what it acts on is read off the rows at the moment it is clicked. */ + async function enableOnAllComputers(): Promise { setBusy(true) setError(null) - let failed = false try { if (!policy.enabled) { try { await writePolicy({ enabled: true }) } catch { - failed = true setError(saveErrorMessage()) } } // One host at a time: a failure is that host's, and it must not stop the rest. - for (const entry of orderedServers) { - if (!isTurnOnableSessionSearchState(entry.state)) { - continue - } + for (const entry of enableableServers) { try { await window.api.aiVault.setSearchEnabled(toRuntimeExecutionHostId(entry.id), true) } catch { - failed = true setError(serverToggleErrorMessage(entry.name)) } } - if (!failed) { - await updateSettings({ aiVaultSearchAutoEnableNewComputers: true }) - } } finally { if (mounted.current) { setBusy(false) @@ -181,13 +144,6 @@ export function SessionHistorySettingsPane({ } } - useSessionSearchAutoEnable({ - active: autoEnableNewComputers && !isWebClient, - targets: autoEnableTargets, - onError: setError, - onSettled: () => setRefresh((value) => value + 1) - }) - /** False when the settings write failed or the pane went away, so the delete is skipped. */ async function turnSearchOffBeforeDelete(): Promise { try { @@ -230,23 +186,18 @@ export function SessionHistorySettingsPane({ )}

- {/* With no paired server the line and the button only restate the single switch below them. */} - {serverEntries.length === 0 ? null : ( -
-

- {sessionSearchSummarySentence(summary, autoEnableNewComputers)} -

- {summary.turnOnable > 0 ? ( - - ) : null} + {/* Nothing left to switch on means nothing to offer: each row already speaks for itself. */} + {enableableServers.length === 0 ? null : ( +
+
)} ) }))} diff --git a/src/renderer/src/components/settings/session-search-computer-rollup.test.ts b/src/renderer/src/components/settings/session-search-computer-rollup.test.ts index 66f94a1a33b..454be5e31bd 100644 --- a/src/renderer/src/components/settings/session-search-computer-rollup.test.ts +++ b/src/renderer/src/components/settings/session-search-computer-rollup.test.ts @@ -1,17 +1,10 @@ -import { expect, it, vi } from 'vitest' +import { expect, it } from 'vitest' import { isTurnOnableSessionSearchState, orderSessionSearchServers, - sessionSearchSummarySentence, - summarizeSessionSearchComputers, type SessionSearchComputerEntry } from './session-search-computer-rollup' -vi.mock('@/i18n/i18n', () => ({ - translate: (_key: string, fallback: string, args?: Record) => - fallback.replace(/{{(\w+)}}/g, (_, key: string) => String(args?.[key])) -})) - const fleet: SessionSearchComputerEntry[] = [ { id: 'local', name: 'Local Mac', state: 'on' }, { id: 'a', name: 'build-01', state: 'on' }, @@ -22,37 +15,12 @@ const fleet: SessionSearchComputerEntry[] = [ { id: 'f', name: 'probing', state: 'checking' } ] -it('counts what the user can see and what they could act on', () => { - expect(summarizeSessionSearchComputers(fleet)).toEqual({ - on: 2, - total: 7, - offline: 2, - needUpdate: 1, - turnOnable: 1 - }) -}) - it('will not offer to turn on a computer it cannot reach or that is too old', () => { expect(isTurnOnableSessionSearchState('off')).toBe(true) for (const state of ['on', 'offline', 'needs-update', 'checking'] as const) { expect(isTurnOnableSessionSearchState(state)).toBe(false) } -}) - -it('leaves a zero segment out of the sentence rather than printing it', () => { - expect(sessionSearchSummarySentence(summarizeSessionSearchComputers(fleet), false)).toBe( - 'On 2 of 7 computers · 2 offline · 1 need an update' - ) - const onlyLocal = summarizeSessionSearchComputers([fleet[0]]) - expect(sessionSearchSummarySentence(onlyLocal, false)).toBe('On 1 of 1 computers') -}) - -it('promises to keep new computers turned on only when that is the standing consent', () => { - const summary = summarizeSessionSearchComputers([fleet[0]]) - expect(sessionSearchSummarySentence(summary, true)).toBe( - 'On 1 of 1 computers New computers turn on when they can.' - ) - expect(sessionSearchSummarySentence(summary, false)).not.toContain('New computers') + expect(fleet.filter((entry) => isTurnOnableSessionSearchState(entry.state))).toHaveLength(1) }) it('orders reachable and working first, then by name inside each group', () => { diff --git a/src/renderer/src/components/settings/session-search-computer-rollup.ts b/src/renderer/src/components/settings/session-search-computer-rollup.ts index 9a9c693e3f9..2d830f8584b 100644 --- a/src/renderer/src/components/settings/session-search-computer-rollup.ts +++ b/src/renderer/src/components/settings/session-search-computer-rollup.ts @@ -1,5 +1,3 @@ -import { translate } from '@/i18n/i18n' - /** * What one computer in the pane is doing, as far as this client can tell. * @@ -14,64 +12,10 @@ export type SessionSearchComputerEntry = { state: SessionSearchComputerState } -export type SessionSearchFleetSummary = { - on: number - total: number - offline: number - needUpdate: number - /** Reachable, new enough, and still off: exactly what Turn on all would act on. */ - turnOnable: number -} - export function isTurnOnableSessionSearchState(state: SessionSearchComputerState): boolean { return state === 'off' } -export function summarizeSessionSearchComputers( - entries: readonly SessionSearchComputerEntry[] -): SessionSearchFleetSummary { - const count = (state: SessionSearchComputerState): number => - entries.filter((entry) => entry.state === state).length - return { - on: count('on'), - total: entries.length, - offline: count('offline'), - needUpdate: count('needs-update'), - turnOnable: entries.filter((entry) => isTurnOnableSessionSearchState(entry.state)).length - } -} - -/** Sentence above the list. A segment worth zero is left out rather than printed as "0". */ -export function sessionSearchSummarySentence( - summary: SessionSearchFleetSummary, - autoEnableNewComputers: boolean -): string { - const segments = [ - translate('sessionHistory.settings.summaryOn', 'On {{on}} of {{total}} computers', { - on: summary.on, - total: summary.total - }) - ] - if (summary.offline > 0) { - segments.push( - translate('sessionHistory.settings.summaryOffline', '{{offline}} offline', { - offline: summary.offline - }) - ) - } - if (summary.needUpdate > 0) { - segments.push( - translate('sessionHistory.settings.summaryNeedUpdate', '{{needUpdate}} need an update', { - needUpdate: summary.needUpdate - }) - ) - } - const sentence = segments.join(' · ') - return autoEnableNewComputers - ? `${sentence} ${translate('sessionHistory.settings.summaryAutoEnable', 'New computers turn on when they can.')}` - : sentence -} - // Reachable and working first, then what the user could act on, then what they cannot. const STATE_RANK: Record = { on: 0, diff --git a/src/renderer/src/components/settings/use-session-search-auto-enable.ts b/src/renderer/src/components/settings/use-session-search-auto-enable.ts deleted file mode 100644 index 4efdde61db5..00000000000 Binary files a/src/renderer/src/components/settings/use-session-search-auto-enable.ts and /dev/null differ diff --git a/src/renderer/src/i18n/locales/en.json b/src/renderer/src/i18n/locales/en.json index b115ca1a2d5..5968c086497 100644 --- a/src/renderer/src/i18n/locales/en.json +++ b/src/renderer/src/i18n/locales/en.json @@ -17979,16 +17979,12 @@ "clearedAndTurnedOff": "Search turned off and search data cleared.", "thisComputer": "This computer", "remoteServers": "Orca remote servers", - "summaryOn": "On {{on}} of {{total}} computers", - "summaryOffline": "{{offline}} offline", - "summaryNeedUpdate": "{{needUpdate}} need an update", - "summaryAutoEnable": "New computers turn on when they can.", - "turnOnAll": "Turn on all", "showMore": "Show {{count}} more", "showFewer": "Show fewer", "openInSidebar": "Open in the sidebar", "openInSidebarCopy": "Type what you remember, or ask an agent: “find the session where we fixed the login timeout.”", - "open": "Open" + "open": "Open", + "enableOnAll": "Enable on all computers" } }, "aiVault": { diff --git a/src/shared/global-settings-types.ts b/src/shared/global-settings-types.ts index 5d921c9d395..6369033c892 100644 --- a/src/shared/global-settings-types.ts +++ b/src/shared/global-settings-types.ts @@ -491,13 +491,6 @@ export type GlobalSettings = { voice?: VoiceSettings /** Transcript full-text search consent + retention. Absent means off; nothing indexes until the user opts in. */ aiVaultSearch?: AiVaultSearchSettings - /** - * Standing consent from "Turn on all": a paired server that becomes reachable - * and new enough gets session search turned on without another dialog. Kept - * out of `aiVaultSearch` because it changes no indexer configuration, so it - * must never close and reconstruct one. Cleared when a server is turned off by hand. - */ - aiVaultSearchAutoEnableNewComputers?: boolean } export type OrcaWorkspaceLayout = {