mirror of
https://github.com/stablyai/orca.git
synced 2026-11-03 02:27:43 +00:00
* feat(native-chat): choose which agents open as a chat Settings → Chat gets a Providers list. Each agent the host can open as a chat has a switch; off opens that agent in a terminal instead. Only choices the user makes are saved. Unchosen: Claude and Codex on, others on when their CLI is installed on this computer. The renderer's route and the host's launch-mode decision both read one rule, so the tab menu, shortcuts, CLI, orchestration and relayed phone launches agree. * fix: keep chat provider settings with the other chat settings types * test: route input carries this computer's detected agents * fix(chat): resolve provider choices from their owning computer * test(chat): use checked native inventory fixtures * Fix launch preparation lifetime and execution host startup * fix(chat): keep unchosen providers on without discovery * test(chat): type provider routing fixtures against store contracts * test(chat): align shared launch fixture with private settings reader * test(chat): give the structured-session RPC fixture the private settings reader The host now reads saved chat choices through requireStore().getSettings(); the shared RPC fixture only had getClientSettings, so the launch-mode reader saw no settings and create support refused Claude. The fixture now follows each test's client settings. * fix: preserve provider choice and cached native inventory
138 lines
6.2 KiB
TypeScript
138 lines
6.2 KiB
TypeScript
/**
|
|
* The one place that answers "should this launch be a structured native chat session?".
|
|
*
|
|
* Both launch surfaces call it. The renderer asks when a user opens an agent tab
|
|
* (`resolveAgentLaunchRoute`); orchestration asks when it dispatches a worker, because the mode is
|
|
* the user's own default rather than a per-call flag. Keeping the two halves — the settings default
|
|
* and the per-launch feasibility — here is what stops the second caller from growing a copy that
|
|
* drifts.
|
|
*/
|
|
|
|
import { isAgentSessionHandleProvider } from './agent-session-provider-handle'
|
|
import { parseExecutionHostId } from './execution-host'
|
|
import type { GlobalSettings } from './global-settings-types'
|
|
import type { NativeChatProviderSettings } from './native-chat-providers'
|
|
import type { ProjectExecutionRuntimeResolution } from './project-execution-runtime'
|
|
import {
|
|
STRUCTURED_AGENT_SESSION_CLIENT_LAUNCH_MODE_CAPABILITY,
|
|
STRUCTURED_AGENT_SESSION_REGISTERED_AGENTS_RUNTIME_CAPABILITY,
|
|
STRUCTURED_AGENT_SESSION_RUNTIME_CAPABILITY
|
|
} from './protocol-version'
|
|
import type { TuiAgent } from './tui-agent'
|
|
import type { WorkspaceLaunchKind } from './workspace-launch-kind'
|
|
|
|
export type NativeChatDefaultSettings = Pick<GlobalSettings, 'experimentalNativeChat'> &
|
|
Partial<NativeChatProviderSettings>
|
|
|
|
/** Why a launch that the user's default asked to be structured cannot be. */
|
|
export type StructuredNativeChatBlocker =
|
|
| 'reused-terminal'
|
|
| 'agent-without-structured-session'
|
|
/** The launch names a start directory outside its workspace, which only a PTY can apply. The
|
|
* configured launch command and arguments never decide the surface. */
|
|
| 'custom-start-directory'
|
|
| 'remote-execution-host'
|
|
| 'project-runtime'
|
|
| 'runtime-capability'
|
|
/** The owning host has not answered yet. Distinct from `runtime-capability`, which is the
|
|
* host saying no: an unestablished answer must not read as a refusal. */
|
|
| 'runtime-capability-unknown'
|
|
/** This client does not tell a paired host it can read and launch structured sessions. */
|
|
| 'client-capability'
|
|
|
|
export type StructuredNativeChatSupport =
|
|
| { supported: true }
|
|
| { supported: false; blocker: StructuredNativeChatBlocker }
|
|
|
|
export type StructuredNativeChatSupportInput = {
|
|
agent: TuiAgent
|
|
executionHostId: string
|
|
/** Capabilities of the host this launch would run on — a paired server's own, not this
|
|
* machine's. `null` = not yet established. */
|
|
hostCapabilities: readonly string[] | null
|
|
/** What this client advertises to a paired host; read only for `runtime:` hosts. */
|
|
clientCapabilities?: readonly string[]
|
|
/** Host-derived. Absent means the kind was never established, which is not evidence of any kind. */
|
|
workspaceKind?: WorkspaceLaunchKind
|
|
projectRuntime?: ProjectExecutionRuntimeResolution | null
|
|
startsOutsideWorkspaceRoot?: boolean
|
|
/** An existing PTY agent keeps its execution transport. */
|
|
reusesTerminal?: boolean
|
|
/** The agents the host listed through `agentSession.agents`. Absent: none learned, so only
|
|
* Claude and Codex, which every structured host runs, can be offered. */
|
|
hostStructuredAgents?: readonly string[]
|
|
}
|
|
|
|
/** The single Chat UI switch selects structured chat for supported new launches. */
|
|
export function isNativeChatEnabled(
|
|
settings: Partial<NativeChatDefaultSettings> | null | undefined
|
|
): boolean {
|
|
return settings?.experimentalNativeChat === true
|
|
}
|
|
|
|
/** Unchosen agents retain the chat default, independently of installation inventory. */
|
|
export function isNativeChatDefaultFor(
|
|
settings: Partial<NativeChatDefaultSettings> | null | undefined,
|
|
agent: TuiAgent
|
|
): boolean {
|
|
return isNativeChatEnabled(settings) && (settings?.nativeChatProviders?.[agent] ?? true)
|
|
}
|
|
|
|
function clientChoosesStructuredLaunches(capabilities: readonly string[] | undefined): boolean {
|
|
return (
|
|
capabilities?.includes(STRUCTURED_AGENT_SESSION_RUNTIME_CAPABILITY) === true &&
|
|
capabilities.includes(STRUCTURED_AGENT_SESSION_CLIENT_LAUNCH_MODE_CAPABILITY)
|
|
)
|
|
}
|
|
|
|
export function resolveStructuredNativeChatSupport(
|
|
input: StructuredNativeChatSupportInput
|
|
): StructuredNativeChatSupport {
|
|
// A paired Orca server runs structured sessions itself and answers for them below. An SSH host
|
|
// has no Orca runtime of its own to run one.
|
|
const host = parseExecutionHostId(input.executionHostId)
|
|
if (host?.kind !== 'local' && host?.kind !== 'runtime') {
|
|
return { supported: false, blocker: 'remote-execution-host' }
|
|
}
|
|
if (input.reusesTerminal === true) {
|
|
return { supported: false, blocker: 'reused-terminal' }
|
|
}
|
|
const builtInAgent = isAgentSessionHandleProvider(input.agent)
|
|
if (!builtInAgent && !input.hostStructuredAgents?.includes(input.agent)) {
|
|
return { supported: false, blocker: 'agent-without-structured-session' }
|
|
}
|
|
if (input.startsOutsideWorkspaceRoot === true) {
|
|
return { supported: false, blocker: 'custom-start-directory' }
|
|
}
|
|
const projectRuntime = input.projectRuntime
|
|
if (projectRuntime?.status === 'repair-required' || projectRuntime?.runtime.kind === 'wsl') {
|
|
return { supported: false, blocker: 'project-runtime' }
|
|
}
|
|
if (input.hostCapabilities === null) {
|
|
return { supported: false, blocker: 'runtime-capability-unknown' }
|
|
}
|
|
if (
|
|
!input.hostCapabilities.includes(STRUCTURED_AGENT_SESSION_RUNTIME_CAPABILITY) ||
|
|
// A host that does not advertise its registered agents accepts only Claude and Codex.
|
|
(!builtInAgent &&
|
|
!input.hostCapabilities.includes(
|
|
STRUCTURED_AGENT_SESSION_REGISTERED_AGENTS_RUNTIME_CAPABILITY
|
|
))
|
|
) {
|
|
return { supported: false, blocker: 'runtime-capability' }
|
|
}
|
|
if (host.kind === 'runtime') {
|
|
// An older paired host advertises structured sessions but admits them only with its own chat
|
|
// setting on, so a chat opened there could never start; it keeps the terminal it always got.
|
|
if (!input.hostCapabilities.includes(STRUCTURED_AGENT_SESSION_CLIENT_LAUNCH_MODE_CAPABILITY)) {
|
|
return { supported: false, blocker: 'runtime-capability' }
|
|
}
|
|
// The host refuses a client that did not say it reads structured sessions, as the browser
|
|
// client does not; that client keeps the host terminal.
|
|
if (!clientChoosesStructuredLaunches(input.clientCapabilities)) {
|
|
return { supported: false, blocker: 'client-capability' }
|
|
}
|
|
}
|
|
return { supported: true }
|
|
}
|