mirror of
https://github.com/stablyai/orca.git
synced 2026-11-05 19:47:27 +00:00
* feat(session-search): add ranked history panel search and consent * test: wait for initial session indexing before refreshing results * feat(session-history): add local search settings and index controls * Use shared local host identifier for session index status * feat(session-search): merge all-computers search across hosts The `all` scope on `aiVault:searchSessions` now fans out from the desktop to every host the session list enumerates and merges the pages into one. Legs run in parallel: the local index through the search service, SSH and runtime hosts through the existing remote search client. Two fixed orders, because relevance scores from independent indexes are not comparable. `newest` asks every leg for recency and k-way merges on `updatedAt`, nulls last, ties broken on execution host id. `relevance` rotates hosts in host-id order by their own rank. The merged cursor is an opaque base64url payload holding each host's cursor, how many of its current page were already emitted, and the generation that offset counts into, plus the page size and sort the cursor belongs to. A host whose index moved is fenced to `stale` and stops contributing; the rest keep paging. Per-host outcomes ride back on one new optional `hosts` field on the results response. `aiVault:searchStatus` with `all` stays refused, and neither the runtime RPC nor the CLI gains the scope, so a fan-out is never two hops. * fix(preload): let the search bridge address the all-computers scope * feat(settings): live index status, enable confirm, advanced delete * feat(session-search): search every computer from the history panel The panel's "All computers" scope produced no request: the hook parsed the scope into a single host id and stopped when that was null, so the panel answered "Choose one computer to search its sessions." The desktop already merges every enumerated host behind `aiVault:searchSessions`, so pass the scope straight through and stamp each hit with the host it came back on. Hosts the merge could not search are named under the results header with a short reason, since a silent partial answer reads as "no such session". (cherry picked from commitc6b9179316) * feat(session-search): enable indexing on paired servers from a client Adds `aiVault.setSearchEnabled` so a desktop can turn a paired Orca server's transcript index on or off and have the server apply it without a restart. The runtime method refuses any caller without a `pairedDeviceId` with a `forbidden`-class error, writes the whole resolved policy through the runtime store so retention rides along untouched, then reaches the index through a host-supplied hook: `applySessionSearchSettingsChange` on the desktop, the in-process instance's new `apply` on orcad. The relay is unchanged. Wire compatibility is Rule 1 shaped: a new optional method. A server that predates it answers method-not-found, which the desktop IPC handler maps to an error whose message is exactly `host-too-old`. Old clients never call it. The method is deliberately absent from the mobile allowlist, and `aiVaultSearch` stays out of the paired settings projection. (cherry picked from commit640c715fbd) * fix(session-search): report a paired server without session search as host-too-old on status reads (cherry picked from commit1463e8a4bc) * fix(settings): let Button and Collapsible own their spacing and type
74 lines
3.0 KiB
TypeScript
74 lines
3.0 KiB
TypeScript
import { describe, expect, it, vi } from 'vitest'
|
|
import {
|
|
RuntimeSessionSearchSettingsController,
|
|
type SessionSearchSettingsStore
|
|
} from './runtime-session-search-settings'
|
|
import type { GlobalSettings } from '../../shared/global-settings-types'
|
|
|
|
type Settings = ReturnType<SessionSearchSettingsStore['getSettings']>
|
|
|
|
function storeWith(aiVaultSearch: GlobalSettings['aiVaultSearch'] | undefined) {
|
|
let settings: Settings = {
|
|
workspaceDir: '/workspaces',
|
|
nestWorkspaces: false,
|
|
refreshLocalBaseRefOnWorktreeCreate: false,
|
|
branchPrefix: 'none',
|
|
branchPrefixCustom: '',
|
|
...(aiVaultSearch ? { aiVaultSearch } : {})
|
|
}
|
|
const updateSettings = vi.fn((updates: Partial<GlobalSettings>) => {
|
|
settings = { ...settings, ...updates }
|
|
})
|
|
const store: SessionSearchSettingsStore = {
|
|
getSettings: () => settings,
|
|
updateSettings
|
|
}
|
|
return { store, updateSettings, read: () => settings }
|
|
}
|
|
|
|
describe('runtime session search consent', () => {
|
|
it('writes the whole policy and hands the host before/after exactly once', async () => {
|
|
const { store, updateSettings, read } = storeWith({ enabled: false, historyDays: 30 })
|
|
const apply = vi.fn()
|
|
await new RuntimeSessionSearchSettingsController(store, apply).setEnabled(true)
|
|
|
|
expect(updateSettings).toHaveBeenCalledExactlyOnceWith(
|
|
{ aiVaultSearch: { enabled: true, historyDays: 30 } },
|
|
{ notifyListeners: true }
|
|
)
|
|
// Retention must ride along untouched; a partial write would reset it to "all history".
|
|
expect(read().aiVaultSearch).toEqual({ enabled: true, historyDays: 30 })
|
|
expect(apply).toHaveBeenCalledExactlyOnceWith(
|
|
expect.objectContaining({ aiVaultSearch: { enabled: false, historyDays: 30 } }),
|
|
expect.objectContaining({ aiVaultSearch: { enabled: true, historyDays: 30 } })
|
|
)
|
|
})
|
|
|
|
it('normalizes an absent or malformed stored policy instead of writing it back', async () => {
|
|
const { store, updateSettings } = storeWith(undefined)
|
|
await new RuntimeSessionSearchSettingsController(store, null).setEnabled(true)
|
|
|
|
expect(updateSettings).toHaveBeenCalledExactlyOnceWith(
|
|
{ aiVaultSearch: { enabled: true, historyDays: null } },
|
|
{ notifyListeners: true }
|
|
)
|
|
})
|
|
|
|
it('hands the host an unchanged pair when the value did not move', async () => {
|
|
const { store, updateSettings } = storeWith({ enabled: true, historyDays: null })
|
|
const apply = vi.fn()
|
|
await new RuntimeSessionSearchSettingsController(store, apply).setEnabled(true)
|
|
|
|
// The write still happens; the host hook is what refuses to restart a live index.
|
|
expect(updateSettings).toHaveBeenCalledOnce()
|
|
const [before, after] = apply.mock.calls[0] ?? []
|
|
expect(before?.aiVaultSearch).toEqual(after?.aiVaultSearch)
|
|
})
|
|
|
|
it('refuses on a host with no settings store rather than reporting success', async () => {
|
|
await expect(
|
|
new RuntimeSessionSearchSettingsController(null, vi.fn()).setEnabled(true)
|
|
).rejects.toThrow('runtime_unavailable')
|
|
})
|
|
})
|