mirror of
https://github.com/stablyai/orca.git
synced 2026-11-02 10:08:13 +00:00
* feat(accounts): add Claude and Codex accounts to a remote server from a paired client The server runs the login (Claude's pasted-code sign-in, Codex's device code) and keeps the credentials; the client only opens the page and relays the code. New capability-negotiated accounts.hostSignIn* methods; older servers degrade to an update message. Fixes #20009 * fix(accounts): settle host sign-ins by the login's outcome, pin pairing, guard stdin Review fixes: a cancel the login refused still reports the added account; the client pins each flow to its pairing revision; Claude's stdin EPIPE no longer crashes the host; both sides accept only the provider's sign-in host; pasted codes reject whitespace; a host-side supersede says so; Codex cancels its own login handle; opt-in real-CLI test. * fix(accounts): keep an early Cancel, retry polls through the host's window, scan Claude stdout only A Cancel clicked before the host answers the start is applied once it does; the client keeps polling through a short outage until the host would have ended the flow; Claude's link is read from stdout only; a timeout after the code was sent keeps its own message. * test(accounts): name the stdin sender and allow its stream write in the persistence ratchet * fix(accounts): register each Add Account attempt before its first await One attempt per click owns the pairing pin, the cancel flag and the host's flow, so a Cancel during the capability check or the start is never lost; polls retry only lost answers; the stdout forwarder is removed on cleanup. * refactor(accounts): drive the client sign-in as one explicit state machine per server and provider checking -> starting -> following -> settled, with Cancel as a flag the run loop delivers once the host names its flow and re-asks after any answer until the host takes it. A second Add joins the running attempt; a Cancel the server could not be told about is surfaced; retry policy tested. * fix(accounts): retry a lost start answer, and report a cancelled sign-in as cancelled however it ends * fix(accounts): Cancel reaches the server a sign-in runs on, and a typed code stays with its flow The host picker can move while a sign-in runs: Cancel now targets the running server sign-in (or the desktop's own login when none runs), and the pasted-code form is keyed by server and flow. * fix(accounts): run at most one server sign-in per provider, so Cancel has one meaning * fix(accounts): the server sign-in prompt owns its Cancel Cancel for a server sign-in now lives in the prompt bound to that server, which also reappears with its Cancel when Settings reopens mid-sign-in; the header Cancel is only for this desktop's own login. * fix(accounts): hide the desktop-login Cancel while a server sign-in runs; narrow the prompt's status * fix(accounts): keep the desktop-login Cancel in local scope even while a server sign-in runs * fix(accounts): the header Cancel follows the running addition, wherever Settings now points Each Add Account records where it is signing in. Cancel stops that sign-in, on a server or this desktop, and is hidden only where that server's own prompt already offers Cancel.
201 lines
13 KiB
TypeScript
201 lines
13 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']
|
|
getSubscriptionRegistrationVersion: RuntimeSubscriptionRegistry['getRegistrationVersion']
|
|
releaseSubscriptionByRequest: RuntimeSubscriptionRegistry['releaseByRequest']
|
|
onNotificationDispatched: RuntimeMobileNotificationController['onDispatched']
|
|
getMobileNotificationListenerCount: RuntimeMobileNotificationController['getListenerCount']
|
|
dispatchMobileNotification: RuntimeMobileNotificationController['dispatch']
|
|
getMissedNotificationsSince: RuntimeMobileNotificationController['getMissedSince']
|
|
configureNotificationDismissalStore: RuntimeMobileNotificationController['configureDismissalStore']
|
|
closeNotificationDismissals: RuntimeMobileNotificationController['closeDismissals']
|
|
reconcileDismissedPushes: RuntimeMobileNotificationController['reconcileDismissedPushes']
|
|
getMobileNotificationEpoch: RuntimeMobileNotificationController['getEpoch']
|
|
dismissMobileNotification: RuntimeMobileNotificationController['dismiss']
|
|
retireStructuredAttention: RuntimeMobileNotificationController['retireStructuredAttention']
|
|
reconcileStructuredPromptAttention: RuntimeMobileNotificationController['reconcileStructuredPromptAttention']
|
|
dispatchPluginNotification: RuntimeMobileNotificationController['dispatchPlugin']
|
|
setMobilePushRegistrar: RuntimeMobileNotificationController['setPushRegistrar']
|
|
testMobilePushDevice: RuntimeMobileNotificationController['testPushDevice']
|
|
registerMobilePushDevice: RuntimeMobileNotificationController['registerPushDevice']
|
|
unregisterMobilePushDevice: RuntimeMobileNotificationController['unregisterPushDevice']
|
|
setAccountServices: RuntimeAccountController['setServices']
|
|
getDataAccountsSnapshot: RuntimeAccountController['dataAccountsSnapshot']
|
|
setCommitMessageAgentEnvironmentResolvers: RuntimeAccountController['setCommitMessageAgentEnvironment']
|
|
getCommitMessageAgentEnvironmentResolvers: RuntimeAccountController['getCommitMessageAgentEnvironment']
|
|
getAccountsSnapshot: RuntimeAccountController['getSnapshot']
|
|
getUsageRateLimits: RuntimeAccountController['usageRateLimits']
|
|
requireUsageRateLimits: RuntimeAccountController['requireUsageRateLimits']
|
|
refreshAccountsForMobile: RuntimeAccountController['refreshForMobile']
|
|
refreshAccountsForMobileSubscriber: RuntimeAccountController['refreshForMobileSubscriber']
|
|
selectClaudeAccount: RuntimeAccountController['selectClaude']
|
|
selectCodexAccount: RuntimeAccountController['selectCodex']
|
|
selectCodexAccountForTarget: RuntimeAccountController['selectCodexForTarget']
|
|
consumeCodexRateLimitResetCredit: RuntimeAccountController['consumeCodexResetCredit']
|
|
removeClaudeAccount: RuntimeAccountController['removeClaude']
|
|
beginClaudeSignIn: RuntimeAccountController['beginClaudeSignIn']
|
|
finishClaudeSignIn: RuntimeAccountController['finishClaudeSignIn']
|
|
cancelClaudeSignIn: RuntimeAccountController['cancelClaudeSignIn']
|
|
runHostAccountSignIn: RuntimeAccountController['runHostSignIn']
|
|
removeCodexAccount: RuntimeAccountController['removeCodex']
|
|
addCodexAccountFromHome: RuntimeAccountController['addCodexFromHome']
|
|
addDataAccountFromHome: RuntimeAccountController['addDataFromHome']
|
|
selectDataAccount: RuntimeAccountController['selectData']
|
|
removeDataAccount: RuntimeAccountController['removeData']
|
|
onAccountsChanged: RuntimeAccountController['onChanged']
|
|
setHostAccountLaunchEnv: RuntimeAccountController['setHostLaunchEnv']
|
|
restoreHostAccountLaunchEnv: RuntimeAccountController['restoreHostLaunchEnv']
|
|
listMobileSpeechModels: RuntimeMobileSpeechCatalog['list']
|
|
downloadMobileSpeechModel: RuntimeMobileSpeechCatalog['download']
|
|
deleteMobileSpeechModel: RuntimeMobileSpeechCatalog['delete']
|
|
configureMobileDictation: RuntimeMobileSpeechCatalog['configure']
|
|
getOpenAiSpeechKeyStatus: RuntimeMobileSpeechCatalog['openAiKeyStatus']
|
|
setOpenAiSpeechKey: RuntimeMobileSpeechCatalog['setOpenAiKey']
|
|
clearOpenAiSpeechKey: RuntimeMobileSpeechCatalog['clearOpenAiKey']
|
|
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),
|
|
getSubscriptionRegistrationVersion: subscriptions.getRegistrationVersion.bind(subscriptions),
|
|
releaseSubscriptionByRequest: subscriptions.releaseByRequest.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),
|
|
closeNotificationDismissals: notifications.closeDismissals.bind(notifications),
|
|
reconcileDismissedPushes: notifications.reconcileDismissedPushes.bind(notifications),
|
|
getMobileNotificationEpoch: notifications.getEpoch.bind(notifications),
|
|
dismissMobileNotification: notifications.dismiss.bind(notifications),
|
|
retireStructuredAttention: notifications.retireStructuredAttention.bind(notifications),
|
|
reconcileStructuredPromptAttention:
|
|
notifications.reconcileStructuredPromptAttention.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),
|
|
getDataAccountsSnapshot: accounts.dataAccountsSnapshot.bind(accounts),
|
|
setCommitMessageAgentEnvironmentResolvers:
|
|
accounts.setCommitMessageAgentEnvironment.bind(accounts),
|
|
getCommitMessageAgentEnvironmentResolvers:
|
|
accounts.getCommitMessageAgentEnvironment.bind(accounts),
|
|
getAccountsSnapshot: accounts.getSnapshot.bind(accounts),
|
|
getUsageRateLimits: accounts.usageRateLimits.bind(accounts),
|
|
requireUsageRateLimits: accounts.requireUsageRateLimits.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),
|
|
beginClaudeSignIn: accounts.beginClaudeSignIn.bind(accounts),
|
|
finishClaudeSignIn: accounts.finishClaudeSignIn.bind(accounts),
|
|
cancelClaudeSignIn: accounts.cancelClaudeSignIn.bind(accounts),
|
|
runHostAccountSignIn: accounts.runHostSignIn.bind(accounts),
|
|
removeCodexAccount: accounts.removeCodex.bind(accounts),
|
|
addCodexAccountFromHome: accounts.addCodexFromHome.bind(accounts),
|
|
addDataAccountFromHome: accounts.addDataFromHome.bind(accounts),
|
|
selectDataAccount: accounts.selectData.bind(accounts),
|
|
removeDataAccount: accounts.removeData.bind(accounts),
|
|
onAccountsChanged: accounts.onChanged.bind(accounts),
|
|
setHostAccountLaunchEnv: accounts.setHostLaunchEnv.bind(accounts),
|
|
restoreHostAccountLaunchEnv: accounts.restoreHostLaunchEnv.bind(accounts),
|
|
listMobileSpeechModels: speech.list.bind(speech),
|
|
downloadMobileSpeechModel: speech.download.bind(speech),
|
|
deleteMobileSpeechModel: speech.delete.bind(speech),
|
|
configureMobileDictation: speech.configure.bind(speech),
|
|
getOpenAiSpeechKeyStatus: speech.openAiKeyStatus.bind(speech),
|
|
setOpenAiSpeechKey: speech.setOpenAiKey.bind(speech),
|
|
clearOpenAiSpeechKey: speech.clearOpenAiKey.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)
|
|
})
|
|
}
|