mirror of
https://github.com/stablyai/orca.git
synced 2026-10-31 09:07:40 +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
* feat(native-chat): the + menu shows chat, disabled chat and CLI rows
With Chat UI on, each agent whose chat provider is on gets a chat row, disabled
with the reason where the workspace's machine can't run that chat (not
installed, SSH, WSL, not on this host). Every other installed agent gets a row
marked (CLI) that opens its terminal. With Chat UI off, or on an older server
that opens no chats, the menu is unchanged.
* fix: new tab menu rows use a launch variant instead of restyling the menu item
* fix: name every route blocker the menu rows map
* fix: menu row subscriptions render on the server too
* feat(native-chat): open a chat agent's CLI from beside its + menu row
A chat row whose CLI is installed on the workspace's machine gets a › beside
it. Its side menu offers '<Agent> (CLI)', which opens that agent's terminal for
this launch only; nothing is saved and the row itself still opens a chat.
* fix: keep new tab agent surfaces authoritative
* fix: handle unknown host support reasons
* fix(chat): resolve provider choices from their owning computer
* fix: include chat launch feedback translations
* test(chat): use checked native inventory fixtures
* chore: retrigger PR checks
* test: pin pending chat row behavior
* chore: retrigger PR checks after cancellation
* Fix launch preparation lifetime and execution host startup
* fix: keep agent menu choices bound to their launch
* Simplify agent menu support reads and preserve explicit surfaces
* Keep focused menu behavior coverage within its budget
* Keep menu host fixtures typed as execution hosts
* Assert notes payload on the current launch action
* Combine agent session launch type imports
* fix(chat): keep unchosen providers on without discovery
* fix(chat): use workspace installation for menu defaults
* test(chat): type provider routing fixtures against store contracts
* fix(tab-bar): keep + menu chat rows clickable while the host answers
Each menu open discarded the host's answers, so every open showed the chat
rows disabled with 'Checking chat support' until the host replied. Answers
now outlive the menu and refresh in the background, rows use this client's
facts until the host answers, and a slow host no longer reads as a refusal.
* test(chat): align shared launch fixture with private settings reader
* fix(tab-bar): keep known + menu refusals and never treat a silent host as one
Closing the menu dropped the workspace's agent list, which changed which agents
the menu asks about and threw away the host's earlier answers, so a reopen showed
a refused chat as clickable. A support read that could not answer was stored as
"No chat on this host", disabling chats the client knows are supported.
The menu now keeps the stored agent list while closed (only fetching waits for
it to open) and only a real host answer relabels a row. The workspace's
detection target is resolved where main already resolves it (QuickLaunchButton
and the tab-bar runtime model) and passed in, so no new focus read is added.
Tab-bar test mocks now stub the QuickLaunchButton exports the menu imports.
* fix: sync runtime English catalog for agent menu
* 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.
* test(chat): pin create support's options argument and kept decline reason
* fix(native-chat): + menu chat rows wait disabled for the host and share its answer across tab groups
A chat the host refuses showed clickable for up to the 10s host read, then flipped to disabled,
once per tab group. Rows now stay quietly disabled until their host answers (or the read times
out), and every tab group on the same workspace host reads the same answers.
* fix(native-chat): + menu rows also wait before the host's chat list loads, and on every reopen of a host that could not answer
Asking the host no longer waits for its chat list, so Claude and Codex no longer show clickable,
then disabled, while it loads. A read that times out releases rows for that open only, so the
next open waits again instead of flipping on a late refusal. Agents not installed skip the read.
* fix(native-chat): typed + menu search puts a launchable row ahead of a disabled chat
In a WSL workspace, typing an agent's name and pressing Enter highlighted its disabled chat row,
so nothing launched where main opened the CLI. Search now ranks rows that can launch first.
* fix: preserve provider choice and cached native inventory
* feat(native-chat): hosts publish each workspace's chat availability as one stream
A new negotiated stream, agentSession.subscribeAvailability, advertised as
agent-session.structured.availability.v1. The host keeps one in-memory resource per
workspace, shared by every reader, that asks the existing createSupport check for every
registered agent in that workspace and publishes only complete generations. It answers
again when a launch setting changes, after an explicit agent refresh, and (for answers
that could not be checked) when a workspace appears; the last reader leaving retires it.
A check that could not answer is unknown, never a refusal. agentSession.agents stays
static and unchanged.
* feat(native-chat): + menus read a publishing host's workspace availability
The renderer holds the workspace's availability stream while its tab bar is mounted
(not while a menu is open), shared by every tab group, with runtime and contact fences.
It exposes a readiness model, { ready: false } | { ready: true; byAgent }, from
useHostStructuredAgentAvailability and the menu hook. On a host advertising the
capability the menu reads its answers from that model and makes no per-agent
createSupport reads; an older host keeps the per-agent reads, answer cache and deadline
unchanged. Row building and presentation are unchanged.
* fix(native-chat): workspace availability checks only what the shown workspace needs
Only the shown worktree (or an open menu) holds its host's availability stream, and only with Chat
UI on. Display checks no longer force a fresh login shell for an agent that isn't installed, so
Claude and Codex answer without waiting on it. A change to the Windows runtime default or a project's
WSL choice re-checks, and a workspace still on its first check answers again when one appears.
* fix: keep main's bytes for the create-support file the merge reformatted
The pre-commit formatter rewrapped main's unformatted lines during the merge and pushed the file past its line limit.
* test: type the + menu rows hook mocks without assertions
* test: match the availability stream's types in three tests
* test(cross-version): declare the workspace availability stream in the agent-session surface
* feat(native-chat): + menu holds a placeholder until its agents are known, and learns them while the workspace is shown
* feat(native-chat): + menu reads one agent health snapshot per machine
The + menu no longer checks chat availability per workspace. Each Orca
runtime keeps one agent health snapshot: every agent's location rule on
this machine plus a typed observation of its installed release, probed
in the server's own home. It is taken on first use and replaced only on
a launch setting change, an explicit agent Refresh (after a fresh shell
capture), or a restart, and kept without readers.
- The version probe reports supported, unsupported or inconclusive; a
launch still refuses an inconclusive run, while the snapshot reads it
as unknown rather than a refusal.
- agentSession.subscribeAvailability takes no params and publishes that
snapshot; a replay that closes the transport releases its reader.
- The renderer keeps the snapshot per host, runtime and pairing, through
lost contact and across workspace switches on the same host.
- The menu hook drops the per-open createSupport display reads, their
deadline and answer cache; older hosts show client facts and the
click's admission, unchanged, states any refusal.
* fix: a failed shell recapture still publishes the agent health snapshot
* fix(native-chat): capture fresh shell facts for every health snapshot
* fix(tab-menu): share settled agent actions across menu surfaces
* fix(tab-menu): preserve loading row line height
* feat(native-chat): the whole chat row opens its CLI submenu
Hovering a chat row in the + menu, or pressing ArrowRight on it, now opens
the "<Agent> (CLI)" submenu to the right instead of needing a separate
chevron button. Clicking the row, or Enter/Space, still starts the chat
and closes the menu; hosts pass closeMenu since a submenu trigger never
closes the menu on its own.
* fix(tab-menu): line the CLI submenu up with its chat row
* feat(native-chat): a chat row's terminal button opens its CLI
Clicking a chat row in the + menu starts a chat. Hovering the row (or
pressing ArrowRight on it) reveals a terminal button at its right edge that
opens the agent in a terminal instead. This replaces the submenu, whose
chevron made the row look like it only opened a submenu. The button's click
bubbles into the row, so the menu closes as for any item and the hosts no
longer need a closeMenu callback.
* fix(native-chat): the chat row's terminal button handles drags, arrows and screen readers
A press on + dragged onto a chat row's terminal button and released there
now opens the CLI: the row reads whether the pointer or focus is on its
button instead of the click target, which a drag-release never sets.
Arrow, Home and End keys pressed on the focused button move between rows
as they do on the row. The row is announced as "Start a <Agent> chat"
instead of including the button's label.
* fix(native-chat): chat rows read "Start a chat with <Agent>"
"Start a OpenCode chat" was ungrammatical for agents whose name starts with a vowel.
91 lines
3.3 KiB
TypeScript
91 lines
3.3 KiB
TypeScript
import {
|
|
hasReachedAppVersion,
|
|
isPrereleaseAppVersion,
|
|
parseCliVersion
|
|
} from '../shared/app-version'
|
|
import { runProcess } from '@orca/process-host'
|
|
import type { ProcessSpec } from '@orca/process-host/process-spec'
|
|
|
|
/** One line naming why a probe said no; the binary's own words, bounded. */
|
|
function warnRefused(program: string, why: string, output = ''): void {
|
|
const said = output.trim().replace(/\s+/g, ' ').slice(0, 200)
|
|
console.warn(`[agent-cli-version] ${program} --version: ${why}${said ? `: ${said}` : ''}`)
|
|
}
|
|
|
|
/** What one `--version` run showed; `inconclusive` is no verdict on the installed release. */
|
|
export type AgentCliVersionObservation = 'supported' | 'unsupported' | 'inconclusive'
|
|
|
|
/** Runs `<program> --version` on this host and asks `supports` about the version it prints.
|
|
* Inconclusive when it cannot tell: a failed spawn, a non-zero exit, a timeout, or no version. */
|
|
export async function observeAgentCliVersion(
|
|
input: Pick<ProcessSpec, 'program' | 'cwd' | 'env'>,
|
|
supports: (version: string) => boolean
|
|
): Promise<AgentCliVersionObservation> {
|
|
const result = await readAgentCliVersion(input)
|
|
if (result.version === null) {
|
|
return 'inconclusive'
|
|
}
|
|
if (!supports(result.version)) {
|
|
warnRefused(input.program, `${result.version} is not a supported release`)
|
|
return 'unsupported'
|
|
}
|
|
console.info(`[agent-cli-version] ${input.program} --version: ${result.version} is supported`)
|
|
return 'supported'
|
|
}
|
|
|
|
/** A launch's projection: only an observed supported release runs; an inconclusive run refuses. */
|
|
export async function probeAgentCliVersion(
|
|
input: Pick<ProcessSpec, 'program' | 'cwd' | 'env'>,
|
|
supports: (version: string) => boolean
|
|
): Promise<boolean> {
|
|
return (await observeAgentCliVersion(input, supports)) === 'supported'
|
|
}
|
|
|
|
export async function readAgentCliVersion(
|
|
input: Pick<ProcessSpec, 'program' | 'cwd' | 'env'>,
|
|
parseVersion: typeof parseCliVersion = parseCliVersion
|
|
): Promise<{ version: string | null }> {
|
|
let result
|
|
try {
|
|
result = await runProcess({
|
|
...input,
|
|
args: ['--version'],
|
|
timeoutMs: 5_000,
|
|
maxOutputBytes: 4_096,
|
|
killOnOutputLimit: true
|
|
})
|
|
} catch (error) {
|
|
warnRefused(input.program, `did not start (${String(error)})`)
|
|
return { version: null }
|
|
}
|
|
const output = `${result.stdout}\n${result.stderr}`
|
|
if (result.timedOut || result.outputTruncated || result.code !== 0) {
|
|
const why = result.timedOut
|
|
? 'timed out'
|
|
: result.outputTruncated
|
|
? 'printed too much'
|
|
: `exited ${result.code ?? result.signal}`
|
|
warnRefused(input.program, why, output)
|
|
return { version: null }
|
|
}
|
|
const version = parseVersion(parseVersion === parseCliVersion ? result.stdout : output)
|
|
if (version === null) {
|
|
warnRefused(input.program, 'printed no version', output)
|
|
return { version: null }
|
|
}
|
|
return { version }
|
|
}
|
|
|
|
/** A stable release at or after `floor`, on any later major line too. */
|
|
export function isStableCliVersionFrom(version: string, floor: string): boolean {
|
|
return !isPrereleaseAppVersion(version) && hasReachedAppVersion(version, floor)
|
|
}
|
|
|
|
/** A stable release on `major`'s line, at or after `floor`. */
|
|
export function isStableCliVersionOnLine(
|
|
version: string,
|
|
line: { major: number; floor: string }
|
|
): boolean {
|
|
return version.startsWith(`${line.major}.`) && isStableCliVersionFrom(version, line.floor)
|
|
}
|