mirror of
https://github.com/stablyai/orca.git
synced 2026-11-02 10:08:13 +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
162 lines
10 KiB
TypeScript
162 lines
10 KiB
TypeScript
import type { RuntimeAccountController } from './runtime-account-controller'
|
|
import type { RuntimeAiVaultCommands } from './runtime-ai-vault-commands'
|
|
import type { RuntimeBrowserDriverController } from './runtime-browser-driver-controller'
|
|
import type { RuntimeClientEventBus } from './runtime-client-event-bus'
|
|
import type { RuntimeMessageWaiters } from './runtime-message-waiters'
|
|
import type { RuntimeMobileDictationController } from './runtime-mobile-dictation-controller'
|
|
import type { RuntimeMobileNotificationController } from './runtime-mobile-notification-controller'
|
|
import type { RuntimeMobileSpeechCatalog } from './runtime-mobile-speech-catalog'
|
|
import type { RuntimeNativeChatDraftResolutions } from './runtime-native-chat-draft-resolutions'
|
|
import type { RuntimeSessionSearchSettingsController } from './runtime-session-search-settings'
|
|
import type { RuntimeSubscriptionRegistry } from './runtime-subscription-registry'
|
|
|
|
export type RuntimeServiceCommandSurface = {
|
|
listAiVaultSessions: RuntimeAiVaultCommands['list']
|
|
resolveAiVaultSessionTitles: RuntimeAiVaultCommands['resolveTitles']
|
|
prepareAiVaultSessionResume: RuntimeAiVaultCommands['prepare']
|
|
setSessionSearchEnabled: RuntimeSessionSearchSettingsController['setEnabled']
|
|
onClientEvent: RuntimeClientEventBus['on']
|
|
notifyNativeChatLaunchDraftResolved: RuntimeNativeChatDraftResolutions['notify']
|
|
registerSubscriptionCleanup: RuntimeSubscriptionRegistry['register']
|
|
registerOwnedSubscriptionCleanup: RuntimeSubscriptionRegistry['registerOwned']
|
|
cleanupSubscription: RuntimeSubscriptionRegistry['cleanup']
|
|
retrySubscriptionCleanupAfter: RuntimeSubscriptionRegistry['retryAfter']
|
|
cleanupSubscriptionAndWait: RuntimeSubscriptionRegistry['cleanupAndWait']
|
|
cleanupSubscriptionsByPrefix: RuntimeSubscriptionRegistry['cleanupByPrefix']
|
|
cleanupSubscriptionsForConnection: RuntimeSubscriptionRegistry['cleanupForConnection']
|
|
cleanupSubscriptionIfOwnedByConnection: RuntimeSubscriptionRegistry['cleanupIfOwnedByConnection']
|
|
onNotificationDispatched: RuntimeMobileNotificationController['onDispatched']
|
|
getMobileNotificationListenerCount: RuntimeMobileNotificationController['getListenerCount']
|
|
dispatchMobileNotification: RuntimeMobileNotificationController['dispatch']
|
|
getMissedNotificationsSince: RuntimeMobileNotificationController['getMissedSince']
|
|
configureNotificationDismissalStore: RuntimeMobileNotificationController['configureDismissalStore']
|
|
reconcileDismissedPushes: RuntimeMobileNotificationController['reconcileDismissedPushes']
|
|
getMobileNotificationEpoch: RuntimeMobileNotificationController['getEpoch']
|
|
dismissMobileNotification: RuntimeMobileNotificationController['dismiss']
|
|
dispatchPluginNotification: RuntimeMobileNotificationController['dispatchPlugin']
|
|
setMobilePushRegistrar: RuntimeMobileNotificationController['setPushRegistrar']
|
|
testMobilePushDevice: RuntimeMobileNotificationController['testPushDevice']
|
|
registerMobilePushDevice: RuntimeMobileNotificationController['registerPushDevice']
|
|
unregisterMobilePushDevice: RuntimeMobileNotificationController['unregisterPushDevice']
|
|
setAccountServices: RuntimeAccountController['setServices']
|
|
setCommitMessageAgentEnvironmentResolvers: RuntimeAccountController['setCommitMessageAgentEnvironment']
|
|
getCommitMessageAgentEnvironmentResolvers: RuntimeAccountController['getCommitMessageAgentEnvironment']
|
|
getAccountsSnapshot: RuntimeAccountController['getSnapshot']
|
|
refreshAccountsForMobile: RuntimeAccountController['refreshForMobile']
|
|
refreshAccountsForMobileSubscriber: RuntimeAccountController['refreshForMobileSubscriber']
|
|
selectClaudeAccount: RuntimeAccountController['selectClaude']
|
|
selectCodexAccount: RuntimeAccountController['selectCodex']
|
|
selectCodexAccountForTarget: RuntimeAccountController['selectCodexForTarget']
|
|
consumeCodexRateLimitResetCredit: RuntimeAccountController['consumeCodexResetCredit']
|
|
removeClaudeAccount: RuntimeAccountController['removeClaude']
|
|
addClaudeAccountFromConfigDir: RuntimeAccountController['addClaudeFromConfigDir']
|
|
removeCodexAccount: RuntimeAccountController['removeCodex']
|
|
addCodexAccountFromHome: RuntimeAccountController['addCodexFromHome']
|
|
onAccountsChanged: RuntimeAccountController['onChanged']
|
|
listMobileSpeechModels: RuntimeMobileSpeechCatalog['list']
|
|
downloadMobileSpeechModel: RuntimeMobileSpeechCatalog['download']
|
|
deleteMobileSpeechModel: RuntimeMobileSpeechCatalog['delete']
|
|
configureMobileDictation: RuntimeMobileSpeechCatalog['configure']
|
|
startMobileDictation: RuntimeMobileDictationController['start']
|
|
feedMobileDictation: RuntimeMobileDictationController['feed']
|
|
finishMobileDictation: RuntimeMobileDictationController['finish']
|
|
cancelMobileDictation: RuntimeMobileDictationController['cancel']
|
|
cancelMobileDictationForConnection: RuntimeMobileDictationController['cancelForConnection']
|
|
getAllBrowserDrivers: RuntimeBrowserDriverController['getAll']
|
|
reclaimBrowserForDesktop: RuntimeBrowserDriverController['reclaimForDesktop']
|
|
notifyMessageArrived(handle: string, messageType?: string): void
|
|
waitForMessage: RuntimeMessageWaiters['wait']
|
|
cancelMessageWaiters: RuntimeMessageWaiters['cancel']
|
|
}
|
|
|
|
type RuntimeServiceCommandOwners = {
|
|
aiVault: RuntimeAiVaultCommands
|
|
sessionSearchSettings: RuntimeSessionSearchSettingsController
|
|
clientEvents: RuntimeClientEventBus
|
|
nativeChatDraftResolutions: RuntimeNativeChatDraftResolutions
|
|
subscriptions: RuntimeSubscriptionRegistry
|
|
mobileNotifications: RuntimeMobileNotificationController
|
|
accounts: RuntimeAccountController
|
|
mobileSpeech: RuntimeMobileSpeechCatalog
|
|
mobileDictation: RuntimeMobileDictationController
|
|
browserDrivers: RuntimeBrowserDriverController
|
|
messageWaiters: RuntimeMessageWaiters
|
|
}
|
|
|
|
export function installRuntimeServiceCommandSurface(
|
|
target: RuntimeServiceCommandSurface,
|
|
owners: RuntimeServiceCommandOwners
|
|
): void {
|
|
const vault = owners.aiVault
|
|
const sessionSearchSettings = owners.sessionSearchSettings
|
|
const events = owners.clientEvents
|
|
const drafts = owners.nativeChatDraftResolutions
|
|
const subscriptions = owners.subscriptions
|
|
const notifications = owners.mobileNotifications
|
|
const accounts = owners.accounts
|
|
const speech = owners.mobileSpeech
|
|
const dictation = owners.mobileDictation
|
|
const browsers = owners.browserDrivers
|
|
const waiters = owners.messageWaiters
|
|
Object.assign(target, {
|
|
listAiVaultSessions: vault.list.bind(vault),
|
|
resolveAiVaultSessionTitles: vault.resolveTitles.bind(vault),
|
|
prepareAiVaultSessionResume: vault.prepare.bind(vault),
|
|
setSessionSearchEnabled: sessionSearchSettings.setEnabled.bind(sessionSearchSettings),
|
|
onClientEvent: events.on.bind(events),
|
|
notifyNativeChatLaunchDraftResolved: drafts.notify.bind(drafts),
|
|
registerSubscriptionCleanup: subscriptions.register.bind(subscriptions),
|
|
registerOwnedSubscriptionCleanup: subscriptions.registerOwned.bind(subscriptions),
|
|
cleanupSubscription: subscriptions.cleanup.bind(subscriptions),
|
|
retrySubscriptionCleanupAfter: subscriptions.retryAfter.bind(subscriptions),
|
|
cleanupSubscriptionAndWait: subscriptions.cleanupAndWait.bind(subscriptions),
|
|
cleanupSubscriptionsByPrefix: subscriptions.cleanupByPrefix.bind(subscriptions),
|
|
cleanupSubscriptionsForConnection: subscriptions.cleanupForConnection.bind(subscriptions),
|
|
cleanupSubscriptionIfOwnedByConnection:
|
|
subscriptions.cleanupIfOwnedByConnection.bind(subscriptions),
|
|
onNotificationDispatched: notifications.onDispatched.bind(notifications),
|
|
getMobileNotificationListenerCount: notifications.getListenerCount.bind(notifications),
|
|
dispatchMobileNotification: notifications.dispatch.bind(notifications),
|
|
getMissedNotificationsSince: notifications.getMissedSince.bind(notifications),
|
|
configureNotificationDismissalStore: notifications.configureDismissalStore.bind(notifications),
|
|
reconcileDismissedPushes: notifications.reconcileDismissedPushes.bind(notifications),
|
|
getMobileNotificationEpoch: notifications.getEpoch.bind(notifications),
|
|
dismissMobileNotification: notifications.dismiss.bind(notifications),
|
|
dispatchPluginNotification: notifications.dispatchPlugin.bind(notifications),
|
|
setMobilePushRegistrar: notifications.setPushRegistrar.bind(notifications),
|
|
testMobilePushDevice: notifications.testPushDevice.bind(notifications),
|
|
registerMobilePushDevice: notifications.registerPushDevice.bind(notifications),
|
|
unregisterMobilePushDevice: notifications.unregisterPushDevice.bind(notifications),
|
|
setAccountServices: accounts.setServices.bind(accounts),
|
|
setCommitMessageAgentEnvironmentResolvers:
|
|
accounts.setCommitMessageAgentEnvironment.bind(accounts),
|
|
getCommitMessageAgentEnvironmentResolvers:
|
|
accounts.getCommitMessageAgentEnvironment.bind(accounts),
|
|
getAccountsSnapshot: accounts.getSnapshot.bind(accounts),
|
|
refreshAccountsForMobile: accounts.refreshForMobile.bind(accounts),
|
|
refreshAccountsForMobileSubscriber: accounts.refreshForMobileSubscriber.bind(accounts),
|
|
selectClaudeAccount: accounts.selectClaude.bind(accounts),
|
|
selectCodexAccount: accounts.selectCodex.bind(accounts),
|
|
selectCodexAccountForTarget: accounts.selectCodexForTarget.bind(accounts),
|
|
consumeCodexRateLimitResetCredit: accounts.consumeCodexResetCredit.bind(accounts),
|
|
removeClaudeAccount: accounts.removeClaude.bind(accounts),
|
|
addClaudeAccountFromConfigDir: accounts.addClaudeFromConfigDir.bind(accounts),
|
|
removeCodexAccount: accounts.removeCodex.bind(accounts),
|
|
addCodexAccountFromHome: accounts.addCodexFromHome.bind(accounts),
|
|
onAccountsChanged: accounts.onChanged.bind(accounts),
|
|
listMobileSpeechModels: speech.list.bind(speech),
|
|
downloadMobileSpeechModel: speech.download.bind(speech),
|
|
deleteMobileSpeechModel: speech.delete.bind(speech),
|
|
configureMobileDictation: speech.configure.bind(speech),
|
|
startMobileDictation: dictation.start.bind(dictation),
|
|
feedMobileDictation: dictation.feed.bind(dictation),
|
|
finishMobileDictation: dictation.finish.bind(dictation),
|
|
cancelMobileDictation: dictation.cancel.bind(dictation),
|
|
cancelMobileDictationForConnection: dictation.cancelForConnection.bind(dictation),
|
|
getAllBrowserDrivers: browsers.getAll.bind(browsers),
|
|
reclaimBrowserForDesktop: browsers.reclaimForDesktop.bind(browsers),
|
|
waitForMessage: waiters.wait.bind(waiters),
|
|
cancelMessageWaiters: waiters.cancel.bind(waiters)
|
|
})
|
|
}
|